decidim-core 0.25.2 → 0.26.0.rc1

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.
Files changed (146) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/activity_cell.rb +2 -1
  3. data/app/cells/decidim/author/flag_user.erb +1 -1
  4. data/app/cells/decidim/author/profile_inline.erb +1 -1
  5. data/app/cells/decidim/author/withdraw.erb +2 -2
  6. data/app/cells/decidim/author_cell.rb +32 -0
  7. data/app/cells/decidim/card_m_cell.rb +1 -1
  8. data/app/cells/decidim/content_blocks/cta_cell.rb +1 -1
  9. data/app/cells/decidim/content_blocks/hero_cell.rb +1 -1
  10. data/app/cells/decidim/content_blocks/highlighted_content_banner/show.erb +1 -1
  11. data/app/cells/decidim/content_blocks/last_activity_cell.rb +1 -1
  12. data/app/cells/decidim/content_blocks/stats_cell.rb +12 -0
  13. data/app/cells/decidim/endorsers_list_cell.rb +3 -1
  14. data/app/cells/decidim/flag_modal/flag_user.erb +2 -2
  15. data/app/cells/decidim/flag_modal/show.erb +2 -2
  16. data/app/cells/decidim/flag_modal_cell.rb +10 -0
  17. data/app/cells/decidim/notification/show.erb +31 -0
  18. data/app/cells/decidim/notification_cell.rb +20 -0
  19. data/app/cells/decidim/notifications/show.erb +1 -24
  20. data/app/cells/decidim/notifications_cell.rb +0 -1
  21. data/app/cells/decidim/user_conversation/conversation_header.erb +1 -1
  22. data/app/cells/decidim/user_conversation/show.erb +4 -2
  23. data/app/cells/decidim/user_conversations/conversation_item.erb +1 -1
  24. data/app/commands/decidim/create_editor_image.rb +41 -0
  25. data/app/controllers/decidim/cookie_policy_controller.rb +2 -0
  26. data/app/controllers/decidim/editor_images_controller.rb +47 -0
  27. data/app/controllers/decidim/user_activities_controller.rb +2 -1
  28. data/app/forms/decidim/editor_image_form.rb +16 -0
  29. data/app/helpers/decidim/amendments_helper.rb +1 -1
  30. data/app/helpers/decidim/application_helper.rb +2 -2
  31. data/app/helpers/decidim/messaging/conversation_helper.rb +32 -3
  32. data/app/helpers/decidim/resource_versions_helper.rb +1 -1
  33. data/app/helpers/decidim/sanitize_helper.rb +65 -0
  34. data/app/models/decidim/editor_image.rb +14 -0
  35. data/app/models/decidim/messaging/conversation.rb +9 -0
  36. data/app/models/decidim/participatory_space_private_user.rb +16 -0
  37. data/app/models/decidim/user.rb +3 -3
  38. data/app/models/decidim/user_group.rb +40 -0
  39. data/app/packs/entrypoints/decidim_core.js +1 -0
  40. data/app/packs/src/decidim/dialog_mode.js +143 -0
  41. data/app/packs/src/decidim/dialog_mode.test.js +168 -0
  42. data/app/packs/src/decidim/editor.js +56 -14
  43. data/app/packs/src/decidim/form_attachments.js +5 -0
  44. data/app/packs/src/decidim/index.js +4 -0
  45. data/app/packs/src/decidim/vendor/image-resize.min.js +3 -0
  46. data/app/packs/src/decidim/vendor/image-upload.min.js +8 -0
  47. data/app/packs/stylesheets/decidim/extras/_extras.scss +0 -1
  48. data/app/packs/stylesheets/decidim/extras/_quill.scss +7 -0
  49. data/app/packs/stylesheets/decidim/modules/_buttons.scss +11 -4
  50. data/app/packs/stylesheets/decidim/modules/_cards.scss +4 -0
  51. data/app/packs/stylesheets/decidim/modules/_layout.scss +1 -1
  52. data/app/presenters/decidim/nil_presenter.rb +2 -2
  53. data/app/presenters/decidim/notification_presenter.rb +25 -0
  54. data/app/presenters/decidim/official_author_presenter.rb +1 -1
  55. data/app/presenters/decidim/validation_errors_presenter.rb +27 -0
  56. data/app/queries/decidim/similar_emendations.rb +1 -1
  57. data/app/resolvers/decidim/core/metric_resolver.rb +1 -1
  58. data/app/services/decidim/activity_search.rb +2 -2
  59. data/app/services/decidim/email_notification_generator.rb +4 -1
  60. data/app/services/decidim/html_truncation.rb +130 -0
  61. data/app/services/decidim/open_data_exporter.rb +29 -5
  62. data/app/services/decidim/resource_search.rb +1 -1
  63. data/app/uploaders/decidim/editor_image_uploader.rb +6 -0
  64. data/app/validators/password_validator.rb +123 -0
  65. data/app/views/decidim/account/_password_fields.html.erb +1 -1
  66. data/app/views/decidim/devise/passwords/edit.html.erb +1 -1
  67. data/app/views/decidim/devise/registrations/new.html.erb +1 -1
  68. data/app/views/decidim/devise/shared/_omniauth_buttons_mini.html.erb +6 -4
  69. data/app/views/decidim/messaging/conversations/_conversation.html.erb +3 -3
  70. data/app/views/decidim/messaging/conversations/_messages.html.erb +8 -2
  71. data/app/views/decidim/messaging/conversations/_show.html.erb +10 -12
  72. data/app/views/decidim/messaging/conversations/show.html.erb +4 -2
  73. data/app/views/decidim/newsletters/show.html.erb +1 -1
  74. data/app/views/decidim/notification_mailer/event_received.html.erb +17 -0
  75. data/app/views/decidim/pages/_tabbed.html.erb +1 -1
  76. data/app/views/decidim/searches/_filters_small_view.html.erb +3 -3
  77. data/app/views/decidim/shared/_login_modal.html.erb +5 -5
  78. data/app/views/decidim/shared/_orders.html.erb +1 -1
  79. data/app/views/decidim/shared/_results_per_page.html.erb +1 -1
  80. data/app/views/decidim/shared/participatory_space_filters/_filters_small_view.html.erb +3 -3
  81. data/app/views/layouts/decidim/_application.html.erb +1 -12
  82. data/app/views/layouts/decidim/_head.html.erb +4 -0
  83. data/app/views/layouts/decidim/_language_chooser.html.erb +1 -1
  84. data/app/views/layouts/decidim/_meta_tags_config.html.erb +11 -0
  85. data/app/views/layouts/decidim/_wrapper.html.erb +1 -1
  86. data/config/brakeman.ignore +149 -0
  87. data/config/initializers/devise.rb +1 -1
  88. data/config/initializers/rack_attack.rb +23 -21
  89. data/config/locales/ca.yml +2 -0
  90. data/config/locales/cs.yml +60 -0
  91. data/config/locales/en.yml +45 -0
  92. data/config/locales/es.yml +45 -0
  93. data/config/locales/eu.yml +5 -0
  94. data/config/locales/fi-plain.yml +6 -0
  95. data/config/locales/fi.yml +45 -0
  96. data/config/locales/fr-CA.yml +38 -0
  97. data/config/locales/fr.yml +44 -6
  98. data/config/locales/gl.yml +5 -0
  99. data/config/locales/it.yml +11 -0
  100. data/config/locales/ja.yml +72 -36
  101. data/config/locales/lb-LU.yml +1354 -0
  102. data/config/locales/lb.yml +1 -1
  103. data/config/locales/nl.yml +54 -0
  104. data/config/locales/pl.yml +5 -5
  105. data/config/locales/pt-BR.yml +1 -1
  106. data/config/locales/ro-RO.yml +8 -0
  107. data/config/locales/sv.yml +5 -0
  108. data/config/locales/val-ES.yml +1 -0
  109. data/config/routes.rb +2 -0
  110. data/db/migrate/20210730112319_create_decidim_editor_images.rb +12 -0
  111. data/db/migrate/20211126183540_add_timestamps_to_content_blocks.rb +14 -0
  112. data/db/seeds.rb +16 -14
  113. data/lib/decidim/api/functions/user_entity_list.rb +1 -0
  114. data/lib/decidim/api/input_sorts/component_input_sort.rb +1 -1
  115. data/lib/decidim/common_passwords.rb +56 -0
  116. data/lib/decidim/content_parsers/inline_images_parser.rb +68 -0
  117. data/lib/decidim/content_parsers.rb +1 -0
  118. data/lib/decidim/content_renderers/link_renderer.rb +85 -1
  119. data/lib/decidim/content_renderers/user_group_renderer.rb +1 -1
  120. data/lib/decidim/content_renderers/user_renderer.rb +1 -1
  121. data/lib/decidim/core/engine.rb +3 -12
  122. data/lib/decidim/core/test/factories.rb +7 -1
  123. data/lib/decidim/core/test/shared_examples/translated_event_examples.rb +131 -0
  124. data/lib/decidim/core/test.rb +1 -0
  125. data/lib/decidim/core/version.rb +1 -1
  126. data/lib/decidim/core.rb +22 -5
  127. data/lib/decidim/db/common-passwords.txt +128420 -0
  128. data/lib/decidim/etherpad/pad.rb +48 -0
  129. data/lib/decidim/etherpad.rb +7 -0
  130. data/lib/decidim/events/base_event.rb +18 -0
  131. data/lib/decidim/events/machine_translated_event.rb +36 -0
  132. data/lib/decidim/events/user_group_event.rb +1 -3
  133. data/lib/decidim/events.rb +1 -0
  134. data/lib/decidim/exporters/csv.rb +7 -7
  135. data/lib/decidim/faker/localized.rb +15 -6
  136. data/lib/decidim/form_builder.rb +14 -4
  137. data/lib/decidim/has_attachments.rb +11 -4
  138. data/lib/decidim/importers/import_manifest.rb +103 -3
  139. data/lib/decidim/paddable.rb +1 -9
  140. data/lib/decidim/searchable.rb +2 -2
  141. data/lib/decidim/settings_manifest.rb +2 -0
  142. data/lib/decidim/translatable_attributes.rb +6 -6
  143. data/lib/decidim/view_model.rb +10 -0
  144. data/lib/tasks/decidim_active_storage_migration_tasks.rake +68 -0
  145. metadata +56 -65
  146. data/app/packs/stylesheets/decidim/extras/_social_icons_mini.scss +0 -11
