Add sorting by rating
This commit is contained in:
@@ -9,7 +9,7 @@ from collections.abc import Sequence
|
|||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
from typing import assert_never
|
from typing import assert_never
|
||||||
|
|
||||||
from sqlalchemy import ColumnElement, and_, func, or_, select
|
from sqlalchemy import ColumnElement, Select, and_, func, or_, select
|
||||||
from sqlalchemy.orm import InstrumentedAttribute, joinedload
|
from sqlalchemy.orm import InstrumentedAttribute, joinedload
|
||||||
|
|
||||||
from hxbooks.search import IsOperatorValue, QueryParser, SortDirection, ValueT
|
from hxbooks.search import IsOperatorValue, QueryParser, SortDirection, ValueT
|
||||||
@@ -313,31 +313,9 @@ def search_books_advanced(
|
|||||||
sort_columns = []
|
sort_columns = []
|
||||||
for field_filter in parsed_query.field_filters:
|
for field_filter in parsed_query.field_filters:
|
||||||
if field_filter.field == Field.SORT:
|
if field_filter.field == Field.SORT:
|
||||||
if (
|
query, sort_column = _build_sort_column(query, field_filter.value, username)
|
||||||
isinstance(field_filter.value, tuple)
|
if sort_column is not None:
|
||||||
and field_filter.value[0] == Field.READ_DATE
|
sort_columns.append(sort_column)
|
||||||
):
|
|
||||||
# Special handling for sorting by read date - sort by latest reading end
|
|
||||||
# date
|
|
||||||
subq = (
|
|
||||||
select(
|
|
||||||
Reading.book_id,
|
|
||||||
func.max(Reading.end_date).label("latest_read_date"),
|
|
||||||
)
|
|
||||||
.where(
|
|
||||||
Reading.user.has(User.username == username),
|
|
||||||
Reading.end_date.isnot(None),
|
|
||||||
)
|
|
||||||
.group_by(Reading.book_id)
|
|
||||||
.subquery()
|
|
||||||
)
|
|
||||||
query = query.outerjoin(subq, Book.id == subq.c.book_id)
|
|
||||||
if field_filter.value[1] == SortDirection.ASC:
|
|
||||||
sort_columns.append(subq.c.latest_read_date.asc().nullslast())
|
|
||||||
else:
|
|
||||||
sort_columns.append(subq.c.latest_read_date.desc().nullslast())
|
|
||||||
else:
|
|
||||||
sort_columns.append(_build_sort_column(field_filter.value))
|
|
||||||
else:
|
else:
|
||||||
condition = _build_field_condition(field_filter, username)
|
condition = _build_field_condition(field_filter, username)
|
||||||
|
|
||||||
@@ -449,7 +427,9 @@ def _build_field_condition(
|
|||||||
return condition
|
return condition
|
||||||
|
|
||||||
|
|
||||||
def _build_sort_column(value: ValueT) -> ColumnElement | None:
|
def _build_sort_column(
|
||||||
|
query: Select, value: ValueT, username: str | None = None
|
||||||
|
) -> tuple[Select, ColumnElement | None]:
|
||||||
"""Build a sort column for the 'sort' field."""
|
"""Build a sort column for the 'sort' field."""
|
||||||
assert isinstance(value, tuple) and len(value) == 2
|
assert isinstance(value, tuple) and len(value) == 2
|
||||||
field, direction = value
|
field, direction = value
|
||||||
@@ -476,21 +456,60 @@ def _build_sort_column(value: ValueT) -> ColumnElement | None:
|
|||||||
column = Book.location_shelf
|
column = Book.location_shelf
|
||||||
case Field.OWNER:
|
case Field.OWNER:
|
||||||
column = User.username
|
column = User.username
|
||||||
# These relationship-based fields are not supported here as they need subqueries
|
case Field.READ_DATE:
|
||||||
case Field.RATING | Field.READ_DATE:
|
# Special handling for sorting by read date - sort by latest reading end
|
||||||
return None
|
# date
|
||||||
|
subq = (
|
||||||
|
select(
|
||||||
|
Reading.book_id,
|
||||||
|
func.max(Reading.end_date).label("latest_read_date"),
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
Reading.user.has(User.username == username),
|
||||||
|
Reading.end_date.isnot(None),
|
||||||
|
)
|
||||||
|
.group_by(Reading.book_id)
|
||||||
|
.subquery("latest_readings")
|
||||||
|
)
|
||||||
|
query = query.outerjoin(subq, Book.id == subq.c.book_id)
|
||||||
|
column = subq.c.latest_read_date
|
||||||
|
case Field.RATING:
|
||||||
|
# Special handling for sorting by rating - sort by latest reading rating
|
||||||
|
subq = (
|
||||||
|
select(
|
||||||
|
Reading.book_id,
|
||||||
|
Reading.rating.label("latest_rating"),
|
||||||
|
func
|
||||||
|
.row_number()
|
||||||
|
.over(
|
||||||
|
partition_by=Reading.book_id,
|
||||||
|
order_by=Reading.end_date.desc(),
|
||||||
|
)
|
||||||
|
.label("rn"),
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
Reading.user.has(User.username == username),
|
||||||
|
Reading.rating.isnot(None),
|
||||||
|
Reading.end_date.isnot(None),
|
||||||
|
)
|
||||||
|
.subquery("latest_ratings")
|
||||||
|
)
|
||||||
|
query = query.outerjoin(
|
||||||
|
subq, (Book.id == subq.c.book_id) & (subq.c.rn == 1)
|
||||||
|
)
|
||||||
|
column = subq.c.latest_rating
|
||||||
# These fields don't make sense to sort by
|
# These fields don't make sense to sort by
|
||||||
case Field.AUTHOR | Field.GENRE | Field.IS | Field.SORT:
|
case Field.AUTHOR | Field.GENRE | Field.IS | Field.SORT:
|
||||||
return None
|
return query, None
|
||||||
case _:
|
case _:
|
||||||
assert_never(field)
|
assert_never(field)
|
||||||
|
|
||||||
if direction == SortDirection.ASC:
|
if direction == SortDirection.ASC:
|
||||||
return column.asc().nullslast()
|
return query, column.asc().nullslast()
|
||||||
elif direction == SortDirection.DESC:
|
elif direction == SortDirection.DESC:
|
||||||
return column.desc().nullslast()
|
return query, column.desc().nullslast()
|
||||||
else:
|
else:
|
||||||
return None
|
return query, None
|
||||||
|
|
||||||
|
|
||||||
def _apply_operator(
|
def _apply_operator(
|
||||||
|
|||||||
@@ -377,6 +377,11 @@ class TestBookSearchCommand:
|
|||||||
"",
|
"",
|
||||||
["Dune", "The Fellowship", "The Hobbit", "Programming Book"],
|
["Dune", "The Fellowship", "The Hobbit", "Programming Book"],
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"sort:rating-desc",
|
||||||
|
"alice",
|
||||||
|
["The Hobbit", "Dune", "Programming Book", "The Fellowship"],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_book_search_advanced_queries(
|
def test_book_search_advanced_queries(
|
||||||
|
|||||||
Reference in New Issue
Block a user