From d4c80dedeb25f085d284e2c6c15e8a36ddb339cb Mon Sep 17 00:00:00 2001 From: Thomas Sileo Date: Tue, 28 Jun 2022 20:10:25 +0200 Subject: [PATCH] Pagination in the admin --- app/admin.py | 50 ++++++++++++++++++++++++++++++--- app/main.py | 23 ++++++--------- app/templates/admin_inbox.html | 6 ++++ app/templates/admin_outbox.html | 16 ++++------- app/templates/followers.html | 10 +++++++ app/templates/following.html | 10 +++++++ app/templates/utils.html | 14 +++++++++ app/utils/pagination.py | 12 ++++++++ 8 files changed, 112 insertions(+), 29 deletions(-) create mode 100644 app/utils/pagination.py diff --git a/app/admin.py b/app/admin.py index 929f93d..e2057b8 100644 --- a/app/admin.py +++ b/app/admin.py @@ -26,6 +26,7 @@ from app.config import verify_password from app.database import get_db from app.lookup import lookup from app.uploads import save_upload +from app.utils import pagination from app.utils.emoji import EMOJIS_BY_NAME @@ -165,10 +166,25 @@ def admin_bookmarks( def admin_inbox( request: Request, db: Session = Depends(get_db), + filter_by: str | None = None, + cursor: str | None = None, ) -> templates.TemplateResponse: + q = db.query(models.InboxObject).filter( + models.InboxObject.ap_type.not_in(["Accept"]) + ) + + if filter_by: + q = q.filter(models.InboxObject.ap_type == filter_by) + if cursor: + q = q.filter( + models.InboxObject.ap_published_at < pagination.decode_cursor(cursor) + ) + + page_size = 20 + remaining_count = q.count() + inbox = ( - db.query(models.InboxObject) - .options( + q.options( joinedload(models.InboxObject.relates_to_inbox_object), joinedload(models.InboxObject.relates_to_outbox_object), ) @@ -176,25 +192,43 @@ def admin_inbox( .limit(20) .all() ) + + next_cursor = ( + pagination.encode_cursor(inbox[-1].ap_published_at) + if inbox and remaining_count > page_size + else None + ) + return templates.render_template( db, request, "admin_inbox.html", { "inbox": inbox, + "next_cursor": next_cursor, }, ) @router.get("/outbox") def admin_outbox( - request: Request, db: Session = Depends(get_db), filter_by: str | None = None + request: Request, + db: Session = Depends(get_db), + filter_by: str | None = None, + cursor: str | None = None, ) -> templates.TemplateResponse: q = db.query(models.OutboxObject).filter( models.OutboxObject.ap_type.not_in(["Accept"]) ) if filter_by: q = q.filter(models.OutboxObject.ap_type == filter_by) + if cursor: + q = q.filter( + models.OutboxObject.ap_published_at < pagination.decode_cursor(cursor) + ) + + page_size = 20 + remaining_count = q.count() outbox = ( q.options( @@ -203,9 +237,16 @@ def admin_outbox( joinedload(models.OutboxObject.relates_to_actor), ) .order_by(models.OutboxObject.ap_published_at.desc()) - .limit(20) + .limit(page_size) .all() ) + + next_cursor = ( + pagination.encode_cursor(outbox[-1].ap_published_at) + if outbox and remaining_count > page_size + else None + ) + actors_metadata = get_actors_metadata( db, [ @@ -222,6 +263,7 @@ def admin_outbox( { "actors_metadata": actors_metadata, "outbox": outbox, + "next_cursor": next_cursor, }, ) diff --git a/app/main.py b/app/main.py index 4f60e57..2dfc38c 100644 --- a/app/main.py +++ b/app/main.py @@ -2,14 +2,12 @@ import base64 import os import sys import time -from datetime import datetime from datetime import timezone from io import BytesIO from typing import Any from typing import Type import httpx -from dateutil.parser import isoparse from fastapi import Depends from fastapi import FastAPI from fastapi import Form @@ -52,6 +50,7 @@ from app.config import verify_csrf_token from app.database import get_db from app.templates import is_current_user_admin from app.uploads import UPLOAD_DIR +from app.utils import pagination from app.utils.emoji import EMOJIS_BY_NAME from app.webfinger import get_remote_follow_template @@ -154,7 +153,7 @@ def index( models.OutboxObject.is_hidden_from_homepage.is_(False), ) total_count = q.count() - page_size = 2 + page_size = 20 page_offset = (page - 1) * page_size outbox_objects = ( @@ -203,7 +202,9 @@ def _build_followx_collection( q = db.query(model_cls).order_by(model_cls.created_at.desc()) # type: ignore if next_cursor: - q = q.filter(model_cls.created_at < _decode_cursor(next_cursor)) # type: ignore + q = q.filter( + model_cls.created_at < pagination.decode_cursor(next_cursor) # type: ignore + ) q = q.limit(20) items = [followx for followx in q.all()] @@ -215,7 +216,7 @@ def _build_followx_collection( .count() > 0 ): - next_cursor = _encode_cursor(items[-1].created_at) + next_cursor = pagination.encode_cursor(items[-1].created_at) collection_page = { "@context": ap.AS_CTX, @@ -234,14 +235,6 @@ def _build_followx_collection( return collection_page -def _encode_cursor(val: datetime) -> str: - return base64.urlsafe_b64encode(val.isoformat().encode()).decode() - - -def _decode_cursor(cursor: str) -> datetime: - return isoparse(base64.urlsafe_b64decode(cursor).decode()) - - @app.get("/followers") def followers( request: Request, @@ -262,6 +255,7 @@ def followers( ) ) + # We only show the most recent 20 followers on the public website followers = ( db.query(models.Follower) .options(joinedload(models.Follower.actor)) @@ -270,7 +264,6 @@ def followers( .all() ) - # TODO: support next_cursor/prev_cursor actors_metadata = {} if is_current_user_admin(request): actors_metadata = get_actors_metadata( @@ -309,6 +302,7 @@ def following( ) ) + # We only show the most recent 20 follows on the public website q = ( db.query(models.Following) .options(joinedload(models.Following.actor)) @@ -341,6 +335,7 @@ def outbox( db: Session = Depends(get_db), _: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker), ) -> ActivityPubResponse: + # By design, we only show the last 20 public activities in the oubox outbox_objects = ( db.query(models.OutboxObject) .filter( diff --git a/app/templates/admin_inbox.html b/app/templates/admin_inbox.html index 8d02445..632b3f1 100644 --- a/app/templates/admin_inbox.html +++ b/app/templates/admin_inbox.html @@ -2,6 +2,8 @@ {% extends "layout.html" %} {% block content %} +{{ utils.display_box_filters("admin_inbox") }} + {% for inbox_object in inbox %} {% if inbox_object.ap_type == "Announce" %} {{ utils.display_object(inbox_object.relates_to_anybox_object) }} @@ -14,4 +16,8 @@ {% endif %} {% endfor %} +{% if next_cursor %} +

See more

+{% endif %} + {% endblock %} diff --git a/app/templates/admin_outbox.html b/app/templates/admin_outbox.html index 9f6a91b..51f1078 100644 --- a/app/templates/admin_outbox.html +++ b/app/templates/admin_outbox.html @@ -2,17 +2,7 @@ {% extends "layout.html" %} {% block content %} -

Filter by -{% for ap_type in ["Note", "Like", "Announce", "Follow"] %} - - {% if request.query_params.filter_by == ap_type %} - {{ ap_type }} - {% else %} - {{ ap_type }} - {% endif %} -{% endfor %}. -{% if request.query_params.filter_by %}Reset filter{% endif %}

-

+{{ utils.display_box_filters("admin_outbox") }} {% for outbox_object in outbox %} @@ -33,4 +23,8 @@ {% endfor %} +{% if next_cursor %} +

See more

+{% endif %} + {% endblock %} diff --git a/app/templates/followers.html b/app/templates/followers.html index 4a0429f..5728d1b 100644 --- a/app/templates/followers.html +++ b/app/templates/followers.html @@ -8,5 +8,15 @@
  • {{ utils.display_actor(follower.actor, actors_metadata) }}
  • {% endfor %} + +{% set x_more = followers_count - followers | length %} +{% if x_more > 0 %} +

    And {{ x_more }} more.

    +{% endif %} + +{% if is_admin %} +

    Manage followers

    +{% endif %} + {% endblock %} diff --git a/app/templates/following.html b/app/templates/following.html index be55eb5..7dacc4c 100644 --- a/app/templates/following.html +++ b/app/templates/following.html @@ -8,5 +8,15 @@
  • {{ utils.display_actor(follow.actor, actors_metadata) }}
  • {% endfor %} + +{% set x_more = following_count - following | length %} +{% if x_more > 0 %} +

    And {{ x_more }} more.

    +{% endif %} + +{% if is_admin %} +

    Manage follows

    +{% endif %} + {% endblock %} diff --git a/app/templates/utils.html b/app/templates/utils.html index b878272..bd3cef6 100644 --- a/app/templates/utils.html +++ b/app/templates/utils.html @@ -109,6 +109,20 @@ {% endmacro %} +{% macro display_box_filters(route) %} +

    Filter by +{% for ap_type in ["Note", "Like", "Announce", "Follow"] %} + + {% if request.query_params.filter_by == ap_type %} + {{ ap_type }} + {% else %} + {{ ap_type }} + {% endif %} +{% endfor %}. +{% if request.query_params.filter_by %}Reset filter{% endif %}

    +

    +{% endmacro %} + {% macro display_actor(actor, actors_metadata) %} {% set metadata = actors_metadata.get(actor.ap_id) %}
    diff --git a/app/utils/pagination.py b/app/utils/pagination.py new file mode 100644 index 0000000..3fe0551 --- /dev/null +++ b/app/utils/pagination.py @@ -0,0 +1,12 @@ +import base64 +from datetime import datetime + +from dateutil.parser import isoparse + + +def encode_cursor(val: datetime) -> str: + return base64.urlsafe_b64encode(val.isoformat().encode()).decode() + + +def decode_cursor(cursor: str) -> datetime: + return isoparse(base64.urlsafe_b64decode(cursor).decode())