Skip to content

Latest commit

 

History

History
910 lines (670 loc) · 24.4 KB

BE102.md

File metadata and controls

910 lines (670 loc) · 24.4 KB

Ruby on Rails

Introduction

Ruby on Rails is an open source full-stack web framework built in Ruby. It's optimized for programmer happiness with Convention over Configuration (CoC) and Don't Repeat Yourself (DRY) principles.

As always, the Documentation is our best friend.

Rails Basics

We are going to learn Rails by building an example app. Let's say we want to create a simple blog with these specs:

  • It will have some static pages (home, about, contact)
  • It will have a page with all the posts.
  • Users will be able to see all the posts, create a new post, edit it, and delete it.
  • Users will also be able to see comments on posts and write new comments.

Install Rails

$ gem install rails -v 4.2.6

Let's quit and restart the terminal when it's done. On Ubuntu, we need to run this as well:

$ sudo apt-get install nodejs

New App

To create a new rails application in the current folder, let's run this terminal command:

$ rails new blog

This will create a new folder named blog

Rails Directory structure

After creating the app, let's go to this folder and launch Sublime Text:

$ cd blog
$ stt

Rails follows the Model-View-Controller (MVC) pattern

MVC Architecture

The application code is in /app, for now we will focus on app/controllers, app/models and app/views

The other file we will care about for now is config/routes.rb where we will describe all the application's routes.

Launch the Rails server

Let's run this terminal command from our rails app folder (in a new console tab, preferably):

$ rails server # or: rails s

if we go to http://localhost:3000/ on our browser, we will see the rails home page:

Rails home page image

Launch the Rails console

The Rails console is IRB with the rails environment loaded. Let's run this terminal command from our rails app folder (in a new console tab, preferably):

$ rails console # or: rails c

💡 Tips

A few usefull commands for the rails console:

# From the rails console:
irb(main)> reload! # will reload the console with the new code we wrote
irb(main)> exit    # exit the console

# On launch:
$ rails console --sandbox # all the changes will be rolled back at exit

Rails Controller

Rails integrates some generators that allow us to quickly generate classes. Let's generate a pages controller for our static pages:

$ rails generate controller pages home about contact
# create  app/controllers/pages_controller.rb
#  route  get 'pages/contact'
#  route  get 'pages/about'
#  route  get 'pages/home'
# invoke  erb
# create    app/views/pages
# create    app/views/pages/home.html.erb
# create    app/views/pages/about.html.erb
# create    app/views/pages/contact.html.erb
# ...
# < other stuff >

We can now navigate to those pages:

The generator created:

  • a new controller class in app/controllers/pages_controller.rb:

    # app/controllers/pages_controller.rb
    class PagesController < ApplicationController
      def home
      end
    
      def about
      end
    
      def contact
      end
    end
  • 3 routes in config/routes.rb:

    # config/routes.rb
    Rails.application.routes.draw do
      get 'pages/home'
    
      get 'pages/about'
    
      get 'pages/contact'
    end
  • and 3 view files in app/views/pages like this one:

    <!-- app/views/pages/home.html.erb -->
    <h1>Pages#home</h1>
    <p>Find me in app/views/pages/home.html.erb</p>

At this point, we might wonder how Rails does this all magically. That is all the power of Convention over Configuration: Rails made some choices for us, if we are happy with them, we don't need to configure anything.

We can see a pattern here:

  • Controller names are plural: PagesController
  • The router will also route by default: get 'pages/home' will execute the home method of the pages controller.
  • the controller will render HTML views by default, and will seek them in a folder with the controller name (pages) and a filename being the method name (home.html.erb)
  • As we can see, there is no initialize method in this pages controller, nor attr_reader, attr_writer or attr_accessor, no require nor require_relative. This is all defined with some defaults in the mother classe that controllers (and later models as we will see soon) inherit from.

Note: .erb files (Embedded RuBy) will render an HTML file, we will understand this later.

Modify the routes

We want to modify the routes this way:

We will have to modify config/routes.rb:

