Rails update attribute in Model - ruby-on-rails

I want to update the :position_status in the model based on if :position_date which is equal Date.today, let say I have the :position_date which is Mon, 26 Oct 2017 stored in the database, if the Date.today is on that particular date, then update the :position_status
I have tried to change the position_date to today date to see if it will update or not, but the :position_date was not updated.
attr_accessor :update_position_status
def update_position_status
if Date.today == self.position_date
self.update_attribute(:position_status => 'Filled')
end
end

update_attribute(name, value)
http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_attribute
update_attribute updates a single attribute and skips validations. update_attributes takes a hash of attributes and performs validations.
So the corrected code would be:
def update_position_status!
update_attribute(:position_status, 'Filled') if self.position_date.today?
end
The name should end with ! since it mutates (changes) the object.
However updating the records one by one is not a particularly scalable solution. Instead you want to select the all the records by date and do a mass update:
# MySQL
Thing.where("DATE(position_date)=CURDATE()")
.update_all(position_status: 'Filled')
# Postgres
Thing.where("date_trunc('day', position_date) = current_date()")
.update_all(position_status: 'Filled')

Yes update_attribute requires two arguments.
The correct syntax is:
self.update_attribute(:position_status, 'Filled')

Related

Rails AR cannot update NULL datetime field

I am using Rails 3.1. I have a database column t.datetime "end_at". If I enter a date at the time of object creation, then I can change the value (update) later. But If I leave it blank (NULL), I found that I cannot update it. I verified that the name and new value of the field are in the params. Why?
def update
begin
model = MyModel.find(params[:id])
model.update_attributes!(params[:my_model])
rescue ActiveRecord::RecordNotFound => e
#something
end
end
You need to make sure two things are true:
your params[:end_at] must contain a nil or a DateTime. Normally params do not come in as datetime.
your column should not be a TIMESTAMP (but this is probably not the case already)
Also investigate multiparameter attributes.

Change default_timezone of active record

In rails, I have the below config for activerecord at first.
config.active_record.default_timezone = :utc
Now, I want to use the local timezone, so I changed it to:
config.active_record.default_timezone = :local
The problem is, I need to shift all the existing data in the date/datetime column to the local timezone.
What is the best way to achieve this?
Why I have to do this is because I have to do aggregation on the local timezone, for example, :group => 'DATE(created_at)', GROUP BY DATE(created_at) will be based on the UTC, but I want to aggregate with one day in local timezone.
I knew how to write a migration file to migrate a certain datetime column. But there are a lot of such column, so I'm seeking for a better solution.
This is dangerous, but here is what I'd do in the migration:
class MigrateDateTimesFromUTCToLocal < ActiveRecord::Migration
def self.up
# Eager load the application, in order to find all the models
# Check your application.rb's load_paths is minimal and doesn't do anything adverse
Rails.application.eager_load!
# Now all the models are loaded. Let's loop through them
# But first, Rails can have multiple models inheriting the same table
# Let's get the unique tables
uniq_models = ActiveRecord::Base.models.uniq_by{ |model| model.table_name }
begin
# Now let's loop
uniq_models.each do |model|
# Since we may be in the middle of many migrations,
# Let's refresh the latest schema for that model
model.reset_column_information
# Filter only the date/time columns
datetime_columns = model.columns.select{ |column| [ :datetime, :date, :time].include? column.type }
# Process them
# Remember *not* to loop through model.all.each, or something like that
# Use plain SQL, since the migrations for many columns in that model may not have run yet
datetime_columns.each do |column|
execute <<-SQL
UPDATE #{model.table_name} SET #{column.name} = /* DB-specific date/time conversion */
SQL
end
rescue
# Probably time to think about your rescue strategy
# If you have tested the code properly in Test/Staging environments
# Then it should run fine in Production
# So if an exception happens, better re-throw it and handle it manually
end
end
end
end
My first advice is to strongly encourage you to not do this. You are opening yourself up to a world of hurt. That said, here is what you want:
class ShootMyFutureSelfInTheFootMigration
def up
Walrus.find_each do |walrus|
married_at_utc = walrus.married_at
walrus.update_column(:married_at, married_at_utc.in_time_zone)
end
end
def down
Walrus.find_each do |walrus|
married_at_local = walrus.married_at
walrus.update_column(:married_at, married_at_local.utc)
end
end
end
You may pass in your preferred timezone into DateTime#in_time_zone, like so:
central_time_zone = ActiveSupport::TimeZone.new("Central Time (US & Canada)")
walrus.update_column(:married_at, married_at_utc.in_time_zone(central_time_zone))
Or you can leave it and Rails will use your current timezone. Note that this isn't where you are, it is where your server is. So if you have users in Iceland and Shanghai, but your server is in California, every single 'local' time zone will be US Pacific Time.
Must you change the data in the database? Can you instead display the dates in local time zone. Does this help: Convert UTC to local time in Rails 3
Like a lot of other people said, you probably don't want to do this.
You can convert the time to a different zone before grouping, all in the database. For example, with postgres, converting to Mountain Standard Time:
SELECT COUNT(id), DATE(created_at AT TIME ZONE 'MST') AS created_at_in_mst
FROM users GROUP BY created_at_in_mst;

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.

How can I reference attributes before a save?

