rails increment non-primary ID column in after create callback - ruby-on-rails

I'm trying to create a record and increment two non primary id columns in an after create callback with a integer and a date. The first column needs to be auto incremented with an integer from 1 to given number. The second column needs to be incremented with a date from the start date specified and by either 7, 14, or 30 days which is also specified. I tried creating the first record with the first value then incrementing from there but all records just have the same integer or date saved.
Here's the code
def create_positions
#slots = (self.slots - 1)
#payout_date = (self.start_date)
#position = Position.create(:susu_id => self.id, :user_id => 2, :position_number => 1, :pay_in => self.contribution, :payout_date => (#payout_date + self.frequency_in_days.days))
#positions= self.positions.map { |p| p["position_number"] }.last.to_i
#position_number = (#positions += 1)
#dates = self.positions.map { |d| d["payout_date"] }.last
#add_date = (#dates + self.frequency_in_days.days)
#all_positions = #slots.times {Position.create(:susu_id => self.id, :user_id => 2, :position_number => #position_number, :pay_in => self.contribution, :payout_date => #add_date) }
end
What i get is: 1 2 2 2 and 1/2/14 1/9/14 1/9/14 1/9/14
instead of: 1 2 3 4 and 1/2/14 1/9/14 1/16/14 1/23/14

My lack of reputation prohibits me from commenting on your original post, so I'll just have to do my best to answer your question without asking for clarification (for I don't fully understand your explanation of what you're trying to do).
It sounds like you are trying to create four Position instances, each subsequent position having a position_number that is one greater than the previous one and having a payout_date that is 7 days later than the previous one.
If this is what you're trying to do, please consider the following:
# in the top of your susu model:
after_create :create_positions
# later in your susu model
def create_positions
4.times do |n|
Position.create(susu: self,
user_id: 2,
position_number: n,
pay_in: contribution,
payout_date: start_date + n * frequency_in_days.days)
end
end
You have a lot of logic in your version of this method, so let me know if I have missed something that you need.
And finally, let me point out a few things about my version of this code:
It uses Ruby 1.9 hash syntax ("susu: self" instead of ":susu => self")
It removes unnecessary calls to self, which is considered bad ruby style
It still has hardcoded user_id: 2, which seems like you're going to need to change at some point
Doing this in an after_create hook will be in the same transaction as the original object being created. This means that if a Position cannot be created for whatever reason (e.g., a database-level unique key or a validates_uniqueness_of on the Position model failing), it will roll back the creation of the original object. If you don't like that behavior you can use the after_commit callback.
Cheers!

Related

How to compare objects in array RUBY

How to compare objects with the same parameter in ruby? How to define elsif part?
def compare
array_of_items = #items.map(&:object_id)
if array_of_items.uniq.size == array_of_items.size #array has only uniq vlaues - it's not possible to duplicate object - good!
return
elsif
#the comparision of objects with the same object_id by other param (i.e. date_of_lease param). The part I can not formulate
else
errors.add('It is not possible to purchase many times one item with the same values')
end
end
You can use Enumerable#group_by, e.g.
elsif #items.group_by(&:date_of_lease).count == array_of_items.size
As far as I understand, I guess you want to compare two objects with same object_id.
same_objects = #items.select { | element |
if array_of_items.count(element.object_id) > 1 do
# Duplicate object
end
}
I don't know how about other Ruby implementations, but in MRI Object#object_id returns unique (integer representation of the object in memory) value for every object. If you try to redefine it, you will get the warning:
class Object
def object_id
'a'
end
end
#=> warning: redefining `object_id' may cause serious problems
:object_id
First of all, since this is tagged as rails, isn't this the type of thing you can solve with a built in validation?
validates_uniqueness_of :date_of_lease, scope: :object_id
I don't know know about your implementation, but if you used the primary key of your database you might not even need that scope.
Otherwise, assuming you have overriden ruby object_id so two objects can have the same ids (¿?) I can only think of something complex like:
def compare
duplicate_items = #items.group_by(&:object_id).select { |k,v| v.size > 1}
if duplicate_items.keys.empty?
return
elsif duplicate_items.select{|k,v| v.group_by(&:date_of_lease).count != v.count}.empty?
# There are no duplicate object ids that also have duplicate
# dates of lease between themselves
else
errors.add('It is not possible to purchase many times one item with the same values')
end
end
Check that you have to handle the case where there are different object ids with the same date of lease in the same items array that has duplicates, that should be valid. For example : Item id 1, date 12, Item id 1, date 13, item id 2, date 12 Should be valid.

Rails Everyday Action

I need the system to run the following code everyday but I don't know how to do accomplish this.
#user = User.all
date = Date.today
if date.workingday?
#user.each do |user|
if !Bank.where(:user_id => user.id , :created_at => (date.beginning_of_day..date.end_of_day) , :bank_type => 2 ).exists?
banco = Bank.new()
banco.user = user
banco.bank_type = 2
banco.hours = 8
banco.save
end
end
end
The most conventional way is to set this up to be executed with rails runner in a cron job.
There are tools like whenever that make it easier to create these jobs by defining how often they need to be executed in Ruby rather than in the peculiar and sometimes difficult to understand crontab format.
As a note, User.all is a very dangerous thing to do. As the number of users in your system grows, loading them all into memory will eventually blow up your server. You should load them in groups of 100 or so to avoid overloading the memory.
Additionally, that where clause shouldn't be necessary if you've set up proper has_many and belongs_to relationships here. I would expect this could work:
unless (user.bank)
user.create_bank(
bank_type: 2,
hours: 8
)
end
It's not clear how created_at factors in here. Are these assigned daily? If so, that should be something like bank_date as a DATE type column, not date and time. Using the created_at timestamp as part of the relationship is asking for trouble, that should reflect when the record was created, nothing more.

Rails App Plotting Zeros in Highchart Graph via Lazy_High_Charts Gem

