From b0a35a987ddacaf3e3ce9eb8be5db9e3f5ea88a8 Mon Sep 17 00:00:00 2001 From: Justin Coyne Date: Fri, 3 Jan 2014 14:33:37 -0600 Subject: [PATCH] Add scoping. Remove metaprogramming. This brings ActiveFedora into parity with a more recent version of ActiveRecord --- lib/active_fedora.rb | 23 +++++++- lib/active_fedora/association_relation.rb | 18 ++++++ lib/active_fedora/associations.rb | 1 + lib/active_fedora/associations/association.rb | 25 +++++++- .../associations/association_scope.rb | 33 +++++++++++ .../associations/collection_association.rb | 20 +++++++ .../associations/collection_proxy.rb | 10 +--- lib/active_fedora/base.rb | 1 + lib/active_fedora/core.rb | 8 +++ lib/active_fedora/null_relation.rb | 7 +++ lib/active_fedora/querying.rb | 17 +----- lib/active_fedora/reflection.rb | 8 ++- lib/active_fedora/relation.rb | 58 +++++++++++-------- lib/active_fedora/relation/delegation.rb | 12 ++++ lib/active_fedora/relation/finder_methods.rb | 15 +++++ lib/active_fedora/relation/merger.rb | 21 +++++++ lib/active_fedora/relation/query_methods.rb | 54 +++++++++++++++++ lib/active_fedora/relation/spawn_methods.rb | 46 +++++++++++++++ lib/active_fedora/scoping.rb | 20 +++++++ lib/active_fedora/scoping/default.rb | 38 ++++++++++++ lib/active_fedora/scoping/named.rb | 32 ++++++++++ spec/integration/associations_spec.rb | 4 -- spec/integration/model_spec.rb | 2 +- spec/integration/scoped_query_spec.rb | 2 +- ...has_and_belongs_to_many_collection_spec.rb | 33 ++++++++--- spec/unit/has_many_collection_spec.rb | 20 ++++++- 26 files changed, 458 insertions(+), 70 deletions(-) create mode 100644 lib/active_fedora/association_relation.rb create mode 100644 lib/active_fedora/associations/association_scope.rb create mode 100644 lib/active_fedora/null_relation.rb create mode 100644 lib/active_fedora/relation/delegation.rb create mode 100644 lib/active_fedora/relation/finder_methods.rb create mode 100644 lib/active_fedora/relation/merger.rb create mode 100644 lib/active_fedora/relation/query_methods.rb create mode 100644 lib/active_fedora/relation/spawn_methods.rb create mode 100644 lib/active_fedora/scoping.rb create mode 100644 lib/active_fedora/scoping/default.rb create mode 100644 lib/active_fedora/scoping/named.rb diff --git a/lib/active_fedora.rb b/lib/active_fedora.rb index 83c96589a..4fad6805f 100644 --- a/lib/active_fedora.rb +++ b/lib/active_fedora.rb @@ -26,6 +26,7 @@ class IllegalOperation < RuntimeError; end # :nodoc: eager_autoload do + autoload :AssociationRelation autoload :Associations autoload :Attributes autoload :Auditable @@ -47,6 +48,7 @@ class IllegalOperation < RuntimeError; end # :nodoc: autoload :NestedAttributes autoload :NomDatastream autoload :NtriplesRDFDatastream + autoload :NullRelation autoload :OmDatastream autoload :Property autoload :Persistence @@ -60,8 +62,17 @@ class IllegalOperation < RuntimeError; end # :nodoc: autoload :RdfxmlRDFDatastream autoload :Reflection autoload :Relation + + autoload_under 'relation' do + autoload :Delegation + autoload :SpawnMethods + autoload :QueryMethods + autoload :FinderMethods + end + autoload :RelationshipGraph autoload :RelsExtDatastream + autoload :Scoping autoload :SemanticNode autoload :ServiceDefinitions autoload :Serialization @@ -77,7 +88,17 @@ class IllegalOperation < RuntimeError; end # :nodoc: autoload :Validations autoload :SolrInstanceLoader end - + + module Scoping + extend ActiveSupport::Autoload + + eager_autoload do + autoload :Default + autoload :Named + end + end + + include Loggable diff --git a/lib/active_fedora/association_relation.rb b/lib/active_fedora/association_relation.rb new file mode 100644 index 000000000..d42ca6e8b --- /dev/null +++ b/lib/active_fedora/association_relation.rb @@ -0,0 +1,18 @@ +module ActiveFedora + class AssociationRelation < Relation + def initialize(klass, association) + super(klass) + @association = association + end + + def proxy_association + @association + end + + private + + def exec_queries + super.each { |r| @association.set_inverse_instance r } + end + end +end diff --git a/lib/active_fedora/associations.rb b/lib/active_fedora/associations.rb index cbb384fe4..d761d0ca2 100644 --- a/lib/active_fedora/associations.rb +++ b/lib/active_fedora/associations.rb @@ -6,6 +6,7 @@ module Associations extend ActiveSupport::Concern autoload :Association, 'active_fedora/associations/association' + autoload :AssociationScope, 'active_fedora/associations/association_scope' autoload :SingularAssociation, 'active_fedora/associations/singular_association' autoload :CollectionAssociation, 'active_fedora/associations/collection_association' autoload :CollectionProxy, 'active_fedora/associations/collection_proxy' diff --git a/lib/active_fedora/associations/association.rb b/lib/active_fedora/associations/association.rb index e7327f1b2..ca13aa821 100644 --- a/lib/active_fedora/associations/association.rb +++ b/lib/active_fedora/associations/association.rb @@ -61,9 +61,21 @@ def target=(target) loaded! end - # def scoped - # target_scope.merge(@association_scope) - # end + def scope + target_scope.merge(association_scope) + end + + # The scope for this association. + # + # Note that the association_scope is merged into the target_scope only when the + # scope method is called. This is because at that point the call may be surrounded + # by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which + # actually gets built. + def association_scope + if klass + @association_scope ||= AssociationScope.new(self).scope + end + end # Set the inverse association, if possible def set_inverse_instance(record) @@ -73,6 +85,13 @@ def set_inverse_instance(record) end end + + # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the + # through association's scope) + def target_scope + klass.all + end + # Loads the \target if needed and returns it. # # This method is abstract in the sense that it relies on +find_target+, diff --git a/lib/active_fedora/associations/association_scope.rb b/lib/active_fedora/associations/association_scope.rb new file mode 100644 index 000000000..a0a7c1395 --- /dev/null +++ b/lib/active_fedora/associations/association_scope.rb @@ -0,0 +1,33 @@ +module ActiveFedora + module Associations + class AssociationScope #:nodoc: + + attr_reader :association + + delegate :klass, :owner, :reflection, :interpolate, :to => :association + delegate :chain, :scope_chain, :options, :source_options, :active_record, :to => :reflection + + def initialize(association) + @association = association + end + + + def scope + scope = klass.unscoped + add_constraints(scope) + end + + private + def add_constraints(scope) + chain.each_with_index do |reflection, i| + is_first_chain = i == 0 + klass = is_first_chain ? self.klass : reflection.klass + scope.where_values[options[:property]] = owner.id + end + + scope + end + + end + end +end diff --git a/lib/active_fedora/associations/collection_association.rb b/lib/active_fedora/associations/collection_association.rb index c4f2c8e4a..d377533c2 100644 --- a/lib/active_fedora/associations/collection_association.rb +++ b/lib/active_fedora/associations/collection_association.rb @@ -92,6 +92,17 @@ def replace(other_array) concat(other_array.select { |v| !current.include?(v) }) end + def include?(record) + if record.is_a?(reflection.klass) + if record.new_record? + target.include?(record) + else + loaded? ? target.include?(record) : scope.exists?(record) + end + else + false + end + end def to_ary load_target.dup @@ -273,6 +284,15 @@ def add_to_target(record) record end + def scope(opts = {}) + scope = super() + scope.none! if opts.fetch(:nullify, true) && null_scope? + scope + end + + def null_scope? + owner.new_record? && !foreign_key_present? + end protected def reset_target! @target = Array.new diff --git a/lib/active_fedora/associations/collection_proxy.rb b/lib/active_fedora/associations/collection_proxy.rb index cbee2e89a..8f71444f0 100644 --- a/lib/active_fedora/associations/collection_proxy.rb +++ b/lib/active_fedora/associations/collection_proxy.rb @@ -33,12 +33,7 @@ module Associations # # is computed directly through SQL and does not trigger by itself the # instantiation of the actual post records. - class CollectionProxy # :nodoc: - alias :proxy_extend :extend - - #TODO remove using: https://github.com/rails/rails/commit/c86a32d7451c5d901620ac58630460915292f88b#diff-bee622c17de67f292adf310f75288e25 - instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ } - + class CollectionProxy < Relation # :nodoc: delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :to => :scoped @@ -54,7 +49,8 @@ class CollectionProxy # :nodoc: def initialize(association) @association = association - Array(association.options[:extend]).each { |ext| proxy_extend(ext) } + super association.klass + merge! association.scope end def respond_to?(*args) diff --git a/lib/active_fedora/base.rb b/lib/active_fedora/base.rb index db17a2620..ddc3e937a 100644 --- a/lib/active_fedora/base.rb +++ b/lib/active_fedora/base.rb @@ -29,6 +29,7 @@ class Base include SemanticNode include Sharding include ActiveFedora::Persistence + include Scoping include Loggable include Indexing include ActiveModel::Conversion diff --git a/lib/active_fedora/core.rb b/lib/active_fedora/core.rb index f7e587951..982457be1 100644 --- a/lib/active_fedora/core.rb +++ b/lib/active_fedora/core.rb @@ -1,5 +1,7 @@ module ActiveFedora module Core + extend ActiveSupport::Concern + attr_reader :inner_object # Constructor. You may supply a custom +:pid+, or we call the Fedora Rest API for the @@ -139,5 +141,11 @@ def reify! self.init_with DigitalObject.find(self.class,self.pid) end + module ClassMethods + private + def relation + Relation.new(self) + end + end end end diff --git a/lib/active_fedora/null_relation.rb b/lib/active_fedora/null_relation.rb new file mode 100644 index 000000000..11a6e2e0e --- /dev/null +++ b/lib/active_fedora/null_relation.rb @@ -0,0 +1,7 @@ +module ActiveFedora + module NullRelation # :nodoc: + def exists?(_id = false) + false + end + end +end diff --git a/lib/active_fedora/querying.rb b/lib/active_fedora/querying.rb index 9ba920ef7..6325839f5 100644 --- a/lib/active_fedora/querying.rb +++ b/lib/active_fedora/querying.rb @@ -1,16 +1,11 @@ module ActiveFedora module Querying - delegate :find, :first, :where, :limit, :order, :all, :delete_all, :destroy_all, :count, :last, :to=>:relation + delegate :find, :first, :exists?, :where, :limit, :order, :delete_all, :destroy_all, :count, :last, :to=>:all def self.extended(base) base.class_attribute :solr_query_handler base.solr_query_handler = 'standard' end - - - def relation - Relation.new(self) - end # Yields each batch of solr records that was found by the find +options+ as # an array. The size of each batch is set by the :batch_size @@ -73,16 +68,6 @@ def find_each( conditions={}, opts={}) end - # Returns true if the pid exists in the repository - # @param[String] pid - # @return[boolean] - def exists?(pid) - return false if pid.nil? || pid.empty? - !!DigitalObject.find(self, pid) - rescue ActiveFedora::ObjectNotFoundError - false - end - # Returns a solr result matching the supplied conditions # @param[Hash,String] conditions can either be specified as a string, or # hash representing the query part of an solr statement. If a hash is diff --git a/lib/active_fedora/reflection.rb b/lib/active_fedora/reflection.rb index 2c0ec290b..328bcf5ce 100644 --- a/lib/active_fedora/reflection.rb +++ b/lib/active_fedora/reflection.rb @@ -165,9 +165,11 @@ def foreign_key @foreign_key ||= options[:foreign_key] || derive_foreign_key end - # def primary_key_name - # @primary_key_name ||= options[:foreign_key] || derive_primary_key_name - # end + # A chain of reflections from this one back to the owner. For more see the explanation in + # ThroughReflection. + def chain + [self] + end def inverse_of return unless inverse_name diff --git a/lib/active_fedora/relation.rb b/lib/active_fedora/relation.rb index 5e372c3b8..4d10806d9 100644 --- a/lib/active_fedora/relation.rb +++ b/lib/active_fedora/relation.rb @@ -1,17 +1,39 @@ module ActiveFedora class Relation - delegate :map, :each, :collect, :all?, :include?, :to => :to_a + + include Delegation, SpawnMethods, QueryMethods, FinderMethods attr_reader :loaded + attr_accessor :default_scoped alias :loaded? :loaded - - attr_accessor :limit_value, :where_values, :order_values - def initialize(klass) + attr_accessor :values, :klass + + def initialize(klass, values = {}) @klass = klass @loaded = false - self.where_values = [] - self.order_values = [] + @values = {} + end + + # Tries to create a new record with the same scoped attributes + # defined in the relation. Returns the initialized object if validation fails. + # + # Expects arguments in the same format as +Base.create+. + # + # ==== Examples + # users = User.where(name: 'Oscar') + # users.create # # + # + # users.create(name: 'fxn') + # users.create # # + # + # users.create { |user| user.name = 'tenderlove' } + # # # + # + # users.create(name: nil) # validation on name + # # # + def create(*args, &block) + scoping { @klass.create(*args, &block) } end def reset @@ -115,7 +137,7 @@ def find(*args) if options.present? options = args.first unless args.empty? options = {conditions: options} - apply_finder_options(options).all + apply_finder_options(options) else raise ArgumentError, "#{self}.find() expects a pid. You provided `#{args.inspect}'" unless args.is_a? Array find_with_ids(args, cast) @@ -143,22 +165,13 @@ def find_some(ids, cast) ids.map{|id| @klass.find_one(id, cast)} end - # A convenience wrapper for find(:all, *args). You can pass in all the - # same arguments to this method as you can to find(:all). - def all(*args) - args.any? ? apply_finder_options(args.first).to_a : to_a - end - - - def to_a return @records if loaded? args = @klass == ActiveFedora::Base ? {:cast=>true} : {} - args[:rows] = @limit_value if @limit_value - args[:sort] = @order_values if @order_values + args[:rows] = limit_value if limit_value + args[:sort] = order_values if order_values - query = @where_values.present? ? @where_values : {} - @records = @klass.to_enum(:find_each, query, args).to_a + @records = @klass.to_enum(:find_each, where_values, args).to_a @records end @@ -168,11 +181,10 @@ def to_a def count(*args) return apply_finder_options(args.first).count if args.any? opts = {} - opts[:rows] = @limit_value if @limit_value - opts[:sort] = @order_values if @order_values + opts[:rows] = limit_value if limit_value + opts[:sort] = order_values if order_values - query = @where_values.present? ? @where_values : {} - @klass.calculate :count, query, opts + @klass.calculate :count, where_values, opts end diff --git a/lib/active_fedora/relation/delegation.rb b/lib/active_fedora/relation/delegation.rb new file mode 100644 index 000000000..064160dca --- /dev/null +++ b/lib/active_fedora/relation/delegation.rb @@ -0,0 +1,12 @@ +module ActiveFedora + module Delegation # :nodoc: + extend ActiveSupport::Concern + + # This module creates compiled delegation methods dynamically at runtime, which makes + # subsequent calls to that method faster by avoiding method_missing. The delegations + # may vary depending on the klass of a relation, so we create a subclass of Relation + # for each different klass, and the delegations are compiled into that subclass only. + + delegate :length, :collect, :map, :each, :all?, :include?, :to_ary, :to => :to_a + end +end diff --git a/lib/active_fedora/relation/finder_methods.rb b/lib/active_fedora/relation/finder_methods.rb new file mode 100644 index 000000000..2a033c76e --- /dev/null +++ b/lib/active_fedora/relation/finder_methods.rb @@ -0,0 +1,15 @@ +module ActiveFedora + module FinderMethods + # Returns true if the pid exists in the repository + # @param[String] pid + # @return[boolean] + def exists?(conditions) + conditions = conditions.id if Base === conditions + return false if !conditions + !!DigitalObject.find(self.klass, conditions) + rescue ActiveFedora::ObjectNotFoundError + false + end + + end +end diff --git a/lib/active_fedora/relation/merger.rb b/lib/active_fedora/relation/merger.rb new file mode 100644 index 000000000..080c87a62 --- /dev/null +++ b/lib/active_fedora/relation/merger.rb @@ -0,0 +1,21 @@ +module ActiveFedora + class Relation + class HashMerger # :nodoc: + end + + class Merger # :nodoc: + attr_reader :relation, :values, :other + + def initialize(relation, other) + @relation = relation + @values = other.values + @other = other + end + + def merge + # TODO merge where and order + relation + end + end + end +end diff --git a/lib/active_fedora/relation/query_methods.rb b/lib/active_fedora/relation/query_methods.rb new file mode 100644 index 000000000..9cc53dd3a --- /dev/null +++ b/lib/active_fedora/relation/query_methods.rb @@ -0,0 +1,54 @@ +module ActiveFedora + module QueryMethods # :nodoc: + + def extending_values + @values[:extending] || [] + end + + def extending_values=(values) + raise ImmutableRelation if @loaded + @values[:extending] = values + end + + def where_values + @values[:where] || {} + end + + def where_values=(values) + raise ImmutableRelation if @loaded + @values[:where] = values + end + + def order_values + @values[:order] || [] + end + + def order_values=(values) + raise ImmutableRelation if @loaded + @values[:order] = values + end + + def limit_value + @values[:limit] + end + + def limit_value=(value) + raise ImmutableRelation if @loaded + @values[:limit] = value + end + + def none! # :nodoc: + extending!(NullRelation) + end + + def extending!(*modules, &block) # :nodoc: + modules << Module.new(&block) if block + modules.flatten! + + self.extending_values += modules + extend(*extending_values) if extending_values.any? + + self + end + end +end diff --git a/lib/active_fedora/relation/spawn_methods.rb b/lib/active_fedora/relation/spawn_methods.rb new file mode 100644 index 000000000..4bed3ee10 --- /dev/null +++ b/lib/active_fedora/relation/spawn_methods.rb @@ -0,0 +1,46 @@ +require 'active_fedora/relation/merger' + +module ActiveFedora + module SpawnMethods + + # This is overridden by Associations::CollectionProxy + def spawn #:nodoc: + clone + end + + # Merges in the conditions from other, if other is an ActiveRecord::Relation. + # Returns an array representing the intersection of the resulting records with other, if other is an array. + # Post.where(published: true).joins(:comments).merge( Comment.where(spam: false) ) + # # Performs a single join query with both where conditions. + # + # recent_posts = Post.order('created_at DESC').first(5) + # Post.where(published: true).merge(recent_posts) + # # Returns the intersection of all published posts with the 5 most recently created posts. + # # (This is just an example. You'd probably want to do this with a single query!) + # + # Procs will be evaluated by merge: + # + # Post.where(published: true).merge(-> { joins(:comments) }) + # # => Post.where(published: true).joins(:comments) + # + # This is mainly intended for sharing common conditions between multiple associations. + def merge(other) + if other.is_a?(Array) + to_a & other + elsif other + spawn.merge!(other) + else + self + end + end + + def merge!(other) # :nodoc: + if !other.is_a?(Relation) && other.respond_to?(:to_proc) + instance_exec(&other) + else + klass = other.is_a?(Hash) ? Relation::HashMerger : Relation::Merger + klass.new(self, other).merge + end + end + end +end diff --git a/lib/active_fedora/scoping.rb b/lib/active_fedora/scoping.rb new file mode 100644 index 000000000..4097da496 --- /dev/null +++ b/lib/active_fedora/scoping.rb @@ -0,0 +1,20 @@ +module ActiveFedora + module Scoping + extend ActiveSupport::Concern + included do + include Default + include Named + end + + + module ClassMethods + def current_scope #:nodoc: + Thread.current["#{self}_current_scope"] + end + + def current_scope=(scope) #:nodoc: + Thread.current["#{self}_current_scope"] = scope + end + end + end +end diff --git a/lib/active_fedora/scoping/default.rb b/lib/active_fedora/scoping/default.rb new file mode 100644 index 000000000..2d83aec61 --- /dev/null +++ b/lib/active_fedora/scoping/default.rb @@ -0,0 +1,38 @@ +module ActiveFedora + module Scoping + module Default + extend ActiveSupport::Concern + + module ClassMethods + # Returns a scope for the model without the +default_scope+. + # + # class Post < ActiveRecord::Base + # def self.default_scope + # where published: true + # end + # end + # + # Post.all # Fires "SELECT * FROM posts WHERE published = true" + # Post.unscoped.all # Fires "SELECT * FROM posts" + # + # This method also accepts a block. All queries inside the block will + # not use the +default_scope+: + # + # Post.unscoped { + # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10" + # } + # + # It is recommended that you use the block form of unscoped because + # chaining unscoped with +scope+ does not work. Assuming that + # +published+ is a +scope+, the following two statements + # are equal: the +default_scope+ is applied on both. + # + # Post.unscoped.published + # Post.published + def unscoped + block_given? ? relation.scoping { yield } : relation + end + end + end + end +end diff --git a/lib/active_fedora/scoping/named.rb b/lib/active_fedora/scoping/named.rb new file mode 100644 index 000000000..6ed9e7745 --- /dev/null +++ b/lib/active_fedora/scoping/named.rb @@ -0,0 +1,32 @@ +module ActiveFedora + # = Active Fedora \Named \Scopes + module Scoping + module Named + extend ActiveSupport::Concern + + module ClassMethods + # Returns an ActiveFedora::Relation scope object. + # + # posts = Post.all + # posts.size # Fires "select count(*) from posts" and returns the count + # posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects + # + # fruits = Fruit.all + # fruits = fruits.where(color: 'red') if options[:red_only] + # fruits = fruits.limit(10) if limited? + # + # You can define a scope that applies to all finders using + # ActiveRecord::Base.default_scope. + def all + if current_scope + current_scope.clone + else + scope = relation + scope.default_scoped = true + scope + end + end + end + end + end +end diff --git a/spec/integration/associations_spec.rb b/spec/integration/associations_spec.rb index fc96f5629..6ae21d02c 100644 --- a/spec/integration/associations_spec.rb +++ b/spec/integration/associations_spec.rb @@ -215,10 +215,6 @@ class Publisher < ActiveFedora::Base @library.books << @book << Book.create @library.save - # @book.library.pid.should == @library.pid - # @library.books.reload - # @library.books.should == [@book] - @library2 = Library.find(@library.pid) @library2.books.size.should == 2 end diff --git a/spec/integration/model_spec.rb b/spec/integration/model_spec.rb index 6c5f27dda..573545a52 100644 --- a/spec/integration/model_spec.rb +++ b/spec/integration/model_spec.rb @@ -28,7 +28,7 @@ class Basic < Base describe "#all" do it "should return an array of instances of the calling Class" do - result = ModelIntegrationSpec::Basic.all + result = ModelIntegrationSpec::Basic.all.to_a result.should be_instance_of(Array) # this test is meaningless if the array length is zero result.length.should > 0 diff --git a/spec/integration/scoped_query_spec.rb b/spec/integration/scoped_query_spec.rb index d86c9f579..21ed046b5 100644 --- a/spec/integration/scoped_query_spec.rb +++ b/spec/integration/scoped_query_spec.rb @@ -38,7 +38,7 @@ def to_solr(doc = {}) describe ".all" do it "should return an array of instances of the calling Class" do - result = ModelIntegrationSpec::Basic.all + result = ModelIntegrationSpec::Basic.all.to_a result.should be_instance_of(Array) # this test is meaningless if the array length is zero result.length.should > 0 diff --git a/spec/unit/has_and_belongs_to_many_collection_spec.rb b/spec/unit/has_and_belongs_to_many_collection_spec.rb index 5aaa1cd09..a2f4700e1 100644 --- a/spec/unit/has_and_belongs_to_many_collection_spec.rb +++ b/spec/unit/has_and_belongs_to_many_collection_spec.rb @@ -1,13 +1,27 @@ require 'spec_helper' describe ActiveFedora::Associations::HasAndBelongsToManyAssociation do + before do + class Book < ActiveFedora::Base + end + class Page < ActiveFedora::Base + end + end + + after do + Object.send(:remove_const, :Book) + Object.send(:remove_const, :Page) + end + it "should call add_relationship" do - subject = double("subject", :new_record? => false, :pid => 'subject:a', :internal_uri => 'info:fedora/subject:a', :ids_for_outbound => []) - predicate = double(:klass => double.class, :options=>{:property=>'predicate'}, :class_name=> nil) + subject = Book.new(pid: 'subject:a') + subject.stub(:new_record? => false, save: true) + predicate = Book.create_reflection(:has_and_belongs_to_many, 'pages', {:property=>'predicate'}, nil) ActiveFedora::SolrService.stub(:query).and_return([]) ac = ActiveFedora::Associations::HasAndBelongsToManyAssociation.new(subject, predicate) ac.should_receive(:callback).twice - object = double("object", :new_record? => false, :pid => 'object:b', :save => nil) + object = Page.new(:pid => 'object:b') + object.stub(:new_record? => false, save: true) subject.should_receive(:add_relationship).with('predicate', object) @@ -16,12 +30,14 @@ end it "should call add_relationship on subject and object when inverse_of given" do - subject = double("subject", :new_record? => false, :pid => 'subject:a', :internal_uri => 'info:fedora/subject:a', :ids_for_outbound => []) - predicate = double(:klass => double.class, :options=>{:property=>'predicate', :inverse_of => 'inverse_predicate'}, :class_name=> nil) + subject = Book.new(pid: 'subject:a') + subject.stub(:new_record? => false, save: true) + predicate = Book.create_reflection(:has_and_belongs_to_many, 'pages', {:property=>'predicate', :inverse_of => 'inverse_predicate'}, nil) ActiveFedora::SolrService.stub(:query).and_return([]) ac = ActiveFedora::Associations::HasAndBelongsToManyAssociation.new(subject, predicate) ac.should_receive(:callback).twice - object = double("object", :new_record? => false, :pid => 'object:b', :save => nil) + object = Page.new(:pid => 'object:b') + object.stub(:new_record? => false, save: true) subject.should_receive(:add_relationship).with('predicate', object) @@ -33,8 +49,9 @@ it "should call solr query multiple times" do - subject = double("subject", :new_record? => false, :pid => 'subject:a', :internal_uri => 'info:fedora/subject:a', :ids_for_outbound => []) - predicate = double(:klass => double.class, :options=>{:property=>'predicate', solr_page_size:10}, :class_name=> nil) + subject = Book.new(pid: 'subject:a') + subject.stub(:new_record? => false, save: true) + predicate = Book.create_reflection(:has_and_belongs_to_many, 'pages', {:property=>'predicate', :solr_page_size => 10}, nil) ids = [] 0.upto(15) {|i| ids << i.to_s} query1 = ids.slice(0,10).map {|i| "_query_:\"{!raw f=id}#{i}\""}.join(" OR ") diff --git a/spec/unit/has_many_collection_spec.rb b/spec/unit/has_many_collection_spec.rb index 840868716..d29bf857c 100644 --- a/spec/unit/has_many_collection_spec.rb +++ b/spec/unit/has_many_collection_spec.rb @@ -1,13 +1,27 @@ require 'spec_helper' describe ActiveFedora::Associations::HasManyAssociation do + before do + class Book < ActiveFedora::Base + end + class Page < ActiveFedora::Base + end + end + + after do + Object.send(:remove_const, :Book) + Object.send(:remove_const, :Page) + end + it "should call add_relationship" do - subject = double("subject", :new_record? => false, :pid => 'subject:a', :internal_uri => 'info:fedora/subject:a') - predicate = double(:klass => double.class, :options=>{:property=>'predicate'}, :class_name=> nil) + subject = Book.new(pid: 'subject:a') + subject.stub(:new_record? => false, save: true) + predicate = Book.create_reflection(:has_many, 'pages', {:property=>'predicate'}, nil) ActiveFedora::SolrService.stub(:query).and_return([]) ac = ActiveFedora::Associations::HasManyAssociation.new(subject, predicate) ac.should_receive(:callback).twice - object = double("object", :new_record? => false, :pid => 'object:b', :save => nil) + object = Page.new(:pid => 'object:b') + object.stub(:new_record? => false, save: true) object.should_receive(:add_relationship).with('predicate', subject)