Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d1d865b6ff | |||
| 55a4a98b4d | |||
| 255f24904c | |||
| 178c688203 | |||
| d2fe3bf34f | |||
| 36a21c8891 | |||
| aca2a2c339 | |||
| dbf0face76 | |||
| 26931c734b | |||
| 6e3899b25e | |||
| f2899b968c | |||
| a537c12a3b | |||
| 2552ba8e94 | |||
| c7abbfe4d4 | |||
| 625d2a2af1 | |||
| a5c4c0bbec | |||
| 488e3763e3 | |||
| b48ab1e4de | |||
| b1bad80426 | |||
| a280647575 | |||
| acdc3972bd | |||
| c4753ea388 | |||
| 407f44d1e6 | |||
| 126dea4689 | |||
| 8d97d00e93 | |||
| bcde39d51d | |||
| 32d39cabcd | |||
| c1b6b61678 |
34
demodata.sql
34
demodata.sql
@@ -108,7 +108,7 @@ INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('
|
|||||||
INSERT INTO user_books(created_at, user_id, book_id, read, rating) VALUES ('NOW',(SELECT id FROM users WHERE name = 'demo2'),(SELECT id FROM books WHERE title = 'L''insoutenable légèreté de l''être'), true,8);
|
INSERT INTO user_books(created_at, user_id, book_id, read, rating) VALUES ('NOW',(SELECT id FROM users WHERE name = 'demo2'),(SELECT id FROM books WHERE title = 'L''insoutenable légèreté de l''être'), true,8);
|
||||||
INSERT INTO books(created_at, title, isbn, author_id, summary, added_by_id, cover_id) VALUES ('NOW', 'Le complot contre l''Amérique', '9782070337903', (SELECT id FROM authors WHERE name = 'Philip Roth'), 'Lorsque le célèbre aviateur Charles Lindbergh battit le président Roosevelt aux élections présidentielles de 1940, la peur s''empara des Juifs américains. Non seulement Lindbergh avait, dans son discours radiophonique à la nation, reproché aux Juifs de pousser l''Amérique à entreprendre une guerre inutile avec l''Allemagne nazie, mais, en devenant trente-troisième président des États-Unis, il s''empressa de signer un pacte de non-agression avec Hitler. Alors la terreur pénétra dans les foyers juifs, notamment dans celui de la famille Roth. Ce contexte sert de décor historique au Complot contre l''Amérique, un roman où Philip Roth, qui avait sept ans à l''époque, raconte ce que vécut et ressentit sa famille - et des millions de familles semblables dans tout le pays - lors des lourdes années où s''exerça la présidence de Lindbergh, quand les citoyens américains qui étaient aussi des Juifs avaient de bonnes raisons de craindre le pire. Ce faisant, il nous offre un nouveau chef-d''oeuvre.', (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'lecomplotcontrelamerique.jpg'));
|
INSERT INTO books(created_at, title, isbn, author_id, summary, added_by_id, cover_id) VALUES ('NOW', 'Le complot contre l''Amérique', '9782070337903', (SELECT id FROM authors WHERE name = 'Philip Roth'), 'Lorsque le célèbre aviateur Charles Lindbergh battit le président Roosevelt aux élections présidentielles de 1940, la peur s''empara des Juifs américains. Non seulement Lindbergh avait, dans son discours radiophonique à la nation, reproché aux Juifs de pousser l''Amérique à entreprendre une guerre inutile avec l''Allemagne nazie, mais, en devenant trente-troisième président des États-Unis, il s''empressa de signer un pacte de non-agression avec Hitler. Alors la terreur pénétra dans les foyers juifs, notamment dans celui de la famille Roth. Ce contexte sert de décor historique au Complot contre l''Amérique, un roman où Philip Roth, qui avait sept ans à l''époque, raconte ce que vécut et ressentit sa famille - et des millions de familles semblables dans tout le pays - lors des lourdes années où s''exerça la présidence de Lindbergh, quand les citoyens américains qui étaient aussi des Juifs avaient de bonnes raisons de craindre le pire. Ce faisant, il nous offre un nouveau chef-d''oeuvre.', (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'lecomplotcontrelamerique.jpg'));
|
||||||
INSERT INTO user_books(created_at, user_id, book_id, read, rating) VALUES ('NOW',(SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM books WHERE title = 'Le complot contre l''Amérique'),true,6);
|
INSERT INTO user_books(created_at, user_id, book_id, read, rating) VALUES ('NOW',(SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM books WHERE title = 'Le complot contre l''Amérique'),true,6);
|
||||||
INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'Nord','Louis-Ferdinand Céline', (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'Nord.jpg'));
|
INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'Nord',(SELECT id FROM authors WHERE name = 'Louis-Ferdinand Céline'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'Nord.jpg'));
|
||||||
INSERT INTO user_books(created_at, user_id, book_id, read, rating) VALUES ('NOW',(SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM books WHERE title = 'Nord'),true, 10);
|
INSERT INTO user_books(created_at, user_id, book_id, read, rating) VALUES ('NOW',(SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM books WHERE title = 'Nord'),true, 10);
|
||||||
INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'Sa majesté des mouches',(SELECT id FROM authors WHERE name = 'William Golding'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'sa+majesté+des+mouches.jpg'));
|
INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'Sa majesté des mouches',(SELECT id FROM authors WHERE name = 'William Golding'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'sa+majesté+des+mouches.jpg'));
|
||||||
INSERT INTO user_books(created_at, user_id, book_id, read, rating) VALUES ('NOW',(SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM books WHERE title = 'Sa majesté des mouches'),true, 5);
|
INSERT INTO user_books(created_at, user_id, book_id, read, rating) VALUES ('NOW',(SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM books WHERE title = 'Sa majesté des mouches'),true, 5);
|
||||||
@@ -125,3 +125,35 @@ INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('
|
|||||||
INSERT INTO user_books(created_at, user_id, book_id, start_read_date, rating) VALUES ('NOW',(SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM books WHERE title = 'Recherches philosophiques'), '2025-11-22 00:00:00+00:00',0);
|
INSERT INTO user_books(created_at, user_id, book_id, start_read_date, rating) VALUES ('NOW',(SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM books WHERE title = 'Recherches philosophiques'), '2025-11-22 00:00:00+00:00',0);
|
||||||
INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'Le château',(SELECT id FROM authors WHERE name = 'Franz Kafka'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'le_chateau.jpg'));
|
INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'Le château',(SELECT id FROM authors WHERE name = 'Franz Kafka'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'le_chateau.jpg'));
|
||||||
INSERT INTO user_books(created_at, user_id, book_id, start_read_date, rating) VALUES ('NOW',(SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM books WHERE title = 'Le château'), '2025-10-30 00:00:00+00:00',0);
|
INSERT INTO user_books(created_at, user_id, book_id, start_read_date, rating) VALUES ('NOW',(SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM books WHERE title = 'Le château'), '2025-10-30 00:00:00+00:00',0);
|
||||||
|
|
||||||
|
-- collections
|
||||||
|
INSERT INTO collections(name, user_id) VALUES ('Littérature française',(SELECT id FROM users WHERE name = 'demo'));
|
||||||
|
INSERT INTO collections(name, user_id) VALUES ('Nouvelles',(SELECT id FROM users WHERE name = 'demo'));
|
||||||
|
INSERT INTO collections(name, user_id) VALUES ('Non fiction',(SELECT id FROM users WHERE name = 'demo2'));
|
||||||
|
INSERT INTO collections(name, user_id) VALUES ('Empty',(SELECT id FROM users WHERE name = 'demo'));
|
||||||
|
INSERT INTO collections(name, user_id) VALUES ('Lu récemment',(SELECT id FROM users WHERE name = 'demo'));
|
||||||
|
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Littérature française'), (SELECT id FROM books WHERE title = 'Nord'), 1);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Littérature française'), (SELECT id FROM books WHERE title = 'Gargantua'), 2);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Littérature française'), (SELECT id FROM books WHERE title = 'Duo'), 3);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Littérature française'), (SELECT id FROM books WHERE title = 'Un barrage contre le Pacifique'), 4);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Littérature française'), (SELECT id FROM books WHERE title = 'Rigodon'), 5);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Littérature française'), (SELECT id FROM books WHERE title = 'Les dieux ont soif'), 6);
|
||||||
|
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Nouvelles'), (SELECT id FROM books WHERE title = 'Dojoji et autres nouvelles'), 1);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Nouvelles'), (SELECT id FROM books WHERE title = 'Le meurtre d''O-tsuya'), 2);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Nouvelles'), (SELECT id FROM books WHERE title = 'Le coup de pistolet'), 3);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Nouvelles'), (SELECT id FROM books WHERE title = 'Duo'), 4);
|
||||||
|
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Non fiction'), (SELECT id FROM books WHERE title = 'Recherches philosophiques'), 1);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Non fiction'), (SELECT id FROM books WHERE title = 'De sang-froid'), 2);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Non fiction'), (SELECT id FROM books WHERE title = 'The Life of Jesus'), 3);
|
||||||
|
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Lu récemment'), (SELECT id FROM books WHERE title = 'L''Homme sans qualités, tome 1'), 1);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Lu récemment'), (SELECT id FROM books WHERE title = 'Iliade'), 2);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Lu récemment'), (SELECT id FROM books WHERE title = 'Duo'), 3);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Lu récemment'), (SELECT id FROM books WHERE title = 'De sang-froid'), 4);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Lu récemment'), (SELECT id FROM books WHERE title = 'Le Pavillon d''or'), 5);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Lu récemment'), (SELECT id FROM books WHERE title = 'Recherches philosophiques'), 6);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Lu récemment'), (SELECT id FROM books WHERE title = 'Dojoji et autres nouvelles'), 7);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Lu récemment'), (SELECT id FROM books WHERE title = 'Le château'), 8);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "bibliomane",
|
"name": "bibliomane",
|
||||||
"version": "0.6.0",
|
"version": "0.7.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref, reactive, computed } from 'vue'
|
|
||||||
import { postBook, extractFormErrorFromField } from './api.js'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import CoverUpload from './CoverUpload.vue'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const book = ref({
|
|
||||||
title: '',
|
|
||||||
author: '',
|
|
||||||
coverId: null,
|
|
||||||
})
|
|
||||||
const errors = ref(null)
|
|
||||||
const titleError = computed(() => {
|
|
||||||
return extractFormErrorFromField('Title', errors.value)
|
|
||||||
})
|
|
||||||
const authorError = computed(() => {
|
|
||||||
return extractFormErrorFromField('Author', errors.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
function onSubmit(e) {
|
|
||||||
postBook(book).then((res) => {
|
|
||||||
if (res.ok) {
|
|
||||||
router.push('/')
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
res.json().then((json) => (errors.value = json))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<form @submit.prevent="onSubmit">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">{{ $t('addbook.title') }}</label>
|
|
||||||
<div class="control">
|
|
||||||
<input
|
|
||||||
:class="'input ' + (titleError ? 'is-danger' : '')"
|
|
||||||
type="text"
|
|
||||||
maxlength="300"
|
|
||||||
required
|
|
||||||
v-model="book.title"
|
|
||||||
:placeholder="$t('addbook.title')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p v-if="titleError" class="help is-danger">{{ titleError }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">{{ $t('addbook.author') }}</label>
|
|
||||||
<div class="control">
|
|
||||||
<input
|
|
||||||
:class="'input ' + (authorError ? 'is-danger' : '')"
|
|
||||||
type="text"
|
|
||||||
maxlength="100"
|
|
||||||
v-model="book.author"
|
|
||||||
:placeholder="$t('addbook.author')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p v-if="authorError" class="help is-danger">{{ authorError }}</p>
|
|
||||||
</div>
|
|
||||||
<CoverUpload name="cover" @on-image-upload="(id) => (book.coverId = id)" />
|
|
||||||
<div class="field">
|
|
||||||
<div class="control">
|
|
||||||
<button class="button is-link">{{ $t('addbook.submit') }}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
94
front/src/AddBookToCollection.vue
Normal file
94
front/src/AddBookToCollection.vue
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { getSearchBooks, postCollectionAddBook, extractFormErrorFromField } from './api.js'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
collectionId: Number,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['created'])
|
||||||
|
|
||||||
|
const book = ref({
|
||||||
|
id: 0,
|
||||||
|
title: '',
|
||||||
|
})
|
||||||
|
const addingBook = ref(false)
|
||||||
|
const data = ref(null)
|
||||||
|
const error = ref(null)
|
||||||
|
const titleError = computed(() => {
|
||||||
|
return extractFormErrorFromField('Title', error.value)
|
||||||
|
})
|
||||||
|
const vFocus = {
|
||||||
|
mounted: (el) => el.focus(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const limit = 5
|
||||||
|
|
||||||
|
function fetchBooks() {
|
||||||
|
if (!book || book.value.title.length < 3) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const lang = navigator.language.substring(0, 2)
|
||||||
|
getSearchBooks(data, error, book.value.title, lang, 0, limit, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addBook(bookId) {
|
||||||
|
postCollectionAddBook(props.collectionId, bookId).then((res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
addingBook.value = false
|
||||||
|
book.value.id = 0
|
||||||
|
book.value.title = ''
|
||||||
|
data.value = null
|
||||||
|
error.value = null
|
||||||
|
emit('created')
|
||||||
|
} else {
|
||||||
|
res.json().then((json) => {
|
||||||
|
error.value = json
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="field has-addons">
|
||||||
|
<div v-if="addingBook" class="control">
|
||||||
|
<input
|
||||||
|
:class="'input is-large ' + (titleError ? 'is-danger' : '')"
|
||||||
|
v-focus
|
||||||
|
@keyup="fetchBooks()"
|
||||||
|
type="text"
|
||||||
|
maxlength="300"
|
||||||
|
v-model="book.title"
|
||||||
|
:placeholder="$t('inputbookwidget.searchinput')"
|
||||||
|
/>
|
||||||
|
<p v-if="titleError" class="help is-danger">{{ titleError }}</p>
|
||||||
|
<ul v-if="data" class="popupresults has-background-dark">
|
||||||
|
<li v-for="book in data.books" @click="addBook(book.id)" class="bookresult p-2">
|
||||||
|
{{ book.title }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div v-if="!addingBook" class="control">
|
||||||
|
<button @click="addingBook = true" class="button is-large mb-2">
|
||||||
|
<span class="icon" :title="$t('collections.add')">
|
||||||
|
<b-icon-plus />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.popupresults {
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookresult {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookresult:hover {
|
||||||
|
background-color: var(--bulma-text-40);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
65
front/src/AddCollection.vue
Normal file
65
front/src/AddCollection.vue
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { postCollection, extractFormErrorFromField } from './api.js'
|
||||||
|
|
||||||
|
const emit = defineEmits(['created'])
|
||||||
|
|
||||||
|
const collection = ref({
|
||||||
|
name: '',
|
||||||
|
})
|
||||||
|
const addingCollection = ref(false)
|
||||||
|
const errors = ref(null)
|
||||||
|
const error = computed(() => {
|
||||||
|
return extractFormErrorFromField('Name', errors.value)
|
||||||
|
})
|
||||||
|
const vFocus = {
|
||||||
|
mounted: (el) => el.focus(),
|
||||||
|
}
|
||||||
|
|
||||||
|
function onButtonClick() {
|
||||||
|
if (addingCollection.value) {
|
||||||
|
createCollection()
|
||||||
|
} else {
|
||||||
|
addingCollection.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCollection() {
|
||||||
|
postCollection(collection.value).then((res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
addingCollection.value = false
|
||||||
|
collection.value.name = ''
|
||||||
|
res.json().then((json) => emit('created', json.id))
|
||||||
|
} else {
|
||||||
|
res.json().then((json) => (errors.value = json))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="field has-addons">
|
||||||
|
<div v-if="addingCollection" class="control">
|
||||||
|
<input
|
||||||
|
:class="'input is-medium ' + (error ? 'is-danger' : '')"
|
||||||
|
v-focus
|
||||||
|
@keyup.enter="createCollection()"
|
||||||
|
type="text"
|
||||||
|
maxlength="300"
|
||||||
|
v-model="collection.name"
|
||||||
|
:placeholder="$t('collections.name')"
|
||||||
|
/>
|
||||||
|
<p v-if="error" class="help is-danger">{{ error }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<button @click="onButtonClick" class="button is-medium mb-2">
|
||||||
|
<span class="icon" :title="$t('collections.add')">
|
||||||
|
<b-icon-check v-if="addingCollection" />
|
||||||
|
<b-icon-plus v-else />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -86,6 +86,14 @@ onMounted(() => {
|
|||||||
<RouterLink v-if="authStore.user" to="/books" class="navbar-item" activeClass="is-active">
|
<RouterLink v-if="authStore.user" to="/books" class="navbar-item" activeClass="is-active">
|
||||||
{{ $t('navbar.mybooks') }}
|
{{ $t('navbar.mybooks') }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
<RouterLink
|
||||||
|
v-if="authStore.user"
|
||||||
|
to="/collections"
|
||||||
|
class="navbar-item"
|
||||||
|
activeClass="is-active"
|
||||||
|
>
|
||||||
|
{{ $t('navbar.mycollections') }}
|
||||||
|
</RouterLink>
|
||||||
<RouterLink v-if="authStore.user" to="/browse" class="navbar-item" activeClass="is-active">
|
<RouterLink v-if="authStore.user" to="/browse" class="navbar-item" activeClass="is-active">
|
||||||
{{ $t('navbar.explore') }}
|
{{ $t('navbar.explore') }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|||||||
206
front/src/BookFormEdit.vue
Normal file
206
front/src/BookFormEdit.vue
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed } from 'vue'
|
||||||
|
import { postBook, putBook, extractFormErrorFromField, getBookCall } from './api.js'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import CoverUpload from './CoverUpload.vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
id: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchError = ref(null)
|
||||||
|
|
||||||
|
const book = ref({
|
||||||
|
title: '',
|
||||||
|
author: '',
|
||||||
|
isbn: '',
|
||||||
|
inventaireid: '',
|
||||||
|
openlibraryid: '',
|
||||||
|
shortdescription: '',
|
||||||
|
summary: '',
|
||||||
|
coverId: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (props.id) {
|
||||||
|
getBookCall(props.id)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.status === 401) {
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
authStore.logout()
|
||||||
|
}
|
||||||
|
return res.json()
|
||||||
|
})
|
||||||
|
.then(fillBookWithJson)
|
||||||
|
.catch((err) => (fetchError.value = err))
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillBookWithJson(json) {
|
||||||
|
book.value.title = json.title
|
||||||
|
book.value.author = json.author
|
||||||
|
book.value.isbn = json.isbn
|
||||||
|
book.value.inventaireid = json.inventaireid
|
||||||
|
book.value.openlibraryid = json.openlibraryid
|
||||||
|
book.value.shortdescription = json.shortdescription
|
||||||
|
book.value.summary = json.summary
|
||||||
|
book.value.coverId = json.coverId
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors = ref(null)
|
||||||
|
const titleError = computed(() => {
|
||||||
|
return extractFormErrorFromField('Title', errors.value)
|
||||||
|
})
|
||||||
|
const authorError = computed(() => {
|
||||||
|
return extractFormErrorFromField('Author', errors.value)
|
||||||
|
})
|
||||||
|
const isbnError = computed(() => {
|
||||||
|
return extractFormErrorFromField('ISBN', errors.value)
|
||||||
|
})
|
||||||
|
const inventaireError = computed(() => {
|
||||||
|
return extractFormErrorFromField('InventaireID', errors.value)
|
||||||
|
})
|
||||||
|
const openLibraryError = computed(() => {
|
||||||
|
return extractFormErrorFromField('OpenLibraryId', errors.value)
|
||||||
|
})
|
||||||
|
const shortDescError = computed(() => {
|
||||||
|
return extractFormErrorFromField('ShortDescription', errors.value)
|
||||||
|
})
|
||||||
|
const summaryError = computed(() => {
|
||||||
|
return extractFormErrorFromField('ShortDescription', errors.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
function postOrPutBook(book) {
|
||||||
|
if (props.id) {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
return postBook(book)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSubmit(e) {
|
||||||
|
if (props.id) {
|
||||||
|
putBook(props.id, book).then((res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
router.push('/book/' + props.id)
|
||||||
|
} else {
|
||||||
|
res.json().then((json) => (errors.value = json))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
postBook(book).then((res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
res.json().then((json) => router.push('/book/' + json.id))
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
res.json().then((json) => (errors.value = json))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="error">{{ $t('bookform.error', { error: fetchError.message }) }}</div>
|
||||||
|
<form v-else @submit.prevent="onSubmit">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ $t('addbook.title') }}</label>
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
:class="'input is-medium ' + (titleError ? 'is-danger' : '')"
|
||||||
|
type="text"
|
||||||
|
maxlength="300"
|
||||||
|
required
|
||||||
|
v-model="book.title"
|
||||||
|
:placeholder="$t('addbook.title')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p v-if="titleError" class="help is-danger">{{ titleError }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ $t('addbook.author') }}</label>
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
:class="'input is-medium ' + (authorError ? 'is-danger' : '')"
|
||||||
|
type="text"
|
||||||
|
maxlength="100"
|
||||||
|
v-model="book.author"
|
||||||
|
:placeholder="$t('addbook.author')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p v-if="authorError" class="help is-danger">{{ authorError }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ $t('addbook.shortdesc') }}</label>
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
:class="'input ' + (shortDescError ? 'is-danger' : '')"
|
||||||
|
type="text"
|
||||||
|
maxlength="300"
|
||||||
|
v-model="book.shortdescription"
|
||||||
|
:placeholder="$t('addbook.shortdesc')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p v-if="shortDescError" class="help is-danger">{{ shortDescError }}</p>
|
||||||
|
</div>
|
||||||
|
<CoverUpload name="cover" @on-image-upload="(id) => (book.coverId = id)" />
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ $t('addbook.summary') }}</label>
|
||||||
|
<div class="control">
|
||||||
|
<textarea
|
||||||
|
:class="'textarea ' + (summaryError ? 'is-danger' : '')"
|
||||||
|
type="text"
|
||||||
|
v-model="book.summary"
|
||||||
|
:placeholder="$t('addbook.summary')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p v-if="summaryError" class="help is-danger">{{ summaryError }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">ISBN</label>
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
:class="'input ' + (isbnError ? 'is-danger' : '')"
|
||||||
|
type="text"
|
||||||
|
maxlength="18"
|
||||||
|
v-model="book.isbn"
|
||||||
|
placeholder="ISBN"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p v-if="isbnError" class="help is-danger">{{ isbnError }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Inventaire</label>
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
:class="'input ' + (inventaireError ? 'is-danger' : '')"
|
||||||
|
type="text"
|
||||||
|
maxlength="50"
|
||||||
|
v-model="book.inventaireid"
|
||||||
|
placeholder="Inventaire"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p v-if="inventaireError" class="help is-danger">{{ inventaireError }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">OpenLibrary</label>
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
:class="'input ' + (openLibraryError ? 'is-danger' : '')"
|
||||||
|
type="text"
|
||||||
|
maxlength="50"
|
||||||
|
v-model="book.openlibraryid"
|
||||||
|
placeholder="OpenLibrary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p v-if="openLibraryError" class="help is-danger">{{ openLibraryError }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<button class="button is-link">{{ $t('addbook.submit') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
putEndReadDateUnset,
|
putEndReadDateUnset,
|
||||||
putUnreadBook,
|
putUnreadBook,
|
||||||
} from './api.js'
|
} from './api.js'
|
||||||
import { useRouter, onBeforeRouteUpdate } from 'vue-router'
|
import { useRouter, onBeforeRouteUpdate, RouterLink } from 'vue-router'
|
||||||
import { VRating } from 'vuetify/components/VRating'
|
import { VRating } from 'vuetify/components/VRating'
|
||||||
import BookFormIcons from './BookFormIcons.vue'
|
import BookFormIcons from './BookFormIcons.vue'
|
||||||
import ReviewWidget from './ReviewWidget.vue'
|
import ReviewWidget from './ReviewWidget.vue'
|
||||||
@@ -105,15 +105,24 @@ function goToAuthor() {
|
|||||||
<figure class="image">
|
<figure class="image">
|
||||||
<img v-bind:src="imagePathOrDefault" v-bind:alt="data.title" />
|
<img v-bind:src="imagePathOrDefault" v-bind:alt="data.title" />
|
||||||
</figure>
|
</figure>
|
||||||
|
<div class="centered mt-2">
|
||||||
|
<RouterLink :to="'/book/' + props.id + '/edit'">
|
||||||
|
<span>
|
||||||
|
<b-icon-pencil-fill />
|
||||||
|
</span>
|
||||||
|
Modifier le livre
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<h3 class="title">{{ data.title }}</h3>
|
<h3 class="title">{{ data.title }}</h3>
|
||||||
<h3 class="subtitle clickable" @click="goToAuthor">{{ data.author }}</h3>
|
<h3 class="subtitle clickable" @click="goToAuthor">{{ data.author }}</h3>
|
||||||
<p>{{ data.summary }}</p>
|
<p class="mb-2">{{ data.summary }}</p>
|
||||||
<div class="my-5" v-if="data.isbn">ISBN: {{ data.isbn }}</div>
|
<div class="my-4" v-if="data.isbn">ISBN: {{ data.isbn }}</div>
|
||||||
<div class="my-5" v-if="data.inventaireid">Inventaire ID: {{ data.inventaireid }}</div>
|
<div class="my-4" v-if="data.inventaireid">Inventaire ID: {{ data.inventaireid }}</div>
|
||||||
<div class="my-5" v-if="data.openlibraryid">OLID: {{ data.openlibraryid }}</div>
|
<div class="my-4" v-if="data.openlibraryid">OLID: {{ data.openlibraryid }}</div>
|
||||||
<ReviewWidget
|
<ReviewWidget
|
||||||
|
class="mt-5"
|
||||||
:reviewtext="data.review"
|
:reviewtext="data.review"
|
||||||
:rating="data.rating"
|
:rating="data.rating"
|
||||||
@on-review-update="onReviewUpdate"
|
@on-review-update="onReviewUpdate"
|
||||||
@@ -164,6 +164,7 @@ async function importInventaireEdition(inventaireid) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
111
front/src/CollectionForm.vue
Normal file
111
front/src/CollectionForm.vue
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { getCollection, postCollectionChangePosition } from './api.js'
|
||||||
|
import CollectionFormElement from './CollectionFormElement.vue'
|
||||||
|
import AddBookToCollection from './AddBookToCollection.vue'
|
||||||
|
import Pagination from './Pagination.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
id: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
const limit = 5
|
||||||
|
const pageNumber = ref(1)
|
||||||
|
|
||||||
|
const offset = computed(() => (pageNumber.value - 1) * limit)
|
||||||
|
|
||||||
|
const data = ref(null)
|
||||||
|
const error = ref(null)
|
||||||
|
|
||||||
|
const itemIdBeingGrabbed = ref(null)
|
||||||
|
|
||||||
|
const itemIdBeingOvered = ref(null)
|
||||||
|
|
||||||
|
let totalElementsNumber = computed(() =>
|
||||||
|
typeof data != 'undefined' && data.value != null ? data.value['count'] : 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
let pageTotal = computed(() => Math.ceil(totalElementsNumber.value / limit))
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragStart(event, id) {
|
||||||
|
event.dataTransfer.effectAllowed = 'move'
|
||||||
|
// Custom type to identify a collectionitem drag
|
||||||
|
event.dataTransfer.setData('collectionitem', '')
|
||||||
|
itemIdBeingGrabbed.value = id
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragover(event) {
|
||||||
|
if (event.dataTransfer.types.includes('collectionitem')) {
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrop(id, position) {
|
||||||
|
if (id == itemIdBeingGrabbed.value) {
|
||||||
|
//nothing to do
|
||||||
|
return
|
||||||
|
}
|
||||||
|
changePosition(itemIdBeingGrabbed.value, position)
|
||||||
|
}
|
||||||
|
|
||||||
|
function changePosition(id, position) {
|
||||||
|
postCollectionChangePosition(props.id, id, position).then((res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
getCollection(data, error, props.id, limit, offset.value)
|
||||||
|
} else {
|
||||||
|
res.json().then((json) => {
|
||||||
|
error.value = json
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragend() {
|
||||||
|
itemIdBeingGrabbed.value = null
|
||||||
|
itemIdBeingOvered.value = null
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="error">{{ $t('collection.error', { error: error }) }}</div>
|
||||||
|
<div v-if="data">
|
||||||
|
<h2 class="title">{{ data.name }}</h2>
|
||||||
|
<AddBookToCollection :collection-id="props.id" @created="fetchCollection" />
|
||||||
|
<div>
|
||||||
|
<CollectionFormElement
|
||||||
|
@drop="onDrop(item.id, item.position)"
|
||||||
|
@dragstart="(e) => onDragStart(e, item.id)"
|
||||||
|
@dragend="onDragend"
|
||||||
|
@dragover="onDragover"
|
||||||
|
@dragenter="itemIdBeingOvered = item.id"
|
||||||
|
@positionchange="(pos) => changePosition(item.id, pos)"
|
||||||
|
v-for="item in data.items"
|
||||||
|
:key="item.id"
|
||||||
|
:is-dragover="itemIdBeingOvered === item.id"
|
||||||
|
v-bind="item"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Pagination
|
||||||
|
class="mt-5"
|
||||||
|
:pageNumber="pageNumber"
|
||||||
|
:pageTotal="pageTotal"
|
||||||
|
maxItemDisplayed="11"
|
||||||
|
@pageChange="pageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
119
front/src/CollectionFormElement.vue
Normal file
119
front/src/CollectionFormElement.vue
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import BookListElement from './BookListElement.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
isDragover: Boolean,
|
||||||
|
id: Number,
|
||||||
|
position: Number,
|
||||||
|
book: Array,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits('positionchange')
|
||||||
|
|
||||||
|
const vFocus = {
|
||||||
|
mounted: (el) => el.focus(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const isInputtingPosition = ref(false)
|
||||||
|
const inputtedPosition = ref('')
|
||||||
|
|
||||||
|
function onPositionInput() {
|
||||||
|
if (inputtedPosition.value != '' && !isNaN(inputtedPosition.value)) {
|
||||||
|
const parsedPosition = parseInt(inputtedPosition.value)
|
||||||
|
if (parsedPosition != props.position) {
|
||||||
|
emit('positionchange', parsedPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clearPositionInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearPositionInput() {
|
||||||
|
isInputtingPosition.value = false
|
||||||
|
inputtedPosition.value = ''
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div :class="isDragover ? 'dragover' : ''" draggable="true" class="collectionitembox">
|
||||||
|
<BookListElement v-bind="props.book">
|
||||||
|
<div class="separator" />
|
||||||
|
<div class="centered">
|
||||||
|
<div
|
||||||
|
v-if="!isInputtingPosition"
|
||||||
|
@click="isInputtingPosition = true"
|
||||||
|
class="positionindicator centered is-narrow clickable"
|
||||||
|
>
|
||||||
|
{{ props.position }}
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="inputtedPosition"
|
||||||
|
v-focus
|
||||||
|
@blur="clearPositionInput"
|
||||||
|
@keyup.enter="onPositionInput"
|
||||||
|
size="1"
|
||||||
|
class="positioninput"
|
||||||
|
:placeholder="props.position"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="separator" />
|
||||||
|
<div class="positionwidget centered is-narrow">
|
||||||
|
<b-icon-list />
|
||||||
|
</div>
|
||||||
|
</BookListElement>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.collectionitembox {
|
||||||
|
transition: ease-in-out 0.04s;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collectionitembox:hover {
|
||||||
|
transform: scale(1.01);
|
||||||
|
transition: ease-in-out 0.02s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
width: 5px;
|
||||||
|
background: var(--bulma-scheme-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.positionindicator {
|
||||||
|
font-size: 36px;
|
||||||
|
margin-left: 40px;
|
||||||
|
margin-right: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.positioninput {
|
||||||
|
font-size: 36px;
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-right: 20px;
|
||||||
|
text-align: center;
|
||||||
|
background: var(--bulma-scheme-main);
|
||||||
|
border-radius: 10%;
|
||||||
|
color: var(--bulma-body-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.positionwidget {
|
||||||
|
color: var(--bulma-scheme-main);
|
||||||
|
font-size: 48px;
|
||||||
|
margin-left: 30px;
|
||||||
|
margin-right: 30px;
|
||||||
|
border-top-right-radius: var(--bulma-box-radius);
|
||||||
|
border-bottom-right-radius: var(--bulma-box-radius);
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.positionwidget:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragover {
|
||||||
|
border: 3px solid var(--bulma-primary);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
87
front/src/CollectionListElement.vue
Normal file
87
front/src/CollectionListElement.vue
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<script setup>
|
||||||
|
import { getImagePathOrDefault } from './api.js'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
id: Number,
|
||||||
|
name: String,
|
||||||
|
books: Array,
|
||||||
|
count: Number,
|
||||||
|
})
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
function goToCollection() {
|
||||||
|
const collectionId = props.id
|
||||||
|
router.push(`/collection/${props.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBookOpacity(index) {
|
||||||
|
if (props.books.length > 7 && index > 3) {
|
||||||
|
return 'opacity: ' + (100 - index * 13) + '%;'
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="collectioncontainer has-background-dark p-2" @click="goToCollection">
|
||||||
|
<div class="collectionheader">
|
||||||
|
<h2 class="subtitle">
|
||||||
|
{{ props.name }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="collectionpreviewbooks" v-if="props.books && props.books.length > 0">
|
||||||
|
<div class="bookpreview mx-1" v-for="(book, index) in props.books" :key="book.id">
|
||||||
|
<img
|
||||||
|
:style="setBookOpacity(index)"
|
||||||
|
v-bind:src="getImagePathOrDefault(book.coverPath)"
|
||||||
|
v-bind:alt="book.title"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
img {
|
||||||
|
max-height: 150px;
|
||||||
|
max-width: 150px;
|
||||||
|
height: auto;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collectioncontainer {
|
||||||
|
display: flex;
|
||||||
|
transition: ease-in-out 0.04s;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collectioncontainer:hover {
|
||||||
|
transform: scale(1.01);
|
||||||
|
transition: ease-in-out 0.02s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collectionheader {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collectionpreviewbooks {
|
||||||
|
flex: 6;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
img {
|
||||||
|
max-height: 50px;
|
||||||
|
max-width: 50px;
|
||||||
|
}
|
||||||
|
.collectionpreviewbooks {
|
||||||
|
flex: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
62
front/src/CollectionsBrowser.vue
Normal file
62
front/src/CollectionsBrowser.vue
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { getCollections } from './api.js'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import CollectionListElement from './CollectionListElement.vue'
|
||||||
|
import Pagination from './Pagination.vue'
|
||||||
|
import AddCollection from './AddCollection.vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const limit = 5
|
||||||
|
const pageNumber = ref(1)
|
||||||
|
|
||||||
|
const offset = computed(() => (pageNumber.value - 1) * limit)
|
||||||
|
|
||||||
|
const data = ref(null)
|
||||||
|
const error = ref(null)
|
||||||
|
|
||||||
|
let totalCollectionsNumber = computed(() =>
|
||||||
|
typeof data != 'undefined' && data.value != null ? data.value['count'] : 0,
|
||||||
|
)
|
||||||
|
let pageTotal = computed(() => Math.ceil(totalCollectionsNumber.value / limit))
|
||||||
|
|
||||||
|
fetchData()
|
||||||
|
|
||||||
|
function fetchData() {
|
||||||
|
getCollections(data, error, limit, offset.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageChange(newPageNumber) {
|
||||||
|
pageNumber.value = newPageNumber
|
||||||
|
data.value = null
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToCollection(id) {
|
||||||
|
router.push(`/collection/${id}`)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="error">{{ $t('collections.error', { error: error.message }) }}</div>
|
||||||
|
<div v-else-if="data">
|
||||||
|
<AddCollection @created="goToCollection" />
|
||||||
|
<div class="collectionslist">
|
||||||
|
<div class="my-2" v-for="collection in data.collections" :key="collection.id">
|
||||||
|
<CollectionListElement v-bind="collection" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Pagination
|
||||||
|
class="mt-5"
|
||||||
|
:pageNumber="pageNumber"
|
||||||
|
:pageTotal="pageTotal"
|
||||||
|
maxItemDisplayed="11"
|
||||||
|
@pageChange="pageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else>{{ $t('bookbrowser.loading') }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -36,7 +36,15 @@ function fetchData(searchTerm, authorId) {
|
|||||||
error.value = null
|
error.value = null
|
||||||
if (searchTerm != null) {
|
if (searchTerm != null) {
|
||||||
const lang = navigator.language.substring(0, 2)
|
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) {
|
} else if (authorId != null) {
|
||||||
getAuthorBooks(data, error, authorId, limit, offset.value)
|
getAuthorBooks(data, error, authorId, limit, offset.value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,28 +18,34 @@ export function getImagePathOrGivenDefault(path, defaultpath) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function useFetch(data, error, url) {
|
function userFetch(url) {
|
||||||
const { user } = useAuthStore()
|
const { user } = useAuthStore()
|
||||||
|
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
fetch(url, {
|
return fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: 'Bearer ' + user.token,
|
Authorization: 'Bearer ' + user.token,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((res) => {
|
} else {
|
||||||
if (res.status === 401) {
|
return Promise.resolve()
|
||||||
const authStore = useAuthStore()
|
|
||||||
authStore.logout()
|
|
||||||
}
|
|
||||||
return res.json()
|
|
||||||
})
|
|
||||||
.then((json) => (data.value = json))
|
|
||||||
.catch((err) => (error.value = err))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useFetch(data, error, url) {
|
||||||
|
userFetch(url)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.status === 401) {
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
authStore.logout()
|
||||||
|
}
|
||||||
|
return res.json()
|
||||||
|
})
|
||||||
|
.then((json) => (data.value = json))
|
||||||
|
.catch((err) => (error.value = err))
|
||||||
|
}
|
||||||
|
|
||||||
export async function getAppInfo(appInfo, appInfoErr) {
|
export async function getAppInfo(appInfo, appInfoErr) {
|
||||||
return fetch('/ws/appinfo', {
|
return fetch('/ws/appinfo', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -49,6 +55,16 @@ export async function getAppInfo(appInfo, appInfoErr) {
|
|||||||
.catch((err) => (appInfoErr.value = err))
|
.catch((err) => (appInfoErr.value = err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCollections(data, error, limit, offset) {
|
||||||
|
const queryParams = new URLSearchParams({ limit: limit, offset: offset })
|
||||||
|
return useFetch(data, error, '/ws/collections' + '?' + queryParams.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCollection(data, error, id, limit, offset) {
|
||||||
|
const queryParams = new URLSearchParams({ limit: limit, offset: offset })
|
||||||
|
return useFetch(data, error, '/ws/collection/' + id + '?' + queryParams.toString())
|
||||||
|
}
|
||||||
|
|
||||||
export function getMyBooks(data, error, arg, limit, offset) {
|
export function getMyBooks(data, error, arg, limit, offset) {
|
||||||
const queryParams = new URLSearchParams({ limit: limit, offset: offset })
|
const queryParams = new URLSearchParams({ limit: limit, offset: offset })
|
||||||
return useFetch(data, error, '/ws/mybooks/' + arg + '?' + queryParams.toString())
|
return useFetch(data, error, '/ws/mybooks/' + arg + '?' + queryParams.toString())
|
||||||
@@ -98,6 +114,10 @@ export function getBook(data, error, id) {
|
|||||||
return useFetch(data, error, '/ws/book/' + id)
|
return useFetch(data, error, '/ws/book/' + id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getBookCall(id) {
|
||||||
|
return userFetch('/ws/book/' + id)
|
||||||
|
}
|
||||||
|
|
||||||
export function postBook(book) {
|
export function postBook(book) {
|
||||||
return genericPayloadCall('/ws/book', book.value, 'POST')
|
return genericPayloadCall('/ws/book', book.value, 'POST')
|
||||||
}
|
}
|
||||||
@@ -106,6 +126,30 @@ export async function postImportBook(id, language) {
|
|||||||
return genericPayloadCall('/ws/importbook', { inventaireid: id, lang: language }, 'POST')
|
return genericPayloadCall('/ws/importbook', { inventaireid: id, lang: language }, 'POST')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function postCollection(collection) {
|
||||||
|
return genericPayloadCall('/ws/collection', collection, 'POST')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function postCollectionAddBook(collectionId, position) {
|
||||||
|
return genericPayloadCall(
|
||||||
|
'/ws/collection/' + collectionId + '/addbook',
|
||||||
|
{ bookId: bookId },
|
||||||
|
'POST',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function postCollectionChangePosition(collectionId, itemId, position) {
|
||||||
|
return genericPayloadCall(
|
||||||
|
'/ws/collection/' + collectionId + '/changeposition',
|
||||||
|
{ itemId: itemId, position: position },
|
||||||
|
'POST',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function putBook(id, book) {
|
||||||
|
return genericPayloadCall('/ws/book/edit/' + id, book.value, 'PUT')
|
||||||
|
}
|
||||||
|
|
||||||
export async function putReadBook(bookId) {
|
export async function putReadBook(bookId) {
|
||||||
return putEndReadDate(bookId, new Date().toISOString().slice(0, 10))
|
return putEndReadDate(bookId, new Date().toISOString().slice(0, 10))
|
||||||
}
|
}
|
||||||
@@ -191,13 +235,9 @@ export function genericPayloadCall(apiRoute, object, method) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function extractFormErrorFromField(fieldName, errors) {
|
export function extractFormErrorFromField(fieldName, errors) {
|
||||||
if (errors === null) {
|
if (errors == null || typeof errors == 'undefined' || !Array.isArray(errors)) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
if (errors.value == null) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
console.log(errors.value)
|
|
||||||
const titleErr = errors.find((e) => e['field'] === fieldName)
|
const titleErr = errors.find((e) => e['field'] === fieldName)
|
||||||
if (typeof titleErr !== 'undefined') {
|
if (typeof titleErr !== 'undefined') {
|
||||||
return titleErr.error
|
return titleErr.error
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
},
|
},
|
||||||
"navbar": {
|
"navbar": {
|
||||||
"mybooks": "My Books",
|
"mybooks": "My Books",
|
||||||
|
"mycollections": "My Collections",
|
||||||
"addbook": "Add Book",
|
"addbook": "Add Book",
|
||||||
"explore": "Explore",
|
"explore": "Explore",
|
||||||
"logout": "Log out",
|
"logout": "Log out",
|
||||||
@@ -20,9 +21,14 @@
|
|||||||
"addbook": {
|
"addbook": {
|
||||||
"title": "Title",
|
"title": "Title",
|
||||||
"author": "Author",
|
"author": "Author",
|
||||||
|
"shortdesc": "Short description",
|
||||||
|
"summary": "Summary",
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
"coverupload": "Upload cover"
|
"coverupload": "Upload cover"
|
||||||
},
|
},
|
||||||
|
"inputbookwidget": {
|
||||||
|
"searchinput": "Book title to add..."
|
||||||
|
},
|
||||||
"signup": {
|
"signup": {
|
||||||
"title": "Sign up",
|
"title": "Sign up",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
@@ -86,5 +92,13 @@
|
|||||||
"review": {
|
"review": {
|
||||||
"title": "My review",
|
"title": "My review",
|
||||||
"textplaceholder": "Write my review..."
|
"textplaceholder": "Write my review..."
|
||||||
|
},
|
||||||
|
"collections": {
|
||||||
|
"error": "Error when loading collections: {error}",
|
||||||
|
"add": "Add a collection",
|
||||||
|
"name": "Name"
|
||||||
|
},
|
||||||
|
"collection": {
|
||||||
|
"error": "Error when loading collection: {error}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
},
|
},
|
||||||
"navbar": {
|
"navbar": {
|
||||||
"mybooks": "Mes Livres",
|
"mybooks": "Mes Livres",
|
||||||
|
"mycollections": "Mes Listes",
|
||||||
"explore": "Explorer",
|
"explore": "Explorer",
|
||||||
"addbook": "Ajouter Un Livre",
|
"addbook": "Ajouter Un Livre",
|
||||||
"logout": "Se déconnecter",
|
"logout": "Se déconnecter",
|
||||||
@@ -20,9 +21,14 @@
|
|||||||
"addbook": {
|
"addbook": {
|
||||||
"title": "Titre",
|
"title": "Titre",
|
||||||
"author": "Auteur",
|
"author": "Auteur",
|
||||||
|
"shortdesc": "Description rapide",
|
||||||
|
"summary": "Résumé",
|
||||||
"submit": "Confirmer",
|
"submit": "Confirmer",
|
||||||
"coverupload": "Téléverser la couverture"
|
"coverupload": "Téléverser la couverture"
|
||||||
},
|
},
|
||||||
|
"inputbookwidget": {
|
||||||
|
"searchinput": "Titre du livre à ajouter..."
|
||||||
|
},
|
||||||
"signup": {
|
"signup": {
|
||||||
"title": "Inscription",
|
"title": "Inscription",
|
||||||
"username": "Nom d'utilisateur",
|
"username": "Nom d'utilisateur",
|
||||||
@@ -86,5 +92,13 @@
|
|||||||
"review": {
|
"review": {
|
||||||
"title": "Ma critique",
|
"title": "Ma critique",
|
||||||
"textplaceholder": "Écrire ma critique..."
|
"textplaceholder": "Écrire ma critique..."
|
||||||
|
},
|
||||||
|
"collections": {
|
||||||
|
"error": "Erreur pendant le chargement des listes: {error}",
|
||||||
|
"add": "Ajouter une liste",
|
||||||
|
"name": "Nom"
|
||||||
|
},
|
||||||
|
"collection": {
|
||||||
|
"error": "Erreur pendant le chargement de la liste : {error}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
import BooksBrowser from './BooksBrowser.vue'
|
import BooksBrowser from './BooksBrowser.vue'
|
||||||
import AddBook from './AddBook.vue'
|
import CollectionsBrowser from './CollectionsBrowser.vue'
|
||||||
|
import CollectionForm from './CollectionForm.vue'
|
||||||
|
import BookFormEdit from './BookFormEdit.vue'
|
||||||
import AuthorForm from './AuthorForm.vue'
|
import AuthorForm from './AuthorForm.vue'
|
||||||
import BookForm from './BookForm.vue'
|
import BookFormView from './BookFormView.vue'
|
||||||
import SignUp from './SignUp.vue'
|
import SignUp from './SignUp.vue'
|
||||||
import LogIn from './LogIn.vue'
|
import LogIn from './LogIn.vue'
|
||||||
import Home from './Home.vue'
|
import Home from './Home.vue'
|
||||||
@@ -18,11 +20,14 @@ const routes = [
|
|||||||
{ path: '/scan', component: ScanBook },
|
{ path: '/scan', component: ScanBook },
|
||||||
{ path: '/browse', component: InstanceBrowser },
|
{ path: '/browse', component: InstanceBrowser },
|
||||||
{ path: '/books', component: BooksBrowser },
|
{ path: '/books', component: BooksBrowser },
|
||||||
{ path: '/book/:id', component: BookForm, props: true },
|
{ path: '/book/:id', component: BookFormView, props: true },
|
||||||
|
{ path: '/book/:id/edit', component: BookFormEdit, props: true },
|
||||||
|
{ path: '/collections', component: CollectionsBrowser },
|
||||||
|
{ path: '/collection/:id', component: CollectionForm, props: true },
|
||||||
{ path: '/author/:id', component: AuthorForm, props: true },
|
{ path: '/author/:id', component: AuthorForm, props: true },
|
||||||
{ path: '/search/:searchterm', component: SearchBook, props: true },
|
{ path: '/search/:searchterm', component: SearchBook, props: true },
|
||||||
{ path: '/import/inventaire/:inventaireid', component: ImportInventaire, props: true },
|
{ path: '/import/inventaire/:inventaireid', component: ImportInventaire, props: true },
|
||||||
{ path: '/add', component: AddBook },
|
{ path: '/add', component: BookFormEdit },
|
||||||
{ path: '/signup', component: SignUp },
|
{ path: '/signup', component: SignUp },
|
||||||
{ path: '/login', component: LogIn },
|
{ path: '/login', component: LogIn },
|
||||||
]
|
]
|
||||||
|
|||||||
133
internal/adapter/adapter.go
Normal file
133
internal/adapter/adapter.go
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
package adapter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"git.artlef.fr/bibliomane/internal/appcontext"
|
||||||
|
"git.artlef.fr/bibliomane/internal/dto"
|
||||||
|
"git.artlef.fr/bibliomane/internal/model"
|
||||||
|
"git.artlef.fr/bibliomane/internal/query"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CollectionItemsQueryToDto(itemsQueryResult []query.CollectionItemQueryResult) []dto.CollectionItemGet {
|
||||||
|
var dtoItems []dto.CollectionItemGet
|
||||||
|
for _, queryResult := range itemsQueryResult {
|
||||||
|
bookItem := dto.BookItemGet{
|
||||||
|
ID: queryResult.ID,
|
||||||
|
Title: queryResult.Title,
|
||||||
|
Author: queryResult.Author,
|
||||||
|
Description: queryResult.Description,
|
||||||
|
InventaireID: queryResult.InventaireID,
|
||||||
|
IsInventaireEdition: queryResult.IsInventaireEdition,
|
||||||
|
Rating: queryResult.Rating,
|
||||||
|
Read: queryResult.Read,
|
||||||
|
StartReadDate: queryResult.StartReadDate,
|
||||||
|
WantRead: queryResult.WantRead,
|
||||||
|
CoverPath: queryResult.CoverPath,
|
||||||
|
}
|
||||||
|
dtoItems = append(dtoItems,
|
||||||
|
dto.CollectionItemGet{
|
||||||
|
ID: queryResult.ItemID,
|
||||||
|
Position: queryResult.Position,
|
||||||
|
Book: bookItem,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return dtoItems
|
||||||
|
}
|
||||||
|
|
||||||
|
func CollectionQueryToCollectionItemDto(collectionsQueryResult []query.CollectionsQueryResult) []dto.CollectionListItemGet {
|
||||||
|
var collections []dto.CollectionListItemGet
|
||||||
|
for _, collectionDb := range collectionsQueryResult {
|
||||||
|
i := findIdInCollection(collections, collectionDb.ID)
|
||||||
|
if i == -1 {
|
||||||
|
collections = append(collections, dto.CollectionListItemGet{
|
||||||
|
ID: collectionDb.ID,
|
||||||
|
Name: collectionDb.Name,
|
||||||
|
})
|
||||||
|
//current collection is the last element
|
||||||
|
i = len(collections) - 1
|
||||||
|
}
|
||||||
|
book := collectionDbToCollectionBookItem(&collectionDb)
|
||||||
|
if book != nil {
|
||||||
|
collections[i].Books = append(collections[i].Books, *book)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return collections
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionDbToCollectionBookItem(collectionDb *query.CollectionsQueryResult) *dto.CollectionListBookItemGet {
|
||||||
|
if collectionDb.BookId > 0 {
|
||||||
|
bookItem := dto.CollectionListBookItemGet{
|
||||||
|
ID: collectionDb.BookId,
|
||||||
|
Title: collectionDb.BookTitle,
|
||||||
|
CoverPath: collectionDb.CoverPath,
|
||||||
|
}
|
||||||
|
return &bookItem
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the position in collections, -1 if not found
|
||||||
|
func findIdInCollection(collections []dto.CollectionListItemGet, collectionId uint) int {
|
||||||
|
for i, collection := range collections {
|
||||||
|
if collection.ID == collectionId {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
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.ShortDescription = *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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
package apitest
|
package apitest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
|
||||||
"net/url"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.artlef.fr/bibliomane/internal/dto"
|
"git.artlef.fr/bibliomane/internal/dto"
|
||||||
@@ -14,45 +10,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestFetchAllBooks(t *testing.T) {
|
func TestFetchAllBooks(t *testing.T) {
|
||||||
result := testFetchBooks(t, "15", "0")
|
status, result := testFetchBooks(t, "15", "0")
|
||||||
|
assert.Equal(t, http.StatusOK, status)
|
||||||
assert.Equal(t, int64(31), result.Count)
|
assert.Equal(t, int64(31), result.Count)
|
||||||
assert.Equal(t, 15, len(result.Books))
|
assert.Equal(t, 15, len(result.Books))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testFetchBooks(t *testing.T, limit string, offset string) dto.BookItemsGet {
|
func testFetchBooks(t *testing.T, limit string, offset string) (int, dto.BookItemsGet) {
|
||||||
router := testutils.TestSetup()
|
return testutils.TestFetchModel[dto.BookItemsGet](t, "/ws/books", limit, offset)
|
||||||
|
|
||||||
u, err := url.Parse("/ws/books")
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
if limit != "" {
|
|
||||||
q := u.Query()
|
|
||||||
q.Set("limit", limit)
|
|
||||||
u.RawQuery = q.Encode()
|
|
||||||
}
|
|
||||||
if offset != "" {
|
|
||||||
q := u.Query()
|
|
||||||
q.Set("offset", offset)
|
|
||||||
u.RawQuery = q.Encode()
|
|
||||||
}
|
|
||||||
|
|
||||||
q := u.Query()
|
|
||||||
q.Set("lang", "fr")
|
|
||||||
u.RawQuery = q.Encode()
|
|
||||||
|
|
||||||
token := testutils.ConnectDemoUser(router)
|
|
||||||
req, _ := http.NewRequest("GET", u.String(), nil)
|
|
||||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
var result dto.BookItemsGet
|
|
||||||
s := w.Body.String()
|
|
||||||
err = json.Unmarshal([]byte(s), &result)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
assert.Equal(t, 200, w.Code)
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|||||||
21
internal/apitest/fetchallcollections_test.go
Normal file
21
internal/apitest/fetchallcollections_test.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package apitest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.artlef.fr/bibliomane/internal/dto"
|
||||||
|
"git.artlef.fr/bibliomane/internal/testutils"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFetchAllCollections_OK(t *testing.T) {
|
||||||
|
status, res := testFetchCollections(t, "10", "0")
|
||||||
|
assert.Equal(t, http.StatusOK, status)
|
||||||
|
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) {
|
||||||
|
return testutils.TestFetchModel[dto.CollectionItemsGet](t, "/ws/collections", limit, offset)
|
||||||
|
}
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
package apitest
|
package apitest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.artlef.fr/bibliomane/internal/dto"
|
"git.artlef.fr/bibliomane/internal/dto"
|
||||||
@@ -61,20 +58,8 @@ func TestGetBook_IdNotInt(t *testing.T) {
|
|||||||
testGetBook(t, "wrong", http.StatusBadRequest)
|
testGetBook(t, "wrong", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testGetBook(t *testing.T, id string, status int) dto.FullBookGet {
|
func testGetBook(t *testing.T, id string, expectedStatus int) dto.FullBookGet {
|
||||||
router := testutils.TestSetup()
|
status, book := testutils.TestFetchOneModel[dto.FullBookGet](t, "/ws/book", id)
|
||||||
|
assert.Equal(t, expectedStatus, status)
|
||||||
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)
|
|
||||||
return book
|
return book
|
||||||
}
|
}
|
||||||
|
|||||||
49
internal/apitest/get_collection_test.go
Normal file
49
internal/apitest/get_collection_test.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
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.Items))
|
||||||
|
}
|
||||||
|
|
||||||
|
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.Items))
|
||||||
|
assert.Equal(t, int64(4), collection.Count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCollection_Unauthorized(t *testing.T) {
|
||||||
|
status, _ := testGetCollection(t, "3", "10", "0")
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCollection_Empty(t *testing.T) {
|
||||||
|
status, collection := testGetCollection(t, "4", "10", "0")
|
||||||
|
assert.Equal(t, http.StatusOK, status)
|
||||||
|
assert.Equal(t, "Empty", collection.Name)
|
||||||
|
assert.Equal(t, 0, len(collection.Items))
|
||||||
|
assert.Equal(t, int64(0), collection.Count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCollection_Position(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, uint(3), collection.Items[2].Position)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
package apitest
|
package apitest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.artlef.fr/bibliomane/internal/testutils"
|
"git.artlef.fr/bibliomane/internal/testutils"
|
||||||
@@ -45,6 +43,28 @@ func TestPostBookHandler_noTitle(t *testing.T) {
|
|||||||
testPostBookHandler(t, bookJson, 400)
|
testPostBookHandler(t, bookJson, 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPostBookHandler_AllFields(t *testing.T) {
|
||||||
|
bookJson :=
|
||||||
|
`{
|
||||||
|
"author": "Kafka",
|
||||||
|
"title": "Amerika",
|
||||||
|
"isbn": "978-2-07-036803-7",
|
||||||
|
"inventaireid": "isbn:9782070368037",
|
||||||
|
"openlibraryid": "OL8838048M",
|
||||||
|
"shortdescription": "Roman de Franz Kafka",
|
||||||
|
"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)."
|
||||||
|
}`
|
||||||
|
id := testPostBookHandler(t, bookJson, 200)
|
||||||
|
createdBook := testGetBook(t, strconv.FormatUint(uint64(id), 10), http.StatusOK)
|
||||||
|
|
||||||
|
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) {
|
func TestPostBookHandler_TitleTooLong(t *testing.T) {
|
||||||
bookJson :=
|
bookJson :=
|
||||||
`{
|
`{
|
||||||
@@ -63,16 +83,8 @@ func TestPostBookHandler_AuthorTooLong(t *testing.T) {
|
|||||||
testPostBookHandler(t, bookJson, 400)
|
testPostBookHandler(t, bookJson, 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testPostBookHandler(t *testing.T, bookJson string, expectedCode int) {
|
func testPostBookHandler(t *testing.T, bookJson string, expectedCode int) uint {
|
||||||
router := testutils.TestSetup()
|
status, id := testutils.TestPostCall(t, "/ws/book", bookJson)
|
||||||
w := httptest.NewRecorder()
|
assert.Equal(t, expectedCode, status)
|
||||||
|
return id
|
||||||
token := testutils.ConnectDemoUser(router)
|
|
||||||
req, _ := http.NewRequest("POST", "/ws/book",
|
|
||||||
strings.NewReader(string(bookJson)))
|
|
||||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
assert.Equal(t, expectedCode, w.Code)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
76
internal/apitest/post_collection_addbook_test.go
Normal file
76
internal/apitest/post_collection_addbook_test.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package apitest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.artlef.fr/bibliomane/internal/testutils"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPostCollectionBookHandler_AddOk(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"bookId": 9
|
||||||
|
}`
|
||||||
|
collectionId := "1"
|
||||||
|
status := testPostCollectionBookHandler(collectionId, payload)
|
||||||
|
assert.Equal(t, http.StatusOK, status)
|
||||||
|
_, collection := testGetCollection(t, collectionId, "10", "0")
|
||||||
|
assert.Equal(t, int64(7), collection.Count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostCollectionBookHandler_BookOK(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"bookId": 7
|
||||||
|
}`
|
||||||
|
collectionId := "2"
|
||||||
|
status := testPostCollectionBookHandler(collectionId, payload)
|
||||||
|
assert.Equal(t, http.StatusOK, status)
|
||||||
|
_, collection := testGetCollection(t, collectionId, "10", "0")
|
||||||
|
assert.Equal(t, uint(7), collection.Items[0].Book.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostCollectionBookHandler_CollectionNotFound(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"bookId": 9
|
||||||
|
}`
|
||||||
|
status := testPostCollectionBookHandler("12", payload)
|
||||||
|
assert.Equal(t, http.StatusNotFound, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostCollectionBookHandler_BookNotFound(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"bookId": 14654
|
||||||
|
}`
|
||||||
|
status := testPostCollectionBookHandler("1", payload)
|
||||||
|
assert.Equal(t, http.StatusNotFound, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostCollectionBookHandler_Unauthorized(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"bookId": 9
|
||||||
|
}`
|
||||||
|
status := testPostCollectionBookHandler("3", payload)
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPostCollectionBookHandler(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
|
||||||
|
}
|
||||||
114
internal/apitest/post_collection_changeposition_test.go
Normal file
114
internal/apitest/post_collection_changeposition_test.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
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)
|
||||||
|
assert.Equal(t, uint(8), collection.Items[7].Position)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
31
internal/apitest/post_collection_test.go
Normal file
31
internal/apitest/post_collection_test.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package apitest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.artlef.fr/bibliomane/internal/testutils"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPostCollectionHandler_Ok(t *testing.T) {
|
||||||
|
collectionJson :=
|
||||||
|
`{
|
||||||
|
"name": "My collection"
|
||||||
|
}`
|
||||||
|
status, _ := testPostCollectionHandler(t, collectionJson)
|
||||||
|
assert.Equal(t, http.StatusOK, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostCollectionHandler_NameTooLong(t *testing.T) {
|
||||||
|
collectionJson :=
|
||||||
|
`{
|
||||||
|
"name": "rsteerdemenschderraumschiffgebrauchlichtalsseinursprungvonkraftgestartseinlangefahrthinzwischensternartigraumaufdersuchenrsteerdemenschderraumschiffgebrauchlichtalsseinursprungvonkraftgestartseinlangefahrthinzwischensternartigraumaufdersuchenrsteerdemenschderraumschiffgebrauchlichtalsseinursprungvonkraftgestartseinlangefahrthinzwischensternartigraumaufdersuchen"
|
||||||
|
}`
|
||||||
|
status, _ := testPostCollectionHandler(t, collectionJson)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPostCollectionHandler(t *testing.T, collectionJson string) (int, uint) {
|
||||||
|
return testutils.TestPostCall(t, "/ws/collection", collectionJson)
|
||||||
|
}
|
||||||
@@ -24,10 +24,6 @@ func TestPostImportBookHandler_Ok(t *testing.T) {
|
|||||||
assert.Equal(t, "Emily Brontë", book.Author)
|
assert.Equal(t, "Emily Brontë", book.Author)
|
||||||
assert.Equal(t, "isbn:9782253004752", book.InventaireId)
|
assert.Equal(t, "isbn:9782253004752", book.InventaireId)
|
||||||
assert.Equal(t, "/static/bookcover/44abbcbdc1092212c2bae66f5165019dac1e2a7b.webp", book.CoverPath)
|
assert.Equal(t, "/static/bookcover/44abbcbdc1092212c2bae66f5165019dac1e2a7b.webp", book.CoverPath)
|
||||||
expectedDesc := `Roman unique, à la croisée du fantastique et du romantisme, ce texte inclassable bouleverse les codes du XIXe siècle par sa violence émotionnelle, sa narration fragmentée et ses personnages à fleur de peau.
|
|
||||||
Sur les landes battues par les vents, à l'ombre des murs de Hurlevent, se joue une tragédie d'amour et de vengeance entre Catherine et Heathcliff - deux âmes tourmentées, liées par une passion aussi absolue que destructrice.
|
|
||||||
Sublimée par l'univers graphique intense d'Isabella Mazzanti, cette édition s'impose comme un objet littéraire à part, mêlant innovations narratives et force d'évocation. Les images semblent vibrer d'un souffle secret, comme si le vent y faisait surgir, en silence, le tumulte des passions.`
|
|
||||||
assert.Equal(t, expectedDesc, book.Summary)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPostImportBookHandler_OkAuthorKey(t *testing.T) {
|
func TestPostImportBookHandler_OkAuthorKey(t *testing.T) {
|
||||||
@@ -37,9 +33,6 @@ func TestPostImportBookHandler_OkAuthorKey(t *testing.T) {
|
|||||||
assert.Equal(t, "Philip K. Dick", book.Author)
|
assert.Equal(t, "Philip K. Dick", book.Author)
|
||||||
assert.Equal(t, "isbn:9782290033630", book.InventaireId)
|
assert.Equal(t, "isbn:9782290033630", book.InventaireId)
|
||||||
assert.Equal(t, "/static/bookcover/1d1493159d031224a42b37c4417fcbb8c76b00bd.webp", book.CoverPath)
|
assert.Equal(t, "/static/bookcover/1d1493159d031224a42b37c4417fcbb8c76b00bd.webp", book.CoverPath)
|
||||||
expectedDesc := `Les bombes étaient finalement tombées. Malgré l'équilibre de la terreur, un jour un homme avait été assez fou pour appuyer sur le bouton. Cependant, dans ce coin perdu de Californie, la vie continuait. Pour Bonny Keller, toujours perturbée malgré six ans d'analyse ; pour Bruno Bluthgeld, l'un des responsables de la grande catastrophe ; pour Hoppy, le phocomèle, l'ancien bébé thalidomide doté de pouvoirs supranormaux... Elle continuait aussi pour Walt Dangerfield, l'astronaute expédié sur mars, mais dont la cabine s'était satellisée autour de la terre. Là, à l'abri des radiations, il s'était transformé en une sorte de super disc-jockey dont l'écoute était devenue une drogue pour tous les survivants...
|
|
||||||
Philip K. Dick (1928-1982). A travers une œuvre imposante, il ne cessera de traiter ses thèmes de prédilection : la juxtaposition de deux niveaux de réalité — l'un "objectivement" déterminé, l'autre n'étant qu'un monde d'apparences — et la poranoïa qu'impliquent ces manipulations de la réalité dont personne ne tonnait jamais le degré exact de "virtualité".`
|
|
||||||
assert.Equal(t, expectedDesc, book.Summary)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPostImportBookHandler_NoOLID(t *testing.T) {
|
func TestPostImportBookHandler_NoOLID(t *testing.T) {
|
||||||
|
|||||||
57
internal/apitest/put_book_test.go
Normal file
57
internal/apitest/put_book_test.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
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, "Roman de Pouchkine", modifiedBook.ShortDescription)
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package apitest
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@@ -15,14 +16,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestSearchBook_MultipleBooks(t *testing.T) {
|
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, int64(2), result.Count)
|
||||||
assert.Equal(t, 2, len(result.Books))
|
assert.Equal(t, 2, len(result.Books))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSearchBook_OneBookNotUserBook(t *testing.T) {
|
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, int64(1), result.Count)
|
||||||
assert.Equal(t,
|
assert.Equal(t,
|
||||||
[]dto.BookItemGet{{
|
[]dto.BookItemGet{{
|
||||||
@@ -38,7 +39,7 @@ func TestSearchBook_OneBookNotUserBook(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSearchBook_OneBookRead(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, int64(1), result.Count)
|
||||||
assert.Equal(t,
|
assert.Equal(t,
|
||||||
[]dto.BookItemGet{{
|
[]dto.BookItemGet{{
|
||||||
@@ -55,7 +56,7 @@ func TestSearchBook_OneBookRead(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSearchBook_OneBookStartRead(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, int64(1), result.Count)
|
||||||
assert.Equal(t,
|
assert.Equal(t,
|
||||||
[]dto.BookItemGet{{
|
[]dto.BookItemGet{{
|
||||||
@@ -72,7 +73,7 @@ func TestSearchBook_OneBookStartRead(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSearchBook_ISBN(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, int64(1), result.Count)
|
||||||
assert.Equal(t,
|
assert.Equal(t,
|
||||||
[]dto.BookItemGet{{
|
[]dto.BookItemGet{{
|
||||||
@@ -88,7 +89,7 @@ func TestSearchBook_ISBN(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSearchBook_ISBNInventaire(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, int64(1), result.Count)
|
||||||
assert.Equal(t,
|
assert.Equal(t,
|
||||||
[]dto.BookItemGet{{
|
[]dto.BookItemGet{{
|
||||||
@@ -107,17 +108,17 @@ func TestSearchBook_ISBNInventaire(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSearchBook_Limit(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))
|
assert.Equal(t, 10, len(result.Books))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSearchBook_Offset(t *testing.T) {
|
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, int64(5), result.Count)
|
||||||
assert.Equal(t, 3, len(result.Books))
|
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()
|
router := testutils.TestSetup()
|
||||||
|
|
||||||
u, err := url.Parse("/ws/search/" + searchterm)
|
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 := u.Query()
|
||||||
q.Set("lang", "fr")
|
q.Set("lang", "fr")
|
||||||
|
q.Set("inventaire", strconv.Itoa(int(inventaireSearchType)))
|
||||||
u.RawQuery = q.Encode()
|
u.RawQuery = q.Encode()
|
||||||
|
|
||||||
token := testutils.ConnectDemoUser(router)
|
token := testutils.ConnectDemoUser(router)
|
||||||
|
|||||||
@@ -1,128 +0,0 @@
|
|||||||
package babelio
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.artlef.fr/bibliomane/internal/callapiutils"
|
|
||||||
"git.artlef.fr/bibliomane/internal/myvalidator"
|
|
||||||
"github.com/PuerkitoBio/goquery"
|
|
||||||
"golang.org/x/text/encoding/charmap"
|
|
||||||
)
|
|
||||||
|
|
||||||
type babelioSearchArg struct {
|
|
||||||
Term string `json:"term"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type babelioSearchResult struct {
|
|
||||||
//only parsing the url
|
|
||||||
Url string `json:"url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetDescriptionFromISBN(baseUrl string, isbn string) (string, error) {
|
|
||||||
url, err := searchPageIsbn(baseUrl, isbn)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
//we either find the full summary, or we have to make another call to get it.
|
|
||||||
fullSummary, payloadToQuery, err := parseBookPage(baseUrl, url)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if fullSummary != "" {
|
|
||||||
return decodeAndCleanText(strings.NewReader(fullSummary)), err
|
|
||||||
} else if payloadToQuery != "" {
|
|
||||||
return queryDescription(baseUrl, payloadToQuery)
|
|
||||||
} else {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func searchPageIsbn(baseUrl, isbn string) (string, error) {
|
|
||||||
searchUrl, err := callapiutils.ComputeUrl(baseUrl, "aj_recherche.php")
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
term := babelioSearchArg{Term: isbn}
|
|
||||||
var searchResults []babelioSearchResult
|
|
||||||
callapiutils.FetchAndParseResultFromPost(searchUrl, &term, &searchResults)
|
|
||||||
if len(searchResults) == 0 {
|
|
||||||
return "", myvalidator.TranslatedError{Err: errors.New("ISBNNotFoundBabelio"), Arg: isbn}
|
|
||||||
}
|
|
||||||
|
|
||||||
return searchResults[0].Url, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseBookPage(baseUrl, bookUrl string) (string, string, error) {
|
|
||||||
|
|
||||||
url, err := callapiutils.ComputeUrl(baseUrl, bookUrl)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
resp, err := http.Get(url.String())
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
|
||||||
//we either find the full summary, or we have to make another call to get it.
|
|
||||||
fullsummary := ""
|
|
||||||
jsToParse := ""
|
|
||||||
doc.Find(".livre_resume").Each(func(i int, s *goquery.Selection) {
|
|
||||||
onclick, ok := s.Find("a").Attr("onclick")
|
|
||||||
if ok {
|
|
||||||
jsToParse = onclick
|
|
||||||
} else {
|
|
||||||
fullsummary = s.Text()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if fullsummary != "" {
|
|
||||||
return fullsummary, "", nil
|
|
||||||
}
|
|
||||||
typeStr, idObj, err := extractNumbersFromExpression(jsToParse)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
return "", fmt.Sprintf("type=%s&id_obj=%s", typeStr, idObj), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractNumbersFromExpression(jsToParse string) (string, string, error) {
|
|
||||||
splitted := strings.Split(jsToParse, ",")
|
|
||||||
if len(splitted) < 3 {
|
|
||||||
return "", "", myvalidator.TranslatedError{Err: errors.New("BabelioParseError")}
|
|
||||||
}
|
|
||||||
if len(splitted[2]) < 3 {
|
|
||||||
return "", "", myvalidator.TranslatedError{Err: errors.New("BabelioParseError")}
|
|
||||||
}
|
|
||||||
return splitted[1], splitted[2][:len(splitted[2])-2], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func queryDescription(baseUrl string, payloadToQuery string) (string, error) {
|
|
||||||
url, err := callapiutils.ComputeUrl(baseUrl, "aj_voir_plus_a.php")
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
resp, err := http.Post(url.String(),
|
|
||||||
"application/x-www-form-urlencoded; charset=UTF-8",
|
|
||||||
strings.NewReader(payloadToQuery))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return "", myvalidator.TranslatedError{Err: fmt.Errorf("BabelioFetchDescError")}
|
|
||||||
}
|
|
||||||
return decodeAndCleanText(resp.Body), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeAndCleanText(reader io.Reader) string {
|
|
||||||
tr := charmap.Windows1252.NewDecoder().Reader(reader)
|
|
||||||
var decodedString strings.Builder
|
|
||||||
io.Copy(&decodedString, tr)
|
|
||||||
return strings.TrimSpace(strings.ReplaceAll(decodedString.String(), "<br>", "\n"))
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package babelio
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGetDescriptionFromISBN_Philip(t *testing.T) {
|
|
||||||
desc, err := GetDescriptionFromISBN("https://www.babelio.com", "9782290033630")
|
|
||||||
expectedDesc := `Les bombes étaient finalement tombées. Malgré l'équilibre de la terreur, un jour un homme avait été assez fou pour appuyer sur le bouton. Cependant, dans ce coin perdu de Californie, la vie continuait. Pour Bonny Keller, toujours perturbée malgré six ans d'analyse ; pour Bruno Bluthgeld, l'un des responsables de la grande catastrophe ; pour Hoppy, le phocomèle, l'ancien bébé thalidomide doté de pouvoirs supranormaux... Elle continuait aussi pour Walt Dangerfield, l'astronaute expédié sur mars, mais dont la cabine s'était satellisée autour de la terre. Là, à l'abri des radiations, il s'était transformé en une sorte de super disc-jockey dont l'écoute était devenue une drogue pour tous les survivants...
|
|
||||||
Philip K. Dick (1928-1982). A travers une œuvre imposante, il ne cessera de traiter ses thèmes de prédilection : la juxtaposition de deux niveaux de réalité — l'un "objectivement" déterminé, l'autre n'étant qu'un monde d'apparences — et la poranoïa qu'impliquent ces manipulations de la réalité dont personne ne tonnait jamais le degré exact de "virtualité".`
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
assert.Equal(t, expectedDesc, desc)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetDescriptionFromISBN_Emily(t *testing.T) {
|
|
||||||
desc, err := GetDescriptionFromISBN("https://www.babelio.com", "9782253004752")
|
|
||||||
expectedDesc := `Roman unique, à la croisée du fantastique et du romantisme, ce texte inclassable bouleverse les codes du XIXe siècle par sa violence émotionnelle, sa narration fragmentée et ses personnages à fleur de peau.
|
|
||||||
Sur les landes battues par les vents, à l'ombre des murs de Hurlevent, se joue une tragédie d'amour et de vengeance entre Catherine et Heathcliff - deux âmes tourmentées, liées par une passion aussi absolue que destructrice.
|
|
||||||
Sublimée par l'univers graphique intense d'Isabella Mazzanti, cette édition s'impose comme un objet littéraire à part, mêlant innovations narratives et force d'évocation. Les images semblent vibrer d'un souffle secret, comme si le vent y faisait surgir, en silence, le tumulte des passions.`
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
assert.Equal(t, expectedDesc, desc)
|
|
||||||
}
|
|
||||||
@@ -22,19 +22,17 @@ type CLI struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Port string `toml:"port" short:"p" default:"8080" help:"Port to listen on for the server." comment:"Port to listen on for the server."`
|
Port string `toml:"port" short:"p" default:"8080" help:"Port to listen on for the server." comment:"Port to listen on for the server."`
|
||||||
DatabaseFilePath string `toml:"database-file-path" short:"d" default:"bibliomane.db" type:"path" help:"Path to sqlite database file." comment:"Path to sqlite database file."`
|
DatabaseFilePath string `toml:"database-file-path" short:"d" default:"bibliomane.db" type:"path" help:"Path to sqlite database file." comment:"Path to sqlite database file."`
|
||||||
DemoDataPath string `toml:"demo-data-path" help:"Path to the sql file to load for demo data." comment:"Path to the sql file to load for demo data."`
|
DemoDataPath string `toml:"demo-data-path" help:"Path to the sql file to load for demo data." comment:"Path to the sql file to load for demo data."`
|
||||||
JWTKey string `toml:"jwt-key" help:"Key used to encrypt JWT." comment:"Key used to encrypt the generated JWT. Encoded in base64. If empty a random one will be generated on every restart."`
|
JWTKey string `toml:"jwt-key" help:"Key used to encrypt JWT." comment:"Key used to encrypt the generated JWT. Encoded in base64. If empty a random one will be generated on every restart."`
|
||||||
ImageFolderPath string `toml:"image-folder-path" short:"i" default:"img" type:"path" help:"Folder where uploaded files will be stored." comment:"Folder where uploaded files will be stored."`
|
ImageFolderPath string `toml:"image-folder-path" short:"i" default:"img" type:"path" help:"Folder where uploaded files will be stored." comment:"Folder where uploaded files will be stored."`
|
||||||
Limit int `toml:"limit" default:"100" help:"A single API call will return at most this number of records." comment:"A single API call will return at most this number of records."`
|
Limit int `toml:"limit" default:"100" help:"A single API call will return at most this number of records." comment:"A single API call will return at most this number of records."`
|
||||||
InventaireUrl string `toml:"inventaire-url" default:"https://inventaire.io" help:"An inventaire.io instance URL." comment:"An inventaire.io instance URL."`
|
InventaireUrl string `toml:"inventaire-url" default:"https://inventaire.io" help:"An inventaire.io instance URL." comment:"An inventaire.io instance URL."`
|
||||||
BookDescriptionFromBabelio bool `toml:"book-description-from-babelio" default:"false" help:"Activate fetching description from babelio.com." comment:"Activate fetching description from babelio.com."`
|
DisableRegistration bool `toml:"disable-registration" short:"n" default:"false" help:"Disable new account creation." comment:"Disable new account creation."`
|
||||||
BabelioUrl string `toml:"babelio-url" default:"https://www.babelio.com" comment:"Link to babelio website."`
|
DemoMode bool `toml:"demo-mode" short:"D" default:"false" help:"Activate demo mode." comment:"Activate demo mode: anyone connecting to the instance will be logged in as a single user."`
|
||||||
DisableRegistration bool `toml:"disable-registration" short:"n" default:"false" help:"Disable new account creation." comment:"Disable new account creation."`
|
DemoUsername string `toml:"demo-username" default:"demo" help:"Name of the single user used for the demo." comment:"Name of the single user used for the demo."`
|
||||||
DemoMode bool `toml:"demo-mode" short:"D" default:"false" help:"Activate demo mode." comment:"Activate demo mode: anyone connecting to the instance will be logged in as a single user."`
|
AddUser UserListAsStrings `toml:"add-user" short:"a" help:"Add users on startup following htpasswd bcrypt format." comment:"Add users on startup following htpasswd bcrypt format, example: [\"demo:$2y$10$UHR2646SZo2W.Rhna7bn5eWNLXWJZ/Sa3oLd9RlxlXs57Bwp6isOS\",\"user:$2y$10$3WYUp.VDpzJRywtrxO1s/uWfUIKpTE4yh5B1d2RCef3hvczYbEWTC\"]"`
|
||||||
DemoUsername string `toml:"demo-username" default:"demo" help:"Name of the single user used for the demo." comment:"Name of the single user used for the demo."`
|
|
||||||
AddUser UserListAsStrings `toml:"add-user" short:"a" help:"Add users on startup following htpasswd bcrypt format." comment:"Add users on startup following htpasswd bcrypt format, example: [\"demo:$2y$10$UHR2646SZo2W.Rhna7bn5eWNLXWJZ/Sa3oLd9RlxlXs57Bwp6isOS\",\"user:$2y$10$3WYUp.VDpzJRywtrxO1s/uWfUIKpTE4yh5B1d2RCef3hvczYbEWTC\"]"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserListAsStrings []string
|
type UserListAsStrings []string
|
||||||
@@ -50,19 +48,17 @@ func (u UserListAsStrings) Validate() error {
|
|||||||
|
|
||||||
func defaultConfig() CLI {
|
func defaultConfig() CLI {
|
||||||
c := Config{
|
c := Config{
|
||||||
Port: "8080",
|
Port: "8080",
|
||||||
DatabaseFilePath: "bibliomane.db",
|
DatabaseFilePath: "bibliomane.db",
|
||||||
DemoDataPath: "",
|
DemoDataPath: "",
|
||||||
JWTKey: "",
|
JWTKey: "",
|
||||||
ImageFolderPath: "img",
|
ImageFolderPath: "img",
|
||||||
Limit: 100,
|
Limit: 100,
|
||||||
InventaireUrl: "https://inventaire.io",
|
InventaireUrl: "https://inventaire.io",
|
||||||
BookDescriptionFromBabelio: false,
|
DisableRegistration: false,
|
||||||
BabelioUrl: "https://www.babelio.com",
|
DemoMode: false,
|
||||||
DisableRegistration: false,
|
DemoUsername: "demo",
|
||||||
DemoMode: false,
|
AddUser: []string{},
|
||||||
DemoUsername: "demo",
|
|
||||||
AddUser: []string{},
|
|
||||||
}
|
}
|
||||||
return CLI{NoConfigFile: false, ConfigFilePath: "bibliomane.toml", DisableStoreJWTKeyInConfig: false, ConfigFile: c}
|
return CLI{NoConfigFile: false, ConfigFilePath: "bibliomane.toml", DisableStoreJWTKeyInConfig: false, ConfigFile: c}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ func Initdb(databasePath string, demoDataPath string) *gorm.DB {
|
|||||||
db.AutoMigrate(&model.User{})
|
db.AutoMigrate(&model.User{})
|
||||||
db.AutoMigrate(&model.UserBook{})
|
db.AutoMigrate(&model.UserBook{})
|
||||||
db.AutoMigrate(&model.StaticFile{})
|
db.AutoMigrate(&model.StaticFile{})
|
||||||
|
db.AutoMigrate(&model.Collection{})
|
||||||
|
db.AutoMigrate(&model.CollectionItem{})
|
||||||
var book model.Book
|
var book model.Book
|
||||||
queryResult := db.Limit(1).Find(&book)
|
queryResult := db.Limit(1).Find(&book)
|
||||||
if queryResult.RowsAffected == 0 && demoDataPath != "" {
|
if queryResult.RowsAffected == 0 && demoDataPath != "" {
|
||||||
|
|||||||
@@ -5,15 +5,28 @@ type AuthorGet struct {
|
|||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InventaireSearchType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
NoInventaireSearch InventaireSearchType = iota
|
||||||
|
InventaireIfNothingFound
|
||||||
|
ForceInventaireSearch
|
||||||
|
)
|
||||||
|
|
||||||
type BookSearchGetParam struct {
|
type BookSearchGetParam struct {
|
||||||
Lang string `form:"lang" binding:"max=5"`
|
Lang string `form:"lang" binding:"max=5"`
|
||||||
Inventaire bool `form:"inventaire"`
|
Inventaire InventaireSearchType `form:"inventaire"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BookPostCreate struct {
|
type BookFields struct {
|
||||||
Title string `json:"title" binding:"required,max=300"`
|
Title *string `json:"title" binding:"omitempty,max=300"`
|
||||||
Author string `json:"author" binding:"max=100"`
|
Author *string `json:"author" binding:"omitempty,max=100"`
|
||||||
CoverID uint `json:"coverId"`
|
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 {
|
type BookPostImport struct {
|
||||||
@@ -30,6 +43,19 @@ type UserBookPutUpdate struct {
|
|||||||
Review *string `json:"review"`
|
Review *string `json:"review"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CollectionFields struct {
|
||||||
|
Name string `json:"name" binding:"required,max=300"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
type FileInfoPost struct {
|
||||||
FileID uint `json:"fileId"`
|
FileID uint `json:"fileId"`
|
||||||
FilePath string `json:"filepath"`
|
FilePath string `json:"filepath"`
|
||||||
|
|||||||
@@ -7,20 +7,21 @@ type AppInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FullBookGet struct {
|
type FullBookGet struct {
|
||||||
Title string `json:"title" binding:"required,max=300"`
|
Title string `json:"title" binding:"required,max=300"`
|
||||||
Author string `json:"author" binding:"max=100"`
|
Author string `json:"author" binding:"max=100"`
|
||||||
AuthorID uint `json:"authorId"`
|
AuthorID uint `json:"authorId"`
|
||||||
ISBN string `json:"isbn"`
|
ISBN string `json:"isbn"`
|
||||||
InventaireId string `json:"inventaireid"`
|
InventaireId string `json:"inventaireid"`
|
||||||
OpenLibraryId string `json:"openlibraryid"`
|
OpenLibraryId string `json:"openlibraryid"`
|
||||||
Summary string `json:"summary"`
|
ShortDescription string `json:"shortdescription"`
|
||||||
Review string `json:"review"`
|
Summary string `json:"summary"`
|
||||||
Rating int `json:"rating"`
|
Review string `json:"review"`
|
||||||
Read bool `json:"read"`
|
Rating int `json:"rating"`
|
||||||
WantRead bool `json:"wantread"`
|
Read bool `json:"read"`
|
||||||
StartReadDate string `json:"startReadDate"`
|
WantRead bool `json:"wantread"`
|
||||||
EndReadDate string `json:"endReadDate"`
|
StartReadDate string `json:"startReadDate"`
|
||||||
CoverPath string `json:"coverPath"`
|
EndReadDate string `json:"endReadDate"`
|
||||||
|
CoverPath string `json:"coverPath"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BookItemsGet struct {
|
type BookItemsGet struct {
|
||||||
@@ -42,3 +43,32 @@ type BookItemGet struct {
|
|||||||
WantRead bool `json:"wantread"`
|
WantRead bool `json:"wantread"`
|
||||||
CoverPath string `json:"coverPath"`
|
CoverPath string `json:"coverPath"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CollectionGet struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
Items []CollectionItemGet `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CollectionItemGet struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Position uint `json:"position"`
|
||||||
|
Book BookItemGet `json:"book"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CollectionItemsGet struct {
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
Collections []CollectionListItemGet `json:"collections"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CollectionListItemGet struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Books []CollectionListBookItemGet `json:"books"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CollectionListBookItemGet struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
CoverPath string `json:"coverPath"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,5 @@ ValidationPropertyFail = "Validation failed for '%s' property."
|
|||||||
RegistrationDisabled = "Registration has been disabled on this instance."
|
RegistrationDisabled = "Registration has been disabled on this instance."
|
||||||
UserAlreadyExists = "An user with this name already exists."
|
UserAlreadyExists = "An user with this name already exists."
|
||||||
ErrorWhenCreatingUserFromStr = "Error when creating user from string %s"
|
ErrorWhenCreatingUserFromStr = "Error when creating user from string %s"
|
||||||
ISBNNotFoundBabelio = "ISBN %s not found on babelio."
|
Unauthorized = "You are not allowed to access this document."
|
||||||
BabelioParseError = "Error when parsing babelio."
|
ItemDoesNotBelongToCollection = "Item does not belong to the collection."
|
||||||
BabelioFetchDescError = "Error when fetching description on babelio."
|
|
||||||
|
|||||||
@@ -9,6 +9,5 @@ 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."
|
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à."
|
UserAlreadyExists = "Un utilisateur avec le même nom existe déjà."
|
||||||
ErrorWhenCreatingUserFromStr = "Erreur lors de la création de l'utilisateur %s"
|
ErrorWhenCreatingUserFromStr = "Erreur lors de la création de l'utilisateur %s"
|
||||||
ISBNNotFoundBabelio = "L'ISBN %s n'est pas sur babelio."
|
Unauthorized = "Vous n'êtes pas autorisé à accéder à cette page."
|
||||||
BabelioParseError = "Erreur en parsant babelio."
|
ItemDoesNotBelongToCollection = "Cet élément n'appartient pas à la liste."
|
||||||
BabelioFetchDescError = "Erreur lors de la récupération de la description sur babelio."
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ type Book struct {
|
|||||||
ISBN string `json:"isbn"`
|
ISBN string `json:"isbn"`
|
||||||
InventaireID string `json:"inventaireid"`
|
InventaireID string `json:"inventaireid"`
|
||||||
OpenLibraryId string `json:"openlibraryid"`
|
OpenLibraryId string `json:"openlibraryid"`
|
||||||
SmallDescription string
|
ShortDescription string
|
||||||
Summary string `json:"summary"`
|
Summary string `json:"summary"`
|
||||||
Author Author
|
Author Author
|
||||||
AuthorID uint
|
AuthorID uint
|
||||||
|
|||||||
11
internal/model/collection.go
Normal file
11
internal/model/collection.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "gorm.io/gorm"
|
||||||
|
|
||||||
|
type Collection struct {
|
||||||
|
gorm.Model
|
||||||
|
Name string
|
||||||
|
User User
|
||||||
|
UserID uint
|
||||||
|
Items []CollectionItem
|
||||||
|
}
|
||||||
11
internal/model/collectionitem.go
Normal file
11
internal/model/collectionitem.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "gorm.io/gorm"
|
||||||
|
|
||||||
|
type CollectionItem struct {
|
||||||
|
gorm.Model
|
||||||
|
CollectionID uint
|
||||||
|
Position uint
|
||||||
|
Book Book
|
||||||
|
BookID uint
|
||||||
|
}
|
||||||
@@ -57,6 +57,9 @@ func ReturnErrorsAsJsonResponse(ac *appcontext.AppContext, err error) {
|
|||||||
ac.C.JSON(httpError.StatusCode, gin.H{"error": httpError.Err.Error()})
|
ac.C.JSON(httpError.StatusCode, gin.H{"error": httpError.Err.Error()})
|
||||||
return
|
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()})
|
ac.C.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
func FetchBookGet(db *gorm.DB, userId uint, bookId uint64) (dto.FullBookGet, error) {
|
func FetchBookGet(db *gorm.DB, userId uint, bookId uint64) (dto.FullBookGet, error) {
|
||||||
var book dto.FullBookGet
|
var book dto.FullBookGet
|
||||||
query := db.Model(&model.Book{})
|
query := db.Model(&model.Book{})
|
||||||
selectQueryString := "books.title, authors.name as author, authors.id as author_id, books.isbn, books.inventaire_id, books.open_library_id, books.summary, " +
|
selectQueryString := "books.title, authors.name as author, authors.id as author_id, books.isbn, books.inventaire_id, books.open_library_id, books.short_description, books.summary, " +
|
||||||
"user_books.review, user_books.rating, user_books.read, user_books.want_read, " +
|
"user_books.review, user_books.rating, user_books.read, user_books.want_read, " +
|
||||||
"DATE(user_books.start_read_date) as start_read_date, " +
|
"DATE(user_books.start_read_date) as start_read_date, " +
|
||||||
"DATE(user_books.end_read_date) AS end_read_date, " +
|
"DATE(user_books.end_read_date) AS end_read_date, " +
|
||||||
@@ -181,7 +181,7 @@ func fetchBookQueryBuilder(db *gorm.DB, userId uint) *gorm.DB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func selectBookItem() string {
|
func selectBookItem() string {
|
||||||
return "books.id, books.title, authors.name as author, books.small_description as description, books.inventaire_id, user_books.rating, user_books.read, DATE(user_books.start_read_date) as start_read_date, user_books.want_read, " + selectStaticFilesPath()
|
return "books.id, books.title, authors.name as author, books.short_description as description, books.inventaire_id, user_books.rating, user_books.read, DATE(user_books.start_read_date) as start_read_date, user_books.want_read, " + selectStaticFilesPath()
|
||||||
}
|
}
|
||||||
|
|
||||||
func selectStaticFilesPath() string {
|
func selectStaticFilesPath() string {
|
||||||
126
internal/query/querycollections.go
Normal file
126
internal/query/querycollections.go
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
package query
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.artlef.fr/bibliomane/internal/model"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CollectionHeader struct {
|
||||||
|
Name string
|
||||||
|
UserID uint
|
||||||
|
}
|
||||||
|
|
||||||
|
// collection header without the books
|
||||||
|
func FetchCollectionHeader(db *gorm.DB, collectionId uint) (CollectionHeader, error) {
|
||||||
|
var collection CollectionHeader
|
||||||
|
query := db.Model(&model.Collection{})
|
||||||
|
query = query.Select("collections.name, collections.user_id")
|
||||||
|
query = query.Where("collections.id = ?", collectionId)
|
||||||
|
res := query.Find(&collection)
|
||||||
|
return collection, res.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
type CollectionsQueryResult struct {
|
||||||
|
ID uint
|
||||||
|
UserID uint
|
||||||
|
Name string
|
||||||
|
BookId uint
|
||||||
|
BookTitle string
|
||||||
|
CoverPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
type collectionId struct {
|
||||||
|
ID uint
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchAllCollections(db *gorm.DB, userId uint, limit int, offset int) ([]CollectionsQueryResult, error) {
|
||||||
|
var collections []CollectionsQueryResult
|
||||||
|
var collectionIds []collectionId
|
||||||
|
res := fetchCollections(db, userId).Limit(limit).Offset(offset).Order("collections.id DESC").Find(&collectionIds)
|
||||||
|
if res.Error != nil {
|
||||||
|
return collections, res.Error
|
||||||
|
}
|
||||||
|
for _, collectionId := range collectionIds {
|
||||||
|
//only takes first 8 books
|
||||||
|
queryResults, err := fetchCollectionItemBook(db, collectionId.ID, 8, 0)
|
||||||
|
if err != nil {
|
||||||
|
return collections, res.Error
|
||||||
|
}
|
||||||
|
collections = append(collections, queryResults...)
|
||||||
|
}
|
||||||
|
return collections, res.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchCollectionItemBook(db *gorm.DB, collectionId uint, limit int, offset int) ([]CollectionsQueryResult, error) {
|
||||||
|
var collections []CollectionsQueryResult
|
||||||
|
query := fetchCollectionItemBooksQuery(db, collectionId)
|
||||||
|
query = query.Limit(limit)
|
||||||
|
query = query.Offset(offset)
|
||||||
|
res := query.Find(&collections)
|
||||||
|
return collections, res.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchCollectionItemBooksQuery(db *gorm.DB, collectionId uint) *gorm.DB {
|
||||||
|
query := db.Model(&model.Collection{})
|
||||||
|
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_items on (collection_items.collection_id = collections.id)")
|
||||||
|
query = query.Joins("left join books on (books.id = collection_items.book_id)")
|
||||||
|
query = joinStaticFiles(query)
|
||||||
|
query = query.Where("collections.id = ?", collectionId)
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchAllCollectionsCount(db *gorm.DB, userId uint) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
res := fetchCollections(db, userId).Count(&count)
|
||||||
|
return count, res.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchCollections(db *gorm.DB, userId uint) *gorm.DB {
|
||||||
|
return db.Model(&model.Collection{}).Where("collections.user_id = ?", userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CollectionItemQueryResult struct {
|
||||||
|
ItemID uint
|
||||||
|
Position uint
|
||||||
|
ID uint
|
||||||
|
Title string
|
||||||
|
Author string
|
||||||
|
Description string
|
||||||
|
InventaireID string
|
||||||
|
IsInventaireEdition bool
|
||||||
|
Rating int
|
||||||
|
Read bool
|
||||||
|
StartReadDate string
|
||||||
|
WantRead bool
|
||||||
|
CoverPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchCollectionItems(db *gorm.DB, userId uint, collectionId uint, limit int, offset int) ([]CollectionItemQueryResult, error) {
|
||||||
|
var collectionitems []CollectionItemQueryResult
|
||||||
|
query := fetchCollectionBooksQuery(db, userId, collectionId)
|
||||||
|
query = query.Limit(limit)
|
||||||
|
query = query.Offset(offset)
|
||||||
|
query = query.Order("collection_items.position")
|
||||||
|
res := query.Find(&collectionitems)
|
||||||
|
return collectionitems, res.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchCollectionBooksCount(db *gorm.DB, userId uint, collectionId uint) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
res := fetchCollectionBooksQuery(db, userId, collectionId).Count(&count)
|
||||||
|
return count, res.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchCollectionBooksQuery(db *gorm.DB, userId uint, collectionId uint) *gorm.DB {
|
||||||
|
query := db.Model(&model.CollectionItem{})
|
||||||
|
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)
|
||||||
|
query = query.Joins("left join user_books on (user_books.book_id = books.id and user_books.user_id = ?)", userId)
|
||||||
|
query = joinStaticFiles(query)
|
||||||
|
query = query.Order("collection_items.position")
|
||||||
|
query = query.Where("collections.id = ?", collectionId)
|
||||||
|
return query
|
||||||
|
}
|
||||||
@@ -2,72 +2,63 @@ package routes
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.artlef.fr/bibliomane/internal/adapter"
|
||||||
"git.artlef.fr/bibliomane/internal/appcontext"
|
"git.artlef.fr/bibliomane/internal/appcontext"
|
||||||
"git.artlef.fr/bibliomane/internal/dto"
|
"git.artlef.fr/bibliomane/internal/dto"
|
||||||
|
"git.artlef.fr/bibliomane/internal/i18nresource"
|
||||||
"git.artlef.fr/bibliomane/internal/model"
|
"git.artlef.fr/bibliomane/internal/model"
|
||||||
"git.artlef.fr/bibliomane/internal/myvalidator"
|
"git.artlef.fr/bibliomane/internal/myvalidator"
|
||||||
"gorm.io/gorm"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func PostBookHandler(ac appcontext.AppContext) {
|
func PostBookHandler(ac appcontext.AppContext) {
|
||||||
var book dto.BookPostCreate
|
var book dto.BookFields
|
||||||
err := ac.C.ShouldBindJSON(&book)
|
err := ac.C.ShouldBindJSON(&book)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = myvalidator.ValidateId(ac.Db, book.CoverID, &model.StaticFile{})
|
//when creating a book, title is required
|
||||||
if err != nil {
|
if book.Title == nil {
|
||||||
|
err = myvalidator.HttpError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Err: errors.New(i18nresource.GetTranslatedMessage(&ac, "ValidationRequired")),
|
||||||
|
}
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
return
|
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()
|
user, fetchUserErr := ac.GetAuthenticatedUser()
|
||||||
if fetchUserErr != nil {
|
if fetchUserErr != nil {
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = saveBookToDb(ac, book, &user)
|
id, err := saveBookToDb(ac, book, &user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ac.C.String(200, "Success")
|
ac.C.JSON(http.StatusOK, gin.H{"id": id})
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveBookToDb(ac appcontext.AppContext, b dto.BookPostCreate, user *model.User) error {
|
func saveBookToDb(ac appcontext.AppContext, b dto.BookFields, user *model.User) (uint, error) {
|
||||||
author, err := fetchOrCreateAuthor(ac, b.Author)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
book := model.Book{
|
book := model.Book{
|
||||||
Title: b.Title,
|
AddedBy: *user,
|
||||||
AuthorID: author.ID,
|
|
||||||
AddedBy: *user,
|
|
||||||
}
|
}
|
||||||
if b.CoverID > 0 {
|
err := adapter.FillBookDbFromFields(ac, &b, &book)
|
||||||
book.CoverID = b.CoverID
|
|
||||||
}
|
|
||||||
return ac.Db.Save(&book).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
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 err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
return 0, err
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = ac.Db.Save(&book).Error
|
||||||
|
return book.ID, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,9 @@ package routes
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.artlef.fr/bibliomane/internal/appcontext"
|
"git.artlef.fr/bibliomane/internal/appcontext"
|
||||||
"git.artlef.fr/bibliomane/internal/babelio"
|
|
||||||
"git.artlef.fr/bibliomane/internal/dto"
|
"git.artlef.fr/bibliomane/internal/dto"
|
||||||
"git.artlef.fr/bibliomane/internal/fileutils"
|
"git.artlef.fr/bibliomane/internal/fileutils"
|
||||||
"git.artlef.fr/bibliomane/internal/inventaire"
|
"git.artlef.fr/bibliomane/internal/inventaire"
|
||||||
@@ -45,7 +43,7 @@ func PostImportBookHandler(ac appcontext.AppContext) {
|
|||||||
func saveInventaireBookToDb(ac appcontext.AppContext, inventaireEdition inventaire.InventaireEditionDetailedSingleResult, user *model.User) (*model.Book, error) {
|
func saveInventaireBookToDb(ac appcontext.AppContext, inventaireEdition inventaire.InventaireEditionDetailedSingleResult, user *model.User) (*model.Book, error) {
|
||||||
book := model.Book{
|
book := model.Book{
|
||||||
Title: inventaireEdition.Title,
|
Title: inventaireEdition.Title,
|
||||||
SmallDescription: inventaireEdition.Description,
|
ShortDescription: inventaireEdition.Description,
|
||||||
InventaireID: inventaireEdition.Id,
|
InventaireID: inventaireEdition.Id,
|
||||||
AddedBy: *user,
|
AddedBy: *user,
|
||||||
}
|
}
|
||||||
@@ -66,25 +64,6 @@ func saveInventaireBookToDb(ac appcontext.AppContext, inventaireEdition inventai
|
|||||||
book.Cover = cover
|
book.Cover = cover
|
||||||
}
|
}
|
||||||
|
|
||||||
if ac.Config.BookDescriptionFromBabelio {
|
|
||||||
isbn := findIsbn(&inventaireEdition)
|
|
||||||
if isbn != "" {
|
|
||||||
desc, err := babelio.GetDescriptionFromISBN(ac.Config.BabelioUrl, isbn)
|
|
||||||
if err != nil {
|
|
||||||
te, isTrError := errors.AsType[myvalidator.TranslatedError](err)
|
|
||||||
var errToPrint string
|
|
||||||
if isTrError {
|
|
||||||
errToPrint = te.ToTranslatedMessage(&ac)
|
|
||||||
} else {
|
|
||||||
errToPrint = err.Error()
|
|
||||||
}
|
|
||||||
log.Println(errToPrint)
|
|
||||||
} else {
|
|
||||||
book.Summary = desc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err := ac.Db.Save(&book).Error
|
err := ac.Db.Save(&book).Error
|
||||||
return &book, err
|
return &book, err
|
||||||
}
|
}
|
||||||
|
|||||||
43
internal/routes/bookputupdate.go
Normal file
43
internal/routes/bookputupdate.go
Normal 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")
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ func GetSearchBooksHandler(ac appcontext.AppContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var returnedBooks dto.BookItemsGet
|
var returnedBooks dto.BookItemsGet
|
||||||
if !params.Inventaire {
|
if params.Inventaire != dto.ForceInventaireSearch {
|
||||||
books, err := query.FetchBookSearchGet(ac.Db, user.ID, searchterm, limit, offset)
|
books, err := query.FetchBookSearchGet(ac.Db, user.ID, searchterm, limit, offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
@@ -51,7 +51,7 @@ func GetSearchBooksHandler(ac appcontext.AppContext) {
|
|||||||
}
|
}
|
||||||
returnedBooks = dto.BookItemsGet{Count: count, Inventaire: false, Books: books}
|
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)
|
returnedBooksPtr, err := searchInInventaireAPI(ac.Config.InventaireUrl, searchterm, limit, offset, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
|||||||
72
internal/routes/collectionaddbookpost.go
Normal file
72
internal/routes/collectionaddbookpost.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
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"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
//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 == newPosition {
|
||||||
|
//nothing to do
|
||||||
|
ac.C.String(http.StatusOK, "Success")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lowerPosition := item.Position + 1
|
||||||
|
higherPosition := item.Position - 1
|
||||||
|
operationToDo := ""
|
||||||
|
if item.Position < newPosition {
|
||||||
|
higherPosition = newPosition
|
||||||
|
operationToDo = "position - 1"
|
||||||
|
} else {
|
||||||
|
lowerPosition = newPosition
|
||||||
|
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 = newPosition
|
||||||
|
ac.Db.Save(&item)
|
||||||
|
ac.C.String(http.StatusOK, "Success")
|
||||||
|
}
|
||||||
68
internal/routes/collectionget.go
Normal file
68
internal/routes/collectionget.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"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/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
|
||||||
|
}
|
||||||
|
collectionHeader, err := query.FetchCollectionHeader(ac.Db, uint(collectionId))
|
||||||
|
if collectionHeader.UserID != user.ID {
|
||||||
|
err := myvalidator.HttpError{
|
||||||
|
StatusCode: http.StatusUnauthorized,
|
||||||
|
Err: errors.New(i18nresource.GetTranslatedMessage(&ac, "Unauthorized")),
|
||||||
|
}
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
collection := dto.CollectionGet{Name: collectionHeader.Name}
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
itemsQueryResult, err := query.FetchCollectionItems(ac.Db, user.ID, uint(collectionId), limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items := adapter.CollectionItemsQueryToDto(itemsQueryResult)
|
||||||
|
collection.Items = items
|
||||||
|
count, err := query.FetchCollectionBooksCount(ac.Db, user.ID, uint(collectionId))
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
collection.Count = count
|
||||||
|
ac.C.JSON(http.StatusOK, collection)
|
||||||
|
}
|
||||||
41
internal/routes/collectionsget.go
Normal file
41
internal/routes/collectionsget.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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/myvalidator"
|
||||||
|
"git.artlef.fr/bibliomane/internal/query"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetCollectionsHandler(ac appcontext.AppContext) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
collectionsDb, err := query.FetchAllCollections(ac.Db, user.ID, limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
collections := adapter.CollectionQueryToCollectionItemDto(collectionsDb)
|
||||||
|
count, err := query.FetchAllCollectionsCount(ac.Db, user.ID)
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ac.C.JSON(http.StatusOK, dto.CollectionItemsGet{Count: count, Collections: collections})
|
||||||
|
}
|
||||||
41
internal/routes/collectionspost.go
Normal file
41
internal/routes/collectionspost.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"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 PostCollectionHandler(ac appcontext.AppContext) {
|
||||||
|
var collection dto.CollectionFields
|
||||||
|
err := ac.C.ShouldBindJSON(&collection)
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user, fetchUserErr := ac.GetAuthenticatedUser()
|
||||||
|
if fetchUserErr != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := saveCollectionToDb(ac, &collection, &user)
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ac.C.JSON(http.StatusOK, gin.H{"id": id})
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveCollectionToDb(ac appcontext.AppContext, c *dto.CollectionFields, user *model.User) (uint, error) {
|
||||||
|
collection := model.Collection{
|
||||||
|
Name: c.Name,
|
||||||
|
User: *user,
|
||||||
|
}
|
||||||
|
err := ac.Db.Save(&collection).Error
|
||||||
|
return collection.ID, err
|
||||||
|
}
|
||||||
@@ -63,6 +63,9 @@ func Setup(config *config.Config) *gin.Engine {
|
|||||||
ws.PUT("/book/:id", func(c *gin.Context) {
|
ws.PUT("/book/:id", func(c *gin.Context) {
|
||||||
routes.PutUserBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
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) {
|
ws.POST("/book", func(c *gin.Context) {
|
||||||
routes.PostBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
routes.PostBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||||
})
|
})
|
||||||
@@ -75,6 +78,21 @@ func Setup(config *config.Config) *gin.Engine {
|
|||||||
ws.GET("/author/:id/books", func(c *gin.Context) {
|
ws.GET("/author/:id/books", func(c *gin.Context) {
|
||||||
routes.GetAuthorBooksHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
routes.GetAuthorBooksHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||||
})
|
})
|
||||||
|
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/: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})
|
||||||
|
})
|
||||||
ws.POST("/auth/signup", func(c *gin.Context) {
|
ws.POST("/auth/signup", func(c *gin.Context) {
|
||||||
routes.PostSignupHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
routes.PostSignupHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -65,3 +67,81 @@ func TestBookPutCallWithDemoPayload(t *testing.T, payload string, bookId string,
|
|||||||
t.Errorf("%s", w.Body.String())
|
t.Errorf("%s", w.Body.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()
|
||||||
|
|
||||||
|
u, err := url.Parse(urlpath)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if limit != "" {
|
||||||
|
q := u.Query()
|
||||||
|
q.Set("limit", limit)
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
}
|
||||||
|
if offset != "" {
|
||||||
|
q := u.Query()
|
||||||
|
q.Set("offset", offset)
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
q := u.Query()
|
||||||
|
q.Set("lang", "fr")
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
token := ConnectDemoUser(router)
|
||||||
|
req, _ := http.NewRequest("GET", u.String(), nil)
|
||||||
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
var result T
|
||||||
|
s := w.Body.String()
|
||||||
|
err = json.Unmarshal([]byte(s), &result)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
return w.Code, result
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostCall(t *testing.T, urlpath string, payload string) (int, uint) {
|
||||||
|
router := TestSetup()
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
token := ConnectDemoUser(router)
|
||||||
|
req, _ := http.NewRequest("POST", urlpath,
|
||||||
|
strings.NewReader(payload))
|
||||||
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
return w.Code, 0
|
||||||
|
}
|
||||||
|
var parsed struct {
|
||||||
|
ID uint
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.Unmarshal(w.Body.Bytes(), &parsed)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
return w.Code, parsed.ID
|
||||||
|
}
|
||||||
|
|||||||
2
main.go
2
main.go
@@ -6,7 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
applicationVersion := "0.6.0"
|
applicationVersion := "0.7.0"
|
||||||
c := config.LoadConfig(applicationVersion)
|
c := config.LoadConfig(applicationVersion)
|
||||||
r := setup.Setup(&c)
|
r := setup.Setup(&c)
|
||||||
r.Run(":" + c.Port)
|
r.Run(":" + c.Port)
|
||||||
|
|||||||
Reference in New Issue
Block a user