Développement d’applications d’entreprise 2

Développement d’applications d’entreprise 2


2.1- Angular, suite

2.2- Laboratoire 2

3- Analyse

Combinez vos apprentissages des outils Angular pour ajouter les fonctionnalités ci-dessous au projet de démonstration Housing.

HTTP

Il est fréquent pour les applications de type SPA de communiquer avec un API HTTP pour accéder aux fonctionnalités serveurs et services de traitement des données.

Votre travail se situe au niveau du Front-end, vous n'avez pas à modifier l'implémentation SQL sur Martha.

Ressources
angular
Formulaire
copier
import { Component } from '@angular/core';

import {MatFormFieldModule} from '@angular/material/form-field';
import {MatIconModule} from '@angular/material/icon';
import {MatInputModule} from '@angular/material/input';
import {MatButtonModule} from '@angular/material/button';
import {MatCheckboxModule} from '@angular/material/checkbox';

@Component({
selector: 'app-location-form',
imports: [
MatFormFieldModule, MatInputModule, MatIconModule, MatButtonModule, MatCheckboxModule
],
template: `
<h1>Location</h1>
<form>
<div id="layout">
<div id="inputs">
<mat-form-field appearance="outline">
<mat-label>Name</mat-label>
<input matInput>
</mat-form-field>

<mat-form-field appearance="outline">
<mat-label>City</mat-label>
<input matInput>
</mat-form-field>

<mat-form-field appearance="outline">
<mat-label>State</mat-label>
<input matInput>
</mat-form-field>

<div class="small-inputs">
<mat-form-field appearance="outline">
<mat-label>Units</mat-label>
<input matInput type="number">
</mat-form-field>

<mat-checkbox>Wifi</mat-checkbox>

<mat-checkbox>Laundry</mat-checkbox>
</div>

<div class="buttons-all">

<button mat-button color="danger">Delete</button>

<div class="buttons-right">
<button mat-button>Cancel</button>
<button mat-flat-button>Save</button>
</div>

</div>
</div>

<div id="photo">
<img src="https://angular.dev/assets/images/tutorials/common/i-do-nothing-but-love-lAyXdl1-Wmc-unsplash.jpg">

<mat-form-field appearance="outline">
<mat-label>Image URL</mat-label>
<input matInput type="text">
</mat-form-field>
</div>
</div>
</form>

`
,
styles: `
#layout {
display: flex;
gap: 32px;
justify-content: space-between;
}

#inputs {
flex-grow: 1;
display: flex;
flex-direction: column;
}

#photo {
display: flex;
flex-direction: column;
gap: 16px;
height: 600px;
width: 50%;
}

#photo img {
object-fit: cover;
border-radius: 4px;
width: 100%;
height: 100%;
}

.small-inputs {
display: flex;
align-items: baseline;
justify-content: space-between;
}

.mdc-label {
color: black;
}

.buttons-all {
display: flex;
justify-content: space-between;
}
.buttons-right {
display: flex;
justify-content: center;
gap: 32px;
}

@media (max-width: 890px) {
#layout {
flex-direction: column-reverse;
gap: 0px;
}

#photo {
width: 100%;
height: 300px;
}
}
`

})
export class LocationFormPage {

}
angular
Liste
copier
import { Component } from '@angular/core';
import { RouterModule } from '@angular/router';

import {MatTableModule} from '@angular/material/table';
import {MatIconModule} from '@angular/material/icon';
import {MatButtonModule} from '@angular/material/button';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatInputModule} from '@angular/material/input';

