Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support added AR::Dirty methods #111

Merged
merged 6 commits into from
Nov 29, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 55 additions & 13 deletions lib/mobility/plugins/active_record/dirty.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ module Plugins
Dirty tracking for AR models. See {Mobility::Plugins::ActiveModel::Dirty} for
details on usage.

In addition to methods added by {Mobility::Plugins::ActiveModel::Diryt}, the
AR::Dirty plugin adds support for the following persistence-specific methods
(for a model with a translated attribute +title+):
- +saved_changes+
- +saved_change_to_title?+
- +saved_change_to_title+
- +title_before_last_save+
- +will_save_change_to_title?+
- +title_change_to_be_saved+
- +title_in_database+

=end
module ActiveRecord
module Dirty
Expand All @@ -19,7 +30,30 @@ class MethodsBuilder < ActiveModel::Dirty::MethodsBuilder
def initialize(*attribute_names)
super
@attribute_names = attribute_names
define_method_overrides
define_attribute_methods if ::ActiveRecord::VERSION::STRING >= '5.1'
end

# Overrides +ActiveRecord::AttributeMethods::ClassMethods#has_attribute+ to treat fallthrough attribute methods
# just like "real" attribute methods.
#
# @note Patching +has_attribute?+ is necessary as of AR 5.1 due to this commit[https://github.com/rails/rails/commit/4fed08fa787a316fa51f14baca9eae11913f5050].
# (I have voiced my opposition to this change here[https://github.com/rails/rails/pull/27963#issuecomment-310092787]).
# @param [Attributes] attributes
def included(model_class)
names = @attribute_names
method_name_regex = /\A(#{names.join('|'.freeze)})_([a-z]{2}(_[a-z]{2})?)(=?|\??)\z/.freeze
has_attribute = Module.new do
define_method :has_attribute? do |attr_name|
super(attr_name) || !!method_name_regex.match(attr_name)
end
end
model_class.extend has_attribute
end

private

def define_method_overrides
changes_applied_method = ::ActiveRecord::VERSION::STRING < '5.1' ? :changes_applied : :changes_internally_applied
define_method changes_applied_method do
@previously_changed = changes
Expand All @@ -36,21 +70,29 @@ def initialize(*attribute_names)
end
end

# Overrides +ActiveRecord::AttributeMethods::ClassMethods#has_attribute+ to treat fallthrough attribute methods
# just like "real" attribute methods.
#
# @note Patching +has_attribute?+ is necessary as of AR 5.1 due to this commit[https://github.com/rails/rails/commit/4fed08fa787a316fa51f14baca9eae11913f5050].
# (I have voiced my opposition to this change here[https://github.com/rails/rails/pull/27963#issuecomment-310092787]).
# @param [Attributes] attributes
def included(model_class)
names = @attribute_names
method_name_regex = /\A(#{names.join('|'.freeze)})_([a-z]{2}(_[a-z]{2})?)(=?|\??)\z/.freeze
has_attribute = Module.new do
define_method :has_attribute? do |attr_name|
super(attr_name) || !!method_name_regex.match(attr_name)
# For AR >= 5.1 only
def define_attribute_methods
define_method :saved_changes do
(@previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new).merge(super())
end

@attribute_names.each do |name|
define_method :"saved_change_to_#{name}?" do
previous_changes.include?(Mobility.normalize_locale_accessor(name))
end

define_method :"saved_change_to_#{name}" do
previous_changes[Mobility.normalize_locale_accessor(name)]
end

define_method :"#{name}_before_last_save" do
previous_changes[Mobility.normalize_locale_accessor(name)].first
end

alias_method :"will_save_change_to_#{name}?", :"#{name}_changed?"
alias_method :"#{name}_change_to_be_saved", :"#{name}_change"
alias_method :"#{name}_in_database", :"#{name}_was"
end
model_class.extend has_attribute
end
end
end
Expand Down
118 changes: 115 additions & 3 deletions spec/mobility/plugins/active_record/dirty_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def values
stub_const 'Article', Class.new(ActiveRecord::Base)
Article.extend Mobility
Article.translates :title, backend: backend_class, dirty: true, cache: false
Article.translates :content, backend: backend_class, dirty: true, cache: false

# ensure we include these methods as a module rather than override in class
changes_applied_method = ::ActiveRecord::VERSION::STRING < '5.1' ? :changes_applied : :changes_internally_applied
Expand Down Expand Up @@ -141,6 +142,32 @@ def clear_changes_information
"title_fr" => ["Titre en Francais 1", "Titre en Francais 2"]})
end

it "tracks forced changes" do
article = Article.create(title: "foo")

article.title_will_change!

aggregate_failures do
expect(article.changed?).to eq(true)
expect(article.title_changed?).to eq(true)
if ENV['RAILS_VERSION'].present? && ENV['RAILS_VERSION'] < '5.0'
expect(article.content_changed?).to eq(nil)
else
expect(article.content_changed?).to eq(false)
end
expect(article.title_change).to eq(["foo", "foo"])
expect(article.content_change).to eq(nil)
expect(article.previous_changes).to include({ "title_en" => [nil, "foo"]})

article.save

expect(article.changed?).to eq(false)
expect(article.title_change).to eq(nil)
expect(article.content_change).to eq(nil)
expect(article.previous_changes).to include({ "title_en" => ["foo", "foo"]})
end
end

it "resets changes when locale is set to original value" do
article = Article.new

Expand Down Expand Up @@ -174,11 +201,33 @@ def clear_changes_information
it "defines suffix methods on translated attribute" do
article = Article.new
article.title = "foo"

article.save
aggregate_failures "after save" do
expect(article.changed?).to eq(false)
expect(article.title_change).to eq(nil)
expect(article.title_was).to eq("foo")

if ENV['RAILS_VERSION'].present? && ENV['RAILS_VERSION'] < '5.0'
expect(article.title_changed?).to eq(nil)
else
expect(article.title_previously_changed?).to eq(true)
expect(article.title_previous_change).to eq([nil, "foo"])
expect(article.title_changed?).to eq(false)
end

# AR-specific suffix methods, added in AR 5.1
if ENV['RAILS_VERSION'].present? && ENV['RAILS_VERSION'] > '5.0'
expect(article.saved_change_to_title?).to eq(true)
expect(article.saved_change_to_title).to eq([nil, "foo"])
expect(article.title_before_last_save).to eq(nil)
expect(article.title_in_database).to eq("foo")
end
end

article.title = "bar"

aggregate_failures do
aggregate_failures "changed after save" do
expect(article.title_changed?).to eq(true)
expect(article.title_change).to eq(["foo", "bar"])
expect(article.title_was).to eq("foo")
Expand All @@ -192,8 +241,53 @@ def clear_changes_information
expect(article.title_changed?).to eq(false)
end

# AR-specific suffix methods
if ENV['RAILS_VERSION'].present? && ENV['RAILS_VERSION'] > '5.0'
expect(article.saved_change_to_title?).to eq(true)
expect(article.saved_change_to_title).to eq(["foo", "bar"])
expect(article.title_before_last_save).to eq("foo")
expect(article.will_save_change_to_title?).to eq(false)
expect(article.title_change_to_be_saved).to eq(nil)
expect(article.title_in_database).to eq("bar")
end
end

aggregate_failures "force change" do
article.title_will_change!
expect(article.title_changed?).to eq(true)

aggregate_failures "before save" do
expect(article.title_changed?).to eq(true)

# AR-specific suffix methods
if ENV['RAILS_VERSION'].present? && ENV['RAILS_VERSION'] > '5.0'
expect(article.saved_change_to_title?).to eq(true)
expect(article.saved_change_to_title).to eq(["foo", "bar"])
expect(article.title_before_last_save).to eq("foo")
expect(article.will_save_change_to_title?).to eq(true)
expect(article.title_change_to_be_saved).to eq(["bar", "bar"])
expect(article.title_in_database).to eq("bar")
end
end

article.save!

aggregate_failures "after save" do
if ENV['RAILS_VERSION'].present? && ENV['RAILS_VERSION'] < '5.0'
expect(article.title_changed?).to eq(nil)
else
expect(article.title_changed?).to eq(false)
end

# AR-specific suffix methods
if ENV['RAILS_VERSION'].present? && ENV['RAILS_VERSION'] > '5.0'
expect(article.saved_change_to_title?).to eq(true)
expect(article.saved_change_to_title).to eq(["bar", "bar"])
expect(article.title_before_last_save).to eq("bar")
expect(article.will_save_change_to_title?).to eq(false)
expect(article.title_change_to_be_saved).to eq(nil)
expect(article.title_in_database).to eq("bar")
end
end
end
end

Expand Down Expand Up @@ -252,7 +346,6 @@ def clear_changes_information
end
end


describe "resetting original values hash on actions" do
shared_examples_for "resets on model action" do |action|
it "resets changes when model on #{action}" do
Expand All @@ -279,4 +372,23 @@ def clear_changes_information
it_behaves_like "resets on model action", :save
it_behaves_like "resets on model action", :reload
end

if ENV['RAILS_VERSION'].present? && ENV['RAILS_VERSION'] > '5.0'
describe "#saved_changes" do
it "includes translated attributes" do
article = Article.create

article.title = "foo en"
Mobility.with_locale(:ja) { article.title = "foo ja" }
article.save

aggregate_failures do
saved_changes = article.saved_changes
expect(saved_changes).to include("title_en", "title_ja")
expect(saved_changes["title_en"]).to eq([nil, "foo en"])
expect(saved_changes["title_ja"]).to eq([nil, "foo ja"])
end
end
end
end
end if Mobility::Loaded::ActiveRecord