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())