decidim-core 0.26.3 → 0.26.4

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of decidim-core might be problematic. Click here for more details.

Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/amendable/announcement_cell.rb +1 -1
  3. data/app/cells/decidim/card_m_cell.rb +1 -1
  4. data/app/controllers/decidim/devise/invitations_controller.rb +9 -2
  5. data/app/controllers/decidim/devise/registrations_controller.rb +4 -0
  6. data/app/forms/decidim/account_form.rb +3 -3
  7. data/app/forms/decidim/amendable/form.rb +2 -1
  8. data/app/forms/decidim/registration_form.rb +3 -3
  9. data/app/mailers/decidim/notification_mailer.rb +1 -0
  10. data/app/packs/src/decidim/editor/clipboard_override.js +143 -0
  11. data/app/packs/src/decidim/editor/clipboard_utilities.js +119 -0
  12. data/app/packs/src/decidim/editor/linebreak_module.js +0 -8
  13. data/app/packs/src/decidim/editor.js +9 -2
  14. data/app/presenters/decidim/home_stats_presenter.rb +11 -4
  15. data/app/presenters/decidim/stats_presenter.rb +7 -8
  16. data/app/presenters/decidim/user_presenter.rb +9 -4
  17. data/app/services/decidim/activity_search.rb +1 -0
  18. data/app/validators/etiquette_validator.rb +7 -3
  19. data/app/views/decidim/notification_mailer/event_received.html.erb +1 -1
  20. data/config/locales/ar.yml +0 -22
  21. data/config/locales/bg.yml +0 -22
  22. data/config/locales/ca.yml +2 -22
  23. data/config/locales/cs.yml +3 -23
  24. data/config/locales/de.yml +0 -22
  25. data/config/locales/el.yml +0 -22
  26. data/config/locales/en.yml +2 -22
  27. data/config/locales/es-MX.yml +2 -22
  28. data/config/locales/es-PY.yml +2 -22
  29. data/config/locales/es.yml +6 -26
  30. data/config/locales/eu.yml +1 -22
  31. data/config/locales/fi-plain.yml +2 -22
  32. data/config/locales/fi.yml +2 -22
  33. data/config/locales/fr-CA.yml +2 -22
  34. data/config/locales/fr.yml +2 -22
  35. data/config/locales/ga-IE.yml +0 -4
  36. data/config/locales/gl.yml +0 -22
  37. data/config/locales/gn-PY.yml +1 -0
  38. data/config/locales/hu.yml +0 -22
  39. data/config/locales/id-ID.yml +0 -22
  40. data/config/locales/it.yml +0 -22
  41. data/config/locales/ja.yml +2 -22
  42. data/config/locales/lb.yml +0 -22
  43. data/config/locales/lo-LA.yml +1 -0
  44. data/config/locales/lt.yml +0 -22
  45. data/config/locales/lv.yml +0 -22
  46. data/config/locales/nl.yml +0 -22
  47. data/config/locales/no.yml +0 -22
  48. data/config/locales/pl.yml +0 -22
  49. data/config/locales/pt-BR.yml +0 -22
  50. data/config/locales/pt.yml +0 -22
  51. data/config/locales/ro-RO.yml +0 -22
  52. data/config/locales/ru.yml +0 -4
  53. data/config/locales/sk.yml +0 -22
  54. data/config/locales/sv.yml +0 -22
  55. data/config/locales/tr-TR.yml +0 -22
  56. data/config/locales/zh-CN.yml +0 -22
  57. data/config/routes.rb +20 -2
  58. data/lib/decidim/attributes/localized_date.rb +9 -1
  59. data/lib/decidim/attributes/time_with_zone.rb +13 -1
  60. data/lib/decidim/core/engine.rb +0 -5
  61. data/lib/decidim/core/test/shared_examples/mcell_examples.rb +17 -0
  62. data/lib/decidim/core/test.rb +1 -0
  63. data/lib/decidim/core/version.rb +1 -1
  64. data/lib/decidim/form_builder.rb +8 -1
  65. data/lib/decidim/resourceable.rb +5 -4
  66. data/lib/decidim/settings_manifest.rb +1 -1
  67. metadata +11 -8
  68. data/app/packs/images/decidim/gamification/badges/decidim_gamification_badges_invitations.svg +0 -1
  69. data/app/views/decidim/devise/registrations/edit.html.erb +0 -41
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3cbd72f4c386bc8730dfa1d3958a691c5ed719b3cf43b15f778d2011ae5a7f0d
4
- data.tar.gz: 32e72e8185fc2f28f0298ef2a9a0350cd8b436e1a39bda136fe82c9da5644433
3
+ metadata.gz: 1863adec9ae8ce9d4d528f247a2f18561de3618eacb84e50c03fae6a89fb2d6e
4
+ data.tar.gz: 032b9cce96757af5e5d87d687f64231a1630fdc003973b3b16cc4044f7f667bb
5
5
  SHA512:
