Ruby on Rails: How to strip insignificant zeros on dynamically added items? - ruby-on-rails

I have an Invoice to which Items can be added using some jQuery magic.
Controller:
def new
#invoice = Invoice.new
#invoice.with_blank_items(current_user)
#title = "New invoice"
end
Model:
def with_blank_items(user, n = 1)
n.times do
items.build(:price => user.preference.hourly_rate)
end
self
end
View:
<%= f.text_field number_with_precision(:price, :strip_insignificant_zeros => true) %>
Now the problem is that the price of a newly added item is always displayed in the format XX.X, i.e. with one decimal place, no matter if it is zero or not.
I don't like that and I want a price of 50 to be displayed as 50 and not as 50.0.
Once the invoice gets saved to the database, unnecessary zeros get dropped and that's perfect.
How can I strip insignificant zeros on newly added items as well?

You can try to format the values the right way before assigning:
items.build(:price => '%g' % user.preference.hourly_rate)

If I understand your question, I believe your issue can fixed with a JavaScript function (which removed the decimal places).
Try something like:
function removeDecimal(val){
return val.toFixed(0);
}
These are my test cases:
removeDecimal(123.45) -> 123
removeDecimal(123.4) -> 123
removeDecimal(123) -> 123

Related

Sanitize dollar value before saving

I have an input field on a form that has the users enter in a dollar amount. I'm using autoNumeric to mask that field so that as a user inputs 1000, it displays 1,000.00 in the text field.
The column in the database (requested_amount) is a decimal with precision => 8 and scale => 2.
I've made a callback based on other SO answers that attempts to strip the comma out of the input before validating and saving to the database.
def strip_commas_from_dollar_fields
self.requested_amount = self.requested_amount.to_s.gsub(',', '')
end
# Callback
before_validation :strip_commas_from_dollar_fields
The params hash that is passed through then I submit the form:
"requested_amount"=>"80,000.00"
However, when I view the number in console, it shows:
=> 80.0
I need the number to be 80000.00 in the database.
Because:
def strip_commas_from_dollar_fields
p self.requested_amount // <- here is 80.0 already
self.requested_amount = self.requested_amount.to_s.gsub(',', '')
end
check it. So, instead of this way try to create wrapper around requested_amount field:
def requested_amount_wrapper=(val)
self.requested_amount = val.to_s.gsub(',', '')
end
def requested_amount_wrapper
self.requested_amount
end
and use this wrapper field in your hash parameters:
"requested_amount_wrapper"=>"80,000.00"

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}

Rails Specifying the order of validations

I have a validator class that i am writing that has three validations, that are run when calling MyVariableName.valid?
validates_length_of :id_number, :is => 13, :message => "A SA ID has to be 13 digits long"
validates_format_of :id_number, :with => /^[0-9]+$/, :message => "A SA ID cannot have any symbols or letters"
validate :sa_id_validator
The third one is a custom validator. The thing is that my validator sa_id_validator requires that the data that is passed in is a 13 digit number, or I will get errors. How can I make sure that the validate :sa_id_validator is only considered after the first two have run?
Sorry if this is a very simple question I have tried figuring this out all of yesterday afternoon.
Note: this validator has to run over a couple thousand entries and is also run on a spreadsheet upload so I need it to be fast..
I saw a way of doing this but it potentially runs the validations twice, which in my case would be bad.
EDIT:
my custom validator looks like this
def sa_id_validator
#note this is specific to South African id's
id_makeup = /(\d{6})(\d{4})(\d{1})(\d{1})(\d{1})/.match(#id_number)
birthdate = /(\d{2})(\d{2})(\d{2})/.match(id_makeup[1])
citizenship = id_makeup[3]
variable = id_makeup[4]
validator_key = id_makeup[5]
birthdate_validator(birthdate) && citizenship_validator(citizenship) && variable_validator(variable) && id_algorithm(id_makeup[0], validator_key)
end
private
def birthdate_validator(birthdate)
Date.valid_date?(birthdate[1].to_i,birthdate[2].to_i,birthdate[3].to_i)
end
def citizenship_validator(citizenship)
/[0]|[1]/.match(citizenship)
end
def variable_validator(variable)
/[8]|[9]/.match(variable)
end
def id_algorithm(id_num, validator_key)
odd_numbers = digits_at_odd_positions
even_numbers = digits_at_even_positions
# step1: the sum off all the digits in odd positions excluding the last digit.
odd_numbers.pop
a = odd_numbers.inject {|sum, x| sum + x}
# step2: concate all the digits in the even positions.
b = even_numbers.join.to_i
# step3: multiply step2 by 2 then add all the numbers in the result together
b_multiplied = (b*2)
b_multiplied_array = b_multiplied.to_s.split('')
int_array = b_multiplied_array.collect{|i| i.to_i}
c = int_array.inject {|sum, x| sum + x}
# step4: add the result from step 1 and 3 together
d = a + c
# step5: the last digit of the id must equal the result of step 4 mod 10, subtracted from 10
return false unless
validator_key == 10 - (d % 10)
end
def digits_at_odd_positions
id_num_as_array.values_at(*id_num_as_array.each_index.select(&:even?))
end
def digits_at_even_positions
id_num_as_array.values_at(*id_num_as_array.each_index.select(&:odd?))
end
def id_num_as_array
id_number.split('').map(&:to_i)
end
end
if i add the :calculations_ok => true attribute to my validation, and then pass in a 12 digit number instead i get this error:
i.valid?
NoMethodError: undefined method `[]' for nil:NilClass
from /home/ruberto/work/toolkit_3/toolkit/lib/id_validator.rb:17:in `sa_id_validator'
so you can see its getting to the custom validation even though it should have failed the validates_length_of :id_number??
I am not quite sure but i have read at some blog that Rails always runs all validations even if the first one is invalid.
What you can do is to make your custom method in such a way that it would become flexible or bouncy in such a way that i would handle all the cases.
This answer would definitely help you.
Hope it would answer your question

How to represent dynamically derived data? (model without a table?)

I have tables for salespeople, products, and sales_activities (consider these to be 'transactions', but Rails reserves that name, so I'm calling them sales_activities).
For each salesperson, I need to dynamically derive their sales_total for a given day.
To do this, I run through the list of sales_activities, and create my derived content list (as an array of objects that hold salesperson_id & sales_total). I then want to display it in a view somewhat equivalent to an 'index' view of salespeople, but this view does not correspond to any of the existing index views I already have, due to the extra field (sales_total).
My question is how do I best define the class (or whatever) for each instance of my dynamically derived data (salesperson_id + sales_total)? It seems I could use a model without a table (with columns salesperson_id and the derived sales_total). That way, I could build an array of instances of these types as I generate the dynamic content, and then hand off the resulting array to the corresponding index view. However, from reading around, this doesn't seem 'the Rails way'.
I'd really appreciate advice on how to tackle this. The examples I've seen only show cases where a single overall total is required in the index view, and not dynamic content per row that can't be derived by a simple 'sum' or equivalent.
[This is a simplified explanation of the actual problem I'm trying to solve, so I'd appreciate help with the 'dynamically derived view / model without table' problem, rather than a short-cut answer to the simplified problem outlined above, thanks]
Maybe a plain Ruby class would do the trick?
class SalesStats
def initialize(sales_person, date_range = Date.today)
#sales_person = sales_person
#date_range = date_range
end
def results
# return a array or hash (anything which responds to an 'each' method), e.g:
SalesActivity.find(:all, :conditions => { :sales_person => #sales_person, :created_at => #date_range }).group_by(&:sales_person).collect { |person, sales_activities| { :person => person, :total => sales_activities.sum(&:price) } }
end
end
in the view:
<% #sales_stats.results.each do | stat | %>
<%= stat[:person] %> - <%= stat[:total] %>
<% end %>
However like mischa said in the comments this could equally be achieved using a method on SalePerson:
class SalesPerson < AR::Base
has_many :sales_activities
def total_sales(date_range)
sales_activities.find(:all, :conditions => { :created_at => date_range }).collect { ... }
end
end
Note: date_range can be a single date or a range e.g (Date.today-7.days)..Date.today

Even Spaced Primary / Secondary Columns in Rails

I have a set of regions and cities (nested) and want to be able to output them in a few even length columns ordered alphabetically. For example:
[Alberta] [Ontario] [Quebec]
Calgary Hamilton Hull
Edmonton Kitchener Laval
[Manitoba] Ottawa Montreal
Winnipeg Toronto
Waterloo
I took a look at 'in_groups' (and 'in_groups_of') however, I need to group based on the size of a relationship (i.e. the number of cities a region has). Not sure if a good Rails way of doing this exists. Thus far my code looks something like this:
<% regions.in_groups(3, false) do |group| %>
<div class="column">
<% group.each do |region| %>
<h1><%= region.name %></h1>
<% region.cities.each do |city| %>
<p><%= city.name %></p>
<% end %>
<% end %>
</div>
<% end %>
However, certain regions are extremely unbalanced (i.e. have many cities) and don't display correctly.
I agree this should be helper code, not embedded in a view.
Suppose you have the province-to-city map in a hash:
map = {
"Alberta" => ["Calgary", "Edmonton"],
"Manitoba" => ["Winnipeg"],
"Ontario" => ["Hamilton", "Kitchener", "Ottawa", "Toronto", "Waterloo"],
"Quebec" => ["Hull", "Laval", "Montreal"]
}
It's easier to start by thinking about 2 columns. For 2 columns, we want to decide where to stop the 1st column and begin the 2nd. There are 3 choices for this data: between Alberta and Manitoba, Manitoba and Ontario and between Ontario and Quebec.
So let's start by making a function so that we can split a list at several places at once:
def split(items, indexes)
if indexes.size == 0
return [items]
else
index = indexes.shift
first = items.take(index)
indexes = indexes.map { |i| i - index }
rest = split(items.drop(index), indexes)
return rest.unshift(first)
end
end
Then we can look at all of the different ways we can make 2 columns:
require 'pp' # Pretty print function: pp
provinces = map.keys.sort
1.upto(provinces.size - 1) do |i|
puts pp(split(provinces, [i]))
end
=>
[["Alberta"], ["Manitoba", "Ontario", "Quebec"]]
[["Alberta", "Manitoba"], ["Ontario", "Quebec"]]
[["Alberta", "Manitoba", "Ontario"], ["Quebec"]]
Or we can look at the different ways we can make 3 columns:
1.upto(provinces.size - 2) do |i|
(i+1).upto(provinces.size - 1) do |j|
puts pp(split(provinces, [i, j]))
end
end
=>
[["Alberta"], ["Manitoba"], ["Ontario", "Quebec"]]
[["Alberta"], ["Manitoba", "Ontario"], ["Quebec"]]
[["Alberta", "Manitoba"], ["Ontario"], ["Quebec"]]
Once you can do this, you can look for the arrangement where the columns have the most uniform heights. We'll want a way to find the height of a column:
def column_height(map, provinces)
provinces.clone.reduce(0) do |sum,province|
sum + map[province].size
end
end
Then you can use the loop from before to look for the 3 column layout with the least difference between the tallest and shortest columns:
def find_best_columns(map)
provinces = map.keys.sort
best_columns = []
min_difference = -1
1.upto(provinces.size - 2) do |i|
(i+1).upto(provinces.size - 1) do |j|
columns = split(provinces, [i, j])
heights = columns.map {|col| column_height(map, col) }
difference = heights.max - heights.min
if min_difference == -1 or difference < min_difference
min_difference = difference
best_columns = columns
end
end
end
return best_columns
end
That'll give you a list for each column:
puts pp(find_best_columns(map))
=>
[["Alberta", "Manitoba"], ["Ontario"], ["Quebec"]]
This is great because you figure out which provinces belong in each column independently of the model structure, and it doesn't generate HTML directly. So both the models and views can change but you can still reuse this code. Since these functions are self-contained, they're also easy to write unit tests for. If you need to balance 4 columns, you just need to adjust the find_best_columns function, or you could rewrite it recursively to support n columns, where n is another parameter.
if you want to keep them left to right alphabetical, I cannot come up with a good way. Using what you have. Here is something for what I had in mind. This should be divided up into helper/controller/model a bit but should give you an idea if this is something along the lines of what you were thinking
def region_columns(column_count)
regions = Region.all(:include => :cities)
regions.sort!{|a,b| a.cities.size <=> b.cities.size}.invert
columns = Array.new(column_count, [])
regions.each do |region|
columns.sort!{|a,b| a.size <=> b.size}
columns[0] << "<h1>#{region.name}</h1>"
columns[0] << region.cities.map{|city| "<p>#{city.name}</p>"}
columns[0].flatten
end
columns
end
that would give you columns of html that you would just need to loop through in your view.

Resources