3
0
Fork 0

fix: use fanstatic to serve built-in images by default

Refs: #1
This commit is contained in:
Lance Edgar 2024-12-10 16:54:02 -06:00
parent 783d4dc8ef
commit 16ed125113
11 changed files with 206 additions and 28 deletions

View file

@ -29,6 +29,7 @@ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
intersphinx_mapping = {
'colander': ('https://docs.pylonsproject.org/projects/colander/en/latest/', None),
'deform': ('https://docs.pylonsproject.org/projects/deform/en/latest/', None),
'fanstatic': ('https://www.fanstatic.org/en/latest/', None),
'pyramid': ('https://docs.pylonsproject.org/projects/pyramid/en/latest/', None),
'python': ('https://docs.python.org/3/', None),
'rattail-manual': ('https://rattailproject.org/docs/rattail-manual/', None),

View file

@ -13,3 +13,18 @@ Glossary
tools.
See also the :class:`~wuttaweb.grids.base.Grid` base class.
menu handler
This is the :term:`handler` responsible for constructing the main
app menu at top of page.
The menu handler is accessed by way of the :term:`web handler`.
See also the :class:`~wuttaweb.menus.MenuHandler` base class.
web handler
This is the :term:`handler` responsible for overall web layer
customizations, e.g. logo image and menu overrides. Although
the latter it delegates to the :term:`menu handler`.
See also the :class:`~wuttaweb.handler.WebHandler` base class.

View file

@ -53,6 +53,10 @@ docs = ["Sphinx", "furo"]
tests = ["pytest-cov", "tox"]
[project.entry-points."fanstatic.libraries"]
wuttaweb_img = "wuttaweb.static:img"
[project.entry-points."paste.app_factory"]
main = "wuttaweb.app:main"

View file

@ -22,21 +22,88 @@
################################################################################
"""
Web Handler
This defines the :term:`handler` for the web layer.
"""
from wuttjamaican.app import GenericHandler
from wuttaweb import static
class WebHandler(GenericHandler):
"""
Base class and default implementation for the "web" :term:`handler`.
Base class and default implementation for the :term:`web handler`.
This is responsible for determining the "menu handler" and
(eventually) possibly other things.
This is responsible for determining the :term:`menu handler` and
various other customizations.
"""
def get_fanstatic_url(self, request, resource):
"""
Returns the full URL to the given Fanstatic resource.
:param request: Current :term:`request` object.
:param resource: :class:`fanstatic:fanstatic.Resource`
instance representing an image file or other resource.
"""
needed = request.environ['fanstatic.needed']
url = needed.library_url(resource.library) + '/'
if request.script_name:
url = request.script_name + url
return url + resource.relpath
def get_favicon_url(self, request):
"""
Returns the canonical app favicon image URL.
This will return the fallback favicon from WuttaWeb unless
config specifies an override:
.. code-block:: ini
[wuttaweb]
favicon_url = http://example.com/favicon.ico
"""
url = self.config.get('wuttaweb.favicon_url')
if url:
return url
return self.get_fanstatic_url(request, static.favicon)
def get_header_logo_url(self, request):
"""
Returns the canonical app header image URL.
This will return the value from config if specified (as shown
below); otherwise it will just call :meth:`get_favicon_url()`
and return that.
.. code-block:: ini
[wuttaweb]
header_logo_url = http://example.com/logo.png
"""
url = self.config.get('wuttaweb.header_logo_url')
if url:
return url
return self.get_favicon_url(request)
def get_main_logo_url(self, request):
"""
Returns the canonical app logo image URL.
This will return the fallback logo from WuttaWeb unless config
specifies an override:
.. code-block:: ini
[wuttaweb]
logo_url = http://example.com/logo.png
"""
url = self.config.get('wuttaweb.logo_url')
if url:
return url
return self.get_fanstatic_url(request, static.logo)
def get_menu_handler(self, **kwargs):
"""
Get the configured "menu" handler for the web app.

View file

@ -35,7 +35,7 @@ log = logging.getLogger(__name__)
class MenuHandler(GenericHandler):
"""
Base class and default implementation for menu handler.
Base class and default implementation for :term:`menu handler`.
It is assumed that most apps will override the menu handler with
their own subclass. In particular the subclass will override

View file

@ -23,15 +23,52 @@
"""
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()`::
Note that (for now?) 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.
However, an attempt is being made to incorporate Fanstatic for use
with the built-in static assets. It is possible the above mechanism
could be abandoned in the future.
So on the Fanstatic front, we currently have defined:
.. data:: img
A :class:`fanstatic:fanstatic.Library` representing the ``img``
static folder.
.. data:: favicon
A :class:`fanstatic:fanstatic.Resource` representing the
``img/favicon.ico`` image file.
.. data:: logo
A :class:`fanstatic:fanstatic.Resource` representing the
``img/logo.png`` image file.
.. data:: testing
A :class:`fanstatic:fanstatic.Resource` representing the
``img/testing.png`` image file.
"""
from fanstatic import Library, Resource
# fanstatic img library
img = Library('wuttaweb_img', 'img')
favicon = Resource(img, 'favicon.ico')
# nb. mock out the renderers here, to appease fanstatic
logo = Resource(img, 'logo.png', renderer=True)
testing = Resource(img, 'testing.png', renderer=True)
# TODO: should consider deprecating this?
def includeme(config):
config.add_static_view('wuttaweb', 'wuttaweb:static')

View file

@ -260,13 +260,17 @@ def before_render(event):
Here are the keys added to context dict by this hook:
.. data:: 'config'
Reference to the app :term:`config object`.
.. data:: 'app'
Reference to the :term:`app handler`.
.. data:: 'config'
.. data:: 'web'
Reference to the app :term:`config object`.
Reference to the :term:`web handler`.
.. data:: 'h'
@ -293,8 +297,9 @@ def before_render(event):
web = app.get_web_handler()
context = event
context['app'] = app
context['config'] = config
context['app'] = app
context['web'] = web
context['h'] = helpers
context['url'] = request.route_url
context['json'] = json

View file

@ -5,15 +5,15 @@
<%def name="extra_styles()"></%def>
<%def name="favicon()">
<link rel="icon" type="image/x-icon" href="${config.get('wuttaweb.favicon_url', default=request.static_url('wuttaweb:static/img/favicon.ico'))}" />
<link rel="icon" type="image/x-icon" href="${web.get_favicon_url(request)}" />
</%def>
<%def name="header_logo()">
${h.image(config.get('wuttaweb.header_logo_url', default=request.static_url('wuttaweb:static/img/favicon.ico')), "Header Logo", style="height: 49px;")}
${h.image(web.get_header_logo_url(request), "Header Logo", style="height: 49px;")}
</%def>
<%def name="full_logo(image_url=None)">
${h.image(image_url or config.get('wuttaweb.logo_url', default=request.static_url('wuttaweb:static/img/logo.png')), f"{app.get_title()} logo")}
${h.image(image_url or web.get_main_logo_url(request), f"App Logo for {app.get_title()}")}
</%def>
<%def name="footer()">

View file

@ -1,20 +1,64 @@
# -*- coding: utf-8; -*-
from unittest import TestCase
from wuttjamaican.conf import WuttaConfig
from wuttaweb import handler as mod
from wuttaweb import handler as mod, static
from wuttaweb.menus import MenuHandler
from tests.util import WebTestCase
class TestWebHandler(TestCase):
class TestWebHandler(WebTestCase):
def setUp(self):
self.config = WuttaConfig()
self.app = self.config.get_app()
self.handler = mod.WebHandler(self.config)
def make_handler(self):
return mod.WebHandler(self.config)
def test_get_fanstatic_url(self):
handler = self.make_handler()
# default with / root path
url = handler.get_fanstatic_url(self.request, static.logo)
self.assertEqual(url, '/fanstatic/wuttaweb_img/logo.png')
# what about a subpath
self.request.script_name = '/testing'
url = handler.get_fanstatic_url(self.request, static.logo)
self.assertEqual(url, '/testing/fanstatic/wuttaweb_img/logo.png')
def test_get_favicon_url(self):
handler = self.make_handler()
# default
url = handler.get_favicon_url(self.request)
self.assertEqual(url, '/fanstatic/wuttaweb_img/favicon.ico')
# config override
self.config.setdefault('wuttaweb.favicon_url', '/testing/other.ico')
url = handler.get_favicon_url(self.request)
self.assertEqual(url, '/testing/other.ico')
def test_get_header_logo_url(self):
handler = self.make_handler()
# default
url = handler.get_header_logo_url(self.request)
self.assertEqual(url, '/fanstatic/wuttaweb_img/favicon.ico')
# config override
self.config.setdefault('wuttaweb.header_logo_url', '/testing/header.png')
url = handler.get_header_logo_url(self.request)
self.assertEqual(url, '/testing/header.png')
def test_get_main_logo_url(self):
handler = self.make_handler()
# default
url = handler.get_main_logo_url(self.request)
self.assertEqual(url, '/fanstatic/wuttaweb_img/logo.png')
# config override
self.config.setdefault('wuttaweb.logo_url', '/testing/other.png')
url = handler.get_main_logo_url(self.request)
self.assertEqual(url, '/testing/other.png')
def test_menu_handler_default(self):
menus = self.handler.get_menu_handler()
handler = self.make_handler()
menus = handler.get_menu_handler()
self.assertIsInstance(menus, MenuHandler)

View file

@ -3,6 +3,7 @@
from unittest import TestCase
from unittest.mock import MagicMock
import fanstatic
from pyramid import testing
from wuttjamaican.conf import WuttaConfig
@ -66,6 +67,10 @@ class WebTestCase(DataTestCase):
'pyramid.events.BeforeRender')
self.pyramid_config.include('wuttaweb.static')
# nb. mock out fanstatic env..good enough for now to avoid errors..
needed = fanstatic.init_needed()
self.request.environ[fanstatic.NEEDED] = needed
# setup new request w/ anonymous user
event = MagicMock(request=self.request)
subscribers.new_request(event)

View file

@ -1028,7 +1028,7 @@ class TestMasterView(WebTestCase):
self.session.commit()
def get_instance():
setting = self.session.query(model.Setting).get('foo.bar')
setting = self.session.get(model.Setting, 'foo.bar')
return {
'name': setting.name,
'value': setting.value,
@ -1092,7 +1092,7 @@ class TestMasterView(WebTestCase):
self.assertEqual(self.session.query(model.Setting).count(), 1)
def get_instance():
setting = self.session.query(model.Setting).get('foo.bar')
setting = self.session.get(model.Setting, 'foo.bar')
return {
'name': setting.name,
'value': setting.value,