Rails 3: Should I explicitly save an object in an after_create callback? - ruby-on-rails

Relevant Code: http://pastebin.com/EnLJUJ8G
class Task < ActiveRecord::Base
after_create :check_room_schedule
...
scope :for_date, lambda { |date| where(day: date) }
scope :for_room, lambda { |room| where(room: room) }
scope :room_stats, lambda { |room| where(room: room) }
scope :gear_stats, lambda { |gear| where(gear: gear) }
def check_room_schedule
#tasks = Task.for_date(self.day).for_room(self.room).list_in_asc_order
#self_position = #tasks.index(self)
if #tasks.length <= 2
if #self_position == 0
self.notes = "There is another meeting in
this room beginning at # {#tasks[1].begin.strftime("%I:%M%P")}."
self.save
end
end
end
private
def self.list_in_asc_order
order('begin asc')
end
end
I'm making a small task app. Each task is assigned to a room. Once I add a task, I want to use a callback to check to see if there are tasks in the same room before and or after the task I just added (although my code only handles one edge case right now).
So I decided to use after_create (since the user will manually check for this if they edit it, hence not after_save) so I could use two scopes and a class method to query the tasks on the day, in the room, and order them by time. I then find the object in the array and start using if statements.
I have to explicitly save the object. It works. But it feels weird that I'm doing that. I'm not too experienced (first app), so I'm not sure if this is frowned upon or if it is convention. I've searched a bunch and looked through a reference book, but I haven't see anything this specific.
Thanks.

This looks like a task for before_create to me. If you have to save in your after_* callback, you probably meant to use a before_* callback instead.
In before_create you wouldn't have to call save, as the save happens after the callback code runs for you.
And rather than saving then querying to see if you get 2 or more objects returns, you should be querying for one object that will clash before you save.
In psuedo code, what you have now:
after creation
now that I'm saved, find all tasks in my room and at my time
did I find more than one?
Am I the first one?
yes: add note about another task, then save again
no: everything is fine, no need to re-save any edits
What you should have:
before creation
is there at least 1 task in this room at the same time?
yes: add note about another task
no: everything is fine, allow saving without modification
Something more like this:
before_create :check_room_schedule
def check_room_schedule
conflicting_task = Task.for_date(self.day)
.for_room(self.room)
.where(begin: self.begin) # unsure what logic you need here...
.first
if conflicting_task
self.notes =
"There is another meeting in this room beginning at #{conflicting_task.begin.strftime("%I:%M%P")}."
end
end

Related

How do I create a transaction out of multiple Rails save methods?

I'm using Rails 5. I have a model that looks like this
class CryptoIndexCurrency < ApplicationRecord
belongs_to :crypto_currency
end
I have a service method where I want to populate this table with records, which I do like so
CryptoIndexCurrency.delete_all
currencies.each do |currency|
cindex_currency = CryptoIndexCurrency.new({:crypto_currency => currency})
cindex_currency.save
end
The problem is the above is not very transactional, in as far as if something happens after the first statement, the "delete_all" will have executed but nothing else will have. What is the proper way to create a transaction here and equally as important, where do I place that code? Would like to know the Rails convention here.
I think you can just do:
CryptoIndexCurrency.transaction do
CryptoIndexCurrency.delete_all
CryptoIndexCurrency.create(currencies.map{ |c| {crypto_currency: c} })
end
If you are using Activerecord you can use the builtin transaction mechanism. Otherwise, one way would be to make sure you validate all your data and only save when everything is valid. Take a look at validates_associate and the like.
That said, if your process is inherently non validatable/nondeterministic (eg. you call external APIs to validate a payment) then the best is to ensure you have some cleaning methods that take care of your failure
If you have deterministic failures:
def new_currencies_valid?(currencies)
currencies.each do
return false if not currency.valid?(:create)
end
true
end
if new_currencies_valid?(new_currencies)
Currency.delete_all # See note
new_currencies.each(&:save)
end
A sidenote : unless you really understand what you are doing, I suggest calling destroy_all which runs callbacks on deletion (such as deleting dependent: :destroy) associations

Rails. Update model attributes on save

