# Esercizio
Sito web di raccolta dati meteo
- [ ] Inserimento e salvataggio dati
- [ ] Città
- [ ] Condizioni meteo
- [ ] Temperatura
- [ ] DateTime
- [ ] Commento
- [ ] Ricerca dati inseriti
- [ ] Città
- [ ] Intervallo di tempo
- [ ] Visione a tabella dei record che corrispondono alla ricerca
- [ ] Vista home con gli ultimi inserimenti
- [ ] Vista di dettaglio dell'inserimento con i dati completi
## Creazione del server base
1. Creazione cartella e `npm init`
2. nodemon e script in package.json
3. creazione index.js e app.js
4. test di una chiamata alla home per verificare il funzionamento
### 1- npm init
Serve ad inizializzare il package.json. File che conterrà tutti gli script e le dipendenze (librerie) del progetto.
```bash
npm init
```
### 2- nodemon e live reload
Nodemon è una utility che avvia il programma nodejs tenendo monitorati i file del progetto, ogni volta che un file cambia riavvia automaticamente il processo. Serve ad ad avere sempre in esecuzione le ultime modifiche salvate.
```bash
npm install --save-dev nodemon
```
aggiungere nel file package.json lo script `dev`
```json=
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
```
### 3 index e app.js
`Express` è la libreria che si occupa di gestire il server web.
```bash
npm install --save express
```
Il file `app.js` contiene la configurazione del server `express`, si occupa di registrare i middleware comuni a tutta l'applicazione (es: body-parser, log, autenticazione, ecc...)
Il file instanzia l'applicazione express `const app = express()`, la configura e la esporta.
Creare il file `app.js` nella cartella src
```javascript
const express = require('express');
const app = express();
module.exports = app;
```
Il file `index.js` si occupa solo di avviare l'applicazione. Importa l'app dal file `app.js` e chiama il metodo `listen`.
Creare il file `index.js` nella root del progetto
```javascript
const app = require('./src/app');
app.listen(3000, () => {
console.log('App listening on port 3000');
});
```
Avviare l'applicazione con:
```bash
npm run dev
```
### 4 - aggiungere una chiamata di test (poi va rimossa)
E' sempre buona norma procedere per piccoli passi e testare il funzionamento.
Per verificare che il server funzioni aggiungere nel file `app.js` un middleware che risponde a qualsiasi chiamata con una risposta di conferma.
```javascript=
app.use((req, res) => {
res.send('funziono!');
})
```
Aprire il browser all'indirizzo [http://localhost:3000](http://localhost:3000) e verificare che compaia il testo corretto.
Una volta eseguito il test rimuovere il middleware.
## Suddivisione in sottoproblemi
Una volta avviato un server funzionante si procede con l'implementazione del codice relativo al progetto in esame.
Prima di procedere è sempre bene suddividere il problema in parti più piccole, stabilire l'ordine in cui affrontarle e i test intermedi da fare.
Sottoproblemi:
* Servono 3 pagine
* Inserimento
* Visualizzazione ultimi risultati
* Visualizzazione risultati filtrati (stessa pagina del punto precedente)
* Dettaglio di un record
* Abbiamo 1 tipo di dato: entry del meteo, lo chiamiamo `record`
* E' necessario avere un sistema di filtri
Ordine di implementazione
1. creare le pagine, anche vuote, con i relativi router e configurare l'indizzamento
1. Creazione e configurazione del router generico e di quello specifico per i record
2. Creazione del controller e dei metodi necessari a gestire le varie pagine
3. Verifica che navigando i vari url venga utilizzato il metodo previsto nel controller
4. Creazione dei vari template per le pagine
5. Test che per ogni url venga renderizzato il template corretto
2. Visualizzazione del dettaglio di un record con dati di mock
1. definire la struttura del dato del record
2. creare un oggetto direttamente in javascript da passare alla view
3. verificare che la view mostri correttamente i dati di prova
3. Gestire la pagina di inserimento
1. Creare il form nella view della pagina
2. Creare la route per gestire la chiamata `POST`
3. Configurare il server per accettare il body della richiesta
4. Verificare che i dati arrivino correttamente e siano leggibili
4. Scrittura dello schema e del model
1. Scrivere il file `record.schema.js` seguendo la struttura dei dati definita in precedenza
2. Scrivere il file `record.model.js` e il suo metodo `create`
5. Attaccare la pagina di inserimento al model
6. Attaccare la pagina di dettaglio al model
1. Aggiungere il metodo `getById` al model
2. Modificare il controller in modo che vada ad utilizzare il metodo `getById` del model
7. Home: visualizzare tutti i record
1. Implementare il metodo `find` nel model
2. Visualizzare tutta la lista dei record inseriti
8. Home: visualizzare i record filtrati
1. Definizione dei filtri previsti
2. Implementazione del form dei filtri nella view della lista
3. Modifica del metodo `find` del model perchè accetti e utilizzi i filtri
4. Modifica del controller perchè accetti i filtri e chiami il metodo `find` del model nel modo corretto
9. Home: visualizzare gli ultimi record inseriti se non sono applicati filtri
1. Modifica del controller perchè applichi dei filtri predefiniti nel caso in cui non ne arrivino dalla richiesta
### 1 - Creazione delle pagine
1. Creazione e configurazione del router generico e di quello specifico per i record
2. Creazione del controller e dei metodi necessari a gestire le varie pagine
3. Verifica che navigando i vari url venga utilizzato il metodo previsto nel controller
4. Creazione dei vari template per le pagine
5. Test che per ogni url venga renderizzato il template corretto
#### 1.1 Configurazione router
I router sono i componenti che si occupano di gestire in modo gerarchico quale funzione andrà a gestire un determinato url.
Creare il file `router.js` nella cartella pages:
```javascript
const express = require('express');
const router = express.Router();
module.exports = router;
```
Modificare `app.js` in modo da registrare il router creato:
```javascript
//in alto vicino agli altri require:
const router = require('./pages/router');
//dopo la configurazione dei middleware generici:
app.use(router);
```
Creare il file `record.router.js` nella cartella `pages/record`.
```javascript
const express = require('express');
const router = express.Router();
module.exports = router;
```
Modificare il file `router.js` creato in precedenza per fare in modo che tutte le richieste che iniziano con `/record` vengano gestite dal `record.router`:
```javascript
const express = require('express');
const router = express.Router();
const recordRouter = require('./record/record.router');
router.use('/records', recordRouter);
router.get('/', (req, res) => {
res.redirect('/records');
})
module.exports = router;
```
#### 1.2 Creare record.controller
Il controller esporta le funzioni che andranno a gestire le varie chiamate. In genere ogni funzione che viene esportata dal controller viene registrata nel router per gestire un tipo di url.
Creare il file `record.controller.js` nella cartella `pages/record` e predisporre un metodo per ogni pagina che si andrà a gestire:
``` javascript
module.exports.list = async (req, res, next) => {
res.send('List Page');
}
module.exports.add = (req, res, next) => {
res.send('Add Page');
}
module.exports.details = async (req, res, next) => {
res.send('Detail Page');
}
```
Configurare `record.router.js` con le varie route e i corrispettivi metodi del controller:
```javascript
const express = require('express');
const router = express.Router();
const recordController = require('./record.controller');
router.get('/', recordController.list);
router.get('/add', recordController.add);
router.get('/:id', recordController.details);
module.exports = router;
```
#### 1.3 Verifica del corretto indirizzamento
Aprire il browser e navigare nelle varie pagine previste dai router, verificando che la risposta corrisponda a quella prevista nel controller.
#### 1.4 Creazione dei template di esempio per le varie pagine
Per renderizzare i template viene usata la libreria `ejs`:
```bash
npm i --save ejs
```
Configurare express perchè utilizzi eja come `view engine`, nel file `app.js`:
```javascript
// prima della registrazione del router
app.set('view engine', 'ejs');
app.set('views', './src/templates'); // dice a express in che cartella cercare i template
```
Creazione dei file template nella cartella `src/templates`: `add.ejs`, `details.ejs`, `list.ejs`.
Ogni template per il momento è una pagina html con dentro solo il nome della pagina visualizzata, ci serve solo per verificare che tutto funzioni.
```html
<html>
<head>
<title>Add</title>
</head>
<body>
Add page
</body>
</html>
```
Configurare `record.controller` perchè renderizzi la pagina corretta ad ogni richiesta:
```javascript
module.exports.list = async (req, res, next) => {
res.render('list.ejs')
}
module.exports.add = (req, res, next) => {
res.render('add.ejs')
}
module.exports.details = async (req, res, next) => {
res.render('details.ejs')
}
```
#### 1.5 Verifica del corretto funzionamento
Aprire il browser e navigare nelle varie pagine previste dai router, verificando che ogni pagina contenga il testo previsto nel template corretto.
### 2 - Pagina di dettaglio con dati di mock
1. definire la struttura del dato del record
2. creare un oggetto direttamente in javascript da passare alla view
3. verificare che la view mostri correttamente i dati di prova
#### 2.1 Definire la struttura del dato del record
Ragionare su che dati ci serve avere nella nostra entità `record`, ci serve:
- data (data a cui corrispondono le rilevazioni)
- city (luogo di pertinenza)
- conditions (sole, pioggia, nuvolo, nebbia, neve)
- temperature (temperature in °C)
- note (eventuali annotazioni)
#### 2.2 Creare un oggetto di mock da passare alla view
Prima di gestire il salvataggio e il model è utile verificare il funzionamento e la visualizzazione del dato usanto dati di prova (mock).
Definire un oggetto e passarlo al template di `details`, nel file `record.controller.js`:
```javascript
module.exports.details = async (req, res, next) => {
const record = {
date: new Date(),
city: 'Vicenza',
temperature: 21,
conditions: 'sole',
note: 'nota di prova'
};
res.render('details.ejs', {record: record})
}
```
Modificare il template in modo da mostrare i dati, nel file `details.ejs`:
```html
<html>
<head>
<title>Detail</title>
</head>
<body>
Data: <%= record.date %> <br>
Condizioni: <%= record.conditions %> <br>
Temperatura: <%= record.temperature %>°C <br>
Città: <%= record.city %> <br>
Nota: <%= record.note %>
</body>
</html>
```
#### 2.3 Verificare che la view mostri correttamente i dati di prova
Aprire la pagina di dettaglio dal browser e verificare che i dati vengano mostrati correttamente
### 3 Gestire la pagina di inserimento
1. Creare il form nella view della pagina
2. Creare la route per gestire la chiamata `POST`
3. Configurare il server per accettare il body della richiesta
4. Verificare che i dati arrivino correttamente e siano leggibili
#### 3.1 Creare il form nella pagina add
I dati verranno mandati tramite un form html, creare il form nel template `add.ejs`.
`action` e `method` nel tag `form` corrispondono all'url e al method con cui il form invierà i dati al server.
```html
<html>
<head>
<title>Add</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css" integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-Piv4xVNRyMGpqkS2by6br4gNJ7DXjqk09RmUpJ8jgGtD7zP9yug3goQfGII0yAns" crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<h1>Add Record</h1>
<form action="/records/add" method="POST">
<div class="form-group">
<label for="city">Data</label>
<input type="datetime-local" class="form-control" name="date" >
</div>
<div class="form-group">
<label for="city">Città</label>
<input type="text" class="form-control" name="city" >
</div>
<div class="form-group">
<label for="temperature">Temperatura</label>
<input type="number" class="form-control" name="temperature" >
</div>
<div class="form-group">
<label>Condizioni</label>
<select class="custom-select" name="conditions">
<option selected>Choose...</option>
<option value="sole">Sole</option>
<option value="pioggia">Pioggia</option>
<option value="nuvolo">nuvolo</option>
<option value="nebbia">nebbia</option>
<option value="neve">neve</option>
</select>
</div>
<div class="form-group">
<label for="note">Nota</label>
<input type="text" class="form-control" name="note" >
</div>
<button type="submit" class="btn btn-primary">Aggiungi</button>
</form>
</div>
</body>
</html>
```
#### 3.2 Creare la route per gestire la chiamata `POST`
La richiesta che viene fatta dal form è di tipo `POST` all'url `records/add`, attualmente il nostro server non è configurato per gestire tale richiesta.
Nel file `record.controller.js` creare il metodo che gestirà la chiamata di aggiunta:
```javascript
module.exports.create = async (req, res, next) => {
console.log(req.body);
const newId = 'test';
res.redirect(`records/${newId}`);
}
```
Nel file `record.router.js` configurare la chiamata post perchè venga gestita dal metodo creato:
```javascript
router.post('/add', recordController.create);
```
#### 3.3 Configurare il server per accettare il body della richiesta
Attualmente facendo il submit del form il log che abbiamo messo nel controller non mostra correttamente i dati, questo perchè non abbiamo detto al server come interpretarli.
Nel caso di form HTML il formato con cui vengono inviati i dati di una chiamata post è `x-www-form-urlencoded`, per abilitare express a leggere questo formato è necessario installare la libreria `body-parser`:
```bash
npm i --save body-parser
```
E configuare il server nel file `app.js`:
```javascript
// prima della registrazione del router
app.use(express.urlencoded());
```
#### 3.4 Verificare che i dati arrivino correttamente e siano leggibili
Facendo il submit del form il console log dovrebbe stampare correttamente l'oggetto con i dati inviati.
### 4 Scrittura dello schema e del model
1. Scrivere il file `record.schema.js` seguendo la struttura dei dati definita in precedenza
2. Scrivere il file `record.model.js` e il suo metodo `create`
#### 4.1 Creazione dello schema
Mongoose è la libreria che utilizziamo per interagire con un database `mongodb`:
```bash
npm i --save mongoose
```
Per connettersi al database è necessario chiamare il metodo `connect` nel file `app.js`:
```javascript
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/its_meteo', { useNewUrlParser: true, useUnifiedTopology: true });
```
La libreria crea automaticamente un database chiamato `its_meteo` quando necessario.
Lo `schema` è l'entità che usa mongoose per definire la struttura di un dato che può essere scritto a db.
Creare il file `record.schema.js` nella cartella `record`:
```javascript
const mongoose = require('mongoose');
const RecordSchema = mongoose.Schema({
date: {type: Date},
city: String,
conditions: {type: String, enum: ['sole', 'pioggia', 'nuvolo', 'nebbia', 'neve']},
temperature: Number,
note: String
});
module.exports = mongoose.model('Record', RecordSchema);
```
#### 4.2 Scrivere il file `record.model.js` e il suo metodo `create`
Il model è l'entità che verrà richiamata dal controller per manipolare i dati (lettura o scrittura). Importa lo schema e viene importato dal controller.
Creare il file `record.model.js` che esporti il metodo create:
```javascript
const Record = require('./record.schema');
module.exports.create = async (data) => {
const newRecord = new Record(data);
return newRecord.save();
// return Record.create(data);
};
```
### 5 Attaccare la logica della pagina di create al model
Modificare `record.controller.js` in modo che utilizzi il model creato:
```javascript
const recordModel = require('./record.model');
module.exports.create = async (req, res, next) => {
try {
const newRecord = await recordModel.create(req.body);
res.redirect(`/records/${newRecord.id}`);
} catch(err) {
console.log(err);
res.status(500);
res.send();
}
}
```
Verificare che i dati siano correttamente scritti a database.
### 6 Attaccare la pagina di dettaglio al model
1. Aggiungere il metodo getById al model
2. Modificare il controller in modo che vada ad utilizzare il metodo getById del model
#### 6.1 Aggiungere `getById` al model
Nel file `record.model.js`:
```javascript
module.exports.getById = async (id) => {
return Record.findById(id);
}
```
#### 6.2 Modificare il controller
In `record.router.js` l'url è definito come `/:id`, questo permette di accedere al parametro `id` richiesto accedendo alla variabile `req.params.id` nel controller.
Nel file `record.controller.js`:
```javascript=
module.exports.details = async (req, res, next) => {
try {
const record = await recordModel.getById(req.params.id);
if (!record) {
res.status(404);
res.send('Not Found');
return;
}
res.render('details.ejs', {record: record});
} catch(err) {
console.log(err);
res.status(500);
res.send();
}
}
```
Viene controllato se il record richiesto esiste (tramite l'`if`) e in caso contrario la risposta avrà codice `404 Not Found`.
### 7 Visualizzare tutti i record
Nel file `record.model.js` creare il metodo `find` che richiama lo schema di mongoose e torna tutti i Record inseriti:
```javascript=
module.exports.find = async () => {
return Record.find();
}
```
Nel file `record.controller.js` chiamare il metodo `find` del model e passare i risultati al template:
```javascript=
module.exports.list = async (req, res, next) => {
const records = await recordModel.find();
res.render('list.ejs', {records: records});
}
```
Nel file `list.ejs` renderizzare con un forEach una riga della tabella per ogni record trovato:
```htmlembedded=
<html>
<head>
<title>List</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css" integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-Piv4xVNRyMGpqkS2by6br4gNJ7DXjqk09RmUpJ8jgGtD7zP9yug3goQfGII0yAns" crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<h3>Records</h3>
<table class="table">
<thead>
<tr>
<th>
Data
</th>
<th>
Città
</th>
<th>
Condizioni
</th>
<th>
Link
</th>
</tr>
</thead>
<tbody>
<% records.forEach(record => { %>
<tr>
<td>
<%= record.date %>
</td>
<td>
<%= record.city %>
</td>
<td>
<%= record.conditions %> - <%= record.temperature %>°C
</td>
<td>
<a href="/records/<%= record.id %>">Details</a>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
</body>
</html>
```
### 8 Gestione dei filtri
Iniziare col definire i filtri che ci interessa avere a disposizione e aggiungendo al template `list.ejs` il form di ricerca.
A differenza del form di aggiunta, questo form avrà `method="GET"`. Questo significa che i valori inseriti dall'utente verranno inviati tramite queryparams nell'url della richiesta. `action="/records"` significa che l'url che verrà richiesto al submit è sempre quello della lista dei record, ma conterrà anche i queryparams dei filtri.
Aggiungere a `list.ejs` il form di ricerca:
```htmlembedded=
<form class="form-inline" action="/records" method="GET">
<label class="mr-2">Città:</label>
<input type="text" class="form-control mr-2" name="city">
<label class="mr-2">Da:</label>
<input type="datetime-local" class="form-control mr-2" name="from">
<label class="mr-2">A:</label>
<input type="datetime-local" class="form-control mr-2" name="to">
<button class="btn btn-secondary" type="submit">Cerca</button>
</form>
```
Nel controller i parametri di ricerca sono messi a disposizione nell'oggetto req.query.
In questo caso i parametri non richiedono trasformazioni particolari, quindi è compito direttamente del model controllare che filtri sono richiesti e comporre la query adatta per mongoose.
Nel file `record.model.js`:
```javascript=
module.exports.find = async (query) => {
const q = {};
if (query.city) {
q.city = { $regex : new RegExp(query.city, "i")};
}
if (query.from || query.to) {
q.date = {};
}
if (query.from) {
q.date.$gte = query.from;
}
if (query.to) {
q.date.$lte = query.to;
}
return Record.find(q);
}
```
La strategia utilizzata è quella di partire con un oggetto `q` vuoto e aggiungere in seguito le proprietà necessarie a filtrare nel caso arrivino nell'argomento query della funzione. Questo permette di tornare tutti i risultati se arriva un oggetto `query` vuoto come argomento, e invece di popolare la query nel caso arrivino uno o più parametri di ricerca.
Nel file `record.controller.js` è sufficiente passare al metodo `find` del model l'oggetto `req.query`, che contiene i queryparams della richiesta.
Nel file `record.controller.js`:
```javascript=
module.exports.list = async (req, res, next) => {
const query = req.query;
const records = await recordModel.find(query);
res.render('list.ejs', {records: records});
}
```
### 9 - Visualizzare gli ultimi record inseriti se non sono applicati filtri
Iniziare col definire cosa si intende per "ultimi record": in questo caso si è deciso di tornare i record inseriti nella data di oggi.
Questo riduce il problema all'applicazione di un filtro per date di default nel caso che l'utente non ne imposti uno nei filtri.
Impostare questo filtro di default è compito del controller, il model sa solo come cercare i dati a partire da determinati parametri, se in assenza di richieste particolari vengono applicati dei valori di default è compito del controller definirlo e gestirlo.
Nel file `record.controller.js`:
```javascript=
module.exports.list = async (req, res, next) => {
const query = req.query;
if (!query.city && !query.from && !query.to) {
const from = new Date();
from.setHours(0, 0, 0, 0);
const to = new Date();
to.setHours(23, 59, 59, 999);
query.from = from;
query.to = to;
}
const records = await recordModel.find(query);
res.render('list.ejs', {records: records, query: query});
}
```
Si è deciso di applicare l'intervallo di date di default solo se non è presente alcun filtro impostato dall'utente, questo controllo avviene tramite l'`if`. Se ci troviamo in queste condizioni viene calcolata una data che rappresenta l'inizio della giornata di oggi e una che rappresenta la fine e vengono passate come `from` e `to` al model. Il resto della logica non richiede modifiche.
# Migliorie
- popolare i valori dei filtri con quelli applicati
- navbar
- validazione nel campo data in inserimento
- template della pagina di dettaglio
- possibilità di eliminare un record
- formattazione date nel template
- prepopolare pagina add con data di oggi e ultima città inserita