has_many :through creating child after_save --> ActionView::Template::Error - ruby-on-rails

I have three models: List, Food, and Quantity. List and Food are associated through Quantity via has_many :through. The model association is doing what I want, but when I test, there is an error.
test_valid_list_creation_information#ListsCreateTest (1434538267.92s)
ActionView::Template::Error: ActionView::Template::Error: Couldn't find Food with 'id'=14
app/views/lists/show.html.erb:11:in `block in _app_views_lists_show_html_erb__3286583530286700438_40342200'
app/views/lists/show.html.erb:10:in `_app_views_lists_show_html_erb__3286583530286700438_40342200'
test/integration/lists_create_test.rb:17:in `block (2 levels) in <class:ListsCreateTest>'
test/integration/lists_create_test.rb:16:in `block in <class:ListsCreateTest>'
app/views/lists/show.html.erb:11:in `block in _app_views_lists_show_html_erb__3286583530286700438_40342200'
app/views/lists/show.html.erb:10:in `_app_views_lists_show_html_erb__3286583530286700438_40342200'
test/integration/lists_create_test.rb:17:in `block (2 levels) in <class:ListsCreateTest>'
test/integration/lists_create_test.rb:16:in `block in <class:ListsCreateTest>'
My aim is to create a new Quantity (associated with that list) each time a list is created. Each Quantity has amount, food_id, and list_id.
list_id should equal the id of the list that was just created.
food_id should equal the id of a random food that already exists.
amount should be a random integer.
In the error, the number 14 ("Food with 'id'=14) is generated by randomly selecting a number from 1 to Food.count. Food.count equals the number of food objects in test/fixtures/foods.yml, so the foods are definitely recognized, at least when I run Food.count. So why wouldn't food with 'id'=14 exist?
I believe there is something wrong with either the Lists controller, the fixtures, or the integration test. Whatever is causing the test to fail doesn't seem to affect performance (everything works in the console and server/user interface), but I am trying to understand TDD and write good tests, so I will appreciate any guidance.
Lists model:
class List < ActiveRecord::Base
has_many :quantities
has_many :foods, :through => :quantities
validates :days, presence: true
validates :name, uniqueness: { case_sensitive: false }
after_save do
Quantity.create(food_id: rand(Food.count), list_id: self.id, amount: rand(6))
end
end
Quantities fixture:
one:
food: grape
list: weekend
amount: 1
two:
food: banana
list: weekend
amount: 1
Note: the Quantities fixture was previously organized as follows ...
one:
food_id: 1
list_id: 1
amount: 1
... and it seems to make no difference.
lists_create integration test:
require 'test_helper'
class ListsCreateTest < ActionDispatch::IntegrationTest
test "invalid list creation information" do
get addlist_path
assert_no_difference 'List.count' do
post lists_path, list: { days: "a",
name: "a" * 141 }
end
assert_template 'lists/new'
end
test "valid list creation information" do
get addlist_path
assert_difference 'List.count', 1 do
post_via_redirect lists_path, list: {
days: 2,
name: "example list"
}
end
assert_template 'lists/show'
end
end
And app/views/lists/show.html.erb referenced in the error:
<% provide(:title, #list.name) %>
<div class="row"><aside class="col-md-4"><section class="user_info">
<h1> <%= #list.name %></h1>
<p><%= #list.days %> day(s)</p><p>
<% Quantity.where(:list_id => #list.id).each do |f| %>
<%= "#{f.amount} #{Food.find(f.food_id).name}" %>
<% end %>
</p></section></aside></div><%= link_to "edit the properties of this list", edit_list_path %>
Thank you for any advice or references. Please let me know if you need other code or information that you consider relevant. I am hoping to accomplish this all using fixtures and not another method such as FactoryGirl, even if it means a little extra code.
Rails 4.2.3, Cloud9. Development database = SQLite3, production database = postgres heroku.

Besides being very weird to create a random value in the after_save callback (which I think you're doing as an exercise, but anyway it's better to use good practices from the start), you should never use rand(Model.count) to get a sample record. There's two main problems:
The rand(upper_bound) method returns a number between zero and the upper_bound argument, but there's no guarantee that zero is the first created id. I'm using PostgreSQL and the first model has the id 1. You can specify a range (rand(1..upper_bound)), but anyway you're gambling on the way the current database works.
You're assuming that all the records exist in a sequential order at any given time, which is not always true. If you delete a record and it's id is randomly chosen, you'll get an error. The library also can use any strategy to create the fixtures, so it's better not to assume anything about how it works.
If you really need to choose randomly a record, I'd recommend simply using the array's sample method: Food.all.sample. It's slow, but it works. If you need to optimize, there's other options.
Now, I'd really recommend to avoid random values at all costs, using them only when necessary. It's difficult to test, and difficult to track bugs. Also, I'd avoid creating a relation inside a callback, it grows rapidly into a unmanageable mess.

I am posting an answer because after implementing the suggestions, my error is gone and I think I have a better understanding of what's going on.
Previously, I had Quantities created in the List model upon creation of a List using a relation. The relation is now in the controller, not the model.
List model without relation:
class List < ActiveRecord::Base
has_many :quantities
has_many :foods, :through => :quantities
validates :days, presence: true
validates :name, uniqueness: { case_sensitive: false }
end
Quantities fixture and lists_create integration test are unchanged.
Previously this show.html.erb contained a query. Now, it has only #quantities, which is defined in the Lists controller. The query is in the controller, not the view.
app/views/lists/show.html.erb:
<% provide(:title, #list.name) %>
<div class="row"><aside class="col-md-4"><section class="user_info">
<h1> <%= #list.name %></h1>
<p><%= #list.days %> day(s)</p>
<p><%= #quantities %></p>
</section></aside></div><%= link_to "edit the properties of this list", edit_list_path %>
The List controller with the query in the show method (to filter for quantities that have the proper list_id) and the relation in the create method (to create new quantities upon list creation).
class ListsController < ApplicationController
def show
#list = List.find(params[:id])
#quantities = []
Quantity.where(:list_id => #list.id).each do |f|
#quantities.push("#{f.amount} #{Food.find(f.food_id).name}")
end
end
# ...
def create
#list = List.new(list_params)
if #list.save
flash[:success] = "A list has been created!"
#a = Food.all.sample.id
#b = Food.all.sample.id
Quantity.create(food_id: #a, list_id: #list.id, amount: rand(6))
if (#a != #b)
Quantity.create(food_id: #b, list_id: #list.id, amount: rand(6))
end
redirect_to #list
else
render 'new'
end
end
# ...
end
If I understand correctly, I was misusing the model and view and inappropriately using rand with Food.count.
Please let me know if you think I've missed anything or if you can recommend anything to improve my code. Thank you #mrodrigues, #jonathan, and #vamsi for your help!

Related

How can I test a model's custom predicate? (Ransack search)

I have a model called Invoice with an amount_cents attribute.
I'm using Ransack gem to perform searching and want users to be able to search by integer amounts.
I added a custom predicate to my Invoice model to format the search params and multiply whatever the user enters in the search field by 100. This way it will match values in the amount_cents column:
Invoice model:
ransacker :integer_amount,
type: :integer,
formatter: proc { |amount| amount * 100 } do |amount|
amount.table[:amount_cents]
end
form search fields:
<div class="form-group">
<%= f.search_field :integer_amount_gt,
class: "form-control" %>
</div>
<div class="mx-1 form-group">
<%= f.search_field :integer_amount_lt,
class: "form-control" %>
</div>
Invoices controller action:
def index
#invoices = current_account.invoices.ransack(params[:q])
end
I'd like to test this, but from the model's perspective. So far I've written a controller test like so:
test "integer_amount params should fetch right invoices" do
invoice_within_range = invoices(:one) # amount_cents = 900
invoice_outside_range = invoices(:two) # amount_cents = 10000
get invoices_path, params: {
q: {
integer_amount_gt: "8",
integer_amount_lt: "10",
},
}
invoices = controller.view_assigns["invoices"]
assert_includes invoices, invoice_within_range
assert_not_includes invoices, invoice_outside_range
end
I think this is sort of an integration test as the custom predicate is doing it's job correctly, but I don't think it's clean to test it this way as it's a model feature and not the controller's.
Is there a better way to test this?
I'd suggest to test the custom ransackers from the model's perspective. The integration tests are usually much slower.
Your test case could look like that (I'm not familiar with the MiniTest framework, so the test example might contain some errors):
test "integer_amount ransacker should fetch right invoices" do
invoice_within_range = invoices(:one) # amount_cents = 900
invoice_outside_range = invoices(:two) # amount_cents = 10000
invoices = Invoice.ransack(
integer_amount_gt: 8,
integer_amount_lt: 10
).result
assert_includes invoices, invoice_within_range
assert_not_includes invoices, invoice_outside_range
end
There's also an alternative idea. Instead of creating test data in DB and fetching it you can test the Ransack generates the correct SQL query and includes the proper conditions:
test "integer_amount ransacker generates proper query" do
query = Invoice.ransack(
integer_amount_gt: 8,
integer_amount_lt: 10
).result
# test the query is correct and executed
assert_empty query.to_a
# test the query contains the proper SQL conditions
assert_match /`invoices`.`amount_cents` > 800 AND `invoices`.`amount_cents` < 1000/, query.to_sql
end

Rails has_many :through with uniqueness

I asked this question differently, but deleted it to attempt more clarity.
I have an Article model. It has_many contacts, through: :article_contacts.
I have a Contact model. It has_many articles, through: :article_contacts.
What I need is each Contact object to be unique, but be able to be associated with different articles. My last question led to people showing me how to display only unique contacts, or to validate the join model, but that's not what I need.
I need, for example, tables like the following:
Article
id: 1, name: "Whatever", content: "Whatever"
id: 2, name: "Again, whatever", content: "Whateva"
Contact
id: 1, email: "email#email.com"
id: 2, email: "secondemail#email.com"
ArticleContact
id: 1, article_id: 1, contact_id: 1
id: 2, article_id: 1, contact_id: 2
id: 3, article_id: 2, contact_id: 1
So, when I build the association in my Article controller in the new action and I call #article.save in the create action, I get an insert of the new article, an insert of the contact and an insert of the article_relationship. Great.
BUT, on Article 2, if I add the same email to the contact form, I do not want to create another Contact with the same email. But I do want (as you see with the third ID in ArticleContact above) to create. But each call to #article.save does it. I've tried first_or_initialize and << into the collection, but it always creates multiple times and if I have a validation, it means I can't create the ArticleContact relationship because contact is unique.
I may eventually have a drop down with contacts, but I suspect that will be long, so I'd rather simply enter an email into a form and have the code check that it is unique and if so, just create the join relationship using the existing ID.
This must be easier than I am conceiving, but I can't find a single article that demonstrates this anywhere.
Updated with code per request, though, again, I don't have the code to only create a unique contact and associate the relationship with an existing one. That's what I'm missing so this code will only show you what I already know works and not how to get to what I want :).
articlecontroller:
def new
#article = #business.articles.build
authorize #business
#article.attachments.build
#article.contacts.build
end
def create
#article = #business.articles.new(article_params)
authorize #business
respond_to do |format|
if #article.save
format.html { redirect_to business_article_path(#business, #article), notice: "Knowledge created." }
else
format.html { render 'new' }
end
end
end
Models have the standard has_many :thruogh and belongs to. I can show it, but they are the right way. View is just a standard simple_form building the contact:
<%= f.simple_fields_for :contacts, class: "form-inline" do |contact| %>
<%= contact.input :first_name, label: "Contact First Name" %>
<%= contact.input :last_name, label: "Contact Last Name" %>
<%= contact.input :email, label: "Contact Email" %>
<% end %>
You should enforce uniqueness, just in case:
# contact.rb
class Contact
...
validates_uniqueness_of :email
...
end
But in your controller you could do:
# articles_controller.rb
def create
...
# we can rely on it's uniqueness
contacts << Contact.find_or_create_by(email: param[:email)
...
end
Or whatever best fits your needs.
Hope this helps!
give your ArticleContact a validation with a scope.
validates :article_id, unique {scope: :contact_id}
this will prevent you from having ArticleContacts that are exactly the same.
when you create a contact, try:
contact = Contact.where(email: "e#mail.com:").first_or_create
in contact.rb
validates :email, uniqueness: true
You could try this in the create method
verified_contacts = []
#article.contacts.each do |contact|
existing_contact = Contact.find_by(email: contact.email)
verified_contacts << (existing_contact || contact)
end
#article.contacts = verified_contacts

Getting rails3-autocomplete-jquery gem to work nicely with Simple_Form with multiple inputs

So I am trying to implement multiple autocomplete using this gem and simple_form and am getting an error.
I tried this:
<%= f.input_field :neighborhood_id, collection: Neighborhood.order(:name), :url => autocomplete_neighborhood_name_searches_path, :as => :autocomplete, 'data-delimiter' => ',', :multiple => true, :class => "span8" %>
This is the error I get:
undefined method `to_i' for ["Alley Park, Madison"]:Array
In my params, it is sending this in neighborhood_id:
"search"=>{"neighborhood_id"=>["Alley Park, Madison"],
So it isn't even using the IDs for those values.
Does anyone have any ideas?
Edit 1:
In response to #jvnill's question, I am not explicitly doing anything with params[:search] in the controller. A search creates a new record, and is searching listings.
In my Searches Controller, create action, I am simply doing this:
#search = Search.create!(params[:search])
Then my search.rb (i.e. search model) has this:
def listings
#listings ||= find_listings
end
private
def find_listings
key = "%#{keywords}%"
listings = Listing.order(:headline)
listings = listings.includes(:neighborhood).where("listings.headline like ? or neighborhoods.name like ?", key, key) if keywords.present?
listings = listings.where(neighborhood_id: neighborhood_id) if neighborhood_id.present?
#truncated for brevity
listings
end
First of all, this would be easier if the form is returning the ids instead of the name of the neighborhood. I haven't used the gem yet so I'm not familiar how it works. Reading on the readme says that it will return ids but i don't know why you're only getting names. I'm sure once you figure out how to return the ids, you'll be able to change the code below to suit that.
You need to create a join table between a neighborhood and a search. Let's call that search_neighborhoods.
rails g model search_neighborhood neighborhood_id:integer search_id:integer
# dont forget to add indexes in the migration
After that, you'd want to setup your models.
# search.rb
has_many :search_neighborhoods
has_many :neighborhoods, through: :search_neighborhoods
# search_neighborhood.rb
belongs_to :search
belongs_to :neighborhood
# neighborhood.rb
has_many :search_neighborhoods
has_many :searches, through: :search_neighborhoods
Now that we've setup the associations, we need to setup the setters and the attributes
# search.rb
attr_accessible :neighborhood_names
# this will return a list of neighborhood names which is usefull with prepopulating
def neighborhood_names
neighborhoods.map(&:name).join(',')
end
# we will use this to find the ids of the neighborhoods given their names
# this will be called when you call create!
def neighborhood_names=(names)
names.split(',').each do |name|
next if name.blank?
if neighborhood = Neighborhood.find_by_name(name)
search_neighborhoods.build neighborhood_id: neighborhood.id
end
end
end
# view
# you need to change your autocomplete to use the getter method
<%= f.input :neighborhood_names, url: autocomplete_neighborhood_name_searches_path, as: :autocomplete, input_html: { data: { delimiter: ',', multiple: true, class: "span8" } %>
last but not the least is to update find_listings
def find_listings
key = "%#{keywords}%"
listings = Listing.order(:headline).includes(:neighborhood)
if keywords.present?
listings = listings.where("listings.headline LIKE :key OR neighborhoods.name LIKE :key", { key: "#{keywords}")
end
if neighborhoods.exists?
listings = listings.where(neighborhood_id: neighborhood_ids)
end
listings
end
And that's it :)
UPDATE: using f.input_field
# view
<%= f.input_field :neighborhood_names, url: autocomplete_neighborhood_name_searches_path, as: :autocomplete, data: { delimiter: ',' }, multiple: true, class: "span8" %>
# model
# we need to put [0] because it returns an array with a single element containing
# the string of comma separated neighborhoods
def neighborhood_names=(names)
names[0].split(',').each do |name|
next if name.blank?
if neighborhood = Neighborhood.find_by_name(name)
search_neighborhoods.build neighborhood_id: neighborhood.id
end
end
end
Your problem is how you're collecting values from the neighborhood Model
Neighborhood.order(:name)
will return an array of names, you need to also collect the id, but just display the names
use collect and pass a block, I beleive this might owrk for you
Neighborhood.collect {|n| [n.name, n.id]}
Declare a scope on the Neighborhood class to order it by name if you like to get theat functionality back, as that behavior also belongs in the model anyhow.
edit>
To add a scope/class method to neighborhood model, you'd typically do soemthing like this
scope :desc, where("name DESC")
Than you can write something like:
Neighborhood.desc.all
which will return an array, thus allowing the .collect but there are other way to get those name and id attributes recognized by the select option.

Problem on Rails fixtures creation sequence

I am reading the book Simply Rails by Sitepoint and given these models:
story.rb
class Story < ActiveRecord::Base
validates_presence_of :name, :link
has_many :votes do
def latest
find :all, :order => 'id DESC', :limit => 3
end
end
def to_param
"#{id}-#{name.gsub(/\W/, '-').downcase}"
end
end
vote.rb
class Vote < ActiveRecord::Base
belongs_to :story
end
and given this fixtures
stories.yml
one:
name: MyString
link: MyString
two:
name: MyString2
link: MyString2
votes.yml
one:
story: one
two:
story: one
these tests fail:
story_test.rb
def test_should_have_a_votes_association
assert_equal [votes(:one),votes(:two)], stories(:one).votes
end
def test_should_return_highest_vote_id_first
assert_equal votes(:two), stories(:one).votes.latest.first
end
however, if I reverse the order of the stories, for the first assertion and provide the first vote for the first assertion, it passes
story_test.rb
def test_should_have_a_votes_association
assert_equal [votes(:two),votes(:one)], stories(:one).votes
end
def test_should_return_highest_vote_id_first
assert_equal votes(:one), stories(:one).votes.latest.first
end
I copied everything as it is in the book and have not seen an errata about this. My first conclusion was that the fixture is creating the records from bottom to top as it was declared, but that doesn't make any point
any ideas?
EDIT: I am using Rails 2.9 running in an RVM
Your fixtures aren't getting IDs 1, 2, 3, etc. like you'd expect - when you add fixtures, they get IDs based (I think) on a hash of the table name and the fixture name. To us humans, they just look like random numbers.
Rails does this so you can refer to other fixtures by name easily. For example, the fixtures
#parents.yml
vladimir:
name: Vladimir Ilyich Lenin
#children.yml
joseph:
name: Joseph Vissarionovich Stalin
parent: vladimir
actually show up in your database like
#parents.yml
vladimir:
id: <%= fixture_hash('parents', 'vladimir') %>
name: Vladimir Ilyich Lenin
#children.yml
joseph:
id: <%= fixture_hash('children', 'joseph') %>
name: Joseph Vissarionovich Stalin
parent_id: <%= fixture_hash('parents', 'vladimir') %>
Note in particular the expansion from parent: vladimir to parent_id: <%= ... %> in the child model - this is how Rails handles relations between fixtures.
Moral of the story: Don't count on your fixtures being in any particular order, and don't count on :order => :id giving you meaningful results in tests. Use results.member? objX repeatedly instead of results == [obj1, obj2, ...]. And if you need fixed IDs, hard-code them in yourself.
Hope this helps!
PS: Lenin and Stalin weren't actually related.
Xavier Holt already gave the main answer, but wanted to also mention that it is possible to force rails to read in fixtures in a certain order.
By default rails assigns its own IDs, but you can leverage the YAML omap specification to specify an ordered mapping
# countries.yml
--- !omap
- netherlands:
id: 1
title: Kingdom of Netherlands
- canada:
id: 2
title: Canada
Since you are forcing the order, you have to also specify the ID yourself manually, as shown above.
Also I'm not sure about this part, but I think once you commit to overriding the default rails generated ID and use your own, you have to do the same for all downstream references.
In the above example, suppose each country can have multiple leaders, you would have do something like
# leaders.yml
netherlands-leader:
country_id: 1 #you have to specify this now!
name: Willem-Alexander
You need to manually specify the id that refers to the previous Model (Countries)

rails, `flash[:notice]` in my model?

I have a user model in which I have a method for seeing if the user has earned a "badge"
def check_if_badges_earned(user)
if user.recipes.count > 10
award_badge(1)
end
If they have earned a badge, the the award_badge method runs and gives the user the associated badge. Can I do something like this?
def check_if_badges_earned(user)
if user.recipes.count > 10
flash.now[:notice] = "you got a badge!"
award_badge(1)
end
Bonus Question! (lame, I know)
Where would the best place for me to keep all of these "conditions" for which my users could earn badges, similar to stackoverflows badges I suppose. I mean in terms of architecture, I already have badge and badgings models.
How can I organize the conditions in which they are earned? some of them are vary complex, like the user has logged in 100 times without commenting once. etc. so there doesn’t seem to be a simple place to put this sort of logic since it spans pretty much every model.
I'm sorry for you but the flash hash is not accessible in models, it gets created when the request is handled in your controller. You still can use implement your method storing the badge infos (flash message included) in a badge object that belongs to your users:
class Badge
# columns:
# t.string :name
# seed datas:
# Badge.create(:name => "Recipeador", :description => "Posted 10 recipes")
# Badge.create(:name => "Answering Machine", :description => "Answered 1k questions")
end
class User
#...
has_many :badges
def earn_badges
awards = []
awards << earn(Badge.find(:conditions => { :name => "Recipeador" })) if user.recipes.count > 10
awards << earn(Badge.find(:conditions => { :name => "Answering Machine" })) if user.answers.valids.count > 1000 # an example
# I would also change the finds with some id (constant) for speedup
awards
end
end
then:
class YourController
def your_action
#user = User.find(# the way you like)...
flash[:notice] = "You earned these badges: "+ #user.earn_badges.map(:&name).join(", ")
#...
end
end

Resources