I have a Position model for which I have a scope defined:
scope :default, where('is_default = ?', 1)
Idea being that I want to know which is the default position. I can do something like: #profile.positions.default and this returns an activerecord relation and the default position record. The issue is that now that I have the default record, I need to access other attributes of Positions such as title..
#profile.positions.default.title
but the above returns an error: NoMethodError: undefined method `title' for #
Any clues? Thanks.
A scope turns a collection of objects, not a single object, so you're trying to call title on an array of ActiveRecord results.
You probably want something like this:
#profile.positions.default.first.title
Or if you always want just one record, you may switch from a scope to a class method:
def self.default
where('is_default = ?', 1).first
end
class Profile < ActiveRecord::Base
has_many :positions
has_one :default_position, :class_name => 'Position',
:conditions => ['is_default = ?', true]
end
Then
#profile.default_position.title
Related
I have two models: Users and PaymentMethods, the association between this models is:
class User < ApplicationRecord
has_many :payment_methods, dependent: :destroy
end
class PaymentMethod < ApplicationRecord
belongs_to :user, optional: true
end
I want to loop in each user and see in an attribute of PaymentMethod, named 'period_end_date'. so I do this:
#users = User.all
#users.each do |u|
u.payment_methods.last.period_end_date
end
I'm getting this error => NoMethodError: undefined method `payment_methods' for User::ActiveRecord_Relation
The error is shown because I have 2 test users, in the first user there is still no data in the attribute 'period_end_date' and association exist, but is empty, in the second user there is data in the attributes, if I say, u.payment_methods.last.period_end_date I get => Wed, 13 Jun 2018 (only in the second user)
I want to filter in my loop only the users who has data in PaymentMethod attributes for get rid of => NoMethodError: undefined method `payment_methods' for User::ActiveRecord_Relation
How I do this?
thanks
I want to filter in my loop only the users who has data in PaymentMethod attributes for get rid of => NoMethodError: undefined method `payment_methods' for User::ActiveRecord_Relation
The actual problem seems to be you have users without payment methods (see my comment on your question).
You have some options, depending on how you're going to use the results.
1) You can filter out users without payment methods when you query them from the database like this:
#users = User.joins :payment_methods
2) If #users must include users that without payment methods, you can skip them when looping like this:
#users.map do |user|
next unless user.payment_methods.any?
user.payment_methods.last.period_end_date
end
3) You can guard by checking for payment_methods before calling .last.
User.all.map do |user|
user.payment_methods.last.period_end_date if user.payment_methods.any?
end
4) You can add a period_end_date method to the user
class User < ApplicationRecord
def period_end_date
payment_methods.limit(1).pluck :period_end_date
end
end
5) push #4 into the association by extending it with a helper method
class User < ApplicationRecord
has_many :payment_methods, class_name: 'PaymentMethod' do
def last_period_end_date
last.period_end_date if any?
end
end
end
which you can call like this
User.all.map do |user|
user.payment_methods.last_period_end_date
end
If you're really only concerned about PaymentMethods without a period_end_date then try this:
6) You can still filter users when you query them from the database
#users = User.joins(:payment_methods).where.not(payment_methods: { period_end_date: nil })
7) This can be simplified a bit by pushing the where.not conditions into a scope of the PaymentMethod class:
class PaymentMethod < ApplicationRecord
scope :period_ends, -> { where.not period_end_date: nil }
end
and merging it
#users = User.joins(:payment_methods).merge PaymentMethod.period_ends
Notes
payment_methods.last doesn't specify an order, you should set one (either as part of this chain, when you specify the association, or with a default scope) otherwise the order is up to your database and may be indeterminate.
chain .includes(:payment_methods) to eager load the payment methods and avoid n+1 queries
it sounds like a nil period_end_date could be invalid data. Consider adding a validation / database constraint to prevent this from happening
UPDATE:
I've put this in my person class
has_many :things, :dependent => :destroy do
def [](kind)
where("kind = ?", kind.to_s)
end
end
but when I invoke <Person Instance>.things[:table], I get this error:
undefined method `where' for #<Class:0x111dc3ba8>
ORIGINAL QUESTION:
I have a person, who has_many things.
I'd like to be able to do something like:
<Person Instance>.things[:table]
that would be defined similar to
def things[](arg)
self.things.find(:first, :conditions => ["kind = ?", arg.to_s])
end
currently, that method gives me this error:
syntax error, unexpected '[', expecting '\n' or ';'
So, how do I properly define things[]?
What you're looking for are called association extensions in rails.
Read about it here
Your implementation might look something like:
has_many :things do
def [](kind)
where(:kind => kind)
end
end
I think [] are not allowed in method names...
You could just skip them in your method name, but then your association method would be overwritten/replaced, if you've set up your assocations properly.
I'd do something like this:
# in your Person model
def things_of_kind(kind)
self.things.find(:first, :conditions => ["kind = ?", arg.to_s]
end
# then you could call
<PersonInstance>.things_of_kind(:table)
Alternatively there are association extensions which use this technique but in the proper place.
And there are also scopes, which can be helpful.
I've been using a model of my application as a proxy to other objects that define behavior.
class Box < ActiveRecord::Base
belongs_to :box_behavior, :polymorphic => true, :validate => true, :foreign_key => 'box_behavior_id', :dependent => :destroy
[...]
def initialize(opts = {})
super(opts)
self.box_behavior = BoxBehaviorDefault.new if self.box_behavior.blank?
end
private
def method_missing(method, *args, &block)
super
rescue NoMethodError
return self.box_behavior.send(method,*args,&block)
end
end
So I implement all the methods on my BoxBehavior objects, and when I call a method on a box instance then it redirects the call to the associated boxbehavior object. It all works fine except when i tried to create a hook on my purchase model where it gets the total from its box object and saves it:
class Purchase < ActiveRecord::Base
belongs_to :box
before_validation_on_create { |r| r.total = r.box.total }
end
When I try to save any purchase object that has a box associated, I get this error:
undefined method `total' for #<ActiveRecord::Associations::BelongsToAssociation:0x7fe944320390>
And I don't have a clue on what to do next... When I implement the total method directly in the box class then it works fine... what can I do to solve this? Isn't the proxy working properly?
I found out that Rails doesn't always use initialize to create a new instance of a model. So i used the hook after_initialize and solved the problem!
It seems that when a child object has a reference to its parent in its setter, it fails to get initialized unless the foreign key is given first in the parameter hash.
class Bot < ActiveRecord::Base
has_many :items
end
class Item < ActiveRecord::Base
belongs_to :bot
def name=(text)
write_attribute(:name, "#{self.bot.name}'s #{text}")
end
end
Item.new(:name => 'pitchfork', :bot_id => 1, ... )
# => undefined method `name' for nil:NilClass (NoMethodError)
Item.new(:bot_id => 1, :name => 'pitchfork', ... )
# => #<Item id: nil, bot_id: 1, name: "r2d2's pitchfork", ... >
Note that the order of hash keys is preserved in Ruby 1.9, but the point is, bot_id must be set before the accessor that has a reference to its parent.
So, the following code works too:
item = Item.new
item.bot_id = 1
item.attributes = { :name => 'pitchfork', ... }
What's really annoying is that the build method on has_many collection doesn't work either, which I think is the right place to patch if I'd have to.
Bot.find(1).items.build(:name => 'pitchfork')
# => undefined method `name' for nil:NilClass (NoMethodError)
What's the best idea to get around this, or patch this, or am I doing anything wrong here?
You could move the string merging to an after_update callback. That way you won't have to access the Bot model until after it's properly setup.
However, I would probably keep name as a simple string and then add a virtual attribute for the merged string. That way it's also updated if the name of Bot is changed.
class Item < ActiveRecord::Base
belongs_to :bot
def full_name
#full_name ||= "#{bot.name}'s #{name}"
end
end
I have a MailingList model that has_may :people
For most of my application, I only want to get people that are active
So #mailing_list.people should only return people that are active
In my model, I can't do
def people
self.people.find_all{ |p| !p.activated_at.nil? }
end
because that keeps calling itself. What is the ruby/rails way to automatically filter the people. Another possible issue is that I think self.people returns an array of active record objects where self.people.find_all... will return an array. This will cause some of my code to break. It's easy fixes but is there a way to return active record objects? It would be nice to have the option.
Thanks!
This is a perfect example for a named scope:
class Person < ActiveRecord::Base
named_scope :active, :conditions => 'activated_at is not null'
end
Then just call it:
# equivalent to Person.find(:all, :conditions => 'activated_at is not null')
#active_people = Person.active
You can also filter at the association level.
has_many :people, :conditions => {:activated => true}
You can used the standard find method or a dynamic finder. Your find might read as follows:
people.find(:all, :conditions => "activated_at = nil")
OR
people.find_all(:conditions => "activated_at = nil")
A dynamic version of this might read as:
people.find_by_activated_at(nil)