+%def>
diff --git a/tailbone/util.py b/tailbone/util.py
index 890cd778..bc891292 100644
--- a/tailbone/util.py
+++ b/tailbone/util.py
@@ -33,7 +33,9 @@ import pytz
import humanize
from rattail.time import timezone, make_utc
+from rattail.files import resource_path
+from pyramid.renderers import get_renderer
from webhelpers2.html import HTML, tags
@@ -115,3 +117,61 @@ def raw_datetime(config, value):
kwargs['title'] = humanize.naturaltime(time_ago)
return HTML.tag('span', **kwargs)
+
+
+def set_app_theme(request, theme, session=None):
+ """
+ Set the app theme. This modifies the *global* Mako template lookup
+ directory path, i.e. theme for all users will change immediately.
+
+ This also saves the setting for the new theme, and updates the running app
+ registry settings with the new theme.
+ """
+ from rattail.db import api
+
+ theme = get_effective_theme(request.rattail_config, theme=theme, session=session)
+ theme_path = get_theme_template_path(request.rattail_config, theme=theme, session=session)
+
+ # there's only one global template lookup; can get to it via any renderer
+ # but should *not* use /base.mako since that one is about to get volatile
+ renderer = get_renderer('/menu.mako')
+ lookup = renderer.lookup
+
+ # overwrite first entry in lookup's directory list
+ lookup.directories[0] = theme_path
+
+ # remove base template from lookup cache, so it will reload from new theme path
+ lookup._collection.pop('/base.mako', None)
+
+ api.save_setting(session, 'tailbone.theme', theme)
+ request.registry.settings['tailbone.theme'] = theme
+
+
+def get_theme_template_path(rattail_config, theme=None, session=None):
+ """
+ Retrieves the template path for the given theme.
+ """
+ theme = get_effective_theme(rattail_config, theme=theme, session=session)
+ theme_path = rattail_config.get('tailbone', 'theme.{}'.format(theme),
+ default='tailbone:templates/themes/{}'.format(theme))
+ return resource_path(theme_path)
+
+
+def get_effective_theme(rattail_config, theme=None, session=None):
+ """
+ Validates and returns the "effective" theme. If you provide a theme, that
+ will be used; otherwise it is read from database setting.
+ """
+ from rattail.db import api
+
+ if not theme:
+ theme = api.get_setting(session, 'tailbone.theme') or 'default'
+
+ # confirm requested theme is available
+ available = rattail_config.getlist('tailbone', 'themes',
+ default=['bobcat'])
+ available.append('default')
+ if theme not in available:
+ raise ValueError("theme not available: {}".format(theme))
+
+ return theme
diff --git a/tailbone/views/common.py b/tailbone/views/common.py
index 22401a04..14370ad4 100644
--- a/tailbone/views/common.py
+++ b/tailbone/views/common.py
@@ -42,6 +42,7 @@ import tailbone
from tailbone import forms
from tailbone.db import Session
from tailbone.views import View
+from tailbone.util import set_app_theme
class Feedback(colander.Schema):
@@ -125,6 +126,22 @@ class CommonView(View):
('Tailbone', tailbone.__version__),
])
+ def change_theme(self):
+ """
+ Simple view which can change user's visible UI theme, then redirect
+ user back to referring page.
+ """
+ theme = self.request.params.get('theme')
+ if theme:
+ try:
+ set_app_theme(self.request, theme, session=Session())
+ except Exception as error:
+ msg = "Failed to set theme: {}: {}".format(error.__class__.__name__, error)
+ self.request.session.flash(msg, 'error')
+ else:
+ self.request.session.flash("App theme has been changed to: {}".format(theme))
+ return self.redirect(self.request.get_referrer())
+
def feedback(self):
"""
Generic view to present/handle the user feedback form.
@@ -188,6 +205,12 @@ class CommonView(View):
config.add_route('mobile.about', '/mobile/about')
config.add_view(cls, attr='about', route_name='mobile.about', renderer='/mobile/about.mako')
+ # change theme
+ config.add_tailbone_permission('common', 'common.change_app_theme',
+ "Change global App Template Theme")
+ config.add_route('change_theme', '/change-theme', request_method='POST')
+ config.add_view(cls, attr='change_theme', route_name='change_theme')
+
# feedback
config.add_route('feedback', '/feedback', request_method='POST')
config.add_view(cls, attr='feedback', route_name='feedback', renderer='json')