Skip to content

Commit 926931f

Browse files
authored
Merge pull request #12186 from Harshul23/12153/hotfix/series-position-sorting
Fix series sorting for numeric, non-numeric, and range positions
1 parent 23f1eb4 commit 926931f

2 files changed

Lines changed: 132 additions & 10 deletions

File tree

openlibrary/core/lists/model.py

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import contextlib
44
import logging
5+
import re
56
import typing
67
from collections.abc import Iterable
78
from functools import cached_property
@@ -21,10 +22,17 @@
2122
Thing,
2223
ThingKey,
2324
ThingReferenceDict,
25+
WorkSeriesEdgeDB,
2426
does_seed_have_metadata,
2527
update_list_seed_metadata,
2628
)
27-
from openlibrary.plugins.upstream.models import Author, Changeset, Edition, User, Work
29+
from openlibrary.plugins.upstream.models import (
30+
Author,
31+
Changeset,
32+
Edition,
33+
User,
34+
Work,
35+
)
2836
from openlibrary.plugins.worksearch.search import get_solr
2937
from openlibrary.plugins.worksearch.subjects import get_subject
3038
from openlibrary.utils.solr import Solr
@@ -656,6 +664,24 @@ class SeriesDict(ListDict):
656664
pass
657665

658666

667+
def get_work_sort_key(
668+
tpl: tuple[Work, WorkSeriesEdgeDB],
669+
) -> tuple[str, int, float, str]:
670+
work, edge = tpl
671+
position = edge.get('position')
672+
pos_str = str(position or "").strip()
673+
674+
with contextlib.suppress(ValueError):
675+
return ("A: Numeric", 1, float(pos_str), work.key)
676+
677+
if match := re.fullmatch(r'(\d+)\s*-\s*(\d+)', pos_str):
678+
lower = int(match.group(1))
679+
upper = int(match.group(2))
680+
return ("C: Range", upper - lower, float(lower), work.key)
681+
682+
return ("B: Non-numeric", 0, 0.0, work.key)
683+
684+
659685
class Series(List):
660686
SEED_LIMIT = 100
661687

@@ -681,15 +707,7 @@ def get_seeds(self, sort=False, resolve_redirects=False) -> list['Seed']:
681707
(work, edge) for work in works if (edge := work.find_series_edge(self.key))
682708
]
683709

684-
def get_work_position(position: str | None) -> float:
685-
position_float = 9999.0
686-
with contextlib.suppress(ValueError):
687-
position_float = float(position or 9999)
688-
return position_float
689-
690-
sorted_edges = sorted(
691-
series_edges, key=lambda tpl: get_work_position(tpl[1].get('position'))
692-
)
710+
sorted_edges = sorted(series_edges, key=get_work_sort_key)
693711

694712
seeds: list[Seed] = []
695713
for work, edge in sorted_edges:

openlibrary/tests/core/lists/test_model.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import web
2+
13
import openlibrary.core.lists.model as list_model
24
from openlibrary.mocks.mock_infobase import MockSite
35

@@ -27,3 +29,105 @@ def save_doc(self, site, type, key, **fields):
2729
d = {"key": key, "type": {"key": type}}
2830
d.update(fields)
2931
site.save(d)
32+
33+
34+
class MockWork:
35+
def __init__(self, key, position):
36+
self.key = key
37+
self._position = position
38+
39+
def find_series_edge(self, series_key):
40+
return {"position": self._position}
41+
42+
43+
class MockSeriesSite:
44+
def __init__(self, works):
45+
self._works = works
46+
47+
def things(self, query):
48+
return [work.key for work in self._works]
49+
50+
def get_many(self, keys):
51+
return self._works
52+
53+
54+
class TestSeries:
55+
def test_series_sorting(self, monkeypatch):
56+
works = [
57+
MockWork("/works/OL5W", "2"),
58+
MockWork("/works/OL1W", "22-23"),
59+
MockWork("/works/OL2W", "1"),
60+
MockWork("/works/OL3W", "3"),
61+
MockWork("/works/OL4W", "24-25"),
62+
MockWork("/works/OL8W", "Non-numeric"),
63+
MockWork("/works/OL7W", "4-5"),
64+
MockWork("/works/OL6W", "1-3"),
65+
MockWork("/works/OL9W", "1-9"),
66+
MockWork("/works/OL10W", None),
67+
MockWork("/works/OL11W", "3"),
68+
]
69+
70+
mock_site = MockSeriesSite(works)
71+
monkeypatch.setattr(web, "ctx", web.storage(site=mock_site))
72+
73+
series = list_model.Series(mock_site, "/series/OL1L")
74+
seeds = series.get_seeds()
75+
76+
result_keys = [seed.key for seed in seeds]
77+
expected_keys = [
78+
"/works/OL2W", # 1
79+
"/works/OL5W", # 2
80+
"/works/OL11W", # 3 (tie-break by work key)
81+
"/works/OL3W", # 3
82+
"/works/OL10W", # None -> non-numeric bucket
83+
"/works/OL8W", # Non-numeric
84+
"/works/OL7W", # 4-5 (range size 1, lower 4)
85+
"/works/OL1W", # 22-23 (range size 1, lower 22)
86+
"/works/OL4W", # 24-25 (range size 1, lower 24)
87+
"/works/OL6W", # 1-3 (range size 2, lower 1)
88+
"/works/OL9W", # 1-9 (range size 8, lower 1)
89+
]
90+
91+
assert result_keys == expected_keys
92+
93+
94+
class TestWorkSortKey:
95+
def test_numeric_position(self):
96+
class DummyWork:
97+
def __init__(self, key):
98+
self.key = key
99+
100+
work = DummyWork("/works/OL1W")
101+
102+
key = list_model.get_work_sort_key((work, {"position": "2"}))
103+
assert key == ("A: Numeric", 1, 2.0, "/works/OL1W")
104+
105+
def test_range_position(self):
106+
class DummyWork:
107+
def __init__(self, key):
108+
self.key = key
109+
110+
work = DummyWork("/works/OL1W")
111+
112+
key = list_model.get_work_sort_key((work, {"position": "1-3"}))
113+
assert key == ("C: Range", 2, 1.0, "/works/OL1W")
114+
115+
def test_non_numeric_position(self):
116+
class DummyWork:
117+
def __init__(self, key):
118+
self.key = key
119+
120+
work = DummyWork("/works/OL1W")
121+
122+
key = list_model.get_work_sort_key((work, {"position": "abc"}))
123+
assert key == ("B: Non-numeric", 0, 0.0, "/works/OL1W")
124+
125+
def test_none_position(self):
126+
class DummyWork:
127+
def __init__(self, key):
128+
self.key = key
129+
130+
work = DummyWork("/works/OL1W")
131+
132+
key = list_model.get_work_sort_key((work, {"position": None}))
133+
assert key == ("B: Non-numeric", 0, 0.0, "/works/OL1W")

0 commit comments

Comments
 (0)