diff --git a/src/jelu-ui/package-lock.json b/src/jelu-ui/package-lock.json
index bba250af..1202e69e 100644
--- a/src/jelu-ui/package-lock.json
+++ b/src/jelu-ui/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "jelu-ui",
- "version": "0.16.1",
+ "version": "0.17.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "jelu-ui",
- "version": "0.16.1",
+ "version": "0.17.1",
"dependencies": {
"@formkit/i18n": "^1.0.0-beta.7-c3de75f",
"@formkit/tailwindcss": "^1.0.0-beta.7-c3de75f",
diff --git a/src/jelu-ui/src/components/AdminBase.vue b/src/jelu-ui/src/components/AdminBase.vue
index ac204f6a..c64127b8 100644
--- a/src/jelu-ui/src/components/AdminBase.vue
+++ b/src/jelu-ui/src/components/AdminBase.vue
@@ -18,6 +18,7 @@ const items = ref([{ name:t('settings.profile'), tooltip:t('settings.my_profile'
{ name:t('settings.settings'), icon:"bxs-cog", href:"/profile/settings", tooltip: t('settings.profile') },
{ name:t('settings.authors'), icon:"bxs-user-account", href:"/profile/admin/authors", tooltip: t('settings.author_management') },
{ name:t('settings.imports'), icon:"bxs-file-plus", href:"/profile/imports", tooltip: t('settings.csv_import') },
+ { name:t('settings.messages'), icon:"bxs-message-alt-detail", href:"/profile/messages" },
])
if (store.getters.isAdmin) {
diff --git a/src/jelu-ui/src/components/Imports.vue b/src/jelu-ui/src/components/Imports.vue
index d85f0a40..9ba46146 100644
--- a/src/jelu-ui/src/components/Imports.vue
+++ b/src/jelu-ui/src/components/Imports.vue
@@ -18,6 +18,8 @@ const fetchMetadata = ref(true)
const fetchCovers = ref(true)
const uploadPercentage = ref(0);
const errorMessage = ref("");
+const exportErrorMessage = ref("");
+const exportMessage = ref("");
const handleFileUpload = (event: any) => {
file.value = event.target.files[0];
@@ -42,6 +44,16 @@ const importFile = async () => {
})
}
+const exportFile =async () => {
+ console.log("export requested")
+ try {
+ await dataService.exportCsv()
+ exportMessage.value = t('csv_import.export_ok')
+ } catch (error) {
+ exportErrorMessage.value = t('csv_import.export_ko')
+ }
+}
+
@@ -131,6 +143,36 @@ const importFile = async () => {
+
+ {{ t('csv_import.export') }}
+
+
+
+ {{ t('csv_import.export_message') }}
+
+
+
+
+
+
+ {{ exportMessage }}
+
+
+ {{ exportErrorMessage }}
+
+
+
diff --git a/src/jelu-ui/src/components/UserMessages.vue b/src/jelu-ui/src/components/UserMessages.vue
new file mode 100644
index 00000000..75ee18b9
--- /dev/null
+++ b/src/jelu-ui/src/components/UserMessages.vue
@@ -0,0 +1,196 @@
+
+
+
+
+
+ {{ t('settings.messages') }}
+
+
+
+
+
+
+
+
diff --git a/src/jelu-ui/src/locales/en.json b/src/jelu-ui/src/locales/en.json
index 7d2c4551..85ae3e6d 100644
--- a/src/jelu-ui/src/locales/en.json
+++ b/src/jelu-ui/src/locales/en.json
@@ -92,7 +92,7 @@
"profile" : "Profile",
"my_profile" : "My profile",
"settings" : "Settings",
- "imports" : "Imports",
+ "imports" : "Imports/Exports",
"author_management" : "Authors management",
"csv_import" : "Csv import",
"authors" : "Authors",
@@ -101,7 +101,8 @@
"users" : "Users",
"users_management" : "Users management",
"shortcuts" : "Shortcuts",
- "shortcuts_tooltip" : "Keyboard shortcuts"
+ "shortcuts_tooltip" : "Keyboard shortcuts",
+ "messages" : "Messages"
},
"user" : {
"log_first" : "Please log in first"
@@ -201,7 +202,12 @@
"auto_fetch_online" : "Automatically fetch metadata online",
"fetch_covers" : "Also fetch covers",
"choose_file" : "Choose file to import",
- "import_file" : "import file"
+ "import_file" : "import file",
+ "export" : "export your data",
+ "export_file" : "export file",
+ "export_message" : "clicking the button below will trigger an export request, you will be warned in the messages section when your export is ready.",
+ "export_ok" : "Your export request has been saved. You can leave this page, the report will be available in the messages section.",
+ "export_ko" : "An error happened and you export request has probably not been registered."
},
"login" : {
"login" : "login",
@@ -234,5 +240,9 @@
},
"profile" : {
"edit_user" : "Edit user"
+ },
+ "user-messages" : {
+ "link" : "link",
+ "mark_read": "mark as read"
}
}
diff --git a/src/jelu-ui/src/locales/fr.json b/src/jelu-ui/src/locales/fr.json
index 8d17a7f4..e191f892 100644
--- a/src/jelu-ui/src/locales/fr.json
+++ b/src/jelu-ui/src/locales/fr.json
@@ -93,7 +93,7 @@
"profile" : "Profil",
"my_profile" : "Mon profil",
"settings" : "Paramètres",
- "imports" : "Imports",
+ "imports" : "Imports/Exports",
"author_management" : "Gestion des auteurs",
"csv_import" : "Import Csv",
"authors" : "Auteurs",
@@ -102,7 +102,8 @@
"users" : "Utilisateurs",
"users_management" : "gestion des utilisateurs",
"shortcuts" : "Raccourcis",
- "shortcuts_tooltip" : "Raccourcis claviers"
+ "shortcuts_tooltip" : "Raccourcis claviers",
+ "messages" : "Messages"
},
"user" : {
"log_first" : "Veuillez vous identifier"
@@ -202,7 +203,12 @@
"auto_fetch_online" : "Récupérer automatiquement les métadonnées depuis le web",
"fetch_covers" : "Récupérer aussi les couvertures",
"choose_file" : "Choisissez le fichier à importer",
- "import_file" : "importer le fichier"
+ "import_file" : "importer le fichier",
+ "export" : "exportez vos données",
+ "export_file" : "exporter fichier",
+ "export_message" : "Cliquer sur le bouton pour déclencher un export, consulter ensuite la section messages pour savoir quand l'export est prêt.",
+ "export_ok" : "Votre demande d'export a bien été enregistrée. Vous pouvez quitter cette page, l'export sera disponible dans la section messages.",
+ "export_ko" : "Une erreur est survenue, votre demande d'export n'a probablement pas été prise en compte."
},
"login" : {
"login" : "identifiant",
@@ -235,5 +241,9 @@
},
"profile" : {
"edit_user" : "Modifier utilisateur"
+ },
+ "user-messages" : {
+ "link" : "lien",
+ "mark_read": "marquer comme lu"
}
}
diff --git a/src/jelu-ui/src/model/UserMessage.ts b/src/jelu-ui/src/model/UserMessage.ts
new file mode 100644
index 00000000..a1b4cab6
--- /dev/null
+++ b/src/jelu-ui/src/model/UserMessage.ts
@@ -0,0 +1,23 @@
+export interface UserMessage {
+ id?: string,
+ creationDate?: string,
+ modificationDate?: Date,
+ category: MessageCategory,
+ message?: string,
+ link?: string,
+ read?: boolean
+ }
+
+ export interface UpdateUserMessage {
+ category?: MessageCategory,
+ message?: string,
+ link?: string,
+ read?: boolean
+ }
+
+ export enum MessageCategory {
+ SUCCESS = 'SUCCESS',
+ INFO = 'INFO',
+ WARNING = 'WARNING',
+ ERROR = 'ERROR'
+ }
\ No newline at end of file
diff --git a/src/jelu-ui/src/router.ts b/src/jelu-ui/src/router.ts
index 47668485..6b46444f 100644
--- a/src/jelu-ui/src/router.ts
+++ b/src/jelu-ui/src/router.ts
@@ -90,6 +90,7 @@ const router = createRouter({
{ path : 'admin/users', beforeEnter: [isAdmin], component: () => import(/* webpackChunkName: "recommend" */ './components/AdminUsers.vue')},
{ path: 'imports', component: () => import(/* webpackChunkName: "recommend" */ './components/Imports.vue')},
{ path: 'settings', component: () => import(/* webpackChunkName: "recommend" */ './components/UserSettings.vue')},
+ { path: 'messages', component: () => import(/* webpackChunkName: "recommend" */ './components/UserMessages.vue')},
]
},
],
diff --git a/src/jelu-ui/src/services/DataService.ts b/src/jelu-ui/src/services/DataService.ts
index fa324ee2..c6ccc444 100644
--- a/src/jelu-ui/src/services/DataService.ts
+++ b/src/jelu-ui/src/services/DataService.ts
@@ -15,6 +15,7 @@ import dayjs from "dayjs";
import { LibraryFilter } from "../model/LibraryFilter";
import { WikipediaSearchResult } from "../model/WikipediaSearchResult";
import { WikipediaPageResult } from "../model/WikipediaPageResult";
+import { MessageCategory, UpdateUserMessage, UserMessage } from "../model/UserMessage";
class DataService {
@@ -46,6 +47,8 @@ class DataService {
private API_IMPORTS = '/imports';
+ private API_EXPORTS = '/exports';
+
private API_WIKIPEDIA = '/wikipedia';
private API_SEARCH = '/search';
@@ -54,6 +57,8 @@ class DataService {
private API_MERGE = '/merge';
+ private API_USER_MESSAGES = '/user-messages';
+
private MODE: string;
private BASE_URL: string;
@@ -790,6 +795,19 @@ class DataService {
}
}
+ exportCsv = async () => {
+ try {
+ await this.apiClient.post(this.API_EXPORTS)
+ } catch (error) {
+ if (axios.isAxiosError(error) && error.response) {
+ console.log("error export csv " + error.response.status + " " + error.response.data.error)
+ throw new Error("error export csv " + error.response.status + " " + error)
+ }
+ console.log("error export csv " + (error as AxiosError).code)
+ throw new Error("error exporting csv request" + error)
+ }
+ }
+
updateReadingEvent = async (event: ReadingEvent) => {
try {
const resp = await this.apiClient.put(`${this.API_READING_EVENTS}/${event.id}`, {
@@ -904,6 +922,62 @@ class DataService {
}
}
+ /*
+ * Dates are deserialized as strings, convert to Date instead
+ */
+ transformUserMessage = (data: string) => {
+ const ev = JSON.parse(data)
+ if (ev.modificationDate != null) {
+ ev.modificationDate = dayjs(ev.modificationDate).toDate()
+ }
+ return ev
+ }
+
+ messages = async (messageCategories?: Array|null, read?: boolean,
+ page?: number, size?: number, sort?: string) => {
+ try {
+ const response = await this.apiClient.get>(`${this.API_USER_MESSAGES}`, {
+ params: {
+ messageCategories: messageCategories,
+ read: read,
+ page: page,
+ size: size,
+ sort: sort
+ },
+ paramsSerializer: function(params) {
+ return qs.stringify(params, {arrayFormat: 'comma'})
+ },
+ transformResponse: this.transformUserMessage
+ });
+ console.log("called userMessages")
+ 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 userMessages " + (error as AxiosError).code)
+ throw new Error("error userMessages " + error)
+ }
+ }
+
+ updateUserMessage = async (messageId: string, updateDto: UpdateUserMessage) => {
+ try {
+ const response = await this.apiClient.put(`${this.API_USER_MESSAGES}/${messageId}`, updateDto);
+ console.log("called update userMessage")
+ 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 update userMessage " + (error as AxiosError).code)
+ throw new Error("error update userMessage " + error)
+ }
+ }
+
}
diff --git a/src/jelu-ui/vite.config.ts b/src/jelu-ui/vite.config.ts
index a90ba048..7b6c9343 100644
--- a/src/jelu-ui/vite.config.ts
+++ b/src/jelu-ui/vite.config.ts
@@ -16,7 +16,8 @@ export default defineConfig({
],
server : {
proxy : {
- '/files/': 'http://localhost:11111/'
+ '/files/': 'http://localhost:11111/',
+ '/exports/': 'http://localhost:11111/'
}
},
build: {
diff --git a/src/main/kotlin/io/github/bayang/jelu/config/SecurityConfig.kt b/src/main/kotlin/io/github/bayang/jelu/config/SecurityConfig.kt
index 48f81c64..32744974 100644
--- a/src/main/kotlin/io/github/bayang/jelu/config/SecurityConfig.kt
+++ b/src/main/kotlin/io/github/bayang/jelu/config/SecurityConfig.kt
@@ -33,6 +33,10 @@ class SecurityConfig : WebSecurityConfigurerAdapter() {
it.antMatchers(
"/api/v1/users",
).hasRole("ADMIN")
+ it.mvcMatchers(
+ HttpMethod.POST,
+ "/api/v1/user-messages",
+ ).hasRole("ADMIN")
it.antMatchers(
"/api/**",
).hasRole("USER")
diff --git a/src/main/kotlin/io/github/bayang/jelu/config/WebMvcConfig.kt b/src/main/kotlin/io/github/bayang/jelu/config/WebMvcConfig.kt
index 175aa091..b9ed0bed 100644
--- a/src/main/kotlin/io/github/bayang/jelu/config/WebMvcConfig.kt
+++ b/src/main/kotlin/io/github/bayang/jelu/config/WebMvcConfig.kt
@@ -9,14 +9,22 @@ import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
import java.util.concurrent.TimeUnit
+const val EXPORTS_PREFIX = "/exports"
+
@Configuration
class WebMvcConfig(private val properties: JeluProperties) : WebMvcConfigurer {
override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
+ // serve pictures
registry.addResourceHandler("/files/**")
.addResourceLocations(getExternalFilesFolderPath())
.setCacheControl(CacheControl.maxAge(7, TimeUnit.DAYS).cachePublic())
+ // serve export csv
+ registry.addResourceHandler("$EXPORTS_PREFIX/**")
+ .addResourceLocations(getExternalExportsFolderPath())
+ .setCacheControl(CacheControl.noCache())
+
registry.addResourceHandler("/assets/**")
.addResourceLocations("classpath:public/assets/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic())
@@ -55,6 +63,11 @@ class WebMvcConfig(private val properties: JeluProperties) : WebMvcConfigurer {
var suffix: String = if (properties.files.images.endsWith("/")) { "" } else { "/" }
return "file:" + properties.files.images + suffix
}
+
+ fun getExternalExportsFolderPath(): String {
+ var suffix: String = if (properties.files.imports.endsWith("/")) { "" } else { "/" }
+ return "file:" + properties.files.imports + suffix
+ }
}
@ControllerAdvice
class Customizer {
diff --git a/src/main/kotlin/io/github/bayang/jelu/controllers/ImportController.kt b/src/main/kotlin/io/github/bayang/jelu/controllers/ImportController.kt
index c8966d4c..c6d734f0 100644
--- a/src/main/kotlin/io/github/bayang/jelu/controllers/ImportController.kt
+++ b/src/main/kotlin/io/github/bayang/jelu/controllers/ImportController.kt
@@ -3,6 +3,7 @@ package io.github.bayang.jelu.controllers
import io.github.bayang.jelu.config.JeluProperties
import io.github.bayang.jelu.dto.ImportConfigurationDto
import io.github.bayang.jelu.dto.JeluUser
+import io.github.bayang.jelu.service.exports.CsvExportService
import io.github.bayang.jelu.service.imports.CsvImportService
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.responses.ApiResponse
@@ -18,6 +19,7 @@ import org.springframework.web.bind.annotation.RequestPart
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile
import java.io.File
+import java.util.Locale
import javax.validation.Valid
private val logger = KotlinLogging.logger {}
@@ -26,6 +28,7 @@ private val logger = KotlinLogging.logger {}
@RequestMapping("/api/v1")
class ImportController(
val csvImportService: CsvImportService,
+ val csvExportService: CsvExportService,
private val properties: JeluProperties,
) {
@@ -47,4 +50,15 @@ class ImportController(
csvImportService.import(destFile, (principal.principal as JeluUser).user.id.value, importConfig)
return ResponseEntity.status(HttpStatus.CREATED).build()
}
+
+ @ApiResponse(responseCode = "201", description = "Saved the export csv request")
+ @Operation(description = "Trigger a csv export")
+ @PostMapping(path = ["/exports"])
+ fun exportCsv(
+ principal: Authentication,
+ locale: Locale
+ ): ResponseEntity {
+ csvExportService.export((principal.principal as JeluUser).user, locale)
+ return ResponseEntity.status(HttpStatus.CREATED).build()
+ }
}
diff --git a/src/main/kotlin/io/github/bayang/jelu/controllers/ReadingEventsController.kt b/src/main/kotlin/io/github/bayang/jelu/controllers/ReadingEventsController.kt
index e3caa0e9..cfd8f8d7 100644
--- a/src/main/kotlin/io/github/bayang/jelu/controllers/ReadingEventsController.kt
+++ b/src/main/kotlin/io/github/bayang/jelu/controllers/ReadingEventsController.kt
@@ -42,8 +42,9 @@ class ReadingEventsController(
fun readingEvents(
@RequestParam(name = "eventTypes", required = false) eventTypes: List?,
@RequestParam(name = "userId", required = false) userId: UUID?,
+ @RequestParam(name = "bookId", required = false) bookId: UUID?,
@PageableDefault(page = 0, size = 20, direction = Sort.Direction.DESC, sort = ["modificationDate"]) @ParameterObject pageable: Pageable
- ): Page = repository.findAll(eventTypes, userId, pageable)
+ ): Page = repository.findAll(eventTypes, userId, bookId, pageable)
@GetMapping(path = ["/reading-events/me"])
fun myReadingEvents(
@@ -52,7 +53,7 @@ class ReadingEventsController(
principal: Authentication
): Page {
assertIsJeluUser(principal.principal)
- return repository.findAll(eventTypes, (principal.principal as JeluUser).user.id.value, pageable)
+ return repository.findAll(eventTypes, (principal.principal as JeluUser).user.id.value, null, pageable)
}
@PostMapping(path = ["/reading-events"])
diff --git a/src/main/kotlin/io/github/bayang/jelu/controllers/UserMessagesController.kt b/src/main/kotlin/io/github/bayang/jelu/controllers/UserMessagesController.kt
new file mode 100644
index 00000000..d6f3a51b
--- /dev/null
+++ b/src/main/kotlin/io/github/bayang/jelu/controllers/UserMessagesController.kt
@@ -0,0 +1,58 @@
+package io.github.bayang.jelu.controllers
+
+import io.github.bayang.jelu.config.JeluProperties
+import io.github.bayang.jelu.dao.MessageCategory
+import io.github.bayang.jelu.dto.CreateUserMessageDto
+import io.github.bayang.jelu.dto.JeluUser
+import io.github.bayang.jelu.dto.UpdateUserMessageDto
+import io.github.bayang.jelu.dto.UserMessageDto
+import io.github.bayang.jelu.service.UserMessageService
+import mu.KotlinLogging
+import org.springdoc.api.annotations.ParameterObject
+import org.springframework.data.domain.Page
+import org.springframework.data.domain.Pageable
+import org.springframework.data.domain.Sort
+import org.springframework.data.web.PageableDefault
+import org.springframework.security.core.Authentication
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.PathVariable
+import org.springframework.web.bind.annotation.PostMapping
+import org.springframework.web.bind.annotation.PutMapping
+import org.springframework.web.bind.annotation.RequestBody
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RequestParam
+import org.springframework.web.bind.annotation.RestController
+import java.util.UUID
+import javax.validation.Valid
+
+private val logger = KotlinLogging.logger {}
+
+@RestController
+@RequestMapping("/api/v1")
+class UserMessagesController(
+ private val userMessageService: UserMessageService,
+ private val properties: JeluProperties
+) {
+
+ @GetMapping(path = ["/user-messages"])
+ fun userMessages(
+ @RequestParam(name = "messageCategories", required = false) messageCategories: List?,
+ @RequestParam(name = "read", required = false) read: Boolean?,
+ @PageableDefault(page = 0, size = 20, direction = Sort.Direction.DESC, sort = ["modificationDate"]) @ParameterObject pageable: Pageable,
+ principal: Authentication
+ ): Page = userMessageService.find((principal.principal as JeluUser).user, read, messageCategories, pageable)
+
+ @PutMapping(path = ["/user-messages/{id}"])
+ fun updateMessage(@PathVariable("id") messageId: UUID, @RequestBody @Valid updateDto: UpdateUserMessageDto): UserMessageDto {
+ return userMessageService.update(messageId, updateDto)
+ }
+
+ @PostMapping(path = ["/user-messages"])
+ fun testMessages(
+ @RequestBody @Valid createUserMessageDto: CreateUserMessageDto,
+ principal: Authentication
+ ): UserMessageDto {
+ return userMessageService.save(createUserMessageDto, (principal.principal as JeluUser).user)
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/io/github/bayang/jelu/dao/ReadingEventRepository.kt b/src/main/kotlin/io/github/bayang/jelu/dao/ReadingEventRepository.kt
index e50d89a4..4cbe945f 100644
--- a/src/main/kotlin/io/github/bayang/jelu/dao/ReadingEventRepository.kt
+++ b/src/main/kotlin/io/github/bayang/jelu/dao/ReadingEventRepository.kt
@@ -26,6 +26,7 @@ class ReadingEventRepository {
fun findAll(
eventTypes: List?,
userId: UUID?,
+ bookId: UUID?,
pageable: Pageable
): Page {
val query = ReadingEventTable.join(UserBookTable, JoinType.LEFT)
@@ -36,6 +37,9 @@ class ReadingEventRepository {
if (userId != null) {
query.andWhere { UserBookTable.user eq userId }
}
+ if (bookId != null) {
+ query.andWhere { UserBookTable.book eq bookId }
+ }
val total = query.count()
query.limit(pageable.pageSize, pageable.offset)
val orders: Array, SortOrder>> = parseSorts(pageable.sort, Pair(ReadingEventTable.modificationDate, SortOrder.DESC_NULLS_LAST), ReadingEventTable)
diff --git a/src/main/kotlin/io/github/bayang/jelu/dao/UserMessageRepository.kt b/src/main/kotlin/io/github/bayang/jelu/dao/UserMessageRepository.kt
new file mode 100644
index 00000000..a7400102
--- /dev/null
+++ b/src/main/kotlin/io/github/bayang/jelu/dao/UserMessageRepository.kt
@@ -0,0 +1,84 @@
+package io.github.bayang.jelu.dao
+
+import io.github.bayang.jelu.dto.CreateUserMessageDto
+import io.github.bayang.jelu.dto.UpdateUserMessageDto
+import io.github.bayang.jelu.utils.nowInstant
+import mu.KotlinLogging
+import org.jetbrains.exposed.sql.Expression
+import org.jetbrains.exposed.sql.JoinType
+import org.jetbrains.exposed.sql.SortOrder
+import org.jetbrains.exposed.sql.andWhere
+import org.jetbrains.exposed.sql.select
+import org.springframework.data.domain.Page
+import org.springframework.data.domain.PageImpl
+import org.springframework.data.domain.Pageable
+import org.springframework.stereotype.Repository
+import java.time.Instant
+import java.util.UUID
+
+private val logger = KotlinLogging.logger {}
+
+@Repository
+class UserMessageRepository {
+
+ fun save(createUserMessageDto: CreateUserMessageDto, user: User): UserMessage {
+ val instant: Instant = nowInstant()
+ return UserMessage.new {
+ this.creationDate = instant
+ this.modificationDate = instant
+ this.user = user
+ this.message = createUserMessageDto.message
+ this.link = createUserMessageDto.link
+ this.messageCategory = createUserMessageDto.category
+ this.read = false
+ }
+ }
+
+ fun find(
+ user: User,
+ read: Boolean?,
+ messageCategories: List?,
+ pageable: Pageable
+ ): Page {
+ val query = UserMessageTable.join(UserTable, JoinType.LEFT)
+ .select { UserMessageTable.user eq user.id }
+ if (read != null) {
+ query.andWhere { UserMessageTable.read eq read }
+ }
+ if (messageCategories != null && messageCategories.isNotEmpty()) {
+ query.andWhere { UserMessageTable.messageCategory inList messageCategories }
+ }
+ val total = query.count()
+ query.limit(pageable.pageSize, pageable.offset)
+ val orders: Array, SortOrder>> = parseSorts(pageable.sort, Pair(UserMessageTable.modificationDate, SortOrder.DESC_NULLS_LAST), UserMessageTable)
+ query.orderBy(*orders)
+ val res = UserMessage.wrapRows(query).toList()
+ return PageImpl(
+ res,
+ pageable,
+ total
+ )
+ }
+
+ fun update(userMessageId: UUID, updateDto: UpdateUserMessageDto): UserMessage {
+ return UserMessage[userMessageId].apply {
+ this.modificationDate = nowInstant()
+ if (updateDto.read != null) {
+ this.read = updateDto.read
+ }
+ if (! updateDto.message.isNullOrEmpty()) {
+ this.message = updateDto.message
+ }
+ if (! updateDto.link.isNullOrEmpty()) {
+ this.link = updateDto.link
+ }
+ if (updateDto.category != null) {
+ this.messageCategory = updateDto.category
+ }
+ }
+ }
+
+ fun delete(userMessageId: UUID) {
+ UserMessage[userMessageId].delete()
+ }
+}
diff --git a/src/main/kotlin/io/github/bayang/jelu/dao/UserMessageTable.kt b/src/main/kotlin/io/github/bayang/jelu/dao/UserMessageTable.kt
new file mode 100644
index 00000000..e773866d
--- /dev/null
+++ b/src/main/kotlin/io/github/bayang/jelu/dao/UserMessageTable.kt
@@ -0,0 +1,47 @@
+package io.github.bayang.jelu.dao
+
+import io.github.bayang.jelu.dto.UserMessageDto
+import org.jetbrains.exposed.dao.UUIDEntity
+import org.jetbrains.exposed.dao.UUIDEntityClass
+import org.jetbrains.exposed.dao.id.EntityID
+import org.jetbrains.exposed.dao.id.UUIDTable
+import org.jetbrains.exposed.sql.Column
+import org.jetbrains.exposed.sql.ReferenceOption
+import org.jetbrains.exposed.sql.javatime.timestamp
+import java.util.UUID
+
+object UserMessageTable : UUIDTable("user_message") {
+ val creationDate = timestamp("creation_date")
+ val modificationDate = timestamp("modification_date")
+ val user = reference("user", UserTable, onDelete = ReferenceOption.CASCADE)
+ val messageCategory = enumerationByName("category", 200, MessageCategory::class)
+ val message: Column = varchar("message", 50000).nullable()
+ val link: Column = varchar("link", 50000).nullable()
+ val read: Column = bool("read")
+}
+class UserMessage(id: EntityID) : UUIDEntity(id) {
+ companion object : UUIDEntityClass(UserMessageTable)
+ var creationDate by UserMessageTable.creationDate
+ var modificationDate by UserMessageTable.modificationDate
+ var user by User referencedOn UserMessageTable.user
+ var messageCategory by UserMessageTable.messageCategory
+ var message by UserMessageTable.message
+ var link by UserMessageTable.link
+ var read by UserMessageTable.read
+
+ fun toUserMessageDto(): UserMessageDto = UserMessageDto(
+ id = this.id.value,
+ message = this.message,
+ creationDate = this.creationDate,
+ modificationDate = this.modificationDate,
+ link = this.link,
+ read = this.read,
+ category = this.messageCategory
+ )
+}
+enum class MessageCategory {
+ SUCCESS,
+ INFO,
+ WARNING,
+ ERROR
+}
diff --git a/src/main/kotlin/io/github/bayang/jelu/dto/UserMessageDto.kt b/src/main/kotlin/io/github/bayang/jelu/dto/UserMessageDto.kt
new file mode 100644
index 00000000..0ac92ae6
--- /dev/null
+++ b/src/main/kotlin/io/github/bayang/jelu/dto/UserMessageDto.kt
@@ -0,0 +1,28 @@
+package io.github.bayang.jelu.dto
+
+import io.github.bayang.jelu.dao.MessageCategory
+import java.time.Instant
+import java.util.UUID
+
+data class CreateUserMessageDto(
+ val message: String?,
+ val link: String?,
+ val category: MessageCategory,
+)
+
+data class UpdateUserMessageDto(
+ val message: String?,
+ val link: String?,
+ val category: MessageCategory?,
+ val read: Boolean?,
+)
+
+data class UserMessageDto(
+ val id: UUID?,
+ val message: String?,
+ val link: String?,
+ val category: MessageCategory,
+ val read: Boolean,
+ val creationDate: Instant?,
+ val modificationDate: Instant?,
+)
diff --git a/src/main/kotlin/io/github/bayang/jelu/service/ReadingEventService.kt b/src/main/kotlin/io/github/bayang/jelu/service/ReadingEventService.kt
index 10d72b85..5b06db0e 100644
--- a/src/main/kotlin/io/github/bayang/jelu/service/ReadingEventService.kt
+++ b/src/main/kotlin/io/github/bayang/jelu/service/ReadingEventService.kt
@@ -15,8 +15,8 @@ import java.util.UUID
class ReadingEventService(private val readingEventRepository: ReadingEventRepository) {
@Transactional
- fun findAll(eventTypes: List?, userId: UUID?, pageable: Pageable) =
- readingEventRepository.findAll(eventTypes, userId, pageable).map { it.toReadingEventDto() }
+ fun findAll(eventTypes: List?, userId: UUID?, bookId: UUID?, pageable: Pageable) =
+ readingEventRepository.findAll(eventTypes, userId, bookId, pageable).map { it.toReadingEventDto() }
@Transactional
fun save(createReadingEventDto: CreateReadingEventDto, user: User): ReadingEventDto {
diff --git a/src/main/kotlin/io/github/bayang/jelu/service/UserMessageService.kt b/src/main/kotlin/io/github/bayang/jelu/service/UserMessageService.kt
new file mode 100644
index 00000000..d4774749
--- /dev/null
+++ b/src/main/kotlin/io/github/bayang/jelu/service/UserMessageService.kt
@@ -0,0 +1,42 @@
+package io.github.bayang.jelu.service
+
+import io.github.bayang.jelu.dao.MessageCategory
+import io.github.bayang.jelu.dao.User
+import io.github.bayang.jelu.dao.UserMessageRepository
+import io.github.bayang.jelu.dto.CreateUserMessageDto
+import io.github.bayang.jelu.dto.UpdateUserMessageDto
+import io.github.bayang.jelu.dto.UserMessageDto
+import org.springframework.data.domain.Page
+import org.springframework.data.domain.Pageable
+import org.springframework.stereotype.Component
+import org.springframework.transaction.annotation.Transactional
+import java.util.UUID
+
+@Component
+class UserMessageService(private val userMessageRepository: UserMessageRepository) {
+
+ @Transactional
+ fun save(createUserMessageDto: CreateUserMessageDto, user: User): UserMessageDto {
+ return userMessageRepository.save(createUserMessageDto, user).toUserMessageDto()
+ }
+
+ @Transactional
+ fun find(
+ user: User,
+ read: Boolean?,
+ messageCategories: List?,
+ pageable: Pageable
+ ): Page {
+ return userMessageRepository.find(user, read, messageCategories, pageable).map { it.toUserMessageDto() }
+ }
+
+ @Transactional
+ fun update(userMessageId: UUID, updateDto: UpdateUserMessageDto): UserMessageDto {
+ return userMessageRepository.update(userMessageId, updateDto).toUserMessageDto()
+ }
+
+ @Transactional
+ fun delete(userMessageId: UUID) {
+ userMessageRepository.delete(userMessageId)
+ }
+}
diff --git a/src/main/kotlin/io/github/bayang/jelu/service/exports/CsvExportService.kt b/src/main/kotlin/io/github/bayang/jelu/service/exports/CsvExportService.kt
new file mode 100644
index 00000000..4752addf
--- /dev/null
+++ b/src/main/kotlin/io/github/bayang/jelu/service/exports/CsvExportService.kt
@@ -0,0 +1,204 @@
+package io.github.bayang.jelu.service.exports
+
+import io.github.bayang.jelu.config.EXPORTS_PREFIX
+import io.github.bayang.jelu.config.JeluProperties
+import io.github.bayang.jelu.dao.MessageCategory
+import io.github.bayang.jelu.dao.ReadingEventType
+import io.github.bayang.jelu.dao.User
+import io.github.bayang.jelu.dto.CreateUserMessageDto
+import io.github.bayang.jelu.dto.UserBookWithoutEventsAndUserDto
+import io.github.bayang.jelu.service.BookService
+import io.github.bayang.jelu.service.ReadingEventService
+import io.github.bayang.jelu.service.UserMessageService
+import io.github.bayang.jelu.service.imports.CURRENTLY_READING
+import io.github.bayang.jelu.service.imports.TO_READ
+import io.github.bayang.jelu.service.imports.goodreadsDateFormatter
+import mu.KotlinLogging
+import org.apache.commons.csv.CSVFormat
+import org.apache.commons.csv.CSVPrinter
+import org.apache.commons.csv.QuoteMode
+import org.springframework.data.domain.Page
+import org.springframework.data.domain.PageRequest
+import org.springframework.data.domain.Pageable
+import org.springframework.stereotype.Service
+import java.io.BufferedWriter
+import java.io.File
+import java.io.FileWriter
+import java.time.Instant
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.OffsetDateTime
+import java.time.ZoneId
+import java.time.format.DateTimeFormatter
+import java.time.format.FormatStyle
+import java.util.Locale
+import java.util.UUID
+import java.util.stream.Collectors
+
+private val logger = KotlinLogging.logger {}
+
+@Service
+class CsvExportService(
+ private val bookService: BookService,
+ private val properties: JeluProperties,
+ private val readingEventService: ReadingEventService,
+ private val userMessageService: UserMessageService
+) {
+
+ /**
+ * Columns from Title to Bookshelves are used for Goodreads reimport.
+ * see https://help.goodreads.com/s/article/How-to-import-my-books-from-other-cataloging-services-1553870934585
+ * Other columns are generic data that could be used elsewhere.
+ */
+ fun export(user: User, locale: Locale) {
+ val format: CSVFormat = CSVFormat.Builder.create(CSVFormat.EXCEL).setQuoteMode(QuoteMode.MINIMAL).build()
+ val start = System.currentTimeMillis()
+ val userId = user.id.value
+ logger.debug { "beginning csv export" }
+ val nowString: String = OffsetDateTime.now(ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss"))
+ val nowPretty: String = try {
+ LocalDateTime.now(ZoneId.systemDefault()).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.MEDIUM).withLocale(locale))
+ } catch (e: Exception) {
+ nowString
+ }
+ val destFileName = "jelu-export-${user.login}-$nowString.csv"
+ try {
+ userMessageService.save(
+ CreateUserMessageDto(
+ "Export started at $nowPretty",
+ null,
+ MessageCategory.INFO
+ ),
+ user
+ )
+ } catch (e: Exception) {
+ logger.error(e) { "failed to save message for $destFileName export" }
+ }
+ var currentPage = 0
+ val pageSize = 100
+ var books: Page
+ var count: Long = 0
+ val destFile = File(properties.files.imports, destFileName)
+ logger.debug { "target export file at ${destFile.absolutePath}" }
+ try {
+ CSVPrinter(BufferedWriter(FileWriter(destFile)), format).use { printer ->
+ printer.printRecord("Title", "Author", "ISBN", "Publisher", "Date Read", "Shelves", "Bookshelves", "read_dates", "tags", "authors", "isbn10", "isbn13", "owned")
+ do {
+ books = bookService.findUserBookByCriteria(userId, null, null, PageRequest.of(currentPage, pageSize))
+ currentPage ++
+ logger.debug { "current $currentPage" }
+ count += books.content.size
+ processBooks(books, printer, userId)
+ } while (! books.isEmpty)
+ }
+ } catch (ex: Exception) {
+ logger.error(ex) { "Error processing block of entries for export at page $currentPage" }
+ }
+ val end = System.currentTimeMillis()
+ val msg = "$count books have been processed in ${end - start} milliseconds"
+ logger.debug { msg }
+ try {
+ userMessageService.save(
+ CreateUserMessageDto(
+ "Export completed : $msg",
+ "$EXPORTS_PREFIX/$destFileName",
+ MessageCategory.SUCCESS
+ ),
+ user
+ )
+ } catch (e: Exception) {
+ logger.error(e) { "failed to save message for $destFileName export" }
+ }
+ }
+
+ private fun processBooks(books: Page, printer: CSVPrinter, userId: UUID) {
+ books.content.forEach {
+ logger.debug { it.book.title }
+ printer.printRecord(
+ it.book.title,
+ if (it.book.authors.isNullOrEmpty()) "" else it.book.authors[0].name,
+ isbn(it),
+ if (it.book.publisher.isNullOrBlank()) "" else it.book.publisher,
+ dateRead(it),
+ shelves(it),
+ bookShelves(it),
+ readDates(it, userId),
+ tags(it),
+ authors(it),
+ if (it.book.isbn10.isNullOrBlank()) "" else it.book.isbn10,
+ if (it.book.isbn13.isNullOrBlank()) "" else it.book.isbn13,
+ if (it.owned == true) "true" else "",
+ )
+ }
+ }
+
+ fun isbn(userbook: UserBookWithoutEventsAndUserDto): String {
+ return if (userbook.book.isbn13 != null && userbook.book.isbn13.isNotBlank()) {
+ userbook.book.isbn13
+ } else if (userbook.book.isbn10 != null && userbook.book.isbn10.isNotBlank()) {
+ userbook.book.isbn10
+ } else {
+ ""
+ }
+ }
+
+ fun dateRead(userbook: UserBookWithoutEventsAndUserDto): String {
+ return if (userbook.lastReadingEventDate != null) {
+ toDateString(userbook.lastReadingEventDate)
+ } else {
+ ""
+ }
+ }
+
+ fun toDateString(instant: Instant?): String {
+ return if (instant != null) {
+ LocalDate.ofInstant(instant, ZoneId.systemDefault()).format(
+ goodreadsDateFormatter
+ )
+ } else {
+ ""
+ }
+ }
+
+ fun shelves(userbook: UserBookWithoutEventsAndUserDto): String {
+ return if (userbook.lastReadingEvent == ReadingEventType.CURRENTLY_READING) {
+ CURRENTLY_READING
+ } else if (userbook.toRead == true) {
+ TO_READ
+ } else {
+ ""
+ }
+ }
+
+ fun bookShelves(userbook: UserBookWithoutEventsAndUserDto): String {
+ return if (! userbook.book.tags.isNullOrEmpty()) {
+ userbook.book.tags.stream().map { it.name.replace(" ", "", true) }.collect(Collectors.joining(" "))
+ } else {
+ ""
+ }
+ }
+
+ fun readDates(userbook: UserBookWithoutEventsAndUserDto, userId: UUID): String {
+ val reads = readingEventService.findAll(listOf(ReadingEventType.FINISHED), userId, userbook.book.id, Pageable.ofSize(100))
+ if (! reads.isEmpty) {
+ return reads.content.stream().map { toDateString(it.modificationDate) }.collect(Collectors.joining(","))
+ }
+ return ""
+ }
+
+ fun tags(userbook: UserBookWithoutEventsAndUserDto): String {
+ return if (! userbook.book.tags.isNullOrEmpty()) {
+ userbook.book.tags.stream().map { it.name }.collect(Collectors.joining(","))
+ } else {
+ ""
+ }
+ }
+
+ fun authors(userbook: UserBookWithoutEventsAndUserDto): String {
+ return if (! userbook.book.authors.isNullOrEmpty()) {
+ userbook.book.authors.stream().map { it.name }.collect(Collectors.joining(","))
+ } else {
+ return ""
+ }
+ }
+}
diff --git a/src/main/kotlin/io/github/bayang/jelu/service/imports/CsvImportService.kt b/src/main/kotlin/io/github/bayang/jelu/service/imports/CsvImportService.kt
index 1513b893..66b451ae 100644
--- a/src/main/kotlin/io/github/bayang/jelu/service/imports/CsvImportService.kt
+++ b/src/main/kotlin/io/github/bayang/jelu/service/imports/CsvImportService.kt
@@ -3,6 +3,7 @@ package io.github.bayang.jelu.service.imports
import io.github.bayang.jelu.config.JeluProperties
import io.github.bayang.jelu.dao.ImportEntity
import io.github.bayang.jelu.dao.ImportSource
+import io.github.bayang.jelu.dao.MessageCategory
import io.github.bayang.jelu.dao.ProcessingStatus
import io.github.bayang.jelu.dao.ReadingEventType
import io.github.bayang.jelu.dto.AuthorDto
@@ -10,6 +11,7 @@ import io.github.bayang.jelu.dto.BookCreateDto
import io.github.bayang.jelu.dto.BookDto
import io.github.bayang.jelu.dto.CreateReadingEventDto
import io.github.bayang.jelu.dto.CreateUserBookDto
+import io.github.bayang.jelu.dto.CreateUserMessageDto
import io.github.bayang.jelu.dto.ImportConfigurationDto
import io.github.bayang.jelu.dto.ImportDto
import io.github.bayang.jelu.dto.LibraryFilter
@@ -20,6 +22,7 @@ import io.github.bayang.jelu.dto.UserBookUpdateDto
import io.github.bayang.jelu.service.BookService
import io.github.bayang.jelu.service.ImportService
import io.github.bayang.jelu.service.ReadingEventService
+import io.github.bayang.jelu.service.UserMessageService
import io.github.bayang.jelu.service.UserService
import io.github.bayang.jelu.service.metadata.FetchMetadataService
import io.github.bayang.jelu.utils.toInstant
@@ -34,6 +37,7 @@ import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.io.File
import java.time.LocalDate
+import java.time.OffsetDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.UUID
@@ -55,6 +59,11 @@ const val CURRENTLY_READING = "currently-reading"
*/
const val DROPPED = "did-not-finish"
+/**
+ * Works for goodreads and storygraph
+ */
+val goodreadsDateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd")
+
@Service
class CsvImportService(
private val properties: JeluProperties,
@@ -62,18 +71,31 @@ class CsvImportService(
private val fetchMetadataService: FetchMetadataService,
private val bookService: BookService,
private val userService: UserService,
- private val readingEventService: ReadingEventService
+ private val readingEventService: ReadingEventService,
+ private val userMessageService: UserMessageService
) {
- /**
- * Works for goodreads and storygraph
- */
- val goodreadsDateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd")
+ // /**
+ // * Works for goodreads and storygraph
+ // */
+ // val goodreadsDateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd")
// maybe later : use coroutines ?
@Async
fun import(file: File, user: UUID, importConfig: ImportConfigurationDto) {
val start = System.currentTimeMillis()
+ val userEntity = userService.findUserEntityById(user)
+ val nowString: String = OffsetDateTime.now(ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss"))
+ try {
+ userMessageService.save(
+ CreateUserMessageDto(
+ "Import started at $nowString",
+ null,
+ MessageCategory.INFO
+ ), userEntity)
+ } catch (e: Exception) {
+ logger.error(e) { "failed to save message for ${file.absolutePath} import" }
+ }
// put file content in db
parse(file, user, importConfig)
var end = System.currentTimeMillis()
@@ -88,6 +110,16 @@ class CsvImportService(
end = System.currentTimeMillis()
deltaInSec = (end - start) / 1000
logger.info { "Import for ${file.absolutePath} ended after : $deltaInSec seconds, with $success imports and $failures failures" }
+ try {
+ userMessageService.save(
+ CreateUserMessageDto(
+ "Import for ${file.absolutePath} ended after : $deltaInSec seconds, with $success imports and $failures failures",
+ null,
+ MessageCategory.SUCCESS
+ ), userEntity)
+ } catch (e: Exception) {
+ logger.error(e) { "failed to save message for ${file.absolutePath} import" }
+ }
}
fun importFromDb(user: UUID, importConfig: ImportConfigurationDto): Pair {
diff --git a/src/main/resources/liquibase.xml b/src/main/resources/liquibase.xml
index 3c1ae910..f82f5506 100644
--- a/src/main/resources/liquibase.xml
+++ b/src/main/resources/liquibase.xml
@@ -319,5 +319,36 @@
ALTER TABLE author ADD instagram_page varchar(5000);
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/test/kotlin/io/github/bayang/jelu/service/BookServiceTest.kt b/src/test/kotlin/io/github/bayang/jelu/service/BookServiceTest.kt
index 2c35578e..6167a2b5 100644
--- a/src/test/kotlin/io/github/bayang/jelu/service/BookServiceTest.kt
+++ b/src/test/kotlin/io/github/bayang/jelu/service/BookServiceTest.kt
@@ -61,7 +61,7 @@ class BookServiceTest(
tempDir.listFiles().forEach {
it.deleteRecursively()
}
- readingEventService.findAll(null, null, Pageable.ofSize(30)).content.forEach {
+ readingEventService.findAll(null, null, null, Pageable.ofSize(30)).content.forEach {
readingEventService.deleteReadingEventById(it.id!!)
}
bookService.findUserBookByCriteria(user().id.value, null, null, Pageable.ofSize(30))
@@ -244,7 +244,7 @@ class BookServiceTest(
Assertions.assertNull(saved.book.image)
Assertions.assertNull(saved.lastReadingEvent)
Assertions.assertNull(saved.lastReadingEventDate)
- Assertions.assertTrue(readingEventService.findAll(null, null, Pageable.ofSize(30)).isEmpty)
+ Assertions.assertTrue(readingEventService.findAll(null, null, null, Pageable.ofSize(30)).isEmpty)
Assertions.assertEquals(0, File(jeluProperties.files.images).listFiles().size)
}
@@ -272,7 +272,7 @@ class BookServiceTest(
Assertions.assertTrue(saved.book.image!!.contains(slugify(saved.book.title), true))
Assertions.assertEquals(ReadingEventType.CURRENTLY_READING, saved.lastReadingEvent)
Assertions.assertNotNull(saved.lastReadingEventDate)
- Assertions.assertEquals(1, readingEventService.findAll(null, null, Pageable.ofSize(30)).totalElements)
+ Assertions.assertEquals(1, readingEventService.findAll(null, null, null, Pageable.ofSize(30)).totalElements)
Assertions.assertEquals(1, File(jeluProperties.files.images).listFiles().size)
}
@@ -307,7 +307,7 @@ class BookServiceTest(
Assertions.assertNotNull(saved.book.modificationDate)
Assertions.assertNull(saved.lastReadingEvent)
Assertions.assertNull(saved.lastReadingEventDate)
- Assertions.assertTrue(readingEventService.findAll(null, null, Pageable.ofSize(30)).isEmpty)
+ Assertions.assertTrue(readingEventService.findAll(null, null, null, Pageable.ofSize(30)).isEmpty)
Assertions.assertNull(saved.book.image)
Assertions.assertEquals(0, File(jeluProperties.files.images).listFiles().size)
}
@@ -345,7 +345,7 @@ class BookServiceTest(
Assertions.assertNotNull(saved.book.modificationDate)
Assertions.assertNull(saved.lastReadingEvent)
Assertions.assertNull(saved.lastReadingEventDate)
- Assertions.assertTrue(readingEventService.findAll(null, null, Pageable.ofSize(30)).isEmpty)
+ Assertions.assertTrue(readingEventService.findAll(null, null, null, Pageable.ofSize(30)).isEmpty)
Assertions.assertTrue(saved.book.image!!.contains(slugify(savedBook.title), true))
Assertions.assertEquals(1, File(jeluProperties.files.images).listFiles().size)
}
@@ -384,7 +384,7 @@ class BookServiceTest(
Assertions.assertNotNull(saved.book.modificationDate)
Assertions.assertNull(saved.lastReadingEvent)
Assertions.assertNull(saved.lastReadingEventDate)
- Assertions.assertTrue(readingEventService.findAll(null, null, Pageable.ofSize(30)).isEmpty)
+ Assertions.assertTrue(readingEventService.findAll(null, null, null, Pageable.ofSize(30)).isEmpty)
Assertions.assertTrue(saved.book.image!!.contains(slugify(modified.title), true))
Assertions.assertEquals(1, File(jeluProperties.files.images).listFiles().size)
}
@@ -414,7 +414,7 @@ class BookServiceTest(
Assertions.assertTrue(saved.book.image!!.contains(slugify(saved.book.title), true))
Assertions.assertEquals(ReadingEventType.CURRENTLY_READING, saved.lastReadingEvent)
Assertions.assertNotNull(saved.lastReadingEventDate)
- Assertions.assertEquals(1, readingEventService.findAll(null, null, Pageable.ofSize(30)).totalElements)
+ Assertions.assertEquals(1, readingEventService.findAll(null, null, null, Pageable.ofSize(30)).totalElements)
Assertions.assertEquals(1, File(jeluProperties.files.images).listFiles().size)
val updater = UserBookUpdateDto(
@@ -446,7 +446,7 @@ class BookServiceTest(
Assertions.assertEquals(ReadingEventType.FINISHED, updated.lastReadingEvent)
Assertions.assertNotNull(updated.lastReadingEventDate)
Assertions.assertEquals(1, updated.readingEvents?.size)
- Assertions.assertEquals(1, readingEventService.findAll(null, null, Pageable.ofSize(30)).totalElements)
+ Assertions.assertEquals(1, readingEventService.findAll(null, null, null, Pageable.ofSize(30)).totalElements)
Assertions.assertEquals(1, File(jeluProperties.files.images).listFiles().size)
}
@@ -475,7 +475,7 @@ class BookServiceTest(
Assertions.assertTrue(saved.book.image!!.contains(slugify(saved.book.title), true))
Assertions.assertEquals(ReadingEventType.FINISHED, saved.lastReadingEvent)
Assertions.assertNotNull(saved.lastReadingEventDate)
- Assertions.assertEquals(1, readingEventService.findAll(null, null, Pageable.ofSize(30)).totalElements)
+ Assertions.assertEquals(1, readingEventService.findAll(null, null, null, Pageable.ofSize(30)).totalElements)
Assertions.assertEquals(1, File(jeluProperties.files.images).listFiles().size)
val updater = UserBookUpdateDto(
@@ -507,7 +507,7 @@ class BookServiceTest(
Assertions.assertEquals(ReadingEventType.DROPPED, updated.lastReadingEvent)
Assertions.assertNotNull(updated.lastReadingEventDate)
Assertions.assertEquals(2, updated.readingEvents?.size)
- Assertions.assertEquals(2, readingEventService.findAll(null, null, Pageable.ofSize(30)).totalElements)
+ Assertions.assertEquals(2, readingEventService.findAll(null, null, null, Pageable.ofSize(30)).totalElements)
Assertions.assertEquals(1, File(jeluProperties.files.images).listFiles().size)
}
@@ -536,7 +536,7 @@ class BookServiceTest(
Assertions.assertTrue(saved.book.image!!.contains(slugify(saved.book.title), true))
Assertions.assertEquals(ReadingEventType.FINISHED, saved.lastReadingEvent)
Assertions.assertNotNull(saved.lastReadingEventDate)
- Assertions.assertEquals(1, readingEventService.findAll(null, null, Pageable.ofSize(30)).totalElements)
+ Assertions.assertEquals(1, readingEventService.findAll(null, null, null, Pageable.ofSize(30)).totalElements)
Assertions.assertEquals(1, File(jeluProperties.files.images).listFiles().size)
val updater = UserBookUpdateDto(
@@ -569,7 +569,7 @@ class BookServiceTest(
Assertions.assertEquals(ReadingEventType.DROPPED, updated.lastReadingEvent)
Assertions.assertNotNull(updated.lastReadingEventDate)
Assertions.assertEquals(2, updated.readingEvents?.size)
- Assertions.assertEquals(2, readingEventService.findAll(null, null, Pageable.ofSize(30)).totalElements)
+ Assertions.assertEquals(2, readingEventService.findAll(null, null, null, Pageable.ofSize(30)).totalElements)
Assertions.assertEquals(1, File(jeluProperties.files.images).listFiles().size)
}
@@ -597,12 +597,12 @@ class BookServiceTest(
Assertions.assertNotNull(saved2)
var nb = bookService.findUserBookByCriteria(user().id.value, null, null, Pageable.ofSize(30)).totalElements
Assertions.assertEquals(2, nb)
- var eventsNb = readingEventService.findAll(null, null, Pageable.ofSize(30)).totalElements
+ var eventsNb = readingEventService.findAll(null, null, null, Pageable.ofSize(30)).totalElements
Assertions.assertEquals(2, eventsNb)
bookService.deleteUserBookById(saved1.id!!)
nb = bookService.findUserBookByCriteria(user().id.value, null, null, Pageable.ofSize(30)).totalElements
Assertions.assertEquals(1, nb)
- eventsNb = readingEventService.findAll(null, null, Pageable.ofSize(30)).totalElements
+ eventsNb = readingEventService.findAll(null, null, null, Pageable.ofSize(30)).totalElements
Assertions.assertEquals(1, eventsNb)
}
@@ -630,7 +630,7 @@ class BookServiceTest(
Assertions.assertNotNull(saved2)
var nb = bookService.findUserBookByCriteria(user().id.value, null, null, Pageable.ofSize(30)).totalElements
Assertions.assertEquals(2, nb)
- var eventsNb = readingEventService.findAll(null, null, Pageable.ofSize(30)).totalElements
+ var eventsNb = readingEventService.findAll(null, null, null, Pageable.ofSize(30)).totalElements
Assertions.assertEquals(2, eventsNb)
var authorsNb = bookService.findAllAuthors(null, Pageable.ofSize(30)).totalElements
Assertions.assertEquals(1, authorsNb)
@@ -642,7 +642,7 @@ class BookServiceTest(
Assertions.assertEquals(0, nb)
authorsNb = bookService.findAllAuthors(null, Pageable.ofSize(30)).totalElements
Assertions.assertEquals(1, authorsNb)
- eventsNb = readingEventService.findAll(null, null, Pageable.ofSize(30)).totalElements
+ eventsNb = readingEventService.findAll(null, null, null, Pageable.ofSize(30)).totalElements
Assertions.assertEquals(0, eventsNb)
tagsNb = bookService.findAllTags(null, Pageable.ofSize(30)).totalElements
Assertions.assertEquals(2, tagsNb)
diff --git a/src/test/kotlin/io/github/bayang/jelu/service/ReadingEventServiceTest.kt b/src/test/kotlin/io/github/bayang/jelu/service/ReadingEventServiceTest.kt
index eba181ec..4a7b41a9 100644
--- a/src/test/kotlin/io/github/bayang/jelu/service/ReadingEventServiceTest.kt
+++ b/src/test/kotlin/io/github/bayang/jelu/service/ReadingEventServiceTest.kt
@@ -58,7 +58,7 @@ class ReadingEventServiceTest(
tempDir.listFiles().forEach {
it.deleteRecursively()
}
- readingEventService.findAll(null, null, Pageable.ofSize(30)).content.forEach {
+ readingEventService.findAll(null, null, null, Pageable.ofSize(30)).content.forEach {
readingEventService.deleteReadingEventById(it.id!!)
}
bookService.findUserBookByCriteria(user().id.value, null, null, Pageable.ofSize(30))
@@ -94,7 +94,7 @@ class ReadingEventServiceTest(
Assertions.assertNotNull(saved.book.modificationDate)
Assertions.assertEquals(ReadingEventType.CURRENTLY_READING, saved.lastReadingEvent)
Assertions.assertNotNull(saved.lastReadingEventDate)
- Assertions.assertEquals(1, readingEventService.findAll(null, null, Pageable.ofSize(30)).totalElements)
+ Assertions.assertEquals(1, readingEventService.findAll(null, null, null, Pageable.ofSize(30)).totalElements)
Assertions.assertEquals(0, File(jeluProperties.files.images).listFiles().size)
val updater = UserBookUpdateDto(
@@ -125,7 +125,7 @@ class ReadingEventServiceTest(
Assertions.assertEquals(ReadingEventType.DROPPED, updated.lastReadingEvent)
Assertions.assertNotNull(updated.lastReadingEventDate)
Assertions.assertEquals(1, updated.readingEvents?.size)
- Assertions.assertEquals(1, readingEventService.findAll(null, null, Pageable.ofSize(30)).totalElements)
+ Assertions.assertEquals(1, readingEventService.findAll(null, null, null, Pageable.ofSize(30)).totalElements)
Assertions.assertEquals(0, File(jeluProperties.files.images).listFiles().size)
}
@@ -152,7 +152,7 @@ class ReadingEventServiceTest(
Assertions.assertNotNull(saved.book.modificationDate)
Assertions.assertEquals(ReadingEventType.FINISHED, saved.lastReadingEvent)
Assertions.assertNotNull(saved.lastReadingEventDate)
- Assertions.assertEquals(1, readingEventService.findAll(null, null, Pageable.ofSize(30)).totalElements)
+ Assertions.assertEquals(1, readingEventService.findAll(null, null, null, Pageable.ofSize(30)).totalElements)
Assertions.assertEquals(0, File(jeluProperties.files.images).listFiles().size)
val updater = UserBookUpdateDto(
@@ -183,7 +183,7 @@ class ReadingEventServiceTest(
Assertions.assertEquals(ReadingEventType.DROPPED, updated.lastReadingEvent)
Assertions.assertNotNull(updated.lastReadingEventDate)
Assertions.assertEquals(2, updated.readingEvents?.size)
- Assertions.assertEquals(2, readingEventService.findAll(null, null, Pageable.ofSize(30)).totalElements)
+ Assertions.assertEquals(2, readingEventService.findAll(null, null, null, Pageable.ofSize(30)).totalElements)
Assertions.assertEquals(0, File(jeluProperties.files.images).listFiles().size)
}
@@ -210,7 +210,7 @@ class ReadingEventServiceTest(
Assertions.assertNotNull(saved.book.modificationDate)
Assertions.assertEquals(ReadingEventType.FINISHED, saved.lastReadingEvent)
Assertions.assertNotNull(saved.lastReadingEventDate)
- Assertions.assertEquals(1, readingEventService.findAll(null, null, Pageable.ofSize(30)).totalElements)
+ Assertions.assertEquals(1, readingEventService.findAll(null, null, null, Pageable.ofSize(30)).totalElements)
Assertions.assertEquals(0, File(jeluProperties.files.images).listFiles().size)
val thirtyDaysBefore = nowInstant().minus(30, ChronoUnit.DAYS)
@@ -228,11 +228,11 @@ class ReadingEventServiceTest(
Assertions.assertEquals(ReadingEventType.FINISHED, userbook.lastReadingEvent)
Assertions.assertTrue(userbook.lastReadingEventDate?.isAfter(nowInstant().minus(2, ChronoUnit.MINUTES))!!)
- Assertions.assertEquals(2, readingEventService.findAll(null, null, Pageable.ofSize(30)).totalElements)
+ Assertions.assertEquals(2, readingEventService.findAll(null, null, null, Pageable.ofSize(30)).totalElements)
Assertions.assertEquals(0, File(jeluProperties.files.images).listFiles().size)
readingEventService.deleteReadingEventById(newEvent.id!!)
- Assertions.assertEquals(1, readingEventService.findAll(null, null, Pageable.ofSize(30)).totalElements)
+ Assertions.assertEquals(1, readingEventService.findAll(null, null, null, Pageable.ofSize(30)).totalElements)
userbook = bookService.findUserBookById(saved.id!!)
Assertions.assertEquals(ReadingEventType.FINISHED, userbook.lastReadingEvent)
Assertions.assertTrue(userbook.lastReadingEventDate?.isAfter(nowInstant().minus(2, ChronoUnit.MINUTES))!!)
@@ -261,7 +261,7 @@ class ReadingEventServiceTest(
Assertions.assertNotNull(saved.book.modificationDate)
Assertions.assertEquals(ReadingEventType.FINISHED, saved.lastReadingEvent)
Assertions.assertNotNull(saved.lastReadingEventDate)
- Assertions.assertEquals(1, readingEventService.findAll(null, null, Pageable.ofSize(30)).totalElements)
+ Assertions.assertEquals(1, readingEventService.findAll(null, null, null, Pageable.ofSize(30)).totalElements)
Assertions.assertEquals(0, File(jeluProperties.files.images).listFiles().size)
val thirtyDaysAfter = nowInstant().plus(30, ChronoUnit.DAYS)
@@ -279,11 +279,11 @@ class ReadingEventServiceTest(
Assertions.assertEquals(ReadingEventType.CURRENTLY_READING, userbook.lastReadingEvent)
Assertions.assertTrue(userbook.lastReadingEventDate?.isAfter(nowInstant().plus(29, ChronoUnit.DAYS))!!)
- Assertions.assertEquals(2, readingEventService.findAll(null, null, Pageable.ofSize(30)).totalElements)
+ Assertions.assertEquals(2, readingEventService.findAll(null, null, null, Pageable.ofSize(30)).totalElements)
Assertions.assertEquals(0, File(jeluProperties.files.images).listFiles().size)
readingEventService.deleteReadingEventById(newEvent.id!!)
- Assertions.assertEquals(1, readingEventService.findAll(null, null, Pageable.ofSize(30)).totalElements)
+ Assertions.assertEquals(1, readingEventService.findAll(null, null, null, Pageable.ofSize(30)).totalElements)
userbook = bookService.findUserBookById(saved.id!!)
Assertions.assertEquals(ReadingEventType.FINISHED, userbook.lastReadingEvent)
Assertions.assertTrue(userbook.lastReadingEventDate?.isAfter(nowInstant().minus(2, ChronoUnit.MINUTES))!!)
@@ -300,7 +300,7 @@ class ReadingEventServiceTest(
var nbUserBooks = bookService.findUserBookByCriteria(user.id.value, null, null, Pageable.ofSize(30)).totalElements
Assertions.assertEquals(0, nbUserBooks)
- Assertions.assertEquals(0, readingEventService.findAll(null, null, Pageable.ofSize(30)).totalElements)
+ Assertions.assertEquals(0, readingEventService.findAll(null, null, null, Pageable.ofSize(30)).totalElements)
val dateAfter = nowInstant().plus(1, ChronoUnit.DAYS)
val newEvent = readingEventService.save(
@@ -315,7 +315,7 @@ class ReadingEventServiceTest(
nbUserBooks = bookService.findUserBookByCriteria(user.id.value, null, null, Pageable.ofSize(30)).totalElements
Assertions.assertEquals(1, nbUserBooks)
- Assertions.assertEquals(1, readingEventService.findAll(null, null, Pageable.ofSize(30)).totalElements)
+ Assertions.assertEquals(1, readingEventService.findAll(null, null, null, Pageable.ofSize(30)).totalElements)
}
@Test
@@ -324,7 +324,7 @@ class ReadingEventServiceTest(
val nbBooks = bookService.findAll(null, null, null, null, null, null, Pageable.ofSize(30), user(), LibraryFilter.ANY).totalElements
Assertions.assertEquals(0, nbBooks)
- var nbUserBooks = bookService.findUserBookByCriteria(user.id.value, null, null, Pageable.ofSize(30)).totalElements
+ val nbUserBooks = bookService.findUserBookByCriteria(user.id.value, null, null, Pageable.ofSize(30)).totalElements
Assertions.assertEquals(0, nbUserBooks)
val dateAfter = nowInstant().plus(1, ChronoUnit.DAYS)
@@ -338,7 +338,7 @@ class ReadingEventServiceTest(
user()
)
}
- Assertions.assertEquals(0, readingEventService.findAll(null, null, Pageable.ofSize(30)).totalElements)
+ Assertions.assertEquals(0, readingEventService.findAll(null, null, null, Pageable.ofSize(30)).totalElements)
}
@Test
@@ -364,7 +364,7 @@ class ReadingEventServiceTest(
Assertions.assertNotNull(saved.book.modificationDate)
Assertions.assertEquals(ReadingEventType.FINISHED, saved.lastReadingEvent)
Assertions.assertNotNull(saved.lastReadingEventDate)
- Assertions.assertEquals(1, readingEventService.findAll(null, null, Pageable.ofSize(30)).totalElements)
+ Assertions.assertEquals(1, readingEventService.findAll(null, null, null, Pageable.ofSize(30)).totalElements)
Assertions.assertEquals(0, File(jeluProperties.files.images).listFiles().size)
val thirtyDaysAfter = nowInstant().plus(30, ChronoUnit.DAYS)
@@ -382,7 +382,7 @@ class ReadingEventServiceTest(
Assertions.assertEquals(ReadingEventType.CURRENTLY_READING, userbook.lastReadingEvent)
Assertions.assertTrue(userbook.lastReadingEventDate?.isAfter(nowInstant().plus(29, ChronoUnit.DAYS))!!)
- Assertions.assertEquals(2, readingEventService.findAll(null, null, Pageable.ofSize(30)).totalElements)
+ Assertions.assertEquals(2, readingEventService.findAll(null, null, null, Pageable.ofSize(30)).totalElements)
Assertions.assertEquals(0, File(jeluProperties.files.images).listFiles().size)
val updated = readingEventService.updateReadingEvent(
diff --git a/src/test/kotlin/io/github/bayang/jelu/service/UserMessageServiceTest.kt b/src/test/kotlin/io/github/bayang/jelu/service/UserMessageServiceTest.kt
new file mode 100644
index 00000000..db8200a5
--- /dev/null
+++ b/src/test/kotlin/io/github/bayang/jelu/service/UserMessageServiceTest.kt
@@ -0,0 +1,93 @@
+package io.github.bayang.jelu.service
+
+import io.github.bayang.jelu.dao.MessageCategory
+import io.github.bayang.jelu.dao.User
+import io.github.bayang.jelu.dto.CreateUserDto
+import io.github.bayang.jelu.dto.CreateUserMessageDto
+import io.github.bayang.jelu.dto.JeluUser
+import io.github.bayang.jelu.dto.UpdateUserMessageDto
+import org.junit.jupiter.api.AfterAll
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.BeforeAll
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.TestInstance
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.data.domain.Pageable
+
+@SpringBootTest
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class UserMessageServiceTest(
+ @Autowired private val userMessageService: UserMessageService,
+ @Autowired private val userService: UserService,
+) {
+
+ @BeforeAll
+ fun setupUser() {
+ userService.save(CreateUserDto(login = "testuser", password = "1234", isAdmin = true))
+ }
+
+ @AfterAll
+ fun teardDown() {
+ userMessageService.find(user(), null, null, Pageable.ofSize(200))
+ .forEach { userMessageDto -> userMessageService.delete(userMessageDto.id!!) }
+ userService.findAll(null).forEach { userService.deleteUser(it.id!!) }
+ }
+
+ @Test
+ fun testSaveFindAndUpdate() {
+ val saved = userMessageService.save(
+ CreateUserMessageDto(
+ "this is a message",
+ "/test/myfile.csv",
+ MessageCategory.INFO
+ ),
+ user()
+ )
+ Assertions.assertEquals("this is a message", saved.message)
+ Assertions.assertEquals(MessageCategory.INFO, saved.category)
+ Assertions.assertNotNull(saved.creationDate)
+ Assertions.assertNotNull(saved.modificationDate)
+ Assertions.assertNotNull(saved.id)
+ Assertions.assertEquals("/test/myfile.csv", saved.link)
+
+ val found = userMessageService.find(user(), null, null, Pageable.ofSize(30))
+ Assertions.assertEquals(1, found.totalElements)
+ val first = found.content[0]
+ Assertions.assertEquals("this is a message", first.message)
+ Assertions.assertEquals(MessageCategory.INFO, first.category)
+ Assertions.assertNotNull(first.creationDate)
+ Assertions.assertNotNull(first.modificationDate)
+ Assertions.assertNotNull(saved.id)
+ Assertions.assertEquals("/test/myfile.csv", first.link)
+
+ val readMessage = userMessageService.update(first.id!!, UpdateUserMessageDto(null, null, null, true))
+ Assertions.assertEquals(true, readMessage.read)
+ Assertions.assertEquals("this is a message", readMessage.message)
+ Assertions.assertEquals(MessageCategory.INFO, readMessage.category)
+ Assertions.assertNotNull(readMessage.creationDate)
+ Assertions.assertNotNull(readMessage.modificationDate)
+ Assertions.assertNotNull(readMessage.id)
+ Assertions.assertEquals("/test/myfile.csv", readMessage.link)
+
+ val notFound = userMessageService.find(user(), false, null, Pageable.ofSize(30))
+ Assertions.assertEquals(0, notFound.totalElements)
+
+ val foundRead = userMessageService.find(user(), true, null, Pageable.ofSize(30))
+ Assertions.assertEquals(1, foundRead.totalElements)
+
+ val foundCategory = userMessageService.find(user(), null, listOf(MessageCategory.INFO), Pageable.ofSize(30))
+ Assertions.assertEquals(1, foundCategory.totalElements)
+
+ val notFoundCategory = userMessageService.find(user(), null, listOf(MessageCategory.ERROR), Pageable.ofSize(30))
+ Assertions.assertEquals(0, notFoundCategory.totalElements)
+
+ val foundSeveralCategories = userMessageService.find(user(), null, listOf(MessageCategory.ERROR, MessageCategory.INFO), Pageable.ofSize(30))
+ Assertions.assertEquals(1, foundSeveralCategories.totalElements)
+ }
+
+ fun user(): User {
+ val userDetail = userService.loadUserByUsername("testuser")
+ return (userDetail as JeluUser).user
+ }
+}
diff --git a/src/test/kotlin/io/github/bayang/jelu/service/exports/CsvExportServiceTest.kt b/src/test/kotlin/io/github/bayang/jelu/service/exports/CsvExportServiceTest.kt
new file mode 100644
index 00000000..757f1fd4
--- /dev/null
+++ b/src/test/kotlin/io/github/bayang/jelu/service/exports/CsvExportServiceTest.kt
@@ -0,0 +1,172 @@
+package io.github.bayang.jelu.service.exports
+
+import io.github.bayang.jelu.authorDto
+import io.github.bayang.jelu.config.JeluProperties
+import io.github.bayang.jelu.createUserBookDto
+import io.github.bayang.jelu.dao.ReadingEventType
+import io.github.bayang.jelu.dao.User
+import io.github.bayang.jelu.dto.BookCreateDto
+import io.github.bayang.jelu.dto.CreateReadingEventDto
+import io.github.bayang.jelu.dto.CreateUserBookDto
+import io.github.bayang.jelu.dto.CreateUserDto
+import io.github.bayang.jelu.dto.JeluUser
+import io.github.bayang.jelu.dto.UserBookLightDto
+import io.github.bayang.jelu.service.BookService
+import io.github.bayang.jelu.service.ReadingEventService
+import io.github.bayang.jelu.service.UserMessageService
+import io.github.bayang.jelu.service.UserService
+import io.github.bayang.jelu.tags
+import org.assertj.core.util.Files
+import org.junit.jupiter.api.AfterAll
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.BeforeAll
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.TestInstance
+import org.junit.jupiter.api.io.TempDir
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.data.domain.Pageable
+import java.io.File
+import java.time.OffsetDateTime
+import java.time.ZoneId
+import java.util.Locale
+
+@SpringBootTest
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class CsvExportServiceTest(
+ @Autowired private val csvExportService: CsvExportService,
+ @Autowired private val bookService: BookService,
+ @Autowired private val userService: UserService,
+ @Autowired private val jeluProperties: JeluProperties,
+ @Autowired private val readingEventService: ReadingEventService,
+ @Autowired private val userMessageService: UserMessageService,
+) {
+
+ companion object {
+ @TempDir
+ lateinit var tempDir: File
+ }
+
+ @BeforeAll
+ fun setupUser() {
+ userService.save(CreateUserDto(login = "testuser", password = "1234", isAdmin = true))
+ jeluProperties.files.imports = tempDir.absolutePath
+ println(jeluProperties.files.imports)
+ }
+
+ @AfterAll
+ fun teardDown() {
+ userService.findAll(null).forEach { userService.deleteUser(it.id!!) }
+ }
+
+ @AfterEach
+ fun cleanTest() {
+ tempDir.listFiles().forEach {
+ it.deleteRecursively()
+ }
+ readingEventService.findAll(null, null, null, Pageable.ofSize(30)).content.forEach {
+ readingEventService.deleteReadingEventById(it.id!!)
+ }
+ bookService.findUserBookByCriteria(user().id.value, null, null, Pageable.ofSize(30))
+ .forEach { bookService.deleteUserBookById(it.id!!) }
+ bookService.findAllAuthors(null, Pageable.ofSize(30)).forEach {
+ bookService.deleteAuthorById(it.id!!)
+ }
+ userMessageService.find(user(), null, null, Pageable.ofSize(200))
+ .forEach { userMessageDto -> userMessageService.delete(userMessageDto.id!!) }
+ }
+
+ @Test
+ fun testExport() {
+ val book1 = BookCreateDto(
+ id = null,
+ title = "book1",
+ isbn10 = "1566199093",
+ isbn13 = "9781566199094 ",
+ summary = "This is a test summary\nwith a newline",
+ image = "",
+ publisher = "test-publisher",
+ pageCount = 50,
+ publishedDate = "",
+ series = "",
+ authors = mutableListOf(authorDto(), authorDto("author2 name")),
+ numberInSeries = null,
+ tags = tags(),
+ goodreadsId = "4321abc",
+ googleId = "1234",
+ librarythingId = "",
+ language = "",
+ amazonId = ""
+ )
+ val createUserBookDto1 = CreateUserBookDto(
+ personalNotes = "test personal notes\nwith a newline",
+ lastReadingEvent = null,
+ lastReadingEventDate = null,
+ owned = false,
+ toRead = true,
+ percentRead = null,
+ book = book1
+ )
+ bookService.save(createUserBookDto1, user(), null)
+
+ val book2 = BookCreateDto(
+ id = null,
+ title = "book2",
+ isbn10 = "1566199093",
+ isbn13 = "9781566199094 ",
+ summary = "This is a test summary\nwith a newline",
+ image = "",
+ publisher = "test-publisher",
+ pageCount = 50,
+ publishedDate = "",
+ series = "",
+ authors = mutableListOf(authorDto()),
+ numberInSeries = null,
+ tags = emptyList(),
+ goodreadsId = "4321abc",
+ googleId = "1234",
+ librarythingId = "",
+ language = "",
+ amazonId = ""
+ )
+ val offset = OffsetDateTime.now(ZoneId.systemDefault()).offset
+ val date1 = OffsetDateTime.of(2022, 2, 10, 6, 30, 0, 0, offset)
+ val createUserBookDto2 = createUserBookDto(book2, ReadingEventType.FINISHED, date1.toInstant())
+ val saved2: UserBookLightDto = bookService.save(createUserBookDto2, user(), null)
+ val date3 = OffsetDateTime.of(2020, 2, 10, 6, 30, 0, 0, offset)
+ val date2 = OffsetDateTime.of(2021, 2, 10, 6, 30, 0, 0, offset)
+ readingEventService.save(
+ CreateReadingEventDto(
+ ReadingEventType.FINISHED,
+ saved2.book.id,
+ date3.toInstant(),
+ ),
+ user()
+ )
+ readingEventService.save(
+ CreateReadingEventDto(
+ ReadingEventType.FINISHED,
+ saved2.book.id,
+ date2.toInstant(),
+ ),
+ user()
+ )
+ csvExportService.export(user(), Locale.ENGLISH)
+ val csv = File(jeluProperties.files.imports).listFiles()[0]
+ var content = Files.contentOf(csv, Charsets.UTF_8)
+ content = content.replace("\r\n", "\n")
+ val expectedCsv = File(this::class.java.getResource("/csv-export/expected.csv").file)
+ val expectedContent = Files.contentOf(expectedCsv, Charsets.UTF_8)
+ Assertions.assertEquals(expectedContent, content)
+ val fileBeginning = "jelu-export-${user().login}"
+ Assertions.assertTrue(csv.name.startsWith(fileBeginning, true))
+ val messages = userMessageService.find(user(), false, null, Pageable.ofSize(30))
+ Assertions.assertEquals(2, messages.numberOfElements)
+ }
+
+ fun user(): User {
+ val userDetail = userService.loadUserByUsername("testuser")
+ return (userDetail as JeluUser).user
+ }
+}
diff --git a/src/test/kotlin/io/github/bayang/jelu/service/imports/CsvImportServiceTest.kt b/src/test/kotlin/io/github/bayang/jelu/service/imports/CsvImportServiceTest.kt
index 4862da43..40dbdeba 100644
--- a/src/test/kotlin/io/github/bayang/jelu/service/imports/CsvImportServiceTest.kt
+++ b/src/test/kotlin/io/github/bayang/jelu/service/imports/CsvImportServiceTest.kt
@@ -57,7 +57,7 @@ class CsvImportServiceTest(
it.deleteRecursively()
}
importService.deleteByprocessingStatusAndUser(ProcessingStatus.SAVED, user().id.value)
- readingEventService.findAll(null, null, Pageable.ofSize(30)).content.forEach {
+ readingEventService.findAll(null, null, null, Pageable.ofSize(30)).content.forEach {
readingEventService.deleteReadingEventById(it.id!!)
}
bookService.findUserBookByCriteria(user().id.value, null, null, Pageable.ofSize(30))
diff --git a/src/test/resources/csv-export/expected.csv b/src/test/resources/csv-export/expected.csv
new file mode 100644
index 00000000..ec4a2fbf
--- /dev/null
+++ b/src/test/resources/csv-export/expected.csv
@@ -0,0 +1,3 @@
+Title,Author,ISBN,Publisher,Date Read,Shelves,Bookshelves,read_dates,tags,authors,isbn10,isbn13,owned
+book2,test author,9781566199094,test-publisher,2022/02/10,,,"2022/02/10,2021/02/10,2020/02/10",,test author,1566199093,9781566199094,true
+book1,test author,9781566199094,test-publisher,,to-read,sciencefiction fantasy,,"science fiction,fantasy","test author,author2 name",1566199093,9781566199094,