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.
Related
I've set papertrail to only record changes containing a whodunnit value/when an admin makes a change by using the below condition in my model:
has_paper_trail if: proc { |model| PaperTrail.request.whodunnit.present? }
However I've noticed there are still a decent amount of records being stored with empty whodunnit values. From having a look at the records, these seem to be mostly 'update' actions all having empty object changes for some reason. I am unsure as to why the value is empty, or how it would get saved at all considering the above condition.
I am getting whodunnit values from warden in my application controller using:
def user_for_paper_trail
request.env['warden']&.user(:admin)&.id
end
Has anyone come across similar behaviour before?
Adding a source_location and command field to your papertrail versions table will help you:
track what command is causing the nil whodunnit changes.
Ensure you record correctly changes initiated by a rake task or rails console.
To do this, create a migration to add the source_location and command fields to your Versions table:
class AddSourceLocationAndCommandToVersions < ActiveRecord::Migration
def change
add_column :versions, :source_location, :text
add_column :versions, :command, :text
end
end
I have the following set up in my papertrail.rb. I am using the JSON serializer but these changes may work with the normal serializer. The code after the serializer declaration is what is of interest to you:
require "paper_trail/frameworks/rails"
require "paper_trail"
# the following line is required for PaperTrail >= 4.0.0 with Rails
PaperTrail::Rails::Engine.eager_load!
PaperTrail.config.enabled = true
PaperTrail.serializer = PaperTrail::Serializers::JSON
# Store some metadata about where the change came from, even for rake tasks, etc.
# See: https://github.com/paper-trail-gem/paper_trail/wiki/Setting-whodunnit-in-the-rails-console
def PaperTrail.set_global_metadata
request.controller_info ||= {}
request.controller_info[:command] ||= "#{File.basename($PROGRAM_NAME)} #{ARGV.join ' '} (#{$PID})"
request.controller_info[:source_location] = caller.find { |line|
line.starts_with? Rails.root.to_s and
!line.starts_with? __FILE__
}
end
# Defer evaluation in case we're using spring loader (otherwise it would be something like "spring app | app | started 13 secs ago | development")
# There's no way to set up deferred evaluation of PaperTrail.request.controller_info from here like
# we can with whodunnit, so abuse that property of PaperTrail.request.whodunnit to set other
# metadata. Reserve the whodunnit field for storing the actual name or id of the human who made the
# change.
PaperTrail.request.whodunnit = ->() {
PaperTrail.set_global_metadata
if Rails.const_defined?("Console") || $rails_rake_task
"#{`whoami`.strip}: console"
else
"#{`whoami`.strip}: #{File.basename($PROGRAM_NAME)} #{ARGV.join ' '}"
end
}
Rails.application.configure do
console do
PaperTrail.request.controller_info = { command: "rails console" }
PaperTrail.request.whodunnit = ->() {
PaperTrail.set_global_metadata
#paper_trail_whodunnit ||= (
if Rails.const_defined?("Console") || $rails_rake_task
"#{`whoami`.strip}: console"
else
"#{`whoami`.strip}: #{File.basename($PROGRAM_NAME)} #{ARGV.join ' '}"
end
)
}
end
end
Note that in any place where record creation occurs outside of a request, you can manually set the whodunnit value to something specific if you don't want it blank. E.g. in my seed file I do the following:
class SeedCreator
def initialize
PaperTrail.request.whodunnit = ->() {
PaperTrail.set_global_metadata # save source_location & command
"seed" # set whodunnit value to "seed"
}
create_campaign
create_users
# more seeding of models....
end
end
In addition to improving the quality of your Papertrail table (knowing which command triggered a record update), this config should help you identify where phantom whodunnits are being saved.
I have a class called CachedObject that stores generic serialized objects indexed by a key. I want this class to implement a create_or_update method. If an object is found it will update it, otherwise it will create a new one.
Is there a way to do this in Rails or do I have to write my own method?
Rails 6
Rails 6 added an upsert and upsert_all methods that deliver this functionality.
Model.upsert(column_name: value)
[upsert] It does not instantiate any models nor does it trigger Active Record callbacks or validations.
Rails 5, 4, and 3
Not if you are looking for an "upsert" (where the database executes an update or an insert statement in the same operation) type of statement. Out of the box, Rails and ActiveRecord have no such feature. You can use the upsert gem, however.
Otherwise, you can use: find_or_initialize_by or find_or_create_by, which offer similar functionality, albeit at the cost of an additional database hit, which, in most cases, is hardly an issue at all. So unless you have serious performance concerns, I would not use the gem.
For example, if no user is found with the name "Roger", a new user instance is instantiated with its name set to "Roger".
user = User.where(name: "Roger").first_or_initialize
user.email = "email#example.com"
user.save
Alternatively, you can use find_or_initialize_by.
user = User.find_or_initialize_by(name: "Roger")
In Rails 3.
user = User.find_or_initialize_by_name("Roger")
user.email = "email#example.com"
user.save
You can use a block, but the block only runs if the record is new.
User.where(name: "Roger").first_or_initialize do |user|
# this won't run if a user with name "Roger" is found
user.save
end
User.find_or_initialize_by(name: "Roger") do |user|
# this also won't run if a user with name "Roger" is found
user.save
end
If you want to use a block regardless of the record's persistence, use tap on the result:
User.where(name: "Roger").first_or_initialize.tap do |user|
user.email = "email#example.com"
user.save
end
In Rails 4 you can add to a specific model:
def self.update_or_create(attributes)
assign_or_new(attributes).save
end
def self.assign_or_new(attributes)
obj = first || new
obj.assign_attributes(attributes)
obj
end
and use it like
User.where(email: "a#b.com").update_or_create(name: "Mr A Bbb")
Or if you'd prefer to add these methods to all models put in an initializer:
module ActiveRecordExtras
module Relation
extend ActiveSupport::Concern
module ClassMethods
def update_or_create(attributes)
assign_or_new(attributes).save
end
def update_or_create!(attributes)
assign_or_new(attributes).save!
end
def assign_or_new(attributes)
obj = first || new
obj.assign_attributes(attributes)
obj
end
end
end
end
ActiveRecord::Base.send :include, ActiveRecordExtras::Relation
The magic you have been looking for has been added in Rails 6
Now you can upsert (update or insert).
For single record use:
Model.upsert(column_name: value)
For multiple records use upsert_all :
Model.upsert_all(column_name: value, unique_by: :column_name)
Note:
Both methods do not trigger Active Record callbacks or validations
unique_by => PostgreSQL and SQLite only
Add this to your model:
def self.update_or_create_by(args, attributes)
obj = self.find_or_create_by(args)
obj.update(attributes)
return obj
end
With that, you can:
User.update_or_create_by({name: 'Joe'}, attributes)
Old question but throwing my solution into the ring for completeness.
I needed this when I needed a specific find but a different create if it doesn't exist.
def self.find_by_or_create_with(args, attributes) # READ CAREFULLY! args for finding, attributes for creating!
obj = self.find_or_initialize_by(args)
return obj if obj.persisted?
return obj if obj.update_attributes(attributes)
end
By chaining find_or_initialize_by and update, this can be achieved in a simple way which avoids the (in my experience, often) unwanted caveats of upsert, and also minimises database calls.
For example:
Class.find_or_initialize_by(
key: "foo",
...
).update(
new_attribute: "bar",
...
)
will return you newly created or updated object.
It is worth noting that if your find_or_initialize_by attributes match multiple Class instances, only the 'first' one will be selected and updated.
You can do it in one statement like this:
CachedObject.where(key: "the given key").first_or_create! do |cached|
cached.attribute1 = 'attribute value'
cached.attribute2 = 'attribute value'
end
The sequel gem adds an update_or_create method which seems to do what you're looking for.
Requirements
I need to create a migration, for a live production database that does the following:
For each company verify if a set of names are instances of an utterance class associated with it.
If said validation doesn't exist have the validation fail and return an error.
If not continue migrating
Current behaviour
I tried this:
class AddFeature < ActiveRecord::Migration[5.1]
def change
run_migration = true
Company.all.each do |organization|
Company.product_types_names.each { |type| run_migration &= Utterance.exists?("utter_#{type.to_s}", organization.id) }
end
if run_migration
# my code
end
end
end
Although the changes to the database, don't occur I need the migration to stop with an error. Currently, the migration isn't stopped by any form of error when I an utterance doesn't exist.
Expected behavior
I would like to know how to simply return an error and stop the migration when one any of the instances don't exist.
Something like this:
class AddFeature < ActiveRecord::Migration[5.1]
def change
Company.all.each do |organization|
Company.product_types_names.each { |type| run_migration &= Utterance.exists?("utter_#{type.to_s}", organization.id) }
# return_errors_and stop the app if validation false
end
# my code
end
end
Generally speaking, it is not recommended to write your custom code in Rails migrations. Migrations are for manipulations on database schema. You manipulate on data.
Answering your question: you can simply stop your migration by raising an exception, such as:
raise StandardError, "Invalid data"
In this case the migration will be stopped and not marked as completed (migration version won't be saved to schema_migrations table in your database). On next call of rake db:migrate it will try to run that migration again.
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.
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.