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

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

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

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

View file

@ -189,9 +189,16 @@ def make_pyramid_config(settings, configure_csrf=True):
for spec in includes:
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')

View file

@ -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)

View file

@ -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:

View file

@ -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 = {

View file

@ -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):
"""

View file

@ -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,32 +96,13 @@ 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
# TODO: why would this ever be null?
if rattail_config:
app = rattail_config.get_app()
auth = app.get_auth_handler()
request.tailbone_cached_permissions = auth.get_permissions(
Session(), request.user)
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
if hasattr(request, 'client_addr'):
session.continuum_remote_addr = request.client_addr
# request.register_component()
def register_component(tagname, classname):
"""
Register a Vue 3 component, so the base template knows to

View file

@ -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: [],
}

View file

@ -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

View file

@ -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
}

View file

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

View file

@ -1,15 +1,15 @@
## -*- coding: utf-8; -*-
<% 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
},

View file

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

View file

@ -1,6 +1,6 @@
## -*- coding: utf-8; -*-
<%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()}

View file

@ -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>

View file

@ -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

View file

@ -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..."
}

View file

@ -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="&quot;Touch&quot; 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

View file

@ -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 = []

View file

@ -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')
}

View file

@ -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"

View file

@ -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>

View file

@ -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(),

View file

@ -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)

View file

@ -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:

View file

@ -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,

View file

@ -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
View file

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

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

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

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

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

View file

@ -3,14 +3,14 @@
import os
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
View file

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

12
tests/test_config.py Normal file
View file

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

58
tests/test_subscribers.py Normal file
View file

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

75
tests/util.py Normal file
View file

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

View file

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

View file

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

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

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

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

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