Thought it's an easy task however I have stuck a little bit with this issue:
Would like to update one of the attributes of the model whenever it's saved, thus having a callback in the model:
after_save :calculate_and_save_budget_contingency
def calculate_and_save_budget_contingency
self.total_contingency = self.budget_contingency + self.risk_contingency
self.save
# => this doesn't work as well.... self.update_attribute :budget_contingency, (self.budget_accuracy * self.budget_estimate) / 1
end
And the webserver shoots back with the message ActiveRecord::StatementInvalid (SystemStackError: stack level too deep: INSERT INTO "versions"
Which basically tells me that there is an infite loop of save to the model, after_save and then we save the model again... which goes into another loop of saving the model
Just stuck at this point of time on this model attribute calculation. If anyone has encountered this issue, and has a nice nifty/rails solution, please shoot me a message below, thanks
Change your code to following
before_save :calculate_and_save_budget_contingency
def calculate_and_save_budget_contingency
self.total_contingency = self.budget_contingency + self.risk_contingency
end
Reason for that is - if you run save in after_save you end up in infinite loop: a save calls after_save callback, which calls save which calls after_save, which...
In general it's wise you use after save only for changing associated models, etc.
Try before_save or before_validation, but don't include the .save

ActiveRecord::Base Class Not Mutable?

I have a class I've extended from ActiveRecord::Base...
class Profile < ActiveRecord::Base
and I collect the records from it like so...
records = #profile.all
which works fine, but it doesn't seem that I can successfully Update the attributes. I don't want to save them back to the database, just modify them before I export them as JSON. My question is, why can't I update these? I'm doing the following (converting date formats before exporting):
records.collect! { |record|
unless record.term_start_date.nil?
record.term_start_date = Date.parse(record.term_start_date.to_s).strftime('%Y,%m,%d')
end
unless record.term_end_date.nil?
record.term_end_date = Date.parse(record.term_end_date.to_s).strftime('%Y,%m,%d')
end
record
}
At first I had just been doing this in a do each loop, but tried collect! to see if it would fix things, but no difference. What am I missing?
P.S. - I tried this in irb on one record and got the same results.
I suggest a different way to solve the problem, that keeps the logic encapsulated in the class itself.
Override the as_json instance method in your Profile class.
def as_json(options={})
attrs = super(options)
unless attrs['term_start_date'].nil?
attrs['term_start_date'] = Date.parse(attrs['term_start_date'].to_s).strftime('%Y,%m,%d')
end
unless attrs['term_end_date'].nil?
attrs['term_end_date'] = Date.parse(attrs['term_end_date'].to_s).strftime('%Y,%m,%d')
end
attrs
end
Now when you render the records to json, they'll automatically use this logic to generate the intermediate hash. You also don't run the risk of accidentally saving the formatted dates to the database.
You can also set up your own custom option name in the case that you don't want the formatting logic.
This blog post explains in more detail.
Try to add record.save! before record.
Actually, by using collect!, you just modifying records array, but to save modified record to database you should use save or save! (which raises exception if saving failed) on every record.

Custom Model Method, setting scope for automatic sending of mail

There are several stages to this, and as I am relatively new to rails I am unsure if I am approaching this in the best way.
Users follow Firms, Firms applications open and close on certain days. If a user follows a firm I would like them to automatically get an email when a) the firms application opens, b) a week before the firms applications close, c) on the day that the firms applications close.
I have tried using named scope. I have the following model method (I presume this will need a little work) setting each firms scope, depending on the date.
model firms.rb
def application_status
if open_date == Today.date
self.opening = true
else
self.opening = false
end
if ((close_day - Today.date) == 7)
self.warning = true
else
self.warning = false
end
if close_day == Today.date
self.closing = true
else
self.closing = false
end
end
I would like this method to be called on each firm once a day, so that each firm has the appropriate scope - so I have tried using the whenever gem (cron) and the following code. Running the above model method on each firm.
Schedule.rb
every 1.day do
runner "Firm.all.each do |firm|
firm.application_status
end"
end
Then for each of the scopes opening, warning, closing i have a method in the whenever schedules file, For simplicity I shall show just the opening methods. The following queries for all firms that have had the opening scope applied to them, and runs the application_open_notification method on them.
Schedule.rb
every 1.day do
runner "Firm.opening.each do |firm|
firm.application_open_notification
end"
end
This calls the following method in the Firm.rb model
def application_open_notification
self.users.each do |user|
FirmMailer.application_open(user, self).deliver
end
end
Which in turn calls the final piece of the puzzle... which should send the user an email, including the name of the firm.
def application_open(user,firm)
#firm = firm
#user = user
mail to: #user.email, subject: #firm' is now accepting applications'
end
end
Is this a viable way to approach this problem? In particular I am not very familiar with coding in the model.
Many thanks for any help that you can offer.
I'll guess that opening, warning and closing are database fields, and you have scopes like:
class Firm < ActiveRecord::Base
scope :opening, :where => { :opening => true }
# etc
end
There is a general rule for database (and, well, all storage): don't store things you can caculate, if you don't have to.
Since an application's status can be dermined from the day's date and the open_date and close_day fields, you could calculate them as needed instead of creating extra fields for them. You can do this with SQL and Active Record:
scope :opening, :where { :open_date => (Date.today .. Date.today+1) }
scope :warning, :where { :close_day => (Date.today+7 .. Date.today+8) }
scope :closing, :where { :close_day => (Date.today .. Date.today+1) }
(Note that these select time ranges. They may have to be changed depending on if you are using date or time fields.)
But there is another issue: what happens if, for some reason (computer crash, code bug etc) your scheduled program doesn't run on a particular day? You need a way of making sure notices are sent eventually even if something breaks. There are two solutions:
Write your schedule program to optionally accept a date besides today (via ARGV)
keep flags for each firm for whether each kind of notice has been sent. These will have to be stored in the databse.
Note that scopes aren't necessary. You are able to do this:
Firm.where(:open_date => (Date.today .. Date.today+1)).each do |firm|
#...
end
but the scope at least encapsulates the details of identifying the various sets of records.

