3- Advanced Rails



$ rails c
  1. class Movie < ActiveRecord::Base
  2.     @@grandfathered_date = Date.new(2010,2,3)
  3.   RATINGS = %w[G PG PG-13 R NC-17]  #  %w[] shortcut for array of strings
  4.   validates :title, :presence => true
  5.   validates :release_date, :presence => true
  6.   validate :released_1930_or_later # uses custom validator below
  7.   validates :rating, :inclusion => {:in => RATINGS}, :unless => :grandfathered?
  8.   def released_1930_or_later
  9.     errors.add(:release_date, 'must be 1930 or later') if
  10.       self.release_date < Date.parse('1 Jan 1930')
  11.   end
  12.   def grandfathered? ; self.release_date >= @@grandfathered_date ; end
  13.    
  14. end
  15. # try in console:
  16. m = Movie.new(:title => '', :rating => 'RG', :release_date => '1929-01-01')
  17. # force validation checks to be performed:
  18. m.valid?  # => false
  19. m.errors
  20. m.errors[:title] # => ["can't be blank"]
  21. m.errors[:rating] # => ["is not included in the list"]
  22. m.errors[:release_date] # => ["must be 1930 or later"]
  23. m.errors.full_messages # => ["Title can't be blank", "Rating is not included in the list", "Release date must be 1930 or later"]


You just have to know that when you save a model, the lifecycle accepts that validations are running in the certain point.



These are a bunch of functions that I can provide to control what happens to that point, and validation happens at a specific point in the cycle, so I have controlled before validations happen, and after the validations have run.

This is a form of Aspect-oriented programming. These functions will be injected before and after each model save, or model.valid?.

Make Checks DRY
Goal: enforce that movie names must be less than 40 characters
– Call a “check” function from every place in app where a Movie might get created or edited? That’s not DRY!
Everywhere you want to save do as follows:
check consistency
log
save
check consistency                   # you might miss to call these. It is not DRY
•  How do we DRY out cross-cutting concerns: Logically centralized, but may appear multiple places in implementation?

Aspect

•  Advice is a specific piece of code that implements a cross-cutting concern
•  Pointcuts are the places you want to “inject” advice at runtime
•  Advice+Pointcut = Aspect"
•  Goal: DRY out your code

•  Specify declaratively in model class
Validation is advice in AOP sense
– many places in app where a model could be modified/updated
– including indirectly via associations!
•  So where are the pointcuts?      (as shown above in the picture)

even though there isn't necessarily something in the source code that says, " Call this validation function," you essentially want the ability to annotate the code in different places saying, " These are the places that I would like to have it called."

So, we would want to check that the changes are valid, so the question is, " Where are the pointcuts here?" This brings to the idea of model lifecycle callbacks. This is what you can think of as rails very limited implementation of a certain type of Aspect- Oriented Programming. What actually happens when you use the active record calls like create or update attributes is that before they actually do their thing and talk to the database, there's a number of hooks that are provided where you can essentially cause other things to happen. They're called callbacks ... lifecycle callbacks.
But the idea is that this is a limited version of Aspect- Oriented Program because we don't get to decide where the pointcuts are, but the pointcuts have been determined to be, " This is where validations are run, and these are other specific places where you can add your functionality," so when you call valid explicitly, validation gets triggered; you could do that.


that we think of Validation as kind of Aspect- Oriented is because nowhere in the code in a typical active record model are you going to find something that says, " Call this everything that does validation." It's implicit that it's going to happen along these two paths.


anytime that you're going to modify model information in the database if you're doing it via one of the active record calls, they all have to funnel down one of these two paths (create/update), and both paths include the ability to call validation.

If you think of goto as bad, think of this as come-from :)  of that, if Go To is bad, think of this as Come From. Like you find yourself in a validation method, and how did you get here because there isn't a call to that method that's obvious anywhere. You just have to know that when you save a model, the lifecycle accepts that validations are running in the certain point.



So far we saw checking constraints on a model; what about constraints on call and controller actions?
The classic example of this is in most interesting applications, there are certain actions you can only do after you have logged in. There are other actions that anybody can do whether they're logged in or not, so what you really like to do is rather than putting a specific check in front of every action that requires you to be logged in, you would like declare that we say, " The following list of actions should actually do some check before calling the actual controller method to make sure ..." Let's say that the user's logged in.

want to set a filter for any action (any controller action) that requires you to be logged- in,

  1. class ApplicationController < ActionController::Base
  2.   before_filter :set_current_user, :except => '/login'         # we could've used a route helper for better readbility
  3.   protected # prevents method from being invoked by a route
  4.   def set_current_user
  5.     # we exploit the fact that find_by_id(nil) returns nil
  6.     @current_user ||= Moviegoer.find_by_id(session[:user_id]# ||= to set a variable's value if its value were 'Nothing' (false or nil in Ruby).
  7.     redirect_to '/login' and return unless @current_user
  8.   end
  9. end

before filter
,
Means that this is something that's going to happen before the controller action is called.
Which controller actions does it apply to?
Well, we're saying it applies to everything except whatever action is matched by this route.

Note:
Notice where I'm putting this filter, I'm putting it in Application Controller which is the parents in the class hierarchy sense of all of your applications' other controllers, so when you put a filter in Application Controller, it will apply to all the actions across all the controllers.
-You can also create more controller specific filter e.g. they only apply to actions on Movies.

inside the filter I am redirecting to login.
This means that the regular Controller Action that was going to run when this before filter was triggered doesn't get to run because I have terminated things with the redirect.

Caution:
Controlled filters can actually change the flow of execution


Summary
Filters declared in a controller also apply to its subclasses
–  Corollary: filters in ApplicationController apply to all controllers
•  A filter can change the flow of execution!
–  by calling redirect_to or render
– You should add something to the flash to explain to the user what happened, otherwise will manifest as a “silent failure”!

I've helped debug a lot of cases like this, where if somebody would say, " I'm clearly ... My route is correct. My route is supposed to take me into this controller action. I'm putting a debugger break point in the controller action, but when I run, I'm not hitting the debugger breakpoint. What happened?" Well it's not that you're not hitting the debugger breakpoint; it's that you're never getting to the controller action because you've got a filter earlier on that's actually stopping the show somewhere else.


Higher-order Functions 

Functions that do one or more of the following:     
  • Accept a function as an argument.    
  • Return a function as the return value.

Single Sign-on and Third-Party Authentication


So, as a general rule, even though you want sites to possibly be able to share information with each other about you, for your benefit, you don't want to do this by revealing your login information to different sites.

 
 

 The idea is pretty neat. You actually model a session as its own entity. So if you think of a session beginning at the time that I log in and ending either with me logging out or with it timing out, like my abandoning. Then we can think of a session controller, whose job it is to create and delete the session and that's where we can centralize the logic that's going to negotiate with the remote service and try to act on my behalf.




Association

class Movie < ActiveRecord::Base
  has_many :reviews
end
class Review < ActiveRecord::Base
  belongs_to :movie
end

•  Now you can say:

@movie.reviews # Enumerable of reviews
•  And also go the other way:

@review.movie # what movie is reviewed?
• You can add new reviews for a movie:                                          Association proxy methods!
@movie = Movie.where("title='Fargo'")

@movie.reviews.build(:potatoes => 5)
OR @movie.reviews.create(:newspaper=>'Chron', ...)  # how are these different from just new() & create()?
OR @movie.reviews << @new_review                # instantly updates @new_review's FK in database!
@movie.reviews.find(:first,:conditions => '...')


Summary
To add a one to many association:
1. Add has_many to owning model and belongs_to to owned model
2. Create migration to add foreign key to owned site that references owning side
3. Apply migration
4. rake db:test:prepare to regenerate test database schema


Suppose we have setup the foreign key movie_id in reviews table. If we then add has_many :reviews to Movie, but forget to put belongs_to :movie in Review, what happens?

We can say movie.reviews, but review.movie won't work

because we have already added the foreign key so the datasbe does it's job. These macros add abstraction for rails to fetch foreign keys so that we don't have to. so if we add it to one side we can use it but if we forget it to the other side that side won't work.

Many-to-Many Association

A moviegoer has many reviews and movies have many reviews, movie goer to movie is many to many

moviegoer:          has_many :reviews

movie:           has_many :reviews

review:          belongs_to :moviegoer
   belongs_to :movie            # belong to deals with foreign key management, and can belong to a lot of other model classes


All the movies reviewed by some person

moviegoer:             has_many :reviews
                        
has_many :movies, :through => :reviews

movie:            has_many :reviews
                           
has_many :moviegoers, :through => :reviews"

reviews: belongs_to :moviegoer    belongs_to :movie

•  Now you can do:
  @user.movies # movies rated by user
  @movie.users # users who rated this movie
•  My potato scores for R-rated movies"
  @user.reviews.select {
    |r| r.movie.rating == 'R' }



If we only have movies and reviews in our schema, Which of these, if any, is NOT a correct way of saving a new association, given m is an existing movie:

 ☐ Review.create!(:movie_id=>m.id, :potatoes=>5) 
 create and save in one statement. explicit assignment of movie_id is not suggested as it might be error prone. This is only suggested if dealing with a legacy schema where id might have a different name than rails's convention over configuration.
 ☐ r = m.reviews.build(:potatoes => 5)      
   r.save!
 separates create and save in two statements
 ☐ m.reviews << Review.new(:potatoes=>5)
   m.save!
 append to the end of the collection. The over-rided append auto assigns foreign keys.
All will work


does m.save also save new reviews associated with it?
yes. it might be counter intuitive as the movie table does not change but merely one review has been added but, we can save on movie itself. If you call save on an object that owns other objects it calls save on all objects it owns/has created.
how about m.destroy?
 yes all reviews associated with that movie will be destroyed also

difference between option 2 and 3: in 3 all other pending changes that are for an object that belongs to the movie also get saved, but in 2 only the review gets saved.

If we had moviegoers also, we would have to use one of the above on movie or moviegoer and set the other foreignkey explicitly.

A shortcut: has and belongs to many (habtm)

  1. class CreateGenresMovies < ActiveRecord::Migration
  2.   def up
  3.     create_table 'genres_movies', :id => false do t
  4.       t.references 'genres'
  5.       t.references 'movies'
  6.     end
  7.   end
  8.   def down
  9.     drop_table 'genres_movies'
  10.   end
  11. end

join tables express a relationship between existing model tables using FKs"

Join table has no primary key!

because theres no object being represented!

movie has_and_belongs_to_many :genres

genre has_and_belongs_to_many :movies

  @movie.genres << Genre.find_by_name('scifi')




















Subpages (2): Associations Associations
Comments