Send email to only certain Users, Rails 5 - ruby-on-rails

I'm having trouble sending an email blast to only certain users who have a boolean set to true and to not send the email to those users who have it set to false.
In my app I have Fans following Artists through Artists Relationships. Inside my ArtistRelationship model I have a boolean that fans can set to true or false based on if they want email blasts from Artists or not when the Artist makes a post.
So far, I have this:
artist.rb
class Artist < ApplicationRecord
def self.fan_post_email
Artist.inlcudes(:fans).find_each do |fan|
fan.includes(:artist_relationships).where(:post_email => true).find_each do |fan|
FanMailer.post_email(fan).deliver_now
end
end
end
end
posts_controller.rb
class Artists::PostsController < ApplicationController
def create
#artist = current_artist
#post = #artist.artist_posts.build(post_params)
if #post.save
redirect_to artist_path(#artist)
#artist.fan_post_email
else
render 'new'
flash.now[:alert] = "You've failed!"
end
end
end
I'm having trouble getting the fan_post_email method to work. I'm not entirely sure how to find the Fans that have the boolean set to true in the ArtistRelationship model.

You want to send mails to fans of a particular artist. Therefore you call
#artist.fan_post_email
That is you call a method on an instance of the Artist class. Instance methods are not defined with a self.[METHOD_NAME]. Doing so defines class methods (if you where to call e.g. Artist.foo).
First part then is to remove the self. part, second is adapting the scope. The complete method should look like this:
def fan_post_email
artists_relationships
.includes(:fan)
.where(post_email: true)
.find_each do |relationship|
FanMailer.post_email(relationship.fan).deliver_now
end
end
end
Let's walk through this method.
We need to get all fans in order to send mails to them. This can be done by using the artist_relationships association. But as we only want to have those fans having checked the e-mail flag, we limit those by the where statement.
The resulting SQL condition will give us all such relationships. But we do it in batches (find_each) in order to not have to load all of the records into memory upfront.
The block provided to find_each is yielded with an artists_relationships instance. But we need the fan instances and not the artists_relationships instances to send the mail in our block and thus call post_email with the fan instance associated with the relationship. In order to avoid N+1 queries (a query for the fan record of every artists_relationships record one by one) there, we eager load the fan association on the artists_relationships.
Unrelated to the question
The usage of that method within the normal request/response cycle of a user's request will probably slow down the application quite a lot. If an artists has many fans, the application will send an e-mail to every one of them before rendering the response for the user. If it is a popular artist, I can easily imagine this taking minutes.
There is a counterpart to deliver_now which is deliver_later (documentation. Jobs, like sending an e-mail, can be queued and resolved independent from the request/response cycle. It will require setting up a worker like Sidekiq or delayed_job but the increase in performance is definitely worth it.
If the queueing mechanism is set up, it probably makes sense to move the call to fan_post_email there as well as the method itself might also take some time.
Additionally, it might make sense to send e-mail as BCC which would allow you to send one e-mail to multiple fans at the same time.

Related

Is there a way I can force a record to not be destroyed when running a feature test in RSpec? (Rails 6)

For context, I have a controller method called delete_cars. Inside of the method, I call destroy_all on an ActiveRecord::Collection of Cars. Below the destroy_all, I call another method, get_car_nums_not_deleted_from_portal, which looks like the following:
def get_car_nums_not_deleted_from_portal(cars_to_be_deleted)
reloaded_cars = cars_to_be_deleted.reload
car_nums = reloaded_cars.car_numbers
if reloaded_cars.any?
puts "Something went wrong. The following cars were not deleted from the portal: #{car_nums.join(', ')}"
end
car_nums
end
Here, I check to see if any cars were not deleted during the destroy_all transaction. If there are any, I just add a puts message. I also return the ActiveRecord::Collection whether there are any records or not, so the code to follow can handle it.
The goal with one of my feature tests is to mimic a user trying to delete three selected cars, but one fails to be deleted. When this scenario occurs, I display a specific notice on the page stating:
'Some selected cars have been successfully deleted from the portal, however, some have not. The '\
"following cars have not been deleted from the portal:\n\n#{some_car_numbers_go_here}"
How can I force just one record to fail when my code executes the destroy_all, WITHOUT adding extra code to my Car model (in the form of a before_destroy or something similar)? I've tried using a spy, but the issue is, when it's created, it's not a real record in the DB, so my query:
cars_to_be_deleted = Car.where(id: params[:car_ids].split(',').collect { |id| id.to_i })
doesn't include it.
For even more context, here's the test code:
context 'when at least one car is not deleted, but the rest are' do
it "should display a message stating 'Some selected cars have been successfully...' and list out the cars that were not deleted" do
expect(Car.count).to eq(100)
visit bulk_edit_cars_path
select(#location.name.upcase, from: 'Location')
select(#track.name.upcase, from: 'Track')
click_button("Search".upcase)
find_field("cars_to_edit[#{Car.first.id}]").click
find_field("cars_to_edit[#{Car.second.id}]").click
find_field("cars_to_edit[#{Car.third.id}]").click
click_button('Delete cars')
cars_to_be_deleted = Car.where(id: Car.first(3).map(&:id)).ids
click_button('Yes')
expect(page).to have_text(
'Some selected cars have been successfully deleted from the portal, however, some have not. The '\
"following cars have not been deleted from the portal:\n\n#{#first_three_cars_car_numbers[0]}".upcase
)
expect(Car.count).to eq(98)
expect(Car.where(id: cars_to_be_deleted).length).to eq(1)
end
end
Any help with this would be greatly appreciated! It's becoming quite frustrating lol.
One way to "mock" not deleting a record for a test could be to use the block version of .to receive to return a falsy value.
The argument for the block is the instance of the record that would be :destroyed.
Since we have this instance, we can check for an arbitrary record to be "not destroyed" and have the block return nil, which would indicate a "failure" from the :destroy method.
In this example, we check for the record of the first Car record in the database and return nil if it is.
If it is not the first record, we use the :delete method, as to not cause an infinite loop in the test (the test would keep calling the mock :destroy).
allow_any_instance_of(Car).to receive(:destroy) { |car|
# use car.delete to prevent infinite loop with the mocked :destroy method
if car.id != Car.first.id
car.delete
end
# this will return `nil`, which means failure from the :destroy method
}
You could create a method that accepts a list of records and decide which one you want to :destroy for more accurate testing!
I am sure there are other ways to work around this, but this is the best we have found so far :)
If there is a specific reason why the deletion might fail you can simulate that case.
Say you have a RaceResult record that must always refer to a valid Car and you have a DB constraint enforcing this (in Postgres: ON DELETE RESTRICT). You could write a test that creates the RaceResult records for some of your Car records:
it 'Cars prevented from deletion are reported` do
...
do_not_delete_cars = Car.where(id: Car.first(3).map(&:id)).ids
do_not_delete_cars.each { |car| RaceResult.create(car: car, ...) }
click_button('Yes')
expect(page).to have_text(...
end
Another option would be to use some knowledge of how your controller interacts with the model:
allow(Car).to receive(:destroy_list_of_cars).with(1,2,3).and_return(false) # or whatever your method would return
This would not actually run the destroy_list_of_cars method, so all the records would still be there in the DB. Then you can expect error messages for each of your selected records.
Or since destroy_all calls each record's destroy method, you could mock that method:
allow_any_instance_of('Car').to receive(:destroy).and_return(false) # simulates a callback halting things
allow_any_instance_of makes tests brittle however.
Finally, you could consider just not anticipating problems before they exist (maybe you don't even need the bulk delete page to be this helpful?). If your users see a more generic error, is there a page they could filter to verify for themselves what might still be there? (there's a lot of factors to consider here, it depends on the importance of the feature to the business and what sort of things could go wrong if the data is inconsistent).

Rails - conditional deliver_now based on the content returned by the mailer method

How can I deliver_now conditionally based on the content of the data returned by the mailer method?
I have a Mailer that is sent via a loop through a list of users, like so:
users.each do |u|
AbcMailer.with(user_id: u.user_id).abc_report.deliver_now
end
The list of users that should receive the mailer (users in the loop) lives in ActiveRecord, and all the users' actual data lives in an external MySql DB.
The abc_report method in the AbcMailer class makes some queries to the MySql DB and returns a bunch of info for each user, which is then inserted into an html.erb email template, and delivered now.
My issue is that I need to only deliver to some of those users, because in the DB, one of the pieces of info that comes back is whether the user is active or not. So I would like to only deliver_now if the user has active = 1. But I can't find any examples of unchaining these methods to do what I want.
When I just do AbcMailer.with(user_id: u.user_id).abc_report, what it returns is actually the filled-out html.erb template already. When I do AbcMailer.with(user_id: u.user_id) by itself, it returns #<ActionMailer::Parameterized::Mailer:0x00007fdd43eb5528>.
Things I've Tried
I tried inserting a return if user["Active"] == 0 in the abc_report method but that obviously killed the entire loop rather than skipping to the next item, so I'm working under the assumption that the skip has to happen with a next in the actual loop itself, not in an external method being called.
I also found this which seems like a great solution in a plain Ruby context but because in this case, abc_report is automatically filling out and returning the AbcMailer html.erb template...I'm stumped on how I would get it to just return a boolean without killing the whole loop.
Assuming that you have active field of boolean type in users table and you want to send the emails to all active users.
users.each do |u|
AbcMailer.with(user_id: u.user_id).abc_report.deliver_now if u.active
end
you can add any other condition on this as well.
Add this to user.rb
scope :active, -> {
where(active: 1)
}
Now in your mailer
Users.active.each do |user|
AbcMailer.with(user_id: user.user_id).abc_report.deliver_now
end
This way you have to get less data from the database, resulting in a faster query.

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

Working with stale data when performing asynchronous jobs with Sidekiq

In order to process events asynchronously and create an activity feed, I'm using Sidekiq and Ruby on Rails' Global ID.
This works well for most types of activities, however some of them require data that could change by the time the job is performed.
Here's a completely made-up example:
class Movie < ActiveRecord::Base
include Redis::Objects
value :score # stores an integer in Redis
has_many :likes
def popular?
likes.count > 1000
end
end
And a Sidekiq worker performing a job every time a movie is updated:
class MovieUpdatedWorker
include Sidekiq::Worker
def perform(global_id)
movie = GlobalID::Locator.locate(global_id)
MovieUpdatedActivity.create(movie: movie, score: movie.score) if movie.popular?
end
end
Now, imagine Sidekiq is lagging behind and, before it gets a chance to perform its job, the movie's score is updated in Redis, some users unliked the movie and the popular method now returns false.
Sidekiq ends up working with updated data.
I'm looking for ways to schedule jobs while making sure the required data won't change when the job is performed. A few ideas:
1/ Manually pass in all the required data and adjust the worker accordingly:
MovieUpdatedWorker.perform_async(
movie: self,
score: score,
likes_count: likes.count
)
This could work but would require reimplementing/duplicating all methods that rely on data such as score and popular? (imagine an app with much more than these two/three movable pieces).
This doesn't scale well either since serialized objects could take up a lot of room in Redis.
2/ Stubbing some methods on the record passed in to the worker:
MovieUpdatedWorker.perform_async(
global_id,
stubs: { score: score, popular?: popular? }
)
class MovieUpdatedWorker
include Sidekiq::Worker
def perform(global_id, stubs: {})
movie = GlobalID::Locator.locate(global_id)
# inspired by RSpec
stubs.each do |message, return_value|
movie.stub(message) { return_value }
end
MovieUpdatedActivity.create(movie: movie, score: movie.score) if movie.popular?
end
end
This isn't functional, but you can imagine the convenience of dealing with an actual record, not having to reimplement existing methods, and dealing with the actual data.
Do you see other strategies to "freeze" object data and asynchronously process them? What do you think about these two?
I wouldn't say that the data was stale since you would actually have the newest version of it, just that it was no longer popular. It sounds like you actually want the stale version.
If you don't want the data to have changed you need to cache it somehow. Either like you say, pass the data to the job directly, or You can add some form of versioning of the data in the database and pass a reference to the old version.
I think passing on the data you need to Redis is a reasonable way. You could serialize only the attributes you actually care about, like score.

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)

Resources