Search: implemented pagination

- Got rid of the nasty off-by-one logic in the JavaScript.
- Implemented pagination at the API.
This commit is contained in:
2018-01-10 17:07:21 +01:00
parent 82a2e9a523
commit 61673ef273
6 changed files with 66 additions and 34 deletions

View File

@@ -12,6 +12,7 @@ log = logging.getLogger(__name__)
NODE_AGG_TERMS = ['node_type', 'media', 'tags', 'is_free'] NODE_AGG_TERMS = ['node_type', 'media', 'tags', 'is_free']
USER_AGG_TERMS = ['roles', ] USER_AGG_TERMS = ['roles', ]
ITEMS_PER_PAGE = 10
# Will be set in setup_app() # Will be set in setup_app()
client: Elasticsearch = None client: Elasticsearch = None
@@ -54,7 +55,7 @@ def nested_bool(must: list, should: list, terms: dict, *, index_alias: str) -> S
return search return search
def do_node_search(query: str, terms: dict) -> dict: def do_node_search(query: str, terms: dict, page: int) -> dict:
""" """
Given user query input and term refinements Given user query input and term refinements
search for public published nodes search for public published nodes
@@ -82,6 +83,7 @@ def do_node_search(query: str, terms: dict) -> dict:
if not query: if not query:
search = search.sort('-created_at') search = search.sort('-created_at')
add_aggs_to_search(search, NODE_AGG_TERMS) add_aggs_to_search(search, NODE_AGG_TERMS)
search = paginate(search, page)
if log.isEnabledFor(logging.DEBUG): if log.isEnabledFor(logging.DEBUG):
log.debug(json.dumps(search.to_dict(), indent=4)) log.debug(json.dumps(search.to_dict(), indent=4))
@@ -94,12 +96,13 @@ def do_node_search(query: str, terms: dict) -> dict:
return response.to_dict() return response.to_dict()
def do_user_search(query: str, terms: dict) -> dict: def do_user_search(query: str, terms: dict, page: int) -> dict:
""" return user objects represented in elasicsearch result dict""" """ return user objects represented in elasicsearch result dict"""
must, should = _common_user_search(query) must, should = _common_user_search(query)
search = nested_bool(must, should, terms, index_alias='USER') search = nested_bool(must, should, terms, index_alias='USER')
add_aggs_to_search(search, USER_AGG_TERMS) add_aggs_to_search(search, USER_AGG_TERMS)
search = paginate(search, page)
if log.isEnabledFor(logging.DEBUG): if log.isEnabledFor(logging.DEBUG):
log.debug(json.dumps(search.to_dict(), indent=4)) log.debug(json.dumps(search.to_dict(), indent=4))
@@ -130,7 +133,7 @@ def _common_user_search(query: str) -> (typing.List[Query], typing.List[Query]):
return [], should return [], should
def do_user_search_admin(query: str, terms: dict) -> dict: def do_user_search_admin(query: str, terms: dict, page: int) -> dict:
""" """
return users seach result dict object return users seach result dict object
search all user fields and provide aggregation information search all user fields and provide aggregation information
@@ -150,6 +153,7 @@ def do_user_search_admin(query: str, terms: dict) -> dict:
search = nested_bool(must, should, terms, index_alias='USER') search = nested_bool(must, should, terms, index_alias='USER')
add_aggs_to_search(search, USER_AGG_TERMS) add_aggs_to_search(search, USER_AGG_TERMS)
search = paginate(search, page)
if log.isEnabledFor(logging.DEBUG): if log.isEnabledFor(logging.DEBUG):
log.debug(json.dumps(search.to_dict(), indent=4)) log.debug(json.dumps(search.to_dict(), indent=4))
@@ -162,6 +166,10 @@ def do_user_search_admin(query: str, terms: dict) -> dict:
return response.to_dict() return response.to_dict()
def paginate(search: Search, page_idx: int) -> Search:
return search[page_idx * ITEMS_PER_PAGE:(page_idx + 1) * ITEMS_PER_PAGE]
def setup_app(app): def setup_app(app):
global client global client

View File