# config/routes.rb
Rails.application.routes.draw do

  # this will give us a home page on http://localhost:3000/
  root 'pages#home'

  # For other routes, the syntax is:
  # verb 'path' => 'controller#method'
  get 'about' => 'pages#about'
  get 'contact' => 'pages#contact'
end

Let's check if it worked by running this terminal command:

$ rake routes
#  Prefix Verb URI Pattern        Controller#Action
#    root GET  /                  pages#home
#   about GET  /about(.:format)   pages#about
# contact GET  /contact(.:format) pages#contact

Rails View

Now we can style our HTML files. First, we can see that Rails uses a Layout in app/views/layouts/application.html.erb as a general architecture for all HTML files. We only need to write the body in our HTML files. Of course we can also modify the Layouts and even have different layouts for different purposes.

As we saw, our view files are .html.erb file, which allows us to include Ruby code in the views:

<!-- use '<% %>' to write ruby code -->
<% time = Time.now %>

<h1>Welcome!</h1>

<!-- use '<%= %>' to print -->
<p>The time is <%= time %></p>

Instance variables declared in the controller method are accessible in the view:

class PagesController < ApplicationController
  def home
    @today = Date.today
  end
end
<!-- app/views/pages/home.html.erb -->
<h1>Welcome!</h1>
<p>Today is <%= @today %></p>

We can write our css in app/assets/stylesheets/application.css, and our javascript in app/assets/javascripts/application.js.

📝 Note

By default, Rails controllers render HTML views. For your Camp Project, the Rails application will communicate with other apps (mostly the AngularJS client), not with humans. Humans like HTML, but computers prefer another format: JSON. For this blog example, we will render HTML views though, so that we can have a visual example.

Rails Model

All this is nice for static content. But how are we going to manage our blog posts and comments? They will be created by the app's users, and we will have to store them in the database.

That is where we need Models, the classes that will interact with the database. Model classes will be ActiveRecord classes.

ActiveRecord

Active Record is a pattern to store data in relational databases.

It will allow us to store an object as a row of a table in a relational database.

In Ruby, it's implemented in the activerecord gem, that is included by default in Rails. To put it simple, ActiveRecord allows us to write Ruby code instead of SQL to persist data and query a relational database.

Documentation

Migration

A migration is a modification of the database structure. For example, create a new table, add a column to a table, rename a column, etc.

ActiveRecord allows us to write our migrations in Ruby, not SQL. As we will see soon, Rails provides some generators for migrations.

Basic commands

Basic rake tasks:

$ rake db:create   # create a blank database
$ rake db:drop     # get rid of the database (:warning: you lose all your data!)
$ rake db:migrate  # run all the pending migrations
$ rake db:rollback # rollback the last migration
$ rake db:seed     # seed the database with data from db/seeds.rb

Back to our models. We will need two models, Post and Comment.

Let's start with Post, it will have two attributes: title and content. Rails provides a usefull generator:

$ rails generate model Post title:string content:text
# invoke  active_record
# create    db/migrate/20160615082457_create_posts.rb
# create    app/models/post.rb
# ...
# <other stuff>

The generator created:

  • a new model class in app/models/post.rb:

    # app/models/post.rb
    class Post < ActiveRecord::Base
    end

    It is an ActiveRecord model as it inherits from ActiveRecord::Base. This will allow us to use all the Ruby methods to persist objects and query the database.

  • a migration file in db/migrate/20160615082457_create_posts.rb:

    # db/migrate/20160615082457_create_posts.rb
    class CreatePosts < ActiveRecord::Migration
      def change
        create_table :posts do |t|
          t.string :title
          t.text :content
    
          t.timestamps null: false
        end
      end
    end

By convention, a model name is singular (and CamelCase as in Ruby convention), and it corresponds to it's snake_case pluralized table name in the database: Model Post corresponds to the posts table in the database.

Let's not forget to run the migration:

$ rake db:migrate

We can also generate custom migration, for example to modify our Post model:

$ rails generate migration AddAuthorNameToPosts
# invoke  active_record
# create    db/migrate/20160615085144_add_author_name_to_posts.rb

We can then write our code in the change method:

# db/migrate/20160615085144_add_author_name_to_posts.rb
class AddAuthorNameToPosts < ActiveRecord::Migration
  def change
    add_column :posts, :author_name, :string
  end
