diff --git a/lib/shrine/plugins/mongoid.rb b/lib/shrine/plugins/mongoid.rb index 2b7e228..614b187 100644 --- a/lib/shrine/plugins/mongoid.rb +++ b/lib/shrine/plugins/mongoid.rb @@ -5,12 +5,18 @@ class Shrine module Plugins module Mongoid + VALID_FINALIZE_OPTS = [nil, :before_save, :after_save].freeze + def self.load_dependencies(uploader, *) uploader.plugin :model uploader.plugin :_persistence, plugin: self end def self.configure(uploader, **opts) + unless VALID_FINALIZE_OPTS.include?(opts[:finalize]) + fail ArgumentError, "valid finalize options: #{VALID_FINALIZE_OPTS}" + end + uploader.opts[:mongoid] ||= { validations: true, callbacks: true } uploader.opts[:mongoid].merge!(opts) end @@ -44,6 +50,10 @@ def included(model) end end + define_method :"#{name}_finalize" do + send(:"#{name}_attacher").finalize + end + define_method :reload do |*args| result = super(*args) instance_variable_set(:"@#{name}_attacher", nil) @@ -69,19 +79,22 @@ def mongoid_validate end end - # Calls Attacher#save. Called before model save. + # Calls Attacher#save and finalizes attachment if so configured. + # Called before model save. def mongoid_before_save return unless changed? save + finalize if shrine_class.opts[:mongoid][:finalize] == :before_save end - # Finalizes attachment and persists changes. Called after model save. + # Finalizes attachment if so configured. + # Makes sense when used with the backgrounding plugin. + # Called after model save. def mongoid_after_save return unless changed? - finalize - persist + finalize if shrine_class.opts[:mongoid][:finalize] == :after_save end # Deletes attached files. Called after model destroy. @@ -89,18 +102,43 @@ def mongoid_after_destroy destroy_attached end - # Saves changes to the model instance, raising exception on validation - # errors. Used by the _persistence plugin. + # Saves changes to the model instance, skipping validation. + # Used by the _persistence plugin. def mongoid_persist record.save(validate: false) end + # Internal only + def _find_root_parent(record) + parent = record._parent + return parent unless parent.embedded? + + _find_root_parent(parent) + end + + # Internal only + def _copy_record_instance(record) + copy = record.dup + copy.id = record.id + copy + end + # Yields the reloaded record. Used by the _persistence plugin. def mongoid_reload - record_copy = record.dup - record_copy.id = record.id + unless record.persisted? + return yield record + end + + unless record.embedded? + return yield _copy_record_instance(record).reload + end + + parent_copy = _copy_record_instance(_find_root_parent(record)).reload + record_copy = parent_copy._children.find do |child| + child.class == record.class && child.id == record.id + end - yield record_copy.reload + yield record_copy end # Returns true if the data attribute represents a Hash field. Used by diff --git a/test/mongoid_test.rb b/test/mongoid_test.rb index a1330ba..353f79e 100644 --- a/test/mongoid_test.rb +++ b/test/mongoid_test.rb @@ -7,7 +7,7 @@ @shrine.storages[:cache] = Shrine::Storage::Memory.new @shrine.storages[:store] = Shrine::Storage::Memory.new - @shrine.plugin :mongoid + @shrine.plugin :mongoid, finalize: :before_save user_class = Class.new user_class.include Mongoid::Document @@ -547,4 +547,140 @@ def validate end end end + + describe "child relations support" do + before do + User = @user.class + Photo = Class.new do + include Mongoid::Document + field :title, type: String + field :image_data, type: Hash + end + Photo.include @shrine::Attachment.new(:image) + end + + after do + Object.send(:remove_const, "Photo") + Object.send(:remove_const, "User") + end + + describe "nested attributes support" do + 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.reload.photos.first + assert photo.image_data["storage"] == "store" + end + + describe "with not yet existing parent" do + it "stores files for nested models" do + user = + User.create!(name: "Moe", photos_attributes: [{ image: fakeio }]) + photo = user.reload.photos.first + assert photo.image_data["storage"] == "store" + end + end + end + + describe "for embedded (many) 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.reload.photos.first + assert photo.image_data["storage"] == "store" + end + + describe "with not yet existing parent" do + it "stores files for nested models" do + user = + User.create!(name: "Moe", photos_attributes: [{ image: fakeio }]) + photo = user.reload.photos.first + assert photo.image_data["storage"] == "store" + end + end + end + + describe "for embedded (one) model" do + before do + Photo.embedded_in :user + User.embeds_one :photo, cascade_callbacks: true + User.accepts_nested_attributes_for :photo, allow_destroy: true + end + + it "stores files for nested model" do + user = User.create!(name: "Jacob") + user.update!(photo_attributes: { image: fakeio }) + photo = user.reload.photo + assert photo.image_data["storage"] == "store" + end + + describe "with not yet existing parent" do + it "stores files for nested model" do + user = + User.create!(name: "Moe", photo_attributes: { image: fakeio }) + photo = user.reload.photo + assert photo.image_data["storage"] == "store" + end + end + end + end + + describe "(embedded)" do + before do + Photo.embedded_in :user + User.embeds_one :photo, cascade_callbacks: true + end + + describe "Attacher" do + describe "#atomic_persist" do + it "persists the record" do + photo = Photo.new(user: @user) + attacher = @shrine::Attacher.from_model(photo, :image) + + file = attacher.attach(fakeio) + photo.save! + + photo.title = "me" + attacher.atomic_persist + + photo = @user.reload.photo + attacher = @shrine::Attacher.from_model(photo, :image) + + assert_equal "me", photo.title + assert_equal file, attacher.file + end + end + + describe "#atomic_promote" do + it "promotes cached file to permanent storage" do + photo = Photo.new(user: @user) + attacher = @shrine::Attacher.from_model(photo, :image) + + attacher.attach_cached(fakeio) + photo.save! + + attacher.atomic_promote + + assert attacher.stored? + attacher.reload + assert attacher.stored? + end + end + end + end + end end