Compare commits
No commits in common. "8a09fb1a3ca59b60db74f8edd6aa91d5f06dd349" and "e3c0a8d99e0b51bcc366ddf0b2dceb30e9c2b049" have entirely different histories.
8a09fb1a3c
...
e3c0a8d99e
8 changed files with 6 additions and 488 deletions
18
CHANGELOG.md
18
CHANGELOG.md
|
@ -5,24 +5,6 @@ All notable changes to wuttaweb will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## v0.23.0 (2025-08-09)
|
|
||||||
|
|
||||||
### Feat
|
|
||||||
|
|
||||||
- add tools to manage user API tokens
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
- add default sorter, tools for basic table-element grid
|
|
||||||
- add custom password+confirmation widget for Vue3 + Oruga
|
|
||||||
- fix butterfly wrapper for b-notification component
|
|
||||||
- add butterfly wrapper for b-timepicker component
|
|
||||||
- style tweaks for butterfly/oruga; mostly expand fields
|
|
||||||
- fix b-datepicker component wrapper per oruga 0.9.0
|
|
||||||
- fix b-button component wrapper per oruga 0.9.0
|
|
||||||
- update butterfly component for b-autocomplete, per oruga 0.11.4
|
|
||||||
- update default versions for Vue3 + Oruga + FontAwesome
|
|
||||||
|
|
||||||
## v0.22.0 (2025-06-29)
|
## v0.22.0 (2025-06-29)
|
||||||
|
|
||||||
### Feat
|
### Feat
|
||||||
|
|
|
@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "WuttaWeb"
|
name = "WuttaWeb"
|
||||||
version = "0.23.0"
|
version = "0.22.0"
|
||||||
description = "Web App for Wutta Framework"
|
description = "Web App for Wutta Framework"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
|
authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
|
||||||
|
@ -44,7 +44,7 @@ dependencies = [
|
||||||
"pyramid_tm",
|
"pyramid_tm",
|
||||||
"waitress",
|
"waitress",
|
||||||
"WebHelpers2",
|
"WebHelpers2",
|
||||||
"WuttJamaican[db]>=0.22.0",
|
"WuttJamaican[db]>=0.20.6",
|
||||||
"zope.sqlalchemy>=1.5",
|
"zope.sqlalchemy>=1.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -2041,9 +2041,9 @@ class Grid:
|
||||||
"""
|
"""
|
||||||
Render a simple Vue table element for the grid.
|
Render a simple Vue table element for the grid.
|
||||||
|
|
||||||
This is what you want for a "simple" grid which does not
|
This is what you want for a "simple" grid which does require a
|
||||||
require a unique Vue component, but can instead use the
|
unique Vue component, but can instead use the standard table
|
||||||
standard table component.
|
component.
|
||||||
|
|
||||||
This returns something like:
|
This returns something like:
|
||||||
|
|
||||||
|
@ -2227,35 +2227,6 @@ class Grid:
|
||||||
'order': sorter['dir']})
|
'order': sorter['dir']})
|
||||||
return sorters
|
return sorters
|
||||||
|
|
||||||
def get_vue_first_sorter(self):
|
|
||||||
"""
|
|
||||||
Returns the first active sorter, if applicable.
|
|
||||||
|
|
||||||
This method is used to declare the initial sort for a simple
|
|
||||||
table component, i.e. for use with the ``table-element.mako``
|
|
||||||
template. It generally is assumed that frontend sorting is in
|
|
||||||
use, as opposed to backend sorting, although it should work
|
|
||||||
for either scenario.
|
|
||||||
|
|
||||||
This checks :attr:`active_sorters` and if set, will use the
|
|
||||||
first sorter from that. Note that ``active_sorters`` will
|
|
||||||
*not* be set unless :meth:`load_settings()` has been called.
|
|
||||||
|
|
||||||
Otherwise this will use the first sorter from
|
|
||||||
:attr:`sort_defaults` which is defined in constructor.
|
|
||||||
|
|
||||||
:returns: The first sorter in format ``[sortkey, sortdir]``,
|
|
||||||
or ``None``.
|
|
||||||
"""
|
|
||||||
if hasattr(self, 'active_sorters'):
|
|
||||||
if self.active_sorters:
|
|
||||||
sorter = self.active_sorters[0]
|
|
||||||
return [sorter['key'], sorter['dir']]
|
|
||||||
|
|
||||||
elif self.sort_defaults:
|
|
||||||
sorter = self.sort_defaults[0]
|
|
||||||
return [sorter.sortkey, sorter.sortdir]
|
|
||||||
|
|
||||||
def get_vue_filters(self):
|
def get_vue_filters(self):
|
||||||
"""
|
"""
|
||||||
Returns a list of Vue-compatible filter definitions.
|
Returns a list of Vue-compatible filter definitions.
|
||||||
|
|
|
@ -1,22 +1,5 @@
|
||||||
## -*- coding: utf-8; -*-
|
## -*- coding: utf-8; -*-
|
||||||
<div>
|
<${b}-table :data="gridContext['${grid.key}'].data">
|
||||||
|
|
||||||
% if grid.tools:
|
|
||||||
<div class="table-tools-wrapper">
|
|
||||||
% for html in grid.tools.values():
|
|
||||||
${html}
|
|
||||||
% endfor
|
|
||||||
</div>
|
|
||||||
% endif
|
|
||||||
|
|
||||||
<${b}-table :data="gridContext['${grid.key}'].data"
|
|
||||||
|
|
||||||
## sorting
|
|
||||||
% if grid.sortable:
|
|
||||||
:default-sort="${grid.get_vue_first_sorter() or 'null'}"
|
|
||||||
% endif
|
|
||||||
|
|
||||||
icon-pack="fas">
|
|
||||||
|
|
||||||
% for column in grid.get_vue_columns():
|
% for column in grid.get_vue_columns():
|
||||||
% if not column['hidden']:
|
% if not column['hidden']:
|
||||||
|
@ -69,5 +52,3 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
</${b}-table>
|
</${b}-table>
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
|
@ -1,126 +0,0 @@
|
||||||
## -*- coding: utf-8; -*-
|
|
||||||
<%inherit file="/master/view.mako" />
|
|
||||||
|
|
||||||
<%def name="page_content()">
|
|
||||||
${parent.page_content()}
|
|
||||||
|
|
||||||
% if master.has_perm('manage_api_tokens'):
|
|
||||||
<b-modal :active.sync="newTokenShowDialog"
|
|
||||||
has-modal-card>
|
|
||||||
<div class="modal-card">
|
|
||||||
<header class="modal-card-head">
|
|
||||||
<p class="modal-card-title">
|
|
||||||
New API Token
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
<section class="modal-card-body">
|
|
||||||
|
|
||||||
<div v-if="!newTokenSaved">
|
|
||||||
<b-field label="Description"
|
|
||||||
:type="{'is-danger': !newTokenDescription}">
|
|
||||||
<b-input v-model.trim="newTokenDescription"
|
|
||||||
expanded
|
|
||||||
ref="newTokenDescription">
|
|
||||||
</b-input>
|
|
||||||
</b-field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="newTokenSaved">
|
|
||||||
<p class="block">
|
|
||||||
Your new API token is shown below.
|
|
||||||
</p>
|
|
||||||
<p class="block">
|
|
||||||
IMPORTANT: You must record this token elsewhere
|
|
||||||
for later reference. You will NOT be able to
|
|
||||||
recover the value if you lose it.
|
|
||||||
</p>
|
|
||||||
<b-field horizontal label="API Token">
|
|
||||||
{{ newTokenRaw }}
|
|
||||||
</b-field>
|
|
||||||
<b-field horizontal label="Description">
|
|
||||||
{{ newTokenDescription }}
|
|
||||||
</b-field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</section>
|
|
||||||
<footer class="modal-card-foot">
|
|
||||||
<b-button @click="newTokenShowDialog = false">
|
|
||||||
{{ newTokenSaved ? "Close" : "Cancel" }}
|
|
||||||
</b-button>
|
|
||||||
<b-button v-if="!newTokenSaved"
|
|
||||||
type="is-primary"
|
|
||||||
icon-pack="fas"
|
|
||||||
icon-left="save"
|
|
||||||
@click="newTokenSave()"
|
|
||||||
:disabled="!newTokenDescription || newTokenSaving">
|
|
||||||
{{ newTokenSaving ? "Working, please wait..." : "Save" }}
|
|
||||||
</b-button>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</b-modal>
|
|
||||||
% endif
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
<%def name="render_form_tag()">
|
|
||||||
% if master.has_perm('manage_api_tokens'):
|
|
||||||
${form.render_vue_tag(**{'@new-token': 'newTokenInit', '@delete-token': 'deleteTokenInit'})}
|
|
||||||
% else:
|
|
||||||
${form.render_vue_tag()}
|
|
||||||
% endif
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
<%def name="modify_vue_vars()">
|
|
||||||
${parent.modify_vue_vars()}
|
|
||||||
<script>
|
|
||||||
% if master.has_perm('manage_api_tokens'):
|
|
||||||
|
|
||||||
ThisPageData.newTokenShowDialog = false
|
|
||||||
ThisPageData.newTokenDescription = null
|
|
||||||
ThisPageData.newTokenRaw = null
|
|
||||||
ThisPageData.newTokenSaved = false
|
|
||||||
ThisPageData.newTokenSaving = false
|
|
||||||
|
|
||||||
ThisPage.methods.newTokenInit = function() {
|
|
||||||
this.newTokenDescription = null
|
|
||||||
this.newTokenRaw = null
|
|
||||||
this.newTokenSaved = false
|
|
||||||
this.newTokenShowDialog = true
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.$refs.newTokenDescription.focus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
ThisPage.methods.newTokenSave = function() {
|
|
||||||
this.newTokenSaving = true
|
|
||||||
|
|
||||||
const url = '${master.get_action_url('add_api_token', instance)}'
|
|
||||||
const params = {
|
|
||||||
description: this.newTokenDescription,
|
|
||||||
}
|
|
||||||
|
|
||||||
this.wuttaPOST(url, params, response => {
|
|
||||||
this.newTokenSaving = false
|
|
||||||
this.newTokenRaw = response.data.token_string
|
|
||||||
${form.vue_component}Data.gridContext['users.view.api_tokens'].data.push(response.data)
|
|
||||||
this.newTokenSaved = true
|
|
||||||
}, response => {
|
|
||||||
this.newTokenSaving = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
ThisPage.methods.deleteTokenInit = function(token) {
|
|
||||||
if (!confirm("Really delete this API token?")) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = '${master.get_action_url('delete_api_token', instance)}'
|
|
||||||
const params = {uuid: token.uuid}
|
|
||||||
this.wuttaPOST(url, params, response => {
|
|
||||||
const i = ${form.vue_component}Data.gridContext['users.view.api_tokens'].data.indexOf(token)
|
|
||||||
${form.vue_component}Data.gridContext['users.view.api_tokens'].data.splice(i, 1)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
% endif
|
|
||||||
</script>
|
|
||||||
</%def>
|
|
|
@ -48,10 +48,6 @@ class UserView(MasterView):
|
||||||
"""
|
"""
|
||||||
model_class = User
|
model_class = User
|
||||||
|
|
||||||
labels = {
|
|
||||||
'api_tokens': "API Tokens",
|
|
||||||
}
|
|
||||||
|
|
||||||
grid_columns = [
|
grid_columns = [
|
||||||
'username',
|
'username',
|
||||||
'person',
|
'person',
|
||||||
|
@ -70,7 +66,6 @@ class UserView(MasterView):
|
||||||
'active',
|
'active',
|
||||||
'prevent_edit',
|
'prevent_edit',
|
||||||
'roles',
|
'roles',
|
||||||
'api_tokens',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_query(self, session=None):
|
def get_query(self, session=None):
|
||||||
|
@ -156,12 +151,6 @@ class UserView(MasterView):
|
||||||
if not self.creating:
|
if not self.creating:
|
||||||
f.set_default('roles', [role.uuid.hex for role in user.roles])
|
f.set_default('roles', [role.uuid.hex for role in user.roles])
|
||||||
|
|
||||||
# api_tokens
|
|
||||||
if self.viewing and self.has_perm('manage_api_tokens'):
|
|
||||||
f.set_grid('api_tokens', self.make_api_tokens_grid(user))
|
|
||||||
else:
|
|
||||||
f.remove('api_tokens')
|
|
||||||
|
|
||||||
def unique_username(self, node, value):
|
def unique_username(self, node, value):
|
||||||
""" """
|
""" """
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
@ -262,93 +251,6 @@ class UserView(MasterView):
|
||||||
role = session.get(model.Role, uuid)
|
role = session.get(model.Role, uuid)
|
||||||
user.roles.remove(role)
|
user.roles.remove(role)
|
||||||
|
|
||||||
def make_api_tokens_grid(self, user):
|
|
||||||
"""
|
|
||||||
Make and return the grid for the API Tokens field.
|
|
||||||
|
|
||||||
This is only shown when current user has permission to manage
|
|
||||||
API tokens for other users.
|
|
||||||
|
|
||||||
:rtype: :class:`~wuttaweb.grids.base.Grid`
|
|
||||||
"""
|
|
||||||
model = self.app.model
|
|
||||||
route_prefix = self.get_route_prefix()
|
|
||||||
|
|
||||||
grid = self.make_grid(key=f'{route_prefix}.view.api_tokens',
|
|
||||||
data=[self.normalize_api_token(t) for t in user.api_tokens],
|
|
||||||
columns=[
|
|
||||||
'description',
|
|
||||||
'created',
|
|
||||||
],
|
|
||||||
sortable=True,
|
|
||||||
sort_on_backend=False,
|
|
||||||
sort_defaults=[('created', 'desc')])
|
|
||||||
|
|
||||||
if self.has_perm('manage_api_tokens'):
|
|
||||||
|
|
||||||
# create token
|
|
||||||
button = self.make_button("New", primary=True, icon_left='plus', **{'@click': "$emit('new-token')"})
|
|
||||||
grid.add_tool(button, key='create')
|
|
||||||
|
|
||||||
# delete token
|
|
||||||
grid.add_action('delete', url='#', icon='trash', link_class='has-text-danger', click_handler="$emit('delete-token', props.row)")
|
|
||||||
|
|
||||||
return grid
|
|
||||||
|
|
||||||
def normalize_api_token(self, token):
|
|
||||||
""" """
|
|
||||||
return {
|
|
||||||
'uuid': token.uuid.hex,
|
|
||||||
'description': token.description,
|
|
||||||
'created': self.app.render_datetime(token.created),
|
|
||||||
}
|
|
||||||
|
|
||||||
def add_api_token(self):
|
|
||||||
"""
|
|
||||||
AJAX view for adding a new user API token.
|
|
||||||
|
|
||||||
This calls
|
|
||||||
:meth:`wuttjamaican:wuttjamaican.auth.AuthHandler.add_api_token()`
|
|
||||||
for the creation logic.
|
|
||||||
"""
|
|
||||||
session = self.Session()
|
|
||||||
auth = self.app.get_auth_handler()
|
|
||||||
user = self.get_instance()
|
|
||||||
data = self.request.json_body
|
|
||||||
|
|
||||||
token = auth.add_api_token(user, data['description'])
|
|
||||||
session.flush()
|
|
||||||
session.refresh(token)
|
|
||||||
|
|
||||||
result = self.normalize_api_token(token)
|
|
||||||
result['token_string'] = token.token_string
|
|
||||||
result['_action_url_delete'] = '#'
|
|
||||||
return result
|
|
||||||
|
|
||||||
def delete_api_token(self):
|
|
||||||
"""
|
|
||||||
AJAX view for deleting a user API token.
|
|
||||||
|
|
||||||
This calls
|
|
||||||
:meth:`wuttjamaican:wuttjamaican.auth.AuthHandler.delete_api_token()`
|
|
||||||
for the deletion logic.
|
|
||||||
"""
|
|
||||||
model = self.app.model
|
|
||||||
session = self.Session()
|
|
||||||
auth = self.app.get_auth_handler()
|
|
||||||
user = self.get_instance()
|
|
||||||
data = self.request.json_body
|
|
||||||
|
|
||||||
token = session.get(model.UserAPIToken, data['uuid'])
|
|
||||||
if not token:
|
|
||||||
return {'error': "API token not found"}
|
|
||||||
|
|
||||||
if token.user is not user:
|
|
||||||
return {'error': "API token not found"}
|
|
||||||
|
|
||||||
auth.delete_api_token(token)
|
|
||||||
return {}
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def defaults(cls, config):
|
def defaults(cls, config):
|
||||||
""" """
|
""" """
|
||||||
|
@ -358,38 +260,8 @@ class UserView(MasterView):
|
||||||
app = wutta_config.get_app()
|
app = wutta_config.get_app()
|
||||||
cls.model_class = app.model.User
|
cls.model_class = app.model.User
|
||||||
|
|
||||||
cls._user_defaults(config)
|
|
||||||
cls._defaults(config)
|
cls._defaults(config)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _user_defaults(cls, config):
|
|
||||||
"""
|
|
||||||
Provide extra default configuration for the User master view.
|
|
||||||
"""
|
|
||||||
route_prefix = cls.get_route_prefix()
|
|
||||||
permission_prefix = cls.get_permission_prefix()
|
|
||||||
instance_url_prefix = cls.get_instance_url_prefix()
|
|
||||||
model_title = cls.get_model_title()
|
|
||||||
|
|
||||||
# manage API tokens
|
|
||||||
config.add_wutta_permission(permission_prefix,
|
|
||||||
f'{permission_prefix}.manage_api_tokens',
|
|
||||||
f"Manage API tokens for any {model_title}")
|
|
||||||
config.add_route(f'{route_prefix}.add_api_token',
|
|
||||||
f'{instance_url_prefix}/add-api-token',
|
|
||||||
request_method='POST')
|
|
||||||
config.add_view(cls, attr='add_api_token',
|
|
||||||
route_name=f'{route_prefix}.add_api_token',
|
|
||||||
permission=f'{permission_prefix}.manage_api_tokens',
|
|
||||||
renderer='json')
|
|
||||||
config.add_route(f'{route_prefix}.delete_api_token',
|
|
||||||
f'{instance_url_prefix}/delete-api-token',
|
|
||||||
request_method='POST')
|
|
||||||
config.add_view(cls, attr='delete_api_token',
|
|
||||||
route_name=f'{route_prefix}.delete_api_token',
|
|
||||||
permission=f'{permission_prefix}.manage_api_tokens',
|
|
||||||
renderer='json')
|
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs):
|
def defaults(config, **kwargs):
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
|
@ -1610,50 +1610,6 @@ class TestGrid(WebTestCase):
|
||||||
sorters = grid.get_vue_active_sorters()
|
sorters = grid.get_vue_active_sorters()
|
||||||
self.assertEqual(sorters, [{'field': 'name', 'order': 'asc'}])
|
self.assertEqual(sorters, [{'field': 'name', 'order': 'asc'}])
|
||||||
|
|
||||||
def test_get_vue_first_sorter(self):
|
|
||||||
|
|
||||||
# empty by default
|
|
||||||
grid = self.make_grid(key='foo', sortable=True)
|
|
||||||
sorter = grid.get_vue_first_sorter()
|
|
||||||
self.assertIsNone(sorter)
|
|
||||||
|
|
||||||
# will use first element from sort_defaults when applicable...
|
|
||||||
|
|
||||||
# basic
|
|
||||||
grid = self.make_grid(key='foo', sortable=True, sort_defaults='name')
|
|
||||||
sorter = grid.get_vue_first_sorter()
|
|
||||||
self.assertEqual(sorter, ['name', 'asc'])
|
|
||||||
|
|
||||||
# descending
|
|
||||||
grid = self.make_grid(key='foo', sortable=True, sort_defaults=('name', 'desc'))
|
|
||||||
sorter = grid.get_vue_first_sorter()
|
|
||||||
self.assertEqual(sorter, ['name', 'desc'])
|
|
||||||
|
|
||||||
# multiple
|
|
||||||
grid = self.make_grid(key='foo', sortable=True, sort_defaults=[('key', 'asc'), ('name', 'asc')])
|
|
||||||
sorter = grid.get_vue_first_sorter()
|
|
||||||
self.assertEqual(sorter, ['key', 'asc'])
|
|
||||||
|
|
||||||
# will use first element from active_sorters when applicable...
|
|
||||||
|
|
||||||
# basic
|
|
||||||
grid = self.make_grid(key='foo', sortable=True)
|
|
||||||
grid.active_sorters = [{'key': 'name', 'dir': 'asc'}]
|
|
||||||
sorter = grid.get_vue_first_sorter()
|
|
||||||
self.assertEqual(sorter, ['name', 'asc'])
|
|
||||||
|
|
||||||
# descending
|
|
||||||
grid = self.make_grid(key='foo', sortable=True)
|
|
||||||
grid.active_sorters = [{'key': 'name', 'dir': 'desc'}]
|
|
||||||
sorter = grid.get_vue_first_sorter()
|
|
||||||
self.assertEqual(sorter, ['name', 'desc'])
|
|
||||||
|
|
||||||
# multiple
|
|
||||||
grid = self.make_grid(key='foo', sortable=True)
|
|
||||||
grid.active_sorters = [{'key': 'key', 'dir': 'asc'}, {'key': 'name', 'dir': 'asc'}]
|
|
||||||
sorter = grid.get_vue_first_sorter()
|
|
||||||
self.assertEqual(sorter, ['key', 'asc'])
|
|
||||||
|
|
||||||
def test_get_vue_filters(self):
|
def test_get_vue_filters(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ from sqlalchemy import orm
|
||||||
|
|
||||||
import colander
|
import colander
|
||||||
|
|
||||||
from wuttaweb.grids import Grid
|
|
||||||
from wuttaweb.views import users as mod
|
from wuttaweb.views import users as mod
|
||||||
from wuttaweb.testing import WebTestCase
|
from wuttaweb.testing import WebTestCase
|
||||||
|
|
||||||
|
@ -123,18 +122,6 @@ class TestUserView(WebTestCase):
|
||||||
view.configure_form(form)
|
view.configure_form(form)
|
||||||
self.assertNotIn('password', form)
|
self.assertNotIn('password', form)
|
||||||
|
|
||||||
# api tokens grid shown only if current user has perm
|
|
||||||
with patch.object(view, 'viewing', new=True):
|
|
||||||
form = view.make_form(model_instance=barney)
|
|
||||||
self.assertIn('api_tokens', form)
|
|
||||||
view.configure_form(form)
|
|
||||||
self.assertNotIn('api_tokens', form)
|
|
||||||
with patch.object(self.request, 'is_root', new=True):
|
|
||||||
form = view.make_form(model_instance=barney)
|
|
||||||
self.assertIn('api_tokens', form)
|
|
||||||
view.configure_form(form)
|
|
||||||
self.assertIn('api_tokens', form)
|
|
||||||
|
|
||||||
def test_unique_username(self):
|
def test_unique_username(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
|
@ -330,108 +317,3 @@ class TestUserView(WebTestCase):
|
||||||
user = view.objectify(form)
|
user = view.objectify(form)
|
||||||
self.assertIs(user, barney)
|
self.assertIs(user, barney)
|
||||||
self.assertEqual(len(user.roles), 2)
|
self.assertEqual(len(user.roles), 2)
|
||||||
|
|
||||||
def test_normalize_api_token(self):
|
|
||||||
model = self.app.model
|
|
||||||
auth = self.app.get_auth_handler()
|
|
||||||
view = self.make_view()
|
|
||||||
|
|
||||||
user = model.User(username='foo')
|
|
||||||
self.session.add(user)
|
|
||||||
token = auth.add_api_token(user, 'test token')
|
|
||||||
self.session.commit()
|
|
||||||
|
|
||||||
normal = view.normalize_api_token(token)
|
|
||||||
self.assertIn('uuid', normal)
|
|
||||||
self.assertEqual(normal['uuid'], token.uuid.hex)
|
|
||||||
self.assertIn('description', normal)
|
|
||||||
self.assertEqual(normal['description'], 'test token')
|
|
||||||
self.assertIn('created', normal)
|
|
||||||
|
|
||||||
def test_make_api_tokens_grid(self):
|
|
||||||
model = self.app.model
|
|
||||||
auth = self.app.get_auth_handler()
|
|
||||||
view = self.make_view()
|
|
||||||
|
|
||||||
user = model.User(username='foo')
|
|
||||||
self.session.add(user)
|
|
||||||
token1 = auth.add_api_token(user, 'test1')
|
|
||||||
token2 = auth.add_api_token(user, 'test2')
|
|
||||||
self.session.commit()
|
|
||||||
|
|
||||||
# grid should have 2 records but no tools/actions
|
|
||||||
grid = view.make_api_tokens_grid(user)
|
|
||||||
self.assertIsInstance(grid, Grid)
|
|
||||||
self.assertEqual(len(grid.data), 2)
|
|
||||||
self.assertEqual(len(grid.tools), 0)
|
|
||||||
self.assertEqual(len(grid.actions), 0)
|
|
||||||
|
|
||||||
# create + delete allowed
|
|
||||||
with patch.object(self.request, 'is_root', new=True):
|
|
||||||
grid = view.make_api_tokens_grid(user)
|
|
||||||
self.assertEqual(len(grid.tools), 1)
|
|
||||||
self.assertIn('create', grid.tools)
|
|
||||||
self.assertEqual(len(grid.actions), 1)
|
|
||||||
self.assertEqual(grid.actions[0].key, 'delete')
|
|
||||||
|
|
||||||
def test_add_api_token(self):
|
|
||||||
model = self.app.model
|
|
||||||
view = self.make_view()
|
|
||||||
|
|
||||||
user = model.User(username='foo')
|
|
||||||
self.session.add(user)
|
|
||||||
self.session.commit()
|
|
||||||
self.session.refresh(user)
|
|
||||||
self.assertEqual(len(user.api_tokens), 0)
|
|
||||||
|
|
||||||
with patch.object(view, 'Session', return_value=self.session):
|
|
||||||
with patch.object(self.request, 'matchdict', new={'uuid': user.uuid}):
|
|
||||||
with patch.object(self.request, 'json_body', create=True,
|
|
||||||
new={'description': 'testing'}):
|
|
||||||
result = view.add_api_token()
|
|
||||||
self.assertEqual(len(user.api_tokens), 1)
|
|
||||||
token = user.api_tokens[0]
|
|
||||||
self.assertEqual(result['token_string'], token.token_string)
|
|
||||||
self.assertEqual(result['description'], 'testing')
|
|
||||||
|
|
||||||
def test_delete_api_token(self):
|
|
||||||
model = self.app.model
|
|
||||||
auth = self.app.get_auth_handler()
|
|
||||||
view = self.make_view()
|
|
||||||
|
|
||||||
user = model.User(username='foo')
|
|
||||||
self.session.add(user)
|
|
||||||
token1 = auth.add_api_token(user, 'test1')
|
|
||||||
token2 = auth.add_api_token(user, 'test2')
|
|
||||||
self.session.commit()
|
|
||||||
self.session.refresh(user)
|
|
||||||
self.assertEqual(len(user.api_tokens), 2)
|
|
||||||
|
|
||||||
with patch.object(view, 'Session', return_value=self.session):
|
|
||||||
with patch.object(self.request, 'matchdict', new={'uuid': user.uuid}):
|
|
||||||
|
|
||||||
# normal behavior
|
|
||||||
with patch.object(self.request, 'json_body', create=True,
|
|
||||||
new={'uuid': token1.uuid.hex}):
|
|
||||||
result = view.delete_api_token()
|
|
||||||
self.assertEqual(result, {})
|
|
||||||
self.session.refresh(user)
|
|
||||||
self.assertEqual(len(user.api_tokens), 1)
|
|
||||||
token = user.api_tokens[0]
|
|
||||||
self.assertIs(token, token2)
|
|
||||||
|
|
||||||
# token for wrong user
|
|
||||||
user2 = model.User(username='bar')
|
|
||||||
self.session.add(user2)
|
|
||||||
token3 = auth.add_api_token(user2, 'test3')
|
|
||||||
self.session.commit()
|
|
||||||
with patch.object(self.request, 'json_body', create=True,
|
|
||||||
new={'uuid': token3.uuid.hex}):
|
|
||||||
result = view.delete_api_token()
|
|
||||||
self.assertEqual(result, {'error': "API token not found"})
|
|
||||||
|
|
||||||
# token not found
|
|
||||||
with patch.object(self.request, 'json_body', create=True,
|
|
||||||
new={'uuid': self.app.make_true_uuid().hex}):
|
|
||||||
result = view.delete_api_token()
|
|
||||||
self.assertEqual(result, {'error': "API token not found"})
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue