Browse Source

Merge branch 'master' of gitea.invaders.stream:onja/bodacc

pull/6/head
Yann 5 months ago
parent
commit
c3cb714858
  1. 1
      bin/www
  2. 8939
      package-lock.json
  3. 6
      package.json
  4. 16
      public/js/main.min.js.LICENSE.txt
  5. 3
      src/app.js
  6. 50
      src/assets/js/main.js
  7. 14
      src/assets/sass/main.scss
  8. 17
      src/config/params.json
  9. 230
      src/models/file.js
  10. 186
      src/routes/index.js
  11. 29
      src/services/file.js
  12. 20
      src/services/template.js
  13. 42
      src/subscribers/consoleSubscriber.js
  14. 110
      src/subscribers/emailSubscriber.js
  15. 26
      src/subscribers/ioSubscriber.js
  16. 8
      src/views/emails/parse-success.hbs
  17. 13
      src/views/layout/email.hbs
  18. 131
      src/views/pages/index.hbs

1
bin/www

@ -30,6 +30,7 @@ const io = configureSocket(server);
server.listen(port); server.listen(port);
server.on('error', onError); server.on('error', onError);
server.on('listening', onListening); server.on('listening', onListening);
server.timeout = 15 * 60 * 1000;
/** /**
* Normalize a port into a number, string, or false. * Normalize a port into a number, string, or false.

8939
package-lock.json

File diff suppressed because it is too large

6
package.json

@ -10,6 +10,8 @@
"watch": "webpack --watch --mode development" "watch": "webpack --watch --mode development"
}, },
"dependencies": { "dependencies": {
"@popperjs/core": "^2.11.8",
"axios": "^0.19.2",
"bootstrap": "^5.3.2", "bootstrap": "^5.3.2",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"csv-parser": "^3.0.0", "csv-parser": "^3.0.0",
@ -22,13 +24,13 @@
"http-errors": "^2.0.0", "http-errors": "^2.0.0",
"jquery": "^3.7.1", "jquery": "^3.7.1",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"popper.js": "^1.16.1",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"socket.io": "^4.7.2", "socket.io": "^4.7.2",
"socket.io-client": "^4.7.2", "socket.io-client": "^4.7.2",
"toastr": "^2.1.4" "toastr": "^2.1.4"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.23.2",
"@babel/preset-env": "^7.22.20", "@babel/preset-env": "^7.22.20",
"babel-core": "^6.26.3", "babel-core": "^6.26.3",
"babel-loader": "^9.1.3", "babel-loader": "^9.1.3",
@ -37,7 +39,7 @@
"css-loader": "^6.8.1", "css-loader": "^6.8.1",
"css-minimizer-webpack-plugin": "^5.0.1", "css-minimizer-webpack-plugin": "^5.0.1",
"mini-css-extract-plugin": "^2.7.6", "mini-css-extract-plugin": "^2.7.6",
"node-sass": "^4.9.4", "node-sass": "^9.0.0",
"nodemon": "^3.0.1", "nodemon": "^3.0.1",
"sass-loader": "^13.3.2", "sass-loader": "^13.3.2",
"webpack": "^5.88.2", "webpack": "^5.88.2",

16
public/js/main.min.js.LICENSE.txt

@ -0,0 +1,16 @@
/*!
* Bootstrap v5.3.2 (https://getbootstrap.com/)
* Copyright 2011-2023 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
/*!
* jQuery JavaScript Library v3.7.1
* https://jquery.com/
*
* Copyright OpenJS Foundation and other contributors
* Released under the MIT license
* https://jquery.org/license
*
* Date: 2023-08-28T13:37Z
*/

3
src/app.js

@ -8,6 +8,7 @@ const { basedir } = require('./config/constants');
var indexRouter = require('./routes/index'); var indexRouter = require('./routes/index');
const consoleSubscriber = require('./subscribers/consoleSubscriber'); const consoleSubscriber = require('./subscribers/consoleSubscriber');
const mailerSubscriber = require('./subscribers/emailSubscriber');
var app = express(); var app = express();
@ -26,6 +27,8 @@ app.use(express.urlencoded({ extended: true }));
app.use(cookieParser()); app.use(cookieParser());
app.use(express.static(path.join(basedir, 'public'))); app.use(express.static(path.join(basedir, 'public')));
mailerSubscriber(app);
app.use('/', indexRouter); app.use('/', indexRouter);
// catch 404 and forward to error handler // catch 404 and forward to error handler

50
src/assets/js/main.js

