issue/2 #28

Merged
dbroqua merged 6 commits from issue/2 into master 2022-03-04 16:33:46 +01:00
16 changed files with 325 additions and 52 deletions
Showing only changes of commit 1f63c74ac3 - Show all commits

View file

@ -51,10 +51,12 @@
"debug": "^4.3.3", "debug": "^4.3.3",
"disconnect": "^1.2.2", "disconnect": "^1.2.2",
"ejs": "^3.1.6", "ejs": "^3.1.6",
"excel4node": "^1.7.2",
"express": "^4.17.2", "express": "^4.17.2",
"express-session": "^1.17.2", "express-session": "^1.17.2",
"knacss": "^8.0.4", "knacss": "^8.0.4",
"moment": "^2.29.1", "moment": "^2.29.1",
"moment-timezone": "^0.5.34",
"mongoose": "^6.2.1", "mongoose": "^6.2.1",
"mongoose-unique-validator": "^3.0.0", "mongoose-unique-validator": "^3.0.0",
"passport": "^0.5.2", "passport": "^0.5.2",

Binary file not shown.

View file

@ -24,6 +24,8 @@
<glyph glyph-name="right-open" unicode="&#xe808;" d="M618 361l-414-415q-11-10-25-10t-25 10l-93 93q-11 11-11 25t11 25l296 297-296 296q-11 11-11 25t11 25l93 93q10 11 25 11t25-11l414-414q10-11 10-25t-10-25z" horiz-adv-x="714.3" /> <glyph glyph-name="right-open" unicode="&#xe808;" d="M618 361l-414-415q-11-10-25-10t-25 10l-93 93q-11 11-11 25t11 25l296 297-296 296q-11 11-11 25t11 25l93 93q10 11 25 11t25-11l414-414q10-11 10-25t-10-25z" horiz-adv-x="714.3" />
<glyph glyph-name="export" unicode="&#xe809;" d="M786 298v-144q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h142q7 0 13-6t5-12q0-15-15-18-43-15-74-34-5-2-9-2h-62q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v119q0 11 10 16 16 7 31 21 8 9 19 4 12-5 12-16z m132 277l-214-214q-10-11-25-11-7 0-14 3-22 9-22 33v107h-89q-181 0-245-73-66-77-41-264 2-13-11-19-5-1-7-1-9 0-14 7-6 8-12 17t-22 39-28 55-21 64-10 68q0 27 2 51t8 50 15 49 27 45 38 42 52 34 70 27 89 17 110 6h89v107q0 24 22 33 7 3 14 3 14 0 25-11l214-214q11-10 11-25t-11-25z" horiz-adv-x="928.6" />
<glyph glyph-name="spin" unicode="&#xe839;" d="M855 9c-189-190-520-172-705 13-190 190-200 494-28 695 11 13 21 26 35 34 36 23 85 18 117-13 30-31 35-76 16-112-5-9-9-15-16-22-140-151-145-379-8-516 153-153 407-121 542 34 106 122 142 297 77 451-83 198-305 291-510 222l0 1c236 82 492-24 588-252 71-167 37-355-72-493-11-15-23-29-36-42z" horiz-adv-x="1000" /> <glyph glyph-name="spin" unicode="&#xe839;" d="M855 9c-189-190-520-172-705 13-190 190-200 494-28 695 11 13 21 26 35 34 36 23 85 18 117-13 30-31 35-76 16-112-5-9-9-15-16-22-140-151-145-379-8-516 153-153 407-121 542 34 106 122 142 297 77 451-83 198-305 291-510 222l0 1c236 82 492-24 588-252 71-167 37-355-72-493-11-15-23-29-36-42z" horiz-adv-x="1000" />
<glyph glyph-name="link-ext" unicode="&#xf08e;" d="M786 332v-178q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h393q7 0 12-5t5-13v-36q0-8-5-13t-12-5h-393q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v178q0 8 5 13t13 5h36q8 0 13-5t5-13z m214 482v-285q0-15-11-25t-25-11-25 11l-98 98-364-364q-5-6-13-6t-12 6l-64 64q-6 5-6 12t6 13l364 364-98 98q-11 11-11 25t11 25 25 11h285q15 0 25-11t11-25z" horiz-adv-x="1000" /> <glyph glyph-name="link-ext" unicode="&#xf08e;" d="M786 332v-178q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h393q7 0 12-5t5-13v-36q0-8-5-13t-12-5h-393q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v178q0 8 5 13t13 5h36q8 0 13-5t5-13z m214 482v-285q0-15-11-25t-25-11-25 11l-98 98-364-364q-5-6-13-6t-12 6l-64 64q-6 5-6 12t6 13l364 364-98 98q-11 11-11 25t11 25 25 11h285q15 0 25-11t11-25z" horiz-adv-x="1000" />

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -3,6 +3,10 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
&.inline {
flex-direction: row;
}
&.has-addons { &.has-addons {
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
@ -40,6 +44,28 @@
} }
} }
input[type="radio"] {
border-radius: 50%;
appearance: none;
width: 1.2rem;
height: 1.2rem;
vertical-align: text-bottom;
outline: 0;
box-shadow: inset 0 0 0 1px var(--input-active-color);
background-color: #fff;
transition: background-size .15s;
cursor: pointer;
&:checked {
box-shadow: inset 0 0 0 4px var(--input-active-color);
}
}
input[type="radio"] + label,
label + input[type="radio"] {
margin-left: 12px;
}
select { select {
appearance: none; appearance: none;
background-image: url("data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20standalone%3D%22no%22%3F%3E%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20xmlns%3Axlink%3D%22http%3A//www.w3.org/1999/xlink%22%20style%3D%22isolation%3Aisolate%22%20viewBox%3D%220%200%2020%2020%22%20width%3D%2220%22%20height%3D%2220%22%3E%3Cpath%20d%3D%22%20M%209.96%2011.966%20L%203.523%205.589%20C%202.464%204.627%200.495%206.842%201.505%207.771%20L%201.505%207.771%20L%208.494%2014.763%20C%209.138%2015.35%2010.655%2015.369%2011.29%2014.763%20L%2011.29%2014.763%20L%2018.49%207.771%20C%2019.557%206.752%2017.364%204.68%2016.262%205.725%20L%2016.262%205.725%20L%209.96%2011.966%20Z%20%22%20fill%3D%22inherit%22/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20standalone%3D%22no%22%3F%3E%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20xmlns%3Axlink%3D%22http%3A//www.w3.org/1999/xlink%22%20style%3D%22isolation%3Aisolate%22%20viewBox%3D%220%200%2020%2020%22%20width%3D%2220%22%20height%3D%2220%22%3E%3Cpath%20d%3D%22%20M%209.96%2011.966%20L%203.523%205.589%20C%202.464%204.627%200.495%206.842%201.505%207.771%20L%201.505%207.771%20L%208.494%2014.763%20C%209.138%2015.35%2010.655%2015.369%2011.29%2014.763%20L%2011.29%2014.763%20L%2018.49%207.771%20C%2019.557%206.752%2017.364%204.68%2016.262%205.725%20L%2016.262%205.725%20L%209.96%2011.966%20Z%20%22%20fill%3D%22inherit%22/%3E%3C/svg%3E");

View file

@ -41,6 +41,7 @@
.icon-eye:before { content: '\e806'; } /* '' */ .icon-eye:before { content: '\e806'; } /* '' */
.icon-left-open:before { content: '\e807'; } /* '' */ .icon-left-open:before { content: '\e807'; } /* '' */
.icon-right-open:before { content: '\e808'; } /* '' */ .icon-right-open:before { content: '\e808'; } /* '' */
.icon-export:before { content: '\e809'; } /* '' */
.icon-spin:before { content: '\e839'; } /* '' */ .icon-spin:before { content: '\e839'; } /* '' */
.icon-link-ext:before { content: '\f08e'; } /* '' */ .icon-link-ext:before { content: '\f08e'; } /* '' */
.icon-sun:before { content: '\f185'; } /* '' */ .icon-sun:before { content: '\f185'; } /* '' */

View file

@ -19,8 +19,8 @@
// COMPOSANTS (à ajouter au besoin) // COMPOSANTS (à ajouter au besoin)
// @import "../node_modules/knacss/sass/components/button"; // @import "../node_modules/knacss/sass/components/button";
// @import "components/burger"; // @import "components/burger";
// @import "components/checkbox"; // @import "../node_modules/knacss/sass/components/checkbox";
// @import "components/radio"; @import "../node_modules/knacss/sass/components/radio";
// @import "../node_modules/knacss/sass/components/select"; // @import "../node_modules/knacss/sass/components/select";
// @import "components/quote"; // @import "components/quote";

View file

@ -12,6 +12,7 @@ import config, { env, mongoDbUri, secret } from "./config";
import { isXhr } from "./helpers"; import { isXhr } from "./helpers";
import indexRouter from "./routes"; import indexRouter from "./routes";
import maCollectionRouter from "./routes/ma-collection";
import importAlbumRouterApiV1 from "./routes/api/v1/albums"; import importAlbumRouterApiV1 from "./routes/api/v1/albums";
import importSearchRouterApiV1 from "./routes/api/v1/search"; import importSearchRouterApiV1 from "./routes/api/v1/search";
@ -80,6 +81,7 @@ app.use(
); );
app.use("/", indexRouter); app.use("/", indexRouter);
app.use("/ma-collection", maCollectionRouter);
app.use("/api/v1/albums", importAlbumRouterApiV1); app.use("/api/v1/albums", importAlbumRouterApiV1);
app.use("/api/v1/search", importSearchRouterApiV1); app.use("/api/v1/search", importSearchRouterApiV1);

View file

@ -1,4 +1,6 @@
import moment from "moment"; import moment from "moment";
import momenttz from "moment-timezone";
import xl from "excel4node";
import Pages from "./Pages"; import Pages from "./Pages";
@ -9,6 +11,126 @@ import ErrorEvent from "../libs/error";
* Classe permettant la gestion des albums d'un utilisateur * Classe permettant la gestion des albums d'un utilisateur
*/ */
class Albums extends Pages { class Albums extends Pages {
/**
* Méthode permettant de convertir les rows en csv
* @param {Array} rows
*
* @return {string}
*/
static async convertToCsv(rows) {
let data = "";
data +=
"Artiste;Titre;Genre;Styles;Pays;Année;Date de sortie;Format\n\r";
for (let i = 0; i < rows.length; i += 1) {
const {
artists_sort,
title,
genres,
styles,
country,
year,
released,
formats,
} = rows[i];
let format = "";
for (let j = 0; j < formats.length; j += 1) {
format += `${format !== "" ? ", " : ""}${formats[j].name}`;
}
data += `${artists_sort};${title};${genres.join()};${styles.join()};${country};${year};${released};${format}\n\r`;
}
return data;
}
/**
* Méthode permettant de convertir les rows en fichier xls
* @param {Array} rows
*
* @return {string}
*/
static async convertToXls(rows) {
const wb = new xl.Workbook();
const ws = wb.addWorksheet("MusicTopus");
const headerStyle = wb.createStyle({
font: {
color: "#FFFFFF",
size: 11,
},
fill: {
type: "pattern",
patternType: "solid",
bgColor: "#595959",
fgColor: "#595959",
},
});
const style = wb.createStyle({
font: {
color: "#000000",
size: 11,
},
numberFormat: "0000",
});
const header = [
"Artiste",
"Titre",
"Genre",
"Styles",
"Pays",
"Année",
"Date de sortie",
"Format",
];
for (let i = 0; i < header.length; i += 1) {
ws.cell(1, i + 1)
.string(header[i])
.style(headerStyle);
}
for (let i = 0; i < rows.length; i += 1) {
const currentRow = i + 2;
const {
artists_sort,
title,
genres,
styles,
country,
year,
released,
formats,
} = rows[i];
let format = "";
for (let j = 0; j < formats.length; j += 1) {
format += `${format !== "" ? ", " : ""}${formats[j].name}`;
}
ws.cell(currentRow, 1).string(artists_sort).style(style);
ws.cell(currentRow, 2).string(title).style(style);
ws.cell(currentRow, 3).string(genres.join()).style(style);
ws.cell(currentRow, 4).string(styles.join()).style(style);
if (country) {
ws.cell(currentRow, 5).string(country).style(style);
}
if (year) {
ws.cell(currentRow, 6).number(year).style(style);
}
if (released) {
ws.cell(currentRow, 7)
.date(momenttz.tz(released, "Europe/Paris").hour(12))
.style({ numberFormat: "dd/mm/yyyy" });
}
ws.cell(currentRow, 8).string(format).style(style);
}
return wb;
}
/** /**
* Méthode permettant d'ajouter un album dans une collection * Méthode permettant d'ajouter un album dans une collection
* @param {Object} req * @param {Object} req
@ -59,16 +181,15 @@ class Albums extends Pages {
*/ */
async getAll() { async getAll() {
const { const {
page = 1, page,
limit = 4, limit,
exportFormat = "json",
sort = "artists_sort", sort = "artists_sort",
order = "asc", order = "asc",
artists_sort, artists_sort,
format, format,
} = this.req.query; } = this.req.query;
const skip = (page - 1) * limit;
const where = {}; const where = {};
if (artists_sort) { if (artists_sort) {
@ -83,26 +204,44 @@ class Albums extends Pages {
...where, ...where,
}); });
let params = {
sort: {
[sort]: order.toLowerCase() === "asc" ? 1 : -1,
},
};
if (page && limit) {
const skip = (page - 1) * limit;
params = {
...params,
skip,
limit,
};
}
const rows = await AlbumsModel.find( const rows = await AlbumsModel.find(
{ {
user: this.req.user._id, user: this.req.user._id,
...where, ...where,
}, },
[], [],
{ params
skip,
limit,
sort: {
[sort]: order.toLowerCase() === "asc" ? 1 : -1,
},
}
); );
switch (exportFormat) {
case "csv":
return Albums.convertToCsv(rows);
case "xls":
return Albums.convertToXls(rows);
case "json":
default:
return { return {
rows, rows,
count, count,
}; };
} }
}
/** /**
* Méthode permettant de supprimer un élément d'une collection * Méthode permettant de supprimer un élément d'une collection

View file

@ -13,10 +13,20 @@ router
try { try {
const albums = new Albums(req); const albums = new Albums(req);
const data = await albums.getAll(); const data = await albums.getAll();
const { exportFormat = "json" } = req.query;
sendResponse(req, res, data); switch (exportFormat) {
case "csv":
res.header("Content-Type", "text/csv");
return res.status(200).send(data);
case "xls":
return data.write("musictopus.xls", res);
case "json":
default:
return sendResponse(req, res, data);
}
} catch (err) { } catch (err) {
next(err); return next(err);
} }
}) })
.post(ensureLoggedIn("/connexion"), async (req, res, next) => { .post(ensureLoggedIn("/connexion"), async (req, res, next) => {

View file

@ -4,7 +4,6 @@ import { ensureLoggedIn } from "connect-ensure-login";
import Pages from "../middleware/Pages"; import Pages from "../middleware/Pages";
import Auth from "../middleware/Auth"; import Auth from "../middleware/Auth";
import Albums from "../middleware/Albums";
import render from "../libs/format"; import render from "../libs/format";
@ -89,38 +88,6 @@ router
} }
}); });
router
.route("/ma-collection")
.get(ensureLoggedIn("/connexion"), async (req, res, next) => {
try {
const page = new Albums(req, "mon-compte/ma-collection");
await page.loadMyCollection();
if (page.getPageContent("artists").length > 0) {
render(res, page);
} else {
res.redirect("/ajouter-un-album");
}
} catch (err) {
next(err);
}
});
router
.route("/ma-collection/:itemId")
.get(ensureLoggedIn("/connexion"), async (req, res, next) => {
try {
const page = new Albums(req, "mon-compte/ma-collection/details");
await page.loadItem();
render(res, page);
} catch (err) {
next(err);
}
});
router.route("/nous-contacter").get(async (req, res, next) => { router.route("/nous-contacter").get(async (req, res, next) => {
try { try {
const page = new Pages(req, "nous-contacter"); const page = new Pages(req, "nous-contacter");

View file

@ -0,0 +1,53 @@
import express from "express";
import { ensureLoggedIn } from "connect-ensure-login";
import Albums from "../middleware/Albums";
import render from "../libs/format";
// eslint-disable-next-line new-cap
const router = express.Router();
router.route("/").get(ensureLoggedIn("/connexion"), async (req, res, next) => {
try {
const page = new Albums(req, "mon-compte/ma-collection");
await page.loadMyCollection();
if (page.getPageContent("artists").length > 0) {
render(res, page);
} else {
res.redirect("/ajouter-un-album");
}
} catch (err) {
next(err);
}
});
router
.route("/exporter")
.get(ensureLoggedIn("/connexion"), async (req, res, next) => {
try {
const page = new Albums(req, "mon-compte/ma-collection/exporter");
render(res, page);
} catch (err) {
next(err);
}
});
router
.route("/:itemId")
.get(ensureLoggedIn("/connexion"), async (req, res, next) => {
try {
const page = new Albums(req, "mon-compte/ma-collection/details");
await page.loadItem();
render(res, page);
} catch (err) {
next(err);
}
});
export default router;

View file

@ -68,6 +68,9 @@
<a class="navbar-item" href="/ma-collection"> <a class="navbar-item" href="/ma-collection">
Ma collection Ma collection
</a> </a>
<a class="navbar-item" href="/ma-collection/exporter">
Exporter ma collection
</a>
</div> </div>
</div> </div>
<% } %> <% } %>

View file

@ -0,0 +1,68 @@
<main class="layout-maxed ma-collection-exporter" id="app">
<h1>Exporter ma collection</h1>
<p>
Les formats CSV et Excel sont facilement lisiblent par un humain. Dans ces 2 formats vous trouverez seulement les informations principales de vos albums, à savoir :
</p>
<ul>
<li>Nom de l'artiste</li>
<li>Nom de l'album</li>
<li>Liste des genres</li>
<li>Liste des styles</li>
<li>Pays (ou région) de distribution</li>
<li>Année de sortie</li>
<li>Date de sortie</li>
<li>Format de l'album</li>
</ul>
<p>
Le format XML quand a lui est un peu moins lisible par un humain, même s'il reste un fichier texte. Dans ce format vous retrouverez toute les informations de vos albums.
</p>
<p>
Enfin le dernier format, MusicTopus, vous permettra d'exporter votre collection afin de l'importer ensuite sur une autre instance MusicTopus.
</p>
<form @submit="exportCollection">
<strong>Choisir le format d'export</strong>
<div class="field inline">
<input type="radio" name="format" v-model="format" value="csv" id="csv">
<label for="csv">CSV</label>
</div>
<div class="field inline">
<input type="radio" name="format" v-model="format" value="xls" id="xls">
<label for="xls">Excel</label>
</div>
<div class="field inline">
<input type="radio" name="format" v-model="format" value="xml" id="xml">
<label for="xml">XML</label>
</div>
<div class="field inline">
<input type="radio" name="format" v-model="format" value="musictopus" id="musictopus">
<label for="musictopus">MusicTopus</label>
</div>
<button type="submit" class="button is-primary my-16">
<i class="icon-export"></i>
Exporter
</button>
</form>
</main>
<script>
Vue.createApp({
data() {
return {
format: 'xml',
}
},
created() {
},
destroyed() {
},
methods: {
exportCollection(event) {
event.preventDefault();
window.open(`/api/v1/albums?exportFormat=${this.format}`, '_blank');
}
},
}).mount('#app');
</script>