Ruby on Rails: Set data in my model based on another field - ruby-on-rails

I have an item model that has a name and a price (int). How could I make it so that when the user selects the name from a drop down the price will automatically be added to the db?
My name drop down is populated by a hash of this format: THINGS = { 'Item 1' => 'item1', 'Item 2' => 'item2', etc } I'm thinking that a large switch statement where I do something like
case s
when hammer
item.price = 15
when nails
item.price = 5
when screwdriver
item.price = 7
end
But I'm not sure where I would put this switch.
Thanks

You need push it in a before_save callback.
Inside this callback you check the name choose by your user and update the price
class Item
before_save :update_price
def update_price
self.price = Product.find_by_name(self.name).price
end
end
You can do in before_validation too if you want validate that your price is really define in your model
class Item
before_validation :update_price
validates_presence_of :price
def update_price
self.price = Product.find_by_name(self.name).price
end
end

Related

Set a default value if no value is found in the API

I've created a little app based on an assignment that shows a random set of countries and facts about them, including the countries they share borders with. If they don't share any borders the field is currently empty which I would like to change to "I'm an island", or something.
https://countries-display.herokuapp.com/
This should be easy, but I'm very new, and am not sure how to approach it. Any help or points in the right direction would be greatly appreciated.
I tried initialising a default value:
class Country
include HTTParty
default_options.update(verify: false) # Turn off SSL verification
base_uri 'https://restcountries.eu/rest/v2/'
format :json
def initialize(countries = 'water')
#countries = countries
end
def self.all
#countries = get('/all')
#countries.each do |country|
country['borders'].map! do |country_code|
#countries.find { |country| country['alpha3Code'] == country_code } ['name']
end
country['languages'].map! { |language| language['name'] }
country['currencies'].map! { |currency| currency['name'] }
end
#countries
end
end
Setting a default in active record:
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
class Country < ActiveRecord::Base
before_create :set_defaults
def set_defaults
self.countries_to_display = 'water' if #countries_to_display.nil?
end
end
I also tried implementing some if statements that were equally unsuccessful.
Solution:
#countries.each do |country|
if country['borders'].empty?
country['borders'] << "I'm an island"
else country['borders'].map! do |country_code|
#countries.find { |country| country['alpha3Code'] == country_code } ['name']
end
end
...
I'm assuming that country['borders'] maps to the field that should contain I'm an island. I'm also assuming that you directly render the output of Country.all.
What you'll need to do is to add a check whether there are any borders and if there aren't write that string to the field instead of the list of bordering countries.
#countries.each do |country|
if country['borders'].empty?
borders = "I'm an island"
else
borders = country['borders'].map do |country_code|
#countries.find { |country| country['alpha3Code'] == country_code }['name']
end
end
country['borders'] = borders
...
end
Note that we use map instead of map! here to not modify the existing collection. You may need to adjust your rendering logic, as country['borders'] previously contained a list of countries and now contains a string.

Rails faker gem produces same product name

I'm trying to use rails Faker gem to produce unique product names to make sample Item models in the database. I've used Faker multiple times but for some reason I can't produce new product names. I've made the nameMaker function to avoid possible early repeats, but I get a record invalidation just after one insert. Does anyone know how I could fix this?
seed.rb:
98.times do |n|
name = Item.nameMaker
description = Faker::Lorem.sentence(1)
price = Item.priceMaker
item = Item.create!(
name: name,
description: description,
price: price)
end
item.rb:
class Item < ActiveRecord::Base
validates :name, presence: true, length: { maximum: 100 }
validates :description, presence: true,
length: { maximum: 1000 }
VALID_PRICE_REGEX = /\A\d+(?:\.\d{0,3})?\z/
validates :price, presence: true,
:format => { with: VALID_PRICE_REGEX },
:numericality => {:greater_than => 0}
validates_uniqueness_of :name
def Item.nameMaker
loop do
name = Item.newName
break if Item.find_by(name: name).nil?
end
return name
end
def Item.newName
Faker::Commerce.product_name
end
end
To get a unique name, enclose the faker in brackets. Eg
name { Faker::Commerce.product_name }
To achieve this, you could also make use of factory girl and when you want to create 98 different Items, you could have something like
factories/item.rb
FactoryGirl.define do
factory :item do
name { Faker::Commerce.product_name }
description { Faker::Lorem.sentence(1) }
price Faker::Commerce.price
end
end
in your spec file
let(:item) { create_list(:item, 98) }
You can add validates_uniqueness_of :name in your model. When you run seed method if there is already exists same name, it will throw error and skip to the next.
There is possibility that you will not have exactly 98 Items. You can increase number of times or edit Faker itself.
I figured it out after some experimentation, apparently the loop in some ways acts as like a function in terms of scoping. If you initialize a local variable in a loop, the function outside of the loop will not see it. In this case name always returning the string Item from the Item.nameMaker function. Thus the first attempt would always succeed and the second one would obtain the validation restriction.
def Item.nameMaker
loop do
name = Faker::Commerce.product_name # 'Random Product Name'
puts "Name: #{name}" # "Name: Random Product Name"
item = Item.find_by(name: name)
if item.nil?
puts "#{name} not found" # "Random Product Name not found"
break
else
end
end
puts "Returning Name #{name}" # "Returning Name Item"
return name
end
I managed to fix this by initializing the local variable outside of the loop. By doing this the entire function now has visibility to the same local variable for some reason.
def Item.nameMaker
name = "" #initializing
loop do
name = Faker::Commerce.product_name # 'Random Product Name'
puts "Name: #{name}" # "Name: Random Product Name"
item = Item.find_by(name: name)
if item.nil?
puts "#{name} not found" # "Random Product Name not found"
break
else
end
end
puts "Returning Name #{name}" # "Returning Random Product Name"
return name
end