@@ -11,6 +11,5 @@
11
11
  @import "stylesheets/decidim/extras/quill";
12
12
  @import "stylesheets/decidim/extras/reference";
13
13
  @import "stylesheets/decidim/extras/results-per-page";
14
- @import "stylesheets/decidim/extras/social_icons_mini";
15
14
  @import "stylesheets/decidim/extras/social_share";
16
15
  @import "stylesheets/decidim/extras/status-labels";
@@ -11,3 +11,10 @@
11
11
  margin-top: 0;
12
12
  margin-bottom: 0;
13
13
  }
14
+
15
+ .ql-reset-decidim{
16
+ display: flex;
17
+ flex-direction: column;
18
+ padding: 0;
19
+ white-space: inherit;
20
+ }
@@ -441,14 +441,21 @@
441
441
  text-align: center;
442
442
  }
443
443
 
444
- .button--social--mini .button--social__icon{
445
- position: relative;
446
- }
447
-
448
444
  .button--social__text{
449
445
  margin-left: 3rem;
450
446
  }
451
447
 
448
+ .button--social--mini{
449
+ padding: 0;
450
+ min-width: 0;
451
+
452
+ .button--social__icon{
453
+ border: 0;
454
+ padding: .85em 0;
455
+ margin-right: 0;
456
+ }
457
+ }
458
+
452
459
  .button--twitter{
453
460
  background-color: var(--twitter);
454
461
 
@@ -634,6 +634,10 @@ a .card__title{
634
634
  pointer-events: none;
635
635
  }
636
636
 
637
+ &--inline{
638
+ display: inline-block;
639
+ }
640
+
637
641
  svg{
638
642
  flex-basis: auto;
639
643
  }
@@ -3,7 +3,7 @@
3
3
  position: relative;
4
4
 
5
5
  @include breakpoint(medium){
6
- padding: 5rem 1.5rem 3rem;
6
+ padding: 3rem 1.5rem;
7
7
  }
8
8
 
9
9
  @include breakpoint(large){
@@ -12,11 +12,11 @@ module Decidim
12
12
  true
13
13
  end
14
14
 
15
- def avatar_url
15
+ def avatar_url(_variant = nil)
16
16
  Decidim::AvatarUploader.new(Decidim::User.new, :avatar).default_url
17
17
  end
18
18
 
19
- def respond_to_missing?
19
+ def respond_to_missing?(*)
20
20
  true
21
21
  end
22
22
 
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ #
5
+ # Decorator for notifications.
6
+ #
7
+ class NotificationPresenter < SimpleDelegator
8
+ include ActionView::Helpers::DateHelper
9
+
10
+ delegate :resource_text, to: :event_class_instance
11
+
12
+ def created_at_in_words
13
+ if created_at.between?(1.month.ago, Time.current)
14
+ time_ago_in_words(created_at)
15
+ else
16
+ format = created_at.year == Time.current.year ? :ddmm : :ddmmyyyy
17
+ I18n.l(created_at, format: format)
18
+ end
19
+ end
20
+
21
+ def display_resource_text?
22
+ event_class.constantize.included_modules.include?(Decidim::Comments::CommentEvent)
23
+ end
24
+ end
25
+ end
@@ -17,7 +17,7 @@ module Decidim
17
17
  ""
18
18
  end
19
19
 
20
- def avatar_url
20
+ def avatar_url(_variant = nil)
21
21
  ActionController::Base.helpers.asset_pack_path("media/images/default-avatar.svg")
22
22
  end
23
23
 
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ #
5
+ # Decorator to format validation errors of a form in html format
6
+ #
7
+ class ValidationErrorsPresenter < Rectify::Presenter
8
+ include Decidim::SanitizeHelper
9
+
10
+ attr_reader :error, :form
11
+
12
+ def initialize(error, form)
13
+ @error = error
14
+ @form = form
15
+ end
16
+
17
+ def message
18
+ "<p>#{error}</p>#{validation_errors_list}"
19
+ end
20
+
21
+ def validation_errors_list
22
+ return "" if form.valid?
23
+
24
+ content_tag(:ul, decidim_sanitize(form.errors.full_messages.map { |err| content_tag(:li, err) }.join))
25
+ end
26
+ end
27
+ end
@@ -29,7 +29,7 @@ module Decidim
29
29
  .published
30
30
  .not_hidden
31
31
  .where(
32
- "GREATEST(#{title_similarity}, #{body_similarity}) >= ?",
32
+ Arel.sql("GREATEST(#{title_similarity}, #{body_similarity}) >= ?").to_s,
33
33
  translated_attribute(emendation.title),
34
34
  translated_attribute(emendation.body),
35
35
  amendable_module.similarity_threshold
@@ -56,7 +56,7 @@ module Decidim
56
56
  def group
57
57
  @records = @records
58
58
  .group(group_by)
59
- .order("#{group_by} DESC")
59
+ .order(Arel.sql("#{group_by} DESC").to_s)
60
60
  end
61
61
 
62
62
  def sum
@@ -150,7 +150,7 @@ module Decidim
150
150
  ON #{manifest.name}_private_users.privatable_to_type = '#{manifest.model_class_name}'
151
151
  AND #{table}.id = #{manifest.name}_private_users.privatable_to_id
152
152
  SQL
153
- )
153
+ ).to_s
154
154
  ).where(
155
155
  Arel.sql(
156
156
  <<~SQL.squish
@@ -158,7 +158,7 @@ module Decidim
158
158
  #{table}.private_space = 'f' OR
159
159
  #{manifest.name}_private_users.decidim_user_id = #{current_user_id}
160
160
  SQL
161
- )
161
+ ).to_s
162
162
  )
