merb-helpers 0.9.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (192) hide show
  1. data/LICENSE +20 -0
  2. data/README +180 -0
  3. data/Rakefile +77 -0
  4. data/TODO +5 -0
  5. data/lib/merb-helpers.rb +38 -0
  6. data/lib/merb-helpers/core_ext.rb +57 -0
  7. data/lib/merb-helpers/core_ext/numeric.rb +388 -0
  8. data/lib/merb-helpers/date_time_formatting.rb +127 -0
  9. data/lib/merb-helpers/date_time_helpers.rb +190 -0
  10. data/lib/merb-helpers/form/builder.rb +410 -0
  11. data/lib/merb-helpers/form/helpers.rb +448 -0
  12. data/lib/merb-helpers/form_helpers.rb +24 -0
  13. data/lib/merb-helpers/tag_helpers.rb +61 -0
  14. data/lib/merb-helpers/text_helpers.rb +61 -0
  15. data/lib/merb-helpers/time_dsl.rb +59 -0
  16. data/spec/core_ext_spec.rb +19 -0
  17. data/spec/fixture/app/controllers/application.rb +4 -0
  18. data/spec/fixture/app/controllers/bound_check_box.rb +2 -0
  19. data/spec/fixture/app/controllers/bound_file_field.rb +2 -0
  20. data/spec/fixture/app/controllers/bound_hidden_field.rb +2 -0
  21. data/spec/fixture/app/controllers/bound_option_tag.rb +2 -0
  22. data/spec/fixture/app/controllers/bound_password_field.rb +2 -0
  23. data/spec/fixture/app/controllers/bound_radio_button.rb +2 -0
  24. data/spec/fixture/app/controllers/bound_radio_group.rb +2 -0
  25. data/spec/fixture/app/controllers/bound_select.rb +2 -0
  26. data/spec/fixture/app/controllers/bound_text_area.rb +2 -0
  27. data/spec/fixture/app/controllers/bound_text_field.rb +2 -0
  28. data/spec/fixture/app/controllers/button.rb +2 -0
  29. data/spec/fixture/app/controllers/check_box.rb +2 -0
  30. data/spec/fixture/app/controllers/custom_builder.rb +2 -0
  31. data/spec/fixture/app/controllers/delete_button.rb +2 -0
  32. data/spec/fixture/app/controllers/exceptions.rb +25 -0
  33. data/spec/fixture/app/controllers/field_set.rb +2 -0
  34. data/spec/fixture/app/controllers/fields_for.rb +3 -0
  35. data/spec/fixture/app/controllers/file_field.rb +2 -0
  36. data/spec/fixture/app/controllers/foo.rb +23 -0
  37. data/spec/fixture/app/controllers/form.rb +2 -0
  38. data/spec/fixture/app/controllers/form_for.rb +2 -0
  39. data/spec/fixture/app/controllers/hidden_field.rb +2 -0
  40. data/spec/fixture/app/controllers/label.rb +3 -0
  41. data/spec/fixture/app/controllers/numeric_ext.rb +3 -0
  42. data/spec/fixture/app/controllers/option_tag.rb +2 -0
  43. data/spec/fixture/app/controllers/password_field.rb +2 -0
  44. data/spec/fixture/app/controllers/radio_button.rb +2 -0
  45. data/spec/fixture/app/controllers/radio_group.rb +2 -0
  46. data/spec/fixture/app/controllers/select.rb +2 -0
  47. data/spec/fixture/app/controllers/specs_controller.rb +11 -0
  48. data/spec/fixture/app/controllers/submit.rb +2 -0
  49. data/spec/fixture/app/controllers/tag_helper.rb +21 -0
  50. data/spec/fixture/app/controllers/text_area.rb +2 -0
  51. data/spec/fixture/app/controllers/text_field.rb +3 -0
  52. data/spec/fixture/app/helpers/global_helpers.rb +8 -0
  53. data/spec/fixture/app/models/fake_dm_model.rb +25 -0
  54. data/spec/fixture/app/models/first_generic_fake_model.rb +57 -0
  55. data/spec/fixture/app/models/second_generic_fake_model.rb +18 -0
  56. data/spec/fixture/app/models/third_generic_fake_model.rb +3 -0
  57. data/spec/fixture/app/views/bound_check_box_specs/basic.html.erb +4 -0
  58. data/spec/fixture/app/views/bound_check_box_specs/checked.html.erb +4 -0
  59. data/spec/fixture/app/views/bound_check_box_specs/errors.html.erb +4 -0
  60. data/spec/fixture/app/views/bound_check_box_specs/label.html.erb +3 -0
  61. data/spec/fixture/app/views/bound_check_box_specs/on_and_off.html.erb +3 -0
  62. data/spec/fixture/app/views/bound_check_box_specs/raise_value_error.html.erb +3 -0
  63. data/spec/fixture/app/views/bound_file_field_specs/additional_attributes.html.erb +3 -0
  64. data/spec/fixture/app/views/bound_file_field_specs/takes_string.html.erb +3 -0
  65. data/spec/fixture/app/views/bound_file_field_specs/with_label.html.erb +3 -0
  66. data/spec/fixture/app/views/bound_hidden_field_specs/basic.html.erb +3 -0
  67. data/spec/fixture/app/views/bound_hidden_field_specs/errors.html.erb +3 -0
  68. data/spec/fixture/app/views/bound_hidden_field_specs/hidden_error.html.erb +3 -0
  69. data/spec/fixture/app/views/bound_hidden_field_specs/label.html.erb +3 -0
  70. data/spec/fixture/app/views/bound_option_tag_specs/grouped.html.erb +3 -0
  71. data/spec/fixture/app/views/bound_option_tag_specs/nested.html.erb +3 -0
  72. data/spec/fixture/app/views/bound_option_tag_specs/text_and_value.html.erb +4 -0
  73. data/spec/fixture/app/views/bound_password_field_specs/attributes.html.erb +3 -0
  74. data/spec/fixture/app/views/bound_password_field_specs/basic.html.erb +3 -0
  75. data/spec/fixture/app/views/bound_password_field_specs/label.html.erb +3 -0
  76. data/spec/fixture/app/views/bound_radio_button_specs/basic.html.erb +3 -0
  77. data/spec/fixture/app/views/bound_radio_group_specs/basic.html.erb +3 -0
  78. data/spec/fixture/app/views/bound_radio_group_specs/hashes.html.erb +3 -0
  79. data/spec/fixture/app/views/bound_radio_group_specs/mixed.html.erb +4 -0
  80. data/spec/fixture/app/views/bound_radio_group_specs/override_id.html.erb +3 -0
  81. data/spec/fixture/app/views/bound_select_specs/basic.html.erb +3 -0
  82. data/spec/fixture/app/views/bound_select_specs/blank.html.erb +3 -0
  83. data/spec/fixture/app/views/bound_select_specs/multiple.html.erb +3 -0
  84. data/spec/fixture/app/views/bound_select_specs/prompt.html.erb +3 -0
  85. data/spec/fixture/app/views/bound_select_specs/with_options.html.erb +3 -0
  86. data/spec/fixture/app/views/bound_select_specs/with_options_with_blank.html.erb +3 -0
  87. data/spec/fixture/app/views/bound_text_area_specs/basic.html.erb +3 -0
  88. data/spec/fixture/app/views/bound_text_field_specs/basic.html.erb +3 -0
  89. data/spec/fixture/app/views/button_specs/button_with_label.html.erb +1 -0
  90. data/spec/fixture/app/views/button_specs/button_with_values.html.erb +1 -0
  91. data/spec/fixture/app/views/button_specs/disabled_button.html.erb +1 -0
  92. data/spec/fixture/app/views/check_box_specs/basic.html.erb +1 -0
  93. data/spec/fixture/app/views/check_box_specs/boolean.html.erb +1 -0
  94. data/spec/fixture/app/views/check_box_specs/disabled.html.erb +1 -0
  95. data/spec/fixture/app/views/check_box_specs/label.html.erb +1 -0
  96. data/spec/fixture/app/views/check_box_specs/on_off_is_boolean.html.erb +1 -0
  97. data/spec/fixture/app/views/check_box_specs/raise_unless_both_on_and_off.html.erb +2 -0
  98. data/spec/fixture/app/views/check_box_specs/raises_error_if_not_boolean.html.erb +1 -0
  99. data/spec/fixture/app/views/check_box_specs/raises_error_if_on_off_and_boolean_false.html.erb +1 -0
  100. data/spec/fixture/app/views/check_box_specs/simple.html.erb +1 -0
  101. data/spec/fixture/app/views/check_box_specs/to_string.html.erb +8 -0
  102. data/spec/fixture/app/views/check_box_specs/unchecked.html.erb +2 -0
  103. data/spec/fixture/app/views/custom_builder_specs/everything.html.erb +10 -0
  104. data/spec/fixture/app/views/delete_button_specs/delete_with_explicit_url.html.erb +1 -0
  105. data/spec/fixture/app/views/delete_button_specs/delete_with_extra_params.html.erb +1 -0
  106. data/spec/fixture/app/views/delete_button_specs/delete_with_label.html.erb +1 -0
  107. data/spec/fixture/app/views/delete_button_specs/simple_delete.html.erb +1 -0
  108. data/spec/fixture/app/views/exeptions/client_error.html.erb +37 -0
  109. data/spec/fixture/app/views/exeptions/internal_server_error.html.erb +216 -0
  110. data/spec/fixture/app/views/exeptions/not_acceptable.html.erb +38 -0
  111. data/spec/fixture/app/views/exeptions/not_found.html.erb +40 -0
  112. data/spec/fixture/app/views/fields_for_specs/basic.html.erb +3 -0
  113. data/spec/fixture/app/views/fields_for_specs/midstream.html.erb +7 -0
  114. data/spec/fixture/app/views/fields_for_specs/nil.html.erb +3 -0
  115. data/spec/fixture/app/views/fieldset_specs/legend.html.erb +3 -0
  116. data/spec/fixture/app/views/file_field_specs/disabled.html.erb +1 -0
  117. data/spec/fixture/app/views/file_field_specs/makes_multipart.html.erb +3 -0
  118. data/spec/fixture/app/views/file_field_specs/with_label.html.erb +1 -0
  119. data/spec/fixture/app/views/file_field_specs/with_values.html.erb +1 -0
  120. data/spec/fixture/app/views/foo/bar.html.erb +0 -0
  121. data/spec/fixture/app/views/form_for_specs/basic.html.erb +3 -0
  122. data/spec/fixture/app/views/form_specs/create_a_form.html.erb +3 -0
  123. data/spec/fixture/app/views/form_specs/create_a_multipart_form.html.erb +3 -0
  124. data/spec/fixture/app/views/form_specs/fake_delete_if_set.html.erb +3 -0
  125. data/spec/fixture/app/views/form_specs/fake_put_if_set.html.erb +3 -0
  126. data/spec/fixture/app/views/form_specs/get_if_set.html.erb +3 -0
  127. data/spec/fixture/app/views/form_specs/post_by_default.html.erb +3 -0
  128. data/spec/fixture/app/views/form_specs/resourceful_form.html.erb +3 -0
  129. data/spec/fixture/app/views/hidden_field_specs/basic.html.erb +1 -0
  130. data/spec/fixture/app/views/hidden_field_specs/disabled.html.erb +1 -0
  131. data/spec/fixture/app/views/hidden_field_specs/label.html.erb +1 -0
  132. data/spec/fixture/app/views/label_specs/basic.html.erb +1 -0
  133. data/spec/fixture/app/views/layout/application.html.erb +11 -0
  134. data/spec/fixture/app/views/numeric_ext_specs/minutes_to_hours.html.erb +1 -0
  135. data/spec/fixture/app/views/numeric_ext_specs/to_concurrency_default.html.erb +1 -0
  136. data/spec/fixture/app/views/numeric_ext_specs/two_digits.html.erb +1 -0
  137. data/spec/fixture/app/views/option_tag_specs/array.html.erb +1 -0
  138. data/spec/fixture/app/views/option_tag_specs/clean.html.erb +1 -0
  139. data/spec/fixture/app/views/option_tag_specs/collection.html.erb +1 -0
  140. data/spec/fixture/app/views/option_tag_specs/multiple_selects.html.erb +1 -0
  141. data/spec/fixture/app/views/option_tag_specs/no_extra_attributes.html.erb +1 -0
  142. data/spec/fixture/app/views/option_tag_specs/optgroups.html.erb +1 -0
  143. data/spec/fixture/app/views/option_tag_specs/selected.html.erb +1 -0
  144. data/spec/fixture/app/views/option_tag_specs/with_blank.html.erb +1 -0
  145. data/spec/fixture/app/views/option_tag_specs/with_prompt.html.erb +1 -0
  146. data/spec/fixture/app/views/password_field_specs/basic.html.erb +1 -0
  147. data/spec/fixture/app/views/password_field_specs/disabled.html.erb +1 -0
  148. data/spec/fixture/app/views/radio_button_specs/basic.html.erb +1 -0
  149. data/spec/fixture/app/views/radio_button_specs/disabled.html.erb +1 -0
  150. data/spec/fixture/app/views/radio_button_specs/label.html.erb +1 -0
  151. data/spec/fixture/app/views/radio_group_specs/attributes.html.erb +1 -0
  152. data/spec/fixture/app/views/radio_group_specs/basic.html.erb +1 -0
  153. data/spec/fixture/app/views/radio_group_specs/hash.html.erb +1 -0
  154. data/spec/fixture/app/views/radio_group_specs/specific_attributes.html.erb +1 -0
  155. data/spec/fixture/app/views/select_specs/blank.html.erb +1 -0
  156. data/spec/fixture/app/views/select_specs/multiple.html.erb +1 -0
  157. data/spec/fixture/app/views/submit_specs/disabled_submit.html.erb +1 -0
  158. data/spec/fixture/app/views/submit_specs/submit_with_label.html.erb +1 -0
  159. data/spec/fixture/app/views/submit_specs/submit_with_values.html.erb +1 -0
  160. data/spec/fixture/app/views/tag_helper/nested_tags.html.erb +5 -0
  161. data/spec/fixture/app/views/tag_helper/tag_with_attributes.html.erb +1 -0
  162. data/spec/fixture/app/views/tag_helper/tag_with_content.html.erb +1 -0
  163. data/spec/fixture/app/views/tag_helper/tag_with_content_in_the_block.html.erb +3 -0
  164. data/spec/fixture/app/views/text_area_specs/basic.html.erb +1 -0
  165. data/spec/fixture/app/views/text_area_specs/disabled.html.erb +1 -0
  166. data/spec/fixture/app/views/text_area_specs/label.html.erb +1 -0
  167. data/spec/fixture/app/views/text_area_specs/nil.html.erb +1 -0
  168. data/spec/fixture/app/views/text_field_specs/basic.html.erb +1 -0
  169. data/spec/fixture/app/views/text_field_specs/class.html.erb +1 -0
  170. data/spec/fixture/app/views/text_field_specs/disabled.html.erb +1 -0
  171. data/spec/fixture/app/views/text_field_specs/label.html.erb +1 -0
  172. data/spec/fixture/config/environments/development.rb +6 -0
  173. data/spec/fixture/config/environments/production.rb +5 -0
  174. data/spec/fixture/config/environments/test.rb +6 -0
  175. data/spec/fixture/config/init.rb +46 -0
  176. data/spec/fixture/config/rack.rb +11 -0
  177. data/spec/fixture/config/router.rb +36 -0
  178. data/spec/fixture/log/merb.main.pid +1 -0
  179. data/spec/fixture/log/merb_test.log +624 -0
  180. data/spec/fixture/public/images/merb.jpg +0 -0
  181. data/spec/fixture/public/merb.fcgi +4 -0
  182. data/spec/fixture/public/stylesheets/master.css +119 -0
  183. data/spec/merb_helpers_config_spec.rb +82 -0
  184. data/spec/merb_helpers_date_time_spec.rb +257 -0
  185. data/spec/merb_helpers_form_spec.rb +1266 -0
  186. data/spec/merb_helpers_tag_helper_spec.rb +46 -0
  187. data/spec/merb_helpers_text_spec.rb +67 -0
  188. data/spec/numeric_extlib_spec.rb +135 -0
  189. data/spec/ordinalize_spec.rb +51 -0
  190. data/spec/spec_helper.rb +149 -0
  191. data/spec/time_dsl_spec.rb +43 -0
  192. metadata +303 -0
