Skip to content

Instantly share code, notes, and snippets.

@jendiamond
Last active February 28, 2017 23:31
Show Gist options
  • Select an option

  • Save jendiamond/30300d39fcd1f3953f9e22e3e8faf0a0 to your computer and use it in GitHub Desktop.

Select an option

Save jendiamond/30300d39fcd1f3953f9e22e3e8faf0a0 to your computer and use it in GitHub Desktop.

$ rails new chittychat -d postgres -T

$ rails g model user name:string email:string password_digest:string

$ rails g model room name:string

$ rails g model message user:references room:references content:text

$ bundle exec rake db:migrate


[Deploying ActionCable to Heroku]

PART 1 - Rails 5 Preview: Action Cable Part I PART 2 - Action Cable Part II: Deploying Action Cable to Heroku


Navigating and Seeding the App

Create a rooms controller

rooms_controller.rb

class RoomsController < ApplicationController
  def index
  end

  def show
  end
end

Create a rooms index page

app/views/index.html

<div id='container'>
  <nav>
    <ul>
      <li><%= link_to 'Chill Out Room', '#' %></li>
      <li><%= link_to 'Hang Out Room', '#' %></li>
    <ul>
    <ul>
      <li><%= link_to 'Log Out', '#' %></li>
    <ul>
  </nav>
</div>

<div id='chat-container'>
  <header>Chill Out Room</header>
  <div id='chat-box'>
    <div class='row'>
      <p>Message Sample</p>
      <span>--author</p>
    </div>
  </div>

  <div id='form-container'>
    <%= form_for message do |f| %>
      <%= f.text_field :content, id: 'message_input' %>
      <%= f.submit 'Send Message' %>
    <%= end %> 
  </div>
</div>

Add CSS Styles to the views

app/views/index.html

#container {
  width: 700px;
  margin: auto;
}

.pull-left {
  float: left;
}

.pull-right {
  float: right;
}

nav {
  overflow: auto;
  width: 100%;
}

#chat-container {
  border: 2px blue solid;
  background-color: #f3f3f3;
}

#container header {
  background-color: blue;
  padding: 10px;
  text-align: center;
  color: #fff;
}

#chat-box {
  background-color: #fff;
  height: 300px;
  padding: 20px;
  overflow: scroll;
}

.row {
  padding: 10px;
  margin-bottom: 5px;
  border-bottom: 1px #f0f0f0 solid;
}

.row p {
  margin: 0px;
}

#form-box {
  padding: 20px;
}

#message_input {
  width: 500px;
  font-size: 14px;
}

Create routes for the Rooms

config/routes.rb

Rails.application.routes.draw do
  resources :rooms, only: [:index, :show]
end

Change routes to go rooms/index as the root page

config/routes.rb

 Rails.application.routes.draw do
   root 'rooms#index'
   resources :rooms, only: [:index, :show]
 end

Extract Shared Navigation into a Partial

app/views/shared/_nav.html.erb

<nav>
  <ul class='pull-left'>
    <li><%= 'Relax' %></li>
    <li><%= 'I need HELP with some code' %></li>
    <li><%= 'Meetup of the night' %></li>
  </ul>
  <ul class='pull-right'>
    <li><%= link_to 'Logout', '#' %></li>
  </ul>
</nav>

Create a rooms show page by cutting this from the app/views/rooms/index.html page.

Also add the nav partial
app/views/rooms/show.html

<%= render 'shared/nav' %>
<div id='chat-container'>
  <header>Relaxation Room</header>
  <div id='chat-box'>
    <div class='row'>
      <p>Sample message</p>
      <span>--author</span>
    </div>
  </div>

  <div id='form-box'>
    <%= form_for :message do |f| %>
      <%= f.text_field :content, id: 'message_input' %>
      <%= f.submit 'Send Message' %>
    <% end %>
  </div>
</div>

Add the Relationships to the Models

app/models/room.rb

class Room < ApplicationRecord
  has_many :messages
end

app/models/user.rb

 class User < ApplicationRecord
  has_many :messages
 end

app/models/messages.rb

class Message < ApplicationRecord
  belongs_to :user
  belongs_to :room
end

Seed the Database with Users and Rooms

db/seeds.rb

rooms = Room.create(
        [{name: 'Relax'},
         {name:'I need HELP with some code'},
         {name:'Meetup of the night'}])
  • $rake db:seed

Add BCrypt to your Gemfile and bundle

Gemfile

source 'https://rubygems.org'
ruby '2.3.0'

+ gem 'bcrypt', '~> 3.1', '>= 3.1.11'
gem 'coffee-rails', '~> 4.2'
gem 'jbuilder', '~> 2.5'
gem 'jquery-rails'
gem 'pg', '~> 0.18'
gem 'puma', '~> 3.0'
gem 'rails', '~> 5.0.0', '>= 5.0.0.1'
gem 'sass-rails', '~> 5.0'
gem 'turbolinks', '~> 5'
gem 'uglifier', '>= 1.3.0'

group :development, :test do
  gem 'byebug', platform: :mri
end

group :development do
  gem 'web-console'
  gem 'listen', '~> 3.0.5'
  gem 'spring'
  gem 'spring-watcher-listen', '~> 2.0.0'
end

gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
  • $bundle

*app/models/user.rb

class User < ApplicationRecord
  has_many :messages
  has_secure_password
end

more bcrypt research: https://gist.github.com/thebucknerlife/10090014


Seed Users in the database

db/seeds.rb

rooms = Room.create([
        {name: 'Hang Out'},
        {name:'Chill Out'}])

users = User.create([
        {name: 'Jane', email: '[email protected]', password: 'secret'}
        {name: 'John', email: '[email protected]', password: 'secret'}])
  • rake db:seed

Create MessagesController and Routes

Create a MessageController with a create action

app/controllers/messages_controller.rb

class MessagesController < ApplicationController
  def create
  end
end

Add url: messages_path to <%= form_for :message do |f| %>

app/views/rooms/show.html.erb

<%= render 'shared/nav' %>
<div id='chat-container'>
  <header>Relaxation Room</header>
  <div id='chat-box'>
    <div class='row'>
      <p>Sample message</p>
      <span>--author</span>
    </div>
  </div>

  <div id='form-box'>
+    <%= form_for :message, url: messages_path do |f| %>
      <%= f.text_field :content, id: 'message_input' %>
      <%= f.submit 'Send Message' %>
    <% end %>
  </div>
</div>

Add messages resources to the routes

config/routes.rb

Rails.application.routes.draw do
  root 'rooms#index'
  resources :rooms, only: [:index, :show]
+  resources :messages, only: [:create]
end

Add Message Parameters

app/controllers/messages_controller.rb

class MessagesController < ApplicationController
  def create
  end

  private

+  def message_params
+    params.require(:message).permit(:content).merge(user_id: current_user.id, room_id: current_room.id)
+  end
end

Add current_room & current_user to the ApplicationController to be able to check that it works

app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

+  private

+  def current_room
+    @room ||= Room.first
+  end

+  def current_user
+    @user ||= User.first
+  end
end

Set Fallback Location redirect_back fallback_location: root_path

Rails 5 redirect_back method

In Rails 4.x, for going back to previous page we use redirect_to :back.
However sometimes we get ActionController::RedirectBackError exception when HTTP_REFERER is not present.
This works well when HTTP_REFERER is present and it redirects to previous page.
Issue comes up when HTTP_REFERER is not present and which in turn throws exception.
To avoid this exception we can use rescue and redirect to root url.

In Rails 5, redirect_to :back has been deprecated and instead a new method has been added called redirect_back.
To deal with the situation when HTTP_REFERER is not present, it takes required option fallback_location.
This redirects to HTTP_REFERER when it is present and when HTTP_REFERER is not present then redirects to whatever is passed as fallback_location.

===

The HTTP referer is an HTTP header field that identifies the address, URI or IRI, of the webpage that linked to the resource being requested. By checking the referrer, the new webpage can see where the request originated.

In the most common situation this means that when a user clicks a hyperlink in a web browser, the browser sends a request to the server holding the destination webpage. The request includes the referrer field, which indicates the last page the user was on (the one where they clicked the link).

Referer logging is used to allow websites and web servers to identify where people are visiting them from, for promotional or statistical purposes.

===

app/controllers/messages_controller.rb

class MessagesController < ApplicationController
  def create
 +   message = Message.new(message_params).save
 +   redirect_back fallback_location: root_path
  end

  private

  def message_params
    params.require(:message).permit(:content).merge(user_id: current_user.id, room_id: current_room.id)
  end
end

app/controllers/rooms_controller.rb

class RoomsController < ApplicationController
  before_action :rooms

  def index
  end

  def show
    @messages = room.messages
  end

  private

  def rooms
    @rooms ||= Room.all
  end

  def room
    @room ||= Room.find(params[:id])
  end
end

Loop through the Messages in the Rooms Show Page @messages.each

app/views/rooms/show.html.erb

<div id='container'>
  <%= render 'shared/nav' %>
  <div id='chat-container'>
    <header>Relaxation Room</header>
    <div id='chat-box'>
+      <% @messages.each do |msg| %>
+        <div class='row'>
+          <p><%= msg.content %></p>
+          <span><%= msg.user.name %></span>
+        </div>
+      <% end %>
    </div>

    <div id='form-box'>
      <%= form_for :message, url: messages_path do |f| %>
        <%= f.text_field :content, id: 'message_input' %>
        <%= f.submit 'Send Message' %>
      <% end %>
    </div>
</div>

Adding Routes and Authentication

Add Authentication

Create the SessionsController with the new, create and destroy actions

app/controllers/sessions_controller.rb

class SessionsController < ApplicationController
  def new
  end
  
  def create
  end
  
  def destroy
  end
end

Create Routes for Login

config/routes.rb

Rails.application.routes.draw do
  root 'rooms#index'
  resources :rooms, only: [:index, :show]
  resources :messages, only: [:create]

  get '/login' => 'sessions#new'
  post '/login' => 'sessions#create'
  get '/logout' => 'sessions#destroy'
end

Create Login Page

app/views/sessions/new.html.erb

<div id='container'>
  <%= form_tag login_path do %>
    Email: <%= text_field_tag :email %>
    Password: <%= password_field_tag :password %>
    <%= submit_tag 'Submit' %>
  <% end %>
</div>

In the SessionController

Authenticate the user using the private user method in the create method

Add the session to the create and destroy methods

app/controllers/sessions_controller.rb

class SessionsController < ApplicationController

  def new
  end

  def create
    if user.try(:authenticate, params[:password])
      session[:current_user]= user.id
      redirect_to root_url
    else
      redirect_to login_url
    end
  end

  def destroy
    session[:current_user] = nil
    redirect_to login_url
  end

  private

  def user
    @user = User.find_by_email(params[:email])
  end
end

Before any action authenticating the user

Add to the create and destroy action in the ApplicationController and

Add the before_action :authenticate to the ApplicationController

app/controller/application_controller.rb

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  before_action :authenticate

  private

  def authenticate
    redirect_to login_url unless current_user
  end

  def current_room
    @room ||= Room.find(session[:current_room]) if session[:current_room]
  end

  def current_user
    @user ||= User.find(session[:current_user]) if session[:current_user]
  end
end

Add the before action to the SessionsController

app/controllers/sessions_controller.rb

class SessionsController < ApplicationController
  before_action :authenticate, except: [:new, :create]

  def new
  end

  def create
    if user.try(:authenticate, params[:password])
      session[:current_user]= user.id
      redirect_to root_url
    else
      redirect_to login_url
    end
  end

  def destroy
    session[:current_user] = nil
    redirect_to login_url
  end

  private

  def user
    @user = User.find_by_email(params[:email])
  end
end

Add the set_current_room method to the RoomsController

which sets the current room to the session if one exists

app/controllers/rooms_controller.rb

class RoomsController < ApplicationController
  before_action :rooms

  def index
  end

  def show
    set_current_room
    @messages = room.messages
  end

  private

  def set_current_room
    session[:current_room] = params[:id] if params[:id]
  end

  def rooms
    @rooms ||= Room.all
  end

  def room
    @room ||= Room.find(params[:id])
  end
end

Add the Logout path to the navbar

app/views/shares/_nav.html.erb

<nav>
  <ul class='pull-left'>
    <% @rooms.each do |room| %>
      <li><%= link_to room.name, room_path(room) %></li>
    <% end %>
  </ul>
  <ul class='pull-right'>
    <li><%= link_to 'Logout', 'logout_path' %></li>
  </ul>
</nav>

I received an Error

This is my process of figuring out the problem.

Error:
Routing Error
No route matches [GET] "/logout_path"

1. Check the config/routes

Rails.application.routes.draw do
  root 'rooms#index'
  resources :rooms, only: [:index, :show]
  resources :messages, only: [:create]

  get '/login' => 'sessions#new'
  post '/login' => 'sessions#create'
  get '/logout' => 'sessions#destroy'
end

1. Result:

When I go to config/routes Then I expect to see the get '/logout' => 'sessions#destroy'

It is the correct get request to retrieve the sessions destroy method

===

2. Run rake routes and check for the route [GET] "/logout_path"

Prefix Verb URI Pattern Controller#Action
root | GET  | /                    |  rooms#index   

rooms | GET | /rooms(.:format) | rooms#index
room | GET | /rooms/:id(.:format) | rooms#show
messages | POST | /messages(.:format) | messages#create
login | GET | /login(.:format) | sessions#new

  • | POST | /login(.:format)     | sessions#create   
    

logout | GET | /logout(.:format) | sessions#destroy

2. Result:

When I run rake routes Then I check for the route [GET] "/logout_path" I expect to see logout | GET | /logout(.:format) | sessions#destroy

I confirm that the GET method is there.
And that the path to the sessions destroy action is logout_path.

===

3. Check the destroy method in the SessionsController

  def destroy
    session[:current_user] = nil
    redirect_to login_url
  end

Hmmm - maybe it's not destroying

===

4. Stopped the server and reloaded it

Now the error is:
ActiveRecord::RecordNotFound in RoomsController#show
Couldn't find Room with 'id'=logout_path
Parameters:
{"id"=>"logout_path"}

 def room
    @room ||= Room.find(params[:id])
  end
end

5. Ran the server in an incognito window

Received the same error when I tried to log out

ActiveRecord::RecordNotFound in RoomsController#show
Couldn't find Room with 'id'=logout_path

  def room
    @room ||= Room.find(params[:id])
  end
end

6. I thought, okay, everything was working before I added the link to log out.

What is different?

7. I went back to the nav partial to see what I added.

I added the logout_path.
I looked at the other path room_path(room) and suddenly saw that it was different from the logout_path which was surrounded by quotes: 'logout_path'

I deleted them and ran the server again and everything worked.

<nav>
  <ul class='pull-left'>
    <% @rooms.each do |room| %>
      <li><%= link_to room.name, room_path(room) %></li>
    <% end %>
  </ul>
  <ul class='pull-right'>
    <li><%= link_to 'Logout', 'logout_path' %></li>
  </ul>
</nav>

8. I changed it to this, ran the server and I was able to log out.

<nav>
  <ul class='pull-left'>
    <% @rooms.each do |room| %>
      <li><%= link_to room.name, room_path(room) %></li>
    <% end %>
  </ul>
  <ul class='pull-right'>
    <li><%= link_to 'Logout', logout_path %></li>
  </ul>
</nav>

Moral of the story, paths don't have quotes.

Add a helper_method

app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  before_action :authenticate

  private

  def authenticate
    redirect_to login_url unless current_user
  end

  def current_room
    @room ||= Room.find(session[:current_room]) if session[:current_room]
  end

  def current_user
    @user ||= User.find(session[:current_user]) if session[:current_user]
  end

+  helper_method :current_user, :current_room
end

http://blog.bigbinary.com/2016/06/26/rails-add-helpers-method-to-ease-usage-of-helper-modules-in-controllers.html
https://6ftdan.com/allyourdev/2015/01/28/rails-helper-methods/

helper_method makes some or all of the controller's methods available to the view.

helper_method(*methods) public Declare a controller method as a helper. For example, the following makes the current_user controller method available to the view:

  class ApplicationController < ActionController::Base
    helper_method :current_user, :logged_in?

    def current_user
      @current_user ||= User.find_by_id(session[:user])
    end

     def logged_in?
       current_user != nil
     end
  end

In a view:

<% if logged_in? -%>Welcome, <%= current_user.name %><% end -%>


Add the current_room name to the Room Box

app/views/rooms/show.html.erb

<div id='container'>
  <%= render 'shared/nav' %>
  <div id='chat-container'>
+   <header><% current_room.name %></header>
    <div id='chat-box'>
      <% @messages.each do |msg| %>
        <div class='row'>
          <p><%= msg.content %></p>
          <span><%= msg.user.name %></span>
        </div>
      <% end %>
    </div>

    <div id='form-box'>
      <%= form_for :message, url: messages_path do |f| %>
        <%= f.text_field :content, id: 'message_input' %>
        <%= f.submit 'Send Message' %>
      <% end %>
    </div>
</div>

Section 2

  • Creating and Subscribing to Channels
  • Streaming and rejecting unauthorized requests
  • Broadcasting messages to Channels

Create channels/message.coffee

Add a javascript data-channel and data-room-id to the room show page

app/views/rooms/show.html.erb

<div id='container'>
  <%= render 'shared/nav' %>
  <div id='chat-container'>
    <header><%= current_room.name %></header>
+    <div id='chat-box' data-channel='rooms' data-room-id=<%= current_room.id %>>
      <% @messages.each do |msg| %>
        <div class='row'>
          <p><%= msg.content %></p>
          <span><%= msg.user.name %></span>
        </div>
      <% end %>
    </div>

    <div id='form-box'>
      <%= form_for :message, url: messages_path do |f| %>
        <%= f.text_field :content, id: 'message_input' %>
        <%= f.submit 'Send Message' %>
      <% end %>
    </div>
</div>

Create a partial from the content & user name div in app/views/rooms/show.html.erb

app/views/shared/_message.html.erb

<div class='row'>
  <p><%= msg.content %></p>
  <span><%= msg.user.name %></span>
</div>

Render the message partial in the rooms/show page and Add the locals variable locals: {msg: msg}

app/views/rooms/show.html.erb

<div id='container'>
  <%= render 'shared/nav' %>
  <div id='chat-container'>
    <header><%= current_room.name %></header>
    <div id='chat-box' data-channel='rooms' data-room-id=<%= current_room.id %>>
      <% @messages.each do |msg| %>
+        <%= render partial: 'shared/message', locals: {msg: msg} %>
      <% end %>
    </div>

    <div id='form-box'>
      <%= form_for :message, url: messages_path do |f| %>
        <%= f.text_field :content, id: 'message_input' %>
        <%= f.submit 'Send Message' %>
      <% end %>
    </div>
</div>

Javascript

Activate ActionCable = Turn on cable connection

app/assets/javascript/channels/message.coffee

App.message = App.cable.subscriptions.create "MessageChannel",
  connected: ->
    console.log('connected')

Inspect the console log of the rooms/show page

Uncaught ReferenceError: App is not defined

This error tells us that we haven't hooked up the ActionCable yet

Create app/assets/javascripts/cable.coffee

# Action Cable provides the framework to deal with WebSockets in Rails.
# You can generate new channels where WebSocket features live using the rails generate channel command.
#
# Turn on the cable connection by removing the comments after the require statements (and ensure it's also on in config/routes.rb).
#
#= require action_cable
#= require_self
#= require_tree ./channels
#
@App ||= {}
App.cable = ActionCable.createConsumer()

Inspect the console log of the rooms/show page

WebSocket connection to 'ws://33.33.33.33:3000/cable' failed:
Error during WebSocket handshake: Unexpected response code: 404

This error tells us that we don't have out routes set up

Mount ActionCable

config/routes.rb

Rails.application.routes.draw do
  root 'rooms#index'
  resources :rooms, only: [:index, :show]
  resources :messages, only: [:create]

  get '/login' => 'sessions#new'
  post '/login' => 'sessions#create'
  get '/logout' => 'sessions#destroy'

  # Serve websocket cable requests in-process
  + mount ActionCable.server => '/cable'
end

Look at the ActionCable Gem

$ bundle show actioncable
This returns the path where ActionCable is $ /home/vagrant/.rvm/gems/ruby-2.3.0/gems/actioncable-5.0.0.1
Open that in vim. Check out the reject method. This can be called within your application to do a check.

lib/actioncable/channel/base.rb

def initialize(connection, identifier, params = {}) 
  @connection = connection
  @identifier = identifier
  @params     = params

  # When a channel is streaming via pubsub, we want to delay the confirmation
  # transmission until pubsub subscription is confirmed.
  @defer_subscription_confirmation = false

  @reject_subscription = nil 
  @subscription_confirmation_sent = nil 

  delegate_connection_identifiers
  subscribe_to_channel
end 

...

def subscribe_to_channel
  run_callbacks :subscribe do
    subscribed
  end

  if subscription_rejected?
    reject_subscription
  else
    transmit_subscription_confirmation unless defer_subscription_confirmation?
  end
end

...

def reject_subscription
  connection.subscriptions.remove_subscription self
  transmit_subscription_rejection
end

Next do:

####Section 2 6. Streaming and Rejecting Unauthorized Requests

Create channel


Create subscription to message channel

Mount ActionCable in routes

Create message channel

Reject subscriptions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment