feat: refactor forms/grids/views/templates per wuttaweb compat

this starts to get things more aligned between wuttaweb and tailbone.
the use case in mind so far is for a wuttaweb view to be included in a
tailbone app.

form and grid classes now have some new methods to match wuttaweb, so
templates call the shared method names where possible.

templates can no longer assume they have tailbone-native master view,
form, grid etc. so must inspect context more closely in some cases.
This commit is contained in:
Lance Edgar 2024-08-15 14:34:20 -05:00
parent b53479f8e4
commit a6ce5eb21d
39 changed files with 1037 additions and 300 deletions

View file

@ -189,9 +189,16 @@ def make_pyramid_config(settings, configure_csrf=True):
for spec in includes: for spec in includes:
config.include(spec) config.include(spec)
# Add some permissions magic. # add some permissions magic
config.add_directive('add_tailbone_permission_group', 'tailbone.auth.add_permission_group') config.add_directive('add_wutta_permission_group',
config.add_directive('add_tailbone_permission', 'tailbone.auth.add_permission') 'wuttaweb.auth.add_permission_group')
config.add_directive('add_wutta_permission',
'wuttaweb.auth.add_permission')
# TODO: deprecate / remove these
config.add_directive('add_tailbone_permission_group',
'wuttaweb.auth.add_permission_group')
config.add_directive('add_tailbone_permission',
'wuttaweb.auth.add_permission')
# and some similar magic for certain master views # and some similar magic for certain master views
config.add_directive('add_tailbone_index_page', 'tailbone.app.add_index_page') config.add_directive('add_tailbone_index_page', 'tailbone.app.add_index_page')

View file

@ -27,7 +27,7 @@ Authentication & Authorization
import logging import logging
import re import re
from rattail.util import prettify, NOTSET from rattail.util import NOTSET
from zope.interface import implementer from zope.interface import implementer
from pyramid.authentication import SessionAuthenticationHelper from pyramid.authentication import SessionAuthenticationHelper
@ -159,30 +159,3 @@ class TailboneSecurityPolicy:
user = self.identity(request) user = self.identity(request)
return auth.has_permission(Session(), user, permission) return auth.has_permission(Session(), user, permission)
def add_permission_group(config, key, label=None, overwrite=True):
"""
Add a permission group to the app configuration.
"""
def action():
perms = config.get_settings().get('tailbone_permissions', {})
if key not in perms or overwrite:
group = perms.setdefault(key, {'key': key})
group['label'] = label or prettify(key)
config.add_settings({'tailbone_permissions': perms})
config.action(None, action)
def add_permission(config, groupkey, key, label=None):
"""
Add a permission to the app configuration.
"""
def action():
perms = config.get_settings().get('tailbone_permissions', {})
group = perms.setdefault(groupkey, {'key': groupkey})
group.setdefault('label', prettify(groupkey))
perm = group.setdefault('perms', {}).setdefault(key, {'key': key})
perm['label'] = label or prettify(key)
config.add_settings({'tailbone_permissions': perms})
config.action(None, action)

View file

@ -26,13 +26,14 @@ Rattail config extension for Tailbone
import warnings import warnings
from rattail.config import ConfigExtension as BaseExtension from wuttjamaican.conf import WuttaConfigExtension
from rattail.db.config import configure_session from rattail.db.config import configure_session
from tailbone.db import Session from tailbone.db import Session
class ConfigExtension(BaseExtension): class ConfigExtension(WuttaConfigExtension):
""" """
Rattail config extension for Tailbone. Does the following: Rattail config extension for Tailbone. Does the following:

View file

