Slide 1

Slide 1 text

Beyond Convention Over Configuration Rails 3 Best Practices

Slide 2

Slide 2 text

What I’ll Cover • Emaciated controllers • Scopes • Custom Models • Cached Counters • Security • Using Delegates • Presenters • Tips and Tricks for Views

Slide 3

Slide 3 text

Rails 3 ActiveRecord Query Interface • Options hash on find() will be deprecated in Rails 3.1 • Also find(:first) and find(:all) • Lazy loading • Fluent • Clauses are broken out into chain-able method calls

Slide 4

Slide 4 text

ActiveRecord Example Queries Post.where(:id => 1) "SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 1"

Slide 5

Slide 5 text

ActiveRecord Example Queries Post.where(:id => 1) "SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 1" .order(“id DESC”) ORDER BY id DESC

Slide 6

Slide 6 text

ActiveRecord Example Queries Post.where(:id => 1) "SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 1" .where(:can_comment => true) AND `posts`.`can_comment` = 1 .order(“id DESC”) ORDER BY id DESC

Slide 7

Slide 7 text

Emaciated Controllers

Slide 8

Slide 8 text

Emaciated Controllers • “Skinny controller, fat model.” - Jamis Buck • Controllers should only be setting up data for the view • I.e. Your controller should only be setting instance/session/cookie/flash variables

Slide 9

Slide 9 text

Emaciated Controllers 1 class AssignmentsController < ApplicationController 2 def drop_assignment 3 assignment = Assignment.find(params[:id]) 4 5 if !current_user.is_teacher? 6 flash[:notice] = "You must be a teacher to drop this assignment" 7 elsif assignment.dropped? 8 flash[:notice] = "You've already dropped this assignment" 9 else 10 assignment.dropped = true 11 assignment.save 12 flash[:notice] = "Assignment dropped" 13 end 14 15 redirect_to assignment 16 end 17 end

Slide 10

Slide 10 text

Emaciated Controllers 1 class AssignmentsController < ApplicationController 2 def drop_assignment 3 assignment = Assignment.find(params[:id]) 4 flash[:notice] = assignment.drop(current_user) 5 redirect_to assignment 6 end 7 end 8 9 class Assignment < ActiveRecord::Base 10 def drop(current_user) 11 if !current_user.is_teacher? 12 "You must be a teacher to drop this assignment" 13 elsif self.dropped? 14 "You've already dropped this assignment" 15 else 16 self.dropped = true 17 self.save 18 "Assignment dropped" 19 end 20 end 21 end

Slide 11

Slide 11 text

Filters • Filters should be used for authorization, logging, wizards, or anything that should happen before or after a controller action executes • You should not be defining instance variables in filters

Slide 12

Slide 12 text

Filters 1 class GradeBooksController < ApplicationController 2 before_filter :load_gradebook, :only => [:show, :edit, :update, :destroy] 3 def index 4 # ... 5 end 6 def show 7 # ... 8 end 9 def edit 10 # ... 11 end 12 def update 13 # ... 14 end 15 def destroy 16 # ... 17 end 18 private 19 def load_gradebook 20 @gradebook = GradeBook.find(params[:id]) 21 end 22 end 23

Slide 13

Slide 13 text

Filters 1 class GradeBooksController < ApplicationController 2 before_filter :load_gradebook, :only => [:show, :edit, :update, :destroy] 3 def index 4 # ... 5 end 6 def show 7 # ... 8 end 9 def edit 10 # ... 11 end 12 def update 13 # ... 14 end 15 def destroy 16 # ... 17 end 18 private 19 def load_gradebook 20 @gradebook = GradeBook.find(params[:id]) 21 end 22 end 23 Why?

Slide 14

Slide 14 text

Filters 1 class GradeBooksController < ApplicationController 2 before_filter :load_gradebook, :only => [:show, :edit, :update, :destroy] 3 def index 4 # ... 5 end 6 def show 7 # ... 8 end 9 def edit 10 # ... 11 end 12 def update 13 # ... 14 end 15 def destroy 16 # ... 17 end 18 private 19 def load_gradebook 20 @gradebook = GradeBook.find(params[:id]) 21 end 22 end 23 Why? Magic instance variables

Slide 15

Slide 15 text

