feat: improve page linkage between role/user/person
- show Users grid when viewing a Role - add hyperlinks between things
This commit is contained in:
parent
9d261de45a
commit
770c4612d5
|
@ -25,6 +25,7 @@ Base form classes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
import colander
|
import colander
|
||||||
import deform
|
import deform
|
||||||
|
@ -311,6 +312,9 @@ class Form:
|
||||||
|
|
||||||
self.set_fields(fields or self.get_fields())
|
self.set_fields(fields or self.get_fields())
|
||||||
|
|
||||||
|
# nb. this tracks grid JSON data for inclusion in page template
|
||||||
|
self.grid_vue_data = OrderedDict()
|
||||||
|
|
||||||
def __contains__(self, name):
|
def __contains__(self, name):
|
||||||
"""
|
"""
|
||||||
Custom logic for the ``in`` operator, to allow easily checking
|
Custom logic for the ``in`` operator, to allow easily checking
|
||||||
|
@ -750,6 +754,10 @@ class Form:
|
||||||
kwargs['appstruct'] = self.model_instance
|
kwargs['appstruct'] = self.model_instance
|
||||||
|
|
||||||
form = deform.Form(schema, **kwargs)
|
form = deform.Form(schema, **kwargs)
|
||||||
|
# nb. must give a reference back to wutta form; this is
|
||||||
|
# for sake of field schema nodes and widgets, e.g. to
|
||||||
|
# access the main model instance
|
||||||
|
form.wutta_form = self
|
||||||
self.deform_form = form
|
self.deform_form = form
|
||||||
|
|
||||||
return self.deform_form
|
return self.deform_form
|
||||||
|
@ -818,6 +826,17 @@ class Form:
|
||||||
output = render(template, context)
|
output = render(template, context)
|
||||||
return HTML.literal(output)
|
return HTML.literal(output)
|
||||||
|
|
||||||
|
def add_grid_vue_data(self, grid):
|
||||||
|
""" """
|
||||||
|
if not grid.key:
|
||||||
|
raise ValueError("grid must have a key!")
|
||||||
|
|
||||||
|
if grid.key in self.grid_vue_data:
|
||||||
|
log.warning("grid data with key '%s' already registered, "
|
||||||
|
"but will be replaced", grid.key)
|
||||||
|
|
||||||
|
self.grid_vue_data[grid.key] = grid.get_vue_data()
|
||||||
|
|
||||||
def render_vue_field(
|
def render_vue_field(
|
||||||
self,
|
self,
|
||||||
fieldname,
|
fieldname,
|
||||||
|
|
|
@ -246,6 +246,9 @@ class ObjectRef(colander.SchemaType):
|
||||||
values.insert(0, self.empty_option)
|
values.insert(0, self.empty_option)
|
||||||
kwargs['values'] = values
|
kwargs['values'] = values
|
||||||
|
|
||||||
|
if 'url' not in kwargs:
|
||||||
|
kwargs['url'] = lambda person: self.request.route_url('people.view', uuid=person.uuid)
|
||||||
|
|
||||||
return widgets.ObjectRefWidget(self.request, **kwargs)
|
return widgets.ObjectRefWidget(self.request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -321,6 +324,28 @@ class RoleRefs(WuttaSet):
|
||||||
return widgets.RoleRefsWidget(self.request, **kwargs)
|
return widgets.RoleRefsWidget(self.request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class UserRefs(WuttaSet):
|
||||||
|
"""
|
||||||
|
Form schema type for the Role
|
||||||
|
:attr:`~wuttjamaican:wuttjamaican.db.model.auth.Role.users`
|
||||||
|
association proxy field.
|
||||||
|
|
||||||
|
This is a subclass of :class:`WuttaSet`. It uses a ``set`` of
|
||||||
|
:class:`~wuttjamaican:wuttjamaican.db.model.auth.User` ``uuid``
|
||||||
|
values for underlying data format.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def widget_maker(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Constructs a default widget for the field.
|
||||||
|
|
||||||
|
:returns: Instance of
|
||||||
|
:class:`~wuttaweb.forms.widgets.UserRefsWidget`.
|
||||||
|
"""
|
||||||
|
kwargs.setdefault('session', self.session)
|
||||||
|
return widgets.UserRefsWidget(self.request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class Permissions(WuttaSet):
|
class Permissions(WuttaSet):
|
||||||
"""
|
"""
|
||||||
Form schema type for the Role
|
Form schema type for the Role
|
||||||
|
|
|
@ -44,6 +44,7 @@ from deform.widget import (Widget, TextInputWidget, TextAreaWidget,
|
||||||
from webhelpers2.html import HTML
|
from webhelpers2.html import HTML
|
||||||
|
|
||||||
from wuttaweb.db import Session
|
from wuttaweb.db import Session
|
||||||
|
from wuttaweb.grids import Grid
|
||||||
|
|
||||||
|
|
||||||
class ObjectRefWidget(SelectWidget):
|
class ObjectRefWidget(SelectWidget):
|
||||||
|
@ -83,9 +84,19 @@ class ObjectRefWidget(SelectWidget):
|
||||||
"""
|
"""
|
||||||
readonly_template = 'readonly/objectref'
|
readonly_template = 'readonly/objectref'
|
||||||
|
|
||||||
def __init__(self, request, *args, **kwargs):
|
def __init__(self, request, url=None, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.request = request
|
self.request = request
|
||||||
|
self.url = url
|
||||||
|
|
||||||
|
def get_template_values(self, field, cstruct, kw):
|
||||||
|
""" """
|
||||||
|
values = super().get_template_values(field, cstruct, kw)
|
||||||
|
|
||||||
|
if 'url' not in values and self.url and field.schema.model_instance:
|
||||||
|
values['url'] = self.url(field.schema.model_instance)
|
||||||
|
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
class NotesWidget(TextAreaWidget):
|
class NotesWidget(TextAreaWidget):
|
||||||
|
@ -137,12 +148,17 @@ class RoleRefsWidget(WuttaCheckboxChoiceWidget):
|
||||||
"""
|
"""
|
||||||
Widget for use with User
|
Widget for use with User
|
||||||
:attr:`~wuttjamaican:wuttjamaican.db.model.auth.User.roles` field.
|
:attr:`~wuttjamaican:wuttjamaican.db.model.auth.User.roles` field.
|
||||||
|
This is the default widget for the
|
||||||
|
:class:`~wuttaweb.forms.schema.RoleRefs` type.
|
||||||
|
|
||||||
This is a subclass of :class:`WuttaCheckboxChoiceWidget`.
|
This is a subclass of :class:`WuttaCheckboxChoiceWidget`.
|
||||||
"""
|
"""
|
||||||
|
readonly_template = 'readonly/rolerefs'
|
||||||
|
|
||||||
def serialize(self, field, cstruct, **kw):
|
def serialize(self, field, cstruct, **kw):
|
||||||
""" """
|
""" """
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
# special logic when field is editable
|
# special logic when field is editable
|
||||||
readonly = kw.get('readonly', self.readonly)
|
readonly = kw.get('readonly', self.readonly)
|
||||||
if not readonly:
|
if not readonly:
|
||||||
|
@ -159,10 +175,78 @@ class RoleRefsWidget(WuttaCheckboxChoiceWidget):
|
||||||
if val[0] != admin.uuid]
|
if val[0] != admin.uuid]
|
||||||
kw['values'] = values
|
kw['values'] = values
|
||||||
|
|
||||||
|
else: # readonly
|
||||||
|
|
||||||
|
# roles
|
||||||
|
roles = []
|
||||||
|
if cstruct:
|
||||||
|
for uuid in cstruct:
|
||||||
|
role = self.session.query(model.Role).get(uuid)
|
||||||
|
if role:
|
||||||
|
roles.append(role)
|
||||||
|
kw['roles'] = roles
|
||||||
|
|
||||||
|
# url
|
||||||
|
url = lambda role: self.request.route_url('roles.view', uuid=role.uuid)
|
||||||
|
kw['url'] = url
|
||||||
|
|
||||||
# default logic from here
|
# default logic from here
|
||||||
return super().serialize(field, cstruct, **kw)
|
return super().serialize(field, cstruct, **kw)
|
||||||
|
|
||||||
|
|
||||||
|
class UserRefsWidget(WuttaCheckboxChoiceWidget):
|
||||||
|
"""
|
||||||
|
Widget for use with Role
|
||||||
|
:attr:`~wuttjamaican:wuttjamaican.db.model.auth.Role.users` field.
|
||||||
|
This is the default widget for the
|
||||||
|
:class:`~wuttaweb.forms.schema.UserRefs` type.
|
||||||
|
|
||||||
|
This is a subclass of :class:`WuttaCheckboxChoiceWidget`; however
|
||||||
|
it only supports readonly mode and does not use a template.
|
||||||
|
Rather, it generates and renders a
|
||||||
|
:class:`~wuttaweb.grids.base.Grid` showing the users list.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def serialize(self, field, cstruct, **kw):
|
||||||
|
""" """
|
||||||
|
readonly = kw.get('readonly', self.readonly)
|
||||||
|
if not readonly:
|
||||||
|
raise NotImplementedError("edit not allowed for this widget")
|
||||||
|
|
||||||
|
model = self.app.model
|
||||||
|
columns = ['person', 'username', 'active']
|
||||||
|
|
||||||
|
# generate data set for users
|
||||||
|
users = []
|
||||||
|
if cstruct:
|
||||||
|
for uuid in cstruct:
|
||||||
|
user = self.session.query(model.User).get(uuid)
|
||||||
|
if user:
|
||||||
|
users.append(dict([(key, getattr(user, key))
|
||||||
|
for key in columns + ['uuid']]))
|
||||||
|
|
||||||
|
# grid
|
||||||
|
grid = Grid(self.request, key='roles.view.users',
|
||||||
|
columns=columns, data=users)
|
||||||
|
|
||||||
|
# view action
|
||||||
|
if self.request.has_perm('users.view'):
|
||||||
|
url = lambda user, i: self.request.route_url('users.view', uuid=user['uuid'])
|
||||||
|
grid.add_action('view', icon='eye', url=url)
|
||||||
|
grid.set_link('person')
|
||||||
|
grid.set_link('username')
|
||||||
|
|
||||||
|
# edit action
|
||||||
|
if self.request.has_perm('users.edit'):
|
||||||
|
url = lambda user, i: self.request.route_url('users.edit', uuid=user['uuid'])
|
||||||
|
grid.add_action('edit', url=url)
|
||||||
|
|
||||||
|
# render as simple <b-table>
|
||||||
|
# nb. must indicate we are a part of this form
|
||||||
|
form = getattr(field.parent, 'wutta_form', None)
|
||||||
|
return grid.render_table_element(form)
|
||||||
|
|
||||||
|
|
||||||
class PermissionsWidget(WuttaCheckboxChoiceWidget):
|
class PermissionsWidget(WuttaCheckboxChoiceWidget):
|
||||||
"""
|
"""
|
||||||
Widget for use with Role
|
Widget for use with Role
|
||||||
|
|
|
@ -543,6 +543,13 @@ class Grid:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def add_action(self, key, **kwargs):
|
||||||
|
"""
|
||||||
|
Convenience to add a new :class:`GridAction` instance to the
|
||||||
|
grid's :attr:`actions` list.
|
||||||
|
"""
|
||||||
|
self.actions.append(GridAction(self.request, key, **kwargs))
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
# sorting methods
|
# sorting methods
|
||||||
##############################
|
##############################
|
||||||
|
@ -1251,6 +1258,9 @@ class Grid:
|
||||||
"""
|
"""
|
||||||
Render the Vue template block for the grid.
|
Render the Vue template block for the grid.
|
||||||
|
|
||||||
|
This is what you want for a "full-featured" grid which will
|
||||||
|
exist as its own unique Vue component on the frontend.
|
||||||
|
|
||||||
This returns something like:
|
This returns something like:
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
@ -1261,12 +1271,21 @@ class Grid:
|
||||||
</b-table>
|
</b-table>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
WuttaGridData = {}
|
||||||
|
WuttaGrid = {
|
||||||
|
template: 'wutta-grid-template',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
.. todo::
|
.. todo::
|
||||||
|
|
||||||
Why can't Sphinx render the above code block as 'html' ?
|
Why can't Sphinx render the above code block as 'html' ?
|
||||||
|
|
||||||
It acts like it can't handle a ``<script>`` tag at all?
|
It acts like it can't handle a ``<script>`` tag at all?
|
||||||
|
|
||||||
|
See :meth:`render_table_element()` for a simpler variant.
|
||||||
|
|
||||||
Actual output will of course depend on grid attributes,
|
Actual output will of course depend on grid attributes,
|
||||||
:attr:`vue_tagname` and :attr:`columns` etc.
|
:attr:`vue_tagname` and :attr:`columns` etc.
|
||||||
|
|
||||||
|
@ -1278,6 +1297,58 @@ class Grid:
|
||||||
output = render(template, context)
|
output = render(template, context)
|
||||||
return HTML.literal(output)
|
return HTML.literal(output)
|
||||||
|
|
||||||
|
def render_table_element(
|
||||||
|
self,
|
||||||
|
form=None,
|
||||||
|
template='/grids/element.mako',
|
||||||
|
**context):
|
||||||
|
"""
|
||||||
|
Render a simple Vue table element for the grid.
|
||||||
|
|
||||||
|
This is what you want for a "simple" grid which does require a
|
||||||
|
unique Vue component, but can instead use the standard table
|
||||||
|
component.
|
||||||
|
|
||||||
|
This returns something like:
|
||||||
|
|
||||||
|
.. code-block:: html
|
||||||
|
|
||||||
|
<b-table :data="gridData['mykey']">
|
||||||
|
<!-- columns etc. -->
|
||||||
|
</b-table>
|
||||||
|
|
||||||
|
See :meth:`render_vue_template()` for a more complete variant.
|
||||||
|
|
||||||
|
Actual output will of course depend on grid attributes,
|
||||||
|
:attr:`key`, :attr:`columns` etc.
|
||||||
|
|
||||||
|
:param form: Reference to the
|
||||||
|
:class:`~wuttaweb.forms.base.Form` instance which
|
||||||
|
"contains" this grid. This is needed in order to ensure
|
||||||
|
the grid data is available to the form Vue component.
|
||||||
|
|
||||||
|
:param template: Path to Mako template which is used to render
|
||||||
|
the output.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The above example shows ``gridData['mykey']`` as the Vue
|
||||||
|
data reference. This should "just work" if you provide the
|
||||||
|
correct ``form`` arg and the grid is contained directly by
|
||||||
|
that form's Vue component.
|
||||||
|
|
||||||
|
However, this may not account for all use cases. For now
|
||||||
|
we wait and see what comes up, but know the dust may not
|
||||||
|
yet be settled here.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# nb. must register data for inclusion on page template
|
||||||
|
if form:
|
||||||
|
form.add_grid_vue_data(self)
|
||||||
|
|
||||||
|
# otherwise logic is the same, just different template
|
||||||
|
return self.render_vue_template(template=template, **context)
|
||||||
|
|
||||||
def render_vue_finalize(self):
|
def render_vue_finalize(self):
|
||||||
"""
|
"""
|
||||||
Render the Vue "finalize" script for the grid.
|
Render the Vue "finalize" script for the grid.
|
||||||
|
|
|
@ -501,7 +501,7 @@
|
||||||
label="Delete This" />
|
label="Delete This" />
|
||||||
% endif
|
% endif
|
||||||
% elif master.editing:
|
% elif master.editing:
|
||||||
% if instance_viewable and master.has_perm('view'):
|
% if master.has_perm('view'):
|
||||||
<wutta-button once
|
<wutta-button once
|
||||||
tag="a" href="${master.get_action_url('view', instance)}"
|
tag="a" href="${master.get_action_url('view', instance)}"
|
||||||
icon-left="eye"
|
icon-left="eye"
|
||||||
|
@ -514,7 +514,7 @@
|
||||||
label="Delete This" />
|
label="Delete This" />
|
||||||
% endif
|
% endif
|
||||||
% elif master.deleting:
|
% elif master.deleting:
|
||||||
% if instance_viewable and master.has_perm('view'):
|
% if master.has_perm('view'):
|
||||||
<wutta-button once
|
<wutta-button once
|
||||||
tag="a" href="${master.get_action_url('view', instance)}"
|
tag="a" href="${master.get_action_url('view', instance)}"
|
||||||
icon-left="eye"
|
icon-left="eye"
|
||||||
|
|
|
@ -1 +1,9 @@
|
||||||
<span>${str(field.schema.model_instance or '')}</span>
|
<tal:omit tal:define="url url|None;">
|
||||||
|
<a tal:condition="url"
|
||||||
|
href="${url}">
|
||||||
|
${str(field.schema.model_instance or '')}
|
||||||
|
</a>
|
||||||
|
<span tal:condition="not url">
|
||||||
|
${str(field.schema.model_instance or '')}
|
||||||
|
</span>
|
||||||
|
</tal:omit>
|
||||||
|
|
7
src/wuttaweb/templates/deform/readonly/rolerefs.pt
Normal file
7
src/wuttaweb/templates/deform/readonly/rolerefs.pt
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<ul class="list-group">
|
||||||
|
<tal:loop tal:repeat="role roles">
|
||||||
|
<li class="list-group-item">
|
||||||
|
<a href="${url(role)}">${role}</a>
|
||||||
|
</li>
|
||||||
|
</tal:loop>
|
||||||
|
</ul>
|
|
@ -68,6 +68,14 @@
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
|
% if form.grid_vue_data:
|
||||||
|
gridData: {
|
||||||
|
% for key, data in form.grid_vue_data.items():
|
||||||
|
'${key}': ${json.dumps(data)|n},
|
||||||
|
% endfor
|
||||||
|
},
|
||||||
|
% endif
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
49
src/wuttaweb/templates/grids/element.mako
Normal file
49
src/wuttaweb/templates/grids/element.mako
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
## -*- coding: utf-8; -*-
|
||||||
|
<${b}-table :data="gridData['${grid.key}']">
|
||||||
|
|
||||||
|
% for column in grid.get_vue_columns():
|
||||||
|
<${b}-table-column field="${column['field']}"
|
||||||
|
label="${column['label']}"
|
||||||
|
v-slot="props"
|
||||||
|
:sortable="${json.dumps(column.get('sortable', False))|n}"
|
||||||
|
cell-class="c_${column['field']}">
|
||||||
|
% if grid.is_linked(column['field']):
|
||||||
|
<a :href="props.row._action_url_view"
|
||||||
|
v-html="props.row.${column['field']}" />
|
||||||
|
% else:
|
||||||
|
<span v-html="props.row.${column['field']}"></span>
|
||||||
|
% endif
|
||||||
|
</${b}-table-column>
|
||||||
|
% endfor
|
||||||
|
|
||||||
|
% if grid.actions:
|
||||||
|
<${b}-table-column field="actions"
|
||||||
|
label="Actions"
|
||||||
|
v-slot="props">
|
||||||
|
% for action in grid.actions:
|
||||||
|
<a v-if="props.row._action_url_${action.key}"
|
||||||
|
:href="props.row._action_url_${action.key}"
|
||||||
|
class="${action.link_class}">
|
||||||
|
${action.render_icon_and_label()}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
% endfor
|
||||||
|
</${b}-table-column>
|
||||||
|
% endif
|
||||||
|
|
||||||
|
<template #empty>
|
||||||
|
<section class="section">
|
||||||
|
<div class="content has-text-grey has-text-centered">
|
||||||
|
<p>
|
||||||
|
<b-icon
|
||||||
|
pack="fas"
|
||||||
|
icon="sad-tear"
|
||||||
|
size="is-large">
|
||||||
|
</b-icon>
|
||||||
|
</p>
|
||||||
|
<p>Nothing here.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</${b}-table>
|
|
@ -28,6 +28,7 @@ import sqlalchemy as sa
|
||||||
|
|
||||||
from wuttjamaican.db.model import Person
|
from wuttjamaican.db.model import Person
|
||||||
from wuttaweb.views import MasterView
|
from wuttaweb.views import MasterView
|
||||||
|
from wuttaweb.forms.schema import UserRefs
|
||||||
|
|
||||||
|
|
||||||
class PersonView(MasterView):
|
class PersonView(MasterView):
|
||||||
|
@ -70,23 +71,25 @@ class PersonView(MasterView):
|
||||||
# last_name
|
# last_name
|
||||||
g.set_link('last_name')
|
g.set_link('last_name')
|
||||||
|
|
||||||
# TODO: master should handle this?
|
|
||||||
def configure_form(self, f):
|
def configure_form(self, f):
|
||||||
""" """
|
""" """
|
||||||
super().configure_form(f)
|
super().configure_form(f)
|
||||||
|
person = f.model_instance
|
||||||
|
|
||||||
# first_name
|
# TODO: master should handle these? (nullable column)
|
||||||
f.set_required('first_name', False)
|
f.set_required('first_name', False)
|
||||||
|
|
||||||
# middle_name
|
|
||||||
f.set_required('middle_name', False)
|
f.set_required('middle_name', False)
|
||||||
|
|
||||||
# last_name
|
|
||||||
f.set_required('last_name', False)
|
f.set_required('last_name', False)
|
||||||
|
|
||||||
# users
|
# users
|
||||||
if 'users' in f:
|
# nb. colanderalchemy wants to do some magic for the true
|
||||||
f.fields.remove('users')
|
# 'users' relationship, so we use a different field name
|
||||||
|
f.remove('users')
|
||||||
|
if not (self.creating or self.editing):
|
||||||
|
f.append('_users')
|
||||||
|
f.set_readonly('_users')
|
||||||
|
f.set_node('_users', UserRefs(self.request))
|
||||||
|
f.set_default('_users', [u.uuid for u in person.users])
|
||||||
|
|
||||||
def autocomplete_query(self, term):
|
def autocomplete_query(self, term):
|
||||||
""" """
|
""" """
|
||||||
|
|
|
@ -28,7 +28,7 @@ from wuttjamaican.db.model import Role
|
||||||
from wuttaweb.views import MasterView
|
from wuttaweb.views import MasterView
|
||||||
from wuttaweb.db import Session
|
from wuttaweb.db import Session
|
||||||
from wuttaweb.forms import widgets
|
from wuttaweb.forms import widgets
|
||||||
from wuttaweb.forms.schema import Permissions
|
from wuttaweb.forms.schema import UserRefs, Permissions
|
||||||
|
|
||||||
|
|
||||||
class RoleView(MasterView):
|
class RoleView(MasterView):
|
||||||
|
@ -115,6 +115,13 @@ class RoleView(MasterView):
|
||||||
# notes
|
# notes
|
||||||
f.set_widget('notes', widgets.NotesWidget())
|
f.set_widget('notes', widgets.NotesWidget())
|
||||||
|
|
||||||
|
# users
|
||||||
|
if not (self.creating or self.editing):
|
||||||
|
f.append('users')
|
||||||
|
f.set_readonly('users')
|
||||||
|
f.set_node('users', UserRefs(self.request))
|
||||||
|
f.set_default('users', [u.uuid for u in role.users])
|
||||||
|
|
||||||
# permissions
|
# permissions
|
||||||
f.append('permissions')
|
f.append('permissions')
|
||||||
self.wutta_permissions = self.get_available_permissions()
|
self.wutta_permissions = self.get_available_permissions()
|
||||||
|
|
|
@ -10,6 +10,7 @@ from pyramid import testing
|
||||||
from wuttjamaican.conf import WuttaConfig
|
from wuttjamaican.conf import WuttaConfig
|
||||||
from wuttaweb.forms import base, widgets
|
from wuttaweb.forms import base, widgets
|
||||||
from wuttaweb import helpers
|
from wuttaweb import helpers
|
||||||
|
from wuttaweb.grids import Grid
|
||||||
|
|
||||||
|
|
||||||
class TestForm(TestCase):
|
class TestForm(TestCase):
|
||||||
|
@ -405,6 +406,29 @@ class TestForm(TestCase):
|
||||||
self.assertIn('<script type="text/x-template" id="wutta-form-template">', html)
|
self.assertIn('<script type="text/x-template" id="wutta-form-template">', html)
|
||||||
self.assertNotIn('@submit', html)
|
self.assertNotIn('@submit', html)
|
||||||
|
|
||||||
|
def test_add_grid_vue_data(self):
|
||||||
|
form = self.make_form()
|
||||||
|
|
||||||
|
# grid must have key
|
||||||
|
grid = Grid(self.request)
|
||||||
|
self.assertRaises(ValueError, form.add_grid_vue_data, grid)
|
||||||
|
|
||||||
|
# otherwise it works
|
||||||
|
grid = Grid(self.request, key='foo')
|
||||||
|
self.assertEqual(len(form.grid_vue_data), 0)
|
||||||
|
form.add_grid_vue_data(grid)
|
||||||
|
self.assertEqual(len(form.grid_vue_data), 1)
|
||||||
|
self.assertIn('foo', form.grid_vue_data)
|
||||||
|
self.assertEqual(form.grid_vue_data['foo'], [])
|
||||||
|
|
||||||
|
# calling again with same key will replace data
|
||||||
|
records = [{'foo': 1}, {'foo': 2}]
|
||||||
|
grid = Grid(self.request, key='foo', columns=['foo'], data=records)
|
||||||
|
form.add_grid_vue_data(grid)
|
||||||
|
self.assertEqual(len(form.grid_vue_data), 1)
|
||||||
|
self.assertIn('foo', form.grid_vue_data)
|
||||||
|
self.assertEqual(form.grid_vue_data['foo'], records)
|
||||||
|
|
||||||
def test_render_vue_finalize(self):
|
def test_render_vue_finalize(self):
|
||||||
form = self.make_form()
|
form = self.make_form()
|
||||||
html = form.render_vue_finalize()
|
html = form.render_vue_finalize()
|
||||||
|
|
|
@ -9,6 +9,7 @@ from sqlalchemy import orm
|
||||||
|
|
||||||
from wuttjamaican.conf import WuttaConfig
|
from wuttjamaican.conf import WuttaConfig
|
||||||
from wuttaweb.forms import schema as mod
|
from wuttaweb.forms import schema as mod
|
||||||
|
from wuttaweb.forms import widgets
|
||||||
from tests.util import DataTestCase
|
from tests.util import DataTestCase
|
||||||
|
|
||||||
|
|
||||||
|
@ -200,6 +201,19 @@ class TestPersonRef(DataTestCase):
|
||||||
self.assertIsNot(sorted_query, query)
|
self.assertIsNot(sorted_query, query)
|
||||||
|
|
||||||
|
|
||||||
|
class TestUserRefs(DataTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.setup_db()
|
||||||
|
self.request = testing.DummyRequest(wutta_config=self.config)
|
||||||
|
|
||||||
|
def test_widget_maker(self):
|
||||||
|
model = self.app.model
|
||||||
|
typ = mod.UserRefs(self.request, session=self.session)
|
||||||
|
widget = typ.widget_maker()
|
||||||
|
self.assertIsInstance(widget, widgets.UserRefsWidget)
|
||||||
|
|
||||||
|
|
||||||
class TestRoleRefs(DataTestCase):
|
class TestRoleRefs(DataTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
# -*- coding: utf-8; -*-
|
# -*- coding: utf-8; -*-
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import colander
|
import colander
|
||||||
import deform
|
import deform
|
||||||
from pyramid import testing
|
from pyramid import testing
|
||||||
|
|
||||||
from wuttaweb.forms import widgets as mod
|
from wuttaweb.forms import widgets as mod
|
||||||
from wuttaweb.forms.schema import PersonRef, RoleRefs, Permissions
|
from wuttaweb.forms.schema import PersonRef, RoleRefs, UserRefs, Permissions
|
||||||
from tests.util import WebTestCase
|
from tests.util import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
|
@ -36,7 +38,18 @@ class TestObjectRefWidget(WebTestCase):
|
||||||
widget = mod.ObjectRefWidget(self.request)
|
widget = mod.ObjectRefWidget(self.request)
|
||||||
field = self.make_field(node)
|
field = self.make_field(node)
|
||||||
html = widget.serialize(field, person.uuid, readonly=True)
|
html = widget.serialize(field, person.uuid, readonly=True)
|
||||||
self.assertEqual(html.strip(), '<span>Betty Boop</span>')
|
self.assertIn('Betty Boop', html)
|
||||||
|
self.assertNotIn('<a', html)
|
||||||
|
|
||||||
|
# with hyperlink
|
||||||
|
node = colander.SchemaNode(PersonRef(self.request, session=self.session))
|
||||||
|
node.model_instance = person
|
||||||
|
widget = mod.ObjectRefWidget(self.request, url=lambda p: '/foo')
|
||||||
|
field = self.make_field(node)
|
||||||
|
html = widget.serialize(field, person.uuid, readonly=True)
|
||||||
|
self.assertIn('Betty Boop', html)
|
||||||
|
self.assertIn('<a', html)
|
||||||
|
self.assertIn('href="/foo"', html)
|
||||||
|
|
||||||
|
|
||||||
class TestRoleRefsWidget(WebTestCase):
|
class TestRoleRefsWidget(WebTestCase):
|
||||||
|
@ -48,6 +61,7 @@ class TestRoleRefsWidget(WebTestCase):
|
||||||
return deform.Field(node, **kwargs)
|
return deform.Field(node, **kwargs)
|
||||||
|
|
||||||
def test_serialize(self):
|
def test_serialize(self):
|
||||||
|
self.pyramid_config.add_route('roles.view', '/roles/{uuid}')
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
auth = self.app.get_auth_handler()
|
auth = self.app.get_auth_handler()
|
||||||
admin = auth.get_role_administrator(self.session)
|
admin = auth.get_role_administrator(self.session)
|
||||||
|
@ -77,6 +91,49 @@ class TestRoleRefsWidget(WebTestCase):
|
||||||
self.assertIn(blokes.uuid, html)
|
self.assertIn(blokes.uuid, html)
|
||||||
|
|
||||||
|
|
||||||
|
class TestUserRefsWidget(WebTestCase):
|
||||||
|
|
||||||
|
def make_field(self, node, **kwargs):
|
||||||
|
# TODO: not sure why default renderer is in use even though
|
||||||
|
# pyramid_deform was included in setup? but this works..
|
||||||
|
kwargs.setdefault('renderer', deform.Form.default_renderer)
|
||||||
|
return deform.Field(node, **kwargs)
|
||||||
|
|
||||||
|
def test_serialize(self):
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
|
# nb. we let the field construct the widget via our type
|
||||||
|
node = colander.SchemaNode(UserRefs(self.request, session=self.session))
|
||||||
|
field = self.make_field(node)
|
||||||
|
widget = field.widget
|
||||||
|
|
||||||
|
# readonly is required
|
||||||
|
self.assertRaises(NotImplementedError, widget.serialize, field, set())
|
||||||
|
self.assertRaises(NotImplementedError, widget.serialize, field, set(), readonly=False)
|
||||||
|
|
||||||
|
# empty
|
||||||
|
html = widget.serialize(field, set(), readonly=True)
|
||||||
|
self.assertIn('<b-table ', html)
|
||||||
|
|
||||||
|
# with data, no actions
|
||||||
|
user = model.User(username='barney')
|
||||||
|
self.session.add(user)
|
||||||
|
self.session.commit()
|
||||||
|
html = widget.serialize(field, {user.uuid}, readonly=True)
|
||||||
|
self.assertIn('<b-table ', html)
|
||||||
|
self.assertNotIn('Actions', html)
|
||||||
|
self.assertNotIn('View', html)
|
||||||
|
self.assertNotIn('Edit', html)
|
||||||
|
|
||||||
|
# with view/edit actions
|
||||||
|
with patch.object(self.request, 'is_root', new=True):
|
||||||
|
html = widget.serialize(field, {user.uuid}, readonly=True)
|
||||||
|
self.assertIn('<b-table ', html)
|
||||||
|
self.assertIn('Actions', html)
|
||||||
|
self.assertIn('View', html)
|
||||||
|
self.assertIn('Edit', html)
|
||||||
|
|
||||||
|
|
||||||
class TestPermissionsWidget(WebTestCase):
|
class TestPermissionsWidget(WebTestCase):
|
||||||
|
|
||||||
def make_field(self, node, **kwargs):
|
def make_field(self, node, **kwargs):
|
||||||
|
|
|
@ -10,7 +10,8 @@ from pyramid import testing
|
||||||
|
|
||||||
from wuttjamaican.conf import WuttaConfig
|
from wuttjamaican.conf import WuttaConfig
|
||||||
from wuttaweb.grids import base as mod
|
from wuttaweb.grids import base as mod
|
||||||
from wuttaweb.forms import FieldList
|
from wuttaweb.util import FieldList
|
||||||
|
from wuttaweb.forms import Form
|
||||||
from tests.util import WebTestCase
|
from tests.util import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
|
@ -186,6 +187,14 @@ class TestGrid(WebTestCase):
|
||||||
self.assertFalse(grid.is_linked('foo'))
|
self.assertFalse(grid.is_linked('foo'))
|
||||||
self.assertTrue(grid.is_linked('bar'))
|
self.assertTrue(grid.is_linked('bar'))
|
||||||
|
|
||||||
|
def test_add_action(self):
|
||||||
|
grid = self.make_grid()
|
||||||
|
self.assertEqual(len(grid.actions), 0)
|
||||||
|
|
||||||
|
grid.add_action('view')
|
||||||
|
self.assertEqual(len(grid.actions), 1)
|
||||||
|
self.assertIsInstance(grid.actions[0], mod.GridAction)
|
||||||
|
|
||||||
def test_get_pagesize_options(self):
|
def test_get_pagesize_options(self):
|
||||||
grid = self.make_grid()
|
grid = self.make_grid()
|
||||||
|
|
||||||
|
@ -855,6 +864,25 @@ class TestGrid(WebTestCase):
|
||||||
html = grid.render_vue_template()
|
html = grid.render_vue_template()
|
||||||
self.assertIn('<script type="text/x-template" id="wutta-grid-template">', html)
|
self.assertIn('<script type="text/x-template" id="wutta-grid-template">', html)
|
||||||
|
|
||||||
|
def test_render_table_element(self):
|
||||||
|
self.pyramid_config.include('pyramid_mako')
|
||||||
|
self.pyramid_config.add_subscriber('wuttaweb.subscribers.before_render',
|
||||||
|
'pyramid.events.BeforeRender')
|
||||||
|
|
||||||
|
grid = self.make_grid(key='foobar', columns=['foo', 'bar'])
|
||||||
|
|
||||||
|
# form not required
|
||||||
|
html = grid.render_table_element()
|
||||||
|
self.assertNotIn('<script ', html)
|
||||||
|
self.assertIn('<b-table ', html)
|
||||||
|
|
||||||
|
# form will register grid data
|
||||||
|
form = Form(self.request)
|
||||||
|
self.assertEqual(len(form.grid_vue_data), 0)
|
||||||
|
html = grid.render_table_element(form)
|
||||||
|
self.assertEqual(len(form.grid_vue_data), 1)
|
||||||
|
self.assertIn('foobar', form.grid_vue_data)
|
||||||
|
|
||||||
def test_render_vue_finalize(self):
|
def test_render_vue_finalize(self):
|
||||||
grid = self.make_grid()
|
grid = self.make_grid()
|
||||||
html = grid.render_vue_finalize()
|
html = grid.render_vue_finalize()
|
||||||
|
|
|
@ -35,12 +35,27 @@ class TestPersonView(WebTestCase):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
form = view.make_form(model_class=model.Person)
|
form = view.make_form(model_class=model.Person)
|
||||||
|
|
||||||
|
# required fields
|
||||||
|
with patch.object(view, 'creating', new=True):
|
||||||
form.set_fields(form.get_model_fields())
|
form.set_fields(form.get_model_fields())
|
||||||
self.assertEqual(form.required_fields, {})
|
self.assertEqual(form.required_fields, {})
|
||||||
view.configure_form(form)
|
view.configure_form(form)
|
||||||
self.assertTrue(form.required_fields)
|
self.assertTrue(form.required_fields)
|
||||||
self.assertFalse(form.required_fields['middle_name'])
|
self.assertFalse(form.required_fields['middle_name'])
|
||||||
|
|
||||||
|
person = model.Person(full_name="Barney Rubble")
|
||||||
|
user = model.User(username='barney', person=person)
|
||||||
|
self.session.add(user)
|
||||||
|
self.session.commit()
|
||||||
|
|
||||||
|
# users field
|
||||||
|
with patch.object(view, 'viewing', new=True):
|
||||||
|
form = view.make_form(model_instance=person)
|
||||||
|
self.assertEqual(form.defaults, {})
|
||||||
|
view.configure_form(form)
|
||||||
|
self.assertIn('_users', form.defaults)
|
||||||
|
|
||||||
def test_autocomplete_query(self):
|
def test_autocomplete_query(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue