From d84a7cff1269ea2848bfb42936c50d07c0dc2d34 Mon Sep 17 00:00:00 2001 From: Pavel Raiskup Date: Sep 21 2021 13:42:46 +0000 Subject: frontend: web-ui: server-side pagination for too-many-builds When the list of builds is larger than N (10.000 currently), instead of loading all-in-one page and sort by JavaScript - provide a server-side pagination feature. This required a new `current_url()` helper (in templates as well). Note that I had to add 'dataTable' class (along the already existing 'datatable') to drop the 'margin-bottom: 20px' value causing an ugly gap between pagination and the table. We now also import all context_processors.py (not only the given list) into coprs.__init__.py. Fixes: #54 --- diff --git a/frontend/coprs_frontend/coprs/__init__.py b/frontend/coprs_frontend/coprs/__init__.py index d877ba1..b86c6e0 100644 --- a/frontend/coprs_frontend/coprs/__init__.py +++ b/frontend/coprs_frontend/coprs/__init__.py @@ -141,7 +141,7 @@ from coprs.exceptions import ( NonAdminCannotDisableAutoPrunning, ) from coprs.error_handlers import get_error_handler -from .context_processors import include_banner, inject_fedmenu, counter_processor +import coprs.context_processors setup_log() diff --git a/frontend/coprs_frontend/coprs/context_processors.py b/frontend/coprs_frontend/coprs/context_processors.py index a2e7181..99dc78a 100644 --- a/frontend/coprs_frontend/coprs/context_processors.py +++ b/frontend/coprs_frontend/coprs/context_processors.py @@ -1,7 +1,10 @@ import os -from . import app import flask +from coprs import app +from coprs.helpers import current_url + + BANNER_LOCATION = "/var/lib/copr/banner-include.html" @@ -82,3 +85,9 @@ def counter_processor(): return str(flask.g.counters[name]) return dict(counter=counter) + + +@app.context_processor +def current_url_processor(): + """ Provide 'current_url()' method in templates """ + return dict(current_url=current_url) diff --git a/frontend/coprs_frontend/coprs/helpers.py b/frontend/coprs_frontend/coprs/helpers.py index d812c33..90f155c 100644 --- a/frontend/coprs_frontend/coprs/helpers.py +++ b/frontend/coprs_frontend/coprs/helpers.py @@ -20,6 +20,8 @@ from sqlalchemy.types import TypeDecorator, VARCHAR from sqlalchemy.engine.default import DefaultDialect from sqlalchemy.sql.sqltypes import String, DateTime, NullType +from werkzeug.urls import url_encode + from copr_common.enums import EnumType # TODO: don't import BuildSourceEnum from helpers, use copr_common.enum instead from copr_common.enums import BuildSourceEnum # pylint: disable=unused-import @@ -700,3 +702,16 @@ def clone_sqlalchemy_instance(instance, ignored=None): setattr(new_instance, attr, rel) return new_instance + + +def current_url(**kwargs): + """ + Generate the same url as is currently processed, but define (or replace) the + arguments in kwargs. + """ + new_args = {} + new_args.update(flask.request.args) + new_args.update(kwargs) + if not new_args: + return flask.request.path + return '{}?{}'.format(flask.request.path, url_encode(new_args)) diff --git a/frontend/coprs_frontend/coprs/templates/_helpers.html b/frontend/coprs_frontend/coprs/templates/_helpers.html index a9dc9b6..c937909 100644 --- a/frontend/coprs_frontend/coprs/templates/_helpers.html +++ b/frontend/coprs_frontend/coprs/templates/_helpers.html @@ -866,3 +866,31 @@ https://accounts.fedoraproject.org/group/{{name}} {% endmacro %} + + +{% macro pagination_form(pagination) %} +
+
+ + + {{ pagination.per_page*(pagination.page -1) + 1}} - + {{ pagination.per_page*(pagination.page) }} + + of + {{ pagination.total }} +
    +
  • +
  • +
+ + + of {{ pagination.pages }} +
    +
  • +
  • +
