grouped_collection_select with I18n - ruby-on-rails

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.

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!

How do I use date_select without a model?

I have a form that I want a date in. The date that the controller defines is #date_end. I want this date in the form, but without an AR model this seems REALLY STUPIDLY hard. I used to use this:
= date_select :date_end_, nil, :default => #date_end
But this only works if your 'name' is different from the instance var. If they are the same you get an 'interning string' error.
The only way I've found to solve this is to add a method to Object to return self
class Object
def get_self
self
end
end
Then I can do:
= date_select :date_end, :get_self
But we all know this is stupid. But there doesn't seem to be another way...
Any suggestions?
Would something like this do the trick:
= select_date Date.today, :prefix => :date_end
According to this documentation, under section "4.1 Barebones Helpers", using this particular helper will create a series of select tags for the day, month year. The names should end up looking like "date_end[day]", "date_end[month]"...etc. Let me know if that helps.

Struct with types and conversion

I am trying to accomplish the following in Ruby:
person_struct = StructWithType.new "Person",
:name => String,
:age => Fixnum,
:money_into_bank_account => Float
And I would like it to accept both:
person_struct.new "Some Name",10,100000.0
and
person_struct.new "Some Name","10","100000.0"
That is, I'd like it to do data conversion stuff automatically.
I know Ruby is dinamically and I should not care about data types but this kind of conversion would be handy.
What I am asking is something similar to ActiveRecord already does: convert String to thedatatype defined in the table column.
After searching into ActiveModel I could not figure out how to to some TableLess that do this conversion.
After all I think my problem may require much less that would be offered by ActiveModel modules.
Of course I could implement a class by myself that presents this conversion feature, but I would rather know this has not yet been done in order to not reinvent the wheel.
Tks in advance.
I think that the implementation inside a class is so easy, and there is no overhead at all, so I don't see the reason to use StructWithType at all. Ruby is not only dynamic, but very efficient in storing its instances. As long as you don't use an attribute, there is none.
The implementation in a class should be:
def initialize(name, age, money_into_bank_account)
self.name = name
self.age = age.to_i
self.money_into_bank_account = money_into_bank_account.to_f
end
The implementation in StructWithType would then be one layer higher:
Implement for each type a converter.
Bind an instance of that converter in the class.
Use in the new implementation of StructWithType instances (not class) the converters of the class to do the conversion.
A very first sketch of it could go like that:
class StructWithType
def create(args*)
<Some code to create new_inst>
args.each_with_index do |arg,index|
new_value = self.converter[index].convert(arg)
new_inst[argname[index]]= new_value
end
end
end
The ideas here are:
You have an instance method named create that creates from the factory a new struct instance.
The factory iterates through all args (with the index) and searches for each arg the converter to use.
It converts the arg with the converter.
It stores in the new instance at the argname (method argname[] has to be written) the new value.
So you have to implement the creation of the struct, the lookup for converter, the lookup for the argument name and the setter for the attributes of the new instance. Sorry, no more time today ...
I have used create because new has a different meaning in Ruby, I did not want to mess this up.
I have found a project in github that fulfill some of my requirements: ActiveHash.
Even though I still have to create a class for each type but the type conversion is free.
I am giving it a try.
Usage example:
class Country < ActiveHash::Base
self.data = [
{:id => 1, :name => "US"},
{:id => 2, :name => "Canada"}
]
end
country = Country.new(:name => "Mexico")
country.name # => "Mexico"
country.name? # => true

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

How can I reduce repetition in this Ruby on Rails code?

This is a snippet of code from an update method in my application. The method is POSTed an array of user id's in params[:assigned_ users_ list_ id]
The idea is to synchronise the DB associations entries with the ones that were just submitted, by removing the right ones (those that exist in the DB but not the list) and adding the right ones (vise-versa).
#list_assigned_users = User.find(:all, :conditions => { :id => params[:assigned_users_list_id]})
#assigned_users_to_remove = #task.assigned_users - #list_assigned_users
#assigned_users_to_add = #list_assigned_users - #task.assigned_users
#assigned_users_to_add.each do |user|
unless #task.assigned_users.include?(user)
#task.assigned_users << user
end
end
#assigned_users_to_remove.each do |user|
if #task.assigned_users.include?(user)
#task.assigned_users.delete user
end
end
It works - great!
My first questions is, are those 'if' and 'unless' statements totally redundant, or is it prudent to leave them in place?
My next question is, I want to repeat this exact code immediately after this, but with 'subscribed' in place of 'assigned'... To achieve this I just did a find & replace in my text editor, leaving me with almost this code in my app twice. That's hardly in keeping with the DRY principal!
Just to be clear, every instance of the letters 'assigned' becomes 'subscribed'. It is passed params[:subscribed_ users_ list_ id], and uses #task.subscribed_ users.delete user etc...
How can I repeat this code without repeating it?
Thanks as usual
You don't need if and unless statements.
As for the repetition you can make array of hashes representing what you need.
Like this:
[
{ :where_clause => params[:assigned_users_list_id], :user_list => #task.assigned_users} ,
{ :where_clause => params[:subscribed_users_list_id], :user_list => #task.subscribed_users}
] each do |list|
#list_users = User.find(:all, :conditions => { :id => list[:where_clause] })
#users_to_remove = list[:user_list] - #list_users
#users_to_add = #list_users - list[:user_list]
#users_to_add.each do |user|
list[:user_list] << user
end
#users_to_remove.each do |user|
list[:user_list].delete user
end
end
My variable names are not the happiest choice so you can change them to improve readability.
I seem to be missing something here, but aren't you just doing this?
#task.assigned_users = User.find(params[:assigned_users_list_id])

Resources