decidim-core 0.31.7 → 0.31.8

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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/content_blocks/participatory_space_main_data/title.erb +1 -14
  3. data/app/cells/decidim/following_cell.rb +1 -1
  4. data/app/cells/decidim/nav_links/show.erb +3 -1
  5. data/app/cells/decidim/participatory_space_private_user/show.erb +1 -1
  6. data/app/cells/decidim/share_text_widget/modal.erb +1 -0
  7. data/app/cells/decidim/share_widget/modal.erb +1 -0
  8. data/app/cells/decidim/share_widget/qr_code_modal.erb +2 -4
  9. data/app/commands/decidim/create_omniauth_registration.rb +17 -2
  10. data/app/controllers/concerns/decidim/devise_authentication_methods.rb +4 -0
  11. data/app/controllers/concerns/decidim/devise_controllers.rb +2 -0
  12. data/app/controllers/concerns/decidim/disable_redirection_to_external_host.rb +2 -0
  13. data/app/controllers/concerns/decidim/filter_resource.rb +2 -0
  14. data/app/controllers/concerns/decidim/impersonate_users.rb +2 -2
  15. data/app/controllers/concerns/decidim/locale_switcher.rb +62 -11
  16. data/app/controllers/concerns/decidim/needs_permission.rb +22 -10
  17. data/app/controllers/concerns/decidim/onboarding_action_methods.rb +2 -0
  18. data/app/controllers/concerns/decidim/paginable.rb +7 -3
  19. data/app/controllers/concerns/decidim/payload_info.rb +2 -0
  20. data/app/controllers/concerns/decidim/resource_versions_concern.rb +14 -0
  21. data/app/controllers/decidim/application_controller.rb +0 -6
  22. data/app/controllers/decidim/authorization_modals_controller.rb +2 -0
  23. data/app/controllers/decidim/components/base_controller.rb +2 -0
  24. data/app/controllers/decidim/devise/passwords_controller.rb +8 -0
  25. data/app/controllers/decidim/favicon_controller.rb +9 -0
  26. data/app/controllers/decidim/free_resource_authorization_modals_controller.rb +2 -0
  27. data/app/controllers/decidim/gamification/badges_controller.rb +2 -0
  28. data/app/controllers/decidim/homepage_controller.rb +4 -1
  29. data/app/controllers/decidim/likes_controller.rb +1 -1
  30. data/app/controllers/decidim/links_controller.rb +2 -0
  31. data/app/controllers/decidim/manifests_controller.rb +2 -0
  32. data/app/controllers/decidim/messaging/conversations_controller.rb +10 -4
  33. data/app/controllers/decidim/newsletters_controller.rb +4 -0
  34. data/app/controllers/decidim/notifications_subscriptions_controller.rb +4 -0
  35. data/app/controllers/decidim/offline_controller.rb +5 -1
  36. data/app/controllers/decidim/open_data_controller.rb +5 -1
  37. data/app/controllers/decidim/pages_controller.rb +31 -3
  38. data/app/controllers/decidim/private_downloads_controller.rb +2 -0
  39. data/app/controllers/decidim/profiles_controller.rb +10 -0
  40. data/app/controllers/decidim/qr_controller.rb +3 -1
  41. data/app/controllers/decidim/resource_autocomplete_controller.rb +2 -0
  42. data/app/controllers/decidim/searches_controller.rb +2 -0
  43. data/app/controllers/decidim/short_links_controller.rb +4 -0
  44. data/app/controllers/decidim/static_map_controller.rb +2 -0
  45. data/app/controllers/decidim/timeouts_controller.rb +4 -0
  46. data/app/controllers/decidim/tos_controller.rb +2 -0
  47. data/app/controllers/decidim/upload_validations_controller.rb +4 -2
  48. data/app/controllers/decidim/user_activities_controller.rb +2 -0
  49. data/app/forms/decidim/upload_validation_form.rb +16 -10
  50. data/app/helpers/concerns/decidim/user_role_checker.rb +35 -10
  51. data/app/helpers/decidim/sanitize_helper.rb +11 -1
  52. data/app/jobs/decidim/application_job.rb +4 -0
  53. data/app/models/decidim/authorization.rb +24 -0
  54. data/app/models/decidim/oauth_application.rb +1 -0
  55. data/app/models/decidim/user.rb +1 -0
  56. data/app/models/decidim/user_base_entity.rb +1 -1
  57. data/app/packs/src/decidim/callout.js +10 -0
  58. data/app/packs/src/decidim/callout.test.js +32 -0
  59. data/app/packs/src/decidim/controllers/accordion/accordion.test.js +64 -0
  60. data/app/packs/src/decidim/controllers/accordion/controller.js +43 -0
  61. data/app/packs/src/decidim/datepicker/datepicker_functions.js +34 -23
  62. data/app/packs/src/decidim/datepicker/generate_datepicker.js +4 -4
  63. data/app/packs/src/decidim/datepicker/test/time.test.js +39 -24
  64. data/app/packs/src/decidim/utilities/dom.js +2 -2
  65. data/app/packs/stylesheets/decidim/_content_blocks.scss +4 -10
  66. data/app/packs/stylesheets/decidim/_conversations.scss +0 -8
  67. data/app/packs/stylesheets/decidim/_modal.scss +16 -8
  68. data/app/packs/stylesheets/decidim/_modal_share.scss +0 -22
  69. data/app/packs/stylesheets/decidim/_participatory_spaces.scss +11 -1
  70. data/app/packs/stylesheets/decidim/_rich_text.scss +12 -9
  71. data/app/packs/stylesheets/decidim/_typography.scss +5 -2
  72. data/app/packs/stylesheets/decidim/legacy/email.scss +0 -5
  73. data/app/permissions/decidim/permissions.rb +20 -0
  74. data/app/presenters/decidim/admin_log/component_presenter.rb +1 -1
  75. data/app/presenters/decidim/log/base_presenter.rb +10 -1
  76. data/app/presenters/decidim/log/user_presenter.rb +2 -2
  77. data/app/serializers/decidim/exporters/open_data_blocked_user_serializer.rb +11 -2
  78. data/app/serializers/decidim/exporters/open_data_moderation_serializer.rb +25 -1
  79. data/app/services/decidim/svg_content_validator.rb +554 -0
  80. data/app/uploaders/decidim/application_uploader.rb +2 -2
  81. data/app/validators/uploader_image_content_validator.rb +278 -0
  82. data/app/views/decidim/application/_radio_accordion.html.erb +1 -5
  83. data/app/views/decidim/messaging/conversations/create.js.erb +1 -3
  84. data/app/views/decidim/shared/_login_modal.html.erb +12 -14
  85. data/app/views/layouts/decidim/_mailer_logo.html.erb +1 -1
  86. data/config/initializers/rack_attack.rb +10 -0
  87. data/config/locales/bg.yml +0 -1
  88. data/config/locales/ca-IT.yml +3 -1
  89. data/config/locales/ca.yml +3 -1
  90. data/config/locales/cs.yml +7 -1
  91. data/config/locales/de.yml +61 -3
  92. data/config/locales/en.yml +3 -1
  93. data/config/locales/es-MX.yml +3 -1
  94. data/config/locales/es-PY.yml +3 -1
  95. data/config/locales/es.yml +7 -5
  96. data/config/locales/eu.yml +155 -156
  97. data/config/locales/fi-plain.yml +1 -1
  98. data/config/locales/fi.yml +1 -1
  99. data/config/locales/fr-CA.yml +3 -1
  100. data/config/locales/fr.yml +3 -1
  101. data/config/locales/gl.yml +1441 -33
  102. data/config/locales/hu.yml +0 -1
  103. data/config/locales/ja.yml +1 -1
  104. data/config/locales/pl.yml +0 -1
  105. data/config/locales/pt-BR.yml +0 -1
  106. data/config/locales/ro-RO.yml +0 -1
  107. data/config/locales/sk.yml +2 -1
  108. data/config/locales/sv.yml +47 -10
  109. data/config/locales/tr-TR.yml +0 -2
  110. data/db/migrate/20260909000000_add_failed_attempts_and_locked_at_to_decidim_authorizations.rb +8 -0
  111. data/lib/decidim/core/test/shared_examples/participatory_space_members_page_examples.rb +77 -0
  112. data/lib/decidim/core/test/shared_examples/participatory_space_members_shared_examples.rb +85 -6
  113. data/lib/decidim/core/test/shared_examples/versions_controller_examples.rb +23 -1
  114. data/lib/decidim/core/version.rb +1 -1
  115. data/lib/decidim/core.rb +9 -0
  116. data/lib/decidim/has_upload_validations.rb +1 -0
  117. data/lib/decidim/legacy_form_builder.rb +4 -0
  118. data/lib/decidim/query_extensions.rb +2 -0
  119. metadata +10 -6
