Source code for tailbone.subscribers

# -*- 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 six
import json
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

import tailbone
from tailbone import helpers
from tailbone.db import Session
from tailbone.config import csrf_header_name, should_expose_websockets
from tailbone.menus import make_simple_menus
from tailbone.util import (get_available_themes, get_global_search_options,
                           should_use_oruga)


log = logging.getLogger(__name__)


[docs] def new_request(event): """ Identify the current user, and cache their current permissions. Also adds the ``rattail_config`` attribute to the request. A global Rattail ``config`` should already be present within the Pyramid application registry's settings, which would normally be accessed via:: request.registry.settings['rattail_config'] This function merely "promotes" that config object so that it is more directly accessible, a la:: request.rattail_config .. note:: This of course assumes that a Rattail ``config`` object *has* in fact already been placed in the application registry settings. If this is not the case, this function will do nothing. Also, attach some goodies to the request object: * The currently logged-in user instance (if any), as ``user``. * ``is_admin`` flag indicating whether user has the Administrator role. * ``is_root`` flag indicating whether user is currently elevated to root. * A shortcut method for permission checking, as ``has_perm()``. """ request = event.request rattail_config = request.registry.settings.get('rattail_config') # TODO: why would this ever be null? if rattail_config: request.rattail_config = rattail_config def user(request): user = None uuid = request.authenticated_userid if uuid: model = request.rattail_config.get_model() user = Session.get(model.User, uuid) if user: Session().set_continuum_user(user) return user request.set_property(user, reify=True) # nb. only add oruga check for "classic" web app classic = rattail_config.parse_bool(request.registry.settings.get('tailbone.classic')) if classic: def use_oruga(request): return should_use_oruga(request) request.set_property(use_oruga, reify=True) # assign client IP address to the session, for sake of versioning Session().continuum_remote_addr = request.client_addr request.is_admin = bool(request.user) and request.user.is_admin() request.is_root = request.is_admin and request.session.get('is_root', False) # TODO: why would this ever be null? if rattail_config: app = rattail_config.get_app() auth = app.get_auth_handler() request.tailbone_cached_permissions = auth.get_permissions( Session(), request.user) def has_perm(name): if name in request.tailbone_cached_permissions: return True return request.is_root request.has_perm = has_perm def has_any_perm(*names): for name in names: if has_perm(name): return True return False request.has_any_perm = has_any_perm 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. """ request = event.get('request') or threadlocal.get_current_request() rattail_config = request.rattail_config renderer_globals = event renderer_globals['rattail_app'] = request.rattail_config.get_app() renderer_globals['app_title'] = request.rattail_config.app_title() renderer_globals['h'] = helpers renderer_globals['url'] = request.route_url renderer_globals['rattail'] = rattail renderer_globals['tailbone'] = tailbone renderer_globals['model'] = request.rattail_config.get_model() renderer_globals['enum'] = request.rattail_config.get_enum() renderer_globals['six'] = six renderer_globals['json'] = json renderer_globals['datetime'] = datetime renderer_globals['colander'] = colander renderer_globals['deform'] = deform renderer_globals['csrf_header_name'] = csrf_header_name(request.rattail_config) # 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['b'] = 'o' if request.use_oruga else 'b' # for buefy renderer_globals['theme'] = request.registry.settings['tailbone.theme'] # note, this is just a global flag; user still needs permission to see picker expose_picker = request.rattail_config.getbool('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(request.rattail_config) options = [tags.Option(theme, value=theme) for theme in available] renderer_globals['theme_picker_options'] = options # heck while we're assuming the classic web app here... # (we don't want this to happen for the API either!) # TODO: just..awful *shrug* # note that we assume "simple" menus nowadays if request.rattail_config.getbool('tailbone', 'menus.simple', default=True): renderer_globals['menus'] = make_simple_menus(request) # TODO: ugh, same deal here renderer_globals['messaging_enabled'] = request.rattail_config.getbool( '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'] = request.rattail_config.get( 'tailbone', 'background_color') # maybe set custom stylesheet css = None if request.user: css = rattail_config.get(f'tailbone.{request.user.uuid}', 'user_css') if not css: css = rattail_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 = request.rattail_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(rattail_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 enum = request.rattail_config.get_enum() model = request.rattail_config.get_model() 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_referrer()`` function * ``get_session_timeout()`` function """ request = event.request def get_referrer(default=None, **kwargs): if request.params.get('referrer'): return request.params['referrer'] if request.session.get('referrer'): return request.session.pop('referrer') referrer = request.referrer if (not referrer or referrer == request.current_route_url() or not referrer.startswith(request.host_url)): if default: referrer = default else: referrer = request.route_url('home') return referrer request.get_referrer = get_referrer 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')