AASM: Call event for multiple records(array) at once - ruby-on-rails

I do have AASM event which do changes status of the record.
event :change_status do
transitions from: [:created], to: :confirmed
end
Currently in the code i'm going through each passed record and call event, like:
def status_update(ids)
payments = Payment.where(id: ids)
payments.each do |payment|
if payment.valid?
payment.change_status
payment.save
end
end
end
Is is somehow possible to call change_status for all records at once (in bulk), nor just go through each record? Otherwise it's time consuming, especially if there are a lot of records.

Related

Rails & postgresql, notify/listen to when a new record is created

I'm experimenting & learning how to work with PostgreSQL, namely its Notify/Listen feature, in the context of making Server-Sent Events according to this tutorial.
The tutorial publishes NOTIFY to the user channel (via its id) whenever a user is saved and an attribute, authy_status is changed. The LISTEN method then yields the new authy_status Code:
class Order < ActiveRecord::Base
after_commit :notify_creation
def notify_creation
if created?
ActiveRecord::Base.connection_pool.with_connection do |connection|
execute_query(connection, ["NOTIFY user_?, ?", id, authy_status])
end
end
end
def on_creation
ActiveRecord::Base.connection_pool.with_connection do |connection|
begin
execute_query(connection, ["LISTEN user_?", id])
connection.raw_connection.wait_for_notify do |event, pid, status|
yield status
end
ensure
execute_query(connection, ["UNLISTEN user_?", id])
end
end
end
end
I would like to do something different, but haven't been able to find information on how to do this. I would like to NOTIFY when a user is created in the first place (i.e., inserted into the database), and then in the LISTEN, I'd like to yield up the newly created user itself (or rather its id).
How would I modify the code to achieve this? I'm really new to writing SQL so for example, I'm not very sure about how to change ["NOTIFY user_?, ?", id, authy_status] to a statement that looks not at a specific user, but the entire USER table, listening for new records (something like... ["NOTIFY USER on INSERT", id] ?? )
CLARIFICATIONS
Sorry about not being clear. The after_save was a copy error, have corrected to after_commit above. That's not the issue though. The issue is that the listener listens to changes in a SPECIFIC existing user, and the notifier notifies on changes to a SPECIFIC user.
I instead want to listen for any NEW user creation, and therefore notify of that. How does the Notify and Listen code need to change to meet this requirement?
I suppose, unlike my guess at the code, the notify code may not need to change, since notifying on an id when it's created seems to make sense still (but again, I don't know, feel free to correct me). However, how do you listen to the entire table, not a particular record, because again I don't have an existing record to listen to?
For broader context, this is the how the listener is used in the SSE in the controller from the original tutorial:
def one_touch_status_live
response.headers['Content-Type'] = 'text/event-stream'
#user = User.find(session[:pre_2fa_auth_user_id])
sse = SSE.new(response.stream, event: "authy_status")
begin
#user.on_creation do |status|
if status == "approved"
session[:user_id] = #user.id
session[:pre_2fa_auth_user_id] = nil
end
sse.write({status: status})
end
rescue ClientDisconnected
ensure
sse.close
end
end
But again, in my case, this doesn't work, I don't have a specific #user I'm listening to, I want the SSE to fire when any user has been created... Perhaps it's this controller code that also needs to be modified? But this is where I'm very unclear. If I have something like...
User.on_creation do |u|
A class method makes sense, but again how do I get the listen code to listen to the entire table?
Please use after_commit instead of after_save. This way, the user record is surely committed in the database
There are two additional callbacks that are triggered by the completion of a database transaction: after_commit and after_rollback. These callbacks are very similar to the after_save callback except that they don't execute until after database changes have either been committed or rolled back.
https://guides.rubyonrails.org/active_record_callbacks.html#transaction-callbacks
Actually it's not relevant to your question, you can use either.
Here's how I would approach your use case: You want to get notified when an user is created:
#app/models/user.rb
class User < ActiveRecord::Base
after_commit :notify_creation
def notify_creation
if id_previously_changed?
ActiveRecord::Base.connection_pool.with_connection do |connection|
self.class.execute_query(connection, ["NOTIFY user_created, '?'", id])
end
end
end
def self.on_creation
ActiveRecord::Base.connection_pool.with_connection do |connection|
begin
execute_query(connection, ["LISTEN user_created"])
connection.raw_connection.wait_for_notify do |event, pid, id|
yield self.find id
end
ensure
execute_query(connection, ["UNLISTEN user_created"])
end
end
end
def self.clean_sql(query)
sanitize_sql(query)
end
def self.execute_query(connection, query)
sql = self.clean_sql(query)
connection.execute(sql)
end
end
So that if you use
User.on_creation do |user|
#do something with the user
#check user.authy_status or whatever attribute you want.
end
One thing I am not sure why you want to do this, because it could have a race condition situation where 2 users being created and the unwanted one finished first.

