[RUBY] Tutoriel Rails 6e édition Résumé de l'apprentissage Chapitre 9

Aperçu

Cet article approfondit ma compréhension en écrivant un article de commentaire du tutoriel Rails pour solidifier davantage mes connaissances Cela fait partie de mon étude. Dans de rares cas, un contenu ridicule ou un contenu incorrect peut être écrit. Notez s'il vous plaît. J'apprécierais que vous me disiez implicitement ...

La source Tutoriel Rails 6e édition

Que faire dans ce chapitre

-Ajouter une fonction pour stocker les informations de connexion à la discrétion de l'utilisateur et se connecter même si le navigateur est redémarré.

Souviens-toi de moi fonction

↑ Comme mentionné ci-dessus, implémentez la fonction pour conserver la connexion même si le navigateur est fermé (Remember me) Créez une branche thématique et commencez.

Jeton de stockage et chiffrement

Comme il est assez difficile de travailler et de créer à partir de maintenant, vérifiez les connaissances à l'avance.

・ Qu'est-ce qu'un jeton? C'est comme un mot de passe utilisé par un ordinateur. Les mots de passe sont créés par des humains et gérés par des humains, mais les jetons sont créés par des ordinateurs et gérés par des ordinateurs.

・ À propos des cookies persistants et des sessions temporaires Pour la session temporaire créée dans le chapitre précédent, la méthode de session a été utilisée pour créer une session dans les cookies dont la date d'expiration correspond à la fermeture du navigateur. Cette fois, nous utiliserons la méthode des cookies pour créer une session avec une date d'expiration infinie (pour être exact, environ 20 ans). Contrairement à la méthode de session, la méthode des cookies ne protège pas les informations et est la cible d'une attaque appelée détournement de session. En enregistrant l'ID utilisateur et le jeton de stockage sous forme d'ensemble dans les cookies et en enregistrant le jeton haché dans la base de données Assurer la sécurité.

・ Quel type de traitement est utilisé pour l'implémenter?

  1. Enregistrez l'ID utilisateur et le jeton de stockage cryptés à l'aide de la méthode des cookies dans le navigateur
  2. Le jeton de stockage haché (résumé de stockage) est stocké dans la base de données en même temps.
  3. Lors de votre prochain accès, comparez le jeton de cookies à durée limitée stocké dans le navigateur avec le résumé de stockage stocké dans la base de données. Le processus de connexion est effectué automatiquement.

J'ai vérifié le contenu à peu près Ajoutez immédiatement un résumé de la mémoire (Remember_digest) à la base de données.

string


 Comme expliqué précédemment, si vous ajoutez une colonne à la table users en ajoutant to_users à la fin du nom de fichier, elle sera reconnue sans autorisation.

 Comme Remember_digest n'est pas lisible par l'utilisateur, il n'est pas nécessaire d'ajouter un index.
 Par conséquent, il sera migré tel quel.

 Quoi utiliser pour créer un jeton de mémoire
 Les chaînes longues et aléatoires sont préférées.
 La méthode ```urlsafe_base64``` du module SecureRandom correspond à l'objectif, nous allons donc l'utiliser.
 Cette méthode utilise 64 types de caractères et renvoie une chaîne de caractères aléatoires de longueur 22.
 Le jeton de stockage sera automatiquement généré à l'aide de cette méthode.

```irb
>> SecureRandom.urlsafe_base64
=> "Rr2i4cNWOwhtDeVA4bnT2g"
>> SecureRandom.urlsafe_base64
=> "pQ86_IsKILLv4AxAnx9iHA"

Comme le mot de passe, le jeton peut être dupliqué avec d'autres utilisateurs⁻, mais en utilisant un unique À moins que l'ID utilisateur et le jeton ne soient volés, cela n'entraînera pas de piratage de session.

Définissez une méthode pour créer (générer) un nouveau jeton dans le modèle utilisateur.

  def User.new_token
    SecureRandom.urlsafe_base64
  end

Cette méthode ne nécessite pas non plus d'objet utilisateur, elle est donc définie comme une méthode de classe.

Ensuite, créez la méthode Remember. Cette méthode enregistre le résumé de stockage correspondant au jeton dans la base de données. Remember_digest existe dans DB, mais Remember_token n'existe pas. Je souhaite uniquement enregistrer le résumé dans la base de données, mais je veux enregistrer le résumé du jeton associé à l'objet utilisateur. Je souhaite également accéder à l'attribut de jeton. En d'autres termes, un jeton est requis comme attribut virtuel comme dans le cas d'un mot de passe. Has_secure_password a été généré automatiquement lorsque le mot de passe a été implémenté, mais cette fois attr_accessorN'oubliez pas d'utiliser_Créez un jeton.

user.rb


class User < ApplicationRecord
  attr_accessor :remember_token
  
  # before_save { self.email.downcase! }
  # has_secure_password
  # VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  # validates :name, presence: true, length:{maximum: 50}
  # validates :email, presence: true, length:{maximum: 255},
  #                   format: {with: VALID_EMAIL_REGEX},uniqueness: true
  # validates :password, presence: true, length:{minimum: 6}
  
  # def User.digest(string)
  #   cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
  #                                                 BCrypt::Engine::cost
  #   BCrypt::Password.create(string, cost: cost)
  # end
  
  # def User.new_token
  #   SecureRandom.urlsafe_base64
  # end
  
  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest,User.digest(remember_token))
  end
end

Première ligne de la méthode Remember

self.remember_token = User.new_le jeton est


 Obligatoire ici car si vous n'écrivez pas self, une variable locale appelée Remember_token sera créée.

 Puisque le mot de passe est inaccessible ici, update_attribute est utilisé pour contourner la validation.

##### Exercice
 1. Bougez fermement.
 Remember_token est une chaîne de 22 caractères générée aléatoirement
 Vous pouvez voir que Remember_digest est leur chaîne hachée.

```irb
>> user.remember
   (0.1ms)  begin transaction
  User Update (2.4ms)  UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ?  [["updated_at", "2020-06-17 14:30:27.202627"], ["remember_digest", "$2a$12$u8cnBDCQX85e9gBHbQWWNeJq.mxKq8hhpt/FSYMtm2rI2ljWIhRLi"], ["id", 1]]
   (6.1ms)  commit transaction
=> true
>> user.remember_token
=> "lZaXgeF42y5XeP-EEPzstw"
>> user.remember_digest
=> "$2a$12$u8cnBDCQX85e9gBHbQWWNeJq.mxKq8hhpt/FSYMtm2rI2ljWIhRLi"
>> 
  1. Les deux fonctionnent de la même manière class << selfSi vous utilisez, tout jusqu'à la fin est défini comme une méthode de classe. Notez que le mot-clé self représente ici la classe User elle-même, pas l'objet d'instance.

Rester connecté

Utilisez la méthode `` cookies '' pour enregistrer dans des cookies persistants. Il peut être utilisé comme une session de type hachage.

les cookies ont une valeur et expirent

cookies[:remember_token] =  { value: remember_token, expires: 20.years.from_now.utc }

Ce faisant, la valeur de Remember_token avec une date d'expiration de 20 ans peut être enregistrée dans les cookies [: Remember_token]. De plus, comme la date d'expiration de 20 ans est souvent utilisée, une méthode dédiée a été ajoutée à Rails.

cookies.permanent[:remember_token] = remember_token

Cela a le même effet.

De plus, l'ID utilisateur est enregistré dans les cookies persistants, mais si vous l'enregistrez tel quel, l'ID sera enregistré tel quel, Parce que l'attaquant sera confus quant au format dans lequel les cookies sont stockés. Crypter. Utilisez des cookies signés pour le cryptage.

cookies.signed[:user_id] = user.id


 Vous pouvez maintenant le crypter et l'enregistrer en toute sécurité.

 Bien sûr, l'ID utilisateur doit également être enregistré en tant que cookies persistants, utilisez-le donc en connectant la méthode permanente.

#### **`cookies.permanent.signed[:user_id] = user.id`**

En plaçant l'ID utilisateur et le jeton de mémoire dans les cookies comme un ensemble comme celui-ci Lorsqu'un utilisateur se déconnecte, il ne peut pas se connecter (car le condensé de base de données est supprimé)

Enfin, comment comparer le jeton stocké dans le navigateur avec le condensé de la base de données Une partie du code source de secure_password

BCrypt::Password.new(remember_digest) == remember_token


 Utilisez un code comme celui-ci.
 Ce code compare directement Remember_digest et Remember_token.
 En fait, Bcrypt a redéfini l'opérateur ==, et ce code

#### **`BCrypt::Password.new(remember_digest).is_password?(remember_token)`**