@ -47,7 +47,7 @@ from pyramid_deform import SessionFileUploadTempStore
from pyramid.renderers import render from pyramid.renderers import render
from webhelpers2.html import tags, HTML from webhelpers2.html import tags, HTML
from wuttaweb.util import get_form_data from wuttaweb.util import get_form_data, make_json_safe
from tailbone.db import Session from tailbone.db import Session
from tailbone.util import raw_datetime, render_markdown from tailbone.util import raw_datetime, render_markdown
@ -328,7 +328,7 @@ class Form(object):
""" """
Base class for all forms. Base class for all forms.
""" """
save_label = "Save" save_label = "Submit"
update_label = "Save" update_label = "Save"
show_cancel = True show_cancel = True
auto_disable = True auto_disable = True
@ -339,10 +339,12 @@ class Form(object):
model_instance=None, model_class=None, appstruct=UNSPECIFIED, nodes={}, enums={}, labels={}, model_instance=None, model_class=None, appstruct=UNSPECIFIED, nodes={}, enums={}, labels={},
assume_local_times=False, renderers=None, renderer_kwargs={}, assume_local_times=False, renderers=None, renderer_kwargs={},
hidden={}, widgets={}, defaults={}, validators={}, required={}, helptext={}, focus_spec=None, hidden={}, widgets={}, defaults={}, validators={}, required={}, helptext={}, focus_spec=None,
action_url=None, cancel_url=None, component='tailbone-form', action_url=None, cancel_url=None,
vue_tagname=None,
vuejs_component_kwargs=None, vuejs_field_converters={}, json_data={}, included_templates={}, vuejs_component_kwargs=None, vuejs_field_converters={}, json_data={}, included_templates={},
# TODO: ugh this is getting out hand! # TODO: ugh this is getting out hand!
can_edit_help=False, edit_help_url=None, route_prefix=None, can_edit_help=False, edit_help_url=None, route_prefix=None,
**kwargs
): ):
self.fields = None self.fields = None
if fields is not None: if fields is not None:
@ -380,7 +382,17 @@ class Form(object):
self.focus_spec = focus_spec self.focus_spec = focus_spec
self.action_url = action_url self.action_url = action_url
self.cancel_url = cancel_url self.cancel_url = cancel_url
self.component = component
# vue_tagname
self.vue_tagname = vue_tagname
if not self.vue_tagname and kwargs.get('component'):
warnings.warn("component kwarg is deprecated for Form(); "
"please use vue_tagname param instead",
DeprecationWarning, stacklevel=2)
self.vue_tagname = kwargs['component']
if not self.vue_tagname:
self.vue_tagname = 'tailbone-form'
self.vuejs_component_kwargs = vuejs_component_kwargs or {} self.vuejs_component_kwargs = vuejs_component_kwargs or {}
self.vuejs_field_converters = vuejs_field_converters or {} self.vuejs_field_converters = vuejs_field_converters or {}
self.json_data = json_data or {} self.json_data = json_data or {}
@ -393,10 +405,54 @@ class Form(object):
return iter(self.fields) return iter(self.fields)
@property @property
def component_studly(self): def vue_component(self):
words = self.component.split('-') """
String name for the Vue component, e.g. ``'TailboneGrid'``.
This is a generated value based on :attr:`vue_tagname`.
"""
words = self.vue_tagname.split('-')
return ''.join([word.capitalize() for word in words]) return ''.join([word.capitalize() for word in words])
@property
def component(self):
"""
DEPRECATED - use :attr:`vue_tagname` instead.
"""
warnings.warn("Form.component is deprecated; "
"please use vue_tagname instead",
DeprecationWarning, stacklevel=2)
return self.vue_tagname
@property
def component_studly(self):
"""
DEPRECATED - use :attr:`vue_component` instead.
"""
warnings.warn("Form.component_studly is deprecated; "
"please use vue_component instead",
DeprecationWarning, stacklevel=2)
return self.vue_component
def get_button_label_submit(self):
""" """
if hasattr(self, '_button_label_submit'):
return self._button_label_submit
label = getattr(self, 'submit_label', None)
if label:
return label
return self.save_label
def set_button_label_submit(self, value):
""" """
self._button_label_submit = value
# wutta compat
button_label_submit = property(get_button_label_submit,
set_button_label_submit)
def __contains__(self, item): def __contains__(self, item):
return item in self.fields return item in self.fields
@ -805,6 +861,10 @@ class Form(object):
DeprecationWarning, stacklevel=2) DeprecationWarning, stacklevel=2)
return self.render_deform(**kwargs) return self.render_deform(**kwargs)
def get_deform(self):
""" """
return self.make_deform_form()
def make_deform_form(self): def make_deform_form(self):
if not hasattr(self, 'deform_form'): if not hasattr(self, 'deform_form'):
@ -843,6 +903,10 @@ class Form(object):
return self.deform_form return self.deform_form
def render_vue_template(self, template='/forms/deform.mako', **context):
""" """
return self.render_deform(template=template, **context)
def render_deform(self, dform=None, template=None, **kwargs): def render_deform(self, dform=None, template=None, **kwargs):
if not template: if not template:
template = '/forms/deform.mako' template = '/forms/deform.mako'
@ -865,8 +929,8 @@ class Form(object):
context.setdefault('form_kwargs', {}) context.setdefault('form_kwargs', {})
# TODO: deprecate / remove the latter option here # TODO: deprecate / remove the latter option here
if self.auto_disable_save or self.auto_disable: if self.auto_disable_save or self.auto_disable:
context['form_kwargs'].setdefault('ref', self.component_studly) context['form_kwargs'].setdefault('ref', self.vue_component)
context['form_kwargs']['@submit'] = 'submit{}'.format(self.component_studly) context['form_kwargs']['@submit'] = 'submit{}'.format(self.vue_component)
if self.focus_spec: if self.focus_spec:
context['form_kwargs']['data-focus'] = self.focus_spec context['form_kwargs']['data-focus'] = self.focus_spec
context['request'] = self.request context['request'] = self.request
@ -878,12 +942,13 @@ class Form(object):
return dict([(field, self.get_label(field)) return dict([(field, self.get_label(field))
for field in self]) for field in self])
def get_field_markdowns(self): def get_field_markdowns(self, session=None):
app = self.request.rattail_config.get_app() app = self.request.rattail_config.get_app()
model = app.model model = app.model
session = session or Session()
if not hasattr(self, 'field_markdowns'): if not hasattr(self, 'field_markdowns'):
infos = Session.query(model.TailboneFieldInfo)\ infos = session.query(model.TailboneFieldInfo)\
.filter(model.TailboneFieldInfo.route_prefix == self.route_prefix)\ .filter(model.TailboneFieldInfo.route_prefix == self.route_prefix)\
.all() .all()
self.field_markdowns = dict([(info.field_name, info.markdown_text) self.field_markdowns = dict([(info.field_name, info.markdown_text)
@ -891,6 +956,18 @@ class Form(object):
return self.field_markdowns return self.field_markdowns
def get_vue_field_value(self, key):
""" """
if key not in self.fields:
return
dform = self.get_deform()
if key not in dform:
return
field = dform[key]
return make_json_safe(field.cstruct)
def get_vuejs_model_value(self, field): def get_vuejs_model_value(self, field):
""" """
This method must return "raw" JS which will be assigned as the initial This method must return "raw" JS which will be assigned as the initial
@ -957,6 +1034,10 @@ class Form(object):
def set_vuejs_component_kwargs(self, **kwargs): def set_vuejs_component_kwargs(self, **kwargs):
self.vuejs_component_kwargs.update(kwargs) self.vuejs_component_kwargs.update(kwargs)
def render_vue_tag(self, **kwargs):
""" """
return self.render_vuejs_component()
def render_vuejs_component(self): def render_vuejs_component(self):
""" """
Render the Vue.js component HTML for the form. Render the Vue.js component HTML for the form.
@ -971,7 +1052,7 @@ class Form(object):
kwargs = dict(self.vuejs_component_kwargs) kwargs = dict(self.vuejs_component_kwargs)
if self.can_edit_help: if self.can_edit_help:
kwargs.setdefault(':configure-fields-help', 'configureFieldsHelp') kwargs.setdefault(':configure-fields-help', 'configureFieldsHelp')
return HTML.tag(self.component, **kwargs) return HTML.tag(self.vue_tagname, **kwargs)
def set_json_data(self, key, value): def set_json_data(self, key, value):
""" """
@ -997,7 +1078,12 @@ class Form(object):
templates.append(HTML.literal(render(template, context))) templates.append(HTML.literal(render(template, context)))
return HTML.literal('\n').join(templates) return HTML.literal('\n').join(templates)
def render_field_complete(self, fieldname, bfield_attrs={}): def render_vue_field(self, fieldname, **kwargs):
""" """
return self.render_field_complete(fieldname, **kwargs)
def render_field_complete(self, fieldname, bfield_attrs={},
session=None):
""" """
Render the given field completely, i.e. with ``<b-field>`` Render the given field completely, i.e. with ``<b-field>``
wrapper. Note that this is meant to render *editable* fields, wrapper. Note that this is meant to render *editable* fields,
@ -1015,7 +1101,7 @@ class Form(object):
if self.field_visible(fieldname): if self.field_visible(fieldname):
label = self.get_label(fieldname) label = self.get_label(fieldname)
markdowns = self.get_field_markdowns() markdowns = self.get_field_markdowns(session=session)
# these attrs will be for the <b-field> (*not* the widget) # these attrs will be for the <b-field> (*not* the widget)
attrs = { attrs = {

View file

@ -198,7 +198,8 @@ class Grid:
checkable=None, row_uuid_getter=None, checkable=None, row_uuid_getter=None,
clicking_row_checks_box=False, click_handlers=None, clicking_row_checks_box=False, click_handlers=None,
main_actions=[], more_actions=[], delete_speedbump=False, main_actions=[], more_actions=[], delete_speedbump=False,
ajax_data_url=None, component='tailbone-grid', ajax_data_url=None,
vue_tagname=None,
expose_direct_link=False, expose_direct_link=False,
**kwargs): **kwargs):
@ -268,19 +269,63 @@ class Grid:
if ajax_data_url: if ajax_data_url:
self.ajax_data_url = ajax_data_url self.ajax_data_url = ajax_data_url
elif self.request: elif self.request:
self.ajax_data_url = self.request.current_route_url(_query=None) self.ajax_data_url = self.request.path_url
else: else:
self.ajax_data_url = '' self.ajax_data_url = ''
self.component = component # vue_tagname
self.vue_tagname = vue_tagname
if not self.vue_tagname and kwargs.get('component'):
warnings.warn("component kwarg is deprecated for Grid(); "
"please use vue_tagname param instead",
DeprecationWarning, stacklevel=2)
self.vue_tagname = kwargs['component']
if not self.vue_tagname:
self.vue_tagname = 'tailbone-grid'
self.expose_direct_link = expose_direct_link self.expose_direct_link = expose_direct_link
self._whgrid_kwargs = kwargs self._whgrid_kwargs = kwargs
@property @property
def component_studly(self): def vue_component(self):
words = self.component.split('-') """
String name for the Vue component, e.g. ``'TailboneGrid'``.
This is a generated value based on :attr:`vue_tagname`.
"""
words = self.vue_tagname.split('-')
return ''.join([word.capitalize() for word in words]) return ''.join([word.capitalize() for word in words])
@property
def component(self):
"""
DEPRECATED - use :attr:`vue_tagname` instead.
"""
warnings.warn("Grid.component is deprecated; "
"please use vue_tagname instead",
DeprecationWarning, stacklevel=2)
return self.vue_tagname
@property
def component_studly(self):
"""
DEPRECATED - use :attr:`vue_component` instead.
"""
warnings.warn("Grid.component_studly is deprecated; "
"please use vue_component instead",
DeprecationWarning, stacklevel=2)
return self.vue_component
@property
def actions(self):
""" """
actions = []
if self.main_actions:
actions.extend(self.main_actions)
if self.more_actions:
actions.extend(self.more_actions)
return actions
def make_columns(self): def make_columns(self):
""" """
Return a default list of columns, based on :attr:`model_class`. Return a default list of columns, based on :attr:`model_class`.
@ -1334,6 +1379,21 @@ class Grid:
data = self.pager data = self.pager
return data return data
def render_vue_tag(self, master=None, **kwargs):
""" """
kwargs.setdefault('ref', 'grid')
kwargs.setdefault(':csrftoken', 'csrftoken')
if (master and master.deletable and master.has_perm('delete')
and master.delete_confirm == 'simple'):
kwargs.setdefault('@deleteActionClicked', 'deleteObject')
return HTML.tag(self.vue_tagname, **kwargs)
def render_vue_template(self, template='/grids/complete.mako', **context):
""" """
return self.render_complete(template=template, **context)
def render_complete(self, template='/grids/complete.mako', **kwargs): def render_complete(self, template='/grids/complete.mako', **kwargs):
""" """
Render the grid, complete with filters. Note that this also Render the grid, complete with filters. Note that this also
@ -1359,7 +1419,8 @@ class Grid:
context['request'] = self.request context['request'] = self.request
context.setdefault('allow_save_defaults', True) context.setdefault('allow_save_defaults', True)
context.setdefault('view_click_handler', self.get_view_click_handler()) context.setdefault('view_click_handler', self.get_view_click_handler())
return render(template, context) html = render(template, context)
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; "
@ -1575,6 +1636,10 @@ class Grid:
return True return True
return False return False
def get_vue_columns(self):
""" """
return self.get_table_columns()
def get_table_columns(self): def get_table_columns(self):
""" """
Return a list of dicts representing all grid columns. Meant Return a list of dicts representing all grid columns. Meant
@ -1600,11 +1665,19 @@ class Grid:
if hasattr(rowobj, 'uuid'): if hasattr(rowobj, 'uuid'):
return rowobj.uuid return rowobj.uuid
def get_vue_data(self):
""" """
table_data = self.get_table_data()
return table_data['data']
def get_table_data(self): def get_table_data(self):
""" """
Returns a list of data rows for the grid, for use with Returns a list of data rows for the grid, for use with
client-side JS table. client-side JS table.
""" """
if hasattr(self, '_table_data'):
return self._table_data
# filter / sort / paginate to get "visible" data # filter / sort / paginate to get "visible" data
raw_data = self.make_visible_data() raw_data = self.make_visible_data()
data = [] data = []
@ -1704,7 +1777,8 @@ class Grid:
else: else:
results['total_items'] = count results['total_items'] = count
return results self._table_data = results
return self._table_data
def set_action_urls(self, row, rowobj, i): def set_action_urls(self, row, rowobj, i):
""" """

View file

@ -48,7 +48,7 @@ from tailbone.util import get_available_themes, get_global_search_options
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def new_request(event): def new_request(event, session=None):
""" """
Event hook called when processing a new request. Event hook called when processing a new request.
@ -64,15 +64,6 @@ def new_request(event):
Reference to the app :term:`config object`. Note that this Reference to the app :term:`config object`. Note that this
will be the same as :attr:`wuttaweb:request.wutta_config`. will be the same as :attr:`wuttaweb:request.wutta_config`.
.. method:: request.has_perm(name)
Function to check if current user has the given permission.
.. method:: request.has_any_perm(*names)
Function to check if current user has any of the given
permissions.
.. method:: request.register_component(tagname, classname) .. method:: request.register_component(tagname, classname)
Function to register a Vue component for use with the app. Function to register a Vue component for use with the app.
@ -90,6 +81,7 @@ def new_request(event):
config = request.wutta_config config = request.wutta_config
app = config.get_app() app = config.get_app()
auth = app.get_auth_handler() auth = app.get_auth_handler()
session = session or Session()
# compatibility # compatibility
rattail_config = config rattail_config = config
@ -104,50 +96,31 @@ def new_request(event):
return user return user
# invoke upstream hook to set user # invoke upstream hook to set user
base.new_request_set_user(event, user_getter=user_getter, db_session=Session()) base.new_request_set_user(event, user_getter=user_getter, db_session=session)
# assign client IP address to the session, for sake of versioning # assign client IP address to the session, for sake of versioning
Session().continuum_remote_addr = request.client_addr if hasattr(request, 'client_addr'):
session.continuum_remote_addr = request.client_addr
# TODO: why would this ever be null? # request.register_component()
if rattail_config: def register_component(tagname, classname):
"""
Register a Vue 3 component, so the base template knows to
declare it for use within the app (page).
"""
if not hasattr(request, '_tailbone_registered_components'):
request._tailbone_registered_components = OrderedDict()
app = rattail_config.get_app() if tagname in request._tailbone_registered_components:
auth = app.get_auth_handler() log.warning("component with tagname '%s' already registered "
request.tailbone_cached_permissions = auth.get_permissions( "with class '%s' but we are replacing that with "
Session(), request.user) "class '%s'",
tagname,
request._tailbone_registered_components[tagname],
classname)
def has_perm(name): request._tailbone_registered_components[tagname] = classname
if name in request.tailbone_cached_permissions: request.register_component = register_component
return True
return request.is_root
request.has_perm = has_perm
def has_any_perm(*names):
for name in names:
if has_perm(name):
return True
return False
request.has_any_perm = has_any_perm
def register_component(tagname, classname):
"""
Register a Vue 3 component, so the base template knows to
declare it for use within the app (page).
"""
if not hasattr(request, '_tailbone_registered_components'):
request._tailbone_registered_components = OrderedDict()
if tagname in request._tailbone_registered_components:
log.warning("component with tagname '%s' already registered "
"with class '%s' but we are replacing that with "
"class '%s'",
tagname,
request._tailbone_registered_components[tagname],
classname)
request._tailbone_registered_components[tagname] = classname
request.register_component = register_component
def before_render(event): def before_render(event):

View file

@ -153,12 +153,16 @@
<style type="text/css"> <style type="text/css">
.filters .filter-fieldname, .filters .filter-fieldname,
.filters .filter-fieldname .button { .filters .filter-fieldname .button {
% if filter_fieldname_width is not Undefined:
min-width: ${filter_fieldname_width}; min-width: ${filter_fieldname_width};
% endif
justify-content: left; justify-content: left;
} }
% if filter_fieldname_width is not Undefined:
.filters .filter-verb { .filters .filter-verb {
min-width: ${filter_verb_width}; min-width: ${filter_verb_width};
} }
% endif
</style> </style>
</%def> </%def>
@ -856,7 +860,7 @@
feedbackMessage: "", feedbackMessage: "",
% if expose_theme_picker and request.has_perm('common.change_app_theme'): % if expose_theme_picker and request.has_perm('common.change_app_theme'):
globalTheme: ${json.dumps(theme)|n}, globalTheme: ${json.dumps(theme or None)|n},
referrer: location.href, referrer: location.href,
% endif % endif
@ -866,7 +870,7 @@
globalSearchActive: false, globalSearchActive: false,
globalSearchTerm: '', globalSearchTerm: '',
globalSearchData: ${json.dumps(global_search_data)|n}, globalSearchData: ${json.dumps(global_search_data or [])|n},
mountedHooks: [], mountedHooks: [],
} }

View file

@ -6,12 +6,12 @@
<%def name="render_form_buttons()"></%def> <%def name="render_form_buttons()"></%def>
<%def name="render_form_template()"> <%def name="render_form_template()">
${form.render_deform(buttons=capture(self.render_form_buttons))|n} ${form.render_vue_template(buttons=capture(self.render_form_buttons))|n}
</%def> </%def>
<%def name="render_form()"> <%def name="render_form()">
<div class="form"> <div class="form">
${form.render_vuejs_component()} ${form.render_vue_tag()}
</div> </div>
</%def> </%def>
@ -111,9 +111,9 @@
% if form is not Undefined: % if form is not Undefined:
<script type="text/javascript"> <script type="text/javascript">
${form.component_studly}.data = function() { return ${form.component_studly}Data } ${form.vue_component}.data = function() { return ${form.vue_component}Data }
Vue.component('${form.component}', ${form.component_studly}) Vue.component('${form.vue_tagname}', ${form.vue_component})
</script> </script>
% endif % endif

View file

@ -1,19 +1,19 @@
## -*- coding: utf-8; -*- ## -*- coding: utf-8; -*-
<% request.register_component(form.component, form.component_studly) %> <% request.register_component(form.vue_tagname, form.vue_component) %>
<script type="text/x-template" id="${form.component}-template"> <script type="text/x-template" id="${form.vue_tagname}-template">
<div> <div>
% if not form.readonly: % if not form.readonly:
${h.form(form.action_url, id=dform.formid, method='post', enctype='multipart/form-data', **form_kwargs)} ${h.form(form.action_url, id=dform.formid, method='post', enctype='multipart/form-data', **(form_kwargs or {}))}
${h.csrf_token(request)} ${h.csrf_token(request)}
% endif % endif
<section> <section>
% if form_body is not Undefined and form_body: % if form_body is not Undefined and form_body:
${form_body|n} ${form_body|n}
% elif form.grouping: % elif getattr(form, 'grouping', None):
% for group in form.grouping: % for group in form.grouping:
<nav class="panel"> <nav class="panel">
<p class="panel-heading">${group}</p> <p class="panel-heading">${group}</p>
@ -27,8 +27,8 @@
</nav> </nav>
% endfor % endfor
% else: % else:
% for field in form.fields: % for fieldname in form.fields:
${form.render_field_complete(field)} ${form.render_vue_field(fieldname, session=session)}
% endfor % endfor
% endif % endif
</section> </section>
@ -54,20 +54,20 @@
<input type="reset" value="Reset" class="button" /> <input type="reset" value="Reset" class="button" />
% endif % endif
## TODO: deprecate / remove the latter option here ## TODO: deprecate / remove the latter option here
% if form.auto_disable_save or form.auto_disable: % if getattr(form, 'auto_disable_submit', False) or form.auto_disable_save or form.auto_disable:
<b-button type="is-primary" <b-button type="is-primary"
native-type="submit" native-type="submit"
:disabled="${form.component_studly}Submitting" :disabled="${form.vue_component}Submitting"
icon-pack="fas" icon-pack="fas"
icon-left="save"> icon-left="save">
{{ ${form.component_studly}ButtonText }} {{ ${form.vue_component}Submitting ? "Working, please wait..." : "${form.button_label_submit}" }}
</b-button> </b-button>
% else: % else:
<b-button type="is-primary" <b-button type="is-primary"
native-type="submit" native-type="submit"
icon-pack="fas" icon-pack="fas"
icon-left="save"> icon-left="save">
${getattr(form, 'submit_label', getattr(form, 'save_label', "Submit"))} ${form.button_label_submit}
</b-button> </b-button>
% endif % endif
</div> </div>
@ -122,8 +122,8 @@
<script type="text/javascript"> <script type="text/javascript">
let ${form.component_studly} = { let ${form.vue_component} = {
template: '#${form.component}-template', template: '#${form.vue_tagname}-template',
mixins: [FormPosterMixin], mixins: [FormPosterMixin],
components: {}, components: {},
props: { props: {
@ -136,10 +136,9 @@
methods: { methods: {
## TODO: deprecate / remove the latter option here ## TODO: deprecate / remove the latter option here
% if form.auto_disable_save or form.auto_disable: % if getattr(form, 'auto_disable_submit', False) or form.auto_disable_save or form.auto_disable:
submit${form.component_studly}() { submit${form.vue_component}() {
this.${form.component_studly}Submitting = true this.${form.vue_component}Submitting = true
this.${form.component_studly}ButtonText = "Working, please wait..."
}, },
% endif % endif
@ -178,7 +177,7 @@
} }
} }
let ${form.component_studly}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(request.session.get_csrf_token() or request.session.new_csrf_token())|n}, csrftoken: ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n},
@ -198,16 +197,14 @@
% if not form.readonly: % if not form.readonly:
% for field in form.fields: % for field in form.fields:
% if field in dform: % if field in dform:
<% field = dform[field] %> field_model_${field}: ${json.dumps(form.get_vue_field_value(field))|n},
field_model_${field.name}: ${form.get_vuejs_model_value(field)|n},
% endif % endif
% endfor % endfor
% endif % endif
## TODO: deprecate / remove the latter option here ## TODO: deprecate / remove the latter option here
% if form.auto_disable_save or form.auto_disable: % if getattr(form, 'auto_disable_submit', False) or form.auto_disable_save or form.auto_disable:
${form.component_studly}Submitting: false, ${form.vue_component}Submitting: false,
${form.component_studly}ButtonText: ${json.dumps(getattr(form, 'submit_label', getattr(form, 'save_label', "Submit")))|n},
% endif % endif
} }

