I'm newbie in RoR, and I am trying to test a simple named_scope for my Model.
But I don't know if I have a problem in my model (I'm using mongoid), in my code test (I'm using rspec) or in my factory. I got this error
Mongoid::Errors::InvalidCollection:
Access to the collection for Movement is not allowed since it is an embedded document, please access a collection from the root
document.
My models
class Movement
include Mongoid::Document
field :description, :type => String
embedded_in :category
named_scope :top, lambda { |number| { :limit => (number.size > 0 ? number : 10) } }
end
class Category
include Mongoid::Document
field :name
embeds_many :movement
end
My factory, con factory_girl
Factory.define :movement do |m|
m.amount 24
m.date "30/10/2011"
m.description "Beer"
m.association :category, :factory => :category
end
Factory.define :category do |c|
c.name "Drink"
end
My test
describe "when i have a movement list" do
it "recent method should return last 2 movements" do
#movements = (1..3).collect { Factory(:movement) }
recent_movements = Movement.top(2)
recent_movements.should have(2).entries
end
end
And the error:
Mongoid::Errors::InvalidCollection:
Access to the collection for Movement is not allowed since it is an embedded >document,
please access a collection from the root document.
I tried a little change in my factory.
Factory.define :movement do |m|
m.amount 24
m.date "30/10/2011"
m.description "Beer"
m.category { [ Factory.build(:category) ] }
end
But then I got other different error:
Failure/Error: #movements = (1..3).collect { Factory(:movement) }
NoMethodError:
undefined method `reflect_on_association' for #
Could someone help me?
Thanks
I just had a the same error in my app. I ended up having an error in my class and that solved my problem.
Related
so I have these two models:
class Tag < ActiveRecord::Base
has_many :event_tags
attr_accessible :tag_id, :tag_type, :value
end
class EventTag < ActiveRecord::Base
belongs_to :tag
attr_accessible :tag_id, :event_id, :region
end
and this table for Tags:
**tag_id** **tag_type** **value**
1 "funLevel" "Boring..."
2 "funLevel" "A Little"
3 "funLevel" "Hellz ya"
4 "generic" "Needs less clowns"
5 "generic" "Lazer Tag"
...
What I would like to do is write a custom validation where it checks to see:
Each event_id has only one tag_type of "funLevel" attached to it, but can have more than one "generic" tags
For example:
t1 = EventTag.new(:tag_id => 1, :event_id =>777, :region => 'US')
t1.save # success
t2 = EventTag.new(:tag_id => 2, :event_id =>777, :region => 'US')
t2.save # failure
# because (event_id: 777) already has a tag_type of
# "funLevel" associated with it
t3 = EventTag.new(:tag_id => 4, :event_id =>777, :region => 'US')
t3.save # success, because as (tag_id:4) is not "funLevel" type
I have come up with one ugly solution:
def cannot_have_multiple_funLevel_tag
list_of_tag_ids = EventTag.where("event_id = ?", event_id).pluck(:tag_id)
if(Tag.where("tag_id in ?", list_of_tag_ids).pluck(:tag_type).include? "funLevel")
errors.add(:tag_id, "Already has a Fun Level Tag!")
end
Being new to rails, is there a more better/more elegant/more inexpensive way?
The way you have your data structured means that the inbuilt Rails validations are probably not going to be a heap of help to you. If the funLevel attribute was directly accessible by the EventTag class, you could just use something like:
# event_tag.rb
validate :tag_type, uniqueness: { scope: :event_id },
if: Proc.new { |tag| tag.tag_type == "funLevel" }
(unfortunately, from a quick test you don't seem to be able to validate the uniqueness of a virtual attribute.)
Without that, you're probably stuck using a custom validation. The obvious improvement to the custom validation you have (given it looks like you want to have the validation on EventTag) would be to not run the validation unless that EventTag is a funLevel tag:
def cannot_have_multiple_funLevel_tag
return unless self.tag.tag_type == "funLevel"
...
end
I use Rails 3.0.6 with mongoID 2.0.2. Recently I encountered an issue with save! method when overriding setter (I am trying to create my own nested attributes).
So here is the model:
class FeedItem
include Mongoid::Document
has_many :audio_refs
def audio_refs=(attributes_array, binding)
attributes_array.each do |attributes|
if attributes[:audio_track][:id]
self.audio_refs.build(:audio_track => AudioTrack.find(attributes[:audio_track][:id]))
elsif attributes[:audio_track][:file]
self.audio_refs.build(:audio_track => AudioTrack.new(:user_id => attributes[:audio_track][:user_id], :file => attributes[:audio_track][:file]))
end
end
if !binding
self.save!
end
end
AudioRef model (which is just buffer between audio_tracks and feed_items) is:
class AudioRef
include Mongoid::Document
belongs_to :feed_item
belongs_to :audio_track
end
And AudioTrack:
class AudioTrack
include Mongoid::Document
has_many :audio_refs
mount_uploader :file, AudioUploader
end
So here is the spec for the FeedItem model which doesn`t work:
it "Should create audio_track and add audio_ref" do
#audio_track = Fabricate(:audio_track, :user_id => #author.id, :file => File.open("#{Rails.root}/spec/stuff/test.mp3"))
#feed_item= FeedItem.new(
:user => #author,
:message => {:body => Faker::Lorem.sentence(4)},
:audio_refs => [
{:audio_track => {:id => #audio_track.id}},
{:audio_track => {:user_id => #author.id, :file => File.open("#{Rails.root}/spec/stuff/test.mp3")}}
]
)
#feed_item.save!
#feed_item.reload
#feed_item.audio_refs.length.should be(2)
end
As you can see, the reason I am overriding audio_refs= method is that FeedItem can be created from existing AudioTracks (when there is params[:audio_track][:id]) or from uploaded file (params[:audio_track][:file]).
The problem is that #feed_item.audio_refs.length == 0 when I run this spec, i.e. audio_refs are not saved. Could you please help me with that?
Some investigation:
1) binding param is "true" by default (this means we are in building mode)
I found a solution to my problem but I didnt understand why save method doesnt work and didn`t make my code work. So first of all let me describe my investigations about the problem. After audio_refs= is called an array of audio_refs is created BUT in any audio_ref is no feed_item_id. Probably it is because the feed_item is not saved by the moment.
So the solution is quite simple - Virtual Attributes. To understand them watch corresponding railscasts
So my solution is to create audio_refs by means of callback "after_save"
I slightly changed my models:
In FeedItem.rb I added
attr_writer :audio_tracks #feed_item operates with audio_tracks array
after_save :assign_audio #method to be called on callback
def assign_audio
if #audio_tracks
#audio_tracks.each do |attributes|
if attributes[:id]
self.audio_refs << AudioRef.new(:audio_track => AudioTrack.find(attributes[:id]))
elsif attributes[:file]
self.audio_refs << AudioRef.new(:audio_track => AudioTrack.new(:user_id => attributes[:user_id], :file => attributes[:file]))
end
end
end
end
And the spec is now:
it "Should create audio_track and add audio_ref" do
#audio_track = Fabricate(:audio_track, :user_id => #author.id, :file => File.open("#{Rails.root}/spec/stuff/test.mp3"))
#feed_item= FeedItem.new(
:user => #author,
:message => {:body => Faker::Lorem.sentence(4)},
:audio_tracks => [
{:id => #audio_track.id},
{:user_id => #author.id, :file => File.open("#{Rails.root}/spec/stuff/test.mp3")}
]
)
#feed_item.save!
#feed_item.reload
#feed_item.audio_refs.length.should be(2)
end
And it works fine!!! Good luck with your coding)
Check that audio_refs=() is actually being called, by adding debug output of some kind. My feeling is that your FeedItem.new() call doesn't use the audio_refs=() setter.
Here's the source code of the ActiveRecord::Base#initialize method, taken from APIdock:
# File activerecord/lib/active_record/base.rb, line 1396
def initialize(attributes = nil)
#attributes = attributes_from_column_definition
#attributes_cache = {}
#new_record = true
#readonly = false
#destroyed = false
#marked_for_destruction = false
#previously_changed = {}
#changed_attributes = {}
ensure_proper_type
populate_with_current_scope_attributes
self.attributes = attributes unless attributes.nil?
result = yield self if block_given?
_run_initialize_callbacks
result
end
I don't currently have an environment to test this, but it looks like it's setting the attributes hash directly without going through each attribute's setter. If that's the case, you'll need to call your setter manually.
Actually, I think the fact you're not getting an exception for the number of arguments (binding not set) proves that your setter isn't being called.
I've a mongoid embedded one to one model in a Rails app (User --> Watchlist) :
class User
include Mongoid::Document
field :name, :type => String
field :email, :type => String
embeds_one :watchlist
def self.create_with_omniauth(auth)
conn = FaradayStack.build 'https://api.github.com'
resp = conn.get '/users/octocat/watched'
create! do |user|
user.name = auth["user_info"]["name"]
user.email = auth["user_info"]["email"]
resp.body.each do |repo|
user.build_watchlist(html_url: "#{repo['html_url']}")
end
end
end
end
class Watchlist
include Mongoid::Document
field :html_url
embedded_in :user
end
Now resp.body, in User model is an Arry which contains several elements ( 2 in this case ):
ruby-1.9.2-p136 :061 > pp resp.body.length
2
=> 2
ruby-1.9.2-p136 :054 > resp.body.each do |repo|
ruby-1.9.2-p136 :055 > pp repo['html_url']
ruby-1.9.2-p136 :056?> end
"https://github.com/octocat/Hello-World"
"https://github.com/octocat/Spoon-Knife"
which I expect to save in the db at the end of self.create_with_omniauth(auth) method, anyway I just get one, nested "watchlist" child :
> db.users.find()
{
"_id" : ObjectId("4e1844ee1d41c843c7000003"),
"name" : "Luca G. Soave",
"email" : "luca.soave#gmail.com",
"watchlist" : { "html_url" : "https://github.com/octocat/Spoon-Knife",
"_id" : ObjectId("4e1844ee1d41c843c7000002") }
}
>
Pretty sure something goes wrong with this part of code:
resp.body.each do |repo|
user.build_watchlist(html_url: "#{repo['html_url']}", description: "#{repo['description']}")
end
... which probably cicles for the n. array elements and exit, wich also mean the last element is saved into the DB at the end of create! method,
... but I've not idea on how to decoupling that ...
Do you have a suggestion ?
I just get one, nested "watchlist" child.
You're only getting one watchlist because that's what you told Mongoid you have:
class User
embeds_one :watchlist # only one watchlist
end
If you want more than one watchlist, you need to change your model:
class User
embeds_many :watchlists
end
it helps if you use the term matching the collection you seek
embeds_many :watches
or
has_one :watchlist (but class Watchlist will in turn embeds_many :watch)
I was hoping someone would spot why this wouldn't work.
I am getting an error thats being called because the attributes I specify with Factory_Girl are not being applied to the stub before validation.
The Error:
undefined method `downcase' for #<Category:0x1056f2f60>
RSpec2
it "should vote up" do
#mock_vote = Factory.create(:vote)
Vote.stub(:get_vote).and_return(#mock_vote)
get :vote_up, :id => "1"
end
Factories
Factory.define :vote, :class => Vote do |v|
v.user_id "1"
v.association :post
end
Factory.define :post, :class => Post do |p|
p.category "spirituality"
p.name "sleezy snail potluck"
p.association :category
end
Factory.define :category, :class => Category do |c|
c.name "spirituality"
c.id "37"
end
Post.rb - Model
before_save :prepare_posts
validate :category?
def prepare_posts
self.update_attribute("category", self.category.downcase)
if self.url?
self.url = "http://" + self.url unless self.url.match /^(https?|ftp):\/\//
end
end
def category?
unless Category.exists?(:name => self.category.downcase)
errors.add(:category, "There's no categories with that name.")
end
return true
end
Also, feel free to nitpick any blatantly gross looking code. :D
Thanks!!
You have a category attribute, which appears to be a string, but you also seem to have a category association which automatically creates, among other things, an attribute on Post called category, probably overwriting your category attribute. Hence, the Category class has no downcase method, because it's not a String.
Rename your category attribute to something like category_name, but really you shouldn't have that attribute at all.
Maybe where you're calling self.category.downcase you meant self.category.name.downcase?
I have a Person model that has a many-to-many relationship with an Email model and I want to create a factory that lets me generate a first and last name for the person (this is already done) and create an email address that is based off of that person's name. Here is what I have for create a person's name:
Factory.sequence :first_name do |n|
first_name = %w[FirstName1 FirstName2] # ... etc (I'm using a real subset of first names)
first_name[(rand * first_name.length)]
end
Factory.sequence :last_name do |n|
last_name = %w[LastName1 LastName2] # ... etc (I'm using a real subset of last names)
last_name[(rand * last_name.length)]
end
Factory.define :person do |p|
#p.id ???
p.first_name { Factory.next(:first_name) }
p.last_name { Factory.next(:last_name) }
#ok here is where I'm stuck
#p.email_addresses {|p| Factory(:email_address_person_link) }
end
Factory.define :email_address_person_link do |eapl|
# how can I link this with :person and :email_address ?
# eapl.person_id ???
# eapl.email_address_id ???
end
Factory.define :email_address do |e|
#how can I pass p.first_name and p.last_name into here?
#e.id ???
e.email first_name + "." + last_name + "#test.com"
end
Ok, I think I understand what you're asking now. Something like this should work (untested, but I've done something similar in another project):
Factory.define :person do |f|
f.first_name 'John'
f.last_name 'Doe'
end
Factory.define :email do |f|
end
# This is optional for isolating association testing; if you want this
# everywhere, add the +after_build+ block to the :person factory definition
Factory.define :person_with_email, :parent => :person do |f|
f.after_build do |p|
p.emails << Factory(:email, :email => "#{p.first_name}.#{p.last_name}#gmail.com")
# OR
# Factory(:email, :person => p, :email => "#{p.first_name}.#{p.last_name}#gmail.com")
end
end
As noted, using a third, separate factory is optional. In my case I didn't always want to generate the association for every test, so I made a separate factory that I only used in a few specific tests.
Use a callback (see FG docs for more info). Callbacks get passed the current model being built.
Factory.define :person do |p|
p.first_name { Factory.next(:first_name) }
p.last_name { Factory.next(:last_name) }
p.after_build { |m| p.email_addresses << "#{m.first_name}.#{m.last_name}#test.com" }
end
I think that works.
You could also save yourself some work by looking into using the Faker gem which creates realistic first and last names and e-mail addresses for you.