diff --git a/front/src/BookFormView.vue b/front/src/BookFormView.vue
index e0289c7..55809da 100644
--- a/front/src/BookFormView.vue
+++ b/front/src/BookFormView.vue
@@ -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() {
+
+
+
+
+
+ Modifier le livre
+
+
{{ data.title }}
diff --git a/front/src/api.js b/front/src/api.js
index 3352b64..e3f0dd0 100644
--- a/front/src/api.js
+++ b/front/src/api.js
@@ -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))
}
diff --git a/front/src/router.js b/front/src/router.js
index 036cdc9..6126207 100644
--- a/front/src/router.js
+++ b/front/src/router.js
@@ -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 },
diff --git a/internal/adapter/adapter.go b/internal/adapter/adapter.go
new file mode 100644
index 0000000..1971e13
--- /dev/null
+++ b/internal/adapter/adapter.go
@@ -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
+ }
+}
diff --git a/internal/apitest/post_book_test.go b/internal/apitest/post_book_test.go
index 1fa3f6b..3902717 100644
--- a/internal/apitest/post_book_test.go
+++ b/internal/apitest/post_book_test.go
@@ -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) {
diff --git a/internal/apitest/put_book_test.go b/internal/apitest/put_book_test.go
new file mode 100644
index 0000000..4d89322
--- /dev/null
+++ b/internal/apitest/put_book_test.go
@@ -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)
+}
diff --git a/internal/dto/in.go b/internal/dto/in.go
index 39c3068..2d2188f 100644
--- a/internal/dto/in.go
+++ b/internal/dto/in.go
@@ -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 {
diff --git a/internal/routes/bookpostcreate.go b/internal/routes/bookpostcreate.go
index 0c0b083..f837521 100644
--- a/internal/routes/bookpostcreate.go
+++ b/internal/routes/bookpostcreate.go
@@ -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
- }
-}
diff --git a/internal/routes/bookputupdate.go b/internal/routes/bookputupdate.go
new file mode 100644
index 0000000..8a4583a
--- /dev/null
+++ b/internal/routes/bookputupdate.go
@@ -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")
+}
diff --git a/internal/setup/setup.go b/internal/setup/setup.go
index da3c1e4..31f1361 100644
--- a/internal/setup/setup.go
+++ b/internal/setup/setup.go
@@ -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})
})