# -*- 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, HTTPFound from webhelpers2.html import HTML 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 wuttaweb.testing import WebTestCase, VersionWebTestCase from wuttaweb.grids import Grid 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, mergeable=True, has_rows=True, rows_creatable=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") def test_get_row_model_class(self): model = self.app.model # no default self.assertIsNone(mod.MasterView.get_row_model_class()) # class may specify with patch.object( mod.MasterView, "row_model_class", create=True, new=model.User ): self.assertIs(mod.MasterView.get_row_model_class(), model.User) def test_get_row_model_name(self): # error by default (since no model class) self.assertRaises(AttributeError, mod.MasterView.get_row_model_name) # may specify model name directly with patch.object(mod.MasterView, "row_model_name", new="Widget", create=True): self.assertEqual(mod.MasterView.get_row_model_name(), "Widget") # or indirectly via model class MyModel = MagicMock(__name__="Blaster") with patch.object(mod.MasterView, "row_model_class", new=MyModel): self.assertEqual(mod.MasterView.get_row_model_name(), "Blaster") def test_get_row_model_title(self): # error by default (since no model class) self.assertRaises(AttributeError, mod.MasterView.get_row_model_title) # may specify model title directly with patch.object( mod.MasterView, "row_model_title", new="Wutta Widget", create=True ): self.assertEqual(mod.MasterView.get_row_model_title(), "Wutta Widget") # or may specify model name with patch.object(mod.MasterView, "row_model_name", new="Blaster", create=True): self.assertEqual(mod.MasterView.get_row_model_title(), "Blaster") # or may specify model class MyModel = MagicMock(__name__="Dinosaur") with patch.object(mod.MasterView, "row_model_class", new=MyModel): self.assertEqual(mod.MasterView.get_row_model_title(), "Dinosaur") # model class may have wutta hint MyModel.__wutta_hint__ = {"model_title": "T-Rex"} with patch.object(mod.MasterView, "row_model_class", new=MyModel): self.assertEqual(mod.MasterView.get_row_model_title(), "T-Rex") def test_get_row_model_title_plural(self): # error by default (since no model class) self.assertRaises(AttributeError, mod.MasterView.get_row_model_title_plural) # subclass may specify *plural* model title with patch.object( mod.MasterView, "row_model_title_plural", new="People", create=True ): self.assertEqual(mod.MasterView.get_row_model_title_plural(), "People") # or it may specify *singular* model title with patch.object( mod.MasterView, "row_model_title", new="Wutta Widget", create=True ): self.assertEqual( mod.MasterView.get_row_model_title_plural(), "Wutta Widgets" ) # or it may specify model name with patch.object(mod.MasterView, "row_model_name", new="Blaster", create=True): self.assertEqual(mod.MasterView.get_row_model_title_plural(), "Blasters") # or it may specify model class MyModel = MagicMock(__name__="Dinosaur") with patch.object(mod.MasterView, "row_model_class", new=MyModel, create=True): self.assertEqual(mod.MasterView.get_row_model_title_plural(), "Dinosaurs") # model class may have wutta hint MyModel.__wutta_hint__ = {"model_title_plural": "T-Rexes"} with patch.object(mod.MasterView, "row_model_class", new=MyModel): self.assertEqual(mod.MasterView.get_row_model_title_plural(), "T-Rexes") ############################## # 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("Merge cannot proceed:

' '

because i said so

' ), ], ) # error executing merge with patch.object(view, "merge_execute", side_effect=RuntimeError): result = view.merge_validate_and_execute(person1, person2) self.assertFalse(result) self.assertEqual(self.session.query(model.Person).count(), 2) self.assertFalse(self.request.session.peek_flash()) self.assertFalse(self.request.session.peek_flash("warning")) self.assertEqual( self.request.session.pop_flash("error"), [ HTML.literal( '

Merge failed:

' '

RuntimeError

