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