PART 1 - Rails 5 Preview: Action Cable Part I PART 2 - Action Cable Part II: Deploying Action Cable to Heroku
rooms_controller.rb
class RoomsController < ApplicationController
def index
end
def show
end
endapp/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>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;
}config/routes.rb
Rails.application.routes.draw do
resources :rooms, only: [:index, :show]
endconfig/routes.rb
Rails.application.routes.draw do
root 'rooms#index'
resources :rooms, only: [:index, :show]
endapp/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>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>app/models/room.rb
class Room < ApplicationRecord
has_many :messages
endapp/models/user.rb
class User < ApplicationRecord
has_many :messages
endapp/models/messages.rb
class Message < ApplicationRecord
belongs_to :user
belongs_to :room
enddb/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
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
app/controllers/messages_controller.rb
class MessagesController < ApplicationController
def create
end
endapp/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>config/routes.rb
Rails.application.routes.draw do
root 'rooms#index'
resources :rooms, only: [:index, :show]
+ resources :messages, only: [:create]
endapp/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
endapp/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
endRails 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
endapp/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
endapp/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>app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def new
end
def create
end
def destroy
end
endconfig/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'
endapp/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>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
endapp/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
endapp/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
endwhich 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
endapp/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>
This is my process of figuring out the problem.
Error:
Routing Error
No route matches [GET] "/logout_path"
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
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
===
| 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
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.
===
def destroy
session[:current_user] = nil
redirect_to login_url
end
Hmmm - maybe it's not destroying
===
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
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
What is different?
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><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>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
endIn a view:
<% if logged_in? -%>Welcome, <%= current_user.name %><% end -%>
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>
- Creating and Subscribing to Channels
- Streaming and rejecting unauthorized requests
- Broadcasting messages to Channels
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>app/views/shared/_message.html.erb
<div class='row'>
<p><%= msg.content %></p>
<span><%= msg.user.name %></span>
</div>
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>app/assets/javascript/channels/message.coffee
App.message = App.cable.subscriptions.create "MessageChannel",
connected: ->
console.log('connected')Uncaught ReferenceError: App is not defined
This error tells us that we haven't hooked up the ActionCable yet
# 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()
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
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
$ 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
####Section 2 6. Streaming and Rejecting Unauthorized Requests