17 Oct 2014
Rails Model Backed Forms - Create a Resource called: Dog
Command to change port: rails s -p 3001
- Run migration
rails g create_dogs
class CreateDogs < ActiveRecord::Base
def change
create_table :dogs do |t|
t.string :name
t.integer :height_in_inches, :weight_in_lbs
end
end
end
- Create dog.rb under Models
class Dogs < ActiveRecord::Base
end
rake db:migrate or bundle exec rake:db migrate
Hop into Rails Console to add instances of Dog class
Create Resources or Routes manually
1. Create Using Resources
resources :dogs, except [:destroy]
2. Write routes manually in routes file
get '/dogs/', to: 'dogs#index'
get '/dogs/:id', to: 'dogs#show'
get 'dogs/new', to: 'dogs#new'
post '/dogs', to: 'dogs#create'
get '/dogs/:id/edit', to: dogs#edit
patch '/dogs/:id', to: dogs#update
delete 'dogs/:id', to: dogs#destroy
- Create Dogs Controller
#dogs_controller.rb
class DogsController < ApplicationController
def index
end
end
We will get an error because there no index.html.erb under the dogs folders within
the views folder
Create instance variable to pass into the template
def index
@dogs = Dogs.all
end
- Better yet, in your model, you could:
class Dog < ActiveRecord::Base
def info
"#{self.name} is #{self.height_in_inches} inches tall and weighs #{self.weight_in_lbs} lbs."
end
end
Something like the above is okay, but not okay to add html to that statement. In no way should you ever add presentational concerns at the model level. Application_helpers is for formatting logic on the user interface
- A better solution is to add this code to the application_helper.
#application_helper.rb
def dog_info(dog)
"#{dog.name} is #{dog.height_in_inches} inches tall and weighs #{dog.weight_in_lbs} lbs."
end
#index.html.erb
<h2>All the dogs </h2>
<ul>
<% @dogs.each do |dog| %>
<li><%= dog_info(dog) %></li>
<% end %>
</ul>
Lets add the new action to the controller:
- Add link to navigation bar for a new dog:
<div class='nav_item'>
<%= link_to 'New Dog', new_dog_path, class: 'btn btn-success btn-small' %>
</div>
- Must update routes to include the named route:
get '/dogs/', to: 'dogs#index'
get '/dogs/:id', to: 'dogs#show'
get 'dogs/new', to: 'dogs#new', as: 'new_dog'
post '/dogs', to: 'dogs#create'
get '/dogs/:id/edit', to: dogs#edit
patch '/dogs/:id', to: dogs#update
delete 'dogs/:id', to: dogs#destroy
- You must list routes in proper order - order matters
get 'dogs/new', to: 'dogs#new', as: 'new_dog'
get '/dogs/', to: 'dogs#index' as: 'dogs'
get '/dogs/:id', to: 'dogs#show' as: 'dog'
post '/dogs', to: 'dogs#create'
get '/dogs/:id/edit', to: dogs#edit
patch '/dogs/:id', to: dogs#update
delete 'dogs/:id', to: dogs#destroy
- Create
#new.html.erb
<h4> Create a new dog </h4>
<%= form_for @dog do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<br/>
<%= f.label :weight %>
<%= f.text_field :weight_in_inches %>
<br/>
<%= f.label :height %>
<%= f.text_field :height_in_inches %>
</br>
<%= f.submit "Create dog" %>
<% end %>
Create
def create
@dog = Dog.new(params.require(:dog).permit!)
if @dog.save
flash[:notice] = "You created a dog!"
redirect_to dogs_path
else
render :new
end
end
end
Render can take a string or a method. Form_for looks at @post and determines are you a new post or are you an existing post. If it is in a existing object, then the _method is injected to execute a patch, rather than post. Params is a nested hash.
Edit
#dogs_controller.rb
def edit
@dog = Dog.find(params[:id])
end
edit.hmtl.erb
<h4> Create a new dog </h4>
<%= form_for @dog do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<br/>
<%= f.label :weight %>
<%= f.text_field :weight_in_inches %>
<br/>
<%= f.label :height %>
<%= f.text_field :height_in_inches %>
</br>
<%= f.submit "Create dog" %>
<% end %>
Update
def update
@dog = Dog.find(params[:id])
if @dog.update?
flash[:notice] = Your dog has been updated!
redirect_to dogs_path
else
render :edit
end
end
end
Resources
So all the routes from below:
get 'dogs/new', to: 'dogs#new', as: 'new_dog'
get '/dogs/', to: 'dogs#index' as: 'dogs'
get '/dogs/:id', to: 'dogs#show' as: 'dog'
post '/dogs', to: 'dogs#create'
get '/dogs/:id/edit', to: dogs#edit
patch '/dogs/:id', to: dogs#update
delete 'dogs/:id', to: dogs#destroy
Can be replaced with resources :dogs
Add an edit link to the index page:
index.html.erb
<h2>All the dogs </h2>
<ul>
<% @dogs.each do |dog| %>
<li><%= dog_info(dog) %></li><%= link_to '[edit]', edit_dog_path(dog)%></li>
<% end %>
</ul>
Extract common code from new and edit forms into partial:
_form.html.erb
<%= form_for @dog do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<br/>
<%= f.label :weight %>
<%= f.text_field :weight_in_inches %>
<br/>
<%= f.label :height %>
<%= f.text_field :height_in_inches %>
</br>
<%= f.submit "#{@dog.new_record? ? 'Create' : 'Update'}dog" %>
new.html.erb
<h4> Create a Dog </h4>
<%= render 'form' %>
edit.hmtl.erb
<h4> Edit a Dog </h4>
<%= render 'form' %>
Validations
dog.rb
validates :name presence: true
validates :height_in_inches presence: true
validates :weight_in_lbs presence: true
rails console
Failure to satisfy strong_paramters will generate errors. There are several methods
we can call regarding errors in our rails console
dog = Dog.new(), then dog.save will generate the errors array on the object itself
1. dog.errors #will list errors
2. dog.valid? # will be false because there were errors
3. dog.errors.empty? #false
4. dog.errors.full_messages #will display an array of current error messages that you can use to display on the user interface
Display errors in _form.html.erb
_form.html.erb
<% if @dog.errors.any? %>
Please correct the following errors:
<ul>
<% @dog.errors.full_messages.each do |msg|
<li><%= msg %></li>
<% end %>
</ul>
<% end %>
<%= form_for @dog do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<br/>
<%= f.label :weight %>
<%= f.text_field :weight_in_inches %>
<br/>
<%= f.label :height %>
<%= f.text_field :height_in_inches %>
</br>
<%= f.submit "#{@dog.new_record? ? 'Create' : 'Update'}dog" %>
Field with errors
When an 'form_for' is submitted with errors, automatically insert a div called 'field_with_errors' around those fields with errors to allow for styling. This will also cause the form fields to appear wider apart (padding)
15 Oct 2014
Items Covered:
- Changes to Schema
- data definition statements vs query statements
- setting up db and creating migration
- rolling back
- How does Rails keep track of migrations we have run
- When to modify a migration
- What happens if I can't run migrations cleanly from end to end
Data Definitions
Data Definitions are to databases what metaprogramming is to code.
* statement that alter the database rather than query statements that manipulate the
data within a database
* the steup commands before we can even query the database
* statements that alter the metadata and schema
* these occur outside of the code (kinda like setting up the spreadsheet before entering data)
* migrations are a good example of this
Migrations
We never want to push our database to other developers when working on a team,
we want to push our schema however, and this is done in the form of migrations.
- by default, all sqlite3 files under db will be ignored when pushing to git, as per gitignore
- To list rake commands pertaining to db:
Database Schema Migrations
- There is table called schema_migrations which stores the migrations of all currently within
your database
- rake db:rollback will roll back you most recently ran
Rollbacks
Have you pushed the migration to github... have you shared locally?
If not, then rollback is fine, if not.. don't. Because rollback will not create a diff file name.
The code changed and its fine on your end.. but for your team members, since the filename hasn't
changed, it will cause their application to blow-up since their migration did not re-run. Instead,
create a new migration such as rails g migration rename_users. Always google, "rails migrations methods"
What happens if applications can't run cleanly from end-to-end?
One of the first thing your team-members will do is run rake db:migrate when cloning your projectand migrations that don't run smoothly is a red flag.
Schema.rb
- can also be loaded when your migrations don't run from to end-to-end.
- you can search for available commands using
10 Oct 2014
Items covered:
- Nested Resources
- Helpers
- Fat Models, Thin Controllers
- Drop-downs
- Application Helpers
Review nested resource
Setting up categories association in post form
- How to create a comment in the show posts page.
1.1 We need to know how the route to submit to:
* rake routes | grep comments (console)
or
- localhost:3000/rails/info/routes (browser)
1.2 How do we tell our model backed form to submit to the POST create comments action?
By adding @comments to our model backed form_for POST create posts:
show.html.erb
<h5>Create a Comment </h5>
<%= form_for [@post, @comment] do |f| #[parent obj, child obj] to create this 'posts/id/comments'
<f.text_area :body %>
<br/>
<%= f.submit "Create Comment", class: 'btn btn-primary' %>
<% end %>
If ever you feel as though you can't tell which object you are passing through the form_for:
look at the create controller action - there it will tell you where you one which object you are calling
.save on. This is the object that will generate the error if it does not pass validations.
Helpers: What if we wanted to link_to post.url in the show page?
show.html.erb
<h4> Hi there, you're viewing <%= link_to @post.title, @post.url.starts_with?('http://') ?
@post.url : "http://#{post.url}"" %></h4>
Helpers:
We don't want our model to be concerned with presentation level concerns.
application_helper.rb
module ApplicationHelper
def fix_url
str.starts_with?('http://') ? str : "http://#{str}"
end
end
show.html.erb
<h4>Hi there, you're viewing <%= link_to post.title, fix_url(post.url) %></h4>
Fat Models, Thin Controllers
Move logic to a model method
Problems with logic at the controller level:
1. Sometimes you can't tell where the input stream is (console, another action)
2. Its really hard to test
**How to associate a post and category in a form?
In order to use the form_for, you must use a getter or setter (colmun name of virtual attribute
In console:
1. post = Post.first
2. post.categories.first.name => "Rubyists"
3. post.categories = [] => Delete category associations
4. post.category_ids = [2, 3] => add category_ids 2 and 3 to post, saves to database
5. post.categories.size => 2
6. post.category_ids = [4] # removes categories previously entered and saves caegory_id 4
How to create a drop down
Learn how to do use Select forms in HTML first:
*Selecting one option:
<select>
<option value='1'>option 1</option>
<option value='2'>option 2</option>
</select>
** Selecting multiple options
<select name='post[category_ids][]' multiple ='multiple'>
<option value='1'>Rubyists</option>
<option value='2'>Pythonistas</option>
</select>
But helpers will save in memory your previous selections rather than hard coding
when edit/updating a post.
Note: that name='post[category_ids]' requires an [] for arrays.
Strong_params for checkboxes or drop-downs
-> category_ids requires an array
params.require(:post).permit(:title, :url, :description, category_ids:[])
Application_helper
Is were you store redundant, reusable code that shouldn't be stored at the model layer.
The model should never deal presentation level concerns but rather pure logic, data structures (arrays and hashes).
It should present data in whatever form and the views and helpers should just deal with it
accordingly.
Helpers are good for extracting logic that deals with the view but doesn't belong to a particular model
or resource.
07 Oct 2014
Topics Covered:
- Validation
- Errors
- General Create Pattern
- Update Path
- Form Partial
- Before_action
- Nested Resources
Validations
are added always at the model layer:
class Post < ActiveRecord::Base
has_many :comments
validates :title, :presence true
end
Now if you go into the rails console and do Post.new without a title, an error will
not occur until you try to hit the database, so either Post.save or Post.create.
When you try to do so, the console will return "False". This is because, there is
an if clause in the create method. See below. Running post.errors in your console will
print out an array called ActiveModel Errors and will tell you all fields that require
validation... in other words.. can't be blank.
def create
@post = Post.new(post_params)
if @post.save
flash[:notice] = "Your post was created."
redirect_to posts_path
else
render 'new'
end
end
Errors
post.errors will generate an array. If you want to parse out the messages...
type post.errors.full_messages
If post.save/post.create is unsuccessful, then you will 'render' the new form.
You will 'render' and not 'redirect' becasue we need to access the errors on the instance
variable in order to display the errors. When the validations arent met and it fails to save,
save, false is returned in the console and the errors array is populated. However, as
it stands, the new form will be rendered.
How to display the Errors (Rails Backed Forms Only)
<h3> Creating a Post </h3>
<% if @post.errors.any? %>
<h5> There were some errors: </h5>
<ul>
<% @post.errors.full_messages.each do |msg|
<div class='alert alert-error'>
<li><% msg %> </li>
<% end %>
</div>
</ul>
<% end %>
<h4> Rails model-backed form helpers </h4>
<%= form_for @post do |f| %> # @post could also be Post.new
<%= f.label :title %>
<%= f.text_field :title %>
<%= f.label :url %>
<% f.text_field :title %>
<br/>
<%= f.submit "Create Post", class: 'btn btn-primary' %>
<% end %>
Field with Errors
Rails creates a div class to allow you to easily point out or style your fields with
errors called "div class = fields with errors"
Form_for does alot of guessing
It guesses that New forms should go to /'posts'
If you:
<%= form_for @post do |f| %> # @post could also be Post.new
If you:
<%= form_for Post.first do |f| %> # Will bring up the first Post object
When doing so, that Post.first is calling
method='POST' with a route of '/posts/id' - which is not a listed route, therefore
should not be valid.
But instead of erroring out, it is going to the post/update controller action.
Why?
This is bc the in addition to the Utf8 and the authenticity token, there is hidden
method called _method = 'patch'
_method is what Rails uses to determine which HTTP verb is used in the routes file
or input method. This is because not every browser supports all HTTP methods, just most likely GET and POST.
If the object is an existing object, Rails will automatically give it an
_method = "PATCH" and the route will become '/post/id'
The new post form and the edit post form can be identical because method in formfor will take care of it.
Now you can create a partial to extract the form code that will be used for both
new and update partials
So now:
1. Create a partial called _form.html.erb under posts
2. Extract common code
_form.html.erb
<% if @post.errors.any? %>
<h5> There were some errors: </h5>
<ul>
<% @post.errors.full_messages.each do |msg|
<div class='alert alert-error'>
<li><% msg %> </li>
<% end %>
</div>
</ul>
<% end %>
<h4> Rails model-backed form helpers </h4>
<%= form_for @post do |f| %> # @post could also be Post.new
<%= f.label :title %>
<%= f.text_field :title %>
<%= f.label :url %>
<%= f.text_field :title %>
<br/>
<%= f.submit(@post.new_record? ? "Create Post" : "Update Post", class:
'btn btn-primary' %>
<% end %>
- Then update new_html.erb
<h3> Creating a Post </h3>
<%= render 'form' %>
- Then do the same for edit.html.erb
<h3> Edit a Post </h3>
<%= render 'form' %>
BUT you must also ensure that you have all your necessary instance variables
created so in your controller
def edit
@post = Post.find(params[:id])
end
which will route to 'post/:id/edit'... named route edit_post_path(post)
UPDATE
def update
@post = Post.find(params[:id])
if @post.update(post_params)
flash[:notice] = "The post has been updated"
redirect_to posts_path
else
render :edit
end
end
Remember since your _form.html.erb is being used by four actions:
new, create, edit, and update,
So you have to make sure you have @post defined in each action in your controller
or an exception will be thrown. Consequences of dry.
Before_action
For Rails 4, its beforeaction
For Rails 3, its beforefilter
Since we are using @post = Post.find(params[:id])
several times in our controller by various actions (show, edit, and update), we should create a method called setpost and pass it the beforeaction, then we can remove @post from those
class PostController < ApplicationController
before_action :set_post, only: [:show, :edit, :update]
def show
end
def edit
end
def update
end
def set_post
@post = Post.find(params[:id])
end
end
We use before_actions for two reasons
1. Set up an instance variable
2. Set up a redirect based on some condition (authentication)
Nested Resources
Nested Routes
- Redirect => URL
- Render => template
How to create a comment on the Show Post page:
Routes File:
resources :posts, except: [:destroy] do
resources :comments, only: [:create]
end
show.html.erb (show posts page)
<h5> Create a Comment </h5>
<%= form_for [@post, @ comment] %>
<%= f.text_area :body %>
<br/>
<%= f.submit "Create Comment" class: "btn btn-primary" %>
<% end %>
'/posts/3/comments'
* We are trying to @post must be an existing post and @comment has to be an existing
comment.
PostsController.rb
def show
@comment = Comment.new
end
Create a CommentsController
comments_controller.rb
class CommentsController < ApplicationController
def create
@post = Post.new(params[:post_id]) #Always run params in binding.pry (in your controller) to check the attribute name. Nested routes always have the parent route appended to the id, therefore it would be 'post_id'
@comment = Comment.new(params.require(:comment).permit(:body))
#we could also associate comment and post
@comment.post = @post.comments
or
you could change the above line for @ comments to create an in memory post.comment
object with that has the post_id already filled out.
@comment = @post.comments.build(params.require(:comment).permit(:body))
if @comment.save
flash[:notice] = "Your comment was added"
redirect_to post_path(@post)
else
render 'posts/show' #must be a template file... you are trying to indicate
where the file is located
end
end
#typially, we would just create a seperate method for comment_params, we aren't doing
that because we aren't trying to reuse this line of code for the Update action, only create.
end
CRUD
CRUD is an acronym that stands for "create", "retrieve", "update" and "delete". You'll hear many developers say something like "we just need a CRUD web interface for tickets". This is what Rails gives us out of the box: an ability to perform basic CRUD actions on a resource as a web application. Below is how the default Rails actions map to CRUD:
- Create: new, create
- Retrieve: index, show
- Update: edit, update
- Delete: destroy
Render vs Redirect
You should also understand what a render is vs a redirect. Render compiles the template
into HTML and sends the HTML back as part of the response. Redirect sends back a URL
as part of the response; there's no HTML in a redirect. Most browsers follow the redirected
URL automatically, and a new request is issued. All redirects will eventually lead to rendering of some template, otherwise your browser will display a "too many redirects" error.
Why does the URL stay at /posts when there's a validation error? Shouldn't it be /posts/new?
The request URL is what's showing up in the address bar, not the response.
The response is processed by your browser. The request URL is shown in the address bar.
In the case of a new post form submission, the request URL is /posts.
This has nothing to do with the response sent back. The URL only changes on successful
post creation because in that case the response is a redirect, and your browser issues a
new request, which changes the address bar.
05 Oct 2014
Items covered:
1. Rails Forms
2. Strong Parameters
Rails Forms
There are 3 ways to create a form
1. Pure HTML
2. Rails Forms Helpers
3. Rails model-backed Forms Helpers
- PURE HTML
<h5> Pure HTML Form </h5>
<form action='/posts' method='POST'>
Title <input type='text' name='my_title'>
<br/>
<input type='submit' class='btn btn-primary'>
</form>
Rails has something called CSFP - cross site forgery protection built into it.
It tries to prevent fake forms from submitting to your website.. without token.
This can be disabled in your application_controller.rb
In the above section, where it says
Title <input type='text name='my_title'>
Giving the title attribute a name called "my_title" gives you the ability refer to it as a hash. So params typed into pry would give you:
{"my_title" => "some title"}
or
params[:my_title] would give you "some title" returned
*Hash with indifferent access - means although the key is a string, I can refer to it as a :symbol"
So therefore, in your console, you could type:
Post.create(title: params[:my_title]) **This is called mass assignment**
- RAILS FORMS HELPERS
When creating pure html forms, use form helpers.
<h4> Rails Forms Helpers </h4>
<%= form_tag '/posts' do %>
<%= label_tag :title, "Input a title" %>
<%= text_field_tag :title %>
<br/>
<%= submit_tag "Create Post", class: 'btn btn-primary' %>
<% end %>
Form helpers, do several things:
- Automatically determines the method='POST'
B/c it is a form
- Security
Provides an UTF 8 & authenticity token in a hidden div that helps allows us to submit under cross site forgery protection
Using the label_tag (helper)
Gives more guidance to the blind. Label elements correalate with input elements and is read
by screenreaders for the hearing impaired.
- RAILS MODEL-BACKED FORMS
<h4> Rails model-backed form helpers </h4>
<%= form_for @post do |f| %>
<%= f.label :title %>
<%= f.text_field :title %>
<%= f.label :url %>
<% f.text_field :title %>
<br/>
<%= f.submit "Create Post", class: 'btn btn-primary' %>
<% end %>
You can mix and match forms and tags (from 2 and 3), if needed.
What is happening here:
The form_for is taking an object (@post). We must define @post in our controller:
def new
@post = Post.new
end
This gives us a few liberties:
Post.create(params[:post]) or Post.new(params[:post]) is effectively doing mass assignment,
using the hash assigned to (params[:post])
Associations
has_many :comments
has_many :posts
These become getters and setter bc they are virtual attributes as opposed to the
actual column names which are actual attributes
When you have a model-backed form, you must use fields that are virtual or actual attributes
because your input will attempt roll up and mass assign. This will not work if the actual
attribute does not exist.
f.label :whatever (yes)
f.text_field :whatever (no)
However, the above can be accomplished with Rails Forms Helpers (#2) or Pure HTML,
you can do what you want and just parse it out on the back-end.
URL
name = (post[:url]) is how rails magic, mass assignment occurs
In console or during binding.pry, typing params will list
UTf8
Authenticity Token
Post attributes in hash form
Commit
Action
Controller
Typing params[:post] is the same as post.
Strong Parameters
def create
post = Post.new(params[:post])
end
Will through an error in Rails 4 bc you don't have strong_parameters.
They realized that the concern regarding strong parameters and security really
didn't occur at the model layer but rather in the action based on user access,
roles, etc.
So no-longer a white list of attributes available for massassignment,
attraccessible :name :sex :location
So the Rails 4 version is:
def create
@post = Post.new(post_params)
end
private
def post_params #because we want to reuse this, so lets create a post_params method
params.require(:post).permit(:title, :url)
end
OR you can assign strong parameters based roles
def post_params
if user.admin?
params.require(:post).permit! #permit everything
else
params.require(:post).permit(:title, :url) #only permit title and url
end
end
If in the future, you add other input fields but do not update your strong params, it will just wipe away
your entry attempt, it will not throw an error as to let hackers know what ways to pass the security restriction
So Rails 3 is attraccessible :title (whitelist) and Rails 4 is strongparams in controller action.
Now that you have understood post params, next memorize the following:
def create
@post = Post.new(post_params)
if @post.save
flash[:notice] = "Your post was created."
redirect_to posts_path
else
render 'new'
end
end