6
- metadata.gz: 3d7a4d41fa9aeb76c68e21e5a486b8a3149d117a956c4d0ec17020db1abad1c8a8ce1316ba014a158bbb2fe254ebc0db4b4923a756d0f97997c06bb128c3e440
7
- data.tar.gz: e282e746492e32a9e85edb4b72ede7804be3b5b7fd9567e49eac2ba6b9430f6d46da2596f044b87fa520fb4cde0e92259c40695837c87d6fb0943f2c244fa8e2
6
+ metadata.gz: 44579a2195e6d79ca6e5d78a1695815c4db2d2dd850b9a94e819d5219cda61cad771aa172ad99628bb9460e79a99b02fa4cd72fb377649b565b22ad010eb6924
7
+ data.tar.gz: 959d4d1552af3fd6b25fb1fb27292d8d107c260bedddd5a49dce829f6271772fd8a2c043d401cdc5061a615e1de71157d0b084eeab3b794e9aa3373451ad7ef8
@@ -39,7 +39,7 @@ module Decidim::Amendable
39
39
  end
40
40
 
41
41
  def proposal_link(resource = model.amendable, text = nil)
42
- text ||= %(<strong>#{present(model.amendable).title}</strong>)
42
+ text ||= %(<strong>#{decidim_sanitize(present(model.amendable).title, strip_tags: true)}</strong>)
43
43
  link_to resource_locator(resource).path do
44
44
  text
45
45
  end
@@ -57,7 +57,7 @@ module Decidim
57
57
  end
58
58
 
59
59
  def title
60
- translated_attribute model.title
60
+ decidim_html_escape(translated_attribute(model.title))
61
61
  end
62
62
 
63
63
  def description
@@ -19,7 +19,7 @@ module Decidim
19
19
  # invitation. Using the param `invite_redirect` we can redirect the user
20
20
  # to a custom path after it has accepted the invitation.
21
21
  def after_accept_path_for(resource)
22
- params[:invite_redirect] || after_sign_in_path_for(resource)
22
+ invite_redirect_path || after_sign_in_path_for(resource)
23
23
  end
24
24
 
25
25
  # When a managed user accepts the invitation is promoted to non-managed user.
@@ -30,7 +30,6 @@ module Decidim
30
30
  resource.update!(newsletter_notifications_at: Time.current) if update_resource_params[:newsletter_notifications]
31
31
  resource.update!(managed: false) if resource.managed?
32
32
  resource.update!(accepted_tos_version: resource.organization.tos_version)
33
- Decidim::Gamification.increment_score(resource.invited_by, :invitations) if resource.invited_by
34
33
  end
35
34
 
36
35
  resource
@@ -38,6 +37,14 @@ module Decidim
38
37
 
39
38
  protected
40
39
 
40
+ def invite_redirect_path
41
+ path = params[:invite_redirect]
42
+ return unless path
43
+ return unless path.starts_with?(%r{^/[a-z0-9]+})
44
+
45
+ path
46
+ end
47
+
41
48
  def configure_permitted_parameters
42
49
  devise_parameter_sanitizer.permit(:accept_invitation, keys: [:nickname, :tos_agreement, :newsletter_notifications])
43
50
  end
@@ -58,6 +58,10 @@ module Decidim
58
58
  super(hash)
59
59
  resource.organization = current_organization
60
60
  end
61
+
62
+ def devise_mapping
63
+ ::Devise.mappings[:user]
64
+ end
61
65
  end
62
66
  end
63
67
  end
@@ -19,9 +19,9 @@ module Decidim
19
19
  attribute :personal_url
20
20
  attribute :about
21
21
 
22
- validates :name, presence: true
23
- validates :email, presence: true, 'valid_email_2/email': { disposable: true }
24
- validates :nickname, presence: true, format: Decidim::User::REGEXP_NICKNAME
22
+ validates :name, presence: true, format: { with: Decidim::User::REGEXP_NAME }
23
+ validates :email, presence: true, "valid_email_2/email": { disposable: true }
24
+ validates :nickname, presence: true, format: { with: Decidim::User::REGEXP_NICKNAME }
25
25
 
26
26
  validates :nickname, length: { maximum: Decidim::User.nickname_max_length, allow_blank: true }
27
27
  validates :password, confirmation: true
@@ -66,7 +66,8 @@ module Decidim
66
66
  errors = amendable_form_errors.details[key] - @original_form.errors.details[key]
67
67
 
68
68
  errors.map do |hash|
69
- @amendable_form.errors.add(key, hash[:error]) unless @amendable_form.errors.details[key].include? error: hash[:error]
69
+ error = hash.delete(:error)
70
+ @amendable_form.errors.add(key, error, **hash) unless @amendable_form.errors.details[key].include?(error: error)
70
71
  end
71
72
  end
72
73
  end
@@ -14,9 +14,9 @@ module Decidim
14
14
  attribute :tos_agreement, Boolean
15
15
  attribute :current_locale, String
16
16
 
17
- validates :name, presence: true
18
- validates :nickname, presence: true, format: /\A[\w\-]+\z/, length: { maximum: Decidim::User.nickname_max_length }
19
- validates :email, presence: true, 'valid_email_2/email': { disposable: true }
17
+ validates :name, presence: true, format: { with: Decidim::User::REGEXP_NAME }
18
+ validates :nickname, presence: true, format: { with: Decidim::User::REGEXP_NICKNAME }, length: { maximum: Decidim::User.nickname_max_length }
19
+ validates :email, presence: true, "valid_email_2/email": { disposable: true }
20
20
  validates :password, confirmation: true
21
21
  validates :password, password: { name: :name, email: :email, username: :nickname }
22
22
  validates :password_confirmation, presence: true
@@ -5,6 +5,7 @@ module Decidim
5
5
  # a events are received.
6
6
  class NotificationMailer < Decidim::ApplicationMailer
7
7
  helper Decidim::ResourceHelper
8
+ helper Decidim::SanitizeHelper
8
9
 
9
10
  def event_received(event, event_class_name, resource, user, user_role, extra) # rubocop:disable Metrics/ParameterLists
10
11
  with_user(user) do
@@ -0,0 +1,143 @@
1
+ /* eslint max-lines: ["error", 350] */
2
+
3
+ /**
4
+ * Quill clipboard utilities
5
+ *
6
+ * Copyright (c) 2017, Slab
7
+ * Copyright (c) 2014, Jason Chen
8
+ * Copyright (c) 2013, salesforce.com
9
+ * BSD 3-Clause "New" or "Revised" License
10
+ *
11
+ * Extends the original version from https://github.com/quilljs/quill
12
+ * Relevant parts converted from TypeScript to JavaScript
13
+ */
14
+
15
+ import CodeBlock from "quill/formats/code";
16
+ import { matchNewline, matchBreak, deltaEndsWith, traverse } from "src/decidim/editor/clipboard_utilities";
17
+
18
+ const Delta = Quill.import("delta");
19
+ const Clipboard = Quill.import("modules/clipboard");
20
+
21
+ /**
22
+ * Pasting bold text is broken in Quill as described at:
23
+ * https://github.com/quilljs/quill/issues/306
24
+ *
25
+ * The reason is that the `<strong>` nodes are not recognized as bold types.
26
+ * This override fixes the issue by introducing parts of the newer Quill code
27
+ * at GitHub and defining the `<strong>` tags as bold tags.
28
+ */
29
+ export default class ClipboardOverride extends Clipboard {
30
+ constructor(quill, options) {
31
+ super(quill, options);
32
+ this.overrideMatcher("b", "b, strong");
33
+ this.overrideMatcher("br", "br", matchBreak);
34
+
35
+ // Change the matchNewLine matchers to the newer version
36
+ this.matchers[1][1] = matchNewline;
37
+ this.matchers[3][1] = matchNewline;
38
+
39
+ // Remove `matchSpacing` as that is also removed in the newer versions.
40
+ this.removeMatcher(Node.ELEMENT_NODE, "matchSpacing");
41
+ }
42
+
43
+ overrideMatcher(originalSelector, newSelector, newMatcher = null) {
44
+ const idx = this.matchers.findIndex((item) => item[0] === originalSelector);
45
+ if (idx >= 0) {
46
+ this.matchers[idx][0] = newSelector;
47
+ if (newMatcher) {
48
+ this.matchers[idx][1] = newMatcher;
49
+ }
50
+ }
51
+ }
52
+
53
+ removeMatcher(selector, matcherName) {
54
+ const idx = this.matchers.findIndex((item) => item[0] === selector && item[1].name === matcherName);
55
+ if (idx >= 0) {
56
+ this.matchers.splice(idx, 1);
57
+ }
58
+ }
59
+
60
+ onPaste(ev) {
61
+ if (ev.defaultPrevented || !this.quill.isEnabled()) {
62
+ return;
63
+ }
64
+ ev.preventDefault();
65
+ const range = this.quill.getSelection(true);
66
+ if (range === null) {
67
+ return;
68
+ }
69
+ const html = ev.clipboardData.getData("text/html");
70
+ const text = ev.clipboardData.getData("text/plain");
71
+ const files = Array.from(ev.clipboardData.files || []);
72
+ if (!html && files.length > 0) {
73
+ this.quill.uploader.upload(range, files);
74
+ return;
75
+ }
76
+ if (html && files.length > 0) {
77
+ const doc = new DOMParser().parseFromString(html, "text/html");
78
+ if (
79
+ doc.body.childElementCount === 1 &&
80
+ doc.body.firstElementChild.tagName === "IMG"
81
+ ) {
82
+ this.quill.uploader.upload(range, files);
83
+ return;
84
+ }
85
+ }
86
+ this.onPasteRange(range, { html, text });
87
+ }
88
+
89
+ onPasteRange(range, { text, html }) {
90
+ const formats = this.quill.getFormat(range.index);
91
+ const pastedDelta = this.convertPaste({ text, html }, formats);
92
+ // debug.log('onPaste", pastedDelta, { text, html });
93
+ const delta = new Delta().retain(range.index).delete(range.length).concat(pastedDelta);
94
+ this.quill.updateContents(delta, Quill.sources.USER);
95
+ // range.length contributes to delta.length()
96
+ this.quill.setSelection(
97
+ delta.length() - range.length,
98
+ Quill.sources.SILENT,
99
+ );
100
+ this.quill.scrollIntoView();
101
+ }
102
+
103
+ convertPaste({ html, text }, formats = {}) {
104
+ if (formats[CodeBlock.blotName]) {
105
+ return new Delta().insert(text, {
106
+ [CodeBlock.blotName]: formats[CodeBlock.blotName]
107
+ });
108
+ }
109
+ if (!html) {
110
+ return new Delta().insert(text || "");
111
+ }
112
+ const delta = this.convertPasteHTML(html);
113
+ // Remove trailing newline
114
+ if (
115
+ deltaEndsWith(delta, "\n") &&
116
+ (delta.ops[delta.ops.length - 1].attributes === null || formats.table)
117
+ ) {
118
+ return delta.compose(new Delta().retain(delta.length() - 1).delete(1));
119
+ }
120
+ return delta;
121
+ }
122
+
123
+ convertPasteHTML(html) {
124
+ const doc = new DOMParser().parseFromString(html, "text/html");
125
+ const container = doc.body;
126
+ const nodeMatches = new WeakMap();
127
+ const [elementMatchers, textMatchers] = this.prepareMatching(
128
+ container,
129
+ nodeMatches
130
+ );
131
+ return traverse(
132
+ this.quill.scroll,
133
+ container,
134
+ elementMatchers,
135
+ textMatchers,
136
+ nodeMatches
137
+ );
138
+ }
139
+ }
140
+
141
+ // Disable warning messages from overwritting modules
142
+ Quill.debug("error");
143
+ Quill.register({"modules/clipboard": ClipboardOverride}, true);
@@ -0,0 +1,119 @@
1
+ import { BlockEmbed } from "quill/blots/block";
2
+
3
+ const Delta = Quill.import("delta");
4
+ const Parchment = Quill.import("parchment");
5
+
6
+ // Newer version used only for the pasting, not compatible with the version of
7
+ // Quill in use.
8
+ const traverse = (scroll, node, elementMatchers, textMatchers, nodeMatches) => { // eslint-disable-line max-params
9
+ // Post-order
10
+ if (node.nodeType === node.TEXT_NODE) {
11
+ return textMatchers.reduce((delta, matcher) => {
12
+ return matcher(node, delta, scroll);
13
+ }, new Delta());
14
+ }
15
+ if (node.nodeType === node.ELEMENT_NODE) {
16
+ return Array.from(node.childNodes || []).reduce((delta, childNode) => {
17
+ let childrenDelta = traverse(
18
+ scroll,
19
+ childNode,
20
+ elementMatchers,
21
+ textMatchers,
22
+ nodeMatches,
23
+ );
24
+ if (childNode.nodeType === node.ELEMENT_NODE) {
25
+ childrenDelta = elementMatchers.reduce((reducedDelta, matcher) => {
26
+ return matcher(childNode, reducedDelta, scroll);
27
+ }, childrenDelta);
28
+ childrenDelta = (nodeMatches.get(childNode) || []).reduce(
29
+ (reducedDelta, matcher) => {
30
+ return matcher(childNode, reducedDelta, scroll);
31
+ },
32
+ childrenDelta,
33
+ );
34
+ }
35
+ return delta.concat(childrenDelta);
36
+ }, new Delta());
37
+ }
38
+ return new Delta();
39
+ }
40
+
41
+ const deltaEndsWith = (delta, text) => {
42
+ let endText = "";
43
+ for (let idx = delta.ops.length - 1; idx >= 0 && endText.length < text.length; idx -= 1) {
44
+ const op = delta.ops[idx];
45
+ if (typeof op.insert !== "string") {
46
+ break;
47
+ }
48
+ endText = op.insert + endText;
49
+ }
50
+ return endText.slice(-1 * text.length) === text;
51
+ }
52
+
53
+ const isLine = (node) => {
54
+ if (node.childNodes.length === 0) {
55
+ // Exclude embed blocks
56
+ return false;
57
+ }
58
+ return [
59
+ "address", "article", "blockquote", "canvas", "dd", "div", "dl", "dt",
60
+ "fieldset", "figcaption", "figure", "footer", "form", "h1", "h2", "h3",
61
+ "h4", "h5", "h6", "header", "iframe", "li", "main", "nav", "ol", "output",
62
+ "p", "pre", "section", "table", "td", "tr", "ul", "video"
63
+ ].includes(node.tagName.toLowerCase());
64
+ }
65
+
66
+ const matchNewLineScroll = (nextSibling, delta, scroll) => {
67
+ if (!scroll) {
68
+ return null;
69
+ }
70
+
71
+ const match = Parchment.query(nextSibling)
72
+ if (match && match.prototype instanceof BlockEmbed) {
73
+ return delta.insert("\n");
74
+ }
75
+ return null;
76
+ }
77
+
78
+ const matchNewline = (node, delta, scroll) => {
79
+ if (!deltaEndsWith(delta, "\n")) {
80
+ // When scroll is defined, it was initiated from the paste event. Otherwise
81
+ // it is a normal Quill initiated traversal which handles adding the line
82
+ // breaks already.
83
+ if (scroll && node.nodeType === node.ELEMENT_NODE && node.tagName === "BR") {
84
+ return delta.insert({"break": ""});
85
+ }
86
+ if (isLine(node)) {
87
+ return delta.insert("\n");
88
+ }
89
+ if (delta.length() > 0 && node.nextSibling) {
90
+ let { nextSibling } = node;
91
+ while (nextSibling !== null) {
92
+ if (isLine(nextSibling)) {
93
+ return delta.insert("\n");
94
+ }
95
+ const scrollMatch = matchNewLineScroll(nextSibling, delta, scroll);
96
+ if (scrollMatch) {
97
+ return scrollMatch;
98
+ }
99
+ nextSibling = nextSibling.firstChild;
100
+ }
101
+ }
102
+ }
103
+ return delta;
104
+ }
105
+
106
+ const matchBreak = (node, delta) => {
107
+ if (!deltaEndsWith(delta, "\n")) {
108
+ delta.insert({"break": ""});
109
+ }
110
+ return delta;
111
+ }
112
+
113
+ export {
114
+ traverse,
115
+ deltaEndsWith,
116
+ isLine,
117
+ matchNewline,
118
+ matchBreak
119
+ }
@@ -129,7 +129,6 @@ class ScrollOvderride extends Scroll {
129
129
  Quill.register("blots/scroll", ScrollOvderride, true);
130
130
  Parchment.register(ScrollOvderride);
131
131
 
132
-
133
132
  export default function lineBreakButtonHandler(quill) {
134
133
  let range = quill.selection.getRange()[0];
135
134
  let currentLeaf = quill.getLeaf(range.index)[0];
@@ -167,13 +166,6 @@ Quill.register("modules/linebreak", (quill) => {
167
166
  }
168
167
  });
169
168
 
170
- quill.clipboard.addMatcher("BR", (node) => {
171
- if (node?.parentNode?.tagName === "A") {
172
- return new Delta().insert("\n");
173
- }
174
- return new Delta().insert({"break": ""});
175
- });
176
-
177
169
  addEnterBindings(quill);
178
170
  backspaceBindingsRangeAny(quill);
179
171
  backspaceBindings(quill);
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable require-jsdoc */
2
2
 
3
3
  import lineBreakButtonHandler from "src/decidim/editor/linebreak_module"
4
+ import "src/decidim/editor/clipboard_override"
4
5
  import "src/decidim/vendor/image-resize.min"
5
6
  import "src/decidim/vendor/image-upload.min"
6
7
 
@@ -10,6 +11,7 @@ export default function createQuillEditor(container) {
10
11
  const toolbar = $(container).data("toolbar");
11
12
  const disabled = $(container).data("disabled");
12
13
 
14
+ const allowedEmptyContentSelector = "iframe";
13
15
  let quillToolbar = [
14
16
  ["bold", "italic", "underline", "linebreak"],
15
17
  [{ list: "ordered" }, { list: "bullet" }],
@@ -93,10 +95,15 @@ export default function createQuillEditor(container) {
93
95
  });
94
96
  container.dispatchEvent(event);
95
97
 
96
- if (text === "\n" || text === "\n\n") {
98
+ if ((text === "\n" || text === "\n\n") && quill.root.querySelectorAll(allowedEmptyContentSelector).length === 0) {
97
99
  $input.val("");
98
100
  } else {
99
- $input.val(quill.root.innerHTML);
101
+ const emptyParagraph = "<p><br></p>";
102
+ const cleanHTML = quill.root.innerHTML.replace(
103
+ new RegExp(`^${emptyParagraph}|${emptyParagraph}$`, "g"),
104
+ ""
105
+ );
106
+ $input.val(cleanHTML);
100
107
  }
101
108
  });
102
109
  // After editor is ready, linebreak_module deletes two extraneous new lines
@@ -40,12 +40,19 @@ module Decidim
40
40
  end
41
41
 
42
42
  def component_stats(conditions)
43
+ stats = {}
43
44
  Decidim.component_manifests.flat_map do |component|
44
- component.stats.except([:supports_count])
45
- .filter(conditions)
46
- .with_context(published_components)
47
- .map { |name, data| [name, data] }
45
+ component
46
+ .stats.except([:supports_count])
47
+ .filter(conditions)
48
+ .with_context(published_components)
49
+ .each do |name, data|
50
+ stats[name] ||= 0
51
+ stats[name] += data
52
+ end
48
53
  end
54
+
55
+ stats.to_a
49
56
  end
50
57
 
51
58
  def published_components
@@ -15,18 +15,17 @@ module Decidim
15
15
  end
16
16
 
17
17
  def statistics(grouped_stats)
18
- statistics = []
18
+ statistics = {}
19
+
19
20
  grouped_stats.each do |_manifest_name, stats|
20
- stats.each_with_index.each do |stat, _index|
21
- stat.each_with_index.map do |_item, subindex|
22
- next unless (subindex % 3).zero?
23
- next if stat[subindex + 2].zero?
21
+ stats.each do |_space_manifest, component_manifest, count|
22
+ next if count.zero?
24
23
 
25
- statistics << { stat_title: stat[subindex + 1], stat_number: stat[subindex + 2] }
26
- end
24
+ statistics[component_manifest] ||= 0
25
+ statistics[component_manifest] += count
27
26
  end
28
27
  end
29
- statistics
28
+ statistics.map { |key, number| { stat_title: key, stat_number: number } }
30
29
  end
31
30
  end
32
31
  end
@@ -5,7 +5,6 @@ module Decidim
5
5
  # Decorator for users
6
6
  #
7
7
  class UserPresenter < SimpleDelegator
8
- include Rails.application.routes.mounted_helpers
9
8
  include ActionView::Helpers::UrlHelper
10
9
  include Decidim::TranslatableAttributes
11
10
 
@@ -27,7 +26,7 @@ module Decidim
27
26
  def profile_url
28
27
  return "" if respond_to?(:deleted?) && deleted?
29
28
 
30
- decidim.profile_url(__getobj__.nickname, host: __getobj__.organization.host)
29
+ decidim.profile_url(__getobj__.nickname)
31
30
  end
32
31
 
33
32
  def avatar
@@ -36,13 +35,13 @@ module Decidim
36
35
 
37
36
  def avatar_url(variant = nil)
38
37
  return default_avatar_url if __getobj__.blocked?
39
- return avatar.default_url unless avatar.attached?
38
+ return default_avatar_url unless avatar.attached?
40
39
 
41
40
  avatar.path(variant: variant)
42
41
  end
43
42
 
44
43
  def default_avatar_url
45
- attached_uploader(:avatar).default_url
44
+ avatar.default_url
46
45
  end
47
46
 
48
47
  def profile_path
@@ -77,5 +76,11 @@ module Decidim
77
76
  def has_tooltip?
78
77
  true
79
78
  end
79
+
80
+ private
81
+
82
+ def decidim
83
+ @decidim ||= Decidim::EngineRouter.new("decidim", { host: __getobj__.organization.host })
84
+ end
80
85
  end
81
86
  end
@@ -149,6 +149,7 @@ module Decidim
149
149
  LEFT JOIN decidim_participatory_space_private_users AS #{manifest.name}_private_users
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
+ AND #{table}.private_space = 't'
152
153
  SQL
153
154
  ).to_s
154
155
  ).where(
@@ -3,12 +3,16 @@
3
3
  # This validator takes care of ensuring the validated content is
4
4
  # respectful, doesn't use caps, and overall is meaningful.
5
5
  class EtiquetteValidator < ActiveModel::EachValidator
6
+ include ActionView::Helpers::SanitizeHelper
7
+
6
8
  def validate_each(record, attribute, value)
7
9
  return if value.blank?
8
10
 
9
- validate_caps(record, attribute, value)
10
- validate_marks(record, attribute, value)
11
- validate_caps_first(record, attribute, value)
11
+ text_value = strip_tags(value)
12
+
13
+ validate_caps(record, attribute, text_value)
14
+ validate_marks(record, attribute, text_value)
15
+ validate_caps_first(record, attribute, text_value)
12
16
  end
13
17
 
14
18
  private
@@ -4,7 +4,7 @@
4
4
 
5
5
  <% if @event_instance.resource_path.present? && @event_instance.resource_title.present? %>
6
6
  <p class="email-button email-button__cta">
7
- <%= link_to @event_instance.resource_title, @event_instance.resource_url %>
7
+ <%= link_to decidim_sanitize(@event_instance.resource_title, strip_tags: true), @event_instance.resource_url %>
8
8
  </p>
9
9
  <% end %>
10
10
 
@@ -633,18 +633,6 @@ ar:
633
633
  how: كيف يمكنك كسب ذلك
634
634
  page_description: الشارات عبارة عن اعتراف بإجراءات المشارك والتقدم في المنصة. عند البدء في الاكتشاف والمشاركة والتفاعل في المنصة ، ستربح شارات مختلفة. فيما يلي قائمة بالشارات وبعض الطرق التي يمكنك من خلالها كسبها.
635
635
  title: شارات
636
- invitations:
637
- conditions:
638
- - استخدم رابط "دعوة الأصدقاء" في صفحة المستخدم الخاصة بك لدعوة أصدقائك
639
- - تخصيص ، إذا كنت تريد ، الرسالة التي ترسلها
640
- - ستصل إلى أعلى مستوى عن طريق إرسال الدعوات وتسجيلها.
641
- description: تُمنح هذه الشارة عند قيامك بدعوة بعض الأشخاص وقضاء بعض الوقت للتسجيل في %{organization_name} وتصبح مشاركًا. شكرًا لك على تعريف شخص %{organization_name} للآخرين والمساعدة في توسيع نطاق المجتمع!
642
- description_another: قام هذا المشارك بدعوة %{score} شخصًا.
643
- description_own: لقد قمت بدعوة %{score} شخصًا.
644
- name: دعوات
645
- next_level_in: دعوة %{score} أشخاص آخرين للوصول إلى المستوى التالي!
646
- unearned_another: هذا المشارك لم يدع أي شخص حتى الآن
647
- unearned_own: لم تقم بدعوة أي شخص حتى الآن.
648
636
  description: الشارات عبارة عن اعتراف بإجراءات المشارك والتقدم في المنصة. عند البدء في الاكتشاف والمشاركة والتفاعل في المنصة ، ستربح شارات مختلفة.
649
637
  level: المستوى %{level}
650
638
  reached_top: لقد وصلت إلى المستوى الأعلى لهذه الشارة.
@@ -1198,15 +1186,6 @@ ar:
1198
1186
  updated_not_active: كلمة السر الخاصة بك تم تغييرها بنجاح.
1199
1187
  registrations:
1200
1188
  destroyed: تم إلغاء حسابك بنجاح. نحن نأمل ان نراك قريبا مره اخرى.
1201
- edit:
1202
- are_you_sure: هل أنت واثق؟
1203
- cancel_my_account: الغاء حسابي
1204
- currently_waiting_confirmation_for_email: 'حاليا في انتظار تأكيد ل: %{email}'
1205
- leave_blank_if_you_don_t_want_to_change_it: اتركه فارغا إذا كنت لا تريد تغييره
1206
- title: تحرير %{resource}
1207
- unhappy: تعيس؟
1208
- update: تحديث
1209
- we_need_your_current_password_to_confirm_your_changes: نحتاج كلمة المرور الحالية لتأكيد تغييراتك
1210
1189
  new:
1211
1190
  sign_up: انشئ حسابًا
1212
1191
  signed_up: أهلا بك! لقد اشتركت بنجاح
@@ -1261,7 +1240,6 @@ ar:
1261
1240
  not_locked: لم يكن مغلقا
1262
1241
  too_many_marks: يستخدم الكثير من علامات الترقيم المتتالية (مثل! و؟)
1263
1242
  too_much_caps: يستخدم عددًا كبيرًا جدًا من الأحرف الكبيرة (أكثر من 25٪ من النص)
1264
- too_short: قصير جدًا (أقل من 15 حرفًا)
1265
1243
  forms:
1266
1244
  required: مطلوب
1267
1245
  invisible_captcha: