dbview_cti 0.1.4 → 0.1.5

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/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 0.1.5 (25/04/2014)
2
+
3
+ * Made associations respect the class_name option
4
+ * Fixed cti_recreate_views_after_change_to so it also works for the base class
5
+
1
6
  ## 0.1.4 (8/04/2014)
2
7
 
3
8
  * Fixed association issues
data/README.md CHANGED
@@ -175,7 +175,7 @@ end
175
175
 
176
176
  ## Associations
177
177
 
178
- Associations (`has_many`, `has_one`, etc.) work and are inherited as you would expect. There are two caveats:
178
+ Associations (`has_many`, `has_one`, etc.) work and are inherited as you would expect. There are three caveats:
179
179
 
180
180
  * In the base class, you have to call `cti_base_class` before defining any associations:
181
181
 
@@ -199,6 +199,8 @@ class SpaceShip < Vehicle
199
199
  end
200
200
  ```
201
201
 
202
+ * You have to make sure that the association is defined in both classes, e.g. if you have `belongs_to :car` in a class called Part then Car should also define the association with `has_many :parts` (or `has_one :part`).
203
+
202
204
  ## API
203
205
 
204
206
  ### Models
@@ -22,19 +22,18 @@ module DBViewCTI
22
22
  # use with block in up/down methods
23
23
  def cti_recreate_views_after_change_to(class_name, options = {})
24
24
  klass = class_name.constantize
25
- classes = [ class_name ] + klass.cti_all_descendants
25
+ classes = klass.cti_all_descendants
26
+ # only add class_name if it is not the base class
27
+ classes = classes.unshift( class_name ) unless klass.cti_base_class?
26
28
  # drop all views in reverse order
27
29
  classes.reverse.each do |kklass|
28
30
  cti_drop_view(kklass, options)
29
31
  end
30
32
  yield # perform table changes in block (e.g. add column)
31
33
  # recreate views in forward order
34
+ cti_reset_column_information(class_name) if klass.cti_base_class?
32
35
  classes.each do |kklass|
33
- # any column changes are reflected in the real table cache, but not in the
34
- # view cache, so we have to make sure it is cleared
35
- true_klass = kklass.constantize
36
- true_klass.connection.schema_cache.clear_table_cache!(true_klass.table_name)
37
- true_klass.reset_column_information
36
+ cti_reset_column_information(kklass)
38
37
  cti_create_view(kklass, options)
39
38
  end
40
39
  end
@@ -47,6 +46,16 @@ module DBViewCTI
47
46
  execute(query)
48
47
  end
49
48
  end
49
+
50
+ private
51
+
52
+ def cti_reset_column_information(class_name)
53
+ # any column changes are reflected in the real table cache, but not in the
54
+ # view cache, so we have to make sure it is cleared
55
+ klass = class_name.constantize
56
+ klass.connection.schema_cache.clear_table_cache!(klass.table_name)
57
+ klass.reset_column_information
58
+ end
50
59
 
51
60
  end
52
61
  end
@@ -0,0 +1,57 @@
1
+ module DBViewCTI
2
+ module Model
3
+ module CTI
4
+ module AssociationValidations
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ # validations
9
+ validate :cti_validate_associations, :cti_no_disable => true
10
+ attr_accessor :cti_disable_validations
11
+ end
12
+
13
+ def cti_validate_associations
14
+ return_value = true
15
+ self.class.cti_association_proxies.each_key do |proxy_name|
16
+ proxy = instance_variable_get(proxy_name)
17
+ if proxy && !proxy.valid?
18
+ errors.messages.merge!(proxy.errors.messages)
19
+ return_value = false
20
+ end
21
+ end
22
+ return_value
23
+ end
24
+
25
+ module ClassMethods
26
+
27
+ # redefine validate to always add :unless proc so we can disable the validations for an object
28
+ # by setting the cti_disable_validations accessor to true
29
+ def validate(*args, &block)
30
+ # we specifically don't want to disable balidations belonging to associations. Based on the naming
31
+ # rails uses, we return immediately in such cases (there must be a cleaner way to do this...)
32
+ return super if args.first && args.first.to_s =~ /^validate_associated_records_for_/
33
+ # rest of implementation insipred by the validate implementation in rails
34
+ options = args.extract_options!.dup
35
+ return super if options[:cti_no_disable]
36
+ if options.key?(:unless)
37
+ options[:unless] = Array(options[:unless])
38
+ options[:unless].unshift( cti_validation_unless_proc )
39
+ else
40
+ options[:unless] = cti_validation_unless_proc
41
+ end
42
+ args << options
43
+ return super(*args, &block)
44
+ end
45
+
46
+ def cti_validation_unless_proc
47
+ @cti_validation_unless_proc ||= Proc.new do |object|
48
+ object.respond_to?(:cti_disable_validations) && object.cti_disable_validations
49
+ end
50
+ end
51
+
52
+ end
53
+
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,180 @@
1
+ module DBViewCTI
2
+ module Model
3
+ module CTI
4
+ module Associations
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ # for associations:
9
+ alias_method_chain :association, :cti
10
+ # save callbacks (necessary for saving associations)
11
+ after_save :cti_save_associations
12
+ end
13
+
14
+ def cti_save_associations
15
+ self.class.cti_association_proxies.each_key do |proxy_name|
16
+ proxy = instance_variable_get(proxy_name)
17
+ proxy.save if proxy
18
+ end
19
+ true
20
+ end
21
+
22
+ def association_with_cti(*args)
23
+ return association_without_cti(*args) unless args.length == 1
24
+ association_name = args[0]
25
+ proxy = cti_association_proxy(association_name)
26
+ proxy ||= self
27
+ proxy.association_without_cti(association_name)
28
+ end
29
+
30
+ def cti_association_proxy(association_name)
31
+ return nil if self.class.reflect_on_all_associations(:belongs_to).map(&:name).include?(association_name.to_sym)
32
+ proxy_name = self.class.cti_association_proxy_name(association_name)
33
+ proxy = instance_variable_get(proxy_name)
34
+ if !proxy && !self.class.cti_has_association?(association_name)
35
+ instance_variable_set(proxy_name,
36
+ ModelDelegator.new(self, self.class.cti_association_proxies[proxy_name]))
37
+ proxy = instance_variable_get(proxy_name)
38
+ end
39
+ proxy
40
+ end
41
+
42
+ module ClassMethods
43
+
44
+ # redefine association class methods
45
+ [:has_many, :has_and_belongs_to_many, :has_one].each do |name|
46
+ self.class_eval <<-eos, __FILE__, __LINE__+1
47
+ def #{name}(*args, &block)
48
+ cti_initialize_cti_associations
49
+ @cti_associations[:#{name}] << args.first
50
+ super
51
+ end
52
+ eos
53
+ end
54
+
55
+ def cti_create_association_proxies
56
+ # create hash with proxy and class names. The proxies themselves will be created
57
+ # by the 'association' instance method when the association is used for the first time.
58
+ @cti_association_proxies ||= {}
59
+ @cti_ascendants.each do |ascendant|
60
+ [:has_many, :has_and_belongs_to_many, :has_one].each do |association_type|
61
+ ascendant.constantize.cti_associations[association_type].each do |association|
62
+ proxy_name = cti_association_proxy_name(association)
63
+ @cti_association_proxies[proxy_name] = ascendant
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ # fix the 'remote' (i.e. belongs_to) part of any has_one of has_many association in this class
70
+ def cti_redefine_remote_associations
71
+ cti_initialize_cti_associations
72
+ # redefine remote belongs_to associations
73
+ [:has_many, :has_one].each do |association_type|
74
+ @cti_associations[association_type].each do |association|
75
+ next if @cti_redefined_remote_associations[association_type].include?( association )
76
+ if cti_reciprocal_association_present_for?( association, :belongs_to )
77
+ remote_association = cti_reciprocal_association_for( association, :belongs_to )
78
+ remote_class = cti_association_name_to_class_name( association ).constantize
79
+ cti_redefine_remote_belongs_to_association(remote_class, remote_association.name.to_sym)
80
+ @cti_redefined_remote_associations[association_type] << association
81
+ end
82
+ end
83
+ end
84
+ # redefine remote has_many and has_and_belongs_to_many associations
85
+ [:has_many, :has_and_belongs_to_many].each do |association_type|
86
+ @cti_associations[association_type].each do |association|
87
+ next if @cti_redefined_remote_associations[association_type].include?( association )
88
+ if cti_reciprocal_association_present_for?( association, association_type)
89
+ remote_association = cti_reciprocal_association_for( association, association_type )
90
+ remote_class = cti_association_name_to_class_name( association ).constantize
91
+ cti_redefine_remote_to_many_association(remote_class, remote_association.name.to_sym)
92
+ @cti_redefined_remote_associations[association_type] << association
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ # Gets reciprocal association of type 'type' for the given association.
99
+ # (example: if a has_many association has a corresponding belongs_to in the remote class).
100
+ # Normally, the method checks if the remote association refers to this class, but it is possible to
101
+ # pass in 'class_name' to check different classes
102
+ def cti_reciprocal_association_for(association, type, class_name = nil)
103
+ class_name ||= self.name
104
+ remote_class = cti_association_name_to_class_name( association, class_name ).constantize
105
+ remote_associations = remote_class.reflect_on_all_associations( type ).select { |a| a.class_name == class_name }
106
+ remote_associations.first
107
+ end
108
+
109
+ # Check if a reciprocal association of type 'type' is present for the given association.
110
+ # (example: check if a has_many association has a corresponding belongs_to in the remote class).
111
+ # Normally, the method checks if the remote association refers to this class, but it is possible to
112
+ # pass in 'class_name' to check different classes
113
+ def cti_reciprocal_association_present_for?(association, type, class_name = nil)
114
+ !cti_reciprocal_association_for(association, type, class_name).nil?
115
+ end
116
+
117
+ # converts e.g. :space_ships to SpaceShip
118
+ # Normally operates on associations of this class, but it is possible to
119
+ # pass in 'class_name' if 'association_name' is an association on a different classes
120
+ def cti_association_name_to_class_name(association_name, class_name = nil)
121
+ klass = self
122
+ klass = class_name.constantize if class_name
123
+ klass.reflect_on_all_associations.select { |a| a.name == association_name }.first.class_name
124
+ end
125
+
126
+ def cti_redefine_remote_belongs_to_association(remote_class, remote_association)
127
+ remote_class.class_eval <<-eos, __FILE__, __LINE__+1
128
+ def #{remote_association}=(object, *args, &block)
129
+ super( object.try(:convert_to, '#{self.name}'), *args, &block )
130
+ end
131
+ eos
132
+ end
133
+
134
+ def cti_redefine_remote_to_many_association(remote_class, remote_association)
135
+ remote_class.class_eval <<-eos, __FILE__, __LINE__+1
136
+ def #{remote_association}=(objects, *args, &block)
137
+ super( objects.map { |o| o.try(:convert_to, '#{self.name}') }, *args, &block)
138
+ end
139
+ def #{remote_association}(*args, &block)
140
+ collection = super
141
+ DBViewCTI::Model::CollectionDelegator.new(collection, '#{self.name}')
142
+ end
143
+ eos
144
+ end
145
+
146
+ def cti_association_proxy_name(association)
147
+ "@cti_#{association}_association_proxy"
148
+ end
149
+
150
+ def cti_associations
151
+ cti_initialize_cti_associations
152
+ @cti_associations
153
+ end
154
+
155
+ def cti_has_association?(association_name)
156
+ if !@cti_all_associations
157
+ @cti_all_associations = @cti_associations.keys.inject([]) do |result, key|
158
+ result += @cti_associations[key]
159
+ result
160
+ end
161
+ end
162
+ @cti_all_associations.include?(association_name.to_sym)
163
+ end
164
+
165
+ def cti_initialize_cti_associations
166
+ @cti_associations ||= {}
167
+ @cti_redefined_remote_associations ||= {}
168
+ [:has_many, :has_and_belongs_to_many, :has_one].each do |name|
169
+ @cti_associations[name] ||= []
170
+ @cti_redefined_remote_associations[name] ||= []
171
+ end
172
+ @cti_association_proxies ||= {}
173
+ end
174
+
175
+ end
176
+
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,30 @@
1
+ module DBViewCTI
2
+ module Model
3
+ module CTI
4
+ module Destroy
5
+ extend ActiveSupport::Concern
6
+
7
+ # change destroy and delete methods to operate on most specialized object
8
+ included do
9
+ alias_method_chain :destroy, :cti
10
+ alias_method_chain :delete, :cti
11
+ # destroy! seems te be defined in Rails 4
12
+ alias_method_chain :destroy!, :cti if self.method_defined?(:destroy!)
13
+ end
14
+
15
+ def destroy_with_cti
16
+ specialize.destroy_without_cti
17
+ end
18
+
19
+ def destroy_with_cti!
20
+ specialize.destroy_without_cti!
21
+ end
22
+
23
+ def delete_with_cti
24
+ specialize.delete_without_cti
25
+ end
26
+
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,58 @@
1
+ module DBViewCTI
2
+ module Model
3
+ module CTI
4
+ module Hierarchy
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+
9
+ def cti_base_class?
10
+ !!@cti_base_class
11
+ end
12
+
13
+ def cti_derived_class?
14
+ !!@cti_derived_class
15
+ end
16
+
17
+ attr_accessor :cti_descendants, :cti_ascendants, :cti_association_proxies
18
+
19
+ # registers a derived class and its descendants in the current class
20
+ # class_name: name of derived class (the one calling cti_register_descendants on this class)
21
+ # descendants: the descendants of the derived class
22
+ def cti_register_descendants(class_name, descendants = {})
23
+ @cti_descendants ||= {}
24
+ @cti_descendants[class_name] = descendants
25
+ if cti_derived_class?
26
+ # call up the chain. This will also cause the register_ascendants callbacks
27
+ self.superclass.cti_register_descendants(self.name, @cti_descendants)
28
+ end
29
+ # call back to calling class
30
+ @cti_ascendants ||= []
31
+ class_name.constantize.cti_register_ascendants(@cti_ascendants + [ self.name ])
32
+ end
33
+
34
+ # registers the ascendants of the current class. Called on this class by the parent class.
35
+ # ascendants: array of ascendants. The first element is the highest level class, derived
36
+ # classes follow, the last element is the parent of this class.
37
+ def cti_register_ascendants(ascendants)
38
+ @cti_ascendants = ascendants
39
+ end
40
+
41
+ # returns a list of all descendants
42
+ def cti_all_descendants
43
+ result = []
44
+ block = Proc.new do |klass, descendants|
45
+ result << klass
46
+ descendants.each(&block)
47
+ end
48
+ @cti_descendants ||= {}
49
+ @cti_descendants.each(&block)
50
+ result
51
+ end
52
+
53
+ end
54
+
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,23 @@
1
+ module DBViewCTI
2
+ module Model
3
+ module CTI
4
+ module SQL
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+
9
+ include DBViewCTI::SQLGeneration::Model
10
+
11
+ # this method is only used in testing. It returns the number of rows present in the real database
12
+ # table, not the number of rows present in the view (as returned by count)
13
+ def cti_table_count
14
+ result = connection.execute("SELECT COUNT(*) FROM #{DBViewCTI::Names.table_name(self)};")
15
+ result[0]['count'].to_i
16
+ end
17
+
18
+ end
19
+
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,55 @@
1
+ module DBViewCTI
2
+ module Model
3
+ module CTI
4
+ module TypeConversion
5
+
6
+ def specialize
7
+ class_name, id = type(true)
8
+ return self if class_name == self.class.name
9
+ class_name.constantize.find(id)
10
+ end
11
+
12
+ # Return the 'true' (i.e. most specialized) classname of this object
13
+ # When return_id is true, the 'specialized' database id is also returned
14
+ def type(return_id = false)
15
+ query, levels = self.class.cti_outer_join_sql(id)
16
+ result = self.class.connection.execute(query).first
17
+ # replace returned ids with the levels corresponding to their classes
18
+ result_levels = result.inject({}) do |hash, (k,v)|
19
+ hash[k] = levels[k] unless v.nil?
20
+ hash
21
+ end
22
+ # find class with maximum level value
23
+ foreign_key = result_levels.max_by { |k,v| v }.first
24
+ class_name = DBViewCTI::Names.table_to_class_name(foreign_key[0..-4])
25
+ if return_id
26
+ id_ = result[foreign_key].to_i
27
+ [class_name, id_]
28
+ else
29
+ class_name
30
+ end
31
+ end
32
+
33
+ def convert_to(type)
34
+ return nil unless persisted?
35
+ type_string = type.to_s
36
+ type_string = type_string.camelize if type.is_a?(Symbol)
37
+ return self if type_string == self.class.name
38
+ query = self.class.cti_inner_join_sql(id, type_string)
39
+ # query is nil when we try to cenvert to a descendant class (instead of an ascendant),
40
+ # or when we try to convert to a class outside of the hierarchy
41
+ if query.nil?
42
+ specialized = specialize
43
+ return nil if specialized == self
44
+ return specialized.convert_to(type_string)
45
+ end
46
+ result = self.class.connection.execute(query).first
47
+ id = result[ DBViewCTI::Names.foreign_key(type.to_s) ]
48
+ return nil if id.nil?
49
+ type_string.constantize.find(id.to_i)
50
+ end
51
+
52
+ end
53
+ end
54
+ end
55
+ end
@@ -3,328 +3,12 @@ module DBViewCTI
3
3
  module CTI
4
4
  extend ActiveSupport::Concern
5
5
 
6
- def specialize
7
- class_name, id = type(true)
8
- return self if class_name == self.class.name
9
- class_name.constantize.find(id)
10
- end
11
-
12
- # Return the 'true' (i.e. most specialized) classname of this object
13
- # When return_id is true, the 'specialized' database id is also returned
14
- def type(return_id = false)
15
- query, levels = self.class.cti_outer_join_sql(id)
16
- result = self.class.connection.execute(query).first
17
- # replace returned ids with the levels corresponding to their classes
18
- result_levels = result.inject({}) do |hash, (k,v)|
19
- hash[k] = levels[k] unless v.nil?
20
- hash
21
- end
22
- # find class with maximum level value
23
- foreign_key = result_levels.max_by { |k,v| v }.first
24
- class_name = DBViewCTI::Names.table_to_class_name(foreign_key[0..-4])
25
- if return_id
26
- id_ = result[foreign_key].to_i
27
- [class_name, id_]
28
- else
29
- class_name
30
- end
31
- end
32
-
33
- def convert_to(type)
34
- return nil unless persisted?
35
- type_string = type.to_s
36
- type_string = type_string.camelize if type.is_a?(Symbol)
37
- return self if type_string == self.class.name
38
- query = self.class.cti_inner_join_sql(id, type_string)
39
- # query is nil when we try to cenvert to a descendant class (instead of an ascendant),
40
- # or when we try to convert to a class outside of the hierarchy
41
- if query.nil?
42
- specialized = specialize
43
- return nil if specialized == self
44
- return specialized.convert_to(type_string)
45
- end
46
- result = self.class.connection.execute(query).first
47
- id = result[ DBViewCTI::Names.foreign_key(type.to_s) ]
48
- return nil if id.nil?
49
- type_string.constantize.find(id.to_i)
50
- end
51
-
52
- # change destroy and delete methods to operate on most specialized object
53
- included do
54
- alias_method_chain :destroy, :cti
55
- alias_method_chain :delete, :cti
56
- # destroy! seems te be defined in Rails 4
57
- alias_method_chain :destroy!, :cti if self.method_defined?(:destroy!)
58
-
59
- # for associations:
60
- alias_method_chain :association, :cti
61
- # save callbacks (necessary for saving associations)
62
- after_save :cti_save_associations
63
-
64
- # validations
65
- validate :cti_validate_associations, :cti_no_disable => true
66
- attr_accessor :cti_disable_validations
67
- end
68
-
69
- def destroy_with_cti
70
- specialize.destroy_without_cti
71
- end
72
-
73
- def destroy_with_cti!
74
- specialize.destroy_without_cti!
75
- end
76
-
77
- def delete_with_cti
78
- specialize.delete_without_cti
79
- end
80
-
81
- def cti_validate_associations
82
- return_value = true
83
- self.class.cti_association_proxies.each_key do |proxy_name|
84
- proxy = instance_variable_get(proxy_name)
85
- if proxy && !proxy.valid?
86
- errors.messages.merge!(proxy.errors.messages)
87
- return_value = false
88
- end
89
- end
90
- return_value
91
- end
92
-
93
- def cti_save_associations
94
- self.class.cti_association_proxies.each_key do |proxy_name|
95
- proxy = instance_variable_get(proxy_name)
96
- proxy.save if proxy
97
- end
98
- true
99
- end
100
-
101
- def association_with_cti(*args)
102
- return association_without_cti(*args) unless args.length == 1
103
- association_name = args[0]
104
- proxy = cti_association_proxy(association_name)
105
- proxy ||= self
106
- proxy.association_without_cti(association_name)
107
- end
108
-
109
- def cti_association_proxy(association_name)
110
- return nil if self.class.reflect_on_all_associations(:belongs_to).map(&:name).include?(association_name.to_sym)
111
- proxy_name = self.class.cti_association_proxy_name(association_name)
112
- proxy = instance_variable_get(proxy_name)
113
- if !proxy && !self.class.cti_has_association?(association_name)
114
- instance_variable_set(proxy_name,
115
- ModelDelegator.new(self, self.class.cti_association_proxies[proxy_name]))
116
- proxy = instance_variable_get(proxy_name)
117
- end
118
- proxy
119
- end
120
-
121
- module ClassMethods
122
-
123
- def cti_base_class?
124
- !!@cti_base_class
125
- end
126
-
127
- def cti_derived_class?
128
- !!@cti_derived_class
129
- end
130
-
131
- attr_accessor :cti_descendants, :cti_ascendants, :cti_association_proxies
132
-
133
- # registers a derived class and its descendants in the current class
134
- # class_name: name of derived class (the one calling cti_register_descendants on this class)
135
- # descendants: the descendants of the derived class
136
- def cti_register_descendants(class_name, descendants = {})
137
- @cti_descendants ||= {}
138
- @cti_descendants[class_name] = descendants
139
- if cti_derived_class?
140
- # call up the chain. This will also cause the register_ascendants callbacks
141
- self.superclass.cti_register_descendants(self.name, @cti_descendants)
142
- end
143
- # call back to calling class
144
- @cti_ascendants ||= []
145
- class_name.constantize.cti_register_ascendants(@cti_ascendants + [ self.name ])
146
- end
147
-
148
- # registers the ascendants of the current class. Called on this class by the parent class.
149
- # ascendants: array of ascendants. The first element is the highest level class, derived
150
- # classes follow, the last element is the parent of this class.
151
- def cti_register_ascendants(ascendants)
152
- @cti_ascendants = ascendants
153
- end
154
-
155
- # returns a list of all descendants
156
- def cti_all_descendants
157
- result = []
158
- block = Proc.new do |klass, descendants|
159
- result << klass
160
- descendants.each(&block)
161
- end
162
- @cti_descendants ||= {}
163
- @cti_descendants.each(&block)
164
- result
165
- end
166
-
167
- # redefine validate to always add :unless proc so we can disable the validations for an object
168
- # by setting the cti_disable_validations accessor to true
169
- def validate(*args, &block)
170
- # we specifically don't want to disable balidations belonging to associations. Based on the naming
171
- # rails uses, we return immediately in such cases (there must be a cleaner way to do this...)
172
- return super if args.first && args.first.to_s =~ /^validate_associated_records_for_/
173
- # rest of implementation insipred by the validate implementation in rails
174
- options = args.extract_options!.dup
175
- return super if options[:cti_no_disable]
176
- if options.key?(:unless)
177
- options[:unless] = Array(options[:unless])
178
- options[:unless].unshift( cti_validation_unless_proc )
179
- else
180
- options[:unless] = cti_validation_unless_proc
181
- end
182
- args << options
183
- return super(*args, &block)
184
- end
185
-
186
- def cti_validation_unless_proc
187
- @cti_validation_unless_proc ||= Proc.new do |object|
188
- object.respond_to?(:cti_disable_validations) && object.cti_disable_validations
189
- end
190
- end
191
-
192
- # redefine association class methods
193
- [:has_many, :has_and_belongs_to_many, :has_one].each do |name|
194
- self.class_eval <<-eos, __FILE__, __LINE__+1
195
- def #{name}(*args, &block)
196
- cti_initialize_cti_associations
197
- @cti_associations[:#{name}] << args.first
198
- super
199
- end
200
- eos
201
- end
202
-
203
- def cti_create_association_proxies
204
- # create hash with proxy and class names. The proxies themselves will be created
205
- # by the 'association' instance method when the association is used for the first time.
206
- @cti_association_proxies ||= {}
207
- @cti_ascendants.each do |ascendant|
208
- [:has_many, :has_and_belongs_to_many, :has_one].each do |association_type|
209
- ascendant.constantize.cti_associations[association_type].each do |association|
210
- proxy_name = cti_association_proxy_name(association)
211
- @cti_association_proxies[proxy_name] = ascendant
212
- end
213
- end
214
- end
215
- end
216
-
217
- # fix the 'remote' (i.e. belongs_to) part of any has_one of has_many association in this class
218
- def cti_redefine_remote_associations
219
- cti_initialize_cti_associations
220
- # redefine remote belongs_to associations
221
- [:has_many, :has_one].each do |association_type|
222
- @cti_associations[association_type].each do |association|
223
- next if @cti_redefined_remote_associations[association_type].include?( association )
224
- if cti_reciprocal_association_present_for?( association, :belongs_to )
225
- remote_class = cti_association_name_to_class_name( association ).constantize
226
- cti_redefine_remote_belongs_to_association(remote_class, cti_class_name_to_association_name.to_sym)
227
- @cti_redefined_remote_associations[association_type] << association
228
- end
229
- end
230
- end
231
- # redefine remote has_many and has_and_belongs_to_many associations
232
- [:has_many, :has_and_belongs_to_many].each do |association_type|
233
- @cti_associations[association_type].each do |association|
234
- next if @cti_redefined_remote_associations[association_type].include?( association )
235
- if cti_reciprocal_association_present_for?( association, association_type, true )
236
- remote_class = cti_association_name_to_class_name( association ).constantize
237
- cti_redefine_remote_to_many_association(remote_class, cti_class_name_to_association_name( true ).to_sym)
238
- @cti_redefined_remote_associations[association_type] << association
239
- end
240
- end
241
- end
242
- end
243
-
244
- # Check if a reciprocal association of type 'type' is present for the given association.
245
- # (example: check if a has_many association has a corresponding belongs_to in the remote class).
246
- # Plural indicates wether the remote association we're looking for is plural (i.e. has_many :space_ships)
247
- # or singular (i.e. belongs_to :space_ship).
248
- # Normally, the method checks if the remote association refers to this class, but it is possible to
249
- # pass in 'class_name' to check different classes
250
- def cti_reciprocal_association_present_for?(association, type, plural = false, class_name = nil)
251
- remote_class = cti_association_name_to_class_name( association ).constantize
252
- remote_associations = remote_class.reflect_on_all_associations( type ).map(&:name)
253
- remote_associations.include?( cti_class_name_to_association_name(plural, class_name).to_sym )
254
- end
255
-
256
- # converts e.g. SpaceShip to :space_ship (for plural == false), or :space_ships (for plural true)
257
- def cti_class_name_to_association_name(plural = false, class_name = nil)
258
- class_name ||= self.name
259
- association_name = class_name.underscore
260
- association_name = association_name.pluralize if plural
261
- association_name
262
- end
263
-
264
- # converts e.g. :space_ships to SpaceShip
265
- def cti_association_name_to_class_name(association_name)
266
- association_name.to_s.camelize.singularize
267
- end
268
-
269
- def cti_redefine_remote_belongs_to_association(remote_class, remote_association)
270
- remote_class.class_eval <<-eos, __FILE__, __LINE__+1
271
- def #{remote_association}=(object, *args, &block)
272
- super( object.try(:convert_to, '#{self.name}'), *args, &block )
273
- end
274
- eos
275
- end
276
-
277
- def cti_redefine_remote_to_many_association(remote_class, remote_association)
278
- remote_class.class_eval <<-eos, __FILE__, __LINE__+1
279
- def #{remote_association}=(objects, *args, &block)
280
- super( objects.map { |o| o.try(:convert_to, '#{self.name}') }, *args, &block)
281
- end
282
- def #{remote_association}(*args, &block)
283
- collection = super
284
- DBViewCTI::Model::CollectionDelegator.new(collection, '#{self.name}')
285
- end
286
- eos
287
- end
288
-
289
- def cti_association_proxy_name(association)
290
- "@cti_#{association}_association_proxy"
291
- end
292
-
293
- def cti_associations
294
- cti_initialize_cti_associations
295
- @cti_associations
296
- end
297
-
298
- def cti_has_association?(association_name)
299
- if !@cti_all_associations
300
- @cti_all_associations = @cti_associations.keys.inject([]) do |result, key|
301
- result += @cti_associations[key]
302
- result
303
- end
304
- end
305
- @cti_all_associations.include?(association_name.to_sym)
306
- end
307
-
308
- include DBViewCTI::SQLGeneration::Model
309
-
310
- # this method is only used in testing. It returns the number of rows present in the real database
311
- # table, not the number of rows present in the view (as returned by count)
312
- def cti_table_count
313
- result = connection.execute("SELECT COUNT(*) FROM #{DBViewCTI::Names.table_name(self)};")
314
- result[0]['count'].to_i
315
- end
316
-
317
- def cti_initialize_cti_associations
318
- @cti_associations ||= {}
319
- @cti_redefined_remote_associations ||= {}
320
- [:has_many, :has_and_belongs_to_many, :has_one].each do |name|
321
- @cti_associations[name] ||= []
322
- @cti_redefined_remote_associations[name] ||= []
323
- end
324
- @cti_association_proxies ||= {}
325
- end
326
-
327
- end
6
+ include Hierarchy
7
+ include TypeConversion
8
+ include Destroy
9
+ include SQL
10
+ include Associations
11
+ include AssociationValidations
328
12
  end
329
13
  end
330
14
  end
@@ -1,3 +1,3 @@
1
1
  module DBViewCTI
2
- VERSION = "0.1.4"
2
+ VERSION = "0.1.5"
3
3
  end
data/lib/dbview_cti.rb CHANGED
@@ -1,3 +1,8 @@
1
+ ActiveSupport::Inflector.inflections do |inflect|
2
+ inflect.acronym('CTI')
3
+ inflect.acronym('CTIs')
4
+ end
5
+
1
6
  module DBViewCTI
2
7
  extend ActiveSupport::Autoload
3
8
  autoload :Names
@@ -30,6 +35,12 @@ module DBViewCTI
30
35
  autoload :Extensions
31
36
  autoload :ModelDelegator
32
37
  autoload :CollectionDelegator
38
+ autoload :TypeConversion, 'db_view_cti/model/cti/type_conversion'
39
+ autoload :Hierarchy, 'db_view_cti/model/cti/hierarchy'
40
+ autoload :Destroy, 'db_view_cti/model/cti/destroy'
41
+ autoload :SQL, 'db_view_cti/model/cti/sql'
42
+ autoload :Associations, 'db_view_cti/model/cti/associations'
43
+ autoload :AssociationValidations, 'db_view_cti/model/cti/association_validations'
33
44
  end
34
45
  end
35
46
 
@@ -16,4 +16,6 @@ class SpaceShip < Vehicle
16
16
  has_many :experiment_space_ship_performances
17
17
  has_many :experiments, :through => :experiment_space_ship_performances
18
18
  accepts_nested_attributes_for :experiments
19
+
20
+ has_many :upgraded_to, :class_name => 'SpaceShuttle', :foreign_key => 'upgraded_from_id'
19
21
  end
@@ -1,4 +1,9 @@
1
1
  class SpaceShuttle < SpaceShip
2
2
  attr_accessible :single_use unless Rails::VERSION::MAJOR > 3
3
+
4
+ belongs_to :upgraded_from, :class_name => 'SpaceShip'
5
+
6
+ # cti_derived_class has to come after te above belongs_to, otherwise the association will not work correctly.
7
+ # This is only because SpaceShuttle is a leaf class (i.e. had no descendants).
3
8
  cti_derived_class
4
9
  end
@@ -0,0 +1,15 @@
1
+ class AddUpgradedFromToSpaceShuttles < ActiveRecord::Migration
2
+ def up
3
+ cti_recreate_views_after_change_to('SpaceShuttle') do
4
+ add_column(:space_shuttles, :upgraded_from_id, :integer)
5
+ end
6
+ add_foreign_key :space_shuttles, :space_ships, :column => 'upgraded_from_id'
7
+ end
8
+
9
+ def down
10
+ cti_recreate_views_after_change_to('SpaceShuttle') do
11
+ remove_column(:space_shuttles, :upgraded_from_id)
12
+ end
13
+ remove_foreign_key :space_shuttles, :space_ships, :column => 'upgraded_from_id'
14
+ end
15
+ end
@@ -0,0 +1,14 @@
1
+ class AddFieldToBaseClass < ActiveRecord::Migration
2
+ def up
3
+ # this makes cti_recreate_views_after_change_to also works for the base class
4
+ cti_recreate_views_after_change_to('Vehicle') do
5
+ add_column(:vehicles, :bogus_field, :string)
6
+ end
7
+ end
8
+
9
+ def down
10
+ cti_recreate_views_after_change_to('Vehicle') do
11
+ remove_column(:vehicles, :bogus_field)
12
+ end
13
+ end
14
+ end
@@ -11,7 +11,10 @@
11
11
  #
12
12
  # It's strongly recommended that you check this file into your version control system.
13
13
 
14
- ActiveRecord::Schema.define(version: 20131022020655) do
14
+ ActiveRecord::Schema.define(version: 20140425182847) do
15
+
16
+ # These are extensions that must be enabled in order to support this database
17
+ enable_extension "plpgsql"
15
18
 
16
19
  create_table "astronauts", force: true do |t|
17
20
  t.string "name"
@@ -39,6 +42,26 @@ ActiveRecord::Schema.define(version: 20131022020655) do
39
42
  t.datetime "updated_at"
40
43
  end
41
44
 
45
+ create_table "categories", force: true do |t|
46
+ t.string "name"
47
+ t.datetime "created_at"
48
+ t.datetime "updated_at"
49
+ end
50
+
51
+ create_table "experiment_space_ship_performances", force: true do |t|
52
+ t.integer "experiment_id"
53
+ t.integer "space_ship_id"
54
+ t.date "performed_at"
55
+ t.datetime "created_at"
56
+ t.datetime "updated_at"
57
+ end
58
+
59
+ create_table "experiments", force: true do |t|
60
+ t.string "name"
61
+ t.datetime "created_at"
62
+ t.datetime "updated_at"
63
+ end
64
+
42
65
  create_table "launches", force: true do |t|
43
66
  t.integer "space_ship_id"
44
67
  t.date "date"
@@ -70,6 +93,7 @@ ActiveRecord::Schema.define(version: 20131022020655) do
70
93
 
71
94
  create_table "space_ships", force: true do |t|
72
95
  t.integer "vehicle_id"
96
+ t.integer "category_id"
73
97
  t.boolean "single_use"
74
98
  t.datetime "created_at"
75
99
  t.datetime "updated_at"
@@ -81,6 +105,7 @@ ActiveRecord::Schema.define(version: 20131022020655) do
81
105
  t.integer "power"
82
106
  t.datetime "created_at"
83
107
  t.datetime "updated_at"
108
+ t.integer "upgraded_from_id"
84
109
  end
85
110
 
86
111
  create_table "vehicles", force: true do |t|
@@ -88,6 +113,7 @@ ActiveRecord::Schema.define(version: 20131022020655) do
88
113
  t.integer "mass"
89
114
  t.datetime "created_at"
90
115
  t.datetime "updated_at"
116
+ t.string "bogus_field"
91
117
  end
92
118
 
93
119
  cti_create_view('MotorVehicle')
@@ -101,12 +127,17 @@ ActiveRecord::Schema.define(version: 20131022020655) do
101
127
 
102
128
  add_foreign_key "captains", "space_ships", :name => "captains_space_ship_id_fk"
103
129
 
130
+ add_foreign_key "experiment_space_ship_performances", "experiments", :name => "experiment_space_ship_performances_experiment_id_fk"
131
+ add_foreign_key "experiment_space_ship_performances", "space_ships", :name => "experiment_space_ship_performances_space_ship_id_fk"
132
+
104
133
  add_foreign_key "launches", "space_ships", :name => "launches_space_ship_id_fk"
105
134
 
106
135
  add_foreign_key "rocket_engines", "space_ships", :name => "rocket_engines_space_ship_id_fk"
107
136
 
137
+ add_foreign_key "space_ships", "categories", :name => "space_ships_category_id_fk"
108
138
  add_foreign_key "space_ships", "vehicles", :name => "space_ships_vehicle_id_fk"
109
139
 
110
140
  add_foreign_key "space_shuttles", "space_ships", :name => "space_shuttles_space_ship_id_fk"
141
+ add_foreign_key "space_shuttles", "space_ships", :name => "space_shuttles_upgraded_from_id_fk", :column => "upgraded_from_id"
111
142
 
112
143
  end
@@ -18,11 +18,13 @@ describe Car do
18
18
  @car.name = 'Porsche'
19
19
  @car.fuel = 'gasoline'
20
20
  @car.convertible = true
21
+ @car.bogus_field = 'bogus'
21
22
  @car.save!
22
23
  car = Car.find(id)
23
24
  car.name.should eq 'Porsche'
24
25
  car.mass.should eq 1000
25
26
  car.fuel.should eq 'gasoline'
27
+ car.bogus_field.should eq 'bogus'
26
28
  car.stick_shift.should be_true
27
29
  car.convertible.should be_true
28
30
  end
@@ -441,6 +441,21 @@ describe SpaceShuttle do
441
441
  }.to change(SpaceShuttle, :count).by(1)
442
442
  end
443
443
 
444
+ it "association logic also works for associations with non-standard names" do
445
+ # check has_many side
446
+ shuttle2 = SpaceShuttle.create(:name => 'Endeavour', :reliability => 100)
447
+ @shuttle.upgraded_to << shuttle2
448
+ @shuttle.save!
449
+ shuttle2.reload
450
+ shuttle2.upgraded_from.specialize.id.should eq @shuttle.id
451
+ # check belongs_to side
452
+ shuttle3 = SpaceShuttle.create(:name => 'Endeavour', :reliability => 100)
453
+ @shuttle.upgraded_from = shuttle3
454
+ @shuttle.save!
455
+ @shuttle.reload
456
+ @shuttle.upgraded_from.specialize.id.should eq shuttle3.id
457
+ end
458
+
444
459
  end
445
460
 
446
461
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dbview_cti
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-04-08 00:00:00.000000000 Z
12
+ date: 2014-04-25 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -80,6 +80,12 @@ files:
80
80
  - lib/db_view_cti/migration/command_recorder.rb
81
81
  - lib/db_view_cti/model/collection_delegator.rb
82
82
  - lib/db_view_cti/model/cti.rb
83
+ - lib/db_view_cti/model/cti/association_validations.rb
84
+ - lib/db_view_cti/model/cti/associations.rb
85
+ - lib/db_view_cti/model/cti/destroy.rb
86
+ - lib/db_view_cti/model/cti/hierarchy.rb
87
+ - lib/db_view_cti/model/cti/sql.rb
88
+ - lib/db_view_cti/model/cti/type_conversion.rb
83
89
  - lib/db_view_cti/model/extensions.rb
84
90
  - lib/db_view_cti/model/model_delegator.rb
85
91
  - lib/db_view_cti/names.rb
@@ -149,6 +155,8 @@ files:
149
155
  - spec/dummy-rails-3/db/migrate/20131022030659_create_experiments.rb
150
156
  - spec/dummy-rails-3/db/migrate/20131022030720_create_experiment_space_ship_performances.rb
151
157
  - spec/dummy-rails-3/db/migrate/20140408013710_check_view_exists.rb
158
+ - spec/dummy-rails-3/db/migrate/20140411001620_add_upgraded_from_to_space_shuttles.rb
159
+ - spec/dummy-rails-3/db/migrate/20140425182847_add_field_to_base_class.rb
152
160
  - spec/dummy-rails-3/db/schema.rb
153
161
  - spec/dummy-rails-3/lib/assets/.gitkeep
154
162
  - spec/dummy-rails-3/log/.gitkeep
@@ -215,7 +223,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
215
223
  version: '0'
216
224
  segments:
217
225
  - 0
218
- hash: 1297181597632204944
226
+ hash: 1166807647709519992
219
227
  required_rubygems_version: !ruby/object:Gem::Requirement
220
228
  none: false
221
229
  requirements:
@@ -224,7 +232,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
224
232
  version: '0'
225
233
  segments:
226
234
  - 0
227
- hash: 1297181597632204944
235
+ hash: 1166807647709519992
228
236
  requirements: []
229
237
  rubyforge_project:
230
238
  rubygems_version: 1.8.25