testing rails' nested attributes with rspec - ruby-on-rails

I'm pretty new to testing and rails and tried to figure it out myself but without any luck.
I have the following models
class Picture < ActiveRecord::Base
belongs_to :product
has_attached_file :image
end
class Product < ActiveRecord::Base
has_many :pictures, :dependent => :destroy
accepts_nested_attributes_for :pictures, :reject_if => lambda { |p| p[:image].blank? }, :allow_destroy => true
end
and a controller which is pretty standard, I guess…
def create
#product = Product.new(params[:product])
if #product.save
redirect_to products_path, :notice => "blah."
else
render :action => "new"
end
end
how would I go about and test this? i tried something like this but I can't get it to work:
describe ProductsController do
it "adds given pictures to the product" do
product = Factory.build(:product)
product.pictures.build(Factory.attributes_for(:picture))
post :create, :product => product.attributes
Product.where(:name => product[:name]).first.pictures.count.should == 1 # or something
end
end
It probably has something to do with the way the attributes are passed to the create action but how can I get this to work? Im using rails 3.1.rc5 but i doubt that that hast anything to do with why its not working…
or would you not test this at all since it is basic rails functionality and most likely well tested to begin with?

Try:
post :create, :product => Factory.attributes_for(:product, :pictures => [ Factory.build(:picture) ])

As you say you don't really need to test this, necessarily, because it will be covered by the basic rails functionality, and stuff like this should be thoroughly covered by your integration tests.
However if you DO want to test this the best way is tail your development logs and see what is being posted to the action, copy and paste that into your test and then modify it to suite your needs.
Using attributes or the factory_girl attributes isn't going to cut it unfortunately.

Related

How do I do eager loading for multiple associations?

I am using public_activity and one of my partials I have this call:
<%= link_to "#{activity.trackable.user.name} " + "commented on " + "#{activity.trackable.node.name}", node_path(activity.trackable.node.family_tree_id,activity.trackable.node) %>
That is executed by this call:
<% #unread_act.each do |friend| %>
<li><%= render_activity friend %> </li>
<% end %>
That instance variable is assigned in my ApplicationController here:
before_filter :handle_nofications
def handle_nofications
if current_user.present?
#unread_act = Notification.where(owner_id: current_user).includes(:trackable => :user).unread_by(current_user)
end
end
I am using the gem Bullet, to find N+1 queries and the feedback I get is this:
N+1 Query detected
Comment => [:node]
Add to your finder: :include => [:node]
N+1 Query method call stack
I initially got that message for :trackable and for :user. Both of which I am now eager loading via includes within that assignment statement.
However, I have no idea how to go 3 levels deep on an eager loading - rather than just two.
Given that the node is called like activity.trackable.node.name, I imagine some appropriate eager loading assignment would look something like:
#unread_act = Notification.where(owner_id: current_user).includes(:trackable => [:user, :node]).unread_by(current_user)
But the error I get is this:
ActiveRecord::AssociationNotFoundError at /
Association named 'node' was not found on Node; perhaps you misspelled it?
I even get that when I simply do:
#unread_act = Notification.where(owner_id: current_user).includes(:trackable => :node).unread_by(current_user)
So I suspect something else is going wrong here.
Any ideas?
Edit 1
See Node & Notification models and associations below.
Node.rb
class Node < ActiveRecord::Base
include PublicActivity::Model
tracked except: :update, owner: ->(controller, model) { controller && controller.current_user }
belongs_to :family_tree
belongs_to :user
belongs_to :media, polymorphic: true, dependent: :destroy
has_many :comments, dependent: :destroy
has_many :node_comments, dependent: :destroy
Notification.rb
class Notification < PublicActivity::Activity
acts_as_readable :on => :created_at
end
This will hopefully be fixed in rails 5.0. There is already an issue and a pull request for it.
https://github.com/rails/rails/pull/17479
https://github.com/rails/rails/issues/8005
I have forked rails and applied the patch to 4.2-stable and it works great for me. Feel free to use it, even though I cannot guarantee to sync with upstream on a regular basis.
https://github.com/ttosch/rails/tree/4-2-stable
It seems the correct way to approach this is by using an array on the trackable attribute like so:
#unread_act = Notification.where(recipient_id: current_user).includes(:trackable => [:user, :node]).unread_by(current_user).order("created_at desc")
The main part is:
includes(:trackable => [:user, :node])
This seems to have worked beautifully.

Add record to a model upon create used in many models

I have a survey and I would like to add participants to a Participant model whenever a user answers to a question for the first time. The survey is a bit special because it has many functions to answer questions such as Tag words, Multiple choices and Open Question and each function is actually a model that has its own records. Also I only want the Participant to be saved once.
The Participant model is fairly simple:
class Participant < ActiveRecord::Base
belongs_to :survey
attr_accessible :survey_id, :user_id
end
The Survey model is also straightforward:
class Survey < ActiveRecord::Base
...
has_many :participants, :through => :users
has_many :rating_questions, :dependent => :destroy
has_many :open_questions, :dependent => :destroy
has_many :tag_questions, :dependent => :destroy
belongs_to :account
belongs_to :user
accepts_nested_attributes_for :open_questions
accepts_nested_attributes_for :rating_questions
accepts_nested_attributes_for :tag_questions
...
end
Then you have models such as rating_answers that belong to a rating_question, open_answers that belong to open_questions and so on.
So initially I thought for within my model rating_answers I could add after_create callback to add_participant
like this:
class RatingAnswer < ActiveRecord::Base
belongs_to :rating_question
after_create :add_participant
...
protected
def add_participant
#participant = Participant.where(:user_id => current_user.id, :survey_id => Survey.find(params[:survey_id]))
if #participant.nil?
Participant.create!(:user_id => current_user.id, :survey_id => Survey.find(params[:survey_id]))
end
end
end
In this case, I didn't know how to find the survey_id, so I tried using the params but I don't think that is the right way to do it. regardles it returned this error
NameError (undefined local variable or method `current_user' for #<RatingAnswer:0x0000010325ef00>):
app/models/rating_answer.rb:25:in `add_participant'
app/controllers/rating_answers_controller.rb:12:in `create'
Another idea I had was to create instead a module Participants.rb that I could use in each controllers
module Participants
def add_participant
#participant = Participant.where(:user_id => current_user.id, :survey_id => Survey.find(params[:survey_id]))
if #participant.nil?
Participant.create!(:user_id => current_user.id, :survey_id => Survey.find(params[:survey_id]))
end
end
end
and in the controller
class RatingAnswersController < ApplicationController
include Participants
def create
#rating_question = RatingQuestion.find_by_id(params[:rating_question_id])
#rating_answer = RatingAnswer.new(params[:rating_answer])
#survey = Survey.find(params[:survey_id])
if #rating_answer.save
add_participant
respond_to do |format|
format.js
end
end
end
end
And I got a routing error
ActionController::RoutingError (uninitialized constant RatingAnswersController::Participants):
I can understand this error, because I don't have a controller for participants with a create method and its routes resources
I am not sure what is the proper way to add a record to a model from a nested model and what is the cleaner approach.
Ideas are most welcome!
current_user is a helper that's accessible in views/controller alone. You need to pass it as a parameter into the model. Else, it ain't accessible in the models. May be, this should help.
In the end I ended up using the after_create callback but instead of fetching the data from the params, I used the associations. Also if #participant.nil? didn't work for some reason.
class RatingAnswer < ActiveRecord::Base
belongs_to :rating_question
after_create :add_participant
...
protected
def add_participant
#participant = Participant.where(:user_id => self.user.id, :survey_id => self.rating_question.survey.id)
unless #participant.any?
#new_participant = Participant.create(:user_id => self.user.id, :survey_id => self.survey.rating_question.id)
end
end
end
The cool thing with associations is if you have deeply nested associations for instead
Survey has_many questions
Question has_many answers
Answer has_many responses
in order to fetch the survey id from within the responses model you can do
self.answer.question.survey.id
very nifty!

How to make one Rails 3 route work with 2 controllers depending on database data?

I am developing a Rails 3 app on Heroku, and here is the situation:
There are two models: User and App. Both have "slugs" and can be accessed via the same url:
/slug
Example:
/myuser => 'users#show'
/myapp => 'apps#show'
What is the best practice to handle this? What clean solution should I implement?
The same logic you can see on AngelList. For example, my personal profile is http://angel.co/martynasjocius, and my app can be found at http://angel.co/metricious.
Thank you!
I would think about introducing a third model, ill call it Lookup for the example, but you will probably want to find a better name. I will also assume your User and App models also define a name field.
class Lookup < ActiveRecord::Base
belongs_to :owner, :polymorphic => true
validates_uniqueness_of :name
end
class User < Active::Record::Base
has_a :lookup, :as => :owner, :validate => true
before_create :create_lookup_record
def create_lookup_record
build_lookup(:name => name)
end
end
class App < Active::Record::Base
has_a :lookup, :as => :owner, :validate => true
before_create :create_lookup_record
def create_lookup_record
build_lookup(:name => name)
end
end
LookupsController < ApplicationController
def show
#lookup = Lookup.find_by_name(params[:id])
render :action => "#{#lookup.owner.class.name.pluralize}/show"
end
end
# routes.rb
resources :lookups
I hope this idea helps, sorry if its no use :)
Try this (substituting action for your actions, like show, edit, etc):
class SlugsController < ApplicationController
def action
#object = Slug.find_by_name(params[:slug]).object # or something
self.send :"#{#object.class.to_s.downcase}_action"
end
protected
def app_action
# whatever
end
def user_action
# something
end
end
Separate stuff out into modules as you see fit. You can have objects of each class get their own actions.

before_destroy is not firing from update_attributes

I have a student that has many courses. In the student#update action and form, I accept a list of course_ids. When that list changes, I'd like to call a certain function. The code I have does get called if the update_attributes creates a course_student, but does not get called if the update_attributes destroys a course_student. Can I get this to fire, or do I have to detect the changes myself?
# app/models/student.rb
class Student < ActiveRecord::Base
belongs_to :teacher
has_many :grades
has_many :course_students, :dependent => :destroy
has_many :courses, :through => :course_students
has_many :course_efforts, :through => :course_efforts
# Uncommenting this line has no effect:
#accepts_nested_attributes_for :course_students, :allow_destroy => true
#attr_accessible :first_name, :last_name, :email, :course_students_attributes
validates_presence_of :first_name, :last_name
...
end
# app/models/course_student.rb
class CourseStudent < ActiveRecord::Base
after_create :reseed_queues
before_destroy :reseed_queues
belongs_to :course
belongs_to :student
private
def reseed_queues
logger.debug "******** attempting to reseed queues"
self.course.course_efforts.each do |ce|
ce.reseed
end
end
end
# app/controllers/students_controller.rb
def update
params[:student][:course_ids] ||= []
respond_to do |format|
if #student.update_attributes(params[:student])
format.html { redirect_to(#student, :notice => 'Student was successfully updated.') }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => #student.errors, :status => :unprocessable_entity }
end
end
end
It turns out that this behavior is documented, right on the has_many method. From the API documentation:
collection=objects
Replaces the collections content by deleting and adding objects as appropriate. If the :through option is true callbacks in the join models are triggered except destroy callbacks, since deletion is direct.
I'm not sure what "since deletion is direct" means, but there we have it.
When a record is deleted using update/update_attributes, it fires delete method instead of destroy.
#student.update_attributes(params[:student])
Delete method skips callbacks and therefore after_create / before_destroy would not be called.
Instead of this accepts_nested_attributes_for can be used which deletes the record and also support callbacks.
accepts_nested_attributes_for :courses, allow_destroy: true
#student.update_attributes(courses_attributes: [ {id: student_course_association_id, _destroy: 1 } ])
Accepts nested attributes requires a flag to trigger nested destroy. Atleast it did back when.
If you add dependent: :destroy, it will honour that. Note that if you are using has_many through:, one needs to add that option to both.
has_many :direct_associations, dependent: :destroy, autosave: true
has_many :indirect_associations, through: :direct_associations, dependent: :destroy
(I have used that in Rails 3, I bet it will work on Rails 4 too)
Should your CourseStudent specify belongs_to :student, :dependent => :destroy, it seems like a CourseStudent record would not be valid without a Student.
Trying to follow the LH discussion I linked to above, and this one I'd also try moving the before_destroy callback in CourseStudent below the belongs_to. The linked example demonstrates how the order of callbacks matters with an after_create, perhaps the same applies to before_destroy. And of course, since you're using Edge Rails, I'd also try the RC, maybe there was a bug they fixed.
Failing those things, I'd try to make a really simple Rails app with two models that demonstrates the problem and post it to Rails' Lighthouse.
Wasn't able to leave a comment, so I'll just add an answer entry.
Just encountered the same bug. After hours of trying to figure this issue out and an hour of google-ing, I stumbled to this question by chance. Along with the linked LH ticket, and quote from the API, it now makes sense. Thank you!
While googling, found an old ticket. Direct link does not work, but google cache has a copy.
Just check Google for cached version of dev.rubyonrails.org/ticket/7743
Seems like a patch never made it to Rails.

Associating multiple ids on creation of Item

G'day guys,
I'm currently flitting through building a test "Auction" website to learn rails. I've set up my Auction and User models and have it so that only authenticated users can edit or delete auctions that are associated with them.
What I'm having difficulty doing is associating bid items with the Auction.
My models are as follows:
class Auction < ActiveRecord::Base
belongs_to :creator, :class_name => "User"
has_many :bids
validates_presence_of :title
validates_presence_of :description
validates_presence_of :curprice
validates_presence_of :finish_time
attr_reader :bids
def initialize
#bids = []
end
def add_bid(bid)
#bids << bid
end
end
class Bid < ActiveRecord::Base
belongs_to :auction, :class_name => "Auction", :foreign_key => "auction_id"
belongs_to :bidder, :class_name => "User", :foreign_key => "bidder_id"
validates_presence_of :amount
validates_numericality_of :amount
#retracted = false
end
class User < ActiveRecord::Base
has_many :auctions, :foreign_key => "owner_id"
has_many :bids, :foreign_key => "owner_id"
#auth stuff here
end
I'm attempting to add a bid record to an auction, but the auction_id simply will not add to the record.
I create a bid with a value from within a view of the auction, having the #auction as the local variable.
<% form_for :bid, :url => {:controller => "auction", :action => "add_bids"} do |f|%>
<p>Bid Amount <%= f.text_field :amount %></p>
<%= submit_tag "Add Bid", :auction_id => #auction %>
<% end %>
This is connected to the following code:
def add_bids
#bid = current_user.bids.create(params[:bid])
if #bid.save
flash[:notice] = "New Bid Added"
redirect_to :action => "view_auction", :id => #bid.auction_id
end
end
The problem I am getting is that the auction_id is not put into the bid element. I've tried setting it in the form HTML, but I think I'm missing something very simple.
My Data model, to recap is
Users have both bids and auctions
Auctions have a user and have many bids
Bids have a user and have a auction
I've been struggling with trying to fix this for the past 4 hours and I'm starting to get really downhearted about it all.
Any help would be really appreciated!
You're not quite doing things the Rails way, and that's causing you a bit of confusion.
Successful coding in Rails is all about convention over configuration. Meaning, Rails will guess at what you mean unless you tell it otherwise. There's usually a couple of things it will try if it guesses wrong. But in general stick to the deterministic names and you'll be fine.
There are so many errors in your code, so I'm going to clean it up and put comments every way to let you know what's wrong.
app/models/auction.rb
class Auction < ActiveRecord::Base
belongs_to :creator, :class_name => "User"
has_many :bids
# Given the nature of your relationships, you're going to want to add this
# to quickly find out who bid on an object.
has_many :bidders, :through => :bids
validates_presence_of :title
validates_presence_of :description
validates_presence_of :curprice
validates_presence_of :finish_time attr_reader :bids
#These two methods are unnecessary.
# Also don't override initialize in ActiveRecord. Instead use after_initialize
#def initialize Supplied by rails when you do has_many :bids
# #bids = [] #bids will be populated by what is picked up from
#end the database based on the has_many relationship
#def add_bid(bid) Supplied by rails when you do has_many :bids
# #bids << bid auction.bids << is a public method after has_many :bids
#end
end
app/models/bid.rb
class Bid < ActiveRecord::Base
# :class_name and :foreign_key are ony necessary when rails cannot guess from a
# association name. :class_name default is the association singularized and
# capitalized. :foreign_key default is association_id
belongs_to :auction #, :class_name => "Auction", :foreign_key => "auction_id"
# here we need :class_name because Rails is looking for a Bidder class.
# also there's an inconsistency. Later user refers to has_many bids with
# a foreign_key of owner_id, which one is it? bidder_id or owner_id?
# if it's owner_id? you will need the :foreign_key option.
belongs_to :bidder, :class_name => "User" #, :foreign_key => "bidder_id"
validates_presence_of :amount
validates_numericality_of :amount
# This will never get called in a useful way.
# It really should be done in the migration, setting default
# value for bids.retracted to false
# #retracted = false
end
app/models/user.rb
class User < ActiveRecord::Base
# This makes sense, because an auction can have many bidders, who are also users.
has_many :auctions, :foreign_key => "owner_id"
# This doesn't. A bid belongs to a user, there's no need to change the name.
# See above note re: owner_id vs. bidder_id
has_many :bids, :foreign_key => "owner_id"
# You could also use this to quickly get a list of auctions a user has bid on
has_many :bid_on_auctions, :through => :bids, :source => :auction
... auth stuff ...
end
So far so good, right?
The view isn't bad but it's missing the form parts for the bid amount. This code assumes that you store the value of the bid in an amount column. I also arbitrarily named it auctions/bid
app/views/auctions/bid.html.erb
<% form_for :bid, #auction.bids.new do |f|%>
<%= f.label_for :amount %>
<%= f.text_field :amount%>
<!-- Don't need to supply #auction.id, because form_for does it for you. -->
<%= submit_tag "Add Bid" %>
params hash generated by the form: that is passed to the controller:
params =
{
:bid =>
{
:auction_id => #auction.id
:amount => value of text_field
}
}
params hash generated by the from as you wrote it (note: I'm guessing at names because they were left out of the posted code):
params =
{
:id => #auction_id ,
:bid => { :amount => value of text_field }
}
However, your controller code is where all your problems are coming from this is almost entirely wrong. I'm guessing this is in the auction controller, which seems wrong because you're trying to create a bid. Lets see why:
app/controllers/auctions_controller.rb
...
def add_bids
# not bad, but... #bid will only fill in the owner_id/bidder_id. and bid amount.
#bid = current_user.bids.create(params[:bid])
# create calls save, so this next line is redundant. It still works though.
# because nothing's happening between them to alter the outcome of save.
if #bid.save
flash[:notice] = "New Bid Added"
# you should be using restful routes, this almost works, but is ugly and deprecated.
# it doesn't work becasue #bid.auction_id is never set. In fact you never use
# the auction_id any where, which was in your params_hash as params[:id]
redirect_to :action => "view_auction", :id => #bid.auction_id
end
end
...
Here's how your controller should work. First of all, this should be in the bids_controller, not auctions_controller
app/controllers/bids_controller.rb
...
def create
#bid = Bid.new(params[:bid]) # absorb values from form via params
#bid.bidder = current_user # link bid to current_user.
#auction = bid.auction based on.
# #auction is set, set because we added it to the #bid object the form was based on.
if #bid.save
flash[:notice] = "New Bid Added"
redirect_to #auction #assumes there is a show method in auctions_controller
else
render "auctions/show" # or what ever you called the above view
end
end
...
You'll also need to make sure the following is in your routes.rb (in addition to what may already be there. These few lines will set you up with RESTful routes.
config/routes.rb
ActionController::Routing::Routes.draw do |map|
...
map.resources :auctions
map.resources :bids
...
end
In any case you weren't far off. It seems you're off to a decent start, and could probably benefit from reading a book about rails. Just blindly following tutorials doesn't do you much good if you don't understand the underlying framework. It doesn't help that 90% of the tutorials out there are for older versions of rails and way out of date.
A lot of your code is the old way of doing things. Particularly redirect_to :action => "view_auction", :id => #bid.auction_id and <% form_for :bid, :url => {:controller => "auction", :action => "add_bids"} do |f|%>. With RESTful routing, they become redirect_to #auction and <% form_for #auction.bid.new do |f| %>`
Here's something resources you should read up on:
ActiveRecord::Associations: defines has_many, belongs_to, their options, and the convenience methods they add.
Understanding MVC: Provides a better understanding of the flow of information as it relates to Rails
RESTful resources: Understanding resources.
Form Helpers: In depth description of form_for and why the above code works.

Resources