Skip to content
This repository has been archived by the owner on Feb 21, 2025. It is now read-only.

Commit

Permalink
Paginate results
Browse files Browse the repository at this point in the history
This is done on the backend (completely by hand,
unfortunately), and handled by a dedicated js
object handling the front-end part of the pagination.

- API fixes page argument, if out of range.
- comparison_id and page in URL control
  page behaviour.
  • Loading branch information
nothingface0 committed Jan 31, 2025
1 parent f68d049 commit db2d7d7
Show file tree
Hide file tree
Showing 5 changed files with 685 additions and 58 deletions.
74 changes: 48 additions & 26 deletions db.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
### DQM^2 Mirror DB === >
import os
import sys
import math
import shutil
import logging
import psycopg2
Expand Down Expand Up @@ -575,42 +576,63 @@ def fill_cluster_status(self, cluster_status: dict):
)
)

def get_cmssw_comparison_reports(self) -> list[dict]:
def get_cmssw_comparison_reports(self, items_per_page: int, page: int) -> dict:
"""
Check if comparison report exists in DB and return result
"""
with self.engine.connect() as cur:
results = (
cur.execute(
text(
f"SET TIMEZONE = '{TIMEZONE}'; "
+ f"SELECT * FROM {self.TB_NAME_CMSSW_COMPARISON_REPORTS} ORDER BY comparison_ran_at DESC;"
)
)
.mappings()
.all()
# TODO: This is awful, maybe use flask-sqlalchemy at some point, it comes
# with pagination.
cur = self.engine.connect()
num_total_reports = cur.execute(
text(f"SELECT COUNT (*) FROM {self.TB_NAME_CMSSW_COMPARISON_REPORTS};")
).first()[0]
num_total_pages = math.ceil(num_total_reports / items_per_page)
if page < 0:
page = 1
elif page > num_total_pages:
page = num_total_pages
offset = (page - 1) * items_per_page

self.log.info(num_total_reports)
results = (
cur.execute(
text(
f"SET TIMEZONE = '{TIMEZONE}'; "
+ f"SELECT * FROM {self.TB_NAME_CMSSW_COMPARISON_REPORTS} "
+ "ORDER BY comparison_ran_at DESC "
+ "LIMIT :limit OFFSET :offset ;"
),
limit=items_per_page,
offset=offset,
)
if results:
return [dict(result) for result in results]
return []
.mappings()
.all()
)
if results:
return {
"data": [dict(result) for result in results],
"page": page,
"total_pages": num_total_pages,
}
return {"data": [], "page": page, "total_pages": num_total_pages}

def get_cmssw_comparison_report(self, id: str) -> dict:
"""
Check if comparison report exists in DB and return result
"""
with self.engine.connect() as cur:
result = (
cur.execute(
text(
f"SET TIMEZONE = '{TIMEZONE}'; SELECT * FROM {self.TB_NAME_CMSSW_COMPARISON_REPORTS} WHERE id=:id"
),
id=id,
)
.mappings()
.first()
cur = self.engine.connect()
result = (
cur.execute(
text(
f"SET TIMEZONE = '{TIMEZONE}'; SELECT * FROM {self.TB_NAME_CMSSW_COMPARISON_REPORTS} WHERE id=:id"
),
id=id,
)
if result:
return dict(result)
.mappings()
.first()
)
if result:
return dict(result)
return {}

def _store_cmssw_comparison_report_html(
Expand Down
9 changes: 8 additions & 1 deletion server.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,9 +264,16 @@ def dqm2_api():
elif what == "get_cmssw_comparison_reports":
id = flask.request.args.get("id", type=str)
type = flask.request.args.get("type", type=str)
page = flask.request.args.get("page", type=int, default=1)
items_per_page = flask.request.args.get(
"items_per_page", type=int, default=10
)
if not id:
return json.dumps(
db_playback.get_cmssw_comparison_reports(), default=json_serializer
db_playback.get_cmssw_comparison_reports(
items_per_page=items_per_page, page=page
),
default=json_serializer,
)
else:
if not type:
Expand Down
277 changes: 277 additions & 0 deletions static/pagination_helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
/*
Pagination helper for splitting array-like data into pages
and showing a page index to browse it.
This implementation assumes that all the data is available at
instantiation, and then split into pages.
*/
function pagination_helper() {
let _current_page = 1;
let _data = {}
let _num_total_items = 1;
let _dom_element = ''; // The DOM element where the data will be put in
let _items_per_page = 1;
let _total_pages = 1;
let _item_creation_callback;
let _is_initialized = false;
const NUM_PAGES_SHOW_ELLIPSIS = 2


function _calculate_num_pages() {
_total_pages = Math.ceil(_num_total_items / _items_per_page);
}

/* Helper function for adding CSS classes to a paginator page button */
function _add_bootstrap_classes_to_page_li(el) {
el.classList.add('page-item')
el.firstChild.classList.add('page-link')
return el
}

/* Callback to run when clicking the next page button */
function _goto_next_page() {
if (_current_page + 1 > _total_pages) {
_current_page = _total_pages;
}
else {
_current_page += 1;
}
_goto_specific_page(_current_page)
}

/* Callback to run when clicking the previous page button */
function _goto_prev_page() {
if (_current_page - 1 < 1) {
_current_page = 1;
}
else {
_current_page -= 1;
}
_goto_specific_page(_current_page)
}

/* To be run on any page change */
function _on_page_change() {
_clear_paginator()
_create_paginator_buttons()
_clear_page_data()
_fill_page_data()
}
/* Callback to run when clicking on a page button */
function _goto_specific_page(page_num) {
if (page_num <= _total_pages && page_num >= 1) {
_current_page = page_num;
_on_page_change()
}
}

/*
Function that crates a paginator button for a single page.
content: can be either a DOM Element (in which case it is appended directly
into the button), or a text/number, in which case it replaces the innerHTML.
additional classes: a string of CSS classes to be added to the button (e.g., "class1 class2")
onclick: the function to run when the button is clicked. It will be passed the
DOM Element as an argument.
title: the string to display on button mouse hover.
*/
function _create_page_el(content, additional_classes, onclick, title) {
let li = document.createElement('li')
if (additional_classes) {
li.classList.add(additional_classes)
}
if (title) {
li.setAttribute('title', title)
}
let a = document.createElement('a')
a.setAttribute.href = '#'
if (content instanceof Element) {
a.appendChild(content)
} else if (typeof content === 'string' || typeof content === 'number') {
a.innerHTML = content
if (!title) {
li.setAttribute('title', `Go to page ${content}`)
}
}
if (typeof onclick === 'function') {
li.onclick = onclick
}
li.appendChild(a)
return _add_bootstrap_classes_to_page_li(li)
}

function _create_first_page_el() {
let i = document.createElement('i')
i.classList.add('bi', 'bi-chevron-double-left')

return _create_page_el(i, _current_page === 1 ? "disabled" : "",
() => { _goto_specific_page(1) }, "First page")
}

function _create_previous_page_el() {
let i = document.createElement('i')
i.classList.add('bi', 'bi-chevron-left')
return _create_page_el(i, _current_page === 1 ? "disabled" : "",
_goto_prev_page, "Previous page")
}

function _create_last_page_el() {
let i = document.createElement('i')
i.classList.add('bi', 'bi-chevron-double-right')
return _create_page_el(i, (_current_page === _total_pages || _total_pages === 0) ?
"disabled" : "",
() => { _goto_specific_page(_total_pages) },
'Last page')
}

function _create_next_page_el() {
let i = document.createElement('i')
i.classList.add('bi', 'bi-chevron-right')
return _create_page_el(i, (_current_page === _total_pages || _total_pages === 0) ?
"disabled" : "",
_goto_next_page,
"Next page")
}

/* Create a single page button for a specific page */
function _create_page_number_el(page_num) {
return _create_page_el(
String(page_num),
_current_page === page_num ? "active" : "",
() => { _goto_specific_page(page_num) }
)
}

/* Get the DOM element id where page buttons will be added */
function _get_pagination_el_id() {
return `${_dom_element}_pagination`
}

/* Clear the pagination buttons completely */
function _clear_paginator() {
let pagination_el = document.getElementById(_get_pagination_el_id())
if (pagination_el) {
pagination_el.innerHTML = ''
}
}

/*
Create the pagination element under the same parent as the
_dom_element passed at initialization.
*/
function _create_pagination_el() {
let base_el = document.getElementById(_dom_element).parentElement;
let pagination_el = document.createElement('nav');
let ul_el = document.createElement('ul');
ul_el.classList.add('pagination', 'mt-2')
ul_el.setAttribute('id', _get_pagination_el_id())
pagination_el.appendChild(ul_el)
base_el.appendChild(pagination_el);
}

/*
Create the page links inside the pagination DOM element.
*/
function _create_paginator_buttons() {
let pagination_el = document.getElementById(_get_pagination_el_id())
pagination_el.appendChild(_create_first_page_el())
pagination_el.appendChild(_create_previous_page_el())
if (_total_pages < 3) {
for (i = 1; i <= _total_pages; i++) {
pagination_el.appendChild(_create_page_number_el(i))
}
}
else {
if (_current_page - 1 <= NUM_PAGES_SHOW_ELLIPSIS) {
for (let i = 1; i <= _current_page - 1; i++) {
pagination_el.appendChild(_create_page_number_el(i))
}
}
else {
pagination_el.appendChild(_create_page_el('...', 'disabled'))
pagination_el.appendChild(_create_page_number_el(_current_page - 1))
}

pagination_el.appendChild(_create_page_number_el(_current_page))

if (_total_pages - _current_page <= NUM_PAGES_SHOW_ELLIPSIS) {
for (let i = _current_page + 1; i <= _total_pages; i++) {
pagination_el.appendChild(_create_page_number_el(i))
}
}
else {
pagination_el.appendChild(_create_page_number_el(_current_page + 1))
pagination_el.appendChild(_create_page_el('...', 'disabled'))

}
}
pagination_el.appendChild(_create_next_page_el())
pagination_el.appendChild(_create_last_page_el())
}

// Create a single li element for a single comparison report.
function _create_result_list_element(data) {
let li = document.createElement("li");

if (typeof _item_creation_callback === 'function') {
li = _item_creation_callback(li, data)
}
return li;
}

function _clear_page_data() {
let data_el = document.getElementById(_dom_element)
data_el.innerHTML = ''
}
function _fill_page_data() {
let data_el = document.getElementById(_dom_element)
let index_start = (_current_page - 1) * _items_per_page
let page_data = _data.slice(index_start, index_start + _items_per_page)
page_data.forEach(element => {
data_el.appendChild(_create_result_list_element(element))
});
}
function _paginate(data, dom_element, item_creation_callback, items_per_page) {
if (typeof data !== 'object' || !data.hasOwnProperty('length')) {
throw Error(`data passed should be array-like, cannot initialize pagination`)
}
_data = data;
_num_total_items = data.length;
if (typeof dom_element !== 'string' || !document.getElementById(dom_element)) {
throw Error(`Could not find DOM element "${dom_element}" to attach pagination to`)
}
_dom_element = dom_element;

_items_per_page = Number(items_per_page)
if (isNaN(_items_per_page)) {
throw Error(`items_per_page should be a number, received ${typeof items_per_page}: ${items_per_page}`)
}

if (typeof item_creation_callback === 'function') {
_item_creation_callback = item_creation_callback
}
_calculate_num_pages()
_create_pagination_el()
_on_page_change()
}
/* "Public" methods are returned as an object with functions */
return {
paginate: (data, dom_element, item_creation_callback, items_per_page = _items_per_page) => {
_paginate(data, dom_element, item_creation_callback, items_per_page)
_is_initialized = true
},
data: (data) => {
if (!_is_initialized) {
throw Error(`Paginator has not been initialized!`)
}
_paginate(data, _dom_element, _item_creation_callback, _items_per_page)
if (_current_page > _total_pages) {
_current_page = _total_pages
}
_on_page_change()
}
}
};
Loading

0 comments on commit db2d7d7

Please sign in to comment.