acts_as_sourceable 1.0.6 → 2.0.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.
data/lib/acts_as_sourceable.rb
CHANGED
@@ -1,53 +1,92 @@
|
|
1
1
|
module ActsAsSourceable
|
2
2
|
module ActMethod
|
3
3
|
def acts_as_sourceable(options = {})
|
4
|
-
|
5
|
-
|
6
|
-
|
4
|
+
options.assert_valid_keys :through, :cache_column, :used_by
|
5
|
+
raise "Can't have a cache column and be sourced through an association" if options[:through] && options [:cache_column]
|
6
|
+
class_attribute :acts_as_sourceable_options
|
7
|
+
self.acts_as_sourceable_options = options
|
8
|
+
|
9
|
+
# INSTANCE SETUP
|
7
10
|
include ActsAsSourceable::InstanceMethods
|
8
11
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
# If we get our sources through an association use that,
|
13
|
+
# Else use the sourceable institutions table
|
14
|
+
class_eval do
|
15
|
+
if acts_as_sourceable_options[:through]
|
16
|
+
def sources; send(acts_as_sourceable_options[:through]) || []; end
|
17
|
+
else
|
18
|
+
has_one :sourceable_institution, :class_name => 'ActsAsSourceable::Registry', :as => :sourceable, :dependent => :delete
|
19
|
+
def sources; sourceable_institution.try(:sources) || []; end
|
20
|
+
end
|
15
21
|
end
|
16
22
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
self.sourceable_sourced_by = options[:sourced_by]
|
21
|
-
|
23
|
+
# CLASS SETUP
|
24
|
+
extend ActsAsSourceable::ClassMethods
|
25
|
+
|
22
26
|
# If a cache column is provided, use that to determine which records are sourced and unsourced
|
23
27
|
# Elsif the records can be derived, we need to check the flattened item tables for any references
|
24
28
|
# Else we check the sourceable_institutions to see if the record has a recorded source
|
25
|
-
if
|
26
|
-
scope :sourced, where(
|
27
|
-
scope :unsourced, where(
|
29
|
+
if options[:cache_column]
|
30
|
+
scope :sourced, where(options[:cache_column] => true)
|
31
|
+
scope :unsourced, where(options[:cache_column] => false)
|
32
|
+
elsif options[:through]
|
33
|
+
scope :sourced, joins(options[:through]).uniq
|
34
|
+
scope :unsourced, joins("LEFT OUTER JOIN (#{sourced.to_sql}) sourced ON sourced.id = #{table_name}.id").where("sourced.id IS NULL")
|
35
|
+
else
|
36
|
+
scope :sourced, joins(:sourceable_institution)
|
37
|
+
scope :unsourced, joins("LEFT OUTER JOIN (#{sourced.to_sql}) sourced ON sourced.id = #{table_name}.id").where("sourced.id IS NULL")
|
38
|
+
end
|
39
|
+
|
40
|
+
# Add a way of finding everything sourced by a particular set of records
|
41
|
+
if options[:through]
|
42
|
+
def sourced_by(*sources)
|
43
|
+
raise NotImplementedError # TODO
|
44
|
+
end
|
28
45
|
else
|
29
|
-
|
30
|
-
|
46
|
+
def sourced_by(*sources)
|
47
|
+
holding_institution_ids, collection_ids, item_ids = ActsAsSourceable::HelperMethods.group_ids_by_class(sources)
|
48
|
+
|
49
|
+
arel_table = ActsAsSourceable::Registry.arel_table
|
50
|
+
h_contraint = arel_table[:holding_institution_ids].array_overlap(holding_institution_ids)
|
51
|
+
c_contraint = arel_table[:collection_ids].array_overlap(collection_ids)
|
52
|
+
i_contraint = arel_table[:item_ids].array_overlap(item_ids)
|
53
|
+
|
54
|
+
sourced.where(h_contraint.or(c_contraint).or(i_contraint))
|
55
|
+
end
|
31
56
|
end
|
32
57
|
|
33
|
-
# Create a scope that returns record that is not used by the associations in
|
34
|
-
if
|
35
|
-
scope :unused, where(Array(
|
58
|
+
# Create a scope that returns record that is not used by the associations in options[:used_by]
|
59
|
+
if options[:used_by]
|
60
|
+
scope :unused, where(Array(options[:used_by]).collect {|usage_association| "#{table_name}.id NOT IN (" + select("#{table_name}.id").joins(usage_association).group("#{table_name}.id").to_sql + ")"}.join(' AND '))
|
36
61
|
scope :orphaned, unsourced.unused
|
37
62
|
else
|
38
63
|
scope :orphaned, unsourced
|
39
64
|
end
|
65
|
+
|
66
|
+
# ACTIVE RELATION SETUP
|
67
|
+
ActiveRecord::Relation.send(:include, ActsAsSourceable::ActiveRelationMethods)
|
68
|
+
|
69
|
+
# Delegate the relation methods to the relation so we can call Klass.unsourced (do this in the metaclass because all these methods are class level)
|
70
|
+
class << self
|
71
|
+
delegate :add_sources, :add_source, :remove_source, :remove_sources, :unsource, :to => :scoped
|
72
|
+
end
|
40
73
|
end
|
41
74
|
end
|
42
75
|
|
43
76
|
module ActiveRelationMethods
|
44
|
-
def
|
45
|
-
scoping { @klass.find_each(
|
77
|
+
def remove_sources(*sources)
|
78
|
+
scoping { @klass.find_each{|record| record.remove_sources(*sources) } }
|
46
79
|
end
|
80
|
+
alias_method :remove_source, :remove_sources
|
81
|
+
|
82
|
+
def add_sources(*sources)
|
83
|
+
scoping { @klass.find_each{|record| record.add_sources(*sources) } }
|
84
|
+
end
|
85
|
+
alias_method :add_source, :add_sources
|
47
86
|
|
48
87
|
def unsource
|
49
|
-
scoping { @klass.update_all("#{
|
50
|
-
scoping {
|
88
|
+
scoping { @klass.update_all("#{acts_as_sourceable_options[:cache_column]} = false", @klass.acts_as_sourceable_options[:cache_column] => true) } if @klass.acts_as_sourceable_options[:cache_column]
|
89
|
+
scoping { ActsAsSourceable::Registry.where("sourceable_type = ? AND sourceable_id IN (#{@klass.select("#{@klass.table_name}.id").to_sql})", @klass.name).delete_all }
|
51
90
|
end
|
52
91
|
end
|
53
92
|
|
@@ -61,27 +100,10 @@ module ActsAsSourceable
|
|
61
100
|
def acts_like_sourceable?
|
62
101
|
true
|
63
102
|
end
|
64
|
-
|
65
|
-
# Automatically update the sources for this model
|
66
|
-
# If the model gets its sources from another model, collect the sources of that model and record them as your own
|
67
|
-
# Else, this model must belong to a holding institution, so that is the source
|
68
|
-
def update_sources
|
69
|
-
if sourceable_sourced_by
|
70
|
-
if self.class.reflect_on_association(sourceable_sourced_by.to_sym).collection?
|
71
|
-
source_id_sql = send(sourceable_sourced_by).joins(:sourceable_institutions).select("sourceable_institutions.holding_institution_id").to_sql
|
72
|
-
sources = HoldingInstitution.where("id IN (#{source_id_sql})")
|
73
|
-
set_sources(sources)
|
74
|
-
else
|
75
|
-
set_sources(send(sourceable_sourced_by).sources)
|
76
|
-
end
|
77
|
-
else
|
78
|
-
set_sources(holding_institution)
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
103
|
+
|
82
104
|
def sourced?
|
83
|
-
if
|
84
|
-
self[
|
105
|
+
if acts_as_sourceable_options[:cache_column]
|
106
|
+
self[acts_as_sourceable_options[:cache_column]]
|
85
107
|
else
|
86
108
|
self.class.sourced.exists?(self)
|
87
109
|
end
|
@@ -91,32 +113,97 @@ module ActsAsSourceable
|
|
91
113
|
!sourced?
|
92
114
|
end
|
93
115
|
|
94
|
-
#
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
116
|
+
# Add the given holding_institutions, collections, and items
|
117
|
+
def add_sources(*sources)
|
118
|
+
holding_institution_ids, collection_ids, item_ids = ActsAsSourceable::HelperMethods.group_ids_by_class(sources)
|
119
|
+
registry = init_registry_entry
|
120
|
+
set_sources(registry.holding_institution_ids + holding_institution_ids, registry.collection_ids + collection_ids, registry.item_ids + item_ids)
|
121
|
+
end
|
122
|
+
alias_method :add_source, :add_sources
|
123
|
+
|
124
|
+
# Remove the given holding_institutions, collections, and items
|
125
|
+
def remove_sources(*sources)
|
126
|
+
holding_institution_ids, collection_ids, item_ids = ActsAsSourceable::HelperMethods.group_ids_by_class(sources)
|
127
|
+
registry = init_registry_entry
|
128
|
+
set_sources(registry.holding_institution_ids - holding_institution_ids, registry.collection_ids - collection_ids, registry.item_ids - item_ids)
|
129
|
+
end
|
130
|
+
alias_method :remove_source, :remove_sources
|
131
|
+
|
132
|
+
# Record which holding_institution, collection, and optionally which item the record came from
|
133
|
+
# If the record has no sources, the sourceable institution is deleted
|
134
|
+
# NOTE: HoldingInstitutions are stored in the production database (so don't get any crazy ideas when refactoring this code)
|
135
|
+
def set_sources(holding_institution_ids, collection_ids, item_ids)
|
136
|
+
registry = init_registry_entry
|
137
|
+
registry.holding_institution_ids = Array(holding_institution_ids).uniq
|
138
|
+
registry.collection_ids = Array(collection_ids).uniq
|
139
|
+
registry.item_ids = Array(item_ids).uniq
|
140
|
+
|
141
|
+
if holding_institution_ids.any? || collection_ids.any? || item_ids.any?
|
142
|
+
registry.save!
|
143
|
+
set_sourceable_cache_column(true)
|
144
|
+
elsif registry.persisted?
|
145
|
+
registry.destroy
|
146
|
+
set_sourceable_cache_column(false)
|
110
147
|
end
|
111
|
-
|
112
|
-
set_sourceable_cache_column(holding_institution_ids.any?)
|
113
148
|
end
|
114
149
|
|
115
150
|
private
|
151
|
+
|
152
|
+
def init_registry_entry
|
153
|
+
raise "Cannot set sources of a #{self.class.name}. They are sourced through #{acts_as_sourceable_options[:through]}" if acts_as_sourceable_options[:through]
|
154
|
+
|
155
|
+
ActsAsSourceable::Registry.where(:sourceable_type => self.class.name, :sourceable_id => self.id).first_or_initialize
|
156
|
+
end
|
116
157
|
|
117
158
|
def set_sourceable_cache_column(value)
|
118
|
-
# Update via sql because we don't need callbacks and validations called
|
119
|
-
|
159
|
+
update_column(acts_as_sourceable_options[:cache_column], value) if acts_as_sourceable_options[:cache_column] # Update via sql because we don't need callbacks and validations called
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
module HelperMethods
|
164
|
+
# Given an array of HoldingInstitutions, Collections, and Items, returns arrays containing only the records of each class.
|
165
|
+
# Order of return arrays is [HoldingInstitutions, Collections, Items]
|
166
|
+
def self.group_by_class(*sources)
|
167
|
+
groups = Array(sources).flatten.group_by(&:class)
|
168
|
+
return [groups[HoldingInstitution] || [], groups[Collection] || [], groups[Item] || []]
|
169
|
+
end
|
170
|
+
|
171
|
+
def self.group_ids_by_class(*sources)
|
172
|
+
group_by_class(*sources).collect!{|group| group.collect(&:id)}
|
173
|
+
end
|
174
|
+
|
175
|
+
# Removes sourceable institutions that no longer belong to a record or holding institution
|
176
|
+
def self.garbage_collect
|
177
|
+
ActsAsSourceable::Registry.pluck(:sourceable_type).uniq.each do |sourceable_type|
|
178
|
+
sourceable_table_name = sourceable_type.constantize.table_name
|
179
|
+
sourceable_id_sql = ActsAsSourceable::Registry
|
180
|
+
.select("#{ActsAsSourceable::Registry.table_name}.id")
|
181
|
+
.where(:sourceable_type => sourceable_type)
|
182
|
+
.joins("LEFT OUTER JOIN #{sourceable_table_name} ON #{sourceable_table_name}.id = #{ActsAsSourceable::Registry.table_name}.sourceable_id")
|
183
|
+
.where("#{sourceable_table_name}.id IS NULL").to_sql
|
184
|
+
|
185
|
+
ActsAsSourceable::Registry.delete_all("id IN (#{sourceable_id_sql})")
|
186
|
+
end
|
187
|
+
|
188
|
+
|
189
|
+
# Repair all Registry entries that reference missing items, collections, or holding institutions
|
190
|
+
holding_institution_ids = HoldingInstitution.pluck(:id)
|
191
|
+
collection_ids = Collection.pluck(:id)
|
192
|
+
|
193
|
+
[:holding_institution, :collection, :item].each do |type|
|
194
|
+
registries = ActsAsSourceable::Registry
|
195
|
+
registries = registries.joins("LEFT OUTER JOIN #{type}s ON #{type}s.id = ANY(#{type}_ids)")
|
196
|
+
registries = registries.group("#{ActsAsSourceable::Registry.table_name}.id")
|
197
|
+
# Having at least one listed source_id and no matching sources, or fewer matches than the total listed sources
|
198
|
+
registries = registries.having("(array_length(#{type}_ids, 1) > 0 AND EVERY(#{type}s.id IS NULL)) OR count(*) < array_length(#{type}_ids, 1)")
|
199
|
+
|
200
|
+
# Fix the registry entries that are wrong
|
201
|
+
registries.includes(:sourceable).each do |registry|
|
202
|
+
item_ids = Item.where(:id => registry.item_ids).pluck(:id)
|
203
|
+
# ActiveRecord::Base.logger.debug "Registry #{registry.id}: holding_institution_ids: #{registry.holding_institution_ids.inspect} => #{registry.holding_institution_ids & holding_institution_ids}, collection_ids: #{registry.collection_ids.inspect} => #{registry.collection_ids & collection_ids}, item_ids: #{registry.item_ids.inspect} => #{registry.item_ids & item_ids} "
|
204
|
+
registry.sourceable.set_sources(registry.holding_institution_ids & holding_institution_ids, registry.collection_ids & collection_ids, registry.item_ids & item_ids)
|
205
|
+
end
|
206
|
+
end
|
120
207
|
end
|
121
208
|
end
|
122
209
|
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module ActsAsSourceable
|
2
|
+
class Registry < ActiveRecord::Base
|
3
|
+
self.table_name = 'acts_as_sourceable_registry'
|
4
|
+
|
5
|
+
belongs_to :sourceable, :polymorphic => true
|
6
|
+
validates_presence_of :sourceable_type, :sourceable_id
|
7
|
+
|
8
|
+
def sources
|
9
|
+
HoldingInstitution.find(self.holding_institution_ids) + Collection.find(self.collection_ids) + Item.find(self.item_ids)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: acts_as_sourceable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,8 +10,19 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date:
|
14
|
-
dependencies:
|
13
|
+
date: 2013-01-26 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: postgres_ext
|
17
|
+
requirement: &70193900896400 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ~>
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 0.1.0
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *70193900896400
|
15
26
|
description: Allows the RRN to perform garbage collection on categories that are no
|
16
27
|
longer referenced.
|
17
28
|
email: technical@rrnpilot.org
|
@@ -20,7 +31,7 @@ extensions: []
|
|
20
31
|
extra_rdoc_files: []
|
21
32
|
files:
|
22
33
|
- lib/acts_as_sourceable/acts_as_sourceable.rb
|
23
|
-
- lib/acts_as_sourceable/
|
34
|
+
- lib/acts_as_sourceable/registry.rb
|
24
35
|
- lib/acts_as_sourceable.rb
|
25
36
|
- README.rdoc
|
26
37
|
homepage: http://github.com/rrn/acts_as_sourceable
|
@@ -1,16 +0,0 @@
|
|
1
|
-
class SourceableInstitution < ActiveRecord::Base
|
2
|
-
belongs_to :sourceable, :polymorphic => true
|
3
|
-
belongs_to :holding_institution
|
4
|
-
|
5
|
-
validates_presence_of :sourceable_type, :sourceable_id, :holding_institution_id
|
6
|
-
|
7
|
-
# Removes sourceable institutions that no longer belong to a record or holding institution
|
8
|
-
def self.garbage_collect
|
9
|
-
ActiveRecord::Base.connection.select_values(SourceableInstitution.select("DISTINCT sourceable_type").to_sql).each do |sourceable_type|
|
10
|
-
sourceable_table_name = sourceable_type.constantize.table_name
|
11
|
-
sourceable_id_sql = SourceableInstitution.select("sourceable_institutions.id").where(:sourceable_type => sourceable_type).joins("LEFT OUTER JOIN #{sourceable_table_name} ON #{sourceable_table_name}.id = sourceable_institutions.sourceable_id").where("#{sourceable_table_name}.id IS NULL").to_sql
|
12
|
-
SourceableInstitution.delete_all("id IN (#{sourceable_id_sql})")
|
13
|
-
end
|
14
|
-
SourceableInstitution.delete_all(["holding_institution_id NOT IN (?)", HoldingInstitution.all.collect(&:id)])
|
15
|
-
end
|
16
|
-
end
|