date_helper.rb 48.9 KB
Newer Older
J
José Valim 已提交
1
require 'date'
2
require 'action_view/helpers/tag_helper'
3
require 'active_support/core_ext/date/conversions'
4
require 'active_support/core_ext/hash/slice'
J
José Valim 已提交
5
require 'active_support/core_ext/object/with_options'
D
Initial  
David Heinemeier Hansson 已提交
6 7 8

module ActionView
  module Helpers
9 10
    # = Action View Date Helpers
    #
11 12
    # The Date Helper primarily creates select/option tags for different kinds of dates and date elements. All of the
    # select-type methods share a number of common options that are as follows:
D
Initial  
David Heinemeier Hansson 已提交
13
    #
14 15
    # * <tt>:prefix</tt> - overwrites the default prefix of "date" used for the select names. So specifying "birthday"
    # would give birthday[month] instead of date[month] if passed to the select_month method.
D
Initial  
David Heinemeier Hansson 已提交
16
    # * <tt>:include_blank</tt> - set to true if it should be possible to set an empty date.
17 18 19
    # * <tt>:discard_type</tt> - set to true if you want to discard the type part of the select name. If set to true,
    #   the select_month method would use simply "date" (which can be overwritten using <tt>:prefix</tt>) instead of
    #   "date[month]".
D
Initial  
David Heinemeier Hansson 已提交
20
    module DateHelper
21 22
      # Reports the approximate distance in time between two Time or Date objects or integers as seconds.
      # Set <tt>include_seconds</tt> to true if you want more detailed approximations when distance < 1 min, 29 secs
23
      # Distances are reported based on the following table:
24
      #
25 26 27 28 29 30 31 32 33
      #   0 <-> 29 secs                                                             # => less than a minute
      #   30 secs <-> 1 min, 29 secs                                                # => 1 minute
      #   1 min, 30 secs <-> 44 mins, 29 secs                                       # => [2..44] minutes
      #   44 mins, 30 secs <-> 89 mins, 29 secs                                     # => about 1 hour
      #   89 mins, 29 secs <-> 23 hrs, 59 mins, 29 secs                             # => about [2..24] hours
      #   23 hrs, 59 mins, 29 secs <-> 47 hrs, 59 mins, 29 secs                     # => 1 day
      #   47 hrs, 59 mins, 29 secs <-> 29 days, 23 hrs, 59 mins, 29 secs            # => [2..29] days
      #   29 days, 23 hrs, 59 mins, 30 secs <-> 59 days, 23 hrs, 59 mins, 29 secs   # => about 1 month
      #   59 days, 23 hrs, 59 mins, 30 secs <-> 1 yr minus 1 sec                    # => [2..12] months
34 35 36 37
      #   1 yr <-> 1 yr, 3 months                                                   # => about 1 year
      #   1 yr, 3 months <-> 1 yr, 9 months                                         # => over 1 year
      #   1 yr, 9 months <-> 2 yr minus 1 sec                                       # => almost 2 years
      #   2 yrs <-> max time or date                                                # => (same rules as 1 yr)
38
      #
39 40 41 42 43 44 45
      # With <tt>include_seconds</tt> = true and the difference < 1 minute 29 seconds:
      #   0-4   secs      # => less than 5 seconds
      #   5-9   secs      # => less than 10 seconds
      #   10-19 secs      # => less than 20 seconds
      #   20-39 secs      # => half a minute
      #   40-59 secs      # => less than a minute
      #   60-89 secs      # => 1 minute
46
      #
47
      # ==== Examples
48
      #   from_time = Time.now
49 50 51 52
      #   distance_of_time_in_words(from_time, from_time + 50.minutes)        # => about 1 hour
      #   distance_of_time_in_words(from_time, 50.minutes.from_now)           # => about 1 hour
      #   distance_of_time_in_words(from_time, from_time + 15.seconds)        # => less than a minute
      #   distance_of_time_in_words(from_time, from_time + 15.seconds, true)  # => less than 20 seconds
53
      #   distance_of_time_in_words(from_time, 3.years.from_now)              # => about 3 years
54 55 56 57
      #   distance_of_time_in_words(from_time, from_time + 60.hours)          # => about 3 days
      #   distance_of_time_in_words(from_time, from_time + 45.seconds, true)  # => less than a minute
      #   distance_of_time_in_words(from_time, from_time - 45.seconds, true)  # => less than a minute
      #   distance_of_time_in_words(from_time, 76.seconds.from_now)           # => 1 minute
J
Jeremy Kemper 已提交
58
      #   distance_of_time_in_words(from_time, from_time + 1.year + 3.days)   # => about 1 year
59 60
      #   distance_of_time_in_words(from_time, from_time + 3.years + 6.months) # => over 3 years
      #   distance_of_time_in_words(from_time, from_time + 4.years + 9.days + 30.minutes + 5.seconds) # => about 4 years
61 62
      #
      #   to_time = Time.now + 6.years + 19.days
63 64
      #   distance_of_time_in_words(from_time, to_time, true)     # => about 6 years
      #   distance_of_time_in_words(to_time, from_time, true)     # => about 6 years
65
      #   distance_of_time_in_words(Time.now, Time.now)           # => less than a minute
66
      #
S
Sven Fuchs 已提交
67
      def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false, options = {})
68 69 70 71
        from_time = from_time.to_time if from_time.respond_to?(:to_time)
        to_time = to_time.to_time if to_time.respond_to?(:to_time)
        distance_in_minutes = (((to_time - from_time).abs)/60).round
        distance_in_seconds = ((to_time - from_time).abs).round
72

73
        I18n.with_options :locale => options[:locale], :scope => :'datetime.distance_in_words' do |locale|
S
Sven Fuchs 已提交
74 75
          case distance_in_minutes
            when 0..1
76
              return distance_in_minutes == 0 ?
S
Sven Fuchs 已提交
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
                     locale.t(:less_than_x_minutes, :count => 1) :
                     locale.t(:x_minutes, :count => distance_in_minutes) unless include_seconds

              case distance_in_seconds
                when 0..4   then locale.t :less_than_x_seconds, :count => 5
                when 5..9   then locale.t :less_than_x_seconds, :count => 10
                when 10..19 then locale.t :less_than_x_seconds, :count => 20
                when 20..39 then locale.t :half_a_minute
                when 40..59 then locale.t :less_than_x_minutes, :count => 1
                else             locale.t :x_minutes,           :count => 1
              end

            when 2..44           then locale.t :x_minutes,      :count => distance_in_minutes
            when 45..89          then locale.t :about_x_hours,  :count => 1
            when 90..1439        then locale.t :about_x_hours,  :count => (distance_in_minutes.to_f / 60.0).round
92 93
            when 1440..2529      then locale.t :x_days,         :count => 1
            when 2530..43199     then locale.t :x_days,         :count => (distance_in_minutes.to_f / 1440.0).round
S
Sven Fuchs 已提交
94
            when 43200..86399    then locale.t :about_x_months, :count => 1
95
            when 86400..525599   then locale.t :x_months,       :count => (distance_in_minutes.to_f / 43200.0).round
96
            else
97 98 99 100 101 102 103 104 105 106
              distance_in_years           = distance_in_minutes / 525600
              minute_offset_for_leap_year = (distance_in_years / 4) * 1440
              remainder                   = ((distance_in_minutes - minute_offset_for_leap_year) % 525600)
              if remainder < 131400
                locale.t(:about_x_years,  :count => distance_in_years)
              elsif remainder < 394200
                locale.t(:over_x_years,   :count => distance_in_years)
              else
                locale.t(:almost_x_years, :count => distance_in_years + 1)
              end
S
Sven Fuchs 已提交
107
          end
D
Initial  
David Heinemeier Hansson 已提交
108
        end
109
      end
110

D
Initial  
David Heinemeier Hansson 已提交
111
      # Like distance_of_time_in_words, but where <tt>to_time</tt> is fixed to <tt>Time.now</tt>.
112 113 114 115 116 117 118
      #
      # ==== Examples
      #   time_ago_in_words(3.minutes.from_now)       # => 3 minutes
      #   time_ago_in_words(Time.now - 15.hours)      # => 15 hours
      #   time_ago_in_words(Time.now)                 # => less than a minute
      #
      #   from_time = Time.now - 3.days - 14.minutes - 25.seconds     # => 3 days
119
      def time_ago_in_words(from_time, include_seconds = false)
120
        distance_of_time_in_words(from_time, Time.now, include_seconds)
D
Initial  
David Heinemeier Hansson 已提交
121
      end
122

123
      alias_method :distance_of_time_in_words_to_now, :time_ago_in_words
D
Initial  
David Heinemeier Hansson 已提交
124

125
      # Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based
L
lifo 已提交
126 127
      # attribute (identified by +method+) on an object assigned to the template (identified by +object+).
      #
128 129 130
      #
      # ==== Options
      # * <tt>:use_month_numbers</tt> - Set to true if you want to use month numbers rather than month names (e.g.
P
Pratik Naik 已提交
131
      #   "2" instead of "February").
P
Pratik Naik 已提交
132 133 134
      # * <tt>:use_short_month</tt>   - Set to true if you want to use abbreviated month names instead of full
      #   month names (e.g. "Feb" instead of "February").
      # * <tt>:add_month_numbers</tt>  - Set to true if you want to use both month numbers and month names (e.g.
135 136
      #   "2 - February" instead of "February").
      # * <tt>:use_month_names</tt>   - Set to an array with 12 month names if you want to customize month names.
P
Pratik Naik 已提交
137
      #   Note: You can also use Rails' i18n functionality for this.
138 139 140 141 142 143 144 145 146 147
      # * <tt>:date_separator</tt>    - Specifies a string to separate the date fields. Default is "" (i.e. nothing).
      # * <tt>:start_year</tt>        - Set the start year for the year select. Default is <tt>Time.now.year - 5</tt>.
      # * <tt>:end_year</tt>          - Set the end year for the year select. Default is <tt>Time.now.year + 5</tt>.
      # * <tt>:discard_day</tt>       - Set to true if you don't want to show a day select. This includes the day
      #   as a hidden field instead of showing a select field. Also note that this implicitly sets the day to be the
      #   first of the given month in order to not create invalid dates like 31 February.
      # * <tt>:discard_month</tt>     - Set to true if you don't want to show a month select. This includes the month
      #   as a hidden field instead of showing a select field. Also note that this implicitly sets :discard_day to true.
      # * <tt>:discard_year</tt>      - Set to true if you don't want to show a year select. This includes the year
      #   as a hidden field instead of showing a select field.
P
Pratik Naik 已提交
148
      # * <tt>:order</tt>             - Set to an array containing <tt>:day</tt>, <tt>:month</tt> and <tt>:year</tt> to
149 150
      #   customize the order in which the select fields are shown. If you leave out any of the symbols, the respective
      #   select will not be shown (like when you set <tt>:discard_xxx => true</tt>. Defaults to the order defined in
151
      #   the respective locale (e.g. [:year, :month, :day] in the en locale that ships with Rails).
152 153 154 155
      # * <tt>:include_blank</tt>     - Include a blank option in every select field so it's possible to set empty
      #   dates.
      # * <tt>:default</tt>           - Set a default date if the affected date isn't set or is nil.
      # * <tt>:disabled</tt>          - Set to true if you want show the select fields as disabled.
156 157 158 159
      # * <tt>:prompt</tt>            - Set to true (for a generic prompt), a prompt string or a hash of prompt strings
      #   for <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>, <tt>:minute</tt> and <tt>:second</tt>.
      #   Setting this option prepends a select option with a generic prompt  (Day, Month, Year, Hour, Minute, Seconds)
      #   or the given prompt string.
160
      #
161
      # If anything is passed in the +html_options+ hash it will be applied to every select tag in the set.
162
      #
163
      # NOTE: Discarded selects will default to 1. So if no month select is available, January will be assumed.
D
Initial  
David Heinemeier Hansson 已提交
164
      #
165 166
      # ==== Examples
      #   # Generates a date select that when POSTed is stored in the post variable, in the written_on attribute
D
Initial  
David Heinemeier Hansson 已提交
167
      #   date_select("post", "written_on")
168 169 170
      #
      #   # Generates a date select that when POSTed is stored in the post variable, in the written_on attribute,
      #   # with the year in the year drop down box starting at 1995.
D
Initial  
David Heinemeier Hansson 已提交
171
      #   date_select("post", "written_on", :start_year => 1995)
172 173 174
      #
      #   # Generates a date select that when POSTed is stored in the post variable, in the written_on attribute,
      #   # with the year in the year drop down box starting at 1995, numbers used for months instead of words,
175
      #   # and without a day select box.
176
      #   date_select("post", "written_on", :start_year => 1995, :use_month_numbers => true,
D
Initial  
David Heinemeier Hansson 已提交
177
      #                                     :discard_day => true, :include_blank => true)
178 179 180
      #
      #   # Generates a date select that when POSTed is stored in the post variable, in the written_on attribute
      #   # with the fields ordered as day, month, year rather than month, day, year.
181
      #   date_select("post", "written_on", :order => [:day, :month, :year])
D
Initial  
David Heinemeier Hansson 已提交
182
      #
183 184 185 186 187 188
      #   # Generates a date select that when POSTed is stored in the user variable, in the birthday attribute
      #   # lacking a year field.
      #   date_select("user", "birthday", :order => [:month, :day])
      #
      #   # Generates a date select that when POSTed is stored in the user variable, in the birthday attribute
      #   # which is initially set to the date 3 days from the current date
189
      #   date_select("post", "written_on", :default => 3.days.from_now)
190 191 192
      #
      #   # Generates a date select that when POSTed is stored in the credit_card variable, in the bill_due attribute
      #   # that will have a default day of 20.
193 194
      #   date_select("credit_card", "bill_due", :default => { :day => 20 })
      #
195 196 197
      #   # Generates a date select with custom prompts
      #   date_select("post", "written_on", :prompt => { :day => 'Select day', :month => 'Select month', :year => 'Select year' })
      #
D
Initial  
David Heinemeier Hansson 已提交
198
      # The selects are prepared for multi-parameter assignment to an Active Record object.
199
      #
200 201
      # Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that
      # all month choices are valid.
202
      def date_select(object_name, method, options = {}, html_options = {})
203
        InstanceTag.new(object_name, method, self, options.delete(:object)).to_date_select_tag(options, html_options)
D
Initial  
David Heinemeier Hansson 已提交
204 205
      end

206 207 208
      # Returns a set of select tags (one for hour, minute and optionally second) pre-selected for accessing a
      # specified time-based attribute (identified by +method+) on an object assigned to the template (identified by
      # +object+). You can include the seconds with <tt>:include_seconds</tt>.
209 210 211 212
      #
      # This method will also generate 3 input hidden tags, for the actual year, month and day unless the option
      # <tt>:ignore_date</tt> is set to +true+.
      #
213 214
      # If anything is passed in the html_options hash it will be applied to every select tag in the set.
      #
215 216
      # ==== Examples
      #   # Creates a time select tag that, when POSTed, will be stored in the post variable in the sunrise attribute
217
      #   time_select("post", "sunrise")
218
      #
219 220
      #   # Creates a time select tag with a seconds field that, when POSTed, will be stored in the post variables in
      #   # the sunrise attribute.
221 222
      #   time_select("post", "start_time", :include_seconds => true)
      #
223 224 225
      #   # You can set the :minute_step to 15 which will give you: 00, 15, 30 and 45.
      #   time_select 'game', 'game_time', {:minute_step => 15}
      #
226 227 228 229 230
      #   # Creates a time select tag with a custom prompt. Use :prompt => true for generic prompts.
      #   time_select("post", "written_on", :prompt => {:hour => 'Choose hour', :minute => 'Choose minute', :second => 'Choose seconds'})
      #   time_select("post", "written_on", :prompt => {:hour => true}) # generic prompt for hours
      #   time_select("post", "written_on", :prompt => true) # generic prompts for all
      #
231
      # The selects are prepared for multi-parameter assignment to an Active Record object.
232
      #
233 234
      # Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that
      # all month choices are valid.
235
      def time_select(object_name, method, options = {}, html_options = {})
236
        InstanceTag.new(object_name, method, self, options.delete(:object)).to_time_select_tag(options, html_options)
237 238
      end

239 240
      # Returns a set of select tags (one for year, month, day, hour, and minute) pre-selected for accessing a
      # specified datetime-based attribute (identified by +method+) on an object assigned to the template (identified
L
lifo 已提交
241
      # by +object+).
D
Initial  
David Heinemeier Hansson 已提交
242
      #
243 244
      # If anything is passed in the html_options hash it will be applied to every select tag in the set.
      #
245
      # ==== Examples
246 247
      #   # Generates a datetime select that, when POSTed, will be stored in the post variable in the written_on
      #   # attribute
D
Initial  
David Heinemeier Hansson 已提交
248
      #   datetime_select("post", "written_on")
249
      #
250
      #   # Generates a datetime select with a year select that starts at 1995 that, when POSTed, will be stored in the
251
      #   # post variable in the written_on attribute.
D
Initial  
David Heinemeier Hansson 已提交
252 253
      #   datetime_select("post", "written_on", :start_year => 1995)
      #
254 255
      #   # Generates a datetime select with a default value of 3 days from the current time that, when POSTed, will
      #   # be stored in the trip variable in the departing attribute.
256 257
      #   datetime_select("trip", "departing", :default => 3.days.from_now)
      #
258 259
      #   # Generates a datetime select that discards the type that, when POSTed, will be stored in the post variable
      #   # as the written_on attribute.
260 261
      #   datetime_select("post", "written_on", :discard_type => true)
      #
262 263 264 265 266
      #   # Generates a datetime select with a custom prompt. Use :prompt=>true for generic prompts.
      #   datetime_select("post", "written_on", :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year'})
      #   datetime_select("post", "written_on", :prompt => {:hour => true}) # generic prompt for hours
      #   datetime_select("post", "written_on", :prompt => true) # generic prompts for all
      #
D
Initial  
David Heinemeier Hansson 已提交
267
      # The selects are prepared for multi-parameter assignment to an Active Record object.
268
      def datetime_select(object_name, method, options = {}, html_options = {})
