Internationalization for constants-hashes in rails 3 - ruby-on-rails

Could you tell me whats the best practice for storing constants with internationalization in rails3?
f.e. i want to have a constant-hash for haircolours for my user model:
# btw: how can I store such hashes in the locales.yml-files?
# en.yml
HAIR_COLOURS = { "brown" => 0, "white" => 1, "red" => 2, "dark-brown" => 3...}
# de.yml
HAIR_COLOURS = { "braun" => 0, "weiss" => 1, "rot" => 2, "dunkel-braun" => 3...}
# i18n.default_locale = :de
User.find(1).haircolour
=> 0
User.find(1).haircolour_str
=> "brown"
# i18n.default_locale = :de
User.find(1).haircolour
=> 0
User.find(1).haircolour_str
=> "braun"

I would suggest the following. Create a string column for the hair colour. This would normally be an enumeration column (ENUM), but this isn't supported by Rails unless you're okay with some SQL in your migrations.
In your model, restrict the colours to a few valid values.
class User < ActiveRecord::Base
# Store the colours in the database as string identifiers (my preference
# would be English, lower case, with underscores). Only accept known values.
validates_inclusion_of :hair_colour, :in => %w{brown white red dark_brown}
end
Then, in config/locales/en.yml:
en:
user:
hair_colours:
brown: brown
white: white
red: red
dark_brown: dark brown
And in config/locales/de.yml:
de:
user:
hair_colours:
brown: braun
white: weiss
red: rot
dark_brown: dunkelbraun
In any view, you can do:
<%= t "user.hair_colours.#{#user.hair_colour}" %>
Or you can write a helper method in app/helpers/users_helper.rb:
def translated_hair_colour(user)
t "user.hair_colours.#{user.hair_colour}"
end
Because I believe that translation is in principle a concern of the presentation, I would not create a method on the User model, but in principle there is nothing stopping you from doing:
class User
# ...
def hair_colour_name
I18n.t "user.hair_colours.#{hair_colour}"
end
end
Update:
Making select boxes in a view that are translated can be done in two ways. The first option is to use the translated values as a source. This requires the translations to be complete and accurate. If not all values are translated, the missing values will not be displayed in the select box.
<%= form_for #user do |user| %>
<%= user.select :hair_colour, t("user.hair_colours").invert %>
<%= user.submit %>
<% end %>
The second option is to use the validation values from your model. This is the "right" way, but it requires a slight adjustment to the setup of the validation.
class User < ActiveRecord::Base
HAIR_COLOURS = %w{brown white red dark_brown}
validates_inclusion_of :hair_colour, :in => HAIR_COLOURS
end
Now, in your views:
<%= form_for #user do |user| %>
<%= user.select :hair_colour,
User::HAIR_COLOURS.map { |c| [t("user.hair_colours.#{c}"), c] } %>
<%= user.submit %>
<% end %>
Of course, the mapping can be easily extracted into a helper.

Related

Select_tag: Style and Modify Options Upon Condition

I have a form_categories table which has a column called active which is of type tinyint/boolean. If the form category record's active attribute is true, then it is active. If false, then it is inactive.
I have a select box that displays all of the records within the form_categories table. I want to style the inactive form_category options red in order to convey to the user that that form_category is inactive. Or Even better, I'd like to put in parentheses next to each inactive form_category option: (inactive) in red letters.
Is this possible?
Below is my select box:
<%= form_tag some_path, method: :get do %>
<%= label_tag "Choose Category" %><br>
<%= select_tag :category_id, options_from_collection_for_select(FormCategory.all, :id, :name), include_blank: true %>
<% end %>
You can use options_for_select and provide the options hash yourself:
options_for_select(form_categories_options, 1) # Where 1 is the current selected option; you would use some value passed from the controller for it
For form_categories_options, you can use a helper, like:
def form_categories_options
FormCategory.all.map do |form_category|
if form_category.inactive
["#{form_category.name} (inactive)", form_category.id]
else
[form_category.name, form_category.id]
end
end
end
If you really want to use options_from_collection_for_select, you can tweak the third argument, namely the text_method: you can define a formatted_name method in your FormCategory model:
class FormCategory < ActiveRecord::Base
...
def formatted_name
if inactive
"#{name} (inactive)"
else
name
end
end
...
end
then use it:
options_from_collection_for_select(FormCategory.all, :id, :formatted_name)

