activerecord_csi 2.3.5.p6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. data/CHANGELOG +5858 -0
  2. data/README +351 -0
  3. data/RUNNING_UNIT_TESTS +36 -0
  4. data/Rakefile +270 -0
  5. data/examples/associations.png +0 -0
  6. data/examples/performance.rb +162 -0
  7. data/install.rb +30 -0
  8. data/lib/active_record/aggregations.rb +261 -0
  9. data/lib/active_record/association_preload.rb +389 -0
  10. data/lib/active_record/associations/association_collection.rb +475 -0
  11. data/lib/active_record/associations/association_proxy.rb +278 -0
  12. data/lib/active_record/associations/belongs_to_association.rb +76 -0
  13. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +53 -0
  14. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +143 -0
  15. data/lib/active_record/associations/has_many_association.rb +122 -0
  16. data/lib/active_record/associations/has_many_through_association.rb +266 -0
  17. data/lib/active_record/associations/has_one_association.rb +133 -0
  18. data/lib/active_record/associations/has_one_through_association.rb +37 -0
  19. data/lib/active_record/associations.rb +2241 -0
  20. data/lib/active_record/attribute_methods.rb +388 -0
  21. data/lib/active_record/autosave_association.rb +364 -0
  22. data/lib/active_record/base.rb +3171 -0
  23. data/lib/active_record/batches.rb +81 -0
  24. data/lib/active_record/calculations.rb +311 -0
  25. data/lib/active_record/callbacks.rb +360 -0
  26. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +371 -0
  27. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +139 -0
  28. data/lib/active_record/connection_adapters/abstract/database_statements.rb +289 -0
  29. data/lib/active_record/connection_adapters/abstract/query_cache.rb +94 -0
  30. data/lib/active_record/connection_adapters/abstract/quoting.rb +69 -0
  31. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +722 -0
  32. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +434 -0
  33. data/lib/active_record/connection_adapters/abstract_adapter.rb +241 -0
  34. data/lib/active_record/connection_adapters/mysql_adapter.rb +630 -0
  35. data/lib/active_record/connection_adapters/postgresql_adapter.rb +1113 -0
  36. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +34 -0
  37. data/lib/active_record/connection_adapters/sqlite_adapter.rb +453 -0
  38. data/lib/active_record/dirty.rb +183 -0
  39. data/lib/active_record/dynamic_finder_match.rb +41 -0
  40. data/lib/active_record/dynamic_scope_match.rb +25 -0
  41. data/lib/active_record/fixtures.rb +996 -0
  42. data/lib/active_record/i18n_interpolation_deprecation.rb +26 -0
  43. data/lib/active_record/locale/en.yml +58 -0
  44. data/lib/active_record/locking/optimistic.rb +148 -0
  45. data/lib/active_record/locking/pessimistic.rb +55 -0
  46. data/lib/active_record/migration.rb +566 -0
  47. data/lib/active_record/named_scope.rb +192 -0
  48. data/lib/active_record/nested_attributes.rb +392 -0
  49. data/lib/active_record/observer.rb +197 -0
  50. data/lib/active_record/query_cache.rb +33 -0
  51. data/lib/active_record/reflection.rb +320 -0
  52. data/lib/active_record/schema.rb +51 -0
  53. data/lib/active_record/schema_dumper.rb +182 -0
  54. data/lib/active_record/serialization.rb +101 -0
  55. data/lib/active_record/serializers/json_serializer.rb +91 -0
  56. data/lib/active_record/serializers/xml_serializer.rb +357 -0
  57. data/lib/active_record/session_store.rb +326 -0
  58. data/lib/active_record/test_case.rb +66 -0
  59. data/lib/active_record/timestamp.rb +71 -0
  60. data/lib/active_record/transactions.rb +235 -0
  61. data/lib/active_record/validations.rb +1135 -0
  62. data/lib/active_record/version.rb +9 -0
  63. data/lib/active_record.rb +84 -0
  64. data/lib/activerecord.rb +2 -0
  65. data/test/assets/example.log +1 -0
  66. data/test/assets/flowers.jpg +0 -0
  67. data/test/cases/aaa_create_tables_test.rb +24 -0
  68. data/test/cases/active_schema_test_mysql.rb +100 -0
  69. data/test/cases/active_schema_test_postgresql.rb +24 -0
  70. data/test/cases/adapter_test.rb +145 -0
  71. data/test/cases/aggregations_test.rb +167 -0
  72. data/test/cases/ar_schema_test.rb +32 -0
  73. data/test/cases/associations/belongs_to_associations_test.rb +425 -0
  74. data/test/cases/associations/callbacks_test.rb +161 -0
  75. data/test/cases/associations/cascaded_eager_loading_test.rb +131 -0
  76. data/test/cases/associations/eager_load_includes_full_sti_class_test.rb +36 -0
  77. data/test/cases/associations/eager_load_nested_include_test.rb +130 -0
  78. data/test/cases/associations/eager_singularization_test.rb +145 -0
  79. data/test/cases/associations/eager_test.rb +834 -0
  80. data/test/cases/associations/extension_test.rb +62 -0
  81. data/test/cases/associations/habtm_join_table_test.rb +56 -0
  82. data/test/cases/associations/has_and_belongs_to_many_associations_test.rb +822 -0
  83. data/test/cases/associations/has_many_associations_test.rb +1134 -0
  84. data/test/cases/associations/has_many_through_associations_test.rb +346 -0
  85. data/test/cases/associations/has_one_associations_test.rb +330 -0
  86. data/test/cases/associations/has_one_through_associations_test.rb +209 -0
  87. data/test/cases/associations/inner_join_association_test.rb +93 -0
  88. data/test/cases/associations/join_model_test.rb +712 -0
  89. data/test/cases/associations_test.rb +262 -0
  90. data/test/cases/attribute_methods_test.rb +305 -0
  91. data/test/cases/autosave_association_test.rb +1142 -0
  92. data/test/cases/base_test.rb +2154 -0
  93. data/test/cases/batches_test.rb +61 -0
  94. data/test/cases/binary_test.rb +30 -0
  95. data/test/cases/calculations_test.rb +348 -0
  96. data/test/cases/callbacks_observers_test.rb +38 -0
  97. data/test/cases/callbacks_test.rb +438 -0
  98. data/test/cases/class_inheritable_attributes_test.rb +32 -0
  99. data/test/cases/column_alias_test.rb +17 -0
  100. data/test/cases/column_definition_test.rb +70 -0
  101. data/test/cases/connection_pool_test.rb +25 -0
  102. data/test/cases/connection_test_firebird.rb +8 -0
  103. data/test/cases/connection_test_mysql.rb +64 -0
  104. data/test/cases/copy_table_test_sqlite.rb +80 -0
  105. data/test/cases/database_statements_test.rb +12 -0
  106. data/test/cases/datatype_test_postgresql.rb +204 -0
  107. data/test/cases/date_time_test.rb +37 -0
  108. data/test/cases/default_test_firebird.rb +16 -0
  109. data/test/cases/defaults_test.rb +111 -0
  110. data/test/cases/deprecated_finder_test.rb +30 -0
  111. data/test/cases/dirty_test.rb +316 -0
  112. data/test/cases/finder_respond_to_test.rb +76 -0
  113. data/test/cases/finder_test.rb +1066 -0
  114. data/test/cases/fixtures_test.rb +656 -0
  115. data/test/cases/helper.rb +68 -0
  116. data/test/cases/i18n_test.rb +46 -0
  117. data/test/cases/inheritance_test.rb +262 -0
  118. data/test/cases/invalid_date_test.rb +24 -0
  119. data/test/cases/json_serialization_test.rb +205 -0
  120. data/test/cases/lifecycle_test.rb +193 -0
  121. data/test/cases/locking_test.rb +304 -0
  122. data/test/cases/method_scoping_test.rb +704 -0
  123. data/test/cases/migration_test.rb +1523 -0
  124. data/test/cases/migration_test_firebird.rb +124 -0
  125. data/test/cases/mixin_test.rb +96 -0
  126. data/test/cases/modules_test.rb +81 -0
  127. data/test/cases/multiple_db_test.rb +85 -0
  128. data/test/cases/named_scope_test.rb +361 -0
  129. data/test/cases/nested_attributes_test.rb +581 -0
  130. data/test/cases/pk_test.rb +119 -0
  131. data/test/cases/pooled_connections_test.rb +103 -0
  132. data/test/cases/query_cache_test.rb +123 -0
  133. data/test/cases/readonly_test.rb +107 -0
  134. data/test/cases/reflection_test.rb +194 -0
  135. data/test/cases/reload_models_test.rb +22 -0
  136. data/test/cases/repair_helper.rb +50 -0
  137. data/test/cases/reserved_word_test_mysql.rb +176 -0
  138. data/test/cases/sanitize_test.rb +25 -0
  139. data/test/cases/schema_authorization_test_postgresql.rb +75 -0
  140. data/test/cases/schema_dumper_test.rb +211 -0
  141. data/test/cases/schema_test_postgresql.rb +178 -0
  142. data/test/cases/serialization_test.rb +47 -0
  143. data/test/cases/synonym_test_oracle.rb +17 -0
  144. data/test/cases/timestamp_test.rb +75 -0
  145. data/test/cases/transactions_test.rb +522 -0
  146. data/test/cases/unconnected_test.rb +32 -0
  147. data/test/cases/validations_i18n_test.rb +955 -0
  148. data/test/cases/validations_test.rb +1640 -0
  149. data/test/cases/xml_serialization_test.rb +240 -0
  150. data/test/config.rb +5 -0
  151. data/test/connections/jdbc_jdbcderby/connection.rb +18 -0
  152. data/test/connections/jdbc_jdbch2/connection.rb +18 -0
  153. data/test/connections/jdbc_jdbchsqldb/connection.rb +18 -0
  154. data/test/connections/jdbc_jdbcmysql/connection.rb +26 -0
  155. data/test/connections/jdbc_jdbcpostgresql/connection.rb +26 -0
  156. data/test/connections/jdbc_jdbcsqlite3/connection.rb +25 -0
  157. data/test/connections/native_db2/connection.rb +25 -0
  158. data/test/connections/native_firebird/connection.rb +26 -0
  159. data/test/connections/native_frontbase/connection.rb +27 -0
  160. data/test/connections/native_mysql/connection.rb +25 -0
  161. data/test/connections/native_openbase/connection.rb +21 -0
  162. data/test/connections/native_oracle/connection.rb +27 -0
  163. data/test/connections/native_postgresql/connection.rb +25 -0
  164. data/test/connections/native_sqlite/connection.rb +25 -0
  165. data/test/connections/native_sqlite3/connection.rb +25 -0
  166. data/test/connections/native_sqlite3/in_memory_connection.rb +18 -0
  167. data/test/connections/native_sybase/connection.rb +23 -0
  168. data/test/fixtures/accounts.yml +29 -0
  169. data/test/fixtures/all/developers.yml +0 -0
  170. data/test/fixtures/all/people.csv +0 -0
  171. data/test/fixtures/all/tasks.yml +0 -0
  172. data/test/fixtures/author_addresses.yml +5 -0
  173. data/test/fixtures/author_favorites.yml +4 -0
  174. data/test/fixtures/authors.yml +9 -0
  175. data/test/fixtures/binaries.yml +132 -0
  176. data/test/fixtures/books.yml +7 -0
  177. data/test/fixtures/categories/special_categories.yml +9 -0
  178. data/test/fixtures/categories/subsubdir/arbitrary_filename.yml +4 -0
  179. data/test/fixtures/categories.yml +14 -0
  180. data/test/fixtures/categories_ordered.yml +7 -0
  181. data/test/fixtures/categories_posts.yml +23 -0
  182. data/test/fixtures/categorizations.yml +17 -0
  183. data/test/fixtures/clubs.yml +6 -0
  184. data/test/fixtures/comments.yml +59 -0
  185. data/test/fixtures/companies.yml +56 -0
  186. data/test/fixtures/computers.yml +4 -0
  187. data/test/fixtures/courses.yml +7 -0
  188. data/test/fixtures/customers.yml +26 -0
  189. data/test/fixtures/developers.yml +21 -0
  190. data/test/fixtures/developers_projects.yml +17 -0
  191. data/test/fixtures/edges.yml +6 -0
  192. data/test/fixtures/entrants.yml +14 -0
  193. data/test/fixtures/fixture_database.sqlite3 +0 -0
  194. data/test/fixtures/fixture_database_2.sqlite3 +0 -0
  195. data/test/fixtures/fk_test_has_fk.yml +3 -0
  196. data/test/fixtures/fk_test_has_pk.yml +2 -0
  197. data/test/fixtures/funny_jokes.yml +10 -0
  198. data/test/fixtures/items.yml +4 -0
  199. data/test/fixtures/jobs.yml +7 -0
  200. data/test/fixtures/legacy_things.yml +3 -0
  201. data/test/fixtures/mateys.yml +4 -0
  202. data/test/fixtures/member_types.yml +6 -0
  203. data/test/fixtures/members.yml +6 -0
  204. data/test/fixtures/memberships.yml +20 -0
  205. data/test/fixtures/minimalistics.yml +2 -0
  206. data/test/fixtures/mixed_case_monkeys.yml +6 -0
  207. data/test/fixtures/mixins.yml +29 -0
  208. data/test/fixtures/movies.yml +7 -0
  209. data/test/fixtures/naked/csv/accounts.csv +1 -0
  210. data/test/fixtures/naked/yml/accounts.yml +1 -0
  211. data/test/fixtures/naked/yml/companies.yml +1 -0
  212. data/test/fixtures/naked/yml/courses.yml +1 -0
  213. data/test/fixtures/organizations.yml +5 -0
  214. data/test/fixtures/owners.yml +7 -0
  215. data/test/fixtures/parrots.yml +27 -0
  216. data/test/fixtures/parrots_pirates.yml +7 -0
  217. data/test/fixtures/people.yml +15 -0
  218. data/test/fixtures/pets.yml +14 -0
  219. data/test/fixtures/pirates.yml +9 -0
  220. data/test/fixtures/posts.yml +52 -0
  221. data/test/fixtures/price_estimates.yml +7 -0
  222. data/test/fixtures/projects.yml +7 -0
  223. data/test/fixtures/readers.yml +9 -0
  224. data/test/fixtures/references.yml +17 -0
  225. data/test/fixtures/reserved_words/distinct.yml +5 -0
  226. data/test/fixtures/reserved_words/distincts_selects.yml +11 -0
  227. data/test/fixtures/reserved_words/group.yml +14 -0
  228. data/test/fixtures/reserved_words/select.yml +8 -0
  229. data/test/fixtures/reserved_words/values.yml +7 -0
  230. data/test/fixtures/ships.yml +5 -0
  231. data/test/fixtures/sponsors.yml +9 -0
  232. data/test/fixtures/subscribers.yml +7 -0
  233. data/test/fixtures/subscriptions.yml +12 -0
  234. data/test/fixtures/taggings.yml +28 -0
  235. data/test/fixtures/tags.yml +7 -0
  236. data/test/fixtures/tasks.yml +7 -0
  237. data/test/fixtures/topics.yml +42 -0
  238. data/test/fixtures/toys.yml +4 -0
  239. data/test/fixtures/treasures.yml +10 -0
  240. data/test/fixtures/vertices.yml +4 -0
  241. data/test/fixtures/warehouse-things.yml +3 -0
  242. data/test/migrations/broken/100_migration_that_raises_exception.rb +10 -0
  243. data/test/migrations/decimal/1_give_me_big_numbers.rb +15 -0
  244. data/test/migrations/duplicate/1_people_have_last_names.rb +9 -0
  245. data/test/migrations/duplicate/2_we_need_reminders.rb +12 -0
  246. data/test/migrations/duplicate/3_foo.rb +7 -0
  247. data/test/migrations/duplicate/3_innocent_jointable.rb +12 -0
  248. data/test/migrations/duplicate_names/20080507052938_chunky.rb +7 -0
  249. data/test/migrations/duplicate_names/20080507053028_chunky.rb +7 -0
  250. data/test/migrations/interleaved/pass_1/3_innocent_jointable.rb +12 -0
  251. data/test/migrations/interleaved/pass_2/1_people_have_last_names.rb +9 -0
  252. data/test/migrations/interleaved/pass_2/3_innocent_jointable.rb +12 -0
  253. data/test/migrations/interleaved/pass_3/1_people_have_last_names.rb +9 -0
  254. data/test/migrations/interleaved/pass_3/2_i_raise_on_down.rb +8 -0
  255. data/test/migrations/interleaved/pass_3/3_innocent_jointable.rb +12 -0
  256. data/test/migrations/missing/1000_people_have_middle_names.rb +9 -0
  257. data/test/migrations/missing/1_people_have_last_names.rb +9 -0
  258. data/test/migrations/missing/3_we_need_reminders.rb +12 -0
  259. data/test/migrations/missing/4_innocent_jointable.rb +12 -0
  260. data/test/migrations/valid/1_people_have_last_names.rb +9 -0
  261. data/test/migrations/valid/2_we_need_reminders.rb +12 -0
  262. data/test/migrations/valid/3_innocent_jointable.rb +12 -0
  263. data/test/models/author.rb +146 -0
  264. data/test/models/auto_id.rb +4 -0
  265. data/test/models/binary.rb +2 -0
  266. data/test/models/bird.rb +3 -0
  267. data/test/models/book.rb +4 -0
  268. data/test/models/categorization.rb +5 -0
  269. data/test/models/category.rb +34 -0
  270. data/test/models/citation.rb +6 -0
  271. data/test/models/club.rb +13 -0
  272. data/test/models/column_name.rb +3 -0
  273. data/test/models/comment.rb +29 -0
  274. data/test/models/company.rb +171 -0
  275. data/test/models/company_in_module.rb +61 -0
  276. data/test/models/computer.rb +3 -0
  277. data/test/models/contact.rb +16 -0
  278. data/test/models/contract.rb +5 -0
  279. data/test/models/course.rb +3 -0
  280. data/test/models/customer.rb +73 -0
  281. data/test/models/default.rb +2 -0
  282. data/test/models/developer.rb +101 -0
  283. data/test/models/edge.rb +5 -0
  284. data/test/models/entrant.rb +3 -0
  285. data/test/models/essay.rb +3 -0
  286. data/test/models/event.rb +3 -0
  287. data/test/models/guid.rb +2 -0
  288. data/test/models/item.rb +7 -0
  289. data/test/models/job.rb +5 -0
  290. data/test/models/joke.rb +3 -0
  291. data/test/models/keyboard.rb +3 -0
  292. data/test/models/legacy_thing.rb +3 -0
  293. data/test/models/matey.rb +4 -0
  294. data/test/models/member.rb +12 -0
  295. data/test/models/member_detail.rb +5 -0
  296. data/test/models/member_type.rb +3 -0
  297. data/test/models/membership.rb +9 -0
  298. data/test/models/minimalistic.rb +2 -0
  299. data/test/models/mixed_case_monkey.rb +3 -0
  300. data/test/models/movie.rb +5 -0
  301. data/test/models/order.rb +4 -0
  302. data/test/models/organization.rb +6 -0
  303. data/test/models/owner.rb +5 -0
  304. data/test/models/parrot.rb +16 -0
  305. data/test/models/person.rb +16 -0
  306. data/test/models/pet.rb +5 -0
  307. data/test/models/pirate.rb +70 -0
  308. data/test/models/post.rb +100 -0
  309. data/test/models/price_estimate.rb +3 -0
  310. data/test/models/project.rb +30 -0
  311. data/test/models/reader.rb +4 -0
  312. data/test/models/reference.rb +4 -0
  313. data/test/models/reply.rb +46 -0
  314. data/test/models/ship.rb +10 -0
  315. data/test/models/ship_part.rb +5 -0
  316. data/test/models/sponsor.rb +4 -0
  317. data/test/models/subject.rb +4 -0
  318. data/test/models/subscriber.rb +8 -0
  319. data/test/models/subscription.rb +4 -0
  320. data/test/models/tag.rb +7 -0
  321. data/test/models/tagging.rb +10 -0
  322. data/test/models/task.rb +3 -0
  323. data/test/models/topic.rb +80 -0
  324. data/test/models/toy.rb +6 -0
  325. data/test/models/treasure.rb +8 -0
  326. data/test/models/vertex.rb +9 -0
  327. data/test/models/warehouse_thing.rb +5 -0
  328. data/test/schema/mysql_specific_schema.rb +24 -0
  329. data/test/schema/postgresql_specific_schema.rb +114 -0
  330. data/test/schema/schema.rb +493 -0
  331. data/test/schema/schema2.rb +6 -0
  332. data/test/schema/sqlite_specific_schema.rb +25 -0
  333. metadata +420 -0
