How to write a virtual attribute's getter and setter methods? - ruby-on-rails

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

Related

What is the purpose of defining methods in a model?

What is the purpose of defining methods inside a model like the example here? What does this get me? I was under the impression that only the fields of a model are defined in the model.
class Bean
include Mongoid::Document
field :name, type: String
field :roast, type: String
field :origin, type: String
field :quantity, type: Float
has_many :pairings
# has_many :pastries
def pastries
Pastry.find pastry_ids
end
#accepts_nested_attributes_for :pastries
def pastry_ids
pastry_ids_array = []
self.pairings.each do |one_pairing|
if one_pairing.pastry_id
pastry_ids_array.push one_pairing.pastry_id
end
end
pastry_ids_array
end
def pastry_ids=(list)
self.pairings.destroy
list.each do |pastry_id|
self.pairings.create(pastry_id: pastry_id)
end
end
# some way of showing a list
def pastry_list
pastries_string = ""
pastries.each do |one_pastry|
pastries_string += ", " + one_pastry.name
end
pastries_string.slice(2,pastries_string.length - 1)
pastries_string
end
end
I don't know if you know enough ruby but let's say you don't. This is a basic Class question? Defining methods on a model it's like having an helper. Let's say that you have
class CanadianPopulation
attr_accessor :population, :number_of_french_speaker, :number_of_english_speaker
def initialize(a,b,c)
#population = a
#number_of_french_speaker = b
#number_of_english_speaker = c
end
def total_people_that_have_a_different_mother_tongue
#Canadian who speak english or french but have a different mother tongue
self.population - (self.number_of_french_speaker + self.number_of_english_speaker)
end
end
census_2014 = CanadianPopulation.new(34_000_000, 4_000_000, 12_000_000)
let's say that you didn't have the method total_people_that_have_a_different_mother_tonguehow will you do to retrieve the total number of Canadians that have a different mother tongue? you will do the caculation yourself like for a view
<p>Canadian who speak english or french but have a different mother tongue
<br>
<%= #census = #census.population - (#census.number_of_english_speaker + #census.number_of_french_speaker) %>
</p>
Your view or your controller shouldn't do much logic (calculations) so that's one of the reason why you have a method inside the model (or class) it should be like this
<p>Canadian who speak english or french but have a different mother tongue
<br>
<%= #census.total_people_that_have_a_different_mother_tongue %>
</p>
For the second part of your question what does those methods do. rails c -s on your terminal than call or create a new instance model Bean and check to see what it does (the output/results)
Bean.first
b = _
b.pastries
b.pastry_ids
b.pastry_list
edit: #paul-richer recommends to maintain a thin controller

Validate uniqueness of value between multiple fields

I understand that validating uniqueness of a standard, single field like "username" is easy. However, for something that has an unlimited number of inputs like, for example, "Favorite Movies" where a user can add as many favorite movies, is something I can't figure out.
They can choose to add or remove fields via the builder, but how do I ensure that no two or more entries are duplicates?
I think the easiest way to accomplish something like this is to validate the uniqueness of something in a scope. I can't say for sure how it would fit in your scenario since you did not describe you model associations but here is an example of how it could work in a FavoriteMovie model:
class FavoriteMovie < ActiveRecord::Base
belongs_to :user
validates_uniqueness_of :movie_name, :scope => :user_id
end
This makes sure that there can't be two movie names that are the same for one specific user.
It turns out that when using nested attributes, you can only validate what's already in the database and not new duplicate occurrences. So, a validation extension (below) with memory validation is really the only option, unfortunately.
#user.rb
class User
has_many :favorite_movies
validate :validate_unique_movies
def validate_unique_movies
validate_uniqueness_of_in_memory(
favorite_movies, [:name, :user_id], 'Duplicate movie.')
end
end
#lib/extensions.rb
module ActiveRecord
class Base
def validate_uniqueness_of_in_memory(collection, attrs, message)
hashes = collection.inject({}) do |hash, record|
key = attrs.map {|a| record.send(a).to_s }.join
if key.blank? || record.marked_for_destruction?
key = record.object_id
end
hash[key] = record unless hash[key]
hash
end
if collection.length > hashes.length
self.errors.add_to_base(message)
end
end
end
end
A very un-rails like solution to the problem would be to add a unique key constraint on the columns that in combination are required to be unique:
create unique index names_idx on yourtable (id, name);
you could easly check it like:
params[:user][:favourite_movies].sort.uniq == params[:user][:favourite_movies].sort
or in model:
self.favourite_movies.sort.uniq == self.favourite_movies.sort
irb(main):046:0> movies = ['terminator', 'ninja turtles', 'titanic', 'terminator' ].map {|movie| movie.downcase }
=> ["terminator", "ninja turtles", "titanic", "terminator"]
irb(main):047:0> movies.sort.uniq == movies.sort
=> false
You can try to create virtual attribute and check it uniqueness:
def full_name
[first_name, last_name].joun(' ')
end
def full_name=(name)
split = name.split(' ', 2)
self.first_name = split.first
self.last_name = split.last
end
You can check uniqueness on the database level by fix your migration:
CREATE TABLE properties (
namespace CHAR(50),
name CHAR(50),
value VARCHAR(100),
);
execute <<-SQL
ALTER TABLE properties
ADD CONSTRAINT my_constraint UNIQUE (namespace, name)
SQL
Little more modern approach: validates method
validates :movie_name, :uniqueness => {:scope => : user_id}

Rails Model: Name -- First, Last

I'm fairly new to rails, working on a Rails 3 app with a Profile model for users.
In the profile Model I'd like to have a "name" entry, and I'd like to be able to access logical variations of it using simple syntax like:
user.profile.name = "John Doe"
user.profile.name.first = "John"
user.profile.name.last = "Doe"
Is this possible, or do I need to stick with "first_name" and "last_name" as my fields in this model?
It's possible, but I wouldn't recommend it.
I would just stick with first_name and last_name if I were you and add a method fullname:
def fullname
"#{first_name} #{last_name}"
end
Edit:
If you really do want user.profile.name, you could create a Name model like this:
class Name < ActiveRecord::Base
belongs_to :profile
def to_s
"#{first} #{last}"
end
end
This allows you to do:
user.profile.name.to_s # John Doe
user.profile.name.first # John
user.profile.name.last # Doe
The other answers are all correct, in so far as they ignore the #composed_of aggregator:
class Name
attr_reader :first, :last
def initialize(first_name, last_name)
#first, #last = first_name, last_name
end
def full_name
[#first, #last].reject(&:blank?).join(" ")
end
def to_s
full_name
end
end
class Profile < ActiveRecord::Base
composed_of :name, :mapping => %w(first_name last_name)
end
# Rails console prompt
> profile = Profile.new(:name => Name.new("Francois", "Beausoleil"))
> profile.save!
> profile = Profile.find_by_first_name("Francois")
> profile.name.first
"Francois"
As noted on the #composed_of page, you must assign a new instance of the aggregator: you cannot just replace values within the aggregator. The aggregator class acts as a Value, just like a simple string or number.
I also sent a response yesterday with a very similar answer: How best to associate an Address to multiple models in rails?
As Capt. Tokyo said that's a horrible idea but here's how you would do it:
rails g model User full_name:hash
Then you would store data in it like so:
user = User.new
user.full_name = {:first => "Forrest", :last => "Gump"}
Now your problems begin.
To search the field requires both names and you can't do a partial search like searching for all people with the same last name. Worst of all you can store anything in the field! So imagine another programmer mistypes one of the field names so for a week you have {:fist => "Name", :last => "Last"} being inserted into the database! Noooooooooooooooooo!
If you used proper field names you could do this:
user = User.new(:first_name => "First", :last_name => "Last")
Easy to read and no need for hashes. Now that you know how to do it the wrong way, do it the right way. :)
FYI (assume you have a field fullname. ie your profile.name = "John Doe")
class Profile
def name
#splited_name ||= fullname.split # #splited_name would cache the result so that no need to split the fullname every time
end
end
Now, you could do something like this:
user.profile.fullname # "John Doe"
user.profile.name.first # "John"
user.profile.name.last # "Doe"
Note the following case:
user.profile.fullname = "John Ronald Doe"
user.profile.name.first # "John"
user.profile.name.second # "Ronald"
user.profile.name.last # "Doe"
I agree with captaintokyo. You won't miss out the middle names.
Also this method assume no Chinese, Japanese names are input. It's because those names contain no spaces in between first name and last name normally.

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

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

