Ruby on Rails

Parte 1

Agenda

  • Introducción
  • Instalación, configuración
  • Estructura de un proyecto Rails
  • Routes, Controllers, Views
  • ActiveRecord (introducción)
  • Aplicación de ejemplo (parte 1)

Ruby on Rails por dentro

  • Active Record
  • Active Model
  • Action Controller
  • Action View
  • Action Mailer
  • Action Cable
  • Active Support
  • Railties

The Rails Doctrine

  • Optimize for programmer happiness
  • Convention over Configuration
  • The menu is omakase
  • No one paradigm
  • Exalt beautiful code
  • Provide sharp knives
  • Value integrated systems
  • Progress over stability
  • Push up a big tent

Optimize for programmer happiness

  • 10.days.ago
  • Date.tomorrow
  • User.find_by_name_and_surname

Convention over Configuration

class User < ApplicationRecord
  has_many :posts
end

The menu is omakase

Rails tiene opiniones fuertes...

  • jQuery, CoffeeScript, SASS
  • ActionCable (websockets)
  • Turbolinks
  • Minitest > RSpec
  • Fixtures > Factories
  • ...

Exalt beautiful code

class Project < ApplicationRecord
  belongs_to :account
  has_many :participants, class_name: 'Person'
  validates_presence_of :name
end
if people.include? person

if person.in? people

Instalación, configuración

$ gem install rails --no-document
34 gems installed

$ rails --version
Rails 5.2.3

$ rails new demo_blog
      create  README.md
      create  Rakefile
      create  .ruby-version
      ...

$ rails s
=> Booting Puma (...)
* Listening on tcp://localhost:3000

Estructura de un proyecto Rails

Rails scaffolds

$ rails g controller --help
$ rails g controller NAME [action ...] [options]

Otros generators:

  • model
  • migration
  • resource
  • scaffold
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
  end
end
# app/views/posts/index.html.erb
<h1 class='title'>Blog index</h1>
# config/routes.rb
Rails.application.routes.draw do
  resources :posts
  root 'posts#index'
end

Routes (docs)

$ rails routes

   Prefix Verb   URI Pattern       Controller#Action
    posts GET    /posts            posts#index
          POST   /posts            posts#create
 new_post GET    /posts/new        posts#new
edit_post GET    /posts/:id/edit   posts#edit
     post GET    /posts/:id        posts#show
          PATCH  /posts/:id        posts#update
          PUT    /posts/:id        posts#update
          DELETE /posts/:id        posts#destroy
     root GET    /                 posts#index

Routes: más opciones

Restricting the Routes Created (docs)

resources :posts, only: [:index, :show]
resources :posts, except: [:destroy]

Routes: más opciones

Controller Namespaces and Routing (docs)

namespace :admin do
  resources :users
end
Helper            users => admin_users
Request      GET /users => GET /admin/users
Controller  users#index => admin/users#index

Routes: más opciones

Nested resources (docs)

resources :posts do
  resources :comments, only: [:create, :destroy]
end
post_comments    POST   /posts/:post_id/comments        comments#create
post_comment     DELETE /posts/:post_id/comments/:id    comments#destroy

Routes: más opciones

Adding More RESTful Actions (docs)

resources :articles do
  get 'pinned', on: :collection
  member do
    get 'likes'
  end
end
 pinned_articles    GET /articles/pinned       articles#pinned
   likes_article    GET /articles/:id/likes    articles#likes

Routes: más opciones

Non-Resourceful Routes (docs)

  get 'posts/top' # PhotosController#top

  get 'photos(/:id)', to: :display # PhotosController#display, /:id opcional

  get 'photos/:id/:user_id', to: 'photos#show' # params[:id], params[:user_id]

  get 'exit', to: 'sessions#destroy', as: :logout
  # logout_path => "/exit"

Migraciones (docs)

$ rails g model Post title:string content:text
    create    db/migrate/20190724032006_create_posts.rb
    create    app/models/post.rb
class Post < ApplicationRecord
end

Migraciones

class CreatePosts < ActiveRecord::Migration[5.2]
  def change
    create_table :posts do |t|
      t.string :title
      t.text :content
      t.timestamps
    end
  end
end
$ rails db:migrate
== 20190724032006 CreatePosts: migrating ======================================
-- create_table(:posts) -> 0.0011s

Migraciones: otros comandos

$ rails -T db

rails db:create              # Creates the database from config/database.yml [RAILS_ENV]
rails db:drop                # Drops the database from config/database.yml [RAILS_ENV]
rails db:migrate             # Migrate the database [RAILS_ENV, VERSION]
rails db:migrate:status      # Display status of migrations
rails db:rollback            # Rolls the schema back to the previous version [STEP=n]
rails db:schema:dump         # Creates a db/schema.rb file
rails db:schema:load         # Loads a schema.rb file into the database
rails db:seed                # Loads the seed data from db/seeds.rb
rails db:setup               # Creates the database, loads the schema, and seeds the data
rails db:reset               # Idem db:setup, but drop the database first
rails db:version             # Retrieves the current schema version number

