feat: add basic configure view for appinfo
This commit is contained in:
parent
dd207a4a05
commit
ed67cdb2d8
|
@ -110,7 +110,11 @@ def make_pyramid_config(settings):
|
||||||
|
|
||||||
The config is initialized with certain features deemed useful for
|
The config is initialized with certain features deemed useful for
|
||||||
all apps.
|
all apps.
|
||||||
|
|
||||||
|
:returns: Instance of
|
||||||
|
:class:`pyramid:pyramid.config.Configurator`.
|
||||||
"""
|
"""
|
||||||
|
settings.setdefault('mako.directories', ['wuttaweb:templates'])
|
||||||
settings.setdefault('pyramid_deform.template_search_path',
|
settings.setdefault('pyramid_deform.template_search_path',
|
||||||
'wuttaweb:templates/deform')
|
'wuttaweb:templates/deform')
|
||||||
|
|
||||||
|
@ -119,6 +123,11 @@ def make_pyramid_config(settings):
|
||||||
# configure user authorization / authentication
|
# configure user authorization / authentication
|
||||||
pyramid_config.set_security_policy(WuttaSecurityPolicy())
|
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_beaker')
|
||||||
pyramid_config.include('pyramid_deform')
|
pyramid_config.include('pyramid_deform')
|
||||||
pyramid_config.include('pyramid_mako')
|
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
|
will need to define their own ``main()`` function, and use that
|
||||||
instead.
|
instead.
|
||||||
"""
|
"""
|
||||||
settings.setdefault('mako.directories', ['wuttaweb:templates'])
|
|
||||||
|
|
||||||
wutta_config = make_wutta_config(settings)
|
wutta_config = make_wutta_config(settings)
|
||||||
pyramid_config = make_pyramid_config(settings)
|
pyramid_config = make_pyramid_config(settings)
|
||||||
|
|
||||||
|
|
|
@ -323,6 +323,7 @@ class Form:
|
||||||
"""
|
"""
|
||||||
context['form'] = self
|
context['form'] = self
|
||||||
context.setdefault('form_attrs', {})
|
context.setdefault('form_attrs', {})
|
||||||
|
context.setdefault('request', self.request)
|
||||||
|
|
||||||
# auto disable button on submit
|
# auto disable button on submit
|
||||||
if self.auto_disable_submit:
|
if self.auto_disable_submit:
|
||||||
|
|
|
@ -38,12 +38,20 @@ instance:
|
||||||
|
|
||||||
This module contains the following references:
|
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`
|
||||||
* all names from :mod:`webhelpers2:webhelpers2.html.tags`
|
* 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 import *
|
||||||
from webhelpers2.html.tags 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
|
||||||
|
|
21
src/wuttaweb/templates/appinfo/configure.mako
Normal file
21
src/wuttaweb/templates/appinfo/configure.mako
Normal 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()}
|
|
@ -209,16 +209,14 @@
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</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">
|
<div class="level-left">
|
||||||
|
|
||||||
## Current Context
|
## Current Context
|
||||||
<div id="current-context" class="level-item">
|
<div id="current-context" class="level-item">
|
||||||
% if index_title:
|
% if index_title:
|
||||||
% if index_url:
|
% if index_url:
|
||||||
<span class="header-text">
|
<h1 class="title">${h.link_to(index_title, index_url)}</h1>
|
||||||
${h.link_to(index_title, index_url)}
|
|
||||||
</span>
|
|
||||||
% else:
|
% else:
|
||||||
<h1 class="title">${index_title}</h1>
|
<h1 class="title">${index_title}</h1>
|
||||||
% endif
|
% endif
|
||||||
|
@ -226,6 +224,23 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div><!-- level-left -->
|
</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 -->
|
</nav><!-- level -->
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
@ -318,8 +333,7 @@
|
||||||
<div class="navbar-dropdown">
|
<div class="navbar-dropdown">
|
||||||
% if request.is_root:
|
% if request.is_root:
|
||||||
${h.form(url('stop_root'), ref='stopBeingRootForm')}
|
${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()}" />
|
<input type="hidden" name="referrer" value="${request.current_route_url()}" />
|
||||||
<a @click="stopBeingRoot()"
|
<a @click="stopBeingRoot()"
|
||||||
class="navbar-item has-background-danger has-text-white">
|
class="navbar-item has-background-danger has-text-white">
|
||||||
|
@ -328,8 +342,7 @@
|
||||||
${h.end_form()}
|
${h.end_form()}
|
||||||
% elif request.is_admin:
|
% elif request.is_admin:
|
||||||
${h.form(url('become_root'), ref='startBeingRootForm')}
|
${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()}" />
|
<input type="hidden" name="referrer" value="${request.current_route_url()}" />
|
||||||
<a @click="startBeingRoot()"
|
<a @click="startBeingRoot()"
|
||||||
class="navbar-item has-background-danger has-text-white">
|
class="navbar-item has-background-danger has-text-white">
|
||||||
|
|
181
src/wuttaweb/templates/configure.mako
Normal file
181
src/wuttaweb/templates/configure.mako
Normal 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. 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"><%def name="form_content()"></span>
|
||||||
|
template block
|
||||||
|
</h4>
|
||||||
|
<p class="block">
|
||||||
|
or if you need more control, define the
|
||||||
|
<span class="is-family-monospace"><%def name="page_content()"></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()}
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
<script type="text/x-template" id="${form.vue_tagname}-template">
|
<script type="text/x-template" id="${form.vue_tagname}-template">
|
||||||
${h.form(form.action_url, method='post', enctype='multipart/form-data', **form_attrs)}
|
${h.form(form.action_url, method='post', enctype='multipart/form-data', **form_attrs)}
|
||||||
|
${h.csrf_token(request)}
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
% for fieldname in form:
|
% for fieldname in form:
|
||||||
|
|
9
src/wuttaweb/templates/master/configure.mako
Normal file
9
src/wuttaweb/templates/master/configure.mako
Normal 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()}
|
|
@ -26,6 +26,8 @@ Web Utilities
|
||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
|
|
||||||
|
from webhelpers2.html import HTML, tags
|
||||||
|
|
||||||
|
|
||||||
def get_form_data(request):
|
def get_form_data(request):
|
||||||
"""
|
"""
|
||||||
|
@ -257,3 +259,44 @@ def get_liburl(
|
||||||
|
|
||||||
elif key == 'bb_vue_fontawesome':
|
elif key == 'bb_vue_fontawesome':
|
||||||
return f'https://cdn.jsdelivr.net/npm/@fortawesome/vue-fontawesome@{version}/+esm'
|
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;')
|
||||||
|
|
|
@ -27,6 +27,8 @@ Base Logic for Master Views
|
||||||
from pyramid.renderers import render_to_response
|
from pyramid.renderers import render_to_response
|
||||||
|
|
||||||
from wuttaweb.views import View
|
from wuttaweb.views import View
|
||||||
|
from wuttaweb.util import get_form_data
|
||||||
|
from wuttaweb.db import Session
|
||||||
|
|
||||||
|
|
||||||
class MasterView(View):
|
class MasterView(View):
|
||||||
|
@ -98,6 +100,14 @@ class MasterView(View):
|
||||||
Code should not access this directly but instead call
|
Code should not access this directly but instead call
|
||||||
:meth:`get_model_title_plural()`.
|
: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
|
.. attribute:: route_prefix
|
||||||
|
|
||||||
Optional override for the view's route prefix,
|
Optional override for the view's route prefix,
|
||||||
|
@ -125,17 +135,29 @@ class MasterView(View):
|
||||||
.. attribute:: listable
|
.. attribute:: listable
|
||||||
|
|
||||||
Boolean indicating whether the view model supports "listing" -
|
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
|
# attributes
|
||||||
##############################
|
##############################
|
||||||
|
|
||||||
|
# features
|
||||||
listable = True
|
listable = True
|
||||||
|
configurable = False
|
||||||
|
|
||||||
|
# current action
|
||||||
|
configuring = False
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
# view methods
|
# index methods
|
||||||
##############################
|
##############################
|
||||||
|
|
||||||
def index(self):
|
def index(self):
|
||||||
|
@ -145,8 +167,304 @@ class MasterView(View):
|
||||||
This is the "default" view for the model and is what user sees
|
This is the "default" view for the model and is what user sees
|
||||||
when visiting the "root" path under the :attr:`url_prefix`,
|
when visiting the "root" path under the :attr:`url_prefix`,
|
||||||
e.g. ``/widgets/``.
|
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
|
# support methods
|
||||||
|
@ -162,6 +480,16 @@ class MasterView(View):
|
||||||
"""
|
"""
|
||||||
return self.get_model_title_plural()
|
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):
|
def render_to_response(self, template, context):
|
||||||
"""
|
"""
|
||||||
Locate and render an appropriate template, with the given
|
Locate and render an appropriate template, with the given
|
||||||
|
@ -192,7 +520,11 @@ class MasterView(View):
|
||||||
:returns: Response object containing the rendered template.
|
:returns: Response object containing the rendered template.
|
||||||
"""
|
"""
|
||||||
defaults = {
|
defaults = {
|
||||||
|
'master': self,
|
||||||
|
'route_prefix': self.get_route_prefix(),
|
||||||
'index_title': self.get_index_title(),
|
'index_title': self.get_index_title(),
|
||||||
|
'index_url': self.get_index_url(),
|
||||||
|
'config_title': self.get_config_title(),
|
||||||
}
|
}
|
||||||
|
|
||||||
# merge defaults + caller-provided context
|
# merge defaults + caller-provided context
|
||||||
|
@ -406,6 +738,26 @@ class MasterView(View):
|
||||||
|
|
||||||
return cls.get_url_prefix()
|
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
|
# configuration
|
||||||
##############################
|
##############################
|
||||||
|
@ -436,8 +788,15 @@ class MasterView(View):
|
||||||
route_prefix = cls.get_route_prefix()
|
route_prefix = cls.get_route_prefix()
|
||||||
url_prefix = cls.get_url_prefix()
|
url_prefix = cls.get_url_prefix()
|
||||||
|
|
||||||
# index view
|
# index
|
||||||
if cls.listable:
|
if cls.listable:
|
||||||
config.add_route(route_prefix, f'{url_prefix}/')
|
config.add_route(route_prefix, f'{url_prefix}/')
|
||||||
config.add_view(cls, attr='index',
|
config.add_view(cls, attr='index',
|
||||||
route_name=route_prefix)
|
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')
|
||||||
|
|
|
@ -29,11 +29,26 @@ from wuttaweb.views import MasterView
|
||||||
|
|
||||||
class AppInfoView(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_name = 'AppInfo'
|
||||||
model_title_plural = "App Info"
|
model_title_plural = "App Info"
|
||||||
route_prefix = 'appinfo'
|
route_prefix = 'appinfo'
|
||||||
|
configurable = True
|
||||||
|
|
||||||
|
def configure_get_simple_settings(self):
|
||||||
|
""" """
|
||||||
|
return [
|
||||||
|
|
||||||
|
# basics
|
||||||
|
{'name': f'{self.app.appname}.app_title'},
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs):
|
def defaults(config, **kwargs):
|
||||||
|
|
|
@ -8,6 +8,17 @@ from pyramid.config import Configurator
|
||||||
from pyramid.router import Router
|
from pyramid.router import Router
|
||||||
|
|
||||||
from wuttaweb import app as mod
|
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):
|
class TestMakeWuttaConfig(FileConfigTestCase):
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# -*- coding: utf-8; -*-
|
# -*- coding: utf-8; -*-
|
||||||
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from pyramid import testing
|
from pyramid import testing
|
||||||
|
|
||||||
|
@ -290,3 +291,45 @@ class TestGetFormData(TestCase):
|
||||||
request = self.make_request(POST=None, content_type='application/json')
|
request = self.make_request(POST=None, content_type='application/json')
|
||||||
data = util.get_form_data(request)
|
data = util.get_form_data(request)
|
||||||
self.assertEqual(data, {'foo2': 'baz'})
|
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)
|
||||||
|
|
|
@ -1,39 +1,21 @@
|
||||||
# -*- coding: utf-8; -*-
|
# -*- coding: utf-8; -*-
|
||||||
|
|
||||||
|
import functools
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from pyramid import testing
|
from pyramid import testing
|
||||||
from pyramid.response import Response
|
from pyramid.response import Response
|
||||||
|
from pyramid.httpexceptions import HTTPFound
|
||||||
|
|
||||||
from wuttjamaican.conf import WuttaConfig
|
from wuttjamaican.conf import WuttaConfig
|
||||||
from wuttaweb.views import master
|
from wuttaweb.views import master
|
||||||
from wuttaweb.subscribers import new_request_set_user
|
from wuttaweb.subscribers import new_request_set_user
|
||||||
|
|
||||||
|
from tests.views.utils import WebTestCase
|
||||||
|
|
||||||
class TestMasterView(TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
class TestMasterView(WebTestCase):
|
||||||
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()
|
|
||||||
|
|
||||||
def test_defaults(self):
|
def test_defaults(self):
|
||||||
master.MasterView.model_name = 'Widget'
|
master.MasterView.model_name = 'Widget'
|
||||||
|
@ -233,6 +215,37 @@ class TestMasterView(TestCase):
|
||||||
self.assertEqual(master.MasterView.get_template_prefix(), '/machines')
|
self.assertEqual(master.MasterView.get_template_prefix(), '/machines')
|
||||||
del master.MasterView.model_class
|
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
|
# support methods
|
||||||
##############################
|
##############################
|
||||||
|
@ -245,6 +258,10 @@ class TestMasterView(TestCase):
|
||||||
|
|
||||||
def test_render_to_response(self):
|
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
|
# basic sanity check using /master/index.mako
|
||||||
# (nb. it skips /widgets/index.mako since that doesn't exist)
|
# (nb. it skips /widgets/index.mako since that doesn't exist)
|
||||||
master.MasterView.model_name = 'Widget'
|
master.MasterView.model_name = 'Widget'
|
||||||
|
@ -255,12 +272,14 @@ class TestMasterView(TestCase):
|
||||||
|
|
||||||
# basic sanity check using /appinfo/index.mako
|
# basic sanity check using /appinfo/index.mako
|
||||||
master.MasterView.model_name = 'AppInfo'
|
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)
|
view = master.MasterView(self.request)
|
||||||
response = view.render_to_response('index', {})
|
response = view.render_to_response('index', {})
|
||||||
self.assertIsInstance(response, Response)
|
self.assertIsInstance(response, Response)
|
||||||
del master.MasterView.model_name
|
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
|
# bad template name causes error
|
||||||
master.MasterView.model_name = 'Widget'
|
master.MasterView.model_name = 'Widget'
|
||||||
|
@ -275,8 +294,77 @@ class TestMasterView(TestCase):
|
||||||
|
|
||||||
# basic sanity check using /appinfo
|
# basic sanity check using /appinfo
|
||||||
master.MasterView.model_name = 'AppInfo'
|
master.MasterView.model_name = 'AppInfo'
|
||||||
|
master.MasterView.route_prefix = 'appinfo'
|
||||||
master.MasterView.template_prefix = '/appinfo'
|
master.MasterView.template_prefix = '/appinfo'
|
||||||
view = master.MasterView(self.request)
|
view = master.MasterView(self.request)
|
||||||
response = view.index()
|
response = view.index()
|
||||||
del master.MasterView.model_name
|
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
|
del master.MasterView.template_prefix
|
||||||
|
|
|
@ -11,3 +11,8 @@ class TestAppInfoView(WebTestCase):
|
||||||
# just a sanity check
|
# just a sanity check
|
||||||
view = settings.AppInfoView(self.request)
|
view = settings.AppInfoView(self.request)
|
||||||
response = view.index()
|
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()
|
||||||
|
|
Loading…
Reference in a new issue