So i'm trying to override a relation has_many in a rails 4.2 application.
I've a model Event who has_many :picturables.
The Event can also has_one :contact and this Contact also has_many :picturables.
What I try to achieve is that when we call event.picturables, is returns the picturables for the Event and also those of the Contact.
I've tried to do this with the 'extend association' :
has_many :picturables, ->{ order(:position) }, as: :picturable, class_name: "Picturable", dependent: :delete_all do
def <missing_name>
event = proxy_association.owner
event.contact.present? ? event.picturables + event.contact.picturables : event.picturables
end
def poney
event = proxy_association.owner
event.contact.present? ? event.picturables + event.contact.picturables : event.picturables
end
end
So when I do event.picturables.poney it works like I want.However, I'd like to override the event.picturables directly.
I tried to name the method self or itself but it doesn't work.
Is what i want to achieve possible the way I tried, or do I instead need to declare a 'normal' method?
Can you guys help me?
Thanks in advance
Edit :
The solution i choose :
I wasn't able to find the solution the way i search, so i implemented an other idea :
def picturables
unless contact.nil?
Picturable.where('(picturable_id = ? AND picturable_type = ?) OR (picturable_id = ? AND picturable_type = ?)', id, 'Event', contact.id, 'Contact')
else
super
end
end
def pictures
Picture.where(id: picturables.map(&:picture_id))
end
The advantage of making a 'where' by hand is that we still have a ActiveRecord:RelationProxy as return, because when i made event.picturables + event.contact.picturables it's return an array
Related
So in a rails-api I'm working on, we're currently trying to optimize some of the longer running calls, and I'm having an issue with the .includes functionality. I've got it working in most situations, but there's one particular situation where it's not working in the way that I want it to.
Here's an example:
User class
class User < ActiveRecord::Base
has_many :images
has_one :active_image, -> { where(images: { active_image: true })}, class_name: 'Image'
has_many :facebook_auth
def get_profile_image
if active_image
active_image.image.url(:profile)
else
facebook = facebook_auth.last
if facebook
"https://graph.facebook.com/#{facebook.provider_user_id}/picture?width=150&height=150"
end
end
nil
end
end
Controller:
class UserController < BaseAPIController
def get_user_image
user_id = params[:user_id]
user = User.includes(:active_image, :facebook_auth).find(user_id)
render json: user.get_profile_image
end
end
With this, I would assume that the .includes(:active_image, :facebook_auth) would cache the data so that when I call them in the get_profile_image method, it doesn't make any more db calls, but this isn't the case. What am I doing wrong here?
Thanks,
Charlie
You where almost there!
Try this approach:
class User < ApplicationRecord
has_many :images, dependent: :destroy
has_one :active_image,
-> { where(active: true) },
class_name: 'Image'
has_many :facebook_auths, dependent: :destroy
has_one :active_facebook_auth,
-> { order("created_at desc") },
class_name: 'FacebookAuth'
scope :eager_load_image_data,
-> { includes(:active_image).includes(:active_facebook_auth) }
def profile_image_url
if active_image
active_image.url
elsif active_facebook_auth
"https://graph.facebook.com/#{active_facebook_auth.provider_user_id}/picture?width=150&height=150"
else
nil
end
end
end
Then in your controller or whenever you want to eager load images:
# for one user, with id 2:
User.eager_load_image_data.find(2).profile_image_url
# for a collection (using 'all' here):
User.eager_load_image_data.all.map{ |user|
[user.name, user.profile_image_url]
}
This way the image data is eagerloaded, both from the Image class and the FacebookAuth class.
There where also some other issues in your method User#get_profile_image that I have fixed:
It always returns nil. I am sure in your real code you have early returns.
For collections, it does a N+1 query if looking for facebook_auth_tokens.
Well, I wanted to comment, but couldn't put code into the comments, so I'm giving a non-answer...
I don't see anything obviously wrong, but as a work around, you could do this in User or somewhere:
def self.user_profile_image(user_id)
active_image = Images.where(user_id: user_id).where(active_image: true).first
if active_image
active_image.image.url(:profile)
else
facebook = FaceBookAuth.where(user_id: user_id).last
if facebook
"https://graph.facebook.com/#{facebook.provider_user_id}/picture?width=150&height=150"
end
end
nil
end
And just call/cache the image in your controller, if that's not overly simplistic...
def get_user_image
render json: User.user_profile_image(params[:user_id])
end
That makes at most 2 relatively efficient queries. It doesn't needlessly load user, etc.
Question regarding the paper_trail gem.
When only associations change, a version record won't be created for the main model. So what's the right way to list all versions for a certain record including its associations?
Should I query something like this? (The bad point is this SQL query might be long and low performance.)
f = "(item_type = 'Place' AND item_id = ?) OR (item_type = 'PlaceName' AND item_id IN (?))"
PaperTrail::Version.where(f, #place.id, #place.names.map { |n| n.id })
Or should I create a version record when only associations changed? I think #DavidHam tried the same thing and asked a similar question but nobody has answered it yet.
So, I sort of found a way to do this, but it's not exactly pretty and it doesn't create a new version everytime an association is changed. It does, however, give you an efficient way to retrieve the versions chronologically so you can see what the version looked like before/after association changes.
First, I retrieve all the ids for for the asscoiation versions given the id of that model:
def associations_version_ids(item_id=nil)
if !item_id.nil?
ids = PaperTrail::VersionAssociation.where(foreign_key_id: item_id, foreign_key_name: 'item_id').select(:version_id)
return ids
else
ids = PaperTrail::VersionAssociation.where(foreign_key_name: 'item_id').select(:version_id)
return ids
end
end
Then I get all versions together using the VersionAssociation ids from this function. It will return a chronological array of PaperTrail::Version's. So the information is useful for an activity log, etc. And it's pretty simple to piece back together a version and its associations this way:
def all_versions
if !#item_id.nil?
association_version_ids = associations_version_ids(#item_id)
all_versions = PaperTrail::Version
.where("(item_type = ? AND item_id = ?) OR id IN (?)", 'Item', #item_id, association_version_ids)
.where("object_changes IS NOT NULL")
.order(created_at: :desc)
return all_versions
else
assocation_ids = associations_version_ids
all_versions = PaperTrail::Version
.where("item_type = ? OR id IN (?)", 'Item', association_ids)
.where("object_changes IS NOT NULL")
.order(created_at: :desc)
return all_versions
end
end
Again, not a perfect answer since there isn't a new version everytime there's a change, but it's manageable.
This is more of an approach than a specific answer, but here goes.
In my case, I needed a version history such that any time anyone changed a Child, they also changed a flag on the `Parent'. But I needed a way to show an audit trail that would show the initial values for all the children, and an audit line for the parent whenever anyone changed a child.
So, much simplified, it's like this:
class Parent < ActiveRecord::Base
has_paper_trail
has_many :children
end
class Child < ActiveRecord::Base
has_paper_trail
belongs_to :parent
end
So, whenever there's a change on a Child we need to create a version on the Parent.
First, try changing Child as follows:
class Child < ActiveRecord::Base
has_paper_trail
belongs_to :parent, touch: true
end
This should (should! have not tested) create a timestamp on the Parent whenever the Child changes.
Then, to get the state of the :children at each version of Parent, you search the Child's versions for the one where the transaction_id matches the Parent.
# loop through the parent versions
#parent.versions.each do |version|
#parent.children.versions.each do |child|
# Then loop through the children and get the version of the Child where the transaction_id matches the given Parent version
child_version = child.versions.find_by transaction_id: version.transaction_id
if child_version # it will only exist if this Child changed in this Parent's version
# do stuff with the child's version
end
This worked in my situation, hope something in here is useful for you.
[UPDATED]
I found a better way. You need to update associations inside transaction to make this code work.
class Place < ActiveRecord::Base
has_paper_trail
before_update :check_update
def check_update
return if changed_notably?
tracking_has_many_associations = [ ... ]
tracking_has_has_one_associations = [ ... ]
tracking_has_many_associations.each do |a|
send(a).each do |r|
if r.send(:changed_notably?) || r.marked_for_destruction?
self.updated_at = DateTime.now
return
end
end
end
tracking_has_one_associations.each do |a|
r = send(a)
if r.send(:changed_notably?) || r.marked_for_destruction?
self.updated_at = DateTime.now
return
end
end
end
end
class Version < PaperTrail::Version
def associated_versions
Version.where(transaction_id: transaction_id) if transaction_id
end
end
[Original Answer]
This is the best way I've found so far. (#JohnSchaum's answer helps me a lot, thanks!)
Before starting, I've added polymorphic_type column to the versions table.
class AddPolymorphicTypeToVersions < ActiveRecord::Migration
def change
add_column :versions, :polymorphic_type, :string
end
end
And setup models like this:
# app/models/version.rb
class Version < PaperTrail::Version
has_many :associations, class_name: 'PaperTrail::VersionAssociation'
end
# app/models/link.rb
class Link < ActiveRecord::Base
has_paper_trail meta: { polymorphic_type: :linkable_type }
belongs_to :linkable, polymorphic: true
end
# app/models/place.rb
class Place < ActiveRecord::Base
has_paper_trail
has_many :links, as: :linkable
def all_versions
f = '(item_type = "Place" AND item_id = ?) OR ' +
'(foreign_key_name = "place_id" AND foreign_key_id = ?) OR ' +
'(polymorphic_type = "Place" AND foreign_key_id = ?)'
Version.includes(:associations).references(:associations).where(f, id, id, id)
end
end
And we can now get versions including associations like following:
#place.all_versions.order('created_at DESC')
Inside my "Course" model:
One method is supposed to select all user_ids which belong to a certain course
def participants
Course.joins(:click).pluck(:user_id)
end
The other method is supposed to pick a random user_id
def set_winner
Course.participants.sample
end
However, I get the following error:
undefined method `participants' for #<Class:0x007fc639811468>
If somebody could explain to me, why this doesn't work, I'd be really grateful.
Your example doesn't work because you define instance methods. And then you try to run them on class as if they are class methods.To fix it you could write:
def self.participants
Course.joins(:click).pluck(:user_id)
end
def self.set_winner
Course.participants.sample
end
or, better
class Course < ActiveRecord::Base
scope :participants, -> { joins(:click).pluck(:user_id) }
scope :set_winner, -> { participants.sample }
end
I'm having a stack level too deep error using Ruby 1.8.7 with Rails
3.0.4 and with the rails console I performed the following commands.
leo%>rails console
Loading development environment (Rails 3.0.4)
ruby-1.8.7-head > leo = Organization.find(1)
SystemStackError: stack level too deep
from /app/models/organization.rb:105:in `parents'
Here is the object that is having issues..
class Organization < ActiveRecord::Base
has_many :group_organizations, :dependent =>
:delete_all
has_many :groups, :through => :group_organizations
has_many :orders
has_many :product_contracts
has_many :people
accepts_nested_attributes_for :people
has_many :addresses
accepts_nested_attributes_for :addresses
has_many :organizations
has_many :departments
has_many :organization_credits
has_many :documents
validates_presence_of :name
def self.parents
#organizations = Organization.where("is_company = ?",true)
##organization_parents = []
select_choice = I18n.t("select") + " "+ I18n.t("segments.description")
#organization_parents = [select_choice]
for organization in #organizations
#organization_parents << [organization.name, organization.id]
end
return #organization_parents
end
This error generally happens when you accidentally recursively changing an attribute. If you have a username attribute in User model, and a virtual attribute named username, that is directly changing the username, you end up calling the virtual, the virtual calls the virtual again and so on.. Therefore, take a look on whether something like that happens somewhere in your code.
The stack level too deep error occurs also, if you want to destroy a record and you have an association with :dependent => :destroy to another model. If the other model has a association with :dependent => :destroy back to this model, the stack level is too deep, too.
I had a "stack-level too deep" issue too. it was due to recursiveness in one of my functions and had been caused by a typo as you can see from below:
def has_password?(submitted_password)
encrypt_password == encrypt(submitted_password)
end
private
def encrypt_password
self.salt = make_salt unless has_password?(password)
self.encrypted_password = encrypt(password)
end
I realised I had to change the second line to encrypted and it worked. Just checkout for recursion in your code it must be happening somewhere. Unfortunately I can't be of better use since I can't look at all your code files.
I was getting same stack level too deep error & it turns out that the issue was of recurring rendering of a partial.
I happened to call render a_partial in main view and then in the partial, I accidentally called the same partial again.
HTH
As you are not showing all the code, I can only speculate that you have defined inspect or to_s to build a string containing, among other things the parents.
Your current parents method doesn't seem to be doing anything reasonable, as it returns all organisations that are companies, no matter which association you start from. Thus, any company has itself as parent. Attempting to convert it to string will induce an infinite loop to try to show the parents' of the parents' of ...
In any case, the bulk of your parents method should be in a helper, called something like options_for_parents_select, because that's what it seems to be doing? Even then, the first empty choice should be passed as allow_null to select.
The fact that it sets instance variables is a code smell.
Good luck
I've found the solution to this issue...
I'm using Rails 3 and my class looks like this (and the problematic methods was this too)
class Organization < ActiveRecord::Base
def self.parents
#organizations = self.find :all, :conditions => ['is_company = ? ',true]
select_choice = I18n.t("select") + " "+ I18n.t("segments.description")
#organization_parents = [select_choice]
for organization in #organizations
#organization_parents << [organization.name, organization.id]
end
return #organization_parents
end
#...
end
I did have to hack a lot in the code to find out something was wrong with the named_scope on the line
#organizations = self.find :all, :conditions => ['is_company = ? ',true]
So I had to change it to something like this
#organizations = Organization.where("is_company = ?",true)
But it was wrong too.. So I decided to add an scope for this below the class name so the final code looks like this:
class Organization < ActiveRecord::Base
scope :company, where("is_company = ?",true)
def self.parents
#organizations = self.company
select_choice = I18n.t("select") + " "+ I18n.t("segments.description")
#organization_parents = [select_choice]
for organization in #organizations
#organization_parents << [organization.name, organization.id]
end
return #organization_parents
end
#...
end
So using this line with the scope
#organizations = self.company
it worked flawlessly in every part of the code.
I was wondering if the named_scope is deprecated when using class methods or they are not supported from now and throws an error and not a warning before
Thanks for your help
Leo
If you are getting this error it means rails version that you are using in your application is not compatible with Ruby Version.
Solutions you can use to solve this issue.
1) You need to downgrade the ruby version to older version.
2) or you need to upgrade Rails to latest version.
I got this error when incorrectly creating a has_many relationship like this:
has_many :foos, through: foo
So don't put the same model as 'through' or it will loop endlessly.
I'm having some issues in RoR with some model methods I am setting. I'm trying to build a method on one model, with an argument that gets supplied a default value (nil). The ideal is that if a value is passed to the method, it will do something other than the default behavior. Here is the setup:
I currently have four models: Market, Deal, Merchant, and BusinessType
Associations look like this:
class Deal
belongs_to :market
belongs_to :merchant
end
class Market
has_many :deals
has_many :merchants
end
class Merchant
has_many :deals
belongs_to :market
belongs_to :business_type
end
class BusinessType
has_many :merchants
has_many :deals, :through => :merchants
end
I am trying to pull some data based on Business Type (I have greatly simplified the return, for the sake of brevity):
class BusinessType
def revenue(market=nil)
if market.nil?
return self.deals.sum('price')
else
return self.deals(:conditions => ['market_id = ?',market]).sum('price')
end
end
end
So, if I do something like:
puts BusinessType.first.revenue
I get the expected result, that is the sum of the price of all deals associated with that business type. However, when I do this:
puts BusinessType.first.revenue(1)
It still returns the sum price of all deals, NOT the sum price of all deals from market 1. I've also tried:
puts BusinessType.first.revenue(market=1)
Also with no luck.
What am I missing?
Thanks!
Try this:
class BusinessType
def revenue(market=nil)
if market.nil?
return self.deals.all.sum(&:price)
else
return self.deals.find(:all, :conditions => ['market_id = ?',market]).sum(&:price)
end
end
end
That should work for you, or at least it did for some basic testing I did first.
As I have gathered, this is because the sum method being called is on enumerable, not the sum method from ActiveRecord as you might have expected.
Note:
I just looked a bit further, and noticed you can still use your old code with a smaller tweak than the one I noted:
class BusinessType
def revenue(market=nil)
if market.nil?
return self.deals.sum('price')
else
return self.deals.sum('price', :conditions => ['market_id = ?', market])
end
end
end
Try this!
class BusinessType
def revenue(market=nil)
if market.nil?
return self.deals.sum(:price)
else
return self.deals.sum(:price,:conditions => ['market_id = ?',market])
end
end
end
You can refer this link for other functions. http://en.wikibooks.org/wiki/Ruby_on_Rails/ActiveRecord/Calculations