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