@Component({
selector: 'app-locations',
imports: [
RouterModule,
MatIconModule, MatButtonModule, MatFormFieldModule, MatInputModule, MatTableModule,
],
template: `
<div id="header">
<h1>Locations</h1>

<a class="add" matIconButton routerLink="/locations/new">
<mat-icon>add</mat-icon>
</a>
</div>

<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">

<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> Name </th>
<td mat-cell *matCellDef="let element"> {{element.name}} </td>
</ng-container>

<ng-container matColumnDef="city">
<th mat-header-cell *matHeaderCellDef> City </th>
<td mat-cell *matCellDef="let element"> {{element.city}} </td>
</ng-container>

<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> State </th>
<td mat-cell *matCellDef="let element"> {{element.state}} </td>
</ng-container>

<ng-container matColumnDef="availableUnits">
<th mat-header-cell *matHeaderCellDef> Units </th>
<td mat-cell *matCellDef="let element"> {{element.availableUnits}} </td>
</ng-container>

<ng-container matColumnDef="wifi">
<th mat-header-cell *matHeaderCellDef>Wifi</th>
<td mat-cell *matCellDef="let element">
<mat-icon class="availability" [attr.available]="element.wifi">{{ availabilityIcon(element.wifi) }}</mat-icon>
</td>
</ng-container>

<ng-container matColumnDef="laundry">
<th mat-header-cell *matHeaderCellDef> Laundry </th>
<td mat-cell *matCellDef="let element">
<mat-icon class="availability" [attr.available]="element.laundry">{{ availabilityIcon(element.laundry) }}</mat-icon>
</td>
</ng-container>

<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let element">
<a class="edit" matIconButton>
<mat-icon>edit</mat-icon>
</a>
</td>
</ng-container>

<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
`
,
styles: `
#header {
display: flex;
flex-direction: row;
align-items: baseline;
gap: 16px;
}

#header mat-icon {
margin: 0;
}

.mat-mdc-row {
background-color: white;
color: var(--mat-on-sys-secondary);
}

.mat-mdc-header-row {
color: var(--mat-on-sys-secondary);
background-color: var(--mat-sys-secondary);
}

.mat-mdc-row:hover .mat-mdc-cell {
background-color: color-mix(in lab, var(--mat-sys-secondary) 30%, transparent 100%);
}

.add {
--color: var(--mat-button-filled-label-text-color, var(--mat-sys-on-primary));

background-color: var(--mat-sys-surface-tint);
}

.edit {
--color: rgb(255, 165, 0);
background-color: color-mix(in lab, var(--mat-sys-secondary) 30%, transparent 100%);
}

a[matIconButton] {
color: var(--color);

/* https://www.fusonic.net/en/blog/angular-material-customization */
--mat-icon-button-ripple-color: color-mix(in lab, var(--color) 10%, transparent 100%);
--mat-icon-button-hover-state-layer-opacity: .8;
--mat-icon-button-state-layer-color: color-mix(in lab, var(--mat-sys-secondary) 100%, transparent 100%);
}

.mat-column-actions, .mat-column-laundry, .mat-column-wifi, mat-column-availableUnits, mat-column-state {
width: 0px;
}

.mat-column-availableUnits, .mat-column-laundry, .mat-column-wifi, .mat-column-state {
text-align: center
}

.availability[available=true] {
color: green;
}

.availability[available=false] {
color: red;
}
`

})
export class LocationsPage {
displayedColumns: string[] = ['name', 'city', 'state', 'availableUnits', 'wifi', 'laundry', 'actions'];

readonly baseUrl = 'https://angular.dev/assets/images/tutorials/common';
dataSource = [
{
id: 0,
name: 'Acme Fresh Start Housing',
city: 'Chicago',
state: 'IL',
photo: `${this.baseUrl}/bernard-hermant-CLKGGwIBTaY-unsplash.jpg`,
availableUnits: 4,
wifi: true,
laundry: true,
},
{
id: 1,
name: 'A113 Transitional Housing',
city: 'Santa Monica',
state: 'CA',
photo: `${this.baseUrl}/brandon-griggs-wR11KBaB86U-unsplash.jpg`,
availableUnits: 0,
wifi: false,
laundry: true,
},
{
id: 2,
name: 'Warm Beds Housing Support',
city: 'Juneau',
state: 'AK',
photo: `${this.baseUrl}/i-do-nothing-but-love-lAyXdl1-Wmc-unsplash.jpg`,
availableUnits: 1,
wifi: false,
laundry: false,
},
]

availabilityIcon(available: boolean | undefined): string | undefined {

if( available != undefined ) {
return available ? 'check' : 'close'
}

return undefined
}

}
sql
copier
create or replace table locations (
id int unsigned auto_increment key,
name varchar(250) not null,
city varchar(250) not null,
state char(2) not null,
photo varchar(500) not null,
available_units int unsigned,
wifi boolean,
laundry boolean,

user_email varchar(50) not null,

foreign key (user_email) references users(email)
);

