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/)
|
||||
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)
|
||||
|
||||
### Feat
|
||||
|
|
|
@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
|||
|
||||
[project]
|
||||
name = "WuttaWeb"
|
||||
version = "0.19.0"
|
||||
version = "0.19.1"
|
||||
description = "Web App for Wutta Framework"
|
||||
readme = "README.md"
|
||||
authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
|
||||
|
@ -45,7 +45,7 @@ dependencies = [
|
|||
"SQLAlchemy-Utils",
|
||||
"waitress",
|
||||
"WebHelpers2",
|
||||
"WuttJamaican[db]>=0.19.1",
|
||||
"WuttJamaican[db]>=0.19.2",
|
||||
"zope.sqlalchemy>=1.5",
|
||||
]
|
||||
|
||||
|
|
|
@ -177,25 +177,41 @@ class WuttaMoney(colander.Money):
|
|||
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):
|
||||
"""
|
||||
Custom schema type for :class:`python:set` fields.
|
||||
|
||||
This is a subclass of :class:`colander.Set`, but adds
|
||||
Wutta-related params to the constructor.
|
||||
This is a subclass of :class:`colander.Set`.
|
||||
|
||||
: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__()
|
||||
self.request = request
|
||||
self.config = self.request.wutta_config
|
||||
self.app = self.config.get_app()
|
||||
self.session = session or Session()
|
||||
|
||||
|
||||
class ObjectRef(colander.SchemaType):
|
||||
|
@ -231,16 +247,16 @@ class ObjectRef(colander.SchemaType):
|
|||
self,
|
||||
request,
|
||||
empty_option=None,
|
||||
session=None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
# nb. allow session injection for tests
|
||||
self.session = kwargs.pop('session', Session())
|
||||
super().__init__(*args, **kwargs)
|
||||
self.request = request
|
||||
self.config = self.request.wutta_config
|
||||
self.app = self.config.get_app()
|
||||
self.model_instance = None
|
||||
self.session = session or Session()
|
||||
|
||||
if empty_option:
|
||||
if empty_option is True:
|
||||
|
@ -472,7 +488,7 @@ class RoleRefs(WuttaSet):
|
|||
:returns: Instance of
|
||||
:class:`~wuttaweb.forms.widgets.RoleRefsWidget`.
|
||||
"""
|
||||
kwargs.setdefault('session', self.session)
|
||||
session = kwargs.setdefault('session', Session())
|
||||
|
||||
if 'values' not in kwargs:
|
||||
model = self.app.model
|
||||
|
@ -480,20 +496,20 @@ class RoleRefs(WuttaSet):
|
|||
|
||||
# avoid built-ins which cannot be assigned to users
|
||||
avoid = {
|
||||
auth.get_role_authenticated(self.session),
|
||||
auth.get_role_anonymous(self.session),
|
||||
auth.get_role_authenticated(session),
|
||||
auth.get_role_anonymous(session),
|
||||
}
|
||||
avoid = set([role.uuid for role in avoid])
|
||||
|
||||
# also avoid admin unless current user 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
|
||||
roles = self.session.query(model.Role)\
|
||||
.filter(~model.Role.uuid.in_(avoid))\
|
||||
.order_by(model.Role.name)\
|
||||
.all()
|
||||
roles = session.query(model.Role)\
|
||||
.filter(~model.Role.uuid.in_(avoid))\
|
||||
.order_by(model.Role.name)\
|
||||
.all()
|
||||
values = [(role.uuid.hex, role.name) for role in roles]
|
||||
kwargs['values'] = values
|
||||
|
||||
|
@ -518,7 +534,7 @@ class UserRefs(WuttaSet):
|
|||
:returns: Instance of
|
||||
:class:`~wuttaweb.forms.widgets.UserRefsWidget`.
|
||||
"""
|
||||
kwargs.setdefault('session', self.session)
|
||||
kwargs.setdefault('session', Session())
|
||||
return widgets.UserRefsWidget(self.request, **kwargs)
|
||||
|
||||
|
||||
|
@ -548,7 +564,7 @@ class Permissions(WuttaSet):
|
|||
:returns: Instance of
|
||||
:class:`~wuttaweb.forms.widgets.PermissionsWidget`.
|
||||
"""
|
||||
kwargs.setdefault('session', self.session)
|
||||
kwargs.setdefault('session', Session())
|
||||
kwargs.setdefault('permissions', self.permissions)
|
||||
|
||||
if 'values' not in kwargs:
|
||||
|
|
|
@ -136,26 +136,21 @@ class WuttaCheckboxChoiceWidget(CheckboxChoiceWidget):
|
|||
Custom widget for :class:`python:set` fields.
|
||||
|
||||
This is a subclass of
|
||||
:class:`deform:deform.widget.CheckboxChoiceWidget`, but adds
|
||||
Wutta-related params to the constructor.
|
||||
:class:`deform:deform.widget.CheckboxChoiceWidget`.
|
||||
|
||||
: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:
|
||||
|
||||
* ``checkbox_choice``
|
||||
* ``readonly/checkbox_choice``
|
||||
"""
|
||||
|
||||
def __init__(self, request, session=None, *args, **kwargs):
|
||||
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()
|
||||
self.session = session or Session()
|
||||
|
||||
|
||||
class WuttaDateTimeWidget(DateTimeInputWidget):
|
||||
|
@ -231,6 +226,42 @@ class WuttaMoneyInputWidget(MoneyInputWidget):
|
|||
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):
|
||||
"""
|
||||
Widget for use with :class:`~wuttaweb.forms.schema.FileDownload`
|
||||
|
|
|
@ -116,7 +116,8 @@ class Grid:
|
|||
|
||||
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
|
||||
|
||||
|
@ -388,7 +389,6 @@ class Grid:
|
|||
self.key = key
|
||||
self.data = data
|
||||
self.labels = labels or {}
|
||||
self.renderers = renderers or {}
|
||||
self.row_class = row_class
|
||||
self.actions = actions or []
|
||||
self.linked_columns = linked_columns or []
|
||||
|
@ -398,6 +398,10 @@ class Grid:
|
|||
self.app = self.config.get_app()
|
||||
|
||||
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_tools(tools)
|
||||
|
||||
|
@ -592,8 +596,29 @@ class Grid:
|
|||
grid = Grid(request, columns=['foo', 'bar'])
|
||||
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`.
|
||||
"""
|
||||
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:
|
||||
renderer = functools.partial(renderer, **kwargs)
|
||||
self.renderers[key] = renderer
|
||||
|
@ -602,15 +627,18 @@ class Grid:
|
|||
"""
|
||||
Set default column value renderers, where applicable.
|
||||
|
||||
This will add new entries to :attr:`renderers` for columns
|
||||
whose data type implies a default renderer should be used.
|
||||
This is generally only possible if :attr:`model_class` is set
|
||||
to a valid SQLAlchemy mapped class.
|
||||
This is called automatically from the class constructor. It
|
||||
will add new entries to :attr:`renderers` for columns whose
|
||||
data type implies a default renderer. This is only possible
|
||||
if :attr:`model_class` is set to a SQLAlchemy mapped class.
|
||||
|
||||
This (for now?) only looks for
|
||||
:class:`sqlalchemy:sqlalchemy.types.DateTime` columns and if
|
||||
any are found, they are configured to use
|
||||
:meth:`render_datetime()`.
|
||||
This only looks for a couple of data types, and configures as
|
||||
follows:
|
||||
|
||||
* :class:`sqlalchemy:sqlalchemy.types.Boolean` ->
|
||||
:meth:`render_boolean()`
|
||||
* :class:`sqlalchemy:sqlalchemy.types.DateTime` ->
|
||||
:meth:`render_datetime()`
|
||||
"""
|
||||
if not self.model_class:
|
||||
return
|
||||
|
@ -626,6 +654,8 @@ class Grid:
|
|||
column = prop.columns[0]
|
||||
if isinstance(column.type, sa.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):
|
||||
"""
|
||||
|
@ -1753,23 +1783,80 @@ class Grid:
|
|||
# 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):
|
||||
"""
|
||||
Default cell value renderer for
|
||||
:class:`sqlalchemy:sqlalchemy.types.DateTime` columns, which
|
||||
calls
|
||||
Column renderer for :class:`python:datetime.datetime` values.
|
||||
|
||||
This calls
|
||||
:meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_datetime()`
|
||||
for the return value.
|
||||
|
||||
This may be used automatically per
|
||||
:meth:`set_default_renderers()` or you can use it explicitly
|
||||
for any :class:`python:datetime.datetime` column with::
|
||||
:meth:`set_default_renderers()` or you can use it explicitly::
|
||||
|
||||
grid.set_renderer('foo', grid.render_datetime)
|
||||
"""
|
||||
dt = getattr(obj, key)
|
||||
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(
|
||||
self,
|
||||
form=None,
|
||||
|
|
|
@ -1,19 +1,6 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%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()">
|
||||
${parent.tool_panels()}
|
||||
${self.tool_panel_execution()}
|
||||
|
@ -65,7 +52,7 @@
|
|||
<p class="block has-text-weight-bold">
|
||||
What will happen when this batch is executed?
|
||||
</p>
|
||||
<div class="markdown">
|
||||
<div class="content">
|
||||
${execution_described|n}
|
||||
</div>
|
||||
${h.form(master.get_action_url('execute', batch), ref='executeForm')}
|
||||
|
|
|
@ -33,6 +33,21 @@
|
|||
% endif
|
||||
</%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()">
|
||||
${parent.render_vue_templates()}
|
||||
% 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 = {
|
||||
'id': "Batch ID",
|
||||
'status_code': "Batch Status",
|
||||
'status_code': "Status",
|
||||
}
|
||||
|
||||
sort_defaults = ('id', 'desc')
|
||||
|
@ -62,6 +62,10 @@ class BatchMasterView(MasterView):
|
|||
rows_title = "Batch Rows"
|
||||
rows_sort_defaults = 'sequence'
|
||||
|
||||
row_labels = {
|
||||
'status_code': "Status",
|
||||
}
|
||||
|
||||
def __init__(self, request, context=None):
|
||||
super().__init__(request, context=context)
|
||||
self.batch_handler = self.get_batch_handler()
|
||||
|
@ -381,6 +385,12 @@ class BatchMasterView(MasterView):
|
|||
|
||||
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
|
||||
##############################
|
||||
|
|
|
@ -372,6 +372,20 @@ class MasterView(View):
|
|||
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()`.
|
||||
|
||||
.. 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_paginated = True
|
||||
rows_paginate_on_backend = True
|
||||
rows_viewable = False
|
||||
|
||||
# current action
|
||||
listing = False
|
||||
|
@ -601,6 +616,8 @@ class MasterView(View):
|
|||
|
||||
context['rows_grid'] = grid
|
||||
|
||||
context['xref_buttons'] = self.get_xref_buttons(obj)
|
||||
|
||||
return self.render_to_response('view', context)
|
||||
|
||||
##############################
|
||||
|
@ -1569,6 +1586,7 @@ class MasterView(View):
|
|||
label,
|
||||
variant=None,
|
||||
primary=False,
|
||||
url=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
|
@ -1595,10 +1613,17 @@ class MasterView(View):
|
|||
avoids the Buefy vs. Oruga confusion, and the
|
||||
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
|
||||
underlying ``HTML.tag()`` call, so will be rendered as
|
||||
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
|
||||
along the lines of:
|
||||
|
||||
|
@ -1620,7 +1645,45 @@ class MasterView(View):
|
|||
elif 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):
|
||||
"""
|
||||
|
@ -2366,6 +2429,16 @@ class MasterView(View):
|
|||
kwargs.setdefault('paginated', self.rows_paginated)
|
||||
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)
|
||||
self.configure_row_grid(grid)
|
||||
grid.load_settings()
|
||||
|
@ -2484,6 +2557,16 @@ class MasterView(View):
|
|||
labels.update(cls.row_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
|
||||
##############################
|
||||
|
|
|
@ -4,9 +4,8 @@ from unittest.mock import patch, MagicMock
|
|||
|
||||
import pytest
|
||||
|
||||
from tests.util import WebTestCase
|
||||
|
||||
from wuttaweb.db import continuum as mod
|
||||
from wuttaweb.testing import WebTestCase
|
||||
|
||||
|
||||
class TestWuttaWebContinuumPlugin(WebTestCase):
|
||||
|
|
|
@ -12,7 +12,7 @@ from sqlalchemy import orm
|
|||
from wuttjamaican.conf import WuttaConfig
|
||||
from wuttaweb.forms import schema as mod
|
||||
from wuttaweb.forms import widgets
|
||||
from tests.util import DataTestCase, WebTestCase
|
||||
from wuttaweb.testing import DataTestCase, WebTestCase
|
||||
|
||||
|
||||
class TestWutaDateTime(TestCase):
|
||||
|
@ -89,6 +89,15 @@ class TestWuttaMoney(WebTestCase):
|
|||
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):
|
||||
|
||||
def setUp(self):
|
||||
|
@ -155,9 +164,10 @@ class TestObjectRef(DataTestCase):
|
|||
self.session.commit()
|
||||
self.assertIsNotNone(person.uuid)
|
||||
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
|
||||
typ = mod.ObjectRef(self.request, session=self.session)
|
||||
value = typ.deserialize(node, person.uuid)
|
||||
self.assertIs(value, person)
|
||||
with patch.object(mod, 'Session', return_value=self.session):
|
||||
typ = mod.ObjectRef(self.request)
|
||||
value = typ.deserialize(node, person.uuid)
|
||||
self.assertIs(value, person)
|
||||
|
||||
def test_dictify(self):
|
||||
model = self.app.model
|
||||
|
@ -181,42 +191,46 @@ class TestObjectRef(DataTestCase):
|
|||
value = typ.objectify(None)
|
||||
self.assertIsNone(value)
|
||||
|
||||
# model instance
|
||||
person = model.Person(full_name="Betty Boop")
|
||||
self.session.add(person)
|
||||
self.session.commit()
|
||||
self.assertIsNotNone(person.uuid)
|
||||
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
|
||||
with patch.object(mod, 'Session', return_value=self.session):
|
||||
|
||||
# can specify as uuid
|
||||
typ = mod.ObjectRef(self.request, session=self.session)
|
||||
value = typ.objectify(person.uuid)
|
||||
self.assertIs(value, person)
|
||||
# model instance
|
||||
person = model.Person(full_name="Betty Boop")
|
||||
self.session.add(person)
|
||||
self.session.commit()
|
||||
self.assertIsNotNone(person.uuid)
|
||||
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
|
||||
|
||||
# or can specify object proper
|
||||
typ = mod.ObjectRef(self.request, session=self.session)
|
||||
value = typ.objectify(person)
|
||||
self.assertIs(value, person)
|
||||
# can specify as uuid
|
||||
typ = mod.ObjectRef(self.request)
|
||||
value = typ.objectify(person.uuid)
|
||||
self.assertIs(value, person)
|
||||
|
||||
# error if not found
|
||||
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
|
||||
typ = mod.ObjectRef(self.request, session=self.session)
|
||||
self.assertRaises(ValueError, typ.objectify, 'WRONG-UUID')
|
||||
# or can specify object proper
|
||||
typ = mod.ObjectRef(self.request)
|
||||
value = typ.objectify(person)
|
||||
self.assertIs(value, person)
|
||||
|
||||
# error if not found
|
||||
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
|
||||
typ = mod.ObjectRef(self.request)
|
||||
self.assertRaises(ValueError, typ.objectify, 'WRONG-UUID')
|
||||
|
||||
def test_get_query(self):
|
||||
model = self.app.model
|
||||
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
|
||||
typ = mod.ObjectRef(self.request, session=self.session)
|
||||
query = typ.get_query()
|
||||
self.assertIsInstance(query, orm.Query)
|
||||
with patch.object(mod, 'Session', return_value=self.session):
|
||||
typ = mod.ObjectRef(self.request)
|
||||
query = typ.get_query()
|
||||
self.assertIsInstance(query, orm.Query)
|
||||
|
||||
def test_sort_query(self):
|
||||
model = self.app.model
|
||||
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
|
||||
typ = mod.ObjectRef(self.request, session=self.session)
|
||||
query = typ.get_query()
|
||||
sorted_query = typ.sort_query(query)
|
||||
self.assertIs(sorted_query, query)
|
||||
with patch.object(mod, 'Session', return_value=self.session):
|
||||
typ = mod.ObjectRef(self.request)
|
||||
query = typ.get_query()
|
||||
sorted_query = typ.sort_query(query)
|
||||
self.assertIs(sorted_query, query)
|
||||
|
||||
def test_widget_maker(self):
|
||||
model = self.app.model
|
||||
|
@ -226,90 +240,98 @@ class TestObjectRef(DataTestCase):
|
|||
|
||||
# basic
|
||||
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
|
||||
typ = mod.ObjectRef(self.request, session=self.session)
|
||||
widget = typ.widget_maker()
|
||||
self.assertEqual(len(widget.values), 1)
|
||||
self.assertEqual(widget.values[0][1], "Betty Boop")
|
||||
with patch.object(mod, 'Session', return_value=self.session):
|
||||
typ = mod.ObjectRef(self.request)
|
||||
widget = typ.widget_maker()
|
||||
self.assertEqual(len(widget.values), 1)
|
||||
self.assertEqual(widget.values[0][1], "Betty Boop")
|
||||
|
||||
# empty option
|
||||
with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
|
||||
typ = mod.ObjectRef(self.request, session=self.session, empty_option=True)
|
||||
widget = typ.widget_maker()
|
||||
self.assertEqual(len(widget.values), 2)
|
||||
self.assertEqual(widget.values[0][1], "(none)")
|
||||
self.assertEqual(widget.values[1][1], "Betty Boop")
|
||||
with patch.object(mod, 'Session', return_value=self.session):
|
||||
typ = mod.ObjectRef(self.request, empty_option=True)
|
||||
widget = typ.widget_maker()
|
||||
self.assertEqual(len(widget.values), 2)
|
||||
self.assertEqual(widget.values[0][1], "(none)")
|
||||
self.assertEqual(widget.values[1][1], "Betty Boop")
|
||||
|
||||
|
||||
class TestPersonRef(WebTestCase):
|
||||
|
||||
def test_sort_query(self):
|
||||
typ = mod.PersonRef(self.request, session=self.session)
|
||||
query = typ.get_query()
|
||||
self.assertIsInstance(query, orm.Query)
|
||||
sorted_query = typ.sort_query(query)
|
||||
self.assertIsInstance(sorted_query, orm.Query)
|
||||
self.assertIsNot(sorted_query, query)
|
||||
with patch.object(mod, 'Session', return_value=self.session):
|
||||
typ = mod.PersonRef(self.request)
|
||||
query = typ.get_query()
|
||||
self.assertIsInstance(query, orm.Query)
|
||||
sorted_query = typ.sort_query(query)
|
||||
self.assertIsInstance(sorted_query, orm.Query)
|
||||
self.assertIsNot(sorted_query, query)
|
||||
|
||||
def test_get_object_url(self):
|
||||
self.pyramid_config.add_route('people.view', '/people/{uuid}')
|
||||
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")
|
||||
self.session.add(person)
|
||||
self.session.commit()
|
||||
person = model.Person(full_name="Barney Rubble")
|
||||
self.session.add(person)
|
||||
self.session.commit()
|
||||
|
||||
url = typ.get_object_url(person)
|
||||
self.assertIsNotNone(url)
|
||||
self.assertIn(f'/people/{person.uuid}', url)
|
||||
url = typ.get_object_url(person)
|
||||
self.assertIsNotNone(url)
|
||||
self.assertIn(f'/people/{person.uuid}', url)
|
||||
|
||||
|
||||
class TestRoleRef(WebTestCase):
|
||||
|
||||
def test_sort_query(self):
|
||||
typ = mod.RoleRef(self.request, session=self.session)
|
||||
query = typ.get_query()
|
||||
self.assertIsInstance(query, orm.Query)
|
||||
sorted_query = typ.sort_query(query)
|
||||
self.assertIsInstance(sorted_query, orm.Query)
|
||||
self.assertIsNot(sorted_query, query)
|
||||
with patch.object(mod, 'Session', return_value=self.session):
|
||||
typ = mod.RoleRef(self.request)
|
||||
query = typ.get_query()
|
||||
self.assertIsInstance(query, orm.Query)
|
||||
sorted_query = typ.sort_query(query)
|
||||
self.assertIsInstance(sorted_query, orm.Query)
|
||||
self.assertIsNot(sorted_query, query)
|
||||
|
||||
def test_get_object_url(self):
|
||||
self.pyramid_config.add_route('roles.view', '/roles/{uuid}')
|
||||
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')
|
||||
self.session.add(role)
|
||||
self.session.commit()
|
||||
role = model.Role(name='Manager')
|
||||
self.session.add(role)
|
||||
self.session.commit()
|
||||
|
||||
url = typ.get_object_url(role)
|
||||
self.assertIsNotNone(url)
|
||||
self.assertIn(f'/roles/{role.uuid}', url)
|
||||
url = typ.get_object_url(role)
|
||||
self.assertIsNotNone(url)
|
||||
self.assertIn(f'/roles/{role.uuid}', url)
|
||||
|
||||
|
||||
class TestUserRef(WebTestCase):
|
||||
|
||||
def test_sort_query(self):
|
||||
typ = mod.UserRef(self.request, session=self.session)
|
||||
query = typ.get_query()
|
||||
self.assertIsInstance(query, orm.Query)
|
||||
sorted_query = typ.sort_query(query)
|
||||
self.assertIsInstance(sorted_query, orm.Query)
|
||||
self.assertIsNot(sorted_query, query)
|
||||
with patch.object(mod, 'Session', return_value=self.session):
|
||||
typ = mod.UserRef(self.request)
|
||||
query = typ.get_query()
|
||||
self.assertIsInstance(query, orm.Query)
|
||||
sorted_query = typ.sort_query(query)
|
||||
self.assertIsInstance(sorted_query, orm.Query)
|
||||
self.assertIsNot(sorted_query, query)
|
||||
|
||||
def test_get_object_url(self):
|
||||
self.pyramid_config.add_route('users.view', '/users/{uuid}')
|
||||
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')
|
||||
self.session.add(user)
|
||||
self.session.commit()
|
||||
user = model.User(username='barney')
|
||||
self.session.add(user)
|
||||
self.session.commit()
|
||||
|
||||
url = typ.get_object_url(user)
|
||||
self.assertIsNotNone(url)
|
||||
self.assertIn(f'/users/{user.uuid}', url)
|
||||
url = typ.get_object_url(user)
|
||||
self.assertIsNotNone(url)
|
||||
self.assertIn(f'/users/{user.uuid}', url)
|
||||
|
||||
|
||||
class TestUserRefs(DataTestCase):
|
||||
|
@ -320,9 +342,10 @@ class TestUserRefs(DataTestCase):
|
|||
|
||||
def test_widget_maker(self):
|
||||
model = self.app.model
|
||||
typ = mod.UserRefs(self.request, session=self.session)
|
||||
widget = typ.widget_maker()
|
||||
self.assertIsInstance(widget, widgets.UserRefsWidget)
|
||||
with patch.object(mod, 'Session', return_value=self.session):
|
||||
typ = mod.UserRefs(self.request)
|
||||
widget = typ.widget_maker()
|
||||
self.assertIsInstance(widget, widgets.UserRefsWidget)
|
||||
|
||||
|
||||
class TestRoleRefs(DataTestCase):
|
||||
|
@ -341,20 +364,22 @@ class TestRoleRefs(DataTestCase):
|
|||
self.session.add(blokes)
|
||||
self.session.commit()
|
||||
|
||||
# with root access, default values include: admin, blokes
|
||||
self.request.is_root = True
|
||||
typ = mod.RoleRefs(self.request, session=self.session)
|
||||
widget = typ.widget_maker()
|
||||
self.assertEqual(len(widget.values), 2)
|
||||
self.assertEqual(widget.values[0][1], "Administrator")
|
||||
self.assertEqual(widget.values[1][1], "Blokes")
|
||||
with patch.object(mod, 'Session', return_value=self.session):
|
||||
|
||||
# without root, default values include: blokes
|
||||
self.request.is_root = False
|
||||
typ = mod.RoleRefs(self.request, session=self.session)
|
||||
widget = typ.widget_maker()
|
||||
self.assertEqual(len(widget.values), 1)
|
||||
self.assertEqual(widget.values[0][1], "Blokes")
|
||||
# with root access, default values include: admin, blokes
|
||||
self.request.is_root = True
|
||||
typ = mod.RoleRefs(self.request)
|
||||
widget = typ.widget_maker()
|
||||
self.assertEqual(len(widget.values), 2)
|
||||
self.assertEqual(widget.values[0][1], "Administrator")
|
||||
self.assertEqual(widget.values[1][1], "Blokes")
|
||||
|
||||
# without root, default values include: blokes
|
||||
self.request.is_root = False
|
||||
typ = mod.RoleRefs(self.request)
|
||||
widget = typ.widget_maker()
|
||||
self.assertEqual(len(widget.values), 1)
|
||||
self.assertEqual(widget.values[0][1], "Blokes")
|
||||
|
||||
|
||||
class TestPermissions(DataTestCase):
|
||||
|
|
|
@ -10,9 +10,10 @@ from pyramid import testing
|
|||
|
||||
from wuttaweb import grids
|
||||
from wuttaweb.forms import widgets as mod
|
||||
from wuttaweb.forms import schema
|
||||
from wuttaweb.forms.schema import (FileDownload, PersonRef, RoleRefs, UserRefs, Permissions,
|
||||
WuttaDateTime, EmailRecipients)
|
||||
from tests.util import WebTestCase
|
||||
from wuttaweb.testing import WebTestCase
|
||||
|
||||
|
||||
class TestObjectRefWidget(WebTestCase):
|
||||
|
@ -32,31 +33,33 @@ class TestObjectRefWidget(WebTestCase):
|
|||
self.session.add(person)
|
||||
self.session.commit()
|
||||
|
||||
# standard (editable)
|
||||
node = colander.SchemaNode(PersonRef(self.request, session=self.session))
|
||||
widget = self.make_widget()
|
||||
field = self.make_field(node)
|
||||
html = widget.serialize(field, person.uuid)
|
||||
self.assertIn('<b-select ', html)
|
||||
with patch.object(schema, 'Session', return_value=self.session):
|
||||
|
||||
# readonly
|
||||
node = colander.SchemaNode(PersonRef(self.request, session=self.session))
|
||||
node.model_instance = person
|
||||
widget = self.make_widget()
|
||||
field = self.make_field(node)
|
||||
html = widget.serialize(field, person.uuid, readonly=True)
|
||||
self.assertIn('Betty Boop', html)
|
||||
self.assertNotIn('<a', html)
|
||||
# standard (editable)
|
||||
node = colander.SchemaNode(PersonRef(self.request))
|
||||
widget = self.make_widget()
|
||||
field = self.make_field(node)
|
||||
html = widget.serialize(field, person.uuid)
|
||||
self.assertIn('<b-select ', html)
|
||||
|
||||
# with hyperlink
|
||||
node = colander.SchemaNode(PersonRef(self.request, session=self.session))
|
||||
node.model_instance = person
|
||||
widget = self.make_widget(url=lambda p: '/foo')
|
||||
field = self.make_field(node)
|
||||
html = widget.serialize(field, person.uuid, readonly=True)
|
||||
self.assertIn('Betty Boop', html)
|
||||
self.assertIn('<a', html)
|
||||
self.assertIn('href="/foo"', html)
|
||||
# readonly
|
||||
node = colander.SchemaNode(PersonRef(self.request))
|
||||
node.model_instance = person
|
||||
widget = self.make_widget()
|
||||
field = self.make_field(node)
|
||||
html = widget.serialize(field, person.uuid, readonly=True)
|
||||
self.assertIn('Betty Boop', html)
|
||||
self.assertNotIn('<a', html)
|
||||
|
||||
# with hyperlink
|
||||
node = colander.SchemaNode(PersonRef(self.request))
|
||||
node.model_instance = person
|
||||
widget = self.make_widget(url=lambda p: '/foo')
|
||||
field = self.make_field(node)
|
||||
html = widget.serialize(field, person.uuid, readonly=True)
|
||||
self.assertIn('Betty Boop', html)
|
||||
self.assertIn('<a', html)
|
||||
self.assertIn('href="/foo"', html)
|
||||
|
||||
def test_get_template_values(self):
|
||||
model = self.app.model
|
||||
|
@ -64,22 +67,24 @@ class TestObjectRefWidget(WebTestCase):
|
|||
self.session.add(person)
|
||||
self.session.commit()
|
||||
|
||||
# standard
|
||||
node = colander.SchemaNode(PersonRef(self.request, session=self.session))
|
||||
widget = self.make_widget()
|
||||
field = self.make_field(node)
|
||||
values = widget.get_template_values(field, person.uuid, {})
|
||||
self.assertIn('cstruct', values)
|
||||
self.assertNotIn('url', values)
|
||||
with patch.object(schema, 'Session', return_value=self.session):
|
||||
|
||||
# readonly w/ empty option
|
||||
node = colander.SchemaNode(PersonRef(self.request, session=self.session,
|
||||
empty_option=('_empty_', '(empty)')))
|
||||
widget = self.make_widget(readonly=True, url=lambda obj: '/foo')
|
||||
field = self.make_field(node)
|
||||
values = widget.get_template_values(field, '_empty_', {})
|
||||
self.assertIn('cstruct', values)
|
||||
self.assertNotIn('url', values)
|
||||
# standard
|
||||
node = colander.SchemaNode(PersonRef(self.request))
|
||||
widget = self.make_widget()
|
||||
field = self.make_field(node)
|
||||
values = widget.get_template_values(field, person.uuid, {})
|
||||
self.assertIn('cstruct', values)
|
||||
self.assertNotIn('url', values)
|
||||
|
||||
# readonly w/ empty option
|
||||
node = colander.SchemaNode(PersonRef(self.request,
|
||||
empty_option=('_empty_', '(empty)')))
|
||||
widget = self.make_widget(readonly=True, url=lambda obj: '/foo')
|
||||
field = self.make_field(node)
|
||||
values = widget.get_template_values(field, '_empty_', {})
|
||||
self.assertIn('cstruct', values)
|
||||
self.assertNotIn('url', values)
|
||||
|
||||
|
||||
class TestWuttaDateTimeWidget(WebTestCase):
|
||||
|
@ -120,7 +125,7 @@ class TestWuttaMoneyInputWidget(WebTestCase):
|
|||
return mod.WuttaMoneyInputWidget(self.request, **kwargs)
|
||||
|
||||
def test_serialize(self):
|
||||
node = colander.SchemaNode(WuttaDateTime())
|
||||
node = colander.SchemaNode(schema.WuttaMoney(self.request))
|
||||
field = self.make_field(node)
|
||||
widget = self.make_widget()
|
||||
amount = decimal.Decimal('12.34')
|
||||
|
@ -138,6 +143,36 @@ class TestWuttaMoneyInputWidget(WebTestCase):
|
|||
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):
|
||||
|
||||
def make_field(self, node, **kwargs):
|
||||
|
@ -230,28 +265,29 @@ class TestRoleRefsWidget(WebTestCase):
|
|||
self.session.commit()
|
||||
|
||||
# nb. we let the field construct the widget via our type
|
||||
node = colander.SchemaNode(RoleRefs(self.request, session=self.session))
|
||||
field = self.make_field(node)
|
||||
widget = field.widget
|
||||
with patch.object(schema, 'Session', return_value=self.session):
|
||||
node = colander.SchemaNode(RoleRefs(self.request))
|
||||
field = self.make_field(node)
|
||||
widget = field.widget
|
||||
|
||||
# readonly values list includes admin
|
||||
html = widget.serialize(field, {admin.uuid, blokes.uuid}, readonly=True)
|
||||
self.assertIn(admin.name, html)
|
||||
self.assertIn(blokes.name, html)
|
||||
# readonly values list includes admin
|
||||
html = widget.serialize(field, {admin.uuid, blokes.uuid}, readonly=True)
|
||||
self.assertIn(admin.name, html)
|
||||
self.assertIn(blokes.name, html)
|
||||
|
||||
# editable values list *excludes* admin (by default)
|
||||
html = widget.serialize(field, {admin.uuid, blokes.uuid})
|
||||
self.assertNotIn(str(admin.uuid.hex), html)
|
||||
self.assertIn(str(blokes.uuid.hex), html)
|
||||
# editable values list *excludes* admin (by default)
|
||||
html = widget.serialize(field, {admin.uuid, blokes.uuid})
|
||||
self.assertNotIn(str(admin.uuid.hex), html)
|
||||
self.assertIn(str(blokes.uuid.hex), html)
|
||||
|
||||
# but admin is included for root user
|
||||
self.request.is_root = True
|
||||
node = colander.SchemaNode(RoleRefs(self.request, session=self.session))
|
||||
field = self.make_field(node)
|
||||
widget = field.widget
|
||||
html = widget.serialize(field, {admin.uuid, blokes.uuid})
|
||||
self.assertIn(str(admin.uuid.hex), html)
|
||||
self.assertIn(str(blokes.uuid.hex), html)
|
||||
# but admin is included for root user
|
||||
self.request.is_root = True
|
||||
node = colander.SchemaNode(RoleRefs(self.request))
|
||||
field = self.make_field(node)
|
||||
widget = field.widget
|
||||
html = widget.serialize(field, {admin.uuid, blokes.uuid})
|
||||
self.assertIn(str(admin.uuid.hex), html)
|
||||
self.assertIn(str(blokes.uuid.hex), html)
|
||||
|
||||
|
||||
class TestUserRefsWidget(WebTestCase):
|
||||
|
@ -266,35 +302,37 @@ class TestUserRefsWidget(WebTestCase):
|
|||
model = self.app.model
|
||||
|
||||
# nb. we let the field construct the widget via our type
|
||||
node = colander.SchemaNode(UserRefs(self.request, session=self.session))
|
||||
field = self.make_field(node)
|
||||
widget = field.widget
|
||||
# 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)
|
||||
widget = field.widget
|
||||
|
||||
# readonly is required
|
||||
self.assertRaises(NotImplementedError, widget.serialize, field, set())
|
||||
self.assertRaises(NotImplementedError, widget.serialize, field, set(), readonly=False)
|
||||
# readonly is required
|
||||
self.assertRaises(NotImplementedError, widget.serialize, field, set())
|
||||
self.assertRaises(NotImplementedError, widget.serialize, field, set(), readonly=False)
|
||||
|
||||
# empty
|
||||
html = widget.serialize(field, set(), readonly=True)
|
||||
self.assertEqual(html, '<span></span>')
|
||||
# empty
|
||||
html = widget.serialize(field, set(), readonly=True)
|
||||
self.assertEqual(html, '<span></span>')
|
||||
|
||||
# with data, no actions
|
||||
user = model.User(username='barney')
|
||||
self.session.add(user)
|
||||
self.session.commit()
|
||||
html = widget.serialize(field, {user.uuid}, readonly=True)
|
||||
self.assertIn('<b-table ', html)
|
||||
self.assertNotIn('Actions', html)
|
||||
self.assertNotIn('View', html)
|
||||
self.assertNotIn('Edit', html)
|
||||
|
||||
# with view/edit actions
|
||||
with patch.object(self.request, 'is_root', new=True):
|
||||
# with data, no actions
|
||||
user = model.User(username='barney')
|
||||
self.session.add(user)
|
||||
self.session.commit()
|
||||
html = widget.serialize(field, {user.uuid}, readonly=True)
|
||||
self.assertIn('<b-table ', html)
|
||||
self.assertIn('Actions', html)
|
||||
self.assertIn('View', html)
|
||||
self.assertIn('Edit', html)
|
||||
self.assertIn('<b-table ', html)
|
||||
self.assertNotIn('Actions', html)
|
||||
self.assertNotIn('View', html)
|
||||
self.assertNotIn('Edit', html)
|
||||
|
||||
# with view/edit actions
|
||||
with patch.object(self.request, 'is_root', new=True):
|
||||
html = widget.serialize(field, {user.uuid}, readonly=True)
|
||||
self.assertIn('<b-table ', html)
|
||||
self.assertIn('Actions', html)
|
||||
self.assertIn('View', html)
|
||||
self.assertIn('Edit', html)
|
||||
|
||||
|
||||
class TestPermissionsWidget(WebTestCase):
|
||||
|
@ -318,7 +356,7 @@ class TestPermissionsWidget(WebTestCase):
|
|||
}
|
||||
|
||||
# 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)
|
||||
widget = field.widget
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
|
||||
import datetime
|
||||
import decimal
|
||||
from unittest import TestCase
|
||||
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.util import FieldList
|
||||
from wuttaweb.forms import Form
|
||||
from tests.util import WebTestCase
|
||||
from wuttaweb.testing import WebTestCase
|
||||
|
||||
|
||||
class TestGrid(WebTestCase):
|
||||
|
@ -207,6 +208,11 @@ class TestGrid(WebTestCase):
|
|||
self.assertIsNot(grid.renderers['foo'], render2)
|
||||
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):
|
||||
model = self.app.model
|
||||
|
||||
|
@ -232,6 +238,17 @@ class TestGrid(WebTestCase):
|
|||
self.assertIn('created', grid.renderers)
|
||||
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):
|
||||
grid = self.make_grid(columns=['foo', 'bar'])
|
||||
self.assertEqual(grid.linked_columns, [])
|
||||
|
@ -1331,6 +1348,73 @@ class TestGrid(WebTestCase):
|
|||
# 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):
|
||||
grid = self.make_grid(columns=['foo', 'bar'])
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import sqlalchemy as sa
|
|||
from wuttjamaican.db.model import Base
|
||||
|
||||
from wuttaweb.grids import filters as mod
|
||||
from tests.util import WebTestCase
|
||||
from wuttaweb.testing import WebTestCase
|
||||
|
||||
|
||||
class TestGridFilter(WebTestCase):
|
||||
|
|
|
@ -8,7 +8,7 @@ from pyramid import testing
|
|||
|
||||
from wuttjamaican.conf import WuttaConfig
|
||||
from wuttaweb import auth as mod
|
||||
from tests.util import WebTestCase
|
||||
from wuttaweb.testing import WebTestCase
|
||||
|
||||
|
||||
class TestLoginUser(TestCase):
|
||||
|
|
|
@ -4,7 +4,7 @@ from wuttaweb import handler as mod, static
|
|||
from wuttaweb.forms import Form
|
||||
from wuttaweb.grids import Grid
|
||||
from wuttaweb.menus import MenuHandler
|
||||
from tests.util import WebTestCase
|
||||
from wuttaweb.testing import WebTestCase
|
||||
|
||||
|
||||
class TestWebHandler(WebTestCase):
|
||||
|
|
|
@ -4,7 +4,7 @@ from unittest import TestCase
|
|||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from wuttaweb import menus as mod
|
||||
from tests.util import WebTestCase
|
||||
from wuttaweb.testing import WebTestCase
|
||||
|
||||
|
||||
class TestMenuHandler(WebTestCase):
|
||||
|
|
|
@ -1,14 +1,7 @@
|
|||
# -*- 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.testing import FileConfigTestCase
|
||||
from wuttaweb import subscribers
|
||||
from wuttaweb.menus import MenuHandler
|
||||
|
||||
|
||||
|
@ -39,56 +32,6 @@ class DataTestCase(FileConfigTestCase):
|
|||
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):
|
||||
"""
|
||||
Dummy menu handler for testing.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
|
||||
from tests.util import WebTestCase
|
||||
from wuttaweb.testing import WebTestCase
|
||||
|
||||
|
||||
class TestIncludeMe(WebTestCase):
|
||||
|
|
|
@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch
|
|||
from pyramid.httpexceptions import HTTPFound, HTTPForbidden
|
||||
|
||||
from wuttaweb.views import auth as mod
|
||||
from tests.util import WebTestCase
|
||||
from wuttaweb.testing import WebTestCase
|
||||
|
||||
|
||||
class TestAuthView(WebTestCase):
|
||||
|
|
|
@ -5,7 +5,7 @@ from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPNotFound
|
|||
from wuttaweb.views import base as mod
|
||||
from wuttaweb.forms import Form
|
||||
from wuttaweb.grids import Grid, GridAction
|
||||
from tests.util import WebTestCase
|
||||
from wuttaweb.testing import WebTestCase
|
||||
|
||||
|
||||
class TestView(WebTestCase):
|
||||
|
|
|
@ -10,7 +10,7 @@ from wuttjamaican.db import model
|
|||
from wuttjamaican.batch import BatchHandler
|
||||
from wuttaweb.views import MasterView, batch as mod
|
||||
from wuttaweb.progress import SessionProgress
|
||||
from tests.util import WebTestCase
|
||||
from wuttaweb.testing import WebTestCase
|
||||
|
||||
|
||||
class MockBatch(model.BatchMixin, model.Base):
|
||||
|
@ -367,6 +367,12 @@ class TestBatchMasterView(WebTestCase):
|
|||
self.assertIn('sequence', grid.labels)
|
||||
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):
|
||||
# nb. coverage only
|
||||
with patch.object(mod.BatchMasterView, 'model_class', new=MockBatch, create=True):
|
||||
|
|
|
@ -5,7 +5,7 @@ from unittest.mock import patch
|
|||
import colander
|
||||
|
||||
from wuttaweb.views import common as mod
|
||||
from tests.util import WebTestCase
|
||||
from wuttaweb.testing import WebTestCase
|
||||
|
||||
|
||||
class TestCommonView(WebTestCase):
|
||||
|
|
|
@ -9,7 +9,7 @@ from pyramid.httpexceptions import HTTPNotFound
|
|||
from pyramid.response import Response
|
||||
|
||||
from wuttaweb.views import email as mod
|
||||
from tests.util import WebTestCase
|
||||
from wuttaweb.testing import WebTestCase
|
||||
|
||||
|
||||
class TestEmailSettingViews(WebTestCase):
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
|
||||
from wuttaweb.views import essential as mod
|
||||
from tests.util import WebTestCase
|
||||
from wuttaweb.testing import WebTestCase
|
||||
|
||||
|
||||
class TestEssentialViews(WebTestCase):
|
||||
|
|
|
@ -16,7 +16,7 @@ from wuttaweb.views import master as mod
|
|||
from wuttaweb.views import View
|
||||
from wuttaweb.progress import SessionProgress
|
||||
from wuttaweb.subscribers import new_request_set_user
|
||||
from tests.util import WebTestCase
|
||||
from wuttaweb.testing import WebTestCase
|
||||
|
||||
|
||||
class TestMasterView(WebTestCase):
|
||||
|
@ -441,6 +441,12 @@ class TestMasterView(WebTestCase):
|
|||
self.assertIn('click me', 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):
|
||||
|
||||
# basic
|
||||
|
@ -1649,6 +1655,18 @@ class TestMasterView(WebTestCase):
|
|||
self.assertIsNone(grid.model_class)
|
||||
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):
|
||||
view = self.make_view()
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ from sqlalchemy import orm
|
|||
from pyramid.httpexceptions import HTTPNotFound
|
||||
|
||||
from wuttaweb.views import people
|
||||
from tests.util import WebTestCase
|
||||
from wuttaweb.testing import WebTestCase
|
||||
|
||||
|
||||
class TestPersonView(WebTestCase):
|
||||
|
|
|
@ -4,7 +4,7 @@ from pyramid import testing
|
|||
|
||||
from wuttaweb.views import progress as mod
|
||||
from wuttaweb.progress import get_progress_session
|
||||
from tests.util import WebTestCase
|
||||
from wuttaweb.testing import WebTestCase
|
||||
|
||||
|
||||
class TestProgressView(WebTestCase):
|
||||
|
|
|
@ -8,7 +8,7 @@ import colander
|
|||
|
||||
from wuttaweb.views import roles as mod
|
||||
from wuttaweb.forms.schema import RoleRef
|
||||
from tests.util import WebTestCase
|
||||
from wuttaweb.testing import WebTestCase
|
||||
|
||||
|
||||
class TestRoleView(WebTestCase):
|
||||
|
|
|
@ -6,7 +6,7 @@ import colander
|
|||
from pyramid.httpexceptions import HTTPNotFound
|
||||
|
||||
from wuttaweb.views import settings as mod
|
||||
from tests.util import WebTestCase
|
||||
from wuttaweb.testing import WebTestCase
|
||||
|
||||
|
||||
class TestAppInfoView(WebTestCase):
|
||||
|
|
|
@ -8,7 +8,7 @@ from unittest.mock import patch, MagicMock
|
|||
from wuttaweb.views import upgrades as mod
|
||||
from wuttjamaican.exc import ConfigurationError
|
||||
from wuttaweb.progress import get_progress_session
|
||||
from tests.util import WebTestCase
|
||||
from wuttaweb.testing import WebTestCase
|
||||
|
||||
|
||||
class TestUpgradeView(WebTestCase):
|
||||
|
|
|
@ -7,7 +7,7 @@ from sqlalchemy import orm
|
|||
import colander
|
||||
|
||||
from wuttaweb.views import users as mod
|
||||
from tests.util import WebTestCase
|
||||
from wuttaweb.testing import WebTestCase
|
||||
|
||||
|
||||
class TestUserView(WebTestCase):
|
||||
|
|
Loading…
Reference in a new issue