View file

@ -0,0 +1,3 @@
## -*- coding: utf-8; -*-
<%inherit file="/forms/deform.mako" />
${parent.body()}

View file

@ -1,15 +1,15 @@
## -*- coding: utf-8; -*- ## -*- coding: utf-8; -*-
<% request.register_component(grid.component, grid.component_studly) %> <% request.register_component(grid.vue_tagname, grid.vue_component) %>
<script type="text/x-template" id="${grid.component}-template"> <script type="text/x-template" id="${grid.vue_tagname}-template">
<div> <div>
<div style="display: flex; justify-content: space-between; margin-bottom: 0.5em;"> <div style="display: flex; justify-content: space-between; margin-bottom: 0.5em;">
<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 grid.filterable: % if getattr(grid, 'filterable', False):
## TODO: stop using |n filter ## TODO: stop using |n filter
${grid.render_filters(allow_save_defaults=allow_save_defaults)|n} ${grid.render_filters(allow_save_defaults=allow_save_defaults)|n}
% endif % endif
@ -55,7 +55,7 @@
:checkable="checkable" :checkable="checkable"
% if grid.checkboxes: % if getattr(grid, 'checkboxes', False):
% if request.use_oruga: % if request.use_oruga:
v-model:checked-rows="checkedRows" v-model:checked-rows="checkedRows"
% else: % else:
@ -66,20 +66,22 @@
% endif % endif
% endif % endif
% if grid.check_handler: % if getattr(grid, 'check_handler', None):
@check="${grid.check_handler}" @check="${grid.check_handler}"
% endif % endif
% if grid.check_all_handler: % if getattr(grid, 'check_all_handler', None):
@check-all="${grid.check_all_handler}" @check-all="${grid.check_all_handler}"
% endif % endif
% if hasattr(grid, 'checkable'):
% if isinstance(grid.checkable, str): % if isinstance(grid.checkable, str):
:is-row-checkable="${grid.row_checkable}" :is-row-checkable="${grid.row_checkable}"
% elif grid.checkable: % elif grid.checkable:
:is-row-checkable="row => row._checkable" :is-row-checkable="row => row._checkable"
% endif % endif
% endif
% if grid.sortable: % if getattr(grid, 'sortable', False):
backend-sorting backend-sorting
@sort="onSort" @sort="onSort"
@sorting-priority-removed="sortingPriorityRemoved" @sorting-priority-removed="sortingPriorityRemoved"
@ -101,7 +103,7 @@
sort-multiple-key="ctrlKey" sort-multiple-key="ctrlKey"
% endif % endif
% if grid.click_handlers: % if getattr(grid, 'click_handlers', None):
@cellclick="cellClick" @cellclick="cellClick"
% endif % endif
@ -119,17 +121,17 @@
:hoverable="true" :hoverable="true"
:narrowed="true"> :narrowed="true">
% for column in grid_columns: % for column in grid.get_vue_columns():
<${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['sortable'])}" :sortable="${json.dumps(column.get('sortable', False))}"
% if grid.is_searchable(column['field']): % if hasattr(grid, 'is_searchable') and grid.is_searchable(column['field']):
searchable searchable
% endif % endif
cell-class="c_${column['field']}" cell-class="c_${column['field']}"
:visible="${json.dumps(column['visible'])}"> :visible="${json.dumps(column.get('visible', True))}">
% if column['field'] in grid.raw_renderers: % if hasattr(grid, 'raw_renderers') and column['field'] in grid.raw_renderers:
${grid.raw_renderers[column['field']]()} ${grid.raw_renderers[column['field']]()}
% elif grid.is_linked(column['field']): % elif grid.is_linked(column['field']):
<a :href="props.row._action_url_view" <a :href="props.row._action_url_view"
@ -144,20 +146,20 @@
</${b}-table-column> </${b}-table-column>
% endfor % endfor
% if grid.main_actions or grid.more_actions: % if grid.actions:
<${b}-table-column field="actions" <${b}-table-column field="actions"
label="Actions" label="Actions"
v-slot="props"> v-slot="props">
## TODO: we do not currently differentiate for "main vs. more" ## TODO: we do not currently differentiate for "main vs. more"
## here, but ideally we would tuck "more" away in a drawer etc. ## here, but ideally we would tuck "more" away in a drawer etc.
% for action in grid.main_actions + grid.more_actions: % for action in grid.actions:
<a v-if="props.row._action_url_${action.key}" <a v-if="props.row._action_url_${action.key}"
:href="props.row._action_url_${action.key}" :href="props.row._action_url_${action.key}"
class="grid-action${' has-text-danger' if action.key == 'delete' else ''} ${action.link_class or ''}" class="grid-action${' has-text-danger' if action.key == 'delete' else ''} ${action.link_class or ''}"
% if action.click_handler: % if getattr(action, 'click_handler', None):
@click.prevent="${action.click_handler}" @click.prevent="${action.click_handler}"
% endif % endif
% if action.target: % if getattr(action, 'target', None):
target="${action.target}" target="${action.target}"
% endif % endif
> >
@ -192,7 +194,7 @@
<template #footer> <template #footer>
<div style="display: flex; justify-content: space-between;"> <div style="display: flex; justify-content: space-between;">
% if grid.expose_direct_link: % if getattr(grid, 'expose_direct_link', False):
<b-button type="is-primary" <b-button type="is-primary"
size="is-small" size="is-small"
@click="copyDirectLink()" @click="copyDirectLink()"
@ -207,7 +209,7 @@
<div></div> <div></div>
% endif % endif
% if grid.pageable: % if getattr(grid, 'pageable', False):
<div v-if="firstItem" <div v-if="firstItem"
style="display: flex; gap: 0.5rem; align-items: center;"> style="display: flex; gap: 0.5rem; align-items: center;">
<span> <span>
@ -234,7 +236,7 @@
</${b}-table> </${b}-table>
## dummy input field needed for sharing links on *insecure* sites ## dummy input field needed for sharing links on *insecure* sites
% if request.scheme == 'http': % if getattr(request, 'scheme', None) == 'http':
<b-input v-model="shareLink" ref="shareLink" v-show="shareLink"></b-input> <b-input v-model="shareLink" ref="shareLink" v-show="shareLink"></b-input>
% endif % endif
@ -243,30 +245,30 @@
<script type="text/javascript"> <script type="text/javascript">
let ${grid.component_studly}CurrentData = ${json.dumps(grid_data['data'])|n} let ${grid.vue_component}CurrentData = ${json.dumps(grid.get_vue_data())|n}
let ${grid.component_studly}Data = { let ${grid.vue_component}Data = {
loading: false, loading: false,
ajaxDataUrl: ${json.dumps(grid.ajax_data_url)|n}, ajaxDataUrl: ${json.dumps(getattr(grid, 'ajax_data_url', request.path_url))|n},
savingDefaults: false, savingDefaults: false,
data: ${grid.component_studly}CurrentData, data: ${grid.vue_component}CurrentData,
rowStatusMap: ${json.dumps(grid_data['row_status_map'])|n}, rowStatusMap: ${json.dumps(grid_data['row_status_map'] if grid_data is not Undefined else {})|n},
checkable: ${json.dumps(grid.checkboxes)|n}, checkable: ${json.dumps(getattr(grid, 'checkboxes', False))|n},
% if grid.checkboxes: % if getattr(grid, 'checkboxes', False):
checkedRows: ${grid_data['checked_rows_code']|n}, checkedRows: ${grid_data['checked_rows_code']|n},
% endif % endif
paginated: ${json.dumps(grid.pageable)|n}, paginated: ${json.dumps(getattr(grid, 'pageable', False))|n},
total: ${len(grid_data['data']) if static_data else grid_data['total_items']}, total: ${len(grid_data['data']) if static_data else (grid_data['total_items'] if grid_data is not Undefined else 0)},
perPage: ${json.dumps(grid.pagesize if grid.pageable else None)|n}, perPage: ${json.dumps(grid.pagesize if getattr(grid, 'pageable', False) else None)|n},
currentPage: ${json.dumps(grid.page if grid.pageable else None)|n}, currentPage: ${json.dumps(grid.page if getattr(grid, 'pageable', False) else None)|n},
firstItem: ${json.dumps(grid_data['first_item'] if grid.pageable else None)|n}, firstItem: ${json.dumps(grid_data['first_item'] if getattr(grid, 'pageable', False) else None)|n},
lastItem: ${json.dumps(grid_data['last_item'] if grid.pageable else None)|n}, lastItem: ${json.dumps(grid_data['last_item'] if getattr(grid, 'pageable', False) else None)|n},
% if grid.sortable: % if getattr(grid, 'sortable', False):
## TODO: there is a bug (?) which prevents the arrow from ## TODO: there is a bug (?) which prevents the arrow from
## displaying for simple default single-column sort. so to ## displaying for simple default single-column sort. so to
@ -289,19 +291,19 @@
% endif % endif
## filterable: ${json.dumps(grid.filterable)|n}, ## filterable: ${json.dumps(grid.filterable)|n},
filters: ${json.dumps(filters_data if grid.filterable else None)|n}, filters: ${json.dumps(filters_data if getattr(grid, 'filterable', False) else None)|n},
filtersSequence: ${json.dumps(filters_sequence if grid.filterable else None)|n}, filtersSequence: ${json.dumps(filters_sequence if getattr(grid, 'filterable', False) else None)|n},
addFilterTerm: '', addFilterTerm: '',
addFilterShow: false, addFilterShow: false,
## dummy input value needed for sharing links on *insecure* sites ## dummy input value needed for sharing links on *insecure* sites
% if request.scheme == 'http': % if getattr(request, 'scheme', None) == 'http':
shareLink: null, shareLink: null,
% endif % endif
} }
let ${grid.component_studly} = { let ${grid.vue_component} = {
template: '#${grid.component}-template', template: '#${grid.vue_tagname}-template',
mixins: [FormPosterMixin], mixins: [FormPosterMixin],
@ -358,7 +360,7 @@
directLink() { directLink() {
let params = new URLSearchParams(this.getAllParams()) let params = new URLSearchParams(this.getAllParams())
return `${request.current_route_url(_query=None)}?${'$'}{params}` return `${request.path_url}?${'$'}{params}`
}, },
}, },
@ -380,7 +382,7 @@
return filtr.label || filtr.key return filtr.label || filtr.key
}, },
% if grid.click_handlers: % if getattr(grid, 'click_handlers', None):
cellClick(row, column, rowIndex, columnIndex) { cellClick(row, column, rowIndex, columnIndex) {
% for key in grid.click_handlers: % for key in grid.click_handlers:
if (column._props.field == '${key}') { if (column._props.field == '${key}') {
@ -437,13 +439,13 @@
getBasicParams() { getBasicParams() {
let params = {} let params = {}
% if grid.sortable: % if getattr(grid, 'sortable', False):
for (let i = 1; i <= this.backendSorters.length; i++) { for (let i = 1; i <= this.backendSorters.length; i++) {
params['sort'+i+'key'] = this.backendSorters[i-1].field params['sort'+i+'key'] = this.backendSorters[i-1].field
params['sort'+i+'dir'] = this.backendSorters[i-1].order params['sort'+i+'dir'] = this.backendSorters[i-1].order
} }
% endif % endif
% if grid.pageable: % if getattr(grid, 'pageable', False):
params.pagesize = this.perPage params.pagesize = this.perPage
params.page = this.currentPage params.page = this.currentPage
% endif % endif
@ -488,8 +490,8 @@
this.loading = true this.loading = true
this.$http.get(`${'$'}{this.ajaxDataUrl}?${'$'}{params}`).then(({ data }) => { this.$http.get(`${'$'}{this.ajaxDataUrl}?${'$'}{params}`).then(({ data }) => {
if (!data.error) { if (!data.error) {
${grid.component_studly}CurrentData = data.data ${grid.vue_component}CurrentData = data.data
this.data = ${grid.component_studly}CurrentData this.data = ${grid.vue_component}CurrentData
this.rowStatusMap = data.row_status_map this.rowStatusMap = data.row_status_map
this.total = data.total_items this.total = data.total_items
this.firstItem = data.first_item this.firstItem = data.first_item
@ -776,7 +778,7 @@
} else { } else {
this.checkedRows.push(row) this.checkedRows.push(row)
} }
% if grid.check_handler: % if getattr(grid, 'check_handler', None):
this.${grid.check_handler}(this.checkedRows, row) this.${grid.check_handler}(this.checkedRows, row)
% endif % endif
}, },

View file

@ -0,0 +1,3 @@
## -*- coding: utf-8; -*-
<%inherit file="/grids/complete.mako" />
${parent.body()}

View file

@ -1,6 +1,6 @@
## -*- coding: utf-8; -*- ## -*- coding: utf-8; -*-
<%inherit file="/form.mako" /> <%inherit file="/form.mako" />
<%def name="title()">New ${model_title_plural if master.creates_multiple else model_title}</%def> <%def name="title()">New ${model_title_plural if getattr(master, 'creates_multiple', False) else model_title}</%def>
${parent.body()} ${parent.body()}

View file

@ -27,7 +27,7 @@
<b-button type="is-primary is-danger" <b-button type="is-primary is-danger"
native-type="submit" native-type="submit"
:disabled="formSubmitting"> :disabled="formSubmitting">
{{ formButtonText }} {{ formSubmitting ? "Working, please wait..." : "${form.button_label_submit}" }}
</b-button> </b-button>
</div> </div>
${h.end_form()} ${h.end_form()}
@ -35,14 +35,12 @@
<%def name="modify_this_page_vars()"> <%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()} ${parent.modify_this_page_vars()}
<script type="text/javascript"> <script>
TailboneFormData.formSubmitting = false ${form.vue_component}Data.formSubmitting = false
TailboneFormData.formButtonText = "Yes, please DELETE this data forever!"
TailboneForm.methods.submitForm = function() { ${form.vue_component}.methods.submitForm = function() {
this.formSubmitting = true this.formSubmitting = true
this.formButtonText = "Working, please wait..."
} }
</script> </script>

View file

@ -6,13 +6,13 @@
<script type="text/javascript"> <script type="text/javascript">
## declare extra data needed by form ## declare extra data needed by form
% if form is not Undefined: % if form is not Undefined and getattr(form, 'json_data', None):
% for key, value in form.json_data.items(): % for key, value in form.json_data.items():
${form.component_studly}Data.${key} = ${json.dumps(value)|n} ${form.component_studly}Data.${key} = ${json.dumps(value)|n}
% endfor % endfor
% endif % endif
% if master.deletable and instance_deletable and master.has_perm('delete') and master.delete_confirm == 'simple': % if master.deletable and instance_deletable and master.has_perm('delete') and getattr(master, 'delete_confirm', 'full') == 'simple':
ThisPage.methods.deleteObject = function() { ThisPage.methods.deleteObject = function() {
if (confirm("Are you sure you wish to delete this ${model_title}?")) { if (confirm("Are you sure you wish to delete this ${model_title}?")) {
@ -23,7 +23,7 @@
% endif % endif
</script> </script>
% if form is not Undefined: % if form is not Undefined and hasattr(form, 'render_included_templates'):
${form.render_included_templates()} ${form.render_included_templates()}
% endif % endif

View file

@ -15,7 +15,7 @@
<%def name="grid_tools()"> <%def name="grid_tools()">
## grid totals ## grid totals
% if master.supports_grid_totals: % if getattr(master, 'supports_grid_totals', False):
<div style="display: flex; align-items: center;"> <div style="display: flex; align-items: center;">
<b-button v-if="gridTotalsDisplay == null" <b-button v-if="gridTotalsDisplay == null"
:disabled="gridTotalsFetching" :disabled="gridTotalsFetching"
@ -30,7 +30,7 @@
% endif % endif
## download search results ## download search results
% if master.results_downloadable and master.has_perm('download_results'): % if getattr(master, 'results_downloadable', False) and master.has_perm('download_results'):
<div> <div>
<b-button type="is-primary" <b-button type="is-primary"
icon-pack="fas" icon-pack="fas"
@ -180,7 +180,7 @@
% endif % endif
## download rows for search results ## download rows for search results
% if master.has_rows and master.results_rows_downloadable and master.has_perm('download_results_rows'): % if getattr(master, 'has_rows', False) and master.results_rows_downloadable and master.has_perm('download_results_rows'):
<b-button type="is-primary" <b-button type="is-primary"
icon-pack="fas" icon-pack="fas"
icon-left="download" icon-left="download"
@ -194,7 +194,7 @@
% endif % endif
## merge 2 objects ## merge 2 objects
% if master.mergeable and request.has_perm('{}.merge'.format(permission_prefix)): % if getattr(master, 'mergeable', False) and request.has_perm('{}.merge'.format(permission_prefix)):
${h.form(url('{}.merge'.format(route_prefix)), class_='control', **{'@submit': 'submitMergeForm'})} ${h.form(url('{}.merge'.format(route_prefix)), class_='control', **{'@submit': 'submitMergeForm'})}
${h.csrf_token(request)} ${h.csrf_token(request)}
@ -212,7 +212,7 @@
% endif % endif
## enable / disable selected objects ## enable / disable selected objects
% if master.supports_set_enabled_toggle and master.has_perm('enable_disable_set'): % if getattr(master, 'supports_set_enabled_toggle', False) and master.has_perm('enable_disable_set'):
${h.form(url('{}.enable_set'.format(route_prefix)), class_='control', ref='enable_selected_form')} ${h.form(url('{}.enable_set'.format(route_prefix)), class_='control', ref='enable_selected_form')}
${h.csrf_token(request)} ${h.csrf_token(request)}
@ -234,7 +234,7 @@
% endif % endif
## delete selected objects ## delete selected objects
% if master.set_deletable and master.has_perm('delete_set'): % if getattr(master, 'set_deletable', False) and master.has_perm('delete_set'):
${h.form(url('{}.delete_set'.format(route_prefix)), ref='delete_selected_form', class_='control')} ${h.form(url('{}.delete_set'.format(route_prefix)), ref='delete_selected_form', class_='control')}
${h.csrf_token(request)} ${h.csrf_token(request)}
${h.hidden('uuids', v_model='selected_uuids')} ${h.hidden('uuids', v_model='selected_uuids')}
@ -249,7 +249,7 @@
% endif % endif
## delete search results ## delete search results
% if master.bulk_deletable and request.has_perm('{}.bulk_delete'.format(permission_prefix)): % if getattr(master, 'bulk_deletable', False) and request.has_perm('{}.bulk_delete'.format(permission_prefix)):
${h.form(url('{}.bulk_delete'.format(route_prefix)), ref='delete_results_form', class_='control')} ${h.form(url('{}.bulk_delete'.format(route_prefix)), ref='delete_results_form', class_='control')}
${h.csrf_token(request)} ${h.csrf_token(request)}
<b-button type="is-danger" <b-button type="is-danger"
@ -283,7 +283,7 @@
${self.render_grid_component()} ${self.render_grid_component()}
% if master.deletable and request.has_perm('{}.delete'.format(permission_prefix)) and master.delete_confirm == 'simple': % if master.deletable and master.has_perm('delete') and getattr(master, 'delete_confirm', 'full') == 'simple':
${h.form('#', ref='deleteObjectForm')} ${h.form('#', ref='deleteObjectForm')}
${h.csrf_token(request)} ${h.csrf_token(request)}
${h.end_form()} ${h.end_form()}
@ -291,17 +291,11 @@
</%def> </%def>
<%def name="make_grid_component()"> <%def name="make_grid_component()">
## TODO: stop using |n filter? ${grid.render_vue_template(tools=capture(self.grid_tools).strip(), context_menu=capture(self.context_menu_items).strip())}
${grid.render_complete(tools=capture(self.grid_tools).strip(), context_menu=capture(self.context_menu_items).strip())|n}
</%def> </%def>
<%def name="render_grid_component()"> <%def name="render_grid_component()">
<${grid.component} ref="grid" :csrftoken="csrftoken" ${grid.render_vue_tag()}
% if master.deletable and request.has_perm('{}.delete'.format(permission_prefix)) and master.delete_confirm == 'simple':
@deleteActionClicked="deleteObject"
% endif
>
</${grid.component}>
</%def> </%def>
<%def name="make_this_page_component()"> <%def name="make_this_page_component()">
@ -313,10 +307,8 @@
## finalize grid ## finalize grid
<script> <script>
${grid.vue_component}.data = function() { return ${grid.vue_component}Data }
${grid.component_studly}.data = () => { return ${grid.component_studly}Data } Vue.component('${grid.vue_tagname}', ${grid.vue_component})
Vue.component('${grid.component}', ${grid.component_studly})
</script> </script>
</%def> </%def>
@ -328,11 +320,11 @@
${parent.modify_this_page_vars()} ${parent.modify_this_page_vars()}
<script type="text/javascript"> <script type="text/javascript">
% if master.supports_grid_totals: % if getattr(master, 'supports_grid_totals', False):
${grid.component_studly}Data.gridTotalsDisplay = null ${grid.vue_component}Data.gridTotalsDisplay = null
${grid.component_studly}Data.gridTotalsFetching = false ${grid.vue_component}Data.gridTotalsFetching = false
${grid.component_studly}.methods.gridTotalsFetch = function() { ${grid.vue_component}.methods.gridTotalsFetch = function() {
this.gridTotalsFetching = true this.gridTotalsFetching = true
let url = '${url(f'{route_prefix}.fetch_grid_totals')}' let url = '${url(f'{route_prefix}.fetch_grid_totals')}'
@ -344,7 +336,7 @@
}) })
} }
${grid.component_studly}.methods.appliedFiltersHook = function() { ${grid.vue_component}.methods.appliedFiltersHook = function() {
this.gridTotalsDisplay = null this.gridTotalsDisplay = null
this.gridTotalsFetching = false this.gridTotalsFetching = false
} }
@ -388,7 +380,7 @@
% endif % endif
## delete single object ## delete single object
% if master.deletable and master.has_perm('delete') and master.delete_confirm == 'simple': % if master.deletable and master.has_perm('delete') and getattr(master, 'delete_confirm', 'full') == 'simple':
ThisPage.methods.deleteObject = function(url) { ThisPage.methods.deleteObject = function(url) {
if (confirm("Are you sure you wish to delete this ${model_title}?")) { if (confirm("Are you sure you wish to delete this ${model_title}?")) {
let form = this.$refs.deleteObjectForm let form = this.$refs.deleteObjectForm
@ -399,19 +391,19 @@
% endif % endif
## download results ## download results
% if master.results_downloadable and master.has_perm('download_results'): % if getattr(master, 'results_downloadable', False) and master.has_perm('download_results'):
${grid.component_studly}Data.downloadResultsFormat = '${master.download_results_default_format()}' ${grid.vue_component}Data.downloadResultsFormat = '${master.download_results_default_format()}'
${grid.component_studly}Data.showDownloadResultsDialog = false ${grid.vue_component}Data.showDownloadResultsDialog = false
${grid.component_studly}Data.downloadResultsFieldsMode = 'default' ${grid.vue_component}Data.downloadResultsFieldsMode = 'default'
${grid.component_studly}Data.downloadResultsFieldsAvailable = ${json.dumps(download_results_fields_available)|n} ${grid.vue_component}Data.downloadResultsFieldsAvailable = ${json.dumps(download_results_fields_available)|n}
${grid.component_studly}Data.downloadResultsFieldsDefault = ${json.dumps(download_results_fields_default)|n} ${grid.vue_component}Data.downloadResultsFieldsDefault = ${json.dumps(download_results_fields_default)|n}
${grid.component_studly}Data.downloadResultsFieldsIncluded = ${json.dumps(download_results_fields_default)|n} ${grid.vue_component}Data.downloadResultsFieldsIncluded = ${json.dumps(download_results_fields_default)|n}
${grid.component_studly}Data.downloadResultsExcludedFieldsSelected = [] ${grid.vue_component}Data.downloadResultsExcludedFieldsSelected = []
${grid.component_studly}Data.downloadResultsIncludedFieldsSelected = [] ${grid.vue_component}Data.downloadResultsIncludedFieldsSelected = []
${grid.component_studly}.computed.downloadResultsFieldsExcluded = function() { ${grid.vue_component}.computed.downloadResultsFieldsExcluded = function() {
let excluded = [] let excluded = []
this.downloadResultsFieldsAvailable.forEach(field => { this.downloadResultsFieldsAvailable.forEach(field => {
if (!this.downloadResultsFieldsIncluded.includes(field)) { if (!this.downloadResultsFieldsIncluded.includes(field)) {
@ -421,7 +413,7 @@
return excluded return excluded
} }
${grid.component_studly}.methods.downloadResultsExcludeFields = function() { ${grid.vue_component}.methods.downloadResultsExcludeFields = function() {
const selected = Array.from(this.downloadResultsIncludedFieldsSelected) const selected = Array.from(this.downloadResultsIncludedFieldsSelected)
if (!selected) { if (!selected) {
return return
@ -445,7 +437,7 @@
}) })
} }
${grid.component_studly}.methods.downloadResultsIncludeFields = function() { ${grid.vue_component}.methods.downloadResultsIncludeFields = function() {
const selected = Array.from(this.downloadResultsExcludedFieldsSelected) const selected = Array.from(this.downloadResultsExcludedFieldsSelected)
if (!selected) { if (!selected) {
return return
@ -466,28 +458,28 @@
}) })
} }
${grid.component_studly}.methods.downloadResultsUseDefaultFields = function() { ${grid.vue_component}.methods.downloadResultsUseDefaultFields = function() {
this.downloadResultsFieldsIncluded = Array.from(this.downloadResultsFieldsDefault) this.downloadResultsFieldsIncluded = Array.from(this.downloadResultsFieldsDefault)
this.downloadResultsFieldsMode = 'default' this.downloadResultsFieldsMode = 'default'
} }
${grid.component_studly}.methods.downloadResultsUseAllFields = function() { ${grid.vue_component}.methods.downloadResultsUseAllFields = function() {
this.downloadResultsFieldsIncluded = Array.from(this.downloadResultsFieldsAvailable) this.downloadResultsFieldsIncluded = Array.from(this.downloadResultsFieldsAvailable)
this.downloadResultsFieldsMode = 'all' this.downloadResultsFieldsMode = 'all'
} }
${grid.component_studly}.methods.downloadResultsSubmit = function() { ${grid.vue_component}.methods.downloadResultsSubmit = function() {
this.$refs.download_results_form.submit() this.$refs.download_results_form.submit()
} }
% endif % endif
## download rows for results ## download rows for results
% if master.has_rows and master.results_rows_downloadable and master.has_perm('download_results_rows'): % if getattr(master, 'has_rows', False) and master.results_rows_downloadable and master.has_perm('download_results_rows'):
${grid.component_studly}Data.downloadResultsRowsButtonDisabled = false ${grid.vue_component}Data.downloadResultsRowsButtonDisabled = false
${grid.component_studly}Data.downloadResultsRowsButtonText = "Download Rows for Results" ${grid.vue_component}Data.downloadResultsRowsButtonText = "Download Rows for Results"
${grid.component_studly}.methods.downloadResultsRows = function() { ${grid.vue_component}.methods.downloadResultsRows = function() {
if (confirm("This will generate an Excel file which contains " if (confirm("This will generate an Excel file which contains "
+ "not the results themselves, but the *rows* for " + "not the results themselves, but the *rows* for "
+ "each.\n\nAre you sure you want this?")) { + "each.\n\nAre you sure you want this?")) {
@ -499,12 +491,12 @@
% endif % endif
## enable / disable selected objects ## enable / disable selected objects
% if master.supports_set_enabled_toggle and master.has_perm('enable_disable_set'): % if getattr(master, 'supports_set_enabled_toggle', False) and master.has_perm('enable_disable_set'):
${grid.component_studly}Data.enableSelectedSubmitting = false ${grid.vue_component}Data.enableSelectedSubmitting = false
${grid.component_studly}Data.enableSelectedText = "Enable Selected" ${grid.vue_component}Data.enableSelectedText = "Enable Selected"
${grid.component_studly}.computed.enableSelectedDisabled = function() { ${grid.vue_component}.computed.enableSelectedDisabled = function() {
if (this.enableSelectedSubmitting) { if (this.enableSelectedSubmitting) {
return true return true
} }
@ -514,7 +506,7 @@
return false return false
} }
${grid.component_studly}.methods.enableSelectedSubmit = function() { ${grid.vue_component}.methods.enableSelectedSubmit = function() {
let uuids = this.checkedRowUUIDs() let uuids = this.checkedRowUUIDs()
if (!uuids.length) { if (!uuids.length) {
alert("You must first select one or more objects to disable.") alert("You must first select one or more objects to disable.")
@ -529,10 +521,10 @@
this.$refs.enable_selected_form.submit() this.$refs.enable_selected_form.submit()
} }
${grid.component_studly}Data.disableSelectedSubmitting = false ${grid.vue_component}Data.disableSelectedSubmitting = false
${grid.component_studly}Data.disableSelectedText = "Disable Selected" ${grid.vue_component}Data.disableSelectedText = "Disable Selected"
${grid.component_studly}.computed.disableSelectedDisabled = function() { ${grid.vue_component}.computed.disableSelectedDisabled = function() {
if (this.disableSelectedSubmitting) { if (this.disableSelectedSubmitting) {
return true return true
} }
@ -542,7 +534,7 @@
return false return false
} }
${grid.component_studly}.methods.disableSelectedSubmit = function() { ${grid.vue_component}.methods.disableSelectedSubmit = function() {
let uuids = this.checkedRowUUIDs() let uuids = this.checkedRowUUIDs()
if (!uuids.length) { if (!uuids.length) {
alert("You must first select one or more objects to disable.") alert("You must first select one or more objects to disable.")
@ -560,12 +552,12 @@
% endif % endif
## delete selected objects ## delete selected objects
% if master.set_deletable and master.has_perm('delete_set'): % if getattr(master, 'set_deletable', False) and master.has_perm('delete_set'):
${grid.component_studly}Data.deleteSelectedSubmitting = false ${grid.vue_component}Data.deleteSelectedSubmitting = false
${grid.component_studly}Data.deleteSelectedText = "Delete Selected" ${grid.vue_component}Data.deleteSelectedText = "Delete Selected"
${grid.component_studly}.computed.deleteSelectedDisabled = function() { ${grid.vue_component}.computed.deleteSelectedDisabled = function() {
if (this.deleteSelectedSubmitting) { if (this.deleteSelectedSubmitting) {
return true return true
} }
@ -575,7 +567,7 @@
return false return false
} }
${grid.component_studly}.methods.deleteSelectedSubmit = function() { ${grid.vue_component}.methods.deleteSelectedSubmit = function() {
let uuids = this.checkedRowUUIDs() let uuids = this.checkedRowUUIDs()
if (!uuids.length) { if (!uuids.length) {
alert("You must first select one or more objects to disable.") alert("You must first select one or more objects to disable.")
@ -591,12 +583,12 @@
} }
% endif % endif
% if master.bulk_deletable and master.has_perm('bulk_delete'): % if getattr(master, 'bulk_deletable', False) and master.has_perm('bulk_delete'):
${grid.component_studly}Data.deleteResultsSubmitting = false ${grid.vue_component}Data.deleteResultsSubmitting = false
${grid.component_studly}Data.deleteResultsText = "Delete Results" ${grid.vue_component}Data.deleteResultsText = "Delete Results"
${grid.component_studly}.computed.deleteResultsDisabled = function() { ${grid.vue_component}.computed.deleteResultsDisabled = function() {
if (this.deleteResultsSubmitting) { if (this.deleteResultsSubmitting) {
return true return true
} }
@ -606,7 +598,7 @@
return false return false
} }
${grid.component_studly}.methods.deleteResultsSubmit = function() { ${grid.vue_component}.methods.deleteResultsSubmit = function() {
// TODO: show "plural model title" here? // TODO: show "plural model title" here?
if (!confirm("You are about to delete " + this.total.toLocaleString('en') + " objects.\n\nAre you sure?")) { if (!confirm("You are about to delete " + this.total.toLocaleString('en') + " objects.\n\nAre you sure?")) {
return return
@ -619,12 +611,12 @@
% endif % endif
% if master.mergeable and master.has_perm('merge'): % if getattr(master, 'mergeable', False) and master.has_perm('merge'):
${grid.component_studly}Data.mergeFormButtonText = "Merge 2 ${model_title_plural}" ${grid.vue_component}Data.mergeFormButtonText = "Merge 2 ${model_title_plural}"
${grid.component_studly}Data.mergeFormSubmitting = false ${grid.vue_component}Data.mergeFormSubmitting = false
${grid.component_studly}.methods.submitMergeForm = function() { ${grid.vue_component}.methods.submitMergeForm = function() {
this.mergeFormSubmitting = true this.mergeFormSubmitting = true
this.mergeFormButtonText = "Working, please wait..." this.mergeFormButtonText = "Working, please wait..."
} }

View file

@ -8,7 +8,7 @@
</%def> </%def>
<%def name="render_instance_header_title_extras()"> <%def name="render_instance_header_title_extras()">
% if master.touchable and master.has_perm('touch'): % if getattr(master, 'touchable', False) and master.has_perm('touch'):
<b-button title="&quot;Touch&quot; this record to trigger sync" <b-button title="&quot;Touch&quot; this record to trigger sync"
@click="touchRecord()" @click="touchRecord()"
:disabled="touchSubmitting"> :disabled="touchSubmitting">
@ -93,7 +93,7 @@
${parent.render_this_page()} ${parent.render_this_page()}
## render row grid ## render row grid
% if master.has_rows: % if getattr(master, 'has_rows', False):
<br /> <br />
% if rows_title: % if rows_title:
<h4 class="block is-size-4">${rows_title}</h4> <h4 class="block is-size-4">${rows_title}</h4>
@ -241,7 +241,7 @@
</%def> </%def>
<%def name="render_this_page_template()"> <%def name="render_this_page_template()">
% if master.has_rows: % if getattr(master, 'has_rows', False):
## TODO: stop using |n filter ## TODO: stop using |n filter
${rows_grid.render_complete(allow_save_defaults=False, tools=capture(self.render_row_grid_tools))|n} ${rows_grid.render_complete(allow_save_defaults=False, tools=capture(self.render_row_grid_tools))|n}
% endif % endif
@ -318,7 +318,7 @@
${parent.modify_whole_page_vars()} ${parent.modify_whole_page_vars()}
<script type="text/javascript"> <script type="text/javascript">
% if master.touchable and master.has_perm('touch'): % if getattr(master, 'touchable', False) and master.has_perm('touch'):
WholePageData.touchSubmitting = false WholePageData.touchSubmitting = false
@ -340,7 +340,7 @@
${parent.finalize_this_page_vars()} ${parent.finalize_this_page_vars()}
<script type="text/javascript"> <script type="text/javascript">
% if master.has_rows: % if getattr(master, 'has_rows', False):
TailboneGrid.data = function() { return TailboneGridData } TailboneGrid.data = function() { return TailboneGridData }
Vue.component('tailbone-grid', TailboneGrid) Vue.component('tailbone-grid', TailboneGrid)
% endif % endif

View file

@ -3,7 +3,7 @@
<%def name="grid_tools()"> <%def name="grid_tools()">
% if master.mergeable and master.has_perm('request_merge'): % if getattr(master, 'mergeable', False) and master.has_perm('request_merge'):
<b-button @click="showMergeRequest()" <b-button @click="showMergeRequest()"
icon-pack="fas" icon-pack="fas"
icon-left="object-ungroup" icon-left="object-ungroup"
@ -65,7 +65,7 @@
${parent.modify_this_page_vars()} ${parent.modify_this_page_vars()}
<script type="text/javascript"> <script type="text/javascript">
% if master.mergeable and master.has_perm('request_merge'): % if getattr(master, 'mergeable', False) and master.has_perm('request_merge'):
${grid.component_studly}Data.mergeRequestShowDialog = false ${grid.component_studly}Data.mergeRequestShowDialog = false
${grid.component_studly}Data.mergeRequestRows = [] ${grid.component_studly}Data.mergeRequestRows = []

View file

@ -9,7 +9,7 @@
<%def name="render_form()"> <%def name="render_form()">
<div class="form"> <div class="form">
<tailbone-form v-on:make-user="makeUser"></tailbone-form> <${form.vue_tagname} v-on:make-user="makeUser"></${form.vue_tagname}>
</div> </div>
</%def> </%def>
@ -17,7 +17,7 @@
${parent.modify_this_page_vars()} ${parent.modify_this_page_vars()}
<script type="text/javascript"> <script type="text/javascript">
TailboneForm.methods.clickMakeUser = function(event) { ${form.vue_component}.methods.clickMakeUser = function(event) {
this.$emit('make-user') this.$emit('make-user')
} }

View file

@ -15,7 +15,7 @@
<script type="text/x-template" id="find-principals-template"> <script type="text/x-template" id="find-principals-template">
<div> <div>
${h.form(request.current_route_url(), method='GET', **{'@submit': 'formSubmitting = true'})} ${h.form(request.url, method='GET', **{'@submit': 'formSubmitting = true'})}
<div style="margin-left: 10rem; max-width: 50%;"> <div style="margin-left: 10rem; max-width: 50%;">
${h.hidden('permission_group', **{':value': 'selectedGroup'})} ${h.hidden('permission_group', **{':value': 'selectedGroup'})}
@ -63,7 +63,7 @@
<b-field horizontal> <b-field horizontal>
<div class="buttons" style="margin-top: 1rem;"> <div class="buttons" style="margin-top: 1rem;">
<once-button tag="a" <once-button tag="a"
href="${request.current_route_url(_query=None)}" href="${request.path_url}"
text="Reset Form"> text="Reset Form">
</once-button> </once-button>
<b-button type="is-primary" <b-button type="is-primary"

View file

@ -686,7 +686,7 @@
<h1 class="title"> <h1 class="title">
${index_title} ${index_title}
</h1> </h1>
% if master.creatable and master.show_create_link and master.has_perm('create'): % if master.creatable and getattr(master, 'show_create_link', True) and master.has_perm('create'):
<once-button type="is-primary" <once-button type="is-primary"
tag="a" href="${url('{}.create'.format(route_prefix))}" tag="a" href="${url('{}.create'.format(route_prefix))}"
icon-left="plus" icon-left="plus"
@ -712,7 +712,7 @@
<h1 class="title"> <h1 class="title">
${h.link_to(instance_title, instance_url)} ${h.link_to(instance_title, instance_url)}
</h1> </h1>
% elif master.creatable and master.show_create_link and master.has_perm('create'): % elif master.creatable and getattr(master, 'show_create_link', True) and master.has_perm('create'):
% if not request.matched_route.name.endswith('.create'): % if not request.matched_route.name.endswith('.create'):
<once-button type="is-primary" <once-button type="is-primary"
tag="a" href="${url('{}.create'.format(route_prefix))}" tag="a" href="${url('{}.create'.format(route_prefix))}"
@ -966,23 +966,23 @@
</%def> </%def>
<%def name="render_crud_header_buttons()"> <%def name="render_crud_header_buttons()">
% if master and master.viewing and not master.cloning: % if master and master.viewing and not getattr(master, 'cloning', False):
## TODO: is there a better way to check if viewing parent? ## TODO: is there a better way to check if viewing parent?
% if parent_instance is Undefined: % if parent_instance is Undefined:
% if master.editable and instance_editable and master.has_perm('edit'): % if master.editable and instance_editable and master.has_perm('edit'):
<once-button tag="a" href="${action_url('edit', instance)}" <once-button tag="a" href="${master.get_action_url('edit', instance)}"
icon-left="edit" icon-left="edit"
text="Edit This"> text="Edit This">
</once-button> </once-button>
% endif % endif
% if not master.cloning and master.cloneable and master.has_perm('clone'): % if not getattr(master, 'cloning', False) and getattr(master, 'cloneable', False) and master.has_perm('clone'):
<once-button tag="a" href="${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">
</once-button> </once-button>
% endif % endif
% if master.deletable and instance_deletable and master.has_perm('delete'): % if master.deletable and instance_deletable and master.has_perm('delete'):
<once-button tag="a" href="${action_url('delete', instance)}" <once-button tag="a" href="${master.get_action_url('delete', instance)}"
type="is-danger" type="is-danger"
icon-left="trash" icon-left="trash"
text="Delete This"> text="Delete This">
@ -991,7 +991,7 @@
% else: % else:
## viewing row ## viewing row
% if instance_deletable and master.has_perm('delete_row'): % if instance_deletable and master.has_perm('delete_row'):
<once-button tag="a" href="${action_url('delete', instance)}" <once-button tag="a" href="${master.get_action_url('delete', instance)}"
type="is-danger" type="is-danger"
icon-left="trash" icon-left="trash"
text="Delete This"> text="Delete This">
@ -1000,13 +1000,13 @@
% endif % endif
% elif master and master.editing: % elif master and master.editing:
% if master.viewable and master.has_perm('view'): % if master.viewable and master.has_perm('view'):
<once-button tag="a" href="${action_url('view', instance)}" <once-button tag="a" href="${master.get_action_url('view', instance)}"
icon-left="eye" icon-left="eye"
text="View This"> text="View This">
</once-button> </once-button>
% endif % endif
% if master.deletable and instance_deletable and master.has_perm('delete'): % if master.deletable and instance_deletable and master.has_perm('delete'):
<once-button tag="a" href="${action_url('delete', instance)}" <once-button tag="a" href="${master.get_action_url('delete', instance)}"
type="is-danger" type="is-danger"
icon-left="trash" icon-left="trash"
text="Delete This"> text="Delete This">
@ -1014,20 +1014,20 @@
% endif % endif
% elif master and master.deleting: % elif master and master.deleting:
% if master.viewable and master.has_perm('view'): % if master.viewable and master.has_perm('view'):
<once-button tag="a" href="${action_url('view', instance)}" <once-button tag="a" href="${master.get_action_url('view', instance)}"
icon-left="eye" icon-left="eye"
text="View This"> text="View This">
</once-button> </once-button>
% endif % endif
% if master.editable and instance_editable and master.has_perm('edit'): % if master.editable and instance_editable and master.has_perm('edit'):
<once-button tag="a" href="${action_url('edit', instance)}" <once-button tag="a" href="${master.get_action_url('edit', instance)}"
icon-left="edit" icon-left="edit"
text="Edit This"> text="Edit This">
</once-button> </once-button>
% endif % endif
% elif master and master.cloning: % elif master and getattr(master, 'cloning', False):
% if master.viewable and master.has_perm('view'): % if master.viewable and master.has_perm('view'):
<once-button tag="a" href="${action_url('view', instance)}" <once-button tag="a" href="${master.get_action_url('view', instance)}"
icon-left="eye" icon-left="eye"
text="View This"> text="View This">
</once-button> </once-button>

View file

@ -1366,7 +1366,7 @@ class MasterView(View):
txnid=txn.id) txnid=txn.id)
kwargs = { kwargs = {
'component': 'versions-grid', 'vue_tagname': 'versions-grid',
'ajax_data_url': self.get_action_url('revisions_data', obj), 'ajax_data_url': self.get_action_url('revisions_data', obj),
'sortable': True, 'sortable': True,
'default_sortkey': 'changed', 'default_sortkey': 'changed',
@ -4421,7 +4421,7 @@ class MasterView(View):
'request': self.request, 'request': self.request,
'readonly': self.viewing, 'readonly': self.viewing,
'model_class': getattr(self, 'model_class', None), 'model_class': getattr(self, 'model_class', None),
'action_url': self.request.current_route_url(_query=None), 'action_url': self.request.path_url,
'assume_local_times': self.has_local_times, 'assume_local_times': self.has_local_times,
'route_prefix': route_prefix, 'route_prefix': route_prefix,
'can_edit_help': self.can_edit_help(), 'can_edit_help': self.can_edit_help(),

View file

@ -54,7 +54,7 @@ class PrincipalMasterView(MasterView):
View for finding all users who have been granted a given permission View for finding all users who have been granted a given permission
""" """
permissions = copy.deepcopy( permissions = copy.deepcopy(
self.request.registry.settings.get('tailbone_permissions', {})) self.request.registry.settings.get('wutta_permissions', {}))
# sort groups, and permissions for each group, for UI's sake # sort groups, and permissions for each group, for UI's sake
sorted_perms = sorted(permissions.items(), key=self.perm_sortkey) sorted_perms = sorted(permissions.items(), key=self.perm_sortkey)

View file

@ -287,8 +287,8 @@ class RoleView(PrincipalMasterView):
if the current user is an admin; otherwise it will be the "subset" of if the current user is an admin; otherwise it will be the "subset" of
permissions which the current user has been granted. permissions which the current user has been granted.
""" """
# fetch full set of permissions registered in the app # get all known permissions from settings cache
permissions = self.request.registry.settings.get('tailbone_permissions', {}) permissions = self.request.registry.settings.get('wutta_permissions', {})
# admin user gets to manage all permissions # admin user gets to manage all permissions
if self.request.is_admin: if self.request.is_admin:

View file

@ -276,7 +276,7 @@ class UserView(PrincipalMasterView):
# fs.confirm_password.attrs(autocomplete='new-password') # fs.confirm_password.attrs(autocomplete='new-password')
if self.viewing: if self.viewing:
permissions = self.request.registry.settings.get('tailbone_permissions', {}) permissions = self.request.registry.settings.get('wutta_permissions', {})
f.set_renderer('permissions', PermissionsRenderer(request=self.request, f.set_renderer('permissions', PermissionsRenderer(request=self.request,
permissions=permissions, permissions=permissions,
include_anonymous=True, include_anonymous=True,

View file

@ -12,9 +12,6 @@ class TestCase(unittest.TestCase):
def setUp(self): def setUp(self):
self.config = testing.setUp() self.config = testing.setUp()
# TODO: this probably shouldn't (need to) be here
self.config.add_directive('add_tailbone_permission_group', 'tailbone.auth.add_permission_group')
self.config.add_directive('add_tailbone_permission', 'tailbone.auth.add_permission')
def tearDown(self): def tearDown(self):
testing.tearDown() testing.tearDown()

0
tests/forms/__init__.py Normal file
View file

153
tests/forms/test_core.py Normal file
View file

@ -0,0 +1,153 @@
# -*- coding: utf-8; -*-
from unittest.mock import patch
import deform
from pyramid import testing
from tailbone.forms import core as mod
from tests.util import WebTestCase
class TestForm(WebTestCase):
def setUp(self):
self.setup_web()
self.config.setdefault('rattail.web.menus.handler_spec', 'tests.util:NullMenuHandler')
def make_form(self, **kwargs):
kwargs.setdefault('request', self.request)
return mod.Form(**kwargs)
def test_basic(self):
form = self.make_form()
self.assertIsInstance(form, mod.Form)
def test_vue_tagname(self):
# default
form = self.make_form()
self.assertEqual(form.vue_tagname, 'tailbone-form')
# can override with param
form = self.make_form(vue_tagname='something-else')
self.assertEqual(form.vue_tagname, 'something-else')
# can still pass old param
form = self.make_form(component='legacy-name')
self.assertEqual(form.vue_tagname, 'legacy-name')
def test_vue_component(self):
# default
form = self.make_form()
self.assertEqual(form.vue_component, 'TailboneForm')
# can override with param
form = self.make_form(vue_tagname='something-else')
self.assertEqual(form.vue_component, 'SomethingElse')
# can still pass old param
form = self.make_form(component='legacy-name')
self.assertEqual(form.vue_component, 'LegacyName')
def test_component(self):
# default
form = self.make_form()
self.assertEqual(form.component, 'tailbone-form')
# can override with param
form = self.make_form(vue_tagname='something-else')
self.assertEqual(form.component, 'something-else')
# can still pass old param
form = self.make_form(component='legacy-name')
self.assertEqual(form.component, 'legacy-name')
def test_component_studly(self):
# default
form = self.make_form()
self.assertEqual(form.component_studly, 'TailboneForm')
# can override with param
form = self.make_form(vue_tagname='something-else')
self.assertEqual(form.component_studly, 'SomethingElse')
# can still pass old param
form = self.make_form(component='legacy-name')
self.assertEqual(form.component_studly, 'LegacyName')
def test_button_label_submit(self):
form = self.make_form()
# default
self.assertEqual(form.button_label_submit, "Submit")
# can set submit_label
with patch.object(form, 'submit_label', new="Submit Label", create=True):
self.assertEqual(form.button_label_submit, "Submit Label")
# can set save_label
with patch.object(form, 'save_label', new="Save Label"):
self.assertEqual(form.button_label_submit, "Save Label")
# can set button_label_submit
form.button_label_submit = "New Label"
self.assertEqual(form.button_label_submit, "New Label")
def test_get_deform(self):
model = self.app.model
# sanity check
form = self.make_form(model_class=model.Setting)
dform = form.get_deform()
self.assertIsInstance(dform, deform.Form)
def test_render_vue_tag(self):
model = self.app.model
# sanity check
form = self.make_form(model_class=model.Setting)
html = form.render_vue_tag()
self.assertIn('<tailbone-form', html)
def test_render_vue_template(self):
self.pyramid_config.include('tailbone.views.common')
model = self.app.model
# sanity check
form = self.make_form(model_class=model.Setting)
html = form.render_vue_template(session=self.session)
self.assertIn('<form ', html)
def test_get_vue_field_value(self):
model = self.app.model
form = self.make_form(model_class=model.Setting)
# TODO: yikes what a hack (?)
dform = form.get_deform()
dform.set_appstruct({'name': 'foo', 'value': 'bar'})
# null for missing field
value = form.get_vue_field_value('doesnotexist')
self.assertIsNone(value)
# normal value is returned
value = form.get_vue_field_value('name')
self.assertEqual(value, 'foo')
# but not if we remove field from deform
# TODO: what is the use case here again?
dform.children.remove(dform['name'])
value = form.get_vue_field_value('name')
self.assertIsNone(value)
def test_render_vue_field(self):
model = self.app.model
# sanity check
form = self.make_form(model_class=model.Setting)
html = form.render_vue_field('name', session=self.session)
self.assertIn('<b-field ', html)

0
tests/grids/__init__.py Normal file
View file

139
tests/grids/test_core.py Normal file
View file

@ -0,0 +1,139 @@
# -*- coding: utf-8; -*-
from unittest.mock import MagicMock
from tailbone.grids import core as mod
from tests.util import WebTestCase
class TestGrid(WebTestCase):
def setUp(self):
self.setup_web()
self.config.setdefault('rattail.web.menus.handler_spec', 'tests.util:NullMenuHandler')
def make_grid(self, key, data=[], **kwargs):
kwargs.setdefault('request', self.request)
return mod.Grid(key, data=data, **kwargs)
def test_basic(self):
grid = self.make_grid('foo')
self.assertIsInstance(grid, mod.Grid)
def test_vue_tagname(self):
# default
grid = self.make_grid('foo')
self.assertEqual(grid.vue_tagname, 'tailbone-grid')
# can override with param
grid = self.make_grid('foo', vue_tagname='something-else')
self.assertEqual(grid.vue_tagname, 'something-else')
# can still pass old param
grid = self.make_grid('foo', component='legacy-name')
self.assertEqual(grid.vue_tagname, 'legacy-name')
def test_vue_component(self):
# default
grid = self.make_grid('foo')
self.assertEqual(grid.vue_component, 'TailboneGrid')
# can override with param
grid = self.make_grid('foo', vue_tagname='something-else')
self.assertEqual(grid.vue_component, 'SomethingElse')
# can still pass old param
grid = self.make_grid('foo', component='legacy-name')
self.assertEqual(grid.vue_component, 'LegacyName')
def test_component(self):
# default
grid = self.make_grid('foo')
self.assertEqual(grid.component, 'tailbone-grid')
# can override with param
grid = self.make_grid('foo', vue_tagname='something-else')
self.assertEqual(grid.component, 'something-else')
# can still pass old param
grid = self.make_grid('foo', component='legacy-name')
self.assertEqual(grid.component, 'legacy-name')
def test_component_studly(self):
# default
grid = self.make_grid('foo')
self.assertEqual(grid.component_studly, 'TailboneGrid')
# can override with param
grid = self.make_grid('foo', vue_tagname='something-else')
self.assertEqual(grid.component_studly, 'SomethingElse')
# can still pass old param
grid = self.make_grid('foo', component='legacy-name')
self.assertEqual(grid.component_studly, 'LegacyName')
def test_actions(self):
# default
grid = self.make_grid('foo')
self.assertEqual(grid.actions, [])
# main actions
grid = self.make_grid('foo', main_actions=['foo'])
self.assertEqual(grid.actions, ['foo'])
# more actions
grid = self.make_grid('foo', main_actions=['foo'], more_actions=['bar'])
self.assertEqual(grid.actions, ['foo', 'bar'])
def test_render_vue_tag(self):
model = self.app.model
# standard
grid = self.make_grid('settings', model_class=model.Setting)
html = grid.render_vue_tag()
self.assertIn('<tailbone-grid', html)
self.assertNotIn('@deleteActionClicked', html)
# with delete hook
master = MagicMock(deletable=True, delete_confirm='simple')
master.has_perm.return_value = True
grid = self.make_grid('settings', model_class=model.Setting)
html = grid.render_vue_tag(master=master)
self.assertIn('<tailbone-grid', html)
self.assertIn('@deleteActionClicked', html)
def test_render_vue_template(self):
# self.pyramid_config.include('tailbone.views.common')
model = self.app.model
# sanity check
grid = self.make_grid('settings', model_class=model.Setting)
html = grid.render_vue_template(session=self.session)
self.assertIn('<b-table', html)
def test_get_vue_columns(self):
model = self.app.model
# sanity check
grid = self.make_grid('settings', model_class=model.Setting)
columns = grid.get_vue_columns()
self.assertEqual(len(columns), 2)
self.assertEqual(columns[0]['field'], 'name')
self.assertEqual(columns[1]['field'], 'value')
def test_get_vue_data(self):
model = self.app.model
# sanity check
grid = self.make_grid('settings', model_class=model.Setting)
data = grid.get_vue_data()
self.assertEqual(data, [])
# calling again returns same data
data2 = grid.get_vue_data()
self.assertIs(data2, data)

View file

@ -3,14 +3,14 @@
import os import os
from unittest import TestCase from unittest import TestCase
from sqlalchemy import create_engine from pyramid.config import Configurator
from wuttjamaican.testing import FileConfigTestCase
from rattail.config import RattailConfig
from rattail.exceptions import ConfigurationError from rattail.exceptions import ConfigurationError
from rattail.db import Session as RattailSession from rattail.config import RattailConfig
from tailbone import app as mod
from tailbone import app from tests.util import DataTestCase
from tailbone.db import Session as TailboneSession
class TestRattailConfig(TestCase): class TestRattailConfig(TestCase):
@ -18,15 +18,34 @@ class TestRattailConfig(TestCase):
config_path = os.path.abspath( config_path = os.path.abspath(
os.path.join(os.path.dirname(__file__), 'data', 'tailbone.conf')) os.path.join(os.path.dirname(__file__), 'data', 'tailbone.conf'))
def tearDown(self):
# may or may not be necessary depending on test
TailboneSession.remove()
def test_settings_arg_must_include_config_path_by_default(self): def test_settings_arg_must_include_config_path_by_default(self):
# error raised if path not provided # error raised if path not provided
self.assertRaises(ConfigurationError, app.make_rattail_config, {}) self.assertRaises(ConfigurationError, mod.make_rattail_config, {})
# get a config object if path provided # get a config object if path provided
result = app.make_rattail_config({'rattail.config': self.config_path}) result = mod.make_rattail_config({'rattail.config': self.config_path})
# nb. cannot test isinstance(RattailConfig) b/c now uses wrapper! # nb. cannot test isinstance(RattailConfig) b/c now uses wrapper!
self.assertIsNotNone(result) self.assertIsNotNone(result)
self.assertTrue(hasattr(result, 'get')) self.assertTrue(hasattr(result, 'get'))
class TestMakePyramidConfig(DataTestCase):
def make_config(self):
myconf = self.write_file('web.conf', """
[rattail.db]
default.url = sqlite://
""")
self.settings = {
'rattail.config': myconf,
'mako.directories': 'tailbone:templates',
}
return mod.make_rattail_config(self.settings)
def test_basic(self):
model = self.app.model
model.Base.metadata.create_all(bind=self.config.appdb_engine)
# sanity check
pyramid_config = mod.make_pyramid_config(self.settings)
self.assertIsInstance(pyramid_config, Configurator)

3
tests/test_auth.py Normal file
View file

@ -0,0 +1,3 @@
# -*- coding: utf-8; -*-
from tailbone import auth as mod

12
tests/test_config.py Normal file
View file

@ -0,0 +1,12 @@
# -*- coding: utf-8; -*-
from tailbone import config as mod
from tests.util import DataTestCase
class TestConfigExtension(DataTestCase):
def test_basic(self):
# sanity / coverage check
ext = mod.ConfigExtension()
ext.configure(self.config)

58
tests/test_subscribers.py Normal file
View file

@ -0,0 +1,58 @@
# -*- coding: utf-8; -*-
from unittest.mock import MagicMock
from pyramid import testing
from tailbone import subscribers as mod
from tests.util import DataTestCase
class TestNewRequest(DataTestCase):
def setUp(self):
self.setup_db()
self.request = self.make_request()
self.pyramid_config = testing.setUp(request=self.request, settings={
'wutta_config': self.config,
})
def tearDown(self):
self.teardown_db()
testing.tearDown()
def make_request(self, **kwargs):
return testing.DummyRequest(**kwargs)
def make_event(self):
return MagicMock(request=self.request)
def test_continuum_remote_addr(self):
event = self.make_event()
# nothing happens
mod.new_request(event, session=self.session)
self.assertFalse(hasattr(self.session, 'continuum_remote_addr'))
# unless request has client_addr
self.request.client_addr = '127.0.0.1'
mod.new_request(event, session=self.session)
self.assertEqual(self.session.continuum_remote_addr, '127.0.0.1')
def test_register_component(self):
event = self.make_event()
# function added
self.assertFalse(hasattr(self.request, 'register_component'))
mod.new_request(event, session=self.session)
self.assertTrue(callable(self.request.register_component))
# call function
self.request.register_component('tailbone-datepicker', 'TailboneDatepicker')
self.assertEqual(self.request._tailbone_registered_components,
{'tailbone-datepicker': 'TailboneDatepicker'})
# duplicate registration ignored
self.request.register_component('tailbone-datepicker', 'TailboneDatepicker')
self.assertEqual(self.request._tailbone_registered_components,
{'tailbone-datepicker': 'TailboneDatepicker'})

75
tests/util.py Normal file
View file

@ -0,0 +1,75 @@
# -*- coding: utf-8; -*-
from unittest.mock import MagicMock
from pyramid import testing
from tailbone import subscribers
from wuttaweb.menus import MenuHandler
# from wuttaweb.subscribers import new_request_set_user
from rattail.testing import DataTestCase
class WebTestCase(DataTestCase):
"""
Base class for test suites requiring a full (typical) web app.
"""
def setUp(self):
self.setup_web()
def setup_web(self):
self.setup_db()
self.request = self.make_request()
self.pyramid_config = testing.setUp(request=self.request, settings={
'wutta_config': self.config,
'rattail_config': self.config,
'mako.directories': ['tailbone:templates'],
# 'pyramid_deform.template_search_path': 'wuttaweb:templates/deform',
})
# init web
# self.pyramid_config.include('pyramid_deform')
self.pyramid_config.include('pyramid_mako')
self.pyramid_config.add_directive('add_wutta_permission_group',
'wuttaweb.auth.add_permission_group')
self.pyramid_config.add_directive('add_wutta_permission',
'wuttaweb.auth.add_permission')
self.pyramid_config.add_directive('add_tailbone_permission_group',
'wuttaweb.auth.add_permission_group')
self.pyramid_config.add_directive('add_tailbone_permission',
'wuttaweb.auth.add_permission')
self.pyramid_config.add_directive('add_tailbone_index_page',
'tailbone.app.add_index_page')
self.pyramid_config.add_directive('add_tailbone_model_view',
'tailbone.app.add_model_view')
self.pyramid_config.add_subscriber('tailbone.subscribers.before_render',
'pyramid.events.BeforeRender')
self.pyramid_config.include('tailbone.static')
# setup new request w/ anonymous user
event = MagicMock(request=self.request)
subscribers.new_request(event, session=self.session)
# def user_getter(request, **kwargs): pass
# new_request_set_user(event, db_session=self.session,
# user_getter=user_getter)
def tearDown(self):
self.teardown_web()
def teardown_web(self):
testing.tearDown()
self.teardown_db()
def make_request(self, **kwargs):
kwargs.setdefault('rattail_config', self.config)
# kwargs.setdefault('wutta_config', self.config)
return testing.DummyRequest(**kwargs)
class NullMenuHandler(MenuHandler):
"""
Dummy menu handler for testing.
"""
def make_menus(self, request, **kwargs):
return []

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8; -*-
from unittest.mock import patch
from tailbone.views import master as mod
from tests.util import WebTestCase
class TestMasterView(WebTestCase):
def make_view(self):
return mod.MasterView(self.request)
def test_make_form_kwargs(self):
self.pyramid_config.add_route('settings.view', '/settings/{name}')
model = self.app.model
setting = model.Setting(name='foo', value='bar')
self.session.add(setting)
self.session.commit()
with patch.multiple(mod.MasterView, create=True,
model_class=model.Setting):
view = self.make_view()
# sanity / coverage check
kw = view.make_form_kwargs(model_instance=setting)
self.assertIsNotNone(kw['action_url'])

View file

@ -0,0 +1,29 @@
# -*- coding: utf-8; -*-
from unittest.mock import patch, MagicMock
from tailbone.views import principal as mod
from tests.util import WebTestCase
class TestPrincipalMasterView(WebTestCase):
def make_view(self):
return mod.PrincipalMasterView(self.request)
def test_find_by_perm(self):
model = self.app.model
self.config.setdefault('rattail.web.menus.handler_spec', 'tests.util:NullMenuHandler')
self.pyramid_config.include('tailbone.views.common')
self.pyramid_config.include('tailbone.views.auth')
self.pyramid_config.add_route('roles', '/roles/')
with patch.multiple(mod.PrincipalMasterView, create=True,
model_class=model.Role,
get_help_url=MagicMock(return_value=None),
get_help_markdown=MagicMock(return_value=None),
can_edit_help=MagicMock(return_value=False)):
# sanity / coverage check
view = self.make_view()
response = view.find_by_perm()
self.assertEqual(response.status_code, 200)

80
tests/views/test_roles.py Normal file
View file

@ -0,0 +1,80 @@
# -*- coding: utf-8; -*-
from unittest.mock import patch
from tailbone.views import roles as mod
from tests.util import WebTestCase
class TestRoleView(WebTestCase):
def make_view(self):
return mod.RoleView(self.request)
def test_includeme(self):
self.pyramid_config.include('tailbone.views.roles')
def get_permissions(self):
return {
'widgets': {
'label': "Widgets",
'perms': {
'widgets.list': {
'label': "List widgets",
},
'widgets.polish': {
'label': "Polish the widgets",
},
'widgets.view': {
'label': "View widget",
},
},
},
}
def test_get_available_permissions(self):
model = self.app.model
auth = self.app.get_auth_handler()
blokes = model.Role(name="Blokes")
auth.grant_permission(blokes, 'widgets.list')
self.session.add(blokes)
barney = model.User(username='barney')
barney.roles.append(blokes)
self.session.add(barney)
self.session.commit()
view = self.make_view()
all_perms = self.get_permissions()
self.request.registry.settings['wutta_permissions'] = all_perms
def has_perm(perm):
if perm == 'widgets.list':
return True
return False
with patch.object(self.request, 'has_perm', new=has_perm, create=True):
# sanity check; current request has 1 perm
self.assertTrue(self.request.has_perm('widgets.list'))
self.assertFalse(self.request.has_perm('widgets.polish'))
self.assertFalse(self.request.has_perm('widgets.view'))
# when editing, user sees only the 1 perm
with patch.object(view, 'editing', new=True):
perms = view.get_available_permissions()
self.assertEqual(list(perms), ['widgets'])
self.assertEqual(list(perms['widgets']['perms']), ['widgets.list'])
# but when viewing, same user sees all perms
with patch.object(view, 'viewing', new=True):
perms = view.get_available_permissions()
self.assertEqual(list(perms), ['widgets'])
self.assertEqual(list(perms['widgets']['perms']),
['widgets.list', 'widgets.polish', 'widgets.view'])
# also, when admin user is editing, sees all perms
self.request.is_admin = True
with patch.object(view, 'editing', new=True):
perms = view.get_available_permissions()
self.assertEqual(list(perms), ['widgets'])
self.assertEqual(list(perms['widgets']['perms']),
['widgets.list', 'widgets.polish', 'widgets.view'])

33
tests/views/test_users.py Normal file
View file

@ -0,0 +1,33 @@
# -*- coding: utf-8; -*-
from unittest.mock import patch, MagicMock
from tailbone.views import users as mod
from tailbone.views.principal import PermissionsRenderer
from tests.util import WebTestCase
class TestUserView(WebTestCase):
def make_view(self):
return mod.UserView(self.request)
def test_includeme(self):
self.pyramid_config.include('tailbone.views.users')
def test_configure_form(self):
self.pyramid_config.include('tailbone.views.users')
model = self.app.model
barney = model.User(username='barney')
self.session.add(barney)
self.session.commit()
view = self.make_view()
# must use mock configure when making form
def configure(form): pass
form = view.make_form(instance=barney, configure=configure)
with patch.object(view, 'viewing', new=True):
self.assertNotIn('permissions', form.renderers)
view.configure_form(form)
self.assertIsInstance(form.renderers['permissions'], PermissionsRenderer)