1
0
Fork 0

feat: add basic configure view for appinfo

This commit is contained in:
Lance Edgar 2024-08-06 18:52:54 -05:00
parent dd207a4a05
commit ed67cdb2d8
15 changed files with 847 additions and 42 deletions

View file

@ -110,7 +110,11 @@ def make_pyramid_config(settings):
The config is initialized with certain features deemed useful for
all apps.
:returns: Instance of
:class:`pyramid:pyramid.config.Configurator`.
"""
settings.setdefault('mako.directories', ['wuttaweb:templates'])
settings.setdefault('pyramid_deform.template_search_path',
'wuttaweb:templates/deform')
@ -119,6 +123,11 @@ def make_pyramid_config(settings):
# configure user authorization / authentication
pyramid_config.set_security_policy(WuttaSecurityPolicy())
# require CSRF token for POST
pyramid_config.set_default_csrf_options(require_csrf=True,
token='_csrf',
header='X-CSRF-TOKEN')
pyramid_config.include('pyramid_beaker')
pyramid_config.include('pyramid_deform')
pyramid_config.include('pyramid_mako')
@ -143,8 +152,6 @@ def main(global_config, **settings):
will need to define their own ``main()`` function, and use that
instead.
"""
settings.setdefault('mako.directories', ['wuttaweb:templates'])
wutta_config = make_wutta_config(settings)
pyramid_config = make_pyramid_config(settings)

View file

@ -323,6 +323,7 @@ class Form:
"""
context['form'] = self
context.setdefault('form_attrs', {})
context.setdefault('request', self.request)
# auto disable button on submit
if self.auto_disable_submit:

View file

@ -38,12 +38,20 @@ instance:
This module contains the following references:
* :func:`~wuttaweb.util.get_liburl()`
* all names from :mod:`webhelpers2:webhelpers2.html`
* all names from :mod:`webhelpers2:webhelpers2.html.tags`
* :func:`~wuttaweb.util.get_liburl()`
* :func:`~wuttaweb.util.get_csrf_token()`
* :func:`~wuttaweb.util.render_csrf_token()` (as :func:`csrf_token()`)
.. function:: csrf_token
This is a shorthand reference to
:func:`wuttaweb.util.render_csrf_token()`.
"""
from webhelpers2.html import *
from webhelpers2.html.tags import *
from wuttaweb.util import get_liburl
from wuttaweb.util import get_liburl, get_csrf_token, render_csrf_token as csrf_token

View file

@ -0,0 +1,21 @@
## -*- coding: utf-8; -*-
<%inherit file="/configure.mako" />
<%def name="form_content()">
<h3 class="block is-size-3">Basics</h3>
<div class="block" style="padding-left: 2rem; width: 50%;">
<b-field label="App Title">
<b-input name="${app.appname}.app_title"
v-model="simpleSettings['${app.appname}.app_title']"
@input="settingsNeedSaved = true">
</b-input>
</b-field>
</div>
</%def>
${parent.body()}

View file

@ -209,16 +209,14 @@
</div>
</nav>
<nav class="level" style="margin: 0.5rem auto;">
<nav class="level" style="margin: 0.5rem 0.5rem 0.5rem auto;">
<div class="level-left">
## Current Context
<div id="current-context" class="level-item">
% if index_title:
% if index_url:
<span class="header-text">
${h.link_to(index_title, index_url)}
</span>
<h1 class="title">${h.link_to(index_title, index_url)}</h1>
% else:
<h1 class="title">${index_title}</h1>
% endif
@ -226,6 +224,23 @@
</div>
</div><!-- level-left -->
<div class="level-right">
## TODO
% if master and master.configurable and not master.configuring:
<div class="level-item">
<b-button type="is-primary"
tag="a"
href="${url(f'{route_prefix}.configure')}"
icon-pack="fas"
icon-left="cog">
Configure
</b-button>
</div>
% endif
</div> <!-- level-right -->
</nav><!-- level -->
</header>
@ -318,8 +333,7 @@
<div class="navbar-dropdown">
% if request.is_root:
${h.form(url('stop_root'), ref='stopBeingRootForm')}
## TODO
## ${h.csrf_token(request)}
${h.csrf_token(request)}
<input type="hidden" name="referrer" value="${request.current_route_url()}" />
<a @click="stopBeingRoot()"
class="navbar-item has-background-danger has-text-white">
@ -328,8 +342,7 @@
${h.end_form()}
% elif request.is_admin:
${h.form(url('become_root'), ref='startBeingRootForm')}
## TODO
## ${h.csrf_token(request)}
${h.csrf_token(request)}
<input type="hidden" name="referrer" value="${request.current_route_url()}" />
<a @click="startBeingRoot()"
class="navbar-item has-background-danger has-text-white">

