How to represent dynamically derived data? (model without a table?) - ruby-on-rails

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

Related

How to pass default filter into Filterrific get

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!

Fill array with nils for when nothing returned from query in ruby (RoR)

I have a model called foo with a date field.
On my index view, I am showing a typical "weekly view" for a specified week. To put the data in my view, I loop through each day of the specified week and query the data one day at time. I do this so that I can make sure to put a NIL on the correct day.
foos_controller.rb
for day in 0..6
foo = Foo.this_date(#date+day.days).first
#foos[day] = foo
end
index.html.haml
- for day in 0..6
%li
- if #foos[day].nil?
Create a new foo?
- else
Display a foo information here
Obviously, there's a lot of things wrong here.
I should find someone smart member to tell me how to write a good query so that I only have to do it once.
I should not have any if/else in my view
My goal here is to either show the content if the it is there for a particular day or show a "create new" link if not.
thanks for the help in advance!!
First, I have no idea what this_date actually does, but I'll assume it's retrieving a record with a specific date from your datastore. Instead of doing 7 queries, you can condense this into one using a date range:
Foo.where(date: (#date..(#date + 6.days)))
You can tack on a .group_by(&:date) to return something similar to the hash you are manually constructing, but using the actual dates as keys instead of the date offset.
To iterate over the dates in the view, I would recommend using Hash#fetch, which allows you to define a default return when a key is not present, e.g:
hash = { :a => 1, :b => 2 }
hash.fetch(:a){ Object.new } #=> 1
hash.fetch(:c){ Object.new } # #<Object:...>
The question now is what object to substitute for nil. If you want to avoid using conditionals here, I'd recommend going with the NullObject pattern (you could involve presenters as well but that might be a bit overkill for your situation). The idea here is that you would create a new class to substitute for a missing foo, and then simply define a method called to_partial_path on it that will tell Rails how to render it:
class NullFoo
def to_partial_path
"null_foos/null_foo"
end
end
You'll need to create partials at both app/views/foos/_foo.html.erb and app/views/null_foos/_null_foo.html.erb that define what to render in each case. Then, in your view, you can simply iterate thusly:
<% (#date..(#date + 6.days)).each do |date| %>
<%= render #foos.fetch(date){ NullDate.new } %>
<% end %>
Is this appropriate for your situation? Maybe it's also a bit overkill, but in general, I think it's a good idea to get in the habit of avoid nil checks whenever possible. Another benefit of the NullObject is that you can hang all sorts of behavior on it that handle these situations all throughout your app.

rails "where" statement: How do i ignore blank params

I am pretty new to Rails and I have a feeling I'm approaching this from the wrong angle but here it goes... I have a list page that displays vehicles and i am trying to add filter functionality where the user can filter the results by vehicle_size, manufacturer and/or payment_options.
Using three select form fields the user can set the values of :vehicle_size, :manufacturer and/or :payment_options parameters and submit these values to the controller where i'm using a
#vehicles = Vehicle.order("vehicles.id ASC").where(:visible => true, :vehicle_size => params[:vehicle_size] )
kind of query. this works fine for individual params (the above returns results for the correct vehicle size) but I want to be able to pass in all 3 params without getting no results if one of the parameters is left blank..
Is there a way of doing this without going through the process of writing if statements that define different where statements depending on what params are set? This could become very tedious if I add more filter options.. perhaps some sort of inline if has_key solution to the effect of:
#vehicles = Vehicle.order("vehicles.id ASC").where(:visible => true, if(params.has_key?(:vehicle_size):vehicle_size => params[:vehicle_size], end if(params.has_key?(:manufacturer):manufacturer => params[:manufacturer] end )
You can do:
#vehicles = Vehicle.order('vehicles.id ASC')
if params[:vehicle_size].present?
#vehicles = #vehicles.where(vehicle_size: params[:vehicle_size])
end
Or, you can create scope in your model:
scope :vehicle_size, ->(vehicle_size) { where(vehicle_size: vehicle_size) if vehicle_size.present? }
Or, according to this answer, you can create class method:
def self.vehicle_size(vehicle_size)
if vehicle_size.present?
where(vehicle_size: vehicle_size)
else
scoped # `all` if you use Rails 4
end
end
You call both scope and class method in your controller with, for example:
#vehicles = Vehicle.order('vehicles.id ASC').vehicle_size(params[:vehicle_size])
You can do same thing with remaining parameters respectively.
The has_scope gem applies scope methods to your search queries, and by default it ignores when parameters are empty, it might be worth checking

grouped_collection_select with I18n

I've been trying to come up with a way to declare array constants in a class, and then present the members of the arrays as grouped options in a select control. The reason I am using array constants is because I do not want the options being backed by a database model.
This can be done in the basic sense rather easily using the grouped_collection_select view helper. What is not so straightforward is making this localizable, while keeping the original array entries in the background. In other words, I want to display the options in whatever locale, but I want the form to submit the original array values.
Anyway, I've come up with a solution, but it seems overly complex. My question is: is there a better way? Is a complex solution required, or have I overlooked a much easier solution?
I'll explain my solution using a contrived example. Let's start with my model class:
class CharacterDefinition < ActiveRecord::Base
HOBBITS = %w[bilbo frodo sam merry pippin]
DWARVES = %w[gimli gloin oin thorin]
##TYPES = nil
def CharacterDefinition.TYPES
if ##TYPES.nil?
hobbits = TranslatableString.new('hobbits', 'character_definition')
dwarves = TranslatableString.new('dwarves', 'character_definition')
##TYPES = [
{ hobbits => HOBBITS.map {|c| TranslatableString.new(c, 'character_definition')} },
{ dwarves => DWARVES.map {|c| TranslatableString.new(c, 'character_definition')} }
]
end
##TYPES
end
end
The TranslatableString class does the translation:
class TranslatableString
def initialize(string, scope = nil)
#string = string;
#scope = scope
end
def to_s
#string
end
def translate
I18n.t #string, :scope => #scope, :default => #string
end
end
And the view erb statement look like:
<%= f.grouped_collection_select :character_type, CharacterDefinition.TYPES, 'values[0]', 'keys[0].translate', :to_s, :translate %>
With the following yml:
en:
character_definition:
hobbits: Hobbits of the Shire
bilbo: Bilbo Baggins
frodo: Frodo Baggins
sam: Samwise Gamgee
merry: Meriadoc Brandybuck
pippin: Peregrin Took
dwarves: Durin's Folk
gimli: Gimli, son of Glóin
gloin: Glóin, son of Gróin
oin: Óin, son of Gróin
thorin: Thorin Oakenshield, son of Thráin
The result is:
So, have I come up with a reasonable solution? Or have I gone way off the rails?
Thanks!
From the resounding silence my question received in response, I am guessing that there is not a better way. Anyway, the approach works and I am sticking to it until I discover something better.

Ruby on Rails 3: Associating extra data with a model

Pretty much a total beginner to Ruby/Rails.
I have a Video with many Sections:
#video = Video.find(params[:id])
#sections=#video.sections;
I want to associate a colour attribute with each Section, but the colour is calculated in the controller, rather than stored in the database.
So far I have been simply creating a #colours array in my controller where the index matched up with the index of the section
hues = [41, 6, 189, 117, 279]
saturation = 100;
brightness = 45;
#colours = [];
#sections.each { #colours.push Color::HSL.new(hues[j%hues.length], saturation, brightness) }
so that #sections[i] corresponds to #colours[i].
This works fine, but doesn't seem like the best approach. I would like to extend my Sections model so that it has a 'colour' attribute, so that I could access it by doing #sections[i].colour
I tried putting this in models/sectiondata.rb :
class SectionData
extend Section
attr_accessor :colour
end
but when I try to do SectionData.new in my Controller I get an error saying it can't find the class. I also don't know how I would get the original #section to be part of the new SectionData class.
What is the best way to approach this problem? Any tips on my coding style would also be appreciated, Ruby is a big step away from what I'm used to.
I think its a better idea to implement hashes in this situation instead of having two arrays corresponding to each other.
For example,
result_hash = {sports_section => 'blue'}
First I tried putting them as attr_accessors in the Section model, don't do that. Bad coder! They will not show up if ActiveRecord queries the database again for that page load, even if the query is cached. If you need the ActiveRecord methods you can use ActiveModel
Instead, I created a class for the attribute accessors:
sectiondata.rb:
class SectionData
attr_accessor :left, :width, :timeOffset, :colour, :borderColour
end
Then you can work with them using an object which maps a Section object to SectionData:
xxx_controller.rb
#video = Video.find(params[:id])
#sections=#video.sections
require 'sectiondata.rb'
#sectionDatas=Hash.new
#sections.each do |i|
sd=SectionData.new
# set whatever attributes here, e.g.
sd.left= total/multiplier
#sectionDatas[i]=sd
end
Then, given a Section object called 'section', you could access it using #sectionDatas[section], and this will work for any database queries that occur in the page of

Resources