Rails: around_save callback to conditionally trigger an action specific to one attribute?

My Rails app has events and users. I need to send a message to a user if/when they're added to an event, whether it's a new event being created or an existed one being updated. To avoid messaging them multiple times, I'm considering a design pattern like this:
class Event < ActiveRecord::Base
has_many :users
around_save :contact_added_users
def contact_added_users
existing_users = self.users
yield
added_users = self.users.reject{|u| existing_users.include? u }
added_users.each { |u| u.contact }
end
end
Is this a sensible approach? I'm not sure how to work this functionality into the conditional callbacks pattern.
Edit: It occurs to me that this won't work as added_users will always be empty.
Update: In order to minimize conflicts with existing code, I think I'm going to avoid using callbacks in the model, and instead use an around filter in the controller. Also, that allows me to query the database for the existing users before they're attached to the event object. Something like this (using ActiveAdmin):
ActiveAdmin.register Event do
controller do
around_filter :contact_added_users
def contact_added_users
#event = Event.find(params[:id])
existing_users = #event.users
yield
added_users = #event.users.reject{|u| existing_users.include? u }
added_users.each { |u| u.contact }
end
end
end

Destroy a post after 30 days from its creation

For learning purposes I created a blog, now I want to destroy a post automatically after 30 days from its creation. how can I do it?
This is my Posts controller
def index
#posts = Post.all
end
def create
#post = current_user.posts.new(post_params)
#post.save
redirect_to posts_path
end
def destroy
#post.destroy
redirect_to posts_path
end
I would set up a task with whenever that runs every 1 day.
To generate a task:
rails g task posts delete_30_days_old
Then on the created file (lib/tasks/posts.rb), add the following code:
namespace :posts do
desc "TODO"
task delete_30_days_old: :environment do
Post.where(['created_at < ?', 30.days.ago]).destroy_all
end
end
This is of course if you want to delete the posts that have more than 30 days, other answers might as well work but I would rather have my database with clean data that I'll use on my application.
Posts will be stored in your database. The model is what interacts with your database. Your controller never sees the database, it only sees what the model shows it. If you wanted to pull from the database using your model inside the controller you could do it with this code.
#posts = Post.where('created_at >= :thirty_days_ago', thiryty_days_ago: Time.now - 30.days)
Post in this code calls you app/model/Post.rb which inherited active record. .where is the active record method that looks at your database based on the stuff you define. Here we have defined to pull only rows where the created_at column has a time in it that is 30 days ago.
If you look inside your database you'll notice the created_at column was automagically put in there for you.
Along with the aforementioned whenever gem, you can also use two gems called Sidekiq and Sidetiq for scheduling tasks/workers.
I've been using these on a large app at work and am very pleased with it. It's fast (uses Redis, added with a simple gem, reliable, and easy to use).
# in app/workers/clean_posts.rb
class CleanPosts
include Sidekiq::Worker
include Sidetiq::Schedulable
recurrence { monthly }
def perform
# stealing from toolz
Post.where('created_at >= :thirty_days_ago', thiryty_days_ago: Time.now - 30.days).destroy_all
end
end
This will, however, remove the posts from your DB and they will no longer be accessible by your application.
To achieve desired result, you need to change your index action like this:
def index
#posts = Post.where(created_at: 30.days.ago..Time.now)
end
In such way, you won't need to destroy posts and you will get the desired result.
If you need to limit access to the older posts, then you can use:
def show
#post = Post.where(created_at: 30.days.ago..Time.now).find(params[:id])
end
And, if we are speaking about code beauty, then you should move where(created_at: 30.days.ago..Time.now) part to a scope in your model, like this:
class Post
...
scope :recent, -> { where(created_at: 30.days.ago..Time.now) }
end
And use it like this:
Post.recent #=> to get list of recent posts
Post.recent.find(params[:id]) #=> to get recent post with specified id
You can not do that from your controller, you need to add some functionality to your application.
You will need a cron job running everyday that will look for posts that are more than 30 days old and destroy them.
eg Post.where('created_at < ?', 30.days.ago)
For handling the cron jobs better you might consider using the whenever gem that helps a lot and keeps the cron setup in your app.

Base.save, callbacks and observers

Let us say that we have the model Champ, with the following attributes, all with default values of nil: winner, lose, coach, awesome, should_watch.
Let's assume that two separate operations are performed: (1) a new record is created and (2) c.the_winner is called on a instance of Champ.
Based on my mock code, and the observer on the model, what values are saved to the DB for these two scenarios? What I am trying to understand is the principles of how callbacks work within the context of Base.save operation, and if and when the Base.save operation has to be called more than once to commit the changes.
class Champ
def the_winner
self.winner = 'me'
self.save
end
def the_loser
self.loser = 'you'
end
def the_coach
self.coach = 'Lt Wiggles'
end
def awesome_game(awesome_or_not=false)
self.awesome = awesome_or_not
end
def should_watch_it(should=false)
self.should_watch = should
end
end
class ChampObserver
def after_update(c)
c.the_loser
end
def after_create(c)
c.the_coach
end
def before_create(c)
c.awesome_game(true)
c.should_watch_it(true) if c.awesome_game
end
end
With your example, if you called champ.winner on a new and unmodified instance of Champ, the instance of Champ would be committed to the DB and would look like this in the database:
winner: 'me'
awesome: true
should_watch: true
loser: nil
coach: nil
The after_create callback would be called if it is a new record, and if not, the after_update callback would (this is why loser would be nil if the instance was new). However, because they just call a setter method on the instance, they will only update the instance and will not commit more changes to the DB.
You could use update_attribute in your observer or model methods to commit the change, but unless you actually need to have the record in the database and then update it, it's wasteful. In this example, if you wanted those callbacks to actually set loser and coach in the database, it'd be more efficient to use before_save and before_create.
The Rails guides site has a good overview of callbacks here, if you haven't read it already.

How to ensure atomicity when updating a field in a table?

I want to write a piece of code such that it is guaranteed that at any one time, only one process can update a field for a certain record in the posts table.
Is this the correct way to do it?
#Make a check before entering transaction, so that a transaction
#is not entered into needlessly (this check is just for avoiding
#using DB resources that will be used when starting a transaction)
if #post.can_set_to_active?
ActiveRecord::Base.transaction do
#Make a check again, this time after entering transaction, to be
#sure that post can be marked active.
#Expectation is that inside a transaction, it is guaranteed that no other
#process can set the status of this post.
if #post.can_set_to_active?
#now set the post to active
#post.status = :active
#post.save
end #end of check inside transaction
end #end of transaction
end #end of check outside transaction
Also, is there some way to test this scenario using RSpec or even some other method?
class Post
##activation_lock = Mutex.new
def activate
self.status = :active
self.save
end
synchronize :activate, :with => :##activation_lock
end

Resources