Best practice for converting integer value column to string representation - ruby-on-rails

Lets say you have a model like the following:
class Stock < ActiveRecord::Base
# Positions
BUY = 1
SELL = 2
end
And in that class as an attribute of type integer called 'position' that can hold any of the above values. What is the Rails best practice for converting those integer values into human readable strings?
a) Use a helper method, but then you're force to make sure that you keep the helper method and model in sync
def stock_position_to_s(position)
case position
when Stock::BUY
'buy'
when Stock::SELL
'sell'
end
''
end
b) Create a method in the model, which sort of breaks a clean MVC approach.
class Stock < ActiveRecord::Base
def position_as_string
...snip
end
end
c) A newer way using the new I18N stuff in Rails 2.2?
Just curious what other people are doing when they have an integer column in the database that needs to be output as a user friendly string.
Thanks,
Kenny

Sounds to me like something that belongs in the views as it is a presentation issue.
If it is used widely, then in a helper method for DRY purposes, and use I18N if you need it.

Try out something like this
class Stock < ActiveRecord::Base
##positions => {"Buy" => 1, "Sell" => 2}
cattr_reader :positions
validates_inclusion_of :position, :in => positions.values
end
It lets you to save position as an integer, as well as use select helpers easily.
Of course, views are still a problem. You might want to either use helpers or create position_name for this purpose method
class Stock < ActiveRecord::Base
##positions => {"Buy" => 1, "Sell" => 2}
cattr_reader :positions
validates_inclusion_of :position, :in => positions.values
def position_name
positions.index(position)
end
end

Is there a good reason for the app be converting the integer to the human readable string programmatically?
I would make the positions objects which have a position integer attribute and a name attribute.
Then you can just do
stock.position.name

#HermanD: I think it's a lot better to store the values in an integer column rather than a string column for numerous reasons.
It saves database space.
Easier/faster to index on an integer than a string.
Your not hard coding a human readable string as values in a database. (What happens if the client says that "Buy" should become "Purchase"? Now the UI shows "Purchase" everywhere but you need to keep setting "Buy" in the database.)
So, if you store certain values in the database as integers, then at some point, you're going to need to show them to the user as strings, and I think the only way you can do that is programatically.
You could move this info into another object but, IMHO, I'd say this is overkill. You'd then have to add another database table. Add another 'admin' section for adding, removing and renaming these values and so on. Not to mention that if you had several columns, in different models that needed this behavior, you'd either have to create lots of these objects (ex: stock_positions, stock_actions, transaction_kinds, etc...) or you'd have to design it generically enough to use polymorphic associations. Finally, if the position name is hard coded, then you lose the ability to easy localize it at a later date.
#frankodwyer: I'd have to agree that using a helper method is probably the best way to go. I was hoping their might be a "slicker" way to do this, but it doesn't look like it. For now, I think the best method is to create a new helper module, maybe something like StringsHelper, and stuff a bunch of methods in their for converting model constants to strings. That way I can use all the I18N stuff in the helper to pull out the localized string if I need to in the future. The annoying part is that if someone needs to add a new value to the models column, then they will also have to add a check for that in the helper. Not 100% DRY, but I guess "close enough"...
Thanks to both of you for the input.
Kenny

Why not use the properties of a native data structure? example:
class Stock < ActiveRecord::Base
ACTIONS = [nil,'buy','sell']
end
Then you could grab them using Stock::ACTIONS[1] #=> 'buy' or Stock::ACTIONS[2] #=> 'sell'
or, you could use a hash {:buy => 1, :sell => 2} and access it as Stock::ACTIONS[:buy] #=> 1
you get the idea.

#Derek P. That's the implementation I first went with and while it definitely works, it sort of breaks the MVC metaphor because the model, now has view related info defined in its class. Strings in controllers are one thing, but strings in models (in my opinion) are definitely against the spirit of clean MVC.
It also doesn't really work if you want to start localizing, so while it was the method I originally used, I don't think it's the method for future development (and definitely not in an I18N world.)
Thanks for the input though.
Sincerely,
Kenny

I wrote a plugin that may help a while ago. See this. It lets you define lists and gives you nice methods ending in _str for display purposes.

Related

How to choose a continent by the country?

I use Ruby on Rails 5.2 and mongoid 7.0
I need to choose a continent by the country
I understand that it should look something like this:
class Place
field :country, type: String
field :continent, type: String
after_save :update_continent
def update_continent
cont = self.country
case cont
when 'United States', 'Grenada'
'NA'
when 'Netherlands', 'Spain'
'EU'
end
self.continent = cont
end
end
Since you indicated you are using Mongoid:
Each Mongoid model class must include Mongoid::Document, per the documentation in https://docs.mongodb.com/mongoid/master/tutorials/mongoid-documents/.
after_save callbacks are normally used for things like creating external jobs, not for setting attributes, because the attribute changes won't be persisted (as the model was already saved). Usually attribute changes are done in before_validation or before_save callbacks. See https://guides.rubyonrails.org/active_record_callbacks.html for the list of available callbacks.
As pointed out by Toby, the case statement is not correctly used. Its result should be assigned like this:
.
def update_continent
self.continent = case self.country
when 'United States', 'Grenada'
'NA'
when 'Netherlands', 'Spain'
'EU'
end
end
You haven't given enough context to be able to answer your question, but since you just want to be pointed in the right direction, and since you seem to be new here I'm happy to give you some pointers.
You're class uses the after_save method as if it is an ActiveRecord Model, but without extending or including anything it's just a Plain Old Ruby Object. To make the after_save callback work you need to at least extend ActiveModel::Callbacks but probably you want to make it a full ActiveRecord Model. To do that in Rails 4 you subclass ActiveRecord::Base and in rails 6 you subclass ApplicationRecord But I don't actually know how it's done in Rails 5.
If you have a normal database in the back end as is usual for rails you don't need to declare the fields, it automatically gets them from the equivalent table in the database (though perhaps this is not true when using Mongoid. I don't know). if you run this command in your terminal in your app base directory: rails generate model Place country:string continent:string it will create the migration file needed to make the database table and the Model file (with whatever the correct superclass is) and you wont need to do all the boilerplate stuff yourself.
You have a variable named cont and you assign a country to it. This will get very confusing given that you also have a separate concept of "continent" Better to not abbreviate your variable names and choose sensible naming.
You're not using the case statement correctly. The output of the statement doesn't automatically get assigned to the the variable you're switching on. You need to read up on Ruby syntax.
Overall I suspect in the long run you would do well to have separate models for Continent and Country. With a Continent having many countries and a country belonging to a continent. Rails is a framework that makes that sort of thing very easy to do and manage. You probably need to read some more and look at examples and videos about the basics of Ruby on Rails.
I highly recommend The Rails Tutorial by Hartl. It's free online. Working through that or an equivalent should give you a much better understanding of how Rails is equipped to handle your situation and how to best utilise it to get the outcome you need. This was indispensable for me when I was first starting out with Rails.

Rails - Format number as currency format in the Getter

I am making a simple retail commerce solution, where there are prices in a few different models. These prices contribute to a total price. Imagine paying $0.30 more for selecting a topping for your yogurt.
When I set the price field to
t.decimal :price, precision:8, scale:2
The database stores 6.50 as 6.5. I know in the standard rails way, you call number_to_currency(price) to get the formatted value in the Views. I need to programmatically call the price field as well formatted string, i.e. $6.50 a few places that are not directly part of the View. Also, my needs are simple (no currency conversion etc), I prefer to have the price formatted universally in the model without repeated calling number_to_currency in views.
Is there a good way I can modify my getter for price such that it always returns two decimal place with a dollar sign, i.e. $6.50 when it's called?
Thanks in advance.
UPDATE
Thanks everyone.
I've elected to use Alex's approach because it seems very 'hackish' to do the includes just for formatting the number. Using his approach, I did:
def price_change=(val)
write_attribute :price_change, val.to_s.gsub(/[\$]/,'').to_d
end
def price_change
"$%.2f" % self[:price_change]
end
Cheers.
UPDATE 2
Caveat Emptor. Once you do this, you lose the ability to do operations to the number because it's now a string.
Please beware if anyone is facing the same problem as me.
Just add a method in your model which is named like your attribute in the database like:
def price
"$%.2f" % self[:price]
end
which gives you full control over the formatting or use the Rails provided helper method
def price
ActionController::Base.helpers.number_to_currency(self[:price])
end
this should do the trick.
hope it helps!
You can use the helpers module, but you should not include the whole module, because it includes a lot of methods which you may not really need or overrides some of yours. But you can use them directly:
ActionController::Base.helpers.number_to_currency(6.5)
#=> "$6.50"
You could also define a method for the helpers, so you can easily use them.
def helpers
ActionController::Base.helpers
end
"#{helpers.number_to_currency(6.5)}"
Have a look at this railscast
I'd suggest going with a Presenter approach, like Draper (see this reailscast) does.
Another solution would be to implement your own method in your model, i.e. formatted_price and do the formatting on your own, (i.e. with the ActionView::Helpers::NumberHelper module). But since models represent the plain data in your rails application, it's kinda shady doing something like this and it interferes with the convention over configuration approach, I think.
try
include ActionView::Helpers::NumberHelper
to your model

Rails Best Practice for User-Configurable Global Attribute "Defaults"

Sorry about the awkward phrasing of the title -- not quite sure of the best way to title this but here's what I'm seeing assistance with:
In a Rails app, let's say we've got a model for a Product, and one of the attributes of the product is Price.
On the admin side of my app, I'd like to be able to set a "default" price that could be referred to if any new Product created isn't assigned a Price. If a Product does have a value for Price, then it would be used.
This is, of course, an example -- and I ask this question because I've got to imagine this is a common pattern. This can be applied to any resource that might have user-configurable global defaults or resource-specific values.
In pure Ruby, this would be solved, I think, with a class variable, so I'd be able to define ##default_price within the Product class and be able to refer to Product.default_price if the instantiated object's value doesn't exist.
My research here has pointed me towards the rails-settings-cached gem, which would allow for something like MyApp.default_price, but I'm wondering if there's a more elegant (non-plugin) way to accomplish this within the base Rails framework.
Note I'd like to setup this structure in code, but I want to be able to define the actual values through my app (i.e. config files aren't the solution I'm looking for).
Can someone enlighten me with the Rails way of handling this?
ActiveRecord picks up default attribute values from the database schema. However, these are baked into the migration and table schema and not configurable.
If you want configurability, the pattern that I've used is a before_validation callback method to set a value if the attribute is blank, e.g.:
class Product < ActiveRecord::Base
before_validation :set_price_if_blank
validates :price, :presence => true # sanity check in case the default is missing
has_one :price
private
def set_price_if_blank
self.price = Price.default if self.price.blank?
end
end
class Price < ActiveRecord::Base
def self.default
##default ||= Price.where(:default => true).first
end
end
This assumes that your price table is populated with a row that has a default flag. You could achieve this, e.g. through a seeds.rb file. I've added a validation rule to make sure that you still get an error if no default exists. It adds robustness to your application.
Also note that it's best to use Integers or Decimals for price data, not floats. See this answer.

Rails: translations for table's column

In rails application I have two models: Food and Drink.
Both food and drink have a name, which has to be stored in two languages.
How do I better realize translations for theese tables?
First solution I realized was to replace name column with name_en and name_ru.
Another solution is to encode with YAML hash like { :en => 'eng', :ru => 'rus' } and store yaml as a name.
What would you recommend, assuming content is not static?
Maybe there's good article?
The first option (name_en, name_ru) is the easiest to implement.
You could include a name method that returns the right value depending on the selected locale. You could even create a module, if you are going to use this on lots of models/fields:
class Food < ActiveRecord::Base
...
def name
self.send("name_#{I18n.locale}")
end
end
If in the future you must include an additional language, you will have to add a migration, of course. But that shoudn't be too troublesome.
The second one (encoding using YAML) seems a bit more cumbersome - you will not have to make an additional migration with it, but you loose other functionality. For example, search is made much more difficult - you can't use SQL any more for looking through descriptions, as they are coded on YAML instead of being plain text.
So I recommend having two fields.

How to iterate ActiveRecord Attributes, including attr_accessor methods

I've looked everywhere for an elegant solution. The essential problem seems to be that ActiveRecord attributes that map to database columns are handled completely differently in ActiveRecord::Base than attr_accessor methods.
I would like to do something like:
model.attribute_names.each do |name|
# do stuff
end
in a way that also includes attr_accessor fields, but not any other instance methods. I know this in not built-in, but what is the most elegant way to do it?
You can't really solve this. You can approximate a hack, but it's not something that will ever work nicely.
model.attribute_names should get you all the ActiveRecord ones, but the attr_accessor fields are not fields. They are just ordinary ruby methods, and the only way to get them is with model.instance_methods.
Idea 1
You could do model.attribute_names + model.instance_methods, but then you'd have to filter out all your other normal ruby methods initialize, save, etc which would be impractical.
To help filter the instance_methods you could match them up against model.instance_variables (you'd have to account for the # sign in the instance variables manually), but the problem with this is that instance variables don't actually exist at all until they are first assigned.
Idea 2
In your environment.rb, before anything else ever gets loaded, define your own self.attr_accessor in ActiveRecord::Base. This could then wrap the underlying attr_accessor but also save the attribute names to a private list. Then you'd be able to pull out of this list later on. However I'd advise against this... monkey-patching core language facilities like attr_accessor is guaranteed to bring you a lot of pain.
Idea 3
Define your own custom_attr_accessor in ActiveRecord::Base, which does the same thing as Idea 2, and use it in your code where you want to be able to retrieve the attribute names. This would be safe as you won't be clobbering the built-in attr_accessor method any more, but you'll have to change all your code to use custom_attr_accessor where neccessary
I guess in summary, what are you trying to do that needs to know about all the attr_accessor fields? Try look at your problem from a different angle if you can.
I came here looking to do the same thing, and found out it was the wrong approach altogether thanks to Orion's answer.
Incase anyone else's use case is similar to mine, here's my solution. I was using attr_accessor to add extra properties to the models after querying them from ActiveRecord. I then wanted to output the results as JSON etc.
A better solution is to first convert the Models from ActiveRecord into regular hashes, and then add the attr_accessor properties as regular hash keys.
Example:
model_hash = model_from_activerecord.attributes.to_options
model_hash[:key] = value
The solution I came up with for myself builds upon Orion Edwards' answer.
The code:
klass_attributes = klass.column_names + klass.instance_methods(false).
map(&:to_s).keep_if{|a| a.include?('=')}.map{|a| a.sub('=', '')}
The breakdown:
klass.instance_methods(false) brings back only instance methods, and not inherited methods.
map(&:to_s) converts the array of symbols into an array of strings so we can use the include? method. Also needed to merge with array of strings returned by column_names.
keep_if{|a| a.include?('=')} will remove all strings within the array that do not have an equals sign. This was important, since I noticed that attr_accessor attributes all had '='. Ex: 'app_url' vs 'login_url='
map{|a| a.sub('=', '')} is the final piece that then removes the '=' from each string in the array.
You end up with an array of strings that represent attributes, including attr_accessor attributes.

Resources