acts_as_sourceable 1.0.6 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
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
|