Skip to content

Commit

Permalink
feat: display total read and unread bayang#171
Browse files Browse the repository at this point in the history
  • Loading branch information
bayang committed Jan 22, 2025
1 parent 72d4c75 commit 04e0bfe
Show file tree
Hide file tree
Showing 9 changed files with 135 additions and 2 deletions.
15 changes: 15 additions & 0 deletions src/jelu-ui/src/components/UserStats.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Ref, ref, watch } from "vue";
import { Bar } from 'vue-chartjs';
import { useI18n } from 'vue-i18n';
import dataService from "../services/DataService";
import { TotalsStats } from '../model/YearStats';
const { t } = useI18n({
inheritLocale: true,
Expand Down Expand Up @@ -112,6 +113,12 @@ const getYearStats = () => {
}
}
const totalStats = () => {
dataService.totalsStats()
.then( data => totals.value = data)
.catch(e => console.log(e))
}
const loading = ref(false)
const chartData = ref<ChartData<'bar'>>({
datasets: []
Expand All @@ -133,6 +140,7 @@ const chartOptions = ref({
const years: Ref<Array<number>> = ref([])
const currentYear: Ref<number|null> = ref(null)
const totals: Ref<TotalsStats> = ref({"read": 0, "unread": 0})
watch(currentYear, (newVal, oldVal) => {
console.log("year " + newVal + " " + oldVal)
Expand All @@ -144,11 +152,18 @@ const loaderFullPage = ref(false)
getAllStats()
getYears()
totalStats()
</script>

<template>
<div class="grid grid-cols-1 justify-center justify-items-center justify-self-center">
<h1 class="text-2xl typewriter w-11/12 sm:w-8/12 pb-4 capitalize">
{{ t('stats.total') }}
</h1>
<div class="mb-2">
{{ t('stats.read') }}:&nbsp;{{ totals.read }} / {{ t('stats.unread') }}:&nbsp;:{{ totals.unread }}
</div>
<h1 class="text-2xl typewriter w-11/12 sm:w-8/12 pb-4 capitalize">
{{ t('stats.all_time') }}
</h1>
Expand Down
5 changes: 4 additions & 1 deletion src/jelu-ui/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,10 @@
"choose_year" : "Choose a year",
"pages_read" : "pages read",
"finished" : "finished",
"dropped" : "dropped"
"dropped" : "dropped",
"total": "total",
"read": "read",
"unread": "unread"

},
"history" : {
Expand Down
5 changes: 5 additions & 0 deletions src/jelu-ui/src/model/YearStats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,8 @@ export interface MonthStats {
month: number,
pageCount: number
}

export interface TotalsStats {
read: number,
unread: number
}
18 changes: 17 additions & 1 deletion src/jelu-ui/src/services/DataService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { LibraryFilter } from "../model/LibraryFilter";
import { WikipediaSearchResult } from "../model/WikipediaSearchResult";
import { WikipediaPageResult } from "../model/WikipediaPageResult";
import { MessageCategory, UpdateUserMessage, UserMessage } from "../model/UserMessage";
import { MonthStats, YearStats } from "../model/YearStats";
import { MonthStats, TotalsStats, YearStats } from "../model/YearStats";
import { Shelf } from "../model/Shelf";
import { CreateReviewDto, Review, UpdateReviewDto, Visibility } from "../model/Review";
import { Role } from "../model/Role";
Expand Down Expand Up @@ -1455,6 +1455,22 @@ class DataService {
}
}

totalsStats = async () => {
try {
const response = await this.apiClient.get<TotalsStats>(`${this.API_STATS}/total`);
console.log("called stats total")
console.log(response)
return response.data;
}
catch (error) {
if (axios.isAxiosError(error) && error.response) {
console.log("error axios " + error.response.status + " " + error.response.data.error)
}
console.log("error stats total " + (error as AxiosError).code)
throw new Error("error stats total " + error)
}
}

shelves = async (name?: string, targetId?: string) => {
try {
const response = await this.apiClient.get<Array<Shelf>>(`${this.API_SHELVES}`, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import io.github.bayang.jelu.dto.Role
import io.github.bayang.jelu.dto.SeriesDto
import io.github.bayang.jelu.dto.SeriesUpdateDto
import io.github.bayang.jelu.dto.TagDto
import io.github.bayang.jelu.dto.TotalsStatsDto
import io.github.bayang.jelu.dto.UserBookBulkUpdateDto
import io.github.bayang.jelu.dto.UserBookLightDto
import io.github.bayang.jelu.dto.UserBookUpdateDto
Expand Down Expand Up @@ -409,4 +410,12 @@ class BooksController(
assertIsJeluUser(principal.principal)
return repository.updateSeries(seriesId, seriesUpdate, (principal.principal as JeluUser).user)
}

@GetMapping(path = ["/stats/total"])
fun totalStats(
principal: Authentication,
): TotalsStatsDto {
assertIsJeluUser(principal.principal)
return repository.stats((principal.principal as JeluUser).user.id!!)
}
}
14 changes: 14 additions & 0 deletions src/main/kotlin/io/github/bayang/jelu/dao/BookRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import io.github.bayang.jelu.dto.SeriesCreateDto
import io.github.bayang.jelu.dto.SeriesOrderDto
import io.github.bayang.jelu.dto.SeriesUpdateDto
import io.github.bayang.jelu.dto.TagDto
import io.github.bayang.jelu.dto.TotalsStatsDto
import io.github.bayang.jelu.dto.UserBookBulkUpdateDto
import io.github.bayang.jelu.dto.UserBookUpdateDto
import io.github.bayang.jelu.dto.UserDto
Expand Down Expand Up @@ -42,6 +43,7 @@ import org.jetbrains.exposed.sql.andWhere
import org.jetbrains.exposed.sql.avg
import org.jetbrains.exposed.sql.batchInsert
import org.jetbrains.exposed.sql.count
import org.jetbrains.exposed.sql.countDistinct
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.lowerCase
import org.jetbrains.exposed.sql.or
Expand Down Expand Up @@ -1529,4 +1531,16 @@ class BookRepository(
}
}
}

fun stats(userId: UUID): TotalsStatsDto {
val query = UserBookTable.join(ReadingEventTable, JoinType.LEFT, onColumn = UserBookTable.id, otherColumn = ReadingEventTable.userBook)
.select(UserBookTable.id.countDistinct())
.andWhere { UserBookTable.user eq userId }
.andWhere { ReadingEventTable.eventType eq ReadingEventType.FINISHED }
.distinct()
val resultRow = query.single()
val readCount = resultRow[UserBookTable.id.countDistinct()]
val totalUserBooks = UserBook.count(UserBookTable.user eq userId)
return TotalsStatsDto(read = readCount, unread = totalUserBooks - readCount)
}
}
5 changes: 5 additions & 0 deletions src/main/kotlin/io/github/bayang/jelu/dto/ReadStatsDto.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,8 @@ data class MonthStatsDto(
val month: Int,
val pageCount: Int = 0,
)

data class TotalsStatsDto(
val read: Long = 0,
val unread: Long = 0,
)
6 changes: 6 additions & 0 deletions src/main/kotlin/io/github/bayang/jelu/service/BookService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import io.github.bayang.jelu.dto.SeriesDto
import io.github.bayang.jelu.dto.SeriesRatingDto
import io.github.bayang.jelu.dto.SeriesUpdateDto
import io.github.bayang.jelu.dto.TagDto
import io.github.bayang.jelu.dto.TotalsStatsDto
import io.github.bayang.jelu.dto.UserBookBulkUpdateDto
import io.github.bayang.jelu.dto.UserBookLightDto
import io.github.bayang.jelu.dto.UserBookUpdateDto
Expand Down Expand Up @@ -647,4 +648,9 @@ class BookService(
}
} while (booksPage.hasNext())
}

@Transactional
fun stats(userId: UUID): TotalsStatsDto {
return bookRepository.stats(userId)
}
}
60 changes: 60 additions & 0 deletions src/test/kotlin/io/github/bayang/jelu/service/BookServiceTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2022,6 +2022,66 @@ class BookServiceTest(
Assertions.assertEquals(1, entitiesIds?.size)
}

@Test
fun testGlobalStats() {
var entitiesIds = luceneHelper.searchEntitiesIds("title1", LuceneEntity.Book)
Assertions.assertEquals(0, entitiesIds?.size)
val createBook = bookDto()
val createUserBookDto = createUserBookDto(createBook, ReadingEventType.FINISHED, nowInstant())
val uploadFile = MockMultipartFile("test-cover.jpg", "test-cover.jpg", "image/jpeg", this::class.java.getResourceAsStream("test-cover.jpg"))
val saved: UserBookLightDto = bookService.save(createUserBookDto, user(), uploadFile)
Assertions.assertEquals(createBook.isbn10, saved.book.isbn10)
Assertions.assertEquals(ReadingEventType.FINISHED, saved.lastReadingEvent)
Assertions.assertNotNull(saved.lastReadingEventDate)
Assertions.assertEquals(1, readingEventService.findAll(null, null, null, null, null, null, null, Pageable.ofSize(30)).totalElements)
entitiesIds = luceneHelper.searchEntitiesIds("title1", LuceneEntity.Book)
Assertions.assertEquals(1, entitiesIds?.size)
Assertions.assertEquals(1, bookService.stats(user().id!!).read)
Assertions.assertEquals(0, bookService.stats(user().id!!).unread)

val updater = UserBookUpdateDto(
ReadingEventType.DROPPED,
personalNotes = "new notes",
owned = false,
book = null,
toRead = null,
percentRead = 50,
borrowed = true,
currentPageNumber = null,
)
val updated = bookService.update(saved.id!!, updater, null)
Assertions.assertEquals(ReadingEventType.DROPPED, updated.lastReadingEvent)
Assertions.assertNotNull(updated.lastReadingEventDate)
Assertions.assertEquals(2, updated.readingEvents?.size)
Assertions.assertEquals(2, readingEventService.findAll(null, null, null, null, null, null, null, Pageable.ofSize(30)).totalElements)
Assertions.assertEquals(1, bookService.stats(user().id!!).read)
Assertions.assertEquals(0, bookService.stats(user().id!!).unread)

val createBook2 = bookDto("title 2")
val createUserBookDto2 = createUserBookDto(createBook2, ReadingEventType.CURRENTLY_READING, nowInstant())
val uploadFile2 = MockMultipartFile("test-cover.jpg", "test-cover.jpg", "image/jpeg", this::class.java.getResourceAsStream("test-cover.jpg"))
val saved2: UserBookLightDto = bookService.save(createUserBookDto2, user(), uploadFile2)
Assertions.assertEquals(1, bookService.stats(user().id!!).read)
Assertions.assertEquals(1, bookService.stats(user().id!!).unread)

val updater2 = UserBookUpdateDto(
ReadingEventType.FINISHED,
personalNotes = "new notes",
owned = false,
book = null,
toRead = null,
percentRead = 50,
borrowed = true,
currentPageNumber = null,
)
val updated2 = bookService.update(saved2.id!!, updater2, null)
Assertions.assertEquals(ReadingEventType.FINISHED, updated2.lastReadingEvent)
Assertions.assertNotNull(updated2.lastReadingEventDate)
Assertions.assertEquals(1, updated2.readingEvents?.size)
Assertions.assertEquals(2, bookService.stats(user().id!!).read)
Assertions.assertEquals(0, bookService.stats(user().id!!).unread)
}

@Test
fun testUpdateUserbookWithImageAndEventNewEventRequiredAndDeleteExistingImage() {
var entitiesIds = luceneHelper.searchEntitiesIds("title1", LuceneEntity.Book)
Expand Down

0 comments on commit 04e0bfe

Please sign in to comment.