Let's say I have a devise model called "User" which has_many :notes and :notebooks and each :notebook has_many :notes.
So a notes will have two foreign key, :user_id and :notebook_id, so how to build/find a note?
current_user.notebooks.find(param).notes.new(params[:item]) will create the foreign_key only for notebook or also for the user in the note record in the DB?
If the second case (foreign key only for notebook), how should I write?
Using MongoDB with MongoID and referenced relations
Mongoid will manage the document references and queries for you, just make sure to specify the association/relationship for each direction that you need (e.g., User has_many :notes AND Note belongs_to :user). Like ActiveRecord, it appears to be "smart" about the relations. Please do not manipulate the references manually, but instead let your ODM (Mongoid) work for you. As you run your tests (or use the rails console), you can tail -f log/test.log (or log/development.log) to see what MongoDB operations are being done by Mongoid for you and you can see the actual object references as the documents are updated. You can see how a relationship makes use of a particular object reference, and if you pay attention to this, the link optimization should become clearer.
The following models and test work for me. Details on the setup are available on request. Hope that this helps.
Models
class User
include Mongoid::Document
field :name
has_many :notebooks
has_many :notes
end
class Note
include Mongoid::Document
field :text
belongs_to :user
belongs_to :notebook
end
class Notebook
include Mongoid::Document
belongs_to :user
has_many :notes
end
Test
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
User.delete_all
Note.delete_all
Notebook.delete_all
end
test "user" do
user = User.create!(name: 'Charles Dickens')
note = Note.create!(text: 'It was the best of times')
notebook = Notebook.create!(title: 'Revolutionary France')
user.notes << note
assert_equal(1, user.notes.count)
user.notebooks << notebook
assert_equal(1, user.notebooks.count)
notebook.notes << note
assert_equal(1, notebook.notes.count)
puts "user notes: " + user.notes.inspect
puts "user notebooks: " + user.notebooks.inspect
puts "user notebooks notes: " + user.notebooks.collect{|notebook|notebook.notes}.inspect
puts "note user: " + note.user.inspect
puts "note notebook: " + note.notebook.inspect
puts "notebook user: " + notebook.user.inspect
end
end
Result
Run options: --name=test_user
# Running tests:
user notes: [#<Note _id: 4fa430937f11ba65ce000002, _type: nil, text: "It was the best of times", user_id: BSON::ObjectId('4fa430937f11ba65ce000001'), notebook_id: BSON::ObjectId('4fa430937f11ba65ce000003')>]
user notebooks: [#<Notebook _id: 4fa430937f11ba65ce000003, _type: nil, user_id: BSON::ObjectId('4fa430937f11ba65ce000001'), title: "Revolutionary France">]
user notebooks notes: [[#<Note _id: 4fa430937f11ba65ce000002, _type: nil, text: "It was the best of times", user_id: BSON::ObjectId('4fa430937f11ba65ce000001'), notebook_id: BSON::ObjectId('4fa430937f11ba65ce000003')>]]
note user: #<User _id: 4fa430937f11ba65ce000001, _type: nil, name: "Charles Dickens">
note notebook: #<Notebook _id: 4fa430937f11ba65ce000003, _type: nil, user_id: BSON::ObjectId('4fa430937f11ba65ce000001'), title: "Revolutionary France">
notebook user: #<User _id: 4fa430937f11ba65ce000001, _type: nil, name: "Charles Dickens">
.
Finished tests in 0.018622s, 53.6999 tests/s, 161.0998 assertions/s.
1 tests, 3 assertions, 0 failures, 0 errors, 0 skips
I would use
class User
has_many :notebooks
has_many :notes, :through => :notebooks
end
http://guides.rubyonrails.org/association_basics.html#the-has_many-through-association
Update
You could always set the user_id manually, like this (I assume param is the ID for your notebook?):
Notebook.find(param).notes.new(params[:item].merge(:user_id => current_user.id))
Related
I have two models, Event and Registration:
class Event < ApplicationRecord
has_many :registrations
end
class Registration < ApplicationRecord
belongs_to :event
end
I also have fixtures for these models:
event_one:
[...]
registration_one:
event: event_one
However, in test, if I get the event and check its registrations, there are none.
event = events(:event_one)
puts event.registrations.count # Prints: 0
How do I make sure these associations are loaded for tests?
I did a simple example of your code and try solve the problem, the first thinks I found when review the Event and Registration entries is that the Registrations created has event_ids that dont exist in your database.
A solution:
events.yml
event_one:
id: 111
name: MyString
description: MyString
registrations.yml
registration_one:
name: MyString
event_id: 111
registration_two:
name: MyString
event_id: 111
event_test.rb
require 'test_helper'
class EventTest < ActiveSupport::TestCase
def setup
#event = events(:event_one)
end
test "the truth" do
byebug
end
end
debugger:
(byebug) #event.registrations
#<ActiveRecord::Associations::CollectionProxy [#<Registration id: 357093202, name: "MyString", event_id: 111, created_at: "2021-01-09 17:21:48", updated_at: "2021-01-09 17:21:48">, #<Registration id: 1055835082, name: "MyString", event_id: 111, created_at: "2021-01-09 17:21:48", updated_at: "2021-01-09 17:21:48">]>
I don't think this is the better solution but work.
I'm having issues trying to access my associated model in a one-to-many relationship during testing. I don't understand why the below doesn't return the association.
The reason I care about this problem is because I wanted to use Factory Girl to call my guardian through my_factory_girl_user.guardians.first which wasn't working. I tried to simplify by using just ActiveRecord and it still isn't behaving how I would expect.
I'm a true Rails beginner so any advice on this problem or how to better debug it would be much appreciated!
Question: Why doesn't #target_user.guardians return anything?
User Model
class User < ActiveRecord::Base
has_many :guardians, dependent: :destroy
...
end
Guardian Model
class Guardian < ActiveRecord::Base
belongs_to :user
...
end
Controller Spec
describe 'PUT #update' do
context 'when logged in and authorized' do
let(:new_attributes){
{level: 31}
}
it 'sets current guardian' do
#target_user = User.create!(profile_name: 'MyGuardian', system: 'Xbox One', region: 'North America', password: "password", password_confirmation: "password")
#target_guardian = Guardian.create!(level: 31, guardian_class: 'Titan', activity: 'Vault of Glass', user_id: #target_user.id)
puts "User: #{#target_user.inspect}"
puts "Guardian: #{#target_guardian.inspect}"
puts "user.guardians: #{#target_user.guardians}"
put :update, {id: #target_guardian.id, guardian: new_attributes}, {user_id: #target_user.id}
expect(assigns(:guardian)).to eq(#target_guardian)
end
end
Output
User: #<User id: 3147, email: nil, password_digest: "$2a$04$QPk3kCsKLp0IX.YmMPAPdO2gCe79mwVybHkFpcEsVkG...", created_at: "2014-12-16 02:55:57", updated_at: "2014-12-16 02:55:57", is_admin: nil, profile_name: "MyGuardian", system: "Xbox One", region: "North America">
Guaridan: #<Guardian id: 3284, created_at: "2014-12-16 02:55:57", updated_at: "2014-12-16 02:55:57", user_id: 3147, fireteam_id: nil, level: 31, guardian_class: "Titan", activity: "Vault of Glass", comment: nil>
user.guardians:
Answer:
Thanks Typpex for the help! My issue was that I was not refreshing the User after creating the Guardian, the solution I used was adding the below line after creating my Guardian. For more information read the answer below.
#target_user.reload
It is because you never actually added the guardian to the user's guardian list such as:
#target_user.guardians << #target_guardian
Only after that you will be able to see guardians in #target_user.guardians.
When testing with rspec you need to call the action in your controller that is actualling adding the guardian to the guardians collection in user and then test if the guardian was correctly added with something like
#target_user.guardians.size.should_be 1
I have a Mongo collection that simply references an ID to another collection. Hypothetically the collection I'm specifically referring to might be called:
Walks. Walks has a reference to owner_id. The owner takes many walks with his many pets every day. What I want to do is query Walks for a list of N owner_ids and get only the last walk they took for each owner and group that by owner_id. To get a list of all walks by said list we'd do something like.
Walk.any_in(:owner_id => list_of_ids)
My question is, is there a way to query that list_of_ids, get only one walk per owner_id (the last one they took which can be sorted by the field created_at and returned in a hash where each walk is pointed to by an owner_id such as:
{ 5 => {..walk data..}, 10 => {.. walk data ..}}
Here's an answer that uses MongoDB's group command.
For the purposes of testing, I've used walk_time instead of created_at.
Hope that this helps and that you like it.
class Owner
include Mongoid::Document
field :name, type: String
has_many :walks
end
class Walk
include Mongoid::Document
field :pet_name, type: String
field :walk_time, type: Time
belongs_to :owner
end
test/unit/walk_test.rb
require 'test_helper'
class WalkTest < ActiveSupport::TestCase
def setup
Owner.delete_all
Walk.delete_all
end
test "group in Ruby" do
walks_input = {
'George' => [ ['Fido', 2.days.ago], ['Fifi', 1.day.ago], ['Fozzy', 3.days.ago] ],
'Helen' => [ ['Gerty', 4.days.ago], ['Gilly', 2.days.ago], ['Garfield', 3.days.ago] ],
'Ivan' => [ ['Happy', 2.days.ago], ['Harry', 6.days.ago], ['Hipster', 4.days.ago] ]
}
owners = walks_input.map do |owner_name, pet_walks|
owner = Owner.create(name: owner_name)
pet_walks.each do |pet_name, time|
owner.walks << Walk.create(pet_name: pet_name, walk_time: time)
end
owner
end
assert_equal(3, Owner.count)
assert_equal(9, Walk.count)
condition = { owner_id: { '$in' => owners[0..1].map(&:id) } } # don't use all owners for testing
reduce = <<-EOS
function(doc, out) {
if (out.last_walk == undefined || out.last_walk.walk_time < doc.walk_time)
out.last_walk = doc;
}
EOS
last_walk_via_group = Walk.collection.group(key: :owner_id, cond: condition, initial: {}, reduce: reduce)
p last_walk_via_group.collect{|r|[Owner.find(r['owner_id']).name, r['last_walk']['pet_name']]}
last_walk = last_walk_via_group.collect{|r|Walk.new(r['last_walk'])}
p last_walk
end
end
test output
Run options: --name=test_group_in_Ruby
# Running tests:
[["George", "Fifi"], ["Helen", "Gilly"]]
[#<Walk _id: 4fbfa7a97f11ba53b3000003, _type: nil, pet_name: "Fifi", walk_time: 2012-05-24 15:39:21 UTC, owner_id: BSON::ObjectId('4fbfa7a97f11ba53b3000001')>, #<Walk _id: 4fbfa7a97f11ba53b3000007, _type: nil, pet_name: "Gilly", walk_time: 2012-05-23 15:39:21 UTC, owner_id: BSON::ObjectId('4fbfa7a97f11ba53b3000005')>]
.
Finished tests in 0.051868s, 19.2797 tests/s, 38.5594 assertions/s.
1 tests, 2 assertions, 0 failures, 0 errors, 0 skips
I'm trying to setup single table inheritance in my Rails app for a User model and its subclasses Member, Subscriber, and Staff.
I have a model file for each: user.rb, member.rb, etc
The user model is defined: class User < ActiveRecord::Base; end;
I subclassed the other models as such: class Member < User; end; and so on.
In my users table I have all the fields every class needs plus the type field. Now when I go to the console and try to create a new instance of say member or subscriber i get the following error:
TypeError: can't dup NilClass
from /Library/Ruby/Gems/1.8/gems/activerecord-2.3.4/lib/active_record/base.rb:2184:in 'dup'
from /Library/Ruby/Gems/1.8/gems/activerecord-2.3.4/lib/active_record/base.rb:2184:in 'scoped_methods'
from /Library/Ruby/Gems/1.8/gems/activerecord-2.3.4/lib/active_record/base.rb:2188:in 'current_scoped_methods'
from /Library/Ruby/Gems/1.8/gems/activerecord-2.3.4/lib/active_record/base.rb:2171:in 'scoped?'
from /Library/Ruby/Gems/1.8/gems/activerecord-2.3.4/lib/active_record/base.rb:2439:in 'send'
from /Library/Ruby/Gems/1.8/gems/activerecord-2.3.4/lib/active_record/base.rb:2439:in 'initialize'
from (irb):6:in 'new'
from (irb):6
Rails know the subclasses models are there because in the console when I simply call Member or Subscriber, i get the class definition returned.
I've read the simple documentation, but I must be missing something?
I tried on my side starting from a scratch application and it works
Here is my User model (User.rb)
class User < ActiveRecord::Base
end
My member model (Member.rb)
class Member < User
end
I have one migration file to create my users table which contains:
class CreateUsers < ActiveRecord::Migration
def self.up
create_table :users do |t|
t.string :name
t.timestamps
end
end
def self.down
drop_table :users
end
end
Now launching the console:
➜ ./script/console
Loading development environment (Rails 2.3.4)
>> u = User.new
=> #<User id: nil, name: nil, created_at: nil, updated_at: nil>
>> m = Member.new
=> #<Member id: nil, name: nil, created_at: nil, updated_at: nil>
>> m.name="hop"
=> "hop"
>> m.save
=> true
However I did not manage to reproduce your error :(
Do you have a type column of type varchar (string in ruby)? Try the following commands (in a new rails project)
class Member < User
end
C:\projects\test\sti>ruby script\generate model user name:string type:string membertype:string
exists app/models/
exists test/unit/
exists test/fixtures/
create app/models/user.rb
create test/unit/user_test.rb
create test/fixtures/users.yml
create db/migrate
create db/migrate/20091019051506_create_users.rb
C:\projects\test\sti>rake db:migrate
(in C:/projects/test/sti)
== CreateUsers: migrating ====================================================
-- create_table(:users)
-> 0.0000s
== CreateUsers: migrated (0.0000s) ===========================================
C:\projects\test\sti>ruby script\console
Loading development environment (Rails 2.3.4)
>> u = User.new
=> #<User id: nil, name: nil, type: nil, membertype: nil, created_at: nil, updated_at: nil>
>> m = Member.new
=> #<Member id: nil, name: nil, type: "Member", membertype: nil, created_at: nil, updated_at: nil>
>> m.name = 'fred'
=> "fred"
>> m.save
=> true
>> u.name = 'rader'
=> "rader"
>> u.save
=> true
>> User.find :all
=> [#<Member id: 1, name: "fred", type: "Member", membertype: nil, created_at: "2009-10-19 05:17:11", updated_at: "2009-10-19 05:17:11">, #<User id: 2, name: "rader", type: nil, membertype: nil, created_at: "2009-10-19 05:17:24", updated_at: "2009-10-19 05:17:24">]
>>
Check this page, there are more than few solutions to this problem (even in comments).
http://strd6.com/2009/04/cant-dup-nilclass-maybe-try-unloadable/
I'm thinking that the problem is in one of your model definitions because of the stack trace you show. If you still are having a problem, pastie your code, and i'm sure you'll get a good answer.
I hade exactly this problem, after I extracted some functionality to a plugin.
But i my case it worked from the console, so i made sure id reloaded, with this line in init.rb
ActiveSupport::Dependencies.load_once_paths.delete(
File.expand_path(File.dirname(__FILE__))+'/app/models')
I ran into something similar a while back and this website helped:
http://www.dansketcher.com/2009/05/11/cant-dup-nilclass/
class User < ActiveRecord::Base
unloadable
...
end
Not sure why this occurs as I could not track down anything abnormal. I do believe it was a STI situation though.
This is not exactly a question, it's rather a report on how I solved an issue with write_attribute when the attribute is an object, on Rails' Active Record. I hope this can be useful to others facing the same problem.
Let me explain with an example. Suppose you have two classes, Book and Author:
class Book < ActiveRecord::Base
belongs_to :author
end
class Author < ActiveRecord::Base
has_many :books
end
Very simple. But, for whatever reason, you need to override the author= method on Book. As I'm new to Rails, I've followed the Sam Ruby's suggestion on Agile Web Development with Rails: use attribute_writer private method. So, my first try was:
class Book < ActiveRecord::Base
belongs_to :author
def author=(author)
author = Author.find_or_initialize_by_name(author) if author.is_a? String
self.write_attribute(:author, author)
end
end
Unfortunately, this does not work. That's what I get from console:
>> book = Book.new(:name => "Alice's Adventures in Wonderland", :pub_year => 1865)
=> #<Book id: nil, name: "Alice's Adventures in Wonderland", pub_year: 1865, author_id: nil, created_at: nil, updated_at: nil>
>> book.author = "Lewis Carroll"
=> "Lewis Carroll"
>> book
=> #<Book id: nil, name: "Alice's Adventures in Wonderland", pub_year: 1865, author_id: nil, created_at: nil, updated_at: nil>
>> book.author
=> nil
It seems that Rails does not recognize it is an object and makes nothing: after the attribuition, author is still nil! Of course, I could try write_attribute(:author_id, author.id), but it does not help when the author is not saved yet (it still has no id!) and I need the objects be saved together (author must be saved only if book is valid).
After search a lot for a solution (and try many other things in vain), I found this message: http://groups.google.com/group/rubyonrails-talk/browse_thread/thread/4fe057494c6e23e8, so finally I could had some working code:
class Book < ActiveRecord::Base
belongs_to :author
def author_with_lookup=(author)
author = Author.find_or_initialize_by_name(author) if author.is_a? String
self.author_without_lookup = author
end
alias_method_chain :author=, :lookup
end
This time, the console was nice to me:
>> book = Book.new(:name => "Alice's Adventures in Wonderland", :pub_year => 1865)
=> #<Book id: nil, name: "Alice's Adventures in Wonderland", pub_year: 1865, author_id: nil, created_at: nil, updated_at: nil>
>> book.author = "Lewis Carroll"=> "Lewis Carroll"
>> book
=> #<Book id: nil, name: "Alice's Adventures in Wonderland", pub_year: 1865, author_id: nil, created_at: nil, updated_at: nil>
>> book.author
=> #<Author id: nil, name: "Lewis Carroll", created_at: nil, updated_at: nil>
The trick here is the alias_method_chain, that creates an interceptor (in this case author_with_lookup) and an alternative name to the old setter (author_without_lookup). I confess it took some time to understand this arrangement and I'd be glad if someone care to explain it in detail, but what surprised me was the lack of information about this kind of problem. I have to google a lot to find just one post, that by the title seemed initially unrelated to the problem. I'm new to Rails, so what do you think guys: is this a bad practice?
I recommend creating a virtual attribute instead of overriding the author= method.
class Book < ActiveRecord::Base
belongs_to :author
def author_name=(author_name)
self.author = Author.find_or_initialize_by_name(author_name)
end
def author_name
author.name if author
end
end
Then you could do cool things like apply it to a form field.
<%= f.text_field :author_name %>
Would this work for your situation?
When you override the accessor, you have to set an actual DB attribute for write_attribute and self[:the_attribute]=, and not the name of the association-generated attribute you're overriding. This works for me.
require 'rubygems'
require 'active_record'
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
ActiveRecord::Schema.define do
create_table(:books) {|t| t.string :title }
create_table(:authors) {|t| t.string :name }
end
class Book < ActiveRecord::Base
belongs_to :author
def author=(author_name)
found_author = Author.find_by_name(author_name)
if found_author
self[:author_id] = found_author.id
else
build_author(:name => author_name)
end
end
end
class Author < ActiveRecord::Base
end
Author.create!(:name => "John Doe")
Author.create!(:name => "Tolkien")
b1 = Book.new(:author => "John Doe")
p b1.author
# => #<Author id: 1, name: "John Doe">
b2 = Book.new(:author => "Noone")
p b2.author
# => #<Author id: nil, name: "Noone">
b2.save
p b2.author
# => #<Author id: 3, name: "Noone">
I strongly recommend doing what Ryan Bates suggests, though; create a new author_name attribute and leave the association generated methods as they are. Less fuzz, less confusion.
I solved this problem using alias_method
class Book < ActiveRecord::Base
belongs_to :author
alias_method :set_author, :author=
def author=(author)
author = Author.find_or_initialize_by_name(author) if author.is_a? String
set_author(author)
end
end