Rails doesn't allow me to set 'date' field to more than 30 days in advance - Help - Rails 3.0 - ruby-on-rails

On my User model, I have an attribute trial_end_date.
The column in the table looks like this:
# trial_end_date :date
However, if I try to change the date to far in the future, at the Rails console, this is what happens:
a = User.find(2)
a.trial_end_date = "2019-12-30"
=> "2019-12-30"
>> a.save
=> true
>> a.trial_end_date
=> Sat, 19 Nov 2011
WTF? Why does it do that? I have no idea why it does this?
Even if I try update_attributes(:trial_end_date => "2019-12-30") the same thing happens.
Here are all the methods in my User model that relate to trial_end_date:
after_validation :set_trial_end
def has_trial_expired?
if (self.trial_end_date <= Date.today)
return true
else
return false
end
end
def set_trial_end
plan = self.plan
end_of_trial = Date.today + self.plan.trial_duration.days
self.trial_end_date = end_of_trial.to_date
end
def trial_will_almost_end?
if (self.trial_end_date - Date.today <= 3)
return true
else
return false
end
end
def when_does_trial_end?
self.trial_end_date
end

marcamillion,
You commented that you thought that you thought that validation would happen "just on the initial user creation." As comments have pointed out, that's not true if you use after_validation, but it IS true if you use
before_validation_on_create
(See, for example, http://ar.rubyonrails.org/classes/ActiveRecord/Callbacks.html )
Using that would restrict the creation of dates by your users, but wouldn't prevent you (or them! Be careful!) from changing them later in other ways.

After validation the trial_end_date gets set based on the plan duration, no?

One refinement to Bob's answer: *_on_create and its ilk are deprecated in 3.0 and removed in 3.1. In the interest of maintainability, you probably want to adopt the new form:
before_validation :some_method, :on => :create
It's a quick tweak that'll save you headaches in the future.

Related

How to check if last_updated more than 15.minutes.ago?

This should be simple, but I am getting edge cases that seem to be failing, I am doing something wrong and it kinda confuses me. I have a method like this:
def self.needs_updating?(last_updated_time, time_since_update)
return false if last_updated_time.nil?
time_since_update < last_updated_time
end
It's called using this syntax:
Tools::Utils.needs_updating?(#entry.updated_at, 15.minutes.ago)
What I am trying to do is discover whether #entry has been updated at least 15.minutes.ago. If it has been updated 20.minutes.ago this should return true, if it has been updated 1.minute.ago, it should return false.
I think i may need to insert DateTime.now.utc in there maybe. But how can i do it so that I can keep using the Rails idiom (14.minutes.ago, 1.hour.ago etc..) ?
Thanks!
It should be
def self.needs_updating?(last_updated_time, time_since_update)
return false if last_updated_time.nil?
last_updated_time < time_since_update
end
If you want time in UTC use last_updated_time.utc < time_since_update.utc
If you want time in your local time use last_updated_time.localtime < time_since_update.localtime
example
last_updated_time = #entry.updated_at
=> 2022-05-11 22:30:11 +0200
time_since_update = 15.minutes.ago
=> 2022-05-12 22:35:15 +0200
last_updated_time < time_since_update
=> true

Rails - Update attributes using rake task. Need help troubleshooting model method code

I am trying to use a rake task that will run every night (using Heroku Scheduler) and update some attributes if some certain criteria is met.
A little logic about the application:
The application allows users to "take the challenge" and read a book a week over the course of a year. It's pretty simple: users sign up, create the first book that they will read for their first week, then can enter what they're going to read next week. After they've "queued" up next week's book, that form is then hidden until it's been 7 days since their first book was created. At that point, the book that was queued up gets moved to the top of the list and marked as 'currently reading', while the previous 'currently reading' book moves down to the second position in the list.
And IF a user doesn't 'queue' a book, the system will automatically create one if it's been 7 days since the latest 'currently reading' book was created.
Where I need some advice
The place I'm currently stuck is getting the books to update attributes if it's been 7 days since the last 'currently reading' book was created. Below is my book model and the method update_queue is what gets called during the rake task. Running the rake task currently gives no errors and properly loops through the code, but it just doesn't change any attribute values. So I'm sure the code in the update_queue method is not correct somewhere along the lines and I would love your help troubleshooting the reason why. And how I'm testing this is by adding a book then manually changing my system's date to 8 days ahead. Pretty barbaric, but I don't have a test suite written for this application & it's the easiest way to do it for me :)
class Book < ActiveRecord::Base
attr_accessible :author, :date, :order, :title, :user_id, :status, :queued, :reading
belongs_to :user
scope :reading_books, lambda {
{:conditions => {:reading => 1}}
}
scope :latest_first, lambda {
{:order => "created_at DESC"}
}
def move_from_queue_to_reading
self.update_attributes(:queued => false, :reading => 1);
end
def move_from_reading_to_list
self.update_attributes(:reading => 0);
end
def update_queue
days_gone = (Date.today - Date.parse(Book.where(:reading => 1).last.created_at.to_s)).to_i
# If been 7 days since last 'currently reading' book created
if days_gone >= 7
# If there's a queued book, move it to 'currently reading'
if Book.my_books(user_id).where(:queued => true)
new_book = Book.my_books(user_id).latest_first.where(:queued => true).last
new_book.move_from_queue_to_reading
Book.my_books(user_id).reading_books.move_from_reading_to_list
# Otherwise, create a new one
else
Book.my_books(user_id).create(:title => "Sample book", :reading => 1)
end
end
end
My rake task looks like this (scheduler.rake placed in lib/tasks):
task :queue => :environment do
puts "Updating feed..."
#books = Book.all
#books.each do |book|
book.update_queue
end
puts "done."
end
I would move the update_queue logic to the User model and modify the Book model somewhat, and do something like this:
# in book.rb
# change :reading Boolean field to :reading_at Timestamp
scope :queued, where(:queued => true)
scope :earliest_first, order("books.created_at")
scope :reading_books, where("books.reading_at IS NOT NULL")
def move_from_queue_to_reading
self.update_attributes(:queued => false, :reading_at => Time.current);
end
def move_from_reading_to_list
self.update_attributes(:reading_at => nil);
end
# in user.rb
def update_queue
reading_book = books.reading_books.first
# there is an edge-case where reading_book can't be found
# for the moment we will simply exit and not address it
return unless reading_book
days_gone = Date.today - reading_book.reading_at.to_date
# If less than 7 days since last 'currently reading' book created then exit
return if days_gone < 7
# wrap modifications in a transaction so they can be rolled back together
# if an error occurs
transaction do
# First deal with the 'currently reading' book if there is one
reading_book.move_from_reading_to_list
# If there's a queued book, move it to 'currently reading'
if books.queued.exists?
books.queued.earliest_first.first.move_from_queue_to_reading
# Otherwise, create a new one
else
books.create(:title => "Sample book", :reading_at => Time.current)
end
end
end
Now you can have the Heroku scheduler run something like this once a day:
User.all.each(&:update_queue)
Modify User.all to only return active users if you need to.
Oh, and you can use the timecop gem to manipulate times and dates when testing.
Probably the reason is that, the program flow is not going inside the if days_gone >= 7 condition.
you could check this in two ways
1 - Simple and easy way (but not a very good way)
use p statements every were with some meaning full text
Ex:
def update_queue
days_gone = (Date.today - Date.parse(Book.where(:reading => 1).last.created_at.to_s)).to_i
p "days gone : #{days_gone}"
# If been 7 days since last 'currently reading' book created
if days_gone >= 7
p "inside days_gone >= 7"
etc...
2 - Use ruby debugger and use debug points
in Gem file add
gem 'debugger'
and insert break points where ever needed
def update_queue
days_gone = (Date.today - Date.parse(Book.where(:reading => 1).last.created_at.to_s)).to_i
debugger
more help
HTH

How to format values before saving to database in rails 3

I have a User model with Profit field. Profit field is a DECIMAL (11,0) type. I have a masked input on the form which allows user to input something like $1,000. I want to format that value and remove everything except numbers from it so i will have 1000 saved. Here is what i have so far:
class User < ActiveRecord::Base
before_save :format_values
private
def format_values
self.profit.to_s.delete!('^0-9') unless self.profit.nil?
end
end
But it keeps saving 0 in database. Looks like it is converting it to decimal before my formatting function.
Try this:
def profit=(new_profit)
self[:profit] = new_profit.gsub(/[^0-9]/, '')
end
First of all, this:
def format_values
self.profit.to_s.delete!('^0-9') unless self.profit.nil?
end
is pretty much the same as this:
def format_values
return if(self.profit.nil?)
p = self.profit
s = p.to_s
s.delete!('^0-9')
end
So there's no reason to expect your format_values method to have any effect whatsoever on self.profit.
You could of course change format_values to assign the processed string to self.profit but that won't help because your cleansing logic is in the wrong place and it will be executed after '$1,000' has been turned into a zero.
When you assign a value to a property, ActiveRecord will apply some type conversions along the way. What happens when you try to convert '$1,000' to a number? You get zero of course. You can watch this happening if you play around in the console:
> a = M.find(id)
> puts a.some_number
11
> a.some_number = 'pancakes'
=> "pancakes"
> puts a.some_number
0
> a.some_number = '$1,000'
=> "1,000"
> puts a.some_number
0
> a.some_number = '1000'
=> "1000"
> puts a.some_number
1000
So, your data cleanup has to take place before the data goes into the model instance because as soon as AR gets its hands on the value, your '$1,000' will become 0 and all is lost. I'd put the logic in the controller, the controller's job is to mediate between the outside world and the models and data formatting and mangling certainly counts as mediation. So you could have something like this in your controller:
def some_controller
fix_numbers_in(:profit)
# assign from params as usual...
end
private
def fix_numbers_in(*which)
which.select { |p| params.has_key?(p) }.each do |p|
params[p] = params[p].gsub(/\D/, '') # Or whatever works for you
end
end
Then everything would be clean before ActiveRecord gets its grubby little hands on your data and makes a mess of things.
You could do similar things by overriding the profit= method in your model but that's really not the model's job.
class User < ActiveRecord::Base
before_save :format_values
private
def format_values
self.profit = profit.to_s.gsub(/\D/,'') if profit
end
end
def format_values
self.profit.to_d!
end
I recommend you to write custom setter for this particular instance variable #profit:
class User
attr_accessor :profit
def profit= value
#profit = value.gsub(/\D/,'')
end
end
u = User.new
u.profit = "$1,000"
p u.profit # => "1000"
I would suggest using the rails helper of number with precision. Below is some code.
Generic Example:
number_with_precision(111.2345, :precision => 1, :significant => true) # => 100
Rails code Example:
def profit=(new_profit)
number_with_precision(self[:profit], :precision => 1, :significant => true)
end

after_commit for an attribute

I am using an after_commit in my application.
I would like it to trigger only when a particular field is updated in my model. Anyone know how to do that?
Old question, but this is one method that I've found that might work with the after_commit callback (working off paukul's answer). At least, the values both persist post-commit in IRB.
after_commit :callback,
if: proc { |record|
record.previous_changes.key?(:attribute) &&
record.previous_changes[:attribute].first != record.previous_changes[:attribute].last
}
Answering this old question because it still pops up in search results
you can use the previous_changes method which returnes a hash of the format:
{ "changed_attribute" => ["old value", "new value"] }
it's what changes was until the record gets actually saved (from active_record/attribute_methods/dirty.rb):
def save(*) #:nodoc:
if status = super
#previously_changed = changes
#changed_attributes.clear
# .... whatever goes here
so in your case you can check for previous_changes.key? "your_attribute" or something like that
Old question but still pops up in search results.
As of Rails 5 attribute_changed? was deprecated. Using saved_change_to_attribute? instead of attribute_changed? is recommended.
I don't think you can do it in after_commit
The after_commit is called after the transaction is commited Rails Transactions
For example in my rails console
> record = MyModel.find(1)
=> #<MyModel id: 1, label: "test", created_at: "2011-08-19 22:57:54", updated_at: "2011-08-19 22:57:54">
> record.label = "Changing text"
=> "Changing text"
> record.label_changed?
=> true
> record.save
=> true
> record.label_changed?
=> false
Therefore you won't be able to use the :if condition on after_commit because the attribute will not be marked as changed anymore as it has been saved. You may need to track whether the field you are after is changed? in another callback before the record is saved?
This is a very old problem, but the accepted previous_changes solution just isn't robust enough. In an ActiveRecord transaction, there are many reasons why you might save a Model twice. previous_changes only reflects the result of the final save. Consider this example
class Test < ActiveRecord::Base
after_commit: :after_commit_test
def :after_commit_test
puts previous_changes.inspect
end
end
test = Test.create(number: 1, title: "1")
test = Test.find(test.id) # to initialize a fresh object
test.transaction do
test.update(number: 2)
test.update(title: "2")
end
which outputs:
{"title"=>["1", "2"], "updated_at"=>[...]}
but, what you need is:
{"title"=>["1", "2"], "number"=>[1, 2], "updated_at"=>[...]}
So, my solution is this:
module TrackSavedChanges
extend ActiveSupport::Concern
included do
# expose the details if consumer wants to do more
attr_reader :saved_changes_history, :saved_changes_unfiltered
after_initialize :reset_saved_changes
after_save :track_saved_changes
end
# on initalize, but useful for fine grain control
def reset_saved_changes
#saved_changes_unfiltered = {}
#saved_changes_history = []
end
# filter out any changes that result in the original value
def saved_changes
#saved_changes_unfiltered.reject { |k,v| v[0] == v[1] }
end
private
# on save
def track_saved_changes
# maintain an array of ActiveModel::Dirty.changes
#saved_changes_history << changes.dup
# accumulate the most recent changes
#saved_changes_history.last.each_pair { |k, v| track_saved_change k, v }
end
# v is an an array of [prev, current]
def track_saved_change(k, v)
if #saved_changes_unfiltered.key? k
#saved_changes_unfiltered[k][1] = track_saved_value v[1]
else
#saved_changes_unfiltered[k] = v.dup
end
end
# type safe dup inspred by http://stackoverflow.com/a/20955038
def track_saved_value(v)
begin
v.dup
rescue TypeError
v
end
end
end
which you can try out here: https://github.com/ccmcbeck/after-commit
It sounds like you want something like a conditional callback. If you had posted some code I could have pointed you in the right direction however I think you would want to use something like this:
after_commit :callback,
:if => Proc.new { |record| record.field_modified? }
Use gem ArTransactionChanges. previous_changes is not working for me in Rails 4.0.x
Usage:
class User < ActiveRecord::Base
include ArTransactionChanges
after_commit :print_transaction_changes
def print_transaction_changes
transaction_changed_attributes.each do |name, old_value|
puts "attribute #{name}: #{old_value.inspect} -> #{send(name).inspect}"
end
end
end

