activerecord 3.2.22.5 → 4.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (162) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1024 -543
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +20 -29
  5. data/examples/performance.rb +1 -1
  6. data/lib/active_record.rb +55 -44
  7. data/lib/active_record/aggregations.rb +40 -34
  8. data/lib/active_record/associations.rb +204 -276
  9. data/lib/active_record/associations/alias_tracker.rb +1 -1
  10. data/lib/active_record/associations/association.rb +30 -35
  11. data/lib/active_record/associations/association_scope.rb +40 -40
  12. data/lib/active_record/associations/belongs_to_association.rb +15 -2
  13. data/lib/active_record/associations/builder/association.rb +81 -28
  14. data/lib/active_record/associations/builder/belongs_to.rb +35 -57
  15. data/lib/active_record/associations/builder/collection_association.rb +54 -40
  16. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +23 -41
  17. data/lib/active_record/associations/builder/has_many.rb +8 -64
  18. data/lib/active_record/associations/builder/has_one.rb +13 -50
  19. data/lib/active_record/associations/builder/singular_association.rb +13 -13
  20. data/lib/active_record/associations/collection_association.rb +92 -88
  21. data/lib/active_record/associations/collection_proxy.rb +913 -63
  22. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +12 -10
  23. data/lib/active_record/associations/has_many_association.rb +35 -9
  24. data/lib/active_record/associations/has_many_through_association.rb +24 -14
  25. data/lib/active_record/associations/has_one_association.rb +33 -13
  26. data/lib/active_record/associations/has_one_through_association.rb +1 -1
  27. data/lib/active_record/associations/join_dependency.rb +2 -2
  28. data/lib/active_record/associations/join_dependency/join_association.rb +17 -22
  29. data/lib/active_record/associations/join_dependency/join_part.rb +1 -1
  30. data/lib/active_record/associations/join_helper.rb +1 -11
  31. data/lib/active_record/associations/preloader.rb +14 -17
  32. data/lib/active_record/associations/preloader/association.rb +29 -33
  33. data/lib/active_record/associations/preloader/collection_association.rb +1 -1
  34. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +1 -1
  35. data/lib/active_record/associations/preloader/has_many_through.rb +1 -1
  36. data/lib/active_record/associations/preloader/has_one.rb +1 -1
  37. data/lib/active_record/associations/preloader/through_association.rb +13 -17
  38. data/lib/active_record/associations/singular_association.rb +11 -11
  39. data/lib/active_record/associations/through_association.rb +2 -2
  40. data/lib/active_record/attribute_assignment.rb +133 -153
  41. data/lib/active_record/attribute_methods.rb +196 -93
  42. data/lib/active_record/attribute_methods/before_type_cast.rb +44 -5
  43. data/lib/active_record/attribute_methods/dirty.rb +31 -28
  44. data/lib/active_record/attribute_methods/primary_key.rb +38 -30
  45. data/lib/active_record/attribute_methods/query.rb +5 -4
  46. data/lib/active_record/attribute_methods/read.rb +62 -91
  47. data/lib/active_record/attribute_methods/serialization.rb +97 -66
  48. data/lib/active_record/attribute_methods/time_zone_conversion.rb +39 -45
  49. data/lib/active_record/attribute_methods/write.rb +32 -39
  50. data/lib/active_record/autosave_association.rb +56 -70
  51. data/lib/active_record/base.rb +53 -450
  52. data/lib/active_record/callbacks.rb +53 -18
  53. data/lib/active_record/coders/yaml_column.rb +11 -9
  54. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +353 -197
  55. data/lib/active_record/connection_adapters/abstract/database_limits.rb +9 -0
  56. data/lib/active_record/connection_adapters/abstract/database_statements.rb +130 -131
  57. data/lib/active_record/connection_adapters/abstract/query_cache.rb +24 -19
  58. data/lib/active_record/connection_adapters/abstract/quoting.rb +23 -3
  59. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +101 -91
  60. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +59 -0
  61. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +225 -96
  62. data/lib/active_record/connection_adapters/abstract/transaction.rb +203 -0
  63. data/lib/active_record/connection_adapters/abstract_adapter.rb +99 -46
  64. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +114 -36
  65. data/lib/active_record/connection_adapters/column.rb +46 -24
  66. data/lib/active_record/connection_adapters/connection_specification.rb +96 -0
  67. data/lib/active_record/connection_adapters/mysql2_adapter.rb +16 -32
  68. data/lib/active_record/connection_adapters/mysql_adapter.rb +181 -64
  69. data/lib/active_record/connection_adapters/postgresql/array_parser.rb +97 -0
  70. data/lib/active_record/connection_adapters/postgresql/cast.rb +132 -0
  71. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +242 -0
  72. data/lib/active_record/connection_adapters/postgresql/oid.rb +347 -0
  73. data/lib/active_record/connection_adapters/postgresql/quoting.rb +158 -0
  74. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +30 -0
  75. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +448 -0
  76. data/lib/active_record/connection_adapters/postgresql_adapter.rb +454 -885
  77. data/lib/active_record/connection_adapters/schema_cache.rb +48 -16
  78. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +574 -13
  79. data/lib/active_record/connection_handling.rb +98 -0
  80. data/lib/active_record/core.rb +428 -0
  81. data/lib/active_record/counter_cache.rb +106 -108
  82. data/lib/active_record/dynamic_matchers.rb +110 -63
  83. data/lib/active_record/errors.rb +25 -8
  84. data/lib/active_record/explain.rb +8 -58
  85. data/lib/active_record/explain_subscriber.rb +6 -3
  86. data/lib/active_record/fixture_set/file.rb +56 -0
  87. data/lib/active_record/fixtures.rb +146 -148
  88. data/lib/active_record/inheritance.rb +77 -59
  89. data/lib/active_record/integration.rb +5 -5
  90. data/lib/active_record/locale/en.yml +8 -1
  91. data/lib/active_record/locking/optimistic.rb +38 -42
  92. data/lib/active_record/locking/pessimistic.rb +4 -4
  93. data/lib/active_record/log_subscriber.rb +19 -9
  94. data/lib/active_record/migration.rb +318 -153
  95. data/lib/active_record/migration/command_recorder.rb +90 -31
  96. data/lib/active_record/migration/join_table.rb +15 -0
  97. data/lib/active_record/model_schema.rb +69 -92
  98. data/lib/active_record/nested_attributes.rb +113 -148
  99. data/lib/active_record/null_relation.rb +65 -0
  100. data/lib/active_record/persistence.rb +188 -97
  101. data/lib/active_record/query_cache.rb +18 -36
  102. data/lib/active_record/querying.rb +19 -15
  103. data/lib/active_record/railtie.rb +91 -36
  104. data/lib/active_record/railties/console_sandbox.rb +0 -2
  105. data/lib/active_record/railties/controller_runtime.rb +2 -2
  106. data/lib/active_record/railties/databases.rake +90 -309
  107. data/lib/active_record/railties/jdbcmysql_error.rb +1 -1
  108. data/lib/active_record/readonly_attributes.rb +7 -3
  109. data/lib/active_record/reflection.rb +72 -56
  110. data/lib/active_record/relation.rb +241 -157
  111. data/lib/active_record/relation/batches.rb +25 -22
  112. data/lib/active_record/relation/calculations.rb +143 -121
  113. data/lib/active_record/relation/delegation.rb +96 -18
  114. data/lib/active_record/relation/finder_methods.rb +117 -183
  115. data/lib/active_record/relation/merger.rb +133 -0
  116. data/lib/active_record/relation/predicate_builder.rb +90 -42
  117. data/lib/active_record/relation/query_methods.rb +666 -136
  118. data/lib/active_record/relation/spawn_methods.rb +43 -150
  119. data/lib/active_record/result.rb +33 -6
  120. data/lib/active_record/sanitization.rb +24 -50
  121. data/lib/active_record/schema.rb +19 -12
  122. data/lib/active_record/schema_dumper.rb +31 -39
  123. data/lib/active_record/schema_migration.rb +36 -0
  124. data/lib/active_record/scoping.rb +0 -124
  125. data/lib/active_record/scoping/default.rb +48 -45
  126. data/lib/active_record/scoping/named.rb +74 -103
  127. data/lib/active_record/serialization.rb +6 -2
  128. data/lib/active_record/serializers/xml_serializer.rb +9 -15
  129. data/lib/active_record/store.rb +119 -15
  130. data/lib/active_record/tasks/database_tasks.rb +158 -0
  131. data/lib/active_record/tasks/mysql_database_tasks.rb +138 -0
  132. data/lib/active_record/tasks/postgresql_database_tasks.rb +90 -0
  133. data/lib/active_record/tasks/sqlite_database_tasks.rb +51 -0
  134. data/lib/active_record/test_case.rb +61 -38
  135. data/lib/active_record/timestamp.rb +8 -9
  136. data/lib/active_record/transactions.rb +65 -51
  137. data/lib/active_record/validations.rb +17 -15
  138. data/lib/active_record/validations/associated.rb +20 -14
  139. data/lib/active_record/validations/presence.rb +65 -0
  140. data/lib/active_record/validations/uniqueness.rb +93 -52
  141. data/lib/active_record/version.rb +4 -4
  142. data/lib/rails/generators/active_record.rb +3 -5
  143. data/lib/rails/generators/active_record/migration/migration_generator.rb +37 -7
  144. data/lib/rails/generators/active_record/migration/templates/migration.rb +20 -15
  145. data/lib/rails/generators/active_record/model/model_generator.rb +4 -3
  146. data/lib/rails/generators/active_record/model/templates/model.rb +1 -6
  147. data/lib/rails/generators/active_record/model/templates/module.rb +1 -1
  148. metadata +53 -46
  149. data/lib/active_record/attribute_methods/deprecated_underscore_read.rb +0 -32
  150. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +0 -191
  151. data/lib/active_record/connection_adapters/sqlite_adapter.rb +0 -583
  152. data/lib/active_record/dynamic_finder_match.rb +0 -68
  153. data/lib/active_record/dynamic_scope_match.rb +0 -23
  154. data/lib/active_record/fixtures/file.rb +0 -65
  155. data/lib/active_record/identity_map.rb +0 -162
  156. data/lib/active_record/observer.rb +0 -121
  157. data/lib/active_record/session_store.rb +0 -360
  158. data/lib/rails/generators/active_record/migration.rb +0 -15
  159. data/lib/rails/generators/active_record/observer/observer_generator.rb +0 -15
  160. data/lib/rails/generators/active_record/observer/templates/observer.rb +0 -4
  161. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +0 -25
  162. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +0 -12
@@ -2,16 +2,16 @@ module ActiveRecord
2
2
  module Associations
3
3
  class Preloader
4
4
  class Association #:nodoc:
5
- attr_reader :owners, :reflection, :preload_options, :model, :klass
6
-
7
- def initialize(klass, owners, reflection, preload_options)
8
- @klass = klass
9
- @owners = owners
10
- @reflection = reflection
11
- @preload_options = preload_options || {}
12
- @model = owners.first && owners.first.class
13
- @scoped = nil
14
- @owners_by_key = nil
5
+ attr_reader :owners, :reflection, :preload_scope, :model, :klass
6
+
7
+ def initialize(klass, owners, reflection, preload_scope)
8
+ @klass = klass
9
+ @owners = owners
10
+ @reflection = reflection
11
+ @preload_scope = preload_scope
12
+ @model = owners.first && owners.first.class
13
+ @scope = nil
14
+ @owners_by_key = nil
15
15
  end
16
16
 
17
17
  def run
@@ -24,12 +24,12 @@ module ActiveRecord
24
24
  raise NotImplementedError
25
25
  end
26
26
 
27
- def scoped
28
- @scoped ||= build_scope
27
+ def scope
28
+ @scope ||= build_scope
29
29
  end
30
30
 
31
31
  def records_for(ids)
32
- scoped.where(association_key.in(ids))
32
+ scope.where(association_key.in(ids))
33
33
  end
34
34
 
35
35
  def table
@@ -76,8 +76,8 @@ module ActiveRecord
76
76
  else
77
77
  # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
78
78
  # Make several smaller queries if necessary or make one query if the adapter supports it
79
- sliced = owner_keys.each_slice(model.connection.in_clause_length || owner_keys.size)
80
- records = sliced.map { |slice| records_for(slice) }.flatten
79
+ sliced = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size)
80
+ records = sliced.map { |slice| records_for(slice).to_a }.flatten
81
81
  end
82
82
 
83
83
  # Each record may have multiple owners, and vice-versa
@@ -92,33 +92,29 @@ module ActiveRecord
92
92
  records_by_owner
93
93
  end
94
94
 
95
+ def reflection_scope
96
+ @reflection_scope ||= reflection.scope ? klass.unscoped.instance_exec(nil, &reflection.scope) : klass.unscoped
97
+ end
98
+
95
99
  def build_scope
96
- scope = klass.scoped
100
+ scope = klass.unscoped
101
+ scope.default_scoped = true
97
102
 
98
- scope = scope.where(process_conditions(options[:conditions]))
99
- scope = scope.where(process_conditions(preload_options[:conditions]))
103
+ values = reflection_scope.values
104
+ preload_values = preload_scope.values
100
105
 
101
- scope = scope.select(preload_options[:select] || options[:select] || table[Arel.star])
102
- scope = scope.includes(preload_options[:include] || options[:include])
106
+ scope.where_values = Array(values[:where]) + Array(preload_values[:where])
107
+ scope.references_values = Array(values[:references]) + Array(preload_values[:references])
108
+
109
+ scope.select! preload_values[:select] || values[:select] || table[Arel.star]
110
+ scope.includes! preload_values[:includes] || values[:includes]
103
111
 
104
112
  if options[:as]
105
- scope = scope.where(
106
- klass.table_name => {
107
- reflection.type => model.base_class.sti_name
108
- }
109
- )
113
+ scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name })
110
114
  end
111
115
 
112
116
  scope
113
117
  end
114
-
115
- def process_conditions(conditions)
116
- if conditions.respond_to?(:to_proc) && !conditions.is_a?(Hash)
117
- conditions = klass.send(:instance_eval, &conditions)
118
- end
119
-
120
- conditions
121
- end
122
118
  end
123
119
  end
124
120
  end
@@ -6,7 +6,7 @@ module ActiveRecord
6
6
  private
7
7
 
8
8
  def build_scope
9
- super.order(preload_options[:order] || options[:order])
9
+ super.order(preload_scope.values[:order] || reflection_scope.values[:order])
10
10
  end
11
11
 
12
12
  def preload
@@ -6,7 +6,7 @@ module ActiveRecord
6
6
 
7
7
  def initialize(klass, records, reflection, preload_options)
8
8
  super
9
- @join_table = Arel::Table.new(options[:join_table]).alias('t0')
9
+ @join_table = Arel::Table.new(reflection.join_table).alias('t0')
10
10
  end
11
11
 
12
12
  # Unlike the other associations, we want to get a raw array of rows so that we can
@@ -6,7 +6,7 @@ module ActiveRecord
6
6
 
7
7
  def associated_records_by_owner
8
8
  super.each do |owner, records|
9
- records.uniq! if options[:uniq]
9
+ records.uniq! if reflection_scope.uniq_value
10
10
  end
11
11
  end
12
12
  end
@@ -14,7 +14,7 @@ module ActiveRecord
14
14
  private
15
15
 
16
16
  def build_scope
17
- super.order(preload_options[:order] || options[:order])
17
+ super.order(preload_scope.values[:order] || reflection_scope.values[:order])
18
18
  end
19
19
 
20
20
  end
@@ -14,10 +14,7 @@ module ActiveRecord
14
14
  def associated_records_by_owner
15
15
  through_records = through_records_by_owner
16
16
 
17
- ActiveRecord::Associations::Preloader.new(
18
- through_records.values.flatten,
19
- source_reflection.name, options
20
- ).run
17
+ Preloader.new(through_records.values.flatten, source_reflection.name, reflection_scope).run
21
18
 
22
19
  through_records.each do |owner, records|
23
20
  records.map! { |r| r.send(source_reflection.name) }.flatten!
@@ -28,16 +25,13 @@ module ActiveRecord
28
25
  private
29
26
 
30
27
  def through_records_by_owner
31
- ActiveRecord::Associations::Preloader.new(
32
- owners, through_reflection.name,
33
- through_options
34
- ).run
28
+ Preloader.new(owners, through_reflection.name, through_scope).run
35
29
 
36
30
  Hash[owners.map do |owner|
37
31
  through_records = Array.wrap(owner.send(through_reflection.name))
38
32
 
39
33
  # Dont cache the association - we would only be caching a subset
40
- if (preload_options != through_options) ||
34
+ if (through_scope != through_reflection.klass.unscoped) ||
41
35
  (reflection.options[:source_type] && through_reflection.collection?)
42
36
  owner.association(through_reflection.name).reset
43
37
  end
@@ -46,20 +40,22 @@ module ActiveRecord
46
40
  end]
47
41
  end
48
42
 
49
- def through_options
50
- through_options = {}
43
+ def through_scope
44
+ through_scope = through_reflection.klass.unscoped
51
45
 
52
46
  if options[:source_type]
53
- through_options[:conditions] = { reflection.foreign_type => options[:source_type] }
47
+ through_scope.where! reflection.foreign_type => options[:source_type]
54
48
  else
55
- if options[:conditions]
56
- through_options[:include] = options[:include] || options[:source]
57
- through_options[:conditions] = options[:conditions]
49
+ unless reflection_scope.where_values.empty?
50
+ through_scope.includes_values = Array(reflection_scope.values[:includes] || options[:source])
51
+ through_scope.where_values = reflection_scope.values[:where]
58
52
  end
59
- through_options[:order] = options[:order] if options.has_key?(:order)
53
+
54
+ through_scope.references! reflection_scope.values[:references]
55
+ through_scope.order! reflection_scope.values[:order] if through_scope.eager_loading?
60
56
  end
61
57
 
62
- through_options
58
+ through_scope
63
59
  end
64
60
  end
65
61
  end
@@ -12,21 +12,21 @@ module ActiveRecord
12
12
  target
13
13
  end
14
14
 
15
- # Implements the writer method, e.g. foo.items= for Foo.has_many :items
15
+ # Implements the writer method, e.g. foo.bar= for Foo.belongs_to :bar
16
16
  def writer(record)
17
17
  replace(record)
18
18
  end
19
19
 
20
- def create(attributes = {}, options = {}, &block)
21
- create_record(attributes, options, &block)
20
+ def create(attributes = {}, &block)
21
+ create_record(attributes, &block)
22
22
  end
23
23
 
24
- def create!(attributes = {}, options = {}, &block)
25
- create_record(attributes, options, true, &block)
24
+ def create!(attributes = {}, &block)
25
+ create_record(attributes, true, &block)
26
26
  end
27
27
 
28
- def build(attributes = {}, options = {})
29
- record = build_record(attributes, options)
28
+ def build(attributes = {})
29
+ record = build_record(attributes)
30
30
  yield(record) if block_given?
31
31
  set_new_record(record)
32
32
  record
@@ -35,11 +35,11 @@ module ActiveRecord
35
35
  private
36
36
 
37
37
  def create_scope
38
- scoped.scope_for_create.stringify_keys.except(klass.primary_key)
38
+ scope.scope_for_create.stringify_keys.except(klass.primary_key)
39
39
  end
40
40
 
41
41
  def find_target
42
- scoped.first.tap { |record| set_inverse_instance(record) }
42
+ scope.first.tap { |record| set_inverse_instance(record) }
43
43
  end
44
44
 
45
45
  # Implemented by subclasses
@@ -51,8 +51,8 @@ module ActiveRecord
51
51
  replace(record)
52
52
  end
53
53
 
54
- def create_record(attributes, options, raise_error = false)
55
- record = build_record(attributes, options)
54
+ def create_record(attributes, raise_error = false)
55
+ record = build_record(attributes)
56
56
  yield(record) if block_given?
57
57
  saved = record.save
58
58
  set_new_record(record)
@@ -15,7 +15,7 @@ module ActiveRecord
15
15
  scope = super
16
16
  chain[1..-1].each do |reflection|
17
17
  scope = scope.merge(
18
- reflection.klass.scoped.with_default_scope.
18
+ reflection.klass.all.with_default_scope.
19
19
  except(:select, :create_with, :includes, :preload, :joins, :eager_load)
20
20
  )
21
21
  end
@@ -28,7 +28,7 @@ module ActiveRecord
28
28
  # methods which create and delete records on the association.
29
29
  #
30
30
  # We only support indirectly modifying through associations which has a belongs_to source.
31
- # This is the "has_many :tags, :through => :taggings" situation, where the join model
31
+ # This is the "has_many :tags, through: :taggings" situation, where the join model
32
32
  # typically has a belongs_to on both side. In other words, associations which could also
33
33
  # be represented as has_and_belongs_to_many associations.
34
34
  #
@@ -1,206 +1,91 @@
1
- require 'active_support/concern'
2
1
 
3
2
  module ActiveRecord
4
3
  module AttributeAssignment
5
4
  extend ActiveSupport::Concern
6
- include ActiveModel::MassAssignmentSecurity
5
+ include ActiveModel::DeprecatedMassAssignmentSecurity
6
+ include ActiveModel::ForbiddenAttributesProtection
7
7
 
8
- module ClassMethods
9
- private
10
-
11
- # The primary key and inheritance column can never be set by mass-assignment for security reasons.
12
- def attributes_protected_by_default
13
- default = [ primary_key, inheritance_column ]
14
- default << 'id' unless primary_key.eql? 'id'
15
- default
16
- end
17
- end
18
-
19
- # Allows you to set all the attributes at once by passing in a hash with keys
20
- # matching the attribute names (which again matches the column names).
21
- #
22
- # If any attributes are protected by either +attr_protected+ or
23
- # +attr_accessible+ then only settable attributes will be assigned.
24
- #
25
- # class User < ActiveRecord::Base
26
- # attr_protected :is_admin
27
- # end
28
- #
29
- # user = User.new
30
- # user.attributes = { :username => 'Phusion', :is_admin => true }
31
- # user.username # => "Phusion"
32
- # user.is_admin? # => false
33
- def attributes=(new_attributes)
34
- return unless new_attributes.is_a?(Hash)
35
-
36
- assign_attributes(new_attributes)
37
- end
38
-
39
- # Allows you to set all the attributes for a particular mass-assignment
40
- # security role by passing in a hash of attributes with keys matching
41
- # the attribute names (which again matches the column names) and the role
42
- # name using the :as option.
8
+ # Allows you to set all the attributes by passing in a hash of attributes with
9
+ # keys matching the attribute names (which again matches the column names).
43
10
  #
44
- # To bypass mass-assignment security you can use the :without_protection => true
45
- # option.
46
- #
47
- # class User < ActiveRecord::Base
48
- # attr_accessible :name
49
- # attr_accessible :name, :is_admin, :as => :admin
50
- # end
51
- #
52
- # user = User.new
53
- # user.assign_attributes({ :name => 'Josh', :is_admin => true })
54
- # user.name # => "Josh"
55
- # user.is_admin? # => false
56
- #
57
- # user = User.new
58
- # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :as => :admin)
59
- # user.name # => "Josh"
60
- # user.is_admin? # => true
61
- #
62
- # user = User.new
63
- # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :without_protection => true)
64
- # user.name # => "Josh"
65
- # user.is_admin? # => true
66
- def assign_attributes(new_attributes, options = {})
11
+ # If the passed hash responds to <tt>permitted?</tt> method and the return value
12
+ # of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt>
13
+ # exception is raised.
14
+ def assign_attributes(new_attributes)
67
15
  return if new_attributes.blank?
68
16
 
69
- attributes = new_attributes.stringify_keys
70
- multi_parameter_attributes = []
17
+ attributes = new_attributes.stringify_keys
18
+ multi_parameter_attributes = []
71
19
  nested_parameter_attributes = []
72
- @mass_assignment_options = options
73
20
 
74
- unless options[:without_protection]
75
- attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role)
76
- end
21
+ attributes = sanitize_for_mass_assignment(attributes)
77
22
 
78
23
  attributes.each do |k, v|
79
24
  if k.include?("(")
80
25
  multi_parameter_attributes << [ k, v ]
81
- elsif respond_to?("#{k}=")
82
- if v.is_a?(Hash)
83
- nested_parameter_attributes << [ k, v ]
84
- else
85
- send("#{k}=", v)
86
- end
26
+ elsif v.is_a?(Hash)
27
+ nested_parameter_attributes << [ k, v ]
87
28
  else
88
- raise(UnknownAttributeError, "unknown attribute: #{k}")
29
+ _assign_attribute(k, v)
89
30
  end
90
31
  end
91
32
 
92
- # assign any deferred nested attributes after the base attributes have been set
93
- nested_parameter_attributes.each do |k,v|
94
- send("#{k}=", v)
95
- end
96
-
97
- @mass_assignment_options = nil
98
- assign_multiparameter_attributes(multi_parameter_attributes)
33
+ assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty?
34
+ assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty?
99
35
  end
100
36
 
101
- protected
37
+ alias attributes= assign_attributes
102
38
 
103
- def mass_assignment_options
104
- @mass_assignment_options ||= {}
105
- end
39
+ private
106
40
 
107
- def mass_assignment_role
108
- mass_assignment_options[:as] || :default
41
+ def _assign_attribute(k, v)
42
+ public_send("#{k}=", v)
43
+ rescue NoMethodError
44
+ if respond_to?("#{k}=")
45
+ raise
46
+ else
47
+ raise UnknownAttributeError, "unknown attribute: #{k}"
48
+ end
109
49
  end
110
50
 
111
- private
51
+ # Assign any deferred nested attributes after the base attributes have been set.
52
+ def assign_nested_parameter_attributes(pairs)
53
+ pairs.each { |k, v| _assign_attribute(k, v) }
54
+ end
112
55
 
113
56
  # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
114
57
  # by calling new on the column type or aggregation type (through composed_of) object with these parameters.
115
58
  # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
116
59
  # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
117
- # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum,
118
- # f for Float, s for String, and a for Array. If all the values for a given attribute are empty, the
119
- # attribute will be set to nil.
60
+ # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum and
61
+ # f for Float. If all the values for a given attribute are empty, the attribute will be set to +nil+.
120
62
  def assign_multiparameter_attributes(pairs)
121
63
  execute_callstack_for_multiparameter_attributes(
122
64
  extract_callstack_for_multiparameter_attributes(pairs)
123
65
  )
124
66
  end
125
67
 
126
- def instantiate_time_object(name, values)
127
- if self.class.send(:create_time_zone_conversion_attribute?, name, column_for_attribute(name))
128
- Time.zone.local(*values)
129
- else
130
- Time.time_with_datetime_fallback(self.class.default_timezone, *values)
131
- end
132
- end
133
-
134
68
  def execute_callstack_for_multiparameter_attributes(callstack)
135
69
  errors = []
136
70
  callstack.each do |name, values_with_empty_parameters|
137
71
  begin
138
- send(name + "=", read_value_from_parameter(name, values_with_empty_parameters))
72
+ send("#{name}=", MultiparameterAttribute.new(self, name, values_with_empty_parameters).read_value)
139
73
  rescue => ex
140
- errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name}", ex, name)
74
+ errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name)
141
75
  end
142
76
  end
143
77
  unless errors.empty?
144
- raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes"
145
- end
146
- end
147
-
148
- def read_value_from_parameter(name, values_hash_from_param)
149
- klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass
150
- if values_hash_from_param.values.all?{|v|v.nil?}
151
- nil
152
- elsif klass == Time
153
- read_time_parameter_value(name, values_hash_from_param)
154
- elsif klass == Date
155
- read_date_parameter_value(name, values_hash_from_param)
156
- else
157
- read_other_parameter_value(klass, name, values_hash_from_param)
158
- end
159
- end
160
-
161
- def read_time_parameter_value(name, values_hash_from_param)
162
- # If Date bits were not provided, error
163
- raise "Missing Parameter" if [1,2,3].any?{|position| !values_hash_from_param.has_key?(position)}
164
- max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param, 6)
165
- # If Date bits were provided but blank, then return nil
166
- return nil if (1..3).any? {|position| values_hash_from_param[position].blank?}
167
-
168
- set_values = (1..max_position).collect{|position| values_hash_from_param[position] }
169
- # If Time bits are not there, then default to 0
170
- (3..5).each {|i| set_values[i] = set_values[i].blank? ? 0 : set_values[i]}
171
- instantiate_time_object(name, set_values)
172
- end
173
-
174
- def read_date_parameter_value(name, values_hash_from_param)
175
- return nil if (1..3).any? {|position| values_hash_from_param[position].blank?}
176
- set_values = [values_hash_from_param[1], values_hash_from_param[2], values_hash_from_param[3]]
177
- begin
178
- Date.new(*set_values)
179
- rescue ArgumentError # if Date.new raises an exception on an invalid date
180
- instantiate_time_object(name, set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
181
- end
182
- end
183
-
184
- def read_other_parameter_value(klass, name, values_hash_from_param)
185
- max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param)
186
- values = (1..max_position).collect do |position|
187
- raise "Missing Parameter" if !values_hash_from_param.has_key?(position)
188
- values_hash_from_param[position]
78
+ error_descriptions = errors.map { |ex| ex.message }.join(",")
79
+ raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes [#{error_descriptions}]"
189
80
  end
190
- klass.new(*values)
191
- end
192
-
193
- def extract_max_param_for_multiparameter_attributes(values_hash_from_param, upper_cap = 100)
194
- [values_hash_from_param.keys.max,upper_cap].min
195
81
  end
196
82
 
197
83
  def extract_callstack_for_multiparameter_attributes(pairs)
198
84
  attributes = { }
199
85
 
200
- pairs.each do |pair|
201
- multiparameter_name, value = pair
86
+ pairs.each do |(multiparameter_name, value)|
202
87
  attribute_name = multiparameter_name.split("(").first
203
- attributes[attribute_name] = {} unless attributes.include?(attribute_name)
88
+ attributes[attribute_name] ||= {}
204
89
 
205
90
  parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
206
91
  attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value
@@ -217,5 +102,100 @@ module ActiveRecord
217
102
  multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
218
103
  end
219
104
 
105
+ class MultiparameterAttribute #:nodoc:
106
+ attr_reader :object, :name, :values, :column
107
+
108
+ def initialize(object, name, values)
109
+ @object = object
110
+ @name = name
111
+ @values = values
112
+ end
113
+
114
+ def read_value
115
+ return if values.values.compact.empty?
116
+
117
+ @column = object.class.reflect_on_aggregation(name.to_sym) || object.column_for_attribute(name)
118
+ klass = column.klass
119
+
120
+ if klass == Time
121
+ read_time
122
+ elsif klass == Date
123
+ read_date
124
+ else
125
+ read_other(klass)
126
+ end
127
+ end
128
+
129
+ private
130
+
131
+ def instantiate_time_object(set_values)
132
+ if object.class.send(:create_time_zone_conversion_attribute?, name, column)
133
+ Time.zone.local(*set_values)
134
+ else
135
+ Time.send(object.class.default_timezone, *set_values)
136
+ end
137
+ end
138
+
139
+ def read_time
140
+ # If column is a :time (and not :date or :timestamp) there is no need to validate if
141
+ # there are year/month/day fields
142
+ if column.type == :time
143
+ # if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil
144
+ { 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value|
145
+ values[key] ||= value
146
+ end
147
+ else
148
+ # else column is a timestamp, so if Date bits were not provided, error
149
+ validate_missing_parameters!([1,2,3])
150
+
151
+ # If Date bits were provided but blank, then return nil
152
+ return if blank_date_parameter?
153
+ end
154
+
155
+ max_position = extract_max_param(6)
156
+ set_values = values.values_at(*(1..max_position))
157
+ # If Time bits are not there, then default to 0
158
+ (3..5).each { |i| set_values[i] = set_values[i].presence || 0 }
159
+ instantiate_time_object(set_values)
160
+ end
161
+
162
+ def read_date
163
+ return if blank_date_parameter?
164
+ set_values = values.values_at(1,2,3)
165
+ begin
166
+ Date.new(*set_values)
167
+ rescue ArgumentError # if Date.new raises an exception on an invalid date
168
+ instantiate_time_object(set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
169
+ end
170
+ end
171
+
172
+ def read_other(klass)
173
+ max_position = extract_max_param
174
+ positions = (1..max_position)
175
+ validate_missing_parameters!(positions)
176
+
177
+ set_values = values.values_at(*positions)
178
+ klass.new(*set_values)
179
+ end
180
+
181
+ # Checks whether some blank date parameter exists. Note that this is different
182
+ # than the validate_missing_parameters! method, since it just checks for blank
183
+ # positions instead of missing ones, and does not raise in case one blank position
184
+ # exists. The caller is responsible to handle the case of this returning true.
185
+ def blank_date_parameter?
186
+ (1..3).any? { |position| values[position].blank? }
187
+ end
188
+
189
+ # If some position is not provided, it errors out a missing parameter exception.
190
+ def validate_missing_parameters!(positions)
191
+ if missing_parameter = positions.detect { |position| !values.key?(position) }
192
+ raise ArgumentError.new("Missing Parameter - #{name}(#{missing_parameter})")
193
+ end
194
+ end
195
+
196
+ def extract_max_param(upper_cap = 100)
197
+ [values.keys.max, upper_cap].min
198
+ end
199
+ end
220
200
  end
221
201
  end