163
163
  end
164
164
 
@@ -66,6 +66,9 @@ module Decidim
66
66
  return unless recipient.email_on_notification?
67
67
  return if resource.respond_to?(:can_participate?) && !resource.can_participate?(recipient)
68
68
 
69
+ wait_time = 0
70
+ wait_time = Decidim.machine_translation_delay.to_i + 1.minute if recipient.organization.enable_machine_translations
71
+
69
72
  NotificationMailer
70
73
  .event_received(
71
74
  event,
@@ -75,7 +78,7 @@ module Decidim
75
78
  user_role.to_s,
76
79
  extra
77
80
  )
78
- .deliver_later
81
+ .deliver_later(wait: wait_time)
79
82
  end
80
83
 
81
84
  def component
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ class HtmlTruncation
5
+ include Decidim::SanitizeHelper
6
+
7
+ # Truncates html content
8
+ # text - Content to be truncated
9
+ # options - Hash with the options
10
+ # max_length: An Integer maximum number of characters
11
+ # tail: Suffix to add after truncation
12
+ # count_tags: A boolean which tells if html tags should be calculated to max length, otherwise just content
13
+ # count_tail: A boolean value that determines whether max_length contains the tail
14
+ # tail_before_final_tag: A boolean, add tail before final tag if true, otherwise add tail where content is cut.
15
+ def initialize(text, options = {})
16
+ @options = {
17
+ max_length: options[:max_length] || 30,
18
+ tail: options[:tail] || "...",
19
+ count_tags: options[:count_tags] || false,
20
+ count_tail: options[:count_tail] || false,
21
+ tail_before_final_tag: options[:tail_before_final_tag] || false
22
+ }
23
+ @text = text
24
+ end
25
+
26
+ # Perform truncation to the html content (text) added in constructor
27
+ # Returns truncated String
28
+ def perform
29
+ @document = Nokogiri::HTML::DocumentFragment.parse(@text)
30
+ @tail_added = false
31
+ @remaining = initial_remaining
32
+ @cut = false
33
+ cut_children(document, options[:count_tags])
34
+ add_tail(document) if @remaining.negative? && !@tail_added
35
+ escape_html_from_content(document)
36
+
37
+ # Nokogiri's to_html escapes &quot; to &amp;quot; and we do not want extra &amp so we have to unescape.
38
+ CGI.unescape_html(document.to_html).gsub("\n", "")
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :document, :options
44
+
45
+ def cut_children(node, count_html)
46
+ return @remaining -= node_length(node, count_html) if @remaining >= node_length(node, count_html)
47
+ return node.unlink if @remaining.negative?
48
+ return cut_with_tags(node, true) if count_html && @remaining < node_length(node, count_html)
49
+
50
+ if node.children.empty?
51
+ if node.content.present? && @remaining < node_length(node, count_html)
52
+ cut_content(node)
53
+ @remaining = -1
54
+ else
55
+ @remaining -= node_length(node, count_html)
56
+ end
57
+
58
+ return
59
+ end
60
+
61
+ node.children.each do |child|
62
+ cut_children(child, count_html)
63
+ end
64
+ end
65
+
66
+ def escape_html_from_content(node)
67
+ node.content = decidim_html_escape(node.content) if node.is_a? Nokogiri::XML::Text
68
+ return if node.children.empty?
69
+
70
+ node.children.each do |child|
71
+ escape_html_from_content(child)
72
+ end
73
+ end
74
+
75
+ def cut_with_tags(node, parent)
76
+ if parent
77
+ return node.children.each { |child| cut_with_tags(child, false) } if node.children.present?
78
+
79
+ return ignore_closing_tag_and_cut(node)
80
+ end
81
+ return node.unlink if @cut
82
+
83
+ if @remaining < node_length(node, true)
84
+ @cut = true
85
+ return ignore_closing_tag_and_cut(node) if @remaining > opening_tag_length(node) || node.content.present?
86
+ end
87
+ @remaining -= node_length(node, true)
88
+ end
89
+
90
+ def ignore_closing_tag_and_cut(node)
91
+ @remaining -= opening_tag_length(node)
92
+ cut_content(node)
93
+ @remaining = -1
94
+ end
95
+
96
+ def add_tail(document)
97
+ return if document.children.empty? || @tail_added
98
+
99
+ target = document.children[-1]
100
+ if target.is_a?(Nokogiri::XML::Text) || target.content.empty?
101
+ document.add_child(Nokogiri::XML::Text.new(options[:tail], document))
102
+ else
103
+ target.add_child(Nokogiri::XML::Text.new(options[:tail], document))
104
+ end
105
+ @tail_added = true
106
+ end
107
+
108
+ def cut_content(node)
109
+ tail = options[:tail_before_final_tag] ? "" : options[:tail]
110
+ @tail_added = true if tail.present?
111
+
112
+ node.content = "#{node.content.truncate(@remaining, omission: "")}#{tail}"
113
+ end
114
+
115
+ def initial_remaining
116
+ return options[:max_length] unless options[:count_tail]
117
+
118
+ options[:max_length] - options[:tail].length
119
+ end
120
+
121
+ def opening_tag_length(node)
122
+ closing_tag_index = node.to_html.rindex("</") || node.to_html.length
123
+ node.to_html.length - (node.to_html.length - closing_tag_index) - node.content.length
124
+ end
125
+
126
+ def node_length(node, count_html)
127
+ count_html ? node.to_html.length : node.content.length
128
+ end
129
+ end
130
+ end
@@ -33,7 +33,6 @@ module Decidim
33
33
  open_data_component_manifests.each do |manifest|
34
34
  add_file_to_output(out, format(FILE_NAME_PATTERN, { host: organization.host, entity: manifest.name }), data_for_component(manifest))
35
35
  end
36
-
37
36
  open_data_participatory_space_manifests.each do |manifest|
38
37
  add_file_to_output(out, format(FILE_NAME_PATTERN, { host: organization.host, entity: manifest.name }), data_for_participatory_space(manifest))
39
38
  end
@@ -42,12 +41,37 @@ module Decidim
42
41
  buffer.string
43
42
  end
44
43
 
45
- def data_for_component(export_manifest)
46
- collection = components.where(manifest_name: export_manifest.manifest.name).find_each.flat_map do |component|
47
- export_manifest.collection.call(component)
44
+ def data_for_component(export_manifest, col_sep = Decidim.default_csv_col_sep)
45
+ headers = []
46
+ collection = []
47
+ ActiveRecord::Base.uncached do
48
+ components.where(manifest_name: export_manifest.manifest.name).find_each do |component|
49
+ export_manifest.collection.call(component).find_in_batches(batch_size: 100) do |batch|
50
+ exporter = Decidim::Exporters::CSV.new(batch, export_manifest.serializer)
51
+ headers.push(*exporter.headers)
52
+ exported = exporter.export
53
+
54
+ tmpfile = Tempfile.new("#{export_manifest.name}-#{component.id}-")
55
+ tmpfile.write(exported.read)
56
+ # Do not delete the file when the reference is deleted
57
+ ObjectSpace.undefine_finalizer(tmpfile)
58
+ tmpfile.close
59
+
60
+ collection.push(tmpfile.path)
61
+ end
62
+ end
48
63
  end
