active-fedora 9.10.0.pre1 → 9.10.0.pre2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,27 @@
1
+ module ActiveFedora
2
+ module FilePersistence
3
+ extend ActiveSupport::Concern
4
+
5
+ include ActiveFedora::Persistence
6
+
7
+ private
8
+
9
+ def _create_record(_options = {})
10
+ return false if content.nil?
11
+ ldp_source.content = content
12
+ ldp_source.create do |req|
13
+ req.headers.merge!(ldp_headers)
14
+ end
15
+ refresh
16
+ end
17
+
18
+ def _update_record(_options = {})
19
+ return true unless content_changed?
20
+ ldp_source.content = content
21
+ ldp_source.update do |req|
22
+ req.headers.merge!(ldp_headers)
23
+ end
24
+ refresh
25
+ end
26
+ end
27
+ end
@@ -78,11 +78,6 @@ module ActiveFedora
78
78
  rescue ActiveFedora::ObjectNotFoundError, Ldp::Gone
79
79
  ActiveTriples::Resource.new(uri)
80
80
  end
81
-
82
- # Abstract classes can't have default scopes.
83
- def abstract_class?
84
- self == Base
85
- end
86
81
  end
87
82
  end
88
83
  end
@@ -59,14 +59,14 @@ module ActiveFedora
59
59
  private
60
60
 
61
61
  # index the record after it has been persisted to Fedora
62
- def create_record(options = {})
62
+ def _create_record(options = {})
63
63
  super
64
64
  update_index if create_needs_index? && options.fetch(:update_index, true)
65
65
  true
66
66
  end
67
67
 
68
68
  # index the record after it has been updated in Fedora
69
- def update_record(options = {})
69
+ def _update_record(options = {})
70
70
  super
71
71
  update_index if update_needs_index? && options.fetch(:update_index, true)
72
72
  true
@@ -0,0 +1,34 @@
1
+ module ActiveFedora
2
+ module Inheritance
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ # Returns the class descending directly from ActiveFedora::Base, or
7
+ # an abstract class, if any, in the inheritance hierarchy.
8
+ #
9
+ # If A extends ActiveFedora::Base, A.base_class will return A. If B descends from A
10
+ # through some arbitrarily deep hierarchy, B.base_class will return A.
11
+ #
12
+ # If B < A and C < B and if A is an abstract_class then both B.base_class
13
+ # and C.base_class would return B as the answer since A is an abstract_class.
14
+ def base_class
15
+ return File if self == File || superclass == File
16
+
17
+ unless self <= Base
18
+ raise ActiveFedoraError, "#{name} doesn't belong in a hierarchy descending from ActiveFedora"
19
+ end
20
+
21
+ if self == Base || superclass == Base || superclass.abstract_class?
22
+ self
23
+ else
24
+ superclass.base_class
25
+ end
26
+ end
27
+
28
+ # Abstract classes can't have default scopes.
29
+ def abstract_class?
30
+ self == Base
31
+ end
32
+ end
33
+ end
34
+ end
@@ -33,7 +33,7 @@ module ActiveFedora
33
33
 
34
34
  # Pushes the object and all of its new or dirty attached files into Fedora
35
35
  def update(attributes)
36
- self.attributes = attributes
36
+ assign_attributes(attributes)
37
37
  save
38
38
  end
39
39
 
@@ -70,6 +70,17 @@ module ActiveFedora
70
70
  delete(*opts)
71
71
  end
72
72
 
73
+ # Deletes the record in the database and freezes this instance to reflect
74
+ # that no changes should be made (since they can't be persisted).
75
+ #
76
+ # There's a series of callbacks associated with #destroy!. If the
77
+ # <tt>before_destroy</tt> callback throws +:abort+ the action is cancelled
78
+ # and #destroy! raises ActiveFedora::RecordNotDestroyed.
79
+ # See ActiveFedora::Callbacks for further details.
80
+ def destroy!
81
+ destroy || _raise_record_not_destroyed
82
+ end
83
+
73
84
  def eradicate
74
85
  self.class.eradicate(id)
75
86
  end
