server: use index-based paging (#123)

This commit is contained in:
rr- 2017-02-09 00:48:06 +01:00
parent ba7ca0cd87
commit fdad08e176
34 changed files with 222 additions and 193 deletions

16
API.md
View file

@ -404,7 +404,7 @@ data.
## Listing tags ## Listing tags
- **Request** - **Request**
`GET /tags/?page=<page>&pageSize=<page-size>&query=<query>` `GET /tags/?offset=<initial-pos>&limit=<page-size>&query=<query>`
- **Output** - **Output**
@ -675,7 +675,7 @@ data.
## Listing posts ## Listing posts
- **Request** - **Request**
`GET /posts/?page=<page>&pageSize=<page-size>&query=<query>` `GET /posts/?offset=<initial-pos>&limit=<page-size>&query=<query>`
- **Output** - **Output**
@ -1102,7 +1102,7 @@ data.
## Listing comments ## Listing comments
- **Request** - **Request**
`GET /comments/?page=<page>&pageSize=<page-size>&query=<query>` `GET /comments/?offset=<initial-pos>&limit=<page-size>&query=<query>`
- **Output** - **Output**
@ -1291,7 +1291,7 @@ data.
## Listing users ## Listing users
- **Request** - **Request**
`GET /users/?page=<page>&pageSize=<page-size>&query=<query>` `GET /users/?offset=<initial-pos>&limit=<page-size>&query=<query>`
- **Output** - **Output**
@ -1539,7 +1539,7 @@ data.
## Listing snapshots ## Listing snapshots
- **Request** - **Request**
`GET /snapshots/?page=<page>&pageSize=<page-size>&query=<query>` `GET /snapshots/?offset=<initial-pos>&limit=<page-size>&query=<query>`
- **Output** - **Output**
@ -2167,8 +2167,8 @@ A result of search operation that involves paging.
```json5 ```json5
{ {
"query": <query>, // same as in input "query": <query>, // same as in input
"page": <page>, // same as in input "offset": <offset>, // same as in input
"pageSize": <page-size>, "limit": <page-size>,
"total": <total-count>, "total": <total-count>,
"results": [ "results": [
<resource>, <resource>,
@ -2181,7 +2181,7 @@ A result of search operation that involves paging.
**Field meaning** **Field meaning**
- `<query>`: the query passed in the original request that contains standard - `<query>`: the query passed in the original request that contains standard
[search query](#search). [search query](#search).
- `<page>`: the page number, passed in the original request. - `<offset>`: the record starting offset, passed in the original request.
- `<page-size>`: number of records on one page. - `<page-size>`: number of records on one page.
- `<total-count>`: how many resources were found. To get the page count, divide - `<total-count>`: how many resources were found. To get the page count, divide
this number by `<page-size>`. this number by `<page-size>`.

View file

@ -1,6 +1,6 @@
<div class='global-comment-list'> <div class='global-comment-list'>
<ul><!-- <ul><!--
--><% for (let post of ctx.results) { %><!-- --><% for (let post of ctx.response.results) { %><!--
--><li><!-- --><li><!--
--><div class='post-thumbnail'><!-- --><div class='post-thumbnail'><!--
--><% if (ctx.canViewPosts) { %><!-- --><% if (ctx.canViewPosts) { %><!--

View file

@ -1,8 +1,8 @@
<nav class='buttons'> <nav class='buttons'>
<ul> <ul>
<li> <li>
<% if (ctx.prevLinkActive) { %> <% if (ctx.prevPage !== ctx.currentPage) { %>
<a class='prev' href='<%- ctx.prevLink %>'> <a class='prev' href='<%- ctx.getClientUrlForPage(ctx.pages.get(ctx.prevPage).offset, ctx.pages.get(ctx.prevPage).limit) %>'>
<% } else { %> <% } else { %>
<a class='prev disabled'> <a class='prev disabled'>
<% } %> <% } %>
@ -11,7 +11,7 @@
</a> </a>
</li> </li>
<% for (let page of ctx.pages) { %> <% for (let page of ctx.pages.values()) { %>
<% if (page.ellipsis) { %> <% if (page.ellipsis) { %>
<li>&hellip;</li> <li>&hellip;</li>
<% } else { %> <% } else { %>
@ -20,14 +20,14 @@
<% } else { %> <% } else { %>
<li> <li>
<% } %> <% } %>
<a href='<%- page.link %>'><%- page.number %></a> <a href='<%- ctx.getClientUrlForPage(page.offset, page.limit) %>'><%- page.number %></a>
</li> </li>
<% } %> <% } %>
<% } %> <% } %>
<li> <li>
<% if (ctx.nextLinkActive) { %> <% if (ctx.nextPage !== ctx.currentPage) { %>
<a class='next' href='<%- ctx.nextLink %>'> <a class='next' href='<%- ctx.getClientUrlForPage(ctx.pages.get(ctx.nextPage).offset, ctx.pages.get(ctx.nextPage).limit) %>'>
<% } else { %> <% } else { %>
<a class='next disabled'> <a class='next disabled'>
<% } %> <% } %>

View file

@ -1,7 +1,7 @@
<div class='post-list'> <div class='post-list'>
<% if (ctx.results.length) { %> <% if (ctx.response.results.length) { %>
<ul> <ul>
<% for (let post of ctx.results) { %> <% for (let post of ctx.response.results) { %>
<li> <li>
<a class='thumbnail-wrapper <%= post.tags.length > 0 ? "tags" : "no-tags" %>' <a class='thumbnail-wrapper <%= post.tags.length > 0 ? "tags" : "no-tags" %>'
title='@<%- post.id %> (<%- post.type %>)&#10;&#10;Tags: <%- post.tags.map(tag => '#' + tag).join(' ') || 'none' %>' title='@<%- post.id %> (<%- post.type %>)&#10;&#10;Tags: <%- post.tags.map(tag => '#' + tag).join(' ') || 'none' %>'

View file

@ -1,7 +1,7 @@
<div class='snapshot-list'> <div class='snapshot-list'>
<% if (ctx.results.length) { %> <% if (ctx.response.results.length) { %>
<ul> <ul>
<% for (let item of ctx.results) { %> <% for (let item of ctx.response.results) { %>
<li> <li>
<div class='header operation-<%= item.operation %>'> <div class='header operation-<%= item.operation %>'>
<span class='time'> <span class='time'>

View file

@ -1,5 +1,5 @@
<div class='tag-list'> <div class='tag-list'>
<% if (ctx.results.length) { %> <% if (ctx.response.results.length) { %>
<table> <table>
<thead> <thead>
<th class='names'> <th class='names'>
@ -39,7 +39,7 @@
</th> </th>
</thead> </thead>
<tbody> <tbody>
<% for (let tag of ctx.results) { %> <% for (let tag of ctx.response.results) { %>
<tr> <tr>
<td class='names'> <td class='names'>
<ul> <ul>

View file

@ -1,6 +1,6 @@
<div class='user-list'> <div class='user-list'>
<ul><!-- <ul><!--
--><% for (let user of ctx.results) { %><!-- --><% for (let user of ctx.response.results) { %><!--
--><li> --><li>
<div class='wrapper'> <div class='wrapper'>
<% if (ctx.canViewUsers) { %> <% if (ctx.canViewUsers) { %>

View file

@ -25,14 +25,16 @@ class CommentsController {
this._pageController = new PageController(); this._pageController = new PageController();
this._pageController.run({ this._pageController.run({
parameters: ctx.parameters, parameters: ctx.parameters,
getClientUrlForPage: page => { defaultLimit: 10,
getClientUrlForPage: (offset, limit) => {
const parameters = Object.assign( const parameters = Object.assign(
{}, ctx.parameters, {page: page}); {}, ctx.parameters, {offset: offset, limit: limit});
return uri.formatClientLink('comments', parameters); return uri.formatClientLink('comments', parameters);
}, },
requestPage: page => { requestPage: (offset, limit) => {
return PostList.search( return PostList.search(
'sort:comment-date comment-count-min:1', page, 10, fields); 'sort:comment-date comment-count-min:1',
offset, limit, fields);
}, },
pageRenderer: pageCtx => { pageRenderer: pageCtx => {
Object.assign(pageCtx, { Object.assign(pageCtx, {

View file

@ -18,12 +18,6 @@ class PageController {
} }
run(ctx) { run(ctx) {
const extendedContext = {
getClientUrlForPage: ctx.getClientUrlForPage,
parameters: ctx.parameters,
};
ctx.pageContext = Object.assign({}, extendedContext);
this._view.run(ctx); this._view.run(ctx);
} }

View file

@ -88,14 +88,17 @@ class PostListController {
_syncPageController() { _syncPageController() {
this._pageController.run({ this._pageController.run({
parameters: this._ctx.parameters, parameters: this._ctx.parameters,
getClientUrlForPage: page => { defaultLimit: parseInt(settings.get().postsPerPage),
return uri.formatClientLink('posts', getClientUrlForPage: (offset, limit) => {
Object.assign({}, this._ctx.parameters, {page: page})); const parameters = Object.assign(
{}, this._ctx.parameters, {offset: offset, limit: limit});
return uri.formatClientLink('posts', parameters);
}, },
requestPage: page => { requestPage: (offset, limit) => {
return PostList.search( return PostList.search(
this._decorateSearchQuery(this._ctx.parameters.query), this._decorateSearchQuery(
page, settings.get().postsPerPage, fields); this._ctx.parameters.query || ''),
offset, limit, fields);
}, },
pageRenderer: pageCtx => { pageRenderer: pageCtx => {
Object.assign(pageCtx, { Object.assign(pageCtx, {

View file

@ -21,7 +21,7 @@ class PostMainController extends BasePostController {
Post.get(ctx.parameters.id), Post.get(ctx.parameters.id),
PostList.getAround( PostList.getAround(
ctx.parameters.id, this._decorateSearchQuery( ctx.parameters.id, this._decorateSearchQuery(
parameters ? parameters.query : '')), parameters ? parameters.query || '' : '')),
]).then(responses => { ]).then(responses => {
const [post, aroundResponse] = responses; const [post, aroundResponse] = responses;

View file

@ -22,13 +22,14 @@ class SnapshotsController {
this._pageController = new PageController(); this._pageController = new PageController();
this._pageController.run({ this._pageController.run({
parameters: ctx.parameters, parameters: ctx.parameters,
getClientUrlForPage: page => { defaultLimit: 25,
getClientUrlForPage: (offset, limit) => {
const parameters = Object.assign( const parameters = Object.assign(
{}, ctx.parameters, {page: page}); {}, ctx.parameters, {offset: offset, limit: limit});
return uri.formatClientLink('history', parameters); return uri.formatClientLink('history', parameters);
}, },
requestPage: page => { requestPage: (offset, limit) => {
return SnapshotList.search('', page, 25); return SnapshotList.search('', offset, limit);
}, },
pageRenderer: pageCtx => { pageRenderer: pageCtx => {
Object.assign(pageCtx, { Object.assign(pageCtx, {

View file

@ -57,14 +57,15 @@ class TagListController {
_syncPageController() { _syncPageController() {
this._pageController.run({ this._pageController.run({
parameters: this._ctx.parameters, parameters: this._ctx.parameters,
getClientUrlForPage: page => { defaultLimit: 50,
getClientUrlForPage: (offset, limit) => {
const parameters = Object.assign( const parameters = Object.assign(
{}, this._ctx.parameters, {page: page}); {}, this._ctx.parameters, {offset: offset, limit: limit});
return uri.formatClientLink('tags', parameters); return uri.formatClientLink('tags', parameters);
}, },
requestPage: page => { requestPage: (offset, limit) => {
return TagList.search( return TagList.search(
this._ctx.parameters.query, page, 50, fields); this._ctx.parameters.query, offset, limit, fields);
}, },
pageRenderer: pageCtx => { pageRenderer: pageCtx => {
return new TagsPageView(pageCtx); return new TagsPageView(pageCtx);

View file

@ -49,13 +49,15 @@ class UserListController {
_syncPageController() { _syncPageController() {
this._pageController.run({ this._pageController.run({
parameters: this._ctx.parameters, parameters: this._ctx.parameters,
getClientUrlForPage: page => { defaultLimit: 30,
getClientUrlForPage: (offset, limit) => {
const parameters = Object.assign( const parameters = Object.assign(
{}, this._ctx.parameters, {page: page}); {}, this._ctx.parameters, {offset, offset, limit: limit});
return uri.formatClientLink('users', parameters); return uri.formatClientLink('users', parameters);
}, },
requestPage: page => { requestPage: (offset, limit) => {
return UserList.search(this._ctx.parameters.query, page); return UserList.search(
this._ctx.parameters.query || '', offset, limit);
}, },
pageRenderer: pageCtx => { pageRenderer: pageCtx => {
Object.assign(pageCtx, { Object.assign(pageCtx, {

View file

@ -12,13 +12,13 @@ class PostList extends AbstractList {
'post', id, 'around', {query: searchQuery, fields: 'id'})); 'post', id, 'around', {query: searchQuery, fields: 'id'}));
} }
static search(text, page, pageSize, fields) { static search(text, offset, limit, fields) {
return api.get( return api.get(
uri.formatApiLink( uri.formatApiLink(
'posts', { 'posts', {
query: text, query: text,
page: page, offset: offset,
pageSize: pageSize, limit: limit,
fields: fields.join(','), fields: fields.join(','),
})) }))
.then(response => { .then(response => {

View file

@ -6,9 +6,9 @@ const AbstractList = require('./abstract_list.js');
const Snapshot = require('./snapshot.js'); const Snapshot = require('./snapshot.js');
class SnapshotList extends AbstractList { class SnapshotList extends AbstractList {
static search(text, page, pageSize) { static search(text, offset, limit) {
return api.get(uri.formatApiLink( return api.get(uri.formatApiLink(
'snapshots', {query: text, page: page, pageSize: pageSize})) 'snapshots', {query: text, offset: offset, limit: limit}))
.then(response => { .then(response => {
return Promise.resolve(Object.assign( return Promise.resolve(Object.assign(
{}, {},

View file

@ -6,13 +6,13 @@ const AbstractList = require('./abstract_list.js');
const Tag = require('./tag.js'); const Tag = require('./tag.js');
class TagList extends AbstractList { class TagList extends AbstractList {
static search(text, page, pageSize, fields) { static search(text, offset, limit, fields) {
return api.get( return api.get(
uri.formatApiLink( uri.formatApiLink(
'tags', { 'tags', {
query: text, query: text,
page: page, offset: offset,
pageSize: pageSize, limit: limit,
fields: fields.join(','), fields: fields.join(','),
})) }))
.then(response => { .then(response => {

View file

@ -6,10 +6,10 @@ const AbstractList = require('./abstract_list.js');
const User = require('./user.js'); const User = require('./user.js');
class UserList extends AbstractList { class UserList extends AbstractList {
static search(text, page) { static search(text, offset, limit) {
return api.get( return api.get(
uri.formatApiLink( uri.formatApiLink(
'users', {query: text, page: page, pageSize: 30})) 'users', {query: text, offset: offset, limit: limit}))
.then(response => { .then(response => {
return Promise.resolve(Object.assign( return Promise.resolve(Object.assign(
{}, {},

View file

@ -112,10 +112,6 @@ class Route {
return false; return false;
} }
// XXX: it is very unfitting place for this
parameters.query = parameters.query || '';
parameters.page = parseInt(parameters.page || '1');
return true; return true;
} }
}; };

View file

@ -13,7 +13,7 @@ class CommentsPageView extends events.EventTarget {
const sourceNode = template(ctx); const sourceNode = template(ctx);
for (let post of ctx.results) { for (let post of ctx.response.results) {
const commentListControl = new CommentListControl( const commentListControl = new CommentListControl(
sourceNode.querySelector( sourceNode.querySelector(
`.comments-container[data-for="${post.id}"]`), `.comments-container[data-for="${post.id}"]`),

View file

@ -21,13 +21,16 @@ class EndlessPageView {
views.emptyContent(this._pagesHolderNode); views.emptyContent(this._pagesHolderNode);
this.threshold = window.innerHeight / 3; this.threshold = window.innerHeight / 3;
this.minPageShown = null; this.minOffsetShown = null;
this.maxPageShown = null; this.maxOffsetShown = null;
this.totalPages = null; this.totalRecords = null;
this.currentPage = null; this.currentOffset = 0;
this._loadPage(ctx, ctx.parameters.page, true).then(pageNode => { const offset = parseInt(ctx.parameters.offset || 0);
if (ctx.parameters.page !== 1) { const limit = parseInt(ctx.parameters.limit || ctx.defaultLimit);
this._loadPage(ctx, offset, limit, true)
.then(pageNode => {
if (offset !== 0) {
pageNode.scrollIntoView(); pageNode.scrollIntoView();
} }
}); });
@ -75,42 +78,45 @@ class EndlessPageView {
if (!topPageNode) { if (!topPageNode) {
return; return;
} }
let topPageNumber = parseInt(topPageNode.getAttribute('data-page')); let topOffset = parseInt(topPageNode.getAttribute('data-offset'));
if (topPageNumber !== this.currentPage) { let topLimit = parseInt(topPageNode.getAttribute('data-limit'));
if (topOffset !== this.currentOffset) {
router.replace( router.replace(
ctx.getClientUrlForPage(topPageNumber), ctx.getClientUrlForPage(
topOffset,
topLimit === ctx.defaultLimit ? null : topLimit),
ctx.state, ctx.state,
false); false);
this.currentPage = topPageNumber; this.currentOffset = topOffset;
} }
if (this.totalPages === null) { if (this.totalRecords === null) {
return; return;
} }
let scrollHeight = let scrollHeight =
document.documentElement.scrollHeight - document.documentElement.scrollHeight -
document.documentElement.clientHeight; document.documentElement.clientHeight;
if (this.minPageShown > 1 && window.scrollY < this.threshold) { if (this.minOffsetShown > 0 && window.scrollY < this.threshold) {
this._loadPage(ctx, this.minPageShown - 1, false); this._loadPage(
} else if (this.maxPageShown < this.totalPages && ctx, this.minOffsetShown - topLimit, topLimit, false);
} else if (this.maxOffsetShown < this.totalRecords &&
window.scrollY + this.threshold > scrollHeight) { window.scrollY + this.threshold > scrollHeight) {
this._loadPage(ctx, this.maxPageShown + 1, true); this._loadPage(
ctx, this.maxOffsetShown, topLimit, true);
} }
} }
_loadPage(ctx, pageNumber, append) { _loadPage(ctx, offset, limit, append) {
this._working++; this._working++;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
ctx.requestPage(pageNumber).then(response => { ctx.requestPage(offset, limit).then(response => {
if (!this._active) { if (!this._active) {
this._working--; this._working--;
return Promise.reject(); return Promise.reject();
} }
this.totalPages = Math.ceil(response.total / response.pageSize);
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
let pageNode = this._renderPage( let pageNode = this._renderPage(ctx, append, response);
ctx, pageNumber, append, response);
this._working--; this._working--;
resolve(pageNode); resolve(pageNode);
}); });
@ -122,33 +128,40 @@ class EndlessPageView {
}); });
} }
_renderPage(ctx, pageNumber, append, response) { _renderPage(ctx, append, response) {
let pageNode = null; let pageNode = null;
if (response.total) { if (response.total) {
pageNode = pageTemplate({ pageNode = pageTemplate({
page: pageNumber, totalPages: Math.ceil(response.total / response.limit),
totalPages: this.totalPages, page: Math.ceil(
(response.offset + response.limit) / response.limit),
}); });
pageNode.setAttribute('data-page', pageNumber); pageNode.setAttribute('data-offset', response.offset);
pageNode.setAttribute('data-limit', response.limit);
Object.assign(ctx.pageContext, response); ctx.pageRenderer({
ctx.pageContext.hostNode = pageNode.querySelector( parameters: ctx.parameters,
'.page-content-holder'); response: response,
ctx.pageRenderer(ctx.pageContext); hostNode: pageNode.querySelector('.page-content-holder'),
});
if (pageNumber < this.minPageShown || this.totalRecords = response.total;
this.minPageShown === null) {
this.minPageShown = pageNumber; if (response.offset < this.minOffsetShown ||
this.minOffsetShown === null) {
this.minOffsetShown = response.offset;
} }
if (pageNumber > this.maxPageShown || if (response.offset + response.results.length
this.maxPageShown === null) { > this.maxOffsetShown ||
this.maxPageShown = pageNumber; this.maxOffsetShown === null) {
this.maxOffsetShown =
response.offset + response.results.length;
} }
if (append) { if (append) {
this._pagesHolderNode.appendChild(pageNode); this._pagesHolderNode.appendChild(pageNode);
if (!this._init && pageNumber !== 1) { if (!this._init && response.offset > 0) {
window.scroll(0, pageNode.getBoundingClientRect().top); window.scroll(0, pageNode.getBoundingClientRect().top);
} }
} else { } else {
@ -158,7 +171,7 @@ class EndlessPageView {
window.scrollX, window.scrollX,
window.scrollY + pageNode.offsetHeight); window.scrollY + pageNode.offsetHeight);
} }
} else if (response.total <= (pageNumber - 1) * response.pageSize) { } else if (!response.results.length) {
this.showInfo('No data to show'); this.showInfo('No data to show');
} }

View file

@ -35,19 +35,20 @@ function _getVisiblePageNumbers(currentPage, totalPages) {
return pagesVisible; return pagesVisible;
} }
function _getPages(currentPage, pageNumbers, ctx) { function _getPages(currentPage, pageNumbers, ctx, limit) {
const pages = []; const pages = new Map();
let lastPage = 0; let prevPage = 0;
for (let page of pageNumbers) { for (let page of pageNumbers) {
if (page !== lastPage + 1) { if (page !== prevPage + 1) {
pages.push({ellipsis: true}); pages.set(page - 1, {ellipsis: true});
} }
pages.push({ pages.set(page, {
number: page, number: page,
link: ctx.getClientUrlForPage(page), offset: (page - 1) * limit,
limit: limit === ctx.defaultLimit ? null : limit,
active: currentPage === page, active: currentPage === page,
}); });
lastPage = page; prevPage = page;
} }
return pages; return pages;
} }
@ -59,43 +60,30 @@ class ManualPageView {
} }
run(ctx) { run(ctx) {
const currentPage = ctx.parameters.page; const offset = parseInt(ctx.parameters.offset || 0);
const limit = parseInt(ctx.parameters.limit || ctx.defaultLimit);
this.clearMessages(); this.clearMessages();
views.emptyContent(this._pageNavNode); views.emptyContent(this._pageNavNode);
ctx.requestPage(currentPage).then(response => { ctx.requestPage(offset, limit).then(response => {
Object.assign(ctx.pageContext, response); ctx.pageRenderer({
ctx.pageContext.hostNode = this._pageContentHolderNode; parameters: ctx.parameters,
ctx.pageRenderer(ctx.pageContext); response: response,
hostNode: this._pageContentHolderNode,
const totalPages = Math.ceil(response.total / response.pageSize); });
const pageNumbers = _getVisiblePageNumbers(currentPage, totalPages);
const pages = _getPages(currentPage, pageNumbers, ctx);
keyboard.bind(['a', 'left'], () => { keyboard.bind(['a', 'left'], () => {
if (currentPage > 1) { this._navigateToPrevNextPage('prev');
router.show(ctx.getClientUrlForPage(currentPage - 1));
}
}); });
keyboard.bind(['d', 'right'], () => { keyboard.bind(['d', 'right'], () => {
if (currentPage < totalPages) { this._navigateToPrevNextPage('next');
router.show(ctx.getClientUrlForPage(currentPage + 1));
}
}); });
if (response.total) { if (response.total) {
views.replaceContent( this._refreshNav(response, ctx);
this._pageNavNode,
navTemplate({
prevLink: ctx.getClientUrlForPage(currentPage - 1),
nextLink: ctx.getClientUrlForPage(currentPage + 1),
prevLinkActive: currentPage > 1,
nextLinkActive: currentPage < totalPages,
pages: pages,
}));
} }
if (response.total <= (currentPage - 1) * response.pageSize) { if (!response.results.length) {
this.showInfo('No data to show'); this.showInfo('No data to show');
} }
@ -132,6 +120,33 @@ class ManualPageView {
showInfo(message) { showInfo(message) {
views.showInfo(this._hostNode, message); views.showInfo(this._hostNode, message);
} }
_navigateToPrevNextPage(className) {
const linkNode = this._hostNode.querySelector('a.' + className);
if (linkNode.classList.contains('disabled')) {
return;
}
router.show(linkNode.getAttribute('href'));
}
_refreshNav(response, ctx) {
const currentPage = Math.ceil(
(response.offset + response.limit) / response.limit);
const totalPages = Math.ceil(response.total / response.limit);
const pageNumbers = _getVisiblePageNumbers(currentPage, totalPages);
const pages = _getPages(currentPage, pageNumbers, ctx, response.limit);
views.replaceContent(
this._pageNavNode,
navTemplate({
getClientUrlForPage: ctx.getClientUrlForPage,
prevPage: Math.min(totalPages, Math.max(1, currentPage - 1)),
nextPage: Math.min(totalPages, Math.max(1, currentPage + 1)),
currentPage: currentPage,
totalPages: totalPages,
pages: pages,
}));
}
} }
module.exports = ManualPageView; module.exports = ManualPageView;

View file

@ -89,7 +89,8 @@ class PostsHeaderView extends events.EventTarget {
this._toggleMassTagVisibility(false); this._toggleMassTagVisibility(false);
this.dispatchEvent(new CustomEvent('navigate', {detail: {parameters: { this.dispatchEvent(new CustomEvent('navigate', {detail: {parameters: {
query: this._ctx.parameters.query, query: this._ctx.parameters.query,
page: this._ctx.parameters.page, offset: this._ctx.parameters.offset,
limit: this._ctx.parameters.limit,
tag: null, tag: null,
}}})); }}}));
} }
@ -107,7 +108,7 @@ class PostsHeaderView extends events.EventTarget {
'navigate', { 'navigate', {
detail: { detail: {
parameters: Object.assign( parameters: Object.assign(
{}, this._ctx.parameters, {tag: null, page: 1}), {}, this._ctx.parameters, {tag: null, offset: 0}),
}, },
})); }));
} }
@ -119,8 +120,8 @@ class PostsHeaderView extends events.EventTarget {
this._masstagAutoCompleteControl.hide(); this._masstagAutoCompleteControl.hide();
} }
let parameters = {query: this._queryInputNode.value}; let parameters = {query: this._queryInputNode.value};
parameters.page = parameters.query === this._ctx.parameters.query ? parameters.offset = parameters.query === this._ctx.parameters.query ?
this._ctx.parameters.page : 1; this._ctx.parameters.offset : 0;
if (this._massTagInputNode) { if (this._massTagInputNode) {
parameters.tag = this._massTagInputNode.value; parameters.tag = this._massTagInputNode.value;
this._massTagInputNode.blur(); this._massTagInputNode.blur();

View file

@ -13,7 +13,7 @@ class PostsPageView extends events.EventTarget {
views.replaceContent(this._hostNode, template(ctx)); views.replaceContent(this._hostNode, template(ctx));
this._postIdToPost = {}; this._postIdToPost = {};
for (let post of ctx.results) { for (let post of ctx.response.results) {
this._postIdToPost[post.id] = post; this._postIdToPost[post.id] = post;
post.addEventListener('change', e => this._evtPostChange(e)); post.addEventListener('change', e => this._evtPostChange(e));
} }

View file

@ -78,8 +78,8 @@ class Executor:
def execute( def execute(
self, self,
query_text: str, query_text: str,
page: int, offset: int,
page_size: int limit: int
) -> Tuple[int, List[model.Base]]: ) -> Tuple[int, List[model.Base]]:
search_query = self.parser.parse(query_text) search_query = self.parser.parse(query_text)
self.config.on_search_query_parsed(search_query) self.config.on_search_query_parsed(search_query)
@ -89,7 +89,7 @@ class Executor:
if token.name == 'random': if token.name == 'random':
disable_eager_loads = True disable_eager_loads = True
key = (id(self.config), hash(search_query), page, page_size) key = (id(self.config), hash(search_query), offset, limit)
if cache.has(key): if cache.has(key):
return cache.get(key) return cache.get(key)
@ -97,8 +97,8 @@ class Executor:
filter_query = filter_query.options(sa.orm.lazyload('*')) filter_query = filter_query.options(sa.orm.lazyload('*'))
filter_query = self._prepare_db_query(filter_query, search_query, True) filter_query = self._prepare_db_query(filter_query, search_query, True)
entities = filter_query \ entities = filter_query \
.offset(max(page - 1, 0) * page_size) \ .offset(offset) \
.limit(page_size) \ .limit(limit) \
.all() .all()
count_query = self.config.create_count_query(disable_eager_loads) count_query = self.config.create_count_query(disable_eager_loads)
@ -120,14 +120,13 @@ class Executor:
serializer: Callable[[model.Base], rest.Response] serializer: Callable[[model.Base], rest.Response]
) -> rest.Response: ) -> rest.Response:
query = ctx.get_param_as_string('query', default='') query = ctx.get_param_as_string('query', default='')
page = ctx.get_param_as_int('page', default=1, min=1) offset = ctx.get_param_as_int('offset', default=0, min=0)
page_size = ctx.get_param_as_int( limit = ctx.get_param_as_int('limit', default=100, min=1, max=100)
'pageSize', default=100, min=1, max=100) count, entities = self.execute(query, offset, limit)
count, entities = self.execute(query, page, page_size)
return { return {
'query': query, 'query': query,
'page': page, 'offset': offset,
'pageSize': page_size, 'limit': limit,
'total': count, 'total': count,
'results': [serializer(entity) for entity in entities], 'results': [serializer(entity) for entity in entities],
} }

View file

@ -23,12 +23,12 @@ def test_retrieving_multiple(user_factory, comment_factory, context_factory):
comments.serialize_comment.return_value = 'serialized comment' comments.serialize_comment.return_value = 'serialized comment'
result = api.comment_api.get_comments( result = api.comment_api.get_comments(
context_factory( context_factory(
params={'query': '', 'page': 1}, params={'query': '', 'offset': 0},
user=user_factory(rank=model.User.RANK_REGULAR))) user=user_factory(rank=model.User.RANK_REGULAR)))
assert result == { assert result == {
'query': '', 'query': '',
'page': 1, 'offset': 0,
'pageSize': 100, 'limit': 100,
'total': 2, 'total': 2,
'results': ['serialized comment', 'serialized comment'], 'results': ['serialized comment', 'serialized comment'],
} }
@ -39,7 +39,7 @@ def test_trying_to_retrieve_multiple_without_privileges(
with pytest.raises(errors.AuthError): with pytest.raises(errors.AuthError):
api.comment_api.get_comments( api.comment_api.get_comments(
context_factory( context_factory(
params={'query': '', 'page': 1}, params={'query': '', 'offset': 0},
user=user_factory(rank=model.User.RANK_ANONYMOUS))) user=user_factory(rank=model.User.RANK_ANONYMOUS)))

View file

@ -24,12 +24,12 @@ def test_retrieving_multiple(user_factory, post_factory, context_factory):
posts.serialize_post.return_value = 'serialized post' posts.serialize_post.return_value = 'serialized post'
result = api.post_api.get_posts( result = api.post_api.get_posts(
context_factory( context_factory(
params={'query': '', 'page': 1}, params={'query': '', 'offset': 0},
user=user_factory(rank=model.User.RANK_REGULAR))) user=user_factory(rank=model.User.RANK_REGULAR)))
assert result == { assert result == {
'query': '', 'query': '',
'page': 1, 'offset': 0,
'pageSize': 100, 'limit': 100,
'total': 2, 'total': 2,
'results': ['serialized post', 'serialized post'], 'results': ['serialized post', 'serialized post'],
} }
@ -48,12 +48,12 @@ def test_using_special_tokens(user_factory, post_factory, context_factory):
'serialized post %d' % post.post_id 'serialized post %d' % post.post_id
result = api.post_api.get_posts( result = api.post_api.get_posts(
context_factory( context_factory(
params={'query': 'special:fav', 'page': 1}, params={'query': 'special:fav', 'offset': 0},
user=auth_user)) user=auth_user))
assert result == { assert result == {
'query': 'special:fav', 'query': 'special:fav',
'page': 1, 'offset': 0,
'pageSize': 100, 'limit': 100,
'total': 1, 'total': 1,
'results': ['serialized post 1'], 'results': ['serialized post 1'],
} }
@ -67,7 +67,7 @@ def test_trying_to_use_special_tokens_without_logging_in(
with pytest.raises(errors.SearchError): with pytest.raises(errors.SearchError):
api.post_api.get_posts( api.post_api.get_posts(
context_factory( context_factory(
params={'query': 'special:fav', 'page': 1}, params={'query': 'special:fav', 'offset': 0},
user=user_factory(rank=model.User.RANK_ANONYMOUS))) user=user_factory(rank=model.User.RANK_ANONYMOUS)))
@ -76,7 +76,7 @@ def test_trying_to_retrieve_multiple_without_privileges(
with pytest.raises(errors.AuthError): with pytest.raises(errors.AuthError):
api.post_api.get_posts( api.post_api.get_posts(
context_factory( context_factory(
params={'query': '', 'page': 1}, params={'query': '', 'offset': 0},
user=user_factory(rank=model.User.RANK_ANONYMOUS))) user=user_factory(rank=model.User.RANK_ANONYMOUS)))

View file

@ -28,11 +28,11 @@ def test_retrieving_multiple(user_factory, context_factory):
db.session.flush() db.session.flush()
result = api.snapshot_api.get_snapshots( result = api.snapshot_api.get_snapshots(
context_factory( context_factory(
params={'query': '', 'page': 1}, params={'query': '', 'offset': 0},
user=user_factory(rank=model.User.RANK_REGULAR))) user=user_factory(rank=model.User.RANK_REGULAR)))
assert result['query'] == '' assert result['query'] == ''
assert result['page'] == 1 assert result['offset'] == 0
assert result['pageSize'] == 100 assert result['limit'] == 100
assert result['total'] == 2 assert result['total'] == 2
assert len(result['results']) == 2 assert len(result['results']) == 2
@ -42,5 +42,5 @@ def test_trying_to_retrieve_multiple_without_privileges(
with pytest.raises(errors.AuthError): with pytest.raises(errors.AuthError):
api.snapshot_api.get_snapshots( api.snapshot_api.get_snapshots(
context_factory( context_factory(
params={'query': '', 'page': 1}, params={'query': '', 'offset': 0},
user=user_factory(rank=model.User.RANK_ANONYMOUS))) user=user_factory(rank=model.User.RANK_ANONYMOUS)))

View file

@ -23,12 +23,12 @@ def test_retrieving_multiple(user_factory, tag_factory, context_factory):
tags.serialize_tag.return_value = 'serialized tag' tags.serialize_tag.return_value = 'serialized tag'
result = api.tag_api.get_tags( result = api.tag_api.get_tags(
context_factory( context_factory(
params={'query': '', 'page': 1}, params={'query': '', 'offset': 0},
user=user_factory(rank=model.User.RANK_REGULAR))) user=user_factory(rank=model.User.RANK_REGULAR)))
assert result == { assert result == {
'query': '', 'query': '',
'page': 1, 'offset': 0,
'pageSize': 100, 'limit': 100,
'total': 2, 'total': 2,
'results': ['serialized tag', 'serialized tag'], 'results': ['serialized tag', 'serialized tag'],
} }
@ -39,7 +39,7 @@ def test_trying_to_retrieve_multiple_without_privileges(
with pytest.raises(errors.AuthError): with pytest.raises(errors.AuthError):
api.tag_api.get_tags( api.tag_api.get_tags(
context_factory( context_factory(
params={'query': '', 'page': 1}, params={'query': '', 'offset': 0},
user=user_factory(rank=model.User.RANK_ANONYMOUS))) user=user_factory(rank=model.User.RANK_ANONYMOUS)))

View file

@ -28,8 +28,8 @@ def test_retrieving_multiple(user_factory, context_factory):
user=user_factory(rank=model.User.RANK_REGULAR))) user=user_factory(rank=model.User.RANK_REGULAR)))
assert result == { assert result == {
'query': '', 'query': '',
'page': 1, 'offset': 0,
'pageSize': 100, 'limit': 100,
'total': 2, 'total': 2,
'results': ['serialized user', 'serialized user'], 'results': ['serialized user', 'serialized user'],
} }

View file

@ -13,7 +13,7 @@ def executor():
def verify_unpaged(executor): def verify_unpaged(executor):
def verify(input, expected_comment_text): def verify(input, expected_comment_text):
actual_count, actual_comments = executor.execute( actual_count, actual_comments = executor.execute(
input, page=1, page_size=100) input, offset=0, limit=100)
actual_comment_text = [c.text for c in actual_comments] actual_comment_text = [c.text for c in actual_comments]
assert actual_count == len(expected_comment_text) assert actual_count == len(expected_comment_text)
assert actual_comment_text == expected_comment_text assert actual_comment_text == expected_comment_text

View file

@ -65,7 +65,7 @@ def auth_executor(executor, user_factory):
def verify_unpaged(executor): def verify_unpaged(executor):
def verify(input, expected_post_ids, test_order=False): def verify(input, expected_post_ids, test_order=False):
actual_count, actual_posts = executor.execute( actual_count, actual_posts = executor.execute(
input, page=1, page_size=100) input, offset=0, limit=100)
actual_post_ids = list([p.post_id for p in actual_posts]) actual_post_ids = list([p.post_id for p in actual_posts])
if not test_order: if not test_order:
actual_post_ids = sorted(actual_post_ids) actual_post_ids = sorted(actual_post_ids)
@ -381,7 +381,7 @@ def test_filter_by_safety(
def test_filter_by_invalid_type(executor): def test_filter_by_invalid_type(executor):
with pytest.raises(errors.SearchError): with pytest.raises(errors.SearchError):
executor.execute('type:invalid', page=1, page_size=100) executor.execute('type:invalid', offset=0, limit=100)
@pytest.mark.parametrize('input,expected_post_ids', [ @pytest.mark.parametrize('input,expected_post_ids', [
@ -458,7 +458,7 @@ def test_filter_by_image_size(
def test_filter_by_invalid_aspect_ratio(executor): def test_filter_by_invalid_aspect_ratio(executor):
with pytest.raises(errors.SearchError): with pytest.raises(errors.SearchError):
executor.execute('image-ar:1:1:1', page=1, page_size=100) executor.execute('image-ar:1:1:1', offset=0, limit=100)
@pytest.mark.parametrize('input,expected_post_ids', [ @pytest.mark.parametrize('input,expected_post_ids', [
@ -706,7 +706,7 @@ def test_own_disliked(
]) ])
def test_someones_score(executor, input): def test_someones_score(executor, input):
with pytest.raises(errors.SearchError): with pytest.raises(errors.SearchError):
executor.execute(input, page=1, page_size=100) executor.execute(input, offset=0, limit=100)
def test_own_fav( def test_own_fav(

View file

@ -13,7 +13,7 @@ def executor():
def verify_unpaged(executor): def verify_unpaged(executor):
def verify(input, expected_tag_names): def verify(input, expected_tag_names):
actual_count, actual_tags = executor.execute( actual_count, actual_tags = executor.execute(
input, page=1, page_size=100) input, offset=0, limit=100)
actual_tag_names = [u.names[0].name for u in actual_tags] actual_tag_names = [u.names[0].name for u in actual_tags]
assert actual_count == len(expected_tag_names) assert actual_count == len(expected_tag_names)
assert actual_tag_names == expected_tag_names assert actual_tag_names == expected_tag_names
@ -183,7 +183,7 @@ def test_filter_by_post_count(
]) ])
def test_filter_by_invalid_input(executor, input): def test_filter_by_invalid_input(executor, input):
with pytest.raises(errors.SearchError): with pytest.raises(errors.SearchError):
executor.execute(input, page=1, page_size=100) executor.execute(input, offset=0, limit=100)
@pytest.mark.parametrize('input,expected_tag_names', [ @pytest.mark.parametrize('input,expected_tag_names', [

View file

@ -13,7 +13,7 @@ def executor():
def verify_unpaged(executor): def verify_unpaged(executor):
def verify(input, expected_user_names): def verify(input, expected_user_names):
actual_count, actual_users = executor.execute( actual_count, actual_users = executor.execute(
input, page=1, page_size=100) input, offset=0, limit=100)
actual_user_names = [u.name for u in actual_users] actual_user_names = [u.name for u in actual_users]
assert actual_count == len(expected_user_names) assert actual_count == len(expected_user_names)
assert actual_user_names == expected_user_names assert actual_user_names == expected_user_names
@ -135,21 +135,23 @@ def test_combining_tokens(
@pytest.mark.parametrize( @pytest.mark.parametrize(
'page,page_size,expected_total_count,expected_user_names', [ 'offset,limit,expected_total_count,expected_user_names', [
(1, 1, 2, ['u1']),
(2, 1, 2, ['u2']),
(3, 1, 2, []),
(0, 1, 2, ['u1']), (0, 1, 2, ['u1']),
(1, 1, 2, ['u2']),
(2, 1, 2, []),
(-1, 1, 2, ['u1']),
(0, 2, 2, ['u1', 'u2']),
(3, 1, 2, []),
(0, 0, 2, []), (0, 0, 2, []),
]) ])
def test_paging( def test_paging(
executor, user_factory, page, page_size, executor, user_factory, offset, limit,
expected_total_count, expected_user_names): expected_total_count, expected_user_names):
db.session.add(user_factory(name='u1')) db.session.add(user_factory(name='u1'))
db.session.add(user_factory(name='u2')) db.session.add(user_factory(name='u2'))
db.session.flush() db.session.flush()
actual_count, actual_users = executor.execute( actual_count, actual_users = executor.execute(
'', page=page, page_size=page_size) '', offset=offset, limit=limit)
actual_user_names = [u.name for u in actual_users] actual_user_names = [u.name for u in actual_users]
assert actual_count == expected_total_count assert actual_count == expected_total_count
assert actual_user_names == expected_user_names assert actual_user_names == expected_user_names
@ -222,7 +224,7 @@ def test_random_sort(executor, user_factory):
db.session.add_all([user3, user1, user2]) db.session.add_all([user3, user1, user2])
db.session.flush() db.session.flush()
actual_count, actual_users = executor.execute( actual_count, actual_users = executor.execute(
'sort:random', page=1, page_size=100) 'sort:random', offset=0, limit=100)
actual_user_names = [u.name for u in actual_users] actual_user_names = [u.name for u in actual_users]
assert actual_count == 3 assert actual_count == 3
assert len(actual_user_names) == 3 assert len(actual_user_names) == 3
@ -251,4 +253,4 @@ def test_random_sort(executor, user_factory):
]) ])
def test_bad_tokens(executor, input, expected_error): def test_bad_tokens(executor, input, expected_error):
with pytest.raises(expected_error): with pytest.raises(expected_error):
executor.execute(input, page=1, page_size=100) executor.execute(input, offset=0, limit=100)