activerecord 1.0.0 → 3.0.0
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.
- data/CHANGELOG +5518 -76
- data/README.rdoc +222 -0
- data/examples/performance.rb +162 -0
- data/examples/simple.rb +14 -0
- data/lib/active_record/aggregations.rb +192 -80
- data/lib/active_record/association_preload.rb +403 -0
- data/lib/active_record/associations/association_collection.rb +545 -53
- data/lib/active_record/associations/association_proxy.rb +295 -0
- data/lib/active_record/associations/belongs_to_association.rb +91 -0
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +78 -0
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +127 -36
- data/lib/active_record/associations/has_many_association.rb +108 -84
- data/lib/active_record/associations/has_many_through_association.rb +116 -0
- data/lib/active_record/associations/has_one_association.rb +143 -0
- data/lib/active_record/associations/has_one_through_association.rb +40 -0
- data/lib/active_record/associations/through_association_scope.rb +154 -0
- data/lib/active_record/associations.rb +2086 -368
- data/lib/active_record/attribute_methods/before_type_cast.rb +33 -0
- data/lib/active_record/attribute_methods/dirty.rb +95 -0
- data/lib/active_record/attribute_methods/primary_key.rb +50 -0
- data/lib/active_record/attribute_methods/query.rb +39 -0
- data/lib/active_record/attribute_methods/read.rb +116 -0
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +61 -0
- data/lib/active_record/attribute_methods/write.rb +37 -0
- data/lib/active_record/attribute_methods.rb +60 -0
- data/lib/active_record/autosave_association.rb +369 -0
- data/lib/active_record/base.rb +1603 -721
- data/lib/active_record/callbacks.rb +176 -225
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +365 -0
- data/lib/active_record/connection_adapters/abstract/connection_specification.rb +113 -0
- data/lib/active_record/connection_adapters/abstract/database_limits.rb +57 -0
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +329 -0
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +81 -0
- data/lib/active_record/connection_adapters/abstract/quoting.rb +72 -0
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +739 -0
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +543 -0
- data/lib/active_record/connection_adapters/abstract_adapter.rb +165 -279
- data/lib/active_record/connection_adapters/mysql_adapter.rb +594 -82
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +988 -135
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +53 -0
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +365 -71
- data/lib/active_record/counter_cache.rb +115 -0
- data/lib/active_record/dynamic_finder_match.rb +53 -0
- data/lib/active_record/dynamic_scope_match.rb +32 -0
- data/lib/active_record/errors.rb +172 -0
- data/lib/active_record/fixtures.rb +941 -105
- data/lib/active_record/locale/en.yml +40 -0
- data/lib/active_record/locking/optimistic.rb +172 -0
- data/lib/active_record/locking/pessimistic.rb +55 -0
- data/lib/active_record/log_subscriber.rb +48 -0
- data/lib/active_record/migration.rb +617 -0
- data/lib/active_record/named_scope.rb +138 -0
- data/lib/active_record/nested_attributes.rb +417 -0
- data/lib/active_record/observer.rb +105 -36
- data/lib/active_record/persistence.rb +291 -0
- data/lib/active_record/query_cache.rb +36 -0
- data/lib/active_record/railtie.rb +91 -0
- data/lib/active_record/railties/controller_runtime.rb +38 -0
- data/lib/active_record/railties/databases.rake +512 -0
- data/lib/active_record/reflection.rb +364 -87
- data/lib/active_record/relation/batches.rb +89 -0
- data/lib/active_record/relation/calculations.rb +286 -0
- data/lib/active_record/relation/finder_methods.rb +355 -0
- data/lib/active_record/relation/predicate_builder.rb +41 -0
- data/lib/active_record/relation/query_methods.rb +261 -0
- data/lib/active_record/relation/spawn_methods.rb +112 -0
- data/lib/active_record/relation.rb +393 -0
- data/lib/active_record/schema.rb +59 -0
- data/lib/active_record/schema_dumper.rb +195 -0
- data/lib/active_record/serialization.rb +60 -0
- data/lib/active_record/serializers/xml_serializer.rb +244 -0
- data/lib/active_record/session_store.rb +340 -0
- data/lib/active_record/test_case.rb +67 -0
- data/lib/active_record/timestamp.rb +88 -0
- data/lib/active_record/transactions.rb +329 -75
- data/lib/active_record/validations/associated.rb +48 -0
- data/lib/active_record/validations/uniqueness.rb +185 -0
- data/lib/active_record/validations.rb +58 -179
- data/lib/active_record/version.rb +9 -0
- data/lib/active_record.rb +100 -24
- data/lib/rails/generators/active_record/migration/migration_generator.rb +25 -0
- data/lib/rails/generators/active_record/migration/templates/migration.rb +17 -0
- data/lib/rails/generators/active_record/model/model_generator.rb +38 -0
- data/lib/rails/generators/active_record/model/templates/migration.rb +16 -0
- data/lib/rails/generators/active_record/model/templates/model.rb +5 -0
- data/lib/rails/generators/active_record/model/templates/module.rb +5 -0
- data/lib/rails/generators/active_record/observer/observer_generator.rb +15 -0
- data/lib/rails/generators/active_record/observer/templates/observer.rb +2 -0
- data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +24 -0
- data/lib/rails/generators/active_record/session_migration/templates/migration.rb +16 -0
- data/lib/rails/generators/active_record.rb +27 -0
- metadata +216 -158
- data/README +0 -361
- data/RUNNING_UNIT_TESTS +0 -36
- data/dev-utils/eval_debugger.rb +0 -9
- data/examples/associations.rb +0 -87
- data/examples/shared_setup.rb +0 -15
- data/examples/validation.rb +0 -88
- data/install.rb +0 -60
- data/lib/active_record/deprecated_associations.rb +0 -70
- data/lib/active_record/support/class_attribute_accessors.rb +0 -43
- data/lib/active_record/support/class_inheritable_attributes.rb +0 -37
- data/lib/active_record/support/clean_logger.rb +0 -10
- data/lib/active_record/support/inflector.rb +0 -70
- data/lib/active_record/vendor/mysql.rb +0 -1117
- data/lib/active_record/vendor/simple.rb +0 -702
- data/lib/active_record/wrappers/yaml_wrapper.rb +0 -15
- data/lib/active_record/wrappings.rb +0 -59
- data/rakefile +0 -122
- data/test/abstract_unit.rb +0 -16
- data/test/aggregations_test.rb +0 -34
- data/test/all.sh +0 -8
- data/test/associations_test.rb +0 -477
- data/test/base_test.rb +0 -513
- data/test/class_inheritable_attributes_test.rb +0 -33
- data/test/connections/native_mysql/connection.rb +0 -24
- data/test/connections/native_postgresql/connection.rb +0 -24
- data/test/connections/native_sqlite/connection.rb +0 -24
- data/test/deprecated_associations_test.rb +0 -336
- data/test/finder_test.rb +0 -67
- data/test/fixtures/accounts/signals37 +0 -3
- data/test/fixtures/accounts/unknown +0 -2
- data/test/fixtures/auto_id.rb +0 -4
- data/test/fixtures/column_name.rb +0 -3
- data/test/fixtures/companies/first_client +0 -6
- data/test/fixtures/companies/first_firm +0 -4
- data/test/fixtures/companies/second_client +0 -6
- data/test/fixtures/company.rb +0 -37
- data/test/fixtures/company_in_module.rb +0 -33
- data/test/fixtures/course.rb +0 -3
- data/test/fixtures/courses/java +0 -2
- data/test/fixtures/courses/ruby +0 -2
- data/test/fixtures/customer.rb +0 -30
- data/test/fixtures/customers/david +0 -6
- data/test/fixtures/db_definitions/mysql.sql +0 -96
- data/test/fixtures/db_definitions/mysql2.sql +0 -4
- data/test/fixtures/db_definitions/postgresql.sql +0 -113
- data/test/fixtures/db_definitions/postgresql2.sql +0 -4
- data/test/fixtures/db_definitions/sqlite.sql +0 -85
- data/test/fixtures/db_definitions/sqlite2.sql +0 -4
- data/test/fixtures/default.rb +0 -2
- data/test/fixtures/developer.rb +0 -8
- data/test/fixtures/developers/david +0 -2
- data/test/fixtures/developers/jamis +0 -2
- data/test/fixtures/developers_projects/david_action_controller +0 -2
- data/test/fixtures/developers_projects/david_active_record +0 -2
- data/test/fixtures/developers_projects/jamis_active_record +0 -2
- data/test/fixtures/entrant.rb +0 -3
- data/test/fixtures/entrants/first +0 -3
- data/test/fixtures/entrants/second +0 -3
- data/test/fixtures/entrants/third +0 -3
- data/test/fixtures/fixture_database.sqlite +0 -0
- data/test/fixtures/fixture_database_2.sqlite +0 -0
- data/test/fixtures/movie.rb +0 -5
- data/test/fixtures/movies/first +0 -2
- data/test/fixtures/movies/second +0 -2
- data/test/fixtures/project.rb +0 -3
- data/test/fixtures/projects/action_controller +0 -2
- data/test/fixtures/projects/active_record +0 -2
- data/test/fixtures/reply.rb +0 -21
- data/test/fixtures/subscriber.rb +0 -5
- data/test/fixtures/subscribers/first +0 -2
- data/test/fixtures/subscribers/second +0 -2
- data/test/fixtures/topic.rb +0 -20
- data/test/fixtures/topics/first +0 -9
- data/test/fixtures/topics/second +0 -8
- data/test/fixtures_test.rb +0 -20
- data/test/inflector_test.rb +0 -104
- data/test/inheritance_test.rb +0 -125
- data/test/lifecycle_test.rb +0 -110
- data/test/modules_test.rb +0 -21
- data/test/multiple_db_test.rb +0 -46
- data/test/pk_test.rb +0 -57
- data/test/reflection_test.rb +0 -78
- data/test/thread_safety_test.rb +0 -33
- data/test/transactions_test.rb +0 -83
- data/test/unconnected_test.rb +0 -24
- data/test/validations_test.rb +0 -126
@@ -0,0 +1,185 @@
|
|
1
|
+
require 'active_support/core_ext/array/wrap'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Validations
|
5
|
+
class UniquenessValidator < ActiveModel::EachValidator
|
6
|
+
def initialize(options)
|
7
|
+
super(options.reverse_merge(:case_sensitive => true))
|
8
|
+
end
|
9
|
+
|
10
|
+
# Unfortunately, we have to tie Uniqueness validators to a class.
|
11
|
+
def setup(klass)
|
12
|
+
@klass = klass
|
13
|
+
end
|
14
|
+
|
15
|
+
def validate_each(record, attribute, value)
|
16
|
+
finder_class = find_finder_class_for(record)
|
17
|
+
table = finder_class.unscoped
|
18
|
+
|
19
|
+
table_name = record.class.quoted_table_name
|
20
|
+
sql, params = mount_sql_and_params(finder_class, table_name, attribute, value)
|
21
|
+
|
22
|
+
relation = table.where(sql, *params)
|
23
|
+
|
24
|
+
Array.wrap(options[:scope]).each do |scope_item|
|
25
|
+
scope_value = record.send(scope_item)
|
26
|
+
relation = relation.where(scope_item => scope_value)
|
27
|
+
end
|
28
|
+
|
29
|
+
unless record.new_record?
|
30
|
+
# TODO : This should be in Arel
|
31
|
+
relation = relation.where("#{record.class.quoted_table_name}.#{record.class.primary_key} <> ?", record.send(:id))
|
32
|
+
end
|
33
|
+
|
34
|
+
if relation.exists?
|
35
|
+
record.errors.add(attribute, :taken, options.except(:case_sensitive, :scope).merge(:value => value))
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
protected
|
40
|
+
|
41
|
+
# The check for an existing value should be run from a class that
|
42
|
+
# isn't abstract. This means working down from the current class
|
43
|
+
# (self), to the first non-abstract class. Since classes don't know
|
44
|
+
# their subclasses, we have to build the hierarchy between self and
|
45
|
+
# the record's class.
|
46
|
+
def find_finder_class_for(record) #:nodoc:
|
47
|
+
class_hierarchy = [record.class]
|
48
|
+
|
49
|
+
while class_hierarchy.first != @klass
|
50
|
+
class_hierarchy.insert(0, class_hierarchy.first.superclass)
|
51
|
+
end
|
52
|
+
|
53
|
+
class_hierarchy.detect { |klass| !klass.abstract_class? }
|
54
|
+
end
|
55
|
+
|
56
|
+
def mount_sql_and_params(klass, table_name, attribute, value) #:nodoc:
|
57
|
+
column = klass.columns_hash[attribute.to_s]
|
58
|
+
|
59
|
+
operator = if value.nil?
|
60
|
+
"IS ?"
|
61
|
+
elsif column.text?
|
62
|
+
value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s
|
63
|
+
"#{klass.connection.case_sensitive_equality_operator} ?"
|
64
|
+
else
|
65
|
+
"= ?"
|
66
|
+
end
|
67
|
+
|
68
|
+
sql_attribute = "#{table_name}.#{klass.connection.quote_column_name(attribute)}"
|
69
|
+
|
70
|
+
if value.nil? || (options[:case_sensitive] || !column.text?)
|
71
|
+
sql = "#{sql_attribute} #{operator}"
|
72
|
+
else
|
73
|
+
sql = "LOWER(#{sql_attribute}) = LOWER(?)"
|
74
|
+
end
|
75
|
+
|
76
|
+
[sql, [value]]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
module ClassMethods
|
81
|
+
# Validates whether the value of the specified attributes are unique across the system.
|
82
|
+
# Useful for making sure that only one user
|
83
|
+
# can be named "davidhh".
|
84
|
+
#
|
85
|
+
# class Person < ActiveRecord::Base
|
86
|
+
# validates_uniqueness_of :user_name, :scope => :account_id
|
87
|
+
# end
|
88
|
+
#
|
89
|
+
# It can also validate whether the value of the specified attributes are unique based on multiple
|
90
|
+
# scope parameters. For example, making sure that a teacher can only be on the schedule once
|
91
|
+
# per semester for a particular class.
|
92
|
+
#
|
93
|
+
# class TeacherSchedule < ActiveRecord::Base
|
94
|
+
# validates_uniqueness_of :teacher_id, :scope => [:semester_id, :class_id]
|
95
|
+
# end
|
96
|
+
#
|
97
|
+
# When the record is created, a check is performed to make sure that no record exists in the database
|
98
|
+
# with the given value for the specified attribute (that maps to a column). When the record is updated,
|
99
|
+
# the same check is made but disregarding the record itself.
|
100
|
+
#
|
101
|
+
# Configuration options:
|
102
|
+
# * <tt>:message</tt> - Specifies a custom error message (default is: "has already been taken").
|
103
|
+
# * <tt>:scope</tt> - One or more columns by which to limit the scope of the uniqueness constraint.
|
104
|
+
# * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by non-text columns (+true+ by default).
|
105
|
+
# * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
|
106
|
+
# * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).
|
107
|
+
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
|
108
|
+
# occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>).
|
109
|
+
# The method, proc or string should return or evaluate to a true or false value.
|
110
|
+
# * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
|
111
|
+
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or
|
112
|
+
# <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The method, proc or string should
|
113
|
+
# return or evaluate to a true or false value.
|
114
|
+
#
|
115
|
+
# === Concurrency and integrity
|
116
|
+
#
|
117
|
+
# Using this validation method in conjunction with ActiveRecord::Base#save
|
118
|
+
# does not guarantee the absence of duplicate record insertions, because
|
119
|
+
# uniqueness checks on the application level are inherently prone to race
|
120
|
+
# conditions. For example, suppose that two users try to post a Comment at
|
121
|
+
# the same time, and a Comment's title must be unique. At the database-level,
|
122
|
+
# the actions performed by these users could be interleaved in the following manner:
|
123
|
+
#
|
124
|
+
# User 1 | User 2
|
125
|
+
# ------------------------------------+--------------------------------------
|
126
|
+
# # User 1 checks whether there's |
|
127
|
+
# # already a comment with the title |
|
128
|
+
# # 'My Post'. This is not the case. |
|
129
|
+
# SELECT * FROM comments |
|
130
|
+
# WHERE title = 'My Post' |
|
131
|
+
# |
|
132
|
+
# | # User 2 does the same thing and also
|
133
|
+
# | # infers that his title is unique.
|
134
|
+
# | SELECT * FROM comments
|
135
|
+
# | WHERE title = 'My Post'
|
136
|
+
# |
|
137
|
+
# # User 1 inserts his comment. |
|
138
|
+
# INSERT INTO comments |
|
139
|
+
# (title, content) VALUES |
|
140
|
+
# ('My Post', 'hi!') |
|
141
|
+
# |
|
142
|
+
# | # User 2 does the same thing.
|
143
|
+
# | INSERT INTO comments
|
144
|
+
# | (title, content) VALUES
|
145
|
+
# | ('My Post', 'hello!')
|
146
|
+
# |
|
147
|
+
# | # ^^^^^^
|
148
|
+
# | # Boom! We now have a duplicate
|
149
|
+
# | # title!
|
150
|
+
#
|
151
|
+
# This could even happen if you use transactions with the 'serializable'
|
152
|
+
# isolation level. There are several ways to get around this problem:
|
153
|
+
#
|
154
|
+
# - By locking the database table before validating, and unlocking it after
|
155
|
+
# saving. However, table locking is very expensive, and thus not
|
156
|
+
# recommended.
|
157
|
+
# - By locking a lock file before validating, and unlocking it after saving.
|
158
|
+
# This does not work if you've scaled your Rails application across
|
159
|
+
# multiple web servers (because they cannot share lock files, or cannot
|
160
|
+
# do that efficiently), and thus not recommended.
|
161
|
+
# - Creating a unique index on the field, by using
|
162
|
+
# ActiveRecord::ConnectionAdapters::SchemaStatements#add_index. In the
|
163
|
+
# rare case that a race condition occurs, the database will guarantee
|
164
|
+
# the field's uniqueness.
|
165
|
+
#
|
166
|
+
# When the database catches such a duplicate insertion,
|
167
|
+
# ActiveRecord::Base#save will raise an ActiveRecord::StatementInvalid
|
168
|
+
# exception. You can either choose to let this error propagate (which
|
169
|
+
# will result in the default Rails exception page being shown), or you
|
170
|
+
# can catch it and restart the transaction (e.g. by telling the user
|
171
|
+
# that the title already exists, and asking him to re-enter the title).
|
172
|
+
# This technique is also known as optimistic concurrency control:
|
173
|
+
# http://en.wikipedia.org/wiki/Optimistic_concurrency_control
|
174
|
+
#
|
175
|
+
# Active Record currently provides no way to distinguish unique
|
176
|
+
# index constraint errors from other types of database errors, so you
|
177
|
+
# will have to parse the (database-specific) exception message to detect
|
178
|
+
# such a case.
|
179
|
+
#
|
180
|
+
def validates_uniqueness_of(*attr_names)
|
181
|
+
validates_with UniquenessValidator, _merge_attributes(attr_names)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
@@ -1,205 +1,84 @@
|
|
1
1
|
module ActiveRecord
|
2
|
-
# Active
|
3
|
-
# +validate_on_update+). Each of these methods can inspect the state of the object, which usually means ensuring
|
4
|
-
# that a number of attributes have a certain value (such as not empty, within a given range, matching a certain regular expression).
|
2
|
+
# = Active Record Validations
|
5
3
|
#
|
6
|
-
#
|
4
|
+
# Raised by <tt>save!</tt> and <tt>create!</tt> when the record is invalid. Use the
|
5
|
+
# +record+ method to retrieve the record which did not validate.
|
7
6
|
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
# errors.add("phone_number", "has invalid format") unless phone_number =~ /[0-9]*/
|
13
|
-
# end
|
14
|
-
#
|
15
|
-
# def validate_on_create # is only run the first time a new object is saved
|
16
|
-
# unless valid_discount?(membership_discount)
|
17
|
-
# errors.add("membership_discount", "has expired")
|
18
|
-
# end
|
19
|
-
# end
|
20
|
-
#
|
21
|
-
# def validate_on_update
|
22
|
-
# errors.add_to_base("No changes have occured") if unchanged_attributes?
|
23
|
-
# end
|
7
|
+
# begin
|
8
|
+
# complex_operation_that_calls_save!_internally
|
9
|
+
# rescue ActiveRecord::RecordInvalid => invalid
|
10
|
+
# puts invalid.record.errors
|
24
11
|
# end
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
# "Phone number has invalid format"
|
34
|
-
#
|
35
|
-
# person.attributes = { "last_name" => "Heinemeier", "phone_number" => "555-555" }
|
36
|
-
# person.save # => true (and person is now saved in the database)
|
37
|
-
#
|
38
|
-
# An +Errors+ object is automatically created for every Active Record.
|
39
|
-
module Validations
|
40
|
-
def self.append_features(base) # :nodoc:
|
41
|
-
super
|
42
|
-
|
43
|
-
base.class_eval do
|
44
|
-
alias_method :save_without_validation, :save
|
45
|
-
alias_method :save, :save_with_validation
|
12
|
+
class RecordInvalid < ActiveRecordError
|
13
|
+
attr_reader :record
|
14
|
+
def initialize(record)
|
15
|
+
@record = record
|
16
|
+
errors = @record.errors.full_messages.join(", ")
|
17
|
+
super(I18n.t("activerecord.errors.messages.record_invalid", :errors => errors))
|
18
|
+
end
|
19
|
+
end
|
46
20
|
|
47
|
-
|
48
|
-
|
21
|
+
module Validations
|
22
|
+
extend ActiveSupport::Concern
|
23
|
+
include ActiveModel::Validations
|
24
|
+
|
25
|
+
module ClassMethods
|
26
|
+
# Creates an object just like Base.create but calls save! instead of save
|
27
|
+
# so an exception is raised if the record is invalid.
|
28
|
+
def create!(attributes = nil, &block)
|
29
|
+
if attributes.is_a?(Array)
|
30
|
+
attributes.collect { |attr| create!(attr, &block) }
|
31
|
+
else
|
32
|
+
object = new(attributes)
|
33
|
+
yield(object) if block_given?
|
34
|
+
object.save!
|
35
|
+
object
|
36
|
+
end
|
49
37
|
end
|
50
38
|
end
|
51
39
|
|
52
40
|
# The validation process on save can be skipped by passing false. The regular Base#save method is
|
53
41
|
# replaced with this when the validations module is mixed in, which it is by default.
|
54
|
-
def
|
55
|
-
|
42
|
+
def save(options={})
|
43
|
+
perform_validations(options) ? super : false
|
56
44
|
end
|
57
45
|
|
58
|
-
#
|
59
|
-
#
|
60
|
-
|
61
|
-
|
62
|
-
@attributes[name] = value
|
63
|
-
save(false)
|
64
|
-
end
|
65
|
-
|
66
|
-
# Runs validate and validate_on_create or validate_on_update and returns true if no errors were added otherwise false.
|
67
|
-
def valid?
|
68
|
-
errors.clear
|
69
|
-
validate
|
70
|
-
if new_record? then validate_on_create else validate_on_update end
|
71
|
-
errors.empty?
|
72
|
-
end
|
73
|
-
|
74
|
-
# Returns the Errors object that holds all information about attribute error messages.
|
75
|
-
def errors
|
76
|
-
@errors = Errors.new(self) if @errors.nil?
|
77
|
-
@errors
|
78
|
-
end
|
79
|
-
|
80
|
-
protected
|
81
|
-
# Overwrite this method for validation checks on all saves and use Errors.add(field, msg) for invalid attributes.
|
82
|
-
def validate #:doc:
|
83
|
-
end
|
84
|
-
|
85
|
-
# Overwrite this method for validation checks used only on creation.
|
86
|
-
def validate_on_create #:doc:
|
87
|
-
end
|
88
|
-
|
89
|
-
# Overwrite this method for validation checks used only on updates.
|
90
|
-
def validate_on_update # :doc:
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
# Active Record validation is reported to and from this object, which is used by Base#save to
|
95
|
-
# determine whether the object in a valid state to be saved. See usage example in Validations.
|
96
|
-
class Errors
|
97
|
-
def initialize(base) # :nodoc:
|
98
|
-
@base, @errors = base, {}
|
99
|
-
end
|
100
|
-
|
101
|
-
# Adds an error to the base object instead of any particular attribute. This is used
|
102
|
-
# to report errors that doesn't tie to any specific attribute, but rather to the object
|
103
|
-
# as a whole. These error messages doesn't get prepended with any field name when iterating
|
104
|
-
# with each_full, so they should be complete sentences.
|
105
|
-
def add_to_base(msg)
|
106
|
-
add(:base, msg)
|
46
|
+
# Attempts to save the record just like Base#save but will raise a RecordInvalid exception instead of returning false
|
47
|
+
# if the record is not valid.
|
48
|
+
def save!(options={})
|
49
|
+
perform_validations(options) ? super : raise(RecordInvalid.new(self))
|
107
50
|
end
|
108
51
|
|
109
|
-
#
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
def add(attribute, msg = "invalid")
|
114
|
-
@errors[attribute] = [] if @errors[attribute].nil?
|
115
|
-
@errors[attribute] << msg
|
116
|
-
end
|
52
|
+
# Runs all the specified validations and returns true if no errors were added otherwise false.
|
53
|
+
def valid?(context = nil)
|
54
|
+
context ||= (new_record? ? :create : :update)
|
55
|
+
output = super(context)
|
117
56
|
|
118
|
-
|
119
|
-
|
120
|
-
[attributes].flatten.each { |attr| add(attr, msg) unless @base.attribute_present?(attr) }
|
121
|
-
end
|
57
|
+
deprecated_callback_method(:validate)
|
58
|
+
deprecated_callback_method(:"validate_on_#{context}")
|
122
59
|
|
123
|
-
|
124
|
-
# If the length is above the boundary, the too_long_msg message will be used. If below, the too_short_msg.
|
125
|
-
def add_on_boundary_breaking(attributes, range, too_long_msg = "is too long (max is %d characters)", too_short_msg = "is too short (min is %d characters)")
|
126
|
-
for attr in [attributes].flatten
|
127
|
-
add(attr, too_short_msg % range.begin) if @base.attribute_present?(attr) && @base.send(attr).length < range.begin
|
128
|
-
add(attr, too_long_msg % range.end) if @base.attribute_present?(attr) && @base.send(attr).length > range.end
|
129
|
-
end
|
60
|
+
errors.empty? && output
|
130
61
|
end
|
131
62
|
|
132
|
-
|
63
|
+
protected
|
133
64
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
# * Returns nil, if no errors are associated with the specified +attribute+.
|
140
|
-
# * Returns the error message, if one error is associated with the specified +attribute+.
|
141
|
-
# * Returns an array of error messages, if more than one error is associated with the specified +attribute+.
|
142
|
-
def on(attribute)
|
143
|
-
if @errors[attribute].nil?
|
144
|
-
nil
|
145
|
-
elsif @errors[attribute].length == 1
|
146
|
-
@errors[attribute].first
|
65
|
+
def perform_validations(options={})
|
66
|
+
perform_validation = case options
|
67
|
+
when Hash
|
68
|
+
options[:validate] != false
|
147
69
|
else
|
148
|
-
|
70
|
+
ActiveSupport::Deprecation.warn "save(#{options}) is deprecated, please give save(:validate => #{options}) instead", caller
|
71
|
+
options
|
149
72
|
end
|
150
|
-
end
|
151
73
|
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
on(:base)
|
157
|
-
end
|
158
|
-
|
159
|
-
# Yields each attribute and associated message per error added.
|
160
|
-
def each
|
161
|
-
@errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } }
|
162
|
-
end
|
163
|
-
|
164
|
-
# Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned
|
165
|
-
# through iteration as "First name can't be empty".
|
166
|
-
def each_full
|
167
|
-
full_messages.each { |msg| yield msg }
|
168
|
-
end
|
169
|
-
|
170
|
-
# Returns all the full error messages in an array.
|
171
|
-
def full_messages
|
172
|
-
full_messages = []
|
173
|
-
|
174
|
-
@errors.each_key do |attr|
|
175
|
-
@errors[attr].each do |msg|
|
176
|
-
if attr == :base
|
177
|
-
full_messages << msg
|
178
|
-
else
|
179
|
-
full_messages << @base.class.human_attribute_name(attr) + " " + msg
|
180
|
-
end
|
181
|
-
end
|
74
|
+
if perform_validation
|
75
|
+
valid?(options.is_a?(Hash) ? options[:context] : nil)
|
76
|
+
else
|
77
|
+
true
|
182
78
|
end
|
183
|
-
|
184
|
-
return full_messages
|
185
|
-
end
|
186
|
-
|
187
|
-
# Returns true if no errors have been added.
|
188
|
-
def empty?
|
189
|
-
return @errors.empty?
|
190
|
-
end
|
191
|
-
|
192
|
-
# Removes all the errors that have been added.
|
193
|
-
def clear
|
194
|
-
@errors = {}
|
195
|
-
end
|
196
|
-
|
197
|
-
# Returns the total number of errors added. Two errors added to the same attribute will be counted as such
|
198
|
-
# with this as well.
|
199
|
-
def count
|
200
|
-
error_count = 0
|
201
|
-
@errors.each_value { |attribute| error_count += attribute.length }
|
202
|
-
error_count
|
203
79
|
end
|
204
80
|
end
|
205
81
|
end
|
82
|
+
|
83
|
+
require "active_record/validations/associated"
|
84
|
+
require "active_record/validations/uniqueness"
|
data/lib/active_record.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
#--
|
2
|
-
# Copyright (c) 2004 David Heinemeier Hansson
|
2
|
+
# Copyright (c) 2004-2010 David Heinemeier Hansson
|
3
3
|
#
|
4
4
|
# Permission is hereby granted, free of charge, to any person obtaining
|
5
5
|
# a copy of this software and associated documentation files (the
|
@@ -21,28 +21,104 @@
|
|
21
21
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
22
|
#++
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
require '
|
32
|
-
require '
|
33
|
-
require '
|
34
|
-
require '
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
24
|
+
|
25
|
+
activesupport_path = File.expand_path('../../../activesupport/lib', __FILE__)
|
26
|
+
$:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path)
|
27
|
+
|
28
|
+
activemodel_path = File.expand_path('../../../activemodel/lib', __FILE__)
|
29
|
+
$:.unshift(activemodel_path) if File.directory?(activemodel_path) && !$:.include?(activemodel_path)
|
30
|
+
|
31
|
+
require 'active_support'
|
32
|
+
require 'active_support/i18n'
|
33
|
+
require 'active_model'
|
34
|
+
require 'arel'
|
35
|
+
|
36
|
+
module ActiveRecord
|
37
|
+
extend ActiveSupport::Autoload
|
38
|
+
|
39
|
+
eager_autoload do
|
40
|
+
autoload :VERSION
|
41
|
+
|
42
|
+
autoload :ActiveRecordError, 'active_record/errors'
|
43
|
+
autoload :ConnectionNotEstablished, 'active_record/errors'
|
44
|
+
|
45
|
+
autoload :Aggregations
|
46
|
+
autoload :AssociationPreload
|
47
|
+
autoload :Associations
|
48
|
+
autoload :AttributeMethods
|
49
|
+
autoload :AutosaveAssociation
|
50
|
+
|
51
|
+
autoload :Relation
|
52
|
+
|
53
|
+
autoload_under 'relation' do
|
54
|
+
autoload :QueryMethods
|
55
|
+
autoload :FinderMethods
|
56
|
+
autoload :Calculations
|
57
|
+
autoload :PredicateBuilder
|
58
|
+
autoload :SpawnMethods
|
59
|
+
autoload :Batches
|
60
|
+
end
|
61
|
+
|
62
|
+
autoload :Base
|
63
|
+
autoload :Callbacks
|
64
|
+
autoload :CounterCache
|
65
|
+
autoload :DynamicFinderMatch
|
66
|
+
autoload :DynamicScopeMatch
|
67
|
+
autoload :Migration
|
68
|
+
autoload :Migrator, 'active_record/migration'
|
69
|
+
autoload :NamedScope
|
70
|
+
autoload :NestedAttributes
|
71
|
+
autoload :Observer
|
72
|
+
autoload :Persistence
|
73
|
+
autoload :QueryCache
|
74
|
+
autoload :Reflection
|
75
|
+
autoload :Schema
|
76
|
+
autoload :SchemaDumper
|
77
|
+
autoload :Serialization
|
78
|
+
autoload :SessionStore
|
79
|
+
autoload :Timestamp
|
80
|
+
autoload :Transactions
|
81
|
+
autoload :Validations
|
82
|
+
end
|
83
|
+
|
84
|
+
module AttributeMethods
|
85
|
+
extend ActiveSupport::Autoload
|
86
|
+
|
87
|
+
eager_autoload do
|
88
|
+
autoload :BeforeTypeCast
|
89
|
+
autoload :Dirty
|
90
|
+
autoload :PrimaryKey
|
91
|
+
autoload :Query
|
92
|
+
autoload :Read
|
93
|
+
autoload :TimeZoneConversion
|
94
|
+
autoload :Write
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
module Locking
|
99
|
+
extend ActiveSupport::Autoload
|
100
|
+
|
101
|
+
eager_autoload do
|
102
|
+
autoload :Optimistic
|
103
|
+
autoload :Pessimistic
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
module ConnectionAdapters
|
108
|
+
extend ActiveSupport::Autoload
|
109
|
+
|
110
|
+
eager_autoload do
|
111
|
+
autoload :AbstractAdapter
|
112
|
+
autoload :ConnectionManagement, "active_record/connection_adapters/abstract/connection_pool"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
autoload :TestCase
|
117
|
+
autoload :TestFixtures, 'active_record/fixtures'
|
118
|
+
end
|
119
|
+
|
120
|
+
ActiveSupport.on_load(:active_record) do
|
121
|
+
Arel::Table.engine = Arel::Sql::Engine.new(self)
|
44
122
|
end
|
45
123
|
|
46
|
-
|
47
|
-
require 'active_record/connection_adapters/postgresql_adapter'
|
48
|
-
require 'active_record/connection_adapters/sqlite_adapter'
|
124
|
+
I18n.load_path << File.dirname(__FILE__) + '/active_record/locale/en.yml'
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'rails/generators/active_record'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Generators
|
5
|
+
class MigrationGenerator < Base
|
6
|
+
argument :attributes, :type => :array, :default => [], :banner => "field:type field:type"
|
7
|
+
|
8
|
+
def create_migration_file
|
9
|
+
set_local_assigns!
|
10
|
+
migration_template "migration.rb", "db/migrate/#{file_name}.rb"
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
attr_reader :migration_action
|
15
|
+
|
16
|
+
def set_local_assigns!
|
17
|
+
if file_name =~ /^(add|remove)_.*_(?:to|from)_(.*)/
|
18
|
+
@migration_action = $1
|
19
|
+
@table_name = $2.pluralize
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
<% attributes.each do |attribute| -%>
|
4
|
+
<%- if migration_action -%>
|
5
|
+
<%= migration_action %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'add' %>, :<%= attribute.type %><% end %>
|
6
|
+
<%- end -%>
|
7
|
+
<%- end -%>
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.down
|
11
|
+
<% attributes.reverse.each do |attribute| -%>
|
12
|
+
<%- if migration_action -%>
|
13
|
+
<%= migration_action == 'add' ? 'remove' : 'add' %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'remove' %>, :<%= attribute.type %><% end %>
|
14
|
+
<%- end -%>
|
15
|
+
<%- end -%>
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'rails/generators/active_record'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Generators
|
5
|
+
class ModelGenerator < Base
|
6
|
+
argument :attributes, :type => :array, :default => [], :banner => "field:type field:type"
|
7
|
+
|
8
|
+
check_class_collision
|
9
|
+
|
10
|
+
class_option :migration, :type => :boolean
|
11
|
+
class_option :timestamps, :type => :boolean
|
12
|
+
class_option :parent, :type => :string, :desc => "The parent class for the generated model"
|
13
|
+
|
14
|
+
def create_migration_file
|
15
|
+
return unless options[:migration] && options[:parent].nil?
|
16
|
+
migration_template "migration.rb", "db/migrate/create_#{table_name}.rb"
|
17
|
+
end
|
18
|
+
|
19
|
+
def create_model_file
|
20
|
+
template 'model.rb', File.join('app/models', class_path, "#{file_name}.rb")
|
21
|
+
end
|
22
|
+
|
23
|
+
def create_module_file
|
24
|
+
return if class_path.empty?
|
25
|
+
template 'module.rb', File.join('app/models', "#{class_path.join('/')}.rb") if behavior == :invoke
|
26
|
+
end
|
27
|
+
|
28
|
+
hook_for :test_framework
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
def parent_class_name
|
33
|
+
options[:parent] || "ActiveRecord::Base"
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|