active-fedora 9.10.0.pre1 → 9.10.0.pre2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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