Is this an efficient way of producing sales reporting / analytics? - ruby-on-rails

I have an app where I receive & ingest daily sales reports from multiple sources. All are structured differently so I store down into my Postgres db in separate tables.
I'm currently doing something like this to iterate over the last 30 days sales for one report source and it seems to work quite nicely. My concern is how efficient & scaleable this will be when I add additional report sources as the way I currently have it structured means I'd have to add and repeat large amounts of the code for each new source.
<% from = Date.today - 30 %> #30 days ago
<% to = Date.today %> #Today
<% step_date = from %>
<% source_one_chart_data = [] %> #Initialise empty array to later pass to JS Chart library
<% begin %>
<% count = #product.sales_source_one.total.where(:report_date => step_date).count %> #check if there are any sales for this product on the current step date
<% if count != 0 %>
<% sale = #product.sum_total_net_by_day(step_date) %>
<% source_one_chart_data.push(sale.to_s) %> #Push sales total to array if sales exist on that date
<% else %>
<% source_one_chart_data.push("0") %> #Otherwise push a zero into array so all 30 days map to a value
<% end %>
<% step_date += 1.day %> #Increase step_date by 1 on each iteration of the loop
<% end while step_date <= to %> #Stop loop when we reach to date
Can anyone offer any guidance on how efficiently bring in additional sales sources without having to repeat code? Also, it would be good if I could change the step from day to week/month/year and have it calculate sales accordingly; so if the sales source is reported daily and the step is week it would sum all values that occur in the step week.

Why do you have all of that code in your view? You should move most of it to your model / controller.
def process_chart_data
from = 1.month.ago.to_date
to = Date.today
step_date = from
chart_data = []
while step_date <= to
sales_total = sales_source_one.total.where(report_date: step_date).count
if sales_total.zero?
chart_data.push(0)
else
sale = sum_total_net_by_day(step_date)
chart_data.push(sale.to_s)
end
step_date += 1
end
return chart_data
end
The above could probably be refactored further, but now if you place it in your Product model then in your controller you can just do:
#product = Product.find(params[:id]) # or whatever
#chart_data = #product.process_chart_data
Then in your view you can use #chart_data.
Moving that code into a method also allows you to update it quicker, lets say I want the user to control how far back records are retrieved:
def process_chart_data(start_date)
from = start_date
...
end
In the controller:
#product = Product.find(params[:id])
#chart_data = #product.process_chart_data(params[:start_date])

Related

Filling in blanks for DATE_TRUNC grouping with rails

