Skip to content

Commit 5eb6e0a

Browse files
committed
Added selecting comics by their read or unread state [#2256]
1 parent eaa443c commit 5eb6e0a

File tree

19 files changed

+465
-23
lines changed

19 files changed

+465
-23
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* ComiXed - A digital comic book library management application.
3+
* Copyright (C) 2024, The ComiXed Project
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses>
17+
*/
18+
package org.comixedproject.model.net.comicbooks;
19+
20+
import com.fasterxml.jackson.annotation.JsonProperty;
21+
import lombok.AllArgsConstructor;
22+
import lombok.Getter;
23+
import lombok.NoArgsConstructor;
24+
25+
/**
26+
* <code>UnreadComicBooksSelectionRequest</code> represents the request body when setting the
27+
* selected state for comics based on their read state.
28+
*
29+
* @author Darryl L. Pierce
30+
*/
31+
@NoArgsConstructor
32+
@AllArgsConstructor
33+
public class UnreadComicBooksSelectionRequest {
34+
@JsonProperty("selected")
35+
@Getter
36+
private boolean selected;
37+
38+
@JsonProperty("unreadOnly")
39+
@Getter
40+
private boolean unreadOnly;
41+
}

comixed-repositories/src/main/java/org/comixedproject/repositories/library/LastReadRepository.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,23 @@ List<LastRead> loadByComicBookIds(
9191
@Query(
9292
"SELECT new org.comixedproject.model.net.user.ComicsReadStatistic(d.publisher, COUNT(d)) FROM ComicDetail d WHERE d.publisher IS NOT NULL AND d IN (SELECT l.comicDetail FROM LastRead l WHERE l.user = :user) GROUP BY d.publisher")
9393
List<ComicsReadStatistic> loadComicsReadStatistics(@Param("user") ComiXedUser user);
94+
95+
/**
96+
* Returns the list of read comic book ids for the specified user.
97+
*
98+
* @param email the user
99+
* @return the comic book ids
100+
*/
101+
@Query("SELECT l.comicDetail.id FROM LastRead l WHERE l.user.email = :email")
102+
List<Long> loadReadComicBookIds(@Param("email") String email);
103+
104+
/**
105+
* Returns the list of unread comic detail ids for the specified user.
106+
*
107+
* @param email the user
108+
* @return the comic book ids
109+
*/
110+
@Query(
111+
"SELECT d.id FROM ComicDetail d WHERE d.id NOT IN (select l.comicDetail.id FROM LastRead l WHERE l.user.email = :email)")
112+
List<Long> loadUnreadComicBookIds(@Param("email") String email);
94113
}

comixed-rest/src/main/java/org/comixedproject/rest/comicbooks/ComicBookSelectionController.java

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import io.micrometer.core.annotation.Timed;
2222
import jakarta.servlet.http.HttpSession;
23+
import java.security.Principal;
2324
import java.util.List;
2425
import lombok.extern.log4j.Log4j2;
2526
import org.comixedproject.model.comicbooks.ComicTagType;
@@ -28,6 +29,7 @@
2829
import org.comixedproject.service.comicbooks.ComicBookSelectionException;
2930
import org.comixedproject.service.comicbooks.ComicBookSelectionService;
3031
import org.comixedproject.service.comicbooks.ComicBookService;
32+
import org.comixedproject.service.library.LastReadService;
3133
import org.springframework.beans.factory.annotation.Autowired;
3234
import org.springframework.http.MediaType;
3335
import org.springframework.security.access.prepost.PreAuthorize;
@@ -46,6 +48,7 @@ public class ComicBookSelectionController {
4648
public static final String LIBRARY_SELECTIONS = "library.selections";
4749

4850
@Autowired private ComicBookSelectionService comicBookSelectionService;
51+
@Autowired private LastReadService lastReadService;
4952
@Autowired private OPDSUtils opdsUtils;
5053
@Autowired private ComicBookService comicBookService;
5154

@@ -169,7 +172,7 @@ public void addComicBooksByTagTypeAndValue(
169172
final List selections =
170173
this.comicBookSelectionService.decodeSelections(session.getAttribute(LIBRARY_SELECTIONS));
171174
this.comicBookSelectionService.addByTagTypeAndValue(selections, tagType, tagValue);
172-
this.comicBookSelectionService.publisherSelections(selections);
175+
this.comicBookSelectionService.publishSelections(selections);
173176
session.setAttribute(
174177
LIBRARY_SELECTIONS, this.comicBookSelectionService.encodeSelections(selections));
175178
}
@@ -200,7 +203,7 @@ public void removeComicBooksByTagTypeAndValue(
200203
final List selections =
201204
this.comicBookSelectionService.decodeSelections(session.getAttribute(LIBRARY_SELECTIONS));
202205
this.comicBookSelectionService.removeByTagTypeAndValue(selections, tagType, tagValue);
203-
this.comicBookSelectionService.publisherSelections(selections);
206+
this.comicBookSelectionService.publishSelections(selections);
204207
session.setAttribute(
205208
LIBRARY_SELECTIONS, this.comicBookSelectionService.encodeSelections(selections));
206209
}
@@ -229,7 +232,7 @@ public void addComicBookSelectionsById(
229232
selections.removeAll(request.getComicBookIds());
230233
}
231234

232-
this.comicBookSelectionService.publisherSelections(selections);
235+
this.comicBookSelectionService.publishSelections(selections);
233236
session.setAttribute(
234237
LIBRARY_SELECTIONS, this.comicBookSelectionService.encodeSelections(selections));
235238
}
@@ -262,7 +265,7 @@ public void addComicBookSelectionsByPublisher(
262265
selections.removeAll(comicBookService.getIdsByPublisher(publisher));
263266
}
264267

265-
this.comicBookSelectionService.publisherSelections(selections);
268+
this.comicBookSelectionService.publishSelections(selections);
266269
session.setAttribute(
267270
LIBRARY_SELECTIONS, this.comicBookSelectionService.encodeSelections(selections));
268271
}
@@ -304,7 +307,7 @@ public void addComicBookSelectionsByPublisherSeriesVolume(
304307
comicBookService.getIdsByPublisherSeriesAndVolume(publisher, series, volume));
305308
}
306309

307-
this.comicBookSelectionService.publisherSelections(selections);
310+
this.comicBookSelectionService.publishSelections(selections);
308311
session.setAttribute(
309312
LIBRARY_SELECTIONS, this.comicBookSelectionService.encodeSelections(selections));
310313
}
@@ -336,7 +339,44 @@ public void addDuplicateComicBooksSelection(
336339
selections.removeAll(idList);
337340
}
338341

339-
this.comicBookSelectionService.publisherSelections(selections);
342+
this.comicBookSelectionService.publishSelections(selections);
343+
session.setAttribute(
344+
LIBRARY_SELECTIONS, this.comicBookSelectionService.encodeSelections(selections));
345+
}
346+
347+
/**
348+
* Sets the selected state on comic books based on their read state.
349+
*
350+
* @param session the user session
351+
* @param principal the user principal
352+
* @param request the request body
353+
* @throws ComicBookSelectionException if an error occurs
354+
*/
355+
@PostMapping(value = "/api/comics/selections/unread", consumes = MediaType.APPLICATION_JSON_VALUE)
356+
@PreAuthorize("hasRole('READER')")
357+
@Timed(value = "comixed.comic-book.selections.add-by-unread-state")
358+
public void addUnreadComicBooksSelection(
359+
final HttpSession session,
360+
final Principal principal,
361+
@RequestBody() final UnreadComicBooksSelectionRequest request)
362+
throws ComicBookSelectionException {
363+
final String email = principal.getName();
364+
final boolean selected = request.isSelected();
365+
final boolean unread = request.isUnreadOnly();
366+
log.info(
367+
"Setting the selection state for comics based on their read state: email={} selected={} unread={}",
368+
email,
369+
selected,
370+
unread);
371+
final List<Long> selections =
372+
this.comicBookSelectionService.decodeSelections(session.getAttribute(LIBRARY_SELECTIONS));
373+
374+
if (selected) {
375+
selections.addAll(this.lastReadService.getComicBookIdsForUser(email, unread));
376+
} else {
377+
selections.removeAll(this.lastReadService.getComicBookIdsForUser(email, unread));
378+
}
379+
this.comicBookSelectionService.publishSelections(selections);
340380
session.setAttribute(
341381
LIBRARY_SELECTIONS, this.comicBookSelectionService.encodeSelections(selections));
342382
}

comixed-rest/src/test/java/org/comixedproject/rest/comicbooks/ComicBookSelectionControllerTest.java

Lines changed: 78 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import static org.comixedproject.rest.comicbooks.ComicBookSelectionController.LIBRARY_SELECTIONS;
2323

2424
import jakarta.servlet.http.HttpSession;
25+
import java.security.Principal;
2526
import java.util.ArrayList;
2627
import java.util.List;
2728
import org.comixedproject.model.comicbooks.ComicTagType;
@@ -30,6 +31,7 @@
3031
import org.comixedproject.service.comicbooks.ComicBookSelectionException;
3132
import org.comixedproject.service.comicbooks.ComicBookSelectionService;
3233
import org.comixedproject.service.comicbooks.ComicBookService;
34+
import org.comixedproject.service.library.LastReadService;
3335
import org.junit.Before;
3436
import org.junit.Test;
3537
import org.junit.runner.RunWith;
@@ -48,13 +50,17 @@ public class ComicBookSelectionControllerTest {
4850
private static final String TEST_PUBLISHER = "The Publisher";
4951
private static final String TEST_SERIES = "The Series";
5052
private static final String TEST_VOLUME = "2024";
53+
private static final String TEST_EMAIL = "[email protected]";
5154

5255
@InjectMocks private ComicBookSelectionController controller;
5356
@Mock private ComicBookSelectionService comicBookSelectionService;
5457
@Mock private ComicBookService comicBookService;
58+
@Mock private LastReadService lastReadService;
5559
@Mock private OPDSUtils opdsUtils;
5660
@Mock private HttpSession httpSession;
5761
@Mock private List selectionIdList;
62+
@Mock private Principal principal;
63+
@Mock private List<Long> comicDetailIdList;
5864

5965
@Before
6066
public void setUp() throws ComicBookSelectionException {
@@ -63,6 +69,9 @@ public void setUp() throws ComicBookSelectionException {
6369
.thenReturn(selectionIdList);
6470
Mockito.when(comicBookSelectionService.encodeSelections(Mockito.anyList()))
6571
.thenReturn(TEST_REENCODED_SELECTIONS);
72+
Mockito.when(principal.getName()).thenReturn(TEST_EMAIL);
73+
Mockito.when(lastReadService.getComicBookIdsForUser(Mockito.anyString(), Mockito.anyBoolean()))
74+
.thenReturn(comicDetailIdList);
6675
}
6776

6877
@Test
@@ -76,7 +85,7 @@ public void testGetAllSelections() throws ComicBookSelectionException {
7685
}
7786

7887
@Test
79-
public void testAddSingleSelection() throws ComicBookSelectionException {
88+
public void testAddSingleSelection_selecting() throws ComicBookSelectionException {
8089
controller.addSingleSelection(httpSession, TEST_COMIC_BOOK_ID);
8190

8291
Mockito.verify(comicBookSelectionService, Mockito.times(1))
@@ -87,7 +96,7 @@ public void testAddSingleSelection() throws ComicBookSelectionException {
8796
}
8897

8998
@Test
90-
public void testRemoveSingleSelection() throws ComicBookSelectionException {
99+
public void testAddSingleSelection_deselecting() throws ComicBookSelectionException {
91100
controller.deleteSingleSelection(httpSession, TEST_COMIC_BOOK_ID);
92101

93102
Mockito.verify(httpSession, Mockito.times(1)).getAttribute(LIBRARY_SELECTIONS);
@@ -99,7 +108,7 @@ public void testRemoveSingleSelection() throws ComicBookSelectionException {
99108
}
100109

101110
@Test
102-
public void testSelectMultipleComicWithAdded() throws ComicBookSelectionException {
111+
public void testSelectComicBooksByFilter_selecting() throws ComicBookSelectionException {
103112
controller.selectComicBooksByFilter(
104113
httpSession,
105114
new MultipleComicBooksSelectionRequest(null, null, null, null, null, false, null, true));
@@ -112,7 +121,7 @@ public void testSelectMultipleComicWithAdded() throws ComicBookSelectionExceptio
112121
}
113122

114123
@Test
115-
public void testSelectMultipleComicWithRemoving() throws ComicBookSelectionException {
124+
public void testSelectComicBooksByFilter_deselecting() throws ComicBookSelectionException {
116125
controller.selectComicBooksByFilter(
117126
httpSession,
118127
new MultipleComicBooksSelectionRequest(null, null, null, null, null, false, null, false));
@@ -126,8 +135,7 @@ public void testSelectMultipleComicWithRemoving() throws ComicBookSelectionExcep
126135

127136
@Test
128137
public void testAddComicBooksByTagTypeAndValue() throws ComicBookSelectionException {
129-
Mockito.when(opdsUtils.urlDecodeString(TEST_TAG_VALUE.toString()))
130-
.thenReturn(TEST_TAG_VALUE.toString());
138+
Mockito.when(opdsUtils.urlDecodeString(TEST_TAG_VALUE)).thenReturn(TEST_TAG_VALUE);
131139

132140
controller.addComicBooksByTagTypeAndValue(
133141
httpSession, TEST_TAG_TYPE.getValue(), TEST_TAG_VALUE);
@@ -142,8 +150,7 @@ public void testAddComicBooksByTagTypeAndValue() throws ComicBookSelectionExcept
142150

143151
@Test
144152
public void testRemoveComicBooksByTagTypeAndValue() throws ComicBookSelectionException {
145-
Mockito.when(opdsUtils.urlDecodeString(TEST_TAG_VALUE.toString()))
146-
.thenReturn(TEST_TAG_VALUE.toString());
153+
Mockito.when(opdsUtils.urlDecodeString(TEST_TAG_VALUE)).thenReturn(TEST_TAG_VALUE);
147154

148155
controller.removeComicBooksByTagTypeAndValue(
149156
httpSession, TEST_TAG_TYPE.getValue(), TEST_TAG_VALUE);
@@ -157,7 +164,7 @@ public void testRemoveComicBooksByTagTypeAndValue() throws ComicBookSelectionExc
157164
}
158165

159166
@Test
160-
public void testAddSelectionsByIdForAddition() throws ComicBookSelectionException {
167+
public void testAddSelectionsById_selecting() throws ComicBookSelectionException {
161168
final List<Long> comicBookIdList = new ArrayList<Long>();
162169
for (long index = 1000L; index < 2000L; index++) comicBookIdList.add(index);
163170

@@ -172,7 +179,7 @@ public void testAddSelectionsByIdForAddition() throws ComicBookSelectionExceptio
172179
}
173180

174181
@Test
175-
public void testAddSelectionsByIdForRemoval() throws ComicBookSelectionException {
182+
public void testAddSelectionsById_deselecting() throws ComicBookSelectionException {
176183
final List<Long> comicBookIdList = new ArrayList<Long>();
177184
for (long index = 1000L; index < 2000L; index++) comicBookIdList.add(index);
178185

@@ -187,7 +194,8 @@ public void testAddSelectionsByIdForRemoval() throws ComicBookSelectionException
187194
}
188195

189196
@Test
190-
public void testAddSelectionsByPublisherForAddition() throws ComicBookSelectionException {
197+
public void testAddSelectionsByPublisherForAddition_selecting()
198+
throws ComicBookSelectionException {
191199
Mockito.when(comicBookService.getIdsByPublisher(Mockito.anyString()))
192200
.thenReturn(selectionIdList);
193201

@@ -203,7 +211,8 @@ public void testAddSelectionsByPublisherForAddition() throws ComicBookSelectionE
203211
}
204212

205213
@Test
206-
public void testAddSelectionsByPublisherForRemoval() throws ComicBookSelectionException {
214+
public void testAddSelectionsByPublisherForAddition_deselecting()
215+
throws ComicBookSelectionException {
207216
Mockito.when(comicBookService.getIdsByPublisher(Mockito.anyString()))
208217
.thenReturn(selectionIdList);
209218

@@ -219,7 +228,7 @@ public void testAddSelectionsByPublisherForRemoval() throws ComicBookSelectionEx
219228
}
220229

221230
@Test
222-
public void testAddSelectionsByPublisherSeriesVolumeForAddition()
231+
public void testAddSelectionsByPublisherSeriesVolume_selecting()
223232
throws ComicBookSelectionException {
224233
Mockito.when(
225234
comicBookService.getIdsByPublisherSeriesAndVolume(
@@ -241,7 +250,7 @@ public void testAddSelectionsByPublisherSeriesVolumeForAddition()
241250
}
242251

243252
@Test
244-
public void testAddSelectionsByPublisherSeriesVolumeForRemoval()
253+
public void testAddSelectionsByPublisherSeriesVolume_deselecting()
245254
throws ComicBookSelectionException {
246255
Mockito.when(
247256
comicBookService.getIdsByPublisherSeriesAndVolume(
@@ -292,6 +301,61 @@ public void testRemoveDuplicateComicBooksToSelections() throws ComicBookSelectio
292301
.setAttribute(LIBRARY_SELECTIONS, TEST_REENCODED_SELECTIONS);
293302
}
294303

304+
@Test
305+
public void testAddUnreadComicBooksSelection_selectingRead() throws ComicBookSelectionException {
306+
controller.addUnreadComicBooksSelection(
307+
httpSession, principal, new UnreadComicBooksSelectionRequest(true, false));
308+
309+
Mockito.verify(httpSession, Mockito.times(1)).getAttribute(LIBRARY_SELECTIONS);
310+
Mockito.verify(lastReadService, Mockito.times(1)).getComicBookIdsForUser(TEST_EMAIL, false);
311+
Mockito.verify(selectionIdList, Mockito.times(1)).addAll(comicDetailIdList);
312+
Mockito.verify(comicBookSelectionService, Mockito.times(1)).encodeSelections(selectionIdList);
313+
Mockito.verify(httpSession, Mockito.times(1))
314+
.setAttribute(LIBRARY_SELECTIONS, TEST_REENCODED_SELECTIONS);
315+
}
316+
317+
@Test
318+
public void testAddUnreadComicBooksSelection_deselectingRead()
319+
throws ComicBookSelectionException {
320+
controller.addUnreadComicBooksSelection(
321+
httpSession, principal, new UnreadComicBooksSelectionRequest(false, false));
322+
323+
Mockito.verify(httpSession, Mockito.times(1)).getAttribute(LIBRARY_SELECTIONS);
324+
Mockito.verify(lastReadService, Mockito.times(1)).getComicBookIdsForUser(TEST_EMAIL, false);
325+
Mockito.verify(selectionIdList, Mockito.times(1)).removeAll(comicDetailIdList);
326+
Mockito.verify(comicBookSelectionService, Mockito.times(1)).encodeSelections(selectionIdList);
327+
Mockito.verify(httpSession, Mockito.times(1))
328+
.setAttribute(LIBRARY_SELECTIONS, TEST_REENCODED_SELECTIONS);
329+
}
330+
331+
@Test
332+
public void testAddUnreadComicBooksSelection_selectingUnread()
333+
throws ComicBookSelectionException {
334+
controller.addUnreadComicBooksSelection(
335+
httpSession, principal, new UnreadComicBooksSelectionRequest(true, true));
336+
337+
Mockito.verify(httpSession, Mockito.times(1)).getAttribute(LIBRARY_SELECTIONS);
338+
Mockito.verify(lastReadService, Mockito.times(1)).getComicBookIdsForUser(TEST_EMAIL, true);
339+
Mockito.verify(selectionIdList, Mockito.times(1)).addAll(comicDetailIdList);
340+
Mockito.verify(comicBookSelectionService, Mockito.times(1)).encodeSelections(selectionIdList);
341+
Mockito.verify(httpSession, Mockito.times(1))
342+
.setAttribute(LIBRARY_SELECTIONS, TEST_REENCODED_SELECTIONS);
343+
}
344+
345+
@Test
346+
public void testAddUnreadComicBooksSelection_deselectingUnread()
347+
throws ComicBookSelectionException {
348+
controller.addUnreadComicBooksSelection(
349+
httpSession, principal, new UnreadComicBooksSelectionRequest(false, true));
350+
351+
Mockito.verify(httpSession, Mockito.times(1)).getAttribute(LIBRARY_SELECTIONS);
352+
Mockito.verify(lastReadService, Mockito.times(1)).getComicBookIdsForUser(TEST_EMAIL, true);
353+
Mockito.verify(selectionIdList, Mockito.times(1)).removeAll(comicDetailIdList);
354+
Mockito.verify(comicBookSelectionService, Mockito.times(1)).encodeSelections(selectionIdList);
355+
Mockito.verify(httpSession, Mockito.times(1))
356+
.setAttribute(LIBRARY_SELECTIONS, TEST_REENCODED_SELECTIONS);
357+
}
358+
295359
@Test
296360
public void testClearSelections() throws ComicBookSelectionException {
297361
controller.clearSelections(httpSession);

0 commit comments

Comments
 (0)