' ), ], ) def test_merge(self): self.pyramid_config.add_route("home", "/") self.pyramid_config.add_route("login", "/auth/login") self.pyramid_config.add_route("people", "/people/") self.pyramid_config.add_route("people.merge", "/people/merge") self.pyramid_config.add_route("people.view", "/people/{uuid}") model = self.app.model class MergeRoute: name = "people.merge" person1 = model.Person( first_name="Freddie", last_name="Flintstone", full_name="Freddie Flintstone" ) self.session.add(person1) person2 = model.Person( first_name="Fred", last_name="Flintstone", full_name="Fred Flintstone" ) self.session.add(person2) self.session.commit() self.assertEqual(self.session.query(model.Person).count(), 2) self.assertIn(person1, self.session) with patch.multiple( mod.MasterView, Session=MagicMock(return_value=self.session), model_class=model.Person, route_prefix="people", create=True, ): view = self.make_view() # GET request will redirect to index result = view.merge() self.assertIsInstance(result, HTTPFound) self.assertEqual(result.location, "http://example.com/people/") self.assertEqual(self.session.query(model.Person).count(), 2) # assume POST from now on with patch.multiple( self.request, matched_route=MergeRoute, method="POST", create=True ): # POST without 'execute-merge' flag shows user the diff with patch.object( self.request, "POST", new={"uuids": f"{person1.uuid},{person2.uuid}"}, ): response = view.merge() self.assertIsInstance(response, Response) self.assertEqual(response.status_code, 200) self.assertEqual(self.session.query(model.Person).count(), 2) self.assertFalse(self.request.session.peek_flash()) self.assertFalse(self.request.session.peek_flash("warning")) self.assertFalse(self.request.session.peek_flash("error")) # default merge logic deletes person1, then redirects with patch.object( self.request, "POST", new={ "uuids": f"{person1.uuid},{person2.uuid}", "execute-merge": "true", }, ): result = view.merge() self.assertIsInstance(result, HTTPFound) self.assertEqual( result.location, f"http://example.com/people/{person2.uuid}" ) self.assertEqual(self.session.query(model.Person).count(), 1) self.assertNotIn(person1, self.session) self.assertIn(person2, self.session) person = self.session.query(model.Person).one() self.assertIs(person, person2) self.assertEqual(person.first_name, "Fred") self.assertEqual(person.full_name, "Fred Flintstone") self.assertFalse(self.request.session.peek_flash("warning")) self.assertFalse(self.request.session.peek_flash("error")) self.assertEqual( self.request.session.pop_flash(), ["Freddie Flintstone has been merged into Fred Flintstone"], ) # restore Freddie self.assertNotIn(person1, self.session) person1 = self.session.merge(person1) self.session.commit() self.assertEqual(self.session.query(model.Person).count(), 2) # simple redirect if invalid uuids specified with patch.object( self.request, "POST", new={ "uuids": "bogus1,bogus2", "execute-merge": "true", }, ): with self.assertRaises(HTTPFound) as cm: view.merge() self.assertEqual( cm.exception.location, "http://example.com/people/" ) self.assertEqual(self.session.query(model.Person).count(), 2) self.assertFalse(self.request.session.peek_flash()) self.assertFalse(self.request.session.peek_flash("warning")) self.assertFalse(self.request.session.peek_flash("error")) # simple redirect if unknown uuids specified fake1 = self.app.make_true_uuid() fake2 = self.app.make_true_uuid() with patch.object( self.request, "POST", new={ "uuids": f"{fake1},{fake2}", "execute-merge": "true", }, ): with self.assertRaises(HTTPFound) as cm: view.merge() self.assertEqual( cm.exception.location, "http://example.com/people/" ) self.assertEqual(self.session.query(model.Person).count(), 2) self.assertFalse(self.request.session.peek_flash()) self.assertFalse(self.request.session.peek_flash("warning")) self.assertFalse(self.request.session.peek_flash("error")) # warning redirect if merge does not validate with patch.object( self.request, "POST", new={ "uuids": f"{person1.uuid},{person2.uuid}", "execute-merge": "true", }, ): with patch.object( view, "merge_why_not", return_value="because i said so" ): response = view.merge() self.assertIsInstance(response, Response) self.assertEqual(self.session.query(model.Person).count(), 2) self.assertFalse(self.request.session.peek_flash()) self.assertFalse(self.request.session.peek_flash("error")) # TODO: since response is already rendered, the warning flash # msg has already been popped off the stack..will have to # avoid render_to_response() to properly test that.. self.assertFalse(self.request.session.peek_flash("warning")) # self.assertEqual( # self.request.session.pop_flash("warning"), # [ # HTML.literal( # '

Merge cannot proceed:

' # '

because i said so

' # ), # ], # ) # error redirect if merge execution fails with patch.object( self.request, "POST", new={ "uuids": f"{person1.uuid},{person2.uuid}", "execute-merge": "true", }, ): with patch.object(view, "merge_execute", side_effect=RuntimeError): response = view.merge() self.assertIsInstance(response, Response) self.assertEqual(self.session.query(model.Person).count(), 2) self.assertFalse(self.request.session.peek_flash()) self.assertFalse(self.request.session.peek_flash("warning")) # TODO: since response is already rendered, the error flash # msg has already been popped off the stack..will have to # avoid render_to_response() to properly test that.. self.assertFalse(self.request.session.peek_flash("error")) # self.assertEqual( # self.request.session.pop_flash("error"), # [ # HTML.literal( # '

