Compare commits
No commits in common. "f9fad67f4a2a27f76e28756e8c4eeec2536b56c5" and "7feaa844afcd389c9350fe49ad3db5df8c955575" have entirely different histories.
f9fad67f4a
...
7feaa844af
|
@ -5,13 +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/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## v0.9.0 (2024-08-16)
|
||||
|
||||
### Feat
|
||||
|
||||
- add backend pagination support for grids
|
||||
- add initial/basic pagination for grids
|
||||
|
||||
## v0.8.1 (2024-08-15)
|
||||
|
||||
### Fix
|
||||
|
|
|
@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
|||
|
||||
[project]
|
||||
name = "WuttaWeb"
|
||||
version = "0.9.0"
|
||||
version = "0.8.1"
|
||||
description = "Web App for Wutta Framework"
|
||||
readme = "README.md"
|
||||
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
|
||||
|
@ -31,7 +31,6 @@ classifiers = [
|
|||
requires-python = ">= 3.8"
|
||||
dependencies = [
|
||||
"ColanderAlchemy",
|
||||
"paginate",
|
||||
"pyramid>=2",
|
||||
"pyramid_beaker",
|
||||
"pyramid_deform",
|
||||
|
|
|
@ -30,11 +30,9 @@ import logging
|
|||
|
||||
import sqlalchemy as sa
|
||||
|
||||
import paginate
|
||||
from pyramid.renderers import render
|
||||
from webhelpers2.html import HTML
|
||||
|
||||
from wuttaweb.db import Session
|
||||
from wuttaweb.util import FieldList, get_model_fields, make_json_safe
|
||||
|
||||
|
||||
|
@ -63,11 +61,6 @@ class Grid:
|
|||
Presumably unique key for the grid; used to track per-grid
|
||||
sort/filter settings etc.
|
||||
|
||||
.. attribute:: vue_tagname
|
||||
|
||||
String name for Vue component tag. By default this is
|
||||
``'wutta-grid'``. See also :meth:`render_vue_tag()`.
|
||||
|
||||
.. attribute:: model_class
|
||||
|
||||
Model class for the grid, if applicable. When set, this is
|
||||
|
@ -89,9 +82,6 @@ class Grid:
|
|||
model records) or else an object capable of producing such a
|
||||
list, e.g. SQLAlchemy query.
|
||||
|
||||
This is the "full" data set; see also
|
||||
:meth:`get_visible_data()`.
|
||||
|
||||
.. attribute:: labels
|
||||
|
||||
Dict of column label overrides.
|
||||
|
@ -116,57 +106,15 @@ class Grid:
|
|||
|
||||
See also :meth:`set_link()` and :meth:`is_linked()`.
|
||||
|
||||
.. attribute:: paginated
|
||||
.. attribute:: vue_tagname
|
||||
|
||||
Boolean indicating whether the grid data should be paginated
|
||||
vs. all data shown at once. Default is ``False`` which means
|
||||
the full set of grid data is sent for each request.
|
||||
|
||||
See also :attr:`pagesize` and :attr:`page`, and
|
||||
:attr:`paginate_on_backend`.
|
||||
|
||||
.. attribute:: paginate_on_backend
|
||||
|
||||
Boolean indicating whether the grid data should be paginated on
|
||||
the backend. Default is ``True`` which means only one "page"
|
||||
of data is sent to the client-side component.
|
||||
|
||||
If this is ``False``, the full set of grid data is sent for
|
||||
each request, and the client-side Vue component will handle the
|
||||
pagination.
|
||||
|
||||
Only relevant if :attr:`paginated` is also true.
|
||||
|
||||
.. attribute:: pagesize_options
|
||||
|
||||
List of "page size" options for the grid. See also
|
||||
:attr:`pagesize`.
|
||||
|
||||
Only relevant if :attr:`paginated` is true. If not specified,
|
||||
constructor will call :meth:`get_pagesize_options()` to get the
|
||||
value.
|
||||
|
||||
.. attribute:: pagesize
|
||||
|
||||
Number of records to show in a data page. See also
|
||||
:attr:`pagesize_options` and :attr:`page`.
|
||||
|
||||
Only relevant if :attr:`paginated` is true. If not specified,
|
||||
constructor will call :meth:`get_pagesize()` to get the value.
|
||||
|
||||
.. attribute:: page
|
||||
|
||||
The current page number (of data) to display in the grid. See
|
||||
also :attr:`pagesize`.
|
||||
|
||||
Only relevant if :attr:`paginated` is true. If not specified,
|
||||
constructor will assume ``1`` (first page).
|
||||
String name for Vue component tag. By default this is
|
||||
``'wutta-grid'``. See also :meth:`render_vue_tag()`.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
request,
|
||||
vue_tagname='wutta-grid',
|
||||
model_class=None,
|
||||
key=None,
|
||||
columns=None,
|
||||
|
@ -175,14 +123,9 @@ class Grid:
|
|||
renderers={},
|
||||
actions=[],
|
||||
linked_columns=[],
|
||||
paginated=False,
|
||||
paginate_on_backend=True,
|
||||
pagesize_options=None,
|
||||
pagesize=None,
|
||||
page=1,
|
||||
vue_tagname='wutta-grid',
|
||||
):
|
||||
self.request = request
|
||||
self.vue_tagname = vue_tagname
|
||||
self.model_class = model_class
|
||||
self.key = key
|
||||
self.data = data
|
||||
|
@ -190,18 +133,13 @@ class Grid:
|
|||
self.renderers = renderers or {}
|
||||
self.actions = actions or []
|
||||
self.linked_columns = linked_columns or []
|
||||
self.vue_tagname = vue_tagname
|
||||
|
||||
self.config = self.request.wutta_config
|
||||
self.app = self.config.get_app()
|
||||
|
||||
self.set_columns(columns or self.get_columns())
|
||||
|
||||
self.paginated = paginated
|
||||
self.paginate_on_backend = paginate_on_backend
|
||||
self.pagesize_options = pagesize_options or self.get_pagesize_options()
|
||||
self.pagesize = pagesize or self.get_pagesize()
|
||||
self.page = page
|
||||
|
||||
def get_columns(self):
|
||||
"""
|
||||
Returns the official list of column names for the grid, or
|
||||
|
@ -402,207 +340,6 @@ class Grid:
|
|||
return True
|
||||
return False
|
||||
|
||||
##############################
|
||||
# paging methods
|
||||
##############################
|
||||
|
||||
def get_pagesize_options(self, default=None):
|
||||
"""
|
||||
Returns a list of default page size options for the grid.
|
||||
|
||||
It will check config but if no setting exists, will fall
|
||||
back to::
|
||||
|
||||
[5, 10, 20, 50, 100, 200]
|
||||
|
||||
:param default: Alternate default value to return if none is
|
||||
configured.
|
||||
|
||||
This method is intended for use in the constructor. Code can
|
||||
instead access :attr:`pagesize_options` directly.
|
||||
"""
|
||||
options = self.config.get_list('wuttaweb.grids.default_pagesize_options')
|
||||
if options:
|
||||
options = [int(size) for size in options
|
||||
if size.isdigit()]
|
||||
if options:
|
||||
return options
|
||||
|
||||
return default or [5, 10, 20, 50, 100, 200]
|
||||
|
||||
def get_pagesize(self, default=None):
|
||||
"""
|
||||
Returns the default page size for the grid.
|
||||
|
||||
It will check config but if no setting exists, will fall back
|
||||
to a value from :attr:`pagesize_options` (will return ``20`` if
|
||||
that is listed; otherwise the "first" option).
|
||||
|
||||
:param default: Alternate default value to return if none is
|
||||
configured.
|
||||
|
||||
This method is intended for use in the constructor. Code can
|
||||
instead access :attr:`pagesize` directly.
|
||||
"""
|
||||
size = self.config.get_int('wuttaweb.grids.default_pagesize')
|
||||
if size:
|
||||
return size
|
||||
|
||||
if default:
|
||||
return default
|
||||
|
||||
if 20 in self.pagesize_options:
|
||||
return 20
|
||||
|
||||
return self.pagesize_options[0]
|
||||
|
||||
##############################
|
||||
# configuration methods
|
||||
##############################
|
||||
|
||||
def load_settings(self, store=True):
|
||||
"""
|
||||
Load all effective settings for the grid, from the following
|
||||
places:
|
||||
|
||||
* request params
|
||||
* user session
|
||||
|
||||
The first value found for a given setting will be applied to
|
||||
the grid.
|
||||
|
||||
.. note::
|
||||
|
||||
As of now, "pagination" settings are the only type
|
||||
supported by this logic. Filter/sort coming soon...
|
||||
|
||||
The overall logic for this method is as follows:
|
||||
|
||||
* collect settings
|
||||
* apply settings to current grid
|
||||
* optionally save settings to user session
|
||||
|
||||
Saving the settings to user session will allow the grid to
|
||||
"remember" its current settings when user refreshes the page.
|
||||
|
||||
:param store: Flag indicating whether the collected settings
|
||||
should then be saved to the user session.
|
||||
"""
|
||||
|
||||
# initial default settings
|
||||
settings = {}
|
||||
if self.paginated and self.paginate_on_backend:
|
||||
settings['pagesize'] = self.pagesize
|
||||
settings['page'] = self.page
|
||||
|
||||
# grab settings from request and/or user session
|
||||
if self.paginated and self.paginate_on_backend:
|
||||
self.update_page_settings(settings)
|
||||
|
||||
else:
|
||||
# no settings were found in request or user session, so
|
||||
# nothing needs to be saved
|
||||
store = False
|
||||
|
||||
# maybe store settings in user session, for next time
|
||||
if store:
|
||||
self.persist_settings(settings)
|
||||
|
||||
# update ourself to reflect settings
|
||||
if self.paginated and self.paginate_on_backend:
|
||||
self.pagesize = settings['pagesize']
|
||||
self.page = settings['page']
|
||||
|
||||
def request_has_settings(self):
|
||||
""" """
|
||||
for key in ['pagesize', 'page']:
|
||||
if key in self.request.GET:
|
||||
return True
|
||||
return False
|
||||
|
||||
def update_page_settings(self, settings):
|
||||
""" """
|
||||
# update the settings dict from request and/or user session
|
||||
|
||||
# pagesize
|
||||
pagesize = self.request.GET.get('pagesize')
|
||||
if pagesize is not None:
|
||||
if pagesize.isdigit():
|
||||
settings['pagesize'] = int(pagesize)
|
||||
else:
|
||||
pagesize = self.request.session.get(f'grid.{self.key}.pagesize')
|
||||
if pagesize is not None:
|
||||
settings['pagesize'] = pagesize
|
||||
|
||||
# page
|
||||
page = self.request.GET.get('page')
|
||||
if page is not None:
|
||||
if page.isdigit():
|
||||
settings['page'] = int(page)
|
||||
else:
|
||||
page = self.request.session.get(f'grid.{self.key}.page')
|
||||
if page is not None:
|
||||
settings['page'] = int(page)
|
||||
|
||||
def persist_settings(self, settings):
|
||||
""" """
|
||||
model = self.app.model
|
||||
session = Session()
|
||||
|
||||
# func to save a setting value to user session
|
||||
def persist(key, value=lambda k: settings.get(k)):
|
||||
skey = f'grid.{self.key}.{key}'
|
||||
self.request.session[skey] = value(key)
|
||||
|
||||
if self.paginated and self.paginate_on_backend:
|
||||
persist('pagesize')
|
||||
persist('page')
|
||||
|
||||
##############################
|
||||
# data methods
|
||||
##############################
|
||||
|
||||
def get_visible_data(self):
|
||||
"""
|
||||
Returns the "effective" visible data for the grid.
|
||||
|
||||
This uses :attr:`data` as the starting point but may morph it
|
||||
for pagination etc. per the grid settings.
|
||||
|
||||
Code can either access :attr:`data` directly, or call this
|
||||
method to get only the data for current view (e.g. assuming
|
||||
pagination is used), depending on the need.
|
||||
|
||||
See also these methods which may be called by this one:
|
||||
|
||||
* :meth:`paginate_data()`
|
||||
"""
|
||||
data = self.data or []
|
||||
|
||||
if self.paginated and self.paginate_on_backend:
|
||||
self.pager = self.paginate_data(data)
|
||||
data = self.pager
|
||||
|
||||
return data
|
||||
|
||||
def paginate_data(self, data):
|
||||
"""
|
||||
Apply pagination to the given data set, based on grid settings.
|
||||
|
||||
This returns a "pager" object which can then be used as a
|
||||
"data replacement" in subsequent logic.
|
||||
|
||||
This method is called by :meth:`get_visible_data()`.
|
||||
"""
|
||||
pager = paginate.Page(data,
|
||||
items_per_page=self.pagesize,
|
||||
page=self.page)
|
||||
return pager
|
||||
|
||||
##############################
|
||||
# rendering methods
|
||||
##############################
|
||||
|
||||
def render_vue_tag(self, **kwargs):
|
||||
"""
|
||||
Render the Vue component tag for the grid.
|
||||
|
@ -681,18 +418,17 @@ class Grid:
|
|||
"""
|
||||
Returns a list of Vue-compatible data records.
|
||||
|
||||
This calls :meth:`get_visible_data()` but then may modify the
|
||||
result, e.g. to add URLs for :attr:`actions` etc.
|
||||
This uses :attr:`data` as the basis, but may add some extra
|
||||
values to each record, e.g. URLs for :attr:`actions` etc.
|
||||
|
||||
Importantly, this also ensures each value in the dict is
|
||||
JSON-serializable, using
|
||||
:func:`~wuttaweb.util.make_json_safe()`.
|
||||
|
||||
:returns: List of data record dicts for use with Vue table
|
||||
component. May be the full set of data, or just the
|
||||
current page, per :attr:`paginate_on_backend`.
|
||||
component.
|
||||
"""
|
||||
original_data = self.get_visible_data()
|
||||
original_data = self.data or []
|
||||
|
||||
# TODO: at some point i thought it was useful to wrangle the
|
||||
# columns here, but now i can't seem to figure out why..?
|
||||
|
@ -743,22 +479,6 @@ class Grid:
|
|||
|
||||
return data
|
||||
|
||||
def get_vue_pager_stats(self):
|
||||
"""
|
||||
Returns a simple dict with current grid pager stats.
|
||||
|
||||
This is used when :attr:`paginate_on_backend` is in effect.
|
||||
"""
|
||||
pager = self.pager
|
||||
return {
|
||||
'item_count': pager.item_count,
|
||||
'items_per_page': pager.items_per_page,
|
||||
'page': pager.page,
|
||||
'page_count': pager.page_count,
|
||||
'first_item': pager.first_item,
|
||||
'last_item': pager.last_item,
|
||||
}
|
||||
|
||||
|
||||
class GridAction:
|
||||
"""
|
||||
|
|
|
@ -2,25 +2,8 @@
|
|||
|
||||
<script type="text/x-template" id="${grid.vue_tagname}-template">
|
||||
<${b}-table :data="data"
|
||||
:loading="loading"
|
||||
|
||||
narrowed
|
||||
hoverable
|
||||
icon-pack="fas"
|
||||
|
||||
## paging
|
||||
% if grid.paginated:
|
||||
paginated
|
||||
pagination-size="is-small"
|
||||
:per-page="perPage"
|
||||
:current-page="currentPage"
|
||||
@page-change="onPageChange"
|
||||
% if grid.paginate_on_backend:
|
||||
backend-pagination
|
||||
:total="pagerStats.item_count"
|
||||
% endif
|
||||
% endif
|
||||
>
|
||||
:loading="loading">
|
||||
|
||||
% for column in grid.get_vue_columns():
|
||||
<${b}-table-column field="${column['field']}"
|
||||
|
@ -51,165 +34,21 @@
|
|||
</${b}-table-column>
|
||||
% endif
|
||||
|
||||
% if grid.paginated:
|
||||
<template #footer>
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<div></div>
|
||||
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
||||
<span>
|
||||
showing
|
||||
{{ renderNumber(pagerStats.first_item) }}
|
||||
- {{ renderNumber(pagerStats.last_item) }}
|
||||
of {{ renderNumber(pagerStats.item_count) }} results;
|
||||
</span>
|
||||
<b-select v-model="perPage"
|
||||
% if grid.paginate_on_backend:
|
||||
@input="onPageSizeChange"
|
||||
% endif
|
||||
size="is-small">
|
||||
<option v-for="size in pageSizeOptions"
|
||||
:value="size">
|
||||
{{ size }}
|
||||
</option>
|
||||
</b-select>
|
||||
<span>
|
||||
per page
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
% endif
|
||||
|
||||
</${b}-table>
|
||||
</script>
|
||||
|
||||
<script>
|
||||
|
||||
let ${grid.vue_component} = {
|
||||
template: '#${grid.vue_tagname}-template',
|
||||
methods: {},
|
||||
}
|
||||
|
||||
let ${grid.vue_component}CurrentData = ${json.dumps(grid.get_vue_data())|n}
|
||||
|
||||
const ${grid.vue_component}Data = {
|
||||
let ${grid.vue_component}Data = {
|
||||
data: ${grid.vue_component}CurrentData,
|
||||
loading: false,
|
||||
|
||||
## paging
|
||||
% if grid.paginated:
|
||||
pageSizeOptions: ${json.dumps(grid.pagesize_options)|n},
|
||||
perPage: ${json.dumps(grid.pagesize)|n},
|
||||
currentPage: ${json.dumps(grid.page)|n},
|
||||
% if grid.paginate_on_backend:
|
||||
pagerStats: ${json.dumps(grid.get_vue_pager_stats())|n},
|
||||
% endif
|
||||
% endif
|
||||
}
|
||||
|
||||
const ${grid.vue_component} = {
|
||||
template: '#${grid.vue_tagname}-template',
|
||||
computed: {
|
||||
|
||||
% if not grid.paginate_on_backend:
|
||||
|
||||
pagerStats() {
|
||||
let last = this.currentPage * this.perPage
|
||||
let first = last - this.perPage + 1
|
||||
if (last > this.data.length) {
|
||||
last = this.data.length
|
||||
}
|
||||
return {
|
||||
'item_count': this.data.length,
|
||||
'items_per_page': this.perPage,
|
||||
'page': this.currentPage,
|
||||
'first_item': first,
|
||||
'last_item': last,
|
||||
}
|
||||
},
|
||||
|
||||
% endif
|
||||
},
|
||||
methods: {
|
||||
|
||||
renderNumber(value) {
|
||||
if (value != undefined) {
|
||||
return value.toLocaleString('en')
|
||||
}
|
||||
},
|
||||
|
||||
getBasicParams() {
|
||||
return {
|
||||
% if grid.paginated and grid.paginate_on_backend:
|
||||
pagesize: this.perPage,
|
||||
page: this.currentPage,
|
||||
% endif
|
||||
}
|
||||
},
|
||||
|
||||
async fetchData(params, success, failure) {
|
||||
|
||||
if (params === undefined || params === null) {
|
||||
params = new URLSearchParams(this.getBasicParams())
|
||||
} else {
|
||||
params = new URLSearchParams(params)
|
||||
}
|
||||
if (!params.has('partial')) {
|
||||
params.append('partial', true)
|
||||
}
|
||||
params = params.toString()
|
||||
|
||||
this.loading = true
|
||||
this.$http.get(`${request.path_url}?${'$'}{params}`).then(response => {
|
||||
console.log(response)
|
||||
console.log(response.data)
|
||||
if (!response.data.error) {
|
||||
${grid.vue_component}CurrentData = response.data.data
|
||||
this.data = ${grid.vue_component}CurrentData
|
||||
% if grid.paginated and grid.paginate_on_backend:
|
||||
this.pagerStats = response.data.pager_stats
|
||||
% endif
|
||||
this.loading = false
|
||||
if (success) {
|
||||
success()
|
||||
}
|
||||
} else {
|
||||
this.$buefy.toast.open({
|
||||
message: data.error,
|
||||
type: 'is-danger',
|
||||
duration: 2000, // 4 seconds
|
||||
})
|
||||
this.loading = false
|
||||
if (failure) {
|
||||
failure()
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.data = []
|
||||
% if grid.paginated and grid.paginate_on_backend:
|
||||
this.pagerStats = {}
|
||||
% endif
|
||||
this.loading = false
|
||||
if (failure) {
|
||||
failure()
|
||||
}
|
||||
throw error
|
||||
})
|
||||
},
|
||||
|
||||
% if grid.paginated:
|
||||
|
||||
% if grid.paginate_on_backend:
|
||||
onPageSizeChange(size) {
|
||||
this.fetchData()
|
||||
},
|
||||
% endif
|
||||
|
||||
onPageChange(page) {
|
||||
this.currentPage = page
|
||||
% if grid.paginate_on_backend:
|
||||
this.fetchData()
|
||||
% endif
|
||||
},
|
||||
|
||||
% endif
|
||||
},
|
||||
}
|
||||
|
||||
</script>
|
||||
|
|
|
@ -25,7 +25,6 @@ Base Logic for Views
|
|||
"""
|
||||
|
||||
from pyramid import httpexceptions
|
||||
from pyramid.renderers import render_to_response
|
||||
|
||||
from wuttaweb import forms, grids
|
||||
|
||||
|
@ -118,13 +117,3 @@ class View:
|
|||
correctly no matter what.
|
||||
"""
|
||||
return httpexceptions.HTTPFound(location=url, **kwargs)
|
||||
|
||||
def json_response(self, context):
|
||||
"""
|
||||
Convenience method to return a JSON response.
|
||||
|
||||
:param context: Context data to be rendered as JSON.
|
||||
|
||||
:returns: A :term:`response` with JSON content type.
|
||||
"""
|
||||
return render_to_response('json', context, request=self.request)
|
||||
|
|
|
@ -181,23 +181,6 @@ class MasterView(View):
|
|||
|
||||
This is optional; see also :meth:`get_grid_columns()`.
|
||||
|
||||
.. attribute:: paginated
|
||||
|
||||
Boolean indicating whether the grid data for the
|
||||
:meth:`index()` view should be paginated. Default is ``True``.
|
||||
|
||||
This is used by :meth:`make_model_grid()` to set the grid's
|
||||
:attr:`~wuttaweb.grids.base.Grid.paginated` flag.
|
||||
|
||||
.. attribute:: paginate_on_backend
|
||||
|
||||
Boolean indicating whether the grid data for the
|
||||
:meth:`index()` view should be paginated on the backend.
|
||||
Default is ``True``.
|
||||
|
||||
This is used by :meth:`make_model_grid()` to set the grid's
|
||||
:attr:`~wuttaweb.grids.base.Grid.paginate_on_backend` flag.
|
||||
|
||||
.. attribute:: creatable
|
||||
|
||||
Boolean indicating whether the view model supports "creating" -
|
||||
|
@ -246,8 +229,6 @@ class MasterView(View):
|
|||
# features
|
||||
listable = True
|
||||
has_grid = True
|
||||
paginated = True
|
||||
paginate_on_backend = True
|
||||
creatable = True
|
||||
viewable = True
|
||||
editable = True
|
||||
|
@ -294,16 +275,7 @@ class MasterView(View):
|
|||
}
|
||||
|
||||
if self.has_grid:
|
||||
grid = self.make_model_grid()
|
||||
|
||||
# so-called 'partial' requests get just data, no html
|
||||
if self.request.GET.get('partial'):
|
||||
context = {'data': grid.get_vue_data()}
|
||||
if grid.paginated and grid.paginate_on_backend:
|
||||
context['pager_stats'] = grid.get_vue_pager_stats()
|
||||
return self.json_response(context)
|
||||
|
||||
context['grid'] = grid
|
||||
context['grid'] = self.make_model_grid()
|
||||
|
||||
return self.render_to_response('index', context)
|
||||
|
||||
|
@ -1089,12 +1061,8 @@ class MasterView(View):
|
|||
|
||||
kwargs['actions'] = actions
|
||||
|
||||
kwargs.setdefault('paginated', self.paginated)
|
||||
kwargs.setdefault('paginate_on_backend', self.paginate_on_backend)
|
||||
|
||||
grid = self.make_grid(**kwargs)
|
||||
self.configure_grid(grid)
|
||||
grid.load_settings()
|
||||
return grid
|
||||
|
||||
def get_grid_columns(self):
|
||||
|
|
|
@ -3,16 +3,29 @@
|
|||
from unittest import TestCase
|
||||
from unittest.mock import patch
|
||||
|
||||
from paginate import Page
|
||||
from pyramid import testing
|
||||
|
||||
from wuttjamaican.conf import WuttaConfig
|
||||
from wuttaweb.grids import base
|
||||
from wuttaweb.forms import FieldList
|
||||
from tests.util import WebTestCase
|
||||
|
||||
|
||||
class TestGrid(WebTestCase):
|
||||
class TestGrid(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.config = WuttaConfig(defaults={
|
||||
'wutta.web.menus.handler_spec': 'tests.util:NullMenuHandler',
|
||||
})
|
||||
self.app = self.config.get_app()
|
||||
|
||||
self.request = testing.DummyRequest(wutta_config=self.config, use_oruga=False)
|
||||
|
||||
self.pyramid_config = testing.setUp(request=self.request, settings={
|
||||
'mako.directories': ['wuttaweb:templates'],
|
||||
})
|
||||
|
||||
def tearDown(self):
|
||||
testing.tearDown()
|
||||
|
||||
def make_grid(self, request=None, **kwargs):
|
||||
return base.Grid(request or self.request, **kwargs)
|
||||
|
@ -131,143 +144,6 @@ class TestGrid(WebTestCase):
|
|||
self.assertFalse(grid.is_linked('foo'))
|
||||
self.assertTrue(grid.is_linked('bar'))
|
||||
|
||||
def test_get_pagesize_options(self):
|
||||
grid = self.make_grid()
|
||||
|
||||
# default
|
||||
options = grid.get_pagesize_options()
|
||||
self.assertEqual(options, [5, 10, 20, 50, 100, 200])
|
||||
|
||||
# override default
|
||||
options = grid.get_pagesize_options(default=[42])
|
||||
self.assertEqual(options, [42])
|
||||
|
||||
# from config
|
||||
self.config.setdefault('wuttaweb.grids.default_pagesize_options', '1 2 3')
|
||||
options = grid.get_pagesize_options()
|
||||
self.assertEqual(options, [1, 2, 3])
|
||||
|
||||
def test_get_pagesize(self):
|
||||
grid = self.make_grid()
|
||||
|
||||
# default
|
||||
size = grid.get_pagesize()
|
||||
self.assertEqual(size, 20)
|
||||
|
||||
# override default
|
||||
size = grid.get_pagesize(default=42)
|
||||
self.assertEqual(size, 42)
|
||||
|
||||
# override default options
|
||||
self.config.setdefault('wuttaweb.grids.default_pagesize_options', '10 15 30')
|
||||
grid = self.make_grid()
|
||||
size = grid.get_pagesize()
|
||||
self.assertEqual(size, 10)
|
||||
|
||||
# from config
|
||||
self.config.setdefault('wuttaweb.grids.default_pagesize', '15')
|
||||
size = grid.get_pagesize()
|
||||
self.assertEqual(size, 15)
|
||||
|
||||
##############################
|
||||
# configuration methods
|
||||
##############################
|
||||
|
||||
def test_load_settings(self):
|
||||
grid = self.make_grid(key='foo', paginated=True, paginate_on_backend=True,
|
||||
pagesize=20, page=1)
|
||||
|
||||
# settings are loaded, applied, saved
|
||||
self.assertEqual(grid.page, 1)
|
||||
self.assertNotIn('grid.foo.page', self.request.session)
|
||||
self.request.GET = {'pagesize': '10', 'page': '2'}
|
||||
grid.load_settings()
|
||||
self.assertEqual(grid.page, 2)
|
||||
self.assertEqual(self.request.session['grid.foo.page'], 2)
|
||||
|
||||
# can skip the saving step
|
||||
self.request.GET = {'pagesize': '10', 'page': '3'}
|
||||
grid.load_settings(store=False)
|
||||
self.assertEqual(grid.page, 3)
|
||||
self.assertEqual(self.request.session['grid.foo.page'], 2)
|
||||
|
||||
# no error for non-paginated grid
|
||||
grid = self.make_grid(key='foo', paginated=False)
|
||||
grid.load_settings()
|
||||
self.assertFalse(grid.paginated)
|
||||
|
||||
def test_request_has_settings(self):
|
||||
grid = self.make_grid(key='foo')
|
||||
|
||||
self.assertFalse(grid.request_has_settings())
|
||||
|
||||
with patch.object(self.request, 'GET', new={'pagesize': '20'}):
|
||||
self.assertTrue(grid.request_has_settings())
|
||||
|
||||
with patch.object(self.request, 'GET', new={'page': '1'}):
|
||||
self.assertTrue(grid.request_has_settings())
|
||||
|
||||
def test_update_page_settings(self):
|
||||
grid = self.make_grid(key='foo')
|
||||
|
||||
# settings are updated from session
|
||||
settings = {'pagesize': 20, 'page': 1}
|
||||
self.request.session['grid.foo.pagesize'] = 10
|
||||
self.request.session['grid.foo.page'] = 2
|
||||
grid.update_page_settings(settings)
|
||||
self.assertEqual(settings['pagesize'], 10)
|
||||
self.assertEqual(settings['page'], 2)
|
||||
|
||||
# settings are updated from request
|
||||
self.request.GET = {'pagesize': '15', 'page': '4'}
|
||||
grid.update_page_settings(settings)
|
||||
self.assertEqual(settings['pagesize'], 15)
|
||||
self.assertEqual(settings['page'], 4)
|
||||
|
||||
def test_persist_settings(self):
|
||||
grid = self.make_grid(key='foo', paginated=True, paginate_on_backend=True)
|
||||
|
||||
# nb. no error if empty settings, but it saves null values
|
||||
grid.persist_settings({})
|
||||
self.assertIsNone(self.request.session['grid.foo.page'])
|
||||
|
||||
# provided values are saved
|
||||
grid.persist_settings({'pagesize': 15, 'page': 3})
|
||||
self.assertEqual(self.request.session['grid.foo.page'], 3)
|
||||
|
||||
##############################
|
||||
# data methods
|
||||
##############################
|
||||
|
||||
def test_get_visible_data(self):
|
||||
data = [
|
||||
{'foo': 1, 'bar': 1},
|
||||
{'foo': 2, 'bar': 2},
|
||||
{'foo': 3, 'bar': 3},
|
||||
{'foo': 4, 'bar': 4},
|
||||
{'foo': 5, 'bar': 5},
|
||||
{'foo': 6, 'bar': 6},
|
||||
{'foo': 7, 'bar': 7},
|
||||
{'foo': 8, 'bar': 8},
|
||||
{'foo': 9, 'bar': 9},
|
||||
]
|
||||
grid = self.make_grid(data=data,
|
||||
columns=['foo', 'bar'],
|
||||
paginated=True, paginate_on_backend=True,
|
||||
pagesize=4, page=2)
|
||||
visible = grid.get_visible_data()
|
||||
self.assertEqual(len(visible), 4)
|
||||
self.assertEqual(visible[0], {'foo': 5, 'bar': 5})
|
||||
|
||||
def test_paginate_data(self):
|
||||
grid = self.make_grid()
|
||||
pager = grid.paginate_data([])
|
||||
self.assertIsInstance(pager, Page)
|
||||
|
||||
##############################
|
||||
# rendering methods
|
||||
##############################
|
||||
|
||||
def test_render_vue_tag(self):
|
||||
grid = self.make_grid(columns=['foo', 'bar'])
|
||||
html = grid.render_vue_tag()
|
||||
|
@ -321,28 +197,6 @@ class TestGrid(WebTestCase):
|
|||
data = grid.get_vue_data()
|
||||
self.assertEqual(data, [{'foo': 'blah blah', '_action_url_view': '/blarg'}])
|
||||
|
||||
def test_get_vue_pager_stats(self):
|
||||
data = [
|
||||
{'foo': 1, 'bar': 1},
|
||||
{'foo': 2, 'bar': 2},
|
||||
{'foo': 3, 'bar': 3},
|
||||
{'foo': 4, 'bar': 4},
|
||||
{'foo': 5, 'bar': 5},
|
||||
{'foo': 6, 'bar': 6},
|
||||
{'foo': 7, 'bar': 7},
|
||||
{'foo': 8, 'bar': 8},
|
||||
{'foo': 9, 'bar': 9},
|
||||
]
|
||||
|
||||
grid = self.make_grid(columns=['foo', 'bar'], pagesize=4, page=2)
|
||||
grid.pager = grid.paginate_data(data)
|
||||
stats = grid.get_vue_pager_stats()
|
||||
self.assertEqual(stats['item_count'], 9)
|
||||
self.assertEqual(stats['items_per_page'], 4)
|
||||
self.assertEqual(stats['page'], 2)
|
||||
self.assertEqual(stats['first_item'], 5)
|
||||
self.assertEqual(stats['last_item'], 8)
|
||||
|
||||
|
||||
class TestGridAction(TestCase):
|
||||
|
||||
|
|
|
@ -1,56 +1,46 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
|
||||
from unittest import TestCase
|
||||
|
||||
from pyramid import testing
|
||||
from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPNotFound
|
||||
|
||||
from wuttaweb.views import base as mod
|
||||
from wuttjamaican.conf import WuttaConfig
|
||||
from wuttaweb.views import base
|
||||
from wuttaweb.forms import Form
|
||||
from wuttaweb.grids import Grid, GridAction
|
||||
from tests.util import WebTestCase
|
||||
from wuttaweb.grids import Grid
|
||||
|
||||
|
||||
class TestView(WebTestCase):
|
||||
class TestView(TestCase):
|
||||
|
||||
def make_view(self):
|
||||
return mod.View(self.request)
|
||||
def setUp(self):
|
||||
self.config = WuttaConfig()
|
||||
self.app = self.config.get_app()
|
||||
self.request = testing.DummyRequest(wutta_config=self.config)
|
||||
self.view = base.View(self.request)
|
||||
|
||||
def test_basic(self):
|
||||
view = self.make_view()
|
||||
self.assertIs(view.request, self.request)
|
||||
self.assertIs(view.config, self.config)
|
||||
self.assertIs(view.app, self.app)
|
||||
self.assertIs(self.view.request, self.request)
|
||||
self.assertIs(self.view.config, self.config)
|
||||
self.assertIs(self.view.app, self.app)
|
||||
|
||||
def test_forbidden(self):
|
||||
view = self.make_view()
|
||||
error = view.forbidden()
|
||||
error = self.view.forbidden()
|
||||
self.assertIsInstance(error, HTTPForbidden)
|
||||
|
||||
def test_make_form(self):
|
||||
view = self.make_view()
|
||||
form = view.make_form()
|
||||
form = self.view.make_form()
|
||||
self.assertIsInstance(form, Form)
|
||||
|
||||
def test_make_grid(self):
|
||||
view = self.make_view()
|
||||
grid = view.make_grid()
|
||||
grid = self.view.make_grid()
|
||||
self.assertIsInstance(grid, Grid)
|
||||
|
||||
def test_make_grid_action(self):
|
||||
view = self.make_view()
|
||||
action = view.make_grid_action('view')
|
||||
self.assertIsInstance(action, GridAction)
|
||||
|
||||
def test_notfound(self):
|
||||
view = self.make_view()
|
||||
error = view.notfound()
|
||||
error = self.view.notfound()
|
||||
self.assertIsInstance(error, HTTPNotFound)
|
||||
|
||||
def test_redirect(self):
|
||||
view = self.make_view()
|
||||
error = view.redirect('/')
|
||||
error = self.view.redirect('/')
|
||||
self.assertIsInstance(error, HTTPFound)
|
||||
self.assertEqual(error.location, '/')
|
||||
|
||||
def test_json_response(self):
|
||||
view = self.make_view()
|
||||
response = view.json_response({'foo': 'bar'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
|
|
@ -747,14 +747,6 @@ class TestMasterView(WebTestCase):
|
|||
data = [{'name': 'foo', 'value': 'bar'}]
|
||||
with patch.object(view, 'get_grid_data', return_value=data):
|
||||
response = view.index()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.content_type, 'text/html')
|
||||
|
||||
# then once more as 'partial' - aka. data only
|
||||
self.request.GET = {'partial': '1'}
|
||||
response = view.index()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.content_type, 'application/json')
|
||||
|
||||
def test_create(self):
|
||||
self.pyramid_config.include('wuttaweb.views.common')
|
||||
|
|
Loading…
Reference in a new issue