rails: Accessing member variables in after_create - ruby-on-rails

I am building a survey app where, based on ratings I need certain things to happen.
Basically, if a survey is submitted with a total rating under 15, we need to notify a supervisor. That's easy enough with mailers, but I can't seem to access the rating data in an after_create method.
My model has 5 fields named A,B,C,D, and E which are integers and they hold the rating data in the form.
I have tried :notation I have tried self.notation, I've tried after_create(service) service.notation and nothing works - the email never gets sent because it doesn't realize that the rating is lower than 15.
I also have a checkbox with similar issues. In the database it appears as "true" but before it is saved it usually shows up as 1 so testing for the correct value is tricky. Similar to the code below, I can't access it's value either. I've listed all the various methods I've tried with no success.
Obviously these are not all existent in the model at the same time, they are listed below as examples of what I have attempted
How do I access these data values in an after_create call?!
class Service < ActiveRecord::Base
after_create :lowScore
def lowScore
if(A+B+C+D+E) < 15 #does not work
ServiceMailer.toSupervisor(self).deliver
end
end
def lowScore
if(self.A+self.B+self.C+self.D+self.E) < 15 #does not work either
ServiceMailer.toSupervisor(self).deliver
end
end
#this does not work either!
def after_create(service)
if service.contactMe == :true || service.contactMe == 1
ServiceMailer.contactAlert(service).deliver
end
if (service.A + service.B + service.C + service.D + service.E) < 15
ServiceMailer.toSupervisor(service).deliver
ServiceMailer.adminAlert(service).deliver
end
end

Figured out a solution.
In model.rb:
after_create :contactAlert, :if => Proc.new {self.contactMe?}
after_create :lowScore, :if => Proc.new {[self.A, self.B, self.C, self.D, self.E].sum < 15}
def contactAlert
ServiceMailer.contactAlert(self).deliver
end
def lowScore
ServiceMailer.adminAlert(self).deliver
ServiceMailer.toSupervisor(self).deliver
end
The key was using the Proc.new to do the tests for conditions.

Do debug:
class Service < ActiveRecord::Base
after_create :low_score
def low_score
# raise (A+B+C+D+E).inspect # uncomment this line to debug your code
# it will raise exception with string containing (A+B+C+D+E). See what is result this line in your console tab where rails server started
# Or you can see result in your browser for this raise
ServiceMailer.toSupervisor(self).deliver if (A+B+C+D+E) < 15
end
end

Related

Safest way to override the update method of a model

I have the following model:
class TwitterEngagement < ApplicationRecord
end
And I would like to override create (and create!), update (and
update!) methods of it so no one can manually entry fake data. I would like the help of someone more experienced with active record and rails so I don't mess anything up. Right now what I have is:
class TwitterEngagement < ApplicationRecord
belongs_to :page
def create
super(metrics)
end
def update
super(metrics)
end
private
def metrics
client.get_engagements(page.url)
def client
TwitterClient.new
end
end
Thank you.
TL;DR:
class FacebookEngagement < ApplicationRecord
def create_or_update(*args, &block)
super(metrics)
end
Probably depends on your Rails version, but I traced the ActiveRecord::Persistence sometime before in Rails 5, and found out that both create and update eventually calls create_or_update.
Suggestion:
If ever possible, I'll just do a validation, because it kinda makes more sense because you are validating the inputs, and then probably set an optional readonly?, to prevent saving of records. This will also prevent "silent failing" code / behaviour as doing TL;DR above would not throw an exception / populate the validation errors, if say an unsuspecting developer does: facebook_engagement.update(someattr: 'somevalue') as the arguments are gonna basically be ignored because it's instead calling super(metrics), and would then break the principle of least surprise.
So, I'll probably do something like below:
class FacebookEngagement < ApplicationRecord
belongs_to :page
validate :attributes_should_not_be_set_manually
before_save :set_attributes_from_facebook_engagement
# optional
def readonly?
# allows `create`, prevents `update`
persisted?
end
private
def attributes_should_not_be_set_manually
changes.keys.except('page_id').each do |attribute|
errors.add(attribute, 'should not be set manually!')
end
end
def set_attributes_from_facebook_engagement
assign_attributes(metrics)
end
def metrics
# simple memoization to prevent wasteful duplicate requests (or remove if not needed)
#metrics ||= graph.get_object("#{page.url}?fields=engagement")
end
def graph
Koala::Facebook::API.new
end
end

