3
0
Fork 0

Compare commits

..

No commits in common. "01aa08b33d62187e63773abc8f0aafaee7376188" and "f5891d36fa2f21133138d8a1fd5175f4421ce676" have entirely different histories.

22 changed files with 9 additions and 981 deletions

View file

@ -5,18 +5,6 @@ All notable changes to wuttaweb will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## v0.4.0 (2024-08-05)
### Feat
- add basic App Info view (index only)
- add initial `MasterView` support
### Fix
- add `notfound()` View method; auto-append trailing slash
- bump min version for wuttjamaican
## v0.3.0 (2024-08-05) ## v0.3.0 (2024-08-05)
### Feat ### Feat

View file

@ -23,5 +23,3 @@
views.base views.base
views.common views.common
views.essential views.essential
views.master
views.settings

View file

@ -1,6 +0,0 @@
``wuttaweb.views.master``
=========================
.. automodule:: wuttaweb.views.master
:members:

View file

@ -1,6 +0,0 @@
``wuttaweb.views.settings``
===========================
.. automodule:: wuttaweb.views.settings
:members:

View file

@ -6,7 +6,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "WuttaWeb" name = "WuttaWeb"
version = "0.4.0" version = "0.3.0"
description = "Web App for Wutta Framework" description = "Web App for Wutta Framework"
readme = "README.md" readme = "README.md"
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]

View file

@ -118,9 +118,8 @@ class MenuHandler(GenericHandler):
'type': 'menu', 'type': 'menu',
'items': [ 'items': [
{ {
'title': "App Info", 'title': "TODO!",
'route': 'appinfo', 'url': '#',
'perm': 'appinfo.list',
}, },
], ],
} }

View file

@ -249,7 +249,6 @@ def before_render(event):
context['h'] = helpers context['h'] = helpers
context['url'] = request.route_url context['url'] = request.route_url
context['json'] = json context['json'] = json
context['b'] = 'o' if request.use_oruga else 'b' # for buefy
# TODO: this should be avoided somehow, for non-traditional web # TODO: this should be avoided somehow, for non-traditional web
# apps, esp. "API" web apps. (in the meantime can configure the # apps, esp. "API" web apps. (in the meantime can configure the

View file

@ -1,56 +0,0 @@
## -*- coding: utf-8; -*-
<%inherit file="/master/index.mako" />
<%def name="page_content()">
<nav class="panel item-panel">
<p class="panel-heading">Application</p>
<div class="panel-block">
<div style="width: 100%;">
<b-field horizontal label="Distribution">
<span>${app.get_distribution(obj=app.get_web_handler()) or f'?? - set config for `{app.appname}.app_dist`'}</span>
</b-field>
<b-field horizontal label="Version">
<span>${app.get_version(obj=app.get_web_handler()) or f'?? - set config for `{app.appname}.app_dist`'}</span>
</b-field>
<b-field horizontal label="App Title">
<span>${app.get_title()}</span>
</b-field>
</div>
</div>
</nav>
<nav class="panel item-panel">
<p class="panel-heading">Configuration Files</p>
<div class="panel-block">
<div style="width: 100%;">
<${b}-table :data="configFiles">
<${b}-table-column field="priority"
label="Priority"
v-slot="props">
{{ props.row.priority }}
</${b}-table-column>
<${b}-table-column field="path"
label="File Path"
v-slot="props">
{{ props.row.path }}
</${b}-table-column>
</${b}-table>
</div>
</div>
</nav>
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script>
ThisPageData.configFiles = ${json.dumps([dict(path=p, priority=i) for i, p in enumerate(config.get_prioritized_files(), 1)])|n}
</script>
</%def>
${parent.body()}

View file

@ -1,13 +0,0 @@
## -*- coding: utf-8; -*-
<%inherit file="/page.mako" />
<%def name="title()">${index_title}</%def>
<%def name="content_title()"></%def>
<%def name="page_content()">
<p>TODO: index page content</p>
</%def>
${parent.body()}

View file

@ -27,11 +27,9 @@ For convenience, from this ``wuttaweb.views`` namespace you can access
the following: the following:
* :class:`~wuttaweb.views.base.View` * :class:`~wuttaweb.views.base.View`
* :class:`~wuttaweb.views.master.MasterView`
""" """
from .base import View from .base import View
from .master import MasterView
def includeme(config): def includeme(config):

View file