Combine multiple scope or where queries with OR

How do I get the arel components in such a ways that I can do something like:
queries = []
queries << MyModel.some_scope.get_the_arel_component
queries << MyModel.some_scope_with_param("Dave").get_the_arel_component
queries << MyModel.where(:something => 'blah').get_the_arel_component
queries << MyModel.some_scope_with_join_and_merge.get_arel_component
# etc ... (may be any number of queries)
# join each query with OR
combined_query = nil
queries.each do |query|
combined_query ||= query
combined_query = combined_query.or(q)
end
# run the query so it just works
MyModel.where(combined_query)
I've encountered some issues with accepted answers of similar questions.
Lets say I have a class like so:
class Patient
has_one :account
scope :older_than, ->(date) { where(arel_table[:dob].lt(date)) }
scope :with_gender, ->(gender) { where(:gender => gender) }
scope :with_name_like, ->(name) { where("lower(name) LIKE ?", name.downcase) }
scope :in_arrears, -> { joins(:account).merge( Account.in_arrears ) }
end
The goal is to combine any scope or where clause with an OR.
One way would be Patient.with_name_like("Susan") | Patient.with_name_like("Dave"). This seems to run each individual query separately instead of combine into a single query. I've ruled this solution out.
Another method that only works in some instances is:
# this fails because `where_values` for the `with_name_like` scope returns a string
sues = Patient.with_name_like("Susan").where_values.reduce(:and)
daves = Patient.with_name_like("Dave").where_values.reduce(:and)
Patient.where(sues.or(daves))
# this works as `where_values` returns an `Arel::Nodes::Equality` object
ages = Patient.older_than(7.years.ago).where_values.reduce(:and)
males = Patients.with_gender('M').where_values.reduce(:and)
Patient.where(ages.or(males))
# this fails as `in_arrears` scope requires a joins
of_age = Patient.older_than(18.years.ago).where_values.reduce(:and)
arrears = Patients.in_arrears.where_values.reduce(:and)
Patient.where(of_age.or(arrears)) # doesn't work as no join on accounts
Patient.join(:account).where(of_age.or(arrears)) # does work as we have our join
To sum up, the issues with ORing queries arise when where is passed a string or the query requires a join.
I'm pretty sure where converts anything passed to it into an arel object, it's just a matter of getting access to the correct pieces and recombining them in the correct way. I just haven't managed to work it out yet.
Preferably the answer will only make use of ActiveRecord and AREL and not a third party library.
Since you're open to using a third party library, how about Ransack?
It has a very robust implementation allowing for all kinds of and and or condition combinations and works well with associated models as well.
For a use case like yours where there are a few predefined queries/scopes that I want the user to be able to select from and run the or combination of them, I use ransack's out of the box implementation and then on the view level, I use javascript to insert hidden fields with values that will result in the structured params hash ransack is expecting in the controller.
All of your scopes are simple to define in a view using ransack helpers. Your code should look like:
Controller
def index
#q = Patient.search(params[:q])
#patients = #q.result(distinct: true)
end
View
<%= search_form_for #q do |f| %>
<%= f.label :older_than %>
<%= f.date_field :dob_lt %>
<%= f.label :with_gender %>
<%= f.text_field :gender_eq %>
<%= f.label :with_name_like %>
<%= f.text_field :name_cont %>
<%= f.label :in_arrears_exceeding %>
<%= f.text_field :accounts_total_due_gte %>
<%= f.submit %>
<% end %>
Also, if you want more control over the anding and oring take a look at the complex search form builder example using ransack.
I had worked on a similar problem in one of my previous projects. The requirement was to find a set of volunteers to scribe matching a set of criteria like email, location, stream of study etc. The solution that worked for me is to define fine-grained scopes and writing up my own query builder like this:
class MatchMaker
# Scopes
# Volunteer => [ * - 'q' is mandatory, # - 'q' is optional, ** - 's', 'e' are mandatory ]
# active - activation_state is 'active'
# scribes - type is 'scribe'
# readers - type is 'reader'
# located - located near (Geocoder)
# *by_name - name like 'q'
# *by_email - email like 'q'
# educated - has education and title is not null
# any_stream - has education stream and is not null
# *streams - has education stream in 'q'
# #stream - has education stream like 'q'
# #education - has education and title like 'q'
# *level - education level (title) is 'q'
# *level_lt - education level (title) is < 'q'
# *level_lteq - education level (title) is <= 'q'
# *marks_lt - has education and marks obtained < 'q'
# *marks_lteq - has education and marks obtained <= 'q'
# *marks_gt - has education and marks obtained > 'q'
# *marks_gteq - has education and marks obtained >= 'q'
# *knows - knows language 'q'
# *reads - knows and reads language 'q'
# *writes - knows and writes language 'q'
# *not_engaged_on - doesn't have any volunteering engagements on 'q'
# **not_engaged_between - doesn't have any volunteering engagements betwee 'q' & 'q'
# #skyped - has skype id and is not null
def search(scope, criteria)
scope = scope.constantize.scoped
criteria, singular = singular(criteria)
singular.each do |k|
scope = scope.send(k.to_sym)
end
if criteria.has_key?(:not_engaged_between)
multi = criteria.select { |k, v| k.eql?(:not_engaged_between) }
criteria.delete(:not_engaged_between)
attrs = multi.values.flatten
scope = scope.send(:not_engaged_between, attrs[0], attrs[1])
end
build(criteria).each do |k, v|
scope = scope.send(k.to_sym, v)
end
scope.includes(:account).limit(Configuration.service_requests['limit']).all
end
def build(params)
rejects = ['utf8', 'authenticity_token', 'action']
required = ['by_name', 'by_email', 'by_mobile', 'streams', 'marks_lt', 'marks_lteq', 'marks_gt',
'marks_gteq', 'knows', 'reads', 'writes', 'not_engaged_on', 'located', 'excluding',
'level', 'level_lt', 'level_lteq']
optional = ['stream', 'education']
params.delete_if { |k, v| rejects.include?(k) }
params.delete_if { |k, v| required.include?(k) && v.blank? }
params.each { |k, v| params.delete(k) if optional.include?(k.to_s) && v.blank? }
params
end
def singular(params)
pattrs = params.dup
singular = ['active', 'scribes', 'readers', 'educated', 'any_stream', 'skyped']
original = []
pattrs.each { |k, v| original << k && pattrs.delete(k) if singular.include?(k.to_s) }
[pattrs, original]
end
end
The form would be something like this:
...
<%= f.input :paper ... %>
<%= f.input :writes ... %>
<%= f.input :exam_date ... %>
<%= f.time_select :start_time, { :combined => true, ... } %>
<%= f.time_select :end_time, { :combined => true, ... } %>
<fieldset>
<legend>Education criteria</legend>
<%= f.input :streams, :as => :check_boxes,
:collection => ...,
:input_html => { :title => 'The stream(s) from which the scribe can be taken' } %>
<%= f.input :education, :as => :select,
:collection => ...,
:input_html => { :class => 'input-large', :title => configatron.scribe_request.labels[:education]}, :label => configatron.scribe_request.labels[:education] %>
<%= f.input :marks_lteq, :label => configatron.scribe_request.labels[:marks_lteq],
:wrapper => :append do %>
<%= f.input_field :marks_lteq, :title => "Marks", :class => 'input-mini' %>
<%= content_tag :span, "%", :class => "add-on" ... %>
<% end %>
</fieldset>
...
And finally
# Start building search criteria
criteria = service_request.attributes
...
# do cleanup of criteria
MatchMaker.new.search('<Klass>', criteria)
This has worked for me very well in the past. Hope this would lead you in the right direction in solving the problems you are facing. All the best.