The following query to the postgres database
#interventos_2 = Intervento.where(['previsto >= ?', start_date]).group("DATE_TRUNC('week', previsto)").count
generates a hash from which the view can extract the data as follows
<% #interventos_2.each do |w| %>
<%= w[0].strftime('%Y-%W') %> <%= w[1] %><br />
<% end %>
However if there is a blank (count = 0) in the range of weeks we are concerned with, the sequence of years and commercial weeks will look weird and or misleading.
What is an efficient way to declare the range and then fill in the blank weeks with zero?
Update the query is being run via specific sql for performance reasons as the data set is expected to be sufficiently large, frequently changing (cache may not help all that much) and frequently asked.
If I understand you correctly, you want to present a full range of YYYY-WW labels and counts, starting with start_date regardless of whether there's data for a given week. You didn't mention if previsto is a Date, a Time, or a DateTime; I'll assume it's a Time just for maximum inconvenience. =]
I think the main challenge you're struggling with is that you're conflating the dataset with the presentation. I like handling the two separately: first, get the data and put it into a year-week format; then present the range.
For getting the data, I like a more Railsy, less database-specific solution. Your mileage may vary, especially if you've got a large dataset and/or need to make the database do the heavy lifting. Here's a query that gets only the previsto field for each record while also forcing the database to evaluate the date range. Probably the most concise query without having to break out SQL:
#interventos_2 = Intervento.select(:previsto).
where(previsto: (start_date..Time.now)).
map {|iv| iv.previsto.strftime('%Y-%W')}
Note that this also maps the result down to a simple array of YYYY-WW. Speaking of which, let's map out that range of YYYY-WW now:
# make sure the end_date a clean multiple of 7 days from start_date
end_date = Date.today + (7 - (Date.today - start_date.to_date) % 7)
#timespan = (start_date.to_date..end_date).step(7).map {|date| date.strftime('%Y-%W')}
(There are probably much tidier ways to write that)
Given those bits, here's a version of your view code that presents the full range of weeks and the count for that week, even if it's 0:
<% #timespan.each do |yearweek| %>
<%= yearweek %> <%= #interventos_2.count(yearweek) %><br />
<% end %>
In bocca al lupo!
Update: Your update notes that your use case requires the direct SQL query, so here's the same general approach with that in mind:
#interventos_2 = Intervento.where(['previsto >= ?', start_date]).
group("DATE_TRUNC('week', previsto)").count.
map {|timestamp,count| [timestamp.strftime('%Y-%W'), count]}.to_h
# make sure the end_date a clean multiple of 7 days from start_date
end_date = Date.today + (7 - (Date.today - start_date.to_date) % 7)
#timespan = (start_date.to_date..end_date).step(7).map {|date| date.strftime('%Y-%W')}
<% #timespan.each do |yearweek| %>
<%= yearweek %> <%= #interventos_2[yearweek] || 0 %><br />
<% end %>
With the following controller method, the range of weeks and the data required for the view is generated (beginning_of_week for comparing with the has data, in addition to the data itself).
#weeks = []
while start_date < final_date
#weeks[start_date.year] = [] unless #weeks[start_date.year]
#weeks << [start_date.beginning_of_week, start_date.cweek]
start_date += 1.week
end
#weeks.reject!{|a| a.blank?}
Thus the view can generate a layout item for each week and compare it to the hash, and where there is nil, generate zero.
<% #weeks.each do |week| %>
<% if !week.nil? %>
<%= week[0] %>:
<% z = #interventos_2.detect {|f| f[0] == week[0] } %>
<% if !z.nil? %>
<%= z.to_a[1] %>
<% else %>
0
<% end %>
<% end %>

Dynamically show total per page, as I paginate

Rails 3.2
I have the following in my invoices_controller.rb:
def index
#invoices = Invoice.all.paginate(:page => params[:page], per_page: 10)
....
#invoices_total = #invoices.compact.inject(0){ |sum, invoice| sum + invoice.total }
Here's the view (slim):
- if #invoices.any?
tfooter
tr
td colspan="#{checkin_action ? '10' : '9'}" Total
td style='text-align:right'
'$#{number_with_delimiter(#invoices_total)}
This allows me to display the total for each page, in the footer. The problem, is that when I click on next, to move on to the next page, the total does not change. How do I get it to change dynamically with each page change.
Any ideas?
In general, the invoices_total amount should not be changed, no matter what page you are in. The invoices_total can write this way:
#invoices_total = Invoice.count
Or if total is invoics's column,you can:
#invoices_total = Invoice.pluck('total')
I updated my answer.
No need to write this query.
#invoices_total = #invoices.compact.inject(0){ |sum, invoice| sum + invoice.total }
I am giving logic to get the sum of #invoices show in the page.
#Declare a variable to store the sum of the invoices.
<% invoices_total = 0 %>
<% #invoices.each do |invoice|%>
# While looping through your invoices add invoice total to that above variable
<% invoices_total += invoice.total %>
#Rest of your code for show
<% end %>
#Finally the Total Sum like this
<%= invoices_total %>

How to query two tables from database in one controller

I have two tables which are one to many (1 challenge to many entry)
I want to get the last entry for all challenges but I also want to get the title of the challenge for that last entry.
So far I have:
def index
#discovers = Challenge.all.map{|c| c.entries.last}
end
How to I also add the fact I want the Challenge.title?
def index
#challenges = Challenge.all
end
Then inside your view
<% #challenges.each do |challenge| %>
<%= challenge.title %> # will give you challenge title
<%= challenge.entries.last %> # will give you last entry for the challnge
<% end %>

