Populating Rails Fields Based on Calculation - ruby-on-rails-3

In my Rails 3.2 app, I want to populate some fields based on calculations where the field values users enter are the variables. However, with my current code, the calculation seems to only work based on the values already in the database - it doesn't calculate correctly on the initial save, but it will calculate correctly if I go back in the record and save it a second time.
I have these four fields in my model (Trade):
entry_price
exit_price
percent_result
dollar_result
The user creates a trade with an entry price, and then later edits the trade with the exit_price. When the exit_price is entered, the app should calculate percent_result and dollar_result. However, right now, these result fields are not populating correctly on the first update - it seems to be because it doesn't read the exit_price from the field (when a user enters it in the form), only once it is saved in the DB.
What is going wrong in my controller?
my controller:
def update
#trade = Trade.find(params[:id])
exit_price = params[:trade][:exit_price]
if !exit_price.blank?
#trade.percent_result = ((exit_price.to_f - #trade.entry_price)/#trade.entry_price) * 100
#trade.dollar_result = exit_price.to_f - #trade.entry_price
end
params[:trade][:exit_date] = Date.strptime(params[:trade][:exit_date], '%m/%d/%Y') unless params[:trade][:exit_date].blank?
params[:trade][:entry_date] = Date.strptime(params[:trade][:entry_date], '%m/%d/%Y') unless params[:trade][:entry_date].blank?
respond_to do |format|
if #trade.update_attributes(params[:trade])
format.html { redirect_to #trade, :flash => {:share =>"Your trade was successfully updated. Don't forget to share it with your friends, so you can profit together!"} }
format.json { head :no_content }
else
format.html { render action: "edit" }
format.json { render json: #trade.errors, status: :unprocessable_entity }
end
end
end
the view
<%= simple_form_for(#trade, :html=>{:class=> "form-horizontal well"}) do |f| %>
<%= f.text_field :entry_price, :class=>"input-small" %>
<%= f.text_field :exit_price, :class=>"input-small" %>
<%= submit_tag "Edit Trade" %>
<% end %>

This would probably be better accomplished with a before_save filter in your model.
Add
before_save :calculate_results
to the top of your model and then define
def calculate_results
unless self.exit_price.blank? || self.entry_price.blank?
self.percent_result = ((self.exit_price - self.entry_price)/self.entry_price) * 100
self.dollar_result = self.exit_price - self.entry_price
end
end
in your model as well. Taking this approach ensures that your results will always be consistent with your values for entry and exit price. Enforcing this in the controller violates the Rails principle of "thick model and thin controller" and may also lead to data consistency issues.
An even more consistent way of doing this would be to define dollar_result and percent_result as methods in your model. As your model is now, you have dollar_result stored in the database even though it is a derived value. As a general rule, you should only have one representation of each piece of data whereas here you have two. A helper method might look something like
def dollar_result
self.exit_price - self.entry_price unless self.exit_price.blank? || self.entry_price.blank?
end
You would define a similar method for percent_result. Using this method, you can guarantee that all of your data is consistent because it only has one, canonical representation in the system.

Related

If a Rails model has_many children, how do I store the id of the first?

I'm new to Rails, so it's possible I'm overlooking something simple. I have a Rails model called a story. Each story has_many segments, and each segment belongs_to a story. I use the same form to create both the story and its first segment by using a fields_for section of the form and setting the story model to accepts_nested_attributes_for :segments. I am currently able to use the form to create both a story and a segment simultaneously.
The problem is that each story also needs to store the id of its first segment, but when I'm saving the story, the segment hasn't yet been saved, so it doesn't yet have an id to store in the story, and I haven't been able to find a handle for the segment after the form is submitted so that I can save the segment first before the story is created. So my question is how do I save a record of the first_segment_id inside the story?
The following code may be relevant:
in app/models/story.rb
class Story < ActiveRecord::Base
attr_accessible :segments_attributes
has_many :segments
accepts_nested_attributes_for :segments
end
in app/models/segment.rb
class Segment < ActiveRecord::Base
attr_accessible :words
belongs_to :story
end
in app/views/stories/_ form.html.erb
<%= form_for(#story) do |f| %>
#...stories fields...
<%= f.fields_for :segments do |segment_form| %>
<div class="field">
<%= segment_form.label :words %><br />
<%= segment_form.text_area :words %>
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
in app/controllers/stories_ controller.rb
def new
#story = Story.new
#segment = #story.segments.build
# If I try replacing the above with #segment = #story.segments.create
# then I get the error message "You cannot call create unless the
# parent is saved," which is problematic because I need to find some way
# to get the id of #segment to save in the parent story, but the segment
# won't have an id until after it has been saved.
respond_to do |format|
format.html # new.html.erb
format.json { render json: #story }
end
end
def create
#story = Story.new(params[:story])
# #segment.save
# #story.first_segment_id = #segment.id
# If I uncomment the above two lines, I get the error message
# "undefined method `save' for nil:NilClass". It appears that
# #segment hasn't been passed from the "new" method above to
# this method as a handle of the first segment created, so I'm not
# able to save it to get an id for it before saving the story.
# Is there some way to save the segment here?
respond_to do |format|
#...if #story.save...
end
end
The params hash submitted by the form looks something like:
{ "story"=>{ Various_other_story_fields,
"segments_attributes"=>{"0"=>{"words"=>"dfsdsa"}}},
"commit"=>"Create Story"}
Is there a way to save the first segment's id in the story? I think perhaps I need to add a before_create inside my story model instead, but I'm not sure how do to this.
I would approach this differently, adding a numeric sort_order column to Segment so that you're not relying on Segment ids to determine what order they go in. Then you can define something like the following on your Story model rather than explicitly referencing the first segment in the database:
def first_segment
segments.order(:sort_order).first
end
If you're certain you need to store the first segment ID in the story, you can .save the story so that it knows its ID, saves its children, and knows their IDs. Something like:
def create
#story = Story.new(params[:story])
#story.save # Save the story and its child segment so that they both have IDs
#story.first_segment_id = #story.segments.first.id
#story.save
...
end
You should be able to do something like:
class Story < ActiveRecord::Base
attr_accessible :segments_attributes
has_many :segments
has_one :first_segment
accepts_nested_attributes_for :segments
end
def new
#story = Story.new
#segment = #story.segments.build
#story.first_segment = #segment
...
You'll have to add story_id to your segment table.

Using nested attributes to easily select associations in a form

I am trying to create a nested attribute form to create a model which is primarily an association "connector" between two other models. In my case, the models represent books, awards, and the "connector" model book_awards. When I am editing a book, I want to be able to quickly select which awards it has won.
I've been using
http://railscasts.com/episodes/196-nested-model-form-part-1
to help me get started, but I'm afraid I'm pretty much stuck.
Another SO question which seems similar is
accepts_nested_attributes_for with find_or_create? Unfortunately, it's also not quite what I'm doing and I haven't been able to adapt it.
My models look like this. Each model has additional attributes and validations etc, but I've removed them for clarity.
class Book < ActiveRecord::Base
has_many :book_awards
accepts_nested_attributes_for :book_awards, :allow_destroy => true
end
class Award < ActiveRecord::Base
has_many :book_awards
end
class BookAward < ActiveRecord::Base
belongs_to :book, :award
end
In my book controller methods for edit and new, and the failure cases for create and update I have a line #awards = Award.all.
In my view, I would like to see a list of all awards with check boxes next to them. When I submit, I would like to either update, create, or destroy a book_award model. If the check box is selected, I would like to update an existing model or create a new one if it doesn't exist. If the check box isn't selected, then I would like to destroy an existing model or do nothing if the award never existed. I have a partial for book_awards. I'm not sure if the check box selector should be in this partial or not.
I think my check box will be my hook to :_destroy but with its polarity reversed. I think something like this will basically do it:
= f.check_box :_destroy, {}, 0, 1
Currently, I have this in my partial but I'm not sure where it really belongs.
Next comes my view which currently doesn't work, but maybe it will help demonstrate what I'm trying to do. I loop through the awards and use a fields_for to set nested attributes for anything that already exists. It's horribly ugly, but I think it somewhat works. However, I don't really know how to get started with the else case.
= f.label :awards
- #awards.each do |a|
- if f.object.awards && f.object.awards.include?(a)
= f.fields_for :book_awards, f.object.book_award.select{|bas| bas.award == a } do |ba|
= render 'book_awards', :f => ba, :a => a
- else
= fields_for :book_awards do |ba|
= render 'book_awards', :f => ba, :a => a
I would prefer the awards to be listed in the same order each time (my #awards assignment in the controller will probably specify the order) as opposed to listing the existing awards first or last.
I hate to answer my own question, but I finally figured out something which works. The first thing I needed to do was to update the "new" case based on the crazy object which was included in the railscast. Next, I needed to manually set the :child_index. Finally, I needed to manually set the :_destroy check box appropriately.
.field
= f.label :awards
- #awards.each_with_index do |a,i|
- if exists = (f.object.awards && f.object.awards.include?(a))
- new_ba = f.object.book_awards.select{|s| s.award == a}
- else
- new_ba = f.object.class.reflect_on_association(:book_awards).klass.new
= f.fields_for :book_awards, new_ba, :child_index => i do |ba|
= render 'book_awards', :f => ba, :a => a, :existing => exists
My partial looks like this:
.field
= f.check_box :_destroy, {:checked => existing}, 0, 1
= f.label a.name
= f.hidden_field :award_id, :value => a.id
= f.label :year
= f.number_field :year
It's not horribly pretty, but it seems to do exactly what I wanted.

rails OR query based on multiple checkbox selections

This seems like it should be a common problem but I'm having trouble finding an answer. Basically I want to have a form with 10 or so checkboxes which I'm creating with check_box_tag. When the form is submitted I want to generate a query that return all records that match ANY of the checked selections. So, the number of checked selections will vary.
So, for example, if I have
class Book < ActiveRecord::Base
belongs_to :author
end
I want to generate something like
Book.where("author_id = ? or author_id = ?", params[authors[0]], params[authors[1]]) if there are two boxes checked, etc.
Thanks for any insight.
Will this work for you?
Book.where(author_id: [array_of_author_ids])
You need to collect author_ids from params first
I recently had to do something similar, this is how I achieved this. It's pretty clever (at least I think so. :))
I created a query model that serializes the query column (text field) in JSON. I use a form to get the query data from the user with selection fields.
class BookQuery < ActiveRecord::Base
has_many :books
# loop through each foreign key of the Book table and create a hash with empty selection
def self.empty_query
q = {}
Book.column_names.each do |column_name|
next unless column_name.ends_with?("_id")
q.merge column_name => []
end
end
end
I'm using Author as an example below:
<%= form_for #book_query do |f| %>
<% for author in Author.all %>
<%= check_box_tag "book_query[query][author_ids][]", author.id, false%>
<%= author.name %>
<% end %>
<%= f.submit "Save Query" %>
<% end %>
When this form is submitted you ended up with parameters like this:
When the form is submitted it generates this parameter:
Parameters: {"utf8"=>"✓", "authenticity_token"=>"XXXXXXXXXXX", "book_query"=>{"query"=>{"author_ids"=>["2", "3"]}}, "commit"=>"Save Query"}
Now in the BookQuery controller's create action you can just do what create function always does:
def create
#book_query = BookQuery.build(params[:book_query])
if #book_query.save
flash[:success] = "Book query successfully saved."
redirect_to ...
else
flash[:error] = "Failed to save book query."
render :new
end
end
But by default rails serializes the data in hash type:
1.9.3p194 :015 > pp BookQuery.find(9).query
BookQuery Load (0.7ms) SELECT "book_queries".* FROM "book_queries" WHERE "book_queries"."id" = $1 LIMIT 1 [["id", 9]]
"--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nauthor_ids:\n- '2'\n- '3'\n"
=> "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nauthor_ids:\n- '2'\n- '3'\n"
In BookQuery model, add the following:
serialize :query, JSON
But rail would change the IDs to string:
1.9.3p194 :018 > query = JSON.parse(BookQuery.find(10).query)
BookQuery Load (0.5ms) SELECT "book_queries".* FROM "book_queries" WHERE "book_queries"."id" = $1 LIMIT 1 [["id", 10]]
=> {"author_ids"=>["2", "3"]}
1.9.3p194 :019 > query["author_ids"]
=> ["2", "3"]
What I did then is override the attribute accessors in BookQuery model:
The below has to be done because the hash returns strings, not ids in integer.
def query=(query)
query.each_pair do |k, v|
if query[k].first.present?
query[k].map!(&:to_i)
else
query.except!(k)
end
end
write_attribute(:query, query)
end
# just want to avoid getting nil query's
def query
read_attribute(:query) || {}
end
To find book with this query, you can simply add this function to your Book model:
def self.find_by_book_query(book_query, options = {})
options[:conditions] = book_query.query
find(:all, options)
end
Now you get a customizable query string based on the model definition Book and everything works like the Rails way. :)

How to pass parameters in Rails routes helper methods?

I know how to pass parameters the dumb way. For example,
<%= link_to "Order", new_order_item_path(:item_id => #item.id) %>
The OrderItemsController receives it as params[:item_id] = id.
Problem:
#order_item = OrderItem.new(params)
raises an exception (Can't mass-assign protected attributes: action, controller). I can get around this with the following code.
#order_item = OrderItem.new
#order_item.item_id = params[:item_id]
I know the controller requires params[:order_item][:item_id] for new to work the first way. My question is, how do I get new_order_item_path to generate url? I know this isn't a major problem, but it just bugs me that I don't know the cleaner/proper way to do this. I have tried searching, but only received unrelated questions/answers/results.
Thanks
You didn't really specify if you didn't want to use it or not, but in your model, you could make the attribute item_id accessible like so:
class OrderItem < ActiveRecord::Base
attr_accessible :item_id
...
In this way,
#order_item = OrderItem.new(params)
would work.
Hope this helps.
How about this:
# Controller
def get_item_edit_method
#order = OrderItem.find("your criteria")
##order = OrderItem.new # if new
#item = Item.new()
end
def post_item_edit_method
#order = OrderItem.new(params) # should work now
#order.save
end
# End controller
<!-- view -->
<% #order.item = #item %>
<%= link_to "Order", new_order_item_path(#order) %>
<!-- end view -->

validates_acceptance_of still saves the record

I am using ruby 1.9.2-p180, rails 3.0.7. I have used validates_acceptance_of since the user has to agree to our terms and conditions. We don't have a column for this, but I understand that "If the database column does not exist, the terms_of_service attribute is entirely virtual. " from http://ar.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html#M000082
Anyway, I double checked this by smoke testing the app manually and I see from the logs that the record is still inserted into the db, which is weird because upon submitting the form, I am redirected back to the form with the error: "Must agree to terms and conditions"(which made me think it worked before)
Am I doing something wrong here?
_form.haml:
%label.checkbox-label{:for => "operator_terms_and_conditions"}
= f.check_box :terms_and_conditions
I agree to
= link_to "Terms and Conditions", operator_terms_path, :target => "_blank"
operators_controller:
def create
user_params = params[:operator][:user]
user_params.merge!(:login => user_params[:email])
#password = params[:operator][:user][:password]
Operator.transaction do # don't save User if operator is invalid
#operator = Operator.create(params[:operator])
end
respond_to do |format|
unless #operator.new_record?
UserMailer.operator_confirmation_email(#operator, #password).deliver
UserMailer.operator_registration_admin_notification_email(#operator).deliver
UserSession.create(#operator.user)
format.html {redirect_to new_operator_aircraft_path}
else
format.html { render :action => "new" }
end
end
end
and in the model:
validates_acceptance_of :terms_and_conditions
Found the answer. The problem was not with validates_acceptance_of but rather with how I was saving the data. When an operator was created, a user was also created that was tied to it and it was this user that was being inserted into the db.
This happens because although the operator was being rolled back(because it wasn't valid) the user was still created(because it was not in a transaction).
I solved this by using nested_transactions:
operator model:
...
User.transaction(:requires_new => true) do
create_user
raise ActiveRecord::Rollback unless self.valid?
end
...