Given an array like this:
%w{ field_one, field_two, some_association.field_one }
I need to iterate over this and dynamically call these methods on a given object, exactly as described here: Ruby send method with rails associations
So far I have this, which does fetch the values correctly:
field.include?('.') ? field.split('.').inject(some_object, :send) : some_object.send(field)
Additionally though, I need to call :human_attribute_name on the correct class to generate labels. What is a clean way to accomplish this?
It's not clear what you're trying to accomplish but if I had to guess I'd say you're trying to make some kind of CMS or reporting system in which the definition of what data to display can be altered through a user interface. There are gems for this kind of thing but seeing as you're on this track...
By :human_attribute_name I assume you mean the friendly name to be displayed on the page as a label. First of all it would be more appropriate to use a string rather than symbol as you can then use spaces and more characters. Assuming you never intend to have duplicate label names on the same webpage you could try using a hash instead of an array.
fields = {
"Field One" => "field_one" ,
"Field Two" => "field_two" ,
"Field One Of Some Association" => "some_association.field_one"
}
and then fetch the data in a similar way to that shown in the answer you referenced.
data = {}
fields.each_pair do | friendly_name, attribute |
data[friendly_name] = if attribute.include?('.')
attribute.split('.').inject(some_object, :send)
else
some_object.send(attribute)
end
end
That should leave you with a hash you can loop over in your view like this
{
"Field One" => "Mr" ,
"Field Two" => "Data",
"Field One Of Some Association" => "A Friend Of Mr Data"
}
Related
I am accessing a web service, which returns data as XML that gets populated into a custom model. One of the nodes is called AccountType, and has 2 child nodes, Code, and Description.
So, coming back from the web service, it looks like this:
...
<AccountType>
<Code>ABC</Code>
<Description>Description for ABC</Description>
</AccountType>
...
Once it's transferred into the custom model, those values can then be accessed programmatically from an instance of the Account class like so:
myAccount.account_type_code
=> ABC
myAccount.account_type_description
=> Description for ABC
There is a small amount of possible values for account types, and i need to be able to program logic based on what account type an account is associated with. So i'd like to enumerate the possible values within the Account class, with the goal of being able to write code that looks something like this:
if (myAccount.account_type_code == Account.account_types.ABC)
...do something...
end
Another aspect that is complicating things for me here is the fact that there is Code and Description associated with account type, so that a definition of the enumeration may need to look something like this:
def self.account_classes
{
:ABC => { :code=>"ABC", :description=>"Description for ABC" }
,:DEF => { :code=>"DEF", :description=>"Description for DEF" }
}
end
which would then require logic to look like this:
if (myAccount.account_type_code == Account.account_types[:ABC][:code])
...do something...
end
Realistically, logic will only depend on the Code, but having the Description stored with it may be useful for test/user messages.
This functionality is part of a gem which will be used by multiple applications with different databases, so i'd prefer to stay away from a database solution.
EDIT
For simplicity, i only listed one property that acts like this (account type), but there are actually 3 or 4 different properties that all follow the same pattern (so also account class, account status, etc.).
Any thoughts much appreciated!
While I am still not sure I understand your exact requirements, I will just throw what I would write right away if faced with your problem.
class Account
attr_accessor :type
##types = [:ABC, :DEF, :GHI]
##types_to_codes = {:ABC => "ABC", :DEF => "DEF", :GHI => "GHI"}
##types_to_descriptions = {:ABC => "Description for ABC", :DEF => "Description for DEF", :GHI => "Description for GHI"}
def self.code_for(type)
##types_to_codes[type]
end
def self.description_for(type)
##types_to_descriptions[type]
end
def self.valid_type?(t)
##types.include?(t)
end
def is_type?(t)
self.type == t
end
def description
self.class.description_for(self.type)
end
end
To be perfectly fair to your problem, if I have an ahead knowledge of what are all the properties you need such code for, I would go even further: I would code-generate those, in the same manner as there are libraries who code-generate proxies for pieces of data coming from web services. If you would like an example, I'll be happy to provide you with one. Ruby really makes it extremely easy to do such things.
As a side note: you could also add a constructor to the Account class which takes a Nokogiri XML fragment and fills the account fields from it.
I hope this helps you somewhat.
I'm using the Facebook API to pull a list of all of my friends. However, I don't want to store the results in the database. I just want to take the hash of results that I get back and perform selects on the results.
However, I'm having a hard time doing that...
I get results like this...
[{"name"=>"John Smith", "id"=>"12345"}, {"name"=>"Jane Doe", "id"=>"23456"}, {"name"=>"Samuel Jackson", "id"=>"34567"}, {"name"=>"Kate Upton", "id"=>"45678"}]
They're stored in #friends
My select is:
#friends.select{|key, hash| hash["name"] == "John Smith" }
but I keep getting an error "undefined method `[]' for nil:NilClass"
Two questions:
1. How can I get the search of the hash to return John Smith (without storing in a database, and without modifying the initial Facebook API query)
2. Is it possible to use "Like" rather than "==" so that it returns results containing the word?
Thank you!
You are getting "undefined method [] for nil:NilClass" because you are using the method select wrongly. Try doing this:
#friends.select{ |hash| hash["name"] == "John Smith" }
So I believe this answers your first question. You don't need to store anything in the database, but in the instance variable #friends, and use select correctly.
Regarding the second question, I believe you should look into regular expressions, as it really depends on what do you mean by "like", but if you say that you want:
it returns results containing the word?
Well, if you want to check for a specific word (let's say Smith), you would do the select as:
#friends.select{ |hash| hash["name"].include? "Smith" }
Which would give you all the elements on the array which name includes the word Smith.
If you need to code a considerably complex validation, the error sometimes doesnt lie in a particular attribute, but in a combination of several of them.
For example, if i want to validate that a the time period between :start_date and :end_date doesnt contain any sunday, the error doesnt belong specifically to either of those fields, but the Errors add method requires to specify it.
Try doing something like this:
# Your Model.rb
validate :my_own_validation_method
...
private
def my_own_validation_method
if there_is_no_sunday_in_the_range
self.errors[:base] << "You must have a Sunday in the time range!"
end
end
Basically, you can add your own complex validations to a model, and when you see that something erroneous has happened, you can add an error string in the array of errors.
model_instance.errors[:base] << "msg"
The answers above are outdated. For Rails 5 and higher you need to call the ActiveModel::Errors add method with :base as the first parameter. See the example below.
model_instance.errors.add(
:base,
:name_or_email_blank,
message: "either name or email must be present"
)
You can use errors[:base] to add general errors that aren't specifically tied to one attribute - rails guide link.
You can actually name the hash key whatever you'd like:
instance.errors[:case_of_the_sundays] << "Error, son."
Just a little more descriptive.
Update:
message array in order to add an error is deprecated.
Now we do something like that.
self.errors.add(:some_key, "Some Error!")
In my Rails 3.1 application, I need to read the raw data of a field, without serialization, and then write it down without serialization. Is this possible? How?
By serialization I mean
class Tenant
serialize :profile_template
end
I obviously can access the field like this:
> t.profile_template
=> [{:title=>"Page 1", ....}]
I then also tried with read_attribute_before_type_cast (as per lucapette's suggestion):
> t.read_attribute_before_type_cast(:profile_template)
=> nil
Using a string instead of a symbol had a different but disappointing result:
> t.read_attribute_before_type_cast("profile_template")
=> [{:title=>"Page 1", ...}]
and same with the attribute name:
> t.profile_template_before_type_cast
=> [{:title=>"Page 1", ...}]
Just for the record, what I was expecting is:
"---
- :title: Page 1
...."
In all samples, ... is the rest of a very long structure.
Yes there is a way. You have to use
read_attribute_before_type_cast(:foo)
where :foo is the name of the field. The doc is not that good about that but I remember that there is a good explanation about it in The Rails 3 way.
EDIT
Although you're saying that this way isn't working for you I re-read the piece of information from the above-mentioned book. Well, there's another way of doing that. You can use
bar = foo_before_type_cast
where foo is the name of the field. It works like magic finders, pre-pending the name of the field to _before_type_cast . I can't try it right now but it really should work fine.
I am using Ruby on Rails 3.0.9 and I am trying to validate a nested model. Supposing that I run validation for the "main" model and that generates some errors for the nested model I get the following:
#user.valid?
#user.errors.inspect
# => {:"account.firstname"=>["is too short", "can not be blank"], :"account.lastname"=>["is too short", "can not be blank"], :account=>["is invalid"]}
How you can see the RoR framework creates an errors hash having following keys: account.firstname, account.lastname, account. Since I would like to display error messages on the front-end content by handling those error key\value pairs with JavaScript (BTW: I use jQuery) that involves CSS properties I thought to "prepare" that data and to change those keys to account_firstname, account_lastname, account (note: I substitute the . with the _ character).
How can I change key values from, for example, account.firstname to account_firstname?
And, mostly important, how I should handle this situation? Is what I am trying to do a "good" way to handle nested model errors? If no, what is the common\best approach to do that?
I've made a quick Concern which shows full error messages for nested models:
https://gist.github.com/4710856
#1.9.3-p362 :008 > s.all_full_error_messages
# => ["Purchaser can't be blank", "Consumer email can't be blank", "Consumer email is invalid", "Consumer full name can't be blank"]
Some creative patching of the Rails errors hash will let you achieve your aim. Create an initializer in config/initalizers, let call it errors_hash_patch.rb and put the following in it:
ActiveModel::Errors.class_eval do
def [](attribute)
attribute = attribute.to_sym
dotted_attribute = attribute.to_s.gsub("_", ".").to_sym
attribute_result = get(attribute)
dotted_attribute_result = get(dotted_attribute)
if attribute_result
attribute_result
elsif dotted_attribute_result
dotted_attribute_result
else
set(attribute, [])
end
end
end
All you're doing in here is simply overriding the accessor method [] to try a little harder. More specifically, if the key you're looking for has underscores, it will try to look it up as is, but if it can't find anything it will also replace all the underscores with dots and try to look that up as well. Other than that the behaviour is the same as the regular [] method. For example, let's say you have an errors hash like the one from your example:
errors = {:"account.firstname"=>["is too short", "can not be blank"], :"account.lastname"=>["is too short", "can not be blank"], :account=>["is invalid"]}
Here are some of the ways you can access it and the results that come back:
errors[:account] => ["is invalid"]
errors[:"account.lastname"] => ["is too short", "can not be blank"]
errors[:account_lastname] => ["is too short", "can not be blank"]
errors[:blah] => []
We don't change the way the keys are stored in the errors hash, so we won't accidentally break libraries and behaviours that may rely on the format of the the hash. All we're doing is being a little smarter regarding how we access the data in the hash. Of course, if you DO want to change the data in the hash, the pattern is the same you will just need to override the []= method, and every time rails tries to store keys with dots in them, just change the dots to underscores.
As to your second question, even though I have shown you how to do what you're asking, in general it is best to try and comply with the way rails tries to do things, rather than trying to bend rails to your will. In your case, if you want to display the error messages via javascript, presumably your javascript will have access to a hash of error data, so why not tweak this data with javascript to be in the format that you need it to be. Alternatively you may clone the error data inside a controller and tweak it there (before your javascript ever has access to it). It is difficult to give advice without knowing more about your situation (how are you writing your forms, what exactly is your validation JS trying to do etc.), but those are some general guidelines.
I had the same problem with AngularJs, so I decided to overwrite the as_json method for the ActiveModel::Errors class in an initializer called active_model_errors.rb so that it can replace . for _
Here is the initializer code:
module ActiveModel
class Errors
def as_json(options=nil)
hash = {}
to_hash(options && options[:full_messages]).each{ |k,v| hash[k.to_s.sub('.', '_')] = messages[k] }
hash
end
end
end
I hope it can be helpful for someone
I'm not sure but I think you can't change that behavior without pain. But you could give a try to solutions like http://bcardarella.com/post/4211204716/client-side-validations-3-0