Using two separate fields for the same parameter in a Rails form handler?

I'm new to Rails and am fixing a Rails 2 site. I have a form that lets the user add information for the starting location (:start) EITHER with an input OR with a dropdown field. However, I have found that when I include both options, only the dropdown (which comes last) submits data, while the input is ignored. What's the right way to include both options?
MY VIEW
<% form_for #newsavedmap, :html=>{:id=>'createaMap'} do |f| %>
<%= f.error_messages %>
<p>Enter a street address, city, and state:
<%= f.text_field :start, {:id=>"startinput", :size=>50}%></p>
<p>Or, select a location from the list:
<%= f.select :start, options_for_select(#itinerary.locations), {:include_blank => true }, {:id=>"startdrop"} %>
<input type="submit" id="savethismap" value="Save Map">
<% end %>
One way to achieve this is by using virtual attributes. Since both fields map to same attribute, you are going to have to pick which one to use.
# app/models/newsavedmap.rb
class Newsavedmap < ActiveRecord::Base
...
attr_accessible :start_text, :start_select
...
def start_text=(value)
#start_text = value if value
prepare_start
end
def start_select=(value)
#start_select = value if value
prepare_start
end
# start_text will fall back to self.start if #start_text is not set
def start_text
#start_text || self.start
end
# start_select will fall back to self.start if #start_select is not set
def start_select
#start_select || self.start
end
private
def prepare_start
# Pick one of the following or use however you see fit.
self.start = start_text if start_text
self.start = start_select if start_select
end
end
Then your form needs to use the virtual attributes:
<%= f.text_field :start_text, {:id=>"startinput", :size=>50}%></p>
<p>Or, select a location from the list:
<%= f.select :start_select, options_for_select(#itinerary.locations), {:include_blank => true }, {:id=>"startdrop"} %>
Other options are:
Use text_field as the primary and update it's value with selected option if user selects an option.
Add a hidden field in your form and use JavaScript to update the hidden field's value when text_field text gets updated or select option changes

Move Multiple-Input Virtual Attributes to SimpleForm Custom Input Component

Height is stored in the database in inches.
However feet and inches need their own individual inputs in the form:
Height: [_______] feet [_______] inches
So I used virtual attributes, and got it working. Here is a simplified version of my model:
class Client < ActiveRecord::Base
attr_accessible :name, :height_feet, :height_inches
before_save :combine_height
def height_feet
height.floor/12 if height
end
def height_feet=(feet)
#feet = feet
end
def height_inches
if height && height%12 != 0
height%12
end
end
def height_inches=(inches) #on save?
#inches = inches
end
def combine_height
self.height = #feet.to_d*12 + #inches.to_d #if #feet.present?
end
end
And the _form partial using simple_form:
<%= simple_form_for(#client) do |f| %>
<ul>
<%= f.error_notification %>
<%= f.input :name %>
<%= f.input :weight %>
<li>
<%= f.input :height_feet, :label => 'Height', :wrapper => false %>
<span>feet</span>
<%= f.input :height_inches, :label => false, :wrapper => false %>
<span>inches</span>
</li>
<%= f.error :base %>
</ul>
<%= f.button :submit %>
<% end %>
This works. But it is not ideal.
I'd like to DRY this up and create a custom input component so I can add height to the form with <%= f.input :height, as: :feet_and_inch %>—and therefore any other input that follows the same pattern such as <%= f.input :wingspan, as: :feet_and_inch %>.
I've experimented with custom components, but I can't get two inputs to display—and I'm not sure where is the best place to put the 'conversion' logic from feet and inches to inches (and likewise from inches back to feet and inches).
As far as I know, you can't really move anything but the rendering to custom input. SimpleForm doesn't get called once the form is submitted so it can't really interfere with the values in any way. I would love to be wrong about this as I needed it in the past also. Anyway, here's a version that keeps the conversion logic in the model.
The custom SimpleForm input:
# app/inputs/feet_and_inch_input.rb
class FeetAndInchInput < SimpleForm::Inputs::Base
def input
output = ""
label = #options.fetch(:label) { #attribute_name.to_s.capitalize }
feet_attribute = "#{attribute_name}_feet".to_sym
inches_attribute = "#{attribute_name}_inches".to_sym
output << #builder.input(feet_attribute, wrapper: false, label: label)
output << template.content_tag(:span, " feet ")
output << #builder.input(inches_attribute, wrapper: false, label: false)
output << template.content_tag(:span, " inches ")
output.html_safe
end
def label
""
end
end
The form. Note that I did not put the <li> tags inside the custom input, I think this way it's more flexible but feel free to change it.
# app/views/clients/_form.html.erb
<li>
<%= f.input :height, as: :feet_and_inch %>
</li>
All of this relies on the fact that for every height attribute, you also have height_feet and height_inches attributes.
Now for the model, I am not honestly sure if this is the way to go, maybe someone might come up a better solution, BUT here it goes:
class Client < ActiveRecord::Base
attr_accessible :name
["height", "weight"].each do |attribute|
attr_accessible "#{attribute}_feet".to_sym
attr_accessible "#{attribute}_inches".to_sym
before_save do
feet = instance_variable_get("##{attribute}_feet_ins_var").to_d
inches = instance_variable_get("##{attribute}_inches_ins_var").to_d
self.send("#{attribute}=", feet*12 + inches)
end
define_method "#{attribute}_feet" do
value = self.send(attribute)
value.floor / 12 if value
end
define_method "#{attribute}_feet=" do |feet|
instance_variable_set("##{attribute}_feet_ins_var", feet)
end
define_method "#{attribute}_inches=" do |inches|
instance_variable_set("##{attribute}_inches_ins_var", inches)
end
define_method "#{attribute}_inches" do
value = self.send(attribute)
value % 12 if value && value % 12 != 0
end
end
end
It basically does the same but defines the methods dynamically. You can see at the top there's a list of attributes for which you want these methods to be generated.
Note that all of this is not really thoroughly tested and might kill your cat but hopefully can give you some ideas.
My humble opinion is that you would give better user experience if the user inputs the data in just one field . Here are my concerns :
Assuming you are using heights in limited range (probably human's height) , you can write a validation that detects what is the user input - inches or feet . Then you could make a validation link (or better a button ) asking if the input is what it meant to be (inches or feet detected) .
All this (including the dimension transformation while it's just inches -> feet) can be done in javascript , you can fetch the current dimensions by Ajax call and avoid reloading the whole code of the page .
EDIT : I've found an interesting point of view related with complicated inputs . Another useful resource about user interaction in filling form with feet and inches .
Your question is really interesting and I would love to see the solution you choose .

How to show a serialized Array attribute for a Rails ActiveRecord Model in a form?

We're using the "serialize" feature of ActiveRecord in Rails like this:
class User < ActiveRecord::Base
serialize :favorite_colors, Array
....
end
So we can have
u = User.last
u.favorite_colors = [ 'blue', 'red', 'grey' ]
u.save!
So basically ActiveRecord is serializing the array above and stores it in one database field called favorite_colors.
My question is: How do you allow a user to enter his favorite colors in a form?
Do you use a series of textfields? And once they're entered, how do you show them in a form for him to edit?
This is a question related to Rails Form Helpers for serialized array attribute.
Thanks
If you want multi-select HTML field, try:
= form_for #user do |f|
= f.select :favorite_colors, %w[full colors list], {}, :multiple => true
If you're using simple_form gem, you can present the options as check boxes easily:
= simple_form_for #user do |f|
= f.input :favorite_colors, as: :check_boxes, collection: %w[full colors list]
I have solved this problem by 'flattening' the array in the view and
reconstituting the array in the controller.
Some changes are needed in the model too, see below.
class User < ActiveRecord::Base
serialize :favorite_colors, Array
def self.create_virtual_attributes (*args)
args.each do |method_name|
10.times do |key|
define_method "#{method_name}_#{key}" do
end
define_method "#{method_name}_#{key}=" do
end
end
end
end
create_virtual_attributes :favorite_colors
end
If you don't define methods like the above, Rails would complain about the form element's
names in the view, such as "favorite_colors_0" (see below).
In the view, I dynamically create 10 text fields, favorite_colors_0, favorite_colors_1, etc.
<% 10.times do |key| %>
<%= form.label :favorite_color %>
<%= form.text_field "favorite_colors_#{key}", :value => #user.favorite_colors[key] %>
<% end %>
In the controller, I have to merge the favorite_colors_* text fields into an array BEFORE calling
save or update_attributes:
unless params[:user].select{|k,v| k =~ /^favorite_colors_/}.empty?
params[:user][:favorite_colors] = params[:user].select{|k,v| k =~ /^favorite_colors_/}.values.reject{|v| v.empty?}
params[:user].reject! {|k,v| k=~ /^favorite_colors_/}
end
One thing I'm doing is to hard-code 10, which limits how many elements you can have in the favorite_colors array. In the form, it also outputs 10 text fields. We can change 10 to 100 easily. But we will still have a limit. Your suggestion on how to remove this limit is welcome.
Hope you find this post useful.
To allow access to AR attributes, you have to grant them like this:
class User < ActiveRecord::Base
serialize :favorite_colors, Array
attr_accessible :favorite_colors
....
end

Resources