Add a view to see all books in a collection

This commit is contained in:
2026-04-03 22:57:45 +02:00
parent a5c4c0bbec
commit 625d2a2af1
20 changed files with 285 additions and 44 deletions

View File

@@ -10,7 +10,17 @@ import (
"gorm.io/gorm"
)
func CollectionQueryToDto(collectionsQueryResult []query.CollectionsQueryResult) []dto.CollectionItemGet {
func CollectionQueryToCollectionDto(collectionsQueryResult []query.CollectionsQueryResult) dto.CollectionGet {
var collection dto.CollectionGet
for _, collectionsDb := range collectionsQueryResult {
collection.Name = collectionsDb.Name
collection.Books = append(collection.Books, collectionDbToBookItem(&collectionsDb))
collection.UserID = collectionsDb.UserID
}
return collection
}
func CollectionQueryToCollectionItemDto(collectionsQueryResult []query.CollectionsQueryResult) []dto.CollectionItemGet {
var collections []dto.CollectionItemGet
for _, collectionDb := range collectionsQueryResult {
i := findIdInCollection(collections, collectionDb.ID)
@@ -22,15 +32,19 @@ func CollectionQueryToDto(collectionsQueryResult []query.CollectionsQueryResult)
//current collection is the last element
i = len(collections) - 1
}
collections[i].Books = append(collections[i].Books, dto.CollectionBookItemGet{
ID: collectionDb.BookId,
Title: collectionDb.BookTitle,
CoverPath: collectionDb.CoverPath,
})
collections[i].Books = append(collections[i].Books, collectionDbToBookItem(&collectionDb))
}
return collections
}
func collectionDbToBookItem(collectionDb *query.CollectionsQueryResult) dto.CollectionBookItemGet {
return dto.CollectionBookItemGet{
ID: collectionDb.BookId,
Title: collectionDb.BookTitle,
CoverPath: collectionDb.CoverPath,
}
}
// returns the position in collections, -1 if not found
func findIdInCollection(collections []dto.CollectionItemGet, collectionId uint) int {
for i, collection := range collections {

View File

@@ -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(2), res.Count)
assert.Equal(t, 2, len(res.Collections))
}
func testFetchCollections(t *testing.T, limit string, offset string) (int, dto.CollectionItemsGet) {

View File

@@ -1,10 +1,7 @@
package apitest
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"git.artlef.fr/bibliomane/internal/dto"
@@ -61,20 +58,8 @@ func TestGetBook_IdNotInt(t *testing.T) {
testGetBook(t, "wrong", http.StatusBadRequest)
}
func testGetBook(t *testing.T, id string, status int) dto.FullBookGet {
router := testutils.TestSetup()
token := testutils.ConnectDemoUser(router)
req, _ := http.NewRequest("GET", "/ws/book/"+id, nil)
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
var book dto.FullBookGet
err := json.Unmarshal(w.Body.Bytes(), &book)
if err != nil {
t.Error(err)
}
assert.Equal(t, status, w.Code)
func testGetBook(t *testing.T, id string, expectedStatus int) dto.FullBookGet {
status, book := testutils.TestFetchOneModel[dto.FullBookGet](t, "/ws/book", id)
assert.Equal(t, expectedStatus, status)
return book
}

View File

@@ -0,0 +1,34 @@
package apitest
import (
"net/http"
"testing"
"git.artlef.fr/bibliomane/internal/dto"
"git.artlef.fr/bibliomane/internal/testutils"
"github.com/stretchr/testify/assert"
)
func TestGetCollection_Ok(t *testing.T) {
status, collection := testGetCollection(t, "1", "10", "0")
assert.Equal(t, http.StatusOK, status)
assert.Equal(t, "Littérature française", collection.Name)
assert.Equal(t, 6, len(collection.Books))
}
func TestGetCollection_Limit(t *testing.T) {
status, collection := testGetCollection(t, "2", "3", "0")
assert.Equal(t, http.StatusOK, status)
assert.Equal(t, "Nouvelles", collection.Name)
assert.Equal(t, 3, len(collection.Books))
assert.Equal(t, int64(4), collection.Count)
}
func TestGetCollection_Unauthorized(t *testing.T) {
status, _ := testGetCollection(t, "4", "10", "0")
assert.Equal(t, http.StatusUnauthorized, status)
}
func testGetCollection(t *testing.T, id string, limit string, offset string) (int, dto.CollectionGet) {
return testutils.TestFetchModel[dto.CollectionGet](t, "/ws/collection/"+id, limit, offset)
}

View File

@@ -44,6 +44,13 @@ type BookItemGet struct {
CoverPath string `json:"coverPath"`
}
type CollectionGet struct {
Name string `json:"name"`
Count int64 `json:"count"`
Books []CollectionBookItemGet `json:"books"`
UserID uint `json:"-"`
}
type CollectionItemsGet struct {
Count int64 `json:"count"`
Collections []CollectionItemGet `json:"collections"`

View File

@@ -9,6 +9,4 @@ ValidationPropertyFail = "Validation failed for '%s' property."
RegistrationDisabled = "Registration has been disabled on this instance."
UserAlreadyExists = "An user with this name already exists."
ErrorWhenCreatingUserFromStr = "Error when creating user from string %s"
ISBNNotFoundBabelio = "ISBN %s not found on babelio."
BabelioParseError = "Error when parsing babelio."
BabelioFetchDescError = "Error when fetching description on babelio."
Unauthorized = "You are not allowed to access this document."

View File

@@ -9,6 +9,4 @@ ValidationPropertyFail = "La validation a échoué pour la propriété '%s'."
RegistrationDisabled = "La création de nouveaux comptes a été désactivée sur cette instance."
UserAlreadyExists = "Un utilisateur avec le même nom existe déjà."
ErrorWhenCreatingUserFromStr = "Erreur lors de la création de l'utilisateur %s"
ISBNNotFoundBabelio = "L'ISBN %s n'est pas sur babelio."
BabelioParseError = "Erreur en parsant babelio."
BabelioFetchDescError = "Erreur lors de la récupération de la description sur babelio."
Unauthorized = "Vous n'êtes pas autorisé à accéder à cette page."

View File

@@ -186,6 +186,7 @@ func selectBookItem() string {
type CollectionsQueryResult struct {
ID uint
UserID uint
Name string
BookId uint
BookTitle string
@@ -204,7 +205,8 @@ func FetchAllCollections(db *gorm.DB, userId uint, limit int, offset int) ([]Col
return collections, res.Error
}
for _, collectionId := range collectionIds {
queryResults, err := queryBooksForCollection(db, collectionId.ID)
//only takes first 5 books
queryResults, err := FetchCollectionBooks(db, collectionId.ID, 5, 0)
if err != nil {
return collections, res.Error
}
@@ -213,18 +215,29 @@ func FetchAllCollections(db *gorm.DB, userId uint, limit int, offset int) ([]Col
return collections, res.Error
}
func queryBooksForCollection(db *gorm.DB, collectionId uint) ([]CollectionsQueryResult, error) {
func FetchCollectionBooks(db *gorm.DB, collectionId uint, limit int, offset int) ([]CollectionsQueryResult, error) {
var collections []CollectionsQueryResult
query := fetchCollectionBooksQuery(db, collectionId)
query = query.Limit(limit)
query = query.Offset(offset)
res := query.Find(&collections)
return collections, res.Error
}
func FetchCollectionBooksCount(db *gorm.DB, collectionId uint) (int64, error) {
var count int64
res := fetchCollectionBooksQuery(db, collectionId).Count(&count)
return count, res.Error
}
func fetchCollectionBooksQuery(db *gorm.DB, collectionId uint) *gorm.DB {
query := db.Model(&model.Collection{})
query = query.Select("collections.id, collections.name, books.id as book_id, books.title as book_title, " + selectStaticFilesPath())
query = query.Select("collections.id, collections.user_id, collections.name, books.id as book_id, books.title as book_title, " + selectStaticFilesPath())
query = query.Joins("left join collection_books on (collection_books.collection_id = collections.id)")
query = query.Joins("left join books on (books.id = collection_books.book_id)")
query = joinStaticFiles(query)
query = query.Where("collections.id = ?", collectionId)
//only takes first 5 books
query = query.Limit(5)
res := query.Find(&collections)
return collections, res.Error
return query
}
func FetchAllCollectionsCount(db *gorm.DB, userId uint) (int64, error) {

View File

@@ -0,0 +1,61 @@
package routes
import (
"errors"
"net/http"
"strconv"
"git.artlef.fr/bibliomane/internal/adapter"
"git.artlef.fr/bibliomane/internal/appcontext"
"git.artlef.fr/bibliomane/internal/i18nresource"
"git.artlef.fr/bibliomane/internal/myvalidator"
"git.artlef.fr/bibliomane/internal/query"
"github.com/gin-gonic/gin"
)
func GetCollectionHandler(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
}
limit, err := ac.GetQueryLimit()
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
offset, err := ac.GetQueryOffset()
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
collectionBooksDb, err := query.FetchCollectionBooks(ac.Db, uint(collectionId), limit, offset)
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
collection := adapter.CollectionQueryToCollectionDto(collectionBooksDb)
count, err := query.FetchCollectionBooksCount(ac.Db, uint(collectionId))
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
collection.Count = count
if collection.UserID != user.ID {
err := myvalidator.HttpError{
StatusCode: http.StatusUnauthorized,
Err: errors.New(i18nresource.GetTranslatedMessage(&ac, "Unauthorized")),
}
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
} else {
ac.C.JSON(http.StatusOK, collection)
}
}

View File

@@ -31,7 +31,7 @@ func GetCollectionsHandler(ac appcontext.AppContext) {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
collections := adapter.CollectionQueryToDto(collectionsDb)
collections := adapter.CollectionQueryToCollectionItemDto(collectionsDb)
count, err := query.FetchAllCollectionsCount(ac.Db, user.ID)
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)

View File

@@ -81,6 +81,9 @@ func Setup(config *config.Config) *gin.Engine {
ws.GET("/collections", func(c *gin.Context) {
routes.GetCollectionsHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
})
ws.GET("/collection/:id", func(c *gin.Context) {
routes.GetCollectionHandler(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})
})

View File

@@ -68,6 +68,23 @@ func TestBookPutCallWithDemoPayload(t *testing.T, payload string, bookId string,
}
}
func TestFetchOneModel[T any](t *testing.T, urlpath string, id string) (int, T) {
router := TestSetup()
token := ConnectDemoUser(router)
req, _ := http.NewRequest("GET", urlpath+"/"+id, nil)
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
var result T
err := json.Unmarshal(w.Body.Bytes(), &result)
if err != nil {
t.Error(err)
}
return w.Code, result
}
func TestFetchModel[T any](t *testing.T, urlpath string, limit string, offset string) (int, T) {
router := TestSetup()