Il fonctionne. Utilisez ceci pour définir une méthode ```authenticated? '' `` Qui compare un résumé de stockage avec un jeton de stockage.

  def authenticated?(remember_token)
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

Remember_digest ici est identique à self.remember_digest. Compare le résumé de stockage de la base de données avec le jeton de stockage passé à l'argument et renvoie true s'il est correct

Ajoutez immédiatement le traitement de rappel à la partie de traitement de connexion de sessions_controller.

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user&.authenticate(params[:session][:password])
      log_in(user)
      remember user
      redirect_to user
    else
      flash.now[:danger] = "Invalid email/password combination"
      render 'new'
    end
  end

Ici, nous utilisons la méthode d'aide à la mémoire. (Pas encore défini)

↓ rappelez-vous de la méthode d'aide

sessions_helper.rb


  def remember(user) 
    user.remember
    cookies.signed.permanent[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember_token
  end

Supplément car il est difficile à comprendre. Avec la méthode Remember définie dans le modèle User

user.rb


  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest,User.digest(remember_token))
  end

Générez un jeton de mémoire et un résumé de mémoire pour un objet utilisateur.

Avec la méthode Remember définie dans sessions_helper

  1. Appelez la méthode Remember du modèle User pour générer des jetons et des résumés.
  2. Crypter et enregistrer l'ID utilisateur dans les cookies
  3. Enregistrez le token généré en 1 dans les cookies

Flux de. Notez que le nom de la méthode est couvert.

Vous pouvez désormais stocker en toute sécurité vos informations d'utilisateur dans des cookies, mais regardez votre statut de connexion La méthode `` utilisateur_actuel '' utilisée pour modifier dynamiquement la mise en page est uniquement pour les sessions temporaires Ce n'est pas pris en charge, alors corrigez-le.

  def current_user #Renvoie l'objet utilisateur actuellement connecté
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      if user &. authenticated?(cookies[remember_token])
        log_in user
        @current_user = user
      end
    end
  end

-La duplication de code est réduite en utilisant une variable locale appelée user_id. ・ Lorsque le navigateur est ouvert pour la première fois, les cookies persistants sont traités et le traitement de connexion est également effectué en même temps. L'utilisateur est stocké dans @current_user jusqu'à ce que le navigateur soit fermé.

Pour le moment, il n'y a aucun moyen de supprimer le processus de déconnexion (cookies persistants) Je ne peux pas me déconnecter. (L'action de déconnexion existante supprime uniquement la session temporaire, récupérez donc les informations à partir des cookies persistants Je ne peux pas me déconnecter car je me connecte automatiquement. )

Exercice
  1. Oui. image.png

  2. Cela fonctionne.

>> user = User.first
   (1.1ms)  SELECT sqlite_version(*)
  User Load (0.2ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "take", email: "[email protected]", created_at: "2020-06-14 02:57:10", updated_at: "2020-06-18 15:18:53", password_digest: [FILTERED], remember_digest: "$2a$12$tAZFCVr39lkPONLS4/7zneYgOE5pcYDM2kX6F1yKew2...">
>> user.remember
   (0.1ms)  begin transaction
  User Update (2.8ms)  UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ?  [["updated_at", "2020-06-18 15:23:21.357804"], ["remember_digest", "$2a$12$h3K3aZSBmXB7wGkNdsBrS.2/UaawMQ199DGMvTDU8upvvOKCzbeba"], ["id", 1]]
   (10.3ms)  commit transaction
=> true
>> user.authenticated?(user.remember_token)
=> true

Oubliez l'utilisateur

Je ne peux pas me déconnecter car je n'ai pas supprimé les cookies persistants. Définissez la méthode oublier '' pour résoudre ce problème. Définissez le résumé de la mémoire sur nul avec cette méthode. De plus, en définissant la méthode forget``` dans sessions_helper également Cela supprime également l'ID utilisateur et le jeton de stockage stockés dans les cookies.

  def forget(user) #Supprimer la session persistante / réinitialiser le résumé de la mémoire
    user.forget
    cookies.delete[:user_id]
    cookies.delete[:remember_token]
  end
  def log_out
    forget(current_user)
    session.delete(:user_id)
    @current_user = nil
  end

Passons en revue le flux de traitement de déconnexion.

  1. Définissez le résumé de stockage enregistré dans l'objet utilisateur sur nil (méthode forget du modèle utilisateur)
  2. Supprimez l'ID utilisateur et le jeton de mémoire des cookies (oubliez la méthode de sessions_helper)
  3. Supprimez l'ID utilisateur de la session temporaire
  4. Définissez l'utilisateur actuel (utilisateur actuellement connecté) sur nil.
