merb-helpers 0.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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