Ruby each loop x to variable substitution - ruby

<% #parts.each do|x| %>
<% #bbb = Relation.where(part: "#{x}") %>
<%= **#"#{x}"** = #bbb.collect {|x| x.car} %>
<% end %>
I'm trying to set the variable in line 3 to the x part value #"#{x}". I can't find the right syntax. I know about send(x) and x.to_sym. But I need to know how to set x in the each loop to an #variable with #. Thanks!

So you're looking for instance_variable_set
instance_variable_set "##{x}", #bbb.collect { |x| x.car }
But this is probably not the best way to handle this. Without seeing the code that uses it, its hard to really say, but maybe consider putting the results into a hash: result[x] = #bbb.collect { |x| x.car }
Also note that "#{x}" is the same as x.to_s
Also, it's best to avoid querying models directly in your views (assuming you're doing Rails here, since you appear to be using ActiveRecord), because it mixes presentation with code (you can't take them as separate pieces. It has a tendency to get really ugly), it couples your view to the one use case you initially had (can't present different data in the same view even though it should be presented the same). Consider moving this code into your controller (or even some other object whose responsibility is to manage the querying of this data, leaving your controller responsibilities at just dealing with the request.

Related

Can I use sort and uniq on an array of chef node attributes?

We have a template that generates a bash script, that includes a list of patches to check for based on node attributes - specifically the role assigned to the node.
patchlist=( <%= node['oracledb']['install']['oneoff']['db_oo_apply'] rescue nil %> <%= node['oracledb']['install']['oneoff']['db_psu_num'] rescue nil %> )
I've snipped the actual list a bit, but you get the point.
The issue is that there are potentially duplicate entries between these attributes, so we want to sort them and get a unique list.
Also, complicating matters, not all nodes in every environment have those attributes - which is why we have the rescue nil sprinkled in there.
I can build the array as
patchlist=( <%= ( node['oracledb']['install']['oneoff']['db_oo_apply'] + ' ' + node['oracledb']['install']['oneoff']['db_psu_num'] rescue nil).split(' ').uniq.sort.join(' ') %> )
which works, if all the attributes have values.
But what seems to happen is that if any single attribute in the list is empty, the rescue nil kicks in and the entire array comes back as nil.
I think that in the first variant, the scope of the rescue nil is limited to the specific attribute, but in the second it applies to the entire string.
So, can I somehow build this array, have it sorted and unique, and still control for cases where individual elements in the array might be null?
just use pure ruby for that, for instance:
irb(main):001:0> ([nil] + [2,1]).compact.uniq.sort
=> [1, 2]
in short, concatenate arrays into a new array, compact it to get a copy of the concatenated array with all nil elements removed, then apply uniq and sort.
One way to achieve this is to create an actual array of node attributes. In your current implementation, it is not an array, just attributes separated by space.
This is a little bit of logic overload on the template, but it should work.
Example template:
<% p_list = %W( #{node['oracledb']['install']['oneoff']['db_oo_apply']} #{node['oracledb']['install']['oneoff']['db_psu_num']} )
<% p_list_fin = p_list.uniq.sort %>
patchlist=( <% p_list_fin.each do |p| %><% next if p.nil? %><%= p %> <% end %>)
Of course the creation of array p_list of node attributes can be done from within recipe and passed in variables of the template resource as well.

How to delete a character from the last instance of .each

I want to generate a JSON in my view of a rails project. I am doing like this: (I am not a developer, it is bad coding I know)
<% #content.keywords.each do |x| %>
<% if x.type == "keyword" %>
{
"key": "<%= x.keyword %>",
"weight": <%= x.weight %>
},
<% else %>
<% end %>
<% end %>
Nonetheless, for the last keyword of my JSON
{
"key": "<%= x.keyword %>",
"weight": <%= x.weight %>
},
I have a character "," that I want do delete for my JSON to be valid. I don't know how to do it. Can you provide any help?
Here's how I would go about refactoring this to achieve what you want:
Make things a little easier.
Part of the reason we avoid lots of logic in an ERB file is that it makes things like this a lot more awkward. So generally we'd put this logic into a helper file, like app/helpers/keywords_helper.rb, so that we make a distinction between displaying things and computing things. For you, the helper would look like this:
module KeywordsHelper
def keywords_json
#content.keywords.map do |x|
if x.type == 'keyword'
%Q({ "key": "#{x.keyword}", "weight": "#{x.weight}" })
else
# did you mean to do nothing here?
end
end.compact.join(', ')
end
end
You'll see I haven't actually changed all that much - instead of using ERB to insert things into the page, I've used #{blah} to insert things into strings.
At the end, I do a little bit of magic:
.compact will get rid of any "null" values (nil). Before that, if x.type is not equal to "keyword", nothing will happen, which will leave you with nil. You might end up with an array like [nil, "{\"key\"...}", "{\"key\"...}", nil, nil] - and this might break some of the other solutions too. By using .compact, it will strip all the nils out and leave you with ["{\"key\"...}", "{\"key\"...}"]
.join(', ') - once we've got an array, we want to sandwich commas between each one. Join takes any array, tries to turn the items into strings (if they weren't already), and puts whatever string you choose in between. Note that I've also added a space - this isn't strictly necessary, just makes the json look prettier.
In the middle there, you see %Q( - this is one of the things that makes Ruby special. Ruby has a lot of ways of making strings. Some might say too many ways. %Q is one of my favourites - it's the same as using "I'm a string" and gives you all the same facilities, with one exception: you get to choose how you start and end your string - %Q{normally you see this}, %Q[also perfectly valid], %Q~you can even do this~ You could do this with a 'normal' string, but then we'd have to escape the double quotes, e.g. "this is \"how it's done\" in some inferior languages"
We replace the ERB with:
<%= keywords_json %>
... or, more likely,
[ <%= keywords_json %> ]
... because after all, this is a JSON array.
Go even further
This is a much nicer solution... but can we make it even nicer? Have a look at this:
module KeywordsHelper
def keywords_json
#content.keywords.map do |x|
next if x.type != 'keyword'
{ key: x.keyword, weight: x.weight }
end.compact.to_json
end
end
and your erb returns to:
<%= keywords_json %>
You'll notice this is much shorter. A few notes on what we're doing here:
next if x.type != 'keyword'
Where we had the else before, rather than have an empty section in our if... else... end we could have just done if... end. But if we don't need to do that, we can also say "skip this loop if..." - which is what I've done here. Note that we can also be explicit about what we expect to receive from a skipped loop - this is equivalent to the above:
next nil if x.type != 'keyword'
or, if there was something else you wanted return, we could put that here:
next {error: 'not a keyword'} if x.type != 'keyword'
Secondly, rather than building JSON objects in strings and joining them together, Rails provides us some handy tools. We can build Ruby objects (called Hashes) and call .to_json on them to get a valid json object. And we can even do it on arrays to get a valid json array, which is what we're doing here.
I know that's more of an answer than you were looking for, but hopefully it gives you some idea of the things you can do in ruby and gives you a good launch pad for moving on! Good luck!
Generally, it's impossible/difficult to withdraw content you've already written. The trick is to not write it in the first place.
I think, the minimal modification of your code would be to use each_with_index and check current index to determine if we need to write the comma or not. Something like this:
<% #content.keywords.each_with_index do |x, idx| %>
<% if x.type == "keyword" %>
<% if idx > 0 %>
,
<% end %>
{
"key": "<%= x.keyword %>",
"weight": <%= x.weight %>
}
<% end %>
<% end %>
Of course, this code is even worse than what you had, but it does the job and it's not radically different.
Probably what you want, is to move some of the logic out of your view and either into the model or into a helper (or a dedicated serialiser, plenty of options). For example, you could do the following (you would lose a bit of control over the actual layout, I hope that's not a problem):
In a helper:
def keywords_for_json(keywords):
keywords.filter {|k| k.type == 'keyword'}
.map {|k| {key: k.keyword, weight: x.weight} }
end
This does the filtering and converts the objects to hashes using map,
and in the view you could just do :
<%= keywords_for_json(#content.keywords).to_json %>
which converts all the hashes to json objects.
As I said, the actual logic could go in any number of places, depending on personal preference, this example uses a helper because I think it's the simplest to show here. Ideally you'd just set up the model for serialisation, using as_json and only call to_json on your collection (and pre-filter #content.keywords, maybe in your controller or in a model).

Mutating list of instantiated objects in Rails

I'm attempting to clean up my view by moving Rails' sanitizer method to a helper, but it's not producing the desired result. So below is what my index action looks like. I know it's ugly and not very OOP, but I simplified it down so I could follow what was happening when debugging.
I'm attempting to loop through all the sources' attributes, running the sanitizer on any attribute that is a non-empty string, replacing original strings with the sanitized strings (transform_values!), and writing over the original #sources (map!).
I tried storing them in different variables than #sources and using .each instead of .map! but the sanitized values don't make it through.
def index
#sources = Source.all
#sources.map! { |source|
source.attributes.transform_values! { |attr|
attr.blank? || !attr.is_a?(String) ? attr
: ActionController::Base.helpers.sanitize(attr) } }
end
However, after examining my list of sources in the view, it's removing the source instances and instead returning a nondescript array of hashes. I can loop through these, but I can't call specific attributes like source.author which is not great.
Here's some images for reference. The first one is what it should look like and second is what I'm currently getting
Unsanitized sources
Sanitized sources
map! replaces each item in the array with the result of the block. This is not what you intend to do, because you just want to mutate the items, not replace them with something else. Use a plain each instead of map! would do the trick.
On another side, sanitization is actually a responsibility of the view (that’s why it’s defined in a helper). If you need to sanitize often with the same argument, define your own helper:
class ApplicationHelper
def sany(str)
sanitize(str, %w[...])
end
end
<%= sany(source.some_attr) %>
You could also set the default sanitization options following the documentation:
# In config/application.rb
config.action_view.sanitized_allowed_tags = ['strong', 'em', 'a']
config.action_view.sanitized_allowed_attributes = ['href', 'title']

Why pass dot (.) separated string to "p"?

I'm working on some ERB templates that are not my own, and I see the developer has used statements like this a lot:
<%= p("object.property.foo") %>
Where object is an OpenStruct. This method call results in the value of object.property.foo being printed (as in JavaScript, or most languages I know). Which is awesome because it is much more simple than writing:
<%= object["property"]["foo"] %>
My questions are:
Why am I able to access properties with "." notation?
Why do I pass a string to p and not the object itself?
Why is p preferable in this case? (I know p vs. puts, but why use p here?)
<%= %> tells the ERB parser to evaluate the content as a ruby expression, and to include the return value in the resulting HTML text.
p( ) is likely to be a view helper function that creates some HTML tags. This is not obvious from the code fragment. Apparently it evaluates the string argument, again as another ruby expression. p is not a standard rails or ruby method.
object is according to the questioner an OpenStruct. OpenStruct is a data structure that combines the behaviour of a Hash with the syntax of class methods. It is documented here: http://ruby-doc.org/stdlib-2.1.0/libdoc/ostruct/rdoc/OpenStruct.html
object.property asks OpenStruct to apply property on object. OpenStruct replies with a stored value, something like #value[property], where #value would be a Hash. You do not need square bracket syntax, because OpenStruct provides dynamic access methods. The '.' is the ruby operator to apply a method to an object. The internal implementation of OpenStruct's data storage does not have be be a Hash at all. According to the questioner, the return value is another instance of OpenStruct.
object.property.foo calls method foo on the instance of OpenStruct that was returned from object.property. Now we receive the value of a nested OpenStruct object structure.
Seems to me that the developer may have aliased the eval statement with p somewhere earlier in the code.
require 'ostruct'
o = OpenStruct.new(key: 5)
o.key # returns 5
alias p eval
p("o.key") # returns 5
eval is simply a function that executes any string passed into it as ruby code.
Regarding your question
Why is p preferable in this case? (I know p vs. puts, but why use p here?)
I don't believe you are using the classic p function here. By default that p does not "eval" strings passed into it. The p function must have been overwritten with the eval function. Check the code base for something like alias p eval or alias_method :p, :eval

How does form_for generate paths in rails?

Can anyone explain me on how the paths are generated using form_for?. The below code generates a path article_comments_path.
<%= form_for [#article, #comment] do |f| %>
<% end %>
I mean how does it exactly generate article_comments_path and not articles_comment_path
It is using polymorphic_path method to determine the path. It is basicaly a wrpaper around polymorphic_url: http://apidock.com/rails/v4.0.2/ActionDispatch/Routing/PolymorphicRoutes/polymorphic_url
Update:
polymorphic_path is using internally method called build_named_route_call. When it gets an array, it pull the last record out of the array (with pop) and then iterates over the remaining array, changing all the objects to
if it is a model it takes it class and calls `model_name.singular_route_key'
if it is a symbol or string, leaves it as it is
Then we are left with the last element. It can be singular or plural, this is resolved in polymorphic_url method with this case statement:
inflection = if options[:action] && options[:action].to_s == "new"
args.pop
:singular
elsif (record.respond_to?(:persisted?) && !record.persisted?)
args.pop
:plural
elsif record.is_a?(Class)
args.pop
:plural
else
:singular
end
where record is a first element of the array (if array is passed). inlection var is then passed to build_named_route_call so it can build correct helper name. As you can see this form will generate different paths depending on whether the first element is already persisted or not.

Resources