3
0
Fork 0

Compare commits

...

11 commits

Author SHA1 Message Date
Lance Edgar 49b13306c4 bump: version 0.19.0 → 0.19.1 2025-01-06 17:00:27 -06:00
Lance Edgar b3f1f8b6d9 fix: improve built-in grid renderer logic
- add `render_batch_id()`
- allow kwargs for `render_currency()`
- caller may specify built-in renderer w/ string identifier
2025-01-06 16:56:31 -06:00
Lance Edgar 2de08ad50d fix: allow session injection for ObjectRef constructor
for sake of simpler tests
2025-01-06 16:48:42 -06:00
Lance Edgar 7895ce4676 tests: move WebTestCase to wuttaweb.testing module 2025-01-06 16:47:48 -06:00
Lance Edgar 5cec585fdf fix: improve rendering for batch row status 2025-01-02 23:14:10 -06:00
Lance Edgar 86ffb5d58f fix: add basic support for row grid "view" action links
still no actual "view row" support just yet, but subclass can
implement however they like..
2025-01-02 22:52:32 -06:00
Lance Edgar 170afe650b fix: add "xref buttons" tool panel for master view
also add `url` param for `MasterView.make_button()`
2025-01-02 22:35:43 -06:00
Lance Edgar 0631b8e16b fix: add WuttaQuantity schema type, widget 2025-01-02 21:28:55 -06:00
Lance Edgar a219f3e30d fix: remove session param from some form schema, widget classes
this was originally used for injecting the test session, but i wound
up using mock instead elsewhere, so this is just for consistency
2025-01-02 21:09:31 -06:00
Lance Edgar a612bf3846 fix: add grid renderers for bool, currency, quantity
also set bool renderer by default when possible
2025-01-02 20:13:04 -06:00
Lance Edgar 08a895a07b fix: use proper bulma styles for markdown content
cf. https://bulma.io/documentation/elements/content/
2024-12-29 20:07:10 -06:00
33 changed files with 761 additions and 315 deletions

View file

@ -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

View file

@ -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",
]

View file

@ -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:

View file

@ -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`

View file

@ -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,

View file

@ -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')}

View file

@ -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
View 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()

View file

@ -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
##############################

View file

@ -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
##############################

View file

@ -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):

View file

@ -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):

View file

@ -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

View file

@ -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'])

View file

@ -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):

View file

@ -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):

View file

@ -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):

View file

@ -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):

View file

@ -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.

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8; -*-
from tests.util import WebTestCase
from wuttaweb.testing import WebTestCase
class TestIncludeMe(WebTestCase):

View file

@ -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):

View file

@ -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):

View file

@ -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):

View file

@ -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):

View file

@ -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):

View file

@ -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):

View file

@ -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()

View file

@ -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):

View file

@ -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):

View file

@ -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):

View file

@ -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):

View file

@ -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):

View file

@ -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):