Skip to content

Commit c6837f4

Browse files
Admin add-to-portal: retroactively associate an existing map with a portal
Portal membership is a Submission row, so association needs no tag mechanism: POST /api/submissions/admin/add inserts a submitted, live-referenced row (map_is_clone=false — takedown never demotes the author's working map) after the same require_portal_admin team check as every other admin action. One map can join any number of portals, each row moderated independently; a duplicate add 409s. The entry surfaces publicly only once the map's draft_status is past scratch, exactly like an auto-collected entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent eb5b810 commit c6837f4

3 files changed

Lines changed: 125 additions & 0 deletions

File tree

backend/app/submissions/main.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
NsfwUpdate,
6868
Submission,
6969
SubmissionAdmin,
70+
SubmissionAdminAdd,
7071
SubmissionContent,
7172
SubmissionCreate,
7273
SubmissionCreated,
@@ -731,3 +732,57 @@ async def set_submission_hidden(
731732
)
732733
session.commit()
733734
return {"id": submission_pk, "hidden": body.hidden}
735+
736+
737+
@router.post(
738+
"/admin/add", response_model=SubmissionCreated, status_code=status.HTTP_201_CREATED
739+
)
740+
async def admin_add_submission(
741+
data: SubmissionAdminAdd,
742+
session: Session = Depends(get_session),
743+
auth_result: dict = Security(auth.verify, scopes=[TokenScope.review_content]),
744+
):
745+
"""Retroactively associate an existing map with a portal.
746+
747+
Portal membership is a Submission row, so one map can join any number of
748+
portals — each row is hidden/blurred independently. The row references
749+
the LIVE map (map_is_clone=False, like auto-collection): no snapshot is
750+
taken, and takedown never demotes the author's working document. The
751+
entry surfaces in the public gallery only once the map's draft_status is
752+
past scratch, exactly like an auto-collected entry. No moderation task —
753+
there is no text content to score, and an admin vouched for the map.
754+
"""
755+
config = get_form_config(data.portal_id, session)
756+
require_portal_admin(auth_result, config)
757+
document = session.exec(
758+
select(Document).where(col(Document.public_id) == data.map_public_id)
759+
).first()
760+
if document is None:
761+
raise HTTPException(
762+
status_code=status.HTTP_404_NOT_FOUND,
763+
detail=f"Map {data.map_public_id} not found",
764+
)
765+
existing = session.exec(
766+
select(Submission).where(
767+
col(Submission.portal_id) == config.portal_id,
768+
col(Submission.map_public_id) == data.map_public_id,
769+
)
770+
).first()
771+
if existing is not None:
772+
raise HTTPException(
773+
status_code=status.HTTP_409_CONFLICT,
774+
detail=f"Map {data.map_public_id} already has a {existing.status} "
775+
f"submission in portal {config.portal_id!r}",
776+
)
777+
submission = Submission(
778+
portal_id=config.portal_id,
779+
map_public_id=data.map_public_id,
780+
tags=[config.portal_id],
781+
status=SubmissionStatus.submitted,
782+
submitted_at=datetime.now(timezone.utc),
783+
map_is_clone=False,
784+
)
785+
session.add(submission)
786+
session.commit()
787+
session.refresh(submission)
788+
return SubmissionCreated(id=submission.id, submission_id=submission.submission_id)

backend/app/submissions/models.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,13 @@ class FlagSubmissionRequest(BaseModel):
369369
id: int
370370

371371

372+
class SubmissionAdminAdd(BaseModel):
373+
"""Body for retroactively associating an existing map with a portal."""
374+
375+
portal_id: str
376+
map_public_id: int
377+
378+
372379
class NsfwUpdate(BaseModel):
373380
nsfw: bool
374381

backend/tests/test_submissions.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,3 +1037,66 @@ def test_custom_textarea_length_cap(self, client):
10371037
response = _submit(client, fields={**base, "custom_story": "x" * 5001})
10381038
assert response.status_code == 422
10391039
assert any("custom_story" in e for e in response.json()["detail"])
1040+
1041+
1042+
# ---------------------------------------------------------------------------
1043+
# Admin add-to-portal (retroactive association)
1044+
# ---------------------------------------------------------------------------
1045+
1046+
1047+
class TestAdminAdd:
1048+
"""Retroactive portal membership is a Submission row: team-scoped like
1049+
every other admin action, duplicate-safe, and live-referenced (no clone,
1050+
so takedown can never demote the author's working map)."""
1051+
1052+
def _add(self, client, public_id, portal_id=PORTAL):
1053+
return client.post(
1054+
"/api/submissions/admin/add",
1055+
json={"portal_id": portal_id, "map_public_id": public_id},
1056+
)
1057+
1058+
def _public_id(self, client, document_id):
1059+
return client.get(f"/api/document/{document_id}").json()["public_id"]
1060+
1061+
def test_scoped_admin_adds_live_reference(
1062+
self, client, form_config, document_id, session
1063+
):
1064+
public_id = self._public_id(client, document_id)
1065+
_set_auth(TEAM_A_PAYLOAD)
1066+
response = self._add(client, public_id)
1067+
assert response.status_code == 201, response.json()
1068+
submission = session.get(Submission, response.json()["id"])
1069+
assert submission.status == "submitted"
1070+
# The LIVE map, not a snapshot: no clone row, no demotable copy.
1071+
assert submission.map_public_id == public_id
1072+
assert submission.map_is_clone is False
1073+
assert submission.tags == [PORTAL]
1074+
1075+
def test_one_map_can_join_multiple_portals(
1076+
self, client, form_config, document_id, session
1077+
):
1078+
public_id = self._public_id(client, document_id)
1079+
_set_auth(UNRESTRICTED_PAYLOAD)
1080+
assert self._add(client, public_id).status_code == 201
1081+
assert self._add(client, public_id, portal_id=OTHER_PORTAL).status_code == 201
1082+
rows = session.exec(
1083+
select(Submission).where(col(Submission.map_public_id) == public_id)
1084+
).all()
1085+
assert sorted(r.portal_id for r in rows) == sorted([PORTAL, OTHER_PORTAL])
1086+
1087+
def test_wrong_team_cannot_add(self, client, form_config, document_id):
1088+
public_id = self._public_id(client, document_id)
1089+
_set_auth(TEAM_B_PAYLOAD) # form_config's portal is team-a's
1090+
assert self._add(client, public_id).status_code == 403
1091+
1092+
def test_duplicate_add_conflicts(self, client, form_config, document_id):
1093+
public_id = self._public_id(client, document_id)
1094+
_set_auth(TEAM_A_PAYLOAD)
1095+
assert self._add(client, public_id).status_code == 201
1096+
response = self._add(client, public_id)
1097+
assert response.status_code == 409
1098+
assert "already has" in response.json()["detail"]
1099+
1100+
def test_unknown_map_404(self, client, form_config):
1101+
_set_auth(TEAM_A_PAYLOAD)
1102+
assert self._add(client, 99999999).status_code == 404

0 commit comments

Comments
 (0)