I have a Reservation model that takes an appointment attribute as date and has a virtual attribute duration that indicates how long the appointment will take. The real attribute, booking_end takes a Time that is referenced all over my application. However, for ease of input, we use duration instead of choosing another Time. The fields are below:
def duration
( (booking_end - date) / 1.hour ).round( 2 ) rescue nil
end
def duration=(temp)
if ( true if Float(temp) rescue false )
self.booking_end = time_round(date + temp.to_f.hours, 15.minutes)
else
errors.add(:duration, "must be a number, stated in hours.")
self.booking_end = nil
end
end
The whole thing fails when I reference the date field while creating a new record. I get a 'nil' error because date hasn't been initialized. How can I fix this problem? The rest of this works when updating existing records.
When you call Reservation.new(:date => date, :duration => duration) ActiveRecord::Base assigns attributes values this way (see assign_attributes method):
attributes.each do |k, v|
...
respond_to?("#{k}=") ? send("#{k}="", v)
...
Hash#each method iterates through the values the way that :duration key is accessed before :date one, so date is nil inside the duration= method:
ruby-1.8.7-p302 > {:date => Date.today, :duration => 5}.each do |key,value|
ruby-1.8.7-p302 > puts "#{key} = #{value}"
ruby-1.8.7-p302 ?> end
duration = 5
date = 2010-11-17
So you'll have to call duration= after initialization.
Or you can redefine Reservation#initialize to call super with :date and then update_attributes with the rest of parameters.
I tried to initialize the model with
Reservation.new(params[:reservation][:date])
and then call update_attributes on it. This works in console, but not otherwise. The only workaround that seems to take hold is stripping duration out of the params hash and then passing it back before save. This seems really stupid, though, and probably not the right or Rails way to do things. Using 4 lines where one should suffice.
duration = params[:reservation][:duration]
params[:reservation].delete('duration')
#reservation = Reservation.new(params[:reservation])
#reservation.duration = duration
# Then go to save, etc.
Is there a different way to initialize the model or perhaps access the attributes hash from inside the model?

Using a duration field in a Rails model

I'm looking for the best way to use a duration field in a Rails model. I would like the format to be HH:MM:SS (ex: 01:30:23). The database in use is sqlite locally and Postgres in production.
I would also like to work with this field so I can take a look at all of the objects in the field and come up with the total time of all objects in that model and end up with something like:
30 records totaling 45 hours, 25 minutes, and 34 seconds.
So what would work best for?
Field type for the migration
Form field for the CRUD forms (hour, minute, second drop downs?)
Least expensive method to generate the total duration of all records in the model
Store as integers in your database (number of seconds, probably).
Your entry form will depend on the exact use case. Dropdowns are painful; better to use small text fields for duration in hours + minutes + seconds.
Simply run a SUM query over the duration column to produce a grand total. If you use integers, this is easy and fast.
Additionally:
Use a helper to display the duration in your views. You can easily convert a duration as integer of seconds to ActiveSupport::Duration by using 123.seconds (replace 123 with the integer from the database). Use inspect on the resulting Duration for nice formatting. (It is not perfect. You may want to write something yourself.)
In your model, you'll probably want attribute readers and writers that return/take ActiveSupport::Duration objects, rather than integers. Simply define duration=(new_duration) and duration, which internally call read_attribute / write_attribute with integer arguments.
In Rails 5, you can use ActiveRecord::Attributes to store ActiveSupport::Durations as ISO8601 strings. The advantage of using ActiveSupport::Duration over integers is that you can use them for date/time calculations right out of the box. You can do things like Time.now + 1.month and it's always correct.
Here's how:
Add config/initializers/duration_type.rb
class DurationType < ActiveRecord::Type::String
def cast(value)
return value if value.blank? || value.is_a?(ActiveSupport::Duration)
ActiveSupport::Duration.parse(value)
end
def serialize(duration)
duration ? duration.iso8601 : nil
end
end
ActiveRecord::Type.register(:duration, DurationType)
Migration
create_table :somethings do |t|
t.string :duration
end
Model
class Something < ApplicationRecord
attribute :duration, :duration
end
Usage
something = Something.new
something.duration = 1.year # 1 year
something.duration = nil
something.duration = "P2M3D" # 2 months, 3 days (ISO8601 string)
Time.now + something.duration # calculation is always correct
I tried using ActiveSupport::Duration but had trouble getting the output to be clear.
You may like ruby-duration, an immutable type that represents some amount of time with accuracy in seconds. It has lots of tests and a Mongoid model field type.
I wanted to also easily parse human duration strings so I went with Chronic Duration. Here's an example of adding it to a model that has a time_spent in seconds field.
class Completion < ActiveRecord::Base
belongs_to :task
belongs_to :user
def time_spent_text
ChronicDuration.output time_spent
end
def time_spent_text= text
self.time_spent = ChronicDuration.parse text
logger.debug "time_spent: '#{self.time_spent_text}' for text '#{text}'"
end
end
I've wrote a some stub to support and use PostgreSQL's interval type as ActiveRecord::Duration.
See this gist (you can use it as initializer in Rails 4.1): https://gist.github.com/Envek/7077bfc36b17233f60ad
Also I've opened pull requests to the Rails there:
https://github.com/rails/rails/pull/16919

Resources