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}
Related
I finally got my filterrific get working and its a great gem, if not a little complex for a noob like me.
My original index page was filtering the active records based on those nearby to the user like this:
def index
location_ids = Location.near([session[:latitude], session[:longitude]], 50, order: '').pluck(:id)
#vendor_locations = VendorLocation.includes(:location).where(location_id: location_ids)
#appointments = Appointment.includes(:vendor).
where(vendor_id: #vendor_locations.select(:vendor_id))
end
So this pulls in all of the Appointments with Vendors in the area, but how do I pass this over to the Filterrific search:
#filterrific = initialize_filterrific(
params[:filterrific],
select_options:{ sorted_by: Appointment.options_for_sorted_by, with_service_id: Service.options_for_select },
) or return
#appointments = #filterrific.find.page(params[:page])
respond_to do |format|
format.html
format.js
end
It seems like the Filterrerrific is loading ALL of the appointments by default, but I want to limit to the ones nearby. What am I missing?
What you appear to be missing is a param default_filter_params to filterrific macro in the model. (Your question didn't mention that you made any adjustments to the VendorLocation model, since that is the object that you want to filter, that's where the macro should be called. Maybe you just omitted it from your question...)
From the model docs:
filterrific(
default_filter_params: { sorted_by: 'created_at_desc' },
available_filters: [
:sorted_by,
:search_query,
:with_country_id,
:with_created_at_gte
]
)
You probably found this already, it was on the first page of the documentation, but there's more important stuff in the example application that you need (I ran into this too, when I was just recently using Filterrific for the first time.)
The information on the start page is not enough to really get you started at all.
You have to read a bit further to see the other ways you may need to change your models, model accesses, and views in order to support Filterrific.
The part that makes the default filter setting effective is this default_filter_params hash (NOT select_options, which provides the options for "select" aka dropdown boxes. That's not what you want at all, unless you're doing a dropdown filter.) This hash holds a list of the scopes that need to be applied by default (the hash keys) and the scope parameter is used as the hash value.
That default_filter_params hash may not be the only thing you are missing... You also must define those ActiveRecord scopes for each filter that you want to use in the model, and name these in available_filters as above to make them available to filterrific:
scope :with_created_at_gte, lambda { |ref_date|
where('created_at >= ?', ref_date)
end
It's important that these scopes all take an argument (the value comes from the value of the filter field on the view page, you must add these to your view even if you want to keep them hidden from the user). It's also important that they always return ActiveRecord associations.
This is more like what you want:
scope :location_near, lambda { |location_string|
l = Location.near(location_string).pluck(:id)
where(location_id: l)
end
The problem with this approach is that in your case, there is no location_string or any single location variable, you have multiple coordinates for your location parameters. But you are not the first person to have this problem at all!
This issue describes almost exactly the problem you set out to solve. The author of Filterrific recommended embedding the location fields into hidden form fields in a nested fields_for, so that the form can still pass a single argument into the scope (as in with_distance_fields):
<%= f.fields_for :with_distance do |with_distance_fields| %>
<%= with_distance_fields.hidden_field :lat, value: current_user.lat %>
<%= with_distance_fields.hidden_field :lng, value: current_user.lng %>
<%= with_distance_fields.select :distance_in_meters,
#filterrific.select_options[:with_distance] %>
<% end %>
... make that change in your view, and add a matching scope that looks something like (copied from the linked GitHub issue):
scope :with_distance, -> (with_distance_attrs) {
['lng' => '-123', 'lat' => '49', 'distance_in_meters' => '2000']
where(%{
ST_DWithin(
ST_GeographyFromText(
'SRID=4326;POINT(' || courses.lng || ' ' || courses.lat || ')'
),
ST_GeographyFromText('SRID=4326;POINT(%f %f)'),
%d
)
} % [with_distance_attrs['lng'], with_distance_attrs['lat'], with_distance_attrs['distance_in_meters']])
}
So, your :with_distance scope should go onto the VendorLocation model and it should probably look like this:
scope :with_distance, -> (with_distance_attrs) {
lat = with_distance_attrs['lat']
lng = with_distance_attrs['lng']
dist = with_distance_attrs['distance']
location_ids = Location.near([lat, lng], dist, order: '').pluck(:id)
where(location_id: location_ids)
end
Last but not least, you probably noticed that I removed your call to includes(:location) — I know you put it there on purpose, and I didn't find it very clear in the documentation, but you can still get eager loading and have ActiveRecord optimize into a single query before passing off the filter work to Filterrific by defining your controller's index method in this way:
def index
#appointments = Appointment.includes(:vendor).
filterrific_find(#filterrific).page(params[:page])
end
Hope this helps!
I'm trying to create a record and increment two non primary id columns in an after create callback with a integer and a date. The first column needs to be auto incremented with an integer from 1 to given number. The second column needs to be incremented with a date from the start date specified and by either 7, 14, or 30 days which is also specified. I tried creating the first record with the first value then incrementing from there but all records just have the same integer or date saved.
Here's the code
def create_positions
#slots = (self.slots - 1)
#payout_date = (self.start_date)
#position = Position.create(:susu_id => self.id, :user_id => 2, :position_number => 1, :pay_in => self.contribution, :payout_date => (#payout_date + self.frequency_in_days.days))
#positions= self.positions.map { |p| p["position_number"] }.last.to_i
#position_number = (#positions += 1)
#dates = self.positions.map { |d| d["payout_date"] }.last
#add_date = (#dates + self.frequency_in_days.days)
#all_positions = #slots.times {Position.create(:susu_id => self.id, :user_id => 2, :position_number => #position_number, :pay_in => self.contribution, :payout_date => #add_date) }
end
What i get is: 1 2 2 2 and 1/2/14 1/9/14 1/9/14 1/9/14
instead of: 1 2 3 4 and 1/2/14 1/9/14 1/16/14 1/23/14
My lack of reputation prohibits me from commenting on your original post, so I'll just have to do my best to answer your question without asking for clarification (for I don't fully understand your explanation of what you're trying to do).
It sounds like you are trying to create four Position instances, each subsequent position having a position_number that is one greater than the previous one and having a payout_date that is 7 days later than the previous one.
If this is what you're trying to do, please consider the following:
# in the top of your susu model:
after_create :create_positions
# later in your susu model
def create_positions
4.times do |n|
Position.create(susu: self,
user_id: 2,
position_number: n,
pay_in: contribution,
payout_date: start_date + n * frequency_in_days.days)
end
end
You have a lot of logic in your version of this method, so let me know if I have missed something that you need.
And finally, let me point out a few things about my version of this code:
It uses Ruby 1.9 hash syntax ("susu: self" instead of ":susu => self")
It removes unnecessary calls to self, which is considered bad ruby style
It still has hardcoded user_id: 2, which seems like you're going to need to change at some point
Doing this in an after_create hook will be in the same transaction as the original object being created. This means that if a Position cannot be created for whatever reason (e.g., a database-level unique key or a validates_uniqueness_of on the Position model failing), it will roll back the creation of the original object. If you don't like that behavior you can use the after_commit callback.
Cheers!
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
I'm using the find method as follows:
#records = Effort.find( :all,
:select => 'full_name, taskType, sum(hours)',
:group => 'full_name, taskType',
...
#records.each do |record|
The query works fine. What I am confused about is how to access the sum(hours) value. I've tried the following:
record.sum(hours) # undefined local variable hours
record.sum_hours # sum_hours undefined
record[2] # Gives empty string
record[3] # Same, just double checking where the index might start...
I'm a bit stuck how to access the value! If I add <%= debug #records %> to my view, I see debugging output such as this:
---
- !ruby/object:Effort
attributes:
full_name: admin
taskType: Pre-Sales
sum(hours): '16'
What exactly are you trying to achieve with this query? Are you trying to get the sum of all Effort's hours or group them by some other means?
The query below
#records = Effort.find( :all,
:select => 'full_name, taskType, sum(hours)',
...
Will only ever return 1 value, because you're selecting a sum(hours) in there, which results in SQL aggregating the results into the first row. This means you'll always get your first Effort row, with a sum(hours) field set to the total amount of hours spent on all efforts.
If you just want the sum of all Effort hours, you can do this:
Effort.sum(:hours)
If you're looking to sum hours based on some other criteria, please update your question.
EDIT
In this case you could do something like this,
#records = Effort.group(:full_name, :taskType).sum(:hours)
You'll end up with a hash that looks like this:
[full_name, taskType] => count
i.e.
['baking cookies', 'baking'] => 12
['baking cakes', 'baking'] => 2
...
You could iterate over it like:
#records.each do | (full_name, task_type), hours |
puts 'Full Name: #{full_name}, Task Type: #{task_type}, Total Hours: #{hours}'
end
While looking back at my post and the debug output, it suddenly occurred to me it is right there on the page:
record.attributes[ 'sum(hours)' ]
I suppose that stuff like record.full_name is really a convenience method for accessing the attributes array?
not sure if it works, but you might want to try this :
#sums = Effort.sum(:hours,
:group => [:project_task_id, :user_id],
:joins => [:project_task, :user])
it should give you an array of records. You should then normally be able to collect these:
#array = #sums.collect do |aggregate|
[aggregate.project_task.name, aggregate.user.name, aggregate.hours]
end
I am implementing a full text search API for my rails apps, and so far have been having great success with Thinking Sphinx.
I now want to implement a date range search, and keep getting the "bad value for range" error.
Here is a snippet of the controller code, and i'm a bit stuck on what to do next.
#search_options = { :page => params[:page], :per_page => params[:per_page]||50 }
unless params[:since].blank?
# make sure date is in specified format - YYYY-MM-DD
d = nil
begin
d = DateTime.strptime(params[:since], '%Y-%m-%d')
rescue
raise ArgumentError, "Value for since parameter is not a valid date - please use format YYYY-MM-DD"
end
#search_options.merge!(:with => {:post_date => d..Time.now.utc})
end
logger.info #search_options
#posts = Post.search(params[:q], #search_options)
When I have a look at the log, I am seeing this bit which seems to imply the date hasn't been converted into the same time format as the Time.now.utc.
withpost_date2010-05-25T00:00:00+00:00..Tue Jun 01 17:45:13 UTC 2010
Any ideas? Basically I am trying to have the API request pass in a "since" date to see all posts after a certain date. I am specifying that the date should be in the YYYY-MM-DD format.
Thanks for your help.
Chris
EDIT: I just changed the date parameters merge statement to this
#search_options.merge!(:with => {:post_date => d.to_date..DateTime.now})
and now I get this error
undefined method `to_i' for Tue, 25 May 2010:Date
So obviously there is something still not setup right...
lets say d = "2010-12-10"
:post_date => (d.to_time.to_i..Time.now.to_i) would have gotten you there. I just did this in my project and it works great
I finally solved this, but it takes a slightly different approach but it works fine.
I was trying to put the date-range search inside a sphinx_scope (in the model) or as a :condition or :with (in the controller). This did not work, so instead I had to implement it inside the define_index in the model.
So what I did was put a check in the define_index to see if a record fell within a date range, the date range being defined by some SQL code, as shown below. In this case, I wanted to see if "start_date" fell within a date between now and 30 days ago, and an "end_date" fell within today and 30 days from now.
If the dates fell within the ranges, the code below causes the :live to be 0 or 1, depending on whether it falls outside or inside the date ranges (respectively):
define index do
# fields:
...
# attributes:
has "CASE WHEN start_date > DATE_ADD(NOW(), INTERVAL -30 DAY) AND end_date < DATE_ADD(NOW(), INTERVAL 30 DAY) THEN 1 ELSE 0 END", :type => :integer, :as => :live
...
# delta:
...
end
Then in your controller, all you have to do is check if :live => 1 to obtain all records that have start_dates and end_dates within the date ranges.
I used a sphinx_scope like this:
sphinx_scope(:live) {
{ :with => { :live => 1 } }
}
and then in my controller:
#models = Model.live.search(...)
To make sure it works well, you of course need to implement frequent reindexing to make sure the index is up to date, i.e. the correct records are :live => 1 or 0!
Anyway, this is probably a bit late for you now, but I implemented it and it works like a charm!!!
Wouldn't it work if you replaced
d = DateTime.strptime(params[:since], '%Y-%m-%d')
by
Time.parse(params[:since]).strftime("%Y-%m-%d")
(It seems the first one doesn't return a date in the expected format)