diff --git a/comixed-messaging/src/main/java/org/comixedproject/messaging/library/PublishLastReadRemovedAction.java b/comixed-messaging/src/main/java/org/comixedproject/messaging/library/PublishLastReadRemovedAction.java index abde7465d..3298737a4 100644 --- a/comixed-messaging/src/main/java/org/comixedproject/messaging/library/PublishLastReadRemovedAction.java +++ b/comixed-messaging/src/main/java/org/comixedproject/messaging/library/PublishLastReadRemovedAction.java @@ -37,6 +37,7 @@ public class PublishLastReadRemovedAction extends AbstractPublishActionPublishLastReadUpdateAction publishes updates to {@link LastRead} instances. + * PublishLastReadUpdatedAction publishes updates to {@link LastRead} instances. * * @author Darryl L. Pierce */ @Component @Log4j2 -public class PublishLastReadUpdateAction extends AbstractPublishAction { +public class PublishLastReadUpdatedAction extends AbstractPublishAction { @Override public void publish(final LastRead lastRead) throws PublishingException { log.trace("Publishing last read update"); - this.doPublish(Constants.LAST_READ_UPDATE_TOPIC, lastRead, View.LastReadList.class); + this.doPublish( + lastRead.getUser(), Constants.LAST_READ_UPDATED_TOPIC, lastRead, View.LastReadList.class); } } diff --git a/comixed-messaging/src/test/java/org/comixedproject/messaging/library/PublishLastReadRemovedActionTest.java b/comixed-messaging/src/test/java/org/comixedproject/messaging/library/PublishLastReadRemovedActionTest.java index ec38f7a0b..f1deb9a3c 100644 --- a/comixed-messaging/src/test/java/org/comixedproject/messaging/library/PublishLastReadRemovedActionTest.java +++ b/comixed-messaging/src/test/java/org/comixedproject/messaging/library/PublishLastReadRemovedActionTest.java @@ -24,6 +24,7 @@ import org.comixedproject.messaging.PublishingException; import org.comixedproject.model.library.LastRead; import org.comixedproject.model.messaging.Constants; +import org.comixedproject.model.user.ComiXedUser; import org.comixedproject.views.View; import org.junit.Before; import org.junit.Test; @@ -37,17 +38,21 @@ @RunWith(MockitoJUnitRunner.class) public class PublishLastReadRemovedActionTest { private static final String TEST_LAST_READ_AS_JSON = "Object as JSON"; + private static final String TEST_EMAIL = "reader@comixedproject.org"; @InjectMocks private PublishLastReadRemovedAction action; @Mock private SimpMessagingTemplate messagingTemplate; @Mock private ObjectMapper objectMapper; @Mock private ObjectWriter objectWriter; @Mock private LastRead lastRead; + @Mock private ComiXedUser user; @Before public void setUp() throws JsonProcessingException { Mockito.when(objectMapper.writerWithView(Mockito.any())).thenReturn(objectWriter); Mockito.when(objectWriter.writeValueAsString(Mockito.any())).thenReturn(TEST_LAST_READ_AS_JSON); + Mockito.when(lastRead.getUser()).thenReturn(user); + Mockito.when(user.getEmail()).thenReturn(TEST_EMAIL); } @Test(expected = PublishingException.class) @@ -72,6 +77,7 @@ public void testPublish() throws PublishingException, JsonProcessingException { Mockito.verify(objectMapper, Mockito.times(1)).writerWithView(View.LastReadList.class); Mockito.verify(objectWriter, Mockito.times(1)).writeValueAsString(lastRead); Mockito.verify(messagingTemplate, Mockito.times(1)) - .convertAndSend(Constants.LAST_READ_REMOVAL_TOPIC, TEST_LAST_READ_AS_JSON); + .convertAndSendToUser( + TEST_EMAIL, Constants.LAST_READ_REMOVED_TOPIC, TEST_LAST_READ_AS_JSON); } } diff --git a/comixed-messaging/src/test/java/org/comixedproject/messaging/library/PublishLastReadUpdateActionTest.java b/comixed-messaging/src/test/java/org/comixedproject/messaging/library/PublishLastReadUpdatedActionTest.java similarity index 85% rename from comixed-messaging/src/test/java/org/comixedproject/messaging/library/PublishLastReadUpdateActionTest.java rename to comixed-messaging/src/test/java/org/comixedproject/messaging/library/PublishLastReadUpdatedActionTest.java index 31bdeacd9..3e98f3888 100644 --- a/comixed-messaging/src/test/java/org/comixedproject/messaging/library/PublishLastReadUpdateActionTest.java +++ b/comixed-messaging/src/test/java/org/comixedproject/messaging/library/PublishLastReadUpdatedActionTest.java @@ -24,6 +24,7 @@ import org.comixedproject.messaging.PublishingException; import org.comixedproject.model.library.LastRead; import org.comixedproject.model.messaging.Constants; +import org.comixedproject.model.user.ComiXedUser; import org.comixedproject.views.View; import org.junit.Before; import org.junit.Test; @@ -35,19 +36,23 @@ import org.springframework.messaging.simp.SimpMessagingTemplate; @RunWith(MockitoJUnitRunner.class) -public class PublishLastReadUpdateActionTest { +public class PublishLastReadUpdatedActionTest { private static final String TEST_LAST_READ_AS_JSON = "Object as JSON"; + private static final String TEST_EMAIL = "read@comixedproject.org"; - @InjectMocks private PublishLastReadUpdateAction action; + @InjectMocks private PublishLastReadUpdatedAction action; @Mock private SimpMessagingTemplate messagingTemplate; @Mock private ObjectMapper objectMapper; @Mock private ObjectWriter objectWriter; @Mock private LastRead lastRead; + @Mock private ComiXedUser user; @Before public void setUp() throws JsonProcessingException { Mockito.when(objectMapper.writerWithView(Mockito.any())).thenReturn(objectWriter); Mockito.when(objectWriter.writeValueAsString(Mockito.any())).thenReturn(TEST_LAST_READ_AS_JSON); + Mockito.when(lastRead.getUser()).thenReturn(user); + Mockito.when(user.getEmail()).thenReturn(TEST_EMAIL); } @Test(expected = PublishingException.class) @@ -72,6 +77,7 @@ public void testPublish() throws PublishingException, JsonProcessingException { Mockito.verify(objectMapper, Mockito.times(1)).writerWithView(View.LastReadList.class); Mockito.verify(objectWriter, Mockito.times(1)).writeValueAsString(lastRead); Mockito.verify(messagingTemplate, Mockito.times(1)) - .convertAndSend(Constants.LAST_READ_UPDATE_TOPIC, TEST_LAST_READ_AS_JSON); + .convertAndSendToUser( + TEST_EMAIL, Constants.LAST_READ_UPDATED_TOPIC, TEST_LAST_READ_AS_JSON); } } diff --git a/comixed-model/src/main/java/org/comixedproject/model/library/LastRead.java b/comixed-model/src/main/java/org/comixedproject/model/library/LastRead.java index 1e5606148..3c0179ccc 100644 --- a/comixed-model/src/main/java/org/comixedproject/model/library/LastRead.java +++ b/comixed-model/src/main/java/org/comixedproject/model/library/LastRead.java @@ -27,7 +27,6 @@ import org.comixedproject.model.user.ComiXedUser; import org.comixedproject.views.View; import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedDate; /** * LastRead holds the date and time for when a user last read a specific comic. @@ -76,13 +75,6 @@ public class LastRead { @JsonView({View.LastReadList.class, View.AuditLogEntryDetail.class}) private Date createdOn = new Date(); - @Column(name = "LastModifiedOn", nullable = false, updatable = true) - @LastModifiedDate - @Getter - @Setter - @JsonView({View.LastReadList.class, View.AuditLogEntryDetail.class}) - private Date lastModifiedOn = new Date(); - @Override public boolean equals(final Object o) { if (this == o) return true; diff --git a/comixed-model/src/main/java/org/comixedproject/model/messaging/Constants.java b/comixed-model/src/main/java/org/comixedproject/model/messaging/Constants.java index b29431449..578a1adb2 100644 --- a/comixed-model/src/main/java/org/comixedproject/model/messaging/Constants.java +++ b/comixed-model/src/main/java/org/comixedproject/model/messaging/Constants.java @@ -40,10 +40,10 @@ public interface Constants { String CURRENT_USER_UPDATE_TOPIC = "/topic/user/current"; /** Topic which receives notices when last read entries are updated. */ - String LAST_READ_UPDATE_TOPIC = "/topic/last-read-list.update"; + String LAST_READ_UPDATED_TOPIC = "/topic/last-read-list.update"; /** Topic which receives notices when last read entries are removed. */ - String LAST_READ_REMOVAL_TOPIC = "/topic/last-read-list.removal"; + String LAST_READ_REMOVED_TOPIC = "/topic/last-read-list.remove"; /** Topic which receives notices when the duplicate page list is updated. */ String DUPLICATE_PAGE_LIST_TOPIC = "/topic/duplicate-page-list.update"; diff --git a/comixed-model/src/main/java/org/comixedproject/model/net/library/GetLastReadDatesRequest.java b/comixed-model/src/main/java/org/comixedproject/model/net/library/library/SetComicsReadRequest.java similarity index 75% rename from comixed-model/src/main/java/org/comixedproject/model/net/library/GetLastReadDatesRequest.java rename to comixed-model/src/main/java/org/comixedproject/model/net/library/library/SetComicsReadRequest.java index 7281dfc9b..01ac9b34d 100644 --- a/comixed-model/src/main/java/org/comixedproject/model/net/library/GetLastReadDatesRequest.java +++ b/comixed-model/src/main/java/org/comixedproject/model/net/library/library/SetComicsReadRequest.java @@ -16,23 +16,28 @@ * along with this program. If not, see */ -package org.comixedproject.model.net.library; +package org.comixedproject.model.net.library.library; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; /** - * GetLastReadDatesRequest represents the payload for a single request for last read - * dates for a user. + * SetComicsReadRequest represents the request body when marking multiple comics as + * read. * * @author Darryl L. Pierce */ @NoArgsConstructor @AllArgsConstructor -public class GetLastReadDatesRequest { - @JsonProperty("lastId") +public class SetComicsReadRequest { + @JsonProperty("ids") @Getter - private long lastId; + private List ids; + + @JsonProperty("read") + @Getter() + private boolean read; } diff --git a/comixed-model/src/main/resources/db/migrations/0.11.0/009_1059_removed_dates_from_last_read.xml b/comixed-model/src/main/resources/db/migrations/0.11.0/009_1059_removed_dates_from_last_read.xml new file mode 100644 index 000000000..5820f1fa8 --- /dev/null +++ b/comixed-model/src/main/resources/db/migrations/0.11.0/009_1059_removed_dates_from_last_read.xml @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/comixed-model/src/main/resources/db/migrations/0.11.0/changelog-0.11.0.xml b/comixed-model/src/main/resources/db/migrations/0.11.0/changelog-0.11.0.xml index 9cd1f6eae..2a06af8e9 100644 --- a/comixed-model/src/main/resources/db/migrations/0.11.0/changelog-0.11.0.xml +++ b/comixed-model/src/main/resources/db/migrations/0.11.0/changelog-0.11.0.xml @@ -14,5 +14,6 @@ + diff --git a/comixed-repositories/src/main/java/org/comixedproject/repositories/library/LastReadRepository.java b/comixed-repositories/src/main/java/org/comixedproject/repositories/library/LastReadRepository.java index 37a6ff16c..d91f1ccbd 100644 --- a/comixed-repositories/src/main/java/org/comixedproject/repositories/library/LastReadRepository.java +++ b/comixed-repositories/src/main/java/org/comixedproject/repositories/library/LastReadRepository.java @@ -35,7 +35,7 @@ */ public interface LastReadRepository extends CrudRepository { /** - * Retrieves a batch of last read entries. + * Loads a batch of last read entries. * * @param user the user * @param threshold the threshold id @@ -43,16 +43,16 @@ public interface LastReadRepository extends CrudRepository { * @return the entries */ @Query("SELECT e FROM LastRead e WHERE e.id > :threshold AND e.user = :user ORDER BY e.id") - List getEntriesForUser( + List loadEntriesForUser( @Param("user") ComiXedUser user, @Param("threshold") long threshold, Pageable pageRequest); /** - * Retrieves the last read entry for the given user and comic. + * Retrieves the last read entry for the given comic and user. * - * @param user the user * @param comic the comic + * @param user the user * @return the entry, or null if non exists */ - @Query("SELECT e FROM LastRead e WHERE e.user = :user AND e.comic = :comic") - LastRead findEntryForUserAndComic(@Param("user") ComiXedUser user, @Param("comic") Comic comic); + @Query("SELECT e FROM LastRead e WHERE e.comic = :comic AND e.user = :user") + LastRead loadEntryForComicAndUser(@Param("comic") Comic comic, @Param("user") ComiXedUser user); } diff --git a/comixed-repositories/src/test/java/org/comixedproject/repositories/library/LastReadRepositoryTest.java b/comixed-repositories/src/test/java/org/comixedproject/repositories/library/LastReadRepositoryTest.java index e8be65a10..629ec7f20 100644 --- a/comixed-repositories/src/test/java/org/comixedproject/repositories/library/LastReadRepositoryTest.java +++ b/comixed-repositories/src/test/java/org/comixedproject/repositories/library/LastReadRepositoryTest.java @@ -73,18 +73,18 @@ public void setUp() { } @Test - public void testGetEntriesForUserNoEntries() { + public void testLoadEntriesForUserNoEntries() { final List result = - this.repository.getEntriesForUser(userWithoutEntries, 0, PageRequest.of(0, TEST_MAXIMUM)); + this.repository.loadEntriesForUser(userWithoutEntries, 0, PageRequest.of(0, TEST_MAXIMUM)); assertNotNull(result); assertTrue(result.isEmpty()); } @Test - public void testGetEntriesForUser() { + public void testLoadEntriesForUser() { final List result = - this.repository.getEntriesForUser(user, 0, PageRequest.of(0, TEST_MAXIMUM)); + this.repository.loadEntriesForUser(user, 0, PageRequest.of(0, TEST_MAXIMUM)); assertNotNull(result); assertFalse(result.isEmpty()); @@ -96,9 +96,9 @@ public void testGetEntriesForUser() { } @Test - public void testGetEntriesForUserNoMoreEntries() { + public void testLoadEntriesForUserNoMoreEntries() { final List result = - this.repository.getEntriesForUser(user, 1000L, PageRequest.of(0, TEST_MAXIMUM)); + this.repository.loadEntriesForUser(user, 1000L, PageRequest.of(0, TEST_MAXIMUM)); assertNotNull(result); assertTrue(result.isEmpty()); @@ -107,21 +107,21 @@ public void testGetEntriesForUserNoMoreEntries() { @Test public void testFindEntryForUserAndComicForUserWithNoEntries() { final LastRead result = - this.repository.findEntryForUserAndComic(userWithoutEntries, comicWithEntries); + this.repository.loadEntryForComicAndUser(comicWithEntries, userWithoutEntries); assertNull(result); } @Test public void testFindEntryForUserAndComicForUserWithEntriesButNoThisComic() { - final LastRead result = this.repository.findEntryForUserAndComic(user, comicWithNoEntries); + final LastRead result = this.repository.loadEntryForComicAndUser(comicWithNoEntries, user); assertNull(result); } @Test public void testFindEntryForUserAndComic() { - final LastRead result = this.repository.findEntryForUserAndComic(user, comicWithEntries); + final LastRead result = this.repository.loadEntryForComicAndUser(comicWithEntries, user); assertNotNull(result); assertEquals(user, result.getUser()); diff --git a/comixed-repositories/src/test/resources/test-database.xml b/comixed-repositories/src/test/resources/test-database.xml index 37e9ab647..496363aab 100644 --- a/comixed-repositories/src/test/resources/test-database.xml +++ b/comixed-repositories/src/test/resources/test-database.xml @@ -405,62 +405,52 @@ UserId="1000" ComicId="1000" CreatedOn="[now-1d]" - LastReadOn="[now]" - LastModifiedOn="[now-1d]"/> + LastReadOn="[now]"/> + LastReadOn="[now]"/> + LastReadOn="[now]"/> + LastReadOn="[now]"/> + LastReadOn="[now]"/> + LastReadOn="[now]"/> + LastReadOn="[now]"/> + LastReadOn="[now]"/> + LastReadOn="[now]"/> + LastReadOn="[now]"/> LastReadController provides REST APIs for working with the last read dates for + * comics. + * + * @author Darryl L. Pierce + */ @RestController @Log4j2 public class LastReadController { @@ -44,23 +50,19 @@ public class LastReadController { * Retrieves a batch of last read entries for a given user. * * @param principal the user principal - * @param request the request body + * @param lastId the last record id returned * @return the response body * @throws LastReadException if an error occurs */ - @PostMapping( - value = "/api/library/read", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(value = "/api/library/read", produces = MediaType.APPLICATION_JSON_VALUE) @JsonView(View.LastReadList.class) @AuditableEndpoint public GetLastReadDatesResponse getLastReadEntries( - final Principal principal, @RequestBody() final GetLastReadDatesRequest request) + final Principal principal, @RequestParam("lastId") final long lastId) throws LastReadException { final String email = principal.getName(); - final long threshold = request.getLastId(); - log.info("Loading last read entries for user: email={} threshold={}", email, threshold); - List entries = this.lastReadService.getLastReadEntries(email, threshold, MAXIMUM + 1); + log.info("Loading last read entries for user: email={} threshold={}", email, lastId); + List entries = this.lastReadService.getLastReadEntries(email, lastId, MAXIMUM + 1); final boolean lastPayload = entries.size() <= MAXIMUM; if (!lastPayload) { log.trace("Reduce entry list to {} records", MAXIMUM); @@ -70,42 +72,30 @@ public GetLastReadDatesResponse getLastReadEntries( } /** - * Marks a comic as read by the current user. + * Sets the last read state for a set of comics for a given user. * * @param principal the user principal - * @param comicId the comic id - * @return the last read entry + * @param request the request body * @throws LastReadException if an error occurs */ @PostMapping( - value = "/api/comic/{comicId}/read", + value = "/api/library/read", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) - @AuditableEndpoint @JsonView(View.LastReadList.class) - public LastRead markAsRead(final Principal principal, @PathVariable("comicId") final long comicId) - throws LastReadException { - final String email = principal.getName(); - log.info("Marking comic as read for {}: id={}", email, comicId); - return this.lastReadService.setLastReadState(email, comicId, true); - } - - /** - * Marks a comic as unread by the current user. - * - * @param principal the user principal - * @param comicId the comic id - * @return the removed last read entry - * @throws LastReadException if an error occurs - */ - @DeleteMapping(value = "/api/comic/{comicId}/read", produces = MediaType.APPLICATION_JSON_VALUE) @AuditableEndpoint - @JsonView(View.LastReadList.class) - public LastRead markAsUnread( - final Principal principal, @PathVariable("comicId") final long comicId) + public void setComicsReadState( + final Principal principal, @RequestBody() SetComicsReadRequest request) throws LastReadException { + final List ids = request.getIds(); + final boolean read = request.isRead(); final String email = principal.getName(); - log.info("Marking comic as read for {}: id={}", email, comicId); - return this.lastReadService.setLastReadState(email, comicId, false); + log.info( + "Marking {} comic{} as {} for {}", + ids.size(), + ids.size() == 1 ? "" : "s", + read ? "read" : "unread", + email); + this.lastReadService.setLastReadState(email, ids, read); } } diff --git a/comixed-rest/src/test/java/org/comixedproject/rest/library/LastReadControllerTest.java b/comixed-rest/src/test/java/org/comixedproject/rest/library/LastReadControllerTest.java index 5f624eb19..dcdbe6b75 100644 --- a/comixed-rest/src/test/java/org/comixedproject/rest/library/LastReadControllerTest.java +++ b/comixed-rest/src/test/java/org/comixedproject/rest/library/LastReadControllerTest.java @@ -25,8 +25,8 @@ import java.security.Principal; import java.util.List; import org.comixedproject.model.library.LastRead; -import org.comixedproject.model.net.library.GetLastReadDatesRequest; import org.comixedproject.model.net.library.GetLastReadDatesResponse; +import org.comixedproject.model.net.library.library.SetComicsReadRequest; import org.comixedproject.service.library.LastReadException; import org.comixedproject.service.library.LastReadService; import org.junit.Before; @@ -49,6 +49,7 @@ public class LastReadControllerTest { @Mock private List lastReadEntries; @Mock private List reducedLastReadEntries; @Mock private LastRead lastReadEntry; + @Mock private List comicIdList; @Before public void setUp() { @@ -63,7 +64,7 @@ public void testGetLastReadEntriesForUserServiceException() throws LastReadExcep .thenThrow(LastReadException.class); try { - controller.getLastReadEntries(principal, new GetLastReadDatesRequest(TEST_LAST_ID)); + controller.getLastReadEntries(principal, TEST_LAST_ID); } finally { Mockito.verify(lastReadService, Mockito.times(1)) .getLastReadEntries(TEST_EMAIL, TEST_LAST_ID, MAXIMUM + 1); @@ -81,7 +82,7 @@ public void testGetLastReadEntriesTooManyEntries() throws LastReadException { .thenReturn(reducedLastReadEntries); final GetLastReadDatesResponse response = - controller.getLastReadEntries(principal, new GetLastReadDatesRequest(TEST_LAST_ID)); + controller.getLastReadEntries(principal, TEST_LAST_ID); assertNotNull(response); assertSame(reducedLastReadEntries, response.getEntries()); @@ -100,7 +101,7 @@ public void testGetLastReadEntries() throws LastReadException { Mockito.when(lastReadEntries.size()).thenReturn(MAXIMUM); final GetLastReadDatesResponse response = - controller.getLastReadEntries(principal, new GetLastReadDatesRequest(TEST_LAST_ID)); + controller.getLastReadEntries(principal, TEST_LAST_ID); assertNotNull(response); assertSame(lastReadEntries, response.getEntries()); @@ -110,64 +111,22 @@ public void testGetLastReadEntries() throws LastReadException { } @Test(expected = LastReadException.class) - public void testMarkAsReadServiceException() throws LastReadException { - Mockito.when( - lastReadService.setLastReadState( - Mockito.anyString(), Mockito.anyLong(), Mockito.anyBoolean())) - .thenThrow(LastReadException.class); - - try { - controller.markAsRead(principal, TEST_COMIC_ID); - } finally { - Mockito.verify(lastReadService, Mockito.times(1)) - .setLastReadState(TEST_EMAIL, TEST_COMIC_ID, true); - } - } - - @Test - public void testMarkAsRead() throws LastReadException { - Mockito.when( - lastReadService.setLastReadState( - Mockito.anyString(), Mockito.anyLong(), Mockito.anyBoolean())) - .thenReturn(lastReadEntry); + public void testSetComicsReadStateServiceException() throws LastReadException { + Mockito.doThrow(LastReadException.class) + .when(lastReadService) + .setLastReadState(Mockito.anyString(), Mockito.anyList(), Mockito.anyBoolean()); - final LastRead response = controller.markAsRead(principal, TEST_COMIC_ID); - - assertNotNull(response); - assertSame(lastReadEntry, response); + controller.setComicsReadState(principal, new SetComicsReadRequest(comicIdList, true)); Mockito.verify(lastReadService, Mockito.times(1)) - .setLastReadState(TEST_EMAIL, TEST_COMIC_ID, true); - } - - @Test(expected = LastReadException.class) - public void testMarkAsUnreadServiceException() throws LastReadException { - Mockito.when( - lastReadService.setLastReadState( - Mockito.anyString(), Mockito.anyLong(), Mockito.anyBoolean())) - .thenThrow(LastReadException.class); - - try { - controller.markAsUnread(principal, TEST_COMIC_ID); - } finally { - Mockito.verify(lastReadService, Mockito.times(1)) - .setLastReadState(TEST_EMAIL, TEST_COMIC_ID, false); - } + .setLastReadState(TEST_EMAIL, comicIdList, true); } @Test - public void testMarkAsUnread() throws LastReadException { - Mockito.when( - lastReadService.setLastReadState( - Mockito.anyString(), Mockito.anyLong(), Mockito.anyBoolean())) - .thenReturn(lastReadEntry); - - final LastRead response = controller.markAsUnread(principal, TEST_COMIC_ID); - - assertNotNull(response); - assertSame(lastReadEntry, response); + public void testSetComicsReadState() throws LastReadException { + controller.setComicsReadState(principal, new SetComicsReadRequest(comicIdList, true)); Mockito.verify(lastReadService, Mockito.times(1)) - .setLastReadState(TEST_EMAIL, TEST_COMIC_ID, false); + .setLastReadState(TEST_EMAIL, comicIdList, true); } } diff --git a/comixed-services/src/main/java/org/comixedproject/service/library/LastReadService.java b/comixed-services/src/main/java/org/comixedproject/service/library/LastReadService.java index 5b1f340f6..fadc67a44 100644 --- a/comixed-services/src/main/java/org/comixedproject/service/library/LastReadService.java +++ b/comixed-services/src/main/java/org/comixedproject/service/library/LastReadService.java @@ -18,13 +18,18 @@ package org.comixedproject.service.library; -import java.util.Date; +import static org.comixedproject.state.comicbooks.ComicStateHandler.HEADER_COMIC; +import static org.comixedproject.state.comicbooks.ComicStateHandler.HEADER_USER; + +import java.util.HashMap; import java.util.List; +import java.util.Map; import lombok.extern.log4j.Log4j2; import org.comixedproject.messaging.PublishingException; import org.comixedproject.messaging.library.PublishLastReadRemovedAction; -import org.comixedproject.messaging.library.PublishLastReadUpdateAction; +import org.comixedproject.messaging.library.PublishLastReadUpdatedAction; import org.comixedproject.model.comicbooks.Comic; +import org.comixedproject.model.comicbooks.ComicState; import org.comixedproject.model.library.LastRead; import org.comixedproject.model.user.ComiXedUser; import org.comixedproject.repositories.library.LastReadRepository; @@ -32,8 +37,14 @@ import org.comixedproject.service.comicbooks.ComicService; import org.comixedproject.service.user.ComiXedUserException; import org.comixedproject.service.user.UserService; +import org.comixedproject.state.comicbooks.ComicEvent; +import org.comixedproject.state.comicbooks.ComicStateChangeListener; +import org.comixedproject.state.comicbooks.ComicStateHandler; +import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; +import org.springframework.messaging.Message; +import org.springframework.statemachine.state.State; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -45,13 +56,76 @@ */ @Service @Log4j2 -public class LastReadService { +public class LastReadService implements InitializingBean, ComicStateChangeListener { + @Autowired private ComicStateHandler comicStateHandler; @Autowired private UserService userService; @Autowired private ComicService comicService; @Autowired private LastReadRepository lastReadRepository; - @Autowired private PublishLastReadUpdateAction publishLastReadUpdateAction; + @Autowired private PublishLastReadUpdatedAction publishLastReadUpdatedAction; @Autowired private PublishLastReadRemovedAction publishLastReadRemovedAction; + @Override + public void afterPropertiesSet() throws Exception { + log.trace("Subscribing to comic state change events"); + this.comicStateHandler.addListener(this); + } + + /** + * Modifies the last read state for a comic when a related event occurs. + * + * @param state the new state + * @param message the event message + */ + @Override + @Transactional + public void onComicStateChange( + final State state, final Message message) { + log.trace("Fetching comic"); + final Comic comic = message.getHeaders().get(HEADER_COMIC, Comic.class); + log.trace("Fetching event type"); + final ComicEvent event = message.getPayload(); + if (event == ComicEvent.markAsRead) { + log.trace("Fetching user"); + final ComiXedUser user = message.getHeaders().get(HEADER_USER, ComiXedUser.class); + log.trace("Marking comic as read"); + this.markComicAsRead(comic, user); + } + if (event == ComicEvent.markAsUnread) { + log.trace("Fetching user"); + final ComiXedUser user = message.getHeaders().get(HEADER_USER, ComiXedUser.class); + log.trace("Marking comic as read"); + this.markComicAsUnread(comic, user); + } + } + + /** + * Creates a record marking the specified comic as read by the given user. + * + * @param comic the comic + * @param user the user + */ + public void markComicAsRead(final Comic comic, final ComiXedUser user) { + log.trace("Creating last read record"); + final LastRead lastRead = this.lastReadRepository.save(new LastRead(comic, user)); + try { + this.publishLastReadUpdatedAction.publish(lastRead); + } catch (PublishingException error) { + log.error("Failed to publish last read update", error); + } + } + + public void markComicAsUnread(final Comic comic, final ComiXedUser user) { + log.trace("Loading last read record"); + final LastRead lastRead = this.lastReadRepository.loadEntryForComicAndUser(comic, user); + log.trace("Deleting last read record"); + this.lastReadRepository.delete(lastRead); + try { + this.publishLastReadRemovedAction.publish(lastRead); + } catch (PublishingException error) { + log.error("Failed to publish last read removed", error); + } + } + /** * Gets a batch of last read dates for the given user. The records returned will come after the * specified threshold id, and will contain no more than the maximum specified entries. @@ -67,7 +141,7 @@ public List getLastReadEntries( log.debug("Loading user: {}", email); ComiXedUser user = this.doFindUser(email); log.debug("Loading {} last read entries for {} with id > {}", maximum, email, threshold); - return this.lastReadRepository.getEntriesForUser(user, threshold, PageRequest.of(0, maximum)); + return this.lastReadRepository.loadEntriesForUser(user, threshold, PageRequest.of(0, maximum)); } private ComiXedUser doFindUser(final String email) throws LastReadException { @@ -79,62 +153,35 @@ private ComiXedUser doFindUser(final String email) throws LastReadException { } /** - * Sets the last read state of a comic for a given user. If the comic is marked as read then an - * entry is created, otherwise if marked as unread any existing entry is deleted. + * Sets the read state for a set of comics for a given user. * * @param email the user's email - * @param comicId the comic is - * @param markRead the read state - * @return the last read entry - * @throws LastReadException if an error occurs + * @param ids the set of comic ids + * @param state the read state + * @throws LastReadException if the user was not found */ - @Transactional - public LastRead setLastReadState(final String email, final long comicId, final boolean markRead) + public void setLastReadState(final String email, final List ids, final boolean state) throws LastReadException { - log.debug("Marking comic as {} for {}: id={}", markRead ? "read" : "unread", email, comicId); + log.debug("Loading the user: {}", email); final ComiXedUser user = this.doFindUser(email); - final var comic = this.doGetComic(comicId); - - log.trace("Looking for existing last read entry"); - LastRead entry = this.lastReadRepository.findEntryForUserAndComic(user, comic); - - LastRead result; - if (markRead) { - if (entry == null) { - log.trace("No such entry; creating new last read entry"); - entry = new LastRead(comic, user); - } - log.trace("Setting last read entry"); - entry.setLastReadOn(new Date()); - entry.setLastModifiedOn(new Date()); - result = this.lastReadRepository.save(entry); - } else { - if (entry == null) throw new LastReadException("No last read entry found"); - log.trace("Deleting entry: id={}", entry.getId()); - this.lastReadRepository.delete(entry); - result = entry; - } - this.doPublishLastReadAction(result, markRead); - return result; - } - - private void doPublishLastReadAction(final LastRead lastRead, final boolean markRead) { - try { - if (markRead) { - this.publishLastReadUpdateAction.publish(lastRead); - } else { - this.publishLastReadRemovedAction.publish(lastRead); - } - } catch (PublishingException error) { - log.error("Failed to publish last read update", error); - } - } - - private Comic doGetComic(final long comicId) throws LastReadException { - try { - return this.comicService.getComic(comicId); - } catch (ComicException error) { - throw new LastReadException("Failed to load comic", error); - } + ids.forEach( + id -> { + try { + log.trace("Loading comic: id={}", id); + final Comic comic = this.comicService.getComic(id); + log.trace("Creating additional headers"); + Map headers = new HashMap<>(); + headers.put(HEADER_USER, user); + if (state) { + log.trace("Firing event: mark comic as read"); + this.comicStateHandler.fireEvent(comic, ComicEvent.markAsRead, headers); + } else { + log.trace("Firing event: mark comic as unread"); + this.comicStateHandler.fireEvent(comic, ComicEvent.markAsUnread, headers); + } + } catch (ComicException error) { + log.error("Failed to process comic last read state", error); + } + }); } } diff --git a/comixed-services/src/main/java/org/comixedproject/service/library/LibraryService.java b/comixed-services/src/main/java/org/comixedproject/service/library/LibraryService.java index 9951a3158..bae2d12b5 100644 --- a/comixed-services/src/main/java/org/comixedproject/service/library/LibraryService.java +++ b/comixed-services/src/main/java/org/comixedproject/service/library/LibraryService.java @@ -94,7 +94,7 @@ public void prepareForConsolidation( throw new LibraryException("Target directory is not configured"); } log.trace("Preparing message headers"); - Map headers = new HashMap<>(); + Map headers = new HashMap<>(); headers.put(HEADER_DELETE_REMOVED_COMIC_FILE, String.valueOf(deleteRemovedComicFiles)); headers.put(HEADER_TARGET_DIRECTORY, targetDirectory); headers.put(HEADER_RENAMING_RULE, renamingRule); diff --git a/comixed-services/src/test/java/org/comixedproject/service/library/LastReadServiceTest.java b/comixed-services/src/test/java/org/comixedproject/service/library/LastReadServiceTest.java index aaa7da8d3..7c37113ed 100644 --- a/comixed-services/src/test/java/org/comixedproject/service/library/LastReadServiceTest.java +++ b/comixed-services/src/test/java/org/comixedproject/service/library/LastReadServiceTest.java @@ -19,13 +19,17 @@ package org.comixedproject.service.library; import static junit.framework.TestCase.*; +import static org.comixedproject.state.comicbooks.ComicStateHandler.HEADER_COMIC; +import static org.comixedproject.state.comicbooks.ComicStateHandler.HEADER_USER; -import java.util.Date; +import java.util.ArrayList; import java.util.List; +import java.util.Map; import org.comixedproject.messaging.PublishingException; import org.comixedproject.messaging.library.PublishLastReadRemovedAction; -import org.comixedproject.messaging.library.PublishLastReadUpdateAction; +import org.comixedproject.messaging.library.PublishLastReadUpdatedAction; import org.comixedproject.model.comicbooks.Comic; +import org.comixedproject.model.comicbooks.ComicState; import org.comixedproject.model.library.LastRead; import org.comixedproject.model.user.ComiXedUser; import org.comixedproject.repositories.library.LastReadRepository; @@ -33,11 +37,17 @@ import org.comixedproject.service.comicbooks.ComicService; import org.comixedproject.service.user.ComiXedUserException; import org.comixedproject.service.user.UserService; +import org.comixedproject.state.comicbooks.ComicEvent; +import org.comixedproject.state.comicbooks.ComicStateHandler; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.*; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.data.domain.PageRequest; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.statemachine.state.State; @RunWith(MockitoJUnitRunner.class) public class LastReadServiceTest { @@ -47,8 +57,9 @@ public class LastReadServiceTest { private static final long TEST_COMIC_ID = 27L; @InjectMocks private LastReadService service; + @Mock private ComicStateHandler comicStateHandler; @Mock private LastReadRepository lastReadRepository; - @Mock private PublishLastReadUpdateAction publishLastReadUpdateAction; + @Mock private PublishLastReadUpdatedAction publishLastReadUpdatedAction; @Mock private PublishLastReadRemovedAction publishLastReadRemovedAction; @Mock private UserService userService; @Mock private ComicService comicService; @@ -57,9 +68,36 @@ public class LastReadServiceTest { @Mock private Comic comic; @Mock private LastRead savedLastReadEntry; @Mock private LastRead lastReadEntry; + @Mock private State state; + @Mock private Message message; + @Mock private MessageHeaders messageHeaders; @Captor private ArgumentCaptor pageRequestArgumentCaptor; @Captor private ArgumentCaptor lastReadArgumentCaptor; + @Captor private ArgumentCaptor> eventHeaders; + + private List comicIdList = new ArrayList<>(); + + @Before + public void setUp() throws ComiXedUserException { + Mockito.when(message.getHeaders()).thenReturn(messageHeaders); + Mockito.when(messageHeaders.get(HEADER_COMIC, Comic.class)).thenReturn(comic); + Mockito.when(messageHeaders.get(HEADER_USER, ComiXedUser.class)).thenReturn(user); + Mockito.when(lastReadRepository.save(lastReadArgumentCaptor.capture())) + .thenReturn(savedLastReadEntry); + Mockito.when(userService.findByEmail(Mockito.anyString())).thenReturn(user); + comicIdList.add(TEST_COMIC_ID); + Mockito.doNothing() + .when(comicStateHandler) + .fireEvent(Mockito.any(Comic.class), Mockito.any(ComicEvent.class), eventHeaders.capture()); + } + + @Test + public void testAfterPropertiesSet() throws Exception { + service.afterPropertiesSet(); + + Mockito.verify(comicStateHandler, Mockito.times(1)).addListener(service); + } @Test(expected = LastReadException.class) public void testGetLastReadEntriesUserException() throws ComiXedUserException, LastReadException { @@ -77,7 +115,7 @@ public void testGetLastReadEntriesUserException() throws ComiXedUserException, L public void testGetLastReadEntries() throws ComiXedUserException, LastReadException { Mockito.when(userService.findByEmail(Mockito.anyString())).thenReturn(user); Mockito.when( - lastReadRepository.getEntriesForUser( + lastReadRepository.loadEntriesForUser( Mockito.any(ComiXedUser.class), Mockito.anyLong(), pageRequestArgumentCaptor.capture())) @@ -93,236 +131,127 @@ public void testGetLastReadEntries() throws ComiXedUserException, LastReadExcept Mockito.verify(userService, Mockito.times(1)).findByEmail(TEST_EMAIL); Mockito.verify(lastReadRepository, Mockito.times(1)) - .getEntriesForUser(user, TEST_THRESHOLD, pageRequestArgumentCaptor.getValue()); + .loadEntriesForUser(user, TEST_THRESHOLD, pageRequestArgumentCaptor.getValue()); } - @Test(expected = LastReadException.class) - public void testSetLastReadStateAsReadNoSuchUser() - throws ComiXedUserException, LastReadException { - Mockito.when(userService.findByEmail(Mockito.anyString())) - .thenThrow(ComiXedUserException.class); + @Test + public void testOnComicStateChangeMarkedAsRead() throws PublishingException { + Mockito.when(message.getPayload()).thenReturn(ComicEvent.markAsRead); - try { - service.setLastReadState(TEST_EMAIL, TEST_COMIC_ID, true); - } finally { - Mockito.verify(userService, Mockito.times(1)).findByEmail(TEST_EMAIL); - } - } + service.onComicStateChange(state, message); - @Test(expected = LastReadException.class) - public void testSetLastReadStateAsReadNoSuchComic() - throws ComiXedUserException, LastReadException, ComicException { - Mockito.when(userService.findByEmail(Mockito.anyString())).thenReturn(user); - Mockito.when(comicService.getComic(Mockito.anyLong())).thenThrow(ComicException.class); + final LastRead record = lastReadArgumentCaptor.getValue(); + assertNotNull(record); + assertSame(comic, record.getComic()); + assertSame(user, record.getUser()); - try { - service.setLastReadState(TEST_EMAIL, TEST_COMIC_ID, true); - } finally { - Mockito.verify(userService, Mockito.times(1)).findByEmail(TEST_EMAIL); - Mockito.verify(comicService, Mockito.times(1)).getComic(TEST_COMIC_ID); - } + Mockito.verify(publishLastReadUpdatedAction, Mockito.times(1)).publish(savedLastReadEntry); } @Test - public void testSetLastReadStateAsReadExistingEntryPublishingException() - throws ComiXedUserException, LastReadException, ComicException, PublishingException { - Mockito.when(userService.findByEmail(Mockito.anyString())).thenReturn(user); - Mockito.when(comicService.getComic(Mockito.anyLong())).thenReturn(comic); - Mockito.when( - lastReadRepository.findEntryForUserAndComic( - Mockito.any(ComiXedUser.class), Mockito.any(Comic.class))) - .thenReturn(lastReadEntry); - Mockito.when(lastReadRepository.save(Mockito.any(LastRead.class))) - .thenReturn(savedLastReadEntry); + public void testOnComicStateChangeMarkedAsReadPublishingException() throws PublishingException { + Mockito.when(message.getPayload()).thenReturn(ComicEvent.markAsRead); Mockito.doThrow(PublishingException.class) - .when(publishLastReadUpdateAction) + .when(publishLastReadUpdatedAction) .publish(Mockito.any(LastRead.class)); - final LastRead result = service.setLastReadState(TEST_EMAIL, TEST_COMIC_ID, true); + service.onComicStateChange(state, message); - assertNotNull(result); - assertSame(savedLastReadEntry, result); + final LastRead record = lastReadArgumentCaptor.getValue(); + assertNotNull(record); + assertSame(comic, record.getComic()); + assertSame(user, record.getUser()); - Mockito.verify(userService, Mockito.times(1)).findByEmail(TEST_EMAIL); - Mockito.verify(comicService, Mockito.times(1)).getComic(TEST_COMIC_ID); - Mockito.verify(lastReadRepository, Mockito.times(1)).findEntryForUserAndComic(user, comic); - Mockito.verify(lastReadEntry, Mockito.times(1)).setLastReadOn(Mockito.any(Date.class)); - Mockito.verify(lastReadRepository, Mockito.times(1)).save(lastReadEntry); - Mockito.verify(publishLastReadUpdateAction, Mockito.times(1)).publish(savedLastReadEntry); + Mockito.verify(publishLastReadUpdatedAction, Mockito.times(1)).publish(savedLastReadEntry); } @Test - public void testSetLastReadStateAsReadExistingEntry() - throws ComiXedUserException, LastReadException, ComicException, PublishingException { - Mockito.when(userService.findByEmail(Mockito.anyString())).thenReturn(user); - Mockito.when(comicService.getComic(Mockito.anyLong())).thenReturn(comic); + public void testOnComicStateChangeMarkedAsUnread() throws PublishingException { + Mockito.when(message.getPayload()).thenReturn(ComicEvent.markAsUnread); Mockito.when( - lastReadRepository.findEntryForUserAndComic( - Mockito.any(ComiXedUser.class), Mockito.any(Comic.class))) - .thenReturn(lastReadEntry); - Mockito.when(lastReadRepository.save(Mockito.any(LastRead.class))) + lastReadRepository.loadEntryForComicAndUser( + Mockito.any(Comic.class), Mockito.any(ComiXedUser.class))) .thenReturn(savedLastReadEntry); - final LastRead result = service.setLastReadState(TEST_EMAIL, TEST_COMIC_ID, true); + service.onComicStateChange(state, message); - assertNotNull(result); - assertSame(savedLastReadEntry, result); - - Mockito.verify(userService, Mockito.times(1)).findByEmail(TEST_EMAIL); - Mockito.verify(comicService, Mockito.times(1)).getComic(TEST_COMIC_ID); - Mockito.verify(lastReadRepository, Mockito.times(1)).findEntryForUserAndComic(user, comic); - Mockito.verify(lastReadEntry, Mockito.times(1)).setLastReadOn(Mockito.any(Date.class)); - Mockito.verify(lastReadRepository, Mockito.times(1)).save(lastReadEntry); - Mockito.verify(publishLastReadUpdateAction, Mockito.times(1)).publish(savedLastReadEntry); + Mockito.verify(lastReadRepository, Mockito.times(1)).loadEntryForComicAndUser(comic, user); + Mockito.verify(lastReadRepository, Mockito.times(1)).delete(savedLastReadEntry); + Mockito.verify(publishLastReadRemovedAction, Mockito.times(1)).publish(savedLastReadEntry); } @Test - public void testSetLastReadStateAsReadPublishingException() - throws ComiXedUserException, LastReadException, ComicException, PublishingException { - Mockito.when(userService.findByEmail(Mockito.anyString())).thenReturn(user); - Mockito.when(comicService.getComic(Mockito.anyLong())).thenReturn(comic); + public void testOnComicStateChangeMarkedAsUnreadPublishingError() throws PublishingException { + Mockito.when(message.getPayload()).thenReturn(ComicEvent.markAsUnread); Mockito.when( - lastReadRepository.findEntryForUserAndComic( - Mockito.any(ComiXedUser.class), Mockito.any(Comic.class))) - .thenReturn(null); - Mockito.when(lastReadRepository.save(lastReadArgumentCaptor.capture())) + lastReadRepository.loadEntryForComicAndUser( + Mockito.any(Comic.class), Mockito.any(ComiXedUser.class))) .thenReturn(savedLastReadEntry); Mockito.doThrow(PublishingException.class) - .when(publishLastReadUpdateAction) + .when(publishLastReadRemovedAction) .publish(Mockito.any(LastRead.class)); - final LastRead result = service.setLastReadState(TEST_EMAIL, TEST_COMIC_ID, true); + service.onComicStateChange(state, message); - assertNotNull(result); - assertSame(savedLastReadEntry, result); - - assertSame(user, lastReadArgumentCaptor.getValue().getUser()); - assertSame(comic, lastReadArgumentCaptor.getValue().getComic()); - - Mockito.verify(userService, Mockito.times(1)).findByEmail(TEST_EMAIL); - Mockito.verify(comicService, Mockito.times(1)).getComic(TEST_COMIC_ID); - Mockito.verify(lastReadRepository, Mockito.times(1)).save(lastReadArgumentCaptor.getValue()); - Mockito.verify(publishLastReadUpdateAction, Mockito.times(1)).publish(savedLastReadEntry); - } - - @Test - public void testSetLastReadStateAsRead() - throws ComiXedUserException, LastReadException, ComicException, PublishingException { - Mockito.when(userService.findByEmail(Mockito.anyString())).thenReturn(user); - Mockito.when(comicService.getComic(Mockito.anyLong())).thenReturn(comic); - Mockito.when( - lastReadRepository.findEntryForUserAndComic( - Mockito.any(ComiXedUser.class), Mockito.any(Comic.class))) - .thenReturn(null); - Mockito.when(lastReadRepository.save(lastReadArgumentCaptor.capture())) - .thenReturn(savedLastReadEntry); - - final LastRead result = service.setLastReadState(TEST_EMAIL, TEST_COMIC_ID, true); - - assertNotNull(result); - assertSame(savedLastReadEntry, result); - - assertSame(user, lastReadArgumentCaptor.getValue().getUser()); - assertSame(comic, lastReadArgumentCaptor.getValue().getComic()); - - Mockito.verify(userService, Mockito.times(1)).findByEmail(TEST_EMAIL); - Mockito.verify(comicService, Mockito.times(1)).getComic(TEST_COMIC_ID); - Mockito.verify(lastReadRepository, Mockito.times(1)).save(lastReadArgumentCaptor.getValue()); - Mockito.verify(publishLastReadUpdateAction, Mockito.times(1)).publish(savedLastReadEntry); + Mockito.verify(lastReadRepository, Mockito.times(1)).loadEntryForComicAndUser(comic, user); + Mockito.verify(lastReadRepository, Mockito.times(1)).delete(savedLastReadEntry); + Mockito.verify(publishLastReadRemovedAction, Mockito.times(1)).publish(savedLastReadEntry); } @Test(expected = LastReadException.class) - public void testSetLastReadStateAsUnreadNoSuchUser() - throws ComiXedUserException, LastReadException { + public void markAsReadNoSuchUser() throws ComiXedUserException, LastReadException { Mockito.when(userService.findByEmail(Mockito.anyString())) .thenThrow(ComiXedUserException.class); try { - service.setLastReadState(TEST_EMAIL, TEST_COMIC_ID, false); + service.setLastReadState(TEST_EMAIL, comicIdList, true); } finally { Mockito.verify(userService, Mockito.times(1)).findByEmail(TEST_EMAIL); } } - @Test(expected = LastReadException.class) - public void testSetLastReadStateAsUnreadNoSuchComic() + @Test + public void markAsReadNoSuchComic() throws ComiXedUserException, LastReadException, ComicException { - Mockito.when(userService.findByEmail(Mockito.anyString())).thenReturn(user); Mockito.when(comicService.getComic(Mockito.anyLong())).thenThrow(ComicException.class); try { - service.setLastReadState(TEST_EMAIL, TEST_COMIC_ID, false); + service.setLastReadState(TEST_EMAIL, comicIdList, true); } finally { Mockito.verify(userService, Mockito.times(1)).findByEmail(TEST_EMAIL); Mockito.verify(comicService, Mockito.times(1)).getComic(TEST_COMIC_ID); } } - @Test(expected = LastReadException.class) - public void testSetLastReadStateAsUnreadNoSuchUnreadEntry() - throws ComiXedUserException, LastReadException, ComicException { - Mockito.when(userService.findByEmail(Mockito.anyString())).thenReturn(user); - Mockito.when(comicService.getComic(Mockito.anyLong())).thenReturn(comic); - Mockito.when( - lastReadRepository.findEntryForUserAndComic( - Mockito.any(ComiXedUser.class), Mockito.any(Comic.class))) - .thenReturn(null); - - try { - service.setLastReadState(TEST_EMAIL, TEST_COMIC_ID, false); - } finally { - Mockito.verify(userService, Mockito.times(1)).findByEmail(TEST_EMAIL); - Mockito.verify(comicService, Mockito.times(1)).getComic(TEST_COMIC_ID); - Mockito.verify(lastReadRepository, Mockito.times(1)).findEntryForUserAndComic(user, comic); - } - } - @Test - public void testSetLastReadStateAsUnreadPublishingException() - throws ComiXedUserException, LastReadException, ComicException, PublishingException { - Mockito.when(userService.findByEmail(Mockito.anyString())).thenReturn(user); + public void markAsRead() throws ComiXedUserException, LastReadException, ComicException { Mockito.when(comicService.getComic(Mockito.anyLong())).thenReturn(comic); - Mockito.when( - lastReadRepository.findEntryForUserAndComic( - Mockito.any(ComiXedUser.class), Mockito.any(Comic.class))) - .thenReturn(lastReadEntry); - Mockito.doNothing().when(lastReadRepository).delete(Mockito.any()); - Mockito.doThrow(PublishingException.class) - .when(publishLastReadRemovedAction) - .publish(Mockito.any(LastRead.class)); - final LastRead result = service.setLastReadState(TEST_EMAIL, TEST_COMIC_ID, false); + service.setLastReadState(TEST_EMAIL, comicIdList, true); - assertNotNull(result); - assertSame(lastReadEntry, result); + final Map headers = eventHeaders.getValue(); + assertNotNull(headers); + assertSame(user, headers.get(HEADER_USER)); Mockito.verify(userService, Mockito.times(1)).findByEmail(TEST_EMAIL); Mockito.verify(comicService, Mockito.times(1)).getComic(TEST_COMIC_ID); - Mockito.verify(lastReadRepository, Mockito.times(1)).findEntryForUserAndComic(user, comic); - Mockito.verify(lastReadRepository, Mockito.times(1)).delete(lastReadEntry); - Mockito.verify(publishLastReadRemovedAction, Mockito.times(1)).publish(lastReadEntry); + Mockito.verify(comicStateHandler, Mockito.times(1)) + .fireEvent(comic, ComicEvent.markAsRead, headers); } @Test - public void testSetLastReadStateAsUnread() - throws ComiXedUserException, LastReadException, ComicException, PublishingException { - Mockito.when(userService.findByEmail(Mockito.anyString())).thenReturn(user); + public void markAsUnread() throws ComiXedUserException, LastReadException, ComicException { Mockito.when(comicService.getComic(Mockito.anyLong())).thenReturn(comic); - Mockito.when( - lastReadRepository.findEntryForUserAndComic( - Mockito.any(ComiXedUser.class), Mockito.any(Comic.class))) - .thenReturn(lastReadEntry); - Mockito.doNothing().when(lastReadRepository).delete(Mockito.any()); - final LastRead result = service.setLastReadState(TEST_EMAIL, TEST_COMIC_ID, false); + service.setLastReadState(TEST_EMAIL, comicIdList, false); - assertNotNull(result); - assertSame(lastReadEntry, result); + final Map headers = eventHeaders.getValue(); + assertNotNull(headers); + assertSame(user, headers.get(HEADER_USER)); Mockito.verify(userService, Mockito.times(1)).findByEmail(TEST_EMAIL); Mockito.verify(comicService, Mockito.times(1)).getComic(TEST_COMIC_ID); - Mockito.verify(lastReadRepository, Mockito.times(1)).findEntryForUserAndComic(user, comic); - Mockito.verify(lastReadRepository, Mockito.times(1)).delete(lastReadEntry); - Mockito.verify(publishLastReadRemovedAction, Mockito.times(1)).publish(lastReadEntry); + Mockito.verify(comicStateHandler, Mockito.times(1)) + .fireEvent(comic, ComicEvent.markAsUnread, headers); } } diff --git a/comixed-services/src/test/java/org/comixedproject/service/library/LibraryServiceTest.java b/comixed-services/src/test/java/org/comixedproject/service/library/LibraryServiceTest.java index 84039ec2c..0df7a61c4 100644 --- a/comixed-services/src/test/java/org/comixedproject/service/library/LibraryServiceTest.java +++ b/comixed-services/src/test/java/org/comixedproject/service/library/LibraryServiceTest.java @@ -58,7 +58,7 @@ public class LibraryServiceTest { @Mock private PageCacheService pageCacheService; @Mock private ComicStateHandler comicStateHandler; - @Captor private ArgumentCaptor> headersArgumentCaptor; + @Captor private ArgumentCaptor> headersArgumentCaptor; private List comicList = new ArrayList<>(); private Comic comic1 = new Comic(); @@ -159,7 +159,7 @@ public void testPrepareForConsolidation() throws LibraryException { service.prepareForConsolidation( TEST_TARGET_DIRECTORY, TEST_RENAMING_RULE, TEST_DELETE_REMOVED_COMIC_FILES); - final Map headers = headersArgumentCaptor.getAllValues().get(0); + final Map headers = headersArgumentCaptor.getAllValues().get(0); assertEquals( String.valueOf(TEST_DELETE_REMOVED_COMIC_FILES), headers.get(HEADER_DELETE_REMOVED_COMIC_FILE)); diff --git a/comixed-state/src/main/java/org/comixedproject/state/StateContextAccessor.java b/comixed-state/src/main/java/org/comixedproject/state/StateContextAccessor.java index 7891b233c..d107a5ad3 100644 --- a/comixed-state/src/main/java/org/comixedproject/state/StateContextAccessor.java +++ b/comixed-state/src/main/java/org/comixedproject/state/StateContextAccessor.java @@ -19,10 +19,12 @@ package org.comixedproject.state; import static org.comixedproject.state.comicbooks.ComicStateHandler.HEADER_COMIC; +import static org.comixedproject.state.comicbooks.ComicStateHandler.HEADER_USER; import static org.comixedproject.state.lists.ReadingListStateHandler.HEADER_READING_LIST; import org.comixedproject.model.comicbooks.Comic; import org.comixedproject.model.lists.ReadingList; +import org.comixedproject.model.user.ComiXedUser; import org.springframework.statemachine.StateContext; /** @@ -51,4 +53,14 @@ protected Comic fetchComic(final StateContext context) { protected ReadingList fetchReadingList(final StateContext context) { return context.getMessageHeaders().get(HEADER_READING_LIST, ReadingList.class); } + + /** + * Retrieves a user from the state context. + * + * @param context the context + * @return the user + */ + protected ComiXedUser fetchUser(final StateContext context) { + return context.getMessageHeaders().get(HEADER_USER, ComiXedUser.class); + } } diff --git a/comixed-state/src/main/java/org/comixedproject/state/comicbooks/ComicEvent.java b/comixed-state/src/main/java/org/comixedproject/state/comicbooks/ComicEvent.java index 7a769c8aa..e6404b10b 100644 --- a/comixed-state/src/main/java/org/comixedproject/state/comicbooks/ComicEvent.java +++ b/comixed-state/src/main/java/org/comixedproject/state/comicbooks/ComicEvent.java @@ -39,6 +39,8 @@ public enum ComicEvent { undeleteComic, // the comic is being unmarked for removal recreateComicFile, // recreate the comic file comicFileRecreated, // the comic file was recreated + markAsRead, // the comic has been marked as read by a user + markAsUnread, // the comic has been marked as unread by a user scraped, detailsUpdated, metadataCleared, diff --git a/comixed-state/src/main/java/org/comixedproject/state/comicbooks/ComicStateHandler.java b/comixed-state/src/main/java/org/comixedproject/state/comicbooks/ComicStateHandler.java index 2dbcd1812..894b740e2 100644 --- a/comixed-state/src/main/java/org/comixedproject/state/comicbooks/ComicStateHandler.java +++ b/comixed-state/src/main/java/org/comixedproject/state/comicbooks/ComicStateHandler.java @@ -49,6 +49,7 @@ public class ComicStateHandler extends LifecycleObjectSupport { public static final String HEADER_DELETE_REMOVED_COMIC_FILE = "header.remove-comic-file"; public static final String HEADER_TARGET_DIRECTORY = "header.target-directory"; public static final String HEADER_RENAMING_RULE = "header.renaming-rule"; + public static final String HEADER_USER = "header.user"; @Autowired private StateMachine stateMachine; @@ -102,7 +103,7 @@ public void fireEvent(final Comic comic, final ComicEvent event) { * @param headers the message headers */ public void fireEvent( - final Comic comic, final ComicEvent event, final Map headers) { + final Comic comic, final ComicEvent event, final Map headers) { log.debug("Firing comic event: {} => {}", comic.getId(), event); final Message message = MessageBuilder.withPayload(event) diff --git a/comixed-state/src/main/java/org/comixedproject/state/comicbooks/ComicStateMachineConfiguration.java b/comixed-state/src/main/java/org/comixedproject/state/comicbooks/ComicStateMachineConfiguration.java index f0460ff0f..0215230f0 100644 --- a/comixed-state/src/main/java/org/comixedproject/state/comicbooks/ComicStateMachineConfiguration.java +++ b/comixed-state/src/main/java/org/comixedproject/state/comicbooks/ComicStateMachineConfiguration.java @@ -22,10 +22,7 @@ import org.comixedproject.model.comicbooks.Comic; import org.comixedproject.model.comicbooks.ComicState; import org.comixedproject.state.comicbooks.actions.*; -import org.comixedproject.state.comicbooks.guards.ComicContentsProcessedGuard; -import org.comixedproject.state.comicbooks.guards.ComicFileAlreadyRecreatingGuard; -import org.comixedproject.state.comicbooks.guards.ConsolidateComicGuard; -import org.comixedproject.state.comicbooks.guards.FileDetailsCreatedGuard; +import org.comixedproject.state.comicbooks.guards.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.statemachine.config.EnableStateMachine; @@ -58,6 +55,8 @@ public class ComicStateMachineConfiguration @Autowired private ComicFileRecreatedAction comicFileRecreatedAction; @Autowired private MarkComicForRemovalAction markComicForRemovalAction; @Autowired private UnmarkComicForRemovalAction unmarkComicForRemovalAction; + @Autowired private ComicAlreadyReadByUserGuard comicAlreadReadByUserGuard; + @Autowired private ComicNotAlreadyReadByUserGuard comicNotAlreadReadByUserGuard; @Override public void configure(final StateMachineStateConfigurer states) @@ -131,6 +130,12 @@ public void configure(final StateMachineTransitionConfigurerComicAlreadyReadByUserGuard ensures a comic is marked as read by the specified user. + * + * @author Darryl L. Pierce + */ +@Component +@Log4j2 +public class ComicAlreadyReadByUserGuard extends AbstractComicGuard { + @Override + public boolean evaluate(final StateContext context) { + log.trace("Fetching comic"); + final Comic comic = this.fetchComic(context); + log.trace("Fetching user"); + final ComiXedUser user = this.fetchUser(context); + return comic.getLastReads().stream().anyMatch(lastRead -> lastRead.getUser().equals(user)); + } +} diff --git a/comixed-state/src/main/java/org/comixedproject/state/comicbooks/guards/ComicNotAlreadyReadByUserGuard.java b/comixed-state/src/main/java/org/comixedproject/state/comicbooks/guards/ComicNotAlreadyReadByUserGuard.java new file mode 100644 index 000000000..1a070cce2 --- /dev/null +++ b/comixed-state/src/main/java/org/comixedproject/state/comicbooks/guards/ComicNotAlreadyReadByUserGuard.java @@ -0,0 +1,46 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2021, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +package org.comixedproject.state.comicbooks.guards; + +import lombok.extern.log4j.Log4j2; +import org.comixedproject.model.comicbooks.Comic; +import org.comixedproject.model.comicbooks.ComicState; +import org.comixedproject.model.user.ComiXedUser; +import org.comixedproject.state.comicbooks.ComicEvent; +import org.springframework.statemachine.StateContext; +import org.springframework.stereotype.Component; + +/** + * ComicNotAlreadyReadByUserGuard ensures that a comic was not already marked as read + * by a user. + * + * @author Darryl L. Pierce + */ +@Component +@Log4j2 +public class ComicNotAlreadyReadByUserGuard extends AbstractComicGuard { + @Override + public boolean evaluate(final StateContext context) { + log.trace("Fetching comic"); + final Comic comic = this.fetchComic(context); + log.trace("Fetching user"); + final ComiXedUser user = this.fetchUser(context); + return comic.getLastReads().stream().noneMatch(lastRead -> lastRead.getUser().equals(user)); + } +} diff --git a/comixed-state/src/test/java/org/comixedproject/state/comicbooks/guards/ComicAlreadyReadByUserGuardTest.java b/comixed-state/src/test/java/org/comixedproject/state/comicbooks/guards/ComicAlreadyReadByUserGuardTest.java new file mode 100644 index 000000000..6c91a08c4 --- /dev/null +++ b/comixed-state/src/test/java/org/comixedproject/state/comicbooks/guards/ComicAlreadyReadByUserGuardTest.java @@ -0,0 +1,78 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2021, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +package org.comixedproject.state.comicbooks.guards; + +import static org.comixedproject.state.comicbooks.ComicStateHandler.HEADER_COMIC; +import static org.comixedproject.state.comicbooks.ComicStateHandler.HEADER_USER; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import org.comixedproject.model.comicbooks.Comic; +import org.comixedproject.model.comicbooks.ComicState; +import org.comixedproject.model.library.LastRead; +import org.comixedproject.model.user.ComiXedUser; +import org.comixedproject.state.comicbooks.ComicEvent; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.messaging.MessageHeaders; +import org.springframework.statemachine.StateContext; + +@RunWith(MockitoJUnitRunner.class) +public class ComicAlreadyReadByUserGuardTest { + @InjectMocks private ComicAlreadyReadByUserGuard guard; + @Mock private StateContext context; + @Mock private MessageHeaders messageHeaders; + @Mock private Comic comic; + @Mock private ComiXedUser user; + @Mock private LastRead lastRead; + + private List lastReadList = new ArrayList<>(); + + @Before + public void setUp() { + Mockito.when(context.getMessageHeaders()).thenReturn(messageHeaders); + Mockito.when(messageHeaders.get(HEADER_COMIC, Comic.class)).thenReturn(comic); + Mockito.when(messageHeaders.get(HEADER_USER, ComiXedUser.class)).thenReturn(user); + Mockito.when(comic.getLastReads()).thenReturn(lastReadList); + Mockito.when(lastRead.getUser()).thenReturn(user); + } + + @Test + public void testEvaluateNotAlreadyRead() { + final boolean result = guard.evaluate(context); + + assertFalse(result); + } + + @Test + public void testEvaluate() { + lastReadList.add(lastRead); + + final boolean result = guard.evaluate(context); + + assertTrue(result); + } +} diff --git a/comixed-state/src/test/java/org/comixedproject/state/comicbooks/guards/ComicNotAlreadyReadByUserGuardTest.java b/comixed-state/src/test/java/org/comixedproject/state/comicbooks/guards/ComicNotAlreadyReadByUserGuardTest.java new file mode 100644 index 000000000..3b9c77c56 --- /dev/null +++ b/comixed-state/src/test/java/org/comixedproject/state/comicbooks/guards/ComicNotAlreadyReadByUserGuardTest.java @@ -0,0 +1,78 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2021, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +package org.comixedproject.state.comicbooks.guards; + +import static org.comixedproject.state.comicbooks.ComicStateHandler.HEADER_COMIC; +import static org.comixedproject.state.comicbooks.ComicStateHandler.HEADER_USER; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import org.comixedproject.model.comicbooks.Comic; +import org.comixedproject.model.comicbooks.ComicState; +import org.comixedproject.model.library.LastRead; +import org.comixedproject.model.user.ComiXedUser; +import org.comixedproject.state.comicbooks.ComicEvent; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.messaging.MessageHeaders; +import org.springframework.statemachine.StateContext; + +@RunWith(MockitoJUnitRunner.class) +public class ComicNotAlreadyReadByUserGuardTest { + @InjectMocks private ComicNotAlreadyReadByUserGuard guard; + @Mock private StateContext context; + @Mock private MessageHeaders messageHeaders; + @Mock private Comic comic; + @Mock private ComiXedUser user; + @Mock private LastRead lastRead; + + private List lastReadList = new ArrayList<>(); + + @Before + public void setUp() { + Mockito.when(context.getMessageHeaders()).thenReturn(messageHeaders); + Mockito.when(messageHeaders.get(HEADER_COMIC, Comic.class)).thenReturn(comic); + Mockito.when(messageHeaders.get(HEADER_USER, ComiXedUser.class)).thenReturn(user); + Mockito.when(comic.getLastReads()).thenReturn(lastReadList); + Mockito.when(lastRead.getUser()).thenReturn(user); + } + + @Test + public void testEvaluateAlreadyRead() { + lastReadList.add(lastRead); + + final boolean result = guard.evaluate(context); + + assertFalse(result); + } + + @Test + public void testEvaluate() { + final boolean result = guard.evaluate(context); + + assertTrue(result); + } +} diff --git a/comixed-webui/angular.json b/comixed-webui/angular.json index 0155e8ddf..aa96da90e 100644 --- a/comixed-webui/angular.json +++ b/comixed-webui/angular.json @@ -20,7 +20,9 @@ "allowedCommonJsDependencies": [ "lodash", "sockjs-client", - "file-saver" + "file-saver", + "zone.js/dist/zone-error", + "clone-deep" ], "outputPath": "target/classes/static", "index": "src/index.html", diff --git a/comixed-webui/src/app/comic-books/components/comic-detail-card/comic-detail-card.component.html b/comixed-webui/src/app/comic-books/components/comic-detail-card/comic-detail-card.component.html index 42367e8c3..8f77e1dda 100644 --- a/comixed-webui/src/app/comic-books/components/comic-detail-card/comic-detail-card.component.html +++ b/comixed-webui/src/app/comic-books/components/comic-detail-card/comic-detail-card.component.html @@ -28,6 +28,9 @@ > info + + bookmark + (); @Output() showContextMenu = new EventEmitter(); diff --git a/comixed-webui/src/app/comic-books/models/comic.ts b/comixed-webui/src/app/comic-books/models/comic.ts index 5fd73b875..b09b2ba46 100644 --- a/comixed-webui/src/app/comic-books/models/comic.ts +++ b/comixed-webui/src/app/comic-books/models/comic.ts @@ -58,5 +58,4 @@ export interface Comic { notes: string; pages?: Page[]; duplicateCount?: number; - lastRead?: number; } diff --git a/comixed-webui/src/app/comic-books/pages/comic-book-page/comic-book-page.component.spec.ts b/comixed-webui/src/app/comic-books/pages/comic-book-page/comic-book-page.component.spec.ts index 41818a391..587d7c345 100644 --- a/comixed-webui/src/app/comic-books/pages/comic-book-page/comic-book-page.component.spec.ts +++ b/comixed-webui/src/app/comic-books/pages/comic-book-page/comic-book-page.component.spec.ts @@ -66,7 +66,7 @@ import { } from '@app/last-read/reducers/last-read-list.reducer'; import { TitleService } from '@app/core/services/title.service'; import { ConfirmationService } from '@app/core/services/confirmation.service'; -import { updateComicReadStatus } from '@app/last-read/actions/update-read-status.actions'; +import { setComicsRead } from '@app/last-read/actions/set-comics-read.actions'; import { Confirmation } from '@app/core/models/confirmation'; import { updateMetadata } from '@app/library/actions/update-metadata.actions'; import { LAST_READ_1 } from '@app/last-read/last-read.fixtures'; @@ -277,7 +277,7 @@ describe('ComicBookPageComponent', () => { it('fires an action', () => { expect(store.dispatch).toHaveBeenCalledWith( - updateComicReadStatus({ comic: COMIC, status: true }) + setComicsRead({ comics: [COMIC], read: true }) ); }); }); @@ -289,7 +289,7 @@ describe('ComicBookPageComponent', () => { it('fires an action', () => { expect(store.dispatch).toHaveBeenCalledWith( - updateComicReadStatus({ comic: COMIC, status: false }) + setComicsRead({ comics: [COMIC], read: false }) ); }); }); diff --git a/comixed-webui/src/app/comic-books/pages/comic-book-page/comic-book-page.component.ts b/comixed-webui/src/app/comic-books/pages/comic-book-page/comic-book-page.component.ts index c5bd153f5..fc9896d38 100644 --- a/comixed-webui/src/app/comic-books/pages/comic-book-page/comic-book-page.component.ts +++ b/comixed-webui/src/app/comic-books/pages/comic-book-page/comic-book-page.component.ts @@ -51,7 +51,7 @@ import { selectComicBusy } from '@app/comic-books/selectors/comic.selectors'; import { selectLastReadEntries } from '@app/last-read/selectors/last-read-list.selectors'; -import { updateComicReadStatus } from '@app/last-read/actions/update-read-status.actions'; +import { setComicsRead } from '@app/last-read/actions/set-comics-read.actions'; import { LastRead } from '@app/last-read/models/last-read'; import { TitleService } from '@app/core/services/title.service'; import { ConfirmationService } from '@app/core/services/confirmation.service'; @@ -240,9 +240,9 @@ export class ComicBookPageComponent ); } - setReadState(status: boolean): void { + setReadState(read: boolean): void { this.logger.debug(`Marking comic as ${status ? 'read' : 'unread'}`); - this.store.dispatch(updateComicReadStatus({ comic: this.comic, status })); + this.store.dispatch(setComicsRead({ comics: [this.comic], read })); } onUpdateMetadata(): void { diff --git a/comixed-webui/src/app/comic-books/selectors/comic-list.selectors.spec.ts b/comixed-webui/src/app/comic-books/selectors/comic-list.selectors.spec.ts index 44707c4a5..d16509682 100644 --- a/comixed-webui/src/app/comic-books/selectors/comic-list.selectors.spec.ts +++ b/comixed-webui/src/app/comic-books/selectors/comic-list.selectors.spec.ts @@ -25,7 +25,6 @@ import { selectComicListCollection, selectComicListCount, selectComicListDeletedCount, - selectComicListReadCount, selectComicListState } from './comic-list.selectors'; import { @@ -75,12 +74,6 @@ describe('Comic List Selectors', () => { ); }); - it('should select the number of read comics in the list', () => { - expect( - selectComicListReadCount({ [COMIC_LIST_FEATURE_KEY]: state }) - ).toEqual(state.comics.filter(comic => !!comic.lastRead).length); - }); - it('should select the number of deleted comics in the list', () => { expect( selectComicListDeletedCount({ [COMIC_LIST_FEATURE_KEY]: state }) diff --git a/comixed-webui/src/app/comic-books/selectors/comic-list.selectors.ts b/comixed-webui/src/app/comic-books/selectors/comic-list.selectors.ts index f8539a54a..77a650b39 100644 --- a/comixed-webui/src/app/comic-books/selectors/comic-list.selectors.ts +++ b/comixed-webui/src/app/comic-books/selectors/comic-list.selectors.ts @@ -40,11 +40,6 @@ export const selectComicListCount = createSelector( state => state.comics.length ); -export const selectComicListReadCount = createSelector( - selectComicListState, - state => state.comics.filter(comic => !!comic.lastRead).length -); - export const selectComicListDeletedCount = createSelector( selectComicListState, state => state.comics.filter(comic => !!comic.deletedDate).length diff --git a/comixed-webui/src/app/components/footer/footer.component.spec.ts b/comixed-webui/src/app/components/footer/footer.component.spec.ts index 9131a5316..6a37ef34f 100644 --- a/comixed-webui/src/app/components/footer/footer.component.spec.ts +++ b/comixed-webui/src/app/components/footer/footer.component.spec.ts @@ -28,11 +28,16 @@ import { COMIC_LIST_FEATURE_KEY, initialState as initialComicListState } from '@app/comic-books/reducers/comic-list.reducer'; +import { + initialState as initialLastReadState, + LAST_READ_LIST_FEATURE_KEY +} from '@app/last-read/reducers/last-read-list.reducer'; describe('FooterComponent', () => { const initialState = { [IMPORT_COUNT_FEATURE_KEY]: initialImportCountState, - [COMIC_LIST_FEATURE_KEY]: initialComicListState + [COMIC_LIST_FEATURE_KEY]: initialComicListState, + [LAST_READ_LIST_FEATURE_KEY]: initialLastReadState }; let component: FooterComponent; let fixture: ComponentFixture; diff --git a/comixed-webui/src/app/components/footer/footer.component.ts b/comixed-webui/src/app/components/footer/footer.component.ts index 925407b5d..30bc3c7df 100644 --- a/comixed-webui/src/app/components/footer/footer.component.ts +++ b/comixed-webui/src/app/components/footer/footer.component.ts @@ -23,9 +23,9 @@ import { selectImportCount } from '@app/selectors/import-count.selectors'; import { User } from '@app/user/models/user'; import { selectComicListCount, - selectComicListDeletedCount, - selectComicListReadCount + selectComicListDeletedCount } from '@app/comic-books/selectors/comic-list.selectors'; +import { selectLastReadEntries } from '@app/last-read/selectors/last-read-list.selectors'; @Component({ selector: 'cx-footer', @@ -48,8 +48,8 @@ export class FooterComponent implements OnInit { .select(selectComicListCount) .subscribe(count => (this.comicCount = count)); this.store - .select(selectComicListReadCount) - .subscribe(count => (this.readCount = count)); + .select(selectLastReadEntries) + .subscribe(entries => (this.readCount = entries.length)); this.store .select(selectComicListDeletedCount) .subscribe(count => (this.deletedCount = count)); diff --git a/comixed-webui/src/app/last-read/actions/update-read-status.actions.ts b/comixed-webui/src/app/last-read/actions/set-comics-read.actions.ts similarity index 70% rename from comixed-webui/src/app/last-read/actions/update-read-status.actions.ts rename to comixed-webui/src/app/last-read/actions/set-comics-read.actions.ts index 0721d5d95..d615ee87c 100644 --- a/comixed-webui/src/app/last-read/actions/update-read-status.actions.ts +++ b/comixed-webui/src/app/last-read/actions/set-comics-read.actions.ts @@ -19,15 +19,15 @@ import { createAction, props } from '@ngrx/store'; import { Comic } from '@app/comic-books/models/comic'; -export const updateComicReadStatus = createAction( - '[Update Read Status] Update the comic read state', - props<{ comic: Comic; status: boolean }>() +export const setComicsRead = createAction( + '[Set Comics Read] Update the comic read state', + props<{ comics: Comic[]; read: boolean }>() ); -export const comicReadStatusUpdated = createAction( - '[Update Read Status] The comic read state updated' +export const comicsReadSet = createAction( + '[Set Comics Read] The comic read state updated' ); -export const updateComicReadStatusFailed = createAction( - '[Update Read Status] Failed to update the comic read status' +export const setComicsReadFailed = createAction( + '[Set Comics Read] Failed to update the comic read status' ); diff --git a/comixed-webui/src/app/last-read/effects/update-read-status.effects.spec.ts b/comixed-webui/src/app/last-read/effects/set-comics-read.effects.spec.ts similarity index 61% rename from comixed-webui/src/app/last-read/effects/update-read-status.effects.spec.ts rename to comixed-webui/src/app/last-read/effects/set-comics-read.effects.spec.ts index d5bfef00c..e14ecb701 100644 --- a/comixed-webui/src/app/last-read/effects/update-read-status.effects.spec.ts +++ b/comixed-webui/src/app/last-read/effects/set-comics-read.effects.spec.ts @@ -20,7 +20,7 @@ import { TestBed } from '@angular/core/testing'; import { provideMockActions } from '@ngrx/effects/testing'; import { Observable, of, throwError } from 'rxjs'; -import { UpdateReadStatusEffects } from './update-read-status.effects'; +import { SetComicsReadEffects } from './set-comics-read.effects'; import { LastReadService } from '@app/last-read/services/last-read.service'; import { AlertService } from '@app/core/services/alert.service'; import { COMIC_4 } from '@app/comic-books/comic-books.fixtures'; @@ -28,10 +28,10 @@ import { LoggerModule } from '@angular-ru/logger'; import { TranslateModule } from '@ngx-translate/core'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { - comicReadStatusUpdated, - updateComicReadStatus, - updateComicReadStatusFailed -} from '@app/last-read/actions/update-read-status.actions'; + comicsReadSet, + setComicsRead, + setComicsReadFailed +} from '@app/last-read/actions/set-comics-read.actions'; import { hot } from 'jasmine-marbles'; import { HttpErrorResponse } from '@angular/common/http'; import { @@ -46,7 +46,7 @@ describe('UpdateReadStatusEffects', () => { const ENTRY = LAST_READ_2; let actions$: Observable; - let effects: UpdateReadStatusEffects; + let effects: SetComicsReadEffects; let lastReadService: jasmine.SpyObj; let alertService: AlertService; @@ -58,19 +58,19 @@ describe('UpdateReadStatusEffects', () => { MatSnackBarModule ], providers: [ - UpdateReadStatusEffects, + SetComicsReadEffects, provideMockActions(() => actions$), { provide: LastReadService, useValue: { - setStatus: jasmine.createSpy('LastReadService.setStatus()') + setRead: jasmine.createSpy('LastReadService.setRead()') } }, AlertService ] }); - effects = TestBed.inject(UpdateReadStatusEffects); + effects = TestBed.inject(SetComicsReadEffects); lastReadService = TestBed.inject( LastReadService ) as jasmine.SpyObj; @@ -83,57 +83,42 @@ describe('UpdateReadStatusEffects', () => { expect(effects).toBeTruthy(); }); - describe('updating the last read status of a comic', () => { - it('fires an action on success marking', () => { + describe('updating the read state of comics', () => { + it('fires an action on success', () => { const serviceResponse = ENTRY; - const action = updateComicReadStatus({ comic: COMIC, status: true }); - const outcome1 = comicReadStatusUpdated(); - const outcome2 = lastReadDateUpdated({ entry: ENTRY }); + const action = setComicsRead({ comics: [COMIC], read: READ }); + const outcome = comicsReadSet(); actions$ = hot('-a', { a: action }); - lastReadService.setStatus.and.returnValue(of(serviceResponse)); + lastReadService.setRead.and.returnValue(of(serviceResponse)); - const expected = hot('-(bc)', { b: outcome1, c: outcome2 }); - expect(effects.updateStatus$).toBeObservable(expected); - expect(alertService.info).toHaveBeenCalledWith(jasmine.any(String)); - }); - - it('fires an action on success unmarking', () => { - const serviceResponse = ENTRY; - const action = updateComicReadStatus({ comic: COMIC, status: false }); - const outcome1 = comicReadStatusUpdated(); - const outcome2 = lastReadDateRemoved({ entry: ENTRY }); - - actions$ = hot('-a', { a: action }); - lastReadService.setStatus.and.returnValue(of(serviceResponse)); - - const expected = hot('-(bc)', { b: outcome1, c: outcome2 }); - expect(effects.updateStatus$).toBeObservable(expected); + const expected = hot('-b', { b: outcome }); + expect(effects.setComicsRead$).toBeObservable(expected); expect(alertService.info).toHaveBeenCalledWith(jasmine.any(String)); }); it('fires an action on service failure', () => { const serviceResponse = new HttpErrorResponse({}); - const action = updateComicReadStatus({ comic: COMIC, status: READ }); - const outcome = updateComicReadStatusFailed(); + const action = setComicsRead({ comics: [COMIC], read: READ }); + const outcome = setComicsReadFailed(); actions$ = hot('-a', { a: action }); - lastReadService.setStatus.and.returnValue(throwError(serviceResponse)); + lastReadService.setRead.and.returnValue(throwError(serviceResponse)); const expected = hot('-b', { b: outcome }); - expect(effects.updateStatus$).toBeObservable(expected); + expect(effects.setComicsRead$).toBeObservable(expected); expect(alertService.error).toHaveBeenCalledWith(jasmine.any(String)); }); it('fires an action on general failure', () => { - const action = updateComicReadStatus({ comic: COMIC, status: READ }); - const outcome = updateComicReadStatusFailed(); + const action = setComicsRead({ comics: [COMIC], read: READ }); + const outcome = setComicsReadFailed(); actions$ = hot('-a', { a: action }); - lastReadService.setStatus.and.throwError('expected'); + lastReadService.setRead.and.throwError('expected'); const expected = hot('-(b|)', { b: outcome }); - expect(effects.updateStatus$).toBeObservable(expected); + expect(effects.setComicsRead$).toBeObservable(expected); expect(alertService.error).toHaveBeenCalledWith(jasmine.any(String)); }); }); diff --git a/comixed-webui/src/app/last-read/effects/update-read-status.effects.ts b/comixed-webui/src/app/last-read/effects/set-comics-read.effects.ts similarity index 72% rename from comixed-webui/src/app/last-read/effects/update-read-status.effects.ts rename to comixed-webui/src/app/last-read/effects/set-comics-read.effects.ts index c76ea072d..49dfc893e 100644 --- a/comixed-webui/src/app/last-read/effects/update-read-status.effects.ts +++ b/comixed-webui/src/app/last-read/effects/set-comics-read.effects.ts @@ -22,55 +22,46 @@ import { LoggerService } from '@angular-ru/logger'; import { TranslateService } from '@ngx-translate/core'; import { LastReadService } from '@app/last-read/services/last-read.service'; import { - comicReadStatusUpdated, - updateComicReadStatus, - updateComicReadStatusFailed -} from '@app/last-read/actions/update-read-status.actions'; -import { catchError, mergeMap, switchMap, tap } from 'rxjs/operators'; + comicsReadSet, + setComicsRead, + setComicsReadFailed +} from '@app/last-read/actions/set-comics-read.actions'; +import { catchError, map, switchMap, tap } from 'rxjs/operators'; import { AlertService } from '@app/core/services/alert.service'; import { of } from 'rxjs'; -import { - lastReadDateRemoved, - lastReadDateUpdated -} from '@app/last-read/actions/last-read-list.actions'; import { LastRead } from '@app/last-read/models/last-read'; @Injectable() -export class UpdateReadStatusEffects { - updateStatus$ = createEffect(() => { +export class SetComicsReadEffects { + setComicsRead$ = createEffect(() => { return this.actions$.pipe( - ofType(updateComicReadStatus), + ofType(setComicsRead), tap(action => this.logger.debug('Effect: updating comic read status:', action) ), switchMap(action => this.lastReadService - .setStatus({ comic: action.comic, status: action.status }) + .setRead({ comics: action.comics, read: action.read }) .pipe( tap(response => this.logger.debug('Response received:', response)), tap(() => this.alertService.info( this.translateService.instant( 'update-read-status.effect-success', - { status: action.status } + { status: action.read } ) ) ), - mergeMap((response: LastRead) => [ - comicReadStatusUpdated(), - action.status - ? lastReadDateUpdated({ entry: response }) - : lastReadDateRemoved({ entry: response }) - ]), + map((response: LastRead) => comicsReadSet()), catchError(error => { this.logger.error('Service failure:', error); this.alertService.error( this.translateService.instant( 'update-read-status.effect-failure', - { status: action.status } + { status: action.read } ) ); - return of(updateComicReadStatusFailed()); + return of(setComicsReadFailed()); }) ) ), @@ -79,7 +70,7 @@ export class UpdateReadStatusEffects { this.alertService.error( this.translateService.instant('app.general-effect-failure') ); - return of(updateComicReadStatusFailed()); + return of(setComicsReadFailed()); }) ); }); diff --git a/comixed-webui/src/app/last-read/index.ts b/comixed-webui/src/app/last-read/index.ts index 0b724dd57..2dc2954f8 100644 --- a/comixed-webui/src/app/last-read/index.ts +++ b/comixed-webui/src/app/last-read/index.ts @@ -26,9 +26,9 @@ import { import { ActionReducerMap } from '@ngrx/store'; import { reducer as updateReadStatusReducer, - UPDATE_READ_STATUS_FEATURE_KEY, - UpdateReadStatusState -} from '@app/last-read/reducers/update-read-status.reducer'; + SET_COMICS_READ_FEATURE_KEY, + SetComicsReadState +} from './reducers/set-comics-read-state.reducer'; interface RouterStateUrl { url: string; @@ -39,7 +39,7 @@ interface RouterStateUrl { export interface LastReadModuleState { router: RouterReducerState; [LAST_READ_LIST_FEATURE_KEY]: LastReadListState; - [UPDATE_READ_STATUS_FEATURE_KEY]: UpdateReadStatusState; + [SET_COMICS_READ_FEATURE_KEY]: SetComicsReadState; } export type ModuleState = LastReadModuleState; @@ -47,5 +47,5 @@ export type ModuleState = LastReadModuleState; export const reducers: ActionReducerMap = { router: routerReducer, [LAST_READ_LIST_FEATURE_KEY]: lastReadDatesReducer, - [UPDATE_READ_STATUS_FEATURE_KEY]: updateReadStatusReducer + [SET_COMICS_READ_FEATURE_KEY]: updateReadStatusReducer }; diff --git a/comixed-webui/src/app/last-read/last-read.constants.ts b/comixed-webui/src/app/last-read/last-read.constants.ts index 44c9d02f6..636baf4f2 100644 --- a/comixed-webui/src/app/last-read/last-read.constants.ts +++ b/comixed-webui/src/app/last-read/last-read.constants.ts @@ -17,12 +17,10 @@ */ import { API_ROOT_URL } from '../core'; -import { USER_SELF_TOPIC } from '@app/user/user.constants'; import { SECURED_PREFIX } from '@app/messaging/messaging.constants'; -export const LOAD_LAST_READ_ENTRIES_URL = `${API_ROOT_URL}/library/read`; -export const SET_COMIC_READ_STATUS_URL = `${API_ROOT_URL}/comic/\${comicId}/read`; -export const CLEAR_COMIC_READ_STATUS_URL = `${API_ROOT_URL}/comic/\${comicId}/read`; +export const LOAD_LAST_READ_ENTRIES_URL = `${API_ROOT_URL}/library/read?lastId=\${lastId}`; +export const SET_COMIC_READ_STATUS_URL = `${API_ROOT_URL}/library/read`; -export const LAST_READ_UPDATE_TOPIC = `${SECURED_PREFIX}/topic/last-read-list.update`; -export const LAST_READ_REMOVE_TOPIC = `${SECURED_PREFIX}/topic/last-read-list.removal`; +export const LAST_READ_UPDATED_TOPIC = `${SECURED_PREFIX}/topic/last-read-list.update`; +export const LAST_READ_REMOVED_TOPIC = `${SECURED_PREFIX}/topic/last-read-list.remove`; diff --git a/comixed-webui/src/app/last-read/last-read.module.ts b/comixed-webui/src/app/last-read/last-read.module.ts index 557ed7f74..d168c1ea7 100644 --- a/comixed-webui/src/app/last-read/last-read.module.ts +++ b/comixed-webui/src/app/last-read/last-read.module.ts @@ -29,9 +29,9 @@ import { EffectsModule } from '@ngrx/effects'; import { LastReadListEffects } from './effects/last-read-list.effects'; import { reducer as updateReadStatusReducer, - UPDATE_READ_STATUS_FEATURE_KEY -} from '@app/last-read/reducers/update-read-status.reducer'; -import { UpdateReadStatusEffects } from '@app/last-read/effects/update-read-status.effects'; + SET_COMICS_READ_FEATURE_KEY +} from '@app/last-read/reducers/set-comics-read-state.reducer'; +import { SetComicsReadEffects } from '@app/last-read/effects/set-comics-read.effects'; @NgModule({ declarations: [], @@ -41,10 +41,10 @@ import { UpdateReadStatusEffects } from '@app/last-read/effects/update-read-stat TranslateModule.forRoot(), StoreModule.forFeature(LAST_READ_LIST_FEATURE_KEY, lastReadDatesReducer), StoreModule.forFeature( - UPDATE_READ_STATUS_FEATURE_KEY, + SET_COMICS_READ_FEATURE_KEY, updateReadStatusReducer ), - EffectsModule.forFeature([LastReadListEffects, UpdateReadStatusEffects]) + EffectsModule.forFeature([LastReadListEffects, SetComicsReadEffects]) ], exports: [CommonModule] }) diff --git a/comixed-webui/src/app/last-read/models/net/load-last-read-entries-request.ts b/comixed-webui/src/app/last-read/models/net/set-comics-read-request.ts similarity index 91% rename from comixed-webui/src/app/last-read/models/net/load-last-read-entries-request.ts rename to comixed-webui/src/app/last-read/models/net/set-comics-read-request.ts index b5ceb324c..602670d14 100644 --- a/comixed-webui/src/app/last-read/models/net/load-last-read-entries-request.ts +++ b/comixed-webui/src/app/last-read/models/net/set-comics-read-request.ts @@ -16,6 +16,7 @@ * along with this program. If not, see */ -export interface LoadLastReadEntriesRequest { - lastId: number; +export interface SetComicsReadRequest { + ids: number[]; + read: boolean; } diff --git a/comixed-webui/src/app/last-read/reducers/last-read-list.reducer.ts b/comixed-webui/src/app/last-read/reducers/last-read-list.reducer.ts index 6f6da9783..d8ee5a7a0 100644 --- a/comixed-webui/src/app/last-read/reducers/last-read-list.reducer.ts +++ b/comixed-webui/src/app/last-read/reducers/last-read-list.reducer.ts @@ -57,12 +57,14 @@ export const reducer = createReducer( }), on(lastReadDateUpdated, (state, action) => { const entries = state.entries - .filter(entry => entry.id !== action.entry.id) + .filter(entry => entry.comic.id !== action.entry.comic.id) .concat([action.entry]); return { ...state, entries }; }), on(lastReadDateRemoved, (state, action) => { - const entries = state.entries.filter(entry => entry.id !== action.entry.id); + const entries = state.entries.filter( + entry => entry.comic.id !== action.entry.comic.id + ); return { ...state, entries }; }), on(loadLastReadDatesFailed, state => ({ ...state, loading: false })) diff --git a/comixed-webui/src/app/last-read/reducers/update-read-status.reducer.ts b/comixed-webui/src/app/last-read/reducers/set-comics-read-state.reducer.ts similarity index 64% rename from comixed-webui/src/app/last-read/reducers/update-read-status.reducer.ts rename to comixed-webui/src/app/last-read/reducers/set-comics-read-state.reducer.ts index 45c5e046d..49687bb4a 100644 --- a/comixed-webui/src/app/last-read/reducers/update-read-status.reducer.ts +++ b/comixed-webui/src/app/last-read/reducers/set-comics-read-state.reducer.ts @@ -18,25 +18,25 @@ import { createReducer, on } from '@ngrx/store'; import { - comicReadStatusUpdated, - updateComicReadStatus, - updateComicReadStatusFailed -} from '../actions/update-read-status.actions'; + comicsReadSet, + setComicsRead, + setComicsReadFailed +} from '../actions/set-comics-read.actions'; -export const UPDATE_READ_STATUS_FEATURE_KEY = 'update_read_status_state'; +export const SET_COMICS_READ_FEATURE_KEY = 'set_comics_read_state'; -export interface UpdateReadStatusState { +export interface SetComicsReadState { updating: boolean; } -export const initialState: UpdateReadStatusState = { +export const initialState: SetComicsReadState = { updating: false }; export const reducer = createReducer( initialState, - on(updateComicReadStatus, state => ({ ...state, updating: true })), - on(comicReadStatusUpdated, state => ({ ...state, updating: false })), - on(updateComicReadStatusFailed, state => ({ ...state, updating: false })) + on(setComicsRead, state => ({ ...state, updating: true })), + on(comicsReadSet, state => ({ ...state, updating: false })), + on(setComicsReadFailed, state => ({ ...state, updating: false })) ); diff --git a/comixed-webui/src/app/last-read/reducers/update-read-status.reducer.spec.ts b/comixed-webui/src/app/last-read/reducers/set-comics-read.reducer.spec.ts similarity index 75% rename from comixed-webui/src/app/last-read/reducers/update-read-status.reducer.spec.ts rename to comixed-webui/src/app/last-read/reducers/set-comics-read.reducer.spec.ts index e6ed7718f..de8aa3aec 100644 --- a/comixed-webui/src/app/last-read/reducers/update-read-status.reducer.spec.ts +++ b/comixed-webui/src/app/last-read/reducers/set-comics-read.reducer.spec.ts @@ -19,20 +19,20 @@ import { initialState, reducer, - UpdateReadStatusState -} from './update-read-status.reducer'; + SetComicsReadState +} from './set-comics-read-state.reducer'; import { COMIC_4 } from '@app/comic-books/comic-books.fixtures'; import { - comicReadStatusUpdated, - updateComicReadStatus, - updateComicReadStatusFailed -} from '@app/last-read/actions/update-read-status.actions'; + comicsReadSet, + setComicsRead, + setComicsReadFailed +} from '@app/last-read/actions/set-comics-read.actions'; -describe('MarkComicRead Reducer', () => { +describe('SetComicRead Reducer', () => { const COMIC = COMIC_4; const READ = Math.random() > 0.5; - let state: UpdateReadStatusState; + let state: SetComicsReadState; beforeEach(() => { state = { ...initialState }; @@ -48,11 +48,11 @@ describe('MarkComicRead Reducer', () => { }); }); - describe('updating the read status of a comic', () => { + describe('updating the read status of comics', () => { beforeEach(() => { state = reducer( { ...state, updating: false }, - updateComicReadStatus({ comic: COMIC, status: READ }) + setComicsRead({ comics: [COMIC], read: READ }) ); }); @@ -63,7 +63,7 @@ describe('MarkComicRead Reducer', () => { describe('success updating the status', () => { beforeEach(() => { - state = reducer({ ...state, updating: true }, comicReadStatusUpdated()); + state = reducer({ ...state, updating: true }, comicsReadSet()); }); it('clears the updating flag', () => { @@ -73,10 +73,7 @@ describe('MarkComicRead Reducer', () => { describe('failure updating the status', () => { beforeEach(() => { - state = reducer( - { ...state, updating: true }, - updateComicReadStatusFailed() - ); + state = reducer({ ...state, updating: true }, setComicsReadFailed()); }); it('clears the updating flag', () => { diff --git a/comixed-webui/src/app/last-read/selectors/update-read-status.selectors.spec.ts b/comixed-webui/src/app/last-read/selectors/set-comics-read.selectors.spec.ts similarity index 75% rename from comixed-webui/src/app/last-read/selectors/update-read-status.selectors.spec.ts rename to comixed-webui/src/app/last-read/selectors/set-comics-read.selectors.spec.ts index 4f5ec6827..de5a5f505 100644 --- a/comixed-webui/src/app/last-read/selectors/update-read-status.selectors.spec.ts +++ b/comixed-webui/src/app/last-read/selectors/set-comics-read.selectors.spec.ts @@ -17,13 +17,13 @@ */ import { - UPDATE_READ_STATUS_FEATURE_KEY, - UpdateReadStatusState -} from '../reducers/update-read-status.reducer'; -import { selectMarkComicReadState } from './update-read-status.selectors'; + SET_COMICS_READ_FEATURE_KEY, + SetComicsReadState +} from '../reducers/set-comics-read-state.reducer'; +import { selectMarkComicReadState } from './set-comics-read.selectors'; -describe('UpdateReadStatus Selectors', () => { - let state: UpdateReadStatusState; +describe('SetComicsRead Selectors', () => { + let state: SetComicsReadState; beforeEach(() => { state = { updating: Math.random() > 0.5 }; @@ -32,7 +32,7 @@ describe('UpdateReadStatus Selectors', () => { it('should select the feature state', () => { expect( selectMarkComicReadState({ - [UPDATE_READ_STATUS_FEATURE_KEY]: state + [SET_COMICS_READ_FEATURE_KEY]: state }) ).toEqual(state); }); diff --git a/comixed-webui/src/app/last-read/selectors/update-read-status.selectors.ts b/comixed-webui/src/app/last-read/selectors/set-comics-read.selectors.ts similarity index 82% rename from comixed-webui/src/app/last-read/selectors/update-read-status.selectors.ts rename to comixed-webui/src/app/last-read/selectors/set-comics-read.selectors.ts index f6ceccd3b..a0ff89680 100644 --- a/comixed-webui/src/app/last-read/selectors/update-read-status.selectors.ts +++ b/comixed-webui/src/app/last-read/selectors/set-comics-read.selectors.ts @@ -18,9 +18,9 @@ import { createFeatureSelector } from '@ngrx/store'; import { - UPDATE_READ_STATUS_FEATURE_KEY, - UpdateReadStatusState -} from '../reducers/update-read-status.reducer'; + SET_COMICS_READ_FEATURE_KEY, + SetComicsReadState +} from '../reducers/set-comics-read-state.reducer'; export const selectMarkComicReadState = - createFeatureSelector(UPDATE_READ_STATUS_FEATURE_KEY); + createFeatureSelector(SET_COMICS_READ_FEATURE_KEY); diff --git a/comixed-webui/src/app/last-read/services/last-read.service.spec.ts b/comixed-webui/src/app/last-read/services/last-read.service.spec.ts index d48aa2173..62bc4bed0 100644 --- a/comixed-webui/src/app/last-read/services/last-read.service.spec.ts +++ b/comixed-webui/src/app/last-read/services/last-read.service.spec.ts @@ -31,11 +31,9 @@ import { HttpTestingController } from '@angular/common/http/testing'; import { interpolate } from '@app/core'; -import { LoadLastReadEntriesRequest } from '@app/last-read/models/net/load-last-read-entries-request'; import { - CLEAR_COMIC_READ_STATUS_URL, - LAST_READ_REMOVE_TOPIC, - LAST_READ_UPDATE_TOPIC, + LAST_READ_REMOVED_TOPIC, + LAST_READ_UPDATED_TOPIC, LOAD_LAST_READ_ENTRIES_URL, SET_COMIC_READ_STATUS_URL } from '@app/last-read/last-read.constants'; @@ -56,7 +54,8 @@ import { initialState as initialLastReadListState, LAST_READ_LIST_FEATURE_KEY } from '@app/last-read/reducers/last-read-list.reducer'; -import { Subscription } from 'webstomp-client'; +import { SetComicsReadRequest } from '@app/last-read/models/net/set-comics-read-request'; +import { HttpResponse } from '@angular/common/http'; describe('LastReadService', () => { const ENTRIES = [LAST_READ_1, LAST_READ_3, LAST_READ_5]; @@ -108,42 +107,29 @@ describe('LastReadService', () => { } as LoadLastReadEntriesResponse) ); - const req = httpMock.expectOne(interpolate(LOAD_LAST_READ_ENTRIES_URL)); - expect(req.request.method).toEqual('POST'); - expect(req.request.body).toEqual({ - lastId: LAST_ID - } as LoadLastReadEntriesRequest); + const req = httpMock.expectOne( + interpolate(LOAD_LAST_READ_ENTRIES_URL, { lastId: LAST_ID }) + ); + expect(req.request.method).toEqual('GET'); req.flush({ entries: ENTRIES, lastPayload: LAST_PAYLOAD } as LoadLastReadEntriesResponse); }); - describe('updating the last read status of a comic', () => { - it('can mark a comic as read', () => { - service - .setStatus({ comic: COMIC, status: true }) - .subscribe(response => expect(response).toEqual(COMIC)); - - const req = httpMock.expectOne( - interpolate(SET_COMIC_READ_STATUS_URL, { comicId: COMIC.id }) - ); - expect(req.request.method).toEqual('POST'); - expect(req.request.body).toEqual({}); - req.flush(COMIC); - }); + it('can set comics read', () => { + const read = Math.random() > 0.5; + service + .setRead({ comics: [COMIC], read }) + .subscribe(response => expect(response.status).toEqual(200)); - it('can mark a comic as unread', () => { - service - .setStatus({ comic: COMIC, status: false }) - .subscribe(response => expect(response).toEqual(COMIC)); - - const req = httpMock.expectOne( - interpolate(CLEAR_COMIC_READ_STATUS_URL, { comicId: COMIC.id }) - ); - expect(req.request.method).toEqual('DELETE'); - req.flush(COMIC); - }); + const req = httpMock.expectOne(interpolate(SET_COMIC_READ_STATUS_URL)); + expect(req.request.method).toEqual('POST'); + expect(req.request.body).toEqual({ + ids: [COMIC.id], + read + } as SetComicsReadRequest); + req.flush(new HttpResponse({ status: 200 })); }); describe('when messaging starts', () => { @@ -174,7 +160,7 @@ describe('LastReadService', () => { service.updateSubscription = null; service.removeSubscription = null; webSocketService.subscribe - .withArgs(LAST_READ_UPDATE_TOPIC, jasmine.anything()) + .withArgs(LAST_READ_UPDATED_TOPIC, jasmine.anything()) .and.callFake((topic, callback) => { callback(ENTRY); return { @@ -182,7 +168,7 @@ describe('LastReadService', () => { } as any; }); webSocketService.subscribe - .withArgs(LAST_READ_REMOVE_TOPIC, jasmine.anything()) + .withArgs(LAST_READ_REMOVED_TOPIC, jasmine.anything()) .and.callFake((topic, callback) => { callback(ENTRY); return { @@ -194,6 +180,11 @@ describe('LastReadService', () => { [MESSAGING_FEATURE_KEY]: { ...initialMessagingState, started: true + }, + [LAST_READ_LIST_FEATURE_KEY]: { + ...initialLastReadListState, + loading: false, + lastPayload: true } }); }); diff --git a/comixed-webui/src/app/last-read/services/last-read.service.ts b/comixed-webui/src/app/last-read/services/last-read.service.ts index dac522d41..75a58bdc9 100644 --- a/comixed-webui/src/app/last-read/services/last-read.service.ts +++ b/comixed-webui/src/app/last-read/services/last-read.service.ts @@ -22,13 +22,11 @@ import { LoggerService } from '@angular-ru/logger'; import { HttpClient } from '@angular/common/http'; import { interpolate } from '@app/core'; import { - CLEAR_COMIC_READ_STATUS_URL, - LAST_READ_REMOVE_TOPIC, - LAST_READ_UPDATE_TOPIC, + LAST_READ_REMOVED_TOPIC, + LAST_READ_UPDATED_TOPIC, LOAD_LAST_READ_ENTRIES_URL, SET_COMIC_READ_STATUS_URL } from '@app/last-read/last-read.constants'; -import { LoadLastReadEntriesRequest } from '@app/last-read/models/net/load-last-read-entries-request'; import { Comic } from '@app/comic-books/models/comic'; import { Store } from '@ngrx/store'; import { Subscription } from 'webstomp-client'; @@ -41,6 +39,7 @@ import { } from '@app/last-read/actions/last-read-list.actions'; import { WebSocketService } from '@app/messaging'; import { LastRead } from '@app/last-read/models/last-read'; +import { SetComicsReadRequest } from '@app/last-read/models/net/set-comics-read-request'; @Injectable({ providedIn: 'root' @@ -74,29 +73,39 @@ export class LastReadService { } if (!lastReadState.loading && lastReadState.lastPayload) { this.loaded = true; + if (!this.updateSubscription) { + this.updateSubscription = + this.webSocketService.subscribe( + LAST_READ_UPDATED_TOPIC, + entry => { + this.logger.debug('Last read entry updated:', entry); + this.store.dispatch(lastReadDateUpdated({ entry })); + } + ); + } + if (!this.removeSubscription) { + this.removeSubscription = + this.webSocketService.subscribe( + LAST_READ_REMOVED_TOPIC, + entry => { + this.logger.debug('Last read entry removed:', entry); + this.store.dispatch(lastReadDateRemoved({ entry })); + } + ); + } } }); } - if (!this.updateSubscription) { - this.updateSubscription = this.webSocketService.subscribe( - LAST_READ_UPDATE_TOPIC, - entry => this.store.dispatch(lastReadDateUpdated({ entry })) - ); - } - if (!this.removeSubscription) { - this.removeSubscription = this.webSocketService.subscribe( - LAST_READ_REMOVE_TOPIC, - entry => this.store.dispatch(lastReadDateRemoved({ entry })) - ); - } } if (!state.started) { if (!!this.updateSubscription) { + this.logger.trace('Unsubscribing from last read updates'); this.updateSubscription.unsubscribe(); this.updateSubscription = null; this.loaded = false; } if (!!this.removeSubscription) { + this.logger.trace('Unsubscribing from last read removals'); this.removeSubscription.unsubscribe(); this.removeSubscription = null; } @@ -106,23 +115,16 @@ export class LastReadService { loadEntries(args: { lastId: number }): Observable { this.logger.debug('Service: loading last read entries:', args); - return this.http.post(interpolate(LOAD_LAST_READ_ENTRIES_URL), { - lastId: args.lastId - } as LoadLastReadEntriesRequest); + return this.http.get( + interpolate(LOAD_LAST_READ_ENTRIES_URL, { lastId: args.lastId }) + ); } - setStatus(args: { comic: Comic; status: boolean }): Observable { - if (args.status) { - this.logger.debug('Service: marking comic as read:', args); - return this.http.post( - interpolate(SET_COMIC_READ_STATUS_URL, { comicId: args.comic.id }), - {} - ); - } else { - this.logger.debug('Service: marking comic as unread:', args); - return this.http.delete( - interpolate(CLEAR_COMIC_READ_STATUS_URL, { comicId: args.comic.id }) - ); - } + setRead(args: { comics: Comic[]; read: boolean }): Observable { + this.logger.debug('Service: set comics read:', args); + return this.http.post(interpolate(SET_COMIC_READ_STATUS_URL), { + ids: args.comics.map(comic => comic.id), + read: args.read + } as SetComicsReadRequest); } } diff --git a/comixed-webui/src/app/library/actions/library.actions.ts b/comixed-webui/src/app/library/actions/library.actions.ts index 40a05e8aa..55a4c5d08 100644 --- a/comixed-webui/src/app/library/actions/library.actions.ts +++ b/comixed-webui/src/app/library/actions/library.actions.ts @@ -19,11 +19,6 @@ import { createAction, props } from '@ngrx/store'; import { Comic } from '@app/comic-books/models/comic'; -export const updateComics = createAction( - '[Library] Library updates received', - props<{ updated: Comic[]; removed: number[] }>() -); - export const selectComics = createAction( '[Library] Mark a set of comics as selected', props<{ comics: Comic[] }>() @@ -33,16 +28,3 @@ export const deselectComics = createAction( '[Library] Unmark a set of comics as selected', props<{ comics: Comic[] }>() ); - -export const setReadState = createAction( - '[Library] Set the read state for comics', - props<{ comics: Comic[]; read: boolean }>() -); - -export const readStateSet = createAction( - '[Library] Successfully set the read state for comics' -); - -export const setReadStateFailed = createAction( - '[Library] Failed to set the read state for comics' -); diff --git a/comixed-webui/src/app/library/components/comic-covers/comic-covers.component.html b/comixed-webui/src/app/library/components/comic-covers/comic-covers.component.html index d76ee7640..0c41abef6 100644 --- a/comixed-webui/src/app/library/components/comic-covers/comic-covers.component.html +++ b/comixed-webui/src/app/library/components/comic-covers/comic-covers.component.html @@ -29,6 +29,7 @@ [selected]="isSelected(comic)" [comicChanged]="isChanged(comic)" [isAdmin]="isAdmin" + [isRead]="isRead(comic)" [showActions]="showActions" (selectionChanged)="onSelectionChanged($event.comic, $event.selected)" (showContextMenu)="onShowContextMenu($event.comic, $event.x, $event.y)" @@ -81,7 +82,7 @@