decidim-friendly_signup 0.4.2 → 0.4.3
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 +4 -4
- data/app/controllers/decidim/devise/registrations_controller.rb +64 -0
- data/app/mailers/decidim/friendly_signup/confirmation_codes_mailer.rb +11 -0
- data/app/views/decidim/friendly_signup/confirmation_codes_mailer/confirmation_instructions.html.erb +1 -1
- data/config/locales/ca.yml +110 -0
- data/config/locales/de.yml +110 -0
- data/config/locales/en.yml +9 -0
- data/config/locales/es.yml +110 -0
- data/config/locales/eu.yml +110 -0
- data/config/locales/fr.yml +110 -0
- data/lib/decidim/form_builder.rb +915 -0
- data/lib/decidim/friendly_signup/user_attribute_validator.rb +4 -1
- data/lib/decidim/friendly_signup/version.rb +1 -1
- metadata +9 -2
@@ -0,0 +1,915 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "foundation_rails_helper/form_builder"
|
4
|
+
|
5
|
+
module Decidim
|
6
|
+
# This custom FormBuilder adds fields needed to deal with translatable fields,
|
7
|
+
# following the conventions set on `Decidim::TranslatableAttributes`.
|
8
|
+
class FormBuilder < FoundationRailsHelper::FormBuilder
|
9
|
+
include ActionView::Context
|
10
|
+
include Decidim::TranslatableAttributes
|
11
|
+
include Decidim::Map::Autocomplete::FormBuilder
|
12
|
+
|
13
|
+
# Public: generates a check boxes input from a collection and adds help
|
14
|
+
# text and errors.
|
15
|
+
#
|
16
|
+
# attribute - the name of the field
|
17
|
+
# collection - the collection from which we will render the check boxes
|
18
|
+
# value_attribute - a Symbol or a Proc defining how to find the value
|
19
|
+
# attribute
|
20
|
+
# text_attribute - a Symbol or a Proc defining how to find the text
|
21
|
+
# attribute
|
22
|
+
# options - a Hash with options
|
23
|
+
# html_options - a Hash with options
|
24
|
+
#
|
25
|
+
# Renders a collection of check boxes.
|
26
|
+
# rubocop:disable Metrics/ParameterLists
|
27
|
+
def collection_check_boxes(attribute, collection, value_attribute, text_attribute, options = {}, html_options = {})
|
28
|
+
super + error_and_help_text(attribute, options)
|
29
|
+
end
|
30
|
+
# rubocop:enable Metrics/ParameterLists
|
31
|
+
|
32
|
+
# Public: generates a radio buttons input from a collection and adds help
|
33
|
+
# text and errors.
|
34
|
+
#
|
35
|
+
# attribute - the name of the field
|
36
|
+
# collection - the collection from which we will render the radio buttons
|
37
|
+
# value_attribute - a Symbol or a Proc defining how to find the value attribute
|
38
|
+
# text_attribute - a Symbol or a Proc defining how to find the text attribute
|
39
|
+
# options - a Hash with options
|
40
|
+
# html_options - a Hash with options
|
41
|
+
#
|
42
|
+
# Renders a collection of radio buttons.
|
43
|
+
# rubocop:disable Metrics/ParameterLists
|
44
|
+
def collection_radio_buttons(attribute, collection, value_attribute, text_attribute, options = {}, html_options = {})
|
45
|
+
super + error_and_help_text(attribute, options)
|
46
|
+
end
|
47
|
+
# rubocop:enable Metrics/ParameterLists
|
48
|
+
|
49
|
+
def create_language_selector(locales, tabs_id, name)
|
50
|
+
if Decidim.available_locales.count > 4
|
51
|
+
language_selector_select(locales, tabs_id, name)
|
52
|
+
else
|
53
|
+
language_tabs(locales, tabs_id, name)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Public: Generates an form field for each locale.
|
58
|
+
#
|
59
|
+
# type - The form field's type, like `text_area` or `text_input`
|
60
|
+
# name - The name of the field
|
61
|
+
# options - The set of options to send to the field
|
62
|
+
#
|
63
|
+
# Renders form fields for each locale.
|
64
|
+
def translated(type, name, options = {})
|
65
|
+
return translated_one_locale(type, name, locales.first, options.merge(label: (options[:label] || label_for(name)))) if locales.count == 1
|
66
|
+
|
67
|
+
tabs_id = sanitize_tabs_selector(options[:tabs_id] || "#{object_name}-#{name}-tabs")
|
68
|
+
|
69
|
+
label_tabs = content_tag(:div, class: "label--tabs") do
|
70
|
+
field_label = label_i18n(name, options[:label] || label_for(name))
|
71
|
+
|
72
|
+
language_selector = "".html_safe
|
73
|
+
language_selector = create_language_selector(locales, tabs_id, name) if options[:label] != false
|
74
|
+
|
75
|
+
safe_join [field_label, language_selector]
|
76
|
+
end
|
77
|
+
|
78
|
+
hashtaggable = options.delete(:hashtaggable)
|
79
|
+
tabs_content = content_tag(:div, class: "tabs-content", data: { tabs_content: tabs_id }) do
|
80
|
+
locales.each_with_index.inject("".html_safe) do |string, (locale, index)|
|
81
|
+
tab_content_id = "#{tabs_id}-#{name}-panel-#{index}"
|
82
|
+
string + content_tag(:div, class: tab_element_class_for("panel", index), id: tab_content_id) do
|
83
|
+
if hashtaggable
|
84
|
+
hashtaggable_text_field(type, name, locale, options.merge(label: false))
|
85
|
+
elsif type.to_sym == :editor
|
86
|
+
send(type, name_with_locale(name, locale), options.merge(label: false, hashtaggable: hashtaggable))
|
87
|
+
else
|
88
|
+
send(type, name_with_locale(name, locale), options.merge(label: false))
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
safe_join [label_tabs, tabs_content]
|
95
|
+
end
|
96
|
+
|
97
|
+
def translated_one_locale(type, name, locale, options = {})
|
98
|
+
return hashtaggable_text_field(type, name, locale, options) if options.delete(:hashtaggable)
|
99
|
+
|
100
|
+
send(
|
101
|
+
type,
|
102
|
+
"#{name}_#{locale.to_s.gsub("-", "__")}",
|
103
|
+
options.merge(label: options[:label] || label_for(name))
|
104
|
+
)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Public: Generates a field for hashtaggable type.
|
108
|
+
# type - The form field's type, like `text_area` or `text_input`
|
109
|
+
# name - The name of the field
|
110
|
+
# handlers - The social handlers to be created
|
111
|
+
# options - The set of options to send to the field
|
112
|
+
#
|
113
|
+
# Renders form fields for each locale.
|
114
|
+
def hashtaggable_text_field(type, name, locale, options = {})
|
115
|
+
options[:hashtaggable] = true if type.to_sym == :editor
|
116
|
+
|
117
|
+
content_tag(:div, class: "hashtags__container") do
|
118
|
+
if options[:value]
|
119
|
+
send(type, name_with_locale(name, locale), options.merge(label: options[:label], value: options[:value][locale]))
|
120
|
+
else
|
121
|
+
send(type, name_with_locale(name, locale), options.merge(label: options[:label]))
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Public: Generates an form field for each social.
|
127
|
+
#
|
128
|
+
# type - The form field's type, like `text_area` or `text_input`
|
129
|
+
# name - The name of the field
|
130
|
+
# handlers - The social handlers to be created
|
131
|
+
# options - The set of options to send to the field
|
132
|
+
#
|
133
|
+
# Renders form fields for each locale.
|
134
|
+
def social_field(type, name, handlers, options = {})
|
135
|
+
tabs_id = sanitize_tabs_selector(options[:tabs_id] || "#{object_name}-#{name}-tabs")
|
136
|
+
|
137
|
+
label_tabs = content_tag(:div, class: "label--tabs") do
|
138
|
+
field_label = label_i18n(name, options[:label] || label_for(name))
|
139
|
+
|
140
|
+
tabs_panels = "".html_safe
|
141
|
+
if options[:label] != false
|
142
|
+
tabs_panels = content_tag(:ul, class: "tabs tabs--lang", id: tabs_id, data: { tabs: true }) do
|
143
|
+
handlers.each_with_index.inject("".html_safe) do |string, (handler, index)|
|
144
|
+
string + content_tag(:li, class: tab_element_class_for("title", index)) do
|
145
|
+
title = I18n.t(".#{handler}", scope: "activemodel.attributes.#{object_name}")
|
146
|
+
tab_content_id = sanitize_tabs_selector "#{tabs_id}-#{name}-panel-#{index}"
|
147
|
+
content_tag(:a, title, href: "##{tab_content_id}")
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
safe_join [field_label, tabs_panels]
|
154
|
+
end
|
155
|
+
|
156
|
+
tabs_content = content_tag(:div, class: "tabs-content", data: { tabs_content: tabs_id }) do
|
157
|
+
handlers.each_with_index.inject("".html_safe) do |string, (handler, index)|
|
158
|
+
tab_content_id = sanitize_tabs_selector "#{tabs_id}-#{name}-panel-#{index}"
|
159
|
+
string + content_tag(:div, class: tab_element_class_for("panel", index), id: tab_content_id) do
|
160
|
+
send(type, "#{handler}_handler", options.merge(label: false))
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
safe_join [label_tabs, tabs_content]
|
166
|
+
end
|
167
|
+
|
168
|
+
# Public: generates a hidden field and a container for WYSIWYG editor
|
169
|
+
#
|
170
|
+
# name - The name of the field
|
171
|
+
# options - The set of options to send to the field
|
172
|
+
# :label - The Boolean value to create or not the input label (optional) (default: true)
|
173
|
+
# :toolbar - The String value to configure WYSIWYG toolbar. It should be 'basic' or
|
174
|
+
# or 'full' (optional) (default: 'basic')
|
175
|
+
# :lines - The Integer to indicate how many lines should editor have (optional) (default: 10)
|
176
|
+
# :disabled - Whether the editor should be disabled
|
177
|
+
# :editor_images - Allow attached images (optional) (default: false)
|
178
|
+
#
|
179
|
+
# Renders a container with both hidden field and editor container
|
180
|
+
def editor(name, options = {})
|
181
|
+
options[:disabled] ||= false
|
182
|
+
toolbar = options.delete(:toolbar) || "basic"
|
183
|
+
lines = options.delete(:lines) || 10
|
184
|
+
label_text = options[:label].to_s
|
185
|
+
label_text = label_for(name) if label_text.blank?
|
186
|
+
options.delete(:required)
|
187
|
+
hashtaggable = options.delete(:hashtaggable)
|
188
|
+
hidden_options = extract_validations(name, options).merge(options)
|
189
|
+
|
190
|
+
content_tag(:div, class: "editor #{"hashtags__container" if hashtaggable}") do
|
191
|
+
template = ""
|
192
|
+
template += label(name, label_text + required_for_attribute(name)) if options.fetch(:label, true)
|
193
|
+
template += hidden_field(name, hidden_options)
|
194
|
+
template += content_tag(:div, nil, class: "editor-container #{"js-hashtags" if hashtaggable}", data: {
|
195
|
+
toolbar: toolbar,
|
196
|
+
disabled: options[:disabled]
|
197
|
+
}.merge(editor_images_options(options)), style: "height: #{lines}rem")
|
198
|
+
template += error_for(name, options) if error?(name)
|
199
|
+
template.html_safe
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# Public: Generates a select field with the categories. Only leaf categories can be set as selected.
|
204
|
+
#
|
205
|
+
# name - The name of the field (usually category_id)
|
206
|
+
# collection - A collection of categories.
|
207
|
+
# options - An optional Hash with options:
|
208
|
+
# - prompt - An optional String with the text to display as prompt.
|
209
|
+
# - disable_parents - A Boolean to disable parent categories. Defaults to `true`.
|
210
|
+
# html_options - HTML options for the select
|
211
|
+
#
|
212
|
+
# Returns a String.
|
213
|
+
def categories_select(name, collection, options = {}, html_options = {})
|
214
|
+
options = {
|
215
|
+
disable_parents: true
|
216
|
+
}.merge(options)
|
217
|
+
|
218
|
+
disable_parents = options[:disable_parents]
|
219
|
+
|
220
|
+
selected = object.send(name)
|
221
|
+
selected = selected.first if selected.is_a?(Array) && selected.length > 1
|
222
|
+
categories = categories_for_select(collection)
|
223
|
+
disabled = if disable_parents
|
224
|
+
disabled_categories_for(collection)
|
225
|
+
else
|
226
|
+
[]
|
227
|
+
end
|
228
|
+
|
229
|
+
select(name, @template.options_for_select(categories, selected: selected, disabled: disabled), options, html_options)
|
230
|
+
end
|
231
|
+
|
232
|
+
# Public: Generates a select field for areas.
|
233
|
+
#
|
234
|
+
# name - The name of the field (usually area_id)
|
235
|
+
# collection - A collection of areas or area_types.
|
236
|
+
# If it's areas, we sort the selectable options alphabetically.
|
237
|
+
#
|
238
|
+
# Returns a String.
|
239
|
+
def areas_select(name, collection, options = {}, html_options = {})
|
240
|
+
selectables = if collection.first.is_a?(Decidim::Area)
|
241
|
+
assemblies = collection
|
242
|
+
.map { |a| [a.name[I18n.locale.to_s], a.id] }
|
243
|
+
.sort_by { |arr| arr[0] }
|
244
|
+
|
245
|
+
@template.options_for_select(
|
246
|
+
assemblies,
|
247
|
+
selected: options[:selected]
|
248
|
+
)
|
249
|
+
else
|
250
|
+
@template.option_groups_from_collection_for_select(
|
251
|
+
collection,
|
252
|
+
:areas,
|
253
|
+
:translated_name,
|
254
|
+
:id,
|
255
|
+
:translated_name,
|
256
|
+
selected: options[:selected]
|
257
|
+
)
|
258
|
+
end
|
259
|
+
|
260
|
+
select(name, selectables, options, html_options)
|
261
|
+
end
|
262
|
+
|
263
|
+
# Public: Generates a select field for resource types.
|
264
|
+
#
|
265
|
+
# name - The name of the field (usually resource_type)
|
266
|
+
# collection - A collection of resource types.
|
267
|
+
# The options are sorted alphabetically.
|
268
|
+
#
|
269
|
+
# Returns a String.
|
270
|
+
def resources_select(name, collection, options = {})
|
271
|
+
resources =
|
272
|
+
collection
|
273
|
+
.map { |r| [I18n.t(r.split("::").last.underscore, scope: "decidim.components.component_order_selector.order"), r] }
|
274
|
+
.sort
|
275
|
+
|
276
|
+
select(name, @template.options_for_select(resources, selected: options[:selected]), options)
|
277
|
+
end
|
278
|
+
|
279
|
+
# Public: Generates a picker field for scope selection.
|
280
|
+
#
|
281
|
+
# attribute - The name of the field (usually scope_id)
|
282
|
+
# options - An optional Hash with options:
|
283
|
+
# - multiple - Multiple mode, to allow multiple scopes selection.
|
284
|
+
# - label - Show label?
|
285
|
+
# - checkboxes_on_top - Show checked picker values on top (default) or below the picker prompt (only for multiple pickers)
|
286
|
+
# - namespace - prepend a custom name to the html element's DOM id.
|
287
|
+
#
|
288
|
+
# Also it should receive a block that returns a Hash with :url and :text for each selected scope (and for null scope for prompt)
|
289
|
+
#
|
290
|
+
# Returns a String.
|
291
|
+
def scopes_picker(attribute, options = {})
|
292
|
+
id = if self.options.has_key?(:namespace)
|
293
|
+
"#{self.options[:namespace]}_#{sanitize_for_dom_selector(@object_name)}"
|
294
|
+
else
|
295
|
+
"#{sanitize_for_dom_selector(@object_name)}_#{attribute}"
|
296
|
+
end
|
297
|
+
|
298
|
+
picker_options = {
|
299
|
+
id: id,
|
300
|
+
class: "picker-#{options[:multiple] ? "multiple" : "single"}",
|
301
|
+
name: "#{@object_name}[#{attribute}]"
|
302
|
+
}
|
303
|
+
|
304
|
+
picker_options[:class] += " is-invalid-input" if error?(attribute)
|
305
|
+
|
306
|
+
prompt_params = yield(nil)
|
307
|
+
scopes = selected_scopes(attribute).map { |scope| [scope, yield(scope)] }
|
308
|
+
template = ""
|
309
|
+
template += "<label>#{label_for(attribute) + required_for_attribute(attribute)}</label>" unless options[:label] == false
|
310
|
+
template += @template.render("decidim/scopes/scopes_picker_input",
|
311
|
+
picker_options: picker_options,
|
312
|
+
prompt_params: prompt_params,
|
313
|
+
scopes: scopes,
|
314
|
+
values_on_top: !options[:multiple] || options[:checkboxes_on_top])
|
315
|
+
template += error_and_help_text(attribute, options)
|
316
|
+
template.html_safe
|
317
|
+
end
|
318
|
+
|
319
|
+
# Public: Generates a picker field for selection (either simple or multiselect).
|
320
|
+
#
|
321
|
+
# attribute - The name of the object's attribute.
|
322
|
+
# options - A Hash with options:
|
323
|
+
# - multiple: Multiple mode, to allow selection of multiple items.
|
324
|
+
# - label: Show label?
|
325
|
+
# - name: (optional) The name attribute of the input elements.
|
326
|
+
# prompt_params - Hash with options:
|
327
|
+
# - url: The url where the ajax endpoint that will fill the content of the selector popup (the prompt).
|
328
|
+
# - text: Text in the button to open the Data Picker selector.
|
329
|
+
#
|
330
|
+
# Also it should receive a block that returns a Hash with :url and :text for each selected scope
|
331
|
+
#
|
332
|
+
# Returns an html String.
|
333
|
+
def data_picker(attribute, options = {}, prompt_params = {})
|
334
|
+
picker_options = {
|
335
|
+
id: "#{@object_name}_#{attribute}",
|
336
|
+
class: "picker-#{options[:multiple] ? "multiple" : "single"}",
|
337
|
+
name: options[:name] || "#{@object_name}[#{attribute}]"
|
338
|
+
}
|
339
|
+
picker_options[:class] += " is-invalid-input" if error?(attribute)
|
340
|
+
picker_options[:class] += " picker-autosort" if options[:autosort]
|
341
|
+
|
342
|
+
items = object.send(attribute).collect { |item| [item, yield(item)] }
|
343
|
+
|
344
|
+
template = ""
|
345
|
+
template += label(attribute, label_for(attribute) + required_for_attribute(attribute)) unless options[:label] == false
|
346
|
+
template += @template.render("decidim/widgets/data_picker", picker_options: picker_options, prompt_params: prompt_params, items: items)
|
347
|
+
template += error_and_help_text(attribute, options)
|
348
|
+
template.html_safe
|
349
|
+
end
|
350
|
+
|
351
|
+
# Public: Override so checkboxes are rendered before the label.
|
352
|
+
def check_box(attribute, options = {}, checked_value = "1", unchecked_value = "0")
|
353
|
+
custom_label(attribute, options[:label], options[:label_options], field_before_label: true) do
|
354
|
+
options.delete(:label)
|
355
|
+
options.delete(:label_options)
|
356
|
+
@template.check_box(@object_name, attribute, objectify_options(options), checked_value, unchecked_value)
|
357
|
+
end + error_and_help_text(attribute, options)
|
358
|
+
end
|
359
|
+
|
360
|
+
# Public: Override so the date fields are rendered using foundation
|
361
|
+
# datepicker library
|
362
|
+
def date_field(attribute, options = {})
|
363
|
+
value = object.send(attribute)
|
364
|
+
data = { datepicker: "" }
|
365
|
+
data[:startdate] = I18n.l(value, format: :decidim_short) if value.present? && value.is_a?(Date)
|
366
|
+
datepicker_format = ruby_format_to_datepicker(I18n.t("date.formats.decidim_short"))
|
367
|
+
data[:"date-format"] = datepicker_format
|
368
|
+
|
369
|
+
template = text_field(
|
370
|
+
attribute,
|
371
|
+
options.merge(data: data)
|
372
|
+
)
|
373
|
+
help_text = I18n.t("decidim.datepicker.help_text", datepicker_format: datepicker_format)
|
374
|
+
template += error_and_help_text(attribute, options.merge(help_text: help_text))
|
375
|
+
template.html_safe
|
376
|
+
end
|
377
|
+
|
378
|
+
# Public: Generates a timepicker field using foundation
|
379
|
+
# datepicker library
|
380
|
+
def datetime_field(attribute, options = {})
|
381
|
+
value = object.send(attribute)
|
382
|
+
data = { datepicker: "", timepicker: "" }
|
383
|
+
data[:startdate] = I18n.l(value, format: :decidim_short) if value.present? && value.is_a?(ActiveSupport::TimeWithZone)
|
384
|
+
datepicker_format = ruby_format_to_datepicker(I18n.t("time.formats.decidim_short"))
|
385
|
+
data[:"date-format"] = datepicker_format
|
386
|
+
|
387
|
+
template = text_field(
|
388
|
+
attribute,
|
389
|
+
options.merge(data: data)
|
390
|
+
)
|
391
|
+
help_text = I18n.t("decidim.datepicker.help_text", datepicker_format: datepicker_format)
|
392
|
+
template += content_tag(:span, help_text, class: "help-text")
|
393
|
+
template.html_safe
|
394
|
+
end
|
395
|
+
|
396
|
+
# Public: Generates a file upload field and sets the form as multipart.
|
397
|
+
# If the file is an image it displays the default image if present or the current one.
|
398
|
+
# By default it also generates a checkbox to delete the file. This checkbox can
|
399
|
+
# be hidden if `options[:optional]` is passed as `false`.
|
400
|
+
#
|
401
|
+
# attribute - The String name of the attribute to buidl the field.
|
402
|
+
# options - A Hash with options to build the field.
|
403
|
+
# * optional: Whether the file can be optional or not.
|
404
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
405
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
406
|
+
def upload(attribute, options = {})
|
407
|
+
self.multipart = true
|
408
|
+
options[:optional] = options[:optional].nil? ? true : options[:optional]
|
409
|
+
label_text = options[:label] || label_for(attribute)
|
410
|
+
alt_text = label_text
|
411
|
+
|
412
|
+
file = object.send attribute
|
413
|
+
template = ""
|
414
|
+
template += label(attribute, label_text + required_for_attribute(attribute))
|
415
|
+
template += upload_help(attribute, options)
|
416
|
+
template += @template.file_field @object_name, attribute
|
417
|
+
|
418
|
+
template += extension_allowlist_help(options[:extension_allowlist]) if options[:extension_allowlist].present?
|
419
|
+
template += image_dimensions_help(options[:dimensions_info]) if options[:dimensions_info].present?
|
420
|
+
|
421
|
+
default_image_path = uploader_default_image_path(attribute)
|
422
|
+
file_path = file_attachment_path(file)
|
423
|
+
|
424
|
+
if file_path.present? && file.attachment.image? || default_image_path.present?
|
425
|
+
if file_path.present?
|
426
|
+
template += @template.content_tag :label, I18n.t("current_image", scope: "decidim.forms")
|
427
|
+
template += @template.link_to @template.image_tag(file_path, alt: alt_text), file_path, target: "_blank", rel: "noopener"
|
428
|
+
else
|
429
|
+
template += @template.content_tag :label, I18n.t("default_image", scope: "decidim.forms")
|
430
|
+
template += @template.link_to @template.image_tag(default_image_path, alt: alt_text), default_image_path, target: "_blank", rel: "noopener"
|
431
|
+
end
|
432
|
+
elsif file_path.present?
|
433
|
+
template += @template.label_tag I18n.t("current_file", scope: "decidim.forms")
|
434
|
+
template += @template.link_to file.filename, file_path, target: "_blank", rel: "noopener"
|
435
|
+
end
|
436
|
+
|
437
|
+
if file_path.present? && options[:optional]
|
438
|
+
template += content_tag :div, class: "field" do
|
439
|
+
safe_join([
|
440
|
+
@template.check_box(@object_name, "remove_#{attribute}"),
|
441
|
+
label("remove_#{attribute}", I18n.t("remove_this_file", scope: "decidim.forms"))
|
442
|
+
])
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
if object.errors[attribute].any?
|
447
|
+
template += content_tag :p, class: "is-invalid-label" do
|
448
|
+
safe_join object.errors[attribute], "<br/>".html_safe
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
template.html_safe
|
453
|
+
end
|
454
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
455
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
456
|
+
|
457
|
+
def upload_help(attribute, options = {})
|
458
|
+
humanizer = FileValidatorHumanizer.new(object, attribute)
|
459
|
+
|
460
|
+
help_scope = begin
|
461
|
+
if options[:help_i18n_scope].present?
|
462
|
+
options[:help_i18n_scope]
|
463
|
+
elsif humanizer.uploader.is_a?(Decidim::ImageUploader)
|
464
|
+
"decidim.forms.file_help.image"
|
465
|
+
else
|
466
|
+
"decidim.forms.file_help.file"
|
467
|
+
end
|
468
|
+
end
|
469
|
+
|
470
|
+
help_messages = begin
|
471
|
+
if options[:help_i18n_messages].present?
|
472
|
+
Array(options[:help_i18n_messages])
|
473
|
+
else
|
474
|
+
%w(message_1 message_2)
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
content_tag(:div, class: "help-text") do
|
479
|
+
inner = "<p>#{I18n.t("explanation", scope: help_scope)}</p>".html_safe
|
480
|
+
inner + content_tag(:ul) do
|
481
|
+
messages = help_messages.each.map { |msg| I18n.t(msg, scope: help_scope) }
|
482
|
+
messages += humanizer.messages
|
483
|
+
|
484
|
+
messages.map { |msg| content_tag(:li, msg) }.join("\n").html_safe
|
485
|
+
end.html_safe
|
486
|
+
end
|
487
|
+
end
|
488
|
+
|
489
|
+
# Public: Returns the translated name for the given attribute.
|
490
|
+
#
|
491
|
+
# attribute - The String name of the attribute to return the name.
|
492
|
+
def label_for(attribute)
|
493
|
+
if object.class.respond_to?(:human_attribute_name)
|
494
|
+
object.class.human_attribute_name(attribute)
|
495
|
+
else
|
496
|
+
attribute.to_s.humanize
|
497
|
+
end
|
498
|
+
end
|
499
|
+
|
500
|
+
def form_field_for(attribute, options = {})
|
501
|
+
if attribute == :body
|
502
|
+
text_area(attribute, options.merge(rows: 10))
|
503
|
+
else
|
504
|
+
text_field(attribute, options)
|
505
|
+
end
|
506
|
+
end
|
507
|
+
|
508
|
+
# Discard the pattern attribute which is not allowed for textarea elements.
|
509
|
+
def text_area(attribute, options = {})
|
510
|
+
field(attribute, options) do |opts|
|
511
|
+
opts.delete(:pattern)
|
512
|
+
@template.send(
|
513
|
+
:text_area,
|
514
|
+
@object_name,
|
515
|
+
attribute,
|
516
|
+
objectify_options(opts)
|
517
|
+
)
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
private
|
522
|
+
|
523
|
+
# Private: Override from FoundationRailsHelper in order to render
|
524
|
+
# inputs inside the label and to automatically inject validations
|
525
|
+
# from the object.
|
526
|
+
#
|
527
|
+
# attribute - The String name of the attribute to buidl the field.
|
528
|
+
# options - A Hash with options to build the field.
|
529
|
+
# html_options - An optional Hash with options to pass to the html element.
|
530
|
+
#
|
531
|
+
# Returns a String
|
532
|
+
def field(attribute, options, html_options = nil, &block)
|
533
|
+
label = options.delete(:label)
|
534
|
+
label_options = options.delete(:label_options)
|
535
|
+
custom_label(attribute, label, label_options) do
|
536
|
+
field_with_validations(attribute, options, html_options, &block)
|
537
|
+
end
|
538
|
+
end
|
539
|
+
|
540
|
+
# Private: Builds a form field and detects validations from
|
541
|
+
# the form object.
|
542
|
+
#
|
543
|
+
# attribute - The String name of the attribute to build the field.
|
544
|
+
# options - A Hash with options to build the field.
|
545
|
+
# html_options - An optional Hash with options to pass to the html element.
|
546
|
+
#
|
547
|
+
# Returns a String.
|
548
|
+
def field_with_validations(attribute, options, html_options)
|
549
|
+
class_options = html_options || options
|
550
|
+
|
551
|
+
if error?(attribute)
|
552
|
+
class_options[:class] = class_options[:class].to_s
|
553
|
+
class_options[:class] += " is-invalid-input"
|
554
|
+
end
|
555
|
+
|
556
|
+
help_text = options.delete(:help_text)
|
557
|
+
prefix = options.delete(:prefix)
|
558
|
+
postfix = options.delete(:postfix)
|
559
|
+
|
560
|
+
class_options = extract_validations(attribute, options).merge(class_options)
|
561
|
+
|
562
|
+
content = yield(class_options)
|
563
|
+
content += abide_error_element(attribute) if class_options[:pattern] || class_options[:required]
|
564
|
+
content = content.html_safe
|
565
|
+
|
566
|
+
html = wrap_prefix_and_postfix(content, prefix, postfix)
|
567
|
+
html + error_and_help_text(attribute, options.merge(help_text: help_text))
|
568
|
+
end
|
569
|
+
|
570
|
+
# rubocop: disable Metrics/CyclomaticComplexity
|
571
|
+
# rubocop: disable Metrics/PerceivedComplexity
|
572
|
+
|
573
|
+
# Private: Builds a Hash of options to be injected at the HTML output as
|
574
|
+
# HTML5 validations.
|
575
|
+
#
|
576
|
+
# attribute - The String name of the attribute to extract the validations.
|
577
|
+
# options - A Hash of options to extract validations.
|
578
|
+
#
|
579
|
+
# Returns a Hash.
|
580
|
+
def extract_validations(attribute, options)
|
581
|
+
min_length = options.delete(:minlength) || length_for_attribute(attribute, :minimum) || 0
|
582
|
+
max_length = options.delete(:maxlength) || length_for_attribute(attribute, :maximum)
|
583
|
+
|
584
|
+
validation_options = {}
|
585
|
+
validation_options[:pattern] = "^(.|[\n\r]){#{min_length},#{max_length}}$" if min_length.to_i.positive? || max_length.to_i.positive?
|
586
|
+
validation_options[:required] = options[:required] || attribute_required?(attribute)
|
587
|
+
validation_options[:minlength] ||= min_length if min_length.to_i.positive?
|
588
|
+
validation_options[:maxlength] ||= max_length if max_length.to_i.positive?
|
589
|
+
validation_options
|
590
|
+
end
|
591
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|
592
|
+
# rubocop: enable Metrics/PerceivedComplexity
|
593
|
+
|
594
|
+
# Private: Tries to find if an attribute is required in the form object.
|
595
|
+
#
|
596
|
+
# Returns Boolean.
|
597
|
+
def attribute_required?(attribute)
|
598
|
+
validator = find_validator(attribute, ActiveModel::Validations::PresenceValidator) ||
|
599
|
+
find_validator(attribute, TranslatablePresenceValidator)
|
600
|
+
|
601
|
+
return unless validator
|
602
|
+
|
603
|
+
# Check if the if condition is present and it evaluates to true
|
604
|
+
if_condition = validator.options[:if]
|
605
|
+
validator_if_condition = if_condition.nil? ||
|
606
|
+
(string_or_symbol?(if_condition) ? object.send(if_condition) : if_condition.call(object))
|
607
|
+
|
608
|
+
# Check if the unless condition is present and it evaluates to false
|
609
|
+
unless_condition = validator.options[:unless]
|
610
|
+
validator_unless_condition = unless_condition.nil? ||
|
611
|
+
(string_or_symbol?(unless_condition) ? !object.send(unless_condition) : !unless_condition.call(object))
|
612
|
+
|
613
|
+
validator_if_condition && validator_unless_condition
|
614
|
+
end
|
615
|
+
|
616
|
+
def string_or_symbol?(obj)
|
617
|
+
obj.is_a?(String) || obj.is_a?(Symbol)
|
618
|
+
end
|
619
|
+
|
620
|
+
# Private: Tries to find a length validator in the form object.
|
621
|
+
#
|
622
|
+
# attribute - The attribute to look for the validations.
|
623
|
+
# type - A Symbol for the type of length to fetch. Currently only :minimum & :maximum are supported.
|
624
|
+
#
|
625
|
+
# Returns an Integer or Nil.
|
626
|
+
def length_for_attribute(attribute, type)
|
627
|
+
length_validator = find_validator(attribute, ActiveModel::Validations::LengthValidator)
|
628
|
+
return length_validator.options[type] if length_validator
|
629
|
+
|
630
|
+
length_validator = find_validator(attribute, ProposalLengthValidator) if Object.const_defined?("ProposalLengthValidator")
|
631
|
+
if length_validator
|
632
|
+
length = length_validator.options[type]
|
633
|
+
return length.call(object) if length.respond_to?(:call)
|
634
|
+
|
635
|
+
return length
|
636
|
+
end
|
637
|
+
|
638
|
+
nil
|
639
|
+
end
|
640
|
+
|
641
|
+
# Private: Finds a validator.
|
642
|
+
#
|
643
|
+
# attribute - The attribute to validate.
|
644
|
+
# klass - The Class of the validator to find.
|
645
|
+
#
|
646
|
+
# Returns a klass object.
|
647
|
+
def find_validator(attribute, klass)
|
648
|
+
return unless object.respond_to?(:_validators)
|
649
|
+
|
650
|
+
object._validators[attribute.to_sym].find { |validator| validator.class == klass }
|
651
|
+
end
|
652
|
+
|
653
|
+
# Private: Override method from FoundationRailsHelper to render the text of the
|
654
|
+
# label before the input, instead of after.
|
655
|
+
#
|
656
|
+
# attribute - The String name of the attribute we're build the label.
|
657
|
+
# text - The String text to use as label.
|
658
|
+
# options - A Hash to build the label.
|
659
|
+
#
|
660
|
+
# Returns a String.
|
661
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
662
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
663
|
+
def custom_label(attribute, text, options, field_before_label: false, show_required: true)
|
664
|
+
return block_given? ? yield.html_safe : "".html_safe if text == false
|
665
|
+
|
666
|
+
text = default_label_text(object, attribute) if text.nil? || text == true
|
667
|
+
text += required_for_attribute(attribute) if show_required
|
668
|
+
|
669
|
+
text = if field_before_label && block_given?
|
670
|
+
safe_join([yield, text.html_safe])
|
671
|
+
elsif block_given?
|
672
|
+
safe_join([text.html_safe, yield])
|
673
|
+
end
|
674
|
+
|
675
|
+
label(attribute, text, options || {})
|
676
|
+
end
|
677
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
678
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
679
|
+
|
680
|
+
# Private: Builds a span to be shown when there's a validation error in a field.
|
681
|
+
# It looks for the text that will be the content in a similar way `human_attribute_name`
|
682
|
+
# does it.
|
683
|
+
#
|
684
|
+
# attribute - The name of the attribute of the field.
|
685
|
+
#
|
686
|
+
# Returns a String.
|
687
|
+
def abide_error_element(attribute)
|
688
|
+
defaults = []
|
689
|
+
defaults << :"decidim.forms.errors.#{object.class.model_name.i18n_key}.#{attribute}"
|
690
|
+
defaults << :"decidim.forms.errors.#{attribute}"
|
691
|
+
defaults << :"forms.errors.#{attribute}"
|
692
|
+
defaults << :"decidim.forms.errors.error"
|
693
|
+
|
694
|
+
options = { count: 1, default: defaults }
|
695
|
+
|
696
|
+
text = I18n.t(defaults.shift, **options)
|
697
|
+
content_tag(:span, text, class: "form-error")
|
698
|
+
end
|
699
|
+
|
700
|
+
def categories_for_select(scope)
|
701
|
+
sorted_main_categories = scope.first_class.includes(:subcategories).sort_by do |category|
|
702
|
+
[category.weight, translated_attribute(category.name, category.participatory_space.organization)]
|
703
|
+
end
|
704
|
+
|
705
|
+
sorted_main_categories.flat_map do |category|
|
706
|
+
parent = [[translated_attribute(category.name, category.participatory_space.organization), category.id]]
|
707
|
+
|
708
|
+
sorted_subcategories = category.subcategories.sort_by do |subcategory|
|
709
|
+
[subcategory.weight, translated_attribute(subcategory.name, subcategory.participatory_space.organization)]
|
710
|
+
end
|
711
|
+
|
712
|
+
sorted_subcategories.each do |subcategory|
|
713
|
+
parent << ["- #{translated_attribute(subcategory.name, subcategory.participatory_space.organization)}", subcategory.id]
|
714
|
+
end
|
715
|
+
|
716
|
+
parent
|
717
|
+
end
|
718
|
+
end
|
719
|
+
|
720
|
+
def disabled_categories_for(scope)
|
721
|
+
scope.first_class.joins(:subcategories).pluck(:id)
|
722
|
+
end
|
723
|
+
|
724
|
+
def tab_element_class_for(type, index)
|
725
|
+
element_class = "tabs-#{type}"
|
726
|
+
element_class += " is-active" if index.zero?
|
727
|
+
element_class
|
728
|
+
end
|
729
|
+
|
730
|
+
def locales
|
731
|
+
@locales ||= if @template.respond_to?(:available_locales)
|
732
|
+
Set.new([@template.current_locale] + @template.available_locales).to_a
|
733
|
+
else
|
734
|
+
Decidim.available_locales
|
735
|
+
end
|
736
|
+
end
|
737
|
+
|
738
|
+
def name_with_locale(name, locale)
|
739
|
+
"#{name}_#{locale.to_s.gsub("-", "__")}"
|
740
|
+
end
|
741
|
+
|
742
|
+
def label_i18n(attribute, text = nil, options = {})
|
743
|
+
errored = error?(attribute) || locales.any? { |locale| error?(name_with_locale(attribute, locale)) }
|
744
|
+
|
745
|
+
if errored
|
746
|
+
options[:class] ||= ""
|
747
|
+
options[:class] += " is-invalid-label"
|
748
|
+
end
|
749
|
+
text += required_for_attribute(attribute)
|
750
|
+
|
751
|
+
label(attribute, (text || "").html_safe, options)
|
752
|
+
end
|
753
|
+
|
754
|
+
# Private: Returns default url for attribute when uploader is an
|
755
|
+
# image and has defined a default url
|
756
|
+
def uploader_default_image_path(attribute)
|
757
|
+
uploader = FileValidatorHumanizer.new(object, attribute).uploader
|
758
|
+
return if uploader.blank?
|
759
|
+
return unless uploader.is_a?(Decidim::ImageUploader)
|
760
|
+
|
761
|
+
uploader.try(:default_url)
|
762
|
+
end
|
763
|
+
|
764
|
+
# Private: Returns blob path when file is attached
|
765
|
+
def file_attachment_path(file)
|
766
|
+
return unless file && file.try(:attached?)
|
767
|
+
|
768
|
+
Rails.application.routes.url_helpers.rails_blob_url(file.blob, only_path: true)
|
769
|
+
end
|
770
|
+
|
771
|
+
def required_for_attribute(attribute)
|
772
|
+
if attribute_required?(attribute) || (attribute == :password && object.class.name == "Decidim::RegistrationForm")
|
773
|
+
visible_title = content_tag(:span, "*", "aria-hidden": true)
|
774
|
+
screenreader_title = content_tag(
|
775
|
+
:span,
|
776
|
+
I18n.t("required", scope: "forms"),
|
777
|
+
class: "show-for-sr"
|
778
|
+
)
|
779
|
+
return content_tag(
|
780
|
+
:span,
|
781
|
+
visible_title + screenreader_title,
|
782
|
+
title: I18n.t("required", scope: "forms"),
|
783
|
+
data: { tooltip: true, disable_hover: false, keep_on_hover: true },
|
784
|
+
class: "label-required"
|
785
|
+
).html_safe
|
786
|
+
end
|
787
|
+
"".html_safe
|
788
|
+
end
|
789
|
+
|
790
|
+
# Private: Returns an array of scopes related to object attribute
|
791
|
+
def selected_scopes(attribute)
|
792
|
+
selected = object.send(attribute) || []
|
793
|
+
selected = selected.values if selected.is_a?(Hash)
|
794
|
+
selected = [selected] unless selected.is_a?(Array)
|
795
|
+
selected = Decidim::Scope.where(id: selected.map(&:to_i)) unless selected.first.is_a?(Decidim::Scope)
|
796
|
+
selected
|
797
|
+
end
|
798
|
+
|
799
|
+
# Private: Returns the help text and error tags at the end of the field.
|
800
|
+
# Modified to change the tag to a valid HTML tag inside the <label> element.
|
801
|
+
def error_and_help_text(attribute, options = {})
|
802
|
+
html = ""
|
803
|
+
html += content_tag(:span, options[:help_text], class: "help-text") if options[:help_text]
|
804
|
+
html += error_for(attribute, options) || ""
|
805
|
+
html.html_safe
|
806
|
+
end
|
807
|
+
|
808
|
+
def ruby_format_to_datepicker(ruby_date_format)
|
809
|
+
ruby_date_format.gsub("%d", "dd").gsub("%m", "mm").gsub("%Y", "yyyy").gsub("%H", "hh").gsub("%M", "ii")
|
810
|
+
end
|
811
|
+
|
812
|
+
def sanitize_tabs_selector(id)
|
813
|
+
id.tr("[", "-").tr("]", "-")
|
814
|
+
end
|
815
|
+
|
816
|
+
def sanitize_for_dom_selector(name)
|
817
|
+
name.to_s.parameterize.underscore
|
818
|
+
end
|
819
|
+
|
820
|
+
def extension_allowlist_help(extension_allowlist)
|
821
|
+
content_tag :p, class: "extensions-help help-text" do
|
822
|
+
safe_join([
|
823
|
+
content_tag(:span, I18n.t("extension_allowlist", scope: "decidim.forms.files")),
|
824
|
+
" ",
|
825
|
+
safe_join(extension_allowlist.map { |ext| content_tag(:b, ext) }, ", ")
|
826
|
+
])
|
827
|
+
end
|
828
|
+
end
|
829
|
+
|
830
|
+
def image_dimensions_help(dimensions_info)
|
831
|
+
content_tag :p, class: "image-dimensions-help help-text" do
|
832
|
+
safe_join([
|
833
|
+
content_tag(:span, I18n.t("dimensions_info", scope: "decidim.forms.images")),
|
834
|
+
" ",
|
835
|
+
content_tag(:span) do
|
836
|
+
safe_join(dimensions_info.map do |_version, info|
|
837
|
+
processor = @template.content_tag(:span, I18n.t("processors.#{info[:processor]}", scope: "decidim.forms.images"))
|
838
|
+
dimensions = @template.content_tag(:b, I18n.t("dimensions", scope: "decidim.forms.images", width: info[:dimensions].first, height: info[:dimensions].last))
|
839
|
+
safe_join([processor, " ", dimensions, ". ".html_safe])
|
840
|
+
end)
|
841
|
+
end
|
842
|
+
])
|
843
|
+
end
|
844
|
+
end
|
845
|
+
|
846
|
+
# Private: Creates a tag from the given options for the field prefix and
|
847
|
+
# suffix. Overridden from Foundation Rails helper to make the generated HTML
|
848
|
+
# valid since these elements are printed within <label> elements and <div>'s
|
849
|
+
# are not allowed there.
|
850
|
+
def tag_from_options(name, options)
|
851
|
+
return "".html_safe unless options && options[:value].present?
|
852
|
+
|
853
|
+
content_tag(:span,
|
854
|
+
content_tag(:span, options[:value], class: name),
|
855
|
+
class: column_classes(options).to_s)
|
856
|
+
end
|
857
|
+
|
858
|
+
# Private: Wraps the prefix and postfix for the field. Overridden from
|
859
|
+
# Foundation Rails helper to make the generated HTML valid since these
|
860
|
+
# elements are printed within <label> elements and <div>'s are not allowed
|
861
|
+
# there.
|
862
|
+
def wrap_prefix_and_postfix(block, prefix_options, postfix_options)
|
863
|
+
prefix = tag_from_options("prefix", prefix_options)
|
864
|
+
postfix = tag_from_options("postfix", postfix_options)
|
865
|
+
|
866
|
+
input_size = calculate_input_size(prefix_options, postfix_options)
|
867
|
+
klass = column_classes(input_size.marshal_dump).to_s
|
868
|
+
input = content_tag(:span, block, class: klass)
|
869
|
+
|
870
|
+
return block unless input_size.changed?
|
871
|
+
|
872
|
+
content_tag(:span, prefix + input + postfix, class: "row collapse")
|
873
|
+
end
|
874
|
+
|
875
|
+
def language_selector_select(locales, tabs_id, name)
|
876
|
+
content_tag(:div) do
|
877
|
+
content_tag(:select, id: tabs_id, class: "language-change") do
|
878
|
+
locales.each_with_index.inject("".html_safe) do |string, (locale, index)|
|
879
|
+
title = if error?(name_with_locale(name, locale))
|
880
|
+
I18n.with_locale(locale) { I18n.t("name_with_error", scope: "locale") }
|
881
|
+
else
|
882
|
+
I18n.with_locale(locale) { I18n.t("name", scope: "locale") }
|
883
|
+
end
|
884
|
+
tab_content_id = sanitize_tabs_selector "#{tabs_id}-#{name}-panel-#{index}"
|
885
|
+
string + content_tag(:option, title, value: "##{tab_content_id}")
|
886
|
+
end
|
887
|
+
end
|
888
|
+
end
|
889
|
+
end
|
890
|
+
|
891
|
+
def language_tabs(locales, tabs_id, name)
|
892
|
+
content_tag(:ul, class: "tabs tabs--lang", id: tabs_id, data: { tabs: true }) do
|
893
|
+
locales.each_with_index.inject("".html_safe) do |string, (locale, index)|
|
894
|
+
string + content_tag(:li, class: tab_element_class_for("title", index)) do
|
895
|
+
title = I18n.with_locale(locale) { I18n.t("name", scope: "locale") }
|
896
|
+
element_class = nil
|
897
|
+
element_class = "is-tab-error" if error?(name_with_locale(name, locale))
|
898
|
+
tab_content_id = sanitize_tabs_selector "#{tabs_id}-#{name}-panel-#{index}"
|
899
|
+
content_tag(:a, title, href: "##{tab_content_id}", class: element_class)
|
900
|
+
end
|
901
|
+
end
|
902
|
+
end
|
903
|
+
end
|
904
|
+
|
905
|
+
def editor_images_options(options)
|
906
|
+
return {} unless options[:editor_images]
|
907
|
+
|
908
|
+
{
|
909
|
+
editor_images: true,
|
910
|
+
upload_images_path: Decidim::Core::Engine.routes.url_helpers.editor_images_path,
|
911
|
+
drag_and_drop_help_text: I18n.t("drag_and_drop_help", scope: "decidim.editor_images")
|
912
|
+
}
|
913
|
+
end
|
914
|
+
end
|
915
|
+
end
|