Skip to content

Commit 9b5f8f9

Browse files
committed
Add ListUserUploads admin RPC to list a user's photo uploads
Lets admins fetch all photo uploads created by a given user, with cursor-based pagination ordered newest-first.
1 parent 983963a commit 9b5f8f9

3 files changed

Lines changed: 128 additions & 2 deletions

File tree

app/backend/src/couchers/servicers/admin.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import grpc
66
from google.protobuf import empty_pb2
7-
from sqlalchemy import select
7+
from sqlalchemy import select, tuple_
88
from sqlalchemy.orm import Session, aliased, selectinload
99
from sqlalchemy.sql import and_, func, or_
1010
from user_agents import parse as user_agents_parse
@@ -43,7 +43,7 @@
4343
UserBadge,
4444
)
4545
from couchers.models.notifications import NotificationTopicAction
46-
from couchers.models.uploads import has_avatar_photo_expression
46+
from couchers.models.uploads import Upload, has_avatar_photo_expression
4747
from couchers.notifications.notify import notify
4848
from couchers.proto import admin_pb2, admin_pb2_grpc, api_pb2, notification_data_pb2
4949
from couchers.proto.internal import jobs_pb2
@@ -1194,3 +1194,40 @@ def ListAdminActions(
11941194
admin_actions=action_pbs,
11951195
next_page_token=str(rows[page_size - 1][0].id) if len(rows) > page_size else None,
11961196
)
1197+
1198+
def ListUserUploads(
1199+
self, request: admin_pb2.ListUserUploadsReq, context: CouchersContext, session: Session
1200+
) -> admin_pb2.ListUserUploadsRes:
1201+
user = session.execute(select(User).where(username_or_email_or_id(request.user))).scalar_one_or_none()
1202+
if not user:
1203+
context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "user_not_found")
1204+
1205+
page_size = min(MAX_PAGINATION_LENGTH, request.page_size or MAX_PAGINATION_LENGTH)
1206+
1207+
statement = select(Upload).where(Upload.creator_user_id == user.id)
1208+
if request.page_token:
1209+
cursor_created = session.execute(
1210+
select(Upload.created).where(Upload.key == request.page_token)
1211+
).scalar_one()
1212+
statement = statement.where(tuple_(Upload.created, Upload.key) < (cursor_created, request.page_token))
1213+
1214+
uploads = (
1215+
session.execute(statement.order_by(Upload.created.desc(), Upload.key.desc()).limit(page_size + 1))
1216+
.scalars()
1217+
.all()
1218+
)
1219+
1220+
return admin_pb2.ListUserUploadsRes(
1221+
uploads=[
1222+
admin_pb2.UserUpload(
1223+
key=upload.key,
1224+
filename=upload.filename,
1225+
full_url=upload.full_url,
1226+
thumbnail_url=upload.thumbnail_url,
1227+
credit=upload.credit or "",
1228+
created=Timestamp_from_datetime(upload.created),
1229+
)
1230+
for upload in uploads[:page_size]
1231+
],
1232+
next_page_token=uploads[page_size - 1].key if len(uploads) > page_size else None,
1233+
)

app/backend/src/tests/test_admin.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
ModerationUserList,
1919
ModerationVisibility,
2020
Reference,
21+
Upload,
2122
User,
2223
UserActivity,
2324
UserSession,
@@ -1511,6 +1512,70 @@ def test_ListAdminActions_pagination(db):
15111512
assert set(all_notes) == {"note 0", "note 1", "note 2"}
15121513

15131514

1515+
def test_ListUserUploads(db):
1516+
super_user, super_token = generate_user(is_superuser=True)
1517+
user, _ = generate_user(complete_profile=False)
1518+
other_user, _ = generate_user()
1519+
1520+
with session_scope() as session:
1521+
for i in range(3):
1522+
session.add(
1523+
Upload(
1524+
key=f"key{i}",
1525+
filename=f"photo{i}.jpg",
1526+
creator_user_id=user.id,
1527+
credit=f"credit {i}" if i == 0 else None,
1528+
)
1529+
)
1530+
session.add(Upload(key="other_key", filename="other.jpg", creator_user_id=other_user.id))
1531+
1532+
with real_admin_session(super_token) as api:
1533+
res = api.ListUserUploads(admin_pb2.ListUserUploadsReq(user=user.username))
1534+
1535+
assert len(res.uploads) == 3
1536+
assert res.next_page_token == ""
1537+
assert {u.filename for u in res.uploads} == {"photo0.jpg", "photo1.jpg", "photo2.jpg"}
1538+
1539+
upload0 = next(u for u in res.uploads if u.key == "key0")
1540+
assert upload0.credit == "credit 0"
1541+
assert upload0.full_url.endswith("/img/full/photo0.jpg")
1542+
assert upload0.thumbnail_url.endswith("/img/thumbnail/photo0.jpg")
1543+
assert upload0.HasField("created")
1544+
1545+
1546+
def test_ListUserUploads_pagination(db):
1547+
super_user, super_token = generate_user(is_superuser=True)
1548+
user, _ = generate_user(complete_profile=False)
1549+
1550+
with session_scope() as session:
1551+
for i in range(3):
1552+
session.add(Upload(key=f"key{i}", filename=f"photo{i}.jpg", creator_user_id=user.id))
1553+
1554+
with real_admin_session(super_token) as api:
1555+
res = api.ListUserUploads(admin_pb2.ListUserUploadsReq(user=user.username, page_size=2))
1556+
assert len(res.uploads) == 2
1557+
assert res.next_page_token != ""
1558+
first_page_keys = [u.key for u in res.uploads]
1559+
1560+
res2 = api.ListUserUploads(
1561+
admin_pb2.ListUserUploadsReq(user=user.username, page_size=2, page_token=res.next_page_token)
1562+
)
1563+
assert len(res2.uploads) == 1
1564+
assert res2.next_page_token == ""
1565+
1566+
all_keys = first_page_keys + [u.key for u in res2.uploads]
1567+
assert set(all_keys) == {"key0", "key1", "key2"}
1568+
1569+
1570+
def test_ListUserUploads_not_found(db):
1571+
super_user, super_token = generate_user(is_superuser=True)
1572+
1573+
with real_admin_session(super_token) as api:
1574+
with pytest.raises(grpc.RpcError) as e:
1575+
api.ListUserUploads(admin_pb2.ListUserUploadsReq(user="nonexistent"))
1576+
assert e.value.code() == grpc.StatusCode.NOT_FOUND
1577+
1578+
15141579
# community invite feature tested in test_events.py
15151580
# SendBlogPostNotification tested in test_notifications.py
15161581
# MarkUserNeedsLocationUpdate tested in test_jail.py

app/proto/admin.proto

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ service Admin {
105105
rpc SetModScore(SetModScoreReq) returns (UserDetails) {}
106106

107107
rpc ListAdminActions(ListAdminActionsReq) returns (ListAdminActionsRes) {}
108+
109+
rpc ListUserUploads(ListUserUploadsReq) returns (ListUserUploadsRes) {}
108110
}
109111

110112
message AdminActionLog {
@@ -574,3 +576,25 @@ message ListAdminActionsRes {
574576
repeated AdminActionLog admin_actions = 1;
575577
string next_page_token = 2;
576578
}
579+
580+
message ListUserUploadsReq {
581+
// username, email, or user id
582+
string user = 1;
583+
584+
uint32 page_size = 2;
585+
string page_token = 3;
586+
}
587+
588+
message UserUpload {
589+
string key = 1;
590+
string filename = 2;
591+
string full_url = 3;
592+
string thumbnail_url = 4;
593+
string credit = 5;
594+
google.protobuf.Timestamp created = 6;
595+
}
596+
597+
message ListUserUploadsRes {
598+
repeated UserUpload uploads = 1;
599+
string next_page_token = 2;
600+
}

0 commit comments

Comments
 (0)