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.
@@ -1,4 +1,4 @@
1
1
  require 'acts_as_sourceable/acts_as_sourceable'
2
- require 'acts_as_sourceable/sourceable_institution'
2
+ require 'acts_as_sourceable/registry'
3
3
 
4
4
  ActiveRecord::Base.extend ActsAsSourceable::ActMethod
@@ -1,53 +1,92 @@
1
1
  module ActsAsSourceable
2
2
  module ActMethod
3
3
  def acts_as_sourceable(options = {})
4
- # Include Class and Instance Methods
5
- ActiveRecord::Relation.send(:include, ActsAsSourceable::ActiveRelationMethods)
6
- extend ActsAsSourceable::ClassMethods
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
- has_many :sourceable_institutions, :as => :sourceable, :dependent => :destroy
10
- has_many :sources, :through => :sourceable_institutions, :source => :holding_institution
11
-
12
- # Delegate the relation methods to the relation
13
- class << self
14
- delegate :update_sources, :unsource, :to => :scoped
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
- class_attribute :sourceable_cache_column, :sourceable_used_by, :sourceable_sourced_by
18
- self.sourceable_cache_column = options[:cache_column]
19
- self.sourceable_used_by = options[:used_by]
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 sourceable_cache_column
26
- scope :sourced, where(sourceable_cache_column => true)
27
- scope :unsourced, where(sourceable_cache_column => false)
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
- scope :sourced, joins(:sourceable_institutions).group("#{table_name}.id")
30
- scope :unsourced, joins("LEFT OUTER JOIN sourceable_institutions ON sourceable_id = #{table_name}.id and sourceable_type = '#{self.name}'").where("sourceable_id IS NULL")
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 sourceable_used_by
34
- if sourceable_used_by
35
- scope :unused, where(Array(sourceable_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 '))
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 update_sources
45
- scoping { @klass.find_each(&:update_sources) }
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("#{sourceable_cache_column} = false", @klass.sourceable_cache_column => true) } if @klass.sourceable_cache_column
50
- scoping { SourceableInstitution.where("sourceable_type = ? AND sourceable_id IN (#{@klass.select(:id).to_sql})", @klass.name).delete_all }
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 sourceable_cache_column
84
- self[sourceable_cache_column]
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
- # NOTE: We do a much more verbose method of assigning sources than the obvious self.sources = Array(holding_institutions)
95
- # because HoldingInstitutions are present in the production database, and assigning sources causes rails to use the
96
- # production database (as opposed to the conversion database) to check for existing sources. This is obviously bad
97
- # because the sources in the production database do not reflect those that are in the conversion database since we
98
- # unsource many things during conversion.
99
- def set_sources(holding_institutions)
100
- holding_institution_ids = Array(holding_institutions).collect(&:id)
101
- existing_source_ids = sourceable_institutions.pluck('holding_institution_id')
102
-
103
- # Delete those that have been removed
104
- condition = holding_institution_ids.any? ? ["holding_institution_id NOT IN (?)", existing_source_ids] : nil # Can't use "NOT IN (?)" for an empty array because the result is always false
105
- SourceableInstitution.where(:sourceable_type => self.class.name, :sourceable_id => self.id).delete_all(condition)
106
-
107
- # Add those that are not present
108
- holding_institution_ids.each do |holding_institution_id|
109
- self.sourceable_institutions << SourceableInstitution.new(:holding_institution_id => holding_institution_id) unless existing_source_ids.include?(holding_institution_id)
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
- self.class.update_all({sourceable_cache_column => value}, :id => id) if sourceable_cache_column
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: 1.0.6
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: 2010-09-16 00:00:00.000000000 Z
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/sourceable_institution.rb
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