Update positions of other items affected by moving/adding/removing#3218
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates the /content_lists/<id>/items bulk-patch behavior so that when items are added, moved, or deleted, the positions of other affected (non-sticky) items are adjusted to keep positions unique and to close gaps after deletes.
Changes:
- Add position-shifting logic to the content list items bulk patch service for add/move/delete operations (excluding sticky items).
- Introduce helper methods to shift positions up/down and to handle range shifts on moves.
- Add Behave scenarios covering add-at-position, move-to-occupied, move-forward, and delete-gap-closing behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| features/content_lists.feature | Adds end-to-end Behave scenarios asserting reordered positions after bulk-patch operations. |
| apps/content_lists/service.py | Implements MongoDB update_many-based position shifting during bulk patch add/move/delete. |
Comments suppressed due to low confidence (2)
apps/content_lists/service.py:66
- In the
addaction, positions are shifted before attemptingcreate(). Ifcreate()fails (e.g. content relation validation fails, or the unique (list_id, content) index rejects a duplicate), the list will be left with all later items shifted even though nothing was inserted. Consider creating first and then shifting other items (excluding the inserted_id), or running the shift+insert in a transaction / compensating rollback on error.
This issue also appears on line 67 of the same file.
if action == "add":
new_position = item_data.get("position")
if new_position is not None:
await self._shift_positions_up(list_id, new_position)
await self.create(
[
{
"list_id": list_id,
"content": content_id,
"position": new_position,
"sticky": item_data.get("sticky", False),
"sticky_position": item_data.get("stickyPosition"),
"enabled": True,
}
]
)
apps/content_lists/service.py:72
- In the
moveaction,_shift_positions_for_move(...)is called beforeupdate(). If the subsequentupdate()fails validation (e.g. invalidpositiontype) or raises a DB error, other items will already have been shifted, leaving the list in an inconsistent state. To avoid partial updates, consider updating the moved item first and then shifting others (excluding the moved_id), or wrapping both operations in a MongoDB transaction.
elif action == "move":
existing = await self.find_one(req=None, list_id=list_id, content=content_id)
if existing:
new_position = item_data.get("position")
await self._shift_positions_for_move(list_id, existing.id, existing.position, new_position)
await self.update(existing.id, {"position": new_position})
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
eos87
left a comment
There was a problem hiding this comment.
Looks good in general, just two concerns:
-
Atomicity: each action does a shift + a create/update/delete as two separate Mongo calls. If the second call fails after the shift succeeds, the list ends up in a broken state?
-
Sticky items: I see they are being excluded intentionally but, we need to make sure the sticky items get their
positionvalue properly set. Like: when an item becomes sticky, set its position toNone. When an item becomes non-sticky, it needs a real position assigned (probably appended to the end, or supplied by the client). Thoughts?
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
apps/content_lists/service.py:121
_renumberalways rewrites non-sticky item positions into a contiguous0..N-1sequence. This changes the externally observable meaning of requests that set positions beyond the current list length (e.g. moving a single item to position 5 will be collapsed back to 0). If the API is expected to preserve the requested position (even if it creates gaps), this behavior is breaking; if the new behavior is intended, the API/tests/docs should be updated to reflect that out-of-range positions are clamped/normalized.
ops = [
UpdateOne({"_id": doc["_id"]}, {"$set": {"position": i}})
for i, doc in enumerate(docs)
if doc.get("position") != i
]
if ops:
await self.mongo_async.bulk_write(ops, ordered=False)
| @auth | ||
| Scenario: Moving an item shifts other items to keep positions unique | ||
| Given an archive item "article-a" | ||
| And an archive item "article-b" | ||
| When we post to "/content_lists" | ||
| """ | ||
| {"name": "Breaking News", "type": "manual"} | ||
| """ |
…ure/content-lists-with-develop
Summary
Bulk patches on
/content_lists/<id>/itemsnow keep positions contiguous and unique when items are added, moved, or removed. Previously, only the directly-touched item'spositionwas updated, which could leave gaps after deletes or produce duplicate positions on inserts/moves.The new behavior in
apps/content_lists/service.py:p: shift every non-sticky item withposition >= pup by 1 before inserting.oldtonew: items strictly between the two endpoints shift by 1 in the opposite direction; the moved item itself is excluded from the shift.p: shift every non-sticky item withposition > pdown by 1.sticky: true) are excluded from all reordering.position: Noneare handled — add/delete still works, and a move from/toNonedegrades to a one-sided shift.Test plan
Four new behave scenarios in
features/content_lists.featureexercise each path end-to-end via the bulk-patch endpoint and assert the final ordering:Fixes: SDESK-7928