dry_crud 1.7.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (158) hide show
  1. checksums.yaml +15 -0
  2. data/MIT-LICENSE +1 -1
  3. data/README.rdoc +126 -87
  4. data/VERSION +1 -1
  5. data/lib/generators/dry_crud/dry_crud_generator.rb +42 -22
  6. data/lib/generators/dry_crud/templates/INSTALL +5 -5
  7. data/lib/generators/dry_crud/templates/app/assets/stylesheets/crud.scss +0 -20
  8. data/lib/generators/dry_crud/templates/app/assets/stylesheets/sample.scss +24 -4
  9. data/lib/generators/dry_crud/templates/app/controllers/crud/generic_model.rb +89 -0
  10. data/lib/generators/dry_crud/templates/app/controllers/crud/nestable.rb +70 -0
  11. data/lib/generators/dry_crud/templates/app/controllers/crud/rememberable.rb +64 -0
  12. data/lib/generators/dry_crud/templates/app/controllers/crud/render_callbacks.rb +46 -0
  13. data/lib/generators/dry_crud/templates/app/controllers/crud/responder.rb +31 -0
  14. data/lib/generators/dry_crud/templates/app/controllers/crud/searchable.rb +55 -0
  15. data/lib/generators/dry_crud/templates/app/controllers/crud/sortable.rb +63 -0
  16. data/lib/generators/dry_crud/templates/app/controllers/crud_controller.rb +66 -69
  17. data/lib/generators/dry_crud/templates/app/controllers/list_controller.rb +23 -326
  18. data/lib/generators/dry_crud/templates/app/helpers/actions_helper.rb +64 -0
  19. data/lib/generators/dry_crud/templates/app/helpers/crud/form_builder.rb +331 -0
  20. data/lib/generators/dry_crud/templates/app/helpers/crud/table_builder.rb +280 -0
  21. data/lib/generators/dry_crud/templates/app/helpers/form_helper.rb +52 -0
  22. data/lib/generators/dry_crud/templates/app/helpers/format_helper.rb +164 -0
  23. data/lib/generators/dry_crud/templates/app/helpers/i18n_helper.rb +85 -0
  24. data/lib/generators/dry_crud/templates/app/helpers/table_helper.rb +83 -0
  25. data/lib/generators/dry_crud/templates/app/helpers/utility_helper.rb +84 -0
  26. data/lib/generators/dry_crud/templates/app/views/crud/_actions_edit.html.erb +3 -3
  27. data/lib/generators/dry_crud/templates/app/views/crud/_actions_edit.html.haml +3 -3
  28. data/lib/generators/dry_crud/templates/app/views/crud/_actions_index.html.erb +1 -1
  29. data/lib/generators/dry_crud/templates/app/views/crud/_actions_index.html.haml +1 -1
  30. data/lib/generators/dry_crud/templates/app/views/crud/_actions_show.html.erb +3 -3
  31. data/lib/generators/dry_crud/templates/app/views/crud/_actions_show.html.haml +3 -3
  32. data/lib/generators/dry_crud/templates/app/views/crud/_attrs.html.erb +1 -1
  33. data/lib/generators/dry_crud/templates/app/views/crud/_attrs.html.haml +1 -1
  34. data/lib/generators/dry_crud/templates/app/views/crud/_form.html.erb +1 -1
  35. data/lib/generators/dry_crud/templates/app/views/crud/_form.html.haml +1 -1
  36. data/lib/generators/dry_crud/templates/app/views/crud/edit.html.erb +1 -1
  37. data/lib/generators/dry_crud/templates/app/views/crud/edit.html.haml +1 -1
  38. data/lib/generators/dry_crud/templates/app/views/crud/new.html.erb +2 -2
  39. data/lib/generators/dry_crud/templates/app/views/crud/new.html.haml +2 -2
  40. data/lib/generators/dry_crud/templates/app/views/crud/show.html.erb +1 -1
  41. data/lib/generators/dry_crud/templates/app/views/crud/show.html.haml +1 -1
  42. data/lib/generators/dry_crud/templates/app/views/layouts/_flash.html.haml +1 -1
  43. data/lib/generators/dry_crud/templates/app/views/layouts/_nav.html.erb +1 -1
  44. data/lib/generators/dry_crud/templates/app/views/layouts/_nav.html.haml +1 -1
  45. data/lib/generators/dry_crud/templates/app/views/layouts/crud.html.erb +15 -7
  46. data/lib/generators/dry_crud/templates/app/views/layouts/crud.html.haml +18 -8
  47. data/lib/generators/dry_crud/templates/app/views/list/_search.html.erb +3 -3
  48. data/lib/generators/dry_crud/templates/app/views/list/_search.html.haml +3 -3
  49. data/lib/generators/dry_crud/templates/app/views/list/index.html.erb +1 -1
  50. data/lib/generators/dry_crud/templates/app/views/list/index.html.haml +1 -1
  51. data/lib/generators/dry_crud/templates/app/views/shared/_error_messages.html.erb +1 -1
  52. data/lib/generators/dry_crud/templates/app/views/shared/_error_messages.html.haml +1 -1
  53. data/lib/generators/dry_crud/templates/app/views/shared/_labeled.html.erb +2 -4
  54. data/lib/generators/dry_crud/templates/app/views/shared/_labeled.html.haml +2 -3
  55. data/lib/generators/dry_crud/templates/config/initializers/field_error_proc.rb +5 -1
  56. data/lib/generators/dry_crud/templates/config/locales/crud.de.yml +64 -0
  57. data/lib/generators/dry_crud/templates/config/locales/{en_crud.yml → crud.en.yml} +2 -2
  58. data/lib/generators/dry_crud/templates/spec/controllers/crud_test_models_controller_spec.rb +241 -231
  59. data/lib/generators/dry_crud/templates/spec/helpers/crud/form_builder_spec.rb +226 -0
  60. data/lib/generators/dry_crud/templates/spec/helpers/{standard_table_builder_spec.rb → crud/table_builder_spec.rb} +36 -34
  61. data/lib/generators/dry_crud/templates/spec/helpers/form_helper_spec.rb +238 -0
  62. data/lib/generators/dry_crud/templates/spec/helpers/format_helper_spec.rb +244 -0
  63. data/lib/generators/dry_crud/templates/spec/helpers/i18n_helper_spec.rb +132 -0
  64. data/lib/generators/dry_crud/templates/spec/helpers/table_helper_spec.rb +265 -0
  65. data/lib/generators/dry_crud/templates/spec/helpers/utility_helper_spec.rb +74 -0
  66. data/lib/generators/dry_crud/templates/spec/support/crud_controller_examples.rb +185 -100
  67. data/lib/generators/dry_crud/templates/spec/support/crud_controller_test_helper.rb +58 -49
  68. data/lib/generators/dry_crud/templates/test/{functional → controllers}/crud_test_models_controller_test.rb +112 -91
  69. data/lib/generators/dry_crud/templates/test/{unit/helpers/standard_form_builder_test.rb → helpers/crud/form_builder_test.rb} +79 -62
  70. data/lib/generators/dry_crud/templates/test/{unit/helpers/standard_table_builder_test.rb → helpers/crud/table_builder_test.rb} +31 -28
  71. data/lib/generators/dry_crud/templates/test/helpers/custom_assertions_test.rb +85 -0
  72. data/lib/generators/dry_crud/templates/test/helpers/form_helper_test.rb +129 -0
  73. data/lib/generators/dry_crud/templates/test/helpers/format_helper_test.rb +163 -0
  74. data/lib/generators/dry_crud/templates/test/helpers/i18n_helper_test.rb +79 -0
  75. data/lib/generators/dry_crud/templates/test/helpers/table_helper_test.rb +217 -0
  76. data/lib/generators/dry_crud/templates/test/helpers/utility_helper_test.rb +63 -0
  77. data/lib/generators/dry_crud/templates/test/{functional → support}/crud_controller_test_helper.rb +70 -59
  78. data/lib/generators/dry_crud/templates/test/{crud_test_model.rb → support/crud_test_model.rb} +107 -75
  79. data/lib/generators/dry_crud/templates/test/support/custom_assertions.rb +83 -0
  80. metadata +83 -146
  81. data/Rakefile +0 -211
  82. data/lib/generators/dry_crud/templates/app/helpers/crud_helper.rb +0 -168
  83. data/lib/generators/dry_crud/templates/app/helpers/list_helper.rb +0 -27
  84. data/lib/generators/dry_crud/templates/app/helpers/standard_form_builder.rb +0 -261
  85. data/lib/generators/dry_crud/templates/app/helpers/standard_helper.rb +0 -304
  86. data/lib/generators/dry_crud/templates/app/helpers/standard_table_builder.rb +0 -178
  87. data/lib/generators/dry_crud/templates/spec/helpers/crud_helper_spec.rb +0 -146
  88. data/lib/generators/dry_crud/templates/spec/helpers/list_helper_spec.rb +0 -154
  89. data/lib/generators/dry_crud/templates/spec/helpers/standard_form_builder_spec.rb +0 -215
  90. data/lib/generators/dry_crud/templates/spec/helpers/standard_helper_spec.rb +0 -387
  91. data/lib/generators/dry_crud/templates/test/custom_assertions.rb +0 -78
  92. data/lib/generators/dry_crud/templates/test/unit/custom_assertions_test.rb +0 -117
  93. data/lib/generators/dry_crud/templates/test/unit/helpers/crud_helper_test.rb +0 -111
  94. data/lib/generators/dry_crud/templates/test/unit/helpers/list_helper_test.rb +0 -123
  95. data/lib/generators/dry_crud/templates/test/unit/helpers/standard_helper_test.rb +0 -281
  96. data/test/templates/Gemfile +0 -46
  97. data/test/templates/app/controllers/admin/cities_controller.rb +0 -7
  98. data/test/templates/app/controllers/admin/countries_controller.rb +0 -13
  99. data/test/templates/app/controllers/ajax_controller.rb +0 -9
  100. data/test/templates/app/controllers/people_controller.rb +0 -13
  101. data/test/templates/app/controllers/vips_controller.rb +0 -19
  102. data/test/templates/app/helpers/cities_helper.rb +0 -9
  103. data/test/templates/app/helpers/people_helper.rb +0 -8
  104. data/test/templates/app/models/city.rb +0 -28
  105. data/test/templates/app/models/country.rb +0 -16
  106. data/test/templates/app/models/person.rb +0 -12
  107. data/test/templates/app/views/admin/cities/_actions_index.html.erb +0 -2
  108. data/test/templates/app/views/admin/cities/_actions_index.html.haml +0 -2
  109. data/test/templates/app/views/admin/cities/_attrs.html.erb +0 -1
  110. data/test/templates/app/views/admin/cities/_attrs.html.haml +0 -1
  111. data/test/templates/app/views/admin/cities/_form.html.erb +0 -7
  112. data/test/templates/app/views/admin/cities/_form.html.haml +0 -5
  113. data/test/templates/app/views/admin/cities/_hello.html.erb +0 -1
  114. data/test/templates/app/views/admin/cities/_hello.html.haml +0 -1
  115. data/test/templates/app/views/admin/cities/_list.html.erb +0 -3
  116. data/test/templates/app/views/admin/cities/_list.html.haml +0 -3
  117. data/test/templates/app/views/admin/countries/_list.html.erb +0 -4
  118. data/test/templates/app/views/admin/countries/_list.html.haml +0 -3
  119. data/test/templates/app/views/ajax/_actions_index.html.erb +0 -8
  120. data/test/templates/app/views/ajax/_actions_index.html.haml +0 -8
  121. data/test/templates/app/views/ajax/_actions_show.html.erb +0 -4
  122. data/test/templates/app/views/ajax/_actions_show.html.haml +0 -4
  123. data/test/templates/app/views/ajax/_form.html.erb +0 -2
  124. data/test/templates/app/views/ajax/_form.html.haml +0 -2
  125. data/test/templates/app/views/ajax/_hello.html.erb +0 -1
  126. data/test/templates/app/views/ajax/_hello.html.haml +0 -1
  127. data/test/templates/app/views/ajax/ajax.js.erb +0 -1
  128. data/test/templates/app/views/ajax/ajax.js.haml +0 -1
  129. data/test/templates/app/views/ajax/edit.js.erb +0 -1
  130. data/test/templates/app/views/ajax/edit.js.haml +0 -1
  131. data/test/templates/app/views/ajax/show.js.erb +0 -1
  132. data/test/templates/app/views/ajax/show.js.haml +0 -1
  133. data/test/templates/app/views/ajax/update.js.erb +0 -5
  134. data/test/templates/app/views/ajax/update.js.haml +0 -5
  135. data/test/templates/app/views/layouts/_nav.html.erb +0 -6
  136. data/test/templates/app/views/layouts/_nav.html.haml +0 -5
  137. data/test/templates/app/views/layouts/bootstrap.html.erb +0 -68
  138. data/test/templates/app/views/layouts/bootstrap.html.haml +0 -49
  139. data/test/templates/app/views/people/_attrs.html.erb +0 -5
  140. data/test/templates/app/views/people/_attrs.html.haml +0 -4
  141. data/test/templates/app/views/people/_list.html.erb +0 -1
  142. data/test/templates/app/views/people/_list.html.haml +0 -1
  143. data/test/templates/config/database.yml +0 -21
  144. data/test/templates/config/locales/en_cities.yml +0 -56
  145. data/test/templates/config/routes.rb +0 -32
  146. data/test/templates/db/migrate/20100511174904_create_people_and_cities.rb +0 -26
  147. data/test/templates/db/seeds.rb +0 -74
  148. data/test/templates/spec/controllers/admin/cities_controller_spec.rb +0 -74
  149. data/test/templates/spec/controllers/admin/countries_controller_spec.rb +0 -56
  150. data/test/templates/spec/controllers/people_controller_spec.rb +0 -80
  151. data/test/templates/spec/routing/cities_routing_spec.rb +0 -11
  152. data/test/templates/spec/routing/countries_routing_spec.rb +0 -11
  153. data/test/templates/test/fixtures/cities.yml +0 -11
  154. data/test/templates/test/fixtures/countries.yml +0 -11
  155. data/test/templates/test/fixtures/people.yml +0 -14
  156. data/test/templates/test/functional/admin/cities_controller_test.rb +0 -59
  157. data/test/templates/test/functional/admin/countries_controller_test.rb +0 -42
  158. data/test/templates/test/functional/people_controller_test.rb +0 -68
@@ -0,0 +1,64 @@
1
+ # encoding: UTF-8
2
+
3
+ # Helpers to create action links. This default implementation supports
4
+ # regular links with an icon and a label. To change the general style
5
+ # of action links, change the method #action_link, e.g. to generate a button.
6
+ # The common crud actions show, edit, destroy, index and add are provided here.
7
+ module ActionsHelper
8
+
9
+ # A generic helper method to create action links.
10
+ # These link could be styled to look like buttons, for example.
11
+ def action_link(label, icon = nil, url = {}, html_options = {})
12
+ add_css_class html_options, 'action btn'
13
+ link_to(icon ? action_icon(icon, label) : label,
14
+ url, html_options)
15
+ end
16
+
17
+ # Outputs an icon for an action with an optional label.
18
+ def action_icon(icon, label = nil)
19
+ html = content_tag(:i, '', class: "icon-#{icon}")
20
+ html << ' ' << label if label
21
+ html
22
+ end
23
+
24
+ # Standard show action to the given path.
25
+ # Uses the current +entry+ if no path is given.
26
+ def show_action_link(path = nil)
27
+ path ||= path_args(entry)
28
+ action_link(ti('link.show'), 'zoom-in', path)
29
+ end
30
+
31
+ # Standard edit action to given path.
32
+ # Uses the current +entry+ if no path is given.
33
+ def edit_action_link(path = nil)
34
+ path ||= path_args(entry)
35
+ path = path.is_a?(String) ? path : edit_polymorphic_path(path)
36
+ action_link(ti('link.edit'), 'pencil', path)
37
+ end
38
+
39
+ # Standard destroy action to the given path.
40
+ # Uses the current +entry+ if no path is given.
41
+ def destroy_action_link(path = nil)
42
+ path ||= path_args(entry)
43
+ action_link(ti('link.delete'), 'remove', path,
44
+ data: { confirm: ti(:confirm_delete),
45
+ method: :delete })
46
+ end
47
+
48
+ # Standard list action to the given path.
49
+ # Uses the current +model_class+ if no path is given.
50
+ def index_action_link(path = nil, url_options = { returning: true })
51
+ path ||= path_args(model_class)
52
+ path = path.is_a?(String) ? path : polymorphic_path(path, url_options)
53
+ action_link(ti('link.list'), 'list', path)
54
+ end
55
+
56
+ # Standard add action to given path.
57
+ # Uses the current +model_class+ if no path is given.
58
+ def add_action_link(path = nil, url_options = {})
59
+ path ||= path_args(model_class)
60
+ path = path.is_a?(String) ? path : new_polymorphic_path(path, url_options)
61
+ action_link(ti('link.add'), 'plus', path)
62
+ end
63
+
64
+ end
@@ -0,0 +1,331 @@
1
+ # encoding: UTF-8
2
+
3
+ module Crud
4
+ # A form builder that automatically selects the corresponding input field
5
+ # for ActiveRecord column types. Convenience methods for each column type
6
+ # allow one to customize the different fields.
7
+ #
8
+ # All field methods may be prefixed with +labeled_+ in order to render
9
+ # a standard label, required mark and an optional help block with them.
10
+ class FormBuilder < ActionView::Helpers::FormBuilder
11
+
12
+ REQUIRED_MARK = '<span class="required">*</span>'.html_safe
13
+
14
+ attr_reader :template
15
+
16
+ delegate :association, :column_type, :column_property, :captionize,
17
+ :ti, :ta, :link_to, :content_tag, :safe_join, :capture,
18
+ :add_css_class, :assoc_and_id_attr,
19
+ to: :template
20
+
21
+ ### INPUT FIELDS
22
+
23
+ # Render multiple input fields together with a label for the given
24
+ # attributes.
25
+ def labeled_input_fields(*attrs)
26
+ options = attrs.extract_options!
27
+ safe_join(attrs) { |a| labeled_input_field(a, options.clone) }
28
+ end
29
+
30
+ # Render a corresponding input field for the given attribute.
31
+ # The input field is chosen based on the ActiveRecord column type.
32
+ # Use additional html_options for the input element.
33
+ def input_field(attr, html_options = {})
34
+ type = column_type(@object, attr)
35
+ custom_field_method = :"#{type}_field"
36
+ if type == :text
37
+ text_area(attr, html_options)
38
+ elsif association_kind?(attr, type, :belongs_to)
39
+ belongs_to_field(attr, html_options)
40
+ elsif association_kind?(attr, type, :has_and_belongs_to_many, :has_many)
41
+ has_many_field(attr, html_options)
42
+ elsif attr.to_s.include?('password')
43
+ password_field(attr, html_options)
44
+ elsif attr.to_s.include?('email')
45
+ email_field(attr, html_options)
46
+ elsif respond_to?(custom_field_method)
47
+ send(custom_field_method, attr, html_options)
48
+ else
49
+ text_field(attr, html_options)
50
+ end
51
+ end
52
+
53
+ # Render a number field.
54
+ def number_field(attr, html_options = {})
55
+ html_options[:size] ||= 10
56
+ super(attr, html_options)
57
+ end
58
+
59
+ # Render a standard string field with column contraints.
60
+ def string_field(attr, html_options = {})
61
+ html_options[:maxlength] ||= column_property(@object, attr, :limit)
62
+ html_options[:size] ||= 30
63
+ text_field(attr, html_options)
64
+ end
65
+
66
+ # Render a text_area.
67
+ def text_area(attr, html_options = {})
68
+ html_options[:rows] ||= 5
69
+ super(attr, html_options)
70
+ end
71
+
72
+ # Render a email field.
73
+ def email_field(attr, html_options = {})
74
+ html_options[:size] ||= 30
75
+ super(attr, html_options)
76
+ end
77
+
78
+ # Render an integer field.
79
+ def integer_field(attr, html_options = {})
80
+ html_options[:step] ||= 1
81
+ number_field(attr, html_options)
82
+ end
83
+
84
+ # Render a float field.
85
+ def float_field(attr, html_options = {})
86
+ html_options[:step] ||= 'any'
87
+ number_field(attr, html_options)
88
+ end
89
+
90
+ # Render a decimal field.
91
+ def decimal_field(attr, html_options = {})
92
+ html_options[:step] ||= 'any'
93
+ number_field(attr, html_options)
94
+ end
95
+
96
+ # Render a boolean field.
97
+ def boolean_field(attr, html_options = {})
98
+ check_box(attr, html_options)
99
+ end
100
+
101
+ # Render a field to select a date. You might want to customize this.
102
+ def date_field(attr, html_options = {})
103
+ date_select(attr, {}, html_options)
104
+ end
105
+
106
+ # Render a field to enter a time. You might want to customize this.
107
+ def time_field(attr, html_options = {})
108
+ time_select(attr, {}, html_options)
109
+ end
110
+
111
+ # Render a field to enter a date and time.
112
+ # You might want to customize this.
113
+ def datetime_field(attr, html_options = {})
114
+ datetime_select(attr, {}, html_options)
115
+ end
116
+
117
+ # Render a select element for a :belongs_to association defined by attr.
118
+ # Use additional html_options for the select element.
119
+ # To pass a custom element list, specify the list with the :list key or
120
+ # define an instance variable with the pluralized name of the association.
121
+ def belongs_to_field(attr, html_options = {})
122
+ list = association_entries(attr, html_options).to_a
123
+ if list.present?
124
+ collection_select(attr, list, :id, :to_s,
125
+ select_options(attr, html_options),
126
+ html_options)
127
+ else
128
+ ta(:none_available, association(@object, attr)).html_safe
129
+ end
130
+ end
131
+
132
+ # Render a multi select element for a :has_many or :has_and_belongs_to_many
133
+ # association defined by attr.
134
+ # Use additional html_options for the select element.
135
+ # To pass a custom element list, specify the list with the :list key or
136
+ # define an instance variable with the pluralized name of the association.
137
+ def has_many_field(attr, html_options = {})
138
+ html_options[:multiple] = true
139
+ add_css_class(html_options, 'multiselect')
140
+ belongs_to_field(attr, html_options)
141
+ end
142
+
143
+ # Dispatch methods starting with 'labeled_' to render a label and the
144
+ # corresponding input field.
145
+ # E.g. labeled_boolean_field(:checked, class: 'bold')
146
+ # To add an additional help text, use the help option.
147
+ # E.g. labeled_boolean_field(:checked, help: 'Some Help')
148
+ def method_missing(name, *args)
149
+ field_method = labeled_field_method?(name)
150
+ if field_method
151
+ build_labeled_field(field_method, *args)
152
+ else
153
+ super(name, *args)
154
+ end
155
+ end
156
+
157
+ # Overriden to fullfill contract with method_missing 'labeled_' methods.
158
+ def respond_to?(name)
159
+ labeled_field_method?(name).present? || super(name)
160
+ end
161
+
162
+ ### VARIOUS FORM ELEMENTS
163
+
164
+ # Render the error messages for the current form.
165
+ def error_messages
166
+ @template.render('shared/error_messages',
167
+ errors: @object.errors,
168
+ object: @object)
169
+ end
170
+
171
+ # Generates a help block for fields
172
+ def help_block(text)
173
+ content_tag(:p, text, class: 'help-block')
174
+ end
175
+
176
+ # Render a submit button and a cancel link for this form.
177
+ def standard_actions(submit_label = ti('button.save'), cancel_url = nil)
178
+ content_tag(:div, class: 'form-actions') do
179
+ safe_join([submit_button(submit_label), cancel_link(cancel_url)], ' ')
180
+ end
181
+ end
182
+
183
+ # Render a standard submit button with the given label.
184
+ def submit_button(label = ti('button.save'))
185
+ button(label, class: 'btn btn-primary', data: { disable_with: label })
186
+ end
187
+
188
+ # Render a cancel link pointing to the given url.
189
+ def cancel_link(url = nil)
190
+ url ||= cancel_url
191
+ link_to(ti('button.cancel'), url, class: 'cancel')
192
+ end
193
+
194
+ # Render a label for the given attribute with the passed field html
195
+ # section. The following parameters may be specified:
196
+ # labeled(:attr) { #content }
197
+ # labeled(:attr, content)
198
+ # labeled(:attr, 'Caption') { #content }
199
+ # labeled(:attr, 'Caption', content)
200
+ def labeled(attr, caption_or_content = nil, content = nil,
201
+ html_options = {}, &block)
202
+ caption, content = extract_caption_and_content(
203
+ attr, caption_or_content, content, &block)
204
+ add_css_class(html_options, 'controls')
205
+ errors = errors_on?(attr) ? ' error' : ''
206
+
207
+ content_tag(:div, class: "control-group#{errors}") do
208
+ label(attr, caption, class: 'control-label') +
209
+ content_tag(:div, content, html_options)
210
+ end
211
+ end
212
+
213
+ # Depending if the given attribute must be present, return
214
+ # only an initial selection prompt or a blank option, respectively.
215
+ def select_options(attr, options = {})
216
+ prompt = options.delete(:prompt)
217
+ blank = options.delete(:include_blank)
218
+ if options[:multiple]
219
+ {}
220
+ elsif prompt
221
+ { prompt: prompt }
222
+ elsif blank
223
+ { include_blank: blank }
224
+ else
225
+ assoc = association(@object, attr)
226
+ if required?(attr)
227
+ { prompt: ta(:please_select, assoc) }
228
+ else
229
+ { include_blank: ta(:no_entry, assoc) }
230
+ end
231
+ end
232
+ end
233
+
234
+ private
235
+
236
+ # Returns true if attr is a non-polymorphic association.
237
+ # If one or more macros are given, the association must be of this kind.
238
+ def association_kind?(attr, type, *macros)
239
+ if type == :integer || type.nil?
240
+ assoc = association(@object, attr, *macros)
241
+ assoc.present? && assoc.options[:polymorphic].nil?
242
+ else
243
+ false
244
+ end
245
+ end
246
+
247
+ # Returns the list of association entries, either from options[:list],
248
+ # the instance variable with the pluralized association name or all
249
+ # entries of the association klass.
250
+ def association_entries(attr, options)
251
+ list = options.delete(:list)
252
+ unless list
253
+ assoc = association(@object, attr)
254
+ list = @template.send(:instance_variable_get,
255
+ :"@#{assoc.name.to_s.pluralize}")
256
+ unless list
257
+ list = assoc.klass.where(assoc.options[:conditions])
258
+ .order(assoc.options[:order])
259
+ end
260
+ end
261
+ list
262
+ end
263
+
264
+ # Returns true if the given attribute must be present.
265
+ def required?(attr)
266
+ attr = attr.to_s
267
+ attr, attr_id = assoc_and_id_attr(attr)
268
+ validators = @object.class.validators_on(attr) +
269
+ @object.class.validators_on(attr_id)
270
+ validators.any? do |v|
271
+ v.kind == :presence &&
272
+ !v.options.key?(:if) &&
273
+ !v.options.key?(:unless)
274
+ end
275
+ end
276
+
277
+ # Returns true if any errors are found on the passed attribute or its
278
+ # association.
279
+ def errors_on?(attr)
280
+ attr_plain, attr_id = assoc_and_id_attr(attr)
281
+ @object.errors.has_key?(attr_plain.to_sym) ||
282
+ @object.errors.has_key?(attr_id.to_sym)
283
+ end
284
+
285
+ # Get caption and content value from the arguments of #labeled.
286
+ def extract_caption_and_content(attr, caption_or_content, content, &block)
287
+ if block_given?
288
+ content = capture(&block)
289
+ elsif content.nil?
290
+ content = caption_or_content
291
+ caption_or_content = nil
292
+ end
293
+ caption_or_content ||= captionize(attr, @object.class)
294
+
295
+ [caption_or_content, content]
296
+ end
297
+
298
+ # Checks if the passed name corresponds to a field method with a
299
+ # 'labeled_' prefix.
300
+ def labeled_field_method?(name)
301
+ prefix = 'labeled_'
302
+ if name.to_s.start_with?(prefix)
303
+ field_method = name.to_s[prefix.size..-1]
304
+ field_method if respond_to?(field_method)
305
+ end
306
+ end
307
+
308
+ # Renders the corresponding field together with a label, required mark and
309
+ # an optional help block.
310
+ def build_labeled_field(field_method, *args)
311
+ required = required?(args.first)
312
+ options = args.extract_options!
313
+ options[:required] ||= 'required' if required
314
+ help = options.delete(:help)
315
+ content = send(field_method, *(args << options))
316
+ content << REQUIRED_MARK if required
317
+ content << help_block(help) if help.present?
318
+ labeled(args.first, content)
319
+ end
320
+
321
+ # Get the cancel url for the given object considering options:
322
+ # 1. Use :cancel_url_new or :cancel_url_edit option, if present
323
+ # 2. Use :cancel_url option, if present
324
+ def cancel_url
325
+ url = @object.new_record? ? options[:cancel_url_new] :
326
+ options[:cancel_url_edit]
327
+ url || options[:cancel_url]
328
+ end
329
+
330
+ end
331
+ end
@@ -0,0 +1,280 @@
1
+ # encoding: UTF-8
2
+
3
+ module Crud
4
+ # A simple helper to easily define tables listing several rows of the same
5
+ # data type.
6
+ #
7
+ # Example Usage:
8
+ # Crud::TableBuilder.table(entries, template) do |t|
9
+ # t.col('My Header', class: 'css') {|e| link_to 'Show', e }
10
+ # t.attrs :name, :city
11
+ # end
12
+ class TableBuilder
13
+ attr_reader :entries, :cols, :options, :template
14
+
15
+ delegate :content_tag, :format_attr, :column_type, :association, :dom_id,
16
+ :captionize, :add_css_class, :content_tag_nested,
17
+ to: :template
18
+
19
+ def initialize(entries, template, options = {})
20
+ @entries = entries
21
+ @template = template
22
+ @options = options
23
+ @cols = []
24
+ end
25
+
26
+ # Convenience method to directly generate a table. Renders a row for each
27
+ # entry in entries. Takes a block that gets the table object as parameter
28
+ # for configuration. Returns the generated html for the table.
29
+ def self.table(entries, template, options = {})
30
+ t = new(entries, template, options)
31
+ yield t
32
+ t.to_html
33
+ end
34
+
35
+ # Define a column for the table with the given header, the html_options
36
+ # used for each td and a block rendering the contents of a cell for the
37
+ # current entry. The columns appear in the order they are defined.
38
+ def col(header = '', html_options = {}, &block)
39
+ @cols << Col.new(header, html_options, @template, block)
40
+ end
41
+
42
+ # Convenience method to add one or more attribute columns.
43
+ # The attribute name will become the header, the cells will contain
44
+ # the formatted attribute value for the current entry.
45
+ def attrs(*attrs)
46
+ attrs.each do |a|
47
+ attr(a)
48
+ end
49
+ end
50
+
51
+ # Define a column for the given attribute and an optional header.
52
+ # If no header is given, the attribute name is used. The cell will
53
+ # contain the formatted attribute value for the current entry.
54
+ def attr(a, header = nil, html_options = {}, &block)
55
+ header ||= attr_header(a)
56
+ block ||= ->(e) { format_attr(e, a) }
57
+ add_css_class(html_options, align_class(a))
58
+ col(header, html_options, &block)
59
+ end
60
+
61
+ # Renders the table as HTML.
62
+ def to_html
63
+ content_tag :table, options do
64
+ content_tag(:thead, html_header) +
65
+ content_tag_nested(:tbody, entries) { |e| html_row(e) }
66
+ end
67
+ end
68
+
69
+ # Returns css classes used for alignment of the cell data.
70
+ # Based on the column type of the attribute.
71
+ def align_class(attr)
72
+ entry = entries.present? ? entry_class.new : nil
73
+ case column_type(entry, attr)
74
+ when :integer, :float, :decimal
75
+ 'right' unless association(entry, attr, :belongs_to)
76
+ when :boolean
77
+ 'center'
78
+ end
79
+ end
80
+
81
+ # Creates a header string for the given attr.
82
+ def attr_header(attr)
83
+ captionize(attr, entry_class)
84
+ end
85
+
86
+ private
87
+
88
+ # Renders the header row of the table.
89
+ def html_header
90
+ content_tag_nested(:tr, cols) { |c| c.html_header }
91
+ end
92
+
93
+ # Renders a table row for the given entry.
94
+ def html_row(entry)
95
+ attrs = {}
96
+ attrs[:id] = dom_id(entry) if entry.respond_to?(:to_key)
97
+ content_tag_nested(:tr, cols, attrs) { |c| c.html_cell(entry) }
98
+ end
99
+
100
+ # Determines the class of the table entries.
101
+ # All entries should be of the same type.
102
+ def entry_class
103
+ if entries.respond_to?(:klass)
104
+ entries.klass
105
+ else
106
+ entries.first.class
107
+ end
108
+ end
109
+
110
+ # Helper class to store column information.
111
+ class Col < Struct.new(:header, :html_options, :template, :block) #:nodoc:
112
+
113
+ delegate :content_tag, :capture, to: :template
114
+
115
+ # Runs the Col block for the given entry.
116
+ def content(entry)
117
+ entry.nil? ? '' : capture(entry, &block)
118
+ end
119
+
120
+ # Renders the header cell of the Col.
121
+ def html_header
122
+ content_tag(:th, header, html_options)
123
+ end
124
+
125
+ # Renders a table cell for the given entry.
126
+ def html_cell(entry)
127
+ content_tag(:td, content(entry), html_options)
128
+ end
129
+
130
+ end
131
+
132
+ # Provides headers with sort links. Expects a method :sortable?(attr)
133
+ # in the template/controller to tell if an attribute is sortable or not.
134
+ # Extracted into an own module for convenience.
135
+ module Sorting
136
+ # Create a header with sort links and a mark for the current sort
137
+ # direction.
138
+ def sort_header(attr, label = nil)
139
+ label ||= attr_header(attr)
140
+ template.link_to(label, sort_params(attr)) + current_mark(attr)
141
+ end
142
+
143
+ # Same as :attrs, except that it renders a sort link in the header
144
+ # if an attr is sortable.
145
+ def sortable_attrs(*attrs)
146
+ attrs.each { |a| sortable_attr(a) }
147
+ end
148
+
149
+ # Renders a sort link header, otherwise similar to :attr.
150
+ def sortable_attr(a, header = nil, &block)
151
+ if template.sortable?(a)
152
+ attr(a, sort_header(a, header), &block)
153
+ else
154
+ attr(a, header, &block)
155
+ end
156
+ end
157
+
158
+ private
159
+
160
+ # Request params for the sort link.
161
+ def sort_params(attr)
162
+ params.merge({ sort: attr, sort_dir: sort_dir(attr) })
163
+ end
164
+
165
+ # The sort mark, if any, for the given attribute.
166
+ def current_mark(attr)
167
+ if current_sort?(attr)
168
+ (sort_dir(attr) == 'asc' ? ' &uarr;' : ' &darr;').html_safe
169
+ else
170
+ ''
171
+ end
172
+ end
173
+
174
+ # Returns true if the given attribute is the current sort column.
175
+ def current_sort?(attr)
176
+ params[:sort] == attr.to_s
177
+ end
178
+
179
+ # The sort direction to use in the sort link for the given attribute.
180
+ def sort_dir(attr)
181
+ current_sort?(attr) && params[:sort_dir] == 'asc' ? 'desc' : 'asc'
182
+ end
183
+
184
+ # Delegate to template.
185
+ def params
186
+ template.params
187
+ end
188
+ end
189
+
190
+ include Sorting
191
+
192
+ # Adds action columns to the table builder.
193
+ # Predefined actions are available for show, edit and destroy.
194
+ # Additionally, a special col type to define cells linked to the show page
195
+ # of the row entry is provided.
196
+ module Actions
197
+ extend ActiveSupport::Concern
198
+
199
+ included do
200
+ delegate :link_to, :path_args, :edit_polymorphic_path, :ti,
201
+ to: :template
202
+ end
203
+
204
+ # Renders the passed attr with a link to the show action for
205
+ # the current entry.
206
+ # A block may be given to define the link path for the row entry.
207
+ def attr_with_show_link(attr, &block)
208
+ sortable_attr(attr) do |e|
209
+ link_to(format_attr(e, attr), action_path(e, &block))
210
+ end
211
+ end
212
+
213
+ # Action column to show the row entry.
214
+ # A block may be given to define the link path for the row entry.
215
+ # If the block returns nil, no link is rendered.
216
+ def show_action_col(html_options = {}, &block)
217
+ action_col do |e|
218
+ path = action_path(e, &block)
219
+ if path
220
+ table_action_link('zoom-in',
221
+ path,
222
+ html_options)
223
+ end
224
+ end
225
+ end
226
+
227
+ # Action column to edit the row entry.
228
+ # A block may be given to define the link path for the row entry.
229
+ # If the block returns nil, no link is rendered.
230
+ def edit_action_col(html_options = {}, &block)
231
+ action_col do |e|
232
+ path = action_path(e, &block)
233
+ if path
234
+ path = path.is_a?(String) ? path : edit_polymorphic_path(path)
235
+ table_action_link('pencil', path, html_options)
236
+ end
237
+ end
238
+ end
239
+
240
+ # Action column to destroy the row entry.
241
+ # A block may be given to define the link path for the row entry.
242
+ # If the block returns nil, no link is rendered.
243
+ def destroy_action_col(html_options = {}, &block)
244
+ action_col do |e|
245
+ path = action_path(e, &block)
246
+ if path
247
+ table_action_link('remove',
248
+ path,
249
+ html_options.merge(
250
+ data: { confirm: ti(:confirm_delete),
251
+ method: :delete }))
252
+ end
253
+ end
254
+ end
255
+
256
+ # Action column inside a table. No header.
257
+ # The cell content should be defined in the passed block.
258
+ def action_col(&block)
259
+ col('', class: 'action', &block)
260
+ end
261
+
262
+ # Generic action link inside a table.
263
+ def table_action_link(icon, url, html_options = {})
264
+ add_css_class(html_options, "icon-#{icon}")
265
+ link_to('', url, html_options)
266
+ end
267
+
268
+ private
269
+
270
+ # If a block is given, call it to get the path for the current row entry.
271
+ # Otherwise, return the standard path args.
272
+ def action_path(e, &block)
273
+ block_given? ? yield(e) : path_args(e)
274
+ end
275
+ end
276
+
277
+ include Actions
278
+
279
+ end
280
+ end