Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
 · 
<a href="https://docs.librislog.app/api/">API Reference</a>
&nbsp;·&nbsp;
<a href="https://docs.librislog.app/releases">Release Notes</a>
&nbsp;·&nbsp;
<a href="https://docs.librislog.app/next/">Nightly Docs</a>
</p>

Expand Down Expand Up @@ -128,8 +130,6 @@ MIT

## Star History

## Star History

<a href="https://www.star-history.com/?repos=codebude%2Flibrislog&type=date&legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=codebude/librislog&type=date&theme=dark&legend=top-left&sealed_token=63MQnBuCo06yuka4CK7jSufogWTsYJDymR_l4uRyMUp_LWPSC65IxbPKjm2UeEPNaU4GKsdNJG308hwGsMjSqtjKwc6Br0SiEkEx-UkS4-7OKgvFQfRqR8rB8bFTNi8eWhacZ2clPYvs_oKBPqUsbCDUXDqGQxuneV_1LAEC3AoIiqlsodcwv_RAvCvm" />
Expand Down
2 changes: 1 addition & 1 deletion backend/app/routers/books.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ def list_books(
default=None,
description=(
"Search phrase. Use <field>:<value> to restrict a term to a single field "
"(author, publisher, title, tag, language, availability, notes, description). "
"(author, publisher, title, tag, language, possession, notes, description). "
"Wrap multi-word values in double quotes (e.g. author:\"Marlen Haushofer\") and "
"prefix any term with - to negate it (e.g. tag:cars -tag:audi)."
),
Expand Down
14 changes: 7 additions & 7 deletions backend/app/services/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@
"description": Book.blurb,
}

# Availability is a special case: it maps to an exact enum comparison.
AVAILABILITY_PREFIX = "availability"
# Possession is a special case: it maps to an exact enum comparison.
POSSESSION_PREFIX = "possession"
TAG_PREFIX = "tag"
AUTHOR_PREFIX = "author"

SUPPORTED_PREFIXES: frozenset[str] = frozenset(
[*FIELD_COLUMNS.keys(), AVAILABILITY_PREFIX, TAG_PREFIX, AUTHOR_PREFIX]
[*FIELD_COLUMNS.keys(), POSSESSION_PREFIX, TAG_PREFIX, AUTHOR_PREFIX]
)

# Default fields searched by an unprefixed term (unchanged from the previous
Expand Down Expand Up @@ -168,7 +168,7 @@ def _unprefixed_condition(value: str, user_id: int) -> Any:
)


def _availability_condition(value: str) -> Any | None:
def _possession_condition(value: str) -> Any | None:
"""Build the exact acquisition-status condition, or ``None`` if invalid."""
normalized = value.strip().lower().replace(" ", "_")
try:
Expand All @@ -180,8 +180,8 @@ def _availability_condition(value: str) -> Any | None:

def _field_condition(field: str, value: str, user_id: int) -> Any | None:
"""Build the condition for a single field-specific term."""
if field == AVAILABILITY_PREFIX:
return _availability_condition(value)
if field == POSSESSION_PREFIX:
return _possession_condition(value)
if field == TAG_PREFIX:
return _tag_condition(value, user_id)
if field == AUTHOR_PREFIX:
Expand Down Expand Up @@ -212,7 +212,7 @@ def apply_search_filter(statement: Any, query: str, user_id: int) -> Any:

condition = _field_condition(term.field, term.value, user_id)
if condition is None:
# Invalid availability value: positive yields no rows, negated is a no-op.
# Invalid possession value: positive yields no rows, negated is a no-op.
conditions.append(sa.false() if not term.negated else sa.true())
elif term.negated:
conditions.append(sa.not_(condition))
Expand Down
12 changes: 6 additions & 6 deletions backend/tests/test_books.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,19 +281,19 @@ def test_list_books_search_by_language(client: TestClient) -> None:
assert body["books"][0]["title"] == "Dune"


def test_list_books_search_by_availability(client: TestClient) -> None:
def test_list_books_search_by_possession(client: TestClient) -> None:
_create_book(client, title="Borrowed", acquisition_status="borrowed")
_create_book(client, title="Owned", acquisition_status="owned")
resp = client.get("/api/books?q=availability:borrowed")
resp = client.get("/api/books?q=possession:borrowed")
assert resp.status_code == 200
body = resp.json()
assert body["total"] == 1
assert body["books"][0]["title"] == "Borrowed"


def test_list_books_search_by_availability_invalid_value(client: TestClient) -> None:
def test_list_books_search_by_possession_invalid_value(client: TestClient) -> None:
_create_book(client, title="Borrowed", acquisition_status="borrowed")
resp = client.get("/api/books?q=availability:not-a-status")
resp = client.get("/api/books?q=possession:not-a-status")
assert resp.status_code == 200
assert resp.json()["total"] == 0