269
        InstanceTag.new(object_name, method, self, options.delete(:object)).to_datetime_select_tag(options, html_options)
D
Initial  
David Heinemeier Hansson 已提交
270 271
      end

272 273 274 275 276 277
      # Returns a set of html select-tags (one for year, month, day, hour, and minute) pre-selected with the
      # +datetime+. It's also possible to explicitly set the order of the tags using the <tt>:order</tt> option with
      # an array of symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not
      # supply a Symbol, it will be appended onto the <tt>:order</tt> passed in. You can also add
      # <tt>:date_separator</tt>, <tt>:datetime_separator</tt> and <tt>:time_separator</tt> keys to the +options+ to
      # control visual display of the elements.
278
      #
279 280
      # If anything is passed in the html_options hash it will be applied to every select tag in the set.
      #
281 282 283 284 285 286 287 288 289 290
      # ==== Examples
      #   my_date_time = Time.now + 4.days
      #
      #   # Generates a datetime select that defaults to the datetime in my_date_time (four days after today)
      #   select_datetime(my_date_time)
      #
      #   # Generates a datetime select that defaults to today (no specified datetime)
      #   select_datetime()
      #
      #   # Generates a datetime select that defaults to the datetime in my_date_time (four days after today)
291
      #   # with the fields ordered year, month, day rather than month, day, year.
292 293 294 295 296 297
      #   select_datetime(my_date_time, :order => [:year, :month, :day])
      #
      #   # Generates a datetime select that defaults to the datetime in my_date_time (four days after today)
      #   # with a '/' between each date field.
      #   select_datetime(my_date_time, :date_separator => '/')
      #
298 299 300 301 302 303
      #   # Generates a datetime select that defaults to the datetime in my_date_time (four days after today)
      #   # with a date fields separated by '/', time fields separated by '' and the date and time fields
      #   # separated by a comma (',').
      #   select_datetime(my_date_time, :date_separator => '/', :time_separator => '', :datetime_separator => ',')
      #
      #   # Generates a datetime select that discards the type of the field and defaults to the datetime in
304 305 306 307 308 309 310
      #   # my_date_time (four days after today)
      #   select_datetime(my_date_time, :discard_type => true)
      #
      #   # Generates a datetime select that defaults to the datetime in my_date_time (four days after today)
      #   # prefixed with 'payday' rather than 'date'
      #   select_datetime(my_date_time, :prefix => 'payday')
      #
311 312 313 314 315
      #   # Generates a datetime select with a custom prompt. Use :prompt=>true for generic prompts.
      #   select_datetime(my_date_time, :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year'})
      #   select_datetime(my_date_time, :prompt => {:hour => true}) # generic prompt for hours
      #   select_datetime(my_date_time, :prompt => true) # generic prompts for all
      #
316
      def select_datetime(datetime = Time.current, options = {}, html_options = {})
317
        DateTimeSelector.new(datetime, options, html_options).select_datetime
318
      end
319

D
Initial  
David Heinemeier Hansson 已提交
320
      # Returns a set of html select-tags (one for year, month, and day) pre-selected with the +date+.
321
      # It's possible to explicitly set the order of the tags using the <tt>:order</tt> option with an array of
322 323
      # symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not supply a Symbol,
      # it will be appended onto the <tt>:order</tt> passed in.
324
      #
325 326
      # If anything is passed in the html_options hash it will be applied to every select tag in the set.
      #
327 328 329 330 331 332 333 334 335 336
      # ==== Examples
      #   my_date = Time.today + 6.days
      #
      #   # Generates a date select that defaults to the date in my_date (six days after today)
      #   select_date(my_date)
      #
      #   # Generates a date select that defaults to today (no specified date)
      #   select_date()
      #
      #   # Generates a date select that defaults to the date in my_date (six days after today)
337
      #   # with the fields ordered year, month, day rather than month, day, year.
338 339
      #   select_date(my_date, :order => [:year, :month, :day])
      #
340
      #   # Generates a date select that discards the type of the field and defaults to the date in
341
      #   # my_date (six days after today)
P
Pratik Naik 已提交
342
      #   select_date(my_date, :discard_type => true)
343
      #
344 345 346 347
      #   # Generates a date select that defaults to the date in my_date,
      #   # which has fields separated by '/'
      #   select_date(my_date, :date_separator => '/')
      #
348 349
      #   # Generates a date select that defaults to the datetime in my_date (six days after today)
      #   # prefixed with 'payday' rather than 'date'
P
Pratik Naik 已提交
350
      #   select_date(my_date, :prefix => 'payday')
351
      #
352 353 354 355 356
      #   # Generates a date select with a custom prompt. Use :prompt=>true for generic prompts.
      #   select_date(my_date, :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year'})
      #   select_date(my_date, :prompt => {:hour => true}) # generic prompt for hours
      #   select_date(my_date, :prompt => true) # generic prompts for all
      #
357
      def select_date(date = Date.current, options = {}, html_options = {})
358
        DateTimeSelector.new(date, options, html_options).select_date
D
Initial  
David Heinemeier Hansson 已提交
359 360
      end

361
      # Returns a set of html select-tags (one for hour and minute)
362
      # You can set <tt>:time_separator</tt> key to format the output, and
363 364
      # the <tt>:include_seconds</tt> option to include an input for seconds.
      #
365 366
      # If anything is passed in the html_options hash it will be applied to every select tag in the set.
      #
367 368 369 370 371 372 373 374 375 376
      # ==== Examples
      #   my_time = Time.now + 5.days + 7.hours + 3.minutes + 14.seconds
      #
      #   # Generates a time select that defaults to the time in my_time
      #   select_time(my_time)
      #
      #   # Generates a time select that defaults to the current time (no specified time)
      #   select_time()
      #
      #   # Generates a time select that defaults to the time in my_time,
377
      #   # which has fields separated by ':'
378 379 380 381 382 383 384 385 386 387
      #   select_time(my_time, :time_separator => ':')
      #
      #   # Generates a time select that defaults to the time in my_time,
      #   # that also includes an input for seconds
      #   select_time(my_time, :include_seconds => true)
      #
      #   # Generates a time select that defaults to the time in my_time, that has fields
      #   # separated by ':' and includes an input for seconds
      #   select_time(my_time, :time_separator => ':', :include_seconds => true)
      #
388 389 390 391 392
      #   # Generates a time select with a custom prompt. Use :prompt=>true for generic prompts.
      #   select_time(my_time, :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year'})
      #   select_time(my_time, :prompt => {:hour => true}) # generic prompt for hours
      #   select_time(my_time, :prompt => true) # generic prompts for all
      #
393
      def select_time(datetime = Time.current, options = {}, html_options = {})
394
        DateTimeSelector.new(datetime, options, html_options).select_time
395 396 397 398
      end

      # Returns a select tag with options for each of the seconds 0 through 59 with the current second selected.
      # The <tt>second</tt> can also be substituted for a second number.
399
      # Override the field name using the <tt>:field_name</tt> option, 'second' by default.
400 401 402 403 404 405 406 407 408
      #
      # ==== Examples
      #   my_time = Time.now + 16.minutes
      #
      #   # Generates a select field for seconds that defaults to the seconds for the time in my_time
      #   select_second(my_time)
      #
      #   # Generates a select field for seconds that defaults to the number given
      #   select_second(33)
409
      #
410 411 412 413
      #   # Generates a select field for seconds that defaults to the seconds for the time in my_time
      #   # that is named 'interval' rather than 'second'
      #   select_second(my_time, :field_name => 'interval')
      #
414 415 416 417
      #   # Generates a select field for seconds with a custom prompt.  Use :prompt=>true for a
      #   # generic prompt.
      #   select_minute(14, :prompt => 'Choose seconds')
      #
418
      def select_second(datetime, options = {}, html_options = {})
419
        DateTimeSelector.new(datetime, options, html_options).select_second
420 421
      end

D
Initial  
David Heinemeier Hansson 已提交
422
      # Returns a select tag with options for each of the minutes 0 through 59 with the current minute selected.
423 424
      # Also can return a select tag with options by <tt>minute_step</tt> from 0 through 59 with the 00 minute
      # selected. The <tt>minute</tt> can also be substituted for a minute number.
425
      # Override the field name using the <tt>:field_name</tt> option, 'minute' by default.
426 427 428 429 430 431 432 433 434
      #
      # ==== Examples
      #   my_time = Time.now + 6.hours
      #
      #   # Generates a select field for minutes that defaults to the minutes for the time in my_time
      #   select_minute(my_time)
      #
      #   # Generates a select field for minutes that defaults to the number given
      #   select_minute(14)
435
      #
436 437 438 439
      #   # Generates a select field for minutes that defaults to the minutes for the time in my_time
      #   # that is named 'stride' rather than 'second'
      #   select_minute(my_time, :field_name => 'stride')
      #
440 441 442 443
      #   # Generates a select field for minutes with a custom prompt.  Use :prompt=>true for a
      #   # generic prompt.
      #   select_minute(14, :prompt => 'Choose minutes')
      #
444
      def select_minute(datetime, options = {}, html_options = {})
445
        DateTimeSelector.new(datetime, options, html_options).select_minute
D
Initial  
David Heinemeier Hansson 已提交
446 447 448 449
      end

      # Returns a select tag with options for each of the hours 0 through 23 with the current hour selected.
      # The <tt>hour</tt> can also be substituted for a hour number.
450
      # Override the field name using the <tt>:field_name</tt> option, 'hour' by default.
451 452 453 454
      #
      # ==== Examples
      #   my_time = Time.now + 6.hours
      #
455 456
      #   # Generates a select field for hours that defaults to the hour for the time in my_time
      #   select_hour(my_time)
457
      #
458 459
      #   # Generates a select field for hours that defaults to the number given
      #   select_hour(13)
460
      #
461
      #   # Generates a select field for hours that defaults to the minutes for the time in my_time
462
      #   # that is named 'stride' rather than 'second'
463
      #   select_hour(my_time, :field_name => 'stride')
464
      #
465 466 467 468
      #   # Generates a select field for hours with a custom prompt.  Use :prompt => true for a
      #   # generic prompt.
      #   select_hour(13, :prompt =>'Choose hour')
      #
469
      def select_hour(datetime, options = {}, html_options = {})
470
        DateTimeSelector.new(datetime, options, html_options).select_hour
D
Initial  
David Heinemeier Hansson 已提交
471 472 473 474
      end

      # Returns a select tag with options for each of the days 1 through 31 with the current day selected.
      # The <tt>date</tt> can also be substituted for a hour number.
475
      # Override the field name using the <tt>:field_name</tt> option, 'day' by default.
476 477 478 479 480 481 482 483 484
      #
      # ==== Examples
      #   my_date = Time.today + 2.days
      #
      #   # Generates a select field for days that defaults to the day for the date in my_date
      #   select_day(my_time)
      #
      #   # Generates a select field for days that defaults to the number given
      #   select_day(5)
485
      #
486 487 488 489
      #   # Generates a select field for days that defaults to the day for the date in my_date
      #   # that is named 'due' rather than 'day'
      #   select_day(my_time, :field_name => 'due')
      #
490 491 492 493
      #   # Generates a select field for days with a custom prompt.  Use :prompt => true for a
      #   # generic prompt.
      #   select_day(5, :prompt => 'Choose day')
      #
494
      def select_day(date, options = {}, html_options = {})
495
        DateTimeSelector.new(date, options, html_options).select_day
D
Initial  
David Heinemeier Hansson 已提交
496
      end
497

498 499 500 501 502 503 504 505
      # Returns a select tag with options for each of the months January through December with the current month
      # selected. The month names are presented as keys (what's shown to the user) and the month numbers (1-12) are
      # used as values (what's submitted to the server). It's also possible to use month numbers for the presentation
      # instead of names -- set the <tt>:use_month_numbers</tt> key in +options+ to true for this to happen. If you
      # want both numbers and names, set the <tt>:add_month_numbers</tt> key in +options+ to true. If you would prefer
      # to show month names as abbreviations, set the <tt>:use_short_month</tt> key in +options+ to true. If you want
      # to use your own month names, set the <tt>:use_month_names</tt> key in +options+ to an array of 12 month names.
      # Override the field name using the <tt>:field_name</tt> option, 'month' by default.
506
      #
507 508 509 510
      # ==== Examples
      #   # Generates a select field for months that defaults to the current month that
      #   # will use keys like "January", "March".
      #   select_month(Date.today)
D
Initial  
David Heinemeier Hansson 已提交
511
      #
512 513 514 515 516
      #   # Generates a select field for months that defaults to the current month that
      #   # is named "start" rather than "month"
      #   select_month(Date.today, :field_name => 'start')
      #
      #   # Generates a select field for months that defaults to the current month that
517
      #   # will use keys like "1", "3".
518 519 520 521 522 523 524 525 526 527 528 529 530
      #   select_month(Date.today, :use_month_numbers => true)
      #
      #   # Generates a select field for months that defaults to the current month that
      #   # will use keys like "1 - January", "3 - March".
      #   select_month(Date.today, :add_month_numbers => true)
      #
      #   # Generates a select field for months that defaults to the current month that
      #   # will use keys like "Jan", "Mar".
      #   select_month(Date.today, :use_short_month => true)
      #
      #   # Generates a select field for months that defaults to the current month that
      #   # will use keys like "Januar", "Marts."
      #   select_month(Date.today, :use_month_names => %w(Januar Februar Marts ...))
531
      #
532 533 534 535
      #   # Generates a select field for months with a custom prompt.  Use :prompt => true for a
      #   # generic prompt.
      #   select_month(14, :prompt => 'Choose month')
      #
536
      def select_month(date, options = {}, html_options = {})
537
        DateTimeSelector.new(date, options, html_options).select_month
538
      end
539

540 541 542 543 544
      # Returns a select tag with options for each of the five years on each side of the current, which is selected.
      # The five year radius can be changed using the <tt>:start_year</tt> and <tt>:end_year</tt> keys in the
      # +options+. Both ascending and descending year lists are supported by making <tt>:start_year</tt> less than or
      # greater than <tt>:end_year</tt>. The <tt>date</tt> can also be substituted for a year given as a number.
      # Override the field name using the <tt>:field_name</tt> option, 'year' by default.
545 546 547 548 549 550 551 552 553 554 555 556 557
      #
      # ==== Examples
      #   # Generates a select field for years that defaults to the current year that
      #   # has ascending year values
      #   select_year(Date.today, :start_year => 1992, :end_year => 2007)
      #
      #   # Generates a select field for years that defaults to the current year that
      #   # is named 'birth' rather than 'year'
      #   select_year(Date.today, :field_name => 'birth')
      #
      #   # Generates a select field for years that defaults to the current year that
      #   # has descending year values
      #   select_year(Date.today, :start_year => 2005, :end_year => 1900)
D
Initial  
David Heinemeier Hansson 已提交
558
      #
559 560
      #   # Generates a select field for years that defaults to the year 2006 that
      #   # has ascending year values
561
      #   select_year(2006, :start_year => 2000, :end_year => 2010)
562
      #
563 564 565 566
      #   # Generates a select field for years with a custom prompt.  Use :prompt => true for a
      #   # generic prompt.
      #   select_year(14, :prompt => 'Choose year')
      #
567
      def select_year(date, options = {}, html_options = {})
568 569
        DateTimeSelector.new(date, options, html_options).select_year
      end
570

571 572 573 574 575 576 577 578 579 580 581 582 583 584
      # Returns an html time tag for the given date or time.
      #
      # ==== Examples
      #   time_tag Date.today  # =>
      #     <time datetime="2010-11-04">November 04, 2010</time>
      #   time_tag Time.now  # =>
      #     <time datetime="2010-11-04T17:55:45+01:00">November 04, 2010 17:55</time>
      #   time_tag Date.yesterday, 'Yesterday'  # =>
      #     <time datetime="2010-11-03">Yesterday</time>
      #   time_tag Date.today, :pubdate => true  # =>
      #     <time datetime="2010-11-04" pubdate="pubdate">November 04, 2010</time>
      #
      def time_tag(date_or_time, *args)
        options  = args.extract_options!
585 586
        format   = options.delete(:format) || :long
        content  = args.first || I18n.l(date_or_time, :format => format)
587
        datetime = date_or_time.acts_like?(:time) ? date_or_time.xmlschema : date_or_time.rfc3339
588 589

        content_tag(:time, content, options.reverse_merge(:datetime => datetime))
590
      end
591 592 593 594 595 596
    end

    class DateTimeSelector #:nodoc:
      extend ActiveSupport::Memoizable
      include ActionView::Helpers::TagHelper

597
      DEFAULT_PREFIX = 'date'.freeze
598 599
      POSITION = {
        :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6
600
      }.freeze
601 602 603 604 605

      def initialize(datetime, options = {}, html_options = {})
        @options      = options.dup
        @html_options = html_options.dup
        @datetime     = datetime
606 607
        @options[:datetime_separator] ||= ' &mdash; '
        @options[:time_separator]     ||= ' : '
608 609 610
      end

      def select_datetime
611 612 613 614 615 616 617 618 619
        order = date_order.dup
        order -= [:hour, :minute, :second]
        @options[:discard_year]   ||= true unless order.include?(:year)
        @options[:discard_month]  ||= true unless order.include?(:month)
        @options[:discard_day]    ||= true if @options[:discard_month] || !order.include?(:day)
        @options[:discard_minute] ||= true if @options[:discard_hour]
        @options[:discard_second] ||= true unless @options[:include_seconds] && !@options[:discard_minute]

        # If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are
R
R.T. Lechow 已提交
620
        # valid (otherwise it could be 31 and February wouldn't be a valid date)
621 622 623 624
        if @datetime && @options[:discard_day] && !@options[:discard_month]
          @datetime = @datetime.change(:day => 1)
        end

625 626
        if @options[:tag] && @options[:ignore_date]
          select_time
627
        else
628 629 630 631 632 633 634 635 636 637
          [:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) }
          order += [:hour, :minute, :second] unless @options[:discard_hour]

          build_selects_from_types(order)
        end
      end

      def select_date
        order = date_order.dup

638 639 640
        @options[:discard_hour]     = true
        @options[:discard_minute]   = true
        @options[:discard_second]   = true
641

642 643 644
        @options[:discard_year]   ||= true unless order.include?(:year)
        @options[:discard_month]  ||= true unless order.include?(:month)
        @options[:discard_day]    ||= true if @options[:discard_month] || !order.include?(:day)
645

646
        # If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are
R
R.T. Lechow 已提交
647
        # valid (otherwise it could be 31 and February wouldn't be a valid date)
648 649
        if @datetime && @options[:discard_day] && !@options[:discard_month]
          @datetime = @datetime.change(:day => 1)
650 651 652 653 654 655 656 657 658 659
        end

        [:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) }

        build_selects_from_types(order)
      end

      def select_time
        order = []

660 661 662 663
        @options[:discard_month]    = true
        @options[:discard_year]     = true
        @options[:discard_day]      = true
        @options[:discard_second] ||= true unless @options[:include_seconds]
664

665
        order += [:year, :month, :day] unless @options[:ignore_date]
666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720

        order += [:hour, :minute]
        order << :second if @options[:include_seconds]

        build_selects_from_types(order)
      end

      def select_second
        if @options[:use_hidden] || @options[:discard_second]
          build_hidden(:second, sec) if @options[:include_seconds]
        else
          build_options_and_select(:second, sec)
        end
      end

      def select_minute
        if @options[:use_hidden] || @options[:discard_minute]
          build_hidden(:minute, min)
        else
          build_options_and_select(:minute, min, :step => @options[:minute_step])
        end
      end

      def select_hour
        if @options[:use_hidden] || @options[:discard_hour]
          build_hidden(:hour, hour)
        else
          build_options_and_select(:hour, hour, :end => 23)
        end
      end

      def select_day
        if @options[:use_hidden] || @options[:discard_day]
          build_hidden(:day, day)
        else
          build_options_and_select(:day, day, :start => 1, :end => 31, :leading_zeros => false)
        end
      end

      def select_month
        if @options[:use_hidden] || @options[:discard_month]
          build_hidden(:month, month)
        else
          month_options = []
          1.upto(12) do |month_number|
            options = { :value => month_number }
            options[:selected] = "selected" if month == month_number
            month_options << content_tag(:option, month_name(month_number), options) + "\n"
          end
          build_select(:month, month_options.join)
        end
      end

      def select_year
        if !@datetime || @datetime == 0
721
          val = ''
722 723
          middle_year = Date.today.year
        else
724
          val = middle_year = year
725 726
        end

727 728
        if @options[:use_hidden] || @options[:discard_year]
          build_hidden(:year, val)
729
        else
730 731 732 733 734 735 736
          options                 = {}
          options[:start]         = @options[:start_year] || middle_year - 5
          options[:end]           = @options[:end_year] || middle_year + 5
          options[:step]          = options[:start] < options[:end] ? 1 : -1
          options[:leading_zeros] = false

          build_options_and_select(:year, val, options)
D
Initial  
David Heinemeier Hansson 已提交
737 738
        end
      end
739

D
Initial  
David Heinemeier Hansson 已提交
740
      private
741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765
        %w( sec min hour day month year ).each do |method|
          define_method(method) do
            @datetime.kind_of?(Fixnum) ? @datetime : @datetime.send(method) if @datetime
          end
        end

        # Returns translated month names, but also ensures that a custom month
        # name array has a leading nil element
        def month_names
          month_names = @options[:use_month_names] || translated_month_names
          month_names.unshift(nil) if month_names.size < 13
          month_names
        end
        memoize :month_names

        # Returns translated month names
        #  => [nil, "January", "February", "March",
        #           "April", "May", "June", "July",
        #           "August", "September", "October",
        #           "November", "December"]
        #
        # If :use_short_month option is set
        #  => [nil, "Jan", "Feb", "Mar", "Apr", "May", "Jun",
        #           "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
        def translated_month_names
766 767
          key = @options[:use_short_month] ? :'date.abbr_month_names' : :'date.month_names'
          I18n.translate(key, :locale => @options[:locale])
768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793
        end

        # Lookup month name for number
        #  month_name(1) => "January"
        #
        # If :use_month_numbers option is passed
        #  month_name(1) => 1
        #
        # If :add_month_numbers option is passed
        #  month_name(1) => "1 - January"
        def month_name(number)
          if @options[:use_month_numbers]
            number
          elsif @options[:add_month_numbers]
            "#{number} - #{month_names[number]}"
          else
            month_names[number]
          end
        end

        def date_order
          @options[:order] || translated_date_order
        end
        memoize :date_order

        def translated_date_order
794
          I18n.translate(:'date.order', :locale => @options[:locale]) || []
795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810
        end

        # Build full select tag from date type and options
        def build_options_and_select(type, selected, options = {})
          build_select(type, build_options(selected, options))
        end

        # Build select option html from date value and options
        #  build_options(15, :start => 1, :end => 31)
        #  => "<option value="1">1</option>
        #      <option value=\"2\">2</option>
        #      <option value=\"3\">3</option>..."
        def build_options(selected, options = {})
          start         = options.delete(:start) || 0
          stop          = options.delete(:end) || 59
          step          = options.delete(:step) || 1
811 812
          options.reverse_merge!({:leading_zeros => true})
          leading_zeros = options.delete(:leading_zeros)
813 814

          select_options = []
815 816
          start.step(stop, step) do |i|
            value = leading_zeros ? sprintf("%02d", i) : i
817 818 819 820
            tag_options = { :value => value }
            tag_options[:selected] = "selected" if selected == i
            select_options << content_tag(:option, value, tag_options)
          end
821
          (select_options.join("\n") + "\n").html_safe
822
        end
823

824 825 826 827 828 829 830 831 832 833 834 835
        # Builds select tag from date type and html select options
        #  build_select(:month, "<option value="1">January</option>...")
        #  => "<select id="post_written_on_2i" name="post[written_on(2i)]">
        #        <option value="1">January</option>...
        #      </select>"
        def build_select(type, select_options_as_html)
          select_options = {
            :id => input_id_from_type(type),
            :name => input_name_from_type(type)
          }.merge(@html_options)
          select_options.merge!(:disabled => 'disabled') if @options[:disabled]

836
          select_html = "\n"
837
          select_html << content_tag(:option, '', :value => '') + "\n" if @options[:include_blank]
838
          select_html << prompt_option_tag(type, @options[:prompt]) + "\n" if @options[:prompt]
839
          select_html << select_options_as_html
840

841
          (content_tag(:select, select_html.html_safe, select_options) + "\n").html_safe
D
Initial  
David Heinemeier Hansson 已提交
842
        end
843

844 845 846 847
        # Builds a prompt option tag with supplied options or from default options
        #  prompt_option_tag(:month, :prompt => 'Select month')
        #  => "<option value="">Select month</option>"
        def prompt_option_tag(type, options)
848 849 850 851 852 853 854 855
          prompt = case options
            when Hash
              default_options = {:year => false, :month => false, :day => false, :hour => false, :minute => false, :second => false}
              default_options.merge!(options)[type.to_sym]
            when String
              options
            else
              I18n.translate(:"datetime.prompts.#{type}", :locale => @options[:locale])
856 857
          end

858
          prompt ? content_tag(:option, prompt, :value => '') : ''
859 860
        end

861 862 863 864
        # Builds hidden input tag for date part and value
        #  build_hidden(:year, 2008)
        #  => "<input id="post_written_on_1i" name="post[written_on(1i)]" type="hidden" value="2008" />"
        def build_hidden(type, value)
865
          (tag(:input, {
866 867 868 869
            :type => "hidden",
            :id => input_id_from_type(type),
            :name => input_name_from_type(type),
            :value => value
870
          }.merge(@html_options.slice(:disabled))) + "\n").html_safe
871 872
        end

873 874 875 876
        # Returns the name attribute for the input tag
        #  => post[written_on(1i)]
        def input_name_from_type(type)
          prefix = @options[:prefix] || ActionView::Helpers::DateTimeSelector::DEFAULT_PREFIX
877
          prefix += "[#{@options[:index]}]" if @options.has_key?(:index)
878 879 880 881 882 883 884 885 886 887 888 889 890 891 892

          field_name = @options[:field_name] || type
          if @options[:include_position]
            field_name += "(#{ActionView::Helpers::DateTimeSelector::POSITION[type]}i)"
          end

          @options[:discard_type] ? prefix : "#{prefix}[#{field_name}]"
        end

        # Returns the id attribute for the input tag
        #  => "post_written_on_1i"
        def input_id_from_type(type)
          input_name_from_type(type).gsub(/([\[\(])|(\]\[)/, '_').gsub(/[\]\)]/, '')
        end

P
Pratik Naik 已提交
893 894
        # Given an ordering of datetime components, create the selection HTML
        # and join them with their appropriate separators.
895 896 897 898 899 900
        def build_selects_from_types(order)
          select = ''
          order.reverse.each do |type|
            separator = separator(type) unless type == order.first # don't add on last field
            select.insert(0, separator.to_s + send("select_#{type}").to_s)
          end
901
          select.html_safe
902 903 904 905 906
        end

        # Returns the separator for a given datetime component
        def separator(type)
          case type
907 908
            when :year
              @options[:discard_year] ? "" : @options[:date_separator]
909 910 911 912
            when :month
              @options[:discard_month] ? "" : @options[:date_separator]
            when :day
              @options[:discard_day] ? "" : @options[:date_separator]
913 914 915
            when :hour
              (@options[:discard_year] && @options[:discard_day]) ? "" : @options[:datetime_separator]
            when :minute
916
              @options[:discard_minute] ? "" : @options[:time_separator]
917 918 919
            when :second
              @options[:include_seconds] ? @options[:time_separator] : ""
          end
920
        end
D
Initial  
David Heinemeier Hansson 已提交
921 922 923
    end

    class InstanceTag #:nodoc:
924
      def to_date_select_tag(options = {}, html_options = {})
925
        datetime_selector(options, html_options).select_date.html_safe
926
      end
D
Initial  
David Heinemeier Hansson 已提交
927

928
      def to_time_select_tag(options = {}, html_options = {})
929
        datetime_selector(options, html_options).select_time.html_safe
930
      end
931

932
      def to_datetime_select_tag(options = {}, html_options = {})
933
        datetime_selector(options, html_options).select_datetime.html_safe
934
      end
935

936
      private
937 938
        def datetime_selector(options, html_options)
          datetime = value(object) || default_datetime(options)
939
          @auto_index ||= nil
940 941 942 943 944

          options = options.dup
          options[:field_name]           = @method_name
          options[:include_position]     = true
          options[:prefix]             ||= @object_name
945
          options[:index]                = @auto_index if @auto_index && !options.has_key?(:index)
946

947
          DateTimeSelector.new(datetime, options, html_options)
948
        end
949

950
        def default_datetime(options)
951
          return if options[:include_blank] || options[:prompt]
952

953
          case options[:default]
954
            when nil
955
              Time.current
956
            when Date, Time
957
              options[:default]
958
            else
959 960
              default = options[:default].dup

961 962 963 964
              # Rename :minute and :second to :min and :sec
              default[:min] ||= default[:minute]
              default[:sec] ||= default[:second]

965
              time = Time.current
966

967
              [:year, :month, :day, :hour, :min, :sec].each do |key|
968
                default[key] ||= time.send(key)
969 970
              end

971 972 973 974 975
              Time.utc_time(
                default[:year], default[:month], default[:day],
                default[:hour], default[:min], default[:sec]
              )
          end
976
        end
D
Initial  
David Heinemeier Hansson 已提交
977
    end
978 979

    class FormBuilder
980
      def date_select(method, options = {}, html_options = {})
981
        @template.date_select(@object_name, method, objectify_options(options), html_options)
982 983
      end

984
      def time_select(method, options = {}, html_options = {})
985
        @template.time_select(@object_name, method, objectify_options(options), html_options)
986 987
      end

988
      def datetime_select(method, options = {}, html_options = {})
989
        @template.datetime_select(@object_name, method, objectify_options(options), html_options)
990 991
      end
    end
D
Initial  
David Heinemeier Hansson 已提交
992
  end
993
end