decidim-core 0.30.7 → 0.30.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5110ff22ed67aef95bf387023ec7d18d9602a92944c82c2ab9373e786c04393c
4
- data.tar.gz: b754a2b2ddb4a72ae07dcd384203d61454ced7f6a3a41a4f80a5bb501b3dc310
3
+ metadata.gz: b31c19b0f1263634559541d974f68dba477328f79a493ab4188eb3d8cb42a0eb
4
+ data.tar.gz: a9d06e9ea81dfe61b62c9f99961a4284af380a5343a12ce5c7ca7b055f4041b8
5
5
  SHA512:
6
- metadata.gz: 3659a2a34d2531735e1a03710329261952c724361d0661bb9cf02c7a2acc76c2516266e8030a82e4dcc40789361d67845f1421addfcbf0f1fe4e0743a1288572
7
- data.tar.gz: 9ec948751168cd3f4a8cc2b768be6a118cd22b47aecb0712fa64a1c01d58a5cdf2b7f2f20963fdd0eec2384a41753bbaec558016f533bfab7176227df6f3cadd
6
+ metadata.gz: 4c821103cefa1cfc8c3688fe69dce51bd2e6c94a448bee4fdc05ef5abd08e522cc4785e0827217b464b9b4a601cb52daa8539b21b0bf98f39e69fdff1b5824a5
7
+ data.tar.gz: 9c27f4b8ce372ac3295e719a3985b7276ec91d90303b4e627545d4571012936d3d4f1694c6e81e4df95fac154bbb2aaa3889130b9ffcfc609a1b2a40a699a99f
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ # Helper that provides methods to render order selector and links
5
+ module MailerHelper
6
+ # Transforms relative image URLs in HTML content to absolute URLs using the provided host.
7
+ # This is used in emails (newsletters and notifications) to ensure images display correctly
8
+ # in email clients.
9
+ #
10
+ # @param content [String] - HTML content with img tags
11
+ # @param host [String] - the Decidim::Organization host to use for the root URL
12
+ #
13
+ # @return [String] - the content with transformed image URLs
14
+ def decidim_transform_image_urls(content, host)
15
+ return content if host.blank? || content.blank?
16
+
17
+ root_url = if Rails.application.secrets.dig(:storage, :cdn_host).present?
18
+ Rails.application.secrets.dig(:storage, :cdn_host).chomp("/")
19
+ else
20
+ Decidim::EngineRouter.new("decidim", {}).root_url(host:).chomp("/")
21
+ end
22
+
23
+ content.gsub(/src\s*=\s*(['"])([^'"]*)\1/) do
24
+ quote = Regexp.last_match(1)
25
+ src_value = Regexp.last_match(2)
26
+
27
+ if src_value.blank? || src_value.start_with?("http://", "https://", "data:", "//", "cid:")
28
+ %(src=#{quote}#{src_value}#{quote})
29
+ else
30
+ normalized_src = src_value.start_with?("/") ? src_value : "/#{src_value}"
31
+ %(src=#{quote}#{root_url}#{normalized_src}#{quote})
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -57,7 +57,8 @@ module Decidim
57
57
  self,
58
58
  element_class: "font-semibold underline",
59
59
  active_class: "is-active",
60
- container_options: { class: "space-y-4 break-inside-avoid", role: :menu },
60
+ role: false,
61
+ container_options: { class: "space-y-4 break-inside-avoid" },
61
62
  label: t("layouts.decidim.footer.decidim_title")
62
63
  )
63
64
  end
@@ -3,6 +3,9 @@
3
3
  module Decidim
4
4
  # Helper that provides methods to render links with utm codes, and replaced name
5
5
  module NewslettersHelper
6
+ include Decidim::SanitizeHelper
7
+ include Decidim::MailerHelper
8
+
6
9
  # If the newsletter body there are some links and the Decidim.track_newsletter_links = true
7
10
  # it will be replaced with the utm_codes method described below.
8
11
  # for example transform "https://es.lipsum.com/" to "https://es.lipsum.com/?utm_source=localhost&utm_campaign=newsletter_11"
@@ -19,7 +22,7 @@ module Decidim
19
22
 
20
23
  content = interpret_name(content, user)
21
24
  content = track_newsletter_links(content, id, host)
22
- transform_image_urls(content, host)
25
+ decidim_transform_image_urls(content, host)
23
26
  end
24
27
 
25
28
  # this method is used to generate the root link on mail with the utm_codes
@@ -67,27 +70,6 @@ module Decidim
67
70
  content.gsub("%{name}", user.name)
68
71
  end
69
72
 
70
- # Find each img HTML tag with relative path in src attribute
71
- # For each URL, prepends the decidim.root_url
72
- # If host is not defined it returns full content
73
- #
74
- # @param content [String] - the string to convert
75
- # @param host [String] - the Decidim::Organization host to replace
76
- #
77
- # @return [String] - the content converted
78
- #
79
- def transform_image_urls(content, host)
80
- return content if host.blank?
81
-
82
- content.scan(/src\s*=\s*"([^"]*)"/).each do |src|
83
- root_url = decidim.root_url(host:)[0..-2]
84
- src_replaced = "#{root_url}#{src.first}"
85
- content = content.gsub(/src\s*=\s*"([^"]*#{src.first})"/, %(src="#{src_replaced}"))
86
- end
87
-
88
- content
89
- end
90
-
91
73
  # Add tracking query params to each links
92
74
  #
93
75
  # @param content [String] - the string to convert
@@ -7,9 +7,13 @@ module Decidim
7
7
  include LocalisedMailer
8
8
  include MultitenantAssetHost
9
9
  include Decidim::SanitizeHelper
10
+ include Decidim::MailerHelper
10
11
  include Decidim::OrganizationHelper
11
12
  helper_method :organization_name, :decidim_escape_translated, :decidim_sanitize_translated, :translated_attribute, :decidim_sanitize, :decidim_sanitize_newsletter
12
13
 
14
+ helper Decidim::SanitizeHelper
15
+ helper Decidim::MailerHelper
16
+
13
17
  after_action :set_smtp
14
18
  after_action :set_from
15
19
 
@@ -1,5 +1,31 @@
1
1
  // Utility helper functions for the date and time picker functionality
2
2
 
3
+ export const adjustPickerPosition = (input, datePickerContainer, selector) => {
4
+ const parent = input.closest(selector);
5
+
6
+ if (getComputedStyle(parent).position === "static") {
7
+ parent.style.position = "relative";
8
+ }
9
+
10
+ const rect = input.getBoundingClientRect();
11
+ const calendarHeight = datePickerContainer.offsetHeight;
12
+ const spaceAbove = rect.top;
13
+ const spaceBelow = window.innerHeight - rect.bottom;
14
+ const openBelow = spaceBelow >= calendarHeight || spaceBelow >= spaceAbove;
15
+
16
+ if (openBelow) {
17
+ // Open below
18
+ datePickerContainer.style.top = `${input.offsetHeight}px`;
19
+ datePickerContainer.style.bottom = "";
20
+ } else {
21
+ // Open above
22
+ datePickerContainer.style.top = "";
23
+ datePickerContainer.style.bottom = `${input.offsetHeight}px`;
24
+ }
25
+
26
+ datePickerContainer.style.right = "0px";
27
+ };
28
+
3
29
  export const setHour = (value, format) => {
4
30
  const hour = value.split(":")[0];
5
31
  if (format === 12) {
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable require-jsdoc */
2
2
  import icon from "src/decidim/icon"
3
- import { dateToPicker, formatDate, displayDate, formatTime, calculateDatepickerPos } from "src/decidim/datepicker/datepicker_functions"
3
+ import { dateToPicker, formatDate, displayDate, formatTime, calculateDatepickerPos, adjustPickerPosition } from "src/decidim/datepicker/datepicker_functions"
4
4
  import { dateKeyDownListener, dateBeforeInputListener } from "src/decidim/datepicker/datepicker_listeners"
5
5
  import { getDictionary } from "src/decidim/i18n"
6
6
 
@@ -130,6 +130,7 @@ export default function generateDatePicker(input, row, formats) {
130
130
  };
131
131
  pickedDate = null;
132
132
  datePickerContainer.style.display = "block";
133
+ adjustPickerPosition(date, datePickerContainer, ".datepicker__date-column");
133
134
 
134
135
  document.addEventListener("click", datePickerDisplay);
135
136
 
@@ -1,6 +1,8 @@
1
1
  /* eslint-disable require-jsdoc */
2
+ /* eslint max-lines: ["error", 310] */
3
+
2
4
  import icon from "src/decidim/icon"
3
- import { changeHourDisplay, changeMinuteDisplay, formatDate, hourDisplay, minuteDisplay, formatTime, setHour, setMinute, updateTimeValue, updateInputValue } from "src/decidim/datepicker/datepicker_functions"
5
+ import { changeHourDisplay, changeMinuteDisplay, formatDate, hourDisplay, minuteDisplay, formatTime, setHour, setMinute, updateTimeValue, updateInputValue, adjustPickerPosition } from "src/decidim/datepicker/datepicker_functions"
4
6
  import { timeKeyDownListener, timeBeforeInputListener } from "src/decidim/datepicker/datepicker_listeners";
5
7
  import { getDictionary } from "src/decidim/i18n";
6
8
 
@@ -21,6 +23,10 @@ export default function generateTimePicker(input, row, formats) {
21
23
  clock.setAttribute("type", "button");
22
24
  clock.setAttribute("aria-label", input.dataset.buttonTimeLabel);
23
25
 
26
+ if (input.attributes.disabled) {
27
+ clock.setAttribute("disabled", input.attributes.disabled);
28
+ };
29
+
24
30
  timeColumn.appendChild(time);
25
31
  timeColumn.appendChild(clock);
26
32
 
@@ -270,6 +276,8 @@ export default function generateTimePicker(input, row, formats) {
270
276
  event.preventDefault();
271
277
  timePicker.style.display = "block";
272
278
  document.addEventListener("click", timePickerDisplay);
279
+ adjustPickerPosition(time, timePicker, ".datepicker__time-column")
280
+
273
281
  hours.value = hourDisplay(hour);
274
282
  minutes.value = minuteDisplay(minute);
275
283
  });
@@ -0,0 +1,234 @@
1
+ /* global jest */
2
+
3
+ import { adjustPickerPosition } from "src/decidim/datepicker/datepicker_functions";
4
+
5
+ describe("adjustDatePickerPosition", () => {
6
+ let input = null;
7
+ let parent = null;
8
+ let datePickerContainer = null;
9
+
10
+ let originalInnerHeight = window.innerHeight;
11
+
12
+ beforeEach(() => {
13
+ // Setup DOM structure
14
+ parent = document.createElement("div");
15
+ parent.className = "datepicker__date-column";
16
+ document.body.appendChild(parent);
17
+
18
+ input = document.createElement("input");
19
+ Reflect.defineProperty(input, "offsetHeight", {
20
+ configurable: true,
21
+ value: 40
22
+ });
23
+ parent.appendChild(input);
24
+
25
+ datePickerContainer = document.createElement("div");
26
+ datePickerContainer.className = "datepicker__container";
27
+ parent.appendChild(datePickerContainer);
28
+
29
+ // Mock offsetHeight for calendar
30
+ Reflect.defineProperty(datePickerContainer, "offsetHeight", {
31
+ configurable: true,
32
+ value: 300
33
+ });
34
+
35
+ // store original viewport height
36
+ originalInnerHeight = window.innerHeight;
37
+ });
38
+
39
+ afterEach(() => {
40
+ document.body.removeChild(parent);
41
+
42
+ Reflect.defineProperty(window, "innerHeight", {
43
+ writable: true,
44
+ configurable: true,
45
+ value: originalInnerHeight
46
+ });
47
+
48
+ jest.restoreAllMocks();
49
+ });
50
+
51
+ it("sets parent position to relative when static", () => {
52
+ parent.style.position = "static";
53
+
54
+ adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column");
55
+
56
+ expect(parent.style.position).toBe("relative");
57
+ });
58
+
59
+ it("does not change parent position when already positioned", () => {
60
+ parent.style.position = "absolute";
61
+
62
+ adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column");
63
+
64
+ expect(parent.style.position).toBe("absolute");
65
+ });
66
+
67
+ it("opens below when sufficient space below", () => {
68
+ jest.spyOn(input, "getBoundingClientRect").mockReturnValue({
69
+ top: 100,
70
+ bottom: 140
71
+ });
72
+
73
+ Reflect.defineProperty(window, "innerHeight", {
74
+ writable: true,
75
+ configurable: true,
76
+ value: 800
77
+ });
78
+
79
+ adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column");
80
+
81
+ expect(datePickerContainer.style.top).toBe("40px");
82
+ expect(datePickerContainer.style.bottom).toBe("");
83
+ });
84
+
85
+ it("opens above when insufficient space below", () => {
86
+ jest.spyOn(input, "getBoundingClientRect").mockReturnValue({
87
+ top: 400,
88
+ bottom: 440
89
+ });
90
+
91
+ Reflect.defineProperty(window, "innerHeight", {
92
+ writable: true,
93
+ configurable: true,
94
+ value: 500
95
+ });
96
+
97
+ adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column");
98
+
99
+ expect(datePickerContainer.style.top).toBe("");
100
+ expect(datePickerContainer.style.bottom).toBe("40px");
101
+ });
102
+
103
+ it("prefers opening below when space is equal above and below", () => {
104
+ jest.spyOn(input, "getBoundingClientRect").mockReturnValue({
105
+ top: 250,
106
+ bottom: 290
107
+ });
108
+
109
+ Reflect.defineProperty(window, "innerHeight", {
110
+ writable: true,
111
+ configurable: true,
112
+ value: 540
113
+ });
114
+
115
+ adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column");
116
+
117
+ expect(datePickerContainer.style.top).toBe("40px");
118
+ expect(datePickerContainer.style.bottom).toBe("");
119
+ });
120
+
121
+ it("always sets right position to 0px", () => {
122
+ jest.spyOn(input, "getBoundingClientRect").mockReturnValue({
123
+ top: 100,
124
+ bottom: 140
125
+ });
126
+
127
+ adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column");
128
+
129
+ expect(datePickerContainer.style.right).toBe("0px");
130
+ });
131
+ });
132
+
133
+
134
+ describe("adjustTimePickerPosition", () => {
135
+ let input = null;
136
+ let parent = null;
137
+ let timePicker = null;
138
+
139
+ let originalInnerHeight = window.innerHeight;
140
+
141
+ beforeEach(() => {
142
+ parent = document.createElement("div");
143
+ parent.className = "datepicker__time-column";
144
+ document.body.appendChild(parent);
145
+
146
+ input = document.createElement("input");
147
+ Reflect.defineProperty(input, "offsetHeight", {
148
+ configurable: true,
149
+ value: 30
150
+ });
151
+ parent.appendChild(input);
152
+
153
+ timePicker = document.createElement("div");
154
+ timePicker.className = "timepicker__container";
155
+ parent.appendChild(timePicker);
156
+
157
+ Reflect.defineProperty(timePicker, "offsetHeight", {
158
+ configurable: true,
159
+ value: 200
160
+ });
161
+
162
+ // store original value before any test mutates it
163
+ originalInnerHeight = window.innerHeight;
164
+ });
165
+
166
+ afterEach(() => {
167
+ // restore DOM
168
+ document.body.removeChild(parent);
169
+
170
+ // restore window.innerHeight (fix for CodeRabbit warning)
171
+ Reflect.defineProperty(window, "innerHeight", {
172
+ writable: true,
173
+ configurable: true,
174
+ value: originalInnerHeight
175
+ });
176
+
177
+ jest.restoreAllMocks();
178
+ });
179
+
180
+ it("sets parent position to relative when static", () => {
181
+ parent.style.position = "static";
182
+
183
+ adjustPickerPosition(input, timePicker, ".datepicker__time-column");
184
+
185
+ expect(parent.style.position).toBe("relative");
186
+ });
187
+
188
+ it("opens below when there is enough space", () => {
189
+ jest.spyOn(input, "getBoundingClientRect").mockReturnValue({
190
+ top: 100,
191
+ bottom: 130
192
+ });
193
+
194
+ Reflect.defineProperty(window, "innerHeight", {
195
+ writable: true,
196
+ configurable: true,
197
+ value: 700
198
+ });
199
+
200
+ adjustPickerPosition(input, timePicker, ".datepicker__time-column");
201
+
202
+ expect(timePicker.style.top).toBe("30px");
203
+ expect(timePicker.style.bottom).toBe("");
204
+ });
205
+
206
+ it("opens above when there is not enough space below", () => {
207
+ jest.spyOn(input, "getBoundingClientRect").mockReturnValue({
208
+ top: 400,
209
+ bottom: 430
210
+ });
211
+
212
+ Reflect.defineProperty(window, "innerHeight", {
213
+ writable: true,
214
+ configurable: true,
215
+ value: 500
216
+ });
217
+
218
+ adjustPickerPosition(input, timePicker, ".datepicker__time-column");
219
+
220
+ expect(timePicker.style.top).toBe("");
221
+ expect(timePicker.style.bottom).toBe("30px");
222
+ });
223
+
224
+ it("always aligns to the right", () => {
225
+ jest.spyOn(input, "getBoundingClientRect").mockReturnValue({
226
+ top: 100,
227
+ bottom: 130
228
+ });
229
+
230
+ adjustPickerPosition(input, timePicker, ".datepicker__time-column");
231
+
232
+ expect(timePicker.style.right).toBe("0px");
233
+ });
234
+ });
@@ -26,7 +26,7 @@ module Decidim
26
26
  delegate :content_tag, :safe_join, :link_to, :active_link_to_class, :is_active_link?, :icon, to: :@view
27
27
 
28
28
  def render
29
- content_tag :li, role: :menuitem, class: link_wrapper_classes do
29
+ content_tag :li, role: menuitem_role, class: link_wrapper_classes do
30
30
  output = if url == "#"
31
31
  [content_tag(:span, composed_label, class: "sidebar-menu__item-disabled")]
32
32
  else
@@ -62,6 +62,12 @@ module Decidim
62
62
  [@options.element_class, active_class].compact.join(" ")
63
63
  end
64
64
 
65
+ def menuitem_role
66
+ return if @options.role == false
67
+
68
+ @options.role || :menuitem
69
+ end
70
+
65
71
  def active_class
66
72
  active_link_to_class(
67
73
  url,
@@ -34,6 +34,7 @@
34
34
  <%= f.text_field :name, help_text: t("decidim.devise.registrations.new.username_help"), autocomplete: "name", placeholder: "John Doe" %>
35
35
 
36
36
  <%= f.email_field :email, autocomplete: "email", placeholder: t("placeholder_email", scope: "decidim.devise.shared") %>
37
+ <span class="sr-only"><%= t("placeholder_email", scope: "decidim.devise.shared") %></span>
37
38
 
38
39
  <%= render partial: "decidim/account/password_fields", locals: { form: f, user: :user } %>
39
40
  </div>
@@ -1,13 +1,13 @@
1
1
  <div id="card__tos" class="form__wrapper-block border-y-2">
2
2
  <h2 class="h4"><%= t("decidim.devise.registrations.new.tos_title") %></h2>
3
+ <span class="sr-only"><%= t("forms.required") %></span>
3
4
 
4
- <div>
5
+ <div id="terms_of_service_summary">
5
6
  <% terms_of_service_summary_content_blocks.each do |content_block| %>
6
7
  <%= cell content_block.manifest.cell, content_block %>
7
8
  <% end %>
8
9
  </div>
9
-
10
- <%= form.check_box :tos_agreement, label: t("decidim.devise.registrations.new.tos_agreement", link: link_to(t("decidim.devise.registrations.new.terms"), decidim.page_path("terms-of-service"))), label_options: { class: "form__wrapper-checkbox-label" } %>
10
+ <%= form.check_box :tos_agreement, label: t("decidim.devise.registrations.new.tos_agreement", link: link_to(t("decidim.devise.registrations.new.terms"), decidim.page_path("terms-of-service"))), label_options: { class: "form__wrapper-checkbox-label" }, "aria-describedby": "terms_of_service_summary" %>
11
11
  </div>
12
12
 
13
13
  <div id="card__newsletter" class="form__wrapper-block">
@@ -15,7 +15,7 @@
15
15
 
16
16
  <blockquote>
17
17
  <p>
18
- <%= @event_instance.safe_resource_text %>
18
+ <%= decidim_transform_image_urls(@event_instance.safe_resource_text, @organization.host).html_safe %>
19
19
  </p>
20
20
  </blockquote>
21
21
  <% end %>
@@ -28,7 +28,7 @@
28
28
  <p style="font-weight: bold"><%= t(".translated_text") %></p>
29
29
  <blockquote>
30
30
  <p>
31
- <%= @event_instance.safe_resource_translated_text %>
31
+ <%= decidim_transform_image_urls(@event_instance.safe_resource_translated_text, @organization.host).html_safe %>
32
32
  </p>
33
33
  </blockquote>
34
34
  <% end %>
@@ -40,7 +40,7 @@
40
40
  <table>
41
41
  <tr>
42
42
  <td>
43
- <%= link_to @event_instance.button_text, @event_instance.button_url, target: :blank %>
43
+ <%= link_to decidim_sanitize(@event_instance.button_text, strip_tags: true), @event_instance.button_url, target: :blank %>
44
44
  </td>
45
45
  </tr>
46
46
  </table>
@@ -1214,6 +1214,9 @@ cs:
1214
1214
  actions:
1215
1215
  confirm_modal:
1216
1216
  ok_add: Přidat administrátora
1217
+ ok_remove: Odstranit správce
1218
+ title_add: Potvrdit nového správce
1219
+ title_remove: Odstranit správce
1217
1220
  demote_admin: Odstranit admin
1218
1221
  demote:
1219
1222
  error: Při odebrání tohoto účastníka ze seznamu administrátorů došlo k chybě.
@@ -1118,6 +1118,8 @@ de:
1118
1118
  explanation: 'Anleitung für Bild:'
1119
1119
  message_1: Vorzugsweise ein Bild im Querformat, das keinen Text enthält.
1120
1120
  message_2: Der Dienst schneidet die Datei zu.
1121
+ import_file:
1122
+ message_1: Muss ein JSON-Dokument sein, das über die Exportfunktion heruntergeladen wurde.
1121
1123
  file_validation:
1122
1124
  allowed_file_extensions: 'Erlaubte Dateiformate: %{extensions}'
1123
1125
  max_file_dimension: 'Maximale Dateigröße: %{resolution} Pixel'
@@ -1478,9 +1480,13 @@ de:
1478
1480
  same_language: Der Inhalt wurde in Ihrer bevorzugten Sprache (%{language}) veröffentlicht, daher wird in dieser E-Mail keine automatisierte Übersetzung angezeigt.
1479
1481
  translated_text: 'Automatisch übersetzter Text:'
1480
1482
  notifications:
1483
+ action_error: Beim Aktualisieren der Benachrichtigungseinstellungen ist ein Fehler aufgetreten.
1481
1484
  no_notifications: Noch keine Benachrichtigungen
1482
1485
  show:
1486
+ deleted: Inhalt wurde vom Autor gelöscht.
1483
1487
  missing_event: Hoppla, diese Benachrichtigung gehört zu einem Artikel, der nicht mehr verfügbar ist. Du kannst sie verwerfen.
1488
+ moderated: Inhalt wurde durch Moderation versteckt.
1489
+ not_available: Hoppla, diese Benachrichtigung gehört zu einem Artikel, der nicht mehr verfügbar ist. Es ist keine weitere Aktion erforderlich.
1484
1490
  notifications_digest_mailer:
1485
1491
  header:
1486
1492
  daily: Tägliche Zusammenfassung
@@ -1489,6 +1495,7 @@ de:
1489
1495
  hello: Hallo %{name}
1490
1496
  intro:
1491
1497
  daily: 'Dies sind die Benachrichtigungen vom letzten Tag basierend auf den Aktivitäten, denen Sie folgen:'
1498
+ real_time: 'Es gibt eine Benachrichtigung über die Aktivität, die Sie folgen:'
1492
1499
  weekly: 'Dies sind die Benachrichtigungen der letzten Woche, basierend auf den Aktivitäten, die Sie folgen:'
1493
1500
  outro: Sie haben diese Benachrichtigung erhalten, weil Sie diesen Inhalt oder seine Verfassenden folgen. Sie können dem Inhalt direkt auf seiner Seite entfolgen.
1494
1501
  see_more: Weitere Benachrichtigungen ansehen
@@ -1642,7 +1649,9 @@ de:
1642
1649
  title: Wie man diese Dateien öffnet und mit ihnen arbeitet
1643
1650
  license:
1644
1651
  body_1_html: Diese Datenbank von %{organization_name} wird unter %{link_database} zur Verfügung gestellt. Alle Rechte an einzelnen Inhalten der Datenbank sind unter %{link_contents} lizenziert.
1652
+ license_contents_link: https://opendatacommons.org/licenses/dbcl/1.0/
1645
1653
  license_contents_name: Lizenz für Datenbankinhalte
1654
+ license_database_link: https://opendatacommons.org/licenses/odbl/1.0/
1646
1655
  license_database_name: Offene Datenbanklizenz
1647
1656
  title: Lizenz
1648
1657
  title: Offene Daten
@@ -1962,6 +1971,7 @@ de:
1962
1971
  send_paranoid_instructions: Wenn Ihre E-Mail-Adresse in unserer Datenbank vorhanden ist, erhalten Sie innerhalb weniger Minuten eine E-Mail mit Anweisungen zur Bestätigung Ihrer E-Mail-Adresse.
1963
1972
  failure:
1964
1973
  already_authenticated: Sie sind bereits angemeldet.
1974
+ csrf_token: Ihre Anfrage konnte nicht verifiziert werden. Bitte versuchen Sie es erneut.
1965
1975
  inactive: Dein Benutzerkonto ist noch nicht aktiviert.
1966
1976
  invalid: Ungültige %{authentication_keys} oder Passwort
1967
1977
  invited: Sie haben eine ausstehende Einladung, akzeptieren Sie sie, um die Erstellung Ihres Kontos abzuschließen.
@@ -1976,6 +1986,7 @@ de:
1976
1986
  nickname_help: Ihr Pseudonym auf %{organization}. Kann nur Buchstaben, Zahlen, '-' und '_' enthalten.
1977
1987
  submit_button: Speichern
1978
1988
  subtitle: Wenn Sie die Einladung annehmen, geben Sie bitte Ihren Kontonamen und Ihr Passwort ein.
1989
+ subtitle_no_password: Wenn Sie die Einladung annehmen, geben Sie bitte Ihren Kontonamen ein.
1979
1990
  invitation_removed: Ihre Einladung wurde entfernt.
1980
1991
  invitation_token_invalid: Das angegebene Einladungstoken ist nicht gültig!
1981
1992
  new:
@@ -1983,6 +1994,8 @@ de:
1983
1994
  submit_button: Eine Einladung schicken
1984
1995
  no_invitations_remaining: Keine Einladungen übrig
1985
1996
  send_instructions: Eine Einladungs-E-Mail wurde an %{email}gesendet.
1997
+ updated: Einladung erfolgreich angenommen. Sie sind jetzt angemeldet.
1998
+ updated_not_active: Einladung erfolgreich angenommen.
1986
1999
  mailer:
1987
2000
  confirmation_instructions:
1988
2001
  action: Konto bestätigen
@@ -1989,6 +1989,7 @@ eu:
1989
1989
  nickname_help: Zure ezizena %{organization}-an. Letrak, zenbakiak, '-' eta '_' soilik eduki ditzake.
1990
1990
  submit_button: Gorde
1991
1991
  subtitle: Gonbidapena onartzen baduzu, mesedez, ezarri zure ezizena eta pasahitza.
1992
+ subtitle_no_password: Gonbidapena onartzen baduzu, jarri zure ezizena.
1992
1993
  invitation_removed: Zure gonbidapena kendu egin da.
1993
1994
  invitation_token_invalid: Gonbidapen token hori ez da baliozkoa!
1994
1995
  new:
@@ -1996,6 +1997,8 @@ eu:
1996
1997
  submit_button: Bidali gonbidapena
1997
1998
  no_invitations_remaining: Ez da gonbidapenik geratzen
1998
1999
  send_instructions: Gonbidapen-mezu elektroniko bat %{email} helbidera bidali da.
2000
+ updated: Gonbidapena behar bezala onartu da. Orain erregistratuta zaude.
2001
+ updated_not_active: Gonbidapena behar bezala onartu da.
1999
2002
  mailer:
2000
2003
  confirmation_instructions:
2001
2004
  action: Berretsi nire kontua
@@ -1991,6 +1991,7 @@ fi-pl:
1991
1991
  nickname_help: Nimimerkkisi palvelussa %{organization}. Voi sisältää ainoastaan kirjaimia, numeroita sekä yhdysmerkkejä "-" ja alaviivoja "_".
1992
1992
  submit_button: Tallenna
1993
1993
  subtitle: Jos hyväksyt kutsun, aseta käyttäjänimesi ja salasana.
1994
+ subtitle_no_password: Jos hyväksyt kutsun, aseta tilillesi nimi.
1994
1995
  invitation_removed: Kutsusi peruutettiin.
1995
1996
  invitation_token_invalid: Käyttämäsi kutsuavain ei ole voimassa!
1996
1997
  new:
@@ -1998,6 +1999,8 @@ fi-pl:
1998
1999
  submit_button: Lähetä kutsu
1999
2000
  no_invitations_remaining: Ei kutsuja jäljellä
2000
2001
  send_instructions: Kutsuviesti on lähetetty %{email}.
2002
+ updated: Kutsun hyväksyminen onnistui. Olet nyt kirjautunut sisään.
2003
+ updated_not_active: Kutsun hyväksyminen onnistui.
2001
2004
  mailer:
2002
2005
  confirmation_instructions:
2003
2006
  action: Vahvista käyttäjätilini
@@ -2057,6 +2060,8 @@ fi-pl:
2057
2060
  confirm_new_password: Vahvista uusi salasana
2058
2061
  new_password: Uusi salasana
2059
2062
  old_password_help: Vahvistaaksesi muutokset käyttäjätiliisi, anna nykyinen salasanasi.
2063
+ password_help: "Vähintään %{minimum_characters} merkkiä, vähintään 5 eri merkkiä, ei voi olla yleisesti käytetty salasana (esim. 123456), eikä voi vastata nimeäsi, nimimerkkiäsi, sähköpostiosoitettasi tai tämän palvelun verkko-osoitetta."
2064
+ password_help_admin: "Vähintään %{minimum_characters} merkkiä, vähintään 5 eri merkkiä, ei voi olla yleisesti käytetty salasana (esim. 123456), eikä voi vastata nimeäsi, nimimerkkiäsi, sähköpostiosoitettasi tai tämän palvelun verkko-osoitetta. Et myöskään voi käyttää aikaisempia salasanojasi."
2060
2065
  title: Salasanan vaihto
2061
2066
  new:
2062
2067
  forgot_your_password: Unohditko salasanasi?
@@ -1991,6 +1991,7 @@ fi:
1991
1991
  nickname_help: Nimimerkkisi palvelussa %{organization}. Voi sisältää ainoastaan kirjaimia, numeroita sekä yhdysmerkkejä "-" ja alaviivoja "_".
1992
1992
  submit_button: Tallenna
1993
1993
  subtitle: Jos hyväksyt kutsun, aseta käyttäjänimesi ja salasana.
1994
+ subtitle_no_password: Jos hyväksyt kutsun, aseta tilillesi nimi.
1994
1995
  invitation_removed: Kutsusi on peruttu.
1995
1996
  invitation_token_invalid: Käyttämäsi kutsuavain ei ole voimassa!
1996
1997
  new:
@@ -1998,6 +1999,8 @@ fi:
1998
1999
  submit_button: Lähetä kutsu
1999
2000
  no_invitations_remaining: Ei kutsuja jäljellä
2000
2001
  send_instructions: Kutsuviesti on lähetetty %{email}.
2002
+ updated: Kutsun hyväksyminen onnistui. Olet nyt kirjautunut sisään.
2003
+ updated_not_active: Kutsun hyväksyminen onnistui.
2001
2004
  mailer:
2002
2005
  confirmation_instructions:
2003
2006
  action: Vahvista käyttäjätilini
@@ -2057,6 +2060,8 @@ fi:
2057
2060
  confirm_new_password: Vahvista uusi salasana
2058
2061
  new_password: Uusi salasana
2059
2062
  old_password_help: Vahvistaaksesi muutokset käyttäjätiliisi, anna nykyinen salasanasi.
2063
+ password_help: "Vähintään %{minimum_characters} merkkiä, vähintään 5 eri merkkiä, ei voi olla yleisesti käytetty salasana (esim. 123456), eikä voi vastata nimeäsi, nimimerkkiäsi, sähköpostiosoitettasi tai tämän palvelun verkko-osoitetta."
2064
+ password_help_admin: "Vähintään %{minimum_characters} merkkiä, vähintään 5 eri merkkiä, ei voi olla yleisesti käytetty salasana (esim. 123456), eikä voi vastata nimeäsi, nimimerkkiäsi, sähköpostiosoitettasi tai tämän palvelun verkko-osoitetta. Et myöskään voi käyttää aikaisempia salasanojasi."
2060
2065
  title: Salasanan vaihto
2061
2066
  new:
2062
2067
  forgot_your_password: Unohditko salasanasi?
@@ -34,8 +34,8 @@ module Decidim
34
34
  # Group 6: Variation key for representations
35
35
  /(?<variation_part>[\w.=-]+)
36
36
  )?
37
- # Group 7: Filename
38
- /([\w.=-]+)
37
+ # Group 7: Filename (supports apostrophes inside names but not as HTML quote delimiters)
38
+ /((?:[^\s/"<>']|'(?=[^\s/"<>']))+)
39
39
  }x
40
40
 
41
41
  def rewrite
@@ -18,8 +18,8 @@ module Decidim
18
18
  #
19
19
  # @see BaseRenderer Examples of how to use a content renderer
20
20
  class BlobRenderer < BaseRenderer
21
- # Matches a global id representing a Decidim::User
22
- GLOBAL_ID_REGEX = %r{(gid://[\w-]+/ActiveStorage::Blob/\d+)(/([\w=-]+))?}
21
+ # Matches a global id representing an ActiveStorage::Blob (optionally with a variant key)
22
+ GLOBAL_ID_REGEX = %r{(gid://[\w-]+/ActiveStorage::Blob/\d+)(/([\w=-]+))?(?:[^\s"'<>]*)}
23
23
 
24
24
  # Replaces found Global IDs matching an existing blob with a URL to
25
25
  # that blob. The Global IDs representing an invalid ActiveStorage::Blob
@@ -4,7 +4,7 @@ module Decidim
4
4
  # This holds the decidim-core version.
5
5
  module Core
6
6
  def self.version
7
- "0.30.7"
7
+ "0.30.8"
8
8
  end
9
9
  end
10
10
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: decidim-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.30.7
4
+ version: 0.30.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josep Jaume Rey Peroy
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2026-03-26 00:00:00.000000000 Z
13
+ date: 2026-04-24 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: concurrent-ruby
@@ -794,28 +794,28 @@ dependencies:
794
794
  requirements:
795
795
  - - '='
796
796
  - !ruby/object:Gem::Version
797
- version: 0.30.7
797
+ version: 0.30.8
798
798
  type: :development
799
799
  prerelease: false
800
800
  version_requirements: !ruby/object:Gem::Requirement
801
801
  requirements:
802
802
  - - '='
803
803
  - !ruby/object:Gem::Version
804
- version: 0.30.7
804
+ version: 0.30.8
805
805
  - !ruby/object:Gem::Dependency
806
806
  name: decidim-dev
807
807
  requirement: !ruby/object:Gem::Requirement
808
808
  requirements:
809
809
  - - '='
810
810
  - !ruby/object:Gem::Version
811
- version: 0.30.7
811
+ version: 0.30.8
812
812
  type: :development
813
813
  prerelease: false
814
814
  version_requirements: !ruby/object:Gem::Requirement
815
815
  requirements:
816
816
  - - '='
817
817
  - !ruby/object:Gem::Version
818
- version: 0.30.7
818
+ version: 0.30.8
819
819
  description: Adds core features so other engines can hook into the framework.
820
820
  email:
821
821
  - josepjaume@gmail.com
@@ -1388,6 +1388,7 @@ files:
1388
1388
  - app/helpers/decidim/language_chooser_helper.rb
1389
1389
  - app/helpers/decidim/layout_helper.rb
1390
1390
  - app/helpers/decidim/localized_locales_helper.rb
1391
+ - app/helpers/decidim/mailer_helper.rb
1391
1392
  - app/helpers/decidim/map_helper.rb
1392
1393
  - app/helpers/decidim/markup_helper.rb
1393
1394
  - app/helpers/decidim/menu_helper.rb
@@ -1619,6 +1620,7 @@ files:
1619
1620
  - app/packs/src/decidim/datepicker/generate_datepicker.js
1620
1621
  - app/packs/src/decidim/datepicker/generate_timepicker.js
1621
1622
  - app/packs/src/decidim/datepicker/test/date.test.js
1623
+ - app/packs/src/decidim/datepicker/test/datepicker_functions_adjust_picker_position.test.js
1622
1624
  - app/packs/src/decidim/datepicker/test/time.test.js
1623
1625
  - app/packs/src/decidim/decidim_application.js
1624
1626
  - app/packs/src/decidim/delayed.js