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') + } +} + 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 @@ + + + + + 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,