Exercice
  1. Il a été supprimé. (L'écran d'exécution est omis) Dans Chrome, une session temporaire reste comme avant, mais en termes de fonctionnement de l'application aucun problème.

Deux bugs discrets

Pour le moment, il reste deux bugs. C'est assez gênant, je vais donc l'expliquer en détail.

Premier bug Lorsque vous êtes connecté sur plusieurs onglets, déconnectez-vous sur l'onglet 1, puis déconnectez-vous sur l'onglet 2. Après vous être déconnecté à l'aide de la méthode log_out dans l'onglet 1, current_user est nul. Si vous essayez de vous déconnecter à nouveau dans cet état, cela échouera car le cookie à supprimer est introuvable.

Deuxième bug Lorsque vous êtes connecté avec un autre navigateur (Chrome, Firefox, etc.).

  1. Remember_digest devient nul lors de la déconnexion dans Firefox.
  2. Lorsque Chrome est fermé, la session temporaire est supprimée mais les cookies restent, de sorte que l'utilisateur peut être trouvé par ID utilisateur.
  3. Parce que Remember_digest à comparer avec la méthode ```user.authenticated? `` `A déjà été supprimée du côté de Firefox. Il n'y a pas de cible de comparaison et une erreur se produit.

Pour corriger ce bogue, écrivez d'abord un test pour attraper le bogue Écrivez du code pour y remédier.

delete logout_path Reproduisez la déconnexion deux fois en l'insérant à nouveau après le processus de déconnexion du test de connexion.

Pour réussir ce test Vous ne devez vous déconnecter que lorsque vous êtes connecté.

  def destroy
    log_out if logged_in?
    redirect_to root_url
  end

Concernant le second bug, il est difficile de reproduire différents environnements de navigateur dans le test, donc Ne testez que Remember_digest dans le modèle User. Plus précisément, il teste qu'il renvoie false lorsque Remember_digest est nul.

  test "authenticated? should return false for a user with nil digest" do 
    assert_not @user.authenticated?('')
  end

Améliorer authentifié? Méthode pour réussir le test

  def authenticated?(remember_token)
    return false if remember_digest.nil?
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

Si Remember_digest vaut nil, renvoie immédiatement false avec le mot-clé return et met fin au processus.

Cela corrige deux bogues.

Exercice
  1. Une erreur se produit. (L'écran d'exécution est omis.)
  2. Cela donne également une erreur (Edge et Chrome)
  3. Confirmé.

Case à cocher Remember me

