Book form: can now edit an existing book

This commit is contained in:
2026-04-01 00:34:09 +02:00
parent bcde39d51d
commit 8d97d00e93
11 changed files with 297 additions and 85 deletions

View 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
}
}

View File

@@ -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) {

View 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)
}

View File

@@ -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 {

View File

@@ -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
}
}

View 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")
}

View File

@@ -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})
})