Compare commits
No commits in common. "49b13306c466061129e0d0a430d7d91d894160d7" and "6515a0a224036d332f75fbd13388c586cd528360" have entirely different histories.
49b13306c4
...
6515a0a224
20
CHANGELOG.md
20
CHANGELOG.md
|
@ -5,26 +5,6 @@ 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.1"
|
version = "0.19.0"
|
||||||
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.2",
|
"WuttJamaican[db]>=0.19.1",
|
||||||
"zope.sqlalchemy>=1.5",
|
"zope.sqlalchemy>=1.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -177,41 +177,25 @@ 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`.
|
This is a subclass of :class:`colander.Set`, but adds
|
||||||
|
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):
|
def __init__(self, request, session=None):
|
||||||
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):
|
||||||
|
@ -247,16 +231,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:
|
||||||
|
@ -488,7 +472,7 @@ class RoleRefs(WuttaSet):
|
||||||
:returns: Instance of
|
:returns: Instance of
|
||||||
:class:`~wuttaweb.forms.widgets.RoleRefsWidget`.
|
:class:`~wuttaweb.forms.widgets.RoleRefsWidget`.
|
||||||
"""
|
"""
|
||||||
session = kwargs.setdefault('session', Session())
|
kwargs.setdefault('session', self.session)
|
||||||
|
|
||||||
if 'values' not in kwargs:
|
if 'values' not in kwargs:
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
@ -496,17 +480,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(session),
|
auth.get_role_authenticated(self.session),
|
||||||
auth.get_role_anonymous(session),
|
auth.get_role_anonymous(self.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(session).uuid)
|
avoid.add(auth.get_role_administrator(self.session).uuid)
|
||||||
|
|
||||||
# everything else can be (un)assigned for users
|
# everything else can be (un)assigned for users
|
||||||
roles = session.query(model.Role)\
|
roles = self.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()
|
||||||
|
@ -534,7 +518,7 @@ class UserRefs(WuttaSet):
|
||||||
:returns: Instance of
|
:returns: Instance of
|
||||||
:class:`~wuttaweb.forms.widgets.UserRefsWidget`.
|
:class:`~wuttaweb.forms.widgets.UserRefsWidget`.
|
||||||
"""
|
"""
|
||||||
kwargs.setdefault('session', Session())
|
kwargs.setdefault('session', self.session)
|
||||||
return widgets.UserRefsWidget(self.request, **kwargs)
|
return widgets.UserRefsWidget(self.request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -564,7 +548,7 @@ class Permissions(WuttaSet):
|
||||||
:returns: Instance of
|
:returns: Instance of
|
||||||
:class:`~wuttaweb.forms.widgets.PermissionsWidget`.
|
:class:`~wuttaweb.forms.widgets.PermissionsWidget`.
|
||||||
"""
|
"""
|
||||||
kwargs.setdefault('session', Session())
|
kwargs.setdefault('session', self.session)
|
||||||
kwargs.setdefault('permissions', self.permissions)
|
kwargs.setdefault('permissions', self.permissions)
|
||||||
|
|
||||||
if 'values' not in kwargs:
|
if 'values' not in kwargs:
|
||||||
|
|
|
@ -136,21 +136,26 @@ 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`.
|
:class:`deform:deform.widget.CheckboxChoiceWidget`, but adds
|
||||||
|
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, *args, **kwargs):
|
def __init__(self, request, session=None, *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):
|
||||||
|
@ -226,42 +231,6 @@ 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,8 +116,7 @@ class Grid:
|
||||||
|
|
||||||
Dict of column (cell) value renderer overrides.
|
Dict of column (cell) value renderer overrides.
|
||||||
|
|
||||||
See also :meth:`set_renderer()` and
|
See also :meth:`set_renderer()`.
|
||||||
:meth:`set_default_renderers()`.
|
|
||||||
|
|
||||||
.. attribute:: row_class
|
.. attribute:: row_class
|
||||||
|
|
||||||
|
@ -389,6 +388,7 @@ 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,10 +398,6 @@ 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)
|
||||||
|
|
||||||
|
@ -596,29 +592,8 @@ 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
|
||||||
|
@ -627,18 +602,15 @@ class Grid:
|
||||||
"""
|
"""
|
||||||
Set default column value renderers, where applicable.
|
Set default column value renderers, where applicable.
|
||||||
|
|
||||||
This is called automatically from the class constructor. It
|
This will add new entries to :attr:`renderers` for columns
|
||||||
will add new entries to :attr:`renderers` for columns whose
|
whose data type implies a default renderer should be used.
|
||||||
data type implies a default renderer. This is only possible
|
This is generally only possible if :attr:`model_class` is set
|
||||||
if :attr:`model_class` is set to a SQLAlchemy mapped class.
|
to a valid SQLAlchemy mapped class.
|
||||||
|
|
||||||
This only looks for a couple of data types, and configures as
|
This (for now?) only looks for
|
||||||
follows:
|
:class:`sqlalchemy:sqlalchemy.types.DateTime` columns and if
|
||||||
|
any are found, they are configured to use
|
||||||
* :class:`sqlalchemy:sqlalchemy.types.Boolean` ->
|
:meth:`render_datetime()`.
|
||||||
:meth:`render_boolean()`
|
|
||||||
* :class:`sqlalchemy:sqlalchemy.types.DateTime` ->
|
|
||||||
:meth:`render_datetime()`
|
|
||||||
"""
|
"""
|
||||||
if not self.model_class:
|
if not self.model_class:
|
||||||
return
|
return
|
||||||
|
@ -654,8 +626,6 @@ 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):
|
||||||
"""
|
"""
|
||||||
|
@ -1783,80 +1753,23 @@ 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):
|
||||||
"""
|
"""
|
||||||
Column renderer for :class:`python:datetime.datetime` values.
|
Default cell value renderer for
|
||||||
|
:class:`sqlalchemy:sqlalchemy.types.DateTime` columns, which
|
||||||
This calls
|
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,6 +1,19 @@
|
||||||
## -*- 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()}
|
||||||
|
@ -52,7 +65,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="content">
|
<div class="markdown">
|
||||||
${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,21 +33,6 @@
|
||||||
% 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:
|
||||||
|
|
|
@ -1,84 +0,0 @@
|
||||||
# -*- 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': "Status",
|
'status_code': "Batch Status",
|
||||||
}
|
}
|
||||||
|
|
||||||
sort_defaults = ('id', 'desc')
|
sort_defaults = ('id', 'desc')
|
||||||
|
@ -62,10 +62,6 @@ 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()
|
||||||
|
@ -385,12 +381,6 @@ 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,20 +372,6 @@ 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.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
|
@ -423,7 +409,6 @@ 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
|
||||||
|
@ -616,8 +601,6 @@ 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)
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
|
@ -1586,7 +1569,6 @@ class MasterView(View):
|
||||||
label,
|
label,
|
||||||
variant=None,
|
variant=None,
|
||||||
primary=False,
|
primary=False,
|
||||||
url=None,
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
|
@ -1613,17 +1595,10 @@ 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:
|
||||||
|
|
||||||
|
@ -1645,45 +1620,7 @@ class MasterView(View):
|
||||||
elif primary:
|
elif primary:
|
||||||
btn_kw['type'] = 'is-primary'
|
btn_kw['type'] = 'is-primary'
|
||||||
|
|
||||||
if url:
|
return HTML.tag('b-button', **btn_kw)
|
||||||
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):
|
||||||
"""
|
"""
|
||||||
|
@ -2429,16 +2366,6 @@ 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()
|
||||||
|
@ -2557,16 +2484,6 @@ 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,8 +4,9 @@ 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 wuttaweb.testing import DataTestCase, WebTestCase
|
from tests.util import DataTestCase, WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestWutaDateTime(TestCase):
|
class TestWutaDateTime(TestCase):
|
||||||
|
@ -89,15 +89,6 @@ 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):
|
||||||
|
@ -164,8 +155,7 @@ 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):
|
||||||
with patch.object(mod, 'Session', return_value=self.session):
|
typ = mod.ObjectRef(self.request, session=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)
|
||||||
|
|
||||||
|
@ -191,8 +181,6 @@ 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)
|
||||||
|
@ -201,33 +189,31 @@ 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)
|
typ = mod.ObjectRef(self.request, session=self.session)
|
||||||
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)
|
typ = mod.ObjectRef(self.request, session=self.session)
|
||||||
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)
|
typ = mod.ObjectRef(self.request, session=self.session)
|
||||||
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):
|
||||||
with patch.object(mod, 'Session', return_value=self.session):
|
typ = mod.ObjectRef(self.request, session=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):
|
||||||
with patch.object(mod, 'Session', return_value=self.session):
|
typ = mod.ObjectRef(self.request, session=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)
|
||||||
|
@ -240,16 +226,14 @@ 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):
|
||||||
with patch.object(mod, 'Session', return_value=self.session):
|
typ = mod.ObjectRef(self.request, session=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):
|
||||||
with patch.object(mod, 'Session', return_value=self.session):
|
typ = mod.ObjectRef(self.request, session=self.session, empty_option=True)
|
||||||
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)")
|
||||||
|
@ -259,8 +243,7 @@ class TestObjectRef(DataTestCase):
|
||||||
class TestPersonRef(WebTestCase):
|
class TestPersonRef(WebTestCase):
|
||||||
|
|
||||||
def test_sort_query(self):
|
def test_sort_query(self):
|
||||||
with patch.object(mod, 'Session', return_value=self.session):
|
typ = mod.PersonRef(self.request, session=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)
|
||||||
|
@ -270,8 +253,7 @@ 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
|
||||||
with patch.object(mod, 'Session', return_value=self.session):
|
typ = mod.PersonRef(self.request, session=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)
|
||||||
|
@ -285,8 +267,7 @@ class TestPersonRef(WebTestCase):
|
||||||
class TestRoleRef(WebTestCase):
|
class TestRoleRef(WebTestCase):
|
||||||
|
|
||||||
def test_sort_query(self):
|
def test_sort_query(self):
|
||||||
with patch.object(mod, 'Session', return_value=self.session):
|
typ = mod.RoleRef(self.request, session=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)
|
||||||
|
@ -296,8 +277,7 @@ 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
|
||||||
with patch.object(mod, 'Session', return_value=self.session):
|
typ = mod.RoleRef(self.request, session=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)
|
||||||
|
@ -311,8 +291,7 @@ class TestRoleRef(WebTestCase):
|
||||||
class TestUserRef(WebTestCase):
|
class TestUserRef(WebTestCase):
|
||||||
|
|
||||||
def test_sort_query(self):
|
def test_sort_query(self):
|
||||||
with patch.object(mod, 'Session', return_value=self.session):
|
typ = mod.UserRef(self.request, session=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)
|
||||||
|
@ -322,8 +301,7 @@ 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
|
||||||
with patch.object(mod, 'Session', return_value=self.session):
|
typ = mod.UserRef(self.request, session=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)
|
||||||
|
@ -342,8 +320,7 @@ class TestUserRefs(DataTestCase):
|
||||||
|
|
||||||
def test_widget_maker(self):
|
def test_widget_maker(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
with patch.object(mod, 'Session', return_value=self.session):
|
typ = mod.UserRefs(self.request, session=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)
|
||||||
|
|
||||||
|
@ -364,11 +341,9 @@ 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)
|
typ = mod.RoleRefs(self.request, session=self.session)
|
||||||
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")
|
||||||
|
@ -376,7 +351,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)
|
typ = mod.RoleRefs(self.request, session=self.session)
|
||||||
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,10 +10,9 @@ 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 wuttaweb.testing import WebTestCase
|
from tests.util import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestObjectRefWidget(WebTestCase):
|
class TestObjectRefWidget(WebTestCase):
|
||||||
|
@ -33,17 +32,15 @@ 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))
|
node = colander.SchemaNode(PersonRef(self.request, session=self.session))
|
||||||
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))
|
node = colander.SchemaNode(PersonRef(self.request, session=self.session))
|
||||||
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)
|
||||||
|
@ -52,7 +49,7 @@ class TestObjectRefWidget(WebTestCase):
|
||||||
self.assertNotIn('<a', html)
|
self.assertNotIn('<a', html)
|
||||||
|
|
||||||
# with hyperlink
|
# with hyperlink
|
||||||
node = colander.SchemaNode(PersonRef(self.request))
|
node = colander.SchemaNode(PersonRef(self.request, session=self.session))
|
||||||
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)
|
||||||
|
@ -67,10 +64,8 @@ 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))
|
node = colander.SchemaNode(PersonRef(self.request, session=self.session))
|
||||||
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, {})
|
||||||
|
@ -78,7 +73,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,
|
node = colander.SchemaNode(PersonRef(self.request, session=self.session,
|
||||||
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)
|
||||||
|
@ -125,7 +120,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(schema.WuttaMoney(self.request))
|
node = colander.SchemaNode(WuttaDateTime())
|
||||||
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')
|
||||||
|
@ -143,36 +138,6 @@ 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):
|
||||||
|
@ -265,8 +230,7 @@ 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
|
||||||
with patch.object(schema, 'Session', return_value=self.session):
|
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
|
||||||
|
|
||||||
|
@ -282,7 +246,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))
|
node = colander.SchemaNode(RoleRefs(self.request, session=self.session))
|
||||||
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})
|
||||||
|
@ -302,9 +266,7 @@ 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
|
||||||
|
|
||||||
|
@ -356,7 +318,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))
|
node = colander.SchemaNode(Permissions(self.request, permissions, session=self.session))
|
||||||
field = self.make_field(node)
|
field = self.make_field(node)
|
||||||
widget = field.widget
|
widget = field.widget
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
# -*- 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
|
||||||
|
|
||||||
|
@ -16,7 +15,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 wuttaweb.testing import WebTestCase
|
from tests.util import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestGrid(WebTestCase):
|
class TestGrid(WebTestCase):
|
||||||
|
@ -208,11 +207,6 @@ 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
|
||||||
|
|
||||||
|
@ -238,17 +232,6 @@ 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, [])
|
||||||
|
@ -1348,73 +1331,6 @@ 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 wuttaweb.testing import WebTestCase
|
from tests.util 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 wuttaweb.testing import WebTestCase
|
from tests.util 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 wuttaweb.testing import WebTestCase
|
from tests.util 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 wuttaweb.testing import WebTestCase
|
from tests.util import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestMenuHandler(WebTestCase):
|
class TestMenuHandler(WebTestCase):
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
# -*- 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
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,6 +39,56 @@ 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 wuttaweb.testing import WebTestCase
|
from tests.util 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 wuttaweb.testing import WebTestCase
|
from tests.util 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 wuttaweb.testing import WebTestCase
|
from tests.util 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 wuttaweb.testing import WebTestCase
|
from tests.util import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class MockBatch(model.BatchMixin, model.Base):
|
class MockBatch(model.BatchMixin, model.Base):
|
||||||
|
@ -367,12 +367,6 @@ 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 wuttaweb.testing import WebTestCase
|
from tests.util 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 wuttaweb.testing import WebTestCase
|
from tests.util 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 wuttaweb.testing import WebTestCase
|
from tests.util 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 wuttaweb.testing import WebTestCase
|
from tests.util import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestMasterView(WebTestCase):
|
class TestMasterView(WebTestCase):
|
||||||
|
@ -441,12 +441,6 @@ 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
|
||||||
|
@ -1655,18 +1649,6 @@ 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 wuttaweb.testing import WebTestCase
|
from tests.util 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 wuttaweb.testing import WebTestCase
|
from tests.util 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 wuttaweb.testing import WebTestCase
|
from tests.util 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 wuttaweb.testing import WebTestCase
|
from tests.util 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 wuttaweb.testing import WebTestCase
|
from tests.util 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 wuttaweb.testing import WebTestCase
|
from tests.util import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestUserView(WebTestCase):
|
class TestUserView(WebTestCase):
|
||||||
|
|
Loading…
Reference in a new issue