Compare commits
84 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ed99ee772a | |||
| 2f5fc3d0a3 | |||
| b48c42c40c | |||
| 08a273b500 | |||
| 1ae76ed525 | |||
| 7d867af654 | |||
| 11a23d174e | |||
| e746e67e89 | |||
| b47b09eb85 | |||
| 8d9431874f | |||
| 3c621c01ce | |||
| 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 | |||
| b8eacb9c10 | |||
| e05c9f2b45 | |||
| 726c640657 | |||
| 7b5da2df61 | |||
| 57a41e0e3e | |||
| bc077f176e | |||
| 9c18206483 | |||
| d8fc7396ff | |||
| 4d687e3dcb | |||
| 1da482c2ad | |||
| 83088c689e | |||
| 950340beed | |||
| 315d7db56a | |||
| 9db7957ad3 | |||
| 5e6715d586 | |||
| 843c5b5dbc | |||
| c4390742b3 | |||
| 0efc3629b0 | |||
| a77d57603f | |||
| a023c97618 | |||
| 67c475f14c | |||
| be5be81cbd | |||
| bc75334590 | |||
| 7fdadf4b0b | |||
| 97198efb1c | |||
| 2d0bce143a | |||
| 524e517066 | |||
| d07f18d380 | |||
| f32bb49972 | |||
| 8290f77889 | |||
| ce8145a42e | |||
| 3064235a80 | |||
| 17068aa28c | |||
| aee6fbaf73 | |||
| 0d591c0fa9 | |||
| 898846c654 | |||
| 65127c2273 | |||
| 55e80181df | |||
| f01dfa01cb | |||
| d398de1b47 | |||
| 8a707610bf | |||
| 2a1d8e13c8 | |||
| 93757126e1 | |||
| e8e2df3c43 | |||
| 28e86e5032 |
@@ -3,7 +3,7 @@ FROM node:lts AS buildfront
|
|||||||
COPY front .
|
COPY front .
|
||||||
RUN npm install && npm run build
|
RUN npm install && npm run build
|
||||||
|
|
||||||
FROM golang:1.25 AS build
|
FROM golang:1.26 AS build
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY . .
|
COPY . .
|
||||||
COPY --from=buildfront ./dist front/dist
|
COPY --from=buildfront ./dist front/dist
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ Or with a volume, for example if you created a volume named `bibliomane_data`:
|
|||||||
|
|
||||||
`--add-user` or `-a` can be used to create an account on startup. It requires a string following htpasswd format `[username]:[bcrypt hashed password]`.
|
`--add-user` or `-a` can be used to create an account on startup. It requires a string following htpasswd format `[username]:[bcrypt hashed password]`.
|
||||||
|
|
||||||
The password can be generated using `htpasswd -nB [username]`.
|
The password can be generated using `htpasswd -nBC10 [username]`.
|
||||||
|
|
||||||
For example, to create an user account `demo`:
|
For example, to create an user account `demo`:
|
||||||
|
|
||||||
|
|||||||
52
demodata.sql
52
demodata.sql
@@ -69,9 +69,9 @@ INSERT INTO user_books(created_at, user_id, book_id, read, rating) VALUES ('NOW'
|
|||||||
INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'Le petit bleu de la côte Ouest',(SELECT id FROM authors WHERE name = 'Jean-Patrick Manchette'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'le-petit-bleu-de-la-cote-ouest.jpg'));
|
INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'Le petit bleu de la côte Ouest',(SELECT id FROM authors WHERE name = 'Jean-Patrick Manchette'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'le-petit-bleu-de-la-cote-ouest.jpg'));
|
||||||
INSERT INTO user_books(created_at, user_id, book_id, want_read, rating) VALUES ('NOW',(SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM books WHERE title = 'Le petit bleu de la côte Ouest'), true,0);
|
INSERT INTO user_books(created_at, user_id, book_id, want_read, rating) VALUES ('NOW',(SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM books WHERE title = 'Le petit bleu de la côte Ouest'), true,0);
|
||||||
INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'D''un château l''autre',(SELECT id FROM authors WHERE name = 'Louis-Ferdinand Céline'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'dunchateaulautre.jpg'));
|
INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'D''un château l''autre',(SELECT id FROM authors WHERE name = 'Louis-Ferdinand Céline'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'dunchateaulautre.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 = 'D''un château l''autre'), true,10);
|
INSERT INTO user_books(created_at, user_id, book_id, read, rating, review) VALUES ('NOW',(SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM books WHERE title = 'D''un château l''autre'), true,10, "Lorem ipsum dolor sit amet, consectetur adipisci elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.");
|
||||||
INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'Les dieux ont soif',(SELECT id FROM authors WHERE name = 'Anatole France'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'lesdieuxontsoif.jpg'));
|
INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'Les dieux ont soif',(SELECT id FROM authors WHERE name = 'Anatole France'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'lesdieuxontsoif.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 = 'Les dieux ont soif'), true,7);
|
INSERT INTO user_books(created_at, user_id, book_id, read, start_read_date, end_read_date, rating) VALUES ('NOW',(SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM books WHERE title = 'Les dieux ont soif'), true,'2026-01-30 00:00:00+00:00','2026-02-13 00:00:00+00:00',7);
|
||||||
INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'Rigodon',(SELECT id FROM authors WHERE name = 'Louis-Ferdinand Céline'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'rigodon.jpg'));
|
INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'Rigodon',(SELECT id FROM authors WHERE name = 'Louis-Ferdinand Céline'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'rigodon.jpg'));
|
||||||
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 = 'Rigodon'),true, 10);
|
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 = 'Rigodon'),true, 10);
|
||||||
INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'Un barrage contre le Pacifique',(SELECT id FROM authors WHERE name = 'Marguerite Duras'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'Un_barrage_contre_le_Pacifique.jpg'));
|
INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'Un barrage contre le Pacifique',(SELECT id FROM authors WHERE name = 'Marguerite Duras'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'Un_barrage_contre_le_Pacifique.jpg'));
|
||||||
@@ -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,49 @@ 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 collections(name, user_id) VALUES ('Brouillon',(SELECT id FROM users WHERE name = 'demo'));
|
||||||
|
INSERT INTO collections(name, user_id) VALUES ('Traduit de l''anglais',(SELECT id FROM users WHERE name = 'demo'));
|
||||||
|
INSERT INTO collections(name, user_id) VALUES ('À supprimer',(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);
|
||||||
|
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Brouillon'), (SELECT id FROM books WHERE title = 'Nord'), 1);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Brouillon'), (SELECT id FROM books WHERE title = 'Gargantua'), 2);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Brouillon'), (SELECT id FROM books WHERE title = 'Duo'), 3);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Brouillon'), (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 = 'Brouillon'), (SELECT id FROM books WHERE title = 'Rigodon'), 5);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Brouillon'), (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 = 'Traduit de l''anglais'), (SELECT id FROM books WHERE title = 'Sa majesté des mouches'), 1);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Traduit de l''anglais'), (SELECT id FROM books WHERE title = 'Le complot contre l''Amérique'), 2);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Traduit de l''anglais'), (SELECT id FROM books WHERE title = 'De sang-froid'), 3);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<html lang="">
|
<html lang="">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.svg" />
|
||||||
<link rel="stylesheet" href="/css/bulma.min.css" />
|
<link rel="stylesheet" href="/css/bulma.min.css" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Bibliomane</title>
|
<title>Bibliomane</title>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "bibliomane",
|
"name": "bibliomane",
|
||||||
"version": "0.2.0",
|
"version": "0.8.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
15
front/public/favicon.svg
Normal file
15
front/public/favicon.svg
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
<svg width="125.95975mm" height="132.42345mm" viewBox="0 0 125.95975 132.42345" version="1.1" id="svg1008" inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)" sodipodi:docname="logo_carré.svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview id="namedview1010" pagecolor="#505050" bordercolor="#ffffff" borderopacity="1" inkscape:pageshadow="0" inkscape:pageopacity="0" inkscape:pagecheckerboard="1" inkscape:document-units="mm" showgrid="false" fit-margin-top="0" fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0" inkscape:zoom="0.77771465" inkscape:cx="80.363666" inkscape:cy="297.66702" inkscape:window-width="2560" inkscape:window-height="1369" inkscape:window-x="1912" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="layer1"/>
|
||||||
|
<defs id="defs1005">
|
||||||
|
<linearGradient inkscape:collect="always" xlink:href="#linearGradient29626-5" id="linearGradient41043" gradientUnits="userSpaceOnUse" x1="-274.68173" y1="86.602715" x2="-136.50122" y2="141.29916" gradientTransform="matrix(0,-0.975265,0.97958512,0,44.404193,-64.657592)"/>
|
||||||
|
<linearGradient id="linearGradient29626-5" inkscape:swatch="gradient">
|
||||||
|
<stop style="stop-color:#cd82d1;stop-opacity:1;" offset="0" id="stop29622"/>
|
||||||
|
<stop style="stop-color:#45e4be;stop-opacity:1" offset="1" id="stop29624"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(-83.64899,-69.870748)">
|
||||||
|
<path id="rect33397" style="fill:url(#linearGradient41043);stroke-width:14.6613;stroke-linecap:round;paint-order:markers fill stroke" d="m 123.59901,69.870748 c -22.13223,0 -39.95002,17.817785 -39.95002,39.950012 v 52.52341 c 0,22.13223 17.81779,39.95003 39.95002,39.95003 h 46.05972 c 22.13224,0 39.95001,-17.8178 39.95001,-39.95003 v -52.52341 c 0,-22.132227 -17.81777,-39.950012 -39.95001,-39.950012 z m 16.04451,10.09246 c 2.89943,-0.025 5.27029,2.304907 5.2958,5.204333 l 0.093,10.535274 c 0.0259,2.899434 -2.30336,5.270995 -5.20278,5.297355 -2.89984,0.0256 -5.27133,-2.304505 -5.29684,-5.204338 l -0.093,-10.536825 c -0.025,-2.89923 2.30459,-5.270008 5.20381,-5.295799 z m 22.37162,9.874874 c 1.39244,-0.02341 2.73714,0.507288 3.73828,1.475362 2.08486,2.015053 2.14177,5.338588 0.12712,7.423838 l -8.88628,9.193748 c -2.01549,2.08523 -5.33978,2.14171 -7.42487,0.12609 -2.08423,-2.01528 -2.14067,-5.33841 -0.12609,-7.42332 l 8.8868,-9.194266 c 0.96744,-1.001274 2.29295,-1.577317 3.68504,-1.601452 z m -36.12428,8.403542 c 0.077,-0.0011 0.15435,-0.0011 0.23151,5.3e-4 0.30864,0.006 0.61792,0.03233 0.92604,0.08113 2.46499,0.38998 4.30183,1.812396 5.71283,3.422536 l 0.03,0.0341 9.57203,11.31249 h 19.29081 c 5.4245,0 10.14805,2.49331 13.13356,6.02857 2.98552,3.53526 4.46133,7.94487 4.60799,12.361 0.13508,4.06728 -0.94484,8.23683 -3.47576,11.72228 1.71448,1.01562 3.30442,2.15465 4.61729,3.52227 4.26817,4.44617 6.19882,10.33751 6.02082,16.07241 -0.35602,11.46979 -9.7863,22.94071 -24.73595,22.94071 h -24.1298 l -18.22317,-18.95904 0.063,0.063 c -11.56249,-11.62674 -15.08369,-24.70477 -13.06949,-34.38395 1.0071,-4.83959 3.55434,-9.28947 8.20829,-11.16469 0.37422,-0.15075 0.77822,-0.13389 1.1622,-0.23771 -0.63978,-3.80679 -0.50295,-7.69555 0.10542,-11.12801 0.64248,-3.62486 1.54593,-6.92763 4.57957,-9.60974 1.13761,-1.005806 2.85512,-1.845387 4.68291,-2.037607 v 5.29e-4 c 0.22848,-0.02416 0.45874,-0.03788 0.68989,-0.04082 z m -0.38396,11.934156 c -0.60269,0.009 -1.13008,0.50778 -1.28416,1.3069 -1.14194,5.72294 -0.28389,9.83155 3.99252,14.50919 0,0 5.3e-4,7.9e-4 0.001,10e-4 l 0.077,0.0739 0.0119,0.0124 6.21357,6.22855 -0.01,-12.24059 -7.90029,-9.33742 c -0.35161,-0.388 -0.74031,-0.55941 -1.10174,-0.55397 z m 19.50372,13.41779 v 15.93959 h 16.68012 0.0574 c 3.0387,0.0385 4.41726,-0.84879 5.48907,-2.13579 1.07456,-1.29032 1.73633,-3.34125 1.66243,-5.56658 -0.0739,-2.22533 -0.90166,-4.47159 -2.13579,-5.93297 -1.23413,-1.46138 -2.64641,-2.30425 -5.10976,-2.30425 z m -25.58914,8.07651 c -0.20944,2.6e-4 -0.43094,0.0238 -0.66714,0.0734 -1.64588,0.45199 -2.71995,2.05325 -3.14503,4.38784 -1.47473,6.99382 0.018,11.95115 11.36778,23.30814 l 0.0315,0.0315 7.50031,7.80211 v -20.75429 l -12.90825,-13.80742 c -0.54958,-0.56585 -1.16751,-0.97593 -1.97352,-1.03405 -0.0672,-0.005 -0.13586,-0.007 -0.20568,-0.007 z m 25.58914,18.36425 v 25.20363 H 161.822 c 9.92523,0 14.04184,-6.38226 14.23995,-12.7646 0.0991,-3.19116 -0.94119,-6.225 -3.09955,-8.47338 -2.15835,-2.24838 -5.52091,-3.96565 -11.1404,-3.96565 h -0.2434 z" sodipodi:nodetypes="sssssssssccccccccccsccccssccccssscssscccsscssccssccccccccsccccsssscscssccccssccssssccscs"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.8 KiB |
17
front/public/image/logo.svg
Normal file
17
front/public/image/logo.svg
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
<svg width="80.642197mm" height="104.19308mm" viewBox="0 0 80.642197 104.19308" version="1.1" id="svg863" sodipodi:docname="logo_trait.svg" inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview id="namedview865" pagecolor="#505050" bordercolor="#ffffff" borderopacity="1" inkscape:pageshadow="0" inkscape:pageopacity="0" inkscape:pagecheckerboard="1" inkscape:document-units="mm" showgrid="false" inkscape:zoom="1.0998546" inkscape:cx="111.37836" inkscape:cy="219.57448" inkscape:window-width="2560" inkscape:window-height="1369" inkscape:window-x="1912" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="layer1" fit-margin-top="0" fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0"/>
|
||||||
|
<defs id="defs860">
|
||||||
|
<linearGradient inkscape:collect="always" xlink:href="#linearGradient29626-5" id="linearGradient29653" gradientUnits="userSpaceOnUse" x1="180.15134" y1="639.77362" x2="387.26251" y2="162.77214" gradientTransform="matrix(0.18523379,0,0,0.18523379,50.465238,49.46306)"/>
|
||||||
|
<linearGradient id="linearGradient29626-5" inkscape:swatch="gradient">
|
||||||
|
<stop style="stop-color:#cd82d1;stop-opacity:1;" offset="0" id="stop29622"/>
|
||||||
|
<stop style="stop-color:#45e4be;stop-opacity:1" offset="1" id="stop29624"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(-76.709803,-68.336456)">
|
||||||
|
<path id="path11547" style="color:#000000;fill:url(#linearGradient29653);fill-opacity:1;fill-rule:nonzero;stroke-width:0.700097;-inkscape-stroke:none" d="m 96.905008,85.032291 c -0.30864,-0.0061 -0.61647,0.0081 -0.9211,0.04016 v -3.61e-4 c -1.82779,0.192211 -3.54569,1.031778 -4.6833,2.037572 -3.03364,2.682116 -3.937,5.984852 -4.57947,9.609724 -0.60837,3.432464 -0.74506,7.320994 -0.10528,11.127784 -0.38397,0.10382 -0.78784,0.0873 -1.16206,0.23805 -4.65395,1.87521 -7.20143,6.32509 -8.20853,11.16468 -2.01421,9.67918 1.50698,22.75728 13.06947,34.38402 l -0.063,-0.063 18.223102,18.95861 h 24.12996 c 14.94966,0 24.37993,-11.47061 24.73595,-22.94041 0.178,-5.7349 -1.75266,-11.62612 -6.02083,-16.07229 -1.31287,-1.36763 -2.90298,-2.50673 -4.61746,-3.52233 2.53092,-3.48546 3.61075,-7.65527 3.47567,-11.72255 -0.14666,-4.41613 -1.62253,-8.82548 -4.60805,-12.36074 -2.98551,-3.53526 -7.70901,-6.028784 -13.13351,-6.028784 h -19.29073 l -9.57173,-11.312289 -0.03,-0.03437 c -1.411,-1.610146 -3.24796,-3.032503 -5.712952,-3.422483 -0.30812,-0.04875 -0.61753,-0.07496 -0.92617,-0.08104 z m 0.48588,12.487005 7.900662,9.338024 0.01,12.24061 -6.213462,-6.22873 -0.0123,-0.0123 -0.0767,-0.0738 c -3.7e-4,-3.6e-4 -0.001,-0.001 -0.001,-0.001 -4.27641,-4.67764 -5.13453,-8.78607 -3.9926,-14.509014 0.24652,-1.27859 1.44807,-1.7891 2.38607,-0.75336 z m 18.402182,12.864344 h 16.64355 c 2.46335,0 3.87571,0.84282 5.10984,2.3042 1.23413,1.46139 2.06207,3.70758 2.13598,5.93291 0.0739,2.22533 -0.5882,4.27646 -1.66276,5.56678 -1.07181,1.28701 -2.44994,2.17408 -5.48864,2.13562 h -0.0575 -16.68045 z m -26.256232,8.15022 c 1.25974,-0.2642 2.11382,0.21302 2.84659,0.96748 l 12.908122,13.80751 v 20.75451 l -7.500162,-7.80225 -0.0315,-0.0315 c -11.34978,-11.35699 -12.84262,-16.31443 -11.3679,-23.30824 0.42508,-2.33459 1.49895,-3.93552 3.14483,-4.38751 z m 26.256232,18.29082 h 16.5683 0.24348 c 5.61949,0 8.98172,1.71714 11.14007,3.96552 2.15836,2.24838 3.19883,5.2822 3.09978,8.47336 -0.19811,6.38234 -4.31462,12.76449 -14.23985,12.76449 h -16.81178 z" sodipodi:nodetypes="cccssccscccssscssscccsccccccccccccssssccccscccccscccssssccc"/>
|
||||||
|
<path style="color:#000000;fill:#60d1c2;fill-opacity:1;stroke-width:0.653215;stroke-linecap:round;-inkscape-stroke:none" d="m 111.75634,68.336639 a 4.8991144,4.8991144 0 0 0 -4.85575,4.941216 l 0.0868,9.831401 a 4.8991144,4.8991144 0 0 0 4.9425,4.855737 4.8991144,4.8991144 0 0 0 4.85445,-4.942493 l -0.0868,-9.830124 a 4.8991144,4.8991144 0 0 0 -4.94122,-4.855737 z" id="path23529"/>
|
||||||
|
<path style="color:#000000;fill:#60d1c2;fill-opacity:1;stroke-width:0.606037;stroke-linecap:round;-inkscape-stroke:none" d="m 132.27231,78.931592 a 4.442115,4.6508356 0 0 0 -3.16127,1.307975 l -7.77474,7.875191 a 4.442115,4.6508356 0 0 0 -0.10429,6.5754 4.442115,4.6508356 0 0 0 6.28146,0.109238 l 7.77474,-7.87519 a 4.442115,4.6508356 0 0 0 0.10314,-6.57544 4.442115,4.6508356 0 0 0 -3.11904,-1.417174 z" id="path23531"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.6 KiB |
@@ -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>
|
||||||
@@ -13,7 +13,7 @@ const router = useRouter()
|
|||||||
const isMenuActive = ref(false)
|
const isMenuActive = ref(false)
|
||||||
const isSearchBarShown = ref(false)
|
const isSearchBarShown = ref(false)
|
||||||
|
|
||||||
const appVersion = import.meta.env.VITE_APP_VERSION;
|
const appVersion = import.meta.env.VITE_APP_VERSION
|
||||||
|
|
||||||
function logout() {
|
function logout() {
|
||||||
authStore.logout()
|
authStore.logout()
|
||||||
@@ -47,7 +47,9 @@ onMounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<nav class="navbar">
|
<nav class="navbar">
|
||||||
<div class="navbar-brand">
|
<div class="navbar-brand">
|
||||||
<RouterLink to="/" class="navbar-item" :title="'bibliomane v' + appVersion" activeClass="is-active"> B </RouterLink>
|
<RouterLink to="/" class="navbar-item" :title="'bibliomane v' + appVersion">
|
||||||
|
<img class="ml-3" src="/image/logo.svg" />
|
||||||
|
</RouterLink>
|
||||||
<div class="navbar-item is-hidden-desktop">
|
<div class="navbar-item is-hidden-desktop">
|
||||||
<a
|
<a
|
||||||
@click="isSearchBarShown = !isSearchBarShown"
|
@click="isSearchBarShown = !isSearchBarShown"
|
||||||
@@ -84,6 +86,17 @@ 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">
|
||||||
|
{{ $t('navbar.explore') }}
|
||||||
|
</RouterLink>
|
||||||
<RouterLink v-if="authStore.user" to="/add" class="navbar-item" activeClass="is-active">
|
<RouterLink v-if="authStore.user" to="/add" class="navbar-item" activeClass="is-active">
|
||||||
{{ $t('navbar.addbook') }}
|
{{ $t('navbar.addbook') }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|||||||
@@ -5,16 +5,13 @@ const props = defineProps({
|
|||||||
icon: String,
|
icon: String,
|
||||||
legend: String,
|
legend: String,
|
||||||
isSet: Boolean,
|
isSet: Boolean,
|
||||||
isReadonly: Boolean,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const hovered = ref(false)
|
const hovered = ref(false)
|
||||||
const isOnMobile = ref(computeIsOnMobile())
|
const isOnMobile = ref(computeIsOnMobile())
|
||||||
|
|
||||||
const computedIcon = computed(
|
const computedIcon = computed(
|
||||||
() =>
|
() => props.icon + ((hovered.value && !isOnMobile.value) || props.isSet ? 'Fill' : ''),
|
||||||
props.icon +
|
|
||||||
(!props.isReadonly && ((hovered.value && !isOnMobile.value) || props.isSet) ? 'Fill' : ''),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
function computeIsOnMobile() {
|
function computeIsOnMobile() {
|
||||||
@@ -34,8 +31,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="bigiconandlegend"
|
class="bigiconandlegend showcanclick"
|
||||||
:class="props.isReadonly ? '' : 'showcanclick'"
|
|
||||||
@mouseover="hovered = true"
|
@mouseover="hovered = true"
|
||||||
@mouseout="hovered = false"
|
@mouseout="hovered = false"
|
||||||
>
|
>
|
||||||
@@ -91,7 +87,7 @@ onUnmounted(() => {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-bottom: 0px;
|
padding-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bigiconandlegend {
|
.bigiconandlegend {
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { getImagePathOrDefault } from './api.js'
|
|
||||||
import { VRating } from 'vuetify/components/VRating'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
id: Number,
|
|
||||||
title: String,
|
|
||||||
author: String,
|
|
||||||
coverPath: String,
|
|
||||||
rating: Number,
|
|
||||||
read: Boolean,
|
|
||||||
})
|
|
||||||
const imagePathOrDefault = computed(() => getImagePathOrDefault(props.coverPath))
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
function openBook() {
|
|
||||||
router.push(`/book/${props.id}`)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="box container has-background-dark">
|
|
||||||
<div class="media" @click="openBook">
|
|
||||||
<div class="media-left">
|
|
||||||
<figure class="image mb-3">
|
|
||||||
<img v-bind:src="imagePathOrDefault" v-bind:alt="title" />
|
|
||||||
</figure>
|
|
||||||
</div>
|
|
||||||
<div class="media-content">
|
|
||||||
<div class="content">
|
|
||||||
<div class="is-size-5">{{ title }}</div>
|
|
||||||
<div class="is-size-5 is-italic">{{ author }}</div>
|
|
||||||
<VRating
|
|
||||||
v-if="rating > 0"
|
|
||||||
half-increments
|
|
||||||
readonly
|
|
||||||
:length="5"
|
|
||||||
size="medium"
|
|
||||||
:model-value="rating / 2"
|
|
||||||
active-color="bulma-body-color"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
img {
|
|
||||||
max-height: 100px;
|
|
||||||
max-width: 100px;
|
|
||||||
height: auto;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box {
|
|
||||||
transition: ease-in-out 0.04s;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box:hover {
|
|
||||||
transform: scale(1.01);
|
|
||||||
transition: ease-in-out 0.02s;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import BigIcon from './BigIcon.vue'
|
import BigIcon from './BigIcon.vue'
|
||||||
|
import DateWidget from './DateWidget.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
icon: String,
|
icon: String,
|
||||||
@@ -8,7 +9,6 @@ const props = defineProps({
|
|||||||
startReadDate: String,
|
startReadDate: String,
|
||||||
endReadDate: String,
|
endReadDate: String,
|
||||||
isExpanded: Boolean,
|
isExpanded: Boolean,
|
||||||
isReadonly: Boolean,
|
|
||||||
useEndDate: Boolean,
|
useEndDate: Boolean,
|
||||||
lastWidget: Boolean,
|
lastWidget: Boolean,
|
||||||
})
|
})
|
||||||
@@ -26,9 +26,6 @@ function computeParentClasses() {
|
|||||||
} else {
|
} else {
|
||||||
classNames += ' border-radius-right'
|
classNames += ' border-radius-right'
|
||||||
}
|
}
|
||||||
if (props.isReadonly) {
|
|
||||||
classNames += ' widget-readonly'
|
|
||||||
}
|
|
||||||
return classNames
|
return classNames
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -37,35 +34,24 @@ function computeParentClasses() {
|
|||||||
<div :class="computeParentClasses()">
|
<div :class="computeParentClasses()">
|
||||||
<BigIcon
|
<BigIcon
|
||||||
:icon="props.icon"
|
:icon="props.icon"
|
||||||
:is-readonly="props.isReadonly"
|
|
||||||
:legend="props.legend"
|
:legend="props.legend"
|
||||||
:isSet="props.isExpanded"
|
:isSet="props.isExpanded"
|
||||||
@click="props.isReadonly ? null : $emit('onIconClick')"
|
@click="$emit('onIconClick')"
|
||||||
/>
|
/>
|
||||||
<div v-if="props.isExpanded" class="inputdate">
|
<div v-if="props.isExpanded" class="inputdate">
|
||||||
<div class="ontopofinput">
|
<div class="ontopofinput">
|
||||||
<label class="datelabel" for="startread">
|
<DateWidget
|
||||||
{{ $t('bookdatewidget.started') }}
|
dateinputid="startread"
|
||||||
</label>
|
dateinputlabel="bookdatewidget.started"
|
||||||
<input
|
:initdate="props.startReadDate"
|
||||||
class="datepicker has-background-dark has-text-light"
|
@onDateChange="(d) => $emit('onStartDateChange', d)"
|
||||||
id="startread"
|
|
||||||
type="date"
|
|
||||||
@change="(e) => $emit('onStartDateChange', e.target.value)"
|
|
||||||
:value="props.startReadDate"
|
|
||||||
:max="today"
|
|
||||||
/>
|
/>
|
||||||
<div v-if="props.useEndDate">
|
<div v-if="props.useEndDate">
|
||||||
<label class="datelabel" for="endread">
|
<DateWidget
|
||||||
{{ $t('bookdatewidget.finished') }}
|
dateinputid="endread"
|
||||||
</label>
|
dateinputlabel="bookdatewidget.finished"
|
||||||
<input
|
:initdate="props.endReadDate"
|
||||||
class="datepicker has-background-dark has-text-light"
|
@onDateChange="(d) => $emit('onEndDateChange', d)"
|
||||||
id="endread"
|
|
||||||
type="date"
|
|
||||||
@change="(e) => $emit('onEndDateChange', e.target.value)"
|
|
||||||
:value="props.endReadDate"
|
|
||||||
:max="today"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,24 +79,6 @@ function computeParentClasses() {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.datelabel {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 26px;
|
|
||||||
border: none;
|
|
||||||
padding-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker {
|
|
||||||
font-size: 26px;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.widget-readonly {
|
|
||||||
opacity: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
.bookdatewidget {
|
.bookdatewidget {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
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>
|
||||||
174
front/src/BookFormIcons.vue
Normal file
174
front/src/BookFormIcons.vue
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useTemplateRef, nextTick } from 'vue'
|
||||||
|
import BigIcon from './BigIcon.vue'
|
||||||
|
import BookDateWidget from './BookDateWidget.vue'
|
||||||
|
import DateWidget from './DateWidget.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
wantread: Boolean,
|
||||||
|
startReadDate: String,
|
||||||
|
endReadDate: String,
|
||||||
|
read: Boolean,
|
||||||
|
})
|
||||||
|
|
||||||
|
const mobiledatesel = useTemplateRef('mobiledates')
|
||||||
|
|
||||||
|
const emit = defineEmits([
|
||||||
|
'onWantReadIconClick',
|
||||||
|
'onStartReadIconClick',
|
||||||
|
'onReadIconClick',
|
||||||
|
'onStartReadDateChange',
|
||||||
|
'onEndReadDateChange',
|
||||||
|
])
|
||||||
|
|
||||||
|
function isStartReadExpanded() {
|
||||||
|
let isStartReadDateSet = props.startReadDate ? true : false
|
||||||
|
let isReadUnset = !props.read ? true : false
|
||||||
|
return isStartReadDateSet && isReadUnset
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onReadIconClick() {
|
||||||
|
emit('onReadIconClick')
|
||||||
|
await nextTick()
|
||||||
|
mobiledatesel.value.scrollIntoView()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onStartReadIconClick() {
|
||||||
|
emit('onStartReadIconClick')
|
||||||
|
await nextTick()
|
||||||
|
mobiledatesel.value.scrollIntoView()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="iconscontainer" :class="props.read ? 'remove-border-bottom' : ''">
|
||||||
|
<div
|
||||||
|
class="bigiconcontainer"
|
||||||
|
:class="props.wantread ? 'has-text-dark has-background-text border-radius-wantread-fill' : ''"
|
||||||
|
>
|
||||||
|
<BigIcon
|
||||||
|
icon="BIconEye"
|
||||||
|
:legend="$t('bookform.wantread')"
|
||||||
|
:isSet="props.wantread"
|
||||||
|
@click="$emit('onWantReadIconClick')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="bigiconcontainer is-hidden-desktop"
|
||||||
|
:class="isStartReadExpanded() ? 'has-text-dark has-background-text' : ''"
|
||||||
|
>
|
||||||
|
<BigIcon
|
||||||
|
icon="BIconBook"
|
||||||
|
:legend="$t('bookform.startread')"
|
||||||
|
:is-set="isStartReadExpanded()"
|
||||||
|
@click="onStartReadIconClick"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="bigiconcontainer is-hidden-desktop"
|
||||||
|
:class="props.read ? 'has-text-dark has-background-text border-radius-right-fill' : ''"
|
||||||
|
>
|
||||||
|
<BigIcon
|
||||||
|
icon="BIconCheckCircle"
|
||||||
|
:legend="$t('bookform.read')"
|
||||||
|
:isSet="props.read"
|
||||||
|
@click="onReadIconClick"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<BookDateWidget
|
||||||
|
class="is-hidden-mobile"
|
||||||
|
icon="BIconBook"
|
||||||
|
:legend="$t('bookform.startread')"
|
||||||
|
:start-read-date="props.startReadDate"
|
||||||
|
:is-expanded="isStartReadExpanded()"
|
||||||
|
@onStartDateChange="(d) => $emit('onStartReadDateChange', d)"
|
||||||
|
@onIconClick="onStartReadIconClick"
|
||||||
|
/>
|
||||||
|
<BookDateWidget
|
||||||
|
class="is-hidden-mobile"
|
||||||
|
icon="BIconCheckCircle"
|
||||||
|
:legend="$t('bookform.read')"
|
||||||
|
:start-read-date="props.startReadDate"
|
||||||
|
use-end-date
|
||||||
|
last-widget
|
||||||
|
:endReadDate="props.endReadDate"
|
||||||
|
:isExpanded="props.read"
|
||||||
|
@onStartDateChange="(d) => $emit('onStartReadDateChange', d)"
|
||||||
|
@onEndDateChange="(d) => $emit('onEndReadDateChange', d)"
|
||||||
|
@onIconClick="onReadIconClick"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ref="mobiledates" class="mobile-dates pt-3 is-hidden-desktop">
|
||||||
|
<div class="mobiledate">
|
||||||
|
<DateWidget
|
||||||
|
v-if="isStartReadExpanded() || props.read"
|
||||||
|
dateinputid="startread"
|
||||||
|
dateinputlabel="bookdatewidget.started"
|
||||||
|
:initdate="props.startReadDate"
|
||||||
|
is-horizontal
|
||||||
|
@onDateChange="(d) => $emit('onStartReadDateChange', d)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mobiledate pt-2">
|
||||||
|
<DateWidget
|
||||||
|
v-if="props.read"
|
||||||
|
dateinputid="endread"
|
||||||
|
dateinputlabel="bookdatewidget.finished"
|
||||||
|
:initdate="props.endReadDate"
|
||||||
|
is-horizontal
|
||||||
|
@onDateChange="(d) => $emit('onEndReadDateChange', d)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.iconscontainer {
|
||||||
|
border: solid;
|
||||||
|
border-radius: 50px;
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-dates {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobiledate {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.border-radius-wantread-fill {
|
||||||
|
border-radius: 45px 45px 0px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-border-bottom {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bigiconcontainer {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.border-radius-wantread-fill {
|
||||||
|
border-radius: 38px 0px 0px 38px;
|
||||||
|
}
|
||||||
|
.bigiconcontainer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconscontainer {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-radius-right-fill {
|
||||||
|
border-radius: 0px 38px 38px 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,19 +3,18 @@ import { ref, computed } from 'vue'
|
|||||||
import {
|
import {
|
||||||
getBook,
|
getBook,
|
||||||
getImagePathOrDefault,
|
getImagePathOrDefault,
|
||||||
putReadBook,
|
putUpdateBook,
|
||||||
putWantReadBook,
|
|
||||||
putRateBook,
|
|
||||||
putStartReadDate,
|
putStartReadDate,
|
||||||
putStartReadDateUnset,
|
putStartReadDateUnset,
|
||||||
|
putReadBook,
|
||||||
putEndReadDate,
|
putEndReadDate,
|
||||||
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 BigIcon from './BigIcon.vue'
|
import BookFormIcons from './BookFormIcons.vue'
|
||||||
import BookDateWidget from './BookDateWidget.vue'
|
import ReviewWidget from './ReviewWidget.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -38,15 +37,20 @@ function onRatingUpdate(rating) {
|
|||||||
data.value.read = true
|
data.value.read = true
|
||||||
data.value.wantread = false
|
data.value.wantread = false
|
||||||
}
|
}
|
||||||
putRateBook(props.id, { rating: data.value.rating })
|
putUpdateBook(props.id, { rating: data.value.rating })
|
||||||
}
|
}
|
||||||
|
|
||||||
function onReadIconClick() {
|
function onReviewUpdate(review) {
|
||||||
|
data.value.review = review
|
||||||
|
putUpdateBook(props.id, { review: data.value.review })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onReadIconClick() {
|
||||||
data.value.read = !data.value.read
|
data.value.read = !data.value.read
|
||||||
if (data.value.read) {
|
if (data.value.read) {
|
||||||
data.value.wantread = false
|
data.value.wantread = false
|
||||||
data.value.endReadDate = today
|
data.value.endReadDate = today
|
||||||
putEndReadDate(props.id, today)
|
putReadBook(props.id)
|
||||||
} else {
|
} else {
|
||||||
putUnreadBook(props.id)
|
putUnreadBook(props.id)
|
||||||
}
|
}
|
||||||
@@ -54,17 +58,21 @@ function onReadIconClick() {
|
|||||||
|
|
||||||
function onWantReadIconClick() {
|
function onWantReadIconClick() {
|
||||||
data.value.wantread = !data.value.wantread
|
data.value.wantread = !data.value.wantread
|
||||||
putWantReadBook(props.id, { wantread: data.value.wantread })
|
putUpdateBook(props.id, { wantread: data.value.wantread })
|
||||||
}
|
}
|
||||||
|
|
||||||
function onStartReadIconClick() {
|
async function onStartReadIconClick() {
|
||||||
if (!data.value.startReadDate) {
|
if (!data.value.startReadDate) {
|
||||||
data.value.startReadDate = today
|
data.value.startReadDate = today
|
||||||
|
data.value.wantread = false
|
||||||
putStartReadDate(props.id, data.value.startReadDate)
|
putStartReadDate(props.id, data.value.startReadDate)
|
||||||
} else {
|
} else if (!data.value.read) {
|
||||||
data.value.startReadDate = null
|
data.value.startReadDate = null
|
||||||
putStartReadDateUnset(props.id)
|
putStartReadDateUnset(props.id)
|
||||||
}
|
}
|
||||||
|
if (data.value.read) {
|
||||||
|
data.value.read = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onStartReadDateChange(d) {
|
function onStartReadDateChange(d) {
|
||||||
@@ -85,12 +93,6 @@ function onEndReadDateChange(d) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isStartReadExpanded() {
|
|
||||||
let isStartReadDateSet = data.value.startReadDate ? true : false
|
|
||||||
let isReadUnset = !data.value.read ? true : false
|
|
||||||
return isStartReadDateSet && isReadUnset
|
|
||||||
}
|
|
||||||
|
|
||||||
function goToAuthor() {
|
function goToAuthor() {
|
||||||
router.push('/author/' + data.value.authorId)
|
router.push('/author/' + data.value.authorId)
|
||||||
}
|
}
|
||||||
@@ -103,58 +105,39 @@ 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>
|
||||||
<VRating
|
<div class="centered mt-2">
|
||||||
half-increments
|
<RouterLink :to="'/book/' + props.id + '/edit'">
|
||||||
hover
|
<span>
|
||||||
:length="5"
|
<b-icon-pencil-fill />
|
||||||
size="x-large"
|
</span>
|
||||||
density="compact"
|
Modifier le livre
|
||||||
:model-value="data.rating / 2"
|
</RouterLink>
|
||||||
@update:modelValue="onRatingUpdate"
|
</div>
|
||||||
active-color="bulma-body-color"
|
|
||||||
class="centered"
|
|
||||||
/>
|
|
||||||
</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
|
||||||
|
class="mt-5"
|
||||||
|
:reviewtext="data.review"
|
||||||
|
:rating="data.rating"
|
||||||
|
@on-review-update="onReviewUpdate"
|
||||||
|
@on-rating-update="onRatingUpdate"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="iconscontainer" :class="data.read ? 'remove-border-bottom' : ''">
|
<BookFormIcons
|
||||||
<div class="bigiconcontainer">
|
v-bind="data"
|
||||||
<BigIcon
|
@on-want-read-icon-click="onWantReadIconClick"
|
||||||
icon="BIconEye"
|
@on-start-read-icon-click="onStartReadIconClick"
|
||||||
:legend="$t('bookform.wantread')"
|
@on-read-icon-click="onReadIconClick"
|
||||||
:isSet="data.wantread"
|
@on-start-read-date-change="onStartReadDateChange"
|
||||||
@click="onWantReadIconClick"
|
@on-end-read-date-change="onEndReadDateChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<BookDateWidget
|
|
||||||
icon="BIconBook"
|
|
||||||
:legend="$t('bookform.startread')"
|
|
||||||
:start-read-date="data.startReadDate"
|
|
||||||
:is-expanded="isStartReadExpanded()"
|
|
||||||
:is-readonly="data.read"
|
|
||||||
@onStartDateChange="onStartReadDateChange"
|
|
||||||
@onIconClick="onStartReadIconClick"
|
|
||||||
/>
|
|
||||||
<BookDateWidget
|
|
||||||
icon="BIconCheckCircle"
|
|
||||||
:legend="$t('bookform.read')"
|
|
||||||
:start-read-date="data.startReadDate"
|
|
||||||
use-end-date
|
|
||||||
last-widget
|
|
||||||
:endReadDate="data.endReadDate"
|
|
||||||
:isExpanded="data.read"
|
|
||||||
@onStartDateChange="onStartReadDateChange"
|
|
||||||
@onEndDateChange="onEndReadDateChange"
|
|
||||||
@onIconClick="onReadIconClick"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -167,22 +150,6 @@ img {
|
|||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.centered {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconscontainer {
|
|
||||||
border: solid;
|
|
||||||
border-radius: 50px;
|
|
||||||
width: 250px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remove-border-bottom {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
.left-panel {
|
.left-panel {
|
||||||
margin-left: 3rem;
|
margin-left: 3rem;
|
||||||
@@ -193,10 +160,7 @@ img {
|
|||||||
img {
|
img {
|
||||||
max-height: 250px;
|
max-height: 250px;
|
||||||
max-width: 250px;
|
max-width: 250px;
|
||||||
}
|
padding: 20px;
|
||||||
|
|
||||||
.bigiconcontainer {
|
|
||||||
flex: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
@@ -204,10 +168,5 @@ img {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconscontainer {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,6 +1,14 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { putReadBook, getImagePathOrDefault, postImportBook } from './api.js'
|
import {
|
||||||
|
putUpdateBook,
|
||||||
|
putReadBook,
|
||||||
|
putUnreadBook,
|
||||||
|
putStartRead,
|
||||||
|
putStartReadDateUnset,
|
||||||
|
getImagePathOrDefault,
|
||||||
|
postImportBook,
|
||||||
|
} from './api.js'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -14,16 +22,83 @@ const props = defineProps({
|
|||||||
description: String,
|
description: String,
|
||||||
rating: Number,
|
rating: Number,
|
||||||
read: Boolean,
|
read: Boolean,
|
||||||
|
startreaddate: String,
|
||||||
wantread: Boolean,
|
wantread: Boolean,
|
||||||
coverPath: String,
|
coverPath: String,
|
||||||
})
|
})
|
||||||
const imagePathOrDefault = computed(() => getImagePathOrDefault(props.coverPath))
|
const imagePathOrDefault = computed(() => getImagePathOrDefault(props.coverPath))
|
||||||
const error = ref(null)
|
const error = ref(null)
|
||||||
|
|
||||||
|
const isWantRead = ref(props.wantread)
|
||||||
|
const currentStartReadDate = ref(props.startreaddate)
|
||||||
|
const isRead = ref(props.read)
|
||||||
|
|
||||||
|
const today = new Date().toISOString().slice(0, 10)
|
||||||
|
|
||||||
|
const isStartRead = computed(
|
||||||
|
() => currentStartReadDate && currentStartReadDate.value && isRead && !isRead.value,
|
||||||
|
)
|
||||||
|
|
||||||
async function onUserBookRead() {
|
async function onUserBookRead() {
|
||||||
|
if (!isRead.value) {
|
||||||
|
userBookRead()
|
||||||
|
} else {
|
||||||
|
userBookUnread()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function userBookRead() {
|
||||||
const res = await putReadBook(props.id)
|
const res = await putReadBook(props.id)
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
router.push('/books')
|
currentStartReadDate.value = ''
|
||||||
|
isWantRead.value = false
|
||||||
|
isRead.value = true
|
||||||
|
} else {
|
||||||
|
res.json().then((json) => (error.value = json))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function userBookUnread() {
|
||||||
|
const res = await putUnreadBook(props.id)
|
||||||
|
if (res.ok) {
|
||||||
|
isRead.value = false
|
||||||
|
} else {
|
||||||
|
res.json().then((json) => (error.value = json))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onUserBookWantRead() {
|
||||||
|
const res = await putUpdateBook(props.id, { wantread: !isWantRead.value })
|
||||||
|
if (res.ok) {
|
||||||
|
isWantRead.value = !isWantRead.value
|
||||||
|
} else {
|
||||||
|
res.json().then((json) => (error.value = json))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onUserBookStartRead() {
|
||||||
|
if (!isStartRead.value) {
|
||||||
|
userBookStartRead()
|
||||||
|
} else {
|
||||||
|
userBookCancelStartRead()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function userBookStartRead() {
|
||||||
|
const res = await putStartRead(props.id)
|
||||||
|
if (res.ok) {
|
||||||
|
currentStartReadDate.value = today
|
||||||
|
isRead.value = false
|
||||||
|
isWantRead.value = false
|
||||||
|
} else {
|
||||||
|
res.json().then((json) => (error.value = json))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function userBookCancelStartRead() {
|
||||||
|
const res = await putStartReadDateUnset(props.id)
|
||||||
|
if (res.ok) {
|
||||||
|
currentStartReadDate.value = ''
|
||||||
} else {
|
} else {
|
||||||
res.json().then((json) => (error.value = json))
|
res.json().then((json) => (error.value = json))
|
||||||
}
|
}
|
||||||
@@ -55,6 +130,7 @@ async function importInventaireEdition(inventaireid) {
|
|||||||
<p>{{ error }}</p>
|
<p>{{ error }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="columns no-padding box container has-background-dark">
|
<div class="columns no-padding box container has-background-dark">
|
||||||
|
<slot name="left"></slot>
|
||||||
<div class="media column no-margin clickable" @click="openBook">
|
<div class="media column no-margin clickable" @click="openBook">
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
<figure class="image mb-3">
|
<figure class="image mb-3">
|
||||||
@@ -67,25 +143,29 @@ async function importInventaireEdition(inventaireid) {
|
|||||||
<div class="has-text-text-65 is-size-6" v-if="props.description">{{ description }}</div>
|
<div class="has-text-text-65 is-size-6" v-if="props.description">{{ description }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!inventaireid" class="column is-narrow">
|
<div v-if="id && id != 0" class="column is-narrow">
|
||||||
<button @click="" class="button is-large verticalbutton">
|
<div class="buttons">
|
||||||
<span class="icon" :title="$t('booklistelement.wantread')">
|
<button @click="onUserBookWantRead" class="button is-large verticalbutton">
|
||||||
<b-icon-eye-fill v-if="props.wantread" />
|
<span class="icon" :title="$t('booklistelement.wantread')">
|
||||||
<b-icon-eye v-else />
|
<b-icon-eye-fill v-if="isWantRead" />
|
||||||
</span>
|
<b-icon-eye v-else />
|
||||||
</button>
|
</span>
|
||||||
<button @click="" class="button is-large verticalbutton">
|
</button>
|
||||||
<span class="icon" :title="$t('booklistelement.startread')">
|
<button @click="onUserBookStartRead" class="button is-large verticalbutton">
|
||||||
<b-icon-book />
|
<span class="icon" :title="$t('booklistelement.startread')">
|
||||||
</span>
|
<b-icon-book-fill v-if="isStartRead" />
|
||||||
</button>
|
<b-icon-book v-else />
|
||||||
<button @click="onUserBookRead" class="button is-large verticalbutton">
|
</span>
|
||||||
<span class="icon" :title="$t('booklistelement.read')">
|
</button>
|
||||||
<b-icon-check-circle-fill v-if="props.read" />
|
<button @click="onUserBookRead" class="button is-large verticalbutton">
|
||||||
<b-icon-check-circle v-else />
|
<span class="icon" :title="$t('booklistelement.read')">
|
||||||
</span>
|
<b-icon-check-circle-fill v-if="isRead" />
|
||||||
</button>
|
<b-icon-check-circle v-else />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<slot name="right"></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -107,6 +187,10 @@ img {
|
|||||||
transition: ease-in-out 0.02s;
|
transition: ease-in-out 0.02s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.verticalbutton {
|
.verticalbutton {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -118,4 +202,19 @@ img {
|
|||||||
.no-margin {
|
.no-margin {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-height: 100px;
|
||||||
|
max-width: 100px;
|
||||||
|
height: auto;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import BookCard from './BookCard.vue'
|
|
||||||
import { getMyBooks } from './api.js'
|
import { getMyBooks } from './api.js'
|
||||||
|
import BookListElement from './BookListElement.vue'
|
||||||
import Pagination from './Pagination.vue'
|
import Pagination from './Pagination.vue'
|
||||||
|
|
||||||
const FilterStates = Object.freeze({
|
const FilterStates = Object.freeze({
|
||||||
@@ -10,15 +10,15 @@ const FilterStates = Object.freeze({
|
|||||||
READING: 'reading',
|
READING: 'reading',
|
||||||
})
|
})
|
||||||
|
|
||||||
const limit = 6
|
const limit = 5
|
||||||
const pageNumber = ref(1)
|
const pageNumber = ref(1)
|
||||||
|
|
||||||
const offset = computed(() => (pageNumber.value - 1) * limit)
|
const offset = computed(() => (pageNumber.value - 1) * limit)
|
||||||
|
|
||||||
let currentFilterState = ref(FilterStates.READ)
|
let currentFilterState = ref(FilterStates.READ)
|
||||||
|
|
||||||
let data = ref(null)
|
const data = ref(null)
|
||||||
let error = ref(null)
|
const error = ref(null)
|
||||||
|
|
||||||
let totalBooksNumber = computed(() =>
|
let totalBooksNumber = computed(() =>
|
||||||
typeof data != 'undefined' && data.value != null ? data.value['count'] : 0,
|
typeof data != 'undefined' && data.value != null ? data.value['count'] : 0,
|
||||||
@@ -76,7 +76,7 @@ function pageChange(newPageNumber) {
|
|||||||
<div v-else-if="data">
|
<div v-else-if="data">
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="" v-for="book in data.books" :key="book.id">
|
<div class="" v-for="book in data.books" :key="book.id">
|
||||||
<BookCard v-bind="book" />
|
<BookListElement v-bind="book" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Pagination
|
<Pagination
|
||||||
|
|||||||
150
front/src/CollectionForm.vue
Normal file
150
front/src/CollectionForm.vue
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, ref, useTemplateRef } from 'vue'
|
||||||
|
import { getCollection, postCollectionChangePosition, deleteCollectionItem } 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 itemRefs = useTemplateRef('items')
|
||||||
|
|
||||||
|
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 checkGrabbedPosition(itemId, y) {
|
||||||
|
const itemBeingMoved = itemRefs.value.find((it) => it.id == itemId)
|
||||||
|
const itemMovedY = itemBeingMoved.$el.offsetTop + y + itemBeingMoved.$el.offsetHeight / 2
|
||||||
|
itemRefs.value.forEach((it) => {
|
||||||
|
if (it.$props.id == itemIdBeingGrabbed.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let lowerDetectionY = it.$el.offsetTop + it.$el.offsetHeight / 2
|
||||||
|
let upperDetectionY = it.$el.offsetTop + (3 * it.$el.offsetHeight) / 2
|
||||||
|
if (it.$props.position < itemBeingMoved.$props.position) {
|
||||||
|
lowerDetectionY = it.$el.offsetTop
|
||||||
|
upperDetectionY = it.$el.offsetTop + it.$el.offsetHeight / 2
|
||||||
|
}
|
||||||
|
if (lowerDetectionY < itemMovedY && itemMovedY < upperDetectionY) {
|
||||||
|
itemIdBeingOvered.value = it.$props.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onStopGrab() {
|
||||||
|
if (itemIdBeingOvered.value != null) {
|
||||||
|
const position = data.value.items.find((it) => it.id == itemIdBeingOvered.value).position
|
||||||
|
changePosition(itemIdBeingGrabbed.value, position)
|
||||||
|
}
|
||||||
|
itemIdBeingGrabbed.value = null
|
||||||
|
itemIdBeingOvered.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDragoverFromAbove(position) {
|
||||||
|
if (itemIdBeingGrabbed.value == null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const grabbedItemPosition = data.value.items.find(
|
||||||
|
(it) => it.id == itemIdBeingGrabbed.value,
|
||||||
|
).position
|
||||||
|
return position > grabbedItemPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
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 deleteItem(id) {
|
||||||
|
deleteCollectionItem(id).then((res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
getCollection(data, error, props.id, limit, offset.value)
|
||||||
|
} else {
|
||||||
|
res.json().then((json) => {
|
||||||
|
error.value = json
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</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" />
|
||||||
|
<TransitionGroup name="list" tag="div">
|
||||||
|
<CollectionFormElement
|
||||||
|
@positionchange="(pos) => changePosition(item.id, pos)"
|
||||||
|
@startgrab="itemIdBeingGrabbed = item.id"
|
||||||
|
@grabbing="(y) => checkGrabbedPosition(item.id, y)"
|
||||||
|
@stopgrab="onStopGrab"
|
||||||
|
@delete="deleteItem(item.id)"
|
||||||
|
v-for="item in data.items"
|
||||||
|
:key="item.id"
|
||||||
|
v-bind="item"
|
||||||
|
ref="items"
|
||||||
|
:is-dragover="item.id == itemIdBeingOvered"
|
||||||
|
:is-dragover-from-above="isDragoverFromAbove(item.position)"
|
||||||
|
/>
|
||||||
|
</TransitionGroup>
|
||||||
|
<Pagination
|
||||||
|
class="mt-5"
|
||||||
|
:pageNumber="pageNumber"
|
||||||
|
:pageTotal="pageTotal"
|
||||||
|
maxItemDisplayed="11"
|
||||||
|
@pageChange="pageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.list-move, /* apply transition to moving elements */
|
||||||
|
.list-enter-active,
|
||||||
|
.list-leave-active {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-enter-from,
|
||||||
|
.list-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(30px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
195
front/src/CollectionFormElement.vue
Normal file
195
front/src/CollectionFormElement.vue
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import BookListElement from './BookListElement.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
id: Number,
|
||||||
|
position: Number,
|
||||||
|
book: Array,
|
||||||
|
isDragover: Boolean,
|
||||||
|
isDragoverFromAbove: Boolean,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['positionchange', 'startgrab', 'stopgrab', 'grabbing', 'delete'])
|
||||||
|
|
||||||
|
const vFocus = {
|
||||||
|
mounted: (el) => el.focus(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const isInputtingPosition = ref(false)
|
||||||
|
const inputtedPosition = ref('')
|
||||||
|
|
||||||
|
const initialGrabPosition = ref(null)
|
||||||
|
|
||||||
|
const draggedPosition = ref(null)
|
||||||
|
|
||||||
|
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 = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearGrabVariables() {
|
||||||
|
initialGrabPosition.value = null
|
||||||
|
draggedPosition.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerUp() {
|
||||||
|
clearGrabVariables()
|
||||||
|
emit('stopgrab')
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerDown(e) {
|
||||||
|
initialGrabPosition.value = e.pageY
|
||||||
|
e.preventDefault()
|
||||||
|
emit('startgrab')
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerMove(e) {
|
||||||
|
if (initialGrabPosition.value == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.preventDefault()
|
||||||
|
draggedPosition.value = e.pageY - initialGrabPosition.value
|
||||||
|
emit('grabbing', draggedPosition.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="isDragover && !isDragoverFromAbove" class="dragover" />
|
||||||
|
<div
|
||||||
|
:style="
|
||||||
|
draggedPosition
|
||||||
|
? 'transform: translateY(' + draggedPosition + 'px);position:relative;z-index:3'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
ref="collectionitembox"
|
||||||
|
class="collectionitembox"
|
||||||
|
@pointermove="onPointerMove"
|
||||||
|
@pointerup="onPointerUp"
|
||||||
|
@pointerleave="clearGrabVariables"
|
||||||
|
>
|
||||||
|
<BookListElement v-bind="props.book">
|
||||||
|
<template v-slot:left>
|
||||||
|
<div class="is-hidden-desktop align-right">
|
||||||
|
<div @click="$emit('delete')" class="centered closebtn clickable">
|
||||||
|
<b-icon-x />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="inputpositionwidget 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" />
|
||||||
|
</template>
|
||||||
|
<template v-slot:right>
|
||||||
|
<div class="separator" />
|
||||||
|
<div class="positionwidget centered is-narrow" @pointerdown="onPointerDown">
|
||||||
|
<b-icon-list />
|
||||||
|
</div>
|
||||||
|
<div @click="$emit('delete')" class="is-hidden-touch centered closebtn clickable">
|
||||||
|
<b-icon-x />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</BookListElement>
|
||||||
|
</div>
|
||||||
|
<div v-if="isDragover && isDragoverFromAbove" class="dragover" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.collectionitembox {
|
||||||
|
transition: ease-in-out 0.04s;
|
||||||
|
display: flex;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
border-top-right-radius: var(--bulma-box-radius);
|
||||||
|
border-bottom-right-radius: var(--bulma-box-radius);
|
||||||
|
cursor: grab;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.positionwidget:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragover {
|
||||||
|
border: 3px solid var(--bulma-primary);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closebtn {
|
||||||
|
height: 40px;
|
||||||
|
width: 40px;
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-right {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.positionwidget {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
182
front/src/CollectionListElement.vue
Normal file
182
front/src/CollectionListElement.vue
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
<script setup>
|
||||||
|
import { getImagePathOrDefault } from './api.js'
|
||||||
|
import { useTemplateRef, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const emit = defineEmits(['delete'])
|
||||||
|
|
||||||
|
const closeButtonDesktop = useTemplateRef('closeDesktop')
|
||||||
|
const closeButtonMobile = useTemplateRef('closeMobile')
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
id: Number,
|
||||||
|
name: String,
|
||||||
|
books: Array,
|
||||||
|
count: Number,
|
||||||
|
})
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
function onClick(e) {
|
||||||
|
if (
|
||||||
|
(closeButtonDesktop.value && closeButtonDesktop.value.contains(e.target)) ||
|
||||||
|
(closeButtonMobile.value && closeButtonMobile.value.contains(e.target))
|
||||||
|
) {
|
||||||
|
emit('delete')
|
||||||
|
} else {
|
||||||
|
router.push(`/collection/${props.id}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBookOpacityClass(index) {
|
||||||
|
const length = props.books.length
|
||||||
|
if (length < 5) {
|
||||||
|
return ''
|
||||||
|
} else if (length < 8) {
|
||||||
|
if (index < 4) {
|
||||||
|
return 'opacity-' + index
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 'opacity-' + index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="collectioncontainer has-background-dark p-2" @click="onClick">
|
||||||
|
<div class="collectionheader">
|
||||||
|
<h2 class="namecontainer subtitle">
|
||||||
|
{{ props.name }}
|
||||||
|
</h2>
|
||||||
|
<div class="is-hidden-desktop align-right">
|
||||||
|
<div ref="closeMobile" @click="$emit('delete')" class="centered closebtn clickable">
|
||||||
|
<b-icon-x />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="collectionpreviewbooks" v-if="props.books && props.books.length > 0">
|
||||||
|
<div
|
||||||
|
:class="index > 3 ? 'is-hidden-touch' : ''"
|
||||||
|
class="bookpreview mx-1"
|
||||||
|
v-for="(book, index) in props.books"
|
||||||
|
:key="book.id"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:class="setBookOpacityClass(index)"
|
||||||
|
v-bind:src="getImagePathOrDefault(book.coverPath)"
|
||||||
|
v-bind:alt="book.title"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div ref="closeDesktop" class="is-hidden-touch centered closebtn clickable">
|
||||||
|
<b-icon-x />
|
||||||
|
</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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.namecontainer {
|
||||||
|
flex: 1;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collectionheader {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collectionpreviewbooks {
|
||||||
|
flex: 6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacity-1 {
|
||||||
|
opacity: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacity-2 {
|
||||||
|
opacity: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacity-3 {
|
||||||
|
opacity: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacity-4 {
|
||||||
|
opacity: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacity-5 {
|
||||||
|
opacity: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacity-6 {
|
||||||
|
opacity: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacity-7 {
|
||||||
|
opacity: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closebtn {
|
||||||
|
height: 40px;
|
||||||
|
width: 40px;
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-right {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
img {
|
||||||
|
max-height: 75px;
|
||||||
|
max-width: 75px;
|
||||||
|
}
|
||||||
|
.collectionpreviewbooks {
|
||||||
|
flex: 1;
|
||||||
|
margin-top: 20px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.collectioncontainer {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacity-1 {
|
||||||
|
opacity: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacity-2 {
|
||||||
|
opacity: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacity-3 {
|
||||||
|
opacity: 10%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
74
front/src/CollectionsBrowser.vue
Normal file
74
front/src/CollectionsBrowser.vue
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { getCollections, deleteCollection } 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}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeList(id) {
|
||||||
|
deleteCollection(id).then((res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
fetchData()
|
||||||
|
} else {
|
||||||
|
res.json().then((json) => {
|
||||||
|
error.value = json
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</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 @delete="removeList(collection.id)" 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>
|
||||||
51
front/src/DateWidget.vue
Normal file
51
front/src/DateWidget.vue
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
dateinputid: String,
|
||||||
|
dateinputlabel: String,
|
||||||
|
initdate: String,
|
||||||
|
isHorizontal: Boolean,
|
||||||
|
})
|
||||||
|
defineEmits(['onDateChange'])
|
||||||
|
|
||||||
|
const today = new Date().toISOString().slice(0, 10)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<label class="datelabel" :class="props.isHorizontal ? 'pr-2' : 'pb-1'" :for="props.dateinputid">
|
||||||
|
{{ $t(props.dateinputlabel) }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
class="datepicker has-background-dark has-text-light"
|
||||||
|
:id="props.dateinputid"
|
||||||
|
type="date"
|
||||||
|
@change="(e) => $emit('onDateChange', e.target.value)"
|
||||||
|
:value="props.initdate"
|
||||||
|
:max="today"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.datelabel {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 26px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datepicker {
|
||||||
|
font-size: 26px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.datelabel {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.datepicker {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
61
front/src/InstanceBrowser.vue
Normal file
61
front/src/InstanceBrowser.vue
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { getAllBooks } from './api.js'
|
||||||
|
import { onBeforeRouteUpdate } from 'vue-router'
|
||||||
|
import BookListElement from './BookListElement.vue'
|
||||||
|
import Pagination from './Pagination.vue'
|
||||||
|
|
||||||
|
const limit = 5
|
||||||
|
const pageNumber = ref(1)
|
||||||
|
|
||||||
|
const offset = computed(() => (pageNumber.value - 1) * limit)
|
||||||
|
|
||||||
|
const data = ref(null)
|
||||||
|
const error = ref(null)
|
||||||
|
|
||||||
|
const pageTotal = computed(() => {
|
||||||
|
const countValue = data.value !== null ? data.value['count'] : 0
|
||||||
|
return Math.ceil(countValue / limit)
|
||||||
|
})
|
||||||
|
|
||||||
|
function fetchData() {
|
||||||
|
data.value = null
|
||||||
|
error.value = null
|
||||||
|
getAllBooks(data, error, limit, offset.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchData()
|
||||||
|
|
||||||
|
onBeforeRouteUpdate(async (to, from) => {
|
||||||
|
pageNumber.value = 1
|
||||||
|
fetchData()
|
||||||
|
})
|
||||||
|
|
||||||
|
function pageChange(newPageNumber) {
|
||||||
|
pageNumber.value = newPageNumber
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<div v-if="error">{{ error }}</div>
|
||||||
|
<div v-else-if="data && data.books && data.books.length > 0">
|
||||||
|
<div class="booksearchlist" v-for="book in data.books" :key="book.id">
|
||||||
|
<BookListElement v-bind="book" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="data === null">{{ $t('searchbook.loading') }}</div>
|
||||||
|
<div v-else>{{ $t('searchbook.noresult') }}</div>
|
||||||
|
</div>
|
||||||
|
<Pagination
|
||||||
|
:pageNumber="pageNumber"
|
||||||
|
:pageTotal="pageTotal"
|
||||||
|
maxItemDisplayed="11"
|
||||||
|
@pageChange="pageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
135
front/src/ReviewWidget.vue
Normal file
135
front/src/ReviewWidget.vue
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { VRating } from 'vuetify/components/VRating'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
rating: Number,
|
||||||
|
reviewtext: String,
|
||||||
|
})
|
||||||
|
const isTextareaExpanded = ref(false)
|
||||||
|
const isTextareaTransitionEnabled = ref(true)
|
||||||
|
|
||||||
|
defineEmits('onRatingUpdate', 'onReviewUpdate')
|
||||||
|
|
||||||
|
function computeTextareaClass() {
|
||||||
|
let classAttr =
|
||||||
|
isTextareaExpanded && isTextareaExpanded.value ? 'textarea-expanded' : 'textarea-normal'
|
||||||
|
if (isTextareaTransitionEnabled && isTextareaTransitionEnabled.value) {
|
||||||
|
classAttr += ' transition-height'
|
||||||
|
}
|
||||||
|
return classAttr
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTextAreaFocus() {
|
||||||
|
isTextareaExpanded.value = true
|
||||||
|
setTimeout(() => {
|
||||||
|
isTextareaTransitionEnabled.value = false
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="maincontainer py-5">
|
||||||
|
<div class="widget-header mb-5 full-width">
|
||||||
|
<div class="widget-title ml-3">
|
||||||
|
<h2>{{ $t('review.title') }}</h2>
|
||||||
|
<span class="ml-3">
|
||||||
|
<b-icon-pen />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<VRating
|
||||||
|
half-increments
|
||||||
|
clearable
|
||||||
|
hover
|
||||||
|
:length="5"
|
||||||
|
size="x-large"
|
||||||
|
density="compact"
|
||||||
|
:model-value="rating / 2"
|
||||||
|
@update:modelValue="(r) => $emit('onRatingUpdate', r)"
|
||||||
|
active-color="bulma-body-color"
|
||||||
|
class="widget-rating centered"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="full-width centered">
|
||||||
|
<textarea
|
||||||
|
:placeholder="$t('review.textplaceholder')"
|
||||||
|
class="widget-textarea mx-4"
|
||||||
|
@change="(e) => $emit('onReviewUpdate', e.target.value)"
|
||||||
|
@focus="onTextAreaFocus"
|
||||||
|
:class="computeTextareaClass()"
|
||||||
|
>{{ reviewtext }}</textarea
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.maincontainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
border: solid;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-header {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-title {
|
||||||
|
flex: 2;
|
||||||
|
font-size: 2em;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-title h2 {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-rating {
|
||||||
|
flex: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-textarea {
|
||||||
|
color: var(--bulma-body-color);
|
||||||
|
background-color: var(--bulma-text-20);
|
||||||
|
width: 95%;
|
||||||
|
border-radius: 30px;
|
||||||
|
border: none;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea-normal {
|
||||||
|
height: 80px;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea-expanded {
|
||||||
|
height: 350px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition-height {
|
||||||
|
transition: height 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.widget-header {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.widget-title {
|
||||||
|
font-size: 1.5em;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,21 +1,38 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useTemplateRef, onMounted, ref } from 'vue'
|
import { useTemplateRef, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { BrowserMultiFormatReader } from '@zxing/library'
|
import { BrowserMultiFormatReader } from '@zxing/library'
|
||||||
|
import { i18n } from '@/main'
|
||||||
|
|
||||||
const emit = defineEmits('readBarcode')
|
const emit = defineEmits('readBarcode')
|
||||||
|
|
||||||
|
const { t } = i18n.global
|
||||||
|
|
||||||
const scanResult = ref(null)
|
const scanResult = ref(null)
|
||||||
|
const scanErr = ref(null)
|
||||||
const codeReader = new BrowserMultiFormatReader()
|
const codeReader = new BrowserMultiFormatReader()
|
||||||
const scannerElement = useTemplateRef('scanner')
|
const scannerElement = useTemplateRef('scanner')
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
codeReader.decodeFromVideoDevice(undefined, scannerElement.value, (result, err) => {
|
if (!codeReader.isMediaDevicesSuported || !codeReader.canEnumerateDevices) {
|
||||||
if (result) {
|
scanErr.value = 'This browser does not support this feature.'
|
||||||
emit('readBarcode', result.text)
|
return
|
||||||
|
}
|
||||||
|
codeReader.listVideoInputDevices().then((mediaDevicesInfoArray) => {
|
||||||
|
if (mediaDevicesInfoArray.length > 0) {
|
||||||
|
codeReader.decodeFromVideoDevice(undefined, scannerElement.value, (result, err) => {
|
||||||
|
if (result) {
|
||||||
|
emit('readBarcode', result.text)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
scanErr.value = t('barcode.nocamera')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
codeReader.reset()
|
||||||
|
})
|
||||||
function onResult(result) {
|
function onResult(result) {
|
||||||
scanResult.value = result
|
scanResult.value = result
|
||||||
}
|
}
|
||||||
@@ -23,6 +40,7 @@ function onResult(result) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h1 class="subtitle">{{ $t('barcode.title') }}</h1>
|
<h1 class="subtitle">{{ $t('barcode.title') }}</h1>
|
||||||
|
<div v-if="scanErr">{{ scanErr }}</div>
|
||||||
<div v-if="scanResult">{{ scanResult }}</div>
|
<div v-if="scanResult">{{ scanResult }}</div>
|
||||||
<video poster="data:image/gif,AAAA" ref="scanner"></video>
|
<video poster="data:image/gif,AAAA" ref="scanner"></video>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
128
front/src/api.js
128
front/src/api.js
@@ -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,11 +55,29 @@ 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())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAllBooks(data, error, limit, offset) {
|
||||||
|
const queryParams = new URLSearchParams({
|
||||||
|
limit: limit,
|
||||||
|
offset: offset,
|
||||||
|
})
|
||||||
|
return useFetch(data, error, '/ws/books' + '?' + queryParams.toString())
|
||||||
|
}
|
||||||
|
|
||||||
export function getSearchBooks(data, error, searchterm, lang, searchInventaire, limit, offset) {
|
export function getSearchBooks(data, error, searchterm, lang, searchInventaire, limit, offset) {
|
||||||
const queryParams = new URLSearchParams({
|
const queryParams = new URLSearchParams({
|
||||||
lang: lang,
|
lang: lang,
|
||||||
@@ -90,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')
|
||||||
}
|
}
|
||||||
@@ -98,36 +126,68 @@ 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, bookId) {
|
||||||
|
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 deleteCollectionItem(itemId) {
|
||||||
|
return deleteApiCall('/ws/collection/item/' + itemId)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteCollection(id) {
|
||||||
|
return deleteApiCall('/ws/collection/' + id)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 genericPayloadCall('/ws/book/' + bookId + '/read', { read: true }, 'PUT')
|
return putEndReadDate(bookId, new Date().toISOString().slice(0, 10))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function putUnreadBook(bookId) {
|
export async function putUnreadBook(bookId) {
|
||||||
return genericPayloadCall('/ws/book/' + bookId + '/read', { read: false }, 'PUT')
|
return genericPayloadCall('/ws/book/' + bookId, { read: false }, 'PUT')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function putEndReadDate(bookId, enddate) {
|
export async function putEndReadDate(bookId, enddate) {
|
||||||
return genericPayloadCall('/ws/book/' + bookId + '/read', { read: true, endDate: enddate }, 'PUT')
|
return genericPayloadCall('/ws/book/' + bookId, { read: true, endDate: enddate }, 'PUT')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function putEndReadDateUnset(bookId) {
|
export async function putEndReadDateUnset(bookId) {
|
||||||
return genericPayloadCall('/ws/book/' + bookId + '/read', { read: true, endDate: 'null' }, 'PUT')
|
return genericPayloadCall('/ws/book/' + bookId, { read: true, endDate: 'null' }, 'PUT')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function putStartReadDateUnset(bookId) {
|
export async function putStartReadDateUnset(bookId) {
|
||||||
return genericPayloadCall('/ws/book/' + bookId + '/startread', { startDate: 'null' }, 'PUT')
|
return genericPayloadCall('/ws/book/' + bookId, { startDate: 'null' }, 'PUT')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function putStartRead(bookId) {
|
||||||
|
return putStartReadDate(bookId, new Date().toISOString().slice(0, 10))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function putStartReadDate(bookId, startdate) {
|
export async function putStartReadDate(bookId, startdate) {
|
||||||
return genericPayloadCall('/ws/book/' + bookId + '/startread', { startDate: startdate }, 'PUT')
|
return genericPayloadCall('/ws/book/' + bookId, { startDate: startdate }, 'PUT')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function putWantReadBook(bookId, payload) {
|
export async function putUpdateBook(bookId, payload) {
|
||||||
return genericPayloadCall('/ws/book/' + bookId + '/wantread', payload, 'PUT')
|
return genericPayloadCall('/ws/book/' + bookId, payload, 'PUT')
|
||||||
}
|
|
||||||
|
|
||||||
export async function putRateBook(bookId, payload) {
|
|
||||||
return genericPayloadCall('/ws/book/' + bookId + '/rate', payload, 'PUT')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function postLogin(user) {
|
export function postLogin(user) {
|
||||||
@@ -182,14 +242,26 @@ export function genericPayloadCall(apiRoute, object, method) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function deleteApiCall(apiRoute) {
|
||||||
|
const { user } = useAuthStore()
|
||||||
|
|
||||||
|
if (user != null) {
|
||||||
|
return fetch(apiRoute, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: 'Bearer ' + user.token,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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,7 +5,9 @@
|
|||||||
},
|
},
|
||||||
"navbar": {
|
"navbar": {
|
||||||
"mybooks": "My Books",
|
"mybooks": "My Books",
|
||||||
|
"mycollections": "My Collections",
|
||||||
"addbook": "Add Book",
|
"addbook": "Add Book",
|
||||||
|
"explore": "Explore",
|
||||||
"logout": "Log out",
|
"logout": "Log out",
|
||||||
"signup": "Sign up",
|
"signup": "Sign up",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
@@ -13,14 +15,20 @@
|
|||||||
},
|
},
|
||||||
"barcode": {
|
"barcode": {
|
||||||
"title": "Scan barcode",
|
"title": "Scan barcode",
|
||||||
"barcode": "Scan barcode"
|
"barcode": "Scan barcode",
|
||||||
|
"nocamera": "No camera found."
|
||||||
},
|
},
|
||||||
"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",
|
||||||
@@ -58,6 +66,7 @@
|
|||||||
},
|
},
|
||||||
"bookform": {
|
"bookform": {
|
||||||
"error": "Error when loading book: {error}",
|
"error": "Error when loading book: {error}",
|
||||||
|
"reviewbtn": "My review",
|
||||||
"read": "Read",
|
"read": "Read",
|
||||||
"startread": "Started",
|
"startread": "Started",
|
||||||
"wantread": "Interested"
|
"wantread": "Interested"
|
||||||
@@ -79,5 +88,17 @@
|
|||||||
"releasedate": "Release date:",
|
"releasedate": "Release date:",
|
||||||
"publisher": "Publisher:",
|
"publisher": "Publisher:",
|
||||||
"importing": "Importing..."
|
"importing": "Importing..."
|
||||||
|
},
|
||||||
|
"review": {
|
||||||
|
"title": "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,8 @@
|
|||||||
},
|
},
|
||||||
"navbar": {
|
"navbar": {
|
||||||
"mybooks": "Mes Livres",
|
"mybooks": "Mes Livres",
|
||||||
|
"mycollections": "Mes Listes",
|
||||||
|
"explore": "Explorer",
|
||||||
"addbook": "Ajouter Un Livre",
|
"addbook": "Ajouter Un Livre",
|
||||||
"logout": "Se déconnecter",
|
"logout": "Se déconnecter",
|
||||||
"signup": "S'inscrire",
|
"signup": "S'inscrire",
|
||||||
@@ -13,14 +15,20 @@
|
|||||||
},
|
},
|
||||||
"barcode": {
|
"barcode": {
|
||||||
"title": "Scanner le code-barres",
|
"title": "Scanner le code-barres",
|
||||||
"barcode": "Scanner le code-barres"
|
"barcode": "Scanner le code-barres",
|
||||||
|
"nocamera": "Impossible de détecter la caméra."
|
||||||
},
|
},
|
||||||
"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",
|
||||||
@@ -58,6 +66,7 @@
|
|||||||
},
|
},
|
||||||
"bookform": {
|
"bookform": {
|
||||||
"error": "Erreur pendant le chargement du livre: {error}",
|
"error": "Erreur pendant le chargement du livre: {error}",
|
||||||
|
"reviewbtn": "Ma critique",
|
||||||
"read": "Lu",
|
"read": "Lu",
|
||||||
"startread": "Commencé",
|
"startread": "Commencé",
|
||||||
"wantread": "À lire"
|
"wantread": "À lire"
|
||||||
@@ -79,5 +88,17 @@
|
|||||||
"releasedate": "Date de publication : ",
|
"releasedate": "Date de publication : ",
|
||||||
"publisher": "Maison d'édition : ",
|
"publisher": "Maison d'édition : ",
|
||||||
"importing": "Import en cours..."
|
"importing": "Import en cours..."
|
||||||
|
},
|
||||||
|
"review": {
|
||||||
|
"title": "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}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import fr from './locales/fr.json'
|
|||||||
import en from './locales/en.json'
|
import en from './locales/en.json'
|
||||||
|
|
||||||
// configure i18n
|
// configure i18n
|
||||||
const i18n = createI18n({
|
export const i18n = createI18n({
|
||||||
locale: navigator.language,
|
locale: navigator.language,
|
||||||
fallbackLocale: 'en',
|
fallbackLocale: 'en',
|
||||||
messages: { fr, en },
|
messages: { fr, en },
|
||||||
|
|||||||
@@ -1,26 +1,33 @@
|
|||||||
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'
|
||||||
import ScanBook from './ScanBook.vue'
|
import ScanBook from './ScanBook.vue'
|
||||||
import SearchBook from './SearchBook.vue'
|
import SearchBook from './SearchBook.vue'
|
||||||
import ImportInventaire from './ImportInventaire.vue'
|
import ImportInventaire from './ImportInventaire.vue'
|
||||||
|
import InstanceBrowser from './InstanceBrowser.vue'
|
||||||
import { useAuthStore } from './auth.store'
|
import { useAuthStore } from './auth.store'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{ path: '/', component: Home },
|
{ path: '/', component: Home },
|
||||||
{ path: '/scan', component: ScanBook },
|
{ path: '/scan', component: ScanBook },
|
||||||
|
{ 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 },
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
.clickable {
|
.clickable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.centered {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|||||||
12
go.mod
12
go.mod
@@ -1,8 +1,9 @@
|
|||||||
module git.artlef.fr/bibliomane
|
module git.artlef.fr/bibliomane
|
||||||
|
|
||||||
go 1.25.1
|
go 1.26
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/PuerkitoBio/goquery v1.12.0
|
||||||
github.com/alecthomas/kong v1.14.0
|
github.com/alecthomas/kong v1.14.0
|
||||||
github.com/alecthomas/kong-toml v0.4.0
|
github.com/alecthomas/kong-toml v0.4.0
|
||||||
github.com/gin-gonic/gin v1.11.0
|
github.com/gin-gonic/gin v1.11.0
|
||||||
@@ -11,13 +12,14 @@ require (
|
|||||||
github.com/nicksnyder/go-i18n/v2 v2.6.0
|
github.com/nicksnyder/go-i18n/v2 v2.6.0
|
||||||
github.com/pelletier/go-toml v1.9.5
|
github.com/pelletier/go-toml v1.9.5
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
golang.org/x/crypto v0.48.0
|
golang.org/x/crypto v0.49.0
|
||||||
golang.org/x/text v0.34.0
|
golang.org/x/text v0.35.0
|
||||||
gorm.io/driver/sqlite v1.6.0
|
gorm.io/driver/sqlite v1.6.0
|
||||||
gorm.io/gorm v1.31.0
|
gorm.io/gorm v1.31.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
github.com/bytedance/sonic v1.15.0 // indirect
|
github.com/bytedance/sonic v1.15.0 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||||
@@ -47,8 +49,8 @@ require (
|
|||||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
go.uber.org/mock v0.6.0 // indirect
|
go.uber.org/mock v0.6.0 // indirect
|
||||||
golang.org/x/arch v0.24.0 // indirect
|
golang.org/x/arch v0.24.0 // indirect
|
||||||
golang.org/x/net v0.50.0 // indirect
|
golang.org/x/net v0.52.0 // indirect
|
||||||
golang.org/x/sys v0.41.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
85
go.sum
85
go.sum
@@ -1,5 +1,7 @@
|
|||||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
|
github.com/PuerkitoBio/goquery v1.12.0 h1:pAcL4g3WRXekcB9AU/y1mbKez2dbY2AajVhtkO8RIBo=
|
||||||
|
github.com/PuerkitoBio/goquery v1.12.0/go.mod h1:802ej+gV2y7bbIhOIoPY5sT183ZW0YFofScC4q/hIpQ=
|
||||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||||
github.com/alecthomas/kong v1.14.0 h1:gFgEUZWu2ZmZ+UhyZ1bDhuutbKN1nTtJTwh19Wsn21s=
|
github.com/alecthomas/kong v1.14.0 h1:gFgEUZWu2ZmZ+UhyZ1bDhuutbKN1nTtJTwh19Wsn21s=
|
||||||
@@ -8,6 +10,8 @@ github.com/alecthomas/kong-toml v0.4.0 h1:sSK/HHi2M5jqSXYTxmuxkdZcJ+ip9jhYvwcjDG
|
|||||||
github.com/alecthomas/kong-toml v0.4.0/go.mod h1:hRVV9iGmqYsFqs17jFQgqhkjYIxiklbfy95xJ3nlpKI=
|
github.com/alecthomas/kong-toml v0.4.0/go.mod h1:hRVV9iGmqYsFqs17jFQgqhkjYIxiklbfy95xJ3nlpKI=
|
||||||
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||||
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
|
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||||
|
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||||
@@ -40,6 +44,7 @@ github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
|||||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
@@ -97,19 +102,83 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
|||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
|
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
|
||||||
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
|
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
|
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
|
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
|
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||||
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
|
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
|
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||||
|
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,3 +10,5 @@ image-folder-path = "/tmp"
|
|||||||
|
|
||||||
# The port to listen on for the server.
|
# The port to listen on for the server.
|
||||||
port = "8080"
|
port = "8080"
|
||||||
|
|
||||||
|
book-description-from-babelio = true
|
||||||
|
|||||||
45
internal/apitest/delete_collection_element_test.go
Normal file
45
internal/apitest/delete_collection_element_test.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package apitest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.artlef.fr/bibliomane/internal/testutils"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDeleteCollectionBookHandler_DelOk(t *testing.T) {
|
||||||
|
status := testDeleteCollectionBookHandler("23")
|
||||||
|
assert.Equal(t, http.StatusOK, status)
|
||||||
|
_, collection := testGetCollection(t, "6", "10", "0")
|
||||||
|
assert.Equal(t, int64(5), collection.Count)
|
||||||
|
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)
|
||||||
|
assert.Equal(t, uint(5), collection.Items[4].Position)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteCollectionBookHandler_NotFound(t *testing.T) {
|
||||||
|
status := testDeleteCollectionBookHandler("425")
|
||||||
|
assert.Equal(t, http.StatusNotFound, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteCollectionBookHandler_Unauthorized(t *testing.T) {
|
||||||
|
status := testDeleteCollectionBookHandler("11")
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDeleteCollectionBookHandler(itemId string) int {
|
||||||
|
router := testutils.TestSetup()
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
token := testutils.ConnectDemoUser(router)
|
||||||
|
req, _ := http.NewRequest("DELETE", "/ws/collection/item/"+itemId, nil)
|
||||||
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
return w.Code
|
||||||
|
}
|
||||||
51
internal/apitest/delete_collection_test.go
Normal file
51
internal/apitest/delete_collection_test.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package apitest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.artlef.fr/bibliomane/internal/testutils"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDeleteCollectionHandler_EmptyCollection(t *testing.T) {
|
||||||
|
collectionId := "8"
|
||||||
|
status := testDeleteCollectionHandler(collectionId)
|
||||||
|
assert.Equal(t, http.StatusOK, status)
|
||||||
|
getStatus, _ := testGetCollection(t, collectionId, "10", "0")
|
||||||
|
assert.Equal(t, http.StatusNotFound, getStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteCollectionHandler_NotEmptyCollection(t *testing.T) {
|
||||||
|
collectionId := "7"
|
||||||
|
status := testDeleteCollectionHandler(collectionId)
|
||||||
|
assert.Equal(t, http.StatusOK, status)
|
||||||
|
getStatus, _ := testGetCollection(t, collectionId, "10", "0")
|
||||||
|
assert.Equal(t, http.StatusNotFound, getStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteCollectionHandler_NonExistingCollection(t *testing.T) {
|
||||||
|
collectionId := "425"
|
||||||
|
status := testDeleteCollectionHandler(collectionId)
|
||||||
|
assert.Equal(t, http.StatusNotFound, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteCollectionHandler_ForbiddenCollection(t *testing.T) {
|
||||||
|
collectionId := "3"
|
||||||
|
status := testDeleteCollectionHandler(collectionId)
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDeleteCollectionHandler(id string) int {
|
||||||
|
router := testutils.TestSetup()
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
token := testutils.ConnectDemoUser(router)
|
||||||
|
req, _ := http.NewRequest("DELETE", "/ws/collection/"+id, nil)
|
||||||
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
return w.Code
|
||||||
|
}
|
||||||
21
internal/apitest/fetchallbooks_test.go
Normal file
21
internal/apitest/fetchallbooks_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 TestFetchAllBooks(t *testing.T) {
|
||||||
|
status, result := testFetchBooks(t, "15", "0")
|
||||||
|
assert.Equal(t, http.StatusOK, status)
|
||||||
|
assert.Equal(t, int64(31), result.Count)
|
||||||
|
assert.Equal(t, 15, len(result.Books))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFetchBooks(t *testing.T, limit string, offset string) (int, dto.BookItemsGet) {
|
||||||
|
return testutils.TestFetchModel[dto.BookItemsGet](t, "/ws/books", limit, offset)
|
||||||
|
}
|
||||||
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(5), res.Count)
|
||||||
|
assert.Equal(t, 5, 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"
|
||||||
@@ -15,20 +12,21 @@ import (
|
|||||||
func TestGetBook_Ok(t *testing.T) {
|
func TestGetBook_Ok(t *testing.T) {
|
||||||
book := testGetBook(t, "3", http.StatusOK)
|
book := testGetBook(t, "3", http.StatusOK)
|
||||||
assert.Equal(t,
|
assert.Equal(t,
|
||||||
dto.BookGet{
|
dto.FullBookGet{
|
||||||
Title: "D'un château l'autre",
|
Title: "D'un château l'autre",
|
||||||
Author: "Louis-Ferdinand Céline",
|
Author: "Louis-Ferdinand Céline",
|
||||||
AuthorID: 2,
|
AuthorID: 2,
|
||||||
Rating: 10,
|
Rating: 10,
|
||||||
Read: true,
|
Read: true,
|
||||||
CoverPath: "/static/bookcover/dunchateaulautre.jpg",
|
CoverPath: "/static/bookcover/dunchateaulautre.jpg",
|
||||||
|
Review: "Lorem ipsum dolor sit amet, consectetur adipisci elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
|
||||||
}, book)
|
}, book)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetBook_NoUserBook(t *testing.T) {
|
func TestGetBook_NoUserBook(t *testing.T) {
|
||||||
book := testGetBook(t, "18", http.StatusOK)
|
book := testGetBook(t, "18", http.StatusOK)
|
||||||
assert.Equal(t,
|
assert.Equal(t,
|
||||||
dto.BookGet{
|
dto.FullBookGet{
|
||||||
Title: "De sang-froid",
|
Title: "De sang-froid",
|
||||||
Author: "Truman Capote",
|
Author: "Truman Capote",
|
||||||
AuthorID: 14,
|
AuthorID: 14,
|
||||||
@@ -40,7 +38,7 @@ func TestGetBook_NoUserBook(t *testing.T) {
|
|||||||
func TestGetBook_Description(t *testing.T) {
|
func TestGetBook_Description(t *testing.T) {
|
||||||
book := testGetBook(t, "22", http.StatusOK)
|
book := testGetBook(t, "22", http.StatusOK)
|
||||||
assert.Equal(t,
|
assert.Equal(t,
|
||||||
dto.BookGet{
|
dto.FullBookGet{
|
||||||
Title: "Le complot contre l'Amérique",
|
Title: "Le complot contre l'Amérique",
|
||||||
Author: "Philip Roth",
|
Author: "Philip Roth",
|
||||||
AuthorID: 17,
|
AuthorID: 17,
|
||||||
@@ -60,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.BookGet {
|
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.BookGet
|
|
||||||
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)
|
||||||
|
}
|
||||||
@@ -60,14 +60,14 @@ func TestGetReadBooksHandler_CheckOneBook(t *testing.T) {
|
|||||||
|
|
||||||
token := testutils.ConnectDemo2User(router)
|
token := testutils.ConnectDemo2User(router)
|
||||||
result := testGetReadBooksHandler(t, router, token, 200, "100", "")
|
result := testGetReadBooksHandler(t, router, token, 200, "100", "")
|
||||||
var book dto.BookUserGetBook
|
var book dto.BookItemGet
|
||||||
for _, b := range result.Books {
|
for _, b := range result.Books {
|
||||||
if b.Title == "De sang-froid" {
|
if b.Title == "De sang-froid" {
|
||||||
book = b
|
book = b
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert.Equal(t,
|
assert.Equal(t,
|
||||||
dto.BookUserGetBook{
|
dto.BookItemGet{
|
||||||
ID: 18,
|
ID: 18,
|
||||||
Title: "De sang-froid",
|
Title: "De sang-froid",
|
||||||
Author: "Truman Capote",
|
Author: "Truman Capote",
|
||||||
@@ -77,7 +77,7 @@ func TestGetReadBooksHandler_CheckOneBook(t *testing.T) {
|
|||||||
}, book)
|
}, book)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testGetReadBooksHandler(t *testing.T, router *gin.Engine, userToken string, expectedCode int, limit string, offset string) dto.BookUserGet {
|
func testGetReadBooksHandler(t *testing.T, router *gin.Engine, userToken string, expectedCode int, limit string, offset string) dto.BookItemsGet {
|
||||||
u, err := url.Parse("/ws/mybooks/read")
|
u, err := url.Parse("/ws/mybooks/read")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func TestGetReadingBooksHandler_Demo2(t *testing.T) {
|
|||||||
assert.Equal(t, int64(0), result.Count)
|
assert.Equal(t, int64(0), result.Count)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testGetReadingBooksHandler(t *testing.T, router *gin.Engine, userToken string, expectedCode int, limit string, offset string) dto.BookUserGet {
|
func testGetReadingBooksHandler(t *testing.T, router *gin.Engine, userToken string, expectedCode int, limit string, offset string) dto.BookItemsGet {
|
||||||
u, err := url.Parse("/ws/mybooks/reading")
|
u, err := url.Parse("/ws/mybooks/reading")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testGetbooksHandler(t *testing.T, router *gin.Engine, userToken string, expectedCode int, url string) dto.BookUserGet {
|
func testGetbooksHandler(t *testing.T, router *gin.Engine, userToken string, expectedCode int, url string) dto.BookItemsGet {
|
||||||
req, _ := http.NewRequest("GET", url, nil)
|
req, _ := http.NewRequest("GET", url, nil)
|
||||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", userToken))
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", userToken))
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
router.ServeHTTP(w, req)
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
var parsedResponse dto.BookUserGet
|
var parsedResponse dto.BookItemsGet
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &parsedResponse)
|
err := json.Unmarshal(w.Body.Bytes(), &parsedResponse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
|
|||||||
@@ -27,6 +27,6 @@ func TestGetWantReadBooksHandler_Demo2(t *testing.T) {
|
|||||||
assert.Equal(t, int64(0), result.Count)
|
assert.Equal(t, int64(0), result.Count)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testGetWantReadBooksHandler(t *testing.T, router *gin.Engine, userToken string, expectedCode int) dto.BookUserGet {
|
func testGetWantReadBooksHandler(t *testing.T, router *gin.Engine, userToken string, expectedCode int) dto.BookItemsGet {
|
||||||
return testGetbooksHandler(t, router, userToken, expectedCode, "/ws/mybooks/wantread")
|
return testGetbooksHandler(t, router, userToken, expectedCode, "/ws/mybooks/wantread")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
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)
|
||||||
|
}
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
package apitest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.artlef.fr/bibliomane/internal/testutils"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPutRatingUserBooksHandler_UpdateRating(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"rating": 5
|
|
||||||
}`
|
|
||||||
bookId := "17"
|
|
||||||
testPutRateUserBooks(t, payload, bookId, http.StatusOK)
|
|
||||||
book := testGetBook(t, bookId, http.StatusOK)
|
|
||||||
assert.Equal(t, 5, book.Rating)
|
|
||||||
assert.Equal(t, true, book.Read)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutRatingUserBooksHandler_RateNewBookMakeItRead(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"rating": 7
|
|
||||||
}`
|
|
||||||
bookId := "18"
|
|
||||||
testPutRateUserBooks(t, payload, bookId, http.StatusOK)
|
|
||||||
book := testGetBook(t, bookId, http.StatusOK)
|
|
||||||
assert.Equal(t, 7, book.Rating)
|
|
||||||
assert.Equal(t, true, book.Read)
|
|
||||||
assert.Equal(t, false, book.WantRead)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutRatingUserBooksHandler_RateWantedBook(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"rating": 6
|
|
||||||
}`
|
|
||||||
bookId := "2"
|
|
||||||
testPutRateUserBooks(t, payload, bookId, http.StatusOK)
|
|
||||||
book := testGetBook(t, bookId, http.StatusOK)
|
|
||||||
assert.Equal(t, 6, book.Rating)
|
|
||||||
assert.Equal(t, true, book.Read)
|
|
||||||
assert.Equal(t, false, book.WantRead)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutRatingUserBooksHandler_RatingTypeWrong(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"rating": "bad"
|
|
||||||
}`
|
|
||||||
bookId := "18"
|
|
||||||
testPutRateUserBooks(t, payload, bookId, http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutRatingUserBooksHandler_RatingMin(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"rating": -3
|
|
||||||
}`
|
|
||||||
bookId := "18"
|
|
||||||
testPutRateUserBooks(t, payload, bookId, http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutRatingUserBooksHandler_RatingMax(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"rating": 15
|
|
||||||
}`
|
|
||||||
bookId := "18"
|
|
||||||
testPutRateUserBooks(t, payload, bookId, http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutRatingUserBooksHandler_BadBookId(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"rating": 15
|
|
||||||
}`
|
|
||||||
bookId := "18574"
|
|
||||||
testPutRateUserBooks(t, payload, bookId, http.StatusNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testPutRateUserBooks(t *testing.T, payload string, bookId string, expectedCode int) {
|
|
||||||
testutils.TestBookPutCallWithDemoPayload(t, payload, bookId, expectedCode, "/ws/book/"+bookId+"/rate")
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
package apitest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.artlef.fr/bibliomane/internal/testutils"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPutReadUserBooks_NewReadOk(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"read": true
|
|
||||||
}`
|
|
||||||
bookId := "21"
|
|
||||||
testPutReadUserBooks(t, payload, bookId, http.StatusOK)
|
|
||||||
book := testGetBook(t, bookId, http.StatusOK)
|
|
||||||
assert.Equal(t, true, book.Read)
|
|
||||||
assert.Equal(t, false, book.WantRead)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutReadUserBooks_NewReadDateOk(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"read": true,
|
|
||||||
"endDate": "2025-10-20"
|
|
||||||
}`
|
|
||||||
bookId := "9"
|
|
||||||
testPutReadUserBooks(t, payload, bookId, http.StatusOK)
|
|
||||||
book := testGetBook(t, bookId, http.StatusOK)
|
|
||||||
assert.Equal(t, true, book.Read)
|
|
||||||
assert.Equal(t, "2025-10-20", book.EndReadDate)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutReadUserBooks_UnsetEndDate(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"read": true,
|
|
||||||
"endDate": "null"
|
|
||||||
}`
|
|
||||||
bookId := "9"
|
|
||||||
testPutReadUserBooks(t, payload, bookId, http.StatusOK)
|
|
||||||
book := testGetBook(t, bookId, http.StatusOK)
|
|
||||||
assert.Equal(t, true, book.Read)
|
|
||||||
assert.Equal(t, "", book.EndReadDate)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutReadUserBooks_UnsetReadOk(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"read": false
|
|
||||||
}`
|
|
||||||
bookId := "9"
|
|
||||||
testPutReadUserBooks(t, payload, bookId, http.StatusOK)
|
|
||||||
book := testGetBook(t, bookId, http.StatusOK)
|
|
||||||
assert.Equal(t, false, book.Read)
|
|
||||||
assert.Equal(t, "", book.EndReadDate)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testPutReadUserBooks(t *testing.T, payload string, bookId string, expectedCode int) {
|
|
||||||
testutils.TestBookPutCallWithDemoPayload(t, payload, bookId, expectedCode, "/ws/book/"+bookId+"/read")
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package apitest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.artlef.fr/bibliomane/internal/testutils"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPutStartReadUserBooks_NoDate(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"date": "2025-11-19"
|
|
||||||
}`
|
|
||||||
bookId := "6"
|
|
||||||
testPutStartReadUserBooks(t, payload, bookId, http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutStartReadUserBooks_WrongDateFormat(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"startDate": "19/11/2025"
|
|
||||||
}`
|
|
||||||
bookId := "6"
|
|
||||||
testPutStartReadUserBooks(t, payload, bookId, http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutStartReadUserBooks_NewReadOk(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"startDate": "2025-11-19"
|
|
||||||
}`
|
|
||||||
bookId := "6"
|
|
||||||
testPutStartReadUserBooks(t, payload, bookId, http.StatusOK)
|
|
||||||
book := testGetBook(t, bookId, http.StatusOK)
|
|
||||||
assert.Equal(t, "2025-11-19", book.StartReadDate)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutStartReadUserBooks_Unset(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"startDate": "null"
|
|
||||||
}`
|
|
||||||
bookId := "6"
|
|
||||||
testPutStartReadUserBooks(t, payload, bookId, http.StatusOK)
|
|
||||||
book := testGetBook(t, bookId, http.StatusOK)
|
|
||||||
assert.Equal(t, "", book.StartReadDate)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testPutStartReadUserBooks(t *testing.T, payload string, bookId string, expectedCode int) {
|
|
||||||
testutils.TestBookPutCallWithDemoPayload(t, payload, bookId, expectedCode, "/ws/book/"+bookId+"/startread")
|
|
||||||
}
|
|
||||||
203
internal/apitest/put_userbook_test.go
Normal file
203
internal/apitest/put_userbook_test.go
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
package apitest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.artlef.fr/bibliomane/internal/testutils"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPutRatingUserBooksHandler_UpdateRating(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"rating": 5
|
||||||
|
}`
|
||||||
|
bookId := "17"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusOK)
|
||||||
|
book := testGetBook(t, bookId, http.StatusOK)
|
||||||
|
assert.Equal(t, 5, book.Rating)
|
||||||
|
assert.Equal(t, true, book.Read)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutRatingUserBooksHandler_RateNewBookMakeItRead(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"rating": 7
|
||||||
|
}`
|
||||||
|
bookId := "18"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusOK)
|
||||||
|
book := testGetBook(t, bookId, http.StatusOK)
|
||||||
|
assert.Equal(t, 7, book.Rating)
|
||||||
|
assert.Equal(t, true, book.Read)
|
||||||
|
assert.Equal(t, false, book.WantRead)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutRatingUserBooksHandler_RateWantedBook(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"rating": 6
|
||||||
|
}`
|
||||||
|
bookId := "2"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusOK)
|
||||||
|
book := testGetBook(t, bookId, http.StatusOK)
|
||||||
|
assert.Equal(t, 6, book.Rating)
|
||||||
|
assert.Equal(t, true, book.Read)
|
||||||
|
assert.Equal(t, false, book.WantRead)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutRatingUserBooksHandler_RatingTypeWrong(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"rating": "bad"
|
||||||
|
}`
|
||||||
|
bookId := "18"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutRatingUserBooksHandler_RatingMin(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"rating": -3
|
||||||
|
}`
|
||||||
|
bookId := "18"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutRatingUserBooksHandler_RatingMax(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"rating": 15
|
||||||
|
}`
|
||||||
|
bookId := "18"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutRatingUserBooksHandler_BadBookId(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"rating": 15
|
||||||
|
}`
|
||||||
|
bookId := "18574"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutReadUserBooks_NewReadOk(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"read": true
|
||||||
|
}`
|
||||||
|
bookId := "21"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusOK)
|
||||||
|
book := testGetBook(t, bookId, http.StatusOK)
|
||||||
|
assert.Equal(t, true, book.Read)
|
||||||
|
assert.Equal(t, false, book.WantRead)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutReadUserBooks_NewReadDateOk(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"read": true,
|
||||||
|
"endDate": "2025-10-20"
|
||||||
|
}`
|
||||||
|
bookId := "9"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusOK)
|
||||||
|
book := testGetBook(t, bookId, http.StatusOK)
|
||||||
|
assert.Equal(t, true, book.Read)
|
||||||
|
assert.Equal(t, "2025-10-20", book.EndReadDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutReadUserBooks_UnsetEndDate(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"read": true,
|
||||||
|
"endDate": "null"
|
||||||
|
}`
|
||||||
|
bookId := "9"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusOK)
|
||||||
|
book := testGetBook(t, bookId, http.StatusOK)
|
||||||
|
assert.Equal(t, true, book.Read)
|
||||||
|
assert.Equal(t, "", book.EndReadDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutReadUserBooks_UnsetReadOk(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"read": false
|
||||||
|
}`
|
||||||
|
bookId := "9"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusOK)
|
||||||
|
book := testGetBook(t, bookId, http.StatusOK)
|
||||||
|
assert.Equal(t, false, book.Read)
|
||||||
|
assert.Equal(t, "", book.EndReadDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutStartReadUserBooks_WrongDateFormat(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"startDate": "19/11/2025"
|
||||||
|
}`
|
||||||
|
bookId := "7"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutStartReadUserBooks_UnsetWantReadOk(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"startDate": "2025-11-19"
|
||||||
|
}`
|
||||||
|
bookId := "7"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusOK)
|
||||||
|
book := testGetBook(t, bookId, http.StatusOK)
|
||||||
|
assert.Equal(t, "2025-11-19", book.StartReadDate)
|
||||||
|
assert.Equal(t, false, book.WantRead)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutStartReadUserBooks_UnsetReadOk(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"startDate": "2025-12-20"
|
||||||
|
}`
|
||||||
|
bookId := "13"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusOK)
|
||||||
|
book := testGetBook(t, bookId, http.StatusOK)
|
||||||
|
assert.Equal(t, "2025-12-20", book.StartReadDate)
|
||||||
|
assert.Equal(t, false, book.Read)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutStartReadUserBooks_Unset(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"startDate": "null"
|
||||||
|
}`
|
||||||
|
bookId := "7"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusOK)
|
||||||
|
book := testGetBook(t, bookId, http.StatusOK)
|
||||||
|
assert.Equal(t, "", book.StartReadDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutWantRead_SetTrue(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"wantread": true
|
||||||
|
}`
|
||||||
|
bookId := "17"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusOK)
|
||||||
|
book := testGetBook(t, bookId, http.StatusOK)
|
||||||
|
assert.Equal(t, true, book.WantRead)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutWantRead_SetFalse(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"wantread": false
|
||||||
|
}`
|
||||||
|
bookId := "2"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusOK)
|
||||||
|
book := testGetBook(t, bookId, http.StatusOK)
|
||||||
|
assert.Equal(t, false, book.WantRead)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPutUserBooks(t *testing.T, payload string, bookId string, expectedCode int) {
|
||||||
|
testutils.TestBookPutCallWithDemoPayload(t, payload, bookId, expectedCode, "/ws/book/"+bookId)
|
||||||
|
}
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package apitest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.artlef.fr/bibliomane/internal/testutils"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPutWantRead_SetTrue(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"wantread": true
|
|
||||||
}`
|
|
||||||
bookId := "17"
|
|
||||||
testPutWantReadUserBooks(t, payload, bookId, http.StatusOK)
|
|
||||||
book := testGetBook(t, bookId, http.StatusOK)
|
|
||||||
assert.Equal(t, true, book.WantRead)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutWantRead_SetFalse(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"wantread": false
|
|
||||||
}`
|
|
||||||
bookId := "2"
|
|
||||||
testPutWantReadUserBooks(t, payload, bookId, http.StatusOK)
|
|
||||||
book := testGetBook(t, bookId, http.StatusOK)
|
|
||||||
assert.Equal(t, false, book.WantRead)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testPutWantReadUserBooks(t *testing.T, payload string, bookId string, expectedCode int) {
|
|
||||||
testutils.TestBookPutCallWithDemoPayload(t, payload, bookId, expectedCode, "/ws/book/"+bookId+"/wantread")
|
|
||||||
}
|
|
||||||
@@ -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,17 +16,17 @@ 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.BookSearchGetBook{{
|
[]dto.BookItemGet{{
|
||||||
Title: "Iliade",
|
Title: "Iliade",
|
||||||
Author: "Homère",
|
Author: "Homère",
|
||||||
ID: 29,
|
ID: 29,
|
||||||
@@ -38,26 +39,44 @@ 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.BookSearchGetBook{{
|
[]dto.BookItemGet{{
|
||||||
Title: "Les dieux ont soif",
|
Title: "Les dieux ont soif",
|
||||||
Author: "Anatole France",
|
Author: "Anatole France",
|
||||||
ID: 4,
|
ID: 4,
|
||||||
Rating: 7,
|
Rating: 7,
|
||||||
Read: true,
|
Read: true,
|
||||||
WantRead: false,
|
StartReadDate: "2026-01-30",
|
||||||
CoverPath: "/static/bookcover/lesdieuxontsoif.jpg",
|
WantRead: false,
|
||||||
|
CoverPath: "/static/bookcover/lesdieuxontsoif.jpg",
|
||||||
|
}},
|
||||||
|
result.Books)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSearchBook_OneBookStartRead(t *testing.T) {
|
||||||
|
result := testSearchBook(t, "Recherches", "", "", dto.NoInventaireSearch)
|
||||||
|
assert.Equal(t, int64(1), result.Count)
|
||||||
|
assert.Equal(t,
|
||||||
|
[]dto.BookItemGet{{
|
||||||
|
Title: "Recherches philosophiques",
|
||||||
|
Author: "Ludwig Wittgenstein",
|
||||||
|
ID: 30,
|
||||||
|
Rating: 0,
|
||||||
|
Read: false,
|
||||||
|
StartReadDate: "2025-11-22",
|
||||||
|
WantRead: false,
|
||||||
|
CoverPath: "/static/bookcover/Recherches-philosophiques.jpg",
|
||||||
}},
|
}},
|
||||||
result.Books)
|
result.Books)
|
||||||
}
|
}
|
||||||
|
|
||||||
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.BookSearchGetBook{{
|
[]dto.BookItemGet{{
|
||||||
Title: "Le complot contre l'Amérique",
|
Title: "Le complot contre l'Amérique",
|
||||||
Author: "Philip Roth",
|
Author: "Philip Roth",
|
||||||
ID: 22,
|
ID: 22,
|
||||||
@@ -70,10 +89,10 @@ 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.BookSearchGetBook{{
|
[]dto.BookItemGet{{
|
||||||
ID: 0,
|
ID: 0,
|
||||||
Title: "Les premières enquêtes de Maigret",
|
Title: "Les premières enquêtes de Maigret",
|
||||||
Author: "Georges Simenon",
|
Author: "Georges Simenon",
|
||||||
@@ -89,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.BookSearchGet {
|
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)
|
||||||
@@ -119,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)
|
||||||
@@ -127,7 +147,7 @@ func testSearchBook(t *testing.T, searchterm string, limit string, offset string
|
|||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
router.ServeHTTP(w, req)
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
var result dto.BookSearchGet
|
var result dto.BookItemsGet
|
||||||
s := w.Body.String()
|
s := w.Body.String()
|
||||||
err = json.Unmarshal([]byte(s), &result)
|
err = json.Unmarshal([]byte(s), &result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package callapiutils
|
package callapiutils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -20,9 +21,33 @@ func AddQueryParam(u *url.URL, paramName string, paramValue string) {
|
|||||||
u.RawQuery = q.Encode()
|
u.RawQuery = q.Encode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FetchAndParseResultFromPost[T any, J any](u *url.URL, queryArg *J, queryResult *T) error {
|
||||||
|
payloadBuf := new(bytes.Buffer)
|
||||||
|
json.NewEncoder(payloadBuf).Encode(queryArg)
|
||||||
|
req, err := http.NewRequest("POST", u.String(), payloadBuf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
req.Header.Add("Accept", "application/json")
|
||||||
|
log.Printf("Calling POST %s", u.String())
|
||||||
|
return parseApiQueryResult(u, req, queryResult)
|
||||||
|
}
|
||||||
|
|
||||||
func FetchAndParseResult[T any](u *url.URL, queryResult *T) error {
|
func FetchAndParseResult[T any](u *url.URL, queryResult *T) error {
|
||||||
resp, err := DoApiQuery(u)
|
req, err := http.NewRequest("GET", u.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Add("Accept", "application/json")
|
||||||
|
req.Header.Add("User-Agent", "bibliomane/0.1 (artlef@protonmail.com)")
|
||||||
log.Printf("Calling GET %s", u.String())
|
log.Printf("Calling GET %s", u.String())
|
||||||
|
return parseApiQueryResult(u, req, queryResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseApiQueryResult[T any](u *url.URL, req *http.Request, queryResult *T) error {
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -42,18 +67,8 @@ func FetchAndParseResult[T any](u *url.URL, queryResult *T) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func DoApiQuery(u *url.URL) (*http.Response, error) {
|
return err
|
||||||
client := &http.Client{}
|
|
||||||
req, err := http.NewRequest("GET", u.String(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req.Header.Add("Accept", "application/json")
|
|
||||||
req.Header.Add("User-Agent", "bibliomane/0.1 (artlef@protonmail.com)")
|
|
||||||
return client.Do(req)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ComputeUrl(baseUrl string, paths ...string) (*url.URL, error) {
|
func ComputeUrl(baseUrl string, paths ...string) (*url.URL, error) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -21,6 +34,28 @@ type BookPostImport struct {
|
|||||||
Lang string `json:"lang" binding:"required,max=5"`
|
Lang string `json:"lang" binding:"required,max=5"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UserBookPutUpdate struct {
|
||||||
|
Read *bool `json:"read"`
|
||||||
|
EndDate *string `json:"endDate"`
|
||||||
|
WantRead *bool `json:"wantread"`
|
||||||
|
Rating *int `json:"rating"`
|
||||||
|
StartDate *string `json:"startDate"`
|
||||||
|
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"`
|
||||||
|
|||||||
@@ -6,44 +6,31 @@ type AppInfo struct {
|
|||||||
DemoUsername string `json:"demoUsername"`
|
DemoUsername string `json:"demoUsername"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BookGet 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"`
|
||||||
Rating int `json:"rating"`
|
Summary string `json:"summary"`
|
||||||
Read bool `json:"read"`
|
Review string `json:"review"`
|
||||||
WantRead bool `json:"wantread"`
|
Rating int `json:"rating"`
|
||||||
StartReadDate string `json:"startReadDate"`
|
Read bool `json:"read"`
|
||||||
EndReadDate string `json:"endReadDate"`
|
WantRead bool `json:"wantread"`
|
||||||
CoverPath string `json:"coverPath"`
|
StartReadDate string `json:"startReadDate"`
|
||||||
|
EndReadDate string `json:"endReadDate"`
|
||||||
|
CoverPath string `json:"coverPath"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BookUserGet struct {
|
type BookItemsGet struct {
|
||||||
Count int64 `json:"count"`
|
Count int64 `json:"count"`
|
||||||
Books []BookUserGetBook `json:"books"`
|
Inventaire bool `json:"inventaire"`
|
||||||
|
Books []BookItemGet `json:"books"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BookUserGetBook struct {
|
type BookItemGet struct {
|
||||||
ID uint `json:"id"`
|
|
||||||
Title string `json:"title" binding:"required,max=300"`
|
|
||||||
Author string `json:"author" binding:"max=100"`
|
|
||||||
Rating int `json:"rating" binding:"min=0,max=10"`
|
|
||||||
Read bool `json:"read" binding:"boolean"`
|
|
||||||
WantRead bool `json:"wantread" binding:"boolean"`
|
|
||||||
CoverPath string `json:"coverPath"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BookSearchGet struct {
|
|
||||||
Count int64 `json:"count"`
|
|
||||||
Inventaire bool `json:"inventaire"`
|
|
||||||
Books []BookSearchGetBook `json:"books"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BookSearchGetBook struct {
|
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
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"`
|
||||||
@@ -52,6 +39,36 @@ type BookSearchGetBook struct {
|
|||||||
IsInventaireEdition bool `json:"isinventaireedition"`
|
IsInventaireEdition bool `json:"isinventaireedition"`
|
||||||
Rating int `json:"rating"`
|
Rating int `json:"rating"`
|
||||||
Read bool `json:"read"`
|
Read bool `json:"read"`
|
||||||
|
StartReadDate string `json:"startreaddate"`
|
||||||
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"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ AuthenticationSuccess = "Authentication was a success."
|
|||||||
ValidationRequired = "This field is required."
|
ValidationRequired = "This field is required."
|
||||||
ValidationTooShort = "This field is too short. It should be at least %s characters."
|
ValidationTooShort = "This field is too short. It should be at least %s characters."
|
||||||
ValidationTooLong = "This field is too long. It should be under %s characters."
|
ValidationTooLong = "This field is too long. It should be under %s characters."
|
||||||
|
ValidationLowerThan = "This field should be lower than %s."
|
||||||
|
ValidationGreaterThan = "This field should be greater than %s."
|
||||||
ValidationPropertyFail = "Validation failed for '%s' property."
|
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"
|
||||||
|
Unauthorized = "You are not allowed to access this document."
|
||||||
|
ItemDoesNotBelongToCollection = "Item does not belong to the collection."
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ AuthenticationSuccess = "Connexion réussie."
|
|||||||
ValidationRequired = "Ce champ est requis."
|
ValidationRequired = "Ce champ est requis."
|
||||||
ValidationTooShort = "Ce champ est trop court. Il devrait contenir au moins %s caractères."
|
ValidationTooShort = "Ce champ est trop court. Il devrait contenir au moins %s caractères."
|
||||||
ValidationTooLong = "Ce champ est trop long. Il ne devrait pas dépasser %s caractères."
|
ValidationTooLong = "Ce champ est trop long. Il ne devrait pas dépasser %s caractères."
|
||||||
|
ValidationLowerThan = "Ce champ devrait être inférieur à %s."
|
||||||
|
ValidationGreaterThan = "Ce champ devrait être supérieur à %s."
|
||||||
ValidationPropertyFail = "La validation a échoué pour la propriété '%s'."
|
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"
|
||||||
|
Unauthorized = "Vous n'êtes pas autorisé à accéder à cette page."
|
||||||
|
ItemDoesNotBelongToCollection = "Cet élément n'appartient pas à la liste."
|
||||||
|
|||||||
@@ -132,6 +132,27 @@ func TestCallInventaireEdition(t *testing.T) {
|
|||||||
result)
|
result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCalInventaireEditionNoAuthor(t *testing.T) {
|
||||||
|
|
||||||
|
result, err := CallInventaireEdition(getBaseInventaireUrl(), "isbn:9782226487162", "fr")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
assert.Equal(t,
|
||||||
|
InventaireEditionDetailedSingleResult{
|
||||||
|
Id: "isbn:9782226487162",
|
||||||
|
Title: "Les Yeux de Mona",
|
||||||
|
Author: nil,
|
||||||
|
Description: "",
|
||||||
|
ISBN: "978-2-226-48716-2",
|
||||||
|
Publisher: "éditions Albin Michel",
|
||||||
|
ReleaseDate: "2024-02-01",
|
||||||
|
Image: "https://inventaire.io/img/entities/3ca857913983d694be03dee712bb2af9e2c51747",
|
||||||
|
Lang: "fr",
|
||||||
|
},
|
||||||
|
result)
|
||||||
|
}
|
||||||
|
|
||||||
func TestCallInventaireEditionFromISBN(t *testing.T) {
|
func TestCallInventaireEditionFromISBN(t *testing.T) {
|
||||||
result, err := CallInventaireFromISBN(getBaseInventaireUrl(), "9782070379248", "fr")
|
result, err := CallInventaireFromISBN(getBaseInventaireUrl(), "9782070379248", "fr")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package inventaire
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"git.artlef.fr/bibliomane/internal/callapiutils"
|
"git.artlef.fr/bibliomane/internal/callapiutils"
|
||||||
@@ -32,14 +33,15 @@ func CallInventaireEditionFromWork(inventaireUrl string, workId string, lang str
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return queryResult, err
|
return queryResult, err
|
||||||
}
|
}
|
||||||
queryResult.Count = int64(len(uris.Uris))
|
|
||||||
sort.Strings(uris.Uris)
|
sort.Strings(uris.Uris)
|
||||||
limitedUris := uris.Uris
|
listUris := slices.Compact(uris.Uris)
|
||||||
|
queryResult.Count = int64(len(listUris))
|
||||||
|
limitedUris := listUris
|
||||||
if limit != 0 {
|
if limit != 0 {
|
||||||
l := len(uris.Uris)
|
l := len(listUris)
|
||||||
startIndex := int(math.Min(float64(offset), float64(l)))
|
startIndex := int(math.Min(float64(offset), float64(l)))
|
||||||
endIndex := int(math.Min(float64(limit+offset), float64(l)))
|
endIndex := int(math.Min(float64(limit+offset), float64(l)))
|
||||||
limitedUris = uris.Uris[startIndex:endIndex]
|
limitedUris = listUris[startIndex:endIndex]
|
||||||
}
|
}
|
||||||
editionEntities, err := callInventaireEditionEntities(inventaireUrl, limitedUris)
|
editionEntities, err := callInventaireEditionEntities(inventaireUrl, limitedUris)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
10
internal/model/collection.go
Normal file
10
internal/model/collection.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "gorm.io/gorm"
|
||||||
|
|
||||||
|
type Collection struct {
|
||||||
|
gorm.Model
|
||||||
|
Name string
|
||||||
|
User User
|
||||||
|
UserID uint
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ type UserBook struct {
|
|||||||
Rating int
|
Rating int
|
||||||
Read bool
|
Read bool
|
||||||
WantRead bool
|
WantRead bool
|
||||||
|
Review string
|
||||||
StartReadDate *time.Time
|
StartReadDate *time.Time
|
||||||
EndReadDate *time.Time
|
EndReadDate *time.Time
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type TranslatedError struct {
|
||||||
|
Err error
|
||||||
|
Arg string
|
||||||
|
}
|
||||||
|
|
||||||
type HttpError struct {
|
type HttpError struct {
|
||||||
StatusCode int
|
StatusCode int
|
||||||
Err error
|
Err error
|
||||||
@@ -41,21 +46,35 @@ func ValidateId(db *gorm.DB, id uint, value any) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ReturnErrorsAsJsonResponse(ac *appcontext.AppContext, err error) {
|
func ReturnErrorsAsJsonResponse(ac *appcontext.AppContext, err error) {
|
||||||
var httpError HttpError
|
|
||||||
var ve validator.ValidationErrors
|
ve, isValidationErrors := errors.AsType[validator.ValidationErrors](err)
|
||||||
if errors.As(err, &ve) {
|
if isValidationErrors {
|
||||||
ac.C.JSON(http.StatusBadRequest, getValidationErrors(ac, &ve))
|
ac.C.JSON(http.StatusBadRequest, getValidationErrors(ac, &ve))
|
||||||
} else if errors.As(err, &httpError) {
|
return
|
||||||
ac.C.JSON(httpError.StatusCode, gin.H{"error": httpError.Err.Error()})
|
|
||||||
} else {
|
|
||||||
ac.C.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
}
|
}
|
||||||
|
httpError, isHttpError := errors.AsType[HttpError](err)
|
||||||
|
if isHttpError {
|
||||||
|
ac.C.JSON(httpError.StatusCode, gin.H{"error": httpError.Err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
ac.C.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||||
|
}
|
||||||
|
ac.C.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h HttpError) Error() string {
|
func (h HttpError) Error() string {
|
||||||
return fmt.Sprintf("%d: err %v", h.StatusCode, h.Err)
|
return fmt.Sprintf("%d: err %v", h.StatusCode, h.Err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e TranslatedError) Error() string {
|
||||||
|
return fmt.Sprintf("%v", e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e TranslatedError) ToTranslatedMessage(ac *appcontext.AppContext) string {
|
||||||
|
return fmt.Sprintf(i18nresource.GetTranslatedMessage(ac, e.Error()), e.Arg)
|
||||||
|
}
|
||||||
|
|
||||||
type apiValidationError struct {
|
type apiValidationError struct {
|
||||||
Field string `json:"field"`
|
Field string `json:"field"`
|
||||||
Err string `json:"error"`
|
Err string `json:"error"`
|
||||||
@@ -81,6 +100,10 @@ func computeValidationMessage(ac *appcontext.AppContext, fe *validator.FieldErro
|
|||||||
return fmt.Sprintf(i18nresource.GetTranslatedMessage(ac, "ValidationTooShort"), (*fe).Param())
|
return fmt.Sprintf(i18nresource.GetTranslatedMessage(ac, "ValidationTooShort"), (*fe).Param())
|
||||||
case "max":
|
case "max":
|
||||||
return fmt.Sprintf(i18nresource.GetTranslatedMessage(ac, "ValidationTooLong"), (*fe).Param())
|
return fmt.Sprintf(i18nresource.GetTranslatedMessage(ac, "ValidationTooLong"), (*fe).Param())
|
||||||
|
case "lte":
|
||||||
|
return fmt.Sprintf(i18nresource.GetTranslatedMessage(ac, "ValidationLowerThan"), (*fe).Param())
|
||||||
|
case "gte":
|
||||||
|
return fmt.Sprintf(i18nresource.GetTranslatedMessage(ac, "ValidationGreaterThan"), (*fe).Param())
|
||||||
default:
|
default:
|
||||||
return fmt.Sprintf(i18nresource.GetTranslatedMessage(ac, "ValidationPropertyFail"), tag)
|
return fmt.Sprintf(i18nresource.GetTranslatedMessage(ac, "ValidationPropertyFail"), tag)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
package query
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.artlef.fr/bibliomane/internal/dto"
|
|
||||||
"git.artlef.fr/bibliomane/internal/fileutils"
|
|
||||||
"git.artlef.fr/bibliomane/internal/model"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
func FetchBookGet(db *gorm.DB, userId uint, bookId uint64) (dto.BookGet, error) {
|
|
||||||
var book dto.BookGet
|
|
||||||
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, " +
|
|
||||||
"user_books.rating, user_books.read, user_books.want_read, " +
|
|
||||||
"DATE(user_books.start_read_date) as start_read_date, " +
|
|
||||||
"DATE(user_books.end_read_date) AS end_read_date, " +
|
|
||||||
selectStaticFilesPath()
|
|
||||||
query = query.Select(selectQueryString)
|
|
||||||
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.Where("books.id = ?", bookId)
|
|
||||||
res := query.First(&book)
|
|
||||||
return book, res.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func FetchReadUserBook(db *gorm.DB, userId uint, limit int, offset int) ([]dto.BookUserGetBook, error) {
|
|
||||||
var books []dto.BookUserGetBook
|
|
||||||
query := fetchReadUserBookQuery(db, userId)
|
|
||||||
query = query.Limit(limit)
|
|
||||||
query = query.Offset(offset)
|
|
||||||
res := query.Find(&books)
|
|
||||||
return books, res.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func FetchReadUserBookCount(db *gorm.DB, userId uint) (int64, error) {
|
|
||||||
query := fetchReadUserBookQuery(db, userId)
|
|
||||||
var count int64
|
|
||||||
res := query.Count(&count)
|
|
||||||
return count, res.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func FetchReadingUserBook(db *gorm.DB, userId uint, limit int, offset int) ([]dto.BookUserGetBook, error) {
|
|
||||||
var books []dto.BookUserGetBook
|
|
||||||
query := fetchReadingUserBookQuery(db, userId)
|
|
||||||
query = query.Limit(limit)
|
|
||||||
query = query.Offset(offset)
|
|
||||||
res := query.Find(&books)
|
|
||||||
return books, res.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func FetchReadingUserBookCount(db *gorm.DB, userId uint) (int64, error) {
|
|
||||||
query := fetchReadingUserBookQuery(db, userId)
|
|
||||||
var count int64
|
|
||||||
res := query.Count(&count)
|
|
||||||
return count, res.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchReadUserBookQuery(db *gorm.DB, userId uint) *gorm.DB {
|
|
||||||
query := fetchUserBookGet(db, userId)
|
|
||||||
query = query.Where("user_books.read IS TRUE")
|
|
||||||
return query
|
|
||||||
}
|
|
||||||
func fetchReadingUserBookQuery(db *gorm.DB, userId uint) *gorm.DB {
|
|
||||||
query := fetchUserBookGet(db, userId)
|
|
||||||
query = query.Where("user_books.start_read_date IS NOT NULL")
|
|
||||||
return query
|
|
||||||
}
|
|
||||||
|
|
||||||
func FetchWantReadUserBook(db *gorm.DB, userId uint, limit int, offset int) ([]dto.BookUserGetBook, error) {
|
|
||||||
var books []dto.BookUserGetBook
|
|
||||||
|
|
||||||
query := fetchWantReadUserBookQuery(db, userId)
|
|
||||||
query = query.Limit(limit)
|
|
||||||
query = query.Offset(offset)
|
|
||||||
res := query.Find(&books)
|
|
||||||
return books, res.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func FetchWantReadUserBookCount(db *gorm.DB, userId uint) (int64, error) {
|
|
||||||
query := fetchWantReadUserBookQuery(db, userId)
|
|
||||||
var count int64
|
|
||||||
res := query.Count(&count)
|
|
||||||
return count, res.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchWantReadUserBookQuery(db *gorm.DB, userId uint) *gorm.DB {
|
|
||||||
query := fetchUserBookGet(db, userId)
|
|
||||||
query = query.Where("user_books.want_read IS TRUE")
|
|
||||||
return query
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchUserBookGet(db *gorm.DB, userId uint) *gorm.DB {
|
|
||||||
query := db.Model(&model.UserBook{})
|
|
||||||
query = query.Select("books.id, books.title, authors.name as author, user_books.rating, user_books.read, user_books.want_read, " + selectStaticFilesPath())
|
|
||||||
query = query.Joins("left join books on (books.id = user_books.book_id)")
|
|
||||||
query = joinAuthors(query)
|
|
||||||
query = joinStaticFiles(query)
|
|
||||||
query = query.Where("user_id = ?", userId)
|
|
||||||
return query
|
|
||||||
}
|
|
||||||
|
|
||||||
func selectStaticFilesPath() string {
|
|
||||||
return "(CASE COALESCE(static_files.path, '') WHEN '' THEN '' ELSE concat('" + fileutils.GetWsLinkPrefix() + "', static_files.path) END) as CoverPath"
|
|
||||||
}
|
|
||||||
|
|
||||||
func joinAuthors(query *gorm.DB) *gorm.DB {
|
|
||||||
return query.Joins("left join authors on (authors.id = books.author_id)")
|
|
||||||
}
|
|
||||||
|
|
||||||
func joinStaticFiles(query *gorm.DB) *gorm.DB {
|
|
||||||
return query.Joins("left join static_files on (static_files.id = books.cover_id)")
|
|
||||||
}
|
|
||||||
197
internal/query/querybooks.go
Normal file
197
internal/query/querybooks.go
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
package query
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.artlef.fr/bibliomane/internal/dto"
|
||||||
|
"git.artlef.fr/bibliomane/internal/fileutils"
|
||||||
|
"git.artlef.fr/bibliomane/internal/model"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FetchBookGet(db *gorm.DB, userId uint, bookId uint64) (dto.FullBookGet, error) {
|
||||||
|
var book dto.FullBookGet
|
||||||
|
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.short_description, books.summary, " +
|
||||||
|
"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.end_read_date) AS end_read_date, " +
|
||||||
|
selectStaticFilesPath()
|
||||||
|
query = query.Select(selectQueryString)
|
||||||
|
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.Where("books.id = ?", bookId)
|
||||||
|
res := query.First(&book)
|
||||||
|
return book, res.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchReadUserBook(db *gorm.DB, userId uint, limit int, offset int) ([]dto.BookItemGet, error) {
|
||||||
|
var books []dto.BookItemGet
|
||||||
|
query := fetchReadUserBookQuery(db, userId)
|
||||||
|
query = query.Limit(limit)
|
||||||
|
query = query.Offset(offset)
|
||||||
|
res := query.Find(&books)
|
||||||
|
return books, res.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchReadUserBookCount(db *gorm.DB, userId uint) (int64, error) {
|
||||||
|
query := fetchReadUserBookQuery(db, userId)
|
||||||
|
var count int64
|
||||||
|
res := query.Count(&count)
|
||||||
|
return count, res.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchReadingUserBook(db *gorm.DB, userId uint, limit int, offset int) ([]dto.BookItemGet, error) {
|
||||||
|
var books []dto.BookItemGet
|
||||||
|
query := fetchReadingUserBookQuery(db, userId)
|
||||||
|
query = query.Limit(limit)
|
||||||
|
query = query.Offset(offset)
|
||||||
|
res := query.Find(&books)
|
||||||
|
return books, res.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchReadingUserBookCount(db *gorm.DB, userId uint) (int64, error) {
|
||||||
|
query := fetchReadingUserBookQuery(db, userId)
|
||||||
|
var count int64
|
||||||
|
res := query.Count(&count)
|
||||||
|
return count, res.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchReadUserBookQuery(db *gorm.DB, userId uint) *gorm.DB {
|
||||||
|
query := fetchUserBookGet(db, userId)
|
||||||
|
query = query.Where("user_books.read IS TRUE")
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
func fetchReadingUserBookQuery(db *gorm.DB, userId uint) *gorm.DB {
|
||||||
|
query := fetchUserBookGet(db, userId)
|
||||||
|
query = query.Where("user_books.start_read_date IS NOT NULL AND (user_books.read IS NULL OR user_books.read IS FALSE)")
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchWantReadUserBook(db *gorm.DB, userId uint, limit int, offset int) ([]dto.BookItemGet, error) {
|
||||||
|
var books []dto.BookItemGet
|
||||||
|
|
||||||
|
query := fetchWantReadUserBookQuery(db, userId)
|
||||||
|
query = query.Limit(limit)
|
||||||
|
query = query.Offset(offset)
|
||||||
|
res := query.Find(&books)
|
||||||
|
return books, res.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchWantReadUserBookCount(db *gorm.DB, userId uint) (int64, error) {
|
||||||
|
query := fetchWantReadUserBookQuery(db, userId)
|
||||||
|
var count int64
|
||||||
|
res := query.Count(&count)
|
||||||
|
return count, res.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchWantReadUserBookQuery(db *gorm.DB, userId uint) *gorm.DB {
|
||||||
|
query := fetchUserBookGet(db, userId)
|
||||||
|
query = query.Where("user_books.want_read IS TRUE")
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch only books where userbook exists
|
||||||
|
func fetchUserBookGet(db *gorm.DB, userId uint) *gorm.DB {
|
||||||
|
query := db.Model(&model.UserBook{})
|
||||||
|
query = query.Select(selectBookItem())
|
||||||
|
query = query.Joins("left join books on (books.id = user_books.book_id)")
|
||||||
|
query = joinAuthors(query)
|
||||||
|
query = joinStaticFiles(query)
|
||||||
|
query = query.Where("user_id = ?", userId)
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchAllBooks(db *gorm.DB, userId uint, limit int, offset int) ([]dto.BookItemGet, error) {
|
||||||
|
var books []dto.BookItemGet
|
||||||
|
query := fetchBookQueryBuilder(db, userId)
|
||||||
|
query = query.Limit(limit)
|
||||||
|
query = query.Offset(offset)
|
||||||
|
query = query.Order("books.id DESC")
|
||||||
|
res := query.Find(&books)
|
||||||
|
return books, res.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchAllBooksCount(db *gorm.DB, userId uint) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
query := fetchBookQueryBuilder(db, userId)
|
||||||
|
res := query.Count(&count)
|
||||||
|
return count, res.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchBookSearchByAuthorGet(db *gorm.DB, userId uint, authorId uint64, limit int, offset int) ([]dto.BookItemGet, error) {
|
||||||
|
var books []dto.BookItemGet
|
||||||
|
query := fetchBookSearchByAuthorQuery(db, userId, authorId)
|
||||||
|
query = query.Limit(limit)
|
||||||
|
query = query.Offset(offset)
|
||||||
|
res := query.Find(&books)
|
||||||
|
return books, res.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchBookSearchByAuthorGetCount(db *gorm.DB, userId uint, authorId uint64) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
query := fetchBookSearchByAuthorQuery(db, userId, authorId)
|
||||||
|
res := query.Count(&count)
|
||||||
|
return count, res.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchBookSearchByAuthorQuery(db *gorm.DB, userId uint, authorId uint64) *gorm.DB {
|
||||||
|
query := fetchBookQueryBuilder(db, userId)
|
||||||
|
return query.Where("authors.id = ?", authorId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchBookSearchGet(db *gorm.DB, userId uint, searchterm string, limit int, offset int) ([]dto.BookItemGet, error) {
|
||||||
|
var books []dto.BookItemGet
|
||||||
|
query := fetchBookSearchQuery(db, userId, searchterm)
|
||||||
|
query = query.Limit(limit)
|
||||||
|
query = query.Offset(offset)
|
||||||
|
res := query.Find(&books)
|
||||||
|
return books, res.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchBookSearchGetCount(db *gorm.DB, userId uint, searchterm string) (int64, error) {
|
||||||
|
query := fetchBookSearchQuery(db, userId, searchterm)
|
||||||
|
var count int64
|
||||||
|
res := query.Count(&count)
|
||||||
|
return count, res.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchBookSearchQuery(db *gorm.DB, userId uint, searchterm string) *gorm.DB {
|
||||||
|
query := fetchBookQueryBuilder(db, userId)
|
||||||
|
isIsbn, _ := regexp.Match(`\d{10,13}`, []byte(searchterm))
|
||||||
|
if isIsbn {
|
||||||
|
query = query.Where("books.isbn = ?", searchterm)
|
||||||
|
} else {
|
||||||
|
query = query.Where("LOWER(books.title) LIKE ?", "%"+strings.ToLower(searchterm)+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch all books even whithout user books
|
||||||
|
func fetchBookQueryBuilder(db *gorm.DB, userId uint) *gorm.DB {
|
||||||
|
query := db.Model(&model.Book{})
|
||||||
|
query = query.Select(selectBookItem())
|
||||||
|
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)
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectBookItem() string {
|
||||||
|
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 {
|
||||||
|
return "(CASE COALESCE(static_files.path, '') WHEN '' THEN '' ELSE concat('" + fileutils.GetWsLinkPrefix() + "', static_files.path) END) as CoverPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinAuthors(query *gorm.DB) *gorm.DB {
|
||||||
|
return query.Joins("left join authors on (authors.id = books.author_id)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinStaticFiles(query *gorm.DB) *gorm.DB {
|
||||||
|
return query.Joins("left join static_files on (static_files.id = books.cover_id)")
|
||||||
|
}
|
||||||
136
internal/query/querycollections.go
Normal file
136
internal/query/querycollections.go
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchCollectionHeaderFromItem(db *gorm.DB, itemId uint) (CollectionHeader, error) {
|
||||||
|
var collection CollectionHeader
|
||||||
|
query := db.Model(&model.Collection{})
|
||||||
|
query = query.Select("collections.name, collections.user_id")
|
||||||
|
query = query.Joins("left join collection_items on (collection_items.collection_id = collections.id)")
|
||||||
|
query = query.Where("collection_items.id = ?", itemId)
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
package query
|
|
||||||
|
|
||||||
import (
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.artlef.fr/bibliomane/internal/dto"
|
|
||||||
"git.artlef.fr/bibliomane/internal/model"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
func FetchBookSearchByAuthorGet(db *gorm.DB, userId uint, authorId uint64, limit int, offset int) ([]dto.BookSearchGetBook, error) {
|
|
||||||
var books []dto.BookSearchGetBook
|
|
||||||
query := fetchBookSearchByAuthorQuery(db, userId, authorId)
|
|
||||||
query = query.Limit(limit)
|
|
||||||
query = query.Offset(offset)
|
|
||||||
res := query.Find(&books)
|
|
||||||
return books, res.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func FetchBookSearchByAuthorGetCount(db *gorm.DB, userId uint, authorId uint64) (int64, error) {
|
|
||||||
var count int64
|
|
||||||
query := fetchBookSearchByAuthorQuery(db, userId, authorId)
|
|
||||||
res := query.Count(&count)
|
|
||||||
return count, res.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchBookSearchByAuthorQuery(db *gorm.DB, userId uint, authorId uint64) *gorm.DB {
|
|
||||||
query := fetchBookSearchQueryBuilder(db, userId)
|
|
||||||
return query.Where("authors.id = ?", authorId)
|
|
||||||
}
|
|
||||||
|
|
||||||
func FetchBookSearchGet(db *gorm.DB, userId uint, searchterm string, limit int, offset int) ([]dto.BookSearchGetBook, error) {
|
|
||||||
var books []dto.BookSearchGetBook
|
|
||||||
query := fetchBookSearchQuery(db, userId, searchterm)
|
|
||||||
query = query.Limit(limit)
|
|
||||||
query = query.Offset(offset)
|
|
||||||
res := query.Find(&books)
|
|
||||||
return books, res.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func FetchBookSearchGetCount(db *gorm.DB, userId uint, searchterm string) (int64, error) {
|
|
||||||
query := fetchBookSearchQuery(db, userId, searchterm)
|
|
||||||
var count int64
|
|
||||||
res := query.Count(&count)
|
|
||||||
return count, res.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchBookSearchQuery(db *gorm.DB, userId uint, searchterm string) *gorm.DB {
|
|
||||||
query := fetchBookSearchQueryBuilder(db, userId)
|
|
||||||
isIsbn, _ := regexp.Match(`\d{10,13}`, []byte(searchterm))
|
|
||||||
if isIsbn {
|
|
||||||
query = query.Where("books.isbn = ?", searchterm)
|
|
||||||
} else {
|
|
||||||
query = query.Where("LOWER(books.title) LIKE ?", "%"+strings.ToLower(searchterm)+"%")
|
|
||||||
}
|
|
||||||
|
|
||||||
return query
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchBookSearchQueryBuilder(db *gorm.DB, userId uint) *gorm.DB {
|
|
||||||
query := db.Model(&model.Book{})
|
|
||||||
query = query.Select("books.id, books.title, authors.name as author, books.small_description as description, books.inventaire_id, user_books.rating, user_books.read, user_books.want_read, " + selectStaticFilesPath())
|
|
||||||
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)
|
|
||||||
return query
|
|
||||||
}
|
|
||||||
@@ -50,5 +50,5 @@ func GetAuthorBooksHandler(ac appcontext.AppContext) {
|
|||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ac.C.JSON(http.StatusOK, dto.BookSearchGet{Books: books, Count: count})
|
ac.C.JSON(http.StatusOK, dto.BookItemsGet{Books: books, Count: count})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,6 +2,7 @@ package routes
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.artlef.fr/bibliomane/internal/appcontext"
|
"git.artlef.fr/bibliomane/internal/appcontext"
|
||||||
"git.artlef.fr/bibliomane/internal/dto"
|
"git.artlef.fr/bibliomane/internal/dto"
|
||||||
@@ -40,18 +41,21 @@ 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) {
|
||||||
author, err := fetchOrCreateInventaireAuthor(ac, inventaireEdition.Author)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
book := model.Book{
|
book := model.Book{
|
||||||
Title: inventaireEdition.Title,
|
Title: inventaireEdition.Title,
|
||||||
SmallDescription: inventaireEdition.Description,
|
ShortDescription: inventaireEdition.Description,
|
||||||
InventaireID: inventaireEdition.Id,
|
InventaireID: inventaireEdition.Id,
|
||||||
Author: *author,
|
|
||||||
AddedBy: *user,
|
AddedBy: *user,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if inventaireEdition.Author != nil {
|
||||||
|
author, err := fetchOrCreateInventaireAuthor(ac, inventaireEdition.Author)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
book.Author = *author
|
||||||
|
}
|
||||||
|
|
||||||
if inventaireEdition.Image != "" {
|
if inventaireEdition.Image != "" {
|
||||||
cover, err := fileutils.DownloadFile(ac, inventaireEdition.Image)
|
cover, err := fileutils.DownloadFile(ac, inventaireEdition.Image)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -59,10 +63,21 @@ func saveInventaireBookToDb(ac appcontext.AppContext, inventaireEdition inventai
|
|||||||
}
|
}
|
||||||
book.Cover = cover
|
book.Cover = cover
|
||||||
}
|
}
|
||||||
err = ac.Db.Save(&book).Error
|
|
||||||
|
err := ac.Db.Save(&book).Error
|
||||||
return &book, err
|
return &book, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func findIsbn(inventaireEdition *inventaire.InventaireEditionDetailedSingleResult) string {
|
||||||
|
if inventaireEdition.ISBN != "" {
|
||||||
|
return strings.ReplaceAll(inventaireEdition.ISBN, "-", "")
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(inventaireEdition.Id, "isbn:") {
|
||||||
|
return inventaireEdition.Id[5:]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func fetchOrCreateInventaireAuthor(ac appcontext.AppContext, inventaireAuthor *inventaire.InventaireAuthorResult) (*model.Author, error) {
|
func fetchOrCreateInventaireAuthor(ac appcontext.AppContext, inventaireAuthor *inventaire.InventaireAuthorResult) (*model.Author, error) {
|
||||||
var author model.Author
|
var author model.Author
|
||||||
res := ac.Db.Where("inventaire_id = ?", inventaireAuthor.ID).First(&author)
|
res := ac.Db.Where("inventaire_id = ?", inventaireAuthor.ID).First(&author)
|
||||||
|
|||||||
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")
|
||||||
|
}
|
||||||
@@ -37,8 +37,8 @@ func GetSearchBooksHandler(ac appcontext.AppContext) {
|
|||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var returnedBooks dto.BookSearchGet
|
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)
|
||||||
@@ -49,9 +49,9 @@ func GetSearchBooksHandler(ac appcontext.AppContext) {
|
|||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
returnedBooks = dto.BookSearchGet{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)
|
||||||
@@ -62,7 +62,7 @@ func GetSearchBooksHandler(ac appcontext.AppContext) {
|
|||||||
ac.C.JSON(http.StatusOK, returnedBooks)
|
ac.C.JSON(http.StatusOK, returnedBooks)
|
||||||
}
|
}
|
||||||
|
|
||||||
func searchInInventaireAPI(inventaireUrl string, searchterm string, limit int, offset int, params dto.BookSearchGetParam) (*dto.BookSearchGet, error) {
|
func searchInInventaireAPI(inventaireUrl string, searchterm string, limit int, offset int, params dto.BookSearchGetParam) (*dto.BookItemsGet, error) {
|
||||||
|
|
||||||
isIsbn, err := regexp.Match(`\d{10,13}`, []byte(searchterm))
|
isIsbn, err := regexp.Match(`\d{10,13}`, []byte(searchterm))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -74,11 +74,11 @@ func searchInInventaireAPI(inventaireUrl string, searchterm string, limit int, o
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var bookSearchGet dto.BookSearchGet
|
var bookSearchGet dto.BookItemsGet
|
||||||
if queryResult != nil {
|
if queryResult != nil {
|
||||||
bookSearchGet = inventaireEditionToBookSearchGet(*queryResult)
|
bookSearchGet = inventaireEditionToBookSearchGet(*queryResult)
|
||||||
} else {
|
} else {
|
||||||
bookSearchGet = dto.BookSearchGet{Count: 0, Inventaire: true}
|
bookSearchGet = dto.BookItemsGet{Count: 0, Inventaire: true}
|
||||||
}
|
}
|
||||||
return &bookSearchGet, err
|
return &bookSearchGet, err
|
||||||
} else {
|
} else {
|
||||||
@@ -91,9 +91,9 @@ func searchInInventaireAPI(inventaireUrl string, searchterm string, limit int, o
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func inventaireEditionToBookSearchGet(result inventaire.InventaireEditionDetailedSingleResult) dto.BookSearchGet {
|
func inventaireEditionToBookSearchGet(result inventaire.InventaireEditionDetailedSingleResult) dto.BookItemsGet {
|
||||||
var books []dto.BookSearchGetBook
|
var books []dto.BookItemGet
|
||||||
bookSearchGetBook := dto.BookSearchGetBook{
|
bookSearchGetBook := dto.BookItemGet{
|
||||||
ID: 0,
|
ID: 0,
|
||||||
Title: result.Title,
|
Title: result.Title,
|
||||||
Author: result.Author.Name,
|
Author: result.Author.Name,
|
||||||
@@ -106,17 +106,17 @@ func inventaireEditionToBookSearchGet(result inventaire.InventaireEditionDetaile
|
|||||||
CoverPath: result.Image,
|
CoverPath: result.Image,
|
||||||
}
|
}
|
||||||
books = append(books, bookSearchGetBook)
|
books = append(books, bookSearchGetBook)
|
||||||
return dto.BookSearchGet{Count: 1, Inventaire: true, Books: books}
|
return dto.BookItemsGet{Count: 1, Inventaire: true, Books: books}
|
||||||
}
|
}
|
||||||
|
|
||||||
func inventaireBooksToBookSearchGet(inventaireUrl string, results inventaire.InventaireSearchResult) dto.BookSearchGet {
|
func inventaireBooksToBookSearchGet(inventaireUrl string, results inventaire.InventaireSearchResult) dto.BookItemsGet {
|
||||||
var books []dto.BookSearchGetBook
|
var books []dto.BookItemGet
|
||||||
for _, b := range results.Results {
|
for _, b := range results.Results {
|
||||||
coverPath := ""
|
coverPath := ""
|
||||||
if b.Image != "" && strings.HasPrefix(b.Image, "/") {
|
if b.Image != "" && strings.HasPrefix(b.Image, "/") {
|
||||||
coverPath = inventaireUrl + b.Image
|
coverPath = inventaireUrl + b.Image
|
||||||
}
|
}
|
||||||
bookSearchGetBook := dto.BookSearchGetBook{
|
bookSearchGetBook := dto.BookItemGet{
|
||||||
ID: 0,
|
ID: 0,
|
||||||
Title: b.Label,
|
Title: b.Label,
|
||||||
Author: "",
|
Author: "",
|
||||||
@@ -129,5 +129,5 @@ func inventaireBooksToBookSearchGet(inventaireUrl string, results inventaire.Inv
|
|||||||
}
|
}
|
||||||
books = append(books, bookSearchGetBook)
|
books = append(books, bookSearchGetBook)
|
||||||
}
|
}
|
||||||
return dto.BookSearchGet{Count: results.Total, Inventaire: true, Books: books}
|
return dto.BookItemsGet{Count: results.Total, Inventaire: true, Books: books}
|
||||||
}
|
}
|
||||||
|
|||||||
40
internal/routes/booksget.go
Normal file
40
internal/routes/booksget.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"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 GetBooksHandler(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
|
||||||
|
}
|
||||||
|
books, err := query.FetchAllBooks(ac.Db, user.ID, limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
count, err := query.FetchAllBooksCount(ac.Db, user.ID)
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ac.C.JSON(http.StatusOK, dto.BookItemsGet{Count: count, Inventaire: false, Books: books})
|
||||||
|
}
|
||||||
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")
|
||||||
|
}
|
||||||
66
internal/routes/collectiondelete.go
Normal file
66
internal/routes/collectiondelete.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.artlef.fr/bibliomane/internal/appcontext"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DeleteCollectionHandler(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
|
||||||
|
}
|
||||||
|
|
||||||
|
err = myvalidator.ValidateId(ac.Db, uint(collectionId), &model.Collection{})
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
collection, err := query.FetchCollectionHeader(ac.Db, uint(collectionId))
|
||||||
|
|
||||||
|
if collection.UserID != user.ID {
|
||||||
|
err := myvalidator.HttpError{
|
||||||
|
StatusCode: http.StatusUnauthorized,
|
||||||
|
Err: errors.New(i18nresource.GetTranslatedMessage(&ac, "Unauthorized")),
|
||||||
|
}
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
collectionDb := model.Collection{}
|
||||||
|
collectionDb.ID = uint(collectionId)
|
||||||
|
|
||||||
|
err = ac.Db.Where("collection_id = ?", collectionId).Delete(&model.CollectionItem{}).Error
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = ac.Db.Delete(&collectionDb).Error
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = ac.Db.Delete(&collectionDb).Error
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ac.C.JSON(http.StatusOK, "Success")
|
||||||
|
}
|
||||||
75
internal/routes/collectionget.go
Normal file
75
internal/routes/collectionget.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
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/model"
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
err = myvalidator.ValidateId(ac.Db, uint(collectionId), &model.Collection{})
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, 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)
|
||||||
|
}
|
||||||
62
internal/routes/collectionremovebookdelete.go
Normal file
62
internal/routes/collectionremovebookdelete.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.artlef.fr/bibliomane/internal/appcontext"
|
||||||
|
"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 DeleteCollectionBookHandler(ac appcontext.AppContext) {
|
||||||
|
collectionItemId, 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 collectionItem model.CollectionItem
|
||||||
|
err = ac.Db.First(&collectionItem, collectionItemId).Error
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
collection, err := query.FetchCollectionHeaderFromItem(ac.Db, collectionItem.ID)
|
||||||
|
|
||||||
|
if collection.UserID != user.ID {
|
||||||
|
err := myvalidator.HttpError{
|
||||||
|
StatusCode: http.StatusUnauthorized,
|
||||||
|
Err: errors.New(i18nresource.GetTranslatedMessage(&ac, "Unauthorized")),
|
||||||
|
}
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ac.Db.Delete(&collectionItem).Error
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//update position on remaining items
|
||||||
|
q := ac.Db.Model(&model.CollectionItem{})
|
||||||
|
q = q.Where("collection_id = ? AND position > ?", collectionItem.CollectionID, collectionItem.Position)
|
||||||
|
err = q.UpdateColumn("position", gorm.Expr("position - 1")).Error
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ac.C.JSON(http.StatusOK, "Success")
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
@@ -7,21 +7,35 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.artlef.fr/bibliomane/internal/appcontext"
|
"git.artlef.fr/bibliomane/internal/appcontext"
|
||||||
|
"git.artlef.fr/bibliomane/internal/dto"
|
||||||
"git.artlef.fr/bibliomane/internal/model"
|
"git.artlef.fr/bibliomane/internal/model"
|
||||||
"git.artlef.fr/bibliomane/internal/myvalidator"
|
"git.artlef.fr/bibliomane/internal/myvalidator"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func PutReadUserBookHandler(ac appcontext.AppContext) {
|
func PutUserBookHandler(ac appcontext.AppContext) {
|
||||||
data, err := retrieveDataFromContext(ac)
|
bookId64, err := strconv.ParseUint(ac.C.Param("id"), 10, 64)
|
||||||
|
bookId := uint(bookId64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
ac.C.JSON(http.StatusBadRequest, gin.H{"error": err})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
bookId := data.BookId
|
err = myvalidator.ValidateId(ac.Db, bookId, &model.Book{})
|
||||||
user := data.User
|
if err != nil {
|
||||||
var read userbookPutRead
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
err = ac.C.ShouldBindJSON(&read)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := ac.GetAuthenticatedUser()
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var userBookPut dto.UserBookPutUpdate
|
||||||
|
err = ac.C.ShouldBindJSON(&userBookPut)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
return
|
return
|
||||||
@@ -32,14 +46,61 @@ func PutReadUserBookHandler(ac appcontext.AppContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userbook.Read = read.Read
|
if userBookPut.Read != nil {
|
||||||
|
err = updateReadStatus(&userbook, &userBookPut)
|
||||||
if read.EndDate != "" {
|
|
||||||
d, err := parseDate(read.EndDate)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if userBookPut.WantRead != nil {
|
||||||
|
userbook.WantRead = *userBookPut.WantRead
|
||||||
|
}
|
||||||
|
if userBookPut.StartDate != nil {
|
||||||
|
d, err := parseDate(*userBookPut.StartDate)
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if d != nil {
|
||||||
|
userbook.Read = false
|
||||||
|
userbook.WantRead = false
|
||||||
|
}
|
||||||
|
userbook.StartReadDate = d
|
||||||
|
}
|
||||||
|
if userBookPut.Rating != nil {
|
||||||
|
err = validateRating(*userBookPut.Rating)
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateRating(&userbook, &userBookPut)
|
||||||
|
}
|
||||||
|
if userBookPut.Review != nil {
|
||||||
|
userbook.Review = *userBookPut.Review
|
||||||
|
}
|
||||||
|
ac.Db.Save(&userbook)
|
||||||
|
ac.C.String(http.StatusOK, "Success")
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateRating(rating int) error {
|
||||||
|
//struct used for validation
|
||||||
|
var ratingStruct struct {
|
||||||
|
Rating int `validate:"gte=0,lte=10"`
|
||||||
|
}
|
||||||
|
ratingStruct.Rating = rating
|
||||||
|
validate := validator.New()
|
||||||
|
return validate.Struct(ratingStruct)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateReadStatus(userbook *model.UserBook, userBookPut *dto.UserBookPutUpdate) error {
|
||||||
|
userbook.Read = *userBookPut.Read
|
||||||
|
|
||||||
|
if userBookPut.EndDate != nil && *userBookPut.EndDate != "" {
|
||||||
|
d, err := parseDate(*userBookPut.EndDate)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
userbook.EndReadDate = d
|
userbook.EndReadDate = d
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,84 +113,11 @@ func PutReadUserBookHandler(ac appcontext.AppContext) {
|
|||||||
if !userbook.Read {
|
if !userbook.Read {
|
||||||
userbook.EndReadDate = nil
|
userbook.EndReadDate = nil
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
ac.Db.Save(&userbook)
|
|
||||||
ac.C.String(http.StatusOK, "Success")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func PutWantReadUserBookHandler(ac appcontext.AppContext) {
|
func updateRating(userbook *model.UserBook, userBookPut *dto.UserBookPutUpdate) {
|
||||||
data, err := retrieveDataFromContext(ac)
|
userbook.Rating = *userBookPut.Rating
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
bookId := data.BookId
|
|
||||||
user := data.User
|
|
||||||
var wantread userbookPutWantRead
|
|
||||||
err = ac.C.ShouldBindJSON(&wantread)
|
|
||||||
if err != nil {
|
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userbook, err := fetchOrCreateUserBook(ac, bookId, &user)
|
|
||||||
if err != nil {
|
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userbook.WantRead = wantread.WantRead
|
|
||||||
ac.Db.Save(&userbook)
|
|
||||||
ac.C.String(http.StatusOK, "Success")
|
|
||||||
}
|
|
||||||
|
|
||||||
func PutStartReadUserBookHandler(ac appcontext.AppContext) {
|
|
||||||
data, err := retrieveDataFromContext(ac)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
bookId := data.BookId
|
|
||||||
user := data.User
|
|
||||||
|
|
||||||
var startDateToParse userbookPutStartRead
|
|
||||||
err = ac.C.ShouldBindJSON(&startDateToParse)
|
|
||||||
if err != nil {
|
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userbook, err := fetchOrCreateUserBook(ac, bookId, &user)
|
|
||||||
if err != nil {
|
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
d, err := parseDate(startDateToParse.StartDate)
|
|
||||||
if err != nil {
|
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userbook.StartReadDate = d
|
|
||||||
|
|
||||||
ac.Db.Save(&userbook)
|
|
||||||
ac.C.String(http.StatusOK, "Success")
|
|
||||||
}
|
|
||||||
|
|
||||||
func PutRateUserBookHandler(ac appcontext.AppContext) {
|
|
||||||
data, err := retrieveDataFromContext(ac)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
bookId := data.BookId
|
|
||||||
user := data.User
|
|
||||||
var rating userbookPutRating
|
|
||||||
err = ac.C.ShouldBindJSON(&rating)
|
|
||||||
if err != nil {
|
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userbook, err := fetchOrCreateUserBook(ac, bookId, &user)
|
|
||||||
if err != nil {
|
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userbook.Rating = rating.Rating
|
|
||||||
|
|
||||||
//if rated, set to "read" (a rating = 0 means unrated)
|
//if rated, set to "read" (a rating = 0 means unrated)
|
||||||
if userbook.Rating > 0 {
|
if userbook.Rating > 0 {
|
||||||
@@ -137,30 +125,6 @@ func PutRateUserBookHandler(ac appcontext.AppContext) {
|
|||||||
//if set to read, remove want read
|
//if set to read, remove want read
|
||||||
userbook.WantRead = false
|
userbook.WantRead = false
|
||||||
}
|
}
|
||||||
ac.Db.Save(&userbook)
|
|
||||||
ac.C.String(http.StatusOK, "Success")
|
|
||||||
}
|
|
||||||
|
|
||||||
type userbookPutRead struct {
|
|
||||||
Read bool `json:"read"`
|
|
||||||
EndDate string `json:"endDate"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type userbookPutWantRead struct {
|
|
||||||
WantRead bool `json:"wantread"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type userbookPutRating struct {
|
|
||||||
Rating int `json:"rating" binding:"min=0,max=10"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type userbookPutStartRead struct {
|
|
||||||
StartDate string `json:"startDate" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type apiCallData struct {
|
|
||||||
BookId uint
|
|
||||||
User model.User
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseDate(dateToParse string) (*time.Time, error) {
|
func parseDate(dateToParse string) (*time.Time, error) {
|
||||||
@@ -173,27 +137,6 @@ func parseDate(dateToParse string) (*time.Time, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func retrieveDataFromContext(ac appcontext.AppContext) (apiCallData, error) {
|
|
||||||
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 apiCallData{}, err
|
|
||||||
}
|
|
||||||
err = myvalidator.ValidateId(ac.Db, bookId, &model.Book{})
|
|
||||||
if err != nil {
|
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
|
||||||
return apiCallData{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
user, fetchUserErr := ac.GetAuthenticatedUser()
|
|
||||||
if fetchUserErr != nil {
|
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
|
||||||
return apiCallData{}, fetchUserErr
|
|
||||||
}
|
|
||||||
return apiCallData{BookId: bookId, User: user}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchOrCreateUserBook(ac appcontext.AppContext, bookId uint, user *model.User) (model.UserBook, error) {
|
func fetchOrCreateUserBook(ac appcontext.AppContext, bookId uint, user *model.User) (model.UserBook, error) {
|
||||||
var userbook model.UserBook
|
var userbook model.UserBook
|
||||||
res := ac.Db.Where("user_id = ? AND book_id = ?", user.ID, bookId).First(&userbook)
|
res := ac.Db.Where("user_id = ? AND book_id = ?", user.ID, bookId).First(&userbook)
|
||||||
|
|||||||
@@ -35,5 +35,5 @@ func GetMyBooksReadHandler(ac appcontext.AppContext) {
|
|||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ac.C.JSON(http.StatusOK, dto.BookUserGet{Count: count, Books: userbooks})
|
ac.C.JSON(http.StatusOK, dto.BookItemsGet{Count: count, Books: userbooks})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,5 +35,5 @@ func GetMyBooksReadingHandler(ac appcontext.AppContext) {
|
|||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ac.C.JSON(http.StatusOK, dto.BookUserGet{Count: count, Books: userbooks})
|
ac.C.JSON(http.StatusOK, dto.BookItemsGet{Count: count, Books: userbooks})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,5 +35,5 @@ func GetMyBooksWantReadHandler(ac appcontext.AppContext) {
|
|||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ac.C.JSON(http.StatusOK, dto.BookUserGet{Count: count, Books: userbooks})
|
ac.C.JSON(http.StatusOK, dto.BookItemsGet{Count: count, Books: userbooks})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ func Setup(config *config.Config) *gin.Engine {
|
|||||||
ws.GET("/appinfo", func(c *gin.Context) {
|
ws.GET("/appinfo", func(c *gin.Context) {
|
||||||
routes.GetAppInfo(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
routes.GetAppInfo(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||||
})
|
})
|
||||||
|
ws.GET("/books", func(c *gin.Context) {
|
||||||
|
routes.GetBooksHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||||
|
})
|
||||||
ws.GET("/mybooks/read", func(c *gin.Context) {
|
ws.GET("/mybooks/read", func(c *gin.Context) {
|
||||||
routes.GetMyBooksReadHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
routes.GetMyBooksReadHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||||
})
|
})
|
||||||
@@ -58,17 +60,11 @@ func Setup(config *config.Config) *gin.Engine {
|
|||||||
ws.GET("/book/:id", func(c *gin.Context) {
|
ws.GET("/book/:id", func(c *gin.Context) {
|
||||||
routes.GetBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
routes.GetBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||||
})
|
})
|
||||||
ws.PUT("/book/:id/read", func(c *gin.Context) {
|
ws.PUT("/book/:id", func(c *gin.Context) {
|
||||||
routes.PutReadUserBookHandler(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/:id/wantread", func(c *gin.Context) {
|
ws.PUT("/book/edit/:id", func(c *gin.Context) {
|
||||||
routes.PutWantReadUserBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
routes.PutBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||||
})
|
|
||||||
ws.PUT("/book/:id/startread", func(c *gin.Context) {
|
|
||||||
routes.PutStartReadUserBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
|
||||||
})
|
|
||||||
ws.PUT("/book/:id/rate", func(c *gin.Context) {
|
|
||||||
routes.PutRateUserBookHandler(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})
|
||||||
@@ -82,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})
|
||||||
})
|
})
|
||||||
@@ -91,7 +102,12 @@ func Setup(config *config.Config) *gin.Engine {
|
|||||||
ws.POST("/upload/cover", func(c *gin.Context) {
|
ws.POST("/upload/cover", func(c *gin.Context) {
|
||||||
routes.PostUploadBookCoverHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
routes.PostUploadBookCoverHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||||
})
|
})
|
||||||
|
ws.DELETE("/collection/:id", func(c *gin.Context) {
|
||||||
|
routes.DeleteCollectionHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||||
|
})
|
||||||
|
ws.DELETE("/collection/item/:id", func(c *gin.Context) {
|
||||||
|
routes.DeleteCollectionBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||||
|
})
|
||||||
r.Static("/static/bookcover", config.ImageFolderPath)
|
r.Static("/static/bookcover", config.ImageFolderPath)
|
||||||
|
|
||||||
folders := []string{"assets", "css", "image"}
|
folders := []string{"assets", "css", "image"}
|
||||||
@@ -104,7 +120,7 @@ func Setup(config *config.Config) *gin.Engine {
|
|||||||
r.StaticFS("/"+folder, http.FS(subFs))
|
r.StaticFS("/"+folder, http.FS(subFs))
|
||||||
}
|
}
|
||||||
|
|
||||||
r.StaticFileFS("/favicon.ico", "favicon.ico", http.FS(front.Frontend))
|
r.StaticFileFS("/favicon.svg", "favicon.svg", http.FS(front.Frontend))
|
||||||
r.GET("/", serveIndexHtml)
|
r.GET("/", serveIndexHtml)
|
||||||
|
|
||||||
r.NoRoute(serveIndexHtml)
|
r.NoRoute(serveIndexHtml)
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.artlef.fr/bibliomane/internal/config"
|
"git.artlef.fr/bibliomane/internal/config"
|
||||||
"git.artlef.fr/bibliomane/internal/setup"
|
"git.artlef.fr/bibliomane/internal/setup"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSetup() *gin.Engine {
|
func TestSetup() *gin.Engine {
|
||||||
@@ -62,5 +63,85 @@ func TestBookPutCallWithDemoPayload(t *testing.T, payload string, bookId string,
|
|||||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
router.ServeHTTP(w, req)
|
router.ServeHTTP(w, req)
|
||||||
assert.Equal(t, expectedCode, w.Code)
|
if w.Code != expectedCode {
|
||||||
|
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.2.0"
|
applicationVersion := "0.8.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