From 7f3c49280b48b38e9308025e4dd95b6f1cc921c0 Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Thu, 4 Jun 2020 18:05:01 +0800 Subject: [PATCH] feat(api): search books by read status related to #25 --- .../gotson/komga/domain/model/BookSearch.kt | 14 +- .../komga/infrastructure/jooq/BookDtoDao.kt | 26 +- .../komga/interfaces/rest/BookController.kt | 11 +- .../komga/interfaces/rest/SeriesController.kt | 4 +- .../rest/persistence/BookDtoRepository.kt | 4 +- .../infrastructure/jooq/BookDtoDaoTest.kt | 224 ++++++++++++++++++ .../interfaces/rest/BookControllerTest.kt | 2 +- 7 files changed, 271 insertions(+), 14 deletions(-) create mode 100644 komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/BookDtoDaoTest.kt diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/BookSearch.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/BookSearch.kt index 5b8ee4f57..35fca5df7 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/BookSearch.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/BookSearch.kt @@ -1,8 +1,20 @@ package org.gotson.komga.domain.model -data class BookSearch( +open class BookSearch( val libraryIds: Collection = emptyList(), val seriesIds: Collection = emptyList(), val searchTerm: String? = null, val mediaStatus: Collection = emptyList() ) + +class BookSearchWithReadProgress( + libraryIds: Collection = emptyList(), + seriesIds: Collection = emptyList(), + searchTerm: String? = null, + mediaStatus: Collection = emptyList(), + val readStatus: Collection = emptyList() +) : BookSearch(libraryIds, seriesIds, searchTerm, mediaStatus) + +enum class ReadStatus { + UNREAD, READ, IN_PROGRESS +} 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 995c59f26..5301aac2d 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 @@ -1,6 +1,7 @@ package org.gotson.komga.infrastructure.jooq -import org.gotson.komga.domain.model.BookSearch +import org.gotson.komga.domain.model.BookSearchWithReadProgress +import org.gotson.komga.domain.model.ReadStatus import org.gotson.komga.interfaces.rest.dto.AuthorDto import org.gotson.komga.interfaces.rest.dto.BookDto import org.gotson.komga.interfaces.rest.dto.BookMetadataDto @@ -39,19 +40,24 @@ class BookDtoDao( private val sorts = mapOf( "metadata.numberSort" to d.NUMBER_SORT, + "created" to b.CREATED_DATE, "createdDate" to b.CREATED_DATE, + "lastModified" to b.LAST_MODIFIED_DATE, "lastModifiedDate" to b.LAST_MODIFIED_DATE, - "fileSize" to b.FILE_SIZE + "fileSize" to b.FILE_SIZE, + "readProgress.lastModified" to r.LAST_MODIFIED_DATE ) - override fun findAll(search: BookSearch, userId: Long, pageable: Pageable): Page { + override fun findAll(search: BookSearchWithReadProgress, userId: Long, pageable: Pageable): Page { val conditions = search.toCondition() val count = dsl.selectCount() .from(b) .leftJoin(m).on(b.ID.eq(m.BOOK_ID)) .leftJoin(d).on(b.ID.eq(d.BOOK_ID)) + .leftJoin(r).on(b.ID.eq(r.BOOK_ID)) .where(conditions) + .and(readProgressCondition(userId)) .fetchOne(0, Long::class.java) val orderBy = pageable.sort.toOrderBy(sorts) @@ -131,7 +137,7 @@ class BookDtoDao( br.toDto(mr.toDto(), dr.toDto(authors), if (rr.userId != null) rr.toDto() else null) } - private fun BookSearch.toCondition(): Condition { + private fun BookSearchWithReadProgress.toCondition(): Condition { var c: Condition = DSL.trueCondition() if (libraryIds.isNotEmpty()) c = c.and(b.LIBRARY_ID.`in`(libraryIds)) @@ -139,6 +145,18 @@ class BookDtoDao( searchTerm?.let { c = c.and(d.TITLE.containsIgnoreCase(it)) } if (mediaStatus.isNotEmpty()) c = c.and(m.STATUS.`in`(mediaStatus)) + if (readStatus.isNotEmpty()) { + val cr = readStatus.map { + when (it) { + ReadStatus.UNREAD -> r.COMPLETED.isNull + ReadStatus.READ -> r.COMPLETED.isTrue + ReadStatus.IN_PROGRESS -> r.COMPLETED.isFalse + } + }.reduce { acc, condition -> acc.or(condition) } + + c = c.and(cr) + } + return c } diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/BookController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/BookController.kt index 54a48ba53..a8baf2f67 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/BookController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/BookController.kt @@ -8,10 +8,11 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse import mu.KotlinLogging import org.gotson.komga.application.tasks.TaskReceiver import org.gotson.komga.domain.model.Author -import org.gotson.komga.domain.model.BookSearch +import org.gotson.komga.domain.model.BookSearchWithReadProgress import org.gotson.komga.domain.model.ImageConversionException import org.gotson.komga.domain.model.Media import org.gotson.komga.domain.model.MediaNotReadyException +import org.gotson.komga.domain.model.ReadStatus import org.gotson.komga.domain.persistence.BookMetadataRepository import org.gotson.komga.domain.persistence.BookRepository import org.gotson.komga.domain.persistence.MediaRepository @@ -77,6 +78,7 @@ class BookController( @RequestParam(name = "search", required = false) searchTerm: String?, @RequestParam(name = "library_id", required = false) libraryIds: List?, @RequestParam(name = "media_status", required = false) mediaStatus: List?, + @RequestParam(name = "read_status", required = false) readStatus: List?, @Parameter(hidden = true) page: Pageable ): Page { val pageRequest = PageRequest.of( @@ -86,10 +88,11 @@ class BookController( else Sort.by(Sort.Order.asc("metadata.title").ignoreCase()) ) - val bookSearch = BookSearch( + val bookSearch = BookSearchWithReadProgress( libraryIds = principal.user.getAuthorizedLibraryIds(libraryIds), searchTerm = searchTerm, - mediaStatus = mediaStatus ?: emptyList() + mediaStatus = mediaStatus ?: emptyList(), + readStatus = readStatus ?: emptyList() ) return bookDtoRepository.findAll(bookSearch, principal.user.id, pageRequest) @@ -113,7 +116,7 @@ class BookController( val libraryIds = if (principal.user.sharedAllLibraries) emptyList() else principal.user.sharedLibrariesIds return bookDtoRepository.findAll( - BookSearch( + BookSearchWithReadProgress( libraryIds = libraryIds ), principal.user.id, diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesController.kt index 979b694a3..94e86127d 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesController.kt @@ -7,7 +7,7 @@ import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import mu.KotlinLogging import org.gotson.komga.application.tasks.TaskReceiver -import org.gotson.komga.domain.model.BookSearch +import org.gotson.komga.domain.model.BookSearchWithReadProgress import org.gotson.komga.domain.model.Media import org.gotson.komga.domain.model.SeriesMetadata import org.gotson.komga.domain.model.SeriesSearch @@ -198,7 +198,7 @@ class SeriesController( ) return bookDtoRepository.findAll( - BookSearch( + BookSearchWithReadProgress( seriesIds = listOf(seriesId), mediaStatus = mediaStatus ?: emptyList() ), diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/persistence/BookDtoRepository.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/persistence/BookDtoRepository.kt index 1b432b4f6..c6fb7258d 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/persistence/BookDtoRepository.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/persistence/BookDtoRepository.kt @@ -1,12 +1,12 @@ package org.gotson.komga.interfaces.rest.persistence -import org.gotson.komga.domain.model.BookSearch +import org.gotson.komga.domain.model.BookSearchWithReadProgress import org.gotson.komga.interfaces.rest.dto.BookDto import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable interface BookDtoRepository { - fun findAll(search: BookSearch, userId: Long, pageable: Pageable): Page + fun findAll(search: BookSearchWithReadProgress, userId: Long, pageable: Pageable): Page fun findByIdOrNull(bookId: Long, userId: Long): BookDto? fun findPreviousInSeries(bookId: Long, userId: Long): BookDto? fun findNextInSeries(bookId: Long, userId: Long): BookDto? diff --git a/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/BookDtoDaoTest.kt b/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/BookDtoDaoTest.kt new file mode 100644 index 000000000..4d6c1840e --- /dev/null +++ b/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/BookDtoDaoTest.kt @@ -0,0 +1,224 @@ +package org.gotson.komga.infrastructure.jooq + +import org.assertj.core.api.Assertions.assertThat +import org.gotson.komga.domain.model.BookSearchWithReadProgress +import org.gotson.komga.domain.model.KomgaUser +import org.gotson.komga.domain.model.ReadProgress +import org.gotson.komga.domain.model.ReadStatus +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.BookRepository +import org.gotson.komga.domain.persistence.KomgaUserRepository +import org.gotson.komga.domain.persistence.LibraryRepository +import org.gotson.komga.domain.persistence.ReadProgressRepository +import org.gotson.komga.domain.persistence.SeriesRepository +import org.gotson.komga.domain.service.BookLifecycle +import org.gotson.komga.domain.service.KomgaUserLifecycle +import org.gotson.komga.domain.service.LibraryLifecycle +import org.gotson.komga.domain.service.SeriesLifecycle +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.data.domain.PageRequest +import org.springframework.test.context.junit.jupiter.SpringExtension + +@ExtendWith(SpringExtension::class) +@SpringBootTest +@AutoConfigureTestDatabase +class BookDtoDaoTest( + @Autowired private val bookDtoDao: BookDtoDao, + @Autowired private val bookRepository: BookRepository, + @Autowired private val bookLifecycle: BookLifecycle, + @Autowired private val seriesRepository: SeriesRepository, + @Autowired private val seriesLifecycle: SeriesLifecycle, + @Autowired private val libraryRepository: LibraryRepository, + @Autowired private val libraryLifecycle: LibraryLifecycle, + @Autowired private val readProgressRepository: ReadProgressRepository, + @Autowired private val userRepository: KomgaUserRepository, + @Autowired private val userLifecycle: KomgaUserLifecycle +) { + + private var library = makeLibrary() + private var series = makeSeries("Series") + private var user = KomgaUser("user@example.org", "", false) + + @BeforeAll + fun setup() { + library = libraryRepository.insert(library) + series = seriesLifecycle.createSeries(series.copy(libraryId = library.id)) + user = userRepository.save(user) + } + + @AfterEach + fun deleteBooks() { + bookRepository.findAll().forEach { + bookLifecycle.delete(it.id) + } + } + + @AfterAll + fun tearDown() { + userRepository.findAll().forEach { + userLifecycle.deleteUser(it) + } + libraryRepository.findAll().forEach { + libraryLifecycle.deleteLibrary(it) + } + } + + private fun setupBooks() { + seriesLifecycle.addBooks(series, + (1..3).map { + makeBook("$it", seriesId = series.id, libraryId = library.id) + }) + + val books = bookRepository.findAll().sortedBy { it.name } + books.elementAt(0).let { readProgressRepository.save(ReadProgress(it.id, user.id, 5, false)) } + books.elementAt(1).let { readProgressRepository.save(ReadProgress(it.id, user.id, 5, true)) } + } + + @Test + fun `given books in various read status when searching for read books then only read books are returned`() { + // given + setupBooks() + + // when + val found = bookDtoDao.findAll( + BookSearchWithReadProgress(readStatus = listOf(ReadStatus.READ)), + user.id, + PageRequest.of(0, 20) + ) + + // then + assertThat(found).hasSize(1) + assertThat(found.first().readProgress?.completed).isTrue() + assertThat(found.first().name).isEqualTo("2") + } + + @Test + fun `given books in various read status when searching for unread books then only unread books are returned`() { + // given + setupBooks() + + // when + val found = bookDtoDao.findAll( + BookSearchWithReadProgress(readStatus = listOf(ReadStatus.UNREAD)), + user.id, + PageRequest.of(0, 20) + ) + + // then + assertThat(found).hasSize(1) + assertThat(found.first().readProgress).isNull() + assertThat(found.first().name).isEqualTo("3") + } + + @Test + fun `given books in various read status when searching for in progress books then only in progress books are returned`() { + // given + setupBooks() + + // when + val found = bookDtoDao.findAll( + BookSearchWithReadProgress(readStatus = listOf(ReadStatus.IN_PROGRESS)), + user.id, + PageRequest.of(0, 20) + ) + + // then + assertThat(found).hasSize(1) + assertThat(found.first().readProgress?.completed).isFalse() + assertThat(found.first().name).isEqualTo("1") + } + + @Test + fun `given books in various read status when searching for read and unread books then only matching books are returned`() { + // given + setupBooks() + + // when + val found = bookDtoDao.findAll( + BookSearchWithReadProgress(readStatus = listOf(ReadStatus.READ, ReadStatus.UNREAD)), + user.id, + PageRequest.of(0, 20) + ) + + // then + assertThat(found).hasSize(2) + assertThat(found.map { it.name }).containsExactlyInAnyOrder("2", "3") + } + + @Test + fun `given books in various read status when searching for read and in progress books then only matching books are returned`() { + // given + setupBooks() + + // when + val found = bookDtoDao.findAll( + BookSearchWithReadProgress(readStatus = listOf(ReadStatus.READ, ReadStatus.IN_PROGRESS)), + user.id, + PageRequest.of(0, 20) + ) + + // then + assertThat(found).hasSize(2) + assertThat(found.map { it.name }).containsExactlyInAnyOrder("2", "1") + } + + @Test + fun `given books in various read status when searching for unread and in progress books then only matching books are returned`() { + // given + setupBooks() + + // when + val found = bookDtoDao.findAll( + BookSearchWithReadProgress(readStatus = listOf(ReadStatus.UNREAD, ReadStatus.IN_PROGRESS)), + user.id, + PageRequest.of(0, 20) + ) + + // then + assertThat(found).hasSize(2) + assertThat(found.map { it.name }).containsExactlyInAnyOrder("3", "1") + } + + @Test + fun `given books in various read status when searching for read and unread and in progress books then only matching books are returned`() { + // given + setupBooks() + + // when + val found = bookDtoDao.findAll( + BookSearchWithReadProgress(readStatus = listOf(ReadStatus.UNREAD, ReadStatus.IN_PROGRESS, ReadStatus.READ)), + user.id, + PageRequest.of(0, 20) + ) + + // then + assertThat(found).hasSize(3) + assertThat(found.map { it.name }).containsExactlyInAnyOrder("3", "1", "2") + } + + @Test + fun `given books in various read status when searching without read progress then all books are returned`() { + // given + setupBooks() + + // when + val found = bookDtoDao.findAll( + BookSearchWithReadProgress(), + user.id, + PageRequest.of(0, 20) + ) + + // then + assertThat(found).hasSize(3) + assertThat(found.map { it.name }).containsExactlyInAnyOrder("3", "1", "2") + } +} diff --git a/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/BookControllerTest.kt b/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/BookControllerTest.kt index 0ce0e027d..e84f3a78f 100644 --- a/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/BookControllerTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/BookControllerTest.kt @@ -82,7 +82,7 @@ class BookControllerTest( } @AfterAll - fun `teardown`() { + fun teardown() { userRepository.findAll().forEach { userLifecycle.deleteUser(it) }