Filter nested model attributes in RoR - ruby-on-rails

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

Related

Rails create a Model for a controller that uses a .group method

I need to collect my customers with Spree::Order.group(:email) (since we have a guest checkout option).
The controller is as such:
class CustomersController < ApplicationController
def index
#customers = Spree::Order.where(state: "complete").group(:email).select(
:email,
'count(id) AS total_orders_count',
'sum(payment_total) AS amount',
'array_agg(number) AS order_numbers',
'array_agg(completed_at) AS completion_dates'
)
end
Can I create a customer.rb model for these #customers so I can move the logic there. I need to .joins(:line_items) and filter by date, so I figure it'd be cleaner.
P.S. As an asside...the 'sum(payment_total' AS amount always returns 0.0 because the payment_total is a BigDecimal object. What's the correct syntax for this request that would act like 'sum(payment_total.to_f)'....
Since you don't have a customers table, and you're trying to abstract the concept of a guest customer out of the orders table, creating a model is a reasonable approach.
Consider the following:
class Customer < ActiveRecord::Base
# Use the `orders` table for this "virtual" model
self.table_name = Spree::Order.table_name
# ensure no writes ever occur from this model as a precaution
def readonly?; true; end
scope :with_completed_orders, -> {
where(state: "complete")
.select(
:email,
'count(id) AS total_orders_count',
'sum(payment_total) AS amount',
'array_agg(number) AS order_numbers',
'array_agg(completed_at) AS completion_dates'
)
.group(:email)
.order(:email) # prevents errors - remove it and try Customer.first in the console.
}
scope :by_email, -> (email) { where(email: email) }
# Default scopes are generally a no-no, but a convenience here
def self.default_scope
# this could be any scope that groups by :email
self.with_completed_orders
end
# example of a potentially useful instance method
def orders
# returns a scope, which you can chain
Spree::Order.where(email: email)
end
end
The selects in the scope populate Customer instance attributes of the same name, as you probably know.
This will allow you to do things like:
customers = Customer.all
c = Customer.by_email('test#example.com')
c.amount # returns BigDecimal
c.order_numbers # returns Array
c.orders.first # example of chaining scope for #orders instance method
In the controller:
class CustomersController < ApplicationController
def index
#customers = Customer.all
end
end
Hopefully this gives you some ideas.

Referencing Associated Object Returns Nil

I have two models.
class User < ActiveRecord::Base
has_one :message
end
class Message < ActiveRecord::Base
belongs_to :user
end
If I have a created user with an associated Message and I delete that message and create a new one like, user.message returns nil. For example.
user = User.create
message = Message.create(user_id: user.id)
Message.where(user_id: user.id).destroy_all
Message.create(user_id: user.id)
# Now if I call this below, it always returns nil
user.message
Why does this occur? Shouldn't Rails 3 pick up on that change? How do I fix this?
Just load the object again before doing user.message like, user.reload.
reload - Reloads the record from the database.

Is possible to append a key value to an ActiveRecord object

I want to have an object that has an associated value added to it.
This is what I am attempting to do:
#users = #search.results
#user_results = []
#users.each do |user|
#user_results = #user_results.push user << {photo_url: UsersPhoto.find_by_user_id(user.id).image_name}
end
I'm getting:
NoMethodError (undefined method `<<' for #):
Is there a way to do something like this?
This is what associations are for. You should just add an association to your User for the UsersPhoto you're trying to find, and use #user.users_photo.image_name.
class User < ActiveRecord::Base
has_one :users_photo
end
Failing that, add a photo_url method to your User model, which wraps up the UsersPhoto.find...:
class User < ActiveRecord::Base
def photo_url
UsersPhoto.find_by_user_id(id).image_name
end
end
Failing that, you can do what you're trying to do, but you'll need to add a attr_accessor :photo_url to your User model, and your syntax is all wrong:
class User < ActiveRecord::Base
attr_accessor :photo_url
end
#users.each do |user|
user.photo_url = UsersPhoto.find_by_user_id(user.id).image_name
end
Why don't you use the .pluck method?
#users = #search.results
#user_results = UsersPhoto.where(user_id: #users.pluck(:id) ).select(:image_name)
To explain, at least from what I am understanding, you want an array of image_names for the results #users. So just get their ids, find those UsersPhotos that match those ids and just get the image_name column

How to update instance variable in Rails model?

In my Rails app I have users who can have many payments.
class User < ActiveRecord::Base
has_many :invoices
has_many :payments
def year_ranges
...
end
def quarter_ranges
...
end
def month_ranges
...
end
def revenue_between(range, kind)
payments.sum_within_range(range, kind)
end
end
class Invoice < ActiveRecord::Base
belongs_to :user
has_many :items
has_many :payments
...
end
class Payment < ActiveRecord::Base
belongs_to :user
belongs_to :invoice
def net_amount
invoice.subtotal * percent_of_invoice_total / 100
end
def taxable_amount
invoice.total_tax * percent_of_invoice_total / 100
end
def gross_amount
invoice.total * percent_of_invoice_total / 100
end
def self.chart_data(ranges, unit)
ranges.map do |r| {
:range => range_label(r, unit),
:gross_revenue => sum_within_range(r, :gross),
:taxable_revenue => sum_within_range(r, :taxable),
:net_revenue => sum_within_range(r, :net) }
end
end
def self.sum_within_range(range, kind)
#sum ||= includes(:invoice => :items)
#sum.select { |x| range.cover? x.date }.sum(&:"#{kind}_amount")
end
end
In my dashboard view I am listing the total payments for the ranges depending on the GET parameter that the user picked. The user can pick either years, quarters, or months.
class DashboardController < ApplicationController
def show
if %w[year quarter month].include?(params[:by])
#unit = params[:by]
else
#unit = 'year'
end
#ranges = #user.send("#{#unit}_ranges")
#paginated_ranges = #ranges.paginate(:page => params[:page], :per_page => 10)
#title = "All your payments"
end
end
The use of the instance variable (#sum) greatly reduced the number of SQL queries here because the database won't get hit for the same queries over and over again.
The problem is, however, that when a user creates, deletes or changes one of his payments, this is not reflected in the #sum instance variable. So how can I reset it? Or is there a better solution to this?
Thanks for any help.
This is incidental to your question, but don't use #select with a block.
What you're doing is selecting all payments, and then filtering the relation as an array. Use Arel to overcome this :
scope :within_range, ->(range){ where date: range }
This will build an SQL BETWEEN statement. Using #sum on the resulting relation will build an SQL SUM() statement, which is probably more efficient than loading all the records.
Instead of storing the association as an instance variable of the Class Payment, store it as an instance variable of a user (I know it sounds confusing, I have tried to explain below)
class User < ActiveRecord::Base
has_many :payments
def revenue_between(range)
#payments_with_invoices ||= payments.includes(:invoice => :items).all
# #payments_with_invoices is an array now so cannot use Payment's class method on it
#payments_with_invoices.select { |x| range.cover? x.date }.sum(&:total)
end
end
When you defined #sum in a class method (class methods are denoted by self.) it became an instance variable of Class Payment. That means you can potentially access it as Payment.sum. So this has nothing to do with a particular user and his/her payments. #sum is now an attribute of the class Payment and Rails would cache it the same way it caches the method definitions of a class.
Once #sum is initialized, it will stay the same, as you noticed, even after user creates new payment or if a different user logs in for that matter! It will change when the app is restarted.
However, if you define #payments_with_invoiceslike I show above, it becomes an attribute of a particular instance of User or in other words instance level instance variable. That means you can potentially access it as some_user.payments_with_invoices. Since an app can have many users these are not persisted in Rails memory across requests. So whenever the user instance changes its attributes are loaded again.
So if the user creates more payments the #payments_with_invoices variable would be refreshed since the user instance is re-initialized.
Maybe you could do it with observers:
# payment.rb
def self.cached_sum(force=false)
if #sum.blank? || force
#sum = includes(:invoice => :items)
end
#sum
end
def self.sum_within_range(range)
#sum = cached_sum
#sum.select { |x| range.cover? x.date }.sum(&total)
end
#payment_observer.rb
class PaymentObserver < ActiveRecord::Observer
# force #sum updating
def after_save(comment)
Payment.cached_sum(true)
end
def after_destroy(comment)
Payment.cached_sum(true)
end
end
You could find more about observers at http://apidock.com/rails/v3.2.13/ActiveRecord/Observer
Well your #sum is basically a cache of the values you need. Like any cache, you need to invalidate it if something happens to the values involved.
You could use after_save or after_create filters to call a function which sets #sum = nil. It may also be useful to also save the range your cache is covering and decide the invalidation by the date of the new or changed payment.
class Payment < ActiveRecord::Base
belongs_to :user
after_save :invalidate_cache
def self.sum_within_range(range)
#cached_range = range
#sum ||= includes(:invoice => :items)
#sum.select { |x| range.cover? x.date }.sum(&total)
end
def self.invalidate_cache
#sum = nil if #cached_range.includes?(payment_date)
end

Accessing attributes via Rails relations

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

Resources