From c2c5a6c955d7e72af244bbf6336c641c4b5d8bff Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 24 May 2026 09:10:52 +0000 Subject: [PATCH] Prevent copying assignments (#2614) - Backend: throw InvalidRequestError when attempting to copy an assignment root - Frontend: disable "Make a copy" action bar button when any selected item is an assignment - Tests: add two tests covering attempt to copy singleDoc and sequence assignment roots https://claude.ai/code/session_019HTqQkPWkZ35RM1x1HRQ2W --- apps/api/src/query/copy_move.ts | 13 ++++++++ apps/api/src/test/copy_move.test.ts | 44 +++++++++++++++++++++++++++ apps/app/src/paths/Activities.tsx | 5 +++ apps/app/src/utils/actionBarConfig.ts | 4 ++- 4 files changed, 65 insertions(+), 1 deletion(-) diff --git a/apps/api/src/query/copy_move.ts b/apps/api/src/query/copy_move.ts index ba06d8e34..d5fc291ec 100644 --- a/apps/api/src/query/copy_move.ts +++ b/apps/api/src/query/copy_move.ts @@ -392,6 +392,19 @@ export async function copyContent({ throw new InvalidRequestError("Content not found or not visible"); } + // Prevent copying assignment roots + const assignmentRoots = await prisma.content.findMany({ + where: { + id: { in: contentIds }, + isAssignmentRoot: true, + }, + select: { id: true }, + }); + + if (assignmentRoots.length > 0) { + throw new InvalidRequestError("Cannot copy an assignment"); + } + let desiredParentIsPublic = false; let desiredParentLicenseCode: LicenseCode = "CCDUAL"; let desiredParentShares: Uint8Array[] = []; diff --git a/apps/api/src/test/copy_move.test.ts b/apps/api/src/test/copy_move.test.ts index 991b83685..5ef1c7972 100644 --- a/apps/api/src/test/copy_move.test.ts +++ b/apps/api/src/test/copy_move.test.ts @@ -899,6 +899,50 @@ test("cannot copy into assigned problem set", async () => { ).rejects.toThrow("Cannot copy content into an assigned activity"); }); +test("cannot copy an assignment", async () => { + const { userId: ownerId } = await createTestUser(); + const [problemSetId] = await setupTestContent(ownerId, { + "problem set 1": pset({ + "doc 1": doc(""), + }), + }); + const { assignmentId } = await createAssignment({ + contentId: problemSetId, + loggedInUserId: ownerId, + destinationParentId: null, + closedOn: DateTime.now(), + }); + + await expect( + copyContent({ + contentIds: [assignmentId], + loggedInUserId: ownerId, + parentId: null, + }), + ).rejects.toThrow("Cannot copy an assignment"); +}); + +test("cannot copy an assignment (singleDoc)", async () => { + const { userId: ownerId } = await createTestUser(); + const [docId] = await setupTestContent(ownerId, { + "doc 1": doc(""), + }); + const { assignmentId } = await createAssignment({ + contentId: docId, + loggedInUserId: ownerId, + destinationParentId: null, + closedOn: DateTime.now(), + }); + + await expect( + copyContent({ + contentIds: [assignmentId], + loggedInUserId: ownerId, + parentId: null, + }), + ).rejects.toThrow("Cannot copy an assignment"); +}); + test("MoveCopyContent does not allow singleDoc` as parent type", async () => { const { userId: loggedInUserId } = await createTestUser(); diff --git a/apps/app/src/paths/Activities.tsx b/apps/app/src/paths/Activities.tsx index 950f459f1..da5b7d519 100644 --- a/apps/app/src/paths/Activities.tsx +++ b/apps/app/src/paths/Activities.tsx @@ -452,10 +452,15 @@ export function Activities() { actionBarContext = config.context; actions = config.actions; } else { + const anyAssignmentSelected = selectedContentDescriptions.some( + (c) => c.assignmentInfo !== undefined, + ); + const config = configOrganizeContentList({ cardSelections, cardMovement, forceDisableMoveUpAndDown: haveQuery, + anyAssignmentSelected, FIX_ME_miscellaneous_buttons, onMoveTo: () => { const selectedContent = content.find( diff --git a/apps/app/src/utils/actionBarConfig.ts b/apps/app/src/utils/actionBarConfig.ts index 2bfe05c1a..a3c9d39e2 100644 --- a/apps/app/src/utils/actionBarConfig.ts +++ b/apps/app/src/utils/actionBarConfig.ts @@ -103,6 +103,7 @@ export function configOrganizeContentList({ cardSelections, cardMovement, forceDisableMoveUpAndDown = false, + anyAssignmentSelected = false, onMoveTo, onCopy, onDelete, @@ -111,6 +112,7 @@ export function configOrganizeContentList({ cardSelections: CardSelections; cardMovement: CardMovement; forceDisableMoveUpAndDown?: boolean; + anyAssignmentSelected?: boolean; onMoveTo: () => void; onCopy: () => void; onDelete: () => void; @@ -156,7 +158,7 @@ export function configOrganizeContentList({ { label: "Make a copy", onClick: onCopy, - isDisabled: cardSelections.count === 0, + isDisabled: cardSelections.count === 0 || anyAssignmentSelected, }, ]; return { context, actions };