Rails3 / Mongoid : many-to-many relation and delete operation - ruby-on-rails

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.

Related

Rspecs for model and before_create methods

I have model name: UserApplicationPatient.
That model having two associations:
belongs_to :patient
belongs_to :customer
before_create :set_defaults
private
def set_defaults
self.enrol_date_and_time = Time.now.utc
self.pap = '1'
self.flg = '1'
self.patient_num = "hos_#{patient_id}"
end
Factories of UserApplicationPatient
FactoryBot.define do
factory :user_application_patient do
association :patient
association :customer
before(:create) do |user_application_patient, evaluator|
FactoryBot.create(:patient)
FactoryBot.create(:customer)
end
end
end
Model spec:
require 'spec_helper'
describe UserApplicationPatient do
describe "required attributes" do
let!(:user_application_patient) { described_class.create }
it "return an error with all required attributes" do
expect(user_application_patient.errors.messages).to eq(
{ patient: ["must exist"],
customer: ["must exist"]
},
)
end
end
end
This is the first time I am writing specs of models.
Could someone please tell me how to write specs for set_defaults before_create methods and factories what I have written is correct or not.
Since you are setting default values in the before_create hook, I will recommend validating it like this
describe UserApplicationPatient do
describe "required attributes" do
let!(:user_application_patient) { described_class.create }
it "return an error with all required attributes" do
# it will validate if your default values are populating when you are creating a new object
expect(user_application_patient.pap).to eq('1')
end
end
end
To test your defaults are set, create a user and test if the defaults are set.
You definitely don't want to use let!, there's no need to create the object until you need it.
Since we're testing create there's no use for a let here at all.
How do we test Time.now? We can freeze time!
I assume patient_id should be patient.id.
Here's a first pass.
it 'will set default attributes on create' do
freeze_time do
# Time will be the same everywhere in this block.
uap = create(:user_application_patient)
expect(uap).to have_attributes(
enrol_date_and_time: Time.now.utc,
pap: '1',
flg: '1',
patient_num: "hos_#{uap.patient.id}"
)
end
end
it 'will not override existing attributes' do
uap_attributes = {
enrol_date_and_time: 2.days.ago,
pap: '23',
flg: '42',
patient_num: "hos_1234"
}
uap = create(:user_application_patient, **uap_attributes)
expect(uap).to have_attributes(**uap_attributes)
end
These will probably fail.
Defaults are set after validation has taken place.
Existing attributes are overwritten.
What is patient_id?
We can move setting defaults to before validation. That way the object can pass validation, and we can also see the object's attributes before writing it to the database.
We can fix set_defaults so it doesn't override existing attributes.
Time.now should not be used, it is not aware of time zones. Use Time.current. And there's no reason to pass in UTC, the database will store times as UTC and Rails will convert for you.
belongs_to :patient
belongs_to :customer
before_validation :set_defaults
private
def set_defaults
self.enrol_date_and_time ||= Time.current
self.pap ||= '1'
self.flg ||= '1'
self.patient_num ||= "hos_#{patient.id}"
end
We can also make your factory a bit more flexible.
FactoryBot.define do
factory :user_application_patient do
association :patient, strategy: :create
association :customer, strategy: :create
end
end
This way, the patient and customer will be created regardless whether you build(:user_application_patient) or create(:user_application_patient). This is necessary for user_application_patient to be able to reference its patient.id.
In general, don't do things at create time.

RSpec - refreshing associations

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

Active Record Association CollectionProxy not working: why does #owner.#target.count yield a result of 1 when #owner.#target returns an empty array?

I have models in my Rails app:
Sales_Opportunity which has_many Swots.
I'm setting them up using FactoryGirl and running a test to show that when I delete my Sales_Opportunity I also cause the associated Swots to be deleted. For some reason when debugging with Byebug I'm getting strange results - the Sales_Opportunity and Swot records are being correctly created, but when I run sales_opportunity.swots it returns [ ] whereas sales_opportunity.swots.count returns 1. What's more odd is the exact same code works fine with anther object association (timeline_events are exactly like swots yet work perfectly with the same code).
Can anyone tell me what am I doing wrong please?
Sales_Opportunity.rb:
class SalesOpportunity < ActiveRecord::Base
default_scope { order('close_date ASC') }
belongs_to :user
belongs_to :company
has_many :key_contacts
has_many :timeline_events, dependent: :destroy
has_many :swots, dependent: :destroy
end
Swot.rb:
class Swot < ActiveRecord::Base
belongs_to :sales_opportunity
validates :swot_details, presence: true
validates :sales_opportunity_id, presence: true
enum swot_type: [:strength, :weakness, :opportunity, :threat]
enum swot_importance: { minimal: 1, marginal: 2, noteworthy: 3, significant: 4, critical: 5 }
validates :swot_importance, presence: true
end
Swot FactoryGirl spec:
FactoryGirl.define do
factory :swot do
swot_importance "minimal"
swot_details "Some boring details"
swot_type "threat"
trait :attached do
association :sales_opportunity, :with_user_id
end
end
end
Sales_Opportunity FactoryGirl spec:
FactoryGirl.define do
sequence(:opportunity_name) { |n| "Sales Oppotunity - #{n}" }
factory :sales_opportunity do
user
opportunity_name {generate(:opportunity_name)}
close_date "2014/12/12"
sale_value 10000
company_id 7
trait :with_user_id do
user_id 6
end
end
end
Failing Rspec tests:
describe "when swot's parent sales opportunity is destroyed" do
let(:swot) { FactoryGirl.create(:swot, :attached) }
let(:sales_opportunity) { swot.sales_opportunity }
it "should destroy associated swots" do
dswots = sales_opportunity.swots.to_a
byebug
sales_opportunity.destroy
expect(dswots).not_to be_empty
dswots.each do |dswot|
expect(Swot.where(id: dswot.id)).to be_empty
end
end
end
Output from the console (byebug) when logging swot:
#<Swot id: 13, swot_type: 3, swot_importance: 1, sales_opportunity_id: 564, swot_details: "Some boring details", created_at: "2015-07-27 10:57:23", updated_at: "2015-07-27 10:57:23">
Output from the console when logging sales_opportunity:
#<SalesOpportunity id: 564, close_date: "2014-12-12 00:00:00", user_id: 6, created_at: "2015-07-27 10:57:23", updated_at: "2015-07-27 10:57:23", pipeline_status: 0, opportunity_name: "Sales Oppotunity - 4", company_id: 7, sale_value: #<BigDecimal:7fe9ffd25078,'0.1E5',9(27)>, swot_score: 0>
Output for sales_opportunity.swots.count:
(byebug) sales_opportunity.swots.count
1
Output for sales_opportunity.swots:
(byebug) sales_opportunity.swots
#<ActiveRecord::Associations::CollectionProxy []>
I think I've included all the known info. The Rspec tests, FactoryGirl factories and setup between sales_opportunities and Swots/Timeline_Events is exactly the same - yet the Rspec tests pass for Timeline_Events and the collection_proxy works for those (so as far as I can tell, the code is identical):
Timeline_Event Factory:
FactoryGirl.define do
factory :timeline_event do
activity "Some activity"
due_date "2014/11/11"
trait :attached do
association :sales_opportunity, :with_user_id
end
end
end
Working Rspec tests:
describe "when sales opportunity is destroyed for timeline event" do
let(:timeline_event) { FactoryGirl.create(:timeline_event, :attached) }
let(:sales_opportunity) { timeline_event.sales_opportunity }
it "should destroy associated timeline events" do
timeline_events = sales_opportunity.timeline_events.to_a
sales_opportunity.destroy
expect(timeline_events).not_to be_empty
timeline_events.each do |event|
expect(TimelineEvent.where(id: event.id)).to be_empty
end
end
end
Timeline_Event.rb:
class TimelineEvent < ActiveRecord::Base
belongs_to :sales_opportunity
validates :activity, presence: true
validates :due_date, presence: true
validates :sales_opportunity_id, presence: true
end
When running byebug in the same place here I get an array including the Timeline_Event.
Can anyone help me understand what's going wrong in my code?
Thanks.
I solved the issue with this - it seems the sales_opportunity needs to be reloaded for the Active Record Associations to be persisted. This answer is the key to the solution.
Here's the working code:
describe "when swot's parent sales opportunity is destroyed" do
let!(:swot) { FactoryGirl.create(:swot, sales_opportunity: sales_opportunity) }
it "should destroy associated swots" do
sales_opportunity.reload
expect { sales_opportunity.destroy }.to change(sales_opportunity.swots, :count).by(-1)
end
end
Using some elements of Max's answer above also helped me improve the look and feel of the code.
RSpec.describe SalesOpportunity, type: :model do
let(:sales_opportunity) { create(:sales_opportunity) }
describe "swots" do
let!(:swot) { create(:swot, sales_opportunity: sales_opportunity) }
it "destroys nested swots" do
sales_opportunity.destroy
swot.reload
expect(swot.destroyed?).to be_truthy
end
end
end
Note that i'm adding this to the SalesOpportunity spec, because the dependant destroy behavior belongs to SalesOpportunity not the child association.
Edit.
Another way to write this spec would be:
it "destroys nested swots" do
expect {
sales_opportunity.destroy
}.to change(Swot, :count).by(-1)
end

rspec expect to change record value after create it through post

I have this piece of example to create a new record through post which is passed
describe 'POST create' do
let(:schedule_child) { FactoryGirl.create(:schedule_child) }
let(:post_queue) { post :create, schedule_child_id: schedule_child.id, format: :js }
it { expect{post_queue}.to change(PatientQueue, :count).by(1) }
end
And I have one attribute, PatientQueue.queue_number, which will be increased by 1 every time a new record is added. Now I'd like to see if this attributes has changed.
it { expect{post_queue}.to change(PatientQueue, :queue_number).by(1) }
But here is what I got
NoMethodError: undefined method `queue_number' for #<Class:0x0000000849e780>
How should I write it properly?
== UPDATE ==
model PatientQueue
class PatientQueue < ActiveRecord::Base
# Validations
validates :patient, :schedule_child, presence: true
validate :is_not_exist
# Relations
belongs_to :schedule_child
belongs_to :patient
before_create :insert_queue_number
def is_exist?
PatientQueue.find_by_schedule_child_id_and_patient_id(schedule_child_id, patient_id).present?
end
private
def insert_queue_number
last_id = PatientQueue.where("schedule_child_id = ?", self.schedule_child_id).count
self.queue_number = last_id + 1
end
def is_not_exist
errors.add(:schedule_child, :is_exist) if is_exist?
end
end
PatientQueue is an activerecord class, which has a method count
post_queue is an instance of the class and has the method queue_number
the class does not have the same methods as the instance, so you might write your test like change(post_queue, :queue_number).by(1)
However, the test is a little hard to follow, can you show us your data model relationships? if a PatientQueue has_many schedule_child, maybe you just want to use rails cache_counter? http://www.elegantruby.com/Tutorials/2013/01/25/adding-a-counter-cache-for-fun-and-profit/

validate presence of has_and_belongs_to_many

Hi i'm using a has_and_belongs_to_many in a model.
I want set the valitor of presence for kinds.
and set the max number of kinds per core to 3
class Core < ActiveRecord::Base
has_and_belongs_to_many :kinds, :foreign_key => 'core_id', :association_foreign_key => 'kind_id'
end
how can i do?
thanks
validate :require_at_least_one_kind
validate :limit_to_three_kinds
private
def require_at_least_one_kind
if kinds.count == 0
errors.add_to_base "Please select at least one kind"
end
end
def limit_to_three_kinds
if kinds.count > 3
errors.add_to_base "No more than 3 kinds, please"
end
end
You could try something like this (tested on Rails 2.3.4):
class Core < ActiveRecord::Base
has_and_belongs_to_many :kinds, :foreign_key => 'core_id', :association_foreign_key => 'kind_id'
validate :maximum_three_kinds
validate :minimum_one_kind
def minimum_one_kind
errors.add(:kinds, "must total at least one") if (kinds.length < 1)
end
def maximum_three_kinds
errors.add(:kinds, "must not total more than three") if (kinds.length > 3)
end
end
... which works in the following way:
require 'test_helper'
class CoreTest < ActiveSupport::TestCase
test "a Core may have kinds" do
core = Core.new
3.times { core.kinds << Kind.new }
assert(core.save)
end
test "a Core may have no more than 3 kinds" do
core = Core.new
4.times { core.kinds << Kind.new }
core.save
assert_equal(1, core.errors.length)
assert_not_nil(core.errors['kinds'])
end
test "a Core must have at least one kind" do
core = Core.new
core.save
assert_equal(1, core.errors.length)
assert_not_nil(core.errors['kinds'])
end
end
Obviously the above isn't particularly DRY or production-ready, but you get the idea.

Resources