Ensuite, implémentez une case à cocher indispensable pour la fonction Remember me (une fonction qui ne se souvient que lorsqu'elle est cochée)

      <%= f.label :remember_me, class: "checkbox inline" do %>
        <%= f.check_box :remember_me %>
        <span>Remember me on this computer</span>
      <% end %>

Pour la raison de son placement à l'intérieur de l'étiquette, voir https://html-coding.co.jp/annex/dictionary/html/label/ Ce site est facile à comprendre En d'autres termes, cliquer n'importe où sur l'étiquette peut se comporter comme si vous aviez coché la case.

Une fois que vous l'avez façonné avec CSS, vous êtes prêt à partir. Puisque 1 ou 0 est maintenant entré dans les paramètres [: session] [: Remember_me] dans la case à cocher Vous devez vous en souvenir quand il est 1.

Lorsqu'il est implémenté à l'aide de l'opérateur ternaire

  params[:session][:remember_me] == '1' ? remember(user) : forget(user)

remember userRemplacez simplement la ligne par ceci À propos, l'opérateur ternaire est

Instruction conditionnelle?Traitement lorsque vrai:Traitement lorsque faux

Peut être écrit dans le format. À propos, puisque toutes les valeurs numériques des paramètres sont enregistrées sous forme de chaînes de caractères, 1 de l'instruction conditionnelle doit être placé entre «». Notez que les fausses minutes seront toujours exécutées et vous ne pourrez pas vous en souvenir.

Exercice

  1. ↑ Mais j'ai écrit une note, mais cela ne fonctionne que si la condition des paramètres est «1». Espérons que les valeurs sont stockées dans des cookies Ça marche bien.

>> hungry = true
=> true
>> hungry ? puts("I'm hungry now") : puts("I'm not hungry now")
I'm hungry now
=> nil

[Remember me] test

Maintenant que nous avons implémenté Rememberme, nous allons également créer des tests.

Testez la boîte Remember me

params [: session] [: Remember_me] == '1'? Remember (user): forget (user) `` implémenté avec l'opérateur ternaire précédent Cette partie est 1 (vrai) 0 (faux) pour ceux qui touchent le programme

params[:session][:remember_me] ? remember(user) : forget(user)


 Cependant, la case à cocher renvoie 1 et 0.
 Dans Ruby, 1 et 0 ne sont pas des valeurs booléennes et les deux sont traités comme vrais, donc ce serait une erreur d'écrire de cette façon.
 Vous devez écrire un test qui peut détecter de telles erreurs.

 Vous devez vous connecter pour vous souvenir de l'utilisateur. Jusqu'à présent, j'avais l'habitude d'envoyer des hachages de paramètres en utilisant la méthode post un par un.
 Comme il est difficile de le faire à chaque fois, définissez une méthode de connexion.
 Définissez-le comme une méthode log_in_as pour éviter toute confusion avec la méthode log_in.


#### **`test_helper`**
```rb

class ActiveSupport::TestCase
  # Run tests in parallel with specified workers
  parallelize(workers: :number_of_processors)

  # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
  fixtures :all
  include ApplicationHelper
  # Add more helper methods to be used by all tests here...
  def is_logged_in?
    !session[:user_id].nil?
  end
  
  def log_in_as(user)
    session[:user_id] = user.id
  end
  
  
end

class ActionDispatch::IntegrationTest
  def log_in_as(user, password: 'password', remember_me: '1')
    post login_path, params:{ session: { email: user.email,
                                                password: password,
                                                remember_me: remember_me}}
  end
end

La méthode log_in_as est définie deux fois séparément dans ActionDispatch :: IntegrationTest et ActiveSupport :: TestCase. Vous ne pouvez pas utiliser la méthode `` session '' dans le test d'intégration. Par conséquent, dans le test d'intégration, je me connecte à la place en utilisant la demande de publication.

En donnant aux deux tests le même nom, vous pouvez utiliser la méthode log_in_as sans vous soucier de quoi que ce soit lorsque vous souhaitez vous connecter à la fois pour l'intégration et les tests unitaires. Il suffit d'appeler.

Maintenant que nous avons défini la méthode log_in_as, nous allons implémenter le test Remember_me.

  test "login with remembering" do 
    log_in_as(@user, remember_me: '1')
    assert_not_empty cookies[:remember_token]
  end
  
  test "login without remembering" do
    log_in_as(@user, remember_me: '1')
    delete logout_path
    log_in_as(@user, remember_me: '0')
    assert_empty cookies[:remember_token]
  end

'1')

sessions_controller.rb


  def create
    @user = User.find_by(email: params[:session][:email].downcase)
    if @user&.authenticate(params[:session][:password])
      log_in(@user)
      params[:session][:remember_me] == '1' ? remember(@user) : forget(@user)
      redirect_to @user
    else
      flash.now[:danger] = "Invalid email/password combination"
      render 'new'
    end
  end

Test [Remember me]

J'ai implémenté le traitement des connexions et les méthodes d'assistance liées à la session dans sessions_helper current_userAucun test n'a été effectué sur le processus de branchement de la méthode. Le remplacement d'une chaîne appropriée qui n'a rien à voir avec la preuve réussira le test.

Test VERT ↓

sessions_helper.rb


  def current_user #Renvoie l'objet utilisateur actuellement connecté
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
Le japonais est également autorisé car je ne l'ai pas testé.
      user = User.find_by(id: user_id)
      if user &.authenticated?(cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end

C'est mauvais, alors créez un fichier de test comme sessions_helper```.

sessions_helper_test.rb


require 'test_helper'
class SessionsHelperTest < ActionView::TestCase
  def setup
    @user = users(:michael)
    remember(@user)
  end
  
  test "current_user returns right user when session is nil" do
    assert_equal @user, current_user
    assert is_logged_in?
  end
  
  test "current_user returns nil when remember digest is wrong" do
    @user.update_attribute(:remember_digest, User.digest(User.new_token))
    assert_nil current_user
  end
end

Le premier test s'assure que l'utilisateur mémorisé et l'utilisateur courant sont les mêmes et qu'ils sont connectés. Ce faisant, le test peut confirmer que le traitement du contenu fonctionne lorsque l'ID utilisateur existe dans les cookies.

Dans le second test, en réécrivant Remember_digest, cela ne correspond pas à Remember_token enregistré par la méthode Remember ''. Le current_user renvoie nil comme prévu, c'est-à-dire la méthode ```authenticated? ` Je teste que cela fonctionne correctement.

De plus, en complément, la méthode assert_equal fonctionne même si les premier et deuxième arguments sont échangés. Notez que vous devez écrire la valeur attendue dans le premier argument et la valeur réelle dans le deuxième argument. Si vous n'écrivez pas de cette manière, l'affichage du journal ne s'activera pas en cas d'erreur.

Et, bien sûr, le test ne passe pas à ce stade.

Le test réussit en supprimant les déclarations totalement non pertinentes que vous avez incluses. Maintenant que vous pouvez tester n'importe quelle branche de current_user, vous pouvez attraper les bogues de régression.

Exercice
  1. Même si le jeton de mémoire et le résumé de la mémoire ne correspondent pas correctement, l'instruction if sera transmise simplement parce que l'utilisateur existe. La valeur de retour n'est plus nulle. En d'autres termes, le test échoue également.
 FAIL["test_current_user_returns_nil_when_remember_digest_is_wrong", #<Minitest::Reporters::Suite:0x000055b13fd67928 @name="SessionsHelperTest">, 1.4066297989993473]
 test_current_user_returns_nil_when_remember_digest_is_wrong#SessionsHelperTest (1.41s)
        Expected #<User id: 762146111, name: "Michael Example", email: "[email protected]", created_at: "2020-06-20 15:38:57", updated_at: "2020-06-20 15:38:58", password_digest: [FILTERED], remember_digest: "$2a$04$uoeG1eJEySynSb.wI.vyOewe9s9TJsSoI9vtXNYJxrv..."> to be nil.
        test/helpers/sessions_helper_test.rb:15:in `block in <class:SessionsHelperTest>'

↑ Alors que la valeur de retour de current_user devrait être nulle, l'objet utilisateur est retourné. C'est une erreur.

Vers le chapitre précédent

Vers le chapitre suivant

Recommended Posts

Tutoriel Rails 6e édition Résumé d'apprentissage Chapitre 10
Rails Tutorial 6e édition Résumé de l'apprentissage Chapitre 7
Tutoriel Rails 6e édition Résumé de l'apprentissage Chapitre 4
Tutoriel Rails 6e édition Résumé de l'apprentissage Chapitre 9
Tutoriel Rails 6e édition Résumé de l'apprentissage Chapitre 6
Tutoriel Rails 6e édition Résumé de l'apprentissage Chapitre 5
Rails Tutorial 6e édition Résumé de l'apprentissage Chapitre 2
Tutoriel Rails 6e édition Résumé de l'apprentissage Chapitre 3
Rails Tutorial 6e édition Résumé d'apprentissage Chapitre 8
Tutoriel Rails (4e édition) Mémo Chapitre 6
Tutoriel Rails Chapitre 3 Apprentissage
Tutoriel Rails Chapitre 4 Apprentissage
Tutoriel Rails Chapitre 1 Apprentissage
Tutoriel Rails Chapitre 2 Apprentissage
Tutoriel Rails 4e édition: Chapitre 1 De zéro au déploiement
tutoriel rails Chapitre 6
tutoriel rails Chapitre 1
tutoriel rails Chapitre 7
tutoriel rails Chapitre 5
tutoriel rails Chapitre 10
tutoriel rails Chapitre 9
tutoriel rails Chapitre 8
Tutoriel Rails Chapitre 0: Apprentissage préliminaire des connaissances de base 5
[Rails] Didacticiel Apprendre avec les rails
Mémorandum du didacticiel Rails (Chapitre 3, 3.1)
[Tutoriel Rails Chapitre 4] Rubis à saveur de Rails
[Tutoriel Rails Chapitre 5] Créer une mise en page
Tutoriel de mastication des rails [Chapitre 2 Application jouet]
rails tutry
tutoriel sur les rails
rails tutry
tutoriel sur les rails
rails tutry
tutoriel sur les rails
tutoriel sur les rails
Seul résumé lié à la configuration du tutoriel Rails
Test du tutoriel sur les rails
Mémorandum du didacticiel Rails 1
Tutoriel Rails Chapitre 1 De zéro au déploiement [Essayer]
Rails Learning jour 3
Tutoriel Rails Memorandum 2
Rails Learning jour 4
Résumé du routage Rails 6.0
Rails Learning jour 2