a6ce5eb21d
this starts to get things more aligned between wuttaweb and tailbone. the use case in mind so far is for a wuttaweb view to be included in a tailbone app. form and grid classes now have some new methods to match wuttaweb, so templates call the shared method names where possible. templates can no longer assume they have tailbone-native master view, form, grid etc. so must inspect context more closely in some cases.
258 lines
9.2 KiB
Python
258 lines
9.2 KiB
Python
# -*- coding: utf-8; -*-
|
|
################################################################################
|
|
#
|
|
# Rattail -- Retail Software Framework
|
|
# Copyright © 2010-2024 Lance Edgar
|
|
#
|
|
# This file is part of Rattail.
|
|
#
|
|
# Rattail 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.
|
|
#
|
|
# Rattail 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
|
|
# Rattail. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
################################################################################
|
|
"""
|
|
Event Subscribers
|
|
"""
|
|
|
|
import datetime
|
|
import logging
|
|
import warnings
|
|
from collections import OrderedDict
|
|
|
|
import rattail
|
|
|
|
import colander
|
|
import deform
|
|
from pyramid import threadlocal
|
|
from webhelpers2.html import tags
|
|
|
|
from wuttaweb import subscribers as base
|
|
|
|
import tailbone
|
|
from tailbone import helpers
|
|
from tailbone.db import Session
|
|
from tailbone.config import csrf_header_name, should_expose_websockets
|
|
from tailbone.util import get_available_themes, get_global_search_options
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
def new_request(event, session=None):
|
|
"""
|
|
Event hook called when processing a new request.
|
|
|
|
This first invokes the upstream hooks:
|
|
|
|
* :func:`wuttaweb:wuttaweb.subscribers.new_request()`
|
|
* :func:`wuttaweb:wuttaweb.subscribers.new_request_set_user()`
|
|
|
|
It then adds more things to the request object; among them:
|
|
|
|
.. attribute:: request.rattail_config
|
|
|
|
Reference to the app :term:`config object`. Note that this
|
|
will be the same as :attr:`wuttaweb:request.wutta_config`.
|
|
|
|
.. method:: request.register_component(tagname, classname)
|
|
|
|
Function to register a Vue component for use with the app.
|
|
|
|
This can be called from wherever a component is defined, and
|
|
then in the base template all registered components will be
|
|
properly loaded.
|
|
"""
|
|
request = event.request
|
|
|
|
# invoke main upstream logic
|
|
# nb. this sets request.wutta_config
|
|
base.new_request(event)
|
|
|
|
config = request.wutta_config
|
|
app = config.get_app()
|
|
auth = app.get_auth_handler()
|
|
session = session or Session()
|
|
|
|
# compatibility
|
|
rattail_config = config
|
|
request.rattail_config = rattail_config
|
|
|
|
def user_getter(request, db_session=None):
|
|
user = base.default_user_getter(request, db_session=db_session)
|
|
if user:
|
|
# nb. we also assign continuum user to session
|
|
session = db_session or Session()
|
|
session.set_continuum_user(user)
|
|
return user
|
|
|
|
# invoke upstream hook to set user
|
|
base.new_request_set_user(event, user_getter=user_getter, db_session=session)
|
|
|
|
# assign client IP address to the session, for sake of versioning
|
|
if hasattr(request, 'client_addr'):
|
|
session.continuum_remote_addr = request.client_addr
|
|
|
|
# request.register_component()
|
|
def register_component(tagname, classname):
|
|
"""
|
|
Register a Vue 3 component, so the base template knows to
|
|
declare it for use within the app (page).
|
|
"""
|
|
if not hasattr(request, '_tailbone_registered_components'):
|
|
request._tailbone_registered_components = OrderedDict()
|
|
|
|
if tagname in request._tailbone_registered_components:
|
|
log.warning("component with tagname '%s' already registered "
|
|
"with class '%s' but we are replacing that with "
|
|
"class '%s'",
|
|
tagname,
|
|
request._tailbone_registered_components[tagname],
|
|
classname)
|
|
|
|
request._tailbone_registered_components[tagname] = classname
|
|
request.register_component = register_component
|
|
|
|
|
|
def before_render(event):
|
|
"""
|
|
Adds goodies to the global template renderer context.
|
|
"""
|
|
# log.debug("before_render: %s", event)
|
|
|
|
# invoke upstream logic
|
|
base.before_render(event)
|
|
|
|
request = event.get('request') or threadlocal.get_current_request()
|
|
config = request.wutta_config
|
|
app = config.get_app()
|
|
|
|
renderer_globals = event
|
|
|
|
# overrides
|
|
renderer_globals['h'] = helpers
|
|
|
|
# misc.
|
|
renderer_globals['datetime'] = datetime
|
|
renderer_globals['colander'] = colander
|
|
renderer_globals['deform'] = deform
|
|
renderer_globals['csrf_header_name'] = csrf_header_name(config)
|
|
|
|
# TODO: deprecate / remove these
|
|
renderer_globals['rattail_app'] = app
|
|
renderer_globals['app_title'] = app.get_title()
|
|
renderer_globals['app_version'] = app.get_version()
|
|
renderer_globals['rattail'] = rattail
|
|
renderer_globals['tailbone'] = tailbone
|
|
renderer_globals['model'] = app.model
|
|
renderer_globals['enum'] = app.enum
|
|
|
|
# theme - we only want do this for classic web app, *not* API
|
|
# TODO: so, clearly we need a better way to distinguish the two
|
|
if 'tailbone.theme' in request.registry.settings:
|
|
renderer_globals['theme'] = request.registry.settings['tailbone.theme']
|
|
# note, this is just a global flag; user still needs permission to see picker
|
|
expose_picker = config.get_bool('tailbone.themes.expose_picker',
|
|
default=False)
|
|
renderer_globals['expose_theme_picker'] = expose_picker
|
|
if expose_picker:
|
|
|
|
# TODO: should remove 'falafel' option altogether
|
|
available = get_available_themes(config)
|
|
|
|
options = [tags.Option(theme, value=theme) for theme in available]
|
|
renderer_globals['theme_picker_options'] = options
|
|
|
|
# TODO: ugh, same deal here
|
|
renderer_globals['messaging_enabled'] = config.get_bool('tailbone.messaging.enabled',
|
|
default=False)
|
|
|
|
# background color may be set per-request, by some apps
|
|
if hasattr(request, 'background_color') and request.background_color:
|
|
renderer_globals['background_color'] = request.background_color
|
|
else: # otherwise we use the one from config
|
|
renderer_globals['background_color'] = config.get('tailbone.background_color')
|
|
|
|
# maybe set custom stylesheet
|
|
css = None
|
|
if request.user:
|
|
css = config.get(f'tailbone.{request.user.uuid}', 'user_css')
|
|
if not css:
|
|
css = config.get(f'tailbone.{request.user.uuid}', 'buefy_css')
|
|
if css:
|
|
warnings.warn(f"setting 'tailbone.{request.user.uuid}.buefy_css' should be"
|
|
f"changed to 'tailbone.{request.user.uuid}.user_css'",
|
|
DeprecationWarning)
|
|
renderer_globals['user_css'] = css
|
|
|
|
# add global search data for quick access
|
|
renderer_globals['global_search_data'] = get_global_search_options(request)
|
|
|
|
# here we globally declare widths for grid filter pseudo-columns
|
|
widths = config.get('tailbone.grids.filters.column_widths')
|
|
if widths:
|
|
widths = widths.split(';')
|
|
if len(widths) < 2:
|
|
widths = None
|
|
if not widths:
|
|
widths = ['15em', '15em']
|
|
renderer_globals['filter_fieldname_width'] = widths[0]
|
|
renderer_globals['filter_verb_width'] = widths[1]
|
|
|
|
# declare global support for websockets, or lack thereof
|
|
renderer_globals['expose_websockets'] = should_expose_websockets(config)
|
|
|
|
|
|
def add_inbox_count(event):
|
|
"""
|
|
Adds the current user's inbox message count to the global renderer context.
|
|
|
|
Note that this is not enabled by default; to turn it on you must do this:
|
|
|
|
config.add_subscriber('tailbone.subscribers.add_inbox_count', 'pyramid.events.BeforeRender')
|
|
"""
|
|
request = event.get('request') or threadlocal.get_current_request()
|
|
if request.user:
|
|
renderer_globals = event
|
|
app = request.rattail_config.get_app()
|
|
model = app.model
|
|
enum = request.rattail_config.get_enum()
|
|
renderer_globals['inbox_count'] = Session.query(model.Message)\
|
|
.outerjoin(model.MessageRecipient)\
|
|
.filter(model.MessageRecipient.recipient == Session.merge(request.user))\
|
|
.filter(model.MessageRecipient.status == enum.MESSAGE_STATUS_INBOX)\
|
|
.count()
|
|
|
|
|
|
def context_found(event):
|
|
"""
|
|
Attach some more goodies to the request object:
|
|
|
|
The following is attached to the request:
|
|
|
|
* ``get_session_timeout()`` function
|
|
"""
|
|
request = event.request
|
|
|
|
def get_session_timeout():
|
|
"""
|
|
Returns the timeout in effect for the current session
|
|
"""
|
|
return request.session.get('_timeout')
|
|
request.get_session_timeout = get_session_timeout
|
|
|
|
|
|
def includeme(config):
|
|
config.add_subscriber(new_request, 'pyramid.events.NewRequest')
|
|
config.add_subscriber(before_render, 'pyramid.events.BeforeRender')
|
|
config.add_subscriber(context_found, 'pyramid.events.ContextFound')
|