Ruby Review class creation - ruby-on-rails

The Review class you see below represent a review that a user submitted for a product. Somewhere else in the code, Review.recent is called with a product_id, which is just a unique number that represents a single product. Fill in the code to make it work as expected!
Review.recent - This function should return the 5 most recent reviews (sorted by submit_time) with the specified product_id.
<=> - This special Ruby function is called when comparing two objects for sorting. It returns 1, 0, or +1 depending on whether the object is less than, equal to, or greater than the other object. You'll want to sort items by submit_time so recent items appear first.
set_submit_time - This function is called right before a review is created. We can use Ruby's Time class to set submit_time to now so we know when the review was created.
I'm new to ruby and I want this code for my very important work so how can I complete it help me please!
class Review < ApplicationRecord
# Every Review has a product_id and submit time
attr_accessor :product_id
attr_accessor :submit_time
# Before the new record is created, we'll call the :set_submit_time method
before_create :set_submit_time
def self.recent(product_id)
# Return only the 5 newest results for this product
# Reference: https://ruby-doc.org/core-2.4.2/Enumerable.html
Review.all
end
def <=>(other_review)
# Implement the comparison function for sorting
# Reference: http://ruby-doc.org/core-2.4.2/Comparable.html
end
private
def set_submit_time
# Set the submit_time
# Reference: https://ruby-doc.org/core-2.2.4/Time.html
end
end

self.recent
This is asking you to order by submit_time and return the first 5 results.
To perform the ordering, see: https://apidock.com/rails/ActiveRecord/QueryMethods/order
To perform the limit, see: https://apidock.com/rails/ActiveRecord/QueryMethods/limit
If you're still stuck on this problem, please show us what you've tried.
<=>
If you click the link in the comment you provided (http://ruby-doc.org/core-2.4.2/Comparable.html), the solution is almost identical to that example.
If you're still stuck on this problem, please show us what you've tried.
set_submit_time
It's worth having a quick look at: https://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html - to understand what is meant by a callback. Basically, this method is going to get automatically called whenever a new record is created. (You probably could have guessed this, based on the fairly self-explanatory name: before_create!)
Again, the first example on that page is almost identical to your scenario. You can use Time.now to get the current time.
If you're still stuck on this problem, please show us what you've tried.

Related

How to set parent attribute based on child attributes

I have a situation where a parent Order has multiple child Items. Both Order and Item have a status_id column. I want the user to update the status_id of the Item, and then when all of the Items have a status_id, then the Order's status_id should be auto-set (to some value based on what Item status_ids are).
The code that I currently have is this:
class Item
after_save :set_order_status_id
def set_order_status_id
if self.order.items.where(status_id:nil).blank?
self.order.update_attributes(status_id:X)
end
end
end
Ths is pretty smelly code because it violates SRP, uses a callback, and is pretty inefficient, considering that this means if an Order has 5 Items and all 5 Items are being updated, after EACH Item update, the set_order_status_id method is called, and a database query is run.
So... is there a better way of writing this to avoid these issues? Particularly I'm interested in removing the inefficiency with constantly checking the parent Order's other child Items' statuses... because again if an all 5 Items is updated at once, it's silly to check after each and every update when it should just wait until the 5th update.... Does Rails have a magical way of doing this?
The answer is no, there is no magic way to do that using the framework. Your best option is to use a customised solution and run your check only after your items update. Something like:
...your code...
order.items.update_all(whatever) <-- update items
update_status(order) <-- update order status
...
def update_status(order)
return if order.items.where(status_id: nil).exist? <-- more efficient
update_attributes(status_id: X)
end
the method could also be in the Offer model for simplicity.
If the order status can be derived from the item status at any point in time, it may be better to avoid setting it in the database entirely. You can instead create an accessor to query it on-demand:
class Order < ActiveRecord::Base
def status
# memoize the calculation, including nils
return #status if defined? #status
item_statuses = items.pluck(:status).uniq
# your logic here:
# 1. check for any nils
# 2. check for any 'pending', etc.
#status = 'pending'
end
end
Whether this alternate solution fits your needs depends on your database read patterns.

Rails associated models with a method of the same name

I'm working with a massive legacy code base, so I am looking for advice concerning this particular issue, please, not suggestions of better high-level implementations.
A simplified version of what I'm working with:
class Order < ActiveRecord::Base
has_many :line_items
#other stuff
def balance
#some definition
end
end
class LineItem < ActiveRecord::Base
belongs_to :order
#other stuff
end
module Concerns
module LineItems
module Aggregates
extend ActiveSupport::Concern
#stuff
def balance
#some other definition
end
end
end
end
Order has a method called 'balance,' and a module of LineItem also has a method called 'balance.' It seems that most of the time (in most places in the code base), when specific_line_item.balance is called, it used the method definition under the LineItem module, but there are a couple of places where it instead calls the method from Order.
Is there any way in Ruby/Rails to specify on method call which of these two I'd like to use? OR is there probably something else going on here because Ruby doesn't have method overloading, so the problem I'm describing here isn't possible?
All relevant cases where either method is called are coming from a line_item (i.e. specific_line_item.balance), so I would think it would always choose the method closer to home, rather than making the associative jump and calling Order's 'balance' method without being told to.
EDIT:
Thanks for the responses! It seems I wasn't clear enough with my question. I understand the difference between
Order.first.balance
and
LineItem.first.balance
and that the balance method being called is the one defined within the class for that object. In the situation I'm describing, I observed, in the actual live app environment, that at a place in the code where
LineItem.find(some_id).balance
was called it output not the result that would be computed by the LineItem 'balance' method, but the one from the Order class.
So I had hoped to learn that there's some ruby quirk that might have an object call an associate's method of the same name under some conditions, rather than it's own. But I'm thinking that's not possible, so there's probably something else going on under the covers specific to this situation.
Firstly, ActiveRecord::Concern can change a lot of behaviour and you've left out a lot of code, most crucially, I don't know where it's being injected, but I can make an educated guess.
For a Concern's methods to be available a given object, it must be include'd in the object's class's body.
If you have access to an instance of the Order object, at any point you can call the balance method:
order = Orders.last # grab the last order in your database
order.balance # this will call Order#balance
And if you have the Order then you can also get the LineItem:
order.line_items.first.balance # should call the Concerns:: LineItems::Aggregates#balance
You can open up a Rails console (with rails console) and run the above code to see if it works as you expect. You'll need a working database to get meaningful orders and balances, and you might need to poke around to find a completed order, but Ruby is all about exploration and a REPL is the place to go.
I'd also grep (or ag or ack) the codebase looking for calls to balance maybe doing something like grep -r "(^|\s)\w+\.balance" *, what you want to look for is the word before .balance, that is the receiver of the "balance" message, if that receiver is an Order object then it will call Order#balance and if it is a LineItem object then it will call Concerns:: LineItems::Aggregates#balance instead.
I get the feeling you're not familiar with Ruby's paradigm, and if that's the case then an example might help.
Let's define two simple Ruby objects:
class Doorman
def greet
puts "Good day to you sir!"
end
end
class Bartender
def greet
puts "What are you drinking?"
end
end
Doorman and Bartender both have a greet method, and which is called depends on the object we call greet on.
# Here we instantiate one of each
a_doorman = Doorman.new
a_bartender = Bartender.new
a_doorman.greet # outputs "Good day to you sir!"
a_bartender.greet # outputs "What are you drinking?"
We're still using a method called greet but the receiver is what determines which is called.
Ruby is a "message passing language" and each "method" is not a function but it's a message that is passed to an object and handled by that object.
References
How to use concerns in Rails 4
http://api.rubyonrails.org/classes/ActiveSupport/Concern.html
http://guides.rubyonrails.org/command_line.html#rails-console

Ruby on Rails - ActiveRecord::Relation count method is wrong?

I'm writing an application that allows users to send one another messages about an 'offer'.
I thought I'd save myself some work and use the Mailboxer gem.
I'm following a test driven development approach with RSpec. I'm writing a test that should ensure that only one Conversation is allowed per offer. An offer belongs_to two different users (the user that made the offer, and the user that received the offer).
Here is my failing test:
describe "after a message is sent to the same user twice" do
before do
2.times { sending_user.message_user_regarding_offer! offer, receiving_user, random_string }
end
specify { sending_user.mailbox.conversations.count.should == 1 }
end
So before the test runs a user sending_user sends a message to the receiving_user twice. The message_user_regarding_offer! looks like this:
def message_user_regarding_offer! offer, receiver, body
conversation = offer.conversation
if conversation.nil?
self.send_message(receiver, body, offer.conversation_subject)
else
self.reply_to_conversation(conversation, body)
# I put a binding.pry here to examine in console
end
offer.create_activity key: PublicActivityKeys.message_received, owner: self, recipient: receiver
end
On the first iteration in the test (when the first message is sent) the conversation variable is nil therefore a message is sent and a conversation is created between the two users.
On the second iteration the conversation created in the first iteration is returned and the user replies to that conversation, but a new conversation isn't created.
This all works, but the test fails and I cannot understand why!
When I place a pry binding in the code in the location specified above I can examine what is going on... now riddle me this:
self.mailbox.conversations[0] returns a Conversation instance
self.mailbox.conversations[1] returns nil
self.mailbox.conversations clearly shows a collection containing ONE object.
self.mailbox.conversations.count returns 2?!
What is going on there? the count method is incorrect and my test is failing...
What am I missing? Or is this a bug?!
EDIT
offer.conversation looks like this:
def conversation
Conversation.where({subject: conversation_subject}).last
end
and offer.conversation_subject:
def conversation_subject
"offer-#{self.id}"
end
EDIT 2 - Showing the first and second iteration in pry
Also...
Conversation.all.count returns 1!
and:
Conversation.all == self.mailbox.conversations returns true
and
Conversation.all.count == self.mailbox.conversations.count returns false
How can that be if the arrays are equal? I don't know what's going on here, blown hours on this now. Think it's a bug?!
EDIT 3
From the source of the Mailboxer gem...
def conversations(options = {})
conv = Conversation.participant(#messageable)
if options[:mailbox_type].present?
case options[:mailbox_type]
when 'inbox'
conv = Conversation.inbox(#messageable)
when 'sentbox'
conv = Conversation.sentbox(#messageable)
when 'trash'
conv = Conversation.trash(#messageable)
when 'not_trash'
conv = Conversation.not_trash(#messageable)
end
end
if (options.has_key?(:read) && options[:read]==false) || (options.has_key?(:unread) && options[:unread]==true)
conv = conv.unread(#messageable)
end
conv
end
The reply_to_convesation code is available here -> http://rubydoc.info/gems/mailboxer/frames.
Just can't see what I'm doing wrong! Might rework my tests to get around this. Or ditch the gem and write my own.
see this Rails 3: Difference between Relation.count and Relation.all.count
In short Rails ignores the select columns (if more than one) when you apply count to the query. This is because
SQL's COUNT allows only one or less columns as parameters.
From Mailbox code
scope :participant, lambda {|participant|
select('DISTINCT conversations.*').
where('notifications.type'=> Message.name).
order("conversations.updated_at DESC").
joins(:receipts).merge(Receipt.recipient(participant))
}
self.mailbox.conversations.count ignores the select('DISTINCT conversations.*') and counts the join table with receipts, essentially counting number of receipts with duplicate conversations in it.
On the other hand, self.mailbox.conversations.all.count first gets the records applying the select, which gets unique conversations and then counts it.
self.mailbox.conversations.all == self.mailbox.conversations since both of them query the db with the select.
To solve your problem you can use sending_user.mailbox.conversations.all.count or sending_user.mailbox.conversations.group('conversations.id').length
I have tended to use the size method in my code. As per the ActiveRecord code, size will use a cached count if available and also returns the correct number when models have been created through relations and have not yet been saved.
# File activerecord/lib/active_record/relation.rb, line 228
def size
loaded? ? #records.length : count
end
There is a blog on this here.
In Ruby, #length and #size are synonyms and both do the same thing: they tell you how many elements are in an array or hash. Technically #length is the method and #size is an alias to it.
In ActiveRecord, there are several ways to find out how many records are in an association, and there are some subtle differences in how they work.
post.comments.count - Determine the number of elements with an SQL COUNT query. You can also specify conditions to count only a subset of the associated elements (e.g. :conditions => {:author_name => "josh"}). If you set up a counter cache on the association, #count will return that cached value instead of executing a new query.
post.comments.length - This always loads the contents of the association into memory, then returns the number of elements loaded. Note that this won't force an update if the association had been previously loaded and then new comments were created through another way (e.g. Comment.create(...) instead of post.comments.create(...)).
post.comments.size - This works as a combination of the two previous options. If the collection has already been loaded, it will return its length just like calling #length. If it hasn't been loaded yet, it's like calling #count.
It is also worth mentioning to be careful if you are not creating models through associations, as the related model will not necessarily have those instances in its association proxy/collection.
# do this
mailbox.conversations.build(attrs)
# or this
mailbox.conversations << Conversation.new(attrs)
# or this
mailbox.conversations.create(attrs)
# or this
mailbox.conversations.create!(attrs)
# NOT this
Conversation.new(mailbox_id: some_id, ....)
I don't know if this explains what's going on, but the ActiveRecord count method queries the database for the number of records stored. The length of the Relation could be different, as discussed in http://archive.railsforum.com/viewtopic.php?id=6255, although in that example, the number of records in the database was less than the number of items in the Rails data structure.
Try
self.mailbox.conversations.reload; self.mailbox.conversations.count
or perhaps
self.mailbox.reload; self.mailbox.conversations.count
or, if neither of those work, just try reloading as many of the objects as possible to see if you can get it to work (self, mailbox, conversations, etc.).
My guess is that something is messed up between memory and the DB. This is definitely a really weird error though, might wanna put in an issue on Rails to see why this would be the case.
The result of mailbox.conversations is cached after the first call. To reload it write mailbox.conversations(true)

How Do I Store A Calculated Value In Rails

I am working on a project where I want to always show a post that has the least feedback (the goal is to encourage feedback on works in progress). I have figured out the calculation for this - although feedback is welcome - however, I am having trouble assigning this value to a column (:feedback_score) on the Post model. I need help.
post.rb:
class Post < ActiveRecord::Base
def feedback_score_calc
time_passed = ((Time.now - self.created_at)/1.hour).round(3)
feedback = self.comments.count
self.feedback_score = time_passed / feedback
end
end
I need to know how to call this method whenever a new comment is added, and I also need to be able to calculate it on some form of schedule. My goal is to display the least engaged on first visit with paging to progress to the 2nd to least engaged, 3rd, etc...
My other problem is that I can't even get this method to run through the console, I get a no method error, even when using def self.feedback_score_calc.
With your code as you've provided it, it's an instance method on Post. You should be able to do something like
#post = Post.find(some_id_here)
#post.feedback_score_calc
Creating it as a class method with definition def self.feedback_score_calc isn't what you want to do, because you're making calculations on a specific instance of Post and it's relations.
As #Kien Thanh mentioned in the comments, you'll need to call save on the Post instance after you set the column value if you want to see it reflected in the database.
#post = Post.find(some_id_here)
#post.feedback_score_calc
#post.save
or in the method itself
def feedback_score_calc
time_passed = ((Time.now - self.created_at)/1.hour).round(3)
feedback = self.comments.count
self.feedback_score = time_passed / feedback
save
end
Also worth mentioning, you can write to a column directly (bypassing the feedback_score= method ActiveRecord sets up) by calling write_attribute like this
def feedback_score_calc
time_passed = ((Time.now - self.created_at)/1.hour).round(3)
feedback = self.comments.count
write_attribute(:feedback_score, time_passed / feedback)
save
end
Finally, you need to either kill and restart your console or run reload! from within the console when making changes to your model that you're trying to verify from within the console.

Use find to initialize a constant?

Something like this:
class Category
SOME_CATEGORY = find_by_name("some category")
end
Category::SOME_CATEGORY
tried without a problem, but want to know if it is a bad idea, and the reasons if any..
thanks
If you don't want to hit the database each time you'll have to cache the model. There are several ways to do this, but one quick way is using Memoization. This was introduced in Rails 2.2.
class Category < ActiveRecord::Base
class << self
extend ActiveSupport::Memoizable
def named(name)
find_by_name(name)
end
memoize :named
end
end
Use it like this.
Category.named("some category") # hits the database
Category.named("some category") # doesn't hit the database
The cache should stay persistent across requests. You can reset the cache by passing true as the last parameter.
Category.named("some category", true) # force hitting the database
What do you want to do?
Maybe:
class Category
def self.some_category
Category.find_by_name("some category")
end
end
So you can call:
Category.some_category
=> <Category#2....>
It's not a terrible idea, but it's not really a good one either. It doesn't really fall in line with the way Rails does things. For one thing, you'll end up with a lot of ugly constant code. Too many ALL_CAPS_WORDS and your Ruby starts to look like C++. Bleah.
For another, it's inflexible. Are you going to make one of these constants for every category? If you add a new category two months from now, will you remember to update your Rails code, add a new constant, redeploy it and restart your server?
If it's important to you to be able to access categories very easily, and not repeat DB queries, here's a bit of metaprogramming that'll automatically look them up and create static methods like Lichtamberg's for you on first access:
def self.method_missing(category, *args) # The 'self' makes this a class method
#categories ||= {}
if (#categories[category] = find_by_name(category.to_s))
class_eval "def self.#{category.to_s}; #categories[#{category}]; end"
return #categories[category]
end
super
end
With this method in place, whenever you first call Category.ham, it'll create a class method that returns the value of find_by_name("ham") -- so that neither the query nor method_missing() runs again the next time you call it. This is pretty much the way the OpenStruct class works, BTW; look it up in the Pickaxe book if you want to learn more.
(Of course you'll still have the risk that, because these are all memoized, your Rails app won't reflect any changes you make to your category objects. This makes the assumption that changes won't happen or don't really matter. It's up to you to determine whether that assumption is valid for your app. You could always put an after_update callback in your code that resets ##categories if that's a problem; but at that point this starts to get complicated.)

Resources