49
64
 
50
- Decidim::Exporters::CSV.new(collection, export_manifest.serializer).export
65
+ headers.uniq!
66
+
67
+ data = CSV.generate_line(headers, col_sep: col_sep)
68
+ collection.each do |content|
69
+ CSV.foreach(content, headers: true, col_sep: col_sep) do |row|
70
+ data << CSV.generate_line(row.values_at(*headers), col_sep: col_sep)
71
+ end
72
+ File.unlink(content)
73
+ end
74
+ Decidim::Exporters::ExportData.new(data, "csv")
51
75
  end
52
76
 
53
77
  def data_for_participatory_space(export_manifest)
@@ -75,7 +75,7 @@ module Decidim
75
75
  conditions << "#{query.model_name.plural}.decidim_scope_id IS NULL" if clean_scope_ids.delete("global")
76
76
  conditions.concat(["? = ANY(decidim_scopes.part_of)"] * clean_scope_ids.count) if clean_scope_ids.any?
77
77
 
78
- query.includes(:scope).references(:decidim_scopes).where(conditions.join(" OR "), *clean_scope_ids.map(&:to_i))
78
+ query.includes(:scope).references(:decidim_scopes).where(Arel.sql(conditions.join(" OR ")).to_s, *clean_scope_ids.map(&:to_i))
79
79
  end
80
80
 
81
81
  # Handle the origin filter.
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ # This class deals with uploading images to WYSIWYG editor.
5
+ class EditorImageUploader < ImageUploader; end
6
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Class is used to verify that the user's password is strong enough
4
+ class PasswordValidator < ActiveModel::EachValidator
5
+ MINIMUM_LENGTH = 10
6
+ MAX_LENGTH = 256
7
+ MIN_UNIQUE_CHARACTERS = 5
8
+ IGNORE_SIMILARITY_SHORTER_THAN = 4
9
+ VALIDATION_METHODS = [
10
+ :password_too_short?,
11
+ :password_too_long?,
12
+ :not_enough_unique_characters?,
13
+ :name_included_in_password?,
14
+ :nickname_included_in_password?,
15
+ :email_included_in_password?,
16
+ :domain_included_in_password?,
17
+ :password_too_common?,
18
+ :blacklisted?
19
+ ].freeze
20
+
21
+ # Check if user's password is strong enough
22
+ #
23
+ # record - Instance of a form (e.g. Decidim::RegistrationForm) or model
24
+ # attribute - "password"
25
+ # value - Actual password
26
+ # Returns true if password is strong enough
27
+ def validate_each(record, attribute, value)
28
+ return false if value.blank?
29
+
30
+ @record = record
31
+ @attribute = attribute
32
+ @value = value
33
+ @weak_password_reasons = []
34
+
35
+ return true if strong?
36
+
37
+ @weak_password_reasons.each do |reason|
38
+ record.errors[attribute] << get_message(reason)
39
+ end
40
+
41
+ false
42
+ end
43
+
44
+ private
45
+
46
+ attr_reader :record, :attribute, :value
47
+
48
+ def get_message(reason)
49
+ I18n.t "password_validator.#{reason}"
50
+ end
51
+
52
+ def strong?
53
+ VALIDATION_METHODS.each do |method|
54
+ @weak_password_reasons << method.to_s.sub(/\?$/, "").to_sym if send(method.to_s)
55
+ end
56
+
57
+ @weak_password_reasons.empty?
58
+ end
59
+
60
+ def password_too_short?
61
+ value.length < MINIMUM_LENGTH
62
+ end
63
+
64
+ def password_too_long?
65
+ value.length > MAX_LENGTH
66
+ end
67
+
68
+ def not_enough_unique_characters?
69
+ value.chars.uniq.length < MIN_UNIQUE_CHARACTERS
70
+ end
71
+
72
+ def name_included_in_password?
73
+ return false if !record.respond_to?(:name) || record.name.blank?
74
+ return true if value.include?(record.name.delete(" "))
75
+
76
+ record.name.split(" ").each do |part|
77
+ next if part.length < IGNORE_SIMILARITY_SHORTER_THAN
78
+
79
+ return true if value.include?(part)
80
+ end
81
+
82
+ false
83
+ end
84
+
85
+ def nickname_included_in_password?
86
+ return false if !record.respond_to?(:nickname) || record.nickname.blank?
87
+
88
+ value.include?(record.nickname)
89
+ end
90
+
91
+ def email_included_in_password?
92
+ return false if !record.respond_to?(:email) || record.email.blank?
93
+
94
+ name, domain, _whatever = record.email.split("@")
95
+ value.include?(name) || (domain && value.include?(domain.split(".").first))
96
+ end
97
+
98
+ def domain_included_in_password?
99
+ return false unless record&.current_organization&.host
100
+ return true if value.include?(record.current_organization.host)
101
+
102
+ record.current_organization.host.split(".").each do |part|
103
+ next if part.length < IGNORE_SIMILARITY_SHORTER_THAN
104
+
105
+ return true if value.include?(part)
106
+ end
107
+
108
+ false
109
+ end
110
+
111
+ def blacklisted?
112
+ Array(Decidim.password_blacklist).each do |expression|
113
+ return true if expression.is_a?(Regexp) && value.match?(expression)
114
+ return true if expression.to_s == value
115
+ end
116
+
117
+ false
118
+ end
119
+
120
+ def password_too_common?
121
+ Decidim::CommonPasswords.instance.passwords.include?(value)
122
+ end
123
+ end
@@ -1,2 +1,2 @@
1
- <%= form.password_field :password, value: form.object.password, autocomplete: "off", help_text: t("devise.passwords.edit.password_help", minimun_characters: NOBSPW.configuration.min_password_length) %>
1
+ <%= form.password_field :password, value: form.object.password, autocomplete: "off", help_text: t("devise.passwords.edit.password_help", minimun_characters: ::PasswordValidator::MINIMUM_LENGTH) %>
2
2
  <%= form.password_field :password_confirmation, value: form.object.password_confirmation, autocomplete: "off" %>
