# -*- 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)