Adding dates to the database that can be used to specify an active week

I am in the process of creating an competition app that needs to be pretty much autonomous. The app has a resource called 'Weeks' and each week has many 'Entries'. Users can add an entry to the week, and then they can be voted on.
The week has a database column called 'start_date' and if I populate this with a string, eg "16-03-2015" then on the Show view I can pull in the weeks params and compare the string to todays date and set whether the vote buttons are active or not.
In my weeks controller I use a before action called set_dates and I set a couple of variables
def set_dates
#today = Date.today
#weekstart = Date.parse(#week.active_date)
#weekend = Date.parse(#week.active_date)+7
end
I can then set if and else statements on the view to show the vote buttons if the week is 'active', ie. todays date is within the active_date + 7 days
<% #entries.each do |entry| %>
<div class="entry">
<%= image_tag(entry.photo(:thumb)) %>
<%= entry.cached_votes_up %>
<% if (#weekstart..#weekend).include? #today %>
<%= link_to "Upvote", upvote_week_entry_path(entry.week_id, entry) %>
<%= link_to "Downvote", downvote_week_entry_path(entry.week_id, entry) %>
<% end %>
</div>
<% end %>
This was fine until I realised I needed to have an 'active week' outside of the show view. I want to put a link into the Navigation Bar in the application_layout to 'This Weeks Comp', and I need this week to point to whichever week voting is active on. But without finding the week by its ID, then cross referencing it's active_date, I am unsure how to do this. Any help would be greatly appreciated.
Thanks!
If anyone else is find this looking to solve a similar problem, I solved the issue by adding an end_date column to the weeks resource, and then assigned a variable to an arel query in the application controller
App-Controller
before_action :active_week
protected
def active_week
#aweek = Week.where("? BETWEEN start_date AND end_date", Date.today)
#active = #aweek.first
end
Application Layout
<%= link_to "This Week", #active %>

How do I do a grouping by year?

I have a books model with a date type column named publish_date. On my views I'm iterating through the books and I want to group the books by year such that I have a heading for every year and books that were published on that year to be listed below the year heading.
So by starting with "2010" all books published on 2010 would be listed, then another heading "2009" with all books published in 2009 listed below it and so forth.
<% #all_books.each do |book| %>
<%=link_to book.title + ", (PDF, " + get_file_size(book.size) + ")" %>
<% end %>
By doing a book.publish_date.strftime("%Y") I am able to get the year but I do not know how to group the entries by year. Any help on this would be appreciated.
You can use group_by (see API) like (of the top of my head
<% #all_books.group_by(&:year).each do |year, book| %>
...
<% end %>
def year
self.created_at.strftime('%Y')
end
< % #all_books.group_by(&:year).each do |year, book| %>
Year < %= year %>
# render books here
< % end %>
What say?
You can use group_by for convenience, but your need can be better served by relying on DB for sorting and a each loop. This avoids the cost of client side sorting and hash manipulations for grouping.
Somewhere in your controller
#all_books = Book.all(:order => "publish_date DESC")
In your view
<%year = nil
#all_books.each do |book|
if year.nil? or year > book.publish_date.year
year = book.publish_date.year
%>
<h1> <%=year%><h1>
<%end % >
<%=link_to book.title + ", (PDF, " + get_file_size(book.size) + ")" %>
<%end %>
The quick and dirty approach is to simply group_by the year and iterate over those:
#all_books.group_by { |b| b.created_at.year }.each do |year, books|
# All books for one year, so put heading here
books.each do |book|
# ...
end
end
This requires sorting within the Rails application, so you will need to retrieve all relevant records in order to have the data properly organized. To do the sort on the server you will probably need to introduce a year column and keep it in sync with the created_at time.

Resources