RSpec - refreshing associations - ruby-on-rails

I have a LabCollection:
class LabCollection < ApplicationRecord
# Relationships
belongs_to :lab_container, polymorphic: true, optional: true
has_many :lab_collection_labs
has_many :labs, -> { published }, through: :lab_collection_labs
has_many :lab_collection_inclusions, dependent: :destroy
end
It has many LabCollectionLabs:
class LabCollectionLab < ApplicationRecord
acts_as_list scope: :lab_collection_id, add_new_at: :bottom
belongs_to :lab_collection
belongs_to :lab
end
Which has a lab ID and belongs to a lab.
I have a spec which tests how new associations are created, and it's failling at the following point:
context 'when the lab container has labs already present' do
it 'removes the present labs and adds the new ones' do
subject = RawLabAdder
expect(populated_lab_collection.labs).to eq labs
subject.new(populated_lab_collection, [lab4.id, lab5.id]).perform
expect(populated_lab_collection.labs).not_to eq labs
binding.pry
expect(populated_lab_collection.labs).to eq [lab4, lab5]
end
end
If you need to see the internal working of the code let me know, however the issue seems to be with RSpec and refreshing associations. When I hit the binding.pry point and call populated_lab_collection.lab_collection_labs
populated_lab_collection.lab_collection_labs
=>
[lab_collection_lab_4, lab_collection_lab_5]
However when I call .labs instead:
populated_lab_collection.labs
=>
[]
Inspecting the lab_collection_labs, I can see that they each have a lab_id and that a lab exists for those ID's. I believe my problem is that I'm not refreshing the records correctly, however I've tried:
# populated_lab_collection.reload
# populated_lab_collection.lab_collection_labs.reload
# populated_lab_collection.lab_collection_labs.each do |x|
# x.lab.reload
# end
# populated_lab_collection.labs.reload
Any advice on how I can get RSpec to correctly read in a records nested associations is greatly appreciated. As I say when I inspect the record, it has 2 lab_inclusion_labs, which each have a lab, however the parent record apparently has no labs.
EDIT: RawLabAdder class:
module LabCollections
class RawLabAdder
def initialize(incoming_lab_collection, lab_ids = [])
#lab_ids = lab_ids
#lab_collection = incoming_lab_collection
end
def perform
remove_associated_labs
add_labs
end
private
def add_labs
#lab_ids.each do |x|
lab = Lab.find(x)
LabCollectionInclusionAdder.new(#lab_collection, lab).perform
end
end
def remove_associated_labs
#lab_collection.lab_collection_inclusions.map(&:destroy)
#lab_collection.lab_collection_labs.map(&:destroy)
end
end
end

If you create an instance in a before hook or in a spec and then perform some database related work on it the instance you have will no longer reference the up to date information.
Try reloading the populated_lab_collection before asserting.
expect(populated_lab_collection.reload.labs).not_to eq labs

Related

Access to model instance up the where chain in Rails

Is it possible to, within the record found through an association, retain access to the related model instance which found it?
Example:
class Person < ApplicationRecord
has_many :assignments
attr_accessor :info_of_the_moment
end
p = Person.first
p.info_of_the_moment = "I don't want this in the db"
assignment = p.assignments.first
assignment.somehow_get_p.info_of_the_moment # or some such magic!
And/or is there a way to "hang on to" the parameters of a scope and have access to them from within the found model instance? Like:
class Person < ApplicationRecord
has_many :assignments
attr_accessor :info_of_the_moment
scope :fun_assignments, -> (info) { where(fun: true) }
end
class Assignment < ApplicationRecord
belongs_to :person
def get_original_info
# When I was found, info was passed into the scope. What was it?
end
end
You can add your own extension methods to an association and those methods can get at the association's owner through proxy_association:
has_many :things do
def m
# Look at proxy_association.owner in here
end
end
So you could say things like:
class Person < ApplicationRecord
has_many :assignments do
def with_info
info = proxy_association.owner.info_of_the_moment
# Then we wave our hands and some magic happens to encode
# `info` into a properly escaped SQL literal that we can
# toss in a `select` call. If you're working with PostgreSQL
# then JSON would be a reasonable first choice if the info
# was, say, a hash.
#
# The `::jsonb` in the `select` call is there to tell everyone
# that the `info_of_the_moment` column is JSON and should be
# decoded as such by ActiveRecord.
encoded_info = ApplicationRecord.connection.quote(info.to_json)
select("assignments.*, #{encoded_info}::jsonb as info_of_the_moment")
end
end
#...
end
p = Person.first
p.info_of_the_moment = { 'some hash' => 'that does', 'not go in' => 'the database' }
assignment = p.assignments.with_info.first
assignment.info_of_the_moment # And out comes the hash but with stringified keys regardless of the original format.
# These will also include the `info_of_the_moment`
p.assignments.where(...).with_info
p.assignments.with_info.where(...)
Things of note:
All the columns in select show up as methods even when they're not part of the table in question.
You can add "extension" methods to an association by including a block with those methods when calling the association's method.
An SQL SELECT can include values that aren't columns, literals work just fine.
What format you use to tunnel your extra information through the association depends on the underlying database.
If the encoded extra information is large then this can get expensive.
This is admittedly a bit kludgey and brittle so I'd agree with you that rethinking your whole approach is a better idea.

Is it bad to create ActiveRecord objects in a constant?

I have some code where I create ActiveRecord objects as constants in my model like so:
class OrderStage < ActiveRecord::Base
validates :name, presence: true, uniqueness: true
DISPATCHED = find_or_create_by(name: 'Dispatched')
end
Each Order has an OrderStage:
class Order < ActiveRecord::Base
belongs_to :order_stage
end
Now this seems to work fine throughout the site, and in my integration tests. However it is breaking in my unit test. The following test
it 'lists all order stages' do
# using FactoryGirl
create(:order, order_stage: OrderStage::DISPATCHED)
expect(Order.all.map(&:order_stage)).to eq [OrderStage::DISPATCHED]
end
passes fine when I run it individually, or when I run just order_spec.rb. But when I run the whole test suite, or even just spec/models I get this error:
Failure/Error: expect(Order.all.map(&:order_stage)).to eq [OrderStage::DISPATCHED]
expected: [#<OrderStage id: 1, name: "Dispatched">]
got: [nil]
That error goes away if I write my test like so:
it 'lists all order stages' do
order_stage = OrderStage.find_or_create_by(name: 'Dispatched')
create(:order, order_stage: order_stage)
expect(Order.all.map(&:order_stage)).to eq [order_stage]
end
So it must be something to do with creating the ActiveRecord object in the constant, it this a bad thing to do?
You should use class method.
attr_accessible :name
def self.dispatched
#dispatched ||= find_or_create_by_name('Dispatched')
end
private
def self.reset!
#dispatched = nil
end

undefined method `price_tier' for nil:NilClass

Wondering if someone can help me find this issue. I'm using rails 4, ruby 2, and have spent alot of time trying different accessors, etc and nothing has worked.
The whole plan model:
class Plan < ActiveRecord::Base
has_many :users
end
Some of the user model:
class User < ActiveRecord::Base
...
validate :plan_type_valid
belongs_to :plan
...
def plan_type_valid
if free_ok
# the following line causes error
valid_plans = Plan.where(price_tier: plan.price_tier).pluck(:id)
else
valid_plans = Plan.where(price_tier: plan.price_tier).where.not(stripe_id: 'free').pluck(:id)
end
unless valid_plans.include?(plan.id)
errors.add(:plan_id, 'is invalid')
end
end
end
Here's a pastebin of the whole users controller:
http://pastebin.com/GnXz3R8k
the migration was all messed up because of a superuser issue and it wasn't able to create the extensions for hstore field type.

How can I validate that two associated objects have the same parent object?

I have two different objects which can belong to one parent object. These child objects can both also belong to each other (many to many). What's the best way to ensure that child objects which belong to each other also belong to the same parent object.
As an example of what I'm trying to do I have a Kingdom which has both many People and Land. The People model would have a custom validate which checks each related Land and error.adds if one has a mismatched kingdom_id. The Land model would have a similar validate.
This seems to work, but when updating it allows the record to save the 'THIS IS AN ERROR' error is in people.errors, however the Land which raised the error has been added to the People collection.
kingdom = Kingdom.create
people = People.create(:kingdom => kingdom)
land = Land.create(:kingdom_id => 999)
people.lands << land
people.save
puts people.errors.inspect # #messages={:base=>["THIS IS AN ERROR"]
puts people.lands.inspect # [#<Land id: 1...
Ideally I'd want the error to cancel the record update. Is there another way I should be going about this, or am I going in the wrong direction entirely?
# Models:
class Kingdom < ActiveRecord::Base
has_many :people
has_many :lands
end
class People < ActiveRecord::Base
belongs_to :kingdom
has_and_belongs_to_many :lands
validates :kingdom_id, :presence => true
validates :kingdom, :associated => true
validate :same_kingdom?
private
def same_kingdom?
if self.lands.any?
errors.add(:base, 'THIS IS AN ERROR') unless kingdom_match
end
end
def kingdom_match
self.lands.each do |l|
if l.kingdom_id != self.kingdom_id
return false
end
end
end
end
class Land < ActiveRecord::Base
belongs_to :kingdom
has_and_belongs_to_many :people
end
Firstly, the validation won't prevent the record from being added to the model's unpersisted collection. It will prevent the revised collection from being persisted to the database. So the model will be in an invalid state, and flagged as such with the appropriate errors. To see this, you can simply reload the people object.
You also have an error in your logic - the kingdom_match method will never return true even if no invalid kingdom_id's are found. You should add a line to fix this:
def kingdom_match
self.lands.each do |l|
return false if l.kingdom_id != self.kingdom_id
end
true
end
And you can make this validation a bit more concise and skip the kingdom_match method entirely:
def same_kingdom?
if self.lands.any?{|l| l.kingdom_id != self.kingdom_id }
errors.add(:base, 'THIS IS AN ERROR')
end
end

Rails3 / Mongoid : many-to-many relation and delete operation

I have the following model :
class User
include Mongoid::Document
include Mongoid::Timestamps
has_and_belongs_to_many :challenged, class_name: "Event", inverse_of: "challengers"
def challenge!(event)
self.challenged << event
self.save!
end
def unchallenge!(event)
self.challenged.where(_id: event.id).destroy_all
self.save!
end
end
class Event
include Mongoid::Document
include Mongoid::Timestamps
has_and_belongs_to_many :challengers, class_name: "User", inverse_of: "challenged"
end
The challenge! method works fine, but the unchallenge! one does not.
If I test it with the following code :
require 'spec_helper'
describe User do
before(:each) do
#user = User.create!
#event = Event.create!
end
it "should unchallenge an event" do
#user.challenge!(#event)
#user.unchallenge!(#event)
#user.challenged.should_not include(#event)
end
end
The test fails ; so it seems that the delete operation did not work.
The application logs are :
MONGODB test_mongo_test['users'].insert([{"challenged_ids"=>[], "_id"=>BSON::ObjectId('4f2d13fdbd028603f7000001')}])
MONGODB test_mongo_test['system.namespaces'].find({})
MONGODB test_mongo_test['events'].insert([{"challenger_ids"=>[], "_id"=>BSON::ObjectId('4f2d13fdbd028603f7000002')}])
MONGODB test_mongo_test['events'].update({"_id"=>BSON::ObjectId('4f2d13fdbd028603f7000002')}, {"$addToSet"=>{"challenger_ids"=>{"$each"=>[BSON::ObjectId('4f2d13fdbd028603f7000001')]}}})
MONGODB test_mongo_test['users'].update({"_id"=>BSON::ObjectId('4f2d13fdbd028603f7000001')}, {"$pushAll"=>{"challenged_ids"=>[BSON::ObjectId('4f2d13fdbd028603f7000002')]}})
MONGODB test_mongo_test['$cmd'].find({"count"=>"events", "query"=>{"$and"=>[{:_id=>{"$in"=>[BSON::ObjectId('4f2d13fdbd028603f7000002')]}}, {:_id=>BSON::ObjectId('4f2d13fdbd028603f7000002')}]}, "fields"=>nil}).limit(-1)
MONGODB test_mongo_test['events'].remove({"$and"=>[{:_id=>{"$in"=>[BSON::ObjectId('4f2d13fdbd028603f7000002')]}}, {:_id=>BSON::ObjectId('4f2d13fdbd028603f7000002')}]})
MONGODB test_mongo_test['events'].find({:_id=>{"$in"=>[BSON::ObjectId('4f2d13fdbd028603f7000002')]}})
MONGODB test_mongo_test['events'].find({:_id=>{"$in"=>[BSON::ObjectId('4f2d13fdbd028603f7000002')]}})
Check your application log
If I'm right self.challenged returns Event criteria, not User criteria, so fix is:
self.challenged.where(_id: event.id).destroy_all
Upd:
I've written same test as you did( just different model names ):
# announcement_list.rb
def challenge!(announce)
self.announcements << announce
self.save!
end
def unchallenge!(announce)
self.announcements.where( _id: announce.id ).destroy_all
self.save!
end
#anouncement_list_spec.rb
context 'stackoverflow#9132596' do
let!( :announcement_list ) { Factory( :announcement_list ) }
let!( :announcement ) { Factory( :announcement ) }
it 'kick announcement out of list on unchallenge! method call' do
announcement_list.challenge!( announcement )
announcement_list.unchallenge!( announcement )
announcement_list.announcements.should_not include( announcement )
end
end
This way i get my tests failed. Then I change:
def unchallenge!(announce)
self.announcements.destroy_all( conditions: { _id: announce.id })
self.save!
end
Yap, little bit tricky, and definitely should be placed in mongoid tracker. Hope this will help you.

Resources