feat: add initial/basic forms support
This commit is contained in:
parent
0604651be5
commit
95d3623a5e
6
docs/api/wuttaweb/forms.base.rst
Normal file
6
docs/api/wuttaweb/forms.base.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
``wuttaweb.forms.base``
|
||||
=======================
|
||||
|
||||
.. automodule:: wuttaweb.forms.base
|
||||
:members:
|
6
docs/api/wuttaweb/forms.rst
Normal file
6
docs/api/wuttaweb/forms.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
``wuttaweb.forms``
|
||||
==================
|
||||
|
||||
.. automodule:: wuttaweb.forms
|
||||
:members:
|
|
@ -9,6 +9,8 @@
|
|||
|
||||
app
|
||||
db
|
||||
forms
|
||||
forms.base
|
||||
handler
|
||||
helpers
|
||||
menus
|
||||
|
|
|
@ -20,12 +20,15 @@ extensions = [
|
|||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.viewcode',
|
||||
'sphinx.ext.todo',
|
||||
]
|
||||
|
||||
templates_path = ['_templates']
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
intersphinx_mapping = {
|
||||
'colander': ('https://docs.pylonsproject.org/projects/colander/en/latest/', None),
|
||||
'deform': ('https://docs.pylonsproject.org/projects/deform/en/latest/', None),
|
||||
'pyramid': ('https://docs.pylonsproject.org/projects/pyramid/en/latest/', None),
|
||||
'python': ('https://docs.python.org/3/', None),
|
||||
'webhelpers2': ('https://webhelpers2.readthedocs.io/en/latest/', None),
|
||||
|
|
|
@ -32,6 +32,7 @@ requires-python = ">= 3.8"
|
|||
dependencies = [
|
||||
"pyramid>=2",
|
||||
"pyramid_beaker",
|
||||
"pyramid_deform",
|
||||
"pyramid_mako",
|
||||
"waitress",
|
||||
"WebHelpers2",
|
||||
|
|
|
@ -110,9 +110,13 @@ def make_pyramid_config(settings):
|
|||
The config is initialized with certain features deemed useful for
|
||||
all apps.
|
||||
"""
|
||||
settings.setdefault('pyramid_deform.template_search_path',
|
||||
'wuttaweb:templates/deform')
|
||||
|
||||
pyramid_config = Configurator(settings=settings)
|
||||
|
||||
pyramid_config.include('pyramid_beaker')
|
||||
pyramid_config.include('pyramid_deform')
|
||||
pyramid_config.include('pyramid_mako')
|
||||
|
||||
return pyramid_config
|
||||
|
|
31
src/wuttaweb/forms/__init__.py
Normal file
31
src/wuttaweb/forms/__init__.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# wuttaweb -- Web App for Wutta Framework
|
||||
# Copyright © 2024 Lance Edgar
|
||||
#
|
||||
# This file is part of Wutta Framework.
|
||||
#
|
||||
# Wutta Framework is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option) any
|
||||
# later version.
|
||||
#
|
||||
# Wutta Framework is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Forms Library
|
||||
|
||||
The ``wuttaweb.forms`` namespace contains the following:
|
||||
|
||||
* :class:`~wuttaweb.forms.base.Form`
|
||||
"""
|
||||
|
||||
from .base import Form
|
421
src/wuttaweb/forms/base.py
Normal file
421
src/wuttaweb/forms/base.py
Normal file
|
@ -0,0 +1,421 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# wuttaweb -- Web App for Wutta Framework
|
||||
# Copyright © 2024 Lance Edgar
|
||||
#
|
||||
# This file is part of Wutta Framework.
|
||||
#
|
||||
# Wutta Framework is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option) any
|
||||
# later version.
|
||||
#
|
||||
# Wutta Framework is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Base form classes
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
import colander
|
||||
import deform
|
||||
from pyramid.renderers import render
|
||||
from webhelpers2.html import HTML
|
||||
|
||||
from wuttaweb.util import get_form_data
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FieldList(list):
|
||||
"""
|
||||
Convenience wrapper for a form's field list. This is a subclass
|
||||
of :class:`python:list`.
|
||||
|
||||
You normally would not need to instantiate this yourself, but it
|
||||
is used under the hood for e.g. :attr:`Form.fields`.
|
||||
"""
|
||||
|
||||
def insert_before(self, field, newfield):
|
||||
"""
|
||||
Insert a new field, before an existing field.
|
||||
|
||||
:param field: String name for the existing field.
|
||||
|
||||
:param newfield: String name for the new field, to be inserted
|
||||
just before the existing ``field``.
|
||||
"""
|
||||
if field in self:
|
||||
i = self.index(field)
|
||||
self.insert(i, newfield)
|
||||
else:
|
||||
log.warning("field '%s' not found, will append new field: %s",
|
||||
field, newfield)
|
||||
self.append(newfield)
|
||||
|
||||
def insert_after(self, field, newfield):
|
||||
"""
|
||||
Insert a new field, after an existing field.
|
||||
|
||||
:param field: String name for the existing field.
|
||||
|
||||
:param newfield: String name for the new field, to be inserted
|
||||
just after the existing ``field``.
|
||||
"""
|
||||
if field in self:
|
||||
i = self.index(field)
|
||||
self.insert(i + 1, newfield)
|
||||
else:
|
||||
log.warning("field '%s' not found, will append new field: %s",
|
||||
field, newfield)
|
||||
self.append(newfield)
|
||||
|
||||
|
||||
class Form:
|
||||
"""
|
||||
Base class for all forms.
|
||||
|
||||
:param request: Reference to current :term:`request` object.
|
||||
|
||||
:param fields: List of field names for the form. This is
|
||||
optional; if not specified an attempt will be made to deduce
|
||||
the list automatically. See also :attr:`fields`.
|
||||
|
||||
:param schema: Colander-based schema object for the form. This is
|
||||
optional; if not specified an attempt will be made to construct
|
||||
one automatically. See also :meth:`get_schema()`.
|
||||
|
||||
:param labels: Optional dict of default field labels.
|
||||
|
||||
.. note::
|
||||
|
||||
Some parameters are not explicitly described above. However
|
||||
their corresponding attributes are described below.
|
||||
|
||||
Form instances contain the following attributes:
|
||||
|
||||
.. attribute:: fields
|
||||
|
||||
:class:`FieldList` instance containing string field names for
|
||||
the form. By default, fields will appear in the same order as
|
||||
they are in this list.
|
||||
|
||||
.. attribute:: request
|
||||
|
||||
Reference to current :term:`request` object.
|
||||
|
||||
.. attribute:: action_url
|
||||
|
||||
String URL to which the form should be submitted, if applicable.
|
||||
|
||||
.. attribute:: vue_tagname
|
||||
|
||||
String name for Vue component tag. By default this is
|
||||
``'wutta-form'``. See also :meth:`render_vue_tag()`.
|
||||
|
||||
.. attribute:: align_buttons_right
|
||||
|
||||
Flag indicating whether the buttons (submit, cancel etc.)
|
||||
should be aligned to the right of the area below the form. If
|
||||
not set, the buttons are left-aligned.
|
||||
|
||||
.. attribute:: auto_disable_submit
|
||||
|
||||
Flag indicating whether the submit button should be
|
||||
auto-disabled, whenever the form is submitted.
|
||||
|
||||
.. attribute:: button_label_submit
|
||||
|
||||
String label for the form submit button. Default is ``"Save"``.
|
||||
|
||||
.. attribute:: button_icon_submit
|
||||
|
||||
String icon name for the form submit button. Default is ``'save'``.
|
||||
|
||||
.. attribute:: show_button_reset
|
||||
|
||||
Flag indicating whether a Reset button should be shown.
|
||||
|
||||
.. attribute:: validated
|
||||
|
||||
If the :meth:`validate()` method was called, and it succeeded,
|
||||
this will be set to the validated data dict.
|
||||
|
||||
Note that in all other cases, this attribute may not exist.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
request,
|
||||
fields=None,
|
||||
schema=None,
|
||||
labels={},
|
||||
action_url=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,
|
||||
):
|
||||
self.request = request
|
||||
self.schema = schema
|
||||
self.labels = labels or {}
|
||||
self.action_url = action_url
|
||||
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.config = self.request.wutta_config
|
||||
self.app = self.config.get_app()
|
||||
|
||||
if fields is not None:
|
||||
self.set_fields(fields)
|
||||
elif self.schema:
|
||||
self.set_fields([f.name for f in self.schema])
|
||||
else:
|
||||
self.fields = None
|
||||
|
||||
def __contains__(self, name):
|
||||
"""
|
||||
Custom logic for the ``in`` operator, to allow easily checking
|
||||
if the form contains a given field::
|
||||
|
||||
myform = Form()
|
||||
if 'somefield' in myform:
|
||||
print("my form has some field")
|
||||
"""
|
||||
return bool(self.fields and name in self.fields)
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
Custom logic to allow iterating over form field names::
|
||||
|
||||
myform = Form(fields=['foo', 'bar'])
|
||||
for fieldname in myform:
|
||||
print(fieldname)
|
||||
"""
|
||||
return iter(self.fields)
|
||||
|
||||
@property
|
||||
def vue_component(self):
|
||||
"""
|
||||
String name for the Vue component, e.g. ``'WuttaForm'``.
|
||||
|
||||
This is a generated value based on :attr:`vue_tagname`.
|
||||
"""
|
||||
words = self.vue_tagname.split('-')
|
||||
return ''.join([word.capitalize() for word in words])
|
||||
|
||||
def set_fields(self, fields):
|
||||
"""
|
||||
Explicitly set the list of form fields.
|
||||
|
||||
This will overwrite :attr:`fields` with a new
|
||||
:class:`FieldList` instance.
|
||||
|
||||
:param fields: List of string field names.
|
||||
"""
|
||||
self.fields = FieldList(fields)
|
||||
|
||||
def set_label(self, key, label):
|
||||
"""
|
||||
Set the label for given field name.
|
||||
|
||||
See also :meth:`get_label()`.
|
||||
"""
|
||||
self.labels[key] = label
|
||||
|
||||
# update schema if necessary
|
||||
if self.schema and key in self.schema:
|
||||
self.schema[key].title = label
|
||||
|
||||
def get_label(self, key):
|
||||
"""
|
||||
Get the label for given field name.
|
||||
|
||||
Note that this will always return a string, auto-generating
|
||||
the label if needed.
|
||||
|
||||
See also :meth:`set_label()`.
|
||||
"""
|
||||
return self.labels.get(key, self.app.make_title(key))
|
||||
|
||||
def get_schema(self):
|
||||
"""
|
||||
Return the :class:`colander:colander.Schema` object for the
|
||||
form, generating it automatically if necessary.
|
||||
"""
|
||||
if not self.schema:
|
||||
raise NotImplementedError
|
||||
|
||||
return self.schema
|
||||
|
||||
def get_deform(self):
|
||||
"""
|
||||
Return the :class:`deform:deform.Form` instance for the form,
|
||||
generating it automatically if necessary.
|
||||
"""
|
||||
if not hasattr(self, 'deform_form'):
|
||||
schema = self.get_schema()
|
||||
form = deform.Form(schema)
|
||||
self.deform_form = form
|
||||
|
||||
return self.deform_form
|
||||
|
||||
def render_vue_tag(self, **kwargs):
|
||||
"""
|
||||
Render the Vue component tag for the form.
|
||||
|
||||
By default this simply returns:
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<wutta-form></wutta-form>
|
||||
|
||||
The actual output will depend on various form attributes, in
|
||||
particular :attr:`vue_tagname`.
|
||||
"""
|
||||
return HTML.tag(self.vue_tagname, **kwargs)
|
||||
|
||||
def render_vue_template(
|
||||
self,
|
||||
template='/forms/vue_template.mako',
|
||||
**context):
|
||||
"""
|
||||
Render the Vue template block for the form.
|
||||
|
||||
This returns something like:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
<script type="text/x-template" id="wutta-form-template">
|
||||
<form>
|
||||
<!-- fields etc. -->
|
||||
</form>
|
||||
</script>
|
||||
|
||||
.. todo::
|
||||
|
||||
Why can't Sphinx render the above code block as 'html' ?
|
||||
|
||||
It acts like it can't handle a ``<script>`` tag at all?
|
||||
|
||||
Actual output will of course depend on form attributes, i.e.
|
||||
:attr:`vue_tagname` and :attr:`fields` list etc.
|
||||
|
||||
:param template: Path to Mako template which is used to render
|
||||
the output.
|
||||
"""
|
||||
context['form'] = self
|
||||
context.setdefault('form_attrs', {})
|
||||
|
||||
# auto disable button on submit
|
||||
if self.auto_disable_submit:
|
||||
context['form_attrs']['@submit'] = 'formSubmitting = true'
|
||||
|
||||
output = render(template, context)
|
||||
return HTML.literal(output)
|
||||
|
||||
def render_vue_field(self, fieldname):
|
||||
"""
|
||||
Render the given field completely, i.e. ``<b-field>`` wrapper
|
||||
with label and containing a widget.
|
||||
|
||||
Actual output will depend on the field attributes etc.
|
||||
"""
|
||||
dform = self.get_deform()
|
||||
field = dform[fieldname]
|
||||
|
||||
# render the field widget or whatever
|
||||
html = field.serialize()
|
||||
html = HTML.literal(html)
|
||||
|
||||
# render field label
|
||||
label = self.get_label(fieldname)
|
||||
|
||||
# b-field attrs
|
||||
attrs = {
|
||||
':horizontal': 'true',
|
||||
'label': label,
|
||||
}
|
||||
|
||||
return HTML.tag('b-field', c=[html], **attrs)
|
||||
|
||||
def get_vue_field_value(self, field):
|
||||
"""
|
||||
This method returns a JSON string which will be assigned as
|
||||
the initial model value for the given field. This JSON will
|
||||
be written as part of the overall response, to be interpreted
|
||||
on the client side.
|
||||
|
||||
Again, this must return a *string* such as:
|
||||
|
||||
* ``'null'``
|
||||
* ``'{"foo": "bar"}'``
|
||||
|
||||
In practice this calls :meth:`jsonify_value()` to convert the
|
||||
``field.cstruct`` value to string.
|
||||
"""
|
||||
if isinstance(field, str):
|
||||
dform = self.get_deform()
|
||||
field = dform[field]
|
||||
|
||||
return self.jsonify_value(field.cstruct)
|
||||
|
||||
def jsonify_value(self, value):
|
||||
"""
|
||||
Convert a Python value to JSON string.
|
||||
|
||||
See also :meth:`get_vue_field_value()`.
|
||||
"""
|
||||
if value is colander.null:
|
||||
return 'null'
|
||||
|
||||
return json.dumps(value)
|
||||
|
||||
def validate(self):
|
||||
"""
|
||||
Try to validate the form.
|
||||
|
||||
This should work whether request data was submitted as classic
|
||||
POST data, or as JSON body.
|
||||
|
||||
If the form data is valid, this method returns the data dict.
|
||||
This data dict is also then available on the form object via
|
||||
the :attr:`validated` attribute.
|
||||
|
||||
However if the data is not valid, ``False`` is returned, and
|
||||
there will be no :attr:`validated` attribute.
|
||||
|
||||
:returns: Data dict, or ``False``.
|
||||
"""
|
||||
if hasattr(self, 'validated'):
|
||||
del self.validated
|
||||
|
||||
if self.request.method != 'POST':
|
||||
return False
|
||||
|
||||
dform = self.get_deform()
|
||||
controls = get_form_data(self.request).items()
|
||||
|
||||
try:
|
||||
self.validated = dform.validate(controls)
|
||||
except deform.ValidationFailure:
|
||||
return False
|
||||
|
||||
return self.validated
|
8
src/wuttaweb/templates/deform/password.pt
Normal file
8
src/wuttaweb/templates/deform/password.pt
Normal file
|
@ -0,0 +1,8 @@
|
|||
<div tal:omit-tag=""
|
||||
tal:define="name name|field.name;
|
||||
vmodel vmodel|'model_'+name;">
|
||||
<b-input name="${name}"
|
||||
v-model="${vmodel}"
|
||||
type="password"
|
||||
tal:attributes="attributes|field.widget.attributes|{};" />
|
||||
</div>
|
7
src/wuttaweb/templates/deform/textinput.pt
Normal file
7
src/wuttaweb/templates/deform/textinput.pt
Normal file
|
@ -0,0 +1,7 @@
|
|||
<div tal:omit-tag=""
|
||||
tal:define="name name|field.name;
|
||||
vmodel vmodel|'model_'+name;">
|
||||
<b-input name="${name}"
|
||||
v-model="${vmodel}"
|
||||
tal:attributes="attributes|field.widget.attributes|{};" />
|
||||
</div>
|
18
src/wuttaweb/templates/form.mako
Normal file
18
src/wuttaweb/templates/form.mako
Normal file
|
@ -0,0 +1,18 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/page.mako" />
|
||||
|
||||
<%def name="render_this_page_template()">
|
||||
${parent.render_this_page_template()}
|
||||
${form.render_vue_template()}
|
||||
</%def>
|
||||
|
||||
<%def name="finalize_this_page_vars()">
|
||||
${parent.finalize_this_page_vars()}
|
||||
<script>
|
||||
${form.vue_component}.data = function() { return ${form.vue_component}Data }
|
||||
Vue.component('${form.vue_tagname}', ${form.vue_component})
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
|
||||
${parent.body()}
|
58
src/wuttaweb/templates/forms/vue_template.mako
Normal file
58
src/wuttaweb/templates/forms/vue_template.mako
Normal file
|
@ -0,0 +1,58 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
|
||||
<script type="text/x-template" id="${form.vue_tagname}-template">
|
||||
${h.form(form.action_url, method='post', enctype='multipart/form-data', **form_attrs)}
|
||||
|
||||
<section>
|
||||
% for fieldname in form:
|
||||
${form.render_vue_field(fieldname)}
|
||||
% endfor
|
||||
</section>
|
||||
|
||||
<div style="margin-top: 1.5rem; display: flex; gap: 0.5rem; justify-content: end; width: 100%;">
|
||||
|
||||
% if form.show_button_reset:
|
||||
<b-button native-type="reset">
|
||||
Reset
|
||||
</b-button>
|
||||
% endif
|
||||
|
||||
<b-button type="is-primary"
|
||||
native-type="submit"
|
||||
% if form.auto_disable_submit:
|
||||
:disabled="formSubmitting"
|
||||
% endif
|
||||
icon-pack="fas"
|
||||
icon-left="${form.button_icon_submit}">
|
||||
% if form.auto_disable_submit:
|
||||
{{ formSubmitting ? "Working, please wait..." : "${form.button_label_submit}" }}
|
||||
% else:
|
||||
${form.button_label_submit}
|
||||
% endif
|
||||
</b-button>
|
||||
|
||||
</div>
|
||||
|
||||
${h.end_form()}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
|
||||
let ${form.vue_component} = {
|
||||
template: '#${form.vue_tagname}-template',
|
||||
methods: {},
|
||||
}
|
||||
|
||||
let ${form.vue_component}Data = {
|
||||
|
||||
## field model values
|
||||
% for key in form:
|
||||
model_${key}: ${form.get_vue_field_value(key)|n},
|
||||
% endfor
|
||||
|
||||
% if form.auto_disable_submit:
|
||||
formSubmitting: false,
|
||||
% endif
|
||||
}
|
||||
|
||||
</script>
|
|
@ -24,6 +24,10 @@
|
|||
Base Logic for Views
|
||||
"""
|
||||
|
||||
from pyramid import httpexceptions
|
||||
|
||||
from wuttaweb import forms
|
||||
|
||||
|
||||
class View:
|
||||
"""
|
||||
|
@ -35,8 +39,7 @@ class View:
|
|||
|
||||
.. attribute:: request
|
||||
|
||||
Reference to the current
|
||||
:class:`pyramid:pyramid.request.Request` object.
|
||||
Reference to the current :term:`request` object.
|
||||
|
||||
.. attribute:: app
|
||||
|
||||
|
@ -51,3 +54,30 @@ class View:
|
|||
self.request = request
|
||||
self.config = self.request.wutta_config
|
||||
self.app = self.config.get_app()
|
||||
|
||||
def make_form(self, **kwargs):
|
||||
"""
|
||||
Make and return a new :class:`~wuttaweb.forms.base.Form`
|
||||
instance, per the given ``kwargs``.
|
||||
|
||||
This is the "default" form factory which merely invokes
|
||||
the constructor.
|
||||
"""
|
||||
return forms.Form(self.request, **kwargs)
|
||||
|
||||
def redirect(self, url, **kwargs):
|
||||
"""
|
||||
Convenience method to return a HTTP 302 response.
|
||||
|
||||
Note that this technically returns an "exception" - so in
|
||||
your code, you can either return that error, or raise it::
|
||||
|
||||
return self.redirect('/')
|
||||
# ..or
|
||||
raise self.redirect('/')
|
||||
|
||||
Which you should do will depend on context, but raising the
|
||||
error is always "safe" since Pyramid will handle that
|
||||
correctly no matter what.
|
||||
"""
|
||||
return httpexceptions.HTTPFound(location=url, **kwargs)
|
||||
|
|
0
tests/forms/__init__.py
Normal file
0
tests/forms/__init__.py
Normal file
241
tests/forms/test_base.py
Normal file
241
tests/forms/test_base.py
Normal file
|
@ -0,0 +1,241 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
|
||||
from unittest import TestCase
|
||||
|
||||
import colander
|
||||
import deform
|
||||
from pyramid import testing
|
||||
|
||||
from wuttjamaican.conf import WuttaConfig
|
||||
from wuttaweb.forms import base
|
||||
from wuttaweb import helpers
|
||||
|
||||
|
||||
class TestFieldList(TestCase):
|
||||
|
||||
def test_insert_before(self):
|
||||
fields = base.FieldList(['f1', 'f2'])
|
||||
self.assertEqual(fields, ['f1', 'f2'])
|
||||
|
||||
# typical
|
||||
fields.insert_before('f1', 'XXX')
|
||||
self.assertEqual(fields, ['XXX', 'f1', 'f2'])
|
||||
fields.insert_before('f2', 'YYY')
|
||||
self.assertEqual(fields, ['XXX', 'f1', 'YYY', 'f2'])
|
||||
|
||||
# appends new field if reference field is invalid
|
||||
fields.insert_before('f3', 'ZZZ')
|
||||
self.assertEqual(fields, ['XXX', 'f1', 'YYY', 'f2', 'ZZZ'])
|
||||
|
||||
def test_insert_after(self):
|
||||
fields = base.FieldList(['f1', 'f2'])
|
||||
self.assertEqual(fields, ['f1', 'f2'])
|
||||
|
||||
# typical
|
||||
fields.insert_after('f1', 'XXX')
|
||||
self.assertEqual(fields, ['f1', 'XXX', 'f2'])
|
||||
fields.insert_after('XXX', 'YYY')
|
||||
self.assertEqual(fields, ['f1', 'XXX', 'YYY', 'f2'])
|
||||
|
||||
# appends new field if reference field is invalid
|
||||
fields.insert_after('f3', 'ZZZ')
|
||||
self.assertEqual(fields, ['f1', 'XXX', 'YYY', 'f2', 'ZZZ'])
|
||||
|
||||
|
||||
class TestForm(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.config = WuttaConfig()
|
||||
self.request = testing.DummyRequest(wutta_config=self.config)
|
||||
|
||||
self.pyramid_config = testing.setUp(request=self.request, settings={
|
||||
'mako.directories': ['wuttaweb:templates'],
|
||||
'pyramid_deform.template_search_path': 'wuttaweb:templates/deform',
|
||||
})
|
||||
|
||||
def tearDown(self):
|
||||
testing.tearDown()
|
||||
|
||||
def make_form(self, request=None, **kwargs):
|
||||
return base.Form(request or self.request, **kwargs)
|
||||
|
||||
def make_schema(self):
|
||||
schema = colander.Schema(children=[
|
||||
colander.SchemaNode(colander.String(),
|
||||
name='foo'),
|
||||
colander.SchemaNode(colander.String(),
|
||||
name='bar'),
|
||||
])
|
||||
return schema
|
||||
|
||||
def test_init_with_none(self):
|
||||
form = self.make_form()
|
||||
self.assertIsNone(form.fields)
|
||||
|
||||
def test_init_with_fields(self):
|
||||
form = self.make_form(fields=['foo', 'bar'])
|
||||
self.assertEqual(form.fields, ['foo', 'bar'])
|
||||
|
||||
def test_init_with_schema(self):
|
||||
schema = self.make_schema()
|
||||
form = self.make_form(schema=schema)
|
||||
self.assertEqual(form.fields, ['foo', 'bar'])
|
||||
|
||||
def test_vue_tagname(self):
|
||||
form = self.make_form()
|
||||
self.assertEqual(form.vue_tagname, 'wutta-form')
|
||||
|
||||
def test_vue_component(self):
|
||||
form = self.make_form()
|
||||
self.assertEqual(form.vue_component, 'WuttaForm')
|
||||
|
||||
def test_contains(self):
|
||||
form = self.make_form(fields=['foo', 'bar'])
|
||||
self.assertIn('foo', form)
|
||||
self.assertNotIn('baz', form)
|
||||
|
||||
def test_iter(self):
|
||||
form = self.make_form(fields=['foo', 'bar'])
|
||||
|
||||
fields = list(iter(form))
|
||||
self.assertEqual(fields, ['foo', 'bar'])
|
||||
|
||||
fields = []
|
||||
for field in form:
|
||||
fields.append(field)
|
||||
self.assertEqual(fields, ['foo', 'bar'])
|
||||
|
||||
def test_set_fields(self):
|
||||
form = self.make_form(fields=['foo', 'bar'])
|
||||
self.assertEqual(form.fields, ['foo', 'bar'])
|
||||
form.set_fields(['baz'])
|
||||
self.assertEqual(form.fields, ['baz'])
|
||||
|
||||
def test_get_schema(self):
|
||||
form = self.make_form()
|
||||
self.assertIsNone(form.schema)
|
||||
|
||||
# provided schema is returned
|
||||
schema = self.make_schema()
|
||||
form = self.make_form(schema=schema)
|
||||
self.assertIs(form.schema, schema)
|
||||
self.assertIs(form.get_schema(), schema)
|
||||
|
||||
# auto-generating schema not yet supported
|
||||
form = self.make_form(fields=['foo', 'bar'])
|
||||
self.assertIsNone(form.schema)
|
||||
self.assertRaises(NotImplementedError, form.get_schema)
|
||||
|
||||
def test_get_deform(self):
|
||||
schema = self.make_schema()
|
||||
form = self.make_form(schema=schema)
|
||||
self.assertFalse(hasattr(form, 'deform_form'))
|
||||
dform = form.get_deform()
|
||||
self.assertIsInstance(dform, deform.Form)
|
||||
self.assertIs(form.deform_form, dform)
|
||||
|
||||
def test_get_label(self):
|
||||
form = self.make_form(fields=['foo', 'bar'])
|
||||
self.assertEqual(form.get_label('foo'), "Foo")
|
||||
form.set_label('foo', "Baz")
|
||||
self.assertEqual(form.get_label('foo'), "Baz")
|
||||
|
||||
def test_set_label(self):
|
||||
form = self.make_form(fields=['foo', 'bar'])
|
||||
self.assertEqual(form.get_label('foo'), "Foo")
|
||||
form.set_label('foo', "Baz")
|
||||
self.assertEqual(form.get_label('foo'), "Baz")
|
||||
|
||||
# schema should be updated when setting label
|
||||
schema = self.make_schema()
|
||||
form = self.make_form(schema=schema)
|
||||
form.set_label('foo', "Woohoo")
|
||||
self.assertEqual(form.get_label('foo'), "Woohoo")
|
||||
self.assertEqual(schema['foo'].title, "Woohoo")
|
||||
|
||||
def test_render_vue_tag(self):
|
||||
schema = self.make_schema()
|
||||
form = self.make_form(schema=schema)
|
||||
html = form.render_vue_tag()
|
||||
self.assertEqual(html, '<wutta-form></wutta-form>')
|
||||
|
||||
def test_render_vue_template(self):
|
||||
self.pyramid_config.include('pyramid_mako')
|
||||
self.pyramid_config.add_subscriber('wuttaweb.subscribers.before_render',
|
||||
'pyramid.events.BeforeRender')
|
||||
|
||||
# form button is disabled on @submit by default
|
||||
schema = self.make_schema()
|
||||
form = self.make_form(schema=schema)
|
||||
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)
|
||||
html = form.render_vue_template()
|
||||
self.assertIn('<script type="text/x-template" id="wutta-form-template">', html)
|
||||
self.assertNotIn('@submit', html)
|
||||
|
||||
def test_render_vue_field(self):
|
||||
self.pyramid_config.include('pyramid_deform')
|
||||
|
||||
schema = self.make_schema()
|
||||
form = self.make_form(schema=schema)
|
||||
html = form.render_vue_field('foo')
|
||||
self.assertIn('<b-field :horizontal="true" label="Foo">', html)
|
||||
self.assertIn('<b-input name="foo"', html)
|
||||
|
||||
def test_get_vue_field_value(self):
|
||||
schema = self.make_schema()
|
||||
form = self.make_form(schema=schema)
|
||||
|
||||
# null field value
|
||||
value = form.get_vue_field_value('foo')
|
||||
self.assertEqual(value, 'null')
|
||||
|
||||
# non-default / explicit value
|
||||
# TODO: surely need a different approach to set value
|
||||
dform = form.get_deform()
|
||||
dform['foo'].cstruct = 'blarg'
|
||||
value = form.get_vue_field_value('foo')
|
||||
self.assertEqual(value, '"blarg"')
|
||||
|
||||
def test_jsonify_value(self):
|
||||
form = self.make_form()
|
||||
|
||||
# null field value
|
||||
value = form.jsonify_value(colander.null)
|
||||
self.assertEqual(value, 'null')
|
||||
value = form.jsonify_value(None)
|
||||
self.assertEqual(value, 'null')
|
||||
|
||||
# string value
|
||||
value = form.jsonify_value('blarg')
|
||||
self.assertEqual(value, '"blarg"')
|
||||
|
||||
def test_validate(self):
|
||||
schema = self.make_schema()
|
||||
form = self.make_form(schema=schema)
|
||||
self.assertFalse(hasattr(form, 'validated'))
|
||||
|
||||
# will not validate unless request is POST
|
||||
self.request.POST = {'foo': 'blarg', 'bar': 'baz'}
|
||||
self.request.method = 'GET'
|
||||
self.assertFalse(form.validate())
|
||||
self.request.method = 'POST'
|
||||
data = form.validate()
|
||||
self.assertEqual(data, {'foo': 'blarg', 'bar': 'baz'})
|
||||
|
||||
# validating a second type updates form.validated
|
||||
self.request.POST = {'foo': 'BLARG', 'bar': 'BAZ'}
|
||||
data = form.validate()
|
||||
self.assertEqual(data, {'foo': 'BLARG', 'bar': 'BAZ'})
|
||||
self.assertIs(form.validated, data)
|
||||
|
||||
# bad data does not validate
|
||||
self.request.POST = {'foo': 42, 'bar': None}
|
||||
self.assertFalse(form.validate())
|
||||
dform = form.get_deform()
|
||||
self.assertEqual(len(dform.error.children), 2)
|
||||
self.assertEqual(dform['foo'].errormsg, "Pstruct is not a string")
|
|
@ -3,19 +3,31 @@
|
|||
from unittest import TestCase
|
||||
|
||||
from pyramid import testing
|
||||
from pyramid.httpexceptions import HTTPFound
|
||||
|
||||
from wuttjamaican.conf import WuttaConfig
|
||||
from wuttaweb.views import base
|
||||
from wuttaweb.forms import Form
|
||||
|
||||
|
||||
class TestView(TestCase):
|
||||
|
||||
def test_basic(self):
|
||||
config = WuttaConfig()
|
||||
request = testing.DummyRequest()
|
||||
request.wutta_config = config
|
||||
def setUp(self):
|
||||
self.config = WuttaConfig()
|
||||
self.app = self.config.get_app()
|
||||
self.request = testing.DummyRequest(wutta_config=self.config)
|
||||
self.view = base.View(self.request)
|
||||
|
||||
view = base.View(request)
|
||||
self.assertIs(view.request, request)
|
||||
self.assertIs(view.config, config)
|
||||
self.assertIs(view.app, config.get_app())
|
||||
def test_basic(self):
|
||||
self.assertIs(self.view.request, self.request)
|
||||
self.assertIs(self.view.config, self.config)
|
||||
self.assertIs(self.view.app, self.app)
|
||||
|
||||
def test_make_form(self):
|
||||
form = self.view.make_form()
|
||||
self.assertIsInstance(form, Form)
|
||||
|
||||
def test_redirect(self):
|
||||
error = self.view.redirect('/')
|
||||
self.assertIsInstance(error, HTTPFound)
|
||||
self.assertEqual(error.location, '/')
|
||||
|
|
Loading…
Reference in a new issue