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.
- data/CHANGELOG.md +6749 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +222 -0
- data/examples/associations.png +0 -0
- data/examples/performance.rb +177 -0
- data/examples/simple.rb +14 -0
- data/lib/active_record.rb +147 -0
- data/lib/active_record/aggregations.rb +255 -0
- data/lib/active_record/associations.rb +1604 -0
- data/lib/active_record/associations/alias_tracker.rb +79 -0
- data/lib/active_record/associations/association.rb +239 -0
- data/lib/active_record/associations/association_scope.rb +119 -0
- data/lib/active_record/associations/belongs_to_association.rb +79 -0
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +34 -0
- data/lib/active_record/associations/builder/association.rb +55 -0
- data/lib/active_record/associations/builder/belongs_to.rb +85 -0
- data/lib/active_record/associations/builder/collection_association.rb +75 -0
- data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +57 -0
- data/lib/active_record/associations/builder/has_many.rb +71 -0
- data/lib/active_record/associations/builder/has_one.rb +62 -0
- data/lib/active_record/associations/builder/singular_association.rb +32 -0
- data/lib/active_record/associations/collection_association.rb +574 -0
- data/lib/active_record/associations/collection_proxy.rb +132 -0
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +62 -0
- data/lib/active_record/associations/has_many_association.rb +108 -0
- data/lib/active_record/associations/has_many_through_association.rb +180 -0
- data/lib/active_record/associations/has_one_association.rb +73 -0
- data/lib/active_record/associations/has_one_through_association.rb +36 -0
- data/lib/active_record/associations/join_dependency.rb +214 -0
- data/lib/active_record/associations/join_dependency/join_association.rb +154 -0
- data/lib/active_record/associations/join_dependency/join_base.rb +24 -0
- data/lib/active_record/associations/join_dependency/join_part.rb +78 -0
- data/lib/active_record/associations/join_helper.rb +55 -0
- data/lib/active_record/associations/preloader.rb +177 -0
- data/lib/active_record/associations/preloader/association.rb +127 -0
- data/lib/active_record/associations/preloader/belongs_to.rb +17 -0
- data/lib/active_record/associations/preloader/collection_association.rb +24 -0
- data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +60 -0
- data/lib/active_record/associations/preloader/has_many.rb +17 -0
- data/lib/active_record/associations/preloader/has_many_through.rb +15 -0
- data/lib/active_record/associations/preloader/has_one.rb +23 -0
- data/lib/active_record/associations/preloader/has_one_through.rb +9 -0
- data/lib/active_record/associations/preloader/singular_association.rb +21 -0
- data/lib/active_record/associations/preloader/through_association.rb +67 -0
- data/lib/active_record/associations/singular_association.rb +64 -0
- data/lib/active_record/associations/through_association.rb +83 -0
- data/lib/active_record/attribute_assignment.rb +221 -0
- data/lib/active_record/attribute_methods.rb +272 -0
- data/lib/active_record/attribute_methods/before_type_cast.rb +31 -0
- data/lib/active_record/attribute_methods/deprecated_underscore_read.rb +32 -0
- data/lib/active_record/attribute_methods/dirty.rb +101 -0
- data/lib/active_record/attribute_methods/primary_key.rb +114 -0
- data/lib/active_record/attribute_methods/query.rb +39 -0
- data/lib/active_record/attribute_methods/read.rb +135 -0
- data/lib/active_record/attribute_methods/serialization.rb +93 -0
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +62 -0
- data/lib/active_record/attribute_methods/write.rb +69 -0
- data/lib/active_record/autosave_association.rb +422 -0
- data/lib/active_record/base.rb +716 -0
- data/lib/active_record/callbacks.rb +275 -0
- data/lib/active_record/coders/yaml_column.rb +41 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +452 -0
- data/lib/active_record/connection_adapters/abstract/connection_specification.rb +188 -0
- data/lib/active_record/connection_adapters/abstract/database_limits.rb +58 -0
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +388 -0
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +82 -0
- data/lib/active_record/connection_adapters/abstract/quoting.rb +115 -0
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +492 -0
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +598 -0
- data/lib/active_record/connection_adapters/abstract_adapter.rb +296 -0
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +653 -0
- data/lib/active_record/connection_adapters/column.rb +270 -0
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +288 -0
- data/lib/active_record/connection_adapters/mysql_adapter.rb +426 -0
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +1261 -0
- data/lib/active_record/connection_adapters/schema_cache.rb +50 -0
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +55 -0
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +577 -0
- data/lib/active_record/connection_adapters/statement_pool.rb +40 -0
- data/lib/active_record/counter_cache.rb +119 -0
- data/lib/active_record/dynamic_finder_match.rb +56 -0
- data/lib/active_record/dynamic_matchers.rb +79 -0
- data/lib/active_record/dynamic_scope_match.rb +23 -0
- data/lib/active_record/errors.rb +195 -0
- data/lib/active_record/explain.rb +85 -0
- data/lib/active_record/explain_subscriber.rb +21 -0
- data/lib/active_record/fixtures.rb +906 -0
- data/lib/active_record/fixtures/file.rb +65 -0
- data/lib/active_record/identity_map.rb +156 -0
- data/lib/active_record/inheritance.rb +167 -0
- data/lib/active_record/integration.rb +49 -0
- data/lib/active_record/locale/en.yml +40 -0
- data/lib/active_record/locking/optimistic.rb +183 -0
- data/lib/active_record/locking/pessimistic.rb +77 -0
- data/lib/active_record/log_subscriber.rb +68 -0
- data/lib/active_record/migration.rb +765 -0
- data/lib/active_record/migration/command_recorder.rb +105 -0
- data/lib/active_record/model_schema.rb +366 -0
- data/lib/active_record/nested_attributes.rb +469 -0
- data/lib/active_record/observer.rb +121 -0
- data/lib/active_record/persistence.rb +372 -0
- data/lib/active_record/query_cache.rb +74 -0
- data/lib/active_record/querying.rb +58 -0
- data/lib/active_record/railtie.rb +119 -0
- data/lib/active_record/railties/console_sandbox.rb +6 -0
- data/lib/active_record/railties/controller_runtime.rb +49 -0
- data/lib/active_record/railties/databases.rake +620 -0
- data/lib/active_record/railties/jdbcmysql_error.rb +16 -0
- data/lib/active_record/readonly_attributes.rb +26 -0
- data/lib/active_record/reflection.rb +534 -0
- data/lib/active_record/relation.rb +534 -0
- data/lib/active_record/relation/batches.rb +90 -0
- data/lib/active_record/relation/calculations.rb +354 -0
- data/lib/active_record/relation/delegation.rb +49 -0
- data/lib/active_record/relation/finder_methods.rb +398 -0
- data/lib/active_record/relation/predicate_builder.rb +58 -0
- data/lib/active_record/relation/query_methods.rb +417 -0
- data/lib/active_record/relation/spawn_methods.rb +148 -0
- data/lib/active_record/result.rb +34 -0
- data/lib/active_record/sanitization.rb +194 -0
- data/lib/active_record/schema.rb +58 -0
- data/lib/active_record/schema_dumper.rb +204 -0
- data/lib/active_record/scoping.rb +152 -0
- data/lib/active_record/scoping/default.rb +142 -0
- data/lib/active_record/scoping/named.rb +202 -0
- data/lib/active_record/serialization.rb +18 -0
- data/lib/active_record/serializers/xml_serializer.rb +202 -0
- data/lib/active_record/session_store.rb +358 -0
- data/lib/active_record/store.rb +50 -0
- data/lib/active_record/test_case.rb +73 -0
- data/lib/active_record/timestamp.rb +113 -0
- data/lib/active_record/transactions.rb +360 -0
- data/lib/active_record/translation.rb +22 -0
- data/lib/active_record/validations.rb +83 -0
- data/lib/active_record/validations/associated.rb +43 -0
- data/lib/active_record/validations/uniqueness.rb +180 -0
- data/lib/active_record/version.rb +10 -0
- data/lib/rails/generators/active_record.rb +25 -0
- data/lib/rails/generators/active_record/migration.rb +15 -0
- data/lib/rails/generators/active_record/migration/migration_generator.rb +25 -0
- data/lib/rails/generators/active_record/migration/templates/migration.rb +31 -0
- data/lib/rails/generators/active_record/model/model_generator.rb +43 -0
- data/lib/rails/generators/active_record/model/templates/migration.rb +15 -0
- data/lib/rails/generators/active_record/model/templates/model.rb +7 -0
- data/lib/rails/generators/active_record/model/templates/module.rb +7 -0
- data/lib/rails/generators/active_record/observer/observer_generator.rb +15 -0
- data/lib/rails/generators/active_record/observer/templates/observer.rb +4 -0
- data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +25 -0
- data/lib/rails/generators/active_record/session_migration/templates/migration.rb +12 -0
- 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,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
|