Skip to content

Commit

Permalink
feat(api): search books by read status
Browse files Browse the repository at this point in the history
related to gotson#25
  • Loading branch information
gotson committed Jun 4, 2020
1 parent 1fc893e commit 7f3c492
Show file tree
Hide file tree
Showing 7 changed files with 271 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -131,14 +137,26 @@ 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))
if (seriesIds.isNotEmpty()) c = c.and(b.SERIES_ID.`in`(seriesIds))
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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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)
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -198,7 +198,7 @@ class SeriesController(
)

return bookDtoRepository.findAll(
BookSearch(
BookSearchWithReadProgress(
seriesIds = listOf(seriesId),
mediaStatus = mediaStatus ?: emptyList()
),
Expand Down
Original file line number Diff line number Diff line change
@@ -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?
Expand Down
Original file line number Diff line number Diff line change
@@ -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")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ class BookControllerTest(
}

@AfterAll
fun `teardown`() {
fun teardown() {
userRepository.findAll().forEach {
userLifecycle.deleteUser(it)
}
Expand Down

0 comments on commit 7f3c492

Please sign in to comment.