Compare commits
11 commits
6515a0a224
...
49b13306c4
Author | SHA1 | Date | |
---|---|---|---|
|
49b13306c4 | ||
|
b3f1f8b6d9 | ||
|
2de08ad50d | ||
|
7895ce4676 | ||
|
5cec585fdf | ||
|
86ffb5d58f | ||
|
170afe650b | ||
|
0631b8e16b | ||
|
a219f3e30d | ||
|
a612bf3846 | ||
|
08a895a07b |
20
CHANGELOG.md
20
CHANGELOG.md
|
@ -5,6 +5,26 @@ All notable changes to wuttaweb will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## v0.19.1 (2025-01-06)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- improve built-in grid renderer logic
|
||||||
|
- allow session injection for ObjectRef constructor
|
||||||
|
- improve rendering for batch row status
|
||||||
|
- add basic support for row grid "view" action links
|
||||||
|
- add "xref buttons" tool panel for master view
|
||||||
|
- add WuttaQuantity schema type, widget
|
||||||
|
- remove `session` param from some form schema, widget classes
|
||||||
|
- add grid renderers for bool, currency, quantity
|
||||||
|
- use proper bulma styles for markdown content
|
||||||
|
- use span element for readonly money field widget render
|
||||||
|
- include grid filters for all column properties of model class
|
||||||
|
- use app handler to render error string, when progress fails
|
||||||
|
- add schema node type, widget for "money" (currency) fields
|
||||||
|
- exclude FK fields by default, for model forms
|
||||||
|
- fix style for header title text
|
||||||
|
|
||||||
## v0.19.0 (2024-12-23)
|
## v0.19.0 (2024-12-23)
|
||||||
|
|
||||||
### Feat
|
### Feat
|
||||||
|
|
|
@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "WuttaWeb"
|
name = "WuttaWeb"
|
||||||
version = "0.19.0"
|
version = "0.19.1"
|
||||||
description = "Web App for Wutta Framework"
|
description = "Web App for Wutta Framework"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
|
authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
|
||||||
|
@ -45,7 +45,7 @@ dependencies = [
|
||||||
"SQLAlchemy-Utils",
|
"SQLAlchemy-Utils",
|
||||||
"waitress",
|
"waitress",
|
||||||
"WebHelpers2",
|
"WebHelpers2",
|
||||||
"WuttJamaican[db]>=0.19.1",
|
"WuttJamaican[db]>=0.19.2",
|
||||||
"zope.sqlalchemy>=1.5",
|
"zope.sqlalchemy>=1.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -177,25 +177,41 @@ class WuttaMoney(colander.Money):
|
||||||
return widgets.WuttaMoneyInputWidget(self.request, **kwargs)
|
return widgets.WuttaMoneyInputWidget(self.request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class WuttaQuantity(colander.Decimal):
|
||||||
|
"""
|
||||||
|
Custom schema type for "quantity" fields.
|
||||||
|
|
||||||
|
This is a subclass of :class:`colander:colander.Decimal` but uses
|
||||||
|
:class:`~wuttaweb.forms.widgets.WuttaQuantityWidget` by default.
|
||||||
|
|
||||||
|
:param request: Current :term:`request` object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, request, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.request = request
|
||||||
|
self.config = self.request.wutta_config
|
||||||
|
self.app = self.config.get_app()
|
||||||
|
|
||||||
|
def widget_maker(self, **kwargs):
|
||||||
|
""" """
|
||||||
|
return widgets.WuttaQuantityWidget(self.request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class WuttaSet(colander.Set):
|
class WuttaSet(colander.Set):
|
||||||
"""
|
"""
|
||||||
Custom schema type for :class:`python:set` fields.
|
Custom schema type for :class:`python:set` fields.
|
||||||
|
|
||||||
This is a subclass of :class:`colander.Set`, but adds
|
This is a subclass of :class:`colander.Set`.
|
||||||
Wutta-related params to the constructor.
|
|
||||||
|
|
||||||
:param request: Current :term:`request` object.
|
:param request: Current :term:`request` object.
|
||||||
|
|
||||||
:param session: Optional :term:`db session` to use instead of
|
|
||||||
:class:`wuttaweb.db.sess.Session`.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, request, session=None):
|
def __init__(self, request):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.request = request
|
self.request = request
|
||||||
self.config = self.request.wutta_config
|
self.config = self.request.wutta_config
|
||||||
self.app = self.config.get_app()
|
self.app = self.config.get_app()
|
||||||
self.session = session or Session()
|
|
||||||
|
|
||||||
|
|
||||||
class ObjectRef(colander.SchemaType):
|
class ObjectRef(colander.SchemaType):
|
||||||
|
@ -231,16 +247,16 @@ class ObjectRef(colander.SchemaType):
|
||||||
self,
|
self,
|
||||||
request,
|
request,
|
||||||
empty_option=None,
|
empty_option=None,
|
||||||
session=None,
|
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
|
# nb. allow session injection for tests
|
||||||
|
self.session = kwargs.pop('session', Session())
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.request = request
|
self.request = request
|
||||||
self.config = self.request.wutta_config
|
self.config = self.request.wutta_config
|
||||||
self.app = self.config.get_app()
|
self.app = self.config.get_app()
|
||||||
self.model_instance = None
|
self.model_instance = None
|
||||||
self.session = session or Session()
|
|
||||||
|
|
||||||
if empty_option:
|
if empty_option:
|
||||||
if empty_option is True:
|
if empty_option is True:
|
||||||
|
@ -472,7 +488,7 @@ class RoleRefs(WuttaSet):
|
||||||
:returns: Instance of
|
:returns: Instance of
|
||||||
:class:`~wuttaweb.forms.widgets.RoleRefsWidget`.
|
:class:`~wuttaweb.forms.widgets.RoleRefsWidget`.
|
||||||
"""
|
"""
|
||||||
kwargs.setdefault('session', self.session)
|
session = kwargs.setdefault('session', Session())
|
||||||
|
|
||||||
if 'values' not in kwargs:
|
if 'values' not in kwargs:
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
@ -480,17 +496,17 @@ class RoleRefs(WuttaSet):
|
||||||
|
|
||||||
# avoid built-ins which cannot be assigned to users
|
# avoid built-ins which cannot be assigned to users
|
||||||
avoid = {
|
avoid = {
|
||||||
auth.get_role_authenticated(self.session),
|
auth.get_role_authenticated(session),
|
||||||
auth.get_role_anonymous(self.session),
|
auth.get_role_anonymous(session),
|
||||||
}
|
}
|
||||||
avoid = set([role.uuid for role in avoid])
|
avoid = set([role.uuid for role in avoid])
|
||||||
|
|
||||||
# also avoid admin unless current user is root
|
# also avoid admin unless current user is root
|
||||||
if not self.request.is_root:
|
if not self.request.is_root:
|
||||||
avoid.add(auth.get_role_administrator(self.session).uuid)
|
avoid.add(auth.get_role_administrator(session).uuid)
|
||||||
|
|
||||||
# everything else can be (un)assigned for users
|
# everything else can be (un)assigned for users
|
||||||
roles = self.session.query(model.Role)\
|
roles = session.query(model.Role)\
|
||||||
.filter(~model.Role.uuid.in_(avoid))\
|
.filter(~model.Role.uuid.in_(avoid))\
|
||||||
.order_by(model.Role.name)\
|
.order_by(model.Role.name)\
|
||||||
.all()
|
.all()
|
||||||
|
@ -518,7 +534,7 @@ class UserRefs(WuttaSet):
|
||||||
:returns: Instance of
|
:returns: Instance of
|
||||||
:class:`~wuttaweb.forms.widgets.UserRefsWidget`.
|
:class:`~wuttaweb.forms.widgets.UserRefsWidget`.
|
||||||
"""
|
"""
|
||||||
kwargs.setdefault('session', self.session)
|
kwargs.setdefault('session', Session())
|
||||||
return widgets.UserRefsWidget(self.request, **kwargs)
|
return widgets.UserRefsWidget(self.request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -548,7 +564,7 @@ class Permissions(WuttaSet):
|
||||||
:returns: Instance of
|
:returns: Instance of
|
||||||
:class:`~wuttaweb.forms.widgets.PermissionsWidget`.
|
:class:`~wuttaweb.forms.widgets.PermissionsWidget`.
|
||||||
"""
|
"""
|
||||||
kwargs.setdefault('session', self.session)
|
kwargs.setdefault('session', Session())
|
||||||
kwargs.setdefault('permissions', self.permissions)
|
kwargs.setdefault('permissions', self.permissions)
|
||||||
|
|
||||||
if 'values' not in kwargs:
|
if 'values' not in kwargs:
|
||||||
|
|
|
@ -136,26 +136,21 @@ class WuttaCheckboxChoiceWidget(CheckboxChoiceWidget):
|
||||||
Custom widget for :class:`python:set` fields.
|
Custom widget for :class:`python:set` fields.
|
||||||
|
|
||||||
This is a subclass of
|
This is a subclass of
|
||||||
:class:`deform:deform.widget.CheckboxChoiceWidget`, but adds
|
:class:`deform:deform.widget.CheckboxChoiceWidget`.
|
||||||
Wutta-related params to the constructor.
|
|
||||||
|
|
||||||
:param request: Current :term:`request` object.
|
:param request: Current :term:`request` object.
|
||||||
|
|
||||||
:param session: Optional :term:`db session` to use instead of
|
|
||||||
:class:`wuttaweb.db.sess.Session`.
|
|
||||||
|
|
||||||
It uses these Deform templates:
|
It uses these Deform templates:
|
||||||
|
|
||||||
* ``checkbox_choice``
|
* ``checkbox_choice``
|
||||||
* ``readonly/checkbox_choice``
|
* ``readonly/checkbox_choice``
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, request, session=None, *args, **kwargs):
|
def __init__(self, request, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.request = request
|
self.request = request
|
||||||
self.config = self.request.wutta_config
|
self.config = self.request.wutta_config
|
||||||
self.app = self.config.get_app()
|
self.app = self.config.get_app()
|
||||||
self.session = session or Session()
|
|
||||||
|
|
||||||
|
|
||||||
class WuttaDateTimeWidget(DateTimeInputWidget):
|
class WuttaDateTimeWidget(DateTimeInputWidget):
|
||||||
|
@ -231,6 +226,42 @@ class WuttaMoneyInputWidget(MoneyInputWidget):
|
||||||
return super().serialize(field, cstruct, **kw)
|
return super().serialize(field, cstruct, **kw)
|
||||||
|
|
||||||
|
|
||||||
|
class WuttaQuantityWidget(TextInputWidget):
|
||||||
|
"""
|
||||||
|
Custom widget for "quantity" fields. This is used by default for
|
||||||
|
:class:`~wuttaweb.forms.schema.WuttaQuantity` type nodes.
|
||||||
|
|
||||||
|
The main purpose of this widget is to leverage
|
||||||
|
:meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_quantity()`
|
||||||
|
for the readonly display.
|
||||||
|
|
||||||
|
This is a subclass of
|
||||||
|
:class:`deform:deform.widget.TextInputWidget` and uses these
|
||||||
|
Deform templates:
|
||||||
|
|
||||||
|
* ``textinput``
|
||||||
|
|
||||||
|
:param request: Current :term:`request` object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, request, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.request = request
|
||||||
|
self.config = self.request.wutta_config
|
||||||
|
self.app = self.config.get_app()
|
||||||
|
|
||||||
|
def serialize(self, field, cstruct, **kw):
|
||||||
|
""" """
|
||||||
|
readonly = kw.get('readonly', self.readonly)
|
||||||
|
if readonly:
|
||||||
|
if cstruct in (colander.null, None):
|
||||||
|
return HTML.tag('span')
|
||||||
|
cstruct = decimal.Decimal(cstruct)
|
||||||
|
return HTML.tag('span', c=[self.app.render_quantity(cstruct)])
|
||||||
|
|
||||||
|
return super().serialize(field, cstruct, **kw)
|
||||||
|
|
||||||
|
|
||||||
class FileDownloadWidget(Widget):
|
class FileDownloadWidget(Widget):
|
||||||
"""
|
"""
|
||||||
Widget for use with :class:`~wuttaweb.forms.schema.FileDownload`
|
Widget for use with :class:`~wuttaweb.forms.schema.FileDownload`
|
||||||
|
|
|
@ -116,7 +116,8 @@ class Grid:
|
||||||
|
|
||||||
Dict of column (cell) value renderer overrides.
|
Dict of column (cell) value renderer overrides.
|
||||||
|
|
||||||
See also :meth:`set_renderer()`.
|
See also :meth:`set_renderer()` and
|
||||||
|
:meth:`set_default_renderers()`.
|
||||||
|
|
||||||
.. attribute:: row_class
|
.. attribute:: row_class
|
||||||
|
|
||||||
|
@ -388,7 +389,6 @@ class Grid:
|
||||||
self.key = key
|
self.key = key
|
||||||
self.data = data
|
self.data = data
|
||||||
self.labels = labels or {}
|
self.labels = labels or {}
|
||||||
self.renderers = renderers or {}
|
|
||||||
self.row_class = row_class
|
self.row_class = row_class
|
||||||
self.actions = actions or []
|
self.actions = actions or []
|
||||||
self.linked_columns = linked_columns or []
|
self.linked_columns = linked_columns or []
|
||||||
|
@ -398,6 +398,10 @@ class Grid:
|
||||||
self.app = self.config.get_app()
|
self.app = self.config.get_app()
|
||||||
|
|
||||||
self.set_columns(columns or self.get_columns())
|
self.set_columns(columns or self.get_columns())
|
||||||
|
self.renderers = {}
|
||||||
|
if renderers:
|
||||||
|
for key, val in renderers.items():
|
||||||
|
self.set_renderer(key, val)
|
||||||
self.set_default_renderers()
|
self.set_default_renderers()
|
||||||
self.set_tools(tools)
|
self.set_tools(tools)
|
||||||
|
|
||||||
|
@ -592,8 +596,29 @@ class Grid:
|
||||||
grid = Grid(request, columns=['foo', 'bar'])
|
grid = Grid(request, columns=['foo', 'bar'])
|
||||||
grid.set_renderer('foo', render_foo)
|
grid.set_renderer('foo', render_foo)
|
||||||
|
|
||||||
|
For convenience, in lieu of a renderer callable, you may
|
||||||
|
specify one of the following strings, which will be
|
||||||
|
interpreted as a built-in renderer callable, as shown below:
|
||||||
|
|
||||||
|
* ``'batch_id'`` -> :meth:`render_batch_id()`
|
||||||
|
* ``'boolean'`` -> :meth:`render_boolean()`
|
||||||
|
* ``'currency'`` -> :meth:`render_currency()`
|
||||||
|
* ``'datetime'`` -> :meth:`render_datetime()`
|
||||||
|
* ``'quantity'`` -> :meth:`render_quantity()`
|
||||||
|
|
||||||
Renderer overrides are tracked via :attr:`renderers`.
|
Renderer overrides are tracked via :attr:`renderers`.
|
||||||
"""
|
"""
|
||||||
|
builtins = {
|
||||||
|
'batch_id': self.render_batch_id,
|
||||||
|
'boolean': self.render_boolean,
|
||||||
|
'currency': self.render_currency,
|
||||||
|
'datetime': self.render_datetime,
|
||||||
|
'quantity': self.render_quantity,
|
||||||
|
}
|
||||||
|
|
||||||
|
if renderer in builtins:
|
||||||
|
renderer = builtins[renderer]
|
||||||
|
|
||||||
if kwargs:
|
if kwargs:
|
||||||
renderer = functools.partial(renderer, **kwargs)
|
renderer = functools.partial(renderer, **kwargs)
|
||||||
self.renderers[key] = renderer
|
self.renderers[key] = renderer
|
||||||
|
@ -602,15 +627,18 @@ class Grid:
|
||||||
"""
|
"""
|
||||||
Set default column value renderers, where applicable.
|
Set default column value renderers, where applicable.
|
||||||
|
|
||||||
This will add new entries to :attr:`renderers` for columns
|
This is called automatically from the class constructor. It
|
||||||
whose data type implies a default renderer should be used.
|
will add new entries to :attr:`renderers` for columns whose
|
||||||
This is generally only possible if :attr:`model_class` is set
|
data type implies a default renderer. This is only possible
|
||||||
to a valid SQLAlchemy mapped class.
|
if :attr:`model_class` is set to a SQLAlchemy mapped class.
|
||||||
|
|
||||||
This (for now?) only looks for
|
This only looks for a couple of data types, and configures as
|
||||||
:class:`sqlalchemy:sqlalchemy.types.DateTime` columns and if
|
follows:
|
||||||
any are found, they are configured to use
|
|
||||||
:meth:`render_datetime()`.
|
* :class:`sqlalchemy:sqlalchemy.types.Boolean` ->
|
||||||
|
:meth:`render_boolean()`
|
||||||
|
* :class:`sqlalchemy:sqlalchemy.types.DateTime` ->
|
||||||
|
:meth:`render_datetime()`
|
||||||
"""
|
"""
|
||||||
if not self.model_class:
|
if not self.model_class:
|
||||||
return
|
return
|
||||||
|
@ -626,6 +654,8 @@ class Grid:
|
||||||
column = prop.columns[0]
|
column = prop.columns[0]
|
||||||
if isinstance(column.type, sa.DateTime):
|
if isinstance(column.type, sa.DateTime):
|
||||||
self.set_renderer(key, self.render_datetime)
|
self.set_renderer(key, self.render_datetime)
|
||||||
|
elif isinstance(column.type, sa.Boolean):
|
||||||
|
self.set_renderer(key, self.render_boolean)
|
||||||
|
|
||||||
def set_link(self, key, link=True):
|
def set_link(self, key, link=True):
|
||||||
"""
|
"""
|
||||||
|
@ -1753,23 +1783,80 @@ class Grid:
|
||||||
# rendering methods
|
# rendering methods
|
||||||
##############################
|
##############################
|
||||||
|
|
||||||
|
def render_batch_id(self, obj, key, value):
|
||||||
|
"""
|
||||||
|
Column renderer for batch ID values.
|
||||||
|
|
||||||
|
This is not used automatically but you can use it explicitly::
|
||||||
|
|
||||||
|
grid.set_renderer('foo', 'batch_id')
|
||||||
|
"""
|
||||||
|
if value is None:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
batch_id = int(value)
|
||||||
|
return f'{batch_id:08d}'
|
||||||
|
|
||||||
|
def render_boolean(self, obj, key, value):
|
||||||
|
"""
|
||||||
|
Column renderer for boolean values.
|
||||||
|
|
||||||
|
This calls
|
||||||
|
:meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_boolean()`
|
||||||
|
for the return value.
|
||||||
|
|
||||||
|
This may be used automatically per
|
||||||
|
:meth:`set_default_renderers()` or you can use it explicitly::
|
||||||
|
|
||||||
|
grid.set_renderer('foo', grid.render_boolean)
|
||||||
|
"""
|
||||||
|
return self.app.render_boolean(value)
|
||||||
|
|
||||||
|
def render_currency(self, obj, key, value, **kwargs):
|
||||||
|
"""
|
||||||
|
Column renderer for currency values.
|
||||||
|
|
||||||
|
This calls
|
||||||
|
:meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_currency()`
|
||||||
|
for the return value.
|
||||||
|
|
||||||
|
This is not used automatically but you can use it explicitly::
|
||||||
|
|
||||||
|
grid.set_renderer('foo', 'currency')
|
||||||
|
grid.set_renderer('foo', 'currency', scale=4)
|
||||||
|
"""
|
||||||
|
return self.app.render_currency(value, **kwargs)
|
||||||
|
|
||||||
def render_datetime(self, obj, key, value):
|
def render_datetime(self, obj, key, value):
|
||||||
"""
|
"""
|
||||||
Default cell value renderer for
|
Column renderer for :class:`python:datetime.datetime` values.
|
||||||
:class:`sqlalchemy:sqlalchemy.types.DateTime` columns, which
|
|
||||||
calls
|
This calls
|
||||||
:meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_datetime()`
|
:meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_datetime()`
|
||||||
for the return value.
|
for the return value.
|
||||||
|
|
||||||
This may be used automatically per
|
This may be used automatically per
|
||||||
:meth:`set_default_renderers()` or you can use it explicitly
|
:meth:`set_default_renderers()` or you can use it explicitly::
|
||||||
for any :class:`python:datetime.datetime` column with::
|
|
||||||
|
|
||||||
grid.set_renderer('foo', grid.render_datetime)
|
grid.set_renderer('foo', grid.render_datetime)
|
||||||
"""
|
"""
|
||||||
dt = getattr(obj, key)
|
dt = getattr(obj, key)
|
||||||
return self.app.render_datetime(dt)
|
return self.app.render_datetime(dt)
|
||||||
|
|
||||||
|
def render_quantity(self, obj, key, value):
|
||||||
|
"""
|
||||||
|
Column renderer for quantity values.
|
||||||
|
|
||||||
|
This calls
|
||||||
|
:meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_quantity()`
|
||||||
|
for the return value.
|
||||||
|
|
||||||
|
This is not used automatically but you can use it explicitly::
|
||||||
|
|
||||||
|
grid.set_renderer('foo', grid.render_quantity)
|
||||||
|
"""
|
||||||
|
return self.app.render_quantity(value)
|
||||||
|
|
||||||
def render_table_element(
|
def render_table_element(
|
||||||
self,
|
self,
|
||||||
form=None,
|
form=None,
|
||||||
|
|
|
@ -1,19 +1,6 @@
|
||||||
## -*- coding: utf-8; -*-
|
## -*- coding: utf-8; -*-
|
||||||
<%inherit file="/master/view.mako" />
|
<%inherit file="/master/view.mako" />
|
||||||
|
|
||||||
<%def name="extra_styles()">
|
|
||||||
${parent.extra_styles()}
|
|
||||||
<style>
|
|
||||||
|
|
||||||
## TODO: should we do something like this site-wide?
|
|
||||||
## (so far this is the only place we use markdown)
|
|
||||||
.markdown p {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
<%def name="tool_panels()">
|
<%def name="tool_panels()">
|
||||||
${parent.tool_panels()}
|
${parent.tool_panels()}
|
||||||
${self.tool_panel_execution()}
|
${self.tool_panel_execution()}
|
||||||
|
@ -65,7 +52,7 @@
|
||||||
<p class="block has-text-weight-bold">
|
<p class="block has-text-weight-bold">
|
||||||
What will happen when this batch is executed?
|
What will happen when this batch is executed?
|
||||||
</p>
|
</p>
|
||||||
<div class="markdown">
|
<div class="content">
|
||||||
${execution_described|n}
|
${execution_described|n}
|
||||||
</div>
|
</div>
|
||||||
${h.form(master.get_action_url('execute', batch), ref='executeForm')}
|
${h.form(master.get_action_url('execute', batch), ref='executeForm')}
|
||||||
|
|
|
@ -33,6 +33,21 @@
|
||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
<%def name="tool_panels()">
|
||||||
|
${parent.tool_panels()}
|
||||||
|
${self.tool_panel_xref()}
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="tool_panel_xref()">
|
||||||
|
% if xref_buttons:
|
||||||
|
<wutta-tool-panel heading="Cross-Reference">
|
||||||
|
% for button in xref_buttons:
|
||||||
|
${button}
|
||||||
|
% endfor
|
||||||
|
</wutta-tool-panel>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
<%def name="render_vue_templates()">
|
<%def name="render_vue_templates()">
|
||||||
${parent.render_vue_templates()}
|
${parent.render_vue_templates()}
|
||||||
% if master.has_rows:
|
% if master.has_rows:
|
||||||
|
|
84
src/wuttaweb/testing.py
Normal file
84
src/wuttaweb/testing.py
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
# -*- 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
WuttaWeb - test utilities
|
||||||
|
"""
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import fanstatic
|
||||||
|
from pyramid import testing
|
||||||
|
|
||||||
|
from wuttjamaican.testing import DataTestCase
|
||||||
|
|
||||||
|
from wuttaweb import subscribers
|
||||||
|
|
||||||
|
|
||||||
|
class WebTestCase(DataTestCase):
|
||||||
|
"""
|
||||||
|
Base class for test suites requiring a full (typical) web app.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.setup_web()
|
||||||
|
|
||||||
|
def setup_web(self):
|
||||||
|
self.setup_db()
|
||||||
|
self.request = self.make_request()
|
||||||
|
self.pyramid_config = testing.setUp(request=self.request, settings={
|
||||||
|
'wutta_config': self.config,
|
||||||
|
'mako.directories': ['wuttaweb:templates'],
|
||||||
|
'pyramid_deform.template_search_path': 'wuttaweb:templates/deform',
|
||||||
|
})
|
||||||
|
|
||||||
|
# init web
|
||||||
|
self.pyramid_config.include('pyramid_deform')
|
||||||
|
self.pyramid_config.include('pyramid_mako')
|
||||||
|
self.pyramid_config.add_directive('add_wutta_permission_group',
|
||||||
|
'wuttaweb.auth.add_permission_group')
|
||||||
|
self.pyramid_config.add_directive('add_wutta_permission',
|
||||||
|
'wuttaweb.auth.add_permission')
|
||||||
|
self.pyramid_config.add_subscriber('wuttaweb.subscribers.before_render',
|
||||||
|
'pyramid.events.BeforeRender')
|
||||||
|
self.pyramid_config.include('wuttaweb.static')
|
||||||
|
|
||||||
|
# nb. mock out fanstatic env..good enough for now to avoid errors..
|
||||||
|
needed = fanstatic.init_needed()
|
||||||
|
self.request.environ[fanstatic.NEEDED] = needed
|
||||||
|
|
||||||
|
# setup new request w/ anonymous user
|
||||||
|
event = MagicMock(request=self.request)
|
||||||
|
subscribers.new_request(event)
|
||||||
|
def user_getter(request, **kwargs): pass
|
||||||
|
subscribers.new_request_set_user(event, db_session=self.session,
|
||||||
|
user_getter=user_getter)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.teardown_web()
|
||||||
|
|
||||||
|
def teardown_web(self):
|
||||||
|
testing.tearDown()
|
||||||
|
self.teardown_db()
|
||||||
|
|
||||||
|
def make_request(self):
|
||||||
|
return testing.DummyRequest()
|
|
@ -53,7 +53,7 @@ class BatchMasterView(MasterView):
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
'id': "Batch ID",
|
'id': "Batch ID",
|
||||||
'status_code': "Batch Status",
|
'status_code': "Status",
|
||||||
}
|
}
|
||||||
|
|
||||||
sort_defaults = ('id', 'desc')
|
sort_defaults = ('id', 'desc')
|
||||||
|
@ -62,6 +62,10 @@ class BatchMasterView(MasterView):
|
||||||
rows_title = "Batch Rows"
|
rows_title = "Batch Rows"
|
||||||
rows_sort_defaults = 'sequence'
|
rows_sort_defaults = 'sequence'
|
||||||
|
|
||||||
|
row_labels = {
|
||||||
|
'status_code': "Status",
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, request, context=None):
|
def __init__(self, request, context=None):
|
||||||
super().__init__(request, context=context)
|
super().__init__(request, context=context)
|
||||||
self.batch_handler = self.get_batch_handler()
|
self.batch_handler = self.get_batch_handler()
|
||||||
|
@ -381,6 +385,12 @@ class BatchMasterView(MasterView):
|
||||||
|
|
||||||
g.set_label('sequence', "Seq.", column_only=True)
|
g.set_label('sequence', "Seq.", column_only=True)
|
||||||
|
|
||||||
|
g.set_renderer('status_code', self.render_row_status)
|
||||||
|
|
||||||
|
def render_row_status(self, row, key, value):
|
||||||
|
""" """
|
||||||
|
return row.STATUS.get(value, value)
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
# configuration
|
# configuration
|
||||||
##############################
|
##############################
|
||||||
|
|
|
@ -372,6 +372,20 @@ class MasterView(View):
|
||||||
List of columns for the row grid.
|
List of columns for the row grid.
|
||||||
|
|
||||||
This is optional; see also :meth:`get_row_grid_columns()`.
|
This is optional; see also :meth:`get_row_grid_columns()`.
|
||||||
|
|
||||||
|
This is optional; see also :meth:`get_row_grid_columns()`.
|
||||||
|
|
||||||
|
.. attribute:: rows_viewable
|
||||||
|
|
||||||
|
Boolean indicating whether the row model supports "viewing" -
|
||||||
|
i.e. it should have a "View" action in the row grid.
|
||||||
|
|
||||||
|
(For now) If you enable this, you must also override
|
||||||
|
:meth:`get_row_action_url_view()`.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
This eventually will cause there to be a ``row_view`` route
|
||||||
|
to be configured as well.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
|
@ -409,6 +423,7 @@ class MasterView(View):
|
||||||
rows_sort_defaults = None
|
rows_sort_defaults = None
|
||||||
rows_paginated = True
|
rows_paginated = True
|
||||||
rows_paginate_on_backend = True
|
rows_paginate_on_backend = True
|
||||||
|
rows_viewable = False
|
||||||
|
|
||||||
# current action
|
# current action
|
||||||
listing = False
|
listing = False
|
||||||
|
@ -601,6 +616,8 @@ class MasterView(View):
|
||||||
|
|
||||||
context['rows_grid'] = grid
|
context['rows_grid'] = grid
|
||||||
|
|
||||||
|
context['xref_buttons'] = self.get_xref_buttons(obj)
|
||||||
|
|
||||||
return self.render_to_response('view', context)
|
return self.render_to_response('view', context)
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
|
@ -1569,6 +1586,7 @@ class MasterView(View):
|
||||||
label,
|
label,
|
||||||
variant=None,
|
variant=None,
|
||||||
primary=False,
|
primary=False,
|
||||||
|
url=None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
|
@ -1595,10 +1613,17 @@ class MasterView(View):
|
||||||
avoids the Buefy vs. Oruga confusion, and the
|
avoids the Buefy vs. Oruga confusion, and the
|
||||||
implementation can change in the future.
|
implementation can change in the future.
|
||||||
|
|
||||||
|
:param url: Specify this (instead of ``href``) to make the
|
||||||
|
button act like a link. This will yield something like:
|
||||||
|
``<b-button tag="a" href="{url}">``
|
||||||
|
|
||||||
:param \**kwargs: All remaining kwargs are passed to the
|
:param \**kwargs: All remaining kwargs are passed to the
|
||||||
underlying ``HTML.tag()`` call, so will be rendered as
|
underlying ``HTML.tag()`` call, so will be rendered as
|
||||||
attributes on the button tag.
|
attributes on the button tag.
|
||||||
|
|
||||||
|
**NB.** You cannot specify a ``tag`` kwarg, for technical
|
||||||
|
reasons.
|
||||||
|
|
||||||
:returns: HTML literal for the button element. Will be something
|
:returns: HTML literal for the button element. Will be something
|
||||||
along the lines of:
|
along the lines of:
|
||||||
|
|
||||||
|
@ -1620,7 +1645,45 @@ class MasterView(View):
|
||||||
elif primary:
|
elif primary:
|
||||||
btn_kw['type'] = 'is-primary'
|
btn_kw['type'] = 'is-primary'
|
||||||
|
|
||||||
return HTML.tag('b-button', **btn_kw)
|
if url:
|
||||||
|
btn_kw['href'] = url
|
||||||
|
|
||||||
|
button = HTML.tag('b-button', **btn_kw)
|
||||||
|
|
||||||
|
if url:
|
||||||
|
# nb. unfortunately HTML.tag() calls its first arg 'tag'
|
||||||
|
# and so we can't pass a kwarg with that name...so instead
|
||||||
|
# we patch that into place manually
|
||||||
|
button = str(button)
|
||||||
|
button = button.replace('<b-button ',
|
||||||
|
'<b-button tag="a" ')
|
||||||
|
button = HTML.literal(button)
|
||||||
|
|
||||||
|
return button
|
||||||
|
|
||||||
|
def get_xref_buttons(self, obj):
|
||||||
|
"""
|
||||||
|
Should return a list of "cross-reference" buttons to be shown
|
||||||
|
when viewing the given object.
|
||||||
|
|
||||||
|
Default logic always returns empty list; subclass can override
|
||||||
|
as needed.
|
||||||
|
|
||||||
|
If applicable, this method should do its own permission checks
|
||||||
|
and only include the buttons current user should be allowed to
|
||||||
|
see/use.
|
||||||
|
|
||||||
|
See also :meth:`make_button()` - example::
|
||||||
|
|
||||||
|
def get_xref_buttons(self, product):
|
||||||
|
buttons = []
|
||||||
|
if self.request.has_perm('external_products.view'):
|
||||||
|
url = self.request.route_url('external_products.view',
|
||||||
|
id=product.external_id)
|
||||||
|
buttons.append(self.make_button("View External", url=url))
|
||||||
|
return buttons
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
def make_progress(self, key, **kwargs):
|
def make_progress(self, key, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -2366,6 +2429,16 @@ class MasterView(View):
|
||||||
kwargs.setdefault('paginated', self.rows_paginated)
|
kwargs.setdefault('paginated', self.rows_paginated)
|
||||||
kwargs.setdefault('paginate_on_backend', self.rows_paginate_on_backend)
|
kwargs.setdefault('paginate_on_backend', self.rows_paginate_on_backend)
|
||||||
|
|
||||||
|
if 'actions' not in kwargs:
|
||||||
|
actions = []
|
||||||
|
|
||||||
|
if self.rows_viewable:
|
||||||
|
actions.append(self.make_grid_action('view', icon='eye',
|
||||||
|
url=self.get_row_action_url_view))
|
||||||
|
|
||||||
|
if actions:
|
||||||
|
kwargs['actions'] = actions
|
||||||
|
|
||||||
grid = self.make_grid(**kwargs)
|
grid = self.make_grid(**kwargs)
|
||||||
self.configure_row_grid(grid)
|
self.configure_row_grid(grid)
|
||||||
grid.load_settings()
|
grid.load_settings()
|
||||||
|
@ -2484,6 +2557,16 @@ class MasterView(View):
|
||||||
labels.update(cls.row_labels)
|
labels.update(cls.row_labels)
|
||||||
return labels
|
return labels
|
||||||
|
|
||||||
|
def get_row_action_url_view(self, row, i):
|
||||||
|
"""
|
||||||
|
Must return the "view" action url for the given row object.
|
||||||
|
|
||||||
|
Only relevant if :attr:`rows_viewable` is true.
|
||||||
|
|
||||||
|
There is no default logic; subclass must override if needed.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
# class methods
|
# class methods
|
||||||
##############################
|
##############################
|
||||||
|
|
|
@ -4,9 +4,8 @@ from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from tests.util import WebTestCase
|
|
||||||
|
|
||||||
from wuttaweb.db import continuum as mod
|
from wuttaweb.db import continuum as mod
|
||||||
|
from wuttaweb.testing import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestWuttaWebContinuumPlugin(WebTestCase):
|
class TestWuttaWebContinuumPlugin(WebTestCase):
|
||||||
|
|
|
@ -12,7 +12,7 @@ from sqlalchemy import orm
|
||||||
from wuttjamaican.conf import WuttaConfig
|
from wuttjamaican.conf import WuttaConfig
|
||||||
from wuttaweb.forms import schema as mod
|
from wuttaweb.forms import schema as mod
|
||||||
from wuttaweb.forms import widgets
|
from wuttaweb.forms import widgets
|
||||||
from tests.util import DataTestCase, WebTestCase
|
from wuttaweb.testing import DataTestCase, WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestWutaDateTime(TestCase):
|
class TestWutaDateTime(TestCase):
|
||||||
|
@ -89,6 +89,15 @@ class TestWuttaMoney(WebTestCase):
|
||||||
self.assertIsInstance(widget, widgets.WuttaMoneyInputWidget)
|
self.assertIsInstance(widget, widgets.WuttaMoneyInputWidget)
|
||||||
|
|
||||||
|
|
||||||
|
class TestWuttaQuantity(WebTestCase):
|
||||||
|
|
||||||
|
def test_widget_maker(self):
|
||||||
|
enum = self.app.enum
|
||||||
|
typ = mod.WuttaQuantity(self.request)
|
||||||
|
widget = typ.widget_maker()
|
||||||
|
self.assertIsInstance(widget, widgets.WuttaQuantityWidget)
|
||||||
|
|
||||||
|
|
||||||
class TestObjectRef(DataTestCase):
|
class TestObjectRef(DataTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -155,7 +164,8 @@ class TestObjectRef(DataTestCase):
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
self.assertIsNotNone(person.uuid)
|
self.assertIsNotNone(person.uuid)
|
||||||
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
|
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
|
||||||
typ = mod.ObjectRef(self.request, session=self.session)
|
with patch.object(mod, 'Session', return_value=self.session):
|
||||||
|
typ = mod.ObjectRef(self.request)
|
||||||
value = typ.deserialize(node, person.uuid)
|
value = typ.deserialize(node, person.uuid)
|
||||||
self.assertIs(value, person)
|
self.assertIs(value, person)
|
||||||
|
|
||||||
|
@ -181,6 +191,8 @@ class TestObjectRef(DataTestCase):
|
||||||
value = typ.objectify(None)
|
value = typ.objectify(None)
|
||||||
self.assertIsNone(value)
|
self.assertIsNone(value)
|
||||||
|
|
||||||
|
with patch.object(mod, 'Session', return_value=self.session):
|
||||||
|
|
||||||
# model instance
|
# model instance
|
||||||
person = model.Person(full_name="Betty Boop")
|
person = model.Person(full_name="Betty Boop")
|
||||||
self.session.add(person)
|
self.session.add(person)
|
||||||
|
@ -189,31 +201,33 @@ class TestObjectRef(DataTestCase):
|
||||||
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
|
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
|
||||||
|
|
||||||
# can specify as uuid
|
# can specify as uuid
|
||||||
typ = mod.ObjectRef(self.request, session=self.session)
|
typ = mod.ObjectRef(self.request)
|
||||||
value = typ.objectify(person.uuid)
|
value = typ.objectify(person.uuid)
|
||||||
self.assertIs(value, person)
|
self.assertIs(value, person)
|
||||||
|
|
||||||
# or can specify object proper
|
# or can specify object proper
|
||||||
typ = mod.ObjectRef(self.request, session=self.session)
|
typ = mod.ObjectRef(self.request)
|
||||||
value = typ.objectify(person)
|
value = typ.objectify(person)
|
||||||
self.assertIs(value, person)
|
self.assertIs(value, person)
|
||||||
|
|
||||||
# error if not found
|
# error if not found
|
||||||
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
|
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
|
||||||
typ = mod.ObjectRef(self.request, session=self.session)
|
typ = mod.ObjectRef(self.request)
|
||||||
self.assertRaises(ValueError, typ.objectify, 'WRONG-UUID')
|
self.assertRaises(ValueError, typ.objectify, 'WRONG-UUID')
|
||||||
|
|
||||||
def test_get_query(self):
|
def test_get_query(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
|
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
|
||||||
typ = mod.ObjectRef(self.request, session=self.session)
|
with patch.object(mod, 'Session', return_value=self.session):
|
||||||
|
typ = mod.ObjectRef(self.request)
|
||||||
query = typ.get_query()
|
query = typ.get_query()
|
||||||
self.assertIsInstance(query, orm.Query)
|
self.assertIsInstance(query, orm.Query)
|
||||||
|
|
||||||
def test_sort_query(self):
|
def test_sort_query(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
|
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
|
||||||
typ = mod.ObjectRef(self.request, session=self.session)
|
with patch.object(mod, 'Session', return_value=self.session):
|
||||||
|
typ = mod.ObjectRef(self.request)
|
||||||
query = typ.get_query()
|
query = typ.get_query()
|
||||||
sorted_query = typ.sort_query(query)
|
sorted_query = typ.sort_query(query)
|
||||||
self.assertIs(sorted_query, query)
|
self.assertIs(sorted_query, query)
|
||||||
|
@ -226,14 +240,16 @@ class TestObjectRef(DataTestCase):
|
||||||
|
|
||||||
# basic
|
# basic
|
||||||
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
|
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
|
||||||
typ = mod.ObjectRef(self.request, session=self.session)
|
with patch.object(mod, 'Session', return_value=self.session):
|
||||||
|
typ = mod.ObjectRef(self.request)
|
||||||
widget = typ.widget_maker()
|
widget = typ.widget_maker()
|
||||||
self.assertEqual(len(widget.values), 1)
|
self.assertEqual(len(widget.values), 1)
|
||||||
self.assertEqual(widget.values[0][1], "Betty Boop")
|
self.assertEqual(widget.values[0][1], "Betty Boop")
|
||||||
|
|
||||||
# empty option
|
# empty option
|
||||||
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
|
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
|
||||||
typ = mod.ObjectRef(self.request, session=self.session, empty_option=True)
|
with patch.object(mod, 'Session', return_value=self.session):
|
||||||
|
typ = mod.ObjectRef(self.request, empty_option=True)
|
||||||
widget = typ.widget_maker()
|
widget = typ.widget_maker()
|
||||||
self.assertEqual(len(widget.values), 2)
|
self.assertEqual(len(widget.values), 2)
|
||||||
self.assertEqual(widget.values[0][1], "(none)")
|
self.assertEqual(widget.values[0][1], "(none)")
|
||||||
|
@ -243,7 +259,8 @@ class TestObjectRef(DataTestCase):
|
||||||
class TestPersonRef(WebTestCase):
|
class TestPersonRef(WebTestCase):
|
||||||
|
|
||||||
def test_sort_query(self):
|
def test_sort_query(self):
|
||||||
typ = mod.PersonRef(self.request, session=self.session)
|
with patch.object(mod, 'Session', return_value=self.session):
|
||||||
|
typ = mod.PersonRef(self.request)
|
||||||
query = typ.get_query()
|
query = typ.get_query()
|
||||||
self.assertIsInstance(query, orm.Query)
|
self.assertIsInstance(query, orm.Query)
|
||||||
sorted_query = typ.sort_query(query)
|
sorted_query = typ.sort_query(query)
|
||||||
|
@ -253,7 +270,8 @@ class TestPersonRef(WebTestCase):
|
||||||
def test_get_object_url(self):
|
def test_get_object_url(self):
|
||||||
self.pyramid_config.add_route('people.view', '/people/{uuid}')
|
self.pyramid_config.add_route('people.view', '/people/{uuid}')
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
typ = mod.PersonRef(self.request, session=self.session)
|
with patch.object(mod, 'Session', return_value=self.session):
|
||||||
|
typ = mod.PersonRef(self.request)
|
||||||
|
|
||||||
person = model.Person(full_name="Barney Rubble")
|
person = model.Person(full_name="Barney Rubble")
|
||||||
self.session.add(person)
|
self.session.add(person)
|
||||||
|
@ -267,7 +285,8 @@ class TestPersonRef(WebTestCase):
|
||||||
class TestRoleRef(WebTestCase):
|
class TestRoleRef(WebTestCase):
|
||||||
|
|
||||||
def test_sort_query(self):
|
def test_sort_query(self):
|
||||||
typ = mod.RoleRef(self.request, session=self.session)
|
with patch.object(mod, 'Session', return_value=self.session):
|
||||||
|
typ = mod.RoleRef(self.request)
|
||||||
query = typ.get_query()
|
query = typ.get_query()
|
||||||
self.assertIsInstance(query, orm.Query)
|
self.assertIsInstance(query, orm.Query)
|
||||||
sorted_query = typ.sort_query(query)
|
sorted_query = typ.sort_query(query)
|
||||||
|
@ -277,7 +296,8 @@ class TestRoleRef(WebTestCase):
|
||||||
def test_get_object_url(self):
|
def test_get_object_url(self):
|
||||||
self.pyramid_config.add_route('roles.view', '/roles/{uuid}')
|
self.pyramid_config.add_route('roles.view', '/roles/{uuid}')
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
typ = mod.RoleRef(self.request, session=self.session)
|
with patch.object(mod, 'Session', return_value=self.session):
|
||||||
|
typ = mod.RoleRef(self.request)
|
||||||
|
|
||||||
role = model.Role(name='Manager')
|
role = model.Role(name='Manager')
|
||||||
self.session.add(role)
|
self.session.add(role)
|
||||||
|
@ -291,7 +311,8 @@ class TestRoleRef(WebTestCase):
|
||||||
class TestUserRef(WebTestCase):
|
class TestUserRef(WebTestCase):
|
||||||
|
|
||||||
def test_sort_query(self):
|
def test_sort_query(self):
|
||||||
typ = mod.UserRef(self.request, session=self.session)
|
with patch.object(mod, 'Session', return_value=self.session):
|
||||||
|
typ = mod.UserRef(self.request)
|
||||||
query = typ.get_query()
|
query = typ.get_query()
|
||||||
self.assertIsInstance(query, orm.Query)
|
self.assertIsInstance(query, orm.Query)
|
||||||
sorted_query = typ.sort_query(query)
|
sorted_query = typ.sort_query(query)
|
||||||
|
@ -301,7 +322,8 @@ class TestUserRef(WebTestCase):
|
||||||
def test_get_object_url(self):
|
def test_get_object_url(self):
|
||||||
self.pyramid_config.add_route('users.view', '/users/{uuid}')
|
self.pyramid_config.add_route('users.view', '/users/{uuid}')
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
typ = mod.UserRef(self.request, session=self.session)
|
with patch.object(mod, 'Session', return_value=self.session):
|
||||||
|
typ = mod.UserRef(self.request)
|
||||||
|
|
||||||
user = model.User(username='barney')
|
user = model.User(username='barney')
|
||||||
self.session.add(user)
|
self.session.add(user)
|
||||||
|
@ -320,7 +342,8 @@ class TestUserRefs(DataTestCase):
|
||||||
|
|
||||||
def test_widget_maker(self):
|
def test_widget_maker(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
typ = mod.UserRefs(self.request, session=self.session)
|
with patch.object(mod, 'Session', return_value=self.session):
|
||||||
|
typ = mod.UserRefs(self.request)
|
||||||
widget = typ.widget_maker()
|
widget = typ.widget_maker()
|
||||||
self.assertIsInstance(widget, widgets.UserRefsWidget)
|
self.assertIsInstance(widget, widgets.UserRefsWidget)
|
||||||
|
|
||||||
|
@ -341,9 +364,11 @@ class TestRoleRefs(DataTestCase):
|
||||||
self.session.add(blokes)
|
self.session.add(blokes)
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
|
|
||||||
|
with patch.object(mod, 'Session', return_value=self.session):
|
||||||
|
|
||||||
# with root access, default values include: admin, blokes
|
# with root access, default values include: admin, blokes
|
||||||
self.request.is_root = True
|
self.request.is_root = True
|
||||||
typ = mod.RoleRefs(self.request, session=self.session)
|
typ = mod.RoleRefs(self.request)
|
||||||
widget = typ.widget_maker()
|
widget = typ.widget_maker()
|
||||||
self.assertEqual(len(widget.values), 2)
|
self.assertEqual(len(widget.values), 2)
|
||||||
self.assertEqual(widget.values[0][1], "Administrator")
|
self.assertEqual(widget.values[0][1], "Administrator")
|
||||||
|
@ -351,7 +376,7 @@ class TestRoleRefs(DataTestCase):
|
||||||
|
|
||||||
# without root, default values include: blokes
|
# without root, default values include: blokes
|
||||||
self.request.is_root = False
|
self.request.is_root = False
|
||||||
typ = mod.RoleRefs(self.request, session=self.session)
|
typ = mod.RoleRefs(self.request)
|
||||||
widget = typ.widget_maker()
|
widget = typ.widget_maker()
|
||||||
self.assertEqual(len(widget.values), 1)
|
self.assertEqual(len(widget.values), 1)
|
||||||
self.assertEqual(widget.values[0][1], "Blokes")
|
self.assertEqual(widget.values[0][1], "Blokes")
|
||||||
|
|
|
@ -10,9 +10,10 @@ from pyramid import testing
|
||||||
|
|
||||||
from wuttaweb import grids
|
from wuttaweb import grids
|
||||||
from wuttaweb.forms import widgets as mod
|
from wuttaweb.forms import widgets as mod
|
||||||
|
from wuttaweb.forms import schema
|
||||||
from wuttaweb.forms.schema import (FileDownload, PersonRef, RoleRefs, UserRefs, Permissions,
|
from wuttaweb.forms.schema import (FileDownload, PersonRef, RoleRefs, UserRefs, Permissions,
|
||||||
WuttaDateTime, EmailRecipients)
|
WuttaDateTime, EmailRecipients)
|
||||||
from tests.util import WebTestCase
|
from wuttaweb.testing import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestObjectRefWidget(WebTestCase):
|
class TestObjectRefWidget(WebTestCase):
|
||||||
|
@ -32,15 +33,17 @@ class TestObjectRefWidget(WebTestCase):
|
||||||
self.session.add(person)
|
self.session.add(person)
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
|
|
||||||
|
with patch.object(schema, 'Session', return_value=self.session):
|
||||||
|
|
||||||
# standard (editable)
|
# standard (editable)
|
||||||
node = colander.SchemaNode(PersonRef(self.request, session=self.session))
|
node = colander.SchemaNode(PersonRef(self.request))
|
||||||
widget = self.make_widget()
|
widget = self.make_widget()
|
||||||
field = self.make_field(node)
|
field = self.make_field(node)
|
||||||
html = widget.serialize(field, person.uuid)
|
html = widget.serialize(field, person.uuid)
|
||||||
self.assertIn('<b-select ', html)
|
self.assertIn('<b-select ', html)
|
||||||
|
|
||||||
# readonly
|
# readonly
|
||||||
node = colander.SchemaNode(PersonRef(self.request, session=self.session))
|
node = colander.SchemaNode(PersonRef(self.request))
|
||||||
node.model_instance = person
|
node.model_instance = person
|
||||||
widget = self.make_widget()
|
widget = self.make_widget()
|
||||||
field = self.make_field(node)
|
field = self.make_field(node)
|
||||||
|
@ -49,7 +52,7 @@ class TestObjectRefWidget(WebTestCase):
|
||||||
self.assertNotIn('<a', html)
|
self.assertNotIn('<a', html)
|
||||||
|
|
||||||
# with hyperlink
|
# with hyperlink
|
||||||
node = colander.SchemaNode(PersonRef(self.request, session=self.session))
|
node = colander.SchemaNode(PersonRef(self.request))
|
||||||
node.model_instance = person
|
node.model_instance = person
|
||||||
widget = self.make_widget(url=lambda p: '/foo')
|
widget = self.make_widget(url=lambda p: '/foo')
|
||||||
field = self.make_field(node)
|
field = self.make_field(node)
|
||||||
|
@ -64,8 +67,10 @@ class TestObjectRefWidget(WebTestCase):
|
||||||
self.session.add(person)
|
self.session.add(person)
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
|
|
||||||
|
with patch.object(schema, 'Session', return_value=self.session):
|
||||||
|
|
||||||
# standard
|
# standard
|
||||||
node = colander.SchemaNode(PersonRef(self.request, session=self.session))
|
node = colander.SchemaNode(PersonRef(self.request))
|
||||||
widget = self.make_widget()
|
widget = self.make_widget()
|
||||||
field = self.make_field(node)
|
field = self.make_field(node)
|
||||||
values = widget.get_template_values(field, person.uuid, {})
|
values = widget.get_template_values(field, person.uuid, {})
|
||||||
|
@ -73,7 +78,7 @@ class TestObjectRefWidget(WebTestCase):
|
||||||
self.assertNotIn('url', values)
|
self.assertNotIn('url', values)
|
||||||
|
|
||||||
# readonly w/ empty option
|
# readonly w/ empty option
|
||||||
node = colander.SchemaNode(PersonRef(self.request, session=self.session,
|
node = colander.SchemaNode(PersonRef(self.request,
|
||||||
empty_option=('_empty_', '(empty)')))
|
empty_option=('_empty_', '(empty)')))
|
||||||
widget = self.make_widget(readonly=True, url=lambda obj: '/foo')
|
widget = self.make_widget(readonly=True, url=lambda obj: '/foo')
|
||||||
field = self.make_field(node)
|
field = self.make_field(node)
|
||||||
|
@ -120,7 +125,7 @@ class TestWuttaMoneyInputWidget(WebTestCase):
|
||||||
return mod.WuttaMoneyInputWidget(self.request, **kwargs)
|
return mod.WuttaMoneyInputWidget(self.request, **kwargs)
|
||||||
|
|
||||||
def test_serialize(self):
|
def test_serialize(self):
|
||||||
node = colander.SchemaNode(WuttaDateTime())
|
node = colander.SchemaNode(schema.WuttaMoney(self.request))
|
||||||
field = self.make_field(node)
|
field = self.make_field(node)
|
||||||
widget = self.make_widget()
|
widget = self.make_widget()
|
||||||
amount = decimal.Decimal('12.34')
|
amount = decimal.Decimal('12.34')
|
||||||
|
@ -138,6 +143,36 @@ class TestWuttaMoneyInputWidget(WebTestCase):
|
||||||
self.assertEqual(result, '<span></span>')
|
self.assertEqual(result, '<span></span>')
|
||||||
|
|
||||||
|
|
||||||
|
class TestWuttaQuantityWidget(WebTestCase):
|
||||||
|
|
||||||
|
def make_field(self, node, **kwargs):
|
||||||
|
# TODO: not sure why default renderer is in use even though
|
||||||
|
# pyramid_deform was included in setup? but this works..
|
||||||
|
kwargs.setdefault('renderer', deform.Form.default_renderer)
|
||||||
|
return deform.Field(node, **kwargs)
|
||||||
|
|
||||||
|
def make_widget(self, **kwargs):
|
||||||
|
return mod.WuttaQuantityWidget(self.request, **kwargs)
|
||||||
|
|
||||||
|
def test_serialize(self):
|
||||||
|
node = colander.SchemaNode(schema.WuttaQuantity(self.request))
|
||||||
|
field = self.make_field(node)
|
||||||
|
widget = self.make_widget()
|
||||||
|
amount = decimal.Decimal('42.00')
|
||||||
|
|
||||||
|
# editable widget has normal text input
|
||||||
|
result = widget.serialize(field, str(amount))
|
||||||
|
self.assertIn('<b-input', result)
|
||||||
|
|
||||||
|
# readonly is rendered per app convention
|
||||||
|
result = widget.serialize(field, str(amount), readonly=True)
|
||||||
|
self.assertEqual(result, '<span>42</span>')
|
||||||
|
|
||||||
|
# readonly w/ null value
|
||||||
|
result = widget.serialize(field, None, readonly=True)
|
||||||
|
self.assertEqual(result, '<span></span>')
|
||||||
|
|
||||||
|
|
||||||
class TestFileDownloadWidget(WebTestCase):
|
class TestFileDownloadWidget(WebTestCase):
|
||||||
|
|
||||||
def make_field(self, node, **kwargs):
|
def make_field(self, node, **kwargs):
|
||||||
|
@ -230,7 +265,8 @@ class TestRoleRefsWidget(WebTestCase):
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
|
|
||||||
# nb. we let the field construct the widget via our type
|
# nb. we let the field construct the widget via our type
|
||||||
node = colander.SchemaNode(RoleRefs(self.request, session=self.session))
|
with patch.object(schema, 'Session', return_value=self.session):
|
||||||
|
node = colander.SchemaNode(RoleRefs(self.request))
|
||||||
field = self.make_field(node)
|
field = self.make_field(node)
|
||||||
widget = field.widget
|
widget = field.widget
|
||||||
|
|
||||||
|
@ -246,7 +282,7 @@ class TestRoleRefsWidget(WebTestCase):
|
||||||
|
|
||||||
# but admin is included for root user
|
# but admin is included for root user
|
||||||
self.request.is_root = True
|
self.request.is_root = True
|
||||||
node = colander.SchemaNode(RoleRefs(self.request, session=self.session))
|
node = colander.SchemaNode(RoleRefs(self.request))
|
||||||
field = self.make_field(node)
|
field = self.make_field(node)
|
||||||
widget = field.widget
|
widget = field.widget
|
||||||
html = widget.serialize(field, {admin.uuid, blokes.uuid})
|
html = widget.serialize(field, {admin.uuid, blokes.uuid})
|
||||||
|
@ -266,7 +302,9 @@ class TestUserRefsWidget(WebTestCase):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
||||||
# nb. we let the field construct the widget via our type
|
# nb. we let the field construct the widget via our type
|
||||||
node = colander.SchemaNode(UserRefs(self.request, session=self.session))
|
# node = colander.SchemaNode(UserRefs(self.request, session=self.session))
|
||||||
|
with patch.object(schema, 'Session', return_value=self.session):
|
||||||
|
node = colander.SchemaNode(UserRefs(self.request))
|
||||||
field = self.make_field(node)
|
field = self.make_field(node)
|
||||||
widget = field.widget
|
widget = field.widget
|
||||||
|
|
||||||
|
@ -318,7 +356,7 @@ class TestPermissionsWidget(WebTestCase):
|
||||||
}
|
}
|
||||||
|
|
||||||
# nb. we let the field construct the widget via our type
|
# nb. we let the field construct the widget via our type
|
||||||
node = colander.SchemaNode(Permissions(self.request, permissions, session=self.session))
|
node = colander.SchemaNode(Permissions(self.request, permissions))
|
||||||
field = self.make_field(node)
|
field = self.make_field(node)
|
||||||
widget = field.widget
|
widget = field.widget
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# -*- coding: utf-8; -*-
|
# -*- coding: utf-8; -*-
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import decimal
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
@ -15,7 +16,7 @@ from wuttaweb.grids import base as mod
|
||||||
from wuttaweb.grids.filters import GridFilter, StringAlchemyFilter, default_sqlalchemy_filters
|
from wuttaweb.grids.filters import GridFilter, StringAlchemyFilter, default_sqlalchemy_filters
|
||||||
from wuttaweb.util import FieldList
|
from wuttaweb.util import FieldList
|
||||||
from wuttaweb.forms import Form
|
from wuttaweb.forms import Form
|
||||||
from tests.util import WebTestCase
|
from wuttaweb.testing import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestGrid(WebTestCase):
|
class TestGrid(WebTestCase):
|
||||||
|
@ -207,6 +208,11 @@ class TestGrid(WebTestCase):
|
||||||
self.assertIsNot(grid.renderers['foo'], render2)
|
self.assertIsNot(grid.renderers['foo'], render2)
|
||||||
self.assertEqual(grid.renderers['foo'](None, None, None), 42)
|
self.assertEqual(grid.renderers['foo'](None, None, None), 42)
|
||||||
|
|
||||||
|
# can use built-in string shortcut
|
||||||
|
grid.set_renderer('foo', 'quantity')
|
||||||
|
obj = MagicMock(foo=42.00)
|
||||||
|
self.assertEqual(grid.renderers['foo'](obj, 'foo', 42.00), '42')
|
||||||
|
|
||||||
def test_set_default_renderer(self):
|
def test_set_default_renderer(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
||||||
|
@ -232,6 +238,17 @@ class TestGrid(WebTestCase):
|
||||||
self.assertIn('created', grid.renderers)
|
self.assertIn('created', grid.renderers)
|
||||||
self.assertIs(grid.renderers['created'], myrender)
|
self.assertIs(grid.renderers['created'], myrender)
|
||||||
|
|
||||||
|
# renderer set for boolean mapped field
|
||||||
|
grid = self.make_grid(model_class=model.Upgrade)
|
||||||
|
self.assertIn('executing', grid.renderers)
|
||||||
|
self.assertIsNot(grid.renderers['executing'], myrender)
|
||||||
|
|
||||||
|
# renderer *not* set for boolean, if override present
|
||||||
|
grid = self.make_grid(model_class=model.Upgrade,
|
||||||
|
renderers={'executing': myrender})
|
||||||
|
self.assertIn('executing', grid.renderers)
|
||||||
|
self.assertIs(grid.renderers['executing'], myrender)
|
||||||
|
|
||||||
def test_linked_columns(self):
|
def test_linked_columns(self):
|
||||||
grid = self.make_grid(columns=['foo', 'bar'])
|
grid = self.make_grid(columns=['foo', 'bar'])
|
||||||
self.assertEqual(grid.linked_columns, [])
|
self.assertEqual(grid.linked_columns, [])
|
||||||
|
@ -1331,6 +1348,73 @@ class TestGrid(WebTestCase):
|
||||||
# rendering methods
|
# rendering methods
|
||||||
##############################
|
##############################
|
||||||
|
|
||||||
|
def test_render_batch_id(self):
|
||||||
|
grid = self.make_grid(columns=['foo', 'bar'])
|
||||||
|
|
||||||
|
# null
|
||||||
|
obj = MagicMock(foo=None)
|
||||||
|
self.assertEqual(grid.render_batch_id(obj, 'foo', None), "")
|
||||||
|
|
||||||
|
# int
|
||||||
|
obj = MagicMock(foo=42)
|
||||||
|
self.assertEqual(grid.render_batch_id(obj, 'foo', 42), "00000042")
|
||||||
|
|
||||||
|
def test_render_boolean(self):
|
||||||
|
grid = self.make_grid(columns=['foo', 'bar'])
|
||||||
|
|
||||||
|
# null
|
||||||
|
obj = MagicMock(foo=None)
|
||||||
|
self.assertEqual(grid.render_boolean(obj, 'foo', None), "")
|
||||||
|
|
||||||
|
# true
|
||||||
|
obj = MagicMock(foo=True)
|
||||||
|
self.assertEqual(grid.render_boolean(obj, 'foo', True), "Yes")
|
||||||
|
|
||||||
|
# false
|
||||||
|
obj = MagicMock(foo=False)
|
||||||
|
self.assertEqual(grid.render_boolean(obj, 'foo', False), "No")
|
||||||
|
|
||||||
|
def test_render_currency(self):
|
||||||
|
grid = self.make_grid(columns=['foo', 'bar'])
|
||||||
|
obj = MagicMock()
|
||||||
|
|
||||||
|
# null
|
||||||
|
self.assertEqual(grid.render_currency(obj, 'foo', None), '')
|
||||||
|
|
||||||
|
# basic decimal example
|
||||||
|
value = decimal.Decimal('42.00')
|
||||||
|
self.assertEqual(grid.render_currency(obj, 'foo', value), '$42.00')
|
||||||
|
|
||||||
|
# basic float example
|
||||||
|
value = 42.00
|
||||||
|
self.assertEqual(grid.render_currency(obj, 'foo', value), '$42.00')
|
||||||
|
|
||||||
|
# decimal places will be rounded
|
||||||
|
value = decimal.Decimal('42.12345')
|
||||||
|
self.assertEqual(grid.render_currency(obj, 'foo', value), '$42.12')
|
||||||
|
|
||||||
|
# negative numbers get parens
|
||||||
|
value = decimal.Decimal('-42.42')
|
||||||
|
self.assertEqual(grid.render_currency(obj, 'foo', value), '($42.42)')
|
||||||
|
|
||||||
|
def test_render_quantity(self):
|
||||||
|
grid = self.make_grid(columns=['foo', 'bar'])
|
||||||
|
obj = MagicMock()
|
||||||
|
|
||||||
|
# null
|
||||||
|
self.assertEqual(grid.render_quantity(obj, 'foo', None), "")
|
||||||
|
|
||||||
|
# integer decimals become integers
|
||||||
|
value = decimal.Decimal('1.000')
|
||||||
|
self.assertEqual(grid.render_quantity(obj, 'foo', value), "1")
|
||||||
|
|
||||||
|
# but decimal places are preserved
|
||||||
|
value = decimal.Decimal('1.234')
|
||||||
|
self.assertEqual(grid.render_quantity(obj ,'foo', value), "1.234")
|
||||||
|
|
||||||
|
# zero is *not* empty string (with this renderer)
|
||||||
|
self.assertEqual(grid.render_quantity(obj, 'foo', 0), "0")
|
||||||
|
|
||||||
def test_render_datetime(self):
|
def test_render_datetime(self):
|
||||||
grid = self.make_grid(columns=['foo', 'bar'])
|
grid = self.make_grid(columns=['foo', 'bar'])
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import sqlalchemy as sa
|
||||||
from wuttjamaican.db.model import Base
|
from wuttjamaican.db.model import Base
|
||||||
|
|
||||||
from wuttaweb.grids import filters as mod
|
from wuttaweb.grids import filters as mod
|
||||||
from tests.util import WebTestCase
|
from wuttaweb.testing import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestGridFilter(WebTestCase):
|
class TestGridFilter(WebTestCase):
|
||||||
|
|
|
@ -8,7 +8,7 @@ from pyramid import testing
|
||||||
|
|
||||||
from wuttjamaican.conf import WuttaConfig
|
from wuttjamaican.conf import WuttaConfig
|
||||||
from wuttaweb import auth as mod
|
from wuttaweb import auth as mod
|
||||||
from tests.util import WebTestCase
|
from wuttaweb.testing import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestLoginUser(TestCase):
|
class TestLoginUser(TestCase):
|
||||||
|
|
|
@ -4,7 +4,7 @@ from wuttaweb import handler as mod, static
|
||||||
from wuttaweb.forms import Form
|
from wuttaweb.forms import Form
|
||||||
from wuttaweb.grids import Grid
|
from wuttaweb.grids import Grid
|
||||||
from wuttaweb.menus import MenuHandler
|
from wuttaweb.menus import MenuHandler
|
||||||
from tests.util import WebTestCase
|
from wuttaweb.testing import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestWebHandler(WebTestCase):
|
class TestWebHandler(WebTestCase):
|
||||||
|
|
|
@ -4,7 +4,7 @@ from unittest import TestCase
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
from wuttaweb import menus as mod
|
from wuttaweb import menus as mod
|
||||||
from tests.util import WebTestCase
|
from wuttaweb.testing import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestMenuHandler(WebTestCase):
|
class TestMenuHandler(WebTestCase):
|
||||||
|
|
|
@ -1,14 +1,7 @@
|
||||||
# -*- coding: utf-8; -*-
|
# -*- coding: utf-8; -*-
|
||||||
|
|
||||||
from unittest import TestCase
|
|
||||||
from unittest.mock import MagicMock
|
|
||||||
|
|
||||||
import fanstatic
|
|
||||||
from pyramid import testing
|
|
||||||
|
|
||||||
from wuttjamaican.conf import WuttaConfig
|
from wuttjamaican.conf import WuttaConfig
|
||||||
from wuttjamaican.testing import FileConfigTestCase
|
from wuttjamaican.testing import FileConfigTestCase
|
||||||
from wuttaweb import subscribers
|
|
||||||
from wuttaweb.menus import MenuHandler
|
from wuttaweb.menus import MenuHandler
|
||||||
|
|
||||||
|
|
||||||
|
@ -39,56 +32,6 @@ class DataTestCase(FileConfigTestCase):
|
||||||
self.teardown_files()
|
self.teardown_files()
|
||||||
|
|
||||||
|
|
||||||
class WebTestCase(DataTestCase):
|
|
||||||
"""
|
|
||||||
Base class for test suites requiring a full (typical) web app.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.setup_web()
|
|
||||||
|
|
||||||
def setup_web(self):
|
|
||||||
self.setup_db()
|
|
||||||
self.request = self.make_request()
|
|
||||||
self.pyramid_config = testing.setUp(request=self.request, settings={
|
|
||||||
'wutta_config': self.config,
|
|
||||||
'mako.directories': ['wuttaweb:templates'],
|
|
||||||
'pyramid_deform.template_search_path': 'wuttaweb:templates/deform',
|
|
||||||
})
|
|
||||||
|
|
||||||
# init web
|
|
||||||
self.pyramid_config.include('pyramid_deform')
|
|
||||||
self.pyramid_config.include('pyramid_mako')
|
|
||||||
self.pyramid_config.add_directive('add_wutta_permission_group',
|
|
||||||
'wuttaweb.auth.add_permission_group')
|
|
||||||
self.pyramid_config.add_directive('add_wutta_permission',
|
|
||||||
'wuttaweb.auth.add_permission')
|
|
||||||
self.pyramid_config.add_subscriber('wuttaweb.subscribers.before_render',
|
|
||||||
'pyramid.events.BeforeRender')
|
|
||||||
self.pyramid_config.include('wuttaweb.static')
|
|
||||||
|
|
||||||
# nb. mock out fanstatic env..good enough for now to avoid errors..
|
|
||||||
needed = fanstatic.init_needed()
|
|
||||||
self.request.environ[fanstatic.NEEDED] = needed
|
|
||||||
|
|
||||||
# setup new request w/ anonymous user
|
|
||||||
event = MagicMock(request=self.request)
|
|
||||||
subscribers.new_request(event)
|
|
||||||
def user_getter(request, **kwargs): pass
|
|
||||||
subscribers.new_request_set_user(event, db_session=self.session,
|
|
||||||
user_getter=user_getter)
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
self.teardown_web()
|
|
||||||
|
|
||||||
def teardown_web(self):
|
|
||||||
testing.tearDown()
|
|
||||||
self.teardown_db()
|
|
||||||
|
|
||||||
def make_request(self):
|
|
||||||
return testing.DummyRequest()
|
|
||||||
|
|
||||||
|
|
||||||
class NullMenuHandler(MenuHandler):
|
class NullMenuHandler(MenuHandler):
|
||||||
"""
|
"""
|
||||||
Dummy menu handler for testing.
|
Dummy menu handler for testing.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# -*- coding: utf-8; -*-
|
# -*- coding: utf-8; -*-
|
||||||
|
|
||||||
from tests.util import WebTestCase
|
from wuttaweb.testing import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestIncludeMe(WebTestCase):
|
class TestIncludeMe(WebTestCase):
|
||||||
|
|
|
@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch
|
||||||
from pyramid.httpexceptions import HTTPFound, HTTPForbidden
|
from pyramid.httpexceptions import HTTPFound, HTTPForbidden
|
||||||
|
|
||||||
from wuttaweb.views import auth as mod
|
from wuttaweb.views import auth as mod
|
||||||
from tests.util import WebTestCase
|
from wuttaweb.testing import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestAuthView(WebTestCase):
|
class TestAuthView(WebTestCase):
|
||||||
|
|
|
@ -5,7 +5,7 @@ from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPNotFound
|
||||||
from wuttaweb.views import base as mod
|
from wuttaweb.views import base as mod
|
||||||
from wuttaweb.forms import Form
|
from wuttaweb.forms import Form
|
||||||
from wuttaweb.grids import Grid, GridAction
|
from wuttaweb.grids import Grid, GridAction
|
||||||
from tests.util import WebTestCase
|
from wuttaweb.testing import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestView(WebTestCase):
|
class TestView(WebTestCase):
|
||||||
|
|
|
@ -10,7 +10,7 @@ from wuttjamaican.db import model
|
||||||
from wuttjamaican.batch import BatchHandler
|
from wuttjamaican.batch import BatchHandler
|
||||||
from wuttaweb.views import MasterView, batch as mod
|
from wuttaweb.views import MasterView, batch as mod
|
||||||
from wuttaweb.progress import SessionProgress
|
from wuttaweb.progress import SessionProgress
|
||||||
from tests.util import WebTestCase
|
from wuttaweb.testing import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class MockBatch(model.BatchMixin, model.Base):
|
class MockBatch(model.BatchMixin, model.Base):
|
||||||
|
@ -367,6 +367,12 @@ class TestBatchMasterView(WebTestCase):
|
||||||
self.assertIn('sequence', grid.labels)
|
self.assertIn('sequence', grid.labels)
|
||||||
self.assertEqual(grid.labels['sequence'], "Seq.")
|
self.assertEqual(grid.labels['sequence'], "Seq.")
|
||||||
|
|
||||||
|
def test_render_row_status(self):
|
||||||
|
with patch.object(mod.BatchMasterView, 'get_batch_handler', return_value=None):
|
||||||
|
view = self.make_view()
|
||||||
|
row = MagicMock(foo=1, STATUS={1: 'bar'})
|
||||||
|
self.assertEqual(view.render_row_status(row, 'foo', 1), 'bar')
|
||||||
|
|
||||||
def test_defaults(self):
|
def test_defaults(self):
|
||||||
# nb. coverage only
|
# nb. coverage only
|
||||||
with patch.object(mod.BatchMasterView, 'model_class', new=MockBatch, create=True):
|
with patch.object(mod.BatchMasterView, 'model_class', new=MockBatch, create=True):
|
||||||
|
|
|
@ -5,7 +5,7 @@ from unittest.mock import patch
|
||||||
import colander
|
import colander
|
||||||
|
|
||||||
from wuttaweb.views import common as mod
|
from wuttaweb.views import common as mod
|
||||||
from tests.util import WebTestCase
|
from wuttaweb.testing import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestCommonView(WebTestCase):
|
class TestCommonView(WebTestCase):
|
||||||
|
|
|
@ -9,7 +9,7 @@ from pyramid.httpexceptions import HTTPNotFound
|
||||||
from pyramid.response import Response
|
from pyramid.response import Response
|
||||||
|
|
||||||
from wuttaweb.views import email as mod
|
from wuttaweb.views import email as mod
|
||||||
from tests.util import WebTestCase
|
from wuttaweb.testing import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestEmailSettingViews(WebTestCase):
|
class TestEmailSettingViews(WebTestCase):
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# -*- coding: utf-8; -*-
|
# -*- coding: utf-8; -*-
|
||||||
|
|
||||||
from wuttaweb.views import essential as mod
|
from wuttaweb.views import essential as mod
|
||||||
from tests.util import WebTestCase
|
from wuttaweb.testing import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestEssentialViews(WebTestCase):
|
class TestEssentialViews(WebTestCase):
|
||||||
|
|
|
@ -16,7 +16,7 @@ from wuttaweb.views import master as mod
|
||||||
from wuttaweb.views import View
|
from wuttaweb.views import View
|
||||||
from wuttaweb.progress import SessionProgress
|
from wuttaweb.progress import SessionProgress
|
||||||
from wuttaweb.subscribers import new_request_set_user
|
from wuttaweb.subscribers import new_request_set_user
|
||||||
from tests.util import WebTestCase
|
from wuttaweb.testing import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestMasterView(WebTestCase):
|
class TestMasterView(WebTestCase):
|
||||||
|
@ -441,6 +441,12 @@ class TestMasterView(WebTestCase):
|
||||||
self.assertIn('click me', html)
|
self.assertIn('click me', html)
|
||||||
self.assertIn('is-primary', html)
|
self.assertIn('is-primary', html)
|
||||||
|
|
||||||
|
# with url
|
||||||
|
html = view.make_button('click me', url='http://example.com')
|
||||||
|
self.assertIn('<b-button tag="a"', html)
|
||||||
|
self.assertIn('click me', html)
|
||||||
|
self.assertIn('href="http://example.com"', html)
|
||||||
|
|
||||||
def test_make_progress(self):
|
def test_make_progress(self):
|
||||||
|
|
||||||
# basic
|
# basic
|
||||||
|
@ -1649,6 +1655,18 @@ class TestMasterView(WebTestCase):
|
||||||
self.assertIsNone(grid.model_class)
|
self.assertIsNone(grid.model_class)
|
||||||
self.assertEqual(grid.data, [])
|
self.assertEqual(grid.data, [])
|
||||||
|
|
||||||
|
# view action
|
||||||
|
with patch.object(view, 'rows_viewable', new=True):
|
||||||
|
with patch.object(view, 'get_row_action_url_view', return_value='#'):
|
||||||
|
grid = view.make_row_model_grid(person, data=[])
|
||||||
|
self.assertEqual(len(grid.actions), 1)
|
||||||
|
self.assertEqual(grid.actions[0].key, 'view')
|
||||||
|
|
||||||
|
def test_get_row_action_url_view(self):
|
||||||
|
view = self.make_view()
|
||||||
|
row = MagicMock()
|
||||||
|
self.assertRaises(NotImplementedError, view.get_row_action_url_view, row, 0)
|
||||||
|
|
||||||
def test_get_rows_title(self):
|
def test_get_rows_title(self):
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ from sqlalchemy import orm
|
||||||
from pyramid.httpexceptions import HTTPNotFound
|
from pyramid.httpexceptions import HTTPNotFound
|
||||||
|
|
||||||
from wuttaweb.views import people
|
from wuttaweb.views import people
|
||||||
from tests.util import WebTestCase
|
from wuttaweb.testing import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestPersonView(WebTestCase):
|
class TestPersonView(WebTestCase):
|
||||||
|
|
|
@ -4,7 +4,7 @@ from pyramid import testing
|
||||||
|
|
||||||
from wuttaweb.views import progress as mod
|
from wuttaweb.views import progress as mod
|
||||||
from wuttaweb.progress import get_progress_session
|
from wuttaweb.progress import get_progress_session
|
||||||
from tests.util import WebTestCase
|
from wuttaweb.testing import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestProgressView(WebTestCase):
|
class TestProgressView(WebTestCase):
|
||||||
|
|
|
@ -8,7 +8,7 @@ import colander
|
||||||
|
|
||||||
from wuttaweb.views import roles as mod
|
from wuttaweb.views import roles as mod
|
||||||
from wuttaweb.forms.schema import RoleRef
|
from wuttaweb.forms.schema import RoleRef
|
||||||
from tests.util import WebTestCase
|
from wuttaweb.testing import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestRoleView(WebTestCase):
|
class TestRoleView(WebTestCase):
|
||||||
|
|
|
@ -6,7 +6,7 @@ import colander
|
||||||
from pyramid.httpexceptions import HTTPNotFound
|
from pyramid.httpexceptions import HTTPNotFound
|
||||||
|
|
||||||
from wuttaweb.views import settings as mod
|
from wuttaweb.views import settings as mod
|
||||||
from tests.util import WebTestCase
|
from wuttaweb.testing import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestAppInfoView(WebTestCase):
|
class TestAppInfoView(WebTestCase):
|
||||||
|
|
|
@ -8,7 +8,7 @@ from unittest.mock import patch, MagicMock
|
||||||
from wuttaweb.views import upgrades as mod
|
from wuttaweb.views import upgrades as mod
|
||||||
from wuttjamaican.exc import ConfigurationError
|
from wuttjamaican.exc import ConfigurationError
|
||||||
from wuttaweb.progress import get_progress_session
|
from wuttaweb.progress import get_progress_session
|
||||||
from tests.util import WebTestCase
|
from wuttaweb.testing import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestUpgradeView(WebTestCase):
|
class TestUpgradeView(WebTestCase):
|
||||||
|
|
|
@ -7,7 +7,7 @@ from sqlalchemy import orm
|
||||||
import colander
|
import colander
|
||||||
|
|
||||||
from wuttaweb.views import users as mod
|
from wuttaweb.views import users as mod
|
||||||
from tests.util import WebTestCase
|
from wuttaweb.testing import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestUserView(WebTestCase):
|
class TestUserView(WebTestCase):
|
||||||
|
|
Loading…
Reference in a new issue