This repository contains a complete RESTful Rails-api along with a clean documentation which walks developers through all construction steps. If you wish to start with ruby on rails, this repository is a great resource for you.
- Homebrew is for downloading and installing open source apps on mac. It is an open source package manager.
- RVM is a version manager for ruby which lets you have projects which work with different versions of ruby.
- RubyGems is a package manager for installing plugins and libraries for use with ruby rails.
- A Gem is a ruby code packed for easy distribution. RubyGems.org is a public gem repository.
- Rails itself is a gem.
- Bundler is a gem that helps your rails app to load the right ruby gems.
- Rake is a gem which will make tasks.
- Install homebrew
- Install rvm: https://rvm.io/rvm/install#1-download-and-run-the-rvm-installation-script
- Find out the latest versions of rails and ruby: http://ruby-lang.org, http://rubyonrails.org/
$ rvm get head
$ rvm list known
$ rvm install 2.3.6 (or the version you want)
$ rvm list
$ rvm --default use 2.3.6 (or the version you want)
$ gem install bundler
$ gem install rails --no-document (for latest or specify a version: -v 5.0.1)
- You could have several rubies installed. For each, there could be several
gemsets
only one of which is active at a time.
$ gem list (gives list of gems in your active gemset)
$ gem update --system (update all gems)
$ rvm gemset list (list of gemsets for current ruby version)
- Install nvm using homebrew and then follow instructions outputed to activate it
nvm ls-remote
find the latest LTS version in the list and donvm install 10.16.3
- To switch node version:
nvm use 10.15.0
and to make a version default donvm alias default 10.15.0
$ cd /your/desider/path
$ rails new <name> (--api)?
Whenever you need to install a gem, add to Gemfile
:
gem 'name'
Then do:
$ bundle install
If at some point some command did not work for your project you need to put bundle exec
at front of it so that the command is executed in the context of the specific bundle of gems your project has.
$ brew install mysql
At the end it will give you a command that make mysql launch at startup. You can always manually do:
$ brew services stop/start mysql
Set password for user root:
$ mysqladmin -u root password
Add -p
if root already has a password and you want to change it.
Login to MySql, create database and a user:
$ mysql -u <user> -p
> show databases;
> create database my_db;
> grant all privileges on my_db.* to 'my_user'@'localhost' identified by 'my_pass'
Install mysql2
gem and configure config/database.yml
$ brew install mongodb
If mongodb is going to be accessed from outside of server, you need to create a user that has appropriate access on that specific db name Set password for user root:
$ mongo
> show dbs
> use admin
> db.createUser({
user: "root",
pwd: "<password>",
roles: [{ role: "dbAdminAnyDatabase", db: "admin" }]
})
Create a user for a specific db name and make that user owner of the db
> use my_db
> db.createUser({
user: "my_user",
pwd: "<password>",
roles: ["dbOwner"]
})
On server, mongodb is usually protected by authentication. If you have password for root:
mongo --port 27017 -u "root" -p "<password>" --authenticationDatabase "admin"
If you don't have password of root you need to comment out security
and authorization
in /etc/mongod.conf
, restart the mongodb service and do the above steps.
If you are making a new rails app add --skip-active-record
. If you already have an app, delete any lines starting with config.active_record
from config/environments/development
and config/environments/production
then open config/application.rb
and replace require 'rails/all'
with:
require "rails"
require "active_model/railtie"
require "active_job/railtie"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_view/railtie"
require "action_cable/engine"
require "sprockets/railtie"
require "rails/test_unit/railtie"
Install gem mongoid
Then do:
$ rails g mongoid:config
Configure config/mongoid.yml
accordingly.
Put the following code in ~/.irbrc
:
if Object.const_defined?('Rails') && Object.const_defined?('Mongoid')
Mongoid.load!("#{Rails.root}/config/mongoid.yml")
end
$ brew install postgresql
Create rails app with --database=postgresql
or install gem pg
and configure config/database.yml
$ rails db:create
For installing postgres on ubuntu in production:
$ sudo apt-get install libpq-dev
$ sudo apt-get install postgresql postgresql-contrib
Postgresql super user postgres
is used for administation and database postgres
is used for users and database data.
To reset the password of postgres
role or user:
$ sudo vim /etc/postgresql/<VERSION>/main/pg_hba.conf
Change peer
to trust
for local all postgres
and local all all
then do a sudo service postgresql restart
Now connect to psql client with postgres
user:
[Command shell with postgres user]
$ psql -U postgres
[Change super user password]
ALTER USER postgres with password 'new-password';
[OR Create super user no password]
postgres=# CREATE ROLE postgres WITH SUPERUSER CREATEDB CREATEROLE LOGIN;
[Make a special user and db]
postgres=# CREATE ROLE my_user WITH LOGIN PASSWORD 'password';
postgres=# ALTER ROLE my_user CREATEDB;
postgres=# CREATE DATABASE my_db;
postgres=# GRANT ALL PRIVILEGES ON DATABASE my_db TO my_user;
[Bonuses]
postgres=# \du
postgres=# \list
postgres=# \connect my_db
postgres=# \dt
postgres=# \q
[Syntax]
$ psql db_namee -U user
[Main DB with no user]
$ psql postgres
After this, revert the changes in pg_hba.conf
file from trust
to md5
and restart postgresql.
Importing dumps:
$ psql db_name < dump.sql
- Run server
$ rails s (production)?
- Run console
$ rails c (production)?
- Help for generating stuff
$ rails g
- Insert ruby code in views:
<% code %>
- Print value of an expression:
<%= expression %>
- Drop variables in strings:
"the beginning ... #{var} ... the end"
- Generating links, images, forms, etc. using rails helpers:
<%= image_tag('logo.png') %>
<%= link_to(name, target) %>
target
could be rails hash:
{controller: 'x', action: 'y', some_param: 't', …}
You can create as much html.erb
layouts as you wish in views/layouts
. Then put <%= yield %>
inside them to specify where you want html fragments to be rendered. In the controller class you need to specify layout 'name without html.erb>'
. The default layout is called application
.
You can exclude similar html fragments from full templates and put them in a Partial file. Partials have _
at the beginning of their name. The below code is used to yield a partial within a full template:
<%= render partial: 'name_or_path', locals: {key: value, …}) %>
Rails provide a very strong set of text, number, date and more helpers. Just search for whatever you want to do first, it is likely that a function already exists for that. You can also create custom helper functions to use in the views. There are also sanitize helpers. Consider using them. They are absolutely necessary.
Put all asset files in app/assets
for rails to be able to perform optimization processes for production environment. In development environment, rails directly uses asset files in app/assets
. In production however, it will optimize all asset files and put them in public/assets
. Different kind of assets are kept in different folders under app/assets
. In all of them there is a manifest file called application
. The comments at the beginning of these files are the manifest and are used as input to the optimization process. If you wish to process asset files of a particular kind in a certain order, you must explicitly define this order. To do that, before //= require_tree .
add //= require <asset_name>
By default, each controller action renders a template in views/<controller_name>/<action_name>.html.erb
.
To overwrite this default behavior, you can put render command in controller actions:
render template: 'path/to/the/view/template'
The template being rendered has access to all instance variables of the action which are those with @
at the beginning.
redirect_to controller: :x, action: :y
or
redirect_to 'google.com'
Request parameters can be accessed using:
params[:key]
Rails 4 has a very strong way of passing parameters when instantiating model objects in controllers called Mass Assignment. You can create a safe parameter hash and pass it to model new
or create
methods using the require
and permit
methods:
params.require(:user).permit(:username, :email, :password)
Ignore if you are using mongoid
$ rails generate migration name_for_migration
In each migration ruby class, put an up
and a down
method. These methods must be in the mirror order of each other.
In up
method u can do:
create_table :cards do |t|
t.string :name [, options]?
t.integer :student_id
t.timestamps
end
drop_table :cards
add_column :cards, :name, :string
remove_column :cards, :name
add_index :cards, :student_id
add_index :cards, [:student_id, :course_id]
Available types include:
binary, boolean, date, datetime, decimal, float, integer, string, text, time
Available options:
limit: size
default: value
null: true/false
precision: number (for decimal type only)
scale: number (for decimal type only)
Migration commands:
$ rails db:migrate:status
$ rails db:migrate
$ rails db:migrate VERSION=0
$ rails db:rollback [STEP=n]?
$ rails db:migrate(:up/down/redo)? VERSION=number
It is a good practice to add index on all foreign key columns and those columns which are used frequently to look up for a row of data.
If you have a typo in your migration code it will cause the migration run to abort and you will get stuck in a state where you can not either go up or down. In such states just comment those executed lines of the broken method and try running the migration method again.
Complete association reference
class Student < ApplicationRecord
has_one :card
end
class Card < ApplicationRecord
belongs_to :student
end
You also need to add foreign key to the belongs_to
side in the migration file:
create_table :cards do |t|
t.integer :student_id
end
When you set student.card
or card.student
, rails automatically saves the relationship in the database. Also if you set any of these to nil you will break the relationship.
class Student
include Mongoid::Document
has_one :card
# OR
embeds_one :card
end
class Card
include Mongoid::Document
belongs_to :student
# OR
embedded_in :student
end
class Course < ApplicationRecord
has_many :projects
end
class Project < ApplicationRecord
belongs_to :course
end
You also need to add foreign key to the belongs_to
side in the migration file:
create_table :projects do |t|
t.integer :course_id
end
class Course
include Mongoid::Document
has_many :projects
# OR
embeds_many :projects
end
class Project
include Mongoid::Document
belongs_to :course
# OR
embedded_in :course
end
Then you can do things like:
course.projects
course.projects << p
course.projects = [p1, p2, p3]
course.projects.destroy(p1) # destroys completely
course.projects.delete(p1) # deletes from the list of projects
course.projects.clear
course.projects.empty?
course.projects.size
If you perform any modification on course.projects
, rails will instantly sync the database accordingly.
class Student < ApplicationRecord
has_and_belongs_to_many :courses
end
class Course < ApplicationRecord
has_and_belongs_to_many :students
end
You have to generate a migration to create join table for this relationship. Rails naming convention for the name of join table is:
first_table + _ + second_table
(in alphabetical order)
The migration file should look like this:
def up
create_table :courses_students, :id => false do |t|
t.integer :student_id
t.integer :course_id
end
add_index :courses_students, [:student_id, :course_id]
end
def down
drop_table :courses_students
end
class Student
include Mongoid::Document
has_and_belongs_to_many :courses
end
class Course
include Mongoid::Document
has_and_belongs_to_many :students
end
If you change the name of the relationship from its default name, you would have to configure the relationship in order for it to work as it should:
join_table: :table_name
class_name: 'ActualClassNameInCaseYouChangeTheRelationshipKey'
foreign_key: :name_of_column_used_as_foreign_key
Again, If you perform any modification on objects in either side of such relationship, rails will instantly sync the database accordingly.
Two main types of class inheritance:
- Single Table Inheritance
- Class Table Inheritance (https://github.com/mvdamme/dbview_cti)
u = User.new
u.assign_attributes(hash) # Update without saving
u.update_attributes(hash) # Update and save
u.update_attributes!(hash) # Update and raise errors
u = User.new(hash) # initialize object
u.new_record? # has it been saved in the database?
u.save # save and return true false
u.save! # save and raise error
u = User.create(hash) # new and save
u = User.create!(hash) # new and save and raise error
u.destroy
u = User.find_by(hash)
u = User.find_or_create_by(hash) # try to find, create if not found
User.all
User.first
User.last
User.where(hash)
User.where([“column_1 = ? and column_2 > ?”, a, b])
where
, order
, offset
, and limit
are query methods available to ActiveRecord class.
Named Scopes are to make use of the above four methods but in a nicer way. For that, just add scopes to a Model:
scope :name, -> (params) { where(hash) }
and then we use it like:
Model.scope_name
List of validation functions:
validates_presence_of
# options -> :message
validates_length_of
# options -> :is, :minimum, :maximum, :within, :in, :wrong_length, :too_short, :too_long
validates_numericality_of
# options -> :equal_to, :greater_than, :less_than, :greater_than_or_equal_to, :less_than_or_equal_to, :odd, :even, :only_integer, :message
validates_inclusion_of
# options -> :in, :message
validates_exclusion_of
# options -> :in, :message
validates_format_of
# options -> :with, :message
validates_uniqueness_of
# options -> :case_sensitive, :scope, :message
validates_acceptance_of
# options -> :accept, :message
validates_confirmation_of
# options -> :message
Global Options we can use on almost all of the above:
:allow_blank
:allow_nil
on: :create/:update/:save(default)
if: :method_name
# a boolean returner function, indicating whether or not the object should be validated
There is another way to write validations called Sexy Validation:
validates
:attribute_to_check,
:presence => {}, # or a boolean
:numericality => {}, # or a boolean
:length => {},
:format => {},
:inclusion => {},
:exclusion => {},
:acceptance => {},
:uniqueness => {},
:confirmation => {}
Call .valid?
on objects to check their validity. Call .errors
on objects to see its errors.
You can also add your own custom validation methods to the model:
def :name_of_validation
# check validation criteria
# finally do:
errors.add(:attribute or :base, "msg")
end
And declare this validation on the Model:
validate :method_name
# you can also add :on or :if options here
There are:
before_create
before_save
before_validation
before_commit
before_update
after_create
after_save
after_validation
after_commit
after_update
If you want to see if any attribute has changed in after
callbacks, use self.saved_change_to_attribute?(:title)
and attribute_before_last_save(attr_name)
Read here
To set a cookies and session variables:
cookies[:key] = val
session[:key] = val
cookies[:username] = {
value: val,
expires: 1.week.from_now
}
Session data can be stored in file, database, or we can use cookies to store them. Since we are sending information to the user's local machine we need a mechanism to check whether or not they are changed by anyone other than us. Rails use the secret key stored in config/initializers/secret_token.rb
to validate the integrity of signed cookies.
$ rake secret
will produce new key for you which you can replace the old one with.
The configuration to make rails use cookies for storing sessions is stored in config/initializers/session_store.rb
To see all routes of your app, do:
$ rails routes
Below is the standard list of RESTful routes:
| URL | HTTP Method | Controller Action | Description |
|----------------------|-------------|-------------------|------------------------------------|
| /subjects | GET | index | Show all items |
| /subjects/new | GET | new | Show new form |
| /subjects | POST | create | Create an item |
| /subjects/:id | GET | show | Show item with :id |
| /subjects/:id/edit | GET | edit | Show edit form for item with :id |
| /subjects/:id | PATCH | update | Update item with :id |
| /subjects/:id/delete | GET | delete | Show delete form for item with :id |
| /subjects/:id | DELETE | destroy | Delete item with :id |
You can find routing configuration file in: config/routes.rb
First, install this gem following instruction in its repo.
Then you need to add routes for your RESTful API:
namespace :api, :defaults => {:format => :json} do
resources :users, :only => [:create]
end
Then in controller actions you render json responses:
render json: obj, status: 200
# OR
render nothing: true, status: 200
- Install
bcrypt
gem. - Add a column named
password_digest
on your users table. - Then simply add
has_secure_password
in the User model. What it does, is it adds a virtual attribute namedpassword
to that model. It also adds validation to check ifpassword
andpassword_confirmation
are present and then whether or not they match. - Set plain password on a new user object and call save. It will encrypt this password and save it in
password_digest
. - Check if user's given password is correct:
u = User.last
u.authenticate('give password') # returns false or user
- Install
bcrypt
gem - Add the following code to your User model.
class User
include Mongoid::Document
include BCrypt
field :password_hash, type: String
def password
if !password_hash.nil?
@password ||= Password.new(password_hash)
end
end
def password=(new_password)
if !new_password.blank?
@password = Password.create(new_password)
end
self.password_hash = @password
end
end
- Check if user's give password is correct:
u = User.last
u.password == 'given password'
Put the username of the logged in user in session
If your application is only a rails api, you need to generate a Json Web Token instead of cookie session and send it back to user. To create this token, follow the instructions here.
To check if user has logged in when they request for an action, first, add this method to ApplicationController:
private
def confirm_logged_in
if <token is valid>
return true
else
render nothing: true, status: 401
return false
end
end
Now you have to add this line to all controllers to check if the user has logged in before an actions is actually run:
before_action :confirm_logged_in, except: [:action_1]
# OR
before_action :confirm_logged_in, only: [:action_3]
# OR a combination of both :only and :except
- Make an ubuntu user to use for deployment
$ sudo adduser deploy
$ sudo adduser deploy sudo
$ su deploy
- Install RVM
$ sudo apt-get install libgdbm-dev libncurses5-dev automake libtool bison libffi-dev
$ gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
$ curl -sSL https://get.rvm.io | bash -s stable
$ source ~/.rvm/scripts/rvm
$ rvm install 2.3.6
$ rvm use 2.3.6 --default
- Install ca-certificates, nginx, curl, git
$ sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 561F9B9CAC40B2F7
$ sudo apt-get install -y apt-transport-https ca-certificates curl git-core nginx -y
- Now Nginx is a service
$ sudo service nginx start|stop|restart|status
- Install rails and bundler
$ gem install bundler
$ gem install rails -v 5.x.x
- Make a SSH key
$ ssh-keygen -t rsa
Enter This name: /home/deploy/.ssh/#{appname}_rsa
- Print this key, and add it to your repo keys
$ cat /home/deploy/.ssh/#{appname}_rsa.pub
- Open
~/.ssh/config
and put the following line
IdentityFile ~/.ssh/#{appname}_rsa
- Check if your access is set:
$ ssh -T git@github.com
or
$ ssh -T git@bitbucket.org
- Add to your gem file
group :development do
gem 'capistrano', require: false
gem 'capistrano-rvm', require: false
gem 'capistrano-rails', require: false
gem 'capistrano-bundler', require: false
gem 'capistrano3-puma', require: false
end
- Bundle
$ bundle install
- create capistrano files
cap install
- Edit Capfile and paste
require 'capistrano/setup'
require 'capistrano/deploy'
require "capistrano/scm/git"
install_plugin Capistrano::SCM::Git
require 'capistrano/rails'
require 'capistrano/bundler'
require 'capistrano/rvm'
require 'capistrano/puma'
install_plugin Capistrano::Puma
Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }
- Paste the following in
config/deploy.rb
and edit server_path, repo_url, application_name
# Change these
server '200.122.181.42', port: 22, roles: [:web, :app, :db], primary: true
set :repo_url, 'git@github.com:mefeghhi/poll-api.git'
set :application, 'poll-api'
set :user, 'deploy'
set :puma_threads, [4, 16]
set :puma_workers, 0
# Don't change these unless you know what you're doing
set :pty, true
set :use_sudo, false
set :stage, :production
set :deploy_via, :remote_cache
set :deploy_to, "/home/#{fetch(:user)}/apps/#{fetch(:application)}"
set :puma_bind, "unix://#{shared_path}/tmp/sockets/#{fetch(:application)}-puma.sock"
set :puma_state, "#{shared_path}/tmp/pids/puma.state"
set :puma_pid, "#{shared_path}/tmp/pids/puma.pid"
set :puma_access_log, "#{release_path}/log/puma.error.log"
set :puma_error_log, "#{release_path}/log/puma.access.log"
set :ssh_options, { forward_agent: true, user: fetch(:user), keys: %w(~/.ssh/id_rsa.pub) }
set :puma_preload_app, true
set :puma_worker_timeout, nil
set :puma_init_active_record, true # Change to false when not using ActiveRecord
## Defaults:
# set :scm, :git
# set :branch, :master
# set :format, :pretty
# set :log_level, :debug
# set :keep_releases, 5
## Linked Files & Directories (Default None):
# set :linked_files, %w{config/database.yml}
# set :linked_dirs, %w{bin log tmp/pids tmp/cache tmp/sockets vendor/bundle public/system}
namespace :puma do
desc 'Create Directories for Puma Pids and Socket'
task :make_dirs do
on roles(:app) do
execute "mkdir #{shared_path}/tmp/sockets -p"
execute "mkdir #{shared_path}/tmp/pids -p"
end
end
before :start, :make_dirs
end
namespace :deploy do
desc "Make sure local git is in sync with remote."
task :check_revision do
on roles(:app) do
unless `git rev-parse HEAD` == `git rev-parse origin/master`
puts "WARNING: HEAD is not the same as origin/master"
puts "Run `git push` to sync changes."
exit
end
end
end
desc 'Initial Deploy'
task :initial do
on roles(:app) do
before 'deploy:restart', 'puma:start'
invoke 'deploy'
end
end
before :starting, :check_revision
after :finishing, :compile_assets
after :finishing, :cleanup
end
# ps aux | grep puma # Get puma pid
# kill -s SIGUSR2 pid # Restart puma
# kill -s SIGTERM pid # Stop puma
- Edit your firewall to let incoming connection to port 22 (80 and 443)
- Create file
config/nginx.conf
and paste the following and editappname
upstream puma {
server unix:///home/deploy/apps/#{appname}/shared/tmp/sockets/#{appname}-puma.sock;
}
server {
listen 80 default_server deferred;
# server_name example.com;
root /home/deploy/apps/#{appname}/current/public;
access_log /home/deploy/apps/#{appname}/current/log/nginx.access.log;
error_log /home/deploy/apps/#{appname}/current/log/nginx.error.log info;
location ^~ /assets/ {
gzip_static on;
expires max;
add_header Cache-Control public;
}
try_files $uri/index.html $uri @puma;
location @puma {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://puma;
}
error_page 500 502 503 504 /500.html;
client_max_body_size 10M;
keepalive_timeout 10;
}
- Issue deploy command from development machine
$ cap production deploy:initial
- Create a shortcut or (symbolic link) to
config/nginx.conf
insites-enabled
$ sudo rm /etc/nginx/sites-enabled/default
$ sudo ln -nfs "/home/deploy/apps/#{appname}/current/config/nginx.conf" "/etc/nginx/sites-enabled/#{appname}"
- Restart Nginx
$ sudo service nginx restart
OR
$ sudo systemctl restart nginx.service
$ sudo journalctl -u nginx.service -f (logging live a service with name)
You should now be able to point your web browser to your server IP and see your Rails app in action!
- If you make change to
config/nginx.conf
, commit, issue a deploy command:$ cap production deploy
and restart nginx on the server:sudo service nginx restart
- You might need to add environment variables in production server to read db password or secret_key_base. To do so, add variables like below to
/etc/environment
and reboot the server:
VAR_NAME="value"
read: