Pass parameter to callback in Rails - ruby-on-rails

I have 2 models: User and Favorite. In model Favorite:
class Favorite < ApplicationRecord
belongs_to :user, foreign_key: :user_id
def self.add_favorite(options)
create!(options)
end
def self.unfavorite(options)
where(options).delete_all
end
Now, I want to limit number of records saved to Favorite is 10. It mean that users are only liked 10 products. I researched google, someone said that I try to use callback and I think it's right way, but it raise 2 questions:
1. Can I use query in method for callback?
2. Callback can be pass argument?
It is sample code I think:
class Favorite < ApplicationRecord
after_create :limit_records(user_id)
belongs_to :user, foreign_key: :user_id
def self.add_favorite(options)
create!(options)
end
def self.unfavorite(options)
where(options).delete_all
end
def limit_records(user_id)
count = self.where(user_id: user_id).count
self.where(used_id: user_id).last.delete if count > 10
end
If user had 10 favorite, when they like any products, callback will be called after Favorite is created and will be delete if it's 11th record.

You have:
belongs_to :user, foreign_key: :user_id
in your Favorite model and limit_records is an instance method on Favorite. So you have access to the user as self.user_id (or just user_id since self is implied) inside limit_records and there is no need for an argument:
after_create :limit_records
def limit_records
# same as what you have now, `user_id` will be `self.user_id`
# now that there is no `user_id` argument...
count = self.where(user_id: user_id).count
self.where(used_id: user_id).last.delete if count > 10
end

Related

Can Rails have dynamic relationships in has_many?

I have a Lesson model which has many Completions like this:
class Lesson < ActiveRecord::Base
has_many :completions, as: :completable
belongs_to :course
end
And each Completion belongs to a User as well:
class Completion < ActiveRecord::Base
belongs_to :user
belongs_to :completable, polymorphic: true
end
From my application perspective I'm only interested in the amount of completions for a certain lesson, so I've included a counter cache. In regard to the individual Completions, I'm only interested if the Lesson is completed by the current user (I'm using Devise).
Is there some way to create a dynamic has_one relationship of some kind, that uses the information from the current_user to query the Completion table?
for instance:
has_one :completion do
def from_user current_user
Completion.where(completable: self, user: current_user)
end
end
Although this could work, I'm also having a polymorphic relationship. Rails is complaining that there's no foreign key called lesson_id. When I add a foreign_key: symbol, the do-end block stops working.
Any ideas?
Why not passing both block and options to has_many?
class Lesson < ActiveRecord::Base
has_many :completions, as: :completable do
def from_user user
if loaded?
find {|c| c.user_id = user.id}
else
find(user_id: user.id)
end
end
end
belongs_to :course
end
#lesson = Lesson.last
# Association not loaded - executing sql query
#lesson.completions.from_user(current_user)
#lesson.completions
# Association loaded - no sql query
#lesson.completions.from_user(current_user)
NOTE: You cannot treat it as an association, so it cannot be preloaded on its own.

Save before initial save of an object

When a conversation is created, I want that conversation to have its creator automatically following it:
class Conversation < ActiveRecord::Base
belongs_to :user
has_many :followers
has_many :users, through: :followers
alias_method :user, :creator
before_create { add_follower(self.creator) }
def add_follower(user)
unless self.followers.exists?(user_id: user.id)
self.transaction do
self.update_attributes(follower_count: follower_count + 1)
self.followers.create(user_id: user.id)
end
end
end
end
However, when a user attempts to create a conversation I get a stack level too deep
. I'm creating an infinite loop, and I think this is because the before_create callback is being triggered by the self.update_attributes call.
So how should I efficiently update attributes before creation to stop this loop happening?
Option 1 (preferred)
Rename your column follower_count to followers_count and add:
class Follower
belongs_to :user, counter_cache: true
# you can avoid renaming the column with "counter_cache: :follower_count"
# rest of your code
end
Rails will handle updating followers_count for you.
Then change your add_follower method to:
def add_follower(user)
return if followers.exists?(user_id: user.id)
followers.build(user_id: user.id)
end
Option 2
If you don't want to use counter_cache, use update_column(:follower_count, follower_count + 1). update_column does not trigger any validations or callbacks.
Option 3
Finally you don't need to save anything at this point, just update the values and they will be saved when callback finishes:
def add_follower(user)
return if followers.exists?(user_id: user.id)
followers.build(user_id: user.id)
self.follower_count = follower_count + 1
end

ActiveRecord custom attribute