@@ -10,7 +10,6 @@ log = logging.getLogger(__name__)
blueprint_search = Blueprint('elksearch', __name__) blueprint_search = Blueprint('elksearch', __name__)
TERMS = [ TERMS = [
'node_type', 'media', 'node_type', 'media',
'tags', 'is_free', 'projectname', 'tags', 'is_free', 'projectname',
@@ -35,25 +34,38 @@ def _term_filters() -> dict:
return {term: request.args.get(term, '') for term in TERMS} return {term: request.args.get(term, '') for term in TERMS}
def _page_index() -> int:
"""Return the page index from the query string."""
try:
page_idx = int(request.args.get('page') or '0')
except TypeError:
log.info('invalid page number %r received', request.args.get('page'))
raise wz_exceptions.BadRequest()
return page_idx
@blueprint_search.route('/') @blueprint_search.route('/')
def search_nodes(): def search_nodes():
searchword = _valid_search() searchword = _valid_search()
terms = _term_filters() terms = _term_filters()
data = queries.do_node_search(searchword, terms) page_idx = _page_index()
return jsonify(data)
result = queries.do_node_search(searchword, terms, page_idx)
return jsonify(result)
@blueprint_search.route('/user') @blueprint_search.route('/user')
def search_user(): def search_user():
searchword = _valid_search() searchword = _valid_search()
terms = _term_filters() terms = _term_filters()
# data is the raw elasticseach output. page_idx = _page_index()
# result is the raw elasticseach output.
# we need to filter fields in case of user objects. # we need to filter fields in case of user objects.
data = queries.do_user_search(searchword, terms) result = queries.do_user_search(searchword, terms, page_idx)
# filter sensitive stuff # filter sensitive stuff
# we only need. objectID, full_name, username # we only need. objectID, full_name, username
hits = data.get('hits') hits = result.get('hits', {})
new_hits = [] new_hits = []
@@ -70,9 +82,9 @@ def search_user():
new_hits.append(single_hit) new_hits.append(single_hit)
# replace search result with safe subset # replace search result with safe subset
data['hits']['hits'] = new_hits result['hits']['hits'] = new_hits
return jsonify(data) return jsonify(result)
@blueprint_search.route('/admin/user') @blueprint_search.route('/admin/user')
@@ -84,6 +96,7 @@ def search_user_admin():
searchword = _valid_search() searchword = _valid_search()
terms = _term_filters() terms = _term_filters()
data = queries.do_user_search_admin(searchword, terms) page_idx = _page_index()
result = queries.do_user_search_admin(searchword, terms, page_idx)
return jsonify(data) return jsonify(result)

View File

@@ -1,5 +1,5 @@
$(document).ready(function() { $(document).ready(function() {
var HITS_PER_PAGE = 25; var HITS_PER_PAGE = 10;
var MAX_VALUES_PER_FACET = 30; var MAX_VALUES_PER_FACET = 30;
// DOM binding // DOM binding
@@ -174,41 +174,48 @@ $(document).ready(function() {
return; return;
} }
var maxPages = 2; var maxPages = 3;
var nbPages = content.count / HITS_PER_PAGE; var nbPages = Math.floor(content.count / HITS_PER_PAGE);
// Process pagination // Process pagination
var pages = []; var pages = [];
if (content.page > maxPages) { if (content.page > maxPages) {
pages.push({ pages.push({
current: false, current: false,
number: 1 number: 0,
shownr: 1
}); });
// They don't really add much...
// pages.push({ current: false, number: '...', disabled: true });
} }
for (var p = content.page - maxPages; p < content.page + maxPages; ++p) { for (var p = content.page - maxPages; p < content.page + maxPages; ++p) {
if (p < 0 || p >= nbPages) { if (p < 0 || p > nbPages) {
continue; continue;
} }
pages.push({ pages.push({
current: content.page === p, current: content.page === p,
number: (p + 1) number: p,
shownr: p+1
}); });
} }
if (content.page + maxPages < nbPages) { if (content.page + maxPages < nbPages) {
// They don't really add much...
// pages.push({ current: false, number: '...', disabled: true });
pages.push({ pages.push({
current: false, current: false,
number: nbPages number: nbPages-1,
shownr: nbPages
}); });
} }
console.log('showing page', content.page);
var pagination = { var pagination = {
pages: pages, pages: pages,
prev_page: (content.page > 0 ? content.page : false),
next_page: (content.page + 1 < nbPages ? content.page + 2 : false)
}; };
if (content.page > 0) {
pagination.prev_page = {page: content.page - 1};
}
if (content.page < nbPages) {
pagination.next_page = {page: content.page + 1};
}
console.log('next page', pagination.next_page);
console.log('prev page', pagination.prev_page);
console.log('nbPages', nbPages);
// Display pagination // Display pagination
$pagination.html(paginationTemplate.render(pagination)); $pagination.html(paginationTemplate.render(pagination));
} }
@@ -230,7 +237,10 @@ $(document).ready(function() {
}); });
$(document).on('click', '.gotoPage', function() { $(document).on('click', '.gotoPage', function() {
//helper.setCurrentPage(+$(this).data('page') - 1).search(); const page_idx = $(this).data('page');
search.setCurrentPage(page_idx);
search.execute();
$("html, body").animate({ $("html, body").animate({
scrollTop: 0 scrollTop: 0
}, '500', 'swing'); }, '500', 'swing');

View File

@@ -248,7 +248,8 @@ $search-hit-width_grid: 100px
opacity: .6 opacity: .6
&.active a &.active a
color: white color: $color-text-dark-primary
font-weight: bold
#search-list #search-list
width: 40% width: 40%

View File

@@ -155,11 +155,11 @@ script(type="text/template", id="hit-template")
// Pagination template // Pagination template
script(type="text/template", id="pagination-template") script(type="text/template", id="pagination-template")
ul.search-pagination. ul.search-pagination.
<li {{^prev_page}}class="disabled"{{/prev_page}}><a href="#" {{#prev_page}} class="gotoPage" data-page="{{ prev_page }}" {{/prev_page}}><i class="pi-angle-left"></i></a></li> <li {{^prev_page}}class="disabled"{{/prev_page}}><a href="#" {{#prev_page}} class="gotoPage" data-page="{{ prev_page.page }}" {{/prev_page}}><i class="pi-angle-left"></i></a></li>
{{#pages}} {{#pages}}
<li class="{{#current}}active{{/current}}{{#disabled}}disabled{{/disabled}}"><a href="#" {{^disabled}} class="gotoPage" data-page="{{ number }}" {{/disabled}}>{{ number }}</a></li> <li class="{{#current}}active{{/current}}{{#disabled}}disabled{{/disabled}}"><a href="#" {{^disabled}} class="gotoPage" data-page="{{ number }}" {{/disabled}}>{{ shownr }}</a></li>
{{/pages}} {{/pages}}
<li {{^next_page}}class="disabled"{{/next_page}}><a href="#" {{#next_page}} class="gotoPage" data-page="{{ next_page }}" {{/next_page}}><i class="pi-angle-right"></i></a></li> <li {{^next_page}}class="disabled"{{/next_page}}><a href="#" {{#next_page}} class="gotoPage" data-page="{{ page }}" {{/next_page}}><i class="pi-angle-right"></i></a></li>
// Stats template // Stats template
script(type="text/template", id="stats-template") script(type="text/template", id="stats-template")

View File

@@ -78,11 +78,11 @@ script(type="text/template", id="hit-template")
// Pagination template // Pagination template
script(type="text/template", id="pagination-template") script(type="text/template", id="pagination-template")
ul.search-pagination. ul.search-pagination.
<li {{^prev_page}}class="disabled"{{/prev_page}}><a href="#" {{#prev_page}} class="gotoPage" data-page="{{ prev_page }}" {{/prev_page}}><i class="pi-angle-left"></i></a></li> <li {{^prev_page}}class="disabled"{{/prev_page}}><a href="#" {{#prev_page}} class="gotoPage" data-page="{{ page }}" {{/prev_page}}><i class="pi-angle-left"></i></a></li>
{{#pages}} {{#pages}}
<li class="{{#current}}active{{/current}}{{#disabled}}disabled{{/disabled}}"><a href="#" {{^disabled}} class="gotoPage" data-page="{{ number }}" {{/disabled}}>{{ number }}</a></li> <li class="{{#current}}active{{/current}}{{#disabled}}disabled{{/disabled}}"><a href="#" {{^disabled}} class="gotoPage" data-page="{{ number }}" {{/disabled}}>{{ shownr }}</a></li>
{{/pages}} {{/pages}}
<li {{^next_page}}class="disabled"{{/next_page}}><a href="#" {{#next_page}} class="gotoPage" data-page="{{ next_page }}" {{/next_page}}><i class="pi-angle-right"></i></a></li> <li {{^next_page}}class="disabled"{{/next_page}}><a href="#" {{#next_page}} class="gotoPage" data-page="{{ page }}" {{/next_page}}><i class="pi-angle-right"></i></a></li>
// Stats template // Stats template
script(type="text/template", id="stats-template") script(type="text/template", id="stats-template")