View file

@ -0,0 +1,181 @@
## -*- coding: utf-8; -*-
<%inherit file="/page.mako" />
<%def name="title()">Configure ${config_title}</%def>
<%def name="page_content()">
<br />
${self.buttons_content()}
${h.form(request.current_route_url(), enctype='multipart/form-data', ref='saveSettingsForm', **{'@submit': 'saveSettingsFormSubmit'})}
${h.csrf_token(request)}
${self.form_content()}
${h.end_form()}
<b-modal has-modal-card
:active.sync="purgeSettingsShowDialog">
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Remove All Settings</p>
</header>
<section class="modal-card-body">
<p class="block">
Really remove all settings for ${config_title} from the DB?
</p>
<p class="block">
Note that when you <span class="is-italic">save</span>
settings, any existing settings are first removed and then
new ones are saved.
</p>
<p class="block">
But here you can remove existing without saving new
ones.&nbsp; It is basically "factory reset" for
${config_title}.
</p>
</section>
<footer class="modal-card-foot">
<b-button @click="purgeSettingsShowDialog = false">
Cancel
</b-button>
${h.form(request.current_route_url())}
${h.csrf_token(request)}
${h.hidden('remove_settings', 'true')}
<b-button type="is-danger"
native-type="submit"
:disabled="purgingSettings"
icon-pack="fas"
icon-left="trash"
@click="purgingSettings = true">
{{ purgingSettings ? "Working, please wait..." : "Remove All Settings for ${config_title}" }}
</b-button>
${h.end_form()}
</footer>
</div>
</b-modal>
</%def>
<%def name="buttons_content()">
<div class="level">
<div class="level-left">
<div class="level-item">
${self.intro_message()}
</div>
<div class="level-item">
${self.save_undo_buttons()}
</div>
</div>
<div class="level-right">
<div class="level-item">
${self.purge_button()}
</div>
</div>
</div>
</%def>
<%def name="intro_message()">
<p class="block">
This page lets you modify the settings for ${config_title}.
</p>
</%def>
<%def name="save_undo_buttons()">
<div class="buttons"
v-if="settingsNeedSaved">
<b-button type="is-primary"
@click="saveSettings"
:disabled="savingSettings"
icon-pack="fas"
icon-left="save">
{{ savingSettings ? "Working, please wait..." : "Save All Settings" }}
</b-button>
<b-button tag="a" href="${request.current_route_url()}"
icon-pack="fas"
icon-left="undo"
@click="undoChanges = true"
:disabled="undoChanges">
{{ undoChanges ? "Working, please wait..." : "Undo All Changes" }}
</b-button>
</div>
</%def>
<%def name="purge_button()">
<b-button type="is-danger"
@click="purgeSettingsShowDialog = true"
icon-pack="fas"
icon-left="trash">
Remove All Settings
</b-button>
</%def>
<%def name="form_content()">
<b-notification type="is-warning"
:closable="false">
<h4 class="block is-size-4">
TODO: you must define the
<span class="is-family-monospace">&lt;%def name="form_content()"&gt;</span>
template block
</h4>
<p class="block">
or if you need more control, define the
<span class="is-family-monospace">&lt;%def name="page_content()"&gt;</span>
template block
</p>
<p class="block">
for a real-world example see template at
<span class="is-family-monospace">wuttaweb:templates/appinfo/configure.mako</span>
</p>
</b-notification>
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script>
% if simple_settings is not Undefined:
ThisPageData.simpleSettings = ${json.dumps(simple_settings)|n}
% endif
ThisPageData.purgeSettingsShowDialog = false
ThisPageData.purgingSettings = false
ThisPageData.settingsNeedSaved = false
ThisPageData.undoChanges = false
ThisPageData.savingSettings = false
ThisPage.methods.saveSettings = function() {
this.savingSettings = true
this.$refs.saveSettingsForm.submit()
}
// nb. this is here to avoid auto-submitting form when user
// presses ENTER while some random input field has focus
ThisPage.methods.saveSettingsFormSubmit = function(event) {
if (!this.savingSettings) {
event.preventDefault()
}
}
// cf. https://stackoverflow.com/a/56551646
ThisPage.methods.beforeWindowUnload = function(e) {
if (this.settingsNeedSaved && !this.savingSettings && !this.undoChanges) {
e.preventDefault()
e.returnValue = ''
}
}
ThisPage.created = function() {
window.addEventListener('beforeunload', this.beforeWindowUnload)
}
</script>
</%def>
${parent.body()}

View file

@ -2,6 +2,7 @@
<script type="text/x-template" id="${form.vue_tagname}-template">
${h.form(form.action_url, method='post', enctype='multipart/form-data', **form_attrs)}
${h.csrf_token(request)}
<section>
% for fieldname in form:

View file

@ -0,0 +1,9 @@
## -*- coding: utf-8; -*-
<%inherit file="/configure.mako" />
## NB. /master/configure.mako is only a placeholder.
## there is no reason to *inherit* from this template;
## you can always just inherit from /configure.mako
${parent.body()}

View file

@ -26,6 +26,8 @@ Web Utilities
import importlib
from webhelpers2.html import HTML, tags
def get_form_data(request):
"""
@ -257,3 +259,44 @@ def get_liburl(
elif key == 'bb_vue_fontawesome':
return f'https://cdn.jsdelivr.net/npm/@fortawesome/vue-fontawesome@{version}/+esm'
def get_csrf_token(request):
"""
Convenience function, returns the effective CSRF token (raw
string) for the given request.
See also :func:`render_csrf_token()`.
"""
token = request.session.get_csrf_token()
if token is None:
token = request.session.new_csrf_token()
return token
def render_csrf_token(request, name='_csrf'):
"""
Convenience function, returns CSRF hidden input inside hidden div,
e.g.:
.. code-block:: html
<div style="display: none;">
<input type="hidden" name="_csrf" value="TOKEN" />
</div>
This function is part of :mod:`wuttaweb.helpers` (as
:func:`~wuttaweb.helpers.csrf_token()`) which means you can do
this in page templates:
.. code-block:: mako
${h.form(request.current_route_url())}
${h.csrf_token(request)}
<!-- other fields etc. -->
${h.end_form()}
See also :func:`get_csrf_token()`.
"""
token = get_csrf_token(request)
return HTML.tag('div', tags.hidden(name, value=token), style='display:none;')

View file

@ -27,6 +27,8 @@ Base Logic for Master Views
from pyramid.renderers import render_to_response
from wuttaweb.views import View
from wuttaweb.util import get_form_data
from wuttaweb.db import Session
class MasterView(View):
@ -98,6 +100,14 @@ class MasterView(View):
Code should not access this directly but instead call
:meth:`get_model_title_plural()`.
.. attribute:: config_title
Optional override for the view's "config" title, e.g. ``"Wutta
Widgets"`` (to be displayed as **Configure Wutta Widgets**).
Code should not access this directly but instead call
:meth:`get_config_title()`.
.. attribute:: route_prefix
Optional override for the view's route prefix,
@ -125,17 +135,29 @@ class MasterView(View):
.. attribute:: listable
Boolean indicating whether the view model supports "listing" -
i.e. it should have an :meth:`index()` view.
i.e. it should have an :meth:`index()` view. Default value is
``True``.
.. attribute:: configurable
Boolean indicating whether the master view supports
"configuring" - i.e. it should have a :meth:`configure()` view.
Default value is ``False``.
"""
##############################
# attributes
##############################
# features
listable = True
configurable = False
# current action
configuring = False
##############################
# view methods
# index methods
##############################
def index(self):
@ -145,8 +167,304 @@ class MasterView(View):
This is the "default" view for the model and is what user sees
when visiting the "root" path under the :attr:`url_prefix`,
e.g. ``/widgets/``.
By default, this view is included only if :attr:`listable` is
true.
"""
return self.render_to_response('index', {})
context = {
'index_url': None, # avoid title link since this *is* the index
}
return self.render_to_response('index', context)
##############################
# configure methods
##############################
def configure(self):
"""
View for configuring aspects of the app which are pertinent to
this master view and/or model.
By default, this view is included only if :attr:`configurable`
is true. It usually maps to a URL like ``/widgets/configure``.
The expected workflow is as follows:
* user navigates to Configure page
* user modifies settings and clicks Save
* this view then *deletes* all "known" settings
* then it saves user-submitted settings
That is unless ``remove_settings`` is requested, in which case
settings are deleted but then none are saved. The "known"
settings by default include only the "simple" settings.
As a general rule, a particular setting should be configurable
by (at most) one master view. Some settings may never be
exposed at all. But when exposing a setting, careful thought
should be given to where it logically/best belongs.
Some settings are "simple" and a master view subclass need
only provide their basic definitions via
:meth:`configure_get_simple_settings()`. If complex settings
are needed, subclass must override one or more other methods
to achieve the aim(s).
See also related methods, used by this one:
* :meth:`configure_get_simple_settings()`
* :meth:`configure_get_context()`
* :meth:`configure_gather_settings()`
* :meth:`configure_remove_settings()`
* :meth:`configure_save_settings()`
"""
self.configuring = True
config_title = self.get_config_title()
# was form submitted?
if self.request.method == 'POST':
# maybe just remove settings
if self.request.POST.get('remove_settings'):
self.configure_remove_settings()
self.request.session.flash(f"All settings for {config_title} have been removed.",
'warning')
# reload configure page
return self.redirect(self.request.current_route_url())
# gather/save settings
data = get_form_data(self.request)
settings = self.configure_gather_settings(data)
self.configure_remove_settings()
self.configure_save_settings(settings)
self.request.session.flash("Settings have been saved.")
# reload configure page
return self.redirect(self.request.current_route_url())
# render configure page
context = self.configure_get_context()
return self.render_to_response('configure', context)
def configure_get_context(
self,
simple_settings=None,
):
"""
Returns the full context dict, for rendering the
:meth:`configure()` page template.
Default context will include ``simple_settings`` (normalized
to just name/value).
You may need to override this method, to add additional
"complex" settings etc.
:param simple_settings: Optional list of simple settings, if
already initialized. Otherwise it is retrieved via
:meth:`configure_get_simple_settings()`.
:returns: Context dict for the page template.
"""
context = {}
# simple settings
if simple_settings is None:
simple_settings = self.configure_get_simple_settings()
if simple_settings:
# we got some, so "normalize" each definition to name/value
normalized = {}
for simple in simple_settings:
# name
name = simple['name']
# value
if 'value' in simple:
value = simple['value']
elif simple.get('type') is bool:
value = self.config.get_bool(name, default=simple.get('default', False))
else:
value = self.config.get(name)
normalized[name] = value
# add to template context
context['simple_settings'] = normalized
return context
def configure_get_simple_settings(self):
"""
This should return a list of "simple" setting definitions for
the :meth:`configure()` view, which can be handled in a more
automatic way. (This is as opposed to some settings which are
more complex and must be handled manually; those should not be
part of this method's return value.)
Basically a "simple" setting is one which can be represented
by a single field/widget on the Configure page.
The setting definitions returned must each be a dict of
"attributes" for the setting. For instance a *very* simple
setting might be::
{'name': 'wutta.app_title'}
The ``name`` is required, everything else is optional. Here
is a more complete example::
{
'name': 'wutta.production',
'type': bool,
'default': False,
'save_if_empty': False,
}
Note that if specified, the ``default`` should be of the same
data type as defined for the setting (``bool`` in the above
example). The default ``type`` is ``str``.
Normally if a setting's value is effectively null, the setting
is removed instead of keeping it in the DB. This behavior can
be changed per-setting via the ``save_if_empty`` flag.
:returns: List of setting definition dicts as described above.
Note that their order does not matter since the template
must explicitly define field layout etc.
"""
def configure_gather_settings(
self,
data,
simple_settings=None,
):
"""
Collect the full set of "normalized" settings from user
request, so that :meth:`configure()` can save them.
Settings are gathered from the given request (e.g. POST)
``data``, but also taking into account what we know based on
the simple setting definitions.
Subclass may need to override this method if complex settings
are required.
:param data: Form data submitted via POST request.
:param simple_settings: Optional list of simple settings, if
already initialized. Otherwise it is retrieved via
:meth:`configure_get_simple_settings()`.
This method must return a list of normalized settings, similar
in spirit to the definition syntax used in
:meth:`configure_get_simple_settings()`. However the format
returned here is minimal and contains just name/value::
{
'name': 'wutta.app_title',
'value': 'Wutta Wutta',
}
Note that the ``value`` will always be a string.
Also note, whereas it's possible ``data`` will not contain all
known settings, the return value *should* (potentially)
contain all of them.
The one exception is when a simple setting has null value, by
default it will not be included in the result (hence, not
saved to DB) unless the setting definition has the
``save_if_empty`` flag set.
"""
settings = []
# simple settings
if simple_settings is None:
simple_settings = self.configure_get_simple_settings()
if simple_settings:
# we got some, so "normalize" each definition to name/value
for simple in simple_settings:
name = simple['name']
if name in data:
value = data[name]
else:
value = simple.get('default')
if simple.get('type') is bool:
value = str(bool(value)).lower()
elif simple.get('type') is int:
value = str(int(value or '0'))
elif value is None:
value = ''
else:
value = str(value)
# only want to save this setting if we received a
# value, or if empty values are okay to save
if value or simple.get('save_if_empty'):
settings.append({'name': name,
'value': value})
return settings
def configure_remove_settings(
self,
simple_settings=None,
):
"""
Remove all "known" settings from the DB; this is called by
:meth:`configure()`.
The point of this method is to ensure *all* "known" settings
which are managed by this master view, are purged from the DB.
The default logic can handle this automatically for simple
settings; subclass must override for any complex settings.
:param simple_settings: Optional list of simple settings, if
already initialized. Otherwise it is retrieved via
:meth:`configure_get_simple_settings()`.
"""
names = []
# simple settings
if simple_settings is None:
simple_settings = self.configure_get_simple_settings()
if simple_settings:
names.extend([simple['name']
for simple in simple_settings])
if names:
# nb. must avoid self.Session here in case that does not
# point to our primary app DB
session = Session()
for name in names:
self.app.delete_setting(session, name)
def configure_save_settings(self, settings):
"""
Save the given settings to the DB; this is called by
:meth:`configure()`.
This method expected a list of name/value dicts and will
simply save each to the DB, with no "conversion" logic.
:param settings: List of normalized setting definitions, as
returned by :meth:`configure_gather_settings()`.
"""
# app = self.get_rattail_app()
# nb. must avoid self.Session here in case that does not point
# to our primary app DB
session = Session()
for setting in settings:
self.app.save_setting(session, setting['name'], setting['value'],
force_create=True)
##############################
# support methods
@ -162,6 +480,16 @@ class MasterView(View):
"""
return self.get_model_title_plural()
def get_index_url(self, **kwargs):
"""
Returns the URL for master's :meth:`index()` view.
NB. this returns ``None`` if :attr:`listable` is false.
"""
if self.listable:
route_prefix = self.get_route_prefix()
return self.request.route_url(route_prefix, **kwargs)
def render_to_response(self, template, context):
"""
Locate and render an appropriate template, with the given
@ -192,7 +520,11 @@ class MasterView(View):
:returns: Response object containing the rendered template.
"""
defaults = {
'master': self,
'route_prefix': self.get_route_prefix(),
'index_title': self.get_index_title(),
'index_url': self.get_index_url(),
'config_title': self.get_config_title(),
}
# merge defaults + caller-provided context
@ -406,6 +738,26 @@ class MasterView(View):
return cls.get_url_prefix()
@classmethod
def get_config_title(cls):
"""
Returns the "config title" for the view/model.
The config title is used for page title in the
:meth:`configure()` view, as well as links to it. It is
usually plural, e.g. ``"Wutta Widgets"`` in which case that
winds up being displayed in the web app as: **Configure Wutta
Widgets**
The default logic will call :meth:`get_model_title_plural()`
and return that as-is. A subclass may override by assigning
:attr:`config_title`.
"""
if hasattr(cls, 'config_title'):
return cls.config_title
return cls.get_model_title_plural()
##############################
# configuration
##############################
@ -436,8 +788,15 @@ class MasterView(View):
route_prefix = cls.get_route_prefix()
url_prefix = cls.get_url_prefix()
# index view
# index
if cls.listable:
config.add_route(route_prefix, f'{url_prefix}/')
config.add_view(cls, attr='index',
route_name=route_prefix)
# configure
if cls.configurable:
config.add_route(f'{route_prefix}.configure',
f'{url_prefix}/configure')
config.add_view(cls, attr='configure',
route_name=f'{route_prefix}.configure')

View file

@ -29,11 +29,26 @@ from wuttaweb.views import MasterView
class AppInfoView(MasterView):
"""
Master view for the overall app, to show/edit config etc.
Master view for the core app info, to show/edit config etc.
Notable URLs provided by this class:
* ``/appinfo/``
* ``/appinfo/configure``
"""
model_name = 'AppInfo'
model_title_plural = "App Info"
route_prefix = 'appinfo'
configurable = True
def configure_get_simple_settings(self):
""" """
return [
# basics
{'name': f'{self.app.appname}.app_title'},
]
def defaults(config, **kwargs):

View file

@ -8,6 +8,17 @@ from pyramid.config import Configurator
from pyramid.router import Router
from wuttaweb import app as mod
from wuttjamaican.conf import WuttaConfig
class TestWebAppProvider(TestCase):
def test_basic(self):
# nb. just normal usage here, confirm it does the one thing we
# need it to..
config = WuttaConfig()
app = config.get_app()
handler = app.get_web_handler()
class TestMakeWuttaConfig(FileConfigTestCase):

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8; -*-
from unittest import TestCase
from unittest.mock import patch
from pyramid import testing
@ -290,3 +291,45 @@ class TestGetFormData(TestCase):
request = self.make_request(POST=None, content_type='application/json')
data = util.get_form_data(request)
self.assertEqual(data, {'foo2': 'baz'})
class TestGetCsrfToken(TestCase):
def setUp(self):
self.config = WuttaConfig()
self.request = testing.DummyRequest(wutta_config=self.config)
def test_same_token(self):
# same token returned for same request
# TODO: dummy request is always returning same token!
# so this isn't really testing anything.. :(
first = util.get_csrf_token(self.request)
self.assertIsNotNone(first)
second = util.get_csrf_token(self.request)
self.assertEqual(first, second)
# TODO: ideally would make a new request here and confirm it
# gets a different token, but see note above..
def test_new_token(self):
# nb. dummy request always returns same token, so must
# trick it into thinking it doesn't have one yet
with patch.object(self.request.session, 'get_csrf_token', return_value=None):
token = util.get_csrf_token(self.request)
self.assertIsNotNone(token)
class TestRenderCsrfToken(TestCase):
def setUp(self):
self.config = WuttaConfig()
self.request = testing.DummyRequest(wutta_config=self.config)
def test_basics(self):
html = util.render_csrf_token(self.request)
self.assertIn('type="hidden"', html)
self.assertIn('name="_csrf"', html)
token = util.get_csrf_token(self.request)
self.assertIn(f'value="{token}"', html)

View file

@ -1,39 +1,21 @@
# -*- coding: utf-8; -*-
import functools
from unittest import TestCase
from unittest.mock import MagicMock
from unittest.mock import MagicMock, patch
from pyramid import testing
from pyramid.response import Response
from pyramid.httpexceptions import HTTPFound
from wuttjamaican.conf import WuttaConfig
from wuttaweb.views import master
from wuttaweb.subscribers import new_request_set_user
from tests.views.utils import WebTestCase
class TestMasterView(TestCase):
def setUp(self):
self.config = WuttaConfig(defaults={
'wutta.web.menus.handler_spec': 'tests.utils:NullMenuHandler',
})
self.app = self.config.get_app()
self.request = testing.DummyRequest(wutta_config=self.config, use_oruga=False)
self.pyramid_config = testing.setUp(request=self.request, settings={
'wutta_config': self.config,
'mako.directories': ['wuttaweb:templates'],
})
self.pyramid_config.include('pyramid_mako')
self.pyramid_config.include('wuttaweb.static')
self.pyramid_config.include('wuttaweb.views.essential')
self.pyramid_config.add_subscriber('wuttaweb.subscribers.before_render',
'pyramid.events.BeforeRender')
event = MagicMock(request=self.request)
new_request_set_user(event)
def tearDown(self):
testing.tearDown()
class TestMasterView(WebTestCase):
def test_defaults(self):
master.MasterView.model_name = 'Widget'
@ -233,6 +215,37 @@ class TestMasterView(TestCase):
self.assertEqual(master.MasterView.get_template_prefix(), '/machines')
del master.MasterView.model_class
def test_get_config_title(self):
# error by default (since no model class)
self.assertRaises(AttributeError, master.MasterView.get_config_title)
# subclass may specify config title
master.MasterView.config_title = 'Widgets'
self.assertEqual(master.MasterView.get_config_title(), "Widgets")
del master.MasterView.config_title
# subclass may specify *plural* model title
master.MasterView.model_title_plural = 'People'
self.assertEqual(master.MasterView.get_config_title(), "People")
del master.MasterView.model_title_plural
# or it may specify *singular* model title
master.MasterView.model_title = 'Wutta Widget'
self.assertEqual(master.MasterView.get_config_title(), "Wutta Widgets")
del master.MasterView.model_title
# or it may specify model name
master.MasterView.model_name = 'Blaster'
self.assertEqual(master.MasterView.get_config_title(), "Blasters")
del master.MasterView.model_name
# or it may specify model class
MyModel = MagicMock(__name__='Dinosaur')
master.MasterView.model_class = MyModel
self.assertEqual(master.MasterView.get_config_title(), "Dinosaurs")
del master.MasterView.model_class
##############################
# support methods
##############################
@ -245,6 +258,10 @@ class TestMasterView(TestCase):
def test_render_to_response(self):
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)
master.MasterView.model_name = 'Widget'
@ -255,12 +272,14 @@ class TestMasterView(TestCase):
# basic sanity check using /appinfo/index.mako
master.MasterView.model_name = 'AppInfo'
master.MasterView.template_prefix = '/appinfo'
master.MasterView.route_prefix = 'appinfo'
master.MasterView.url_prefix = '/appinfo'
view = master.MasterView(self.request)
response = view.render_to_response('index', {})
self.assertIsInstance(response, Response)
del master.MasterView.model_name
del master.MasterView.template_prefix
del master.MasterView.route_prefix
del master.MasterView.url_prefix
# bad template name causes error
master.MasterView.model_name = 'Widget'
@ -275,8 +294,77 @@ class TestMasterView(TestCase):
# basic sanity check using /appinfo
master.MasterView.model_name = 'AppInfo'
master.MasterView.route_prefix = 'appinfo'
master.MasterView.template_prefix = '/appinfo'
view = master.MasterView(self.request)
response = view.index()
del master.MasterView.model_name
del master.MasterView.route_prefix
del master.MasterView.template_prefix
def test_configure(self):
model = self.app.model
# setup
master.MasterView.model_name = 'AppInfo'
master.MasterView.route_prefix = 'appinfo'
master.MasterView.template_prefix = '/appinfo'
# 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 = master.MasterView(self.request)
with patch.object(self.request, 'current_route_url',
return_value='/appinfo/configure'):
with patch.object(master.MasterView, 'configure_get_simple_settings',
return_value=settings):
with patch.object(master, 'Session', return_value=self.session):
# get the form page
response = view.configure()
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()
# nb. should get redirect back to configure page
self.assertIsInstance(response, HTTPFound)
# 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()
# nb. should get redirect back to configure page
self.assertIsInstance(response, HTTPFound)
# should now have 0 settings
count = self.session.query(model.Setting).count()
self.assertEqual(count, 0)
# teardown
del master.MasterView.model_name
del master.MasterView.route_prefix
del master.MasterView.template_prefix

View file

@ -11,3 +11,8 @@ class TestAppInfoView(WebTestCase):
# just a sanity check
view = settings.AppInfoView(self.request)
response = view.index()
def test_configure_get_simple_settings(self):
# just a sanity check
view = settings.AppInfoView(self.request)
simple = view.configure_get_simple_settings()