I have a 3 simple models:
class User < ActiveRecord::Base
has_many :subscriptions
end
class Game < ActiveRecord::Base
has_many :subscriptions
end
class Subscription < ActiveRecord::Base
belongs_to :user
belongs_to :game
end
What I am wondering is, is it possible when I query for Games to include another attribute called 'is_subbed' which will contain wether a particular user is subscribed to a game? Something like:
a_user = User.first
games = Game.scoped
games.conditions blah blah blah
and games will include a 'virtual' or in memory attribute that will be custom to a_user called is_subed
You can make a class method and an instance method (for a single game) like so:
class Game < ActiveRecord::Base
def self.subscribed?(user)
joins(:subscriptions).where(subscriptions: { user_id: user.id}).exists?
end
def subscribed?(user)
subscriptions.where(user_id: user.id).exists?
end
end
To get this result for each game using the query API, you can do this:
scope :with_subscriptions, lambda do |user|
joins("LEFT JOIN subscriptions ON subscriptions.game_id = games.id AND subscriptions.user_id = #{user.id}")
select("games.*, CASE WHEN subscriptions.user_id IS NULL THEN true ELSE false END as is_subscribed")
end
This will give you an is_subscribed parameter on each game object returned.
If I'm not mistaken you need to get games which are subscribed by some user and also apply some scope on these games?
May be this way would be acceptable
# extend user model with games collection
class User < ActiveRecord::Base
# your code
has_many :games, through: :subscriptions
end
And then in the controller (or anywhere you need) just call
#user.games.your_games_scope1.your_games_scope2.etc
Sorry if I misunderstood you

Update Post attribute when new Rating is created

I have a Post model which has many ratings and I would like to store the average ratings figure for each post in the post ratings db column. I have a counter cache on the post model which counts the number of ratings.
I have the following code in my post.rb
def update_rating
if self.ratings_count_changed?
self.rating = self.ratings.average(:rating)
end
end
I had this as a before_save callback so whenever the post is edited and saved it updates the ratings, not very useful.
Is there a way I can call this method whenever a new rating is created?
One way of getting closer to your goal is to add after_create callback to Rating model:
class Rating < ActiveRecord::Base
belongs_to :post
# ...
after_create :update_post_average_rating
def update_post_average_rating
self.post.update_attributes(:rating => self.post.ratings.average(:rating))
end
end
Or even more OO friendly version:
class Post < ActiveRecord::Base
has_many :ratings
# ...
def update_average_rating
update_attributes(:rating => self.ratings.average(:rating))
end
end
class Rating < ActiveRecord::Base
belongs_to :post
# ...
after_create :update_post_average_rating
def update_post_average_rating
self.post.update_average_rating
end
end
Do you think about storing average rating in rating model instead post model? In that case you don't need any callback and you need recalc average by first request after changes.
Instead of before_save on post, do an after_create on the rating because it sounds like you need to update that rating score when a new rating is created, not before the post is saved.
How about putting this into Rating model:
after_create :update_post_average_rating
def update_post_average_rating
self.post.rating = #....
end

Active Relation: Retrieving records through an association?

I have the following models:
class User < ActiveRecord::Base
has_many :survey_takings
end
class SurveyTaking < ActiveRecord::Base
belongs_to :survey
def self.surveys_taken # must return surveys, not survey_takings
where(:state => 'completed').map(&:survey)
end
def self.last_survey_taken
surveys_taken.maximum(:position) # that's Survey#position
end
end
The goal is to be able to call #user.survey_takings.last_survey_taken from a controller. (That's contrived, but go with it; the general goal is to be able to call class methods on #user.survey_takings that can use relations on the associated surveys.)
In its current form, this code won't work; surveys_taken collapses the ActiveRelation into an array when I call .map(&:survey). Is there some way to instead return a relation for all the joined surveys? I can't just do this:
def self.surveys_taken
Survey.join(:survey_takings).where("survey_takings.state = 'completed'")
end
because #user.survey_takings.surveys_taken would join all the completed survey_takings, not just the completed survey_takings for #user.
I guess what I want is the equivalent of
class User < ActiveRecord::Base
has_many :survey_takings
has_many :surveys_taken, :through => :survey_takings, :source => :surveys
end
but I can't access that surveys_taken association from SurveyTaking.last_survey_taken.
If I'm understanding correctly you want to find completed surveys by a certain user? If so you can do:
Survey.join(:survey_takings).where("survey_takings.state = 'completed'", :user => #user)
Also it looks like instead of:
def self.surveys_taken
where(:state => 'completed').map(&:survey)
end
You may want to use scopes:
scope :surveys_taken, where(:state => 'completed')
I think what I'm looking for is this:
class SurveyTaking < ActiveRecord::Base
def self.surveys_taken
Survey.joins(:survey_takings).where("survey_takings.state = 'completed'").merge(self.scoped)
end
end
This way, SurveyTaking.surveys_taken returns surveys taken by anyone, but #user.survey_takings.surveys_taken returns surveys taken by #user. The key is merge(self.scoped).
Waiting for further comments before I accept..

Resources