Pagination in the admin
This commit is contained in:
parent
489ed6cbe0
commit
d4c80dedeb
8 changed files with 112 additions and 29 deletions
50
app/admin.py
50
app/admin.py
|
@ -26,6 +26,7 @@ from app.config import verify_password
|
||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
from app.lookup import lookup
|
from app.lookup import lookup
|
||||||
from app.uploads import save_upload
|
from app.uploads import save_upload
|
||||||
|
from app.utils import pagination
|
||||||
from app.utils.emoji import EMOJIS_BY_NAME
|
from app.utils.emoji import EMOJIS_BY_NAME
|
||||||
|
|
||||||
|
|
||||||
|
@ -165,10 +166,25 @@ def admin_bookmarks(
|
||||||
def admin_inbox(
|
def admin_inbox(
|
||||||
request: Request,
|
request: Request,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
|
filter_by: str | None = None,
|
||||||
|
cursor: str | None = None,
|
||||||
) -> templates.TemplateResponse:
|
) -> 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 = (
|
inbox = (
|
||||||
db.query(models.InboxObject)
|
q.options(
|
||||||
.options(
|
|
||||||
joinedload(models.InboxObject.relates_to_inbox_object),
|
joinedload(models.InboxObject.relates_to_inbox_object),
|
||||||
joinedload(models.InboxObject.relates_to_outbox_object),
|
joinedload(models.InboxObject.relates_to_outbox_object),
|
||||||
)
|
)
|
||||||
|
@ -176,25 +192,43 @@ def admin_inbox(
|
||||||
.limit(20)
|
.limit(20)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
next_cursor = (
|
||||||
|
pagination.encode_cursor(inbox[-1].ap_published_at)
|
||||||
|
if inbox and remaining_count > page_size
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
return templates.render_template(
|
return templates.render_template(
|
||||||
db,
|
db,
|
||||||
request,
|
request,
|
||||||
"admin_inbox.html",
|
"admin_inbox.html",
|
||||||
{
|
{
|
||||||
"inbox": inbox,
|
"inbox": inbox,
|
||||||
|
"next_cursor": next_cursor,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/outbox")
|
@router.get("/outbox")
|
||||||
def admin_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:
|
) -> templates.TemplateResponse:
|
||||||
q = db.query(models.OutboxObject).filter(
|
q = db.query(models.OutboxObject).filter(
|
||||||
models.OutboxObject.ap_type.not_in(["Accept"])
|
models.OutboxObject.ap_type.not_in(["Accept"])
|
||||||
)
|
)
|
||||||
if filter_by:
|
if filter_by:
|
||||||
q = q.filter(models.OutboxObject.ap_type == 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 = (
|
outbox = (
|
||||||
q.options(
|
q.options(
|
||||||
|
@ -203,9 +237,16 @@ def admin_outbox(
|
||||||
joinedload(models.OutboxObject.relates_to_actor),
|
joinedload(models.OutboxObject.relates_to_actor),
|
||||||
)
|
)
|
||||||
.order_by(models.OutboxObject.ap_published_at.desc())
|
.order_by(models.OutboxObject.ap_published_at.desc())
|
||||||
.limit(20)
|
.limit(page_size)
|
||||||
.all()
|
.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(
|
actors_metadata = get_actors_metadata(
|
||||||
db,
|
db,
|
||||||
[
|
[
|
||||||
|
@ -222,6 +263,7 @@ def admin_outbox(
|
||||||
{
|
{
|
||||||
"actors_metadata": actors_metadata,
|
"actors_metadata": actors_metadata,
|
||||||
"outbox": outbox,
|
"outbox": outbox,
|
||||||
|
"next_cursor": next_cursor,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
23
app/main.py
23
app/main.py
|
@ -2,14 +2,12 @@ import base64
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
|
||||||
from datetime import timezone
|
from datetime import timezone
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from dateutil.parser import isoparse
|
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi import Form
|
from fastapi import Form
|
||||||
|
@ -52,6 +50,7 @@ from app.config import verify_csrf_token
|
||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
from app.templates import is_current_user_admin
|
from app.templates import is_current_user_admin
|
||||||
from app.uploads import UPLOAD_DIR
|
from app.uploads import UPLOAD_DIR
|
||||||
|
from app.utils import pagination
|
||||||
from app.utils.emoji import EMOJIS_BY_NAME
|
from app.utils.emoji import EMOJIS_BY_NAME
|
||||||
from app.webfinger import get_remote_follow_template
|
from app.webfinger import get_remote_follow_template
|
||||||
|
|
||||||
|
@ -154,7 +153,7 @@ def index(
|
||||||
models.OutboxObject.is_hidden_from_homepage.is_(False),
|
models.OutboxObject.is_hidden_from_homepage.is_(False),
|
||||||
)
|
)
|
||||||
total_count = q.count()
|
total_count = q.count()
|
||||||
page_size = 2
|
page_size = 20
|
||||||
page_offset = (page - 1) * page_size
|
page_offset = (page - 1) * page_size
|
||||||
|
|
||||||
outbox_objects = (
|
outbox_objects = (
|
||||||
|
@ -203,7 +202,9 @@ def _build_followx_collection(
|
||||||
|
|
||||||
q = db.query(model_cls).order_by(model_cls.created_at.desc()) # type: ignore
|
q = db.query(model_cls).order_by(model_cls.created_at.desc()) # type: ignore
|
||||||
if next_cursor:
|
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)
|
q = q.limit(20)
|
||||||
|
|
||||||
items = [followx for followx in q.all()]
|
items = [followx for followx in q.all()]
|
||||||
|
@ -215,7 +216,7 @@ def _build_followx_collection(
|
||||||
.count()
|
.count()
|
||||||
> 0
|
> 0
|
||||||
):
|
):
|
||||||
next_cursor = _encode_cursor(items[-1].created_at)
|
next_cursor = pagination.encode_cursor(items[-1].created_at)
|
||||||
|
|
||||||
collection_page = {
|
collection_page = {
|
||||||
"@context": ap.AS_CTX,
|
"@context": ap.AS_CTX,
|
||||||
|
@ -234,14 +235,6 @@ def _build_followx_collection(
|
||||||
return collection_page
|
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")
|
@app.get("/followers")
|
||||||
def followers(
|
def followers(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
@ -262,6 +255,7 @@ def followers(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# We only show the most recent 20 followers on the public website
|
||||||
followers = (
|
followers = (
|
||||||
db.query(models.Follower)
|
db.query(models.Follower)
|
||||||
.options(joinedload(models.Follower.actor))
|
.options(joinedload(models.Follower.actor))
|
||||||
|
@ -270,7 +264,6 @@ def followers(
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: support next_cursor/prev_cursor
|
|
||||||
actors_metadata = {}
|
actors_metadata = {}
|
||||||
if is_current_user_admin(request):
|
if is_current_user_admin(request):
|
||||||
actors_metadata = get_actors_metadata(
|
actors_metadata = get_actors_metadata(
|
||||||
|
@ -309,6 +302,7 @@ def following(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# We only show the most recent 20 follows on the public website
|
||||||
q = (
|
q = (
|
||||||
db.query(models.Following)
|
db.query(models.Following)
|
||||||
.options(joinedload(models.Following.actor))
|
.options(joinedload(models.Following.actor))
|
||||||
|
@ -341,6 +335,7 @@ def outbox(
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
|
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
|
||||||
) -> ActivityPubResponse:
|
) -> ActivityPubResponse:
|
||||||
|
# By design, we only show the last 20 public activities in the oubox
|
||||||
outbox_objects = (
|
outbox_objects = (
|
||||||
db.query(models.OutboxObject)
|
db.query(models.OutboxObject)
|
||||||
.filter(
|
.filter(
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
{{ utils.display_box_filters("admin_inbox") }}
|
||||||
|
|
||||||
{% for inbox_object in inbox %}
|
{% for inbox_object in inbox %}
|
||||||
{% if inbox_object.ap_type == "Announce" %}
|
{% if inbox_object.ap_type == "Announce" %}
|
||||||
{{ utils.display_object(inbox_object.relates_to_anybox_object) }}
|
{{ utils.display_object(inbox_object.relates_to_anybox_object) }}
|
||||||
|
@ -14,4 +16,8 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% 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 %}
|
{% endblock %}
|
||||||
|
|
|
@ -2,17 +2,7 @@
|
||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<p>Filter by
|
{{ utils.display_box_filters("admin_outbox") }}
|
||||||
{% 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>
|
|
||||||
|
|
||||||
{% for outbox_object in outbox %}
|
{% for outbox_object in outbox %}
|
||||||
|
|
||||||
|
@ -33,4 +23,8 @@
|
||||||
|
|
||||||
{% endfor %}
|
{% 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 %}
|
{% endblock %}
|
||||||
|
|
|
@ -8,5 +8,15 @@
|
||||||
<li>{{ utils.display_actor(follower.actor, actors_metadata) }}</li>
|
<li>{{ utils.display_actor(follower.actor, actors_metadata) }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</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>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -8,5 +8,15 @@
|
||||||
<li>{{ utils.display_actor(follow.actor, actors_metadata) }}</li>
|
<li>{{ utils.display_actor(follow.actor, actors_metadata) }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</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>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -109,6 +109,20 @@
|
||||||
</form>
|
</form>
|
||||||
{% endmacro %}
|
{% 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) %}
|
{% macro display_actor(actor, actors_metadata) %}
|
||||||
{% set metadata = actors_metadata.get(actor.ap_id) %}
|
{% 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">
|
<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
12
app/utils/pagination.py
Normal 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())
|
Loading…
Reference in a new issue