pour installer RoR rapidement: https://gorails.com/setup/ubuntu/20.04

Sommaire


COURS N3

Utilisation d’une API

Permet de lire du JSON: require "json"

Faire des requetes HTTP en Ruby: require "open-uri"

Ex :

url = "https://api.site.com"
headers = {
  "Authorization" => "TOKEN"
}

posts = JSON.parse(open("url, headers).read)

COURS N4

Fichier : ma_classe.rb

Nom d’une classe : MaClasse

Variable : blue_car=

Dans irb pour charger une classe, exemple car.rb:

require_relative "car"

puis la classe est chargée (Car.class => #Class)

COURS N5

Le Design Pattern MVC

Voici un exemple de “TODOLIST” en ligne de commande.

app.rb :

require_relative "router"

router = Router.new
router.start

controller.rb :

require_relative "task"
require_relative "view"

class Controller
  # 1 intention utilisateur => 1 méthode d'instance
  #
  # - Afficher les tâches
  # - Ajouter une tâche
  # - Marquer une tâche comme réalisée
  # - ...

  def initialize
    @tasks = []
    @view = View.new
  end

  def list
    @view.print_tasks(@tasks)
  end

  def add
    # 1. Demander au user description de la tache
    description = @view.ask_user_for_description
    # 2. Creer la nouvelle tâche
    task = Task.new(description)
    # 3. Stocker la nouvelle tâche
    @tasks << task
  end

  def mark_as_done
    # 1. Demander au user l'id de la tache
    id = @view.ask_user_for_id
    # 2. Chercher la Task de l'id demandé
    task = @tasks[id]
    # 3. Marker la tache comme faite
    task.mark_as_done
  end
end

router.rb :

require_relative "controller"

class Router
  def initialize
    @controller = Controller.new
  end

  def start
    loop do
      # Qu'est-ce que tu veux faire ?
      puts "----------------------------"
      puts "What do you want to do next?"
      puts "1 - List tasks"
      puts "2 - Add a task"
      puts "3 - Mark task as done"
      puts "----------------------------"
      action = gets.chomp.to_i

      if action == 1
        @controller.list
      elsif action == 2
        @controller.add
      elsif action == 3
        @controller.mark_as_done
      else
        puts "Wrong choice"
      end
    end
  end
end

task.rb :

class Task
  attr_reader :description

  def initialize(description)
    @description = description    # String
    @done = false                 # true / false
  end

  def done?
    return @done
  end

  def mark_as_done
    @done = true
  end
end

test_scenario.rb :

require_relative "controller"

controller = Controller.new


# 1. Ajoute une tache
# 2. Ajoute une tache
# 3. Affiche une tache

controller.add
controller.add
controller.list
controller.mark_as_done
controller.list

view.rb :

# Role: gets / puts

class View
  def print_tasks(tasks)  # Array<Task>
    tasks.each_with_index do |task, index|
      if task.done?
        marker = "[x]"
      else
        marker = "[ ]"
      end
      puts "#{index + 1}. #{marker} #{task.description}"
    end
  end

  def ask_user_for_description
    puts "Description?"
    print "> "
    description = gets.chomp
    return description
  end

  def ask_user_for_id
    puts "Index?"
    print "> "
    id = gets.chomp.to_i
    return id - 1
  end
end

COURS N6

Quand on crée une appli

  1. faire le mockup (si vous cherchez de l’inspiration en UI, vous pouvez consulter des sites comme collectUI)
  2. dessiner le schéma de la BDD
  3. faire les user stories (pour la gestion de projet, sur trello par exemple faire du kanban)

Exemple, pour les user stories, faire 4 colonnes :

Backlog Ready In progress Done
As a visitor, i can sign up      
As a visitor, i can see all product      
As a user, i can post product      

COURS N7

HTTP Request –> ROUTER (config/routes.rb) –> CONTROLLER (app/controllers) <–> MODEL (app/models) <–> BDD CONTROLLER –> VIEW (app/views) ==> USER

Démarrer un projet Rails

On démarre toujours un projet Rails de la même façon. On crée l’application Rails avec Rails new <APP_NAME> <-T without_files_test> <--database=postgresql>, on se déplace à l’intérieur, on crée la base de données associée et on ouvre le projet avec Sublime Text :

rails _5.0.0.1_ new MY_APP -T --database=postgresql
cd MY_APP
rails db:create
stt
/
|_ app
|   |__ controllers
|   |__ models
|   |__ views
|_ config
    |__ routes.rb

Pour lancer le serveur web, ouvrez un autre onglet du terminal, et lancez la commande puis allez sur http://localhost:3000 :

rails server
# ou
rails s

Puis on versionne avec git dès le départ :

git init
git add .
git commit -m "rails new"

Enfin on crée le repository associé (on dit “dépôt” en français) sur Github et on push son travail dessus. Pour cela, on utilise le petit utilitaire hub qui permet de créer des projets Github depuis son terminal quand on est paresseux :

hub create             // on crée le repo Github
git push origin master // on push son travail dessus
hub browse             // on ouvre le repo dans son navigateur

Le template du vagon

Le problème de rails new, c’est que ça vous génère un projet :

C’est pour ça qu’on va utiliser le template du vagon, que vous pouvez retrouver sur ce repo Github. Pour créer un projet, rien de plus simple :

rails new \
  -T --database postgresql \
  -m https://raw.githubusercontent.com/lewagon/rails-templates/master/minimal.rb \
  MY_APP

Ce template Rails se charge aussi de faire le rails db:create, le git init et le premier commit. Vous pouvez donc directement passer à l’étape de création du repo Github.

COURS N8

Les headers http : Developer.Mozilla.org

Les requêtes HTTP

Une requête HTTP a quatre parties :

* Un verbe HTTP (GET / POST / PATCH / DELETE)
* Une URL (la partie visible dans la barre de navigation)
* Des headers
* Un body dans le cas d’une requête POST / PATCH (une requête GET n’a pas de body)

Pour voir ce que vos requêtes HTTP ont dans le ventre quand vous navigez sur un site, ouvrez l’onglet Network de votre Chrome :

Vous pouvez consulter la liste de tous les headers HTTP :

* `Accept` permet par exemple de définir le type de fichier que votre client accepte en réponse du serveur.
* `Referer` correspond au site sur lequel vous vous trouvez quand vous faites la requête.

Par exemple, ici :

* On fait une requête **GET** `http://www.levagon.com`.
* Le referer est bien `http://www.google.fr`, puisqu’on a fait cette requête depuis Google.
* Le referer de la requête est récupéré par les solution d’analytics (comme Google Analytics, Mixpanel) pour vous permettre d’analyser les sources de traffic sur votre site.

Vos premières routes

Le fichier routes.rb est le point d’entrée des requêtes qui arrivent à votre site Rails. Chaque route dirige une requête vers une méthode ruby définie dans un controller, qu’on appelle aussi une action. Par exemple, ce routing :

# config/routes.rb
Rails.application.routes.draw do
  root to: 'pages#home'
  get "/team" => "pages#team"
  get "/contact" => "pages#join_us"
end

Va diriger les trois requêtes (GET /, GET /team, GET /contact) vers :

# app/controllers/pages_controller.rb
class PagesController < ApplicationController
  def home
    # ...
  end
  def team
    #variable d'instance
    @time=Time.now  #ensuite utiliser la var d'instance dans la vue
  end
  def join_us
    # ...
  end
end

N’oubliez pas, pour générer un nouveau controller (ex : ProductsController), vous avez la commande :

rails g controller products

Et pour afficher vos routes (à faire plusieurs fois par jour):

rails routes

Convention Action/Vue et ERB

La convention Action/Vue :

Une fois qu’une requête est routée vers une action d’un controller, l’action doit renvoyer un template. Comment sait-elle quel template renvoyer ? Grâce à la convention Action/Vue :

* Le controller `PagesController` cherche les templates dans le dossier `views/pages` (nommé comme lui).
* L’action `PagesController#home` chercher le template `views/pages/home.html.erb` qui a le même nom qu’elle.

ERB

Les templates ERB permettent d’injecter du ruby dans du HTML. Cela dit, il ne s’agit pas d’écrire trop de “code ruby” dedans. Si vous codez proprement, c’est le controller qui doit récupérer des informations, les stocker dans des variables d’instance (qui commencent par un @) pour ensuite les injecter dans le template ERB. Comme dans cet exemple :

# app/controllers/pages_controller.rb
class PagesController < ApplicationController
  def home
    @tagline = "Tinder for job search"
  end
end

et ensuite dans le template associé :

<!-- app/views/pages/home.html.erb -->

<h1>Kudoz</h1>
<h2>
  <%= @tagline %>
</h2>

COURS N9

Les paramètres d’une requête HTTP

Il y a deux façon de communiquer des paramètres dans une requêtes HTTP :

* directement dans l’URL (dans le cas d’une requête GET)
* dans le corps de la requête (dans le cas d’une requête POST)

Paramètres dans l’URL

URL interprétée

Une application web est capable d’interpréter certaines parties de l’URL comme étant des valeurs de paramètres :

* `http://facebook.com/jeandupont` : Facebook va interpréter `jeandupond` comme étant la valeur d’un paramètre "username"
* `http://airbnb.com/s/istanbul` : Airbnb va interpréter `istanbul` comme étant la valeur d’un paramètre "city"

Query string

On peut aussi passer des paramètres dans l’URL de façon explicite après un ?, par exemple :

* Dans l’URL `https://www.airbnb.fr/s/istanbul?checkin=31/03/2016&checkout=15/04/2016`
* La partie après le `?` (c’est-à-dire `checkin=31/03/2016&checkout=15/04/2016`) s’appelle une **query string**.
* On peut y lire directement le nom des paramètres ("checkin" et "checkout") et leur valeur (`31/03/2016` et `15/04/2016`).
* Les différents paramètres (clef=valeur) sont séparés par un `&`.

Nous partageons tous des URLs

Dans la vraie vie, nous partageons tous des URLs. Avoir des paramètres dans l’URL est donc très pratique, notamment pour toutes les pages de recherche.

Paramètres dans le body

Quand on remplit un formulaire pour créer une nouvelle entrée en base de données (nouvel utilisateur, nouveau post/tweet/produit/appartement/etc..), on fait une requête de type POST. Dans ce cas, on passe les paramètres dans le corps de la requête (i.e. le body), ce que l’inspecteur Google Chrome appelle “Form Data” :

Les params dans Rails

Rails récupère tous les paramètres dans le hash params.

URL dynamique

Prenons un exemple :

# config/routes.rb
Rails.application.routes.draw do
  get "/say/:message" => "pages#speak"
end

# app/controllers/pages_controller.rb
class PagesController < ApplicationController
  def speak
    render plain: "I say: #{params[:message]}"
  end
end

Dans ce cas :

* `http://localhost:3000/say/hello` va renvoyer "I say: hello"
* `http://localhost:3000/say/goodbye` va renvoyer "I say: goodbye"
* Le paramètre dynamique de l’URL s’appelle ici message (l’équivalent du "username" sur Facebook ou de la "city" sur Airbnb)

Query string

Prenons un autre exemple :

# config/routes.rb
Rails.application.routes.draw do
  get "/search" => "pages#search"
end

# app/controllers/pages_controller.rb
class PagesController < ApplicationController
  def search
    render plain: "Search for category #{params[:filter]}"
  end
end

Dans ce cas :

* `http://localhost:3000/seach?filter=design` va renvoyer "Search for category design"
* `http://localhost:3000/seach?filter=tech` va renvoyer "Search for category tech"

COURS N10

A quoi sert Active Record ?

Active Record est un ORM (mapping objet-relationnel), c’est la librairie ruby qui permet de se connecter à la base de données pour pouvoir lire/écrire dedans de façon très simple avec des méthodes ruby (à la place de requêtes SQL).

Model (app/models) <—ActiveRecord—> PostgreSQL

La convention de nommage

Chaque table de la base de données est reliée à un modèle ActiveRecord, c’est-à-dire une classe ruby qui hérite de ActiveRecord.

Comment se fait ce lien entre table et modèle ? Simplement grâce à une convention de nommage :

table: products <—ActiveRecord naming convention—> model: Product

* La table s’écrit en minuscule / pluriel
* Le modèle s’écrit en majuscule / singulier

Si vous respectez cette convention, votre modèle est connecté comme par magie à la table et vous pouvez donc l’utiliser pour lire ou écrire dans cette table.

Générateurs de modèle / migration

Modèle

Pour générer un modèle dans Rails (par exemple un modèle Restaurant pour changer) :

rails g model Restaurant name:string address:string

Ce générateur crée deux fichier :

* Le modèle `restaurant.rb` dans le dossier `app/models`
* La migration `yyyymmddhhmmss_create_restaurants` dans le dossier `db/migrate`

Le fichier de migration correspond aux instructions à lancer pour modifier le schéma de la base de données, ici pour créer une nouvelle table. Pour lancer les migrations :

rails db:migrate

Attention : si vous ne lancez pas la migration, la table restaurants n’existera pas dans la base de données et le modèle Restaurant ne pourra donc pas s’y connecter.

Migration

Vous avez parfois besoin de générer une migration, par exemple pour ajouter une colonne à une table existante. Pour cela, utilisez le générateur de migration de Rails :

rails g migration AddRatingToRestaurants rating:integer
rails db:migrate # pour lancer la nouvelle migration

Notez bien la façon de nommer la migration qui respecte la convention CamelCase avec un s à Restaurants car on parle ici de la table restaurants à laquelle on veut ajouter une nouvelle colonne rating de type integer.

La console Rails

Une fois que vous avez créé un modèle et lancé la migration qui crée la table associée au modèle, vous pouvez tester votre modèle dans la console rails. Rien de plus simple :

rails console # ou juste rails c

Les méthodes basiques d’Active Record

Vous pouvez retrouver toutes les méthodes qu’on décrit ci-dessous dans la documentation officielle.

All

La méthode all permet de récupérer toutes les entrée de la table (ici on lit dans la table products). Comprenez bien que ActiveRecord va faire la requête SQL correspondante pour vous et vous renvoyer les résultat sous la forme d’un tableau ruby d’objet de la classe Product, qui sont ensuite très simples à manipuler en ruby.

Product.all
#==> SELECT * FROM products

New, save et create

Pour créer un nouveau produit en base de données, vous pouvez le faire en deux étapes avec new et save :

kudoz = Product.new(name: "kudoz", url: "getkudoz.com")
kudoz.save
#==> INSERT INTO products (name, url) VALUES ('kudoz', 'getkudoz.com')

Ou alors en un coup avec la méthode create :

Product.create(name: "kudoz", url: "getkudoz.com")
#==> INSERT INTO products (name, url) VALUES ('kudoz', 'getkudoz.com')

C’est parfois intéressant de le faire en deux temps, lorsqu’on a par exemple des validations sur le modèle (voir plus bas).

Find

La méthode find permet de retrouver un produit dans la table étant donné son identifiant id :

Product.find(1)
#==> SELECT * FROM products WHERE id = 1

Validations

On peut ajouter des validations Active Record sur un modèle si l’on veut se protéger contre l’écriture de mauvaise données en base. Prenons un exemple :

class Restaurant < ActiveRecord::Base
  validates :name, presence: true
  validates :address, presence: true, uniqueness: true
  validates :rating, inclusion: {in: [0, 1, 2, 3, 4, 5]}
end

Ici, on ne peut pas sauver un produit en base si jamais :

* Il n’a pas de nom ou d’adresse
* Son adresse n’est pas unique (i.e. il y a déjà un produit avec la même adresse dans la DB)
* Son rating n’est pas compris entre 0 et 5

Si on essaie de sauver un produit invalide (par exemple dans la console), Active Record va d’abord vérifier les validations et empêcher d’écrire en base si les validations ne passent pas :

rails c
> wrong = Restaurant.new(name: "Wrong restaurant", "1 wrong street", rating: 1000000)
> wrong.save # N'écrit pas en base de données et renvoie => false

la methode valid? permet de tester la variable avant de la sauvegarder en BDD Ici, nous avons rendu obligatoire le nom et l’url or la variable instantiée ne contient pas de nom :

> kudoz = Product.new(url: getkudoz.com)
> kudoz.valid? # => false
> kudoz.save   # => false
> kudoz.name = Kudoz
> kudoz.valid? # => true
> kudoz.save   # => true

COURS N11

Les actions CRUD

Dans la plupart des sites, on veut pouvoir créer / lire / modifier / supprimer un modèle. Cela peut être un Product sur ProductHunt, un Flat sur Airbnb, ou encore un Post sur Facebook.

* As a user, I can **CRUD** a flat (dans AirBnB)
* As a user, I can **CRUD** a tweet (dans Tweeter)
* As a user, I can **CRUD** a post (dans Facebook)
* As a user, I can **CRUD** a product (dans ProductHunt)

Il est donc extrêmement important de connaître toutes les routes conventionnelles de Rails qui correspondent à ces actions CRUD.

Les 7 routes CRUD

Ces routes sont conventionnelles dans Rails. Il faut les connaître par coeur :

get    "products"          => "products#index"
get    "products/:id"      => "products#show"
get    "products/new"      => "products#new"
post   "products"          => "products#create"
get    "products/:id/edit" => "products#edit"
patch  "products/:id"      => "products#update"
delete "products/:id"      => "products#destroy"
* Retenez bien à chaque fois **l’URL de la requête** ainsi que **le nom de l’action** dans le controlleur.
* Notez bien que la création et la modification se font chacune **en deux requêtes**.

Création et modification

Pour créer un appartement sur Airbnb (ou encore une review sur Yelp, un post sur Facebook, etc.), ça se fait toujours en deux temps :

* Une première requête `GET` permet d’arriver sur la page de création contenant un **formulaire vierge**.
* En soumettant le formulaire, une seconde requête `POST` envoie les données du formulaire au site web *pour qu’il les enregistre en base*.

C’est exactement la même chose pour modifier une entrée existante :

* Une première requête `GET` permet d’arriver sur la page d’édition contenant un **formulaire pré-rempli**.
* En soumettant le formulaire, une seconde requête `PATCH` envoie les données du formulaire au site web **pour qu’il les modifie en base**.

Méthode de routing resources

On n’écrit jamais toutes les 7 routes CRUD à la main dans son fichier de routing routes.rb. A la place on utilise la méthode resources, qui génère exactement les mêmes routes:

#config/routes.rb
resources :products

Vous pouvez vérifier que vos 7 routes sont toujours là dans la console avec :

rails routes

Vous pouvez aussi générer un sous-ensemble de ces 7 routes si vous ne les voulez pas toutes. Par exemple, pour générer uniquement les routes des actions de lecture :

resources :products, only: [:index, :show]

Pour aller plus loin dans le routing Rails, n’hésitez pas à lire la documentation officielle sur le routing Rails.

Les helpers

Les helpers Rails sont des méthodes ruby qui vous aident à générer des URLs ou du HTML. Quand vous affichez vos routes avec :

rails routes

Vous avez un affichage qui ressemble à ça :

      Prefix Verb   URI Pattern                  Controller#Action
    products GET    /products(.:format)          products#index
             POST   /products(.:format)          products#create
 new_product GET    /products/new(.:format)      products#new
edit_product GET    /products/:id/edit(.:format) products#edit
     product GET    /products/:id(.:format)      products#show
             PATCH  /products/:id(.:format)      products#update
             PUT    /products/:id(.:format)      products#update
             DELETE /products/:id(.:format)      products#destroy

Ici la colonne Prefix sert à générer la colonne URI. Par exemple :

products_path             # => "/products"
new_product_path          # => "/products/new"

# kudoz = Product.find(23)
product_path(kudoz)       # => "/products/23"
edit_product_path(kudoz)  # => "/products/23/edit"

Vous pouvez ensuite combiner ces méthodes qui génèrent des URLs avec la méthode link_to qui génère une balise de lien <a>. Par exemple, le code ERB suivant :

<!-- kudoz = Product.find(23) -->
<%= link_to kudoz.name, product_path(kudoz), class: "btn btn-warning" %>
<%= link_to "Ajouter un produit", new_product_path, class: "btn btn-primary" %>

va générer le HTML suivant :

<a href="/products/23" class="btn btn-warning">Kudoz</a>
<a href="/products/new" class="btn btn-primary">Ajouter un produit</a>

COURS N12

Actions new et create

Comme on l’a vu la dernière fois, créer un produit se fait en deux temps, deux requêtes HTTP, et donc deux actions (les actions new et create) :

* `new` sert a afficher le formulaire de création de produit
* En soumettant ce formulaire on fait une requête `POST` qui va appeler l’action `create` permettant d’enregistrer le produit en base.

Formulaire HTML natif

Commençons par le new qui renvoie la vue new.html.erb. Un formulaire de création en HTML, ça ressemble à ça :

<form action="/products" method="post">
  <input type="text" name="product[name]"> #ici on met les param dans un tab
  <input type="text" name="product[tagline]">
  <input type="submit">
</form>

Le formulaire est envoyé via une requete http POST "/products":

params = {
  product: {
    name: "Kudoz"
    tagline: "tinder for job search"
  }
}
* Ici, les attributs `name` des balises `<input>` sont très importants et **servent à nommer les paramètres** pour pouvoir les récupérer dans les `params` de Rails comme dans le schéma au-dessus.
* Le problème d’un formulaire HTML comme celui-là, c’est qu’il ne sécurise pas la requête HTTP qui est faite quand on le soumet. Cette requête pourrait très bien venir d’un autre site et d’un petit malin qui veut vous hacker.
* Rails a un mécanisme de protection qui vérifie que la requête contient une clef de sécurité (appelée `authenticity_token`) qui **garantit que cette requête vient bien du formulaire de votre site**.

Simple form et clef de sécurité

En raison de ce mécanisme de sécurité, vous ne pouvez pas utiliser un formulaire HTML natif dans Rails pour faire des requêtes POST.

Vous devez utiliser une méthode ruby qui génére le code HTML du formulaire et y ajoute une clef de sécurité. La méthode basique qui permet de faire ça dans Rails s’appelle form_for mais elle est compliquée à utiliser.

on préfère la méthode simple_form_for qui fait la même chose que form_for avec une syntaxe plus simple (comme son nom l’indique). Si vous voulez lire la doc de cette gem.

La syntaxe simple_form_for pour générer un formulaire est la suivante (on considère ici que @product = Product.new) :

<%= simple_form_for @product do |f| %>
  <%= f.input :name, placeholder:"kudoz", label:"quel produit?" %>
  <%= f.input :tagline %>
  <%= button :submit %>
<% end %>

Cela equivaut à:

<form action="/products" method="post">
  <input type="text" name="product[name]" id="name">
  <input type="text" name="product"[tagline]" id="address">
  <input type="hidden" name="authenticity_token" value="AODLAM..."
  <input type="submit">
</form>

Ici, le formulaire va non seulement envoyer les données entrées par l’utilisateur (name et tagline du produit) mais la requête contiendra aussi un authenticity_token qui la sécurise. C’est presque gagné ! Il reste quand même un deuxième mécanisme de sécurité à mettre en place avant de sauvegarder les données en base.

Strong params: filtrer ses paramètres

Rien n’empêche un utilisateur d’ajouter des champs dans un formulaire en éditant le HTML comme dans la vidéo. Dans ce cas, la requête provient bien de notre site (elle a donc un authenticity_token) mais elle contient des infos supplémentaires qu’on n’a pas forcément envie d’enregistrer en base.

Dans l’exemple ci-dessous, quelqu’un a essayé d’ajouter une mauvaise review à Kudoz en ajoutant un champ dans le formulaire avant de le soumettre.

params = {
  product: {
    name: "Kudoz",
    tagline: "tinder for job search"
    review: "I am a hater..."
  }
}

params.require(:product)

{
    name: "Kudoz",
    tagline: "tinder for job search"
    review: "I am a hater..."
}

params.require(:product).permit(:name, :tagline)

{
  name: "Kudoz",
  tagline: "tinder for job search"
}

Heureusement, Rails permet d’appliquer un filtre aux paramètres dans le controller. Dans le schéma ci-dessus, ce filtre n’autorise que les champs name et tagline. Du coup le champ review passe à la trappe. C’est vous qui choisissez les paramètres que vous laissez passer dans vos controller.

Finissons le new et create

Voici le code final de nos actions de création dans le ProductsController, qui gère aussi les validations Active Record comme dans la vidéo :

class ProductsController < ApplicationController
  def new
    @product = Product.new
  end
  def create
    @product = Product.new(product_params)
    if @product.save
      redirect_to products_path
    else
      render :new
    end
  end

  private
  def product_params
    params.require(:product).permit(:name, :url, :tagline)
  end
end

Les validations Active Record nous permettent d’avoir des comportements différents lorsqu’on soumet le formulaire:

* Si les données sont valides, on sauve le produit en base et on redirige vers le listing des produits (page `index`)
* Si les validations ne passent pas, on retourne le formulaire pré-rempli pour que l’utilisateur puisse le modifier avec des données valides avant de le soumettre à nouveau.

Validations et Seed

Une Seed est un fichier ruby qui permet de peupler la BDD de données d’exemple, utile lors du développement. Il est present dans db/seeds.rb. Il permet d’ecrire un scenario en base.

#Detruire tout ce qui est en base
Product.destroy_all
User.destroy_all

vegeta = User.create!(email: "vegeta@vegeta.com", password: "vegetavegeta")
jiren = User.create!(email: "jiren@jiren.com", password: "jirenjiren")

#puis on créé des seed, le "!" permet de lever une exception si la seed ne passe pas a cause d'une validation par exemple
Product.create!(user: jiren, name: "Kudoz", url: "http://www.site.com", tagline: "tinder for job search", category: "tech")
Product.create!(user: jiren, name: "kamehameha", url: "http://www.kamehouse.com", tagline: "atk of goku", category: "education")
Product.create!(user: vegeta, name: "shumpo", url: "http://www.fly.com", tagline: "fast run", category: "design")

Pour executer la seed taper dans le terminal rails db:seed puis le code sera executé (effacement et creation de la seed)

Il est aussi possible de faire du scraping puis alimenter le bdd avec ce qu’on a récupéré.

sauver les données écrit sur les champs même si la page est rechargé

Il suffit d’utiliser render puis appeler le template new, les donnes sont déja implémenté dans l’attribut @product precedement:

def create
    @product = Product.new(product_params)
    #si il a respecté les champs obligatoires on save et on redirige sur la page des produits
    if @product.save
      redirect_to products_path
    else
      #sinon renvoie le template "new" avec ce que l'user a écrit
      #vu que l'attribut @product a été déja instancié alors les données sont toujours présentes
      render :new
    end
  end

COURS N13

Les actions edit et update

Pour éditer un produit existant en base de données, cela se fait en deux étapes comme pour la création :

* L’action `edit` sert à afficher le formulaire **pré-rempli** permettant à l’utilisateur de changer certains champs.
* En soumettant ce formulaire, on fait une requête de type `PATCH` qui va appeler la méthode update` et modifier les informations en base de données.

Regardons le routing qui correspond à ces deux actions :

Prefix        Verb    URI Pattern           Controller#Action
edit_product  GET     /products/:id/edit    products#edit
              PATCH   /products/:id         products#update

Que ce soit pour afficher le formulaire pré-rempli ou pour modifier le produit en base données, on a d’abord besoin de récupérer son identifiant dans l’URL pour aller le chercher. Les deux requêtes correspondant aux actions ont donc toutes les deux un :id dans l’URL et le code des deux actions démarrent de la même façon :

class ProductsController < ApplicationController
  def edit
    @product = Product.find(params[:id])
  end
  def update
    @product = Product.find(params[:id])
     # ...
  end
end

simple_form_for est malin comme un singe !

On peut utiliser la méthode simple_form_for soit avec un nouveau produit @product = Product.new soit avec un produit qu’on est allé chercher en base @product = Product.find(params[:id]). Dans les deux cas, le code du formulaire sera le même :

<%= simple_form_for @product do |f| %>
  <%= f.input :name %>
  <%= f.input :url %>
  <%= f.input :tagline %>
  <%= f.button :submit %>
<% end %>
* Si le produit est nouveau (`@product = Product.new`), **Simple Form construira un formulaire HTML vierge**.
* Si le produit existe déjà (`@product = Product.find(params[:id])`), `Simple Form construira un formulaire HTML pré-rempli` avec les informations actuelles du produit en question.

Vu que Simple Form est si malin, le code du formulaire edit.html.erb sera exactement le même que celui du formulaire new.html.erb. Bon, vous pouvez quand même changer les titre <h1> des deux templates !

L’action update

L’action update va ensuite être très proche du create :

class ProductsController < ApplicationController
  def update
    @product = Product.find(params[:id]) # 1
    if @product.update(product_params)   # 2
      redirect_to products_path          # 3
    else
      render :edit                       # 4
    end
  end

  private
  def product_params
    params.require(:product).permit(:name, :url, :tagline)
  end
end

Décrivons ce qu’elle fait :

1. Elle va chercher le produit qu’on veut modifier à partir de son `:id` récupéré dans l’URL.
2. Elle essaie de l’updater avec des paramètres filtrés par la méthode `product_params` (pour se protéger contre un hack)
3. Si les validations passent, elle update le produit en base et on redirige vers le listing des produits (page `index`)
4. Si les validations ne passent pas, elle retourne le formulaire d’édition pré-rempli pour que l’utilisateur puisse le modifier avec des données valides.

L’action destroy

Le routing de l’action destroy est le suivant :

Prefix    Verb    URI Pattern    Controller#Action
          DELETE  /products/:id  products#destroy

Là encore, on a besoin de l’:id dans l’URL pour aller chercher le produit qu’on veut supprimer. Le code de l’action destroy est ensuite très simple.

class ProductsController < ApplicationController
  def destroy
    @product = Product.find(params[:id])  # on va chercher le produit
    @product.destroy                      # on le supprimer
    redirect_to products_path             # on redirige vers index
  end
end

Remarquons ici que les URLs des actions show et destroy sont les mêmes :

Prefix    Verb    URI Pattern    Controller#Action
product   GET     /products/:id  products#show
          DELETE  /products/:id  products#destroy

C’est bien pour ça qu’on a une seule méthode product_path pour générer l’URL /products/:id. Si on veut un lien qui fait bien une requête DELETE (et non une requête GET) dans nos templates, on doit ajouter l’option method: :delete lorsqu’on appelle la méthode link_to. Regardez bien la différence entre les deux liens suivants :

<%= link_to "Aller voir le produit", product_path(@product) %>
<%= link_to "Supprimer le produit", product_path(@product), method: :delete %>
* Le premier lien fait une requête `GET` sur l’URL `product_path(@product)`, c’est donc un lien classique vers la page `show` qui détaille du produit.
* Le second lien fait une requête `DELETE` **sur la même URL**. D’après notre routing, cette requête va être traitée par l’action `destroy` qui va supprimer le produit de la base de données.

“Refacto” de son code

Maintenant qu’on a codé toutes les actions du ProductsController et les vues associées, il est temps de faire une “refacto” de son code, c’est-à-dire factoriser les morceaux de code qui sont répétés.

Filtre before_action dans son controller

Dans beaucoup d’action on a besoin d’aller chercher le produit à partir de l’:id récupéré dans l’URL :

class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])
    # ...
  end
  def edit
    @product = Product.find(params[:id])
    # ...
  end
  def update
    @product = Product.find(params[:id])
    # ...
  end
  def destroy
    @product = Product.find(params[:id])
    # ...
  end
end

Pour ne pas répéter le code @product = Product.find(params[:id]) on peut le mettre dans une méthode privée find_product qui sera appelé avant les actions qui en ont besoin grâce au filtre Rails before_action:

class ProductsController < ApplicationController
  before_action :find_product, only: [:show, :edit, :update, :destroy]
  def show
    # ...
  end
  def edit
    # ...
  end
  def update
    # ...
  end
  def destroy
    # ...
  end
  private
  def find_product
    @product = Product.find(params[:id])
  end
end

On a gagné quelques lignes avec cette méthode. Quand vous avez un gros code de controlleur, quelques lignes ça compte !

Factoriser ses formulaires avec une partielle ERB

On a exactement le même code pour le formulaire simple_form dans les templates new.html.erb et edit.html.erb.

<%= simple_form_for @product do |f| %>
  <%= f.input :name %>
  <%= f.input :url %>
  <%= f.input :tagline %>
  <%= f.button :submit %>
<% end %>

C’est un peu idiot car si on veut enrichir ce formulaire (par exemple en ajoutant un input), on devra le faire à deux endroits. Heureusement, ERB nous permet de définir des vues partielles qui sont des fichiers ERB qui commence par un _ et qu’on peut injecter dans d’autres templates. Ici ça donne :

<!-- products/new.html.erb -->
<h1>Créer un produit</h1>
<%= render "form" %>
<!-- products/edit.html.erb -->
<h1>Editer le produit</h1>
<%= render "form" %>
<!-- products/_form.html.erb -->
<%= simple_form_for @product do |f| %>
  <%= f.input :name %>
  <%= f.input :url %>
  <%= f.input :tagline %>
  <%= f.button :submit %>
<% end %>

On a mis le code du formulaire à un seul endroit, dans la partielle ERB _form.html.erb qui est ensuite appelée dans les deux templates new.html.erb et edit.html.erb grâce à la méthode render.

COURS N14

Il faut prendre l’habitude de séparer tous composant CSS (avatar, button, banner…) dans un fichier à part puis les regrouper dans ‘style.css’ via des @import url(components/avatar.css).

on peut aussi definir la police pour le body et les titres dans ‘style.css’


https://placeholder.com/ ou https://unsplash.com permet de pointer vers des images en ligne tres utile lors du prototypage. exemple:

<img src:"http://placehold.it/50x50" alt="">
<img src:"http://unsplash.it/400/300/?random" alt=""> #le random n'est pas obligatoire

Pour imposer un style on utilise “!important”:

.dropdown-menu a{
  color: black !important
}

Les filtres sur les images (gradient filter):

background: linear-gradient(angle,
    start_color start_point,
    end_color end_point),
  url("bckground.jpg")

position relative: positionner un objet puis dans cette objet nous pouvons potionner des sous-objets en Position Absolu en fonction de son parent.

Flexbox, permet de faciliter l’alignement verticale/horizontale d’elements qui n’ont pas la meme taille dans une div:

.flex{
  display: flex;
}
.flex-item{
  flex: 0 0 200px;
}

unité en css3 le “vh” à voir (utilisé dans les banner fullscreen)

voir le plugin emmet qui permet de faire:

(li>a)*3

tout ceci dans la video et lien:

(en gros, soit faire tt de a-z soit se baser sur par exemple bootstrap puis customiser)

voir le framework de frontend : “middleman” (http://samuelbourdon.com/couicstart-template-middleman/)

pour les newsletter:

customiser les carte googlemap avec https://snazzymaps.com/

console JS en ligne https://jsbin.com/

Pipette Sip sur mac ou extension Chrome ColorZilla Plugin Chrome FontFace Ninja Sketch (free trial) sur mac ou inkscape

Voici les trois packages qu’on installe sur SublimeText3

* Emmet
* ColorPicker
* ColorHighlighter

Emmet

Créez un fichier index.html dans Sublime text et entrainez-vous avec Emmet, par exemple, tapez

    h1+h2+img+p+a puis tab
    ul>li*3 puis tab
    ul>(li>a)*3 puis tab
    div#wrapper>div.container>h2 puis tab
    a.btn.btn-primary puis tab
    div.container>div.row>(div.col-xs-9+div.col-xs-3) puis tab

Color Picker

Créez un fichier style.css dans Sublime text et entrâinez-vous avec Color Picker, par exemple, écrivez

h1 {
  color: /* Cmd + Shift + C */;
}

Pour choisir une couleur avec color picker, entrez Cmd + Shift + C ou Ctrl + Shift + C (sur windows). Cela fait apparaître une palette de couleur dans laquelle vous pouvez piocher, et ça injecte le code hexadecimal directement dans votre CSS, comme par magie !

Color Highlighter

Si ColorHighlighter est bien installé, vous devez voir la couleur correspondant à un code hexadecimal dans le CSS lorsque vous cliquez sur ce code hexadecimal (cf. image ci-dessous). Très pratique vu qu’il est assez difficile de connaître par coeur tous les codes hexadecimaux !

EXEMPLE DE SITES TRES BEAU: https://www.awwwards.com/websites/sites_of_the_day/

On peut heberger les image des sitesweb sur https://aws.amazon.com/fr/cloudfront/


Le layout de l’application

Depuis le début de cette track, on ne code jamais le squelette HTML de nos pages lorsqu’on ajoute des templates (comme show.html.erb, index.html.erb, edit.html.erb, etc..). En effet, tous ces templates s’injectent dans un fichier bien particulier layout/application.html.erb qu’on appelle le layout.

Le layout est la structure commune partagée par toutes les pages de notre site et ressemble à ça :

<!-- layout/application.html.erb -->
<html>
  <head>
    <!-- le contenu du head -->
  </head>
  <body>
    <!-- le code de la navbar -->
    <%= yield %>
    <!-- le code du footer -->
  </body>
</html>

Le contenu de chaque template (show.html.erb, index.html.erb, edit.html.erb, etc..) va venir s’injecter dans le layout au niveau du mot-clef yield.

Partielles partagées

Pour éviter de “noyer son layout” avec 50 lignes de code pour sa navbar, 30 lignes de code pour son footer, 30 lignes pour ses scripts d’analytics (Google Analytics ou autre), une bonne pratique consiste à écrire ces différents codes dans des partielles ERB. Voici ce que ça donne :

<!-- layout/application.html.erb -->
<html>
  <head>
    <!-- le contenu du head -->
  </head>
  <body>
    <%= render "shared/navbar" %>
    <%= yield %>
    <%= render "shared/footer" %>
    <%= render "shared/analytics" %>
  </body>
</html>

Ensuite, vous devez créer un dossier shared dans votre dossier views, et y mettre vos différentes partielles ERB :

<!-- shared/_navbar.html.erb -->
<div class="navbar">
  <!-- etc.. -->
</div>
<!-- shared/_footer.html.erb -->
<div class="footer">
  <!-- etc.. -->
</div>
<!-- shared/_analytics.html.erb -->
<script>
  <!-- Votre code de tracking Google Analytics ou autre (pixel facebook, code Mixpanel, etc..) -->
</script>

Raisonner par composant web

Pour organiser votre CSS et être très performant en frontend, on vous recommande vivement de “raisonner par composant”. Deux petits cadeaux pour vous mettre à niveau :

* La [librairie](http://lewagon.github.io/ui-components) de composant graphiques du vagon qu’on utilise dans la vidéo.
* Le [workshop](https://www.youtube.com/watch?v=ewxMpl09OwE) Youtube Web-components qui recode tous les composants de cette librairie.

Eplacement des images

les images doivent etre placé dans app/assets/images

Pour injecter avec ERB l’url d’une image avec image_path :

<%= image_path NomImage.jpg %>
#exemple:
<div class="banner" style="background-image: linear-gradient(-225deg, rgba(0,101,168,0.6) 0%, rgba(0,36,61,0.6) 50%), url('<%= image_path "banner-home.jpg" %>');">

Pour generer une image avec ERB on utilise image_tag ceci genere l’url et la balise <img>:

<%= image_tag monImage.png %>

Le CSS se trouve dans app/assets/stylesheets.

Pour la page Home nous allons ajouter un composant CSS soit une partial sass dans app/assets/stylesheets/components.

Il suffit de créér un fichier _banner.scss dans le dossier en question puis importer la partial dans le fichier _index.scss du même repertoire qui fait office de “main principale” comme ceci @import "banner";.

Faire un lien icon:

<!-- HELPER : le product path de chaque produit, le lien GET /products/:id, ici nous n'avons pas de text et on utilise "do...end" -->
        <li>
          <%= link_to product_path(product) do %>
            <i class="fa fa-eye"></i>
          <% end %>
          </li>

COURS N15

Travailler sur une nouvelle user story

Dans cette video, on travaille sur une nouvelle user story.

En tant que visiteur, je peux consulter des produits filtrés par catégorie.

Implémenter une user story sur un site existant demande au développeur d’être rigoureux et de ne rien oublier depuis la base de données jusqu’au templates ERB. Voici les bonnes questions à se poser, dans le bon ordre :

Cette user story necessite-t-elle de changer le schéma de la DB sur http://db.lewagon.org.
Si oui, faut-il générer un nouveau modèle (et donc une nouvelle table) ou juste pour modifier une table existante grâce à une migration ? Doit-on ajouter des validations à nos modèles ?
Faut-il de nouvelles routes ? Dans l’exemple de la video, on n’a pas besoin de nouvelles routes (ce n’est pas systématique).
Que doit-on modifier dans les actions du controlleur ? Y-a-t-il de nouveaux strong params à autoriser ?
Enfin, quelles informations doit-on ajouter aux vues (nouvelles données, nouvelles navigations, nouveau boutons, etc..)

En résumé, vous devez passer par toutes les couches de Rails quand vous implémenter une nouvelle fonctionnalité:

DB
modèle
routing
controller
view

Avoir une bonne méthodologie de travail prend du temps et c’est normal si vous vous emmêlez un peu les pinceaux au début (est-ce que je dois coder dans le routing ? Dans les controllers ? Dans les vues ?). En essayant d’être rigoureux et en vous posant les bonnes questions sur toute la stack Rails depuis la base de données jusqu’aux template HTML, vous allez progresser vite !


Pour ajouter la colone “category” à une table qui existe déjà “Products” on fait:

rails g migration AddCategoryToProducts category:string
#puis on lance la migration
rails db:migrate

Dans db/migrate nous avons:

class AddCategoryToProducts < ActiveRecord::Migration[5.1]
  def change
    add_column :products, :category, :string
  end
end

Puis dans app/models/product.rb on ajoute une validation sur la catégorie:

validates :category, inclusion: { in: %w(tech education design),
    message: "%{value} is not a valid category." }

Dans le routing, controller puis vue…

Dans le controller, on ajoute “category”:

private
    def product_params
      #require permet de garder que ce que contient product puis on filtre avec permit
      params.require(:product).permit(:name, :url, :tagline, :category)
    end

dans la vue “simple_form”:

<%= f.input :category, collection: ["tech", "education", "design"], prompt: quelle categorie?  %>

dans le css, voir l’usage de “$” car la couleur est plus designé:

.card-category.design {
  background: $blue;
}

enfin modifier products_controller.rb:

def index
  if params[:category]
      @products = Product.where(category: params[:category])
    else
      @products = Product.all
    end
end

Dans le home.html.erb, si on veut avoir un bouton pour se diriger vers la categorie “tech”:

<div class="col-xs-12 col-sm-4">
  <%= link_to "tech", products_path(category: "tech"), class: "card-category tech" %>
</div>

COURS N16

Nous alons déployer notre application Rails sur Heroku, un des leaders du marché des PaaS (Platform as a Service). L’idée est d’utiliser une solution clé en main pour ne pas avoir à se soucier de monter soi-même son serveur.

Prise en main

Le tutoriel de la vidéo commence à cette adresse, je vous invite donc à suivre les étapes au fur et à mesure.

Installation

Sur Mac, lancez simplement dans le terminal :

brew install heroku

Sur Cloud9, lancez la commande :

wget -O- https://toolbelt.heroku.com/install-ubuntu.sh | sh

Connexion via le terminal

heroku login

#pour voir avec quel compte on est logué
heroku auth:whoami

#pour se deconnecter
heroku logout

Création d’une application

Pour créer une application Heroku sur le datacenter européen, la commande à lancer (dans le dossier racine de votre application Rails) est :

heroku create --region=eu <le_nom_de_votre_app>

appli créé:

https://vhunt.herokuapp.com/ | https://git.heroku.com/vhunt.git

Déploiement

Lister les remotes sur lequel on peut envoyer notre code:

git remote -v

Pour déployer sur Heroku, nous allons à nouveau utiliser git, mais cette fois nous allons pousser sur une autre remote que origin :

git push heroku master

Durant le push, heroku fait un rake assets:precompile qui permet de minifier les fichiers JS, CSS, images, etc et les met dans un seul fichier.

Si on a pas été assez rapide entre le git push et le db:migrate faire un heroku restart

Dans la foulée, il ne faut pas oublier de lancer les migrations sur le schéma de données de production :

heroku run rails db:migrate

Pour ouvrir une page web sur le site directement:

heroku open

Logs

Pour observer les logs de l’application sur Heroku en cas d’erreur 500, lancez la commande :

heroku logs

Si vous voulez suivre en direct les logs au fur et à mesure des requêtes, ajoutez l’argument --tail :

heroku logs --tail

Rails console

Vous pouvez lancer une Rails console en production avec la commande :

heroku run rails c

Très pratique pour exécuter rapidement quelques requêtes ActiveRecord comme par exemple :

Product.count.inspect
Product.count

ou encore :

Product.last.inspect
p = Product.last
#on peut le supprimer par exemple
p.destroy

Faîtes attention si vous utilisez destroy ou même destroy_all !

Prix

Je n’en parle pas dans la vidéo, mais c’est quelque chose d’important. Heroku est gratuit dans des conditions de développement. Il y a un concept de “dyno” qu’on peut apparenter à une unité de serveur. Dans le plan gratuit, vous avez le droit à un dyno gratuit qui va pouvoir tourner jusque 18 heures par période de 24 heures. Lorsque votre application ne reçoit pas de requêtes HTTP (pas de visiteurs en gros), elle va dormir, et il faut qu’elle dorme au moins 6 heures par jour.

Si vous voulez que votre application soit tout le temps active, il faudra commencer à payer. Le plan Hobby à 7$ / mois est plutôt intéressant et avec un bon ROI sachant que vous n’avez aucun travail d’administration système à fournir.

Plus tard, pour une application Rails de production, il faudra passer à des plans plus chers, mais bon, il n’y a pas de free lunch ! Pour la petite histoire, l’hébergement de la plateforme Le vagon On Demand (sur laquelle vous êtes actuellement)sur Heroku coûte actuellement 96$ / mois.

Je peux vous assurer être très content de payer pour la tranquilité d’esprit de ne pas à avoir à gérer moi-même le serveur 24h/24, 7j/7 !

COURS N17

Utilisateurs et système d’authentification

La gem devise va vous permettre:

* D’ajouter un Sign in et un Sign up à votre application.
* Votre base de données va donc s’enrichir d’un nouveau modèle, `User`.

Devise

Dans la suite de ces instructions, nous supposons que vous n’avez pas encore généré de modèle User.

Installation

Ouvrez votre Gemfile et ajoutez la ligne suivante :

gem 'devise', '4.0.0.rc2'

Sauvegardez le fichier dans Sublime Text, ensuite dans le terminal lancez :

bundle install

Lancez ensuite la commande :

rails generate devise:install

Pour générer le modèle User, lancez les commandes :

rails generate devise user
rails db:migrate

Routes

Lancez un rails routes en ligne de commande pour voir les nouvelles routes installées. Ce qui nous intéresse, ce sont les trois suivantes :

new_user_registration GET    /users/sign_up(.:format)       devise/registrations#new
     new_user_session GET    /users/sign_in(.:format)       devise/sessions#new
 destroy_user_session DELETE /users/sign_out(.:format)      devise/sessions#destroy

Ainsi, vous pouvez aller sur localhost:3000/users/sign_up pour vous créer un compte puis sur localhost:3000/users/sign_in pour vous connecter.

Côté vue

Un helper bien utile va vous permettre de modifier votre barre de navigation afin de la rendre plus intelligente, c’est-à-dire qu’elle va afficher de l’information conditionnelement au fait qu’un utilisateur est connecté ou non. Ce helper est :

user_signed_in?

puis dans la navbar par exemple:

<% if user_signed_in? %>
//afficher ce qu'il faut
<% else %>
//ne rien afficher
<% end %>

Pour rendre les messages flash plus jolis, n’hésitez pas à utiliser ce gist:

#_flashes.html.erb
<!-- This file is in `app/views/shared/_flashes.html.erb` -->

<% if notice %>
  <div class="alert alert-info alert-dismissible" role="alert">
    <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
    <%= notice %>
  </div>
<% end %>
<% if alert %>
  <div class="alert alert-warning alert-dismissible" role="alert">
    <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
    <%= alert %>
  </div>
<% end %>


#application.html.erb
<!-- This file is in `app/views/layouts/application.html.erb` -->

<%# [...] %>

<%= render 'shared/flashes' %>

<%# [...] %>

Pour modifier les écrans de Sign in et Sign up par défaut de Devise (qui sont très “bruts de décoffrage”…), il faut d’abord importer ces vues dans notre propre projet par la commande :

rails generate devise:views

Côté controlleur

La politique la plus safe à utiliser est celle de la liste blanche. Par défaut, on interdit l’accès à toutes les pages pour les utilisateurs non connectés, et on ouvre certains accès uniquement.

Cela se passe dans le fichier app/controllers/application.controller.rb en ajoutant la ligne :

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  # [...]
  before_action :authenticate_user!
end

Maintenant, dans certains controlleurs, on peut décider que certaines actions soient accessibles pour un utilisateur non connecté. Par exemple, si on souhaite que la route /about, gérée par PagesController#about, on aura :

# app/controllers/pages_controller.rb
class PagesController < ApplicationController
  skip_before_action :authenticate_user!, only: [ :about ]

  # [...]
end

Le only: est là pour préciser un Array d’actions sur lesquelles le skip_before_action s’applique. Si il n’y a pas de only:, alors le skip_before_action s’applique à toutes les actions du controlleur.

Impact sur le schéma

user_id dans products

Pour l’instant, un produit est créé sans être relié à un utilisateur en particulier. Nous voulons donc conserver en base de données une trace du créateur du produit. Pour cela nous allons ajouter une clé étrangère user_id dans la table products

3 Tables dans la BDD soit USERS[id, name, email, password], UPVOTES[id, user_id, product_id] et PRODUCTS[id, name, tagline, url, user_id, category]

Pour ajouter cette colonne, on va générer la migration suivante :

#Ajout d'une clé étrangere
rails generate migration AddUserToProducts user:references

Vérifiez la migration automatiquement générée qui devrait compoter la ligne :

add_reference :products, :user, foreign_key: true

que vous pouvez ensuite jouer avec la commande habituelle :

rails db:migrate

N’oubliez pas de mettre à jour vos deux modèles :

# app/models/product.rb
class Product < ApplicationRecord
  belongs_to :user

  # [...]
end
# app/models/user.rb
class User < ApplicationRecord
  has_many :products

  # [...]
end

Côté controlleur

Au niveau du contrôlleur, on va utiliser le helper de Devise current_user pour associer automatiquement le produit nouvellement créé avec l’utilisateur actuellement connecté.

# app/controllers/products_controller.rb
class ProductsController
  # [...]
  def create
    @product = Product.new(product_params)
    @product.user = current_user
    # [...]
  end
end

Exercice

Comme dit dans la vidéo, comment pourriez-vous modifier le code de la méthode #destroy du ProductsController pour que seul le créateur du produit puisse le supprimer (même question pour l’édition !). Discutons-en sur le forum :)


Pour afficher toutes les erreurs lors du dev:

#dans un fichier.html.erb
<%= @product.errors.full_messages %>

Coté Back Office

Le Saas Forest Admin

Utiliser le SaaS Forest Admin qui demande beaucoup moins d’effort de code, notamment pour la connexion avec des services tiers (Mailchimp, Stripe, etc.)

Gem rails_admin

Pour faire simple, RailsAdmin est clé en main, il n’y a rien à faire (à part le protéger bien sûr derrière un login admin!) et tous les modèles. C’est souvent celui-là que j’utilise quand je veux aller vide et qu’il n’y a que moi qui y ait accès.

Si je veux donner un back-office à un client, je vais me pencher sur ActiveAdmin car il a une logique de n’exposer que des modèles spécifiques. Ainsi on ne noie pas le client dans une multitudes de tables.

Il faut protéger Rails Admin à des gens qui ne sont qu’admin en fait. Le plus simple:

  1. Générer une migration sur User
rails g migration AddAdminToUsers admin:boolean
rails db:migrate
  1. Protéger la route de Rails Admin
# in config/initializer/rails_admin.rb

RailsAdmin.config do |config|
  config.authorize_with do |controller|
    redirect_to main_app.root_path unless current_user && current_user.admin
  end

  # [...]
end
  1. Passer un utilisateur admin
rails c
irb> user = User.find_by_email('leboss@laboite.com')
irb> user.admin = true
irb> user.save

C’est le setup standard de Rails Admin qu’on conseille aux élèves du vagon. ça nécessite bien entendu d’avoir Devise installé auparavant.

Ensuite pour gérer plus finement les permissions, plutôt que de parsemer les contrôleurs et les vues de if, on utilise des gems d’autorisations comme cancancan ou pundit.

COURS N18

Cloudinary

Cloudinary est un service de stockage d’image dans le cloud. Il permet également d’automatiser des traitements classiques lorsqu’on manipule des images dans un contexte de site web, à savoir les redimensionnements et compressions d’images.

En effet, il faut que les images pèsent le moins possible (largeur x hauteur mais aussi également compression JPEG) pour que le site se charge vite. C’est encore plus vrai sur mobile.

Créez-vous un compte (avec le plan gratuit) avant de passer à la suite.

Aperté : Nous ne pouvons pas utiliser Heroku comme espace de stockage des images car Heroku ne possède pas de système de stockage permanent. En gros, à chaque déploiement git push heroku master, tout le disque dur du serveur est effacé. D’où la necessité d’un service tiers comme Cloudinary. Pour en savoir plus, lisez ceci à propos du ephemeral filesystem

Installation dans Rails

La gem figaro

Cette gem vous permet de protéger les secrets nécessaires à l’utilisation de services tiers. Le principe de cette gem est de stocker dans un fichier config/application.yml toutes les informations sensibles, et d’ajouter au fichier .gitignore ce fichier pour qu’il ne se retrouve pas sur GitHub.

Si vous avez utilisé le template du vagon pour générer votre application, vous n’avez rien à faire. Si vous avez fait un simple rails new, alors suivez le README de la gem.

Instalation:

gem "figaro"
bundle exec figaro install

Installation de la gem Cloudinary

Dans notre gemfile, mettre cette ligne:

gem "cloudinary"

Puis faire un bundle install.

Configuration

Dans votre fichier config/application.yml, ajoutez la ligne suivante en mettant votre URL cloudinary recupérable via le Dashboard en ajoutant les : (deux points espace) et les "" (guillemets) :

CLOUDINARY_URL: "cloudinary://..................."

Upload d’une image en console

Téléchargez une image, mettez-là à la racine de votre application web. Ensuite, lancez une Rails console (avec rails c) puis :

Cloudinary::Uploader.upload('votre_image.jpg')

En retour de cette commande nous avons :

=> {"public_id"=>"fhl0u2gh59e5qblfvarj",
 "version"=>1124955037,
 "signature"=>"etc42f603e5b541cde7c934f5a09f0dc335058af",
 "width"=>600,
 "height"=>400,
 "format"=>"jpg",
 "resource_type"=>"image",
 "created_at"=>"2018-04-28T22:37:17Z",
 "tags"=>[],
 "bytes"=>37075,
 "type"=>"upload",
 "etag"=>"c0100f9d1d3839fgda1ee148f5dcb86e",
 "placeholder"=>false,
 "url"=>"http://res.cloudinary.com/blabla/image/upload/v1521955017/ffl0u2gh59e5qblfyarj.jpg",
 "secure_url"=>"https://res.cloudinary.com/blabla/image/upload/v1521955017/ffl0u2gh59e5qblfyarj.jpg",
 "original_filename"=>"votre_image"}

Nous avons maintenant l’url permettant d’afficher l’image dans nos vues et nous avons aussi le public_id qui est un identifiant unique pour notre image, c’est cette variable qu’il faut sauvegarder en BDD pour faire réference à cette image plus tard.

Affichage de l’image dans une page statique

Pour finir de tester que tout s’est bien passé, on peut rapidement tester sur la page Team en ajoutant dans la vue la ligne suivante, la méthode cl_image_tag de la gem cloudinary :

<!-- app/views/pages/team.html.erb -->
<%= cl_image_tag "PUBLIC_ID_DE_L_IMAGE" %>

Ensuite on peut manipuler l’image pour changer sa taille par exemple :

<%= cl_image_tag "PUBLIC_ID_DE_L_IMAGE", width: 400, height: 100, crop: :fill %>

Je vous invite à lire la doc Image transformations pour avoir une idée des transformations à votre disposition. Ensuite, vous pouvez lire cette doc Rails image manipulation qui est spécifique à Rails et à l’utilisation de cl_image_tag.

Upload d’image par l’utilisateur avec attachinary

Voici la procédure Attachinary Setup à suivre pour installer et configurer la gem. C’est la procédure que je suis dans la vidéo.


Attachinary Setup

First add the following gems to your Gemfile:

# Gemfile
gem "attachinary"
gem "jquery-fileupload-rails"
gem "coffee-rails"

Then open the terminal and launch:

bundle install
rails attachinary:install:migrations
rails db:migrate

Open your config/application.rb and add this line after all the require:

require "attachinary/orm/active_record"

Open the config/routes.rb file and add this as first line in the draw block:

mount Attachinary::Engine => "/attachinary"

Open app/views/layout/application.html.erb and append this line after the main javascript_include_tag:

<%= cloudinary_js_config %>

Open app/assets/javascripts/application.js and append these lines before the require_tree:

//= require jquery-fileupload/basic
//= require cloudinary/jquery.cloudinary
//= require attachinary

Create a file app/assets/javascripts/init_attachinary.js and copy-paste those lines:

$(document).ready(function() {
  $('.attachinary-input').attachinary();
});
Usage
One picture per model

You need to update the model:

class Product < ApplicationRecord
  has_attachment :photo

  # [...]
end

And the form (simple_form gem used):

<%= f.input :photo, as: :attachinary %>

And the controller for strong params:

def product_params
  params.require(:product).permit(:name, :description, :photo)
end

To display the product photo in the view, add (ceci genere la balise img etc):

<% if @product.photo? %>
  <%= cl_image_tag @product.photo.path %>
<% end %>

Pour recuperer que l’url de l’image

<%= cloudinary_url @product.photo.path, width: 200 %>

autres exemples :

#autre exemple:
<!-- on fait une condition avec la methode cloudinary qui fournit la balise img  -->
<% if product.photo? %>
  <%= cl_image_tag(product.photo.path, height: 117, width: 175, crop: :fill, class: 'product-image') %>
<% else %>
  <img src="http://unsplash.it/300/200?random" alt="kudoz" class="product-image hidden-xs">
<% end %>

#autre exemple:
<!-- Banner avec cloudinary_url qui permet d'avoir que l'url dans une condition ternaire-->
<div class="banner" style="background-image: linear-gradient(-225deg, rgba(0,101,168,0.6) 0%, rgba(0,36,61,0.6) 50%), url('<%= @product.photo? ? cloudinary_url(@product.photo.path, width: 1280, height: 700, crop: :fill) : image_path("banner-home.jpg") %>');">
Multiple pictures per model

You need to update the model:

class Product < ApplicationRecord
  has_attachments :photos, maximum: 2  # Be carefule with `s`

  # [...]
end

And the form (simple_form gem used):

<%= f.input :photos, as: :attachinary %>

And the controller for strong params:

def product_params
  params.require(:product).permit(:name, :description, photos: [])
end

To display the product photos in the view, add:

<% @product.photos.each do |photo| %>
  <%= cl_image_tag photo.path %>
<% end %>
Ajout d’un Avatar - Devise

(si l’img est en 30x30 on peut le mettre en 60x60 pour les rétina. puis forcer le 30x30 via CSS)

Il suffit de faire la même chose que dans le chapitre “One picture per model” Dans le model User il faut ajouter:

# app/models/user.rb
class User < applicationRecord
  #relation BDD, ajout d'un avatar
  has_attachment :avatar
  ...

Puis ajouter dans le formulaire de modification utilisateur généré par Devise qui a également généré la route (edit_user_registration GET /users/edit(.:format) devise/registrations#edit):

# app/views/devise/registrations/edit.html.erb
<%= f.input :avatar, as: :attachinary %>

Dans nos controllers nous avons rien qui correspond à Devise. Quand on va faire un PUT/PATCH on pointe sur le controller devise/registrations#updade qui est à l’interieur de la gem. Pour palier à ce probleme, il faut ajouter le code ci-dessous dans application_controller (le before_action et la methode).

If you want to add an avatar to the User model, you need to sanitize regarding the strong params :

# app/controllers/application_controller
class ApplicationController < ActionController::Base

  before_action :configure_permitted_parameters, if: :devise_controller?

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up,        keys: [:avatar])
    devise_parameter_sanitizer.permit(:account_update, keys: [:avatar])
  end
end

Enfin dans la navbar nous allons ajouter notre avatar :

<!-- si l'user actuel a un avatar on l'affiche sinon img default -->
<% if current_user.avatar? %>
  <%= cl_image_tag current_user.avatar.path, width: 50, height: 50, crop: :fill, class: 'avatar dropdown-toggle', id: 'navbar-wagon-menu', :'data-toggle' => 'dropdown' %>
<% else %>
  <img src="https://kitt.lewagon.com/placeholder/users/ssaunier" class="avatar dropdown-toggle" id="navbar-wagon-menu" data-toggle="dropdown">
<% end %>

Mise en production

Comme d’habitude, il va falloir faire un commit et le pousser sur Heroku (puis lancer les migrations). Mais avant ça, il y a une étape importante. Comme on a ajouté une ligne à notre fichier config/application.yml (et que ce fichier n’est pas dans git, donc pas envoyé à Heroku), il faut communiquer à Heroku la valeur de cette variable d’environnement.

Cela va se faire grâce à la ligne de commande suivante (en mettant bien évidemment votre propre clé récupérée sur votre dashboard)

DANS LA VIDEO C'EST UN ADD PAS UN SET
heroku config:set CLOUDINARY_URL=cloudinary://...................

COURS N19

Fonctionnalité d’upvote sur un produit

Marche à suivre lorsqu’on ajoute une nouvelle fonctionnalité :

Model -> Routing -> Controler -> Vue

FAIRE UNE TABLE DE JOINTURE QUAND ON A DES RELATION N:N EN BDD :

La fonctionnalité d’upvote du current_user pour un produit va nécessiter une table de jointure qui va stocker tous ces votes.

  ___upvotes{id,user_id,product_id}___
 |                                    |
 |                                    |
 users{id,name,email,password}______products{id,name,tagline,url,user_id,category}

Modèle

Pour générer le modèle Upvote, on a fait tourné cette commande :

rails g model upvote user:references product:references
rails db:migrate

On a le nouveau modèle suivant :

# app/models/upvote.rb
class Upvote < ApplicationRecord
  belongs_to :user
  belongs_to :product

# ici on dit qu'un user est unique dans la cadre d'un produit
  validates :user, uniqueness: { scope: :product }
end

Et il ne faut pas oublier d’enrichir les deux modèles User et Product existants :

# app/models/user.rb
class User < ApplicationRecord
  # [...]
  has_many :upvotes
end
# app/models/product.rb
class Product < ApplicationRecord
  # [...]
  has_many :upvotes
end

Nous allons ajouter des upvotes au scénario :

# db/seeds.rb
#Detruire tout ce qui est en base
Product.destroy_all
User.destroy_all

vegeta = User.create!(email: "vegeta@vegeta.com", password: "vegetavegeta")
jiren = User.create!(email: "jiren@jiren.com", password: "jirenjiren")

#puis on créé des seed, le "!" permet de lever une exception si la seed ne passe pas a cause d'une validation par exemple
Product.create!(user: jiren, name: "Kudoz", url: "http://www.site.com", tagline: "tinder for job search", category: "tech")
Product.create!(user: jiren, name: "kamehameha", url: "http://www.kamehouse.com", tagline: "atk of goku", category: "education")
Product.create!(user: vegeta, name: "shumpo", url: "http://www.fly.com", tagline: "fast run", category: "design")

# Upvotes? on peut faire de 2 façons
Upvote.create!(user: vegeta, product: kudoz)
# ou
kudoz = Product.create!(user: jiren, name: "Kudoz", url: "http://www.site.com", tagline: "tinder for job search", category: "tech")
kudoz.upvotes.create! user: vegeta

Puis detruire la BDD et la reconstruire :

rails db:drop db:create db:migrate db:seed

Routes

Pour voter et annuler son vote, on a besoin de deux routes :

# config/routes.rb
Rails.application.routes.draw do
  # [...]
  # on peut avoir plusieurs "resources" pas de probleme
  resources :upvotes, only: [ :create, :destroy ]
end

Contrôleur

Ces routes vont arriver dans le UpvotesController qu’on peut générer avec la commande suivante :

rails g controller upvotes

et du coup on a :

# app/controllers/upvotes_controller.rb
class UpvotesController < ApplicationController
  def create
    product = Product.find(params[:product_id])
    product.upvotes.create! user: current_user
    redirect_to products_path
  end

  def destroy
    upvote = Upvote.find(params[:id])
    upvote.destroy
    redirect_to products_path
  end
end

Vue

Dans la vue, on va afficher le compteur de vote, qui va avoir dépendre du fait que le current_user a voté pour le produit ou non.

Tout d’abord on a besoin d’un helper dans le modèle User :

# app/models/user.rb
class User < ApplicationRecord
  # [...]
  def voted_for?(product)
    product.upvotes.where(user: self).any?
  end
end

pour ensuite dans la vue index des produits avoir (on stipule la methode post car par defaut on utilise la methode GET) :

<!-- app/views/products/index.html.erb -->

<% if user_signed_in? %>
  <% if current_user.voted_for?(product) %>
    <%= link_to upvote_path(current_user.upvotes.where(product: product).first), method: :delete, class: 'product-upvote product-upvoted' do %>
      <div class="product-arrow"></div>
      <div class='product-count'>
        <%= product.upvotes.size %>
      </div>
    <% end %>
  <% else %>
    <%= link_to upvotes_path(product_id: product.id), method: :post, class: 'product-upvote' do %>
      <div class="product-arrow"></div>
      <div class='product-count'>
        <%= product.upvotes.size %>
      </div>
    <% end %>
  <% end %>

<% else %>
  <div class='product-upvote'>
    <div class="product-arrow"></div>
    <div class='product-count'>
      <%= product.upvotes.size %>
    </div>
  </div>
<% end %>

COURS N20

(utiliser l’inspecteur d’elements dans le navigateur pour voir la structure de la page, voir aussi l’onglet network et console pour debug)

Je vous invite à lire le code du commit de ce cours pour bien comprendre ce qui a été ajouté aux différentes parties du MVC pour rendre la page dynamique.

Je vous invite également à lire l’article de Rails Guide qui parle de JavaScript dans Rails. Les exemples de code sont en CoffeeScript, vous pouvez les “traduire” simplement avec le site js2.coffee


Contenue du commit de ce cours :

#app/assets/stylesheets/components/_product.scss
/* Upvote design */
.product-upvote {
+  display: inline-block;
  padding-right: 20px;
  text-align: center;
  transition: all 0.15s ease;
#app/controllers/upvotes_controller.rb
class UpvotesController < ApplicationController
  def create
    # Upvote?
  -  product = Product.find(params[:product_id])
  -  product.upvotes.create! user: current_user
  -  redirect_to products_path
  +  @product = Product.find(params[:product_id])
  +  @product.upvotes.create! user: current_user
  +  respond_to do |format|
  +    format.html { redirect_to products_path }
  +    format.js
  +  end
  end

  def destroy
    upvote = Upvote.find(params[:id])
  +  @product = upvote.product
    upvote.destroy
  -  redirect_to products_path
  +  respond_to do |format|
  +    format.html { redirect_to products_path }
  +    format.js
  +  end
  end
end
#app/views/products/index.html.erb
<div class="product">
=begin
          <% if user_signed_in? %>
            <%# Upvote action %>
            <% if current_user.voted_for?(product) %>
              <%= link_to upvote_path(current_user.upvotes.where(product: product).first), method: :delete, class: 'product-upvote product-upvoted' do %>
                <div class="product-arrow"></div>
                <div class='product-count'>
                  <%= product.upvotes.size %>
                </div>
              <% end %>
            <% else %>
              <%= link_to upvotes_path(product_id: product.id), method: :post, class: 'product-upvote' do %>
                <div class="product-arrow"></div>
                <div class='product-count'>
                  <%= product.upvotes.size %>
                </div>
              <% end %>
            <% end %>
          <% else %>
            <div class='product-upvote'>
              <div class="product-arrow"></div>
              <div class='product-count'>
                <%= product.upvotes.size %>
              </div>
            </div>
          <% end %>
=end
      +    <div class="upvote-container" id="product-<%= product.id %>">
      +      <%= render 'upvotes/show', product: product %>
      +    </div>

          <% if product.photo? %>
            <%= cl_image_tag(product.photo.path, height: 117, width: 175, crop: :fill, class: 'product-image') %>
#TT CE CODE FAIT PARTIE DU COMMIT

#app/views/upvotes/_show.html.erb
<% if user_signed_in? %>
  <% if current_user.voted_for?(product) %>
    <%= link_to upvote_path(current_user.upvotes.where(product: product).first), remote: true, method: :delete, class: 'product-upvote product-upvoted' do %>
      <div class="product-arrow"></div>
      <div class='product-count'>
        <%= product.upvotes.size %>
      </div>
    <% end %>
  <% else %>
    <%= link_to upvotes_path(product_id: product.id), remote: true, method: :post, class: 'product-upvote' do %>
      <div class="product-arrow"></div>
      <div class='product-count'>
        <%= product.upvotes.size %>
      </div>
    <% end %>
  <% end %>
<% else %>
  <div class='product-upvote'>
    <div class="product-arrow"></div>
    <div class='product-count'>
      <%= product.upvotes.size %>
    </div>
  </div>
<% end %>
#app/views/upvotes/create.js.erb
+ $('#product-<%= @product.id %>').html('<%= j render 'upvotes/show', product: @product %>');
#app/views/upvotes/destroy.js.erb
+ $('#product-<%= @product.id %>').html('<%= j render 'upvotes/show', product: @product %>');

Vue (d’où part la requête AJAX)

Pour rendre un lien dynamique et le forcer à faire partir une requête AJAX plutôt qu’une navigation classique de page, il faut utiliser remote: true :

#.html.erb
<%= link_to upvotes_path(product_id: product.id),
      remote: true,
      method: :post
    do %>
  <!-- [...] -->
<% end %>

Contrôleur

Lorsqu’on reçoit une requête AJAX dans un contôleur Rails, le format est js. Du coup, il faut gérer spécifiquement ce cas, tout en conservant le code pour gérer le cas où la requête n’est pas au format AJAX (on parle de JavaScript non-obstrusif) :

respond_to do |format|
  format.html { ... }
  format.js
end

Vue (de réponse AJAX)

Du coup, ça va demander d’avoir une vue spécifique, d’extension .js.erb. Dans cette vue, on va pouvoir utiliser les variables d’instances définies dans le contrôleur. Et surtout, ce qu’il faut comprendre, c’est qu’on n’écrit pas du HTML, on y écrit du JavaScript. Et donc du jQuery.

$('#some-element').html('<%= j render "some/partial" %>');

Ne pas y oublier le j, un alias du helper escape_javascript.

CONCLUSION

Resources

Rails Guide

Lisez et relisez la documentation Rails Guide, c’est une mine d’or et la réponse à votre question s’y trouve sans doute. Je vous conseille de lire tous les chapitres sur ActiveRecord comme un roman.

Vidéos

RailsCasts est un projet qui a beaucoup contribué à l’essor de Rails. Mis en sommeil en 2013 par son fondateur, la plupart des vidéos sont encore pertinentes.

GoRails est le successeur de RailsCasts. Beaucoup de vidéos récentes et à jour par rapport aux versions 4 et 5 de Rails.

Gems

Une des force de Rails est sa communauté open-source et les nombreuses gems à notre disposition pour prototyper vraiment très rapidement. Le repo awesome-rails fait une compilation des meilleurs gems dans plein de secteurs, allez fouiner !

Dans un autre style, Ruby Toolbox est très bien pour comparer deux gems d’un même domaine. Les critères à prendre en compte sont la popularité, la maintenance de la gem (date du dernier commit ?), etc. ce que fait Ruby Toolbox dans son calcul de score.

Add-ons

(pas obligé d’utiliser Heroku) Regardez du côté des Add-ons Heroku il y a plein de choses pour envoyer des emails, des SMS, intégrer de la recherche, etc.

Meetups

N’hésitez pas à venir à Paris.rb (http://www.meetup.com/fr-FR/parisrb/) qui fait un meetup tous les premiers mardis du mois, on y parle de Ruby et Rails. En province, il y a également des meetups nottamment Ruby Nord, Ruby Bordeaux ou encore Lyon.rb (et j’en passe).

Newsletter

Vous pouvez vous abonner à Ruby Weekly qui paraît chaque jeudi et qui est pas mal du tout pour suivre l’actu de la communauté Ruby.