@ -36,23 +36,7 @@ const sendRequest = (url, data) => {
type: 'POST', type: 'POST',
data: data, data: data,
success: function(data, textStatus, jqXHR) { success: function(data, textStatus, jqXHR) {
// Créez un lien de téléchargement et définissez ses attributs resolve(data);
const blob = new Blob([data], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
let date = new Date();
// format date to YYYY-MM-DD HH:MM
date = date.toISOString().slice(0, 16).replace('T', ' ');
a.href = url;
a.download = `export-${date}.csv`; // Nom du fichier
document.body.appendChild(a);
// Cliquez sur le lien pour déclencher le téléchargement
a.click();
// Supprimez le lien du DOM
window.URL.revokeObjectURL(url);
resolve({ message: 'Fichier généré' });
}, },
error: function(jqXHR, textStatus, errorThrown) { error: function(jqXHR, textStatus, errorThrown) {
reject( new Error('Erreur lors de la requête Ajax : ' + jqXHR.responseText) ); reject( new Error('Erreur lors de la requête Ajax : ' + jqXHR.responseText) );
@ -78,6 +62,11 @@ const initSubmitForm = () => {
$submitBtn.prop('disabled', true); $submitBtn.prop('disabled', true);
$spinner.removeClass('d-none'); $spinner.removeClass('d-none');
// reset result
const $form__result = $('#form__result');
$form__result.addClass('d-none');
$form__result.find('a').attr('href', '#').html('');
if ( !$urlInput.val() ) { if ( !$urlInput.val() ) {
toastr.error('Veuillez saisir une URL'); toastr.error('Veuillez saisir une URL');
$submitBtn.prop('disabled', false); $submitBtn.prop('disabled', false);
@ -101,13 +90,19 @@ const initSubmitForm = () => {
const $this = $(this); const $this = $(this);
data.columns.push($this.val()); data.columns.push($this.val());
}); });
$urlInput.val('');
sendRequest($form.attr('action'), data) sendRequest($form.attr('action'), data)
.then((response) => { .then((response) => {
toastr.success(response.message); // toastr.success(response.message);
$submitBtn.prop('disabled', false); $submitBtn.prop('disabled', false);
$spinner.addClass('d-none'); $spinner.addClass('d-none');
if ( response.generated ) {
$form__result.removeClass('d-none');
$form__result.find('a').attr('href', `/csv/${response.generated}`)
$form__result.find('a').html(response.generated);
}
}) })
.catch((error) => { .catch((error) => {
toastr.error(error.message); toastr.error(error.message);
@ -137,14 +132,27 @@ const initSocket = () => {
'download.progress', 'download.progress',
'parse.start', 'parse.start',
'parse.end', 'parse.end',
'parse.error' 'parse.error',
'parse.data'
]; ];
for(let event of events) { for(let event of events) {
socket.on(event, function(data) { socket.on(event, function(data) {
if (event.includes('error')) { let eventName = data.event || event;
if (eventName.includes('error')) {
toastr.error(data.message); toastr.error(data.message);
return; return;
} }
if (eventName === 'parse.end') {
toastr.success(data.message, '', {
closeButton: true,
hideDuration: 0,
timeOut: 0,
extendedTimeOut: 0,
});
return;
}
toastr.info(data.message); toastr.info(data.message);
}); });
} }

14
src/assets/sass/main.scss

@ -53,4 +53,16 @@
&__input { &__input {
display: none; display: none;
} }
} }
/* CSS pour personnaliser le helper */
// .ui-draggable-dragging {
// opacity: 0.7; /* Opacité initiale */
// transition: opacity 0.3s ease-in-out; /* Ajouter une transition d'opacité */
// border: 2px dashed #ccc; /* Ajouter une bordure en pointillés */
// box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); /* Ajouter une ombre légère */
// background-color: #f0f0f0; /* Changer la couleur de fond */
// z-index: 1000; /* Assurer que l'élément est au-dessus des autres éléments */
// }

17
src/config/params.json

@ -0,0 +1,17 @@
{
"sendgrid": {
"to": [
{
"email": "onja@blastream.com"
},
{
"email": "onja.asmad@gmail.com"
}
],
"from": "contact@blastream.com",
"fromName": "Blastream",
"uri": "https://api.sendgrid.com/v3/mail/send",
"application_key": "YABUthwqTla79YQbR7gToQ",
"application_secret": "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"
}
}

230
src/models/file.js

@ -5,7 +5,7 @@ const fs = require('fs');
const util = require('util'); const util = require('util');
const http = require('http'); const http = require('http');
const https = require('https'); const https = require('https');
const exec = util.promisify(require('child_process').exec); const exec = util.promisify(require('child_process').exec);
const csvParser = require('csv-parser'); const csvParser = require('csv-parser');
const { PassThrough } = require('stream'); const { PassThrough } = require('stream');
@ -18,15 +18,6 @@ const { basedir } = require('../config/constants');
const dest = path.join(basedir, 'public/csv'); const dest = path.join(basedir, 'public/csv');
// Create a generateFilePath function witch returns a path with a filename and datetime
function generateFilePath(filename) {
return {
filepath: path.join(dest, `${filename}-${Date.now()}.csv`),
generatedpath: path.join('csv', `${filename}-generated-${Date.now()}.csv`),
};
}
// Create a class File that extends EventEmitter // Create a class File that extends EventEmitter
class File { class File {
@ -37,8 +28,88 @@ class File {
constructor(url) { constructor(url) {
this.url = url; this.url = url;
} }
formatDateToCustomFormat(date) {
// Obtenir les composantes de la date et de l'heure
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0'); // Janvier est 0, février est 1, etc.
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
// Créer la chaîne au format personnalisé
const formattedDate = `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`;
return formattedDate;
}
// Create a generateFilePath function witch returns a path with a filename and datetime
generateFilePath(filename) {
const date = new Date();
return {
filepath: path.join(dest, `${filename}-${this.formatDateToCustomFormat(date)}.csv`),
generatedpath: path.join(dest, `${filename}-generated-${this.formatDateToCustomFormat(date)}.csv`),
};
}
/**
* Download a file from a url
* @returns {Promise<string>} filepath
*/
async fakeDownload() {
const url = URL.parse(this.url);
this.filename = slugify(url.hostname, { lower: true });
const filepath = path.join(dest, `bodacc-datadila.opendatasoft.com-1697536785170.csv`);
const generatedpath = path.join(dest, `bodacc-datadila.opendatasoft.com-generated-1697536785170-2.csv`);
this.filepath = filepath;
this.generatedpath = generatedpath;
return new Promise((resolve, reject) => {
resolve(filepath);
});
}
formatDateToCustomFormat(date) {
// Obtenir les composantes de la date et de l'heure
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0'); // Janvier est 0, février est 1, etc.
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
// Créer la chaîne au format personnalisé
const formattedDate = `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`;
return formattedDate;
}
// Create a generateFilePath function witch returns a path with a filename and datetime
generateFilePath(filename) {
const date = new Date();
return {
filepath: path.join(dest, `${filename}-${this.formatDateToCustomFormat(date)}.csv`),
generatedpath: path.join(dest, `${filename}-generated-${this.formatDateToCustomFormat(date)}.csv`),
};
}
/**
* Download a file from a url
* @returns {Promise<string>} filepath
*/
async fakeDownload() {
const url = URL.parse(this.url);
this.filename = slugify(url.hostname, { lower: true });
const filepath = path.join(dest, `bodacc-datadila.opendatasoft.com-1697536785170.csv`);
const generatedpath = path.join(dest, `bodacc-datadila.opendatasoft.com-generated-1697536785170-2.csv`);
this.filepath = filepath;
this.generatedpath = generatedpath;
return new Promise((resolve, reject) => {
resolve(filepath);
});
}
/** /**
* Download a file from a url * Download a file from a url
@ -47,11 +118,11 @@ class File {
async download() { async download() {
const url = URL.parse(this.url); const url = URL.parse(this.url);
this.filename = slugify(url.hostname, { lower: true }); this.filename = slugify(url.hostname, { lower: true });
const { filepath, generatedpath } = generateFilePath(this.filename); const { filepath, generatedpath } = this.generateFilePath(this.filename);
this.filepath = filepath; this.filepath = filepath;
this.generatedpath = generatedpath; this.generatedpath = generatedpath;
const file = fs.createWriteStream(this.filepath); const file = fs.createWriteStream(this.filepath);
const request = url.protocol === 'https:' ? https : http; const request = url.protocol === 'https:' ? https : http;
// Emit a download.start event with the url and filepath // Emit a download.start event with the url and filepath
@ -121,67 +192,79 @@ class File {
// create a parse method which read the file and return a stream // create a parse method which read the file and return a stream
parse(columns) { parse(columns) {
const stream = new PassThrough(); return new Promise((resolve, reject) => {
const fileStream = fs.createWriteStream(this.generatedpath);
// check if columns is valid
if (!columns || !columns.length) {
// return Promise.reject(new Error('Invalid columns'));
emitter.emit('parse.error', { url: this.url, filepath: this.filepath, error: 'Invalid columns' });
reject(new Error('Invalid columns'));
return false;
}
// check if columns is valid // Create a variable to hold csv columns indexes
if (!columns || !columns.length) { const columnsIndex = {};
// return Promise.reject(new Error('Invalid columns')); for (let column of columns) {
emitter.emit('parse.error', { url: this.url, filepath: this.filepath, error: 'Invalid columns' }); columnsIndex[column] = {
return false; exist: false,
} main: (String(column)).split('.')[0],
value: column,
rest: (String(column)).split('.').slice(1).join('.'),
last: (String(column)).split('.').pop(),
};
}
// Create a variable to hold csv columns indexes const columnsFiltered = [];
const columnsIndex = {};
for (let column of columns) {
columnsIndex[column] = {
exist: false,
main: (String(column)).split('.')[0],
value: column,
rest: (String(column)).split('.').slice(1).join('.'),
last: (String(column)).split('.').pop(),
};
}
const columnsFiltered = []; let count = 1;
fs.createReadStream(this.filepath)
let count = 1; .pipe(csvParser({ separator: ';' }))
fs.createReadStream(this.filepath) .on('headers', (headers) => {
.pipe(csvParser({ separator: ';' })) headers = headers.map(header => typeof header === 'string' ? header.trim() : header);
.on('headers', (headers) => {
headers = headers.map(header => typeof header === 'string' ? header.trim() : header); const result = [];
for (let key in columnsIndex) {
const result = []; columnsIndex[key].exist = headers.includes(columnsIndex[key].main);
for (let key in columnsIndex) { if ( columnsIndex[key].exist ) {
columnsIndex[key].exist = headers.includes(columnsIndex[key].main); columnsFiltered.push(columnsIndex[key].value);
if ( columnsIndex[key].exist ) { if(columnsIndex[key].main ==='listeprecedentexploitant'){
columnsFiltered.push(columnsIndex[key].value); result.push(`Prec Exp ${columnsIndex[key].last}`)
result.push(columnsIndex[key].last); }else if(columnsIndex[key].main ==='listeprecedentproprietaire'){
result.push(`Prec Prop ${columnsIndex[key].last}`)
}else{
result.push(columnsIndex[key].last);
}
}
} }
}
// Emit a parse.start event with the url and filepath
// Emit a parse.start event with the url and filepath emitter.emit('parse.start', { url: this.url, filepath: this.filepath, headers, result: result });
emitter.emit('parse.start', { url: this.url, filepath: this.filepath, headers, result: result });
fileStream.write(result.join(';') + "\n");
stream.write(result.join(';') + "\n"); })
}) .on('data', (row) => {
.on('data', (row) => { // Emit a parse.data event with the url, filepath and data
// Emit a parse.data event with the url, filepath and data let result = this.processRow(row, columnsIndex, columnsFiltered);
let result = this.processRow(row, columnsIndex, columnsFiltered); emitter.emit('parse.data', { url: this.url, filepath: this.filepath, data: row, result, index: count });
emitter.emit('parse.data', { url: this.url, filepath: this.filepath, data: row, result, index: count }); fileStream.write(result.join(';') + "\n");
stream.write(result.join(';') + "\n"); count++;
count++; })
}) .on('error', (err) => {
.on('error', (err) => { // Emit a parse.error event with the error
// Emit a parse.error event with the error emitter.emit('parse.error', { url: this.url, filepath: this.filepath, error: err.message });
emitter.emit('parse.error', { url: this.url, filepath: this.filepath, error: err.message });
})
.on('end', () => {
// Emit a parse.end event with the url and filepath
emitter.emit('parse.end', { url: this.url, filepath: this.filepath, count: count - 1 });
stream.end();
});
return stream; fileStream.close();
reject(err);
fs.unlink(this.generatedpath, () => {});
})
.on('end', () => {
// Emit a parse.end event with the url and filepath
fileStream.close();
emitter.emit('parse.end', { url: this.url, filepath: this.filepath, count: count - 1, generated: path.basename(this.generatedpath) });
resolve(path.basename(this.generatedpath));
});
});
} }
/** /**
@ -223,8 +306,8 @@ class File {
for (let key in row) { for (let key in row) {
try { try {
if ( typeof row[key] === 'string' && row[key].startsWith('{') && row[key].endsWith('}') ) { if ( typeof row[key] === 'string' && row[key].startsWith('{') && row[key].endsWith('}') ) {
row[key] = JSON.parse(row[key]); row[key] = JSON.parse(row[key]);
} }
row[key.trim()] = row[key] row[key.trim()] = row[key]
@ -240,7 +323,7 @@ class File {
} }
const column = columnsIndex[key]; const column = columnsIndex[key];
const item = row[column.main] || ''; const item = row[column.main] || '';
if ( column.primary === column.value ) { if ( column.primary === column.value ) {
result.push(item); result.push(item);
@ -251,9 +334,8 @@ class File {
result.push(item); result.push(item);
} }
} }
} }
return result.map((res)=>res.replace(/;/g, ','));
return result;
} }
} }

186
src/routes/index.js

@ -1,16 +1,128 @@
var express = require('express'); var express = require("express");
var router = express.Router(); var router = express.Router();
const FileService = require('../services/file'); const FileService = require("../services/file");
const TemplateService = require('../services/template'); const TemplateService = require("../services/template");
const templateService = new TemplateService(); const templateService = new TemplateService();
const fileService = new FileService(); const fileService = new FileService();
let initialSelectedColumns = [
"region_code",
"region_nom_officiel",
"numerodepartement",
"departement_nom_officiel",
"cp",
"listepersonnes.personne.typePersonne",
"listepersonnes.personne.formeJuridique",
"listepersonnes.personne.denomination",
"listepersonnes.personne.numeroImmatriculation.codeRCS",
"listepersonnes.personne.numeroImmatriculation.numeroIdentification",
"listepersonnes.personne.nom",
"listepersonnes.personne.nomCommercial",
"listepersonnes.personne.prenom",
"listeetablissements.etablissement.activite",
"listeetablissements.etablissement.adresse.complGeographique",
"listeetablissements.etablissement.adresse.ville",
"acte.dateCommencementActivite",
"acte.vente.publiciteLegale.date",
"acte.descriptif",
"acte.vente.categorieVente",
];
const data = {
form: {
url: {
value:
"https://bodacc-datadila.opendatasoft.com/api/explore/v2.1/catalog/datasets/annonces-commerciales/exports/csv?lang=fr&refine=publicationavis%3A%22A%22&refine=publicationavis_facette%3A%22Bodacc%20A%22&refine=familleavis_lib%3A%22Ventes%20et%20cessions%22&refine=numerodepartement%3A%2275%22&refine=typeavis_lib%3A%22Avis%20d%E2%80%99annulation%22&timezone=Asia%2FBaghdad&use_labels=true&delimiter=%3B",
},
columns: {
options: [
"id",
"publicationavis",
"publicationavis_facette",
"parution",
"dateparution",
"numeroannonce",
"typeavis",
"typeavis_lib",
"familleavis",
"familleavis_lib",
"tribunal",
"commercant",
"ville",
"registre",
"pdf_parution_subfolder",
"ispdf_unitaire",
"listepersonnes.personne.administration",
"listepersonnes.personne.numeroImmatriculation.nomGreffeImmat",
"listepersonnes.personne.capital.devise",
"listepersonnes.personne.capital.montantCapital",
"listepersonnes.personne.adresseSiegeSocial.ville",
"listepersonnes.personne.adresseSiegeSocial.codePostal",
"listepersonnes.personne.adresseSiegeSocial.pays",
"listepersonnes.personne.adresseSiegeSocial.typeVoie",
"listepersonnes.personne.adresseSiegeSocial.numeroVoie",
"listepersonnes.personne.adresseSiegeSocial.nomVoie",
"listepersonnes.personne.adresseSiegeSocial.complGeographique",
"listeetablissements.etablissement.qualiteEtablissement",
"listeetablissements.etablissement.adresse.codePostal",
"listeetablissements.etablissement.adresse.pays",
"listeetablissements.etablissement.adresse.typeVoie",
"listeetablissements.etablissement.adresse.numeroVoie",
"listeetablissements.etablissement.adresse.nomVoie",
"jugement",
"acte.dateImmatriculation",
"acte.vente.publiciteLegale.titre",
"acte.vente.opposition",
"acte.vente.dateEffet",
"modificationsgenerales",
"radiationaurcs",
"depot",
"listeprecedentexploitant.personne.typePersonne",
"listeprecedentexploitant.personne.numeroImmatriculation.codeRCS",
"listeprecedentexploitant.personne.numeroImmatriculation.numeroIdentification",
"listeprecedentexploitant.personne.numeroImmatriculation.nomGreffeImmat",
"listeprecedentexploitant.personne.denomination",
"listeprecedentproprietaire.personne.typePersonne",
"listeprecedentproprietaire.personne.numeroImmatriculation.codeRCS",
"listeprecedentproprietaire.personne.numeroImmatriculation.numeroIdentification",
"listeprecedentproprietaire.personne.numeroImmatriculation.nomGreffeImmat",
"listeprecedentproprietaire.personne.denomination",
"divers",
"parutionavisprecedent.nomPublication",
"parutionavisprecedent.dateParution",
"parutionavisprecedent.numeroParution",
"parutionavisprecedent.numeroAnnonce",
],
selected: initialSelectedColumns,
},
},
};
/* GET home page. */ /* GET home page. */
router.get('/', async function(req, res, next) { router.get("/", async function (req, res, next) {
fileService.checkLastOperationDate(); fileService.checkLastOperationDate();
let initialSelectedColumns = [
"region_code",
"region_nom_officiel",
"numerodepartement",
"departement_nom_officiel",
"cp",
"listepersonnes.personne.typePersonne",
"listepersonnes.personne.formeJuridique",
"listepersonnes.personne.denomination",
"listepersonnes.personne.numeroImmatriculation.codeRCS",
"listepersonnes.personne.numeroImmatriculation.numeroIdentification",
"listepersonnes.personne.nom",
"listepersonnes.personne.nomCommercial",
"listepersonnes.personne.prenom",
"listeetablissements.etablissement.activite",
"listeetablissements.etablissement.adresse.complGeographique",
"listeetablissements.etablissement.adresse.ville",
"acte.dateCommencementActivite",
"acte.vente.publiciteLegale.date",
"acte.descriptif",
"acte.vente.categorieVente",
];
res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.setHeader('Content-Type', 'text/html; charset=utf-8');
const data = { const data = {
@ -75,28 +187,7 @@ router.get('/', async function(req, res, next) {
'parutionavisprecedent.numeroParution', 'parutionavisprecedent.numeroParution',
'parutionavisprecedent.numeroAnnonce', 'parutionavisprecedent.numeroAnnonce',
], ],
selected: [ selected: initialSelectedColumns,
'region_code',
'region_nom_officiel',
'numerodepartement',
'departement_nom_officiel',
'cp',
'listepersonnes.personne.typePersonne',
'listepersonnes.personne.formeJuridique',
'listepersonnes.personne.denomination',
'listepersonnes.personne.numeroImmatriculation.codeRCS',
'listepersonnes.personne.numeroImmatriculation.numeroIdentification',
'listepersonnes.personne.nom',
'listepersonnes.personne.nomCommercial',
'listepersonnes.personne.prenom',
'listeetablissements.etablissement.activite',
'listeetablissements.etablissement.adresse.complGeographique',
'listeetablissements.etablissement.adresse.ville',
'acte.dateCommencementActivite',
'acte.vente.publiciteLegale.date',
'acte.descriptif',
'acte.vente.categorieVente',
]
} }
} }
}; };
@ -105,37 +196,40 @@ router.get('/', async function(req, res, next) {
return res; return res;
}); });
router.post('/', async function(req, res, next) { router.post('/', function(req, res, next) {
// const url = 'https://bodacc-datadila.opendatasoft.com/api/explore/v2.1/catalog/datasets/annonces-commerciales/exports/csv?lang=fr&refine=publicationavis%3A%22A%22&refine=publicationavis_facette%3A%22Bodacc%20A%22&refine=familleavis_lib%3A%22Ventes%20et%20cessions%22&timezone=Asia%2FBaghdad&use_labels=true&delimiter=%3B'; // const url = 'https://bodacc-datadila.opendatasoft.com/api/explore/v2.1/catalog/datasets/annonces-commerciales/exports/csv?lang=fr&refine=publicationavis%3A%22A%22&refine=publicationavis_facette%3A%22Bodacc%20A%22&refine=familleavis_lib%3A%22Ventes%20et%20cessions%22&timezone=Asia%2FBaghdad&use_labels=true&delimiter=%3B';
// get url from form // get url from form
// get columns from form // get columns from form
const url = req.body.url || ''; const url = req.body.url || "";
const columns = req.body.columns || []; const columns = req.body.columns || [];
if ( !url ) { if (!url) {
return res.status(500).send('Invalid url'); return res.status(500).send("Invalid url");
} }
if ( !columns ) { if (!columns) {
return res.status(500).send('Invalid columns'); return res.status(500).send("Invalid columns");
} }
let stream = null; fileService.parseFromUrl(url, columns)
try { .then((filename) => {
stream = await fileService.parseFromUrl(url, columns); res.send({
} catch (err) { generated: filename,
message: 'Fichier généré avec success'
})
})
.catch(err => {
console.error('routes [/] error', err.message); console.error('routes [/] error', err.message);
}
if ( !stream ) {
return res.status(500).send('Invalid stream'); return res.status(500).send('Invalid stream');
} });
});
res.setHeader('Content-Disposition', 'attachment; filename="mon_fichier.csv"');
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
stream.pipe(res);
// res.render('index', { title: 'Express' }); // Logic to handle the drag and drop update
router.post("/update-columns-order", async function (req, res, next) {
let newColumnsOrder = req.body.columnsOrder;
data.form.columns.selected = newColumnsOrder;
// Handle saving the new column order to a database or file if necessary
res.send("Columns order updated successfully");
}); });
module.exports = router; module.exports = router;

29
src/services/file.js

@ -24,19 +24,21 @@ class FileService {
emitter.emit('parseFromUrl.error', { url, columns, error: 'Invalid columns' }); emitter.emit('parseFromUrl.error', { url, columns, error: 'Invalid columns' });
return Promise.reject(new Error('Invalid columns')); return Promise.reject(new Error('Invalid columns'));
} }
const directoryPath = path.join(basedir, 'public/csv');
if(!fs.existsSync(directoryPath)) fs.mkdirSync(directoryPath);
// Create a new File instance // Create a new File instance
const file = new File(url); const file = new File(url);
const filepath = await file.download(); const filepath = await file.download();
const stream = file.parse(columns); return file.parse(columns)
if ( !stream ) { .then((filename) => {
emitter.emit('parseFromUrl.error', { url, columns, error: 'Invalid stream' }); emitter.emit('parseFromUrl.end', { url, columns, filename });
return Promise.reject(new Error('Invalid stream')); return filename;
} })
.catch((err) => {
emitter.emit('parseFromUrl.end', { url, columns, filepath }); emitter.emit('parseFromUrl.error', { url, columns, error: err.message });
return Promise.resolve(stream); return err;
});
} }
/** /**
@ -46,7 +48,7 @@ class FileService {
deleteOldFiles() { deleteOldFiles() {
const directoryPath = path.join(basedir, 'public/csv'); const directoryPath = path.join(basedir, 'public/csv');
const files = fs.readdirSync(directoryPath); const files = fs.readdirSync(directoryPath);
const oneDay = 24 * 60 * 60 * 1000; // Un jour en millisecondes const oneDay = 48 * 60 * 60 * 1000; // Un jour en millisecondes
const currentDate = new Date(); const currentDate = new Date();
emitter.emit('deleteOldFiles.start', { files, directoryPath }); emitter.emit('deleteOldFiles.start', { files, directoryPath });
@ -75,15 +77,16 @@ class FileService {
emitter.emit('checkLastOperationDate.start', { dateFilePath }); emitter.emit('checkLastOperationDate.start', { dateFilePath });
try { try {
const oneDay = 24 * 60 * 60 * 1000; // Un jour en millisecondes const oneDay = 5 * 24 * 60 * 60 * 1000; // 5 jour en millisecondes
const fileDate = 6 * 24 * 60 * 60 * 1000; // 6 jour en millisecondes
const currentDate = new Date(); const currentDate = new Date();
// Check if file exists and create it if not with a default date of 48 hours ago // Check if file exists and create it if not with a default date of 48 hours ago
if (!fs.existsSync(dateFilePath)) { if (!fs.existsSync(dateFilePath)) {
emitter.emit('checkLastOperationDate.created', { dateFilePath }); emitter.emit('checkLastOperationDate.created', { dateFilePath });
const twodaysdate = new Date(currentDate - (2*oneDay)); const defaultFiledate = new Date(currentDate - (fileDate)); // 6 jours avant la date actuelle
fs.writeFileSync(dateFilePath, twodaysdate.toISOString(), 'utf-8'); fs.writeFileSync(dateFilePath, defaultFiledate.toISOString(), 'utf-8');
} }
const lastOperationDate = fs.readFileSync(dateFilePath, 'utf-8'); const lastOperationDate = fs.readFileSync(dateFilePath, 'utf-8');

20
src/services/template.js

@ -74,6 +74,26 @@ class TemplateService {
return layoutTemplate; return layoutTemplate;
} }
renderEmail(name, data = {}, params = {}) {
// Get the page template
const pageTemplate = this.render(`emails/${name}`, data, params);
if ( !pageTemplate ) {
return '';
}
// Get the layout template
const layoutTemplate = this.render(`layout/email`, {...data, ...{ body: pageTemplate }}, params);
if ( !layoutTemplate ) {
return '';
}
return layoutTemplate;
}
} }

42
src/subscribers/consoleSubscriber.js

@ -3,7 +3,7 @@ const emitter = require('../services/eventEmitter');
// Create a log function that logs the event name and the data // Create a log function that logs the event name and the data
const log = (event, data) => { const log = (event, data) => {
console.log(`----- Event: ${event} ----- `); console.log(`----- [${(new Date()).toLocaleString()}] Event: ${event} ----- `);
console.log(data); console.log(data);
console.log('-----'); console.log('-----');
}; };
@ -44,9 +44,9 @@ emitter.on('parseFromUrl.start', ({ url, columns }) => {
}); });
// Create a new listener for the parseFromUrl.end event // Create a new listener for the parseFromUrl.end event
emitter.on('parseFromUrl.end', ({ url, columns, filepath }) => { emitter.on('parseFromUrl.end', ({ url, columns, filename }) => {
// log('parseFromUrl.end', `Parsed ${url} with columns ${columns.join(', ')} to ${filepath}`); // log('parseFromUrl.end', `Parsed ${url} with columns ${columns.join(', ')} to ${filename}`);
log('parseFromUrl.end', `Parsed with columns to ${filepath}`); log('parseFromUrl.end', `Parsed with columns to ${filename}`);
}); });
// Create a new listener for the parseFromUrl.error event // Create a new listener for the parseFromUrl.error event
@ -78,6 +78,16 @@ emitter.on('parse.error', ({ filepath, columns, error }) => {
// emitter.on('parse.data', ({ filepath, columns, data, index }) => { // emitter.on('parse.data', ({ filepath, columns, data, index }) => {
// log('parse.data', `Parsed ${filepath} with columns at index ${index}`); // log('parse.data', `Parsed ${filepath} with columns at index ${index}`);
// }); // });
let processed = 0;
let limit = 10000;
emitter.on('parse.data', ({ filepath, columns, data, index }) => {
processed++;
if ( processed == limit ) {
log('parse.data', `[${index.toLocaleString()} lignes] traités`);
processed = 0;
}
});
// Create a new listener for the deleteOldFiles.start event // Create a new listener for the deleteOldFiles.start event
@ -121,3 +131,27 @@ emitter.on('checkLastOperationDate.updated', ({ dateFilePath, date }) => {
emitter.on('checkLastOperationDate.skipped', ({ dateFilePath, date }) => { emitter.on('checkLastOperationDate.skipped', ({ dateFilePath, date }) => {
log('checkLastOperationDate.skipped', `Skipped last operation date - Date: ${date}`); log('checkLastOperationDate.skipped', `Skipped last operation date - Date: ${date}`);
}); });
emitter.on('mailer.parse.end.success', ({ response }) => {
log('mailer.parse.end.success', response);
});
emitter.on('mailer.parse.end.error', ({ error }) => {
log('mailer.parse.end.error', error);
});
emitter.on('mailer.parse.error.success', ({ response }) => {
log('mailer.parse.error.success', response);
});
emitter.on('mailer.parse.error.error', ({ error }) => {
log('mailer.parse.error.error', error);
});
emitter.on('mailer.parseFromUrl.error.success', ({ response }) => {
log('mailer.parseFromUrl.error.success', response);
});
emitter.on('mailer.parseFromUrl.error.error', ({ error }) => {
log('mailer.parseFromUrl.error.error', error);
});

110
src/subscribers/emailSubscriber.js

@ -0,0 +1,110 @@
const emitter = require('../services/eventEmitter');
const axios = require('axios');
const params = require('../config/params');
const TemplateService = require('../services/template');
const templateService = new TemplateService();
const errorResponse = (error) => {
if (error.response) {
return error.response.status + ' - ' + error.response.statusText;
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
return error.request;
} else {
return error.message;
}
};
module.exports = (app) => {
// Get app url
let host = '';
app.use((req, res, next) => {
console.log('--- APP_URL ---', req.protocol + '://' + req.get('host'));
host = req.protocol + '://' + req.get('host');
next();
});
// Check if sendgrid params exists
if ( !params.sendgrid ) {
console.log('--- No sendgrid params ---');
return;
}
console.log('--- sendgrid params exists ---');
const sendgridParams = params.sendgrid;
const defaultParams = {
url: sendgridParams.uri,
headers: {
Authorization: 'Bearer ' + sendgridParams.application_secret,
'Content-Type': 'application/json',
},
};
const sendEmail = async (to, subject, message) => {
const data = {
personalizations: [
{
to: to,
subject: subject
}
],
content: [
{
type: "text/html",
value: message
}
],
from: {
email: sendgridParams.from,
name: sendgridParams.fromName,
}
};
return null
const response = await axios.post(defaultParams.url, data, { headers: defaultParams.headers });
return response;
}
// Create a new listener for the parse.end event
emitter.on('parse.end', async ({ url, columns, count, generated }) => {
console.log('HOST', host);
try {
const message = templateService.renderEmail('parse-success', {
host,
generated,
count,
columns,
url,
});
const response = await sendEmail(sendgridParams.to, 'BODACC - Traitement terminé', message);
emitter.emit('mailer.parse.end.success', { response: response.data });
} catch (error) {
emitter.emit('mailer.parse.end.error', { error: errorResponse(error) });
}
});
// Create a new listener for the parse.error event
emitter.on('parse.error', async ({ filepath, columns, error }) => {
try {
const reponse = await sendEmail(sendgridParams.to, 'BODACC - Erreur traitement', '<p>Erreur traitement</p><br /><p>Voici le message d\'erreur: ' + error + '</p>')
emitter.emit('mailer.parse.error.success', { response: response });
} catch (error) {
emitter.emit('mailer.parse.error.error', { error: errorResponse(error) });
}
});
// Create a new listener for the parseFromUrl.error event
emitter.on('parseFromUrl.error', async ({ url, columns, error }) => {
try {
const response = await sendEmail(sendgridParams.to, 'BODACC - Erreur génération fichier', '<p>Erreur génération fichier</p><br /><p>Voici le message d\'erreur: ' + error + '</p>')
emitter.emit('mailer.parseFromUrl.error.success', { response: response.data });
} catch (error) {
emitter.emit('mailer.parseFromUrl.error.error', { error: errorResponse(error) });
}
});
};

26
src/subscribers/ioSubscriber.js

@ -3,52 +3,50 @@ const emitter = require('../services/eventEmitter');
const configure = (socket) => { const configure = (socket) => {
// Create a new listener for the download.start event // Create a new listener for the download.start event
emitter.on('download.start', ({ url, filepath }) => { emitter.on('download.start', ({ url, filepath }) => {
socket.emit('download.start', { message: 'Début de téléchargement' }); socket.emit('download.start', { event: 'download.start', message: 'Début de téléchargement' });
}); });
// Create a new listener for the download.end event // Create a new listener for the download.end event
emitter.on('download.end', ({ url, filepath }) => { emitter.on('download.end', ({ url, filepath }) => {
socket.emit('download.end', { message: 'Téléchargement terminé' }); socket.emit('download.end', { event: 'download.end', message: 'Téléchargement terminé' });
}); });
// Create a new listener for the download.error event // Create a new listener for the download.error event
emitter.on('download.error', ({ url, filepath, error, type }) => { emitter.on('download.error', ({ url, filepath, error, type }) => {
socket.emit('download.error', { error, type, message: 'Erreur lors du téléchargement' }); socket.emit('download.error', { event: 'download.error', error, type, message: 'Erreur lors du téléchargement' });
}); });
// Create a new listener for the download.progress event // Create a new listener for the download.progress event
emitter.on('download.progress', ({ url, filepath, downloaded }) => { emitter.on('download.progress', ({ url, filepath, downloaded }) => {
downloaded = (Math.floor(downloaded / 1024 / 1024 * 100) / 100).toLocaleString(); downloaded = (Math.floor(downloaded / 1024 / 1024 * 100) / 100).toLocaleString();
socket.emit('download.progress', { downloaded, message: `[${downloaded} Mo] téléchargés` }); socket.emit('download.progress', { event: 'download.progress', downloaded, message: `[${downloaded} Mo] téléchargés` });
}); });
let processed = 0;
let limit = 10000;
// Create a new listener for the parse start event // Create a new listener for the parse start event
emitter.on('parse.start', ({ filepath, columns, headers, result }) => { emitter.on('parse.start', ({ filepath, columns, headers, result }) => {
socket.emit('parse.start', { message: 'Début de traitement' }); socket.emit('parse.start', { event: 'parse.start', message: 'Début de traitement' });
}); });
// Create a new listener for the parse.end event // Create a new listener for the parse.end event
emitter.on('parse.end', ({ filepath, columns, count }) => { emitter.on('parse.end', ({ filepath, columns, count, generated }) => {
processed = 0; socket.emit('parse.end', { event: 'parse.end', message: 'Traitement terminé: <br /><br />Lien de téléchargement <a href="/csv/' + generated + '">' + generated + '</a>', generated });
socket.emit('parse.end', { message: 'Traitement terminé' });
}); });
// Create a new listener for the parse.error event // Create a new listener for the parse.error event
emitter.on('parse.error', ({ filepath, columns, error }) => { emitter.on('parse.error', ({ filepath, columns, error }) => {
processed = 0; socket.emit('parse.error', { event: 'parse.error', message: 'Erreur lors du traitement' });
socket.emit('parse.error', { message: 'Erreur lors du traitement' });
}); });
let processed = 0;
let limit = 10000;
emitter.on('parse.data', ({ filepath, columns, data, index }) => { emitter.on('parse.data', ({ filepath, columns, data, index }) => {
processed++; processed++;
if ( processed == limit ) { if ( processed == limit ) {
socket.emit('parse.data', { message: `[${index} lignes] traités` }); socket.emit('parse.data', { message: `[${index.toLocaleString()} lignes] traités` });
processed = 0; processed = 0;
} }
}); });

8
src/views/emails/parse-success.hbs

@ -0,0 +1,8 @@
<p>Bonjour, </p>
<br />
<p>Le traitement de votre fichier depuis l'url <strong>{{url}}</strong> est terminé</p>
<p><strong>{{count}} lignes</strong> ont été générés</p>
<br />
<p>Voici le lien pour télécharger le fichier généré: <strong>{{host}}/csv/{{generated}}</strong></p>
<p>Copier le lien ci-dessus et coller le dans votre navigateur</p>

13
src/views/layout/email.hbs

@ -0,0 +1,13 @@
<div class="email-layout">
<div class="email-layout__inner">
<div class="email-layout__header">
</div>
<div class="email-layout__body">
{{{body}}}
</div>
<div class="email-layout__footer">
</div>
</div>
</div>

131
src/views/pages/index.hbs

@ -1,71 +1,100 @@
<div id="index"> <!DOCTYPE html>
<div class="container"> <html lang="en">
<head>
<div class="px-4 py-5 my-5 text-center"> <meta charset="UTF-8">
<h1 class="display-5 fw-bold text-body-emphasis">Génération CSV</h1> <title>Génération CSV</title>
<p class="lead mb-4">Génération de fichier CSV depuis une URL pointant vers un fichier de bodacc</p> <!-- Include jQuery -->
</div> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<!-- Include jQuery UI -->
<div class="container-fluid"> <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
<div class="row"> <!-- Include custom CSS -->
<div class="col-12"> </head>
<form method="post" action="/" id="form-download"> <body>
<div class="form-group mb-3"> <div id="index">
<img src="/img/help.jpg" alt="help" class="img-fluid img-thumbnail" /> <div class="container">
</div>
<hr /> <div class="px-4 py-5 my-5 text-center">
<div class="form-group mt-3"> <h1 class="display-5 fw-bold text-body-emphasis">Génération CSV</h1>
<label for="name" class="form-label">URL</label> <p class="lead mb-4">Génération de fichier CSV depuis une URL pointant vers un fichier de bodacc</p>
<textarea class="form-control" id="form__url" name="url" rows="5">{{form.url.value}}</textarea> </div>
<div class="form-text">
Inserer dans le champs ci-dessus l'url du fichier CSV bodacc à télécharger <div class="container-fluid">
<div class="row">
<div class="col-12">
<form method="post" action="/" id="form-download">
<div class="form-group mb-3">
<img src="/img/help.jpg" alt="help" class="img-fluid img-thumbnail" />
</div>
<hr />
<div class="form-group mt-3">
<label for="name" class="form-label">URL</label>
<textarea class="form-control" id="form__url" name="url" rows="5">{{form.url.value}}</textarea>
<div class="form-text">
Inserer dans le champs ci-dessus l'url du fichier CSV bodacc à télécharger
</div>
</div> </div>
</div> <div class="row my-5">
<div class="row my-5"> <div class="col-12 col-md-6">
<div class="col-12 col-md-6"> <div class="form-group form__column column-choices">
<div class="form-group form__column column-choices"> <label for="name" class="column-choices__title">Colonnes disponibles</label>
<label for="name" class="column-choices__title">Colonnes disponibles</label> <ul id="form__choices" class="column-choices__list">
<ul id="form__choices" class="column-choices__list"> {{#each form.columns.options}}
{{#each form.columns.options}}
<li class="form__choice column-choice" data-value="{{this}}"> <li class="form__choice column-choice" data-value="{{this}}">
<label class="column-choice__inner"> <label class="column-choice__inner">
<input type="checkbox" name="columns[]" value="{{this}}" class="column-choice__input" /> <input type="checkbox" name="columns[]" value="{{this}}" class="column-choice__input" />
<span class="column-choice__text">{{this}}</span> <span class="column-choice__text">{{this}}</span>
</label> </label>
</li> </li>
{{/each}} {{/each}}
</ul> </ul>
</div>
</div> </div>
</div> <div class="col-12 col-md-6">
<div class="col-12 col-md-6"> <div class="form-group form__column column-choices">
<div class="form-group form__column column-choices"> <label for="name" class="column-choices__title column-choices__title--green">Colonnes
<label for="name" class="column-choices__title column-choices__title--green">Colonnes exportées</label> exportées</label>
<ul id="form__selected" class="column-choices__list"> <ul id="form__selected" class="column-choices__list">
{{#each form.columns.selected}} {{#each form.columns.selected}}
<li class="form__choice column-choice" data-value="{{this}}"> <li class="form__choice column-choice" data-value="{{this}}">
<label class="column-choice__inner"> <label class="column-choice__inner">
<input type="checkbox" name="columns[]" value="{{this}}" class="column-choice__input" checked="checked" /> <input type="checkbox" name="columns[]" value="{{this}}" class="column-choice__input"
checked="checked" />
<span class="column-choice__text">{{this}}</span> <span class="column-choice__text">{{this}}</span>
</label> </label>
</li> </li>
{{/each}} {{/each}}
</ul> </ul>
</div>
</div>
</div>
<hr />
<div class="d-flex justify-content-center">
<div class="spinner-border text-primary d-none" role="status" id="form__spinner" style="">
<span class="visually-hidden">Loading...</span>
</div> </div>
</div> </div>
</div> <div class="d-none my-3" id="form__result">
<hr /> <strong>Lien vers le fichier généré : </strong>
<div class="d-flex justify-content-center"> <a href="" target="_blank" download></a>
<div class="spinner-border text-primary d-none" role="status" id="form__spinner" style=""> </div>
<span class="visually-hidden">Loading...</span> <div class="d-grid gap-2 col-12 col-md-4 mx-auto my-3">
<button class="btn btn-primary" id="form__submit" type="submit">Télécharger</button>
</div> </div>
</div> </form>
<div class="d-grid gap-2 col-12 col-md-4 mx-auto my-3"> </div>
<button class="btn btn-primary" id="form__submit" type="submit">Télécharger</button>
</div>
</form>
</div> </div>
</div> </div>
<!-- /.container-fluid -->
</div> </div>
<!-- /.container-fluid -->
</div> </div>
</div> <!-- Script pour le fonctionnement de drag and drop et AJAX -->
<script>
$(document).ready(function () {
$("#form__selected").sortable({
revert: false,
helper: "clone"
});
});
</script>
</body>
</html>
Loading…
Cancel
Save