sskirby-activerecord 3.2.1

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.
Files changed (150) hide show
  1. data/CHANGELOG.md +6749 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +222 -0
  4. data/examples/associations.png +0 -0
  5. data/examples/performance.rb +177 -0
  6. data/examples/simple.rb +14 -0
  7. data/lib/active_record.rb +147 -0
  8. data/lib/active_record/aggregations.rb +255 -0
  9. data/lib/active_record/associations.rb +1604 -0
  10. data/lib/active_record/associations/alias_tracker.rb +79 -0
  11. data/lib/active_record/associations/association.rb +239 -0
  12. data/lib/active_record/associations/association_scope.rb +119 -0
  13. data/lib/active_record/associations/belongs_to_association.rb +79 -0
  14. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +34 -0
  15. data/lib/active_record/associations/builder/association.rb +55 -0
  16. data/lib/active_record/associations/builder/belongs_to.rb +85 -0
  17. data/lib/active_record/associations/builder/collection_association.rb +75 -0
  18. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +57 -0
  19. data/lib/active_record/associations/builder/has_many.rb +71 -0
  20. data/lib/active_record/associations/builder/has_one.rb +62 -0
  21. data/lib/active_record/associations/builder/singular_association.rb +32 -0
  22. data/lib/active_record/associations/collection_association.rb +574 -0
  23. data/lib/active_record/associations/collection_proxy.rb +132 -0
  24. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +62 -0
  25. data/lib/active_record/associations/has_many_association.rb +108 -0
  26. data/lib/active_record/associations/has_many_through_association.rb +180 -0
  27. data/lib/active_record/associations/has_one_association.rb +73 -0
  28. data/lib/active_record/associations/has_one_through_association.rb +36 -0
  29. data/lib/active_record/associations/join_dependency.rb +214 -0
  30. data/lib/active_record/associations/join_dependency/join_association.rb +154 -0
  31. data/lib/active_record/associations/join_dependency/join_base.rb +24 -0
  32. data/lib/active_record/associations/join_dependency/join_part.rb +78 -0
  33. data/lib/active_record/associations/join_helper.rb +55 -0
  34. data/lib/active_record/associations/preloader.rb +177 -0
  35. data/lib/active_record/associations/preloader/association.rb +127 -0
  36. data/lib/active_record/associations/preloader/belongs_to.rb +17 -0
  37. data/lib/active_record/associations/preloader/collection_association.rb +24 -0
  38. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +60 -0
  39. data/lib/active_record/associations/preloader/has_many.rb +17 -0
  40. data/lib/active_record/associations/preloader/has_many_through.rb +15 -0
  41. data/lib/active_record/associations/preloader/has_one.rb +23 -0
  42. data/lib/active_record/associations/preloader/has_one_through.rb +9 -0
  43. data/lib/active_record/associations/preloader/singular_association.rb +21 -0
  44. data/lib/active_record/associations/preloader/through_association.rb +67 -0
  45. data/lib/active_record/associations/singular_association.rb +64 -0
  46. data/lib/active_record/associations/through_association.rb +83 -0
  47. data/lib/active_record/attribute_assignment.rb +221 -0
  48. data/lib/active_record/attribute_methods.rb +272 -0
  49. data/lib/active_record/attribute_methods/before_type_cast.rb +31 -0
  50. data/lib/active_record/attribute_methods/deprecated_underscore_read.rb +32 -0
  51. data/lib/active_record/attribute_methods/dirty.rb +101 -0
  52. data/lib/active_record/attribute_methods/primary_key.rb +114 -0
  53. data/lib/active_record/attribute_methods/query.rb +39 -0
  54. data/lib/active_record/attribute_methods/read.rb +135 -0
  55. data/lib/active_record/attribute_methods/serialization.rb +93 -0
  56. data/lib/active_record/attribute_methods/time_zone_conversion.rb +62 -0
  57. data/lib/active_record/attribute_methods/write.rb +69 -0
  58. data/lib/active_record/autosave_association.rb +422 -0
  59. data/lib/active_record/base.rb +716 -0
  60. data/lib/active_record/callbacks.rb +275 -0
  61. data/lib/active_record/coders/yaml_column.rb +41 -0
  62. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +452 -0
  63. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +188 -0
  64. data/lib/active_record/connection_adapters/abstract/database_limits.rb +58 -0
  65. data/lib/active_record/connection_adapters/abstract/database_statements.rb +388 -0
  66. data/lib/active_record/connection_adapters/abstract/query_cache.rb +82 -0
  67. data/lib/active_record/connection_adapters/abstract/quoting.rb +115 -0
  68. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +492 -0
  69. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +598 -0
  70. data/lib/active_record/connection_adapters/abstract_adapter.rb +296 -0
  71. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +653 -0
  72. data/lib/active_record/connection_adapters/column.rb +270 -0
  73. data/lib/active_record/connection_adapters/mysql2_adapter.rb +288 -0
  74. data/lib/active_record/connection_adapters/mysql_adapter.rb +426 -0
  75. data/lib/active_record/connection_adapters/postgresql_adapter.rb +1261 -0
  76. data/lib/active_record/connection_adapters/schema_cache.rb +50 -0
  77. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +55 -0
  78. data/lib/active_record/connection_adapters/sqlite_adapter.rb +577 -0
  79. data/lib/active_record/connection_adapters/statement_pool.rb +40 -0
  80. data/lib/active_record/counter_cache.rb +119 -0
  81. data/lib/active_record/dynamic_finder_match.rb +56 -0
  82. data/lib/active_record/dynamic_matchers.rb +79 -0
  83. data/lib/active_record/dynamic_scope_match.rb +23 -0
  84. data/lib/active_record/errors.rb +195 -0
  85. data/lib/active_record/explain.rb +85 -0
  86. data/lib/active_record/explain_subscriber.rb +21 -0
  87. data/lib/active_record/fixtures.rb +906 -0
  88. data/lib/active_record/fixtures/file.rb +65 -0
  89. data/lib/active_record/identity_map.rb +156 -0
  90. data/lib/active_record/inheritance.rb +167 -0
  91. data/lib/active_record/integration.rb +49 -0
  92. data/lib/active_record/locale/en.yml +40 -0
  93. data/lib/active_record/locking/optimistic.rb +183 -0
  94. data/lib/active_record/locking/pessimistic.rb +77 -0
  95. data/lib/active_record/log_subscriber.rb +68 -0
  96. data/lib/active_record/migration.rb +765 -0
  97. data/lib/active_record/migration/command_recorder.rb +105 -0
  98. data/lib/active_record/model_schema.rb +366 -0
  99. data/lib/active_record/nested_attributes.rb +469 -0
  100. data/lib/active_record/observer.rb +121 -0
  101. data/lib/active_record/persistence.rb +372 -0
  102. data/lib/active_record/query_cache.rb +74 -0
  103. data/lib/active_record/querying.rb +58 -0
  104. data/lib/active_record/railtie.rb +119 -0
  105. data/lib/active_record/railties/console_sandbox.rb +6 -0
  106. data/lib/active_record/railties/controller_runtime.rb +49 -0
  107. data/lib/active_record/railties/databases.rake +620 -0
  108. data/lib/active_record/railties/jdbcmysql_error.rb +16 -0
  109. data/lib/active_record/readonly_attributes.rb +26 -0
  110. data/lib/active_record/reflection.rb +534 -0
  111. data/lib/active_record/relation.rb +534 -0
  112. data/lib/active_record/relation/batches.rb +90 -0
  113. data/lib/active_record/relation/calculations.rb +354 -0
  114. data/lib/active_record/relation/delegation.rb +49 -0
  115. data/lib/active_record/relation/finder_methods.rb +398 -0
  116. data/lib/active_record/relation/predicate_builder.rb +58 -0
  117. data/lib/active_record/relation/query_methods.rb +417 -0
  118. data/lib/active_record/relation/spawn_methods.rb +148 -0
  119. data/lib/active_record/result.rb +34 -0
  120. data/lib/active_record/sanitization.rb +194 -0
  121. data/lib/active_record/schema.rb +58 -0
  122. data/lib/active_record/schema_dumper.rb +204 -0
  123. data/lib/active_record/scoping.rb +152 -0
  124. data/lib/active_record/scoping/default.rb +142 -0
  125. data/lib/active_record/scoping/named.rb +202 -0
  126. data/lib/active_record/serialization.rb +18 -0
  127. data/lib/active_record/serializers/xml_serializer.rb +202 -0
  128. data/lib/active_record/session_store.rb +358 -0
  129. data/lib/active_record/store.rb +50 -0
  130. data/lib/active_record/test_case.rb +73 -0
  131. data/lib/active_record/timestamp.rb +113 -0
  132. data/lib/active_record/transactions.rb +360 -0
  133. data/lib/active_record/translation.rb +22 -0
  134. data/lib/active_record/validations.rb +83 -0
  135. data/lib/active_record/validations/associated.rb +43 -0
  136. data/lib/active_record/validations/uniqueness.rb +180 -0
  137. data/lib/active_record/version.rb +10 -0
  138. data/lib/rails/generators/active_record.rb +25 -0
  139. data/lib/rails/generators/active_record/migration.rb +15 -0
  140. data/lib/rails/generators/active_record/migration/migration_generator.rb +25 -0
  141. data/lib/rails/generators/active_record/migration/templates/migration.rb +31 -0
  142. data/lib/rails/generators/active_record/model/model_generator.rb +43 -0
  143. data/lib/rails/generators/active_record/model/templates/migration.rb +15 -0
  144. data/lib/rails/generators/active_record/model/templates/model.rb +7 -0
  145. data/lib/rails/generators/active_record/model/templates/module.rb +7 -0
  146. data/lib/rails/generators/active_record/observer/observer_generator.rb +15 -0
  147. data/lib/rails/generators/active_record/observer/templates/observer.rb +4 -0
  148. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +25 -0
  149. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +12 -0
  150. metadata +242 -0
@@ -0,0 +1,17 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ class Preloader
4
+ class BelongsTo < SingularAssociation #:nodoc:
5
+
6
+ def association_key_name
7
+ reflection.options[:primary_key] || klass && klass.primary_key
8
+ end
9
+
10
+ def owner_key_name
11
+ reflection.foreign_key
12
+ end
13
+
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,24 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ class Preloader
4
+ class CollectionAssociation < Association #:nodoc:
5
+
6
+ private
7
+
8
+ def build_scope
9
+ super.order(preload_options[:order] || options[:order])
10
+ end
11
+
12
+ def preload
13
+ associated_records_by_owner.each do |owner, records|
14
+ association = owner.association(reflection.name)
15
+ association.loaded!
16
+ association.target.concat(records)
17
+ records.each { |record| association.set_inverse_instance(record) }
18
+ end
19
+ end
20
+
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,60 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ class Preloader
4
+ class HasAndBelongsToMany < CollectionAssociation #:nodoc:
5
+ attr_reader :join_table
6
+
7
+ def initialize(klass, records, reflection, preload_options)
8
+ super
9
+ @join_table = Arel::Table.new(options[:join_table]).alias('t0')
10
+ end
11
+
12
+ # Unlike the other associations, we want to get a raw array of rows so that we can
13
+ # access the aliased column on the join table
14
+ def records_for(ids)
15
+ scope = super
16
+ klass.connection.select_all(scope.arel, 'SQL', scope.bind_values)
17
+ end
18
+
19
+ def owner_key_name
20
+ reflection.active_record_primary_key
21
+ end
22
+
23
+ def association_key_name
24
+ 'ar_association_key_name'
25
+ end
26
+
27
+ def association_key
28
+ join_table[reflection.foreign_key]
29
+ end
30
+
31
+ private
32
+
33
+ # Once we have used the join table column (in super), we manually instantiate the
34
+ # actual records, ensuring that we don't create more than one instances of the same
35
+ # record
36
+ def associated_records_by_owner
37
+ records = {}
38
+ super.each do |owner_key, rows|
39
+ rows.map! { |row| records[row[klass.primary_key]] ||= klass.instantiate(row) }
40
+ end
41
+ end
42
+
43
+ def build_scope
44
+ super.joins(join).select(join_select)
45
+ end
46
+
47
+ def join_select
48
+ association_key.as(Arel.sql(association_key_name))
49
+ end
50
+
51
+ def join
52
+ condition = table[reflection.association_primary_key].eq(
53
+ join_table[reflection.association_foreign_key])
54
+
55
+ table.create_join(join_table, table.create_on(condition))
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,17 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ class Preloader
4
+ class HasMany < CollectionAssociation #:nodoc:
5
+
6
+ def association_key_name
7
+ reflection.foreign_key
8
+ end
9
+
10
+ def owner_key_name
11
+ reflection.active_record_primary_key
12
+ end
13
+
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ class Preloader
4
+ class HasManyThrough < CollectionAssociation #:nodoc:
5
+ include ThroughAssociation
6
+
7
+ def associated_records_by_owner
8
+ super.each do |owner, records|
9
+ records.uniq! if options[:uniq]
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,23 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ class Preloader
4
+ class HasOne < SingularAssociation #:nodoc:
5
+
6
+ def association_key_name
7
+ reflection.foreign_key
8
+ end
9
+
10
+ def owner_key_name
11
+ reflection.active_record_primary_key
12
+ end
13
+
14
+ private
15
+
16
+ def build_scope
17
+ super.order(preload_options[:order] || options[:order])
18
+ end
19
+
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,9 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ class Preloader
4
+ class HasOneThrough < SingularAssociation #:nodoc:
5
+ include ThroughAssociation
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,21 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ class Preloader
4
+ class SingularAssociation < Association #:nodoc:
5
+
6
+ private
7
+
8
+ def preload
9
+ associated_records_by_owner.each do |owner, associated_records|
10
+ record = associated_records.first
11
+
12
+ association = owner.association(reflection.name)
13
+ association.target = record
14
+ association.set_inverse_instance(record)
15
+ end
16
+ end
17
+
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,67 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ class Preloader
4
+ module ThroughAssociation #:nodoc:
5
+
6
+ def through_reflection
7
+ reflection.through_reflection
8
+ end
9
+
10
+ def source_reflection
11
+ reflection.source_reflection
12
+ end
13
+
14
+ def associated_records_by_owner
15
+ through_records = through_records_by_owner
16
+
17
+ ActiveRecord::Associations::Preloader.new(
18
+ through_records.values.flatten,
19
+ source_reflection.name, options
20
+ ).run
21
+
22
+ through_records.each do |owner, records|
23
+ records.map! { |r| r.send(source_reflection.name) }.flatten!
24
+ records.compact!
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def through_records_by_owner
31
+ ActiveRecord::Associations::Preloader.new(
32
+ owners, through_reflection.name,
33
+ through_options
34
+ ).run
35
+
36
+ Hash[owners.map do |owner|
37
+ through_records = Array.wrap(owner.send(through_reflection.name))
38
+
39
+ # Dont cache the association - we would only be caching a subset
40
+ if reflection.options[:source_type] && through_reflection.collection?
41
+ owner.association(through_reflection.name).reset
42
+ end
43
+
44
+ [owner, through_records]
45
+ end]
46
+ end
47
+
48
+ def through_options
49
+ through_options = {}
50
+
51
+ if options[:source_type]
52
+ through_options[:conditions] = { reflection.foreign_type => options[:source_type] }
53
+ else
54
+ if options[:conditions]
55
+ through_options[:include] = options[:include] || options[:source]
56
+ through_options[:conditions] = options[:conditions]
57
+ end
58
+
59
+ through_options[:order] = options[:order]
60
+ end
61
+
62
+ through_options
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,64 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ class SingularAssociation < Association #:nodoc:
4
+ # Implements the reader method, e.g. foo.bar for Foo.has_one :bar
5
+ def reader(force_reload = false)
6
+ if force_reload
7
+ klass.uncached { reload }
8
+ elsif !loaded? || stale_target?
9
+ reload
10
+ end
11
+
12
+ target
13
+ end
14
+
15
+ # Implements the writer method, e.g. foo.items= for Foo.has_many :items
16
+ def writer(record)
17
+ replace(record)
18
+ end
19
+
20
+ def create(attributes = {}, options = {}, &block)
21
+ create_record(attributes, options, &block)
22
+ end
23
+
24
+ def create!(attributes = {}, options = {}, &block)
25
+ create_record(attributes, options, true, &block)
26
+ end
27
+
28
+ def build(attributes = {}, options = {})
29
+ record = build_record(attributes, options)
30
+ yield(record) if block_given?
31
+ set_new_record(record)
32
+ record
33
+ end
34
+
35
+ private
36
+
37
+ def create_scope
38
+ scoped.scope_for_create.stringify_keys.except(klass.primary_key)
39
+ end
40
+
41
+ def find_target
42
+ scoped.first.tap { |record| set_inverse_instance(record) }
43
+ end
44
+
45
+ # Implemented by subclasses
46
+ def replace(record)
47
+ raise NotImplementedError, "Subclasses must implement a replace(record) method"
48
+ end
49
+
50
+ def set_new_record(record)
51
+ replace(record)
52
+ end
53
+
54
+ def create_record(attributes, options, raise_error = false)
55
+ record = build_record(attributes, options)
56
+ yield(record) if block_given?
57
+ saved = record.save
58
+ set_new_record(record)
59
+ raise RecordInvalid.new(record) if !saved && raise_error
60
+ record
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,83 @@
1
+ module ActiveRecord
2
+ # = Active Record Through Association
3
+ module Associations
4
+ module ThroughAssociation #:nodoc:
5
+
6
+ delegate :source_reflection, :through_reflection, :chain, :to => :reflection
7
+
8
+ protected
9
+
10
+ # We merge in these scopes for two reasons:
11
+ #
12
+ # 1. To get the default_scope conditions for any of the other reflections in the chain
13
+ # 2. To get the type conditions for any STI models in the chain
14
+ def target_scope
15
+ scope = super
16
+ chain[1..-1].each do |reflection|
17
+ scope = scope.merge(
18
+ reflection.klass.scoped.with_default_scope.
19
+ except(:select, :create_with, :includes, :preload, :joins, :eager_load)
20
+ )
21
+ end
22
+ scope
23
+ end
24
+
25
+ private
26
+
27
+ # Construct attributes for :through pointing to owner and associate. This is used by the
28
+ # methods which create and delete records on the association.
29
+ #
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
32
+ # typically has a belongs_to on both side. In other words, associations which could also
33
+ # be represented as has_and_belongs_to_many associations.
34
+ #
35
+ # We do not support creating/deleting records on the association where the source has
36
+ # some other type, because this opens up a whole can of worms, and in basically any
37
+ # situation it is more natural for the user to just create or modify their join records
38
+ # directly as required.
39
+ def construct_join_attributes(*records)
40
+ if source_reflection.macro != :belongs_to
41
+ raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
42
+ end
43
+
44
+ join_attributes = {
45
+ source_reflection.foreign_key =>
46
+ records.map { |record|
47
+ record.send(source_reflection.association_primary_key(reflection.klass))
48
+ }
49
+ }
50
+
51
+ if options[:source_type]
52
+ join_attributes[source_reflection.foreign_type] =
53
+ records.map { |record| record.class.base_class.name }
54
+ end
55
+
56
+ if records.count == 1
57
+ Hash[join_attributes.map { |k, v| [k, v.first] }]
58
+ else
59
+ join_attributes
60
+ end
61
+ end
62
+
63
+ # Note: this does not capture all cases, for example it would be crazy to try to
64
+ # properly support stale-checking for nested associations.
65
+ def stale_state
66
+ if through_reflection.macro == :belongs_to
67
+ owner[through_reflection.foreign_key].to_s
68
+ end
69
+ end
70
+
71
+ def foreign_key_present?
72
+ through_reflection.macro == :belongs_to &&
73
+ !owner[through_reflection.foreign_key].nil?
74
+ end
75
+
76
+ def ensure_not_nested
77
+ if reflection.nested?
78
+ raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection)
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,221 @@
1
+ require 'active_support/concern'
2
+
3
+ module ActiveRecord
4
+ module AttributeAssignment
5
+ extend ActiveSupport::Concern
6
+ include ActiveModel::MassAssignmentSecurity
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.
43
+ #
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 = {})
67
+ return unless new_attributes
68
+
69
+ attributes = new_attributes.stringify_keys
70
+ multi_parameter_attributes = []
71
+ nested_parameter_attributes = []
72
+ @mass_assignment_options = options
73
+
74
+ unless options[:without_protection]
75
+ attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role)
76
+ end
77
+
78
+ attributes.each do |k, v|
79
+ if k.include?("(")
80
+ 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
87
+ else
88
+ raise(UnknownAttributeError, "unknown attribute: #{k}")
89
+ end
90
+ end
91
+
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)
99
+ end
100
+
101
+ protected
102
+
103
+ def mass_assignment_options
104
+ @mass_assignment_options ||= {}
105
+ end
106
+
107
+ def mass_assignment_role
108
+ mass_assignment_options[:as] || :default
109
+ end
110
+
111
+ private
112
+
113
+ # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
114
+ # by calling new on the column type or aggregation type (through composed_of) object with these parameters.
115
+ # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
116
+ # 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.
120
+ def assign_multiparameter_attributes(pairs)
121
+ execute_callstack_for_multiparameter_attributes(
122
+ extract_callstack_for_multiparameter_attributes(pairs)
123
+ )
124
+ end
125
+
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
+ def execute_callstack_for_multiparameter_attributes(callstack)
135
+ errors = []
136
+ callstack.each do |name, values_with_empty_parameters|
137
+ begin
138
+ send(name + "=", read_value_from_parameter(name, values_with_empty_parameters))
139
+ rescue => ex
140
+ errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name}", ex, name)
141
+ end
142
+ end
143
+ 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]
189
+ 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
+ end
196
+
197
+ def extract_callstack_for_multiparameter_attributes(pairs)
198
+ attributes = { }
199
+
200
+ pairs.each do |pair|
201
+ multiparameter_name, value = pair
202
+ attribute_name = multiparameter_name.split("(").first
203
+ attributes[attribute_name] = {} unless attributes.include?(attribute_name)
204
+
205
+ parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
206
+ attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value
207
+ end
208
+
209
+ attributes
210
+ end
211
+
212
+ def type_cast_attribute_value(multiparameter_name, value)
213
+ multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value
214
+ end
215
+
216
+ def find_parameter_position(multiparameter_name)
217
+ multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
218
+ end
219
+
220
+ end
221
+ end