Book form: can now edit an existing book
This commit is contained in:
@@ -1,11 +1,17 @@
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { postBook, extractFormErrorFromField } from './api.js'
|
||||
import { postBook, putBook, extractFormErrorFromField, getBookCall } from './api.js'
|
||||
import { useRouter } from 'vue-router'
|
||||
import CoverUpload from './CoverUpload.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const props = defineProps({
|
||||
id: String,
|
||||
})
|
||||
|
||||
const fetchError = ref(null)
|
||||
|
||||
const book = ref({
|
||||
title: '',
|
||||
author: '',
|
||||
@@ -16,6 +22,31 @@ const book = ref({
|
||||
summary: '',
|
||||
coverId: null,
|
||||
})
|
||||
|
||||
if (props.id) {
|
||||
getBookCall(props.id)
|
||||
.then((res) => {
|
||||
if (res.status === 401) {
|
||||
const authStore = useAuthStore()
|
||||
authStore.logout()
|
||||
}
|
||||
return res.json()
|
||||
})
|
||||
.then(fillBookWithJson)
|
||||
.catch((err) => (fetchError.value = err))
|
||||
}
|
||||
|
||||
function fillBookWithJson(json) {
|
||||
book.value.title = json.title
|
||||
book.value.author = json.author
|
||||
book.value.isbn = json.isbn
|
||||
book.value.inventaireid = json.inventaireid
|
||||
book.value.openlibraryid = json.openlibraryid
|
||||
book.value.shortdescription = json.shortdescription
|
||||
book.value.summary = json.summary
|
||||
book.value.coverId = json.coverId
|
||||
}
|
||||
|
||||
const errors = ref(null)
|
||||
const titleError = computed(() => {
|
||||
return extractFormErrorFromField('Title', errors.value)
|
||||
@@ -39,20 +70,39 @@ const summaryError = computed(() => {
|
||||
return extractFormErrorFromField('ShortDescription', errors.value)
|
||||
})
|
||||
|
||||
function postOrPutBook(book) {
|
||||
if (props.id) {
|
||||
return
|
||||
} else {
|
||||
return postBook(book)
|
||||
}
|
||||
}
|
||||
|
||||
function onSubmit(e) {
|
||||
postBook(book).then((res) => {
|
||||
if (res.ok) {
|
||||
res.json().then((json) => router.push('/book/' + json.id))
|
||||
return
|
||||
} else {
|
||||
res.json().then((json) => (errors.value = json))
|
||||
}
|
||||
})
|
||||
if (props.id) {
|
||||
putBook(props.id, book).then((res) => {
|
||||
if (res.ok) {
|
||||
router.push('/book/' + props.id)
|
||||
} else {
|
||||
res.json().then((json) => (errors.value = json))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
postBook(book).then((res) => {
|
||||
if (res.ok) {
|
||||
res.json().then((json) => router.push('/book/' + json.id))
|
||||
return
|
||||
} else {
|
||||
res.json().then((json) => (errors.value = json))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="onSubmit">
|
||||
<div v-if="error">{{ $t('bookform.error', { error: fetchError.message }) }}</div>
|
||||
<form v-else @submit.prevent="onSubmit">
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('addbook.title') }}</label>
|
||||
<div class="control">
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
putEndReadDateUnset,
|
||||
putUnreadBook,
|
||||
} from './api.js'
|
||||
import { useRouter, onBeforeRouteUpdate } from 'vue-router'
|
||||
import { useRouter, onBeforeRouteUpdate, RouterLink } from 'vue-router'
|
||||
import { VRating } from 'vuetify/components/VRating'
|
||||
import BookFormIcons from './BookFormIcons.vue'
|
||||
import ReviewWidget from './ReviewWidget.vue'
|
||||
@@ -105,6 +105,14 @@ function goToAuthor() {
|
||||
<figure class="image">
|
||||
<img v-bind:src="imagePathOrDefault" v-bind:alt="data.title" />
|
||||
</figure>
|
||||
<div class="centered mt-2">
|
||||
<RouterLink :to="'/book/' + props.id + '/edit'">
|
||||
<span>
|
||||
<b-icon-pencil-fill />
|
||||
</span>
|
||||
Modifier le livre
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<h3 class="title">{{ data.title }}</h3>
|
||||
|
||||
@@ -18,28 +18,34 @@ export function getImagePathOrGivenDefault(path, defaultpath) {
|
||||
}
|
||||
}
|
||||
|
||||
function useFetch(data, error, url) {
|
||||
function userFetch(url) {
|
||||
const { user } = useAuthStore()
|
||||
|
||||
if (user != null) {
|
||||
fetch(url, {
|
||||
return fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + user.token,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.status === 401) {
|
||||
const authStore = useAuthStore()
|
||||
authStore.logout()
|
||||
}
|
||||
return res.json()
|
||||
})
|
||||
.then((json) => (data.value = json))
|
||||
.catch((err) => (error.value = err))
|
||||
} else {
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
function useFetch(data, error, url) {
|
||||
userFetch(url)
|
||||
.then((res) => {
|
||||
if (res.status === 401) {
|
||||
const authStore = useAuthStore()
|
||||
authStore.logout()
|
||||
}
|
||||
return res.json()
|
||||
})
|
||||
.then((json) => (data.value = json))
|
||||
.catch((err) => (error.value = err))
|
||||
}
|
||||
|
||||
export async function getAppInfo(appInfo, appInfoErr) {
|
||||
return fetch('/ws/appinfo', {
|
||||
method: 'GET',
|
||||
@@ -98,6 +104,10 @@ export function getBook(data, error, id) {
|
||||
return useFetch(data, error, '/ws/book/' + id)
|
||||
}
|
||||
|
||||
export function getBookCall(id) {
|
||||
return userFetch('/ws/book/' + id)
|
||||
}
|
||||
|
||||
export function postBook(book) {
|
||||
return genericPayloadCall('/ws/book', book.value, 'POST')
|
||||
}
|
||||
@@ -106,6 +116,10 @@ export async function postImportBook(id, language) {
|
||||
return genericPayloadCall('/ws/importbook', { inventaireid: id, lang: language }, 'POST')
|
||||
}
|
||||
|
||||
export function putBook(id, book) {
|
||||
return genericPayloadCall('/ws/book/edit/' + id, book.value, 'PUT')
|
||||
}
|
||||
|
||||
export async function putReadBook(bookId) {
|
||||
return putEndReadDate(bookId, new Date().toISOString().slice(0, 10))
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ const routes = [
|
||||
{ path: '/browse', component: InstanceBrowser },
|
||||
{ path: '/books', component: BooksBrowser },
|
||||
{ path: '/book/:id', component: BookFormView, props: true },
|
||||
{ path: '/book/:id/edit', component: BookFormEdit, props: true },
|
||||
{ path: '/author/:id', component: AuthorForm, props: true },
|
||||
{ path: '/search/:searchterm', component: SearchBook, props: true },
|
||||
{ path: '/import/inventaire/:inventaireid', component: ImportInventaire, props: true },
|
||||
|
||||
63
internal/adapter/adapter.go
Normal file
63
internal/adapter/adapter.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"git.artlef.fr/bibliomane/internal/appcontext"
|
||||
"git.artlef.fr/bibliomane/internal/dto"
|
||||
"git.artlef.fr/bibliomane/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func FillBookDbFromFields(ac appcontext.AppContext, fields *dto.BookFields, book *model.Book) error {
|
||||
if fields.Title != nil {
|
||||
book.Title = *fields.Title
|
||||
}
|
||||
if fields.ISBN != nil {
|
||||
book.ISBN = *fields.ISBN
|
||||
}
|
||||
if fields.InventaireID != nil {
|
||||
book.InventaireID = *fields.InventaireID
|
||||
}
|
||||
if fields.OpenLibraryId != nil {
|
||||
book.OpenLibraryId = *fields.OpenLibraryId
|
||||
}
|
||||
if fields.ShortDescription != nil {
|
||||
book.SmallDescription = *fields.ShortDescription
|
||||
}
|
||||
if fields.Summary != nil {
|
||||
book.Summary = *fields.Summary
|
||||
}
|
||||
if fields.CoverID != nil {
|
||||
book.CoverID = *fields.CoverID
|
||||
}
|
||||
|
||||
if fields.Author != nil {
|
||||
author, err := fetchOrCreateAuthor(ac, *fields.Author)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
book.AuthorID = author.ID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fetchOrCreateAuthor(ac appcontext.AppContext, name string) (*model.Author, error) {
|
||||
var author model.Author
|
||||
res := ac.Db.Where("name = ?", name).First(&author)
|
||||
err := res.Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
author = model.Author{Name: name}
|
||||
err = ac.Db.Save(&author).Error
|
||||
if err != nil {
|
||||
return &author, err
|
||||
}
|
||||
return &author, nil
|
||||
} else {
|
||||
return &author, err
|
||||
}
|
||||
} else {
|
||||
return &author, nil
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.artlef.fr/bibliomane/internal/dto"
|
||||
"git.artlef.fr/bibliomane/internal/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -62,20 +61,12 @@ func TestPostBookHandler_AllFields(t *testing.T) {
|
||||
id := testPostBookHandler(t, bookJson, 200)
|
||||
createdBook := testGetBook(t, strconv.FormatUint(uint64(id), 10), http.StatusOK)
|
||||
|
||||
assert.Equal(t,
|
||||
dto.FullBookGet{
|
||||
Title: "Amerika",
|
||||
Author: "Kafka",
|
||||
AuthorID: 27,
|
||||
ISBN: "978-2-07-036803-7",
|
||||
InventaireId: "isbn:9782070368037",
|
||||
OpenLibraryId: "OL8838048M",
|
||||
Summary: "L'Amérique (Amerika en version originale allemande) ou Le Disparu (Der Verschollene, titre voulu par l'auteur et rendu au livre dans ses plus récentes éditions) est le premier roman de Franz Kafka (1883-1924).",
|
||||
Rating: 0,
|
||||
Read: false,
|
||||
CoverPath: "",
|
||||
Review: "",
|
||||
}, createdBook)
|
||||
assert.Equal(t, "Amerika", createdBook.Title)
|
||||
assert.Equal(t, "Kafka", createdBook.Author)
|
||||
assert.Equal(t, "978-2-07-036803-7", createdBook.ISBN)
|
||||
assert.Equal(t, "isbn:9782070368037", createdBook.InventaireId)
|
||||
assert.Equal(t, "OL8838048M", createdBook.OpenLibraryId)
|
||||
assert.Equal(t, "L'Amérique (Amerika en version originale allemande) ou Le Disparu (Der Verschollene, titre voulu par l'auteur et rendu au livre dans ses plus récentes éditions) est le premier roman de Franz Kafka (1883-1924).", createdBook.Summary)
|
||||
}
|
||||
|
||||
func TestPostBookHandler_TitleTooLong(t *testing.T) {
|
||||
|
||||
56
internal/apitest/put_book_test.go
Normal file
56
internal/apitest/put_book_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package apitest
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"git.artlef.fr/bibliomane/internal/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPutBookHandler_TitleChange(t *testing.T) {
|
||||
bookId := "17"
|
||||
bookJson :=
|
||||
`{
|
||||
"title": "Le coup de pistolaid"
|
||||
}`
|
||||
testPutBook(t, bookJson, bookId, 200)
|
||||
modifiedBook := testGetBook(t, bookId, http.StatusOK)
|
||||
assert.Equal(t, "Le coup de pistolaid", modifiedBook.Title)
|
||||
}
|
||||
|
||||
func TestPutBookHandler_Author(t *testing.T) {
|
||||
bookId := "17"
|
||||
bookJson :=
|
||||
`{
|
||||
"author": "Alexander Pouchkine"
|
||||
}`
|
||||
testPutBook(t, bookJson, bookId, 200)
|
||||
modifiedBook := testGetBook(t, bookId, http.StatusOK)
|
||||
assert.Equal(t, "Alexander Pouchkine", modifiedBook.Author)
|
||||
}
|
||||
|
||||
func TestPutBookHandler_MultipleFields(t *testing.T) {
|
||||
bookId := "17"
|
||||
bookJson :=
|
||||
`{
|
||||
"title": "Le pistolet",
|
||||
"author": "Pouchkine",
|
||||
"isbn": "978-2-07-036803-7",
|
||||
"inventaireid": "isbn:9782070368037",
|
||||
"openlibraryid": "OL8838048M",
|
||||
"shortdescription": "Roman de Pouchkine",
|
||||
"summary": "En garnison dans une petite ville, un officier de l'armée impériale russe rencontre Silvio, ancien soldat et tireur exceptionnel. Celui-ci fait forte impression sur lui, jusqu'au jour où il refuse, à la suite d'un affront, de se battre en duel."
|
||||
}`
|
||||
testPutBook(t, bookJson, bookId, 200)
|
||||
modifiedBook := testGetBook(t, bookId, http.StatusOK)
|
||||
assert.Equal(t, "Le pistolet", modifiedBook.Title)
|
||||
assert.Equal(t, "Pouchkine", modifiedBook.Author)
|
||||
assert.Equal(t, "978-2-07-036803-7", modifiedBook.ISBN)
|
||||
assert.Equal(t, "OL8838048M", modifiedBook.OpenLibraryId)
|
||||
assert.Equal(t, "En garnison dans une petite ville, un officier de l'armée impériale russe rencontre Silvio, ancien soldat et tireur exceptionnel. Celui-ci fait forte impression sur lui, jusqu'au jour où il refuse, à la suite d'un affront, de se battre en duel.", modifiedBook.Summary)
|
||||
}
|
||||
|
||||
func testPutBook(t *testing.T, payload string, bookId string, expectedCode int) {
|
||||
testutils.TestBookPutCallWithDemoPayload(t, payload, bookId, expectedCode, "/ws/book/edit/"+bookId)
|
||||
}
|
||||
@@ -10,15 +10,15 @@ type BookSearchGetParam struct {
|
||||
Inventaire bool `form:"inventaire"`
|
||||
}
|
||||
|
||||
type BookPostCreate struct {
|
||||
Title string `json:"title" binding:"required,max=300"`
|
||||
Author string `json:"author" binding:"max=100"`
|
||||
ISBN string `json:"isbn" binding:"max=18"`
|
||||
InventaireID string `json:"inventaireid" binding:"max=50"`
|
||||
OpenLibraryId string `json:"openlibraryid" binding:"max=50"`
|
||||
ShortDescription string `json:"shortdescription" binding:"max=300"`
|
||||
Summary string `json:"summary"`
|
||||
CoverID uint `json:"coverId"`
|
||||
type BookFields struct {
|
||||
Title *string `json:"title" binding:"omitempty,max=300"`
|
||||
Author *string `json:"author" binding:"omitempty,max=100"`
|
||||
ISBN *string `json:"isbn" binding:"omitempty,max=18"`
|
||||
InventaireID *string `json:"inventaireid" binding:"omitempty,max=50"`
|
||||
OpenLibraryId *string `json:"openlibraryid" binding:"omitempty,max=50"`
|
||||
ShortDescription *string `json:"shortdescription" binding:"omitempty,max=300"`
|
||||
Summary *string `json:"summary"`
|
||||
CoverID *uint `json:"coverId"`
|
||||
}
|
||||
|
||||
type BookPostImport struct {
|
||||
|
||||
@@ -4,26 +4,38 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"git.artlef.fr/bibliomane/internal/adapter"
|
||||
"git.artlef.fr/bibliomane/internal/appcontext"
|
||||
"git.artlef.fr/bibliomane/internal/dto"
|
||||
"git.artlef.fr/bibliomane/internal/i18nresource"
|
||||
"git.artlef.fr/bibliomane/internal/model"
|
||||
"git.artlef.fr/bibliomane/internal/myvalidator"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func PostBookHandler(ac appcontext.AppContext) {
|
||||
var book dto.BookPostCreate
|
||||
var book dto.BookFields
|
||||
err := ac.C.ShouldBindJSON(&book)
|
||||
if err != nil {
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
return
|
||||
}
|
||||
err = myvalidator.ValidateId(ac.Db, book.CoverID, &model.StaticFile{})
|
||||
if err != nil {
|
||||
//when creating a book, title is required
|
||||
if book.Title == nil {
|
||||
err = myvalidator.HttpError{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Err: errors.New(i18nresource.GetTranslatedMessage(&ac, "ValidationRequired")),
|
||||
}
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
return
|
||||
}
|
||||
if book.CoverID != nil {
|
||||
err = myvalidator.ValidateId(ac.Db, *book.CoverID, &model.StaticFile{})
|
||||
if err != nil {
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
user, fetchUserErr := ac.GetAuthenticatedUser()
|
||||
if fetchUserErr != nil {
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
@@ -38,44 +50,15 @@ func PostBookHandler(ac appcontext.AppContext) {
|
||||
ac.C.JSON(http.StatusOK, gin.H{"id": id})
|
||||
}
|
||||
|
||||
func saveBookToDb(ac appcontext.AppContext, b dto.BookPostCreate, user *model.User) (uint, error) {
|
||||
author, err := fetchOrCreateAuthor(ac, b.Author)
|
||||
func saveBookToDb(ac appcontext.AppContext, b dto.BookFields, user *model.User) (uint, error) {
|
||||
book := model.Book{
|
||||
AddedBy: *user,
|
||||
}
|
||||
err := adapter.FillBookDbFromFields(ac, &b, &book)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
book := model.Book{
|
||||
Title: b.Title,
|
||||
AuthorID: author.ID,
|
||||
ISBN: b.ISBN,
|
||||
InventaireID: b.InventaireID,
|
||||
OpenLibraryId: b.OpenLibraryId,
|
||||
SmallDescription: b.ShortDescription,
|
||||
Summary: b.Summary,
|
||||
AddedBy: *user,
|
||||
}
|
||||
if b.CoverID > 0 {
|
||||
book.CoverID = b.CoverID
|
||||
}
|
||||
|
||||
err = ac.Db.Save(&book).Error
|
||||
return book.ID, err
|
||||
}
|
||||
|
||||
func fetchOrCreateAuthor(ac appcontext.AppContext, name string) (*model.Author, error) {
|
||||
var author model.Author
|
||||
res := ac.Db.Where("name = ?", name).First(&author)
|
||||
err := res.Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
author = model.Author{Name: name}
|
||||
err = ac.Db.Save(&author).Error
|
||||
if err != nil {
|
||||
return &author, err
|
||||
}
|
||||
return &author, nil
|
||||
} else {
|
||||
return &author, err
|
||||
}
|
||||
} else {
|
||||
return &author, nil
|
||||
}
|
||||
}
|
||||
|
||||
43
internal/routes/bookputupdate.go
Normal file
43
internal/routes/bookputupdate.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"git.artlef.fr/bibliomane/internal/adapter"
|
||||
"git.artlef.fr/bibliomane/internal/appcontext"
|
||||
"git.artlef.fr/bibliomane/internal/dto"
|
||||
"git.artlef.fr/bibliomane/internal/model"
|
||||
"git.artlef.fr/bibliomane/internal/myvalidator"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func PutBookHandler(ac appcontext.AppContext) {
|
||||
bookId64, err := strconv.ParseUint(ac.C.Param("id"), 10, 64)
|
||||
bookId := uint(bookId64)
|
||||
if err != nil {
|
||||
ac.C.JSON(http.StatusBadRequest, gin.H{"error": err})
|
||||
return
|
||||
}
|
||||
var book model.Book
|
||||
err = ac.Db.First(&book, bookId).Error
|
||||
if err != nil {
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
return
|
||||
}
|
||||
|
||||
var bookPut dto.BookFields
|
||||
err = ac.C.ShouldBindJSON(&bookPut)
|
||||
if err != nil {
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = adapter.FillBookDbFromFields(ac, &bookPut, &book)
|
||||
if err != nil {
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
return
|
||||
}
|
||||
ac.Db.Save(&book)
|
||||
ac.C.String(http.StatusOK, "Success")
|
||||
}
|
||||
@@ -63,6 +63,9 @@ func Setup(config *config.Config) *gin.Engine {
|
||||
ws.PUT("/book/:id", func(c *gin.Context) {
|
||||
routes.PutUserBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||
})
|
||||
ws.PUT("/book/edit/:id", func(c *gin.Context) {
|
||||
routes.PutBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||
})
|
||||
ws.POST("/book", func(c *gin.Context) {
|
||||
routes.PostBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user