formbuilder-rb 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/Rakefile +24 -0
- data/app/controllers/formbuilder/forms_controller.rb +53 -0
- data/app/models/formbuilder/entry_attachment.rb +15 -0
- data/app/models/formbuilder/form.rb +12 -0
- data/app/models/formbuilder/response_field.rb +85 -0
- data/app/models/formbuilder/response_field_address.rb +349 -0
- data/app/models/formbuilder/response_field_checkboxes.rb +68 -0
- data/app/models/formbuilder/response_field_date.rb +46 -0
- data/app/models/formbuilder/response_field_dropdown.rb +30 -0
- data/app/models/formbuilder/response_field_email.rb +26 -0
- data/app/models/formbuilder/response_field_file.rb +44 -0
- data/app/models/formbuilder/response_field_number.rb +33 -0
- data/app/models/formbuilder/response_field_paragraph.rb +21 -0
- data/app/models/formbuilder/response_field_price.rb +47 -0
- data/app/models/formbuilder/response_field_radio.rb +52 -0
- data/app/models/formbuilder/response_field_section_break.rb +17 -0
- data/app/models/formbuilder/response_field_text.rb +16 -0
- data/app/models/formbuilder/response_field_time.rb +53 -0
- data/app/models/formbuilder/response_field_website.rb +31 -0
- data/app/uploaders/formbuilder/entry_attachment_uploader.rb +26 -0
- data/db/migrate/20130924185726_create_formbuilder_forms.rb +10 -0
- data/db/migrate/20130924185814_create_formbuilder_response_fields.rb +16 -0
- data/db/migrate/20130924185815_create_formbuilder_entry_attachments.rb +10 -0
- data/lib/formbuilder/engine.rb +5 -0
- data/lib/formbuilder/entry.rb +242 -0
- data/lib/formbuilder/entry_renderer.rb +47 -0
- data/lib/formbuilder/entry_table_renderer.rb +58 -0
- data/lib/formbuilder/entry_validator.rb +107 -0
- data/lib/formbuilder/form_renderer.rb +102 -0
- data/lib/formbuilder/version.rb +3 -0
- data/lib/formbuilder.rb +12 -0
- data/lib/tasks/formbuilder_tasks.rake +4 -0
- metadata +258 -0
@@ -0,0 +1,242 @@
|
|
1
|
+
module Formbuilder
|
2
|
+
module Entry
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
attr_accessor :old_responses
|
7
|
+
attr_accessor :skip_validation
|
8
|
+
validates_with Formbuilder::EntryValidator
|
9
|
+
before_save :calculate_responses_text, if: :responses_changed?
|
10
|
+
include ActionView::Helpers::TextHelper
|
11
|
+
end
|
12
|
+
|
13
|
+
def submit!(skip_validation = false)
|
14
|
+
return false if !skip_validation && !valid?
|
15
|
+
|
16
|
+
self.audit_responses
|
17
|
+
|
18
|
+
self.update_attributes(
|
19
|
+
submitted_at: Time.now,
|
20
|
+
skip_validation: true # don't validate twice
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
def unsubmit!
|
25
|
+
self.update_attributes(
|
26
|
+
submitted_at: nil,
|
27
|
+
skip_validation: true
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
def submitted?
|
32
|
+
self.submitted_at.present?
|
33
|
+
end
|
34
|
+
|
35
|
+
def value_present?(response_field)
|
36
|
+
value = self.response_value(response_field)
|
37
|
+
|
38
|
+
# value isn't blank (ignore hashes)
|
39
|
+
return true if (value && value.present? && !value.is_a?(Hash))
|
40
|
+
|
41
|
+
# no options are available
|
42
|
+
return true if (response_field.options_field && Array(response_field.field_options["options"]).empty?)
|
43
|
+
|
44
|
+
# there is at least one value (for hashes)
|
45
|
+
# reject select fields
|
46
|
+
return true if (value.is_a?(Hash) && value.reject { |k, v| k.in? ['am_pm', 'country'] }.find { |k, v| v.present? })
|
47
|
+
|
48
|
+
# otherwise, it's not present
|
49
|
+
return false
|
50
|
+
end
|
51
|
+
|
52
|
+
# checkboxes can have no values, yet still need to show up as unchecked
|
53
|
+
def value_present_or_checkboxes?(response_field)
|
54
|
+
response_field.field_type == 'checkboxes' || value_present?(response_field)
|
55
|
+
end
|
56
|
+
|
57
|
+
def response_value(response_field)
|
58
|
+
value = responses && responses[response_field.id.to_s]
|
59
|
+
|
60
|
+
if value
|
61
|
+
response_field.serialized ? YAML::load(value) : value
|
62
|
+
elsif !value && response_field.serialized && response_field.field_type != 'checkboxes'
|
63
|
+
{}
|
64
|
+
else # for checkboxes, we need to know the difference between no value and none selected
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def save_responses(response_field_params, response_fields)
|
70
|
+
self.old_responses = self.responses.try(:clone) || {}
|
71
|
+
self.responses = {}
|
72
|
+
|
73
|
+
response_fields.reject { |rf| !rf.input_field }.each do |response_field|
|
74
|
+
self.save_response(response_field_params.try(:[], response_field.id.to_s), response_field, response_field_params)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def save_response(raw_value, response_field, response_field_params = {})
|
79
|
+
value = case response_field.field_type
|
80
|
+
when "checkboxes"
|
81
|
+
# transform checkboxes into {label => on/off} pairs
|
82
|
+
values = {}
|
83
|
+
|
84
|
+
(response_field[:field_options]["options"] || []).each_with_index do |option, index|
|
85
|
+
label = response_field.field_options["options"][index]["label"]
|
86
|
+
values[option["label"]] = raw_value && raw_value[index.to_s] == "on"
|
87
|
+
end
|
88
|
+
|
89
|
+
if raw_value && raw_value['other_checkbox'] == 'on'
|
90
|
+
values['Other'] = raw_value['other']
|
91
|
+
else
|
92
|
+
values.delete('Other') # @todo this might cause unexpected behavior to the user. we should hide/show the other field in the frontend, too
|
93
|
+
end
|
94
|
+
|
95
|
+
# Save 'other' value
|
96
|
+
responses["#{response_field.id}_other"] = raw_value && raw_value['other_checkbox'] == 'on' ?
|
97
|
+
true :
|
98
|
+
nil
|
99
|
+
|
100
|
+
values
|
101
|
+
|
102
|
+
when "file"
|
103
|
+
# if the file is already uploaded and we're not uploading another,
|
104
|
+
# be sure to keep it
|
105
|
+
if raw_value.blank?
|
106
|
+
if old_responses && old_responses[response_field.id.to_s]
|
107
|
+
old_responses[response_field.id.to_s]
|
108
|
+
end
|
109
|
+
else
|
110
|
+
remove_entry_attachment(responses[response_field.id.to_s]) if responses
|
111
|
+
attachment = EntryAttachment.create(upload: raw_value)
|
112
|
+
attachment.id
|
113
|
+
end
|
114
|
+
when "radio"
|
115
|
+
# Save 'other' value
|
116
|
+
responses["#{response_field.id}_other"] = raw_value == 'Other' ?
|
117
|
+
response_field_params["#{response_field.id}_other"] :
|
118
|
+
nil
|
119
|
+
|
120
|
+
raw_value
|
121
|
+
else
|
122
|
+
raw_value
|
123
|
+
end
|
124
|
+
|
125
|
+
self.responses ||= {}
|
126
|
+
|
127
|
+
if value.present?
|
128
|
+
self.responses["#{response_field.id}"] = response_field.serialized ? value.to_yaml : value
|
129
|
+
calculate_sortable_value(response_field, value)
|
130
|
+
end
|
131
|
+
|
132
|
+
self.responses_will_change! # hack to make sure column is marked as dirty
|
133
|
+
end
|
134
|
+
|
135
|
+
def destroy_response(response_field)
|
136
|
+
case response_field.field_type
|
137
|
+
when "file"
|
138
|
+
self.remove_entry_attachment(responses[response_field.id.to_s])
|
139
|
+
end
|
140
|
+
|
141
|
+
id = response_field.id.to_s
|
142
|
+
new_responses = self.responses.reject { |k, v| k.in?([id, "#{id}_sortable_value"]) }
|
143
|
+
self.responses = new_responses
|
144
|
+
|
145
|
+
self.responses_will_change! # hack to make sure column is marked as dirty
|
146
|
+
end
|
147
|
+
|
148
|
+
def remove_entry_attachment(entry_attachment_id)
|
149
|
+
return unless entry_attachment_id.present?
|
150
|
+
EntryAttachment.where(id: entry_attachment_id).first.try(:destroy)
|
151
|
+
end
|
152
|
+
|
153
|
+
def error_for(response_field)
|
154
|
+
(self.errors.messages[:"responses_#{response_field.id}"] || [])[0]
|
155
|
+
end
|
156
|
+
|
157
|
+
def calculate_responses_text
|
158
|
+
return unless self.respond_to?(:"responses_text=")
|
159
|
+
selected_responses = self.responses.select { |k, v| Integer(k) rescue nil }
|
160
|
+
self.responses_text = selected_responses.values.join(' ')
|
161
|
+
end
|
162
|
+
|
163
|
+
# useful when migrating
|
164
|
+
def calculate_sortable_values
|
165
|
+
response_fieldable.response_fields.reject { |rf| !rf.input_field }.each do |response_field|
|
166
|
+
calculate_sortable_value(response_field, response_value(response_field))
|
167
|
+
end
|
168
|
+
|
169
|
+
self.responses_will_change! # hack to make sure column is marked as dirty
|
170
|
+
end
|
171
|
+
|
172
|
+
def calculate_additional_info
|
173
|
+
response_fieldable.response_fields.reject { |rf| !rf.input_field }.each do |response_field|
|
174
|
+
value = response_value(response_field)
|
175
|
+
next unless value.present?
|
176
|
+
|
177
|
+
case response_field.field_type
|
178
|
+
when 'address'
|
179
|
+
begin
|
180
|
+
coords = Geocoder.coordinates("#{value['street']} #{value['city']} #{value['state']} #{value['zipcode']} #{value['country']}")
|
181
|
+
self.responses["#{response_field.id}_x"] = coords[0]
|
182
|
+
self.responses["#{response_field.id}_y"] = coords[1]
|
183
|
+
rescue
|
184
|
+
self.responses["#{response_field.id}_x"] = nil
|
185
|
+
self.responses["#{response_field.id}_y"] = nil
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# def normalize_responses
|
192
|
+
# response_fieldable.response_fields.reject { |rf| !rf.input_field }.each do |response_field|
|
193
|
+
# value = response_value(response_field)
|
194
|
+
# next unless value.present?
|
195
|
+
|
196
|
+
# case response_field.field_type
|
197
|
+
# when 'website'
|
198
|
+
# unless value[/^http:\/\//] || value[/^https:\/\//]
|
199
|
+
# save_response("http://#{value}", response_field)
|
200
|
+
# end
|
201
|
+
# end
|
202
|
+
# end
|
203
|
+
# end
|
204
|
+
|
205
|
+
def audit_responses
|
206
|
+
form.response_fields.each do |response_field|
|
207
|
+
response_field.audit_response(self.response_value(response_field), self.responses)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def calculate_sortable_value(response_field, value)
|
212
|
+
return unless value.present?
|
213
|
+
|
214
|
+
self.responses["#{response_field.id}_sortable_value"] = case response_field.field_type
|
215
|
+
when "date"
|
216
|
+
['year', 'month', 'day'].each { |x| return 0 unless value[x] && !value[x].blank? }
|
217
|
+
DateTime.new(value['year'].to_i, value['month'].to_i, value['day'].to_i).to_i rescue 0
|
218
|
+
when "time"
|
219
|
+
hours = value['hours'].to_i
|
220
|
+
hours += 12 if value['am_pm'] && value['am_pm'] == 'PM'
|
221
|
+
(hours*60*60) + (value['minutes'].to_i * 60) + value['seconds'].to_i
|
222
|
+
when "file"
|
223
|
+
value ? 1 : 0
|
224
|
+
when "checkboxes"
|
225
|
+
calculate_sortable_value_for_checkboxes(response_field, value)
|
226
|
+
return nil
|
227
|
+
when "price"
|
228
|
+
"#{value['dollars'] || '0'}.#{value['cents'] || '0'}".to_f
|
229
|
+
else
|
230
|
+
# do we really need to sort more than the first 10 characters of a string?
|
231
|
+
value[0..10]
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def calculate_sortable_value_for_checkboxes(response_field, value)
|
236
|
+
(response_field.field_options['options'] || []).each do |option|
|
237
|
+
self.responses["#{response_field.id}_sortable_values_#{option['label']}"] = value[option['label']]
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
end
|
242
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Formbuilder
|
2
|
+
class EntryRenderer
|
3
|
+
include ActionView::Context
|
4
|
+
include ActionView::Helpers::TagHelper
|
5
|
+
|
6
|
+
def initialize(entry, form, opts = {})
|
7
|
+
@entry, @form = entry, form
|
8
|
+
@options = opts # merge defaults?
|
9
|
+
end
|
10
|
+
|
11
|
+
def fields
|
12
|
+
return_fields = @form.response_fields.reject { |rf| !rf.input_field }
|
13
|
+
return_fields.reject! { |rf| rf.blind? } unless @options[:show_blind]
|
14
|
+
return_fields
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_html
|
18
|
+
content_tag 'dl', class: 'entry-dl' do
|
19
|
+
fields.map do |rf|
|
20
|
+
"""
|
21
|
+
<dt>#{field_labels(rf)}</dt>
|
22
|
+
<dd>
|
23
|
+
#{field_value(rf) || no_value}
|
24
|
+
</dd>
|
25
|
+
"""
|
26
|
+
end.join('').html_safe
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def field_labels(rf)
|
31
|
+
"""
|
32
|
+
#{rf.label}
|
33
|
+
#{rf.blind? ? '<span class="label">Blind</span>' : ''}
|
34
|
+
#{rf.admin_only? ? '<span class="label">Admin Only</span>' : ''}
|
35
|
+
"""
|
36
|
+
end
|
37
|
+
|
38
|
+
def no_value
|
39
|
+
"<span class='no-response'>No response</span>"
|
40
|
+
end
|
41
|
+
|
42
|
+
def field_value(rf)
|
43
|
+
value = @entry.response_value(rf)
|
44
|
+
rf.render_entry(value, entry: @entry)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Formbuilder
|
2
|
+
class EntryTableRenderer < EntryRenderer
|
3
|
+
# include ActionView::Context
|
4
|
+
# include ActionView::Helpers::TagHelper
|
5
|
+
|
6
|
+
# def initialize(entry, form, opts = {})
|
7
|
+
# @entry, @form = entry, form
|
8
|
+
# @options = opts # merge defaults?
|
9
|
+
# end
|
10
|
+
|
11
|
+
# def fields
|
12
|
+
# return_fields = @form.response_fields.reject { |rf| rf.non_input_field? }
|
13
|
+
# return_fields.reject! { |rf| rf.blind? } unless @options[:show_blind]
|
14
|
+
# return_fields
|
15
|
+
# end
|
16
|
+
|
17
|
+
def to_html
|
18
|
+
content_tag 'dl', class: 'entry-dl' do
|
19
|
+
fields.map do |rf|
|
20
|
+
field_value(rf)
|
21
|
+
end.join('').html_safe
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def field_value(rf)
|
26
|
+
value = @entry.response_value(rf)
|
27
|
+
rf.field_class.render_entry_for_table(rf, value, entry: @entry)
|
28
|
+
end
|
29
|
+
|
30
|
+
# def field_labels(rf)
|
31
|
+
# """
|
32
|
+
# #{rf.label}
|
33
|
+
# #{rf.blind? ? '<span class="label">Blind</span>' : ''}
|
34
|
+
# #{rf.admin_only? ? '<span class="label">Admin Only</span>' : ''}
|
35
|
+
# """
|
36
|
+
# end
|
37
|
+
|
38
|
+
# def no_value
|
39
|
+
# "<span class='no-response'>No response</span>"
|
40
|
+
# end
|
41
|
+
|
42
|
+
# def field_value(rf)
|
43
|
+
# value = @entry.response_value(rf)
|
44
|
+
# Formbuilder::Fields.const_get(rf.field_type.camelize + 'Field').render_entry(rf, value, entry: @entry)
|
45
|
+
# end
|
46
|
+
|
47
|
+
# def checkboxes_for_table
|
48
|
+
# (@response_field.field_options['options'] || []).map do |option|
|
49
|
+
# """
|
50
|
+
# <td data-column-id='#{@response_field.id}_sortable_values_#{option['label']}'>
|
51
|
+
# <i class='#{@value && @value[option['label']] ? 'icon-ok' : 'icon-remove'}'></i>
|
52
|
+
# </td>
|
53
|
+
# """
|
54
|
+
# end.join('')
|
55
|
+
# end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Formbuilder
|
2
|
+
class EntryValidator < ActiveModel::Validator
|
3
|
+
SHARED_VALIDATION_METHODS = [:min_max_length, :min_max, :integer_only]
|
4
|
+
|
5
|
+
def validate(record)
|
6
|
+
@record = record
|
7
|
+
|
8
|
+
# I guess it's valid if there's no form?
|
9
|
+
return unless record.form
|
10
|
+
|
11
|
+
# It's also valid if it has already been submitted
|
12
|
+
return if record.submitted_at_was
|
13
|
+
|
14
|
+
# we can also skip validation by setting this flag
|
15
|
+
return if record.skip_validation
|
16
|
+
|
17
|
+
record.form.response_fields.not_admin_only.reject { |rf| !rf.input_field }.each do |response_field|
|
18
|
+
@response_field = response_field
|
19
|
+
@value = @record.response_value(@response_field)
|
20
|
+
|
21
|
+
if @response_field.required? && !@record.value_present?(@response_field)
|
22
|
+
add_error "can't be blank"
|
23
|
+
next
|
24
|
+
end
|
25
|
+
|
26
|
+
if @record.value_present?(@response_field)
|
27
|
+
# Field-specific validation
|
28
|
+
add_error(@response_field.validate_response(@value))
|
29
|
+
|
30
|
+
SHARED_VALIDATION_METHODS.each do |method_name|
|
31
|
+
run_validation(method_name)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
def add_error(msg)
|
39
|
+
return unless msg.present?
|
40
|
+
@record.errors["responses_#{@response_field.id}"] << msg
|
41
|
+
end
|
42
|
+
|
43
|
+
def run_validation(method_name)
|
44
|
+
if (error_message = send(method_name))
|
45
|
+
add_error(error_message)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def min_max_length
|
50
|
+
return unless @response_field.field_options["minlength"].present? ||
|
51
|
+
@response_field.field_options["maxlength"].present?
|
52
|
+
|
53
|
+
if @response_field.field_options["min_max_length_units"] == 'words'
|
54
|
+
min_max_length_words
|
55
|
+
else
|
56
|
+
min_max_length_characters
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def min_max_length_characters
|
61
|
+
if @response_field.field_options["minlength"].present? && (@value.length < @response_field.field_options["minlength"].to_i)
|
62
|
+
return "is too short. It should be #{@response_field.field_options["minlength"]} characters or more."
|
63
|
+
end
|
64
|
+
|
65
|
+
if @response_field.field_options["maxlength"].present? && (@value.length > @response_field.field_options["maxlength"].to_i)
|
66
|
+
return "is too long. It should be #{@response_field.field_options["maxlength"]} characters or less."
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def min_max_length_words
|
71
|
+
if @response_field.field_options["minlength"].present? && (@value.scan(/\w+/).count < @response_field.field_options["minlength"].to_i)
|
72
|
+
return "is too short. It should be #{@response_field.field_options["minlength"]} words or more."
|
73
|
+
end
|
74
|
+
|
75
|
+
if @response_field.field_options["maxlength"].present? && (@value.scan(/\w+/).count > @response_field.field_options["maxlength"].to_i)
|
76
|
+
return "is too long. It should be #{@response_field.field_options["maxlength"]} words or less."
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def min_max
|
81
|
+
return unless @response_field.field_options["min"].present? ||
|
82
|
+
@response_field.field_options["max"].present?
|
83
|
+
|
84
|
+
value_for_comparison = case @response_field.field_type
|
85
|
+
when 'price'
|
86
|
+
"#{@value['dollars'] || 0}.#{@value['cents'] || 0}".to_f
|
87
|
+
else
|
88
|
+
@value.to_f
|
89
|
+
end
|
90
|
+
|
91
|
+
if @response_field.field_options["min"].present? && (value_for_comparison < @response_field.field_options["min"].to_f)
|
92
|
+
return "is too small. It should be #{@response_field.field_options["min"]} or more."
|
93
|
+
end
|
94
|
+
|
95
|
+
if @response_field.field_options["max"].present? && (value_for_comparison > @response_field.field_options["max"].to_f)
|
96
|
+
return "is too large. It should be #{@response_field.field_options["max"]} or less."
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def integer_only
|
101
|
+
if @response_field.field_options["integer_only"] && !(Integer(@value) rescue false)
|
102
|
+
"only integers allowed."
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Formbuilder
|
2
|
+
class FormRenderer
|
3
|
+
|
4
|
+
include ActionView::Helpers::UrlHelper
|
5
|
+
include ActionView::Helpers::FormTagHelper
|
6
|
+
include ActionView::Context
|
7
|
+
|
8
|
+
def protect_against_forgery?; false; end;
|
9
|
+
|
10
|
+
DEFAULT_OPTIONS = {
|
11
|
+
action: '',
|
12
|
+
method: 'POST'
|
13
|
+
}
|
14
|
+
|
15
|
+
def initialize(form, entry, options = {})
|
16
|
+
@form, @entry = form, entry
|
17
|
+
@options = self.class::DEFAULT_OPTIONS.merge(options)
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_html
|
21
|
+
form_tag @options[:action], method: @options[:method], class: 'formbuilder-form', multipart: true do
|
22
|
+
hidden_fields +
|
23
|
+
render_fields +
|
24
|
+
render_actions
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def hidden_fields
|
29
|
+
"""
|
30
|
+
<input type='hidden' name='draft_only' />
|
31
|
+
""".html_safe
|
32
|
+
end
|
33
|
+
|
34
|
+
def render_fields
|
35
|
+
@form.response_fields.map do |field|
|
36
|
+
@field = field
|
37
|
+
render_field
|
38
|
+
end.join('').html_safe
|
39
|
+
end
|
40
|
+
|
41
|
+
def render_field
|
42
|
+
value = @entry.try(:response_value, @field)
|
43
|
+
|
44
|
+
"""
|
45
|
+
<div class='response-field-wrapper response-field-#{@field.field_type} #{@entry.try(:error_for, @field) && 'error'}'>
|
46
|
+
#{render_label}
|
47
|
+
#{@field.render_input(value, entry: @entry)}
|
48
|
+
<div class='cf'></div>
|
49
|
+
#{render_error}
|
50
|
+
#{render_description}
|
51
|
+
#{render_length_validations}
|
52
|
+
#{render_min_max_validations}
|
53
|
+
</div>
|
54
|
+
"""
|
55
|
+
end
|
56
|
+
|
57
|
+
def render_label
|
58
|
+
return unless @field.input_field
|
59
|
+
|
60
|
+
"""
|
61
|
+
<label>
|
62
|
+
#{@field[:label]}
|
63
|
+
#{render_label_required if @field.required?}
|
64
|
+
</label>
|
65
|
+
"""
|
66
|
+
end
|
67
|
+
|
68
|
+
def render_label_required
|
69
|
+
"<abbr title='required'>*</abbr>"
|
70
|
+
end
|
71
|
+
|
72
|
+
def render_error
|
73
|
+
return unless @field.input_field
|
74
|
+
return unless @entry.error_for(@field)
|
75
|
+
"<span class='help-block validation-message-wrapper'>#{@entry.error_for(@field)}</span>"
|
76
|
+
end
|
77
|
+
|
78
|
+
def render_description
|
79
|
+
return unless @field.input_field
|
80
|
+
return if @field[:field_options]["description"].blank?
|
81
|
+
"<span class='help-block'>#{simple_format(@field[:field_options]["description"])}</span>"
|
82
|
+
end
|
83
|
+
|
84
|
+
def render_length_validations
|
85
|
+
return unless @field.input_field
|
86
|
+
return unless !@field.has_length_validations?
|
87
|
+
end
|
88
|
+
|
89
|
+
def render_min_max_validations
|
90
|
+
return unless @field.input_field
|
91
|
+
end
|
92
|
+
|
93
|
+
def render_actions
|
94
|
+
"""
|
95
|
+
<div class='form-actions'>
|
96
|
+
<button class='button'>Submit</button>
|
97
|
+
<a class='button save-draft-button' data-loading-text='All changes saved'>Save draft</a>
|
98
|
+
</div>
|
99
|
+
""".html_safe
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
data/lib/formbuilder.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require "formbuilder/engine"
|
2
|
+
require "formbuilder/entry"
|
3
|
+
require "formbuilder/entry_renderer"
|
4
|
+
require "formbuilder/entry_table_renderer"
|
5
|
+
require "formbuilder/entry_validator"
|
6
|
+
require "formbuilder/form_renderer"
|
7
|
+
|
8
|
+
module Formbuilder
|
9
|
+
def self.root
|
10
|
+
File.expand_path('../..', __FILE__)
|
11
|
+
end
|
12
|
+
end
|