Filters 1 class GradeBooksController < ApplicationController 2 before_filter :load_gradebook, :only => [:show, :edit, :update, :destroy] 3 def index 4 # ... 5 end 6 def show 7 # ... 8 end 9 def edit 10 # ... 11 end 12 def update 13 # ... 14 end 15 def destroy 16 # ... 17 end 18 private 19 def load_gradebook 20 @gradebook = GradeBook.find(params[:id]) 21 end 22 end 23

Slide 16

Slide 16 text

Filters 1 class GradeBooksController < ApplicationController 2 def index 3 @gradebook = load_gradebook(params[:id]) 4 end 5 def show 6 @gradebook = load_gradebook(params[:id]) 7 end 8 def edit 9 @gradebook = load_gradebook(params[:id]) 10 end 11 def update 12 @gradebook = load_gradebook(params[:id]) 13 end 14 def destroy 15 @gradebook = load_gradebook(params[:id]) 16 end 17 private 18 def load_gradebook(gb_id) 19 GradeBook.find(gb_id) 20 end 21 end

Slide 17

Slide 17 text

Filters 1 class GradeBooksController < ApplicationController 2 def index 3 @gradebook = load_gradebook(params[:id]) 4 end 5 def show 6 @gradebook = load_gradebook(params[:id]) 7 end 8 def edit 9 @gradebook = load_gradebook(params[:id]) 10 end 11 def update 12 @gradebook = load_gradebook(params[:id]) 13 end 14 def destroy 15 @gradebook = load_gradebook(params[:id]) 16 end 17 private 18 def load_gradebook(gb_id) 19 GradeBook.find(gb_id) 20 end 21 end

Slide 18

Slide 18 text

DRY • “Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.” - Dave Thomas and Andy Hunt, The Pragmatic Programmer • The method is the representation, not the instance variable.

Slide 19

Slide 19 text

Global Filters • If you set a before_filter or after_filter in the ApplicationController it will execute on all of the controllers that inherit from it • If you would like to have your controller not execute the filter use the skip_before_filter or skip_after_filter • This situation is ideal for login pages where you would redirect infinitely

Slide 20

Slide 20 text

1 class AssignmentsController < ApplicationController 2 def index 3 @assignments = Assignment.all 4 respond_to do |format| 5 format.html 6 format.xml { render :xml => @assignments.to_xml } 7 format.json { render :json => @assignments.to_json } 8 end 9 end 10 11 def show 12 @assignment = Assignment.find(params[:id]).includes(:problems).all 13 14 respond_to do |format| 15 format.html 16 format.xml { render :xml => @assignment.to_xml } 17 format.json { render :json => @assignment.to_json } 18 end 19 end 20 end Rails 3 respond_to Rails 3 respond_to has changed to make it easier to respond to different content types

Slide 21

Slide 21 text

Rails 3 respond_to Rails 3 respond_to has changed to make it easier to respond to different content types 1 class AssignmentsController < ApplicationController 2 respond_to :html, :xml, :json 3 4 def index 5 @assignments = Assignment.all 6 respond_with @assignments 7 end 8 9 def show 10 @assignment = Assignment.find(params[:id]).includes(:problems).all 11 respond_with @assignment 12 end 13 end

Slide 22

Slide 22 text

Scopes

Slide 23

Slide 23 text

Scopes • Named scopes in ActiveRecord increase readability of your queries • Default scope lets you define how your model should present data • Lambdas can be used to allow values to be passed to scopes and to bypass the ActiveRecord query cache

Slide 24

Slide 24 text

1 class GradeBooksController < ApplicationController 2 def index 3 current_classes = ClassPeriod.where(:completed => false).map {|c| c.id} 4 @classes = GradeBook.where(:class_id => current_classes) 5 end 6 end Named Scope

Slide 25

Slide 25 text

1 class GradeBooksController < ApplicationController 2 def index 3 current_classes = ClassPeriod.where(:completed => false).map {|c| c.id} 4 @classes = GradeBook.where(:class_id => current_classes) 5 end 6 end Named Scope 1 class GradeBooksController < ApplicationController 2 def index 3 current_classes = ClassPeriod.in_progress.map {|c| c.id} 4 @classes = GradeBook.classes(current_classes) 5 end 6 end 1 class ClassPeriod < ActiveRecord::Base 2 scope :in_progress, where(:completed => false) 3 end 1 class GradeBook < ActiveRecord::Base 2 scope :classes, lambda {|current_classes| where(:class_id => current_classes)} 3 end

Slide 26

Slide 26 text

Scopes vs. Methods • Some advocate using plain ol’ Ruby methods instead of using the scope method • Use methods for when passing variables into a scope because lambdas are less readable

Slide 27

Slide 27 text

Scopes vs. Methods • Some advocate using plain ol’ Ruby methods instead of using the scope method • Use methods for when passing variables into a scope because lambdas are less readable IMHO

Slide 28

Slide 28 text

Helper vs. Plain Ruby 1 class ClassPeriod < ActiveRecord::Base 2 scope :in_progress, where(:completed => false) 3 end 1 class ClassPeriod < ActiveRecord::Base 2 def self.in_progress 3 where(:completed => false) 4 end 5 end vs

Slide 29

Slide 29 text

Helper vs. Plain Ruby 1 class ClassPeriod < ActiveRecord::Base 2 scope :during_timeframe, lambda {|trange| where(:start_time => trange)} 3 end 1 class ClassPeriod < ActiveRecord::Base 2 def self.during_timeframe(trange) 3 where(:start_time => trange) 4 end 5 end vs

Slide 30

Slide 30 text

Single Table Inheritance • Kind of like a scope with class inheritance • Based on Martin Fowler’s STI pattern in Patterns of Enterprise Application Architecture • Doing a find with base class returns proper type • Adds ‘type’ column

Slide 31

Slide 31 text

How STI Looks Person Student ActiveRecord::Base Staff Administrator Teacher

Slide 32

Slide 32 text

1 Person.find(1) 2 => # 3 4 Person.find(2) 5 => # STI in Code

Slide 33

Slide 33 text

Custom Models

Slide 34

Slide 34 text

Custom Models • Rails’ ActiveModel gives you all the awesomeness of ActiveRecord without requiring SQL • Validations • Helper methods

Slide 35

Slide 35 text

1 class SupportIncident 2 include ActiveModel::Validations 3 include ActiveModel::Conversion 4 5 attr_accessor :name, :email, :customer_id, :description 6 validates_presence_of :name, :email, :description 7 validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i 8 validates_format_of :customer_id, :with => /\d{8}/i 9 10 def initialize(attributes = {}) 11 attributes.each do |name, value| 12 send("#{name}=", value) 13 end 14 end 15 16 def persisted? 17 false 18 end 19 end Needs to have persisted? method return false ActiveModel

Slide 36

Slide 36 text

1 class SupportIncident 2 include ActiveModel::Validations 3 include ActiveModel::Conversion 4 5 attr_accessor :name, :email, :customer_id, :description 6 validates_presence_of :name, :email, :description 7 validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i 8 validates_format_of :customer_id, :with => /\d{8}/i 9 10 def initialize(attributes = {}) 11 attributes.each do |name, value| 12 send("#{name}=", value) 13 end 14 end 15 16 def persisted? 17 false 18 end 19 end Needs to have persisted? method return false ActiveModel

Slide 37

Slide 37 text

Cached Counters

Slide 38

Slide 38 text

1 class Problem < ActiveRecord::Base 2 belongs_to :assignment, :counter_cache => true 3 end Adding Counter Caches 1 class Assignment < ActiveRecord::Base 2 has_many :problems 3 end

Slide 39

Slide 39 text

Adding Counter Caches 1 2 3 7 10 11 12 13 <% @assignments.each do |assignment| %> 14 15 18 21 22 <% end %> 23 24 4 5 Homework Assignment Name 6 8 Number of Problems 9 16 <%= assignment.name %> 17 19 <%= assignment.problems.size %> 20

Slide 40

Slide 40 text

Adding Counter Caches 1 2 3 7 10 11 12 13 <% @assignments.each do |assignment| %> 14 15 18 21 22 <% end %> 23 24 4 5 Homework Assignment Name 6 8 Number of Problems 9 16 <%= assignment.name %> 17 19 <%= assignment.problems.size %> 20 size? WTF?

Slide 41

Slide 41 text

