Rails Model: Name -- First, Last - ruby-on-rails

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.

Related

Rspec Create multiple posts to check ranking

Afternoon All,
I've just starting learning the testing side of rails and having some trouble with this below.
Let me quickly explain what I'm trying to test. If a user creates 10 approved snippets their rank should == "Author". A snippet is created on a book which I have listed in the factories but my code is all messed up and I'm not sure how I would write this.
Here is the code I've been playing with for the testing:
describe "ranking" do
let!(:book) { Book.create }
let!(:snippet) { FactoryGirl.create(:snippet1, :book1 => book) }
context "should be author after 10 approved posts" do
10.times do
FactoryGirl.create(:snippet1)
end
user.user_rank.should == "Author"
end
end
Here are my factories:
FactoryGirl.define do
factory :admin2, class: User do
first_name "admin1"
last_name "minstrator"
password "admin1234"
profile_name "profilename"
email "admin1#admin.com"
password_confirmation "admin1234"
admin true
end
factory :user2, class: User do
first_name "user2"
last_name "man2"
password "user1234"
profile_name "profilename"
email "user2#user.com"
password_confirmation "user1234"
admin false
end
factory :book1, class: Book do
title "Book1"
approved true
size 0
end
factory :snippet1, class: Snippet do
content "Snippet1"
approved true
end
end
EDIT: Error and related code:
app/models/snippet.rb:32:in `size_limit': undefined method `size' for nil:NilClass (NoMethodError)
This relates to a validation in the model shown below:
BOOK_SIZE = {
0 => {'per' => 500, 'total' => 15000},
1 => {'per' => 700 , 'total' => 30000},
2 => {'per' => 1000, 'total' => 50000}
}
def size_limit
book_limit = self.book.size.to_i
word_count = self.content.scan(/\w+/).size.to_i
current_snippets_size = (self.book.get_word_count || 0) + word_count
errors.add(:base, "Content size is too big") unless word_count < BOOK_SIZE[book_limit]['per'] && current_snippets_size < BOOK_SIZE[book_limit]['total']
end
I think your snippet1 factory should have user_id or something like that. now you create 10 snippet without association
edit: now I read your edit. but hire is almost the same. your snippet1 factory haven't any book so error on this
self.book.size.to_i
Your let(:snippet) clause uses a :book1 attribute, but the code is checking the self.book – could that be it?
Either way, the snippet.rb excerpt you've listed has two references to .size - without knowing your line numbers, it's hard for us to tell which one is throwing the error.
So either the book attribute or the content attribute is returning nil – and so when you call size on that nil attribute you get the error that's happening.
If snippet is only valid with a book reference and a non-nil content, add validations for those conditions. If there are situations where those attributes could be nil, make sure that your code makes allowances for that.

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

Updating Attribute through reference

I am wondering if it's possible to reference an object's attribute.
The object User have attribute first_name, so normally if we want to update the first name attribute we do:
user0 = User.new()
user0.update_attribute(:first_name, "joe")
now my question is can I update the :first_name attribute through another variable/symbol like so:
user0.update_attribute(:fname_ref, "jack")
user0.first_name #=> has the value jack
I was looking for variable reference like in Perl, but came up nothing in Ruby.
---------- EDIT
I am in the middle of doing the lynda ruby on rails tutorial, and in the middle of creating a module to adjust positions of items in a table.
unfortunately when I first started I named my tables columns differently
pages.page_position, subjects.subject_position, sections.section_position
now the module PositionMover is to be used accross three models, so now
I have a problem since the attributes names are different for each model
so I thought no worry I'll just create a pointer / refference for each model
:position = :pages_position , :position = :subjects_position , :position = :section_position
hence the question , if its even possible to do it.
if its not possible , any suggestion what should I do , so the module can
be used accross three different models , with different attribute names.
sorry I am a newbie.
Symbols are not like variables, they are actually a object type, like String or Fixnum so you can have a variable that is of type symbol.
I think this is what you are looking for:
attribute = :first_name
user0.update_attribute(attribute, "jack")
user0.first_name #=> has the value jack
Update: If you have a String and need to convert to a symbol, (I'm not sure if you need this for update_attribute)
foo = "string"
foo.to_sym #=> :string
Use the alias_attribute . Define into each model like :
Page model
alias_attribute :position , :pages_position
Subject Model
alias_attribute :position , :subjects_position
Section Model
alias_attribute :position , :section_position
And use (Model.position = values) with each model . Hope Its solution of your problem .
You can also use send docs and use symbols or strings to reference the attribute's methods. send can be incredibly useful since it enables you to choose the method that you'll be invoking at runtime.
Eg
user.first_name = "Jack" # set first_name to Jack
# Note: method "first_name=" (a settor) is being used
attribute = "first_name"
user.send attribute + "=", "Jack" # set first_name to Jack
# The string "first_name=" is coerced into
# a symbol
attribute = :first_name
val = user.send attribute # => returns "Jack"
see the definition of update_attribute in the ActiveRecord::Persistence module on github:
def update_attribute(name, value)
name = name.to_s
verify_readonly_attribute(name)
send("#{name}=", value)
save(:validate => false)
end
this leads me to believe you could add the following to your model to achieve that behavior:
alias_method :fname_ref= :first_name=
I'd be interested to know why you want to do that as #Andrew Marshall asked.

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}

How to implenent mongodb 'includes?' finder condition

I am using Mongoid and have a project and a user model.
in the Project model, I have a field
class Project
include Mongoid::Document
field :name
field :user_ids, :type => Array
end
class User
include Mongoid::Document
field :email
end
I can find all the users belonging to one project, i.e., 'find this project's users'
#project = Project.first # => 'Housework'
User.criteria.id(#project.user_ids) # => ['Bart','Lisa','Maggie']
But I am having a bit trouble finding all the projects belonging to one user, i.e, 'find this user's projects'
#user = User.first # => 'Bart'
Project.where(:user_ids => #user.id) # doesn't work
Project.where(:user_ids.includes => #user.id) # not such method
Project.where(:user_ids => [#user.id]) # doesn't make sense to compare arrays, but tried anyway and doesn't work
I know that you can have another field in the User model to store project_ids, I would gladly do that, but I am just curious, is there a method to be used in finder conditions that works similarly to #includes? in ruby?
I found a solution to this. it is the all_in finder method
example:
Fruit.all[0].colors = ['red','green','blue'] #=> apple
Fruit.all[1].colors = ['yellow','green'] #=> banana
Fruit.all[2].colors = ['red', 'yellow'] #=> pineapple
To find all fruits that have the color red in their 'colors' array field, one can query:
Fruit.all_in(:colors => ['red'])
=>[apple, pineapple]

Resources