How to check if a model instance was updated - ruby-on-rails

I've searched through the site on how to accomplish this and many answers point towards using the changed? method. I'm trying to notify users of an update after persisting the DB so unfortunately this won't work.
I then found the previous_changes method but this also triggers when a post is first created. The goal for is to do this only edit actions. How can this be done?
<% if #post.previous_changes %>
<span>Updated:</span> <span><%= time_ago_in_words(#post.updated_at) %> ago</span>
<% end %>

One possible solution is to set a flag whenever the element is updated.
class Model
after_update :flag_update
def updated?
!!#updated
end
private
def flag_update
#updated = true
end
end
Then in your code simply check #post.updated?.

You have an updated record when
#post.previous_changes.present? && #post.previous_changes["id"].nil?

Sounds like a good candidate for using after_update callback and in it calling a job to send an e-mail, text message or whatever your means of notifying a user is.
Using the callback to flag the model instance basically adds a state for the model and only works once because it is set after the first update. Any subsequent updates might go unnoticed depending on what and how you want to notify your users, unless you could somehow reset the state (flag), which may be a lot more work than what it is worth.
Relying on the updated_at timestamp to be greater than the created_at suffers from the same one-time opportunity than flagging.
We used an approach where we checked what a model's changes returns (you could also use changed?) before saving it, and then sent a notification after a successful save.

If you're able to call previous_changes, why not just add a conditional to make sure that id was not changed (as per this answer):
<% if #post.previous_changes && !#post.id_changed? %>
...
<% end %>

Related

Rails refactoring a method

im a new developer and i need some help to refactor this code
So, I have a Rails App with a controller called subscription_signups. There i have a New method, that helps me create a Subscription, and it executes a private method called add_plan_to_cookies.
This is my controller
def new
#subscription_signup = SubscriptionSignup.new(account_id: current_account.id)
add_plan_to_cookies
end
private
def add_plan_to_cookies
plan = current_account.base_plans.find_by(id: params[:base_plan])&.current_plan
remember_plan(plan.id) if plan.present?
#plan = current_account.base_plans.find_by(id: cookies.signed[:plan])&.current_plan
end
def remember_plan(plan)
cookies.signed[:plan] = plan
end
In the add_plan_to_cookies method, the plan is obtained through the base_plan_id and then another method called remember_plan is executed and saves the plan in a cookie. What i have to do is using the plan that was saved in the cookie. & I can obtain that with the second query, but there has to be a better way.
So first I get the ID of the params, look for the plan and add its id to the cookies. Then i have to use that id to search for the plan that you already got before.
MY problem is that the im doing too queries for something that is kind of the same, and i dont now have to refactor it. anyone has a suggestion?
In the view i have something like this.
<% if #plan.present? %>
<%= #plan.name %>
<%= image_tag #plan.base_plan.cover(:medium), class: "img-responsive "%>
<% end %>
It seems that you store the id of current_plan in the cookie and then you load a base_plan with the id of your current_plan and then proceeds to check if it has yet another current_plan. Written like this:
#plan = current_account.base_plans.find_by(id: current_account.base_plans.find_by(id: params[:base_plan])&.current_plan || cookies.signed[:plan])&.current_plan
I don't know your datastucture, but did you perhaps mean to write something like?
remember_plan(params[:base_plan]) if plan.present?
If that's the case I would write it like so:
def new
#subscription_signup = SubscriptionSignup.new(account_id: current_account.id)
#plan = load_plan
remember_plan(#plan)
end
private
def load_plan
plan = current_account.base_plans.find_by(id: params[:base_plan_id])&.current_plan
plan ||= current_account.base_plans.find_by(id: cookies.signed[:plan_id])&.current_plan
end
def remember_plan(plan)
cookies.signed[:plan_id] = plan.id if plan.present?
end
I don't entirely understand the intentions of your code, but as I see it you want to load the plan in to an instance variable so it is exposed to the view and also want to save the plan for later use. It's two responsibilities, and they are both in one method add_plan_to_cookies. I would split it up in a load and a store method.
I recommend making your variables names that contain id end with _id, so it's more specific what kind of object you work with. Like params[base_plan_id] or cookies.signed[:plan_id]. But it's personal preference I guess.
I might have compleetly misunderstood your code. Please let me know.

How to skip_callback before_save for specific user?

I've a method named update inside my DailyOrdersController:
def update
if #daily_order.update( daily_order_params.merge({default_order:false}) )
respond_or_redirect(#daily_order)
else
render :edit
end
end
My DailyOrder model:
before_save :refresh_total
def refresh_total
# i do something here
end
What I'm trying to do now is, I want the refresh_total callback to be skipped if the update request is coming from current_admin.
I have 2 user model generated using Devise gem:
User (has current_user)
Admin (has current_admin)
I try to make it like this:
def update
if current_admin
DailyOrder.skip_callback :update, :before, :refresh_total
end
if #daily_order.update( daily_order_params.merge({default_order:false}) )
respond_or_redirect(#daily_order)
else
render :edit
end
end
But it's not working and still keep calling the refresh_total callback if the update request is coming from current_admin (when the logged-in user is admin user).
What should I do now?
I think this is all what you need:
http://guides.rubyonrails.org/active_record_callbacks.html#conditional-callbacks
If you skip callback, you should enable it later. Anyway, this does not look as the best solution. Perhaps you could avoid the callbacks otherwise.
One way would be to use update_all:
DailyOrder.where(id: #daily_order.id).update_all( daily_order_params.merge({default_order:false}) )
Or you could do something like this:
#in the model:
before_validation :refresh_total
#in the controller
#daily_order.assign_attributes( daily_order_params.merge({default_order:false}) )
#daily_order.save(validate: current_admin.nil?)
or maybe it would be the best to add a new column to the model: refresh_needed and then you would conditionally update that column on before_validation, and on before_save you would still call the same callback, but conditionally to the state of refresh_needed. In this callback you should reset that column. Please let me know if you would like me to illustrate this with some code.
This may come in handy:
http://www.davidverhasselt.com/set-attributes-in-activerecord/
UPDATE
Even better, you can call update_columns.
Here is what it says in the documentation of the method:
Updates the attributes directly in the database issuing an UPDATE SQL
statement and sets them in the receiver:
user.update_columns(last_request_at: Time.current)
This is the fastest way to update attributes because it goes straight to
the database, but take into account that in consequence the regular update
procedures are totally bypassed. In particular:
\Validations are skipped.
\Callbacks are skipped.
+updated_at+/+updated_on+ are not updated.
This method raises an ActiveRecord::ActiveRecordError when called on new
objects, or when at least one of the attributes is marked as readonly.

Is there a way to compare the state of an object before and after certain callbacks?

When a comment on our site gets destroyed, the after_destroy callback is used to update stats:
after_destroy do |p|
p.topic.update_attribute(:replies, p.replies - 1)
end
I want to be able to do the same thing when a comment gets "soft-deleted". When a comment is soft-deleted, the comment.visible field is set from 1 to 0.
Is there a way to tell when this happens during the update callbacks? Something like:
after_update do |p|
if p.visible was changed from 1 to 0, then update stats.
p.visible.update_attribute(:replies, p.replies - 1)
end
end
You can do p.visible_changed? to see if it changed. You can also see what it changed from and what it changed to with other methods that ActiveModel::Dirty gives you, which is available by default on all ActiveRecords.

Rails and Authlogic. Show currently logged in users

I would like to have the list of currently logged in users.
This code doesn't work :
<% UserSession.all.each do |user_session| %>
<% end %>
#syed-aslam has a good solution, but you could just let Authlogic do the work. Check out the module Authlogic::ActsAsAuthentic::LoggedInStatus which defines two scopes: logged_in, logged_out
Your code becomes:
<% User.logged_in.each do |user| %>
<% end %>
P.S. I would normally link to the RDoc instead of source code, but the RDoc seems to have problems at the moment.
Authlogic gives you all kind of automatic columns that you don’t really need to update or maintain on your own, they are maintained by the actual code flow of Authlogic itself.
Those fields can contain some basic functionality related issues like the number of login attempts made, the ip address from which the attempt was made an or even what was the ip address the last time that user logged in. fun.
The magic column that will help us find who is probably online is the one called last_request_on, which basically indicates when was the last time that user made a request to your application.
The second parameter we’ll need in order to make a more accurate selection, is the configuration option named logged_in_timeout, which sets the timeout after which a stale session will be expired, by default it will expire after 10 minutes.
so if you set your session expiry to 30 minutes:
class User << ActiveRecord::Base
acts_as_authentic do |c|
c.logged_in_timeout 30.minutes
end
end
searching for those users is pretty easy:
module OnlineUsers
def count_online_users
User.count(:conditions => ["last_request_at > ?", 30.minutes.ago])
end
end
Why not creating a field called currently_active in the user model and update it to true once a session is created, and update to false once the session is destroy.
You can then call User.where(currently_active: true) gives you the users that online.
You cannot get UserSession for all user, UserSession is created every time user sends request and is not remembered between requests.
However you can show users which logged in some period of time (if you have last_logged_in column updated on every signin)
Logged in last 15 minutes:
<% User.find("last_logged_in < ?", 15.minutes.ago ).each do |user| %>
I wrote after_create and before_destroy callbacks in UserSession model.
In after_create callback, I wrote the user id of the user logging in to a text file (self.user.id) and in before_create callback I deleted the same. To check the activity of the user, I read the text file and checked the presence of the user id in that file.

Rails subscription limitations

I have an application with set limits on subscription attributes i/e a user can have five projects for subscription A but have ten for subscription B
At present I run a check on there current usage when linking to the create action and if they are over there limit I do not display the link to create a new project. On the view (for the new project ) I again run the check (in a helper) to see if they can create a new project and if they are not I display a message stating so and a little upgrade link.
Is this a secure method of stopping a user bypassing there subscription attribute limitations ?
What about direct PUT requests etc ?
You can also validate that the user's subscription allows starting a new project when a new project is created. This guarantee that even if they posted directly to the new_project_path they would get an error.
class Project
belongs_to :user
validate_on_create :subscription_allows_new_project
def subscription_allows_new_project
unless self.user.subscription.max_projects > self.user.projects.count
errors.add_to_base("Project limit reached, please upgrade today!")
end
end
end
If you're really cautious about the put requests, you could simply create a helper method that you call in all of the pages.
<% if has_user_hit_project_limits %>
Upgrade Now!
<% else %>
Add project
<% end %>
def has_user_hit_project_limits
if #logic
true
else
false
end
end

Resources