insert into locations (name, city, state, photo, available_units, wifi, laundry, user_email)
values
(
'Acme Fresh Start Housing',
'Chicago',
'IL',
'https://angular.dev/assets/images/tutorials/common/bernard-hermant-CLKGGwIBTaY-unsplash.jpg',
4,
true,
true,
'a@a'
),
(
'A113 Transitional Housing',
'Santa Monica',
'CA',
'https://angular.dev/assets/images/tutorials/common/brandon-griggs-wR11KBaB86U-unsplash.jpg',
0,
false,
true,
'a@a'
),
(
'Warm Beds Housing Support',
'Juneau',
'AK',
'https://angular.dev/assets/images/tutorials/common/i-do-nothing-but-love-lAyXdl1-Wmc-unsplash.jpg',
1,
false,
false,
'a@a'
),
(
'Homesteady Housing',
'Chicago',
'IL',
'https://angular.dev/assets/images/tutorials/common/ian-macdonald-W8z6aiwfi1E-unsplash.jpg',
1,
true,
false,
'a@a'
),
(
'Happy Homes Group',
'Gary',
'IN',
'https://angular.dev/assets/images/tutorials/common/krzysztof-hepner-978RAXoXnH4-unsplash.jpg',
1,
true,
false,
'a@a'
),
(
'Hopeful Apartment Group',
'Oakland',
'CA',
'https://angular.dev/assets/images/tutorials/common/r-architecture-JvQ0Q5IkeMM-unsplash.jpg',
2,
true,
true,
'b@b'
),
(
'Seriously Safe Towns',
'Oakland',
'CA',
'https://angular.dev/assets/images/tutorials/common/phil-hearing-IYfp2Ixe9nM-unsplash.jpg',
5,
true,
true,
'b@b'
),
(
'Hopeful Housing Solutions',
'Oakland',
'CA',
'https://angular.dev/assets/images/tutorials/common/r-architecture-GGupkreKwxA-unsplash.jpg',
2,
true,
true,
'b@b'
),
(
'Seriously Safe Towns',
'Oakland',
'CA',
'https://angular.dev/assets/images/tutorials/common/saru-robert-9rP3mxf8qWI-unsplash.jpg',
10,
false,
false,
'c@c'
),
(
'Capital Safe Towns',
'Portland',
'OR',
'https://angular.dev/assets/images/tutorials/common/webaliser-_TPTXZd9mOo-unsplash.jpg',
6,
true,
true,
'c@c'
);
json
Queries
copier
[
{
"name": "users-login",
"string": "select email from users where email = '?email' && passwd = sha2('?password', 256);",
"type": 0
},
{
"name": "users-signup",
"string": "insert into users(email, passwd) values ('?email', sha2('?password', 256));",
"type": 1
},
{
"name": "select-locations",
"string": "select * from locations where coalesce(?email, user_email) = user_email;",
"type": 0
},
{
"name": "select-location",
"string": "select * from locations where id = ?id && coalesce(?email, user_email) = user_email;",
"type": 0
},
{
"name": "delete-location",
"string": "delete from locations \r\nwhere id = ?id && user_email = \"?email\"\r\nreturning id;",
"type": 0
},
{
"name": "DO_NOT_USE_TEST_RESET",
"string": "create or replace table locations (\r\n id int unsigned auto_increment key,\r\n name varchar(250) not null,\r\n city varchar(250) not null,\r\n state char(2) not null,\r\n photo varchar(500) not null,\r\n available_units int unsigned,\r\n wifi boolean,\r\n laundry boolean,\r\n\r\n user_email varchar(50) not null,\r\n\r\n foreign key (user_email) references users(email)\r\n);\r\n\r\ninsert into locations (name, city, state, photo, available_units, wifi, laundry, user_email)\r\nvalues\r\n (\r\n 'Acme Fresh Start Housing',\r\n 'Chicago',\r\n 'IL',\r\n 'https://angular.dev/assets/images/tutorials/common/bernard-hermant-CLKGGwIBTaY-unsplash.jpg',\r\n 4,\r\n true,\r\n true,\r\n 'a@a'\r\n ),\r\n (\r\n 'A113 Transitional Housing',\r\n 'Santa Monica',\r\n 'CA',\r\n 'https://angular.dev/assets/images/tutorials/common/brandon-griggs-wR11KBaB86U-unsplash.jpg',\r\n 0,\r\n false,\r\n true,\r\n 'a@a'\r\n ),\r\n (\r\n 'Warm Beds Housing Support',\r\n 'Juneau',\r\n 'AK',\r\n 'https://angular.dev/assets/images/tutorials/common/i-do-nothing-but-love-lAyXdl1-Wmc-unsplash.jpg',\r\n 1,\r\n false,\r\n false,\r\n 'a@a'\r\n ),\r\n (\r\n 'Homesteady Housing',\r\n 'Chicago',\r\n 'IL',\r\n 'https://angular.dev/assets/images/tutorials/common/ian-macdonald-W8z6aiwfi1E-unsplash.jpg',\r\n 1,\r\n true,\r\n false,\r\n 'a@a'\r\n ),\r\n (\r\n 'Happy Homes Group',\r\n 'Gary',\r\n 'IN',\r\n 'https://angular.dev/assets/images/tutorials/common/krzysztof-hepner-978RAXoXnH4-unsplash.jpg',\r\n 1,\r\n true,\r\n false,\r\n 'a@a'\r\n ),\r\n (\r\n 'Hopeful Apartment Group',\r\n 'Oakland',\r\n 'CA',\r\n 'https://angular.dev/assets/images/tutorials/common/r-architecture-JvQ0Q5IkeMM-unsplash.jpg',\r\n 2,\r\n true,\r\n true,\r\n 'b@b'\r\n ),\r\n (\r\n 'Seriously Safe Towns',\r\n 'Oakland',\r\n 'CA',\r\n 'https://angular.dev/assets/images/tutorials/common/phil-hearing-IYfp2Ixe9nM-unsplash.jpg',\r\n 5,\r\n true,\r\n true,\r\n 'b@b'\r\n ),\r\n (\r\n 'Hopeful Housing Solutions',\r\n 'Oakland',\r\n 'CA',\r\n 'https://angular.dev/assets/images/tutorials/common/r-architecture-GGupkreKwxA-unsplash.jpg',\r\n 2,\r\n true,\r\n true,\r\n 'b@b'\r\n ),\r\n (\r\n 'Seriously Safe Towns',\r\n 'Oakland',\r\n 'CA',\r\n 'https://angular.dev/assets/images/tutorials/common/saru-robert-9rP3mxf8qWI-unsplash.jpg',\r\n 10,\r\n false,\r\n false,\r\n 'c@c'\r\n ),\r\n (\r\n 'Capital Safe Towns',\r\n 'Portland',\r\n 'OR',\r\n 'https://angular.dev/assets/images/tutorials/common/webaliser-_TPTXZd9mOo-unsplash.jpg',\r\n 6,\r\n true,\r\n true,\r\n 'c@c'\r\n );",
"type": 1
},
{
"name": "insert-location",
"string": "insert into locations (name, city, state, photo, available_units, wifi, laundry, user_email) \r\nvalues (\"?name\", \"?city\", \"?state\", \"?photo\", ?units, ?wifi, ?laundry, \"?email\")\r\nreturning id;",
"type": 0
},
{
"name": "update-location",
"string": "update locations set \r\n\tname = \"?name\",\r\n\tcity = \"?city\",\r\n\tstate = \"?state\",\r\n\tphoto = \"?photo\",\r\n\tavailable_units = ?units,\r\n\twifi = ?wifi,\r\n\tlaundry = ?laundry\r\nwhere id = ?id && user_email = \"?email\";",
"type": 0
}
]
rb
Exemples queries
copier
# USAGE
# MARTHA_USERNAME='...' MARTHA_PASSWORD='...' ruby locations_martha_tests.rb

