feat: add basic Edit support for CRUD master view
This commit is contained in:
parent
9e1fc6e57d
commit
1a8fc8dd44
|
@ -149,10 +149,35 @@ class Form:
|
||||||
Default for this is ``False`` in which case the ``<form>`` tag
|
Default for this is ``False`` in which case the ``<form>`` tag
|
||||||
will exist and submit is allowed.
|
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
|
.. 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.
|
||||||
|
|
||||||
|
.. 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
|
.. attribute:: vue_tagname
|
||||||
|
|
||||||
String name for Vue component tag. By default this is
|
String name for Vue component tag. By default this is
|
||||||
|
@ -180,6 +205,23 @@ class Form:
|
||||||
.. attribute:: show_button_reset
|
.. attribute:: show_button_reset
|
||||||
|
|
||||||
Flag indicating whether a Reset button should be shown.
|
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
|
.. attribute:: validated
|
||||||
|
|
||||||
|
@ -197,26 +239,38 @@ class Form:
|
||||||
model_class=None,
|
model_class=None,
|
||||||
model_instance=None,
|
model_instance=None,
|
||||||
readonly=False,
|
readonly=False,
|
||||||
|
readonly_fields=[],
|
||||||
labels={},
|
labels={},
|
||||||
action_url=None,
|
action_url=None,
|
||||||
|
cancel_url=None,
|
||||||
|
cancel_url_fallback=None,
|
||||||
vue_tagname='wutta-form',
|
vue_tagname='wutta-form',
|
||||||
align_buttons_right=False,
|
align_buttons_right=False,
|
||||||
auto_disable_submit=True,
|
auto_disable_submit=True,
|
||||||
button_label_submit="Save",
|
button_label_submit="Save",
|
||||||
button_icon_submit='save',
|
button_icon_submit='save',
|
||||||
show_button_reset=False,
|
show_button_reset=False,
|
||||||
|
show_button_cancel=True,
|
||||||
|
button_label_cancel="Cancel",
|
||||||
|
auto_disable_cancel=True,
|
||||||
):
|
):
|
||||||
self.request = request
|
self.request = request
|
||||||
self.schema = schema
|
self.schema = schema
|
||||||
self.readonly = readonly
|
self.readonly = readonly
|
||||||
|
self.readonly_fields = set(readonly_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_fallback = cancel_url_fallback
|
||||||
self.vue_tagname = vue_tagname
|
self.vue_tagname = vue_tagname
|
||||||
self.align_buttons_right = align_buttons_right
|
self.align_buttons_right = align_buttons_right
|
||||||
self.auto_disable_submit = auto_disable_submit
|
self.auto_disable_submit = auto_disable_submit
|
||||||
self.button_label_submit = button_label_submit
|
self.button_label_submit = button_label_submit
|
||||||
self.button_icon_submit = button_icon_submit
|
self.button_icon_submit = button_icon_submit
|
||||||
self.show_button_reset = show_button_reset
|
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.config = self.request.wutta_config
|
||||||
self.app = self.config.get_app()
|
self.app = self.config.get_app()
|
||||||
|
@ -262,6 +316,39 @@ class Form:
|
||||||
words = self.vue_tagname.split('-')
|
words = self.vue_tagname.split('-')
|
||||||
return ''.join([word.capitalize() for word in words])
|
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):
|
def set_fields(self, fields):
|
||||||
"""
|
"""
|
||||||
Explicitly set the list of form fields.
|
Explicitly set the list of form fields.
|
||||||
|
@ -273,6 +360,41 @@ class Form:
|
||||||
"""
|
"""
|
||||||
self.fields = FieldList(fields)
|
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):
|
def set_label(self, key, label):
|
||||||
"""
|
"""
|
||||||
Set the label for given field name.
|
Set the label for given field name.
|
||||||
|
@ -381,6 +503,7 @@ class Form:
|
||||||
the output.
|
the output.
|
||||||
"""
|
"""
|
||||||
context['form'] = self
|
context['form'] = self
|
||||||
|
context['dform'] = self.get_deform()
|
||||||
context.setdefault('form_attrs', {})
|
context.setdefault('form_attrs', {})
|
||||||
context.setdefault('request', self.request)
|
context.setdefault('request', self.request)
|
||||||
|
|
||||||
|
@ -408,18 +531,35 @@ class Form:
|
||||||
<!-- widget element(s) -->
|
<!-- widget element(s) -->
|
||||||
</b-field>
|
</b-field>
|
||||||
"""
|
"""
|
||||||
|
# readonly comes from: caller, field flag, or form flag
|
||||||
if readonly is None:
|
if readonly is None:
|
||||||
|
readonly = self.is_readonly(fieldname)
|
||||||
|
if not readonly:
|
||||||
readonly = self.readonly
|
readonly = self.readonly
|
||||||
|
|
||||||
# render the field widget or whatever
|
# but also, fields not in deform/schema must be readonly
|
||||||
dform = self.get_deform()
|
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]
|
field = dform[fieldname]
|
||||||
kw = {}
|
kw = {}
|
||||||
if readonly:
|
if readonly:
|
||||||
kw['readonly'] = True
|
kw['readonly'] = True
|
||||||
html = field.serialize(**kw)
|
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
|
# mark all that as safe
|
||||||
html = HTML.literal(html)
|
html = HTML.literal(html)
|
||||||
|
|
||||||
|
|
|
@ -64,7 +64,7 @@ def new_request(event):
|
||||||
|
|
||||||
Reference to the app :term:`config object`.
|
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.
|
Request method to get the "canonical" HTTP referrer value.
|
||||||
This has logic to check for referrer in the request params,
|
This has logic to check for referrer in the request params,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
## -*- coding: utf-8; -*-
|
## -*- coding: utf-8; -*-
|
||||||
<%namespace name="base_meta" file="/base_meta.mako" />
|
<%namespace name="base_meta" file="/base_meta.mako" />
|
||||||
|
<%namespace file="/wutta-components.mako" import="make_wutta_components" />
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
@ -366,7 +367,21 @@
|
||||||
${self.render_prevnext_header_buttons()}
|
${self.render_prevnext_header_buttons()}
|
||||||
</%def>
|
</%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>
|
<%def name="render_prevnext_header_buttons()"></%def>
|
||||||
|
|
||||||
|
@ -432,6 +447,7 @@
|
||||||
<%def name="finalize_whole_page_vars()"></%def>
|
<%def name="finalize_whole_page_vars()"></%def>
|
||||||
|
|
||||||
<%def name="make_whole_page_component()">
|
<%def name="make_whole_page_component()">
|
||||||
|
${make_wutta_components()}
|
||||||
${self.render_whole_page_template()}
|
${self.render_whole_page_template()}
|
||||||
${self.declare_whole_page_vars()}
|
${self.declare_whole_page_vars()}
|
||||||
${self.modify_whole_page_vars()}
|
${self.modify_whole_page_vars()}
|
||||||
|
|
|
@ -13,6 +13,12 @@
|
||||||
% if not form.readonly:
|
% 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;">
|
<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:
|
% if form.show_button_reset:
|
||||||
<b-button native-type="reset">
|
<b-button native-type="reset">
|
||||||
Reset
|
Reset
|
||||||
|
@ -50,7 +56,9 @@
|
||||||
|
|
||||||
## field model values
|
## field model values
|
||||||
% for key in form:
|
% for key in form:
|
||||||
|
% if key in dform:
|
||||||
model_${key}: ${form.get_vue_field_value(key)|n},
|
model_${key}: ${form.get_vue_field_value(key)|n},
|
||||||
|
% endif
|
||||||
% endfor
|
% endfor
|
||||||
|
|
||||||
% if form.auto_disable_submit:
|
% if form.auto_disable_submit:
|
||||||
|
|
9
src/wuttaweb/templates/master/edit.mako
Normal file
9
src/wuttaweb/templates/master/edit.mako
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
## -*- coding: utf-8; -*-
|
||||||
|
<%inherit file="/master/form.mako" />
|
||||||
|
|
||||||
|
<%def name="title()">${index_title} » ${instance_title} » Edit</%def>
|
||||||
|
|
||||||
|
<%def name="content_title()">Edit: ${instance_title}</%def>
|
||||||
|
|
||||||
|
|
||||||
|
${parent.body()}
|
71
src/wuttaweb/templates/wutta-components.mako
Normal file
71
src/wuttaweb/templates/wutta-components.mako
Normal 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>
|
|
@ -59,13 +59,11 @@ class AuthView(View):
|
||||||
|
|
||||||
form = self.make_form(schema=self.login_make_schema(),
|
form = self.make_form(schema=self.login_make_schema(),
|
||||||
align_buttons_right=True,
|
align_buttons_right=True,
|
||||||
|
show_button_cancel=False,
|
||||||
show_button_reset=True,
|
show_button_reset=True,
|
||||||
button_label_submit="Login",
|
button_label_submit="Login",
|
||||||
button_icon_submit='user')
|
button_icon_submit='user')
|
||||||
|
|
||||||
# TODO
|
|
||||||
# form.show_cancel = False
|
|
||||||
|
|
||||||
# validate basic form data (sanity check)
|
# validate basic form data (sanity check)
|
||||||
data = form.validate()
|
data = form.validate()
|
||||||
if data:
|
if data:
|
||||||
|
@ -155,6 +153,7 @@ class AuthView(View):
|
||||||
return self.redirect(self.request.route_url('home'))
|
return self.redirect(self.request.route_url('home'))
|
||||||
|
|
||||||
form = self.make_form(schema=self.change_password_make_schema(),
|
form = self.make_form(schema=self.change_password_make_schema(),
|
||||||
|
show_button_cancel=False,
|
||||||
show_button_reset=True)
|
show_button_reset=True)
|
||||||
|
|
||||||
data = form.validate()
|
data = form.validate()
|
||||||
|
|
|
@ -174,6 +174,12 @@ class MasterView(View):
|
||||||
i.e. it should have a :meth:`view()` view. Default value is
|
i.e. it should have a :meth:`view()` view. Default value is
|
||||||
``True``.
|
``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
|
.. attribute:: form_fields
|
||||||
|
|
||||||
List of columns for the model form.
|
List of columns for the model form.
|
||||||
|
@ -195,11 +201,13 @@ class MasterView(View):
|
||||||
listable = True
|
listable = True
|
||||||
has_grid = True
|
has_grid = True
|
||||||
viewable = True
|
viewable = True
|
||||||
|
editable = True
|
||||||
configurable = False
|
configurable = False
|
||||||
|
|
||||||
# current action
|
# current action
|
||||||
listing = False
|
listing = False
|
||||||
viewing = False
|
viewing = False
|
||||||
|
editing = False
|
||||||
configuring = False
|
configuring = False
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
|
@ -260,10 +268,15 @@ class MasterView(View):
|
||||||
actions = []
|
actions = []
|
||||||
|
|
||||||
# TODO: should split this off into index_get_grid_actions() ?
|
# TODO: should split this off into index_get_grid_actions() ?
|
||||||
|
|
||||||
if self.viewable:
|
if self.viewable:
|
||||||
actions.append(self.make_grid_action('view', icon='eye',
|
actions.append(self.make_grid_action('view', icon='eye',
|
||||||
url=self.get_action_url_view))
|
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
|
kwargs['actions'] = actions
|
||||||
|
|
||||||
grid = self.make_grid(**kwargs)
|
grid = self.make_grid(**kwargs)
|
||||||
|
@ -309,21 +322,6 @@ class MasterView(View):
|
||||||
"""
|
"""
|
||||||
return []
|
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
|
# view methods
|
||||||
##############################
|
##############################
|
||||||
|
@ -341,9 +339,12 @@ class MasterView(View):
|
||||||
The default view logic will show a read-only form with field
|
The default view logic will show a read-only form with field
|
||||||
values displayed.
|
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:`make_model_form()`
|
||||||
|
* :meth:`configure_form()`
|
||||||
"""
|
"""
|
||||||
self.viewing = True
|
self.viewing = True
|
||||||
instance = self.get_instance()
|
instance = self.get_instance()
|
||||||
|
@ -356,6 +357,71 @@ class MasterView(View):
|
||||||
}
|
}
|
||||||
return self.render_to_response('view', context)
|
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
|
# configure methods
|
||||||
##############################
|
##############################
|
||||||
|
@ -785,6 +851,49 @@ class MasterView(View):
|
||||||
"""
|
"""
|
||||||
return str(instance)
|
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):
|
def make_model_form(self, model_instance=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Create and return a :class:`~wuttaweb.forms.base.Form`
|
Create and return a :class:`~wuttaweb.forms.base.Form`
|
||||||
|
@ -794,6 +903,7 @@ class MasterView(View):
|
||||||
e.g.:
|
e.g.:
|
||||||
|
|
||||||
* :meth:`view()`
|
* :meth:`view()`
|
||||||
|
* :meth:`edit()`
|
||||||
|
|
||||||
See also related methods, which are called by this one:
|
See also related methods, which are called by this one:
|
||||||
|
|
||||||
|
@ -837,12 +947,58 @@ class MasterView(View):
|
||||||
Configure the given model form, as needed.
|
Configure the given model form, as needed.
|
||||||
|
|
||||||
This is called by :meth:`make_model_form()` - for multiple
|
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
|
The default logic here does just one thing: when "editing"
|
||||||
needed. The ``form`` param will already be "complete" and
|
(i.e. in :meth:`edit()` view) then all fields which are part
|
||||||
ready to use as-is, but this method can further modify it
|
of the :attr:`model_key` will be marked via
|
||||||
based on request details etc.
|
: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',
|
config.add_view(cls, attr='view',
|
||||||
route_name=f'{route_prefix}.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
|
# configure
|
||||||
if cls.configurable:
|
if cls.configurable:
|
||||||
config.add_route(f'{route_prefix}.configure',
|
config.add_route(f'{route_prefix}.configure',
|
||||||
|
|
|
@ -197,6 +197,36 @@ class SettingView(MasterView):
|
||||||
""" """
|
""" """
|
||||||
return setting['name']
|
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):
|
def defaults(config, **kwargs):
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# -*- coding: utf-8; -*-
|
# -*- coding: utf-8; -*-
|
||||||
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import colander
|
import colander
|
||||||
import deform
|
import deform
|
||||||
|
@ -151,6 +151,34 @@ class TestForm(TestCase):
|
||||||
dform = form.get_deform()
|
dform = form.get_deform()
|
||||||
self.assertEqual(dform.cstruct, myobj)
|
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):
|
def test_get_label(self):
|
||||||
form = self.make_form(fields=['foo', 'bar'])
|
form = self.make_form(fields=['foo', 'bar'])
|
||||||
self.assertEqual(form.get_label('foo'), "Foo")
|
self.assertEqual(form.get_label('foo'), "Foo")
|
||||||
|
@ -170,6 +198,26 @@ class TestForm(TestCase):
|
||||||
self.assertEqual(form.get_label('foo'), "Woohoo")
|
self.assertEqual(form.get_label('foo'), "Woohoo")
|
||||||
self.assertEqual(schema['foo'].title, "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):
|
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)
|
||||||
|
@ -183,13 +231,13 @@ class TestForm(TestCase):
|
||||||
|
|
||||||
# form button is disabled on @submit by default
|
# form button is disabled on @submit by default
|
||||||
schema = self.make_schema()
|
schema = self.make_schema()
|
||||||
form = self.make_form(schema=schema)
|
form = self.make_form(schema=schema, cancel_url='/')
|
||||||
html = form.render_vue_template()
|
html = form.render_vue_template()
|
||||||
self.assertIn('<script type="text/x-template" id="wutta-form-template">', html)
|
self.assertIn('<script type="text/x-template" id="wutta-form-template">', html)
|
||||||
self.assertIn('@submit', html)
|
self.assertIn('@submit', html)
|
||||||
|
|
||||||
# but not if form is configured otherwise
|
# 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()
|
html = form.render_vue_template()
|
||||||
self.assertIn('<script type="text/x-template" id="wutta-form-template">', html)
|
self.assertIn('<script type="text/x-template" id="wutta-form-template">', html)
|
||||||
self.assertNotIn('@submit', html)
|
self.assertNotIn('@submit', html)
|
||||||
|
@ -224,6 +272,25 @@ class TestForm(TestCase):
|
||||||
html = form.render_vue_field('foo')
|
html = form.render_vue_field('foo')
|
||||||
self.assertIn(':message="`something is wrong`"', html)
|
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):
|
def test_get_field_errors(self):
|
||||||
schema = self.make_schema()
|
schema = self.make_schema()
|
||||||
form = self.make_form(schema=schema)
|
form = self.make_form(schema=schema)
|
||||||
|
|
|
@ -18,11 +18,11 @@ from tests.views.utils import WebTestCase
|
||||||
class TestMasterView(WebTestCase):
|
class TestMasterView(WebTestCase):
|
||||||
|
|
||||||
def test_defaults(self):
|
def test_defaults(self):
|
||||||
master.MasterView.model_name = 'Widget'
|
with patch.multiple(master.MasterView, create=True,
|
||||||
with patch.object(master.MasterView, 'viewable', new=False):
|
model_name='Widget',
|
||||||
# TODO: should inspect pyramid routes after this, to be certain
|
viewable=False,
|
||||||
|
editable=False):
|
||||||
master.MasterView.defaults(self.pyramid_config)
|
master.MasterView.defaults(self.pyramid_config)
|
||||||
del master.MasterView.model_name
|
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
# class methods
|
# class methods
|
||||||
|
@ -374,17 +374,75 @@ class TestMasterView(WebTestCase):
|
||||||
def test_view(self):
|
def test_view(self):
|
||||||
|
|
||||||
# sanity/coverage check using /settings/XXX
|
# 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'}
|
setting = {'name': 'foo.bar', 'value': 'baz'}
|
||||||
self.request.matchdict = {'name': 'foo.bar'}
|
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):
|
with patch.object(view, 'get_instance', return_value=setting):
|
||||||
response = view.view()
|
response = view.view()
|
||||||
del master.MasterView.model_name
|
|
||||||
del master.MasterView.grid_columns
|
def test_edit(self):
|
||||||
del master.MasterView.form_fields
|
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):
|
def test_configure(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
|
@ -65,3 +65,30 @@ class TestSettingView(WebTestCase):
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
title = view.get_instance_title(setting)
|
title = view.get_instance_title(setting)
|
||||||
self.assertEqual(title, 'foo')
|
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')
|
||||||
|
|
Loading…
Reference in a new issue