@@ -0,0 +1,192 @@
1
+ module ActiveRecord
2
+ module NamedScope
3
+ # All subclasses of ActiveRecord::Base have one named scope:
4
+ # * <tt>scoped</tt> - which allows for the creation of anonymous \scopes, on the fly: <tt>Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)</tt>
5
+ #
6
+ # These anonymous \scopes tend to be useful when procedurally generating complex queries, where passing
7
+ # intermediate values (scopes) around as first-class objects is convenient.
8
+ #
9
+ # You can define a scope that applies to all finders using ActiveRecord::Base.default_scope.
10
+ def self.included(base)
11
+ base.class_eval do
12
+ extend ClassMethods
13
+ named_scope :scoped, lambda { |scope| scope }
14
+ end
15
+ end
16
+
17
+ module ClassMethods
18
+ def scopes
19
+ read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {})
20
+ end
21
+
22
+ # Adds a class method for retrieving and querying objects. A scope represents a narrowing of a database query,
23
+ # such as <tt>:conditions => {:color => :red}, :select => 'shirts.*', :include => :washing_instructions</tt>.
24
+ #
25
+ # class Shirt < ActiveRecord::Base
26
+ # named_scope :red, :conditions => {:color => 'red'}
27
+ # named_scope :dry_clean_only, :joins => :washing_instructions, :conditions => ['washing_instructions.dry_clean_only = ?', true]
28
+ # end
29
+ #
30
+ # The above calls to <tt>named_scope</tt> define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red,
31
+ # in effect, represents the query <tt>Shirt.find(:all, :conditions => {:color => 'red'})</tt>.
32
+ #
33
+ # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red is not an Array; it resembles the association object
34
+ # constructed by a <tt>has_many</tt> declaration. For instance, you can invoke <tt>Shirt.red.find(:first)</tt>, <tt>Shirt.red.count</tt>,
35
+ # <tt>Shirt.red.find(:all, :conditions => {:size => 'small'})</tt>. Also, just
36
+ # as with the association objects, named \scopes act like an Array, implementing Enumerable; <tt>Shirt.red.each(&block)</tt>,
37
+ # <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if Shirt.red really was an Array.
38
+ #
39
+ # These named \scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are both red and dry clean only.
40
+ # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> returns the number of garments
41
+ # for which these criteria obtain. Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
42
+ #
43
+ # All \scopes are available as class methods on the ActiveRecord::Base descendant upon which the \scopes were defined. But they are also available to
44
+ # <tt>has_many</tt> associations. If,
45
+ #
46
+ # class Person < ActiveRecord::Base
47
+ # has_many :shirts
48
+ # end
49
+ #
50
+ # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean
51
+ # only shirts.
52
+ #
53
+ # Named \scopes can also be procedural:
54
+ #
55
+ # class Shirt < ActiveRecord::Base
56
+ # named_scope :colored, lambda { |color|
57
+ # { :conditions => { :color => color } }
58
+ # }
59
+ # end
60
+ #
61
+ # In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts.
62
+ #
63
+ # Named \scopes can also have extensions, just as with <tt>has_many</tt> declarations:
64
+ #
65
+ # class Shirt < ActiveRecord::Base
66
+ # named_scope :red, :conditions => {:color => 'red'} do
67
+ # def dom_id
68
+ # 'red_shirts'
69
+ # end
70
+ # end
71
+ # end
72
+ #
73
+ #
74
+ # For testing complex named \scopes, you can examine the scoping options using the
75
+ # <tt>proxy_options</tt> method on the proxy itself.
76
+ #
77
+ # class Shirt < ActiveRecord::Base
78
+ # named_scope :colored, lambda { |color|
79
+ # { :conditions => { :color => color } }
80
+ # }
81
+ # end
82
+ #
83
+ # expected_options = { :conditions => { :colored => 'red' } }
84
+ # assert_equal expected_options, Shirt.colored('red').proxy_options
85
+ def named_scope(name, options = {}, &block)
86
+ name = name.to_sym
87
+ scopes[name] = lambda do |parent_scope, *args|
88
+ Scope.new(parent_scope, case options
89
+ when Hash
90
+ options
91
+ when Proc
92
+ options.call(*args)
93
+ end, &block)
94
+ end
95
+ (class << self; self end).instance_eval do
96
+ define_method name do |*args|
97
+ scopes[name].call(self, *args)
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ class Scope
104
+ attr_reader :proxy_scope, :proxy_options, :current_scoped_methods_when_defined
105
+ NON_DELEGATE_METHODS = %w(nil? send object_id class extend find size count sum average maximum minimum paginate first last empty? any? respond_to?).to_set
106
+ [].methods.each do |m|
107
+ unless m =~ /^__/ || NON_DELEGATE_METHODS.include?(m.to_s)
108
+ delegate m, :to => :proxy_found
109
+ end
110
+ end
111
+
112
+ delegate :scopes, :with_scope, :scoped_methods, :to => :proxy_scope
113
+
114
+ def initialize(proxy_scope, options, &block)
115
+ options ||= {}
116
+ [options[:extend]].flatten.each { |extension| extend extension } if options[:extend]
117
+ extend Module.new(&block) if block_given?
118
+ unless Scope === proxy_scope
119
+ @current_scoped_methods_when_defined = proxy_scope.send(:current_scoped_methods)
120
+ end
121
+ @proxy_scope, @proxy_options = proxy_scope, options.except(:extend)
122
+ end
123
+
124
+ def reload
125
+ load_found; self
126
+ end
127
+
128
+ def first(*args)
129
+ if args.first.kind_of?(Integer) || (@found && !args.first.kind_of?(Hash))
130
+ proxy_found.first(*args)
131
+ else
132
+ find(:first, *args)
133
+ end
134
+ end
135
+
136
+ def last(*args)
137
+ if args.first.kind_of?(Integer) || (@found && !args.first.kind_of?(Hash))
138
+ proxy_found.last(*args)
139
+ else
140
+ find(:last, *args)
141
+ end
142
+ end
143
+
144
+ def size
145
+ @found ? @found.length : count
146
+ end
147
+
148
+ def empty?
149
+ @found ? @found.empty? : count.zero?
150
+ end
151
+
152
+ def respond_to?(method, include_private = false)
153
+ super || @proxy_scope.respond_to?(method, include_private)
154
+ end
155
+
156
+ def any?
157
+ if block_given?
158
+ proxy_found.any? { |*block_args| yield(*block_args) }
159
+ else
160
+ !empty?
161
+ end
162
+ end
163
+
164
+ protected
165
+ def proxy_found
166
+ @found || load_found
167
+ end
168
+
169
+ private
170
+ def method_missing(method, *args, &block)
171
+ if scopes.include?(method)
172
+ scopes[method].call(self, *args)
173
+ else
174
+ with_scope({:find => proxy_options, :create => proxy_options[:conditions].is_a?(Hash) ? proxy_options[:conditions] : {}}, :reverse_merge) do
175
+ method = :new if method == :build
176
+ if current_scoped_methods_when_defined && !scoped_methods.include?(current_scoped_methods_when_defined)
177
+ with_scope current_scoped_methods_when_defined do
178
+ proxy_scope.send(method, *args, &block)
179
+ end
180
+ else
181
+ proxy_scope.send(method, *args, &block)
182
+ end
183
+ end
184
+ end
185
+ end
186
+
187
+ def load_found
188
+ @found = find(:all)
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,392 @@
1
+ module ActiveRecord
2
+ module NestedAttributes #:nodoc:
3
+ class TooManyRecords < ActiveRecordError
4
+ end
5
+
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ base.class_inheritable_accessor :nested_attributes_options, :instance_writer => false
9
+ base.nested_attributes_options = {}
10
+ end
11
+
12
+ # == Nested Attributes
13
+ #
14
+ # Nested attributes allow you to save attributes on associated records
15
+ # through the parent. By default nested attribute updating is turned off,
16
+ # you can enable it using the accepts_nested_attributes_for class method.
17
+ # When you enable nested attributes an attribute writer is defined on
18
+ # the model.
19
+ #
20
+ # The attribute writer is named after the association, which means that
21
+ # in the following example, two new methods are added to your model:
22
+ # <tt>author_attributes=(attributes)</tt> and
23
+ # <tt>pages_attributes=(attributes)</tt>.
24
+ #
25
+ # class Book < ActiveRecord::Base
26
+ # has_one :author
27
+ # has_many :pages
28
+ #
29
+ # accepts_nested_attributes_for :author, :pages
30
+ # end
31
+ #
32
+ # Note that the <tt>:autosave</tt> option is automatically enabled on every
33
+ # association that accepts_nested_attributes_for is used for.
34
+ #
35
+ # === One-to-one
36
+ #
37
+ # Consider a Member model that has one Avatar:
38
+ #
39
+ # class Member < ActiveRecord::Base
40
+ # has_one :avatar
41
+ # accepts_nested_attributes_for :avatar
42
+ # end
43
+ #
44
+ # Enabling nested attributes on a one-to-one association allows you to
45
+ # create the member and avatar in one go:
46
+ #
47
+ # params = { :member => { :name => 'Jack', :avatar_attributes => { :icon => 'smiling' } } }
48
+ # member = Member.create(params)
49
+ # member.avatar.id # => 2
50
+ # member.avatar.icon # => 'smiling'
51
+ #
52
+ # It also allows you to update the avatar through the member:
53
+ #
54
+ # params = { :member' => { :avatar_attributes => { :id => '2', :icon => 'sad' } } }
55
+ # member.update_attributes params['member']
56
+ # member.avatar.icon # => 'sad'
57
+ #
58
+ # By default you will only be able to set and update attributes on the
59
+ # associated model. If you want to destroy the associated model through the
60
+ # attributes hash, you have to enable it first using the
61
+ # <tt>:allow_destroy</tt> option.
62
+ #
63
+ # class Member < ActiveRecord::Base
64
+ # has_one :avatar
65
+ # accepts_nested_attributes_for :avatar, :allow_destroy => true
66
+ # end
67
+ #
68
+ # Now, when you add the <tt>_destroy</tt> key to the attributes hash, with a
69
+ # value that evaluates to +true+, you will destroy the associated model:
70
+ #
71
+ # member.avatar_attributes = { :id => '2', :_destroy => '1' }
72
+ # member.avatar.marked_for_destruction? # => true
73
+ # member.save
74
+ # member.avatar #=> nil
75
+ #
76
+ # Note that the model will _not_ be destroyed until the parent is saved.
77
+ #
78
+ # === One-to-many
79
+ #
80
+ # Consider a member that has a number of posts:
81
+ #
82
+ # class Member < ActiveRecord::Base
83
+ # has_many :posts
84
+ # accepts_nested_attributes_for :posts
85
+ # end
86
+ #
87
+ # You can now set or update attributes on an associated post model through
88
+ # the attribute hash.
89
+ #
90
+ # For each hash that does _not_ have an <tt>id</tt> key a new record will
91
+ # be instantiated, unless the hash also contains a <tt>_destroy</tt> key
92
+ # that evaluates to +true+.
93
+ #
94
+ # params = { :member => {
95
+ # :name => 'joe', :posts_attributes => [
96
+ # { :title => 'Kari, the awesome Ruby documentation browser!' },
97
+ # { :title => 'The egalitarian assumption of the modern citizen' },
98
+ # { :title => '', :_destroy => '1' } # this will be ignored
99
+ # ]
100
+ # }}
101
+ #
102
+ # member = Member.create(params['member'])
103
+ # member.posts.length # => 2
104
+ # member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
105
+ # member.posts.second.title # => 'The egalitarian assumption of the modern citizen'
106
+ #
107
+ # You may also set a :reject_if proc to silently ignore any new record
108
+ # hashes if they fail to pass your criteria. For example, the previous
109
+ # example could be rewritten as:
110
+ #
111
+ # class Member < ActiveRecord::Base
112
+ # has_many :posts
113
+ # accepts_nested_attributes_for :posts, :reject_if => proc { |attributes| attributes['title'].blank? }
114
+ # end
115
+ #
116
+ # params = { :member => {
117
+ # :name => 'joe', :posts_attributes => [
118
+ # { :title => 'Kari, the awesome Ruby documentation browser!' },
119
+ # { :title => 'The egalitarian assumption of the modern citizen' },
120
+ # { :title => '' } # this will be ignored because of the :reject_if proc
121
+ # ]
122
+ # }}
123
+ #
124
+ # member = Member.create(params['member'])
125
+ # member.posts.length # => 2
126
+ # member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
127
+ # member.posts.second.title # => 'The egalitarian assumption of the modern citizen'
128
+ #
129
+ # Alternatively, :reject_if also accepts a symbol for using methods:
130
+ #
131
+ # class Member < ActiveRecord::Base
132
+ # has_many :posts
133
+ # accepts_nested_attributes_for :posts, :reject_if => :new_record?
134
+ # end
135
+ #
136
+ # class Member < ActiveRecord::Base
137
+ # has_many :posts
138
+ # accepts_nested_attributes_for :posts, :reject_if => :reject_posts
139
+ #
140
+ # def reject_posts(attributed)
141
+ # attributed['title].blank?
142
+ # end
143
+ # end
144
+ #
145
+ # If the hash contains an <tt>id</tt> key that matches an already
146
+ # associated record, the matching record will be modified:
147
+ #
148
+ # member.attributes = {
149
+ # :name => 'Joe',
150
+ # :posts_attributes => [
151
+ # { :id => 1, :title => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!' },
152
+ # { :id => 2, :title => '[UPDATED] other post' }
153
+ # ]
154
+ # }
155
+ #
156
+ # member.posts.first.title # => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!'
157
+ # member.posts.second.title # => '[UPDATED] other post'
158
+ #
159
+ # By default the associated records are protected from being destroyed. If
160
+ # you want to destroy any of the associated records through the attributes
161
+ # hash, you have to enable it first using the <tt>:allow_destroy</tt>
162
+ # option. This will allow you to also use the <tt>_destroy</tt> key to
163
+ # destroy existing records:
164
+ #
165
+ # class Member < ActiveRecord::Base
166
+ # has_many :posts
167
+ # accepts_nested_attributes_for :posts, :allow_destroy => true
168
+ # end
169
+ #
170
+ # params = { :member => {
171
+ # :posts_attributes => [{ :id => '2', :_destroy => '1' }]
172
+ # }}
173
+ #
174
+ # member.attributes = params['member']
175
+ # member.posts.detect { |p| p.id == 2 }.marked_for_destruction? # => true
176
+ # member.posts.length #=> 2
177
+ # member.save
178
+ # member.posts.length # => 1
179
+ #
180
+ # === Saving
181
+ #
182
+ # All changes to models, including the destruction of those marked for
183
+ # destruction, are saved and destroyed automatically and atomically when
184
+ # the parent model is saved. This happens inside the transaction initiated
185
+ # by the parents save method. See ActiveRecord::AutosaveAssociation.
186
+ module ClassMethods
187
+ # Defines an attributes writer for the specified association(s). If you
188
+ # are using <tt>attr_protected</tt> or <tt>attr_accessible</tt>, then you
189
+ # will need to add the attribute writer to the allowed list.
190
+ #
191
+ # Supported options:
192
+ # [:allow_destroy]
193
+ # If true, destroys any members from the attributes hash with a
194
+ # <tt>_destroy</tt> key and a value that evaluates to +true+
195
+ # (eg. 1, '1', true, or 'true'). This option is off by default.
196
+ # [:reject_if]
197
+ # Allows you to specify a Proc or a Symbol pointing to a method
198
+ # that checks whether a record should be built for a certain attribute
199
+ # hash. The hash is passed to the supplied Proc or the method
200
+ # and it should return either +true+ or +false+. When no :reject_if
201
+ # is specified, a record will be built for all attribute hashes that
202
+ # do not have a <tt>_destroy</tt> value that evaluates to true.
203
+ # Passing <tt>:all_blank</tt> instead of a Proc will create a proc
204
+ # that will reject a record where all the attributes are blank.
205
+ # [:limit]
206
+ # Allows you to specify the maximum number of the associated records that
207
+ # can be processes with the nested attributes. If the size of the
208
+ # nested attributes array exceeds the specified limit, NestedAttributes::TooManyRecords
209
+ # exception is raised. If omitted, any number associations can be processed.
210
+ # Note that the :limit option is only applicable to one-to-many associations.
211
+ #
212
+ # Examples:
213
+ # # creates avatar_attributes=
214
+ # accepts_nested_attributes_for :avatar, :reject_if => proc { |attributes| attributes['name'].blank? }
215
+ # # creates avatar_attributes= and posts_attributes=
216
+ # accepts_nested_attributes_for :avatar, :posts, :allow_destroy => true
217
+ def accepts_nested_attributes_for(*attr_names)
218
+ options = { :allow_destroy => false }
219
+ options.update(attr_names.extract_options!)
220
+ options.assert_valid_keys(:allow_destroy, :reject_if, :limit)
221
+
222
+ attr_names.each do |association_name|
223
+ if reflection = reflect_on_association(association_name)
224
+ type = case reflection.macro
225
+ when :has_one, :belongs_to
226
+ :one_to_one
227
+ when :has_many, :has_and_belongs_to_many
228
+ :collection
229
+ end
230
+
231
+ reflection.options[:autosave] = true
232
+ self.nested_attributes_options[association_name.to_sym] = options
233
+
234
+ # def pirate_attributes=(attributes)
235
+ # assign_nested_attributes_for_one_to_one_association(:pirate, attributes, false)
236
+ # end
237
+ class_eval %{
238
+ def #{association_name}_attributes=(attributes)
239
+ assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
240
+ end
241
+ }, __FILE__, __LINE__
242
+
243
+ add_autosave_association_callbacks(reflection)
244
+ else
245
+ raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
246
+ end
247
+ end
248
+ end
249
+ end
250
+
251
+ # Returns ActiveRecord::AutosaveAssociation::marked_for_destruction? It's
252
+ # used in conjunction with fields_for to build a form element for the
253
+ # destruction of this association.
254
+ #
255
+ # See ActionView::Helpers::FormHelper::fields_for for more info.
256
+ def _destroy
257
+ marked_for_destruction?
258
+ end
259
+
260
+ # Deal with deprecated _delete.
261
+ #
262
+ def _delete #:nodoc:
263
+ ActiveSupport::Deprecation.warn "_delete is deprecated in nested attributes. Use _destroy instead."
264
+ _destroy
265
+ end
266
+
267
+ private
268
+
269
+ # Attribute hash keys that should not be assigned as normal attributes.
270
+ # These hash keys are nested attributes implementation details.
271
+ #
272
+ # TODO Remove _delete from UNASSIGNABLE_KEYS when deprecation warning are
273
+ # removed.
274
+ UNASSIGNABLE_KEYS = %w( id _destroy _delete )
275
+
276
+ # Assigns the given attributes to the association.
277
+ #
278
+ # If the given attributes include an <tt>:id</tt> that matches the existing
279
+ # record’s id, then the existing record will be modified. Otherwise a new
280
+ # record will be built.
281
+ #
282
+ # If the given attributes include a matching <tt>:id</tt> attribute _and_ a
283
+ # <tt>:_destroy</tt> key set to a truthy value, then the existing record
284
+ # will be marked for destruction.
285
+ def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
286
+ options = self.nested_attributes_options[association_name]
287
+ attributes = attributes.with_indifferent_access
288
+
289
+ if attributes['id'].blank?
290
+ unless reject_new_record?(association_name, attributes)
291
+ method = "build_#{association_name}"
292
+ if respond_to?(method)
293
+ send(method, attributes.except(*UNASSIGNABLE_KEYS))
294
+ else
295
+ raise ArgumentError, "Cannot build association #{association_name}. Are you trying to build a polymorphic one-to-one association?"
296
+ end
297
+ end
298
+ elsif (existing_record = send(association_name)) && existing_record.id.to_s == attributes['id'].to_s
299
+ assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
300
+ end
301
+ end
302
+
303
+ # Assigns the given attributes to the collection association.
304
+ #
305
+ # Hashes with an <tt>:id</tt> value matching an existing associated record
306
+ # will update that record. Hashes without an <tt>:id</tt> value will build
307
+ # a new record for the association. Hashes with a matching <tt>:id</tt>
308
+ # value and a <tt>:_destroy</tt> key set to a truthy value will mark the
309
+ # matched record for destruction.
310
+ #
311
+ # For example:
312
+ #
313
+ # assign_nested_attributes_for_collection_association(:people, {
314
+ # '1' => { :id => '1', :name => 'Peter' },
315
+ # '2' => { :name => 'John' },
316
+ # '3' => { :id => '2', :_destroy => true }
317
+ # })
318
+ #
319
+ # Will update the name of the Person with ID 1, build a new associated
320
+ # person with the name `John', and mark the associatied Person with ID 2
321
+ # for destruction.
322
+ #
323
+ # Also accepts an Array of attribute hashes:
324
+ #
325
+ # assign_nested_attributes_for_collection_association(:people, [
326
+ # { :id => '1', :name => 'Peter' },
327
+ # { :name => 'John' },
328
+ # { :id => '2', :_destroy => true }
329
+ # ])
330
+ def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
331
+ options = self.nested_attributes_options[association_name]
332
+
333
+ unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
334
+ raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
335
+ end
336
+
337
+ if options[:limit] && attributes_collection.size > options[:limit]
338
+ raise TooManyRecords, "Maximum #{options[:limit]} records are allowed. Got #{attributes_collection.size} records instead."
339
+ end
340
+
341
+ if attributes_collection.is_a? Hash
342
+ attributes_collection = attributes_collection.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes }
343
+ end
344
+
345
+ attributes_collection.each do |attributes|
346
+ attributes = attributes.with_indifferent_access
347
+
348
+ if attributes['id'].blank?
349
+ unless reject_new_record?(association_name, attributes)
350
+ send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
351
+ end
352
+ elsif existing_record = send(association_name).detect { |record| record.id.to_s == attributes['id'].to_s }
353
+ assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
354
+ end
355
+ end
356
+ end
357
+
358
+ # Updates a record with the +attributes+ or marks it for destruction if
359
+ # +allow_destroy+ is +true+ and has_destroy_flag? returns +true+.
360
+ def assign_to_or_mark_for_destruction(record, attributes, allow_destroy)
361
+ if has_destroy_flag?(attributes) && allow_destroy
362
+ record.mark_for_destruction
363
+ else
364
+ record.attributes = attributes.except(*UNASSIGNABLE_KEYS)
365
+ end
366
+ end
367
+
368
+ # Determines if a hash contains a truthy _destroy key.
369
+ def has_destroy_flag?(hash)
370
+ ConnectionAdapters::Column.value_to_boolean(hash['_destroy']) ||
371
+ ConnectionAdapters::Column.value_to_boolean(hash['_delete']) # TODO Remove after deprecation.
372
+ end
373
+
374
+ # Determines if a new record should be build by checking for
375
+ # has_destroy_flag? or if a <tt>:reject_if</tt> proc exists for this
376
+ # association and evaluates to +true+.
377
+ def reject_new_record?(association_name, attributes)
378
+ has_destroy_flag?(attributes) || call_reject_if(association_name, attributes)
379
+ end
380
+
381
+ def call_reject_if(association_name, attributes)
382
+ callback = self.nested_attributes_options[association_name][:reject_if]
383
+
384
+ case callback
385
+ when Symbol
386
+ method(callback).arity == 0 ? send(callback) : send(callback, attributes)
387
+ when Proc
388
+ callback.try(:call, attributes)
389
+ end
390
+ end
391
+ end
392
+ end