active-fedora 9.4.3 → 9.5.0
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.
- checksums.yaml +4 -4
- data/History.txt +13 -0
- data/lib/active_fedora.rb +1 -0
- data/lib/active_fedora/association_hash.rb +117 -0
- data/lib/active_fedora/associations.rb +28 -25
- data/lib/active_fedora/associations/basic_contains_association.rb +14 -2
- data/lib/active_fedora/associations/builder/contains.rb +6 -1
- data/lib/active_fedora/associations/id_composite.rb +4 -1
- data/lib/active_fedora/core.rb +12 -0
- data/lib/active_fedora/errors.rb +4 -0
- data/lib/active_fedora/file.rb +5 -2
- data/lib/active_fedora/files_hash.rb +1 -68
- data/lib/active_fedora/fixity_service.rb +13 -4
- data/lib/active_fedora/persistence.rb +18 -9
- data/lib/active_fedora/reflection.rb +5 -1
- data/lib/active_fedora/relation/calculations.rb +2 -2
- data/lib/active_fedora/relation/finder_methods.rb +25 -36
- data/lib/active_fedora/relation/query_methods.rb +5 -3
- data/lib/active_fedora/solr_query_builder.rb +62 -6
- data/lib/active_fedora/solr_service.rb +2 -2
- data/lib/active_fedora/version.rb +1 -1
- data/spec/integration/contains_association_spec.rb +34 -0
- data/spec/integration/scoped_query_spec.rb +8 -4
- data/spec/unit/attached_files_spec.rb +8 -0
- data/spec/unit/file_spec.rb +14 -0
- data/spec/unit/finder_methods_spec.rb +50 -0
- data/spec/unit/fixity_service_spec.rb +33 -9
- data/spec/unit/query_spec.rb +97 -56
- data/spec/unit/solr_config_options_spec.rb +7 -3
- metadata +5 -2
@@ -145,15 +145,15 @@ module ActiveFedora
|
|
145
145
|
assign_rdf_subject
|
146
146
|
serialize_attached_files
|
147
147
|
@ldp_source = @ldp_source.create
|
148
|
-
|
149
|
-
|
148
|
+
assign_uri_to_contained_resources
|
149
|
+
save_contained_resources
|
150
150
|
refresh
|
151
151
|
end
|
152
152
|
|
153
153
|
def update_record(options = {})
|
154
154
|
serialize_attached_files
|
155
155
|
execute_sparql_update
|
156
|
-
|
156
|
+
save_contained_resources
|
157
157
|
refresh
|
158
158
|
end
|
159
159
|
|
@@ -195,16 +195,25 @@ module ActiveFedora
|
|
195
195
|
ActiveFedora.fedora.connection.put(path, "")
|
196
196
|
end
|
197
197
|
|
198
|
-
def
|
199
|
-
|
200
|
-
|
198
|
+
def assign_uri_to_contained_resources
|
199
|
+
contained_resources.each do |name, source|
|
200
|
+
source.uri= "#{uri}/#{name}"
|
201
201
|
end
|
202
202
|
end
|
203
203
|
|
204
|
-
def
|
205
|
-
|
206
|
-
|
204
|
+
def save_contained_resources
|
205
|
+
contained_resources.changed.each do |_, resource|
|
206
|
+
resource.save
|
207
207
|
end
|
208
208
|
end
|
209
|
+
|
210
|
+
def contained_resources
|
211
|
+
@contained_resources ||= attached_files.merge(contained_rdf_sources)
|
212
|
+
end
|
213
|
+
|
214
|
+
def contained_rdf_sources
|
215
|
+
@contained_rdf_sources ||=
|
216
|
+
AssociationHash.new(self, self.class.contained_rdf_source_reflections)
|
217
|
+
end
|
209
218
|
end
|
210
219
|
end
|
@@ -41,7 +41,11 @@ module ActiveFedora
|
|
41
41
|
end
|
42
42
|
|
43
43
|
def child_resource_reflections
|
44
|
-
reflections.select { |_, reflection| reflection.kind_of?(AssociationReflection) && reflection.macro == :contains }
|
44
|
+
reflections.select { |_, reflection| reflection.kind_of?(AssociationReflection) && reflection.macro == :contains && reflection.klass <= ActiveFedora::File}
|
45
|
+
end
|
46
|
+
|
47
|
+
def contained_rdf_source_reflections
|
48
|
+
reflections.select { |_, reflection| reflection.kind_of?(AssociationReflection) && reflection.macro == :contains && !(reflection.klass <= ActiveFedora::File)}
|
45
49
|
end
|
46
50
|
|
47
51
|
# Returns the AssociationReflection object for the +association+ (use the symbol).
|
@@ -8,12 +8,12 @@ module ActiveFedora
|
|
8
8
|
opts = {}
|
9
9
|
opts[:rows] = limit_value if limit_value
|
10
10
|
opts[:sort] = order_values if order_values
|
11
|
-
|
11
|
+
|
12
12
|
calculate :count, where_values, opts
|
13
13
|
end
|
14
14
|
|
15
15
|
def calculate(calculation, conditions, opts={})
|
16
|
-
SolrService.query(create_query(conditions), :
|
16
|
+
SolrService.query(create_query(conditions), raw: true, rows: 0).fetch('response'.freeze)['numFound'.freeze]
|
17
17
|
end
|
18
18
|
end
|
19
19
|
end
|
@@ -111,7 +111,7 @@ module ActiveFedora
|
|
111
111
|
unless opts.include?(:sort)
|
112
112
|
opts[:sort]=@klass.default_sort_params
|
113
113
|
end
|
114
|
-
SolrService.query(create_query(conditions), opts)
|
114
|
+
SolrService.query(create_query(conditions), opts)
|
115
115
|
end
|
116
116
|
|
117
117
|
# Yields the found ActiveFedora::Base object to the passed block
|
@@ -199,7 +199,7 @@ module ActiveFedora
|
|
199
199
|
# The true class may be a subclass of @klass, so always use from_class_uri
|
200
200
|
resource_class = Model.from_class_uri(has_model_value(resource)) || ActiveFedora::Base
|
201
201
|
unless equivalent_class?(resource_class)
|
202
|
-
raise ActiveFedora::ActiveFedoraError.new("Model mismatch. Expected #{@klass}. Got: #{resource_class}")
|
202
|
+
raise ActiveFedora::ActiveFedoraError.new("Model mismatch. Expected #{@klass}. Got: #{resource_class}")
|
203
203
|
end
|
204
204
|
resource_class
|
205
205
|
end
|
@@ -254,45 +254,42 @@ module ActiveFedora
|
|
254
254
|
private
|
255
255
|
|
256
256
|
# Returns a solr query for the supplied conditions
|
257
|
-
# @param[Hash] conditions solr conditions to match
|
257
|
+
# @param[Hash,String,Array] conditions solr conditions to match
|
258
258
|
def create_query(conditions)
|
259
|
-
|
260
|
-
when Hash
|
261
|
-
build_query([create_query_from_hash(conditions)])
|
262
|
-
when String
|
263
|
-
build_query(["(#{conditions})"])
|
264
|
-
else
|
265
|
-
build_query(conditions)
|
266
|
-
end
|
259
|
+
build_query(build_where(conditions))
|
267
260
|
end
|
268
261
|
|
262
|
+
# @param [Array<String>] conditions
|
269
263
|
def build_query(conditions)
|
270
|
-
clauses = search_model_clause ?
|
271
|
-
clauses += conditions.reject{|c| c.blank?}
|
264
|
+
clauses = search_model_clause ? [search_model_clause] : []
|
265
|
+
clauses += conditions.reject { |c| c.blank? }
|
272
266
|
return "*:*" if clauses.empty?
|
273
267
|
clauses.compact.join(" AND ")
|
274
268
|
end
|
275
269
|
|
270
|
+
# @param [Hash<Symbol,String>] conditions
|
271
|
+
# @returns [Array<String>]
|
276
272
|
def create_query_from_hash(conditions)
|
277
|
-
conditions.map {|key,value| condition_to_clauses(key, value)}.compact
|
273
|
+
conditions.map { |key, value| condition_to_clauses(key, value) }.compact
|
278
274
|
end
|
279
275
|
|
276
|
+
# @param [Symbol] key
|
277
|
+
# @param [String] value
|
280
278
|
def condition_to_clauses(key, value)
|
281
|
-
|
282
|
-
|
283
|
-
if @klass.delegated_attributes.key?(key)
|
284
|
-
# TODO Check to see if `key' is a possible solr field for this class, if it isn't try :searchable instead
|
285
|
-
key = ActiveFedora::SolrQueryBuilder.solr_name(key, :stored_searchable, type: :string)
|
286
|
-
end
|
279
|
+
SolrQueryBuilder.construct_query([[field_name_for(key), value]])
|
280
|
+
end
|
287
281
|
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
282
|
+
# If the key is a property name, turn it into a solr field
|
283
|
+
# @param [Symbol] key
|
284
|
+
# @return [String]
|
285
|
+
def field_name_for(key)
|
286
|
+
if @klass.delegated_attributes.key?(key)
|
287
|
+
# TODO Check to see if `key' is a possible solr field for this class, if it isn't try :searchable instead
|
288
|
+
ActiveFedora::SolrQueryBuilder.solr_name(key, :stored_searchable, type: :string)
|
289
|
+
elsif key == :id
|
290
|
+
SOLR_DOCUMENT_ID
|
291
|
+
else
|
292
|
+
key.to_s
|
296
293
|
end
|
297
294
|
end
|
298
295
|
|
@@ -306,13 +303,5 @@ module ActiveFedora
|
|
306
303
|
clauses.size == 1 ? clauses.first : "(#{clauses.join(" OR ")})"
|
307
304
|
end
|
308
305
|
end
|
309
|
-
|
310
|
-
|
311
|
-
private
|
312
|
-
# Adds esaping for spaces which are not handled by RSolr.solr_escape
|
313
|
-
# See rsolr/rsolr#101
|
314
|
-
def solr_escape terms
|
315
|
-
RSolr.solr_escape(terms).gsub(/\s+/,"\\ ")
|
316
|
-
end
|
317
306
|
end
|
318
307
|
end
|
@@ -17,7 +17,7 @@ module ActiveFedora
|
|
17
17
|
def where_values=(values)
|
18
18
|
raise ImmutableRelation if @loaded
|
19
19
|
@values[:where] = values
|
20
|
-
end
|
20
|
+
end
|
21
21
|
|
22
22
|
def order_values
|
23
23
|
@values[:order] || []
|
@@ -62,11 +62,13 @@ module ActiveFedora
|
|
62
62
|
self
|
63
63
|
end
|
64
64
|
|
65
|
+
# @param [Hash,String] values
|
66
|
+
# @return [Array<String>] list of solr hashes
|
65
67
|
def build_where(values)
|
66
68
|
return [] if values.blank?
|
67
69
|
case values
|
68
70
|
when Hash
|
69
|
-
|
71
|
+
create_query_from_hash(values)
|
70
72
|
when String
|
71
73
|
["(#{values})"]
|
72
74
|
else
|
@@ -76,7 +78,7 @@ module ActiveFedora
|
|
76
78
|
|
77
79
|
# Order the returned records by the field and direction provided
|
78
80
|
#
|
79
|
-
# @option [Array<String>] args a list of fields and directions to sort by
|
81
|
+
# @option [Array<String>] args a list of fields and directions to sort by
|
80
82
|
#
|
81
83
|
# @example
|
82
84
|
# Person.where(occupation_s: 'Plumber').order('name_t desc', 'color_t asc')
|
@@ -1,5 +1,7 @@
|
|
1
1
|
module ActiveFedora
|
2
2
|
module SolrQueryBuilder
|
3
|
+
PARSED_SUFFIX = '_tesim'.freeze
|
4
|
+
|
3
5
|
# Construct a solr query for a list of ids
|
4
6
|
# This is used to get a solr response based on the list of ids in an object's RELS-EXT relationhsips
|
5
7
|
# If the id_array is empty, defaults to a query of "id:NEVER_USE_THIS_ID", which will return an empty solr response
|
@@ -29,18 +31,72 @@ module ActiveFedora
|
|
29
31
|
# # => _query_:"{!raw f=has_model_ssim}info:fedora/afmodel:ComplexCollection" OR _query_:"{!raw f=has_model_ssim}info:fedora/afmodel:ActiveFedora_Base"
|
30
32
|
#
|
31
33
|
# construct_query_for_rel [[Book.reflect_on_association(:library), "foo/bar/baz"]]
|
32
|
-
def self.construct_query_for_rel(field_pairs, join_with = 'AND')
|
34
|
+
def self.construct_query_for_rel(field_pairs, join_with = ' AND ')
|
33
35
|
field_pairs = field_pairs.to_a if field_pairs.kind_of? Hash
|
36
|
+
construct_query(property_values_to_solr(field_pairs), join_with)
|
37
|
+
end
|
34
38
|
|
35
|
-
|
36
|
-
|
39
|
+
# Construct a solr query from a list of pairs (e.g. [field name, values])
|
40
|
+
# @param [Array<Array>] pairs a list of pairs of property name and values
|
41
|
+
# @param [String] join_with ('AND') the value we're joining the clauses with
|
42
|
+
# @returns [String] a solr query
|
43
|
+
# @example
|
44
|
+
# construct_query([['library_id_ssim', '123'], ['owner_ssim', 'Fred']])
|
45
|
+
# # => "_query_:\"{!raw f=library_id_ssim}123\" AND _query_:\"{!raw f=owner_ssim}Fred\""
|
46
|
+
def self.construct_query(field_pairs, join_with = ' AND ')
|
47
|
+
pairs_to_clauses(field_pairs).join(join_with)
|
37
48
|
end
|
38
49
|
|
39
50
|
private
|
40
|
-
#
|
51
|
+
# @param [Array<Array>] pairs a list of (key, value) pairs. The value itself may
|
52
|
+
# @return [Array] a list of solr clauses
|
41
53
|
def self.pairs_to_clauses(pairs)
|
42
|
-
pairs.
|
43
|
-
|
54
|
+
pairs.flat_map do |field, value|
|
55
|
+
condition_to_clauses(field, value)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# @param [String] field
|
60
|
+
# @param [String, Array<String>] values
|
61
|
+
# @return [Array<String>]
|
62
|
+
def self.condition_to_clauses(field, values)
|
63
|
+
values = Array(values)
|
64
|
+
values << nil if values.empty?
|
65
|
+
values.map do |value|
|
66
|
+
if value.present?
|
67
|
+
if parsed?(field)
|
68
|
+
# If you do a raw query on a parsed field you won't get the matches you expect.
|
69
|
+
"#{field}:#{solr_escape(value)}"
|
70
|
+
else
|
71
|
+
raw_query(field, value)
|
72
|
+
end
|
73
|
+
else
|
74
|
+
# Check that the field is not present. In SQL: "WHERE field IS NULL"
|
75
|
+
"-#{field}:[* TO *]"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.parsed?(field)
|
81
|
+
field.end_with?(PARSED_SUFFIX)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Adds esaping for spaces which are not handled by RSolr.solr_escape
|
85
|
+
# See rsolr/rsolr#101
|
86
|
+
def self.solr_escape terms
|
87
|
+
RSolr.solr_escape(terms).gsub(/\s+/,"\\ ")
|
88
|
+
end
|
89
|
+
|
90
|
+
# Given a list of pairs (e.g. [field name, values]), convert the field names
|
91
|
+
# to solr names
|
92
|
+
# @param [Array<Array>] pairs a list of pairs of property name and values
|
93
|
+
# @returns [Hash] map of solr fields to values
|
94
|
+
# @example
|
95
|
+
# property_values_to_solr([['library_id', '123'], ['owner', 'Fred']])
|
96
|
+
# # => [['library_id_ssim', '123'], ['owner_ssim', 'Fred']]
|
97
|
+
def self.property_values_to_solr(pairs)
|
98
|
+
pairs.each_with_object([]) do |(property, value), list|
|
99
|
+
list << [solr_field(property), value]
|
44
100
|
end
|
45
101
|
end
|
46
102
|
|
@@ -103,8 +103,8 @@ module ActiveFedora
|
|
103
103
|
|
104
104
|
def query(query, args={})
|
105
105
|
raw = args.delete(:raw)
|
106
|
-
args = args.merge(:
|
107
|
-
result = SolrService.instance.conn.get('select', :
|
106
|
+
args = args.merge(q: query, qt: 'standard')
|
107
|
+
result = SolrService.instance.conn.get('select', params: args)
|
108
108
|
return result if raw
|
109
109
|
result['response']['docs']
|
110
110
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe ActiveFedora::Base do
|
4
|
+
before do
|
5
|
+
class Source < ActiveFedora::Base
|
6
|
+
contains :sub_resource, class_name: "Source"
|
7
|
+
property :title, predicate: ::RDF::DC.title, multiple: false
|
8
|
+
end
|
9
|
+
end
|
10
|
+
after do
|
11
|
+
Object.send(:remove_const, :Source)
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "contains relationships" do
|
15
|
+
it "should be able to have RDF sources" do
|
16
|
+
s = Source.new
|
17
|
+
s.sub_resource.title = "Test"
|
18
|
+
expect(s.sub_resource).not_to be_persisted
|
19
|
+
expect{ s.save }.not_to raise_error
|
20
|
+
s.reload
|
21
|
+
expect(s.sub_resource.title).to eq "Test"
|
22
|
+
expect(s.sub_resource.uri).to eq s.uri.to_s + "/sub_resource"
|
23
|
+
end
|
24
|
+
it "should be able to add RDF sources" do
|
25
|
+
s = Source.create
|
26
|
+
s.sub_resource.title = "Test"
|
27
|
+
expect(s.sub_resource).not_to be_persisted
|
28
|
+
expect{ s.save }.not_to raise_error
|
29
|
+
s.reload
|
30
|
+
expect(s.sub_resource.title).to eq "Test"
|
31
|
+
expect(s.sub_resource.uri).to eq s.uri.to_s + "/sub_resource"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -30,7 +30,6 @@ describe "scoped queries" do
|
|
30
30
|
Object.send(:remove_const, :ModelIntegrationSpec)
|
31
31
|
end
|
32
32
|
|
33
|
-
|
34
33
|
describe "When there is one object in the store" do
|
35
34
|
let!(:test_instance) { ModelIntegrationSpec::Basic.create!()}
|
36
35
|
|
@@ -67,7 +66,7 @@ describe "scoped queries" do
|
|
67
66
|
test_instance3.delete
|
68
67
|
end
|
69
68
|
|
70
|
-
it "
|
69
|
+
it "queries" do
|
71
70
|
field = ActiveFedora::SolrQueryBuilder.solr_name('foo', type: :string)
|
72
71
|
expect(ModelIntegrationSpec::Basic.where(field => 'Beta')).to eq [test_instance1]
|
73
72
|
expect(ModelIntegrationSpec::Basic.where('foo' => 'Beta')).to eq [test_instance1]
|
@@ -90,8 +89,13 @@ describe "scoped queries" do
|
|
90
89
|
expect(ModelIntegrationSpec::Basic.where("foo:bar OR bar:baz").where_values).to eq ["(foo:bar OR bar:baz)"]
|
91
90
|
end
|
92
91
|
|
93
|
-
it "
|
94
|
-
|
92
|
+
it "chains where queries" do
|
93
|
+
first_condition = { ActiveFedora::SolrQueryBuilder.solr_name('bar', type: :string) => 'Peanuts' }
|
94
|
+
second_condition = "foo_tesim:bar"
|
95
|
+
where_values = ModelIntegrationSpec::Basic.where(first_condition)
|
96
|
+
.where(second_condition).where_values
|
97
|
+
expect(where_values).to eq ["bar_tesim:Peanuts",
|
98
|
+
"(foo_tesim:bar)"]
|
95
99
|
end
|
96
100
|
|
97
101
|
it "should chain count" do
|
@@ -4,19 +4,24 @@ describe ActiveFedora::AttachedFiles do
|
|
4
4
|
subject { ActiveFedora::Base.new }
|
5
5
|
describe "contains" do
|
6
6
|
before do
|
7
|
+
class Z < ActiveFedora::File
|
8
|
+
end
|
7
9
|
class FooHistory < ActiveFedora::Base
|
8
10
|
contains 'dsid', class_name: 'ActiveFedora::SimpleDatastream'
|
9
11
|
contains 'complex_ds', autocreate: true, class_name: 'Z'
|
10
12
|
contains 'thumbnail'
|
13
|
+
contains 'child_resource', class_name: 'ActiveFedora::Base'
|
11
14
|
end
|
12
15
|
end
|
13
16
|
after do
|
17
|
+
Object.send(:remove_const, :Z)
|
14
18
|
Object.send(:remove_const, :FooHistory)
|
15
19
|
end
|
16
20
|
|
17
21
|
it "should have a child_resource_reflection" do
|
18
22
|
expect(FooHistory.child_resource_reflections).to have_key(:dsid)
|
19
23
|
expect(FooHistory.child_resource_reflections).to have_key(:thumbnail)
|
24
|
+
expect(FooHistory.child_resource_reflections).not_to have_key(:child_resource)
|
20
25
|
end
|
21
26
|
|
22
27
|
it "should let you override defaults" do
|
@@ -34,6 +39,8 @@ describe ActiveFedora::AttachedFiles do
|
|
34
39
|
before do
|
35
40
|
@original_behavior = Deprecation.default_deprecation_behavior
|
36
41
|
Deprecation.default_deprecation_behavior = :silence
|
42
|
+
class Z < ActiveFedora::File
|
43
|
+
end
|
37
44
|
class FooHistory < ActiveFedora::Base
|
38
45
|
has_metadata :name => 'dsid', type: ActiveFedora::SimpleDatastream
|
39
46
|
has_metadata 'complex_ds', autocreate: true, type: 'Z'
|
@@ -42,6 +49,7 @@ describe ActiveFedora::AttachedFiles do
|
|
42
49
|
after do
|
43
50
|
Deprecation.default_deprecation_behavior = @original_behavior
|
44
51
|
Object.send(:remove_const, :FooHistory)
|
52
|
+
Object.send(:remove_const, :Z)
|
45
53
|
end
|
46
54
|
|
47
55
|
it "should have a child_resource_reflection" do
|
data/spec/unit/file_spec.rb
CHANGED
@@ -39,6 +39,20 @@ describe ActiveFedora::File do
|
|
39
39
|
it "sets the uri using the parent as the base" do
|
40
40
|
expect(subject).to eq "#{ActiveFedora.fedora.host}#{ActiveFedora.fedora.base_path}/1234/FOO1"
|
41
41
|
end
|
42
|
+
|
43
|
+
context "and it's initialized with the URI" do
|
44
|
+
let(:file) { described_class.new(parent.uri+"/FOO1") }
|
45
|
+
it "works" do
|
46
|
+
expect(subject.to_s).to eq "#{ActiveFedora.fedora.host}#{ActiveFedora.fedora.base_path}/1234/FOO1"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context "and it's initialized with an ID" do
|
51
|
+
let(:file) { described_class.new(parent.id+"/FOO1") }
|
52
|
+
it "works" do
|
53
|
+
expect(subject.to_s).to eq "#{ActiveFedora.fedora.host}#{ActiveFedora.fedora.base_path}/1234/FOO1"
|
54
|
+
end
|
55
|
+
end
|
42
56
|
end
|
43
57
|
|
44
58
|
context "when the file doesn't have a uri" do
|