end

Read the Documentation to see everything you can do in migrations.

Rails CRUD

Let's look again at the specs for our blog posts:

Users will be able to see all the posts, create a new post, edit it, and delete it.

Those are the basic actions of any resources in an app: Create - Read - Update - Delete (CRUD). In detail, we will need the following actions:

  • index Read all the posts
  • show Read one post
  • Create one post (2 actions):
    • new Show form with post empty fields
    • create Persist new Data in the database
  • Update one post: (2 actions):
    • edit Show form with post existing data in fields
    • update Persist updated Data in the database
  • destroy Delete one post

Let's create a PostsController (remember the plural convention) with those 7 methods:

$ rails generate controller Posts index show new create edit update destroy

Before we complete the controller methods, let's have a look at the routes. This is how we should write those routes:

# config/routes.rb
Rails.application.routes.draw do
  get    'posts/'         => 'posts#index'
  get    'posts/:id'      => 'posts#show'
  get    'posts/:id/edit' => 'posts#edit'
  get    'posts/new'      => 'posts#new'
  post   'posts/'         => 'posts#create'
  patch  'posts/'         => 'posts#update'
  delete 'posts/:id'      => 'posts#destroy'

  # [...]
end

But actually, Rails has a shortcut for those 7 routes:

# config/routes.rb
Rails.application.routes.draw do
  resources :posts
  # [...]
end

We can also use a subset of those 7 routes:

# config/routes.rb
Rails.application.routes.draw do
  resources :posts, only: [:show, :index]
  # [...]
end

or

# config/routes.rb
Rails.application.routes.draw do
  resources :posts, except: [:edit, :update, :destroy]
end

Let's complete these methods now. The GET methods are pretty straightforward:

# app/controllers/posts_controller.rb
class PostsController < ApplicationController

  # GET /posts
  def index
    @posts = Post.all
  end

  # GET /posts/:id
  def show
    @post = Post.find(params[:id])
  end

  # GET /posts/new
  def new
    @post = Post.new
  end

  # POST /posts
  def create
  end

  # GET /posts/:id/edit
  def edit
    @post = Post.find(params[:id])
  end

  # PATCH /posts/:id
  # PUT   /posts/:id
  def update
  end

  # DELETE /posts/:id
  def destroy
  end
end

For create and update, we want to persist the data and then render the created/updated post to the user. But wait! We already have a view that displays one method that will render one post to the user, the show method. So we can redirect to this method in the end of create and update.

To know the path to redirect to, we can look at the routes and add a suffixe _path to the route name. We can also pass params if the method requires it:

$ rake routes
#    Prefix Verb   URI Pattern               Controller#Action
#     posts GET    /posts(.:format)          posts#index
#           POST   /posts(.:format)          posts#create
#  new_post GET    /posts/new(.:format)      posts#new
# edit_post GET    /posts/:id/edit(.:format) posts#edit
#      post GET    /posts/:id(.:format)      posts#show    # <- we want this one
#           PATCH  /posts/:id(.:format)      posts#update
#           PUT    /posts/:id(.:format)      posts#update
#           DELETE /posts/:id(.:format)      posts#destroy
#     about GET    /about(.:format)          pages#about
#   contact GET    /contact(.:format)        pages#contact

For the show method, we would have post_path and we need to pass at least the :id parameter: post_path(id: @post.id) we can also pass the entire object, only it's id is going to be taken into account: post_path(@post).

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  # [...]
  # POST /posts
  def create
    @post = Post.new(params[:post])
    @post.save
    # Will raise ActiveModel::ForbiddenAttributesError

    redirect_to post_path(@post)
  end

  # PATCH /posts/:id
  # PUT   /posts/:id
  def update
    @post = Post.find(params[:id])
    @post = Post.update(params[:post])
    # Will raise ActiveModel::ForbiddenAttributesError

    redirect_to post_path(@post)
  end
end

📝 Note As you can see, we are using some Post instance and class methods that we did not define in the Post class: #save, #find, #update. Those are all ActiveRecord methods that translates to SQL to persist data or query the database.

