Pagination in the admin

This commit is contained in:
Thomas Sileo 2022-06-28 20:10:25 +02:00
parent 489ed6cbe0
commit d4c80dedeb
8 changed files with 112 additions and 29 deletions

View file

@ -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,
},
)

View file

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

View file

@ -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 %}
<p><a href="{{ url_for("admin_inbox") }}?cursor={{ next_cursor }}{% if request.query_params.filter_by %}&filter_by={{ request.query_params.filter_by }}{% endif %}">See more</a></p>
{% endif %}
{% endblock %}

View file

@ -2,17 +2,7 @@
{% extends "layout.html" %}
{% block content %}
<p>Filter by
{% for ap_type in ["Note", "Like", "Announce", "Follow"] %}
<a style="margin-right:12px;" href="{{ url_for("admin_outbox") }}?filter_by={{ ap_type }}">
{% if request.query_params.filter_by == ap_type %}
<strong>{{ ap_type }}</strong>
{% else %}
{{ ap_type }}
{% endif %}</a>
{% endfor %}.
{% if request.query_params.filter_by %}<a href="{{ url_for("admin_outbox") }}">Reset filter</a>{% endif %}</p>
</p>
{{ utils.display_box_filters("admin_outbox") }}
{% for outbox_object in outbox %}
@ -33,4 +23,8 @@
{% endfor %}
{% if next_cursor %}
<p><a href="{{ url_for("admin_outbox") }}?cursor={{ next_cursor }}{% if request.query_params.filter_by %}&filter_by={{ request.query_params.filter_by }}{% endif %}">See more</a></p>
{% endif %}
{% endblock %}

View file

@ -8,5 +8,15 @@
<li>{{ utils.display_actor(follower.actor, actors_metadata) }}</li>
{% endfor %}
</ul>
{% set x_more = followers_count - followers | length %}
{% if x_more > 0 %}
<p>And {{ x_more }} more.</p>
{% endif %}
{% if is_admin %}
<p><a href="{{ url_for("admin_inbox") }}?filter_by=Follow">Manage followers</a></p>
{% endif %}
</div>
{% endblock %}

View file

@ -8,5 +8,15 @@
<li>{{ utils.display_actor(follow.actor, actors_metadata) }}</li>
{% endfor %}
</ul>
{% set x_more = following_count - following | length %}
{% if x_more > 0 %}
<p>And {{ x_more }} more.</p>
{% endif %}
{% if is_admin %}
<p><a href="{{ url_for("admin_outbox") }}?filter_by=Follow">Manage follows</a></p>
{% endif %}
</div>
{% endblock %}

View file

@ -109,6 +109,20 @@
</form>
{% endmacro %}
{% macro display_box_filters(route) %}
<p>Filter by
{% for ap_type in ["Note", "Like", "Announce", "Follow"] %}
<a style="margin-right:12px;" href="{{ url_for(route) }}?filter_by={{ ap_type }}">
{% if request.query_params.filter_by == ap_type %}
<strong>{{ ap_type }}</strong>
{% else %}
{{ ap_type }}
{% endif %}</a>
{% endfor %}.
{% if request.query_params.filter_by %}<a href="{{ url_for(route) }}">Reset filter</a>{% endif %}</p>
</p>
{% endmacro %}
{% macro display_actor(actor, actors_metadata) %}
{% set metadata = actors_metadata.get(actor.ap_id) %}
<div style="display: flex;column-gap: 20px;margin:20px 0 10px 0;" class="actor-box h-card p-author">

12
app/utils/pagination.py Normal file
View file

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