Rails, Part 2
Quick review of Product ‘crud’
- open the controller
before_action
:- Goal is to reduce redundant typing.
- Tradeoff is that things happen “by magic” for reasons that are not obvious (especially to beginners)
Product.new
: Create a new object by calling the class methodnew
- constructor is named
init
- constructor is named
Product.all
:- Every model class has a set of class methods that retrieve rows
all
: All rowsfind
: fetch the row with the given idwhere
: fetch rows matching the given condition.
save
vsupdate
save
saves the current state of the object to the DBupdate
takes parameters, updates the attributes to match the parameters, then saves.
!
and?
methods:- Methods in ruby can end with
?
or!
. - These symbols have no semantic meaning, but by convention
?
used for methods that return boolean!
used for potentially destructive operations (e.g.,destroy!
)- Some methods have both versions.
save
try “nicely” to save and return boolean to indicate success.save!
try to save and raise an exception if attempt fails
- Methods in ruby can end with
- If
save
is successful, notice the redirect- and that the new location is referenced by name (not hard-coded URL)
- Also notice
notice
Magic
- Notice that there are no instance methods in
Product
- However, you can call
p.title
,p.price
, etc. - How are these defined?
- Meta-programming
class MyClass
def say_hello()
puts "Hello!"
end
end
o = MyClass.new
o.say_hello
# o.do_something # <=== no such method.
new_method_name = 'do_something'
MyClass.define_method(new_method_name) {
puts "Running the new method"
}
o.do_something
- Rails can fetch names of columns from DB, iterate over the list and define new methods on the fly.
- This type of “magic” makes Rails code easy to write (the methods you need just “appear”); but, it can occasionally be frustrating since methods seem to appear out of nowhere.
Validation
- add ` validates :title, :description, :image_url, presence: true
to
products.rb`- The last parameter
presence: true
is an example of a “keyword parameter”. - This sets up a map of rules to check before
save
is allowed to succeed
- The last parameter
- Try to add item with missing parameter.
- Explain
render :new
(i.e., which view) - Show how the errors are detected and displayed
- Explain
- Other validators in the book
validates :title, :description, :image_url, presence: true
validates :title, uniqueness: true
validates :image_url, allow_blank: true, format: {
with: %r{\.(gif|jpg|png)\z}i,
message: 'must be a URL for GIF, JPG or PNG image.'
}
validates :price, numericality: { greater_than_or_equal_to: 0.01 }
Unit tests
- Unit tests on the models verifies that the business logic is correct.
- Right now, the main thing to test is that the validation is correct.
test "product attributes must not be empty" do
product = Product.new
assert product.invalid?
assert product.errors[:title].any?
assert product.errors[:description].any?
assert product.errors[:price].any?
assert product.errors[:image_url].any?
end
Or to be more specific
test "product price must be positive" do
product = Product.new(title: "My Book Title",
description: "yyy",
image_url: "zzz.jpg")
product.price = -1
assert product.invalid?
assert_equal ["must be greater than or equal to 0.01"],
product.errors[:price]
- This may seem trivial for “presence” or “>0” tests, but mistakes still happen.
- More important for things like the URL test, where the code is more complex.
- Helps reduce the need for highly repetitive end-to-end tests
- Note: I was taught that each test should test one thing.
- This book does not subscribe to that.
- You are welcome to do either.
Fixtures
- Most tests need a set of good model objects.
- Also, we want the database to begin each test in a known state.
- Fixtures represent this set of objects used for testing.
- Syntax is YAML
one:
title: MyString
description: MyText
image_url: lorem.jpg
price: 9.99
two:
title: MyString
description: MyText
image_url: lorem.jpg
price: 9.99
- reference as
products(:one)
Store controller
sanitize
: “Safely” render html
Yield in Ruby
def my_map(array)
i = 0
answer = []
while i < array.count
answer << yield(array[i])
i += 1
end
answer
end
squares = my_map([8, 6, 7, 5, 3, 0, 9]) {|n| n*n}
puts(squares)
Layout
- Look at
application.html.erb
- Sets up the basic outline/layout of every page
- calls
yield
to generate the specific content for the page
Cookies / Sessions
- Next step is to add a shopping cart.
- We need some way of finding your shopping cart when you come back to a web page.
Make a cart
- Generate a new
Cart
Resource.- Notice this creates a table with only default columns.
- The use of a module (i.e., “mixin”) allows us to get/set the cart from multiple controllers.
- Generate a
LineItem
Resource- Notice how this applies database design/normalization rules.
- Notice that the
LineItem
references aCart
and aProduct
bin/rails generate scaffold LineItem product:references cart:belongs_to
- Look at migration and schema
references
andbelongs_to
are aliases
- Look at
belongs_to
in model.- Code to fetch referenced object (e.g.,
my_line_item.product.title
) - Adds checks to make sure referenced objects exist.
- Code to fetch referenced object (e.g.,
- add
has_many :line_items, dependent: :destroy
tocart.rb
- Helps enforce consistency when deleting records.
- Also adds code to fetch referenced objects
- add
has_many_lines
andbefore_destroy
toproduct.rb
- When adding a button
button_to
creates a form- default for a button is a
POST
- default for a button is a
- Look at
LineItemsController
and noticeinclude
of concern- the
before_action
to set the cart.- (These actions apply to only certain actions.)
build
“hooks up” both references automatically
Error handling
- Flash sends data to next render.
- Instance variables would get overwritten by the controller
- Need to secure every route.
_url
is full URL with protocol, domain name, etc._path
is just the path part (/products/1
)- Add total_sum to two models