diff --git a/front/src/AddBookToCollection.vue b/front/src/AddBookToCollection.vue new file mode 100644 index 0000000..0a7fd79 --- /dev/null +++ b/front/src/AddBookToCollection.vue @@ -0,0 +1,95 @@ + + + + + + + {{ titleError }} + + + {{ book.title }} + + + + + + + + + + + + + + diff --git a/front/src/CollectionForm.vue b/front/src/CollectionForm.vue index 7636d01..dbd9042 100644 --- a/front/src/CollectionForm.vue +++ b/front/src/CollectionForm.vue @@ -2,6 +2,7 @@ import { computed, ref } from 'vue' import { getCollection } from './api.js' import CollectionFormBookItem from './CollectionFormBookItem.vue' +import AddBookToCollection from './AddBookToCollection.vue' import Pagination from './Pagination.vue' const props = defineProps({ @@ -27,14 +28,20 @@ getCollection(data, error, props.id, limit, offset.value) function pageChange(newPageNumber) { pageNumber.value = newPageNumber data.value = null + error.value = null getCollection(data, error, props.id, limit, offset.value) } + +function fetchCollection() { + pageChange(1) +} {{ $t('bookform.error', { error: error.message }) }} {{ data.name }} + diff --git a/front/src/SearchBook.vue b/front/src/SearchBook.vue index b8f2f94..99738cd 100644 --- a/front/src/SearchBook.vue +++ b/front/src/SearchBook.vue @@ -36,7 +36,15 @@ function fetchData(searchTerm, authorId) { error.value = null if (searchTerm != null) { const lang = navigator.language.substring(0, 2) - getSearchBooks(data, error, searchTerm, lang, forceSearchInventaire.value, limit, offset.value) + getSearchBooks( + data, + error, + searchTerm, + lang, + forceSearchInventaire.value ? 2 : 1, + limit, + offset.value, + ) } else if (authorId != null) { getAuthorBooks(data, error, authorId, limit, offset.value) } diff --git a/front/src/api.js b/front/src/api.js index 32c847d..edffebd 100644 --- a/front/src/api.js +++ b/front/src/api.js @@ -130,6 +130,14 @@ export function postCollection(collection) { return genericPayloadCall('/ws/collection', collection, 'POST') } +export function postCollectionAddBook(collectionId, bookId) { + return genericPayloadCall( + '/ws/collection/' + collectionId + '/addbook', + { bookId: bookId }, + 'POST', + ) +} + export function putBook(id, book) { return genericPayloadCall('/ws/book/edit/' + id, book.value, 'PUT') } @@ -219,10 +227,9 @@ export function genericPayloadCall(apiRoute, object, method) { } export function extractFormErrorFromField(fieldName, errors) { - if (errors == null) { + if (errors == null || typeof errors == 'undefined' || !Array.isArray(errors)) { return '' } - console.log(errors.value) const titleErr = errors.find((e) => e['field'] === fieldName) if (typeof titleErr !== 'undefined') { return titleErr.error diff --git a/internal/apitest/post_collection_addbook_test.go b/internal/apitest/post_collection_addbook_test.go new file mode 100644 index 0000000..533abab --- /dev/null +++ b/internal/apitest/post_collection_addbook_test.go @@ -0,0 +1,64 @@ +package apitest + +import ( + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "git.artlef.fr/bibliomane/internal/testutils" + "github.com/stretchr/testify/assert" +) + +func TestPostCollectionBookHandler_Ok(t *testing.T) { + payload := + `{ + "bookId": 9 + }` + collectionId := "1" + status := testPostCollectionBookHandler(t, collectionId, payload) + assert.Equal(t, http.StatusOK, status) + _, collection := testGetCollection(t, collectionId, "10", "0") + assert.Equal(t, int64(7), collection.Count) +} + +func TestPostCollectionBookHandler_CollectionNotFound(t *testing.T) { + payload := + `{ + "bookId": 9 + }` + status := testPostCollectionBookHandler(t, "12", payload) + assert.Equal(t, http.StatusNotFound, status) +} + +func TestPostCollectionBookHandler_BookNotFound(t *testing.T) { + payload := + `{ + "bookId": 14654 + }` + status := testPostCollectionBookHandler(t, "1", payload) + assert.Equal(t, http.StatusNotFound, status) +} + +func TestPostCollectionBookHandler_Unauthorized(t *testing.T) { + payload := + `{ + "bookId": 9 + }` + status := testPostCollectionBookHandler(t, "3", payload) + assert.Equal(t, http.StatusUnauthorized, status) +} + +func testPostCollectionBookHandler(t *testing.T, collectionId string, payload string) int { + router := testutils.TestSetup() + w := httptest.NewRecorder() + + token := testutils.ConnectDemoUser(router) + req, _ := http.NewRequest("POST", "/ws/collection/"+collectionId+"/addbook", + strings.NewReader(payload)) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + router.ServeHTTP(w, req) + + return w.Code +} diff --git a/internal/apitest/search_book_test.go b/internal/apitest/search_book_test.go index 1766951..655df34 100644 --- a/internal/apitest/search_book_test.go +++ b/internal/apitest/search_book_test.go @@ -3,6 +3,7 @@ package apitest import ( "encoding/json" "fmt" + "strconv" "net/http" "net/http/httptest" @@ -15,14 +16,14 @@ import ( ) func TestSearchBook_MultipleBooks(t *testing.T) { - result := testSearchBook(t, "san", "", "") + result := testSearchBook(t, "san", "", "", dto.NoInventaireSearch) assert.Equal(t, int64(2), result.Count) assert.Equal(t, 2, len(result.Books)) } func TestSearchBook_OneBookNotUserBook(t *testing.T) { - result := testSearchBook(t, "iliade", "", "") + result := testSearchBook(t, "iliade", "", "", dto.NoInventaireSearch) assert.Equal(t, int64(1), result.Count) assert.Equal(t, []dto.BookItemGet{{ @@ -38,7 +39,7 @@ func TestSearchBook_OneBookNotUserBook(t *testing.T) { } func TestSearchBook_OneBookRead(t *testing.T) { - result := testSearchBook(t, "dieux", "", "") + result := testSearchBook(t, "dieux", "", "", dto.NoInventaireSearch) assert.Equal(t, int64(1), result.Count) assert.Equal(t, []dto.BookItemGet{{ @@ -55,7 +56,7 @@ func TestSearchBook_OneBookRead(t *testing.T) { } func TestSearchBook_OneBookStartRead(t *testing.T) { - result := testSearchBook(t, "Recherches", "", "") + result := testSearchBook(t, "Recherches", "", "", dto.NoInventaireSearch) assert.Equal(t, int64(1), result.Count) assert.Equal(t, []dto.BookItemGet{{ @@ -72,7 +73,7 @@ func TestSearchBook_OneBookStartRead(t *testing.T) { } func TestSearchBook_ISBN(t *testing.T) { - result := testSearchBook(t, "9782070337903", "", "") + result := testSearchBook(t, "9782070337903", "", "", dto.NoInventaireSearch) assert.Equal(t, int64(1), result.Count) assert.Equal(t, []dto.BookItemGet{{ @@ -88,7 +89,7 @@ func TestSearchBook_ISBN(t *testing.T) { } func TestSearchBook_ISBNInventaire(t *testing.T) { - result := testSearchBook(t, "9782253158400", "", "") + result := testSearchBook(t, "9782253158400", "", "", dto.InventaireIfNothingFound) assert.Equal(t, int64(1), result.Count) assert.Equal(t, []dto.BookItemGet{{ @@ -107,17 +108,17 @@ func TestSearchBook_ISBNInventaire(t *testing.T) { } func TestSearchBook_Limit(t *testing.T) { - result := testSearchBook(t, "a", "10", "") + result := testSearchBook(t, "a", "10", "", dto.NoInventaireSearch) assert.Equal(t, 10, len(result.Books)) } func TestSearchBook_Offset(t *testing.T) { - result := testSearchBook(t, "sa", "", "2") + result := testSearchBook(t, "sa", "", "2", dto.NoInventaireSearch) assert.Equal(t, int64(5), result.Count) assert.Equal(t, 3, len(result.Books)) } -func testSearchBook(t *testing.T, searchterm string, limit string, offset string) dto.BookItemsGet { +func testSearchBook(t *testing.T, searchterm string, limit string, offset string, inventaireSearchType dto.InventaireSearchType) dto.BookItemsGet { router := testutils.TestSetup() u, err := url.Parse("/ws/search/" + searchterm) @@ -137,6 +138,7 @@ func testSearchBook(t *testing.T, searchterm string, limit string, offset string q := u.Query() q.Set("lang", "fr") + q.Set("inventaire", strconv.Itoa(int(inventaireSearchType))) u.RawQuery = q.Encode() token := testutils.ConnectDemoUser(router) diff --git a/internal/dto/in.go b/internal/dto/in.go index fecd451..10fc867 100644 --- a/internal/dto/in.go +++ b/internal/dto/in.go @@ -5,9 +5,17 @@ type AuthorGet struct { Description string `json:"description"` } +type InventaireSearchType int + +const ( + NoInventaireSearch InventaireSearchType = iota + InventaireIfNothingFound + ForceInventaireSearch +) + type BookSearchGetParam struct { - Lang string `form:"lang" binding:"max=5"` - Inventaire bool `form:"inventaire"` + Lang string `form:"lang" binding:"max=5"` + Inventaire InventaireSearchType `form:"inventaire"` } type BookFields struct { @@ -39,6 +47,10 @@ type CollectionFields struct { Name string `json:"name" binding:"required,max=300"` } +type CollectionBook struct { + BookID uint `json:"bookId" binding:"required"` +} + type FileInfoPost struct { FileID uint `json:"fileId"` FilePath string `json:"filepath"` diff --git a/internal/myvalidator/myvalidator.go b/internal/myvalidator/myvalidator.go index 8ca8550..5b8ab41 100644 --- a/internal/myvalidator/myvalidator.go +++ b/internal/myvalidator/myvalidator.go @@ -57,6 +57,9 @@ func ReturnErrorsAsJsonResponse(ac *appcontext.AppContext, err error) { ac.C.JSON(httpError.StatusCode, gin.H{"error": httpError.Err.Error()}) return } + if errors.Is(err, gorm.ErrRecordNotFound) { + ac.C.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + } ac.C.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } diff --git a/internal/routes/booksearchget.go b/internal/routes/booksearchget.go index 5908ccc..3f79230 100644 --- a/internal/routes/booksearchget.go +++ b/internal/routes/booksearchget.go @@ -38,7 +38,7 @@ func GetSearchBooksHandler(ac appcontext.AppContext) { return } var returnedBooks dto.BookItemsGet - if !params.Inventaire { + if params.Inventaire != dto.ForceInventaireSearch { books, err := query.FetchBookSearchGet(ac.Db, user.ID, searchterm, limit, offset) if err != nil { myvalidator.ReturnErrorsAsJsonResponse(&ac, err) @@ -51,7 +51,7 @@ func GetSearchBooksHandler(ac appcontext.AppContext) { } returnedBooks = dto.BookItemsGet{Count: count, Inventaire: false, Books: books} } - if params.Inventaire || len(returnedBooks.Books) == 0 { + if (params.Inventaire == dto.InventaireIfNothingFound && len(returnedBooks.Books) == 0) || (params.Inventaire == dto.ForceInventaireSearch) { returnedBooksPtr, err := searchInInventaireAPI(ac.Config.InventaireUrl, searchterm, limit, offset, params) if err != nil { myvalidator.ReturnErrorsAsJsonResponse(&ac, err) diff --git a/internal/routes/collectionaddbookpost.go b/internal/routes/collectionaddbookpost.go new file mode 100644 index 0000000..f93d66d --- /dev/null +++ b/internal/routes/collectionaddbookpost.go @@ -0,0 +1,62 @@ +package routes + +import ( + "errors" + "net/http" + "strconv" + + "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" +) + +func PostCollectionBookHandler(ac appcontext.AppContext) { + collectionId, err := strconv.ParseUint(ac.C.Param("id"), 10, 64) + if err != nil { + ac.C.JSON(http.StatusBadRequest, gin.H{"error": err}) + return + } + + user, err := ac.GetAuthenticatedUser() + if err != nil { + myvalidator.ReturnErrorsAsJsonResponse(&ac, err) + return + } + + var collection model.Collection + err = ac.Db.First(&collection, collectionId).Error + if err != nil { + myvalidator.ReturnErrorsAsJsonResponse(&ac, err) + return + } + + if collection.UserID != user.ID { + err := myvalidator.HttpError{ + StatusCode: http.StatusUnauthorized, + Err: errors.New(i18nresource.GetTranslatedMessage(&ac, "Unauthorized")), + } + myvalidator.ReturnErrorsAsJsonResponse(&ac, err) + return + } + + var collectionBook dto.CollectionBook + err = ac.C.ShouldBindJSON(&collectionBook) + if err != nil { + myvalidator.ReturnErrorsAsJsonResponse(&ac, err) + return + } + + var book model.Book + err = ac.Db.First(&book, collectionBook.BookID).Error + if err != nil { + myvalidator.ReturnErrorsAsJsonResponse(&ac, err) + return + } + + collection.Books = append(collection.Books, book) + ac.Db.Save(&collection) + ac.C.String(http.StatusOK, "Success") +} diff --git a/internal/setup/setup.go b/internal/setup/setup.go index a63c7b0..3b78a4c 100644 --- a/internal/setup/setup.go +++ b/internal/setup/setup.go @@ -84,6 +84,9 @@ func Setup(config *config.Config) *gin.Engine { ws.GET("/collection/:id", func(c *gin.Context) { routes.GetCollectionHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config}) }) + ws.POST("/collection/:id/addbook", func(c *gin.Context) { + routes.PostCollectionBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config}) + }) ws.POST("/collection", func(c *gin.Context) { routes.PostCollectionHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config}) })
{{ titleError }}