Instantiating Subclasses from dropdown option - ruby-on-rails

I have a page that allows a user to run a report. I'm using Rails 5. My reporting page allows the user to select different report types. So, the user has a drop down with the following options: Tax, Order, Refund, and so on.
Depending on their selection, I will instantiate a subclass of my Reports::Report class for their selected option.
Currently, I am doing this:
if params[:report_type] == "Tax"
Reports::Tax.new.run
elsif params[:report_type] == "Order"
Reports::Order.new.run
elsif params[:report_type] == "Refund"
Reports::Refund.new.run
end
How can I restructure this code so that I don't have an endlessly long list of conditionals for running certain report types? What's the more OOP approach to solving this?
Thanks in advance.

Well, if your options values map to the exact classes you have defined in your Reports module, (I mean for example, your select options are Tax, Order, Refund, and you have exact Reports::Tax, Reports::Order, Reports::Refund defined) you can try the following
begin
report = Object.const_get("Reports::#{params[:report_type]}")
report.new.run
rescue
# error handler for not existing report type
end

Related

Notifications or user activity log implementation

The application has certain actions that are recorded for each user and recorded in db table notifications. The table has the following structure:
user_id, notification_type, credit, timestamp
notification_type does not store the entire text of the notification but just stores a short type description.
Later when the user wants to view his notifications I use a helper method from my view to fetch the actual text.
def notification_text(type)
case type_id
when 'flagPositive'
return 'A question you flagged has been marked as correct.'
when 'qAccepted'
return 'A question you added has been accepted.'
when 'qModerated'
return 'You moderated a question.'
when 'flagReport'
return 'You moderated a flag.'
end
end
1) Is this an optimum way to do this?
2) Should I replace the type_description with integer values (say 1 -> flagPositive, 2-> qAccepted) for performance benefits?
3) Are there any best practices around the same that I should be following?
1) This highly depends on your application and requirements. What I can say is that I used this approach sometimes and faced no problems so far.
2) If you see a performance problem with the string lookup, you could do so. A general recommendation is to optimize performance only when really needed.
3) Just google for "Ruby", "Rails", "ActiveRecord" and "Enum". You'll find lots of discussions about different solutions for this kind of problem. There are similar questions on this site, e.g., Enums in Ruby or In Rails, how should I implement a Status field for a Tasks app - integer or enum?

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)

Dynamic nested form elements based on inputting a starting number

Like many others, I'm new to Rails and have a question. I'm working on a sample tracker for a small analytical lab. I would like users to submit batches consisting of many samples. I want the front page to be a simple gateway into batch submission. My general plan is:
Front page asks for number of samples in batch. User enters number and hits submit.
A form is generated where the user can enter batch information (Sampling date, experiment name, batch model stuff). Under the batch fields there should be as many fields for individual sample IDs as the user indicated in the first step.
User fills all this out and the batch and its samples are created upon submission.
My feeling is that the homepage should pass some sort of parameter to the batches controller, which then iteratively builds the samples while the model has a method to iteratively build form elements for the view. Is this thinking correct? How could I pass a parameter that isn't directly related to any models or controllers? I could find any similar questions, but if anyone can link me to a solution for a similar problem or a Railscast or something I'd be very grateful!
There's no need to back a form with a model. For your view, you'll just want something like this example (in Haml):
- form_tag new_batch_path, :method => "get" do
= label_tag(:sample_count, "Number of samples:")
= text_field_tag(:sample_count, 3)
= submit_tag("Get Started!")
And then in your controller, and the new_batch view, you can just reference params[:sample_count]
- (params[:sample_count] || 5).to_i.times do |n| ...
Because this isn't tied to a model (and nothing's being saved anyway) you can't use model validations to check the value. If you do want to verify, you'll do the verification in the batches controller - either as a before_filter, or just inline:
#sample_count = params[:sample_count].to_i
unless (1..10).include? #sample_count
flash[:error] = "A batch must contain between 1 and 10 samples."
redirect_to root_url
end
Note that nil.to_i, "".to_i and rubbish like "ajsdgsd".to_i all equal 0, so unless you want people to be able to specify 0 samples, this code is fairly robust
Have a look at these Railscasts series:
Nested Model Form: Part 1, Part 2
Complex Forms: Part 1, Part 2, Part 3
The "Nested Model Form" screencasts are newer, so I'd go with these ones first.

break down a complex search query in Rails 3

I have a controller which has a lot of options being sent to it via a form and I'm wondering how best to separate them out as they are not all being used simultaneously. Ie sometimes no, tags, sometimes no price specified. For prices I have a default price set so I can work around with it always being there, but the tags either need to be there, or not. etc.
#locations = Location.find(params[:id])
#location = #locations.places.active.where("cache_price BETWEEN ? AND ?",price_low,price_high).tagged_with([params[:tags]).order(params[:sort]).paginate :page => params[:page]
I haven't seen any good examples of this, but I'm sure it must happen often... any suggestions? Also, even will_paginate which gets tacked on last should be optional as the results either go to a list or to a google map, and the map needs no pagination.
the first thing to do when refactoring a complex search action is to use an anonymous scope.
Ie :
fruits = Fruit.scoped
fruits = fruits.where(:colour => 'red') if options[:red_only]
fruits = fruits.where(:size => 'big') if options[:big_only]
fruits = fruits.limit(10) if options[:only_first]
...
If the action controller still remains too big, you may use a class to handle the search. Moreover, by using a class with Rails 3 and ActiveModel you'll also be able to use validations if you want...
Take a look at one of my plugins : http://github.com/novagile/basic_active_model that allows you to easily create classes that may be used in forms.
Also take a look at http://github.com/novagile/scoped-search another plugin more specialized in creating search objects by using the scopes of a model.

RESTful nested conventional routing

I have the model:
User -1---n- Transaction(amount,description, date)
User -1---n- TransactionImport -1---n- TransactonImportField(name,value)
(personal expense tracking app).
What I want to achieve is this:
User opens URL and pastes the CSV with the list of transactions.
User submits it.
System extracts data from CSV into TransactionImport (row) + TransactionImportField (cell).
User can choose which column means what (amount, description, date) from the imported data in TransactionImport(Field).
User click save and the system transfers TransactionImport into the Transaction.
What I can't seem to get right is the fact that step 3 creates multiple records of TransactionImport (and related TransactionImportField).
So doing POST /transaction_imports?csv=abcd is expected to produce one record if we would be RESTful. But the code is supposed to be something like this:
# TransactionImportsController
def create
result = TransactionImports.parse(params[:csv])
flash[:notice] = result.message
redirect_to transaction_imports_path
end
I am probably approaching the task from a wrong angle as I feel that implementation doesn't fit in tp the inherited_resources.
Could you please advise what would be the most conventional way of implementing this?
Thanks,
Dmytrii.
REST/HTTP has no expectation that doing POST will only create one record. That maybe the default rails behaviour, but you should not constrain your design because of that.

Resources