@@ -67,7 +67,8 @@ async def bulk_patch(self, list_id: ObjectId, data: dict) -> ContentList:
6767 elif action == "move" :
6868 existing = await self .find_one (req = None , list_id = list_id , content = content_id )
6969 if existing :
70- await self .update (existing .id , {"position" : item_data .get ("position" )})
70+ new_sticky = item_data .get ("sticky" , existing .sticky )
71+ await self .update (existing .id , {"position" : item_data .get ("position" ), "sticky" : new_sticky })
7172 touched_contents .append (content_id )
7273 elif action == "delete" :
7374 existing = await self .find_one (req = None , list_id = list_id , content = content_id )
@@ -85,36 +86,62 @@ async def bulk_patch(self, list_id: ObjectId, data: dict) -> ContentList:
8586 return result
8687
8788 async def _renumber (self , list_id : ObjectId , touched_contents : list [str ]) -> None :
88- """Rewrite non-sticky positions as a contiguous ``0..N-1`` sequence.
89-
90- Items in ``touched_contents`` (added or moved in this batch) win position
91- ties: when an item lands on a slot already occupied, the touched item
92- keeps the slot and the prior occupant shifts to the next one. Later
93- entries in ``touched_contents`` outrank earlier ones.
89+ """Reassign positions so every item has a unique slot.
90+
91+ Sticky items keep their declared position. Non-sticky items in
92+ ``touched_contents`` (added or moved in this batch) also anchor at the
93+ position they were just set to. Remaining untouched non-sticky items
94+ fill the lowest-numbered free positions in their previous order. If
95+ two anchors land on the same slot, sticky beats non-sticky and the
96+ most recently touched wins within a group; the loser spills to the
97+ nearest higher free slot.
9498 """
9599 docs = await self .mongo_async .find (
96- {"list_id" : list_id , "sticky" : { "$ne" : True } },
97- projection = {"_id" : 1 , "content" : 1 , "position" : 1 },
100+ {"list_id" : list_id },
101+ projection = {"_id" : 1 , "content" : 1 , "position" : 1 , "sticky" : 1 },
98102 ).to_list (None )
103+ if not docs :
104+ return
99105
100106 touched_rank = {c : i for i , c in enumerate (touched_contents )}
101107
102- def sort_key (d : dict ) -> tuple :
103- pos = d .get ("position" )
108+ def anchor_priority (d : dict ) -> tuple :
104109 rank = touched_rank .get (d .get ("content" ))
105110 return (
106- pos is None ,
107- pos if pos is not None else 0 ,
108- rank is None ,
109- - rank if rank is not None else 0 ,
111+ 0 if d .get ("sticky" ) else 1 ,
112+ - (rank if rank is not None else - 1 ),
110113 d ["_id" ],
111114 )
112115
113- docs .sort (key = sort_key )
116+ anchors = [d for d in docs if d .get ("sticky" ) or d .get ("content" ) in touched_rank ]
117+ anchors .sort (key = anchor_priority )
118+
119+ placement : dict [int , dict ] = {}
120+ for doc in anchors :
121+ pos = doc .get ("position" )
122+ slot = pos if pos is not None and pos >= 0 else 0
123+ while slot in placement :
124+ slot += 1
125+ placement [slot ] = doc
126+
127+ rest = [d for d in docs if not d .get ("sticky" ) and d .get ("content" ) not in touched_rank ]
128+ rest .sort (
129+ key = lambda d : (
130+ d .get ("position" ) is None ,
131+ d .get ("position" ) if d .get ("position" ) is not None else 0 ,
132+ d ["_id" ],
133+ )
134+ )
135+ next_slot = 0
136+ for doc in rest :
137+ while next_slot in placement :
138+ next_slot += 1
139+ placement [next_slot ] = doc
140+ next_slot += 1
114141
115142 ops = [
116143 UpdateOne ({"_id" : doc ["_id" ]}, {"$set" : {"position" : i }})
117- for i , doc in enumerate ( docs )
144+ for i , doc in placement . items ( )
118145 if doc .get ("position" ) != i
119146 ]
120147 if ops :
0 commit comments