Using ActiveRecord callbacks - ruby-on-rails

Using Rails 6 and Ruby 2.7
I run calculations using callbacks, which works on create; however, this does not work on update. Unless marked complete, my calculation gets stuck in a loop. So, in order to edit/update I'm attempting to use the before_update to mark complete as false, and then run the same calculation as create and then marking it back to true. I can get it to break the loop and update the attributes, however, it does not run the calculations like it does upon create.
I've been trying so many variations to get this to work and feel absolutely lost. I've tried various callbacks and have gone through documentation and have been searching for an answer. If anyone can give me a clue, hint, or any insight, please. I am not receiving any errors of any kind.
For the .previous method, I am using the By_star gem and for the .second_to_last method, I am using the groupdate gem.
Thank you for your time.
belongs_to :user
belongs_to :store
after_create :create_calculations
before_update :before_update_calculations
after_update :update_calculations
def create_calculations
store = Store.find(self.store_id)
sheet = Sheet.find(self.id)
past_sheet = store.sheets.second_to_last
unless store.sheets.last
s = past_sheet.draw - sheet.returned
else
s = sheet.sold
end
unless past_sheet
s = store.sheets.last.draw - sheet.returned
else
s = past_sheet.draw - sheet.returned
end
if sheet.complete != true && sheet.store.vending_box == true
sheet.update(complete: true)
sheet.update(sold: s)
short = (sheet.sold * 0.75) - sheet.collected
sheet.update(:shortage => short)
pilferage = (sheet.shortage / 0.75)
sheet.update(:pilferage => pilferage)
elsif sheet.complete != true && sheet.store.vending_box == false
sheet.update(complete: true)
sheet.update(sold: s)
short = (sheet.sold * 0.55) - sheet.collected
sheet.update(:shortage => short)
pilferage = (sheet.shortage / 0.55)
sheet.update(:pilferage => pilferage)
else
end
end
def before_update_calculations
unless Sheet.find(self.id).complete == true
Sheet.find(self.id).update(complete: false)
end
end
def update_calculations
store = Store.find(self.store_id)
sheet = Sheet.find(self.id)
past_sheet = store.sheets.find(self.id).previous
# will add back conditionals to deal with the .previous method, for when
# it's the first record and as no previous item, such as in the create.
s = past_sheet.draw - sheet.returned
if sheet.complete != true && sheet.store.vending_box == true
sheet.update(complete: true)
sheet.update(:sold => s)
short = (sheet.sold * 0.75) - sheet.collected
sheet.update(:shortage => short)
pilferage = (sheet.shortage / 0.75)
sheet.update(:pilferage => pilferage)
elsif sheet.complete != true && sheet.store.vending_box == false
sheet.update(complete: true)
sheet.update(:sold => s)
short = (sheet.sold * 0.55) - sheet.collected
sheet.update(:shortage => short)
pilferage = (sheet.shortage / 0.55)
sheet.update(:pilferage => pilferage)
else
end
end
end```

You call update in your after_update which starts a new cycle of update callbacks.
I'd rather use methods which I call explicitly over callbacks but if you want to stick to callbacks, try assigning values in a before_update callback the data will be updated once.

A good way to avoid infinite update loops on callbacks, is to use a variable that is only used to that purpose. This variable should work as a flag. When up, run the callback. Otherwise, just skip it.
Thus, you could do, for your example, do the following:
attr_accessor :this_is_the_flag
after_update update_calculations, :if => "this_is_the_flag.nil?"
def update_calculations
[...]
self.this_is_the_flag = true # invalidate the callback
end
This is the general idea, you can adjust this to your need. Also, please, make sure the syntax is correct, because callback syntax tends to change regularly.

Related

How to refactor complicated logic in create_unique method?

I would like to simplify this complicated logic for creating unique Track object.
def self.create_unique(p)
f = Track.find :first, :conditions => ['user_id = ? AND target_id = ? AND target_type = ?', p[:user_id], p[:target_id], p[:target_type]]
x = ((p[:target_type] == 'User') and (p[:user_id] == p[:target_id]))
Track.create(p) if (!f and !x)
end
Here's a rewrite of with a few simple extract methods:
def self.create_unique(attributes)
return if exists_for_user_and_target?(attributes)
return if user_is_target?(attributes)
create(attributes)
end
def self.exists_for_user_and_target?(attributes)
exists?(attributes.slice(:user_id, :target_id, :target_type))
end
def self.user_is_target?(attributes)
attributes[:target_type] == 'User' && attributes[:user_id] == attributes[:target_id]
end
This rewrite shows my preference for small, descriptive methods to help explain intent. I also like using guard clauses in cases like create_unique; the happy path is revealed in the last line (create(attributes)), but the guards clearly describe exceptional cases. I believe my use of exists? in exists_for_user_and_target? could be a good replacement for find :first, though it assumes Rails 3.
You could also consider using uniqueness active model validation instead.
##keys = [:user_id, :target_id, :target_type]
def self.create_unique(p)
return if Track.find :first, :conditions => [
##keys.map{|k| "#{k} = ?"}.join(" and "),
*##keys.map{|k| p[k]}
]
return if p[##keys[0]] == p[##keys[1]]
return if p[##keys[2]] == "User"
Track.create(p)
end

Override setter doesn't work with update_attributes

I'm making an task-manager and have an boolean attribute for 'finished'. I've tried to override the setter to implement an 'finished_at' date when i toggle 'finished' to true.
But i getting some mixed result. It doesn't work in browser but it will work in my rspec test.
Please help me out.
class TasksController < ApplicationController
# ...
def update
# ..
if #task.update_attributes(params[:task]) # where params[:task][:finished] is true
# ...
end
class Task < ActiveRecord::Base
#...
def finished=(f)
write_attribute :finished, f
write_attribute :finished_at, f == true ? DateTime.now : nil
end
end
# and in rspec i have
describe "when marked as finished" do
before { #task.update_attributes(finished: true) }
its(:finished_at) { should_not be_nil }
its(:finished_at) { should > (DateTime.now - 1.minute) }
describe "and then marked as unfinished" do
before { #task.update_attributes(finished: false) }
its(:finished_at) { should be_nil }
end
end
in browser it executes "UPDATE "tasks" SET "finished" = 't', "updated_at" = '2012-10-02 18:55:07.220361' WHERE "tasks"."id" = 17"
and in rails console i got the same with update_attributes.
But in rspec with update_attributes i get "UPDATE "tasks" SET "finished" = 't', "finished_at" = '2012-10-02 18:36:47.725813', "updated_at" = '2012-10-02 18:36:51.607143' WHERE "tasks"."id" = 1"
So I use the same method but it's only working in rspec for some reson...
using latest rails and latest spec (not any rc or beta).
Solution
Not mush i did need to edit. Thanks #Frederick Cheung for the hint.
I did notice i did like "self[:attr]" more than "write_attribute". Looks better imo.
def finished=(value)
self[:finished] = value
self[:finished_at] = (self.finished? ? Time.now.utc : nil)
end
Your setter is passed the values as they are passed to update_attributes. In particular when this is triggered by a form submission (and assuming you are using the regular rails form helpers) f will actually be "0" or "1", so the comparison with true will always be false.
The easiest thing would be to check the value of finished? after the first call to write_attribute, so that rails can convert the submitted value to true/false. It's also unrubyish to do == true - this will break if the thing you are testing returns a truthy value rather than actually true (for example =~ on strings returns an integer when there is a match)
You could use ActiveRecord Dirty Tracking to be notified of this change.
http://api.rubyonrails.org/classes/ActiveModel/Dirty.html
class Task < ActiveRecord::Base
before_save :toggle_finished_at
def toggle_finished_at
if finished_changed?
before = changes['finished'][0]
after = changes['finished'][1]
# transition from finished => not-finished
if before == true && after == false
self.finished_at = nil
end
# transition from not finished => finished
if before == false && after == true
self.finished_at = Time.now.utc
end
end
end
end
This is a use case for a state machine. You call a :finish! event (a method) which is configured to change the state and to do whatever else needed.
https://github.com/pluginaweek/state_machine/

Refactoring a mongoid call

Right now, i am using 2 different but very similar queries (difference is just an additional criteria
pop_answers = Answer.any_of(
{:num_likes.gte=>3, :image_filename.exists=>true},
).desc(:created_at).skip(to_skip).limit(per_page).map{|a|a}
pop_answers_in_topic = Answer.any_of(
{:num_likes.gte=>3, :image_filename.exists=>true, :topic_id=>some_id},
).desc(:created_at).skip(to_skip).limit(per_page).map{|a|a}
How can i refactor this?
You could add a class method to Answer:
def self.popular(offset, limit, for_topic_id = nil)
conditions = { :num_likes.gte => 3, :image_filename.exists => true }
conditions[:topic_id] = for_topic_id if(for_topic_id)
any_of(conditions).desc(:created_at).skip(offset).limit(limit).map{|a|a}
end
Or if you're expecting more than just a topic ID:
def self.popular(offset, limit, options = { })
conditions = { :num_likes.gte => 3, :image_filename.exists => true }.merge(options)
any_of(conditions).desc(:created_at).skip(offset).limit(limit).map{|a|a}
end
I don't use Mongoid but you might be able to drop the funny .map{|a|a} or use .to_a instead.
Or perhaps something scope-ish:
# In answer.rb
def self.popular(for_topic_id = nil)
conditions = { :num_likes.gte => 3, :image_filename.exists => true }
conditions[:topic_id] = for_topic_id if(for_topic_id)
any_of(conditions)
end
# And then where you're using it...
pop_answers = Answer.popular.desc(:created_at).skip(to_skip).limit(per_page).map{|a|a}
pop_in_topic = Answer.popular(some_id).desc(:created).skip(to_skip).limit(per_page).map{|a|a}
And I have to wonder if any_of is really what you're looking for here, perhaps all_of would make more sense.

Rails - Triggering Flash Warning with method returning true

I'm trying to trigger a warning when a price is entered too low. But for some reason, it always returns true and I see the warning regardless. I'm sure there something wrong in the way I'm doing this as I'm really new to RoR.
In model:
def self.too_low(value)
res = Class.find_by_sql("SELECT price ……. WHERE value = '#{value}'")
res.each do |v|
if #{value} < v.price.round(2)
return true
else
return false
end
end
end
In controller:
#too_low = Class.too_low(params[:amount])
if #too_low == true
flash[:warning] = 'Price is too low.'
end
I would write it somewhat different. You iterate over all items, but you are only interested in the first element. You return from inside the iteration block, but for each element the block will be executed. In ruby 1.9.2 this gives an error.
Also i would propose using a different class-name (Class is used to define a class)
So my suggestion:
Class YourGoodClassName
def self.too_low(amount)
res = YourGoodClassName.find_by_sql(...)
if res.size > 0
res[0].price.round(2) < 1.00
else
true
end
end
end
You can see i test if any result is found, and if it is i just return the value of the test (which is true or false); and return true if no price was found.
In the controller you write something like
flash[:warning] = 'Price is too low' if YourGoodClassName.too_low(params[:amount])

Optimizing ActiveRecord Point-in-Polygon Search

The following PiP search was built for a project that lets users find their NYC governmental districts by address or lat/lng (http://staging.placeanddisplaced.org). It works, but its kinda slow, especially when searching through districts that have complex polygons. Can anyone give me some pointers on optimizing this code?
One thought I had was to run the point_in_polygon? method on a simplified version of each polygon, i.e. fewer coordinates. This would mean less processing time, but also decreased accuracy.. thoughts?
class DistrictPolygonsController < ApplicationController
def index
...
if coordinates?
#district_polygons = DistrictPolygon.
coordinates_within_bounding_box(params[:lat], params[:lng]).
find(:all, :include => :district, :select => select).
select { |dp| dp.contains_coordinates?(params[:lat], params[:lng]) }
else
#district_polygons = DistrictPolygon.find(:all, :include => :district, :select => select)
end
...
end
end
class DistrictPolygon < ActiveRecord::Base
named_scope :coordinates_within_bounding_box, lambda { |lat,lng| { :conditions => ['min_lat<? AND max_lat>? AND min_lng<? AND max_lng>?', lat.to_f, lat.to_f, lng.to_f, lng.to_f] } }
named_scope :with_district_type, lambda { |t| { :conditions => ['district_type=?', t] } }
before_save :get_bounding_box_from_geometry
def get_bounding_box_from_geometry
# return true unless self.new_record? || self.geometry_changed? || self.bounds_unknown?
self.min_lat = all_lats.min
self.max_lat = all_lats.max
self.min_lng = all_lngs.min
self.max_lng = all_lngs.max
true # object won't save without this return
end
def bounds_unknown?
%w(min_lat max_lat min_lng max_lng).any? {|b| self[b.to_sym].blank? }
end
def bounds_known?; !bounds_unknown?; end
# Returns an array of XML objects containing each polygons coordinates
def polygons
Nokogiri::XML(self.geometry).search("Polygon/outerBoundaryIs/LinearRing/coordinates")
end
def multi_geometric?
Nokogiri::XML(self.geometry).search("MultiGeometry").size > 0
end
# Returns an array of [lng,lat] arrays
def all_coordinates
pairs = []
polygons.map do |polygon|
polygon.content.split("\n").map do |coord|
# Get rid of third 'altitude' param from coordinate..
pair = coord.strip.split(",")[0..1].map(&:to_f)
# Don't let any nils, blanks, or zeros through..
pairs << pair unless pair.any? {|point| point.blank? || point.zero? }
end
end
pairs
end
# All latitudes, regardless of MultiPolygonal geometry
def all_lats
all_coordinates.map(&:last).reject{|lat| lat.blank? || lat.zero?}
end
# All longitudes, regardless of MultiPolygonal geometry
def all_lngs
all_coordinates.map(&:first).reject{|lng| lng.blank? || lng.zero?}
end
# Check to see if coordinates are in the rectangular bounds of this district
def contains_coordinates?(lat, lng)
return false unless coordinates_within_bounding_box?(lat.to_f, lng.to_f)
polygons.any? { |polygon| DistrictPolygon.point_in_polygon?(all_lats, all_lngs, lat.to_f, lng.to_f) }
end
def coordinates_within_bounding_box?(lat, lng)
return false if (max_lat > lat.to_f == min_lat > lat.to_f) # Not between lats
return false if (max_lng > lng.to_f == min_lng > lng.to_f) # Not between lngs
true
end
# This algorithm came from http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
def self.point_in_polygon?(x_points, y_points, x_target, y_target)
num_points = x_points.size
j = num_points-1
c = false
for i in 0...num_points do
c = !c if ( ((y_points[i]>y_target) != (y_points[j]>y_target)) && (x_target < (x_points[j]-x_points[i]) * (y_target-y_points[i]) / (y_points[j]-y_points[i]) + x_points[i]) )
j = i
end
return c
end
end
If your runtime is longer for more complex shapes, it suggests the performance is in the O(n) loop in the point_in_polygon?
Does profiling back that assumption up?
If performance is critical, consider implementing the exact same algorithm as native code.
I suspect you may be able to push the majority of the work into the DB. PostgreSQL has the PostGIS plugin which enables spatially aware queries to be performed.
PostGIS: http://postgis.refractions.net/
Docs: http://postgis.refractions.net/documentation/manual-1.4/
This breaks the database portability concept, but might be worth it if performance is critical.
Algorithm aside, keeping the polygon data in local memory and recoding this in a statically typed compiled language will likely lead to 100x-1000x increase in speed.

Resources