ActiveModel::Serializer has_many hash of objects - ruby-on-rails

I've got an ActiveModel::Serializer question for the experts. Lets say I have the following JSON output where the root element is a SurveyInvite object. Currently the question_answers hash key is just an array of QuestionAnswer objects. How can I make it so that question_answers is a hash of QuestionAnswer objects where the keys are QuestionAnswer.question.id?
{
id: 1,
email: "foo#example.com",
status: "Submitted",
created_at: "10:57AM Sep 1, 2015",
date_submitted: "10:58AM Sep 1, 2015",
survey_response: {
id: 1,
survey_invite_id: 1,
name: "Foo Bar",
title: "Ninja",
published: true,
created_at: "10:58AM Sep 1, 2015",
updated_at: " 3:42PM Sep 2, 2015",
question_answers: [
{
id: 1,
survey_response_id: 1,
mini_post_question_id: 20,
answer: "What is the answer?",
created_at: "2015-09-14T14:59:39.599Z",
updated_at: "2015-09-14T14:59:39.599Z"
},
{
id: 2,
survey_response_id: 1,
mini_post_question_id: 27,
answer: "What is the answer?",
created_at: "2015-09-15T20:58:32.030Z",
updated_at: "2015-09-15T20:58:32.030Z"
}
]
}
}
Here is my SurveyResponseSerializer:
class SurveyResponseSerializer < ActiveModel::Serializer
attributes :id, :survey_invite_id, :name, :title, :published, :created_at, :updated_at
has_many :question_answers
def created_at
object.created_at.in_time_zone("Eastern Time (US & Canada)").strftime("%l:%M%p %b %w, %Y")
end
def updated_at
object.updated_at.in_time_zone("Eastern Time (US & Canada)").strftime("%l:%M%p %b %w, %Y")
end
end
Basically, I want the question_answers key value to be a hash of QuestionAnswer objects where the keys are the question id QuestionAnswer.question_id. I've looked through the docs and haven't turned up any examples of what I'm trying to do.
Update with solution:
So I came up with a solution that works for what I need, but I'd still like to know if there is a better way to do what I need. I wrote a method to generate the structure I need.
def question_answers
hash = {}
object.question_answers.each do |answer|
hash[answer.mini_post_question_id] = answer
end
hash
end
That yields the following:
question_answers: {
20: {
id: 1,
survey_response_id: 1,
mini_post_question_id: 20,
answer: "Test?",
created_at: "2015-09-14T14:59:39.599Z",
updated_at: "2015-09-14T14:59:39.599Z"
},
27: {
id: 2,
survey_response_id: 1,
mini_post_question_id: 27,
answer: "Blarg!",
created_at: "2015-09-15T20:58:32.030Z",
updated_at: "2015-09-15T20:58:32.030Z"
}
}

I don't think ActiveModelSerializers has an idiomatic way to present a has_many association as a hash, but you can do it with a one-liner:
def question_answers
object.question_answers.index_by(&:id)
end

Related

Rails 4 combining output from models

I am a beginner, so this question maybe very elementary. I appreciate any help. I have three models - one Metal that does a query of a database to get a price and another QuoteMetal which is nested through has_many and accepts_nested_attributes_for in Lead. I need the results of the query conducted through the Metal model to be available in the lead collection so I can display the results in the view.
My question: how do I get those query results (specifically the price attribute) to be in the Lead instance variable. Of if not the lead instance variable - what is the best way to display the results. Below is my code.
class LeadsController < ApplicationController
def index
#lead = Lead.new
#quote_metal = #lead.quote_metals.build
#quote_melee = #lead.quote_melees.build
#quote_diamond = #lead.quote_diamonds.build
end
def create
#raise params.inspect
#lead = Lead.new(lead_params)
#qm = #lead.quote_metals.map { |f| Metal.calculate_metal(f).first}
##lead.quote_metals.each do |f|
# f[:price] = #qm[:price].to_i
#end
#lead = Lead.create!(lead_params)
end
def show
end
private
def lead_params
params.require(:lead).permit([:name, :email, :phone, :zip, :note, :method,
quote_metals_attributes: [:id, :metal, :weight, :unit, :price],
quote_melees_attributes: [:shape, :quality, :quantity, :totalweight, :size],
quote_diamonds_attributes: [:shape, :size, :color, :clarity]
])
end
end
Metal Model:
class Metal < ActiveRecord::Base
enum metal: {platinum: 1, palladium: 2, "10_karat_gold": 3, "14_karat_gold": 4,
"18_karat_gold": 5, "22_karat_gold": 6, silver: 7}
enum unit: {pennyweight: 1, grams: 2, ounce: 3, pound: 4}
attr_accessor :weight
#Validations
validates :metal, :unit, :weight, presence: true
scope :calculate_metal, ->(quote_metal) {
where(metal: QuoteMetal.metals[quote_metal.metal],
unit: QuoteMetal.units[quote_metal.unit])
}
scope :multiple, ->(quote_metal){
}
end
Create.html.erb
<pre>
<%= #qm.inspect %>
<br>
<%= #lead.quote_metals.inspect %>
<br>
<%= #qm.inspect %>
<br>
<%= #qm2.inspect %>
</pre>
Output:
[#<Metal id: 11, metal: 2, unit: 2, price: #<BigDecimal:7ff01dac4be8,'0.1367E2',18(18)>, created_at: "2015-05-26 10:36:38", updated_at: nil, weight: nil>, #<Metal id: 11, metal: 2, unit: 2, price: #<BigDecimal:7ff01da96450,'0.1367E2',18(18)>, created_at: "2015-05-26 10:36:38", updated_at: nil, weight: nil>]
#<ActiveRecord::Associations::CollectionProxy [#<QuoteMetal id: 231, lead_id: 176, metal: 2, unit: 2, price: nil, weight: #<BigDecimal:7ff01cc71320,'0.1E1',9(18)>, quote: nil, created_at: "2015-07-14 18:26:46", updated_at: "2015-07-14 18:26:46">, #<QuoteMetal id: 232, lead_id: 176, metal: 2, unit: 2, price: nil, weight: #<BigDecimal:7ff01ca49d90,'0.2E1',9(18)>, quote: nil, created_at: "2015-07-14 18:26:46", updated_at: "2015-07-14 18:26:46">]>
[#<Metal id: 11, metal: 2, unit: 2, price: #<BigDecimal:7ff01dac4be8,'0.1367E2',18(18)>, created_at: "2015-05-26 10:36:38", updated_at: nil, weight: nil>, #<Metal id: 11, metal: 2, unit: 2, price: #<BigDecimal:7ff01da96450,'0.1367E2',18(18)>, created_at: "2015-05-26 10:36:38", updated_at: nil, weight: nil>]
[[#<Lead id: nil, name:..., note: "asdf", created_at: nil, updated_at: nil>, #<Metal id: 11, metal: 2, unit: 2, price: #<BigDecimal:7ff01dac4be8,'0.1367E2',18(18)>, created_at: "2015-05-26 10:36:38", updated_at: nil, weight: nil>]]
Thank you.

Validates_Overlap Gem Multiple Scopes Overwriting Eachother

I'm using Validates_Overlap Gem which can be found here: https://github.com/robinbortlik/validates_overlap
The essence is I have two rooms that can be booked. I want the validation to step in when the same room already has a CONFIRMED booking in the SAME room. It shouldn't throw me an error when the other room is booked, or if the same room is booked but hasn't been confirmed.
My code so far is as follows
validates :start_time, :end_time,
:overlap => {
:exclude_edges => ["starts_at", "ends_at"],
:scope => { "bookings.studio_id" => proc {|booking| booking.studio_id}} && { "bookings.is_confirmed" => proc {|booking| booking.is_confirmed == true}}
}, on: :update
This returns the following from my server:
Booking Exists (0.4ms) SELECT 1 AS one FROM "bookings" WHERE ((bookings.end_time IS NULL OR bookings.end_time >= '2014-10-23 20:00:00.000000') AND (bookings.start_time IS NULL OR bookings.start_time <= '2014-10-24 03:00:00.000000') AND bookings.id != 9 AND bookings.is_confirmed = 't') LIMIT 1
There are two other bookings (with this studio_id) and none of them are confirmed. What gives?
Here are all the bookings with :studio_id => 2
[#<Booking id: 1, studio_id: 2, engineer_id: 5, is_confirmed: false, title: "", allDay: false, created_at: "2014-10-23 19:59:01", updated_at: "2014-10-23 19:59:01", start_time: "2014-10-23 19:00:00", end_time: "2014-10-23 21:00:00", user_id: nil, booker: "Client", client_id: 3>,
#<Booking id: 8, studio_id: 2, engineer_id: 1, is_confirmed: false, title: "", allDay: false, created_at: "2014-10-24 03:07:34", updated_at: "2014-10-24 03:07:34", start_time: "2014-10-23 19:00:00", end_time: "2014-10-23 22:00:00", user_id: nil, booker: "Pat Sullivan", client_id: nil>,
#<Booking id: 9, studio_id: 2, engineer_id: 2, is_confirmed: false, title: "", allDay: false, created_at: "2014-10-24 03:26:17", updated_at: "2014-10-24 03:26:17", start_time: "2014-10-23 20:00:00", end_time: "2014-10-24 03:00:00", user_id: nil, booker: "Client", client_id: 4>]
Update I noticed that the studio_id isn't being noticed with the && in the scope line. How can I have the scope register both? Can I do it within the scope line or should I create a method?
I've also tried a simpler
validates :start_time, :end_time,
:overlap => {
:exclude_edges => ["starts_at", "ends_at"],
:scope => "is_confirmed" && "studio_id"
}, on: :update
This does the same thing -- only uses the later "studio_id"
I know, that the names of options are confusing and I'm sorry for that.
I suggest you to implement your named scope called :confirmed and pass it as :query_option parameter.
I think, it should look like this:
class Booking < ActiveRecord::Base
scope :confirmed_scope, -> {confirmed: true}
validates :start_time, :end_time, :overlap => {
:exclude_edges => ["starts_at", "ends_at"],
:scope => "studio_id",
:query_options => {:confirmed_scope => nil}
}, on: :update
end
BTW... be careful if you are using Rails 4.1, there is a change https://github.com/robinbortlik/validates_overlap#rails-41-update
Short explanation: what you pass as a :scope option, this behave like attribute. But you can extend it by :query_options. What is inside query options will be called in the query chain. So internally it will be called like this:
Booking.confirmed_scope.where("starts_at > 18-02-2014 AND ends_at < 20-02-2014 AND studio_id = 1")
Is it more clear now?

Grouping by attribute of associated objects

class Event < ActiveRecord::Base
has_many :meetings
end
class Meeting < ActiveRecord::Base
belongs_to :event
end
How to write mysql query to search all events group_by meeting DATE(start_at)?
Event.inludes(:meetings).group ...
As a result I want to get a Hash:
{"2014-01-24"=>[#<Event id: , title: "First", created_at: "2014-01-24 16:02:52", updated_at: "2014-01-24 16:02:52">, #<Event id: 2, title: "Second", created_at: "2014-01-24 16:02:52", updated_at: "2014-01-24 16:02:52">], "2013-01-29"=>[#<Event id: 3, title: "Third", created_at: "2013-01-29 05:30:40", updated_at: "2014-01-29 05:30:40">], ...]}
P.S: I am using PostgreSQL
Now I get it by this way:
hash = {}
Meeting.where("extract(month from start_at) = ?", Date.today.month).pluck('DATE(start_at)').uniq.each do |date|
hash[date] = Event.includes(:meetings).where("DATE(meetings.start_at) = ?", date).references(:meetings)
end
But it produced so many queries to the database :(
Event.joins(:meetings).group('meetings.start_at') should do. But want you want is a group_by array method http://apidock.com/ruby/Enumerable/group_by so what you should do is
#events.group_by {|e| e.meeting.start_date}
In case of many to many you should be better off with
result = Hash.new
Meeting.include(:events).each {|m| result[m.start_at]||=[]; result[m.start_at] << m.events}
and with one liner you could
Meeting.includes(:events).inject(Hash.new) do |result, m|
result[m.start_at]||=[]
result[m.start_at] << w.events
result
end
This code should execute two database calls i think

Why isn't Rails deleting dependent objects?

I have...
/app/models/input.rb:
class Input < ActiveRecord::Base
has_many :questions, :dependent => :destroy
after_commit :create_matching_questions
def create_matching_questions
#element_id = Element.all.select{|e| e.meta == true}.first.id
#standard_id = Standard.all.select{|s| s.meta == true}.first.id
#description = ["Does the site stock ", self.name, "?"].join
Product.all.each do |product|
#question = product.questions.find_or_create_by_element_id_and_standard_id_and_description!(#element_id, #standard_id, #description)
self.questions << #question
#question.fields.find_or_create_by_name("The site sells this product and it is in stock")
#question.fields.find_or_create_by_name("The site sells this product but it is not in stock")
#question.fields.find_or_create_by_name("The site does not sell this product")
#question.update_attributes :active => true
end
return true
end
end
/app/models/question.rb:
class Question < ActiveRecord::Base
belongs_to :input
after_commit :create_matching_surveys
def create_matching_surveys
if self.active == true
self.reload.product.reviews.each do |review|
review.competitors.each do |competitor|
(1..self.iterations).each do |iteration|
survey = competitor.surveys.find_or_create_by_question_id_and_iteration!(self.id, iteration)
survey.save
end
end
end
return true
else
self.destroy_matching_surveys
end
end
def destroy_matching_surveys
self.surveys.each do |survey|
survey.destroy if survey.question_id == self.id
end
return true
end
end
Why, then, do I get...
> #finance = Good.create! :name => "Finance"
=> #<Good id: 6, name: "Finance", created_at: "2013-06-13 02:56:20", updated_at: "2013-06-13 02:56:20">
> #super = Input.create! :name => "Superannuation"
=> #<Input id: 11, name: "Superannuation", mispelling: nil, typo: nil, created_at: "2013-06-13 02:56:28", updated_at: "2013-06-13 02:56:28">
> #first = #super.questions.first
=> #<Question id: 48, standard_id: 1, description: "Does the site stock Superannuation?", element_id: 2, condition_id: nil, blueprint_name: nil, blueprint_url: nil, additive: false, instructions: nil, created_at: "2013-06-13 02:56:41", updated_at: "2013-06-13 02:56:41", active: false, postscript: "<p>If you have any comments about this question or ...", iterations: 1, product_id: 1, precondition_id: nil, input_id: 11>
> #last = #super.questions.last
=> #<Question id: 60, standard_id: 1, description: "Does the site stock Superannuation?", element_id: 2, condition_id: nil, blueprint_name: nil, blueprint_url: nil, additive: false, instructions: nil, created_at: "2013-06-13 02:56:43", updated_at: "2013-06-13 02:56:43", active: false, postscript: "<p>If you have any comments about this question or ...", iterations: 1, product_id: 23, precondition_id: nil, input_id: 11>
> #super.destroy
=> #<Input id: 11, name: "Superannuation", mispelling: nil, typo: nil, created_at: "2013-06-13 02:56:28", updated_at: "2013-06-13 02:56:28">
> #super.destroyed?
=> true
> #first.destroyed?
=> false
> #last.destroyed?
=> false
Surely #first and #last should be destroyed automatically?
I had the same problem, solved it by :dependent => :delete_all instead of :dependent => :destroy.
:delete_all doesn't call the destroy method from your controller and delete data directly from your database.

entry returned on first query, empty on second

I have an application which allows lawyers and law students to answer legal questions. Their answers can be voted up. Beside each answer on the views/question/show.html.erb, the application indicates whether an answer has been voted up and by who (a lawyer, or a law student). However, it's behaving very oddly. Currently, on a test question, if a lawyer votes up an answer, the application is not showing the upvote, but if a student votes up an answer, then both the student's and the lawyer's vote will be displayed, but both are displayed as student votes.
This is the code in the show action of the Questions controller that retrieves all the answers for a question, and then queries for the type of votes each answer has.
def show
#question = Question.find(params[:id])
#answers = #question.answers
#answers.each do |a|
#lawyervotes = AnswerVote.where({:answer_id => a.id, :lawyervote => true}).reload
puts #lawyervotes.inspect
puts "lawyervotes"
#studentvotes = AnswerVote.where({:answer_id => a.id, :studentvote => true}).reload
#uservotes = AnswerVote.where({:answer_id => a.id, :lawyervote => nil, :studentvote => nil}).reload
end
end
If I look in the console for the puts statements, it shows that #lawyervotes contains one result, but then it's suddenly an empty array. Currently, there are two answers for this question, which is why the puts statement is run twice, but I don't know why it's empty on the second time through
[#<AnswerVote id: 34, value: 3, answer_id: 54, user_id: 37, created_at: "2013-05-08 18:29:34", updated_at: "2013-05-08 18:29:34", lawyervote: true, studentvote: nil>]
lawyervotes
[]
lawyervotes
Note, the reason why I put reload on the end of each query was to avoid an ActiveRecord::AssociationTypeMismatch error I was getting, which according to another SO answer I found can happen when you query with 'where.' I found another SO answer that said putting 'reload' on the end of a where query can help avoid that error.
Can you explain why this odd behavior might be happening with my lawyervotes and student votes and possibly tell me how to rewrite the show action to avoid it. Thank you in advance.
Update
This is the console record showing that question 62 has two answers, each with one answer_vote. One of the answer votes was by a lawyer (lawyer = true) while one was by a student (student = true), however, they're both showing up as student votes in my application, even after trying dmitry's solution.
>> q = Question.find_by_id(62)
Question Load (0.2ms) SELECT "questions".* FROM "questions" WHERE "questions"."id" = 62 LIMIT 1
=> #<Question id: 62, details: "I have a terminal illness but don't have time to go...", question: "What happens if I die without a will?", user_id: 35, accepted_answer_id: nil, created_at: "2013-05-08 18:19:48", updated_at: "2013-05-08 18:19:48", city: "Toronto", province: nil, province_id: 6>
>> q.answers
Answer Load (0.2ms) SELECT "answers".* FROM "answers" WHERE "answers"."question_id" = 62
=> [#<Answer id: 54, content: "There is legislation that determines the rules of i...", accepted: nil, user_id: 50, question_id: 62, created_at: "2013-05-08 18:20:41", updated_at: "2013-05-08 18:20:41">, #<Answer id: 55, content: "Ontario has statutory provisions that detail who in...", accepted: nil, user_id: 37, question_id: 62, created_at: "2013-05-08 18:22:53", updated_at: "2013-05-08 18:22:53">]
>> a54 = Answer.find_by_id(54)
Answer Load (0.3ms) SELECT "answers".* FROM "answers" WHERE "answers"."id" = 54 LIMIT 1
=> #<Answer id: 54, content: "There is legislation that determines the rules of i...", accepted: nil, user_id: 50, question_id: 62, created_at: "2013-05-08 18:20:41", updated_at: "2013-05-08 18:20:41">
>> a54.answer_votes
AnswerVote Load (0.2ms) SELECT "answer_votes".* FROM "answer_votes" WHERE "answer_votes"."answer_id" = 54
=> [#<AnswerVote id: 34, value: 3, answer_id: 54, user_id: 37, created_at: "2013-05-08 18:29:34", updated_at: "2013-05-08 18:29:34", lawyervote: true, studentvote: nil>]
>> a55 = Answer.find_by_id(55)
Answer Load (0.3ms) SELECT "answers".* FROM "answers" WHERE "answers"."id" = 55 LIMIT 1
=> #<Answer id: 55, content: "Ontario has statutory provisions that detail who in...", accepted: nil, user_id: 37, question_id: 62, created_at: "2013-05-08 18:22:53", updated_at: "2013-05-08 18:22:53">
>> a55.answer_votes
AnswerVote Load (0.3ms) SELECT "answer_votes".* FROM "answer_votes" WHERE "answer_votes"."answer_id" = 55
=> [#<AnswerVote id: 35, value: 3, answer_id: 55, user_id: 50, created_at: "2013-05-08 18:37:32", updated_at: "2013-05-08 18:37:32", lawyervote: nil, studentvote: true>]
Update
I put this code in the loop
puts AnswerVote.where({:answer_id => a.id}).reload.inspect
puts "inspectinganswervote"
and got this result
[#<AnswerVote id: 34, value: 3, answer_id: 54, user_id: 37, created_at: "2013-05-08 18:29:34", updated_at: "2013-05-08 18:29:34", lawyervote: true, studentvote: nil>]
inspectinganswervote
[#<AnswerVote id: 35, value: 3, answer_id: 55, user_id: 50, created_at: "2013-05-08 18:37:32", updated_at: "2013-05-08 18:37:32", lawyervote: nil, studentvote: true>]
inspectinganswervote
Update
Answer.rb
class Answer < ActiveRecord::Base
attr_accessible :accepted, :content, :question_id, :user_id
has_many :comments
belongs_to :question
belongs_to :user
has_many :answer_votes
has_and_belongs_to_many :watchers, :join_table => "answer_watchers", :class_name => "User"
has_reputation :votes, source: :user, aggregated_by: :sum
has_reputation :lawyervotes, source: :user, aggregated_by: :sum
has_reputation :studentvotes, source: :user, aggregated_by: :sum
has_reputation :best, source: :user, aggregated_by: :sum
#
def add_to_watchers(user)
self.watchers << user unless self.watchers.include?(user)
end
after_create :creator_watches_me
private
def creator_watches_me
self.watchers << user unless self.watchers.include?(user)
end
end
AnswerVote.rb
class AnswerVote < ActiveRecord::Base
attr_accessible :answer_id, :user_id, :value, :answer, :lawyervote, :studentvote
belongs_to :answer
belongs_to :user
validates_uniqueness_of :answer_id, scope: :user_id
validates_inclusion_of :value, in: [1,-1,10,-10, 3]
validate :ensure_not_author
scope :lawyers, where(lawyervote: true)
scope :students, where(studentvote: true)
def ensure_not_author
errors.add :user_id, "is the author of the answer" if answer.user_id == user_id
end
end
One of the problems -- you rewrite your #lawyervotes array during the next iteration. One of the ways out would be to append it instead (using something like:
#lawyervotes = []
#answers.each do |a|
#lawyervotes <<= AnswerVote.where({:answer_id => a.id, :lawyervote => true}).reload
...
end
But it is super-terrible, non-Rails style. As I mentioned above, you do not need this iteration through #answers, you simply write:
UPDATED
#lawyervotes = #question.answers.map {|a| a.answer_votes.lawyers}.reject!(&:empty?).flatten
#studentvotes = #question.answers.map {|a| a.answer_votes.students}.reject!(&:empty?).flatten
And in you AnswerVotes model:
scope :lawyers, where(lawyervote: true)
scope :students, where(studentvote: true)
You are getting the lawyervotes array as empty for second answer as the second answer has only one AnswerVote with laywervote = nil and studentvote = true :) So the vote is present in the #studentvotes variable.
If you inspect your #studentvotes as well, you will see your second vote printed in the second iteration of the loop.

Resources