@@ -138,12 +149,12 @@ module ActiveFedora
138
149
 
139
150
  def create_or_update(*args)
140
151
  raise ReadOnlyRecord if readonly?
141
- result = new_record? ? create_record(*args) : update_record(*args)
152
+ result = new_record? ? _create_record(*args) : _update_record(*args)
142
153
  result != false
143
154
  end
144
155
 
145
156
  # Deals with preparing new object to be saved to Fedora, then pushes it and its attached files into Fedora.
146
- def create_record(_options = {})
157
+ def _create_record(_options = {})
147
158
  assign_rdf_subject
148
159
  serialize_attached_files
149
160
  @ldp_source = @ldp_source.create
@@ -152,13 +163,20 @@ module ActiveFedora
152
163
  refresh
153
164
  end
154
165
 
155
- def update_record(_options = {})
166
+ def _update_record(_options = {})
156
167
  serialize_attached_files
157
168
  execute_sparql_update
158
169
  save_contained_resources
159
170
  refresh
160
171
  end
161
172
 
173
+ def _raise_record_not_destroyed
174
+ @_association_destroy_exception ||= nil
175
+ raise @_association_destroy_exception || RecordNotDestroyed.new("Failed to destroy the record", self)
176
+ ensure
177
+ @_association_destroy_exception = nil
178
+ end
179
+
162
180
  def refresh
163
181
  @ldp_source = build_ldp_resource(id)
164
182
  @resource = nil
@@ -1,6 +1,9 @@
1
+ require 'active_support/per_thread_registry'
2
+
1
3
  module ActiveFedora
2
4
  module Scoping
3
5
  extend ActiveSupport::Concern
6
+
4
7
  included do
5
8
  include Default
6
9
  include Named
@@ -8,11 +11,17 @@ module ActiveFedora
8
11
 
9
12
  module ClassMethods
10
13
  def current_scope #:nodoc:
11
- Thread.current["#{self}_current_scope"]
14
+ ScopeRegistry.value_for(:current_scope, self)
12
15
  end
13
16
 
14
17
  def current_scope=(scope) #:nodoc:
15
- Thread.current["#{self}_current_scope"] = scope
18
+ ScopeRegistry.set_value_for(:current_scope, self, scope)
19
+ end
20
+
21
+ # Collects attributes from scopes that should be applied when creating
22
+ # an AF instance for the particular class this is called on.
23
+ def scope_attributes # :nodoc:
24
+ all.scope_for_create
16
25
  end
17
26
 
18
27
  # Are there attributes associated with this scope?
@@ -20,5 +29,74 @@ module ActiveFedora
20
29
  current_scope
21
30
  end
22
31
  end
32
+
33
+ def populate_with_current_scope_attributes # :nodoc:
34
+ return unless self.class.scope_attributes?
35
+
36
+ self.class.scope_attributes.each do |att, value|
37
+ send("#{att}=", value) if respond_to?("#{att}=")
38
+ end
39
+ end
40
+
41
+ def initialize_internals_callback # :nodoc:
42
+ super
43
+ populate_with_current_scope_attributes
44
+ end
45
+
46
+ # This class stores the +:current_scope+ and +:ignore_default_scope+ values
47
+ # for different classes. The registry is stored as a thread local, which is
48
+ # accessed through +ScopeRegistry.current+.
49
+ #
50
+ # This class allows you to store and get the scope values on different
51
+ # classes and different types of scopes. For example, if you are attempting
52
+ # to get the current_scope for the +Board+ model, then you would use the
53
+ # following code:
54
+ #
55
+ # registry = ActiveFedora::Scoping::ScopeRegistry
56
+ # registry.set_value_for(:current_scope, Board, some_new_scope)
57
+ #
58
+ # Now when you run:
59
+ #
60
+ # registry.value_for(:current_scope, Board)
61
+ #
62
+ # You will obtain whatever was defined in +some_new_scope+. The #value_for
63
+ # and #set_value_for methods are delegated to the current ScopeRegistry
64
+ # object, so the above example code can also be called as:
65
+ #
66
+ # ActiveFedora::Scoping::ScopeRegistry.set_value_for(:current_scope,
67
+ # Board, some_new_scope)
68
+ class ScopeRegistry # :nodoc:
69
+ extend ActiveSupport::PerThreadRegistry
70
+
71
+ VALID_SCOPE_TYPES = [:current_scope, :ignore_default_scope].freeze
72
+
73
+ def initialize
74
+ @registry = Hash.new { |hash, key| hash[key] = {} }
75
+ end
76
+
77
+ # Obtains the value for a given +scope_type+ and +model+.
78
+ def value_for(scope_type, model)
79
+ raise_invalid_scope_type!(scope_type)
80
+ klass = model
81
+ base = model.base_class
82
+ while klass <= base
83
+ value = @registry[scope_type][klass.name]
84
+ return value if value
85
+ klass = klass.superclass
86
+ end
87
+ end
88
+
89
+ # Sets the +value+ for a given +scope_type+ and +model+.
90
+ def set_value_for(scope_type, model, value)
91
+ raise_invalid_scope_type!(scope_type)
92
+ @registry[scope_type][model.name] = value
93
+ end
94
+
95
+ private
96
+
97
+ def raise_invalid_scope_type!(scope_type)
98
+ raise ArgumentError, "Invalid scope type '#{scope_type}' sent to the registry. Scope types must be included in VALID_SCOPE_TYPES" unless VALID_SCOPE_TYPES.include?(scope_type)
99
+ end
100
+ end
23
101
  end
