From e3bf9065a19a649c687e69c4be63d961ae9f36a6 Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Wed, 22 Feb 2023 17:10:01 +0800 Subject: [PATCH] feat: read lists books can be sorted by release date read list are ordered by default, which is manual ordering if manual ordering is disabled, books will be sorted by release date Closes: #846 --- .../dialogs/ReadListAddToDialog.vue | 2 +- .../components/dialogs/ReadListEditDialog.vue | 14 ++ komga-webui/src/locales/en.json | 5 +- komga-webui/src/types/komga-readlists.ts | 3 + komga-webui/src/views/BrowseReadList.vue | 11 +- .../V20230221170726__readlist_ordered.sql | 2 + .../org/gotson/komga/domain/model/ReadList.kt | 4 + .../komga/domain/model/SeriesCollection.kt | 3 + .../komga/infrastructure/jooq/BookDtoDao.kt | 65 +++++-- .../komga/infrastructure/jooq/ReadListDao.kt | 3 + .../interfaces/api/opds/OpdsController.kt | 5 +- .../api/persistence/BookDtoRepository.kt | 5 +- .../interfaces/api/rest/ReadListController.kt | 10 +- .../api/rest/dto/ReadListCreationDto.kt | 1 + .../interfaces/api/rest/dto/ReadListDto.kt | 2 + .../api/rest/dto/ReadListUpdateDto.kt | 1 + .../api/rest/ReadListControllerTest.kt | 175 ++++++++++++++++++ 17 files changed, 281 insertions(+), 30 deletions(-) create mode 100644 komga/src/flyway/resources/db/migration/sqlite/V20230221170726__readlist_ordered.sql diff --git a/komga-webui/src/components/dialogs/ReadListAddToDialog.vue b/komga-webui/src/components/dialogs/ReadListAddToDialog.vue index 61b47a5fa..70bedf24b 100644 --- a/komga-webui/src/components/dialogs/ReadListAddToDialog.vue +++ b/komga-webui/src/components/dialogs/ReadListAddToDialog.vue @@ -150,7 +150,7 @@ export default Vue.extend({ } as ReadListCreationDto try { - const created = await this.$komgaReadLists.postReadList(toCreate) + await this.$komgaReadLists.postReadList(toCreate) this.dialogClose() } catch (e) { this.$eventHub.$emit(ERROR, {message: e.message} as ErrorEvent) diff --git a/komga-webui/src/components/dialogs/ReadListEditDialog.vue b/komga-webui/src/components/dialogs/ReadListEditDialog.vue index 4e860e07a..d7113b364 100644 --- a/komga-webui/src/components/dialogs/ReadListEditDialog.vue +++ b/komga-webui/src/components/dialogs/ReadListEditDialog.vue @@ -46,6 +46,17 @@ + + +
{{ $t('dialog.edit_readlist.label_ordering') }}
+ +
+
+ @@ -118,6 +129,7 @@ export default Vue.extend({ form: { name: '', summary: '', + ordered: true, }, poster: { selectedThumbnail: '', @@ -170,6 +182,7 @@ export default Vue.extend({ this.tab = 0 this.form.name = readList.name this.form.summary = readList.summary + this.form.ordered = readList.ordered this.poster.selectedThumbnail = '' this.poster.deleteQueue = [] @@ -216,6 +229,7 @@ export default Vue.extend({ const update = { name: this.form.name, summary: this.form.summary, + ordered: this.form.ordered, } as ReadListUpdateDto await this.$komgaReadLists.patchReadList(this.readList.id, update) diff --git a/komga-webui/src/locales/en.json b/komga-webui/src/locales/en.json index 31f0c4f06..473d69d68 100644 --- a/komga-webui/src/locales/en.json +++ b/komga-webui/src/locales/en.json @@ -174,7 +174,8 @@ }, "browse_readlist": { "edit_elements": "Edit elements", - "edit_readlist": "Edit read list" + "edit_readlist": "Edit read list", + "manual_ordering": "manual ordering" }, "browse_series": { "earliest_year_from_release_dates": "This is the earliest year from the release dates from all books in the series", @@ -434,7 +435,9 @@ "button_confirm": "Save changes", "dialog_title": "Edit read list", "field_name": "Name", + "field_manual_ordering": "Manual ordering", "field_summary": "Summary", + "label_ordering": "By default, books in a read list are ordered manually. You can disable manual ordering to sort books by release date.", "tab_general": "General", "tab_poster": "Poster" }, diff --git a/komga-webui/src/types/komga-readlists.ts b/komga-webui/src/types/komga-readlists.ts index 847d35cea..303ec5e71 100644 --- a/komga-webui/src/types/komga-readlists.ts +++ b/komga-webui/src/types/komga-readlists.ts @@ -2,6 +2,7 @@ interface ReadListDto { id: string, name: string, summary: string, + ordered: boolean, filtered: boolean, bookIds: string[], createdDate: string, @@ -11,12 +12,14 @@ interface ReadListDto { interface ReadListCreationDto { name: string, summary?: string, + ordered?: boolean, bookIds: string[] } interface ReadListUpdateDto { name?: string, summary?: string, + ordered?: boolean, bookIds?: string[] } diff --git a/komga-webui/src/views/BrowseReadList.vue b/komga-webui/src/views/BrowseReadList.vue index b18c234a3..6134b2ac4 100644 --- a/komga-webui/src/views/BrowseReadList.vue +++ b/komga-webui/src/views/BrowseReadList.vue @@ -11,6 +11,9 @@ {{ readList.bookIds.length }} + ({{ $t('browse_readlist.manual_ordering') }}) @@ -131,10 +134,10 @@ @@ -275,6 +278,10 @@ export default Vue.extend({ next() }, computed: { + itemContext(): ItemContext[] { + if(this.readList?.ordered === false) return [ItemContext.SHOW_SERIES, ItemContext.RELEASE_DATE] + return [ItemContext.SHOW_SERIES] + }, paginationVisible(): number { switch (this.$vuetify.breakpoint.name) { case 'xs': diff --git a/komga/src/flyway/resources/db/migration/sqlite/V20230221170726__readlist_ordered.sql b/komga/src/flyway/resources/db/migration/sqlite/V20230221170726__readlist_ordered.sql new file mode 100644 index 000000000..1922ac3e4 --- /dev/null +++ b/komga/src/flyway/resources/db/migration/sqlite/V20230221170726__readlist_ordered.sql @@ -0,0 +1,2 @@ +alter table READLIST + add column ORDERED boolean NOT NULL DEFAULT 1; diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/ReadList.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/ReadList.kt index 484f7934f..a3cbfb3b6 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/ReadList.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/ReadList.kt @@ -8,6 +8,10 @@ import java.util.SortedMap data class ReadList( val name: String, val summary: String = "", + /** + * Indicates whether the read list is ordered manually + */ + val ordered: Boolean = true, val bookIds: SortedMap = sortedMapOf(), diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/SeriesCollection.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/SeriesCollection.kt index a17eb92c5..7313321fe 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/SeriesCollection.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/SeriesCollection.kt @@ -6,6 +6,9 @@ import java.time.LocalDateTime data class SeriesCollection( val name: String, + /** + * Indicates whether the collection is ordered manually + */ val ordered: Boolean = false, val seriesIds: List = emptyList(), diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookDtoDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookDtoDao.kt index 6cdd4e0d3..521a0e70f 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookDtoDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookDtoDao.kt @@ -2,6 +2,7 @@ package org.gotson.komga.infrastructure.jooq import org.gotson.komga.domain.model.BookSearchWithReadProgress import org.gotson.komga.domain.model.ContentRestrictions +import org.gotson.komga.domain.model.ReadList import org.gotson.komga.domain.model.ReadStatus import org.gotson.komga.infrastructure.datasource.SqliteUdfDataSource import org.gotson.komga.infrastructure.search.LuceneEntity @@ -185,20 +186,20 @@ class BookDtoDao( findSiblingSeries(bookId, userId, next = true) override fun findPreviousInReadListOrNull( - readListId: String, + readList: ReadList, bookId: String, userId: String, filterOnLibraryIds: Collection?, ): BookDto? = - findSiblingReadList(readListId, bookId, userId, filterOnLibraryIds, next = false) + findSiblingReadList(readList, bookId, userId, filterOnLibraryIds, next = false) override fun findNextInReadListOrNull( - readListId: String, + readList: ReadList, bookId: String, userId: String, filterOnLibraryIds: Collection?, ): BookDto? = - findSiblingReadList(readListId, bookId, userId, filterOnLibraryIds, next = true) + findSiblingReadList(readList, bookId, userId, filterOnLibraryIds, next = true) override fun findAllOnDeck(userId: String, filterOnLibraryIds: Collection?, pageable: Pageable, restrictions: ContentRestrictions): Page { val seriesIds = dsl.select(s.ID) @@ -283,28 +284,52 @@ class BookDtoDao( } private fun findSiblingReadList( - readListId: String, + readList: ReadList, bookId: String, userId: String, filterOnLibraryIds: Collection?, next: Boolean, ): BookDto? { - val numberSort = dsl.select(rlb.NUMBER) - .from(b) - .leftJoin(rlb).on(b.ID.eq(rlb.BOOK_ID)) - .where(b.ID.eq(bookId)) - .and(rlb.READLIST_ID.eq(readListId)) - .apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } } - .fetchOne(0, Int::class.java) + if (readList.ordered) { + val numberSort = dsl.select(rlb.NUMBER) + .from(b) + .leftJoin(rlb).on(b.ID.eq(rlb.BOOK_ID)) + .where(b.ID.eq(bookId)) + .and(rlb.READLIST_ID.eq(readList.id)) + .apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } } + .fetchOne(rlb.NUMBER) - return selectBase(userId, true) - .where(rlb.READLIST_ID.eq(readListId)) - .apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } } - .orderBy(rlb.NUMBER.let { if (next) it.asc() else it.desc() }) - .seek(numberSort) - .limit(1) - .fetchAndMap() - .firstOrNull() + return selectBase(userId, true) + .where(rlb.READLIST_ID.eq(readList.id)) + .apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } } + .orderBy(rlb.NUMBER.let { if (next) it.asc() else it.desc() }) + .seek(numberSort) + .limit(1) + .fetchAndMap() + .firstOrNull() + } else { + // it is too complex to perform a seek by release date as it could be null and could also have multiple occurrences of the same value + // instead we pull the whole list of ids, and perform the seek on the list + val bookIds = dsl.select(b.ID) + .from(b) + .leftJoin(rlb).on(b.ID.eq(rlb.BOOK_ID)) + .leftJoin(d).on(b.ID.eq(d.BOOK_ID)) + .where(rlb.READLIST_ID.eq(readList.id)) + .apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } } + .orderBy(d.RELEASE_DATE) + .fetch(b.ID) + + val bookIndex = bookIds.indexOfFirst { it == bookId } + if (bookIndex == -1) return null + val siblingId = bookIds.getOrNull(bookIndex + if (next) 1 else -1) ?: return null + + return selectBase(userId) + .where(b.ID.eq(siblingId)) + .apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } } + .limit(1) + .fetchAndMap() + .firstOrNull() + } } private fun selectBase(userId: String, selectReadListNumber: Boolean = false) = diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReadListDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReadListDao.kt index 00d32183e..70b9022ca 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReadListDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReadListDao.kt @@ -157,6 +157,7 @@ class ReadListDao( .set(rl.ID, readList.id) .set(rl.NAME, readList.name) .set(rl.SUMMARY, readList.summary) + .set(rl.ORDERED, readList.ordered) .set(rl.BOOK_COUNT, readList.bookIds.size) .execute() @@ -178,6 +179,7 @@ class ReadListDao( dsl.update(rl) .set(rl.NAME, readList.name) .set(rl.SUMMARY, readList.summary) + .set(rl.ORDERED, readList.ordered) .set(rl.BOOK_COUNT, readList.bookIds.size) .set(rl.LAST_MODIFIED_DATE, LocalDateTime.now(ZoneId.of("Z"))) .where(rl.ID.eq(readList.id)) @@ -233,6 +235,7 @@ class ReadListDao( ReadList( name = name, summary = summary, + ordered = ordered, bookIds = bookIds, id = id, createdDate = createdDate.toCurrentTimeZone(), diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/opds/OpdsController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/opds/OpdsController.kt index b7e4eb6c8..fe9e02e52 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/opds/OpdsController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/opds/OpdsController.kt @@ -621,7 +621,10 @@ class OpdsController( @Parameter(hidden = true) page: Pageable, ): OpdsFeed = readListRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let { readList -> - val pageable = PageRequest.of(page.pageNumber, page.pageSize, Sort.by(Sort.Order.asc("readList.number"))) + val sort = + if (readList.ordered) Sort.by(Sort.Order.asc("readList.number")) + else Sort.by(Sort.Order.asc("metadata.releaseDate")) + val pageable = PageRequest.of(page.pageNumber, page.pageSize, sort) val bookSearch = BookSearchWithReadProgress( mediaStatus = setOf(Media.Status.READY), diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/persistence/BookDtoRepository.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/persistence/BookDtoRepository.kt index 5e02dc4e5..99c3a4408 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/persistence/BookDtoRepository.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/persistence/BookDtoRepository.kt @@ -2,6 +2,7 @@ package org.gotson.komga.interfaces.api.persistence import org.gotson.komga.domain.model.BookSearchWithReadProgress import org.gotson.komga.domain.model.ContentRestrictions +import org.gotson.komga.domain.model.ReadList import org.gotson.komga.interfaces.api.rest.dto.BookDto import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable @@ -26,14 +27,14 @@ interface BookDtoRepository { fun findNextInSeriesOrNull(bookId: String, userId: String): BookDto? fun findPreviousInReadListOrNull( - readListId: String, + readList: ReadList, bookId: String, userId: String, filterOnLibraryIds: Collection?, ): BookDto? fun findNextInReadListOrNull( - readListId: String, + readList: ReadList, bookId: String, userId: String, filterOnLibraryIds: Collection?, diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReadListController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReadListController.kt index b096f7059..8b2f435a5 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReadListController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReadListController.kt @@ -232,6 +232,7 @@ class ReadListController( ReadList( name = readList.name, summary = readList.summary, + ordered = readList.ordered, bookIds = readList.bookIds.toIndexedMap(), ), ).toDto() @@ -258,6 +259,7 @@ class ReadListController( val updated = existing.copy( name = readList.name ?: existing.name, summary = readList.summary ?: existing.summary, + ordered = readList.ordered ?: existing.ordered, bookIds = readList.bookIds?.toIndexedMap() ?: existing.bookIds, ) try { @@ -295,7 +297,9 @@ class ReadListController( @Parameter(hidden = true) page: Pageable, ): Page = readListRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let { readList -> - val sort = Sort.by(Sort.Order.asc("readList.number")) + val sort = + if (readList.ordered) Sort.by(Sort.Order.asc("readList.number")) + else Sort.by(Sort.Order.asc("metadata.releaseDate")) val pageRequest = if (unpaged) UnpagedSorted(sort) @@ -332,7 +336,7 @@ class ReadListController( ): BookDto = readListRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let { bookDtoRepository.findPreviousInReadListOrNull( - id, + it, bookId, principal.user.id, principal.user.getAuthorizedLibraryIds(null), @@ -348,7 +352,7 @@ class ReadListController( ): BookDto = readListRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let { bookDtoRepository.findNextInReadListOrNull( - id, + it, bookId, principal.user.id, principal.user.getAuthorizedLibraryIds(null), diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/ReadListCreationDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/ReadListCreationDto.kt index 7318601e0..277380243 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/ReadListCreationDto.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/ReadListCreationDto.kt @@ -7,6 +7,7 @@ import javax.validation.constraints.NotEmpty data class ReadListCreationDto( @get:NotBlank val name: String, val summary: String = "", + val ordered: Boolean = true, @get:NotEmpty @get:UniqueElements val bookIds: List, ) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/ReadListDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/ReadListDto.kt index 68b0cbbf0..ee3bd40fa 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/ReadListDto.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/ReadListDto.kt @@ -9,6 +9,7 @@ data class ReadListDto( val id: String, val name: String, val summary: String, + val ordered: Boolean, val bookIds: List, @@ -25,6 +26,7 @@ fun ReadList.toDto() = id = id, name = name, summary = summary, + ordered = ordered, bookIds = bookIds.values.toList(), createdDate = createdDate.toUTC(), lastModifiedDate = lastModifiedDate.toUTC(), diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/ReadListUpdateDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/ReadListUpdateDto.kt index f169efcf3..517daa31a 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/ReadListUpdateDto.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/ReadListUpdateDto.kt @@ -9,4 +9,5 @@ data class ReadListUpdateDto( val summary: String?, @get:NullOrNotEmpty @get:UniqueElements val bookIds: List?, + val ordered: Boolean?, ) diff --git a/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/ReadListControllerTest.kt b/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/ReadListControllerTest.kt index 032f6572d..706dc6f9d 100644 --- a/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/ReadListControllerTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/ReadListControllerTest.kt @@ -6,6 +6,7 @@ import org.gotson.komga.domain.model.ReadList import org.gotson.komga.domain.model.makeBook import org.gotson.komga.domain.model.makeLibrary import org.gotson.komga.domain.model.makeSeries +import org.gotson.komga.domain.persistence.BookMetadataRepository import org.gotson.komga.domain.persistence.LibraryRepository import org.gotson.komga.domain.persistence.ReadListRepository import org.gotson.komga.domain.persistence.SeriesMetadataRepository @@ -14,9 +15,11 @@ import org.gotson.komga.domain.service.ReadListLifecycle import org.gotson.komga.domain.service.SeriesLifecycle import org.gotson.komga.language.toIndexedMap import org.hamcrest.Matchers +import org.hamcrest.Matchers.contains import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -33,6 +36,7 @@ import org.springframework.test.web.servlet.post import java.net.URLEncoder import java.nio.charset.StandardCharsets import java.nio.file.Files +import java.time.LocalDate @ExtendWith(SpringExtension::class) @SpringBootTest @@ -45,6 +49,7 @@ class ReadListControllerTest( @Autowired private val libraryRepository: LibraryRepository, @Autowired private val seriesLifecycle: SeriesLifecycle, @Autowired private val seriesMetadataRepository: SeriesMetadataRepository, + @Autowired private val bookMetadataRepository: BookMetadataRepository, ) { private val library1 = makeLibrary("Library1", id = "1") @@ -1011,4 +1016,174 @@ class ReadListControllerTest( header { string("Content-Disposition", Matchers.containsString(URLEncoder.encode(name, StandardCharsets.UTF_8.name()))) } } } + + @Nested + inner class Unordered { + private val library = makeLibrary("Library") + private val series = makeSeries("Series", library.id) + private lateinit var books: List + private lateinit var rlAllDiffDates: ReadList + private lateinit var rlAllNullDates: ReadList + private lateinit var rlAllBooks: ReadList + + @BeforeAll + fun setup() { + libraryRepository.insert(library) + + seriesLifecycle.createSeries(series) + + books = (1..5).map { makeBook("Book_$it", libraryId = library.id, seriesId = series.id) } + seriesLifecycle.addBooks(series, books) + + bookMetadataRepository.findById(books[0].id).let { bookMetadataRepository.update(it.copy(releaseDate = LocalDate.of(2020, 1, 1))) } + bookMetadataRepository.findById(books[1].id).let { bookMetadataRepository.update(it.copy(releaseDate = LocalDate.of(2020, 1, 1))) } + bookMetadataRepository.findById(books[2].id).let { bookMetadataRepository.update(it.copy(releaseDate = LocalDate.of(2021, 1, 1))) } + } + + @BeforeEach + fun makeReadLists() { + rlAllDiffDates = readListLifecycle.addReadList( + ReadList( + name = "All different dates", + ordered = false, + bookIds = listOf(2, 1).map { books[it].id }.toIndexedMap(), + ), + ) + + rlAllNullDates = readListLifecycle.addReadList( + ReadList( + name = "All null dates", + ordered = false, + bookIds = books.drop(3).map { it.id }.toIndexedMap(), + ), + ) + + rlAllBooks = readListLifecycle.addReadList( + ReadList( + name = "All books", + ordered = false, + bookIds = books.map { it.id }.toIndexedMap(), + ), + ) + } + + @Test + @WithMockCustomUser + fun `given unordered read lists when getting books then books are sorted by release date`() { + mockMvc.get("/api/v1/readlists/${rlAllDiffDates.id}/books") + .andExpect { + status { isOk() } + jsonPath("$.content.[*].['id']") { contains(listOf(1, 2).map { books[it].id }) } + } + + mockMvc.get("/api/v1/readlists/${rlAllNullDates.id}/books") + .andExpect { + status { isOk() } + jsonPath("$.content.[*].['id']") { contains(listOf(3, 4).map { books[it].id }) } + } + + mockMvc.get("/api/v1/readlists/${rlAllBooks.id}/books") + .andExpect { + status { isOk() } + jsonPath("$.content.[*].['id']") { contains(listOf(3, 4, 0, 1, 2).map { books[it].id }) } + } + } + + @Test + @WithMockCustomUser + fun `given unordered read lists when getting book siblings then it is returned according to release date sort or not found`() { + // rlAllDiffDates: 1, 2 + // first book: id=1 + mockMvc.get("/api/v1/readlists/${rlAllDiffDates.id}/books/${books[1].id}/previous") + .andExpect { status { isNotFound() } } + mockMvc.get("/api/v1/readlists/${rlAllDiffDates.id}/books/${books[1].id}/next") + .andExpect { + status { isOk() } + jsonPath("$.id") { value(books[2].id) } + } + + // second book: id=2 + mockMvc.get("/api/v1/readlists/${rlAllDiffDates.id}/books/${books[2].id}/previous") + .andExpect { + status { isOk() } + jsonPath("$.id") { value(books[1].id) } + } + mockMvc.get("/api/v1/readlists/${rlAllDiffDates.id}/books/${books[2].id}/next") + .andExpect { status { isNotFound() } } + + // rlAllNullDates: 3, 4 + // first book: id=3 + mockMvc.get("/api/v1/readlists/${rlAllNullDates.id}/books/${books[3].id}/previous") + .andExpect { status { isNotFound() } } + mockMvc.get("/api/v1/readlists/${rlAllNullDates.id}/books/${books[3].id}/next") + .andExpect { + status { isOk() } + jsonPath("$.id") { value(books[4].id) } + } + + // second book: id=4 + mockMvc.get("/api/v1/readlists/${rlAllNullDates.id}/books/${books[4].id}/previous") + .andExpect { + status { isOk() } + jsonPath("$.id") { value(books[3].id) } + } + mockMvc.get("/api/v1/readlists/${rlAllNullDates.id}/books/${books[4].id}/next") + .andExpect { status { isNotFound() } } + + // rlAllBooks: 3, 4, 0, 1, 2 + // first book: id=3 + mockMvc.get("/api/v1/readlists/${rlAllBooks.id}/books/${books[3].id}/previous") + .andExpect { status { isNotFound() } } + mockMvc.get("/api/v1/readlists/${rlAllBooks.id}/books/${books[3].id}/next") + .andExpect { + status { isOk() } + jsonPath("$.id") { value(books[4].id) } + } + + // second book: id=4 + mockMvc.get("/api/v1/readlists/${rlAllBooks.id}/books/${books[4].id}/previous") + .andExpect { + status { isOk() } + jsonPath("$.id") { value(books[3].id) } + } + mockMvc.get("/api/v1/readlists/${rlAllBooks.id}/books/${books[4].id}/next") + .andExpect { + status { isOk() } + jsonPath("$.id") { value(books[0].id) } + } + + // third book: id=0 + mockMvc.get("/api/v1/readlists/${rlAllBooks.id}/books/${books[0].id}/previous") + .andExpect { + status { isOk() } + jsonPath("$.id") { value(books[4].id) } + } + mockMvc.get("/api/v1/readlists/${rlAllBooks.id}/books/${books[0].id}/next") + .andExpect { + status { isOk() } + jsonPath("$.id") { value(books[1].id) } + } + + // fourth book: id=1 + mockMvc.get("/api/v1/readlists/${rlAllBooks.id}/books/${books[1].id}/previous") + .andExpect { + status { isOk() } + jsonPath("$.id") { value(books[0].id) } + } + mockMvc.get("/api/v1/readlists/${rlAllBooks.id}/books/${books[1].id}/next") + .andExpect { + status { isOk() } + jsonPath("$.id") { value(books[2].id) } + } + + // last book: id=2 + mockMvc.get("/api/v1/readlists/${rlAllBooks.id}/books/${books[2].id}/previous") + .andExpect { + status { isOk() } + jsonPath("$.id") { value(books[1].id) } + } + mockMvc.get("/api/v1/readlists/${rlAllBooks.id}/books/${books[2].id}/next") + .andExpect { status { isNotFound() } } + } + } }