PostsController

class PostsController < ApplicationController
  def index
  end

  def new
    @post = Post.new
  end

  def create
    @post = Post.new(post_params)

    if @post.save
      redirect_to @post
    else
      render :new
    end
  end

  private

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

Gemas

# en Gemfile

# Bulma CSS
gem 'bulma-rails', '~> 0.7.5'
# Simple forms
gem 'simple_form', '~> 4.1'

Instalamos, inicializamos simple_form y reiniciamos el server

$ bundle install
$ rails generate simple_form:install

Agregamos @import "bulma"; a application.css

Form (forma tradicional)

<h1>New Post</h1>

<%= form_for @post do |f| %>
  <label for="title">Title</label>
  <div><%= f.text_field :title %></div>

  <label for="title">Content</label>
  <div><%= f.text_area :content %></div>

  <%= f.submit 'Create new post' %>
<% end %>

Form (Simple Form + Bulma)

<h1 class="title">New Post</h1>

<div class="section">
  <%= simple_form_for @post do |f| %>
    <div class="field">
      <div class="control">
        <%= f.input :title, wrapper: false,
        input_html: {class: 'input'}, label_html: {class: 'label'} %>
      </div>
    </div>
    <div class="field">
      <div class="control">
        <%= f.input :content, wrapper: false,
        input_html: {class: 'textarea'}, label_html: {class: 'label'} %>
      </div>
    </div>

    <%= f.button :submit, 'Create new post', class: 'button is-primary' %>
  <% end %>
</div>

Show

  def show
    @post = Post.find(params[:id])
  end
<section class="section">
  <div class="container">
    <h1 class="title"><%= @post.title %></h1>
    <div class="content"><%= @post.content %></div>
  </div>
</section>

Index

  def index
    @posts = Post.all.order("created_at DESC")
  end
    <% @posts.each do |post| %>
      <div class="card">
      <div class="card-content">
        <div class="media">
          <div class="media-content">
            <p class="title is-4"><%= link_to post.title, post  %></p>
          </div>
        </div>
        <div class="content"> <%= post.content %> </div>
      </div>
    </div>
    <% end %>

content_for

# en el layout
<%= yield :page_title %>

# en el partial
<% content_for :page_title, @post.title %>

Edit/Update

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

  def update
    @post = Post.find(params[:id])
    if @post.update(post_params)
      redirect_to @post
    else
      render :edit
    end
  end
<%= link_to "Edit", edit_post_path(@post), class: "button" %>

DRY (Don't Repeat Yourself)

Creamos un partial: app/views/posts/_form.html.erb
Lo llamamos desde new & edit

<%= render 'form' %>

Delete

@post = Post.find(params[:id])
@post.destroy
<%= link_to "Delete", post_path(@post), method: :delete, data: {confirm: "Are you sure?"} %>

DRY II

class PostsController < ApplicationController
  before_action :set_post, only: [:show, :edit, :update, :destroy]

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

Comments

$ rails g controller comments
$ rails g model Comment name:string comment:text post:references
$ rails db:migrate
class Post < ApplicationRecord
  has_many :comments
end

class Comment < ApplicationRecord
  belongs_to :post
end

# config/routes.rb
resources :posts do
  resources :comments, only: [:create, :destroy]
end

Comments

class CommentsController < ApplicationController
  def create
    @post = Post.find(params[:post_id])
    @comment = @post.comments.create(params[:comment].permit(:name, :comment))
    redirect_to post_path(@post)
  end
end
<%= simple_form_for [@post, @post.comments.build] do |f| %>
  <%= f.input :name, wrapper: false, input_html: {class: 'input'} %>
  <%= f.input :comment, wrapper: false, input_html: {class: 'textarea'} %>
  <%= f.button :submit, class: 'button is-primary' %>
<% end %>

Extras...

Gema útil para desarrollo:

# Provides a better error page for Rails
gem 'better_errors', '~> 2.5', '>= 2.5.1'

Importante: Luego de editar el Gemfile hay que ejecutar bundle install y reiniciar el server

Live Reload

  • Agregar guard y guard-livereload al Gemfile
group :development do
  # Guard is a command line tool to easily handle events on file system modifications.
  gem 'guard', '~> 2.15'
  # reload the browser after changes to assets/helpers/tests 
  gem 'guard-livereload', '~> 2.5', '>= 2.5.2', require: false
end
  • Instalar la extensión de LiveReload
  • Correr bundle exec guard en una nueva terminal