I'm trying not to fight the defaults here and use Rails built-in support for nested attributes (from http://ryandaigle.com/articles/2009/2/1/what-s-new-in-edge-rails-nested-attributes). I'm labeling Things with Tags, and all works swell, and for each Thing I have a form with a nested field that creates a new Tag by a name. Trouble is, I need to make sure that each Tag has a unique name. Instead of creating a new Tag, if a user enters the name of one that already exists, I need to create the associate with that pre-existing Tag. How do I do this?
There's probably a better way to do this but this is about the best I can come up with for now.
In a has_many(:through) association accepts_nested_arguments_for uses an assignment to the virtual attribute #{association}_attributes to work its magic. It expects an array of hashes, where each hash contains attribute keys and their values. Any hashes with an id will be updated (or deleted if there is a :_delete key with the value of true). Any hashes missing an id will be used to create new items of that association. So the key is to intercept the call to tags_associations= and check any of the hashes that are missing ids for an existing tag with the same name value, and replace it with an entry that tags_attributes will use to make the association to the existing tag. N.B. for has_one and belongs_to relationships tag_attributes will expect a single hash. The code will be similar, but much simpler.
class Thing < ActiveRecord::Base
has_many :tags, :through => :taggings
has_many :taggings
accepts_nested_attributes_for :tags
def tags_attributes_with_recycling=(attributes)
existing_attributes = attributes.reject{|attribute| attribute[:id].nil?}
new_attributes = attributes - existing_attributes
new_and_recycled_attributes = new_attributes.map { |attribute|
tag_id = Tag.find_by_name(attribute[:name]).id
tag_id ? {:id => tag_id) : attribute
}
tags_attributes_without_recycling= (existing_attributes + new_and_recycled_attributes)
end
alias_method_chain :tags_attributes=, :recycling
end
It's untested, so no guarantees. But it should at least put you on track for a solution.
In your Thing class, define a tags_attributes= method to override the default and use Tag.find_or_create
def tags_attributes=(attributes)
attributes.each do |attr|
Tag.find_or_create_by_name(attr[:name])
end
end
Not sure what the attributes hash will look like but you get the idea.
Related
Context:
Each Order has many Items & Logistics. Each Item & Logistic (as well as the Order itself) have many Revenues.
I am creating Order + Items & Logistics at once using an accepts_nested_attributes_for on Order. However, Revenues gets created using an after_create callback on each of the models Order, Item, and Logistics. Why? Because given the difference in interpretation in these models, the code reads cleaner this way. (But if this way of doing it is what's causing this question to be asked, I will obviously reconsider!)
One key attribute that I need to store in Revenues is pp_charge_id. But pp_charge_id is not something that either Order, Items, or Logistics needs to worry about. I've attached an attr_accessor :pp_charge_id to Order, so that one works fine, however, once I'm in the child Items or Logistics models, I no longer have access to pp_charge_id which again I need to save an associated Revenue. How should I do this?
Controller Code:
#order = Order.new(params) #params includes Order params, and nested params for child Item & Logistics
#order.pp_charge_id = "cash"
#order.save #I need this to not only save the Order, the children Item & Logistics, but then to also create the associated Revenue for each of the aforementioned 3 models
ORDER Model Code:
has_many :items
has_many :revenues
attr_accessor :pp_charge_id
after_create :create_revenue
def create_revenue
self.revenues.create(pp_charge_id: self.pp_charge_id)
end
#This WORKS as expected because of the attr_accessor
ITEM/ LOGISTIC model code:
has_many :revenues
belongs_to :order
after_create :create_revenue
def create_revenue
self.revenues.create(pp_charge_id: self.order.pp_charge_id)
end
#This DOES NOT work because self.order.pp_charge_id is nil
ORDER model code:
belongs_to :order
belongs_to :item
belongs_to :logistic
Again I understand the attr_accessor is not designed to persist across a request or even if the Order itself is reloaded. But it also doesn't make sense to save it redundantly in a table that has no use for it. If the only way to do this is to put the pp_charge_id into the params for the order and save everything all at once (including Revenues), then let me know because I know how to do that. (Again, would just rather avoid that because of how it's interpreted: params are coming from User, Revenue data is something I'm providing)
I think if you want the order's pp_charge_id to apply to all its items and logistics, I'd put all that into the order's after_create callback:
# order.rb
def create_revenue
revenues.create(pp_charge_id: pp_charge_id)
items.each {|i| i.revenues.create(pp_charge_id: pp_charge_id)}
logistics.each {|l| l.revenues.create(pp_charge_id: pp_charge_id)}
end
EDIT: Alternately, you could add inverse_of to your belongs_to declarations, and then I believe Item#create_revenue would see the same Order instance that you set in the controller. So if you also added an attr_accessor to the Item class, you could write its create_revenue like this:
# item.rb
def create_revenue
revenues.create(pp_charge_id: pp_charge_id || order.pp_charge_id)
end
This should cover the new requirement you've mentioned in your comment.
instead of using after_create and accessors you should consider having a proper method that does exactly what you need, ie:
Order.create_with_charge(:cash, params)
i find it disturbing to persist redundant information in the database just because the code reads cleaner that way!
If I have, for example, a field author_id in my Book entity, and I add belongs_to :author, then the method author is created to access the object to which this id refers.
I can also use the following line to be a little more specific:
belongs_to :author, :primary_key => 'p_code', :foreign_key => 'writer_code'
And it still creates the author method. What I need is, having the writer_code field name, I need to get the generated author method name, or at leas the entity name provided. I'm sure Rails should store all these relations in a table somewhere, I just need to access it and find it out. Any ideas?
ActiveRecord determines most information dynamically; it does not store stuff in the DB.
You can inspect most information by using ActiveRecord::Reflection
For example you can try:
Book.reflect_on_association(:author)
This will return you an instance of ActiveRecord::Reflection::AssociationReflection which encapsulates all information AR stores for this association, including an #options hash with keys such as :foreign_key and :class_name (if custom values have been provided).
If you don't know the name of the association you can reflect over all associations with the reflect_on_all_associations method. From there it should be a straight-forward collection and hash traversal: iterate through them and check for the foreign_id which you already know.
When you have a rails resource defined rails seems to automatically create a params entry of attributes for that resource. e.g. if my model Lesson has a subject attribute and I post subject=Maths it automatically creates the param[lesson] = { subject: 'Hello' }. The problem I am having is getting nested attributes to appear within this created lesson array.
I'm using mongoid as my backend and have an association on Lesson called activities. The code looks like this:
class Lesson
include Mongoid::Document
field :subject, type: String
embeds_many :activities, class_name: 'LessonActivity' do
def ordered
#target.sort { |x, y| x.display_order <=> y.display_order }
end
def reorder!
#target.each_with_index { |val, index| val.display_order = index }
end
end
accepts_nested_attributes_for :activities
However I can't work out how I access this activities from within params.require(:lesson).permit :activities
I can access it via params.permit(:activities) but that feels a bit messy
I've done some digging and found out what's going on with this.
It all comes from a rails feature, the Param wrapper, details and api. Which configured for json will automatically pass the attributes of the model into a param of the model name (in this case Lesson).
The attributes of the model that will be populated based on how the model responds to the method attribute_names so this gives two routes to achieve the aims of the question.
1 - Instruct my controller to include activities as part of Lesson parameters, e.g. using this method:
class Api::LessonsController < Api::ApiController
wrap_parameters Lesson, include: Lesson.attribute_names << :activities
2 - Update the attiribute_names method for the model to include :activities
I'm still left with a couple of things to resolve, namely the reason associations aren't part of attribute_names on Mongoid and if overriding it to include attribute names is a bad idea.
Basing on the params you provided for your JSON POST request, you will need the following code to whitelist the params you need:
def activities_params
params.require(:activities).permit(:title, :display_order, :content, :time)
end
The params forwarded by your JSON POST request did not have the :activities hash as a value to the :lesson key so whitelisting the params you need is simple like above.
I think you may have answered you question here:
"how I can make it part of lessons key or why I can't. I'm not passing a lesson parameter "
If I read that correctly, you are not passing the lesson param, just a hash of Activities?
That would explain why you can access
params.permit(:activities)
but not
params.require(:lesson).permit :activities
I'm having trouble with a model not honoring the :foreign_key policy.
Character model has the following fields:
name:string
level:int
realm:string
realm_id:integer
class Character < ActiveRecord::Base
belongs_to :realm
end
My Realms model looks like this:
class Realm < ActiveRecord::Base
has_many :characters, :foreign_key => "realm_id"
end
However, it seems like it's forcing the character model to use the :realm column as the foreign_key rather than :realm_id. I don't have any clue as to why or how to fix it. Is there any other way to make it ignore the :realm field and go for the :realm_id without having to change the name of the column?
[Edit for clarity]
The character model does have a realm_id:integer field. I have tried not having the foreign_key but the results with both is identical.
ruby-1.9.2-p136 :012 > c = Character.new
=> #
ruby-1.9.2-p136 :013 > c.realm = "Sargeras"
ActiveRecord::AssociationTypeMismatch: Realm(#2154038240) expected, got String(#2151988680)
Despite even having the foreign_key, it just refuses to let go of the realm column.
[Edit 2]
The realm column will just take over due to the has_many and belongs_to association. There is no way so far to break this, so the solution is to either remove the column (the approach i will take), or rename it to something different.
Did you make sure that your Character table has a realm_id column? Please make sure of that, and then get rid of foreign_key => 'realm_id, it is not necessary at all. Your program should work if you get both these things done.
You should not need the :foreign_key part here at all, since you're following the standard Rails naming convention, the realm_id column should be inferred from the model name.
EDIT
I see. I don't think you can have a column and an association by the same name in one model. The easiest solution would probably be to rename the "realm" column to "realmname" or something like that.
belongs_to :realm creates (among other things) methods called realm and realm= used as getters and setters. That means the method_missing magic that ActiveRecord uses to expose database columns as attributes is never triggered when you do Character#realm, as that method isn't in fact missing.
If you want to avoid renaming the realm column in your database, you could create attribute accessors for the field manually under another name:
class Character < ActiveRecord::Base
def realm_name
self['realm']
end
def realm_name=(value)
self['realm'] = value
end
end
This way you'll still have the realm column in your database and be able to access the attribute in Ruby, albeit under a different name. This isn't a great idea, though, as you'll be duplicating the realm name as both Character#realm_name and Character.realm.name.
I would ditch the realm column and instead make sure I use Realm objects when importing from the datasource:
character.realm = Realm.find_by_name('Sargeras')
That way you'd only realm data available where it makes sense; in the Realm model.
I have a parent object, Post, which has the following children.
has_one :link
has_one :picture
has_one :code
These children are mutually exclusive.
Is there a way to use polymorphic associations in reverse so that I don't have to have link_id, picture_id, and code_id fields in my Post table?
I wrote up a small Gist showing how to do this:
https://gist.github.com/1242485
I believe you are looking for the :as option for has_one. It allows you to specify the name of the belongs_to association end.
When all else fails, read the docs: http://apidock.com/rails/ActiveRecord/Associations/ClassMethods/has_one
Is there a way to use polymorphic
associations in reverse so that I
don't have to have link_id,
picture_id, and code_id fields in my
Post table?
has_one implies that the foreign key is in the other table. If you've really defined your model this way, then you won't have link_id, picture_id, and code_id in your Post table. I think you meant to say belongs_to.
I want to do something like
#post.postable and get the child
object, which would be one of link,
picture, or code.
I believe you could do this by using STI and combining the links, pictures, and codes tables, then testing the type of the model when retrieving. That seems kludgey though, and could end up with lots of unused columns.
Is there a reason for not storing the unused id columns, other than saving space? If you're willing to keep them, then you could define a virtual attribute and a postable_type column : (untested code, may fail spectacularly)
def postable
self.send(self.postable_type)
end
def postable=(p)
self.send(postable_type.to_s+"=",p)
end