# Vim remap
# :nnoremap ;; :w <bar> :terminal ruby %<cr>
# set fallback variables at line 23

ENV["APP_ENV"] = "test"

require "bundler/inline"
require "minitest/autorun"
require "rack/test"
require "base64"

gemfile do
source "https://rubygems.org"

gem "faraday"
end

require "faraday"

USERNAME = ENV["MARTHA_USERNAME"] || "..."
PASSWORD = ENV["MARTHA_PASSWORD"] || "..."

class Test < Minitest::Test

def setup
options = { headers: { "auth" => Base64.encode64("#{USERNAME}:#{PASSWORD}") } }

@martha = Faraday.new("http://martha.jh.shawinigan.info", options) do |config|
config.request :json
config.response :json, **{ parser_options: { symbolize_names: true } }
end

q "DO_NOT_USE_TEST_RESET"
end

def q(query, params = nil)
response = @martha.post("/queries/#{query}/execute", params).body

unless response[:success]
puts "FAILED"
puts response[:error]
end

return response
end

##
### LIST
##

def test_select_all_public
r = q "select-locations", { email: nil }

assert r[:data].length == 10
end

def test_select_all_owner_a
# !!!
# !!! Nested string, ' inside "
# !!!
r = q "select-locations", { email: "'a@a'" }