Makin’ It Rain Extraneous SQL Queries assignment.problems.length Pulls all records and then .length Pulls all records and then .length assignment.problems.count COUNT(*) query COUNT(*) query assignment.problems.size COUNT(*) query No SQL Uses cache column Without Cache With Cache

Slide 42

Slide 42 text

Makin’ It Rain Extraneous SQL Queries assignment.problems.length Pulls all records and then .length Pulls all records and then .length assignment.problems.count COUNT(*) query COUNT(*) query assignment.problems.size COUNT(*) query No SQL Uses cache column Without Cache With Cache

Slide 43

Slide 43 text

Makin’ It Rain Extraneous SQL Queries assignment.problems.length Pulls all records and then .length Pulls all records and then .length assignment.problems.count COUNT(*) query COUNT(*) query assignment.problems.size COUNT(*) query No SQL Uses cache column Without Cache With Cache

Slide 44

Slide 44 text

1 desc 'Export Student data' 2 3 task :export_data => :environment do 4 csv = File.open("#{Rails.root}/export.csv") 5 Student.all.each do |p| 6 row = [s.id, s.first_name, s.last_name, s.dob, s.current_average] 7 csv.write("#{row.join(",")}\n") 8 end 9 csv.close 10 end Whuddup batches?

Slide 45

Slide 45 text

1 desc 'Export Student data' 2 3 task :export_data => :environment do 4 csv = File.open("#{Rails.root}/export.csv") 5 Student.all.each do |p| 6 row = [s.id, s.first_name, s.last_name, s.dob, s.current_average] 7 csv.write("#{row.join(",")}\n") 8 end 9 csv.close 10 end Whuddup batches? Pulls down all records!

Slide 46

Slide 46 text

Whuddup batches? 1 desc 'Export Student data' 2 3 task :export_data => :environment do 4 csv = File.open("#{Rails.root}/export.csv") 5 Student.find_each do |p| 6 row = [s.id, s.first_name, s.last_name, s.dob, s.current_average] 7 csv.write("#{row.join(",")}\n") 8 end 9 csv.close 10 end

Slide 47

Slide 47 text

Whuddup batches? 1 desc 'Export Student data' 2 3 task :export_data => :environment do 4 csv = File.open("#{Rails.root}/export.csv") 5 Student.find_each(:batch_size => 200) do |s| 6 row = [s.id, s.first_name, s.last_name, s.dob, s.current_average] 7 csv.write("#{row.join(",")}\n") 8 end 9 csv.close 10 end

Slide 48

Slide 48 text

Security

Slide 49

Slide 49 text

Don’t Implicitly Trust params • An attacker can insert something by adding a simple user[is_admin] =true to the query parameters when they create an account. • attr_protected and attr_accessible were created for this

Slide 50

Slide 50 text

1 class User < ActiveRecord::Base 2 end Get Your Attributes on the List

Slide 51

Slide 51 text

Get Your Attributes on the List 1 class User < ActiveRecord::Base 2 attr_protected :is_admin 3 end The is_admin key-value will be ignored in a hash passed over in new or update_attributes

Slide 52

Slide 52 text

Get Your Attributes on the List 1 class User < ActiveRecord::Base 2 attr_accessible :username, :password, :email 3 end This allows only the keys listed here to be used by update_attributes and new

Slide 53

Slide 53 text

1 Student.where("last_name = '#{params[:last_name]}'") Prevent SQL Injection This is 2011, everyone should be parameterizing SQL statements

Slide 54

Slide 54 text

Prevent SQL Injection This is 2011, everyone should be parameterizing SQL statements 1 Student.where("last_name = ?", params[:last_name])

Slide 55

Slide 55 text

Prevent SQL Injection This is 2011, everyone should be parameterizing SQL statements 1 Student.where("last_name = :last_name AND dob <= :birthdate", 2 {:last_name => params[:last_name], :birthdate => params [:birthdate].to_date}) 1 Student.where(:last_name => params[:last_name]) 1 Student.where(:dob => 2 (params[:begin_dob].to_date)..(params[:end_dob].to_date))

Slide 56

Slide 56 text

Delegates

Slide 57

Slide 57 text

Delegates • Delegates allow you to pass method calls to another property • Why not just call the method directly? • Law of Demeter • “Don’t talk to strangers”

Slide 58

Slide 58 text

Law of Demeter • Referrals can reference Students • Students can reference Attendance and ParentContacts • Referrals should not access ParentContacts ParentContact Student Referral Attendance

Slide 59

Slide 59 text

1 class Referral < ActiveRecord::Base 2 def email_parent 3 parent_email = self.student.parent_contact.work_email 4 # ... 5 end 6 end I’m not lazy, I’m delegating Referral doesn’t need to know about ParentContact

Slide 60

Slide 60 text

I’m not lazy, I’m delegating Referral doesn’t need to know about ParentContact 1 class Student < ActiveRecord::Base 2 has_one :parent_contact 3 delegate :work_email, :to => :parent_contact 4 end 1 class Referral < ActiveRecord::Base 2 def email_parent 3 parent_email = self.student.work_email 4 # ... 5 end 6 end

Slide 61

Slide 61 text

I’m not lazy, I’m delegating Referral doesn’t need to know about ParentContact 1 class Student < ActiveRecord::Base 2 has_one :parent_contact 3 delegate :work_email, :to => :parent_contact 4 end 1 class Referral < ActiveRecord::Base 2 def email_parent 3 parent_email = self.student.work_email 4 # ... 5 end 6 end What if parent_contact is nil?

Slide 62

Slide 62 text

I’m not lazy, I’m delegating Referral doesn’t need to know about ParentContact 1 class Referral < ActiveRecord::Base 2 def email_parent 3 parent_email = self.student.work_email 4 # ... 5 end 6 end 1 class Student < ActiveRecord::Base 2 has_one :parent_contact 3 delegate :work_email, :to => :parent_contact, :allow_nil => true 4 end Now just returns nil if parent_contact is nil

Slide 63

Slide 63 text

Presenter Pattern

Slide 64

Slide 64 text

The Presenter Pattern • Benefits include • Smaller controllers • Less confusing views • Testable • More elegant than view helpers • Stored in app/presenters • config.autoload_paths += [config.root.join("app/ presenters")]

Slide 65

Slide 65 text

1 class ProgressReportController < ApplicationController 2 def index 3 @classroom = Classroom.where(:classroom_location => params[:classroom_location], 4 :period => params[:period]).include(:students).first 5 @semester = Semester.where(:semester_number => params[:semester_number], 6 :year => params[:semester_year]).first 7 end 8 end Stand and Present

Slide 66

Slide 66 text

View to a Kill 1
2 <%= @semester.name %> <%= @semester.year %> 3
4
5
6 7 8 9 12 15 18 21 24 25 10 First Name 11 13 Last Name 14 16 Student ID 17 19 Current Average 20 22 Cumulative Average 23

Slide 67

Slide 67 text

View to a Kill 18 19 Current Average 20 21 22 Cumulative Average 23 24 25 26 27 <% @classroom.students.each do |student| %> 28 29 30 <%= student.first_name %> 31 32 33 <%= student.last_name %> 34 35 36 <%= student.student_id %> 37 38 39 <%= student.current_average(@semester) %> 40 41 42 <%= student.cumulative_average(@semester) %> 43 44 45 <% end %> 46 47 48 49

Slide 68

Slide 68 text

Stand and Present 1 class ProgressReportController < ApplicationController 2 def index 3 @classroom = Classroom.where(:classroom_location => params[:classroom_location], 4 :period => params[:period]).include(:students).first 5 @semester = Semester.where(:semester_number => params[:semester_number], 6 :year => params[:semester_year]).first 7 end 8 end

Slide 69

Slide 69 text

1 class ProgressReportController < ApplicationController 2 def index 3 @progress_report = ProgressReportPresenter.new(params) 4 end 5 end Stand and Present 1 class ProgressReportPresenter 2 delegate :students, :to => :classroom 3 delegate :year, :name, :to => :semester, :prefix => true 4 5 def initialize(params) 6 @classroom = Classroom.where(:classroom_location => params[:classroom_location], 7 :period => params[:period]).include(:students).first 8 @semester = Semester.where(:semester_number => params[:semester_number], 9 :year => params[:semester_year]).first 10 end 11 12 def get_cumulative_average_for_student(student) 13 student.cumulative_average(@semester) 14 end 15 16 def get_current_average_for_student(student) 17 student.current_average(@semester) 18 end 19 end

Slide 70

