Compare commits

..

No commits in common. "master" and "v0.19.2" have entirely different histories.

143 changed files with 2606 additions and 3724 deletions

View file

@ -5,187 +5,6 @@ All notable changes to Tailbone will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## v0.22.7 (2025-02-19)
### Fix
- stop using old config for logo image url on login page
- fix warning msg for deprecated Grid param
## v0.22.6 (2025-02-01)
### Fix
- register vue3 form component for products -> make batch
## v0.22.5 (2024-12-16)
### Fix
- whoops this is latest rattail
- require newer rattail lib
- require newer wuttaweb
- let caller request safe HTML literal for rendered grid table
## v0.22.4 (2024-11-22)
### Fix
- avoid error in product search for duplicated key
- use vmodel for confirm password widget input
## v0.22.3 (2024-11-19)
### Fix
- avoid error for trainwreck query when not a customer
## v0.22.2 (2024-11-18)
### Fix
- use local/custom enum for continuum operations
- add basic master view for Product Costs
- show continuum operation type when viewing version history
- always define `app` attr for ViewSupplement
- avoid deprecated import
## v0.22.1 (2024-11-02)
### Fix
- fix submit button for running problem report
- avoid deprecated grid method
## v0.22.0 (2024-10-22)
### Feat
- add support for new ordering batch from parsed file
### Fix
- avoid deprecated method to suggest username
## v0.21.11 (2024-10-03)
### Fix
- custom method for adding grid action
- become/stop root should redirect to previous url
## v0.21.10 (2024-09-15)
### Fix
- update project repo links, kallithea -> forgejo
- use better icon for submit button on login page
- wrap notes text for batch view
- expose datasync consumer batch size via configure page
## v0.21.9 (2024-08-28)
### Fix
- render custom attrs in form component tag
## v0.21.8 (2024-08-28)
### Fix
- ignore session kwarg for `MasterView.make_row_grid()`
## v0.21.7 (2024-08-28)
### Fix
- avoid error when form value cannot be obtained
## v0.21.6 (2024-08-28)
### Fix
- avoid error when grid value cannot be obtained
## v0.21.5 (2024-08-28)
### Fix
- set empty string for "-new-" file configure option
## v0.21.4 (2024-08-26)
### Fix
- handle differing email profile keys for appinfo/configure
## v0.21.3 (2024-08-26)
### Fix
- show non-standard config values for app info configure email
## v0.21.2 (2024-08-26)
### Fix
- refactor waterpark base template to use wutta feedback component
- fix input/output file upload feature for configure pages, per oruga
- tweak how grid data translates to Vue template context
- merge filters into main grid template
- add basic wutta view for users
- some fixes for wutta people view
- various fixes for waterpark theme
- avoid deprecated `component` form kwarg
## v0.21.1 (2024-08-22)
### Fix
- misc. bugfixes per recent changes
## v0.21.0 (2024-08-22)
### Feat
- move "most" filtering logic for grid class to wuttaweb
- inherit from wuttaweb templates for home, login pages
- inherit from wuttaweb for AppInfoView, appinfo/configure template
- add "has output file templates" config option for master view
### Fix
- change grid reset-view param name to match wuttaweb
- move "searchable columns" grid feature to wuttaweb
- use wuttaweb to get/render csrf token
- inherit from wuttaweb for appinfo/index template
- prefer wuttaweb config for "home redirect to login" feature
- fix master/index template rendering for waterpark theme
- fix spacing for navbar logo/title in waterpark theme
## v0.20.1 (2024-08-20)
### Fix
- fix default filter verbs logic for workorder status
## v0.20.0 (2024-08-20)
### Feat
- add new 'waterpark' theme, based on wuttaweb w/ vue2 + buefy
- refactor templates to simplify base/page/form structure
### Fix
- avoid deprecated reference to app db engine
## v0.19.3 (2024-08-19)
### Fix
- add pager stats to all grid vue data (fixes view history)
## v0.19.2 (2024-08-19) ## v0.19.2 (2024-08-19)
### Fix ### Fix

View file

@ -1,8 +1,10 @@
# Tailbone Tailbone
========
Tailbone is an extensible web application based on Rattail. It provides a Tailbone is an extensible web application based on Rattail. It provides a
"back-office network environment" (BONE) for use in managing retail data. "back-office network environment" (BONE) for use in managing retail data.
Please see Rattail's [home page](http://rattailproject.org/) for more Please see Rattail's `home page`_ for more information.
information.
.. _home page: http://rattailproject.org/

View file

@ -27,10 +27,10 @@ templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
intersphinx_mapping = { intersphinx_mapping = {
'rattail': ('https://docs.wuttaproject.org/rattail/', None), 'rattail': ('https://rattailproject.org/docs/rattail/', None),
'webhelpers2': ('https://webhelpers2.readthedocs.io/en/latest/', None), 'webhelpers2': ('https://webhelpers2.readthedocs.io/en/latest/', None),
'wuttaweb': ('https://docs.wuttaproject.org/wuttaweb/', None), 'wuttaweb': ('https://rattailproject.org/docs/wuttaweb/', None),
'wuttjamaican': ('https://docs.wuttaproject.org/wuttjamaican/', None), 'wuttjamaican': ('https://rattailproject.org/docs/wuttjamaican/', None),
} }
# allow todo entries to show up # allow todo entries to show up

View file

@ -6,9 +6,9 @@ build-backend = "hatchling.build"
[project] [project]
name = "Tailbone" name = "Tailbone"
version = "0.22.7" version = "0.19.2"
description = "Backoffice Web Application for Rattail" description = "Backoffice Web Application for Rattail"
readme = "README.md" readme = "README.rst"
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
license = {text = "GNU GPL v3+"} license = {text = "GNU GPL v3+"}
classifiers = [ classifiers = [
@ -53,13 +53,13 @@ dependencies = [
"pyramid_mako", "pyramid_mako",
"pyramid_retry", "pyramid_retry",
"pyramid_tm", "pyramid_tm",
"rattail[db,bouncer]>=0.20.1", "rattail[db,bouncer]>=0.18.1",
"sa-filters", "sa-filters",
"simplejson", "simplejson",
"transaction", "transaction",
"waitress", "waitress",
"WebHelpers2", "WebHelpers2",
"WuttaWeb>=0.21.0", "WuttaWeb>=0.10.1",
"zope.sqlalchemy>=1.5", "zope.sqlalchemy>=1.5",
] ]
@ -84,9 +84,9 @@ tailbone = "tailbone.config:ConfigExtension"
[project.urls] [project.urls]
Homepage = "https://rattailproject.org" Homepage = "https://rattailproject.org"
Repository = "https://forgejo.wuttaproject.org/rattail/tailbone" Repository = "https://kallithea.rattailproject.org/rattail-project/tailbone"
Issues = "https://forgejo.wuttaproject.org/rattail/tailbone/issues" Issues = "https://redmine.rattailproject.org/projects/tailbone/issues"
Changelog = "https://forgejo.wuttaproject.org/rattail/tailbone/src/branch/master/CHANGELOG.md" Changelog = "https://kallithea.rattailproject.org/rattail-project/tailbone/files/master/CHANGELOG.md"
[tool.commitizen] [tool.commitizen]

View file

@ -29,7 +29,8 @@ import logging
import humanize import humanize
import sqlalchemy as sa import sqlalchemy as sa
from rattail.db.model import PurchaseBatch, PurchaseBatchRow from rattail.db import model
from rattail.util import pretty_quantity
from cornice import Service from cornice import Service
from deform import widget as dfwidget from deform import widget as dfwidget
@ -44,7 +45,7 @@ log = logging.getLogger(__name__)
class ReceivingBatchViews(APIBatchView): class ReceivingBatchViews(APIBatchView):
model_class = PurchaseBatch model_class = model.PurchaseBatch
default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler' default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler'
route_prefix = 'receivingbatchviews' route_prefix = 'receivingbatchviews'
permission_prefix = 'receiving' permission_prefix = 'receiving'
@ -54,8 +55,7 @@ class ReceivingBatchViews(APIBatchView):
supports_execute = True supports_execute = True
def base_query(self): def base_query(self):
model = self.app.model query = super(ReceivingBatchViews, self).base_query()
query = super().base_query()
query = query.filter(model.PurchaseBatch.mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING) query = query.filter(model.PurchaseBatch.mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING)
return query return query
@ -85,7 +85,7 @@ class ReceivingBatchViews(APIBatchView):
# assume "receive from PO" if given a PO key # assume "receive from PO" if given a PO key
if data.get('purchase_key'): if data.get('purchase_key'):
data['workflow'] = 'from_po' data['receiving_workflow'] = 'from_po'
return super().create_object(data) return super().create_object(data)
@ -120,7 +120,6 @@ class ReceivingBatchViews(APIBatchView):
return self._get(obj=batch) return self._get(obj=batch)
def eligible_purchases(self): def eligible_purchases(self):
model = self.app.model
uuid = self.request.params.get('vendor_uuid') uuid = self.request.params.get('vendor_uuid')
vendor = self.Session.get(model.Vendor, uuid) if uuid else None vendor = self.Session.get(model.Vendor, uuid) if uuid else None
if not vendor: if not vendor:
@ -177,7 +176,7 @@ class ReceivingBatchViews(APIBatchView):
class ReceivingBatchRowViews(APIBatchRowView): class ReceivingBatchRowViews(APIBatchRowView):
model_class = PurchaseBatchRow model_class = model.PurchaseBatchRow
default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler' default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler'
route_prefix = 'receiving.rows' route_prefix = 'receiving.rows'
permission_prefix = 'receiving' permission_prefix = 'receiving'
@ -186,8 +185,7 @@ class ReceivingBatchRowViews(APIBatchRowView):
supports_quick_entry = True supports_quick_entry = True
def make_filter_spec(self): def make_filter_spec(self):
model = self.app.model filters = super(ReceivingBatchRowViews, self).make_filter_spec()
filters = super().make_filter_spec()
if filters: if filters:
# must translate certain convenience filters # must translate certain convenience filters
@ -298,11 +296,11 @@ class ReceivingBatchRowViews(APIBatchRowView):
return filters return filters
def normalize(self, row): def normalize(self, row):
data = super().normalize(row) data = super(ReceivingBatchRowViews, self).normalize(row)
model = self.app.model
batch = row.batch batch = row.batch
prodder = self.app.get_products_handler() app = self.get_rattail_app()
prodder = app.get_products_handler()
data['product_uuid'] = row.product_uuid data['product_uuid'] = row.product_uuid
data['item_id'] = row.item_id data['item_id'] = row.item_id
@ -377,7 +375,7 @@ class ReceivingBatchRowViews(APIBatchRowView):
if accounted_for: if accounted_for:
# some product accounted for; button should receive "remainder" only # some product accounted for; button should receive "remainder" only
if remainder: if remainder:
remainder = self.app.render_quantity(remainder) remainder = pretty_quantity(remainder)
data['quick_receive_quantity'] = remainder data['quick_receive_quantity'] = remainder
data['quick_receive_text'] = "Receive Remainder ({} {})".format( data['quick_receive_text'] = "Receive Remainder ({} {})".format(
remainder, data['unit_uom']) remainder, data['unit_uom'])
@ -388,7 +386,7 @@ class ReceivingBatchRowViews(APIBatchRowView):
else: # nothing yet accounted for, button should receive "all" else: # nothing yet accounted for, button should receive "all"
if not remainder: if not remainder:
log.warning("quick receive remainder is empty for row %s", row.uuid) log.warning("quick receive remainder is empty for row %s", row.uuid)
remainder = self.app.render_quantity(remainder) remainder = pretty_quantity(remainder)
data['quick_receive_quantity'] = remainder data['quick_receive_quantity'] = remainder
data['quick_receive_text'] = "Receive ALL ({} {})".format( data['quick_receive_text'] = "Receive ALL ({} {})".format(
remainder, data['unit_uom']) remainder, data['unit_uom'])
@ -416,7 +414,7 @@ class ReceivingBatchRowViews(APIBatchRowView):
data['received_alert'] = None data['received_alert'] = None
if self.batch_handler.get_units_confirmed(row): if self.batch_handler.get_units_confirmed(row):
msg = "You have already received some of this product; last update was {}.".format( msg = "You have already received some of this product; last update was {}.".format(
humanize.naturaltime(self.app.make_utc() - row.modified)) humanize.naturaltime(app.make_utc() - row.modified))
data['received_alert'] = msg data['received_alert'] = msg
return data return data
@ -425,8 +423,6 @@ class ReceivingBatchRowViews(APIBatchRowView):
""" """
View which handles "receiving" against a particular batch row. View which handles "receiving" against a particular batch row.
""" """
model = self.app.model
# first do basic input validation # first do basic input validation
schema = ReceiveRow().bind(session=self.Session()) schema = ReceiveRow().bind(session=self.Session())
form = forms.Form(schema=schema, request=self.request) form = forms.Form(schema=schema, request=self.request)

View file

@ -26,6 +26,7 @@ Tailbone Web API - Master View
import json import json
from rattail.config import parse_bool
from rattail.db.util import get_fieldnames from rattail.db.util import get_fieldnames
from cornice import resource, Service from cornice import resource, Service
@ -184,7 +185,7 @@ class APIMasterView(APIView):
if sortcol: if sortcol:
spec = { spec = {
'field': sortcol.field_name, 'field': sortcol.field_name,
'direction': 'asc' if self.config.parse_bool(self.request.params['ascending']) else 'desc', 'direction': 'asc' if parse_bool(self.request.params['ascending']) else 'desc',
} }
if sortcol.model_name: if sortcol.model_name:
spec['model'] = sortcol.model_name spec['model'] = sortcol.model_name

View file

@ -62,20 +62,9 @@ def make_rattail_config(settings):
# nb. this is for compaibility with wuttaweb # nb. this is for compaibility with wuttaweb
settings['wutta_config'] = rattail_config settings['wutta_config'] = rattail_config
# must import all sqlalchemy models before things get rolling,
# otherwise can have errors about continuum TransactionMeta class
# not yet mapped, when relevant pages are first requested...
# cf. https://docs.pylonsproject.org/projects/pyramid_cookbook/en/latest/database/sqlalchemy.html#importing-all-sqlalchemy-models
# hat tip to https://stackoverflow.com/a/59241485
if getattr(rattail_config, 'tempmon_engine', None):
from rattail_tempmon.db import model as tempmon_model, Session as TempmonSession
tempmon_session = TempmonSession()
tempmon_session.query(tempmon_model.Appliance).first()
tempmon_session.close()
# configure database sessions # configure database sessions
if hasattr(rattail_config, 'appdb_engine'): if hasattr(rattail_config, 'rattail_engine'):
tailbone.db.Session.configure(bind=rattail_config.appdb_engine) tailbone.db.Session.configure(bind=rattail_config.rattail_engine)
if hasattr(rattail_config, 'trainwreck_engine'): if hasattr(rattail_config, 'trainwreck_engine'):
tailbone.db.TrainwreckSession.configure(bind=rattail_config.trainwreck_engine) tailbone.db.TrainwreckSession.configure(bind=rattail_config.trainwreck_engine)
if hasattr(rattail_config, 'tempmon_engine'): if hasattr(rattail_config, 'tempmon_engine'):
@ -332,8 +321,7 @@ def main(global_config, **settings):
""" """
This function returns a Pyramid WSGI application. This function returns a Pyramid WSGI application.
""" """
settings.setdefault('mako.directories', ['tailbone:templates', settings.setdefault('mako.directories', ['tailbone:templates'])
'wuttaweb:templates'])
rattail_config = make_rattail_config(settings) rattail_config = make_rattail_config(settings)
pyramid_config = make_pyramid_config(settings) pyramid_config = make_pyramid_config(settings)
pyramid_config.include('tailbone') pyramid_config.include('tailbone')

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# Rattail -- Retail Software Framework # Rattail -- Retail Software Framework
# Copyright © 2010-2024 Lance Edgar # Copyright © 2010-2023 Lance Edgar
# #
# This file is part of Rattail. # This file is part of Rattail.
# #
@ -270,21 +270,9 @@ class VersionDiff(Diff):
for field in self.fields: for field in self.fields:
values[field] = {'before': self.render_old_value(field), values[field] = {'before': self.render_old_value(field),
'after': self.render_new_value(field)} 'after': self.render_new_value(field)}
operation = None
if self.version.operation_type == continuum.Operation.INSERT:
operation = 'INSERT'
elif self.version.operation_type == continuum.Operation.UPDATE:
operation = 'UPDATE'
elif self.version.operation_type == continuum.Operation.DELETE:
operation = 'DELETE'
else:
operation = self.version.operation_type
return { return {
'key': id(self.version), 'key': id(self.version),
'model_title': self.title, 'model_title': self.title,
'operation': operation,
'diff_class': self.nature, 'diff_class': self.nature,
'fields': self.fields, 'fields': self.fields,
'values': values, 'values': values,

View file

@ -401,8 +401,6 @@ class Form(object):
self.edit_help_url = edit_help_url self.edit_help_url = edit_help_url
self.route_prefix = route_prefix self.route_prefix = route_prefix
self.button_icon_submit = kwargs.get('button_icon_submit', 'save')
def __iter__(self): def __iter__(self):
return iter(self.fields) return iter(self.fields)
@ -907,8 +905,7 @@ class Form(object):
def render_vue_template(self, template='/forms/deform.mako', **context): def render_vue_template(self, template='/forms/deform.mako', **context):
""" """ """ """
output = self.render_deform(template=template, **context) return self.render_deform(template=template, **context)
return HTML.literal(output)
def render_deform(self, dform=None, template=None, **kwargs): def render_deform(self, dform=None, template=None, **kwargs):
if not template: if not template:
@ -1039,9 +1036,9 @@ class Form(object):
def render_vue_tag(self, **kwargs): def render_vue_tag(self, **kwargs):
""" """ """ """
return self.render_vuejs_component(**kwargs) return self.render_vuejs_component()
def render_vuejs_component(self, **kwargs): def render_vuejs_component(self):
""" """
Render the Vue.js component HTML for the form. Render the Vue.js component HTML for the form.
@ -1052,11 +1049,10 @@ class Form(object):
<tailbone-form :configure-fields-help="configureFieldsHelp"> <tailbone-form :configure-fields-help="configureFieldsHelp">
</tailbone-form> </tailbone-form>
""" """
kw = dict(self.vuejs_component_kwargs) kwargs = dict(self.vuejs_component_kwargs)
kw.update(kwargs)
if self.can_edit_help: if self.can_edit_help:
kw.setdefault(':configure-fields-help', 'configureFieldsHelp') kwargs.setdefault(':configure-fields-help', 'configureFieldsHelp')
return HTML.tag(self.vue_tagname, **kw) return HTML.tag(self.vue_tagname, **kwargs)
def set_json_data(self, key, value): def set_json_data(self, key, value):
""" """
@ -1224,18 +1220,6 @@ class Form(object):
# TODO: again, why does serialize() not return literal? # TODO: again, why does serialize() not return literal?
return HTML.literal(field.serialize()) return HTML.literal(field.serialize())
# TODO: this was copied from wuttaweb; can remove when we align
# Form class structure
def render_vue_finalize(self):
""" """
set_data = f"{self.vue_component}.data = function() {{ return {self.vue_component}Data }}"
make_component = f"Vue.component('{self.vue_tagname}', {self.vue_component})"
return HTML.tag('script', c=['\n',
HTML.literal(set_data),
'\n',
HTML.literal(make_component),
'\n'])
def render_field_readonly(self, field_name, **kwargs): def render_field_readonly(self, field_name, **kwargs):
""" """
Render the given field completely, but in read-only fashion. Render the given field completely, but in read-only fashion.
@ -1383,11 +1367,7 @@ class Form(object):
return getattr(record, field_name) return getattr(record, field_name)
except AttributeError: except AttributeError:
pass pass
return record[field_name]
try:
return record[field_name]
except TypeError:
pass
# TODO: is this always safe to do? # TODO: is this always safe to do?
elif self.defaults and field_name in self.defaults: elif self.defaults and field_name in self.defaults:

View file

@ -24,10 +24,9 @@
Core Grid Classes Core Grid Classes
""" """
import inspect
import logging
import warnings
from urllib.parse import urlencode from urllib.parse import urlencode
import warnings
import logging
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy import orm from sqlalchemy import orm
@ -197,7 +196,11 @@ class Grid(WuttaGrid):
raw_renderers={}, raw_renderers={},
extra_row_class=None, extra_row_class=None,
url='#', url='#',
joiners={},
filterable=False,
filters={},
use_byte_string_filters=False, use_byte_string_filters=False,
searchable={},
checkboxes=False, checkboxes=False,
checked=None, checked=None,
check_handler=None, check_handler=None,
@ -213,59 +216,48 @@ class Grid(WuttaGrid):
expose_direct_link=False, expose_direct_link=False,
**kwargs, **kwargs,
): ):
if 'component' in kwargs: if kwargs.get('component'):
warnings.warn("component param is deprecated for Grid(); " warnings.warn("component param is deprecated for Grid(); "
"please use vue_tagname param instead", "please use vue_tagname param instead",
DeprecationWarning, stacklevel=2) DeprecationWarning, stacklevel=2)
kwargs.setdefault('vue_tagname', kwargs.pop('component')) kwargs.setdefault('vue_tagname', kwargs.pop('component'))
if 'default_sortkey' in kwargs: if kwargs.get('default_sortkey'):
warnings.warn("default_sortkey param is deprecated for Grid(); " warnings.warn("default_sortkey param is deprecated for Grid(); "
"please use sort_defaults param instead", "please use sort_defaults param instead",
DeprecationWarning, stacklevel=2) DeprecationWarning, stacklevel=2)
if 'default_sortdir' in kwargs: if kwargs.get('default_sortdir'):
warnings.warn("default_sortdir param is deprecated for Grid(); " warnings.warn("default_sortdir param is deprecated for Grid(); "
"please use sort_defaults param instead", "please use sort_defaults param instead",
DeprecationWarning, stacklevel=2) DeprecationWarning, stacklevel=2)
if 'default_sortkey' in kwargs or 'default_sortdir' in kwargs: if kwargs.get('default_sortkey') or kwargs.get('default_sortdir'):
sortkey = kwargs.pop('default_sortkey', None) sortkey = kwargs.pop('default_sortkey', None)
sortdir = kwargs.pop('default_sortdir', 'asc') sortdir = kwargs.pop('default_sortdir', 'asc')
if sortkey: if sortkey:
kwargs.setdefault('sort_defaults', [(sortkey, sortdir)]) kwargs.setdefault('sort_defaults', [(sortkey, sortdir)])
if 'pageable' in kwargs: if kwargs.get('pageable'):
warnings.warn("pageable param is deprecated for Grid(); " warnings.warn("component param is deprecated for Grid(); "
"please use paginated param instead", "please use vue_tagname param instead",
DeprecationWarning, stacklevel=2) DeprecationWarning, stacklevel=2)
kwargs.setdefault('paginated', kwargs.pop('pageable')) kwargs.setdefault('paginated', kwargs.pop('pageable'))
if 'default_pagesize' in kwargs: if kwargs.get('default_pagesize'):
warnings.warn("default_pagesize param is deprecated for Grid(); " warnings.warn("default_pagesize param is deprecated for Grid(); "
"please use pagesize param instead", "please use pagesize param instead",
DeprecationWarning, stacklevel=2) DeprecationWarning, stacklevel=2)
kwargs.setdefault('pagesize', kwargs.pop('default_pagesize')) kwargs.setdefault('pagesize', kwargs.pop('default_pagesize'))
if 'default_page' in kwargs: if kwargs.get('default_page'):
warnings.warn("default_page param is deprecated for Grid(); " warnings.warn("default_page param is deprecated for Grid(); "
"please use page param instead", "please use page param instead",
DeprecationWarning, stacklevel=2) DeprecationWarning, stacklevel=2)
kwargs.setdefault('page', kwargs.pop('default_page')) kwargs.setdefault('page', kwargs.pop('default_page'))
if 'searchable' in kwargs:
warnings.warn("searchable param is deprecated for Grid(); "
"please use searchable_columns param instead",
DeprecationWarning, stacklevel=2)
kwargs.setdefault('searchable_columns', kwargs.pop('searchable'))
# TODO: this should not be needed once all templates correctly # TODO: this should not be needed once all templates correctly
# reference grid.vue_component etc. # reference grid.vue_component etc.
kwargs.setdefault('vue_tagname', 'tailbone-grid') kwargs.setdefault('vue_tagname', 'tailbone-grid')
# nb. these must be set before super init, as they are
# referenced when constructing filters
self.assume_local_times = assume_local_times
self.use_byte_string_filters = use_byte_string_filters
kwargs['key'] = key kwargs['key'] = key
kwargs['data'] = data kwargs['data'] = data
super().__init__(request, **kwargs) super().__init__(request, **kwargs)
@ -283,11 +275,19 @@ class Grid(WuttaGrid):
self.width = width self.width = width
self.enums = enums or {} self.enums = enums or {}
self.assume_local_times = assume_local_times
self.renderers = self.make_default_renderers(self.renderers) self.renderers = self.make_default_renderers(self.renderers)
self.raw_renderers = raw_renderers or {} self.raw_renderers = raw_renderers or {}
self.invisible = invisible or [] self.invisible = invisible or []
self.extra_row_class = extra_row_class self.extra_row_class = extra_row_class
self.url = url self.url = url
self.joiners = joiners or {}
self.filterable = filterable
self.use_byte_string_filters = use_byte_string_filters
self.filters = self.make_filters(filters)
self.searchable = searchable or {}
self.checkboxes = checkboxes self.checkboxes = checkboxes
self.checked = checked self.checked = checked
@ -443,14 +443,10 @@ class Grid(WuttaGrid):
self.remove(oldfield) self.remove(oldfield)
def set_joiner(self, key, joiner): def set_joiner(self, key, joiner):
""" """
if joiner is None: if joiner is None:
warnings.warn("specifying None is deprecated for Grid.set_joiner(); " self.joiners.pop(key, None)
"please use Grid.remove_joiner() instead",
DeprecationWarning, stacklevel=2)
self.remove_joiner(key)
else: else:
super().set_joiner(key, joiner) self.joiners[key] = joiner
def set_sorter(self, key, *args, **kwargs): def set_sorter(self, key, *args, **kwargs):
""" """ """ """
@ -478,19 +474,41 @@ class Grid(WuttaGrid):
self.sorters[key] = self.make_sorter(*args, **kwargs) self.sorters[key] = self.make_sorter(*args, **kwargs)
def set_filter(self, key, *args, **kwargs): def set_filter(self, key, *args, **kwargs):
""" """ if len(args) == 1 and args[0] is None:
if len(args) == 1: self.remove_filter(key)
if args[0] is None: else:
warnings.warn("specifying None is deprecated for Grid.set_filter(); " if 'label' not in kwargs and key in self.labels:
"please use Grid.remove_filter() instead", kwargs['label'] = self.labels[key]
DeprecationWarning, stacklevel=2) self.filters[key] = self.make_filter(key, *args, **kwargs)
self.remove_filter(key)
return
# TODO: our make_filter() signature differs from upstream, def set_searchable(self, key, searchable=True):
# so must call it explicitly instead of delegating to super if searchable:
kwargs.setdefault('label', self.get_label(key)) self.searchable[key] = True
self.filters[key] = self.make_filter(key, *args, **kwargs) else:
self.searchable.pop(key, None)
def is_searchable(self, key):
return self.searchable.get(key, False)
def remove_filter(self, key):
self.filters.pop(key, None)
def set_label(self, key, label, column_only=False):
"""
Set/override the label for a column.
This overrides
:meth:`~wuttaweb:wuttaweb.grids.base.Grid.set_label()` to add
the following params:
:param column_only: Boolean indicating whether the label
should be applied *only* to the column header (if
``True``), vs. applying also to the filter (if ``False``).
"""
super().set_label(key, label)
if not column_only and key in self.filters:
self.filters[key].label = label
def set_click_handler(self, key, handler): def set_click_handler(self, key, handler):
if handler: if handler:
@ -575,11 +593,7 @@ class Grid(WuttaGrid):
return getattr(obj, column_name) return getattr(obj, column_name)
except AttributeError: except AttributeError:
pass pass
return obj[column_name]
try:
return obj[column_name]
except TypeError:
pass
def render_currency(self, obj, column_name): def render_currency(self, obj, column_name):
value = self.obtain_value(obj, column_name) value = self.obtain_value(obj, column_name)
@ -694,14 +708,6 @@ class Grid(WuttaGrid):
def actions_column_format(self, column_number, row_number, item): def actions_column_format(self, column_number, row_number, item):
return HTML.td(self.render_actions(item, row_number), class_='actions') return HTML.td(self.render_actions(item, row_number), class_='actions')
# TODO: upstream should handle this..
def make_backend_filters(self, filters=None):
""" """
final = self.get_default_filters()
if filters:
final.update(filters)
return final
def get_default_filters(self): def get_default_filters(self):
""" """
Returns the default set of filters provided by the grid. Returns the default set of filters provided by the grid.
@ -726,6 +732,16 @@ class Grid(WuttaGrid):
filters[prop.key] = self.make_filter(prop.key, column) filters[prop.key] = self.make_filter(prop.key, column)
return filters return filters
def make_filters(self, filters=None):
"""
Returns an initial set of filters which will be available to the grid.
The grid itself may or may not provide some default filters, and the
``filters`` kwarg may contain additions and/or overrides.
"""
if filters:
return filters
return self.get_default_filters()
def make_filter(self, key, column, **kwargs): def make_filter(self, key, column, **kwargs):
""" """
Make a filter suitable for use with the given column. Make a filter suitable for use with the given column.
@ -863,13 +879,9 @@ class Grid(WuttaGrid):
settings['page'] = self.page settings['page'] = self.page
if self.filterable: if self.filterable:
for filtr in self.iter_filters(): for filtr in self.iter_filters():
defaults = self.filter_defaults.get(filtr.key, {}) settings['filter.{}.active'.format(filtr.key)] = filtr.default_active
settings[f'filter.{filtr.key}.active'] = defaults.get('active', settings['filter.{}.verb'.format(filtr.key)] = filtr.default_verb
filtr.default_active) settings['filter.{}.value'.format(filtr.key)] = filtr.default_value
settings[f'filter.{filtr.key}.verb'] = defaults.get('verb',
filtr.default_verb)
settings[f'filter.{filtr.key}.value'] = defaults.get('value',
filtr.default_value)
# If user has default settings on file, apply those first. # If user has default settings on file, apply those first.
if self.user_has_defaults(): if self.user_has_defaults():
@ -877,13 +889,13 @@ class Grid(WuttaGrid):
# If request contains instruction to reset to default filters, then we # If request contains instruction to reset to default filters, then we
# can skip the rest of the request/session checks. # can skip the rest of the request/session checks.
if self.request.GET.get('reset-view'): if self.request.GET.get('reset-to-default-filters') == 'true':
pass pass
# If request has filter settings, grab those, then grab sort/pager # If request has filter settings, grab those, then grab sort/pager
# settings from request or session. # settings from request or session.
elif self.request_has_settings('filter'): elif self.filterable and self.request_has_settings('filter'):
self.update_filter_settings(settings, src='request') self.update_filter_settings(settings, 'request')
if self.request_has_settings('sort'): if self.request_has_settings('sort'):
self.update_sort_settings(settings, src='request') self.update_sort_settings(settings, src='request')
else: else:
@ -895,7 +907,7 @@ class Grid(WuttaGrid):
# settings from request or session. # settings from request or session.
elif self.request_has_settings('sort'): elif self.request_has_settings('sort'):
self.update_sort_settings(settings, src='request') self.update_sort_settings(settings, src='request')
self.update_filter_settings(settings, src='session') self.update_filter_settings(settings, 'session')
self.update_page_settings(settings) self.update_page_settings(settings)
# NOTE: These next two are functionally equivalent, but are kept # NOTE: These next two are functionally equivalent, but are kept
@ -905,12 +917,12 @@ class Grid(WuttaGrid):
# grab those, then grab filter/sort settings from session. # grab those, then grab filter/sort settings from session.
elif self.request_has_settings('page'): elif self.request_has_settings('page'):
self.update_page_settings(settings) self.update_page_settings(settings)
self.update_filter_settings(settings, src='session') self.update_filter_settings(settings, 'session')
self.update_sort_settings(settings, src='session') self.update_sort_settings(settings, src='session')
# If request has no settings, grab all from session. # If request has no settings, grab all from session.
elif self.session_has_settings(): elif self.session_has_settings():
self.update_filter_settings(settings, src='session') self.update_filter_settings(settings, 'session')
self.update_sort_settings(settings, src='session') self.update_sort_settings(settings, src='session')
self.update_page_settings(settings) self.update_page_settings(settings)
@ -1050,11 +1062,18 @@ class Grid(WuttaGrid):
merge('page', int) merge('page', int)
def request_has_settings(self, type_): def request_has_settings(self, type_):
""" """ """
if super().request_has_settings(type_): Determine if the current request (GET query string) contains any
return True filter/sort settings for the grid.
"""
if type_ == 'filter':
for filtr in self.iter_filters():
if filtr.key in self.request.GET:
return True
if 'filter' in self.request.GET: # user may be applying empty filters
return True
if type_ == 'sort': elif type_ == 'sort':
# TODO: remove this eventually, but some links in the wild # TODO: remove this eventually, but some links in the wild
# may still include these params, so leave it for now # may still include these params, so leave it for now
@ -1062,6 +1081,14 @@ class Grid(WuttaGrid):
if key in self.request.GET: if key in self.request.GET:
return True return True
if 'sort1key' in self.request.GET:
return True
elif type_ == 'page':
for key in ['pagesize', 'page']:
if key in self.request.GET:
return True
return False return False
def session_has_settings(self): def session_has_settings(self):
@ -1077,6 +1104,72 @@ class Grid(WuttaGrid):
return any([key.startswith(f'{prefix}.filter') return any([key.startswith(f'{prefix}.filter')
for key in self.request.session]) for key in self.request.session])
def update_filter_settings(self, settings, source):
"""
Updates a settings dictionary according to filter settings data found
in either the GET query string, or session storage.
:param settings: Dictionary of initial settings, which is to be updated.
:param source: String identifying the source to consult for settings
data. Must be one of: ``('request', 'session')``.
"""
if not self.filterable:
return
for filtr in self.iter_filters():
prefix = 'filter.{}'.format(filtr.key)
if source == 'request':
# consider filter active if query string contains a value for it
settings['{}.active'.format(prefix)] = filtr.key in self.request.GET
settings['{}.verb'.format(prefix)] = self.get_setting(
settings, f'{filtr.key}.verb', src='request', default='')
settings['{}.value'.format(prefix)] = self.get_setting(
settings, filtr.key, src='request', default='')
else: # source = session
settings['{}.active'.format(prefix)] = self.get_setting(
settings, f'{prefix}.active', src='session',
normalize=lambda v: str(v).lower() == 'true', default=False)
settings['{}.verb'.format(prefix)] = self.get_setting(
settings, f'{prefix}.verb', src='session', default='')
settings['{}.value'.format(prefix)] = self.get_setting(
settings, f'{prefix}.value', src='session', default='')
def update_page_settings(self, settings):
"""
Updates a settings dictionary according to pager settings data found in
either the GET query string, or session storage.
Note that due to how the actual pager functions, the effective settings
will often come from *both* the request and session. This is so that
e.g. the page size will remain constant (coming from the session) while
the user jumps between pages (which only provides the single setting).
:param settings: Dictionary of initial settings, which is to be updated.
"""
if not self.paginated:
return
pagesize = self.request.GET.get('pagesize')
if pagesize is not None:
if pagesize.isdigit():
settings['pagesize'] = int(pagesize)
else:
pagesize = self.request.session.get('grid.{}.pagesize'.format(self.key))
if pagesize is not None:
settings['pagesize'] = pagesize
page = self.request.GET.get('page')
if page is not None:
if page.isdigit():
settings['page'] = int(page)
else:
page = self.request.session.get('grid.{}.page'.format(self.key))
if page is not None:
settings['page'] = int(page)
def persist_settings(self, settings, dest='session'): def persist_settings(self, settings, dest='session'):
""" """ """ """
if dest not in ('defaults', 'session'): if dest not in ('defaults', 'session'):
@ -1164,12 +1257,89 @@ class Grid(WuttaGrid):
return data return data
def make_visible_data(self): def sort_data(self, data, sorters=None):
""" """ """ """
warnings.warn("grid.make_visible_data() method is deprecated; " if sorters is None:
"please use grid.get_visible_data() instead", sorters = self.active_sorters
DeprecationWarning, stacklevel=2) if not sorters:
return self.get_visible_data() return data
# nb. when data is a query, we want to apply sorters in the
# requested order, so the final query has order_by() in the
# correct "as-is" sequence. however when data is a list we
# must do the opposite, applying in the reverse order, so the
# final list has the most "important" sort(s) applied last.
if not isinstance(data, orm.Query):
sorters = reversed(sorters)
for sorter in sorters:
sortkey = sorter['key']
sortdir = sorter['dir']
# cannot sort unless we have a sorter callable
sortfunc = self.sorters.get(sortkey)
if not sortfunc:
return data
# join appropriate model if needed
if sortkey in self.joiners and sortkey not in self.joined:
data = self.joiners[sortkey](data)
self.joined.add(sortkey)
# invoke the sorter
data = sortfunc(data, sortdir)
return data
def paginate_data(self, data):
"""
Paginate the given data set according to current settings, and return
the result.
"""
# we of course assume our current page is correct, at first
pager = self.make_pager(data)
# if pager has detected that our current page is outside the valid
# range, we must re-orient ourself around the "new" (valid) page
if pager.page != self.page:
self.page = pager.page
self.request.session['grid.{}.page'.format(self.key)] = self.page
pager = self.make_pager(data)
return pager
def make_pager(self, data):
# TODO: this seems hacky..normally we expect `data` to be a
# query of course, but in some cases it may be a list instead.
# if so then we can't use ORM pager
if isinstance(data, list):
import paginate
return paginate.Page(data,
items_per_page=self.pagesize,
page=self.page)
return SqlalchemyOrmPage(data,
items_per_page=self.pagesize,
page=self.page,
url_maker=URLMaker(self.request))
def make_visible_data(self):
"""
Apply various settings to the raw data set, to produce a final data
set. This will page / sort / filter as necessary, according to the
grid's defaults and the current request etc.
"""
self.joined = set()
data = self.data
if self.filterable:
data = self.filter_data(data)
if self.sortable:
data = self.sort_data(data)
if self.paginated:
self.pager = self.paginate_data(data)
data = self.pager
return data
def render_vue_tag(self, master=None, **kwargs): def render_vue_tag(self, master=None, **kwargs):
""" """ """ """
@ -1192,7 +1362,7 @@ class Grid(WuttaGrid):
includes the context menu items and grid tools. includes the context menu items and grid tools.
""" """
if 'grid_columns' not in kwargs: if 'grid_columns' not in kwargs:
kwargs['grid_columns'] = self.get_vue_columns() kwargs['grid_columns'] = self.get_table_columns()
if 'grid_data' not in kwargs: if 'grid_data' not in kwargs:
kwargs['grid_data'] = self.get_table_data() kwargs['grid_data'] = self.get_table_data()
@ -1215,7 +1385,6 @@ class Grid(WuttaGrid):
return HTML.literal(html) return HTML.literal(html)
def render_buefy(self, **kwargs): def render_buefy(self, **kwargs):
""" """
warnings.warn("Grid.render_buefy() is deprecated; " warnings.warn("Grid.render_buefy() is deprecated; "
"please use Grid.render_complete() instead", "please use Grid.render_complete() instead",
DeprecationWarning, stacklevel=2) DeprecationWarning, stacklevel=2)
@ -1223,7 +1392,6 @@ class Grid(WuttaGrid):
def render_table_element(self, template='/grids/b-table.mako', def render_table_element(self, template='/grids/b-table.mako',
data_prop='gridData', empty_labels=False, data_prop='gridData', empty_labels=False,
literal=False,
**kwargs): **kwargs):
""" """
This is intended for ad-hoc "small" grids with static data. Renders This is intended for ad-hoc "small" grids with static data. Renders
@ -1235,15 +1403,12 @@ class Grid(WuttaGrid):
context['data_prop'] = data_prop context['data_prop'] = data_prop
context['empty_labels'] = empty_labels context['empty_labels'] = empty_labels
if 'grid_columns' not in context: if 'grid_columns' not in context:
context['grid_columns'] = self.get_vue_columns() context['grid_columns'] = self.get_table_columns()
context.setdefault('paginated', False) context.setdefault('paginated', False)
if context['paginated']: if context['paginated']:
context.setdefault('per_page', 20) context.setdefault('per_page', 20)
context['view_click_handler'] = self.get_view_click_handler() context['view_click_handler'] = self.get_view_click_handler()
result = render(template, context) return render(template, context)
if literal:
result = HTML.literal(result)
return result
def get_view_click_handler(self): def get_view_click_handler(self):
""" """ """ """
@ -1252,7 +1417,7 @@ class Grid(WuttaGrid):
view = None view = None
for action in self.actions: for action in self.actions:
if action.key == 'view': if action.key == 'view':
return getattr(action, 'click_handler', None) return action.click_handler
def set_filters_sequence(self, filters, only=False): def set_filters_sequence(self, filters, only=False):
""" """
@ -1326,6 +1491,28 @@ class Grid(WuttaGrid):
return data return data
def render_filters(self, template='/grids/filters.mako', **kwargs):
"""
Render the filters to a Unicode string, using the specified template.
Additional kwargs are passed along as context to the template.
"""
# Provide default data to filters form, so renderer can do some of the
# work for us.
data = {}
for filtr in self.iter_active_filters():
data['{}.active'.format(filtr.key)] = filtr.active
data['{}.verb'.format(filtr.key)] = filtr.verb
data[filtr.key] = filtr.value
form = gridfilters.GridFiltersForm(self.filters,
request=self.request,
defaults=data)
kwargs['request'] = self.request
kwargs['grid'] = self
kwargs['form'] = form
return render(template, kwargs)
def render_actions(self, row, i): # pragma: no cover def render_actions(self, row, i): # pragma: no cover
""" """ """ """
warnings.warn("grid.render_actions() is deprecated!", warnings.warn("grid.render_actions() is deprecated!",
@ -1387,19 +1574,22 @@ class Grid(WuttaGrid):
def get_vue_columns(self): def get_vue_columns(self):
""" """ """ """
columns = super().get_vue_columns() return self.get_table_columns()
for column in columns:
column['visible'] = column['field'] not in self.invisible
return columns
def get_table_columns(self): def get_table_columns(self):
""" """ """
warnings.warn("grid.get_table_columns() method is deprecated; " Return a list of dicts representing all grid columns. Meant
"please use grid.get_vue_columns() instead", for use with the client-side JS table.
DeprecationWarning, stacklevel=2) """
return self.get_vue_columns() columns = []
for name in self.columns:
columns.append({
'field': name,
'label': self.get_label(name),
'sortable': self.is_sortable(name),
'visible': name not in self.invisible,
})
return columns
def get_uuid_for_row(self, rowobj): def get_uuid_for_row(self, rowobj):
@ -1411,10 +1601,6 @@ class Grid(WuttaGrid):
if hasattr(rowobj, 'uuid'): if hasattr(rowobj, 'uuid'):
return rowobj.uuid return rowobj.uuid
def get_vue_context(self):
""" """
return self.get_table_data()
def get_vue_data(self): def get_vue_data(self):
""" """ """ """
table_data = self.get_table_data() table_data = self.get_table_data()
@ -1429,7 +1615,7 @@ class Grid(WuttaGrid):
return self._table_data return self._table_data
# filter / sort / paginate to get "visible" data # filter / sort / paginate to get "visible" data
raw_data = self.get_visible_data() raw_data = self.make_visible_data()
data = [] data = []
status_map = {} status_map = {}
checked = [] checked = []
@ -1470,22 +1656,10 @@ class Grid(WuttaGrid):
# leverage configured rendering logic where applicable; # leverage configured rendering logic where applicable;
# otherwise use "raw" data value as string # otherwise use "raw" data value as string
value = self.obtain_value(rowobj, name)
if self.renderers and name in self.renderers: if self.renderers and name in self.renderers:
renderer = self.renderers[name] value = self.renderers[name](rowobj, name)
else:
# TODO: legacy renderer callables require 2 args, value = self.obtain_value(rowobj, name)
# but wuttaweb callables require 3 args
sig = inspect.signature(renderer)
required = [param for param in sig.parameters.values()
if param.default == param.empty]
if len(required) == 2:
# TODO: legacy renderer
value = renderer(rowobj, name)
else: # the future
value = renderer(rowobj, name, value)
if value is None: if value is None:
value = "" value = ""
@ -1518,8 +1692,6 @@ class Grid(WuttaGrid):
results = { results = {
'data': data, 'data': data,
'row_classes': status_map,
# TODO: deprecate / remove this
'row_status_map': status_map, 'row_status_map': status_map,
} }
@ -1531,10 +1703,6 @@ class Grid(WuttaGrid):
results['checked_rows_code'] = '[{}]'.format( results['checked_rows_code'] = '[{}]'.format(
', '.join(['{}[{}]'.format(var, i) for i in checked])) ', '.join(['{}[{}]'.format(var, i) for i in checked]))
if self.paginated and self.paginate_on_backend:
results['pager_stats'] = self.get_vue_pager_stats()
# TODO: is this actually needed now that we have pager_stats?
if self.paginated and self.pager is not None: if self.paginated and self.pager is not None:
results['total_items'] = self.pager.item_count results['total_items'] = self.pager.item_count
results['per_page'] = self.pager.items_per_page results['per_page'] = self.pager.items_per_page
@ -1548,11 +1716,6 @@ class Grid(WuttaGrid):
self._table_data = results self._table_data = results
return self._table_data return self._table_data
# TODO: remove this when we use upstream GridAction
def add_action(self, key, **kwargs):
""" """
self.actions.append(GridAction(self.request, key, **kwargs))
def set_action_urls(self, row, rowobj, i): def set_action_urls(self, row, rowobj, i):
""" """
Pre-generate all action URLs for the given data row. Meant for use Pre-generate all action URLs for the given data row. Meant for use

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# Rattail -- Retail Software Framework # Rattail -- Retail Software Framework
# Copyright © 2010-2024 Lance Edgar # Copyright © 2010-2023 Lance Edgar
# #
# This file is part of Rattail. # This file is part of Rattail.
# #
@ -24,9 +24,6 @@
Template Context Helpers Template Context Helpers
""" """
# start off with all from wuttaweb
from wuttaweb.helpers import *
import os import os
import datetime import datetime
from decimal import Decimal from decimal import Decimal
@ -36,7 +33,12 @@ from rattail.time import localtime, make_utc
from rattail.util import pretty_quantity, pretty_hours, hours_as_decimal from rattail.util import pretty_quantity, pretty_hours, hours_as_decimal
from rattail.db.util import maxlen from rattail.db.util import maxlen
from tailbone.util import (pretty_datetime, raw_datetime, from webhelpers2.html import *
from webhelpers2.html.tags import *
from wuttaweb.util import get_liburl
from tailbone.util import (csrf_token, get_csrf_token,
pretty_datetime, raw_datetime,
render_markdown, render_markdown,
route_exists) route_exists)

View file

@ -394,11 +394,6 @@ class TailboneMenuHandler(WuttaMenuHandler):
'route': 'products', 'route': 'products',
'perm': 'products.list', 'perm': 'products.list',
}, },
{
'title': "Product Costs",
'route': 'product_costs',
'perm': 'product_costs.list',
},
{ {
'title': "Departments", 'title': "Departments",
'route': 'departments', 'route': 'departments',
@ -456,11 +451,6 @@ class TailboneMenuHandler(WuttaMenuHandler):
'route': 'vendors', 'route': 'vendors',
'perm': 'vendors.list', 'perm': 'vendors.list',
}, },
{
'title': "Product Costs",
'route': 'product_costs',
'perm': 'product_costs.list',
},
{'type': 'sep'}, {'type': 'sep'},
{ {
'title': "Ordering", 'title': "Ordering",
@ -713,7 +703,7 @@ class TailboneMenuHandler(WuttaMenuHandler):
}, },
{'type': 'sep'}, {'type': 'sep'},
{ {
'title': "App Info", 'title': "App Details",
'route': 'appinfo', 'route': 'appinfo',
'perm': 'appinfo.list', 'perm': 'appinfo.list',
}, },

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# Rattail -- Retail Software Framework # Rattail -- Retail Software Framework
# Copyright © 2010-2024 Lance Edgar # Copyright © 2010-2017 Lance Edgar
# #
# This file is part of Rattail. # This file is part of Rattail.
# #
@ -24,8 +24,9 @@
Static Assets Static Assets
""" """
from __future__ import unicode_literals, absolute_import
def includeme(config): def includeme(config):
config.include('wuttaweb.static')
config.add_static_view('tailbone', 'tailbone:static') config.add_static_view('tailbone', 'tailbone:static')
config.add_static_view('deform', 'deform:static') config.add_static_view('deform', 'deform:static')

View file

@ -1,2 +1,250 @@
## -*- coding: utf-8; -*- ## -*- coding: utf-8; -*-
<%inherit file="wuttaweb:templates/appinfo/configure.mako" /> <%inherit file="/configure.mako" />
<%def name="form_content()">
<h3 class="block is-size-3">Basics</h3>
<div class="block" style="padding-left: 2rem;">
<b-field grouped>
<b-field label="App Title">
<b-input name="rattail.app_title"
v-model="simpleSettings['rattail.app_title']"
@input="settingsNeedSaved = true">
</b-input>
</b-field>
<b-field label="Node Type">
## TODO: should be a dropdown, app handler defines choices
<b-input name="rattail.node_type"
v-model="simpleSettings['rattail.node_type']"
@input="settingsNeedSaved = true">
</b-input>
</b-field>
<b-field label="Node Title">
<b-input name="rattail.node_title"
v-model="simpleSettings['rattail.node_title']"
@input="settingsNeedSaved = true">
</b-input>
</b-field>
</b-field>
<b-field>
<b-checkbox name="rattail.production"
v-model="simpleSettings['rattail.production']"
native-value="true"
@input="settingsNeedSaved = true">
Production Mode
</b-checkbox>
</b-field>
<div class="level-left">
<div class="level-item">
<b-field>
<b-checkbox name="rattail.running_from_source"
v-model="simpleSettings['rattail.running_from_source']"
native-value="true"
@input="settingsNeedSaved = true">
Running from Source
</b-checkbox>
</b-field>
</div>
<div class="level-item">
<b-field label="Top-Level Package" horizontal
v-if="simpleSettings['rattail.running_from_source']">
<b-input name="rattail.running_from_source.rootpkg"
v-model="simpleSettings['rattail.running_from_source.rootpkg']"
@input="settingsNeedSaved = true">
</b-input>
</b-field>
</div>
</div>
</div>
<h3 class="block is-size-3">Display</h3>
<div class="block" style="padding-left: 2rem;">
<b-field grouped>
<b-field label="Background Color">
<b-input name="tailbone.background_color"
v-model="simpleSettings['tailbone.background_color']"
@input="settingsNeedSaved = true">
</b-input>
</b-field>
</b-field>
</div>
<h3 class="block is-size-3">Grids</h3>
<div class="block" style="padding-left: 2rem;">
<b-field grouped>
<b-field label="Default Page Size">
<b-input name="tailbone.grid.default_pagesize"
v-model="simpleSettings['tailbone.grid.default_pagesize']"
@input="settingsNeedSaved = true">
</b-input>
</b-field>
</b-field>
</div>
<h3 class="block is-size-3">Web Libraries</h3>
<div class="block" style="padding-left: 2rem;">
<${b}-table :data="weblibs">
<${b}-table-column field="title"
label="Name"
v-slot="props">
{{ props.row.title }}
</${b}-table-column>
<${b}-table-column field="configured_version"
label="Version"
v-slot="props">
{{ props.row.configured_version || props.row.default_version }}
</${b}-table-column>
<${b}-table-column field="configured_url"
label="URL Override"
v-slot="props">
{{ props.row.configured_url }}
</${b}-table-column>
<${b}-table-column field="live_url"
label="Effective (Live) URL"
v-slot="props">
<span v-if="props.row.modified"
class="has-text-warning">
save settings and refresh page to see new URL
</span>
<span v-if="!props.row.modified">
{{ props.row.live_url }}
</span>
</${b}-table-column>
<${b}-table-column field="actions"
label="Actions"
v-slot="props">
<a href="#"
@click.prevent="editWebLibraryInit(props.row)">
% if request.use_oruga:
<o-icon icon="edit" />
% else:
<i class="fas fa-edit"></i>
% endif
Edit
</a>
</${b}-table-column>
</${b}-table>
% for weblib in weblibs:
${h.hidden('wuttaweb.libver.{}'.format(weblib['key']), **{':value': "simpleSettings['wuttaweb.libver.{}']".format(weblib['key'])})}
${h.hidden('wuttaweb.liburl.{}'.format(weblib['key']), **{':value': "simpleSettings['wuttaweb.liburl.{}']".format(weblib['key'])})}
% endfor
<${b}-modal has-modal-card
% if request.use_oruga:
v-model:active="editWebLibraryShowDialog"
% else:
:active.sync="editWebLibraryShowDialog"
% endif
>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Web Library: {{ editWebLibraryRecord.title }}</p>
</header>
<section class="modal-card-body">
<b-field grouped>
<b-field label="Default Version">
<b-input v-model="editWebLibraryRecord.default_version"
disabled>
</b-input>
</b-field>
<b-field label="Override Version">
<b-input v-model="editWebLibraryVersion">
</b-input>
</b-field>
</b-field>
<b-field label="Override URL">
<b-input v-model="editWebLibraryURL"
expanded />
</b-field>
<b-field label="Effective URL (as of last page load)">
<b-input v-model="editWebLibraryRecord.live_url"
disabled
expanded />
</b-field>
</section>
<footer class="modal-card-foot">
<b-button type="is-primary"
@click="editWebLibrarySave()"
icon-pack="fas"
icon-left="save">
Save
</b-button>
<b-button @click="editWebLibraryShowDialog = false">
Cancel
</b-button>
</footer>
</div>
</${b}-modal>
</div>
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
ThisPageData.weblibs = ${json.dumps(weblibs)|n}
ThisPageData.editWebLibraryShowDialog = false
ThisPageData.editWebLibraryRecord = {}
ThisPageData.editWebLibraryVersion = null
ThisPageData.editWebLibraryURL = null
ThisPage.methods.editWebLibraryInit = function(row) {
this.editWebLibraryRecord = row
this.editWebLibraryVersion = row.configured_version
this.editWebLibraryURL = row.configured_url
this.editWebLibraryShowDialog = true
}
ThisPage.methods.editWebLibrarySave = function() {
this.editWebLibraryRecord.configured_version = this.editWebLibraryVersion
this.editWebLibraryRecord.configured_url = this.editWebLibraryURL
this.editWebLibraryRecord.modified = true
this.simpleSettings[`wuttaweb.libver.${'$'}{this.editWebLibraryRecord.key}`] = this.editWebLibraryVersion
this.simpleSettings[`wuttaweb.liburl.${'$'}{this.editWebLibraryRecord.key}`] = this.editWebLibraryURL
this.settingsNeedSaved = true
this.editWebLibraryShowDialog = false
}
</script>
</%def>
${parent.body()}

View file

@ -1,7 +1,8 @@
## -*- coding: utf-8; -*- ## -*- coding: utf-8; -*-
<%inherit file="wuttaweb:templates/appinfo/index.mako" /> <%inherit file="/master/index.mako" />
<%def name="render_grid_component()">
<%def name="page_content()">
<div class="buttons"> <div class="buttons">
<once-button type="is-primary" <once-button type="is-primary"
@ -27,5 +28,100 @@
</div> </div>
${parent.page_content()} <${b}-collapse class="panel" open>
<template #trigger="props">
<div class="panel-heading"
style="cursor: pointer;"
role="button">
## TODO: for some reason buefy will "reuse" the icon
## element in such a way that its display does not
## refresh. so to work around that, we use different
## structure for the two icons, so buefy is forced to
## re-draw
<b-icon v-if="props.open"
pack="fas"
icon="angle-down">
</b-icon>
<span v-if="!props.open">
<b-icon pack="fas"
icon="angle-right">
</b-icon>
</span>
<span>Configuration Files</span>
</div>
</template>
<div class="panel-block">
<div style="width: 100%;">
<${b}-table :data="configFiles">
<${b}-table-column field="priority"
label="Priority"
v-slot="props">
{{ props.row.priority }}
</${b}-table-column>
<${b}-table-column field="path"
label="File Path"
v-slot="props">
{{ props.row.path }}
</${b}-table-column>
</${b}-table>
</div>
</div>
</${b}-collapse>
<${b}-collapse class="panel"
:open="false">
<template #trigger="props">
<div class="panel-heading"
style="cursor: pointer;"
role="button">
## TODO: for some reason buefy will "reuse" the icon
## element in such a way that its display does not
## refresh. so to work around that, we use different
## structure for the two icons, so buefy is forced to
## re-draw
<b-icon v-if="props.open"
pack="fas"
icon="angle-down">
</b-icon>
<span v-if="!props.open">
<b-icon pack="fas"
icon="angle-right">
</b-icon>
</span>
<strong>Installed Packages</strong>
</div>
</template>
<div class="panel-block">
<div style="width: 100%;">
${parent.render_grid_component()}
</div>
</div>
</${b}-collapse>
</%def> </%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
ThisPageData.configFiles = ${json.dumps([dict(path=p, priority=i) for i, p in enumerate(request.rattail_config.prioritized_files, 1)])|n}
</script>
</%def>
${parent.body()}

View file

@ -15,8 +15,8 @@
<app-settings :groups="groups" :showing-group="showingGroup"></app-settings> <app-settings :groups="groups" :showing-group="showingGroup"></app-settings>
</%def> </%def>
<%def name="render_vue_templates()"> <%def name="render_this_page_template()">
${parent.render_vue_templates()} ${parent.render_this_page_template()}
<script type="text/x-template" id="app-settings-template"> <script type="text/x-template" id="app-settings-template">
<div class="form"> <div class="form">
@ -150,18 +150,19 @@
</script> </script>
</%def> </%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
ThisPageData.groups = ${json.dumps(settings_data)|n} ThisPageData.groups = ${json.dumps(settings_data)|n}
ThisPageData.showingGroup = ${json.dumps(current_group or '')|n} ThisPageData.showingGroup = ${json.dumps(current_group or '')|n}
</script> </script>
</%def> </%def>
<%def name="make_vue_components()"> <%def name="make_this_page_component()">
${parent.make_vue_components()} ${parent.make_this_page_component()}
<script> <script type="text/javascript">
Vue.component('app-settings', { Vue.component('app-settings', {
template: '#app-settings-template', template: '#app-settings-template',
@ -192,3 +193,6 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -1,5 +1,4 @@
## -*- coding: utf-8; -*- ## -*- coding: utf-8; -*-
<%namespace file="/wutta-components.mako" import="make_wutta_components" />
<%namespace file="/grids/nav.mako" import="grid_index_nav" /> <%namespace file="/grids/nav.mako" import="grid_index_nav" />
<%namespace file="/autocomplete.mako" import="tailbone_autocomplete_template" /> <%namespace file="/autocomplete.mako" import="tailbone_autocomplete_template" />
<%namespace name="base_meta" file="/base_meta.mako" /> <%namespace name="base_meta" file="/base_meta.mako" />
@ -35,21 +34,17 @@
</head> </head>
<body> <body>
<div id="app" style="height: 100%;"> ${declare_formposter_mixin()}
${self.body()}
<div id="whole-page-app">
<whole-page></whole-page> <whole-page></whole-page>
</div> </div>
## TODO: this must come before the self.body() call..but why? ${self.render_whole_page_template()}
${declare_formposter_mixin()} ${self.make_whole_page_component()}
${self.make_whole_page_app()}
## content body from derived/child template
${self.body()}
## Vue app
${self.render_vue_templates()}
${self.modify_vue_vars()}
${self.make_vue_components()}
${self.make_vue_app()}
</body> </body>
</html> </html>
@ -186,7 +181,7 @@
<%def name="head_tags()"></%def> <%def name="head_tags()"></%def>
<%def name="render_vue_template_whole_page()"> <%def name="render_whole_page_template()">
<script type="text/x-template" id="whole-page-template"> <script type="text/x-template" id="whole-page-template">
<div> <div>
<header> <header>
@ -632,23 +627,9 @@
% endif % endif
<div class="navbar-dropdown"> <div class="navbar-dropdown">
% if request.is_root: % if request.is_root:
${h.form(url('stop_root'), ref='stopBeingRootForm')} ${h.link_to("Stop being root", url('stop_root'), class_='navbar-item root-user')}
${h.csrf_token(request)}
<input type="hidden" name="referrer" value="${request.current_route_url()}" />
<a @click="$refs.stopBeingRootForm.submit()"
class="navbar-item root-user">
Stop being root
</a>
${h.end_form()}
% elif request.is_admin: % elif request.is_admin:
${h.form(url('become_root'), ref='startBeingRootForm')} ${h.link_to("Become root", url('become_root'), class_='navbar-item root-user')}
${h.csrf_token(request)}
<input type="hidden" name="referrer" value="${request.current_route_url()}" />
<a @click="$refs.startBeingRootForm.submit()"
class="navbar-item root-user">
Become root
</a>
${h.end_form()}
% endif % endif
% if messaging_enabled: % if messaging_enabled:
${h.link_to("Messages{}".format(" ({})".format(inbox_count) if inbox_count else ''), url('messages.inbox'), class_='navbar-item')} ${h.link_to("Messages{}".format(" ({})".format(inbox_count) if inbox_count else ''), url('messages.inbox'), class_='navbar-item')}
@ -656,11 +637,7 @@
% if request.is_root or not request.user.prevent_password_change: % if request.is_root or not request.user.prevent_password_change:
${h.link_to("Change Password", url('change_password'), class_='navbar-item')} ${h.link_to("Change Password", url('change_password'), class_='navbar-item')}
% endif % endif
% try: ${h.link_to("Edit Preferences", url('my.preferences'), class_='navbar-item')}
## nb. does not exist yet for wuttaweb
${h.link_to("Edit Preferences", url('my.preferences'), class_='navbar-item')}
% except:
% endtry
${h.link_to("Logout", url('logout'), class_='navbar-item')} ${h.link_to("Logout", url('logout'), class_='navbar-item')}
</div> </div>
</div> </div>
@ -686,7 +663,7 @@
text="Edit This"> text="Edit This">
</once-button> </once-button>
% endif % endif
% if getattr(master, 'cloneable', False) and not master.cloning and master.has_perm('clone'): % if getattr(master, 'cloneable', False) and master.has_perm('clone'):
<once-button tag="a" href="${master.get_action_url('clone', instance)}" <once-button tag="a" href="${master.get_action_url('clone', instance)}"
icon-left="object-ungroup" icon-left="object-ungroup"
text="Clone This"> text="Clone This">
@ -772,8 +749,11 @@
% endif % endif
</%def> </%def>
<%def name="render_vue_script_whole_page()"> <%def name="declare_whole_page_vars()">
<script> ${page_help.declare_vars()}
${multi_file_upload.declare_vars()}
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.feedback.js') + '?ver={}'.format(tailbone.__version__))}
<script type="text/javascript">
let WholePage = { let WholePage = {
template: '#whole-page-template', template: '#whole-page-template',
@ -909,6 +889,57 @@
</script> </script>
</%def> </%def>
<%def name="modify_whole_page_vars()">
<script type="text/javascript">
% if request.user:
FeedbackFormData.userUUID = ${json.dumps(request.user.uuid)|n}
FeedbackFormData.userName = ${json.dumps(str(request.user))|n}
% endif
</script>
</%def>
<%def name="finalize_whole_page_vars()">
## NOTE: if you override this, must use <script> tags
</%def>
<%def name="make_whole_page_component()">
${make_grid_filter_components()}
${self.declare_whole_page_vars()}
${self.modify_whole_page_vars()}
${self.finalize_whole_page_vars()}
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.autocomplete.js') + '?ver={}'.format(tailbone.__version__))}
${page_help.make_component()}
${multi_file_upload.make_component()}
<script type="text/javascript">
FeedbackForm.data = function() { return FeedbackFormData }
Vue.component('feedback-form', FeedbackForm)
WholePage.data = function() { return WholePageData }
Vue.component('whole-page', WholePage)
</script>
</%def>
<%def name="make_whole_page_app()">
<script type="text/javascript">
new Vue({
el: '#whole-page-app'
})
</script>
</%def>
<%def name="wtfield(form, name, **kwargs)"> <%def name="wtfield(form, name, **kwargs)">
<div class="field-wrapper${' error' if form[name].errors else ''}"> <div class="field-wrapper${' error' if form[name].errors else ''}">
<label for="${name}">${form[name].label}</label> <label for="${name}">${form[name].label}</label>
@ -930,88 +961,3 @@
</div> </div>
</div> </div>
</%def> </%def>
##############################
## vue components + app
##############################
<%def name="render_vue_templates()">
${page_help.declare_vars()}
${multi_file_upload.declare_vars()}
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.feedback.js') + '?ver={}'.format(tailbone.__version__))}
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.autocomplete.js') + '?ver={}'.format(tailbone.__version__))}
## DEPRECATED; called for back-compat
${self.render_whole_page_template()}
</%def>
## DEPRECATED; remains for back-compat
<%def name="render_whole_page_template()">
${self.render_vue_template_whole_page()}
${self.declare_whole_page_vars()}
</%def>
## DEPRECATED; remains for back-compat
<%def name="declare_whole_page_vars()">
${self.render_vue_script_whole_page()}
</%def>
<%def name="modify_vue_vars()">
## DEPRECATED; called for back-compat
${self.modify_whole_page_vars()}
</%def>
## DEPRECATED; remains for back-compat
<%def name="modify_whole_page_vars()">
<script>
% if request.user:
FeedbackFormData.userUUID = ${json.dumps(request.user.uuid)|n}
FeedbackFormData.userName = ${json.dumps(str(request.user))|n}
% endif
</script>
</%def>
<%def name="make_vue_components()">
${make_wutta_components()}
${make_grid_filter_components()}
${page_help.make_component()}
${multi_file_upload.make_component()}
<script>
FeedbackForm.data = function() { return FeedbackFormData }
Vue.component('feedback-form', FeedbackForm)
</script>
## DEPRECATED; called for back-compat
${self.finalize_whole_page_vars()}
${self.make_whole_page_component()}
</%def>
## DEPRECATED; remains for back-compat
<%def name="make_whole_page_component()">
<script>
WholePage.data = function() { return WholePageData }
Vue.component('whole-page', WholePage)
</script>
</%def>
<%def name="make_vue_app()">
## DEPRECATED; called for back-compat
${self.make_whole_page_app()}
</%def>
## DEPRECATED; remains for back-compat
<%def name="make_whole_page_app()">
<script>
new Vue({
el: '#app'
})
</script>
</%def>
##############################
## DEPRECATED
##############################
<%def name="finalize_whole_page_vars()"></%def>

View file

@ -1,7 +1,10 @@
## -*- coding: utf-8; -*- ## -*- coding: utf-8; -*-
<%inherit file="wuttaweb:templates/base_meta.mako" />
<%def name="app_title()">${app.get_node_title()}</%def> <%def name="app_title()">${rattail_app.get_node_title()}</%def>
<%def name="global_title()">${"[STAGE] " if not request.rattail_config.production() else ''}${self.app_title()}</%def>
<%def name="extra_styles()"></%def>
<%def name="favicon()"> <%def name="favicon()">
<link rel="icon" type="image/x-icon" href="${request.rattail_config.get('tailbone', 'favicon_url', default=request.static_url('tailbone:static/img/rattail.ico'))}" /> <link rel="icon" type="image/x-icon" href="${request.rattail_config.get('tailbone', 'favicon_url', default=request.static_url('tailbone:static/img/rattail.ico'))}" />
@ -10,3 +13,9 @@
<%def name="header_logo()"> <%def name="header_logo()">
${h.image(request.rattail_config.get('tailbone', 'header_image_url', default=request.static_url('tailbone:static/img/rattail.ico')), "Header Logo", style="height: 49px;")} ${h.image(request.rattail_config.get('tailbone', 'header_image_url', default=request.static_url('tailbone:static/img/rattail.ico')), "Header Logo", style="height: 49px;")}
</%def> </%def>
<%def name="footer()">
<p class="has-text-centered">
powered by ${h.link_to("Rattail", url('about'))}
</p>
</%def>

View file

@ -43,7 +43,7 @@
<br /> <br />
<div class="form-wrapper"> <div class="form-wrapper">
<div class="form"> <div class="form">
${execute_form.render_vue_tag(ref='executeResultsForm')} <${execute_form.component} ref="executeResultsForm"></${execute_form.component}>
</div> </div>
</div> </div>
</section> </section>
@ -64,17 +64,10 @@
% endif % endif
</%def> </%def>
<%def name="render_vue_templates()"> <%def name="modify_this_page_vars()">
${parent.render_vue_templates()} ${parent.modify_this_page_vars()}
% if master.results_executable and master.has_perm('execute_multiple'):
${execute_form.render_vue_template(form_kwargs={'ref': 'actualExecuteForm'}, buttons=False)}
% endif
</%def>
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
% if master.results_refreshable and master.has_perm('refresh'): % if master.results_refreshable and master.has_perm('refresh'):
<script> <script type="text/javascript">
TailboneGridData.refreshResultsButtonText = "Refresh Results" TailboneGridData.refreshResultsButtonText = "Refresh Results"
TailboneGridData.refreshResultsButtonDisabled = false TailboneGridData.refreshResultsButtonDisabled = false
@ -88,7 +81,7 @@
</script> </script>
% endif % endif
% if master.results_executable and master.has_perm('execute_multiple'): % if master.results_executable and master.has_perm('execute_multiple'):
<script> <script type="text/javascript">
${execute_form.vue_component}.methods.submit = function() { ${execute_form.vue_component}.methods.submit = function() {
this.$refs.actualExecuteForm.submit() this.$refs.actualExecuteForm.submit()
@ -125,9 +118,25 @@
% endif % endif
</%def> </%def>
<%def name="make_vue_components()"> <%def name="make_this_page_component()">
${parent.make_vue_components()} ${parent.make_this_page_component()}
% if master.results_executable and master.has_perm('execute_multiple'): % if master.results_executable and master.has_perm('execute_multiple'):
${execute_form.render_vue_finalize()} <script type="text/javascript">
${execute_form.vue_component}.data = function() { return ${execute_form.vue_component}Data }
Vue.component('${execute_form.component}', ${execute_form.vue_component})
</script>
% endif % endif
</%def> </%def>
<%def name="render_this_page_template()">
${parent.render_this_page_template()}
% if master.results_executable and master.has_perm('execute_multiple'):
${execute_form.render_deform(form_kwargs={'ref': 'actualExecuteForm'}, buttons=False)|n}
% endif
</%def>
${parent.body()}

View file

@ -297,9 +297,14 @@
</script> </script>
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
ThisPageData.toggleCompleteSubmitting = false ThisPageData.toggleCompleteSubmitting = false
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -1,9 +1,13 @@
## -*- coding: utf-8; -*- ## -*- coding: utf-8; -*-
<%inherit file="/batch/view.mako" /> <%inherit file="/batch/view.mako" />
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
${form.vue_component}Data.taxesData = ${json.dumps(taxes_data)|n} ${form.vue_component}Data.taxesData = ${json.dumps(taxes_data)|n}
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -39,9 +39,14 @@
</div> </div>
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
ThisPageData.catalogParsers = ${json.dumps(catalog_parsers_data)|n} ThisPageData.catalogParsers = ${json.dumps(catalog_parsers_data)|n}
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -1,9 +1,9 @@
## -*- coding: utf-8; -*- ## -*- coding: utf-8; -*-
<%inherit file="/batch/create.mako" /> <%inherit file="/batch/create.mako" />
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
${form.vue_component}Data.parsers = ${json.dumps(parsers_data)|n} ${form.vue_component}Data.parsers = ${json.dumps(parsers_data)|n}
@ -37,3 +37,6 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -119,7 +119,8 @@
<div class="markdown"> <div class="markdown">
${execution_described|n} ${execution_described|n}
</div> </div>
${execute_form.render_vue_tag(ref='executeBatchForm')} <${execute_form.component} ref="executeBatchForm">
</${execute_form.component}>
</section> </section>
<footer class="modal-card-foot"> <footer class="modal-card-foot">
@ -148,6 +149,12 @@
</nav> </nav>
</%def> </%def>
<%def name="render_form_template()">
## TODO: should use self.render_form_buttons()
## ${form.render_deform(form_id='batch-form', buttons=capture(self.render_form_buttons))|n}
${form.render_deform(form_id='batch-form', buttons=capture(buttons))|n}
</%def>
<%def name="render_this_page()"> <%def name="render_this_page()">
${parent.render_this_page()} ${parent.render_this_page()}
@ -167,7 +174,8 @@
Please be certain to use the right one! Please be certain to use the right one!
</p> </p>
<br /> <br />
${upload_worksheet_form.render_vue_tag(ref='uploadForm')} <${upload_worksheet_form.component} ref="uploadForm">
</${upload_worksheet_form.component}>
</section> </section>
<footer class="modal-card-foot"> <footer class="modal-card-foot">
@ -189,6 +197,16 @@
</%def> </%def>
<%def name="render_this_page_template()">
${parent.render_this_page_template()}
% if master.has_worksheet_file and master.allow_worksheet(batch) and master.has_perm('worksheet'):
${upload_worksheet_form.render_deform(buttons=False, form_kwargs={'ref': 'actualUploadForm'})|n}
% endif
% if master.handler.executable(batch) and master.has_perm('execute'):
${execute_form.render_deform(form_kwargs={'ref': 'actualExecuteForm'}, buttons=False)|n}
% endif
</%def>
<%def name="render_form()"> <%def name="render_form()">
<div class="form"> <div class="form">
<${form.component} @show-upload="showUploadDialog = true"> <${form.component} @show-upload="showUploadDialog = true">
@ -249,27 +267,9 @@
% endif % endif
</%def> </%def>
<%def name="render_vue_templates()"> <%def name="modify_this_page_vars()">
${parent.render_vue_templates()} ${parent.modify_this_page_vars()}
% if master.has_worksheet_file and master.allow_worksheet(batch) and master.has_perm('worksheet'): <script type="text/javascript">
${upload_worksheet_form.render_vue_template(buttons=False, form_kwargs={'ref': 'actualUploadForm'})}
% endif
% if master.handler.executable(batch) and master.has_perm('execute'):
${execute_form.render_vue_template(form_kwargs={'ref': 'actualExecuteForm'}, buttons=False)}
% endif
</%def>
## DEPRECATED; remains for back-compat
## nb. this is called by parent template, /form.mako
<%def name="render_form_template()">
## TODO: should use self.render_form_buttons()
## ${form.render_deform(form_id='batch-form', buttons=capture(self.render_form_buttons))|n}
${form.render_deform(form_id='batch-form', buttons=capture(buttons))|n}
</%def>
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
ThisPageData.statusBreakdownData = ${json.dumps(status_breakdown_data)|n} ThisPageData.statusBreakdownData = ${json.dumps(status_breakdown_data)|n}
@ -340,12 +340,28 @@
</script> </script>
</%def> </%def>
<%def name="make_vue_components()"> <%def name="make_this_page_component()">
${parent.make_vue_components()} ${parent.make_this_page_component()}
% if master.has_worksheet_file and master.allow_worksheet(batch) and master.has_perm('worksheet'): % if master.has_worksheet_file and master.allow_worksheet(batch) and master.has_perm('worksheet'):
${upload_worksheet_form.render_vue_finalize()} <script type="text/javascript">
## UploadForm
${upload_worksheet_form.vue_component}.data = function() { return ${upload_worksheet_form.vue_component}Data }
Vue.component('${upload_worksheet_form.component}', ${upload_worksheet_form.vue_component})
</script>
% endif % endif
% if execute_enabled and master.has_perm('execute'): % if execute_enabled and master.has_perm('execute'):
${execute_form.render_vue_finalize()} <script type="text/javascript">
## ExecuteForm
${execute_form.vue_component}.data = function() { return ${execute_form.vue_component}Data }
Vue.component('${execute_form.component}', ${execute_form.vue_component})
</script>
% endif % endif
</%def> </%def>
${parent.body()}

View file

@ -208,9 +208,9 @@
% endif % endif
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
ThisPageData.menuSequence = ${json.dumps([m['key'] for m in menus])|n} ThisPageData.menuSequence = ${json.dumps([m['key'] for m in menus])|n}
@ -443,3 +443,6 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -92,7 +92,7 @@
<b-select name="${tmpl['setting_file']}" <b-select name="${tmpl['setting_file']}"
v-model="inputFileTemplateSettings['${tmpl['setting_file']}']" v-model="inputFileTemplateSettings['${tmpl['setting_file']}']"
@input="settingsNeedSaved = true"> @input="settingsNeedSaved = true">
<option value="">-new-</option> <option :value="null">-new-</option>
<option v-for="option in inputFileTemplateFileOptions['${tmpl['key']}']" <option v-for="option in inputFileTemplateFileOptions['${tmpl['key']}']"
:key="option" :key="option"
:value="option"> :value="option">
@ -104,40 +104,22 @@
<b-field label="Upload" <b-field label="Upload"
v-show="inputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted' && !inputFileTemplateSettings['${tmpl['setting_file']}']"> v-show="inputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted' && !inputFileTemplateSettings['${tmpl['setting_file']}']">
% if request.use_oruga: <b-field class="file is-primary"
<o-field class="file"> :class="{'has-name': !!inputFileTemplateSettings['${tmpl['setting_file']}']}">
<o-upload name="${tmpl['setting_file']}.upload" <b-upload name="${tmpl['setting_file']}.upload"
v-model="inputFileTemplateUploads['${tmpl['key']}']" v-model="inputFileTemplateUploads['${tmpl['key']}']"
v-slot="{ onclick }" class="file-label"
@input="settingsNeedSaved = true"> @input="settingsNeedSaved = true">
<o-button variant="primary" <span class="file-cta">
@click="onclick"> <b-icon class="file-icon" pack="fas" icon="upload"></b-icon>
<o-icon icon="upload" /> <span class="file-label">Click to upload</span>
<span>Click to upload</span> </span>
</o-button> </b-upload>
<span class="file-name" v-if="inputFileTemplateUploads['${tmpl['key']}']"> <span v-if="inputFileTemplateUploads['${tmpl['key']}']"
{{ inputFileTemplateUploads['${tmpl['key']}'].name }} class="file-name">
</span> {{ inputFileTemplateUploads['${tmpl['key']}'].name }}
</o-upload> </span>
</o-field> </b-field>
% else:
<b-field class="file is-primary"
:class="{'has-name': !!inputFileTemplateSettings['${tmpl['setting_file']}']}">
<b-upload name="${tmpl['setting_file']}.upload"
v-model="inputFileTemplateUploads['${tmpl['key']}']"
class="file-label"
@input="settingsNeedSaved = true">
<span class="file-cta">
<b-icon class="file-icon" pack="fas" icon="upload"></b-icon>
<span class="file-label">Click to upload</span>
</span>
</b-upload>
<span v-if="inputFileTemplateUploads['${tmpl['key']}']"
class="file-name">
{{ inputFileTemplateUploads['${tmpl['key']}'].name }}
</span>
</b-field>
% endif
</b-field> </b-field>
@ -161,85 +143,6 @@
</div> </div>
</%def> </%def>
<%def name="output_file_template_field(key)">
<% tmpl = output_file_templates[key] %>
<b-field grouped>
<b-field label="${tmpl['label']}">
<b-select name="${tmpl['setting_mode']}"
v-model="outputFileTemplateSettings['${tmpl['setting_mode']}']"
@input="settingsNeedSaved = true">
<option value="default">use default</option>
<option value="hosted">use uploaded file</option>
</b-select>
</b-field>
<b-field label="File"
v-show="outputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted'"
:message="outputFileTemplateSettings['${tmpl['setting_file']}'] ? 'This file lives on disk at: ${output_file_option_dirs[tmpl['key']]}' : null">
<b-select name="${tmpl['setting_file']}"
v-model="outputFileTemplateSettings['${tmpl['setting_file']}']"
@input="settingsNeedSaved = true">
<option value="">-new-</option>
<option v-for="option in outputFileTemplateFileOptions['${tmpl['key']}']"
:key="option"
:value="option">
{{ option }}
</option>
</b-select>
</b-field>
<b-field label="Upload"
v-show="outputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted' && !outputFileTemplateSettings['${tmpl['setting_file']}']">
% if request.use_oruga:
<o-field class="file">
<o-upload name="${tmpl['setting_file']}.upload"
v-model="outputFileTemplateUploads['${tmpl['key']}']"
v-slot="{ onclick }"
@input="settingsNeedSaved = true">
<o-button variant="primary"
@click="onclick">
<o-icon icon="upload" />
<span>Click to upload</span>
</o-button>
<span class="file-name" v-if="outputFileTemplateUploads['${tmpl['key']}']">
{{ outputFileTemplateUploads['${tmpl['key']}'].name }}
</span>
</o-upload>
</o-field>
% else:
<b-field class="file is-primary"
:class="{'has-name': !!outputFileTemplateSettings['${tmpl['setting_file']}']}">
<b-upload name="${tmpl['setting_file']}.upload"
v-model="outputFileTemplateUploads['${tmpl['key']}']"
class="file-label"
@input="settingsNeedSaved = true">
<span class="file-cta">
<b-icon class="file-icon" pack="fas" icon="upload"></b-icon>
<span class="file-label">Click to upload</span>
</span>
</b-upload>
<span v-if="outputFileTemplateUploads['${tmpl['key']}']"
class="file-name">
{{ outputFileTemplateUploads['${tmpl['key']}'].name }}
</span>
</b-field>
% endif
</b-field>
</b-field>
</%def>
<%def name="output_file_templates_section()">
<h3 class="block is-size-3">Output File Templates</h3>
<div class="block" style="padding-left: 2rem;">
% for key in output_file_templates:
${self.output_file_template_field(key)}
% endfor
</div>
</%def>
<%def name="form_content()"></%def> <%def name="form_content()"></%def>
<%def name="page_content()"> <%def name="page_content()">
@ -280,14 +183,15 @@
<b-button @click="purgeSettingsShowDialog = false"> <b-button @click="purgeSettingsShowDialog = false">
Cancel Cancel
</b-button> </b-button>
${h.form(request.current_route_url(), **{'@submit': 'purgingSettings = true'})} ${h.form(request.current_route_url())}
${h.csrf_token(request)} ${h.csrf_token(request)}
${h.hidden('remove_settings', 'true')} ${h.hidden('remove_settings', 'true')}
<b-button type="is-danger" <b-button type="is-danger"
native-type="submit" native-type="submit"
:disabled="purgingSettings" :disabled="purgingSettings"
icon-pack="fas" icon-pack="fas"
icon-left="trash"> icon-left="trash"
@click="purgingSettings = true">
{{ purgingSettings ? "Working, please wait..." : "Remove All Settings" }} {{ purgingSettings ? "Working, please wait..." : "Remove All Settings" }}
</b-button> </b-button>
${h.end_form()} ${h.end_form()}
@ -301,42 +205,62 @@
${h.end_form()} ${h.end_form()}
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
% if simple_settings is not Undefined: % if simple_settings is not Undefined:
ThisPageData.simpleSettings = ${json.dumps(simple_settings)|n} ThisPageData.simpleSettings = ${json.dumps(simple_settings)|n}
% endif % endif
% if input_file_template_settings is not Undefined:
ThisPageData.inputFileTemplateSettings = ${json.dumps(input_file_template_settings)|n}
ThisPageData.inputFileTemplateFileOptions = ${json.dumps(input_file_options)|n}
ThisPageData.inputFileTemplateUploads = {
% for key in input_file_templates:
'${key}': null,
% endfor
}
% endif
ThisPageData.purgeSettingsShowDialog = false ThisPageData.purgeSettingsShowDialog = false
ThisPageData.purgingSettings = false ThisPageData.purgingSettings = false
ThisPageData.settingsNeedSaved = false ThisPageData.settingsNeedSaved = false
ThisPageData.undoChanges = false ThisPageData.undoChanges = false
ThisPageData.savingSettings = false ThisPageData.savingSettings = false
ThisPageData.validators = []
ThisPage.methods.purgeSettingsInit = function() { ThisPage.methods.purgeSettingsInit = function() {
this.purgeSettingsShowDialog = true this.purgeSettingsShowDialog = true
} }
ThisPage.methods.validateSettings = function() {} % if input_file_template_settings is not Undefined:
ThisPage.methods.validateInputFileTemplateSettings = function() {
% for tmpl in input_file_templates.values():
if (this.inputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted') {
if (!this.inputFileTemplateSettings['${tmpl['setting_file']}']) {
if (!this.inputFileTemplateUploads['${tmpl['key']}']) {
return "You must provide a file to upload for the ${tmpl['label']} template."
}
}
}
% endfor
}
% endif
ThisPage.methods.saveSettings = function() { ThisPage.methods.validateSettings = function() {
let msg let msg
// nb. this is the future % if input_file_template_settings is not Undefined:
for (let validator of this.validators) { msg = this.validateInputFileTemplateSettings()
msg = validator.call(this)
if (msg) { if (msg) {
alert(msg) return msg
return
} }
} % endif
}
// nb. legacy method ThisPage.methods.saveSettings = function() {
msg = this.validateSettings() let msg = this.validateSettings()
if (msg) { if (msg) {
alert(msg) alert(msg)
return return
@ -367,65 +291,8 @@
window.addEventListener('beforeunload', this.beforeWindowUnload) window.addEventListener('beforeunload', this.beforeWindowUnload)
} }
##############################
## input file templates
##############################
% if input_file_template_settings is not Undefined:
ThisPageData.inputFileTemplateSettings = ${json.dumps(input_file_template_settings)|n}
ThisPageData.inputFileTemplateFileOptions = ${json.dumps(input_file_options)|n}
ThisPageData.inputFileTemplateUploads = {
% for key in input_file_templates:
'${key}': null,
% endfor
}
ThisPage.methods.validateInputFileTemplateSettings = function() {
% for tmpl in input_file_templates.values():
if (this.inputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted') {
if (!this.inputFileTemplateSettings['${tmpl['setting_file']}']) {
if (!this.inputFileTemplateUploads['${tmpl['key']}']) {
return "You must provide a file to upload for the ${tmpl['label']} template."
}
}
}
% endfor
}
ThisPageData.validators.push(ThisPage.methods.validateInputFileTemplateSettings)
% endif
##############################
## output file templates
##############################
% if output_file_template_settings is not Undefined:
ThisPageData.outputFileTemplateSettings = ${json.dumps(output_file_template_settings)|n}
ThisPageData.outputFileTemplateFileOptions = ${json.dumps(output_file_options)|n}
ThisPageData.outputFileTemplateUploads = {
% for key in output_file_templates:
'${key}': null,
% endfor
}
ThisPage.methods.validateOutputFileTemplateSettings = function() {
% for tmpl in output_file_templates.values():
if (this.outputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted') {
if (!this.outputFileTemplateSettings['${tmpl['setting_file']}']) {
if (!this.outputFileTemplateUploads['${tmpl['key']}']) {
return "You must provide a file to upload for the ${tmpl['label']} template."
}
}
}
% endfor
}
ThisPageData.validators.push(ThisPage.methods.validateOutputFileTemplateSettings)
% endif
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -88,9 +88,9 @@
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
ThisPage.methods.getLabelForKey = function(key) { ThisPage.methods.getLabelForKey = function(key) {
switch (key) { switch (key) {
@ -111,3 +111,6 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -106,9 +106,9 @@
% endif % endif
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
ThisPageData.resolvePersonShowDialog = false ThisPageData.resolvePersonShowDialog = false
ThisPageData.resolvePersonUUID = null ThisPageData.resolvePersonUUID = null
@ -139,3 +139,5 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -16,9 +16,9 @@
</div> </div>
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
% if expose_shoppers: % if expose_shoppers:
${form.vue_component}Data.shoppers = ${json.dumps(shoppers_data)|n} ${form.vue_component}Data.shoppers = ${json.dumps(shoppers_data)|n}
@ -36,3 +36,5 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -47,9 +47,10 @@
</div> </div>
</%def> </%def>
<%def name="render_vue_templates()"> <%def name="render_this_page_template()">
${parent.render_vue_templates()} ${parent.render_this_page_template()}
${product_lookup.tailbone_product_lookup_template()} ${product_lookup.tailbone_product_lookup_template()}
<script type="text/x-template" id="customer-order-creator-template"> <script type="text/x-template" id="customer-order-creator-template">
<div> <div>
@ -1264,7 +1265,12 @@
</div> </div>
</script> </script>
<script> </%def>
<%def name="make_this_page_component()">
${parent.make_this_page_component()}
${product_lookup.tailbone_product_lookup_component()}
<script type="text/javascript">
const CustomerOrderCreator = { const CustomerOrderCreator = {
template: '#customer-order-creator-template', template: '#customer-order-creator-template',
@ -2400,7 +2406,5 @@
</script> </script>
</%def> </%def>
<%def name="make_vue_components()">
${parent.make_vue_components()} ${parent.body()}
${product_lookup.tailbone_product_lookup_component()}
</%def>

View file

@ -291,9 +291,9 @@
% endif % endif
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
${form.vue_component}Data.eventsData = ${json.dumps(events_data)|n} ${form.vue_component}Data.eventsData = ${json.dumps(events_data)|n}
@ -448,3 +448,5 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -26,9 +26,9 @@
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
% if request.has_perm('datasync.restart'): % if request.has_perm('datasync.restart'):
TailboneGridData.restartDatasyncFormSubmitting = false TailboneGridData.restartDatasyncFormSubmitting = false
@ -50,3 +50,6 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -83,8 +83,8 @@
</b-notification> </b-notification>
<b-field> <b-field>
<b-checkbox name="rattail.datasync.use_profile_settings" <b-checkbox name="use_profile_settings"
v-model="simpleSettings['rattail.datasync.use_profile_settings']" v-model="useProfileSettings"
native-value="true" native-value="true"
@input="settingsNeedSaved = true"> @input="settingsNeedSaved = true">
Use these Settings to configure watchers and consumers Use these Settings to configure watchers and consumers
@ -99,7 +99,7 @@
</div> </div>
<div class="level-right"> <div class="level-right">
<div class="level-item" <div class="level-item"
v-show="simpleSettings['rattail.datasync.use_profile_settings']"> v-show="useProfileSettings">
<b-button type="is-primary" <b-button type="is-primary"
@click="newProfile()" @click="newProfile()"
icon-pack="fas" icon-pack="fas"
@ -162,7 +162,7 @@
</${b}-table-column> </${b}-table-column>
<${b}-table-column label="Actions" <${b}-table-column label="Actions"
v-slot="props" v-slot="props"
v-if="simpleSettings['rattail.datasync.use_profile_settings']"> v-if="useProfileSettings">
<a href="#" <a href="#"
class="grid-action" class="grid-action"
@click.prevent="editProfile(props.row)"> @click.prevent="editProfile(props.row)">
@ -580,27 +580,18 @@
<b-field label="Supervisor Process Name" <b-field label="Supervisor Process Name"
message="This should be the complete name, including group - e.g. poser:poser_datasync" message="This should be the complete name, including group - e.g. poser:poser_datasync"
expanded> expanded>
<b-input name="rattail.datasync.supervisor_process_name" <b-input name="supervisor_process_name"
v-model="simpleSettings['rattail.datasync.supervisor_process_name']" v-model="supervisorProcessName"
@input="settingsNeedSaved = true" @input="settingsNeedSaved = true"
expanded> expanded>
</b-input> </b-input>
</b-field> </b-field>
<b-field label="Consumer Batch Size"
message="Max number of changes to be consumed at once."
expanded>
<numeric-input name="rattail.datasync.batch_size_limit"
v-model="simpleSettings['rattail.datasync.batch_size_limit']"
@input="settingsNeedSaved = true" />
</b-field>
<h3 class="is-size-3">Legacy</h3>
<b-field label="Restart Command" <b-field label="Restart Command"
message="This will run as '${system_user}' system user - please configure sudoers as needed. Typical command is like: sudo supervisorctl restart poser:poser_datasync" message="This will run as '${system_user}' system user - please configure sudoers as needed. Typical command is like: sudo supervisorctl restart poser:poser_datasync"
expanded> expanded>
<b-input name="tailbone.datasync.restart" <b-input name="restart_command"
v-model="simpleSettings['tailbone.datasync.restart']" v-model="restartCommand"
@input="settingsNeedSaved = true" @input="settingsNeedSaved = true"
expanded> expanded>
</b-input> </b-input>
@ -608,13 +599,14 @@
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
ThisPageData.showConfigFilesNote = false ThisPageData.showConfigFilesNote = false
ThisPageData.profilesData = ${json.dumps(profiles_data)|n} ThisPageData.profilesData = ${json.dumps(profiles_data)|n}
ThisPageData.showDisabledProfiles = false ThisPageData.showDisabledProfiles = false
ThisPageData.useProfileSettings = ${json.dumps(use_profile_settings)|n}
ThisPageData.editProfileShowDialog = false ThisPageData.editProfileShowDialog = false
ThisPageData.editingProfile = null ThisPageData.editingProfile = null
@ -639,6 +631,9 @@
ThisPageData.editingConsumerRunas = null ThisPageData.editingConsumerRunas = null
ThisPageData.editingConsumerEnabled = true ThisPageData.editingConsumerEnabled = true
ThisPageData.supervisorProcessName = ${json.dumps(supervisor_process_name)|n}
ThisPageData.restartCommand = ${json.dumps(restart_command)|n}
ThisPage.computed.updateConsumerDisabled = function() { ThisPage.computed.updateConsumerDisabled = function() {
if (!this.editingConsumerKey) { if (!this.editingConsumerKey) {
return true return true
@ -987,3 +982,6 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -115,9 +115,8 @@
</${b}-table> </${b}-table>
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} <script type="text/javascript">
<script>
ThisPageData.processInfo = ${json.dumps(process_info)|n} ThisPageData.processInfo = ${json.dumps(process_info)|n}
@ -172,3 +171,6 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -1,7 +1,6 @@
<div i18n:domain="deform" tal:omit-tag="" <div i18n:domain="deform" tal:omit-tag=""
tal:define="oid oid|field.oid; tal:define="oid oid|field.oid;
name name|field.name; name name|field.name;
vmodel vmodel|'field_model_' + name;
css_class css_class|field.widget.css_class; css_class css_class|field.widget.css_class;
style style|field.widget.style;"> style style|field.widget.style;">
@ -9,7 +8,7 @@
${field.start_mapping()} ${field.start_mapping()}
<b-input type="password" <b-input type="password"
name="${name}" name="${name}"
v-model="${vmodel}" value="${field.widget.redisplay and cstruct or ''}"
tal:attributes="class string: form-control ${css_class or ''}; tal:attributes="class string: form-control ${css_class or ''};
style style; style style;
attributes|field.widget.attributes|{};" attributes|field.widget.attributes|{};"
@ -19,6 +18,7 @@
</b-input> </b-input>
<b-input type="password" <b-input type="password"
name="${name}-confirm" name="${name}-confirm"
value="${field.widget.redisplay and confirm or ''}"
tal:attributes="class string: form-control ${css_class or ''}; tal:attributes="class string: form-control ${css_class or ''};
style style; style style;
confirm_attributes|field.widget.confirm_attributes|{};" confirm_attributes|field.widget.confirm_attributes|{};"

View file

@ -1,9 +1,13 @@
## -*- coding: utf-8; -*- ## -*- coding: utf-8; -*-
<%inherit file="/master/view.mako" /> <%inherit file="/master/view.mako" />
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
${form.vue_component}Data.employeesData = ${json.dumps(employees_data)|n} ${form.vue_component}Data.employeesData = ${json.dumps(employees_data)|n}
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -90,15 +90,15 @@
<%def name="before_object_helpers()"></%def> <%def name="before_object_helpers()"></%def>
<%def name="render_vue_templates()"> <%def name="render_this_page_template()">
${parent.render_vue_templates()}
% if form is not Undefined: % if form is not Undefined:
${self.render_form_template()} ${self.render_form_template()}
% endif % endif
${parent.render_this_page_template()}
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
% if main_form_collapsible: % if main_form_collapsible:
<script> <script>
ThisPageData.mainFormPanelOpen = ${'false' if main_form_autocollapse else 'true'} ThisPageData.mainFormPanelOpen = ${'false' if main_form_autocollapse else 'true'}
@ -106,9 +106,18 @@
% endif % endif
</%def> </%def>
<%def name="make_vue_components()"> <%def name="finalize_this_page_vars()">
${parent.make_vue_components()} ${parent.finalize_this_page_vars()}
% if form is not Undefined: % if form is not Undefined:
${form.render_vue_finalize()} <script type="text/javascript">
${form.vue_component}.data = function() { return ${form.vue_component}Data }
Vue.component('${form.vue_tagname}', ${form.vue_component})
</script>
% endif % endif
</%def> </%def>
${parent.body()}

View file

@ -39,7 +39,7 @@
simplePOST(action, params, success, failure) { simplePOST(action, params, success, failure) {
let csrftoken = ${json.dumps(h.get_csrf_token(request))|n} let csrftoken = ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n}
let headers = { let headers = {
'${csrf_header_name}': csrftoken, '${csrf_header_name}': csrftoken,

View file

@ -59,7 +59,7 @@
native-type="submit" native-type="submit"
:disabled="${form.vue_component}Submitting" :disabled="${form.vue_component}Submitting"
icon-pack="fas" icon-pack="fas"
icon-left="${form.button_icon_submit}"> icon-left="save">
{{ ${form.vue_component}Submitting ? "Working, please wait..." : "${form.button_label_submit}" }} {{ ${form.vue_component}Submitting ? "Working, please wait..." : "${form.button_label_submit}" }}
</b-button> </b-button>
% else: % else:
@ -180,7 +180,7 @@
let ${form.vue_component}Data = { let ${form.vue_component}Data = {
## TODO: should find a better way to handle CSRF token ## TODO: should find a better way to handle CSRF token
csrftoken: ${json.dumps(h.get_csrf_token(request))|n}, csrftoken: ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n},
% if can_edit_help: % if can_edit_help:
fieldLabels: ${json.dumps(field_labels)|n}, fieldLabels: ${json.dumps(field_labels)|n},

View file

@ -276,9 +276,9 @@
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
ThisPageData.featureType = ${json.dumps(feature_type)|n} ThisPageData.featureType = ${json.dumps(feature_type)|n}
ThisPageData.resultGenerated = ${json.dumps(bool(result))|n} ThisPageData.resultGenerated = ${json.dumps(bool(result))|n}
@ -385,3 +385,6 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -10,70 +10,8 @@
<div style="display: flex; flex-direction: column; justify-content: end;"> <div style="display: flex; flex-direction: column; justify-content: end;">
<div class="filters"> <div class="filters">
% if getattr(grid, 'filterable', False): % if getattr(grid, 'filterable', False):
<form method="GET" @submit.prevent="applyFilters()"> ## TODO: stop using |n filter
${grid.render_filters(allow_save_defaults=allow_save_defaults)|n}
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
<grid-filter v-for="key in filtersSequence"
:key="key"
:filter="filters[key]"
ref="gridFilters">
</grid-filter>
</div>
<div style="display: flex; gap: 0.5rem; margin-top: 0.5rem;">
<b-button type="is-primary"
native-type="submit"
icon-pack="fas"
icon-left="check">
Apply Filters
</b-button>
<b-button v-if="!addFilterShow"
icon-pack="fas"
icon-left="plus"
@click="addFilterInit()">
Add Filter
</b-button>
<b-autocomplete v-if="addFilterShow"
ref="addFilterAutocomplete"
:data="addFilterChoices"
v-model="addFilterTerm"
placeholder="Add Filter"
field="key"
:custom-formatter="formatAddFilterItem"
open-on-focus
keep-first
icon-pack="fas"
clearable
clear-on-select
@select="addFilterSelect">
</b-autocomplete>
<b-button @click="resetView()"
icon-pack="fas"
icon-left="home">
Default View
</b-button>
<b-button @click="clearFilters()"
icon-pack="fas"
icon-left="trash">
No Filters
</b-button>
% if allow_save_defaults and request.user:
<b-button @click="saveDefaults()"
icon-pack="fas"
icon-left="save"
:disabled="savingDefaults">
{{ savingDefaults ? "Working, please wait..." : "Save Defaults" }}
</b-button>
% endif
</div>
</form>
% endif % endif
</div> </div>
</div> </div>
@ -177,7 +115,7 @@
## paging ## paging
% if grid.paginated: % if grid.paginated:
paginated paginated
pagination-size="${'small' if request.use_oruga else 'is-small'}" pagination-size="is-small"
:per-page="perPage" :per-page="perPage"
:current-page="currentPage" :current-page="currentPage"
@page-change="onPageChange" @page-change="onPageChange"
@ -198,8 +136,10 @@
<${b}-table-column field="${column['field']}" <${b}-table-column field="${column['field']}"
label="${column['label']}" label="${column['label']}"
v-slot="props" v-slot="props"
:sortable="${json.dumps(column.get('sortable', False))|n}" :sortable="${json.dumps(column.get('sortable', False))}"
:searchable="${json.dumps(column.get('searchable', False))|n}" % if hasattr(grid, 'is_searchable') and grid.is_searchable(column['field']):
searchable
% endif
cell-class="c_${column['field']}" cell-class="c_${column['field']}"
:visible="${json.dumps(column.get('visible', True))}"> :visible="${json.dumps(column.get('visible', True))}">
% if hasattr(grid, 'raw_renderers') and column['field'] in grid.raw_renderers: % if hasattr(grid, 'raw_renderers') and column['field'] in grid.raw_renderers:
@ -311,16 +251,12 @@
<script type="text/javascript"> <script type="text/javascript">
const ${grid.vue_component}Context = ${json.dumps(grid.get_vue_context())|n} let ${grid.vue_component}CurrentData = ${json.dumps(grid.get_vue_data())|n}
let ${grid.vue_component}CurrentData = ${grid.vue_component}Context.data
let ${grid.vue_component}Data = { let ${grid.vue_component}Data = {
loading: false, loading: false,
ajaxDataUrl: ${json.dumps(getattr(grid, 'ajax_data_url', request.path_url))|n}, ajaxDataUrl: ${json.dumps(getattr(grid, 'ajax_data_url', request.path_url))|n},
## nb. this tracks whether grid.fetchFirstData() happened
fetchedFirstData: false,
savingDefaults: false, savingDefaults: false,
data: ${grid.vue_component}CurrentData, data: ${grid.vue_component}CurrentData,
@ -583,17 +519,6 @@
...this.getFilterParams()} ...this.getFilterParams()}
}, },
## nb. this is meant to call for a grid which is hidden at
## first, when it is first being shown to the user. and if
## it was initialized with empty data set.
async fetchFirstData() {
if (this.fetchedFirstData) {
return
}
await this.loadAsyncData()
this.fetchedFirstData = true
},
## TODO: i noticed buefy docs show using `async` keyword here, ## TODO: i noticed buefy docs show using `async` keyword here,
## so now i am too. knowing nothing at all of if/how this is ## so now i am too. knowing nothing at all of if/how this is
## supposed to improve anything. we shall see i guess ## supposed to improve anything. we shall see i guess
@ -746,7 +671,7 @@
this.loading = true this.loading = true
// use current url proper, plus reset param // use current url proper, plus reset param
let url = '?reset-view=true' let url = '?reset-to-default-filters=true'
// add current hash, to preserve that in redirect // add current hash, to preserve that in redirect
if (location.hash) { if (location.hash) {

View file

@ -0,0 +1,67 @@
## -*- coding: utf-8; -*-
<form action="${form.action_url}" method="GET" @submit.prevent="applyFilters()">
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
<grid-filter v-for="key in filtersSequence"
:key="key"
:filter="filters[key]"
ref="gridFilters">
</grid-filter>
</div>
<div style="display: flex; gap: 0.5rem; margin-top: 0.5rem;">
<b-button type="is-primary"
native-type="submit"
icon-pack="fas"
icon-left="check">
Apply Filters
</b-button>
<b-button v-if="!addFilterShow"
icon-pack="fas"
icon-left="plus"
@click="addFilterInit()">
Add Filter
</b-button>
<b-autocomplete v-if="addFilterShow"
ref="addFilterAutocomplete"
:data="addFilterChoices"
v-model="addFilterTerm"
placeholder="Add Filter"
field="key"
:custom-formatter="formatAddFilterItem"
open-on-focus
keep-first
icon-pack="fas"
clearable
clear-on-select
@select="addFilterSelect">
</b-autocomplete>
<b-button @click="resetView()"
icon-pack="fas"
icon-left="home">
Default View
</b-button>
<b-button @click="clearFilters()"
icon-pack="fas"
icon-left="trash">
No Filters
</b-button>
% if allow_save_defaults and request.user:
<b-button @click="saveDefaults()"
icon-pack="fas"
icon-left="save"
:disabled="savingDefaults">
{{ savingDefaults ? "Working, please wait..." : "Save Defaults" }}
</b-button>
% endif
</div>
</form>

View file

@ -1,7 +1,33 @@
## -*- coding: utf-8; -*- ## -*- coding: utf-8; -*-
<%inherit file="wuttaweb:templates/home.mako" /> <%inherit file="/page.mako" />
<%namespace name="base_meta" file="/base_meta.mako" />
<%def name="title()">Home</%def>
<%def name="extra_styles()">
${parent.extra_styles()}
<style type="text/css">
.logo {
text-align: center;
}
.logo img {
margin: 3em auto;
max-height: 350px;
max-width: 800px;
}
</style>
</%def>
## DEPRECATED; remains for back-compat
<%def name="render_this_page()"> <%def name="render_this_page()">
${self.page_content()} ${self.page_content()}
</%def> </%def>
<%def name="page_content()">
<div class="logo">
${h.image(image_url, "{} logo".format(capture(base_meta.app_title)))}
<h1>Welcome to ${base_meta.app_title()}</h1>
</div>
</%def>
${parent.body()}

View file

@ -144,9 +144,9 @@
</b-modal> </b-modal>
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
ThisPageData.handlersData = ${json.dumps(handlers_data)|n} ThisPageData.handlersData = ${json.dumps(handlers_data)|n}
@ -203,3 +203,6 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -63,9 +63,9 @@
</div> </div>
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
${form.vue_component}Data.submittingRun = false ${form.vue_component}Data.submittingRun = false
${form.vue_component}Data.submittingExplain = false ${form.vue_component}Data.submittingExplain = false
@ -86,3 +86,5 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -1,17 +1,86 @@
## -*- coding: utf-8; -*- ## -*- coding: utf-8; -*-
<%inherit file="wuttaweb:templates/auth/login.mako" /> <%inherit file="/form.mako" />
<%namespace name="base_meta" file="/base_meta.mako" />
<%def name="title()">Login</%def>
## TODO: this will not be needed with wuttaform
<%def name="extra_styles()"> <%def name="extra_styles()">
${parent.extra_styles()} ${parent.extra_styles()}
<style> <style type="text/css">
.card-content .buttons { .logo img {
display: block;
margin: 3rem auto;
max-height: 350px;
max-width: 800px;
}
/* must force a particular label with, in order to make sure */
/* the username and password inputs are the same size */
.field.is-horizontal .field-label .label {
text-align: left;
width: 6rem;
}
.buttons {
justify-content: right; justify-content: right;
} }
</style> </style>
</%def> </%def>
## DEPRECATED; remains for back-compat <%def name="logo()">
${h.image(image_url, "{} logo".format(capture(base_meta.app_title)))}
</%def>
<%def name="login_form()">
<div class="form">
${form.render_deform(form_kwargs={'data-ajax': 'false'})|n}
</div>
</%def>
<%def name="render_this_page()"> <%def name="render_this_page()">
${self.page_content()} ${self.page_content()}
</%def> </%def>
<%def name="page_content()">
<div class="logo">
${self.logo()}
</div>
<div class="columns is-centered">
<div class="column is-narrow">
<div class="card">
<div class="card-content">
<tailbone-form></tailbone-form>
</div>
</div>
</div>
</div>
</%def>
<%def name="modify_this_page_vars()">
<script type="text/javascript">
${form.vue_component}Data.usernameInput = null
${form.vue_component}.mounted = function() {
this.$refs.username.focus()
this.usernameInput = this.$refs.username.$el.querySelector('input')
this.usernameInput.addEventListener('keydown', this.usernameKeydown)
}
${form.vue_component}.beforeDestroy = function() {
this.usernameInput.removeEventListener('keydown', this.usernameKeydown)
}
${form.vue_component}.methods.usernameKeydown = function(event) {
if (event.which == 13) {
event.preventDefault()
this.$refs.password.focus()
}
}
</script>
</%def>
${parent.body()}

View file

@ -297,9 +297,9 @@
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
ThisPageData.overnightTasks = ${json.dumps(overnight_tasks)|n} ThisPageData.overnightTasks = ${json.dumps(overnight_tasks)|n}
ThisPageData.overnightTaskShowDialog = false ThisPageData.overnightTaskShowDialog = false
@ -425,3 +425,6 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -255,9 +255,9 @@
</div> </div>
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
% if master.has_perm('restart_scheduler'): % if master.has_perm('restart_scheduler'):
@ -374,3 +374,6 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -34,9 +34,9 @@
${h.end_form()} ${h.end_form()}
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
TailboneFormData.formSubmitting = false TailboneFormData.formSubmitting = false
TailboneFormData.submitButtonText = "Yes, please clone away" TailboneFormData.submitButtonText = "Yes, please clone away"
@ -48,3 +48,6 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -33,8 +33,8 @@
${h.end_form()} ${h.end_form()}
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script>
${form.vue_component}Data.formSubmitting = false ${form.vue_component}Data.formSubmitting = false
@ -45,3 +45,6 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -1,9 +1,9 @@
## -*- coding: utf-8; -*- ## -*- coding: utf-8; -*-
<%inherit file="/form.mako" /> <%inherit file="/form.mako" />
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
## declare extra data needed by form ## declare extra data needed by form
% if form is not Undefined and getattr(form, 'json_data', None): % if form is not Undefined and getattr(form, 'json_data', None):
@ -28,3 +28,6 @@
% endif % endif
</%def> </%def>
${parent.body()}

View file

@ -265,11 +265,6 @@
</%def> </%def>
## DEPRECATED; remains for back-compat
<%def name="render_this_page()">
${self.page_content()}
</%def>
<%def name="page_content()"> <%def name="page_content()">
% if download_results_path: % if download_results_path:
@ -295,28 +290,34 @@
% endif % endif
</%def> </%def>
<%def name="render_grid_component()">
${grid.render_vue_tag()}
</%def>
##############################
## vue components
##############################
<%def name="render_vue_templates()">
${parent.render_vue_templates()}
## DEPRECATED; called for back-compat
${self.make_grid_component()}
</%def>
## DEPRECATED; remains for back-compat
<%def name="make_grid_component()"> <%def name="make_grid_component()">
${grid.render_vue_template(tools=capture(self.grid_tools).strip(), context_menu=capture(self.context_menu_items).strip())} ${grid.render_vue_template(tools=capture(self.grid_tools).strip(), context_menu=capture(self.context_menu_items).strip())}
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="render_grid_component()">
${parent.modify_vue_vars()} ${grid.render_vue_tag()}
</%def>
<%def name="make_this_page_component()">
## define grid
${self.make_grid_component()}
${parent.make_this_page_component()}
## finalize grid
<script>
${grid.vue_component}.data = function() { return ${grid.vue_component}Data }
Vue.component('${grid.vue_tagname}', ${grid.vue_component})
</script>
</%def>
<%def name="render_this_page()">
${self.page_content()}
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript"> <script type="text/javascript">
% if getattr(master, 'supports_grid_totals', False): % if getattr(master, 'supports_grid_totals', False):
@ -623,10 +624,5 @@
</script> </script>
</%def> </%def>
<%def name="make_vue_components()">
${parent.make_vue_components()} ${parent.body()}
<script>
${grid.vue_component}.data = function() { return ${grid.vue_component}Data }
Vue.component('${grid.vue_tagname}', ${grid.vue_component})
</script>
</%def>

View file

@ -109,8 +109,8 @@
<merge-buttons></merge-buttons> <merge-buttons></merge-buttons>
</%def> </%def>
<%def name="render_vue_templates()"> <%def name="render_this_page_template()">
${parent.render_vue_templates()} ${parent.render_this_page_template()}
<script type="text/x-template" id="merge-buttons-template"> <script type="text/x-template" id="merge-buttons-template">
<div class="level" style="margin-top: 2em;"> <div class="level" style="margin-top: 2em;">
@ -147,7 +147,11 @@
</div> </div>
</div> </div>
</script> </script>
<script> </%def>
<%def name="make_this_page_component()">
${parent.make_this_page_component()}
<script type="text/javascript">
const MergeButtons = { const MergeButtons = {
template: '#merge-buttons-template', template: '#merge-buttons-template',
@ -171,13 +175,12 @@
} }
} }
Vue.component('merge-buttons', MergeButtons)
<% request.register_component('merge-buttons', 'MergeButtons') %>
</script> </script>
</%def> </%def>
<%def name="make_vue_components()">
${parent.make_vue_components()} ${parent.body()}
<script>
Vue.component('merge-buttons', MergeButtons)
<% request.register_component('merge-buttons', 'MergeButtons') %>
</script>
</%def>

View file

@ -16,16 +16,27 @@
${self.page_content()} ${self.page_content()}
</%def> </%def>
<%def name="make_this_page_component()">
${parent.make_this_page_component()}
<script type="text/javascript">
TailboneGrid.data = function() { return TailboneGridData }
Vue.component('tailbone-grid', TailboneGrid)
</script>
</%def>
<%def name="render_this_page_template()">
${parent.render_this_page_template()}
## TODO: stop using |n filter
${grid.render_complete()|n}
</%def>
<%def name="page_content()"> <%def name="page_content()">
${grid.render_vue_tag(**{':csrftoken': 'csrftoken'})} <tailbone-grid :csrftoken="csrftoken">
</tailbone-grid>
</%def> </%def>
<%def name="render_vue_templates()"> ${parent.body()}
${parent.render_vue_templates()}
${grid.render_vue_template()}
</%def>
<%def name="make_vue_components()">
${parent.make_vue_components()}
${grid.render_vue_finalize()}
</%def>

View file

@ -120,7 +120,9 @@
</p> </p>
</div> </div>
${versions_grid.render_vue_tag(ref='versionsGrid', **{'@view-revision': 'viewRevision'})} <versions-grid ref="versionsGrid"
@view-revision="viewRevision">
</versions-grid>
<${b}-modal :width="1200" <${b}-modal :width="1200"
% if request.use_oruga: % if request.use_oruga:
@ -196,7 +198,6 @@
<p class="block has-text-weight-bold"> <p class="block has-text-weight-bold">
{{ version.model_title }} {{ version.model_title }}
({{ version.operation }})
</p> </p>
<table class="diff monospace is-size-7" <table class="diff monospace is-size-7"
@ -236,37 +237,25 @@
</%def> </%def>
<%def name="render_row_grid_component()"> <%def name="render_row_grid_component()">
${rows_grid.render_vue_tag(id='rowGrid', ref='rowGrid')} <tailbone-grid ref="rowGrid" id="rowGrid"></tailbone-grid>
</%def> </%def>
<%def name="render_vue_templates()"> <%def name="render_this_page_template()">
${parent.render_vue_templates()}
% if getattr(master, 'has_rows', False): % if getattr(master, 'has_rows', False):
${rows_grid.render_vue_template(allow_save_defaults=False, tools=capture(self.render_row_grid_tools))} ## TODO: stop using |n filter
${rows_grid.render_complete(allow_save_defaults=False, tools=capture(self.render_row_grid_tools))|n}
% endif % endif
${parent.render_this_page_template()}
% if expose_versions: % if expose_versions:
${versions_grid.render_vue_template()} ${versions_grid.render_complete()|n}
% endif % endif
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> % if expose_versions:
<script type="text/javascript">
% if getattr(master, 'touchable', False) and master.has_perm('touch'):
WholePageData.touchSubmitting = false
WholePage.methods.touchRecord = function() {
this.touchSubmitting = true
location.href = '${master.get_action_url('touch', instance)}'
}
% endif
% if expose_versions:
WholePageData.viewingHistory = false
ThisPage.props.viewingHistory = Boolean ThisPage.props.viewingHistory = Boolean
ThisPageData.gettingRevisions = false ThisPageData.gettingRevisions = false
@ -321,16 +310,48 @@
this.viewVersionShowAllFields = !this.viewVersionShowAllFields this.viewVersionShowAllFields = !this.viewVersionShowAllFields
} }
</script>
% endif
</%def>
<%def name="modify_whole_page_vars()">
${parent.modify_whole_page_vars()}
<script type="text/javascript">
% if getattr(master, 'touchable', False) and master.has_perm('touch'):
WholePageData.touchSubmitting = false
WholePage.methods.touchRecord = function() {
this.touchSubmitting = true
location.href = '${master.get_action_url('touch', instance)}'
}
% endif % endif
% if expose_versions:
WholePageData.viewingHistory = false
% endif
</script> </script>
</%def> </%def>
<%def name="make_vue_components()"> <%def name="finalize_this_page_vars()">
${parent.make_vue_components()} ${parent.finalize_this_page_vars()}
% if getattr(master, 'has_rows', False): <script type="text/javascript">
${rows_grid.render_vue_finalize()}
% endif % if getattr(master, 'has_rows', False):
% if expose_versions: TailboneGrid.data = function() { return TailboneGridData }
${versions_grid.render_vue_finalize()} Vue.component('tailbone-grid', TailboneGrid)
% endif % endif
% if expose_versions:
VersionsGrid.data = function() { return VersionsGridData }
Vue.component('versions-grid', VersionsGrid)
% endif
</script>
</%def> </%def>
${parent.body()}

View file

@ -52,9 +52,9 @@
</div> </div>
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
ThisPage.methods.getLabelForKey = function(key) { ThisPage.methods.getLabelForKey = function(key) {
switch (key) { switch (key) {
@ -75,3 +75,6 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -32,14 +32,14 @@
% endif % endif
</%def> </%def>
<%def name="render_vue_templates()"> <%def name="render_this_page_template()">
${parent.render_vue_templates()} ${parent.render_this_page_template()}
${message_recipients_template()} ${message_recipients_template()}
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
TailboneFormData.possibleRecipients = new Map(${json.dumps(available_recipients)|n}) TailboneFormData.possibleRecipients = new Map(${json.dumps(available_recipients)|n})
TailboneFormData.recipientDisplayMap = ${json.dumps(recipient_display_map)|n} TailboneFormData.recipientDisplayMap = ${json.dumps(recipient_display_map)|n}
@ -59,3 +59,6 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -22,15 +22,15 @@
% endif % endif
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
% if request.matched_route.name in ('messages.inbox', 'messages.archive'): % if request.matched_route.name in ('messages.inbox', 'messages.archive'):
<script> <script type="text/javascript">
${grid.vue_component}Data.moveMessagesSubmitting = false TailboneGridData.moveMessagesSubmitting = false
${grid.vue_component}Data.moveMessagesText = null TailboneGridData.moveMessagesText = null
${grid.vue_component}.computed.moveMessagesTextCurrent = function() { TailboneGrid.computed.moveMessagesTextCurrent = function() {
if (this.moveMessagesText) { if (this.moveMessagesText) {
return this.moveMessagesText return this.moveMessagesText
} }
@ -38,7 +38,7 @@
return "Move " + count.toString() + " selected to ${'Archive' if request.matched_route.name == 'messages.inbox' else 'Inbox'}" return "Move " + count.toString() + " selected to ${'Archive' if request.matched_route.name == 'messages.inbox' else 'Inbox'}"
} }
${grid.vue_component}.methods.moveMessagesSubmit = function() { TailboneGrid.methods.moveMessagesSubmit = function() {
this.moveMessagesSubmitting = true this.moveMessagesSubmitting = true
this.moveMessagesText = "Working, please wait..." this.moveMessagesText = "Working, please wait..."
} }
@ -46,3 +46,6 @@
</script> </script>
% endif % endif
</%def> </%def>
${parent.body()}

View file

@ -82,19 +82,22 @@
</div> </div>
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
${form.vue_component}Data.showingAllRecipients = false TailboneFormData.showingAllRecipients = false
${form.vue_component}.methods.showMoreRecipients = function() { TailboneForm.methods.showMoreRecipients = function() {
this.showingAllRecipients = true this.showingAllRecipients = true
} }
${form.vue_component}.methods.hideMoreRecipients = function() { TailboneForm.methods.hideMoreRecipients = function() {
this.showingAllRecipients = false this.showingAllRecipients = false
} }
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -1,74 +0,0 @@
## -*- coding: utf-8; -*-
<%inherit file="/configure.mako" />
<%def name="form_content()">
<h3 class="block is-size-3">Workflows</h3>
<div class="block" style="padding-left: 2rem;">
<p class="block">
Users can only choose from the workflows enabled below.
</p>
<b-field>
<b-checkbox name="rattail.batch.purchase.allow_ordering_from_scratch"
v-model="simpleSettings['rattail.batch.purchase.allow_ordering_from_scratch']"
native-value="true"
@input="settingsNeedSaved = true">
From Scratch
</b-checkbox>
</b-field>
<b-field>
<b-checkbox name="rattail.batch.purchase.allow_ordering_from_file"
v-model="simpleSettings['rattail.batch.purchase.allow_ordering_from_file']"
native-value="true"
@input="settingsNeedSaved = true">
From Order File
</b-checkbox>
</b-field>
</div>
<h3 class="block is-size-3">Vendors</h3>
<div class="block" style="padding-left: 2rem;">
<b-field message="If not set, user must choose a &quot;supported&quot; vendor.">
<b-checkbox name="rattail.batch.purchase.allow_ordering_any_vendor"
v-model="simpleSettings['rattail.batch.purchase.allow_ordering_any_vendor']"
native-value="true"
@input="settingsNeedSaved = true">
Allow ordering for <span class="has-text-weight-bold">any</span> vendor
</b-checkbox>
</b-field>
</div>
<h3 class="block is-size-3">Order Parsers</h3>
<div class="block" style="padding-left: 2rem;">
<p class="block">
Only the selected file parsers will be exposed to users.
</p>
% for Parser in order_parsers:
<b-field message="${Parser.key}">
<b-checkbox name="order_parser_${Parser.key}"
v-model="orderParsers['${Parser.key}']"
native-value="true"
@input="settingsNeedSaved = true">
${Parser.title}
</b-checkbox>
</b-field>
% endfor
</div>
</%def>
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
ThisPageData.orderParsers = ${json.dumps(order_parsers_data)|n}
</script>
</%def>

View file

@ -21,8 +21,8 @@
% endif % endif
</%def> </%def>
<%def name="render_vue_templates()"> <%def name="render_this_page_template()">
${parent.render_vue_templates()} ${parent.render_this_page_template()}
% if not batch.executed and not batch.complete and master.has_perm('edit_row'): % if not batch.executed and not batch.complete and master.has_perm('edit_row'):
<script type="text/x-template" id="ordering-scanner-template"> <script type="text/x-template" id="ordering-scanner-template">
<div> <div>
@ -185,10 +185,10 @@
% endif % endif
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
% if not batch.executed and not batch.complete and master.has_perm('edit_row'): % if not batch.executed and not batch.complete and master.has_perm('edit_row'):
<script> <script type="text/javascript">
let OrderingScanner = { let OrderingScanner = {
template: '#ordering-scanner-template', template: '#ordering-scanner-template',
@ -204,7 +204,7 @@
saving: false, saving: false,
## TODO: should find a better way to handle CSRF token ## TODO: should find a better way to handle CSRF token
csrftoken: ${json.dumps(h.get_csrf_token(request))|n}, csrftoken: ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n},
} }
}, },
computed: { computed: {
@ -408,11 +408,16 @@
% endif % endif
</%def> </%def>
<%def name="make_vue_components()"> <%def name="make_this_page_component()">
${parent.make_vue_components()} ${parent.make_this_page_component()}
% if not batch.executed and not batch.complete and master.has_perm('edit_row'): % if not batch.executed and not batch.complete and master.has_perm('edit_row'):
<script> <script type="text/javascript">
Vue.component('ordering-scanner', OrderingScanner) Vue.component('ordering-scanner', OrderingScanner)
</script> </script>
% endif % endif
</%def> </%def>
${parent.body()}

View file

@ -199,8 +199,9 @@
<ordering-worksheet></ordering-worksheet> <ordering-worksheet></ordering-worksheet>
</%def> </%def>
<%def name="render_vue_templates()"> <%def name="render_this_page_template()">
${parent.render_vue_templates()} ${parent.render_this_page_template()}
<script type="text/x-template" id="ordering-worksheet-template"> <script type="text/x-template" id="ordering-worksheet-template">
<div> <div>
<div class="form-wrapper"> <div class="form-wrapper">
@ -238,7 +239,11 @@
${self.order_form_grid()} ${self.order_form_grid()}
</div> </div>
</script> </script>
<script> </%def>
<%def name="make_this_page_component()">
${parent.make_this_page_component()}
<script type="text/javascript">
const OrderingWorksheet = { const OrderingWorksheet = {
template: '#ordering-worksheet-template', template: '#ordering-worksheet-template',
@ -250,7 +255,7 @@
submitting: false, submitting: false,
## TODO: should find a better way to handle CSRF token ## TODO: should find a better way to handle CSRF token
csrftoken: ${json.dumps(h.get_csrf_token(request))|n}, csrftoken: ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n},
} }
}, },
methods: { methods: {
@ -293,12 +298,14 @@
}, },
} }
Vue.component('ordering-worksheet', OrderingWorksheet)
</script> </script>
</%def> </%def>
<%def name="make_vue_components()">
${parent.make_vue_components()} ##############################
<script> ## page body
Vue.component('ordering-worksheet', OrderingWorksheet) ##############################
</script>
</%def> ${parent.body()}

View file

@ -1,26 +1,42 @@
## -*- coding: utf-8; -*- ## -*- coding: utf-8; -*-
<%inherit file="/base.mako" /> <%inherit file="/base.mako" />
<%def name="render_vue_templates()"> <%def name="context_menu_items()">
${parent.render_vue_templates()} % if context_menu_list_items is not Undefined:
${self.render_vue_template_this_page()} % for item in context_menu_list_items:
<li>${item}</li>
% endfor
% endif
</%def> </%def>
<%def name="render_vue_template_this_page()"> <%def name="page_content()"></%def>
## DEPRECATED; called for back-compat
${self.render_this_page_template()} <%def name="render_this_page()">
<div style="display: flex;">
<div class="this-page-content" style="flex-grow: 1;">
${self.page_content()}
</div>
<ul id="context-menu">
${self.context_menu_items()}
</ul>
</div>
</%def> </%def>
<%def name="render_this_page_template()"> <%def name="render_this_page_template()">
<script type="text/x-template" id="this-page-template"> <script type="text/x-template" id="this-page-template">
<div> <div>
## DEPRECATED; called for back-compat
${self.render_this_page()} ${self.render_this_page()}
</div> </div>
</script> </script>
<script> </%def>
const ThisPage = { <%def name="declare_this_page_vars()">
<script type="text/javascript">
let ThisPage = {
template: '#this-page-template', template: '#this-page-template',
mixins: [SimpleRequestMixin], mixins: [SimpleRequestMixin],
props: { props: {
@ -36,71 +52,37 @@
}, },
} }
const ThisPageData = { let ThisPageData = {
## TODO: should find a better way to handle CSRF token ## TODO: should find a better way to handle CSRF token
csrftoken: ${json.dumps(h.get_csrf_token(request))|n}, csrftoken: ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n},
} }
</script> </script>
</%def> </%def>
## DEPRECATED; remains for back-compat <%def name="modify_this_page_vars()">
<%def name="render_this_page()"> ## NOTE: if you override this, must use <script> tags
<div style="display: flex;">
<div class="this-page-content" style="flex-grow: 1;">
${self.page_content()}
</div>
## DEPRECATED; remains for back-compat
<ul id="context-menu">
${self.context_menu_items()}
</ul>
</div>
</%def> </%def>
## nb. this is the canonical block for page content! <%def name="finalize_this_page_vars()">
<%def name="page_content()"></%def> ## NOTE: if you override this, must use <script> tags
## DEPRECATED; remains for back-compat
<%def name="context_menu_items()">
% if context_menu_list_items is not Undefined:
% for item in context_menu_list_items:
<li>${item}</li>
% endfor
% endif
</%def>
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
## DEPRECATED; called for back-compat
${self.declare_this_page_vars()}
${self.modify_this_page_vars()}
</%def>
<%def name="make_vue_components()">
${parent.make_vue_components()}
## DEPRECATED; called for back-compat
${self.make_this_page_component()}
</%def> </%def>
<%def name="make_this_page_component()"> <%def name="make_this_page_component()">
${self.declare_this_page_vars()}
${self.modify_this_page_vars()}
${self.finalize_this_page_vars()} ${self.finalize_this_page_vars()}
<script>
<script type="text/javascript">
ThisPage.data = function() { return ThisPageData } ThisPage.data = function() { return ThisPageData }
Vue.component('this-page', ThisPage) Vue.component('this-page', ThisPage)
<% request.register_component('this-page', 'ThisPage') %> <% request.register_component('this-page', 'ThisPage') %>
</script> </script>
</%def> </%def>
##############################
## DEPRECATED
##############################
<%def name="declare_this_page_vars()"></%def> ${self.render_this_page_template()}
${self.make_this_page_component()}
<%def name="modify_this_page_vars()"></%def>
<%def name="finalize_this_page_vars()"></%def>

View file

@ -61,9 +61,9 @@
${parent.grid_tools()} ${parent.grid_tools()}
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
% if getattr(master, 'mergeable', False) and master.has_perm('request_merge'): % if getattr(master, 'mergeable', False) and master.has_perm('request_merge'):
@ -100,3 +100,5 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -18,10 +18,10 @@
% endif % endif
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
% if not instance.merged and request.has_perm('people.merge'): % if not instance.merged and request.has_perm('people.merge'):
<script> <script type="text/javascript">
ThisPageData.mergeFormButtonText = "Perform Merge" ThisPageData.mergeFormButtonText = "Perform Merge"
ThisPageData.mergeFormSubmitting = false ThisPageData.mergeFormSubmitting = false
@ -34,3 +34,5 @@
</script> </script>
% endif % endif
</%def> </%def>
${parent.body()}

View file

@ -2,16 +2,6 @@
<%inherit file="/master/view.mako" /> <%inherit file="/master/view.mako" />
<%namespace file="/util.mako" import="view_profiles_helper" /> <%namespace file="/util.mako" import="view_profiles_helper" />
<%def name="page_content()">
${parent.page_content()}
% if not instance.users and request.has_perm('users.create'):
${h.form(url('people.make_user'), ref='makeUserForm')}
${h.csrf_token(request)}
${h.hidden('person_uuid', value=instance.uuid)}
${h.end_form()}
% endif
</%def>
<%def name="object_helpers()"> <%def name="object_helpers()">
${parent.object_helpers()} ${parent.object_helpers()}
${view_profiles_helper([instance])} ${view_profiles_helper([instance])}
@ -23,9 +13,9 @@
</div> </div>
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
${form.vue_component}.methods.clickMakeUser = function(event) { ${form.vue_component}.methods.clickMakeUser = function(event) {
this.$emit('make-user') this.$emit('make-user')
@ -39,3 +29,17 @@
</script> </script>
</%def> </%def>
<%def name="page_content()">
${parent.page_content()}
% if not instance.users and request.has_perm('users.create'):
${h.form(url('people.make_user'), ref='makeUserForm')}
${h.csrf_token(request)}
${h.hidden('person_uuid', value=instance.uuid)}
${h.end_form()}
% endif
</%def>
${parent.body()}

View file

@ -1966,97 +1966,30 @@
</div> </div>
</script> </script>
<script> </%def>
let ProfileInfoData = { <%def name="render_this_page_template()">
activeTab: location.hash ? location.hash.substring(1) : 'personal', ${parent.render_this_page_template()}
tabchecks: ${json.dumps(tabchecks or {})|n}, ${self.render_personal_tab_template()}
today: '${rattail_app.today()}',
profileLastChanged: Date.now(),
person: ${json.dumps(person_data or {})|n},
phoneTypeOptions: ${json.dumps(phone_type_options or [])|n},
emailTypeOptions: ${json.dumps(email_type_options or [])|n},
maxLengths: ${json.dumps(max_lengths or {})|n},
% if request.has_perm('people_profile.view_versions'): % if expose_members:
loadingRevisions: false, ${self.render_member_tab_template()}
showingRevisionDialog: false, % endif
revision: {},
revisionShowAllFields: false,
% endif
}
let ProfileInfo = { ${self.render_customer_tab_template()}
template: '#profile-info-template', % if expose_customer_shoppers:
props: { ${self.render_shopper_tab_template()}
% if request.has_perm('people_profile.view_versions'): % endif
viewingHistory: Boolean, ${self.render_employee_tab_template()}
gettingRevisions: Boolean, ${self.render_notes_tab_template()}
revisions: Array,
revisionVersionMap: null,
% endif
},
computed: {},
mounted() {
// auto-refresh whichever tab is shown first % if expose_transactions:
## TODO: how to not assume 'personal' is the default tab? ${transactions_grid.render_complete(allow_save_defaults=False)|n}
let tab = this.$refs['tab_' + (this.activeTab || 'personal')] ${self.render_transactions_tab_template()}
if (tab && tab.refreshTab) { % endif
tab.refreshTab()
}
},
methods: {
profileChanged(data) { ${self.render_user_tab_template()}
this.$emit('change-content-title', data.person.dynamic_content_title) ${self.render_profile_info_template()}
this.person = data.person
this.tabchecks = data.tabchecks
this.profileLastChanged = Date.now()
},
activeTabChanged(value) {
location.hash = value
this.refreshTabIfNeeded(value)
this.activeTabChangedExtra(value)
},
refreshTabIfNeeded(key) {
// TODO: this is *always* refreshing, should be more selective (?)
let tab = this.$refs['tab_' + key]
if (tab && tab.refreshIfNeeded) {
tab.refreshIfNeeded(this.profileLastChanged)
}
},
activeTabChangedExtra(value) {},
% if request.has_perm('people_profile.view_versions'):
viewRevision(row) {
this.revision = this.revisionVersionMap[row.txnid]
this.showingRevisionDialog = true
},
viewPrevRevision() {
let txnid = this.revision.prev_txnid
this.revision = this.revisionVersionMap[txnid]
},
viewNextRevision() {
let txnid = this.revision.next_txnid
this.revision = this.revisionVersionMap[txnid]
},
toggleVersionFields() {
this.revisionShowAllFields = !this.revisionShowAllFields
},
% endif
},
}
</script>
</%def> </%def>
<%def name="declare_personal_tab_vars()"> <%def name="declare_personal_tab_vars()">
@ -3089,46 +3022,114 @@
</script> </script>
</%def> </%def>
<%def name="make_profile_info_component()"> <%def name="declare_profile_info_vars()">
<script type="text/javascript">
## DEPRECATED; called for back-compat let ProfileInfoData = {
${self.declare_profile_info_vars()} activeTab: location.hash ? location.hash.substring(1) : 'personal',
tabchecks: ${json.dumps(tabchecks or {})|n},
today: '${rattail_app.today()}',
profileLastChanged: Date.now(),
person: ${json.dumps(person_data or {})|n},
phoneTypeOptions: ${json.dumps(phone_type_options or [])|n},
emailTypeOptions: ${json.dumps(email_type_options or [])|n},
maxLengths: ${json.dumps(max_lengths or {})|n},
% if request.has_perm('people_profile.view_versions'):
loadingRevisions: false,
showingRevisionDialog: false,
revision: {},
revisionShowAllFields: false,
% endif
}
let ProfileInfo = {
template: '#profile-info-template',
props: {
% if request.has_perm('people_profile.view_versions'):
viewingHistory: Boolean,
gettingRevisions: Boolean,
revisions: Array,
revisionVersionMap: null,
% endif
},
computed: {},
mounted() {
// auto-refresh whichever tab is shown first
## TODO: how to not assume 'personal' is the default tab?
let tab = this.$refs['tab_' + (this.activeTab || 'personal')]
if (tab && tab.refreshTab) {
tab.refreshTab()
}
},
methods: {
profileChanged(data) {
this.$emit('change-content-title', data.person.dynamic_content_title)
this.person = data.person
this.tabchecks = data.tabchecks
this.profileLastChanged = Date.now()
},
activeTabChanged(value) {
location.hash = value
this.refreshTabIfNeeded(value)
this.activeTabChangedExtra(value)
},
refreshTabIfNeeded(key) {
// TODO: this is *always* refreshing, should be more selective (?)
let tab = this.$refs['tab_' + key]
if (tab && tab.refreshIfNeeded) {
tab.refreshIfNeeded(this.profileLastChanged)
}
},
activeTabChangedExtra(value) {},
% if request.has_perm('people_profile.view_versions'):
viewRevision(row) {
this.revision = this.revisionVersionMap[row.txnid]
this.showingRevisionDialog = true
},
viewPrevRevision() {
let txnid = this.revision.prev_txnid
this.revision = this.revisionVersionMap[txnid]
},
viewNextRevision() {
let txnid = this.revision.next_txnid
this.revision = this.revisionVersionMap[txnid]
},
toggleVersionFields() {
this.revisionShowAllFields = !this.revisionShowAllFields
},
% endif
},
}
<script>
ProfileInfo.data = function() { return ProfileInfoData }
Vue.component('profile-info', ProfileInfo)
<% request.register_component('profile-info', 'ProfileInfo') %>
</script> </script>
</%def> </%def>
<%def name="render_vue_templates()"> <%def name="make_profile_info_component()">
${parent.render_vue_templates()} ${self.declare_profile_info_vars()}
<script type="text/javascript">
${self.render_personal_tab_template()} ProfileInfo.data = function() { return ProfileInfoData }
Vue.component('profile-info', ProfileInfo)
<% request.register_component('profile-info', 'ProfileInfo') %>
% if expose_members: </script>
${self.render_member_tab_template()}
% endif
${self.render_customer_tab_template()}
% if expose_customer_shoppers:
${self.render_shopper_tab_template()}
% endif
${self.render_employee_tab_template()}
${self.render_notes_tab_template()}
% if expose_transactions:
${transactions_grid.render_complete(allow_save_defaults=False)|n}
${self.render_transactions_tab_template()}
% endif
${self.render_user_tab_template()}
${self.render_profile_info_template()}
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
% if request.has_perm('people_profile.view_versions'): % if request.has_perm('people_profile.view_versions'):
ThisPage.props.viewingHistory = Boolean ThisPage.props.viewingHistory = Boolean
@ -3176,8 +3177,45 @@
}, },
} }
</script>
</%def>
% if request.has_perm('people_profile.view_versions'): <%def name="make_this_page_component()">
${parent.make_this_page_component()}
${self.make_personal_tab_component()}
% if expose_members:
${self.make_member_tab_component()}
% endif
${self.make_customer_tab_component()}
% if expose_customer_shoppers:
${self.make_shopper_tab_component()}
% endif
${self.make_employee_tab_component()}
${self.make_notes_tab_component()}
% if expose_transactions:
<script type="text/javascript">
TransactionsGrid.data = function() { return TransactionsGridData }
Vue.component('transactions-grid', TransactionsGrid)
## TODO: why is this line not needed?
## <% request.register_component('transactions-grid', 'TransactionsGrid') %>
</script>
${self.make_transactions_tab_component()}
% endif
${self.make_user_tab_component()}
${self.make_profile_info_component()}
</%def>
<%def name="modify_whole_page_vars()">
${parent.modify_whole_page_vars()}
% if request.has_perm('people_profile.view_versions'):
<script type="text/javascript">
WholePageData.viewingHistory = false WholePageData.viewingHistory = false
WholePageData.gettingRevisions = false WholePageData.gettingRevisions = false
@ -3213,44 +3251,9 @@
}) })
} }
% endif
</script>
</%def>
<%def name="make_vue_components()">
${parent.make_vue_components()}
${self.make_personal_tab_component()}
% if expose_members:
${self.make_member_tab_component()}
% endif
${self.make_customer_tab_component()}
% if expose_customer_shoppers:
${self.make_shopper_tab_component()}
% endif
${self.make_employee_tab_component()}
${self.make_notes_tab_component()}
% if expose_transactions:
<script type="text/javascript">
TransactionsGrid.data = function() { return TransactionsGridData }
Vue.component('transactions-grid', TransactionsGrid)
## TODO: why is this line not needed?
## <% request.register_component('transactions-grid', 'TransactionsGrid') %>
</script> </script>
${self.make_transactions_tab_component()}
% endif % endif
${self.make_user_tab_component()}
${self.make_profile_info_component()}
</%def> </%def>
##############################
## DEPRECATED
##############################
<%def name="declare_profile_info_vars()"></%def> ${parent.body()}

View file

@ -62,13 +62,19 @@
<br /> <br />
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
% if master.has_perm('replace'): % if master.has_perm('replace'):
<script> <script type="text/javascript">
${form.vue_component}Data.showUploadForm = false
${form.vue_component}Data.uploadFile = null ${form.vue_component}Data.showUploadForm = false
${form.vue_component}Data.uploadSubmitting = false
</script> ${form.vue_component}Data.uploadFile = null
${form.vue_component}Data.uploadSubmitting = false
</script>
% endif % endif
</%def> </%def>
${parent.body()}

View file

@ -118,9 +118,14 @@
% endif % endif
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
ThisPageData.setupSubmitting = false ThisPageData.setupSubmitting = false
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -10,16 +10,8 @@
</find-principals> </find-principals>
</%def> </%def>
<%def name="principal_table()"> <%def name="render_this_page_template()">
<div ${parent.render_this_page_template()}
style="width: 50%;"
>
${grid.render_table_element(data_prop='principalsData')|n}
</div>
</%def>
<%def name="render_vue_templates()">
${parent.render_vue_templates()}
<script type="text/x-template" id="find-principals-template"> <script type="text/x-template" id="find-principals-template">
<div> <div>
@ -98,6 +90,28 @@
</div> </div>
</script> </script>
</%def>
<%def name="principal_table()">
<div
style="width: 50%;"
>
${grid.render_table_element(data_prop='principalsData')|n}
</div>
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
ThisPageData.permissionGroups = ${json.dumps(perms_data)|n}
ThisPageData.sortedGroups = ${json.dumps(sorted_groups_data)|n}
</script>
</%def>
<%def name="make_this_page_component()">
${parent.make_this_page_component()}
<script type="text/javascript"> <script type="text/javascript">
const FindPrincipals = { const FindPrincipals = {
@ -226,21 +240,12 @@
} }
} }
</script>
</%def>
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
ThisPageData.permissionGroups = ${json.dumps(perms_data)|n}
ThisPageData.sortedGroups = ${json.dumps(sorted_groups_data)|n}
</script>
</%def>
<%def name="make_vue_components()">
${parent.make_vue_components()}
<script>
Vue.component('find-principals', FindPrincipals) Vue.component('find-principals', FindPrincipals)
<% request.register_component('find-principals', 'FindPrincipals') %> <% request.register_component('find-principals', 'FindPrincipals') %>
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -55,20 +55,19 @@
</%def> </%def>
<%def name="render_form_template()"> <%def name="render_form_template()">
<script type="text/x-template" id="${form.vue_tagname}-template"> <script type="text/x-template" id="${form.component}-template">
${self.render_form_innards()} ${self.render_form_innards()}
</script> </script>
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<% request.register_component(form.vue_tagname, form.vue_component) %> <script type="text/javascript">
<script>
## TODO: ugh, an awful lot of duplicated code here (from /forms/deform.mako) ## TODO: ugh, an awful lot of duplicated code here (from /forms/deform.mako)
let ${form.vue_component} = { let ${form.vue_component} = {
template: '#${form.vue_tagname}-template', template: '#${form.component}-template',
methods: { methods: {
## TODO: deprecate / remove the latter option here ## TODO: deprecate / remove the latter option here
@ -115,3 +114,6 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -95,9 +95,9 @@
</div> </div>
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
ThisPage.methods.getTitleForKey = function(key) { ThisPage.methods.getTitleForKey = function(key) {
switch (key) { switch (key) {
@ -118,3 +118,6 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -36,10 +36,10 @@
</${grid.component}> </${grid.component}>
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
% if label_profiles and master.has_perm('print_labels'): % if label_profiles and master.has_perm('print_labels'):
<script> <script type="text/javascript">
${grid.vue_component}Data.quickLabelProfile = ${json.dumps(label_profiles[0].uuid)|n} ${grid.vue_component}Data.quickLabelProfile = ${json.dumps(label_profiles[0].uuid)|n}
${grid.vue_component}Data.quickLabelQuantity = 1 ${grid.vue_component}Data.quickLabelQuantity = 1
@ -83,3 +83,6 @@
</script> </script>
% endif % endif
</%def> </%def>
${parent.body()}

View file

@ -2,6 +2,11 @@
<%inherit file="/master/view.mako" /> <%inherit file="/master/view.mako" />
<%namespace name="product_lookup" file="/products/lookup.mako" /> <%namespace name="product_lookup" file="/products/lookup.mako" />
<%def name="render_this_page_template()">
${parent.render_this_page_template()}
${product_lookup.tailbone_product_lookup_template()}
</%def>
<%def name="page_content()"> <%def name="page_content()">
${parent.page_content()} ${parent.page_content()}
@ -62,14 +67,9 @@
% endif % endif
</%def> </%def>
<%def name="render_vue_templates()"> <%def name="modify_this_page_vars()">
${parent.render_vue_templates()} ${parent.modify_this_page_vars()}
${product_lookup.tailbone_product_lookup_template()} <script type="text/javascript">
</%def>
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
% if master.has_perm('ignore_product') and instance.status_code in (enum.PENDING_PRODUCT_STATUS_PENDING, enum.PENDING_PRODUCT_STATUS_READY): % if master.has_perm('ignore_product') and instance.status_code in (enum.PENDING_PRODUCT_STATUS_PENDING, enum.PENDING_PRODUCT_STATUS_READY):
@ -124,7 +124,10 @@
</script> </script>
</%def> </%def>
<%def name="make_vue_components()"> <%def name="make_this_page_component()">
${parent.make_vue_components()} ${parent.make_this_page_component()}
${product_lookup.tailbone_product_lookup_component()} ${product_lookup.tailbone_product_lookup_component()}
</%def> </%def>
${parent.body()}

View file

@ -282,9 +282,9 @@
% endif % endif
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
ThisPageData.vendorSourcesData = ${json.dumps(vendor_sources['data'])|n} ThisPageData.vendorSourcesData = ${json.dumps(vendor_sources['data'])|n}
ThisPageData.lookupCodesData = ${json.dumps(lookup_codes['data'])|n} ThisPageData.lookupCodesData = ${json.dumps(lookup_codes['data'])|n}
@ -411,3 +411,6 @@
% endif % endif
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -59,9 +59,9 @@
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
${grid.vue_component}Data.changeStatusShowDialog = false ${grid.vue_component}Data.changeStatusShowDialog = false
${grid.vue_component}Data.changeStatusOptions = ${json.dumps(status_options)|n} ${grid.vue_component}Data.changeStatusOptions = ${json.dumps(status_options)|n}
@ -80,3 +80,6 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -69,12 +69,12 @@
<h3 class="block is-size-3">Vendors</h3> <h3 class="block is-size-3">Vendors</h3>
<div class="block" style="padding-left: 2rem;"> <div class="block" style="padding-left: 2rem;">
<b-field message="If not set, user must choose a &quot;supported&quot; vendor."> <b-field message="If set, user must choose a &quot;supported&quot; vendor; otherwise they may choose &quot;any&quot; vendor.">
<b-checkbox name="rattail.batch.purchase.allow_receiving_any_vendor" <b-checkbox name="rattail.batch.purchase.supported_vendors_only"
v-model="simpleSettings['rattail.batch.purchase.allow_receiving_any_vendor']" v-model="simpleSettings['rattail.batch.purchase.supported_vendors_only']"
native-value="true" native-value="true"
@input="settingsNeedSaved = true"> @input="settingsNeedSaved = true">
Allow receiving for <span class="has-text-weight-bold">any</span> vendor Only allow batch for "supported" vendors
</b-checkbox> </b-checkbox>
</b-field> </b-field>

View file

@ -139,15 +139,9 @@
% endif % endif
</%def> </%def>
<%def name="object_helpers()"> <%def name="render_this_page_template()">
${self.render_status_breakdown()} ${parent.render_this_page_template()}
${self.render_po_vs_invoice_helper()}
${self.render_execute_helper()}
${self.render_tools_helper()}
</%def>
<%def name="render_vue_templates()">
${parent.render_vue_templates()}
% if allow_edit_catalog_unit_cost or allow_edit_invoice_unit_cost: % if allow_edit_catalog_unit_cost or allow_edit_invoice_unit_cost:
<script type="text/x-template" id="receiving-cost-editor-template"> <script type="text/x-template" id="receiving-cost-editor-template">
<div> <div>
@ -168,9 +162,16 @@
% endif % endif
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="object_helpers()">
${parent.modify_vue_vars()} ${self.render_status_breakdown()}
<script> ${self.render_po_vs_invoice_helper()}
${self.render_execute_helper()}
${self.render_tools_helper()}
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
% if allow_confirm_all_costs: % if allow_confirm_all_costs:
@ -388,3 +389,6 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -484,9 +484,9 @@
</div> </div>
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
## ThisPage.methods.editUnitCost = function() { ## ThisPage.methods.editUnitCost = function() {
## alert("TODO: not yet implemented") ## alert("TODO: not yet implemented")
@ -720,3 +720,6 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -53,13 +53,13 @@
% endif % endif
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
${form.vue_component}Data.reportDescriptions = ${json.dumps(report_descriptions)|n} TailboneFormData.reportDescriptions = ${json.dumps(report_descriptions)|n}
${form.vue_component}.methods.reportTypeChanged = function(reportType) { TailboneForm.methods.reportTypeChanged = function(reportType) {
this.$emit('report-change', this.reportDescriptions[reportType]) this.$emit('report-change', this.reportDescriptions[reportType])
} }
@ -71,3 +71,6 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -1,11 +1,16 @@
## -*- coding: utf-8; -*- ## -*- coding: utf-8; -*-
<%inherit file="/master/delete.mako" /> <%inherit file="/master/delete.mako" />
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
% if params_data is not Undefined: % if params_data is not Undefined:
${form.vue_component}Data.paramsData = ${json.dumps(params_data)|n} ${form.vue_component}Data.paramsData = ${json.dumps(params_data)|n}
% endif % endif
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -23,11 +23,16 @@
% endif % endif
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
% if params_data is not Undefined: % if params_data is not Undefined:
${form.vue_component}Data.paramsData = ${json.dumps(params_data)|n} ${form.vue_component}Data.paramsData = ${json.dumps(params_data)|n}
% endif % endif
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -48,10 +48,15 @@
${h.end_form()} ${h.end_form()}
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
ThisPageData.departments = ${json.dumps([{'uuid': d.uuid, 'name': d.name} for d in departments])|n} ThisPageData.departments = ${json.dumps([{'uuid': d.uuid, 'name': d.name} for d in departments])|n}
ThisPageData.excludeNotForSale = true ThisPageData.excludeNotForSale = true
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -81,9 +81,9 @@
<%def name="extra_fields()"></%def> <%def name="extra_fields()"></%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
ThisPageData.vendorUUID = null ThisPageData.vendorUUID = null
ThisPageData.departments = [] ThisPageData.departments = []
@ -127,3 +127,6 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -45,10 +45,11 @@
<b-button @click="runReportShowDialog = false"> <b-button @click="runReportShowDialog = false">
Cancel Cancel
</b-button> </b-button>
${h.form(master.get_action_url('execute', instance), **{'@submit': 'runReportSubmitting = true'})} ${h.form(master.get_action_url('execute', instance))}
${h.csrf_token(request)} ${h.csrf_token(request)}
<b-button type="is-primary" <b-button type="is-primary"
native-type="submit" native-type="submit"
@click="runReportSubmitting = true"
:disabled="runReportSubmitting" :disabled="runReportSubmitting"
icon-pack="fas" icon-pack="fas"
icon-left="arrow-circle-right"> icon-left="arrow-circle-right">
@ -61,9 +62,9 @@
% endif % endif
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
% if weekdays_data is not Undefined: % if weekdays_data is not Undefined:
${form.vue_component}Data.weekdaysData = ${json.dumps(weekdays_data)|n} ${form.vue_component}Data.weekdaysData = ${json.dumps(weekdays_data)|n}
@ -74,3 +75,6 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -6,11 +6,15 @@
${h.stylesheet_link(request.static_url('tailbone:static/css/perms.css'))} ${h.stylesheet_link(request.static_url('tailbone:static/css/perms.css'))}
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
// TODO: this variable name should be more dynamic (?) since this is // TODO: this variable name should be more dynamic (?) since this is
// connected to (and only here b/c of) the permissions field // connected to (and only here b/c of) the permissions field
${form.vue_component}Data.showingPermissionGroup = '' TailboneFormData.showingPermissionGroup = ''
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -6,11 +6,15 @@
${h.stylesheet_link(request.static_url('tailbone:static/css/perms.css'))} ${h.stylesheet_link(request.static_url('tailbone:static/css/perms.css'))}
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
// TODO: this variable name should be more dynamic (?) since this is // TODO: this variable name should be more dynamic (?) since this is
// connected to (and only here b/c of) the permissions field // connected to (and only here b/c of) the permissions field
${form.vue_component}Data.showingPermissionGroup = '' TailboneFormData.showingPermissionGroup = ''
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -6,9 +6,9 @@
${h.stylesheet_link(request.static_url('tailbone:static/css/perms.css'))} ${h.stylesheet_link(request.static_url('tailbone:static/css/perms.css'))}
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
% if users_data is not Undefined: % if users_data is not Undefined:
${form.vue_component}Data.usersData = ${json.dumps(users_data)|n} ${form.vue_component}Data.usersData = ${json.dumps(users_data)|n}
@ -23,3 +23,5 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -86,9 +86,9 @@
</div> </div>
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
ThisPageData.testRecipient = ${json.dumps(user_email_address)|n} ThisPageData.testRecipient = ${json.dumps(user_email_address)|n}
ThisPageData.sendingTest = false ThisPageData.sendingTest = false
@ -137,3 +137,6 @@
% endif % endif
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -15,10 +15,10 @@
${parent.render_grid_component()} ${parent.render_grid_component()}
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
% if master.has_perm('configure'): % if master.has_perm('configure'):
<script> <script type="text/javascript">
ThisPageData.showEmails = 'available' ThisPageData.showEmails = 'available'
@ -65,3 +65,5 @@
</script> </script>
% endif % endif
</%def> </%def>
${parent.body()}

View file

@ -6,8 +6,8 @@
<email-preview-tools></email-preview-tools> <email-preview-tools></email-preview-tools>
</%def> </%def>
<%def name="render_vue_templates()"> <%def name="render_this_page_template()">
${parent.render_vue_templates()} ${parent.render_this_page_template()}
<script type="text/x-template" id="email-preview-tools-template"> <script type="text/x-template" id="email-preview-tools-template">
${h.form(url('email.preview'), **{'@submit': 'submitPreviewForm'})} ${h.form(url('email.preview'), **{'@submit': 'submitPreviewForm'})}
@ -72,6 +72,10 @@
${h.end_form()} ${h.end_form()}
</script> </script>
</%def>
<%def name="make_this_page_component()">
${parent.make_this_page_component()}
<script type="text/javascript"> <script type="text/javascript">
const EmailPreviewTools = { const EmailPreviewTools = {
@ -96,13 +100,12 @@
} }
} }
Vue.component('email-preview-tools', EmailPreviewTools)
<% request.register_component('email-preview-tools', 'EmailPreviewTools') %>
</script> </script>
</%def> </%def>
<%def name="make_vue_components()">
${parent.make_vue_components()} ${parent.body()}
<script>
Vue.component('email-preview-tools', EmailPreviewTools)
<% request.register_component('email-preview-tools', 'EmailPreviewTools') %>
</script>
</%def>

View file

@ -695,9 +695,9 @@
</b-steps> </b-steps>
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
// nb. for warning user they may lose changes if leaving page // nb. for warning user they may lose changes if leaving page
ThisPageData.dirty = false ThisPageData.dirty = false
@ -983,3 +983,6 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -8,9 +8,14 @@
% endif % endif
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
${form.vue_component}Data.probesData = ${json.dumps(probes_data)|n} ${form.vue_component}Data.probesData = ${json.dumps(probes_data)|n}
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -22,9 +22,14 @@
% endif % endif
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
${form.vue_component}Data.probesData = ${json.dumps(probes_data)|n} ${form.vue_component}Data.probesData = ${json.dumps(probes_data)|n}
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -59,9 +59,9 @@
% endif % endif
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
ThisPageData.appliances = ${json.dumps(appliances_data)|n} ThisPageData.appliances = ${json.dumps(appliances_data)|n}
ThisPageData.applianceUUID = ${json.dumps(appliance.uuid if appliance else None)|n} ThisPageData.applianceUUID = ${json.dumps(appliance.uuid if appliance else None)|n}
@ -118,3 +118,6 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -66,9 +66,9 @@
<canvas ref="tempchart" width="400" height="150"></canvas> <canvas ref="tempchart" width="400" height="150"></canvas>
</%def> </%def>
<%def name="modify_vue_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_vue_vars()} ${parent.modify_this_page_vars()}
<script> <script type="text/javascript">
ThisPageData.currentTimeRange = ${json.dumps(current_time_range)|n} ThisPageData.currentTimeRange = ${json.dumps(current_time_range)|n}
ThisPageData.chart = null ThisPageData.chart = null
@ -128,3 +128,6 @@
</script> </script>
</%def> </%def>
${parent.body()}

View file

@ -20,21 +20,38 @@
</head> </head>
<body> <body>
<div id="app" style="height: 100%;"> <div id="app" style="height: 100%; display: flex; flex-direction: column; justify-content: space-between;">
<whole-page></whole-page> <whole-page></whole-page>
</div> </div>
## TODO: this must come before the self.body() call..but why? ## TODO: this must come before the self.body() call..but why?
${declare_formposter_mixin()} ${declare_formposter_mixin()}
## global components used by various (but not all) pages
${make_field_components()}
${make_grid_filter_components()}
## global components for buefy-based template compatibility
${make_http_plugin()}
${make_buefy_plugin()}
${make_buefy_components()}
## special global components, used by WholePage
${self.make_menu_search_component()}
${page_help.render_template()}
${page_help.declare_vars()}
% if request.has_perm('common.feedback'):
${self.make_feedback_component()}
% endif
## WholePage component
${self.make_whole_page_component()}
## content body from derived/child template ## content body from derived/child template
${self.body()} ${self.body()}
## Vue app ## Vue app
${self.render_vue_templates()} ${self.make_whole_page_app()}
${self.modify_vue_vars()}
${self.make_vue_components()}
${self.make_vue_app()}
</body> </body>
</html> </html>
@ -579,7 +596,7 @@
</script> </script>
</%def> </%def>
<%def name="render_vue_template_whole_page()"> <%def name="render_whole_page_template()">
<script type="text/x-template" id="whole-page-template"> <script type="text/x-template" id="whole-page-template">
<div id="whole-page" style="height: 100%; display: flex; flex-direction: column; justify-content: space-between;"> <div id="whole-page" style="height: 100%; display: flex; flex-direction: column; justify-content: space-between;">
@ -879,6 +896,8 @@
</footer> </footer>
</div> </div>
</script> </script>
## ${multi_file_upload.render_template()}
</%def> </%def>
<%def name="render_this_page_component()"> <%def name="render_this_page_component()">
@ -909,7 +928,7 @@
${h.form(url('stop_root'), ref='stopBeingRootForm')} ${h.form(url('stop_root'), ref='stopBeingRootForm')}
${h.csrf_token(request)} ${h.csrf_token(request)}
<input type="hidden" name="referrer" value="${request.current_route_url()}" /> <input type="hidden" name="referrer" value="${request.current_route_url()}" />
<a @click="$refs.stopBeingRootForm.submit()" <a @click="stopBeingRoot()"
class="navbar-item has-background-danger has-text-white"> class="navbar-item has-background-danger has-text-white">
Stop being root Stop being root
</a> </a>
@ -918,7 +937,7 @@
${h.form(url('become_root'), ref='startBeingRootForm')} ${h.form(url('become_root'), ref='startBeingRootForm')}
${h.csrf_token(request)} ${h.csrf_token(request)}
<input type="hidden" name="referrer" value="${request.current_route_url()}" /> <input type="hidden" name="referrer" value="${request.current_route_url()}" />
<a @click="$refs.startBeingRootForm.submit()" <a @click="startBeingRoot()"
class="navbar-item has-background-danger has-text-white"> class="navbar-item has-background-danger has-text-white">
Become root Become root
</a> </a>
@ -1049,7 +1068,9 @@
% endif % endif
</%def> </%def>
<%def name="render_vue_script_whole_page()"> <%def name="declare_whole_page_vars()">
## ${multi_file_upload.declare_vars()}
<script> <script>
const WholePage = { const WholePage = {
@ -1103,6 +1124,18 @@
const key = 'menu_' + hash + '_shown' const key = 'menu_' + hash + '_shown'
this[key] = !this[key] this[key] = !this[key]
}, },
% if request.is_admin:
startBeingRoot() {
this.$refs.startBeingRootForm.submit()
},
stopBeingRoot() {
this.$refs.stopBeingRootForm.submit()
},
% endif
}, },
} }
@ -1139,71 +1172,26 @@
</script> </script>
</%def> </%def>
############################## <%def name="modify_whole_page_vars()"></%def>
## vue components + app
##############################
<%def name="render_vue_templates()"> ## TODO: do we really need this?
## ${multi_file_upload.render_template()} ## <%def name="finalize_whole_page_vars()"></%def>
## ${multi_file_upload.declare_vars()}
## global components used by various (but not all) pages
${make_field_components()}
${make_grid_filter_components()}
## global components for buefy-based template compatibility
${make_http_plugin()}
${make_buefy_plugin()}
${make_buefy_components()}
## special global components, used by WholePage
${self.make_menu_search_component()}
${page_help.render_template()}
${page_help.declare_vars()}
% if request.has_perm('common.feedback'):
${self.make_feedback_component()}
% endif
## DEPRECATED; called for back-compat
${self.render_whole_page_template()}
## DEPRECATED; called for back-compat
${self.declare_whole_page_vars()}
</%def>
## DEPRECATED; remains for back-compat
<%def name="render_whole_page_template()">
${self.render_vue_template_whole_page()}
${self.render_vue_script_whole_page()}
</%def>
<%def name="modify_vue_vars()">
## DEPRECATED; called for back-compat
${self.modify_whole_page_vars()}
</%def>
<%def name="make_vue_components()">
${page_help.make_component()}
## ${multi_file_upload.make_component()}
## DEPRECATED; called for back-compat (?)
${self.make_whole_page_component()}
</%def>
## DEPRECATED; remains for back-compat
<%def name="make_whole_page_component()"> <%def name="make_whole_page_component()">
${self.render_whole_page_template()}
${self.declare_whole_page_vars()}
${self.modify_whole_page_vars()}
## ${self.finalize_whole_page_vars()}
${page_help.make_component()}
## ${multi_file_upload.make_component()}
<script> <script>
WholePage.data = () => { return WholePageData } WholePage.data = () => { return WholePageData }
</script> </script>
<% request.register_component('whole-page', 'WholePage') %> <% request.register_component('whole-page', 'WholePage') %>
</%def> </%def>
<%def name="make_vue_app()">
## DEPRECATED; called for back-compat
${self.make_whole_page_app()}
</%def>
## DEPRECATED; remains for back-compat
<%def name="make_whole_page_app()"> <%def name="make_whole_page_app()">
<script type="module"> <script type="module">
import {createApp} from 'vue' import {createApp} from 'vue'
@ -1235,11 +1223,3 @@
app.mount('#app') app.mount('#app')
</script> </script>
</%def> </%def>
##############################
## DEPRECATED
##############################
<%def name="declare_whole_page_vars()"></%def>
<%def name="modify_whole_page_vars()"></%def>

View file

@ -666,7 +666,6 @@
<%def name="make_b_tooltip_component()"> <%def name="make_b_tooltip_component()">
<script type="text/x-template" id="b-tooltip-template"> <script type="text/x-template" id="b-tooltip-template">
<o-tooltip :label="label" <o-tooltip :label="label"
:position="orugaPosition"
:multiline="multilined"> :multiline="multilined">
<slot /> <slot />
</o-tooltip> </o-tooltip>
@ -677,14 +676,6 @@
props: { props: {
label: String, label: String,
multilined: Boolean, multilined: Boolean,
position: String,
},
computed: {
orugaPosition() {
if (this.position) {
return this.position.replace(/^is-/, '')
}
},
}, },
} }
</script> </script>

View file

@ -1,504 +0,0 @@
## -*- coding: utf-8; -*-
<%inherit file="wuttaweb:templates/base.mako" />
<%namespace name="base_meta" file="/base_meta.mako" />
<%namespace file="/formposter.mako" import="declare_formposter_mixin" />
<%namespace file="/grids/filter-components.mako" import="make_grid_filter_components" />
<%namespace name="page_help" file="/page_help.mako" />
<%def name="base_styles()">
${parent.base_styles()}
${h.stylesheet_link(request.static_url('tailbone:static/css/diffs.css') + '?ver={}'.format(tailbone.__version__))}
<style>
.filters .filter-fieldname .field,
.filters .filter-fieldname .field label {
width: 100%;
}
.filters .filter-fieldname,
.filters .filter-fieldname .field label,
.filters .filter-fieldname .button {
justify-content: left;
}
.filters .filter-verb .select,
.filters .filter-verb .select select {
width: 100%;
}
% if filter_fieldname_width is not Undefined:
.filters .filter-fieldname,
.filters .filter-fieldname .button {
min-width: ${filter_fieldname_width};
}
.filters .filter-verb {
min-width: ${filter_verb_width};
}
% endif
</style>
</%def>
<%def name="before_content()">
## TODO: this must come before the self.body() call..but why?
${declare_formposter_mixin()}
</%def>
<%def name="render_navbar_brand()">
<div class="navbar-brand">
<a class="navbar-item" href="${url('home')}"
v-show="!menuSearchActive">
<div style="display: flex; align-items: center;">
${base_meta.header_logo()}
<div id="navbar-brand-title">
${base_meta.global_title()}
</div>
</div>
</a>
<div v-show="menuSearchActive"
class="navbar-item">
<b-autocomplete ref="menuSearchAutocomplete"
v-model="menuSearchTerm"
:data="menuSearchFilteredData"
field="label"
open-on-focus
keep-first
icon-pack="fas"
clearable
@keydown.native="menuSearchKeydown"
@select="menuSearchSelect">
</b-autocomplete>
</div>
<a role="button" class="navbar-burger" data-target="navbar-menu" aria-label="menu" aria-expanded="false">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
</%def>
<%def name="render_navbar_start()">
<div class="navbar-start">
<div v-if="menuSearchData.length"
class="navbar-item">
<b-button type="is-primary"
size="is-small"
@click="menuSearchInit()">
<span><i class="fa fa-search"></i></span>
</b-button>
</div>
% for topitem in menus:
% if topitem['is_link']:
${h.link_to(topitem['title'], topitem['url'], target=topitem['target'], class_='navbar-item')}
% else:
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">${topitem['title']}</a>
<div class="navbar-dropdown">
% for item in topitem['items']:
% if item['is_menu']:
<% item_hash = id(item) %>
<% toggle = 'menu_{}_shown'.format(item_hash) %>
<div>
<a class="navbar-link" @click.prevent="toggleNestedMenu('${item_hash}')">
${item['title']}
</a>
</div>
% for subitem in item['items']:
% if subitem['is_sep']:
<hr class="navbar-divider" v-show="${toggle}">
% else:
${h.link_to("{}".format(subitem['title']), subitem['url'], class_='navbar-item nested', target=subitem['target'], **{'v-show': toggle})}
% endif
% endfor
% else:
% if item['is_sep']:
<hr class="navbar-divider">
% else:
${h.link_to(item['title'], item['url'], class_='navbar-item', target=item['target'])}
% endif
% endif
% endfor
</div>
</div>
% endif
% endfor
</div>
</%def>
<%def name="render_theme_picker()">
% if expose_theme_picker and request.has_perm('common.change_app_theme'):
<div class="level-item">
${h.form(url('change_theme'), method="post", ref='themePickerForm')}
${h.csrf_token(request)}
<input type="hidden" name="referrer" :value="referrer" />
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span>Theme:</span>
<b-select name="theme"
v-model="globalTheme"
@input="changeTheme()">
% for option in theme_picker_options:
<option value="${option.value}">
${option.label}
</option>
% endfor
</b-select>
</div>
${h.end_form()}
</div>
% endif
</%def>
<%def name="render_feedback_button()">
<div class="level-item">
<page-help
% if can_edit_help:
@configure-fields-help="configureFieldsHelp = true"
% endif
/>
</div>
${parent.render_feedback_button()}
</%def>
<%def name="render_crud_header_buttons()">
% if master:
% if master.viewing:
% if instance_editable and master.has_perm('edit'):
<wutta-button once
tag="a" href="${master.get_action_url('edit', instance)}"
icon-left="edit"
label="Edit This" />
% endif
% if getattr(master, 'cloneable', False) and not master.cloning and master.has_perm('clone'):
<wutta-button once
tag="a" href="${master.get_action_url('clone', instance)}"
icon-left="object-ungroup"
label="Clone This" />
% endif
% if instance_deletable and master.has_perm('delete'):
<wutta-button once type="is-danger"
tag="a" href="${master.get_action_url('delete', instance)}"
icon-left="trash"
label="Delete This" />
% endif
% elif master.editing:
% if master.has_perm('view'):
<wutta-button once
tag="a" href="${master.get_action_url('view', instance)}"
icon-left="eye"
label="View This" />
% endif
% if instance_deletable and master.has_perm('delete'):
<wutta-button once type="is-danger"
tag="a" href="${master.get_action_url('delete', instance)}"
icon-left="trash"
label="Delete This" />
% endif
% elif master.deleting:
% if master.has_perm('view'):
<wutta-button once
tag="a" href="${master.get_action_url('view', instance)}"
icon-left="eye"
label="View This" />
% endif
% if instance_editable and master.has_perm('edit'):
<wutta-button once
tag="a" href="${master.get_action_url('edit', instance)}"
icon-left="edit"
label="Edit This" />
% endif
% endif
% endif
</%def>
<%def name="render_prevnext_header_buttons()">
% if show_prev_next is not Undefined and show_prev_next:
% if prev_url:
<wutta-button once
tag="a" href="${prev_url}"
icon-left="arrow-left"
label="Older" />
% else:
<b-button tag="a" href="#"
disabled
icon-pack="fas"
icon-left="arrow-left">
Older
</b-button>
% endif
% if next_url:
<wutta-button once
tag="a" href="${next_url}"
icon-left="arrow-right"
label="Newer" />
% else:
<b-button tag="a" href="#"
disabled
icon-pack="fas"
icon-left="arrow-right">
Newer
</b-button>
% endif
% endif
</%def>
<%def name="render_this_page_component()">
<this-page @change-content-title="changeContentTitle"
% if can_edit_help:
:configure-fields-help="configureFieldsHelp"
% endif
/>
</%def>
<%def name="render_vue_template_feedback()">
<script type="text/x-template" id="feedback-template">
<div>
<div class="level-item">
<b-button type="is-primary"
@click="showFeedback()"
icon-pack="fas"
icon-left="comment">
Feedback
</b-button>
</div>
<b-modal has-modal-card
:active.sync="showDialog">
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">User Feedback</p>
</header>
<section class="modal-card-body">
<p class="block">
Questions, suggestions, comments, complaints, etc.
<span class="red">regarding this website</span> are
welcome and may be submitted below.
</p>
<b-field label="User Name">
<b-input v-model="userName"
% if request.user:
disabled
% endif
>
</b-input>
</b-field>
<b-field label="Referring URL">
<b-input
v-model="referrer"
disabled="true">
</b-input>
</b-field>
<b-field label="Message">
<b-input type="textarea"
v-model="message"
ref="textarea">
</b-input>
</b-field>
% if config.get_bool('tailbone.feedback_allows_reply'):
<div class="level">
<div class="level-left">
<div class="level-item">
<b-checkbox v-model="pleaseReply"
@input="pleaseReplyChanged">
Please email me back{{ pleaseReply ? " at: " : "" }}
</b-checkbox>
</div>
<div class="level-item" v-show="pleaseReply">
<b-input v-model="userEmail"
ref="userEmail">
</b-input>
</div>
</div>
</div>
% endif
</section>
<footer class="modal-card-foot">
<b-button @click="showDialog = false">
Cancel
</b-button>
<b-button type="is-primary"
icon-pack="fas"
icon-left="paper-plane"
@click="sendFeedback()"
:disabled="sendingFeedback || !message || !message.trim()">
{{ sendingFeedback ? "Working, please wait..." : "Send Message" }}
</b-button>
</footer>
</div>
</b-modal>
</div>
</script>
</%def>
<%def name="render_vue_script_feedback()">
${parent.render_vue_script_feedback()}
<script>
WuttaFeedbackForm.template = '#feedback-template'
WuttaFeedbackForm.props.message = String
% if config.get_bool('tailbone.feedback_allows_reply'):
WuttaFeedbackFormData.pleaseReply = false
WuttaFeedbackFormData.userEmail = null
WuttaFeedbackForm.methods.pleaseReplyChanged = function(value) {
this.$nextTick(() => {
this.$refs.userEmail.focus()
})
}
WuttaFeedbackForm.methods.getExtraParams = function() {
return {
please_reply_to: this.pleaseReply ? this.userEmail : null,
}
}
% endif
// TODO: deprecate / remove these
const FeedbackForm = WuttaFeedbackForm
const FeedbackFormData = WuttaFeedbackFormData
</script>
</%def>
<%def name="render_vue_templates()">
${parent.render_vue_templates()}
${page_help.render_template()}
${page_help.declare_vars()}
</%def>
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
##############################
## menu search
##############################
WholePageData.menuSearchActive = false
WholePageData.menuSearchTerm = ''
WholePageData.menuSearchData = ${json.dumps(global_search_data or [])|n}
WholePage.computed.menuSearchFilteredData = function() {
if (!this.menuSearchTerm.length) {
return this.menuSearchData
}
const terms = []
for (let term of this.menuSearchTerm.toLowerCase().split(' ')) {
term = term.trim()
if (term) {
terms.push(term)
}
}
if (!terms.length) {
return this.menuSearchData
}
// all terms must match
return this.menuSearchData.filter((option) => {
const label = option.label.toLowerCase()
for (const term of terms) {
if (label.indexOf(term) < 0) {
return false
}
}
return true
})
}
WholePage.methods.globalKey = function(event) {
// Ctrl+8 opens menu search
if (event.target.tagName == 'BODY') {
if (event.ctrlKey && event.key == '8') {
this.menuSearchInit()
}
}
}
WholePage.mounted = function() {
window.addEventListener('keydown', this.globalKey)
for (let hook of this.mountedHooks) {
hook(this)
}
}
WholePage.beforeDestroy = function() {
window.removeEventListener('keydown', this.globalKey)
}
WholePage.methods.menuSearchInit = function() {
this.menuSearchTerm = ''
this.menuSearchActive = true
this.$nextTick(() => {
this.$refs.menuSearchAutocomplete.focus()
})
}
WholePage.methods.menuSearchKeydown = function(event) {
// ESC will dismiss searchbox
if (event.which == 27) {
this.menuSearchActive = false
}
}
WholePage.methods.menuSearchSelect = function(option) {
location.href = option.url
}
##############################
## theme picker
##############################
% if expose_theme_picker and request.has_perm('common.change_app_theme'):
WholePageData.globalTheme = ${json.dumps(theme or None)|n}
## WholePageData.referrer = location.href
WholePage.methods.changeTheme = function() {
this.$refs.themePickerForm.submit()
}
% endif
##############################
## edit fields help
##############################
% if can_edit_help:
WholePageData.configureFieldsHelp = false
% endif
</script>
</%def>
<%def name="make_vue_components()">
${parent.make_vue_components()}
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.datepicker.js') + f'?ver={tailbone.__version__}')}
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.numericinput.js') + f'?ver={tailbone.__version__}')}
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.oncebutton.js') + f'?ver={tailbone.__version__}')}
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.timepicker.js') + f'?ver={tailbone.__version__}')}
${make_grid_filter_components()}
${page_help.make_component()}
</%def>

View file

@ -1,78 +0,0 @@
## -*- coding: utf-8; -*-
<%inherit file="wuttaweb:templates/configure.mako" />
<%namespace name="tailbone_base" file="tailbone:templates/configure.mako" />
<%def name="input_file_templates_section()">
${tailbone_base.input_file_templates_section()}
</%def>
<%def name="output_file_templates_section()">
${tailbone_base.output_file_templates_section()}
</%def>
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
##############################
## input file templates
##############################
% if input_file_template_settings is not Undefined:
ThisPageData.inputFileTemplateSettings = ${json.dumps(input_file_template_settings)|n}
ThisPageData.inputFileTemplateFileOptions = ${json.dumps(input_file_options)|n}
ThisPageData.inputFileTemplateUploads = {
% for key in input_file_templates:
'${key}': null,
% endfor
}
ThisPage.methods.validateInputFileTemplateSettings = function() {
% for tmpl in input_file_templates.values():
if (this.inputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted') {
if (!this.inputFileTemplateSettings['${tmpl['setting_file']}']) {
if (!this.inputFileTemplateUploads['${tmpl['key']}']) {
return "You must provide a file to upload for the ${tmpl['label']} template."
}
}
}
% endfor
}
ThisPageData.validators.push(ThisPage.methods.validateInputFileTemplateSettings)
% endif
##############################
## output file templates
##############################
% if output_file_template_settings is not Undefined:
ThisPageData.outputFileTemplateSettings = ${json.dumps(output_file_template_settings)|n}
ThisPageData.outputFileTemplateFileOptions = ${json.dumps(output_file_options)|n}
ThisPageData.outputFileTemplateUploads = {
% for key in output_file_templates:
'${key}': null,
% endfor
}
ThisPage.methods.validateOutputFileTemplateSettings = function() {
% for tmpl in output_file_templates.values():
if (this.outputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted') {
if (!this.outputFileTemplateSettings['${tmpl['setting_file']}']) {
if (!this.outputFileTemplateUploads['${tmpl['key']}']) {
return "You must provide a file to upload for the ${tmpl['label']} template."
}
}
}
% endfor
}
ThisPageData.validators.push(ThisPage.methods.validateOutputFileTemplateSettings)
% endif
</script>
</%def>

Some files were not shown because too many files have changed in this diff Show more