diff --git a/front/public/defaultinventairebook.png b/front/public/defaultinventairebook.png new file mode 100644 index 0000000..d76c53f Binary files /dev/null and b/front/public/defaultinventairebook.png differ diff --git a/front/src/BookListElement.vue b/front/src/BookListElement.vue index c9812dc..4a0c378 100644 --- a/front/src/BookListElement.vue +++ b/front/src/BookListElement.vue @@ -32,7 +32,7 @@ function openBook() { if (props.id != 0) { router.push(`/book/${props.id}`); } else { - router.push(`/importinventaire/${props.inventaireid}`) + router.push(`/import/inventaire/${props.inventaireid}`) } } diff --git a/front/src/ImportInventaire.vue b/front/src/ImportInventaire.vue new file mode 100644 index 0000000..9d2353c --- /dev/null +++ b/front/src/ImportInventaire.vue @@ -0,0 +1,68 @@ + + + + + + diff --git a/front/src/ImportListElement.vue b/front/src/ImportListElement.vue new file mode 100644 index 0000000..9abb5d0 --- /dev/null +++ b/front/src/ImportListElement.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/front/src/api.js b/front/src/api.js index 7871494..c0ee8b0 100644 --- a/front/src/api.js +++ b/front/src/api.js @@ -2,9 +2,17 @@ import { useAuthStore } from './auth.store.js' const baseUrl = "http://localhost:8080" +export function getInventaireImagePathOrDefault(path) { + return getImagePathOrGivenDefault(path, "../../defaultinventairebook.png") +} + export function getImagePathOrDefault(path) { + return getImagePathOrGivenDefault(path, "../defaultbook.png") +} + +export function getImagePathOrGivenDefault(path, defaultpath) { if (path == "" || typeof path === 'undefined') { - return "../defaultbook.png"; + return defaultpath; } else if (path.startsWith("https://")) { return path; } else { @@ -38,6 +46,11 @@ export function getSearchBooks(data, error, searchterm, lang, limit, offset) { return useFetch(data, error, baseUrl + '/search/' + encodeURIComponent(searchterm) + "?" + queryParams.toString()); } +export function getInventaireEditionBooks(data, error, inventaireId, lang, limit, offset) { + const queryParams = new URLSearchParams({lang: lang, limit: limit, offset: offset}); + return useFetch(data, error, baseUrl + '/inventaire/books/' + encodeURIComponent(inventaireId) + "?" + queryParams.toString()); +} + export function getAuthor(data, error, id) { return useFetch(data, error, baseUrl + '/author/' + id); } diff --git a/front/src/locales/en.json b/front/src/locales/en.json index c533d07..2588da8 100644 --- a/front/src/locales/en.json +++ b/front/src/locales/en.json @@ -58,8 +58,16 @@ "goto":"Goto page {pageNumber}", "page":"Page {pageNumber}" }, - "bookdatewidget" :{ + "bookdatewidget": { "started": "Started at :", "finished": "Finished at :" + }, + "importinventaire": { + "title":"Please select a book to import" + }, + "importlistelement": { + "releasedate":"Release date:", + "publisher":"Publisher:" } + } diff --git a/front/src/locales/fr.json b/front/src/locales/fr.json index 9abb105..5b30cdf 100644 --- a/front/src/locales/fr.json +++ b/front/src/locales/fr.json @@ -58,8 +58,15 @@ "goto":"Aller à la page {pageNumber}", "page":"Page {pageNumber}" }, - "bookdatewidget" :{ + "bookdatewidget": { "started": "Commencé le :", "finished": "Fini le :" + }, + "importinventaire": { + "title":"Sélectionner l'édition à importer" + }, + "importlistelement": { + "releasedate":"Date de publication : ", + "publisher":"Maison d'édition : " } } diff --git a/front/src/router.js b/front/src/router.js index c3af796..676013c 100644 --- a/front/src/router.js +++ b/front/src/router.js @@ -8,6 +8,7 @@ import SignUp from './SignUp.vue' import LogIn from './LogIn.vue' import Home from './Home.vue' import SearchBook from './SearchBook.vue' +import ImportInventaire from './ImportInventaire.vue' import InventaireImport from './InventaireImport.vue' import { useAuthStore } from './auth.store' @@ -18,6 +19,7 @@ const routes = [ { path: '/importinventaire/:inventaireid', component: InventaireImport, props: true }, { path: '/author/:id', component: AuthorForm, props: true }, { path: '/search/:searchterm', component: SearchBook, props: true }, + { path: '/import/inventaire/:inventaireid', component: ImportInventaire, props: true }, { path: '/add', component: AddBook }, { path: '/signup', component: SignUp }, { path: '/login', component: LogIn }, diff --git a/internal/inventaire/inventaire.go b/internal/inventaire/inventaire.go index c835bad..3ea5ffe 100644 --- a/internal/inventaire/inventaire.go +++ b/internal/inventaire/inventaire.go @@ -12,7 +12,7 @@ type InventaireSearchResult struct { } type InventaireSearchBook struct { - ID string `json:"id"` + ID string `json:"uri"` Label string `json:"label"` Description string `json:"description"` Image string `json:"image"` diff --git a/internal/inventaire/inventaire_test.go b/internal/inventaire/inventaire_test.go index be289c8..a3f0bdc 100644 --- a/internal/inventaire/inventaire_test.go +++ b/internal/inventaire/inventaire_test.go @@ -1,6 +1,7 @@ package inventaire import ( + "sort" "testing" "github.com/stretchr/testify/assert" @@ -53,3 +54,56 @@ func TestCallInventaireBook_BraveNewWorld(t *testing.T) { assert.Equal(t, "Aldous Huxley", result.Author.Name) assert.Equal(t, "écrivain, romancier et philosophe britannique (1894–1963)", result.Author.Description) } + +func TestCallInventaireEdition_TestLimit(t *testing.T) { + result, err := CallInventaireEdition("wd:Q339761", "fr", 10, 0) + if err != nil { + t.Error(err) + } + assert.Equal(t, int64(34), result.Count) + assert.Equal(t, 10, len(result.Results)) +} + +func TestCallInventaireEdition_TestOffset(t *testing.T) { + result, err := CallInventaireEdition("wd:Q3213142", "fr", 0, 0) + if err != nil { + t.Error(err) + } + assert.Equal(t, 3, len(result.Results)) + sortedResults := result.Results + sort.Slice(sortedResults, func(i, j int) bool { + return sortedResults[i].Id > sortedResults[j].Id + }) + + assert.Equal(t, + InventaireEditionResultBook{ + Id: "isbn:9782072525216", + Title: "La théorie de l'information", + ISBN: "978-2-07-252521-6", + ReleaseDate: "", + Image: "https://inventaire.io/img/entities/fac578440d9bf7afc7f4c5698aa618b8a4d80d21", + Lang: "fr", + }, + result.Results[0]) + assert.Equal(t, + InventaireEditionResultBook{ + Id: "isbn:9782070456260", + Title: "La théorie de l'information", + ISBN: "978-2-07-045626-0", + ReleaseDate: "2014", + Image: "https://inventaire.io/img/entities/5044c2265cc42675ac4335387aef189862cbeec1", + Lang: "fr", + }, + result.Results[1]) + assert.Equal(t, + InventaireEditionResultBook{ + Id: "isbn:9782070138098", + Title: "La théorie de l'information", + ISBN: "978-2-07-013809-8", + Publisher: "Éditions Gallimard", + ReleaseDate: "2012", + Image: "https://inventaire.io/img/entities/a7b9d05c041b98e98c2f429e11cb2b424d78223b", + Lang: "fr", + }, + result.Results[2]) +} diff --git a/internal/inventaire/inventaireedition.go b/internal/inventaire/inventaireedition.go new file mode 100644 index 0000000..6c2d41a --- /dev/null +++ b/internal/inventaire/inventaireedition.go @@ -0,0 +1,240 @@ +package inventaire + +import ( + "encoding/json" + "math" + "strings" + + "git.artlef.fr/PersonalLibraryManager/internal/callapiutils" +) + +type InventaireEditionResult struct { + Results []InventaireEditionResultBook `json:"results"` + Count int64 `json:"count"` +} + +type InventaireEditionResultBook struct { + Id string `json:"uri"` + Title string `json:"title"` + ISBN string `json:"isbn"` + Publisher string `json:"publisher"` + ReleaseDate string `json:"date"` + Image string `json:"image"` + Lang string `json:"lang"` +} + +type inventaireReverseClaimsResult struct { + Uris []string `json:"uris"` +} + +type inventaireEditionQueryResult struct { + Entities []inventaireEditionQueryEntity +} + +type inventaireEditionQueryEntity struct { + WdId string + EditionId string + Title string + ISBN string + Uri string + Image string + Lang string + ReleaseDate string +} + +func (i *inventaireEditionQueryResult) UnmarshalJSON(b []byte) error { + var parsed struct { + Entities map[string]json.RawMessage `json:"entities"` + } + err := json.Unmarshal(b, &parsed) + if err != nil { + return err + } + for _, entity := range parsed.Entities { + var parsedEntity struct { + WdId string `json:"wdId"` + Labels map[string]json.RawMessage `json:"labels"` + Type string `json:"type"` + Uri string `json:"uri"` + Image map[string]json.RawMessage `json:"image"` + Lang string `json:"originalLang"` + Claims map[string]json.RawMessage `json:"claims"` + } + err = json.Unmarshal(entity, &parsedEntity) + if err != nil { + return err + } + if parsedEntity.Type == "edition" { + editionId, err := parseStringArrayFieldInJsonRaw(parsedEntity.Claims, "wdt:P123") + if err != nil { + return err + } + releaseDate, err := parseStringArrayFieldInJsonRaw(parsedEntity.Claims, "wdt:P577") + if err != nil { + return err + } + isbn, err := parseStringArrayFieldInJsonRaw(parsedEntity.Claims, "wdt:P212") + if err != nil { + return err + } + label, err := findLangageField(parsedEntity.Labels, "fromclaims") + if err != nil { + return err + } + image := "" + imageFieldToParse, ok := parsedEntity.Image["url"] + if ok { + err := json.Unmarshal(imageFieldToParse, &image) + if err != nil { + return err + } + if image != "" { + image = GetBaseInventaireUrl() + image + } + } + i.Entities = append(i.Entities, inventaireEditionQueryEntity{ + WdId: parsedEntity.WdId, + EditionId: editionId, + Title: label, + ISBN: isbn, + Uri: parsedEntity.Uri, + Image: image, + Lang: parsedEntity.Lang, + ReleaseDate: releaseDate, + }) + } + } + return err +} + +func parseStringArrayFieldInJsonRaw(jsonRawMap map[string]json.RawMessage, key string) (string, error) { + fieldToParse, ok := jsonRawMap[key] + if !ok { + return "", nil + } + var fieldArray []string + s := "" + err := json.Unmarshal(fieldToParse, &fieldArray) + if err != nil { + return s, err + } + if len(fieldArray) > 0 { + s = fieldArray[0] + } + return s, err +} + +func CallInventaireEdition(inventaireId string, lang string, limit int, offset int) (InventaireEditionResult, error) { + var queryResult InventaireEditionResult + uris, err := callInventaireUris(inventaireId) + if err != nil { + return queryResult, err + } + queryResult.Count = int64(len(uris.Uris)) + limitedUris := uris.Uris + if limit != 0 { + l := len(uris.Uris) + startIndex := int(math.Min(float64(offset), float64(l))) + endIndex := int(math.Min(float64(limit+offset), float64(l))) + limitedUris = uris.Uris[startIndex:endIndex] + } + editionEntities, err := callInventaireEditionEntities(limitedUris) + + if err != nil { + return queryResult, err + } + + for _, entity := range editionEntities.Entities { + publisher := "" + if entity.EditionId != "" { + publisher, err = callInventairePublisherGetName(entity.EditionId, lang) + if err != nil { + return queryResult, err + } + } + queryResult.Results = append(queryResult.Results, InventaireEditionResultBook{ + Id: entity.Uri, + ISBN: entity.ISBN, + Title: entity.Title, + ReleaseDate: entity.ReleaseDate, + Image: entity.Image, + Publisher: publisher, + Lang: entity.Lang, + }) + } + return queryResult, err +} + +func callInventaireUris(inventaireId string) (inventaireReverseClaimsResult, error) { + var queryResult inventaireReverseClaimsResult + u, err := computeInventaireApiUrl("entities") + if err != nil { + return queryResult, err + } + callapiutils.AddQueryParam(u, "action", "reverse-claims") + callapiutils.AddQueryParam(u, "property", "wdt:P629") + callapiutils.AddQueryParam(u, "value", inventaireId) + + err = callapiutils.FetchAndParseResult(u, &queryResult) + return queryResult, err +} + +func callInventaireEditionEntities(uris []string) (inventaireEditionQueryResult, error) { + var queryResult inventaireEditionQueryResult + u, err := computeInventaireApiUrl("entities") + if err != nil { + return queryResult, err + } + + callapiutils.AddQueryParam(u, "action", "by-uris") + callapiutils.AddQueryParam(u, "uris", strings.Join(uris, "|")) + + err = callapiutils.FetchAndParseResult(u, &queryResult) + + return queryResult, err +} + +type inventaireEditionPublisherResult struct { + Lang string + Label string +} + +func (i *inventaireEditionPublisherResult) UnmarshalJSON(b []byte) error { + var parsed struct { + Entities map[string]json.RawMessage + } + err := json.Unmarshal(b, &parsed) + if err != nil { + return err + } + + for _, entity := range parsed.Entities { + var publisherEntity struct { + Type string `json:"type"` + Labels map[string]json.RawMessage `json:"labels"` + } + err := json.Unmarshal(entity, &publisherEntity) + if err != nil { + return err + } + label, _ := findLangageField(publisherEntity.Labels, i.Lang) + i.Label = label + } + return err +} + +func callInventairePublisherGetName(editionId string, lang string) (string, error) { + var queryResult inventaireEditionPublisherResult + u, err := computeInventaireApiUrl("entities") + if err != nil { + return "", err + } + queryResult.Lang = lang + + callapiutils.AddQueryParam(u, "action", "by-uris") + callapiutils.AddQueryParam(u, "uris", editionId) + callapiutils.AddQueryParam(u, "attributes", "info|labels") + + err = callapiutils.FetchAndParseResult(u, &queryResult) + return queryResult.Label, err +} diff --git a/internal/routes/booksinventaireget.go b/internal/routes/booksinventaireget.go new file mode 100644 index 0000000..2b48995 --- /dev/null +++ b/internal/routes/booksinventaireget.go @@ -0,0 +1,38 @@ +package routes + +import ( + "net/http" + + "git.artlef.fr/PersonalLibraryManager/internal/appcontext" + "git.artlef.fr/PersonalLibraryManager/internal/dto" + "git.artlef.fr/PersonalLibraryManager/internal/inventaire" + "git.artlef.fr/PersonalLibraryManager/internal/myvalidator" +) + +func GetInventaireBooks(ac appcontext.AppContext) { + workId := ac.C.Param("workId") + var params dto.BookSearchGetParam + err := ac.C.ShouldBind(¶ms) + 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 + } + + inventaireEditionResult, err := inventaire.CallInventaireEdition(workId, params.Lang, limit, offset) + if err != nil { + myvalidator.ReturnErrorsAsJsonResponse(&ac, err) + return + } + ac.C.JSON(http.StatusOK, inventaireEditionResult) +} diff --git a/internal/setup/setup.go b/internal/setup/setup.go index 785c39c..fb72c86 100644 --- a/internal/setup/setup.go +++ b/internal/setup/setup.go @@ -36,6 +36,9 @@ func Setup(config *config.Config) *gin.Engine { r.GET("/search/:searchterm", func(c *gin.Context) { routes.GetSearchBooksHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config}) }) + r.GET("/inventaire/books/:workId", func(c *gin.Context) { + routes.GetInventaireBooks(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config}) + }) r.GET("/book/:id", func(c *gin.Context) { routes.GetBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config}) })