assert r[:data].length == 5
end

def test_select_all_owner_b
r = q "select-locations", { email: "'b@b'" }

assert r[:data].length == 3
end

def test_select_all_owner_invalid
r = q "select-locations", { email: "'z@z'" }

assert r[:data].length == 0
end

##
### READ
##

def test_select_one_public
r = q "select-location", { id: 1, email: nil }

assert r[:data].length == 1
end

def test_select_one_a
r = q "select-location", { id: 2, email: "'a@a'" }

assert r[:data].length == 1
end

def test_select_one_not_a
r = q "select-location", { id: 10, email: "'a@a'" }

assert r[:data].length == 0
end

def test_select_one_public_invalid_id
r = q "select-location", { id: 999, email: nil }

assert r[:data].length == 0
end

##
### CREATE
##

def test_insert_a
r = q "insert-location", { name: "new a", city: "city a", state: "SA", photo: "https://cdn-icons-png.flaticon.com/128/619/619153.png", units: 2, wifi: true, laundry: false, email: "a@a" }

assert r[:data].length == 1

r = q "select-locations", { email: "'a@a'" }

assert r[:data].length == 6
end

##
### UPDATE
##

def test_update_a
r = q "update-location", { id: 2, name: "new a2", city: "city a2", state: "A2", photo: "https://cdn-icons-png.flaticon.com/128/619/619153.png", units: 4, wifi: 0, laundry: 1, email: "a@a" }

assert r[:success]

r = q "select-location", { id: 2, email: "'a@a'" }

assert r[:data].first[:name] == "new a2"
end

##
### DELETE
##

def test_delete_1_a
r = q "delete-location", { id: 1, email: "a@a" }

assert r[:data].length == 1
end

def test_delete_10_not_a
r = q "delete-location", { id: 10, email: "a@a" }

assert r[:data].length == 0
end

def test_delete_1_invalid_email
r = q "delete-location", { id: 1, email: "z@z" }

assert r[:data].length == 0
end

end

Routing

Solidifiez les mécanismes de navigation en ajoutant

Internationalisation

Exploitez la librairie ngx-translate pour implémenter la traduction en 2 langues(FR et EN) du formulaire Location.

Déploiement

Mettre en ligne la version production de votre application sur votre micro-serveur.

Remise

Vous devez présenter une démonstration fonctionnelle de votre implémentation avant vendredi 30 janvier @ 17h30

Développement d’applications d’entreprise 2

Nom

Appréciation générale
UI 0     -0.5     -1     -2
UX 0     -0.5     -1     -2
Code 0     -0.5     -1     -2
Séparations judicieuses des responsabilités 0     -0.5     -1     -2
Fonctionnement de base de l'atelier 0     -0.5     -1     -2
HTTP
Intégration des ressources, Navigation, Connecté vs Public, Liste, Formulaire 2     1.5     1     0
C   (R)U   D   L   Locations 4     3.5     3     2     1     0
Validations 2     1.5     1     0
Routes
Wildcard 1     0.5     0
Guards 2     1.5     1     0
Traduction
Intégration ngx-translate exhaustive du formulaire 2     1.5     1     0
Persistente 2     1.5     1     0
Déploiement
Production en ligne, fallback 2     1     0