ruk·si

🛤️ Ruby on Rails
Basic Setup

Updated at 2016-03-24 11:16

Creating a Project

Install dependencies:

brew install postgresql  # install PostgreSQL the way you want, I prefer `brew`
brew install rbenv       # install Ruby the way you like, I like `rbenv`

# Check latest Heroku supported version at:
# https://devcenter.heroku.com/articles/ruby-support#ruby-versions
# e.g. 2.3.0 : patchlevel 0, Rubygems : 2.5.1
rbenv install 2.3.0
rbenv global 2.3.0
rbenv local 2.3.0   # lock Ruby version for this project
gem install rails --no-document

Create the project:

# keep project name as a single word
rails new -d postgresql --skip-turbolinks projectty
cd projectty
# edit `.gitignore`
*.DS_Store
Thumbs.db
.idea/workspace.xml
.idea/tasks.xml
/.bundle
/log/*
!/log/.keep
/tmp
.env
git init
git add -A
git commit -m "Init commit"
mine .                        # start project in RubyMine, wait for it to load
# close RubyMine, generates more project files after reopen
mine .
git add -A
git commit -m "Define IDEA project"

Edit Gemfile.

source 'https://rubygems.org'

ruby '2.3.0'

gem 'pg'
gem 'rails',        '4.2.4'
gem 'bundler',      '>= 1.8.4' # For Rails Assets
gem 'uglifier',     '>= 1.3.0'
gem 'sass-rails',   '~> 5.0'
gem 'coffee-rails', '~> 4.1.0'
gem 'jbuilder',     '~> 2.0'
gem 'sdoc',         '~> 0.4.0', group: :doc

gem 'puma'
gem 'bcrypt'
gem 'ar-uuid'

group :development do
    gem 'spring'
end

source 'https://rails-assets.tenex.tech' do
  gem 'rails-assets-jquery'
  gem 'rails-assets-jquery-ujs'
end
# edit `app/assets/javascripts/application.js`
//= require jquery
//= require jquery-ujs
//= require_tree .

Install dependencies in the Gemfile:

bundle install
git add -A
git commit -m "Add Gemfile.lock"

Setup database:

# edit `config/database.yml`
default: &default
  adapter: postgresql
  encoding: unicode
  pool: 5

development:
  <<: *default
  database: projectty_development
  username: projectty

test:
  <<: *default
  database: projectty_test
  username: projectty

# Heroku merges this file with their settings so no need for production
createuser projectty -s     # PostgreSQL user as superuser
bundle exec rake db:create
# optional
# add to `development.rb` to show ran SQL queries in console
ActiveRecord::Base.logger = Logger.new(STDOUT) if defined?(ActiveRecord::Base)

Create controller for root pages:

rails g controller RootPages index
# edit `config/routes.rb`
Rails.application.routes.draw do
  root "root_pages#index"
end

Setup webserver and foreman:

# create `config/puma.rb`
required_envs = %w(PORT RACK_ENV WEB_CONCURRENCY MAX_THREADS)
required_envs.each do |env|
  if ENV[env].nil?
    abort %Q(
      #{env} environmental variable is not set.
        Local:  copy `.env.example` to `.env` and edit it.
        Heroku: use `heroku config:set #{env}="value"`
    )
  end
end

workers Integer(ENV["WEB_CONCURRENCY"])
threads_count = Integer(ENV["MAX_THREADS"])
threads threads_count, threads_count

preload_app!

rackup      DefaultRackup
port        ENV["PORT"]
environment ENV["RACK_ENV"]

on_worker_boot do
  ActiveRecord::Base.establish_connection
end
# `.env` is gitignored and used to fill process env variables using foreman.
# By copying `.env.example`, others devs can use to define their own settings.
# Production environments like Heroku should use normal environmental variables.

# Create `.env` and `.env.example` to the project root with:
WEB_CONCURRENCY=2
MAX_THREADS=5
PORT=3000
RACK_ENV=development

Starting the server:

# `Procfile` tells foreman which processes to start

# create file `Procfile` to root with:
web: bundle exec puma -C config/puma.rb
gem install foreman
foreman start
# now you can see your server at localhost:3000

Commit to version control:

git add -A
git commit -m "Add root pages"

Adding User Model

rails g migration EnableCitextAndUuid
rails g scaffold User display_name:text email:citext
class EnableCitextAndUuid < ActiveRecord::Migration
  def change
    enable_extension "citext"
    enable_extension "uuid-ossp"
  end
end
class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.text   :display_name
      t.citext :email
      t.timestamps null: false
    end
    add_index :users, :email, unique: true
  end
end
# add uniqueness validation to `app/models/user.rb`
class User < ActiveRecord::Base
  validates :email, uniqueness: true
end
bundle exec rake db:migrate
git add -A
git commit -m "Add users"

Running Tests

foreman run bundle exec rake test
# `foreman run` sets the environmental variables from `.env`
# `bundle exec` makes sure you use the right gems
# Note that tests will fail, like they should as tests will try to
# create users with duplicate emails. You can fix those tests easily.

Removing a Project

cd /path/to/projectty
bundle exec rake db:drop
dropuser projectty
cd ..
rm -rf projectty
# Remove the git repos and Heroku app.

Heroku

Now is a good time to setup a staging server to see if it all really works.

# edit Gemfile
group :production do
  gem 'rails_12factor' # Heroku
end
# install Heroku Toolbelt
heroku auth:whoami
heroku logout
heroku login
heroku auth:whoami

# create an app
heroku apps:create -r stage --region us projectty-stage
heroku config                         # check the environmental variables
heroku config:set WEB_CONCURRENCY=2   # add the missing ones
heroku config:set MAX_THREADS=5

# add a database
heroku addons:create heroku-postgresql:hobby-dev
heroku pg:wait                        # wait for the database to come online
git push stage master:master
heroku run rake db:schema:load        # if you've done migrations
heroku logs -t

# after you create production server for this app, you must use `-a` with
# all Heroku commands and configure additional remote for git, `production`
heroku logs -t -r stage

# database commands
heroku pg:info -r stage                      # database details and limitations
heroku pg:diagnose -r stage                  # check for problems
heroku pg:ps -r stage                        # view active queries
heroku pg:psql -r stage                      # open SQL connection to DB
heroku pg:credentials DATABASE_URL -r stage  # show DB access credentials
# the app should now work, visit the page
https://projectty-stage.herokuapp.com/
# put on maintenance mode when you don't need the staging
heroku maintenance:on
# heroku maintenance:off

DNS for Heroku

First, extend application to redirect all naked domain calls to www when production. Using basic www for the main consumer facing domain is advisable; it's established standard and it improves search engine rankings, although marginally.

# in `app/controllers/application_controller.rb`
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  before_filter :add_www_subdomain

  private

  def add_www_subdomain
    is_production = ENV['RACK_ENV'] == 'production'
    if is_production && ! /^www/.match(request.host)
      redirect_to(
          "#{request.protocol}www.#{request.host_with_port}",
          status: 301
      )
    end
  end
end
# Second, buy a domain from a domain registry.
# Then tell Heroku all domains you will be configuring.
heroku domains:add projectty.com
heroku domains:add www.projectty.com
heroku domains

# Figure out your app's load balancer using `dig`
dig my-app.herokuapp.com
# the first CNAME is your load balancer e.g. elbXXX.us-east-1.elb.amazonaws.com

AWS > Route 53 > Create Hosted Zone
    Domain Name:        projectty.com
Zone > Create Record Set
    A (ALIAS)   projectty.com         ->    elbXXX.us-east-1.elb.amazonaws.com
    CNAME       www.projectty.com     ->    projectty.herokuapp.com
Hosted Zone List > Select Hosted Zone
    Copy the 4 name server URLs and configure them to your domain provider.

Other possible DNS configurations:

    # route all unspecified subdomains to a single app
    CNAME       *.projectty.com       ->    subdomainizer.herokuapp.com

    # if using SSL endpoint, remember to use that
    heroku certs
    CNAME       www.projectty.com     ->    tokyo-123.herokussl.com

SSL in Heroku

I this day and age, every website should be hosted using SSL, even small personal projects and staging servers.

Namecheap:
1. Figure out what domains and subdomains you want to secure.
   All subdomain cerficates e.g. www include the root domain.
   The most common selections are:
    www.example.com (implicit example.com)
    *.example.com   (implicit example.com)
2. Buy certificates for the domain or domains.
3. Activate the SSL:
    1. Figure out your or your organization's Country Code
       https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
    2. The domain you are securing is sometimes dubbed `common name`.
    3. Generate CSR:
        brew install openssl
        openssl genrsa -des3 -out server.pass.key 2048
        openssl rsa -in server.pass.key -out server.key
        openssl req -nodes -new -key server.key -out server.csr
          US
          www.example.com
          Fill the rest of the info, but those two are the most important.
    4. Provide CSR contents of server.csr to activate it.
    4. If asked, web server is Nginx.
    5. If asked, certificate format is X.509.
    5. Select DCV authenticaion using DNS.
    6. Provide your administrative information.
    8. Submit
    7. Go to Certificate Details page.
    8. Edit Methods > Get Record
      Go to your DNS e.g. Route 53.
      Add a new record set.
      CNAME   <FROM_NAMECHEAP>.example.com  <FROM_NAMECHEAP>.comodoca.com
    9. It will take from 6h to 1 day to get verified.
4. Install SSL addon:
    heroku addons:create ssl:endpoint
    heroku certs:add server.crt server.key
    heroku domains
    heroku domains:add www.example.com
    heroku certs
5. Modify A in your DSN (Route53):
    Make sure load balancer is the same as before
    dig example-2121.herokussl.com
    If it has changed, update the A record.
    A (ALIAS)   projectty.com         elbXXX.us-east-1.elb.amazonaws.com
5. Modify CNAME in your DSN (Route53):
    CNAME       www.projectty.com     example-2121.herokussl.com.
    or
    CNAME       *.projectty.com       example-2121.herokussl.com.
6. Test that it works:
    curl -kvI https://www.example.com
    Expect to find "SSL certificate verify ok"
# add this to config/environments/production.rb
config.force_ssl = true
# update your app
# check that http://www.example.com/ forced to https.
Turn all asset paths to relative:
<script src="http://code.jquery.com/jquery-2.1.0.js"></script>
<script src="//code.jquery.com/jquery-2.1.0.js"></script>
Now all of the following are redirected to `https://www.example.com`.
1. example.com
2. http://example.com
3. https://example.com
4. http://www.example.com
1. Updating certificates is just as easy:
    Get a new certificate using steps 2 and 3 of installation.
    heroku certs:update server.crt server.key

Enable Dynamic Error Pages

rails g controller ErrorPages show
# add this to `config/application.rb`
config.exceptions_app = self.routes
# edit `config/routes.rb`
%w(404 422 500).each do |code|
  get code, :to => "error_pages#show", :code => code
end
class ErrorPagesController < ApplicationController
  def show
    render status_code.to_s, :status => status_code
  end

  protected

  def status_code
    params[:code] || 500
  end
end
# copy and rename `app/views/error_pages/show.html.erb` and remove the original
show.html.erb ->
    404.html.erb
    422.html.erb
    500.html.erb
# remove static error pages at `public/`
404.html
422.html
500.html
# to test in development, temporarily edit `config/environments/development.rb`
config.consider_all_requests_local = false
git add -A
git commit -m "Add dynamic error pages"

Sending Emails

Creating mailer:

rails g mailer UserMailer test_email
# edit `application_mailer.rb`
class ApplicationMailer < ActionMailer::Base
  default from: ENV["MAILER_SENDER"]
  layout "mailer"
end
# edit `user_mailer.rb`
class UserMailer < ApplicationMailer
  def test_email(user)
    @user = user
    mail(to: @user.email, subject: "Welcome to Projectty")
  end
end
# edit `.env` and `.env.example`
MAILER_SENDER=Projectty <Projectty@example.com>

Edit email templates:

# html version
<h1>UserMailer#test_email</h1>
<p>
  <%= @user.display_name %>, find me in app/views/user_mailer/test_email.html.erb
</p>

# text version
UserMailer#test_email

<%= @user.display_name %>, find me in app/views/user_mailer/test_email.text.erb

Create email sending action:

class RootPagesController < ApplicationController
  # ...
  def email_test
    @user = User.first
    UserMailer.test_email(@user).deliver
    render json: {status: :ok}
  end
end
# edit `routes.rb`
get "mail" => "root_pages#mail"
# crash when an email cannot be sent, `development.rb`
config.action_mailer.raise_delivery_errors = true
git add -A
git commit -m "Add user mailer"

Email Provider

Select transactional email provider. I personally prefer Sendgrid but many marketing departments want Mailchimp + Mandrill. Sendgrid registration takes couple hours to get verified but has 12k free emails per month.

https://sendgrid.com
# create account
# wait for verification, takes couple of hours
# go to dashboard
# setup default whitelabel subdomain to mailer.example.com
  # create the whitelabel subdomain inside SendGrid app
  # go to your DNS provider and setup records e.g. Route 53
    # if you have multiple varifications in TXT record:
    v=spf1 include:xxx.sendgrid.net include:xxx.mandrillapp.com ?all
  # You will get this info to sent mailer, not `sendgrid.me`:
  mailed-by:  mailer.example.com
# Setup default whitelabel email link to el.example.com.
  # Create the whitelabel subdomain inside SendGrid app.
  # Go to your DNS provider and setup records e.g. Route 53.
  # All links in sent emails will now have a redirect links to this domain
  # for analytics, not to SendGrid's domain.
# edit `.env` and `.env.example`
APP_HOSTNAME=http://localhost
APP_PORT=3000
SENDGRID_PORT=587
SENDGRID_USERNAME=SAME_AS_WEBSITE
SENDGRID_PASSWORD=SAME_AS_WEBSITE
SENDGRID_DOMAIN=example.com
# add settings for `application.rb`
config.action_mailer.smtp_settings = {
    :address                => 'smtp.sendgrid.net',
    :port                   => ENV['SENDGRID_PORT'],
    :user_name              => ENV['SENDGRID_USERNAME'],
    :password               => ENV['SENDGRID_PASSWORD'],
    :domain                 => ENV['SENDGRID_DOMAIN'],
    :enable_starttls_auto   => true,
    :authentication         => :plain,
}
config.action_mailer.default_url_options = { host: ENV['APP_HOSTNAME'] }
if ENV['APP_PORT'].present?
  config.action_mailer.default_url_options[:port] = ENV['APP_PORT']
end
git add -A
git commit -m "add SendGrid"
git push stage master:master
heroku config:set APP_HOSTNAME=https://projectty-stage.herokuapp.com
heroku config:set SENDGRID_PORT=587
heroku config:set SENDGRID_USERNAME=<SENDGRID_LOGIN>
heroku config:set SENDGRID_PASSWORD=<SENDGRID_LOGIN>
heroku config:set SENDGRID_DOMAIN=example.com
heroku config:set MAILER_SENDER="Projectty Stage <projectty@example.com>"

Sending with Sidekiq

Sidekiq allows sending emails asynchronously so visitors don't have to wait for the sending.

brew install redis      # and follow instructions
# add to Gemfile
gem 'sidekiq'
bundle install
foreman run bundle exec sidekiq     # test that it works and stop it
# add to `Procfile`
worker: bundle exec sidekiq -q emails
# edit `application.rb` so that active jobs use Sidekiq
config.active_job.queue_adapter = :sidekiq
# change `root_pages#mail` function to
def mail
  @user = User.first
  UserMailer.test_email(@user).deliver_later(queue: :emails)
  render json: {status: :ok}
end
foreman start
# go to http://localhost:3000/mail
git add -A
git commit -m "send emails asynchronously"
git push stage master:master
# for Heroku, you need to add a redis addon
# as the time of writing, Redis Cloud is the best and cheapest option
heroku addons:create rediscloud:30
heroku config:set REDIS_PROVIDER=REDISCLOUD_URL
heroku ps:scale worker=1
heroku ps
# go to Heroku Dashboard:
# - Your `worker` dyno should be enabled.
# - Redis Cloud has a dashboard where you can see the queues.

Additionally, you can now use jobs for all your background tasks!

Production Errors - Sentry

Using Sentry to receive notifications about production and staging errors. It's free for solo developers with 7 day history.

https://getsentry.com
# create an account if you don't have one
# create a project, I name it the same as heroku app e.g. projectty-stage
# you should receive a DSN that will be place in env variables
# add to Gemfile
group :production do
  # ...
  gem 'sentry-raven'
end
bundle install
# add to `app/controllers/root_pages_controller.rb`
class RootPagesController < ApplicationController
  # ...
  def crash
    @user = User.first
    @user.i_dont_exist()
  end
end

# add to `config/routes.rb`
get "crash" => 'root_pages#crash'
git add -A
git commit -m "add sentry"
git push stage master:master
heroku config:set SENTRY_DSN=YOU_GET_THIS_FROM_SENTRY

Authentication - Devise

# add to Gemfile
gem 'devise'
gem 'devise-async'
bundle install
rails g devise:install   # create basic files
rails g devise:views     # generate views for customization
# edit following settings in `config/initializers/devise.rb`
config.mailer_sender = ENV["MAILER_SENDER"]
config.paranoid = true
config.pepper = "100_character_random_string"
# create `config/initializers/devise_async.rb`
Devise::Async.backend = :sidekiq
Devise::Async.queue = :emails
# add to `app/views/layouts/application.html.erb`
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>

Bind devise to User model:

# attach devise to the user model
rails g devise User
# edit `app/models/user.rb` to include more features as needed
# at least add :async
class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable,
         :confirmable, :lockable, :timeoutable, :async
end
# go to the generated migration
# - change all `string` to `text`
# - uncomment features you want to use
# - emove email column and index as you already have email defined
bundle exec rake db:migrate

Configure routes:

# replace in `config/routes.rb`
devise_for :users,
  path: "user",
  path_names: {
    sign_in: "sign-in",
    sign_out: "sign-out",
    password: "password",
    confirmation: "confirmation",
    unlock: "unlock",
    registration: "registration",
    sign_up: "sign-up"
  }

Usage:

# add this to all controllers that require authentication
before_action :authenticate_user!
before_action :authenticate_user!, only: [:destroy]
before_action :authenticate_user!, except: [:index]

# following methods are usable in controllers and views
user_signed_in?
current_user
user_session
after_sign_in_path_for
after_sign_out_path_for

# following helpers are available in views
new_user_session_path         # => GET login page
new_user_password_path        # => GET start password reset
new_user_registration_path    # => GET start registration
destroy_user_session_path     # => DELETE here to logout
edit_user_registration_path   # => GET edit email and password
cancel_user_registration_path # => GET cancel (delete) user account

# example navigation:
<div>
  <%= link_to "Sign In", new_user_session_path %>
  <% if user_signed_in? %>
    <%= link_to "Edit", edit_user_registration_path %>
    <%= link_to "Sign Out", destroy_user_session_path, method: :delete %>
  <% end %>
</div>
git add -A
git commit -m "add devise"
git push stage master:master
heroku run rake db:migrate
heroku restart

Authorization - CanCanCan

Install cancancan:

gem 'cancancan', '~> 1.10'
gem 'role_model'
bundle install
rails g migration AddRolesMaskToUser roles_mask:integer
rails g cancan:ability
# edit the migration
class AddRolesMaskToUser < ActiveRecord::Migration
  def change
    add_column :users, :roles_mask, :integer, default: 0, null: false
  end
end
# `app/models/ability.rb` defines all user permissions
class Ability
  include CanCan::Ability

  def initialize(user)
    return if user.nil?
    # generic and low level permissions top
    can :show,   User if user.has_role? :member
    can :update, User if user.has_role? :moderator
    can :edit,   User if user.has_role? :moderator
    can :manage, :all if user.has_role? :admin
  end
end
# edit `app/models/user.rb` to include role information
class User < ActiveRecord::Base
  include RoleModel
  roles_attribute :roles_mask
  roles :admin, :moderator, :member
end
# use controller level macros to limit actions
class UsersController < ApplicationController
  before_action :authenticate_user!, except: [:index]
  load_and_authorize_resource :except => [:index]
  # => automatically sets @user and check for permission to the action
end
bundle exec rake db:migrate

Editing roles:

User.valid_roles                # get all roles
User.mask_for :admin, :member   # calculate role mask for this set of roles

u.roles                             # get user's roles
u.roles = [:member, :moderator]     # set roles
u.roles << :member                  # add roles
u.has_role? :member                 # does user have the role?
u.has_any_role? :member, :manager   # does user have any of the roles?
u.has_all_roles? :member, :manager  # does user have all of the roles?
u.save

Ability definitions:

# :manage and :all are special permission definitions,
# allows all actions on all targets
can :manage, :all

# the first argument is `ability action` as a symbol,
# may or may not be tied to controller actions
can :update, :all

# second argument is `target scope`, usually a Rails model
# but can also be any symbol
can :update, User
can :conquer, :the_world

# you can also deny actions
# allow doing everything except destroy action
can :manage, Project
cannot :destroy, Project

# third argument is an optional method hash for additional model filtering
# (Article instance must have method `published` that returns true)
can :update, Article, :published => true

# check if owner
can :manage, Article, :user_id => user.id

Ability checks:

# returns true or false
# note that third argument method hash is ignored on class checks
can? :destroy, Article
cannot? :destroy, Article

# if you have the third argument specified and require an instance
can? :destroy, @article
cannot? :destroy, @article

# can current user update all articles?
Article.accessible_by(current_ability).count == Article.count

# will raise CanCan::AccessDenied if not allowed by the current user
def show
  @article = Article.find(params[:id])
  authorize! :show, @article
end

# you can use `authorize_resource` to add `authorize!` check before each action
class ArticlesController < ActionController::Base
  authorize_resource
  def discontinue
    # Automatically does the following:
    # authorize! :discontinue, Article
  end
end

class ArticlesController < ActionController::Base
  load_and_authorize_resource :except => :index
  skip_load_resource :only => :new
  def discontinue
    # Automatically does the following:
    # @article = Article.find(params[:id])
    # authorize! :discontinue, @article
  end
  def new
    # Automatically does the following:
    # authorize! :new, Article
  end
end

Handling unauthorized access error:

class ApplicationController < ActionController::Base
  rescue_from CanCan::AccessDenied do |exception|
    redirect_to root_url, :alert => exception.message
  end
end

Sources