How to create a enum type and default to a specific value for new objects

I have a model
class Transaction < ActiveRecord::Base
end
I have a transaction_type column which is an integer.
How can I create an enumeration that I could map values to names like:
one_time = 1
monthly = 2
annually = 3
So in the db column, the values would be 1, 2 or 3.
Also, whenever I create a new instance, or save a model and the field wasn't set like:
#transaction = Transaction.new(params)
It should default to 1 (on_time).
I'm not sure how I can do this?
basically the same answer as Amit, slight variation
class TransactionType
TYPES = {
:one_time => 1,
:monthly => 2,
:annually => 3
}
# use to bind to select helpers in UI as needed
def self.options
TYPES.map { |item| [item[0], item[1].to_s.titleize] }
end
def self.default
TYPES[:one_time]
end
end
one way to control the default value
class Transaction < ActiveRecord::Base
before_create :set_default_for_type
def set_default_for_type
type = TransactionType.default unless type.present?
end
end
but - best way is to just apply the defaults on your database column and let ActiveRecord get it from there automatically
NOTE: it might also make sense to just have a TransactionType ActiveRecord object instead of above, depends on your situation, i.e.
# on Transaction with type_id:integer
belongs_to :type, class_name: "TransactionType"
You can map the values by creating a constant either in the same Transaction model or by creating a new module and place it inside that as explained by #KepaniHaole
In Transaction model, you can do it like :
class Transaction < ActiveRecord::Base
TRANSACTION_TYPES = { 'one_time' => 1, 'monthly' => 2, 'monthly' => 3 }
end
You can access these values by accessing the constant as
Transaction::TRANSACTION_TYPES['one_time'] # => 1
Transaction::TRANSACTION_TYPES['monthly'] # => 2
Transaction::TRANSACTION_TYPES['monthly'] # => 3
To add a default value to transaction_type column just create a new migration with :
def up
change_column :transactions, :transaction_type, :default => Transaction::TRANSACTION_TYPES['one_time']
end
With this, every time you create a Transaction object without passing transaction_type, the default value 1 with be stored in it.
Maybe you could try something like this? Ruby doesn't really support c-style enums..
module TransactionType
ONCE = 1
MONTHLY = 2
ANUALLY = 3
end
then you could access their values like so:
#transaction = Transaction.new(TransactionType::ONCE)

How to write a virtual attribute's getter and setter methods?

My product model belongs to a telephone and I wanted to know how I would join both together for a virtual attribute. I can create a product and can find or create the telephone by it's number with the code below.
Product < ActiveRecord::Base
attr_accessible :name, :telephone_number
belongs_to :telephone
def telephone_number
telephone.try(:number)
end
def telephone_number=(number)
self.telephone = Telephone.find_or_create_by_number(number) if number.pr....
end
Now for getter method I put:
def product_with_number
[name, telephone_number].join(',')
end
But I'm not sure what's next or if this is what I'm getting at. I'm trying to make a single field where I can type in:
Cow's Beef Jerky, 555-323-1234
Where the comma seperates the product name and telephone and if the telephone number is new, it will create it. Am I on the right track, what next?
Thanks.
You need a corresponding setter:
def product_with_number=(str)
parts = str.split(',')
self.telephone_number = parts[0]
self.name = parts[1]
end
Then all you'd do is something like this:
#p = Product.New
#p.product_with_number = "Cow's Beef Jerky, 555-323-1234"
#p.save

Price validation includes word "Free" if users put 0 or words in price field

How do i make it so if users put the number 0 or type any words in my price:decimal field it registers it as the word Free?
As of now i just validate presence of price:
validates :price, :presence => true
I would have your field reference a new pair of get/set methods for "price_string"
#in your model
def price_string
price == 0 ? "Free" : price
end
def price_string=(string)
price = (string == "free" ? 0 : string)
end
Now you can refer to "price_string" in your forms.
#in your form
f.text_field :price_string
A simple way is to add a before_validation callback to do it.
class ModelWithPrice < ActiveRecord::Base
# your validations ...
before_validation :convert_price_to_number
private
def convert_price_to_number
# no need to check for strings, to_f return 0.0 if the value cant be converted
self.price = self.price.to_f
# convert 0 to "Free" if needed
self.price = "Free" if self.price == 0
end
end

Resources