We will apply the same principle for destroy, after a resource is destroyed, we will redirect to the list of posts index:

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  # [...]
  # DELETE /posts/:id
  def destroy
    @post = Post.find(params[:id])
    @post.destroy

    redirect_to posts_path
  end
end

Strong params

By default, Rails will raise an Exception when we try to create or update a post using the params. Why? It's a protection feature. Imagine we have a User model with a boolean attribute called admin. In this case, we will probably not allow a user to define himself if he is admin or not, which he could do by sending a POST or PATCH request to the correct endpoint.

So we need to whitelist what params are allowed in update/create. Here is how to do it:

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  # [...]
  def create
    @post = Post.new(post_params)
    @post.save

    redirect_to post_path(@post)
  end

  def update
    @post = Post.find(params[:id])
    @post.update(post_params)

    redirect_to post_path(@post)
  end

  private

    def post_params
      params.require(:post).permit(:title, :content, :author_name)
    end
end

Filters

As we can see, some of the code is duplicate. Rails integrates filters, for example the before_action filter who will be executed before the execution of the method. Let's use it to make our code DRY:

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  before_action :set_post, only: [:show, :edit, :update, :destroy]

  def show
  end

  def edit
  end

  def update
    @post.update(post_params)

    redirect_to post_path(@post)
  end

  def destroy
    @post.destroy
  end

  private

    def set_post
      @post = Post.find(params[:id])
    end
end

Validations

Validations are another feature of ActiveRecord: we can check the validity of an object before saving it to the database.

We can add some validations to our Post model:

# app/models/post.rb
class Post < ActiveRecord::Base
  validates :content, presence: true
  validates :title, presence: true, uniqueness: true
end

This means that a Post won't save unless it has a content and a title, and that no other currently saved post already have this title. Other validations.

We need to update our controller accordingly:

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  # [...]
  def create
    @post = Post.new(post_params)

    # if @post is valid, @post.save saves the post and returns @post,
    # otherwise it does not save and returns false
    if @post.save
      redirect_to post_path(@post)
    else
      # we can use @post.errors in the view
      render :new
    end
  end

  def update
    # #update reacts the same as #save
    if @post.update(post_params)
      redirect_to post_path(@post)
    else
      render :edit
    end
  end
end

Here is our final Post model and PostsController controller class:

# app/models/post.rb
class Post < ActiveRecord::Base
  validates :content, presence: true
  validates :title, presence: true, uniqueness: true
end
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  before_action :set_post, only: [:show, :edit, :update, :destroy]

  # GET /posts
  def index
    @posts = Post.all
  end

  # GET /posts/:id
  def show
  end

  # GET /posts/new
  def new
    @post = Post.new
  end

  # POST /posts
  def create
    @post = Post.new(post_params)

    if @post.save
      redirect_to post_path(@post)
    else
      render :new
    end
  end

  # GET /posts/:id/edit
  def edit
  end

  # PATCH /posts/:id
  # PUT   /posts/:id
  def update
    if @post.update(post_params)
      redirect_to post_path(@post)
    else
      render :edit
    end
  end

  # DELETE /posts/:id
  def destroy
    @post.destroy

    redirect_to posts_path
  end

  private

    def set_post
      @post = Post.find(params[:id])
    end

    def post_params
      params.require(:post).permit(:title, :content, :author_name)
    end
end

Nested Resources

Now that we have posts, let's implement the comments. The Comment model is going to be related to the Post model in a one-to-many relationship. If you remember the SQL course, we want a post_id column in the comments table:

$ rails generate model Comment post:references content:string
$ rake db:migrate

Associations

Associations are yet another feature of ActiveRecord: it allows us to describe in the Model class how objects are related to each others, and use those to access the related models.

Let's write the associations in the models:

class Comment < ActiveRecord::Base
  belongs_to :post
end
class Post < ActiveRecord::Base
  has_many :comments, dependent: :destroy
  # [...]
end

Now we can easily access the comments of a post, and the post of a comment:

post = Post.first
post.comments
# => SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ?  [["post_id", 1]]

