diff --git a/tailbone/config.py b/tailbone/config.py index 1cb6236e..bcdde8a6 100644 --- a/tailbone/config.py +++ b/tailbone/config.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2022 Lance Edgar +# Copyright © 2010-2023 Lance Edgar # # This file is part of Rattail. # @@ -26,6 +26,7 @@ Rattail config extension for Tailbone from __future__ import unicode_literals, absolute_import +import warnings from pkg_resources import parse_version from rattail.config import ConfigExtension as BaseExtension @@ -64,7 +65,16 @@ def csrf_header_name(config): def get_buefy_version(config): - return config.get('tailbone', 'buefy_version') or '0.8.17' + warnings.warn("get_buefy_version() is deprecated; please use " + "tailbone.util.get_libver() instead", + DeprecationWarning, stacklevel=2) + + version = config.get('tailbone', 'libver.buefy') + if version: + return version + + return config.get('tailbone', 'buefy_version', + default='latest') def get_buefy_0_8(config, version=None): diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index 78fd2cc6..59ab6018 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2022 Lance Edgar +# Copyright © 2010-2023 Lance Edgar # # This file is part of Rattail. # @@ -453,10 +453,18 @@ class Grid(object): return pretty_boolean(value) def obtain_value(self, obj, column_name): + """ + Try to obtain and return the value from the given object, for + the given column name. + + :returns: The value, or ``None`` if no value was found. + """ try: return obj[column_name] + except KeyError: + pass except TypeError: - return getattr(obj, column_name) + return getattr(obj, column_name, None) def render_currency(self, obj, column_name): value = self.obtain_value(obj, column_name) diff --git a/tailbone/helpers.py b/tailbone/helpers.py index 750d3f39..aeb6aa01 100644 --- a/tailbone/helpers.py +++ b/tailbone/helpers.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2022 Lance Edgar +# Copyright © 2010-2023 Lance Edgar # # This file is part of Rattail. # @@ -41,7 +41,8 @@ from webhelpers2.html.tags import * from tailbone.util import (csrf_token, get_csrf_token, pretty_datetime, raw_datetime, render_markdown, - route_exists) + route_exists, + get_liburl) def pretty_date(date): diff --git a/tailbone/menus.py b/tailbone/menus.py index 8b432879..7da22696 100644 --- a/tailbone/menus.py +++ b/tailbone/menus.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2022 Lance Edgar +# Copyright © 2010-2023 Lance Edgar # # This file is part of Rattail. # @@ -403,3 +403,103 @@ def mark_allowed(request, menus): if item['allowed'] and item.get('type') != 'sep': topitem['allowed'] = True break + + +def make_admin_menu(request, include_stores=False): + """ + Generate a typical Admin menu + """ + items = [] + + if include_stores: + items.append({ + 'title': "Stores", + 'route': 'stores', + 'perm': 'stores.list', + }) + + items.extend([ + { + 'title': "Users", + 'route': 'users', + 'perm': 'users.list', + }, + { + 'title': "User Events", + 'route': 'userevents', + 'perm': 'userevents.list', + }, + { + 'title': "Roles", + 'route': 'roles', + 'perm': 'roles.list', + }, + {'type': 'sep'}, + { + 'title': "App Settings", + 'route': 'appsettings', + 'perm': 'settings.list', + }, + { + 'title': "Email Settings", + 'route': 'emailprofiles', + 'perm': 'emailprofiles.list', + }, + { + 'title': "Email Attempts", + 'route': 'email_attempts', + 'perm': 'email_attempts.list', + }, + { + 'title': "Raw Settings", + 'route': 'settings', + 'perm': 'settings.list', + }, + {'type': 'sep'}, + { + 'title': "DataSync Changes", + 'route': 'datasyncchanges', + 'perm': 'datasync_changes.list', + }, + { + 'title': "DataSync Status", + 'route': 'datasync.status', + 'perm': 'datasync.status', + }, + { + 'title': "Importing / Exporting", + 'route': 'importing', + 'perm': 'importing.list', + }, + { + 'title': "Luigi Tasks", + 'route': 'luigi', + 'perm': 'luigi.list', + }, + { + 'title': "Tables", + 'route': 'tables', + 'perm': 'tables.list', + }, + { + 'title': "App Info", + 'route': 'appinfo', + 'perm': 'appinfo.list', + }, + { + 'title': "Configure App", + 'route': 'appinfo.configure', + 'perm': 'appinfo.configure', + }, + { + 'title': "Upgrades", + 'route': 'upgrades', + 'perm': 'upgrades.list', + }, + ]) + + return { + 'title': "Admin", + 'type': 'menu', + 'items': items, + } diff --git a/tailbone/subscribers.py b/tailbone/subscribers.py index 4aed36cd..cbbcb95a 100644 --- a/tailbone/subscribers.py +++ b/tailbone/subscribers.py @@ -41,9 +41,9 @@ import tailbone from tailbone import helpers from tailbone.db import Session from tailbone.config import (csrf_header_name, should_expose_websockets, - get_buefy_version, get_buefy_0_8) + get_buefy_0_8) from tailbone.menus import make_simple_menus -from tailbone.util import should_use_buefy, get_global_search_options +from tailbone.util import should_use_buefy, get_global_search_options, get_libver def new_request(event): @@ -160,13 +160,8 @@ def before_render(event): # buefy themes get some extra treatment if should_use_buefy(request): - # declare vue.js and buefy versions to use. the default - # values here are "quite conservative" as of this writing, - # perhaps too much so, but at least they should work fine. - renderer_globals['vue_version'] = request.rattail_config.get( - 'tailbone', 'vue_version') or '2.6.10' - version = get_buefy_version(rattail_config) - renderer_globals['buefy_version'] = version + # TODO: remove this hack once all nodes safely on buefy 0.9 + version = get_libver(request, 'buefy') renderer_globals['buefy_0_8'] = get_buefy_0_8(rattail_config, version=version) diff --git a/tailbone/templates/appinfo/configure.mako b/tailbone/templates/appinfo/configure.mako new file mode 100644 index 00000000..821f937f --- /dev/null +++ b/tailbone/templates/appinfo/configure.mako @@ -0,0 +1,242 @@ +## -*- coding: utf-8; -*- +<%inherit file="/configure.mako" /> + +<%def name="form_content()"> + +

Basics

+
+ + + + + + + + + + ## TODO: should be a dropdown, app handler defines choices + + + + + + + + + + + + + + Production Mode + + + +
+ +

Display

+
+ + + + + + + + + + +
+ +

Grids

+
+ + + + + + + + + + +
+ +

Web Libraries

+
+ + + + % if buefy_0_8: + + % endif + + + + % for weblib in weblibs: + ${h.hidden('tailbone.libver.{}'.format(weblib['key']), **{':value': "simpleSettings['tailbone.libver.{}']".format(weblib['key'])})} + ${h.hidden('tailbone.liburl.{}'.format(weblib['key']), **{':value': "simpleSettings['tailbone.liburl.{}']".format(weblib['key'])})} + % endfor + + + + + +
+ + +<%def name="modify_this_page_vars()"> + ${parent.modify_this_page_vars()} + + + + +${parent.body()} diff --git a/tailbone/templates/appinfo/index.mako b/tailbone/templates/appinfo/index.mako new file mode 100644 index 00000000..4bf70354 --- /dev/null +++ b/tailbone/templates/appinfo/index.mako @@ -0,0 +1,114 @@ +## -*- coding: utf-8; -*- +<%inherit file="/master/index.mako" /> + +<%def name="render_grid_component()"> + + + + + +
+
+ + + % if buefy_0_8: + + % endif + + +
+
+
+ + + + + +
+
+ ${parent.render_grid_component()} +
+
+
+ + +<%def name="modify_this_page_vars()"> + ${parent.modify_this_page_vars()} + + + + +${parent.body()} diff --git a/tailbone/templates/themes/falafel/base.mako b/tailbone/templates/themes/falafel/base.mako index 888b3bb6..adbcd893 100644 --- a/tailbone/templates/themes/falafel/base.mako +++ b/tailbone/templates/themes/falafel/base.mako @@ -107,21 +107,20 @@ <%def name="jquery()"> - ${h.javascript_link(request.rattail_config.get('tailbone', 'liburl.jquery', default='https://code.jquery.com/jquery-1.12.4.min.js'))} + ${h.javascript_link(h.get_liburl(request, 'jquery'))} <%def name="vuejs()"> - ${h.javascript_link(request.rattail_config.get('tailbone', 'liburl.vue', default='https://unpkg.com/vue@{}/dist/vue.min.js'.format(vue_version)))} - ## TODO: make this version configurable also - ${h.javascript_link(request.rattail_config.get('tailbone', 'liburl.vue_resource', default='https://cdn.jsdelivr.net/npm/vue-resource@1.5.1'))} + ${h.javascript_link(h.get_liburl(request, 'vue'))} + ${h.javascript_link(h.get_liburl(request, 'vue_resource'))} <%def name="buefy()"> - ${h.javascript_link(request.rattail_config.get('tailbone', 'liburl.buefy', default='https://unpkg.com/buefy@{}/dist/buefy.min.js'.format(buefy_version)))} + ${h.javascript_link(h.get_liburl(request, 'buefy'))} <%def name="fontawesome()"> - + <%def name="extra_javascript()"> @@ -159,14 +158,14 @@ ${h.stylesheet_link(buefy_css)} % else: ## upstream Buefy CSS - ${h.stylesheet_link(request.rattail_config.get('tailbone', 'liburl.buefy.css', default='https://unpkg.com/buefy@{}/dist/buefy.min.css'.format(buefy_version)))} + ${h.stylesheet_link(h.get_liburl(request, 'buefy.css'))} % endif ## TODO: this is only being referenced by the progress template i think? ## (so, should make a Buefy progress page at least) <%def name="jquery_theme()"> - ${h.stylesheet_link(request.rattail_config.get('tailbone', 'liburl.jquery.css', default='https://code.jquery.com/ui/1.11.4/themes/dark-hive/jquery-ui.css'))} + ${h.stylesheet_link(h.get_liburl(request, 'jquery_ui'))} <%def name="extra_styles()"> diff --git a/tailbone/util.py b/tailbone/util.py index a9aa3bf3..ccab81c6 100644 --- a/tailbone/util.py +++ b/tailbone/util.py @@ -98,6 +98,106 @@ def get_global_search_options(request): return options +def get_libver(request, key, fallback=True, default_only=False): + """ + Return the appropriate URL for the library identified by ``key``. + """ + config = request.rattail_config + + if not default_only: + version = config.get('tailbone', 'libver.{}'.format(key)) + if version: + return version + + if not fallback and not default_only: + + if key == 'buefy': + version = config.get('tailbone', 'buefy_version') + if version: + return version + + elif key == 'buefy.css': + version = get_libver(request, 'buefy', fallback=False) + if version: + return version + + elif key == 'vue': + version = config.get('tailbone', 'vue_version') + if version: + return version + + return + + if key == 'buefy': + if not default_only: + version = config.get('tailbone', 'buefy_version') + if version: + return version + return 'latest' + + elif key == 'buefy.css': + version = get_libver(request, 'buefy', default_only=default_only) + if version: + return version + return 'latest' + + elif key == 'vue': + if not default_only: + version = config.get('tailbone', '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 == 'jquery': + return '1.12.4' + + elif key == 'jquery_ui': + return '1.11.4' + + +def get_liburl(request, key, fallback=True): + """ + Return the appropriate URL for the library identified by ``key``. + """ + config = request.rattail_config + + url = config.get('tailbone', 'liburl.{}'.format(key)) + if url: + return url + + if not fallback: + return + + version = get_libver(request, key) + + if key == 'buefy': + return 'https://unpkg.com/buefy@{}/dist/buefy.min.js'.format(version) + + elif key == 'buefy.css': + return 'https://unpkg.com/buefy@{}/dist/buefy.min.css'.format(version) + + elif key == 'vue': + return 'https://unpkg.com/vue@{}/dist/vue.min.js'.format(version) + + elif key == 'vue_resource': + return 'https://cdn.jsdelivr.net/npm/vue-resource@{}'.format(version) + + elif key == 'fontawesome': + return 'https://use.fontawesome.com/releases/v{}/js/all.js'.format(version) + + elif key == 'jquery': + return 'https://code.jquery.com/jquery-{}.min.js'.format(version) + + elif key == 'jquery_ui': + return 'https://code.jquery.com/ui/{}/themes/dark-hive/jquery-ui.css'.format(version) + + def should_use_buefy(request): """ Returns a flag indicating whether or not the current theme supports (and diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 1771a3b7..a80b6c26 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -2248,6 +2248,8 @@ class MasterView(View): route = self.get_route_prefix() return self.request.route_url(route, **kwargs) + # TODO: this should not be class method, if possible + # (pretty sure overriding as instance method works fine) @classmethod def get_index_title(cls): """ @@ -4822,6 +4824,8 @@ class MasterView(View): value = six.text_type(bool(value)).lower() elif simple.get('type') is int: value = six.text_type(int(value or '0')) + elif value is None: + value = '' else: value = six.text_type(value) diff --git a/tailbone/views/settings.py b/tailbone/views/settings.py index 9a1e8620..f4a213c0 100644 --- a/tailbone/views/settings.py +++ b/tailbone/views/settings.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2022 Lance Edgar +# Copyright © 2010-2023 Lance Edgar # # This file is part of Rattail. # @@ -26,14 +26,17 @@ Settings Views from __future__ import unicode_literals, absolute_import +import os import re +import subprocess +import sys import json import six from rattail.db import model from rattail.settings import Setting -from rattail.util import import_module_path +from rattail.util import import_module_path, OrderedDict import colander from webhelpers2.html import tags @@ -41,6 +44,153 @@ from webhelpers2.html import tags from tailbone import forms from tailbone.db import Session from tailbone.views import MasterView, View +from tailbone.util import get_libver, get_liburl + + +class AppInfoView(MasterView): + """ + Master view for the overall app, to show/edit config etc. + """ + route_prefix = 'appinfo' + model_key = 'UNUSED' + model_title = "UNUSED" + model_title_plural = "App Info" + creatable = False + viewable = False + editable = False + deletable = False + filterable = False + pageable = False + configurable = True + + grid_columns = [ + 'name', + 'version', + 'editable_project_location', + ] + + def get_index_title(self): + return "App Info for {}".format(self.rattail_config.app_title()) + + def get_data(self, session=None): + pip = os.path.join(sys.prefix, 'bin', 'pip') + output = subprocess.check_output([pip, 'list', '--format=json']) + data = json.loads(output.decode('utf_8').strip()) + + for pkg in data: + pkg.setdefault('editable_project_location', '') + + return data + + def configure_grid(self, g): + super(AppInfoView, self).configure_grid(g) + + g.sorters['name'] = g.make_simple_sorter('name', foldcase=True) + g.set_sort_defaults('name') + g.set_searchable('name') + + g.sorters['version'] = g.make_simple_sorter('version', foldcase=True) + + g.sorters['editable_project_location'] = g.make_simple_sorter( + 'editable_project_location', foldcase=True) + g.set_searchable('editable_project_location') + + def configure_get_context(self, **kwargs): + context = super(AppInfoView, self).configure_get_context(**kwargs) + + weblibs = OrderedDict([ + ('vue', "Vue"), + ('vue_resource', "vue-resource"), + ('buefy', "Buefy"), + ('buefy.css', "Buefy CSS"), + ('fontawesome', "FontAwesome"), + ('jquery', "jQuery"), + ('jquery_ui', "jQuery UI"), + ]) + + for key in weblibs: + title = weblibs[key] + weblibs[key] = { + 'key': key, + 'title': title, + + # nb. these values are exactly as configured, and are + # used for editing the settings + 'configured_version': get_libver(self.request, key, fallback=False), + 'configured_url': get_liburl(self.request, key, fallback=False), + + # these are for informational purposes only + 'default_version': get_libver(self.request, key, default_only=True), + 'live_url': get_liburl(self.request, key), + } + + context['weblibs'] = list(weblibs.values()) + return context + + def configure_get_simple_settings(self): + return [ + + # basics + {'section': 'rattail', + 'option': 'app_title'}, + {'section': 'rattail', + 'option': 'node_type'}, + {'section': 'rattail', + 'option': 'node_title'}, + {'section': 'rattail', + 'option': 'production', + 'type': bool}, + + # display + {'section': 'tailbone', + 'option': 'background_color'}, + + # grids + {'section': 'tailbone', + 'option': 'grid.default_pagesize', + # TODO: seems like should enforce this, but validation is + # not setup yet + # 'type': int + }, + + # web libs + {'section': 'tailbone', + 'option': 'libver.vue'}, + {'section': 'tailbone', + 'option': 'liburl.vue'}, + {'section': 'tailbone', + 'option': 'libver.vue_resource'}, + {'section': 'tailbone', + 'option': 'liburl.vue_resource'}, + {'section': 'tailbone', + 'option': 'libver.buefy'}, + {'section': 'tailbone', + 'option': 'liburl.buefy'}, + {'section': 'tailbone', + 'option': 'libver.buefy.css'}, + {'section': 'tailbone', + 'option': 'liburl.buefy.css'}, + {'section': 'tailbone', + 'option': 'libver.fontawesome'}, + {'section': 'tailbone', + 'option': 'liburl.fontawesome'}, + {'section': 'tailbone', + 'option': 'libver.jquery'}, + {'section': 'tailbone', + 'option': 'liburl.jquery'}, + {'section': 'tailbone', + 'option': 'libver.jquery_ui'}, + {'section': 'tailbone', + 'option': 'liburl.jquery_ui'}, + + # nb. these are no longer used (deprecated), but we keep + # them defined here so the tool auto-deletes them + {'section': 'tailbone', + 'option': 'buefy_version'}, + {'section': 'tailbone', + 'option': 'vue_version'}, + + ] class SettingView(MasterView): @@ -322,6 +472,9 @@ class AppSettingsView(View): def defaults(config, **kwargs): base = globals() + AppInfoView = kwargs.get('AppInfoView', base['AppInfoView']) + AppInfoView.defaults(config) + AppSettingsView = kwargs.get('AppSettingsView', base['AppSettingsView']) AppSettingsView.defaults(config)