@@ -0,0 +1,127 @@
1
+ module DateAndTimeFormatting
2
+
3
+ def self.included(base)
4
+ base.class_eval do
5
+ include DateAndTimeFormatting::InstanceMethods
6
+ include OrdinalizedFormatting
7
+ extend DateAndTimeFormatting::ClassMethods
8
+ end
9
+ end
10
+
11
+ module InstanceMethods
12
+
13
+
14
+ # Format a date/time instance using a defined format
15
+ #
16
+ # ==== Parameters
17
+ # format<Symbol>:: of the format key from Date.date_formats
18
+ #
19
+ # ==== Returns
20
+ # String:: formattred string
21
+ #
22
+ #--
23
+ # @public
24
+ def formatted(format = :default)
25
+ self.strftime(Date.formats[format])
26
+ end
27
+
28
+ end
29
+
30
+ module ClassMethods
31
+
32
+ @@formats = {
33
+ :db => "%Y-%m-%d %H:%M:%S",
34
+ :time => "%H:%M", # 21:12
35
+ :date => "%Y-%m-%d", # 2008-12-04
36
+ :short => "%d %b %H:%M", # 01 Sep 21:12
37
+ :long => "%B %d, %Y %H:%M",
38
+ :rfc822 => "%a, %d %b %Y %H:%M:%S %z"
39
+ }
40
+
41
+ # Lists the date and time formats
42
+ #
43
+ # ==== Returns
44
+ # Hash:: a hash with all formats available
45
+ # --
46
+ # @public
47
+ def formats
48
+ @@formats
49
+ end
50
+
51
+ # Adds a date and time format
52
+ #
53
+ # ==== Parameters
54
+ # key<Symbol>:: name of the format
55
+ # format<Hash>:: time format to use
56
+ #
57
+ # ==== Returns
58
+ # Hash:: a hash with all formats available
59
+ # --
60
+ # @public
61
+ def add_format(key, format)
62
+ formats.merge!({key => format})
63
+ end
64
+
65
+
66
+ # Resets the date and time formats
67
+ # --
68
+ # @private
69
+ def reset_formats
70
+ original_formats = [:db, :time, :short, :date, :long, :long_ordinal, :rfc822]
71
+ formats = @@formats.delete_if{|format, v| !original_formats.include?(format)}
72
+ end
73
+
74
+ end
75
+
76
+ end
77
+
78
+ module Ordinalize
79
+ # Ordinalize turns a number into an ordinal string used to denote the
80
+ # position in an ordered sequence such as 1st, 2nd, 3rd, 4th.
81
+ #
82
+ # Examples
83
+ # 1.ordinalize # => "1st"
84
+ # 2.ordinalize # => "2nd"
85
+ # 1002.ordinalize # => "1002nd"
86
+ # 1003.ordinalize # => "1003rd"
87
+ def ordinalize
88
+ if (11..13).include?(self % 100)
89
+ "#{self}th"
90
+ else
91
+ case self % 10
92
+ when 1; "#{self}st"
93
+ when 2; "#{self}nd"
94
+ when 3; "#{self}rd"
95
+ else "#{self}th"
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ Integer.send :include, Ordinalize
102
+
103
+ # Time.now.to_ordinalized_s :long
104
+ # => "February 28th, 2006 21:10"
105
+ module OrdinalizedFormatting
106
+
107
+ def to_ordinalized_s(format = :default)
108
+ format = Date.formats[format]
109
+ return self.to_s if format.nil?
110
+ strftime_ordinalized(format)
111
+ end
112
+
113
+ # Gives you a relative date in an attractive format
114
+ #
115
+ # ==== Parameters
116
+ # format<String>:: strftime string used to formatt a time/date object
117
+ # locale<String, Symbol>:: An optional value which can be used by localization plugins
118
+ #
119
+ # ==== Returns
120
+ # String:: Ordinalized time/date object
121
+ #
122
+ # ==== Examples
123
+ # 5.days.ago.strftime_ordinalized('%b %d, %Y') # =>
124
+ def strftime_ordinalized(fmt, format=nil)
125
+ strftime(fmt.gsub(/(^|[^-])%d/, '\1_%d_')).gsub(/_(\d+)_/) { |s| s.to_i.ordinalize }
126
+ end
127
+ end
@@ -0,0 +1,190 @@
1
+ module Merb
2
+ module Helpers
3
+ # Provides a number of methods for displaying and dealing with dates and times
4
+ #
5
+ # Parts were strongly based on http://ar-code.svn.engineyard.com/plugins/relative_time_helpers/, and
6
+ # active_support
7
+ #
8
+ # The key methods are `relative_date`, `relative_date_span`, and `relative_time_span`. This also gives
9
+ # you the Rails style Time DSL for working with numbers eg. 3.months.ago or 5.days.until(1.year.from_now)
10
+ module DateAndTime
11
+ @@time_class = Time
12
+ @@time_output = {
13
+ :today => 'today',
14
+ :yesterday => 'yesterday',
15
+ :tomorrow => 'tomorrow',
16
+ :initial_format => '%b %d',
17
+ :year_format => ', %Y'
18
+ }
19
+
20
+ def self.time_class
21
+ @@time_class
22
+ end
23
+
24
+ # ==== Parameters
25
+ # format<Symbol>:: time format to use
26
+ # locale<String, Symbol>:: An optional value which can be used by localization plugins
27
+ #
28
+ # ==== Returns
29
+ # String:: a string used to format time using #strftime
30
+ def self.time_output(format, locale=nil)
31
+ @@time_output[format]
32
+ end
33
+
34
+ # Gives you a relative date in an attractive format
35
+ #
36
+ # ==== Parameters
37
+ # time<~to_date>:: The Date or Time to test
38
+ # locale<String, Symbol>:: An optional value which can be used by localization plugins
39
+ #
40
+ # ==== Returns
41
+ # String:: Relative date
42
+ #
43
+ # ==== Examples
44
+ # relative_date(Time.now.utc) => "today"
45
+ # relative_date(5.days.ago) => "March 5th"
46
+ # relative_date(1.year.ago) => "March 10th, 2007"
47
+ def relative_date(time, locale=nil)
48
+ date = time.to_date
49
+ today = DateAndTime.time_class.now.to_date
50
+ if date == today
51
+ DateAndTime.time_output(:today, locale)
52
+ elsif date == (today - 1)
53
+ DateAndTime.time_output(:yesterday, locale)
54
+ elsif date == (today + 1)
55
+ DateAndTime.time_output(:tomorrow, locale)
56
+ else
57
+ fmt = DateAndTime.time_output(:initial_format, locale).dup
58
+ fmt << DateAndTime.time_output(:year_format, locale) unless date.year == today.year
59
+ time.strftime_ordinalized(fmt, locale)
60
+ end
61
+ end
62
+
63
+ # Gives you a relative date span in an attractive format
64
+ #
65
+ # ==== Parameters
66
+ # times<~first,~last>:: The Dates or Times to test
67
+ #
68
+ # ==== Returns
69
+ # String:: The sexy relative date span
70
+ #
71
+ # ==== Examples
72
+ # relative_date([1.second.ago, 10.seconds.ago]) => "March 10th"
73
+ # relative_date([1.year.ago, 1.year.ago) => "March 10th, 2007"
74
+ # relative_date([Time.now, 1.day.from_now]) => "March 10th - 11th"
75
+ # relative_date([Time.now, 1.year.ago]) => "March 10th, 2007 - March 10th, 2008"
76
+ def relative_date_span(times)
77
+ times = [times.first, times.last].collect! { |t| t.to_date }
78
+ times.sort!
79
+ if times.first == times.last
80
+ relative_date(times.first)
81
+ else
82
+ first = times.first; last = times.last; now = DateAndTime.time_class.now
83
+ arr = [first.strftime_ordinalized('%b %d')]
84
+ arr << ", #{first.year}" unless first.year == last.year
85
+ arr << ' - '
86
+ arr << last.strftime('%b') << ' ' unless first.year == last.year && first.month == last.month
87
+ arr << last.day.ordinalize
88
+ arr << ", #{last.year}" unless first.year == last.year && last.year == now.year
89
+ arr.to_s
90
+ end
91
+ end
92
+
93
+ # Gives you a relative date span in an attractive format
94
+ #
95
+ # ==== Parameters
96
+ # times<~first,~last>:: The Dates or Times to test
97
+ #
98
+ # ==== Returns
99
+ # String:: The sexy relative time span
100
+ #
101
+ # ==== Examples
102
+ # relative_time_span([1.second.ago, 10.seconds.ago]) => "12:00 - 12:09 AM March 10th"
103
+ # relative_time_span([1.year.ago, 1.year.ago) => "12:09 AM March 10th, 2007"
104
+ # relative_time_span([Time.now, 13.hours.from_now]) => "12:09 AM - 1:09 PM March 10th"
105
+ # relative_time_span([Time.now, 1.year.ago]) => "12:09 AM March 10th, 2007 - 12:09 AM March 10th, 2008"
106
+ def relative_time_span(times)
107
+ times = [times.first, times.last].collect! { |t| t.to_time }
108
+ times.sort!
109
+ if times.first == times.last
110
+ "#{prettier_time(times.first)} #{relative_date(times.first)}"
111
+ elsif times.first.to_date == times.last.to_date
112
+ same_half = (times.first.hour/12 == times.last.hour/12)
113
+ "#{prettier_time(times.first, !same_half)} - #{prettier_time(times.last)} #{relative_date(times.first)}"
114
+
115
+ else
116
+ first = times.first; last = times.last; now = DateAndTime.time_class.now
117
+ arr = [prettier_time(first)]
118
+ arr << ' '
119
+ arr << first.strftime_ordinalized('%b %d')
120
+ arr << ", #{first.year}" unless first.year == last.year
121
+ arr << ' - '
122
+ arr << prettier_time(last)
123
+ arr << ' '
124
+ arr << last.strftime('%b') << ' ' unless first.year == last.year && first.month == last.month
125
+ arr << last.day.ordinalize
126
+ arr << ", #{last.year}" unless first.year == last.year && last.year == now.year
127
+ arr.to_s
128
+ end
129
+ end
130
+
131
+ # Condenses time... very similar to time_ago_in_words in ActionPack
132
+ #
133
+ # ==== Parameters
134
+ # from_time<~to_time>:: The Date or Time to start from
135
+ # to_time<~to_time>:: The Date or Time to go to, Defaults to Time.now.utc
136
+ # include_seconds<Boolean>:: Count the seconds initially, Defaults to false
137
+ # locale<String, Symbol>:: An optional value which can be used by localization plugins
138
+ #
139
+ # ==== Returns
140
+ # String:: The time distance
141
+ #
142
+ # ==== Examples
143
+ # time_lost_in_words(3.minutes.from_now) # => 3 minutes
144
+ # time_lost_in_words(Time.now - 15.hours) # => 15 hours
145
+ # time_lost_in_words(Time.now, 3.minutes.from_now) # => 3 minutes
146
+ # time_lost_in_words(Time.now) # => less than a minute
147
+ # time_lost_in_words(Time.now, Time.now, true) # => less than 5 seconds
148
+ #
149
+ def time_lost_in_words(from_time, to_time = Time.now.utc, include_seconds = false, locale=nil)
150
+ from_time = from_time.to_time if from_time.respond_to?(:to_time)
151
+ to_time = to_time.to_time if to_time.respond_to?(:to_time)
152
+ distance_in_minutes = (((to_time - from_time).abs)/60).round
153
+ distance_in_seconds = ((to_time - from_time).abs).round
154
+
155
+ case distance_in_minutes
156
+ when 0..1
157
+ return (distance_in_minutes == 0) ? 'less than a minute' : '1 minute' unless include_seconds
158
+ case distance_in_seconds
159
+ when 0..4 then 'less than 5 seconds'
160
+ when 5..9 then 'less than 10 seconds'
161
+ when 10..19 then 'less than 20 seconds'
162
+ when 20..39 then 'half a minute'
163
+ when 40..59 then 'less than a minute'
164
+ else '1 minute'
165
+ end
166
+
167
+ when 2..44 then "#{distance_in_minutes} minutes"
168
+ when 45..89 then 'about 1 hour'
169
+ when 90..1439 then "about #{(distance_in_minutes.to_f / 60.0).round} hours"
170
+ when 1440..2879 then '1 day'
171
+ when 2880..43199 then "#{(distance_in_minutes / 1440).round} days"
172
+ when 43200..86399 then 'about 1 month'
173
+ when 86400..525599 then "#{(distance_in_minutes / 43200).round} months"
174
+ when 525600..1051199 then 'about 1 year'
175
+ else "over #{(distance_in_minutes / 525600).round} years"
176
+ end
177
+ end
178
+ alias :time_ago_in_words :time_lost_in_words
179
+
180
+ def prettier_time(time, ampm=true, locale=nil)
181
+ time.strftime("%I:%M#{" %p" if ampm}").sub(/^0/, '')
182
+ end
183
+ end
184
+ end
185
+ end
186
+
187
+ class Merb::Controller
188
+ include Merb::Helpers::DateAndTime
189
+ end
190
+
@@ -0,0 +1,410 @@
1
+ load File.dirname(__FILE__) / ".." / "tag_helpers.rb"
2
+
3
+ module Merb::Helpers::Form::Builder
4
+
5
+ class Base
6
+ include Merb::Helpers::Tag
7
+
8
+ def initialize(obj, name, origin)
9
+ @obj, @origin = obj, origin
10
+ @name = name || @obj.class.name.snake_case.split("/").last
11
+ end
12
+
13
+ def concat(attrs, &blk)
14
+ @origin.concat(@origin.capture(&blk), blk.binding)
15
+ end
16
+
17
+ def form(attrs = {}, &blk)
18
+ captured = @origin.capture(&blk)
19
+ fake_method_tag = process_form_attrs(attrs)
20
+ tag(:form, fake_method_tag + captured, attrs)
21
+ end
22
+
23
+ def fieldset(attrs, &blk)
24
+ legend = (l_attr = attrs.delete(:legend)) ? tag(:legend, l_attr) : ""
25
+ tag(:fieldset, legend + @origin.capture(&blk), attrs)
26
+ # @origin.concat(contents, blk.binding)
27
+ end
28
+
29
+ %w(text password hidden file).each do |kind|
30
+ self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
31
+ def bound_#{kind}_field(method, attrs = {})
32
+ name = control_name(method)
33
+ update_bound_controls(method, attrs, "#{kind}")
34
+ unbound_#{kind}_field({:name => name, :value => @obj.send(method)}.merge(attrs))
35
+ end
36
+
37
+ def unbound_#{kind}_field(attrs)
38
+ update_unbound_controls(attrs, "#{kind}")
39
+ self_closing_tag(:input, {:type => "#{kind}"}.merge(attrs))
40
+ end
41
+ RUBY
42
+ end
43
+
44
+ def bound_check_box(method, attrs = {})
45
+ name = control_name(method)
46
+ update_bound_controls(method, attrs, "checkbox")
47
+ unbound_check_box({:name => name}.merge(attrs))
48
+ end
49
+
50
+ def unbound_check_box(attrs)
51
+ update_unbound_controls(attrs, "checkbox")
52
+ if attrs.delete(:boolean)
53
+ on, off = attrs.delete(:on), attrs.delete(:off)
54
+ unbound_hidden_field(:name => attrs[:name], :value => off) <<
55
+ self_closing_tag(:input, {:type => "checkbox", :value => on}.merge(attrs))
56
+ else
57
+ self_closing_tag(:input, {:type => "checkbox"}.merge(attrs))
58
+ end
59
+ end
60
+
61
+ def bound_radio_button(method, attrs = {})
62
+ name = control_name(method)
63
+ update_bound_controls(method, attrs, "radio")
64
+ unbound_radio_button({:name => name, :value => @obj.send(method)}.merge(attrs))
65
+ end
66
+
67
+ def unbound_radio_button(attrs)
68
+ update_unbound_controls(attrs, "radio")
69
+ self_closing_tag(:input, {:type => "radio"}.merge(attrs))
70
+ end
71
+
72
+ def bound_radio_group(method, arr)
73
+ val = @obj.send(method)
74
+ arr.map do |attrs|
75
+ attrs = {:value => attrs} unless attrs.is_a?(Hash)
76
+ attrs[:checked] ||= (val == attrs[:value])
77
+ radio_group_item(method, attrs)
78
+ end.join
79
+ end
80
+
81
+ def unbound_radio_group(arr, attrs = {})
82
+ arr.map do |ind_attrs|
83
+ ind_attrs = {:value => ind_attrs} unless ind_attrs.is_a?(Hash)
84
+ joined = attrs.merge(ind_attrs)
85
+ joined.merge!(:label => joined[:label] || joined[:value])
86
+ unbound_radio_button(joined)
87
+ end.join
88
+ end
89
+
90
+ def bound_select(method, attrs = {})
91
+ name = control_name(method)
92
+ update_bound_controls(method, attrs, "select")
93
+ unbound_select({:name => name}.merge(attrs))
94
+ end
95
+
96
+ def unbound_select(attrs = {})
97
+ update_unbound_controls(attrs, "select")
98
+ attrs[:name] << "[]" if attrs[:multiple] && !(attrs[:name] =~ /\[\]$/)
99
+ tag(:select, options_for(attrs), attrs)
100
+ end
101
+
102
+ def bound_text_area(method, attrs = {})
103
+ name = "#{@name}[#{method}]"
104
+ update_bound_controls(method, attrs, "text_area")
105
+ unbound_text_area(@obj.send(method), {:name => name}.merge(attrs))
106
+ end
107
+
108
+ def unbound_text_area(contents, attrs)
109
+ update_unbound_controls(attrs, "text_area")
110
+ tag(:textarea, contents, attrs)
111
+ end
112
+
113
+ def button(contents, attrs)
114
+ update_unbound_controls(attrs, "button")
115
+ tag(:button, contents, attrs)
116
+ end
117
+
118
+ def submit(value, attrs)
119
+ attrs[:type] ||= "submit"
120
+ attrs[:name] ||= "submit"
121
+ attrs[:value] ||= value
122
+ update_unbound_controls(attrs, "submit")
123
+ self_closing_tag(:input, attrs)
124
+ end
125
+
126
+ private
127
+
128
+ def process_form_attrs(attrs)
129
+ method = attrs[:method]
130
+
131
+ # Unless the method is :get, fake out the method using :post
132
+ attrs[:method] = :post unless attrs[:method] == :get
133
+ # Use a fake PUT if the object is not new, otherwise use the method
134
+ # passed in. Defaults to :post if no method is set.
135
+ method ||= (@obj.respond_to?(:new_record?) && !@obj.new_record?) || (@obj.respond_to?(:new?) && !@obj.new?) ? :put : :post
136
+
137
+ attrs[:enctype] = "multipart/form-data" if attrs.delete(:multipart) || @multipart
138
+
139
+ method == :post || method == :get ? "" : fake_out_method(attrs, method)
140
+ end
141
+
142
+ # This can be overridden to use another method to fake out methods
143
+ def fake_out_method(attrs, method)
144
+ self_closing_tag(:input, :type => "hidden", :name => "_method", :value => method)
145
+ end
146
+
147
+ def update_bound_controls(method, attrs, type)
148
+ case type
149
+ when "checkbox"
150
+ update_bound_check_box(method, attrs)
151
+ when "select"
152
+ update_bound_select(method, attrs)
153
+ end
154
+ end
155
+
156
+ def update_bound_check_box(method, attrs)
157
+ raise ArgumentError, ":value can't be used with a bound_check_box" if attrs.has_key?(:value)
158
+
159
+ attrs[:boolean] = attrs.fetch(:boolean, true)
160
+
161
+ val = @obj.send(method)
162
+ attrs[:checked] = attrs.key?(:on) ? val == attrs[:on] : considered_true?(val)
163
+ end
164
+
165
+ def update_bound_select(method, attrs)
166
+ attrs[:value_method] ||= method
167
+ attrs[:text_method] ||= attrs[:value_method] || :to_s
168
+ attrs[:selected] ||= @obj.send(attrs[:value_method])
169
+ end
170
+
171
+ def update_unbound_controls(attrs, type)
172
+ case type
173
+ when "checkbox"
174
+ update_unbound_check_box(attrs)
175
+ when "file"
176
+ @multipart = true
177
+ end
178
+
179
+ attrs[:disabled] ? attrs[:disabled] = "disabled" : attrs.delete(:disabled)
180
+ end
181
+
182
+ def update_unbound_check_box(attrs)
183
+ boolean = attrs[:boolean] || (attrs[:on] && attrs[:off]) ? true : false
184
+
185
+ case
186
+ when attrs.key?(:on) ^ attrs.key?(:off)
187
+ raise ArgumentError, ":on and :off must be specified together"
188
+ when (attrs[:boolean] == false) && (attrs.key?(:on))
189
+ raise ArgumentError, ":boolean => false cannot be used with :on and :off"
190
+ when boolean && attrs.key?(:value)
191
+ raise ArgumentError, ":value can't be used with a boolean checkbox"
192
+ end
193
+
194
+ if attrs[:boolean] = boolean
195
+ attrs[:on] ||= "1"; attrs[:off] ||= "0"
196
+ end
197
+
198
+ attrs[:checked] = "checked" if attrs.delete(:checked)
199
+ end
200
+
201
+ # Accepts a collection (hash, array, enumerable, your type) and returns a string of option tags.
202
+ # Given a collection where the elements respond to first and last (such as a two-element array),
203
+ # the "lasts" serve as option values and the "firsts" as option text. Hashes are turned into
204
+ # this form automatically, so the keys become "firsts" and values become lasts. If selected is
205
+ # specified, the matching "last" or element will get the selected option-tag. Selected may also
206
+ # be an array of values to be selected when using a multiple select.
207
+ #
208
+ # ==== Parameters
209
+ # attrs<Hash>:: HTML attributes and options
210
+ #
211
+ # ==== Options
212
+ # +selected+:: The value of a selected object, which may be either a string or an array.
213
+ # +prompt+:: Adds an addtional option tag with the provided string with no value.
214
+ # +include_blank+:: Adds an additional blank option tag with no value.
215
+ #
216
+ # ==== Returns
217
+ # String:: HTML
218
+ #
219
+ # ==== Examples
220
+ # <%= options_for [["apple", "Apple Pie"], ["orange", "Orange Juice"]], :selected => "orange"
221
+ # => <option value="apple">Apple Pie</option><option value="orange" selected="selected">Orange Juice</option>
222
+ #
223
+ # <%= options_for [["apple", "Apple Pie"], ["orange", "Orange Juice"]], :selected => ["orange", "apple"], :prompt => "Select One"
224
+ # => <option value="">Select One</option><option value="apple" selected="selected">Apple Pie</option><option value="orange" selected="selected">Orange Juice</option>
225
+ def options_for(attrs)
226
+ blank, prompt = attrs.delete(:include_blank), attrs.delete(:prompt)
227
+ b = blank || prompt ? tag(:option, prompt || "", :value => "") : ""
228
+
229
+ # yank out the options attrs
230
+ collection, selected, text_method, value_method =
231
+ attrs.extract!(:collection, :selected, :text_method, :value_method)
232
+
233
+ # if the collection is a Hash, optgroups are a-coming
234
+ if collection.is_a?(Hash)
235
+ ([b] + collection.map do |g,col|
236
+ tag(:optgroup, options(col, text_method, value_method, selected), :label => g)
237
+ end).join
238
+ else
239
+ options(collection || [], text_method, value_method, selected, b)
240
+ end
241
+ end
242
+
243
+ def options(col, text_meth, value_meth, sel, b = nil)
244
+ ([b] + col.map do |item|
245
+ text_meth = text_meth && item.respond_to?(text_meth) ? text_meth : :last
246
+ value_meth = value_meth && item.respond_to?(value_meth) ? value_meth : :first
247
+
248
+ text = item.is_a?(String) ? item : item.send(text_meth)
249
+ value = item.is_a?(String) ? item : item.send(value_meth)
250
+
251
+ option_attrs = {:value => value}
252
+ if sel.is_a?(Array)
253
+ option_attrs.merge!(:selected => "selected") if value.in? sel
254
+ else
255
+ option_attrs.merge!(:selected => "selected") if value == sel
256
+ end
257
+ tag(:option, text, option_attrs)
258
+ end).join
259
+ end
260
+
261
+ def radio_group_item(method, attrs)
262
+ attrs.merge!(:checked => "checked") if attrs[:checked]
263
+ bound_radio_button(method, attrs)
264
+ end
265
+
266
+ def considered_true?(value)
267
+ value && value != "0" && value != 0
268
+ end
269
+
270
+ def control_name(method)
271
+ "#{@name}[#{method}]"
272
+ end
273
+
274
+ def add_css_class(attrs, new_class)
275
+ attrs[:class] = attrs[:class] ? "#{attrs[:class]} #{new_class}" : new_class
276
+ end
277
+ end
278
+
279
+ class Form < Base
280
+ def label(contents, attrs = {})
281
+ if contents.is_a?(Hash)
282
+ attrs = contents
283
+ contents = attrs.delete(:label)
284
+ end
285
+ if contents
286
+ for_attr = attrs[:id] ? {:for => attrs[:id]} : {}
287
+ if contents.is_a?(Hash)
288
+ attrs = contents
289
+ contents = attrs.delete(:title)
290
+ for_attr = for_attr.merge(attrs)
291
+ end
292
+ tag(:label, contents, for_attr)
293
+ else
294
+ ""
295
+ end
296
+ end
297
+
298
+ %w(text password file).each do |kind|
299
+ self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
300
+ def unbound_#{kind}_field(attrs = {})
301
+ label(attrs) + super
302
+ end
303
+ RUBY
304
+ end
305
+
306
+ def unbound_check_box(attrs = {})
307
+ label_text = label(attrs)
308
+ super + label_text
309
+ end
310
+
311
+ def unbound_hidden_field(attrs = {})
312
+ attrs.delete(:label)
313
+ super
314
+ end
315
+
316
+ def unbound_radio_button(attrs = {})
317
+ label_text = label(attrs)
318
+ super + label_text
319
+ end
320
+
321
+ def unbound_select(attrs = {})
322
+ label(attrs) + super
323
+ end
324
+
325
+ def unbound_text_area(contents, attrs = {})
326
+ label(attrs) + super
327
+ end
328
+
329
+ def button(contents, attrs = {})
330
+ label(attrs) + super
331
+ end
332
+
333
+ def submit(value, attrs = {})
334
+ label(attrs) + super
335
+ end
336
+
337
+ private
338
+
339
+ def update_bound_controls(method, attrs, type)
340
+ attrs.merge!(:id => "#{@name}_#{method}") unless attrs[:id]
341
+ super
342
+ end
343
+
344
+ def update_unbound_controls(attrs, type)
345
+ case type
346
+ when "text", "radio", "password", "hidden", "checkbox", "file"
347
+ add_css_class(attrs, type)
348
+ end
349
+ super
350
+ end
351
+
352
+ def radio_group_item(method, attrs)
353
+ unless attrs[:id]
354
+ attrs.merge!(:id => "#{@name}_#{method}_#{attrs[:value]}")
355
+ end
356
+
357
+ attrs.merge!(:label => attrs[:label] || attrs[:value])
358
+ super
359
+ end
360
+ end
361
+
362
+ module Errorifier
363
+ def error_messages_for(obj, error_class, build_li, header, before)
364
+ obj ||= @obj
365
+ return "" unless obj.respond_to?(:errors)
366
+
367
+ sequel = !obj.errors.respond_to?(:each)
368
+ errors = sequel ? obj.errors.full_messages : obj.errors
369
+
370
+ return "" if errors.empty?
371
+
372
+ header_message = header % [errors.size, errors.size == 1 ? "" : "s"]
373
+ markup = %Q{<div class='#{error_class}'>#{header_message}<ul>}
374
+ errors.each {|err| markup << (build_li % (sequel ? err : err.join(" ")))}
375
+ markup << %Q{</ul></div>}
376
+ end
377
+
378
+ private
379
+
380
+ def update_bound_controls(method, attrs, type)
381
+ if @obj.errors.on(method.to_sym)
382
+ add_css_class(attrs, "error")
383
+ end
384
+ super
385
+ end
386
+ end
387
+
388
+ class FormWithErrors < Form
389
+ include Errorifier
390
+ end
391
+
392
+ module Resourceful
393
+ private
394
+
395
+ def process_form_attrs(attrs)
396
+ attrs[:action] ||= @origin.url(@name, @obj) if @origin
397
+ super
398
+ end
399
+ end
400
+
401
+ class ResourcefulForm < Form
402
+ include Resourceful
403
+ end
404
+
405
+ class ResourcefulFormWithErrors < FormWithErrors
406
+ include Errorifier
407
+ include Resourceful
408
+ end
409
+
410
+ end