Skip to content

Commit 3d8fefb

Browse files
authored
Merge pull request #210 from openzim/display-books-that-need-attention
display books that need attention in inbox
2 parents 8788872 + fed01fe commit 3d8fefb

8 files changed

Lines changed: 297 additions & 102 deletions

File tree

backend/src/cms_backend/api/routes/books.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class BooksGetSchema(BaseModel):
3838
has_error: bool | None = None
3939
needs_file_operation: bool | None = None
4040
location_kinds: list[NotEmptyString] | None = None
41+
needs_attention: bool | None = None
4142

4243

4344
@router.get("")
@@ -57,6 +58,7 @@ def get_books(
5758
has_error=params.has_error,
5859
needs_file_operation=params.needs_file_operation,
5960
location_kinds=params.location_kinds,
61+
needs_attention=params.needs_attention,
6062
)
6163

6264
return ListResponse[BookLightSchema](

backend/src/cms_backend/db/books.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from uuid import UUID
22

33
from pydantic import AnyUrl
4-
from sqlalchemy import String, and_, select
4+
from sqlalchemy import String, and_, or_, select
55
from sqlalchemy.orm import Session as OrmSession
66

77
from cms_backend.db import count_from_stmt
@@ -22,6 +22,7 @@ def get_books(
2222
has_error: bool | None = None,
2323
needs_file_operation: bool | None = None,
2424
location_kinds: list[str] | None = None,
25+
needs_attention: bool | None = None,
2526
) -> ListResult[BookLightSchema]:
2627
"""Get a list of books"""
2728

@@ -60,6 +61,25 @@ def get_books(
6061
if location_kinds is not None:
6162
stmt = stmt.where(Book.location_kind.in_(location_kinds))
6263

64+
if needs_attention is True:
65+
stmt = stmt.where(
66+
or_(
67+
Book.title_id.is_(None),
68+
Book.needs_processing.is_(True),
69+
Book.needs_file_operation.is_(True),
70+
Book.has_error.is_(True),
71+
)
72+
)
73+
elif needs_attention is False:
74+
stmt = stmt.where(
75+
and_(
76+
Book.title_id.is_not(None),
77+
Book.needs_processing.is_(False),
78+
Book.needs_file_operation.is_(False),
79+
Book.has_error.is_(False),
80+
)
81+
)
82+
6383
return ListResult[BookLightSchema](
6484
nb_records=count_from_stmt(session, stmt),
6585
records=[
@@ -88,7 +108,16 @@ def get_books(
88108
name,
89109
date,
90110
flavour,
91-
) in session.execute(stmt.offset(skip).limit(limit)).all()
111+
) in session.execute(
112+
stmt.offset(skip)
113+
.limit(limit)
114+
.order_by(
115+
Book.has_error,
116+
Book.location_kind,
117+
Book.needs_file_operation,
118+
Book.created_at.desc(),
119+
)
120+
).all()
92121
],
93122
)
94123

backend/tests/api/routes/test_books.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,68 @@ def test_get_books_combined_filters(
171171
assert response_doc["meta"]["count"] == 2
172172

173173

174+
@pytest.mark.parametrize(
175+
"needs_attention,expected_count",
176+
[
177+
pytest.param(None, 5, id="no-filter"),
178+
pytest.param(True, 4, id="needs-attention"),
179+
pytest.param(False, 1, id="does-not-need-attention"),
180+
],
181+
)
182+
def test_get_books_filter_by_needs_attention(
183+
client: TestClient,
184+
create_book: Callable[..., Book],
185+
create_title: Callable[..., Title],
186+
needs_attention: bool | None,
187+
expected_count: int,
188+
):
189+
"""Test get books endpoint filtering by needs_attention"""
190+
191+
title = create_title()
192+
193+
book_with_title = create_book(zim_metadata={"Name": title.name})
194+
title.books.append(book_with_title)
195+
196+
book_without_title = create_book(zim_metadata={"Name": "different_name"})
197+
198+
book_needs_processing = create_book(zim_metadata={"Name": title.name})
199+
title.books.append(book_needs_processing)
200+
book_needs_processing.needs_processing = True
201+
202+
book_has_error = create_book(zim_metadata={"Name": title.name})
203+
title.books.append(book_has_error)
204+
book_has_error.has_error = True
205+
206+
book_needs_file_operation = create_book(zim_metadata={"Name": title.name})
207+
title.books.append(book_needs_file_operation)
208+
book_needs_file_operation.needs_file_operation = True
209+
210+
url = (
211+
"/v1/books"
212+
if needs_attention is None
213+
else f"/v1/books?needs_attention={str(needs_attention).lower()}"
214+
)
215+
response = client.get(url)
216+
217+
assert response.status_code == HTTPStatus.OK
218+
response_doc = response.json()
219+
assert response_doc["meta"]["count"] == expected_count
220+
assert len(response_doc["items"]) == expected_count
221+
222+
if needs_attention is True:
223+
returned_ids = {item["id"] for item in response_doc["items"]}
224+
assert returned_ids == {
225+
str(book_without_title.id),
226+
str(book_needs_processing.id),
227+
str(book_has_error.id),
228+
str(book_needs_file_operation.id),
229+
}
230+
231+
if needs_attention is False:
232+
returned_ids = {item["id"] for item in response_doc["items"]}
233+
assert returned_ids == {str(book_with_title.id)}
234+
235+
174236
def test_get_books_filter_by_id(
175237
client: TestClient,
176238
create_book: Callable[..., Book],

backend/tests/db/test_books.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,67 @@ def test_get_books_book_id_combined_with_other_filters(
265265
assert results.records[0].id == book3.id
266266

267267

268+
@pytest.mark.parametrize(
269+
"needs_attention,expected_count",
270+
[
271+
pytest.param(None, 5, id="no-filter"),
272+
pytest.param(True, 4, id="needs-attention"),
273+
pytest.param(False, 1, id="does-not-need-attention"),
274+
],
275+
)
276+
def test_get_books_filter_by_needs_attention(
277+
dbsession: OrmSession,
278+
create_book: Callable[..., Book],
279+
create_title: Callable[..., Title],
280+
needs_attention: bool | None,
281+
expected_count: int,
282+
):
283+
"""Test that get_books works correctly with needs_attention filter"""
284+
285+
title = create_title()
286+
287+
book_with_title = create_book(zim_metadata={"Name": title.name})
288+
title.books.append(book_with_title)
289+
290+
book_without_title = create_book()
291+
292+
book_needs_processing = create_book(zim_metadata={"Name": title.name})
293+
title.books.append(book_needs_processing)
294+
book_needs_processing.needs_processing = True
295+
296+
book_has_error = create_book(zim_metadata={"Name": title.name})
297+
title.books.append(book_has_error)
298+
book_has_error.has_error = True
299+
300+
book_needs_file_operation = create_book(zim_metadata={"Name": title.name})
301+
title.books.append(book_needs_file_operation)
302+
book_needs_file_operation.needs_file_operation = True
303+
304+
dbsession.flush()
305+
306+
results = get_books(
307+
dbsession,
308+
skip=0,
309+
limit=20,
310+
needs_attention=needs_attention,
311+
)
312+
313+
assert results.nb_records == expected_count
314+
assert len(results.records) == expected_count
315+
316+
if needs_attention is True:
317+
returned_ids = {record.id for record in results.records}
318+
assert returned_ids == {
319+
book_without_title.id,
320+
book_needs_processing.id,
321+
book_has_error.id,
322+
book_needs_file_operation.id,
323+
}
324+
elif needs_attention is False:
325+
returned_ids = {record.id for record in results.records}
326+
assert returned_ids == {book_with_title.id}
327+
328+
268329
def test_get_zim_urls(
269330
dbsession: OrmSession,
270331
create_book: Callable[..., Book],

frontend/src/components/BookFilters.vue

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,20 @@
2727
@click:clear="handleClearFilters"
2828
/>
2929
</v-col>
30+
<v-col cols="12" sm="6" md="3">
31+
<v-select
32+
v-model="localFilters.flag"
33+
label="Flag"
34+
:items="flagOptions"
35+
placeholder="Select flag"
36+
variant="outlined"
37+
density="compact"
38+
hide-details
39+
:clearable="hasActiveFilters"
40+
@update:model-value="emitFilters"
41+
@click:clear="handleClearFilters"
42+
/>
43+
</v-col>
3044
<v-col
3145
v-if="hasActiveFilters"
3246
cols="12"
@@ -50,6 +64,7 @@ interface Props {
5064
filters: {
5165
id: string
5266
location_kind: string
67+
flag: string
5368
}
5469
locationKindOptions?: string[]
5570
}
@@ -64,6 +79,7 @@ const emit = defineEmits<{
6479
filters: {
6580
id: string
6681
location_kind: string
82+
flag: string
6783
},
6884
]
6985
clearFilters: []
@@ -73,6 +89,7 @@ const emit = defineEmits<{
7389
const localFilters = ref({
7490
id: props.filters.id,
7591
location_kind: props.filters.location_kind,
92+
flag: props.filters.flag,
7693
})
7794
7895
// Watch for prop changes and update local state
@@ -82,6 +99,7 @@ watch(
8299
localFilters.value = {
83100
id: newFilters.id,
84101
location_kind: newFilters.location_kind,
102+
flag: newFilters.flag,
85103
}
86104
},
87105
)
@@ -93,15 +111,27 @@ const formattedLocationKindOptions = computed(() => {
93111
}))
94112
})
95113
114+
const flagOptions = [
115+
{ title: 'Needs File Operation', value: 'needs_file_operation' },
116+
{ title: 'Needs Processing', value: 'needs_processing' },
117+
{ title: 'Has Error', value: 'has_error' },
118+
{ title: 'Pending Title', value: 'no_title' },
119+
]
120+
96121
const hasActiveFilters = computed(() => {
97-
return props.filters.id.length > 0 || props.filters.location_kind.length > 0
122+
return (
123+
props.filters.id.length > 0 ||
124+
props.filters.location_kind.length > 0 ||
125+
props.filters.flag?.length > 0
126+
)
98127
})
99128
100129
// Emit filters when they change
101130
function emitFilters() {
102131
emit('filtersChanged', {
103132
id: localFilters.value.id,
104133
location_kind: localFilters.value.location_kind,
134+
flag: localFilters.value.flag,
105135
})
106136
}
107137

0 commit comments

Comments
 (0)