1
0
Fork 0

feat: add basic Edit support for CRUD master view

This commit is contained in:
Lance Edgar 2024-08-10 21:07:38 -05:00
parent 9e1fc6e57d
commit 1a8fc8dd44
12 changed files with 640 additions and 51 deletions

View file

@ -149,10 +149,35 @@ class Form:
Default for this is ``False`` in which case the ``<form>`` tag
will exist and submit is allowed.
.. attribute:: readonly_fields
Set of fields which should be readonly. Each will still be
rendered but with static value text and no widget.
This is only applicable if :attr:`readonly` is ``False``.
See also :meth:`set_readonly()`.
.. attribute:: action_url
String URL to which the form should be submitted, if applicable.
.. attribute:: cancel_url
String URL to which the Cancel button should "always" redirect,
if applicable.
Code should not access this directly, but instead call
:meth:`get_cancel_url()`.
.. attribute:: cancel_url_fallback
String URL to which the Cancel button should redirect, if
referrer cannot be determined from request.
Code should not access this directly, but instead call
:meth:`get_cancel_url()`.
.. attribute:: vue_tagname
String name for Vue component tag. By default this is
@ -180,6 +205,23 @@ class Form:
.. attribute:: show_button_reset
Flag indicating whether a Reset button should be shown.
Default is ``False``.
.. attribute:: show_button_cancel
Flag indicating whether a Cancel button should be shown.
Default is ``True``.
.. attribute:: button_label_cancel
String label for the form cancel button. Default is
``"Cancel"``.
.. attribute:: auto_disable_cancel
Flag indicating whether the cancel button should be
auto-disabled, whenever the button is clicked. Default is
``True``.
.. attribute:: validated
@ -197,26 +239,38 @@ class Form:
model_class=None,
model_instance=None,
readonly=False,
readonly_fields=[],
labels={},
action_url=None,
cancel_url=None,
cancel_url_fallback=None,
vue_tagname='wutta-form',
align_buttons_right=False,
auto_disable_submit=True,
button_label_submit="Save",
button_icon_submit='save',
show_button_reset=False,
show_button_cancel=True,
button_label_cancel="Cancel",
auto_disable_cancel=True,
):
self.request = request
self.schema = schema
self.readonly = readonly
self.readonly_fields = set(readonly_fields or [])
self.labels = labels or {}
self.action_url = action_url
self.cancel_url = cancel_url
self.cancel_url_fallback = cancel_url_fallback
self.vue_tagname = vue_tagname
self.align_buttons_right = align_buttons_right
self.auto_disable_submit = auto_disable_submit
self.button_label_submit = button_label_submit
self.button_icon_submit = button_icon_submit
self.show_button_reset = show_button_reset
self.show_button_cancel = show_button_cancel
self.button_label_cancel = button_label_cancel
self.auto_disable_cancel = auto_disable_cancel
self.config = self.request.wutta_config
self.app = self.config.get_app()
@ -262,6 +316,39 @@ class Form:
words = self.vue_tagname.split('-')
return ''.join([word.capitalize() for word in words])
def get_cancel_url(self):
"""
Returns the URL for the Cancel button.
If :attr:`cancel_url` is set, its value is returned.
Or, if the referrer can be deduced from the request, that is
returned.
Or, if :attr:`cancel_url_fallback` is set, that value is
returned.
As a last resort the "default" URL from
:func:`~wuttaweb.subscribers.request.get_referrer()` is
returned.
"""
# use "permanent" URL if set
if self.cancel_url:
return self.cancel_url
# nb. use fake default to avoid normal default logic;
# that way if we get something it's a real referrer
url = self.request.get_referrer(default='NOPE')
if url and url != 'NOPE':
return url
# use fallback URL if set
if self.cancel_url_fallback:
return self.cancel_url_fallback
# okay, home page then (or whatever is the default URL)
return self.request.get_referrer()
def set_fields(self, fields):
"""
Explicitly set the list of form fields.
@ -273,6 +360,41 @@ class Form:
"""
self.fields = FieldList(fields)
def set_readonly(self, key, readonly=True):
"""
Enable or disable the "readonly" flag for a given field.
When a field is marked readonly, it will be shown in the form
but there will be no editable widget. The field is skipped
over (not saved) when form is submitted.
See also :meth:`is_readonly()`; this is tracked via
:attr:`readonly_fields`.
:param key: String key (fieldname) for the field.
:param readonly: New readonly flag for the field.
"""
if readonly:
self.readonly_fields.add(key)
else:
if key in self.readonly_fields:
self.readonly_fields.remove(key)
def is_readonly(self, key):
"""
Returns boolean indicating if the given field is marked as
readonly.
See also :meth:`set_readonly()`.
:param key: Field key/name as string.
"""
if self.readonly_fields:
if key in self.readonly_fields:
return True
return False
def set_label(self, key, label):
"""
Set the label for given field name.
@ -381,6 +503,7 @@ class Form:
the output.
"""
context['form'] = self
context['dform'] = self.get_deform()
context.setdefault('form_attrs', {})
context.setdefault('request', self.request)
@ -408,18 +531,35 @@ class Form:
<!-- widget element(s) -->
</b-field>
"""
# readonly comes from: caller, field flag, or form flag
if readonly is None:
readonly = self.is_readonly(fieldname)
if not readonly:
readonly = self.readonly
# render the field widget or whatever
# but also, fields not in deform/schema must be readonly
dform = self.get_deform()
if not readonly and fieldname not in dform:
readonly = True
# render the field widget or whatever
if fieldname in dform:
# render proper widget if field is in deform/schema
field = dform[fieldname]
kw = {}
if readonly:
kw['readonly'] = True
html = field.serialize(**kw)
else:
# render static text if field not in deform/schema
# TODO: need to abstract this somehow
if self.model_instance:
html = str(self.model_instance[fieldname])
else:
html = ''
# mark all that as safe
html = HTML.literal(html)

View file

@ -64,7 +64,7 @@ def new_request(event):
Reference to the app :term:`config object`.
.. method:: request.get_referrer(default=None)
.. function:: request.get_referrer(default=None)
Request method to get the "canonical" HTTP referrer value.
This has logic to check for referrer in the request params,

View file

@ -1,5 +1,6 @@
## -*- coding: utf-8; -*-
<%namespace name="base_meta" file="/base_meta.mako" />
<%namespace file="/wutta-components.mako" import="make_wutta_components" />
<!DOCTYPE html>
<html lang="en">
<head>
@ -366,7 +367,21 @@
${self.render_prevnext_header_buttons()}
</%def>
<%def name="render_crud_header_buttons()"></%def>
<%def name="render_crud_header_buttons()">
% if master:
% if master.viewing:
<wutta-button once
tag="a" href="${master.get_action_url('edit', instance)}"
icon-left="edit"
label="Edit This" />
% elif master.editing:
<wutta-button once
tag="a" href="${master.get_action_url('view', instance)}"
icon-left="eye"
label="View This" />
% endif
% endif
</%def>
<%def name="render_prevnext_header_buttons()"></%def>
@ -432,6 +447,7 @@
<%def name="finalize_whole_page_vars()"></%def>
<%def name="make_whole_page_component()">
${make_wutta_components()}
${self.render_whole_page_template()}
${self.declare_whole_page_vars()}
${self.modify_whole_page_vars()}

View file

@ -13,6 +13,12 @@
% if not form.readonly:
<div style="margin-top: 1.5rem; display: flex; gap: 0.5rem; justify-content: ${'end' if form.align_buttons_right else 'start'}; width: 100%; padding-left: 10rem;">
% if form.show_button_cancel:
<wutta-button ${'once' if form.auto_disable_cancel else ''}
tag="a" href="${form.get_cancel_url()}"
label="${form.button_label_cancel}" />
% endif
% if form.show_button_reset:
<b-button native-type="reset">
Reset
@ -50,7 +56,9 @@
## field model values
% for key in form:
% if key in dform:
model_${key}: ${form.get_vue_field_value(key)|n},
% endif
% endfor
% if form.auto_disable_submit:

View file

@ -0,0 +1,9 @@
## -*- coding: utf-8; -*-
<%inherit file="/master/form.mako" />
<%def name="title()">${index_title} &raquo; ${instance_title} &raquo; Edit</%def>
<%def name="content_title()">Edit: ${instance_title}</%def>
${parent.body()}

View file

@ -0,0 +1,71 @@
<%def name="make_wutta_components()">
${self.make_wutta_button_component()}
</%def>
<%def name="make_wutta_button_component()">
<script type="text/x-template" id="wutta-button-template">
<b-button :type="type"
:native-type="nativeType"
:tag="tag"
:href="href"
:title="title"
:disabled="buttonDisabled"
@click="clicked"
icon-pack="fas"
:icon-left="iconLeft">
{{ buttonLabel }}
</b-button>
</script>
<script>
const WuttaButton = {
template: '#wutta-button-template',
props: {
type: String,
nativeType: String,
tag: String,
href: String,
label: String,
title: String,
iconLeft: String,
working: String,
workingLabel: String,
disabled: Boolean,
once: Boolean,
},
data() {
return {
currentLabel: null,
currentDisabled: null,
}
},
computed: {
buttonLabel: function() {
return this.currentLabel || this.label
},
buttonDisabled: function() {
if (this.currentDisabled !== null) {
return this.currentDisabled
}
return this.disabled
},
},
methods: {
clicked(event) {
if (this.once) {
this.currentDisabled = true
if (this.workingLabel) {
this.currentLabel = this.workingLabel
} else if (this.working) {
this.currentLabel = this.working + ", please wait..."
} else {
this.currentLabel = "Working, please wait..."
}
}
}
},
}
Vue.component('wutta-button', WuttaButton)
</script>
</%def>

View file

@ -59,13 +59,11 @@ class AuthView(View):
form = self.make_form(schema=self.login_make_schema(),
align_buttons_right=True,
show_button_cancel=False,
show_button_reset=True,
button_label_submit="Login",
button_icon_submit='user')
# TODO
# form.show_cancel = False
# validate basic form data (sanity check)
data = form.validate()
if data:
@ -155,6 +153,7 @@ class AuthView(View):
return self.redirect(self.request.route_url('home'))
form = self.make_form(schema=self.change_password_make_schema(),
show_button_cancel=False,
show_button_reset=True)
data = form.validate()

View file

@ -174,6 +174,12 @@ class MasterView(View):
i.e. it should have a :meth:`view()` view. Default value is
``True``.
.. attribute:: editable
Boolean indicating whether the view model supports "editing" -
i.e. it should have an :meth:`edit()` view. Default value is
``True``.
.. attribute:: form_fields
List of columns for the model form.
@ -195,11 +201,13 @@ class MasterView(View):
listable = True
has_grid = True
viewable = True
editable = True
configurable = False
# current action
listing = False
viewing = False
editing = False
configuring = False
##############################
@ -260,10 +268,15 @@ class MasterView(View):
actions = []
# TODO: should split this off into index_get_grid_actions() ?
if self.viewable:
actions.append(self.make_grid_action('view', icon='eye',
url=self.get_action_url_view))
if self.editable:
actions.append(self.make_grid_action('edit', icon='edit',
url=self.get_action_url_edit))
kwargs['actions'] = actions
grid = self.make_grid(**kwargs)
@ -309,21 +322,6 @@ class MasterView(View):
"""
return []
def get_action_url_view(self, obj, i):
"""
Returns the "view" grid action URL for the given object.
Most typically this is like ``/widgets/XXX`` where ``XXX``
represents the object's key/ID.
"""
route_prefix = self.get_route_prefix()
kw = {}
for key in self.get_model_key():
kw[key] = obj[key]
return self.request.route_url(f'{route_prefix}.view', **kw)
##############################
# view methods
##############################
@ -341,9 +339,12 @@ class MasterView(View):
The default view logic will show a read-only form with field
values displayed.
See also related methods, which are called by this one:
Subclass normally should not override this method, but rather
one of the related methods which are called (in)directly by
this one:
* :meth:`make_model_form()`
* :meth:`configure_form()`
"""
self.viewing = True
instance = self.get_instance()
@ -356,6 +357,71 @@ class MasterView(View):
}
return self.render_to_response('view', context)
##############################
# edit methods
##############################
def edit(self):
"""
View to "edit" details of an existing model record.
This usually corresponds to a URL like ``/widgets/XXX/edit``
where ``XXX`` represents the key/ID for the record.
By default, this view is included only if :attr:`editable` is
true.
The default "edit" view logic will show a form with field
widgets, allowing user to modify and submit new values which
are then persisted to the DB (assuming typical SQLAlchemy
model).
Subclass normally should not override this method, but rather
one of the related methods which are called (in)directly by
this one:
* :meth:`make_model_form()`
* :meth:`configure_form()`
* :meth:`edit_save_form()`
"""
self.editing = True
instance = self.get_instance()
instance_title = self.get_instance_title(instance)
form = self.make_model_form(instance,
cancel_url_fallback=self.get_index_url())
if self.request.method == 'POST':
if form.validate():
self.edit_save_form(form)
return self.redirect(self.get_action_url('view', instance))
context = {
'instance': instance,
'instance_title': instance_title,
'form': form,
}
return self.render_to_response('edit', context)
def edit_save_form(self, form):
"""
This method is responsible for "converting" the validated form
data to a model instance, and then "saving" the result,
e.g. to DB.
Subclass may override this, or any of the related methods
called by this one:
* :meth:`objectify()`
* :meth:`persist()`
:returns: This should return the resulting model instance,
which was produced by :meth:`objectify()`.
"""
obj = self.objectify(form)
self.persist(obj)
return obj
##############################
# configure methods
##############################
@ -785,6 +851,49 @@ class MasterView(View):
"""
return str(instance)
def get_action_url(self, action, obj, **kwargs):
"""
Generate an "action" URL for the given model instance.
This is a shortcut which generates a route name based on
:meth:`get_route_prefix()` and the ``action`` param.
It returns the URL based on generated route name and object's
model key values.
:param action: String name for the action, which corresponds
to part of some named route, e.g. ``'view'`` or ``'edit'``.
:param obj: Model instance object.
"""
route_prefix = self.get_route_prefix()
kw = dict([(key, obj[key])
for key in self.get_model_key()])
kw.update(kwargs)
return self.request.route_url(f'{route_prefix}.{action}', **kw)
def get_action_url_view(self, obj, i):
"""
Returns the "view" grid action URL for the given object.
Most typically this is like ``/widgets/XXX`` where ``XXX``
represents the object's key/ID.
Calls :meth:`get_action_url()` under the hood.
"""
return self.get_action_url('view', obj)
def get_action_url_edit(self, obj, i):
"""
Returns the "edit" grid action URL for the given object.
Most typically this is like ``/widgets/XXX/edit`` where
``XXX`` represents the object's key/ID.
Calls :meth:`get_action_url()` under the hood.
"""
return self.get_action_url('edit', obj)
def make_model_form(self, model_instance=None, **kwargs):
"""
Create and return a :class:`~wuttaweb.forms.base.Form`
@ -794,6 +903,7 @@ class MasterView(View):
e.g.:
* :meth:`view()`
* :meth:`edit()`
See also related methods, which are called by this one:
@ -837,12 +947,58 @@ class MasterView(View):
Configure the given model form, as needed.
This is called by :meth:`make_model_form()` - for multiple
CRUD views.
CRUD views (create, view, edit, delete, possibly others).
There is no default logic here; subclass should override if
needed. The ``form`` param will already be "complete" and
ready to use as-is, but this method can further modify it
based on request details etc.
The default logic here does just one thing: when "editing"
(i.e. in :meth:`edit()` view) then all fields which are part
of the :attr:`model_key` will be marked via
:meth:`set_readonly()` so the user cannot change primary key
values for a record.
Subclass may override as needed. The ``form`` param will
already be "complete" and ready to use as-is, but this method
can further modify it based on request details etc.
"""
if self.editing:
for key in self.get_model_key():
form.set_readonly(key)
def objectify(self, form):
"""
Must return a "model instance" object which reflects the
validated form data.
In simple cases this may just return the
:attr:`~wuttaweb.forms.base.Form.validated` data dict.
When dealing with SQLAlchemy models it would return a proper
mapped instance, creating it if necessary.
:param form: Reference to the *already validated*
:class:`~wuttaweb.forms.base.Form` object. See the form's
:attr:`~wuttaweb.forms.base.Form.validated` attribute for
the data.
See also :meth:`edit_save_form()` which calls this method.
"""
return form.validated
def persist(self, obj):
"""
If applicable, this method should persist ("save") the given
object's data (e.g. to DB), creating or updating it as needed.
This is part of the "submit form" workflow; ``obj`` should be
a model instance which already reflects the validated form
data.
Note that there is no default logic here, subclass must
override if needed.
:param obj: Model instance object as produced by
:meth:`objectify()`.
See also :meth:`edit_save_form()` which calls this method.
"""
##############################
@ -1145,6 +1301,14 @@ class MasterView(View):
config.add_view(cls, attr='view',
route_name=f'{route_prefix}.view')
# edit
if cls.editable:
instance_url_prefix = cls.get_instance_url_prefix()
config.add_route(f'{route_prefix}.edit',
f'{instance_url_prefix}/edit')
config.add_view(cls, attr='edit',
route_name=f'{route_prefix}.edit')
# configure
if cls.configurable:
config.add_route(f'{route_prefix}.configure',

View file

@ -197,6 +197,36 @@ class SettingView(MasterView):
""" """
return setting['name']
def make_model_form(self, *args, **kwargs):
""" """
# TODO: sheesh what a hack. hopefully not needed for long..
# here we ensure deform is created before introducing our
# `name` field, to keep it out of submit handling
if self.editing:
kwargs['fields'] = ['value']
form = super().make_model_form(*args, **kwargs)
if self.editing:
form.get_deform()
form.fields.insert_before('value', 'name')
return form
def configure_form(self, f):
""" """
super().configure_form(f)
if self.editing:
f.set_readonly('name')
def persist(self, setting, session=None):
""" """
name = self.get_instance(session=session)['name']
session = session or Session()
self.app.save_setting(session, name, setting['value'])
def defaults(config, **kwargs):
base = globals()

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8; -*-
from unittest import TestCase
from unittest.mock import MagicMock
from unittest.mock import MagicMock, patch
import colander
import deform
@ -151,6 +151,34 @@ class TestForm(TestCase):
dform = form.get_deform()
self.assertEqual(dform.cstruct, myobj)
def test_get_cancel_url(self):
# is referrer by default
form = self.make_form()
self.request.get_referrer = MagicMock(return_value='/cancel-default')
self.assertEqual(form.get_cancel_url(), '/cancel-default')
del self.request.get_referrer
# or can be static URL
form = self.make_form(cancel_url='/cancel-static')
self.assertEqual(form.get_cancel_url(), '/cancel-static')
# or can be fallback URL (nb. 'NOPE' indicates no referrer)
form = self.make_form(cancel_url_fallback='/cancel-fallback')
self.request.get_referrer = MagicMock(return_value='NOPE')
self.assertEqual(form.get_cancel_url(), '/cancel-fallback')
del self.request.get_referrer
# or can be referrer fallback, i.e. home page
form = self.make_form()
def get_referrer(default=None):
if default == 'NOPE':
return 'NOPE'
return '/home-page'
self.request.get_referrer = get_referrer
self.assertEqual(form.get_cancel_url(), '/home-page')
del self.request.get_referrer
def test_get_label(self):
form = self.make_form(fields=['foo', 'bar'])
self.assertEqual(form.get_label('foo'), "Foo")
@ -170,6 +198,26 @@ class TestForm(TestCase):
self.assertEqual(form.get_label('foo'), "Woohoo")
self.assertEqual(schema['foo'].title, "Woohoo")
def test_readonly_fields(self):
form = self.make_form(fields=['foo', 'bar'])
self.assertEqual(form.readonly_fields, set())
self.assertFalse(form.is_readonly('foo'))
form.set_readonly('foo')
self.assertEqual(form.readonly_fields, {'foo'})
self.assertTrue(form.is_readonly('foo'))
self.assertFalse(form.is_readonly('bar'))
form.set_readonly('bar')
self.assertEqual(form.readonly_fields, {'foo', 'bar'})
self.assertTrue(form.is_readonly('foo'))
self.assertTrue(form.is_readonly('bar'))
form.set_readonly('foo', False)
self.assertEqual(form.readonly_fields, {'bar'})
self.assertFalse(form.is_readonly('foo'))
self.assertTrue(form.is_readonly('bar'))
def test_render_vue_tag(self):
schema = self.make_schema()
form = self.make_form(schema=schema)
@ -183,13 +231,13 @@ class TestForm(TestCase):
# form button is disabled on @submit by default
schema = self.make_schema()
form = self.make_form(schema=schema)
form = self.make_form(schema=schema, cancel_url='/')
html = form.render_vue_template()
self.assertIn('<script type="text/x-template" id="wutta-form-template">', html)
self.assertIn('@submit', html)
# but not if form is configured otherwise
form = self.make_form(schema=schema, auto_disable_submit=False)
form = self.make_form(schema=schema, auto_disable_submit=False, cancel_url='/')
html = form.render_vue_template()
self.assertIn('<script type="text/x-template" id="wutta-form-template">', html)
self.assertNotIn('@submit', html)
@ -224,6 +272,25 @@ class TestForm(TestCase):
html = form.render_vue_field('foo')
self.assertIn(':message="`something is wrong`"', html)
# add another field, but not to deform, so it should still
# display but with no widget
form.fields.append('zanzibar')
html = form.render_vue_field('zanzibar')
self.assertIn('<b-field :horizontal="true" label="Zanzibar">', html)
self.assertNotIn('<b-input', html)
# nb. no error message
self.assertNotIn('message', html)
# try that once more but with a model record instance
with patch.object(form, 'model_instance', new={'zanzibar': 'omgwtfbbq'}):
html = form.render_vue_field('zanzibar')
self.assertIn('<b-field', html)
self.assertIn('label="Zanzibar"', html)
self.assertNotIn('<b-input', html)
self.assertIn('>omgwtfbbq<', html)
# nb. no error message
self.assertNotIn('message', html)
def test_get_field_errors(self):
schema = self.make_schema()
form = self.make_form(schema=schema)

View file

@ -18,11 +18,11 @@ from tests.views.utils import WebTestCase
class TestMasterView(WebTestCase):
def test_defaults(self):
master.MasterView.model_name = 'Widget'
with patch.object(master.MasterView, 'viewable', new=False):
# TODO: should inspect pyramid routes after this, to be certain
with patch.multiple(master.MasterView, create=True,
model_name='Widget',
viewable=False,
editable=False):
master.MasterView.defaults(self.pyramid_config)
del master.MasterView.model_name
##############################
# class methods
@ -374,17 +374,75 @@ class TestMasterView(WebTestCase):
def test_view(self):
# sanity/coverage check using /settings/XXX
master.MasterView.model_name = 'Setting'
master.MasterView.grid_columns = ['name', 'value']
master.MasterView.form_fields = ['name', 'value']
view = master.MasterView(self.request)
setting = {'name': 'foo.bar', 'value': 'baz'}
self.request.matchdict = {'name': 'foo.bar'}
with patch.multiple(master.MasterView, create=True,
model_name='Setting',
model_key='name',
grid_columns=['name', 'value'],
form_fields=['name', 'value']):
view = master.MasterView(self.request)
with patch.object(view, 'get_instance', return_value=setting):
response = view.view()
del master.MasterView.model_name
del master.MasterView.grid_columns
del master.MasterView.form_fields
def test_edit(self):
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(master.MasterView, create=True,
model_name='Setting',
model_key='name',
form_fields=['name', 'value']):
view = master.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, setting['name'], 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.assertIsInstance(response, HTTPFound)
# setting should be updated in DB
self.assertEqual(self.app.get_setting(self.session, 'foo.bar'), 'froogle')
# try another post with invalid data (name is required)
self.request.method = 'POST'
self.request.POST = {
'value': 'gargoyle',
}
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_configure(self):
model = self.app.model

View file

@ -65,3 +65,30 @@ class TestSettingView(WebTestCase):
view = self.make_view()
title = view.get_instance_title(setting)
self.assertEqual(title, 'foo')
def test_make_model_form(self):
view = self.make_view()
view.editing = True
form = view.make_model_form()
self.assertEqual(form.fields, ['name', 'value'])
self.assertIn('name', form)
self.assertIn('value', form)
dform = form.get_deform()
self.assertNotIn('name', dform)
self.assertIn('value', dform)
def test_persist(self):
model = self.app.model
view = self.make_view()
# setup
self.app.save_setting(self.session, 'foo', 'bar')
self.session.commit()
self.assertEqual(self.session.query(model.Setting).count(), 1)
# setting is updated
self.request.matchdict = {'name': 'foo'}
view.persist({'name': 'foo', 'value': 'frazzle'}, session=self.session)
self.session.commit()
self.assertEqual(self.session.query(model.Setting).count(), 1)
self.assertEqual(self.app.get_setting(self.session, 'foo'), 'frazzle')