Expand Down Expand Up @@ -393,10 +393,10 @@ def test_list_books_search_negation_includes_nullable_field_rows(client: TestCli
assert [b["title"] for b in body["books"]] == ["Plain"]


def test_list_books_search_availability_quoted_multiword(client: TestClient) -> None:
def test_list_books_search_possession_quoted_multiword(client: TestClient) -> None:
_create_book(client, title="Wanted", acquisition_status="to_acquire")
_create_book(client, title="Owned", acquisition_status="owned")
resp = client.get('/api/books?q=availability:"to acquire"')
resp = client.get('/api/books?q=possession:"to acquire"')
assert resp.status_code == 200
body = resp.json()
assert [b["title"] for b in body["books"]] == ["Wanted"]
Expand Down
22 changes: 11 additions & 11 deletions backend/tests/test_search_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,30 +86,30 @@ def test_parse_bare_prefix() -> None:


def test_parse_all_supported_prefixes() -> None:
query = "author:a title:t publisher:p tag:g language:en availability:owned notes:n description:d"
query = "author:a title:t publisher:p tag:g language:en possession:owned notes:n description:d"
fields = [t.field for t in parse_search_query(query)]
assert fields == [
"author",
"title",
"publisher",
"tag",
"language",
"availability",
"possession",
"notes",
"description",
]


def test_availability_condition_accepts_enum_values() -> None:
from app.services.search import _availability_condition
def test_possession_condition_accepts_enum_values() -> None:
from app.services.search import _possession_condition

assert _availability_condition("owned") is not None
assert _availability_condition("digital_access") is not None
assert _availability_condition("to acquire") is not None
assert _availability_condition("owned") is not None
assert _possession_condition("owned") is not None
assert _possession_condition("digital_access") is not None
assert _possession_condition("to acquire") is not None
assert _possession_condition("owned") is not None


def test_availability_condition_rejects_unknown_value() -> None:
from app.services.search import _availability_condition
def test_possession_condition_rejects_unknown_value() -> None:
from app.services.search import _possession_condition

assert _availability_condition("not-a-status") is None
assert _possession_condition("not-a-status") is None
2 changes: 2 additions & 0 deletions docs/.vitepress/config.base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export default defineConfig({
nav: [
{ text: 'Guide', link: '/guide/getting-started' },
{ text: 'API', link: '/api/' },
{ text: 'Releases', link: '/releases' },
{ text: 'About', link: '/about' },
],
sidebar: {
Expand All @@ -60,6 +61,7 @@ export default defineConfig({
],
},
{ text: 'Integrations 🔗', link: '/api/integrations/' },
{ text: 'Release Notes', link: '/releases' },
],
},
{
Expand Down
6 changes: 3 additions & 3 deletions docs/guide/using-librislog/library.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ Books are categorized into four statuses:
| **Read** | Books you've finished |
| **Did Not Finish** | Books you started but abandoned |

Each status has its own tab in the library view, making it easy to browse your collection by reading state.
Each status has its own tab in the library view, making it easy to browse your collection by reading state. A fifth **All Books** tab shows every book regardless of status; like the other tabs it supports search and sorting (smart sort is disabled there, since it's based on per-status defaults).

## Availability
## Possession

Availability is separate from reading status. Choose whether a book is owned, borrowed, available digitally, or still needs to be acquired. In the Want to Read view, books that still need to be acquired show a shopping-cart indicator. Use the availability filter to narrow the list without changing its newest-first order.
Possession is separate from reading status. Choose whether a book is owned, borrowed, available digitally, or still needs to be acquired. In the Want to Read view, books that still need to be acquired show a shopping-cart indicator. Use the possession filter to narrow the list without changing its newest-first order.

![Library](/screenshots/library-read.png)

Expand Down
10 changes: 5 additions & 5 deletions docs/guide/using-librislog/search.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,24 @@ Use `<field>:<value>` to search in a single field. The field prefixes are always
| `publisher` | Publisher | `publisher:Penguin` |
| `language` | Language | `language:Japanese` |
| `tag` | Tag name | `tag:fantasy` |
| `availability` | Acquisition status | `availability:owned` |
| `possession` | Possession status | `possession:owned` |
| `notes` | Private notes | `notes:"to reread"` |
| `description` | Blurb / description | `description:"middle earth"` |

Use quotes for values that contain spaces: `title:"The Silmarillion"`.

The `author:` prefix matches **any** author assigned to a book — a book with multiple authors matches if any of them contains the search value.

### Availability values
### Possession values

The `availability` prefix matches the exact acquisition status. Accepted values include:
The `possession` prefix matches the exact possession status. Accepted values include:

- `to_acquire` (or `to acquire`)
- `owned`
- `borrowed`
- `digital`

Example: `availability:"to acquire"` shows books you want to buy.
Example: `possession:"to acquire"` shows books you want to buy.

## Negation

Expand All @@ -45,7 +45,7 @@ Prefix a term with `-` to exclude matches.
Separate terms with spaces. All terms are combined with **AND**.

- `author:Murakami -title:Norwegian` — Murakami books except those whose title contains "Norwegian"
- `tag:fantasy availability:owned` — owned fantasy books
- `tag:fantasy possession:owned` — owned fantasy books

## Plain text

Expand Down
3 changes: 3 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ hero:
- theme: brand
text: Get Started
link: /guide/getting-started
- theme: alt
text: Release Notes
link: /releases
- theme: alt
text: View on GitHub
link: https://github.com/codebude/librislog
Expand Down
Loading
Loading