2012-02-21_08.02.14
3 more weeks of core material
The building weeks there might be breakout topics
This week:
Model callbacks
User auth
Nest resources
Search
Pagination
Mailers
Environmental variables
Weeks 8-10
Twitter/Facebook auth
File uploads
Credit cards/PCI
HAML
Asset pipeline
Javascript and JQuery
AJAX
Coffeescript
Database Scaling
RSpec/Testing
Model Callbacks:
For a user story:
As a user, I want to see the total price in the shopping cart before I check out.
When jeff did it, he put it in the view. This is generally considered bad. Need to calculate outside the view.
How about showing total of the Cart?
So we need to add that column, since we don’t have one now.
Or add a method to the cart
def total sum = 0 cart_items.each do |item| sum = sum + item.product.price end end
Why not do this in the controller
Views cannot call controller methods
You also might need to know the total in another page
But do not call number_to_currency on sum in the model. Do not do any formatting in the model, do not calculate in the view.
Now he is doing total with inject method
cart_items.inject(0) do |total, item| total + item.product.price end
You give it 0 arg because that is where you will start. It is another way of doing +=
It returns the final total at the end
Or on one line:
cart_items.inject(0) { |total, item| total + item.product.price }
————————-
@fredlee from ENova is here.
How to succeed at Enova:
– Skillnacity: skill + tenacity. He thinks tenacity is important. Skill can be learned.
– Takes initiative: it is better to ask for forgiveness than permission.
– Selfish giver: willing to give and take
– Networking: Bell Labs: star engineers were good at networking
His thoughts on Design
– Every design decision trends towards “wrong”
– or, every design get a little more wrong each second. The world changes
– design does not matter. Kind of.
So what does matter?
1. Tests. Preferably automated tests that validate system behavior.
2. Short iterations
3. Courage to throw it all away.
Too much technology!
xml, html, css, js, json, erb, yaml
This too shall pass
So what stays the same?
– Users first!
– Speed/scale/performance
– Money (your ability to generate revenue validates your purpose)
Thoughts On Code Quality:
– It does not matter. As long as it works. But higher quality is better
– Lean Startup: If you do not know who the customer is, you do not know what quality is
– Quality is defined by user
– Or, there is no universal objective definition of quality
——————-
Back to Jeff
CartItem model. Create one in memory, add/edit attritbutes, save it. You might later change it, later delete it.
.new
.save
.update_attributes
.destroy
Model validations can affect some of those methods. You cannot save an invalid object.
So first some validation runs. 1 of 2 things will happen.
The Creation case or the update case.
The create path is the first time this object goes into database. It will assign an ID to that object.
If it has already been saved, it will do the update case.
Which case is run will determine the callbacks you can run.
After creation, we can update a total column in the cart.
We write a hook method for callback. Callback meaning we write it but we do not explicitly call it. Rails will call it.
So we run a migration to add a total to the cart.
rails g migration
def change add_column :carts, :total, :integer, :default => 0 end
That could also go in an “up” method
If I want to undo it, make a “down” method
def down remove_column :carts, :total end
# I missed a bit in here due to wireless issues
So now we have a total method, but Jeff already defined a total method
His version will take priority. You can redefine methods.
But it is not a good idea
So in CartItem model:
Add a callback:
after_create :increase_cart_total
Look at the guides to find the callbacks. The symbol is the name of a method that Rails will call.
def increase_cart_total cart.total += product.price cart.save end
We could do this in the cart_items_controller
But what if I manipulate my models some other way
Callback: When one model changes another model
This after_create is creation in the database, no necessarily the controller “create” method.
Model callbacks: business process events.
Controllers generate the response. If you can have models talk to each other, that is better.
Javascript and jquery use a LOT of callbacks.
1. Add a total column to the cart model
2. Show the total inside the cart in the view
3. Add an after_create callback to update the total whenever a new cart item is created
rails g migration AddTotalToCart total:integer
The user model:
1. Add “bcrypt-ruby” to your Gemfil
2. Create a User model that includes a string column named “password_digest”
3. Use “has_secure_password” in the model
The user form will have attributes password and password_confirmation, even though they are not in the database. Only password_digest is a column. has_secure_password adds those memory-only attributes.
has_secure_password will also validate that the two password fields are the same
Add a login field.
He did not get in, so he used the logger method
logger.debug “Started sign in action”
Make the link to sign out go to /logout
In routes:
get '/logout' => 'sessions#destroy', :as => :logout
Session is not a regular ActiveRecord class
In the destroy method:
reset_session redirect_to
2012-02-23_08.16.47
Authentication (who are you) versus authorization (what can you do)
Authentication: indentifying a user, making them identify themselves – usually username and password
Authorization: Permissions
Only let people check out if they are checked in. You could put this inthe shared/cart partial:
button_to “check out”, orders_url if session[:user_id].present?
Or you could do it in the checkout action
In OrdersController.create:
if session[:user_id].nil? redirect_to root_url, :notice => "Please check in" end
This could give the double render error
the redirect_to has not effect on control flow. You should end the if block with “return”
if session[:user_id].nil? redirect_to root_url, :notice => "Please check in" return end
But we would need that in the index method as well. That gets messy.
You can also call show with /orders/6
We don’t want this code in three places. How to DRY up controller code? Before filters.
before_filter :require_login def require_login if session[:user_id].nil? redirect_to root_url, :notice => "Please check in" return end @user = User.find(session[:user_id]) end
Since we get @user in the filter, we ccan remove all the User.find method calls
If the filter renders or redirects, the action will not be run.
To list all the tables from within the console:
ActiveRecord::Base.connection.tables
In shopping app, we want to remove the item from the cart.
So in CartItem we put in another callback: after_destroy
def decrease_cart_total cart.total -= product.price cart.save end
We can just call product since we have belongs_to :product in the model
after_destroy is better. It will keep it in memory after it deletes the row in the database. It is better to do after_destroy instead of before_destroy since something may go wrong between before_destroy and the actual destruction
Now we want to add some administrators to manage product catalog.
rails g migration AddAdministratorToUser administrator:boolean
Let’s change the migration file:
add_column :users, :administrator, :boolean, :default => false
you can use conditional logic in the view:
Instead of class.boolean == true
you can say
class.boolean?
<% user = User.find_by_id(session[:user_id]) %> <% if user.present? && user.administrator? %>
In all those pages. There has to be an easier way.
Put this is a helper. Partial is good for markup. Helper is good for logic.
app/helpers/application_helper.rb
def administrator? user = User.find_by_id(session[:user_id]) return user.present? && user.administrator? end
In the pages, do this:
<% if administrator? %>
Now we want to sort the data. Use the order method. We have done Product.all, Product.first, etc, now we have
Product.order("$COLUMN_NAME, DIRECTION")
You can also call it on the association proxies
@products = Product.order("name asc") Product.order("LOWER(name) asc")
To limit how much you get back:
Product.limit(n) Product.order("price desc").limit(3)
Most recently added item:
application layout file:
Above the cart:
<div id="news"> <%= Product.recent_items.each do |product| %> link_to product.name, product_url(product) <% end %> </div>
We don’t have Product.recent_items,
But we have columns in database: created_at and updated_at
Product.order("updated_at desc").limit(3)
But let’s not put that in the view. That could be in the controller instead of the view. Or in the model.
def most_recent Product.order("updated_at desc").limit(3) end
So in the view you can say Product.most_recent.each.do
But this is not an instance method. We want this to be a class method.
So you can do it this way:
def Product.most_recent Product.order("updated_at desc").limit(3) end
or
def self.most_recent Product.order("updated_at desc").limit(3) end
So you could even do this:
def self.most_recent order("updated_at desc").limit(3) end
Or self.order. But if you did that, you would still need “self” in the method signature to keep it a class method.
Image from Wikimedia, assumed allowed under Fair Use. Image from the Ambrosian Iliad, a 5th century manuscript of the Iliad.