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.
Related
We are developing a mogration from a small issue tracker software to Redmine. We use the Ruby classes directly to migrate the data. The class for an issue is defined like this:
class BuggyIssue < ActiveRecord::Base
self.table_name = :issues
belongs_to :last_issue_change, :class_name => 'BuggyIssueChange', :foreign_key => 'last_issue_change_id'
has_many :issue_changes, :class_name => 'BuggyIssueChange', :foreign_key => 'issue_id', :order => 'issue_changes.date DESC'
set_inheritance_column :none
# Issue changes: only migrate status changes and comments
has_many :issue_changes, :class_name => "BuggyIssueChange", :foreign_key => :issue_id
def attachments
#BuggyMigrate::BuggyAttachment.all(:conditions => ["type = 'issue' AND id = ?", self.id.to_s])
end
def issue_type
read_attribute(:type)
end
def summary
read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary)
end
def description
read_attribute(:description).blank? ? summary : read_attribute(:description)
end
def time; Time.at(read_attribute(:time)) end
def changetime; Time.at(read_attribute(:changetime)) end
end
Creating an issue and defining custom fields for the issue works. However, populating the custom fields doesn't seem to work. There are 4 custom fields (Contact, Test status, Source and Resolution).
The custom fields are created like this:
repf = IssueCustomField.find_by_name("Contact")
repf ||= IssueCustomField.create(:name => "Contact", :field_format => 'string') if repf.nil?
repf.trackers = Tracker.find(:all)
repf.projects << product_map.values
repf.save!
The values for these fields are passed like this:
i = Issue.new :project => product_map[first_change.product_id],
...
:custom_field_values => {:Contact => issue.contact, 'Test status' => '', :Source => '', :Resolution => ''}
I've also tried a version with an index as hash key:
:custom_field_values => {'1' => issue.contact, 'Test status' => '', :Source => '', :Resolution => ''}
The issue can be saved without an issue, however, no value is ever passed over to Redmine. A
mysql> select count(*) from custom_values where value is not null;
+----------+
| count(*) |
+----------+
| 0 |
+----------+
1 row in set (0.01 sec)
shows that all values for the custom fields are NULL after the migration. I don't seem to be able to find how this is done correctly, the documentation for the Redmine classes is very sparse.
I spent much time to solve near same issue. Take a look on my code written to transfer data from old system to new via Redmine REST API. Cause I used ActiveResource code will be usable for you.
def update_custom_fields(issue, fields)
f_id = Hash.new { |hash, key| hash[key] = nil }
issue.available_custom_fields.each_with_index.map { |f,indx| f_id[f.name] = f.id }
field_list = []
fields.each do |name, value|
field_id = f_id[name].to_s
field_list << Hash[field_id, value]
end
issue.custom_field_values = field_list.reduce({},:merge)
raise issue.errors.full_messages.join(', ') unless issue.save
end
Now you can just call update_custom_fields(Issue.last, "MyField" => "MyValue" .. and so on)
I have such index.rabl:
collection #exchangers, :root => "bank", :object_root => false
extends "exchanger_lists/show"
and such show.rabl:
object #exchanger
attributes :id, :name, :address, :location_id, :latitude, :longitude, :exchanger_type_id
node(:location_name) {|exchanger_list| exchanger_list.location.name }
node(:exchanger_type_name) {"normal" }
child #currencies do
attribute :value, :direction_of_exchange_id, :exchanger_list_id
end
my contoller is such:
def index
#exchangers = ExchangerList.all
end
def show
#exchanger = ExchangerList.find(params[:id])
#currency_list = CurrencyList.all
#currencies = []
#currency_list.each do |c|
#currencies << CurrencyValue.find(:all, :conditions => {:currency_list_id => c.id, :exchanger_list_id => #exchanger.id}, :order => :updated_at).last(2)
end
#currencies.flatten!
end
if i call in browser show method, i see child #currencies and it's data, but if i call index i see all (also i see nodes) but child i didn't see.... What's wrong? what i do bad?
Your architecture is a little bit messed up because in the show action you not only display an #exchanger but also the complete list of #currencies being nil when you render show in the index template. In general I would suggest you to think about the whole app architecture.
When I should give you a simple solution for you current problem I would extract the #currencies code from the show action into helper method in app/helpers/currencies_helper.rb and access it from the show template.
module CurrenciesHelper
def currencies(exchanger)
currencies = CurrencyList.all.map do |c|
CurrencyValue.find(:all, :conditions => {:currency_list_id => c.id, :exchanger_list_id => exchanger.id}, :order => :updated_at).last(2)
end
currencies.flatten!
end
end
By the way I replaced the each method with map because it suits better in this case.
Change the currencies part in the show template to
child currencies(#exchanger) do
attribute :value, :direction_of_exchange_id, :exchanger_list_id
end
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'm learning new tricks all the time and I'm always on the lookout for better ideas.
I have this rather ugly method. How would you clean it up?
def self.likesit(user_id, params)
game_id = params[:game_id]
videolink_id = params[:videolink_id]
like_type = params[:like_type]
return false if like_type.nil?
if like_type == "videolink"
liked = Like.where(:user_id => user_id, :likeable_id => videolink_id, :likeable_type => "Videolink").first unless videolink_id.nil?
elsif like_type == "game"
liked = Like.where(:user_id => user_id, :likeable_id => game_id, :likeable_type => "Game").first unless game_id.nil?
end
if liked.present?
liked.amount = 1
liked.save
return true
else # not voted on before...create Like record
if like_type == "videolink"
Like.create(:user_id => user_id, :likeable_id => videolink_id, :likeable_type => "Videolink", :amount => 1)
elsif like_type == "game"
Like.create(:user_id => user_id, :likeable_id => game_id, :likeable_type => "Game", :amount => 1)
end
return true
end
return false
end
I would do something like:
class User < ActiveRecord::Base
has_many :likes, :dependent => :destroy
def likes_the(obj)
like = likes.find_or_initialize_by_likeable_type_and_likeable_id(obj.class.name, obj.id)
like.amount += 1
like.save
end
end
User.first.likes_the(VideoLink.first)
First, I think its wrong to deal with the "params" hash on the model level. To me its a red flag when you pass the entire params hash to a model. Thats in the scope of your controllers, your models should have no knowledge of the structure of your params hash, imo.
Second, I think its always cleaner to use objects when possible instead of class methods. What you are doing deals with an object, no reason to perform this on the class level. And finding the objects should be trivial in your controllers. After all this is the purpose of the controllers. To glue everything together.
Finally, eliminate all of the "return false" and "return true" madness. The save method takes care of that. The last "return false" in your method will never be called, because the if else clause above prevents it. In my opinion you should rarely be calling "return" in ruby, since ruby always returns the last evaluated line. In only use return if its at the very top of the method to handle an exception.
Hope this helps.
I'm not sure what the rest of your code looks like but you might consider this as a replacement:
def self.likesit(user_id, params)
return false unless params[:like_type]
query = {:user_id => user_id,
:likeable_id => eval("params[:#{params[:like_type]}_id]"),
:likeable_type => params[:like_type].capitalize}
if (liked = Like.where(query).first).present?
liked.amount = 1
liked.save
else # not voted on before...create Like record
Like.create(query.merge({:amount => 1}))
end
end
I assume liked.save and Like.create return true if they are succesful, otherwise nil is returned. And what about the unless game_id.nil? ? Do you really need that? If it's nil, it's nil and saved as nil. But you might as well check in your data model for nil's. (validations or something)
I have a model with an after_create callback. This callback causes a new record to be created in another model. However if a validation fails in the child record creation, the original transaction is still being saved.
This doesn't seem right. According to Rails docs the whole thing is wrapped in a transaction. Am I doing something wrong?
class ServiceProvision < ActiveRecord::Base
has_one :cash_receipt
after_create :receive_payment_for_service_provision, :if => Proc.new { |sp| sp.immediate_settlement == true }
private
def receive_payment_for_service_provision
cash_account = CashAccount.find_by_currency_id_and_institution_id( self.currency_id, self.institution_id )
CashReceipt.create( :account_id => account.id, :service_provision_id => self.id, :amount => self.amount, :currency_id => self.currency.id, :cash_account_id => ( cash_account ? cash_account.id : nil ) )
end
end
class CashReceipt < ActiveRecord::Base
belongs_to :service_provision
validates_presence_of :cash_account_id
end
The CashReceipt does fail and returns an error when its passed nil for the cash_account_id, however my new ServiceProvision object is still being saved.
it "should fail if a cash account doesn't exist for the currency and institution" do
currency = Factory.create( :currency )
institution = Factory.create( :institution )
service_provision = Factory.build( :service_provision, :currency_id => currency.id, :institution_id => institution.id, :immediate_settlement => true )
service_provision.save.should == false
service_provision.should have( 1 ).error
end
'ServiceProvision service provision creation should raise an error if a cash account doesn't exist for the currency and institution' FAILED expected: false,
got: true (using ==)
This seems to contradict this from the docs
Both Base#save and Base#destroy come
wrapped in a transaction that ensures
that whatever you do in validations or
callbacks will happen under the
protected cover of a transaction. So
you can use validations to check for
values that the transaction depends on
or you can raise exceptions in the
callbacks to rollback, including
after_* callbacks.
And if I manually try to cancel the transaction in the callback like so:
cr = CashReceipt.create( :account_id => account.id, :service_provision_id => self.id, :amount => self.amount, :currency_id => self.currency.id, :cash_account_id => ( cash_account ? cash_account.id : nil ) )
unless cr.errors.empty?
errors.add_to_base("Error while creating CashReciept [#{cr.errors}].")
return false
end
then the new ServiceProvision object is still saved.
Move the CacheReceipt creation to before_validation filter. Since you have a has_one association on
ServiceProvision, the CacheReceipt object will have the correct :service_provision_id after save. Your code will be as follows:
before_validation :receive_payment_for_service_provision, :if => :immediate_settlement?
def receive_payment_for_service_provision
cash_account = CashAccount.find_by_currency_id_and_institution_id( self.currency_id, self.institution_id )
self.cash_receipt.build(:account_id => account.id,
:amount => self.amount,
:currency_id => self.currency.id,
:cash_account_id => ( cash_account ? cash_account.id : nil ) )
end
Now the save on ServiceProvision instance will return false if there are errors while saving the associated CacheReceipt.
Rollbacks only happen automatically with before callbacks:
The whole callback chain is wrapped in a transaction. If any before callback method returns exactly false or raises an exception the execution chain gets halted and a ROLLBACK is issued. After callbacks can only accomplish that by raising an exception.
This makes sense because it allows for AR to prime the model and save it in memory before applying the transaction. Since you've done an after it has no knowledge of what to rollback too. Why not try before_save and see what you get.
You have to check the execution status of CashReceipt.create call in receive_payment_for_service_proviion method.
def receive_payment_for_service_provision
cash_account = CashAccount.find_by_currency_id_and_institution_id( self.currency_id, self.institution_id )
cr = CashReceipt.create( :account_id => account.id, :service_provision_id => self.id, :amount => self.amount, :currency_id => self.currency.id, :cash_account_id => ( cash_account ? cash_account.id : nil ) )
unless cr.errors.empty?
# Make the ServiceProvision instance invalid
errors.add_to_base("Error while creating CashReciept [#{cr.errors}].")
return false # terminate the callback chain and roll back the TX immediately.
end
end
PS: You can simplify your after_create specification as follows:
after_create :receive_payment_for_service_provision, :if => :immediate_settlement?
Thanks to #KandadaBoggu, who led me to the solution...
Turns out the solution is to change the callback to before_create, and then do this:
def receive_payment_for_service_provision
cash_account = CashAccount.find_by_currency_id_and_institution_id( self.currency_id, self.institution_id )
cr = self.create_cash_receipt( :account_id => account.id,
:amount => self.amount,
:currency_id => self.currency.id,
:cash_account_id => ( cash_account ? cash_account.id : nil ) )
unless cr.errors.empty?
errors.add_to_base( "Error while creating CashReciept [#{cr.errors}]." )
return false
end
end
In other words, we still need to manually check for validation errors in the association.