I am following this tutorial to make Cart in my Ecomerce App:
https://richonrails.com/articles/building-a-shopping-cart-in-ruby-on-rails
Anything do well but when session end and customer do not complete the order, this order and order item still saved in the database.
My idea is use callback method before_destroy session cart but i dont know exactly what method destroy session
Can anyone help me?
Unfortunately the callback method will be difficult, because you never know when the session ended, since websites are stateless. In other words, you never really know whether a visitor is just idle, or he actually left. The only signs you normally get, are requests (GET/POST/PATCH/..).
What you could do to make this work, is check the last_updated value, and delete all non-confirmed carts after let's say two hours. Or, you could even do a day, because who cares right? It's only a few bytes in your DB.
To make this work, you could write a rake task that queries and deletes all the abandoned carts.
Something like:
# cart model
class Cart
scope :unconfirmed { where(bought: false) }
scope :abandoned, { unconfirmed.where('last_update < ?', 1.day.ago}
end
# rake task
namespace :maintenance do
task delete_abandoned_carts: :environment do
Cart.abandoned.destroy_all
end
end
Learn more about rake tasks here: https://guides.rubyonrails.org/command_line.html#custom-rake-tasks
Related
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).
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.
I want to display a random record from the database for a certain amount of time, after that time it gets refreshed to another random record.
How would I go about that in rails?
Right now I'm looking in the directions of cronjobs, also the whenever gem, .. but I'm not 100% sure I really need all that for what seems to be a pretty simple action?
Use the Rails.cache mechanism.
In your controller:
#record = Rails.cache("cached_record", :expires_in => 5.minutes) do
Model.first( :offset =>rand(Model.count))
end
During the first execution, result gets cached in the Rails cache. A new random record is retrieved after 5 minutes.
I would have an expiry_date in my model and then present the user with a javascript timer. After the time has elapsed, i would send a request back to the server(ajax probably, or maybe refreshing the page) and check whether the time has indeed expired. If so, i would present the new record.
You could simply check the current time in your controller, something like:
def show
#last_refresh ||= DateTime.now
#current ||= MyModel.get_random
#current = MyModel.get_random if (DateTime.now - #last_refresh) > 5.minutes
end
This kind of code wouldn't scale to more servers (as it relies on class variables for data storage), so in reality you would wan't to store the two class variables in something like Redis (or Memcache even) - that is for high performance. Depends really on how accurately you need this and how much performance you need. You could as well use your normal database to store expiry times and then load the record whose time is current.
My first though was to cache the record in a global, but you could end up with different records being served by different servers. How about adding a :chosen_at datetime column to your record...
class Model < AR::Base
def self.random
##random = first(:conditions => 'chosen_at NOT NULL')
return ##random unless ##random.nil? or ##random.chosen_at < 5.minutes.ago
##random.update_attribute(:chosen_at,nil) if ##random
ids = connection.select_all("SELECT id FROM things")
##random = find(ids[rand(ids.length)]["id"].to_i)
end
end
So I'm using Delayed::Job workers (on Heroku) as an after_create callback after a user creates a certain model.
A common use case, though, it turns out, is for users to create something, then immediately delete it (likely because they made a mistake or something).
When this occurs the workers are fired up, but by the time they query for the model at hand, it's already deleted, BUT because of the auto-retry feature, this ill-fated job will retry 25 times, and definitely never work.
Is there any way I can catch certain errors and, when they occur, prevent that specific job from ever retrying again, but if it's not that error, it will retry in the future?
Abstract the checks into the function you call with delayed_job. Make the relevant checks wether your desired job can proceed or not and either work on that job or return success.
To expand on David's answer, instead of doing this:
def after_create
self.send_later :spam_this_user
end
I'd do this:
# user.rb
def after_create
Delayed::Job.enqueue SendWelcomeEmailJob.new(self.id)
end
# send_welcome_email_job.rb
class SendWelcomeEmailJob < Struct(:user_id)
def perform
user = User.find_by_id(self.user_id)
return if user.nil? #user must have been deleted
# do stuff with user
end
end
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.)