@@ -16,7 +16,7 @@
16
16
  <%= f.hidden_field :reset_password_token %>
17
17
 
18
18
  <div class="field">
19
- <%= f.password_field :password, autocomplete: "off", help_text: t("devise.passwords.edit.password_help", minimun_characters: NOBSPW.configuration.min_password_length) %>
19
+ <%= f.password_field :password, autocomplete: "off", help_text: t("devise.passwords.edit.password_help", minimun_characters: ::PasswordValidator::MINIMUM_LENGTH) %>
20
20
  </div>
21
21
 
22
22
  <div class="field">
@@ -49,7 +49,7 @@
49
49
  </div>
50
50
 
51
51
  <div class="field">
52
- <%= f.password_field :password, help_text: t(".password_help", minimun_characters: NOBSPW.configuration.min_password_length), autocomplete: "off" %>
52
+ <%= f.password_field :password, help_text: t(".password_help", minimun_characters: ::PasswordValidator::MINIMUM_LENGTH), autocomplete: "off" %>
53
53
  </div>
54
54
 
55
55
  <div class="field">
@@ -1,21 +1,23 @@
1
1
  <% if Devise.mappings[:user].omniauthable? && current_organization.enabled_omniauth_providers.any? %>
2
2
  <div class="row">
3
3
  <div class="columns medium-8 medium-centered">
4
- <span class="register__separator">
5
- <span class="register__separator__text"><%= t("or", scope: "decidim.devise.shared.omniauth_buttons") %></span>
6
- </span>
7
4
  <div class="text-center">
8
5
  <%- current_organization.enabled_omniauth_providers.keys.each do |provider| %>
6
+ <div class="social-register">
9
7
  <%= link_to decidim.send("user_#{provider}_omniauth_authorize_path"), class: "button button--social button--#{normalize_provider_name(provider)} button--social--mini", method: :post do %>
10
8
  <span class="button--social__icon" aria-hidden="true">
11
9
  <%= oauth_icon provider %>
12
10
  </span>
13
- <span class="show-for-sr">
11
+ <span class="button--social__text">
14
12
  <%= t("devise.shared.links.sign_in_with_provider", provider: normalize_provider_name(provider).titleize) %>
15
13
  </span>
16
14
  <% end %>
15
+ </div>
17
16
  <% end %>
18
17
  </div>
18
+ <span class="register__separator">
19
+ <span class="register__separator__text"><%= t("or", scope: "decidim.devise.shared.omniauth_buttons") %></span>
20
+ </span>
19
21
  </div>
20
22
  </div>
21
23
  <% end %>
@@ -15,9 +15,9 @@
15
15
  <li class="card-data__item card--list__item card-data__item--expand absolutes">
16
16
  <div class="mr-s">
17
17
  <% if conversation.interlocutors(current_user).count == 1 %>
18
- <%= t("from", scope: "decidim.messaging.conversations.index") %>: <strong><%= conversation.interlocutors(current_user).first.name %> <span class="muted">@<%= conversation.interlocutors(current_user).first.nickname %></span></strong>
18
+ <%= t("from", scope: "decidim.messaging.conversations.index") %>: <%= conversation_name_for(conversation.interlocutors(current_user)) %>
19
19
  <% else %>
