3
0
Fork 0

feat: basic support for WSGI app, views, templates

also docs + tests for what we have so far
This commit is contained in:
Lance Edgar 2024-07-12 00:17:15 -05:00
commit 977c196f47
49 changed files with 2805 additions and 0 deletions

27
src/wuttaweb/__init__.py Normal file
View file

@ -0,0 +1,27 @@
# -*- 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 - base package for web app
"""
from ._version import __version__

6
src/wuttaweb/_version.py Normal file
View file

@ -0,0 +1,6 @@
# -*- coding: utf-8; -*-
from importlib.metadata import version
__version__ = version('wuttaweb')

113
src/wuttaweb/app.py Normal file
View file

@ -0,0 +1,113 @@
# -*- 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/>.
#
################################################################################
"""
Application
"""
import os
from wuttjamaican.conf import make_config
from pyramid.config import Configurator
def make_wutta_config(settings):
"""
Make a WuttaConfig object from the given settings.
Note that ``settings`` dict will (typically) correspond to the
``[app:main]`` section of your config file.
Regardless, the ``settings`` must contain a special key/value
which is needed to identify the location of the config file.
Assuming the typical scenario then, your config file should have
an entry like this:
.. code-block:: ini
[app:main]
wutta.config = %(__file__)s
The ``%(__file__)s`` is auto-replaced with the config file path,
so ultimately ``settings`` would contain something like (at
minimum)::
{'wutta.config': '/path/to/config/file'}
If this config file path cannot be discovered, an error is raised.
"""
# initialize config and embed in settings dict, to make
# available for web requests later
path = settings.get('wutta.config')
if not path or not os.path.exists(path):
raise ValueError("Please set 'wutta.config' in [app:main] "
"section of config to the path of your "
"config file. Lame, but necessary.")
wutta_config = make_config(path)
settings['wutta_config'] = wutta_config
return wutta_config
def make_pyramid_config(settings):
"""
Make and return a Pyramid config object from the given settings.
The config is initialized with certain features deemed useful for
all apps.
"""
pyramid_config = Configurator(settings=settings)
pyramid_config.include('pyramid_beaker')
pyramid_config.include('pyramid_mako')
return pyramid_config
def main(global_config, **settings):
"""
This function returns a Pyramid WSGI application.
Typically there is no need to call this function directly, but it
may be configured as the web app entry point like so:
.. code-block:: ini
[app:main]
use = egg:wuttaweb
The app returned by this function is quite minimal, so most apps
will need to define their own ``main()`` function, and use that
instead.
"""
settings.setdefault('mako.directories', ['wuttaweb:templates'])
wutta_config = make_wutta_config(settings)
pyramid_config = make_pyramid_config(settings)
pyramid_config.include('wuttaweb.static')
pyramid_config.include('wuttaweb.subscribers')
pyramid_config.include('wuttaweb.views')
return pyramid_config.make_wsgi_app()

49
src/wuttaweb/helpers.py Normal file
View file

@ -0,0 +1,49 @@
# -*- 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/>.
#
################################################################################
"""
Template Context Helpers
This module serves as a collection of various things deemed useful for
all template renderers. It is made available as simply ``h`` within
the template context.
You can access anything provided by ``h`` within a template then, for
instance:
.. code-block:: mako
${h.link_to('python', 'https://www.python.org')}
(Note that ``link_to()`` comes from ``webhelpers2.html.tags``.)
This module contains the following references:
* :func:`~wuttaweb.util.get_liburl()`
* all names from :mod:`webhelpers2:webhelpers2.html`
* all names from :mod:`webhelpers2:webhelpers2.html.tags`
"""
from webhelpers2.html import *
from webhelpers2.html.tags import *
from wuttaweb.util import get_liburl

View file

@ -0,0 +1,37 @@
# -*- 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/>.
#
################################################################################
"""
Static Assets
It is assumed that all (i.e. even custom) apps will include this
module somewhere during startup. For instance this happens within
:func:`~wuttaweb.app.main()`::
pyramid_config.include('wuttaweb.static')
This allows for certain common assets to be available for all apps.
"""
def includeme(config):
config.add_static_view('wuttaweb', 'wuttaweb:static')

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

140
src/wuttaweb/subscribers.py Normal file
View file

@ -0,0 +1,140 @@
# -*- 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/>.
#
################################################################################
"""
Event Subscribers
It is assumed that most apps will include this module somewhere during
startup. For instance this happens within
:func:`~wuttaweb.app.main()`::
pyramid_config.include('wuttaweb.subscribers')
This allows for certain common logic to be available for all apps.
However some custom apps may need to supplement or replace the event
hooks contained here, depending on the circumstance.
"""
import json
from pyramid import threadlocal
from wuttaweb import helpers
def new_request(event):
"""
Event hook called when processing a new request.
The hook is auto-registered if this module is "included" by
Pyramid config object. Or you can explicitly register it::
pyramid_config.add_subscriber('wuttaweb.subscribers.new_request',
'pyramid.events.NewRequest')
This will add some things to the request object:
.. attribute:: request.wutta_config
Reference to the app :term:`config object`.
.. attribute:: request.use_oruga
Flag indicating whether the frontend should be displayed using
Vue 3 + Oruga (if ``True``), or else Vue 2 + Buefy (if
``False``).
"""
request = event.request
config = request.registry.settings['wutta_config']
app = config.get_app()
request.wutta_config = config
def use_oruga(request):
spec = config.get('wuttaweb.oruga_detector.spec')
if spec:
func = app.load_object(spec)
return func(request)
return False
request.set_property(use_oruga, reify=True)
def before_render(event):
"""
Event hook called just before rendering a template.
The hook is auto-registered if this module is "included" by
Pyramid config object. Or you can explicitly register it::
pyramid_config.add_subscriber('wuttaweb.subscribers.before_render',
'pyramid.events.BeforeRender')
This will add some things to the template context dict. Each of
these may be used "directly" in a template then, e.g.:
.. code-block:: mako
${app.get_title()}
Here are the keys added to context dict by this hook:
.. data:: 'app'
Reference to the :term:`app handler`.
.. data:: 'config'
Reference to the app :term:`config object`.
.. data:: 'h'
Reference to the helper module, :mod:`wuttaweb.helpers`.
.. data:: 'json'
Reference to the built-in module, :mod:`python:json`.
.. data:: 'url'
Reference to the request method,
:meth:`~pyramid:pyramid.request.Request.route_url()`.
"""
request = event.get('request') or threadlocal.get_current_request()
config = request.wutta_config
app = config.get_app()
context = event
context['app'] = app
context['config'] = config
context['h'] = helpers
context['url'] = request.route_url
context['json'] = json
# TODO
context['menus'] = []
def includeme(config):
config.add_subscriber(new_request, 'pyramid.events.NewRequest')
config.add_subscriber(before_render, 'pyramid.events.BeforeRender')

View file

@ -0,0 +1,391 @@
## -*- coding: utf-8; -*-
<%namespace name="base_meta" file="/base_meta.mako" />
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<title>${base_meta.global_title()} &raquo; ${capture(self.title)|n}</title>
${base_meta.favicon()}
${self.header_core()}
${self.head_tags()}
</head>
<body>
<div id="app" style="height: 100%;">
<whole-page />
</div>
## content body from derived/child template
${self.body()}
## Vue app
${self.make_whole_page_component()}
${self.make_whole_page_app()}
</body>
</html>
<%def name="title()"></%def>
<%def name="content_title()">${self.title()}</%def>
<%def name="header_core()">
${self.core_javascript()}
${self.extra_javascript()}
${self.core_styles()}
${self.extra_styles()}
</%def>
<%def name="core_javascript()">
${self.vuejs()}
${self.buefy()}
${self.fontawesome()}
<script>
## NOTE: this code was copied from
## https://bulma.io/documentation/components/navbar/#navbar-menu
document.addEventListener('DOMContentLoaded', () => {
// Get all "navbar-burger" elements
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0)
// Add a click event on each of them
$navbarBurgers.forEach( el => {
el.addEventListener('click', () => {
// Get the target from the "data-target" attribute
const target = el.dataset.target
const $target = document.getElementById(target)
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
el.classList.toggle('is-active')
$target.classList.toggle('is-active')
})
})
})
</script>
</%def>
<%def name="vuejs()">
${h.javascript_link(h.get_liburl(request, 'vue'))}
${h.javascript_link(h.get_liburl(request, 'vue_resource'))}
</%def>
<%def name="buefy()">
${h.javascript_link(h.get_liburl(request, 'buefy'))}
</%def>
<%def name="fontawesome()">
<script defer src="${h.get_liburl(request, 'fontawesome')}"></script>
</%def>
<%def name="extra_javascript()"></%def>
<%def name="core_styles()">
${self.buefy_styles()}
<style>
/* ****************************** */
/* page */
/* ****************************** */
/* nb. helps force footer to bottom of screen */
html, body {
height: 100%;
}
% if not request.wutta_config.production():
html, .navbar, .footer {
background-image: url(${request.static_url('wuttaweb:static/img/testing.png')});
}
% endif
#current-context {
padding-left: 0.5rem;
}
h1.title {
font-size: 2rem;
font-weight: bold;
margin-bottom: 0 !important;
}
#content-title h1 {
max-width: 50%;
overflow: hidden;
padding-left: 0.5rem;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
</%def>
<%def name="buefy_styles()">
${h.stylesheet_link(h.get_liburl(request, 'buefy.css'))}
</%def>
<%def name="extra_styles()">
${base_meta.extra_styles()}
</%def>
<%def name="head_tags()"></%def>
<%def name="render_whole_page_template()">
<script type="text/x-template" id="whole-page-template">
<div id="whole-page"
style="height: 100%; display: flex; flex-direction: column; justify-content: space-between;">
<div class="header-wrapper">
<header>
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="${url('home')}">
${base_meta.header_logo()}
<div id="global-header-title">
${base_meta.global_title()}
</div>
</a>
<a role="button" class="navbar-burger" data-target="navbar-menu" aria-label="menu" aria-expanded="false">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div class="navbar-menu" id="navbar-menu">
<div class="navbar-start">
% for topitem in menus:
% if topitem['is_link']:
${h.link_to(topitem['title'], topitem['url'], target=topitem['target'], class_='navbar-item')}
% else:
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">${topitem['title']}</a>
<div class="navbar-dropdown">
% for item in topitem['items']:
% if item['is_menu']:
<% item_hash = id(item) %>
<% toggle = 'menu_{}_shown'.format(item_hash) %>
<div>
<a class="navbar-link" @click.prevent="toggleNestedMenu('${item_hash}')">
${item['title']}
</a>
</div>
% for subitem in item['items']:
% if subitem['is_sep']:
<hr class="navbar-divider" v-show="${toggle}">
% else:
${h.link_to("{}".format(subitem['title']), subitem['url'], class_='navbar-item nested', target=subitem['target'], **{'v-show': toggle})}
% endif
% endfor
% else:
% if item['is_sep']:
<hr class="navbar-divider">
% else:
${h.link_to(item['title'], item['url'], class_='navbar-item', target=item['target'])}
% endif
% endif
% endfor
</div>
</div>
% endif
% endfor
</div><!-- navbar-start -->
${self.render_navbar_end()}
</div>
</nav>
<nav class="level" style="margin: 0.5rem auto;">
<div class="level-left">
## Current Context
<div id="current-context" class="level-item">
% if index_title:
% if index_url:
<span class="header-text">
${h.link_to(index_title, index_url)}
</span>
% else:
<h1 class="title">${index_title}</h1>
% endif
% endif
</div>
</div><!-- level-left -->
</nav><!-- level -->
</header>
## Page Title
% if capture(self.content_title):
<section id="content-title"
class="has-background-primary">
<div style="display: flex; align-items: center; padding: 0.5rem;">
<h1 class="title has-text-white"
v-html="contentTitleHTML">
</h1>
<div style="flex-grow: 1; display: flex; gap: 0.5rem;">
${self.render_instance_header_title_extras()}
</div>
<div style="display: flex; gap: 0.5rem;">
${self.render_instance_header_buttons()}
</div>
</div>
</section>
% endif
</div> <!-- header-wrapper -->
<div class="content-wrapper"
style="flex-grow: 1; padding: 0.5rem;">
## Page Body
<section id="page-body" style="height: 100%;">
% if request.session.peek_flash('error'):
% for error in request.session.pop_flash('error'):
<b-notification type="is-warning">
${error}
</b-notification>
% endfor
% endif
% if request.session.peek_flash('warning'):
% for msg in request.session.pop_flash('warning'):
<b-notification type="is-warning">
${msg}
</b-notification>
% endfor
% endif
% if request.session.peek_flash():
% for msg in request.session.pop_flash():
<b-notification type="is-info">
${msg}
</b-notification>
% endfor
% endif
<div style="height: 100%;">
${self.render_this_page_component()}
</div>
</section>
</div><!-- content-wrapper -->
## Footer
<footer class="footer">
<div class="content">
${base_meta.footer()}
</div>
</footer>
</div>
</script>
</%def>
<%def name="render_this_page_component()">
<this-page @change-content-title="changeContentTitle" />
</%def>
<%def name="render_navbar_end()">
<div class="navbar-end">
${self.render_user_menu()}
</div>
</%def>
<%def name="render_user_menu()"></%def>
<%def name="render_instance_header_title_extras()"></%def>
<%def name="render_instance_header_buttons()">
${self.render_crud_header_buttons()}
${self.render_prevnext_header_buttons()}
</%def>
<%def name="render_crud_header_buttons()"></%def>
<%def name="render_prevnext_header_buttons()"></%def>
<%def name="declare_whole_page_vars()">
<script>
let WholePage = {
template: '#whole-page-template',
computed: {},
mounted() {
for (let hook of this.mountedHooks) {
hook(this)
}
},
methods: {
changeContentTitle(newTitle) {
this.contentTitleHTML = newTitle
},
toggleNestedMenu(hash) {
const key = 'menu_' + hash + '_shown'
this[key] = !this[key]
},
},
}
let WholePageData = {
contentTitleHTML: ${json.dumps(capture(self.content_title))|n},
mountedHooks: [],
}
## declare nested menu visibility toggle flags
% for topitem in menus:
% if topitem['is_menu']:
% for item in topitem['items']:
% if item['is_menu']:
WholePageData.menu_${id(item)}_shown = false
% endif
% endfor
% endif
% endfor
</script>
</%def>
<%def name="modify_whole_page_vars()"></%def>
<%def name="finalize_whole_page_vars()"></%def>
<%def name="make_whole_page_component()">
${self.render_whole_page_template()}
${self.declare_whole_page_vars()}
${self.modify_whole_page_vars()}
${self.finalize_whole_page_vars()}
<script>
WholePage.data = function() { return WholePageData }
Vue.component('whole-page', WholePage)
</script>
</%def>
<%def name="make_whole_page_app()">
<script>
new Vue({
el: '#app'
})
</script>
</%def>

View file

@ -0,0 +1,21 @@
## -*- coding: utf-8; -*-
<%def name="app_title()">${app.get_title()}</%def>
<%def name="global_title()">${self.app_title()}</%def>
<%def name="extra_styles()"></%def>
<%def name="favicon()">
## <link rel="icon" type="image/x-icon" href="${config.get('tailbone', 'favicon_url', default=request.static_url('wuttaweb:static/img/favicon.ico'))}" />
</%def>
<%def name="header_logo()">
## ${h.image(config.get('wuttaweb.header_image_url', default=request.static_url('wuttaweb:static/img/logo.png')), "Header Logo", style="height: 49px;")}
</%def>
<%def name="footer()">
<p class="has-text-centered">
powered by ${h.link_to("Wutta Framework", 'https://pypi.org/project/WuttJamaican/', target='_blank')}
</p>
</%def>

View file

@ -0,0 +1,21 @@
## -*- coding: utf-8; -*-
<%inherit file="/page.mako" />
<%namespace name="base_meta" file="/base_meta.mako" />
<%def name="title()">Home</%def>
<%def name="render_this_page()">
${self.page_content()}
</%def>
<%def name="page_content()">
<div style="height: 100%; display: flex; align-items: center; justify-content: center;">
<div class="logo">
## ${h.image(image_url, "{} logo".format(capture(base_meta.app_title)))}
<h1 class="is-size-1">Welcome to ${base_meta.app_title()}</h1>
</div>
</div>
</%def>
${parent.body()}

View file

@ -0,0 +1,81 @@
## -*- coding: utf-8; -*-
<%inherit file="/base.mako" />
<%def name="context_menu_items()">
% if context_menu_list_items is not Undefined:
% for item in context_menu_list_items:
<li>${item}</li>
% endfor
% endif
</%def>
<%def name="page_content()"></%def>
<%def name="render_this_page()">
<div style="display: flex;">
<div class="this-page-content" style="flex-grow: 1;">
${self.page_content()}
</div>
<ul id="context-menu">
${self.context_menu_items()}
</ul>
</div>
</%def>
<%def name="render_this_page_template()">
<script type="text/x-template" id="this-page-template">
<div style="height: 100%;">
${self.render_this_page()}
</div>
</script>
</%def>
<%def name="declare_this_page_vars()">
<script type="text/javascript">
let ThisPage = {
template: '#this-page-template',
props: {
configureFieldsHelp: Boolean,
},
computed: {},
watch: {},
methods: {
changeContentTitle(newTitle) {
this.$emit('change-content-title', newTitle)
},
},
}
let ThisPageData = {
}
</script>
</%def>
<%def name="modify_this_page_vars()"></%def>
<%def name="finalize_this_page_vars()"></%def>
<%def name="make_this_page_component()">
${self.declare_this_page_vars()}
${self.modify_this_page_vars()}
${self.finalize_this_page_vars()}
<script type="text/javascript">
ThisPage.data = function() { return ThisPageData }
Vue.component('this-page', ThisPage)
## <% request.register_component('this-page', 'ThisPage') %>
</script>
</%def>
${self.render_this_page_template()}
${self.make_this_page_component()}

238
src/wuttaweb/util.py Normal file
View file

@ -0,0 +1,238 @@
# -*- 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/>.
#
################################################################################
"""
Utilities
"""
import importlib
def get_libver(
request,
key,
default_only=False,
prefix='wuttaweb',
):
"""
Return the appropriate version string for the web resource library
identified by ``key``.
WuttaWeb makes certain assumptions about which libraries would be
used on the frontend, and which versions for each would be used by
default. But it should also be possible to customize which
versions are used, hence this function.
Each library has a built-in default version but your config can
override them, e.g.:
.. code-block:: ini
[wuttaweb]
libver.bb_vue = 3.4.29
:param request: Current request.
:param key: Unique key for the library, as string. Possibilities
are the same as for :func:`get_liburl()`.
:param default_only: If this flag is ``True``, the logic will
*not* look for a "configured" version but rather will *only*
return the "default" version regardless of config.
If the flag is ``False`` (which it is by default) then the
config value will be used if present, and a default version is
used only if the config does not have a value.
:param prefix: If specified, will override the prefix used for
config lookups.
.. warning::
This ``prefix`` param is for backward compatibility and may
be removed in the future.
:returns: The appropriate version string, e.g. ``'1.2.3'`` or
``'latest'`` etc.
"""
config = request.wutta_config
# nb. we prefer a setting to be named like: wuttaweb.libver.vue
# but for back-compat this also can work: wuttaweb.vue_version
# however that compat only works for some of the settings...
if not default_only:
# nb. new/preferred setting
version = config.get(f'{prefix}.libver.{key}')
if version:
return version
if key == 'buefy':
if not default_only:
# nb. old/legacy setting
version = config.get(f'{prefix}.buefy_version')
if version:
return version
return 'latest'
elif key == 'buefy.css':
# nb. this always returns something
return get_libver(request, 'buefy', default_only=default_only)
elif key == 'vue':
if not default_only:
# nb. old/legacy setting
version = config.get(f'{prefix}.vue_version')
if version:
return version
return '2.6.14'
elif key == 'vue_resource':
return 'latest'
elif key == 'fontawesome':
return '5.3.1'
elif key == 'bb_vue':
return '3.4.31'
elif key == 'bb_oruga':
return '0.8.12'
elif key in ('bb_oruga_bulma', 'bb_oruga_bulma_css'):
return '0.3.0'
elif key == 'bb_fontawesome_svg_core':
return '6.5.2'
elif key == 'bb_free_solid_svg_icons':
return '6.5.2'
elif key == 'bb_vue_fontawesome':
return '3.0.6'
def get_liburl(
request,
key,
prefix='wuttaweb',
):
"""
Return the appropriate URL for the web resource library identified
by ``key``.
WuttaWeb makes certain assumptions about which libraries would be
used on the frontend, and which versions for each would be used by
default. But ultimately a URL must be determined for each, hence
this function.
Each library has a built-in default URL which references a public
Internet (i.e. CDN) resource, but your config can override the
final URL in two ways:
The simplest way is to just override the *version* but otherwise
let the default logic construct the URL. See :func:`get_libver()`
for more on that approach.
The most flexible way is to override the URL explicitly, e.g.:
.. code-block:: ini
[wuttaweb]
liburl.bb_vue = https://example.com/cache/vue-3.4.31.js
:param request: Current request.
:param key: Unique key for the library, as string. Possibilities
are:
Vue 2 + Buefy
* ``vue``
* ``vue_resource``
* ``buefy``
* ``buefy.css``
* ``fontawesome``
Vue 3 + Oruga
* ``bb_vue``
* ``bb_oruga``
* ``bb_oruga_bulma``
* ``bb_oruga_bulma_css``
* ``bb_fontawesome_svg_core``
* ``bb_free_solid_svg_icons``
* ``bb_vue_fontawesome``
:param prefix: If specified, will override the prefix used for
config lookups.
.. warning::
This ``prefix`` param is for backward compatibility and may
be removed in the future.
:returns: The appropriate URL as string.
"""
config = request.wutta_config
url = config.get(f'{prefix}.liburl.{key}')
if url:
return url
version = get_libver(request, key, prefix=prefix)
if key == 'buefy':
return f'https://unpkg.com/buefy@{version}/dist/buefy.min.js'
elif key == 'buefy.css':
return f'https://unpkg.com/buefy@{version}/dist/buefy.min.css'
elif key == 'vue':
return f'https://unpkg.com/vue@{version}/dist/vue.min.js'
elif key == 'vue_resource':
return f'https://cdn.jsdelivr.net/npm/vue-resource@{version}'
elif key == 'fontawesome':
return f'https://use.fontawesome.com/releases/v{version}/js/all.js'
elif key == 'bb_vue':
return f'https://unpkg.com/vue@{version}/dist/vue.esm-browser.prod.js'
elif key == 'bb_oruga':
return f'https://unpkg.com/@oruga-ui/oruga-next@{version}/dist/oruga.mjs'
elif key == 'bb_oruga_bulma':
return f'https://unpkg.com/@oruga-ui/theme-bulma@{version}/dist/bulma.mjs'
elif key == 'bb_oruga_bulma_css':
return f'https://unpkg.com/@oruga-ui/theme-bulma@{version}/dist/bulma.css'
elif key == 'bb_fontawesome_svg_core':
return f'https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-svg-core@{version}/+esm'
elif key == 'bb_free_solid_svg_icons':
return f'https://cdn.jsdelivr.net/npm/@fortawesome/free-solid-svg-icons@{version}/+esm'
elif key == 'bb_vue_fontawesome':
return f'https://cdn.jsdelivr.net/npm/@fortawesome/vue-fontawesome@{version}/+esm'

View file

@ -0,0 +1,36 @@
# -*- 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/>.
#
################################################################################
"""
Web Views
For convenience, from this ``wuttaweb.views`` namespace you can access
the following:
* :class:`~wuttaweb.views.base.View`
"""
from .base import View
def includeme(config):
config.include('wuttaweb.views.common')

View file

@ -0,0 +1,53 @@
# -*- 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 Views
"""
class View:
"""
Base class for all class-based views.
Instances of this class (or rather, a subclass) are created by
Pyramid when processing a request. They will have the following
attributes:
.. attribute:: request
Reference to the current
:class:`pyramid:pyramid.request.Request` object.
.. attribute:: app
Reference to the :term:`app handler`.
.. attribute:: config
Reference to the app :term:`config object`.
"""
def __init__(self, request, context=None):
self.request = request
self.config = self.request.wutta_config
self.app = self.config.get_app()

View file

@ -0,0 +1,71 @@
# -*- 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/>.
#
################################################################################
"""
Common Views
"""
from wuttaweb.views import View
class CommonView(View):
"""
Common views shared by all apps.
"""
def home(self):
"""
Home page view.
Template: ``/home.mako``
This is normally the view shown when a user navigates to the
root URL for the web app.
"""
return {
'index_title': self.app.get_title(),
}
@classmethod
def defaults(cls, config):
cls._defaults(config)
@classmethod
def _defaults(cls, config):
# home
config.add_route('home', '/')
config.add_view(cls, attr='home',
route_name='home',
renderer='/home.mako')
def defaults(config, **kwargs):
base = globals()
CommonView = kwargs.get('CommonView', base['CommonView'])
CommonView.defaults(config)
def includeme(config):
defaults(config)