feat: add basic Create support for CRUD master view
This commit is contained in:
parent
c46b42f76d
commit
73014964cb
|
@ -159,6 +159,18 @@ class Form:
|
||||||
|
|
||||||
See also :meth:`set_readonly()` and :meth:`is_readonly()`.
|
See also :meth:`set_readonly()` and :meth:`is_readonly()`.
|
||||||
|
|
||||||
|
.. attribute:: required_fields
|
||||||
|
|
||||||
|
A dict of "required" field flags. Keys are field names, and
|
||||||
|
values are boolean flags indicating whether the field is
|
||||||
|
required.
|
||||||
|
|
||||||
|
Depending on :attr:`schema`, some fields may be "(not)
|
||||||
|
required" by default. However ``required_fields`` keeps track
|
||||||
|
of any "overrides" per field.
|
||||||
|
|
||||||
|
See also :meth:`set_required()` and :meth:`is_required()`.
|
||||||
|
|
||||||
.. attribute:: action_url
|
.. attribute:: action_url
|
||||||
|
|
||||||
String URL to which the form should be submitted, if applicable.
|
String URL to which the form should be submitted, if applicable.
|
||||||
|
@ -256,6 +268,7 @@ class Form:
|
||||||
model_instance=None,
|
model_instance=None,
|
||||||
readonly=False,
|
readonly=False,
|
||||||
readonly_fields=[],
|
readonly_fields=[],
|
||||||
|
required_fields={},
|
||||||
labels={},
|
labels={},
|
||||||
action_url=None,
|
action_url=None,
|
||||||
cancel_url=None,
|
cancel_url=None,
|
||||||
|
@ -275,6 +288,7 @@ class Form:
|
||||||
self.schema = schema
|
self.schema = schema
|
||||||
self.readonly = readonly
|
self.readonly = readonly
|
||||||
self.readonly_fields = set(readonly_fields or [])
|
self.readonly_fields = set(readonly_fields or [])
|
||||||
|
self.required_fields = required_fields or {}
|
||||||
self.labels = labels or {}
|
self.labels = labels or {}
|
||||||
self.action_url = action_url
|
self.action_url = action_url
|
||||||
self.cancel_url = cancel_url
|
self.cancel_url = cancel_url
|
||||||
|
@ -413,6 +427,41 @@ class Form:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def set_required(self, key, required=True):
|
||||||
|
"""
|
||||||
|
Enable or disable the "required" flag for a given field.
|
||||||
|
|
||||||
|
When a field is marked required, a value must be provided
|
||||||
|
or else it fails validation.
|
||||||
|
|
||||||
|
In practice if a field is "not required" then a default
|
||||||
|
"empty" value is assumed, should the user not provide one.
|
||||||
|
|
||||||
|
See also :meth:`is_required()`; this is tracked via
|
||||||
|
:attr:`required_fields`.
|
||||||
|
|
||||||
|
:param key: String key (fieldname) for the field.
|
||||||
|
|
||||||
|
:param required: New required flag for the field. Usually a
|
||||||
|
boolean, but may also be ``None`` to remove any flag and
|
||||||
|
revert to default behavior for the field.
|
||||||
|
"""
|
||||||
|
self.required_fields[key] = required
|
||||||
|
|
||||||
|
def is_required(self, key):
|
||||||
|
"""
|
||||||
|
Returns boolean indicating if the given field is marked as
|
||||||
|
required.
|
||||||
|
|
||||||
|
See also :meth:`set_required()`.
|
||||||
|
|
||||||
|
:param key: Field key/name as string.
|
||||||
|
|
||||||
|
:returns: Value for the flag from :attr:`required_fields` if
|
||||||
|
present; otherwise ``None``.
|
||||||
|
"""
|
||||||
|
return self.required_fields.get(key, None)
|
||||||
|
|
||||||
def set_label(self, key, label):
|
def set_label(self, key, label):
|
||||||
"""
|
"""
|
||||||
Set the label for given field name.
|
Set the label for given field name.
|
||||||
|
@ -452,6 +501,15 @@ class Form:
|
||||||
schema.add(colander.SchemaNode(
|
schema.add(colander.SchemaNode(
|
||||||
colander.String(),
|
colander.String(),
|
||||||
name=name))
|
name=name))
|
||||||
|
|
||||||
|
# apply required flags
|
||||||
|
for key, required in self.required_fields.items():
|
||||||
|
if key in schema:
|
||||||
|
if required is False:
|
||||||
|
# TODO: (why) should we not use colander.null here?
|
||||||
|
#schema[key].missing = colander.null
|
||||||
|
schema[key].missing = None
|
||||||
|
|
||||||
self.schema = schema
|
self.schema = schema
|
||||||
|
|
||||||
else: # no fields
|
else: # no fields
|
||||||
|
|
|
@ -362,7 +362,11 @@ class GridAction:
|
||||||
Default logic returns the output from :meth:`render_icon()`
|
Default logic returns the output from :meth:`render_icon()`
|
||||||
and :meth:`render_label()`.
|
and :meth:`render_label()`.
|
||||||
"""
|
"""
|
||||||
return self.render_icon() + ' ' + self.render_label()
|
html = [
|
||||||
|
self.render_icon(),
|
||||||
|
self.render_label(),
|
||||||
|
]
|
||||||
|
return HTML.literal(' ').join(html)
|
||||||
|
|
||||||
def render_icon(self):
|
def render_icon(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -214,13 +214,20 @@
|
||||||
<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"
|
||||||
|
style="display: flex; gap: 1.5rem;">
|
||||||
% if index_title:
|
% if index_title:
|
||||||
% if index_url:
|
% if index_url:
|
||||||
<h1 class="title">${h.link_to(index_title, index_url)}</h1>
|
<h1 class="title">${h.link_to(index_title, index_url)}</h1>
|
||||||
% else:
|
% else:
|
||||||
<h1 class="title">${index_title}</h1>
|
<h1 class="title">${index_title}</h1>
|
||||||
% endif
|
% endif
|
||||||
|
% if master and master.creatable and not master.creating:
|
||||||
|
<wutta-button once type="is-primary"
|
||||||
|
tag="a" href="${url(f'{route_prefix}.create')}"
|
||||||
|
icon-left="plus"
|
||||||
|
label="Create New" />
|
||||||
|
% endif
|
||||||
% endif
|
% endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -54,15 +54,19 @@
|
||||||
|
|
||||||
let ${form.vue_component}Data = {
|
let ${form.vue_component}Data = {
|
||||||
|
|
||||||
## field model values
|
% if not form.readonly:
|
||||||
% for key in form:
|
|
||||||
% if key in dform:
|
## field model values
|
||||||
model_${key}: ${form.get_vue_field_value(key)|n},
|
% for key in form:
|
||||||
% endif
|
% if key in dform:
|
||||||
% endfor
|
model_${key}: ${form.get_vue_field_value(key)|n},
|
||||||
|
% endif
|
||||||
|
% endfor
|
||||||
|
|
||||||
|
% if form.auto_disable_submit:
|
||||||
|
formSubmitting: false,
|
||||||
|
% endif
|
||||||
|
|
||||||
% if form.auto_disable_submit:
|
|
||||||
formSubmitting: false,
|
|
||||||
% endif
|
% endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
7
src/wuttaweb/templates/master/create.mako
Normal file
7
src/wuttaweb/templates/master/create.mako
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
## -*- coding: utf-8; -*-
|
||||||
|
<%inherit file="/master/form.mako" />
|
||||||
|
|
||||||
|
<%def name="title()">New ${model_title}</%def>
|
||||||
|
|
||||||
|
|
||||||
|
${parent.body()}
|
|
@ -168,6 +168,12 @@ class MasterView(View):
|
||||||
|
|
||||||
This is optional; see also :meth:`index_get_grid_columns()`.
|
This is optional; see also :meth:`index_get_grid_columns()`.
|
||||||
|
|
||||||
|
.. attribute:: creatable
|
||||||
|
|
||||||
|
Boolean indicating whether the view model supports "creating" -
|
||||||
|
i.e. it should have a :meth:`create()` view. Default value is
|
||||||
|
``True``.
|
||||||
|
|
||||||
.. attribute:: viewable
|
.. attribute:: viewable
|
||||||
|
|
||||||
Boolean indicating whether the view model supports "viewing" -
|
Boolean indicating whether the view model supports "viewing" -
|
||||||
|
@ -206,6 +212,7 @@ class MasterView(View):
|
||||||
# features
|
# features
|
||||||
listable = True
|
listable = True
|
||||||
has_grid = True
|
has_grid = True
|
||||||
|
creatable = True
|
||||||
viewable = True
|
viewable = True
|
||||||
editable = True
|
editable = True
|
||||||
deletable = True
|
deletable = True
|
||||||
|
@ -213,6 +220,7 @@ class MasterView(View):
|
||||||
|
|
||||||
# current action
|
# current action
|
||||||
listing = False
|
listing = False
|
||||||
|
creating = False
|
||||||
viewing = False
|
viewing = False
|
||||||
editing = False
|
editing = False
|
||||||
deleting = False
|
deleting = False
|
||||||
|
@ -335,6 +343,62 @@ class MasterView(View):
|
||||||
"""
|
"""
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
##############################
|
||||||
|
# create methods
|
||||||
|
##############################
|
||||||
|
|
||||||
|
def create(self):
|
||||||
|
"""
|
||||||
|
View to "create" a new model record.
|
||||||
|
|
||||||
|
This usually corresponds to a URL like ``/widgets/new``.
|
||||||
|
|
||||||
|
By default, this view is included only if :attr:`creatable` is
|
||||||
|
true.
|
||||||
|
|
||||||
|
The default "create" view logic will show a form with field
|
||||||
|
widgets, allowing user to 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:`create_save_form()`
|
||||||
|
"""
|
||||||
|
self.creating = True
|
||||||
|
form = self.make_model_form(cancel_url_fallback=self.get_index_url())
|
||||||
|
|
||||||
|
if form.validate():
|
||||||
|
obj = self.create_save_form(form)
|
||||||
|
return self.redirect(self.get_action_url('view', obj))
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'form': form,
|
||||||
|
}
|
||||||
|
return self.render_to_response('create', context)
|
||||||
|
|
||||||
|
def create_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. It is called by :meth:`create()`.
|
||||||
|
|
||||||
|
Subclass may override this, or any of the related methods
|
||||||
|
called by this one:
|
||||||
|
|
||||||
|
* :meth:`objectify()`
|
||||||
|
* :meth:`persist()`
|
||||||
|
|
||||||
|
:returns: Should return the resulting model instance, e.g. as
|
||||||
|
produced by :meth:`objectify()`.
|
||||||
|
"""
|
||||||
|
obj = self.objectify(form)
|
||||||
|
self.persist(obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
# view methods
|
# view methods
|
||||||
##############################
|
##############################
|
||||||
|
@ -419,7 +483,7 @@ class MasterView(View):
|
||||||
"""
|
"""
|
||||||
This method is responsible for "converting" the validated form
|
This method is responsible for "converting" the validated form
|
||||||
data to a model instance, and then "saving" the result,
|
data to a model instance, and then "saving" the result,
|
||||||
e.g. to DB.
|
e.g. to DB. It is called by :meth:`edit()`.
|
||||||
|
|
||||||
Subclass may override this, or any of the related methods
|
Subclass may override this, or any of the related methods
|
||||||
called by this one:
|
called by this one:
|
||||||
|
@ -427,8 +491,8 @@ class MasterView(View):
|
||||||
* :meth:`objectify()`
|
* :meth:`objectify()`
|
||||||
* :meth:`persist()`
|
* :meth:`persist()`
|
||||||
|
|
||||||
:returns: This should return the resulting model instance,
|
:returns: Should return the resulting model instance, e.g. as
|
||||||
which was produced by :meth:`objectify()`.
|
produced by :meth:`objectify()`.
|
||||||
"""
|
"""
|
||||||
obj = self.objectify(form)
|
obj = self.objectify(form)
|
||||||
self.persist(obj)
|
self.persist(obj)
|
||||||
|
@ -1395,6 +1459,13 @@ class MasterView(View):
|
||||||
config.add_view(cls, attr='index',
|
config.add_view(cls, attr='index',
|
||||||
route_name=route_prefix)
|
route_name=route_prefix)
|
||||||
|
|
||||||
|
# create
|
||||||
|
if cls.creatable:
|
||||||
|
config.add_route(f'{route_prefix}.create',
|
||||||
|
f'{url_prefix}/new')
|
||||||
|
config.add_view(cls, attr='create',
|
||||||
|
route_name=f'{route_prefix}.create')
|
||||||
|
|
||||||
# view
|
# view
|
||||||
if cls.viewable:
|
if cls.viewable:
|
||||||
instance_url_prefix = cls.get_instance_url_prefix()
|
instance_url_prefix = cls.get_instance_url_prefix()
|
||||||
|
|
|
@ -26,8 +26,9 @@ Views for app settings
|
||||||
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from wuttjamaican.db.model import Setting
|
import colander
|
||||||
|
|
||||||
|
from wuttjamaican.db.model import Setting
|
||||||
from wuttaweb.views import MasterView
|
from wuttaweb.views import MasterView
|
||||||
from wuttaweb.util import get_libver, get_liburl
|
from wuttaweb.util import get_libver, get_liburl
|
||||||
from wuttaweb.db import Session
|
from wuttaweb.db import Session
|
||||||
|
@ -48,6 +49,7 @@ class AppInfoView(MasterView):
|
||||||
model_title_plural = "App Info"
|
model_title_plural = "App Info"
|
||||||
route_prefix = 'appinfo'
|
route_prefix = 'appinfo'
|
||||||
has_grid = False
|
has_grid = False
|
||||||
|
creatable = False
|
||||||
viewable = False
|
viewable = False
|
||||||
editable = False
|
editable = False
|
||||||
deletable = False
|
deletable = False
|
||||||
|
@ -177,6 +179,10 @@ class SettingView(MasterView):
|
||||||
""" """
|
""" """
|
||||||
return {
|
return {
|
||||||
'name': setting.name,
|
'name': setting.name,
|
||||||
|
# TODO: when viewing the record, 'None' is displayed for null
|
||||||
|
# field values. not so if we return colander.null here, but
|
||||||
|
# then that causes other problems..
|
||||||
|
#'value': setting.value if setting.value is not None else colander.null,
|
||||||
'value': setting.value,
|
'value': setting.value,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,7 +194,10 @@ class SettingView(MasterView):
|
||||||
name = self.request.matchdict['name']
|
name = self.request.matchdict['name']
|
||||||
setting = session.query(model.Setting).get(name)
|
setting = session.query(model.Setting).get(name)
|
||||||
if setting:
|
if setting:
|
||||||
return self.normalize_setting(setting)
|
setting = self.normalize_setting(setting)
|
||||||
|
if setting['value'] is None:
|
||||||
|
setting['value'] = colander.null
|
||||||
|
return setting
|
||||||
|
|
||||||
return self.notfound()
|
return self.notfound()
|
||||||
|
|
||||||
|
@ -197,10 +206,19 @@ class SettingView(MasterView):
|
||||||
""" """
|
""" """
|
||||||
return setting['name']
|
return setting['name']
|
||||||
|
|
||||||
|
# TODO: master should handle this
|
||||||
|
def configure_form(self, f):
|
||||||
|
super().configure_form(f)
|
||||||
|
|
||||||
|
f.set_required('value', False)
|
||||||
|
|
||||||
# TODO: master should handle this
|
# TODO: master should handle this
|
||||||
def persist(self, setting, session=None):
|
def persist(self, setting, session=None):
|
||||||
""" """
|
""" """
|
||||||
name = self.get_instance(session=session)['name']
|
if self.creating:
|
||||||
|
name = setting['name']
|
||||||
|
else:
|
||||||
|
name = self.request.matchdict['name']
|
||||||
session = session or Session()
|
session = session or Session()
|
||||||
self.app.save_setting(session, name, setting['value'])
|
self.app.save_setting(session, name, setting['value'])
|
||||||
|
|
||||||
|
|
|
@ -135,6 +135,19 @@ class TestForm(TestCase):
|
||||||
self.assertIsNone(form.schema)
|
self.assertIsNone(form.schema)
|
||||||
self.assertRaises(NotImplementedError, form.get_schema)
|
self.assertRaises(NotImplementedError, form.get_schema)
|
||||||
|
|
||||||
|
# schema nodes are required by default
|
||||||
|
form = self.make_form(fields=['foo', 'bar'])
|
||||||
|
schema = form.get_schema()
|
||||||
|
self.assertIs(schema['foo'].missing, colander.required)
|
||||||
|
self.assertIs(schema['bar'].missing, colander.required)
|
||||||
|
|
||||||
|
# but fields can be marked *not* required
|
||||||
|
form = self.make_form(fields=['foo', 'bar'])
|
||||||
|
form.set_required('bar', False)
|
||||||
|
schema = form.get_schema()
|
||||||
|
self.assertIs(schema['foo'].missing, colander.required)
|
||||||
|
self.assertIsNone(schema['bar'].missing)
|
||||||
|
|
||||||
def test_get_deform(self):
|
def test_get_deform(self):
|
||||||
schema = self.make_schema()
|
schema = self.make_schema()
|
||||||
|
|
||||||
|
@ -218,6 +231,26 @@ class TestForm(TestCase):
|
||||||
self.assertFalse(form.is_readonly('foo'))
|
self.assertFalse(form.is_readonly('foo'))
|
||||||
self.assertTrue(form.is_readonly('bar'))
|
self.assertTrue(form.is_readonly('bar'))
|
||||||
|
|
||||||
|
def test_required_fields(self):
|
||||||
|
form = self.make_form(fields=['foo', 'bar'])
|
||||||
|
self.assertEqual(form.required_fields, {})
|
||||||
|
self.assertIsNone(form.is_required('foo'))
|
||||||
|
|
||||||
|
form.set_required('foo')
|
||||||
|
self.assertEqual(form.required_fields, {'foo': True})
|
||||||
|
self.assertTrue(form.is_required('foo'))
|
||||||
|
self.assertIsNone(form.is_required('bar'))
|
||||||
|
|
||||||
|
form.set_required('bar')
|
||||||
|
self.assertEqual(form.required_fields, {'foo': True, 'bar': True})
|
||||||
|
self.assertTrue(form.is_required('foo'))
|
||||||
|
self.assertTrue(form.is_required('bar'))
|
||||||
|
|
||||||
|
form.set_required('foo', False)
|
||||||
|
self.assertEqual(form.required_fields, {'foo': False, 'bar': True})
|
||||||
|
self.assertFalse(form.is_required('foo'))
|
||||||
|
self.assertTrue(form.is_required('bar'))
|
||||||
|
|
||||||
def test_render_vue_tag(self):
|
def test_render_vue_tag(self):
|
||||||
schema = self.make_schema()
|
schema = self.make_schema()
|
||||||
form = self.make_form(schema=schema)
|
form = self.make_form(schema=schema)
|
||||||
|
|
|
@ -320,22 +320,22 @@ class TestMasterView(WebTestCase):
|
||||||
|
|
||||||
# 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'
|
with patch.multiple(master.MasterView, create=True,
|
||||||
view = master.MasterView(self.request)
|
model_name='Widget',
|
||||||
response = view.render_to_response('index', {})
|
creatable=False):
|
||||||
self.assertIsInstance(response, Response)
|
view = master.MasterView(self.request)
|
||||||
del master.MasterView.model_name
|
response = view.render_to_response('index', {})
|
||||||
|
self.assertIsInstance(response, Response)
|
||||||
|
|
||||||
# basic sanity check using /appinfo/index.mako
|
# basic sanity check using /appinfo/index.mako
|
||||||
master.MasterView.model_name = 'AppInfo'
|
with patch.multiple(master.MasterView, create=True,
|
||||||
master.MasterView.route_prefix = 'appinfo'
|
model_name='AppInfo',
|
||||||
master.MasterView.url_prefix = '/appinfo'
|
route_prefix='appinfo',
|
||||||
view = master.MasterView(self.request)
|
url_prefix='/appinfo',
|
||||||
response = view.render_to_response('index', {})
|
creatable=False):
|
||||||
self.assertIsInstance(response, Response)
|
view = master.MasterView(self.request)
|
||||||
del master.MasterView.model_name
|
response = view.render_to_response('index', {})
|
||||||
del master.MasterView.route_prefix
|
self.assertIsInstance(response, Response)
|
||||||
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'
|
||||||
|
@ -372,6 +372,55 @@ class TestMasterView(WebTestCase):
|
||||||
del master.MasterView.model_key
|
del master.MasterView.model_key
|
||||||
del master.MasterView.grid_columns
|
del master.MasterView.grid_columns
|
||||||
|
|
||||||
|
def test_create(self):
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
|
# sanity/coverage check using /settings/new
|
||||||
|
with patch.multiple(master.MasterView, create=True,
|
||||||
|
model_name='Setting',
|
||||||
|
model_key='name',
|
||||||
|
form_fields=['name', 'value']):
|
||||||
|
view = master.MasterView(self.request)
|
||||||
|
|
||||||
|
# no setting yet
|
||||||
|
self.assertIsNone(self.app.get_setting(self.session, 'foo.bar'))
|
||||||
|
|
||||||
|
# get the form page
|
||||||
|
response = view.create()
|
||||||
|
self.assertIsInstance(response, Response)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
# self.assertIn('frazzle', response.text)
|
||||||
|
# nb. no error
|
||||||
|
self.assertNotIn('Required', response.text)
|
||||||
|
|
||||||
|
def persist(setting):
|
||||||
|
self.app.save_setting(self.session, setting['name'], setting['value'])
|
||||||
|
self.session.commit()
|
||||||
|
|
||||||
|
# post request to save setting
|
||||||
|
self.request.method = 'POST'
|
||||||
|
self.request.POST = {
|
||||||
|
'name': 'foo.bar',
|
||||||
|
'value': 'fraggle',
|
||||||
|
}
|
||||||
|
with patch.object(view, 'persist', new=persist):
|
||||||
|
response = view.create()
|
||||||
|
# nb. should get redirect back to view page
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
# setting should now be in DB
|
||||||
|
self.assertEqual(self.app.get_setting(self.session, 'foo.bar'), 'fraggle')
|
||||||
|
|
||||||
|
# try another post with invalid data (value is required)
|
||||||
|
self.request.method = 'POST'
|
||||||
|
self.request.POST = {}
|
||||||
|
with patch.object(view, 'persist', new=persist):
|
||||||
|
response = view.create()
|
||||||
|
# nb. should get a form with errors
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn('Required', response.text)
|
||||||
|
# setting did not change in DB
|
||||||
|
self.assertEqual(self.app.get_setting(self.session, 'foo.bar'), 'fraggle')
|
||||||
|
|
||||||
def test_view(self):
|
def test_view(self):
|
||||||
|
|
||||||
# sanity/coverage check using /settings/XXX
|
# sanity/coverage check using /settings/XXX
|
||||||
|
@ -500,11 +549,6 @@ class TestMasterView(WebTestCase):
|
||||||
def test_configure(self):
|
def test_configure(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
||||||
# setup
|
|
||||||
master.MasterView.model_name = 'AppInfo'
|
|
||||||
master.MasterView.route_prefix = 'appinfo'
|
|
||||||
master.MasterView.template_prefix = '/appinfo'
|
|
||||||
|
|
||||||
# mock settings
|
# mock settings
|
||||||
settings = [
|
settings = [
|
||||||
{'name': 'wutta.app_title'},
|
{'name': 'wutta.app_title'},
|
||||||
|
@ -516,11 +560,14 @@ class TestMasterView(WebTestCase):
|
||||||
]
|
]
|
||||||
|
|
||||||
view = master.MasterView(self.request)
|
view = master.MasterView(self.request)
|
||||||
with patch.object(self.request, 'current_route_url',
|
with patch.object(self.request, 'current_route_url', return_value='/appinfo/configure'):
|
||||||
return_value='/appinfo/configure'):
|
with patch.object(master, 'Session', return_value=self.session):
|
||||||
with patch.object(master.MasterView, 'configure_get_simple_settings',
|
with patch.multiple(master.MasterView, create=True,
|
||||||
return_value=settings):
|
model_name='AppInfo',
|
||||||
with patch.object(master, 'Session', return_value=self.session):
|
route_prefix='appinfo',
|
||||||
|
template_prefix='/appinfo',
|
||||||
|
creatable=False,
|
||||||
|
configure_get_simple_settings=MagicMock(return_value=settings)):
|
||||||
|
|
||||||
# get the form page
|
# get the form page
|
||||||
response = view.configure()
|
response = view.configure()
|
||||||
|
@ -558,8 +605,3 @@ class TestMasterView(WebTestCase):
|
||||||
# should now have 0 settings
|
# should now have 0 settings
|
||||||
count = self.session.query(model.Setting).count()
|
count = self.session.query(model.Setting).count()
|
||||||
self.assertEqual(count, 0)
|
self.assertEqual(count, 0)
|
||||||
|
|
||||||
# teardown
|
|
||||||
del master.MasterView.model_name
|
|
||||||
del master.MasterView.route_prefix
|
|
||||||
del master.MasterView.template_prefix
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
# -*- coding: utf-8; -*-
|
# -*- coding: utf-8; -*-
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from tests.views.utils import WebTestCase
|
from tests.views.utils import WebTestCase
|
||||||
|
|
||||||
from pyramid.httpexceptions import HTTPNotFound
|
from pyramid.httpexceptions import HTTPNotFound
|
||||||
|
@ -66,6 +68,14 @@ class TestSettingView(WebTestCase):
|
||||||
title = view.get_instance_title(setting)
|
title = view.get_instance_title(setting)
|
||||||
self.assertEqual(title, 'foo')
|
self.assertEqual(title, 'foo')
|
||||||
|
|
||||||
|
def test_configure_form(self):
|
||||||
|
view = self.make_view()
|
||||||
|
form = view.make_form(fields=view.get_form_fields())
|
||||||
|
self.assertNotIn('value', form.required_fields)
|
||||||
|
view.configure_form(form)
|
||||||
|
self.assertIn('value', form.required_fields)
|
||||||
|
self.assertFalse(form.required_fields['value'])
|
||||||
|
|
||||||
def test_persist(self):
|
def test_persist(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
|
@ -82,6 +92,14 @@ class TestSettingView(WebTestCase):
|
||||||
self.assertEqual(self.session.query(model.Setting).count(), 1)
|
self.assertEqual(self.session.query(model.Setting).count(), 1)
|
||||||
self.assertEqual(self.app.get_setting(self.session, 'foo'), 'frazzle')
|
self.assertEqual(self.app.get_setting(self.session, 'foo'), 'frazzle')
|
||||||
|
|
||||||
|
# new setting is created
|
||||||
|
self.request.matchdict = {}
|
||||||
|
with patch.object(view, 'creating', new=True):
|
||||||
|
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')
|
||||||
|
|
||||||
def test_delete_instance(self):
|
def test_delete_instance(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
|
|
Loading…
Reference in a new issue