Jacy Wang

Ruby on Rails Developer/Digital Analyst

Polymorphic Associations

In our app postit, the vote function is applied to both Post model and Comment model. A post has many votes and a comment also has many votes. In this case, we will use polymorphic associations.

First we will add votes table, rails generate migration create_votes.

1
2
3
4
5
6
7
8
9
10
11
12
13
class CreateVotes < ActiveRecord::Migration
  def change
    create_table :votes do |t|
      t.boolean :vote
      t.integer :user_id
      # t.references :voteable, polymorphic: true
      t.string :voteable_type
      t.integer :voteable_id

      t.timestamps
    end
  end
end

In the command line, run rake db:migrate.

:voteable_type column will record the model class name and :voteable_id will record the object ID that’s voted on.

Then the following changes should be applied,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# vote.rb
class Vote < ActiveRecord::Base
  belongs_to :creator, foreign_key: "user_id", class_name: "User"
  belongs_to :voteable, polymorphic: true

  validates_uniqueness_of :creator, scope: :voteable
end

# post.rb
class Post < ActiveRecord::Base
  has_many :votes, as: :voteable
end

# comment.rb
class Comment < ActiveRecord::Base
  has_many :votes, as: :voteable
end

# user.rb
class User < ActiveRecord::Base
  has_many :votes
end

Now we can retrieve @post.votes and set the vote by vote.post = Post.first. Good to go!

Login/Logout From Scratch

In general, we can use authentication gems like Devise and OmniAuth to build up the login and logout process. But here, we will start from scratch and implement it in our postit.

First we will add has_secure_password to the user model. In the user.rb file,

1
2
has_secure_password validates: false
validates :password, on: :create, length: {minimum: 5}

The above code adds virtual attribute password and its validations only happen when the user is created.

Then password_digest column should be added to users table to save the digested password string from the user input.

User new form will be modified to add the password field.

1
2
3
4
<div class="content-group">
  <%= f.label :password %>
  <%= f.password_field :password %>
</div>

To change the route /users/new to /register,

1
get '/register', to: 'users#new'

Now we are ready to add the login/logout route, non-model backed form and sessions controller.

In the route.rb, we will add the following routes,

1
2
3
get '/login', to: 'sessions#new'
post '/login', to: 'sessions#create'
get 'logout', to: 'sessions#destroy'

Then we will add the sessions_controller.rb,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class SessionsController < ApplicationController
  def new
  end

  def create
    user = User.find_by(username: params[username])
    if user && user.authenticate(params[:password])
      session[:user_id] = user.id
      flash[:notice] = "You've logged in!"
      redirect_to root_path
    else
      flash[:error] = "There were something wrong with your username or password."
      render :new
    end
  end

  def destroy
    session[:user_id] = nil
    flash[:notice] = "You've logged out!"
    redirect_to root_path
  end
end

Following the actions in sessions#controller, we will need new.html.erb.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<%= render "shared/content_title", title: "Log in" %>

<div class="well">
  <%= form_tag "/login" do %>
    <div class="content-group">
      <%= label_tag :username %>
      <%= text_field_tag :username, params[:username] || "" %>
    </div>
    <div class="content-group">
      <%= label_tag :password %>
      <%= password_field_tag :password, params[:password] || "" %>
    </div>

    <%= submit_tag "Submit", class: "btn btn-primary" %>
  <% end %>
</div>

Now the login and logout structure is ready to use and in order to make the life easier to modify the view templates, we need several helper methods.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def current_user
  @current_user ||= User.find_by(sessions[:user_id]) if session[:user_id]
end

def logged_in?
  !!current_user
end

def require_user
  unless logged_in?
    flash[:error] = "This action is not allowed."
    redirect_to root_path
  end
end

VoilĂ ! Next step will be to customize the view templates to logged_in user and not.

Has_secure_password

How do we save password to the database in rails?

First, add has_secure_password method to the User model which will allow us to save the password through “one-way hash”, meaning the hash of the password strings will each turn into long, undecipherable tockens.

Second, add a new column password_digest to the users table to save the password token. The password string should never be stored in the application and they can only be digested. If you register at a site and they send you your password in plain text, do not trust the site.

Then make sure gem bcrypt-ruby installed.

After the three steps, virtual attributes password and password_confirmation and authenticate() method can be used in our app.

When user.authenticate(password) is true, it will return the user object. Otherwise, it will return false.

ActiveRecord and Association Review

This is a review on what I have been through in Week 6 of TeaLeaf Academy.

Rails follow Active Record pattern to perform ORM duties. What’s the Active Record pattern?

  • classes to tables
  • objects to rows of data within that table
  • getters/setters(attributes) to columns in that table

We can create, retrieve, update, and delete the object instances by altering the database table.

To create a table in the database, we can use migrations, rails generate migration create_posts. In the migration file,

1
2
3
4
5
6
7
8
9
10
11
class CreatePosts < ActiveRecord::Migration
  def change
    create_table :posts do |t|
      t.string :url
      t.string :title
      t.text :description

      t.timestamps
    end
  end
end

After we run rake db:migrate in the command line, it will create six columns in the database,

  1. primary key: id
  2. url
  3. title
  4. description
  5. created_at
  6. updated_at

Then in the app/models folder, we will create the corresponding model,

1
2
class Post < ActiveRecord::Base
end

The file name will be in singular form, post.rb.

Tips on table name: use the tableize method on the class name. For example, 'Post'.tableize => “posts”.

If we have a Comment model and there is a 1:M association between Post and Comment, a foreign_key column - post_id need to be added to the comments table and it will point to the primary key column in the posts table. Then in the Post and Comment model file, we will do the following changes.

1
2
3
4
5
6
7
class Post < ActiveRecord::Base
  has_many :comments
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

If there is a Category model and it has a M:M association with Post, a joint model and table will be needed to build up the association. The model file name will be post_category.rb and class name will be PostCategory. To get the table name, 'PostCategory'.tableize and the output is ‘post_categories’. There will be two foreign key columns in the table, post_id and category_id. In the model file, we will set up the has_many :through.

1
2
3
4
5
6
7
8
9
10
class Category < ActiveRecord::Base
  has_many :post_categories
  has_many :posts, through: :post_categories
end

class Post < ActiveRecord::Base
  has_many :comments
  has_many :post_categories
  has_many :categories, through: :post_categories
end

To change the default association name,

1
2
3
class Post < ActiveRecord::Base
  belongs_to :creator, foreign_key: 'user_id', class_name: 'User'
end

This will make post.creator work.

Keyword resources can be used to generate routes. resources :posts will make the following routes available,

1
2
3
4
5
6
7
8
9
   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
          | PATCH | /posts/:id(.:format)      | posts#update
          | PUT   | /posts/:id(.:format)      | posts#update

To create nested routes like /posts/:id/comments/:id, we can use nested resources,

1
2
3
resources :posts, except: [:destroy] do
  resources :comments, only: [:create]
end

except and only keywords can be used to add constrains on the routes.

Post-it Note: Sinatra

As mentioned in my previous blog, I just built up a simple online Blackjack game based on Sinatra. Here is the link and you can play with it. Sinatra is a DSL to build up web applications quickly in Ruby with minimal efforts. It’s such a great tool that I want to write a post-it note.

Include the gem require 'sinatra' and then gem install sinatra

Routes

1
2
3
4
5
6
7
get '/' do
  ...code...
end

post '/' do
  ...code...
end

Render template

1
2
3
erb :index
# Disable default layout
erb :index, layout: false

Helpers

1
2
3
helpers do
  ...code...
end

Before filter

1
2
3
before do
  ...code...
end

Halt the request

1
halt erb(:index)

For more information, check out its official intro.

Review on Introduction to Ruby and Web Development

Just deployed the Blackjack game to heroku and this marks the end of TeaLeaf Academy first course - Introduction to Ruby and Web Development.

I started the program on Oct 26th, 2014 and it took me exactly one month to complete the course. I wouldn’t say it’s a hard course, but absolutely not easy. It has been such a great experience for me that I would highly recommend.

For future reference, I will list what I have done and learnt here.

Github repos

  1. Paper Rock Scissors - Procedural and OOP

  2. Tic Tac Toe - Procedural and OOP

  3. Madlibs - Procedural

  4. Blackjack - Procedural, OOP and Sinatra version

Concepts and tips to note

  • Namespace is a way to group classes in Ruby and differentiate from other classes with the same name.

  • To remove a directory with files inside, rm -R dir.

  • Use pry to debugging.

1
2
require 'pry'
binding.pry
  • OOP introduction from zetcode.com.

  • There are so manny more to write that I will put them in separate blogs.

Tools and resources

Start My New Journey

Today is Nov 23rd, 2014 and I just finished Lesson 3 on TeaLeaf Academy.

It has been three weeks since I joined the program and I was surprised by the progress I made. In three weeks, I built up three games in two different approach, procedural and OO. Then game Blackjack was upgraded from the command line to the web by using Sinatra. And today I have my own blog based on Octopress. This is awesome!

Before joining the program, I have been going through different tutorials online but in the end, I’m still wondering where I should start and lacking confidence in building up web applications. After doing a lot of research and visiting bootcamps in Toronto, I chose TeaLeaf because it’s online, cheaper and I don’t need to quit my full time job. Its alumni also gave very positive feedback.

Why am I doing this? To make more money. Yes, that’s why I started. But as I get more and more knowledge and practice, it becomes more and more interesting for me. I love it! Two hours a day from Monday to Friday and over 10 hours in the weekend were spent on it. I’m addicted to it, like chocolate.

I know it’s a long process and requires patience and commitment to become really good at it. But it’s never too late to start. I would like to record this experience and write down the tips I learnt and mistakes I made.

Hello world!