Rails 3: include a field from has_many through association - ruby-on-rails

My model association is as follows:
#book model
class Book < ActiveRecord::Base
has_many :recommendations, :dependent => :destroy
has_many :similars, :through => :recommendations, :conditions => ['recommendation_type IS NULL'], :order => 'recommendations.created_at DESC'
#recommendation model
class Recommendation < ActiveRecord::Base
belongs_to :book
belongs_to :similar, :class_name => 'Book', :foreign_key => 'similar_id'
#Books_controller - injecting the recommendation_id
#book = Book.find(params[:id])
if params[:content_type]
#content_type = params[:content_type];
else
#content_type = "similars"
end
case #content_type
when "similars"
# get the similars
#book_content = #book.similars
#book_content.each do |similar|
#rec_id = Recommendation.where(:book_id=>similar.id, :recommendation_type=>'S').select('id').first.id
similar << {:rec_id => #rec_id}
# ^-- Above line gives NoMethodError (undefined method `<<' for #<Book:0x10de1f40>):
end
when "references"
# get the references
#book_content = #book.references
#book_content.each do |reference|
#rec_id = Recommendation.where(:book_id=>reference.id, :recommendation_type=>'R').select('id').first.id
reference << {:rec_id => #rec_id}
# ^-- Above line gives NoMethodError (undefined method `<<' for #<Book:0x10de1f40>):
end
end
So as noted above, A book has many similars through recommendations. My requirement is that while retrieving similars, I would also like to include the id of the corresponding record in the join table recommendations.
My questions are:
How can I include the field *recommendation_id* alongwith
similars?
If it cannot be included directly, then what is the correct way to
determine this field separately (as shown above) and then
inject it into the similars instance variable so that I can use
it directly in my views?

I recommend you read the Rails guide on associations, specifically about the has_many :through associations.
A lot of your code doesn't make sense - for example:
#book_similars = Book.similars
This means you have a class method on the Book model for similars, but you don't mention it being defined, or what it returns. Rails just doesn't work like this.

Related

Add record to a model upon create used in many models

I have a survey and I would like to add participants to a Participant model whenever a user answers to a question for the first time. The survey is a bit special because it has many functions to answer questions such as Tag words, Multiple choices and Open Question and each function is actually a model that has its own records. Also I only want the Participant to be saved once.
The Participant model is fairly simple:
class Participant < ActiveRecord::Base
belongs_to :survey
attr_accessible :survey_id, :user_id
end
The Survey model is also straightforward:
class Survey < ActiveRecord::Base
...
has_many :participants, :through => :users
has_many :rating_questions, :dependent => :destroy
has_many :open_questions, :dependent => :destroy
has_many :tag_questions, :dependent => :destroy
belongs_to :account
belongs_to :user
accepts_nested_attributes_for :open_questions
accepts_nested_attributes_for :rating_questions
accepts_nested_attributes_for :tag_questions
...
end
Then you have models such as rating_answers that belong to a rating_question, open_answers that belong to open_questions and so on.
So initially I thought for within my model rating_answers I could add after_create callback to add_participant
like this:
class RatingAnswer < ActiveRecord::Base
belongs_to :rating_question
after_create :add_participant
...
protected
def add_participant
#participant = Participant.where(:user_id => current_user.id, :survey_id => Survey.find(params[:survey_id]))
if #participant.nil?
Participant.create!(:user_id => current_user.id, :survey_id => Survey.find(params[:survey_id]))
end
end
end
In this case, I didn't know how to find the survey_id, so I tried using the params but I don't think that is the right way to do it. regardles it returned this error
NameError (undefined local variable or method `current_user' for #<RatingAnswer:0x0000010325ef00>):
app/models/rating_answer.rb:25:in `add_participant'
app/controllers/rating_answers_controller.rb:12:in `create'
Another idea I had was to create instead a module Participants.rb that I could use in each controllers
module Participants
def add_participant
#participant = Participant.where(:user_id => current_user.id, :survey_id => Survey.find(params[:survey_id]))
if #participant.nil?
Participant.create!(:user_id => current_user.id, :survey_id => Survey.find(params[:survey_id]))
end
end
end
and in the controller
class RatingAnswersController < ApplicationController
include Participants
def create
#rating_question = RatingQuestion.find_by_id(params[:rating_question_id])
#rating_answer = RatingAnswer.new(params[:rating_answer])
#survey = Survey.find(params[:survey_id])
if #rating_answer.save
add_participant
respond_to do |format|
format.js
end
end
end
end
And I got a routing error
ActionController::RoutingError (uninitialized constant RatingAnswersController::Participants):
I can understand this error, because I don't have a controller for participants with a create method and its routes resources
I am not sure what is the proper way to add a record to a model from a nested model and what is the cleaner approach.
Ideas are most welcome!
current_user is a helper that's accessible in views/controller alone. You need to pass it as a parameter into the model. Else, it ain't accessible in the models. May be, this should help.
In the end I ended up using the after_create callback but instead of fetching the data from the params, I used the associations. Also if #participant.nil? didn't work for some reason.
class RatingAnswer < ActiveRecord::Base
belongs_to :rating_question
after_create :add_participant
...
protected
def add_participant
#participant = Participant.where(:user_id => self.user.id, :survey_id => self.rating_question.survey.id)
unless #participant.any?
#new_participant = Participant.create(:user_id => self.user.id, :survey_id => self.survey.rating_question.id)
end
end
end
The cool thing with associations is if you have deeply nested associations for instead
Survey has_many questions
Question has_many answers
Answer has_many responses
in order to fetch the survey id from within the responses model you can do
self.answer.question.survey.id
very nifty!

Using ActiveRelation with :joins on polymorphic has_one to implicitly create relation

We can use ActiveRelation like this:
MyModel.where(:field => "test").create => #<Message ... field:"test">
But it doesnt work for joins with polymorphic has_one associations:
class RelatedModel < AR::Base
# has :some_field
belongs_to :subject, :polymorphic => true
end
class MyModel < AR::Base
# need some dirty magic here
# to build default related_model with params from active_relation
has_one :related_model, :as => :subject, :dependent => :destroy
end
describe MyModel do
it "should auto-create has_one association with joins" do
test = MyModel.joins(:related_model).where("related_models.subject_type" => "MyModel", "related_models.some_field" => "chachacha").create
test.related_model.should_not be_nil
test.related_model.some_field.should == "chachacha"
test.related_model.subject_type.should == "MyModel"
test.related_model.subject_id.should == test.id
# fails =)
end
end
Is it possible to extract active_relation params, pass them to MyModel for use in before_create and build RelatedModel with them?
Diving into ActiveRecord sources i found that
ActiveRecord::Relation covers 'create' with 'scoping' method.
ActiveRecord::Persistance 'create' calls 'initialize' from ActiveRecord::Core.
ActiveRecord::Core 'initialize' calls 'populate_with_current_scope_attributes'
This method declared in ActiveRecord::Scoping uses 'scope_attributes' declared in ActiveRecord::Scoping::Named.
scope_attributes creating relation 'all' and calls 'scope_for_create' on it.
'ActiveRecord::Relation's 'scope_for_create' uses only 'where_values_hash' from current_scope that does not contain rules like 'related_models.subject_type' (this values are contained in where_clauses). So we need to have simple key-value wheres to be used with 'create' on ActiveRecord::Relation. But ActiveRecord not clever enough to know that 'some_field' in where clause should be used with join table.
I found it can be implemented only by accessing where options with self.class.current_scope.where_clauses in 'before_create' on MyModel, parsing them and setting up attributes.
class MyModel < AR::Base
before_create :create_default_node
def create_default_node
clause = self.class.current_scope.where_clauses.detect{|clause| clause =~ /\`related_models\`.\`some_field\`/}
value = clause.scan(/\=.+\`([[:word:]]+)\`/).flatten.first
self.create_node(:some_field => value)
end
end
But it is so dirty, then i decided to find simpler solution and inverted dependency as described in Railscast Pro #394, moved RelatedModel functionality to MyModel with STI. Actually i needed such complicated relation creation because RelatedModel had some functionality common for all models (acts as tree). I decided to delegate 'ancestors' and 'children' to RelatedModel. Inverting dependency solved this problem.
class MyModel < AR::Base
acts_as_tree
belongs_to :subject, :polymorphic => true
end
class MyModel2 < MyModel
end
class RelatedModel < AR::Base
# has :some_field
has_one :my_model, :as => :subject, :dependent => :destroy
end
MyModel.create{|m| m.subject = RelatedModel.create(:some_field => "chachacha")}
MyModel.ancestors # no need to proxy relations

ActiveRecord::HasManyThroughAssociationNotFoundError problem, with a twist

I have the following three models
LegacyRole:
class LegacyRole < LegacyModel
has_many :permissions_roles
has_many :permissions, :through => :permissions_roles
end
LegacyPermissionsRole:
class LegacyPermissionsRole < LegacyModel
belongs_to :role
belongs_to :permission
end
and LegacyPermission:
class LegacyPermission < LegacyModel
has_many :permissions_roles
has_many :roles, :through => :permissions_roles
end
And in order for these to all work, and connect the legacy database and whatnot, I have the following class LegacyModel which is possibly trying to be too clever...
require 'active_record'
class LegacyModel < ActiveRecord::Base
self.abstract_class = true
establish_connection "legacy_#{::Rails.env}"
def self.inherited(subclass)
tabeleized_name = subclass.name.tableize
raise "Legacy models must be prefixed with 'Legacy'" unless tabeleized_name.start_with?('legacy_')
logger.info "***********LOAD***********"
logger.info "Loaded legacy model: #{subclass.name} using table: #{tabeleized_name.gsub('legacy_', '')}"
super
subclass.set_table_name tabeleized_name.gsub('legacy_','')
end
# these methods do much the same thing, can probably abstract some of this out
def self.belongs_to(association_id, options = {})
new_association = association_id.to_s.insert(0, 'legacy_').to_sym
old_association = association_id
logger.info "Legacy model has belongs_to association: '#{association_id}'"
association_id = association_id.to_s.insert(0, 'legacy_').to_sym
logger.info "Converting association to: '#{association_id}'"
unless options.has_key?(:foreign_key)
# our foreign_key is missing
options[:foreign_key] = old_association.to_s.foreign_key
logger.info("Foreign_key was missing, is now: #{options[:foreign_key]}")
end
super
alias_method old_association, new_association
end
def self.has_many(association_id, options = {})
new_association = association_id.to_s.insert(0, 'legacy_').to_sym
old_association = association_id
logger.info "Legacy model has_many to association: '#{association_id}'"
association_id = association_id.to_s.insert(0, 'legacy_').to_sym
logger.info "Converting association to: '#{association_id}'"
logger.debug("Association options are: #{options.inspect}")
if options.has_key?(:through)
options[:through] = options[:through].to_s.insert(0, 'legacy_')
logger.info("Through mutated, is now: #{options[:through]}")
end
super
alias_method old_association, new_association
end
end
Whenever I try to access permissions on an instance of LegacyRole, I get the following Active Record error:
ActiveRecord::HasManyThroughAssociationNotFoundError: Could not find the association "legacy_permissions_roles" in model LegacyRole
I've stepped through all this as best I can and I really can't figure out why this is occurring, obviously with this being a bit more complicated than standard by the LegacyModel class I really don't know how to diagnose this further... I'm now at the point with it where I can't see the forest for the trees and feel it might just be something really simple that I've missed out!
Edit:
Here is the log output from the models loading
****************************
Loaded legacy model: LegacyPermission using table: permissions
Legacy model has_many association: 'permissions_roles'
Converting association to: 'legacy_permissions_roles'
Association options are: {}
Legacy model has_many association: 'roles'
Converting association to: 'legacy_roles'
Association options are: {:through=>:permissions_roles}
Changed :through to: 'legacy_permissions_roles'
****************************
Loaded legacy model: LegacyPermissionsRole using table: permissions_roles
Legacy model has belongs_to association: 'role'
Converting association to: 'legacy_role'
Legacy model has belongs_to association: 'permission'
Converting association to: 'legacy_permission'
Foreign_key was missing, is now: 'permission_id'
****************************
Loaded legacy model: LegacyRole using table: roles
Legacy model has_many association: 'permissions_roles'
Converting association to: 'legacy_permissions_roles'
Association options are: {}
Legacy model has_many association: 'permissions'
Converting association to: 'legacy_permissions'
Association options are: {:through=>:permissions_roles}
Changed :through to: 'legacy_permissions_roles'
Perhaps you want
class LegacyRole < LegacyModel
has_many :permissions_roles
has_many :permissions, :through => :legacy_permissions_roles # note the `legacy` prefix
end
Or was this a typo in your post?

Rails: Overriding ActiveRecord association method

Is there a way to override one of the methods provided by an ActiveRecord association?
Say for example I have the following typical polymorphic has_many :through association:
class Story < ActiveRecord::Base
has_many :taggings, :as => :taggable
has_many :tags, :through => :taggings, :order => :name
end
class Tag < ActiveRecord::Base
has_many :taggings, :dependent => :destroy
has_many :stories, :through => :taggings, :source => :taggable, :source_type => "Story"
end
As you probably know this adds a whole slew of associated methods to the Story model like tags, tags<<, tags=, tags.empty?, etc.
How do I go about overriding one of these methods? Specifically the tags<< method. It's pretty easy to override a normal class methods but I can't seem to find any information on how to override association methods. Doing something like
def tags<< *new_tags
#do stuff
end
produces a syntax error when it's called so it's obviously not that simple.
You can use block with has_many to extend your association with methods. See comment "Use a block to extend your associations" here.
Overriding existing methods also works, don't know whether it is a good idea however.
has_many :tags, :through => :taggings, :order => :name do
def << (value)
"overriden" #your code here
super value
end
end
If you want to access the model itself in Rails 3.2 you should use proxy_association.owner
Example:
class Author < ActiveRecord::Base
has_many :books do
def << (book)
proxy_association.owner.add_book(book)
end
end
def add_book (book)
# do your thing here.
end
end
See documentation
I think you wanted def tags.<<(*new_tags) for the signature, which should work, or the following which is equivalent and a bit cleaner if you need to override multiple methods.
class << tags
def <<(*new_tags)
# rawr!
end
end
You would have to define the tags method to return an object which has a << method.
You could do it like this, but I really wouldn't recommend it. You'd be much better off just adding a method to your model that does what you want than trying to replace something ActiveRecord uses.
This essentially runs the default tags method adds a << method to the resulting object and returns that object. This may be a bit resource intensive because it creates a new method every time you run it
def tags_with_append
collection = tags_without_append
def collection.<< (*arguments)
...
end
collection
end
# defines the method 'tags' by aliasing 'tags_with_append'
alias_method_chain :tags, :append
The method I use is to extend the association. You can see the way I handle 'quantity' attributes here: https://gist.github.com/1399762
It basically allows you to just do
has_many : tags, :through => : taggings, extend => QuantityAssociation
Without knowing exactly what your hoping to achieve by overriding the methods its difficult to know if you could do the same.
This may not be helpful in your case but could be useful for others looking into this.
Association Callbacks:
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
Example from the docs:
class Project
has_and_belongs_to_many :developers, :after_add => :evaluate_velocity
def evaluate_velocity(developer)
...
end
end
Also see Association Extensions:
class Account < ActiveRecord::Base
has_many :people do
def find_or_create_by_name(name)
first_name, last_name = name.split(" ", 2)
find_or_create_by_first_name_and_last_name(first_name, last_name)
end
end
end
person = Account.first.people.find_or_create_by_name("David Heinemeier Hansson")
person.first_name # => "David"
person.last_name # => "Heinemeier Hansson"
Rails guides documents about overriding the added methods directly.
OP's issue with overriding << probably is the only exception to this, for which follow the top answer. But it wouldn't work for has_one's = assignment method or getter methods.

how to access rails join model attributes when using has_many :through

I have a data model something like this:
# columns include collection_item_id, collection_id, item_id, position, etc
class CollectionItem < ActiveRecord::Base
self.primary_key = 'collection_item_id'
belongs_to :collection
belongs_to :item
end
class Item < ActiveRecord::Base
has_many :collection_items
has_many :collections, :through => :collection_items, :source => :collection
end
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position
has_many :items, :through => :collection_items, :source => :item, :order => :position
end
An Item can appear in multiple collections and also more than once in the same collection at different positions.
I'm trying to create a helper method that creates a menu containing every item in every collection. I want to use the collection_item_id to keep track of the currently selected item between requests, but I can't access any attributes of the join model via the Item class.
def helper_method( collection_id )
colls = Collection.find :all
colls.each do |coll|
coll.items.each do |item|
# !!! FAILS HERE ( undefined method `collection_item_id' )
do_something_with( item.collection_item_id )
end
end
end
I tried this as well but it also fails with ( undefined method `collection_item' )
do_something_with( item.collection_item.collection_item_id )
Edit: thanks to serioys sam for pointing out that the above is obviously wrong
I have also tried to access other attributes in the join model, like this:
do_something_with( item.position )
and:
do_something_with( item.collection_item.position )
Edit: thanks to serioys sam for pointing out that the above is obviously wrong
but they also fail.
Can anyone advise me how to proceed with this?
Edit: -------------------->
I found from online documentation that using has_and_belongs_to_many will attach the join table attributes to the retreived items, but apparently it is deprecated. I haven't tried it yet.
Currently I am working on amending my Collection model like this:
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position, :include => :item
...
end
and changing the helper to use coll.collection_items instead of coll.items
Edit: -------------------->
I've changed my helper to work as above and it works fine - (thankyou sam)
It's made a mess of my code - because of other factors not detailed here - but nothing that an hour or two of re-factoring wont sort out.
In your example you have defined in Item model relationship as has_many for collection_items and collections the generated association method is collection_items and collections respectively both of them returns an array so the way you are trying to access here is wrong. this is primarily case of mant to many relationship. just check this Asscociation Documentation for further reference.
do_something_with( item.collection_item_id )
This fails because item does not have a collection_item_id member.
do_something_with( item.collection_item.collection_item_id )
This fails because item does not have a collection_item member.
Remember that the relation between item and collection_items is a has_many. So item has collection_items, not just a single item. Also, each collection has a list of collection items. What you want to do is probably this:
colls = Collection.find :all
colls.each do |coll|
coll.collection_items.each do |collection_item|
do_something_with( collection_item.id )
end
end
A couple of other pieces of advice:
Have you read the documentation for has_many :through in the Rails Guides? It is pretty good.
You shouldn't need the :source parameters in the has_many declarations, since you have named your models and associations in a sensible way.
I found from online documentation that using has_and_belongs_to_many will attach the join table attributes to the retreived items, but apparently it is deprecated. I haven't tried it yet.
I recommend you stick with has_many :through, because has_and_belongs_to_many is more confusing and doesn't offer any real benefits.
I was able to get this working for one of my models:
class Group < ActiveRecord::Base
has_many :users, :through => :memberships, :source => :user do
def with_join
proxy_target.map do |user|
proxy_owner = proxy_owner()
user.metaclass.send(:define_method, :membership) do
memberships.detect {|_| _.group == proxy_owner}
end
user
end
end
end
end
In your case, something like this should work (haven't tested):
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position
has_many :items, :through => :collection_items, :source => :item, :order => :position do
def with_join
proxy_target.map do |items|
proxy_owner = proxy_owner()
item.metaclass.send(:define_method, :join) do
collection_items.detect {|_| _.collection == proxy_owner}
end
item
end
end
end
end
Now you should be able to access the CollectionItem from an Item as long as you access your items like this (items.with_join):
def helper_method( collection_id )
colls = Collection.find :all
colls.each do |coll|
coll.items.with_join.each do |item|
do_something_with( item.join.collection_item_id )
end
end
end
Here is a more general solution that you can use to add this behavior to any has_many :through association:
http://github.com/TylerRick/has_many_through_with_join_model
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position
has_many :items, :through => :collection_items, :source => :item, :order => :position, :extend => WithJoinModel
end

Resources