Custom method in model to return an object

In the database I have a field named 'body' that has an XML in it. The
method I created in the model looks like this:
def self.get_personal_data_module(person_id)
person_module = find_by_person_id(person_id)
item_module = Hpricot(person_module.body)
personal_info = Array.new
personal_info = {:studies => (item_module/"studies").inner_html,
:birth_place => (item_module/"birth_place").inner_html,
:marrital_status => (item_module/"marrital_status").inner_html}
return personal_info
end
I want the function to return an object instead of an array. So I can
use Module.studies instead of Model[:studies].
This is relatively simple; you're getting an Array because the code is building one. If you wanted to return an object, you'd do something like this:
class PersonalData
attr_accessor :studies
attr_accessor :birth_place
attr_accessor :marital_status
def initialize(studies,birth_place,marital_status)
#studies = studies
#birth_place = birth_place
#marital_status = marital_status
end
end
And your translation code would look like:
def self.get_personal_data_module(person_id)
person_module = find_by_person_id(person_id)
item_module = Hpricot(person_module.body)
personal_info = PersonalData.new((item_module/"studies").inner_html,
(item_module/"birth_place").inner_html,
(item_module/"marital_status").innner_html)
return personal_info
end
Or, if you want to avoid a model class, you could do something weird:
class Hash
def to_obj
self.inject(Object.new) do |obj, ary| # ary is [:key, "value"]
obj.instance_variable_set("##{ary[0]}", ary[1])
class << obj; self; end.instance_eval do # do this on obj's metaclass
attr_reader ary[0].to_sym # add getter method for this ivar
end
obj # return obj for next iteration
end
end
end
Then:
h = {:foo => "bar", :baz => "wibble"}
o = h.to_obj # => #<Object:0x30bf38 #foo="bar", #baz="wibble">
o.foo # => "bar"
o.baz # => "wibble"
It's like magic!
on a slightly different tack.
The idea of using a class method to do this feels wrong from an OO point of view.
You should really refactor this so that it works from an instance method.
def personal_data_module
item_module = Hpricot(body)
{
:studies => (item_module/"studies").inner_html,
:birth_place => (item_module/"birth_place").inner_html,
:marrital_status => (item_module/"marrital_status").inner_html
}
end
Then, where you need to use it, instead of doing....
Foobar.get_personal_data_module(the_id)
you would do
Foobar.find_by_person_id(the_id).personal_data_module
This looks worse, but in fact, thats a bit artificial, normally, you would be
referencing this from some other object, where in fact you would have a 'handle' on the person object, so would not have to construct it yourself.
For instance, if you have another class, where you reference person_id as a foreign key, you would have
class Organisation
belongs_to :person
end
then, where you have an organisation, you could go
organisation.person.personal_information_module
Yes, I know, that breaks demeter, so it would be better to wrap it in a delegate
class Organisation
belongs_to :person
def personal_info_module
person.personal_info_module
end
end
And then from controller code, you could just say
organisation.personal_info_module
without worrying about where it comes from at all.
This is because a 'personal_data_module' is really an attribute of that class, not something to be accessed through a class method.
But this also brings up some questions, for instance, is person_id the primary key of this table? is this a legacy situation where the primary key of the table is not called 'id'?
If this is the case, have you told ActiveRecord about this or do you have to use 'find_by_person_id' all over where you would really want to write 'find'?

Resources