@ -73,14 +73,6 @@ class View:
""" """
return forms.Form(self.request, **kwargs) return forms.Form(self.request, **kwargs)
def notfound(self):
"""
Convenience method, to raise a HTTP 404 Not Found exception::
raise self.notfound()
"""
return httpexceptions.HTTPNotFound()
def redirect(self, url, **kwargs): def redirect(self, url, **kwargs):
""" """
Convenience method to return a HTTP 302 response. Convenience method to return a HTTP 302 response.

View file

@ -53,10 +53,7 @@ class CommonView(View):
@classmethod @classmethod
def _defaults(cls, config): def _defaults(cls, config):
# auto-correct URLs which require trailing slash # home
config.add_notfound_view(cls, attr='notfound', append_slash=True)
# home page
config.add_route('home', '/') config.add_route('home', '/')
config.add_view(cls, attr='home', config.add_view(cls, attr='home',
route_name='home', route_name='home',

View file

@ -39,7 +39,6 @@ def defaults(config, **kwargs):
config.include(mod('wuttaweb.views.auth')) config.include(mod('wuttaweb.views.auth'))
config.include(mod('wuttaweb.views.common')) config.include(mod('wuttaweb.views.common'))
config.include(mod('wuttaweb.views.settings'))
def includeme(config): def includeme(config):

View file

@ -1,443 +0,0 @@
# -*- coding: utf-8; -*-
################################################################################
#
# wuttaweb -- Web App for Wutta Framework
# Copyright © 2024 Lance Edgar
#
# This file is part of Wutta Framework.
#
# Wutta Framework is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Wutta Framework is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Base Logic for Master Views
"""
from pyramid.renderers import render_to_response
from wuttaweb.views import View
class MasterView(View):
"""
Base class for "master" views.
Master views typically map to a table in a DB, though not always.
They essentially are a set of CRUD views for a certain type of
data record.
Many attributes may be overridden in subclass. For instance to
define :attr:`model_class`::
from wuttaweb.views import MasterView
from wuttjamaican.db.model import Person
class MyPersonView(MasterView):
model_class = Person
def includeme(config):
MyPersonView.defaults(config)
.. note::
Many of these attributes will only exist if they have been
explicitly defined in a subclass. There are corresponding
``get_xxx()`` methods which should be used instead of accessing
these attributes directly.
.. attribute:: model_class
Optional reference to a data model class. While not strictly
required, most views will set this to a SQLAlchemy mapped
class,
e.g. :class:`wuttjamaican:wuttjamaican.db.model.auth.User`.
Code should not access this directly but instead call
:meth:`get_model_class()`.
.. attribute:: model_name
Optional override for the view's data model name,
e.g. ``'WuttaWidget'``.
Code should not access this directly but instead call
:meth:`get_model_name()`.
.. attribute:: model_name_normalized
Optional override for the view's "normalized" data model name,
e.g. ``'wutta_widget'``.
Code should not access this directly but instead call
:meth:`get_model_name_normalized()`.
.. attribute:: model_title
Optional override for the view's "humanized" (singular) model
title, e.g. ``"Wutta Widget"``.
Code should not access this directly but instead call
:meth:`get_model_title()`.
.. attribute:: model_title_plural
Optional override for the view's "humanized" (plural) model
title, e.g. ``"Wutta Widgets"``.
Code should not access this directly but instead call
:meth:`get_model_title_plural()`.
.. attribute:: route_prefix
Optional override for the view's route prefix,
e.g. ``'wutta_widgets'``.
Code should not access this directly but instead call
:meth:`get_route_prefix()`.
.. attribute:: url_prefix
Optional override for the view's URL prefix,
e.g. ``'/widgets'``.
Code should not access this directly but instead call
:meth:`get_url_prefix()`.
.. attribute:: template_prefix
Optional override for the view's template prefix,
e.g. ``'/widgets'``.
Code should not access this directly but instead call
:meth:`get_template_prefix()`.
.. attribute:: listable
Boolean indicating whether the view model supports "listing" -
i.e. it should have an :meth:`index()` view.
"""
##############################
# attributes
##############################
listable = True
##############################
# view methods
##############################
def index(self):
"""
View to "list" (filter/browse) the model data.
This is the "default" view for the model and is what user sees
when visiting the "root" path under the :attr:`url_prefix`,
e.g. ``/widgets/``.
"""
return self.render_to_response('index', {})
##############################
# support methods
##############################
def get_index_title(self):
"""
Returns the main index title for the master view.
By default this returns the value from
:meth:`get_model_title_plural()`. Subclass may override as
needed.
"""
return self.get_model_title_plural()
def render_to_response(self, template, context):
"""
Locate and render an appropriate template, with the given
context, and return a :term:`response`.
The specified ``template`` should be only the "base name" for
the template - e.g. ``'index'`` or ``'edit'``. This method
will then try to locate a suitable template file, based on
values from :meth:`get_template_prefix()` and
:meth:`get_fallback_templates()`.
In practice this *usually* means two different template paths
will be attempted, e.g. if ``template`` is ``'edit'`` and
:attr:`template_prefix` is ``'/widgets'``:
* ``/widgets/edit.mako``
* ``/master/edit.mako``
The first template found to exist will be used for rendering.
It then calls
:func:`pyramid:pyramid.renderers.render_to_response()` and
returns the result.
:param template: Base name for the template.
:param context: Data dict to be used as template context.
:returns: Response object containing the rendered template.
"""
defaults = {
'index_title': self.get_index_title(),
}
# merge defaults + caller-provided context
defaults.update(context)
context = defaults
# first try the template path most specific to this view
template_prefix = self.get_template_prefix()
mako_path = f'{template_prefix}/{template}.mako'
try:
return render_to_response(mako_path, context, request=self.request)
except IOError:
# failing that, try one or more fallback templates
for fallback in self.get_fallback_templates(template):
try:
return render_to_response(fallback, context, request=self.request)
except IOError:
pass
# if we made it all the way here, then we found no
# templates at all, in which case re-attempt the first and
# let that error raise on up
return render_to_response(mako_path, context, request=self.request)
def get_fallback_templates(self, template):
"""
Returns a list of "fallback" template paths which may be
attempted for rendering a view. This is used within
:meth:`render_to_response()` if the "first guess" template
file was not found.
:param template: Base name for a template (without prefix), e.g.
``'custom'``.
:returns: List of full template paths to be tried, based on
the specified template. For instance if ``template`` is
``'custom'`` this will (by default) return::
['/master/custom.mako']
"""
return [f'/master/{template}.mako']
##############################
# class methods
##############################
@classmethod
def get_model_class(cls):
"""
Returns the model class for the view (if defined).
A model class will *usually* be a SQLAlchemy mapped class,
e.g. :class:`wuttjamaican:wuttjamaican.db.model.base.Person`.
There is no default value here, but a subclass may override by
assigning :attr:`model_class`.
Note that the model class is not *required* - however if you
do not set the :attr:`model_class`, then you *must* set the
:attr:`model_name`.
"""
if hasattr(cls, 'model_class'):
return cls.model_class
@classmethod
def get_model_name(cls):
"""
Returns the model name for the view.
A model name should generally be in the format of a Python
class name, e.g. ``'WuttaWidget'``. (Note this is
*singular*, not plural.)
The default logic will call :meth:`get_model_class()` and
return that class name as-is. A subclass may override by
assigning :attr:`model_name`.
"""
if hasattr(cls, 'model_name'):
return cls.model_name
return cls.get_model_class().__name__
@classmethod
def get_model_name_normalized(cls):
"""
Returns the "normalized" model name for the view.
A normalized model name should generally be in the format of a
Python variable name, e.g. ``'wutta_widget'``. (Note this is
*singular*, not plural.)
The default logic will call :meth:`get_model_name()` and
simply lower-case the result. A subclass may override by
assigning :attr:`model_name_normalized`.
"""
if hasattr(cls, 'model_name_normalized'):
return cls.model_name_normalized
return cls.get_model_name().lower()
@classmethod
def get_model_title(cls):
"""
Returns the "humanized" (singular) model title for the view.
The model title will be displayed to the user, so should have
proper grammar and capitalization, e.g. ``"Wutta Widget"``.
(Note this is *singular*, not plural.)
The default logic will call :meth:`get_model_name()` and use
the result as-is. A subclass may override by assigning
:attr:`model_title`.
"""
if hasattr(cls, 'model_title'):
return cls.model_title
return cls.get_model_name()
@classmethod
def get_model_title_plural(cls):
"""
Returns the "humanized" (plural) model title for the view.
The model title will be displayed to the user, so should have
proper grammar and capitalization, e.g. ``"Wutta Widgets"``.
(Note this is *plural*, not singular.)
The default logic will call :meth:`get_model_title()` and
simply add a ``'s'`` to the end. A subclass may override by
assigning :attr:`model_title_plural`.
"""
if hasattr(cls, 'model_title_plural'):
return cls.model_title_plural
model_title = cls.get_model_title()
return f"{model_title}s"
@classmethod
def get_route_prefix(cls):
"""
Returns the "route prefix" for the master view. This prefix
is used for all named routes defined by the view class.
For instance if route prefix is ``'widgets'`` then a view
might have these routes:
* ``'widgets'``
* ``'widgets.create'``
* ``'widgets.edit'``
* ``'widgets.delete'``
The default logic will call
:meth:`get_model_name_normalized()` and simply add an ``'s'``
to the end, making it plural. A subclass may override by
assigning :attr:`route_prefix`.
"""
if hasattr(cls, 'route_prefix'):
return cls.route_prefix
model_name = cls.get_model_name_normalized()
return f'{model_name}s'
@classmethod
def get_url_prefix(cls):
"""
Returns the "URL prefix" for the master view. This prefix is
used for all URLs defined by the view class.
Using the same example as in :meth:`get_route_prefix()`, the
URL prefix would be ``'/widgets'`` and the view would have
defined routes for these URLs:
* ``/widgets/``
* ``/widgets/new``
* ``/widgets/XXX/edit``
* ``/widgets/XXX/delete``
The default logic will call :meth:`get_route_prefix()` and
simply add a ``'/'`` to the beginning. A subclass may
override by assigning :attr:`url_prefix`.
"""
if hasattr(cls, 'url_prefix'):
return cls.url_prefix
route_prefix = cls.get_route_prefix()
return f'/{route_prefix}'
@classmethod
def get_template_prefix(cls):
"""
Returns the "template prefix" for the master view. This
prefix is used to guess which template path to render for a
given view.
Using the same example as in :meth:`get_url_prefix()`, the
template prefix would also be ``'/widgets'`` and the templates
assumed for those routes would be:
* ``/widgets/index.mako``
* ``/widgets/create.mako``
* ``/widgets/edit.mako``
* ``/widgets/delete.mako``
The default logic will call :meth:`get_url_prefix()` and
return that value as-is. A subclass may override by assigning
:attr:`template_prefix`.
"""
if hasattr(cls, 'template_prefix'):
return cls.template_prefix
return cls.get_url_prefix()
##############################
# configuration
##############################
@classmethod
def defaults(cls, config):
"""
Provide default Pyramid configuration for a master view.
This is generally called from within the module's
``includeme()`` function, e.g.::
from wuttaweb.views import MasterView
class WidgetView(MasterView):
model_name = 'Widget'
def includeme(config):
WidgetView.defaults(config)
:param config: Reference to the app's
:class:`pyramid:pyramid.config.Configurator` instance.
"""
cls._defaults(config)
@classmethod
def _defaults(cls, config):
route_prefix = cls.get_route_prefix()
url_prefix = cls.get_url_prefix()
# index view
if cls.listable:
config.add_route(route_prefix, f'{url_prefix}/')
config.add_view(cls, attr='index',
route_name=route_prefix)

View file

@ -1,47 +0,0 @@
# -*- coding: utf-8; -*-
################################################################################
#
# wuttaweb -- Web App for Wutta Framework
# Copyright © 2024 Lance Edgar
#
# This file is part of Wutta Framework.
#
# Wutta Framework is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Wutta Framework is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Views for app settings
"""
from wuttaweb.views import MasterView
class AppInfoView(MasterView):
"""
Master view for the overall app, to show/edit config etc.
"""
model_name = 'AppInfo'
model_title_plural = "App Info"
route_prefix = 'appinfo'
def defaults(config, **kwargs):
base = globals()
AppInfoView = kwargs.get('AppInfoView', base['AppInfoView'])
AppInfoView.defaults(config)
def includeme(config):
defaults(config)

View file

@ -46,10 +46,8 @@ class TestFieldList(TestCase):
class TestForm(TestCase): class TestForm(TestCase):
def setUp(self): def setUp(self):
self.config = WuttaConfig(defaults={ self.config = WuttaConfig()
'wutta.web.menus.handler_spec': 'tests.utils:NullMenuHandler', self.request = testing.DummyRequest(wutta_config=self.config)
})
self.request = testing.DummyRequest(wutta_config=self.config, use_oruga=False)
self.pyramid_config = testing.setUp(request=self.request, settings={ self.pyramid_config = testing.setUp(request=self.request, settings={
'mako.directories': ['wuttaweb:templates'], 'mako.directories': ['wuttaweb:templates'],

View file

@ -214,12 +214,10 @@ class TestNewRequestSetUser(TestCase):
class TestBeforeRender(TestCase): class TestBeforeRender(TestCase):
def setUp(self): def setUp(self):
self.config = WuttaConfig(defaults={ self.config = WuttaConfig()
'wutta.web.menus.handler_spec': 'tests.utils:NullMenuHandler',
})
def make_request(self): def make_request(self):
request = testing.DummyRequest(use_oruga=False) request = testing.DummyRequest()
request.registry.settings = {'wutta_config': self.config} request.registry.settings = {'wutta_config': self.config}
request.wutta_config = self.config request.wutta_config = self.config
return request return request

View file

@ -1,11 +0,0 @@
# -*- coding: utf-8; -*-
from wuttaweb.menus import MenuHandler
class NullMenuHandler(MenuHandler):
"""
Dummy menu handler for testing.
"""
def make_menus(self, request, **kwargs):
return []

View file

@ -3,7 +3,7 @@
from unittest import TestCase from unittest import TestCase
from pyramid import testing from pyramid import testing
from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPNotFound from pyramid.httpexceptions import HTTPFound, HTTPForbidden
from wuttjamaican.conf import WuttaConfig from wuttjamaican.conf import WuttaConfig
from wuttaweb.views import base from wuttaweb.views import base
@ -31,10 +31,6 @@ class TestView(TestCase):
form = self.view.make_form() form = self.view.make_form()
self.assertIsInstance(form, Form) self.assertIsInstance(form, Form)
def test_notfound(self):
error = self.view.notfound()
self.assertIsInstance(error, HTTPNotFound)
def test_redirect(self): def test_redirect(self):
error = self.view.redirect('/') error = self.view.redirect('/')
self.assertIsInstance(error, HTTPFound) self.assertIsInstance(error, HTTPFound)

View file

@ -1,282 +0,0 @@
# -*- coding: utf-8; -*-
from unittest import TestCase
from unittest.mock import MagicMock
from pyramid import testing
from pyramid.response import Response
from wuttjamaican.conf import WuttaConfig
from wuttaweb.views import master
from wuttaweb.subscribers import new_request_set_user
class TestMasterView(TestCase):
def setUp(self):
self.config = WuttaConfig(defaults={
'wutta.web.menus.handler_spec': 'tests.utils:NullMenuHandler',
})
self.app = self.config.get_app()
self.request = testing.DummyRequest(wutta_config=self.config, use_oruga=False)
self.pyramid_config = testing.setUp(request=self.request, settings={
'wutta_config': self.config,
'mako.directories': ['wuttaweb:templates'],
})
self.pyramid_config.include('pyramid_mako')
self.pyramid_config.include('wuttaweb.static')
self.pyramid_config.include('wuttaweb.views.essential')
self.pyramid_config.add_subscriber('wuttaweb.subscribers.before_render',
'pyramid.events.BeforeRender')
event = MagicMock(request=self.request)
new_request_set_user(event)
def tearDown(self):
testing.tearDown()
def test_defaults(self):
master.MasterView.model_name = 'Widget'
# TODO: should inspect pyramid routes after this, to be certain
master.MasterView.defaults(self.pyramid_config)
del master.MasterView.model_name
##############################
# class methods
##############################
def test_get_model_class(self):
# no model class by default
self.assertIsNone(master.MasterView.get_model_class())
# subclass may specify
MyModel = MagicMock()
master.MasterView.model_class = MyModel
self.assertIs(master.MasterView.get_model_class(), MyModel)
del master.MasterView.model_class
def test_get_model_name(self):
# error by default (since no model class)
self.assertRaises(AttributeError, master.MasterView.get_model_name)
# subclass may specify model name
master.MasterView.model_name = 'Widget'
self.assertEqual(master.MasterView.get_model_name(), 'Widget')
del master.MasterView.model_name
# or it may specify model class
MyModel = MagicMock(__name__='Blaster')
master.MasterView.model_class = MyModel
self.assertEqual(master.MasterView.get_model_name(), 'Blaster')
del master.MasterView.model_class
def test_get_model_name_normalized(self):
# error by default (since no model class)
self.assertRaises(AttributeError, master.MasterView.get_model_name_normalized)
# subclass may specify *normalized* model name
master.MasterView.model_name_normalized = 'widget'
self.assertEqual(master.MasterView.get_model_name_normalized(), 'widget')
del master.MasterView.model_name_normalized
# or it may specify *standard* model name
master.MasterView.model_name = 'Blaster'
self.assertEqual(master.MasterView.get_model_name_normalized(), 'blaster')
del master.MasterView.model_name
# or it may specify model class
MyModel = MagicMock(__name__='Dinosaur')
master.MasterView.model_class = MyModel
self.assertEqual(master.MasterView.get_model_name_normalized(), 'dinosaur')
del master.MasterView.model_class
def test_get_model_title(self):
# error by default (since no model class)
self.assertRaises(AttributeError, master.MasterView.get_model_title)
# subclass may specify model title
master.MasterView.model_title = 'Wutta Widget'
self.assertEqual(master.MasterView.get_model_title(), "Wutta Widget")
del master.MasterView.model_title
# or it may specify model name
master.MasterView.model_name = 'Blaster'
self.assertEqual(master.MasterView.get_model_title(), "Blaster")
del master.MasterView.model_name
# or it may specify model class
MyModel = MagicMock(__name__='Dinosaur')
master.MasterView.model_class = MyModel
self.assertEqual(master.MasterView.get_model_title(), "Dinosaur")
del master.MasterView.model_class
def test_get_model_title_plural(self):
# error by default (since no model class)
self.assertRaises(AttributeError, master.MasterView.get_model_title_plural)
# subclass may specify *plural* model title
master.MasterView.model_title_plural = 'People'
self.assertEqual(master.MasterView.get_model_title_plural(), "People")
del master.MasterView.model_title_plural
# or it may specify *singular* model title
master.MasterView.model_title = 'Wutta Widget'
self.assertEqual(master.MasterView.get_model_title_plural(), "Wutta Widgets")
del master.MasterView.model_title
# or it may specify model name
master.MasterView.model_name = 'Blaster'
self.assertEqual(master.MasterView.get_model_title_plural(), "Blasters")
del master.MasterView.model_name
# or it may specify model class
MyModel = MagicMock(__name__='Dinosaur')
master.MasterView.model_class = MyModel
self.assertEqual(master.MasterView.get_model_title_plural(), "Dinosaurs")
del master.MasterView.model_class
def test_get_route_prefix(self):
# error by default (since no model class)
self.assertRaises(AttributeError, master.MasterView.get_route_prefix)
# subclass may specify route prefix
master.MasterView.route_prefix = 'widgets'
self.assertEqual(master.MasterView.get_route_prefix(), 'widgets')
del master.MasterView.route_prefix
# subclass may specify *normalized* model name
master.MasterView.model_name_normalized = 'blaster'
self.assertEqual(master.MasterView.get_route_prefix(), 'blasters')
del master.MasterView.model_name_normalized
# or it may specify *standard* model name
master.MasterView.model_name = 'Dinosaur'
self.assertEqual(master.MasterView.get_route_prefix(), 'dinosaurs')
del master.MasterView.model_name
# or it may specify model class
MyModel = MagicMock(__name__='Truck')
master.MasterView.model_class = MyModel
self.assertEqual(master.MasterView.get_route_prefix(), 'trucks')
del master.MasterView.model_class
def test_get_url_prefix(self):
# error by default (since no model class)
self.assertRaises(AttributeError, master.MasterView.get_url_prefix)
# subclass may specify url prefix
master.MasterView.url_prefix = '/widgets'
self.assertEqual(master.MasterView.get_url_prefix(), '/widgets')
del master.MasterView.url_prefix
# or it may specify route prefix
master.MasterView.route_prefix = 'trucks'
self.assertEqual(master.MasterView.get_url_prefix(), '/trucks')
del master.MasterView.route_prefix
# or it may specify *normalized* model name
master.MasterView.model_name_normalized = 'blaster'
self.assertEqual(master.MasterView.get_url_prefix(), '/blasters')
del master.MasterView.model_name_normalized
# or it may specify *standard* model name
master.MasterView.model_name = 'Dinosaur'
self.assertEqual(master.MasterView.get_url_prefix(), '/dinosaurs')
del master.MasterView.model_name
# or it may specify model class
MyModel = MagicMock(__name__='Machine')
master.MasterView.model_class = MyModel
self.assertEqual(master.MasterView.get_url_prefix(), '/machines')
del master.MasterView.model_class
def test_get_template_prefix(self):
# error by default (since no model class)
self.assertRaises(AttributeError, master.MasterView.get_template_prefix)
# subclass may specify template prefix
master.MasterView.template_prefix = '/widgets'
self.assertEqual(master.MasterView.get_template_prefix(), '/widgets')
del master.MasterView.template_prefix
# or it may specify url prefix
master.MasterView.url_prefix = '/trees'
self.assertEqual(master.MasterView.get_template_prefix(), '/trees')
del master.MasterView.url_prefix
# or it may specify route prefix
master.MasterView.route_prefix = 'trucks'
self.assertEqual(master.MasterView.get_template_prefix(), '/trucks')
del master.MasterView.route_prefix
# or it may specify *normalized* model name
master.MasterView.model_name_normalized = 'blaster'
self.assertEqual(master.MasterView.get_template_prefix(), '/blasters')
del master.MasterView.model_name_normalized
# or it may specify *standard* model name
master.MasterView.model_name = 'Dinosaur'
self.assertEqual(master.MasterView.get_template_prefix(), '/dinosaurs')
del master.MasterView.model_name
# or it may specify model class
MyModel = MagicMock(__name__='Machine')
master.MasterView.model_class = MyModel
self.assertEqual(master.MasterView.get_template_prefix(), '/machines')
del master.MasterView.model_class
##############################
# support methods
##############################
def test_get_index_title(self):
master.MasterView.model_title_plural = "Wutta Widgets"
view = master.MasterView(self.request)
self.assertEqual(view.get_index_title(), "Wutta Widgets")
del master.MasterView.model_title_plural
def test_render_to_response(self):
# basic sanity check using /master/index.mako
# (nb. it skips /widgets/index.mako since that doesn't exist)
master.MasterView.model_name = 'Widget'
view = master.MasterView(self.request)
response = view.render_to_response('index', {})
self.assertIsInstance(response, Response)
del master.MasterView.model_name
# basic sanity check using /appinfo/index.mako
master.MasterView.model_name = 'AppInfo'
master.MasterView.template_prefix = '/appinfo'
view = master.MasterView(self.request)
response = view.render_to_response('index', {})
self.assertIsInstance(response, Response)
del master.MasterView.model_name
del master.MasterView.template_prefix
# bad template name causes error
master.MasterView.model_name = 'Widget'
self.assertRaises(IOError, view.render_to_response, 'nonexistent', {})
del master.MasterView.model_name
##############################
# view methods
##############################
def test_index(self):
# basic sanity check using /appinfo
master.MasterView.model_name = 'AppInfo'
master.MasterView.template_prefix = '/appinfo'
view = master.MasterView(self.request)
response = view.index()
del master.MasterView.model_name
del master.MasterView.template_prefix

View file

@ -1,13 +0,0 @@
# -*- coding: utf-8; -*-
from tests.views.utils import WebTestCase
from wuttaweb.views import settings
class TestAppInfoView(WebTestCase):
def test_index(self):
# just a sanity check
view = settings.AppInfoView(self.request)
response = view.index()

View file

@ -1,57 +0,0 @@
# -*- coding: utf-8; -*-
from unittest import TestCase
from unittest.mock import MagicMock
from pyramid import testing
from wuttjamaican.conf import WuttaConfig
from wuttaweb import subscribers
class WebTestCase(TestCase):
"""
Base class for test suites requiring a full (typical) web app.
"""
def setUp(self):
self.setup_web()
def setup_web(self):
self.config = WuttaConfig(defaults={
'wutta.db.default.url': 'sqlite://',
'wutta.web.menus.handler_spec': 'tests.utils:NullMenuHandler',
})
self.request = testing.DummyRequest()
self.pyramid_config = testing.setUp(request=self.request, settings={
'wutta_config': self.config,
'mako.directories': ['wuttaweb:templates'],
})
# init db
self.app = self.config.get_app()
model = self.app.model
model.Base.metadata.create_all(bind=self.config.appdb_engine)
self.session = self.app.make_session()
# init web
self.pyramid_config.include('pyramid_mako')
self.pyramid_config.include('wuttaweb.static')
self.pyramid_config.include('wuttaweb.views.essential')
self.pyramid_config.add_subscriber('wuttaweb.subscribers.before_render',
'pyramid.events.BeforeRender')
# 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()