Need help walking through nested hashes - ruby-on-rails

Pardon my ignorance, I'm new to Ruby, but not programming. I'm using a gem called Ancestry to store hierarchical data in my application. They have a method called "descendants" that returns a nested hash of all the descending children, grandchildren, etc.. in a node, but I'm having a hard time trying to loop through each node and output all the child nodes.
Here's what the data structure looks like
{
#<Forum id: 16, name: "Parent 1", ancestry: "7", display_order: 1> =>{},
#<Forum id: 17, name: "Parent 2", ancestry: "7", display_order: 2> =>{},
#<Forum id: 13, name: "Parent 3", ancestry: "7", display_order: 3> =>
{
#<Forum id: 14, name: "Child 1", ancestry: "7/13", display_order: 1> =>{},
#<Forum id: 15, name: "Child 2", ancestry: "7/13", display_order: 2> =>
{
#<Forum id: 18, name: "Grand Child", ancestry: "7/13/15", display_order: 1> =>{},
}
}
}
When I loop through that data using the code below, the parent nodes are the only ones that end up being rendered to the screen.
<% forum.descendants.arrange(:order => :display_order).map do |forum,key| %>
<%= render :partial => 'forum', :locals => {:forum => forum} %>
<% end %>
How can I render the child nodes as well? I know they're accessible through the "key" variable, but I don't know how to tell when the "key" variable has data that I can render and how to output that data. Any suggestions?

You can check if the key is a hash by doing key.is_a?(Hash).
In your example, it will always evaluate to true because {} is an empty hash. You can check whether the hash is empty with key.empty?
To iterate through the nested hash, you can do it recursively something like this
def display hash
hash.each do |key, val|
# print key etc
if val.is_a?(Hash)
display val
else
# print value or whatever operation you want
end
end
end

Related

Why do I receive undefined method after dynamically building array of Objects? Ruby

I'm building a most viewed post feature for a simple blog. Each post has a view count that is increased when the Show action is called for that particular post. Then on the Dashboard , I'm trying to list the top 5 posts. So far my code works and returns an array of posts with the post with the highest number of view count being the first index and the last index in the array being the post with the lowest view count. The only thing is when I try to iterate through the array in the view , the view returns:
ERROR
undefined method `title' for nil:NilClass
WHY??? Does it have to do with the "#" infront of the object?
Heres my code.
Dashboard View
<h3> Post </h3>
<% #top_posts.each do |post| %>
<%= post.title %>
<% end %>
Controller Methods
def get_top
#top_posts = []
counts = []
#posts = Post.all
#posts.each do |post|
counts << post.view_count
end
#posts.each do |post|
if counts.max(5).include?(post.view_count)
counts.max(5).each do |n|
if n == post.view_count
#top_posts[counts.max(5).index(n)] = post
end
end
end
end
end
def dashboard
#posts = Post.all
get_top
end
The Top Podcast Array of objects
[#<Post id: 6, title: "Post 6", desc: "", tags: "", view_count: 8, s_desc: "", c_photo: nil, photos: nil, created_at: "2017-06-14 06:02:25", updated_at: "2017-06-15 01:38:40", featured: nil>, #<Post id: 3, title: "post 3", desc: "", tags: "", view_count: 5, s_desc: "", c_photo: nil, photos: nil, created_at: "2017-06-14 05:35:32", updated_at: "2017-06-14 05:35:53", featured: nil>, #<Post id: 5, title: "Post 5", desc: "", tags: "", view_count: 4, s_desc: "", c_photo: nil, photos: nil, created_at: "2017-06-14 06:02:20", updated_at: "2017-06-15 01:38:31", featured: nil>, nil, #<Post id: 4, title: "Post 4", desc: "", tags: "", view_count: 3, s_desc: "", c_photo: nil, photos: nil, created_at: "2017-06-14 05:49:29", updated_at: "2017-06-15 01:38:50", featured: nil>]
The other answer would probably solve your error, but just want to make an attempt to optimize your code.
That array, loop, etc. is unnecessary, ask your db to do that stuff and get the top posts. Fetching all posts and looping over it multiple times...nay, try the following, hopefully this is what you are looking for..
#top_posts = Post.order('view_count desc').limit(5)
That's it, your view needs no change and will work as expected.
Try:
#top_posts << post
instead of:
#top_posts[counts.max(5).index(n)] = post
You don't need to set the array index.

Array of hashes where hash is returned by a function in jbuilder

I have a PORO TutorProfileHandler that has a function json that returns a hash.
class TutorProfileHandler
def initialize(opts)
#profile = opts[:tutor_profile]
end
def json
tutor = #profile.tutor
return {
id: tutor.id,
first_name: tutor.first_name,
last_name: tutor.last_name.first + '.',
school: #profile.school,
avatar: #profile.avatar.url,
bio: #profile.bio,
academic_level: #profile.academic_level,
headline: #profile.headline,
major: #profile.major,
rate: #profile.rate,
rating: #profile.rating,
courses: JSON.parse(#profile.courses),
video_url: #profile.video_url
}
end
end
In my index_tutor_profiles.json.jbuilder, I would like to generate
{
tutor_profile: [{id: 1, ...}, {id: 2, ...}, ...],
tutor_sum: 20
}
However when I do this
json.tutor_profiles (#tutor_profiles) do |profile|
TutorProfileHandler.new({tutor_profile: profile}).json
end
json.tutor_sum #tutor_sum
It gives me an empty array for tutor_profiles.
However if I move everything from TutorProfileHandler.json to the jbuilder file, it works. How do I explicitly include the hash returned by TutorProfileHandler.json in the jbuilder array?
Note: This returns an array, but it creates a new key-value pair array:
json.tutor_profiles json.array(#tutor_profiles) do |profile|
TutorProfileHandler.new({tutor_profile: profile}).json
end
Result:
{
array: [{id: 1, ...}, {id: 2, ...}, ...],
tutor_profile: [],
tutor_sum: 20
}
There is a ugly approach:
json.tutor_profiles #tutor_profiles do |profile|
tmp_json = TutorProfileHandler.new({tutor_profile: profile}).json
json.(tmp_json, *(tmp_json.keys))
end
I think the best practise is directly nesting inside model. You can get more information from the its github page.

Different behavior of build method

I was trying to implement a first_or_build method and I encounter a problem when saving my parent : the children were missing.
Everything is working fine when I call my method on the relation like parent.childs.first_or_build(name: 'Foo'); parent.save! whereas nothing happen when I do parent.childs.where(name: 'Foo').first_or_build; parent.save!.
The main objective was to propose a similar behavior than .first_or_create applied to the result of a query for example. (Don't tell me about .first_or_initialize !)
Any idea?
Examples :
# this is not working :(
2.times { |i| parent.childs.where(name: "child #{i}").build { |c| c.age = 42 } } ; parent.childs
=> #<ActiveRecord::Associations::CollectionProxy []>
# while this is
2.times { |i| parent.childs.build { |c| c.name = "#{child #{i}"; c.age = 42 } } ; parent.childs
=> #<ActiveRecord::Associations::CollectionProxy [#<Child name: "child 0", age: 42>, #<Child name: "child 1", age: 42>]>
Sorry, I don't quit understand the part about first_or_build method, so I will just talk about the examples there.
First of all, we know that parent.childs.where(name: "child #{i}") and parent.childs are in different class
parent.children.where(name: "child").class
#=> Child::ActiveRecord_AssociationRelation
parent.children.class
#=> Child::ActiveRecord_Associations_CollectionProxy
so it's clear why their :build method are different, the doc are here
ActiveRecord_Associations_CollectionProxy
ActiveRecord_AssociationRelation
I will try to express my view here.
When you use ActiveRecord_AssociationRelation to build a new child, it will initialize a new Child object, and set its parent_id, but it is just an Child object. In this time, when you execute parent.children, the result is empty.
parent.children.where(name: "child1").build({age: 1})
#=> <Child id: nil, name: "child1", age: 1, parent_id: 1, created_at: nil, updated_at: nil>
parent.children
#=> <ActiveRecord::Associations::CollectionProxy []>
parent.save #=> true
parent.children.reload
#=> <ActiveRecord::Associations::CollectionProxy []>
But when you use ActiveRecord_Associations_CollectionProxy, it will initialize a new Child object, and it will also attach itself to parent, so then when you execute parent.children, the result is not empty.
parent.children.build({name: "child2", age: 2})
#=> <Child id: nil, name: "child2", age: 2, parent_id: 1, created_at: nil, updated_at: nil
parent.children
#=> <ActiveRecord::Associations::CollectionProxy [#<Child id: nil, name: "child2", age: 2, parent_id: 1, created_at: nil, updated_at: nil>]>
parent.save #=> true
parent.children.reload
#=> <ActiveRecord::Associations::CollectionProxy [#<Child id: 3, name: "child2", age: 2, parent_id: 1, created_at: "2015-05-28 17:02:39", updated_at: "2015-05-28 17:02:39">]>
In the second way, parent know it has children, so when it save, it will save its children.I think this is it.

Attribute changes are not notified and table is not updated

I want to update a table but the update SQL is not performed so the changes has no effect.
Details:
Originally issue.name us 'issue'. I want to change to 'qqqqqqqqqqqq'.
Controller:
def update
if params['cancel']
redirect_to(#issue)
return
end
#issue = Issue.find(params[:id])
logger.debug "original object"
logger.debug "#{#issue.to_yaml}"
logger.debug 'bulk attribute settings...'
#issue.attributes= params[:issue]
logger.debug "after bulk settings"
logger.debug "#{#issue.to_yaml}"
#issue.events.build(:note=>params[:issue][:description],:verb=>'Edited', :changes=>#issue.textalize_changes)
if #issue.save
logger.debug "after save"
logger.debug "#{#issue.to_yaml}"
redirect_to(#issue)
else
render :action => "edit"
end
end
One additional code fregment might be interesting is the textalize_changes:
def textalize_changes
r = ""
if changed?
changes.keys.each do |k|
r << "#{k.humanize} changed "
r << "from `#{translate(k,changes[k][0])}` "
r << "to `#{translate(k,changes[k][1])}`"
r << "<br/>"
end
end
r unless r.blank?
end
The result:
until the line #issue.save everything looks correct. I have checked that #issue contains all the changes i have made.
on the user interface no changes I have notified.
in the log no update SQL is visible. all relevant select and insert is presented but no update - so no change in table issues
even textalize_changes has not realize and changes in the object when checking for that (changed?)
If I manually change the attribute in at source code level by name=XXX it is working.
I don't know what to check or review at all. The code is so simple that i have no idea at all.
Here is the log of the operation:
original object
--- !ruby/object:Issue
attributes:
name: OTTO TEST 2
assigned_to: "29"
updated_at: 2010-12-16 10:25:28
project_id: "1"
current_estimate:
lft: "1"
original_estimate:
priority:
id: "10"
version_id:
area_id:
description:
worktype_id: "2"
status_id: "5"
rgt: "2"
parent_id:
created_at: 2010-05-21 07:37:15
fixed_in_version_id:
attributes_cache: {}
bulk attribute settings...
WARNING: Can't mass-assign these protected attributes: description
[4;36;1mIssue Load (0.0ms)[0m [0;1mSELECT "lft", "rgt", "parent_id" FROM "issues" WHERE ("issues"."id" = 10) [0m
after bulk settings
--- !ruby/object:Issue
area:
assigned_user:
attributes:
name: qqqqqqqqqq
assigned_to: "29"
updated_at: 2010-12-16 10:25:28
project_id: "1"
current_estimate:
lft: "1"
original_estimate:
priority:
id: "10"
version_id:
area_id:
description:
worktype_id: "2"
status_id: "5"
rgt: "2"
parent_id:
created_at: 2010-05-21 07:37:15
fixed_in_version_id:
attributes_cache: {}
changed_attributes: {}
children:
events:
fixed_in_version:
iterations:
marked_for_destruction: false
parent:
project:
status:
timelogs:
version:
work_items:
worktype:
[4;35;1mEvent Create (0.0ms)[0m [0mINSERT INTO "events" ("updated_at", "verb", "external", "issue_id", "note", "changes", "user_id", "created_at") VALUES('2010-12-16 10:33:08', 'Edited', 'f', 10, '', NULL, 1, '2010-12-16 10:33:08')[0m
after save
[4;36;1mEvent Load (16.0ms)[0m [0;1mSELECT * FROM "events" WHERE ("events".issue_id = 10) ORDER BY id ASC, created_at ASC[0m
--- &id001 !ruby/object:Issue
area:
assigned_user:
attributes:
name: qqqqqqqqqq
assigned_to: "29"
updated_at: 2010-12-16 10:25:28
project_id: "1"
current_estimate:
lft: "1"
original_estimate:
priority:
id: "10"
version_id:
area_id:
description:
worktype_id: "2"
status_id: "5"
rgt: "2"
parent_id:
created_at: 2010-05-21 07:37:15
fixed_in_version_id:
attributes_cache: {}
Technical info:
Op system: winXP
rails: rails 2.3.4
Additional info:
I have a bulk operation with the same purpose which is working correctly. I really don't know the differences:
def update_multiple
if params['cancel']
redirect_to issues_path
return
end
#issues = Issue.find(params[:issue_ids])
#issues.each do |issue|
issue.attributes= params[:issue].reject {|k,v| v.blank? }
issue.apply_template_on_name_change
issue.events.build(:note=>params[:issue][:description],:verb=>"Edited", :changes=>issue.textalize_changes)
issue.save!
end
flash[:notice]="Issues updated!"
redirect_to issues_path
end
Additional info:
If I replace #issue.attributes= params[:issue] by #issue.attributes= params[:issue].reject {|k,v| v.blank? } it apply changes and working. But it is not the one I really want. I want to change everything at once. I am going crazy.
Can you try save with exclamation, like #issue.save! to see the errors.
I have found the issue in awesome nested set.
This plugin reloads the object just before executing nested set operations.: I have changed the controller in the following way:
#issue.parent_issue= params[:issue][:parent_issue]
#issue.attributes= params[:issue].reject {|k,v| 'parent_issue'==k }
this the best solutions now.
note: parent_issue is for setting the parent of the given entity and using the same form as for the rest of the attributes.

How to populate select box from db query

I am trying to populate a ruby on rails select box from a database query, the data comes from 3 tables.
My query
#data = Session.all :include => { :term => :courses }
Object
!ruby/object:Session
attributes:
created_at: 2010-06-17 22:12:05
term_id: "15"
updated_at: 2010-06-17 22:12:05
id: "3"
course_id: "1"
attributes_cache: {}
term: &id003 !ruby/object:Term
attributes:
number: "1"
start_date: 2010-06-17
created_at: 2010-06-17 22:12:05
updated_at: 2010-06-17 22:12:05
id: "15"
attributes_cache: {}
courses:
- &id001 !ruby/object:Course
attributes:
created_at:
updated_at:
course_name: Beginner
id: "1"
date:
course_type: Programming
attributes_cache: {}
what i am trying to do is to have the term number followed by the data data and then the course
like this
1 01-09-10 Programming Beginners
The id for the option would be the session_id
any ideas ?
Thanks
Alex
in your erb template you can put the following code withing you form:
<%= select("session","id", #data.map{|d| ["#{d.term.number} #{d.term.start_date} #{d.course.course_type} #{d.course.course_name}",d.id]} %>

Resources