Compare commits
4 commits
f5891d36fa
...
01aa08b33d
Author | SHA1 | Date | |
---|---|---|---|
![]() |
01aa08b33d | ||
![]() |
7766ca6b12 | ||
![]() |
9a739381ae | ||
![]() |
9ac4f7525e |
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -5,6 +5,18 @@ 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
|
||||||
|
|
|
@ -23,3 +23,5 @@
|
||||||
views.base
|
views.base
|
||||||
views.common
|
views.common
|
||||||
views.essential
|
views.essential
|
||||||
|
views.master
|
||||||
|
views.settings
|
||||||
|
|
6
docs/api/wuttaweb/views.master.rst
Normal file
6
docs/api/wuttaweb/views.master.rst
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
``wuttaweb.views.master``
|
||||||
|
=========================
|
||||||
|
|
||||||
|
.. automodule:: wuttaweb.views.master
|
||||||
|
:members:
|
6
docs/api/wuttaweb/views.settings.rst
Normal file
6
docs/api/wuttaweb/views.settings.rst
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
``wuttaweb.views.settings``
|
||||||
|
===========================
|
||||||
|
|
||||||
|
.. automodule:: wuttaweb.views.settings
|
||||||
|
:members:
|
|
@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "WuttaWeb"
|
name = "WuttaWeb"
|
||||||
version = "0.3.0"
|
version = "0.4.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"}]
|
||||||
|
|
|
@ -118,8 +118,9 @@ class MenuHandler(GenericHandler):
|
||||||
'type': 'menu',
|
'type': 'menu',
|
||||||
'items': [
|
'items': [
|
||||||
{
|
{
|
||||||
'title': "TODO!",
|
'title': "App Info",
|
||||||
'url': '#',
|
'route': 'appinfo',
|
||||||
|
'perm': 'appinfo.list',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
|
@ -249,6 +249,7 @@ 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
|
||||||
|
|
56
src/wuttaweb/templates/appinfo/index.mako
Normal file
56
src/wuttaweb/templates/appinfo/index.mako
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
## -*- 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()}
|
13
src/wuttaweb/templates/master/index.mako
Normal file
13
src/wuttaweb/templates/master/index.mako
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
## -*- 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()}
|
|
@ -27,9 +27,11 @@ 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):
|
||||||
|
|
|
@ -73,6 +73,14 @@ 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.
|
||||||
|
|
|
@ -53,7 +53,10 @@ class CommonView(View):
|
||||||
@classmethod
|
@classmethod
|
||||||
def _defaults(cls, config):
|
def _defaults(cls, config):
|
||||||
|
|
||||||
# home
|
# auto-correct URLs which require trailing slash
|
||||||
|
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',
|
||||||
|
|
|
@ -39,6 +39,7 @@ 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):
|
||||||
|
|
443
src/wuttaweb/views/master.py
Normal file
443
src/wuttaweb/views/master.py
Normal file
|
@ -0,0 +1,443 @@
|
||||||
|
# -*- 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)
|
47
src/wuttaweb/views/settings.py
Normal file
47
src/wuttaweb/views/settings.py
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
# -*- 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)
|
|
@ -46,8 +46,10 @@ class TestFieldList(TestCase):
|
||||||
class TestForm(TestCase):
|
class TestForm(TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.config = WuttaConfig()
|
self.config = WuttaConfig(defaults={
|
||||||
self.request = testing.DummyRequest(wutta_config=self.config)
|
'wutta.web.menus.handler_spec': 'tests.utils:NullMenuHandler',
|
||||||
|
})
|
||||||
|
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'],
|
||||||
|
|
|
@ -214,10 +214,12 @@ class TestNewRequestSetUser(TestCase):
|
||||||
class TestBeforeRender(TestCase):
|
class TestBeforeRender(TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.config = WuttaConfig()
|
self.config = WuttaConfig(defaults={
|
||||||
|
'wutta.web.menus.handler_spec': 'tests.utils:NullMenuHandler',
|
||||||
|
})
|
||||||
|
|
||||||
def make_request(self):
|
def make_request(self):
|
||||||
request = testing.DummyRequest()
|
request = testing.DummyRequest(use_oruga=False)
|
||||||
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
|
||||||
|
|
11
tests/utils.py
Normal file
11
tests/utils.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# -*- coding: utf-8; -*-
|
||||||
|
|
||||||
|
from wuttaweb.menus import MenuHandler
|
||||||
|
|
||||||
|
|
||||||
|
class NullMenuHandler(MenuHandler):
|
||||||
|
"""
|
||||||
|
Dummy menu handler for testing.
|
||||||
|
"""
|
||||||
|
def make_menus(self, request, **kwargs):
|
||||||
|
return []
|
|
@ -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
|
from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPNotFound
|
||||||
|
|
||||||
from wuttjamaican.conf import WuttaConfig
|
from wuttjamaican.conf import WuttaConfig
|
||||||
from wuttaweb.views import base
|
from wuttaweb.views import base
|
||||||
|
@ -31,6 +31,10 @@ 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)
|
||||||
|
|
282
tests/views/test_master.py
Normal file
282
tests/views/test_master.py
Normal file
|
@ -0,0 +1,282 @@
|
||||||
|
# -*- 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
|
13
tests/views/test_settings.py
Normal file
13
tests/views/test_settings.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# -*- 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()
|
57
tests/views/utils.py
Normal file
57
tests/views/utils.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
# -*- 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()
|
Loading…
Reference in a new issue