24
102
  end
@@ -46,6 +46,10 @@ module ActiveFedora
46
46
  super || default_scopes.any? || respond_to?(:default_scope)
47
47
  end
48
48
 
49
+ def before_remove_const #:nodoc:
50
+ self.current_scope = nil
51
+ end
52
+
49
53
  protected
50
54
 
51
55
  # Use this macro in your model to set a default scope for all operations on
@@ -125,11 +129,11 @@ module ActiveFedora
125
129
  end
126
130
 
127
131
  def ignore_default_scope? # :nodoc:
128
- Thread.current["#{self}_ignore_default_scope"]
132
+ ScopeRegistry.value_for(:ignore_default_scope, base_class)
129
133
  end
130
134
 
131
135
  def ignore_default_scope=(ignore) # :nodoc:
132
- Thread.current["#{self}_ignore_default_scope"] = ignore
136
+ ScopeRegistry.set_value_for(:ignore_default_scope, base_class, ignore)
133
137
  end
134
138
 
135
139
  # The ignore_default_scope flag is used to prevent an infinite recursion
@@ -16,7 +16,7 @@ module ActiveFedora
16
16
  # fruits = fruits.limit(10) if limited?
17
17
  #
18
18
  # You can define a scope that applies to all finders using
19
- # <tt>ActiveRecord::Base.default_scope</tt>.
19
+ # <tt>ActiveFedora::Base.default_scope</tt>.
20
20
  def all
21
21
  if current_scope
22
22
  current_scope.clone
@@ -34,6 +34,146 @@ module ActiveFedora
34
34
  relation
35
35
  end
36
36
  end
