backlog 0.36.2 → 0.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. data/Gemfile +16 -4
  2. data/Gemfile.lock +130 -0
  3. data/History.txt +17 -0
  4. data/README.txt +0 -2
  5. data/Rakefile +17 -7
  6. data/app/controllers/absences_controller.rb +1 -2
  7. data/app/controllers/application_controller.rb +13 -14
  8. data/app/controllers/application_controller.rb.rails2 +186 -0
  9. data/app/controllers/estimates_controller.rb +1 -1
  10. data/app/controllers/groups_controller.rb +3 -1
  11. data/app/controllers/periods_controller.rb +61 -50
  12. data/app/controllers/{backlogs_controller.rb → projects_controller.rb} +35 -35
  13. data/app/controllers/search_controller.rb +2 -2
  14. data/app/controllers/tasks_controller.rb +11 -11
  15. data/app/controllers/user_controller.rb +6 -4
  16. data/app/controllers/welcome_controller.rb +4 -4
  17. data/app/controllers/work_locks_controller.rb +2 -2
  18. data/app/controllers/works_controller.rb +31 -31
  19. data/app/helpers/application_helper.rb +2 -2
  20. data/app/helpers/application_helper.rb.rails2 +118 -0
  21. data/app/helpers/periods_helper.rb +3 -3
  22. data/app/helpers/{backlogs_helper.rb → projects_helper.rb} +5 -5
  23. data/app/helpers/user_helper.rb +2 -2
  24. data/app/{models → mailers}/user_notify.rb +1 -0
  25. data/app/models/absence.rb +2 -2
  26. data/{lib → app/models}/clock.rb +0 -0
  27. data/app/models/estimate.rb +2 -2
  28. data/app/models/party.rb +1 -2
  29. data/app/models/period.rb +17 -18
  30. data/app/models/{backlog.rb → project.rb} +2 -2
  31. data/app/models/sidebar.rb +2 -2
  32. data/app/models/task.rb +28 -28
  33. data/app/models/user.rb +5 -4
  34. data/app/models/work.rb +29 -23
  35. data/app/models/work_lock_nagger.rb +1 -1
  36. data/app/models/works_report_filter.rb +4 -4
  37. data/app/views/customers/_name_list.rhtml +1 -1
  38. data/app/views/layouts/_headers.rhtml +2 -1
  39. data/app/views/layouts/_left_top.rhtml +32 -29
  40. data/app/views/layouts/_shortcuts.rhtml +15 -9
  41. data/app/views/layouts/_shortcuts_js.rhtml +4 -4
  42. data/app/views/layouts/mwrt002.html.erb +44 -0
  43. data/app/views/periods/_burn_down_chart.rhtml +1 -1
  44. data/app/views/periods/_show_active.rhtml +3 -3
  45. data/app/views/periods/_title.rhtml +1 -1
  46. data/app/views/{backlogs → projects}/_buttons.rhtml +4 -4
  47. data/app/views/projects/_form.rhtml +44 -0
  48. data/app/views/projects/_name_list.rhtml +5 -0
  49. data/app/views/projects/edit.rhtml +14 -0
  50. data/app/views/{backlogs → projects}/finish_task.rjs +0 -0
  51. data/app/views/projects/list.rhtml +16 -0
  52. data/app/views/{backlogs → projects}/move_task_to_period.rjs +0 -0
  53. data/app/views/{backlogs → projects}/new.rhtml +1 -1
  54. data/app/views/{backlogs → projects}/reopen_task.rjs +0 -0
  55. data/app/views/{backlogs → projects}/show.rhtml +6 -6
  56. data/app/views/search/results.rhtml +3 -3
  57. data/app/views/tasks/_backlog_header.rhtml +4 -4
  58. data/app/views/tasks/_completed.rhtml +2 -2
  59. data/app/views/tasks/_form.rhtml +13 -13
  60. data/app/views/tasks/_task.rhtml +10 -10
  61. data/app/views/tasks/edit.rhtml +1 -1
  62. data/app/views/tasks/list.rhtml +3 -3
  63. data/app/views/tasks/list_started.rhtml +4 -4
  64. data/app/views/tasks/start_work.rjs +1 -1
  65. data/app/views/user/login.rhtml +1 -1
  66. data/app/views/user/signup.rhtml +1 -1
  67. data/app/views/user/welcome.rhtml +1 -1
  68. data/app/views/works/_description_list.rhtml +1 -1
  69. data/app/views/works/_form.rhtml +5 -5
  70. data/app/views/works/_new_row.rhtml +8 -8
  71. data/app/views/works/_row.rhtml +1 -1
  72. data/app/views/works/_task_id_list.rhtml +1 -1
  73. data/app/views/works/daily_work_sheet.rhtml +1 -1
  74. data/app/views/works/list.rhtml +5 -5
  75. data/app/views/works/list_excel.rhtml +2 -2
  76. data/app/views/works/timeliste.rhtml +14 -14
  77. data/app/views/works/weekly_work_sheet.rhtml +5 -5
  78. data/app/views/works/weekly_work_sheet_details.rhtml +5 -5
  79. data/backlog.gemspec +44 -0
  80. data/bin/backlog +5 -1
  81. data/config.ru +4 -0
  82. data/config/application.rb +10 -0
  83. data/config/boot.rb +12 -116
  84. data/config/database.yml +3 -6
  85. data/config/database.yml~ +17 -0
  86. data/config/environment.rb +28 -49
  87. data/config/environments/development.rb +24 -20
  88. data/config/environments/development.rb.rails2 +26 -0
  89. data/config/environments/production.rb +26 -22
  90. data/config/environments/test.rb +20 -15
  91. data/config/environments/user_environment.rb +1 -1
  92. data/config/initializers/backtrace_silencers.rb +7 -0
  93. data/config/initializers/inflections.rb +10 -0
  94. data/config/initializers/jdbc.rb +1 -1
  95. data/config/initializers/mime_types.rb +5 -0
  96. data/config/initializers/secret_token.rb +7 -0
  97. data/config/initializers/session_store.rb +8 -0
  98. data/config/locales/en.yml +22 -11
  99. data/config/locales/no.yml +1 -0
  100. data/config/routes.rb +4 -5
  101. data/cruise_build.sh +6 -2
  102. data/db/migrate/004_add_period.rb +22 -22
  103. data/db/migrate/015_add_user_option.rb +5 -19
  104. data/db/migrate/017_increase_backlog_name_limit.rb +10 -0
  105. data/db/migrate/021_create_work_accounts.rb +0 -2
  106. data/db/migrate/20101006092700_rename_backlogs_to_projects.rb +22 -0
  107. data/db/migrate/20101006092700_rename_backlogs_to_projects.rb~ +22 -0
  108. data/db/schema.rb +27 -30
  109. data/db/seeds.rb +7 -0
  110. data/lib/array_helper.rb +0 -8
  111. data/lib/class_table_inheritance.rb +8 -7
  112. data/lib/tasks/backup.rake +3 -3
  113. data/lib/tasks/jdbc.rake +2 -2
  114. data/lib/version_from_history.rb +1 -1
  115. data/public/404.html +23 -7
  116. data/public/422.html +26 -0
  117. data/public/500.html +23 -6
  118. data/public/images/rails.png +0 -0
  119. data/public/javascripts/controls.js +5 -3
  120. data/public/javascripts/dragdrop.js +7 -6
  121. data/public/javascripts/effects.js +8 -13
  122. data/public/javascripts/prototype.js +3381 -1700
  123. data/public/javascripts/rails.js +175 -0
  124. data/public/robots.txt +5 -1
  125. data/script/rails +6 -0
  126. data/test/client/login.rb +0 -2
  127. data/test/client/login_test.rb +1 -1
  128. data/test/client/setup.rb +25 -24
  129. data/test/fixtures/{backlogs.yml → projects.yml} +2 -2
  130. data/test/fixtures/tasks.yml +9 -9
  131. data/test/fixtures/work_lock_subscriptions.yml +2 -2
  132. data/test/fixtures/works.yml +7 -7
  133. data/test/functional/periods_controller_test.rb +1 -1
  134. data/test/functional/{backlogs_controller_test.rb → projects_controller_test.rb} +22 -21
  135. data/test/functional/search_controller_test.rb +1 -1
  136. data/test/functional/tasks_controller_test.rb +17 -17
  137. data/test/functional/user_controller_test.rb +16 -21
  138. data/test/functional/welcome_controller_test.rb +4 -3
  139. data/test/functional/works_controller_test.rb +5 -5
  140. data/test/integration/user_system_test.rb +1 -1
  141. data/test/mocks/test/clock.rb +1 -1
  142. data/test/performance/browsing_test.rb +9 -0
  143. data/test/performance/common.rb +1 -1
  144. data/test/test_helper.rb +23 -6
  145. data/test/test_helper.rb~ +121 -0
  146. data/test/unit/user_test.rb +3 -3
  147. data/test/unit/work_test.rb +7 -7
  148. data/vendor/plugins/assert_cookie/lib/assert_cookie.rb +0 -2
  149. data/vendor/plugins/{foreign_key_migrations → dynamic_form}/MIT-LICENSE +1 -1
  150. data/vendor/plugins/dynamic_form/README +13 -0
  151. data/vendor/plugins/dynamic_form/Rakefile +10 -0
  152. data/vendor/plugins/dynamic_form/dynamic_form.gemspec +12 -0
  153. data/vendor/plugins/dynamic_form/init.rb +1 -0
  154. data/vendor/plugins/dynamic_form/lib/action_view/helpers/dynamic_form.rb +300 -0
  155. data/vendor/plugins/dynamic_form/lib/action_view/locale/en.yml +8 -0
  156. data/vendor/plugins/dynamic_form/lib/dynamic_form.rb +5 -0
  157. data/vendor/plugins/dynamic_form/test/dynamic_form_i18n_test.rb +42 -0
  158. data/vendor/plugins/dynamic_form/test/dynamic_form_test.rb +370 -0
  159. data/vendor/plugins/dynamic_form/test/test_helper.rb +9 -0
  160. data/vendor/plugins/prototype_legacy_helper/lib/prototype_legacy_helper.rb +432 -0
  161. data/vendor/plugins/prototype_legacy_helper/test/test_prototype_helper.rb +297 -0
  162. data/vendor/plugins/rails_time/test/debug.log +1 -0
  163. data/vendor/plugins/{redhillonrails_core → verification}/MIT-LICENSE +1 -1
  164. data/vendor/plugins/verification/README +34 -0
  165. data/vendor/plugins/verification/Rakefile +22 -0
  166. data/vendor/plugins/verification/init.rb +3 -0
  167. data/vendor/plugins/verification/lib/action_controller/verification.rb +132 -0
  168. data/vendor/plugins/verification/test/test_helper.rb +18 -0
  169. data/vendor/plugins/verification/test/verification_test.rb +270 -0
  170. data/vendor/plugins/will_paginate/lib/will_paginate/collection.rb +1 -1
  171. metadata +115 -134
  172. data/Gemfile~ +0 -4
  173. data/History.txt~ +0 -961
  174. data/LICENSE_LOCALIZATION +0 -20
  175. data/README_LOCALIZATION +0 -61
  176. data/README_RAILS +0 -180
  177. data/app/views/backlogs/_form.rhtml +0 -44
  178. data/app/views/backlogs/_name_list.rhtml +0 -5
  179. data/app/views/backlogs/edit.rhtml +0 -14
  180. data/app/views/backlogs/list.rhtml +0 -16
  181. data/app/views/layouts/mwrt002.rhtml +0 -43
  182. data/config/initializers/mongrel.rb +0 -83
  183. data/config/preinitializer.rb +0 -20
  184. data/config/warble.rb~ +0 -84
  185. data/db/migrate/017_insert_datek_projects.rb +0 -98
  186. data/lib/change_column_null_migration_fix.rb +0 -15
  187. data/no_test.rb~ +0 -6
  188. data/public/dispatch.cgi +0 -10
  189. data/public/dispatch.fcgi +0 -24
  190. data/public/dispatch.rb +0 -10
  191. data/script/about +0 -3
  192. data/script/breakpointer +0 -3
  193. data/script/console +0 -3
  194. data/script/dbconsole +0 -3
  195. data/script/destroy +0 -3
  196. data/script/generate +0 -3
  197. data/script/performance/benchmarker +0 -3
  198. data/script/performance/profiler +0 -3
  199. data/script/plugin +0 -3
  200. data/script/process/inspector +0 -3
  201. data/script/process/reaper +0 -3
  202. data/script/process/spawner +0 -3
  203. data/script/runner +0 -3
  204. data/script/server +0 -3
  205. data/test/client/login.rb~ +0 -33
  206. data/test/mocks/test/user_notify.rb +0 -16
  207. data/vendor/plugins/foreign_key_migrations/CHANGELOG +0 -103
  208. data/vendor/plugins/foreign_key_migrations/README +0 -87
  209. data/vendor/plugins/foreign_key_migrations/about.yml +0 -5
  210. data/vendor/plugins/foreign_key_migrations/init.rb +0 -1
  211. data/vendor/plugins/foreign_key_migrations/install.rb +0 -1
  212. data/vendor/plugins/foreign_key_migrations/lib/foreign_key_migrations.rb +0 -3
  213. data/vendor/plugins/foreign_key_migrations/lib/red_hill_consulting/foreign_key_migrations/active_record/base.rb +0 -22
  214. data/vendor/plugins/foreign_key_migrations/lib/red_hill_consulting/foreign_key_migrations/active_record/connection_adapters/abstract_adapter.rb +0 -22
  215. data/vendor/plugins/foreign_key_migrations/lib/red_hill_consulting/foreign_key_migrations/active_record/connection_adapters/table_definition.rb +0 -28
  216. data/vendor/plugins/lightwindow_helper/README +0 -33
  217. data/vendor/plugins/lightwindow_helper/assets/images/ajax-loading.gif +0 -0
  218. data/vendor/plugins/lightwindow_helper/assets/images/arrow-down.gif +0 -0
  219. data/vendor/plugins/lightwindow_helper/assets/images/arrow-up.gif +0 -0
  220. data/vendor/plugins/lightwindow_helper/assets/images/black-70.png +0 -0
  221. data/vendor/plugins/lightwindow_helper/assets/images/black.png +0 -0
  222. data/vendor/plugins/lightwindow_helper/assets/images/nextlabel.gif +0 -0
  223. data/vendor/plugins/lightwindow_helper/assets/images/prevlabel.gif +0 -0
  224. data/vendor/plugins/lightwindow_helper/assets/javascripts/lightwindow.js +0 -1921
  225. data/vendor/plugins/lightwindow_helper/assets/stylesheets/lightwindow.css +0 -376
  226. data/vendor/plugins/lightwindow_helper/init.rb +0 -1
  227. data/vendor/plugins/lightwindow_helper/install.rb +0 -7
  228. data/vendor/plugins/lightwindow_helper/lib/lightwindow_helper.rb +0 -31
  229. data/vendor/plugins/redhillonrails_core/CHANGELOG +0 -150
  230. data/vendor/plugins/redhillonrails_core/README +0 -124
  231. data/vendor/plugins/redhillonrails_core/init.rb +0 -19
  232. data/vendor/plugins/redhillonrails_core/lib/red_hill_consulting/core/active_record/base.rb +0 -54
  233. data/vendor/plugins/redhillonrails_core/lib/red_hill_consulting/core/active_record/connection_adapters/abstract_adapter.rb +0 -31
  234. data/vendor/plugins/redhillonrails_core/lib/red_hill_consulting/core/active_record/connection_adapters/column.rb +0 -21
  235. data/vendor/plugins/redhillonrails_core/lib/red_hill_consulting/core/active_record/connection_adapters/foreign_key_definition.rb +0 -26
  236. data/vendor/plugins/redhillonrails_core/lib/red_hill_consulting/core/active_record/connection_adapters/index_definition.rb +0 -11
  237. data/vendor/plugins/redhillonrails_core/lib/red_hill_consulting/core/active_record/connection_adapters/mysql_adapter.rb +0 -74
  238. data/vendor/plugins/redhillonrails_core/lib/red_hill_consulting/core/active_record/connection_adapters/mysql_column.rb +0 -8
  239. data/vendor/plugins/redhillonrails_core/lib/red_hill_consulting/core/active_record/connection_adapters/postgresql_adapter.rb +0 -99
  240. data/vendor/plugins/redhillonrails_core/lib/red_hill_consulting/core/active_record/connection_adapters/schema_statements.rb +0 -16
  241. data/vendor/plugins/redhillonrails_core/lib/red_hill_consulting/core/active_record/connection_adapters/sqlite3_adapter.rb +0 -9
  242. data/vendor/plugins/redhillonrails_core/lib/red_hill_consulting/core/active_record/connection_adapters/table_definition.rb +0 -27
  243. data/vendor/plugins/redhillonrails_core/lib/red_hill_consulting/core/active_record/schema.rb +0 -27
  244. data/vendor/plugins/redhillonrails_core/lib/red_hill_consulting/core/active_record/schema_dumper.rb +0 -47
  245. data/vendor/plugins/transactional_migrations/CHANGELOG +0 -9
  246. data/vendor/plugins/transactional_migrations/MIT-LICENSE +0 -20
  247. data/vendor/plugins/transactional_migrations/README +0 -15
  248. data/vendor/plugins/transactional_migrations/about.yml +0 -5
  249. data/vendor/plugins/transactional_migrations/init.rb +0 -1
  250. data/vendor/plugins/transactional_migrations/lib/red_hill_consulting/transactional_migrations/active_record/migration.rb +0 -19
