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:
parent
b53479f8e4
commit
a6ce5eb21d
|
@ -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')
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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: [],
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
3
tailbone/templates/forms/vue_template.mako
Normal file
3
tailbone/templates/forms/vue_template.mako
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
## -*- coding: utf-8; -*-
|
||||||
|
<%inherit file="/forms/deform.mako" />
|
||||||
|
${parent.body()}
|
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
3
tailbone/templates/grids/vue_template.mako
Normal file
3
tailbone/templates/grids/vue_template.mako
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
## -*- coding: utf-8; -*-
|
||||||
|
<%inherit file="/grids/complete.mako" />
|
||||||
|
${parent.body()}
|
|
@ -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()}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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..."
|
||||||
}
|
}
|
||||||
|
|
|
@ -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=""Touch" this record to trigger sync"
|
<b-button title=""Touch" 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
|
||||||
|
|
|
@ -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 = []
|
||||||
|
|
|
@ -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')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
0
tests/forms/__init__.py
Normal file
153
tests/forms/test_core.py
Normal file
153
tests/forms/test_core.py
Normal 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
0
tests/grids/__init__.py
Normal file
139
tests/grids/test_core.py
Normal file
139
tests/grids/test_core.py
Normal 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)
|
|
@ -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
3
tests/test_auth.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# -*- coding: utf-8; -*-
|
||||||
|
|
||||||
|
from tailbone import auth as mod
|
12
tests/test_config.py
Normal file
12
tests/test_config.py
Normal 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
58
tests/test_subscribers.py
Normal 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
75
tests/util.py
Normal 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 []
|
26
tests/views/test_master.py
Normal file
26
tests/views/test_master.py
Normal 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'])
|
29
tests/views/test_principal.py
Normal file
29
tests/views/test_principal.py
Normal 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
80
tests/views/test_roles.py
Normal 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
33
tests/views/test_users.py
Normal 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)
|
Loading…
Reference in a new issue