My app's pricing plan has some features that are unlimited. For example in the "Pro" version you get 10 forms, and in the "Unlimited" version you get unlimited forms.
This is represented in the Plan model by an integer or Float::Infinity.
pro = Plan.find_by name: 'Pro'
pro.forms_limit
#=> 10
unl = Plan.find_by name: 'Unlimited'
unl.forms_limit
#=> Float::INFINITY
In the view:
= t('.forms', count: plan.forms_limit)
I am trying to find an efficient way to interpolate this with I18n. What I'd like to do is:
plans:
index:
forms:
zero: No Forms
one: 1 Form
other: "%{count} Forms"
infinity: Unlimited Forms
This will work, but it results in undesired output like:
"Infinity Forms"
Is there a way to structure this so that Infinity will interpolate "Unlimited" instead of "Infinity"?
Create a file config/locales/plurals.rb with the following
{
en: {
i18n: {
plural: {
keys: [:one, :other, :infinity],
rule: lambda do |n|
if n == 1
:one
elsif n == Float::INFINITY
:infinity
else
:other
end
end
}
}
}
}
and then in my config/locales/en.yml, I have
en:
forms:
zero: 'No Forms'
one: '1 Form'
other: '%{count} Forms'
infinity: 'Unlimited Forms'
added in config/initializers/locale.rb
I18n::Backend::Simple.send(:include, I18n::Backend::Pluralization)
and testing it in IRB
I18n.t(:forms, count: 0)
# => "No Forms"
I18n.t(:forms, count: 1)
# => "1 Form"
I18n.t(:forms, count: 10)
# => "10 Forms"
I18n.t(:forms, count: Float::INFINITY)
# => "Unlimited Forms"
What is this doing?
This isn't nearly as mysterious as I thought when. I first tried it (getting the idea from this related question.
When you include I18n::Backend::Pluralization it's going to start looking for a key i18n.plural.rule that should respond to call, anywhere in the load path. So the plurals.rb file name is unimportant, just need to make sure it's a file in I18n.load_path.
We make that file a ruby file with a hash so that we can set that i18n.plural.rule key to a lambda (so it responds to call) and then the lambda gets called with the count getting passed in. As pointed out there are
Related
Got a question on how to setup fixtures for Mobility. Would be very grateful for any tips on how to get this going and would be a valuable lesson for me as well on how to tackle setting up fixtures in general.
Not using any gems to setup fixtures, just the default Rails approach for this case. I have a Song model which has multiple translatable attributes, title uses Mobility, description and content use Mobility Action Text.
It works really well but when setting up fixtures I'm finding it difficult to relate the records. There's three tables at play here songs where the only field used is status. mobility_string_translations stores translations for title and action_text_rich_texts stores translated descriptions and content.
This is how my translation setup looks like in Song:
class Song < ApplicationRecord
extend Mobility
validates :title_pt, presence: true
validates :status, inclusion: { in: %w(draft published private) },
presence: true
translates :title, type: :string, locale_accessors: I18n.available_locales
translates :description, backend: :action_text, locale_accessors: I18n.available_locales
translates :content, backend: :action_text, locale_accessors: I18n.available_locales
# file continuation...
As for fixtures songs.yml looks like this:
one:
status: "published"
Then based on what I've found online I've created mobility/string_translations.yml with the following content:
one:
translatable_id: one (Song)
translatable_type: "Song"
key: "title"
value: "Title in English"
locale: "en"
created_at: <%= Time.now %>
updated_at: <%= Time.now %>
two:
translatable_id: one (Song)
translatable_type: "Song"
key: "title"
value: "Titulo em Português"
locale: "pt"
created_at: <%= Time.now %>
updated_at: <%= Time.now %>
This seems to work but I know it isn't because when I inspect #song = songs(:one) looking for translated values (#song.title_pt and #song.title_en) they're both nil.
Any idea on what to do here? 🙏
I think the issue is with how you declared the translatable relationship in the mobility_string_translation table.
It should be either the fully explicit form.
translatable_id: 1
translatable_type: Song
Or the shorthand provided by fixtures.
translatable: one (Song)
Here you are kind of mixing both.
It's documented here in the Polymorphic belongs_to https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html.
I've managed to make the explicit form work but not the shorthand for some reason.
I've been looking for instructions on how to make fixtures work with mobility myself and you provided the heavy lifting with your example so thanks a lot for that.
The issue in my case is that translatable_type was Song instead of "Song" and it couldn't map the records in mobility_string_translations to the correct Song record. Here's a bit more detail on the setup that I have that does work to write tests:
By work I mean, the Mobility translation records defined in fixture files are detected and can be used to compose tests. Running #song.title_en should output a value instead of nil.
Let's consider the following Song model, it has a title that can be translated and a status which is only used to affect the visibility of the song in the front end. Fixtures for a couple of Songs would look like this:
# test/fixtures/songs.yml
one:
id: 1
status: "published"
two:
id: 2
status: "draft"
The id is usually not specified in fixtures but here it becomes necessary so that we're sure which identifier to use when pointing translated records.
The Mobility implementation will store any translated titles at mobility_string_translations the following can be added to test/fixtures/mobility/string_translations.yml:
# test/fixtures/mobility/string_translations.yml
song_one_en:
translatable_id: 1
translatable_type: "Song"
key: "title"
value: "Maçaranduba Wood"
locale: "en"
created_at: <%= Time.now %>
updated_at: <%= Time.now %>
song_one_pt:
translatable_id: 1
translatable_type: "Song"
key: "title"
value: "Madeira de Maçaranduba"
locale: "pt"
created_at: <%= Time.now %>
updated_at: <%= Time.now %>
song_two_en:
translatable_id: 2
translatable_type: "Song"
key: "title"
value: "Dona Maria from Camboatá"
locale: "en"
created_at: <%= Time.now %>
updated_at: <%= Time.now %>
song_two_pt:
translatable_id: 2
translatable_type: "Song"
key: "title"
value: "Dona Maria do Camboatá"
locale: "pt"
created_at: <%= Time.now %>
updated_at: <%= Time.now %>
Each song includes a title for English and Portuguese in this case but any locales the record is going to make use of or need to be tested can be included here, in an individual record.
The important aspect here is that all translatable_type columns are explicit string types.
For example, do "Song", instead of Song when adding a value to the property.
Setting up fixtures with this method associates translated properties to a record and enables them to be accessed in a test.
For example, to change the title of a song, the record can be brought into the test in a setup block and the title translations will be available and can be modified:
# test/controllers/song_controller_test.rb
require "test_helper"
class SongControllerTest < ActionDispatch::IntegrationTest
setup do
#song = songs(:one)
end
test "admin can edit a song" do
# Keeps a copy of the original record for comparison.
current_record = #song
# Passes the locale to the request helper to keep it from getting confused with the record id.
# Changes the title of the record.
patch song_url(I18n.locale, #song, { song: { title_en: 'Updated Song Title' } })
# Retrieves the same record to be used for comparison.
updated_record = Song.find(#song.id)
# Checks that a change actually occurred.
assert current_record.updated_at != updated_record.updated_at
# Checks that the list of songs is being displayed to the user.
assert_redirected_to songs_path
end
end
To make sure that the fixture has setup the association bettween the model and the translated records, the debugger method can be used. Start by adding it as a breakpoint to your test logic, in this case I'm going to use the example above:
# test/controllers/song_controller_test.rb
test "admin can edit a song" do
current_record = #song
patch song_url(I18n.locale, #song, { song: { title_en: 'Updated Song Title' } })
updated_record = Song.find(#song.id)
debugger # <-- The script will pause here.
assert current_record.updated_at != updated_record.updated_at
assert_redirected_to songs_path
end
Then the test can be run, bin/rails test would work but in this example the command to run just the tests for this file would be:
bin/rails test test/controllers/role_controller_test.rb
The output in the terminal will look similar to this, the program will be paused at this point and it is interactive:
bin/rails test test/controllers/song_controller_test.rb
Running 26 tests in a single process (parallelization threshold is 50)
Run options: --seed 56548
# Running:
.............[64, 73] in ~/Projects/rails_app/test/controllers/song_controller_test.rb
64|
65| current_record = #song
66| patch song_url(I18n.locale, #song, { song: { title_en: 'Updated Song Title' } })
67| updated_record = Song.find(#song.id)
68|
=> 69| debugger # <-- The script will pause here.
70|
71| # Checks that a change actually occurred.
72| assert current_record.updated_at != updated_record.updated_at
73|
=>
#0 block in <class:SongControllerTest> at ~/Projects/rails_app/test/controllers/song_controller_test.rb:69
#1 block in run (3 levels) at ~/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.15.0/lib/minitest/test.rb:98
# and 24 frames (use `bt' command for all frames)
(rdbg)
Any variables defined before debugger can be accessed, this can be used to inspect if #song was changed:
(rdbg) #song.title_en # ruby
"Updated Song Title"
(rdbg) #song.title_pt # ruby
"Madeira de Maçaranduba"
The title was updated using the patch request defined in the test case. Typing continue will move on from the breakpoint and continue running the code in the test file.
That should be it!
I am trying to get a date from a user and send it inside an email as plain text in the following format: "07/30/2015".
In order to do that, if the output I am getting is a string, I could just do:
Date.parse("2015-07-30").strftime("%m/%d/%Y")
The problem is, I am getting a FixNum.
The issues are many:
If I try to convert to a string to parse it with Date.parse, it becomes "2001".
If I apply the code I just wrote, Date.parse... it will throw 'invalid date'.
For instance:
(2016-02-13).to_s #=> "2001"
(2016-02-13).to_date #=> NoMethodError: undefined method `to_date' for 2001:Fixnum
Date.parse("2001").strftime("%m/%d/%Y") #=> invalid date
So if I can convert 2015-07-30 into "2015-07-30", it would work:
Date.parse("2015-07-30").strftime("%m/%d/%Y") #=> "07/30/2015"
Then I tried using date_select instead of date_field, but now the message arrives with those fields empty.
Any suggestions?
Here is my form:
= form_for #contact do |f|
= f.text_field :product_name
= f.date_field :purchase_date
= f.submit
Here is my code:
<%= message.subject %>
<% #resource.mail_form_attributes.each do |attribute, value|
if attribute == "mail_subject"
next
end
%>
<%= "#{#resource.class.human_attribute_name(attribute)}: #{Date.parse(value).class == Date ? Date.parse(value).strftime("%m/%d/%Y") : value}" %>
<% end %>
My controller:
class ContactsController < ApplicationController
before_action :send_email, except: [:create]
def create
#contact = Contact.new(params[:contact])
#contact.request = request
if #contact.deliver
#thank = "Thank you for your message!"
#message = "We have received your inquiry and we'll be in touch shortly."
else
#error = "Cannot send message. Please, try again."
end
end
def contact_page
end
def product_complaint
#the_subject = "Product Complaint Form"
end
private
def send_email
#contact = Contact.new
end
end
My model:
class Contact < MailForm::Base
# all forms
attribute :mail_subject
attribute :first_name, validate: true
attribute :last_name, validate: true
# product-complaint
attribute :best_by, validate: true, allow_blank: true # date
attribute :bag_code, validate: true, allow_blank: true
attribute :purchase_date, validate: true, allow_blank: true # date
attribute :bag_opened, validate: true, allow_blank: true # date
attribute :problem_noticed, validate: true, allow_blank: true # date
# all forms
attribute :message, validate: true
attribute :nickname, captcha: true
def headers
{
content_type: "text/plain",
subject: %(#{mail_subject}),
to: "xxxxx#xxxxxxx.com",
# from: %("#{first_name.capitalize} #{last_name.capitalize}" <#{email.downcase}>)
from: "xxx#xxxxx.com"
}
end
end
(2016-02-13).to_date #=> NoMethodError: undefined method `to_date' for 2001:Fixnum
youre getting this error because you dont have quotes around the value. i.e. its not a string, its a number that is having subtraction applied to it. this is being interpreted as
2016 - 2
2014 - 13
2001.to_date
it needs to be ('2016-02-13').to_date
if youre unable to get it as a string, can you post how you're getting it from the user to begin with? (a date field ought to be sending you a string to your controller, not a series of numbers)
You're not understanding something about receiving values from forms: You can NOT receive an integer, a fixnum or anything else other than strings. So, you can't have received 2016-02-13. Instead you got "2016-02-13" or "2016", "02" or "2" and "13" depending on the form. If you're running under Rails, then it got the strings, and through its meta-data understands you want an integer (which really should probably be defined as a string), and it converts it to an integer for you.
Either way, when you write:
(2016-02-13).to_s
(2016-02-13).to_date
you're propagating that misunderstanding into your testing. This is how it MUST be written because you need to be working with strings:
require 'active_support/core_ext/string/conversions'
("2016-02-13").to_s # => "2016-02-13"
("2016-02-13").to_date # => #<Date: 2016-02-13 ((2457432j,0s,0n),+0s,2299161j)>
You can create dates without them being strings though: Ruby's Date initializer allows us to pass the year, month and day value and receive a new Date object:
year, month, day = 2001, 1, 2
date = Date.new(year, month, day) # => #<Date: 2001-01-02 ((2451912j,0s,0n),+0s,2299161j)>
date.year # => 2001
date.month # => 1
date.day # => 2
Moving on...
Parsing dates in Ruby quickly demonstrates it's not a U.S.-centric language. Americans suppose all dates of 01/01/2001 are in "MM/DD/YYYY" but that's a poor assumption because much of the rest of the world uses "DD/MM/YYYY". Not knowing that means that code written under that assumption is doing the wrong thing. Consider this:
require 'date'
date = Date.parse('01/02/2001')
date.month # => 2
date.day # => 1
Obviously something "wrong" is happening, at least for 'mericans. This is very apparent with:
date = Date.parse('01/31/2001')
# ~> -:3:in `parse': invalid date (ArgumentError)
This occurs because there is no month "31". In the previous example of '01/02/2001', that misunderstanding means the programmer thinks it should be "January 2" but the code thinks it's "February 1", and work with that. That can cause major havoc in an enterprise system, or anything dealing with financial calculations, product scheduling, shipping or anything else that works with dates.
Because the code is assuming DD/MM/YYYY format for that sort of string, the sensible things to do are:
KNOW what format your users are going to send dates in. Don't assume, ever. ASK them and make your code capable of dealing with alternates, or tell them what they MUST use and vet out their data prior to actually committing it to your system. Or, provide a GUI that forces them to pick their dates from popups and never allows them to enter it by hand.
Force the date parser to use explicit formats of dates so it can always do the right thing:
Date.strptime('01/31/2001', '%m/%d/%Y') # => #<Date: 2001-01-31 ((2451941j,0s,0n),+0s,2299161j)>
Date.strptime('31/01/2001', '%d/%m/%Y') # => #<Date: 2001-01-31 ((2451941j,0s,0n),+0s,2299161j)>
The last point is the crux of writing code: We're telling the language what to do, not subjecting ourselves, and our employers, to code that's guessing. Give code half a chance and it'll do the wrong thing, so you control it. That's why programming is hard.
I'm trying to write rspec tests for my spree customizations and i need to create products with variants. i cant seem to do this even though i appear to be doing the exact same thing as the rspec tests that are part of spree core.
def build_option_type_with_values(name, values)
ot = create(:option_type, :name => name)
values.each do |val|
ot.option_values.create(:name => val.downcase, :presentation => val)
end
ot
end
let(:number_size_option_type) do
size = build_option_type_with_values("number sizes", %w(1 2 3 4))
end
let(:product1) { create(:product, name: 'product1') }
it "should have variants" do
hash = {number_size_option_type.id.to_s => number_size_option_type.option_value_ids}
product1.option_values_hash = hash
product1.save
product1.reload
expect(product1.variants.length).to eq(4)
end
no matter what i do, the number of variants for my product is always zero.
Turns out the product.option_values_hash needs to be added during product creation in order to invoke the variant creation code. here is the changed line and then i removed the hash from the test "should have variant"
let(:product1) { create(:product, name: 'product1', option_values_hash: {number_size_option_type.id.to_s => number_size_option_type.option_value_ids}) }
it "should have variants" do
product1.save
expect(product1.option_type_ids.length).to eq(1)
expect(product1.variants.length).to eq(4)
end
I18n provided a convenient way to translate model names. Take the example from Rails Guide
I18n.backend.store_translations :en, inbox: {
one: 'one message',
other: '%{count} messages'
}
I18n.translate :inbox, count: 2
# => '2 messages'
I18n.translate :inbox, count: 1
# => 'one message'
This is good enough if I want to show "one message", "2 messages", and etc.
But what if I also want to show "Message" (for example, in the menu)? Do I have to create another totally different entry in the I18n files? Is there some Rails way to do this? like:
I18n.backend.store_translations :en, inbox: {
title: 'Message'
zero: 'no messages'
one: 'one message',
other: '%{count} messages'
}
Check out titleize for making it look like a title, and pluralize for making it pluralize.
I think Rails mb_chars will do this with unicode rules.
I was surprised to see you needed this, because I'm used to thinking of mb_chars in Rails as being a leftover from ruby 1.8 days which no longer has any use. But it looks like this is one place where default Rails titleize does not do the right i18n thing, but it will if you transform the string to an mb_chars proxy first.
irb(main):019:0> "\u01C9ab".titlecase # "ljab"
=> "ljab" # wrong
irb(main):021:0> "\u01C9ab".mb_chars.titlecase
=> LJab
# right. Or at least closer and as close to right as Rails is gonna get, I'm not
# sure if it should really be "Ljab", which is actually different.
# I'm out of my league here.
I'm not really sure if the Rails devs would consider this a bug or not.
The pluralize helper accepts the plural form as third parameter.
pluralize 3, 'person', 'people'
You can build a helper for your edge cases.
Is there a shorter way to do the following (
#user.employees.map { |e| { id: e.id, name: e.name } }
# => [{ id: 1, name: 'Pete' }, { id: 2, name: 'Fred' }]
User has_many employees. Both classes inherit from ActiveRecord::Base.
Two things I don't like about the above
It loads employees into memory before mapping,
It's verbose (subjective I guess).
Is there a better way?
UPDATE:
see #jamesharker's solution: from ActiveRecord >= 4, pluck accepts multiple arguments:
#user.employees.pluck(:id, :name)
PREVIOUS ANSWER:
for a single column in rails >= 3.2, you can do :
#user.employees.pluck(:name)
... but as you have to pluck two attributes, you can do :
#user.employees.select([:id, :name]).map {|e| {id: e.id, name: e.name} }
# or map &:attributes, maybe
if you really need lower-level operation, just look at the source of #pluck, that uses select_all
In ActiveRecord >= 4 pluck accepts multiple arguments so this example would become:
#user.employees.pluck(:id, :name)
If you are stuck with Rails 3 you can add this .pluck_all extension : http://meltingice.net/2013/06/11/pluck-multiple-columns-rails/
Another option is to:
#user.employees.select(:id, :name).as_json
#=> [{"id" => 1, "name" => "Pete"}, {"id" => 2, "name" => "Fred"}]
I can imagine that you'd rather have symbolized keys.
If that's the case use the #symbolize_keys method.
#user.employees.select(:id, :name).as_json.map(&:symbolize_keys)
#=> [{id: 1, name: "Pete"}, {id: 2, name: "Fred"}]
See: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html#method-i-as_json
Add this monkey patch which provides the multi columns pluck functionality in Rails 3.
# config/initializers/pluck_all.rb
if Rails.version[0] == '3'
ActiveRecord::Relation.class_eval do
def pluck(*args)
args.map! do |column_name|
if column_name.is_a?(Symbol) && column_names.include?(column_name.to_s)
"#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(column_name)}"
else
column_name.to_s
end
end
relation = clone
relation.select_values = args
klass.connection.select_all(relation.arel).map! do |attributes|
initialized_attributes = klass.initialize_attributes(attributes)
attributes.map do |key, attr|
klass.type_cast_attribute(key, initialized_attributes)
end
end
end
end
end
Rename the method from pluck to pluck_all if you dont want to override the original pluck functionality
In terms of making a rails 3 method that behaves the same as the Rails 4 pluck with multiple columns. This outputs a similar array (rather than a hashed key value collection). This should save a bit of pain if you ever come to upgrade and want to clean up the code.
module ActiveRecord
class Relation
def pluck_all(*args)
args.map! do |column_name|
if column_name.is_a?(Symbol) && column_names.include?(column_name.to_s)
"#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(column_name)}"
else
column_name.to_s
end
end
relation = clone
relation.select_values = args
klass.connection.select_all(relation.arel).map! do |attributes|
initialized_attributes = klass.initialize_attributes(attributes)
attributes.map do |key, attribute|
klass.type_cast_attribute(key, initialized_attributes)
end
end
end
end
end
Standing on the shoulders of giants and all
The pluck_all method worked well until I'm going to upgrade from Rails 3.2 to Rails 4.
Here is a gem pluck_all to solve this, making pluck_all method support not only in Rails 3 but in Rails 4 and Rails 5. Hope this will help those who are going to upgrade rails version.