37
+
38
+ # Adds a class method for retrieving and querying objects.
39
+ # The method is intended to return an ActiveFedora::Relation
40
+ # object, which is composable with other scopes.
41
+ # If it returns nil or false, an
42
+ # {all}[rdoc-ref:Scoping::Named::ClassMethods#all] scope is returned instead.
43
+ #
44
+ # A \scope represents a narrowing of a database query, such as
45
+ # <tt>where(color: :red).select('shirts.*').includes(:washing_instructions)</tt>.
46
+ #
47
+ # class Shirt < ActiveFedora::Base
48
+ # scope :red, -> { where(color: 'red') }
49
+ # scope :dry_clean_only, -> { joins(:washing_instructions).where('washing_instructions.dry_clean_only = ?', true) }
50
+ # end
51
+ #
52
+ # The above calls to #scope define class methods <tt>Shirt.red</tt> and
53
+ # <tt>Shirt.dry_clean_only</tt>. <tt>Shirt.red</tt>, in effect,
54
+ # represents the query <tt>Shirt.where(color: 'red')</tt>.
55
+ #
56
+ # You should always pass a callable object to the scopes defined
57
+ # with #scope. This ensures that the scope is re-evaluated each
58
+ # time it is called.
59
+ #
60
+ # Note that this is simply 'syntactic sugar' for defining an actual
61
+ # class method:
62
+ #
63
+ # class Shirt < ActiveFedora::Base
64
+ # def self.red
65
+ # where(color: 'red')
66
+ # end
67
+ # end
68
+ #
69
+ # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by
70
+ # <tt>Shirt.red</tt> is not an Array but an ActiveFedora::Relation,
71
+ # which is composable with other scopes; it resembles the association object
72
+ # constructed by a {has_many}[rdoc-ref:Associations::ClassMethods#has_many]
73
+ # declaration. For instance, you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>,
74
+ # <tt>Shirt.red.where(size: 'small')</tt>. Also, just as with the
75
+ # association objects, named \scopes act like an Array, implementing
76
+ # Enumerable; <tt>Shirt.red.each(&block)</tt>, <tt>Shirt.red.first</tt>,
77
+ # and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if
78
+ # <tt>Shirt.red</tt> really was an array.
79
+ #
80
+ # These named \scopes are composable. For instance,
81
+ # <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are
82
+ # both red and dry clean only. Nested finds and calculations also work
83
+ # with these compositions: <tt>Shirt.red.dry_clean_only.count</tt>
84
+ # returns the number of garments for which these criteria obtain.
85
+ # Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
86
+ #
87
+ # All scopes are available as class methods on the ActiveFedora::Base
88
+ # descendant upon which the \scopes were defined. But they are also
89
+ # available to {has_many}[rdoc-ref:Associations::ClassMethods#has_many]
90
+ # associations. If,
91
+ #
92
+ # class Person < ActiveFedora::Base
93
+ # has_many :shirts
94
+ # end
95
+ #
96
+ # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of
97
+ # Elton's red, dry clean only shirts.
98
+ #
99
+ # \Named scopes can also have extensions, just as with
100
+ # {has_many}[rdoc-ref:Associations::ClassMethods#has_many] declarations:
101
+ #
102
+ # class Shirt < ActiveFedora::Base
103
+ # scope :red, -> { where(color: 'red') } do
104
+ # def dom_id
105
+ # 'red_shirts'
106
+ # end
107
+ # end
108
+ # end
109
+ #
110
+ # Scopes can also be used while creating/building a record.
111
+ #
112
+ # class Article < ActiveFedora::Base
113
+ # scope :published, -> { where(published: true) }
114
+ # end
115
+ #
116
+ # Article.published.new.published # => true
117
+ # Article.published.create.published # => true
118
+ #
119
+ # \Class methods on your model are automatically available
120
+ # on scopes. Assuming the following setup:
121
+ #
122
+ # class Article < ActiveFedora::Base
123
+ # scope :published, -> { where(published: true) }
124
+ # scope :featured, -> { where(featured: true) }
125
+ #
126
+ # def self.latest_article
127
+ # order('published_at desc').first
128
+ # end
129
+ #
130
+ # def self.titles
131
+ # pluck(:title)
132
+ # end
133
+ # end
134
+ #
135
+ # We are able to call the methods like this:
136
+ #
137
+ # Article.published.featured.latest_article
138
+ # Article.featured.titles
139
+ def scope(name, body, &block)
140
+ unless body.respond_to?(:call)
141
+ raise ArgumentError, 'The scope body needs to be callable.'
142
+ end
143
+
144
+ if dangerous_class_method?(name)
145
+ raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
146
+ "on the model \"#{self.name}\", but Active Record already defined " \
147
+ "a class method with the same name."
148
+ end
149
+
150
+ valid_scope_name?(name)
151
+ extension = Module.new(&block) if block
152
+
153
+ if body.respond_to?(:to_proc)
154
+ singleton_class.send(:define_method, name) do |*args|
155
+ scope = all.scoping { instance_exec(*args, &body) }
156
+ scope = scope.extending(extension) if extension
157
+
158
+ scope || all
159
+ end
160
+ else
161
+ singleton_class.send(:define_method, name) do |*args|
162
+ scope = all.scoping { body.call(*args) }
163
+ scope = scope.extending(extension) if extension
164
+
165
+ scope || all
166
+ end
167
+ end
168
+ end
169
+
170
+ protected
171
+
172
+ def valid_scope_name?(name)
173
+ return unless respond_to?(name, true)
174
+ logger.warn "Creating scope :#{name}. " \
175
+ "Overwriting existing method #{self.name}.#{name}."
176
+ end
37
177
  end