Scope vs Class Method in Rails 3

Based on the Rails 3 API, the difference between a scope and a class method is almost non-existent.
class Shipment < ActiveRecord::Base
def self.unshipped
where(:shipped => false)
end
end
is the same as
scope :unshipped, where(:shipped => false)
However, I'm finding that I'm sometimes getting different results using them.
While they both generate the same, correct SQL query, the scope doesn't always seem to return the correct values when called. It looks like this problem only occurs when its called the same way twice, albeit on a different shipment, in the method. The second time it's called, when using scope it returns the same thing it did the first time. Whereas if I use the class method it works correctly.
Is there some sort of query caching that occurs when using scope?
Edit:
order.line_items.unshipped
The line above is how the scope is being called. Orders have many line_items.
The generate_multiple_shipments method is being called twice because the test creates an order and generates the shipments to see how many there are. It then makes a change to the order and regenerates the shipments. However, group_by_ship_date returns the same results it did from the first iteration of the order.
def generate_multiple_shipments(order)
line_items_by_date = group_by_ship_date(order.line_items.unshipped)
line_items_by_date.keys.sort.map do |date|
shipment = clone_from_order(order)
shipment.ship_date = date
line_items_by_date[date].each { |line_item| shipment.line_items << line_item }
shipment
end
end
def group_by_ship_date(line_items)
hash = {}
line_items.each do |line_item|
hash[line_item.ship_date] ||= []
hash[line_item.ship_date] << line_item
end
hash
end
I think your invocation is incorrect. You should add so-called query method to execute the scope, such as all, first, last, i.e.:
order.line_items.unshipped.all
I've observed some inconsistencies, especially in rspec, that are avoided by adding the query method.
You didn't post your test code, so it's hard to say precisely, but my exeprience has been that after you modify associated records, you have to force a reload, as the query cache isn't always smart enough to detect a change. By passing true to the association, you can force the association to reload and the query to re-run:
order.line_items(true).unshipped.all
Assuming that you are referencing Rails 3.1, a scope can be affected by the default scope that may be defined on your model whereas a class method will not be.

Resources