| 1 | require "date" |
|---|
| 2 | |
|---|
| 3 | module ActionView |
|---|
| 4 | module Helpers |
|---|
| 5 | # The Date Helper primarily creates select/option tags for different kinds of dates and date elements. All of the select-type methods |
|---|
| 6 | # share a number of common options that are as follows: |
|---|
| 7 | # |
|---|
| 8 | # * <tt>:prefix</tt> - overwrites the default prefix of "date" used for the select names. So specifying "birthday" would give |
|---|
| 9 | # birthday[month] instead of date[month] if passed to the select_month method. |
|---|
| 10 | # * <tt>:include_blank</tt> - set to true if it should be possible to set an empty date. |
|---|
| 11 | # * <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 |
|---|
| 12 | # method would use simply "date" (which can be overwritten using <tt>:prefix</tt>) instead of "date[month]". |
|---|
| 13 | module DateHelper |
|---|
| 14 | DEFAULT_PREFIX = 'date' unless const_defined?('DEFAULT_PREFIX') |
|---|
| 15 | |
|---|
| 16 | # Reports the approximate distance in time between two Time objects or integers. |
|---|
| 17 | # For example, if the distance is 47 minutes, it'll return |
|---|
| 18 | # "about 1 hour". See the source for the complete wording list. |
|---|
| 19 | # |
|---|
| 20 | # Integers are interpreted as seconds. So, |
|---|
| 21 | # <tt>distance_of_time_in_words(50)</tt> returns "less than a minute". |
|---|
| 22 | # |
|---|
| 23 | # Set <tt>include_seconds</tt> to true if you want more detailed approximations if distance < 1 minute |
|---|
| 24 | def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false) |
|---|
| 25 | from_time = from_time.to_time if from_time.respond_to?(:to_time) |
|---|
| 26 | to_time = to_time.to_time if to_time.respond_to?(:to_time) |
|---|
| 27 | distance_in_minutes = (((to_time - from_time).abs)/60).round |
|---|
| 28 | distance_in_seconds = ((to_time - from_time).abs).round |
|---|
| 29 | |
|---|
| 30 | case distance_in_minutes |
|---|
| 31 | when 0..1 |
|---|
| 32 | return (distance_in_minutes==0) ? 'less than a minute' : '1 minute' unless include_seconds |
|---|
| 33 | case distance_in_seconds |
|---|
| 34 | when 0..5 then 'less than 5 seconds' |
|---|
| 35 | when 6..10 then 'less than 10 seconds' |
|---|
| 36 | when 11..20 then 'less than 20 seconds' |
|---|
| 37 | when 21..40 then 'half a minute' |
|---|
| 38 | when 41..59 then 'less than a minute' |
|---|
| 39 | else '1 minute' |
|---|
| 40 | end |
|---|
| 41 | |
|---|
| 42 | when 2..45 then "#{distance_in_minutes} minutes" |
|---|
| 43 | when 46..90 then 'about 1 hour' |
|---|
| 44 | when 90..1440 then "about #{(distance_in_minutes.to_f / 60.0).round} hours" |
|---|
| 45 | when 1441..2880 then '1 day' |
|---|
| 46 | else "#{(distance_in_minutes / 1440).round} days" |
|---|
| 47 | end |
|---|
| 48 | end |
|---|
| 49 | |
|---|
| 50 | # Like distance_of_time_in_words, but where <tt>to_time</tt> is fixed to <tt>Time.now</tt>. |
|---|
| 51 | def time_ago_in_words(from_time, include_seconds = false) |
|---|
| 52 | distance_of_time_in_words(from_time, Time.now, include_seconds) |
|---|
| 53 | end |
|---|
| 54 | |
|---|
| 55 | alias_method :distance_of_time_in_words_to_now, :time_ago_in_words |
|---|
| 56 | |
|---|
| 57 | # Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based attribute (identified by |
|---|
| 58 | # +method+) on an object assigned to the template (identified by +object+). It's possible to tailor the selects through the +options+ hash, |
|---|
| 59 | # which accepts all the keys that each of the individual select builders do (like :use_month_numbers for select_month) as well as a range of |
|---|
| 60 | # discard options. The discard options are <tt>:discard_year</tt>, <tt>:discard_month</tt> and <tt>:discard_day</tt>. Set to true, they'll |
|---|
| 61 | # drop the respective select. Discarding the month select will also automatically discard the day select. It's also possible to explicitly |
|---|
| 62 | # 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 |
|---|
| 63 | # the desired order. Symbols may be omitted and the respective select is not included. |
|---|
| 64 | # |
|---|
| 65 | # Passing :disabled => true as part of the +options+ will make elements inaccessible for change. |
|---|
| 66 | # |
|---|
| 67 | # NOTE: Discarded selects will default to 1. So if no month select is available, January will be assumed. |
|---|
| 68 | # |
|---|
| 69 | # Examples: |
|---|
| 70 | # |
|---|
| 71 | # date_select("post", "written_on") |
|---|
| 72 | # date_select("post", "written_on", :start_year => 1995) |
|---|
| 73 | # date_select("post", "written_on", :start_year => 1995, :use_month_numbers => true, |
|---|
| 74 | # :discard_day => true, :include_blank => true) |
|---|
| 75 | # date_select("post", "written_on", :order => [:day, :month, :year]) |
|---|
| 76 | # date_select("user", "birthday", :order => [:month, :day]) |
|---|
| 77 | # |
|---|
| 78 | # The selects are prepared for multi-parameter assignment to an Active Record object. |
|---|
| 79 | def date_select(object_name, method, options = {}) |
|---|
| 80 | InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_date_select_tag(options) |
|---|
| 81 | end |
|---|
| 82 | |
|---|
| 83 | # Returns a set of select tags (one for year, month, day, hour, and minute) pre-selected for accessing a specified datetime-based |
|---|
| 84 | # attribute (identified by +method+) on an object assigned to the template (identified by +object+). Examples: |
|---|
| 85 | # |
|---|
| 86 | # datetime_select("post", "written_on") |
|---|
| 87 | # datetime_select("post", "written_on", :start_year => 1995) |
|---|
| 88 | # |
|---|
| 89 | # The selects are prepared for multi-parameter assignment to an Active Record object. |
|---|
| 90 | def datetime_select(object_name, method, options = {}) |
|---|
| 91 | InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_datetime_select_tag(options) |
|---|
| 92 | end |
|---|
| 93 | |
|---|
| 94 | # Returns a set of html select-tags (one for year, month, and day) pre-selected with the +date+. |
|---|
| 95 | def select_date(date = Date.today, options = {}) |
|---|
| 96 | select_year(date, options) + select_month(date, options) + select_day(date, options) |
|---|
| 97 | end |
|---|
| 98 | |
|---|
| 99 | # Returns a set of html select-tags (one for year, month, day, hour, and minute) pre-selected with the +datetime+. |
|---|
| 100 | def select_datetime(datetime = Time.now, options = {}) |
|---|
| 101 | select_year(datetime, options) + select_month(datetime, options) + select_day(datetime, options) + |
|---|
| 102 | select_hour(datetime, options) + select_minute(datetime, options) |
|---|
| 103 | end |
|---|
| 104 | |
|---|
| 105 | # Returns a set of html select-tags (one for hour and minute) |
|---|
| 106 | def select_time(datetime = Time.now, options = {}) |
|---|
| 107 | h = select_hour(datetime, options) + select_minute(datetime, options) + (options[:include_seconds] ? select_second(datetime, options) : '') |
|---|
| 108 | end |
|---|
| 109 | |
|---|
| 110 | # Returns a select tag with options for each of the seconds 0 through 59 with the current second selected. |
|---|
| 111 | # The <tt>second</tt> can also be substituted for a second number. |
|---|
| 112 | # Override the field name using the <tt>:field_name</tt> option, 'second' by default. |
|---|
| 113 | def select_second(datetime, options = {}) |
|---|
| 114 | second_options = [] |
|---|
| 115 | |
|---|
| 116 | 0.upto(59) do |second| |
|---|
| 117 | second_options << ((datetime && (datetime.kind_of?(Fixnum) ? datetime : datetime.sec) == second) ? |
|---|
| 118 | %(<option value="#{leading_zero_on_single_digits(second)}" selected="selected">#{leading_zero_on_single_digits(second)}</option>\n) : |
|---|
| 119 | %(<option value="#{leading_zero_on_single_digits(second)}">#{leading_zero_on_single_digits(second)}</option>\n) |
|---|
| 120 | ) |
|---|
| 121 | end |
|---|
| 122 | |
|---|
| 123 | select_html(options[:field_name] || 'second', second_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled]) |
|---|
| 124 | end |
|---|
| 125 | |
|---|
| 126 | # Returns a select tag with options for each of the minutes 0 through 59 with the current minute selected. |
|---|
| 127 | # Also can return a select tag with options by <tt>minute_step</tt> from 0 through 59 with the 00 minute selected |
|---|
| 128 | # The <tt>minute</tt> can also be substituted for a minute number. |
|---|
| 129 | # Override the field name using the <tt>:field_name</tt> option, 'minute' by default. |
|---|
| 130 | def select_minute(datetime, options = {}) |
|---|
| 131 | minute_options = [] |
|---|
| 132 | |
|---|
| 133 | 0.step(59, options[:minute_step] || 1) do |minute| |
|---|
| 134 | minute_options << ((datetime && (datetime.kind_of?(Fixnum) ? datetime : datetime.min) == minute) ? |
|---|
| 135 | %(<option value="#{leading_zero_on_single_digits(minute)}" selected="selected">#{leading_zero_on_single_digits(minute)}</option>\n) : |
|---|
| 136 | %(<option value="#{leading_zero_on_single_digits(minute)}">#{leading_zero_on_single_digits(minute)}</option>\n) |
|---|
| 137 | ) |
|---|
| 138 | end |
|---|
| 139 | |
|---|
| 140 | select_html(options[:field_name] || 'minute', minute_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled]) |
|---|
| 141 | end |
|---|
| 142 | |
|---|
| 143 | # Returns a select tag with options for each of the hours 0 through 23 with the current hour selected. |
|---|
| 144 | # The <tt>hour</tt> can also be substituted for a hour number. |
|---|
| 145 | # Override the field name using the <tt>:field_name</tt> option, 'hour' by default. |
|---|
| 146 | def select_hour(datetime, options = {}) |
|---|
| 147 | hour_options = [] |
|---|
| 148 | |
|---|
| 149 | 0.upto(23) do |hour| |
|---|
| 150 | hour_options << ((datetime && (datetime.kind_of?(Fixnum) ? datetime : datetime.hour) == hour) ? |
|---|
| 151 | %(<option value="#{leading_zero_on_single_digits(hour)}" selected="selected">#{leading_zero_on_single_digits(hour)}</option>\n) : |
|---|
| 152 | %(<option value="#{leading_zero_on_single_digits(hour)}">#{leading_zero_on_single_digits(hour)}</option>\n) |
|---|
| 153 | ) |
|---|
| 154 | end |
|---|
| 155 | |
|---|
| 156 | select_html(options[:field_name] || 'hour', hour_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled]) |
|---|
| 157 | end |
|---|
| 158 | |
|---|
| 159 | # Returns a select tag with options for each of the days 1 through 31 with the current day selected. |
|---|
| 160 | # The <tt>date</tt> can also be substituted for a hour number. |
|---|
| 161 | # Override the field name using the <tt>:field_name</tt> option, 'day' by default. |
|---|
| 162 | def select_day(date, options = {}) |
|---|
| 163 | day_options = [] |
|---|
| 164 | |
|---|
| 165 | 1.upto(31) do |day| |
|---|
| 166 | day_options << ((date && (date.kind_of?(Fixnum) ? date : date.day) == day) ? |
|---|
| 167 | %(<option value="#{day}" selected="selected">#{day}</option>\n) : |
|---|
| 168 | %(<option value="#{day}">#{day}</option>\n) |
|---|
| 169 | ) |
|---|
| 170 | end |
|---|
| 171 | |
|---|
| 172 | select_html(options[:field_name] || 'day', day_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled]) |
|---|
| 173 | end |
|---|
| 174 | |
|---|
| 175 | # Returns a select tag with options for each of the months January through December with the current month selected. |
|---|
| 176 | # The month names are presented as keys (what's shown to the user) and the month numbers (1-12) are used as values |
|---|
| 177 | # (what's submitted to the server). It's also possible to use month numbers for the presentation instead of names -- |
|---|
| 178 | # set the <tt>:use_month_numbers</tt> key in +options+ to true for this to happen. If you want both numbers and names, |
|---|
| 179 | # set the <tt>:add_month_numbers</tt> key in +options+ to true. Examples: |
|---|
| 180 | # |
|---|
| 181 | # select_month(Date.today) # Will use keys like "January", "March" |
|---|
| 182 | # select_month(Date.today, :use_month_numbers => true) # Will use keys like "1", "3" |
|---|
| 183 | # select_month(Date.today, :add_month_numbers => true) # Will use keys like "1 - January", "3 - March" |
|---|
| 184 | # |
|---|
| 185 | # Override the field name using the <tt>:field_name</tt> option, 'month' by default. |
|---|
| 186 | # |
|---|
| 187 | # If you would prefer to show month names as abbreviations, set the |
|---|
| 188 | # <tt>:use_short_month</tt> key in +options+ to true. |
|---|
| 189 | def select_month(date, options = {}) |
|---|
| 190 | month_options = [] |
|---|
| 191 | month_names = options[:use_short_month] ? Date::ABBR_MONTHNAMES : Date::MONTHNAMES |
|---|
| 192 | |
|---|
| 193 | 1.upto(12) do |month_number| |
|---|
| 194 | month_name = if options[:use_month_numbers] |
|---|
| 195 | month_number |
|---|
| 196 | elsif options[:add_month_numbers] |
|---|
| 197 | month_number.to_s + ' - ' + month_names[month_number] |
|---|
| 198 | else |
|---|
| 199 | month_names[month_number] |
|---|
| 200 | end |
|---|
| 201 | |
|---|
| 202 | month_options << ((date && (date.kind_of?(Fixnum) ? date : date.month) == month_number) ? |
|---|
| 203 | %(<option value="#{month_number}" selected="selected">#{month_name}</option>\n) : |
|---|
| 204 | %(<option value="#{month_number}">#{month_name}</option>\n) |
|---|
| 205 | ) |
|---|
| 206 | end |
|---|
| 207 | |
|---|
| 208 | select_html(options[:field_name] || 'month', month_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled]) |
|---|
| 209 | end |
|---|
| 210 | |
|---|
| 211 | # 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 |
|---|
| 212 | # can be changed using the <tt>:start_year</tt> and <tt>:end_year</tt> keys in the +options+. Both ascending and descending year |
|---|
| 213 | # 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 |
|---|
| 214 | # substituted for a year given as a number. Example: |
|---|
| 215 | # |
|---|
| 216 | # select_year(Date.today, :start_year => 1992, :end_year => 2007) # ascending year values |
|---|
| 217 | # select_year(Date.today, :start_year => 2005, :end_year => 1900) # descending year values |
|---|
| 218 | # |
|---|
| 219 | # Override the field name using the <tt>:field_name</tt> option, 'year' by default. |
|---|
| 220 | def select_year(date, options = {}) |
|---|
| 221 | year_options = [] |
|---|
| 222 | y = date ? (date.kind_of?(Fixnum) ? (y = (date == 0) ? Date.today.year : date) : date.year) : Date.today.year |
|---|
| 223 | |
|---|
| 224 | start_year, end_year = (options[:start_year] || y-5), (options[:end_year] || y+5) |
|---|
| 225 | step_val = start_year < end_year ? 1 : -1 |
|---|
| 226 | |
|---|
| 227 | start_year.step(end_year, step_val) do |year| |
|---|
| 228 | year_options << ((date && (date.kind_of?(Fixnum) ? date : date.year) == year) ? |
|---|
| 229 | %(<option value="#{year}" selected="selected">#{year}</option>\n) : |
|---|
| 230 | %(<option value="#{year}">#{year}</option>\n) |
|---|
| 231 | ) |
|---|
| 232 | end |
|---|
| 233 | |
|---|
| 234 | select_html(options[:field_name] || 'year', year_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled]) |
|---|
| 235 | end |
|---|
| 236 | |
|---|
| 237 | private |
|---|
| 238 | def select_html(type, options, prefix = nil, include_blank = false, discard_type = false, disabled = false) |
|---|
| 239 | select_html = %(<select name="#{prefix || DEFAULT_PREFIX}) |
|---|
| 240 | select_html << "[#{type}]" unless discard_type |
|---|
| 241 | select_html << %(") |
|---|
| 242 | select_html << %( disabled="disabled") if disabled |
|---|
| 243 | select_html << %(>\n) |
|---|
| 244 | select_html << %(<option value=""></option>\n) if include_blank |
|---|
| 245 | select_html << options.to_s |
|---|
| 246 | select_html << "</select>\n" |
|---|
| 247 | end |
|---|
| 248 | |
|---|
| 249 | def leading_zero_on_single_digits(number) |
|---|
| 250 | number > 9 ? number : "0#{number}" |
|---|
| 251 | end |
|---|
| 252 | end |
|---|
| 253 | |
|---|
| 254 | class InstanceTag #:nodoc: |
|---|
| 255 | include DateHelper |
|---|
| 256 | |
|---|
| 257 | def to_date_select_tag(options = {}) |
|---|
| 258 | defaults = { :discard_type => true } |
|---|
| 259 | options = defaults.merge(options) |
|---|
| 260 | options_with_prefix = Proc.new { |position| options.merge(:prefix => "#{@object_name}[#{@method_name}(#{position}i)]") } |
|---|
| 261 | date = options[:include_blank] ? (value || 0) : (value || Date.today) |
|---|
| 262 | |
|---|
| 263 | date_select = '' |
|---|
| 264 | options[:order] = [:month, :year, :day] if options[:month_before_year] # For backwards compatibility |
|---|
| 265 | options[:order] ||= [:year, :month, :day] |
|---|
| 266 | |
|---|
| 267 | position = {:year => 1, :month => 2, :day => 3} |
|---|
| 268 | |
|---|
| 269 | discard = {} |
|---|
| 270 | discard[:year] = true if options[:discard_year] |
|---|
| 271 | discard[:month] = true if options[:discard_month] |
|---|
| 272 | discard[:day] = true if options[:discard_day] or options[:discard_month] |
|---|
| 273 | |
|---|
| 274 | options[:order].each do |param| |
|---|
| 275 | date_select << self.send("select_#{param}", date, options_with_prefix.call(position[param])) unless discard[param] |
|---|
| 276 | end |
|---|
| 277 | |
|---|
| 278 | date_select |
|---|
| 279 | end |
|---|
| 280 | |
|---|
| 281 | def to_datetime_select_tag(options = {}) |
|---|
| 282 | defaults = { :discard_type => true } |
|---|
| 283 | options = defaults.merge(options) |
|---|
| 284 | options_with_prefix = Proc.new { |position| options.merge(:prefix => "#{@object_name}[#{@method_name}(#{position}i)]") } |
|---|
| 285 | datetime = options[:include_blank] ? (value || nil) : (value || Time.now) |
|---|
| 286 | |
|---|
| 287 | datetime_select = select_year(datetime, options_with_prefix.call(1)) |
|---|
| 288 | datetime_select << select_month(datetime, options_with_prefix.call(2)) unless options[:discard_month] |
|---|
| 289 | datetime_select << select_day(datetime, options_with_prefix.call(3)) unless options[:discard_day] || options[:discard_month] |
|---|
| 290 | datetime_select << ' — ' + select_hour(datetime, options_with_prefix.call(4)) unless options[:discard_hour] |
|---|
| 291 | datetime_select << ' : ' + select_minute(datetime, options_with_prefix.call(5)) unless options[:discard_minute] || options[:discard_hour] |
|---|
| 292 | |
|---|
| 293 | datetime_select |
|---|
| 294 | end |
|---|
| 295 | end |
|---|
| 296 | |
|---|
| 297 | class FormBuilder |
|---|
| 298 | def date_select(method, options = {}) |
|---|
| 299 | @template.date_select(@object_name, method, options.merge(:object => @object)) |
|---|
| 300 | end |
|---|
| 301 | |
|---|
| 302 | def datetime_select(method, options = {}) |
|---|
| 303 | @template.datetime_select(@object_name, method, options.merge(:object => @object)) |
|---|
| 304 | end |
|---|
| 305 | end |
|---|
| 306 | end |
|---|
| 307 | end |
|---|