I'm still very new to Rails but moving along fairly smoothly I would say. So for practice I'm working on what's supposed to be a simple application where a user can input their weight and that info, over a 30 day period, is displayed to them via a Highcharts line graph using the Lazy Highcharts gem. I followed Ryan Bates' Railscast #223 to get started.
My Issue:
Ok, so the inputs are showing up except that on the days that a user doesn't input a value it gets displayed on the chart as '0' (the line drops to bottom of the graph), instead of connecting to the next point on any given day. Not sure if all that makes sense so here's a screenshot:
I found this solution:
Highcharts - Rails Array Includes Empty Date Entries
However, when I implement the first option found on that page (convert 0 to null):
(30.days.ago.to_date..Date.today).map { |date| wt = Weight.pounds_on(date).to_f; wt.zero? ? "null" : wt }
the points show up but the line does not, nor do the tooltips...this leads me to think that something is breaking the js. Nothing is apparently wrong in the console though..? From here I thought it might be a matter of using Highchart's 'connectNulls' option but that didn't work either. When implementing the second option (reject method):
(30.days.ago.to_date..Date.today).map { |date| Weight.pounds_on(date).to_f}.reject(&:zero?)
it completely removes all dates that are null from the chart, which messes up the structure completely because the values are supposed to be displayed based on their created_at date.
So back to square one, this is what I'm left with (chart plotting zeros for days without inputs).
Model:
class Weight < ActiveRecord::Base
attr_accessible :notes, :weight_input
validates :weight_input, presence: true
validates :user_id, presence: true
belongs_to :user
def self.pounds_on(date)
where("date(created_at) = ?", date).pluck(:weight_input).last
end
end
Controller:
def index
#weights = current_user.weights.all
#startdate = 30.days.ago.to_date
#pounds = (30.days.ago.to_date..Date.today).map { |date| Weight.pounds_on(date).to_f }
#h = LazyHighCharts::HighChart.new('graph') do |f|
f.options[:title][:text] = " "
f.options[:chart][:defaultSeriesType] = "area"
f.options[:chart][:inverted] = false
f.options[:chart][:zoomType] = 'x'
f.options[:legend][:layout] = "horizontal"
f.options[:legend][:borderWidth] = "0"
f.series(:pointInterval => 1.day, :pointStart => #startdate, :name => 'Weight (lbs)', :color => "#2cc9c5", :data => #pounds )
f.options[:xAxis] = {:minTickInterval => 1, :type => "datetime", :dateTimeLabelFormats => { day: "%b %e"}, :title => { :text => nil }, :labels => { :enabled => true } }
end
respond_to do |format|
format.html # index.html.erb
format.json { render json: #weights }
end
end
Does anyone have a solution for this? I guess I could be going about this all wrong so any help is much appreciated.
This is a HighCharts specfic. You need to pass in the timestamp into your data array vs. defining it for the dataset.
For each data point, I set [time,value] as a tuple.
[ "Date.UTC(#{date.year}, #{date.month}, #{date.day})" , Weight.pounds_on(date).to_f ]
You will still need to remove the zero's in w/e fashion you like, but the data will stay with the proper day value.
I do think you need to remove :pointInterval => 1.day
General tips
You should also, look at optimizing you query for pound_on, as you are doing a DB call per each point on the chart. Do a Weight.where(:created_at => date_start..date_end ).group("Date(created_at)").sum(:weight_input) which will give you an array of the created_at dates with the sum for each day.
ADDITIONS
Improved SQL Query
This leans on sql to do what it does best. First, use where to par down the query to the records you want (in this case past 30 days). Select the fields you need (created_at and weight_input). Then start an inner join that runs a sub_query to group the records by day, selecting the max value of created_at. When they match, it kicks back the greatest (aka last entered) weight input for that given day.
#last_weight_per_day = Weight.where(:created_at => 30.days.ago.beginning_of_day..Time.now.end_of_day)
select("weights.created_at , weights.weight_input").
joins("
inner join (
SELECT weights.weight_input, max(weights.created_at) as max_date
FROM weights
GROUP BY weights.weight_input , date(weights.created_at)
) weights_dates on weights.created_at = weights_dates.max_date
")
With this you should be able #last_weight_per_day like so. This should not have 0 / nil values assuming you have validated them in your DB. And should be pretty quick at it too.
#pounds = #last_weight_per_day.map{|date| date.weight_input.to_f}

How to update a single attribute without touching the updated_at attribute?

How can I achieve this?
tried to create 2 methods, called
def disable_timestamps
ActiveRecord::Base.record_timestamps = false
end
def enable_timestamps
ActiveRecord::Base.record_timestamps = true
end
and the update method itself:
def increment_pagehit
update_attribute(:pagehit, pagehit+1)
end
turn timestamps on and off using callbacks like:
before_update :disable_timestamps, :only => :increment_pagehit
after_update :enable_timestamps, :only => :increment_pagehit
but it's not updating anything, even the desired attribute (pagehit).
Any advice? I don't want to have to create another table just to count the pagehits.
As an alternative to update_attribute, In Rails 3.1+ you can use update_column.
update_attribute skips validations, but will touch updated_at and execute callbacks.
update_column skips validations, does not touch updated_at, and does not execute callbacks.
Thus, update_column is a great choice if you don't want to affect updated_at and don't need callbacks.
See http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html for more information.
Also note that update_column will update the value of the attribute in the in-memory model and it won't be marked as dirty. For example:
p = Person.new(:name => "Nathan")
p.save
p.update_column(:name, "Andrew")
p.name == "Andrew" # True
p.name_changed? # False
If all you're wanting to do is increment a counter, I'd use the increment_counter method instead:
ModelName.increment_counter :pagehit, id
Is there a way to avoid automatically updating Rails timestamp fields?
Or closer to your question:
http://blog.bigbinary.com/2009/01/21/override-automatic-timestamp-in-activerecord-rails.html
it is not a good idea to do this:
self.class.update_all({ pagehit: pagehit+1 }, { id: id })
it should be
self.class.update_all("pagehit = pagehit + 1", { id: id })
the reason is if two requests are parallel, on the first version both will update the pagehits with the same number, as it uses the number saved in the Ruby memory. The second option uses the sql server to increase the number by 1, in case two of these queries come at the same time, the server will process them one after the other, and will end up with the correct number of pagehits.
To avoid Monkeypatchingtroubles you could also use ModelName.update_all for this purpose:
def increment_pagehit
self.class.update_all({ pagehit: pagehit+1 }, { id: id })
end
This also does not touch the timestamps on the record.
You also have decrement and increment (and their bang versions) which do not alter updated_at, do not go trigger validation callbacks and are obviously handy for counters / integers.
If precision is not really that important, and you don't expect the code to run many times, you can try altering the saved in the database updated_at value, like so:
u = User.first
u.name = "Alex 2" # make some changes...
u.updated_at = u.updated_at + 0.000001.second # alter updated_at
u.save
so that Rails will actually try to save the same value, and not replace it with Time.now.

Rails Replace Attributes in Arrays

I have a list of incorrect cities name in Philippines:
>> a = City.find_all_by_country_id(4)
=> [#<City id: 91, name: "Alaminos", country_id: 4, created_at: "2009-11-12 04:06:14", updated_at: "2009-11-12 04:06:14">, #<City id: 92, name: "Angeles", country_id: 4, created_at: "2009-11-12 04:06:14", ...
And I wanted to replace all the names with the correct one:
=> b = ["Abra", "Agusan del Norte", "Agusan del Sur", ...
I wanted to use the replace method because I wanted to update the existing city id, inserting/truncating them only if necessary.
But I still can't figure this one out, since a is an array of arrays (correct me if I am wrong) while b is just a simple, down-to-earth array.
a should be an array of City models. For example if you wanted to change the city name of the city id 91 (the first record) to "Abra" (the first element in the array) you would just do a[0].name = b[0]. I'm a little unclear on what exactly you're trying to do, but hopefully this will get you over the syntax part of the problem.
Have a look at at Array#zip, and ActiveRecord::Base#update_attribute. As Andy Gaskell points out, a is an array of City objects. So zip can be called on a, and update_attribute can be called on any element of a.
The short simple way of doing what you want is this:
a.zip(b){|array| array[0].update_attribute(:name, array[1])}
Zip will turn multiple arrays into an array of arrays. Where each index of the new array is an array composed of elements in the source arrays of the same index.
a.zip(b) = c #=> ∀i: c[i] = [a[i],b[i]]
If you pass a block to zip, Ruby will yield each array in c to the block. It's a handy shortcut for a.zip(b).collect(&block).
In the code above, array[0] = a[i], and array[1] = b[i], each iteration supplies a different value of i. update_attributes will update the record in the database bypassing validations and callbacks.
Caveats:
If b has more elements then a you will get No Method Errors.
If a has more elements than b, the extra elements will have their name set to "".
Again, Update_attributes bypasses validations and saves the updated record automatically. If this is not too your liking you can replace the innards of the block with:
array[0].name = array[1]; array[0].save
I decided to use a migration file instead, so this is the code:
class AddProvinces < ActiveRecord::Migration
def self.up
philippines = Country.find_by_name('Philippines')
old_cities = philippines.cities
new_cities = (['Abra', 'Agusan del Norte', 'And all the cities...'])
old_cities.each do |c|
unless new_cities.blank?
City.update(c.id, :name => new_cities.first)
new_cities.delete(new_cities.first)
else
City.delete(c.id)
end
end
unless new_cities.blank?
new_cities.each do |c|
City.create(:name => c, :country_id => 'philippines.id')
end
end
end
def self.down
end
end

Resources