feat(api): search books by read status

related to #25
This commit is contained in:
Gauthier Roebroeck 2020-06-04 18:05:01 +08:00
parent 1fc893ecb3
commit 7f3c49280b
7 changed files with 271 additions and 14 deletions

View file

@ -1,8 +1,20 @@
package org.gotson.komga.domain.model
data class BookSearch(
open class BookSearch(
val libraryIds: Collection<Long> = emptyList(),
val seriesIds: Collection<Long> = emptyList(),
val searchTerm: String? = null,
val mediaStatus: Collection<Media.Status> = emptyList()
)
class BookSearchWithReadProgress(
libraryIds: Collection<Long> = emptyList(),
seriesIds: Collection<Long> = emptyList(),
searchTerm: String? = null,
mediaStatus: Collection<Media.Status> = emptyList(),
val readStatus: Collection<ReadStatus> = emptyList()
) : BookSearch(libraryIds, seriesIds, searchTerm, mediaStatus)
enum class ReadStatus {
UNREAD, READ, IN_PROGRESS
}

View file

@ -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<BookDto> {
override fun findAll(search: BookSearchWithReadProgress, userId: Long, pageable: Pageable): Page<BookDto> {
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
}

View file

@ -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<Long>?,
@RequestParam(name = "media_status", required = false) mediaStatus: List<Media.Status>?,
@RequestParam(name = "read_status", required = false) readStatus: List<ReadStatus>?,
@Parameter(hidden = true) page: Pageable
): Page<BookDto> {
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<Long>() else principal.user.sharedLibrariesIds
return bookDtoRepository.findAll(
BookSearch(
BookSearchWithReadProgress(
libraryIds = libraryIds
),
principal.user.id,

View file

@ -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()
),

View file

@ -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<BookDto>
fun findAll(search: BookSearchWithReadProgress, userId: Long, pageable: Pageable): Page<BookDto>
fun findByIdOrNull(bookId: Long, userId: Long): BookDto?
fun findPreviousInSeries(bookId: Long, userId: Long): BookDto?
fun findNextInSeries(bookId: Long, userId: Long): BookDto?

View file

@ -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")
}
}

View file

@ -82,7 +82,7 @@ class BookControllerTest(
}
@AfterAll
fun `teardown`() {
fun teardown() {
userRepository.findAll().forEach {
userLifecycle.deleteUser(it)
}