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.
@@ -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