Slide 70 text

View to a Kill 1
2 <%= @progress_report.semester_name %> <%= @progress_report.semester_year %> 3
4
5
6 7 8 9 12 15 18 21 24 25 10 First Name 11 13 Last Name 14 16 Student ID 17 19 Current Average 20 22 Cumulative Average 23

Slide 71

Slide 71 text

View to a Kill 18 19 Current Average 20 21 22 Cumulative Average 23 24 25 26 27 <% @progress_report.students.each do |student| %> 28 29 30 <%= student.first_name %> 31 32 33 <%= student.last_name %> 34 35 36 <%= student.student_id %> 37 38 39 <%= @progress_report.get_current_average_for_student(student) %> 40 41 42 <%= @progress_report.get_cumulative_average_for_student(student) %> 43 44 45 <% end %> 46 47 48 49
50

Slide 72

Slide 72 text

View to a Kill 1
2 <%= @progress_report.semester_name %> <%= @progress_report.semester_year %> 3
4
5
6 <%= render :partial => 'students', :collection => @progress_report.students %> 7
8

Slide 73

Slide 73 text

View to a Kill 1
2 <%= @progress_report.semester_name %> <%= @progress_report.semester_year %> 3
4
5
6 <%= render :partial => 'students', :collection => @progress_report.students %> 7
8
1
2 <%= @progress_report.header %> 3
4
5
6 <%= render @progress_report.students %> 7
8

Slide 74

Slide 74 text

View Tips and Tricks

Slide 75

Slide 75 text

1 class ExamHelper 2 def exam_info(exam) 3 str = "
" + 4 link_to(exam) + 5 "
" 6 raw(str) 7 end 8 end Stringy Helpers

Slide 76

Slide 76 text

Stringy Helpers 1 class ExamHelper 2 def exam_info(exam) 3 content_tag :div, :id => "exam_#{exam.id}" do 4 raw( 5 link_to(exam) 6 ) 7 end 8 end 9 end

Slide 77

Slide 77 text

1 2 3 4 GradeBook.com<%= "- #{@title}" unless @title.nil? %> 5 6 7 <%= yield %> 8 9 Yielding for Content

Slide 78

Slide 78 text

Yielding for Content 1 2 3 4 GradeBook.com<%= yield(:title) %> 5 6 7 <%= yield %> 8 9 1 <% content_for(:title) do %> 2 - Classes 3 <% end %> 4 5

Slide 79

Slide 79 text

1 <%= @exam.name.titleize %> Give it the Good Ol’ College Try If name is nil then the call to the titleize method will throw an exception

Slide 80

Slide 80 text

Give it the Good Ol’ College Try 1 <%= @exam.name.try(:titleize) || "Untitled Exam" %> Calling try will return a nil if the attribute is nil, otherwise it will call the method

Slide 81

Slide 81 text

Winning at Rails • Controllers should be as small as possible • Move code into models or presenters to slim down controllers • Filters should perform an action, not set up instance variables

Slide 82

Slide 82 text

Winning at Rails • Scopes can make ActiveRecord queries more readable and understandable • Single Table Inheritance allows you to use one table for multiple types that inherit from each other

Slide 83

Slide 83 text

Winning at Rails • ActiveModel allows you to create your own models without ActiveRecord • You get validations and form_for conversions with just a couple mix-ins

Slide 84

Slide 84 text

Winning at Rails • Cache counters allow you to use database counter cache columns that return with the size method • Only size uses the column

Slide 85

Slide 85 text

Winning at Rails • Don’t implicitly trust params • Attackers can hijack those • Add attr_protected or attr_accessible to secure those properties • Whitelists are usually better (attr_accessible)

Slide 86

Slide 86 text

Winning at Rails • Law of Demeter states that classes should not know about classes in a 2nd level has-a relationship • Delegates fix that

Slide 87

Slide 87 text

Winning at Rails • The Presenter pattern gives you a way to compose view data without cluttering your controller or view with composition logic

Slide 88

Slide 88 text

Winning at Rails • Don’t use string concatenation in your view helpers • content_tag gives you a nice block syntax instead • content_for and yield blocks let you substitute content blocks

Slide 89

Slide 89 text

Thank you! [email protected] Twitter: @JesseDearing http://www.jessedearing.com