refer to https://legacy.gitbook.com/book/spartchou/ruby-on-rails-basic/details
Do not use @post.user_id = current_user.id
, and use save!
instead of save
unless check return value of save
class PostsController < ApplicationController
# @post.user_id = current_user.id and
def create
@post = current_user.posts.build(params[:post])
@post.save!
end
end
class User < ActiveRecord::Base
has_many :posts
end
Unlike send, public_send can call only public method.
ppublic_send is recommended instead of send, but send is useful for unit test of private method
# process_date_format is private method
assert_equal importer.send(:process_date_format, "01/01/15"), "01-01-15"
assert_equal importer.send(:process_date_format, "01/01/15"), "01-01-15"
Do not use Unscoped access, which could raise brakeman Unscoped Find
exception.
# Bad Smell below....
def edit
@post = Post.find(params[:id])
if @post.user != current_user
flash[:warning] = 'Access denied'
redirect_to posts_url
end
end
# should use below....
def edit
# raise RecordNotFound exception (404 error) if not found
@post = current_user.posts.find(params[:id])
# or use pundit policy_scope, @post = policy_scope(Post).find(params[:id])
end
@libraries = Library.where(size: 'large').includes(:books) #has_many associations
@books = Book.all.includes(:author) #belongs_to / has_one associations
Category.includes(articles: [{ comments: :guest }, :tags]).find(1)
# category with id 1 and eager load all of the associated articles,
# the associated articles' tags and comments, and every comment's guest association.
Do not use User.all.each
for large data, find_each
or find_in_batches
are useful.
# the application only finds 1,000 users once, yield them, then handle the next 1,000 users
User.find_each do |user|
NewsLetter.weekly_deliver(user)
end
# 1,000 is the default batch size, you can use the :batch_size option to change it.
User.find_in_batches(:batch_size => 5000) do |users|
users.each { |user| NewsLetter.weekly_deliver(user) }
end
Time.zone = "Alaska"
# Do not use below, because it show the local time
Time.now
Date.today
Date.today.to_time
Time.parse("2015-07-04 17:05:37")
Time.strptime(string, "%Y-%m-%dT%H:%M:%S%z")
# Use below
Time.zone.now
Time.zone.today
Time.current
2.hours.ago
Date.current
1.day.from_now
Time.zone.parse("2015-07-04 17:05:37")
Time.strptime(string, "%Y-%m-%dT%H:%M:%S%z").in_time_zone
Use ENV.fetch
for environment variables instead of ENV[]
so that unset environment variables are detected on deploy.
development:
<<: *default
database: energy-efficiency-funding_development
username: <%= ENV.fetch("DB_USERNAME", "postgres") %>
password: <%= ENV.fetch("DB_PASSWORD", "") %>
host: <%= ENV.fetch("DB_HOST", "localhost") %>
port: <%= ENV.fetch("DB_PORT", "5432") %>
flash: redirection, for the next request flash.now: render template only, for the same request
class ClientsController < ApplicationController
def create
@client = Client.new(params[:client])
if @client.save
# ...
else
flash.now[:error] = "Could not save client"
render action: "new"
end
end
end
Query condition should be right type
# Bad Smell below....
request.auth_comments.where.not(id: [nil, ""]).each { ... }
# id is not string, but "" is used for where statement.
# before 5.2.3, which is converted below sql statement.
=>
SELECT "auth_comments".* FROM "auth_comments" WHERE "auth_comments"."request_id" = $1
AND NOT (("auth_comments"."id" IS NULL OR "auth_comments"."id" IS NULL)) [["request_id", 1]]
# "auth_comments"."id" IS NULL is duplicated, but it works on 5.2.2.1
# since Rails 5.2.3, the code generated like blow...,
# so it doest not work because "auth_comments"."id" = $2 ["id", nil]]
=>
SELECT "auth_comments".* FROM "auth_comments"
WHERE "auth_comments"."request_id" = $1
AND NOT (("auth_comments"."id" = $2 OR "auth_comments"."id" IS NULL)) [["request_id", 1], ["id", nil]]
# finding records where string column is not null or empty is useful
Product.where.not(title: ["",nil])
#=>
SELECT "products".* FROM "products"
WHERE NOT (("products"."title" = $1 OR "products"."title" IS NULL))
LIMIT $2 [["title", ""], ["LIMIT", 11]]
# Do not use NULL in query string because 1 = NULL is NULL also 1 != NULL is NULL in sql
Person.where.not(manager_id: "NULL")
Person.where("manager_id != NULL")
=> SELECT people.* FROM people WHERE people.manager_id != NULL
Person.where.not(manager_id: nil)
=> SELECT people.* FROM people WHERE people.manager_id IS NOT NULL
Use persisted?
method in order to check a saved record instead of using query for id is null
# use persisted?
class AuthComment < ApplicationRecord
scope :persisted, -> { select(&:persisted?) }
end
request.auth_comments.persisted.each {...}
Use new_record?
method to check this object hasn’t been saved yet. Returns true if this object hasn’t been saved yet
<%= f.submit @contact.new_record? ? "Save" : "Update", class:"btn btn-primary" %>
delete
removes only delete current object record from db but not its associated children records from db.destroy
removes current object record from db and also its associated children record from db when having:dependent
, and also can execute callbackbefore_destroy
,around_destroy
,after_destroy
has_many :addresses, dependent: :destroy
class Book < ActiveRecord::Base
belongs_to :author
scope :available, ->{ where(available: true) }
end
class Author < ActiveRecord::Base
has_many :books
# without merge
scope :with_available_books, joins(:books).where("books.available = ?", true)
# with merge
scope :with_available_books, joins(:books).merge(Book.available)
end
Use rescue_from instead of rescue in each acitons
# Bad Smell
class PostsController < ApplicationController
def show
@post = Post.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
def edit
@post = Post.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
def destroy
@post = Post.find(params[:id])
@post.destroy
rescue ActiveRecord::RecordNotFound
render_404
end
end
class UserController < ApplicationController
def show
@user = User.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
# ...
end
# use rescue_from
class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound, :with => :render_404
...
def render_404
render template: "errors/404", status: :not_found
end
end
class PostsController < ApplicationController
def show
@post = Post.find(params[:id])
end
def edit
@post = Post.find(params[:id])
end
def destroy
@post = Post.find(params[:id])
@post.destroy
end
end
This will work if you have a _book.html.erb partial & use book as your variable inside that template.
# render calls to_partial_path method on each of our objects
# Models that inherit from ActiveRecord::Base will teturn a partial name based on the model's name
Book.new.to_partial_path #=>"books/book"
# app/views/books/index.html.erb
<%= render @books %>
# app/views/books/_book.erb
<div class="book">
<%= image_tag(book.cover) %>
<div class="book-description">
<h4><%= book.title %></h4>
<%= book.author %>
</div>
<%= link_to "Read Book", book %>
</div>
# config/routes.rb
get 'contacts/index'
=>
# Prefix Verb URI Pattern Controller#Action
# contacts_index GET /contacts/index(.:format) contacts#index
# This will create contact_path and contact_url as named route helpers in your application.
# Calling contacts_path will return /contacts/index
get 'contacts/index', as: 'contacts'
# Pefix Verb URI Pattern Controller#Action
# contacts GET /contacts/index(.:format) contacts#index
path is relative while url is absolute.
employees_url => http://localhost:3000/employees
employees_path # => /employees
#_path are generally used in views because hrefs are implicitly linked to the current URL.
#_url is needed typically, with url redirects - redirect_to.
# bad smell
get '/posts/:id/preview', to: 'posts#preview', as: "review_post"
get '/posts/search', to: 'posts#search', as: "search_posts"
# should use below...
resources :posts do
get :preview, on: :member
get :search, on: :collection
end
In form, name attributes in each elements are params attributes.
<form action="/contacts/create", method="post">
<input type="hidden", name="authenticity_token" value="<%= form_authenticity_token %>">
<label for="name" class="control-label col-md-3">Name</label>
<input type="text" name="contact[name]">
<input type="text" name="contact[company]">
</form>
<!-- {
"authenticity_token"=>"nfh1IyqSkvC0qPRrGWCsrBSa8ZEimkkU6OpuqlYXTY0jmGkgWOeM0EsVhOtgjLy+xGAyEURfP3DpSH2Tlw/lrg==",
"contact"=>{"name"=>"test", "company"=>"test com"}, "..."=>""} -->
<%= form_for(@contact) do |f| %>
<%= f.label :name, class: "control-label col-md-3" %>
<%= f.text_field :name, class: "form-control" %>
...
<%= f.text_field :company, class: "form-control" %>
<% end %>
# for update rails convert add input element for patch
<form accept-charset="UTF-8" action="/search" method="post">
<input name="_method" type="hidden" value="patch" />
...
<% end %>
<form method="post" action="/posts/<%= post.id %>" style='display: inline'>
<input name= "_method" type="hidden" value="delete">
<input type="submit" value="Delete" />
</form>
# On the server side, Rails is able to examine the "_method" value in the params to restore the semantics of the HTTP request.
For file upload, use multipart/form-data
<form action="http://localhost:8000" method="post" enctype="multipart/form-data">
</form>
<%= form_for(@contact, html: {multipart: true} ) do |f| %>
<%= f.file_field :avatar %>
...
<% end %>
c = Contact.new(name:"test")
c.save
c.errors.any? #=> true
c.errors.messages
#=> {:group=>["must exist"], :email=>["can't be blank"], :group_id=>["can't be blank"]}
c.errors.full_messages
# => ["Group must exist", "Email can't be blank", "Group can't be blank"]
module UsersHelper
def full_name(user)
user.first_name + user.last_name
end
end
class UsersController < ApplicationController
include UsersHelper
def update
...
redirect_to user_path(@user), notice: "#{full_name(@user) is successfully updated}"
end
end
# or use helpers
class UsersController < ApplicationController
def update
notice = "#{helpers.full_name(@user) is successfully updated}"
redirect_to user_path(@user), notice: notice
end
end
class ApplicationController < ActionController::Base
helper_method :current_user, :logged_in?
def logged_in?
!!current_user
end
end
<label for="group">Group</label>
<select name="contact[group_id]" id="group" class="form-control">
<option value="">Select group</option>
<%# Group.all.each do |group| %>
<option value="<%#=group.id %>"><%#=group.name%></option>
<%# end %>
</select>
<%= f.label :group_id, class: "control-label col-md-3" %>
<%= f.collection_select :group_id, Group.all, :id, :name, { prompt: "Select Group"} %>
In production, use --sandbox option for testing query
$ rails c --sandbox
0.2 + 0.1 == 0.3
==> false
0.2 + 0.1
=> 0.30000000000000004
When a js function is called in view(html.erb
), window.funcname = funcion()...
https://www.theodinproject.com/courses/ruby-on-rails/lessons/advanced-forms
atch : Used for updating an object partially, takes less bandwidth. put : Used for updating the whole object. delete : Used for removal of object.
Now the problem with these requests are that most of the browsers support only get and post. Rails fixed this problem by emulating other methods over POST with a hidden input named "_method". It tells the controller about which type of request is coming, even if the browser doesn't support them. For example if there is a form to update the user's entry and we need to use the patch request, the form will look like this:
form_tag(users_path, method: "patch") It will produce the following HTML: