actionpack 1.8.1 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actionpack might be problematic. Click here for more details.

Files changed (101) hide show
  1. data/CHANGELOG +309 -16
  2. data/README +1 -1
  3. data/lib/action_controller.rb +5 -0
  4. data/lib/action_controller/assertions.rb +57 -12
  5. data/lib/action_controller/auto_complete.rb +47 -0
  6. data/lib/action_controller/base.rb +288 -258
  7. data/lib/action_controller/benchmarking.rb +8 -3
  8. data/lib/action_controller/caching.rb +88 -42
  9. data/lib/action_controller/cgi_ext/cgi_ext.rb +1 -1
  10. data/lib/action_controller/cgi_ext/cgi_methods.rb +41 -11
  11. data/lib/action_controller/cgi_ext/multipart_progress.rb +169 -0
  12. data/lib/action_controller/cgi_ext/raw_post_data_fix.rb +30 -12
  13. data/lib/action_controller/cgi_process.rb +39 -11
  14. data/lib/action_controller/code_generation.rb +235 -0
  15. data/lib/action_controller/cookies.rb +14 -8
  16. data/lib/action_controller/deprecated_renders_and_redirects.rb +76 -0
  17. data/lib/action_controller/filters.rb +8 -7
  18. data/lib/action_controller/helpers.rb +41 -6
  19. data/lib/action_controller/layout.rb +45 -16
  20. data/lib/action_controller/request.rb +86 -23
  21. data/lib/action_controller/rescue.rb +1 -0
  22. data/lib/action_controller/response.rb +1 -1
  23. data/lib/action_controller/routing.rb +536 -272
  24. data/lib/action_controller/scaffolding.rb +30 -25
  25. data/lib/action_controller/session/active_record_store.rb +251 -50
  26. data/lib/action_controller/streaming.rb +133 -0
  27. data/lib/action_controller/templates/rescues/_request_and_response.rhtml +0 -7
  28. data/lib/action_controller/templates/scaffolds/edit.rhtml +2 -2
  29. data/lib/action_controller/templates/scaffolds/layout.rhtml +22 -18
  30. data/lib/action_controller/templates/scaffolds/list.rhtml +3 -3
  31. data/lib/action_controller/templates/scaffolds/new.rhtml +2 -2
  32. data/lib/action_controller/templates/scaffolds/show.rhtml +1 -1
  33. data/lib/action_controller/test_process.rb +68 -47
  34. data/lib/action_controller/upload_progress.rb +421 -0
  35. data/lib/action_controller/url_rewriter.rb +8 -11
  36. data/lib/action_controller/vendor/html-scanner/html/document.rb +6 -5
  37. data/lib/action_controller/vendor/html-scanner/html/node.rb +70 -14
  38. data/lib/action_controller/vendor/html-scanner/html/tokenizer.rb +17 -10
  39. data/lib/action_controller/vendor/html-scanner/html/version.rb +3 -3
  40. data/lib/action_controller/vendor/xml_simple.rb +1019 -0
  41. data/lib/action_controller/verification.rb +36 -30
  42. data/lib/action_view/base.rb +21 -14
  43. data/lib/action_view/helpers/active_record_helper.rb +15 -13
  44. data/lib/action_view/helpers/asset_tag_helper.rb +26 -9
  45. data/lib/action_view/helpers/benchmark_helper.rb +24 -0
  46. data/lib/action_view/helpers/capture_helper.rb +7 -5
  47. data/lib/action_view/helpers/date_helper.rb +63 -46
  48. data/lib/action_view/helpers/form_helper.rb +7 -1
  49. data/lib/action_view/helpers/form_options_helper.rb +19 -11
  50. data/lib/action_view/helpers/form_tag_helper.rb +5 -1
  51. data/lib/action_view/helpers/javascript_helper.rb +403 -35
  52. data/lib/action_view/helpers/javascripts/controls.js +261 -0
  53. data/lib/action_view/helpers/javascripts/dragdrop.js +476 -0
  54. data/lib/action_view/helpers/javascripts/effects.js +570 -0
  55. data/lib/action_view/helpers/javascripts/prototype.js +633 -371
  56. data/lib/action_view/helpers/number_helper.rb +11 -13
  57. data/lib/action_view/helpers/tag_helper.rb +1 -2
  58. data/lib/action_view/helpers/text_helper.rb +69 -6
  59. data/lib/action_view/helpers/upload_progress_helper.rb +433 -0
  60. data/lib/action_view/helpers/url_helper.rb +98 -3
  61. data/lib/action_view/partials.rb +14 -8
  62. data/lib/action_view/vendor/builder/xmlmarkup.rb +11 -0
  63. data/rakefile +13 -5
  64. data/test/abstract_unit.rb +1 -1
  65. data/test/controller/action_pack_assertions_test.rb +52 -9
  66. data/test/controller/active_record_assertions_test.rb +119 -120
  67. data/test/controller/active_record_store_test.rb +111 -0
  68. data/test/controller/addresses_render_test.rb +45 -0
  69. data/test/controller/caching_filestore.rb +92 -0
  70. data/test/controller/capture_test.rb +39 -0
  71. data/test/controller/cgi_test.rb +40 -3
  72. data/test/controller/helper_test.rb +65 -13
  73. data/test/controller/multipart_progress_testx.rb +365 -0
  74. data/test/controller/new_render_test.rb +263 -0
  75. data/test/controller/redirect_test.rb +64 -0
  76. data/test/controller/render_test.rb +20 -21
  77. data/test/controller/request_test.rb +83 -3
  78. data/test/controller/routing_test.rb +702 -0
  79. data/test/controller/send_file_test.rb +2 -0
  80. data/test/controller/test_test.rb +44 -8
  81. data/test/controller/upload_progress_testx.rb +89 -0
  82. data/test/controller/verification_test.rb +94 -29
  83. data/test/fixtures/addresses/list.rhtml +1 -0
  84. data/test/fixtures/test/capturing.rhtml +4 -0
  85. data/test/fixtures/test/list.rhtml +1 -1
  86. data/test/fixtures/test/update_element_with_capture.rhtml +9 -0
  87. data/test/template/active_record_helper_test.rb +30 -15
  88. data/test/template/asset_tag_helper_test.rb +12 -5
  89. data/test/template/benchmark_helper_test.rb +72 -0
  90. data/test/template/date_helper_test.rb +69 -0
  91. data/test/template/form_helper_test.rb +18 -10
  92. data/test/template/form_options_helper_test.rb +40 -5
  93. data/test/template/javascript_helper.rb +149 -2
  94. data/test/template/number_helper_test.rb +2 -0
  95. data/test/template/tag_helper_test.rb +4 -0
  96. data/test/template/text_helper_test.rb +36 -0
  97. data/test/template/upload_progress_helper_testx.rb +272 -0
  98. data/test/template/url_helper_test.rb +30 -0
  99. metadata +30 -6
  100. data/test/controller/layout_test.rb +0 -49
  101. data/test/controller/routing_tests.rb +0 -543
@@ -8,20 +8,18 @@ module ActionView
8
8
  # The area code can be surrounded by parenthesis by setting +:area_code+ to true; default is false
9
9
  # The delimiter can be set using +:delimiter+; default is "-"
10
10
  # Examples:
11
- # number_to_phone(1235551234) => 123-555-1234
12
- # number_to_phone(1235551234, {:area_code => true}) => (123) 555-1234
13
- # number_to_phone(1235551234, {:delimiter => " "}) => 123 555 1234
11
+ # number_to_phone(1235551234) => 123-555-1234
12
+ # number_to_phone(1235551234, {:area_code => true}) => (123) 555-1234
13
+ # number_to_phone(1235551234, {:delimiter => " "}) => 123 555 1234
14
+ # number_to_phone(1235551234, {:area_code => true, :extension => 555}) => (123) 555-1234 x 555
14
15
  def number_to_phone(number, options = {})
15
- options = options.stringify_keys
16
+ options = options.stringify_keys
16
17
  area_code = options.delete("area_code") { false }
17
18
  delimiter = options.delete("delimiter") { "-" }
19
+ extension = options.delete("extension") { "" }
18
20
  begin
19
- str = number.to_s
20
- if area_code == true
21
- str.gsub!(/([0-9]{3})([0-9]{3})([0-9]{4})/,"(\\1) \\2#{delimiter}\\3")
22
- else
23
- str.gsub!(/([0-9]{3})([0-9]{3})([0-9]{4})/,"\\1#{delimiter}\\2#{delimiter}\\3")
24
- end
21
+ str = area_code == true ? number.to_s.gsub(/([0-9]{3})([0-9]{3})([0-9]{4})/,"(\\1) \\2#{delimiter}\\3") : number.to_s.gsub(/([0-9]{3})([0-9]{3})([0-9]{4})/,"\\1#{delimiter}\\2#{delimiter}\\3")
22
+ extension.to_s.strip.empty? ? str : "#{str} x #{extension.to_s.strip}"
25
23
  rescue
26
24
  number
27
25
  end
@@ -51,9 +49,9 @@ module ActionView
51
49
  # The +number+ can contain a level of precision using the +precision+ key; default is 3
52
50
  # The unit separator can be set using the +separator+ key; default is "."
53
51
  # Examples:
54
- # number_to_precision(100) => 100.000%
55
- # number_to_precision(100, {:precision => 0}) => 100%
56
- # number_to_precision(302.0574, {:precision => 2}) => 302.06%
52
+ # number_to_percentage(100) => 100.000%
53
+ # number_to_percentage(100, {:precision => 0}) => 100%
54
+ # number_to_percentage(302.0574, {:precision => 2}) => 302.06%
57
55
  def number_to_percentage(number, options = {})
58
56
  options = options.stringify_keys
59
57
  precision, separator = options.delete("precision") { 3 }, options.delete("separator") { "." }
@@ -25,8 +25,7 @@ module ActionView
25
25
  private
26
26
  def tag_options(options)
27
27
  unless options.empty?
28
- options.symbolize_keys
29
- " " + options.map { |key, value|
28
+ " " + options.symbolize_keys.map { |key, value|
30
29
  %(#{key}="#{html_escape(value.to_s)}")
31
30
  }.sort.join(" ")
32
31
  end
@@ -1,3 +1,5 @@
1
+ require File.dirname(__FILE__) + '/tag_helper'
2
+
1
3
  module ActionView
2
4
  module Helpers #:nodoc:
3
5
  # Provides a set of methods for working with text strings that can help unburden the level of inline Ruby code in the
@@ -26,7 +28,7 @@ module ActionView
26
28
  # passing +highlighter+ as single-quoted string with \1 where the phrase is supposed to be inserted.
27
29
  # N.B.: The +phrase+ is sanitized to include only letters, digits, and spaces before use.
28
30
  def highlight(text, phrase, highlighter = '<strong class="highlight">\1</strong>')
29
- if text.nil? || phrase.nil? then return end
31
+ if phrase.blank? then return text end
30
32
  text.gsub(/(#{escape_regexp(phrase)})/i, highlighter) unless text.nil?
31
33
  end
32
34
 
@@ -63,6 +65,11 @@ module ActionView
63
65
  end
64
66
  end
65
67
 
68
+ # Word wrap long lines to line_width.
69
+ def word_wrap(text, line_width = 80)
70
+ text.gsub(/\n/, "\n\n").gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1\n").strip
71
+ end
72
+
66
73
  begin
67
74
  require "redcloth"
68
75
 
@@ -116,11 +123,11 @@ module ActionView
116
123
  # auto_link("Go to http://www.rubyonrails.com and say hello to david@loudthinking.com") =>
117
124
  # Go to <a href="http://www.rubyonrails.com">http://www.rubyonrails.com</a> and
118
125
  # say hello to <a href="mailto:david@loudthinking.com">david@loudthinking.com</a>
119
- def auto_link(text, link = :all)
126
+ def auto_link(text, link = :all, href_options = {})
120
127
  case link
121
- when :all then auto_link_urls(auto_link_email_addresses(text))
128
+ when :all then auto_link_urls(auto_link_email_addresses(text), href_options)
122
129
  when :email_addresses then auto_link_email_addresses(text)
123
- when :urls then auto_link_urls(text)
130
+ when :urls then auto_link_urls(text, href_options)
124
131
  end
125
132
  end
126
133
 
@@ -128,6 +135,61 @@ module ActionView
128
135
  def strip_links(text)
129
136
  text.gsub(/<a.*>(.*)<\/a>/m, '\1')
130
137
  end
138
+
139
+ # Try to require the html-scanner library
140
+ begin
141
+ require 'html/tokenizer'
142
+ require 'html/node'
143
+ rescue LoadError
144
+ # if there isn't a copy installed, use the vendor version in
145
+ # action controller
146
+ $:.unshift File.join(File.dirname(__FILE__), "..", "..",
147
+ "action_controller", "vendor", "html-scanner")
148
+ require 'html/tokenizer'
149
+ require 'html/node'
150
+ end
151
+
152
+ VERBOTEN_TAGS = %w(form script) unless defined?(VERBOTEN_TAGS)
153
+ VERBOTEN_ATTRS = /^on/i unless defined?(VERBOTEN_ATTRS)
154
+
155
+ # Sanitizes the given HTML by making form and script tags into regular
156
+ # text, and removing all "onxxx" attributes (so that arbitrary Javascript
157
+ # cannot be executed). Also removes href attributes that start with
158
+ # "javascript:".
159
+ #
160
+ # Returns the sanitized text.
161
+ def sanitize(html)
162
+ # only do this if absolutely necessary
163
+ if html.index("<")
164
+ tokenizer = HTML::Tokenizer.new(html)
165
+ new_text = ""
166
+
167
+ while token = tokenizer.next
168
+ node = HTML::Node.parse(nil, 0, 0, token, false)
169
+ new_text << case node
170
+ when HTML::Tag
171
+ if VERBOTEN_TAGS.include?(node.name)
172
+ node.to_s.gsub(/</, "&lt;")
173
+ else
174
+ if node.closing != :close
175
+ node.attributes.delete_if { |attr,v| attr =~ VERBOTEN_ATTRS }
176
+ if node.attributes["href"] =~ /^javascript:/i
177
+ node.attributes.delete "href"
178
+ end
179
+ end
180
+ node.to_s
181
+ end
182
+ else
183
+ node.to_s.gsub(/</, "&lt;")
184
+ end
185
+ end
186
+
187
+ html = new_text
188
+ end
189
+
190
+ html
191
+ end
192
+
131
193
 
132
194
  private
133
195
  # Returns a version of the text that's safe to use in a regular expression without triggering engine features.
@@ -136,13 +198,14 @@ module ActionView
136
198
  end
137
199
 
138
200
  # Turns all urls into clickable links.
139
- def auto_link_urls(text)
201
+ def auto_link_urls(text, href_options = {})
140
202
  text.gsub(/(<\w+.*?>|[^=!:'"\/]|^)((?:http[s]?:\/\/)|(?:www\.))([^\s<]+\/?)([[:punct:]]|\s|<|$)/) do
141
203
  all, a, b, c, d = $&, $1, $2, $3, $4
142
204
  if a =~ /<a\s/i # don't replace URL's that are already linked
143
205
  all
144
206
  else
145
- %(#{a}<a href="#{b=="www."?"http://www.":b}#{c}">#{b}#{c}</a>#{d})
207
+ extra_options = tag_options(href_options.stringify_keys) || ""
208
+ %(#{a}<a href="#{b=="www."?"http://www.":b}#{c}"#{extra_options}>#{b}#{c}</a>#{d})
146
209
  end
147
210
  end
148
211
  end
@@ -0,0 +1,433 @@
1
+ module ActionView
2
+ module Helpers
3
+ # == THIS IS AN EXPERIMENTAL FEATURE
4
+ #
5
+ # Which means that it doesn't yet work on all systems. We're still working on full
6
+ # compatibility. It's thus not advised to use this unless you've verified it to work
7
+ # fully on all the systems that is a part of your environment. Consider this an extended
8
+ # preview.
9
+ #
10
+ # Provides a set of methods to be used in your views to help with the
11
+ # rendering of Ajax enabled status updating during a multipart upload.
12
+ #
13
+ # The basic mechanism for upload progress is that the form will post to a
14
+ # hidden <iframe> element, then poll a status action that will replace the
15
+ # contents of a status container. Client Javascript hooks are provided for
16
+ # +begin+ and +finish+ of the update.
17
+ #
18
+ # If you wish to have a DTD that will validate this page, use XHTML
19
+ # Transitional because this DTD supports the <iframe> element.
20
+ #
21
+ # == Typical usage
22
+ #
23
+ # In your upload view:
24
+ #
25
+ # <%= form_tag_with_upload_progress({ :action => 'create' }) %>
26
+ # <%= file_field "document", "file" %>
27
+ # <%= submit_tag "Upload" %>
28
+ # <%= upload_status_tag %>
29
+ # <%= end_form_tag %>
30
+ #
31
+ # In your controller:
32
+ #
33
+ # class DocumentController < ApplicationController
34
+ # upload_status_for :create
35
+ #
36
+ # def create
37
+ # # ... Your document creation action
38
+ # end
39
+ # end
40
+ #
41
+ # == Javascript callback on begin and finished
42
+ #
43
+ # In your upload view:
44
+ #
45
+ # <%= form_tag_with_upload_progress({ :action => 'create' }, {
46
+ # :begin => "alert('upload beginning'),
47
+ # :finish => "alert('upload finished')}) %>
48
+ # <%= file_field "document", "file" %>
49
+ # <%= submit_tag "Upload" %>
50
+ # <%= upload_status_tag %>
51
+ # <%= end_form_tag %>
52
+ #
53
+ #
54
+ # == CSS Styling of the status text and progress bar
55
+ #
56
+ # See +upload_status_text_tag+ and +upload_status_progress_bar_tag+ for references
57
+ # of the IDs and CSS classes used.
58
+ #
59
+ # Default styling is included with the scaffolding CSS.
60
+ module UploadProgressHelper
61
+ unless const_defined? :FREQUENCY
62
+ # Default number of seconds between client updates
63
+ FREQUENCY = 2.0
64
+
65
+ # Factor to decrease the frequency when the +upload_status+ action returns the same results
66
+ # To disable update decay, set this constant to +false+
67
+ FREQUENCY_DECAY = 1.8
68
+ end
69
+
70
+ # Contains a hash of status messages used for localization of
71
+ # +upload_progress_status+ and +upload_progress_text+. Each string is
72
+ # evaluated in the helper method context so you can include your own
73
+ # calculations and string iterpolations.
74
+ #
75
+ # The following keys are defined:
76
+ #
77
+ # <tt>:begin</tt>:: Displayed before the first byte is received on the server
78
+ # <tt>:update</tt>:: Contains a human representation of the upload progress
79
+ # <tt>:finish</tt>:: Displayed when the file upload is complete, before the action has completed. If you are performing extra activity in your action such as processing of the upload, then inform the user of what you are doing by setting +upload_progress.message+
80
+ #
81
+ @@default_messages = {
82
+ :begin => '"Upload starting..."',
83
+ :update => '"#{human_size(upload_progress.received_bytes)} of #{human_size(upload_progress.total_bytes)} at #{human_size(upload_progress.bitrate)}/s; #{distance_of_time_in_words(0,upload_progress.remaining_seconds,true)} remaining"',
84
+ :finish => 'upload_progress.message.blank? ? "Upload finished." : upload_progress.message',
85
+ }
86
+
87
+
88
+ # Creates a form tag and hidden <iframe> necessary for the upload progress
89
+ # status messages to be displayed in a designated +div+ on your page.
90
+ #
91
+ # == Initializations
92
+ #
93
+ # When the upload starts, the content created by +upload_status_tag+ will be filled out with
94
+ # "Upload starting...". When the upload is finished, "Upload finished." will be used. Every
95
+ # update inbetween the begin and finish events will be determined by the server +upload_status+
96
+ # action. Doing this automatically means that the user can use the same form to upload multiple
97
+ # files without refreshing page while still displaying a reasonable progress.
98
+ #
99
+ # == Upload IDs
100
+ #
101
+ # For the view and the controller to know about the same upload they must share
102
+ # a common +upload_id+. +form_tag_with_upload_progress+ prepares the next available
103
+ # +upload_id+ when called. There are other methods which use the +upload_id+ so the
104
+ # order in which you include your content is important. Any content that depends on the
105
+ # +upload_id+ will use the one defined +form_tag_with_upload_progress+
106
+ # otherwise you will need to explicitly declare the +upload_id+ shared among
107
+ # your progress elements.
108
+ #
109
+ # Status container after the form:
110
+ #
111
+ # <%= form_tag_with_upload_progress %>
112
+ # <%= end_form_tag %>
113
+ #
114
+ # <%= upload_status_tag %>
115
+ #
116
+ # Status container before form:
117
+ #
118
+ # <% my_upload_id = next_upload_id %>
119
+ #
120
+ # <%= upload_status_tag %>
121
+ #
122
+ # <%= form_tag_with_upload_progress :upload_id => my_upload_id %>
123
+ # <%= end_form_tag %>
124
+ #
125
+ # It is recommended that the helpers manage the +upload_id+ parameter.
126
+ #
127
+ # == Options
128
+ #
129
+ # +form_tag_with_upload_progress+ uses similar options as +form_tag+
130
+ # yet accepts another hash for the options used for the +upload_status+ action.
131
+ #
132
+ # <tt>url_for_options</tt>:: The same options used by +form_tag+ including:
133
+ # <tt>:upload_id</tt>:: the upload id used to uniquely identify this upload
134
+ #
135
+ # <tt>options</tt>:: similar options to +form_tag+ including:
136
+ # <tt>:begin</tt>:: Javascript code that executes before the first status update occurs.
137
+ # <tt>:finish</tt>:: Javascript code that executes after the action that receives the post returns.
138
+ # <tt>:frequency</tt>:: number of seconds between polls to the upload status action.
139
+ #
140
+ # <tt>status_url_for_options</tt>:: options passed to +url_for+ to build the url
141
+ # for the upload status action.
142
+ # <tt>:controller</tt>:: defines the controller to be used for a custom update status action
143
+ # <tt>:action</tt>:: defines the action to be used for a custom update status action
144
+ #
145
+ # Parameters passed to the action defined by status_url_for_options
146
+ #
147
+ # <tt>:upload_id</tt>:: the upload_id automatically generated by +form_tag_with_upload_progress+ or the user defined id passed to this method.
148
+ #
149
+ def form_tag_with_upload_progress(url_for_options = {}, options = {}, status_url_for_options = {}, *parameters_for_url_method)
150
+
151
+ ## Setup the action URL and the server-side upload_status action for
152
+ ## polling of status during the upload
153
+
154
+ options = options.dup
155
+
156
+ upload_id = url_for_options.delete(:upload_id) || next_upload_id
157
+ upload_action_url = url_for(url_for_options)
158
+
159
+ if status_url_for_options.is_a? Hash
160
+ status_url_for_options = status_url_for_options.merge({
161
+ :action => 'upload_status',
162
+ :upload_id => upload_id})
163
+ end
164
+
165
+ status_url = url_for(status_url_for_options)
166
+
167
+ ## Prepare the form options. Dynamically change the target and URL to enable the status
168
+ ## updating only if javascript is enabled, otherwise perform the form submission in the same
169
+ ## frame.
170
+
171
+ upload_target = options[:target] || upload_target_id
172
+ upload_id_param = "#{upload_action_url.include?('?') ? '&' : '?'}upload_id=#{upload_id}"
173
+
174
+ ## Externally :begin and :finish are the entry and exit points
175
+ ## Internally, :finish is called :complete
176
+
177
+ js_options = {
178
+ :decay => options[:decay] || FREQUENCY_DECAY,
179
+ :frequency => options[:frequency] || FREQUENCY,
180
+ }
181
+
182
+ updater_options = '{' + js_options.map {|k, v| "#{k}:#{v}"}.sort.join(',') + '}'
183
+
184
+ ## Finish off the updating by forcing the progress bar to 100% and status text because the
185
+ ## results of the post may load and finish in the IFRAME before the last status update
186
+ ## is loaded.
187
+
188
+ options[:complete] = "$('#{status_tag_id}').innerHTML='#{escape_javascript upload_progress_text(:finish)}';"
189
+ options[:complete] << "#{upload_progress_update_bar_js(100)};"
190
+ options[:complete] << "#{upload_update_object} = null"
191
+ options[:complete] = "#{options[:complete]}; #{options[:finish]}" if options[:finish]
192
+
193
+ options[:script] = true
194
+
195
+ ## Prepare the periodic updater, clearing any previous updater
196
+
197
+ updater = "if (#{upload_update_object}) { #{upload_update_object}.stop(); }"
198
+ updater << "#{upload_update_object} = new Ajax.PeriodicalUpdater('#{status_tag_id}',"
199
+ updater << "'#{status_url}', #{options_for_ajax(options)}.extend(#{updater_options}))"
200
+
201
+ updater = "#{options[:begin]}; #{updater}" if options[:begin]
202
+ updater = "#{upload_progress_update_bar_js(0)}; #{updater}"
203
+ updater = "$('#{status_tag_id}').innerHTML='#{escape_javascript upload_progress_text(:begin)}'; #{updater}"
204
+
205
+ ## Touch up the form action and target to use the given target instead of the
206
+ ## default one. Then start the updater
207
+
208
+ options[:onsubmit] = "if (this.action.indexOf('upload_id') < 0){ this.action += '#{escape_javascript upload_id_param}'; }"
209
+ options[:onsubmit] << "this.target = '#{escape_javascript upload_target}';"
210
+ options[:onsubmit] << "#{updater}; return true"
211
+ options[:multipart] = true
212
+
213
+ [:begin, :finish, :complete, :frequency, :decay, :script].each { |sym| options.delete(sym) }
214
+
215
+ ## Create the tags
216
+ ## If a target for the form is given then avoid creating the hidden IFRAME
217
+
218
+ tag = form_tag(upload_action_url, options, *parameters_for_url_method)
219
+
220
+ unless options[:target]
221
+ tag << content_tag('iframe', '', {
222
+ :id => upload_target,
223
+ :name => upload_target,
224
+ :src => '',
225
+ :style => 'width:0px;height:0px;border:0'
226
+ })
227
+ end
228
+
229
+ tag
230
+ end
231
+
232
+ # This method must be called by the action that receives the form post
233
+ # with the +upload_progress+. By default this method is rendered when
234
+ # the controller declares that the action is the receiver of a
235
+ # +form_tag_with_upload_progress+ posting.
236
+ #
237
+ # This template will do a javascript redirect to the URL specified in +redirect_to+
238
+ # if this method is called anywhere in the controller action. When the action
239
+ # performs a redirect, the +finish+ handler will not be called.
240
+ #
241
+ # If there are errors in the action then you should set the controller
242
+ # instance variable +@errors+. The +@errors+ object will be
243
+ # converted to a javascript array from +@errors.full_messages+ and
244
+ # passed to the +finish+ handler of +form_tag_with_upload_progress+
245
+ #
246
+ # If no errors have occured, the parameter to the +finish+ handler will
247
+ # be +undefined+.
248
+ #
249
+ # == Example (in view)
250
+ #
251
+ # <script>
252
+ # function do_finish(errors) {
253
+ # if (errors) {
254
+ # alert(errors);
255
+ # }
256
+ # }
257
+ # </script>
258
+ #
259
+ # <%= form_tag_with_upload_progress {:action => 'create'}, {finish => 'do_finish(arguments[0])'} %>
260
+ #
261
+ def finish_upload_status(options = {})
262
+ # Always trigger the stop/finish callback
263
+ js = "parent.#{upload_update_object}.stop(#{options[:client_js_argument]});\n"
264
+
265
+ # Redirect if redirect_to was called in controller
266
+ js << "parent.location.replace('#{escape_javascript options[:redirect_to]}');\n" unless options[:redirect_to].blank?
267
+
268
+ # Guard against multiple triggers/redirects on back
269
+ js = "if (parent.#{upload_update_object}) { #{js} }\n"
270
+
271
+ content_tag("html",
272
+ content_tag("head",
273
+ content_tag("script", "function finish() { #{js} }",
274
+ {:type => "text/javascript", :language => "javascript"})) +
275
+ content_tag("body", '', :onload => 'finish()'))
276
+ end
277
+
278
+ # Renders the HTML to contain the upload progress bar above the
279
+ # default messages
280
+ #
281
+ # Use this method to display the upload status after your +form_tag_with_upload_progress+
282
+ def upload_status_tag(content='', options={})
283
+ upload_status_progress_bar_tag + upload_status_text_tag(content, options)
284
+ end
285
+
286
+ # Content helper that will create a +div+ with the proper ID and class that
287
+ # will contain the contents returned by the +upload_status+ action. The container
288
+ # is defined as
289
+ #
290
+ # <div id="#{status_tag_id}" class="uploadStatus"> </div>
291
+ #
292
+ # Style this container by selecting the +.uploadStatus+ +CSS+ class.
293
+ #
294
+ # The +content+ parameter will be included in the inner most +div+ when
295
+ # rendered.
296
+ #
297
+ # The +options+ will create attributes on the outer most div. To use a different
298
+ # +CSS+ class, pass a different class option.
299
+ #
300
+ # Example +CSS+:
301
+ # .uploadStatus { font-size: 10px; color: grey; }
302
+ #
303
+ def upload_status_text_tag(content=nil, options={})
304
+ content_tag("div", content, {:id => status_tag_id, :class => 'uploadStatus'}.merge(options))
305
+ end
306
+
307
+ # Content helper that will create the element tree that can be easily styled
308
+ # with +CSS+ to create a progress bar effect. The containers are defined as:
309
+ #
310
+ # <div class="progressBar" id="#{progress_bar_id}">
311
+ # <div class="border">
312
+ # <div class="background">
313
+ # <div class="content"> </div>
314
+ # </div>
315
+ # </div>
316
+ # </div>
317
+ #
318
+ # The +content+ parameter will be included in the inner most +div+ when
319
+ # rendered.
320
+ #
321
+ # The +options+ will create attributes on the outer most div. To use a different
322
+ # +CSS+ class, pass a different class option.
323
+ #
324
+ # Example:
325
+ # <%= upload_status_progress_bar_tag('', {:class => 'progress'}) %>
326
+ #
327
+ # Example +CSS+:
328
+ #
329
+ # div.progressBar {
330
+ # margin: 5px;
331
+ # }
332
+ #
333
+ # div.progressBar div.border {
334
+ # background-color: #fff;
335
+ # border: 1px solid grey;
336
+ # width: 100%;
337
+ # }
338
+ #
339
+ # div.progressBar div.background {
340
+ # background-color: #333;
341
+ # height: 18px;
342
+ # width: 0%;
343
+ # }
344
+ #
345
+ def upload_status_progress_bar_tag(content='', options={})
346
+ css = [options[:class], 'progressBar'].compact.join(' ')
347
+
348
+ content_tag("div",
349
+ content_tag("div",
350
+ content_tag("div",
351
+ content_tag("div", content, :class => 'foreground'),
352
+ :class => 'background'),
353
+ :class => 'border'),
354
+ {:id => progress_bar_id}.merge(options).merge({:class => css}))
355
+ end
356
+
357
+ # The text and Javascript returned by the default +upload_status+ controller
358
+ # action which will replace the contents of the div created by +upload_status_text_tag+
359
+ # and grow the progress bar background to the appropriate width.
360
+ #
361
+ # See +upload_progress_text+ and +upload_progress_update_bar_js+
362
+ def upload_progress_status
363
+ "#{upload_progress_text}<script>#{upload_progress_update_bar_js}</script>"
364
+ end
365
+
366
+ # Javascript helper that will create a script that will change the width
367
+ # of the background progress bar container. Include this in the script
368
+ # portion of your view rendered by your +upload_status+ action to
369
+ # automatically find and update the progress bar.
370
+ #
371
+ # Example (in controller):
372
+ #
373
+ # def upload_status
374
+ # render :inline => "<script><%= update_upload_progress_bar_js %></script>", :layout => false
375
+ # end
376
+ #
377
+ #
378
+ def upload_progress_update_bar_js(percent=nil)
379
+ progress = upload_progress
380
+ percent ||= case
381
+ when progress.nil? || !progress.started? then 0
382
+ when progress.finished? then 100
383
+ else progress.completed_percent
384
+ end.to_i
385
+
386
+ # TODO do animation instead of jumping
387
+ "$('#{progress_bar_id}').firstChild.firstChild.style.width='#{percent}%'"
388
+ end
389
+
390
+ # Generates a nicely formatted string of current upload progress for
391
+ # +UploadProgress::Progress+ object +progress+. Addtionally, it
392
+ # will return "Upload starting..." if progress has not been initialized,
393
+ # "Receiving data..." if there is no received data yet, and "Upload
394
+ # finished" when all data has been sent.
395
+ #
396
+ # You can overload this method to add you own output to the
397
+ #
398
+ # Example return: 223.5 KB of 1.5 MB at 321.2 KB/s; less than 10 seconds
399
+ # remaining
400
+ def upload_progress_text(state=nil)
401
+ eval case
402
+ when state then @@default_messages[state.to_sym]
403
+ when upload_progress.nil? || !upload_progress.started? then @@default_messages[:begin]
404
+ when upload_progress.finished? then @@default_messages[:finish]
405
+ else @@default_messages[:update]
406
+ end
407
+ end
408
+
409
+ protected
410
+ # Javascript object used to contain the polling methods and keep track of
411
+ # the finished state
412
+ def upload_update_object
413
+ "document.uploadStatus#{current_upload_id}"
414
+ end
415
+
416
+ # Element ID of the progress bar
417
+ def progress_bar_id
418
+ "UploadProgressBar#{current_upload_id}"
419
+ end
420
+
421
+ # Element ID of the progress status container
422
+ def status_tag_id
423
+ "UploadStatus#{current_upload_id}"
424
+ end
425
+
426
+ # Element ID of the target <iframe> used as the target of the form
427
+ def upload_target_id
428
+ "UploadTarget#{current_upload_id}"
429
+ end
430
+
431
+ end
432
+ end
433
+ end