comment = Comment.first
comment.post
# => SELECT  "posts".* FROM "posts" WHERE "posts"."id" = ? LIMIT 1  [["id", 1]]

Read more about associations in the Documentation.

Before we generate a CommentsController, let's think about it's methods:

  • We don't want to list all the comments. It would make no sense without the relative Post. However, we want a page with all the comments of a single post. So we still want an index method.
  • We don't want to show a single comment, it would make no sense without context.
  • To keep it simple, we will keep the standards new/create methods.
  • To keep it simple, a User can't edit or destroy a comment (it would be the same logic as for posts).
$ rails generate controller Comments index new create

As the comments are very dependent on their relative post, we can define the routes as nested resources:

# config/routes.rb
Rails.application.routes.draw do
  resources :posts do
    resources :comments, only: [:index, :new, :create]
  end
end

This will add 3 new routes:

$ rake routes
#           Prefix Verb   URI Pattern                            Controller#Action
#    post_comments GET    /posts/:post_id/comments(.:format)     comments#index
#                  POST   /posts/:post_id/comments(.:format)     comments#create
# new_post_comment GET    /posts/:post_id/comments/new(.:format) comments#new
# [...]

We can see that those controller methods are now expecting a :post_id parameter. Let's complete the CommentsController:

# app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  before_action :find_post

  def index
    @comments = @post.comments
  end

  def new
    @comment = Comment.new
  end

  def create
    @comment = @post.comments.build(comment_params)

    if @comment.save
      redirect_to post_path(@post)
    else
      render :new
    end
  end

  private

    def comment_params
      params.require(:comment).permit(:content)
    end

    def find_post
      @post = Post.find(params[:post_id])
    end
end

Update the Views

As said earlier, we will use Rails to render JSON files and not HTML. However, for the purpose of this demo, let's stick with HTML views, so that we can have a visual demo of our blog. Let's update the views:

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

<%= link_to "All the posts", posts_path %>
<!-- app/views/posts/index.html.erb -->
<h1>Posts</h1>

<% @posts.each do |post| %>
  <h2><%= link_to post.title, post %></h2>
  <p><strong>By: <%= post.author_name %></strong></p>
  <p><%= post.content %></p>
<% end %>

<%= link_to "New", new_post_path %>
<!-- app/views/posts/show.html.erb -->
<h1><%= @post.title %></h1>

<p><strong>By: <%= @post.author_name %></strong></p>
<p><%= @post.content %></p>

<p><%= link_to "See Comments", post_comments_path(@post) %></p>

<%= link_to "All the posts", posts_path %> |
<%= link_to "Edit", edit_post_path(@post) %> |
<%= link_to "Destroy", post_path(@post), method: :delete, data: { confirm: "Are you sure?" } %>
<!-- app/views/posts/new.html.erb -->
<h1>New Post</h1>

<%= form_for(@post) do |f| %>
  <%= f.label :title %><br>
  <%= f.text_field :title %><br>
  <%= f.label :author_name %><br>
  <%= f.text_field :author_name %><br>

  <%= f.label :content %><br>
  <%= f.text_area :content %><br>

  <%= f.submit %>
<% end %>
<!-- app/views/posts/edit.html.erb -->
<h1>Edit Post</h1>

<%= form_for(@post) do |f| %>
  <%= f.label :title %><br>
  <%= f.text_field :title %><br>
  <%= f.label :author_name %><br>
  <%= f.text_field :author_name %><br>

  <%= f.label :content %><br>
  <%= f.text_area :content %><br>

  <%= f.submit %>
<% end %>
<!-- app/views/comments/index.html.erb -->
<h1>Comments for <%= @post.title %></h1>

<ul>
<% @comments.each do |comment| %>
  <li><%= comment.content %></li>
<% end %>
</ul>

<%= link_to "New", new_post_comment_path(@post) %>
<!-- app/views/comments/new.html.erb -->
<h1>New Comment for <%= @post.title %></h1>

<%= form_for([@post, @comment]) do |f| %>
  <%= f.label :content %><br>
  <%= f.text_area :content %><br>

  <%= f.submit %>
<% end %>

You can read the Documentation here and here to figure out how to use view helpers like form helpers, link helpers, etc.