Skip to content

(backend/frontend) move access/invitation cleanup from frontend to backend on document move#2223

Open
maboukerfa wants to merge 2 commits intosuitenumerique:mainfrom
maboukerfa:feat/move-delete-accesses-backend
Open

(backend/frontend) move access/invitation cleanup from frontend to backend on document move#2223
maboukerfa wants to merge 2 commits intosuitenumerique:mainfrom
maboukerfa:feat/move-delete-accesses-backend

Conversation

@maboukerfa
Copy link
Copy Markdown
Contributor

Purpose

Move the access and invitation cleanup logic from the frontend to the backend during document move operations.

Problems with the current approach:

  1. No atomicity: The frontend deletes accesses and invitations via separate HTTP requests after the move succeeds. If the user loses their connection mid-process, the move completes but the old permissions remain, leaving the document in
    an inconsistent state.
  2. API consumers bypass cleanup entirely: A direct API call to POST /documents/{id}/move/ moves the document without touching its permissions. This creates sub-documents with their own direct accesses that differ from their parent's,
    breaking the inheritance model.
  3. Race condition on deletion: After the move, the frontend fetches the document's accesses to delete them. But since the document has already moved to a new parent, the permissions it retrieves are the inherited ones from the new tree not the old direct accesses. Attempting to delete these returns 404s.
Screen.Recording.2026-04-16.at.17.29.32.mov

Proposal

  • Backend: When a document leaves its current permission scope (root document being moved, cross-tree move, or sub-document promoted to root), its direct accesses and pending invitations are deleted within the same @transaction.atomic. block as the move. If the move fails, everything rolls back.
  • Frontend: The useMoveDoc hook is simplified to a plain move call, no more manual fetch-and-delete logic.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 16, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6686df24-44cf-43c6-aee8-01cb65817921

📥 Commits

Reviewing files that changed from the base of the PR and between 3714d3b and 61a6dd8.

📒 Files selected for processing (5)
  • src/backend/core/api/viewsets.py
  • src/backend/core/tests/documents/test_api_documents_move.py
  • src/frontend/apps/impress/src/features/docs/doc-management/api/useMoveDoc.tsx
  • src/frontend/apps/impress/src/features/docs/docs-grid/components/DocGridContentList.tsx
  • src/frontend/apps/impress/src/features/docs/docs-grid/components/DocMoveModal.tsx

Walkthrough

Backend: DocumentViewSet.move now materializes owner_accesses into a list for root-target moves, detects permission-scope changes (root status changes, cross-root moves, or promotion-to-root as a sibling), and when scope changes deletes direct document.accesses and document.invitations inside the same transaction before completing the move; ownership-repair uses the precomputed owner_accesses. Frontend: useMoveDoc removes the deleteAccessOnMove parameter and client-side deletion side effects; its call sites were updated. Tests: new and updated tests cover root, cross-tree, same-tree moves, promotions to root, sibling-root moves, owner preservation, and transactional rollback on failure.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • lunika
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: moving access/invitation cleanup from frontend to backend during document move operations.
Description check ✅ Passed The description clearly explains the purpose, problems addressed, and the proposed solution related to the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/backend/core/api/viewsets.py`:
- Around line 965-971: Materialize the owner_accesses queryset before deleting
direct accesses: before calling document.accesses.all().delete() (and
document.invitations.all().delete()), evaluate owner_accesses into a concrete
list (e.g. owner_accesses = list(owner_accesses)) so the later owner-recreation
loop can iterate over the preserved items; update the code that computes
owner_accesses to materialize it and then proceed with the deletes and the owner
recreation logic.
- Around line 969-971: The cleanup condition incorrectly only compares current
roots; instead compute the prospective root after the move and use that to
decide whether to drop direct accesses/invitations. Change the logic around
document.is_root(), document.get_root(), and target_document.get_root() to
determine the new root post-move (e.g., if moving as a child use
target_document.get_root(), if moving as a sibling use
target_document.get_parent() and its root or None if that parent is root) and
then run document.accesses.all().delete() and
document.invitations.all().delete() when document.is_root() or
document.get_root() != prospective_root (i.e., when the root membership changes
after the move).

In `@src/backend/core/tests/documents/test_api_documents_move.py`:
- Around line 594-620: The test currently fails before the deletion block
because the target lookup uses a random UUID; change it to use a real target
document and force Document.move to raise after the deletes so rollback can be
observed. Concretely: create a real target_document (e.g. via
factories.DocumentFactory()) and pass its id in the POST, and monkeypatch or
temporarily stub Document.move (the method on the Document model) to raise an
exception when called; then call client.post on the move endpoint and assert the
response indicates failure and that document.accesses.count() and
document.invitations.count() remain unchanged (verifying the deletion roll
back).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 56acc976-7a4f-4750-b14f-c62dbde5d92e

📥 Commits

Reviewing files that changed from the base of the PR and between e59d8a4 and 0c0f025.

📒 Files selected for processing (5)
  • src/backend/core/api/viewsets.py
  • src/backend/core/tests/documents/test_api_documents_move.py
  • src/frontend/apps/impress/src/features/docs/doc-management/api/useMoveDoc.tsx
  • src/frontend/apps/impress/src/features/docs/docs-grid/components/DocGridContentList.tsx
  • src/frontend/apps/impress/src/features/docs/docs-grid/components/DocMoveModal.tsx

Comment thread src/backend/core/api/viewsets.py Outdated
Comment thread src/backend/core/api/viewsets.py Outdated
Comment thread src/backend/core/tests/documents/test_api_documents_move.py Outdated
@maboukerfa maboukerfa force-pushed the feat/move-delete-accesses-backend branch from 0c0f025 to 99bfe78 Compare April 16, 2026 18:26
Comment thread src/backend/core/tests/documents/test_api_documents_move.py
Comment thread src/backend/core/tests/documents/test_api_documents_move.py Outdated
@maboukerfa maboukerfa force-pushed the feat/move-delete-accesses-backend branch from 99bfe78 to ffe8b59 Compare April 25, 2026 06:24
@maboukerfa maboukerfa requested a review from lunika April 25, 2026 06:25
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/backend/core/tests/documents/test_api_documents_move.py`:
- Line 697: Add a short comment above the monkeypatch line explaining the test's
contract: that the API view under test invokes models.Document.move (inherited
from treebeard.MP_Node) and therefore the test injects failure via
monkeypatch.setattr(models.Document, "move", failing_move) to exercise rollback;
note that if the view later calls a differently-named method the test intent
must be updated. This documents the dependency on models.Document.move and the
purpose of the failing_move injection so future refactors don't silently break
the test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9403005d-2240-4fea-9cae-5a823958856d

