Collections: allow to drag and drop to change book position
This commit is contained in:
@@ -28,6 +28,7 @@ func CollectionItemsQueryToDto(itemsQueryResult []query.CollectionItemQueryResul
|
||||
}
|
||||
dtoItems = append(dtoItems,
|
||||
dto.CollectionItemGet{
|
||||
ID: queryResult.ItemID,
|
||||
Position: queryResult.Position,
|
||||
Book: bookItem,
|
||||
})
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
func TestFetchAllCollections_OK(t *testing.T) {
|
||||
status, res := testFetchCollections(t, "10", "0")
|
||||
assert.Equal(t, http.StatusOK, status)
|
||||
assert.Equal(t, int64(3), res.Count)
|
||||
assert.Equal(t, 3, len(res.Collections))
|
||||
assert.Equal(t, int64(4), res.Count)
|
||||
assert.Equal(t, 4, len(res.Collections))
|
||||
}
|
||||
|
||||
func testFetchCollections(t *testing.T, limit string, offset string) (int, dto.CollectionItemsGet) {
|
||||
|
||||
113
internal/apitest/post_collection_changeposition_test.go
Normal file
113
internal/apitest/post_collection_changeposition_test.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package apitest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.artlef.fr/bibliomane/internal/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPostCollectionChangePositionHandler_PositionOk(t *testing.T) {
|
||||
payload :=
|
||||
`{
|
||||
"itemId": 14,
|
||||
"position": 2
|
||||
}`
|
||||
collectionId := "5"
|
||||
status := testPostCollectionChangePositionHandler(collectionId, payload)
|
||||
assert.Equal(t, http.StatusOK, status)
|
||||
_, collection := testGetCollection(t, collectionId, "10", "0")
|
||||
assert.Equal(t, uint(1), collection.Items[0].Position)
|
||||
assert.Equal(t, uint(2), collection.Items[1].Position)
|
||||
assert.Equal(t, uint(3), collection.Items[2].Position)
|
||||
assert.Equal(t, uint(4), collection.Items[3].Position)
|
||||
}
|
||||
|
||||
func TestPostCollectionChangePositionHandler_ChangeOtherElement(t *testing.T) {
|
||||
payload :=
|
||||
`{
|
||||
"itemId": 17,
|
||||
"position": 3
|
||||
}`
|
||||
collectionId := "5"
|
||||
status := testPostCollectionChangePositionHandler(collectionId, payload)
|
||||
assert.Equal(t, http.StatusOK, status)
|
||||
_, collection := testGetCollection(t, collectionId, "10", "0")
|
||||
assert.Equal(t, "Duo", collection.Items[3].Book.Title)
|
||||
}
|
||||
|
||||
func TestPostCollectionChangePositionHandler_LastPosition(t *testing.T) {
|
||||
payload :=
|
||||
`{
|
||||
"itemId": 19,
|
||||
"position": 546
|
||||
}`
|
||||
collectionId := "5"
|
||||
status := testPostCollectionChangePositionHandler(collectionId, payload)
|
||||
assert.Equal(t, http.StatusOK, status)
|
||||
_, collection := testGetCollection(t, collectionId, "10", "0")
|
||||
assert.Equal(t, "Recherches philosophiques", collection.Items[7].Book.Title)
|
||||
assert.Equal(t, "Le château", collection.Items[6].Book.Title)
|
||||
}
|
||||
|
||||
func TestPostCollectionChangePositionHandler_FirstPosition(t *testing.T) {
|
||||
payload :=
|
||||
`{
|
||||
"itemId": 16,
|
||||
"position": 1
|
||||
}`
|
||||
collectionId := "5"
|
||||
status := testPostCollectionChangePositionHandler(collectionId, payload)
|
||||
assert.Equal(t, http.StatusOK, status)
|
||||
_, collection := testGetCollection(t, collectionId, "10", "0")
|
||||
assert.Equal(t, "Duo", collection.Items[0].Book.Title)
|
||||
}
|
||||
|
||||
func TestPostCollectionChangePositionHandler_WrongPosition(t *testing.T) {
|
||||
payload :=
|
||||
`{
|
||||
"itemId": 9,
|
||||
"position": 0
|
||||
}`
|
||||
collectionId := "5"
|
||||
status := testPostCollectionChangePositionHandler(collectionId, payload)
|
||||
assert.Equal(t, http.StatusBadRequest, status)
|
||||
}
|
||||
|
||||
func TestPostCollectionChangePositionHandler_MissingPosition(t *testing.T) {
|
||||
payload :=
|
||||
`{
|
||||
"itemId": 9
|
||||
}`
|
||||
collectionId := "5"
|
||||
status := testPostCollectionChangePositionHandler(collectionId, payload)
|
||||
assert.Equal(t, http.StatusBadRequest, status)
|
||||
}
|
||||
|
||||
func TestPostCollectionChangePositionWrongCollection(t *testing.T) {
|
||||
payload :=
|
||||
`{
|
||||
"itemId": 1,
|
||||
"position": 9
|
||||
}`
|
||||
collectionId := "5"
|
||||
status := testPostCollectionChangePositionHandler(collectionId, payload)
|
||||
assert.Equal(t, http.StatusInternalServerError, status)
|
||||
}
|
||||
|
||||
func testPostCollectionChangePositionHandler(collectionId string, payload string) int {
|
||||
router := testutils.TestSetup()
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
token := testutils.ConnectDemoUser(router)
|
||||
req, _ := http.NewRequest("POST", "/ws/collection/"+collectionId+"/changeposition",
|
||||
strings.NewReader(payload))
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
return w.Code
|
||||
}
|
||||
@@ -51,6 +51,11 @@ type CollectionBook struct {
|
||||
BookID uint `json:"bookId" binding:"required"`
|
||||
}
|
||||
|
||||
type CollectionItemPosition struct {
|
||||
Position uint `json:"position" binding:"required,gte=1"`
|
||||
ItemID uint `json:"itemId" binding:"required"`
|
||||
}
|
||||
|
||||
type FileInfoPost struct {
|
||||
FileID uint `json:"fileId"`
|
||||
FilePath string `json:"filepath"`
|
||||
|
||||
@@ -51,6 +51,7 @@ type CollectionGet struct {
|
||||
}
|
||||
|
||||
type CollectionItemGet struct {
|
||||
ID uint `json:"id"`
|
||||
Position uint `json:"position"`
|
||||
Book BookItemGet `json:"book"`
|
||||
}
|
||||
|
||||
@@ -10,3 +10,4 @@ RegistrationDisabled = "Registration has been disabled on this instance."
|
||||
UserAlreadyExists = "An user with this name already exists."
|
||||
ErrorWhenCreatingUserFromStr = "Error when creating user from string %s"
|
||||
Unauthorized = "You are not allowed to access this document."
|
||||
ItemDoesNotBelongToCollection = "Item does not belong to the collection."
|
||||
|
||||
@@ -10,3 +10,4 @@ RegistrationDisabled = "La création de nouveaux comptes a été désactivée su
|
||||
UserAlreadyExists = "Un utilisateur avec le même nom existe déjà."
|
||||
ErrorWhenCreatingUserFromStr = "Erreur lors de la création de l'utilisateur %s"
|
||||
Unauthorized = "Vous n'êtes pas autorisé à accéder à cette page."
|
||||
ItemDoesNotBelongToCollection = "Cet élément n'appartient pas à la liste."
|
||||
|
||||
@@ -81,6 +81,7 @@ func fetchCollections(db *gorm.DB, userId uint) *gorm.DB {
|
||||
}
|
||||
|
||||
type CollectionItemQueryResult struct {
|
||||
ItemID uint
|
||||
Position uint
|
||||
ID uint
|
||||
Title string
|
||||
@@ -113,7 +114,7 @@ func FetchCollectionBooksCount(db *gorm.DB, userId uint, collectionId uint) (int
|
||||
|
||||
func fetchCollectionBooksQuery(db *gorm.DB, userId uint, collectionId uint) *gorm.DB {
|
||||
query := db.Model(&model.CollectionItem{})
|
||||
query = query.Select("collection_items.position, " + selectBookItem())
|
||||
query = query.Select("collection_items.position, collection_items.ID as item_id, " + selectBookItem())
|
||||
query = query.Joins("left join collections on (collection_items.collection_id = collections.id)")
|
||||
query = query.Joins("left join books on (books.id = collection_items.book_id)")
|
||||
query = joinAuthors(query)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"git.artlef.fr/bibliomane/internal/model"
|
||||
"git.artlef.fr/bibliomane/internal/myvalidator"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func PostCollectionBookHandler(ac appcontext.AppContext) {
|
||||
@@ -56,7 +57,16 @@ func PostCollectionBookHandler(ac appcontext.AppContext) {
|
||||
return
|
||||
}
|
||||
|
||||
item := model.CollectionItem{Position: 0, BookID: book.ID, CollectionID: collection.ID}
|
||||
//reorder other items
|
||||
q := ac.Db.Model(&model.CollectionItem{})
|
||||
q = q.Where("collection_id = ?", collection.ID)
|
||||
err = q.UpdateColumn("position", gorm.Expr("position + 1")).Error
|
||||
if err != nil {
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
return
|
||||
}
|
||||
|
||||
item := model.CollectionItem{Position: 1, BookID: book.ID, CollectionID: collection.ID}
|
||||
ac.Db.Save(&item)
|
||||
ac.C.String(http.StatusOK, "Success")
|
||||
}
|
||||
|
||||
106
internal/routes/collectionchangepositionpost.go
Normal file
106
internal/routes/collectionchangepositionpost.go
Normal file
@@ -0,0 +1,106 @@
|
||||
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"
|
||||
"git.artlef.fr/bibliomane/internal/query"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func PostCollectionChangePositionHandler(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 collectionBookPosition dto.CollectionItemPosition
|
||||
err = ac.C.ShouldBindJSON(&collectionBookPosition)
|
||||
if err != nil {
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
return
|
||||
}
|
||||
|
||||
var item model.CollectionItem
|
||||
err = ac.Db.First(&item, collectionBookPosition.ItemID).Error
|
||||
if err != nil {
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
return
|
||||
}
|
||||
|
||||
if collection.ID != item.CollectionID {
|
||||
err := myvalidator.HttpError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Err: errors.New(i18nresource.GetTranslatedMessage(&ac, "ItemDoesNotBelongToCollection")),
|
||||
}
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
return
|
||||
}
|
||||
count, err := query.FetchCollectionBooksCount(ac.Db, user.ID, item.CollectionID)
|
||||
if err != nil {
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
return
|
||||
}
|
||||
newPosition := collectionBookPosition.Position
|
||||
if int64(newPosition) > count {
|
||||
newPosition = uint(count)
|
||||
}
|
||||
|
||||
if item.Position == collectionBookPosition.Position {
|
||||
//nothing to do
|
||||
ac.C.String(http.StatusOK, "Success")
|
||||
return
|
||||
}
|
||||
lowerPosition := item.Position + 1
|
||||
higherPosition := item.Position - 1
|
||||
operationToDo := ""
|
||||
if item.Position < collectionBookPosition.Position {
|
||||
higherPosition = collectionBookPosition.Position
|
||||
operationToDo = "position - 1"
|
||||
} else {
|
||||
lowerPosition = collectionBookPosition.Position
|
||||
operationToDo = "position + 1"
|
||||
}
|
||||
|
||||
q := ac.Db.Model(&model.CollectionItem{})
|
||||
q = q.Where("collection_id = ? AND position BETWEEN ? AND ?", collection.ID, lowerPosition, higherPosition)
|
||||
err = q.UpdateColumn("position", gorm.Expr(operationToDo)).Error
|
||||
if err != nil {
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
return
|
||||
}
|
||||
|
||||
item.Position = collectionBookPosition.Position
|
||||
ac.Db.Save(&item)
|
||||
ac.C.String(http.StatusOK, "Success")
|
||||
}
|
||||
@@ -87,6 +87,9 @@ func Setup(config *config.Config) *gin.Engine {
|
||||
ws.POST("/collection/:id/addbook", func(c *gin.Context) {
|
||||
routes.PostCollectionBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||
})
|
||||
ws.POST("/collection/:id/changeposition", func(c *gin.Context) {
|
||||
routes.PostCollectionChangePositionHandler(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})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user