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:
|
||||
config.include(spec)
|
||||
|
||||
# Add some permissions magic.
|
||||
config.add_directive('add_tailbone_permission_group', 'tailbone.auth.add_permission_group')
|
||||
config.add_directive('add_tailbone_permission', 'tailbone.auth.add_permission')
|
||||
# add some permissions magic
|
||||
config.add_directive('add_wutta_permission_group',
|
||||
'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
|
||||
config.add_directive('add_tailbone_index_page', 'tailbone.app.add_index_page')
|
||||
|
|
|
@ -27,7 +27,7 @@ Authentication & Authorization
|
|||
import logging
|
||||
import re
|
||||
|
||||
from rattail.util import prettify, NOTSET
|
||||
from rattail.util import NOTSET
|
||||
|
||||
from zope.interface import implementer
|
||||
from pyramid.authentication import SessionAuthenticationHelper
|
||||
|
@ -159,30 +159,3 @@ class TailboneSecurityPolicy:
|
|||
|
||||
user = self.identity(request)
|
||||
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
|
||||
|
||||
from rattail.config import ConfigExtension as BaseExtension
|
||||
from wuttjamaican.conf import WuttaConfigExtension
|
||||
|
||||
from rattail.db.config import configure_session
|
||||
|
||||
from tailbone.db import Session
|
||||
|
||||
|
||||
class ConfigExtension(BaseExtension):
|
||||
class ConfigExtension(WuttaConfigExtension):
|
||||
"""
|
||||
Rattail config extension for Tailbone. Does the following:
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ from pyramid_deform import SessionFileUploadTempStore
|
|||
from pyramid.renderers import render
|
||||
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.util import raw_datetime, render_markdown
|
||||
|
@ -328,7 +328,7 @@ class Form(object):
|
|||
"""
|
||||
Base class for all forms.
|
||||
"""
|
||||
save_label = "Save"
|
||||
save_label = "Submit"
|
||||
update_label = "Save"
|
||||
show_cancel = True
|
||||
auto_disable = True
|
||||
|
@ -339,10 +339,12 @@ class Form(object):
|
|||
model_instance=None, model_class=None, appstruct=UNSPECIFIED, nodes={}, enums={}, labels={},
|
||||
assume_local_times=False, renderers=None, renderer_kwargs={},
|
||||
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={},
|
||||
# TODO: ugh this is getting out hand!
|
||||
can_edit_help=False, edit_help_url=None, route_prefix=None,
|
||||
**kwargs
|
||||
):
|
||||
self.fields = None
|
||||
if fields is not None:
|
||||
|
@ -380,7 +382,17 @@ class Form(object):
|
|||
self.focus_spec = focus_spec
|
||||
self.action_url = action_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_field_converters = vuejs_field_converters or {}
|
||||
self.json_data = json_data or {}
|
||||
|
@ -393,10 +405,54 @@ class Form(object):
|
|||
return iter(self.fields)
|
||||
|
||||
@property
|
||||
def component_studly(self):
|
||||
words = self.component.split('-')
|
||||
def vue_component(self):
|
||||
"""
|
||||
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])
|
||||
|
||||
@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):
|
||||
return item in self.fields
|
||||
|
||||
|
@ -805,6 +861,10 @@ class Form(object):
|
|||
DeprecationWarning, stacklevel=2)
|
||||
return self.render_deform(**kwargs)
|
||||
|
||||
def get_deform(self):
|
||||
""" """
|
||||
return self.make_deform_form()
|
||||
|
||||
def make_deform_form(self):
|
||||
if not hasattr(self, 'deform_form'):
|
||||
|
||||
|
@ -843,6 +903,10 @@ class Form(object):
|
|||
|
||||
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):
|
||||
if not template:
|
||||
template = '/forms/deform.mako'
|
||||
|
@ -865,8 +929,8 @@ class Form(object):
|
|||
context.setdefault('form_kwargs', {})
|
||||
# TODO: deprecate / remove the latter option here
|
||||
if self.auto_disable_save or self.auto_disable:
|
||||
context['form_kwargs'].setdefault('ref', self.component_studly)
|
||||
context['form_kwargs']['@submit'] = 'submit{}'.format(self.component_studly)
|
||||
context['form_kwargs'].setdefault('ref', self.vue_component)
|
||||
context['form_kwargs']['@submit'] = 'submit{}'.format(self.vue_component)
|
||||
if self.focus_spec:
|
||||
context['form_kwargs']['data-focus'] = self.focus_spec
|
||||
context['request'] = self.request
|
||||
|
@ -878,12 +942,13 @@ class Form(object):
|
|||
return dict([(field, self.get_label(field))
|
||||
for field in self])
|
||||
|
||||
def get_field_markdowns(self):
|
||||
def get_field_markdowns(self, session=None):
|
||||
app = self.request.rattail_config.get_app()
|
||||
model = app.model
|
||||
session = session or Session()
|
||||
|
||||
if not hasattr(self, 'field_markdowns'):
|
||||
infos = Session.query(model.TailboneFieldInfo)\
|
||||
infos = session.query(model.TailboneFieldInfo)\
|
||||
.filter(model.TailboneFieldInfo.route_prefix == self.route_prefix)\
|
||||
.all()
|
||||
self.field_markdowns = dict([(info.field_name, info.markdown_text)
|
||||
|
@ -891,6 +956,18 @@ class Form(object):
|
|||
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
self.vuejs_component_kwargs.update(kwargs)
|
||||
|
||||
def render_vue_tag(self, **kwargs):
|
||||
""" """
|
||||
return self.render_vuejs_component()
|
||||
|
||||
def render_vuejs_component(self):
|
||||
"""
|
||||
Render the Vue.js component HTML for the form.
|
||||
|
@ -971,7 +1052,7 @@ class Form(object):
|
|||
kwargs = dict(self.vuejs_component_kwargs)
|
||||
if self.can_edit_help:
|
||||
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):
|
||||
"""
|
||||
|
@ -997,7 +1078,12 @@ class Form(object):
|
|||
templates.append(HTML.literal(render(template, context)))
|
||||
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>``
|
||||
wrapper. Note that this is meant to render *editable* fields,
|
||||
|
@ -1015,7 +1101,7 @@ class Form(object):
|
|||
|
||||
if self.field_visible(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)
|
||||
attrs = {
|
||||
|
|
|
@ -198,7 +198,8 @@ class Grid:
|
|||
checkable=None, row_uuid_getter=None,
|
||||
clicking_row_checks_box=False, click_handlers=None,
|
||||
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,
|
||||
**kwargs):
|
||||
|
||||
|
@ -268,19 +269,63 @@ class Grid:
|
|||
if ajax_data_url:
|
||||
self.ajax_data_url = ajax_data_url
|
||||
elif self.request:
|
||||
self.ajax_data_url = self.request.current_route_url(_query=None)
|
||||
self.ajax_data_url = self.request.path_url
|
||||
else:
|
||||
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._whgrid_kwargs = kwargs
|
||||
|
||||
@property
|
||||
def component_studly(self):
|
||||
words = self.component.split('-')
|
||||
def vue_component(self):
|
||||
"""
|
||||
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])
|
||||
|
||||
@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):
|
||||
"""
|
||||
Return a default list of columns, based on :attr:`model_class`.
|
||||
|
@ -1334,6 +1379,21 @@ class Grid:
|
|||
data = self.pager
|
||||
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):
|
||||
"""
|
||||
Render the grid, complete with filters. Note that this also
|
||||
|
@ -1359,7 +1419,8 @@ class Grid:
|
|||
context['request'] = self.request
|
||||
context.setdefault('allow_save_defaults', True)
|
||||
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):
|
||||
warnings.warn("Grid.render_buefy() is deprecated; "
|
||||
|
@ -1575,6 +1636,10 @@ class Grid:
|
|||
return True
|
||||
return False
|
||||
|
||||
def get_vue_columns(self):
|
||||
""" """
|
||||
return self.get_table_columns()
|
||||
|
||||
def get_table_columns(self):
|
||||
"""
|
||||
Return a list of dicts representing all grid columns. Meant
|
||||
|
@ -1600,11 +1665,19 @@ class Grid:
|
|||
if hasattr(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):
|
||||
"""
|
||||
Returns a list of data rows for the grid, for use with
|
||||
client-side JS table.
|
||||
"""
|
||||
if hasattr(self, '_table_data'):
|
||||
return self._table_data
|
||||
|
||||
# filter / sort / paginate to get "visible" data
|
||||
raw_data = self.make_visible_data()
|
||||
data = []
|
||||
|
@ -1704,7 +1777,8 @@ class Grid:
|
|||
else:
|
||||
results['total_items'] = count
|
||||
|
||||
return results
|
||||
self._table_data = results
|
||||
return self._table_data
|
||||
|
||||
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__)
|
||||
|
||||
|
||||
def new_request(event):
|
||||
def new_request(event, session=None):
|
||||
"""
|
||||
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
|
||||
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)
|
||||
|
||||
Function to register a Vue component for use with the app.
|
||||
|
@ -90,6 +81,7 @@ def new_request(event):
|
|||
config = request.wutta_config
|
||||
app = config.get_app()
|
||||
auth = app.get_auth_handler()
|
||||
session = session or Session()
|
||||
|
||||
# compatibility
|
||||
rattail_config = config
|
||||
|
@ -104,50 +96,31 @@ def new_request(event):
|
|||
return 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
|
||||
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?
|
||||
if rattail_config:
|
||||
# request.register_component()
|
||||
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()
|
||||
auth = app.get_auth_handler()
|
||||
request.tailbone_cached_permissions = auth.get_permissions(
|
||||
Session(), request.user)
|
||||
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)
|
||||
|
||||
def has_perm(name):
|
||||
if name in request.tailbone_cached_permissions:
|
||||
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
|
||||
request._tailbone_registered_components[tagname] = classname
|
||||
request.register_component = register_component
|
||||
|
||||
|
||||
def before_render(event):
|
||||
|
|
|
@ -153,12 +153,16 @@
|
|||
<style type="text/css">
|
||||
.filters .filter-fieldname,
|
||||
.filters .filter-fieldname .button {
|
||||
% if filter_fieldname_width is not Undefined:
|
||||
min-width: ${filter_fieldname_width};
|
||||
% endif
|
||||
justify-content: left;
|
||||
}
|
||||
% if filter_fieldname_width is not Undefined:
|
||||
.filters .filter-verb {
|
||||
min-width: ${filter_verb_width};
|
||||
}
|
||||
% endif
|
||||
</style>
|
||||
</%def>
|
||||
|
||||
|
@ -856,7 +860,7 @@
|
|||
feedbackMessage: "",
|
||||
|
||||
% 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,
|
||||
% endif
|
||||
|
||||
|
@ -866,7 +870,7 @@
|
|||
|
||||
globalSearchActive: false,
|
||||
globalSearchTerm: '',
|
||||
globalSearchData: ${json.dumps(global_search_data)|n},
|
||||
globalSearchData: ${json.dumps(global_search_data or [])|n},
|
||||
|
||||
mountedHooks: [],
|
||||
}
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
<%def name="render_form_buttons()"></%def>
|
||||
|
||||
<%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 name="render_form()">
|
||||
<div class="form">
|
||||
${form.render_vuejs_component()}
|
||||
${form.render_vue_tag()}
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
|
@ -111,9 +111,9 @@
|
|||
% if form is not Undefined:
|
||||
<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>
|
||||
% endif
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
## -*- 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>
|
||||
% 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)}
|
||||
% endif
|
||||
|
||||
<section>
|
||||
% if form_body is not Undefined and form_body:
|
||||
${form_body|n}
|
||||
% elif form.grouping:
|
||||
% elif getattr(form, 'grouping', None):
|
||||
% for group in form.grouping:
|
||||
<nav class="panel">
|
||||
<p class="panel-heading">${group}</p>
|
||||
|
@ -27,8 +27,8 @@
|
|||
</nav>
|
||||
% endfor
|
||||
% else:
|
||||
% for field in form.fields:
|
||||
${form.render_field_complete(field)}
|
||||
% for fieldname in form.fields:
|
||||
${form.render_vue_field(fieldname, session=session)}
|
||||
% endfor
|
||||
% endif
|
||||
</section>
|
||||
|
@ -54,20 +54,20 @@
|
|||
<input type="reset" value="Reset" class="button" />
|
||||
% endif
|
||||
## 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"
|
||||
native-type="submit"
|
||||
:disabled="${form.component_studly}Submitting"
|
||||
:disabled="${form.vue_component}Submitting"
|
||||
icon-pack="fas"
|
||||
icon-left="save">
|
||||
{{ ${form.component_studly}ButtonText }}
|
||||
{{ ${form.vue_component}Submitting ? "Working, please wait..." : "${form.button_label_submit}" }}
|
||||
</b-button>
|
||||
% else:
|
||||
<b-button type="is-primary"
|
||||
native-type="submit"
|
||||
icon-pack="fas"
|
||||
icon-left="save">
|
||||
${getattr(form, 'submit_label', getattr(form, 'save_label', "Submit"))}
|
||||
${form.button_label_submit}
|
||||
</b-button>
|
||||
% endif
|
||||
</div>
|
||||
|
@ -122,8 +122,8 @@
|
|||
|
||||
<script type="text/javascript">
|
||||
|
||||
let ${form.component_studly} = {
|
||||
template: '#${form.component}-template',
|
||||
let ${form.vue_component} = {
|
||||
template: '#${form.vue_tagname}-template',
|
||||
mixins: [FormPosterMixin],
|
||||
components: {},
|
||||
props: {
|
||||
|
@ -136,10 +136,9 @@
|
|||
methods: {
|
||||
|
||||
## TODO: deprecate / remove the latter option here
|
||||
% if form.auto_disable_save or form.auto_disable:
|
||||
submit${form.component_studly}() {
|
||||
this.${form.component_studly}Submitting = true
|
||||
this.${form.component_studly}ButtonText = "Working, please wait..."
|
||||
% if getattr(form, 'auto_disable_submit', False) or form.auto_disable_save or form.auto_disable:
|
||||
submit${form.vue_component}() {
|
||||
this.${form.vue_component}Submitting = true
|
||||
},
|
||||
% 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
|
||||
csrftoken: ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n},
|
||||
|
@ -198,16 +197,14 @@
|
|||
% if not form.readonly:
|
||||
% for field in form.fields:
|
||||
% if field in dform:
|
||||
<% field = dform[field] %>
|
||||
field_model_${field.name}: ${form.get_vuejs_model_value(field)|n},
|
||||
field_model_${field}: ${json.dumps(form.get_vue_field_value(field))|n},
|
||||
% endif
|
||||
% endfor
|
||||
% endif
|
||||
|
||||
## TODO: deprecate / remove the latter option here
|
||||
% if form.auto_disable_save or form.auto_disable:
|
||||
${form.component_studly}Submitting: false,
|
||||
${form.component_studly}ButtonText: ${json.dumps(getattr(form, 'submit_label', getattr(form, 'save_label', "Submit")))|n},
|
||||
% if getattr(form, 'auto_disable_submit', False) or form.auto_disable_save or form.auto_disable:
|
||||
${form.vue_component}Submitting: false,
|
||||
% 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; -*-
|
||||
|
||||
<% 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 style="display: flex; justify-content: space-between; margin-bottom: 0.5em;">
|
||||
|
||||
<div style="display: flex; flex-direction: column; justify-content: end;">
|
||||
<div class="filters">
|
||||
% if grid.filterable:
|
||||
% if getattr(grid, 'filterable', False):
|
||||
## TODO: stop using |n filter
|
||||
${grid.render_filters(allow_save_defaults=allow_save_defaults)|n}
|
||||
% endif
|
||||
|
@ -55,7 +55,7 @@
|
|||
|
||||
:checkable="checkable"
|
||||
|
||||
% if grid.checkboxes:
|
||||
% if getattr(grid, 'checkboxes', False):
|
||||
% if request.use_oruga:
|
||||
v-model:checked-rows="checkedRows"
|
||||
% else:
|
||||
|
@ -66,20 +66,22 @@
|
|||
% endif
|
||||
% endif
|
||||
|
||||
% if grid.check_handler:
|
||||
% if getattr(grid, 'check_handler', None):
|
||||
@check="${grid.check_handler}"
|
||||
% endif
|
||||
% if grid.check_all_handler:
|
||||
% if getattr(grid, 'check_all_handler', None):
|
||||
@check-all="${grid.check_all_handler}"
|
||||
% endif
|
||||
|
||||
% if hasattr(grid, 'checkable'):
|
||||
% if isinstance(grid.checkable, str):
|
||||
:is-row-checkable="${grid.row_checkable}"
|
||||
% elif grid.checkable:
|
||||
:is-row-checkable="row => row._checkable"
|
||||
% endif
|
||||
% endif
|
||||
|
||||
% if grid.sortable:
|
||||
% if getattr(grid, 'sortable', False):
|
||||
backend-sorting
|
||||
@sort="onSort"
|
||||
@sorting-priority-removed="sortingPriorityRemoved"
|
||||
|
@ -101,7 +103,7 @@
|
|||
sort-multiple-key="ctrlKey"
|
||||
% endif
|
||||
|
||||
% if grid.click_handlers:
|
||||
% if getattr(grid, 'click_handlers', None):
|
||||
@cellclick="cellClick"
|
||||
% endif
|
||||
|
||||
|
@ -119,17 +121,17 @@
|
|||
:hoverable="true"
|
||||
:narrowed="true">
|
||||
|
||||
% for column in grid_columns:
|
||||
% for column in grid.get_vue_columns():
|
||||
<${b}-table-column field="${column['field']}"
|
||||
label="${column['label']}"
|
||||
v-slot="props"
|
||||
:sortable="${json.dumps(column['sortable'])}"
|
||||
% if grid.is_searchable(column['field']):
|
||||
:sortable="${json.dumps(column.get('sortable', False))}"
|
||||
% if hasattr(grid, 'is_searchable') and grid.is_searchable(column['field']):
|
||||
searchable
|
||||
% endif
|
||||
cell-class="c_${column['field']}"
|
||||
:visible="${json.dumps(column['visible'])}">
|
||||
% if column['field'] in grid.raw_renderers:
|
||||
:visible="${json.dumps(column.get('visible', True))}">
|
||||
% if hasattr(grid, 'raw_renderers') and column['field'] in grid.raw_renderers:
|
||||
${grid.raw_renderers[column['field']]()}
|
||||
% elif grid.is_linked(column['field']):
|
||||
<a :href="props.row._action_url_view"
|
||||
|
@ -144,20 +146,20 @@
|
|||
</${b}-table-column>
|
||||
% endfor
|
||||
|
||||
% if grid.main_actions or grid.more_actions:
|
||||
% if grid.actions:
|
||||
<${b}-table-column field="actions"
|
||||
label="Actions"
|
||||
v-slot="props">
|
||||
## TODO: we do not currently differentiate for "main vs. more"
|
||||
## 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}"
|
||||
:href="props.row._action_url_${action.key}"
|
||||
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}"
|
||||
% endif
|
||||
% if action.target:
|
||||
% if getattr(action, 'target', None):
|
||||
target="${action.target}"
|
||||
% endif
|
||||
>
|
||||
|
@ -192,7 +194,7 @@
|
|||
<template #footer>
|
||||
<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"
|
||||
size="is-small"
|
||||
@click="copyDirectLink()"
|
||||
|
@ -207,7 +209,7 @@
|
|||
<div></div>
|
||||
% endif
|
||||
|
||||
% if grid.pageable:
|
||||
% if getattr(grid, 'pageable', False):
|
||||
<div v-if="firstItem"
|
||||
style="display: flex; gap: 0.5rem; align-items: center;">
|
||||
<span>
|
||||
|
@ -234,7 +236,7 @@
|
|||
</${b}-table>
|
||||
|
||||
## 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>
|
||||
% endif
|
||||
|
||||
|
@ -243,30 +245,30 @@
|
|||
|
||||
<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,
|
||||
ajaxDataUrl: ${json.dumps(grid.ajax_data_url)|n},
|
||||
ajaxDataUrl: ${json.dumps(getattr(grid, 'ajax_data_url', request.path_url))|n},
|
||||
|
||||
savingDefaults: false,
|
||||
|
||||
data: ${grid.component_studly}CurrentData,
|
||||
rowStatusMap: ${json.dumps(grid_data['row_status_map'])|n},
|
||||
data: ${grid.vue_component}CurrentData,
|
||||
rowStatusMap: ${json.dumps(grid_data['row_status_map'] if grid_data is not Undefined else {})|n},
|
||||
|
||||
checkable: ${json.dumps(grid.checkboxes)|n},
|
||||
% if grid.checkboxes:
|
||||
checkable: ${json.dumps(getattr(grid, 'checkboxes', False))|n},
|
||||
% if getattr(grid, 'checkboxes', False):
|
||||
checkedRows: ${grid_data['checked_rows_code']|n},
|
||||
% endif
|
||||
|
||||
paginated: ${json.dumps(grid.pageable)|n},
|
||||
total: ${len(grid_data['data']) if static_data else grid_data['total_items']},
|
||||
perPage: ${json.dumps(grid.pagesize if grid.pageable else None)|n},
|
||||
currentPage: ${json.dumps(grid.page if grid.pageable else None)|n},
|
||||
firstItem: ${json.dumps(grid_data['first_item'] if grid.pageable else None)|n},
|
||||
lastItem: ${json.dumps(grid_data['last_item'] if grid.pageable else None)|n},
|
||||
paginated: ${json.dumps(getattr(grid, 'pageable', False))|n},
|
||||
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 getattr(grid, 'pageable', False) else None)|n},
|
||||
currentPage: ${json.dumps(grid.page if getattr(grid, 'pageable', False) 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 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
|
||||
## displaying for simple default single-column sort. so to
|
||||
|
@ -289,19 +291,19 @@
|
|||
% endif
|
||||
|
||||
## filterable: ${json.dumps(grid.filterable)|n},
|
||||
filters: ${json.dumps(filters_data if grid.filterable else None)|n},
|
||||
filtersSequence: ${json.dumps(filters_sequence 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 getattr(grid, 'filterable', False) else None)|n},
|
||||
addFilterTerm: '',
|
||||
addFilterShow: false,
|
||||
|
||||
## dummy input value needed for sharing links on *insecure* sites
|
||||
% if request.scheme == 'http':
|
||||
% if getattr(request, 'scheme', None) == 'http':
|
||||
shareLink: null,
|
||||
% endif
|
||||
}
|
||||
|
||||
let ${grid.component_studly} = {
|
||||
template: '#${grid.component}-template',
|
||||
let ${grid.vue_component} = {
|
||||
template: '#${grid.vue_tagname}-template',
|
||||
|
||||
mixins: [FormPosterMixin],
|
||||
|
||||
|
@ -358,7 +360,7 @@
|
|||
|
||||
directLink() {
|
||||
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
|
||||
},
|
||||
|
||||
% if grid.click_handlers:
|
||||
% if getattr(grid, 'click_handlers', None):
|
||||
cellClick(row, column, rowIndex, columnIndex) {
|
||||
% for key in grid.click_handlers:
|
||||
if (column._props.field == '${key}') {
|
||||
|
@ -437,13 +439,13 @@
|
|||
|
||||
getBasicParams() {
|
||||
let params = {}
|
||||
% if grid.sortable:
|
||||
% if getattr(grid, 'sortable', False):
|
||||
for (let i = 1; i <= this.backendSorters.length; i++) {
|
||||
params['sort'+i+'key'] = this.backendSorters[i-1].field
|
||||
params['sort'+i+'dir'] = this.backendSorters[i-1].order
|
||||
}
|
||||
% endif
|
||||
% if grid.pageable:
|
||||
% if getattr(grid, 'pageable', False):
|
||||
params.pagesize = this.perPage
|
||||
params.page = this.currentPage
|
||||
% endif
|
||||
|
@ -488,8 +490,8 @@
|
|||
this.loading = true
|
||||
this.$http.get(`${'$'}{this.ajaxDataUrl}?${'$'}{params}`).then(({ data }) => {
|
||||
if (!data.error) {
|
||||
${grid.component_studly}CurrentData = data.data
|
||||
this.data = ${grid.component_studly}CurrentData
|
||||
${grid.vue_component}CurrentData = data.data
|
||||
this.data = ${grid.vue_component}CurrentData
|
||||
this.rowStatusMap = data.row_status_map
|
||||
this.total = data.total_items
|
||||
this.firstItem = data.first_item
|
||||
|
@ -776,7 +778,7 @@
|
|||
} else {
|
||||
this.checkedRows.push(row)
|
||||
}
|
||||
% if grid.check_handler:
|
||||
% if getattr(grid, 'check_handler', None):
|
||||
this.${grid.check_handler}(this.checkedRows, row)
|
||||
% 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; -*-
|
||||
<%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()}
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
<b-button type="is-primary is-danger"
|
||||
native-type="submit"
|
||||
:disabled="formSubmitting">
|
||||
{{ formButtonText }}
|
||||
{{ formSubmitting ? "Working, please wait..." : "${form.button_label_submit}" }}
|
||||
</b-button>
|
||||
</div>
|
||||
${h.end_form()}
|
||||
|
@ -35,14 +35,12 @@
|
|||
|
||||
<%def name="modify_this_page_vars()">
|
||||
${parent.modify_this_page_vars()}
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
|
||||
TailboneFormData.formSubmitting = false
|
||||
TailboneFormData.formButtonText = "Yes, please DELETE this data forever!"
|
||||
${form.vue_component}Data.formSubmitting = false
|
||||
|
||||
TailboneForm.methods.submitForm = function() {
|
||||
${form.vue_component}.methods.submitForm = function() {
|
||||
this.formSubmitting = true
|
||||
this.formButtonText = "Working, please wait..."
|
||||
}
|
||||
|
||||
</script>
|
||||
|
|
|
@ -6,13 +6,13 @@
|
|||
<script type="text/javascript">
|
||||
|
||||
## 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():
|
||||
${form.component_studly}Data.${key} = ${json.dumps(value)|n}
|
||||
% endfor
|
||||
% 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() {
|
||||
if (confirm("Are you sure you wish to delete this ${model_title}?")) {
|
||||
|
@ -23,7 +23,7 @@
|
|||
% endif
|
||||
</script>
|
||||
|
||||
% if form is not Undefined:
|
||||
% if form is not Undefined and hasattr(form, 'render_included_templates'):
|
||||
${form.render_included_templates()}
|
||||
% endif
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<%def name="grid_tools()">
|
||||
|
||||
## grid totals
|
||||
% if master.supports_grid_totals:
|
||||
% if getattr(master, 'supports_grid_totals', False):
|
||||
<div style="display: flex; align-items: center;">
|
||||
<b-button v-if="gridTotalsDisplay == null"
|
||||
:disabled="gridTotalsFetching"
|
||||
|
@ -30,7 +30,7 @@
|
|||
% endif
|
||||
|
||||
## 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>
|
||||
<b-button type="is-primary"
|
||||
icon-pack="fas"
|
||||
|
@ -180,7 +180,7 @@
|
|||
% endif
|
||||
|
||||
## 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"
|
||||
icon-pack="fas"
|
||||
icon-left="download"
|
||||
|
@ -194,7 +194,7 @@
|
|||
% endif
|
||||
|
||||
## 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.csrf_token(request)}
|
||||
|
@ -212,7 +212,7 @@
|
|||
% endif
|
||||
|
||||
## 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.csrf_token(request)}
|
||||
|
@ -234,7 +234,7 @@
|
|||
% endif
|
||||
|
||||
## 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.csrf_token(request)}
|
||||
${h.hidden('uuids', v_model='selected_uuids')}
|
||||
|
@ -249,7 +249,7 @@
|
|||
% endif
|
||||
|
||||
## 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.csrf_token(request)}
|
||||
<b-button type="is-danger"
|
||||
|
@ -283,7 +283,7 @@
|
|||
|
||||
${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.csrf_token(request)}
|
||||
${h.end_form()}
|
||||
|
@ -291,17 +291,11 @@
|
|||
</%def>
|
||||
|
||||
<%def name="make_grid_component()">
|
||||
## TODO: stop using |n filter?
|
||||
${grid.render_complete(tools=capture(self.grid_tools).strip(), context_menu=capture(self.context_menu_items).strip())|n}
|
||||
${grid.render_vue_template(tools=capture(self.grid_tools).strip(), context_menu=capture(self.context_menu_items).strip())}
|
||||
</%def>
|
||||
|
||||
<%def name="render_grid_component()">
|
||||
<${grid.component} ref="grid" :csrftoken="csrftoken"
|
||||
% if master.deletable and request.has_perm('{}.delete'.format(permission_prefix)) and master.delete_confirm == 'simple':
|
||||
@deleteActionClicked="deleteObject"
|
||||
% endif
|
||||
>
|
||||
</${grid.component}>
|
||||
${grid.render_vue_tag()}
|
||||
</%def>
|
||||
|
||||
<%def name="make_this_page_component()">
|
||||
|
@ -313,10 +307,8 @@
|
|||
|
||||
## finalize grid
|
||||
<script>
|
||||
|
||||
${grid.component_studly}.data = () => { return ${grid.component_studly}Data }
|
||||
Vue.component('${grid.component}', ${grid.component_studly})
|
||||
|
||||
${grid.vue_component}.data = function() { return ${grid.vue_component}Data }
|
||||
Vue.component('${grid.vue_tagname}', ${grid.vue_component})
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
|
@ -328,11 +320,11 @@
|
|||
${parent.modify_this_page_vars()}
|
||||
<script type="text/javascript">
|
||||
|
||||
% if master.supports_grid_totals:
|
||||
${grid.component_studly}Data.gridTotalsDisplay = null
|
||||
${grid.component_studly}Data.gridTotalsFetching = false
|
||||
% if getattr(master, 'supports_grid_totals', False):
|
||||
${grid.vue_component}Data.gridTotalsDisplay = null
|
||||
${grid.vue_component}Data.gridTotalsFetching = false
|
||||
|
||||
${grid.component_studly}.methods.gridTotalsFetch = function() {
|
||||
${grid.vue_component}.methods.gridTotalsFetch = function() {
|
||||
this.gridTotalsFetching = true
|
||||
|
||||
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.gridTotalsFetching = false
|
||||
}
|
||||
|
@ -388,7 +380,7 @@
|
|||
% endif
|
||||
|
||||
## 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) {
|
||||
if (confirm("Are you sure you wish to delete this ${model_title}?")) {
|
||||
let form = this.$refs.deleteObjectForm
|
||||
|
@ -399,19 +391,19 @@
|
|||
% endif
|
||||
|
||||
## 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.component_studly}Data.showDownloadResultsDialog = false
|
||||
${grid.component_studly}Data.downloadResultsFieldsMode = 'default'
|
||||
${grid.component_studly}Data.downloadResultsFieldsAvailable = ${json.dumps(download_results_fields_available)|n}
|
||||
${grid.component_studly}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.downloadResultsFormat = '${master.download_results_default_format()}'
|
||||
${grid.vue_component}Data.showDownloadResultsDialog = false
|
||||
${grid.vue_component}Data.downloadResultsFieldsMode = 'default'
|
||||
${grid.vue_component}Data.downloadResultsFieldsAvailable = ${json.dumps(download_results_fields_available)|n}
|
||||
${grid.vue_component}Data.downloadResultsFieldsDefault = ${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.component_studly}Data.downloadResultsIncludedFieldsSelected = []
|
||||
${grid.vue_component}Data.downloadResultsExcludedFieldsSelected = []
|
||||
${grid.vue_component}Data.downloadResultsIncludedFieldsSelected = []
|
||||
|
||||
${grid.component_studly}.computed.downloadResultsFieldsExcluded = function() {
|
||||
${grid.vue_component}.computed.downloadResultsFieldsExcluded = function() {
|
||||
let excluded = []
|
||||
this.downloadResultsFieldsAvailable.forEach(field => {
|
||||
if (!this.downloadResultsFieldsIncluded.includes(field)) {
|
||||
|
@ -421,7 +413,7 @@
|
|||
return excluded
|
||||
}
|
||||
|
||||
${grid.component_studly}.methods.downloadResultsExcludeFields = function() {
|
||||
${grid.vue_component}.methods.downloadResultsExcludeFields = function() {
|
||||
const selected = Array.from(this.downloadResultsIncludedFieldsSelected)
|
||||
if (!selected) {
|
||||
return
|
||||
|
@ -445,7 +437,7 @@
|
|||
})
|
||||
}
|
||||
|
||||
${grid.component_studly}.methods.downloadResultsIncludeFields = function() {
|
||||
${grid.vue_component}.methods.downloadResultsIncludeFields = function() {
|
||||
const selected = Array.from(this.downloadResultsExcludedFieldsSelected)
|
||||
if (!selected) {
|
||||
return
|
||||
|
@ -466,28 +458,28 @@
|
|||
})
|
||||
}
|
||||
|
||||
${grid.component_studly}.methods.downloadResultsUseDefaultFields = function() {
|
||||
${grid.vue_component}.methods.downloadResultsUseDefaultFields = function() {
|
||||
this.downloadResultsFieldsIncluded = Array.from(this.downloadResultsFieldsDefault)
|
||||
this.downloadResultsFieldsMode = 'default'
|
||||
}
|
||||
|
||||
${grid.component_studly}.methods.downloadResultsUseAllFields = function() {
|
||||
${grid.vue_component}.methods.downloadResultsUseAllFields = function() {
|
||||
this.downloadResultsFieldsIncluded = Array.from(this.downloadResultsFieldsAvailable)
|
||||
this.downloadResultsFieldsMode = 'all'
|
||||
}
|
||||
|
||||
${grid.component_studly}.methods.downloadResultsSubmit = function() {
|
||||
${grid.vue_component}.methods.downloadResultsSubmit = function() {
|
||||
this.$refs.download_results_form.submit()
|
||||
}
|
||||
% endif
|
||||
|
||||
## 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.component_studly}Data.downloadResultsRowsButtonText = "Download Rows for Results"
|
||||
${grid.vue_component}Data.downloadResultsRowsButtonDisabled = false
|
||||
${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 "
|
||||
+ "not the results themselves, but the *rows* for "
|
||||
+ "each.\n\nAre you sure you want this?")) {
|
||||
|
@ -499,12 +491,12 @@
|
|||
% endif
|
||||
|
||||
## 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.component_studly}Data.enableSelectedText = "Enable Selected"
|
||||
${grid.vue_component}Data.enableSelectedSubmitting = false
|
||||
${grid.vue_component}Data.enableSelectedText = "Enable Selected"
|
||||
|
||||
${grid.component_studly}.computed.enableSelectedDisabled = function() {
|
||||
${grid.vue_component}.computed.enableSelectedDisabled = function() {
|
||||
if (this.enableSelectedSubmitting) {
|
||||
return true
|
||||
}
|
||||
|
@ -514,7 +506,7 @@
|
|||
return false
|
||||
}
|
||||
|
||||
${grid.component_studly}.methods.enableSelectedSubmit = function() {
|
||||
${grid.vue_component}.methods.enableSelectedSubmit = function() {
|
||||
let uuids = this.checkedRowUUIDs()
|
||||
if (!uuids.length) {
|
||||
alert("You must first select one or more objects to disable.")
|
||||
|
@ -529,10 +521,10 @@
|
|||
this.$refs.enable_selected_form.submit()
|
||||
}
|
||||
|
||||
${grid.component_studly}Data.disableSelectedSubmitting = false
|
||||
${grid.component_studly}Data.disableSelectedText = "Disable Selected"
|
||||
${grid.vue_component}Data.disableSelectedSubmitting = false
|
||||
${grid.vue_component}Data.disableSelectedText = "Disable Selected"
|
||||
|
||||
${grid.component_studly}.computed.disableSelectedDisabled = function() {
|
||||
${grid.vue_component}.computed.disableSelectedDisabled = function() {
|
||||
if (this.disableSelectedSubmitting) {
|
||||
return true
|
||||
}
|
||||
|
@ -542,7 +534,7 @@
|
|||
return false
|
||||
}
|
||||
|
||||
${grid.component_studly}.methods.disableSelectedSubmit = function() {
|
||||
${grid.vue_component}.methods.disableSelectedSubmit = function() {
|
||||
let uuids = this.checkedRowUUIDs()
|
||||
if (!uuids.length) {
|
||||
alert("You must first select one or more objects to disable.")
|
||||
|
@ -560,12 +552,12 @@
|
|||
% endif
|
||||
|
||||
## 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.component_studly}Data.deleteSelectedText = "Delete Selected"
|
||||
${grid.vue_component}Data.deleteSelectedSubmitting = false
|
||||
${grid.vue_component}Data.deleteSelectedText = "Delete Selected"
|
||||
|
||||
${grid.component_studly}.computed.deleteSelectedDisabled = function() {
|
||||
${grid.vue_component}.computed.deleteSelectedDisabled = function() {
|
||||
if (this.deleteSelectedSubmitting) {
|
||||
return true
|
||||
}
|
||||
|
@ -575,7 +567,7 @@
|
|||
return false
|
||||
}
|
||||
|
||||
${grid.component_studly}.methods.deleteSelectedSubmit = function() {
|
||||
${grid.vue_component}.methods.deleteSelectedSubmit = function() {
|
||||
let uuids = this.checkedRowUUIDs()
|
||||
if (!uuids.length) {
|
||||
alert("You must first select one or more objects to disable.")
|
||||
|
@ -591,12 +583,12 @@
|
|||
}
|
||||
% 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.component_studly}Data.deleteResultsText = "Delete Results"
|
||||
${grid.vue_component}Data.deleteResultsSubmitting = false
|
||||
${grid.vue_component}Data.deleteResultsText = "Delete Results"
|
||||
|
||||
${grid.component_studly}.computed.deleteResultsDisabled = function() {
|
||||
${grid.vue_component}.computed.deleteResultsDisabled = function() {
|
||||
if (this.deleteResultsSubmitting) {
|
||||
return true
|
||||
}
|
||||
|
@ -606,7 +598,7 @@
|
|||
return false
|
||||
}
|
||||
|
||||
${grid.component_studly}.methods.deleteResultsSubmit = function() {
|
||||
${grid.vue_component}.methods.deleteResultsSubmit = function() {
|
||||
// TODO: show "plural model title" here?
|
||||
if (!confirm("You are about to delete " + this.total.toLocaleString('en') + " objects.\n\nAre you sure?")) {
|
||||
return
|
||||
|
@ -619,12 +611,12 @@
|
|||
|
||||
% 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.component_studly}Data.mergeFormSubmitting = false
|
||||
${grid.vue_component}Data.mergeFormButtonText = "Merge 2 ${model_title_plural}"
|
||||
${grid.vue_component}Data.mergeFormSubmitting = false
|
||||
|
||||
${grid.component_studly}.methods.submitMergeForm = function() {
|
||||
${grid.vue_component}.methods.submitMergeForm = function() {
|
||||
this.mergeFormSubmitting = true
|
||||
this.mergeFormButtonText = "Working, please wait..."
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
</%def>
|
||||
|
||||
<%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"
|
||||
@click="touchRecord()"
|
||||
:disabled="touchSubmitting">
|
||||
|
@ -93,7 +93,7 @@
|
|||
${parent.render_this_page()}
|
||||
|
||||
## render row grid
|
||||
% if master.has_rows:
|
||||
% if getattr(master, 'has_rows', False):
|
||||
<br />
|
||||
% if rows_title:
|
||||
<h4 class="block is-size-4">${rows_title}</h4>
|
||||
|
@ -241,7 +241,7 @@
|
|||
</%def>
|
||||
|
||||
<%def name="render_this_page_template()">
|
||||
% if master.has_rows:
|
||||
% if getattr(master, 'has_rows', False):
|
||||
## TODO: stop using |n filter
|
||||
${rows_grid.render_complete(allow_save_defaults=False, tools=capture(self.render_row_grid_tools))|n}
|
||||
% endif
|
||||
|
@ -318,7 +318,7 @@
|
|||
${parent.modify_whole_page_vars()}
|
||||
<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
|
||||
|
||||
|
@ -340,7 +340,7 @@
|
|||
${parent.finalize_this_page_vars()}
|
||||
<script type="text/javascript">
|
||||
|
||||
% if master.has_rows:
|
||||
% if getattr(master, 'has_rows', False):
|
||||
TailboneGrid.data = function() { return TailboneGridData }
|
||||
Vue.component('tailbone-grid', TailboneGrid)
|
||||
% endif
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
<%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()"
|
||||
icon-pack="fas"
|
||||
icon-left="object-ungroup"
|
||||
|
@ -65,7 +65,7 @@
|
|||
${parent.modify_this_page_vars()}
|
||||
<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.mergeRequestRows = []
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
<%def name="render_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>
|
||||
</%def>
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
|||
${parent.modify_this_page_vars()}
|
||||
<script type="text/javascript">
|
||||
|
||||
TailboneForm.methods.clickMakeUser = function(event) {
|
||||
${form.vue_component}.methods.clickMakeUser = function(event) {
|
||||
this.$emit('make-user')
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<script type="text/x-template" id="find-principals-template">
|
||||
<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%;">
|
||||
|
||||
${h.hidden('permission_group', **{':value': 'selectedGroup'})}
|
||||
|
@ -63,7 +63,7 @@
|
|||
<b-field horizontal>
|
||||
<div class="buttons" style="margin-top: 1rem;">
|
||||
<once-button tag="a"
|
||||
href="${request.current_route_url(_query=None)}"
|
||||
href="${request.path_url}"
|
||||
text="Reset Form">
|
||||
</once-button>
|
||||
<b-button type="is-primary"
|
||||
|
|
|
@ -686,7 +686,7 @@
|
|||
<h1 class="title">
|
||||
${index_title}
|
||||
</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"
|
||||
tag="a" href="${url('{}.create'.format(route_prefix))}"
|
||||
icon-left="plus"
|
||||
|
@ -712,7 +712,7 @@
|
|||
<h1 class="title">
|
||||
${h.link_to(instance_title, instance_url)}
|
||||
</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'):
|
||||
<once-button type="is-primary"
|
||||
tag="a" href="${url('{}.create'.format(route_prefix))}"
|
||||
|
@ -966,23 +966,23 @@
|
|||
</%def>
|
||||
|
||||
<%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?
|
||||
% if parent_instance is Undefined:
|
||||
% 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"
|
||||
text="Edit This">
|
||||
</once-button>
|
||||
% endif
|
||||
% if not master.cloning and master.cloneable and master.has_perm('clone'):
|
||||
<once-button tag="a" href="${action_url('clone', instance)}"
|
||||
% if not getattr(master, 'cloning', False) and getattr(master, 'cloneable', False) and master.has_perm('clone'):
|
||||
<once-button tag="a" href="${master.get_action_url('clone', instance)}"
|
||||
icon-left="object-ungroup"
|
||||
text="Clone This">
|
||||
</once-button>
|
||||
% endif
|
||||
% 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"
|
||||
icon-left="trash"
|
||||
text="Delete This">
|
||||
|
@ -991,7 +991,7 @@
|
|||
% else:
|
||||
## viewing 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"
|
||||
icon-left="trash"
|
||||
text="Delete This">
|
||||
|
@ -1000,13 +1000,13 @@
|
|||
% endif
|
||||
% elif master and master.editing:
|
||||
% 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"
|
||||
text="View This">
|
||||
</once-button>
|
||||
% endif
|
||||
% 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"
|
||||
icon-left="trash"
|
||||
text="Delete This">
|
||||
|
@ -1014,20 +1014,20 @@
|
|||
% endif
|
||||
% elif master and master.deleting:
|
||||
% 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"
|
||||
text="View This">
|
||||
</once-button>
|
||||
% endif
|
||||
% 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"
|
||||
text="Edit This">
|
||||
</once-button>
|
||||
% endif
|
||||
% elif master and master.cloning:
|
||||
% elif master and getattr(master, 'cloning', False):
|
||||
% 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"
|
||||
text="View This">
|
||||
</once-button>
|
||||
|
|
|
@ -1366,7 +1366,7 @@ class MasterView(View):
|
|||
txnid=txn.id)
|
||||
|
||||
kwargs = {
|
||||
'component': 'versions-grid',
|
||||
'vue_tagname': 'versions-grid',
|
||||
'ajax_data_url': self.get_action_url('revisions_data', obj),
|
||||
'sortable': True,
|
||||
'default_sortkey': 'changed',
|
||||
|
@ -4421,7 +4421,7 @@ class MasterView(View):
|
|||
'request': self.request,
|
||||
'readonly': self.viewing,
|
||||
'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,
|
||||
'route_prefix': route_prefix,
|
||||
'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
|
||||
"""
|
||||
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
|
||||
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
|
||||
permissions which the current user has been granted.
|
||||
"""
|
||||
# fetch full set of permissions registered in the app
|
||||
permissions = self.request.registry.settings.get('tailbone_permissions', {})
|
||||
# get all known permissions from settings cache
|
||||
permissions = self.request.registry.settings.get('wutta_permissions', {})
|
||||
|
||||
# admin user gets to manage all permissions
|
||||
if self.request.is_admin:
|
||||
|
|
|
@ -276,7 +276,7 @@ class UserView(PrincipalMasterView):
|
|||
# fs.confirm_password.attrs(autocomplete='new-password')
|
||||
|
||||
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,
|
||||
permissions=permissions,
|
||||
include_anonymous=True,
|
||||
|
|
|
@ -12,9 +12,6 @@ class TestCase(unittest.TestCase):
|
|||
|
||||
def setUp(self):
|
||||
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):
|
||||
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
|
||||
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.db import Session as RattailSession
|
||||
|
||||
from tailbone import app
|
||||
from tailbone.db import Session as TailboneSession
|
||||
from rattail.config import RattailConfig
|
||||
from tailbone import app as mod
|
||||
from tests.util import DataTestCase
|
||||
|
||||
|
||||
class TestRattailConfig(TestCase):
|
||||
|
@ -18,15 +18,34 @@ class TestRattailConfig(TestCase):
|
|||
config_path = os.path.abspath(
|
||||
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):
|
||||
# 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
|
||||
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!
|
||||
self.assertIsNotNone(result)
|
||||
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