Rails: how to add items to an instance variable - ruby-on-rails

In my index action I am creating an instance variable like so:
def index
#cards = Card.where(:cardstack_id => params[:cardstack_id])
end
This creates the output for #cards:
[#<Card id: 3, header: "Crazy", body: "Lege deinen Kopf für zwei Minuten auf den Tisch un...", points: 1, cardstack_id: 1, created_at: "2017-06-09 16:41:09", updated_at: "2017-06-13 17:24:29", lifetime: 240>, #<Card id: 4, header: "Böse Zungen", body: "Sprich 20 Minuten in einem starken Dialekt, der un...", points: 3, cardstack_id: 1, created_at: "2017-06-09 16:42:11", updated_at: "2017-06-13 17:26:24", lifetime: 360> ...
What I want to do now is to add for each object a random token created with SecureRandom.uuid.
So my output should look like this:
[#<Card id: 3, header: "Crazy", token: "34985jlskd908tjkls980", body : "..." ...]
How can I achieve this? I guess I have to somehow loop through the array in my controller, create a token for each element and then add it to a new array. However, I don't know how to achieve this.

I guess I have to somehow loop through the array in my controller,
create a token for each element and then add it to a new array.
However, I don't know how to achieve this.
You won't be able to do this and keep a ActiveRecord::Relation object (which is the result of your query); but you could get an array of hashes, where each hash will contain all attributes (and values) for each record, including any new key you need to add, for example:
#cards = #cards.map { |i| i.attributes.symbolize_keys.merge({ token: SecureRandom.uuid }) }
Using your example, this will be the content for #cards:
[
{
:id=>3,
:header=>"Crazy",
:body=>"Lege deinen Kopf für zwei Minuten auf den Tisch un...",
:points=>1,
:cardstack_id=>1,
:created_at=>"2017-06-09 16:41:09",
:updated_at=>"2017-06-13 17:24:29",
:lifetime=>240,
:token=>"fa637bfa-a781-4029-8f60-2763e75d6d5c"
},
{
:id=>4,
:header=>"Böse Zungen",
:body=>"Sprich 20 Minuten in einem starken Dialekt, der un...",
:points=>3,
:cardstack_id=>1,
:created_at=>"2017-06-09 16:42:11",
:updated_at=>"2017-06-13 17:26:24",
:lifetime=>360,
:token=>"2ff962cf-2258-4f2a-8d50-d8a864fb845a"
}
]
Then, you can iterate in your view just like any array, for example:
<% #cards.each do |card| %>
<p>ID: <%= card[:id] %></p>
<p>Header: <%= card[:header] %></p>
...
<p>Token: <%= card[:token] %></p>
<% end %>

It looks like you want to generate a UUID value for each card so that you can use it as a public id value without exposing the db id. Is that correct?
In that case, you shouldn't be doing it here. This should be done in after_initialize.
class Card < ActiveRecord::Base
after_initialize do
self.token ||= SecureRandom.uuid if has_attribute?(:token)
end
end
Use of has_attribute? is necessary here in case you constrain the lookup with select and don't include token in the query.
Make sure you read up on the performance impacts of UUIDs in databases.
I ended up using this pattern for so many things I wrote a simple gem for it.
class Card < ActiveRecord::Base
safe_initialize :token, with: ->{ SecureRandom.uuid }
end

Related

How to sort a hierarchical array of hashes

I'm working with an array like the below:
arr = [{
item: "Subject",
id: "16",
parent_id: ""
},
{
item: "Math",
id: "17",
parent_id: "16"
},
{
item: "Geology",
id: "988",
parent_id: "208"
},
{
item: "Biology",
id: "844",
parent_id: "208"
},
{
item: "Botany",
id: "594",
parent_id: "844"
},
{
item: "Science",
id: "208",
parent_id: "16"
}
]
I'm wanting to sort them so and print them out so that they display like this, grouping them and showing their parentage within the hierarchy as indentations:
Subject
Math
Science
Geology
Biology
Botany
I'm fairly stumped on how to accomplish this. I am ultimately wanting to iterate through the array only once, but I get stuck when I realize that a parent item may come after its child. Any help is greatly appreciated.
Edit: eliminated the duplicate item
To sort an array by an attribute of its elements...
arr = arr.sort_by{ |e| e[:parent_id] }
Then a little recursion to walk the tree...
def display_hierarchy(arr, parent_id="", level = 0)
current_indent = 2 * level
arr.select{ |e| e[:parent_id] == parent_id }.each do |elem|
name = elem[:item]
puts name.rjust(name.length + current_indent)
display_hierarchy(arr, elem[:id], level + 1)
end
end
Putting it all together...
arr = arr.sort_by{ |e| e[:parent_id] }
display_hierarchy(arr)
Note, if you have duplicate parents, you will get duplicate branches (hint: your example array has a duplicate "Science" node).
Also note, sorting the array ahead of time doesn't really matter for the tree, since it is a hierarchy, I just added it to show how to do it. You'd probably just sort each set of children like so...
def display_hierarchy(arr, parent_id="", level = 0)
current_indent = 2 * level
arr.select{ |e| e[:parent_id] == parent_id }.sort_by{ |e| e[:item]}.each do |elem|
name = elem[:item]
puts name.rjust(name.length + current_indent)
display_hierarchy(arr, elem[:id], level + 1)
end
end
And you get this...
Subject
Math
Science
Biology
Botany
Geology
Science
Biology
Botany
Geology
I am ultimately wanting to iterate through the array only once
In a way, you can do that: by transforming the array into a different structure that's more fit for the task. You'd still have to traverse the resulting collection again though.
Say, you could partition the array into groups by their :parent_id:
children = arr.group_by { |subject| subject[:parent_id] }
Then it's your typical recursive walk, where indentation is added on every level:
hierarchical_print = proc do |node, indent = ""|
puts(indent + node[:item]) # base
new_indent = indent + " "
children[node[:id]]&.each do |child| # recursion
hierarchical_print.(child, new_indent)
end
end
Notice the use of &. (the "lonely operator") in there that only calls the method if the callee isn't nil. Because getting a value from a hash with a key that isn't there returns nil.
In your case the root is a node with an empty parent id, so it's readily available from the children hash as well:
hierarchical_print.(children[""].first)
Subject
Math
Science
Geology
Biology
Botany
Science
Geology
Biology
Botany
...well, yes, you do have two Sciences in there.

How to push/shovel/append active record relation correctly?

I have a fairly complex search form that allows for multiple duplicate nested fields to be submitted at once. I generate unique id's on clone so i can separate them using jquery. I then iterate over the duplicates and do something like the following:
So i have something like:
{..., "search"=>{"searches"=>{}, "searches_0"=>{}...{other_attributes}}
def search_post
search = []
params.each do |k, v|
search << do_search(params, v)
end
end
def do_search(search, v)
search_array = []
search = Model.where() if v[:this_param].present?
search = Model.where() if v[:that_param].present?
# it will be only one of the `search` as this_param or that_param can't be searched
together
search_array << search.where(attr: search[:attr]) if attr.present?
search_array << search.where(attr_2: search[:attr_2]) if attr_2.present?
search_array << search.where(attr_3 search[:attr_3]) if attr_3.present?
search_array.uniq
end
This gives a result like:
[#<ActiveRecord::Relation [#<LineItem id: 15, created_at: "2020-01-03 15:49:19", updated_at: "2020-01-03 15:49:19", ...>]>, #<ActiveRecord::Relation [#<LineItem id: 14, created_at: "2020-01-03 15:49:19", updated_at: "2020-01-03 15:49:19", ...>]>]
I obviously get an array but I need to do more queries on it.
I have tried using search.reduce([], :concat).uniq but this only removes all of the results and only keeps the ActiveRecord::Relation aspect of the array.
What I need is to shovel the results from the loop and be able to use where on it.
How can this be accomplished?
By the looks of it you can try using a query object:
class ModelQuery
def initialize(initial_scope = Model.all)
#initial_scope = initial_scope
end
def call(params)
scoped = by_this_param(initial_scope, params[:this_param])
scoped = by_that_param(initial_scope, params[:that_param])
scoped = by_other_param(initial_scope, params[:other])
# ...
scoped
end
def by_this_param(s, this_param = nil)
this_param ? s.where(this_attr: this_param) : s
end
def by_that_param(s, that_param = nil)
that_param ? s.where(that_attr: that_param) : s
end
# ...
def by_other(s, other = nil)
other ? s.where(other_attr: other) : s
end
end
You can then do things like:
q = ModelQuery.new
# or perhaps...
# q = ModelQuery.new(Model.where(whatever: "however"))
q.call params
q.order(whatever_column: :asc).group(:however)
Obviously you need to adapt and extend the code above to your variable names/parameters. Another upside of using the query object pattern is that it gives you a subtle nudge to structure your parameters coherently so you can pass from view and return from AR quickly, without much fiddling about with the input/output.
Give it a try!

Fastest way to remove a key from array and make it last

I have to remove the 'Other' Category from the array, which is originally sorted alphabetically, and just make it the last index. I created this little helper but believe there could be a faster way of accomplishing this.
The array is something like this [#<Category id: 17, title: "Books">, #<Category id: 18, title: "Children's Clothing">,
Here is what I've done. It works. Although, I was wonder if theres a more efficient way.
<%
#options = []
#other_option = []
#free_item_options.each do |category|
if category.title.downcase == "other"
#other_option << category
else
#options << category
end
end
#options << #other_option[0]
%>
In cases like this, I usually reach for multi-parameter sorting.
#free_item_options.sort_by do |option|
[
option.title.casecmp?('other') ? 1 : 0,
option.title,
]
end
"Other" category will have 1 and will sort last. Everything else will have 0 and will sort between themselves by ascending title.
Another approach is to just use SQL.
#free_item_options = Category.select("categories.*, (LOWER(title) = 'other') as is_other").order('is_other', :title).to_a
There is Enumerable#partition which is designed to split a collection up in two partitions.
#other_option, #options = #free_item_options.partition { |category| category.title.casecmp?('other') }
#options.concat(#other_options)
If you are certain there is a maximum of one "other" category (which seems to be the case based upon #options << #other_option[0]). You could also use find_index in combination with delete_at and <<. find_index stops iterating upon the first match.
index = #free_item_options.find_index { |category| category.title.casecmp?('other') }
#free_item_options << #free_item_options.delete_at(index) if index
Keep in mind the above does mutate #free_item_options.

Ruby Array#sort_by on array of ActiveRecord objects seems slow

I'm writing a controller index method that returns a sorted array of ActiveRecord Contact objects. I need to be able to sort the objects by attributes or by the output of an instance method. For example, I need to be able to sort by contact.email as well as contact.photos_uploaded, which is an instance method that returns the number of photos a contact has.
I can't use ActiveRecord's native order or reorder method because that only works with attributes that are columns in the database. I know from reading that normally array#sort_by is much faster than array#sort for complex objects.
My question is, how can I improve the performance of this block of code in my controller method? The code currently
contacts = company.contacts.order(last_name: :asc)
if params[:order].present? && params[:order_by].present? && (Contact::READ_ONLY_METHOD.include?(params[:order_by].to_sym) || Contact::ATTRIBUTES.include?(params[:order_by].to_sym))
contacts = contacts.sort_by do |contact|
if params[:order_by] == 'engagement'
contact.engagement.to_i
else
contact.method(params[:order_by].to_sym).call
end
end
contacts.reverse! if params[:order] == 'desc'
end
The root problem here (I think) is that I'm calling sort_by on contacts, which is an ActiveRecord::Relation that could have several hundred contacts in it. Ultimately I paginate the results before returning them to the client, however they need to be sorted before they can be paginated. When I run the block of code above with 200 contacts, it takes an average of 900ms to execute, which could be a problem in a production environment if a user has thousands of contacts.
Here's my Contact model showing some relevant methods. The reason I have a special if clause for engagement is because that method returns a string that needs to be turned into an integer for sorting. I'll probably refactor that before I commit any of this to return an integer. Generally all the methods I might sort on return an integer representing the number of associated objects (e.g. number of photos, stories, etc that a contact has). There are many others, so for brevity I'm just showing a few.
class Contact < ActiveRecord::Base
has_many :invites
has_many :responses, through: :invites
has_many :photos
has_many :requests
belongs_to :company
ATTRIBUTES = self.attribute_names.map(&:to_sym)
READ_ONLY_METHOD = [:engagement, :stories_requested, :stories_submitted, :stories_published]
def engagement
invites = self.invites.present? ? self.invites.count : 1
responses = self.responses.present? ? self.responses.count : 0
engagement = ((responses.to_f / invites).round(2) * 100).to_i.to_s + '%'
end
def stories_requested
self.invites.count
end
def stories_submitted
self.responses.count
end
def stories_published
self.responses.where(published: true).count
end
end
When I run a query to get a bunch of contacts and then serialize it to get the values for all these methods, it only takes ~80ms for 200 contacts. The vast majority of the slowdown seems to be happening in the sort_by block.
The output of the controller method should look like this after I iterate over contacts to build a custom data structure, using this line of code:
#contacts = Hash[contacts.map { |contact| [contact.id, ContactSerializer.new(contact)] }]
I've already benchmarked that last line of code so I know that it's not a major source of slowdown. More on that here.
{
contacts: {
79: {
id: 79,
first_name: "Foo",
last_name: "Bar",
email: "t#t.co",
engagement: "0%",
company_id: 94,
created_at: " 9:41AM Jan 30, 2016",
updated_at: "10:57AM Feb 23, 2016",
published_response_count: 0,
groups: {
test: true,
test23: false,
Test222: false,
Last: false
},
stories_requested: 1,
stories_submitted: 0,
stories_published: 0,
amplify_requested: 1,
amplify_completed: 1,
photos_uploaded: 0,
invites: [
{
id: 112,
email: "t#t.co",
status: "Requested",
created_at: "Jan 30, 2016, 8:48 PM",
date_submitted: null,
response: null
}
],
responses: [ ],
promotions: [
{
id: 26,
company_id: 94,
key: "e5cb3bc80b58c29df8a61231d0",
updated_at: "Feb 11, 2016, 2:45 PM",
read: null,
social_media_posts: [ ]
}
]
}
}
}
if params[:order_by] == 'stories_submitted'
contact_ids = company.contact_ids
# count all invites that have the relevant contact ids
invites=Invite.where(contact_id:contact_ids).group('contact_id').count
invites_contact_ids = invites.map(&:first)
# Add contacts with 0 invites
contact_ids.each{|c| invites.push([c, 0]) unless invites_contact_ids.include?(c)}
# Sort all invites by id (add .reverse to the end of this for sort DESC)
contact_id_counts=invites.sort_by{|r| r.last}.map(&:first)
# The [0, 10] limits you to the lowest 10 results
contacts=Contact.where(id: contact_id_counts[0, 10])
contacts.sort_by!{|c| contact_id_counts.index(c.id)}
end

getting attributes from a hash/difference between '.first' and '[i]'

I have the following class methods for goal
def evals
self.evaluations.order("eval_number").group_by(&:student_id)
end
def evals_for(student, i)
#evals = []
self.evals.values.each do |eval|
#evals << eval.keep_if { |e| e.student_id == student.id }
end
#evals = #evals.reject { |array| array.empty? }.first
#evals[i]
end
in the view, i'm calling the second method like this:
<% #student.student_group.eval_count.times do |i| %>
<td><%= goal.evals_for(#student, i) %></td>
<% end %>
which returns
#<Evaluation:x>, #<Evaluation:y>, #<Evaluation:z>
if i change the last line of the class method to call #evals[i].inspect, i can see the data inside each hash, like so:
#<Evaluation id: 1949, score: 3, created_at: "2013-08-28 09:44:32", updated_at: "2013-08-28 09:44:32", student_id: 32, goal_id: 63, eval_number: 29>
I want to get the score, but when I call #evals[i].score on the last line in the class method, i get an error - undefined method 'score' for nil:NilClass
I know I can't call class methods on hashes, but is there a way to pull out just that data? As a sub-question, I'm a bit confused about the difference between .first and [i] as calling #evals.first.score returns "3" - only I can't use first as I need to be able to access each instance of evaluation in turn.
This is not a simple Hash:
#<Evaluation id: 1949, score: 3, created_at: "2013-08-28 09:44:32", updated_at: "2013-08-28 09:44:32", student_id: 32, goal_id: 63, eval_number: 29>
It's an instance of Evaluation class so you should be able to call score on it. I think your issue is due to the fact that you are trying to call score on nil
You'll probably want to make sure that you have an instance object before calling the method:
#evals[i].score if #evals[i]

Resources