I'm using Rails 4.1.0.beta1's new Action Mailer previews and have the following code:
class EventInvitationPreview < ActionMailer::Preview
def invitation_email
invite = FactoryGirl.create :event_invitation, :for_match, :from_user, :to_user
EventInvitationMailer.invitation_email(invite)
end
end
This is all good until I actually try to preview my email and get an error saying that validation on a User object failed due to duplicate email addresses. Turns out that ActionMailer::Preview is writing to my development database.
While I could work around the validation failure or use fixtures instead of factories, is there any way to avoid ActionMailer::Preview writing to the development database, e.g. use the test database instead? Or am I just doing it wrong?
Cleaner/Easier (based on other answers) and tested with Rails 7: Do not change Rails' classes but create your own. Id addition to not change the controller but the call method of ActionMailer::Preview.
# app/mailers/preview_mailer.rb
class PreviewMailer < ActionMailer::Preview
def self.call(...)
message = nil
ActiveRecord::Base.transaction do
message = super(...)
raise ActiveRecord::Rollback
end
message
end
end
# inherit from `PreviewController` for your previews
class EventInvitationPreview < PreviewController
def invitation_email
...
end
end
OLD:
You can simply use a transaction around email previews, just put this inside your lib/monkey_mailers_controller.rb (and require it):
# lib/monkey_mailers_controller.rb
class Rails::MailersController
alias_method :preview_orig, :preview
def preview
ActiveRecord::Base.transaction do
preview_orig
raise ActiveRecord::Rollback
end
end
end
Then you can call .create etc. in your mailer previews but nothing will be saved to database. Works in Rails 4.2.3.
A cleaner way to proceed is to prepend a module overriding and wrapping preview into a transaction:
module RollbackingAfterPreview
def preview
ActiveRecord::Base.transaction do
super
raise ActiveRecord::Rollback
end
end
end
Rails.application.config.to_prepare do
class Rails::MailersController
prepend RollbackingAfterPreview
end
end
TL;DR -- The original author of the ActionMailer preview feature (via the MailView gem) provides three examples of different supported approaches:
Pull data from existing fixtures: Account.first
Factory-like pattern: user = User.create! followed by user.destroy
Stub-like: Struct.new(:email, :name).new('name#example.com', 'Jill Smith')
~ ~ ~ ~ ~ ~ ~ ~ ~ ~
To elaborate on the challenge faced by the OP...
Another manifestation of this challenge is attempting to use FactoryGirl.build (rather than create) to generate non-persistent data. This approach is suggested by one of the top Google results for "Rails 4.1" -- http://brewhouse.io/blog/2013/12/17/whats-new-in-rails-4-1.html?brewPubStart=1 -- in the "how to use this new feature" example. This approach seems reasonable, however if you're attempting to generate a url based on that data, it leads to an error along the lines of:
ActionController::UrlGenerationError in Rails::Mailers#preview
No route matches {:action=>"edit", :controller=>"password_resets", :format=>nil, :id=>nil} missing required keys: [:id]
Using FactoryGirl.create (rather than build) would solve this problem, but as the OP notes, leads to polluting the development database.
If you check out the docs for the original MailView gem which became this Rails 4.1 feature, the original author provides a bit more clarity about his intentions in this situation. Namely, the original author provides the following three examples, all focused on data reuse / cleanup / non-persistence, rather than providing a means of using a different database:
# app/mailers/mail_preview.rb or lib/mail_preview.rb
class MailPreview < MailView
# Pull data from existing fixtures
def invitation
account = Account.first
inviter, invitee = account.users[0, 2]
Notifier.invitation(inviter, invitee)
end
# Factory-like pattern
def welcome
user = User.create!
mail = Notifier.welcome(user)
user.destroy
mail
end
# Stub-like
def forgot_password
user = Struct.new(:email, :name).new('name#example.com', 'Jill Smith')
mail = UserMailer.forgot_password(user)
end
end
For Rails 6:
#Markus' answer worked for me, except that it caused a nasty deprecation-soon-will-be-real error related to how Autoloading has changed in Rails 6:
DEPRECATION WARNING: Initialization autoloaded the constants [many constants seemingly unrelated to what I actually did]
Being able to do this is deprecated. Autoloading during initialization is going to be an error condition in future versions of Rails.
[...]
Well, that's no good!
After more searching, this blog and the docs for
to_prepare helped me come up with this solution, which is just #Markus' answer wrapped in to_prepare. (And also it's in initializer/ instead of lib/.)
# /config/initializers/mailer_previews.rb
---
# Wrap previews in a transaction so they don't create objects.
Rails.application.config.to_prepare do
class Rails::MailersController
alias_method :preview_orig, :preview
def preview
ActiveRecord::Base.transaction do
preview_orig
raise ActiveRecord::Rollback
end
end
end
end
If you have a complicated object hierarchy, you can exploit transactional semantics to rollback the database state, as you would in a test environment (assuming your DB supports transactions). For example:
# spec/mailers/previews/price_change_preview.rb
class PriceChangeMailerPreview < ActionMailer::Preview
#transactional strategy
def price_decrease
User.transaction do
user = FactoryGirl.create(:user, :with_favorited_products) #creates a bunch of nested objects
mail = PriceChange.price_decrease(user, user.favorited_products.first)
raise ActiveRecord::Rollback, "Don't really want these objects committed to the db!"
end
mail
end
end
#spec/factories/user.rb
FactoryGirl.define do
factory :user do
...
trait :with_favorited_products do
after(:create) do |user|
user.favorited_products << create(:product)
user.save!
end
end
end
end
We can't use user.destroy with dependent: :destroy in this case because destroying the associated products normally doesn't make sense (if Amazon removes me as a customer, they don't remove all the products I have favorited from the market).
Note that transactions are supported by previous gem implementations of the preview functionality. Not sure why they aren't supported by ActionMailer::Preview.
Related
I have a question and answer forum on rails 4.1 with a react client where the user can create questions, answers, and comments. I want to pass all the user activity (regardless of type) using active model serializers. Ideally this should be an array of objects sorted by created_at. I'm able to create the array, but the push methods overwrite one another.
I feel like I might need to actually use a hash to avoid a labyrinth of working with an attribute nested inside the individual object arrays, but hoping for a little guidance.
If this doesn't make sense, think about Facebook showing you your recent activity:
[
"You posted 'It's my birthday!' on 08/04/2017",
"You liked Tammy's post on 8/3/2017",
"You commented on Rihanna Tweets as Motivational Posters page on 8/1/2017"
]
user_activity method:
def user_activity
activity = []
self.object.questions.each do |question|
activity.push(question)
end
self.object.answers.each do |answer|
activity.push(answer)
end
self.object.comments.each do |comment|
activity.push(comment)
end
end
Thanks and sorry for the noob question.
Module implementation:
module UserActivityOutputer
def output_for_user_activity
raise NotImplementedError, "You must implement `#{self.class}##{__method__}`"
end
end
Module inclusion:
class Post
include UserActivityOutputer
def title ; 'combining user owned objects in rails' ; end # only here for easy copy-paste test in IRB
def created_at ; DateTime.now ; end # only here for easy copy-paste test in IRB
def output_for_user_activity
"You posted '#{self.title}' on #{I18n.l(self.created_at.to_date)}"
end
end
class SomeModel
include UserActivityOutputer
# did not implement output_for_user_activity method for example purpose
end
Usage:
Post.new.output_for_user_activity
# => "You posted 'combining user owned objects in rails' on 2017-08-09"
SomeModel.new.output_for_user_activity
# => NotImplementedError: You must implement `SomeModel#output_for_user_activity`
You can copy-paste all of the code presented here in a freshly opened IRB console (might redefine existing Post or SomeModel class) and see the output.
This is a very basic implementation only meant to define "what should be output for this record". It does not support the sorting, which would be made somewhere else.
Is it possible to modify the Rails obj?
I only want to modify it briefly and change it back.
My Reasoning:
I am trying to work on my seeds file and make it a little more robust.
In my model there is a process that looks at the current controller and the current user, it tracks this user during there session.
It throws an error though during my seed tests because there is no controller based user session.
What I wanted to do was to add
Rails.seed = true
at the start of my seed, it would get to the model and in the model I would wrap a control flow(if statement) for this property around the block that setups up tracking.
Then I would remove
Rails.seed = true
at the end of the seed file.
Instead of putting it directly on the Rails object, you can use custom configuration
config/initializers/custom_config.rb (name unimportant, just in an initializer)
Rails.configuration.seeding = false
db/seeds.rb
Rails.configuration.seeding = true
User.create
app/models/user.rb
class User < ApplicationRecord
# just as an example
after_initialize do
if Rails.configuration.seeding
puts "Seeding DB"
else
puts "Normal"
end
end
end
output
$ bin/rake db:seed
# Running via Spring preloader in process 19017
# Seeding DB
$ bin/rails c
User.create
# Normal
# => #<User ...>
I wouldn't necessarily recommend modifying the Rails class but to achieve that you could do something like:
class Rails
attr_accessor :seeding
def seeding=(bool)
#seeding = bool
end
def seeding?
#seeding ||= false
end
end
Then you could use Rails.seeding = true to set it and Rails.seeding? to access it. Also it will default to false if it is unset.
Another solution might be wrapping the part that is blowing up in a being rescue block to catch the error.
I working on project using Rails 4.1.6 now. And I have strange problem. Method to_param for my models (Product, Category) sometimes not calling. I use it for SEO-friendly urls.
Here is my to_param method for Category model:
def to_param
puts 'I am in Category to_param'
"#{id}-#{title.to_slug.normalize.to_s}"
end
I use puts for find out is this method working or no. So, when my urls looks good (/categories/39-средства-дезинфекции) I can see the string 'I am in Category to_param' on my server console. This is correct case and all it's great.
But sometimes I have urls like /categories/39 for the same objects. When I look into console for this case, I don't see any prints from my to_param method form Category model.
These two cases I have on the same pages, same views and using the same helpers for category url (category_path).
Most complicated for this situation is that I can't reproduce this bug and don't see any regularity. For the same objects I have correct urls most of times, but sometimes it's not. If I restart rails server and refresh browser with clear cache – problem may out and urls will be correct again.
During my debug and research I found source code for base class. But I can't see there any reasons for the situation described above.
def to_param(method_name = nil)
if method_name.nil?
super()
else
define_method :to_param do
if (default = super()) &&
(result = send(method_name).to_s).present? &&
(param = result.squish.truncate(20, separator: /\s/, omission: nil).parameterize).present?
"#{default}-#{param}"
else
default
end
end
end
end
Also I can tell that this problem was appear, when I used FriendlyID before, using regex for clear and build slugs, and now for babosa gem. So, I think the problem is my to_param sometimes not calling for my model.
So, I found the reason of this behaviour. Now it's resolved!
The reason was I have redefined to_param for Product and Category in my ActiveAdmin files:
before_filter do
Product.class_eval do
def to_param
puts "I am in admin Product to_param"
id.to_s
end
end
Category.class_eval do
def to_param
puts "I am in admin Category to_param"
id.to_s
end
end
end
So, when I was log in Admin panel and go to Product page – "bug" will appear on front-end views.
So, I need to remove Product.class_eval and Category.class_eval blocks from my admin classes.
Consider the following class and methods: (This class is obviously much more complete, but for the sake of this thread...):
class Order < ActiveRecord::Base
def check
if (self.user.phone == "55555555") do
self.a_certain_method
return
end
end
def a_certain_method
# Real implementation goes here
end
end
And the following Unit Test:
describe :do_route do
it "should call a_certain_method if user phone number matches 55555555" do
# Create a user
user = Factory(:user)
# Set hard-coded phone number
user.phone = "55555555"
user.save!
# Create an order made by the ordering user
order = Factory(:order, :ordering_user => user)
# Set expectation for a "a_certain_method" call
mock(order).a_certain_method
# Call the tested method
order.check
end
end
From some reason, the above test produces an RR::Errors::TimesCalledError error, which claims that a_certain_method was called 0 times instead of 1... I've been searching around the web for a solution with no luck.
I've tried building a similiar test on a non-activerecord class, and the test produces no errors.
I've used the debugger to check that it does reach the self.a_certain_method line, and also tried using the following instead of mock(order).a_certain_method:
any_instance_of(Order) do |o|
mock(o).a_certain_method
end
Does anyone have any idea how to solve this issue since i'm kind of desperate...
I figured out what the problem was, it failed since the number was already in the database. so it failed to save the hard coded user.phone change.
Thanks for the help though :)
Basically what I want to do is to log an action on MyModel in the table of MyModelLog. Here's some pseudo code:
class MyModel < ActiveRecord::Base
validate :something
def something
# test
errors.add(:data, "bug!!")
end
end
I also have a model looking like this:
class MyModelLog < ActiveRecord::Base
def self.log_something
self.create(:log => "something happened")
end
end
In order to log I tried to :
Add MyModelLog.log_something in the something method of MyModel
Call MyModelLog.log_something on the after_validation callback of MyModel
In both cases the creation is rolled back when the validation fails because it's in the validation transaction. Of course I also want to log when validations fail. I don't really want to log in a file or somewhere else than the database because I need the relationships of log entries with other models and ability to do requests.
What are my options?
Nested transactions do seem to work in MySQL.
Here is what I tried on a freshly generated rails (with MySQL) project:
./script/generate model Event title:string --skip-timestamps --skip-fixture
./script/generate model EventLog error_message:text --skip-fixture
class Event < ActiveRecord::Base
validates_presence_of :title
after_validation_on_create :log_errors
def log_errors
EventLog.log_error(self) if errors.on(:title).present?
end
end
class EventLog < ActiveRecord::Base
def self.log_error(event)
connection.execute('BEGIN') # If I do transaction do then it doesn't work.
create :error_message => event.errors.on(:title)
connection.execute('COMMIT')
end
end
# And then in script/console:
>> Event.new.save
=> false
>> EventLog.all
=> [#<EventLog id: 1, error_message: "can't be blank", created_at: "2010-10-22 13:17:41", updated_at: "2010-10-22 13:17:41">]
>> Event.all
=> []
Maybe I have over simplified it, or missing some point.
Would this be a good fit for an Observer? I'm not sure, but I'm hoping that exists outside of the transaction... I have a similar need where I might want to delete a record on update...
I've solved a problem like this by taking advantage of Ruby's variable scoping. Basically I declared an error variable outside of a transaction block then catch, store log message, and raise the error again.
It looks something like this:
def something
error = nil
ActiveRecord::Base.transaction do
begin
# place codez here
rescue ActiveRecord::Rollback => e
error = e.message
raise ActiveRecord::Rollback
end
end
MyModelLog.log_something(error) unless error.nil?
end
By declaring the error variable outside of the transaction scope the contents of the variable persist even after the transaction has exited.
I am not sure if it applies to you, but i assume you are trying to save/create a model from your controller. In the controller it is easy to check the outcome of that action, and you most likely already do to provide the user with a useful flash; so you could easily log an appropriate message there.
I am also assuming you do not use any explicit transactions, so if you handle it in the controller, it is outside of the transaction (every save and destroy work in their own transaction).
What do you think?
MyModelLog.log_something should be done using a different connection.
You can make MyModelLog model always use a different connection by using establish_connection.
class MyModelLog < ActiveRecord::Base
establish_connection Rails.env # Use different connection
def self.log_something
self.create(:log => "something happened")
end
end
Not sure if this is the right way to do logging!!
You could use a nested transaction. This way the code in your callback executes in a different transaction than the failing validation. The Rails documentations for ActiveRecord::Transactions::ClassMethods discusses how this is done.