+
+
+ +{% endmacro %} diff --git a/frontend/coprs_frontend/coprs/templates/coprs/detail/_builds_table.html b/frontend/coprs_frontend/coprs/templates/coprs/detail/_builds_table.html index 953334f..baf50be 100644 --- a/frontend/coprs_frontend/coprs/templates/coprs/detail/_builds_table.html +++ b/frontend/coprs_frontend/coprs/templates/coprs/detail/_builds_table.html @@ -1,13 +1,16 @@ {% from "coprs/detail/_builds_forms.html" import copr_build_cancel_form, copr_build_repeat_form, copr_build_delete_form %} {% from "coprs/detail/_build_states.html" import build_states %} {% from "_helpers.html" import build_href_from_sql, build_state, initialize_datatables, copr_url %} +{% from "_helpers.html" import pagination_form with context %} -{% macro builds_table(builds, print_possible_states=True) %} +{% macro builds_table(builds, print_possible_states=True, serverside_pagination=None) %} {% for build in builds %} {% if loop.first %} + {% if not serverside_pagination %} - + {% endif %} +
@@ -62,7 +65,11 @@ {% if loop.last %}
Build ID
- {{ initialize_datatables() }} + {% if not serverside_pagination %} + {{ initialize_datatables() }} + {% else %} + {{ pagination_form(serverside_pagination) }} + {% endif %} {% if print_possible_states %} {{ build_states() }} {% endif %} diff --git a/frontend/coprs_frontend/coprs/templates/coprs/detail/builds.html b/frontend/coprs_frontend/coprs/templates/coprs/detail/builds.html index 700707c..21736a1 100644 --- a/frontend/coprs_frontend/coprs/templates/coprs/detail/builds.html +++ b/frontend/coprs_frontend/coprs/templates/coprs/detail/builds.html @@ -57,6 +57,10 @@ {% endif %} + {% if builds.items %} + {{ builds_table(builds.items, serverside_pagination=builds) }} + {% else %} {{ builds_table(builds) }} + {% endif %} {% endblock %} diff --git a/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_builds.py b/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_builds.py index 9e9392d..6f8bfcc 100644 --- a/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_builds.py +++ b/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_builds.py @@ -1,18 +1,25 @@ import flask from flask import request, render_template, stream_with_context +from sqlalchemy import desc from copr_common.enums import StatusEnum from coprs import app from coprs import db from coprs import forms from coprs import helpers +from coprs import models from coprs.logic import builds_logic from coprs.logic.builds_logic import BuildsLogic from coprs.logic.complex_logic import ComplexLogic from coprs.logic.coprs_logic import CoprDirsLogic -from coprs.views.misc import (login_required, req_with_copr, send_build_icon) +from coprs.views.misc import ( + login_required, + req_with_copr, + req_with_pagination, + send_build_icon, +) from coprs.views.coprs_ns import coprs_ns from coprs.exceptions import ( @@ -52,14 +59,38 @@ def render_copr_build(build_id, copr): ################################ Build table ################################ +@coprs_ns.route("///builds/", methods=["POST"]) +@coprs_ns.route("/g///builds/", methods=["POST"]) +def copr_builds_redirect(**_kwargs): + """ + Redirect the current page to the same page with changed ?page= argument + """ + to_page = flask.request.form.get('go_to_page', 1) + return flask.redirect(helpers.current_url(page=to_page)) + + @coprs_ns.route("///builds/") @coprs_ns.route("/g///builds/") @req_with_copr -def copr_builds(copr): +@req_with_pagination +def copr_builds(copr, page=1): + flashes = flask.session.pop('_flashes', []) dirname = flask.request.args.get('dirname') builds_query = builds_logic.BuildsLogic.get_copr_builds_list(copr, dirname) - builds = builds_query.yield_per(1000) + + one_js_page_limit = 10000 + if builds_query.count() > one_js_page_limit: + # we currently don't support filtering with server-side pagination, + # so order the query so the newest builds are shown first + builds_query = builds_query.order_by(desc(models.Build.id)) + builds = builds_query.paginate( + page=page, + per_page=50, + ) + else: + builds = builds_query.yield_per(1000) + dirs = CoprDirsLogic.get_all_with_latest_submitted_build(copr.id) response = flask.Response(stream_with_context(helpers.stream_template("coprs/detail/builds.html", diff --git a/frontend/coprs_frontend/coprs/views/misc.py b/frontend/coprs_frontend/coprs/views/misc.py index 30debe5..7e3376a 100644 --- a/frontend/coprs_frontend/coprs/views/misc.py +++ b/frontend/coprs_frontend/coprs/views/misc.py @@ -21,6 +21,7 @@ from coprs import oid from coprs.logic.complex_logic import ComplexLogic from coprs.logic.users_logic import UsersLogic from coprs.logic.coprs_logic import CoprsLogic +from coprs.exceptions import ObjectNotFound def create_user_wrapper(username, email, timezone=None): @@ -396,3 +397,19 @@ def send_build_icon(build, no_cache=False): if no_cache: response.headers['Cache-Control'] = 'public, max-age=60' return response + + + +def req_with_pagination(f): + """ + Parse 'page=' option from GET url, and place it as the argument + """ + @wraps(f) + def wrapper(*args, **kwargs): + try: + page = flask.request.args.get('page', 1) + page = int(page) + except ValueError as err: + raise ObjectNotFound("Invalid pagination format") from err + return f(*args, page=page, **kwargs) + return wrapper