@@ -50,10 +50,7 @@ export const setMinute = (value) => {
50
50
  };
51
51
 
52
52
  export const formatInputDate = (date, formats) => {
53
- const dateList = date.split("-");
54
- const year = dateList[0];
55
- const month = dateList[1];
56
- const day = dateList[2];
53
+ const [year, month, day] = date.split("-");
57
54
 
58
55
  if (formats.order === "m-d-y") {
59
56
  return `${month}${formats.separator}${day}${formats.separator}${year}`;
@@ -63,10 +60,34 @@ export const formatInputDate = (date, formats) => {
63
60
  return `${day}${formats.separator}${month}${formats.separator}${year}`;
64
61
  };
65
62
 
63
+ /**
64
+ * Splits a time string into two elements: hours and minutes. Ensures a returned
65
+ * array of size 2 for any kind of strings or `null` values.
66
+ *
67
+ * Examples:
68
+ * splitTime("") => ["00", "00"]
69
+ * splitTime(null) => ["00", "00"]
70
+ * splitTime("12") => ["12", "00"]
71
+ * splitTime("12:30") => ["12", "30"]
72
+ * splitTime("2:4") => ["02", "04"]
73
+ * splitTime("12:30:45") => ["12", "30"]
74
+ *
75
+ * @param {String} value The original time string, typically hh:ss.
76
+ * @returns {Array} An array with two elements where the first element is the
77
+ * hour part and the second element is the minute part.
78
+ */
79
+ const splitTime = (value) => {
80
+ return [...(value || "").split(":"), "00"].splice(0, 2).map((part) => part.padStart(2, "0"));
81
+ };
82
+
66
83
  export const formatInputTime = (time, format, input) => {
67
- const timeList = time.split(":");
68
- let hour = timeList[0];
69
- const minute = timeList[1];
84
+ if (!time) {
85
+ return "";
86
+ } else if (time.length < 1) {
87
+ return time;
88
+ }
89
+
90
+ let [hour, minute] = splitTime(time);
70
91
 
71
92
  if (format === 12) {
72
93
  if (Number(hour) === 12) {
@@ -80,11 +101,9 @@ export const formatInputTime = (time, format, input) => {
80
101
  } else if (Number(hour) === 0) {
81
102
  hour = "12";
82
103
  }
83
-
84
- return `${hour}:${minute}`;
85
104
  };
86
105
 
87
- return time;
106
+ return `${hour}:${minute}`;
88
107
  };
89
108
 
90
109
  export const changeHourDisplay = (change, hour, format) => {
@@ -199,29 +218,21 @@ export const formatDate = (value, formats) => {
199
218
  };
200
219
 
201
220
  export const formatTime = (value, format, inputname) => {
221
+ let [hour, minute] = splitTime(value);
222
+
202
223
  if (format === 12) {
203
- const splitValue = value.split(":");
204
- let hour = splitValue[0];
205
- const minute = splitValue[1];
206
224
  if (document.getElementById(`period_am_${inputname}`).checked) {
207
- switch (hour) {
208
- case "12":
225
+ if (hour === "12") {
209
226
  hour = "00";
210
-
211
- return `${hour}:${minute}`;
212
- default:
213
- return value;
214
- };
227
+ }
215
228
  } else if (document.getElementById(`period_pm_${inputname}`).checked) {
216
229
  if (Number(hour) > 0 && Number(hour) < 12) {
217
230
  hour = `${Number(hour) + 12}`;
218
231
  };
219
-
220
- return `${hour}:${minute}`
221
232
  };
222
233
  };
223
234
 
224
- return value;
235
+ return `${hour}:${minute}`;
225
236
  };
226
237
 
227
238
  export const updateInputValue = (input, formats, time) => {
@@ -58,7 +58,7 @@ export default function generateDatePicker(input, row, formats) {
58
58
  dateColumn.appendChild(datePickerContainer);
59
59
 
60
60
  let prevDate = null;
61
- let defaultTime = input.getAttribute("default_time") || "00:00"
61
+ let defaultTime = input.dataset.defaultTime || "00:00"
62
62
 
63
63
  const datePickerDisplay = (event) => {
64
64
  if (!dateColumn.contains(event.target)) {
@@ -79,7 +79,7 @@ export default function generateDatePicker(input, row, formats) {
79
79
  if (input.type === "date") {
80
80
  input.value = `${formatDate(date.value, formats)}`;
81
81
  } else if (input.type === "datetime-local") {
82
- input.value = `${formatDate(date.value, formats)}T${formatTime(document.querySelector(`#${input.id}_time`).value, formats.time, input.id) || defaultTime}`;
82
+ input.value = `${formatDate(date.value, formats)}T${formatTime(document.querySelector(`#${input.id}_time`).value || defaultTime, formats.time, input.id)}`;
83
83
  };
84
84
  };
85
85
  });
@@ -95,7 +95,7 @@ export default function generateDatePicker(input, row, formats) {
95
95
  if (input.type === "date") {
96
96
  input.value = `${formatDate(date.value, formats)}`;
97
97
  } else if (input.type === "datetime-local") {
98
- input.value = `${formatDate(date.value, formats)}T${formatTime(document.querySelector(`#${input.id}_time`).value, formats.time, input.id) || defaultTime}`;
98
+ input.value = `${formatDate(date.value, formats)}T${formatTime(document.querySelector(`#${input.id}_time`).value || defaultTime, formats.time, input.id)}`;
99
99
  };
100
100
  };
101
101
  });
@@ -115,7 +115,7 @@ export default function generateDatePicker(input, row, formats) {
115
115
  if (input.type === "date") {
116
116
  input.value = `${pickedDate}`;
117
117
  } else if (input.type === "datetime-local") {
118
- input.value = `${pickedDate}T${formatTime(document.querySelector(`#${input.id}_time`).value, formats.time, input.id) || defaultTime}`;
118
+ input.value = `${pickedDate}T${formatTime(document.querySelector(`#${input.id}_time`).value || defaultTime, formats.time, input.id)}`;
119
119
  };
120
120
  datePickerContainer.style.display = "none";
121
121
  });
@@ -49,34 +49,42 @@ describe("setMinute", () => {
49
49
  });
50
50
 
51
51
  describe("formatInputTime", () => {
52
- const setupDOM = () => {
53
- document.body.innerHTML = `
54
- <input type="radio" id="period_am_exampleInput" name="period">
55
- <input type="radio" id="period_pm_exampleInput" name="period">
56
- `;
57
- };
58
-
59
52
  describe("24-hour format", () => {
53
+ const format = 24;
54
+ const input = { id: "exampleInput" };
55
+
60
56
  it("returns the value in correct format", () => {
61
- setupDOM();
62
- const time = "15:30";
63
- const format = 24;
64
- const input = { id: "exampleInput" };
57
+ const result = formatInputTime("15:30", format, input);
65
58
 
66
- const result = formatInputTime(time, format, input);
59
+ expect(result).toBe("15:30");
60
+ });
61
+
62
+ it("returns the value in correct format if seconds are provided", () => {
63
+ const result = formatInputTime("15:30:04", format, input);
67
64
 
68
65
  expect(result).toBe("15:30");
69
66
  });
67
+
68
+ it("returns an empty string if an empty string is provided", () => {
69
+ const result = formatInputTime("", 24, { id: "exampleInput" });
70
+
71
+ expect(result).toBe("");
72
+ });
70
73
  })
71
74
 
72
75
  describe("12-hour format", () => {
73
- it("returns the value in correct format and checks the PM radio-button", () => {
74
- setupDOM();
75
- const time = "12:45";
76
- const format = 12;
77
- const input = { id: "exampleInput" };
76
+ beforeEach(() => {
77
+ document.body.innerHTML = `
78
+ <input type="radio" id="period_am_exampleInput" name="period">
79
+ <input type="radio" id="period_pm_exampleInput" name="period">
80
+ `;
81
+ });
82
+
83
+ const format = 12;
84
+ const input = { id: "exampleInput" };
78
85
 
79
- const result = formatInputTime(time, format, input);
86
+ it("returns the value in correct format and checks the PM radio-button", () => {
87
+ const result = formatInputTime("12:45", format, input);
80
88
 
81
89
  expect(result).toBe("12:45");
82
90
  expect(document.getElementById("period_pm_exampleInput").checked).toBeTruthy();
@@ -84,17 +92,24 @@ describe("formatInputTime", () => {
84
92
  });
85
93
 
86
94
  it("returns the value in correct format", () => {
87
- setupDOM();
88
- const time = "00:25";
89
- const format = 12;
90
- const input = { id: "exampleInput" };
91
-
92
- const result = formatInputTime(time, format, input);
95
+ const result = formatInputTime("00:25", format, input);
93
96
 
94
97
  expect(result).toBe("12:25");
95
98
  // We are not expecting a radio button to be selected here since in a real environment the selected button is not
96
99
  // handled by this method for values < 12
97
100
  });
101
+
102
+ it("returns the value in correct format if seconds are provided", () => {
103
+ const result = formatInputTime("00:25:04", format, input);
104
+
105
+ expect(result).toBe("12:25");
106
+ });
107
+
108
+ it("returns an empty string if an empty string is provided", () => {
109
+ const result = formatInputTime("", 24, { id: "exampleInput" });
110
+
111
+ expect(result).toBe("");
112
+ });
98
113
  })
99
114
  });
100
115
 
@@ -77,7 +77,7 @@ const createUnloadPreventer = () => {
77
77
  ev.preventDefault();
78
78
  ev.stopPropagation();
79
79
 
80
- confirmAction(confirmMessage, link).then((answer) => {
80
+ confirmAction(confirmMessage, link, { iconName: "alert-line" }).then((answer) => {
81
81
  if (!answer) {
82
82
  return;
83
83
  }
@@ -109,7 +109,7 @@ const createUnloadPreventer = () => {
109
109
  ev.stopImmediatePropagation();
110
110
  ev.stopPropagation();
111
111
 
112
- confirmAction(confirmMessage, button).then((answer) => {
112
+ confirmAction(confirmMessage, button, { iconName: "alert-line" }).then((answer) => {
113
113
  if (!answer) {
114
114
  return;
115
115
  }
@@ -13,18 +13,12 @@
13
13
  @apply block max-h-40 overflow-hidden relative first:[&_*]:mt-0 before:content-[''] before:absolute before:inset-0 before:h-full before:w-full before:bg-gradient-to-b before:from-transparent before:to-white after:content-[''] after:absolute after:inset-0 after:h-full after:w-full after:bg-gradient-to-b after:from-transparent after:to-white;
14
14
  }
15
15
 
16
- [aria-expanded="false"] > svg:last-of-type,
17
- [aria-expanded="false"] > span:last-of-type,
18
- [aria-expanded="true"] > span:first-of-type,
19
- [aria-expanded="true"] > svg:first-of-type {
20
- @apply hidden;
16
+ [aria-expanded] > svg {
17
+ @apply transition-transform;
21
18
  }
22
19
 
23
- [aria-expanded="true"] > svg:last-of-type,
24
- [aria-expanded="true"] > span:last-of-type,
25
- [aria-expanded="false"] > span:first-of-type,
26
- [aria-expanded="false"] > svg:first-of-type {
27
- @apply block;
20
+ [aria-expanded="true"] > svg {
21
+ @apply rotate-180;
28
22
  }
29
23
  }
30
24
 
@@ -92,14 +92,6 @@
92
92
  [data-dialog-closable] {
93
93
  @apply top-6 right-6;
94
94
  }
95
-
96
- [data-dialog-container] {
97
- @apply inline grid-cols-none;
98
- }
99
-
100
- [data-dialog-title] {
101
- @apply col-span-2 md:col-span-1 md:col-start-2 grid grid-cols-[auto_1fr] items-start md:items-center gap-2 pb-4 text-left text-xl text-gray-2 md:border-b md:border-gray-3;
102
- }
103
95
  }
104
96
 
105
97
  &-results {
@@ -22,19 +22,27 @@
22
22
  }
23
23
 
24
24
  [data-dialog-container] {
25
- @apply grid grid-cols-[auto_1fr] items-start md:items-center gap-2 text-left;
25
+ @apply block text-left;
26
26
 
27
- > svg {
28
- @apply w-6 h-6 text-gray fill-current flex-none;
29
- }
27
+ &:has(> svg) {
28
+ @apply grid grid-cols-[auto_1fr] items-start md:items-center gap-2;
29
+
30
+ > svg {
31
+ @apply w-6 h-6 text-gray fill-current flex-none mb-6;
32
+ }
30
33
 
31
- > :last-child {
32
- @apply col-span-2 md:col-span-1 md:col-start-2;
34
+ > :last-child {
35
+ @apply col-span-2 md:col-span-1 md:col-start-2;
36
+ }
33
37
  }
34
38
  }
35
39
 
36
40
  [data-dialog-title] {
37
- @apply text-2xl text-black font-semibold focus:outline-none;
41
+ @apply flex items-center gap-2 text-xl text-gray-2 font-semibold focus:outline-none pb-2 mb-4 md:border-b md:border-gray-3;
42
+
43
+ > svg {
44
+ @apply w-6 h-6 text-gray fill-current flex-none;
45
+ }
38
46
  }
39
47
 
40
48
  [data-dialog-actions] {
@@ -59,7 +67,7 @@
59
67
  }
60
68
 
61
69
  [data-dialog-container] {
62
- @apply grid-cols-none;
70
+ @apply grid grid-cols-none;
63
71
 
64
72
  &.layout-content {
65
73
  @apply bg-white;
@@ -1,21 +1,7 @@
1
1
  .qr-modal {
2
2
  @apply md:p-6 lg:p-8;
3
3
 
4
- [data-dialog-title] {
5
- @apply flex justify-start items-center text-2xl text-black font-semibold focus:outline-none;
6
-
7
- > svg {
8
- @apply w-10 h-10 text-gray fill-current flex-none;
9
- }
10
-
11
- > :last-child {
12
- @apply ml-1;
13
- }
14
- }
15
-
16
4
  [data-dialog-container] {
17
- @apply inline grid-cols-none;
18
-
19
5
  .content {
20
6
  @apply md:grid md:grid-cols-2 gap-5 md:py-4;
21
7
 
@@ -59,14 +45,6 @@
59
45
  @apply top-6 right-6;
60
46
  }
61
47
 
62
- [data-dialog-container] {
63
- @apply inline grid-cols-none;
64
- }
65
-
66
- [data-dialog-title] {
67
- @apply col-span-2 md:col-span-1 md:col-start-2 grid grid-cols-[auto_1fr] items-start md:items-center gap-2 pb-4 text-left text-xl text-gray-2 md:border-b md:border-gray-3;
68
- }
69
-
70
48
  &__list {
71
49
  @apply my-6 flex flex-wrap justify-center items-center md:grid grid-cols-4 md:grid-cols-5 gap-3 md:gap-6 max-w-96 mx-auto text-secondary;
72
50
 
@@ -111,7 +111,17 @@
111
111
  }
112
112
 
113
113
  &-item {
114
- @apply flex items-center gap-1 text-white font-semibold hover:underline [&>svg]:fill-current;
114
+ @apply flex items-center gap-1 text-white font-semibold hover:underline;
115
+ }
116
+
117
+ &-label {
118
+ @apply flex-1 min-w-0 overflow-hidden text-ellipsis;
119
+
120
+ overflow-wrap: anywhere;
121
+ }
122
+
123
+ &-item > svg {
124
+ @apply shrink-0 fill-current;
115
125
  }
116
126
  }
117
127
 
@@ -7,18 +7,21 @@
7
7
  .editor-content-videoEmbed {
8
8
  @apply my-10 mx-auto;
9
9
  }
10
+ }
10
11
 
11
- .editor-mention {
12
- @apply inline-flex items-center align-bottom font-semibold underline text-secondary;
12
+ // Resource mentions are rendered both inside rich text (`.rich-text-display`)
13
+ // and inside comments (`.editor-content`), so they cannot be scoped to one of
14
+ // those wrappers.
15
+ .editor-mention {
16
+ @apply inline-flex items-center align-bottom font-semibold underline text-secondary;
13
17
 
14
- img,
15
- svg {
16
- @apply w-5 h-5 rounded-full;
17
- }
18
+ img,
19
+ svg {
20
+ @apply w-5 h-5 m-0 rounded-full;
21
+ }
18
22
 
19
- span {
20
- @apply ml-1;
21
- }
23
+ span {
24
+ @apply ml-1;
22
25
  }
23
26
  }
24
27
 
@@ -27,7 +27,10 @@
27
27
  }
28
28
 
29
29
  .title-decorator {
30
- @apply h2 md:h1 decorator;
30
+ @apply h2 md:h1 decorator break-normal;
31
+
32
+ hyphens: auto;
33
+ overflow-wrap: anywhere;
31
34
  }
32
35
 
33
36
  .text-with-links {
@@ -38,7 +41,7 @@
38
41
 
39
42
  /* default styles for those dynamic text blocks */
40
43
  .editor-content {
41
- @apply prose max-w-full text-gray-2 text-lg prose-a:text-secondary break-words prose-ul:pl-4;
44
+ @apply prose max-w-full text-gray-2 text-lg prose-a:text-secondary break-words prose-ul:pl-8 prose-ol:pl-8;
42
45
 
43
46
  iframe {
44
47
  @apply w-full block max-w-[600px] max-h-[70vh] h-[450px] my-10 mx-auto;
@@ -1725,7 +1725,6 @@ body.outlook p {
1725
1725
  table.body th.decidim-bar,
1726
1726
  table.body td.decidim-bar {
1727
1727
  padding: 10px 0;
1728
- background-color: #1a181d;
1729
1728
  }
1730
1729
 
1731
1730
  table.body {
@@ -1742,10 +1741,6 @@ table.body {
1742
1741
  height: 30px;
1743
1742
  }
1744
1743
 
1745
- .cityhall-bar {
1746
- background-color: #2c2930;
1747
- }
1748
-
1749
1744
  .cityhall-logo {
1750
1745
  width: 150px;
1751
1746
  margin-top: 16px;
@@ -9,6 +9,7 @@ module Decidim
9
9
  locales_action?
10
10
  component_public_action?
11
11
  search_scope_action?
12
+ public_content_action?
12
13
 
13
14
  public_report_content_action?
14
15
 
@@ -66,6 +67,23 @@ module Decidim
66
67
  toggle_allow([:search, :pick].include?(permission_action.action))
67
68
  end
68
69
 
70
+ # Public, non-resource content that any visitor is allowed to reach. The
71
+ # controllers using these subjects enforce the actual data visibility
72
+ # themselves (profile publication, private download tokens, newsletter
73
+ # tokens, etc.).
74
+ def public_content_action?
75
+ case permission_action.subject
76
+ when :open_data, :search, :profile, :private_download, :short_link,
77
+ :calendar, :badges, :static_map, :qr_code, :homepage, :manifest,
78
+ :offline, :link, :authorization, :error, :timeout, :page
79
+ allow! if permission_action.action == :read
80
+ when :newsletter
81
+ allow! if [:read, :unsubscribe].include?(permission_action.action)
82
+ when :upload_validation
83
+ allow! if permission_action.action == :create
84
+ end
85
+ end
86
+
69
87
  def manage_self_user_action?
70
88
  return unless permission_action.subject == :user
71
89
 
@@ -78,6 +96,8 @@ module Decidim
78
96
  authorization = context.fetch(:authorization, nil)
79
97
 
80
98
  case permission_action.action
99
+ when :read
100
+ allow!
81
101
  when :create
82
102
  toggle_allow(authorization.user == user && not_already_active?(authorization))
83
103
  when :update, :destroy
@@ -29,7 +29,7 @@ module Decidim
29
29
 
30
30
  def action_string
31
31
  case action
32
- when "export_component", "soft_delete", "restore"
32
+ when "export_component", "soft_delete", "restore", "menu_hidden"
33
33
  "decidim.admin_log.component.#{action}"
34
34
  when "create", "delete", "publish", "unpublish", "update_permissions", "update_filters"
35
35
  generate_action_string(action)
@@ -111,9 +111,18 @@ module Decidim
111
111
  # Returns an HTML-safe String.
112
112
  def present_explanation
113
113
  h.content_tag(:div, class: "logs__log__explanation") do
114
+ escaped_params = i18n_params.transform_values do |value|
115
+ if value.is_a?(String) && !value.html_safe?
116
+ ERB::Util.html_escape(value)
117
+ else
118
+ value
119
+ end
120
+ end
121
+
114
122
  I18n.t(
115
123
  action_string,
116
- **i18n_params
124
+ escape: false,
125
+ **escaped_params
117
126
  ).html_safe
118
127
  end
119
128
  end
@@ -66,9 +66,9 @@ module Decidim
66
66
 
67
67
  # Private: Presents the nickname of the user performing the action.
68
68
  #
69
- # Returns an HTML-safe String.
69
+ # Returns a String.
70
70
  def present_user_nickname
71
- extra["nickname"].html_safe
71
+ extra["nickname"]
72
72
  end
73
73
 
74
74
  # Private: Calculates the path for the user. Returns the path of the
@@ -16,10 +16,19 @@ module Decidim
16
16
  about: resource.user.about,
17
17
  reasons: resource.reports.map(&:reason),
18
18
  details: resource.reports.map(&:details),
19
- block_reasons: resource.blocking.justification,
20
- blocking_user: resource.blocking.blocking_user.presenter.name
19
+ block_reasons: blocking&.justification,
20
+ blocking_user: blocking_user&.presenter&.name
21
21
  }
22
22
  end
23
+
24
+ private
25
+
26
+ # The collection filters on `decidim_users.blocked_at`, but this
27
+ # association is resolved through `decidim_users.block_id`. The two can
28
+ # disagree, in which case there is no block to read the details from.
29
+ def blocking = resource.blocking
30
+
31
+ def blocking_user = blocking&.blocking_user
23
32
  end
24
33
  end
25
34
  end
@@ -14,7 +14,7 @@ module Decidim
14
14
  id: resource.id,
15
15
  hidden_at: resource.hidden_at,
16
16
  report_count: resource.report_count,
17
- reported_url: resource.reportable.reported_content_url,
17
+ reported_url:,
18
18
  reportable_type: resource.decidim_reportable_type,
19
19
  reportable_id: resource.decidim_reportable_id,
20
20
  reported_content: resource.reported_content,
@@ -25,6 +25,30 @@ module Decidim
25
25
  }
26
26
  }
27
27
  end
28
+
29
+ private
30
+
31
+ # `Decidim::Moderation#reportable` is polymorphic, so the database cannot
32
+ # enforce the reference, and the moderation row outlives the resource it
33
+ # points at. Both of the cases below leave the moderation in the export
34
+ # collection with no URL to build.
35
+ def reported_url
36
+ reportable = resource.reportable
37
+
38
+ # The resource is gone. Reportables such as proposals and meetings are
39
+ # soft-deletable while `belongs_to :reportable` does not use
40
+ # `with_deleted`, so moving one to the trash is enough for this.
41
+ return if reportable.nil?
42
+
43
+ # The component is gone but the resource is not: trashing a component
44
+ # does not trash its contents. `ResourceLocatorPresenter#route_proxy`
45
+ # falls back to the resource itself when `component` is nil, and
46
+ # `EngineRouter.main_proxy` then calls `mounted_engine` on it, which
47
+ # only components and participatory spaces respond to.
48
+ return if reportable.is_a?(Decidim::HasComponent) && reportable.component.nil?
49
+
50
+ reportable.reported_content_url
51
+ end
28
52
  end
29
53
  end
30
54
  end