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.
- checksums.yaml +15 -0
- data/MIT-LICENSE +1 -1
- data/README.rdoc +126 -87
- data/VERSION +1 -1
- data/lib/generators/dry_crud/dry_crud_generator.rb +42 -22
- data/lib/generators/dry_crud/templates/INSTALL +5 -5
- data/lib/generators/dry_crud/templates/app/assets/stylesheets/crud.scss +0 -20
- data/lib/generators/dry_crud/templates/app/assets/stylesheets/sample.scss +24 -4
- data/lib/generators/dry_crud/templates/app/controllers/crud/generic_model.rb +89 -0
- data/lib/generators/dry_crud/templates/app/controllers/crud/nestable.rb +70 -0
- data/lib/generators/dry_crud/templates/app/controllers/crud/rememberable.rb +64 -0
- data/lib/generators/dry_crud/templates/app/controllers/crud/render_callbacks.rb +46 -0
- data/lib/generators/dry_crud/templates/app/controllers/crud/responder.rb +31 -0
- data/lib/generators/dry_crud/templates/app/controllers/crud/searchable.rb +55 -0
- data/lib/generators/dry_crud/templates/app/controllers/crud/sortable.rb +63 -0
- data/lib/generators/dry_crud/templates/app/controllers/crud_controller.rb +66 -69
- data/lib/generators/dry_crud/templates/app/controllers/list_controller.rb +23 -326
- data/lib/generators/dry_crud/templates/app/helpers/actions_helper.rb +64 -0
- data/lib/generators/dry_crud/templates/app/helpers/crud/form_builder.rb +331 -0
- data/lib/generators/dry_crud/templates/app/helpers/crud/table_builder.rb +280 -0
- data/lib/generators/dry_crud/templates/app/helpers/form_helper.rb +52 -0
- data/lib/generators/dry_crud/templates/app/helpers/format_helper.rb +164 -0
- data/lib/generators/dry_crud/templates/app/helpers/i18n_helper.rb +85 -0
- data/lib/generators/dry_crud/templates/app/helpers/table_helper.rb +83 -0
- data/lib/generators/dry_crud/templates/app/helpers/utility_helper.rb +84 -0
- data/lib/generators/dry_crud/templates/app/views/crud/_actions_edit.html.erb +3 -3
- data/lib/generators/dry_crud/templates/app/views/crud/_actions_edit.html.haml +3 -3
- data/lib/generators/dry_crud/templates/app/views/crud/_actions_index.html.erb +1 -1
- data/lib/generators/dry_crud/templates/app/views/crud/_actions_index.html.haml +1 -1
- data/lib/generators/dry_crud/templates/app/views/crud/_actions_show.html.erb +3 -3
- data/lib/generators/dry_crud/templates/app/views/crud/_actions_show.html.haml +3 -3
- data/lib/generators/dry_crud/templates/app/views/crud/_attrs.html.erb +1 -1
- data/lib/generators/dry_crud/templates/app/views/crud/_attrs.html.haml +1 -1
- data/lib/generators/dry_crud/templates/app/views/crud/_form.html.erb +1 -1
- data/lib/generators/dry_crud/templates/app/views/crud/_form.html.haml +1 -1
- data/lib/generators/dry_crud/templates/app/views/crud/edit.html.erb +1 -1
- data/lib/generators/dry_crud/templates/app/views/crud/edit.html.haml +1 -1
- data/lib/generators/dry_crud/templates/app/views/crud/new.html.erb +2 -2
- data/lib/generators/dry_crud/templates/app/views/crud/new.html.haml +2 -2
- data/lib/generators/dry_crud/templates/app/views/crud/show.html.erb +1 -1
- data/lib/generators/dry_crud/templates/app/views/crud/show.html.haml +1 -1
- data/lib/generators/dry_crud/templates/app/views/layouts/_flash.html.haml +1 -1
- data/lib/generators/dry_crud/templates/app/views/layouts/_nav.html.erb +1 -1
- data/lib/generators/dry_crud/templates/app/views/layouts/_nav.html.haml +1 -1
- data/lib/generators/dry_crud/templates/app/views/layouts/crud.html.erb +15 -7
- data/lib/generators/dry_crud/templates/app/views/layouts/crud.html.haml +18 -8
- data/lib/generators/dry_crud/templates/app/views/list/_search.html.erb +3 -3
- data/lib/generators/dry_crud/templates/app/views/list/_search.html.haml +3 -3
- data/lib/generators/dry_crud/templates/app/views/list/index.html.erb +1 -1
- data/lib/generators/dry_crud/templates/app/views/list/index.html.haml +1 -1
- data/lib/generators/dry_crud/templates/app/views/shared/_error_messages.html.erb +1 -1
- data/lib/generators/dry_crud/templates/app/views/shared/_error_messages.html.haml +1 -1
- data/lib/generators/dry_crud/templates/app/views/shared/_labeled.html.erb +2 -4
- data/lib/generators/dry_crud/templates/app/views/shared/_labeled.html.haml +2 -3
- data/lib/generators/dry_crud/templates/config/initializers/field_error_proc.rb +5 -1
- data/lib/generators/dry_crud/templates/config/locales/crud.de.yml +64 -0
- data/lib/generators/dry_crud/templates/config/locales/{en_crud.yml → crud.en.yml} +2 -2
- data/lib/generators/dry_crud/templates/spec/controllers/crud_test_models_controller_spec.rb +241 -231
- data/lib/generators/dry_crud/templates/spec/helpers/crud/form_builder_spec.rb +226 -0
- data/lib/generators/dry_crud/templates/spec/helpers/{standard_table_builder_spec.rb → crud/table_builder_spec.rb} +36 -34
- data/lib/generators/dry_crud/templates/spec/helpers/form_helper_spec.rb +238 -0
- data/lib/generators/dry_crud/templates/spec/helpers/format_helper_spec.rb +244 -0
- data/lib/generators/dry_crud/templates/spec/helpers/i18n_helper_spec.rb +132 -0
- data/lib/generators/dry_crud/templates/spec/helpers/table_helper_spec.rb +265 -0
- data/lib/generators/dry_crud/templates/spec/helpers/utility_helper_spec.rb +74 -0
- data/lib/generators/dry_crud/templates/spec/support/crud_controller_examples.rb +185 -100
- data/lib/generators/dry_crud/templates/spec/support/crud_controller_test_helper.rb +58 -49
- data/lib/generators/dry_crud/templates/test/{functional → controllers}/crud_test_models_controller_test.rb +112 -91
- data/lib/generators/dry_crud/templates/test/{unit/helpers/standard_form_builder_test.rb → helpers/crud/form_builder_test.rb} +79 -62
- data/lib/generators/dry_crud/templates/test/{unit/helpers/standard_table_builder_test.rb → helpers/crud/table_builder_test.rb} +31 -28
- data/lib/generators/dry_crud/templates/test/helpers/custom_assertions_test.rb +85 -0
- data/lib/generators/dry_crud/templates/test/helpers/form_helper_test.rb +129 -0
- data/lib/generators/dry_crud/templates/test/helpers/format_helper_test.rb +163 -0
- data/lib/generators/dry_crud/templates/test/helpers/i18n_helper_test.rb +79 -0
- data/lib/generators/dry_crud/templates/test/helpers/table_helper_test.rb +217 -0
- data/lib/generators/dry_crud/templates/test/helpers/utility_helper_test.rb +63 -0
- data/lib/generators/dry_crud/templates/test/{functional → support}/crud_controller_test_helper.rb +70 -59
- data/lib/generators/dry_crud/templates/test/{crud_test_model.rb → support/crud_test_model.rb} +107 -75
- data/lib/generators/dry_crud/templates/test/support/custom_assertions.rb +83 -0
- metadata +83 -146
- data/Rakefile +0 -211
- data/lib/generators/dry_crud/templates/app/helpers/crud_helper.rb +0 -168
- data/lib/generators/dry_crud/templates/app/helpers/list_helper.rb +0 -27
- data/lib/generators/dry_crud/templates/app/helpers/standard_form_builder.rb +0 -261
- data/lib/generators/dry_crud/templates/app/helpers/standard_helper.rb +0 -304
- data/lib/generators/dry_crud/templates/app/helpers/standard_table_builder.rb +0 -178
- data/lib/generators/dry_crud/templates/spec/helpers/crud_helper_spec.rb +0 -146
- data/lib/generators/dry_crud/templates/spec/helpers/list_helper_spec.rb +0 -154
- data/lib/generators/dry_crud/templates/spec/helpers/standard_form_builder_spec.rb +0 -215
- data/lib/generators/dry_crud/templates/spec/helpers/standard_helper_spec.rb +0 -387
- data/lib/generators/dry_crud/templates/test/custom_assertions.rb +0 -78
- data/lib/generators/dry_crud/templates/test/unit/custom_assertions_test.rb +0 -117
- data/lib/generators/dry_crud/templates/test/unit/helpers/crud_helper_test.rb +0 -111
- data/lib/generators/dry_crud/templates/test/unit/helpers/list_helper_test.rb +0 -123
- data/lib/generators/dry_crud/templates/test/unit/helpers/standard_helper_test.rb +0 -281
- data/test/templates/Gemfile +0 -46
- data/test/templates/app/controllers/admin/cities_controller.rb +0 -7
- data/test/templates/app/controllers/admin/countries_controller.rb +0 -13
- data/test/templates/app/controllers/ajax_controller.rb +0 -9
- data/test/templates/app/controllers/people_controller.rb +0 -13
- data/test/templates/app/controllers/vips_controller.rb +0 -19
- data/test/templates/app/helpers/cities_helper.rb +0 -9
- data/test/templates/app/helpers/people_helper.rb +0 -8
- data/test/templates/app/models/city.rb +0 -28
- data/test/templates/app/models/country.rb +0 -16
- data/test/templates/app/models/person.rb +0 -12
- data/test/templates/app/views/admin/cities/_actions_index.html.erb +0 -2
- data/test/templates/app/views/admin/cities/_actions_index.html.haml +0 -2
- data/test/templates/app/views/admin/cities/_attrs.html.erb +0 -1
- data/test/templates/app/views/admin/cities/_attrs.html.haml +0 -1
- data/test/templates/app/views/admin/cities/_form.html.erb +0 -7
- data/test/templates/app/views/admin/cities/_form.html.haml +0 -5
- data/test/templates/app/views/admin/cities/_hello.html.erb +0 -1
- data/test/templates/app/views/admin/cities/_hello.html.haml +0 -1
- data/test/templates/app/views/admin/cities/_list.html.erb +0 -3
- data/test/templates/app/views/admin/cities/_list.html.haml +0 -3
- data/test/templates/app/views/admin/countries/_list.html.erb +0 -4
- data/test/templates/app/views/admin/countries/_list.html.haml +0 -3
- data/test/templates/app/views/ajax/_actions_index.html.erb +0 -8
- data/test/templates/app/views/ajax/_actions_index.html.haml +0 -8
- data/test/templates/app/views/ajax/_actions_show.html.erb +0 -4
- data/test/templates/app/views/ajax/_actions_show.html.haml +0 -4
- data/test/templates/app/views/ajax/_form.html.erb +0 -2
- data/test/templates/app/views/ajax/_form.html.haml +0 -2
- data/test/templates/app/views/ajax/_hello.html.erb +0 -1
- data/test/templates/app/views/ajax/_hello.html.haml +0 -1
- data/test/templates/app/views/ajax/ajax.js.erb +0 -1
- data/test/templates/app/views/ajax/ajax.js.haml +0 -1
- data/test/templates/app/views/ajax/edit.js.erb +0 -1
- data/test/templates/app/views/ajax/edit.js.haml +0 -1
- data/test/templates/app/views/ajax/show.js.erb +0 -1
- data/test/templates/app/views/ajax/show.js.haml +0 -1
- data/test/templates/app/views/ajax/update.js.erb +0 -5
- data/test/templates/app/views/ajax/update.js.haml +0 -5
- data/test/templates/app/views/layouts/_nav.html.erb +0 -6
- data/test/templates/app/views/layouts/_nav.html.haml +0 -5
- data/test/templates/app/views/layouts/bootstrap.html.erb +0 -68
- data/test/templates/app/views/layouts/bootstrap.html.haml +0 -49
- data/test/templates/app/views/people/_attrs.html.erb +0 -5
- data/test/templates/app/views/people/_attrs.html.haml +0 -4
- data/test/templates/app/views/people/_list.html.erb +0 -1
- data/test/templates/app/views/people/_list.html.haml +0 -1
- data/test/templates/config/database.yml +0 -21
- data/test/templates/config/locales/en_cities.yml +0 -56
- data/test/templates/config/routes.rb +0 -32
- data/test/templates/db/migrate/20100511174904_create_people_and_cities.rb +0 -26
- data/test/templates/db/seeds.rb +0 -74
- data/test/templates/spec/controllers/admin/cities_controller_spec.rb +0 -74
- data/test/templates/spec/controllers/admin/countries_controller_spec.rb +0 -56
- data/test/templates/spec/controllers/people_controller_spec.rb +0 -80
- data/test/templates/spec/routing/cities_routing_spec.rb +0 -11
- data/test/templates/spec/routing/countries_routing_spec.rb +0 -11
- data/test/templates/test/fixtures/cities.yml +0 -11
- data/test/templates/test/fixtures/countries.yml +0 -11
- data/test/templates/test/fixtures/people.yml +0 -14
- data/test/templates/test/functional/admin/cities_controller_test.rb +0 -59
- data/test/templates/test/functional/admin/countries_controller_test.rb +0 -42
- 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' ? ' ↑' : ' ↓').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
|