28 Oct 2014
1. Create vote controller action
class PostsController < ApplicationController
def vote
binding.pry
end
end
Entering params in rails console, will show us that we passing an id parameter but not the
boolean to indicate true/false, whether the post is up or down.
2. Add boolean parameter to views to create link
Add boolean parameter to views to create link /posts/2/vote?vote=true¶m2=2
#posts/index.html.erb
<div class='row'>
<div class='span0 well text-center'>
<%= link_to vote_post_path(post, vote: true), method: 'post' do %>
3. Now that we have our thing we are voting on and our vote, we can create our vote action
class PostsController < ApplicationController
before_action :set_post, only: [:show, edit, :update, :vote]
def vote
#post = Post.find(params[:id]) or use @post(created in the before_action)
@vote = Vote.create(voteable: @post, creator: current_user, vote: params[:vote])
if @vote.valid?
flash[:notice] = "Your vote was recorded"
else
flash[:error] = "Your vote was not recorded"
end
redirect_to :back (to go back to the original page viewed)
end
end
4. Wire up the same for the down vote in the views/posts/index
<div class='row'>
<div class='span0 well text-center'>
<%= link_to vote_post_path(post, vote: false), method: 'post' do %>
5. What if I want to display the number of votes
6. What if I want the number of votes to decrease when pressing the down button.
Although this may seem like a presentational concern that would require a helper
method - however storing the number of votes is a data concern and not a presentational one.
Method regarding business logic belong in the model itself.
class Post < ActiveRecord::Base
def total_votes
up_votes - down_votes
end
def up_votes
self.votes.where(vote: true).size
end
def down_votes
self.votes.where(vote: false).size
end
end
So now in the presentation layer, we can call total_votes:
7. What if we wanted to sort
class PostsController < ApplicationController
def index
@posts = Post.all(|x| x.total_votes).reverse
end
end
8. How can we make sure that each user only votes once for post:
There are many ways to accomplish this but the best way would be to add the
uniqueness validation to the model:
class Vote < ActiveRecord::Base
validates_uniqueness_of :creator, scope: :voteable
end
9. Using html_safe
Rails by default escapes everything so user input html, js or script tags are escaped to
prevent malicious intent.
In order to write html in any place besides the html.erb files, you must append html_safe
at the end of your statement
def vote
#post = Post.find(params[:id]) or use @post(created in the before_action)
@vote = Vote.create(voteable: @post, creator: current_user, vote: params[:vote])
** We do not need to use strong_params because we are not using the key value
pairs for mass assignment, instead we are hard coding in the keys.
if @vote.valid?
flash[:notice] = "Your vote was recorded"
else
flash[:error] = "Your vote was <strong>not</strong> recorded".html_safe
end
redirect_to :back #(to go back to the original page viewed)
end
This will cause the rails to not escape those tags and apply the html. You generally do not
want to add html_safe on user input elements.
26 Oct 2014
Items Covered
- polymorphic associations
- databases (syntax)
- models (syntax)
- Voting
Polymorphic Associations:
At the database level:
When you have one subject but many objects for example, 1 comment or like can be placed on
many objects or foreign keys - postid, photoid, video_id, etc.
- This creates alot of spaces and ineffiencies in the database.
This is solved by restricting the database from:
"id, body, user_id, post_id, photo_id, video_id" to
"id, body, user_id, commentable type, commentable_id"
commentable_type: must be the model name, capitialize first initial, string
commentable_id: is the primary_key on the 1 side.
commentable_type to commentable_id: is a composite foreign_key
In our application, we will implement voting using polymorphic associations and we can
practically vote on anything, but in this case, we will vote on just posts and comments.
Step 1: Generate Migration
Create votes table
=>rails g migration create_votes
Step 2: Generate Table
create_table :votes do |t|
t.boolean :vote
t.integer :user_id
t.string :voteable_type
t.integer :voteable_id
t.timestamps
end
end
or use
t.references :voteable, polymorphic: true
Step 3: Create Vote Model
class Vote < ActiveRecord::Base
belongs_to :creator, class_name: 'User', foreign_key: 'user_id'
belongs_to :voteable, polymorphic: true
end
class User < ActiveRecord::Base
has_many :votes
end
class Post < ActiveRecord::Base
has_many :votes, as: voteable
end
Do the same for comments.
The above action gives you voteable getters/setters and now you can assign a variable to your user and call .votes
So in the console:
v = Votes.first
v.voteable => nil
post = Post.first
post.votes << v
or another way to assign is
v.votable = Comments.first
v.save
In the above - the user and vote are the subjects while the comments and posts
are the objects being voted on.
Remember
- Subjects - has_many, can use same gatters and setters as ususal
- Objects - belongsto, will expect subject foreignkeys, voteable is now your getter/setter
where you pass voteable an object or set the voteable
Voting
Step 1. Now let's show votes on posts index page
#posts/index.html.erb
<div class='row'
<div class='span0 well text-center'>
<%= link_to '' do %>
<i class='icon-arrow-up'></i>
<% end %>
<br/>
<%= link_to '' do %>
<i class='icon-arrow-down'></i>
<% end %>
</div>
Step 2. How do we reflect the increase/decrease of votes every time the link is clicked:
#routes.rb
Two ways....
a) POST/votes => 'VotesController#create'
- needs to pass in two pieces of information (1 - whether its the post/comment
being voted on, 2. The post/comment id
- also this would create another top level resource)
- best for if you're voting on alot of objects
How to implement this in your routes:
resources :votes, only: [:create]
b) POST/posts/3/vote => 'PostsController#vote'
POST/posts/3/comments/4/vote => 'CommentsController#vote'
How to implement this in your routes, you must use something called a member, where
each action will be exposed to the member of that url
resources :posts, except: [:destroy] do
member do
post :vote
end
(where post is the member and vote is the action)
**rake routes | grep vote
vote_post POST /posts/:id/vote(.:format) post#vote
Step 3. Also what about if we want to see archives of our posts
# GET /posts/archives
collection do
get :archives
end
**rake routes | grep archives
archives_posts GET /posts/archives(.:format) posts#archives
So to compare using post vs get, post will require pass in an object.
So basically you can create any route that you want using member, collections, and nested resources
Steps from UX
- Lofi/hifi
- ERD
- Tables
- URL design
Step 4. Let's include these new routes in our posts/index.html.erb
#posts/index.html.erb
<div class='row'
<div class='span0 well text-center'>
<%= link_to vote_post_path(post) method: 'post' do %>
<i class='icon-arrow-up'></i>
<% end %>
<br/>
<%= link_to '' do %>
<i class='icon-arrow-down'></i>
<% end %>
</div>
You must specify method: 'post' because with out it the default will be the GET method and the link, post/id/vote will not work on a GET.
* method: 'post' create data method: There is javascript that comes with rails
that looks for anchor tags and the data-method="post", that will turn the link
into an actual form and submit content.
24 Oct 2014
Items Covererd:
- Creating/Updating User
- Login/Logout
- Memoization
Creating/Updating User
- Create resources
resources :users, only: [:create]
- Create users_controller.rb (new, create, edit, update actions)
- since :password is the virtual attribute for :passworddigest, you have to include it in strongparams:
def users_params
params.require(:user).permit(:username, :password)
end
- Create users folder under views (new.html.erb, edit.html.erb, _form.html.erb)
- Add password validations
validates :password, presence: true, on: :create, length: {minimun: 3}
- Change users#new to register under routes file to add a register path '/register':
get '/register', to: 'users#new', as: 'register'
- Add register link to _navigation.html.erb
<li>
<%= link_to 'Register', register_path %>
Login/Logout
- Create routes - *resources are reserved for models, typcially custom routes are on-offs
get '/login', to: 'sessions#new'
post '/login', to: 'sessions#create'
get '/logout', to: 'sessions#destroy'
- Create sessions_controller.rb
class SessionsController < Application.rb
def new
end
def create
Let's write pseudo code:
What are we trying to do... we want to run user.authenticate('password')
How do we accomplish this?
1. Get user object
2. See if the password matches
3. If so, log in
4. If not, error message
user = User.find_by(username: params[:username])
# we can use a local variable bc we aren't using model backed forms so persistency isn't required.
if user && user.authenticate(params[:password])
session[:user_id] = user_id
flash[:notice] = "You're logged in!"
redirect_to root_path
#never pass an object into sessions ( like @user for example)
bc sessions only have 4KBs in bandwidth and will generate a "cookie overload error" after the user carries out
so many actions.
else
flash[:error] = "There was something wrong with your username and password"
#Avoid being too specific here, (i.e. found username but wrong password)... to prevent hacking
redirect_to register_path
end
end
#Login pages should be https://, if not, passwords submitted over an http:// server are not encrypted. The https://
is not a very high added expense. You just have to purchase SSL certificate.
def delete
session[:user_id] = nil
flash[:notice] = "You've logged out"
redirect_to root_path
end
end
Create a sessions folder under views, new.html.erb file
Add login/logout link to nav bar
#_navigation.html.erb
if logged_in?
<li>Hi<%= current_user.username %></li>
<li>
<%= link_to "Log Out", logout_path %>
</li>
<% else %>
<li>
<%= link_to "Register", register_path %>
</li>
<li>
<%= link_to "Log in", login_path %>
<% end %>
</li>
- We need to create a method called loggedin (referenced above) in the applicationcontroller.rb bc we want this method accessible across the application.
application_controller.rb
helper_method :current_user, :logged_in?
def current_user
#Wpseudo-code
#If current user, return the user obj
#else return nil
@current_user ||= User.find(session[user_id]) if session[:user_id]
end
end
def logged_in?
!!current_user
end
end
Memoization
For effecient performance, we want to hit the database only once per request.
For methods that are being called multiple times, it is more effecient to save in an instance variable rather than
continously hitting the database.
- We can now hide links based on whether a user is logged in or not. Let's hide the 'edit' link
#posts/index.html.erb
<h3> Welcome </h3>
<ul>
<% @posts.each do |post| %>
<li>
<%= link_to post.title, post_path(post)
<% if logged_in? %>
[<%= link_to 'Edit', edit_post_path(post) %>]
- We also want to make sure you can't go to the link via url
#posts_controller.rb
before_action :require_user,except: [:index, :show]
- We must create a require_user method
#application_controller.rb
def require_user
if !logged_in?
flash[:notice] = "Must be logged in to do that"
redirect_to root_path
end
- Set up beforeaction under the commentscontroller.rb, also.
#comments_controller.rb
class CommentsController < ApplicationController.rb
before_action :require_user
def create
@comment.creator = current_user #change from @comment_creator = User.first
end
end
- Define post.creator as current_user
#posts_controller.rb
def create
@post.creator = current_user
end
Remember - when you want to lock down a peice of functionality, you have to
1. Remove it from the user interface (links)
2. Set up a before_action
22 Oct 2014
Items Covered:
- Quiz Lesson 2 Review
- Asset Pipeline
- Authentication from scratch
** has_secured_password
** password_digest
** bcrypt
** validations
** helper methods
** before_action
Quiz Review
- Remember that the show, update, and delete actions routes require an object and not an (:id).
- REST is url convention pattern means that url endpoints are exposed using the resources keyword.
These urls correlate with the CRUD actions on the controller side.
- Difference b/w model and non-model backed helpers is that
- Model backed helpers are tied to an object
- There is slight syntactical difference:
- non-model: text_field_tag :title
- model: f.text_field :title
There is no additional functionality with non-model backed forms.
Non-model forms are used for elements that are not models... such as passwords but model-backed forms are used for CRUD actions for models.
Asset Pipeline:
obfuscates your js code to make it unreadable in production (to prevent unallowable use) and then allows for better response time bc
many browsers can only allow for 10 items to load at a time. Thats why we have to use an asset library that caches
jammit
is a good open source obfuscator, and sprockets
Functions:
1. Obfusgation
2. File compression
3. Timestamp (Cache Buster)
Can be found under the production application -> public/assets folder
Goal is to take all the js files, obfusgate, jam it into one file under the public/assets file
Cache buster
is the string tied to the asset level or DNS level
-different browsers have diff means of caching and thats why Google chrome is considered to be
faster... it's because Chrome caches agressively. The cachebuster gives a reference number in which the browser and
application can manage/track caches to allow refresh of new information or changes.
Application.js is a manifesto file where the obfuscated js code resides (using Sprockets as the asset manager). Same goes for Application.scss
So how/does Sprockets compile your files under assets and places them under public/assests folder in production:
There are two asset management approaches:
- As a development team,every time a change is made, run rake assets:precompile
to make changes to static assests, commit changes and move those files to our production
- We want to delete all files under public and when we deploy, we will run rake assets:precompile at deployment only and then that will
take our code and turn into static assets.
###Which is better:
Option 2 is better but your deployment could take longer (maybe 5 mins)
We will be following Option 2 and waiting to precompile bc Heroku does it for us.
We should delete the public/assets folder. If we don't, Heroku will delete the rake assets:precompile command
and when changes are made, they will not be reflected in your production code.
Another common problem is when running locally, Js files will be picked in both the application.js file
and the assets folder therefore in our case.. when we use js to build our voting functionality, you will have to vote twice
This is all solved if you remove your public/assets directory
How password authentication works!
Passwords should never be saved in your application, in fact, your application should never know your password
and shouldn't be able to send you your 'forgotten password' via email.
How passwords are saved is through a one-way hash. We hash the string (password) into some one-way
indecipherable hash token. When you attempt to enter in your set password, the string will also be converted
into a one-way indecipherable has and its determined if they match.
This password method is vulnerable to dictionary attacks/rainbow attacks, that's why they say pick a password with a capital letter
and a special character.
- So we need a column added to the user table to store the hash token and we will call that, password_digest:
- In terminal - rails g migration add_password_to_users
_ Open migration file:
- def change
add_column :users, :password_digest, :string
- rake db:migrate
2. Go to User model and add has_secure_password
3. Add 'bcrypt-ruby' to your gemfile - contains the complex algorithms to create our one-way hash
Use 'bcrypt-ruby', =3.0.1 if you have installation problems during bundle intall
has_secure_password
4. has_secure_password has validations functionality where password cannot be blank, so add validations: false
has_secure_password validations: false
Tip: Use gems that do one thing really well not multiple things.
5. has_secure_password gives us multiple methods we can now use:
1. password setter: u.password = "apple", when run u.save, you will see that password is just a
virtual attribute, and "apple" was saved to the password_digest column and encrypted using bcrypt.
2. There is NO getter, so you can't run u.password and expect to get the value.
3. But there is method called u.authenticate('password'), where you pass in a password that you are trying to check against
in the database.
- If the password is not correct, false will be returned.
- If correct, the user will be returned.
Authenticate is an instance method being called on the User class
-
20 Oct 2014
Comments - using Nested Routes
def create
@post = Post.find(params[:post_id])
@comment = Comment.new(params.require(:comment).permit(:body))
@comment.post = @post
end
OR
def create
@post = Post.find(params[:post_id])
@comment = @post.comments.build(params.require(:comment).permit(:body))
end
What we've covered:
Creating a Nested Resource
Creating a model-backed form in a nested resource way... to generate a nested URL
In your controller Create action, you must set up the parent object in the controller action, so that it can be called on in your form_for
Under the error condition, you have to render 'posts/show' rather than 'new'