How to add errors before updating attributes?

I'm trying to handle the situation where the user has entered info incorrectly, so I have a path that follows roughly:
class Thing < AR
before_validation :byebug_hook
def byebug_hook
byebug
end
end
thing = Thing.find x
thing.errors.add(:foo, "bad foo")
# Check byebug here, and errors added
if thing.update_attributes(params)
DelayedJobThatDoesntLikeFoo.perform
else
flash.now.errors = #...
end
byebug for byebug_hook> errors.messages #=> {}
Originally I thought that maybe the model was running its own validations and overwriting the ones I added, but as you can see even when I add the before hook the errors are missing, and I'm not sure what's causing it
ACTUAL SOLUTION
So, #SteveTurczyn was right that the errors needed to happen in a certain place, in this case a service object called in my controller
The change I made was
class Thing < AR
validate :includes_builder_added_errors
def builder_added_errors
#builder_added_errors ||= Hash.new { |hash, key| hash[key] = [] }
end
def includes_builder_added_errors
builder_added_errors.each {|k, v| errors.set(k, v) }
end
end
and in the builder object
thing = Thing.find x
# to my thinking this mirrors the `errors.add` syntax better
thing.builder_added_errors[:foo].push("bad foo") if unshown_code_does_stuff?
if thing.update_attributes(params)
DelayedJobThatDoesntLikeFoo.perform
else
flash.now.errors = #...
end
update_attributes will validate the model... this includes clearing all existing errors and then running any before_validation callbacks. Which is why there are never any errors at the pont of before_validation
If you want to add an error condition to the "normal" validation errors you would be better served to do it as a custom validation method in the model.
class Thing < ActiveRecord::Base
validate :add_foo_error
def add_foo_error
errors.add(:foo, "bad foo")
end
end
If you want some validations to occur only in certain controllers or conditions, you can do that by setting an attr_accessor value on the model, and setting a value before you run validations directly (:valid?) or indirectly (:update, :save).
class Thing < ActiveRecord::Base
attr_accessor :check_foo
validate :add_foo_error
def add_foo_error
errors.add(:foo, "bad foo") if check_foo
end
end
In the controller...
thing = Thing.find x
thing.check_foo = true
if thing.update_attributes(params)
DelayedJobThatDoesntLikeFoo.perform
else
flash.now.errors = #...
end

Recommended practice for passing current user to model

Given a model Orderstatus with attributes private_status:string, and private_status_history:json(I'm using Postgresql's json). I would like to record each status transition, together with the user who made the change.
Ideally it would be something like:
class Orderstatus < ActiveRecord::Base
after_save :track_changes
def track_changes
changes = self.changes
if self.private_status_changed?
self.private_status_history_will_change!
self.private_status_history.append({
type: changes[:private_status],
user: current_user.id
})
end
end
end
class OrderstatusController <ApplicationController
def update
if #status.update_attributes(white_params)
# Good response
else
# Bad response
end
end
end
#Desired behaviour (process not run with console)
status = Orderstatus.new(private_status:'one')
status.private_status #=> 'one'
status.private_status_history #=> []
status.update_attributes({:private_status=>'two'}) #=>true
status.private_status #=> 'two'
status.private_status_history #=> [{type:['one','two'],user:32]
What would be the recommended practice to achieve this? Apart from the usual one using Thread. Or maybe, any suggestion to refactor the structure of the app?
So, I finally settled for this option ( I hope it's not alarming to anyone :S)
class Orderstatus < ActiveRecord::Base
after_save :track_changes
attr_accessor :modifying_user
def track_changes
changes = self.changes
if self.private_status_changed?
newchange = {type:changes[:private_status],user: modifying_user.id}
self.update_column(:private_status_history,
self.private_status_history.append(newchange))
end
end
end
class OrderstatusController <ApplicationController
def update
#status.modifying_user = current_user # <---- HERE!
if #status.update_attributes(white_params)
# Good response
else
# Bad response
end
end
end
Notes:
- I pass the from the Controller to the Model through an instance attribute modifying_user of the class Orderstatus. That attribute is ofc not saved to the db.
- Change of method to append new changes to the history field. I.e. attr_will_change! + save to update_column + append

rails 3 caching site settings with ability to reload cache without server restart and third-party tools

I need some solution to make following functionality in my RoR 3 site:
Site needs a user rating system, where users get points for performing some actions (like getting points for answering questions on stackoverflow).
The problems are:
1) I need ability to re-assign amount of points for some actions (not so often, but I can't restart Mongrel each time I need to re-assign, so in-code constants and YAML don't suit)
2) I can't use simple Active Record, because on 5000 users I'll do too many queries for each user action, so I need caching, and an ability to reset cache on re-assignment
3) I would like to make it without memcached or something like this, cause my server hardware is old enough.
Does anyone know such solution?
What about something like this ?
##points_loaded = false
##points
def load_action_points
if (File.ctime("setting.yml") < Time.now) || !##points_loaded
##points = YAML::load( File.open( 'setting.yml' ) )
##points_loaded = true
else
##points
end
or use A::B and cache the DB lookups
class ActionPoints < ActiveRecord::Base
extend ActiveSupport::Memoizable
def points
self.all
end
memoize :points
end
You also cache the points in the User model, something like this .. pseudocode...
class User < A::B
serialize :points
def after_save
points = PointCalculator(self).points
end
end
and....
class PointCalculator
def initialize(user)
##total_points = 0
user.actions.each do |action|
p = action.times * ActionPoints.find_by_action("did_something_cool").points
##total_points = ##total_points + p
end
end
def points
##total_points
end
end
I've found some simple solution, but it works only if you have one RoR server instance:
class Point < ActiveRecord::Base
##cache = {}
validates :name, :presence => true
validates :amount, :numericality => true, :presence => true
after_save :load_to_cache, :revaluate_users
def self.get_for(str)
##cache = Hash[Point.all.map { |point| [point.name.to_sym, point] }] if ##cache == {} #for cache initializing
##cache[str.to_sym].amount
end
private
def load_to_cache
##cache[self.name.to_sym] = self
end
def revaluate_users
#schedule some background method, that will update all users' scores
end
end
If you know a solution for multiple server instances without installing memcached, I will be very glad to see it.

What executes first?

In my rails app I have a User model.
In that model I have some custom validation and a before save block as below
Class User < AvtiveRecord::Base
before_save :save_user
validate :validate_user
def save_user
self.guest = true if(!self.admin? && !self.guest)
end
def validate_user
errors.add(:age, "can't be less than 20") if self.age < 20
end
end
Now, I just wanted to know that whether the validate block executes first or the validate. Because there are other validations based on the user role. So if the validate block executes first and there are no validation errors and then the before save executes and modifies the values. Are those values again validated?
Thanks in Advance.
Validations are called before before_save callbacks. If you want it to execute before the validations then you can use before_validation_on_create or before_validation_on_update, like this:
class User < ActiveRecord::Base
before_validation_on_create :save_user
validate :validate_user
def save_user
self.guest = true if(!self.admin? && !self.guest)
end
def validate_user
errors.add(:age, "can't be less than 20") if self.age < 20
end
end
Those values will not be validated again. Validation happens once as does save, otherwise you could end up in a looping condition anytime you changed a value.
This is the guide you want: http://guides.rubyonrails.org/active_record_validations_callbacks.html
Based on section 10, it looks as if validation happens first.
I don't think the values will be validated again—there's nothing that would cause that to happen.

Resources