@@ -0,0 +1,370 @@
1
+ require 'test_helper'
2
+ require 'action_view/template/handlers/erb'
3
+
4
+ class DynamicFormTest < ActionView::TestCase
5
+ tests ActionView::Helpers::DynamicForm
6
+
7
+ def form_for(*)
8
+ @output_buffer = super
9
+ end
10
+
11
+ silence_warnings do
12
+ class Post < Struct.new(:title, :author_name, :body, :secret, :written_on)
13
+ extend ActiveModel::Naming
14
+ include ActiveModel::Conversion
15
+ end
16
+
17
+ class User < Struct.new(:email)
18
+ extend ActiveModel::Naming
19
+ include ActiveModel::Conversion
20
+ end
21
+
22
+ class Column < Struct.new(:type, :name, :human_name)
23
+ extend ActiveModel::Naming
24
+ include ActiveModel::Conversion
25
+ end
26
+ end
27
+
28
+ class DirtyPost
29
+ class Errors
30
+ def empty?
31
+ false
32
+ end
33
+
34
+ def count
35
+ 1
36
+ end
37
+
38
+ def full_messages
39
+ ["Author name can't be <em>empty</em>"]
40
+ end
41
+
42
+ def [](field)
43
+ ["can't be <em>empty</em>"]
44
+ end
45
+ end
46
+
47
+ def errors
48
+ Errors.new
49
+ end
50
+ end
51
+
52
+ def setup_post
53
+ @post = Post.new
54
+ def @post.errors
55
+ Class.new {
56
+ def [](field)
57
+ case field.to_s
58
+ when "author_name"
59
+ ["can't be empty"]
60
+ when "body"
61
+ ['foo']
62
+ else
63
+ []
64
+ end
65
+ end
66
+ def empty?() false end
67
+ def count() 1 end
68
+ def full_messages() [ "Author name can't be empty" ] end
69
+ }.new
70
+ end
71
+
72
+ def @post.persisted?() false end
73
+ def @post.to_param() nil end
74
+
75
+ def @post.column_for_attribute(attr_name)
76
+ Post.content_columns.select { |column| column.name == attr_name }.first
77
+ end
78
+
79
+ silence_warnings do
80
+ def Post.content_columns() [ Column.new(:string, "title", "Title"), Column.new(:text, "body", "Body") ] end
81
+ end
82
+
83
+ @post.title = "Hello World"
84
+ @post.author_name = ""
85
+ @post.body = "Back to the hill and over it again!"
86
+ @post.secret = 1
87
+ @post.written_on = Date.new(2004, 6, 15)
88
+ end
89
+
90
+ def setup_user
91
+ @user = User.new
92
+ def @user.errors
93
+ Class.new {
94
+ def [](field) field == "email" ? ['nonempty'] : [] end
95
+ def empty?() false end
96
+ def count() 1 end
97
+ def full_messages() [ "User email can't be empty" ] end
98
+ }.new
99
+ end
100
+
101
+ def @user.new_record?() true end
102
+ def @user.to_param() nil end
103
+
104
+ def @user.column_for_attribute(attr_name)
105
+ User.content_columns.select { |column| column.name == attr_name }.first
106
+ end
107
+
108
+ silence_warnings do
109
+ def User.content_columns() [ Column.new(:string, "email", "Email") ] end
110
+ end
111
+
112
+ @user.email = ""
113
+ end
114
+
115
+ def protect_against_forgery?
116
+ @protect_against_forgery ? true : false
117
+ end
118
+ attr_accessor :request_forgery_protection_token, :form_authenticity_token
119
+
120
+ def setup
121
+ super
122
+ setup_post
123
+ setup_user
124
+
125
+ @response = ActionController::TestResponse.new
126
+ end
127
+
128
+ def url_for(options)
129
+ options = options.symbolize_keys
130
+ [options[:action], options[:id].to_param].compact.join('/')
131
+ end
132
+
133
+ def test_generic_input_tag
134
+ assert_dom_equal(
135
+ %(<input id="post_title" name="post[title]" size="30" type="text" value="Hello World" />), input("post", "title")
136
+ )
137
+ end
138
+
139
+ def test_text_area_with_errors
140
+ assert_dom_equal(
141
+ %(<div class="fieldWithErrors"><textarea cols="40" id="post_body" name="post[body]" rows="20">Back to the hill and over it again!</textarea></div>),
142
+ text_area("post", "body")
143
+ )
144
+ end
145
+
146
+ def test_text_field_with_errors
147
+ assert_dom_equal(
148
+ %(<div class="fieldWithErrors"><input id="post_author_name" name="post[author_name]" size="30" type="text" value="" /></div>),
149
+ text_field("post", "author_name")
150
+ )
151
+ end
152
+
153
+ def test_field_error_proc
154
+ old_proc = ActionView::Base.field_error_proc
155
+ ActionView::Base.field_error_proc = Proc.new do |html_tag, instance|
156
+ %(<div class=\"fieldWithErrors\">#{html_tag} <span class="error">#{[instance.error_message].join(', ')}</span></div>).html_safe
157
+ end
158
+
159
+ assert_dom_equal(
160
+ %(<div class="fieldWithErrors"><input id="post_author_name" name="post[author_name]" size="30" type="text" value="" /> <span class="error">can't be empty</span></div>),
161
+ text_field("post", "author_name")
162
+ )
163
+ ensure
164
+ ActionView::Base.field_error_proc = old_proc if old_proc
165
+ end
166
+
167
+ def test_form_with_string
168
+ assert_dom_equal(
169
+ %(<form action="create" method="post"><p><label for="post_title">Title</label><br /><input id="post_title" name="post[title]" size="30" type="text" value="Hello World" /></p>\n<p><label for="post_body">Body</label><br /><div class="fieldWithErrors"><textarea cols="40" id="post_body" name="post[body]" rows="20">Back to the hill and over it again!</textarea></div></p><input name="commit" type="submit" value="Create" /></form>),
170
+ form("post")
171
+ )
172
+
173
+ silence_warnings do
174
+ class << @post
175
+ def persisted?() true end
176
+ def to_param() id end
177
+ def id() 1 end
178
+ end
179
+ end
180
+
181
+ assert_dom_equal(
182
+ %(<form action="update/1" method="post"><input id="post_id" name="post[id]" type="hidden" value="1" /><p><label for="post_title">Title</label><br /><input id="post_title" name="post[title]" size="30" type="text" value="Hello World" /></p>\n<p><label for="post_body">Body</label><br /><div class="fieldWithErrors"><textarea cols="40" id="post_body" name="post[body]" rows="20">Back to the hill and over it again!</textarea></div></p><input name="commit" type="submit" value="Update" /></form>),
183
+ form("post")
184
+ )
185
+ end
186
+
187
+ def test_form_with_protect_against_forgery
188
+ @protect_against_forgery = true
189
+ @request_forgery_protection_token = 'authenticity_token'
190
+ @form_authenticity_token = '123'
191
+ assert_dom_equal(
192
+ %(<form action="create" method="post"><div style='margin:0;padding:0;display:inline'><input type='hidden' name='authenticity_token' value='123' /></div><p><label for="post_title">Title</label><br /><input id="post_title" name="post[title]" size="30" type="text" value="Hello World" /></p>\n<p><label for="post_body">Body</label><br /><div class="fieldWithErrors"><textarea cols="40" id="post_body" name="post[body]" rows="20">Back to the hill and over it again!</textarea></div></p><input name="commit" type="submit" value="Create" /></form>),
193
+ form("post")
194
+ )
195
+ end
196
+
197
+ def test_form_with_method_option
198
+ assert_dom_equal(
199
+ %(<form action="create" method="get"><p><label for="post_title">Title</label><br /><input id="post_title" name="post[title]" size="30" type="text" value="Hello World" /></p>\n<p><label for="post_body">Body</label><br /><div class="fieldWithErrors"><textarea cols="40" id="post_body" name="post[body]" rows="20">Back to the hill and over it again!</textarea></div></p><input name="commit" type="submit" value="Create" /></form>),
200
+ form("post", :method=>'get')
201
+ )
202
+ end
203
+
204
+ def test_form_with_action_option
205
+ output_buffer << form("post", :action => "sign")
206
+ assert_select "form[action=sign]" do |form|
207
+ assert_select "input[type=submit][value=Sign]"
208
+ end
209
+ end
210
+
211
+ def test_form_with_date
212
+ silence_warnings do
213
+ def Post.content_columns() [ Column.new(:date, "written_on", "Written on") ] end
214
+ end
215
+
216
+ assert_dom_equal(
217
+ %(<form action="create" method="post"><p><label for="post_written_on">Written on</label><br /><select id="post_written_on_1i" name="post[written_on(1i)]">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n<select id="post_written_on_2i" name="post[written_on(2i)]">\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n</select>\n<select id="post_written_on_3i" name="post[written_on(3i)]">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n</select>\n</p><input name="commit" type="submit" value="Create" /></form>),
218
+ form("post")
219
+ )
220
+ end
221
+
222
+ def test_form_with_datetime
223
+ silence_warnings do
224
+ def Post.content_columns() [ Column.new(:datetime, "written_on", "Written on") ] end
225
+ end
226
+ @post.written_on = Time.gm(2004, 6, 15, 16, 30)
227
+
228
+ assert_dom_equal(
229
+ %(<form action="create" method="post"><p><label for="post_written_on">Written on</label><br /><select id="post_written_on_1i" name="post[written_on(1i)]">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n<select id="post_written_on_2i" name="post[written_on(2i)]">\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n</select>\n<select id="post_written_on_3i" name="post[written_on(3i)]">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n</select>\n &mdash; <select id="post_written_on_4i" name="post[written_on(4i)]">\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n</select>\n : <select id="post_written_on_5i" name="post[written_on(5i)]">\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30" selected="selected">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n</select>\n</p><input name="commit" type="submit" value="Create" /></form>),
230
+ form("post")
231
+ )
232
+ end
233
+
234
+ def test_error_for_block
235
+ assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>1 error prohibited this post from being saved</h2><p>There were problems with the following fields:</p><ul><li>Author name can't be empty</li></ul></div>), error_messages_for("post")
236
+ assert_equal %(<div class="errorDeathByClass" id="errorDeathById"><h1>1 error prohibited this post from being saved</h1><p>There were problems with the following fields:</p><ul><li>Author name can't be empty</li></ul></div>), error_messages_for("post", :class => "errorDeathByClass", :id => "errorDeathById", :header_tag => "h1")
237
+ assert_equal %(<div id="errorDeathById"><h1>1 error prohibited this post from being saved</h1><p>There were problems with the following fields:</p><ul><li>Author name can't be empty</li></ul></div>), error_messages_for("post", :class => nil, :id => "errorDeathById", :header_tag => "h1")
238
+ assert_equal %(<div class="errorDeathByClass"><h1>1 error prohibited this post from being saved</h1><p>There were problems with the following fields:</p><ul><li>Author name can't be empty</li></ul></div>), error_messages_for("post", :class => "errorDeathByClass", :id => nil, :header_tag => "h1")
239
+ end
240
+
241
+ def test_error_messages_for_escapes_html
242
+ @dirty_post = DirtyPost.new
243
+ assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>1 error prohibited this dirty post from being saved</h2><p>There were problems with the following fields:</p><ul><li>Author name can't be &lt;em&gt;empty&lt;/em&gt;</li></ul></div>), error_messages_for("dirty_post")
244
+ end
245
+
246
+ def test_error_messages_for_handles_nil
247
+ assert_equal "", error_messages_for("notthere")
248
+ end
249
+
250
+ def test_error_message_on_escapes_html
251
+ @dirty_post = DirtyPost.new
252
+ assert_dom_equal "<div class=\"formError\">can't be &lt;em&gt;empty&lt;/em&gt;</div>", error_message_on(:dirty_post, :author_name)
253
+ end
254
+
255
+ def test_error_message_on_handles_nil
256
+ assert_equal "", error_message_on("notthere", "notthere")
257
+ end
258
+
259
+ def test_error_message_on
260
+ assert_dom_equal "<div class=\"formError\">can't be empty</div>", error_message_on(:post, :author_name)
261
+ end
262
+
263
+ def test_error_message_on_no_instance_variable
264
+ other_post = @post
265
+ assert_dom_equal "<div class=\"formError\">can't be empty</div>", error_message_on(other_post, :author_name)
266
+ end
267
+
268
+ def test_error_message_on_with_options_hash
269
+ assert_dom_equal "<div class=\"differentError\">beforecan't be emptyafter</div>", error_message_on(:post, :author_name, :css_class => 'differentError', :prepend_text => 'before', :append_text => 'after')
270
+ end
271
+
272
+ def test_error_message_on_with_tag_option_in_options_hash
273
+ assert_dom_equal "<span class=\"differentError\">beforecan't be emptyafter</span>", error_message_on(:post, :author_name, :html_tag => "span", :css_class => 'differentError', :prepend_text => 'before', :append_text => 'after')
274
+ end
275
+
276
+ def test_error_message_on_handles_empty_errors
277
+ assert_equal "", error_message_on(@post, :tag)
278
+ end
279
+
280
+ def test_error_messages_for_many_objects
281
+ assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>2 errors prohibited this post from being saved</h2><p>There were problems with the following fields:</p><ul><li>Author name can't be empty</li><li>User email can't be empty</li></ul></div>), error_messages_for("post", "user")
282
+
283
+ # reverse the order, error order changes and so does the title
284
+ assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>2 errors prohibited this user from being saved</h2><p>There were problems with the following fields:</p><ul><li>User email can't be empty</li><li>Author name can't be empty</li></ul></div>), error_messages_for("user", "post")
285
+
286
+ # add the default to put post back in the title
287
+ assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>2 errors prohibited this post from being saved</h2><p>There were problems with the following fields:</p><ul><li>User email can't be empty</li><li>Author name can't be empty</li></ul></div>), error_messages_for("user", "post", :object_name => "post")
288
+
289
+ # symbols work as well
290
+ assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>2 errors prohibited this post from being saved</h2><p>There were problems with the following fields:</p><ul><li>User email can't be empty</li><li>Author name can't be empty</li></ul></div>), error_messages_for(:user, :post, :object_name => :post)
291
+
292
+ # any default works too
293
+ assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>2 errors prohibited this monkey from being saved</h2><p>There were problems with the following fields:</p><ul><li>User email can't be empty</li><li>Author name can't be empty</li></ul></div>), error_messages_for(:user, :post, :object_name => "monkey")
294
+
295
+ # should space object name
296
+ assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>2 errors prohibited this chunky bacon from being saved</h2><p>There were problems with the following fields:</p><ul><li>User email can't be empty</li><li>Author name can't be empty</li></ul></div>), error_messages_for(:user, :post, :object_name => "chunky_bacon")
297
+
298
+ # hide header and explanation messages with nil or empty string
299
+ assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><ul><li>User email can't be empty</li><li>Author name can't be empty</li></ul></div>), error_messages_for(:user, :post, :header_message => nil, :message => "")
300
+
301
+ # override header and explanation messages
302
+ header_message = "Yikes! Some errors"
303
+ message = "Please fix the following fields and resubmit:"
304
+ assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>#{header_message}</h2><p>#{message}</p><ul><li>User email can't be empty</li><li>Author name can't be empty</li></ul></div>), error_messages_for(:user, :post, :header_message => header_message, :message => message)
305
+ end
306
+
307
+ def test_error_messages_for_non_instance_variable
308
+ actual_user = @user
309
+ actual_post = @post
310
+ @user = nil
311
+ @post = nil
312
+
313
+ #explicitly set object
314
+ assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>1 error prohibited this post from being saved</h2><p>There were problems with the following fields:</p><ul><li>Author name can't be empty</li></ul></div>), error_messages_for("post", :object => actual_post)
315
+
316
+ #multiple objects
317
+ assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>2 errors prohibited this user from being saved</h2><p>There were problems with the following fields:</p><ul><li>User email can't be empty</li><li>Author name can't be empty</li></ul></div>), error_messages_for("user", "post", :object => [actual_user, actual_post])
318
+
319
+ #nil object
320
+ assert_equal '', error_messages_for('user', :object => nil)
321
+ end
322
+
323
+ def test_error_messages_for_model_objects
324
+ error = error_messages_for(@post)
325
+ assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>1 error prohibited this post from being saved</h2><p>There were problems with the following fields:</p><ul><li>Author name can't be empty</li></ul></div>),
326
+ error
327
+
328
+ error = error_messages_for(@user, @post)
329
+ assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>2 errors prohibited this user from being saved</h2><p>There were problems with the following fields:</p><ul><li>User email can't be empty</li><li>Author name can't be empty</li></ul></div>),
330
+ error
331
+ end
332
+
333
+ def test_form_with_string_multipart
334
+ assert_dom_equal(
335
+ %(<form action="create" enctype="multipart/form-data" method="post"><p><label for="post_title">Title</label><br /><input id="post_title" name="post[title]" size="30" type="text" value="Hello World" /></p>\n<p><label for="post_body">Body</label><br /><div class="fieldWithErrors"><textarea cols="40" id="post_body" name="post[body]" rows="20">Back to the hill and over it again!</textarea></div></p><input name="commit" type="submit" value="Create" /></form>),
336
+ form("post", :multipart => true)
337
+ )
338
+ end
339
+
340
+ def test_default_form_builder_with_dynamic_form_helpers
341
+ form_for(@post, :as => :post, :url => {}) do |f|
342
+ concat f.error_message_on('author_name')
343
+ concat f.error_messages
344
+ end
345
+
346
+ expected = %(<form class="post_new" method="post" action="" id="post_new">) +
347
+ %(<div class="formError">can't be empty</div>) +
348
+ %(<div class="errorExplanation" id="errorExplanation"><h2>1 error prohibited this post from being saved</h2><p>There were problems with the following fields:</p><ul><li>Author name can't be empty</li></ul></div>) +
349
+ %(</form>)
350
+
351
+ assert_dom_equal expected, output_buffer
352
+ end
353
+
354
+ def test_default_form_builder_no_instance_variable
355
+ post = @post
356
+ @post = nil
357
+
358
+ form_for(post, :as => :post, :url => {}) do |f|
359
+ concat f.error_message_on('author_name')
360
+ concat f.error_messages
361
+ end
362
+
363
+ expected = %(<form class="post_new" method="post" action="" id="post_new">) +
364
+ %(<div class="formError">can't be empty</div>) +
365
+ %(<div class="errorExplanation" id="errorExplanation"><h2>1 error prohibited this post from being saved</h2><p>There were problems with the following fields:</p><ul><li>Author name can't be empty</li></ul></div>) +
366
+ %(</form>)
367
+
368
+ assert_dom_equal expected, output_buffer
369
+ end
370
+ end
@@ -0,0 +1,9 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'active_support'
4
+ require 'active_support/core_ext'
5
+ require 'action_view'
6
+ require 'action_controller'
7
+ require 'action_controller/test_case'
8
+ require 'active_model'
9
+ require 'action_view/helpers/dynamic_form'
@@ -0,0 +1,432 @@
1
+ module PrototypeHelper
2
+ # Creates a button with an onclick event which calls a remote action
3
+ # via XMLHttpRequest
4
+ # The options for specifying the target with :url
5
+ # and defining callbacks is the same as link_to_remote.
6
+ def button_to_remote(name, options = {}, html_options = {})
7
+ button_to_function(name, remote_function(options), html_options)
8
+ end
9
+
10
+ # Returns a button input tag with the element name of +name+ and a value (i.e., display text) of +value+
11
+ # that will submit form using XMLHttpRequest in the background instead of a regular POST request that
12
+ # reloads the page.
13
+ #
14
+ # # Create a button that submits to the create action
15
+ # #
16
+ # # Generates: <input name="create_btn" onclick="new Ajax.Request('/testing/create',
17
+ # # {asynchronous:true, evalScripts:true, parameters:Form.serialize(this.form)});
18
+ # # return false;" type="button" value="Create" />
19
+ # <%= submit_to_remote 'create_btn', 'Create', :url => { :action => 'create' } %>
20
+ #
21
+ # # Submit to the remote action update and update the DIV succeed or fail based
22
+ # # on the success or failure of the request
23
+ # #
24
+ # # Generates: <input name="update_btn" onclick="new Ajax.Updater({success:'succeed',failure:'fail'},
25
+ # # '/testing/update', {asynchronous:true, evalScripts:true, parameters:Form.serialize(this.form)});
26
+ # # return false;" type="button" value="Update" />
27
+ # <%= submit_to_remote 'update_btn', 'Update', :url => { :action => 'update' },
28
+ # :update => { :success => "succeed", :failure => "fail" }
29
+ #
30
+ # <tt>options</tt> argument is the same as in form_remote_tag.
31
+ def submit_to_remote(name, value, options = {})
32
+ options[:with] ||= 'Form.serialize(this.form)'
33
+
34
+ html_options = options.delete(:html) || {}
35
+ html_options[:name] = name
36
+
37
+ button_to_remote(value, options, html_options)
38
+ end
39
+
40
+ # Returns a link to a remote action defined by <tt>options[:url]</tt>
41
+ # (using the url_for format) that's called in the background using
42
+ # XMLHttpRequest. The result of that request can then be inserted into a
43
+ # DOM object whose id can be specified with <tt>options[:update]</tt>.
44
+ # Usually, the result would be a partial prepared by the controller with
45
+ # render :partial.
46
+ #
47
+ # Examples:
48
+ # # Generates: <a href="#" onclick="new Ajax.Updater('posts', '/blog/destroy/3', {asynchronous:true, evalScripts:true});
49
+ # # return false;">Delete this post</a>
50
+ # link_to_remote "Delete this post", :update => "posts",
51
+ # :url => { :action => "destroy", :id => post.id }
52
+ #
53
+ # # Generates: <a href="#" onclick="new Ajax.Updater('emails', '/mail/list_emails', {asynchronous:true, evalScripts:true});
54
+ # # return false;"><img alt="Refresh" src="/images/refresh.png?" /></a>
55
+ # link_to_remote(image_tag("refresh"), :update => "emails",
56
+ # :url => { :action => "list_emails" })
57
+ #
58
+ # You can override the generated HTML options by specifying a hash in
59
+ # <tt>options[:html]</tt>.
60
+ #
61
+ # link_to_remote "Delete this post", :update => "posts",
62
+ # :url => post_url(@post), :method => :delete,
63
+ # :html => { :class => "destructive" }
64
+ #
65
+ # You can also specify a hash for <tt>options[:update]</tt> to allow for
66
+ # easy redirection of output to an other DOM element if a server-side
67
+ # error occurs:
68
+ #
69
+ # Example:
70
+ # # Generates: <a href="#" onclick="new Ajax.Updater({success:'posts',failure:'error'}, '/blog/destroy/5',
71
+ # # {asynchronous:true, evalScripts:true}); return false;">Delete this post</a>
72
+ # link_to_remote "Delete this post",
73
+ # :url => { :action => "destroy", :id => post.id },
74
+ # :update => { :success => "posts", :failure => "error" }
75
+ #
76
+ # Optionally, you can use the <tt>options[:position]</tt> parameter to
77
+ # influence how the target DOM element is updated. It must be one of
78
+ # <tt>:before</tt>, <tt>:top</tt>, <tt>:bottom</tt>, or <tt>:after</tt>.
79
+ #
80
+ # The method used is by default POST. You can also specify GET or you
81
+ # can simulate PUT or DELETE over POST. All specified with <tt>options[:method]</tt>
82
+ #
83
+ # Example:
84
+ # # Generates: <a href="#" onclick="new Ajax.Request('/person/4', {asynchronous:true, evalScripts:true, method:'delete'});
85
+ # # return false;">Destroy</a>
86
+ # link_to_remote "Destroy", :url => person_url(:id => person), :method => :delete
87
+ #
88
+ # By default, these remote requests are processed asynchronous during
89
+ # which various JavaScript callbacks can be triggered (for progress
90
+ # indicators and the likes). All callbacks get access to the
91
+ # <tt>request</tt> object, which holds the underlying XMLHttpRequest.
92
+ #
93
+ # To access the server response, use <tt>request.responseText</tt>, to
94
+ # find out the HTTP status, use <tt>request.status</tt>.
95
+ #
96
+ # Example:
97
+ # # Generates: <a href="#" onclick="new Ajax.Request('/words/undo?n=33', {asynchronous:true, evalScripts:true,
98
+ # # onComplete:function(request){undoRequestCompleted(request)}}); return false;">hello</a>
99
+ # word = 'hello'
100
+ # link_to_remote word,
101
+ # :url => { :action => "undo", :n => word_counter },
102
+ # :complete => "undoRequestCompleted(request)"
103
+ #
104
+ # The callbacks that may be specified are (in order):
105
+ #
106
+ # <tt>:loading</tt>:: Called when the remote document is being
107
+ # loaded with data by the browser.
108
+ # <tt>:loaded</tt>:: Called when the browser has finished loading
109
+ # the remote document.
110
+ # <tt>:interactive</tt>:: Called when the user can interact with the
111
+ # remote document, even though it has not
112
+ # finished loading.
113
+ # <tt>:success</tt>:: Called when the XMLHttpRequest is completed,
114
+ # and the HTTP status code is in the 2XX range.
115
+ # <tt>:failure</tt>:: Called when the XMLHttpRequest is completed,
116
+ # and the HTTP status code is not in the 2XX
117
+ # range.
118
+ # <tt>:complete</tt>:: Called when the XMLHttpRequest is complete
119
+ # (fires after success/failure if they are
120
+ # present).
121
+ #
122
+ # You can further refine <tt>:success</tt> and <tt>:failure</tt> by
123
+ # adding additional callbacks for specific status codes.
124
+ #
125
+ # Example:
126
+ # # Generates: <a href="#" onclick="new Ajax.Request('/testing/action', {asynchronous:true, evalScripts:true,
127
+ # # on404:function(request){alert('Not found...? Wrong URL...?')},
128
+ # # onFailure:function(request){alert('HTTP Error ' + request.status + '!')}}); return false;">hello</a>
129
+ # link_to_remote word,
130
+ # :url => { :action => "action" },
131
+ # 404 => "alert('Not found...? Wrong URL...?')",
132
+ # :failure => "alert('HTTP Error ' + request.status + '!')"
133
+ #
134
+ # A status code callback overrides the success/failure handlers if
135
+ # present.
136
+ #
137
+ # If you for some reason or another need synchronous processing (that'll
138
+ # block the browser while the request is happening), you can specify
139
+ # <tt>options[:type] = :synchronous</tt>.
140
+ #
141
+ # You can customize further browser side call logic by passing in
142
+ # JavaScript code snippets via some optional parameters. In their order
143
+ # of use these are:
144
+ #
145
+ # <tt>:confirm</tt>:: Adds confirmation dialog.
146
+ # <tt>:condition</tt>:: Perform remote request conditionally
147
+ # by this expression. Use this to
148
+ # describe browser-side conditions when
149
+ # request should not be initiated.
150
+ # <tt>:before</tt>:: Called before request is initiated.
151
+ # <tt>:after</tt>:: Called immediately after request was
152
+ # initiated and before <tt>:loading</tt>.
153
+ # <tt>:submit</tt>:: Specifies the DOM element ID that's used
154
+ # as the parent of the form elements. By
155
+ # default this is the current form, but
156
+ # it could just as well be the ID of a
157
+ # table row or any other DOM element.
158
+ # <tt>:with</tt>:: A JavaScript expression specifying
159
+ # the parameters for the XMLHttpRequest.
160
+ # Any expressions should return a valid
161
+ # URL query string.
162
+ #
163
+ # Example:
164
+ #
165
+ # :with => "'name=' + $('name').value"
166
+ #
167
+ # You can generate a link that uses AJAX in the general case, while
168
+ # degrading gracefully to plain link behavior in the absence of
169
+ # JavaScript by setting <tt>html_options[:href]</tt> to an alternate URL.
170
+ # Note the extra curly braces around the <tt>options</tt> hash separate
171
+ # it as the second parameter from <tt>html_options</tt>, the third.
172
+ #
173
+ # Example:
174
+ # link_to_remote "Delete this post",
175
+ # { :update => "posts", :url => { :action => "destroy", :id => post.id } },
176
+ # :href => url_for(:action => "destroy", :id => post.id)
177
+ def link_to_remote(name, options = {}, html_options = nil)
178
+ link_to_function(name, remote_function(options), html_options || options.delete(:html))
179
+ end
180
+
181
+ # Returns a form tag that will submit using XMLHttpRequest in the
182
+ # background instead of the regular reloading POST arrangement. Even
183
+ # though it's using JavaScript to serialize the form elements, the form
184
+ # submission will work just like a regular submission as viewed by the
185
+ # receiving side (all elements available in <tt>params</tt>). The options for
186
+ # specifying the target with <tt>:url</tt> and defining callbacks is the same as
187
+ # +link_to_remote+.
188
+ #
189
+ # A "fall-through" target for browsers that doesn't do JavaScript can be
190
+ # specified with the <tt>:action</tt>/<tt>:method</tt> options on <tt>:html</tt>.
191
+ #
192
+ # Example:
193
+ # # Generates:
194
+ # # <form action="/some/place" method="post" onsubmit="new Ajax.Request('',
195
+ # # {asynchronous:true, evalScripts:true, parameters:Form.serialize(this)}); return false;">
196
+ # form_remote_tag :html => { :action =>
197
+ # url_for(:controller => "some", :action => "place") }
198
+ #
199
+ # The Hash passed to the <tt>:html</tt> key is equivalent to the options (2nd)
200
+ # argument in the FormTagHelper.form_tag method.
201
+ #
202
+ # By default the fall-through action is the same as the one specified in
203
+ # the <tt>:url</tt> (and the default method is <tt>:post</tt>).
204
+ #
205
+ # form_remote_tag also takes a block, like form_tag:
206
+ # # Generates:
207
+ # # <form action="/" method="post" onsubmit="new Ajax.Request('/',
208
+ # # {asynchronous:true, evalScripts:true, parameters:Form.serialize(this)});
209
+ # # return false;"> <div><input name="commit" type="submit" value="Save" /></div>
210
+ # # </form>
211
+ # <% form_remote_tag :url => '/posts' do -%>
212
+ # <div><%= submit_tag 'Save' %></div>
213
+ # <% end -%>
214
+ def form_remote_tag(options = {}, &block)
215
+ options[:form] = true
216
+
217
+ options[:html] ||= {}
218
+ options[:html][:onsubmit] =
219
+ (options[:html][:onsubmit] ? options[:html][:onsubmit] + "; " : "") +
220
+ "#{remote_function(options)}; return false;"
221
+
222
+ form_tag(options[:html].delete(:action) || url_for(options[:url]), options[:html], &block)
223
+ end
224
+
225
+ # Creates a form that will submit using XMLHttpRequest in the background
226
+ # instead of the regular reloading POST arrangement and a scope around a
227
+ # specific resource that is used as a base for questioning about
228
+ # values for the fields.
229
+ #
230
+ # === Resource
231
+ #
232
+ # Example:
233
+ # <% remote_form_for(@post) do |f| %>
234
+ # ...
235
+ # <% end %>
236
+ #
237
+ # This will expand to be the same as:
238
+ #
239
+ # <% remote_form_for :post, @post, :url => post_path(@post), :html => { :method => :put, :class => "edit_post", :id => "edit_post_45" } do |f| %>
240
+ # ...
241
+ # <% end %>
242
+ #
243
+ # === Nested Resource
244
+ #
245
+ # Example:
246
+ # <% remote_form_for([@post, @comment]) do |f| %>
247
+ # ...
248
+ # <% end %>
249
+ #
250
+ # This will expand to be the same as:
251
+ #
252
+ # <% remote_form_for :comment, @comment, :url => post_comment_path(@post, @comment), :html => { :method => :put, :class => "edit_comment", :id => "edit_comment_45" } do |f| %>
253
+ # ...
254
+ # <% end %>
255
+ #
256
+ # If you don't need to attach a form to a resource, then check out form_remote_tag.
257
+ #
258
+ # See FormHelper#form_for for additional semantics.
259
+ def remote_form_for(record_or_name_or_array, *args, &proc)
260
+ options = args.extract_options!
261
+
262
+ case record_or_name_or_array
263
+ when String, Symbol
264
+ object_name = record_or_name_or_array
265
+ when Array
266
+ object = record_or_name_or_array.last
267
+ object_name = ActiveModel::Naming.singular(object)
268
+ apply_form_for_options!(record_or_name_or_array, options)
269
+ args.unshift object
270
+ else
271
+ object = record_or_name_or_array
272
+ object_name = ActiveModel::Naming.singular(record_or_name_or_array)
273
+ apply_form_for_options!(object, options)
274
+ args.unshift object
275
+ end
276
+
277
+ form_remote_tag options do
278
+ fields_for object_name, *(args << options), &proc
279
+ end
280
+ end
281
+ alias_method :form_remote_for, :remote_form_for
282
+
283
+ # Returns '<tt>eval(request.responseText)</tt>' which is the JavaScript function
284
+ # that +form_remote_tag+ can call in <tt>:complete</tt> to evaluate a multiple
285
+ # update return document using +update_element_function+ calls.
286
+ def evaluate_remote_response
287
+ "eval(request.responseText)"
288
+ end
289
+
290
+ # Observes the field with the DOM ID specified by +field_id+ and calls a
291
+ # callback when its contents have changed. The default callback is an
292
+ # Ajax call. By default the value of the observed field is sent as a
293
+ # parameter with the Ajax call.
294
+ #
295
+ # Example:
296
+ # # Generates: new Form.Element.Observer('suggest', 0.25, function(element, value) {new Ajax.Updater('suggest',
297
+ # # '/testing/find_suggestion', {asynchronous:true, evalScripts:true, parameters:'q=' + value})})
298
+ # <%= observe_field :suggest, :url => { :action => :find_suggestion },
299
+ # :frequency => 0.25,
300
+ # :update => :suggest,
301
+ # :with => 'q'
302
+ # %>
303
+ #
304
+ # Required +options+ are either of:
305
+ # <tt>:url</tt>:: +url_for+-style options for the action to call
306
+ # when the field has changed.
307
+ # <tt>:function</tt>:: Instead of making a remote call to a URL, you
308
+ # can specify javascript code to be called instead.
309
+ # Note that the value of this option is used as the
310
+ # *body* of the javascript function, a function definition
311
+ # with parameters named element and value will be generated for you
312
+ # for example:
313
+ # observe_field("glass", :frequency => 1, :function => "alert('Element changed')")
314
+ # will generate:
315
+ # new Form.Element.Observer('glass', 1, function(element, value) {alert('Element changed')})
316
+ # The element parameter is the DOM element being observed, and the value is its value at the
317
+ # time the observer is triggered.
318
+ #
319
+ # Additional options are:
320
+ # <tt>:frequency</tt>:: The frequency (in seconds) at which changes to
321
+ # this field will be detected. Not setting this
322
+ # option at all or to a value equal to or less than
323
+ # zero will use event based observation instead of
324
+ # time based observation.
325
+ # <tt>:update</tt>:: Specifies the DOM ID of the element whose
326
+ # innerHTML should be updated with the
327
+ # XMLHttpRequest response text.
328
+ # <tt>:with</tt>:: A JavaScript expression specifying the parameters
329
+ # for the XMLHttpRequest. The default is to send the
330
+ # key and value of the observed field. Any custom
331
+ # expressions should return a valid URL query string.
332
+ # The value of the field is stored in the JavaScript
333
+ # variable +value+.
334
+ #
335
+ # Examples
336
+ #
337
+ # :with => "'my_custom_key=' + value"
338
+ # :with => "'person[name]=' + prompt('New name')"
339
+ # :with => "Form.Element.serialize('other-field')"
340
+ #
341
+ # Finally
342
+ # :with => 'name'
343
+ # is shorthand for
344
+ # :with => "'name=' + value"
345
+ # This essentially just changes the key of the parameter.
346
+ #
347
+ # Additionally, you may specify any of the options documented in the
348
+ # <em>Common options</em> section at the top of this document.
349
+ #
350
+ # Example:
351
+ #
352
+ # # Sends params: {:title => 'Title of the book'} when the book_title input
353
+ # # field is changed.
354
+ # observe_field 'book_title',
355
+ # :url => 'http://example.com/books/edit/1',
356
+ # :with => 'title'
357
+ #
358
+ #
359
+ def observe_field(field_id, options = {})
360
+ if options[:frequency] && options[:frequency] > 0
361
+ build_observer('Form.Element.Observer', field_id, options)
362
+ else
363
+ build_observer('Form.Element.EventObserver', field_id, options)
364
+ end
365
+ end
366
+
367
+ # Observes the form with the DOM ID specified by +form_id+ and calls a
368
+ # callback when its contents have changed. The default callback is an
369
+ # Ajax call. By default all fields of the observed field are sent as
370
+ # parameters with the Ajax call.
371
+ #
372
+ # The +options+ for +observe_form+ are the same as the options for
373
+ # +observe_field+. The JavaScript variable +value+ available to the
374
+ # <tt>:with</tt> option is set to the serialized form by default.
375
+ def observe_form(form_id, options = {})
376
+ if options[:frequency]
377
+ build_observer('Form.Observer', form_id, options)
378
+ else
379
+ build_observer('Form.EventObserver', form_id, options)
380
+ end
381
+ end
382
+
383
+ # Periodically calls the specified url (<tt>options[:url]</tt>) every
384
+ # <tt>options[:frequency]</tt> seconds (default is 10). Usually used to
385
+ # update a specified div (<tt>options[:update]</tt>) with the results
386
+ # of the remote call. The options for specifying the target with <tt>:url</tt>
387
+ # and defining callbacks is the same as link_to_remote.
388
+ # Examples:
389
+ # # Call get_averages and put its results in 'avg' every 10 seconds
390
+ # # Generates:
391
+ # # new PeriodicalExecuter(function() {new Ajax.Updater('avg', '/grades/get_averages',
392
+ # # {asynchronous:true, evalScripts:true})}, 10)
393
+ # periodically_call_remote(:url => { :action => 'get_averages' }, :update => 'avg')
394
+ #
395
+ # # Call invoice every 10 seconds with the id of the customer
396
+ # # If it succeeds, update the invoice DIV; if it fails, update the error DIV
397
+ # # Generates:
398
+ # # new PeriodicalExecuter(function() {new Ajax.Updater({success:'invoice',failure:'error'},
399
+ # # '/testing/invoice/16', {asynchronous:true, evalScripts:true})}, 10)
400
+ # periodically_call_remote(:url => { :action => 'invoice', :id => customer.id },
401
+ # :update => { :success => "invoice", :failure => "error" }
402
+ #
403
+ # # Call update every 20 seconds and update the new_block DIV
404
+ # # Generates:
405
+ # # new PeriodicalExecuter(function() {new Ajax.Updater('news_block', 'update', {asynchronous:true, evalScripts:true})}, 20)
406
+ # periodically_call_remote(:url => 'update', :frequency => '20', :update => 'news_block')
407
+ #
408
+ def periodically_call_remote(options = {})
409
+ frequency = options[:frequency] || 10 # every ten seconds by default
410
+ code = "new PeriodicalExecuter(function() {#{remote_function(options)}}, #{frequency})"
411
+ javascript_tag(code)
412
+ end
413
+
414
+ protected
415
+ def build_observer(klass, name, options = {})
416
+ if options[:with] && (options[:with] !~ /[\{=(.]/)
417
+ options[:with] = "'#{options[:with]}=' + encodeURIComponent(value)"
418
+ else
419
+ options[:with] ||= 'value' unless options[:function]
420
+ end
421
+
422
+ callback = options[:function] || remote_function(options)
423
+ javascript = "new #{klass}('#{name}', "
424
+ javascript << "#{options[:frequency]}, " if options[:frequency]
425
+ javascript << "function(element, value) {"
426
+ javascript << "#{callback}}"
427
+ javascript << ")"
428
+ javascript_tag(javascript)
429
+ end
430
+ end
431
+
432
+ ActionController::Base.helper PrototypeHelper