1
0
Fork 0
wuttaweb/tests/views/test_master.py
Lance Edgar 8669ca2283 feat: add "progress" page for executing upgrades
show scrolling stdout from subprocess

nb. this does *not* show stderr, although that is captured
2024-08-25 15:52:29 -05:00

1469 lines
61 KiB
Python

# -*- coding: utf-8; -*-
import datetime
import decimal
import functools
from unittest import TestCase
from unittest.mock import MagicMock, patch
from sqlalchemy import orm
from pyramid import testing
from pyramid.response import Response
from pyramid.httpexceptions import HTTPNotFound
from wuttjamaican.conf import WuttaConfig
from wuttaweb.views import master as mod
from wuttaweb.views import View
from wuttaweb.progress import SessionProgress
from wuttaweb.subscribers import new_request_set_user
from tests.util import WebTestCase
class TestMasterView(WebTestCase):
def make_view(self):
return mod.MasterView(self.request)
def test_defaults(self):
with patch.multiple(mod.MasterView, create=True,
model_name='Widget',
model_key='uuid',
deletable_bulk=True,
has_autocomplete=True,
downloadable=True,
executable=True,
configurable=True):
mod.MasterView.defaults(self.pyramid_config)
##############################
# class methods
##############################
def test_get_model_class(self):
# no model class by default
self.assertIsNone(mod.MasterView.get_model_class())
# subclass may specify
MyModel = MagicMock()
with patch.multiple(mod.MasterView, create=True,
model_class=MyModel):
self.assertIs(mod.MasterView.get_model_class(), MyModel)
def test_get_model_name(self):
# error by default (since no model class)
self.assertRaises(AttributeError, mod.MasterView.get_model_name)
# subclass may specify model name
with patch.multiple(mod.MasterView, create=True,
model_name='Widget'):
self.assertEqual(mod.MasterView.get_model_name(), 'Widget')
# or it may specify model class
MyModel = MagicMock(__name__='Blaster')
with patch.multiple(mod.MasterView, create=True,
model_class=MyModel):
self.assertEqual(mod.MasterView.get_model_name(), 'Blaster')
def test_get_model_name_normalized(self):
# error by default (since no model class)
self.assertRaises(AttributeError, mod.MasterView.get_model_name_normalized)
# subclass may specify *normalized* model name
with patch.multiple(mod.MasterView, create=True,
model_name_normalized='widget'):
self.assertEqual(mod.MasterView.get_model_name_normalized(), 'widget')
# or it may specify *standard* model name
with patch.multiple(mod.MasterView, create=True,
model_name='Blaster'):
self.assertEqual(mod.MasterView.get_model_name_normalized(), 'blaster')
# or it may specify model class
MyModel = MagicMock(__name__='Dinosaur')
with patch.multiple(mod.MasterView, create=True,
model_class=MyModel):
self.assertEqual(mod.MasterView.get_model_name_normalized(), 'dinosaur')
def test_get_model_title(self):
# error by default (since no model class)
self.assertRaises(AttributeError, mod.MasterView.get_model_title)
# subclass may specify model title
with patch.multiple(mod.MasterView, create=True,
model_title='Wutta Widget'):
self.assertEqual(mod.MasterView.get_model_title(), "Wutta Widget")
# or it may specify model name
with patch.multiple(mod.MasterView, create=True,
model_name='Blaster'):
self.assertEqual(mod.MasterView.get_model_title(), "Blaster")
# or it may specify model class
MyModel = MagicMock(__name__='Dinosaur')
with patch.multiple(mod.MasterView, create=True,
model_class=MyModel):
self.assertEqual(mod.MasterView.get_model_title(), "Dinosaur")
def test_get_model_title_plural(self):
# error by default (since no model class)
self.assertRaises(AttributeError, mod.MasterView.get_model_title_plural)
# subclass may specify *plural* model title
with patch.multiple(mod.MasterView, create=True,
model_title_plural='People'):
self.assertEqual(mod.MasterView.get_model_title_plural(), "People")
# or it may specify *singular* model title
with patch.multiple(mod.MasterView, create=True,
model_title='Wutta Widget'):
self.assertEqual(mod.MasterView.get_model_title_plural(), "Wutta Widgets")
# or it may specify model name
with patch.multiple(mod.MasterView, create=True,
model_name='Blaster'):
self.assertEqual(mod.MasterView.get_model_title_plural(), "Blasters")
# or it may specify model class
MyModel = MagicMock(__name__='Dinosaur')
with patch.multiple(mod.MasterView, create=True,
model_class=MyModel):
self.assertEqual(mod.MasterView.get_model_title_plural(), "Dinosaurs")
def test_get_model_key(self):
# error by default (since no model class)
self.assertRaises(AttributeError, mod.MasterView.get_model_key)
# subclass may specify model key
with patch.multiple(mod.MasterView, create=True,
model_key='uuid'):
self.assertEqual(mod.MasterView.get_model_key(), ('uuid',))
def test_get_route_prefix(self):
# error by default (since no model class)
self.assertRaises(AttributeError, mod.MasterView.get_route_prefix)
# subclass may specify route prefix
with patch.multiple(mod.MasterView, create=True,
route_prefix='widgets'):
self.assertEqual(mod.MasterView.get_route_prefix(), 'widgets')
# subclass may specify *normalized* model name
with patch.multiple(mod.MasterView, create=True,
model_name_normalized='blaster'):
self.assertEqual(mod.MasterView.get_route_prefix(), 'blasters')
# or it may specify *standard* model name
with patch.multiple(mod.MasterView, create=True,
model_name = 'Dinosaur'):
self.assertEqual(mod.MasterView.get_route_prefix(), 'dinosaurs')
# or it may specify model class
MyModel = MagicMock(__name__='Truck')
with patch.multiple(mod.MasterView, create=True,
model_class=MyModel):
self.assertEqual(mod.MasterView.get_route_prefix(), 'trucks')
def test_get_permission_prefix(self):
# error by default (since no model class)
self.assertRaises(AttributeError, mod.MasterView.get_permission_prefix)
# subclass may specify permission prefix
with patch.object(mod.MasterView, 'permission_prefix', new='widgets', create=True):
self.assertEqual(mod.MasterView.get_permission_prefix(), 'widgets')
# subclass may specify route prefix
with patch.object(mod.MasterView, 'route_prefix', new='widgets', create=True):
self.assertEqual(mod.MasterView.get_permission_prefix(), 'widgets')
# or it may specify model class
Truck = MagicMock(__name__='Truck')
with patch.object(mod.MasterView, 'model_class', new=Truck, create=True):
self.assertEqual(mod.MasterView.get_permission_prefix(), 'trucks')
def test_get_url_prefix(self):
# error by default (since no model class)
self.assertRaises(AttributeError, mod.MasterView.get_url_prefix)
# subclass may specify url prefix
with patch.multiple(mod.MasterView, create=True,
url_prefix='/widgets'):
self.assertEqual(mod.MasterView.get_url_prefix(), '/widgets')
# or it may specify route prefix
with patch.multiple(mod.MasterView, create=True,
route_prefix='trucks'):
self.assertEqual(mod.MasterView.get_url_prefix(), '/trucks')
# or it may specify *normalized* model name
with patch.multiple(mod.MasterView, create=True,
model_name_normalized='blaster'):
self.assertEqual(mod.MasterView.get_url_prefix(), '/blasters')
# or it may specify *standard* model name
with patch.multiple(mod.MasterView, create=True,
model_name='Dinosaur'):
self.assertEqual(mod.MasterView.get_url_prefix(), '/dinosaurs')
# or it may specify model class
MyModel = MagicMock(__name__='Machine')
with patch.multiple(mod.MasterView, create=True,
model_class=MyModel):
self.assertEqual(mod.MasterView.get_url_prefix(), '/machines')
def test_get_instance_url_prefix(self):
# error by default (since no model class)
self.assertRaises(AttributeError, mod.MasterView.get_instance_url_prefix)
# typical example with url_prefix and simple key
with patch.multiple(mod.MasterView, create=True,
url_prefix='/widgets',
model_key='uuid'):
self.assertEqual(mod.MasterView.get_instance_url_prefix(), '/widgets/{uuid}')
# typical example with composite key
with patch.multiple(mod.MasterView, create=True,
url_prefix='/widgets',
model_key=('foo', 'bar')):
self.assertEqual(mod.MasterView.get_instance_url_prefix(), '/widgets/{foo}|{bar}')
def test_get_template_prefix(self):
# error by default (since no model class)
self.assertRaises(AttributeError, mod.MasterView.get_template_prefix)
# subclass may specify template prefix
with patch.multiple(mod.MasterView, create=True,
template_prefix='/widgets'):
self.assertEqual(mod.MasterView.get_template_prefix(), '/widgets')
# or it may specify url prefix
with patch.multiple(mod.MasterView, create=True,
url_prefix='/trees'):
self.assertEqual(mod.MasterView.get_template_prefix(), '/trees')
# or it may specify route prefix
with patch.multiple(mod.MasterView, create=True,
route_prefix='trucks'):
self.assertEqual(mod.MasterView.get_template_prefix(), '/trucks')
# or it may specify *normalized* model name
with patch.multiple(mod.MasterView, create=True,
model_name_normalized='blaster'):
self.assertEqual(mod.MasterView.get_template_prefix(), '/blasters')
# or it may specify *standard* model name
with patch.multiple(mod.MasterView, create=True,
model_name='Dinosaur'):
self.assertEqual(mod.MasterView.get_template_prefix(), '/dinosaurs')
# or it may specify model class
MyModel = MagicMock(__name__='Machine')
with patch.multiple(mod.MasterView, create=True,
model_class=MyModel):
self.assertEqual(mod.MasterView.get_template_prefix(), '/machines')
def test_get_grid_key(self):
# error by default (since no model class)
self.assertRaises(AttributeError, mod.MasterView.get_grid_key)
# subclass may specify grid key
with patch.multiple(mod.MasterView, create=True,
grid_key='widgets'):
self.assertEqual(mod.MasterView.get_grid_key(), 'widgets')
# or it may specify route prefix
with patch.multiple(mod.MasterView, create=True,
route_prefix='trucks'):
self.assertEqual(mod.MasterView.get_grid_key(), 'trucks')
# or it may specify *normalized* model name
with patch.multiple(mod.MasterView, create=True,
model_name_normalized='blaster'):
self.assertEqual(mod.MasterView.get_grid_key(), 'blasters')
# or it may specify *standard* model name
with patch.multiple(mod.MasterView, create=True,
model_name='Dinosaur'):
self.assertEqual(mod.MasterView.get_grid_key(), 'dinosaurs')
# or it may specify model class
MyModel = MagicMock(__name__='Machine')
with patch.multiple(mod.MasterView, create=True,
model_class=MyModel):
self.assertEqual(mod.MasterView.get_grid_key(), 'machines')
def test_get_config_title(self):
# error by default (since no model class)
self.assertRaises(AttributeError, mod.MasterView.get_config_title)
# subclass may specify config title
with patch.multiple(mod.MasterView, create=True,
config_title='Widgets'):
self.assertEqual(mod.MasterView.get_config_title(), "Widgets")
# subclass may specify *plural* model title
with patch.multiple(mod.MasterView, create=True,
model_title_plural='People'):
self.assertEqual(mod.MasterView.get_config_title(), "People")
# or it may specify *singular* model title
with patch.multiple(mod.MasterView, create=True,
model_title='Wutta Widget'):
self.assertEqual(mod.MasterView.get_config_title(), "Wutta Widgets")
# or it may specify model name
with patch.multiple(mod.MasterView, create=True,
model_name='Blaster'):
self.assertEqual(mod.MasterView.get_config_title(), "Blasters")
# or it may specify model class
MyModel = MagicMock(__name__='Dinosaur')
with patch.multiple(mod.MasterView, create=True,
model_class=MyModel):
self.assertEqual(mod.MasterView.get_config_title(), "Dinosaurs")
##############################
# support methods
##############################
def test_get_class_hierarchy(self):
class MyView(mod.MasterView):
pass
view = MyView(self.request)
classes = view.get_class_hierarchy()
self.assertEqual(classes, [View, mod.MasterView, MyView])
def test_has_perm(self):
model = self.app.model
auth = self.app.get_auth_handler()
with patch.multiple(mod.MasterView, create=True,
model_name='Setting'):
view = self.make_view()
# anonymous user
self.assertFalse(view.has_perm('list'))
self.assertFalse(self.request.has_perm('list'))
# reset
del self.request.user_permissions
# make user with perms
barney = model.User(username='barney')
self.session.add(barney)
blokes = model.Role(name="Blokes")
self.session.add(blokes)
barney.roles.append(blokes)
auth.grant_permission(blokes, 'settings.list')
self.session.commit()
# this user has perms
self.request.user = barney
self.assertTrue(view.has_perm('list'))
self.assertTrue(self.request.has_perm('settings.list'))
def test_has_any_perm(self):
model = self.app.model
auth = self.app.get_auth_handler()
with patch.multiple(mod.MasterView, create=True,
model_name='Setting'):
view = self.make_view()
# anonymous user
self.assertFalse(view.has_any_perm('list', 'view'))
self.assertFalse(self.request.has_any_perm('settings.list', 'settings.view'))
# reset
del self.request.user_permissions
# make user with perms
barney = model.User(username='barney')
self.session.add(barney)
blokes = model.Role(name="Blokes")
self.session.add(blokes)
barney.roles.append(blokes)
auth.grant_permission(blokes, 'settings.view')
self.session.commit()
# this user has perms
self.request.user = barney
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_make_progress(self):
# basic
view = self.make_view()
self.request.session.id = 'mockid'
progress = view.make_progress('foo')
self.assertIsInstance(progress, SessionProgress)
def test_render_progress(self):
self.pyramid_config.add_route('progress', '/progress/{key}')
# sanity / coverage check
view = self.make_view()
progress = MagicMock()
response = view.render_progress(progress)
def test_render_to_response(self):
self.pyramid_config.include('wuttaweb.views.common')
self.pyramid_config.include('wuttaweb.views.auth')
self.pyramid_config.add_route('appinfo', '/appinfo/')
def widgets(request): return {}
self.pyramid_config.add_route('widgets', '/widgets/')
self.pyramid_config.add_view(widgets, route_name='widgets')
# basic sanity check using /master/index.mako
# (nb. it skips /widgets/index.mako since that doesn't exist)
with patch.multiple(mod.MasterView, create=True,
model_name='Widget',
creatable=False):
view = mod.MasterView(self.request)
response = view.render_to_response('index', {})
self.assertIsInstance(response, Response)
# basic sanity check using /appinfo/index.mako
with patch.multiple(mod.MasterView, create=True,
model_name='AppInfo',
route_prefix='appinfo',
url_prefix='/appinfo',
creatable=False):
view = mod.MasterView(self.request)
response = view.render_to_response('index', {
# nb. grid is required for this template
'grid': MagicMock(),
})
self.assertIsInstance(response, Response)
# bad template name causes error
with patch.multiple(mod.MasterView, create=True,
model_name='Widget'):
self.assertRaises(IOError, view.render_to_response, 'nonexistent', {})
def test_get_index_title(self):
with patch.multiple(mod.MasterView, create=True,
model_title_plural = "Wutta Widgets"):
view = mod.MasterView(self.request)
self.assertEqual(view.get_index_title(), "Wutta Widgets")
def test_collect_labels(self):
# no labels by default
view = self.make_view()
labels = view.collect_labels()
self.assertEqual(labels, {})
# labels come from all classes; subclass wins
with patch.object(View, 'labels', new={'foo': "Foo", 'bar': "Bar"}, create=True):
with patch.object(mod.MasterView, 'labels', new={'foo': "FOO FIGHTERS"}, create=True):
view = self.make_view()
labels = view.collect_labels()
self.assertEqual(labels, {'foo': "FOO FIGHTERS", 'bar': "Bar"})
def test_set_labels(self):
model = self.app.model
with patch.object(mod.MasterView, 'model_class', new=model.Setting, create=True):
# no labels by default
view = self.make_view()
grid = view.make_model_grid(session=self.session)
view.set_labels(grid)
self.assertEqual(grid.labels, {})
# labels come from all classes; subclass wins
with patch.object(mod.MasterView, 'labels', new={'name': "SETTING NAME"}, create=True):
view = self.make_view()
view.set_labels(grid)
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
with patch.multiple(mod.MasterView, create=True,
model_name='Widget',
model_key='uuid'):
view = mod.MasterView(self.request)
grid = view.make_model_grid()
self.assertIsNone(grid.model_class)
# explicit model class
with patch.multiple(mod.MasterView, create=True,
model_class=model.Setting):
grid = view.make_model_grid(session=self.session)
self.assertIs(grid.model_class, model.Setting)
# no row class by default
with patch.multiple(mod.MasterView, create=True,
model_class=model.Setting):
grid = view.make_model_grid(session=self.session)
self.assertIsNone(grid.row_class)
# can specify row class
get_row_class = MagicMock()
with patch.multiple(mod.MasterView, create=True,
model_class=model.Setting,
grid_row_class=get_row_class):
grid = view.make_model_grid(session=self.session)
self.assertIs(grid.row_class, get_row_class)
# no actions by default
with patch.multiple(mod.MasterView, create=True,
model_class=model.Setting):
grid = view.make_model_grid(session=self.session)
self.assertEqual(grid.actions, [])
# now let's test some more actions logic
with patch.multiple(mod.MasterView, create=True,
model_class=model.Setting,
viewable=True,
editable=True,
deletable=True):
# should have 3 actions now, but for lack of perms
grid = view.make_model_grid(session=self.session)
self.assertEqual(len(grid.actions), 0)
# but root user has perms, so gets 3 actions
with patch.object(self.request, 'is_root', new=True):
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')
self.session.commit()
setting = self.session.query(model.Setting).one()
view = self.make_view()
# empty by default
self.assertFalse(hasattr(mod.MasterView, 'model_class'))
data = view.get_grid_data(session=self.session)
self.assertEqual(data, [])
# grid with model class will produce data query
with patch.multiple(mod.MasterView, create=True,
model_class=model.Setting):
view = mod.MasterView(self.request)
query = view.get_grid_data(session=self.session)
self.assertIsInstance(query, orm.Query)
data = query.all()
self.assertEqual(len(data), 1)
self.assertIs(data[0], setting)
def test_configure_grid(self):
model = self.app.model
# uuid field is pruned
with patch.multiple(mod.MasterView, create=True,
model_class=model.Setting):
view = mod.MasterView(self.request)
grid = view.make_grid(model_class=model.Setting,
columns=['uuid', 'name', 'value'])
self.assertIn('uuid', grid.columns)
view.configure_grid(grid)
self.assertNotIn('uuid', grid.columns)
def test_grid_render_bool(self):
model = self.app.model
view = self.make_view()
user = model.User(username='barney', active=None)
# null
value = view.grid_render_bool(user, 'active', None)
self.assertIsNone(value)
# true
user.active = True
value = view.grid_render_bool(user, 'active', True)
self.assertEqual(value, "Yes")
# false
user.active = False
value = view.grid_render_bool(user, 'active', False)
self.assertEqual(value, "No")
def test_grid_render_currency(self):
view = self.make_view()
obj = {'amount': None}
# null
value = view.grid_render_currency(obj, 'amount', None)
self.assertIsNone(value)
# normal amount
obj['amount'] = decimal.Decimal('100.42')
value = view.grid_render_currency(obj, 'amount', '100.42')
self.assertEqual(value, "$100.42")
# negative amount
obj['amount'] = decimal.Decimal('-100.42')
value = view.grid_render_currency(obj, 'amount', '-100.42')
self.assertEqual(value, "($100.42)")
def test_grid_render_datetime(self):
view = self.make_view()
obj = {'dt': None}
# null
value = view.grid_render_datetime(obj, 'dt', None)
self.assertIsNone(value)
# normal
obj['dt'] = datetime.datetime(2024, 8, 24, 11)
value = view.grid_render_datetime(obj, 'dt', '2024-08-24T11:00:00')
self.assertEqual(value, '2024-08-24 11:00:00 AM')
def test_grid_render_enum(self):
enum = self.app.enum
view = self.make_view()
obj = {'status': None}
# null
value = view.grid_render_enum(obj, 'status', None, enum=enum.UpgradeStatus)
self.assertIsNone(value)
# normal
obj['status'] = enum.UpgradeStatus.SUCCESS
value = view.grid_render_enum(obj, 'status', 'SUCCESS', enum=enum.UpgradeStatus)
self.assertEqual(value, 'SUCCESS')
def test_grid_render_notes(self):
model = self.app.model
view = self.make_view()
# null
text = None
role = model.Role(name="Foo", notes=text)
value = view.grid_render_notes(role, 'notes', text)
self.assertIsNone(value)
# short string
text = "hello world"
role = model.Role(name="Foo", notes=text)
value = view.grid_render_notes(role, 'notes', text)
self.assertEqual(value, text)
# long string
text = "hello world " * 20
role = model.Role(name="Foo", notes=text)
value = view.grid_render_notes(role, 'notes', text)
self.assertIn('<span ', value)
def test_get_instance(self):
model = self.app.model
self.app.save_setting(self.session, 'foo', 'bar')
self.session.commit()
self.assertEqual(self.session.query(model.Setting).count(), 1)
# default not implemented
view = mod.MasterView(self.request)
self.assertRaises(NotImplementedError, view.get_instance)
# fetch from DB if model class is known
with patch.multiple(mod.MasterView, create=True,
model_class=model.Setting):
view = mod.MasterView(self.request)
# existing setting is returned
self.request.matchdict = {'name': 'foo'}
setting = view.get_instance(session=self.session)
self.assertIsInstance(setting, model.Setting)
self.assertEqual(setting.name, 'foo')
self.assertEqual(setting.value, 'bar')
# missing setting not found
self.request.matchdict = {'name': 'blarg'}
self.assertRaises(HTTPNotFound, view.get_instance, session=self.session)
def test_get_action_url_view(self):
model = self.app.model
setting = model.Setting(name='foo', value='bar')
self.session.add(setting)
self.session.commit()
with patch.multiple(mod.MasterView, create=True,
model_class=model.Setting):
mod.MasterView.defaults(self.pyramid_config)
view = self.make_view()
url = view.get_action_url_view(setting, 0)
self.assertEqual(url, self.request.route_url('settings.view', name='foo'))
def test_get_action_url_edit(self):
model = self.app.model
setting = model.Setting(name='foo', value='bar')
self.session.add(setting)
self.session.commit()
with patch.multiple(mod.MasterView, create=True,
model_class=model.Setting):
mod.MasterView.defaults(self.pyramid_config)
view = self.make_view()
# typical
url = view.get_action_url_edit(setting, 0)
self.assertEqual(url, self.request.route_url('settings.edit', name='foo'))
# but null if instance not editable
with patch.object(view, 'is_editable', return_value=False):
url = view.get_action_url_edit(setting, 0)
self.assertIsNone(url)
def test_get_action_url_delete(self):
model = self.app.model
setting = model.Setting(name='foo', value='bar')
self.session.add(setting)
self.session.commit()
with patch.multiple(mod.MasterView, create=True,
model_class=model.Setting):
mod.MasterView.defaults(self.pyramid_config)
view = self.make_view()
# typical
url = view.get_action_url_delete(setting, 0)
self.assertEqual(url, self.request.route_url('settings.delete', name='foo'))
# but null if instance not deletable
with patch.object(view, 'is_deletable', return_value=False):
url = view.get_action_url_delete(setting, 0)
self.assertIsNone(url)
def test_make_model_form(self):
model = self.app.model
# no model class
with patch.multiple(mod.MasterView, create=True,
model_name='Widget',
model_key='uuid'):
view = mod.MasterView(self.request)
form = view.make_model_form()
self.assertIsNone(form.model_class)
# explicit model class
with patch.multiple(mod.MasterView, create=True,
model_class=model.Setting):
form = view.make_model_form()
self.assertIs(form.model_class, model.Setting)
def test_configure_form(self):
model = self.app.model
# uuid field is pruned
with patch.multiple(mod.MasterView, create=True,
model_class=model.Setting):
view = mod.MasterView(self.request)
form = view.make_form(model_class=model.Setting,
fields=['uuid', 'name', 'value'])
self.assertIn('uuid', form.fields)
view.configure_form(form)
self.assertNotIn('uuid', form.fields)
def test_objectify(self):
model = self.app.model
self.app.save_setting(self.session, 'foo', 'bar')
self.session.commit()
self.assertEqual(self.session.query(model.Setting).count(), 1)
# no model class
with patch.multiple(mod.MasterView, create=True,
model_name='Widget',
model_key='uuid'):
view = mod.MasterView(self.request)
form = view.make_model_form(fields=['name', 'description'])
form.validated = {'name': 'first'}
obj = view.objectify(form)
self.assertIs(obj, form.validated)
# explicit model class (editing)
with patch.multiple(mod.MasterView, create=True,
model_class=model.Setting,
editing=True):
form = view.make_model_form()
form.validated = {'name': 'foo', 'value': 'blarg'}
form.model_instance = self.session.query(model.Setting).one()
obj = view.objectify(form)
self.assertIsInstance(obj, model.Setting)
self.assertEqual(obj.name, 'foo')
self.assertEqual(obj.value, 'blarg')
# explicit model class (creating)
with patch.multiple(mod.MasterView, create=True,
model_class=model.Setting,
creating=True):
form = view.make_model_form()
form.validated = {'name': 'another', 'value': 'whatever'}
obj = view.objectify(form)
self.assertIsInstance(obj, model.Setting)
self.assertEqual(obj.name, 'another')
self.assertEqual(obj.value, 'whatever')
def test_persist(self):
model = self.app.model
with patch.multiple(mod.MasterView, create=True,
model_class=model.Setting):
view = mod.MasterView(self.request)
# new instance is persisted
setting = model.Setting(name='foo', value='bar')
self.assertEqual(self.session.query(model.Setting).count(), 0)
view.persist(setting, session=self.session)
self.session.commit()
setting = self.session.query(model.Setting).one()
self.assertEqual(setting.name, 'foo')
self.assertEqual(setting.value, 'bar')
##############################
# view methods
##############################
def test_index(self):
self.pyramid_config.include('wuttaweb.views.common')
self.pyramid_config.include('wuttaweb.views.auth')
self.pyramid_config.add_route('settings.create', '/settings/new')
self.pyramid_config.add_route('settings.view', '/settings/{name}')
self.pyramid_config.add_route('settings.edit', '/settings/{name}/edit')
self.pyramid_config.add_route('settings.delete', '/settings/{name}/delete')
# sanity/coverage check using /settings/
with patch.multiple(mod.MasterView, create=True,
model_name='Setting',
model_key='name',
get_index_url=MagicMock(return_value='/settings/'),
grid_columns=['name', 'value']):
view = mod.MasterView(self.request)
response = view.index()
# then again with data, to include view action url
data = [{'name': 'foo', 'value': 'bar'}]
with patch.object(view, 'get_grid_data', return_value=data):
response = view.index()
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content_type, 'text/html')
# then once more as 'partial' - aka. data only
self.request.GET = {'partial': '1'}
response = view.index()
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content_type, 'application/json')
# redirects when view is reset
self.request.GET = {'reset-view': '1', 'hash': 'foo'}
with patch.object(self.request, 'current_route_url'):
response = view.index()
self.assertEqual(response.status_code, 302)
def test_create(self):
self.pyramid_config.include('wuttaweb.views.common')
self.pyramid_config.include('wuttaweb.views.auth')
self.pyramid_config.add_route('settings.view', '/settings/{name}')
model = self.app.model
# sanity/coverage check using /settings/new
with patch.multiple(mod.MasterView, create=True,
model_name='Setting',
model_key='name',
get_index_url=MagicMock(return_value='/settings/'),
form_fields=['name', 'value']):
view = mod.MasterView(self.request)
# no setting yet
self.assertIsNone(self.app.get_setting(self.session, 'foo.bar'))
# get the form page
response = view.create()
self.assertIsInstance(response, Response)
self.assertEqual(response.status_code, 200)
# self.assertIn('frazzle', response.text)
# nb. no error
self.assertNotIn('Required', response.text)
def persist(setting):
self.app.save_setting(self.session, setting['name'], setting['value'])
self.session.commit()
# post request to save setting
self.request.method = 'POST'
self.request.POST = {
'name': 'foo.bar',
'value': 'fraggle',
}
with patch.object(view, 'persist', new=persist):
response = view.create()
# nb. should get redirect back to view page
self.assertEqual(response.status_code, 302)
# setting should now be in DB
self.assertEqual(self.app.get_setting(self.session, 'foo.bar'), 'fraggle')
# try another post with invalid data (value is required)
self.request.method = 'POST'
self.request.POST = {}
with patch.object(view, 'persist', new=persist):
response = view.create()
# nb. should get a form with errors
self.assertEqual(response.status_code, 200)
self.assertIn('Required', response.text)
# setting did not change in DB
self.assertEqual(self.app.get_setting(self.session, 'foo.bar'), 'fraggle')
def test_view(self):
self.pyramid_config.include('wuttaweb.views.common')
self.pyramid_config.include('wuttaweb.views.auth')
self.pyramid_config.add_route('settings.create', '/settings/new')
self.pyramid_config.add_route('settings.edit', '/settings/{name}/edit')
self.pyramid_config.add_route('settings.delete', '/settings/{name}/delete')
# sanity/coverage check using /settings/XXX
setting = {'name': 'foo.bar', 'value': 'baz'}
self.request.matchdict = {'name': 'foo.bar'}
with patch.multiple(mod.MasterView, create=True,
model_name='Setting',
model_key='name',
get_index_url=MagicMock(return_value='/settings/'),
grid_columns=['name', 'value'],
form_fields=['name', 'value']):
view = mod.MasterView(self.request)
with patch.object(view, 'get_instance', return_value=setting):
response = view.view()
def test_edit(self):
self.pyramid_config.include('wuttaweb.views.common')
self.pyramid_config.include('wuttaweb.views.auth')
self.pyramid_config.add_route('settings.create', '/settings/new')
self.pyramid_config.add_route('settings.view', '/settings/{name}')
self.pyramid_config.add_route('settings.delete', '/settings/{name}/delete')
model = self.app.model
self.app.save_setting(self.session, 'foo.bar', 'frazzle')
self.session.commit()
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/edit
self.request.matchdict = {'name': 'foo.bar'}
with patch.multiple(mod.MasterView, create=True,
model_name='Setting',
model_key='name',
get_index_url=MagicMock(return_value='/settings/'),
form_fields=['name', 'value']):
view = mod.MasterView(self.request)
with patch.object(view, 'get_instance', new=get_instance):
# get the form page
response = view.edit()
self.assertIsInstance(response, Response)
self.assertEqual(response.status_code, 200)
self.assertIn('frazzle', response.text)
# nb. no error
self.assertNotIn('Required', response.text)
def persist(setting):
self.app.save_setting(self.session, 'foo.bar', setting['value'])
self.session.commit()
# post request to save settings
self.request.method = 'POST'
self.request.POST = {
'name': 'foo.bar',
'value': 'froogle',
}
with patch.object(view, 'persist', new=persist):
response = view.edit()
# nb. should get redirect back to view page
self.assertEqual(response.status_code, 302)
# setting should be updated in DB
self.assertEqual(self.app.get_setting(self.session, 'foo.bar'), 'froogle')
# try another post with invalid data (value is required)
self.request.method = 'POST'
self.request.POST = {}
with patch.object(view, 'persist', new=persist):
response = view.edit()
# nb. should get a form with errors
self.assertEqual(response.status_code, 200)
self.assertIn('Required', response.text)
# setting did not change in DB
self.assertEqual(self.app.get_setting(self.session, 'foo.bar'), 'froogle')
def test_delete(self):
self.pyramid_config.include('wuttaweb.views.common')
self.pyramid_config.include('wuttaweb.views.auth')
self.pyramid_config.add_route('settings.create', '/settings/new')
self.pyramid_config.add_route('settings.view', '/settings/{name}')
self.pyramid_config.add_route('settings.edit', '/settings/{name}/edit')
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(mod.MasterView, create=True,
model_name='Setting',
model_key='name',
get_index_url=MagicMock(return_value='/settings/'),
form_fields=['name', 'value']):
view = mod.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):
self.app.delete_setting(self.session, setting['name'])
self.request.method = 'POST'
self.request.POST = {}
with patch.object(view, 'delete_instance', new=delete_instance):
# enforces "instance not deletable" rules
with patch.object(view, 'is_deletable', return_value=False):
response = view.delete()
# nb. should get redirect back to view page
self.assertEqual(response.status_code, 302)
# setting remains in DB
self.assertEqual(self.session.query(model.Setting).count(), 1)
# post request to delete setting
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(mod.MasterView, create=True,
model_class=model.Setting,
form_fields=['name', 'value']):
view = mod.MasterView(self.request)
view.delete_instance(setting)
self.session.commit()
self.assertEqual(self.session.query(model.Setting).count(), 0)
def test_delete_bulk(self):
self.pyramid_config.add_route('settings', '/settings/')
self.pyramid_config.add_route('progress', '/progress/{key}')
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 via quick method
# (user should be redirected back to index)
with patch.multiple(view,
deletable_bulk_quick=True,
make_model_grid=MagicMock(return_value=grid)):
response = view.delete_bulk()
self.assertEqual(response.status_code, 302)
self.assertEqual(self.session.query(model.Setting).count(), 7)
# now use another filter since those records are gone
self.request.GET = {'name': 'foo2', 'name.verb': 'equal'}
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), 1)
# this time we delete "slowly" with progress
self.request.session.id = 'ignorethis'
with patch.multiple(view,
deletable_bulk_quick=False,
make_model_grid=MagicMock(return_value=grid)):
with patch.object(mod, 'threading') as threading:
response = view.delete_bulk()
threading.Thread.return_value.start.assert_called_once_with()
# nb. user is shown progress page
self.assertEqual(response.status_code, 200)
def test_delete_bulk_action(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_action(settings)
self.session.commit()
self.assertEqual(self.session.query(model.Setting).count(), 7)
def test_delete_bulk_thread(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 delete, no progress
self.assertEqual(self.session.query(model.Setting).count(), 9)
settings = self.session.query(model.Setting)\
.filter(model.Setting.value.ilike('%s%'))
self.assertEqual(settings.count(), 2)
with patch.object(self.app, 'make_session', return_value=self.session):
view.delete_bulk_thread(settings)
self.assertEqual(self.session.query(model.Setting).count(), 7)
# basic delete, with progress
settings = self.session.query(model.Setting)\
.filter(model.Setting.name == 'foo1')
self.assertEqual(settings.count(), 1)
with patch.object(self.app, 'make_session', return_value=self.session):
view.delete_bulk_thread(settings, progress=MagicMock())
self.assertEqual(self.session.query(model.Setting).count(), 6)
# error, no progress
settings = self.session.query(model.Setting)\
.filter(model.Setting.name == 'foo2')
self.assertEqual(settings.count(), 1)
with patch.object(self.app, 'make_session', return_value=self.session):
with patch.object(view, 'delete_bulk_action', side_effect=RuntimeError):
view.delete_bulk_thread(settings)
# nb. nothing was deleted
self.assertEqual(self.session.query(model.Setting).count(), 6)
# error, with progress
self.assertEqual(settings.count(), 1)
with patch.object(self.app, 'make_session', return_value=self.session):
with patch.object(view, 'delete_bulk_action', side_effect=RuntimeError):
view.delete_bulk_thread(settings, progress=MagicMock())
# nb. nothing was deleted
self.assertEqual(self.session.query(model.Setting).count(), 6)
def test_autocomplete(self):
model = self.app.model
person1 = model.Person(full_name="George Jones")
self.session.add(person1)
person2 = model.Person(full_name="George Strait")
self.session.add(person2)
self.session.commit()
# no results for empty term
self.request.GET = {}
view = self.make_view()
results = view.autocomplete()
self.assertEqual(len(results), 0)
# search yields no results
self.request.GET = {'term': 'sally'}
view = self.make_view()
with patch.object(view, 'autocomplete_data', return_value=[]):
view = self.make_view()
results = view.autocomplete()
self.assertEqual(len(results), 0)
# search yields 2 results
self.request.GET = {'term': 'george'}
view = self.make_view()
with patch.object(view, 'autocomplete_data', return_value=[person1, person2]):
results = view.autocomplete()
self.assertEqual(len(results), 2)
self.assertEqual([res['value'] for res in results],
[p.uuid for p in [person1, person2]])
def test_autocomplete_normalize(self):
model = self.app.model
view = self.make_view()
person = model.Person(full_name="Betty Boop", uuid='bogus')
normal = view.autocomplete_normalize(person)
self.assertEqual(normal, {'value': 'bogus',
'label': "Betty Boop"})
def test_download(self):
model = self.app.model
self.app.save_setting(self.session, 'foo', 'bar')
self.session.commit()
with patch.multiple(mod.MasterView, create=True,
model_class=model.Setting,
model_key='name',
Session=MagicMock(return_value=self.session)):
view = self.make_view()
self.request.matchdict = {'name': 'foo'}
# 404 if no filename
response = view.download()
self.assertEqual(response.status_code, 404)
# 404 if bad filename
self.request.GET = {'filename': 'doesnotexist'}
response = view.download()
self.assertEqual(response.status_code, 404)
# 200 if good filename
foofile = self.write_file('foo.txt', 'foo')
with patch.object(view, 'download_path', return_value=foofile):
response = view.download()
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content_disposition, 'attachment; filename="foo.txt"')
def test_execute(self):
self.pyramid_config.add_route('settings.view', '/settings/{name}')
self.pyramid_config.add_route('progress', '/progress/{key}')
model = self.app.model
self.app.save_setting(self.session, 'foo', 'bar')
user = model.User(username='barney')
self.session.add(user)
self.session.commit()
with patch.multiple(mod.MasterView, create=True,
model_class=model.Setting,
model_key='name',
Session=MagicMock(return_value=self.session)):
view = self.make_view()
self.request.matchdict = {'name': 'foo'}
self.request.session.id = 'mockid'
self.request.user = user
# basic usage; user is shown progress page
with patch.object(mod, 'threading') as threading:
response = view.execute()
threading.Thread.return_value.start.assert_called_once_with()
self.assertEqual(response.status_code, 200)
def test_execute_thread(self):
model = self.app.model
enum = self.app.enum
user = model.User(username='barney')
self.session.add(user)
upgrade = model.Upgrade(description='test', created_by=user,
status=enum.UpgradeStatus.PENDING)
self.session.add(upgrade)
self.session.commit()
with patch.multiple(mod.MasterView, create=True,
model_class=model.Upgrade):
view = self.make_view()
# basic execute, no progress
with patch.object(view, 'execute_instance') as execute_instance:
view.execute_thread({'uuid': upgrade.uuid}, user.uuid)
execute_instance.assert_called_once()
# basic execute, with progress
with patch.object(view, 'execute_instance') as execute_instance:
progress = MagicMock()
view.execute_thread({'uuid': upgrade.uuid}, user.uuid, progress=progress)
execute_instance.assert_called_once()
progress.handle_success.assert_called_once_with()
# error, no progress
with patch.object(view, 'execute_instance') as execute_instance:
execute_instance.side_effect = RuntimeError
view.execute_thread({'uuid': upgrade.uuid}, user.uuid)
execute_instance.assert_called_once()
# error, with progress
with patch.object(view, 'execute_instance') as execute_instance:
progress = MagicMock()
execute_instance.side_effect = RuntimeError
view.execute_thread({'uuid': upgrade.uuid}, user.uuid, progress=progress)
execute_instance.assert_called_once()
progress.handle_error.assert_called_once()
def test_configure(self):
self.pyramid_config.include('wuttaweb.views.common')
self.pyramid_config.include('wuttaweb.views.auth')
model = self.app.model
# mock settings
settings = [
{'name': 'wutta.app_title'},
{'name': 'wutta.foo', 'value': 'bar'},
{'name': 'wutta.flag', 'type': bool},
{'name': 'wutta.number', 'type': int, 'default': 42},
{'name': 'wutta.value1', 'save_if_empty': True},
{'name': 'wutta.value2', 'save_if_empty': False},
]
view = mod.MasterView(self.request)
with patch.object(self.request, 'current_route_url', return_value='/appinfo/configure'):
with patch.object(mod, 'Session', return_value=self.session):
with patch.multiple(mod.MasterView, create=True,
model_name='AppInfo',
route_prefix='appinfo',
template_prefix='/appinfo',
creatable=False,
get_index_url=MagicMock(return_value='/appinfo/'),
configure_get_simple_settings=MagicMock(return_value=settings)):
# get the form page
response = view.configure(session=self.session)
self.assertIsInstance(response, Response)
# post request to save settings
self.request.method = 'POST'
self.request.POST = {
'wutta.app_title': 'Wutta',
'wutta.foo': 'bar',
'wutta.flag': 'true',
}
response = view.configure(session=self.session)
# nb. should get redirect back to configure page
self.assertEqual(response.status_code, 302)
# should now have 5 settings
count = self.session.query(model.Setting).count()
self.assertEqual(count, 5)
get_setting = functools.partial(self.app.get_setting, self.session)
self.assertEqual(get_setting('wutta.app_title'), 'Wutta')
self.assertEqual(get_setting('wutta.foo'), 'bar')
self.assertEqual(get_setting('wutta.flag'), 'true')
self.assertEqual(get_setting('wutta.number'), '42')
self.assertEqual(get_setting('wutta.value1'), '')
self.assertEqual(get_setting('wutta.value2'), None)
# post request to remove settings
self.request.method = 'POST'
self.request.POST = {'remove_settings': '1'}
response = view.configure(session=self.session)
# nb. should get redirect back to configure page
self.assertEqual(response.status_code, 302)
# should now have 0 settings
count = self.session.query(model.Setting).count()
self.assertEqual(count, 0)