From 2b220459c7a39c47c28e59163f2ed39ed4704fb0 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 7 Dec 2022 12:33:32 -0600 Subject: [PATCH 001/794] Temporary cap version for Beaker, per broken web apps! --- setup.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setup.py b/setup.py index 3328785e..eaa9e5b1 100644 --- a/setup.py +++ b/setup.py @@ -75,6 +75,10 @@ requires = [ # (still, probably a better idea is to refactor so we can use 0.9) 'webhelpers2_grid==0.1', # 0.1 + # TODO: latest version breaks us totally! need to fix ASAP, but + # for the moment, must restrict version + 'Beaker<1.12', # 1.11.0 + # TODO: remove version cap once we can drop support for python 2.x 'cornice<5.0', # 3.4.2 4.0.1 From 22176e89dd7f790274665479addcb613a7985fa1 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 7 Dec 2022 14:00:32 -0600 Subject: [PATCH 002/794] Add support for Beaker >= 1.12.0 but still support previous versions too, for now --- setup.py | 4 ---- tailbone/beaker.py | 22 +++++++++++++++------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/setup.py b/setup.py index eaa9e5b1..3328785e 100644 --- a/setup.py +++ b/setup.py @@ -75,10 +75,6 @@ requires = [ # (still, probably a better idea is to refactor so we can use 0.9) 'webhelpers2_grid==0.1', # 0.1 - # TODO: latest version breaks us totally! need to fix ASAP, but - # for the moment, must restrict version - 'Beaker<1.12', # 1.11.0 - # TODO: remove version cap once we can drop support for python 2.x 'cornice<5.0', # 3.4.2 4.0.1 diff --git a/tailbone/beaker.py b/tailbone/beaker.py index 1f7f20c5..b5d592f1 100644 --- a/tailbone/beaker.py +++ b/tailbone/beaker.py @@ -1,8 +1,8 @@ -# -*- coding: utf-8 -*- +# -*- coding: utf-8; -*- ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2017 Lance Edgar +# Copyright © 2010-2022 Lance Edgar # # This file is part of Rattail. # @@ -30,7 +30,9 @@ pyramid_beaker projects. from __future__ import unicode_literals, absolute_import import time +from pkg_resources import parse_version +import beaker from beaker.session import Session from beaker.util import coerce_session_params from pyramid.settings import asbool @@ -45,6 +47,10 @@ class TailboneSession(Session): def load(self): "Loads the data from this session from persistent storage" + + # are we using older version of beaker? + old_beaker = parse_version(beaker.__version__) < parse_version('1.12') + self.namespace = self.namespace_class(self.id, data_dir=self.data_dir, digest_filenames=False, @@ -60,8 +66,12 @@ class TailboneSession(Session): try: session_data = self.namespace['session'] - if (session_data is not None and self.encrypt_key): - session_data = self._decrypt_data(session_data) + if old_beaker: + if (session_data is not None and self.encrypt_key): + session_data = self._decrypt_data(session_data) + else: # beaker >= 1.12 + if session_data is not None: + session_data = self._decrypt_data(session_data) # Memcached always returns a key, its None when its not # present @@ -90,6 +100,7 @@ class TailboneSession(Session): # for this module entirely... timeout = session_data.get('_timeout', self.timeout) if timeout is not None and \ + '_accessed_time' in session_data and \ now - session_data['_accessed_time'] > timeout: timed_out = True else: @@ -103,9 +114,6 @@ class TailboneSession(Session): # Update the current _accessed_time session_data['_accessed_time'] = now - # Set the path if applicable - if '_path' in session_data: - self._path = session_data['_path'] self.update(session_data) self.accessed_dict = session_data.copy() finally: From cea06c96735fa1a98f5b635ca4ea6b78a69835ac Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 7 Dec 2022 14:20:04 -0600 Subject: [PATCH 003/794] Update changelog --- CHANGES.rst | 6 ++++++ tailbone/_version.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index ebba8d69..11b3a793 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,12 @@ CHANGELOG ========= +0.8.268 (2022-12-07) +-------------------- + +* Add support for Beaker >= 1.12.0. + + 0.8.267 (2022-12-06) -------------------- diff --git a/tailbone/_version.py b/tailbone/_version.py index 321a1037..358b32de 100644 --- a/tailbone/_version.py +++ b/tailbone/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.8.267' +__version__ = '0.8.268' From f80d3cd530936d4839dc4303c40c670887ba63f8 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 8 Dec 2022 14:15:38 -0600 Subject: [PATCH 004/794] Show simple error string, when subprocess batch actions fail logs still have more info, can't show user the whole traceback..but this is better than we had before.. --- tailbone/views/batch/core.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tailbone/views/batch/core.py b/tailbone/views/batch/core.py index 6dc2436d..4e3ff9f5 100644 --- a/tailbone/views/batch/core.py +++ b/tailbone/views/batch/core.py @@ -963,12 +963,15 @@ class BatchMasterView(MasterView): # run command in subprocess log.debug("launching command in subprocess: %s", cmd) try: - subprocess.check_output(cmd, stderr=subprocess.PIPE) + # nb. we do not capture stderr, but on failure the stdout + # will contain a simple error string + subprocess.check_output(cmd) except subprocess.CalledProcessError as error: log.warning("command failed with exit code %s! output was:", error.returncode) - log.warning(error.stderr.decode('utf_8')) - raise + output = error.output.decode('utf_8') + log.warning(output) + raise Exception(output) def action_subprocess_thread(self, key, port, username, handler_action, progress, **kwargs): """ @@ -1009,8 +1012,8 @@ class BatchMasterView(MasterView): progress.session.load() progress.session['error'] = True progress.session['error_msg'] = ( - "{} of '{}' batch failed (see logs for more info)").format( - handler_action, self.handler.batch_key) + "{} of '{}' batch failed: {} (see logs for more info)").format( + handler_action, self.handler.batch_key, error) progress.session.save() return From 1a51f3d85449a491187c5d8cd910c03988a3a6d2 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 8 Dec 2022 14:54:36 -0600 Subject: [PATCH 005/794] Fix ordering worksheet API for date objects --- tailbone/api/batch/ordering.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tailbone/api/batch/ordering.py b/tailbone/api/batch/ordering.py index 9ab9617c..3c489fcd 100644 --- a/tailbone/api/batch/ordering.py +++ b/tailbone/api/batch/ordering.py @@ -29,6 +29,8 @@ API. from __future__ import unicode_literals, absolute_import +import datetime + import six from rattail.db import model @@ -94,6 +96,8 @@ class OrderingBatchViews(APIBatchView): if batch.executed: raise self.forbidden() + app = self.get_rattail_app() + # TODO: much of the logic below was copied from the traditional master # view for ordering batches. should maybe let them share it somehow? @@ -179,6 +183,16 @@ class OrderingBatchViews(APIBatchView): for i in range(6 - len(history)): history.append(None) history = list(reversed(history)) + # must convert some date objects to string, for JSON sake + for h in history: + purchase = h.get('purchase') + if purchase: + dt = purchase.get('date_ordered') + if dt and isinstance(dt, datetime.date): + purchase['date_ordered'] = app.render_date(dt) + dt = purchase.get('date_received') + if dt and isinstance(dt, datetime.date): + purchase['date_received'] = app.render_date(dt) return { 'batch': self.normalize(batch), From d5d9c644a26022966c7d8eec3b6635df1b21b40e Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 8 Dec 2022 18:19:32 -0600 Subject: [PATCH 006/794] Add the ViewSupplement concept also fix cell-class for grid columns. cannot use "raw" fieldname because in some cases (e.g. 'number', 'rate') Bulma may interpret that as actually meaning something, and affect the display --- tailbone/app.py | 12 +++ tailbone/templates/grids/buefy.mako | 2 +- tailbone/templates/receiving/view.mako | 8 +- tailbone/views/__init__.py | 4 +- tailbone/views/customers.py | 5 +- tailbone/views/departments.py | 12 ++- tailbone/views/master.py | 118 ++++++++++++++++++++++++- tailbone/views/subdepartments.py | 4 +- tailbone/views/vendors/core.py | 2 +- 9 files changed, 153 insertions(+), 14 deletions(-) diff --git a/tailbone/app.py b/tailbone/app.py index d7155829..1cfae6b2 100644 --- a/tailbone/app.py +++ b/tailbone/app.py @@ -177,6 +177,7 @@ def make_pyramid_config(settings, configure_csrf=True): # and some similar magic for certain master views config.add_directive('add_tailbone_index_page', 'tailbone.app.add_index_page') config.add_directive('add_tailbone_config_page', 'tailbone.app.add_config_page') + config.add_directive('add_tailbone_view_supplement', 'tailbone.app.add_view_supplement') config.add_directive('add_tailbone_websocket', 'tailbone.app.add_websocket') @@ -239,6 +240,17 @@ def add_config_page(config, route_name, label, permission): config.action(None, action) +def add_view_supplement(config, route_prefix, cls): + """ + Register a master view supplement for the app. + """ + def action(): + supplements = config.get_settings().get('tailbone_view_supplements', {}) + supplements.setdefault(route_prefix, []).append(cls) + config.add_settings({'tailbone_view_supplements': supplements}) + config.action(None, action) + + def establish_theme(settings): rattail_config = settings['rattail_config'] diff --git a/tailbone/templates/grids/buefy.mako b/tailbone/templates/grids/buefy.mako index 9d8359d9..47e9a2dc 100644 --- a/tailbone/templates/grids/buefy.mako +++ b/tailbone/templates/grids/buefy.mako @@ -221,7 +221,7 @@ % if grid.is_searchable(column['field']): searchable % endif - cell-class="${column['field']}" + cell-class="c_${column['field']}" % if grid.has_click_handler(column['field']): @click.native="${grid.click_handlers[column['field']]}" % endif diff --git a/tailbone/templates/receiving/view.mako b/tailbone/templates/receiving/view.mako index b16aa5b8..f4a90dcb 100644 --- a/tailbone/templates/receiving/view.mako +++ b/tailbone/templates/receiving/view.mako @@ -267,20 +267,20 @@ % if use_buefy: - - <%def name="page_content()">
-
${rendered_result or ""|n}
+
${rendered_result or ""|n}
diff --git a/tailbone/templates/page_help.mako b/tailbone/templates/page_help.mako new file mode 100644 index 00000000..cd13011e --- /dev/null +++ b/tailbone/templates/page_help.mako @@ -0,0 +1,204 @@ +## -*- coding: utf-8; -*- + +<%def name="render_template()"> + + + +<%def name="declare_vars()"> + + + +<%def name="make_component()"> + + diff --git a/tailbone/templates/themes/falafel/base.mako b/tailbone/templates/themes/falafel/base.mako index fe3ef429..e46be1a5 100644 --- a/tailbone/templates/themes/falafel/base.mako +++ b/tailbone/templates/themes/falafel/base.mako @@ -3,6 +3,7 @@ <%namespace file="/autocomplete.mako" import="tailbone_autocomplete_template" /> <%namespace name="base_meta" file="/base_meta.mako" /> <%namespace file="/formposter.mako" import="declare_formposter_mixin" /> +<%namespace name="page_help" file="/page_help.mako" /> @@ -383,17 +384,9 @@ % endif - ## Help Button - % if help_url is not Undefined and help_url: -
- - Help - -
- % endif +
+ +
## Feedback Button / Dialog % if request.has_perm('common.feedback'): @@ -466,6 +459,8 @@ + ${page_help.render_template()} + + % endif + + <%def name="finalize_this_page_vars()"> ${parent.finalize_this_page_vars()} % if form is not Undefined: diff --git a/tailbone/templates/forms/deform_buefy.mako b/tailbone/templates/forms/deform_buefy.mako index c387d965..0b1e8d90 100644 --- a/tailbone/templates/forms/deform_buefy.mako +++ b/tailbone/templates/forms/deform_buefy.mako @@ -66,6 +66,47 @@ % if not form.readonly: ${h.end_form()} % endif + + % if can_edit_help: + + + + % endif + @@ -85,7 +126,29 @@ submit${form.component_studly}() { this.${form.component_studly}Submitting = true this.${form.component_studly}ButtonText = "Working, please wait..." - } + }, + % endif + + % if can_edit_help: + configureFieldSave() { + this.configureFieldSaving = true + let url = '${edit_help_url}' + let params = { + field_name: this.configureFieldName, + markdown_text: this.configureFieldMarkdown, + } + this.submitForm(url, params, response => { + this.configureFieldShowDialog = false + this.$buefy.toast.open({ + message: "Info was saved; please refresh page to see changes.", + type: 'is-info', + duration: 4000, // 4 seconds + }) + this.configureFieldSaving = false + }, response => { + this.configureFieldSaving = false + }) + }, % endif } } @@ -95,6 +158,16 @@ ## TODO: should find a better way to handle CSRF token csrftoken: ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n}, + % if can_edit_help: + fieldLabels: ${json.dumps(field_labels)|n}, + fieldMarkdowns: ${json.dumps(field_markdowns)|n}, + configureFieldShowDialog: false, + configureFieldSaving: false, + configureFieldName: null, + configureFieldLabel: null, + configureFieldMarkdown: null, + % endif + ## TODO: ugh, this seems pretty hacky. need to declare some data models ## for various field components to bind to... % if not form.readonly: diff --git a/tailbone/templates/page_help.mako b/tailbone/templates/page_help.mako index cd13011e..b745965a 100644 --- a/tailbone/templates/page_help.mako +++ b/tailbone/templates/page_help.mako @@ -108,7 +108,7 @@
- + diff --git a/tailbone/util.py b/tailbone/util.py index f5457149..ca8d0933 100644 --- a/tailbone/util.py +++ b/tailbone/util.py @@ -183,12 +183,14 @@ def raw_datetime(config, value, verbose=False, as_date=False): return HTML.tag('span', **kwargs) -def render_markdown(text, **kwargs): +def render_markdown(text, raw=False, **kwargs): """ Render the given markdown text as HTML. """ kwargs.setdefault('extensions', ['fenced_code', 'codehilite']) md = markdown.markdown(text, **kwargs) + if raw: + return md md = HTML.literal(md) return HTML.tag('div', class_='rendered-markdown', c=[md]) diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 396c953e..2431b437 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -2333,6 +2333,40 @@ class MasterView(View): info.markdown_text = form.validated['markdown_text'] return {'ok': True} + def edit_field_help(self): + if (not self.has_perm('edit_help') + and not self.request.has_perm('common.edit_help')): + raise self.forbidden() + + model = self.model + route_prefix = self.get_route_prefix() + schema = colander.Schema() + + schema.add(colander.SchemaNode(colander.String(), + name='field_name')) + + schema.add(colander.SchemaNode(colander.String(), + name='markdown_text', + missing=None)) + + factory = self.get_form_factory() + form = factory(schema=schema, request=self.request) + if not form.validate(newstyle=True): + return {'error': "Form did not validate"} + + # nb. self.Session may differ, so use tailbone.db.Session + info = Session.query(model.TailboneFieldInfo)\ + .filter(model.TailboneFieldInfo.route_prefix == route_prefix)\ + .filter(model.TailboneFieldInfo.field_name == form.validated['field_name'])\ + .first() + if not info: + info = model.TailboneFieldInfo(route_prefix=route_prefix, + field_name=form.validated['field_name']) + Session.add(info) + + info.markdown_text = form.validated['markdown_text'] + return {'ok': True} + def render_to_response(self, template, data, **kwargs): """ Return a response with the given template rendered with the given data. @@ -3944,6 +3978,7 @@ class MasterView(View): Return a dictionary of kwargs to be passed to the factory when creating new form instances. """ + route_prefix = self.get_route_prefix() defaults = { 'request': self.request, 'readonly': self.viewing, @@ -3951,12 +3986,21 @@ class MasterView(View): 'action_url': self.request.current_route_url(_query=None), 'use_buefy': self.get_use_buefy(), 'assume_local_times': self.has_local_times, + 'route_prefix': route_prefix, + 'can_edit_help': (self.has_perm('edit_help') + or self.request.has_perm('common.edit_help')), } + + if defaults['can_edit_help']: + defaults['edit_help_url'] = self.request.route_url( + '{}.edit_field_help'.format(route_prefix)) + if self.creating: kwargs.setdefault('cancel_url', self.get_index_url()) else: instance = kwargs['model_instance'] kwargs.setdefault('cancel_url', self.get_action_url('view', instance)) + defaults.update(kwargs) return defaults @@ -4832,6 +4876,12 @@ class MasterView(View): config.add_view(cls, attr='edit_help', route_name='{}.edit_help'.format(route_prefix), renderer='json') + config.add_route('{}.edit_field_help'.format(route_prefix), + '{}/edit-field-help'.format(url_prefix), + request_method='POST') + config.add_view(cls, attr='edit_field_help', + route_name='{}.edit_field_help'.format(route_prefix), + renderer='json') # list/search if cls.listable: From b04c1054fcbd6e8acb4f626f235a92e02a8d00f9 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 25 Dec 2022 12:25:55 -0600 Subject: [PATCH 031/794] Override document title when upgrading when using websockets, to mimic old behavior without them --- tailbone/templates/upgrades/view.mako | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tailbone/templates/upgrades/view.mako b/tailbone/templates/upgrades/view.mako index c6ae11f2..a5b6445e 100644 --- a/tailbone/templates/upgrades/view.mako +++ b/tailbone/templates/upgrades/view.mako @@ -66,7 +66,7 @@

- Upgrading (please wait) ... + Upgrading ${app_title} (please wait) ... {{ executeUpgradeComplete ? "DONE!" : "" }}

{ this.adjustTextoutHeight() }) From cd466a64e53406d98aca0f6e9af8724a398d27f3 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 25 Dec 2022 12:45:23 -0600 Subject: [PATCH 032/794] Filter by person instead of user, for Generated Reports "Created by" --- tailbone/views/exports.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/tailbone/views/exports.py b/tailbone/views/exports.py index 3f6d417c..82591099 100644 --- a/tailbone/views/exports.py +++ b/tailbone/views/exports.py @@ -31,12 +31,9 @@ import shutil import six -from rattail.db import model - from pyramid.response import FileResponse -from webhelpers2.html import HTML, tags +from webhelpers2.html import tags -from tailbone import forms from tailbone.views import MasterView @@ -49,6 +46,11 @@ class ExportMasterView(MasterView): downloadable = False delete_export_files = False + labels = { + 'id': "ID", + 'created_by': "Created by", + } + grid_columns = [ 'id', 'created', @@ -82,19 +84,23 @@ class ExportMasterView(MasterView): def configure_grid(self, g): super(ExportMasterView, self).configure_grid(g) + model = self.model - g.joiners['created_by'] = lambda q: q.join(model.User) - g.sorters['created_by'] = g.make_sorter(model.User.username) - g.filters['created_by'] = g.make_filter('created_by', model.User.username) + # id + g.set_renderer('id', self.render_id) + g.set_link('id') + + # filename + g.set_link('filename') + + # created g.set_sort_defaults('created', 'desc') - g.set_renderer('id', self.render_id) - - g.set_label('id', "ID") - g.set_label('created_by', "Created by") - - g.set_link('id') - g.set_link('filename') + # created_by + g.set_joiner('created_by', + lambda q: q.join(model.User).outerjoin(model.Person)) + g.set_sorter('created_by', model.Person.display_name) + g.set_filter('created_by', model.Person.display_name) def render_id(self, export, field): return export.id_str From 8264a69ceca86ee95a049687f7dab0d0542f8a36 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 25 Dec 2022 14:41:58 -0600 Subject: [PATCH 033/794] Add "direct link" support for master grids --- tailbone/grids/core.py | 4 +- tailbone/templates/grids/buefy.mako | 133 ++++++++++++++------ tailbone/templates/grids/filters_buefy.mako | 2 +- tailbone/views/master.py | 4 + 4 files changed, 105 insertions(+), 38 deletions(-) diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index 54f578ed..78fd2cc6 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -189,6 +189,7 @@ class Grid(object): clicking_row_checks_box=False, click_handlers=None, main_actions=[], more_actions=[], delete_speedbump=False, ajax_data_url=None, component='tailbone-grid', + expose_direct_link=False, **kwargs): self.key = key @@ -256,11 +257,12 @@ class Grid(object): if ajax_data_url: self.ajax_data_url = ajax_data_url elif self.request: - self.ajax_data_url = self.request.current_route_url() + self.ajax_data_url = self.request.current_route_url(_query=None) else: self.ajax_data_url = '' self.component = component + self.expose_direct_link = expose_direct_link self._whgrid_kwargs = kwargs @property diff --git a/tailbone/templates/grids/buefy.mako b/tailbone/templates/grids/buefy.mako index 12231606..c99d0f70 100644 --- a/tailbone/templates/grids/buefy.mako +++ b/tailbone/templates/grids/buefy.mako @@ -289,26 +289,41 @@ - % if grid.pageable: -