38
178
  end
39
179
  end
@@ -1,3 +1,3 @@
1
1
  module ActiveFedora
2
- VERSION = "9.10.0.pre1".freeze
2
+ VERSION = "9.10.0.pre2".freeze
3
3
  end
@@ -11,8 +11,6 @@ describe ActiveFedora::Base do
11
11
  end
12
12
 
13
13
  let(:library) { Library.create! }
14
- let!(:book1) { Book.create!(library: library) }
15
- let!(:book2) { Book.create!(library: library) }
16
14
 
17
15
  after do
18
16
  Object.send(:remove_const, :Library)
@@ -20,6 +18,9 @@ describe ActiveFedora::Base do
20
18
  end
21
19
 
22
20
  describe "load_from_solr" do
21
+ let!(:book1) { Book.create!(library: library) }
22
+ let!(:book2) { Book.create!(library: library) }
23
+
23
24
  it "sets rows to count, if not specified" do
24
25
  expect(library.books(response_format: :solr).size).to eq 2
25
26
  end
@@ -35,6 +36,8 @@ describe ActiveFedora::Base do
35
36
  end
36
37
 
37
38
  describe "#delete_all" do
39
+ let!(:book1) { Book.create!(library: library) }
40
+ let!(:book2) { Book.create!(library: library) }
38
41
  it "deletes em" do
39
42
  expect {
40
43
  library.books.delete_all
@@ -59,6 +62,8 @@ describe ActiveFedora::Base do
59
62
  end
60
63
 
61
64
  describe "#destroy_all" do
65
+ let!(:book1) { Book.create!(library: library) }
66
+ let!(:book2) { Book.create!(library: library) }
62
67
  it "deletes em" do
63
68
  expect {
64
69
  library.books.destroy_all
@@ -67,6 +72,8 @@ describe ActiveFedora::Base do
67
72
  end
68
73
 
69
74
  describe "#find" do
75
+ let!(:book1) { Book.create!(library: library) }
76
+ let!(:book2) { Book.create!(library: library) }
70
77
  it "finds the record that matches" do
71
78
  expected = library.books.find(book1.id)
72
79
  expect(expected).to eq book1
@@ -80,8 +87,11 @@ describe ActiveFedora::Base do
80
87
  end
81
88
 
82
89
  describe "#select" do
90
+ let!(:book1) { Book.create!(library: library) }
91
+ let!(:book2) { Book.create!(library: library) }
92
+
83
93
  # TODO: Bug described in issue #609
84
- xit "should choose a subset of objects in the relationship" do
94
+ xit "chooses a subset of objects in the relationship" do
85
95
  expect(library.books.select([:id])).to include(book1.id)
86
96
  end
87
97
  it "works as a block" do
@@ -89,6 +99,27 @@ describe ActiveFedora::Base do
89
99
  end
90
100
  end
91
101
 
102
+ describe "#size" do
103
+ context "with associations in memory" do
104
+ context "and the association is already loaded" do
105
+ before do
106
+ library.books.to_a # force the association to be loaded
107
+ library.books.build
108
+ end
109
+ subject { library.books.size }
110
+ it { is_expected.to eq 1 }
111
+ end
112
+
113
+ context "and the association is not loaded" do
114
+ before do
115
+ library.books.build
116
+ end
117
+ subject { library.books.size }
118
+ it { is_expected.to eq 1 }
119
+ end
120
+ end
121
+ end
122
+
92
123
  describe "finding the inverse" do
93
124
  context "when no inverse exists" do
94
125
  before do