sskirby-activerecord 3.2.1

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