fix: ring 'pocket' — fill gaps from deleted/unlisted products

get_ring_related_products walked exactly backward+forward positions
in the ring and silently dropped entries whose IDs no longer resolved
to visible products. Result: sparse offsets like [-2, -1, 1, 5, 28]
visible in Up Next — a 'pocket' of live items in an otherwise stale
ring slice.

New behavior: fetch every ring product once (bulk query), keep only
visibility==1, then walk further along the ring to collect the
requested backward/forward VALID neighbors. Offsets are renumbered
contiguously (-N..-1, 1..N). Pocket is filled by skipping past
deleted/unlisted entries until we have the requested count or
exhaust the ring.

Ring traversal on the client (ringPosition + direction) uses ring
indices directly and is unaffected — only the rendered Up Next
sidebar slice is densified.
This commit is contained in:
russell@unturf.com 2026-04-21 18:20:28 -04:00
parent 3470d747f3
commit 81a5de4ce1

View file

@ -760,6 +760,10 @@ def get_ring_related_products(product, ring, forward=42, backward=3):
Returns a list of dicts: {"product": Product, "offset": int}
where offset is negative for previous, positive for next.
Falls back to get_related_products if product not in ring.
Walks past deleted or non-public ring entries to fill the requested
backward/forward counts with contiguous offsets. Prevents "pocket"
gaps where most neighbors in the raw ring are stale.
"""
product_id_str = str(product.id)
if product_id_str not in ring:
@ -768,34 +772,49 @@ def get_ring_related_products(product, ring, forward=42, backward=3):
idx = ring.index(product_id_str)
ring_len = len(ring)
if ring_len < 2:
return []
# Cap forward to avoid overlap with backward items in circular ring
max_forward = max(ring_len - 1 - backward, 0)
forward = min(forward, max_forward)
# Fetch every ring product in one query; keep only visibility==1.
# Ring is built from public products, but items can become unlisted
# or be deleted after the fact — skip those so Up Next stays a
# dense list of valid discovery targets.
products = get_products_by_ids(product.dbsession, list(set(ring)))
product_map = {str(p.id): p for p in products if p.visibility == 1}
# Walk backward from current, collect up to `backward` valid neighbors
# (nearest first). Then reverse so sidebar reads -N, ..., -1.
backward_ids = []
step = 0
while len(backward_ids) < backward and step < ring_len - 1:
step += 1
pos = (idx - step + ring_len) % ring_len
pid = ring[pos]
if pid == product_id_str:
break
if pid in product_map:
backward_ids.append(pid)
backward_ids.reverse()
# Walk forward from current, collect up to `forward` valid neighbors.
forward_ids = []
step = 0
while len(forward_ids) < forward and step < ring_len - 1:
step += 1
pos = (idx + step) % ring_len
pid = ring[pos]
if pid == product_id_str:
break
if pid in product_map:
forward_ids.append(pid)
# Collect IDs with their offsets: negative=previous, positive=next
items = []
for i in range(backward, 0, -1):
prev_idx = (idx - i + ring_len) % ring_len
items.append((ring[prev_idx], -i))
for i in range(1, min(forward + 1, ring_len)):
next_idx = (idx + i) % ring_len
items.append((ring[next_idx], i))
if not items:
return []
all_ids = [pid for pid, _ in items]
products = get_products_by_ids(product.dbsession, all_ids)
if not products:
return []
product_map = {str(p.id): p for p in products}
return [
{"product": product_map[pid], "offset": offset}
for pid, offset in items
if pid in product_map
]
backward_offset_start = -len(backward_ids)
for i, pid in enumerate(backward_ids):
items.append({"product": product_map[pid], "offset": backward_offset_start + i})
for i, pid in enumerate(forward_ids):
items.append({"product": product_map[pid], "offset": i + 1})
return items
def get_related_products(product, limit=8):