20
- <%= t("from", scope: "decidim.messaging.conversations.index") %>: <strong><%= username_list(conversation.interlocutors(current_user), shorten: true) %></strong>
20
+ <%= t("from", scope: "decidim.messaging.conversations.index") %>: <strong><%= username_list(conversation.interlocutors(current_user), shorten: true).html_safe %></strong>
21
21
  <% end %>
22
22
  <br>
23
23
  <span class="muted">
@@ -45,7 +45,7 @@
45
45
  <% if conversation.interlocutors(current_user).count == 1 %>
46
46
  <%= icon "chevron-right", class: "card__link icon--big", role: "img", aria_label: t(".show", sender: conversation.interlocutors(current_user).first.name) %>
47
47
  <% else %>
48
- <%= icon "chevron-right", class: "card__link icon--big", role: "img", aria_label: t(".show", sender: username_list(conversation.interlocutors(current_user))) %>
48
+ <%= icon "chevron-right", class: "card__link icon--big", role: "img", aria_label: t(".show", sender: strip_tags(username_list(conversation.interlocutors(current_user)))) %>
49
49
  <% end %>
50
50
  <% end %>
51
51
  </div>
@@ -1,6 +1,12 @@
1
1
  <div class="conversation-chat<%= " conversation-chat--offset" if sender_is_user?(sender) %>">
2
- <%= link_to profile_path(sender.nickname) do %>
3
- <%= image_tag present(sender).avatar_url, alt: t("decidim.author.avatar", name: decidim_sanitize(sender.name)) %>
2
+ <% if sender.deleted? %>
3
+ <span class="label label--small label--basic">
4
+ <%= t("deleted", scope: "decidim.profile") %>
5
+ </span>
6
+ <% else %>
7
+ <%= link_to profile_path(sender.nickname) do %>
8
+ <%= image_tag present(sender).avatar_url, alt: t("decidim.author.avatar", name: decidim_sanitize(sender.name)) %>
9
+ <% end %>
4
10
  <% end %>
5
11
  <div>
6
12
  <% messages.each do |message| %>
@@ -1,12 +1,6 @@
1
- <% if participants.count == 1 %>
2
- <% conversation_label = t(".chat_with") + " #{participants.first.name} (@#{participants.first.nickname})" %>
3
- <% else %>
4
- <% conversation_label = t(".title", usernames: username_list(participants)) %>
5
- <% end %>
6
-
7
1
  <div class="wrapper">
8
2
  <div class="row">
9
- <div class="columns medium-9 medium-centered tabs-panel is-active" id="conversations" role="tabpanel" aria-label="<%= conversation_label %>">
3
+ <div class="columns medium-9 medium-centered tabs-panel is-active" id="conversations" role="tabpanel" aria-label="<%= strip_tags(conversation_label_for(participants)) %>">
10
4
  <div class="conversation">
11
5
 
12
6
  <div class="conversation-header flex--cc absolutes">
@@ -15,20 +9,24 @@
15
9
  <%= icon "chevron-left", role: "img", aria_label: t(".back") %>
16
10
  <% end %>
17
11
  </div>
12
+
18
13
  <% if participants.count == 1 %>
19
- <%= link_to profile_path(participants.first.nickname) do %>
20
- <%= image_tag present(participants.first).avatar_url, alt: t("decidim.author.avatar", name: decidim_sanitize(participants.first.name)) %>
14
+ <% if participants.first.deleted? %>
15
+ <%= image_tag present(participants.first).avatar_url, alt: t("decidim.author.avatar", name: t("decidim.profile.deleted")) %>
16
+ <% else %>
17
+ <%= link_to profile_path(participants.first.nickname) do %>
18
+ <%= image_tag present(participants.first).avatar_url, alt: t("decidim.author.avatar", name: decidim_sanitize(participants.first.name)) %>
19
+ <% end %>
21
20
  <% end %>
22
21
  <% else %>
23
22
  <%= image_tag present(current_user).avatar.default_multiuser_url, alt: t("decidim.author.avatar_multiuser") %>
24
23
  <% end %>
25
-
26
24
  <div class="ml-s">
27
25
  <h1>
28
26
  <% if participants.count == 1 %>
29
- <%= t(".chat_with") %> <strong><%= participants.first.name %></strong><br><span class="muted">@<%= participants.first.nickname %></span>
27
+ <%= t(".chat_with") %> <%= conversation_name_for(participants) %>
30
28
  <% else %>
31
- <%= t(".title", usernames: username_list(participants)) %>
29
+ <%= t(".title", usernames: username_list(participants)).html_safe %>
32
30
  <% end %>
33
31
  </h1>
34
32
  </div>