Merge failed:

' # '

RuntimeError

' # ), # ], # ) 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") self.pyramid_config.add_route( "appinfo.check_timezone", "/appinfo/check-timezone" ) 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), ): # nb. appinfo/configure template requires menu_handlers original_context = view.configure_get_context def get_context(**kw): kw = original_context(**kw) kw["menu_handlers"] = [] kw["default_timezone"] = "UTC" kw["grid_pagesize_options"] = [10, 20, 50] kw["grid_pagesize_default"] = 20 return kw with patch.object(view, "configure_get_context", new=get_context): # 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) def test_configure_get_simple_settings(self): view = self.make_view() settings = view.configure_get_simple_settings() self.assertEqual(settings, []) def test_configure_gather_settings(self): view = self.make_view() simple_settings = [ {"name": "wutta.app_title"}, {"name": "wutta.foo"}, {"name": "wutta.flag", "type": bool, "default": True}, {"name": "wutta.number", "type": int, "default": 42}, {"name": "wutta.value1", "save_if_empty": True}, {"name": "wutta.value2", "save_if_empty": False}, {"name": "wutta.value3", "save_if_empty": False, "default": "baz"}, ] data = { "wutta.app_title": "Poser", "wutta.foo": "bar", "wutta.number": 44, "wutta.value1": None, } with patch.object( view, "configure_get_simple_settings", return_value=simple_settings ): settings = view.configure_gather_settings(data) self.assertEqual(len(settings), 6) self.assertEqual( settings, [ {"name": "wutta.app_title", "value": "Poser"}, {"name": "wutta.foo", "value": "bar"}, {"name": "wutta.flag", "value": "false"}, {"name": "wutta.number", "value": "44"}, {"name": "wutta.value1", "value": ""}, {"name": "wutta.value3", "value": "baz"}, ], ) ############################## # row methods ############################## def test_get_rows_title(self): model = self.app.model with patch.object(mod.MasterView, "row_model_class", new=model.User): view = self.make_view() # default based on row model class self.assertEqual(view.get_rows_title(), "Users") # explicit override with patch.object(view, "rows_title", create=True, new="Mock Rows"): self.assertEqual(view.get_rows_title(), "Mock Rows") def test_get_row_parent(self): model = self.app.model view = self.make_view() person = model.Person(full_name="Fred Flintstone") self.session.add(person) user = model.User(username="fred", person=person) self.session.add(user) self.session.commit() with patch.multiple( mod.MasterView, model_class=model.Person, row_model_class=model.User ): self.assertRaises(NotImplementedError, view.get_row_parent, user) def test_collect_row_labels(self): # default labels view = self.make_view() labels = view.collect_row_labels() self.assertEqual(labels, {}) # labels come from all classes; subclass wins with patch.object( View, "row_labels", create=True, new={"foo": "Foo", "bar": "Bar"} ): with patch.object( mod.MasterView, "row_labels", create=True, new={"foo": "FOO FIGHTERS"} ): view = self.make_view() labels = view.collect_row_labels() self.assertEqual(labels, {"foo": "FOO FIGHTERS", "bar": "Bar"}) def test_set_row_labels(self): model = self.app.model person = model.Person(full_name="Fred Flintstone") self.session.add(person) with patch.multiple( mod.MasterView, create=True, model_class=model.Person, has_rows=True, row_model_class=model.User, ): # no labels by default view = self.make_view() grid = view.make_row_model_grid(person, key="person.users", data=[]) view.set_row_labels(grid) self.assertEqual(grid.labels, {}) # labels come from all classes; subclass wins with patch.object( View, "row_labels", create=True, new={"username": "USERNAME"} ): with patch.object( mod.MasterView, "row_labels", create=True, new={"username": "UserName"}, ): view = self.make_view() grid = view.make_row_model_grid(person, key="person.users", data=[]) view.set_row_labels(grid) self.assertEqual(grid.labels, {"username": "UserName"}) def test_get_row_grid_data(self): model = self.app.model person = model.Person(full_name="Fred Flintstone") self.session.add(person) view = self.make_view() self.assertRaises(NotImplementedError, view.get_row_grid_data, person) def test_get_row_grid_columns(self): # no default view = self.make_view() self.assertIsNone(view.get_row_grid_columns()) # class may specify with patch.object(view, "row_grid_columns", create=True, new=["foo", "bar"]): self.assertEqual(view.get_row_grid_columns(), ["foo", "bar"]) def test_get_row_grid_key(self): view = self.make_view() with patch.multiple( mod.MasterView, create=True, model_key="id", grid_key="widgets" ): self.request.matchdict = {"id": 42} self.assertEqual(view.get_row_grid_key(), "widgets.42") def test_make_row_model_grid(self): model = self.app.model person = model.Person(full_name="Barney Rubble") self.session.add(person) self.session.commit() self.request.matchdict = {"uuid": person.uuid} with patch.multiple(mod.MasterView, create=True, model_class=model.Person): view = self.make_view() # specify data grid = view.make_row_model_grid(person, data=[]) self.assertIsNone(grid.model_class) self.assertEqual(grid.data, []) # fetch data with patch.object(view, "get_row_grid_data", return_value=[]): grid = view.make_row_model_grid(person) self.assertIsNone(grid.model_class) self.assertEqual(grid.data, []) # view action with patch.object(view, "rows_viewable", new=True): with patch.object(view, "get_row_action_url_view", return_value="#"): grid = view.make_row_model_grid(person, data=[]) self.assertEqual(len(grid.actions), 1) self.assertEqual(grid.actions[0].key, "view") def test_get_row_action_url_view(self): view = self.make_view() row = MagicMock() self.assertRaises(NotImplementedError, view.get_row_action_url_view, row, 0) def test_make_row_model_form(self): model = self.app.model view = self.make_view() # no model class form = view.make_row_model_form() self.assertIsNone(form.model_class) # explicit model class + fields form = view.make_row_model_form( model_class=model.User, fields=["username", "active"] ) self.assertIs(form.model_class, model.User) self.assertEqual(form.fields, ["username", "active"]) # implicit model + fields with patch.multiple( mod.MasterView, create=True, row_model_class=model.User, row_form_fields=["username", "person"], ): form = view.make_row_model_form() self.assertIs(form.model_class, model.User) self.assertEqual(form.fields, ["username", "person"]) def test_configure_row_form(self): model = self.app.model view = self.make_view() # uuid field is pruned with patch.object(mod.MasterView, "row_model_class", new=model.User): form = view.make_form(model_class=model.User, fields=["uuid", "username"]) self.assertIn("uuid", form.fields) view.configure_row_form(form) self.assertNotIn("uuid", form.fields) def test_create_row(self): self.pyramid_config.add_route("home", "/") self.pyramid_config.add_route("login", "/auth/login") self.pyramid_config.add_route("people", "/people/") self.pyramid_config.add_route("people.view", "/people/{uuid}") model = self.app.model person = model.Person( first_name="Fred", last_name="Flintstone", full_name="Fred Flintstone" ) self.session.add(person) user = model.User(username="fred", person=person) self.session.add(user) self.session.commit() with patch.multiple( mod.MasterView, create=True, model_class=model.Person, row_model_class=model.User, row_form_fields=["person_uuid", "username"], route_prefix="people", ): with patch.object(mod.MasterView, "Session", return_value=self.session): with patch.object(self.request, "matchdict", {"uuid": person.uuid}): with patch.object( mod.MasterView, "get_row_parent", return_value=person ): view = self.make_view() # get the form page response = view.create_row() self.assertIsInstance(response, Response) self.assertEqual(response.status_code, 200) # nb. no error self.assertNotIn("Required", response.text) self.assertEqual(len(person.users), 1) # post request to add user with patch.multiple( self.request, method="POST", POST={ "person_uuid": person.uuid.hex, "username": "freddie2", }, ): response = view.create_row() # nb. should get redirect back to view page self.assertEqual(response.status_code, 302) # user should now be in DB self.session.refresh(person) self.assertEqual(len(person.users), 2) # try another post with invalid data (username is required) with patch.multiple( self.request, method="POST", POST={"person_uuid": person.uuid.hex, "username": ""}, ): response = view.create_row() # nb. should get a form with errors self.assertEqual(response.status_code, 200) self.assertIn("Required", response.text) self.session.refresh(person) self.assertEqual(len(person.users), 2) class TestVersionedMasterView(VersionWebTestCase): def make_view(self): return mod.MasterView(self.request) def test_is_versioned(self): model = self.app.model with patch.object(mod.MasterView, "model_class", new=model.User): # User is versioned by default self.assertTrue(mod.MasterView.is_versioned()) # but view can override w/ attr with patch.object( mod.MasterView, "model_is_versioned", new=False, create=True ): self.assertFalse(mod.MasterView.is_versioned()) def test_defaults(self): model = self.app.model with patch.object(mod.MasterView, "model_class", new=model.User): mod.MasterView.defaults(self.pyramid_config) def test_get_model_version_class(self): model = self.app.model with patch.object(mod.MasterView, "model_class", new=model.User): view = self.make_view() vercls = view.get_model_version_class() self.assertEqual(vercls.__name__, "UserVersion") def test_should_expose_versions(self): model = self.app.model with patch.object(mod.MasterView, "model_class", new=model.User): # fully enabled for root user with patch.object(self.request, "is_root", new=True): view = self.make_view() self.assertTrue(view.should_expose_versions()) # but not if user has no access view = self.make_view() self.assertFalse(view.should_expose_versions()) # again, works for root user with patch.object(self.request, "is_root", new=True): view = self.make_view() self.assertTrue(view.should_expose_versions()) # but not if config disables versioning with patch.object(view.app, "continuum_is_enabled", return_value=False): self.assertFalse(view.should_expose_versions()) def test_get_version_grid_key(self): model = self.app.model with patch.object(mod.MasterView, "model_class", new=model.User): # default view = self.make_view() self.assertEqual(view.get_version_grid_key(), "users.history") # custom with patch.object( mod.MasterView, "version_grid_key", new="users_custom_history", create=True, ): view = self.make_view() self.assertEqual(view.get_version_grid_key(), "users_custom_history") def test_get_version_grid_columns(self): model = self.app.model with patch.object(mod.MasterView, "model_class", new=model.User): # default view = self.make_view() self.assertEqual( view.get_version_grid_columns(), ["id", "issued_at", "user", "remote_addr", "comment"], ) # custom with patch.object( mod.MasterView, "version_grid_columns", new=["issued_at", "user"], create=True, ): view = self.make_view() self.assertEqual(view.get_version_grid_columns(), ["issued_at", "user"]) def test_get_version_joins(self): view = self.make_view() self.assertEqual(view.get_version_joins(), []) def test_normalize_version_joins(self): model = self.app.model view = self.make_view() joins = [(model.Person, "uuid", "person_uuid")] with patch.object(view, "get_version_joins", return_value=joins): normal = view.normalize_version_joins() self.assertEqual(normal, joins) self.assertEqual(normal, [(model.Person, "uuid", "person_uuid")]) joins = [model.Person] with patch.object(view, "get_version_joins", return_value=joins): normal = view.normalize_version_joins() self.assertNotEqual(normal, joins) self.assertEqual(normal, [(model.Person, "uuid", "uuid")]) def test_get_version_grid_data(self): model = self.app.model user = model.User(username="fred") self.session.add(user) self.session.commit() user.username = "freddie" self.session.commit() with patch.object(mod.MasterView, "model_class", new=model.User): view = self.make_view() query = view.get_version_grid_data(user) self.assertIsInstance(query, orm.Query) transactions = query.all() self.assertEqual(len(transactions), 2) def test_configure_version_grid(self): import sqlalchemy_continuum as continuum model = self.app.model txncls = continuum.transaction_class(model.User) with patch.object(mod.MasterView, "model_class", new=model.User): view = self.make_view() # this is mostly just for coverage, but we at least can # confirm something does change grid = view.make_grid(model_class=txncls) self.assertNotIn("issued_at", grid.linked_columns) view.configure_version_grid(grid) self.assertIn("issued_at", grid.linked_columns) def test_make_version_grid(self): model = self.app.model user = model.User(username="fred") self.session.add(user) self.session.commit() user.username = "freddie" self.session.commit() with patch.object(mod.MasterView, "model_class", new=model.User): with patch.object(mod.MasterView, "Session", return_value=self.session): with patch.dict(self.request.matchdict, uuid=user.uuid): view = self.make_view() grid = view.make_version_grid() self.assertIsInstance(grid, Grid) self.assertIsInstance(grid.data, orm.Query) self.assertEqual(len(grid.data.all()), 2) def test_view_versions(self): self.pyramid_config.add_route("home", "/") self.pyramid_config.add_route("login", "/auth/login") self.pyramid_config.add_route("users", "/users/") self.pyramid_config.add_route("users.view", "/users/{uuid}") self.pyramid_config.add_route("users.version", "/users/{uuid}/versions/{txnid}") model = self.app.model user = model.User(username="fred") self.session.add(user) self.session.commit() user.username = "freddie" self.session.commit() with patch.object(mod.MasterView, "model_class", new=model.User): with patch.object(mod.MasterView, "Session", return_value=self.session): with patch.dict(self.request.matchdict, uuid=user.uuid): view = self.make_view() # normal, full page response = view.view_versions() self.assertEqual(response.content_type, "text/html") self.assertIn("', response.text, ) # second txn second = transactions[1] with patch.dict( self.request.matchdict, uuid=user.uuid, txnid=second.id ): view = self.make_view() response = view.view_version() self.assertIn( '', response.text, )