Rails - Exclude an attribute from being saved

I have a column named updated_at in postgres. I'm trying to have the db set the time by default. But Rails still executes the query updated_at=NULL. But postgres will only set the timestamp by default when updated_at is not in the query at all.
How do I have Rails exclude a column?
You can disable this behaviour by setting ActiveRecord::Base class variable
record_timestamps to false.
In config/environment.rb, Rails::Initializer.run block :
config.active_record.record_timestamps = false
(if this doesn't work, try instead ActiveRecord::Base.record_timestamps = false at the end of the file)
If you want to set only for a given model :
class Foo < ActiveRecord::Base
self.record_timestamps = false
end
Credit to Jean-François at http://www.ruby-forum.com/topic/72569
I've been running into a similar issue in Rails 2.2.2. As of this version there is an attr_readonly method in ActiveRecord but create doesn't respect it, only update. I don't know if this has been changed in the latest version. I overrode the create method to force is to respect this setting.
def create
if self.id.nil? && connection.prefetch_primary_key?(self.class.table_name)
self.id = connection.next_sequence_value(self.class.sequence_name)
end
quoted_attributes = attributes_with_quotes(true, false)
statement = if quoted_attributes.empty?
connection.empty_insert_statement(self.class.table_name)
else
"INSERT INTO #{self.class.quoted_table_name} " +
"(#{quoted_attributes.keys.join(', ')}) " +
"VALUES(#{quoted_attributes.values.join(', ')})"
end
self.id = connection.insert(statement, "#{self.class.name} Create",
self.class.primary_key, self.id, self.class.sequence_name)
#new_record = false
id
end
The change is just to pass false as the second parameter to attributes_with_quotes, and use quoted_attributes.keys for the column names when building the SQL. This has worked for me. The downside is that by overriding this you will lose before_create and after_create callbacks, and I haven't had time to dig into it enough to figure out why. If anyone cares to expand/improve on this solution or offer a better solution, I'm all ears.

Resources