📥 Commits

Reviewing files that changed from the base of the PR and between 99bfe78 and ffe8b59.

📒 Files selected for processing (2)
  • src/backend/core/api/viewsets.py
  • src/backend/core/tests/documents/test_api_documents_move.py

Comment thread src/backend/core/tests/documents/test_api_documents_move.py
@lunika lunika requested a review from AntoLC April 27, 2026 08:33
@lunika
Copy link
Copy Markdown
Member

lunika commented Apr 27, 2026

The backend part is ok for me.
I asked @AntoLC a review
I would like to discuss this new behavior with @virgile-dev when he will come back.

@AntoLC AntoLC added enhancement improve an existing feature frontend backend labels Apr 27, 2026
Copy link
Copy Markdown
Collaborator

@AntoLC AntoLC left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Frontend side seems good.

@lunika lunika force-pushed the feat/move-delete-accesses-backend branch from ffe8b59 to 3714d3b Compare April 28, 2026 19:33
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/backend/core/api/viewsets.py (1)

965-995: ⚠️ Potential issue | 🔴 Critical

Guard cleanup against sibling no-op moves.

For LEFT/RIGHT/FIRST_SIBLING/LAST_SIBLING, treebeard does not raise when the target is the document itself or one of its descendants; it leaves the node in place. In that case this block still treats a root document as a scope change and wipes its direct accesses/invitations even though nothing actually moved. Short-circuit those no-op sibling cases before running the cleanup.

Based on learnings: In the suitenumerique/docs project (Django/treebeard), when moving a document to a sibling position relative to itself or one of its descendants, treebeard performs a no-op and no exception is raised.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend/core/api/viewsets.py` around lines 965 - 995, Detect and skip the
"no-op" sibling moves before wiping access/invitations: if the position is a
sibling move (not FIRST_CHILD/LAST_CHILD — i.e.
enums.MoveNodePositionChoices.LEFT/RIGHT/FIRST_SIBLING/LAST_SIBLING) and the
target_document is the document itself or a descendant of the document (so the
treebeard move will be a no-op), short-circuit and return a successful/no-op
response (or skip the cleanup) instead of treating
becomes_sibling_root/scope_changes as true; update the logic around
becomes_sibling_root, scope_changes and the subsequent
document.accesses.all().delete()/document.invitations.all().delete() so cleanup
only runs for actual moves (use document vs target_document identity/descendant
check before performing deletes).
♻️ Duplicate comments (1)
src/backend/core/tests/documents/test_api_documents_move.py (1)

781-792: ⚠️ Potential issue | 🟡 Minor

This rollback test still fails before the delete path runs.

DocumentViewSet.move() now calls document.move(...) before document.accesses.all().delete() / document.invitations.all().delete(). Patching models.Document.move here makes the request abort before the cleanup block, so the final count assertions stay green even if those deletes are no longer transactional. Make the failure happen after cleanup instead.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/backend/core/api/viewsets.py`:
- Around line 993-995: The bulk delete block that runs when scope_changes is
true currently calls document.accesses.all().delete() and
document.invitations.all().delete() which bypasses
DocumentAccessViewSet.perform_destroy() and therefore doesn't notify the
collaboration server; after performing these deletes wrap a call to queue a
document-level reset/notification inside transaction.on_commit(...) so the
collaboration server is informed only after the DB commit — locate the
scope_changes branch in viewsets.py and add a transaction.on_commit(lambda:
<call to existing collaboration reset/notify helper>) so active websocket
sessions are reset/updated after the bulk cleanup.

---

Outside diff comments:
In `@src/backend/core/api/viewsets.py`:
- Around line 965-995: Detect and skip the "no-op" sibling moves before wiping
access/invitations: if the position is a sibling move (not
FIRST_CHILD/LAST_CHILD — i.e.
enums.MoveNodePositionChoices.LEFT/RIGHT/FIRST_SIBLING/LAST_SIBLING) and the
target_document is the document itself or a descendant of the document (so the
treebeard move will be a no-op), short-circuit and return a successful/no-op
response (or skip the cleanup) instead of treating
becomes_sibling_root/scope_changes as true; update the logic around
becomes_sibling_root, scope_changes and the subsequent
document.accesses.all().delete()/document.invitations.all().delete() so cleanup
only runs for actual moves (use document vs target_document identity/descendant
check before performing deletes).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 39f6d9cf-4b5d-44b1-b9c1-3170ed208a5a

📥 Commits

Reviewing files that changed from the base of the PR and between ffe8b59 and 3714d3b.

📒 Files selected for processing (5)
  • src/backend/core/api/viewsets.py
  • src/backend/core/tests/documents/test_api_documents_move.py
  • src/frontend/apps/impress/src/features/docs/doc-management/api/useMoveDoc.tsx
  • src/frontend/apps/impress/src/features/docs/docs-grid/components/DocGridContentList.tsx
  • src/frontend/apps/impress/src/features/docs/docs-grid/components/DocMoveModal.tsx

Comment on lines +993 to +995
if scope_changes:
document.accesses.all().delete()
document.invitations.all().delete()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reset collaboration sessions after bulk access cleanup.

This bulk delete bypasses the normal access-removal path in DocumentAccessViewSet.perform_destroy(), so the collaboration server is never notified that users lost direct access during the move. Existing websocket sessions can stay alive with stale permissions until they reconnect. Queue a document-level reset in transaction.on_commit(...) when scope_changes is true.

🛠️ Suggested follow-up
         if scope_changes:
             document.accesses.all().delete()
             document.invitations.all().delete()
+            transaction.on_commit(
+                lambda: CollaborationService().reset_connections(str(document.id))
+            )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend/core/api/viewsets.py` around lines 993 - 995, The bulk delete
block that runs when scope_changes is true currently calls
document.accesses.all().delete() and document.invitations.all().delete() which
bypasses DocumentAccessViewSet.perform_destroy() and therefore doesn't notify
the collaboration server; after performing these deletes wrap a call to queue a
document-level reset/notification inside transaction.on_commit(...) so the
collaboration server is informed only after the DB commit — locate the
scope_changes branch in viewsets.py and add a transaction.on_commit(lambda:
<call to existing collaboration reset/notify helper>) so active websocket
sessions are reset/updated after the bulk cleanup.

Access and invitation cleanup on document move is now handled
atomically by the backend. The frontend no longer needs to fetch
and delete accesses/invitations after a successful move.

Signed-off-by: Mohamed El Amine BOUKERFA <boukerfa.ma@gmail.com>
When a document is moved outside its current permission scope (root
document, cross-tree move, or promotion to root), its direct accesses
and pending invitations are now deleted server-side within the same
atomic transaction as the move itself. This ensures consistency: if
the move fails, deletions are rolled back.

Signed-off-by: Mohamed El Amine BOUKERFA <boukerfa.ma@gmail.com>
@lunika lunika force-pushed the feat/move-delete-accesses-backend branch from 3714d3b to 61a6dd8 Compare April 29, 2026 08:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend enhancement improve an existing feature frontend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants