diff --git a/.gitignore b/.gitignore index 3e5e9e6..71f3ce4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ Gemfile.lock pkg/ +gemfiles/*.lock +gemfiles/.bundle/* diff --git a/Appraisals b/Appraisals new file mode 100644 index 0000000..3a86e9b --- /dev/null +++ b/Appraisals @@ -0,0 +1,7 @@ +appraise "mongoid-5" do + gem "mongoid", "~> 5" +end + +appraise "mongoid-6" do + gem "mongoid", "~> 6" +end diff --git a/README.md b/README.md index b5b59ea..5567831 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,14 @@ You can run the tests with the Rake task: $ bundle exec rake test ``` +Also, to run tests against older versions of Mongoid: + +``` +$ bundle exec appraisal install +$ bundle exec appraisal mongoid-5 rake test +$ bundle exec appraisal mongoid-6 rake test +``` + ## License [MIT](LICENSE.txt) diff --git a/gemfiles/mongoid_5.gemfile b/gemfiles/mongoid_5.gemfile new file mode 100644 index 0000000..eeee850 --- /dev/null +++ b/gemfiles/mongoid_5.gemfile @@ -0,0 +1,8 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "pry" +gem "mongoid", "~> 5" + +gemspec path: "../" diff --git a/gemfiles/mongoid_6.gemfile b/gemfiles/mongoid_6.gemfile new file mode 100644 index 0000000..fa24f00 --- /dev/null +++ b/gemfiles/mongoid_6.gemfile @@ -0,0 +1,8 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "pry" +gem "mongoid", "~> 6" + +gemspec path: "../" diff --git a/lib/shrine/plugins/mongoid.rb b/lib/shrine/plugins/mongoid.rb index 5295cb6..c371188 100644 --- a/lib/shrine/plugins/mongoid.rb +++ b/lib/shrine/plugins/mongoid.rb @@ -6,6 +6,7 @@ module Mongoid def self.configure(uploader, opts = {}) uploader.opts[:mongoid_callbacks] = opts.fetch(:callbacks, uploader.opts.fetch(:mongoid_callbacks, true)) uploader.opts[:mongoid_validations] = opts.fetch(:validations, uploader.opts.fetch(:mongoid_validations, true)) + Shrine::Plugins.load_plugin(:backgrounding) end module AttachmentMethods @@ -47,15 +48,111 @@ module AttacherClassMethods def find_record(record_class, record_id) record_class.where(id: record_id).first end + + def load_record(data) + return super unless data["parent_record"] + + _record_class, record_id = data["record"] + + parent_class, parent_id, relation_name = data["parent_record"] + + parent_class = Object.const_get(parent_class) + parent = load_parent_record(parent_class, parent_id) + + find_embedded_record(parent, relation_name, record_id) + end + + private + + def load_parent_record(parent_class, parent_id) + find_record(parent_class, parent_id) || + parent_class.new do |instance| + # so that the id is always included in file deletion logs + instance.singleton_class.send(:define_method, :id) { parent_id } + end + end + + def find_embedded_record(parent, relation_name, record_id) + if parent.public_send(relation_name).is_a?(Array) + find_embedded_record_in_collection(parent, relation_name, record_id) + else + find_single_embedded_record(parent, relation_name, record_id) + end + end + + def find_single_embedded_record(parent, relation_name, record_id) + # NOTE: perhaps it's good to check if record_id matches existing one + parent.public_send(relation_name) || + parent.public_send("build_#{relation_name}") do |instance| + # so that the id is always included in file deletion logs + instance.singleton_class.send(:define_method, :id) { record_id } + end + end + + def find_embedded_record_in_collection(parent, relation_name, record_id) + relation = parent.public_send(relation_name) + relation.where(id: record_id).first || + relation.build do |instance| + # so that the id is always included in file deletion logs + instance.singleton_class.send(:define_method, :id) { record_id } + end + end end module AttacherMethods + MONGOID_ASSOCIATION_NAME_METHOD = + ::Mongoid::VERSION[0] > "6" ? "association_name" : "metadata_name" + + def dump + hash = super + if record.embedded? + hash["parent_record"] = [ + record._parent.class.to_s, + record._parent.id.to_s, + record_association_name + ] + end + hash + end + + def swap(new_file) + return super unless record.embedded? + parent = record._parent + + # NOTE: We check if parent is persisted, just in case it was + # not found and initialized in place, so there will be + # no exception on `reload` + if backgrounding_mode? && parent.persisted? + relation = parent.reload.send(record_association_name) + reloaded = if relation.is_a?(Array) + relation.where(id: record.id).first + else + relation + end + return if reloaded.nil? || + self.class.new(reloaded, name).read != read + end + update(new_file) + new_file if new_file == get + end + private - # We save the record after updating, raising any validation errors. + def backgrounding_mode? + self.class.ancestors.include?( + Shrine::Plugins::Backgrounding::AttacherMethods + ) + end + + def record_association_name + record.public_send(MONGOID_ASSOCIATION_NAME_METHOD).to_s + end + + # We save the record after updating, raising any validation errors, + # unless it is embedded record of not yet saved parent record. def update(uploaded_file) super - record.save! + record.save! unless record.embedded? && record._parent.new_record? end def convert_before_write(value) diff --git a/shrine-mongoid.gemspec b/shrine-mongoid.gemspec index d93ee64..b335261 100644 --- a/shrine-mongoid.gemspec +++ b/shrine-mongoid.gemspec @@ -20,4 +20,5 @@ Gem::Specification.new do |gem| gem.add_development_dependency "rake" gem.add_development_dependency "minitest" gem.add_development_dependency "mocha" + gem.add_development_dependency "appraisal" end diff --git a/test/mongoid_test.rb b/test/mongoid_test.rb index 95e589d..70eb73a 100644 --- a/test/mongoid_test.rb +++ b/test/mongoid_test.rb @@ -20,7 +20,7 @@ end after do - User.delete_all + User.destroy_all Object.send(:remove_const, "User") end @@ -142,7 +142,7 @@ end it "returns nil when record is not found" do - assert_equal nil, @attacher.class.find_record(@user.class, "foo") + assert_nil @attacher.class.find_record(@user.class, "foo") end it "raises an appropriate exception when column is missing" do @@ -155,4 +155,150 @@ klass = Struct.new(:avatar_data) klass.include @uploader.class::Attachment.new(:avatar) end + + describe "backgrounding for embedded records" do + before do + @uploader = uploader do + plugin :backgrounding + plugin :mongoid + end + + EmbeddedDocument = Class.new { + include Mongoid::Document + embedded_in :user + field :title, type: String + field :file_data, type: String + } + EmbeddedDocument.include @uploader.class::Attachment.new(:file) + + User.embeds_many :embedded_documents + User.embeds_one :passport, class_name: "EmbeddedDocument" + + @user.save + @embedded_document = @user.embedded_documents.create(file: fakeio) + @embedded_document_attacher = @embedded_document.file_attacher + + @passport = @user.create_passport(file: fakeio("passport")) + @passport_attacher = @passport.file_attacher + end + + after do + Object.send(:remove_const, "EmbeddedDocument") + end + + describe "Attacher.load_record" do + + it "initializes new instance when no parent_record given" do + assert @embedded_document.persisted? + loaded_record = @embedded_document_attacher.class.load_record( + "record" => ["EmbeddedDocument", @embedded_document.id.to_s] + ) + assert loaded_record.new_record? + assert loaded_record != @embedded_document + + assert loaded_record.id == @embedded_document.id.to_s + end + + it "initializes new parent instance when parent_record does not exist" do + loaded_record = @embedded_document_attacher.class.load_record( + "record" => ["EmbeddedDocument", @embedded_document.id.to_s], + "parent_record" => ["User", "non-existing-id", "embedded_documents"] + ) + assert loaded_record != @embedded_document + assert loaded_record.user.new_record? + assert loaded_record.user != @user + + assert loaded_record.user.id == "non-existing-id" + end + + it "finds embedded record when parent_record given" do + loaded_record = @embedded_document_attacher.class.load_record( + "record" => ["EmbeddedDocument", @embedded_document.id.to_s], + "parent_record" => ["User", @user.id.to_s, "embedded_documents"] + ) + assert @embedded_document == loaded_record + end + + it "finds embedded record when parent_record given for embeds_one" do + loaded_record = @embedded_document_attacher.class.load_record( + "record" => ["EmbeddedDocument", @embedded_document.id.to_s], + "parent_record" => ["User", @user.id.to_s, "passport"] + ) + assert @passport == loaded_record + end + end + + describe "Attacher#dump" do + it "includes parent_record for embedded records" do + assert @embedded_document_attacher.dump["parent_record"] == + ["User", @user.id.to_s, "embedded_documents"] + assert @passport_attacher.dump["parent_record"] == + ["User", @user.id.to_s, "passport"] + end + end + end + + describe "nested attributes support" do + before do + Photo = Class.new { + include Mongoid::Document + field :title, type: String + field :image_data, type: Hash + } + Photo.include @uploader.class::Attachment.new(:image) + end + + after do + Object.send(:remove_const, "Photo") + end + + describe "for referenced models" do + before do + Photo.store_in collection: "photos" + Photo.belongs_to :user + User.has_many :photos, dependent: :destroy + User.accepts_nested_attributes_for :photos, allow_destroy: true + end + + it "stores files for nested models" do + user = User.create!(name: "Moe") + user.update!(photos_attributes: [{ image: fakeio }]) + photo = user.photos.first + assert photo.image_data["storage"] == "store" + end + + describe "and not yet existing parent" do + it "stores files for nested models" do + user = + User.create!(name: "Moe", photos_attributes: [{ image: fakeio }]) + photo = user.photos.first + assert photo.image_data["storage"] == "store" + end + end + end + + describe "for embedded models" do + before do + Photo.embedded_in :user + User.embeds_many :photos, cascade_callbacks: true + User.accepts_nested_attributes_for :photos, allow_destroy: true + end + + it "stores files for nested models" do + user = User.create!(name: "Jacob") + user.update!(photos_attributes: [{ image: fakeio }]) + photo = user.photos.first + assert photo.image_data["storage"] == "store" + end + + describe "and not yet existing parent" do + it "stores files for nested models" do + user = + User.create!(name: "Moe", photos_attributes: [{ image: fakeio }]) + photo = user.photos.first + assert photo.image_data["storage"] == "store" + end + end + end + end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 6325712..2786630 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -2,7 +2,7 @@ require "minitest/autorun" require "minitest/pride" -require "mocha/mini_test" +require "mocha/minitest" require "shrine" require "shrine/storage/memory"