diff --git a/atst/domain/audit_log.py b/atst/domain/audit_log.py index 238cff2e..b5f8522c 100644 --- a/atst/domain/audit_log.py +++ b/atst/domain/audit_log.py @@ -8,8 +8,9 @@ class AuditEventQuery(Query): model = AuditEvent @classmethod - def get_all(cls): - return db.session.query(cls.model).order_by(cls.model.time_created.desc()).all() + def get_all(cls, pagination_opts): + query = db.session.query(cls.model).order_by(cls.model.time_created.desc()) + return cls.paginate(query, pagination_opts) class AuditLog(object): @@ -28,11 +29,11 @@ class AuditLog(object): return cls._log(resource=resource, action=action) @classmethod - def get_all_events(cls, user): + def get_all_events(cls, user, pagination_opts=None): Authorization.check_atat_permission( user, Permissions.VIEW_AUDIT_LOG, "view audit log" ) - return AuditEventQuery.get_all() + return AuditEventQuery.get_all(pagination_opts) @classmethod def _resource_type(cls, resource): diff --git a/atst/domain/common/query.py b/atst/domain/common/query.py index 4f55d6c0..07db46ef 100644 --- a/atst/domain/common/query.py +++ b/atst/domain/common/query.py @@ -5,6 +5,39 @@ from atst.domain.exceptions import NotFoundError from atst.database import db +class Paginator(object): + """ + Uses the Flask-SQLAlchemy extension's pagination method to paginate + a query set. + + Also acts as a proxy object so that the results of the query set can be iterated + over without needing to call `.items`. + """ + + def __init__(self, query_set): + self.query_set = query_set + + @classmethod + def paginate(cls, query, pagination_opts=None): + if pagination_opts is not None: + return cls( + query.paginate( + page=pagination_opts["page"], per_page=pagination_opts["per_page"] + ) + ) + else: + return query.all() + + def __getattr__(self, name): + return getattr(self.query_set, name) + + def __iter__(self): + return self.items.__iter__() + + def __len__(self): + return self.items.__len__() + + class Query(object): model = None @@ -35,3 +68,7 @@ class Query(object): db.session.add(resource) db.session.commit() return resource + + @classmethod + def paginate(cls, query, pagination_opts): + return Paginator.paginate(query, pagination_opts) diff --git a/atst/filters.py b/atst/filters.py index c9aa682f..c172a3df 100644 --- a/atst/filters.py +++ b/atst/filters.py @@ -76,6 +76,16 @@ def dateFromString(value, formatter="%m/%Y"): return datetime.datetime.strptime(value, formatter) +def pageWindow(pagination, size=2): + page = pagination.page + num_pages = pagination.pages + + over = max(0, page + size - num_pages) + under = min(0, page - size - 1) + + return (max(1, (page - size) - over), min(num_pages, (page + size) - under)) + + def register_filters(app): app.jinja_env.filters["iconSvg"] = iconSvg app.jinja_env.filters["dollars"] = dollars @@ -87,3 +97,4 @@ def register_filters(app): app.jinja_env.filters["renderList"] = renderList app.jinja_env.filters["formattedDate"] = formattedDate app.jinja_env.filters["dateFromString"] = dateFromString + app.jinja_env.filters["pageWindow"] = pageWindow diff --git a/atst/routes/__init__.py b/atst/routes/__init__.py index 780ff0c3..9fd690a9 100644 --- a/atst/routes/__init__.py +++ b/atst/routes/__init__.py @@ -121,9 +121,17 @@ def logout(): return redirect(url_for(".root")) +def get_pagination_opts(request, default_page=1, default_per_page=100): + return { + "page": int(request.args.get("page", default_page)), + "per_page": int(request.args.get("perPage", default_per_page)), + } + + @bp.route("/activity-history") def activity_history(): - audit_events = AuditLog.get_all_events(g.current_user) + pagination_opts = get_pagination_opts(request) + audit_events = AuditLog.get_all_events(g.current_user, pagination_opts) return render_template("audit_log.html", audit_events=audit_events) diff --git a/styles/components/_audit_log.scss b/styles/components/_audit_log.scss index 212ebb83..408f8f6b 100644 --- a/styles/components/_audit_log.scss +++ b/styles/components/_audit_log.scss @@ -20,3 +20,15 @@ margin-bottom: $gap; } } + +.pagination { + width: 80%; + display: flex; + margin: auto; + flex-direction: row; + flex-wrap: wrap; +} + +.page { + margin: .5em; +} diff --git a/templates/audit_log.html b/templates/audit_log.html index 7010c0ad..8455276f 100644 --- a/templates/audit_log.html +++ b/templates/audit_log.html @@ -1,4 +1,5 @@ {% extends "base.html" %} +{% from "components/pagination.html" import Pagination %} {% block content %} @@ -40,5 +41,5 @@ - + {{ Pagination(audit_events, 'atst.activity_history') }} {% endblock %} diff --git a/templates/components/pagination.html b/templates/components/pagination.html new file mode 100644 index 00000000..5b1c84a9 --- /dev/null +++ b/templates/components/pagination.html @@ -0,0 +1,48 @@ +{% macro Page(pagination, route, i, label=None, disabled=False) -%} + {% set label = label or i %} + + {% set button_class = "page usa-button " %} + + {% if disabled %} + {% set button_class = button_class + "usa-button-disabled" %} + {% elif i == pagination.page %} + {% set button_class = button_class + "usa-button-primary" %} + {% else %} + {% set button_class = button_class + "usa-button-secondary" %} + {% endif %} + + {{ label }} +{%- endmacro %} + +{% macro Pagination(pagination, route) -%} + +{%- endmacro %} diff --git a/tests/domain/test_audit_log.py b/tests/domain/test_audit_log.py index 1a2d95f7..ab03c48c 100644 --- a/tests/domain/test_audit_log.py +++ b/tests/domain/test_audit_log.py @@ -22,3 +22,12 @@ def test_non_admin_cannot_view_audit_log(developer): def test_ccpo_can_iview_audit_log(ccpo): AuditLog.get_all_events(ccpo) + + +def test_paginate_audit_log(ccpo): + user = UserFactory.create() + for _ in range(100): + AuditLog.log_system_event(user, action="create") + + events = AuditLog.get_all_events(ccpo, pagination_opts={"per_page": 25, "page": 2}) + assert len(events) == 25