dbview_cti 0.1.4 → 0.1.5

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