From 707ace0f8bf9d144a596234ba623c90fd862c0e5 Mon Sep 17 00:00:00 2001 From: stephanie rousset Date: Mon, 8 Dec 2025 17:20:12 +0100 Subject: [PATCH 001/116] fix: replace meaningless span by p tag in statistic cell --- decidim-core/app/cells/decidim/statistic/show.erb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/decidim-core/app/cells/decidim/statistic/show.erb b/decidim-core/app/cells/decidim/statistic/show.erb index 31ba229089d2b..9bb0bb460dabf 100644 --- a/decidim-core/app/cells/decidim/statistic/show.erb +++ b/decidim-core/app/cells/decidim/statistic/show.erb @@ -1,9 +1,9 @@
-
+

<%= stat_title %> -

+

<%= information_tooltip %>
@@ -13,14 +13,14 @@
<% if second_stat_number %> -
+

<%= stat_sub_title %> <%= second_stat_number %> -

+

<% else %>

<% end %> -
+

<%= stat_number %> -

+

From 5abf88e7445a40b487d05a810f217409803dc23a Mon Sep 17 00:00:00 2001 From: stephanie rousset Date: Mon, 8 Dec 2025 17:23:52 +0100 Subject: [PATCH 002/116] fix replace meaningless span by p tag in participatory_space_metadata content cell --- .../content_blocks/participatory_space_metadata/content.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/decidim-core/app/cells/decidim/content_blocks/participatory_space_metadata/content.erb b/decidim-core/app/cells/decidim/content_blocks/participatory_space_metadata/content.erb index 54b23bfe7bd43..0c1da81bef8d9 100644 --- a/decidim-core/app/cells/decidim/content_blocks/participatory_space_metadata/content.erb +++ b/decidim-core/app/cells/decidim/content_blocks/participatory_space_metadata/content.erb @@ -2,9 +2,9 @@ <% metadata_valued_items.each do |item| %> <% end %> From 3c902deb2612a694668011aac6152a63af9577b4 Mon Sep 17 00:00:00 2001 From: stephanie rousset Date: Mon, 8 Dec 2025 17:24:59 +0100 Subject: [PATCH 003/116] fix: replace span by p tag in comment's alignment_badge cell --- .../app/cells/decidim/comments/comment/alignment_badge.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/decidim-comments/app/cells/decidim/comments/comment/alignment_badge.erb b/decidim-comments/app/cells/decidim/comments/comment/alignment_badge.erb index 9f11f2a607000..a8197fb093859 100644 --- a/decidim-comments/app/cells/decidim/comments/comment/alignment_badge.erb +++ b/decidim-comments/app/cells/decidim/comments/comment/alignment_badge.erb @@ -1 +1 @@ - + From 5bf76cb3cf4668993568f6baccd9bf3e3340d6f4 Mon Sep 17 00:00:00 2001 From: stephanie rousset Date: Mon, 8 Dec 2025 17:26:14 +0100 Subject: [PATCH 004/116] fix: add a p tag for reference in participatory process show --- .../participatory_processes/show.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/decidim-participatory_processes/app/views/decidim/participatory_processes/participatory_processes/show.html.erb b/decidim-participatory_processes/app/views/decidim/participatory_processes/participatory_processes/show.html.erb index fc7df00685eb6..fb0b5987bfec6 100644 --- a/decidim-participatory_processes/app/views/decidim/participatory_processes/participatory_processes/show.html.erb +++ b/decidim-participatory_processes/app/views/decidim/participatory_processes/participatory_processes/show.html.erb @@ -38,6 +38,6 @@
- <%= resource_reference(current_participatory_space) %> +

<%= resource_reference(current_participatory_space) %>

From 4cb195279579e33627ad00aded1da382da4f64a1 Mon Sep 17 00:00:00 2001 From: stephanie rousset Date: Mon, 8 Dec 2025 17:41:36 +0100 Subject: [PATCH 005/116] fix: replace span by p tag in upload_field js --- .../app/packs/src/decidim/direct_uploads/upload_field.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/decidim-core/app/packs/src/decidim/direct_uploads/upload_field.js b/decidim-core/app/packs/src/decidim/direct_uploads/upload_field.js index 02027cac228b0..8f4fd49f81b4d 100644 --- a/decidim-core/app/packs/src/decidim/direct_uploads/upload_field.js +++ b/decidim-core/app/packs/src/decidim/direct_uploads/upload_field.js @@ -79,7 +79,7 @@ const updateActiveUploads = (modal) => { const template = `
${(/image/).test(file.type) && "
" || ""} - ${escapeHtml(title)} +

${escapeHtml(title)}

${hidden}
` From 6c681fc0729b4bf8e96b508ed4b0884a501a93a7 Mon Sep 17 00:00:00 2001 From: stephanie rousset Date: Mon, 8 Dec 2025 17:47:59 +0100 Subject: [PATCH 006/116] fix: replace span by p tag, and label by p in upload modal --- .../app/cells/decidim/upload_modal/files.erb | 9 ++++--- .../app/cells/decidim/upload_modal_cell.rb | 4 +++ decidim-core/lib/decidim/form_builder.rb | 26 ++++++++++++++++++- .../_edit_form_fields.html.erb | 3 ++- .../proposals/_edit_form_fields.html.erb | 4 +-- 5 files changed, 38 insertions(+), 8 deletions(-) diff --git a/decidim-core/app/cells/decidim/upload_modal/files.erb b/decidim-core/app/cells/decidim/upload_modal/files.erb index d56cabe7df66b..78913c4883ee7 100644 --- a/decidim-core/app/cells/decidim/upload_modal/files.erb +++ b/decidim-core/app/cells/decidim/upload_modal/files.erb @@ -1,9 +1,10 @@
- <%= label %> + <%# proposals option is only set in proposals and collaborative_drafts edit_form_fields partials %> + <%= options[:proposals] == true ? paragraph : label %> <% if options[:help_text].present? %> - <%= options[:help_text] %> +

<%= options[:help_text] %>

<% end %> <%# NOTE: this block is about wrapping a default image for the avatar with the new styles, @@ -31,11 +32,11 @@ <% end %> <% if has_title? %> - <%= title_for(attachment) %> +

<%= title_for(attachment) %>

<%= form.hidden_field attribute, multiple: true, value: attachment.id, id: attachment.id %> <% else %> <% if blob(attachment).image? %> - <%= title_for(attachment) %> +

<%= title_for(attachment) %>

<% else %> <%= link_to title_for(attachment), file_attachment_path(attachment), class: "w-full break-all mb-2" %> <% end %> diff --git a/decidim-core/app/cells/decidim/upload_modal_cell.rb b/decidim-core/app/cells/decidim/upload_modal_cell.rb index 6f743d00ae465..1d088a79df3d6 100644 --- a/decidim-core/app/cells/decidim/upload_modal_cell.rb +++ b/decidim-core/app/cells/decidim/upload_modal_cell.rb @@ -30,6 +30,10 @@ def label form.send(:custom_label, attribute, options[:label], { required: required?, for: nil }) end + def paragraph + form.send(:custom_paragraph, attribute, options[:label], { required: required? }) + end + def button_label return button_edit_label if attachments.count.positive? diff --git a/decidim-core/lib/decidim/form_builder.rb b/decidim-core/lib/decidim/form_builder.rb index 43ce2905e5302..c74b0e0079bc4 100644 --- a/decidim-core/lib/decidim/form_builder.rb +++ b/decidim-core/lib/decidim/form_builder.rb @@ -615,9 +615,33 @@ def custom_label(attribute, text, options, field_before_label: false, show_requi else text end - label(attribute, text, options || {}) end + + # render p tag instead of label for proposals "add a document" + def custom_paragraph(attribute, text, options, field_before_label: false, show_required: true) + return "".html_safe if text == false + + required = options.is_a?(Hash) && options.delete(:required) + text = attribute.to_s.humanize if text.nil? || text == true + if show_required + text += + if required + required_indicator + else + required_for_attribute(attribute) + end + end + + text = if field_before_label && block_given? + safe_join([yield, text.html_safe]) + elsif block_given? + safe_join([text.html_safe, yield]) + else + text + end + "

#{text}

".html_safe + end # rubocop:enable Metrics/PerceivedComplexity # rubocop:enable Metrics/CyclomaticComplexity diff --git a/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_edit_form_fields.html.erb b/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_edit_form_fields.html.erb index a01db0fd69890..3bfb0af82a6fa 100644 --- a/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_edit_form_fields.html.erb +++ b/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_edit_form_fields.html.erb @@ -21,5 +21,6 @@ button_edit_label: t("decidim.proposals.collaborative_drafts.new.edit_file"), button_class: "button button__lg button__transparent-secondary w-full", help_text: t("attachment_legend", scope: "decidim.proposals.collaborative_drafts.edit"), - help_i18n_scope: "decidim.forms.file_help.file" %> + help_i18n_scope: "decidim.forms.file_help.file", + proposals: true %> <% end %> diff --git a/decidim-proposals/app/views/decidim/proposals/proposals/_edit_form_fields.html.erb b/decidim-proposals/app/views/decidim/proposals/proposals/_edit_form_fields.html.erb index df4e316399256..5838b4536aa3c 100644 --- a/decidim-proposals/app/views/decidim/proposals/proposals/_edit_form_fields.html.erb +++ b/decidim-proposals/app/views/decidim/proposals/proposals/_edit_form_fields.html.erb @@ -22,7 +22,6 @@ <%= filter_taxonomy_items_select_field form, :taxonomies, filter %> <% end %> <% end %> - <% if component_settings.attachments_allowed? && (new_proposal || @proposal) %> <%= form.attachment :documents, multiple: true, @@ -31,5 +30,6 @@ button_edit_label: t("decidim.proposals.proposals.edit.edit_attachments"), button_class: "button button__lg button__transparent-secondary w-full", help_i18n_scope: "decidim.forms.file_help.file", - help_text: t("attachment_legend", scope: "decidim.proposals.proposals.edit") %> + help_text: t("attachment_legend", scope: "decidim.proposals.proposals.edit"), + proposals: true %> <% end %> From b2d0ebec33daf37d9a68cb2d951798621d049213 Mon Sep 17 00:00:00 2001 From: decidim-bot Date: Tue, 9 Dec 2025 17:30:25 +0100 Subject: [PATCH 007/116] New Crowdin updates (#15681) * New translations en.yml (Swedish) * New translations en.yml (Japanese) * New translations en.yml (Japanese) * New translations en.yml (Japanese) * New translations en.yml (Japanese) * New translations en.yml (Japanese) * New translations en.yml (Japanese) * New translations en.yml (Japanese) * New translations en.yml (Japanese) * New translations en.yml (Japanese) * New translations en.yml (Japanese) * New translations en.yml (Japanese) * New translations en.yml (Japanese) * New translations en.yml (Japanese) * New translations en.yml (Basque) * New translations en.yml (German) * New translations en.yml (Basque) * New translations en.yml (French) * New translations en.yml (Spanish) * New translations en.yml (Catalan) * New translations en.yml (Swedish) * New translations en.yml (Turkish) * New translations en.yml (Turkish) * New translations en.yml (Spanish, Mexico) * New translations en.yml (French, Canada) * New translations en.yml (Spanish, Paraguay) * New translations en.yml (Catalan, Algherese) * New translations en.yml (German) * New translations en.yml (Finnish) * New translations en.yml (Finnish) * New translations en.yml (Finnish) * New translations en.yml (German) * New translations en.yml (Basque) * New translations en.yml (Basque) * New translations en.yml (Basque) * New translations en.yml (Finnish) * New translations en.yml (Finnish) * New translations en.yml (Finnish) * New translations en.yml (Finnish) * New translations en.yml (Turkish) * New translations en.yml (Finnish (plain)) * New translations en.yml (Czech) * New translations en.yml (Czech) * New translations en.yml (Czech) * New translations en.yml (Czech) * New translations en.yml (Czech) * New translations en.yml (Basque) * New translations en.yml (Italian) * New translations en.yml (Finnish (plain)) * New translations en.yml (French) * New translations en.yml (Czech) * New translations en.yml (Czech) * New translations en.yml (French, Canada) * New translations en.yml (Finnish (plain)) * New translations en.yml (Finnish (plain)) * New translations en.yml (Finnish (plain)) * New translations en.yml (Finnish (plain)) * New translations en.yml (Finnish (plain)) * New translations en.yml (Spanish) * New translations en.yml (Catalan) * New translations en.yml (Basque) * New translations en.yml (Swedish) * New translations en.yml (Turkish) * New translations en.yml (Turkish) * New translations en.yml (Spanish, Mexico) * New translations en.yml (Spanish, Paraguay) * New translations en.yml (Catalan, Algherese) * New translations en.yml (Basque) * New translations en.yml (Korean) * New translations en.yml (Vietnamese) * New translations en.yml (Persian) * New translations en.yml (Maltese) --- decidim-accountability/config/locales/eu.yml | 2 +- decidim-accountability/config/locales/ja.yml | 1 + .../config/locales/tr-TR.yml | 56 ++++++++++++ decidim-admin/config/locales/ca-IT.yml | 3 + decidim-admin/config/locales/ca.yml | 3 + decidim-admin/config/locales/cs.yml | 16 ++++ decidim-admin/config/locales/de.yml | 5 ++ decidim-admin/config/locales/es-MX.yml | 3 + decidim-admin/config/locales/es-PY.yml | 3 + decidim-admin/config/locales/es.yml | 3 + decidim-admin/config/locales/eu.yml | 3 + decidim-admin/config/locales/fi-plain.yml | 6 ++ decidim-admin/config/locales/fi.yml | 6 ++ decidim-admin/config/locales/fr-CA.yml | 3 + decidim-admin/config/locales/fr.yml | 3 + decidim-admin/config/locales/ja.yml | 6 ++ decidim-admin/config/locales/sv.yml | 3 + decidim-admin/config/locales/tr-TR.yml | 85 +++++++++++++++++++ decidim-assemblies/config/locales/cs.yml | 1 + .../config/locales/fi-plain.yml | 1 + decidim-assemblies/config/locales/fi.yml | 1 + decidim-assemblies/config/locales/ja.yml | 1 + decidim-blogs/config/locales/cs.yml | 1 + decidim-blogs/config/locales/ja.yml | 1 + decidim-budgets/config/locales/cs.yml | 2 + decidim-budgets/config/locales/de.yml | 4 +- decidim-budgets/config/locales/it.yml | 2 + decidim-budgets/config/locales/ja.yml | 1 + .../config/locales/fi-plain.yml | 1 + decidim-conferences/config/locales/fi.yml | 1 + decidim-conferences/config/locales/ja.yml | 1 + decidim-core/config/locales/ca-IT.yml | 2 + decidim-core/config/locales/ca.yml | 2 + decidim-core/config/locales/cs.yml | 10 +++ decidim-core/config/locales/es-MX.yml | 2 + decidim-core/config/locales/es-PY.yml | 2 + decidim-core/config/locales/es.yml | 2 + decidim-core/config/locales/eu.yml | 35 ++++---- decidim-core/config/locales/fa-IR.yml | 2 + decidim-core/config/locales/fi-plain.yml | 20 +++++ decidim-core/config/locales/fi.yml | 20 +++++ decidim-core/config/locales/fr-CA.yml | 2 + decidim-core/config/locales/fr.yml | 2 + decidim-core/config/locales/ja.yml | 18 ++++ decidim-core/config/locales/ko.yml | 2 + decidim-core/config/locales/mt.yml | 2 + decidim-core/config/locales/sv.yml | 2 + decidim-core/config/locales/vi.yml | 2 + decidim-debates/config/locales/ja.yml | 1 + decidim-elections/config/locales/eu.yml | 10 +++ decidim-elections/config/locales/fi-plain.yml | 10 +++ decidim-elections/config/locales/fi.yml | 10 +++ decidim-elections/config/locales/ja.yml | 15 ++++ decidim-elections/config/locales/sv.yml | 10 +++ decidim-initiatives/config/locales/ja.yml | 3 + decidim-meetings/config/locales/cs.yml | 2 + decidim-meetings/config/locales/ja.yml | 1 + .../config/locales/fi-plain.yml | 1 + .../config/locales/fi.yml | 1 + .../config/locales/ja.yml | 1 + decidim-proposals/config/locales/de.yml | 4 +- decidim-proposals/config/locales/eu.yml | 4 +- decidim-surveys/config/locales/ja.yml | 11 +++ .../config/locales/fi-plain.yml | 2 +- decidim-verifications/config/locales/fi.yml | 2 +- 65 files changed, 417 insertions(+), 26 deletions(-) diff --git a/decidim-accountability/config/locales/eu.yml b/decidim-accountability/config/locales/eu.yml index ce4b3076b7fc3..6cb8ecdf900b6 100644 --- a/decidim-accountability/config/locales/eu.yml +++ b/decidim-accountability/config/locales/eu.yml @@ -270,7 +270,7 @@ eu: one: Emaitza 1 other: "%{count} emaitza" home_header: - global_status: Exekuzio-egoera orokorra + global_status: Helburuen betetze-maila milestones: title: Mugarriak no_results: Ez dago proiekturik diff --git a/decidim-accountability/config/locales/ja.yml b/decidim-accountability/config/locales/ja.yml index ad00838eb53c7..67af1d5a694b7 100644 --- a/decidim-accountability/config/locales/ja.yml +++ b/decidim-accountability/config/locales/ja.yml @@ -287,6 +287,7 @@ ja: accountability: actions: comment: コメント + vote_comment: コメントに投票 name: アカウンタビリティ settings: global: diff --git a/decidim-accountability/config/locales/tr-TR.yml b/decidim-accountability/config/locales/tr-TR.yml index 6f722b4ce4d98..129985b4ef9ae 100644 --- a/decidim-accountability/config/locales/tr-TR.yml +++ b/decidim-accountability/config/locales/tr-TR.yml @@ -1,6 +1,10 @@ tr: activemodel: attributes: + milestone: + description: Açıklama + entry_date: Tarih + title: Başlık result: decidim_accountability_status_id: Durum decidim_category_id: Kategori @@ -36,6 +40,7 @@ tr: decidim: accountability: actions: + add_milestone: Aşama ekle confirm_delete_result: Bu sonucu silmek istediğinizden emin misiniz? confirm_destroy: Bu %{name}silmek istediğinize emin misiniz? deleted_results_info: Silinen sonuçlar çöp kutusundan geri yüklenebilir. @@ -43,6 +48,7 @@ tr: edit: Düzenle import: Sonuçları başka bir bileşenden içe aktar import_csv: Sonuçları CSV dosyasından içe aktarın + new_milestone: Yeni aşama new_result: Yeni sonuç new_status: Yeni Durum preview: Ön izleme @@ -83,6 +89,20 @@ tr: create: invalid: Sonuçlar içe aktarılırken bir sorun oluştu. success: Dosya aktarımı başladı. Birkaç dakika içinde içe aktarma işleminin sonucunu içeren bir e-posta alacaksınız. + milestones: + create: + invalid: Bu aşama oluşturulurken bir hata oluştu. + success: Aşama başarıyla oluşturuldu. + destroy: + success: Aşama başarıyla silindi. + edit: + title: Aşamayı düzenle + update: Aşamayı güncelle + new: + create: Aşama oluştur + update: + invalid: Bu aşama güncellenirken bir hata oluştu. + success: Aşama başarıyla güncellendi. models: result: name: Sonuç @@ -133,6 +153,7 @@ tr: invalid: Sonuçlar %{results} için sınıflandırmalar %{taxonomies} seçilemiyor select_a_result: Bir sonuç seç select_a_taxonomy: Bir sınıflandırma seç + success: '%{results} için sınıflandırmalar %{taxonomies} başarıyla güncellendi' shared: subnav: statuses: durumlar @@ -157,6 +178,7 @@ tr: result: create: "%{user_name} sonuç yaratmıştır %{resource_name} içinde %{space_name}" delete: "%{user_name} %{resource_name} sonuçtan %{space_name}sildi" + restore: "%{user_name}, %{space_name}'deki %{resource_name} sonucunu geri yükledi" update: "%{user_name} güncellenen sonuç %{resource_name} in %{space_name}" status: create: "48 / 5.000\nÇeviri sonuçları\nÇeviri sonucu\n%{user_name}, %{resource_name} kaydını oluşturdu" @@ -168,6 +190,8 @@ tr: content_blocks: highlighted_results: results: Sonuç + creation: + text: Bu sonuç eklendi import_mailer: import: errors: Hatalar @@ -177,8 +201,18 @@ tr: success: Sonuçların içe aktarılması başarılı. Sonuçları yönetim arayüzünde inceleyebilirsiniz. import_projects_mailer: import: + added_projects: + one: Projelerden bir sonuç içe aktarıldı. + other: "%{count} sonuç projelerden içe aktarıldı." subject: Projeler başarıyla aktarılmıştır success: '%{component_name} bileşenindekiprojeler başarıyla aktarılmıştır. Sonuçları yönetim arayüzünde inceleyebilirsiniz.' + import_proposals_mailer: + import: + added_proposals: + one: Tekliflerden bir sonuç içe aktarıldı + other: "%{count} sonuç tekliflerden içe aktarıldı." + subject: Tekliflerin içe aktarımı başarılı + success: Teklifler %{component_name} bileşenindeki sonuçlara başarıyla aktarılmıştır. Sonuçları yönetim arayüzünde inceleyebilirsiniz. last_activity: new_result: 'Yeni sonuç:' models: @@ -203,6 +237,8 @@ tr: other: "%{count} sonuç" home_header: global_status: Genel yürütme durumu + milestones: + title: Aşamalar no_results: Proje Bulunamadı search: search: İşlemleri ara @@ -214,19 +250,32 @@ tr: results: status_id_eq: label: Durum + tooltips: + deleted_results_info: Bu sonuç silinemiyor components: accountability: actions: comment: Yorum + vote_comment: Yorumu oyla name: Sorumluluk settings: global: + clear_all: Tümünü sil comments_enabled: Yorumlar etkin comments_max_length: Maksimum yorum uzunluğu (Varsayılan değer için 0 bırakın) + default_taxonomy: Varsayılan sınıflandırma + default_taxonomy_help: Varsayılan olarak hangi sınıflandırmayı göstermek istediğinizi seçin. Eğer herhangi bir sınıflandırma seçilmezse, sonuçlar liste biçiminde gösterilecektir. + define_taxonomy_filters: Bu ayarı kullanmadan önce lütfen bu katılımcı alan için bazı filtreler tanımlayın. display_progress_enabled: İlerlemeyi göster intro: Tanıtım + no_taxonomy_filters_found: Sınıflandırma filtresi bulunamadı. + taxonomy_filters_add: Filtre ekle step: comments_blocked: Yorumlar engellendi + download_your_data: + show: + result_comments: Sonuç yorumlarını dışa aktar + results: Sonuçları içe aktar events: accountability: proposal_linked: @@ -241,8 +290,15 @@ tr: notification_title: '%{proposal_title} teklifini içeren %{resource_title} sonucu: %{progress} tamamlandı.' open_data: help: + result_comments: + author: Bu yorumu yapan katılımcının adı + body: Yorumun kendisi + created_at: Bu yorumun oluşturulduğu tarih + root_commentable_url: Bu yoruma bağlı kaynağın URL bağlantısı results: + address: Sonucun adresi (varsa) created_at: Sonucun oluşturulma tarihi + taxonomies: participatory_spaces: highlighted_results: see_all: Tüm sonuçları gör (%{count}) diff --git a/decidim-admin/config/locales/ca-IT.yml b/decidim-admin/config/locales/ca-IT.yml index 214daf8c77306..f5e6e706dd919 100644 --- a/decidim-admin/config/locales/ca-IT.yml +++ b/decidim-admin/config/locales/ca-IT.yml @@ -125,6 +125,9 @@ ca-IT: show_in_footer: Mostra al peu de pàgina title: Títol weight: Ordre de posició + taxonomy: + item_name: Nom de l'element + parent_id: Taxonomia mare user_group_csv_verification: file: Fitxer errors: diff --git a/decidim-admin/config/locales/ca.yml b/decidim-admin/config/locales/ca.yml index 660ecbdc5bfcd..21afd22f41205 100644 --- a/decidim-admin/config/locales/ca.yml +++ b/decidim-admin/config/locales/ca.yml @@ -125,6 +125,9 @@ ca: show_in_footer: Mostra al peu de pàgina title: Títol weight: Ordre de posició + taxonomy: + item_name: Nom de l'element + parent_id: Taxonomia mare user_group_csv_verification: file: Fitxer errors: diff --git a/decidim-admin/config/locales/cs.yml b/decidim-admin/config/locales/cs.yml index a5966825647b8..b0103ff991807 100644 --- a/decidim-admin/config/locales/cs.yml +++ b/decidim-admin/config/locales/cs.yml @@ -35,6 +35,7 @@ cs: id: ID newsletter: body: Tělo + send_to_all_users: Poslat všem účastníkům send_to_followers: Odeslat sledujícím send_to_participants: Odeslat účastníkům subject: Předmět @@ -292,11 +293,13 @@ cs: block_user: bulk_new: action: Zablokovat účty a odeslat zdůvodnění + already_reported_html: Pokračováním v této akci také skryjete veškerý obsah účastníků. description: Blokováním uživatele bude jejich účet nepoužitelný. Ve svém zdůvodnění můžete uvést veškerá pravidla pro způsob, jakým byste uvažovali o odblokování uživatele. justification: Odůvodnění title: Zablokovat uživatele new: action: Blokovat účet a odeslat odůvodnění + already_reported_html: Pokračováním v této akci také skryjete veškerý obsah účastníků. description: Blokováním uživatele bude jejich účet nepoužitelný. Ve svém zdůvodnění můžete uvést veškerá pravidla pro způsob, jakým byste uvažovali o odblokování uživatele. justification: Odůvodnění title: Blokovat uživatele %{name} @@ -392,6 +395,7 @@ cs: form: domain_too_short: Doména je příliš krátká update: + error: Nepodařilo se aktualizovat seznam povolených externích domén. success: Seznam povolených externích domén byl úspěšně aktualizován. exports: export_as: "%{name} jako %{export_format}" @@ -454,6 +458,9 @@ cs: search_placeholder: name_or_nickname_or_email_cont: Hledat %{collection} podle e-mailu, jména nebo přezdívky report_count_eq: Počet nahlášení + reported_id_string_or_reported_content_cont: Hledat %{collection} podle nahlášeného Id nebo obsahu + title_cont: Hledat %{collection} podle názvu + user_name_or_user_nickname_or_user_email_cont: Hledat %{collection} podle e-mailu, jména nebo přezdívky state_eq: label: Stav values: @@ -473,6 +480,7 @@ cs: import_csv: explanation: 'Pokyny pro soubor:' message_1: CSV soubory jsou podporovány + message_2: ".csv soubor s daty e-mailu" help_sections: error: Při aktualizaci sekcí nápovědy došlo k chybě. form: @@ -559,6 +567,7 @@ cs: logs: filters: participatory_space: Najít participační prostor + text: Hledat podle e-mailu, jména nebo přezdívky user: Uživatel logs_list: no_logs_yet: Zatím nejsou žádné logy. @@ -690,6 +699,7 @@ cs: update_moderated_user_button: Zrušit nahlášení uživatelů title: Akce unblock: Odblokovat uživatele + unreport: Zpět hlášení cancel: Zrušit name: Jméno nickname: Přezdívka @@ -716,6 +726,7 @@ cs: title: Vrátit skrytí update_moderation_button: Odkrýt vybrané zdroje unreport: + title: Zpět hlášení update_moderation_button: Zrušit nahlášení vybraných zdrojů cancel: Zrušit selected: vybráno @@ -1242,6 +1253,9 @@ cs: taxonomy_filters: Filtry taxonomie pro "%{taxonomy}" users: Administrátoři tooltips: + cannot_destroy_taxonomy_filter: Nelze zničit tento filtr taxonomie + cannot_edit_taxonomy_filter: Nelze upravit tento filtr taxonomie + deleted_attachment_collections_info: Tuto složku nelze odstranit, protože obsahuje přílohy. deleted_component_info: Tato komponenta může být odstraněna pouze v případě, že je stav 'Nezveřejněno'. trash_management: restore: @@ -1270,6 +1284,7 @@ cs: last_day: Poslední den last_month: Minulý měsíc last_week: Minulý týden + no_users_count_statistics_yet: Zatím nejsou žádné statistiky počtu účastníků. participants: Účastníci forms: errors: @@ -1284,6 +1299,7 @@ cs: parent_hidden: Nelze odkrýt tento zdroj, protože jeho nadřazená položka je stále skrytá. title: Akce unhide: Vrátit skrytí + unreport: Zpět hlášení admin: reportable: bulk_action: diff --git a/decidim-admin/config/locales/de.yml b/decidim-admin/config/locales/de.yml index aec907a9c4969..7c398e3ab3f97 100644 --- a/decidim-admin/config/locales/de.yml +++ b/decidim-admin/config/locales/de.yml @@ -35,6 +35,7 @@ de: id: ID newsletter: body: Haupttext + send_to_all_users: An alle Teilnehmende senden send_to_followers: An Follower senden send_to_participants: An Teilnehmende senden subject: Betreff @@ -292,11 +293,13 @@ de: block_user: bulk_new: action: Konten sperren und Begründung senden + already_reported_html: Wenn Sie diese Aktion fortsetzen, werden Sie auch alle Inhalte der Teilnehmenden ausblenden. description: Das Blockieren eines Teilnehmenden wird das verknüpfte Konto unbrauchbar machen. Sie können dies begründen und Richtlinien dafür bieten, wie der Teilnehmende vorgehen kann, damit das Konto wieder entsperrt wird. justification: Begründung title: Teilnehmende blockieren new: action: Konto sperren und Begründung senden + already_reported_html: Wenn Sie diese Aktion fortsetzen, werden Sie auch alle Inhalte der Teilnehmenden ausblenden. description: Das Blockieren eines Benutzers wird sein Konto unbrauchbar machen. Sie können begründen und Richtlinien dafür bieten, wie der Benutzer vorgehen könnte, damit Sie in Betracht ziehen, die Blockierung wieder aufzuheben. justification: Begründung title: Benutzer %{name} blockieren @@ -390,6 +393,7 @@ de: form: domain_too_short: Domain zu kurz update: + error: Das Aktualisieren der Liste der zulässigen externen Domains ist fehlgeschlagen. success: Die Liste der zulässigen externen Domains wurde erfolgreich aktualisiert. exports: export_as: "%{name} als %{export_format}" @@ -474,6 +478,7 @@ de: import_csv: explanation: 'Hinweise für die Datei:' message_1: CSV-Dateien werden unterstützt + message_2: "CSV-Datei mit E-Mail-Daten" help_sections: error: Beim Aktualisieren der Hilfeabschnitte ist ein Fehler aufgetreten. form: diff --git a/decidim-admin/config/locales/es-MX.yml b/decidim-admin/config/locales/es-MX.yml index bf9df930afd23..67926595a9eb8 100644 --- a/decidim-admin/config/locales/es-MX.yml +++ b/decidim-admin/config/locales/es-MX.yml @@ -125,6 +125,9 @@ es-MX: show_in_footer: Mostrar en el pie de página title: Título weight: Orden de posición + taxonomy: + item_name: Nombre del elemento + parent_id: Taxonomía madre user_group_csv_verification: file: Expediente errors: diff --git a/decidim-admin/config/locales/es-PY.yml b/decidim-admin/config/locales/es-PY.yml index a600e0380bba1..efec79c30f92b 100644 --- a/decidim-admin/config/locales/es-PY.yml +++ b/decidim-admin/config/locales/es-PY.yml @@ -125,6 +125,9 @@ es-PY: show_in_footer: Mostrar en el pie de página title: Título weight: Orden de posición + taxonomy: + item_name: Nombre del elemento + parent_id: Taxonomía madre user_group_csv_verification: file: Expediente errors: diff --git a/decidim-admin/config/locales/es.yml b/decidim-admin/config/locales/es.yml index fc007682585c5..d5dc956249129 100644 --- a/decidim-admin/config/locales/es.yml +++ b/decidim-admin/config/locales/es.yml @@ -125,6 +125,9 @@ es: show_in_footer: Mostrar en el pie de página title: Título weight: Orden de posición + taxonomy: + item_name: Nombre del elemento + parent_id: Taxonomía madre user_group_csv_verification: file: Archivo errors: diff --git a/decidim-admin/config/locales/eu.yml b/decidim-admin/config/locales/eu.yml index 8bb5f29f864c1..4d9201be4b03a 100644 --- a/decidim-admin/config/locales/eu.yml +++ b/decidim-admin/config/locales/eu.yml @@ -125,6 +125,9 @@ eu: show_in_footer: Erakutsi orri-oinean title: Izenburua weight: Kokapenaren hurrenkera + taxonomy: + item_name: Elementuaren izena + parent_id: Nagusia user_group_csv_verification: file: Fitxategia errors: diff --git a/decidim-admin/config/locales/fi-plain.yml b/decidim-admin/config/locales/fi-plain.yml index ad15d7a023ba4..0441ead0dd5af 100644 --- a/decidim-admin/config/locales/fi-plain.yml +++ b/decidim-admin/config/locales/fi-plain.yml @@ -35,6 +35,7 @@ fi-pl: id: ID newsletter: body: Runko + send_to_all_users: Lähetä kaikille osallistujille send_to_followers: Lähetä seuraajille send_to_participants: Lähetä osallistujille subject: Otsikko @@ -292,11 +293,13 @@ fi-pl: block_user: bulk_new: action: Estä tilit ja lähetä perustelut + already_reported_html: Tämä toiminto piilottaa myös kaiken sisällön, jonka kyseinen osallistuja on luonut. description: Käyttäjätilien estäminen estää kyseisten käyttäjien pääsyn omalle tililleen. Voit antaa perusteluissasi ja ohjeistuksissasi tietoa siitä, mitä käyttäjät voivat tehdä palauttaakseen pääsyn tileilleen. justification: Perustelut title: Estä käyttäjät new: action: Estä tili ja lähetä perustelut + already_reported_html: Tämä toiminto piilottaa myös kaiken sisällön, jonka kyseinen osallistuja on luonut. description: Estämällä käyttäjän estät kyseisen käyttäjän pääsyn hänen omalle tililleen. Voit antaa perusteluissasi ja ohjeistuksissasi tietoa siitä, mitä kyseinen käyttäjä voi tehdä palauttaakseen pääsyn tililleen. justification: Perustelut title: Estä käyttäjä %{name} @@ -390,6 +393,7 @@ fi-pl: form: domain_too_short: Verkkotunnus on liian lyhyt update: + error: Sallittujen ulkoisten verkkotunnusten luettelon päivittäminen epäonnistui. success: Sallittujen ulkoisten verkkotunnusten luettelon päivittäminen onnistui. exports: export_as: "%{name} muodossa %{export_format}" @@ -474,6 +478,7 @@ fi-pl: import_csv: explanation: 'Tiedoston ohjeistus:' message_1: CSV-tiedostomuotoa tuetaan + message_2: "sähköpostiosoitteet sisältävä .csv-tiedosto" help_sections: error: Ohjeosioiden päivitys epäonnistui. form: @@ -1265,6 +1270,7 @@ fi-pl: last_day: Viimeinen päivä last_month: Viimeinen kuukausi last_week: Viimeinen viikko + no_users_count_statistics_yet: Käyttäjämäärätilastoja ei vielä ole. participants: Osallistujat forms: errors: diff --git a/decidim-admin/config/locales/fi.yml b/decidim-admin/config/locales/fi.yml index c23852d5a88de..d9f5cae7357b9 100644 --- a/decidim-admin/config/locales/fi.yml +++ b/decidim-admin/config/locales/fi.yml @@ -35,6 +35,7 @@ fi: id: ID newsletter: body: Runko + send_to_all_users: Lähetä kaikille osallistujille send_to_followers: Lähetä seuraajille send_to_participants: Lähetä osallistujille subject: Otsikko @@ -292,11 +293,13 @@ fi: block_user: bulk_new: action: Estä tilit ja lähetä perustelut + already_reported_html: Tämä toiminto piilottaa myös kaiken sisällön, jonka kyseinen osallistuja on luonut. description: Käyttäjätilien estäminen estää kyseisten käyttäjien pääsyn omalle tililleen. Voit antaa perusteluissasi ja ohjeistuksissasi tietoa siitä, mitä käyttäjät voivat tehdä palauttaakseen pääsyn tileilleen. justification: Perustelut title: Estä käyttäjät new: action: Estä tili ja lähetä perustelut + already_reported_html: Tämä toiminto piilottaa myös kaiken sisällön, jonka kyseinen osallistuja on luonut. description: Estämällä käyttäjän estät kyseisen käyttäjän pääsyn hänen omalle tililleen. Voit antaa perusteluissasi ja ohjeistuksissasi tietoa siitä, mitä kyseinen käyttäjä voi tehdä palauttaakseen pääsyn tililleen. justification: Perustelut title: Estä käyttäjä %{name} @@ -390,6 +393,7 @@ fi: form: domain_too_short: Verkkotunnus on liian lyhyt update: + error: Sallittujen ulkoisten verkkotunnusten luettelon päivittäminen epäonnistui. success: Sallittujen ulkoisten verkkotunnusten luettelon päivittäminen onnistui. exports: export_as: "%{name} muodossa %{export_format}" @@ -474,6 +478,7 @@ fi: import_csv: explanation: 'Tiedoston ohjeistus:' message_1: CSV-tiedostomuotoa tuetaan + message_2: "sähköpostiosoitteet sisältävä .csv-tiedosto" help_sections: error: Ohjeosioiden päivitys epäonnistui. form: @@ -1265,6 +1270,7 @@ fi: last_day: Viimeinen päivä last_month: Viimeinen kuukausi last_week: Viimeinen viikko + no_users_count_statistics_yet: Käyttäjämäärätilastoja ei vielä ole. participants: Osallistujat forms: errors: diff --git a/decidim-admin/config/locales/fr-CA.yml b/decidim-admin/config/locales/fr-CA.yml index 1116d7962e3be..f54972134ee6a 100644 --- a/decidim-admin/config/locales/fr-CA.yml +++ b/decidim-admin/config/locales/fr-CA.yml @@ -124,6 +124,9 @@ fr-CA: show_in_footer: Montrer dans le pied de page title: Titre weight: Rang + taxonomy: + item_name: Nom de l’élément + parent_id: Parent user_group_csv_verification: file: Fichier errors: diff --git a/decidim-admin/config/locales/fr.yml b/decidim-admin/config/locales/fr.yml index 45583581c9155..61845fa999563 100644 --- a/decidim-admin/config/locales/fr.yml +++ b/decidim-admin/config/locales/fr.yml @@ -124,6 +124,9 @@ fr: show_in_footer: Montrer dans le pied de page title: Titre weight: Rang d'affichage + taxonomy: + item_name: Nom de l’élément + parent_id: Parent user_group_csv_verification: file: Fichier errors: diff --git a/decidim-admin/config/locales/ja.yml b/decidim-admin/config/locales/ja.yml index 4b8dcfef4ca01..31285675954b2 100644 --- a/decidim-admin/config/locales/ja.yml +++ b/decidim-admin/config/locales/ja.yml @@ -35,6 +35,7 @@ ja: id: ID newsletter: body: 本文 + send_to_all_users: 全参加者に送信 send_to_followers: フォロワーに送信 send_to_participants: 参加者に送信 subject: 件名 @@ -292,11 +293,13 @@ ja: block_user: bulk_new: action: アカウントをブロックして理由を送信 + already_reported_html: この操作を続行すると、参加者のコンテンツもすべて非表示になります。 description: ユーザーをブロックすると、そのアカウントは利用できなくなります。ユーザーのブロックを解除することを検討する方法に関するガイドラインを判定通知に含めることができます。 justification: 判定理由 title: ユーザーをブロック new: action: アカウントをブロックして理由を送信 + already_reported_html: この操作を続行すると、参加者のコンテンツもすべて非表示になります。 description: ユーザーをブロックすると、そのアカウントは利用できなくなります。ユーザーのブロックを解除することを検討する方法に関するガイドラインを判定通知に含めることができます。 justification: 判定理由 title: ユーザー %{name} をブロックする @@ -389,6 +392,7 @@ ja: form: domain_too_short: ドメインが短すぎます update: + error: 許可された外部ドメインのリストを更新できませんでした。 success: 許可された外部ドメインのリストを更新しました。 exports: export_as: "%{name} を %{export_format} 形式で取得" @@ -473,6 +477,7 @@ ja: import_csv: explanation: 'ファイルのガイダンス:' message_1: CSVファイルがサポートされています + message_2: "電子メールデータを含む.csvファイル" help_sections: error: ヘルプセクションの更新中に問題が発生しました。 form: @@ -1257,6 +1262,7 @@ ja: last_day: 直近24時間 last_month: 直近1ヶ月間 last_week: 直近1週間 + no_users_count_statistics_yet: 参加者数の統計データはまだありません。 participants: 参加者 forms: errors: diff --git a/decidim-admin/config/locales/sv.yml b/decidim-admin/config/locales/sv.yml index 69100c45997e5..1354f28b7297b 100644 --- a/decidim-admin/config/locales/sv.yml +++ b/decidim-admin/config/locales/sv.yml @@ -124,6 +124,9 @@ sv: show_in_footer: Visa i sidfoten title: Titel weight: Position i listan + taxonomy: + item_name: Objektnamn + parent_id: Överordnad user_group_csv_verification: file: Fil errors: diff --git a/decidim-admin/config/locales/tr-TR.yml b/decidim-admin/config/locales/tr-TR.yml index 70d22c1a67775..2a0f1186da356 100644 --- a/decidim-admin/config/locales/tr-TR.yml +++ b/decidim-admin/config/locales/tr-TR.yml @@ -35,6 +35,7 @@ tr: id: İD newsletter: body: Vücut + send_to_all_users: Bütün katılımcılara gönder send_to_followers: Takipçilere gönder send_to_participants: Katılımcılara gönder subject: konu @@ -165,6 +166,7 @@ tr: decidim: admin: actions: + actions_label: '%{resource} için işlemler' add: Eklemek attachment: new: Yeni ek @@ -181,6 +183,7 @@ tr: Fikrinizi değiştirirseniz daha sonra yeniden yükleyebilirsiniz. export-selection: Seçimi dışa aktar import: İçe aktar + menu_hidden: Menüden gizle newsletter: new: Yeni bülten participatory_space_private_user: @@ -281,6 +284,7 @@ tr: block_user: bulk_new: action: Hesapları engelle ve gerekçe gönder. + already_reported_html: Bu işleme devam ederek katılımcının tüm içeriklerini de gizleyeceksiniz. description: Bir kullanıcıyı engellemek, kullanıcının hesabını kullanılamaz hale getirir. Gerekçenizde engeli kaldırmak için gereken yönergeleri ekleyebilirsiniz. justification: Gerekçe title: Kullanıcıları engelle @@ -300,6 +304,7 @@ tr: create: error: Bu bileşeni oluştururken bir hata oluştu. success: Bileşen başarıyla oluşturuldu. + success_landing_page: Bileşen başarıyla oluşturuldu. Alanın ana sayfasına bu bileşen için bir içerik engeli ekleyebilirsiniz. Yapılandırmak için Ana sayfaya gidiniz. edit: title: Bileşeni düzenle update: Güncelleştirme @@ -375,6 +380,7 @@ tr: form: domain_too_short: Alan adı çok kısa update: + error: İzin verilen harici alan adı listesi güncellenemedi. success: İzin verilen harici alan adı listesi başarıyla güncellendi. exports: export_as: "%{name} olarak %{export_format}" @@ -433,6 +439,9 @@ tr: 'true': Yayından kaldırıldı remove_all: Tümünü kaldır search_label: Arama + search_placeholder: + name_or_nickname_or_email_cont: '%{collection}''da e-posta, isim veya takma ad ile ara' + user_name_or_user_nickname_or_user_email_cont: '%{collection}''da e-posta, isim veya takma ad ile ara' state_eq: label: Durum values: @@ -452,6 +461,7 @@ tr: import_csv: explanation: 'Dosya kılavuzu:' message_1: CSV dosyaları desteklenmektedir. + message_2: "e-posta verileri içeren .csv dosyası" help_sections: error: Yardım bölümleri güncellenirken bir hata oluştu. form: @@ -506,6 +516,7 @@ tr: records: detail: Lütfen satırların doğru biçimlendirildiğinden ve geçerli kayıtlar içerdiğinden emin olun. missing_headers: + detail: Lütfen dosyanın istenen sütunlara sahip olduğundan emin olunuz. message: one: Eksik sütun %{columns}. other: Eksik sütunlar %{columns}. @@ -518,12 +529,14 @@ tr: actions: back: Geri download_example: Örneği indir + download_example_format: file_legend: Ayrıştırılmak üzere bir içe aktarma dosyası ekleyin. import: İçe aktar notice: "%{count} %{resource_name} başarıyla içe aktarıldı." logs: filters: participatory_space: Katılımcı alanı bul + text: Kullanıcı e-postası, isim veya kullanıcı adı ile ara user: Kullanıcı logs_list: no_logs_yet: Henüz kayıt yok. @@ -550,6 +563,7 @@ tr: content: Şikayet edilmiş içerik external_domain_allowlist: İzin verilen harici alan adları help_sections: Yardım bölümleri + homepage: Ana sayfa düzeni impersonations: impersonations manage: yönetme moderation: Küresel denetimler @@ -683,6 +697,10 @@ tr: report_language: Rapor dili report_reason: Nedeni title: Rapor ayrıntıları + new_import: + accepted_mime_types: + json: JSON + xlsx: XLSX newsletter_templates: index: preview_template: Ön izleme @@ -692,6 +710,8 @@ tr: preview: 'Şablon özizlemesi: %{template_name}' use_template: Bu şablonu kullan newsletters: + confirm_recipients: + title: Alıcıları doğrula create: error: Bu bülteni oluştururken bir hata oluştu. deliver: @@ -726,6 +746,7 @@ tr: followers_help: Listede seçili herhangi bir katılımcı süreci takip eden bütün onaylanmış kullanıcılara haber bülteni gönderir. participants_help: Listede seçili herhangi bir katılımcı sürece katılmış olan bütün onaylanmış kullanıcılara haber bülteni gönderir. recipients_count: Bu haber bülteni %{count} kullanıcıya gönderilecektir. + select_participatory_processes: Katılımcı süreçleri seç select_spaces: Haber bültenini bölmek için alanları seçin send_to_all_users: Tüm kullanıcılara gönder title: Teslim etmek için alıcıları seçiniz @@ -768,6 +789,7 @@ tr: officialized: resmileştirilmiş reofficialize: Reofficialize reports: Raporlar + send_message: Mesaj Gönder status: durum unofficialize: Unofficialize new: @@ -789,9 +811,17 @@ tr: title: Kuruluş düzenle update: Güncelleştirme form: + admin_terms_of_service: Yönetici kullanım koşulları + colors: + colors_title: Organizasyon renkleri + colors_warning_html: Uyarı! Bu renkleri değiştirmek renk kontrastını bozabilir. Seçtiğiniz kontrastı veya diğer benzer araçları WebAIM Contrast Checker'dan kontrol edebilirsiniz. + explanation: Bu araç organizasyonun web sitesinde kullanılacak renk paletinde eşit aralıklarla yerleştirilmiş tonlardan oluşan bir renk düzeni seçmenize yardımcı olur. + extra_features: İlave özellikler facebook: Facebook github: GitHub instagram: Instagram + logos: + organization_logos: Organizasyon logoları rich_text_editor_in_public_views_help: Bazı metin alanlarında, katılımcılar zengin metin düzenleyicisini kullanarak HTML etiketleri ekleyebilecekler. social_handlers: Sosyal twitter: x @@ -829,11 +859,18 @@ tr: new: create: yaratmak title: Yeni Katılımcı Uzay özel kullanıcısı. + publish_all: + success: Bu katılımcı alan için özel katılımcılar başarıyla yayınlandı participatory_space_private_users_csv_imports: + create: + success: CSV dosyası başarıyla yüklendi, katılımcılara bir davet e-postası gönderiyoruz. Bu biraz zaman alabilir. new: csv_upload: title: CSV dosyanızı yükleyin destroy: + button: Tüm özel kullanıcıları sil + confirm: Tüm özel kullanıcıları silmek istediğinizden emin misiniz? Bu işlem geri alınamaz, silinen katılımcıları geri yükleyemeyeceksiniz. + empty: Özel kullanıcınız yok. title: Özel Kullanıcıları kaldır example_file: 'Örnek dosya:' upload: Yükle @@ -877,10 +914,14 @@ tr: success: Kapsam başarıyla güncellendi share_tokens: actions: + confirm_destroy: Bu erişimi silmek istediğinizden emin misiniz? destroy: Sil edit: Düzenle preview: Ön İzleme + create: + invalid: Erişim linki üretilirken bir hata oluştu. edit: + title: '%{name} için yeni erişim linki' update: Güncelle form: automatic: Otomatik @@ -893,6 +934,19 @@ tr: 'true': 'Evet' index: copied: Erişim bağlantısı kopyalandı! + copy_message: Metin başarıyla panoya kopyalandı. + create_new_token: İlk erişim linkini oluştur! + empty_html: Şu an aktif olan bir erişim linki yok. %{new_token_link} + never: Asla + new_share_token_button: Yeni erişim linki + share_tokens_help_html: | + Diğerlerinin bu yayınlanmamış kaynağı görüntüleyebilmesi için bir erişim linki oluşturun ve paylaşın. + Erişim linkleri sadece kayıtlı kullanıcılar için geçerli olabilir ve gerekirse bir son kullanım tarihi belirlenebilir. + Yeni bir erişim linki paylaşmak için, oluşturduktan sonra "%{clipboard} ikonunu kullanarak linki kopyalayın. + title: '%{name} için erişim linkleri' + new: + create: Oluştur + title: '%{name} için yeni erişim linki' shared: gallery: add_images: Resim ekle @@ -925,9 +979,34 @@ tr: new: title: Yeni sayfa topic: + empty: Bu konu altında hiçbir sayfa yok. without_topic: Konu olmayan sayfalar update: error: Bu sayfa güncellenirken bir hata oluştu. + success: Sayfa başarıyla güncellendi. + taxonomies: + destroy: + invalid: Bu sınıflandırmayı kaldırırken bir hata oluştu. + success: Sınıflandırma başarıyla kaldırıldı. + edit: + description: Bu kategorideki ögeler katılımcı alanları veya bileşenler gibi kaynakları sınıflandırmak veya filtrelemek için kullanılacaktır. Listeyi yeniden düzenlemek için ögeleri sürükleyerek bırakınız. + new: + title: Yeni kategori + update: + success: Kategori başarıyla güncellendi. + taxonomy_filters: + create: + error: Bu kategori filtresi oluşturulurken bir hata oluştu. + destroy: + success: Kategori filtresi başarıyla kaldırıldı. + form: + no_items: Bu kategori için herhangi bir öge mevcut değil. Kategori içindeki yapılandırma ayarlarından ögeler oluşturabilirsiniz. + table: + components_count: Bunu kullanan bileşenler + internal_name: + taxonomy_filters_selector: + new: + save: titles: admin_log: Yönetici günlüğü area_types: Alan türleri @@ -941,6 +1020,10 @@ tr: participants: Kullanıcılar scope_types: Kapsam türleri scopes: kapsamları + tooltips: + cannot_edit_taxonomy_filter: Bu kategori filtresi düzenlenemiyor + deleted_attachment_collections_info: Bu klasör ek dosyalar içerdiğinden silinemiyor. + deleted_component_info: Bu bileşen sadece durumu 'Yayınlanmamış' ise silinebilir. users: create: error: Bu kullanıcıyı davet ederken bir hata oluştu. @@ -954,12 +1037,14 @@ tr: role: Rol new: create: Davet et + title: Yeni yönetici davet et users_statistics: users_count: admins: Yöneticiler last_day: Son gün last_month: Geçen ay last_week: Geçen hafta + no_users_count_statistics_yet: Henüz katılımcı sayısı istatistiği yok. participants: Katılımcılar moderations: actions: diff --git a/decidim-assemblies/config/locales/cs.yml b/decidim-assemblies/config/locales/cs.yml index 9888a825e1b3c..ed44e3e574850 100644 --- a/decidim-assemblies/config/locales/cs.yml +++ b/decidim-assemblies/config/locales/cs.yml @@ -390,6 +390,7 @@ cs: assembly_type: Typ shromáždění closing_date: Datum ukončení shromáždění closing_date_reason: Proč bylo shromáždění uzavřeno + component_settings: Nastavení komponenty shromáždění composition: Složení shromáždění created_at: Datum, kdy byl tento prostor vytvořen created_by: Kdo vytvořil toto shromáždění diff --git a/decidim-assemblies/config/locales/fi-plain.yml b/decidim-assemblies/config/locales/fi-plain.yml index 6daad0218b102..3be39c3507b22 100644 --- a/decidim-assemblies/config/locales/fi-plain.yml +++ b/decidim-assemblies/config/locales/fi-plain.yml @@ -384,6 +384,7 @@ fi-pl: assembly_type: Ryhmän tyyppi closing_date: Ryhmän sulkemispäivä closing_date_reason: Miksi tämä ryhmä suljettiin + component_settings: Ryhmän komponenttiasetukset composition: Ryhmän kokoonpano created_at: Tilan luontiaika created_by: Kuka tämän ryhmän on luonut diff --git a/decidim-assemblies/config/locales/fi.yml b/decidim-assemblies/config/locales/fi.yml index bff1b363cea02..59271c391c40f 100644 --- a/decidim-assemblies/config/locales/fi.yml +++ b/decidim-assemblies/config/locales/fi.yml @@ -384,6 +384,7 @@ fi: assembly_type: Ryhmän tyyppi closing_date: Ryhmän sulkemispäivä closing_date_reason: Miksi tämä ryhmä suljettiin + component_settings: Ryhmän komponenttiasetukset composition: Ryhmän kokoonpano created_at: Tilan luontiaika created_by: Kuka tämän ryhmän on luonut diff --git a/decidim-assemblies/config/locales/ja.yml b/decidim-assemblies/config/locales/ja.yml index e85ef4440c259..e98a732678599 100644 --- a/decidim-assemblies/config/locales/ja.yml +++ b/decidim-assemblies/config/locales/ja.yml @@ -381,6 +381,7 @@ ja: assembly_type: 参加スペースの種別 closing_date: 参加スペースの終了日 closing_date_reason: 参加スペースが終了した理由 + component_settings: 参加スペースのコンポーネント設定 composition: 参加スペースの構成 created_at: このスペースの作成日時 created_by: この参加スペースの作成者 diff --git a/decidim-blogs/config/locales/cs.yml b/decidim-blogs/config/locales/cs.yml index 8125c6a3e8400..4320b8cef0208 100644 --- a/decidim-blogs/config/locales/cs.yml +++ b/decidim-blogs/config/locales/cs.yml @@ -111,6 +111,7 @@ cs: destroy: Smazat like: Líbí se mi update: Aktualizovat + vote_comment: Komentář hlasování name: Blog settings: global: diff --git a/decidim-blogs/config/locales/ja.yml b/decidim-blogs/config/locales/ja.yml index c794a68e776e7..c2741bde08503 100644 --- a/decidim-blogs/config/locales/ja.yml +++ b/decidim-blogs/config/locales/ja.yml @@ -105,6 +105,7 @@ ja: destroy: 削除 like: いいね update: 更新 + vote_comment: コメントに投票 name: ブログ settings: global: diff --git a/decidim-budgets/config/locales/cs.yml b/decidim-budgets/config/locales/cs.yml index 49d004a5beaef..c0b116066b67f 100644 --- a/decidim-budgets/config/locales/cs.yml +++ b/decidim-budgets/config/locales/cs.yml @@ -303,6 +303,8 @@ cs: dynamic_help: minimum_reached: Dosáhli jste minima, abyste mohli hlasovat minimum: Minimum + minimum_projects_rule: + description: "Vyberte alespoň %{minimum_number} projektů, které chcete a hlasujete podle vašich preferencí." orders: highest_cost: Nejvyšší náklady label: Seřadit projekty podle diff --git a/decidim-budgets/config/locales/de.yml b/decidim-budgets/config/locales/de.yml index f8073ef1aab84..ea5a29aa6b7f5 100644 --- a/decidim-budgets/config/locales/de.yml +++ b/decidim-budgets/config/locales/de.yml @@ -307,8 +307,8 @@ de: remove: Projekt %{resource_name} aus Ihrer Abstimmung entfernen. selected: Ausgewählt votes: - one: Abstimmung - other: Abstimmungen + one: Stimme + other: Stimmen you_voted: Sie haben dafür gestimmt project_budget_button: add: Zur Abstimmung hinzufügen diff --git a/decidim-budgets/config/locales/it.yml b/decidim-budgets/config/locales/it.yml index 4f1c3bd162f0a..010a436baa194 100644 --- a/decidim-budgets/config/locales/it.yml +++ b/decidim-budgets/config/locales/it.yml @@ -100,6 +100,8 @@ it: order: status: title: OK, il tuo voto è stato acquisito. + order_pdf: + title: Ok, il tuo voto è stato acquisito. order_summary_mailer: order_summary: selected_projects: 'I progetti che hai selezionato sono:' diff --git a/decidim-budgets/config/locales/ja.yml b/decidim-budgets/config/locales/ja.yml index 3ad5109ac8a3e..f4c87059a3e9c 100644 --- a/decidim-budgets/config/locales/ja.yml +++ b/decidim-budgets/config/locales/ja.yml @@ -328,6 +328,7 @@ ja: actions: comment: コメント vote: 投票 + vote_comment: コメントに投票 name: 予算 settings: global: diff --git a/decidim-conferences/config/locales/fi-plain.yml b/decidim-conferences/config/locales/fi-plain.yml index 8fb676a94b9e0..74c65f2c96147 100644 --- a/decidim-conferences/config/locales/fi-plain.yml +++ b/decidim-conferences/config/locales/fi-plain.yml @@ -622,6 +622,7 @@ fi-pl: open_data: help: conferences: + component_settings: Konferenssin komponenttiasetukset created_at: Tilan luontiaika decidim_scope_id: Konferenssin teema description: Konferenssin pitkä kuvaus diff --git a/decidim-conferences/config/locales/fi.yml b/decidim-conferences/config/locales/fi.yml index 6f67310e6bfa9..3cfc05873cbc9 100644 --- a/decidim-conferences/config/locales/fi.yml +++ b/decidim-conferences/config/locales/fi.yml @@ -622,6 +622,7 @@ fi: open_data: help: conferences: + component_settings: Konferenssin komponenttiasetukset created_at: Tilan luontiaika decidim_scope_id: Konferenssin teema description: Konferenssin pitkä kuvaus diff --git a/decidim-conferences/config/locales/ja.yml b/decidim-conferences/config/locales/ja.yml index 64b14a53698ec..76a038fad586d 100644 --- a/decidim-conferences/config/locales/ja.yml +++ b/decidim-conferences/config/locales/ja.yml @@ -617,6 +617,7 @@ ja: open_data: help: conferences: + component_settings: カンファレンススペースのコンポーネント設定 created_at: このスペースが作成された日時 decidim_scope_id: カンファレンスのスコープ description: カンファレンスの詳しい説明 diff --git a/decidim-core/config/locales/ca-IT.yml b/decidim-core/config/locales/ca-IT.yml index 0a86bd17e725e..ac5e3960be53b 100644 --- a/decidim-core/config/locales/ca-IT.yml +++ b/decidim-core/config/locales/ca-IT.yml @@ -1291,8 +1291,10 @@ ca-IT: action_error: S'ha produït un error en actualitzar la configuració de les notificacions. no_notifications: Encara no hi ha notificacions. show: + deleted: El contingut ha estat eliminat per l'autora. missing_event: Vaja, aquesta notificació pertany a un element que ja no està disponible. Pots descartar-la. moderated: El contingut s'ha amagat correctament a través de la moderació. + not_available: Vaja, aquesta notificació pertany a un element que ja no està disponible. Pots descartar-la. notifications_digest_mailer: header: daily: Resum diari de notificacions diff --git a/decidim-core/config/locales/ca.yml b/decidim-core/config/locales/ca.yml index 74e0bb8631177..31d0d886544ea 100644 --- a/decidim-core/config/locales/ca.yml +++ b/decidim-core/config/locales/ca.yml @@ -1291,8 +1291,10 @@ ca: action_error: S'ha produït un error en actualitzar la configuració de les notificacions. no_notifications: Encara no hi ha notificacions. show: + deleted: El contingut ha estat eliminat per l'autora. missing_event: Vaja, aquesta notificació pertany a un element que ja no està disponible. Pots descartar-la. moderated: El contingut s'ha amagat correctament a través de la moderació. + not_available: Vaja, aquesta notificació pertany a un element que ja no està disponible. Pots descartar-la. notifications_digest_mailer: header: daily: Resum diari de notificacions diff --git a/decidim-core/config/locales/cs.yml b/decidim-core/config/locales/cs.yml index cae9d70d597ec..21bcbc147e5f3 100644 --- a/decidim-core/config/locales/cs.yml +++ b/decidim-core/config/locales/cs.yml @@ -1721,6 +1721,16 @@ cs: instructions_1: Pokud máte přístup k tomuto e-mailovému účtu, zkontrolujte prosím svou doručenou poštu pro pokyny. notify_deprecation_to_owner: greeting: Dobrý den %{name}, + set_password: Nastavte své heslo + subject: Důležitá aktualizace vašeho profilu skupiny + notify_user_group_patched: + body_3_html: Abychom mohli pokračovat v přístupu k vašemu účtu a sdílet přístup, potřebujeme, abyste si nastavili nový e-mail a heslo. Vezměte prosím na vědomí, že každý člen vaší skupiny obdrží tento e-mail. Chcete-li zajistit trvalý přístup, musíte se interně dohodnout na sdílených přihlašovacích údajích (e-mail a heslo), které budou všichni používat. + greeting: Dobrý den %{name}, + instructions_1_html: 'K přihlášení použijte následující dočasné přihlašovací údaje:

Uživatelské jméno: %{email}
heslo: %{password}

' + instructions_2_html: Nastavte novou e-mailovou adresu a heslo. + instructions_3_html: Sdílejte vybrané přihlašovací údaje s vašimi kolegy, aby celá skupina mohla pokračovat v přístupu k účtu. + instructions_title_html: "Co potřebujete udělat" + subject: Důležitá aktualizace vašeho profilu skupiny user_report_mailer: notify: body_1: Uživatel %{user} byl nahlášen od %{token} diff --git a/decidim-core/config/locales/es-MX.yml b/decidim-core/config/locales/es-MX.yml index ea65d73fd35ba..cefa402feb4ab 100644 --- a/decidim-core/config/locales/es-MX.yml +++ b/decidim-core/config/locales/es-MX.yml @@ -1294,8 +1294,10 @@ es-MX: action_error: Se ha producido un error al actualizar la configuración de las notificaciones. no_notifications: No hay notificaciones aún. show: + deleted: El contenido ha sido eliminado por la autora. missing_event: Vaya, esta notificación pertenece a un elemento que ya no está disponible. Puedes descartarla. moderated: El contenido se ha ocultado mediante la moderación. + not_available: Vaya, esta notificación pertenece a un elemento que ya no está disponible. Puedes descartarla. notifications_digest_mailer: header: daily: Resumen diario de notificaciones diff --git a/decidim-core/config/locales/es-PY.yml b/decidim-core/config/locales/es-PY.yml index 1f0e6651ded69..ee62da8a9c0e9 100644 --- a/decidim-core/config/locales/es-PY.yml +++ b/decidim-core/config/locales/es-PY.yml @@ -1294,8 +1294,10 @@ es-PY: action_error: Se ha producido un error al actualizar la configuración de las notificaciones. no_notifications: No hay notificaciones aún. show: + deleted: El contenido ha sido eliminado por la autora. missing_event: Vaya, esta notificación pertenece a un elemento que ya no está disponible. Puedes descartarla. moderated: El contenido se ha ocultado mediante la moderación. + not_available: Vaya, esta notificación pertenece a un elemento que ya no está disponible. Puedes descartarla. notifications_digest_mailer: header: daily: Resumen diario de notificaciones diff --git a/decidim-core/config/locales/es.yml b/decidim-core/config/locales/es.yml index 61c55696f6fae..cbc6fa42c3acd 100644 --- a/decidim-core/config/locales/es.yml +++ b/decidim-core/config/locales/es.yml @@ -1291,8 +1291,10 @@ es: action_error: Se ha producido un error al actualizar la configuración de las notificaciones. no_notifications: Aún no hay notificaciones. show: + deleted: El contenido ha sido eliminado por la autora. missing_event: Vaya, esta notificación pertenece a un elemento que ya no está disponible. Puedes descartarla. moderated: El contenido se ha ocultado mediante la moderación. + not_available: Vaya, esta notificación pertenece a un elemento que ya no está disponible. Puedes descartarla. notifications_digest_mailer: header: daily: Resumen diario de notificaciones diff --git a/decidim-core/config/locales/eu.yml b/decidim-core/config/locales/eu.yml index 46876a3a63f13..4fdad1a1492f7 100644 --- a/decidim-core/config/locales/eu.yml +++ b/decidim-core/config/locales/eu.yml @@ -383,11 +383,10 @@ eu: help_text: Berrikusi aldaketak eta onartu edo ukatu zuzenketa hau. Jakinarazpen bat bidaliko zaio/e bere egileari/ei. announcement: accepted: |- - For the Aldaketa hori %{amendable_type} %{amendable_link} izan da - onartu %{announcement_date}. + %{amendable_type} %{proposal_link} erako zuzenketa hau %{date}ean onartu da. evaluating: |- - For the Aldaketa hori %{amendable_type} %{amendable_link} - ebaluatu dago. + Aldaketa hau %{amendable_type} %{proposal_link} rako + ebaluatzen ari da. promoted: Sustatu hau %{amendable_type}. rejected: "%{amendable_type} %{amendable_link} en zuzenketa\nukatu da %{announcement_date}." withdrawn: |- @@ -876,7 +875,7 @@ eu: email_subject: Onartu da %{emendation_author_nickname} ren %{amendable_title} zuzenketa notification_title: '%{emendation_author_nickname} egileak sortutako zuzenketa onartu da honetarako: %{amendable_title}.' follower: - email_intro: 'Zuzenketa bat bertan bera onartu %{amendable_title}. Orrialde hau ikusi dezakezu:' + email_intro: '%{amendable_title} rako zuzenketa bat onartu da. Orrialde honetan ikus dezakezue:' email_outro: Jakinarazpen hau jaso duzu %{amendable_title} jarraitzen duzulako. Aurreko estekan sartu jakinarazpenak jasotzeari utzi nahi badiozu. email_subject: '%{emendation_author_nickname} ren aldaketa onartuta honetarako: %{amendable_title}' notification_title: '%{emendation_author_nickname} k sortutako zuzenketa onartu da honetarako: %{amendable_title}.' @@ -1053,7 +1052,7 @@ eu: message_2: Irudi honetarako gomendatzen den tamaina da 512x512. image: explanation: 'Jarraibideak irudirako:' - message_1: Ahal dela, testurik gabeko irudi etzana. + message_1: Testurik ez duen irudi etzan bat, ahal dela. message_2: Zerbitzuak irudia moztu egiten du. file_validation: allowed_file_extensions: 'Onartutako artxiboen formatua: %{extensions}' @@ -1292,8 +1291,10 @@ eu: action_error: Arazo bat egon da jakinarazpenaren konfigurazioa eguneratzean. no_notifications: Oraindik ez dago jakinarazpenik. show: + deleted: Egileak edukia ezabatu egin du. missing_event: Aiba, jakinarazpen hau dagoeneko eskuragarri ez dagoen item batena da. Baztertu dezakezu. moderated: Edukia ezkutatu egin da, moderazioaren bidez. + not_available: Hara, jakinarazpen hau dagoeneko erabilgarri ez dagoen elementu bati dagokio. Alde batera utz dezakezu. notifications_digest_mailer: header: daily: Jakinarazpenen eguneroko laburpena @@ -1470,7 +1471,7 @@ eu: footer_sub_hero: footer_sub_hero_body_html: Eraiki dezagun gizarte irekiago, gardenago eta kolaboratiboagoa
Lotu, hartu parte eta erabaki. footer_sub_hero_headline: Ongi etorri %{organization} erakundearen partaidetzarako plataformara. - register: Sortu kontu bat + register: Kontu bat sortu hero: participate: Hartu parte participate_title: Parte hartu plataformako prozesuetan @@ -1606,7 +1607,7 @@ eu: term_input_placeholder: Bilatu searches: filters: - jump_to: 'Salto egin:' + jump_to: 'Hona jo:' resource: "%{label} %{collection} artean" search: Bilatu state: @@ -1641,7 +1642,7 @@ eu: help: "Bilaketa-baldintzak aldatzen direnean, beheko inprimakiak dinamikoki iragazten ditu bilaketaren\nemaitzak." skip: Joan emaitzetara flag_modal: - already_reported: Eduki hau jada salatuta dago, eta administratzaile batek berrikusiko du. + already_reported: Eduki horren berri ematen da dagoeneko, eta administrazio batek berrikusiko du. close: Itxi description: Eduki hau desegokia da? does_not_belong: Bertan badago legez kontrako jardunik, suizidio-mehatxurik, informazio pertsonalik edo beste zernahi, zure ustez %{organization_name}-ri ez dagokionik. @@ -1650,7 +1651,7 @@ eu: offensive: Bertan badago arrazakeriarik, sexismorik, irainik, eraso pertsonalik, heriotza-mehatxurik, suizidio-eskaerarik edo beste edozein eratako gorroto-diskurtsorik. reason: Arrazoia report: Salatu - spam: Bertan badago clickbait-ik, publizitaterik edo iruzurrik. + spam: Clickbait, publizitatea, iruzurrak edo script bot-ak ditu. title: Salatu eduki desegokia flag_user_modal: already_reported: Eduki hau jada salatuta dago, eta administratzaile batek berrikusiko du. @@ -1780,7 +1781,7 @@ eu: version_index: '%{total} en %{index} bertsioa' welcome_notification: default_body:

Kaixo {{name}}, eskerrik asko {{organization}}-era lotzeagatik eta ongi etorria!

  • Hemen zer egin dezakezun ideia azkar bat nahi baduzu, begiratu Laguntza atalean.
  • Behin irakurri ondoren, zure lehen garaikurra jasoko duzu. Hemen da garaikur guztien zerrenda zuk lor ditzakezunak {{organization}} ean parte hartuz.
  • Azkenik, lotu beste pertsona batzuei eta partekatu haiekin inplikatzearen eta {{organization}} ean parte hartzearen esperientzia. Egin proposamenak, iruzkinak, eztabaidak, pentsatu nola lagundu denon onerako, eman argudioak konbentzitzeko, entzun eta irakurri zeure burua konbentzitzeko, adierazi zure ideiak modu zehatzean, erantzun pazientziaz eta erabakiz, defendatu zure ideiak eta mantendu burua zabalik, beste pertsona batzuen ideietan lankidetzan aritzeko.
- default_subject: Eskerrik asko sartzeko {{organization}}! + default_subject: Thanks for joining {{organization}}! wizard_step_form: wizard_aside: back: Atzera @@ -1791,9 +1792,9 @@ eu: confirmations: confirmed: Zure helbide elektronikoa ondo egiaztatu da. new: - resend_confirmation_instructions: Berrikusi berrespen-argibideak + resend_confirmation_instructions: Berrespen-jarraibideak birbidali send_instructions: Jarraibideak azaltzen dituen mezu elektroniko bat jasoko duzu, zure helbide elektronikoa berretsi ahal izateko minutu gutxi barru. - send_paranoid_instructions: Zure helbide elektronikoa gure datu-basean badago, zure helbide elektronikoa berretsi ahal izateko gutxienez minutu bat jasoko duzu. + send_paranoid_instructions: Zure helbide elektronikoa gure datu-basean badago, mezu elektroniko bat jasoko duzu zure helbide elektronikoa minutu gutxi barru baieztatzeko jarraibideekin. failure: already_authenticated: Saioa hasi duzu. inactive: Zure kontua ez dago oraindik aktibatuta. @@ -1816,7 +1817,7 @@ eu: header: Bidali gonbidapena submit_button: Bidali gonbidapena no_invitations_remaining: Ez da gonbidapenik geratzen - send_instructions: Gonbidapen mezu bat bidali da %{email}. + send_instructions: Gonbidapen-mezu elektroniko bat %{email} helbidera bidali da. updated: Pasahitza ondo ezarri da. Saioa hasi duzu. updated_not_active: Pasahitza ondo ezarri da. mailer: @@ -2163,13 +2164,13 @@ eu: user_profile: account: Kontua authorizations: Baimenak - delete_my_account: Ezabatu nire kontua + delete_my_account: Nire kontua ezabatu my_data: Nire datuak notifications_settings: Jakinarazpen-konfigurazioa profile: Profila title: Parte-hartzailearen ezarpenak locale: - name: Euskara + name: Ingelesa name_with_error: Euskera (akatsa!) number: currency: @@ -2178,7 +2179,7 @@ eu: format_html: "%u %n" password_validator: denied: ukatu egin da - domain_included_in_password: domeinu izen honen antzekoa da + domain_included_in_password: domeinu-izen horren oso antzekoa da email_included_in_password: zure posta elektronikoaren antzekoa da fallback: ez da zuzena name_included_in_password: zure izenaren antzekoa da diff --git a/decidim-core/config/locales/fa-IR.yml b/decidim-core/config/locales/fa-IR.yml index 88215f82cbd5d..74e6c76216548 100644 --- a/decidim-core/config/locales/fa-IR.yml +++ b/decidim-core/config/locales/fa-IR.yml @@ -1 +1,3 @@ fa: + locale: + name: فارسی diff --git a/decidim-core/config/locales/fi-plain.yml b/decidim-core/config/locales/fi-plain.yml index a0c9e7990d578..c1eb13f63ddce 100644 --- a/decidim-core/config/locales/fi-plain.yml +++ b/decidim-core/config/locales/fi-plain.yml @@ -760,6 +760,7 @@ fi-pl: created_at: Keskustelun luontiaika id: Keskustelun yksilöivä tunniste messages: Keskustelun viestit + updated_at: Keskustelun viimeisimmän päivityksen ajankohta notifications: created_at: Ilmoituksen luontiaika event_class: Tapahtuman tyyppi, jota ilmoitus koskee @@ -784,6 +785,7 @@ fi-pl: reason: Ilmoituksen syy updated_at: Ilmoituksen viimeisimmän päivityksen ajankohta users: + about: Osallistujan profiilikuvaus accepted_tos_version: Käyttäjän palvelun käyttöehtojen hyväksymisaika admin: Onko tämä käyttäjä hallintakäyttäjä confirmation_sent_at: Käyttäjätilin vahvistusviestin lähetysaika @@ -794,6 +796,7 @@ fi-pl: delete_reason: Käyttäjätilin poistamisen syy deleted_at: Käyttäjätilin poistamisen ajankohta email: Käyttäjätilin sähköpostiosoite + followers_count: Käyttäjää seuraavien osallistujien lukumäärä following_count: Käyttäjän seuraamien osallistujien lukumäärä id: Käyttäjätilin yksilöivä tunniste invitation_accepted_at: Käyttäjätilin kutsun hyväksymisen ajankohta @@ -801,12 +804,16 @@ fi-pl: invitation_sent_at: Käyttäjän kutsun lähetyksen ajankohta invitations_count: Käyttäjälle lähetettyjen kutsujen lukumäärä invited_by: Kutsun lähettänyt käyttäjä + last_sign_in_at: Edellisen kirjautumisen ajankohta + last_sign_in_ip: Edellisen kirjautumisen IP-osoite locale: Käyttäjän valitsema kieliasetus managed: Onko tämä käyttäjätili toisen käyttäjän hallinnoima name: Käyttäjän nimi + newsletter_notifications_at: Uutiskirjeiden tilauksen ajankohta nickname: Käyttäjän nimimerkki notification_settings: Käyttäjän määrittämät ilmoitusten asetukset notifications_sending_frequency: Käyttäjän vastaanottamien ilmoitusten tiheys + officialized_as: Virallistamisen nimi (valtuutettu, kunnanjohtaja, pormestari, jne.) officialized_at: Käyttäjän virallistamisen ajankohta organization: Käyttäjän organisaatio password_updated_at: Salasanan viimeisimmän päivityksen ajankohta @@ -844,6 +851,7 @@ fi-pl: files: file_cannot_be_processed: Tiedostoa ei voida käsitellä file_resolution_too_large: Tiedoston resoluutio on liian suuri + not_inside_organization: Tiedostoa ei ole liitetty mihinkään organisaatioon. internal_server_error: copied: Teksti kopioitu! copy_error_message_clarification: Kopioi virheviesti leikepöydälle @@ -1039,6 +1047,7 @@ fi-pl: file: explanation: 'Tiedoston ohjeistus:' message_1: On oltava kuva tai asiakirja. + message_2: Mikäli lataat kuvan, käytä mieluiten vaakasuuntaista kuvaa, joka ei sisällä ole tekstiä. Palvelu rajaa kuvaa. icon: explanation: 'Ikonin ohjeistus:' message_1: On oltava neliömuotoinen kuva. @@ -1281,9 +1290,13 @@ fi-pl: same_language: Sisältö on lisätty toivomallasi kielellä (%{language}), minkä takia tässä viestissä ei näytetä automaattista käännöstä. translated_text: 'Automaattisesti käännetty teksti:' notifications: + action_error: Ilmoitusasetusten päivittäminen epäonnistui. no_notifications: Ei vielä ilmoituksia. show: + deleted: Sisällön laatija on poistanut tämän sisällön. missing_event: Hups, tämä ilmoitus kuuluu kohteeseen, joka ei ole enää käytettävissä. Voit merkitä sen luetuksi. + moderated: Moderoija on piilottanut tämän sisällön. + not_available: Tämä ilmoitus kuuluu kohteeseen, joka ei ole enää käytettävissä. Voit merkitä sen luetuksi. notifications_digest_mailer: header: daily: Päivittäinen ilmoitusyhteenveto @@ -1292,6 +1305,7 @@ fi-pl: hello: Hei %{name}, intro: daily: 'Tässä on lista edellisen päivän ilmoituksista niistä asioista, joita seuraat:' + real_time: 'Tämä on ilmoitus seuraamistasi alustalla tapahtuneista asioista:' weekly: 'Tässä on lista edellisen viikon ilmoituksista niistä asioista, joita seuraat:' outro: Tämä ilmoitus on lähetetty sinulle, koska seuraat ilmoitukseen liittyviä sisältöjä tai niiden tekijöitä. Voit lopettaa seuraamisen näiden sisältöjen sivuilta. see_more: Näytä lisää ilmoituksia @@ -1434,7 +1448,9 @@ fi-pl: title: Miten nämä tiedostot avataan ja miten niiden kanssa työskennellään? license: body_1_html: '%{organization_name} -palvelun tietokanta on lisensoitu %{link_database} -lisenssillä. Kaikki tietokannan sisällöt on lisensoitu %{link_contents} -lisenssillä.' + license_contents_link: https://opendatacommons.org/licenses/dbcl/1.0/ license_contents_name: Tietokannan sisällön lisenssi + license_database_link: https://opendatacommons.org/licenses/odbl/1.0/ license_database_name: Avoimien tietojen lisenssi title: Lisenssi title: Avoin data @@ -1457,6 +1473,7 @@ fi-pl: footer_sub_hero: footer_sub_hero_body_html: Rakennetaan entistä avoimempi, läpinäkyvämpi ja yhteistyökykyisempi yhteiskunta.
Liity, osallistu ja päätä. footer_sub_hero_headline: Tervetuloa yhteisölliseen osallisuuspalveluun %{organization}. + register: Luo tili hero: participate: Osallistu participate_title: Osallistu alustan prosesseihin @@ -1464,6 +1481,7 @@ fi-pl: statistics: headline: Tilastot sub_hero: + register: Luo tili register_title: Luo tili index: standalone_pages: Sivut @@ -2113,6 +2131,8 @@ fi-pl: expire_time_html: Istuntosi vanhenee %{minutes} minuutin kuluttua. language_chooser: choose_language: Valitse kieli + navigation: + aria_label: 'Navigaatiovalikko: %{title}' notifications_dashboard: mark_all_as_read: Merkitse kaikki luetuiksi mark_as_read: Merkitse luetuiksi diff --git a/decidim-core/config/locales/fi.yml b/decidim-core/config/locales/fi.yml index e5a87fc68225f..7fcb93aaee6dc 100644 --- a/decidim-core/config/locales/fi.yml +++ b/decidim-core/config/locales/fi.yml @@ -760,6 +760,7 @@ fi: created_at: Keskustelun luontiaika id: Keskustelun yksilöivä tunniste messages: Keskustelun viestit + updated_at: Keskustelun viimeisimmän päivityksen ajankohta notifications: created_at: Ilmoituksen luontiaika event_class: Tapahtuman tyyppi, jota ilmoitus koskee @@ -784,6 +785,7 @@ fi: reason: Ilmoituksen syy updated_at: Ilmoituksen viimeisimmän päivityksen ajankohta users: + about: Osallistujan profiilikuvaus accepted_tos_version: Käyttäjän palvelun käyttöehtojen hyväksymisaika admin: Onko tämä käyttäjä hallintakäyttäjä confirmation_sent_at: Käyttäjätilin vahvistusviestin lähetysaika @@ -794,6 +796,7 @@ fi: delete_reason: Käyttäjätilin poistamisen syy deleted_at: Käyttäjätilin poistamisen ajankohta email: Käyttäjätilin sähköpostiosoite + followers_count: Käyttäjää seuraavien osallistujien lukumäärä following_count: Käyttäjän seuraamien osallistujien lukumäärä id: Käyttäjätilin yksilöivä tunniste invitation_accepted_at: Käyttäjätilin kutsun hyväksymisen ajankohta @@ -801,12 +804,16 @@ fi: invitation_sent_at: Käyttäjän kutsun lähetyksen ajankohta invitations_count: Käyttäjälle lähetettyjen kutsujen lukumäärä invited_by: Kutsun lähettänyt käyttäjä + last_sign_in_at: Edellisen kirjautumisen ajankohta + last_sign_in_ip: Edellisen kirjautumisen IP-osoite locale: Käyttäjän valitsema kieliasetus managed: Onko tämä käyttäjätili toisen käyttäjän hallinnoima name: Käyttäjän nimi + newsletter_notifications_at: Uutiskirjeiden tilauksen ajankohta nickname: Käyttäjän nimimerkki notification_settings: Käyttäjän määrittämät ilmoitusten asetukset notifications_sending_frequency: Käyttäjän vastaanottamien ilmoitusten tiheys + officialized_as: Virallistamisen nimi (valtuutettu, kunnanjohtaja, pormestari, jne.) officialized_at: Käyttäjän virallistamisen ajankohta organization: Käyttäjän organisaatio password_updated_at: Salasanan viimeisimmän päivityksen ajankohta @@ -844,6 +851,7 @@ fi: files: file_cannot_be_processed: Tiedostoa ei voida käsitellä file_resolution_too_large: Tiedoston resoluutio on liian suuri + not_inside_organization: Tiedostoa ei ole liitetty mihinkään organisaatioon. internal_server_error: copied: Teksti kopioitu! copy_error_message_clarification: Kopioi virheviesti leikepöydälle @@ -1039,6 +1047,7 @@ fi: file: explanation: 'Tiedoston ohjeistus:' message_1: On oltava kuva tai asiakirja. + message_2: Mikäli lataat kuvan, käytä mieluiten vaakasuuntaista kuvaa, joka ei sisällä ole tekstiä. Palvelu rajaa kuvaa. icon: explanation: 'Kuvakkeen ohjeistus:' message_1: On oltava neliömuotoinen kuva. @@ -1281,9 +1290,13 @@ fi: same_language: Sisältö on lisätty toivomallasi kielellä (%{language}), minkä takia tässä viestissä ei näytetä automaattista käännöstä. translated_text: 'Automaattisesti käännetty teksti:' notifications: + action_error: Ilmoitusasetusten päivittäminen epäonnistui. no_notifications: Ei vielä ilmoituksia. show: + deleted: Sisällön laatija on poistanut tämän sisällön. missing_event: Hups, tämä ilmoitus kuuluu kohteeseen, joka ei ole enää käytettävissä. Voit merkitä sen luetuksi. + moderated: Moderoija on piilottanut tämän sisällön. + not_available: Tämä ilmoitus kuuluu kohteeseen, joka ei ole enää käytettävissä. Voit merkitä sen luetuksi. notifications_digest_mailer: header: daily: Päivittäinen ilmoitusyhteenveto @@ -1292,6 +1305,7 @@ fi: hello: Hei %{name}, intro: daily: 'Tässä on lista edellisen päivän ilmoituksista niistä asioista, joita seuraat:' + real_time: 'Tämä on ilmoitus seuraamistasi alustalla tapahtuneista asioista:' weekly: 'Tässä on lista edellisen viikon ilmoituksista niistä asioista, joita seuraat:' outro: Tämä ilmoitus on lähetetty sinulle, koska seuraat ilmoitukseen liittyviä sisältöjä tai niiden tekijöitä. Voit lopettaa seuraamisen näiden sisältöjen sivuilta. see_more: Näytä lisää ilmoituksia @@ -1434,7 +1448,9 @@ fi: title: Miten nämä tiedostot avataan ja miten niiden kanssa työskennellään? license: body_1_html: '%{organization_name} -palvelun tietokanta on lisensoitu %{link_database} -lisenssillä. Kaikki tietokannan sisällöt on lisensoitu %{link_contents} -lisenssillä.' + license_contents_link: https://opendatacommons.org/licenses/dbcl/1.0/ license_contents_name: Tietokannan sisällön lisenssi + license_database_link: https://opendatacommons.org/licenses/odbl/1.0/ license_database_name: Avoimien tietojen lisenssi title: Lisenssi title: Avoin data @@ -1457,6 +1473,7 @@ fi: footer_sub_hero: footer_sub_hero_body_html: Rakennetaan entistä avoimempi, läpinäkyvämpi ja yhteistyökykyisempi yhteiskunta.
Liity, osallistu ja päätä. footer_sub_hero_headline: Tervetuloa yhteisölliseen osallistumispalveluun %{organization}. + register: Luo tili hero: participate: Osallistu participate_title: Osallistu prosesseihin alustalla @@ -1464,6 +1481,7 @@ fi: statistics: headline: Tilastot sub_hero: + register: Luo tili register_title: Luo tili index: standalone_pages: Sivut @@ -2116,6 +2134,8 @@ fi: expire_time_html: Istuntosi vanhenee %{minutes} minuutin kuluttua. language_chooser: choose_language: Valitse kieli + navigation: + aria_label: 'Navigaatiovalikko: %{title}' notifications_dashboard: mark_all_as_read: Merkitse kaikki luetuiksi mark_as_read: Merkitse luetuiksi diff --git a/decidim-core/config/locales/fr-CA.yml b/decidim-core/config/locales/fr-CA.yml index 1009ef1600cec..cc9a41afa6466 100644 --- a/decidim-core/config/locales/fr-CA.yml +++ b/decidim-core/config/locales/fr-CA.yml @@ -1169,8 +1169,10 @@ fr-CA: action_error: Un problème est survenu lors de la mise à jour des paramètres de notification. no_notifications: Il n'y a pas encore de notifications. show: + deleted: Le contenu a été supprimé par l'auteur. missing_event: Oups, cette notification est liée à un élément qui n'est plus disponible. Vous pouvez la supprimer. moderated: Le contenu a été masqué par modération. + not_available: Oups, cette notification est liée à un élément qui n'est plus disponible. Vous pouvez la supprimer. notifications_digest_mailer: header: daily: Résumé quotidien des notifications diff --git a/decidim-core/config/locales/fr.yml b/decidim-core/config/locales/fr.yml index 50759fa79a744..e6539e3c563d4 100644 --- a/decidim-core/config/locales/fr.yml +++ b/decidim-core/config/locales/fr.yml @@ -1169,8 +1169,10 @@ fr: action_error: Un problème est survenu lors de la mise à jour des paramètres de notification. no_notifications: Il n'y a pas encore de notifications. show: + deleted: Le contenu a été supprimé par l'auteur. missing_event: Oups, cette notification est liée à un élément qui n'est plus disponible. Vous pouvez la supprimer. moderated: Le contenu a été masqué par modération. + not_available: Oups, cette notification est liée à un élément qui n'est plus disponible. Vous pouvez la supprimer. notifications_digest_mailer: header: daily: Résumé quotidien des notifications diff --git a/decidim-core/config/locales/ja.yml b/decidim-core/config/locales/ja.yml index 958033364a424..0daeb2cdb32dd 100644 --- a/decidim-core/config/locales/ja.yml +++ b/decidim-core/config/locales/ja.yml @@ -742,6 +742,7 @@ ja: created_at: この会話の作成日時 id: この会話の固有ID messages: この会話のメッセージ + updated_at: この会話の最終更新日時 notifications: created_at: 通知の作成日時 event_class: 通知をトリガーしたイベントのクラス @@ -766,6 +767,7 @@ ja: reason: レポートの理由 updated_at: レポートの最終更新日時 users: + about: この参加者のプロフィール説明 accepted_tos_version: プラットフォームの利用規約がユーザーに承諾された日時 admin: このユーザーが管理者であるかどうか confirmation_sent_at: 確認の送信日時 @@ -776,6 +778,7 @@ ja: delete_reason: このユーザーの削除理由 deleted_at: このユーザーの削除日時 email: このユーザーのメールアドレス + followers_count: このユーザーをフォローしている参加者数 following_count: このユーザーがフォローしている参加者数 id: ユーザーの固有ID invitation_accepted_at: 招待の承認日時 @@ -783,12 +786,16 @@ ja: invitation_sent_at: 招待の送信日時 invitations_count: このユーザーに送信された招待の数 invited_by: このユーザーを招待したユーザー + last_sign_in_at: 前回のログイン日時 + last_sign_in_ip: 前回ログイン時のIPアドレス locale: このユーザーが選択したロケール(言語) managed: このユーザーが別のユーザーによって管理されているかどうか name: ユーザー名 + newsletter_notifications_at: この参加者のニュースレター購読登録日時 nickname: ユーザーのニックネーム notification_settings: このユーザーが選択した通知設定 notifications_sending_frequency: このユーザーが受け取る通知の頻度 + officialized_as: 役職の説明(市長、局長など) officialized_at: このユーザーの公式化日時 organization: このユーザーが所属する組織 password_updated_at: パスワードの最終更新日時 @@ -826,6 +833,7 @@ ja: files: file_cannot_be_processed: ファイルを処理できません file_resolution_too_large: ファイルの解像度が大きすぎます + not_inside_organization: ファイルはどの組織にも紐ついていません。 internal_server_error: copied: テキストをコピーしました copy_error_message_clarification: エラーメッセージをクリップボードにコピー @@ -1022,6 +1030,7 @@ ja: file: explanation: 'ファイルのガイダンス:' message_1: 画像またはドキュメントである必要があります。 + message_2: 画像を使用する場合は、テキストが含まれない横長の画像が最適です。プラットフォーム側で画像をトリミングします。 icon: explanation: 'アイコンのガイド:' message_1: 正方形の画像でなければなりません。 @@ -1263,9 +1272,11 @@ ja: same_language: コンテンツはあなたの設定された言語(%{language}) で投稿されました。このため、このメールには自動翻訳は表示されません。 translated_text: '自動翻訳されたテキスト:' notifications: + action_error: 通知設定の更新に問題が発生しました。 no_notifications: まだ通知はありません。 show: missing_event: この通知は利用できなくなった要素に属しています。破棄してください。 + moderated: コンテンツはモデレーションにより非表示になっています。 notifications_digest_mailer: header: daily: 日次の通知のダイジェスト @@ -1274,6 +1285,7 @@ ja: hello: こんにちは %{name} さん、 intro: daily: 'あなたがフォローしているアクティビティに基づいた最終日以降の通知です:' + real_time: 'これはあなたがフォローしている活動に関する通知です:' weekly: 'あなたがフォローしているアクティビティに基づいた先週以降の通知です:' outro: このコンテンツまたは著者をフォローしているため、これらの通知を受け取りました。それぞれのページからフォローを解除できます。 see_more: 他の通知を見る @@ -1415,7 +1427,9 @@ ja: title: これらのファイルを開いて操作する方法 license: body_1_html: この %{organization_name} データベースは、 %{link_database} の下で利用可能になります。データベースの個々のコンテンツの権利は、 %{link_contents} の下でライセンスされます。 + license_contents_link: https://opendatacommons.org/licenses/dbcl/1.0/ license_contents_name: Database Contents License + license_database_link: https://opendatacommons.org/licenses/odbl/1.0/ license_database_name: Open Database License title: ライセンス title: オープンデータ @@ -1438,6 +1452,7 @@ ja: footer_sub_hero: footer_sub_hero_body_html: より開かれた透明性のあるコラボレーティブな社会を構築しましょう。
一緒に参加し、決定しましょう。 footer_sub_hero_headline: '%{organization} 参加型プラットフォームへようこそ。' + register: アカウントを作成 hero: participate: 参加 participate_title: プラットフォームのプロセスに参加する @@ -1445,6 +1460,7 @@ ja: statistics: headline: 統計情報 sub_hero: + register: アカウントを作成 register_title: アカウントを作成 index: standalone_pages: ページ @@ -2092,6 +2108,8 @@ ja: expire_time_html: このログインセッションは %{minutes} 分後に無効になります。。 language_chooser: choose_language: 言語を選択 + navigation: + aria_label: 'ナビゲーションメニュー: %{title}' notifications_dashboard: mark_all_as_read: すべての通知を削除する mark_as_read: 既読にする diff --git a/decidim-core/config/locales/ko.yml b/decidim-core/config/locales/ko.yml index 8a7b3b861deda..867fae663ef9a 100644 --- a/decidim-core/config/locales/ko.yml +++ b/decidim-core/config/locales/ko.yml @@ -1 +1,3 @@ ko: + locale: + name: 한국어 diff --git a/decidim-core/config/locales/mt.yml b/decidim-core/config/locales/mt.yml index f7aabc7149a9b..b5b1e3cbc583f 100644 --- a/decidim-core/config/locales/mt.yml +++ b/decidim-core/config/locales/mt.yml @@ -1 +1,3 @@ mt: + locale: + name: Malti diff --git a/decidim-core/config/locales/sv.yml b/decidim-core/config/locales/sv.yml index 5491a98940707..9ce3516a8d050 100644 --- a/decidim-core/config/locales/sv.yml +++ b/decidim-core/config/locales/sv.yml @@ -1212,8 +1212,10 @@ sv: action_error: Det gick inte att uppdatera inställningen för meddelanden. no_notifications: Inga meddelanden ännu. show: + deleted: Innehållet har tagits bort av författaren. missing_event: Hoppsan, det här meddelandet tillhör ett objekt som inte längre är tillgängligt. Du kan kasta det. moderated: Innehållet har dolts av moderator. + not_available: Hoppsan, det här meddelandet tillhör ett objekt som inte längre är tillgängligt. Du kan kasta det. notifications_digest_mailer: header: daily: Dagligt sammandrag diff --git a/decidim-core/config/locales/vi.yml b/decidim-core/config/locales/vi.yml index 326506f0b1a36..8ddf65e6d1d49 100644 --- a/decidim-core/config/locales/vi.yml +++ b/decidim-core/config/locales/vi.yml @@ -1 +1,3 @@ vi: + locale: + name: tiếng Việt diff --git a/decidim-debates/config/locales/ja.yml b/decidim-debates/config/locales/ja.yml index 7da9070acb9ed..377904e1bd22d 100644 --- a/decidim-debates/config/locales/ja.yml +++ b/decidim-debates/config/locales/ja.yml @@ -32,6 +32,7 @@ ja: comment: コメント create: ディベートを作成 like: いいね + vote_comment: コメントに投票 name: ディベート settings: global: diff --git a/decidim-elections/config/locales/eu.yml b/decidim-elections/config/locales/eu.yml index aceb1772ed9d5..f07c6557a04b6 100644 --- a/decidim-elections/config/locales/eu.yml +++ b/decidim-elections/config/locales/eu.yml @@ -96,6 +96,7 @@ eu: start_question_button: Botoa eman daiteke title: Emaitzak status: + allow_census_check_before_start: Parte hartzaileek botoa ematen duten ala ez egiaztatu ahal izango dute aukeraketa hasi aurretik census: 'Errolda:' results_availability: after_end: Emaitzak eskuragarri bozketak amaitu ondoren @@ -140,6 +141,8 @@ eu: publish: invalid: Arazo bat egon da hauteskundea argitaratzean. success: Hauteskundea zuzen argitaratua. + toggle_census_check: + error: Akats bat gertatu da errolda egiaztatzean. unpublish: invalid: Arazo bat egon da hauteskundea despublikatzean. success: Hauteskundea zuzen despublikatua. @@ -182,6 +185,11 @@ eu: update: "%{user_name} parte-hartzaileak %{resource_name} hauteskundea eguneratu du %{space_name} espazioan" question: update: "%{user_name} parte-hartzaileak eguneratu ditu %{resource_name} aukerako galderak" + census_checks: + show: + description: Behin hauteskundeak hasita, bertan bozkatu ahal izango da. + exit_button: Exit the census check + title: Behar bezala egiaztatu da censuses: census_ready_html: Erroldako datuak kargatuta daude eta prest daude % %{election_title}{@eleccion_title} bozketan erabiltzeko. census_size_html: @@ -246,6 +254,8 @@ eu: title: Aukeraketaren galderak show: active_voting_until: 'Bozketa aktibo %{end_date} arte' + check_census_button: Begiratu ea botoa eman dezakedan + check_census_explanation: Hauteskunde hauek oraindik ez dira hasi, baina erroldan sartuta zauden egiazta dezakezu. vote_button: Eman babesa voted: Bozkatu duzu. Berriz bozkatu dezakezu, zure azken botoa baino ez da kontuan hartuko. votes_count: diff --git a/decidim-elections/config/locales/fi-plain.yml b/decidim-elections/config/locales/fi-plain.yml index e905e60076635..5305224a6e71c 100644 --- a/decidim-elections/config/locales/fi-plain.yml +++ b/decidim-elections/config/locales/fi-plain.yml @@ -96,6 +96,7 @@ fi-pl: start_question_button: Ota äänestys käyttöön title: Tulokset status: + allow_census_check_before_start: Salli käyttäjien tarkistaa, voivatko he äänestää ennen vaalin alkamista census: 'Henkilötietorekisteri:' results_availability: after_end: Tulokset ovat saatavilla vaalin päätyttyä @@ -140,6 +141,8 @@ fi-pl: publish: invalid: Vaalin julkaisu epäonnistui. success: Vaalin julkaisu onnistui. + toggle_census_check: + error: Henkilötietorekisterin tarkastuksen käyttöönotto epäonnistui. unpublish: invalid: Vaalin julkaisun peruminen epäonnistui. success: Vaalin julkaisun peruminen onnistui. @@ -182,6 +185,11 @@ fi-pl: update: "%{user_name} päivitti vaalia %{resource_name} tilassa %{space_name}" question: update: "%{user_name} päivitti kysymyksiä vaalille %{resource_name}" + census_checks: + show: + description: Tämä tarkoittaa sitä, että kun vaalit alkavat, voit äänestää niissä. + exit_button: Poistu väestölaskennan tarkastuksesta + title: Tilisi vahvistaminen onnistui censuses: census_ready_html: Henkilötietorekisterin tietoja ladataan ja valmistellaan käytettäväksi vaalissa %{election_title}. census_size_html: @@ -246,6 +254,8 @@ fi-pl: title: Vaaliin liittyvät kysymykset show: active_voting_until: 'Äänestysaika päättyy: %{end_date}' + check_census_button: Tarkasta, voitko äänestää + check_census_explanation: Nämä vaalit eivät ole vielä alkaneet, mutta voit tarkistaa, onko sinut merkitty näiden vaalien henkilötietorekisteriin. vote_button: Äänestä voted: Olet jo äänestänyt. Voit äänestää uudestaan, ainoastaan viimeinen äänesi otetaan huomioon ääntenlaskennassa. votes_count: diff --git a/decidim-elections/config/locales/fi.yml b/decidim-elections/config/locales/fi.yml index 9319fa8e6959c..2f564b450bd6d 100644 --- a/decidim-elections/config/locales/fi.yml +++ b/decidim-elections/config/locales/fi.yml @@ -96,6 +96,7 @@ fi: start_question_button: Ota äänestys käyttöön title: Tulokset status: + allow_census_check_before_start: Salli käyttäjien tarkistaa, voivatko he äänestää ennen vaalin alkamista census: 'Henkilötietorekisteri:' results_availability: after_end: Tulokset ovat saatavilla vaalin päätyttyä @@ -140,6 +141,8 @@ fi: publish: invalid: Vaalin julkaisu epäonnistui. success: Vaalin julkaisu onnistui. + toggle_census_check: + error: Henkilötietorekisterin tarkastuksen käyttöönotto epäonnistui. unpublish: invalid: Vaalin julkaisun peruminen epäonnistui. success: Vaalin julkaisun peruminen onnistui. @@ -182,6 +185,11 @@ fi: update: "%{user_name} päivitti vaalia %{resource_name} tilassa %{space_name}" question: update: "%{user_name} päivitti kysymyksiä vaalille %{resource_name}" + census_checks: + show: + description: Tämä tarkoittaa sitä, että kun vaalit alkavat, voit äänestää niissä. + exit_button: Poistu väestölaskennan tarkastuksesta + title: Tilisi vahvistaminen onnistui censuses: census_ready_html: Henkilötietorekisterin tietoja ladataan ja valmistellaan käytettäväksi vaalissa %{election_title}. census_size_html: @@ -246,6 +254,8 @@ fi: title: Vaaliin liittyvät kysymykset show: active_voting_until: 'Äänestysaika päättyy: %{end_date}' + check_census_button: Tarkasta, voitko äänestää + check_census_explanation: Nämä vaalit eivät ole vielä alkaneet, mutta voit tarkistaa, onko sinut merkitty näiden vaalien henkilötietorekisteriin. vote_button: Äänestä voted: Olet jo äänestänyt. Voit äänestää uudestaan, ainoastaan viimeinen äänesi otetaan huomioon ääntenlaskennassa. votes_count: diff --git a/decidim-elections/config/locales/ja.yml b/decidim-elections/config/locales/ja.yml index c9098848640e4..cb9fa21fa3754 100644 --- a/decidim-elections/config/locales/ja.yml +++ b/decidim-elections/config/locales/ja.yml @@ -1,6 +1,8 @@ ja: activemodel: attributes: + elections_question: + max_choices: 選択肢の最大数 token_csv: file: ファイル remove_all: 現在のセンサスデータをすべて削除する @@ -92,6 +94,7 @@ ja: start_question_button: 投票を有効化 title: 結果 status: + allow_census_check_before_start: 選挙開始前にユーザーが投票可能かどうかを確認できるようにする census: 'センサス:' results_availability: after_end: 選挙終了後に利用可能な結果 @@ -136,6 +139,8 @@ ja: publish: invalid: 選挙の公表中に問題が発生しました。 success: 選挙を公開しました。 + toggle_census_check: + error: センサスチェックを有効にする際にエラーが発生しました。 unpublish: invalid: 選挙の非公表中に問題が発生しました。 success: 選挙を非公開にしました。 @@ -178,6 +183,11 @@ ja: update: "%{user_name} が %{space_name} の %{resource_name} 選挙を更新しました" question: update: "%{user_name} が %{resource_name} 選挙の質問を更新しました" + census_checks: + show: + description: これは、選挙が始まったらあなたはその選挙に投票できるということを意味します。 + exit_button: センサスチェックを終了 + title: 正常に検証されました censuses: census_ready_html: センサスデータはアップロードされ、 %{election_title} 選挙での使用に備えられています。 census_size_html: @@ -240,6 +250,8 @@ ja: title: 選挙の質問 show: active_voting_until: '有効な投票まで: %{end_date}' + check_census_button: 投票できるかどうかチェック + check_census_explanation: この選挙はまだ始まっていませんが、センサスの対象に含まれているかどうか確認できます。 vote_button: 投票 voted: すでに投票済みです。再度投票することは可能ですが、最後に投票した内容のみが集計対象となります。 votes_count: @@ -293,6 +305,9 @@ ja: question: back: 戻る cast_vote: 投票 + max_choices: '最大選択数: %{count}' + max_choices_alert: 選択したオプションが多すぎます。続行するには選択をいくつか解除してください。 + max_choices_exceeded: 選択できるオプションは最大 %{max} 個までです。一度戻って選択を調整してください。 next: 次へ receipt: description: 投票期間中はいつでも再投票が可能です。以前の投票結果は新しい投票結果で上書きされます。 diff --git a/decidim-elections/config/locales/sv.yml b/decidim-elections/config/locales/sv.yml index 75992528f3265..99a5702cb946f 100644 --- a/decidim-elections/config/locales/sv.yml +++ b/decidim-elections/config/locales/sv.yml @@ -67,6 +67,7 @@ sv: publish_button: Publicera resultat title: Resultat status: + allow_census_check_before_start: Tillåt användare att kontrollera om de kan rösta innan valet börjar census: 'Folkräkning:' results_availability: after_end: Resultat tillgängliga efter valets slut @@ -105,6 +106,8 @@ sv: publish: invalid: Det gick inte att publicera valet. success: Valet publicerades. + toggle_census_check: + error: Det gick inte att aktivera kontrollen av folkräkningen. unpublish: invalid: Det gick inte att ta bort valet. success: Valet togs bort. @@ -123,6 +126,11 @@ sv: soft_delete: "%{user_name} Flyttade valet %{resource_name} i %{space_name} till papperskorgen" unpublish: "%{user_name} Tog bort valet %{resource_name} i %{space_name}" update: "%{user_name} Uppdaterade valet %{resource_name} i %{space_name}" + census_checks: + show: + description: Detta innebär att direkt när valet har startat kan ni rösta i det. + exit_button: Avsluta folkräkningskontrollen + title: Du har blivit auktoriserad censuses: internal_users: already_have_an_account?: Har du redan ett konto? @@ -181,6 +189,8 @@ sv: subtitle: 'Detta är frågorna för denna omröstningsprocess:' show: active_voting_until: 'Aktiv röstning till: %{end_date}' + check_census_button: Kontrollera om jag kan rösta + check_census_explanation: Detta val har ännu inte börjat, men du kan kontrollera om du är med i folkräkningen. vote_button: Rösta voted: Du har redan röstat. Om du röstar igen kommer bara den senaste rösten att räknas. votes_count: diff --git a/decidim-initiatives/config/locales/ja.yml b/decidim-initiatives/config/locales/ja.yml index ec4b11ebd5fe9..7dea683486745 100644 --- a/decidim-initiatives/config/locales/ja.yml +++ b/decidim-initiatives/config/locales/ja.yml @@ -92,6 +92,8 @@ ja: validating: 技術的検証 type_id_eq: label: タイプ + search_placeholder: + title_or_description_or_author_name_or_author_nickname_cont: '%{collection} をタイトル、説明、または著者名で検索' initiatives_settings: update: error: イニシアチブ設定の更新中に問題が発生しました。 @@ -677,6 +679,7 @@ ja: initiative: actions: comment: コメント + vote_comment: コメントに投票 initiatives_type: actions: create: イニシアチブを作成 diff --git a/decidim-meetings/config/locales/cs.yml b/decidim-meetings/config/locales/cs.yml index a1d38d6063c96..dc69114b2d969 100644 --- a/decidim-meetings/config/locales/cs.yml +++ b/decidim-meetings/config/locales/cs.yml @@ -153,6 +153,8 @@ cs: copy: Kopírovat title: Duplicitní schůzka tooltips: + cannot_close_meetings: Tuto schůzku nelze zavřít, protože ji vytvořil účastník + cannot_duplicate_meetings: Tuto schůzku nelze duplikovat, protože byla vytvořena účastníkem cannot_manage_attachments_meetings: Nelze spravovat přílohy v této schůzce, protože jsou vytvořeny uživatelem cannot_manage_poll_meetings: Nelze spravovat anketu na této schůzce, protože je vytvořena účastníkem components: diff --git a/decidim-meetings/config/locales/ja.yml b/decidim-meetings/config/locales/ja.yml index 1527b90e67791..8a185720ef21f 100644 --- a/decidim-meetings/config/locales/ja.yml +++ b/decidim-meetings/config/locales/ja.yml @@ -159,6 +159,7 @@ ja: comment: コメント join: 参加する reply_poll: 投票 + vote_comment: コメントに投票 name: ミーティング settings: global: diff --git a/decidim-participatory_processes/config/locales/fi-plain.yml b/decidim-participatory_processes/config/locales/fi-plain.yml index e2913075c3977..ebd8211a98194 100644 --- a/decidim-participatory_processes/config/locales/fi-plain.yml +++ b/decidim-participatory_processes/config/locales/fi-plain.yml @@ -340,6 +340,7 @@ fi-pl: participatory_processes: announcement: Ilmoituksen sisältö area: Alue, jota prosessi koskee + component_settings: Prosessin komponenttiasetukset created_at: Tilan luontiaika description: Prosessin pitkä kuvaus developer_group: Prosessin kehittäjäryhmä, joka edistää tämän prosessin toteutumista. diff --git a/decidim-participatory_processes/config/locales/fi.yml b/decidim-participatory_processes/config/locales/fi.yml index 7e6a8177ea264..cc6a07f75cce0 100644 --- a/decidim-participatory_processes/config/locales/fi.yml +++ b/decidim-participatory_processes/config/locales/fi.yml @@ -340,6 +340,7 @@ fi: participatory_processes: announcement: Ilmoituksen sisältö area: Alue, jota prosessi koskee + component_settings: Prosessin komponenttiasetukset created_at: Tilan luontiaika description: Prosessin pitkä kuvaus developer_group: Prosessin kehittäjäryhmä, joka edistää tämän prosessin toteutumista. diff --git a/decidim-participatory_processes/config/locales/ja.yml b/decidim-participatory_processes/config/locales/ja.yml index 5d938d8dd4313..2881bef1b6f59 100644 --- a/decidim-participatory_processes/config/locales/ja.yml +++ b/decidim-participatory_processes/config/locales/ja.yml @@ -337,6 +337,7 @@ ja: participatory_processes: announcement: アナウンス(コールアウト)情報 area: プロセスが行われているエリア + component_settings: プロセスのコンポーネント設定 created_at: このスペースの作成日時 description: プロセスの詳しい説明 developer_group: プロセスの開発者グループ。これはプロセスをプロモートしている組織です diff --git a/decidim-proposals/config/locales/de.yml b/decidim-proposals/config/locales/de.yml index 0d58a4e0df184..2dd7dd2303e3d 100644 --- a/decidim-proposals/config/locales/de.yml +++ b/decidim-proposals/config/locales/de.yml @@ -110,8 +110,8 @@ de: one: Anmerkung other: Anmerkungen decidim/proposals/proposal_vote: - one: Abstimmung - other: Abstimmungen + one: Stimme + other: Stimmen decidim: admin: admin_log: diff --git a/decidim-proposals/config/locales/eu.yml b/decidim-proposals/config/locales/eu.yml index 6d27b4da114a2..8355b8d9c27cc 100644 --- a/decidim-proposals/config/locales/eu.yml +++ b/decidim-proposals/config/locales/eu.yml @@ -1000,7 +1000,7 @@ eu: already_voted_hover: Kendu babesa maximum_votes_reached: Babesen muga lortua no_votes_remaining: Ez da gelditzen babesik - vote: Alde egin + vote: Babesa eman votes_blocked: Eman botoa votes_count: count: @@ -1023,7 +1023,7 @@ eu: title: Parte hartzeko arauak vote_limit: description: '%{limit} proposameni eman ahal diozu babesa.' - votes: Gainerako %{number} babes + votes: '%{number} babes emateke' wizard_aside: back: Atzera back_from_step_1: Itzuli proposamenetara diff --git a/decidim-surveys/config/locales/ja.yml b/decidim-surveys/config/locales/ja.yml index 0c8d28cb85a9c..3c85ea7cbf8dd 100644 --- a/decidim-surveys/config/locales/ja.yml +++ b/decidim-surveys/config/locales/ja.yml @@ -46,6 +46,17 @@ ja: email_outro: '%{participatory_space_title}をフォローしているため、この通知を受け取りました。前のリンクに続く通知の受信を停止することができます。' email_subject: '%{participatory_space_title} での新しいアンケート' notification_title: %{resource_title}の %{participatory_space_title}のアンケート が公開されました。 + open_data: + help: + published_survey_user_responses: + body: 回答の内容 + created_at: 回答が作成されたタイムスタンプ + id: アンケート回答の固有ID + ip_hash: プライバシー保護のためにハッシュ化された回答者のIPアドレス + question: 回答された質問 + registered: 登録済みの参加者 + unregistered: 未登録の参加者 + user_status: 回答を提出したユーザーのステータス statistics: responses: '応答:' responses_count: 回答 diff --git a/decidim-verifications/config/locales/fi-plain.yml b/decidim-verifications/config/locales/fi-plain.yml index ab4fc7f4411a6..5dc66a492bb15 100644 --- a/decidim-verifications/config/locales/fi-plain.yml +++ b/decidim-verifications/config/locales/fi-plain.yml @@ -178,7 +178,7 @@ fi-pl: no_user: Käyttäjää ei löytynyt success: '%{count} tietueen tuonti onnistui. Tietoja käsitellään parhaillaan. Päivitä tämä sivu muutaman minuutin kuluttua nähdäksesi muutokset.' destroy: - success: Henkilötietorekisteriin tietue poistettiin. + success: Henkilötietorekisterin tietue poistettiin. index: empty: Henkilötietorekisterissä ei ole tietoja. Käytä %{import_csv} -toimintoa tuodaksesi tiedot järjestelmään CSV-tiedostosta. no_sign_in: Ei ole koskaan kirjautunut sisään diff --git a/decidim-verifications/config/locales/fi.yml b/decidim-verifications/config/locales/fi.yml index 34655354df782..c5593d8702391 100644 --- a/decidim-verifications/config/locales/fi.yml +++ b/decidim-verifications/config/locales/fi.yml @@ -178,7 +178,7 @@ fi: no_user: Käyttäjää ei löytynyt success: '%{count} tietueen tuonti onnistui. Tietoja käsitellään parhaillaan. Päivitä tämä sivu muutaman minuutin kuluttua nähdäksesi muutokset.' destroy: - success: Henkilötietorekisteriin tietue poistettiin. + success: Henkilötietorekisterin tietue poistettiin. index: empty: Henkilötietorekisterissä ei ole tietoja. Käytä %{import_csv} -toimintoa tuodaksesi tiedot järjestelmään CSV-tiedostosta. no_sign_in: Ei ole koskaan kirjautunut sisään From f08bf40fc4c7d93bdfae9055b45537f18cd3aa05 Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Wed, 10 Dec 2025 12:49:28 +0200 Subject: [PATCH 008/116] Error handling of not found resources for GraphQL API (#15721) * Error handling of not found resources * Fix Page specs * Add I18n support for not found error * Fix i18n key * Fix locales * Fix failing specs * Add error code token * Rename context from graphQL hidden space to graphQL not found space * Address other review comments --- .../lib/decidim/api/accountability_type.rb | 6 +- .../spec/types/accountability_type_spec.rb | 8 +-- decidim-api/config/locales/en.yml | 6 ++ .../lib/decidim/api/errors/not_found_error.rb | 13 ++++ decidim-api/lib/decidim/api/schema.rb | 4 ++ .../lib/decidim/api/test/component_context.rb | 60 ++++++++++--------- .../lib/decidim/api/test/type_context.rb | 13 +++- decidim-api/lib/decidim/api/types.rb | 4 ++ .../spec/lib/query_extensions_spec.rb | 4 +- decidim-blogs/spec/types/blogs_type_spec.rb | 4 +- .../lib/decidim/api/budgets_type.rb | 4 +- .../spec/types/budgets_type_spec.rb | 4 +- .../spec/types/integration_schema_spec.rb | 32 +++------- .../spec/types/documents_type_spec.rb | 4 +- .../conferences/query_extensions_spec.rb | 4 +- .../api/functions/component_finder_base.rb | 2 +- .../participatory_space_finder_base.rb | 2 +- decidim-core/lib/decidim/query_extensions.rb | 6 +- .../lib/decidim/api/debates_type.rb | 4 +- .../spec/types/debates_type_spec.rb | 4 +- .../config/rubocop/graphql/configuration.yml | 1 + .../decidim/initiatives/query_extensions.rb | 9 +-- .../spec/lib/query_extensions_spec.rb | 6 +- .../lib/decidim/api/meetings_type.rb | 4 +- .../spec/types/meetings_type_spec.rb | 8 +-- decidim-pages/lib/decidim/api/pages_type.rb | 4 +- decidim-pages/spec/types/pages_type_spec.rb | 4 +- .../query_extensions.rb | 18 ++---- .../spec/lib/query_extensions_spec.rb | 12 ++-- .../spec/types/proposals_type_spec.rb | 8 +-- .../lib/decidim/api/surveys_type.rb | 4 +- .../spec/types/integration_schema_spec.rb | 2 +- .../spec/types/surveys_type_spec.rb | 12 ++-- 33 files changed, 144 insertions(+), 136 deletions(-) create mode 100644 decidim-api/config/locales/en.yml create mode 100644 decidim-api/lib/decidim/api/errors/not_found_error.rb diff --git a/decidim-accountability/lib/decidim/api/accountability_type.rb b/decidim-accountability/lib/decidim/api/accountability_type.rb index 7a583baf55fed..8a33b085c37ea 100644 --- a/decidim-accountability/lib/decidim/api/accountability_type.rb +++ b/decidim-accountability/lib/decidim/api/accountability_type.rb @@ -19,8 +19,8 @@ def results Result.where(component: object).includes(:component) end - def result(**args) - Result.where(component: object).find_by(id: args[:id]) + def result(id:) + Result.where(component: object).find(id) end def statuses @@ -28,7 +28,7 @@ def statuses end def status(id:) - Status.where(component: object).find_by(id:) + Status.where(component: object).find(id) end end end diff --git a/decidim-accountability/spec/types/accountability_type_spec.rb b/decidim-accountability/spec/types/accountability_type_spec.rb index ca3d338b2424e..0b53ecb8412ff 100644 --- a/decidim-accountability/spec/types/accountability_type_spec.rb +++ b/decidim-accountability/spec/types/accountability_type_spec.rb @@ -43,8 +43,8 @@ module Accountability context "when the result does not belong to the component" do let!(:result) { create(:result, component: create(:accountability_component)) } - it "returns null" do - expect(response["result"]).to be_nil + it "raises error" do + expect { response }.to raise_error(Decidim::Api::Errors::NotFoundError, "Result not found") end end end @@ -97,8 +97,8 @@ module Accountability context "when the status does not belong to the component" do let!(:status) { create(:status, component: create(:accountability_component)) } - it "returns null" do - expect(response["status"]).to be_nil + it "raises error" do + expect { response }.to raise_error(Decidim::Api::Errors::NotFoundError, "Status not found") end end end diff --git a/decidim-api/config/locales/en.yml b/decidim-api/config/locales/en.yml new file mode 100644 index 0000000000000..3ba96e5fd802a --- /dev/null +++ b/decidim-api/config/locales/en.yml @@ -0,0 +1,6 @@ +--- +en: + decidim: + api: + errors: + not_found: "%{type} not found" diff --git a/decidim-api/lib/decidim/api/errors/not_found_error.rb b/decidim-api/lib/decidim/api/errors/not_found_error.rb new file mode 100644 index 0000000000000..1bb1133858e79 --- /dev/null +++ b/decidim-api/lib/decidim/api/errors/not_found_error.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Decidim + module Api + module Errors + class NotFoundError < GraphQL::ExecutionError + def to_h + super.merge({ "extensions" => { "code" => "NOT_FOUND" } }) + end + end + end + end +end diff --git a/decidim-api/lib/decidim/api/schema.rb b/decidim-api/lib/decidim/api/schema.rb index 8a0905419bece..4e1966155246d 100644 --- a/decidim-api/lib/decidim/api/schema.rb +++ b/decidim-api/lib/decidim/api/schema.rb @@ -12,6 +12,10 @@ class Schema < GraphQL::Schema max_complexity Decidim::Api.schema_max_complexity orphan_types(Api.orphan_types) + + rescue_from(ActiveRecord::RecordNotFound) do |_err, _obj, _args, _ctx, field| + raise Decidim::Api::Errors::NotFoundError, I18n.t("decidim.api.errors.not_found", type: field.type.unwrap.graphql_name) + end end end end diff --git a/decidim-api/lib/decidim/api/test/component_context.rb b/decidim-api/lib/decidim/api/test/component_context.rb index dd1ce779078b6..6b38054853b69 100644 --- a/decidim-api/lib/decidim/api/test/component_context.rb +++ b/decidim-api/lib/decidim/api/test/component_context.rb @@ -42,6 +42,14 @@ end end +shared_examples "graphQL not found space" do + let(:space_type) { "participatoryProcess" } + + it "should not be visible" do + expect { response }.to raise_error(Decidim::Api::Errors::NotFoundError, "#{space_type.classify} not found") + end +end + shared_examples "with resource visibility" do let(:process_space_factory) { :participatory_process } let(:space_type) { "participatoryProcess" } @@ -52,12 +60,6 @@ end end - shared_examples "graphQL hidden space" do - it "should not be visible" do - expect(response[space_type]).to be_nil - end - end - shared_examples "graphQL hidden component" do it "should not be visible" do expect(response[space_type]["components"].first).to be_nil @@ -75,7 +77,7 @@ shared_examples "graphQL space hidden to visitor" do context "when user is visitor" do let!(:current_user) { nil } - it_behaves_like "graphQL hidden space" + it_behaves_like "graphQL not found space" end end @@ -308,33 +310,33 @@ context "when the user is space admin" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } let!(:role) { create(:participatory_process_user_role, participatory_process:, user: current_user, role: "admin") } - it_behaves_like "graphQL hidden space" + it_behaves_like "graphQL not found space" end context "when the user is space collaborator" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } let!(:role) { create(:participatory_process_user_role, participatory_process:, user: current_user, role: "collaborator") } - it_behaves_like "graphQL hidden space" + it_behaves_like "graphQL not found space" end context "when the user is space moderator" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } let!(:role) { create(:participatory_process_user_role, participatory_process:, user: current_user, role: "moderator") } - it_behaves_like "graphQL hidden space" + it_behaves_like "graphQL not found space" end context "when the user is space evaluator" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } let!(:role) { create(:participatory_process_user_role, participatory_process:, user: current_user, role: "evaluator") } - it_behaves_like "graphQL hidden space" + it_behaves_like "graphQL not found space" end it_behaves_like "graphQL space hidden to visitor" context "when user is normal user" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } - it_behaves_like "graphQL hidden space" + it_behaves_like "graphQL not found space" end context "when user is member" do @@ -352,25 +354,25 @@ context "when the user is space admin" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } let!(:role) { create(:participatory_process_user_role, participatory_process:, user: current_user, role: "admin") } - it_behaves_like "graphQL hidden space" + it_behaves_like "graphQL not found space" end context "when the user is space collaborator" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } let!(:role) { create(:participatory_process_user_role, participatory_process:, user: current_user, role: "collaborator") } - it_behaves_like "graphQL hidden space" + it_behaves_like "graphQL not found space" end context "when the user is space moderator" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } let!(:role) { create(:participatory_process_user_role, participatory_process:, user: current_user, role: "moderator") } - it_behaves_like "graphQL hidden space" + it_behaves_like "graphQL not found space" end context "when the user is space evaluator" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } let!(:role) { create(:participatory_process_user_role, participatory_process:, user: current_user, role: "evaluator") } - it_behaves_like "graphQL hidden space" + it_behaves_like "graphQL not found space" end it_behaves_like "graphQL space hidden to visitor" @@ -381,7 +383,7 @@ end context "when user is normal user" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } - it_behaves_like "graphQL hidden space" + it_behaves_like "graphQL not found space" end end end @@ -397,25 +399,25 @@ context "when the user is space admin" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } let!(:role) { create(:participatory_process_user_role, participatory_process:, user: current_user, role: "admin") } - it_behaves_like "graphQL hidden space" + it_behaves_like "graphQL not found space" end context "when the user is space collaborator" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } let!(:role) { create(:participatory_process_user_role, participatory_process:, user: current_user, role: "collaborator") } - it_behaves_like "graphQL hidden space" + it_behaves_like "graphQL not found space" end context "when the user is space moderator" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } let!(:role) { create(:participatory_process_user_role, participatory_process:, user: current_user, role: "moderator") } - it_behaves_like "graphQL hidden space" + it_behaves_like "graphQL not found space" end context "when the user is space evaluator" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } let!(:role) { create(:participatory_process_user_role, participatory_process:, user: current_user, role: "evaluator") } - it_behaves_like "graphQL hidden space" + it_behaves_like "graphQL not found space" end it_behaves_like "graphQL space hidden to visitor" @@ -423,12 +425,12 @@ context "when user is member" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } let!(:participatory_space_private_user) { create(:participatory_space_private_user, user: current_user, privatable_to: participatory_process) } - it_behaves_like "graphQL hidden space" + it_behaves_like "graphQL not found space" end context "when user is normal user" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } - it_behaves_like "graphQL hidden space" + it_behaves_like "graphQL not found space" end end @@ -440,38 +442,38 @@ context "when the user is space admin" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } let!(:role) { create(:participatory_process_user_role, participatory_process:, user: current_user, role: "admin") } - it_behaves_like "graphQL hidden space" + it_behaves_like "graphQL not found space" end context "when the user is space collaborator" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } let!(:role) { create(:participatory_process_user_role, participatory_process:, user: current_user, role: "collaborator") } - it_behaves_like "graphQL hidden space" + it_behaves_like "graphQL not found space" end context "when the user is space moderator" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } let!(:role) { create(:participatory_process_user_role, participatory_process:, user: current_user, role: "moderator") } - it_behaves_like "graphQL hidden space" + it_behaves_like "graphQL not found space" end context "when the user is space evaluator" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } let!(:role) { create(:participatory_process_user_role, participatory_process:, user: current_user, role: "evaluator") } - it_behaves_like "graphQL hidden space" + it_behaves_like "graphQL not found space" end it_behaves_like "graphQL space hidden to visitor" context "when user is member" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } let!(:participatory_space_private_user) { create(:participatory_space_private_user, user: current_user, privatable_to: participatory_process) } - it_behaves_like "graphQL hidden space" + it_behaves_like "graphQL not found space" end context "when user is normal user" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } - it_behaves_like "graphQL hidden space" + it_behaves_like "graphQL not found space" end end end diff --git a/decidim-api/lib/decidim/api/test/type_context.rb b/decidim-api/lib/decidim/api/test/type_context.rb index dd1b52c7ddf7d..aa6304ce16538 100644 --- a/decidim-api/lib/decidim/api/test/type_context.rb +++ b/decidim-api/lib/decidim/api/test/type_context.rb @@ -28,6 +28,17 @@ execute_query query, variables.stringify_keys end + def raise_proper_error(error) + code = error.dig("extensions", "code") + + case code + when "NOT_FOUND" + raise Decidim::Api::Errors::NotFoundError, error["message"] + else + raise StandardError, error["message"] + end + end + def execute_query(query, variables) result = schema.execute( query, @@ -41,7 +52,7 @@ def execute_query(query, variables) variables: ) - raise StandardError, result["errors"].map { |e| e["message"] }.join(", ") if result["errors"] + raise_proper_error(result["errors"].first) if result["errors"] result["data"] end diff --git a/decidim-api/lib/decidim/api/types.rb b/decidim-api/lib/decidim/api/types.rb index d179b78ccf7bc..4e62516a17ccf 100644 --- a/decidim-api/lib/decidim/api/types.rb +++ b/decidim-api/lib/decidim/api/types.rb @@ -9,6 +9,10 @@ module Api autoload :GraphqlPermissions, "decidim/api/graphql_permissions" autoload :ComponentMutationType, "decidim/api/component_mutation_type" + module Errors + autoload :NotFoundError, "decidim/api/errors/not_found_error" + end + module Types autoload :BaseArgument, "decidim/api/types/base_argument" autoload :BaseEnum, "decidim/api/types/base_enum" diff --git a/decidim-assemblies/spec/lib/query_extensions_spec.rb b/decidim-assemblies/spec/lib/query_extensions_spec.rb index 9262797848e0b..c412b88da37d6 100644 --- a/decidim-assemblies/spec/lib/query_extensions_spec.rb +++ b/decidim-assemblies/spec/lib/query_extensions_spec.rb @@ -38,8 +38,8 @@ module Core let!(:assembly) { create(:assembly) } let(:id) { assembly.id } - it "returns nil" do - expect(response["assembly"]).to be_nil + it_behaves_like "graphQL not found space" do + let(:space_type) { "assembly" } end end end diff --git a/decidim-blogs/spec/types/blogs_type_spec.rb b/decidim-blogs/spec/types/blogs_type_spec.rb index 5a9ae0829a6e9..3c0ae899a9e9f 100644 --- a/decidim-blogs/spec/types/blogs_type_spec.rb +++ b/decidim-blogs/spec/types/blogs_type_spec.rb @@ -39,8 +39,8 @@ module Blogs context "when the post does not belong to the component" do let!(:post) { create(:post, component: create(:post_component)) } - it "returns null" do - expect(response["post"]).to be_nil + it "raises error" do + expect { response }.to raise_error(Decidim::Api::Errors::NotFoundError, "Post not found") end end end diff --git a/decidim-budgets/lib/decidim/api/budgets_type.rb b/decidim-budgets/lib/decidim/api/budgets_type.rb index f2e4ff44503dc..f54ed84eaf134 100644 --- a/decidim-budgets/lib/decidim/api/budgets_type.rb +++ b/decidim-budgets/lib/decidim/api/budgets_type.rb @@ -15,8 +15,8 @@ def budgets Budget.where(component: object).includes(:component) end - def budget(**args) - Budget.where(component: object).find_by(id: args[:id]) + def budget(id:) + Decidim::Core::ComponentFinderBase.new(model_class: Budget).call(object, { id: }, context) end end end diff --git a/decidim-budgets/spec/types/budgets_type_spec.rb b/decidim-budgets/spec/types/budgets_type_spec.rb index 46b62664470c9..1aba5e98f8454 100644 --- a/decidim-budgets/spec/types/budgets_type_spec.rb +++ b/decidim-budgets/spec/types/budgets_type_spec.rb @@ -40,8 +40,8 @@ module Budgets context "when the budget does not belong to the component" do let!(:budget) { create(:budget, component: create(:budgets_component)) } - it "returns null" do - expect(response["budget"]).to be_nil + it "raises error" do + expect { response }.to raise_error(Decidim::Api::Errors::NotFoundError, "Budget not found") end end end diff --git a/decidim-budgets/spec/types/integration_schema_spec.rb b/decidim-budgets/spec/types/integration_schema_spec.rb index d164389d7087d..21d1fc4222ccd 100644 --- a/decidim-budgets/spec/types/integration_schema_spec.rb +++ b/decidim-budgets/spec/types/integration_schema_spec.rb @@ -335,17 +335,13 @@ context "when user is visitor" do let!(:current_user) { nil } - it "should not be visible" do - expect(response["participatoryProcess"]).to be_nil - end + it_behaves_like "graphQL not found space" end context "when user is normal user" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } - it "should not be visible" do - expect(response["participatoryProcess"]).to be_nil - end + it_behaves_like "graphQL not found space" end end @@ -363,17 +359,13 @@ context "when user is visitor" do let!(:current_user) { nil } - it "should not be visible" do - expect(response["participatoryProcess"]).to be_nil - end + it_behaves_like "graphQL not found space" end context "when user is normal user" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } - it "should not be visible" do - expect(response["participatoryProcess"]).to be_nil - end + it_behaves_like "graphQL not found space" end end end @@ -522,17 +514,13 @@ context "when user is visitor" do let!(:current_user) { nil } - it "should not be visible" do - expect(response["participatoryProcess"]).to be_nil - end + it_behaves_like "graphQL not found space" end context "when user is normal user" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } - it "should not be visible" do - expect(response["participatoryProcess"]).to be_nil - end + it_behaves_like "graphQL not found space" end end @@ -550,17 +538,13 @@ context "when user is visitor" do let!(:current_user) { nil } - it "should not be visible" do - expect(response["participatoryProcess"]).to be_nil - end + it_behaves_like "graphQL not found space" end context "when user is normal user" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } - it "should not be visible" do - expect(response["participatoryProcess"]).to be_nil - end + it_behaves_like "graphQL not found space" end end end diff --git a/decidim-collaborative_texts/spec/types/documents_type_spec.rb b/decidim-collaborative_texts/spec/types/documents_type_spec.rb index ad8b42dd8ec39..cad1e4492b98d 100644 --- a/decidim-collaborative_texts/spec/types/documents_type_spec.rb +++ b/decidim-collaborative_texts/spec/types/documents_type_spec.rb @@ -39,8 +39,8 @@ module CollaborativeTexts context "when the document does not belong to the component" do let!(:document) { create(:collaborative_text_document, :published, component: create(:collaborative_text_component)) } - it "returns null" do - expect(response["collaborativeText"]).to be_nil + it "raises error" do + expect { response }.to raise_error(Decidim::Api::Errors::NotFoundError, "CollaborativeText not found") end end end diff --git a/decidim-conferences/spec/lib/decidim/conferences/query_extensions_spec.rb b/decidim-conferences/spec/lib/decidim/conferences/query_extensions_spec.rb index 741da0678c957..2ebb49d9f1aa7 100644 --- a/decidim-conferences/spec/lib/decidim/conferences/query_extensions_spec.rb +++ b/decidim-conferences/spec/lib/decidim/conferences/query_extensions_spec.rb @@ -38,8 +38,8 @@ module Conferences let!(:conference) { create(:conference) } let(:id) { conference.id } - it "returns nil" do - expect(response["conference"]).to be_nil + it_behaves_like "graphQL not found space" do + let(:space_type) { "conference" } end end end diff --git a/decidim-core/lib/decidim/api/functions/component_finder_base.rb b/decidim-core/lib/decidim/api/functions/component_finder_base.rb index 7d15191e5773d..f855a207fc129 100644 --- a/decidim-core/lib/decidim/api/functions/component_finder_base.rb +++ b/decidim-core/lib/decidim/api/functions/component_finder_base.rb @@ -26,7 +26,7 @@ def call(component, args, _ctx) args.keys.each do |key| @query[key] = args[key] end - query_scope.find_by(@query) + query_scope.find_by!(@query) end # By default, any model in its default scope diff --git a/decidim-core/lib/decidim/api/functions/participatory_space_finder_base.rb b/decidim-core/lib/decidim/api/functions/participatory_space_finder_base.rb index 192d76de1b262..1c56e87dc864a 100644 --- a/decidim-core/lib/decidim/api/functions/participatory_space_finder_base.rb +++ b/decidim-core/lib/decidim/api/functions/participatory_space_finder_base.rb @@ -32,7 +32,7 @@ def call(_obj, args, ctx) model_class.public_spaces end - @query.find_by(query) + @query.find_by!(query) end end end diff --git a/decidim-core/lib/decidim/query_extensions.rb b/decidim-core/lib/decidim/query_extensions.rb index 323eb3a02c605..660eddeb10ee1 100644 --- a/decidim-core/lib/decidim/query_extensions.rb +++ b/decidim-core/lib/decidim/query_extensions.rb @@ -87,7 +87,7 @@ def participant_details(id: nil, nickname: nil) end def static_pages - Decidim::StaticPage.accessible_for(context[:current_organization], context[:current_user]) + Decidim::StaticPage.accessible_for(organization, context[:current_user]) end def static_page_topics @@ -95,11 +95,11 @@ def static_page_topics end def moderated_users - Decidim::UserModeration.joins(:user).where(decidim_users: { decidim_organization_id: context[:current_organization]&.id }).where.not(decidim_users: { blocked_at: nil }) + Decidim::UserModeration.joins(:user).where(decidim_users: { decidim_organization_id: organization&.id }).where.not(decidim_users: { blocked_at: nil }) end def moderations - Decidim::Moderation.where(participatory_space: context[:current_organization].participatory_spaces).includes(:reports).hidden + Decidim::Moderation.where(participatory_space: organization.participatory_spaces).includes(:reports).hidden end end end diff --git a/decidim-debates/lib/decidim/api/debates_type.rb b/decidim-debates/lib/decidim/api/debates_type.rb index af65b858ec16c..c2d8991076d57 100644 --- a/decidim-debates/lib/decidim/api/debates_type.rb +++ b/decidim-debates/lib/decidim/api/debates_type.rb @@ -15,8 +15,8 @@ def debates Debate.where(component: object).includes(:component) end - def debate(**args) - Debate.where(component: object).find_by(id: args[:id]) + def debate(id:) + Decidim::Core::ComponentFinderBase.new(model_class: Debate).call(object, { id: }, context) end end end diff --git a/decidim-debates/spec/types/debates_type_spec.rb b/decidim-debates/spec/types/debates_type_spec.rb index 5b6fe5ae19d36..761a5ee5b748f 100644 --- a/decidim-debates/spec/types/debates_type_spec.rb +++ b/decidim-debates/spec/types/debates_type_spec.rb @@ -39,8 +39,8 @@ module Debates context "when the debate does not belong to the component" do let(:debate) { create(:debate, component: create(:debates_component)) } - it "does not find the debate" do - expect(response["debate"]).to be_nil + it "raises error" do + expect { response }.to raise_error(Decidim::Api::Errors::NotFoundError, "Debate not found") end end end diff --git a/decidim-dev/config/rubocop/graphql/configuration.yml b/decidim-dev/config/rubocop/graphql/configuration.yml index 113e2a9bb2e6b..11b7860cf4a37 100644 --- a/decidim-dev/config/rubocop/graphql/configuration.yml +++ b/decidim-dev/config/rubocop/graphql/configuration.yml @@ -17,6 +17,7 @@ GraphQL/ObjectDescription: - "**/lib/decidim/api/graphiql/config.rb" - "**/lib/decidim/api/graphql_permissions.rb" - "**/lib/decidim/api/functions/*" + - "**/lib/decidim/api/errors/*" - "spec/**/*" - "test/**/*" diff --git a/decidim-initiatives/lib/decidim/initiatives/query_extensions.rb b/decidim-initiatives/lib/decidim/initiatives/query_extensions.rb index 7fa586154a42d..7a3c9977d53f9 100644 --- a/decidim-initiatives/lib/decidim/initiatives/query_extensions.rb +++ b/decidim-initiatives/lib/decidim/initiatives/query_extensions.rb @@ -36,16 +36,11 @@ def self.included(type) end def initiatives_types - Decidim::InitiativesType.where( - organization: context[:current_organization] - ) + Decidim::InitiativesType.where(organization:) end def initiatives_type(id:) - Decidim::InitiativesType.find_by( - organization: context[:current_organization], - id: - ) + Decidim::InitiativesType.find_by!(organization:, id:) end def initiatives(filter: {}, order: {}) diff --git a/decidim-initiatives/spec/lib/query_extensions_spec.rb b/decidim-initiatives/spec/lib/query_extensions_spec.rb index 60caf695ab075..4c4229d01434b 100644 --- a/decidim-initiatives/spec/lib/query_extensions_spec.rb +++ b/decidim-initiatives/spec/lib/query_extensions_spec.rb @@ -57,12 +57,12 @@ module Initiatives end end - context "with a conference of another organization" do + context "with an initiative of another organization" do let!(:initiative) { create(:initiative) } let(:id) { initiative.id } - it "returns nil" do - expect(response["initiative"]).to be_nil + it_behaves_like "graphQL not found space" do + let(:space_type) { "initiative" } end end end diff --git a/decidim-meetings/lib/decidim/api/meetings_type.rb b/decidim-meetings/lib/decidim/api/meetings_type.rb index 8a174d7c6f76b..10ee36a31d922 100644 --- a/decidim-meetings/lib/decidim/api/meetings_type.rb +++ b/decidim-meetings/lib/decidim/api/meetings_type.rb @@ -15,8 +15,8 @@ def meetings Meeting.published.visible.where(component: object).includes(:component) end - def meeting(**args) - Meeting.published.visible.where(component: object).find_by(id: args[:id]) + def meeting(id:) + Decidim::Core::ComponentFinderBase.new(model_class: Meeting.published.visible).call(object, { id: }, context) end end end diff --git a/decidim-meetings/spec/types/meetings_type_spec.rb b/decidim-meetings/spec/types/meetings_type_spec.rb index 5359d84961ed7..c76def5d4e9d2 100644 --- a/decidim-meetings/spec/types/meetings_type_spec.rb +++ b/decidim-meetings/spec/types/meetings_type_spec.rb @@ -68,16 +68,16 @@ module Meetings context "when the meeting does not belong to the component" do let!(:meeting) { create(:meeting, :published, component: create(:meeting_component)) } - it "returns null" do - expect(response["meeting"]).to be_nil + it "raises error" do + expect { response }.to raise_error(Decidim::Api::Errors::NotFoundError, "Meeting not found") end end context "when private" do let!(:meeting) { create(:meeting, :published, component: model, private_meeting: true, transparent: false) } - it "returns null" do - expect(response["meeting"]).to be_nil + it "raises error" do + expect { response }.to raise_error(Decidim::Api::Errors::NotFoundError, "Meeting not found") end end end diff --git a/decidim-pages/lib/decidim/api/pages_type.rb b/decidim-pages/lib/decidim/api/pages_type.rb index 77ea46fe7b191..c52ec596c4ca9 100644 --- a/decidim-pages/lib/decidim/api/pages_type.rb +++ b/decidim-pages/lib/decidim/api/pages_type.rb @@ -15,8 +15,8 @@ def pages Page.where(component: object).includes(:component) end - def page(**args) - Page.where(component: object).find_by(id: args[:id]) + def page(id:) + Decidim::Core::ComponentFinderBase.new(model_class: Page).call(object, { id: }, context) end end end diff --git a/decidim-pages/spec/types/pages_type_spec.rb b/decidim-pages/spec/types/pages_type_spec.rb index f3ebaa8a820ae..b59fd41523b6c 100644 --- a/decidim-pages/spec/types/pages_type_spec.rb +++ b/decidim-pages/spec/types/pages_type_spec.rb @@ -39,8 +39,8 @@ module Pages context "when the page does not belong to the component" do let!(:page) { create(:page, component: create(:page_component)) } - it "returns null" do - expect(response["page"]).to be_nil + it "raises error" do + expect { response }.to raise_error(Decidim::Api::Errors::NotFoundError, "Page not found") end end end diff --git a/decidim-participatory_processes/lib/decidim/participatory_processes/query_extensions.rb b/decidim-participatory_processes/lib/decidim/participatory_processes/query_extensions.rb index 889c02d350e6c..771ffd6037962 100644 --- a/decidim-participatory_processes/lib/decidim/participatory_processes/query_extensions.rb +++ b/decidim-participatory_processes/lib/decidim/participatory_processes/query_extensions.rb @@ -57,29 +57,19 @@ def participatory_process(id: nil, slug: nil) end def participatory_process_groups(*) - Decidim::ParticipatoryProcessGroup.where( - organization: context[:current_organization] - ) + Decidim::ParticipatoryProcessGroup.where(organization:) end def participatory_process_group(id:) - Decidim::ParticipatoryProcessGroup.find_by( - organization: context[:current_organization], - id: - ) + Decidim::ParticipatoryProcessGroup.where(organization:).find(id) end def participatory_process_types(*) - Decidim::ParticipatoryProcessType.where( - organization: context[:current_organization] - ) + Decidim::ParticipatoryProcessType.where(organization:) end def participatory_process_type(id:) - Decidim::ParticipatoryProcessType.find_by( - organization: context[:current_organization], - id: - ) + Decidim::ParticipatoryProcessType.where(organization:).find(id) end end end diff --git a/decidim-participatory_processes/spec/lib/query_extensions_spec.rb b/decidim-participatory_processes/spec/lib/query_extensions_spec.rb index 44b524a926df9..e425abb6e565e 100644 --- a/decidim-participatory_processes/spec/lib/query_extensions_spec.rb +++ b/decidim-participatory_processes/spec/lib/query_extensions_spec.rb @@ -41,9 +41,7 @@ module Core let!(:process) { create(:participatory_process) } let(:id) { process.id } - it "returns nil" do - expect(response["participatoryProcess"]).to be_nil - end + it_behaves_like "graphQL not found space" end end @@ -77,8 +75,8 @@ module Core let!(:process) { create(:participatory_process_group, :with_participatory_processes) } let(:id) { process.id } - it "returns nil" do - expect(response["participatoryProcessGroup"]).to be_nil + it "should not be visible" do + expect { response }.to raise_error(Decidim::Api::Errors::NotFoundError, "ParticipatoryProcessGroup not found") end end end @@ -113,8 +111,8 @@ module Core let!(:type) { create(:participatory_process_type, :with_active_participatory_processes) } let(:id) { type.id } - it "returns nil" do - expect(response["participatoryProcessType"]).to be_nil + it "should not be found" do + expect { response }.to raise_error(Decidim::Api::Errors::NotFoundError, "ParticipatoryProcessType not found") end end end diff --git a/decidim-proposals/spec/types/proposals_type_spec.rb b/decidim-proposals/spec/types/proposals_type_spec.rb index aab72b587bf09..6184f3fdb5e29 100644 --- a/decidim-proposals/spec/types/proposals_type_spec.rb +++ b/decidim-proposals/spec/types/proposals_type_spec.rb @@ -58,16 +58,16 @@ module Proposals context "when the proposal does not belong to the component" do let!(:proposal) { create(:proposal, component: create(:proposal_component)) } - it "returns null" do - expect(response["proposal"]).to be_nil + it "raises error" do + expect { response }.to raise_error(Decidim::Api::Errors::NotFoundError, "Proposal not found") end end context "when the proposal is not published" do let!(:proposal) { create(:proposal, :draft, component: model) } - it "returns null" do - expect(response["proposal"]).to be_nil + it "raises error" do + expect { response }.to raise_error(Decidim::Api::Errors::NotFoundError, "Proposal not found") end end end diff --git a/decidim-surveys/lib/decidim/api/surveys_type.rb b/decidim-surveys/lib/decidim/api/surveys_type.rb index b7727082cab53..89ae53eb48146 100644 --- a/decidim-surveys/lib/decidim/api/surveys_type.rb +++ b/decidim-surveys/lib/decidim/api/surveys_type.rb @@ -15,8 +15,8 @@ def surveys Survey.where(component: object).includes(:component) end - def survey(**args) - Survey.where(component: object).find_by(id: args[:id]) + def survey(id:) + Survey.published.where(component: object).find(id) end end end diff --git a/decidim-surveys/spec/types/integration_schema_spec.rb b/decidim-surveys/spec/types/integration_schema_spec.rb index 91472d1118763..5f27b5ffa8032 100644 --- a/decidim-surveys/spec/types/integration_schema_spec.rb +++ b/decidim-surveys/spec/types/integration_schema_spec.rb @@ -62,7 +62,7 @@ let(:component_type) { "Surveys" } let!(:current_component) { create(:surveys_component, participatory_space: participatory_process) } - let!(:survey) { create(:survey, component: current_component) } + let!(:survey) { create(:survey, :published, component: current_component) } let!(:questionnaire) { create(:questionnaire, :with_questions, questionnaire_for: survey) } let(:survey_single_result) do diff --git a/decidim-surveys/spec/types/surveys_type_spec.rb b/decidim-surveys/spec/types/surveys_type_spec.rb index 4913a741d69f8..cf8a5aa4a3732 100644 --- a/decidim-surveys/spec/types/surveys_type_spec.rb +++ b/decidim-surveys/spec/types/surveys_type_spec.rb @@ -12,8 +12,8 @@ module Surveys it_behaves_like "a component query type" describe "surveys" do - let!(:component_surveys) { create_list(:survey, 2, component: model) } - let!(:other_surveys) { create_list(:survey, 2) } + let!(:component_surveys) { create_list(:survey, 2, :published, component: model) } + let!(:other_surveys) { create_list(:survey, 2, :published) } let(:query) { "{ surveys { edges { node { id } } } }" } @@ -29,7 +29,7 @@ module Surveys let(:variables) { { id: survey.id.to_s } } context "when the survey belongs to the component" do - let!(:survey) { create(:survey, component: model) } + let!(:survey) { create(:survey, :published, component: model) } it "finds the survey" do expect(response["survey"]["id"]).to eq(survey.id.to_s) @@ -37,10 +37,10 @@ module Surveys end context "when the survey does not belong to the component" do - let!(:survey) { create(:survey, component: create(:surveys_component)) } + let!(:survey) { create(:survey, :published, component: create(:surveys_component)) } - it "returns null" do - expect(response["survey"]).to be_nil + it "raises error" do + expect { response }.to raise_error(Decidim::Api::Errors::NotFoundError, "Survey not found") end end end From a0841c2f8b38308d1ee85d4452b498a864d68d3e Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Thu, 11 Dec 2025 10:52:06 +0200 Subject: [PATCH 009/116] Fix NuValidator image (#15773) --- .github/workflows/test_app.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_app.yml b/.github/workflows/test_app.yml index 58132b6431b16..69849a2b5b2f5 100644 --- a/.github/workflows/test_app.yml +++ b/.github/workflows/test_app.yml @@ -54,7 +54,7 @@ jobs: PARALLEL_TEST_PROCESSORS: 3 services: validator: - image: ghcr.io/validator/validator:latest + image: ghcr.io/validator/validator@sha256:7667b0ffa6d395c27aa8f9e21db1cfe6b66549245a3972e9397d255de5ef0ec6 ports: ["8888:8888"] postgres: image: postgres:14 From f72c523c16d734cac7be916fa74f37ae29196b39 Mon Sep 17 00:00:00 2001 From: Anna Topalidi <60363870+antopalidi@users.noreply.github.com> Date: Thu, 11 Dec 2025 13:21:12 +0100 Subject: [PATCH 010/116] Change Vote button to Edit vote when user has already voted (#15761) --- .../decidim/elections/elections/_election_aside.html.erb | 3 ++- decidim-elections/config/locales/en.yml | 1 + .../decidim/elections/test/per_question_vote_examples.rb | 6 +++--- .../lib/decidim/elections/test/vote_examples.rb | 8 +++++--- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/decidim-elections/app/views/decidim/elections/elections/_election_aside.html.erb b/decidim-elections/app/views/decidim/elections/elections/_election_aside.html.erb index 0826ccd0bb05c..91442a6932dc0 100644 --- a/decidim-elections/app/views/decidim/elections/elections/_election_aside.html.erb +++ b/decidim-elections/app/views/decidim/elections/elections/_election_aside.html.erb @@ -1,7 +1,8 @@
<% if election.ongoing? %> - <%= link_to t("vote_button", scope: "decidim.elections.elections.show"), new_election_vote_path(election), class: "button button__secondary button__lg" %> + <% button_key = voted_by_current_user?(election) ? "edit_vote_button" : "vote_button" %> + <%= link_to t(button_key, scope: "decidim.elections.elections.show"), new_election_vote_path(election), class: "button button__secondary button__lg" %> <% if voted_by_current_user?(election) %> diff --git a/decidim-elections/app/views/decidim/elections/elections/_vote_results_question.html.erb b/decidim-elections/app/views/decidim/elections/elections/_vote_results_question.html.erb index a16df46eedbc4..5c439fd5e8bf4 100644 --- a/decidim-elections/app/views/decidim/elections/elections/_vote_results_question.html.erb +++ b/decidim-elections/app/views/decidim/elections/elections/_vote_results_question.html.erb @@ -9,5 +9,9 @@ <% question.response_options.each_with_index do |option, index| %> <%= render "decidim/elections/elections/vote_results_option", option: %> <% end %> +
+ <%= t("decidim.elections.elections.vote_results.total") %> + <%= t("total_votes", scope: "decidim.elections.elections.vote_results", count: question.total_votes) %> +
diff --git a/decidim-elections/config/locales/en.yml b/decidim-elections/config/locales/en.yml index a452c22a8b96a..d95f3c3c9a7fa 100644 --- a/decidim-elections/config/locales/en.yml +++ b/decidim-elections/config/locales/en.yml @@ -82,6 +82,7 @@ en: questions_table: answer: Answers percentage: Percentage + total: Total votes: Votes votes_count: one: 1 vote @@ -270,6 +271,10 @@ en: per_question: Results are available per question. You can see the results for each question after voting is enabled and results are published. real_time: Results are available in real time. You can see the results while voting is in progress. title: Results + total: 'TOTAL:' + total_votes: + one: 1 vote + other: "%{count} votes" models: election: fields: diff --git a/decidim-elections/spec/controllers/decidim/elections/admin/elections_controller_spec.rb b/decidim-elections/spec/controllers/decidim/elections/admin/elections_controller_spec.rb index 61289d07ec63a..eadcfcb5697c1 100644 --- a/decidim-elections/spec/controllers/decidim/elections/admin/elections_controller_spec.rb +++ b/decidim-elections/spec/controllers/decidim/elections/admin/elections_controller_spec.rb @@ -117,6 +117,8 @@ def dashboard_path(election) "position" => election_question.position, "voting_enabled" => false, "published_results" => false, + "total_votes" => 0, + "total_votes_text" => "0 votes", "response_options" => election_question.response_options.map do |ro| { "id" => ro.id, diff --git a/decidim-elections/spec/presenters/decidim/elections/election_presenter_spec.rb b/decidim-elections/spec/presenters/decidim/elections/election_presenter_spec.rb index eb2e85c5f39e8..11ea9610f36f0 100644 --- a/decidim-elections/spec/presenters/decidim/elections/election_presenter_spec.rb +++ b/decidim-elections/spec/presenters/decidim/elections/election_presenter_spec.rb @@ -43,6 +43,45 @@ module Elections it { expect(presenter.title).to be_nil } it { expect(presenter.election_path).to be_nil } end + + describe "#to_json" do + let(:election) { create(:election, :published, :real_time, :ongoing, component:) } + let!(:question) { create(:election_question, :with_response_options, election:) } + let!(:vote) { create(:election_vote, question:, response_option: question.response_options.first, voter_uid: "voter1") } + + context "when admin is true" do + subject(:json) { presenter.to_json(admin: true) } + + it "includes total_votes for each question" do + question_json = json[:questions].find { |q| q[:id] == question.id } + expect(question_json[:total_votes]).to eq(1) + expect(question_json[:total_votes_text]).to eq("1 vote") + end + end + + context "when admin is false and results are published" do + subject(:json) { presenter.to_json(admin: false) } + + it "includes total_votes for questions with published results" do + question_json = json[:questions].find { |q| q[:id] == question.id } + expect(question_json[:total_votes]).to eq(1) + expect(question_json[:total_votes_text]).to eq("1 vote") + end + end + + context "when admin is false and results are not published" do + let(:election) { create(:election, :published, :per_question, :ongoing, component:) } + let!(:question) { create(:election_question, :with_response_options, election:, voting_enabled_at: Time.current, published_results_at: nil) } + + subject(:json) { presenter.to_json(admin: false) } + + it "does not include total_votes for questions without published results" do + question_json = json[:questions].find { |q| q[:id] == question.id } + expect(question_json).not_to have_key(:total_votes) + expect(question_json).not_to have_key(:total_votes_text) + end + end + end end end end diff --git a/decidim-elections/spec/system/admin/dashboard_spec.rb b/decidim-elections/spec/system/admin/dashboard_spec.rb index 8cebc33cd646e..067380a52a7c3 100644 --- a/decidim-elections/spec/system/admin/dashboard_spec.rb +++ b/decidim-elections/spec/system/admin/dashboard_spec.rb @@ -133,6 +133,27 @@ expect(page).to have_no_content("Election has not started yet.") expect(page).to have_no_content("Publish results") end + + it "shows total votes for each question" do + questions.each do |question| + within("#question_#{question.id}") do + expect(page).to have_content("Total") + expect(page).to have_css("[data-question-total-votes-text='#{question.id}']", text: "0 votes") + end + end + end + + context "when there are votes" do + let!(:questions) { create_list(:election_question, 3, :with_response_options, election:) } + let!(:vote) { create(:election_vote, question: questions.first, response_option: questions.first.response_options.first, voter_uid: "voter1") } + + it "shows the correct total votes count" do + visit election_dashboard_path + within("#question_#{questions.first.id}") do + expect(page).to have_css("[data-question-total-votes-text='#{questions.first.id}']", text: "1 vote") + end + end + end end end @@ -176,6 +197,15 @@ expect(page).to have_button("Enable voting", count: 3, disabled: false) end + it "shows total votes for each question" do + questions.each do |question| + within("#question_#{question.id}") do + expect(page).to have_content("Total") + expect(page).to have_css("[data-question-total-votes-text='#{question.id}']", text: "0 votes") + end + end + end + context "when a question is enabled" do before do within("#question_#{first_question.id}") do @@ -262,6 +292,15 @@ expect(page).to have_no_content("Election has not started yet.") expect(page).to have_button("Publish results") end + + it "shows total votes for each question" do + questions.each do |question| + within("#question_#{question.id}") do + expect(page).to have_content("Total") + expect(page).to have_css("[data-question-total-votes-text='#{question.id}']", text: "0 votes") + end + end + end end end @@ -272,6 +311,15 @@ expect(page).to have_content("Finished") expect(page).to have_no_button("Results published at") end + + it "shows total votes for each question" do + questions.each do |question| + within("#question_#{question.id}") do + expect(page).to have_content("Total") + expect(page).to have_css("[data-question-total-votes-text='#{question.id}']", text: "0 votes") + end + end + end end private diff --git a/decidim-elections/spec/system/election_results_spec.rb b/decidim-elections/spec/system/election_results_spec.rb index 0653401acf4d2..db988ea0edb09 100644 --- a/decidim-elections/spec/system/election_results_spec.rb +++ b/decidim-elections/spec/system/election_results_spec.rb @@ -32,6 +32,11 @@ def expect_vote_count(question, option, count) expect(div).to have_content("#{count} vote") end + def expect_total_votes(question, count) + div = find("[data-question-total-votes-text='#{question.id}']") + expect(div).to have_content("#{count} vote") + end + it "shows the results" do expect(page).to have_content("Results") within "#question-#{question1.id}" do @@ -53,6 +58,8 @@ def expect_vote_count(question, option, count) expect_vote_count(question2, option21, "0") expect_vote_percent(question2, option22, "0.0%") expect_vote_count(question2, option22, "0") + expect_total_votes(question1, "0") + expect_total_votes(question2, "0") create(:election_vote, question: question1, voter_uid: "voter1", response_option: option11) create(:election_vote, question: question2, voter_uid: "voter1", response_option: option22) @@ -66,6 +73,8 @@ def expect_vote_count(question, option, count) expect_vote_count(question2, option21, "0") expect_vote_percent(question2, option22, "100.0%") expect_vote_count(question2, option22, "1") + expect_total_votes(question1, "1") + expect_total_votes(question2, "1") end context "when the election is per question" do @@ -100,6 +109,7 @@ def expect_vote_count(question, option, count) expect_vote_count(question1, option11, "0") expect_vote_percent(question1, option12, "0.0%") expect_vote_count(question1, option12, "0") + expect_total_votes(question1, "0") question1.update(published_results_at: Time.current) question2.update(voting_enabled_at: Time.current) @@ -112,6 +122,7 @@ def expect_vote_count(question, option, count) expect_vote_count(question1, option11, "1") expect_vote_percent(question1, option12, "0.0%") expect_vote_count(question1, option12, "0") + expect_total_votes(question1, "1") expect(page).to have_no_selector("#question-#{question2.id}") question2.update(published_results_at: Time.current) @@ -125,6 +136,7 @@ def expect_vote_count(question, option, count) expect_vote_count(question2, option21, "1") expect_vote_percent(question2, option22, "0.0%") expect_vote_count(question2, option22, "0") + expect_total_votes(question2, "1") end end end From 15dbb71da511ff14cd6d116fb76bb3d24e2fab16 Mon Sep 17 00:00:00 2001 From: Anna Topalidi <60363870+antopalidi@users.noreply.github.com> Date: Thu, 11 Dec 2025 13:40:10 +0100 Subject: [PATCH 012/116] Allow editing last question on receipt page for per-question elections (#15742) --- .../decidim/elections/uses_votes_booth.rb | 10 ++- .../per_question_votes_controller.rb | 8 +++ .../decidim/elections/votes/receipt.html.erb | 19 ++++- decidim-elections/config/locales/en.yml | 3 +- .../test/per_question_vote_examples.rb | 69 +++++++++++++++++++ .../per_question_votes_controller_spec.rb | 29 ++++++-- .../elections/votes_controller_spec.rb | 11 ++- ..._votes_in_an_per_question_election_spec.rb | 13 ++++ 8 files changed, 150 insertions(+), 12 deletions(-) diff --git a/decidim-elections/app/controllers/concerns/decidim/elections/uses_votes_booth.rb b/decidim-elections/app/controllers/concerns/decidim/elections/uses_votes_booth.rb index 08cf7b7c030c8..6dd6b9f38c4a7 100644 --- a/decidim-elections/app/controllers/concerns/decidim/elections/uses_votes_booth.rb +++ b/decidim-elections/app/controllers/concerns/decidim/elections/uses_votes_booth.rb @@ -49,10 +49,16 @@ def create # Shows the receipt page def receipt + if params[:exit].present? + votes_buffer.clear + session_attributes.clear + return redirect_to(exit_path) + end + enforce_permission_to(:create, :vote, election:) - votes_buffer.clear - session_attributes.clear + votes_buffer.clear unless election.per_question? + return redirect_to(exit_path) unless election.votes.exists?(voter_uid: session[:voter_uid]) render "decidim/elections/votes/receipt" diff --git a/decidim-elections/app/controllers/decidim/elections/per_question_votes_controller.rb b/decidim-elections/app/controllers/decidim/elections/per_question_votes_controller.rb index cceaddb0fc58d..ebd2bb2d7681f 100644 --- a/decidim-elections/app/controllers/decidim/elections/per_question_votes_controller.rb +++ b/decidim-elections/app/controllers/decidim/elections/per_question_votes_controller.rb @@ -29,6 +29,7 @@ def update enforce_permission_to(:create, :vote, election:) response_ids = params.dig(:response, question.id.to_s) + requeue_following_questions votes_buffer[question.id.to_s] = response_ids CastVotes.call(election, { question.id.to_s => response_ids }, voter_uid) do on(:ok) do @@ -84,6 +85,13 @@ def next_vote_step_action { action: :show, id: next_question } end + + def requeue_following_questions + election.questions + .where("position > ?", question.position) + .pluck(:id) + .each { |id| votes_buffer.delete(id.to_s) } + end end end end diff --git a/decidim-elections/app/views/decidim/elections/votes/receipt.html.erb b/decidim-elections/app/views/decidim/elections/votes/receipt.html.erb index 7bdeb45e33781..9d240b5609fa5 100644 --- a/decidim-elections/app/views/decidim/elections/votes/receipt.html.erb +++ b/decidim-elections/app/views/decidim/elections/votes/receipt.html.erb @@ -4,5 +4,22 @@

<%= t(".title") %>

<%= t(".description") %>

- <%= link_to t(".exit_button"), exit_path, class: "button button__secondary button__lg w-full mt-12" %> + + <% editable_question = election.per_question? ? election.questions.enabled.unpublished_results.last : nil %> + <% exit_button_path = election.per_question? ? url_for(action: :receipt, exit: true) : exit_path %> + <% if editable_question.present? %> +
+ <%= link_to election_per_question_vote_path(election_id: election, id: editable_question), + class: "button button__lg button__transparent-secondary" do %> + <%= icon "edit-line", class: "fill-current mr-2" %> + <%= t(".edit_vote") %> + <% end %> + +
+ <%= link_to t(".exit_button"), exit_button_path, class: "button button__lg button__secondary" %> +
+
+ <% else %> + <%= link_to t(".exit_button"), exit_button_path, class: "button button__secondary button__lg w-full mt-12" %> + <% end %> diff --git a/decidim-elections/config/locales/en.yml b/decidim-elections/config/locales/en.yml index d95f3c3c9a7fa..489ddf952d99a 100644 --- a/decidim-elections/config/locales/en.yml +++ b/decidim-elections/config/locales/en.yml @@ -322,7 +322,8 @@ en: max_choices_exceeded: You cannot select more than %{max} options. Please go back and adjust your selection. next: Next receipt: - description: Yo can vote again at any time while the voting period is open. Your previous vote will be overwritten by the new one. + description: You can vote again at any time while the voting period is open. Your previous vote will be overwritten by the new one. + edit_vote: Edit your vote exit_button: Exit the voting booth title: Your votes have been cast successfully metadata: diff --git a/decidim-elections/lib/decidim/elections/test/per_question_vote_examples.rb b/decidim-elections/lib/decidim/elections/test/per_question_vote_examples.rb index d18880317f02b..02c98244eb80d 100644 --- a/decidim-elections/lib/decidim/elections/test/per_question_vote_examples.rb +++ b/decidim-elections/lib/decidim/elections/test/per_question_vote_examples.rb @@ -171,3 +171,72 @@ expect(page).to have_no_content("Edit your vote") end end + +shared_examples "a per question votable election with edit from receipt" do + it "allows editing votes from receipt page" do + click_on "Vote" + choose translated_attribute(question1.response_options.first.body) + click_on "Cast vote" + check translated_attribute(question2.response_options.first.body) + click_on "Cast vote" + expect(page).to have_current_path(receipt_election_votes_path) + expect(page).to have_link("Edit your vote") + expect(page).to have_link("Exit the voting booth") + + click_on "Edit your vote" + expect(page).to have_current_path(election_vote_path(question2)) + expect(find("input[value='#{question2.response_options.first.id}']")).to be_checked + + click_on "Back" + expect(page).to have_current_path(election_vote_path(question1)) + expect(find("input[value='#{question1.response_options.first.id}']")).to be_checked + + choose translated_attribute(question1.response_options.second.body) + click_on "Cast vote" + expect(page).to have_current_path(election_vote_path(question2)) + + check translated_attribute(question2.response_options.second.body) + click_on "Cast vote" + expect(page).to have_current_path(receipt_election_votes_path) + expect(election.votes.where(voter_uid:).size).to eq(3) + + click_on "Exit the voting booth" + expect(page).to have_current_path(election_path) + expect(page).to have_content("You have already voted.") + end +end + +shared_examples "a per question votable election with edit from receipt when all questions enabled" do + it "allows editing any question from receipt page" do + click_on "Vote" + choose translated_attribute(question1.response_options.first.body) + click_on "Cast vote" + check translated_attribute(question2.response_options.first.body) + click_on "Cast vote" + expect(page).to have_current_path(receipt_election_votes_path) + + click_on "Edit your vote" + expect(page).to have_current_path(election_vote_path(question2)) + + click_on "Back" + expect(page).to have_current_path(election_vote_path(question1)) + + choose translated_attribute(question1.response_options.second.body) + click_on "Cast vote" + expect(page).to have_current_path(election_vote_path(question2)) + expect(find("input[value='#{question2.response_options.first.id}']")).to be_checked + + click_on "Cast vote" + expect(page).to have_current_path(receipt_election_votes_path) + + click_on "Edit your vote" + click_on "Back" + choose translated_attribute(question1.response_options.first.body) + click_on "Cast vote" + uncheck translated_attribute(question2.response_options.first.body) + check translated_attribute(question2.response_options.second.body) + click_on "Cast vote" + expect(page).to have_current_path(receipt_election_votes_path) + expect(election.votes.where(voter_uid:).size).to eq(2) + end +end diff --git a/decidim-elections/spec/controllers/decidim/elections/per_question_votes_controller_spec.rb b/decidim-elections/spec/controllers/decidim/elections/per_question_votes_controller_spec.rb index a16c780fd1a8c..abab9d9912189 100644 --- a/decidim-elections/spec/controllers/decidim/elections/per_question_votes_controller_spec.rb +++ b/decidim-elections/spec/controllers/decidim/elections/per_question_votes_controller_spec.rb @@ -29,6 +29,7 @@ module Elections allow(controller).to receive(:current_participatory_space).and_return(component.participatory_space) allow(controller).to receive(:current_component).and_return(component) allow(controller).to receive(:election_vote_path).and_return(election_vote_path) + allow(controller).to receive(:second_election_vote_path).and_return(second_election_vote_path) allow(controller).to receive(:new_election_vote_path).and_return(new_election_vote_path) allow(controller).to receive(:election_path).and_return(election_path) allow(controller).to receive(:waiting_election_votes_path).and_return(waiting_election_votes_path) @@ -118,14 +119,23 @@ module Elections end it "casts the votes and redirects to the receipt page if successful" do - # ensure there are no pending votes - allow(controller).to receive(:votes_buffer).and_return({ question.id.to_s => [question.response_options.first.id], second_question.id.to_s => [second_question.response_options.first.id] }) + session[:votes_buffer] = { question.id.to_s => [question.response_options.first.id.to_s], second_question.id.to_s => [second_question.response_options.first.id.to_s] } expect(controller).to receive(:redirect_to).with(action: :receipt) - patch :update, params: params.merge(id: question.id, response: { question.id.to_s => [question.response_options.first.id] }) + patch :update, params: params.merge(id: second_question.id, response: { second_question.id.to_s => [second_question.response_options.first.id] }) expect(session[:voter_uid]).to eq(user.to_global_id.to_s) expect(flash[:notice]).to eq(I18n.t("votes.cast.success", scope: "decidim.elections")) end + + it "clears subsequent questions from buffer when updating a previous question" do + session[:votes_buffer] = { question.id.to_s => [question.response_options.first.id.to_s], second_question.id.to_s => [second_question.response_options.first.id.to_s] } + + expect(controller).to receive(:redirect_to).with(action: :show, id: second_question) + patch :update, params: params.merge(id: question.id, response: { question.id.to_s => [question.response_options.second.id] }) + + expect(session[:votes_buffer]).not_to have_key(second_question.id.to_s) + expect(session[:votes_buffer][question.id.to_s]).to eq([question.response_options.second.id.to_s]) + end end end @@ -217,13 +227,20 @@ module Elections it_behaves_like "a redirect to the waiting room", :receipt - it "renders the receipt page" do - expect(controller.send(:votes_buffer)).to receive(:clear) - expect(controller.send(:session_attributes)).to receive(:clear) + it "renders the receipt page without clearing session" do + expect(controller.send(:votes_buffer)).not_to receive(:clear) + expect(controller.send(:session_attributes)).not_to receive(:clear) get :receipt, params: params expect(response).to have_http_status(:ok) expect(subject).to render_template(:receipt) end + + it "clears session when exit param is present" do + expect(controller.send(:votes_buffer)).to receive(:clear) + expect(controller.send(:session_attributes)).to receive(:clear) + get :receipt, params: params.merge(exit: true) + expect(response).to redirect_to(election_path) + end end end end diff --git a/decidim-elections/spec/controllers/decidim/elections/votes_controller_spec.rb b/decidim-elections/spec/controllers/decidim/elections/votes_controller_spec.rb index 7cab91d3c4936..c086c74b3366b 100644 --- a/decidim-elections/spec/controllers/decidim/elections/votes_controller_spec.rb +++ b/decidim-elections/spec/controllers/decidim/elections/votes_controller_spec.rb @@ -235,13 +235,20 @@ module Elections create(:election_vote, voter_uid: session[:voter_uid], question: question, response_option: question.response_options.first) end - it "renders the receipt page" do + it "renders the receipt page and clears votes buffer" do expect(controller.send(:votes_buffer)).to receive(:clear) - expect(controller.send(:session_attributes)).to receive(:clear) + expect(controller.send(:session_attributes)).not_to receive(:clear) get :receipt, params: params expect(response).to have_http_status(:ok) expect(subject).to render_template(:receipt) end + + it "clears session when exit param is present" do + expect(controller.send(:votes_buffer)).to receive(:clear) + expect(controller.send(:session_attributes)).to receive(:clear) + get :receipt, params: params.merge(exit: true) + expect(response).to redirect_to(election_path) + end end end end diff --git a/decidim-elections/spec/system/user_votes_in_an_per_question_election_spec.rb b/decidim-elections/spec/system/user_votes_in_an_per_question_election_spec.rb index 7be60f6e774b7..3b88aed74897a 100644 --- a/decidim-elections/spec/system/user_votes_in_an_per_question_election_spec.rb +++ b/decidim-elections/spec/system/user_votes_in_an_per_question_election_spec.rb @@ -73,6 +73,19 @@ def election_vote_path(question) it_behaves_like "a per question votable election with already voted questions" end + context "when editing votes from receipt page" do + let!(:question1) { create(:election_question, :with_response_options, :voting_enabled, skip_injection: true, question_type: "single_option", election:, position: 1) } + let!(:question2) { create(:election_question, :with_response_options, :voting_enabled, election:, skip_injection: true, position: 2) } + + before do + login_as user, scope: :user + visit election_path + end + + it_behaves_like "a per question votable election with edit from receipt" + it_behaves_like "a per question votable election with edit from receipt when all questions enabled" + end + context "when the election is real time" do let(:election) { create(:election, :published, :ongoing, :with_internal_users_census, :real_time, :with_questions) } From 00b96d318ca5e57eec9bd79548c13aab3369ae3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=20Bol=C3=ADvar=20=28FBO=29?= Date: Fri, 12 Dec 2025 10:04:25 +0100 Subject: [PATCH 013/116] Prevent server error on proposal page when the user is not logged in (#15776) --- Gemfile.lock | 1 + .../decidim/proposals/proposal_vote/show.erb | 2 +- .../proposals/proposals/_votes_count.html.erb | 2 +- .../system/private_space_proposal_spec.rb | 22 +++++++++++++++++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index fcc840f633463..9a7d810772349 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -914,6 +914,7 @@ GEM PLATFORMS arm64-darwin-23 + arm64-darwin-25 x86_64-linux DEPENDENCIES diff --git a/decidim-proposals/app/cells/decidim/proposals/proposal_vote/show.erb b/decidim-proposals/app/cells/decidim/proposals/proposal_vote/show.erb index 0d4f975adacf9..d3a2517793c0e 100644 --- a/decidim-proposals/app/cells/decidim/proposals/proposal_vote/show.erb +++ b/decidim-proposals/app/cells/decidim/proposals/proposal_vote/show.erb @@ -1,4 +1,4 @@ -<% if !current_settings.votes_hidden? && (current_component.participatory_space.can_participate?(current_user) || current_user.admin?) %> +<% if !current_settings.votes_hidden? && (current_component.participatory_space.can_participate?(current_user) || current_user&.admin?) %> <% if component_settings.participatory_texts_enabled? && from_proposals_list %> <%= render partial: "decidim/proposals/proposals/participatory_texts/proposal_votes_count", locals: { proposal: resource, from_proposals_list: true } %> <% else %> diff --git a/decidim-proposals/app/views/decidim/proposals/proposals/_votes_count.html.erb b/decidim-proposals/app/views/decidim/proposals/proposals/_votes_count.html.erb index df48911abd243..fde49c5ff07b8 100644 --- a/decidim-proposals/app/views/decidim/proposals/proposals/_votes_count.html.erb +++ b/decidim-proposals/app/views/decidim/proposals/proposals/_votes_count.html.erb @@ -1,4 +1,4 @@ -<% if !current_settings.votes_hidden? && (current_component.participatory_space.can_participate?(current_user) || current_user.admin?) %> +<% if !current_settings.votes_hidden? && (current_component.participatory_space.can_participate?(current_user) || current_user&.admin?) %> <% if component_settings.participatory_texts_enabled? && from_proposals_list %> <%= render partial: "decidim/proposals/proposals/participatory_texts/proposal_votes_count", locals: { proposal:, from_proposals_list: true } %> <% else %> diff --git a/decidim-proposals/spec/system/private_space_proposal_spec.rb b/decidim-proposals/spec/system/private_space_proposal_spec.rb index fd46e50dcf45e..7391c759fc311 100644 --- a/decidim-proposals/spec/system/private_space_proposal_spec.rb +++ b/decidim-proposals/spec/system/private_space_proposal_spec.rb @@ -33,6 +33,28 @@ def visit_component expect(page).to have_no_link("New proposal") end end + + context "when the component has votes enabled and the proposal has votes" do + let!(:proposal) { create(:proposal, :official, :with_votes, component:) } + + before do + component.default_step_settings = component.default_step_settings.to_h.merge({ votes_enabled: true }) + component.save! + end + + context "when accessing the proposal page" do + let(:target_path) { Decidim::ResourceLocatorPresenter.new(proposal).path } + + before do + visit target_path + end + + it "can access the page but cannot see the votes" do + expect(page).to have_content(proposal.title["en"]) + expect(page).to have_no_content("Votes") + end + end + end end context "when the user is logged in" do From e55f4c4b92dc601cd0b4d7f5ba6f40e806f7135b Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Fri, 12 Dec 2025 11:56:34 +0200 Subject: [PATCH 014/116] Use `GraphQL::ExecutionError` to handle API permissions (#15682) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor API location * Add Proposal answer mutation example * Add link to decidim docs website * Process page announcement bug fix with system test (#15616) Co-authored-by: Leo * Add debug permissions * Fix exception * Error handling of not found resources * Fix Page specs * Add I18n support for not found error * Fix i18n key * Fix locales * Fix failing specs * Adjust some of the existing mutations * Add error code token * Rename context from graphQL hidden space to graphQL not found space * Address other review comments * Fix typo * Fix some types * Fix failing specs * Fix failing specs * Apply suggestions from code review Co-authored-by: Andrés Pereira de Lucena * Fix NuValidator image * Apply review recommendations * Update decidim-api/lib/decidim/api/test/component_context.rb Co-authored-by: Andrés Pereira de Lucena --------- Co-authored-by: Leo Storey <123873192+Ginger-Leo@users.noreply.github.com> Co-authored-by: Leo Co-authored-by: Andrés Pereira de Lucena --- .../spec/types/result_type_spec.rb | 18 ++-- decidim-api/config/locales/en.yml | 2 + .../decidim/api/component_mutation_type.rb | 3 +- .../api/errors/unauthorized_field_error.rb | 13 +++ .../api/errors/unauthorized_object_error.rb | 13 +++ .../lib/decidim/api/graphql_permissions.rb | 11 +-- decidim-api/lib/decidim/api/schema.rb | 10 ++ .../lib/decidim/api/test/component_context.rb | 24 +++-- .../lib/decidim/api/test/type_context.rb | 4 + decidim-api/lib/decidim/api/types.rb | 2 + .../lib/decidim/api/types/base_mutation.rb | 12 +++ decidim-blogs/spec/types/post_type_spec.rb | 26 +++-- .../spec/types/budget_type_spec.rb | 2 +- .../spec/types/integration_schema_spec.rb | 99 +++++++++++++------ .../spec/types/project_type_spec.rb | 23 ++--- .../spec/types/document_type_spec.rb | 22 ++--- .../decidim/comments/vote_comment_resolver.rb | 3 +- .../decidim/api/commentable_mutation_type.rb | 4 + .../spec/types/comment_type_spec.rb | 34 +++---- .../spec/types/conference_type_spec.rb | 4 +- .../decidim/api/functions/component_list.rb | 3 +- .../spec/types/reportable_type_spec.rb | 14 +-- .../spec/types/reportable_user_type_spec.rb | 18 ++-- decidim-core/spec/types/user_type_spec.rb | 22 ++--- .../spec/types/debate_type_spec.rb | 22 ++--- .../spec/types/questionnaire_type_spec.rb | 6 +- .../spec/types/agenda_type_spec.rb | 4 +- .../spec/types/meeting_type_spec.rb | 34 +++---- .../api/mutations/proposal_answer_type.rb | 8 +- .../spec/shared/proposal_mutation_examples.rb | 4 +- .../spec/types/proposal_mutation_type_spec.rb | 4 +- .../spec/types/proposal_type_spec.rb | 22 ++--- 32 files changed, 283 insertions(+), 207 deletions(-) create mode 100644 decidim-api/lib/decidim/api/errors/unauthorized_field_error.rb create mode 100644 decidim-api/lib/decidim/api/errors/unauthorized_object_error.rb diff --git a/decidim-accountability/spec/types/result_type_spec.rb b/decidim-accountability/spec/types/result_type_spec.rb index 72836f3725d35..480003db3518c 100644 --- a/decidim-accountability/spec/types/result_type_spec.rb +++ b/decidim-accountability/spec/types/result_type_spec.rb @@ -20,6 +20,12 @@ module Accountability include_examples "localizable interface" include_examples "referable interface" + shared_examples "unauthorized Result" do + it "throws Decidim::Api::Errors::UnauthorizedObjectError" do + expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedObjectError, "You cannot view or edit this Result because you do not have permissions") + end + end + describe "id" do let(:query) { "{ id }" } @@ -97,9 +103,7 @@ module Accountability let(:model) { create(:result, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Result" end context "when participatory space is private but transparent" do @@ -119,9 +123,7 @@ module Accountability let(:model) { create(:result, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Result" end context "when component is not published" do @@ -129,9 +131,7 @@ module Accountability let(:model) { create(:result, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Result" end end end diff --git a/decidim-api/config/locales/en.yml b/decidim-api/config/locales/en.yml index 3ba96e5fd802a..62bf91ab4690a 100644 --- a/decidim-api/config/locales/en.yml +++ b/decidim-api/config/locales/en.yml @@ -4,3 +4,5 @@ en: api: errors: not_found: "%{type} not found" + unauthorized_field: You cannot view or edit %{field} field on %{type} because you do not have permission + unauthorized_object: You cannot view or edit this %{type} because you do not have permissions diff --git a/decidim-api/lib/decidim/api/component_mutation_type.rb b/decidim-api/lib/decidim/api/component_mutation_type.rb index b661cd0a1a30a..35bb10b2e7841 100644 --- a/decidim-api/lib/decidim/api/component_mutation_type.rb +++ b/decidim-api/lib/decidim/api/component_mutation_type.rb @@ -11,8 +11,7 @@ def self.resolve_type(obj, _ctx) mod = obj.manifest_name.camelize "Decidim::#{mod}::#{mod}MutationType".constantize rescue NameError - Rails.logger.warn("Mutation type not found for #{mod}: #{e.message}") - nil + raise GraphQL::ExecutionError, "Mutation type not found for #{mod}" end end end diff --git a/decidim-api/lib/decidim/api/errors/unauthorized_field_error.rb b/decidim-api/lib/decidim/api/errors/unauthorized_field_error.rb new file mode 100644 index 0000000000000..8f575408f0b36 --- /dev/null +++ b/decidim-api/lib/decidim/api/errors/unauthorized_field_error.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Decidim + module Api + module Errors + class UnauthorizedFieldError < GraphQL::ExecutionError + def to_h + super.merge({ "extensions" => { "code" => "NO_FIELD_PERMISSION" } }) + end + end + end + end +end diff --git a/decidim-api/lib/decidim/api/errors/unauthorized_object_error.rb b/decidim-api/lib/decidim/api/errors/unauthorized_object_error.rb new file mode 100644 index 0000000000000..e39bd60ef5deb --- /dev/null +++ b/decidim-api/lib/decidim/api/errors/unauthorized_object_error.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Decidim + module Api + module Errors + class UnauthorizedObjectError < GraphQL::ExecutionError + def to_h + super.merge({ "extensions" => { "code" => "NO_OBJECT_PERMISSION" } }) + end + end + end + end +end diff --git a/decidim-api/lib/decidim/api/graphql_permissions.rb b/decidim-api/lib/decidim/api/graphql_permissions.rb index 09c2c4bb2598e..a25f5e529b47e 100644 --- a/decidim-api/lib/decidim/api/graphql_permissions.rb +++ b/decidim-api/lib/decidim/api/graphql_permissions.rb @@ -44,12 +44,7 @@ def allowed_to?(action, subject, object, context, scope: :public) permission_action = Decidim::PermissionAction.new(scope:, action:, subject:) permission_chain(object).inject(permission_action) do |current_permission_action, permission_class| - permission_context = - if scope == :admin - local_admin_context(object, context) - else - local_context(object, context) - end + permission_context = local_user_context(object, context) permission_class.new( context[:current_user], @@ -57,6 +52,8 @@ def allowed_to?(action, subject, object, context, scope: :public) permission_context ).permissions end.allowed? + rescue Decidim::PermissionAction::PermissionNotSetError + false end # Injects into context object current_participatory_space and current_component keys as they are needed @@ -77,7 +74,7 @@ def local_context(object, context) context.to_h end - def local_admin_context(object, context) + def local_user_context(object, context) context = local_context(object, context) component = context[:current_component] diff --git a/decidim-api/lib/decidim/api/schema.rb b/decidim-api/lib/decidim/api/schema.rb index 4e1966155246d..b064e260ed35e 100644 --- a/decidim-api/lib/decidim/api/schema.rb +++ b/decidim-api/lib/decidim/api/schema.rb @@ -13,6 +13,16 @@ class Schema < GraphQL::Schema orphan_types(Api.orphan_types) + def self.unauthorized_object(error) + # Add a top-level error to the response instead of returning nil: + raise Decidim::Api::Errors::UnauthorizedObjectError, I18n.t("decidim.api.errors.unauthorized_object", type: error.type.graphql_name) + end + + def self.unauthorized_field(error) + # Add a top-level error to the response instead of returning nil: + raise Decidim::Api::Errors::UnauthorizedFieldError, I18n.t("decidim.api.errors.unauthorized_field", type: error.type.graphql_name, field: error.field.graphql_name) + end + rescue_from(ActiveRecord::RecordNotFound) do |_err, _obj, _args, _ctx, field| raise Decidim::Api::Errors::NotFoundError, I18n.t("decidim.api.errors.not_found", type: field.type.unwrap.graphql_name) end diff --git a/decidim-api/lib/decidim/api/test/component_context.rb b/decidim-api/lib/decidim/api/test/component_context.rb index 6b38054853b69..e07af3fe0dc53 100644 --- a/decidim-api/lib/decidim/api/test/component_context.rb +++ b/decidim-api/lib/decidim/api/test/component_context.rb @@ -54,15 +54,21 @@ let(:process_space_factory) { :participatory_process } let(:space_type) { "participatoryProcess" } - shared_examples "graphQL visible resource" do - it "is visible" do - expect(response[space_type]["components"].first[lookout_key]).to eq(query_result) + shared_examples "graphQL visible resource" do |visible: true| + if visible + it "should be visible" do + expect(response[space_type]["components"].first[lookout_key]).to eq(query_result) + end + else + it "should not be visible" do + expect(response[space_type]["components"]).to be_empty + end end end shared_examples "graphQL hidden component" do it "should not be visible" do - expect(response[space_type]["components"].first).to be_nil + expect(response[space_type]["components"]).to be_empty end end @@ -144,7 +150,7 @@ context "when the user is space admin" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } let!(:role) { create(:participatory_process_user_role, participatory_process:, user: current_user, role: "admin") } - it_behaves_like "graphQL visible resource" + it_behaves_like "graphQL visible resource", visible: false end context "when the user is space collaborator" do @@ -162,7 +168,7 @@ context "when the user is space evaluator" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } let!(:role) { create(:participatory_process_user_role, participatory_process:, user: current_user, role: "evaluator") } - it_behaves_like "graphQL visible resource" + it_behaves_like "graphQL visible resource", visible: false end context "when user is visitor" do @@ -260,13 +266,13 @@ context "when the user is space admin" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } let!(:role) { create(:assembly_user_role, assembly: participatory_process, user: current_user, role: "admin") } - it_behaves_like "graphQL visible resource" + it_behaves_like "graphQL visible resource", visible: false end context "when the user is space collaborator" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } let!(:role) { create(:assembly_user_role, assembly: participatory_process, user: current_user, role: "collaborator") } - it_behaves_like "graphQL visible resource" + it_behaves_like "graphQL visible resource", visible: false end context "when the user is space moderator" do @@ -278,7 +284,7 @@ context "when the user is space evaluator" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } let!(:role) { create(:assembly_user_role, assembly: participatory_process, user: current_user, role: "evaluator") } - it_behaves_like "graphQL visible resource" + it_behaves_like "graphQL visible resource", visible: false end context "when user is visitor" do diff --git a/decidim-api/lib/decidim/api/test/type_context.rb b/decidim-api/lib/decidim/api/test/type_context.rb index aa6304ce16538..8a9c02a29f77c 100644 --- a/decidim-api/lib/decidim/api/test/type_context.rb +++ b/decidim-api/lib/decidim/api/test/type_context.rb @@ -34,6 +34,10 @@ def raise_proper_error(error) case code when "NOT_FOUND" raise Decidim::Api::Errors::NotFoundError, error["message"] + when "NO_FIELD_PERMISSION" + raise Decidim::Api::Errors::UnauthorizedFieldError, error["message"] + when "NO_OBJECT_PERMISSION" + raise Decidim::Api::Errors::UnauthorizedObjectError, error["message"] else raise StandardError, error["message"] end diff --git a/decidim-api/lib/decidim/api/types.rb b/decidim-api/lib/decidim/api/types.rb index 4e62516a17ccf..d06b589e86252 100644 --- a/decidim-api/lib/decidim/api/types.rb +++ b/decidim-api/lib/decidim/api/types.rb @@ -11,6 +11,8 @@ module Api module Errors autoload :NotFoundError, "decidim/api/errors/not_found_error" + autoload :UnauthorizedFieldError, "decidim/api/errors/unauthorized_field_error" + autoload :UnauthorizedObjectError, "decidim/api/errors/unauthorized_object_error" end module Types diff --git a/decidim-api/lib/decidim/api/types/base_mutation.rb b/decidim-api/lib/decidim/api/types/base_mutation.rb index 2a0d2a44588b3..2a13fe66dde25 100644 --- a/decidim-api/lib/decidim/api/types/base_mutation.rb +++ b/decidim-api/lib/decidim/api/types/base_mutation.rb @@ -11,6 +11,18 @@ class BaseMutation < GraphQL::Schema::RelayClassicMutation input_object_class BaseInputObject required_scopes "api:read", "api:write" + + def current_user + context[:current_user] + end + + def current_component + context[:current_component] + end + + def current_organization + context[:current_organization] + end end end end diff --git a/decidim-blogs/spec/types/post_type_spec.rb b/decidim-blogs/spec/types/post_type_spec.rb index 294b106257278..26c47eeb54cc9 100644 --- a/decidim-blogs/spec/types/post_type_spec.rb +++ b/decidim-blogs/spec/types/post_type_spec.rb @@ -17,6 +17,12 @@ module Blogs include_examples "likeable interface" include_examples "followable interface" + shared_examples "unauthorized Post" do + it "throws Decidim::Api::Errors::UnauthorizedObjectError" do + expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedObjectError, "You cannot view or edit this Post because you do not have permissions") + end + end + describe "id" do let(:query) { "{ id }" } @@ -55,9 +61,7 @@ module Blogs let(:model) { create(:post, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Post" end context "when participatory space is private but transparent" do @@ -77,9 +81,7 @@ module Blogs let(:model) { create(:post, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Post" end context "when component is not published" do @@ -87,9 +89,7 @@ module Blogs let(:model) { create(:post, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Post" end context "when post is moderated" do @@ -97,9 +97,7 @@ module Blogs let(:query) { "{ id }" } let(:root_value) { model.reload } - it "returns all the required fields" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Post" end context "when post is not published" do @@ -107,9 +105,7 @@ module Blogs let(:model) { create(:post, published_at: nil, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Post" end end end diff --git a/decidim-budgets/spec/types/budget_type_spec.rb b/decidim-budgets/spec/types/budget_type_spec.rb index 40b169938a459..006f869243dfc 100644 --- a/decidim-budgets/spec/types/budget_type_spec.rb +++ b/decidim-budgets/spec/types/budget_type_spec.rb @@ -10,7 +10,7 @@ module Budgets let(:model) { create(:budget, :with_projects) } it_behaves_like "traceable interface" do - let(:author) { create(:user, :admin, organization: model.component.organization) } + let(:author) { create(:user, :admin, :confirmed, organization: model.component.organization) } end describe "id" do diff --git a/decidim-budgets/spec/types/integration_schema_spec.rb b/decidim-budgets/spec/types/integration_schema_spec.rb index 21d1fc4222ccd..dea9e7d6783f8 100644 --- a/decidim-budgets/spec/types/integration_schema_spec.rb +++ b/decidim-budgets/spec/types/integration_schema_spec.rb @@ -123,6 +123,12 @@ } end + shared_examples "unauthorized Budget" do + it "throws Decidim::Api::Errors::UnauthorizedObjectError" do + expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedObjectError, "You cannot view or edit this Budget because you do not have permissions") + end + end + describe "commentable" do let(:component_fragment) { nil } @@ -275,8 +281,33 @@ context "when user is visitor" do let!(:current_user) { nil } + let(:component_fragment) do + %( + fragment fooComponent on Budgets { + budget(id: #{budget.id}) { + createdAt + description { + translation(locale:"#{locale}") + } + id + title { + translation(locale:"#{locale}") + } + total_budget + updatedAt + url + versions { + id + } + versionsCount + weight + } + } + ) + end + it "should be visible" do - expect(response["participatoryProcess"]["components"].first[lookout_key]).to eq(query_result.merge("projects" => [nil, nil])) + expect(response["participatoryProcess"]["components"].first[lookout_key]).to eq(query_result.except("projects")) end end @@ -295,16 +326,14 @@ context "when the user is admin" do let!(:current_user) { create(:user, :admin, :confirmed, organization: current_organization) } - it "should not be visible" do - expect(response["participatoryProcess"]["components"].first[lookout_key]).to be_nil - end + it_behaves_like "unauthorized Budget" end context "when user is visitor" do let!(:current_user) { nil } it "should not be visible" do - expect(response["participatoryProcess"]["components"].first).to be_nil + expect(response["participatoryProcess"]["components"]).to be_empty end end @@ -312,7 +341,7 @@ let!(:current_user) { create(:user, :confirmed, organization: current_organization) } it "should not be visible" do - expect(response["participatoryProcess"]["components"].first).to be_nil + expect(response["participatoryProcess"]["components"]).to be_empty end end end @@ -327,9 +356,7 @@ context "when the user is admin" do let!(:current_user) { create(:user, :admin, :confirmed, organization: current_organization) } - it "should not be visible" do - expect(response["participatoryProcess"]["components"].first[lookout_key]).to be_nil - end + it_behaves_like "unauthorized Budget" end context "when user is visitor" do @@ -351,9 +378,7 @@ context "when the user is admin" do let!(:current_user) { create(:user, :admin, :confirmed, organization: current_organization) } - it "should not be visible" do - expect(response["participatoryProcess"]["components"].first[lookout_key]).to be_nil - end + it_behaves_like "unauthorized Budget" end context "when user is visitor" do @@ -413,12 +438,36 @@ end end end - context "when user is visitor" do let!(:current_user) { nil } + let(:component_fragment) do + %( + fragment fooComponent on Budgets { + budget(id: #{budget.id}) { + createdAt + description { + translation(locale:"#{locale}") + } + id + title { + translation(locale:"#{locale}") + } + total_budget + updatedAt + url + versions { + id + } + versionsCount + weight + } + } + ) + end + it "is visible" do - expect(response["assembly"]["components"].first[lookout_key]).to eq(query_result.merge("projects" => [nil, nil])) + expect(response["assembly"]["components"].first[lookout_key]).to eq(query_result.except("projects")) end end @@ -446,9 +495,7 @@ context "when the user is admin" do let!(:current_user) { create(:user, :admin, :confirmed, organization: current_organization) } - it "is visible" do - expect(response["assembly"]["components"].first[lookout_key]).to be_nil - end + it_behaves_like "unauthorized Budget" end %w(admin collaborator evaluator).each do |role| @@ -457,7 +504,7 @@ let!(:role) { create(:assembly_user_role, assembly: participatory_process, user: current_user, role:) } it "is visible" do - expect(response["assembly"]["components"].first[lookout_key]).to be_nil + expect(response["assembly"]["components"]).to be_empty end end end @@ -466,7 +513,7 @@ let!(:role) { create(:assembly_user_role, assembly: participatory_process, user: current_user, role: "moderator") } it "is visible" do - expect(response["assembly"]["components"].first).to be_nil + expect(response["assembly"]["components"]).to be_empty end end @@ -474,7 +521,7 @@ let!(:current_user) { nil } it "should not be visible" do - expect(response["assembly"]["components"].first).to be_nil + expect(response["assembly"]["components"]).to be_empty end context "when user is member" do @@ -482,7 +529,7 @@ let!(:participatory_space_private_user) { create(:assembly_private_user, user: current_user, privatable_to: participatory_process) } it "should not be visible" do - expect(response["assembly"]["components"].first).to be_nil + expect(response["assembly"]["components"]).to be_empty end end end @@ -491,7 +538,7 @@ let!(:current_user) { create(:user, :confirmed, organization: current_organization) } it "should not be visible" do - expect(response["assembly"]["components"].first).to be_nil + expect(response["assembly"]["components"]).to be_empty end end end @@ -506,9 +553,7 @@ context "when the user is admin" do let!(:current_user) { create(:user, :admin, :confirmed, organization: current_organization) } - it "should not be visible" do - expect(response["participatoryProcess"]["components"].first[lookout_key]).to be_nil - end + it_behaves_like "unauthorized Budget" end context "when user is visitor" do @@ -530,9 +575,7 @@ context "when the user is admin" do let!(:current_user) { create(:user, :admin, :confirmed, organization: current_organization) } - it "should not be visible" do - expect(response["participatoryProcess"]["components"].first[lookout_key]).to be_nil - end + it_behaves_like "unauthorized Budget" end context "when user is visitor" do diff --git a/decidim-budgets/spec/types/project_type_spec.rb b/decidim-budgets/spec/types/project_type_spec.rb index 94448418103db..cd19a363da937 100644 --- a/decidim-budgets/spec/types/project_type_spec.rb +++ b/decidim-budgets/spec/types/project_type_spec.rb @@ -21,6 +21,12 @@ module Budgets include_examples "referable interface" include_examples "followable interface" + shared_examples "unauthorized Project" do + it "throws Decidim::Api::Errors::UnauthorizedObjectError" do + expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedObjectError, "You cannot view or edit this Project because you do not have permissions") + end + end + describe "id" do let(:query) { "{ id }" } @@ -155,9 +161,7 @@ module Budgets let(:model) { create(:project, budget:) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Project" end context "when participatory space is not published" do @@ -167,9 +171,7 @@ module Budgets let(:model) { create(:project, budget:) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Project" end context "when component is not published" do @@ -177,9 +179,7 @@ module Budgets let(:model) { create(:project, component:) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Project" end context "when budget is not visible" do @@ -189,10 +189,11 @@ module Budgets let(:query) { "{ id }" } let(:root_value) { model.reload } - it "returns all the required fields" do + before do allow(model).to receive(:visible?).and_return(false) - expect(response).to be_nil end + + it_behaves_like "unauthorized Project" end end end diff --git a/decidim-collaborative_texts/spec/types/document_type_spec.rb b/decidim-collaborative_texts/spec/types/document_type_spec.rb index 7961837fdfad8..6cb15cef4d6e0 100644 --- a/decidim-collaborative_texts/spec/types/document_type_spec.rb +++ b/decidim-collaborative_texts/spec/types/document_type_spec.rb @@ -13,6 +13,12 @@ module CollaborativeTexts include_examples "traceable interface" include_examples "timestamps interface" + shared_examples "unauthorized Document" do + it "throws Decidim::Api::Errors::UnauthorizedObjectError" do + expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedObjectError, "You cannot view or edit this CollaborativeText because you do not have permissions") + end + end + describe "id" do let(:query) { "{ id }" } @@ -93,9 +99,7 @@ module CollaborativeTexts let(:model) { create(:collaborative_text_document, :published, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Document" end context "when participatory space is private but transparent" do @@ -115,9 +119,7 @@ module CollaborativeTexts let(:model) { create(:collaborative_text_document, :published, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Document" end context "when component is not published" do @@ -125,9 +127,7 @@ module CollaborativeTexts let(:model) { create(:collaborative_text_document, :published, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Document" end context "when document is not published" do @@ -135,9 +135,7 @@ module CollaborativeTexts let(:model) { create(:collaborative_text_document, :published, published_at: nil, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Document" end end end diff --git a/decidim-comments/app/resolvers/decidim/comments/vote_comment_resolver.rb b/decidim-comments/app/resolvers/decidim/comments/vote_comment_resolver.rb index aeccb5ef96e0a..2c9384e6123d4 100644 --- a/decidim-comments/app/resolvers/decidim/comments/vote_comment_resolver.rb +++ b/decidim-comments/app/resolvers/decidim/comments/vote_comment_resolver.rb @@ -14,8 +14,9 @@ def call(obj, _args, ctx) on(:ok) do |comment| return comment end + on(:invalid) do - return GraphQL::ExecutionError.new(I18n.t("votes.create.error", scope: "decidim.comments")) + raise GraphQL::ExecutionError, I18n.t("votes.create.error", scope: "decidim.comments") end end end diff --git a/decidim-comments/lib/decidim/api/commentable_mutation_type.rb b/decidim-comments/lib/decidim/api/commentable_mutation_type.rb index 7ce370b8a8a58..0fcada3818574 100644 --- a/decidim-comments/lib/decidim/api/commentable_mutation_type.rb +++ b/decidim-comments/lib/decidim/api/commentable_mutation_type.rb @@ -22,6 +22,10 @@ def add_comment(body:, alignment: nil) on(:ok) do |comment| return comment end + + on(:invalid) do + raise GraphQL::ExecutionError, t("create.error", scope: "decidim.comments.comments") + end end end end diff --git a/decidim-comments/spec/types/comment_type_spec.rb b/decidim-comments/spec/types/comment_type_spec.rb index d65bacb7473ab..d2ba11f4faa9f 100644 --- a/decidim-comments/spec/types/comment_type_spec.rb +++ b/decidim-comments/spec/types/comment_type_spec.rb @@ -11,6 +11,12 @@ module Comments let(:model) { create(:comment) } let(:sgid) { double("sgid", to_s: "1234") } + shared_examples "unauthorized Comment" do + it "throws Decidim::Api::Errors::UnauthorizedObjectError" do + expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedObjectError, "You cannot view or edit this Comment because you do not have permissions") + end + end + context "when participatory space is unpublished" do let(:participatory_space) { create(:assembly, :unpublished) } let(:component) { create(:dummy_component, :published, participatory_space:) } @@ -20,9 +26,7 @@ module Comments let(:model) { create(:comment, commentable:) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Comment" end context "when participatory space is private and transparent" do @@ -45,9 +49,7 @@ module Comments let(:model) { create(:comment, commentable:) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Comment" end context "when component is unpublished" do @@ -57,9 +59,7 @@ module Comments let(:model) { create(:comment, commentable:) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Comment" end context "when resource is unpublished" do @@ -68,9 +68,7 @@ module Comments let(:model) { create(:comment, commentable:) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Comment" end context "when resource is moderated" do @@ -80,27 +78,21 @@ module Comments let(:model) { create(:comment, commentable:) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Comment" end describe "deleted comment" do let(:model) { create(:comment, :deleted) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Comment" end describe "moderated comment" do let(:model) { create(:comment, :moderated) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Comment" end describe "author" do diff --git a/decidim-conferences/spec/types/conference_type_spec.rb b/decidim-conferences/spec/types/conference_type_spec.rb index d12a166b469e3..121de1003b4bb 100644 --- a/decidim-conferences/spec/types/conference_type_spec.rb +++ b/decidim-conferences/spec/types/conference_type_spec.rb @@ -204,8 +204,8 @@ module Conferences context "when registrations are disabled" do let(:registrations_enabled) { false } - it "does not return any registration type" do - expect(response["registrationTypes"]).to eq([nil]) + it "throws Decidim::Api::Errors::UnauthorizedObjectError" do + expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedObjectError, "You cannot view or edit this ConferenceRegistrationType because you do not have permissions") end end diff --git a/decidim-core/lib/decidim/api/functions/component_list.rb b/decidim-core/lib/decidim/api/functions/component_list.rb index a7fbf3f3691bb..23b9f298ca656 100644 --- a/decidim-core/lib/decidim/api/functions/component_list.rb +++ b/decidim-core/lib/decidim/api/functions/component_list.rb @@ -23,11 +23,12 @@ def initialize @model_class = Component end - def call(participatory_space, args, _ctx) + def call(participatory_space, args, ctx) @query = Decidim::Component # remove default ordering if custom order required @query = @query.unscoped if args[:order] @query = @query.where(participatory_space:) + @query = @query.published unless ctx[:current_user]&.admin? add_filter_keys(args[:filter]) add_order_keys(args[:order].to_h) add_default_order diff --git a/decidim-core/spec/types/reportable_type_spec.rb b/decidim-core/spec/types/reportable_type_spec.rb index 55944375888fa..ef7ec7f5c15d2 100644 --- a/decidim-core/spec/types/reportable_type_spec.rb +++ b/decidim-core/spec/types/reportable_type_spec.rb @@ -11,6 +11,12 @@ module Core include_examples "timestamps interface" + shared_examples "unauthorized User object" do + it "throws Decidim::Api::Errors::UnauthorizedObjectError" do + expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedObjectError, "You cannot view or edit this User because you do not have permissions") + end + end + describe "id" do let(:query) { "{ id }" } @@ -56,17 +62,13 @@ module Core context "when user reporting deleted his account" do let!(:model) { create(:report, moderation:, user: create(:user, :confirmed, :deleted, organization: moderation.reportable.organization), details: "Testing reason", locale: "en") } - it "returns nil" do - expect(response["user"]).to be_nil - end + it_behaves_like "unauthorized User object" end context "when user reporting got blocked" do let!(:model) { create(:report, moderation:, user: create(:user, :confirmed, :blocked, organization: moderation.reportable.organization), details: "Testing reason", locale: "en") } - it "returns nil" do - expect(response["user"]).to be_nil - end + it_behaves_like "unauthorized User object" end end end diff --git a/decidim-core/spec/types/reportable_user_type_spec.rb b/decidim-core/spec/types/reportable_user_type_spec.rb index 343e647a172c6..6cc9cce746cbf 100644 --- a/decidim-core/spec/types/reportable_user_type_spec.rb +++ b/decidim-core/spec/types/reportable_user_type_spec.rb @@ -11,6 +11,12 @@ module Core include_examples "timestamps interface" + shared_examples "unauthorized User object" do + it "throws Decidim::Api::Errors::UnauthorizedObjectError" do + expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedObjectError, "You cannot view or edit this User because you do not have permissions") + end + end + describe "id" do let(:query) { "{ id }" } @@ -47,24 +53,18 @@ module Core let!(:model) { create(:user_report, moderation:, user: moderation.user, details: "Testing reason") } - it "returns nil" do - expect(response["user"]).to be_nil - end + it_behaves_like "unauthorized User object" context "when the user that made the report deleted their account" do let(:moderation) { create(:user_moderation, user: create(:user, :confirmed, :deleted)) } - it "returns nil" do - expect(response["user"]).to be_nil - end + it_behaves_like "unauthorized User object" end context "when the user that made the reporting got blocked" do let(:moderation) { create(:user_moderation, user: create(:user, :confirmed, :blocked)) } - it "returns nil" do - expect(response["user"]).to be_nil - end + it_behaves_like "unauthorized User object" end end end diff --git a/decidim-core/spec/types/user_type_spec.rb b/decidim-core/spec/types/user_type_spec.rb index 78e206d2e8f3c..a66e251dc6dc0 100644 --- a/decidim-core/spec/types/user_type_spec.rb +++ b/decidim-core/spec/types/user_type_spec.rb @@ -13,31 +13,31 @@ module Core include_examples "timestamps interface" include_examples "followable interface" + shared_examples "unauthorized User object" do + it "throws Decidim::Api::Errors::UnauthorizedObjectError" do + expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedObjectError, "You cannot view or edit this User because you do not have permissions") + end + end + describe "unconfirmed user" do let(:model) { create(:user) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized User object" end describe "deleted user" do let(:model) { create(:user, :deleted) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized User object" end describe "moderated user" do let(:model) { create(:user, :blocked) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized User object" end describe "name" do @@ -124,9 +124,7 @@ module Core context "when user is deleted" do let(:model) { create(:user, :deleted) } - it "returns empty" do - expect(response).to be_nil - end + it_behaves_like "unauthorized User object" end end diff --git a/decidim-debates/spec/types/debate_type_spec.rb b/decidim-debates/spec/types/debate_type_spec.rb index bf11613707e2e..ee604b48146e3 100644 --- a/decidim-debates/spec/types/debate_type_spec.rb +++ b/decidim-debates/spec/types/debate_type_spec.rb @@ -23,6 +23,12 @@ module Debates let(:model) { create(:debate, :ongoing_ama, :with_likes) } end + shared_examples "unauthorized Debate" do + it "throws Decidim::Api::Errors::UnauthorizedObjectError" do + expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedObjectError, "You cannot view or edit this Debate because you do not have permissions") + end + end + describe "id" do let(:query) { "{ id }" } @@ -140,9 +146,7 @@ module Debates let(:model) { create(:debate, :ongoing_ama, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Debate" end context "when participatory space is private but transparent" do @@ -162,9 +166,7 @@ module Debates let(:model) { create(:debate, :ongoing_ama, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Debate" end context "when component is not published" do @@ -172,9 +174,7 @@ module Debates let(:model) { create(:debate, :ongoing_ama, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Debate" end context "when debate is moderated" do @@ -182,9 +182,7 @@ module Debates let(:query) { "{ id }" } let(:root_value) { model.reload } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Debate" end end end diff --git a/decidim-forms/spec/types/questionnaire_type_spec.rb b/decidim-forms/spec/types/questionnaire_type_spec.rb index 7d84f0821f489..f120e944a6601 100644 --- a/decidim-forms/spec/types/questionnaire_type_spec.rb +++ b/decidim-forms/spec/types/questionnaire_type_spec.rb @@ -85,11 +85,11 @@ module Forms end end - context "when meeting is no published" do + context "when meeting is not published" do let(:meeting) { create(:meeting) } - it "returns the questionnaire's entity corresponding to questionnaire_for_id" do - expect(response["forEntity"]).to be_nil + it "throws Decidim::Api::Errors::UnauthorizedObjectError" do + expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedObjectError, "You cannot view or edit this Meeting because you do not have permissions") end end end diff --git a/decidim-meetings/spec/types/agenda_type_spec.rb b/decidim-meetings/spec/types/agenda_type_spec.rb index 5ac11e57989cc..765d6f3facb07 100644 --- a/decidim-meetings/spec/types/agenda_type_spec.rb +++ b/decidim-meetings/spec/types/agenda_type_spec.rb @@ -42,8 +42,8 @@ module Meetings let(:model) { create(:agenda, :with_agenda_items, visible: false) } - it "returns the agenda's id" do - expect(response).to be_nil + it "throws Decidim::Api::Errors::UnauthorizedObjectError" do + expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedObjectError, "You cannot view or edit this MeetingAgenda because you do not have permissions") end end end diff --git a/decidim-meetings/spec/types/meeting_type_spec.rb b/decidim-meetings/spec/types/meeting_type_spec.rb index 64262900c7570..52a545afff19b 100644 --- a/decidim-meetings/spec/types/meeting_type_spec.rb +++ b/decidim-meetings/spec/types/meeting_type_spec.rb @@ -24,6 +24,12 @@ module Meetings include_examples "referable interface" include_examples "localizable interface" + shared_examples "unauthorized Meeting" do + it "throws Decidim::Api::Errors::UnauthorizedObjectError" do + expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedObjectError, "You cannot view or edit this Meeting because you do not have permissions") + end + end + describe "id" do let(:query) { "{ id }" } @@ -72,9 +78,7 @@ module Meetings context "when is not set" do let(:model) { create(:meeting, component:) } - it "returns the publishedAt field" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Meeting" end end @@ -433,9 +437,7 @@ module Meetings let(:root_value) { model.reload } let!(:current_user) { create(:user, :confirmed, organization: current_organization) } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Meeting" end describe "transparent" do @@ -452,9 +454,7 @@ module Meetings let(:model) { create(:meeting, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Meeting" end context "when participatory space is private but transparent" do @@ -474,9 +474,7 @@ module Meetings let(:model) { create(:meeting, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Meeting" end context "when component is not published" do @@ -484,9 +482,7 @@ module Meetings let(:model) { create(:meeting, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Meeting" end context "when meeting is moderated" do @@ -494,9 +490,7 @@ module Meetings let(:query) { "{ id }" } let(:root_value) { model.reload } - it "returns all the required fields" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Meeting" end context "when meeting is not published" do @@ -504,9 +498,7 @@ module Meetings let(:query) { "{ id }" } let(:root_value) { model.reload } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Meeting" end end end diff --git a/decidim-proposals/lib/decidim/api/mutations/proposal_answer_type.rb b/decidim-proposals/lib/decidim/api/mutations/proposal_answer_type.rb index c9c8f99388229..f5fbbdc621443 100644 --- a/decidim-proposals/lib/decidim/api/mutations/proposal_answer_type.rb +++ b/decidim-proposals/lib/decidim/api/mutations/proposal_answer_type.rb @@ -34,14 +34,8 @@ def resolve(attributes:) return object end on(:invalid) do - return GraphQL::ExecutionError.new( - form.errors.full_messages.join(", ") - ) + raise GraphQL::ExecutionError, form.errors.full_messages.join(", ") end - - GraphQL::ExecutionError.new( - I18n.t("decidim.proposals.admin.proposals.answer.invalid") - ) end end diff --git a/decidim-proposals/spec/shared/proposal_mutation_examples.rb b/decidim-proposals/spec/shared/proposal_mutation_examples.rb index dcfb02f94d840..76ae524fb1cd4 100644 --- a/decidim-proposals/spec/shared/proposal_mutation_examples.rb +++ b/decidim-proposals/spec/shared/proposal_mutation_examples.rb @@ -2,8 +2,8 @@ shared_examples "manage proposal mutation examples" do context "when proposal answering disabled" do - it "does not answer the proposal" do - expect(response["answer"]).to be_nil + it "throws Decidim::Api::Errors::UnauthorizedFieldError" do + expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedFieldError, "You cannot view or edit answer field on Answer because you do not have permission") end end diff --git a/decidim-proposals/spec/types/proposal_mutation_type_spec.rb b/decidim-proposals/spec/types/proposal_mutation_type_spec.rb index 31f11a0653a70..08850130ae4ce 100644 --- a/decidim-proposals/spec/types/proposal_mutation_type_spec.rb +++ b/decidim-proposals/spec/types/proposal_mutation_type_spec.rb @@ -69,8 +69,8 @@ module Proposals end context "with normal user" do - it "returns nil" do - expect(response["answer"]).to be_nil + it "throws Decidim::Api::Errors::UnauthorizedFieldError" do + expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedFieldError, "You cannot view or edit answer field on Answer because you do not have permission") end end diff --git a/decidim-proposals/spec/types/proposal_type_spec.rb b/decidim-proposals/spec/types/proposal_type_spec.rb index 68115fc8f63f9..7b4447837d1c3 100644 --- a/decidim-proposals/spec/types/proposal_type_spec.rb +++ b/decidim-proposals/spec/types/proposal_type_spec.rb @@ -26,6 +26,12 @@ module Proposals include_examples "localizable interface" include_examples "followable interface" + shared_examples "unauthorized Proposal" do + it "throws Decidim::Api::Errors::UnauthorizedObjectError" do + expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedObjectError, "You cannot view or edit this Proposal because you do not have permissions") + end + end + describe "id" do let(:query) { "{ id }" } @@ -275,9 +281,7 @@ module Proposals let(:model) { create(:proposal, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Proposal" end context "when participatory space is private but transparent" do @@ -297,9 +301,7 @@ module Proposals let(:model) { create(:proposal, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Proposal" end context "when component is not published" do @@ -307,9 +309,7 @@ module Proposals let(:model) { create(:proposal, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Proposal" end context "when proposal is moderated" do @@ -317,9 +317,7 @@ module Proposals let(:query) { "{ id }" } let(:root_value) { model.reload } - it "returns all the required fields" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Proposal" end end end From 085ede4f5653c9deb5f545bafba2d7a5e3839e39 Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Fri, 12 Dec 2025 14:35:26 +0200 Subject: [PATCH 015/116] Refactor QueryExtensions to move the implementation to API (#15768) * Refactor QueryType to API * Remove bullet error * Remove unneded autoload * Fix typo * Disable bullet * Fix bullet error --- .github/workflows/ci_api.yml | 1 + decidim-api/lib/decidim/api/query_type.rb | 91 +++++++++++++++ .../spec/lib/decidim/api/query_type_spec.rb | 4 +- .../decidim/api/functions/user_entity_list.rb | 1 - decidim-core/lib/decidim/core.rb | 1 - decidim-core/lib/decidim/core/engine.rb | 2 - decidim-core/lib/decidim/query_extensions.rb | 105 ------------------ 7 files changed, 94 insertions(+), 111 deletions(-) rename decidim-core/spec/lib/query_extensions_spec.rb => decidim-api/spec/lib/decidim/api/query_type_spec.rb (99%) delete mode 100644 decidim-core/lib/decidim/query_extensions.rb diff --git a/.github/workflows/ci_api.yml b/.github/workflows/ci_api.yml index 855c44d3fac4e..e7f078c2a2175 100644 --- a/.github/workflows/ci_api.yml +++ b/.github/workflows/ci_api.yml @@ -33,5 +33,6 @@ jobs: with: working-directory: "decidim-api" test_command: bundle exec parallel_test --type rspec --pattern spec/ + bullet_enabled: true bullet_n_plus_one: true bullet_unused_eager_loading: true diff --git a/decidim-api/lib/decidim/api/query_type.rb b/decidim-api/lib/decidim/api/query_type.rb index cbce0ddee80a3..fcb4fc4bd1c83 100644 --- a/decidim-api/lib/decidim/api/query_type.rb +++ b/decidim-api/lib/decidim/api/query_type.rb @@ -5,6 +5,97 @@ module Api # This type represents the root query type of the whole API. class QueryType < Decidim::Api::Types::BaseObject description "The root query of this schema" + + field :component, Decidim::Core::ComponentInterface, null: true do + description "Lists the components this space contains." + argument :id, GraphQL::Types::ID, required: true, description: "The ID of the component to be found" + end + field :decidim, Core::DecidimType, "Decidim's framework properties.", null: true + field :moderated_users, type: [Decidim::Core::UserModerationType], null: true, + description: "The moderated users for the current organization" + field :moderations, type: [Decidim::Core::ModerationType], null: true, + description: "The moderation for the current organization" + field :organization, Core::OrganizationType, "The current organization", null: true + field :participant_details, type: Decidim::Core::ParticipantDetailsType, null: true do + description "Participant details visible to admin users only" + argument :id, GraphQL::Types::ID, "The ID of the participant", required: true + argument :nickname, GraphQL::Types::String, "The @nickname of the participant", required: false + end + field :session, Core::SessionType, description: "Return's information about the logged in user", null: true + field :static_page_topics, type: [Decidim::Core::StaticPageTopicType], null: true, + description: "The static page topics for the current organization" + field :static_pages, type: [Decidim::Core::StaticPageType], null: true, + description: "The static pages for the current organization" + field :user, + type: Core::UserType, null: true, + description: "A participant (user or group) in the current organization" do + argument :id, GraphQL::Types::ID, "The ID of the participant", required: false + argument :nickname, GraphQL::Types::String, "The @nickname of the participant", required: false + end + field :users, + type: [Core::UserType], null: true, + description: "The participants (users or groups) for the current organization" do + argument :filter, Decidim::Core::UserEntityInputFilter, "Provides several methods to filter the results", required: false + argument :order, Decidim::Core::UserEntityInputSort, "Provides several methods to order the results", required: false + end + + def component(id: {}) + component = Decidim::Component.published.find_by(id:) + component&.organization == context[:current_organization] ? component : nil + end + + def session + context[:current_user] + end + + def decidim + Decidim + end + + def organization + context[:current_organization] + end + + def user(id: nil, nickname: nil) + Core::UserEntityFinder.new.call(object, { id:, nickname: }, context) + end + + def users(filter: {}, order: {}) + Core::UserEntityList.new.call(object, { filter:, order: }, context) + end + + def participant_details(id: nil, nickname: nil) + participant = Decidim::Core::UserEntityFinder.new.call(object, { id: id, nickname: nickname }, context) + return nil unless participant + + return nil unless Decidim::Core::ParticipantDetailsType.authorized?(participant, context) + + Decidim::ActionLogger.log( + "read", + context[:current_user], + participant, + nil, + {} + ) + + participant + end + + def static_pages + Decidim::StaticPage.accessible_for(organization, context[:current_user]) + end + + def static_page_topics + static_pages.collect(&:topic).uniq.compact_blank + end + + def moderated_users + Decidim::UserModeration.joins(:user).where(decidim_users: { decidim_organization_id: organization&.id }).where.not(decidim_users: { blocked_at: nil }) + end + + def moderations + Decidim::Moderation.where(participatory_space: organization.participatory_spaces).includes(:reports).hidden + end end end end diff --git a/decidim-core/spec/lib/query_extensions_spec.rb b/decidim-api/spec/lib/decidim/api/query_type_spec.rb similarity index 99% rename from decidim-core/spec/lib/query_extensions_spec.rb rename to decidim-api/spec/lib/decidim/api/query_type_spec.rb index e271e2ed17746..de9e300ba03eb 100644 --- a/decidim-core/spec/lib/query_extensions_spec.rb +++ b/decidim-api/spec/lib/decidim/api/query_type_spec.rb @@ -4,8 +4,8 @@ require "decidim/api/test" module Decidim - module Core - describe Decidim::Api::QueryType do + module Api + describe QueryType do include_context "with a graphql class type" describe "component" do diff --git a/decidim-core/lib/decidim/api/functions/user_entity_list.rb b/decidim-core/lib/decidim/api/functions/user_entity_list.rb index b125206d8a502..c38623ec99015 100644 --- a/decidim-core/lib/decidim/api/functions/user_entity_list.rb +++ b/decidim-core/lib/decidim/api/functions/user_entity_list.rb @@ -21,7 +21,6 @@ def call(_obj, args, ctx) .where(organization: ctx[:current_organization]) .confirmed .not_blocked - .includes(avatar_attachment: :blob) add_filter_keys(args[:filter]) add_order_keys(args[:order].to_h) @query diff --git a/decidim-core/lib/decidim/core.rb b/decidim-core/lib/decidim/core.rb index f194a6cdfbf30..d91b8c1a79418 100644 --- a/decidim-core/lib/decidim/core.rb +++ b/decidim-core/lib/decidim/core.rb @@ -74,7 +74,6 @@ module Decidim autoload :Searchable, "decidim/searchable" autoload :FilterableResource, "decidim/filterable_resource" autoload :SearchResourceFieldsMapper, "decidim/search_resource_fields_mapper" - autoload :QueryExtensions, "decidim/query_extensions" autoload :ParticipatorySpaceResourceable, "decidim/participatory_space_resourceable" autoload :HasPrivateUsers, "decidim/has_private_users" autoload :ViewModel, "decidim/view_model" diff --git a/decidim-core/lib/decidim/core/engine.rb b/decidim-core/lib/decidim/core/engine.rb index 07d6a4536f9a8..ffa5b3726c457 100644 --- a/decidim-core/lib/decidim/core/engine.rb +++ b/decidim-core/lib/decidim/core/engine.rb @@ -365,8 +365,6 @@ def named_variants end initializer "decidim_core.graphql_api" do - Decidim::Api::QueryType.include Decidim::QueryExtensions - Decidim::Api.add_orphan_type Decidim::Core::UserType end diff --git a/decidim-core/lib/decidim/query_extensions.rb b/decidim-core/lib/decidim/query_extensions.rb deleted file mode 100644 index 660eddeb10ee1..0000000000000 --- a/decidim-core/lib/decidim/query_extensions.rb +++ /dev/null @@ -1,105 +0,0 @@ -# frozen_string_literal: true - -module Decidim - # This module's job is to extend the API with custom fields related to - # decidim-core. - module QueryExtensions - # Public: Extends a type with `decidim-core`'s fields. - # - # type - A GraphQL::BaseType to extend. - # - # Returns nothing. - def self.included(type) - type.field :component, Decidim::Core::ComponentInterface, null: true do - description "Lists the components this space contains." - argument :id, GraphQL::Types::ID, required: true, description: "The ID of the component to be found" - end - type.field :session, Core::SessionType, description: "Return's information about the logged in user", null: true - type.field :decidim, Core::DecidimType, "Decidim's framework properties.", null: true - type.field :organization, Core::OrganizationType, "The current organization", null: true - type.field :user, - type: Core::UserType, null: true, - description: "A participant (user or group) in the current organization" do - argument :id, GraphQL::Types::ID, "The ID of the participant", required: false - argument :nickname, GraphQL::Types::String, "The @nickname of the participant", required: false - end - type.field :users, - type: [Core::UserType], null: true, - description: "The participants (users or groups) for the current organization" do - argument :order, Decidim::Core::UserEntityInputSort, "Provides several methods to order the results", required: false - argument :filter, Decidim::Core::UserEntityInputFilter, "Provides several methods to filter the results", required: false - end - type.field :participant_details, type: Decidim::Core::ParticipantDetailsType, null: true do - description "Participant details visible to admin users only" - argument :id, GraphQL::Types::ID, "The ID of the participant", required: true - argument :nickname, GraphQL::Types::String, "The @nickname of the participant", required: false - end - type.field :static_pages, type: [Decidim::Core::StaticPageType], null: true, - description: "The static pages for the current organization" - type.field :static_page_topics, type: [Decidim::Core::StaticPageTopicType], null: true, - description: "The static page topics for the current organization" - type.field :moderated_users, type: [Decidim::Core::UserModerationType], null: true, - description: "The moderated users for the current organization" - type.field :moderations, type: [Decidim::Core::ModerationType], null: true, - description: "The moderation for the current organization" - end - - def component(id: {}) - component = Decidim::Component.published.find_by(id:) - component&.organization == context[:current_organization] ? component : nil - end - - def session - context[:current_user] - end - - def decidim - Decidim - end - - def organization - context[:current_organization] - end - - def user(id: nil, nickname: nil) - Core::UserEntityFinder.new.call(object, { id:, nickname: }, context) - end - - def users(filter: {}, order: {}) - Core::UserEntityList.new.call(object, { filter:, order: }, context) - end - - def participant_details(id: nil, nickname: nil) - participant = Decidim::Core::UserEntityFinder.new.call(object, { id: id, nickname: nickname }, context) - return nil unless participant - - return nil unless Decidim::Core::ParticipantDetailsType.authorized?(participant, context) - - Decidim::ActionLogger.log( - "read", - context[:current_user], - participant, - nil, - {} - ) - - participant - end - - def static_pages - Decidim::StaticPage.accessible_for(organization, context[:current_user]) - end - - def static_page_topics - static_pages.collect(&:topic).uniq.compact_blank - end - - def moderated_users - Decidim::UserModeration.joins(:user).where(decidim_users: { decidim_organization_id: organization&.id }).where.not(decidim_users: { blocked_at: nil }) - end - - def moderations - Decidim::Moderation.where(participatory_space: organization.participatory_spaces).includes(:reports).hidden - end - end -end From cb5212d63af63c22af8d8530c438058906c7a96d Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Sat, 13 Dec 2025 09:23:44 +0200 Subject: [PATCH 016/116] Extract breadcrumb related specs to their own tests (#15737) * Extract each one spec in own test * Fix budgets specs * Add propsoals specs * Fix failing specs * Update decidim-proposals/spec/system/proposals_breadcrumbs_spec.rb Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update decidim-accountability/spec/system/explore_versions_spec.rb Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Extract results * Extract surveys specs * Extract surveys specs part 2 * Fully extract proposals * Fully extract proposals * Fix typo * Extract meeetings * Extract elections * Extract debates * Extract collaborative texts * Extract blogs * Create uniform specs * Fix filename * Apply review recommendations * Standardize tests * Fix specs --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../system/accountability_breadcrumbs_spec.rb | 86 +++++++++++++ .../spec/system/explore_results_spec.rb | 14 --- .../spec/system/explore_versions_spec.rb | 4 - ...spec.rb => assemblies_breadcrumbs_spec.rb} | 2 +- .../spec/system/blogs_breadcrumbs_spec.rb | 34 +++++ .../spec/system/explore_posts_spec.rb | 9 +- .../decidim/budgets/projects_controller.rb | 22 ++++ .../spec/system/budgets_breadcrumbs_spec.rb | 52 ++++++++ .../collaborative_tests_breadcrumbs_spec.rb | 39 ++++++ .../system/user_sends_suggestions_spec.rb | 8 -- ...pec.rb => conferences_breadcrumbs_spec.rb} | 2 +- .../spec/system/debates_breadcrumbs_spec.rb | 57 +++++++++ .../spec/system/debates_versions_spec.rb | 4 - decidim-debates/spec/system/show_spec.rb | 4 - .../spec/system/elections_breadcrumbs_spec.rb | 34 +++++ .../system/user_votes_in_an_election_spec.rb | 4 - ...pec.rb => initiatives_breadcrumbs_spec.rb} | 2 +- .../decidim/meetings/meetings_controller.rb | 2 +- .../spec/system/explore_meetings_spec.rb | 4 - .../spec/system/explore_versions_spec.rb | 4 - .../spec/system/meetings_breadcrumbs_spec.rb | 65 ++++++++++ decidim-meetings/spec/system/show_spec.rb | 5 - ...rticipatory_processes_breadcrumbs_spec.rb} | 2 +- .../decidim/proposals/proposals_controller.rb | 30 +++-- .../spec/system/proposal_show_spec.rb | 3 - .../spec/system/proposals_breadcrumbs_spec.rb | 119 ++++++++++++++++++ .../spec/system/proposals_spec.rb | 5 - .../spec/system/proposals_versions_spec.rb | 5 - decidim-surveys/spec/system/survey_spec.rb | 9 -- .../spec/system/surveys_breadcrumbs_spec.rb | 53 ++++++++ 30 files changed, 587 insertions(+), 96 deletions(-) create mode 100644 decidim-accountability/spec/system/accountability_breadcrumbs_spec.rb rename decidim-assemblies/spec/system/{assembly_breadcrumb_spec.rb => assemblies_breadcrumbs_spec.rb} (99%) create mode 100644 decidim-blogs/spec/system/blogs_breadcrumbs_spec.rb create mode 100644 decidim-budgets/spec/system/budgets_breadcrumbs_spec.rb create mode 100644 decidim-collaborative_texts/spec/system/collaborative_tests_breadcrumbs_spec.rb rename decidim-conferences/spec/system/{conference_breadcrumb_spec.rb => conferences_breadcrumbs_spec.rb} (96%) create mode 100644 decidim-debates/spec/system/debates_breadcrumbs_spec.rb create mode 100644 decidim-elections/spec/system/elections_breadcrumbs_spec.rb rename decidim-initiatives/spec/system/{initiative_breadcrumb_spec.rb => initiatives_breadcrumbs_spec.rb} (96%) create mode 100644 decidim-meetings/spec/system/meetings_breadcrumbs_spec.rb rename decidim-participatory_processes/spec/system/{participatory_process_breadcrumb_spec.rb => participatory_processes_breadcrumbs_spec.rb} (98%) create mode 100644 decidim-proposals/spec/system/proposals_breadcrumbs_spec.rb create mode 100644 decidim-surveys/spec/system/surveys_breadcrumbs_spec.rb diff --git a/decidim-accountability/spec/system/accountability_breadcrumbs_spec.rb b/decidim-accountability/spec/system/accountability_breadcrumbs_spec.rb new file mode 100644 index 0000000000000..61b89ac754456 --- /dev/null +++ b/decidim-accountability/spec/system/accountability_breadcrumbs_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Accountability Breadcrumb" do + include_context "with a component" + + let(:manifest_name) { "accountability" } + let!(:results) { create_list(:result, 5, component:) } + + describe "index" do + let(:path) { decidim_participatory_process_accountability.results_path(participatory_process_slug: participatory_process.slug, component_id: component.id, locale: I18n.locale) } + + before do + visit path + end + + it "shows the correct information in breadcrumb (space, component)" do + within(".menu-bar") do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + end + end + end + + describe "show" do + let(:path) { decidim_participatory_process_accountability.result_path(id: result.id, participatory_process_slug: participatory_process.slug, component_id: component.id, locale: I18n.locale) } + let(:results_count) { 1 } + let(:result) { results.first } + + before do + visit path + end + + it "shows the correct information in breadcrumb (space, component, result)" do + within(".menu-bar") do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + expect(page).to have_content(translated(result.title)) + end + end + + context "with subresults" do + let!(:subresults) { create_list(:result, 3, component:, parent: result) } + let(:first_subresult) { subresults.first } + + before do + visit current_path + end + + it "shows the correct information in breadcrumb (space, component, result, subresult)" do + click_on translated(first_subresult.title) + within(".menu-bar") do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + expect(page).to have_content(translated(result.title)) + expect(page).to have_content(translated(first_subresult.title)) + end + end + end + end + + describe "versions", versioning: true do + let!(:result) { create(:result, progress: 25.0, component:) } + let(:path) { decidim_participatory_process_accountability.result_path(id: result.id, participatory_process_slug: participatory_process.slug, component_id: component.id, locale: I18n.locale) } + + before do + Decidim.traceability.update!( + result, + "test suite", + progress: 50.0 + ) + visit path + + click_on "see other versions" + end + + it "shows the correct information in breadcrumb (space, component, result)" do + within(".menu-bar") do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + expect(page).to have_content(translated(result.title)) + end + end + end +end diff --git a/decidim-accountability/spec/system/explore_results_spec.rb b/decidim-accountability/spec/system/explore_results_spec.rb index 78df1e1ab6f97..eeaf643e24aba 100644 --- a/decidim-accountability/spec/system/explore_results_spec.rb +++ b/decidim-accountability/spec/system/explore_results_spec.rb @@ -155,10 +155,6 @@ end it "shows all results for the given process and taxonomy" do - within(".menu-bar") do - expect(page).to have_content(translated(component.name)) - end - within("#results") do expect(page).to have_css(".card__list", count: results_count) @@ -179,10 +175,6 @@ end it "shows all result info" do - within(".menu-bar") do - expect(page).to have_content(translated(component.name)) - expect(page).to have_content(translated(result.title)) - end expect(page).to have_i18n_content(result.title) expect(page).to have_i18n_content(result.description, strip_tags: true) expect(page).to have_content(result.reference) @@ -278,12 +270,6 @@ it "the result is mentioned in the subresult page" do click_on translated(first_subresult.title) expect(page).to have_i18n_content(result.title) - - within(".menu-bar") do - expect(page).to have_content(translated(component.name)) - expect(page).to have_content(translated(result.title)) - expect(page).to have_content(translated(first_subresult.title)) - end end it "a banner links back to the result" do diff --git a/decidim-accountability/spec/system/explore_versions_spec.rb b/decidim-accountability/spec/system/explore_versions_spec.rb index bdaf47efd7817..c2feb05e51c1e 100644 --- a/decidim-accountability/spec/system/explore_versions_spec.rb +++ b/decidim-accountability/spec/system/explore_versions_spec.rb @@ -37,10 +37,6 @@ end it "lists all versions" do - within(".menu-bar") do - expect(page).to have_content(translated(component.name)) - expect(page).to have_content(translated(result.title)) - end expect(page).to have_link("Version 1 of 2") expect(page).to have_link("Version 2 of 2") end diff --git a/decidim-assemblies/spec/system/assembly_breadcrumb_spec.rb b/decidim-assemblies/spec/system/assemblies_breadcrumbs_spec.rb similarity index 99% rename from decidim-assemblies/spec/system/assembly_breadcrumb_spec.rb rename to decidim-assemblies/spec/system/assemblies_breadcrumbs_spec.rb index bd826e5802827..491d5563bca50 100644 --- a/decidim-assemblies/spec/system/assembly_breadcrumb_spec.rb +++ b/decidim-assemblies/spec/system/assemblies_breadcrumbs_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -describe "Assembly Breadcrumb" do +describe "Assemblies Breadcrumb" do let(:organization) { create(:organization) } let(:parent_assembly) { create(:assembly, :published, organization:) } let(:child_assembly) { create(:assembly, :published, organization:, parent: parent_assembly) } diff --git a/decidim-blogs/spec/system/blogs_breadcrumbs_spec.rb b/decidim-blogs/spec/system/blogs_breadcrumbs_spec.rb new file mode 100644 index 0000000000000..d3d4dd0b6bf99 --- /dev/null +++ b/decidim-blogs/spec/system/blogs_breadcrumbs_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Blogs Breadcrumb" do + include_context "with a component" + let(:manifest_name) { "blogs" } + let!(:post) { create(:post, component:) } + + before do + visit_component + end + + describe "index" do + it "shows the correct information in breadcrumb (space, component)" do + within(".menu-bar") do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + end + end + end + + describe "show" do + it "shows the correct information in breadcrumb (space, component, post)" do + click_on translated(post.title) + + within(".menu-bar") do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + expect(page).to have_content(translated(post.title)) + end + end + end +end diff --git a/decidim-blogs/spec/system/explore_posts_spec.rb b/decidim-blogs/spec/system/explore_posts_spec.rb index 9cba82f5d0380..babc8cdd00eb2 100644 --- a/decidim-blogs/spec/system/explore_posts_spec.rb +++ b/decidim-blogs/spec/system/explore_posts_spec.rb @@ -37,10 +37,13 @@ visit_component end - it "shows the component name in the sidebar" do + it "shows the correct information in breadcrumb" do within(".menu-bar") do expect(page).to have_content(translated(component.name)) end + end + + it "shows the component name in the sidebar" do within("aside") do expect(page).to have_content(translated(component.name)) end @@ -89,10 +92,6 @@ within ".author__name" do expect(page).to have_content("Official") end - within(".menu-bar") do - expect(page).to have_content(translated(component.name)) - expect(page).to have_content(translated(post.title)) - end end end diff --git a/decidim-budgets/app/controllers/decidim/budgets/projects_controller.rb b/decidim-budgets/app/controllers/decidim/budgets/projects_controller.rb index 9f0508765e23c..7d25b6cf20497 100644 --- a/decidim-budgets/app/controllers/decidim/budgets/projects_controller.rb +++ b/decidim-budgets/app/controllers/decidim/budgets/projects_controller.rb @@ -100,6 +100,28 @@ def items } ].select { |item| item[:enabled] } end + + def add_breadcrumb_item + return {} if project.blank? + + { + label: translated_attribute(project.title), + url: Decidim::EngineRouter.main_proxy(current_component).budget_project_url(budget, project, locale: current_locale), + active: false, + resource: project + } + end + + def add_parent_breadcrumb_item + return {} if budget.blank? + + { + label: translated_attribute(budget.title), + url: Decidim::EngineRouter.main_proxy(current_component).budget_projects_url(budget, locale: current_locale), + active: false, + resource: budget + } + end end end end diff --git a/decidim-budgets/spec/system/budgets_breadcrumbs_spec.rb b/decidim-budgets/spec/system/budgets_breadcrumbs_spec.rb new file mode 100644 index 0000000000000..a491d85ed8048 --- /dev/null +++ b/decidim-budgets/spec/system/budgets_breadcrumbs_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Budgets Breadcrumb" do + let(:organization) { create(:organization) } + let(:participatory_space) { create(:participatory_process, :with_steps, :published, organization:, title: { "en" => "Participatory space" }) } + let(:component) { create(:budgets_component, :published, :with_votes_disabled, participatory_space:, name: { "en" => "Component" }) } + let(:budget) { create(:budget, component:, title: { "en" => "Budget" }) } + let!(:project) { create(:project, budget:, title: { "en" => "Project" }) } + let(:router) { Decidim::EngineRouter.main_proxy(component) } + + before do + switch_to_host(organization.host) + end + + context "when visiting the budgets index page" do + it "shows the correct information in breadcrumb (space, component)" do + visit router.root_path(locale: I18n.locale) + + within ".menu-bar" do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + end + end + end + + context "when visiting single budget page" do + it "shows the correct information in breadcrumb (space, component, budget)" do + visit router.budget_path(budget, locale: I18n.locale) + + within ".menu-bar" do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + expect(page).to have_content(translated(budget.title)) + end + end + end + + context "when visiting single project page" do + it "shows the correct information in breadcrumb (space, component, budget, project)" do + visit router.budget_project_path(budget, project, locale: I18n.locale) + + within ".menu-bar" do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + expect(page).to have_content(translated(budget.title)) + expect(page).to have_content(translated(project.title)) + end + end + end +end diff --git a/decidim-collaborative_texts/spec/system/collaborative_tests_breadcrumbs_spec.rb b/decidim-collaborative_texts/spec/system/collaborative_tests_breadcrumbs_spec.rb new file mode 100644 index 0000000000000..23e02bf5cc047 --- /dev/null +++ b/decidim-collaborative_texts/spec/system/collaborative_tests_breadcrumbs_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "CollaborativeTexts Breadcrumb" do + include_context "with a component" + let(:manifest_name) { "collaborative_texts" } + let!(:component) do + create(:collaborative_text_component, + manifest:, + participatory_space: participatory_process) + end + let!(:document) { create(:collaborative_text_document, :with_body, :published, component:) } + + before do + visit_component + end + + describe "index" do + it "shows the correct information in breadcrumb (space, component)" do + within(".menu-bar") do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + end + end + end + + describe "show" do + it "shows the correct information in breadcrumb (space, component, document)" do + click_on document.title + + within(".menu-bar") do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + expect(page).to have_content(translated(document.title)) + end + end + end +end diff --git a/decidim-collaborative_texts/spec/system/user_sends_suggestions_spec.rb b/decidim-collaborative_texts/spec/system/user_sends_suggestions_spec.rb index 13bdf4b263e25..e12a570f1bc38 100644 --- a/decidim-collaborative_texts/spec/system/user_sends_suggestions_spec.rb +++ b/decidim-collaborative_texts/spec/system/user_sends_suggestions_spec.rb @@ -46,9 +46,6 @@ end it "lists all the documents" do - within(".menu-bar") do - expect(page).to have_content(translated(component.name)) - end within("aside") do expect(page).to have_content(translated(component.name)) end @@ -60,11 +57,6 @@ it "shows the document details" do click_on document.title - within(".menu-bar") do - expect(page).to have_content(translated(component.name)) - expect(page).to have_content(translated(document.title)) - end - expect(page).to have_content(translated(document.title)) within("aside") do expect(page).to have_content("Index") diff --git a/decidim-conferences/spec/system/conference_breadcrumb_spec.rb b/decidim-conferences/spec/system/conferences_breadcrumbs_spec.rb similarity index 96% rename from decidim-conferences/spec/system/conference_breadcrumb_spec.rb rename to decidim-conferences/spec/system/conferences_breadcrumbs_spec.rb index b55a1c4579ce2..22e97ae41826d 100644 --- a/decidim-conferences/spec/system/conference_breadcrumb_spec.rb +++ b/decidim-conferences/spec/system/conferences_breadcrumbs_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -describe "Conference Breadcrumb" do +describe "Conferences Breadcrumb" do let(:organization) { create(:organization) } let(:participatory_space) { create(:conference, :published, organization:) } let(:component) { create(:proposal_component, :published, participatory_space:) } diff --git a/decidim-debates/spec/system/debates_breadcrumbs_spec.rb b/decidim-debates/spec/system/debates_breadcrumbs_spec.rb new file mode 100644 index 0000000000000..2c7132a76255a --- /dev/null +++ b/decidim-debates/spec/system/debates_breadcrumbs_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Debates Breadcrumb" do + include_context "with a component" + let(:manifest_name) { "debates" } + + let!(:debate) { create(:debate, component:) } + + before do + visit_component + end + + describe "index" do + it "shows the correct information in breadcrumb (space, component)" do + within(".menu-bar") do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + end + end + end + + describe "show" do + it "shows the correct information in breadcrumb (space, component, debate)" do + click_on translated(debate.title), class: "card__list" + + within(".menu-bar") do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + expect(page).to have_content(translated(debate.title)) + end + end + end + + describe "versions", versioning: true do + let(:additional_description) { generate_localized_description(:debate_description) } + + before do + Decidim.traceability.update!( + debate, + "Dummy author", + description: additional_description + ) + click_on translated(debate.title), class: "card__list" + click_on "see other versions" + end + + it "shows the correct information in breadcrumb (space, component, debate)" do + within(".menu-bar") do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + expect(page).to have_content(translated(debate.title)) + end + end + end +end diff --git a/decidim-debates/spec/system/debates_versions_spec.rb b/decidim-debates/spec/system/debates_versions_spec.rb index 0cf4735a66503..f75e6b2c21be8 100644 --- a/decidim-debates/spec/system/debates_versions_spec.rb +++ b/decidim-debates/spec/system/debates_versions_spec.rb @@ -14,10 +14,6 @@ end it "has only one version" do - within(".menu-bar") do - expect(page).to have_content(translated(component.name)) - expect(page).to have_content(translated(debate.title)) - end expect(page).to have_content("Version number 1 (of 1)") end diff --git a/decidim-debates/spec/system/show_spec.rb b/decidim-debates/spec/system/show_spec.rb index 579d5f892591c..0dfda11ee6cf5 100644 --- a/decidim-debates/spec/system/show_spec.rb +++ b/decidim-debates/spec/system/show_spec.rb @@ -66,10 +66,6 @@ context "when shows the debate component" do it "shows the debate title" do - within(".menu-bar") do - expect(page).to have_content(translated(component.name)) - expect(page).to have_content(translated(debate.title)) - end expect(page).to have_content(translated(debate.title)) end end diff --git a/decidim-elections/spec/system/elections_breadcrumbs_spec.rb b/decidim-elections/spec/system/elections_breadcrumbs_spec.rb new file mode 100644 index 0000000000000..6b7f17285060f --- /dev/null +++ b/decidim-elections/spec/system/elections_breadcrumbs_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Elections Breadcrumb" do + include_context "with a component" + let(:manifest_name) { "elections" } + + let!(:election) { create(:election, :published, :finished, :with_internal_users_census, component:) } + + before do + visit_component + end + + describe "index" do + it "shows the correct information in breadcrumb (space, component)" do + within(".menu-bar") do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + end + end + end + + describe "show" do + it "shows the correct information in breadcrumb (space, component, election)" do + click_on translated(election.title) + within(".menu-bar") do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + expect(page).to have_content(translated(election.title)) + end + end + end +end diff --git a/decidim-elections/spec/system/user_votes_in_an_election_spec.rb b/decidim-elections/spec/system/user_votes_in_an_election_spec.rb index 54ec42daacb07..f0262fe5b9047 100644 --- a/decidim-elections/spec/system/user_votes_in_an_election_spec.rb +++ b/decidim-elections/spec/system/user_votes_in_an_election_spec.rb @@ -95,10 +95,6 @@ def election_vote_path(question) let(:election) { create(:election, :published, :finished, :with_internal_users_census) } it "does not allow to vote" do - within(".menu-bar") do - expect(page).to have_content(translated(election.component.name)) - expect(page).to have_content(translated(election.title)) - end expect(page).to have_no_link("Vote") expect(page).to have_no_content("You have already voted.") visit new_election_vote_path diff --git a/decidim-initiatives/spec/system/initiative_breadcrumb_spec.rb b/decidim-initiatives/spec/system/initiatives_breadcrumbs_spec.rb similarity index 96% rename from decidim-initiatives/spec/system/initiative_breadcrumb_spec.rb rename to decidim-initiatives/spec/system/initiatives_breadcrumbs_spec.rb index 5f1fbb136371b..fefd289a839e9 100644 --- a/decidim-initiatives/spec/system/initiative_breadcrumb_spec.rb +++ b/decidim-initiatives/spec/system/initiatives_breadcrumbs_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -describe "Initiative Breadcrumb" do +describe "Initiatives Breadcrumb" do let(:organization) { create(:organization) } let(:participatory_space) { create(:initiative, :open, organization:) } let(:component) { create(:meeting_component, :published, participatory_space:) } diff --git a/decidim-meetings/app/controllers/decidim/meetings/meetings_controller.rb b/decidim-meetings/app/controllers/decidim/meetings/meetings_controller.rb index be43cad6c15e9..e11828f8bec04 100644 --- a/decidim-meetings/app/controllers/decidim/meetings/meetings_controller.rb +++ b/decidim-meetings/app/controllers/decidim/meetings/meetings_controller.rb @@ -207,7 +207,7 @@ def add_breadcrumb_item { label: translated_attribute(meeting.title), - url: Decidim::EngineRouter.main_proxy(current_component).meeting_path(meeting), + url: Decidim::EngineRouter.main_proxy(current_component).meeting_path(meeting, locale: current_locale), active: false } end diff --git a/decidim-meetings/spec/system/explore_meetings_spec.rb b/decidim-meetings/spec/system/explore_meetings_spec.rb index a59311e7a6ef0..34a6e87e45cc7 100644 --- a/decidim-meetings/spec/system/explore_meetings_spec.rb +++ b/decidim-meetings/spec/system/explore_meetings_spec.rb @@ -29,10 +29,6 @@ it "shows all meetings for the given process" do visit_component - within(".menu-bar") do - expect(page).to have_content(translated(component.name)) - end - expect(page).to have_selector(meetings_selector, count: meetings_count) meetings.each do |meeting| diff --git a/decidim-meetings/spec/system/explore_versions_spec.rb b/decidim-meetings/spec/system/explore_versions_spec.rb index e13475d70e35f..7bcdcef10ece9 100644 --- a/decidim-meetings/spec/system/explore_versions_spec.rb +++ b/decidim-meetings/spec/system/explore_versions_spec.rb @@ -49,10 +49,6 @@ end it "lists all versions" do - within(".menu-bar") do - expect(page).to have_content(translated(component.name)) - expect(page).to have_content(translated(meeting.reload.title)) - end expect(page).to have_link("Version 1 of 2") expect(page).to have_link("Version 2 of 2") end diff --git a/decidim-meetings/spec/system/meetings_breadcrumbs_spec.rb b/decidim-meetings/spec/system/meetings_breadcrumbs_spec.rb new file mode 100644 index 0000000000000..2e4a1f22fcb9a --- /dev/null +++ b/decidim-meetings/spec/system/meetings_breadcrumbs_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Meetings Breadcrumb" do + include_context "with a component" + let(:manifest_name) { "meetings" } + + let!(:meeting) { create(:meeting, :published, component:) } + + before do + stub_geocoding_coordinates([meeting.latitude, meeting.longitude]) + visit_component + end + + describe "index" do + it "shows the correct information in breadcrumb (space, component)" do + within(".menu-bar") do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + end + end + end + + describe "show" do + it "shows the correct information in breadcrumb (space, component, meeting)" do + click_on meeting.title[I18n.locale.to_s] + within(".menu-bar") do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + expect(page).to have_content(translated(meeting.title)) + end + end + end + + describe "versions", versioning: true do + let(:meeting_path) do + decidim_participatory_process_meetings.meeting_path( + participatory_process_slug: participatory_process.slug, + component_id: component.id, + locale: I18n.locale, + id: meeting.id + ) + end + + before do + stub_geocoding_coordinates([meeting.latitude, meeting.longitude]) + Decidim.traceability.update!( + meeting, + "test suite", + title: { en: "My updated title" } + ) + visit meeting_path + click_on "see other versions" + end + + it "shows the correct information in breadcrumb (space, component, meeting)" do + within(".menu-bar") do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + expect(page).to have_content(translated(meeting.title)) + end + end + end +end diff --git a/decidim-meetings/spec/system/show_spec.rb b/decidim-meetings/spec/system/show_spec.rb index a05cbb334d831..d411bd9662ba2 100644 --- a/decidim-meetings/spec/system/show_spec.rb +++ b/decidim-meetings/spec/system/show_spec.rb @@ -42,11 +42,6 @@ end it "shows the correct time zone" do - within(".menu-bar") do - expect(page).to have_content(translated(component.name)) - expect(page).to have_content(translated(meeting.reload.title)) - end - expect(page).to have_content("HST") end end diff --git a/decidim-participatory_processes/spec/system/participatory_process_breadcrumb_spec.rb b/decidim-participatory_processes/spec/system/participatory_processes_breadcrumbs_spec.rb similarity index 98% rename from decidim-participatory_processes/spec/system/participatory_process_breadcrumb_spec.rb rename to decidim-participatory_processes/spec/system/participatory_processes_breadcrumbs_spec.rb index b99555e9e657c..2a807bb5c4e48 100644 --- a/decidim-participatory_processes/spec/system/participatory_process_breadcrumb_spec.rb +++ b/decidim-participatory_processes/spec/system/participatory_processes_breadcrumbs_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -describe "Participatory Process Breadcrumb" do +describe "Participatory Processes Breadcrumb" do let(:organization) { create(:organization) } let(:participatory_space) { create(:participatory_process, :published, organization:) } let(:component) { create(:proposal_component, :published, participatory_space:) } diff --git a/decidim-proposals/app/controllers/decidim/proposals/proposals_controller.rb b/decidim-proposals/app/controllers/decidim/proposals/proposals_controller.rb index c4022c86680bb..a7ad7ae1ecbe5 100644 --- a/decidim-proposals/app/controllers/decidim/proposals/proposals_controller.rb +++ b/decidim-proposals/app/controllers/decidim/proposals/proposals_controller.rb @@ -290,22 +290,26 @@ def default_view_mode @default_view_mode ||= current_component.settings.attachments_allowed? ? "grid" : "list" end + def add_parent_breadcrumb_item + return {} if proposal.blank? + + object = proposal.emendation? ? proposal.amendable : proposal + { + label: translated_attribute(object.title), + url: Decidim::EngineRouter.main_proxy(current_component).proposal_path(object), + active: false + } + end + def add_breadcrumb_item return {} if proposal.blank? + return {} if proposal.amendable? - if proposal.emendation? - { - label: translated_attribute(proposal.amendable.title), - url: Decidim::EngineRouter.main_proxy(current_component).proposal_path(proposal.amendable), - active: false - } - else - { - label: translated_attribute(proposal.title), - url: Decidim::EngineRouter.main_proxy(current_component).proposal_path(proposal), - active: false - } - end + { + label: I18n.t("decidim.amendments.name"), + url: Decidim::EngineRouter.main_proxy(current_component).proposal_path(proposal), + active: false + } end end end diff --git a/decidim-proposals/spec/system/proposal_show_spec.rb b/decidim-proposals/spec/system/proposal_show_spec.rb index 60e99f5cf1200..2f775e3297852 100644 --- a/decidim-proposals/spec/system/proposal_show_spec.rb +++ b/decidim-proposals/spec/system/proposal_show_spec.rb @@ -65,9 +65,6 @@ def visit_proposal end it "successfully shows the page" do - within(".menu-bar") do - expect(page).to have_content(translated(component.name)) - end expect(page).to have_content("Deleted participant") end end diff --git a/decidim-proposals/spec/system/proposals_breadcrumbs_spec.rb b/decidim-proposals/spec/system/proposals_breadcrumbs_spec.rb new file mode 100644 index 0000000000000..4a49f10d05730 --- /dev/null +++ b/decidim-proposals/spec/system/proposals_breadcrumbs_spec.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Proposals Breadcrumb" do + include_context "with a component" + + let(:organization) { create(:organization) } + let(:participatory_space) { create(:participatory_process, :with_steps, :published, organization:, title: { "en" => "Participatory space" }) } + let(:component) { create(:proposal_component, :published, :with_amendments_enabled, participatory_space:, name: { "en" => "Component" }) } + let(:proposal) { create(:proposal, component:, title: { "en" => "Proposal" }) } + let(:router) { Decidim::EngineRouter.main_proxy(component) } + + before do + switch_to_host(organization.host) + end + + describe "index" do + it "shows the correct information in breadcrumb (space, component)" do + visit router.root_path(locale: I18n.locale) + + within ".menu-bar" do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + end + end + end + + describe "show" do + it "shows the correct information in breadcrumb (space, component, proposal)" do + visit router.proposal_path(proposal, locale: I18n.locale) + + within ".menu-bar" do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + expect(page).to have_content(translated(proposal.title)) + end + end + + context "when it is an official proposal" do + let(:content) { generate_localized_title } + let!(:official_proposal) { create(:proposal, :official, body: content, component:) } + let!(:official_proposal_title) { translated(official_proposal.title) } + + before do + visit_component + click_on official_proposal_title + end + + it "shows the correct information in breadcrumb (space, component, proposal)" do + within(".menu-bar") do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + expect(page).to have_content(translated(official_proposal.title)) + end + end + end + end + + describe "versions", versioning: true do + let!(:amendment) { create(:amendment, amendable: proposal, emendation:) } + let!(:emendation) { create(:proposal, body: { en: "Amended One liner body" }, component:) } + + let(:form) do + Decidim::Amendable::ReviewForm.from_params( + id: amendment.id, + amendable_gid: proposal.to_sgid.to_s, + emendation_gid: emendation.to_sgid.to_s, + emendation_params: { title: emendation.title, body: emendation.body } + ) + end + let(:command) { Decidim::Amendable::Accept.new(form) } + + before do + visit router.proposal_path(proposal, locale: I18n.locale) + command.call + click_on "see other versions" + click_on("Version 2 of 2") + end + + it "shows the correct information in breadcrumb (space, component, proposal)" do + within(".menu-bar") do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + expect(page).to have_content(translated(proposal.reload.title)) + end + end + end + + context "when visiting single amendment page", versioning: true do + let!(:emendation) { create(:proposal, title: { en: "Amended Long enough title" }, component:) } + let!(:amendment) { create(:amendment, amendable: proposal, emendation:) } + let(:form) do + Decidim::Amendable::ReviewForm.from_params( + id: amendment.id, + amendable_gid: proposal.to_sgid.to_s, + emendation_gid: emendation.to_sgid.to_s, + emendation_params: { title: emendation.title, body: emendation.body } + ) + end + let(:command) { Decidim::Amendable::Accept.new(form) } + + before do + component.update!(settings: { amendments_enabled: true }) + command.call + end + + it "shows the correct information in breadcrumb (space, component, amendment)" do + visit router.proposal_path(emendation, locale: I18n.locale) + + within ".menu-bar" do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + expect(page).to have_content(translated(emendation.title)) + expect(page).to have_content("Amendment") + end + end + end +end diff --git a/decidim-proposals/spec/system/proposals_spec.rb b/decidim-proposals/spec/system/proposals_spec.rb index a3b78bd55fb55..0c483739de399 100644 --- a/decidim-proposals/spec/system/proposals_spec.rb +++ b/decidim-proposals/spec/system/proposals_spec.rb @@ -89,11 +89,6 @@ end it "shows the author as official" do - within(".menu-bar") do - expect(page).to have_content(translated(component.name)) - expect(page).to have_content(translated(official_proposal.title)) - end - expect(page).to have_content("Official proposal") end diff --git a/decidim-proposals/spec/system/proposals_versions_spec.rb b/decidim-proposals/spec/system/proposals_versions_spec.rb index 34896c771ac05..1d428ad0c8258 100644 --- a/decidim-proposals/spec/system/proposals_versions_spec.rb +++ b/decidim-proposals/spec/system/proposals_versions_spec.rb @@ -130,11 +130,6 @@ visit current_path click_on("Version 3 of 3") - within(".menu-bar") do - expect(page).to have_content(translated(component.name)) - expect(page).to have_content(translated(proposal.reload.title)) - end - within "#diff-for-state" do expect(page).to have_content("State") within ".diff > ul > .ins" do diff --git a/decidim-surveys/spec/system/survey_spec.rb b/decidim-surveys/spec/system/survey_spec.rb index 30039b8d83f7d..a4e2f14d90a61 100644 --- a/decidim-surveys/spec/system/survey_spec.rb +++ b/decidim-surveys/spec/system/survey_spec.rb @@ -39,10 +39,6 @@ it "does not allow responding the survey" do visit_component - within(".menu-bar") do - expect(page).to have_content(translated(component.name)) - end - choose "All" expect(page).to have_i18n_content(questionnaire.title) @@ -104,11 +100,6 @@ choose "All" click_on translated_attribute(questionnaire.title) - within(".menu-bar") do - expect(page).to have_content(translated(component.name)) - expect(page).to have_content(translated(questionnaire.title)) - end - # does not show the charts if not published expect(page.html).not_to include('new Chartkick["ColumnChart"]("chart-1"') expect(page.html).not_to include('new Chartkick["ColumnChart"]("chart-2"') diff --git a/decidim-surveys/spec/system/surveys_breadcrumbs_spec.rb b/decidim-surveys/spec/system/surveys_breadcrumbs_spec.rb new file mode 100644 index 0000000000000..9c71f04bb39a1 --- /dev/null +++ b/decidim-surveys/spec/system/surveys_breadcrumbs_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Surveys Breadcrumb" do + include_context "with a component" + + let(:manifest_name) { "surveys" } + let(:title) do + { + "en" => "Survey's title", + "ca" => "Títol de l'enquesta'", + "es" => "Título de la encuesta" + } + end + let!(:questionnaire) { create(:questionnaire, title:) } + let!(:survey) { create(:survey, :published, component:, questionnaire:) } + + context "when the survey does not allow responses" do + it "shows the correct information in breadcrumb (space, component)" do + visit_component + + within(".menu-bar") do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + end + end + end + + context "when the survey has questions' responses published" do + let(:question_single_option) { create(:questionnaire_question, :with_response_options, position: 0, question_type: "single_option", questionnaire:) } + + before do + 10.times do + response = create(:response, question: question_single_option, questionnaire:) + response_option = question_single_option.response_options.sample + create(:response_choice, response_option:, response:, matrix_row: nil) + end + end + + it "shows the correct information in breadcrumb (space, component, questionnaire)" do + visit_component + choose "All" + click_on translated_attribute(questionnaire.title) + + within(".menu-bar") do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + expect(page).to have_content(translated(questionnaire.title)) + end + end + end +end From 1a80b1612b8818673185c24385d491103c8834df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Sun, 14 Dec 2025 22:26:53 +0100 Subject: [PATCH 017/116] Rename private participants to members (#15749) --- .github/actions/spelling/expect.txt | 2 +- config/i18n-tasks.yml | 2 +- ...create_participatory_space_private_user.rb | 98 ---------- ...estroy_participatory_space_private_user.rb | 28 --- .../participatory_space/create_member.rb | 100 ++++++++++ .../participatory_space/destroy_member.rb | 30 +++ .../participatory_space/import_member_csv.rb | 46 +++++ .../publish_all_members.rb | 52 ++++++ .../unpublish_all_members.rb | 52 ++++++ .../participatory_space/update_member.rb | 13 ++ ...icipatory_space_private_user_import_csv.rb | 44 ----- ...h_all_participatory_space_private_users.rb | 50 ----- ...h_all_participatory_space_private_users.rb | 50 ----- ...update_participatory_space_private_user.rb | 11 -- .../admin/filterable.rb | 34 ---- .../admin/concerns/has_private_users.rb | 169 ----------------- .../concerns/has_private_users_csv_import.rb | 65 ------- .../concerns/has_members.rb | 171 ++++++++++++++++++ .../concerns/has_members_csv_import.rb | 67 +++++++ .../concerns/members_filterable.rb | 36 ++++ .../member_csv_import_form.rb | 33 ++++ .../admin/participatory_space/member_form.rb | 26 +++ .../participatory_space_admin_user_form.rb | 2 +- ...tory_space_private_user_csv_import_form.rb | 31 ---- .../participatory_space_private_user_form.rb | 24 --- .../destroy_private_users_follows_job.rb | 37 ---- ...articipatory_space_private_user_csv_job.rb | 27 --- .../destroy_members_follows_job.rb | 39 ++++ .../import_member_csv_job.rb | 29 +++ .../decidim/admin/newsletter_recipients.rb | 2 +- .../_form.html.erb | 0 .../edit.html.erb | 4 +- .../index.html.erb | 50 ++--- .../new.html.erb | 4 +- .../new.html.erb | 4 +- decidim-admin/config/locales/en.yml | 142 +++++++-------- decidim-admin/lib/decidim/admin/engine.rb | 1 - .../decidim/admin/deliver_newsletter_spec.rb | 12 +- .../create_member_spec.rb} | 26 +-- .../destroy_member_spec.rb} | 24 +-- .../import_member_csv_spec.rb} | 20 +- .../publish_all_members_spec.rb} | 8 +- .../unpublish_all_members_spec.rb} | 8 +- .../update_member_spec.rb} | 12 +- .../member_csv_import_form_spec.rb | 63 +++++++ .../participatory_space/member_form_spec.rb | 34 ++++ ...space_private_user_csv_import_form_spec.rb | 61 ------- ...ticipatory_space_private_user_form_spec.rb | 32 ---- .../destroy_members_follows_job_spec.rb | 63 +++++++ .../import_member_csv_job_spec.rb | 24 +++ .../destroy_private_users_follows_job_spec.rb | 61 ------- ...ipatory_space_private_user_csv_job_spec.rb | 22 --- .../queries/newsletter_recipients_spec.rb | 2 +- .../system/admin_manages_newsletters_spec.rb | 12 +- ..._spec.rb => member_import_via_csv_spec.rb} | 12 +- ...ce_private_user_spec.rb => member_spec.rb} | 8 +- .../lib/decidim/api/test/component_context.rb | 18 +- .../assemblies/admin/members_controller.rb | 22 +++ .../admin/members_csv_imports_controller.rb | 22 +++ ...cipatory_space_private_users_controller.rb | 22 --- ...ce_private_users_csv_imports_controller.rb | 22 --- ...rs_controller.rb => members_controller.rb} | 4 +- .../decidim/assemblies/assemblies_helper.rb | 4 +- .../app/models/decidim/assembly.rb | 4 +- .../assembly_admin/assembly_admin.test.js | 2 +- .../decidim/assemblies/permissions.rb | 8 +- .../index.html.erb | 2 +- decidim-assemblies/config/locales/en.yml | 4 +- .../lib/decidim/assemblies/admin_engine.rb | 6 +- .../lib/decidim/assemblies/engine.rb | 6 +- .../lib/decidim/assemblies/menu.rb | 10 +- ...ler_spec.rb => members_controller_spec.rb} | 2 +- .../decidim/assemblies/permissions_spec.rb | 4 +- .../manage_assembly_members_examples.rb | 101 +++++++++++ .../manage_assembly_private_users_examples.rb | 101 ----------- ...ers_assemblies_private_space_users_spec.rb | 10 +- .../admin/admin_manages_assemblies_spec.rb | 6 +- ...=> admin_manages_assembly_members_spec.rb} | 4 +- ..._private_users_spec.rb => members_spec.rb} | 20 +- .../spec/system/private_assemblies_spec.rb | 10 +- .../spec/types/integration_schema_spec.rb | 4 +- .../show.erb | 0 ...ce_private_user_cell.rb => member_cell.rb} | 2 +- .../app/commands/decidim/destroy_account.rb | 6 +- .../concerns/decidim/has_members_page.rb | 25 --- .../participatory_space/has_members_page.rb | 27 +++ .../app/helpers/decidim/amendments_helper.rb | 4 +- .../app/helpers/decidim/menu_helper.rb | 4 +- .../decidim/participatory_space/member.rb | 53 ++++++ .../participatory_space_private_user.rb | 51 ------ .../participatory_space/member_presenter.rb | 40 ++++ ...ticipatory_space_private_user_presenter.rb | 38 ---- .../participatory_space/member_presenter.rb | 48 +++++ ...ticipatory_space_private_user_presenter.rb | 46 ----- .../app/queries/decidim/last_activity.rb | 2 +- .../members/_member.html.erb | 1 + ..._participatory_space_private_user.html.erb | 1 - ...e_user.html.erb => invite_member.html.erb} | 4 +- ...e_user.text.erb => invite_member.text.erb} | 4 +- decidim-core/config/locales/en.yml | 28 +-- ...1213075429_rename_members_in_action_log.rb | 22 +++ ...cipatory_space_private_users_to_members.rb | 9 + decidim-core/lib/decidim/core.rb | 5 +- decidim-core/lib/decidim/core/test.rb | 2 +- .../lib/decidim/core/test/factories.rb | 4 +- .../{has_private_users.rb => has_members.rb} | 22 +-- ...rticipatory_space_members_page_examples.rb | 6 +- .../decidim/download_your_data_serializers.rb | 4 +- ...> download_your_data_member_serializer.rb} | 2 +- decidim-core/lib/decidim/has_private_users.rb | 57 ------ .../participatory_space/has_members.rb | 59 ++++++ ..._remove_deleted_users_left_data_tasks.rake | 2 +- .../upgrade/fix_deleted_private_follows.rake | 2 +- .../commands/decidim/destroy_account_spec.rb | 6 +- .../data/rename_members_in_action_log_spec.rb | 128 +++++++++++++ .../spec/helpers/decidim/menu_helper_spec.rb | 6 +- .../participatory_space_list_base_spec.rb | 2 +- ...nload_your_data_member_serializer_spec.rb} | 4 +- .../notification_digest_mailer_spec.rb | 4 +- .../member_spec.rb} | 12 +- .../queries/decidim/public_activities_spec.rb | 10 +- .../download_your_data_exporter_spec.rb | 8 +- ...dim_remove_deleted_users_left_data_spec.rb | 6 +- .../fix_deleted_private_follows_spec.rb | 12 +- .../spec/system/private_space_debate_spec.rb | 10 +- ...e_private_users.csv => import_members.csv} | 0 ...csv => import_members_invalid_col_sep.csv} | 0 ...859-1.csv => import_members_iso8859-1.csv} | 0 ...e_users_nok.csv => import_members_nok.csv} | 0 ...th_bom.csv => import_members_with_bom.csv} | 0 .../questionnaires/_questionnaire.html.erb | 2 +- decidim-forms/config/locales/en.yml | 4 +- .../app/models/decidim/meetings/meeting.rb | 4 +- .../decidim/meetings/permissions.rb | 2 +- .../meetings/meetings_controller_spec.rb | 4 +- .../decidim/meetings/permissions_spec.rb | 6 +- .../spec/system/live_meeting_access_spec.rb | 8 +- .../admin/members_controller.rb | 21 +++ ...r.rb => members_csv_imports_controller.rb} | 8 +- ...cipatory_space_private_users_controller.rb | 21 --- ...rs_controller.rb => members_controller.rb} | 4 +- .../participatory_process_helper.rb | 4 +- .../models/decidim/participatory_process.rb | 2 +- .../participatory_processes/permissions.rb | 8 +- .../index.html.erb | 2 +- .../config/locales/en.yml | 8 +- .../participatory_processes/admin_engine.rb | 6 +- .../decidim/participatory_processes/engine.rb | 2 +- .../decidim/participatory_processes/menu.rb | 12 +- ...=> members_csv_imports_controller_spec.rb} | 12 +- ...ler_spec.rb => members_controller_spec.rb} | 2 +- .../decidim/participatory_process_spec.rb | 2 +- .../permissions_spec.rb | 4 +- ..._participatory_process_members_examples.rb | 101 +++++++++++ ...cipatory_process_private_users_examples.rb | 101 ----------- ...tory_processes_private_space_users_spec.rb | 10 +- ...ges_participatory_process_members_spec.rb} | 4 +- ...in_manages_participatory_processes_spec.rb | 6 +- .../private_participatory_processes_spec.rb | 8 +- .../spec/types/integration_schema_spec.rb | 4 +- .../system/amendable/amend_proposal_spec.rb | 2 +- .../system/private_space_proposal_spec.rb | 10 +- .../spec/system/private_space_survey_spec.rb | 12 +- .../csv_census/admin/census_data_form_spec.rb | 2 +- 164 files changed, 1949 insertions(+), 1744 deletions(-) delete mode 100644 decidim-admin/app/commands/decidim/admin/create_participatory_space_private_user.rb delete mode 100644 decidim-admin/app/commands/decidim/admin/destroy_participatory_space_private_user.rb create mode 100644 decidim-admin/app/commands/decidim/admin/participatory_space/create_member.rb create mode 100644 decidim-admin/app/commands/decidim/admin/participatory_space/destroy_member.rb create mode 100644 decidim-admin/app/commands/decidim/admin/participatory_space/import_member_csv.rb create mode 100644 decidim-admin/app/commands/decidim/admin/participatory_space/publish_all_members.rb create mode 100644 decidim-admin/app/commands/decidim/admin/participatory_space/unpublish_all_members.rb create mode 100644 decidim-admin/app/commands/decidim/admin/participatory_space/update_member.rb delete mode 100644 decidim-admin/app/commands/decidim/admin/process_participatory_space_private_user_import_csv.rb delete mode 100644 decidim-admin/app/commands/decidim/admin/publish_all_participatory_space_private_users.rb delete mode 100644 decidim-admin/app/commands/decidim/admin/unpublish_all_participatory_space_private_users.rb delete mode 100644 decidim-admin/app/commands/decidim/admin/update_participatory_space_private_user.rb delete mode 100644 decidim-admin/app/controllers/concerns/decidim/participatory_space_private_users/admin/filterable.rb delete mode 100644 decidim-admin/app/controllers/decidim/admin/concerns/has_private_users.rb delete mode 100644 decidim-admin/app/controllers/decidim/admin/concerns/has_private_users_csv_import.rb create mode 100644 decidim-admin/app/controllers/decidim/admin/participatory_space/concerns/has_members.rb create mode 100644 decidim-admin/app/controllers/decidim/admin/participatory_space/concerns/has_members_csv_import.rb create mode 100644 decidim-admin/app/controllers/decidim/admin/participatory_space/concerns/members_filterable.rb create mode 100644 decidim-admin/app/forms/decidim/admin/participatory_space/member_csv_import_form.rb create mode 100644 decidim-admin/app/forms/decidim/admin/participatory_space/member_form.rb delete mode 100644 decidim-admin/app/forms/decidim/admin/participatory_space_private_user_csv_import_form.rb delete mode 100644 decidim-admin/app/forms/decidim/admin/participatory_space_private_user_form.rb delete mode 100644 decidim-admin/app/jobs/decidim/admin/destroy_private_users_follows_job.rb delete mode 100644 decidim-admin/app/jobs/decidim/admin/import_participatory_space_private_user_csv_job.rb create mode 100644 decidim-admin/app/jobs/decidim/admin/participatory_space/destroy_members_follows_job.rb create mode 100644 decidim-admin/app/jobs/decidim/admin/participatory_space/import_member_csv_job.rb rename decidim-admin/app/views/decidim/admin/{participatory_space_private_users => members}/_form.html.erb (100%) rename decidim-admin/app/views/decidim/admin/{participatory_space_private_users => members}/edit.html.erb (60%) rename decidim-admin/app/views/decidim/admin/{participatory_space_private_users => members}/index.html.erb (55%) rename decidim-admin/app/views/decidim/admin/{participatory_space_private_users => members}/new.html.erb (61%) rename decidim-admin/app/views/decidim/admin/{participatory_space_private_users_csv_imports => members_csv_imports}/new.html.erb (86%) rename decidim-admin/spec/commands/decidim/admin/{create_participatory_space_private_user_spec.rb => participatory_space/create_member_spec.rb} (81%) rename decidim-admin/spec/commands/decidim/admin/{destroy_participatory_space_private_user_spec.rb => participatory_space/destroy_member_spec.rb} (68%) rename decidim-admin/spec/commands/decidim/admin/{process_participatory_space_private_user_import_csv_spec.rb => participatory_space/import_member_csv_spec.rb} (59%) rename decidim-admin/spec/commands/decidim/admin/{publish_all_participatory_space_private_users_spec.rb => participatory_space/publish_all_members_spec.rb} (72%) rename decidim-admin/spec/commands/decidim/admin/{unpublish_all_participatory_space_private_users_spec.rb => participatory_space/unpublish_all_members_spec.rb} (72%) rename decidim-admin/spec/commands/decidim/admin/{update_participatory_space_private_user_spec.rb => participatory_space/update_member_spec.rb} (71%) create mode 100644 decidim-admin/spec/forms/decidim/admin/participatory_space/member_csv_import_form_spec.rb create mode 100644 decidim-admin/spec/forms/decidim/admin/participatory_space/member_form_spec.rb delete mode 100644 decidim-admin/spec/forms/participatory_space_private_user_csv_import_form_spec.rb delete mode 100644 decidim-admin/spec/forms/participatory_space_private_user_form_spec.rb create mode 100644 decidim-admin/spec/jobs/decidim/admin/participatory_space/destroy_members_follows_job_spec.rb create mode 100644 decidim-admin/spec/jobs/decidim/admin/participatory_space/import_member_csv_job_spec.rb delete mode 100644 decidim-admin/spec/jobs/destroy_private_users_follows_job_spec.rb delete mode 100644 decidim-admin/spec/jobs/import_participatory_space_private_user_csv_job_spec.rb rename decidim-admin/spec/system/{participatory_space_private_user_import_via_csv_spec.rb => member_import_via_csv_spec.rb} (70%) rename decidim-admin/spec/system/{participatory_space_private_user_spec.rb => member_spec.rb} (59%) create mode 100644 decidim-assemblies/app/controllers/decidim/assemblies/admin/members_controller.rb create mode 100644 decidim-assemblies/app/controllers/decidim/assemblies/admin/members_csv_imports_controller.rb delete mode 100644 decidim-assemblies/app/controllers/decidim/assemblies/admin/participatory_space_private_users_controller.rb delete mode 100644 decidim-assemblies/app/controllers/decidim/assemblies/admin/participatory_space_private_users_csv_imports_controller.rb rename decidim-assemblies/app/controllers/decidim/assemblies/{participatory_space_private_users_controller.rb => members_controller.rb} (85%) rename decidim-assemblies/app/views/decidim/assemblies/{participatory_space_private_users => members}/index.html.erb (88%) rename decidim-assemblies/spec/controllers/{participatory_space_private_users_controller_spec.rb => members_controller_spec.rb} (93%) create mode 100644 decidim-assemblies/spec/shared/manage_assembly_members_examples.rb delete mode 100644 decidim-assemblies/spec/shared/manage_assembly_private_users_examples.rb rename decidim-assemblies/spec/system/admin/{admin_manages_assembly_private_users_spec.rb => admin_manages_assembly_members_spec.rb} (69%) rename decidim-assemblies/spec/system/{participatory_space_private_users_spec.rb => members_spec.rb} (77%) rename decidim-core/app/cells/decidim/{participatory_space_private_user => member}/show.erb (100%) rename decidim-core/app/cells/decidim/{participatory_space_private_user_cell.rb => member_cell.rb} (86%) delete mode 100644 decidim-core/app/controllers/concerns/decidim/has_members_page.rb create mode 100644 decidim-core/app/controllers/concerns/decidim/participatory_space/has_members_page.rb create mode 100644 decidim-core/app/models/decidim/participatory_space/member.rb delete mode 100644 decidim-core/app/models/decidim/participatory_space_private_user.rb create mode 100644 decidim-core/app/presenters/decidim/admin_log/participatory_space/member_presenter.rb delete mode 100644 decidim-core/app/presenters/decidim/admin_log/participatory_space_private_user_presenter.rb create mode 100644 decidim-core/app/presenters/decidim/participatory_space/member_presenter.rb delete mode 100644 decidim-core/app/presenters/decidim/participatory_space_private_user_presenter.rb create mode 100644 decidim-core/app/views/decidim/participatory_space/members/_member.html.erb delete mode 100644 decidim-core/app/views/decidim/participatory_space_private_users/_participatory_space_private_user.html.erb rename decidim-core/app/views/devise/mailer/{invite_private_user.html.erb => invite_member.html.erb} (81%) rename decidim-core/app/views/devise/mailer/{invite_private_user.text.erb => invite_member.text.erb} (68%) create mode 100644 decidim-core/db/data/20251213075429_rename_members_in_action_log.rb create mode 100644 decidim-core/db/migrate/20251205122428_rename_participatory_space_private_users_to_members.rb rename decidim-core/lib/decidim/core/test/shared_examples/{has_private_users.rb => has_members.rb} (64%) rename decidim-core/lib/decidim/download_your_data_serializers/{download_your_data_participatory_space_private_user_serializer.rb => download_your_data_member_serializer.rb} (88%) delete mode 100644 decidim-core/lib/decidim/has_private_users.rb create mode 100644 decidim-core/lib/decidim/participatory_space/has_members.rb create mode 100644 decidim-core/spec/db/data/rename_members_in_action_log_spec.rb rename decidim-core/spec/lib/download_your_data_serializers/{download_your_data_participatory_space_private_user_serializer_spec.rb => download_your_data_member_serializer_spec.rb} (88%) rename decidim-core/spec/models/decidim/{participatory_space_private_user_spec.rb => participatory_space/member_spec.rb} (70%) rename decidim-dev/lib/decidim/dev/assets/{import_participatory_space_private_users.csv => import_members.csv} (100%) rename decidim-dev/lib/decidim/dev/assets/{import_participatory_space_private_users_invalid_col_sep.csv => import_members_invalid_col_sep.csv} (100%) rename decidim-dev/lib/decidim/dev/assets/{import_participatory_space_private_users_iso8859-1.csv => import_members_iso8859-1.csv} (100%) rename decidim-dev/lib/decidim/dev/assets/{import_participatory_space_private_users_nok.csv => import_members_nok.csv} (100%) rename decidim-dev/lib/decidim/dev/assets/{import_participatory_space_private_users_with_bom.csv => import_members_with_bom.csv} (100%) create mode 100644 decidim-participatory_processes/app/controllers/decidim/participatory_processes/admin/members_controller.rb rename decidim-participatory_processes/app/controllers/decidim/participatory_processes/admin/{participatory_space_private_users_csv_imports_controller.rb => members_csv_imports_controller.rb} (50%) delete mode 100644 decidim-participatory_processes/app/controllers/decidim/participatory_processes/admin/participatory_space_private_users_controller.rb rename decidim-participatory_processes/app/controllers/decidim/participatory_processes/{participatory_space_private_users_controller.rb => members_controller.rb} (86%) rename decidim-participatory_processes/app/views/decidim/participatory_processes/{participatory_space_private_users => members}/index.html.erb (84%) rename decidim-participatory_processes/spec/controllers/admin/{participatory_space_private_users_csv_imports_controller_spec.rb => members_csv_imports_controller_spec.rb} (70%) rename decidim-participatory_processes/spec/controllers/{participatory_space_private_users_controller_spec.rb => members_controller_spec.rb} (94%) create mode 100644 decidim-participatory_processes/spec/shared/manage_participatory_process_members_examples.rb delete mode 100644 decidim-participatory_processes/spec/shared/manage_participatory_process_private_users_examples.rb rename decidim-participatory_processes/spec/system/admin/{admin_manages_participatory_process_private_users_spec.rb => admin_manages_participatory_process_members_spec.rb} (66%) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 66d3cf4d9aa30..c6d8bf08c0014 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -639,7 +639,7 @@ pamplona pandoc paramxx partcipatory -participatoryspaceprivateusers +participatoryspace patrimonigracia Peguera pepito diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 6dfc3ce28997c..12d2dfe08cd78 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -70,7 +70,7 @@ search: ## %w(*.jpg *.png *.gif *.svg *.ico *.eot *.otf *.ttf *.woff *.woff2 *.pdf *.css *.sass *.scss *.less *.yml *.json) exclude: - decidim-dev/lib/decidim/dev/assets/iso-8859-15.md - - decidim-dev/lib/decidim/dev/assets/import_participatory_space_private_users_iso8859-1.csv + - decidim-dev/lib/decidim/dev/assets/import_members_iso8859-1.csv - decidim-comments/app/assets/javascripts/decidim/comments/bundle.js - decidim-comments/app/assets/javascripts/decidim/comments/bundle.js.map - "*.jpeg" diff --git a/decidim-admin/app/commands/decidim/admin/create_participatory_space_private_user.rb b/decidim-admin/app/commands/decidim/admin/create_participatory_space_private_user.rb deleted file mode 100644 index f95c1da6588cc..0000000000000 --- a/decidim-admin/app/commands/decidim/admin/create_participatory_space_private_user.rb +++ /dev/null @@ -1,98 +0,0 @@ -# frozen_string_literal: true - -module Decidim - module Admin - # A command with all the business logic when creating a new participatory space - # private user in the system. - class CreateParticipatorySpacePrivateUser < Decidim::Command - delegate :current_user, to: :form - # Public: Initializes the command. - # - # form - A form object with the params. - # private_user_to - The private_user_to that will hold the - # user role - def initialize(form, private_user_to, via_csv: false) - @form = form - @private_user_to = private_user_to - @via_csv = via_csv - end - - # Executes the command. Broadcasts these events: - # - # - :ok when everything is valid. - # - :invalid if the form was not valid and we could not proceed. - # - # Returns nothing. - def call - return broadcast(:invalid) if form.invalid? - - ActiveRecord::Base.transaction do - @user ||= existing_user || new_user - create_private_user - end - - broadcast(:ok) - rescue ActiveRecord::RecordInvalid - form.errors.add(:email, :taken) - broadcast(:invalid) - end - - private - - attr_reader :form, :private_user_to, :user - - def create_private_user - action = @via_csv ? "create_via_csv" : "create" - Decidim.traceability.perform_action!( - action, - Decidim::ParticipatorySpacePrivateUser, - current_user, - resource: { - title: user.name - } - ) do - Decidim::ParticipatorySpacePrivateUser.find_or_create_by!( - user:, - privatable_to: @private_user_to, - role: form.role, - published: form.published - ) - end - end - - def existing_user - return @existing_user if defined?(@existing_user) - - @existing_user = User.find_by( - email: form.email.downcase, - organization: private_user_to.organization - ) - - InviteUserAgain.call(@existing_user, invitation_instructions) if @existing_user&.invitation_pending? - - @existing_user - end - - def new_user - @new_user ||= InviteUser.call(user_form) do - on(:ok) do |user| - return user - end - end - end - - def user_form - OpenStruct.new(name: form.name, - email: form.email.downcase, - organization: private_user_to.organization, - admin: false, - invited_by: current_user, - invitation_instructions:) - end - - def invitation_instructions - "invite_private_user" - end - end - end -end diff --git a/decidim-admin/app/commands/decidim/admin/destroy_participatory_space_private_user.rb b/decidim-admin/app/commands/decidim/admin/destroy_participatory_space_private_user.rb deleted file mode 100644 index 6b15e2a84b796..0000000000000 --- a/decidim-admin/app/commands/decidim/admin/destroy_participatory_space_private_user.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -module Decidim - module Admin - # A command with all the business logic to destroy a participatory space private user. - class DestroyParticipatorySpacePrivateUser < Decidim::Commands::DestroyResource - private - - def extra_params - { - resource: { - title: resource.user.name - } - } - end - - def run_after_hooks - return unless resource.privatable_to.respond_to?(:private_space?) - return unless resource.privatable_to.private_space? - return if resource.privatable_to.respond_to?(:is_transparent) && resource.privatable_to.is_transparent? - - # When private user is destroyed, a hook to destroy the follows of user on private non-transparent assembly - # or private participatory process and the follows of their children - DestroyPrivateUsersFollowsJob.perform_later(resource.decidim_user_id, resource.privatable_to) - end - end - end -end diff --git a/decidim-admin/app/commands/decidim/admin/participatory_space/create_member.rb b/decidim-admin/app/commands/decidim/admin/participatory_space/create_member.rb new file mode 100644 index 0000000000000..e4dd150dd8ce1 --- /dev/null +++ b/decidim-admin/app/commands/decidim/admin/participatory_space/create_member.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +module Decidim + module Admin + module ParticipatorySpace + # A command with all the business logic when creating a new participatory space + # member in the system. + class CreateMember < Decidim::Command + delegate :current_user, to: :form + # Public: Initializes the command. + # + # form - A form object with the params. + # member_to - The member_to that will hold the + # user role + def initialize(form, member_to, via_csv: false) + @form = form + @member_to = member_to + @via_csv = via_csv + end + + # Executes the command. Broadcasts these events: + # + # - :ok when everything is valid. + # - :invalid if the form was not valid and we could not proceed. + # + # Returns nothing. + def call + return broadcast(:invalid) if form.invalid? + + ActiveRecord::Base.transaction do + @user ||= existing_user || new_user + create_member + end + + broadcast(:ok) + rescue ActiveRecord::RecordInvalid + form.errors.add(:email, :taken) + broadcast(:invalid) + end + + private + + attr_reader :form, :member_to, :user + + def create_member + action = @via_csv ? "create_via_csv" : "create" + Decidim.traceability.perform_action!( + action, + Decidim::ParticipatorySpace::Member, + current_user, + resource: { + title: user.name + } + ) do + Decidim::ParticipatorySpace::Member.find_or_create_by!( + user:, + privatable_to: @member_to, + role: form.role, + published: form.published + ) + end + end + + def existing_user + return @existing_user if defined?(@existing_user) + + @existing_user = User.find_by( + email: form.email.downcase, + organization: member_to.organization + ) + + InviteUserAgain.call(@existing_user, invitation_instructions) if @existing_user&.invitation_pending? + + @existing_user + end + + def new_user + @new_user ||= InviteUser.call(user_form) do + on(:ok) do |user| + return user + end + end + end + + def user_form + OpenStruct.new(name: form.name, + email: form.email.downcase, + organization: member_to.organization, + admin: false, + invited_by: current_user, + invitation_instructions:) + end + + def invitation_instructions + "invite_member" + end + end + end + end +end diff --git a/decidim-admin/app/commands/decidim/admin/participatory_space/destroy_member.rb b/decidim-admin/app/commands/decidim/admin/participatory_space/destroy_member.rb new file mode 100644 index 0000000000000..6cce98160b482 --- /dev/null +++ b/decidim-admin/app/commands/decidim/admin/participatory_space/destroy_member.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Decidim + module Admin + module ParticipatorySpace + # A command with all the business logic to destroy a member. + class DestroyMember < Decidim::Commands::DestroyResource + private + + def extra_params + { + resource: { + title: resource.user.name + } + } + end + + def run_after_hooks + return unless resource.privatable_to.respond_to?(:private_space?) + return unless resource.privatable_to.private_space? + return if resource.privatable_to.respond_to?(:is_transparent) && resource.privatable_to.is_transparent? + + # When member is destroyed, a hook to destroy the follows of user on private non-transparent assembly + # or private participatory process and the follows of their children + DestroyMembersFollowsJob.perform_later(resource.decidim_user_id, resource.privatable_to) + end + end + end + end +end diff --git a/decidim-admin/app/commands/decidim/admin/participatory_space/import_member_csv.rb b/decidim-admin/app/commands/decidim/admin/participatory_space/import_member_csv.rb new file mode 100644 index 0000000000000..d9ce06ea98eda --- /dev/null +++ b/decidim-admin/app/commands/decidim/admin/participatory_space/import_member_csv.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require "csv" + +module Decidim + module Admin + module ParticipatorySpace + class ImportMemberCsv < Decidim::Command + include Decidim::Admin::CustomImport + + delegate :current_user, to: :form + # Public: Initializes the command. + # + # form - the form object containing the uploaded file + # members_to - The members_to that will hold the user role + def initialize(form, members_to) + @form = form + @members_to = members_to + end + + # Executes the command. Broadcasts these events: + # + # - :ok when everything is valid. + # - :invalid if the form was not valid and we could not proceed. + # + # Returns nothing. + def call + return broadcast(:invalid) unless @form.valid? + + process_csv + broadcast(:ok) + end + + private + + attr_reader :form + + def process_csv + process_import_file(@form.file) do |(email, user_name)| + ImportMemberCsvJob.perform_later(email, user_name, @members_to, current_user) if email.present? && user_name.present? + end + end + end + end + end +end diff --git a/decidim-admin/app/commands/decidim/admin/participatory_space/publish_all_members.rb b/decidim-admin/app/commands/decidim/admin/participatory_space/publish_all_members.rb new file mode 100644 index 0000000000000..ef0a037976127 --- /dev/null +++ b/decidim-admin/app/commands/decidim/admin/participatory_space/publish_all_members.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Decidim + module Admin + module ParticipatorySpace + class PublishAllMembers < Decidim::Command + # Public: Initializes the command. + # + # participatory_space - the participatory space + # current_user - the current user + def initialize(participatory_space, current_user) + @participatory_space = participatory_space + @current_user = current_user + end + + # Executes the command. Broadcasts these events: + # + # - :ok when everything is valid. + # - :invalid if the form was not valid and we could not proceed. + # + # Returns nothing. + def call + publish_all + create_action_log + broadcast(:ok) + rescue ActiveRecord::RecordInvalid + broadcast(:invalid) + end + + private + + attr_reader :participatory_space, :current_user + + def publish_all + # rubocop:disable Rails/SkipsModelValidations + # Using update_all for performance reasons + participatory_space.members.update_all(published: true) + # rubocop:enable Rails/SkipsModelValidations + end + + def create_action_log + Decidim.traceability.perform_action!( + "publish_all_members", + participatory_space, + current_user, + members_ids: participatory_space.members.pluck(:id) + ) + end + end + end + end +end diff --git a/decidim-admin/app/commands/decidim/admin/participatory_space/unpublish_all_members.rb b/decidim-admin/app/commands/decidim/admin/participatory_space/unpublish_all_members.rb new file mode 100644 index 0000000000000..128e0edfaefa8 --- /dev/null +++ b/decidim-admin/app/commands/decidim/admin/participatory_space/unpublish_all_members.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Decidim + module Admin + module ParticipatorySpace + class UnpublishAllMembers < Decidim::Command + # Public: Initializes the command. + # + # participatory_space - the participatory space + # current_user - the current user + def initialize(participatory_space, current_user) + @participatory_space = participatory_space + @current_user = current_user + end + + # Executes the command. Broadcasts these events: + # + # - :ok when everything is valid. + # - :invalid if the form was not valid and we could not proceed. + # + # Returns nothing. + def call + unpublish_all + create_action_log + broadcast(:ok) + rescue ActiveRecord::RecordInvalid + broadcast(:invalid) + end + + private + + attr_reader :participatory_space, :current_user + + def unpublish_all + # rubocop:disable Rails/SkipsModelValidations + # Using update_all for performance reasons + participatory_space.members.update_all(published: false) + # rubocop:enable Rails/SkipsModelValidations + end + + def create_action_log + Decidim.traceability.perform_action!( + "unpublish_all_members", + participatory_space, + current_user, + members_ids: participatory_space.members.pluck(:id) + ) + end + end + end + end +end diff --git a/decidim-admin/app/commands/decidim/admin/participatory_space/update_member.rb b/decidim-admin/app/commands/decidim/admin/participatory_space/update_member.rb new file mode 100644 index 0000000000000..7f235a06c756e --- /dev/null +++ b/decidim-admin/app/commands/decidim/admin/participatory_space/update_member.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Decidim + module Admin + module ParticipatorySpace + # A command with all the business logic when updating a participatory space + # member. + class UpdateMember < Decidim::Commands::UpdateResource + fetch_form_attributes :role, :published + end + end + end +end diff --git a/decidim-admin/app/commands/decidim/admin/process_participatory_space_private_user_import_csv.rb b/decidim-admin/app/commands/decidim/admin/process_participatory_space_private_user_import_csv.rb deleted file mode 100644 index 3f49c5f14924d..0000000000000 --- a/decidim-admin/app/commands/decidim/admin/process_participatory_space_private_user_import_csv.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -require "csv" - -module Decidim - module Admin - class ProcessParticipatorySpacePrivateUserImportCsv < Decidim::Command - include Decidim::Admin::CustomImport - - delegate :current_user, to: :form - # Public: Initializes the command. - # - # form - the form object containing the uploaded file - # private_users_to - The private_users_to that will hold the user role - def initialize(form, private_users_to) - @form = form - @private_users_to = private_users_to - end - - # Executes the command. Broadcasts these events: - # - # - :ok when everything is valid. - # - :invalid if the form was not valid and we could not proceed. - # - # Returns nothing. - def call - return broadcast(:invalid) unless @form.valid? - - process_csv - broadcast(:ok) - end - - private - - attr_reader :form - - def process_csv - process_import_file(@form.file) do |(email, user_name)| - ImportParticipatorySpacePrivateUserCsvJob.perform_later(email, user_name, @private_users_to, current_user) if email.present? && user_name.present? - end - end - end - end -end diff --git a/decidim-admin/app/commands/decidim/admin/publish_all_participatory_space_private_users.rb b/decidim-admin/app/commands/decidim/admin/publish_all_participatory_space_private_users.rb deleted file mode 100644 index 38abcc5c7a0f6..0000000000000 --- a/decidim-admin/app/commands/decidim/admin/publish_all_participatory_space_private_users.rb +++ /dev/null @@ -1,50 +0,0 @@ -# frozen_string_literal: true - -module Decidim - module Admin - class PublishAllParticipatorySpacePrivateUsers < Decidim::Command - # Public: Initializes the command. - # - # participatory_space - the participatory space - # current_user - the current user - def initialize(participatory_space, current_user) - @participatory_space = participatory_space - @current_user = current_user - end - - # Executes the command. Broadcasts these events: - # - # - :ok when everything is valid. - # - :invalid if the form was not valid and we could not proceed. - # - # Returns nothing. - def call - publish_all - create_action_log - broadcast(:ok) - rescue ActiveRecord::RecordInvalid - broadcast(:invalid) - end - - private - - attr_reader :participatory_space, :current_user - - def publish_all - # rubocop:disable Rails/SkipsModelValidations - # Using update_all for performance reasons - participatory_space.participatory_space_private_users.update_all(published: true) - # rubocop:enable Rails/SkipsModelValidations - end - - def create_action_log - Decidim.traceability.perform_action!( - "publish_all_members", - participatory_space, - current_user, - private_users_ids: participatory_space.participatory_space_private_users.pluck(:id) - ) - end - end - end -end diff --git a/decidim-admin/app/commands/decidim/admin/unpublish_all_participatory_space_private_users.rb b/decidim-admin/app/commands/decidim/admin/unpublish_all_participatory_space_private_users.rb deleted file mode 100644 index f32c2274e1fc4..0000000000000 --- a/decidim-admin/app/commands/decidim/admin/unpublish_all_participatory_space_private_users.rb +++ /dev/null @@ -1,50 +0,0 @@ -# frozen_string_literal: true - -module Decidim - module Admin - class UnpublishAllParticipatorySpacePrivateUsers < Decidim::Command - # Public: Initializes the command. - # - # participatory_space - the participatory space - # current_user - the current user - def initialize(participatory_space, current_user) - @participatory_space = participatory_space - @current_user = current_user - end - - # Executes the command. Broadcasts these events: - # - # - :ok when everything is valid. - # - :invalid if the form was not valid and we could not proceed. - # - # Returns nothing. - def call - unpublish_all - create_action_log - broadcast(:ok) - rescue ActiveRecord::RecordInvalid - broadcast(:invalid) - end - - private - - attr_reader :participatory_space, :current_user - - def unpublish_all - # rubocop:disable Rails/SkipsModelValidations - # Using update_all for performance reasons - participatory_space.participatory_space_private_users.update_all(published: false) - # rubocop:enable Rails/SkipsModelValidations - end - - def create_action_log - Decidim.traceability.perform_action!( - "unpublish_all_members", - participatory_space, - current_user, - private_users_ids: participatory_space.participatory_space_private_users.pluck(:id) - ) - end - end - end -end diff --git a/decidim-admin/app/commands/decidim/admin/update_participatory_space_private_user.rb b/decidim-admin/app/commands/decidim/admin/update_participatory_space_private_user.rb deleted file mode 100644 index 96f0c4923fb1d..0000000000000 --- a/decidim-admin/app/commands/decidim/admin/update_participatory_space_private_user.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -module Decidim - module Admin - # A command with all the business logic when updating a participatory space - # private user. - class UpdateParticipatorySpacePrivateUser < Decidim::Commands::UpdateResource - fetch_form_attributes :role, :published - end - end -end diff --git a/decidim-admin/app/controllers/concerns/decidim/participatory_space_private_users/admin/filterable.rb b/decidim-admin/app/controllers/concerns/decidim/participatory_space_private_users/admin/filterable.rb deleted file mode 100644 index 1fc1b13edac48..0000000000000 --- a/decidim-admin/app/controllers/concerns/decidim/participatory_space_private_users/admin/filterable.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -require "active_support/concern" - -module Decidim - module ParticipatorySpacePrivateUsers - module Admin - module Filterable - extend ActiveSupport::Concern - - included do - include Decidim::Admin::Filterable - - private - - def base_query - collection - end - - def filters - [ - :user_invitation_sent_at_not_null, - :user_invitation_accepted_at_not_null - ] - end - - def search_field_predicate - :user_name_or_user_email_cont - end - end - end - end - end -end diff --git a/decidim-admin/app/controllers/decidim/admin/concerns/has_private_users.rb b/decidim-admin/app/controllers/decidim/admin/concerns/has_private_users.rb deleted file mode 100644 index d44c4998fe8c3..0000000000000 --- a/decidim-admin/app/controllers/decidim/admin/concerns/has_private_users.rb +++ /dev/null @@ -1,169 +0,0 @@ -# frozen_string_literal: true - -module Decidim - module Admin - module Concerns - # PrivateUsers can be related to any ParticipatorySpace, in order to - # manage the private users for a given type, you should create a new - # controller and include this concern. - # - # The only requirement is to define a `privatable_to` method that - # returns an instance of the model to relate the private_user to. - module HasPrivateUsers - extend ActiveSupport::Concern - - included do - include Decidim::ParticipatorySpacePrivateUsers::Admin::Filterable - helper PaginateHelper - helper_method :privatable_to, :participatory_space_private_users - - # rubocop:disable Rails/LexicallyScopedActionFilter - before_action :set_private_user, only: [:edit, :update, :destroy, :resend_invitation] - # rubocop:enable Rails/LexicallyScopedActionFilter - - def index - enforce_permission_to :read, :space_private_user - - render template: "decidim/admin/participatory_space_private_users/index" - end - - def new - enforce_permission_to :create, :space_private_user - @form = form(ParticipatorySpacePrivateUserForm).from_params({}, privatable_to:) - render template: "decidim/admin/participatory_space_private_users/new" - end - - def edit - enforce_permission_to :update, :space_private_user, private_user: @private_user - @form = form(ParticipatorySpacePrivateUserForm).from_model(@private_user) - render template: "decidim/admin/participatory_space_private_users/edit" - end - - def update - enforce_permission_to :update, :space_private_user, private_user: @private_user - @form = form(ParticipatorySpacePrivateUserForm).from_params(params, privatable_to:) - - UpdateParticipatorySpacePrivateUser.call(@form, @private_user) do - on(:ok) do - flash[:notice] = I18n.t("participatory_space_private_users.update.success", scope: "decidim.admin") - redirect_to action: :index - end - - on(:invalid) do - flash.now[:alert] = I18n.t("participatory_space_private_users.update.error", scope: "decidim.admin") - render template: "decidim/admin/participatory_space_private_users/edit", status: :unprocessable_entity - end - end - end - - def create - enforce_permission_to :create, :space_private_user - @form = form(ParticipatorySpacePrivateUserForm).from_params(params, privatable_to:) - - CreateParticipatorySpacePrivateUser.call(@form, current_participatory_space) do - on(:ok) do - flash[:notice] = I18n.t("participatory_space_private_users.create.success", scope: "decidim.admin") - redirect_to action: :index - end - - on(:invalid) do - flash.now[:alert] = I18n.t("participatory_space_private_users.create.error", scope: "decidim.admin") - render template: "decidim/admin/participatory_space_private_users/new", status: :unprocessable_entity - end - end - end - - def destroy - enforce_permission_to :destroy, :space_private_user, private_user: @private_user - - DestroyParticipatorySpacePrivateUser.call(@private_user, current_user) do - on(:ok) do - flash[:notice] = I18n.t("participatory_space_private_users.destroy.success", scope: "decidim.admin") - redirect_to after_destroy_path - end - - on(:invalid) do - flash.now[:alert] = I18n.t("participatory_space_private_users.destroy.error", scope: "decidim.admin") - render template: "decidim/admin/participatory_space_private_users/index", status: :unprocessable_entity - end - end - end - - def resend_invitation - enforce_permission_to :invite, :space_private_user, private_user: @private_user - InviteUserAgain.call(@private_user.user, "invite_private_user") do - on(:ok) do - flash[:notice] = I18n.t("users.resend_invitation.success", scope: "decidim.admin") - end - - on(:invalid) do - flash[:alert] = I18n.t("users.resend_invitation.error", scope: "decidim.admin") - end - end - - redirect_to after_destroy_path - end - - def publish_all - PublishAllParticipatorySpacePrivateUsers.call(current_participatory_space, current_user) do - on(:ok) do - flash[:notice] = I18n.t("participatory_space_private_users.publish_all.success", scope: "decidim.admin") - redirect_to action: :index - end - - on(:invalid) do - flash[:alert] = I18n.t("participatory_space_private_users.publish_all.error", scope: "decidim.admin") - redirect_to action: :index - end - end - end - - def unpublish_all - UnpublishAllParticipatorySpacePrivateUsers.call(current_participatory_space, current_user) do - on(:ok) do - flash[:notice] = I18n.t("participatory_space_private_users.unpublish_all.success", scope: "decidim.admin") - redirect_to action: :index - end - - on(:invalid) do - flash[:alert] = I18n.t("participatory_space_private_users.unpublish_all.error", scope: "decidim.admin") - redirect_to action: :index - end - end - end - - # Public: Returns a String or Object that will be passed to `redirect_to` after - # destroying a private user. By default it redirects to the privatable_to. - # - # It can be redefined at controller level if you need to redirect elsewhere. - def after_destroy_path - privatable_to - end - - # Public: The only method to be implemented at the controller. You need to - # return the object where the attachment will be attached to. - def privatable_to - raise NotImplementedError - end - - def collection - # there is an unidentified corner case where Decidim::User - # may have been destroyed, but the related ParticipatorySpacePrivateUser - # remains in the database. That is why filtering by not null users - @collection ||= privatable_to - .participatory_space_private_users - .includes(:user).where.not("decidim_users.id" => nil) - end - - def participatory_space_private_users - filtered_collection - end - - def set_private_user - @private_user = collection.find(params[:id]) - end - end - end - end - end -end diff --git a/decidim-admin/app/controllers/decidim/admin/concerns/has_private_users_csv_import.rb b/decidim-admin/app/controllers/decidim/admin/concerns/has_private_users_csv_import.rb deleted file mode 100644 index e9940d1b0c8bc..0000000000000 --- a/decidim-admin/app/controllers/decidim/admin/concerns/has_private_users_csv_import.rb +++ /dev/null @@ -1,65 +0,0 @@ -# frozen_string_literal: true - -module Decidim - module Admin - module Concerns - # PrivateUsers can be related to any ParticipatorySpace, in order to - # import private users from csv for a given type, you should create a new - # controller and include this concern. - # - # The only requirement is to define a `privatable_to` method that - # returns an instance of the model to relate the private_user to. - module HasPrivateUsersCsvImport - extend ActiveSupport::Concern - - included do - helper_method :privatable_to - - def new - enforce_permission_to :csv_import, :space_private_user - @form = form(ParticipatorySpacePrivateUserCsvImportForm).from_params({}, privatable_to:) - @count = Decidim::ParticipatorySpacePrivateUser.by_participatory_space(privatable_to).count - render template: "decidim/admin/participatory_space_private_users_csv_imports/new" - end - - def create - enforce_permission_to :csv_import, :space_private_user - @form = form(ParticipatorySpacePrivateUserCsvImportForm).from_params(params, privatable_to:) - - ProcessParticipatorySpacePrivateUserImportCsv.call(@form, current_participatory_space) do - on(:ok) do - flash[:notice] = I18n.t("participatory_space_private_users_csv_imports.create.success", scope: "decidim.admin") - redirect_to after_import_path - end - - on(:invalid) do - flash[:alert] = I18n.t("participatory_space_private_users_csv_imports.create.invalid", scope: "decidim.admin") - render template: "decidim/admin/participatory_space_private_users_csv_imports/new", status: :unprocessable_entity - end - end - end - - def destroy_all - enforce_permission_to :csv_import, :space_private_user - Decidim::ParticipatorySpacePrivateUser.by_participatory_space(privatable_to).delete_all - redirect_to new_participatory_space_private_users_csv_imports_path - end - - # Public: Returns a String or Object that will be passed to `redirect_to` after - # importing private users. By default it redirects to the privatable_to. - # - # It can be redefined at controller level if you need to redirect elsewhere. - def after_import_path - privatable_to - end - - # Public: The only method to be implemented at the controller. You need to - # return the object where the attachment will be attached to. - def privatable_to - raise NotImplementedError - end - end - end - end - end -end diff --git a/decidim-admin/app/controllers/decidim/admin/participatory_space/concerns/has_members.rb b/decidim-admin/app/controllers/decidim/admin/participatory_space/concerns/has_members.rb new file mode 100644 index 0000000000000..782b1a9c19178 --- /dev/null +++ b/decidim-admin/app/controllers/decidim/admin/participatory_space/concerns/has_members.rb @@ -0,0 +1,171 @@ +# frozen_string_literal: true + +module Decidim + module Admin + module ParticipatorySpace + module Concerns + # Members can be related to any ParticipatorySpace, in order to + # manage the members for a given type, you should create a new + # controller and include this concern. + # + # The only requirement is to define a `privatable_to` method that + # returns an instance of the model to relate the member to. + module HasMembers + extend ActiveSupport::Concern + + included do + include Decidim::Admin::ParticipatorySpace::Concerns::MembersFilterable + helper PaginateHelper + helper_method :privatable_to, :members + + # rubocop:disable Rails/LexicallyScopedActionFilter + before_action :set_member, only: [:edit, :update, :destroy, :resend_invitation] + # rubocop:enable Rails/LexicallyScopedActionFilter + + def index + enforce_permission_to :read, :space_member + + render template: "decidim/admin/members/index" + end + + def new + enforce_permission_to :create, :space_member + @form = form(MemberForm).from_params({}, privatable_to:) + render template: "decidim/admin/members/new" + end + + def edit + enforce_permission_to :update, :space_member, member: @member + @form = form(MemberForm).from_model(@member) + render template: "decidim/admin/members/edit" + end + + def update + enforce_permission_to :update, :space_member, member: @member + @form = form(MemberForm).from_params(params, privatable_to:) + + UpdateMember.call(@form, @member) do + on(:ok) do + flash[:notice] = I18n.t("members.update.success", scope: "decidim.admin") + redirect_to action: :index + end + + on(:invalid) do + flash.now[:alert] = I18n.t("members.update.error", scope: "decidim.admin") + render template: "decidim/admin/members/edit", status: :unprocessable_entity + end + end + end + + def create + enforce_permission_to :create, :space_member + @form = form(MemberForm).from_params(params, privatable_to:) + + CreateMember.call(@form, current_participatory_space) do + on(:ok) do + flash[:notice] = I18n.t("members.create.success", scope: "decidim.admin") + redirect_to action: :index + end + + on(:invalid) do + flash.now[:alert] = I18n.t("members.create.error", scope: "decidim.admin") + render template: "decidim/admin/members/new", status: :unprocessable_entity + end + end + end + + def destroy + enforce_permission_to :destroy, :space_member, member: @member + + DestroyMember.call(@member, current_user) do + on(:ok) do + flash[:notice] = I18n.t("members.destroy.success", scope: "decidim.admin") + redirect_to after_destroy_path + end + + on(:invalid) do + flash.now[:alert] = I18n.t("members.destroy.error", scope: "decidim.admin") + render template: "decidim/admin/members/index", status: :unprocessable_entity + end + end + end + + def resend_invitation + enforce_permission_to :invite, :space_member, member: @member + InviteUserAgain.call(@member.user, "invite_member") do + on(:ok) do + flash[:notice] = I18n.t("users.resend_invitation.success", scope: "decidim.admin") + end + + on(:invalid) do + flash[:alert] = I18n.t("users.resend_invitation.error", scope: "decidim.admin") + end + end + + redirect_to after_destroy_path + end + + def publish_all + PublishAllMembers.call(current_participatory_space, current_user) do + on(:ok) do + flash[:notice] = I18n.t("members.publish_all.success", scope: "decidim.admin") + redirect_to action: :index + end + + on(:invalid) do + flash[:alert] = I18n.t("members.publish_all.error", scope: "decidim.admin") + redirect_to action: :index + end + end + end + + def unpublish_all + UnpublishAllMembers.call(current_participatory_space, current_user) do + on(:ok) do + flash[:notice] = I18n.t("members.unpublish_all.success", scope: "decidim.admin") + redirect_to action: :index + end + + on(:invalid) do + flash[:alert] = I18n.t("members.unpublish_all.error", scope: "decidim.admin") + redirect_to action: :index + end + end + end + + # Public: Returns a String or Object that will be passed to `redirect_to` after + # destroying a member. By default it redirects to the privatable_to. + # + # It can be redefined at controller level if you need to redirect elsewhere. + def after_destroy_path + privatable_to + end + + # Public: The only method to be implemented at the controller. You need to + # return the object where the attachment will be attached to. + def privatable_to + raise NotImplementedError + end + + def collection + # there is an unidentified corner case where Decidim::User + # may have been destroyed, but the related Member + # remains in the database. That is why filtering by not null users + @collection ||= privatable_to + .members + .includes(:user).where.not("decidim_users.id" => nil) + end + + def members + filtered_collection + end + + def set_member + @member = collection.find(params[:id]) + end + end + end + end + end + end +end diff --git a/decidim-admin/app/controllers/decidim/admin/participatory_space/concerns/has_members_csv_import.rb b/decidim-admin/app/controllers/decidim/admin/participatory_space/concerns/has_members_csv_import.rb new file mode 100644 index 0000000000000..7b05c389b9083 --- /dev/null +++ b/decidim-admin/app/controllers/decidim/admin/participatory_space/concerns/has_members_csv_import.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module Decidim + module Admin + module ParticipatorySpace + module Concerns + # Members can be related to any ParticipatorySpace, in order to + # import members from csv for a given type, you should create a new + # controller and include this concern. + # + # The only requirement is to define a `privatable_to` method that + # returns an instance of the model to relate the member to. + module HasMembersCsvImport + extend ActiveSupport::Concern + + included do + helper_method :privatable_to + + def new + enforce_permission_to :csv_import, :space_member + @form = form(MemberCsvImportForm).from_params({}, privatable_to:) + @count = Decidim::ParticipatorySpace::Member.by_participatory_space(privatable_to).count + render template: "decidim/admin/members_csv_imports/new" + end + + def create + enforce_permission_to :csv_import, :space_member + @form = form(MemberCsvImportForm).from_params(params, privatable_to:) + + ImportMemberCsv.call(@form, current_participatory_space) do + on(:ok) do + flash[:notice] = I18n.t("members_csv_imports.create.success", scope: "decidim.admin") + redirect_to after_import_path + end + + on(:invalid) do + flash[:alert] = I18n.t("members_csv_imports.create.invalid", scope: "decidim.admin") + render template: "decidim/admin/members_csv_imports/new", status: :unprocessable_entity + end + end + end + + def destroy_all + enforce_permission_to :csv_import, :space_member + Decidim::ParticipatorySpace::Member.by_participatory_space(privatable_to).delete_all + redirect_to new_members_csv_imports_path + end + + # Public: Returns a String or Object that will be passed to `redirect_to` after + # importing members. By default it redirects to the privatable_to. + # + # It can be redefined at controller level if you need to redirect elsewhere. + def after_import_path + privatable_to + end + + # Public: The only method to be implemented at the controller. You need to + # return the object where the attachment will be attached to. + def privatable_to + raise NotImplementedError + end + end + end + end + end + end +end diff --git a/decidim-admin/app/controllers/decidim/admin/participatory_space/concerns/members_filterable.rb b/decidim-admin/app/controllers/decidim/admin/participatory_space/concerns/members_filterable.rb new file mode 100644 index 0000000000000..a1af45aa7e282 --- /dev/null +++ b/decidim-admin/app/controllers/decidim/admin/participatory_space/concerns/members_filterable.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require "active_support/concern" + +module Decidim + module Admin + module ParticipatorySpace + module Concerns + module MembersFilterable + extend ActiveSupport::Concern + + included do + include Decidim::Admin::Filterable + + private + + def base_query + collection + end + + def filters + [ + :user_invitation_sent_at_not_null, + :user_invitation_accepted_at_not_null + ] + end + + def search_field_predicate + :user_name_or_user_email_cont + end + end + end + end + end + end +end diff --git a/decidim-admin/app/forms/decidim/admin/participatory_space/member_csv_import_form.rb b/decidim-admin/app/forms/decidim/admin/participatory_space/member_csv_import_form.rb new file mode 100644 index 0000000000000..14efbcbca108c --- /dev/null +++ b/decidim-admin/app/forms/decidim/admin/participatory_space/member_csv_import_form.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require "csv" + +module Decidim + module Admin + module ParticipatorySpace + # A form object used to upload CSV to batch members. + # + class MemberCsvImportForm < Form + include Decidim::HasUploadValidations + include Decidim::Admin::CustomImport + + attribute :file, Decidim::Attributes::Blob + attribute :user_name, String + attribute :email, String + + validates :file, presence: true, file_content_type: { allow: ["text/csv"] } + validate :validate_csv + + def validate_csv + return if file.blank? + + process_import_file(file) do |(_email, user_name)| + errors.add(:user_name, :invalid) if user_name.blank? || !user_name.match?(UserBaseEntity::REGEXP_NAME) + end + rescue CSV::MalformedCSVError + errors.add(:file, :malformed) + end + end + end + end +end diff --git a/decidim-admin/app/forms/decidim/admin/participatory_space/member_form.rb b/decidim-admin/app/forms/decidim/admin/participatory_space/member_form.rb new file mode 100644 index 0000000000000..ddc8665d8e211 --- /dev/null +++ b/decidim-admin/app/forms/decidim/admin/participatory_space/member_form.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Decidim + module Admin + module ParticipatorySpace + # A form object used to create members from the + # admin dashboard. + # + class MemberForm < Form + include TranslatableAttributes + + mimic :member + + attribute :name, String + attribute :email, String + attribute :published, Boolean + + translatable_attribute :role, String + + validates :name, :email, presence: true + + validates :name, format: { with: UserBaseEntity::REGEXP_NAME } + end + end + end +end diff --git a/decidim-admin/app/forms/decidim/admin/participatory_space_admin_user_form.rb b/decidim-admin/app/forms/decidim/admin/participatory_space_admin_user_form.rb index 79118d1e44a5b..bd38aa416be28 100644 --- a/decidim-admin/app/forms/decidim/admin/participatory_space_admin_user_form.rb +++ b/decidim-admin/app/forms/decidim/admin/participatory_space_admin_user_form.rb @@ -2,7 +2,7 @@ module Decidim module Admin - class ParticipatorySpaceAdminUserForm < ParticipatorySpacePrivateUserForm + class ParticipatorySpaceAdminUserForm < Decidim::Admin::ParticipatorySpace::MemberForm attribute :role, String validates :role, presence: true diff --git a/decidim-admin/app/forms/decidim/admin/participatory_space_private_user_csv_import_form.rb b/decidim-admin/app/forms/decidim/admin/participatory_space_private_user_csv_import_form.rb deleted file mode 100644 index 5a09652fdb59d..0000000000000 --- a/decidim-admin/app/forms/decidim/admin/participatory_space_private_user_csv_import_form.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -require "csv" - -module Decidim - module Admin - # A form object used to upload CSV to batch participatory space private users. - # - class ParticipatorySpacePrivateUserCsvImportForm < Form - include Decidim::HasUploadValidations - include Decidim::Admin::CustomImport - - attribute :file, Decidim::Attributes::Blob - attribute :user_name, String - attribute :email, String - - validates :file, presence: true, file_content_type: { allow: ["text/csv"] } - validate :validate_csv - - def validate_csv - return if file.blank? - - process_import_file(file) do |(_email, user_name)| - errors.add(:user_name, :invalid) if user_name.blank? || !user_name.match?(UserBaseEntity::REGEXP_NAME) - end - rescue CSV::MalformedCSVError - errors.add(:file, :malformed) - end - end - end -end diff --git a/decidim-admin/app/forms/decidim/admin/participatory_space_private_user_form.rb b/decidim-admin/app/forms/decidim/admin/participatory_space_private_user_form.rb deleted file mode 100644 index d92ee013d51c4..0000000000000 --- a/decidim-admin/app/forms/decidim/admin/participatory_space_private_user_form.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -module Decidim - module Admin - # A form object used to create participatory space private users from the - # admin dashboard. - # - class ParticipatorySpacePrivateUserForm < Form - include TranslatableAttributes - - mimic :participatory_space_private_user - - attribute :name, String - attribute :email, String - attribute :published, Boolean - - translatable_attribute :role, String - - validates :name, :email, presence: true - - validates :name, format: { with: UserBaseEntity::REGEXP_NAME } - end - end -end diff --git a/decidim-admin/app/jobs/decidim/admin/destroy_private_users_follows_job.rb b/decidim-admin/app/jobs/decidim/admin/destroy_private_users_follows_job.rb deleted file mode 100644 index 04265cb72d121..0000000000000 --- a/decidim-admin/app/jobs/decidim/admin/destroy_private_users_follows_job.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -module Decidim - module Admin - class DestroyPrivateUsersFollowsJob < ApplicationJob - queue_as :default - - def perform(decidim_user_id, space) - return unless space.respond_to?(:private_space?) - - return unless space.private_space? - - return if space.respond_to?(:is_transparent) && space.is_transparent? - - user = Decidim::User.find_by(id: decidim_user_id) - - return if user.blank? - - return if space.respond_to?(:can_participate?) && space.can_participate?(user) - - follows = Decidim::Follow.where(user: user) - follows.where(followable: space).destroy_all - - destroy_children_follows(follows, space) - end - - def destroy_children_follows(follows, space) - follows.map do |follow| - object = follow.followable.presence - next unless object.respond_to?(:decidim_component_id) - - follow.destroy if space.component_ids.include?(object.decidim_component_id) - end - end - end - end -end diff --git a/decidim-admin/app/jobs/decidim/admin/import_participatory_space_private_user_csv_job.rb b/decidim-admin/app/jobs/decidim/admin/import_participatory_space_private_user_csv_job.rb deleted file mode 100644 index adaeabfc294c8..0000000000000 --- a/decidim-admin/app/jobs/decidim/admin/import_participatory_space_private_user_csv_job.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -module Decidim - module Admin - # Custom ApplicationJob scoped to the admin panel. - # - class ImportParticipatorySpacePrivateUserCsvJob < ApplicationJob - queue_as :exports - - def perform(email, user_name, privatable_to, current_user) - return if email.blank? || user_name.blank? - - params = { - name: user_name, - email: email.downcase.strip - } - private_user_form = ParticipatorySpacePrivateUserForm.from_params(params, privatable_to:) - .with_context( - current_user:, - current_participatory_space: privatable_to - ) - - Decidim::Admin::CreateParticipatorySpacePrivateUser.call(private_user_form, privatable_to, via_csv: true) - end - end - end -end diff --git a/decidim-admin/app/jobs/decidim/admin/participatory_space/destroy_members_follows_job.rb b/decidim-admin/app/jobs/decidim/admin/participatory_space/destroy_members_follows_job.rb new file mode 100644 index 0000000000000..95da9158bf3a8 --- /dev/null +++ b/decidim-admin/app/jobs/decidim/admin/participatory_space/destroy_members_follows_job.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Decidim + module Admin + module ParticipatorySpace + class DestroyMembersFollowsJob < ApplicationJob + queue_as :default + + def perform(decidim_user_id, space) + return unless space.respond_to?(:private_space?) + + return unless space.private_space? + + return if space.respond_to?(:is_transparent) && space.is_transparent? + + user = Decidim::User.find_by(id: decidim_user_id) + + return if user.blank? + + return if space.respond_to?(:can_participate?) && space.can_participate?(user) + + follows = Decidim::Follow.where(user: user) + follows.where(followable: space).destroy_all + + destroy_children_follows(follows, space) + end + + def destroy_children_follows(follows, space) + follows.map do |follow| + object = follow.followable.presence + next unless object.respond_to?(:decidim_component_id) + + follow.destroy if space.component_ids.include?(object.decidim_component_id) + end + end + end + end + end +end diff --git a/decidim-admin/app/jobs/decidim/admin/participatory_space/import_member_csv_job.rb b/decidim-admin/app/jobs/decidim/admin/participatory_space/import_member_csv_job.rb new file mode 100644 index 0000000000000..d8b9cc106964f --- /dev/null +++ b/decidim-admin/app/jobs/decidim/admin/participatory_space/import_member_csv_job.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Decidim + module Admin + module ParticipatorySpace + # Custom ApplicationJob scoped to the admin panel. + # + class ImportMemberCsvJob < ApplicationJob + queue_as :exports + + def perform(email, user_name, privatable_to, current_user) + return if email.blank? || user_name.blank? + + params = { + name: user_name, + email: email.downcase.strip + } + member_form = MemberForm.from_params(params, privatable_to:) + .with_context( + current_user:, + current_participatory_space: privatable_to + ) + + CreateMember.call(member_form, privatable_to, via_csv: true) + end + end + end + end +end diff --git a/decidim-admin/app/queries/decidim/admin/newsletter_recipients.rb b/decidim-admin/app/queries/decidim/admin/newsletter_recipients.rb index 697ce9f9bf04c..e74b6c589a422 100644 --- a/decidim-admin/app/queries/decidim/admin/newsletter_recipients.rb +++ b/decidim-admin/app/queries/decidim/admin/newsletter_recipients.rb @@ -129,7 +129,7 @@ def private_member_ids return unless @form.send_to_private_members return [] if private_spaces.blank? - Decidim::ParticipatorySpacePrivateUser.private_user_ids_for_participatory_spaces(private_spaces) + Decidim::ParticipatorySpace::Member.member_ids_for_participatory_spaces(private_spaces) end end end diff --git a/decidim-admin/app/views/decidim/admin/participatory_space_private_users/_form.html.erb b/decidim-admin/app/views/decidim/admin/members/_form.html.erb similarity index 100% rename from decidim-admin/app/views/decidim/admin/participatory_space_private_users/_form.html.erb rename to decidim-admin/app/views/decidim/admin/members/_form.html.erb diff --git a/decidim-admin/app/views/decidim/admin/participatory_space_private_users/edit.html.erb b/decidim-admin/app/views/decidim/admin/members/edit.html.erb similarity index 60% rename from decidim-admin/app/views/decidim/admin/participatory_space_private_users/edit.html.erb rename to decidim-admin/app/views/decidim/admin/members/edit.html.erb index c3bfd1492cf1f..494738f61b422 100644 --- a/decidim-admin/app/views/decidim/admin/participatory_space_private_users/edit.html.erb +++ b/decidim-admin/app/views/decidim/admin/members/edit.html.erb @@ -7,8 +7,8 @@
- <%= decidim_form_for(@form, url: participatory_space_private_user_path(current_participatory_space, @private_user), html: { class: "form-defaults form edit_participatory_space_private_user" }) do |f| %> - <%= render partial: "decidim/admin/participatory_space_private_users/form", object: f %> + <%= decidim_form_for(@form, url: member_path(current_participatory_space, @member), html: { class: "form-defaults form edit_member" }) do |f| %> + <%= render partial: "decidim/admin/members/form", object: f %>
<%= f.submit t(".update"), class: "button button__sm button__secondary" %> diff --git a/decidim-admin/app/views/decidim/admin/participatory_space_private_users/index.html.erb b/decidim-admin/app/views/decidim/admin/members/index.html.erb similarity index 55% rename from decidim-admin/app/views/decidim/admin/participatory_space_private_users/index.html.erb rename to decidim-admin/app/views/decidim/admin/members/index.html.erb index ee68d751bde8b..bfb33b5d93305 100644 --- a/decidim-admin/app/views/decidim/admin/participatory_space_private_users/index.html.erb +++ b/decidim-admin/app/views/decidim/admin/members/index.html.erb @@ -1,18 +1,18 @@ <% add_decidim_page_title(t(".title")) %> -
+

<%= t(".title") %> - <% if allowed_to? :create, :space_private_user %> - <%= link_to t(".publish_all"), publish_all_participatory_space_private_users_path(current_participatory_space), class: "button button__sm button__transparent-secondary publish-all", method: :post %> - <%= link_to t(".unpublish_all"), unpublish_all_participatory_space_private_users_path(current_participatory_space), class: "button button__sm button__transparent-secondary unpublish-all", method: :post %> - <%= link_to t(".import_via_csv"), new_participatory_space_private_users_csv_imports_path, class: "button button__sm button__transparent-secondary import" %> - <%= link_to t("actions.participatory_space_private_user.new", scope: "decidim.admin"), url_for(action: :new), class: "button button__sm button__secondary new" %> + <% if allowed_to? :create, :space_member %> + <%= link_to t(".publish_all"), publish_all_members_path(current_participatory_space), class: "button button__sm button__transparent-secondary publish-all", method: :post %> + <%= link_to t(".unpublish_all"), unpublish_all_members_path(current_participatory_space), class: "button button__sm button__transparent-secondary unpublish-all", method: :post %> + <%= link_to t(".import_via_csv"), new_members_csv_imports_path, class: "button button__sm button__transparent-secondary import" %> + <%= link_to t("actions.member.new", scope: "decidim.admin"), url_for(action: :new), class: "button button__sm button__secondary new" %> <% end %>

- <%= admin_filter_selector(:participatory_space_private_users) %> - <% if participatory_space_private_users.any? %> + <%= admin_filter_selector(:members) %> + <% if members.any? %>
@@ -36,39 +36,39 @@ - <% participatory_space_private_users.each do |private_user| %> + <% members.each do |member| %>
"> - <%= private_user.user.name %> + <%= member.user.name %> "> - <%= private_user.user.email %> + <%= member.user.email %> "> - <% if private_user.published %> + <% if member.published %> <%= icon "check-line", class: "text-success" %> <% end %> "> - <% if private_user.user.invitation_sent_at %> - <%= l private_user.user.invitation_sent_at, format: :short %> + <% if member.user.invitation_sent_at %> + <%= l member.user.invitation_sent_at, format: :short %> <% end %> "> - <% if private_user.user.invitation_accepted_at %> - <%= l private_user.user.invitation_accepted_at, format: :short %> + <% if member.user.invitation_accepted_at %> + <%= l member.user.invitation_accepted_at, format: :short %> <% end %> " class="table-list__actions"> -
-
- <%= decidim_paginate participatory_space_private_users %> + <%= decidim_paginate members %> <% end %>
diff --git a/decidim-admin/app/views/decidim/admin/participatory_space_private_users/new.html.erb b/decidim-admin/app/views/decidim/admin/members/new.html.erb similarity index 61% rename from decidim-admin/app/views/decidim/admin/participatory_space_private_users/new.html.erb rename to decidim-admin/app/views/decidim/admin/members/new.html.erb index 703a2ad0890fe..aebdfd6551717 100644 --- a/decidim-admin/app/views/decidim/admin/participatory_space_private_users/new.html.erb +++ b/decidim-admin/app/views/decidim/admin/members/new.html.erb @@ -7,8 +7,8 @@
- <%= decidim_form_for(@form, url: participatory_space_private_users_path(current_participatory_space), html: { class: "form-defaults form new_participatory_space_private_user" }) do |f| %> - <%= render partial: "decidim/admin/participatory_space_private_users/form", object: f %> + <%= decidim_form_for(@form, url: members_path(current_participatory_space), html: { class: "form-defaults form new_member" }) do |f| %> + <%= render partial: "decidim/admin/members/form", object: f %>
<%= f.submit t(".create"), class: "button button__sm button__secondary" %> diff --git a/decidim-admin/app/views/decidim/admin/participatory_space_private_users_csv_imports/new.html.erb b/decidim-admin/app/views/decidim/admin/members_csv_imports/new.html.erb similarity index 86% rename from decidim-admin/app/views/decidim/admin/participatory_space_private_users_csv_imports/new.html.erb rename to decidim-admin/app/views/decidim/admin/members_csv_imports/new.html.erb index 6aa33b05e379e..959c24ebedeea 100644 --- a/decidim-admin/app/views/decidim/admin/participatory_space_private_users_csv_imports/new.html.erb +++ b/decidim-admin/app/views/decidim/admin/members_csv_imports/new.html.erb @@ -18,7 +18,7 @@ <% if @count != 0 %>

<%= t(".destroy.explanation", count: @count) %>

<%= link_to t(".destroy.button"), - destroy_all_participatory_space_private_users_csv_imports_path, + destroy_all_members_csv_imports_path, method: :delete, class: "button button__sm button__secondary alert", data: { confirm: t(".destroy.confirm") } %> @@ -35,7 +35,7 @@
- <%= decidim_form_for(@form, url: participatory_space_private_users_csv_imports_path, html: { class: "form form-defaults" }) do |form| %> + <%= decidim_form_for(@form, url: members_csv_imports_path, html: { class: "form form-defaults" }) do |form| %>

<%= t(".explanation") %>

<%= t(".example_file") %>

diff --git a/decidim-admin/config/locales/en.yml b/decidim-admin/config/locales/en.yml index c3331fed510df..b9d8f4808ce09 100644 --- a/decidim-admin/config/locales/en.yml +++ b/decidim-admin/config/locales/en.yml @@ -34,6 +34,11 @@ en: help_section: content: Content id: ID + member: + email: Email + name: Name + member_csv_import: + file: File newsletter: body: Body send_to_all_users: Send to all participants @@ -92,11 +97,6 @@ en: welcome_notification_body: Welcome notification body welcome_notification_subject: Welcome notification subject youtube_handler: YouTube handler - participatory_space_private_user: - email: Email - name: Name - participatory_space_private_user_csv_import: - file: File scope: code: Code name: Name @@ -133,6 +133,10 @@ en: file: File errors: models: + member_csv_import: + attributes: + file: + malformed: Malformed import file, please read through the instructions carefully and make sure the file is UTF-8 encoded. newsletter: attributes: base: @@ -141,10 +145,6 @@ en: attributes: official_img_footer: allowed_file_content_types: Invalid image file - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Malformed import file, please read through the instructions carefully and make sure the file is UTF-8 encoded. user_group_csv_verification: attributes: file: @@ -192,12 +192,12 @@ en: export: Export export-selection: Export selection import: Import + member: + new: New member menu_hidden: Hide from menu moderate: Manage moderations newsletter: new: New newsletter - participatory_space_private_user: - new: New participatory space private user per_page: Per page permissions: Manage permissions restore: Restore @@ -419,6 +419,17 @@ en: values: 'false': 'No' 'true': 'Yes' + members: + user_invitation_accepted_at_not_null: + label: Invitation accepted + values: + 'false': Not accepted + 'true': Accepted + user_invitation_sent_at_not_null: + label: Invitation sent + values: + 'false': Not sent + 'true': Sent moderated_users: reports_reason_eq: label: Report reason @@ -434,17 +445,6 @@ en: values: 'false': Officialized 'true': Not officialized - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Invitation accepted - values: - 'false': Not accepted - 'true': Accepted - user_invitation_sent_at_not_null: - label: Invitation sent - values: - 'false': Not sent - 'true': Sent private_space_eq: label: Private values: @@ -577,6 +577,53 @@ en: explanation: Managed participants can be promoted to standard participants. It means they will be invited to the application and you will not be able to manage them again. The invited participant will receive an email to accept your invitation. new_managed_user_promotion: New managed participant promotion promote: Promote + members: + create: + error: There was a problem adding a member for this participatory space. + success: Member access successfully created. + destroy: + error: There was a problem deleting a member for this participatory space. + success: Member access successfully destroyed. + edit: + title: Edit Member. + update: Update + index: + import_via_csv: Import via CSV + publish_all: Publish all + title: Member + unpublish_all: Unpublish all + new: + create: Create + title: New Member. + publish_all: + error: There was a problem publishing all the members for this participatory space. + success: Successfully published all members for this participatory space + unpublish_all: + error: There was a problem unpublishing all the members for this participatory space. + success: Successfully unpublished all members for this participatory space + update: + error: There was a problem updating the member for this participatory space. + success: Member successfully updated + members_csv_imports: + create: + invalid: There was a problem reading the CSV file. Please make sure you have followed the instructions. + success: CSV file uploaded successfully, we are sending an invitation email to participants. This might take a while. + new: + csv_upload: + title: Upload your CSV file + destroy: + button: Delete all members + confirm: Are you sure you want to delete all members? This action cannot be undone, you will not be able to recover them. + empty: You have no members. + explanation: You have %{count} members. + title: Delete members + example_file: 'Example file:' + explanation: Upload your CSV file. It must have two columns with email in the first column of the file and name in the last column of the file of the users that you want to add to the participatory space, without headers. Avoid using invalid chars like `<>?%&^*#@()[]=+:;"{}\|` in user name. + explanation_example: | + john.doe@example.org%{csv_col_sep}John Doe + jane.doe@example.org%{csv_col_sep}Jane Doe + title: Import members via CSV + upload: Upload menu: admin_log: Admin activity log admins: Admins @@ -635,6 +682,8 @@ en: reason: Reason started_at: Started at user: Participant + member: + name: Member newsletter: fields: created_at: Created at @@ -643,8 +692,6 @@ en: sent_to: Sent to subject: Subject name: Newsletter - participatory_space_private_user: - name: Participatory space private participant scope: fields: name: Name @@ -919,53 +966,6 @@ en: form: add: Add to allowed list title: Allowed list of external domains - participatory_space_private_users: - create: - error: There was a problem adding a private participant for this participatory space. - success: Participatory space private participant access successfully created. - destroy: - error: There was a problem deleting a private participant for this participatory space. - success: Participatory space private participant access successfully destroyed. - edit: - title: Edit Participatory Space private participant. - update: Update - index: - import_via_csv: Import via CSV - publish_all: Publish all - title: Participatory space private participant - unpublish_all: Unpublish all - new: - create: Create - title: New Participatory Space private participant. - publish_all: - error: There was a problem publishing all the private participants for this participatory space. - success: Successfully published all private participants for this participatory space - unpublish_all: - error: There was a problem unpublishing all the private participants for this participatory space. - success: Successfully unpublished all private participants for this participatory space - update: - error: There was a problem updating the private participant for this participatory space. - success: Participatory space private participant successfully updated - participatory_space_private_users_csv_imports: - create: - invalid: There was a problem reading the CSV file. Please make sure you have followed the instructions. - success: CSV file uploaded successfully, we are sending an invitation email to participants. This might take a while. - new: - csv_upload: - title: Upload your CSV file - destroy: - button: Delete all private participants - confirm: Are you sure you want to delete all private participants? This action cannot be undone, you will not be able to recover them. - empty: You have no private participants. - explanation: You have %{count} private participants. - title: Delete private participants - example_file: 'Example file:' - explanation: Upload your CSV file. It must have two columns with email in the first column of the file and name in the last column of the file of the users that you want to add to the participatory space, without headers. Avoid using invalid chars like `<>?%&^*#@()[]=+:;"{}\|` in user name. - explanation_example: | - john.doe@example.org%{csv_col_sep}John Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - title: Import private participants via CSV - upload: Upload reminders: create: error: There was a problem creating reminders. diff --git a/decidim-admin/lib/decidim/admin/engine.rb b/decidim-admin/lib/decidim/admin/engine.rb index 9cf85b753ecc0..e80ed6aee9201 100644 --- a/decidim-admin/lib/decidim/admin/engine.rb +++ b/decidim-admin/lib/decidim/admin/engine.rb @@ -44,7 +44,6 @@ class Engine < ::Rails::Engine Decidim.icons.register(name: "earth-line", icon: "earth-line", category: "system", description: "Earth line", engine: :admin) Decidim.icons.register(name: "attachment-2", icon: "attachment-2", category: "system", description: "", engine: :admin) - Decidim.icons.register(name: "spy-line", icon: "spy-line", category: "system", description: "", engine: :admin) Decidim.icons.register(name: "refresh-line", icon: "refresh-line", category: "system", description: "", engine: :admin) Decidim.icons.register(name: "zoom-in-line", icon: "zoom-in-line", category: "system", description: "", engine: :admin) Decidim.icons.register(name: "add-line", icon: "add-line", category: "system", description: "", engine: :admin) diff --git a/decidim-admin/spec/commands/decidim/admin/deliver_newsletter_spec.rb b/decidim-admin/spec/commands/decidim/admin/deliver_newsletter_spec.rb index 69ae26b33b841..653d2be104c7f 100644 --- a/decidim-admin/spec/commands/decidim/admin/deliver_newsletter_spec.rb +++ b/decidim-admin/spec/commands/decidim/admin/deliver_newsletter_spec.rb @@ -315,11 +315,11 @@ def user_localized_body(user) context "when spaces selected" do let!(:participatory_process) { create(:participatory_process, organization:, private_space: true) } let!(:component) { create(:dummy_component, organization:, participatory_space: participatory_process) } - let!(:private_users) do - create_list(:participatory_space_private_user, 30) do |private_user| - private_user.user = create(:user, :confirmed, newsletter_notifications_at: Time.current, organization:) - private_user.privatable_to = participatory_process - private_user.save! + let!(:members) do + create_list(:member, 30) do |member| + member.user = create(:user, :confirmed, newsletter_notifications_at: Time.current, organization:) + member.privatable_to = participatory_process + member.save! end end let(:participatory_space_types) do @@ -339,7 +339,7 @@ def user_localized_body(user) ] end - let!(:deliverable_users) { Decidim::User.where(id: private_users.map(&:decidim_user_id)) } + let!(:deliverable_users) { Decidim::User.where(id: members.map(&:decidim_user_id)) } let!(:undeliverable_users) do create_list(:user, rand(2..9), :confirmed, organization:, newsletter_notifications_at: Time.current) diff --git a/decidim-admin/spec/commands/decidim/admin/create_participatory_space_private_user_spec.rb b/decidim-admin/spec/commands/decidim/admin/participatory_space/create_member_spec.rb similarity index 81% rename from decidim-admin/spec/commands/decidim/admin/create_participatory_space_private_user_spec.rb rename to decidim-admin/spec/commands/decidim/admin/participatory_space/create_member_spec.rb index ef1602ab84e81..e0871b6c4c8e3 100644 --- a/decidim-admin/spec/commands/decidim/admin/create_participatory_space_private_user_spec.rb +++ b/decidim-admin/spec/commands/decidim/admin/participatory_space/create_member_spec.rb @@ -2,8 +2,8 @@ require "spec_helper" -module Decidim::Admin - describe CreateParticipatorySpacePrivateUser do +module Decidim::Admin::ParticipatorySpace + describe CreateMember do subject { described_class.new(form, privatable_to, via_csv:) } let(:via_csv) { false } @@ -16,7 +16,7 @@ module Decidim::Admin let(:form) do double( invalid?: invalid, - delete_current_private_participants?: delete, + delete_current_members?: delete, email:, current_user:, name:, @@ -37,12 +37,12 @@ module Decidim::Admin end context "when everything is ok" do - it "creates the private user" do + it "creates the member" do subject.call - participatory_space_private_users = Decidim::ParticipatorySpacePrivateUser.where(user:) + members = Decidim::ParticipatorySpace::Member.where(user:) - expect(participatory_space_private_users.count).to eq 1 + expect(members.count).to eq 1 end it "creates a new user with no application admin privileges" do @@ -55,7 +55,7 @@ module Decidim::Admin .to receive(:perform_action!) .with( "create", - Decidim::ParticipatorySpacePrivateUser, + Decidim::ParticipatorySpace::Member, current_user, resource: { title: user.name } ) @@ -74,7 +74,7 @@ module Decidim::Admin .to receive(:perform_action!) .with( "create_via_csv", - Decidim::ParticipatorySpacePrivateUser, + Decidim::ParticipatorySpace::Member, current_user, resource: { title: user.name } ) @@ -104,7 +104,7 @@ module Decidim::Admin end end - context "when a private user exist" do + context "when a member exist" do before do subject.call end @@ -112,9 +112,9 @@ module Decidim::Admin it "does not get created twice" do expect { subject.call }.to broadcast(:ok) - participatory_space_private_users = Decidim::ParticipatorySpacePrivateUser.where(user:) + members = Decidim::ParticipatorySpace::Member.where(user:) - expect(participatory_space_private_users.count).to eq 1 + expect(members.count).to eq 1 end end @@ -125,10 +125,10 @@ module Decidim::Admin it "still finds the user" do expect { subject.call }.to broadcast(:ok) - participatory_space_private_users = Decidim::ParticipatorySpacePrivateUser.where(user: admin) + members = Decidim::ParticipatorySpace::Member.where(user: admin) participatory_space_admin = Decidim::User.where(email: "admin@example.org") - expect(participatory_space_private_users.count).to eq 1 + expect(members.count).to eq 1 expect(participatory_space_admin.first.admin?).to be true end end diff --git a/decidim-admin/spec/commands/decidim/admin/destroy_participatory_space_private_user_spec.rb b/decidim-admin/spec/commands/decidim/admin/participatory_space/destroy_member_spec.rb similarity index 68% rename from decidim-admin/spec/commands/decidim/admin/destroy_participatory_space_private_user_spec.rb rename to decidim-admin/spec/commands/decidim/admin/participatory_space/destroy_member_spec.rb index 43ba90f553ff8..354190ba526ca 100644 --- a/decidim-admin/spec/commands/decidim/admin/destroy_participatory_space_private_user_spec.rb +++ b/decidim-admin/spec/commands/decidim/admin/participatory_space/destroy_member_spec.rb @@ -2,17 +2,17 @@ require "spec_helper" -module Decidim::Admin - describe DestroyParticipatorySpacePrivateUser do - subject { described_class.new(participatory_space_private_user, user) } +module Decidim::Admin::ParticipatorySpace + describe DestroyMember do + subject { described_class.new(member, user) } let(:organization) { create(:organization) } let(:user) { create(:user, :admin, :confirmed, organization:) } - let(:participatory_space_private_user) { create(:participatory_space_private_user, user:) } + let(:member) { create(:member, user:) } - it "destroys the participatory space private user" do + it "destroys the member" do subject.call - expect { participatory_space_private_user.reload }.to raise_error(ActiveRecord::RecordNotFound) + expect { member.reload }.to raise_error(ActiveRecord::RecordNotFound) end it "broadcasts ok" do @@ -26,7 +26,7 @@ module Decidim::Admin .to receive(:perform_action!) .with( :delete, - participatory_space_private_user, + member, user, resource: { title: user.name } ) @@ -40,14 +40,14 @@ module Decidim::Admin context "when assembly is private and user follows assembly" do let(:normal_user) { create(:user, organization:) } let(:assembly) { create(:assembly, :private, :published, organization: user.organization) } - let!(:participatory_space_private_user) { create(:participatory_space_private_user, user: normal_user, privatable_to: assembly) } + let!(:member) { create(:member, user: normal_user, privatable_to: assembly) } let!(:follow) { create(:follow, followable: assembly, user: normal_user) } context "and assembly is transparent" do it "does not enqueue a job" do assembly.update(is_transparent: true) expect(Decidim::Follow.where(user: normal_user).count).to eq(1) - expect { subject.call }.not_to have_enqueued_job(DestroyPrivateUsersFollowsJob) + expect { subject.call }.not_to have_enqueued_job(DestroyMembersFollowsJob) end end @@ -55,7 +55,7 @@ module Decidim::Admin it "enqueues a job" do assembly.update(is_transparent: false) expect(Decidim::Follow.where(user: normal_user).count).to eq(1) - expect { subject.call }.to have_enqueued_job(DestroyPrivateUsersFollowsJob) + expect { subject.call }.to have_enqueued_job(DestroyMembersFollowsJob) end end end @@ -63,14 +63,14 @@ module Decidim::Admin context "when participatory process is private" do let(:normal_user) { create(:user, organization:) } let(:participatory_process) { create(:participatory_process, :private, :published, organization: user.organization) } - let!(:participatory_space_private_user) { create(:participatory_space_private_user, user: normal_user, privatable_to: participatory_process) } + let!(:member) { create(:member, user: normal_user, privatable_to: participatory_process) } context "and user follows process" do let!(:follow) { create(:follow, followable: participatory_process, user: normal_user) } it "enqueues a job" do expect(Decidim::Follow.where(user: normal_user).count).to eq(1) - expect { subject.call }.to have_enqueued_job(DestroyPrivateUsersFollowsJob) + expect { subject.call }.to have_enqueued_job(DestroyMembersFollowsJob) end end end diff --git a/decidim-admin/spec/commands/decidim/admin/process_participatory_space_private_user_import_csv_spec.rb b/decidim-admin/spec/commands/decidim/admin/participatory_space/import_member_csv_spec.rb similarity index 59% rename from decidim-admin/spec/commands/decidim/admin/process_participatory_space_private_user_import_csv_spec.rb rename to decidim-admin/spec/commands/decidim/admin/participatory_space/import_member_csv_spec.rb index ff733c42aff8f..ab159b7bf823a 100644 --- a/decidim-admin/spec/commands/decidim/admin/process_participatory_space_private_user_import_csv_spec.rb +++ b/decidim-admin/spec/commands/decidim/admin/participatory_space/import_member_csv_spec.rb @@ -2,20 +2,20 @@ require "spec_helper" -module Decidim::Admin - describe ProcessParticipatorySpacePrivateUserImportCsv do - subject { described_class.new(form, private_users_to) } +module Decidim::Admin::ParticipatorySpace + describe ImportMemberCsv do + subject { described_class.new(form, members_to) } let(:current_user) { create(:user, :admin, organization:) } let(:organization) { create(:organization) } - let(:private_users_to) { create(:participatory_process, organization:) } - let(:file) { upload_test_file(Decidim::Dev.test_file("import_participatory_space_private_users.csv", "text/csv"), return_blob: true) } + let(:members_to) { create(:participatory_process, organization:) } + let(:file) { upload_test_file(Decidim::Dev.test_file("import_members.csv", "text/csv"), return_blob: true) } let(:validity) { true } let(:form) do double( current_user:, - private_users_to:, + members_to:, current_organization: organization, file:, valid?: validity @@ -30,14 +30,14 @@ module Decidim::Admin end it "does not enqueue any job" do - expect(ImportParticipatorySpacePrivateUserCsvJob).not_to receive(:perform_later) + expect(ImportMemberCsvJob).not_to receive(:perform_later) subject.call end end context "when the CSV file has BOM" do - let(:file) { upload_test_file(Decidim::Dev.test_file("import_participatory_space_private_users_with_bom.csv", "text/csv"), return_blob: true) } + let(:file) { upload_test_file(Decidim::Dev.test_file("import_members_with_bom.csv", "text/csv"), return_blob: true) } let(:email) { "john.doe@example.org" } it "broadcasts ok" do @@ -45,7 +45,7 @@ module Decidim::Admin end it "enqueues a job for each present value without BOM" do - expect(ImportParticipatorySpacePrivateUserCsvJob).to receive(:perform_later).with(email, kind_of(String), private_users_to, current_user) + expect(ImportMemberCsvJob).to receive(:perform_later).with(email, kind_of(String), members_to, current_user) subject.call end @@ -56,7 +56,7 @@ module Decidim::Admin end it "enqueues a job for each present value" do - expect(ImportParticipatorySpacePrivateUserCsvJob).to receive(:perform_later).twice.with(kind_of(String), kind_of(String), private_users_to, current_user) + expect(ImportMemberCsvJob).to receive(:perform_later).twice.with(kind_of(String), kind_of(String), members_to, current_user) subject.call end diff --git a/decidim-admin/spec/commands/decidim/admin/publish_all_participatory_space_private_users_spec.rb b/decidim-admin/spec/commands/decidim/admin/participatory_space/publish_all_members_spec.rb similarity index 72% rename from decidim-admin/spec/commands/decidim/admin/publish_all_participatory_space_private_users_spec.rb rename to decidim-admin/spec/commands/decidim/admin/participatory_space/publish_all_members_spec.rb index 409887aee336e..d1088c5a1bfd9 100644 --- a/decidim-admin/spec/commands/decidim/admin/publish_all_participatory_space_private_users_spec.rb +++ b/decidim-admin/spec/commands/decidim/admin/participatory_space/publish_all_members_spec.rb @@ -2,20 +2,20 @@ require "spec_helper" -module Decidim::Admin - describe PublishAllParticipatorySpacePrivateUsers do +module Decidim::Admin::ParticipatorySpace + describe PublishAllMembers do subject { described_class.new(privatable_to, current_user) } let!(:privatable_to) { create(:participatory_process) } let!(:user) { create(:user, email: "my_email@example.org", organization: privatable_to.organization) } - let!(:private_user) { create(:participatory_space_private_user, :unpublished, user:, privatable_to:, role:) } + let!(:member) { create(:member, :unpublished, user:, privatable_to:, role:) } let(:role) { generate_localized_title(:role) } let(:current_user) { create(:user, email: "admin@example.org", organization: privatable_to.organization) } it "updates the published attribute" do subject.call - expect(private_user.reload.published).to be(true) + expect(member.reload.published).to be(true) end it "creates an action log" do diff --git a/decidim-admin/spec/commands/decidim/admin/unpublish_all_participatory_space_private_users_spec.rb b/decidim-admin/spec/commands/decidim/admin/participatory_space/unpublish_all_members_spec.rb similarity index 72% rename from decidim-admin/spec/commands/decidim/admin/unpublish_all_participatory_space_private_users_spec.rb rename to decidim-admin/spec/commands/decidim/admin/participatory_space/unpublish_all_members_spec.rb index dbe01ab48bb50..e461a9686b5b6 100644 --- a/decidim-admin/spec/commands/decidim/admin/unpublish_all_participatory_space_private_users_spec.rb +++ b/decidim-admin/spec/commands/decidim/admin/participatory_space/unpublish_all_members_spec.rb @@ -2,20 +2,20 @@ require "spec_helper" -module Decidim::Admin - describe UnpublishAllParticipatorySpacePrivateUsers do +module Decidim::Admin::ParticipatorySpace + describe UnpublishAllMembers do subject { described_class.new(privatable_to, current_user) } let!(:privatable_to) { create(:participatory_process) } let!(:user) { create(:user, email: "my_email@example.org", organization: privatable_to.organization) } - let!(:private_user) { create(:participatory_space_private_user, :published, user:, privatable_to:, role:) } + let!(:member) { create(:member, :published, user:, privatable_to:, role:) } let(:role) { generate_localized_title(:role) } let(:current_user) { create(:user, email: "admin@example.org", organization: privatable_to.organization) } it "updates the published attribute" do subject.call - expect(private_user.reload.published).to be(false) + expect(member.reload.published).to be(false) end it "creates an action log" do diff --git a/decidim-admin/spec/commands/decidim/admin/update_participatory_space_private_user_spec.rb b/decidim-admin/spec/commands/decidim/admin/participatory_space/update_member_spec.rb similarity index 71% rename from decidim-admin/spec/commands/decidim/admin/update_participatory_space_private_user_spec.rb rename to decidim-admin/spec/commands/decidim/admin/participatory_space/update_member_spec.rb index 8b2da3f6b99d3..a9a1c4d766c8c 100644 --- a/decidim-admin/spec/commands/decidim/admin/update_participatory_space_private_user_spec.rb +++ b/decidim-admin/spec/commands/decidim/admin/participatory_space/update_member_spec.rb @@ -2,12 +2,12 @@ require "spec_helper" -module Decidim::Admin - describe UpdateParticipatorySpacePrivateUser do - subject { described_class.new(form, private_user) } +module Decidim::Admin::ParticipatorySpace + describe UpdateMember do + subject { described_class.new(form, member) } let!(:privatable_to) { create(:participatory_process) } - let!(:private_user) { create(:participatory_space_private_user, :unpublished, user:, role:) } + let!(:member) { create(:member, :unpublished, user:, role:) } let!(:user) { create(:user, email: "my_email@example.org", organization: privatable_to.organization) } let!(:current_user) { create(:user, email: "some_email@example.org", organization: privatable_to.organization) } @@ -35,13 +35,13 @@ module Decidim::Admin it "updates the role" do subject.call - expect(translated(private_user.reload.role)).to eq(translated_attribute(role)) + expect(translated(member.reload.role)).to eq(translated_attribute(role)) end it "updates the published status" do subject.call - expect(private_user.reload.published).to eq(published) + expect(member.reload.published).to eq(published) end end end diff --git a/decidim-admin/spec/forms/decidim/admin/participatory_space/member_csv_import_form_spec.rb b/decidim-admin/spec/forms/decidim/admin/participatory_space/member_csv_import_form_spec.rb new file mode 100644 index 0000000000000..7d1ef2c5c00f1 --- /dev/null +++ b/decidim-admin/spec/forms/decidim/admin/participatory_space/member_csv_import_form_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Admin + module ParticipatorySpace + describe MemberCsvImportForm do + subject do + described_class.from_params( + attributes + ).with_context( + current_user:, + current_organization: + ) + end + + let(:current_organization) { create(:organization) } + let(:current_user) { create(:user, organization: current_organization) } + + let(:attributes) do + { + "file" => file + } + end + let(:file) { upload_test_file(Decidim::Dev.asset("import_members.csv")) } + + context "when everything is OK" do + it { is_expected.to be_valid } + end + + context "when file is missing" do + let(:file) { nil } + + it { is_expected.to be_invalid } + end + + context "when user name contains invalid chars" do + let(:file) { upload_test_file(Decidim::Dev.asset("import_members_nok.csv")) } + + it { is_expected.to be_invalid } + end + + context "when the CSV separator is incorrect" do + let(:file) { upload_test_file(Decidim::Dev.asset("import_members_invalid_col_sep.csv")) } + + it { is_expected.to be_invalid } + end + + context "when the provided file is encoded with incorrect character set" do + let(:file) { upload_test_file(Decidim::Dev.asset("import_members_iso8859-1.csv")) } + + it { is_expected.to be_invalid } + + it "adds the correct error" do + subject.valid? + expect(subject.errors[:file].join).to eq("Malformed import file, please read through the instructions carefully and make sure the file is UTF-8 encoded.") + end + end + end + end + end +end diff --git a/decidim-admin/spec/forms/decidim/admin/participatory_space/member_form_spec.rb b/decidim-admin/spec/forms/decidim/admin/participatory_space/member_form_spec.rb new file mode 100644 index 0000000000000..b273724ea2d9c --- /dev/null +++ b/decidim-admin/spec/forms/decidim/admin/participatory_space/member_form_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Admin + module ParticipatorySpace + describe MemberForm do + subject { described_class.from_params(attributes) } + + let(:email) { "my_email@example.org" } + let(:name) { "John Wayne" } + let(:attributes) do + { + "member" => { + "email" => email, + "name" => name + } + } + end + + context "when everything is OK" do + it { is_expected.to be_valid } + end + + context "when email is missing" do + let(:email) { nil } + + it { is_expected.to be_invalid } + end + end + end + end +end diff --git a/decidim-admin/spec/forms/participatory_space_private_user_csv_import_form_spec.rb b/decidim-admin/spec/forms/participatory_space_private_user_csv_import_form_spec.rb deleted file mode 100644 index 0a99ef331870e..0000000000000 --- a/decidim-admin/spec/forms/participatory_space_private_user_csv_import_form_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -module Decidim - module Admin - describe ParticipatorySpacePrivateUserCsvImportForm do - subject do - described_class.from_params( - attributes - ).with_context( - current_user:, - current_organization: - ) - end - - let(:current_organization) { create(:organization) } - let(:current_user) { create(:user, organization: current_organization) } - - let(:attributes) do - { - "file" => file - } - end - let(:file) { upload_test_file(Decidim::Dev.asset("import_participatory_space_private_users.csv")) } - - context "when everything is OK" do - it { is_expected.to be_valid } - end - - context "when file is missing" do - let(:file) { nil } - - it { is_expected.to be_invalid } - end - - context "when user name contains invalid chars" do - let(:file) { upload_test_file(Decidim::Dev.asset("import_participatory_space_private_users_nok.csv")) } - - it { is_expected.to be_invalid } - end - - context "when the CSV separator is incorrect" do - let(:file) { upload_test_file(Decidim::Dev.asset("import_participatory_space_private_users_invalid_col_sep.csv")) } - - it { is_expected.to be_invalid } - end - - context "when the provided file is encoded with incorrect character set" do - let(:file) { upload_test_file(Decidim::Dev.asset("import_participatory_space_private_users_iso8859-1.csv")) } - - it { is_expected.to be_invalid } - - it "adds the correct error" do - subject.valid? - expect(subject.errors[:file].join).to eq("Malformed import file, please read through the instructions carefully and make sure the file is UTF-8 encoded.") - end - end - end - end -end diff --git a/decidim-admin/spec/forms/participatory_space_private_user_form_spec.rb b/decidim-admin/spec/forms/participatory_space_private_user_form_spec.rb deleted file mode 100644 index 96c3fdd7df1a8..0000000000000 --- a/decidim-admin/spec/forms/participatory_space_private_user_form_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -module Decidim - module Admin - describe ParticipatorySpacePrivateUserForm do - subject { described_class.from_params(attributes) } - - let(:email) { "my_email@example.org" } - let(:name) { "John Wayne" } - let(:attributes) do - { - "participatory_space_private_user" => { - "email" => email, - "name" => name - } - } - end - - context "when everything is OK" do - it { is_expected.to be_valid } - end - - context "when email is missing" do - let(:email) { nil } - - it { is_expected.to be_invalid } - end - end - end -end diff --git a/decidim-admin/spec/jobs/decidim/admin/participatory_space/destroy_members_follows_job_spec.rb b/decidim-admin/spec/jobs/decidim/admin/participatory_space/destroy_members_follows_job_spec.rb new file mode 100644 index 0000000000000..f4857f822f44d --- /dev/null +++ b/decidim-admin/spec/jobs/decidim/admin/participatory_space/destroy_members_follows_job_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Admin + module ParticipatorySpace + describe DestroyMembersFollowsJob do + let(:organization) { create(:organization) } + let!(:user) { create(:user, :admin, :confirmed, organization:) } + let!(:normal_user) { create(:user, organization:) } + let!(:follow) { create(:follow, followable: participatory_space, user: normal_user) } + let(:component) { create(:dummy_component, participatory_space:) } + let(:resource) { create(:dummy_resource, component: component, author: user) } + let!(:followed_resource) { create(:follow, followable: resource, user: normal_user) } + + context "when assembly is private and non transparent" do + let(:participatory_space) { create(:assembly, :private, :published, :opaque, organization: user.organization) } + + it "deletes follows of non members" do + # we have 2 follows, one for assembly, and one for a "child" resource + expect { described_class.perform_now(normal_user.id, participatory_space) }.to change(Decidim::Follow, :count).by(-2) + end + end + + context "when assembly is private but transparent" do + let(:participatory_space) { create(:assembly, :private, :published, organization: user.organization) } + + it "preserves follows of non members" do + # we have 2 follows, one for assembly, and one for a "child" resource + expect { described_class.perform_now(normal_user.id, participatory_space) }.not_to change(Decidim::Follow, :count) + end + end + + context "when assembly is public" do + let(:participatory_space) { create(:assembly, :published, organization: user.organization) } + + it "preserves follows of non members" do + # we have 2 follows, one for assembly, and one for a "child" resource + expect { described_class.perform_now(normal_user.id, participatory_space) }.not_to change(Decidim::Follow, :count) + end + end + + context "when process is private" do + let(:participatory_space) { create(:participatory_process, :private, :published, organization: user.organization) } + + it "deletes follows of non members" do + # we have 2 follows, one for process, and one for a "child" resource + expect { described_class.perform_now(normal_user.id, participatory_space) }.to change(Decidim::Follow, :count).by(-2) + end + end + + context "when process is public" do + let(:participatory_space) { create(:participatory_process, :published, organization: user.organization) } + + it "preserves follows of non members" do + expect { described_class.perform_now(normal_user.id, participatory_space) }.not_to change(Decidim::Follow, :count) + end + end + end + end + end +end diff --git a/decidim-admin/spec/jobs/decidim/admin/participatory_space/import_member_csv_job_spec.rb b/decidim-admin/spec/jobs/decidim/admin/participatory_space/import_member_csv_job_spec.rb new file mode 100644 index 0000000000000..fe8bf9841094e --- /dev/null +++ b/decidim-admin/spec/jobs/decidim/admin/participatory_space/import_member_csv_job_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Admin + module ParticipatorySpace + describe ImportMemberCsvJob do + let!(:email) { "my_user@example.org" } + let!(:user_name) { "My User Name" } + let(:user) { create(:user, :admin, organization:) } + let(:organization) { create(:organization) } + let(:privatable_to) { create(:participatory_process, organization:) } + + context "when the member not exists" do + it "delegates the work to a command" do + expect(Decidim::Admin::ParticipatorySpace::CreateMember).to receive(:call) + described_class.perform_now(email, user_name, privatable_to, user) + end + end + end + end + end +end diff --git a/decidim-admin/spec/jobs/destroy_private_users_follows_job_spec.rb b/decidim-admin/spec/jobs/destroy_private_users_follows_job_spec.rb deleted file mode 100644 index 15ad1b33c7240..0000000000000 --- a/decidim-admin/spec/jobs/destroy_private_users_follows_job_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -module Decidim - module Admin - describe DestroyPrivateUsersFollowsJob do - let(:organization) { create(:organization) } - let!(:user) { create(:user, :admin, :confirmed, organization:) } - let!(:normal_user) { create(:user, organization:) } - let!(:follow) { create(:follow, followable: participatory_space, user: normal_user) } - let(:component) { create(:dummy_component, participatory_space:) } - let(:resource) { create(:dummy_resource, component: component, author: user) } - let!(:followed_resource) { create(:follow, followable: resource, user: normal_user) } - - context "when assembly is private and non transparent" do - let(:participatory_space) { create(:assembly, :private, :published, :opaque, organization: user.organization) } - - it "deletes follows of non private users" do - # we have 2 follows, one for assembly, and one for a "child" resource - expect { described_class.perform_now(normal_user.id, participatory_space) }.to change(Decidim::Follow, :count).by(-2) - end - end - - context "when assembly is private but transparent" do - let(:participatory_space) { create(:assembly, :private, :published, organization: user.organization) } - - it "preserves follows of non private users" do - # we have 2 follows, one for assembly, and one for a "child" resource - expect { described_class.perform_now(normal_user.id, participatory_space) }.not_to change(Decidim::Follow, :count) - end - end - - context "when assembly is public" do - let(:participatory_space) { create(:assembly, :published, organization: user.organization) } - - it "preserves follows of non private users" do - # we have 2 follows, one for assembly, and one for a "child" resource - expect { described_class.perform_now(normal_user.id, participatory_space) }.not_to change(Decidim::Follow, :count) - end - end - - context "when process is private" do - let(:participatory_space) { create(:participatory_process, :private, :published, organization: user.organization) } - - it "deletes follows of non private users" do - # we have 2 follows, one for process, and one for a "child" resource - expect { described_class.perform_now(normal_user.id, participatory_space) }.to change(Decidim::Follow, :count).by(-2) - end - end - - context "when process is public" do - let(:participatory_space) { create(:participatory_process, :published, organization: user.organization) } - - it "preserves follows of non private users" do - expect { described_class.perform_now(normal_user.id, participatory_space) }.not_to change(Decidim::Follow, :count) - end - end - end - end -end diff --git a/decidim-admin/spec/jobs/import_participatory_space_private_user_csv_job_spec.rb b/decidim-admin/spec/jobs/import_participatory_space_private_user_csv_job_spec.rb deleted file mode 100644 index 9b712c1527114..0000000000000 --- a/decidim-admin/spec/jobs/import_participatory_space_private_user_csv_job_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -module Decidim - module Admin - describe ImportParticipatorySpacePrivateUserCsvJob do - let!(:email) { "my_user@example.org" } - let!(:user_name) { "My User Name" } - let(:user) { create(:user, :admin, organization:) } - let(:organization) { create(:organization) } - let(:privatable_to) { create(:participatory_process, organization:) } - - context "when the participatory space private user not exists" do - it "delegates the work to a command" do - expect(Decidim::Admin::CreateParticipatorySpacePrivateUser).to receive(:call) - described_class.perform_now(email, user_name, privatable_to, user) - end - end - end - end -end diff --git a/decidim-admin/spec/queries/newsletter_recipients_spec.rb b/decidim-admin/spec/queries/newsletter_recipients_spec.rb index a52eeba563ecb..ae022ea8a6c8f 100644 --- a/decidim-admin/spec/queries/newsletter_recipients_spec.rb +++ b/decidim-admin/spec/queries/newsletter_recipients_spec.rb @@ -163,7 +163,7 @@ module Decidim::Admin before do recipients.each do |member| - create(:participatory_space_private_user, privatable_to: participatory_process, user: member) + create(:member, privatable_to: participatory_process, user: member) end end diff --git a/decidim-admin/spec/system/admin_manages_newsletters_spec.rb b/decidim-admin/spec/system/admin_manages_newsletters_spec.rb index 04520b06392c1..1e60d09cec460 100644 --- a/decidim-admin/spec/system/admin_manages_newsletters_spec.rb +++ b/decidim-admin/spec/system/admin_manages_newsletters_spec.rb @@ -472,15 +472,15 @@ def select_verification_type(types) context "when private members are selected" do context "with private members" do let!(:participatory_process) { create(:participatory_process, organization:, skip_injection: true, private_space: true) } - let!(:private_users) do - create_list(:participatory_space_private_user, 30) do |private_user| - private_user.user = create(:user, :confirmed, newsletter_notifications_at: Time.current, organization:) - private_user.privatable_to = participatory_process - private_user.save! + let!(:members) do + create_list(:member, 30) do |member| + member.user = create(:user, :confirmed, newsletter_notifications_at: Time.current, organization:) + member.privatable_to = participatory_process + member.save! end end - let(:recipients_count) { private_users.size } + let(:recipients_count) { members.size } it "sends to private members", :slow do visit decidim_admin.select_recipients_to_deliver_newsletter_path(newsletter) diff --git a/decidim-admin/spec/system/participatory_space_private_user_import_via_csv_spec.rb b/decidim-admin/spec/system/member_import_via_csv_spec.rb similarity index 70% rename from decidim-admin/spec/system/participatory_space_private_user_import_via_csv_spec.rb rename to decidim-admin/spec/system/member_import_via_csv_spec.rb index 7236ff8024a13..9f3cf008cd235 100644 --- a/decidim-admin/spec/system/participatory_space_private_user_import_via_csv_spec.rb +++ b/decidim-admin/spec/system/member_import_via_csv_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -describe "Admin manages participatory space private users via csv import" do +describe "Admin manages members via csv import" do let(:organization) { create(:organization) } let!(:user) { create(:user, :admin, :confirmed, organization:) } @@ -18,19 +18,19 @@ click_on "Import via CSV" end - it "show the form to add some private users via csv" do + it "show the form to add some members via csv" do expect(page).to have_content("Upload your CSV file") end context "when there are no existing users" do it "does not propose to delete" do - expect(page).to have_content("You have no private participants.") + expect(page).to have_content("You have no members.") end end context "when there are existing users" do before do - create_list(:assembly_private_user, 3, privatable_to: assembly, user: create(:user, organization: assembly.organization)) + create_list(:assembly_member, 3, privatable_to: assembly, user: create(:user, organization: assembly.organization)) visit current_path end @@ -41,11 +41,11 @@ it "ask you for confirmation and delete existing users" do find(".alert").click - expect(page).to have_content("Are you sure you want to delete all private participants?") + expect(page).to have_content("Are you sure you want to delete all members?") click_on("OK") - expect(page).to have_content("You have no private participants") + expect(page).to have_content("You have no members") end end end diff --git a/decidim-admin/spec/system/participatory_space_private_user_spec.rb b/decidim-admin/spec/system/member_spec.rb similarity index 59% rename from decidim-admin/spec/system/participatory_space_private_user_spec.rb rename to decidim-admin/spec/system/member_spec.rb index ac1babe7a3816..3c028e2e56978 100644 --- a/decidim-admin/spec/system/participatory_space_private_user_spec.rb +++ b/decidim-admin/spec/system/member_spec.rb @@ -2,13 +2,13 @@ require "spec_helper" -describe "Admin checks pagination on participatory space private users" do +describe "Admin checks pagination on members" do let(:organization) { create(:organization) } let!(:user) { create(:user, :admin, :confirmed, organization:) } let(:assembly) { create(:assembly, organization:, private_space: true) } - let!(:private_users) { create_list(:assembly_private_user, 26, privatable_to: assembly, user: create(:user, organization: assembly.organization)) } + let!(:members) { create_list(:assembly_member, 26, privatable_to: assembly, user: create(:user, organization: assembly.organization)) } before do switch_to_host(organization.host) @@ -19,8 +19,8 @@ end end - it "shows private users of the participatory space and changes page correctly" do + it "shows members of the participatory space and changes page correctly" do find("li a", text: "Next").click - expect(page).to have_current_path "#{decidim_admin_assemblies.participatory_space_private_users_path(assembly_slug: assembly.slug)}?page=2" + expect(page).to have_current_path "#{decidim_admin_assemblies.members_path(assembly_slug: assembly.slug)}?page=2" end end diff --git a/decidim-api/lib/decidim/api/test/component_context.rb b/decidim-api/lib/decidim/api/test/component_context.rb index e07af3fe0dc53..5a9c4f3362947 100644 --- a/decidim-api/lib/decidim/api/test/component_context.rb +++ b/decidim-api/lib/decidim/api/test/component_context.rb @@ -126,13 +126,13 @@ context "when user is member" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } - let!(:participatory_space_private_user) { create(:participatory_space_private_user, user: current_user, privatable_to: participatory_process) } + let!(:member) { create(:member, user: current_user, privatable_to: participatory_process) } it_behaves_like "graphQL visible resource" end context "when user is member" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } - let!(:participatory_space_private_user) { create(:participatory_space_private_user, user: current_user, privatable_to: participatory_process) } + let!(:member) { create(:member, user: current_user, privatable_to: participatory_process) } it_behaves_like "graphQL visible resource" end @@ -184,7 +184,7 @@ context "when user is member" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } - let!(:participatory_space_private_user) { create(:participatory_space_private_user, user: current_user, privatable_to: participatory_process) } + let!(:member) { create(:member, user: current_user, privatable_to: participatory_process) } it_behaves_like "graphQL hidden component" end end @@ -248,7 +248,7 @@ context "when user is member" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } - let!(:participatory_space_private_user) { create(:assembly_private_user, user: current_user, privatable_to: participatory_process) } + let!(:member) { create(:assembly_member, user: current_user, privatable_to: participatory_process) } it_behaves_like "graphQL visible resource" end @@ -299,7 +299,7 @@ context "when user is member" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } - let!(:participatory_space_private_user) { create(:assembly_private_user, user: current_user, privatable_to: participatory_process) } + let!(:member) { create(:assembly_member, user: current_user, privatable_to: participatory_process) } it_behaves_like "graphQL hidden component" end end @@ -347,7 +347,7 @@ context "when user is member" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } - let!(:participatory_space_private_user) { create(:participatory_space_private_user, user: current_user, privatable_to: participatory_process) } + let!(:member) { create(:member, user: current_user, privatable_to: participatory_process) } it_behaves_like "graphQL visible resource" end end @@ -384,7 +384,7 @@ context "when user is member" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } - let!(:participatory_space_private_user) { create(:participatory_space_private_user, user: current_user, privatable_to: participatory_process) } + let!(:member) { create(:member, user: current_user, privatable_to: participatory_process) } it_behaves_like "graphQL hidden component" end context "when user is normal user" do @@ -430,7 +430,7 @@ context "when user is member" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } - let!(:participatory_space_private_user) { create(:participatory_space_private_user, user: current_user, privatable_to: participatory_process) } + let!(:member) { create(:member, user: current_user, privatable_to: participatory_process) } it_behaves_like "graphQL not found space" end @@ -472,7 +472,7 @@ context "when user is member" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } - let!(:participatory_space_private_user) { create(:participatory_space_private_user, user: current_user, privatable_to: participatory_process) } + let!(:member) { create(:member, user: current_user, privatable_to: participatory_process) } it_behaves_like "graphQL not found space" end diff --git a/decidim-assemblies/app/controllers/decidim/assemblies/admin/members_controller.rb b/decidim-assemblies/app/controllers/decidim/assemblies/admin/members_controller.rb new file mode 100644 index 0000000000000..1b6429e3acda6 --- /dev/null +++ b/decidim-assemblies/app/controllers/decidim/assemblies/admin/members_controller.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Decidim + module Assemblies + module Admin + # Controller that allows managing assembly members + # on assemblies + class MembersController < Decidim::Assemblies::Admin::ApplicationController + include Concerns::AssemblyAdmin + include Decidim::Admin::ParticipatorySpace::Concerns::HasMembers + + def after_destroy_path + members_path(current_assembly) + end + + def privatable_to + current_assembly + end + end + end + end +end diff --git a/decidim-assemblies/app/controllers/decidim/assemblies/admin/members_csv_imports_controller.rb b/decidim-assemblies/app/controllers/decidim/assemblies/admin/members_csv_imports_controller.rb new file mode 100644 index 0000000000000..e1a35451304c8 --- /dev/null +++ b/decidim-assemblies/app/controllers/decidim/assemblies/admin/members_csv_imports_controller.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Decidim + module Assemblies + module Admin + # Controller that allows importing assembly members + # on assemblies + class MembersCsvImportsController < Decidim::Admin::ApplicationController + include Concerns::AssemblyAdmin + include Decidim::Admin::ParticipatorySpace::Concerns::HasMembersCsvImport + + def after_import_path + members_path(current_assembly) + end + + def privatable_to + current_assembly + end + end + end + end +end diff --git a/decidim-assemblies/app/controllers/decidim/assemblies/admin/participatory_space_private_users_controller.rb b/decidim-assemblies/app/controllers/decidim/assemblies/admin/participatory_space_private_users_controller.rb deleted file mode 100644 index c852029aa5fc1..0000000000000 --- a/decidim-assemblies/app/controllers/decidim/assemblies/admin/participatory_space_private_users_controller.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -module Decidim - module Assemblies - module Admin - # Controller that allows managing assembly private users - # on assemblies - class ParticipatorySpacePrivateUsersController < Decidim::Assemblies::Admin::ApplicationController - include Concerns::AssemblyAdmin - include Decidim::Admin::Concerns::HasPrivateUsers - - def after_destroy_path - participatory_space_private_users_path(current_assembly) - end - - def privatable_to - current_assembly - end - end - end - end -end diff --git a/decidim-assemblies/app/controllers/decidim/assemblies/admin/participatory_space_private_users_csv_imports_controller.rb b/decidim-assemblies/app/controllers/decidim/assemblies/admin/participatory_space_private_users_csv_imports_controller.rb deleted file mode 100644 index 7e2dbc07455fb..0000000000000 --- a/decidim-assemblies/app/controllers/decidim/assemblies/admin/participatory_space_private_users_csv_imports_controller.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -module Decidim - module Assemblies - module Admin - # Controller that allows importing assembly private users - # on assemblies - class ParticipatorySpacePrivateUsersCsvImportsController < Decidim::Admin::ApplicationController - include Concerns::AssemblyAdmin - include Decidim::Admin::Concerns::HasPrivateUsersCsvImport - - def after_import_path - participatory_space_private_users_path(current_assembly) - end - - def privatable_to - current_assembly - end - end - end - end -end diff --git a/decidim-assemblies/app/controllers/decidim/assemblies/participatory_space_private_users_controller.rb b/decidim-assemblies/app/controllers/decidim/assemblies/members_controller.rb similarity index 85% rename from decidim-assemblies/app/controllers/decidim/assemblies/participatory_space_private_users_controller.rb rename to decidim-assemblies/app/controllers/decidim/assemblies/members_controller.rb index 1420c746d03b9..4c9211e9929bd 100644 --- a/decidim-assemblies/app/controllers/decidim/assemblies/participatory_space_private_users_controller.rb +++ b/decidim-assemblies/app/controllers/decidim/assemblies/members_controller.rb @@ -2,9 +2,9 @@ module Decidim module Assemblies - class ParticipatorySpacePrivateUsersController < Decidim::Assemblies::ApplicationController + class MembersController < Decidim::Assemblies::ApplicationController include ParticipatorySpaceContext - include Decidim::HasMembersPage + include Decidim::ParticipatorySpace::HasMembersPage def index raise ActionController::RoutingError, "No members for this assembly" if members.none? diff --git a/decidim-assemblies/app/helpers/decidim/assemblies/assemblies_helper.rb b/decidim-assemblies/app/helpers/decidim/assemblies/assemblies_helper.rb index 7cee36698de2d..643a3ae029a36 100644 --- a/decidim-assemblies/app/helpers/decidim/assemblies/assemblies_helper.rb +++ b/decidim-assemblies/app/helpers/decidim/assemblies/assemblies_helper.rb @@ -20,8 +20,8 @@ def assembly_nav_items(participatory_space) *(if participatory_space.members_public_page? [{ name: t("assembly_member_menu_item", scope: "layouts.decidim.assembly_navigation"), - url: decidim_assemblies.assembly_participatory_space_private_users_path(participatory_space, locale: current_locale), - active: is_active_link?(decidim_assemblies.assembly_participatory_space_private_users_path(participatory_space, locale: current_locale), :inclusive) + url: decidim_assemblies.assembly_members_path(participatory_space, locale: current_locale), + active: is_active_link?(decidim_assemblies.assembly_members_path(participatory_space, locale: current_locale), :inclusive) }] end ) diff --git a/decidim-assemblies/app/models/decidim/assembly.rb b/decidim-assemblies/app/models/decidim/assembly.rb index 43f794a1a7b2e..9f718023f3b27 100644 --- a/decidim-assemblies/app/models/decidim/assembly.rb +++ b/decidim-assemblies/app/models/decidim/assembly.rb @@ -31,7 +31,7 @@ class Assembly < ApplicationRecord include Decidim::Traceable include Decidim::Loggable include Decidim::ParticipatorySpaceResourceable - include Decidim::HasPrivateUsers + include Decidim::ParticipatorySpace::HasMembers include Decidim::Searchable include Decidim::HasUploadValidations include Decidim::TranslatableResource @@ -92,7 +92,7 @@ class Assembly < ApplicationRecord index_on_create: ->(_assembly) { false }, index_on_update: ->(assembly) { assembly.visible? }) - # Overwriting existing method Decidim::HasPrivateUsers.public_spaces + # Overwriting existing method Decidim::ParticipatorySpace::HasMembers.public_spaces def self.public_spaces where(private_space: false).or(where(private_space: true).where(is_transparent: true)).published end diff --git a/decidim-assemblies/app/packs/src/decidim/assemblies/controllers/assembly_admin/assembly_admin.test.js b/decidim-assemblies/app/packs/src/decidim/assemblies/controllers/assembly_admin/assembly_admin.test.js index 12e5ed74c9a7a..0629e11cc3264 100644 --- a/decidim-assemblies/app/packs/src/decidim/assemblies/controllers/assembly_admin/assembly_admin.test.js +++ b/decidim-assemblies/app/packs/src/decidim/assemblies/controllers/assembly_admin/assembly_admin.test.js @@ -748,7 +748,7 @@ describe("AssemblyAdminController", () => { -

You will be able to manage private participants after setting it as private

+

You will be able to manage members after setting it as private

+
+ <%= f.translated :text_field, :short_name, help_text: t(".short_name_hint") %> +
+ <%= f.text_field :host %> <%= f.text_area :secondary_hosts, help_text: t(".secondary_hosts_hint") %> diff --git a/decidim-system/app/views/decidim/system/organizations/new.html.erb b/decidim-system/app/views/decidim/system/organizations/new.html.erb index 25f19ccf0b457..9ce3a24d0b66e 100644 --- a/decidim-system/app/views/decidim/system/organizations/new.html.erb +++ b/decidim-system/app/views/decidim/system/organizations/new.html.erb @@ -8,6 +8,8 @@
<%= f.text_field :name, autofocus: true %> + <%= f.text_field :short_name, help_text: t(".short_name_hint") %> + <%= f.text_field :reference_prefix, help_text: t(".reference_prefix_hint") %> <%= f.text_field :host %> diff --git a/decidim-system/config/locales/en.yml b/decidim-system/config/locales/en.yml index 83a2cebf65400..f474807dc447d 100644 --- a/decidim-system/config/locales/en.yml +++ b/decidim-system/config/locales/en.yml @@ -270,6 +270,7 @@ en: confirm_resend_invitation: Are you sure you want to resend the invitation? resend_invitation: Resend invitation secondary_hosts_hint: Enter each one of them in a new line + short_name_hint: Short name used for the Progressive Web Application. It must have 12 characters maximum. title: Edit organization file_upload_settings: content_types: @@ -299,6 +300,7 @@ en: organization_admin_email_hint: We will send an email to this address so you can confirm it and set up your password. reference_prefix_hint: The reference prefix is used to uniquely identify resources across all organization. secondary_hosts_hint: Enter each one of them in a new line. + short_name_hint: Short name used for the Progressive Web Application. It must have 12 characters maximum. title: New organization omniauth_settings: decidim: diff --git a/decidim-system/spec/commands/decidim/system/create_organization_spec.rb b/decidim-system/spec/commands/decidim/system/create_organization_spec.rb index 89380cbe5b0f2..9fa8ee532a141 100644 --- a/decidim-system/spec/commands/decidim/system/create_organization_spec.rb +++ b/decidim-system/spec/commands/decidim/system/create_organization_spec.rb @@ -17,6 +17,7 @@ module System let(:params) do { name: "Gotham City", + short_name: "GothamCity", host: "decide.example.org", secondary_hosts: "foo.example.org\r\n\r\nbar.example.org", reference_prefix: "JKR", @@ -55,6 +56,7 @@ module System expect { command.call }.to change(Organization, :count).by(1) organization = Organization.last expect(translated(organization.name)).to eq("Gotham City") + expect(translated(organization.short_name)).to eq("GothamCity") expect(organization.host).to eq("decide.example.org") expect(organization.secondary_hosts).to contain_exactly("foo.example.org", "bar.example.org") expect(organization.external_domain_allowlist).to contain_exactly("decidim.org", "github.com") diff --git a/decidim-system/spec/commands/decidim/system/update_organization_spec.rb b/decidim-system/spec/commands/decidim/system/update_organization_spec.rb index 8106da38f5fe1..29fd5487b6376 100644 --- a/decidim-system/spec/commands/decidim/system/update_organization_spec.rb +++ b/decidim-system/spec/commands/decidim/system/update_organization_spec.rb @@ -18,6 +18,7 @@ module System let(:params) do { name: { en: "Gotham City" }, + short_name: { en: "GothamCity" }, host: "decide.example.org", secondary_hosts: "foo.example.org\r\n\r\nbar.example.org", force_users_to_authenticate_before_access_organization: false, @@ -130,6 +131,7 @@ module System let(:params) do { name: { en: "Gotham City" }, + short_name: { en: "GothamCity" }, host: "decide.example.org", users_registration_mode: "existing", file_upload_settings: params_for_uploads(upload_settings), diff --git a/decidim-system/spec/forms/decidim/system/register_organization_form_spec.rb b/decidim-system/spec/forms/decidim/system/register_organization_form_spec.rb new file mode 100644 index 0000000000000..8149a01078c16 --- /dev/null +++ b/decidim-system/spec/forms/decidim/system/register_organization_form_spec.rb @@ -0,0 +1,459 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim::System + describe RegisterOrganizationForm do + subject do + described_class.new( + name: "Gotham City", + short_name: "GothamCity", + host: "decide.example.org", + secondary_hosts: "foo.example.org\r\n\r\nbar.example.org", + reference_prefix: "JKR", + organization_admin_name: "Fiorello Henry La Guardia", + organization_admin_email: "f.laguardia@example.org", + available_locales: ["en"], + default_locale: "en", + users_registration_mode: "enabled", + force_users_to_authenticate_before_access_organization: "false", + **smtp_settings, + **omniauth_settings + ) + end + let(:omniauth_settings) do + { + "omniauth_settings_facebook_enabled" => true, + "omniauth_settings_facebook_app_id" => facebook_app_id, + "omniauth_settings_facebook_app_secret" => facebook_app_secret + } + end + let(:smtp_settings) do + { + "address" => "mail.example.org", + "port" => 25, + "user_name" => "f.laguardia", + "password" => password, + "from_email" => "decide@example.org", + "from_label" => from_label + } + end + let(:password) { "secret_password" } + let(:from_label) { "Decide Gotham" } + let(:facebook_app_id) { "plain-text-facebook-app-id" } + let(:facebook_app_secret) { "plain-text-facebook-app-secret" } + + context "when everything is OK" do + it { is_expected.to be_valid } + + describe "omniauth_settings" do + it "contains attributes as plain text" do + expect(subject.omniauth_settings_facebook_enabled).to be(true) + expect(subject.omniauth_settings_facebook_app_id).to eq(facebook_app_id) + expect(subject.omniauth_settings_facebook_app_secret).to eq(facebook_app_secret) + end + + context "when all values are blank" do + let(:omniauth_settings) do + { + "omniauth_settings_facebook_enabled" => nil, + "omniauth_settings_facebook_app_id" => nil, + "omniauth_settings_facebook_app_secret" => nil + } + end + + it "returns nil" do + expect(subject.encrypted_omniauth_settings).to be_nil + end + end + end + + describe "encrypted_omniauth_settings" do + it "encrypts sensible attributes" do + encrypted_settings = subject.encrypted_omniauth_settings + + expect(encrypted_settings["omniauth_settings_facebook_enabled"]).to be(true) + expect( + Decidim::AttributeEncryptor.decrypt(encrypted_settings["omniauth_settings_facebook_app_id"]) + ).to eq(facebook_app_id) + expect( + Decidim::AttributeEncryptor.decrypt(encrypted_settings["omniauth_settings_facebook_app_secret"]) + ).to eq(facebook_app_secret) + end + end + + describe "#set_from" do + it "concatenates from_label and from_email" do + from = subject.set_from + + expect(from).to eq("Decide Gotham ") + end + + context "when from_label is empty" do + let(:from_label) { "" } + + it "returns the email" do + from = subject.set_from + + expect(from).to eq("decide@example.org") + end + end + end + + describe "smtp_settings" do + it "handles SMTP password properly" do + expect(subject.smtp_settings).to eq(smtp_settings.except("password")) + expect(Decidim::AttributeEncryptor.decrypt(subject.encrypted_smtp_settings[:encrypted_password])).to eq(password) + end + + context "when all values are blank" do + let(:smtp_settings) do + { + "address" => "", + "port" => "", + "user_name" => "", + "password" => "", + "from_email" => "", + "from_label" => "" + } + end + + it "returns nil" do + expect(subject.encrypted_smtp_settings).to be_nil + end + end + end + end + + describe "validations" do + describe "organization_admin_email" do + context "when organization_admin_email is blank" do + before { subject.organization_admin_email = "" } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:organization_admin_email]).to include("cannot be blank") + end + end + + context "when organization_admin_email is nil" do + before { subject.organization_admin_email = nil } + + it { is_expected.not_to be_valid } + end + end + + describe "organization_admin_name" do + context "when organization_admin_name is blank" do + before { subject.organization_admin_name = "" } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:organization_admin_name]).to include("cannot be blank") + end + end + + context "when organization_admin_name is nil" do + before { subject.organization_admin_name = nil } + + it { is_expected.not_to be_valid } + end + end + + describe "name" do + context "when name is blank" do + before { subject.name = "" } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:name]).to include("cannot be blank") + end + end + + context "when name is nil" do + before { subject.name = nil } + + it { is_expected.not_to be_valid } + end + end + + describe "short_name" do + context "when short_name is blank" do + before { subject.short_name = "" } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:short_name]).to include("cannot be blank") + end + end + + context "when short_name is nil" do + before { subject.short_name = nil } + + it { is_expected.not_to be_valid } + end + + context "when short_name is too short" do + before { subject.short_name = "AB" } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:short_name]).to include("is too short (under 3 characters)") + end + end + + context "when short_name is too long" do + before { subject.short_name = "A" * 13 } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:short_name]).to include("is too long (maximum is 12 characters)") + end + end + + context "when short_name has minimum valid length" do + before { subject.short_name = "ABC" } + + it { is_expected.to be_valid } + end + + context "when short_name has maximum valid length" do + before { subject.short_name = "A" * 12 } + + it { is_expected.to be_valid } + end + end + + describe "reference_prefix" do + context "when reference_prefix is blank" do + before { subject.reference_prefix = "" } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:reference_prefix]).to include("cannot be blank") + end + end + + context "when reference_prefix is nil" do + before { subject.reference_prefix = nil } + + it { is_expected.not_to be_valid } + end + end + + describe "available_locales" do + context "when available_locales is blank" do + before { subject.available_locales = [] } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:available_locales]).to include("cannot be blank") + end + end + + context "when available_locales is nil" do + before { subject.available_locales = nil } + + it { is_expected.not_to be_valid } + end + end + + describe "default_locale" do + context "when default_locale is blank" do + before { subject.default_locale = "" } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:default_locale]).to include("cannot be blank") + end + end + + context "when default_locale is nil" do + before { subject.default_locale = nil } + + it { is_expected.not_to be_valid } + end + + context "when default_locale is not included in available_locales" do + before do + subject.available_locales = %w(en es) + subject.default_locale = "fr" + end + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:default_locale]).to include("is not included in the list") + end + end + + context "when default_locale is included in available_locales" do + before do + subject.available_locales = %w(en es fr) + subject.default_locale = "fr" + end + + it { is_expected.to be_valid } + end + end + + describe "organization uniqueness" do + let!(:existing_organization) do + create( + :organization, + name: { en: "Existing City", es: "Ciudad Existente" }, + host: "existing.example.org" + ) + end + + context "when organization name already exists (case-insensitive)" do + before { subject.name = "EXISTING CITY" } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:name]).to include("has already been taken") + end + end + + context "when organization name already exists in different locale" do + before { subject.name = "Ciudad Existente" } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:name]).to include("has already been taken") + end + end + + context "when host already exists" do + before { subject.host = "existing.example.org" } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:host]).to include("has already been taken") + end + end + + context "when organization name is unique" do + before { subject.name = "Unique City" } + + it { is_expected.to be_valid } + end + + context "when host is unique" do + before { subject.host = "unique.example.org" } + + it { is_expected.to be_valid } + end + end + + describe "short_name uniqueness" do + let!(:existing_organization) do + create( + :organization, + short_name: { en: "ExistingCity", es: "CiudadExistente" } + ) + end + + context "when organization short_name already exists (case-insensitive)" do + before { subject.short_name = "EXISTINGCITY" } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:short_name]).to include("has already been taken") + end + end + + context "when organization short_name already exists in different locale" do + before { subject.short_name = "CiudadExistente" } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:short_name]).to include("has already been taken") + end + end + + context "when organization short_name is unique" do + before { subject.short_name = "UniqueCity" } + + it { is_expected.to be_valid } + end + end + end + + describe "#map_model" do + subject { described_class.from_model(organization) } + + let(:organization) do + create( + :organization, + secondary_hosts: ["foobar.example.org", "foobaz.example.org"], + omniauth_settings: { + omniauth_settings_facebook_enabled: Decidim::AttributeEncryptor.encrypt(true), + omniauth_settings_facebook_app_id: Decidim::AttributeEncryptor.encrypt("foo") + }, + file_upload_settings: { + allowed_file_extensions: { + default: %w(jpg jpeg), + admin: %w(jpg jpeg png), + image: %w(jpg jpeg png) + }, + allowed_content_types: { + default: %w(image/*), + admin: %w(image/*) + }, + maximum_file_size: { + default: 7.2, + avatar: 2.4 + } + } + ) + end + + it "maps the organization attributes correctly" do + expect(subject.secondary_hosts).to eq(organization.secondary_hosts.join("\n")) + expect(subject.omniauth_settings).to eq( + { + "omniauth_settings_facebook_app_id" => "foo", + "omniauth_settings_facebook_enabled" => true + } + ) + expect(subject.file_upload_settings.final).to eq( + { + allowed_content_types: { "admin" => %w(image/*), "default" => %w(image/*) }, + allowed_file_extensions: { "admin" => %w(jpg jpeg png), "default" => %w(jpg jpeg), "image" => %w(jpg jpeg png) }, + maximum_file_size: { "avatar" => 2.4, "default" => 7.2 } + } + ) + end + end + end +end diff --git a/decidim-system/spec/forms/decidim/system/update_organization_form_spec.rb b/decidim-system/spec/forms/decidim/system/update_organization_form_spec.rb index b4eebaa4b9d70..dd7084e2792f5 100644 --- a/decidim-system/spec/forms/decidim/system/update_organization_form_spec.rb +++ b/decidim-system/spec/forms/decidim/system/update_organization_form_spec.rb @@ -7,6 +7,7 @@ module Decidim::System subject do described_class.new( name: { ca: "", en: "Gotham City", es: "" }, + short_name: { ca: "", en: "GothamCity", es: "" }, host: "decide.example.org", secondary_hosts: "foo.example.org\r\n\r\nbar.example.org", reference_prefix: "JKR", @@ -124,6 +125,470 @@ module Decidim::System end end + describe "validations" do + describe "organization name presence" do + let(:organization) { create(:organization, default_locale: "en") } + + before do + subject.id = organization.id + allow(subject).to receive(:current_organization).and_return(organization) + end + + context "when name in default locale is present" do + before { subject.name = { en: "Gotham City" } } + + it { is_expected.to be_valid } + end + + context "when name in default locale is blank" do + before { subject.name = { en: "" } } + + it { is_expected.not_to be_valid } + + it "adds an error to the default locale name attribute" do + subject.valid? + expect(subject.errors[:name_en]).to include("cannot be blank") + end + end + + context "when name in default locale is nil" do + before { subject.name = { en: nil } } + + it { is_expected.not_to be_valid } + + it "adds an error to the default locale name attribute" do + subject.valid? + expect(subject.errors[:name_en]).to include("cannot be blank") + end + end + + context "when organization has different default locale" do + let(:organization) { create(:organization, default_locale: "es") } + + before do + subject.default_locale = "es" + subject.name = { es: "" } + end + + it { is_expected.not_to be_valid } + + it "adds an error to the correct locale name attribute" do + subject.valid? + expect(subject.errors[:name_es]).to include("cannot be blank") + end + end + + context "when current_organization is not set" do + before do + allow(subject).to receive(:current_organization).and_return(nil) + subject.send(:"name_#{Decidim.default_locale}=", "") + end + + it { is_expected.not_to be_valid } + + it "uses Decidim default locale" do + subject.valid? + expect(subject.errors[:"name_#{Decidim.default_locale}"]).to include("cannot be blank") + end + end + end + + describe "organization short_name presence" do + let(:organization) { create(:organization, default_locale: "en") } + + before do + subject.id = organization.id + allow(subject).to receive(:current_organization).and_return(organization) + end + + context "when short_name in default locale is present" do + before { subject.short_name = { en: "GothamCity" } } + + it { is_expected.to be_valid } + end + + context "when short_name in default locale is blank" do + before { subject.short_name = { en: "" } } + + it { is_expected.not_to be_valid } + + it "adds an error to the default locale short_name attribute" do + subject.valid? + expect(subject.errors[:short_name_en]).to include("cannot be blank") + end + end + + context "when short_name in default locale is nil" do + before { subject.short_name = { en: nil } } + + it { is_expected.not_to be_valid } + + it "adds an error to the default locale short_name attribute" do + subject.valid? + expect(subject.errors[:short_name_en]).to include("cannot be blank") + end + end + + context "when organization has different default locale" do + let(:organization) { create(:organization, default_locale: "es") } + + before do + subject.default_locale = "es" + subject.short_name = { es: "" } + end + + it { is_expected.not_to be_valid } + + it "adds an error to the correct locale short_name attribute" do + subject.valid? + expect(subject.errors[:short_name_es]).to include("cannot be blank") + end + end + end + + describe "short_name format" do + context "when short_name is too short in one locale" do + before { subject.short_name = { en: "AB", es: "ValidName" } } + + it { is_expected.not_to be_valid } + + it "adds an error to the locale with invalid format" do + subject.valid? + expect(subject.errors[:short_name_en]).to include("is too short (under 3 characters)") + end + end + + context "when short_name is too long in one locale" do + before { subject.short_name = { en: "A" * 13, es: "ValidName" } } + + it { is_expected.not_to be_valid } + + it "adds an error to the locale with invalid format" do + subject.valid? + expect(subject.errors[:short_name_en]).to include("is too long (maximum is 12 characters)") + end + end + + context "when short_name is invalid in multiple locales" do + before { subject.short_name = { en: "AB", es: "A" * 13 } } + + it { is_expected.not_to be_valid } + + it "adds errors to all locales with invalid format" do + subject.valid? + expect(subject.errors[:short_name_en]).to include("is too short (under 3 characters)") + expect(subject.errors[:short_name_es]).to include("is too long (maximum is 12 characters)") + end + end + + context "when short_name has minimum valid length" do + before { subject.short_name = { en: "ABC" } } + + it { is_expected.to be_valid } + end + + context "when short_name has maximum valid length" do + before { subject.short_name = { en: "A" * 12 } } + + it { is_expected.to be_valid } + end + + context "when short_name is blank in a locale" do + before { subject.short_name = { en: "ValidName", es: "" } } + + it "does not add format validation errors for blank values" do + subject.valid? + expect(subject.errors[:short_name_es]).not_to include("is too short (under 3 characters)") + end + end + end + + describe "organization uniqueness" do + let!(:existing_organization) do + create( + :organization, + name: { en: "Existing City", es: "Ciudad Existente" }, + host: "existing.example.org" + ) + end + + context "when creating a new organization" do + context "when organization name already exists (case-insensitive)" do + before { subject.name_en = "EXISTING CITY" } + + it { is_expected.not_to be_valid } + + it "adds an error to the name attribute" do + subject.valid? + expect(subject.errors[:name_en]).to include("has already been taken") + end + end + + context "when organization name already exists in different locale" do + before { subject.name_en = "Ciudad Existente" } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:name_en]).to include("has already been taken") + end + end + + context "when multiple locale names conflict" do + before do + subject.name_en = "Existing City" + subject.name_es = "Ciudad Existente" + end + + it { is_expected.not_to be_valid } + + it "adds errors to both locale attributes" do + subject.valid? + expect(subject.errors[:name_en]).to include("has already been taken") + expect(subject.errors[:name_es]).to include("has already been taken") + end + end + + context "when host already exists" do + before { subject.host = "existing.example.org" } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:host]).to include("has already been taken") + end + end + + context "when organization name is unique" do + before { subject.name_en = "Unique City" } + + it { is_expected.to be_valid } + end + + context "when host is unique" do + before { subject.host = "unique.example.org" } + + it { is_expected.to be_valid } + end + end + + context "when updating an existing organization" do + let(:organization_to_update) do + create( + :organization, + name: { en: "My City", es: "Mi Ciudad" }, + host: "mycity.example.org" + ) + end + + before do + subject.id = organization_to_update.id + end + + context "when keeping the same name" do + before { subject.name_en = "My City" } + + it { is_expected.to be_valid } + end + + context "when keeping the same host" do + before { subject.host = "mycity.example.org" } + + it { is_expected.to be_valid } + end + + context "when changing name to an existing one" do + before { subject.name_en = "Existing City" } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:name_en]).to include("has already been taken") + end + end + + context "when changing host to an existing one" do + before { subject.host = "existing.example.org" } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:host]).to include("has already been taken") + end + end + + context "when changing name to a unique one" do + before { subject.name_en = "Brand New City" } + + it { is_expected.to be_valid } + end + + context "when changing host to a unique one" do + before { subject.host = "other.example.org" } + + it { is_expected.to be_valid } + end + end + + context "when name contains machine_translations" do + let!(:org_with_translations) do + create( + :organization, + name: { + :en => "City", + "machine_translations" => { fr: "Ville" } + } + ) + end + + context "when new name conflicts with machine translation" do + before { subject.name_en = "Ville" } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:name_en]).to include("has already been taken") + end + end + end + + context "when name value is a Hash (nested structure)" do + before do + allow(subject).to receive(:name).and_return({ en: { nested: "value" }, es: "Valid Name" }) + end + + it "skips Hash values during validation" do + expect { subject.valid? }.not_to raise_error + end + end + end + + describe "short_name uniqueness" do + let!(:existing_organization) do + create( + :organization, + short_name: { en: "ExistingCity", es: "CiudadExistente" } + ) + end + + context "when creating a new organization" do + context "when organization short_name already exists (case-insensitive)" do + before { subject.short_name = { en: "EXISTINGCITY" } } + + it { is_expected.not_to be_valid } + + it "adds an error to the short_name attribute" do + subject.valid? + expect(subject.errors[:short_name_en]).to include("has already been taken") + end + end + + context "when organization short_name already exists in different locale" do + before { subject.short_name = { en: "CiudadExistente" } } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:short_name_en]).to include("has already been taken") + end + end + + context "when multiple locale short_names conflict" do + before { subject.short_name = { en: "ExistingCity", es: "CiudadExistente" } } + + it { is_expected.not_to be_valid } + + it "adds errors to both locale attributes" do + subject.valid? + expect(subject.errors[:short_name_en]).to include("has already been taken") + expect(subject.errors[:short_name_es]).to include("has already been taken") + end + end + + context "when organization short_name is unique" do + before { subject.short_name = { en: "UniqueCity" } } + + it { is_expected.to be_valid } + end + end + + context "when updating an existing organization" do + let(:organization_to_update) do + create( + :organization, + short_name: { en: "MyCity", es: "MiCiudad" } + ) + end + + before do + subject.id = organization_to_update.id + end + + context "when keeping the same short_name" do + before { subject.short_name = { en: "My City" } } + + it { is_expected.to be_valid } + end + + context "when changing short_name to an existing one" do + before { subject.short_name = { en: "ExistingCity" } } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:short_name_en]).to include("has already been taken") + end + end + + context "when changing short_name to a unique one" do + before { subject.short_name = { en: "BrandNewCity" } } + + it { is_expected.to be_valid } + end + end + + context "when short_name contains machine_translations" do + let!(:org_with_translations) do + create( + :organization, + short_name: { + :en => "City", + "machine_translations" => { fr: "Ville" } + } + ) + end + + context "when new short_name conflicts with machine translation" do + before { subject.short_name = { en: "Ville" } } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:short_name_en]).to include("has already been taken") + end + end + end + + context "when short_name value is a Hash (nested structure)" do + before do + allow(subject).to receive(:short_name).and_return({ en: { nested: "value" }, es: "ValidShortName" }) + end + + it "skips Hash values during validation" do + expect { subject.valid? }.not_to raise_error + end + end + end + end + describe "#map_model" do subject { described_class.from_model(organization) } diff --git a/decidim-system/spec/system/organizations_spec.rb b/decidim-system/spec/system/organizations_spec.rb index 38797e01ef510..c3e7c77b4d7da 100644 --- a/decidim-system/spec/system/organizations_spec.rb +++ b/decidim-system/spec/system/organizations_spec.rb @@ -42,6 +42,7 @@ it "creates a new organization" do fill_in "Name", with: "Citizen Corp" + fill_in "Short name", with: "CitizenCorp" fill_in "Host", with: "www.example.org" fill_in "Secondary hosts", with: "foo.example.org\n\rbar.example.org" fill_in "Reference prefix", with: "CCORP" @@ -78,6 +79,7 @@ it "does not create an organization" do fill_in "Name", with: "Citizen Corp" + fill_in "Short name", with: "CitizenCorp" fill_in "Host", with: "www.example.org" fill_in "Reference prefix", with: "CCORP" click_on "Create organization & invite admin" @@ -95,6 +97,7 @@ it "does not create an organization" do fill_in "Name", with: "Citizen Corp 2" + fill_in "Short name", with: "CitizenCorp2" fill_in "Reference prefix", with: "CCORP" fill_in "Organization admin name", with: "system@example.org" From 24e19c2c522c403f69acf0c9f9882bfbd5606b3a Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Mon, 15 Dec 2025 16:45:55 +0200 Subject: [PATCH 019/116] Add AWS Bucket configuration for public assets (#15777) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add AWS Bucket configuration for public assets * Fix typo * Apply suggestions from code review Co-authored-by: Andrés Pereira de Lucena Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update docs/modules/services/pages/activestorage.adoc * Apply suggestions from code review Co-authored-by: Andrés Pereira de Lucena * Update docs/modules/services/pages/activestorage.adoc Co-authored-by: Andrés Pereira de Lucena --------- Co-authored-by: Andrés Pereira de Lucena Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../modules/services/pages/activestorage.adoc | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docs/modules/services/pages/activestorage.adoc b/docs/modules/services/pages/activestorage.adoc index 6b75c6099d3c8..abc4f36388742 100644 --- a/docs/modules/services/pages/activestorage.adoc +++ b/docs/modules/services/pages/activestorage.adoc @@ -47,6 +47,46 @@ Locate the bucket, go into the "Permissions" tab and find the section titled "CO Read more at https://docs.aws.amazon.com/AmazonS3/latest/userguide/cors.html[Amazon S3 CORS documentation]. +==== Public assets + +To have public assets in your application, so that you do not rely on the ActiveStorage redirect system, you need to configure your bucket as follows: + +1. Go to your AWS S3 console +2. Select the bucket you are using for uploads +3. Open the *Permissions* tab +4. In the *Block public access* section, click *Edit* +5. Disable all blocking options by *unchecking* every box: + - "Block all public access" + - "Block public access to buckets and objects granted through new access control lists (ACLs)" + - "Block public access to buckets and objects granted through any access control lists (ACLs)" + - "Block public access to buckets and objects granted through new public bucket or access point policies" + - "Block public and cross-account access to buckets and objects through any public bucket or access point policies" +6. Click *Save changes* +7. Still in the *Permissions tab*, locate the *Bucket policy section* and click *Edit*. +8. Add a bucket policy similar to the example below. + - If you are unsure of your bucket’s ARN, you can find it in the Properties tab. For this example, we use `arn:aws:s3:::your-bucket-name` +9. Click *Save changes* +[source,json] +---- +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "Statement1", + "Effect": "Allow", + "Principal": "*", + "Action": "s3:GetObject", + "Resource": "arn:aws:s3:::your-bucket-name/*" + } + ] +} +---- + +[NOTE] +==== +If you use any other provider than the default (`local`) you will need to also configure the xref:customize:content_security_policy.adoc[Content security policy]. For the directives "img-src", "media-src", and "connect-src" adding some additional content like https://$YOUR-BUCKET-NAME.s3.$YOUR-AWS-REGION.amazonaws.com/* (should look like: https://your-bucket-name.s3.eu-west-1.amazonaws.com) +==== + === Google Cloud Storage Google Cloud Storage requires you to use the `gsutil` command line tool to set the CORS policy on your bucket. First you need to know the name of your bucket and then use the following command (replace `your-bucket-name` with the actual name of the bucket): From 0623de48173b332e2d6d871297b771c74f9b8fb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Mon, 15 Dec 2025 20:17:02 +0100 Subject: [PATCH 020/116] Extract documentation from /api to docs.decidim.org (#15804) --- decidim-api/docs/usage.md | 635 +----------------- .../develop/pages/api/authentication.adoc | 95 +++ .../develop/pages/api/core-concepts.adoc | 535 +++++++++++++++ docs/modules/develop/pages/api/index.adoc | 51 +- .../components/proposals.adoc} | 0 .../develop/partials/api/decidim_version.adoc | 102 +++ 6 files changed, 778 insertions(+), 640 deletions(-) create mode 100644 docs/modules/develop/pages/api/authentication.adoc create mode 100644 docs/modules/develop/pages/api/core-concepts.adoc rename docs/modules/develop/pages/api/{proposals/mutations.adoc => reference/components/proposals.adoc} (100%) create mode 100644 docs/modules/develop/partials/api/decidim_version.adoc diff --git a/decidim-api/docs/usage.md b/decidim-api/docs/usage.md index f6ee207d54bbe..2a85a9acdeee5 100644 --- a/decidim-api/docs/usage.md +++ b/decidim-api/docs/usage.md @@ -52,637 +52,4 @@ Response (formatted) should look something like this: } ``` -The most practical way to experiment with GraphQL, however, is just to use the in-browser IDE GraphiQL. It provides access to the documentation and auto-complete (use CTRL-Space) for writing queries. - -From now on, we will skip the "query" keyword for the purpose of readability. You can skip it too if you are using GraphiQL, if you are querying directly (by using CURL for instance) you will need to include it. - -### Signing in to the API - -In case you want to use the API as a sign in user to perform mutations representing a user in Decidim, you have two available options for such integrations through the system administration panel: - -1. Creating an OAuth application and implementing the OAuth authentication flow for the users of your application. Use this option for participant-facing applications where the participants represent themselves in Decidim through the API. -2. Creating API credentials and signing in to the API with these credentials to perform the operations as a signed in machine user. Use this option for machine-to-machine automations where there is no real end user interacting with Decidim. - -If you only want to test the GraphQL queries as a signed in user, you can use the normal Decidim authentication functionality to sign in and then use the GraphiQL IDE to perform these queries as a signed in user. - -#### OAuth flow for participant-facing applications - -Participant-facing applications where the participants need to interact with Decidim through GraphQL mutations can be integrated using OAuth applications. In order to configure such integration capability from the system administration panel, create a new OAuth application and provide the necessary details for your integration. Note that the "application type" for such applications would typically be "Public". For more information regarding the application types, refer to [RFC 6749 Section 2.1. (OAuth client types)](https://datatracker.ietf.org/doc/html/rfc6749#section-2.1). - -In order to use the OAuth access tokens to represent the user through the API, please select the following scopes as "Available" scopes for the application: - -* `user` - Authenticated users have the ability to represent a logged in user in Decidim -* `api:read` - Authenticated users have the ability to read data from the API -* `api:write` - Authenticated users have the ability to write data through the API (in case your external application needs to perform mutations over the API on behalf of the user) - -Once configured, you can now use any OAuth authentication library to perform the OAuth authentication flow with your application users and receive an access token to utilize the Decidim API representing the signed in user. Please note that with public OAuth clients especially (and recommended also for confidential clients), you have to use [PKCE](https://datatracker.ietf.org/doc/html/rfc7636) with the authorization flow. - -Once the OAuth application is created, you can authenticate against it with the following steps: - -1. Send the user to perform an OAuth authorization request at Decidim with the required API scopes (`user`, `api:read` and `api:write` if you want to perform mutations over the API). Along with the authorization request, also send the additional parameters required by PKCE (`code_challenge` and `code_challenge_method`). -2. Receive an OAuth authorization code back to your application's configured redirect URI. -3. Utilizing the received authorization code, request an OAuth access token from the OAuth token endpoint. Along with the token request, also send the additional parameter required by PKCE `code_verifier`. -4. The issued token is a JSON Web Token (JWT) when the authorization request contains the defined scopes. This token can be now used to represent the user in further calls to the API by passing the token with its type (`Bearer`) within the HTTP Authorization header with the request to the API. - -When doing the requests to the API, you also need to pass the OAuth client ID within the `X-Jwt-Aud` header of the requests in order for the token to be recognized as a valid token for the issued client. Passing the bearer token to the `Authorization` header and the OAuth client ID to the `X-Jwt-Aud` header, you can send the following HTTP request to the API to validate that the token works and the user is recognized as signed in: - -```http -POST /api HTTP/1.1 -Accept: application/json -Authorization: Bearer token -Content-Length: 53 -Content-Type: application/json -Host: DOMAIN -X-Jwt-Aud: OAUTH_CLIENT_ID - -{"query":"{ session { user { id name nickname } } }"} -``` - -You should see the user details in the response in case the token is valid and you have configured the API correctly. If the response does not contain the user details, please refer to the Decidim configuration documentation. - -Once the interaction with the API is completed, it is recommended to revoke the tokens, which is similar to the user signing out of the application. This can be done utilizing the OAuth revocation endpoint provided by Decidim. After the token is revoked, it is no longer valid and the user has to perform a re-authorization the next time they want to utilize the API. - -In case you need tokens with a longer life span, you can either look into the Decidim documentation to extend the validity period of the access tokens or enable refresh tokens for the OAuth application when configuring it. However, note that tokens with longer lifespan can weaken the security of your system and make your application users vulnerable to security threats. Such use cases should be carefully planned and the security concerns should be addressed seriously. - -#### API credentials flow for machine-to-machine automations - -The API credentials represent an administrative user in Decidim that performs administrative tasks on behalf of the end users. This type of integration flows should never live on devices that the participants have access to. These types of integrations are meant for different types of automations, such as transferring proposal answers or meeting reports back to Decidim from an external system automatically, e.g. once a day. - -Note that these credentials are highly sensitive and have elevated permissions, so take good care of the system security where you are planning to store these credentials. If these credentials end up in participants' hands, the whole system is compromised and no longer secure. You should always primarily create OAuth integrations where the end users will manually perform the authorization for the application to perform actions on behalf of them. - -Once you have validated that this is the correct way for your integration to operate, you can create the API credentials from the system administration panel. You will receive an API key and API secret after creating the credentials. These credentials should be also manually rotated on a regular basis to prevent unauthorized access to the system with these credentials in case they are leaked. The credentials have to be manually rotated in order to prevent external applications breaking because they cannot rotate the credentials themselves and they are typically statically configured for these applications. - -Given you have issued the API key and API secret, you can now send a sign in request to the API using these credentials as follows: - -```bash -curl -s -i -H "Content-type: application/x-www-form-urlencoded" \ - -d "api_user[key]=PASTE_API_KEY_HERE" \ - -d "api_user[secret]=PASTE_API_SECRET_HERE" \ - -X POST https://DOMAIN/api/sign_in | grep 'Authorization' | cut -d ' ' -f2- -``` - -After running this command, you should see the following string in the console, where `token` is replaced with the access token: - -```bash -Bearer token -``` - -This string is passed to the following requests within the HTTP `Authorization` header to represent the user during API calls. You can use the following example query to test it out and confirm that signing in works as expected: - -```bash -curl -w "\n" -H "Content-Type: application/json" \ - -H "Authorization: Bearer token" \ - -d '{"query":"{ session { user { id name nickname } } }"}' \ - -X POST https://DOMAIN/api -``` - -You should see the user details in the response in case the token is valid and you have configured the API correctly. If the response does not contain the user details, please refer to the Decidim configuration documentation. - -Once the API interaction is done, you should always make an HTTP DELETE request to `/api/sign_out` with the same token in order to revoke the token from further access as follows: - -```bash -curl -s -o /dev/null -w "HTTP %{http_code}\n" \ - -H "Authorization: Bearer token" \ - -X DELETE http://DOMAIN/api/sign_out -``` - -### Usage limits - -Decidim is just a Rails application, meaning that any particular installation may implement custom limits in order to access the API (and the application in general). - -By default (particular installations may change that), API uses the same limitations as the whole Decidim website, provided by the Gem [Rack::Attack](https://github.com/kickstarter/rack-attack). These are 100 maximum requests per minute per IP to prevent DoS attacks - -### Decidim structure, Types, collections and Polymorphism - -There are no endpoints in the GraphQL specification, instead objects are organized according to their "Type". - -These objects can be grouped in a single, complex query. Also, objects may accept parameters, which are "Types" as well. - -Each "Type" is just a pre-defined structure with fields, or just an Scalar (Strings, Integers, Booleans, ...). - -For instance, to obtain *all the participatory processes in a Decidim installation published since January 2018* and order them by published date, we could execute the next query: - -```graphql -{ - participatoryProcesses(filter: {publishedSince: "2018-01-01"}, order: {publishedAt: "asc"}) { - slug - title { - translation(locale: "en") - } - } -} -``` - -Response should look like: - -```json -{ - "data": { - "participatoryProcesses": [ - { - "slug": "consectetur-at", - "title": { - "translation": "Soluta consectetur quos fugit aut." - } - }, - { - "slug": "nostrum-earum", - "title": { - "translation": "Porro hic ipsam cupiditate reiciendis." - } - } - ] - } -} -``` - -#### What happened? - -In the former query, each keyword represents a type, the words `publishedSince`, `publishedAt`, `slug`, `locale` are scalars, all of them Strings. - -The other keywords however, are objects representing certain entities: - -* `participatoryProcesses` is a type that represents a collection of participatory spaces. It accepts arguments (`filter` and `order`), which are other object types as well. `slug` and `title` are the fields of the participatory process we are interested in, there are "Types" too. -* `filter` is a [ParticipatoryProcessFilter](#ParticipatoryProcessFilter)\* input type, it has several properties that allows us to refine our search. One of them is the `publishedSince` property with the initial date from which to list entries. -* `order` is a [ParticipatoryProcessSort](#ParticipatoryProcessSort) type, works the same way as the filter but with the goal of ordering the results. -* `title` is a [TranslatedField](#TranslatedField) type, which allows us to deal with multi-language fields. - -Finally, note that the returned object is an array, each item of which is a representation of the object we requested. - -> \***About how filters and sorting are organized** -> -> There are two types of objects to filter and ordering collections in Decidim, they all work in a similar fashion. The type involved in filtering always have the suffix "Filter", for ordering it has the suffix "Sort". -> -> The types used to filter participatory spaces are: [ParticipatoryProcessFilter](#ParticipatoryProcessFilter), [AssemblyFilter](#AssemblyFilter), and so on. -> -> Other collections (or connections) may have their own filters (i.e. [ComponentFilter](#ComponentFilter)). -> -> Each filter has its own properties, you should check any object in particular for details. The way they work with multi-languages fields, however, is the same: -> -> We can say we have some searchable object with a multi-language field called *title*, and we have a filter that allows us to search through this field. How should it work? Should we look up content for every language in the field? or should we stick to a specific language? -> -> In our case, we have decided to search only one particular language of a multi-language field but we let you choose which language to search. -> If no language is specified, the configured as default in the organization will be used. The keyword to specify the language is `locale`, and it should be provided in the 2 letters ISO 639-1 format (en = English, es = Spanish, ...). -> -> Example (this is not a real Decidim query): -> -> ```graphql -> some_collection(filter: { locale: "en", title: "ideas"}) { -> id -> } -> ``` -> -> The same applies to sorting ([ParticipatoryProcessSort](#ParticipatoryProcessSort), [AssemblySort](#AssemblySort), etc.) -> -> In this case, the content of the field (*title*) only allows 2 values: *ASC* and *DESC*. -> -> Example of ordering alphabetically by the title content in French language: -> -> ```graphql -> some_collection(order: { locale: "en", title: "asc"}) { -> id -> } -> ``` -> -> Of course, you can combine both filter and order. Also remember to check availability of this type of behaviour for any particular filter/sort. - -#### Decidim main types - -Decidim has 2 main types of objects through which content is provided. These are Participatory Spaces and Components. - -A participatory space is the first level, currently there are 5 officially supported: *Participatory Processes*, *Assemblies*, *Conferences* and *Initiatives*. For each participatory process there will correspond a collection type and a "single item" type. - -The previous example uses the collection type for participatory processes. You can try `assemblies`, `conferences`, or `initiatives` for the others. Note that each collection can implement their own filter and order types with different properties. - -As an example for a single item query, you can run: - -```graphql -{ - participatoryProcess(slug: "consectetur-at") { - slug - title { - translation(locale: "en") - } - } -} -``` - -And the response will be: - -```json -{ - "data": { - "participatoryProcess": { - "slug": "consectetur-at", - "title": { - "translation": "Soluta consectetur quos fugit aut." - } - } - } -} -``` - -#### What is different? - -First, note that we are querying, in singular, the type `participatoryProcess`, with a different parameter, `slug`\*, (a String). We can use the `id` instead if we know it. - -Second, the response is not an Array, it is just the object we requested. We can expect to return `null` if the object is not found. - -> \* The `slug` is a convenient way to find a participatory space as is (usually) in the URL. -> -> For instance, consider this real case from Barcelona: -> -> https://www.decidim.barcelona/processes/patrimonigracia -> -> The word `patrimonigracia` indicates the "slug". - -#### Components - -Every participatory space may (and should) have some components. There are 9 official components, these are `Proposals`, `Page`, `Meetings`, `Budgets`, `Surveys`, `Accountability`, `Debates` and `Blog`. Plugins may add their own components. - -If you know the `id`\* of a specific component you can obtain it by querying it directly: - -```graphql -{ - component(id:2) { - id - name { - translation(locale:"en") - } - __typename - participatorySpace { - id - type - } - } -} -``` - -Response: - -```json -{ - "data": { - "component": { - "id": "2", - "name": { - "translation": "Meetings" - }, - "__typename": "Meetings", - "participatorySpace": { - "id": "1", - "type": "Decidim::ParticipatoryProcess" - } - } - } -} -``` - -The process is analogue as what has been explained in the case of searching for one specific participatory process. - -> \*Note that the `id` of a component is present also in the URL after the letter "f": -> -> https://www.decidim.barcelona/processes/patrimonigracia/f/3257/ -> -> In this case, 3257. - -##### What about component's collections? - -Glad you asked, component's collections cannot be retrieved directly, the are available *in the context* of a participatory space. - -For instance, we can query all the components in an particular Assembly as follows: - -```graphql -{ - assembly(id: 3) { - components { - id - name { - translation(locale: "en") - } - __typename - } - } -} -``` - -The response will be similar to: - -```json -{ - "data": { - "assembly": { - "components": [ - { - "id": "42", - "name": { - "translation": "Accountability" - }, - "__typename": "Component" - }, - { - "id": "38", - "name": { - "translation": "Meetings" - }, - "__typename": "Meetings" - }, - { - "id": "37", - "name": { - "translation": "Page" - }, - "__typename": "Pages" - }, - { - "id": "39", - "name": { - "translation": "Proposals" - }, - "__typename": "Proposals" - } - ] - } - } -} -``` - -We can also apply some filters by using the [ComponentFilter](#ComponentFilter) type. In the next query we would like to *find all the components with geolocation enabled in the assembly with id=2*: - -```graphql -{ - assembly(id: 2) { - components(filter: {withGeolocationEnabled: true}) { - id - name { - translation(locale: "en") - } - __typename - } - } -} -``` - -The response: - -```json -{ - "data": { - "assembly": { - "components": [ - { - "id": "39", - "name": { - "translation": "Meetings" - }, - "__typename": "Meetings" - } - ] - } - } -} -``` - -Note that, in this case, there is only one component returned, "Meetings". In some cases Proposals can be geolocated too therefore would be returned in this query. - -### Polymorphism and connections - -Many relationships between tables in Decidim are polymorphic, this means that the related object can belong to different classes and share just a few properties in common. - -For instance, components in a participatory space are polymorphic, while the concept of component is generic and all of them share properties like *published date*, *name* or *weight*, they differ in the rest. *Proposals* have the *status* field while *Meetings* have an *agenda*. - -Another example are the case of linked resources, these are properties that may link objects of different nature between components or participatory spaces. - -In a very simplified way (to know more please refer to the official guide), GraphQL polymorphism is handled through the operator `... on`. You will know when a field is polymorphic because the property `__typename`, which tells you the type of that particular object, will change accordingly. - -In the previous examples we have queried for this property: - -Response fragment: - -```json - "components": [ - { - "id": "38", - "name": { - "translation": "Meetings" - }, - "__typename": "Meetings" - } -``` - -So, if we want to access the rest of the properties in a polymorphic object, we should do it through the `... on` operator as follows: - -```graphql -{ - assembly(id: 2) { - components { - id - ... on Proposals { - - } - } - } -} -``` - -Consider this query: - -```graphql -{ - assembly(id: 3) { - components(filter: {type: "Proposals"}) { - id - name { - translation(locale: "en") - } - ... on Proposals { - proposals(order: {likeCount: "desc"}, first: 2) { - edges { - node { - id - likes { - name - } - } - } - } - } - } - } -} -``` - -The response: - -```json -{ - "data": { - "assembly": { - "components": [ - { - "id": "39", - "name": { - "translation": "Proposals" - }, - "proposals": { - "edges": [ - { - "node": { - "id": "35", - "likes": [ - { - "name": "Ms. Johnathon Schaefer" - }, - { - "name": "Linwood Lakin PhD 3 4 endr1" - }, - { - "name": "Gracie Emmerich" - }, - { - "name": "Randall Rath 3 4 endr3" - }, - { - "name": "Jolene Schmitt MD" - }, - { - "name": "Clarence Hammes IV 3 4 endr5" - }, - { - "name": "Omar Mayer" - }, - { - "name": "Raymundo Jaskolski 3 4 endr7" - } - ] - } - }, - { - "node": { - "id": "33", - "likes": [ - { - "name": "Spring Brakus" - }, - { - "name": "Reiko Simonis IV 3 2 endr1" - }, - { - "name": "Dr. Jim Denesik" - }, - { - "name": "Dr. Mack Schoen 3 2 endr3" - } - ] - } - } - ] - } - } - ] - } - } -} -``` - -#### What is going on? - -Until the `... on Proposals` line, there is nothing new. We are requesting the *Assembly* participatory space identified by the `id=3`, then listing all its components with the type "Proposals". All the components share the *id* and *name* properties, so we can just add them at the query. - -After that, we want content specific from the *Proposals* type. In order to do that we must tell the server that the content we will request shall only be executed if the types matches *Proposals*. We do that by wrapping the rest of the query in the `... on Proposals` clause. - -The next line is just a property of the type *Proposals* which is a type of collection called a "connection". A connection works similar as normal collection (such as *components*) but it can handle more complex cases. - -Typically, a connection is used to paginate long results, for this purpose the results are not directly available but encapsulated inside the list *edges* in several *node* results. Also there are more arguments available in order to navigate between pages. This are the arguments: - -* `first`: Returns the first *n* elements from the list -* `after`: Returns the elements in the list that come after the specified *cursor* -* `last`: Returns the last *n* elements from the list -* `before`: Returns the elements in the list that come before the specified *cursor* - -Example: - -```graphql -{ - assembly(id: 3) { - components(filter: {type: "Proposals"}) { - id - name { - translation(locale: "en") - } - ... on Proposals { - proposals(first:2,after:"Mg") { - pageInfo { - endCursor - startCursor - hasPreviousPage - hasNextPage - } - edges { - node { - id - likes { - name - } - } - } - } - } - } - } -} -``` - -Being the response: - -```json -{ - "data": { - "assembly": { - "components": [ - { - "id": "39", - "name": { - "translation": "Proposals" - }, - "proposals": { - "pageInfo": { - "endCursor": "NA", - "startCursor": "Mw", - "hasPreviousPage": false, - "hasNextPage": true - }, - "edges": [ - { - "node": { - "id": "32", - "likes": [] - } - }, - { - "node": { - "id": "31", - "likes": [ - { - "name": "Mr. Nicolas Raynor" - }, - { - "name": "Gerry Fritsch PhD 3 1 endr1" - } - ] - } - } - ] - } - } - ] - } - } -} -``` - -As you can see, a part from the *edges* list, you can access to the object *pageInfo* which gives you the information needed to navigate through the different pages. - -For more info on how connections work, you can check the official guide: - -https://graphql.org/learn/pagination/ +For additional examples of queries and mutations, check the additional [GraphQL API documentation](https://docs.decidim.org/en/develop/develop/api/index.html) of Decidim. diff --git a/docs/modules/develop/pages/api/authentication.adoc b/docs/modules/develop/pages/api/authentication.adoc new file mode 100644 index 0000000000000..21235ebd5aebf --- /dev/null +++ b/docs/modules/develop/pages/api/authentication.adoc @@ -0,0 +1,95 @@ += Authentication with the API + +By default, the GraphQL API in Decidim is publically available for read-only operations and it can also be used by external applications to read data from Decidim. If you want to write data over the API, i.e. perform GraphQL mutations, you need first need to authenticate the API user to perform these operations. Otherwise, it is not possible to perform such operations as most such operations are performed as an actual user in Decidim. + +More information regarding implementing the API authentication is available in the API documentation of your Decidim instance. + +== Signing in to the API + +In case you want to use the API as a sign in user to perform mutations representing a user in Decidim, you have two available options for such integrations through the system administration panel: + +1. Creating an OAuth application and implementing the OAuth authentication flow for the users of your application. Use this option for participant-facing applications where the participants represent themselves in Decidim through the API. +2. Creating API credentials and signing in to the API with these credentials to perform the operations as a signed in machine user. Use this option for machine-to-machine automations where there is no real end user interacting with Decidim. + +If you only want to test the GraphQL queries as a signed in user, you can use the normal Decidim authentication functionality to sign in and then use the GraphiQL IDE to perform these queries as a signed in user. + +== OAuth flow for participant-facing applications + +Participant-facing applications where the participants need to interact with Decidim through GraphQL mutations can be integrated using OAuth applications. In order to configure such integration capability from the system administration panel, create a new OAuth application and provide the necessary details for your integration. Note that the "application type" for such applications would typically be "Public". For more information regarding the application types, refer to https://datatracker.ietf.org/doc/html/rfc6749#section-2.1[RFC 6749 Section 2.1. (OAuth client types)]. + +In order to use the OAuth access tokens to represent the user through the API, please select the following scopes as "Available" scopes for the application: + +* `user` - Authenticated users have the ability to represent a logged in user in Decidim +* `api:read` - Authenticated users have the ability to read data from the API +* `api:write` - Authenticated users have the ability to write data through the API (in case your external application needs to perform mutations over the API on behalf of the user) + +Once configured, you can now use any OAuth authentication library to perform the OAuth authentication flow with your application users and receive an access token to utilize the Decidim API representing the signed in user. Please note that with public OAuth clients especially (and recommended also for confidential clients), you have to use https://datatracker.ietf.org/doc/html/rfc7636[PKCE] with the authorization flow. + +Once the OAuth application is created, you can authenticate against it with the following steps: + +1. Send the user to perform an OAuth authorization request at Decidim with the required API scopes (`user`, `api:read` and `api:write` if you want to perform mutations over the API). Along with the authorization request, also send the additional parameters required by PKCE (`code_challenge` and `code_challenge_method`). +2. Receive an OAuth authorization code back to your application's configured redirect URI. +3. Utilizing the received authorization code, request an OAuth access token from the OAuth token endpoint. Along with the token request, also send the additional parameter required by PKCE `code_verifier`. +4. The issued token is a JSON Web Token (JWT) when the authorization request contains the defined scopes. This token can be now used to represent the user in further calls to the API by passing the token with its type (`Bearer`) within the HTTP Authorization header with the request to the API. + +When doing the requests to the API, you also need to pass the OAuth client ID within the `X-Jwt-Aud` header of the requests in order for the token to be recognized as a valid token for the issued client. Passing the bearer token to the `Authorization` header and the OAuth client ID to the `X-Jwt-Aud` header, you can send the following HTTP request to the API to validate that the token works and the user is recognized as signed in: + +```http +POST /api HTTP/1.1 +Accept: application/json +Authorization: Bearer token +Content-Length: 53 +Content-Type: application/json +Host: DOMAIN +X-Jwt-Aud: OAUTH_CLIENT_ID + +{"query":"{ session { user { id name nickname } } }"} +``` + +You should see the user details in the response in case the token is valid and you have configured the API correctly. If the response does not contain the user details, please refer to the Decidim configuration documentation. + +Once the interaction with the API is completed, it is recommended to revoke the tokens, which is similar to the user signing out of the application. This can be done utilizing the OAuth revocation endpoint provided by Decidim. After the token is revoked, it is no longer valid and the user has to perform a re-authorization the next time they want to utilize the API. + +In case you need tokens with a longer life span, you can either look into the Decidim documentation to extend the validity period of the access tokens or enable refresh tokens for the OAuth application when configuring it. However, note that tokens with longer lifespan can weaken the security of your system and make your application users vulnerable to security threats. Such use cases should be carefully planned and the security concerns should be addressed seriously. + +== API credentials flow for machine-to-machine automations + +The API credentials represent an administrative user in Decidim that performs administrative tasks on behalf of the end users. This type of integration flows should never live on devices that the participants have access to. These types of integrations are meant for different types of automations, such as transferring proposal answers or meeting reports back to Decidim from an external system automatically, e.g. once a day. + +Note that these credentials are highly sensitive and have elevated permissions, so take good care of the system security where you are planning to store these credentials. If these credentials end up in participants' hands, the whole system is compromised and no longer secure. You should always primarily create OAuth integrations where the end users will manually perform the authorization for the application to perform actions on behalf of them. + +Once you have validated that this is the correct way for your integration to operate, you can create the API credentials from the system administration panel. You will receive an API key and API secret after creating the credentials. These credentials should be also manually rotated on a regular basis to prevent unauthorized access to the system with these credentials in case they are leaked. The credentials have to be manually rotated in order to prevent external applications breaking because they cannot rotate the credentials themselves and they are typically statically configured for these applications. + +Given you have issued the API key and API secret, you can now send a sign in request to the API using these credentials as follows: + +```bash +curl -s -i -H "Content-type: application/x-www-form-urlencoded" \ + -d "api_user[key]=PASTE_API_KEY_HERE" \ + -d "api_user[secret]=PASTE_API_SECRET_HERE" \ + -X POST https://DOMAIN/api/sign_in | grep 'Authorization' | cut -d ' ' -f2- +``` + +After running this command, you should see the following string in the console, where `token` is replaced with the access token: + +```bash +Bearer token +``` + +This string is passed to the following requests within the HTTP `Authorization` header to represent the user during API calls. You can use the following example query to test it out and confirm that signing in works as expected: + +```bash +curl -w "\n" -H "Content-Type: application/json" \ + -H "Authorization: Bearer token" \ + -d '{"query":"{ session { user { id name nickname } } }"}' \ + -X POST https://DOMAIN/api +``` + +You should see the user details in the response in case the token is valid and you have configured the API correctly. If the response does not contain the user details, please refer to the Decidim configuration documentation. + +Once the API interaction is done, you should always make an HTTP DELETE request to `/api/sign_out` with the same token in order to revoke the token from further access as follows: + +```bash +curl -s -o /dev/null -w "HTTP %{http_code}\n" \ + -H "Authorization: Bearer token" \ + -X DELETE http://DOMAIN/api/sign_out +``` diff --git a/docs/modules/develop/pages/api/core-concepts.adoc b/docs/modules/develop/pages/api/core-concepts.adoc new file mode 100644 index 0000000000000..524cd362c84d4 --- /dev/null +++ b/docs/modules/develop/pages/api/core-concepts.adoc @@ -0,0 +1,535 @@ += Core concepts in the API: Decidim structure, Types, collections and Polymorphism + +There are no endpoints in the GraphQL specification, instead objects are organized according to their "Type". + +These objects can be grouped in a single, complex query. Also, objects may accept parameters, which are "Types" as well. + +Each "Type" is just a pre-defined structure with fields, or just an Scalar (Strings, Integers, Booleans, ...). + +For instance, to obtain *all the participatory processes in a Decidim installation published since January 2018* and order them by published date, we could execute the next query: + +```graphql +{ + participatoryProcesses(filter: {publishedSince: "2018-01-01"}, order: {publishedAt: "asc"}) { + slug + title { + translation(locale: "en") + } + } +} +``` + +Response should look like: + +```json +{ + "data": { + "participatoryProcesses": [ + { + "slug": "consectetur-at", + "title": { + "translation": "Soluta consectetur quos fugit aut." + } + }, + { + "slug": "nostrum-earum", + "title": { + "translation": "Porro hic ipsam cupiditate reiciendis." + } + } + ] + } +} +``` + +== What happened? + +In the former query, each keyword represents a type, the words `publishedSince`, `publishedAt`, `slug`, `locale` are scalars, all of them Strings. + +The other keywords however, are objects representing certain entities: + +* `participatoryProcesses` is a type that represents a collection of participatory spaces. It accepts arguments (`filter` and `order`), which are other object types as well. `slug` and `title` are the fields of the participatory process we are interested in, there are "Types" too. +* `filter` is a https://nightly.decidim.org/api/docs/input_object/participatoryprocessfilter/[ParticipatoryProcessFilter] input type, it has several properties that allows us to refine our search. One of them is the `publishedSince` property with the initial date from which to list entries. +* `order` is a https://nightly.decidim.org/api/docs/input_object/participatoryprocesssort/[ParticipatoryProcessSort] type, works the same way as the filter but with the goal of ordering the results. +* `title` is a https://nightly.decidim.org/api/docs/object/translatedfield/[TranslatedField] type, which allows us to deal with multi-language fields. + +Finally, note that the returned object is an array, each item of which is a representation of the object we requested. + +> \***About how filters and sorting are organized** +> +> There are two types of objects to filter and ordering collections in Decidim, they all work in a similar fashion. The type involved in filtering always have the suffix "Filter", for ordering it has the suffix "Sort". +> +> The types used to filter participatory spaces are: https://nightly.decidim.org/api/docs/input_object/participatoryprocessfilter/[ParticipatoryProcessFilter], [AssemblyFilter](#AssemblyFilter), and so on. +> +> Other collections (or connections) may have their own filters (i.e. https://nightly.decidim.org/api/docs/input_object/componentfilter/[ComponentFilter]). +> +> Each filter has its own properties, you should check any object in particular for details. The way they work with multi-languages fields, however, is the same: +> +> We can say we have some searchable object with a multi-language field called *title*, and we have a filter that allows us to search through this field. How should it work? Should we look up content for every language in the field? or should we stick to a specific language? +> +> In our case, we have decided to search only one particular language of a multi-language field but we let you choose which language to search. +> If no language is specified, the configured as default in the organization will be used. The keyword to specify the language is `locale`, and it should be provided in the 2 letters ISO 639-1 format (en = English, es = Spanish, ...). +> +> Example (this is not a real Decidim query): +> +> ```graphql +> some_collection(filter: { locale: "en", title: "ideas"}) { +> id +> } +> ``` +> +> The same applies to sorting (https://nightly.decidim.org/api/docs/input_object/participatoryprocesssort/[ParticipatoryProcessSort], [AssemblySort](#AssemblySort), etc.) +> +> In this case, the content of the field (*title*) only allows 2 values: *ASC* and *DESC*. +> +> Example of ordering alphabetically by the title content in French language: +> +> ```graphql +> some_collection(order: { locale: "en", title: "asc"}) { +> id +> } +> ``` +> +> Of course, you can combine both filter and order. Also remember to check availability of this type of behaviour for any particular filter/sort. + +== Decidim main types + +Decidim has 2 main types of objects through which content is provided. These are Participatory Spaces and Components. + +A participatory space is the first level, currently there are 5 officially supported: *Participatory Processes*, *Assemblies*, *Conferences* and *Initiatives*. For each participatory process there will correspond a collection type and a "single item" type. + +The previous example uses the collection type for participatory processes. You can try `assemblies`, `conferences`, or `initiatives` for the others. Note that each collection can implement their own filter and order types with different properties. + +As an example for a single item query, you can run: + +```graphql +{ + participatoryProcess(slug: "consectetur-at") { + slug + title { + translation(locale: "en") + } + } +} +``` + +And the response will be: + +```json +{ + "data": { + "participatoryProcess": { + "slug": "consectetur-at", + "title": { + "translation": "Soluta consectetur quos fugit aut." + } + } + } +} +``` + +== What is different? + +First, note that we are querying, in singular, the type `participatoryProcess`, with a different parameter, `slug`\*, (a String). We can use the `id` instead if we know it. + +Second, the response is not an Array, it is just the object we requested. We can expect to return `null` if the object is not found. + +> \* The `slug` is a convenient way to find a participatory space as is (usually) in the URL. +> +> For instance, consider this real case from Barcelona: +> +> https://www.decidim.barcelona/processes/patrimonigracia +> +> The word `patrimonigracia` indicates the "slug". + +== Components + +Every participatory space may (and should) have some components. There are 9 official components, these are `Proposals`, `Page`, `Meetings`, `Budgets`, `Surveys`, `Accountability`, `Debates` and `Blog`. Plugins may add their own components. + +If you know the `id`\* of a specific component you can obtain it by querying it directly: + +```graphql +{ + component(id:2) { + id + name { + translation(locale:"en") + } + __typename + participatorySpace { + id + type + } + } +} +``` + +Response: + +```json +{ + "data": { + "component": { + "id": "2", + "name": { + "translation": "Meetings" + }, + "__typename": "Meetings", + "participatorySpace": { + "id": "1", + "type": "Decidim::ParticipatoryProcess" + } + } + } +} +``` + +The process is analogue as what has been explained in the case of searching for one specific participatory process. + +> \*Note that the `id` of a component is present also in the URL after the letter "f": +> +> https://www.decidim.barcelona/processes/patrimonigracia/f/3257/ +> +> In this case, 3257. + +== What about component's collections? + +Glad you asked, component's collections cannot be retrieved directly, the are available *in the context* of a participatory space. + +For instance, we can query all the components in an particular Assembly as follows: + +```graphql +{ + assembly(id: 3) { + components { + id + name { + translation(locale: "en") + } + __typename + } + } +} +``` + +The response will be similar to: + +```json +{ + "data": { + "assembly": { + "components": [ + { + "id": "42", + "name": { + "translation": "Accountability" + }, + "__typename": "Component" + }, + { + "id": "38", + "name": { + "translation": "Meetings" + }, + "__typename": "Meetings" + }, + { + "id": "37", + "name": { + "translation": "Page" + }, + "__typename": "Pages" + }, + { + "id": "39", + "name": { + "translation": "Proposals" + }, + "__typename": "Proposals" + } + ] + } + } +} +``` + +We can also apply some filters by using the [ComponentFilter](#ComponentFilter) type. In the next query we would like to *find all the components with geolocation enabled in the assembly with id=2*: + +```graphql +{ + assembly(id: 2) { + components(filter: {withGeolocationEnabled: true}) { + id + name { + translation(locale: "en") + } + __typename + } + } +} +``` + +The response: + +```json +{ + "data": { + "assembly": { + "components": [ + { + "id": "39", + "name": { + "translation": "Meetings" + }, + "__typename": "Meetings" + } + ] + } + } +} +``` + +Note that, in this case, there is only one component returned, "Meetings". In some cases Proposals can be geolocated too therefore would be returned in this query. + +== Polymorphism and connections + +Many relationships between tables in Decidim are polymorphic, this means that the related object can belong to different classes and share just a few properties in common. + +For instance, components in a participatory space are polymorphic, while the concept of component is generic and all of them share properties like *published date*, *name* or *weight*, they differ in the rest. *Proposals* have the *status* field while *Meetings* have an *agenda*. + +Another example are the case of linked resources, these are properties that may link objects of different nature between components or participatory spaces. + +In a very simplified way (to know more please refer to the official guide), GraphQL polymorphism is handled through the operator `... on`. You will know when a field is polymorphic because the property `__typename`, which tells you the type of that particular object, will change accordingly. + +In the previous examples we have queried for this property: + +Response fragment: + +```json + "components": [ + { + "id": "38", + "name": { + "translation": "Meetings" + }, + "__typename": "Meetings" + } +``` + +So, if we want to access the rest of the properties in a polymorphic object, we should do it through the `... on` operator as follows: + +```graphql +{ + assembly(id: 2) { + components { + id + ... on Proposals { + + } + } + } +} +``` + +Consider this query: + +```graphql +{ + assembly(id: 3) { + components(filter: {type: "Proposals"}) { + id + name { + translation(locale: "en") + } + ... on Proposals { + proposals(order: {likeCount: "desc"}, first: 2) { + edges { + node { + id + likes { + name + } + } + } + } + } + } + } +} +``` + +The response: + +```json +{ + "data": { + "assembly": { + "components": [ + { + "id": "39", + "name": { + "translation": "Proposals" + }, + "proposals": { + "edges": [ + { + "node": { + "id": "35", + "likes": [ + { + "name": "Ms. Johnathon Schaefer" + }, + { + "name": "Linwood Lakin PhD 3 4 endr1" + }, + { + "name": "Gracie Emmerich" + }, + { + "name": "Randall Rath 3 4 endr3" + }, + { + "name": "Jolene Schmitt MD" + }, + { + "name": "Clarence Hammes IV 3 4 endr5" + }, + { + "name": "Omar Mayer" + }, + { + "name": "Raymundo Jaskolski 3 4 endr7" + } + ] + } + }, + { + "node": { + "id": "33", + "likes": [ + { + "name": "Spring Brakus" + }, + { + "name": "Reiko Simonis IV 3 2 endr1" + }, + { + "name": "Dr. Jim Denesik" + }, + { + "name": "Dr. Mack Schoen 3 2 endr3" + } + ] + } + } + ] + } + } + ] + } + } +} +``` + +== What is going on? + +Until the `... on Proposals` line, there is nothing new. We are requesting the *Assembly* participatory space identified by the `id=3`, then listing all its components with the type "Proposals". All the components share the *id* and *name* properties, so we can just add them at the query. + +After that, we want content specific from the *Proposals* type. In order to do that we must tell the server that the content we will request shall only be executed if the types matches *Proposals*. We do that by wrapping the rest of the query in the `... on Proposals` clause. + +The next line is just a property of the type *Proposals* which is a type of collection called a "connection". A connection works similar as normal collection (such as *components*) but it can handle more complex cases. + +Typically, a connection is used to paginate long results, for this purpose the results are not directly available but encapsulated inside the list *edges* in several *node* results. Also there are more arguments available in order to navigate between pages. This are the arguments: + +* `first`: Returns the first *n* elements from the list +* `after`: Returns the elements in the list that come after the specified *cursor* +* `last`: Returns the last *n* elements from the list +* `before`: Returns the elements in the list that come before the specified *cursor* + +Example: + +```graphql +{ + assembly(id: 3) { + components(filter: {type: "Proposals"}) { + id + name { + translation(locale: "en") + } + ... on Proposals { + proposals(first:2,after:"Mg") { + pageInfo { + endCursor + startCursor + hasPreviousPage + hasNextPage + } + edges { + node { + id + likes { + name + } + } + } + } + } + } + } +} +``` + +Being the response: + +```json +{ + "data": { + "assembly": { + "components": [ + { + "id": "39", + "name": { + "translation": "Proposals" + }, + "proposals": { + "pageInfo": { + "endCursor": "NA", + "startCursor": "Mw", + "hasPreviousPage": false, + "hasNextPage": true + }, + "edges": [ + { + "node": { + "id": "32", + "likes": [] + } + }, + { + "node": { + "id": "31", + "likes": [ + { + "name": "Mr. Nicolas Raynor" + }, + { + "name": "Gerry Fritsch PhD 3 1 endr1" + } + ] + } + } + ] + } + } + ] + } + } +} +``` + +As you can see, a part from the *edges* list, you can access to the object *pageInfo* which gives you the information needed to navigate through the different pages. + +For more info on how connections work, you can check the official guide: + +https://graphql.org/learn/pagination/ + diff --git a/docs/modules/develop/pages/api/index.adoc b/docs/modules/develop/pages/api/index.adoc index e485acde910f0..0396de69388f4 100644 --- a/docs/modules/develop/pages/api/index.adoc +++ b/docs/modules/develop/pages/api/index.adoc @@ -8,14 +8,53 @@ Documentation for this API is auto-generated in each instance of Decidim, usuall Accessing that URL will give a full details of all the objects that can be requested and how. It includes the full field types reference. Note that the API can be slightly different for different Decidim instances and versions. -For example, you can see the Decidim demo application API documentation under the following URL: +== Using the GraphQL API -`https://try.decidim.org/api/docs` +The GraphQL format is a JSON formatted text that is specified in a query. Response is a JSON object as well. For details about specification check the official https://graphql.org/learn/[GraphQL site]. -As an alternative, if you cannot access that URL on your instance you can read about how this work https://github.com/decidim/decidim/blob/develop/decidim-api/docs/usage.md[Using the Decidim GraphQL API]. +Exercise caution when utilizing the output of this API, as it may include HTML that has not been escaped. Take particular care in handling this data, specially if you intend to render it on a webpage. -== Integrating external applications with the API +For instance, you can check the version of a Decidim installation by using different technologies and languages: -By default, the GraphQL API in Decidim is publically available for read-only operations and it can also be used by external applications to read data from Decidim. If you want to write data over the API, i.e. perform GraphQL mutations, you need first need to authenticate the API user to perform these operations. Otherwise, it is not possible to perform such operations as most such operations are performed as an actual user in Decidim. +include::develop:partial$api/decidim_version.adoc[] -More information regarding implementing the API authentication is available in the API documentation of your Decidim instance. +Note that `Content-Type` needs to be specified. + +The query can also be used in GraphiQL, in that case you can skip the `"query"` text: + +```graphql +{ + decidim { + version + } +} +``` + +Response (formatted) should look something like this: + +```json +{ + "data": { + "decidim": { + "version": "0.18.1" + } + } +} +``` + +The most practical way to experiment with GraphQL, however, is just to use the in-browser IDE GraphiQL. It provides access to the documentation and auto-complete (use CTRL-Space) for writing queries. + +From now on, we will skip the "query" keyword for the purpose of readability. You can skip it too if you are using GraphiQL, if you are querying directly (by using CURL for instance) you will need to include it. + +** xref:develop:api/authentication.adoc[Authentication] +** xref:develop:api/core-concepts.adoc[Core concepts] +** API reference by module +*** Spaces +*** Components +**** xref:develop:api/reference/components/proposals.adoc[Proposals] + +== Usage limits + +Decidim is just a Rails application, meaning that any particular installation may implement custom limits in order to access the API (and the application in general). + +By default (particular installations may change that), API uses the same limitations as the whole Decidim website, provided by the Gem https://github.com/kickstarter/rack-attack[Rack::Attack]. These are 100 maximum requests per minute per IP to prevent DoS attacks diff --git a/docs/modules/develop/pages/api/proposals/mutations.adoc b/docs/modules/develop/pages/api/reference/components/proposals.adoc similarity index 100% rename from docs/modules/develop/pages/api/proposals/mutations.adoc rename to docs/modules/develop/pages/api/reference/components/proposals.adoc diff --git a/docs/modules/develop/partials/api/decidim_version.adoc b/docs/modules/develop/partials/api/decidim_version.adoc new file mode 100644 index 0000000000000..7c3286b3570a5 --- /dev/null +++ b/docs/modules/develop/partials/api/decidim_version.adoc @@ -0,0 +1,102 @@ + +++++ +
+
+ + + + +
+
+
+++++ + +[source,bash] +---- +curl -sSH "Content-Type: application/json" \ + -d '{"query": "{ decidim { version } }"}' \ + https://www.decidim.barcelona/api/ +---- + +++++ +
+
+++++ + +[source,javascript] +---- +const url = "https://www.decidim.barcelona/api/"; +const query = "{ decidim { version } }"; + +fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ query }), +}) + .then((response) => response.json()) + .then((data) => { + console.log(data); + }) + .catch((error) => { + console.error(error); + }); +---- + +++++ +
+
+++++ + +[source,python] +---- +import requests + +url = "https://www.decidim.barcelona/api/" +query = "{ decidim { version } }" + +headers = { + "Content-Type": "application/json" +} + +response = requests.post( + url, + json={"query": query}, + headers=headers +) + +print(response.json()) +---- + +++++ +
+
+++++ + +[source,ruby] +---- +require "net/http" +require "uri" +require "json" + +uri = URI.parse("https://www.decidim.barcelona/api/") + +http = Net::HTTP.new(uri.host, uri.port) +http.use_ssl = true + +request = Net::HTTP::Post.new(uri.request_uri) +request["Content-Type"] = "application/json" +request.body = { + query: "{ decidim { version } }" +}.to_json + +response = http.request(request) +puts JSON.parse(response.body) +---- + +++++ +
+
+
+++++ From c4aa90c6f395f25bc6976d4c9738e349972c5e2d Mon Sep 17 00:00:00 2001 From: Leo Storey <123873192+Ginger-Leo@users.noreply.github.com> Date: Mon, 15 Dec 2025 23:34:09 +0200 Subject: [PATCH 021/116] Fix search result comment with link (#15774) Co-authored-by: Alexandru Emil Lupu Co-authored-by: Leo --- .../cells/decidim/comments/comment_s_cell.rb | 2 +- .../spec/system/search_comments_spec.rb | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/decidim-comments/app/cells/decidim/comments/comment_s_cell.rb b/decidim-comments/app/cells/decidim/comments/comment_s_cell.rb index 001cb75b5ef1c..4e035c23d1f05 100644 --- a/decidim-comments/app/cells/decidim/comments/comment_s_cell.rb +++ b/decidim-comments/app/cells/decidim/comments/comment_s_cell.rb @@ -12,7 +12,7 @@ class CommentSCell < Decidim::CardSCell private def title - resource_link_text + sanitize(translated_attribute(model.body)) end def resource_path diff --git a/decidim-comments/spec/system/search_comments_spec.rb b/decidim-comments/spec/system/search_comments_spec.rb index 4a898f2c3e744..93a610dcf3681 100644 --- a/decidim-comments/spec/system/search_comments_spec.rb +++ b/decidim-comments/spec/system/search_comments_spec.rb @@ -16,5 +16,22 @@ searchables << comment end + context "when there is a link in the comment search result" do + let(:search_input_selector) { "input#input-search" } + + before do + create(:comment, body: "Here is an interesting link: https://github.com/decidim", commentable:) + visit decidim.root_path + field = find(search_input_selector) + field.set "Here is an interesting" + send_keys(:enter) + end + + it "does not allow clickable link" do + expect(page).to have_no_link(href: "https://github.com/decidim") + expect(page).to have_text("Here is an interesting link: https://github.com/decidim") + end + end + include_examples "searchable results" end From 4c1c6a04a1a6bfc6f66de70413cd70000bc394bb Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Tue, 16 Dec 2025 11:03:31 +0200 Subject: [PATCH 022/116] Improve handling of `PermissionNotSetError` exceptions in GraphQL API (#15787) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- decidim-api/config/locales/en.yml | 1 + .../api/errors/permission_not_set_error.rb | 13 ++++++ decidim-api/lib/decidim/api/schema.rb | 4 ++ .../lib/decidim/api/test/type_context.rb | 2 + decidim-api/lib/decidim/api/types.rb | 1 + decidim-blogs/lib/decidim/api/post_type.rb | 2 - .../lib/decidim/api/budget_type.rb | 2 - .../lib/decidim/api/project_type.rb | 2 - .../spec/types/integration_schema_spec.rb | 21 ++++++++++ .../api/conference_registration_type_type.rb | 2 - .../decidim/api/conference_speaker_type.rb | 2 - decidim-dev/spec/types/errors_spec.rb | 40 +++++++++++++++++++ .../lib/decidim/api/agenda_type.rb | 2 - .../lib/decidim/api/proposal_type.rb | 2 - .../lib/decidim/api/survey_type.rb | 2 - 15 files changed, 82 insertions(+), 16 deletions(-) create mode 100644 decidim-api/lib/decidim/api/errors/permission_not_set_error.rb create mode 100644 decidim-dev/spec/types/errors_spec.rb diff --git a/decidim-api/config/locales/en.yml b/decidim-api/config/locales/en.yml index 62bf91ab4690a..51d5113311583 100644 --- a/decidim-api/config/locales/en.yml +++ b/decidim-api/config/locales/en.yml @@ -4,5 +4,6 @@ en: api: errors: not_found: "%{type} not found" + permission_not_set: Permission has not been set for this %{type} unauthorized_field: You cannot view or edit %{field} field on %{type} because you do not have permission unauthorized_object: You cannot view or edit this %{type} because you do not have permissions diff --git a/decidim-api/lib/decidim/api/errors/permission_not_set_error.rb b/decidim-api/lib/decidim/api/errors/permission_not_set_error.rb new file mode 100644 index 0000000000000..340947a119a3b --- /dev/null +++ b/decidim-api/lib/decidim/api/errors/permission_not_set_error.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Decidim + module Api + module Errors + class PermissionNotSetError < GraphQL::ExecutionError + def to_h + super.merge({ "extensions" => { "code" => "NO_PERMISSION_SET" } }) + end + end + end + end +end diff --git a/decidim-api/lib/decidim/api/schema.rb b/decidim-api/lib/decidim/api/schema.rb index b064e260ed35e..13d5cd70b13f0 100644 --- a/decidim-api/lib/decidim/api/schema.rb +++ b/decidim-api/lib/decidim/api/schema.rb @@ -26,6 +26,10 @@ def self.unauthorized_field(error) rescue_from(ActiveRecord::RecordNotFound) do |_err, _obj, _args, _ctx, field| raise Decidim::Api::Errors::NotFoundError, I18n.t("decidim.api.errors.not_found", type: field.type.unwrap.graphql_name) end + + rescue_from(Decidim::PermissionAction::PermissionNotSetError) do |_err, _obj, _args, _ctx, field| + raise Decidim::Api::Errors::PermissionNotSetError, I18n.t("decidim.api.errors.permission_not_set", type: field.type.unwrap.graphql_name) + end end end end diff --git a/decidim-api/lib/decidim/api/test/type_context.rb b/decidim-api/lib/decidim/api/test/type_context.rb index 8a9c02a29f77c..ae6c2c331df97 100644 --- a/decidim-api/lib/decidim/api/test/type_context.rb +++ b/decidim-api/lib/decidim/api/test/type_context.rb @@ -32,6 +32,8 @@ def raise_proper_error(error) code = error.dig("extensions", "code") case code + when "NO_PERMISSION_SET" + raise Decidim::Api::Errors::PermissionNotSetError, error["message"] when "NOT_FOUND" raise Decidim::Api::Errors::NotFoundError, error["message"] when "NO_FIELD_PERMISSION" diff --git a/decidim-api/lib/decidim/api/types.rb b/decidim-api/lib/decidim/api/types.rb index d06b589e86252..8d16cd75fe078 100644 --- a/decidim-api/lib/decidim/api/types.rb +++ b/decidim-api/lib/decidim/api/types.rb @@ -10,6 +10,7 @@ module Api autoload :ComponentMutationType, "decidim/api/component_mutation_type" module Errors + autoload :PermissionNotSetError, "decidim/api/errors/permission_not_set_error" autoload :NotFoundError, "decidim/api/errors/not_found_error" autoload :UnauthorizedFieldError, "decidim/api/errors/unauthorized_field_error" autoload :UnauthorizedObjectError, "decidim/api/errors/unauthorized_object_error" diff --git a/decidim-blogs/lib/decidim/api/post_type.rb b/decidim-blogs/lib/decidim/api/post_type.rb index 5e83e70cff8bd..894eb6c470f77 100644 --- a/decidim-blogs/lib/decidim/api/post_type.rb +++ b/decidim-blogs/lib/decidim/api/post_type.rb @@ -33,8 +33,6 @@ def self.authorized?(object, context) ].all? super && chain - rescue Decidim::PermissionAction::PermissionNotSetError - false end end end diff --git a/decidim-budgets/lib/decidim/api/budget_type.rb b/decidim-budgets/lib/decidim/api/budget_type.rb index 0e08730102dcd..1f3de24edeb81 100644 --- a/decidim-budgets/lib/decidim/api/budget_type.rb +++ b/decidim-budgets/lib/decidim/api/budget_type.rb @@ -23,8 +23,6 @@ def url def self.authorized?(object, context) super && object.visible? - rescue Decidim::PermissionAction::PermissionNotSetError - false end end end diff --git a/decidim-budgets/lib/decidim/api/project_type.rb b/decidim-budgets/lib/decidim/api/project_type.rb index f26390c3a70e3..d293ed9371458 100644 --- a/decidim-budgets/lib/decidim/api/project_type.rb +++ b/decidim-budgets/lib/decidim/api/project_type.rb @@ -54,8 +54,6 @@ def self.authorized?(object, context) ].all? super && chain - rescue Decidim::PermissionAction::PermissionNotSetError - false end end end diff --git a/decidim-budgets/spec/types/integration_schema_spec.rb b/decidim-budgets/spec/types/integration_schema_spec.rb index 3de28c2cd462d..29cb3dceae115 100644 --- a/decidim-budgets/spec/types/integration_schema_spec.rb +++ b/decidim-budgets/spec/types/integration_schema_spec.rb @@ -469,6 +469,26 @@ it "is visible" do expect(response["assembly"]["components"].first[lookout_key]).to eq(query_result.except("projects")) end + + context "and requests projects that is not supposed to see" do + let!(:current_user) { nil } + + let(:component_fragment) do + %( + fragment fooComponent on Budgets { + budget(id: #{budget.id}) { + id + projects { + id + } + } + }) + end + + it "throws Decidim::Api::Errors::UnauthorizedObjectError" do + expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedObjectError, "You cannot view or edit this Project because you do not have permissions") + end + end end context "when user is member" do @@ -508,6 +528,7 @@ end end end + context "when the user is space moderator" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } let!(:role) { create(:assembly_user_role, assembly: participatory_process, user: current_user, role: "moderator") } diff --git a/decidim-conferences/lib/decidim/api/conference_registration_type_type.rb b/decidim-conferences/lib/decidim/api/conference_registration_type_type.rb index a4c6062292abd..54916546008e1 100644 --- a/decidim-conferences/lib/decidim/api/conference_registration_type_type.rb +++ b/decidim-conferences/lib/decidim/api/conference_registration_type_type.rb @@ -22,8 +22,6 @@ def self.authorized?(object, context) ].all? super && chain - rescue Decidim::PermissionAction::PermissionNotSetError - false end end end diff --git a/decidim-conferences/lib/decidim/api/conference_speaker_type.rb b/decidim-conferences/lib/decidim/api/conference_speaker_type.rb index 2992a2a024184..239890b9393c0 100644 --- a/decidim-conferences/lib/decidim/api/conference_speaker_type.rb +++ b/decidim-conferences/lib/decidim/api/conference_speaker_type.rb @@ -30,8 +30,6 @@ def self.authorized?(object, context) ].all? super && chain - rescue Decidim::PermissionAction::PermissionNotSetError - false end end end diff --git a/decidim-dev/spec/types/errors_spec.rb b/decidim-dev/spec/types/errors_spec.rb new file mode 100644 index 0000000000000..62063da1cf875 --- /dev/null +++ b/decidim-dev/spec/types/errors_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require "spec_helper" +require "decidim/api/test" + +describe "Decidim::Api::Errors" do + include_context "with a graphql class type" + + let(:query) do + %( + query { + testedError + } + ) + end + + let(:schema) do + klass = type_class + Class.new(Decidim::Api::Schema) do + query klass + end + end + + context "when Decidim::Api::Errors::PermissionNotSetError is raised" do + let(:type_class) do + Class.new(Decidim::Api::Types::BaseObject) do + graphql_name "ErrorTypeTest" + field :tested_error, String, null: false + + def tested_error + raise Decidim::PermissionAction::PermissionNotSetError, "Exemplifying permission not set error" + end + end + end + + it "throws exception" do + expect { response }.to raise_error(Decidim::Api::Errors::PermissionNotSetError, /Permission has not been set for this/) + end + end +end diff --git a/decidim-meetings/lib/decidim/api/agenda_type.rb b/decidim-meetings/lib/decidim/api/agenda_type.rb index 99cbc43606509..93ef97d4acded 100644 --- a/decidim-meetings/lib/decidim/api/agenda_type.rb +++ b/decidim-meetings/lib/decidim/api/agenda_type.rb @@ -14,8 +14,6 @@ class AgendaType < Decidim::Api::Types::BaseObject def self.authorized?(object, context) super && object.visible? - rescue Decidim::PermissionAction::PermissionNotSetError - false end end end diff --git a/decidim-proposals/lib/decidim/api/proposal_type.rb b/decidim-proposals/lib/decidim/api/proposal_type.rb index ce8af12ecc1ac..209d4f382c8da 100644 --- a/decidim-proposals/lib/decidim/api/proposal_type.rb +++ b/decidim-proposals/lib/decidim/api/proposal_type.rb @@ -97,8 +97,6 @@ def self.authorized?(object, context) ].all? super && chain - rescue Decidim::PermissionAction::PermissionNotSetError - false end private diff --git a/decidim-surveys/lib/decidim/api/survey_type.rb b/decidim-surveys/lib/decidim/api/survey_type.rb index 520da3b766667..d925c69a1b49c 100644 --- a/decidim-surveys/lib/decidim/api/survey_type.rb +++ b/decidim-surveys/lib/decidim/api/survey_type.rb @@ -27,8 +27,6 @@ def self.authorized?(object, context) context[:current_settings] = object.component.current_settings super - rescue Decidim::PermissionAction::PermissionNotSetError - false end end end From 5fb38f0706570ac6df3fb0cdf7621d63a5934a45 Mon Sep 17 00:00:00 2001 From: Tom Greenwood <101816158+greenwoodt@users.noreply.github.com> Date: Tue, 16 Dec 2025 13:40:25 +0100 Subject: [PATCH 023/116] Show responses menu entry in Surveys' admin (#15785) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * condition to display responses in dropdown * i18n transalation for responses * Update decidim-surveys/app/views/decidim/surveys/admin/surveys/index.html.erb Co-authored-by: Andrés Pereira de Lucena * Update decidim-surveys/config/locales/en.yml Co-authored-by: Andrés Pereira de Lucena * Update decidim-surveys/config/locales/en.yml Co-authored-by: Andrés Pereira de Lucena * updated the icon for responses tab * route for admin added in spec, specs for responses and no responses added * fixed translation conflict from suggestion --------- Co-authored-by: Andrés Pereira de Lucena --- .../surveys/admin/surveys/index.html.erb | 10 +++++- decidim-surveys/config/locales/en.yml | 1 + .../spec/system/admin_manages_surveys_spec.rb | 33 +++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/decidim-surveys/app/views/decidim/surveys/admin/surveys/index.html.erb b/decidim-surveys/app/views/decidim/surveys/admin/surveys/index.html.erb index af902c996941e..308db89b9beaf 100644 --- a/decidim-surveys/app/views/decidim/surveys/admin/surveys/index.html.erb +++ b/decidim-surveys/app/views/decidim/surveys/admin/surveys/index.html.erb @@ -61,10 +61,18 @@ + <% if survey.questionnaire.responses.any? %> + + <% end %>
diff --git a/decidim-surveys/config/locales/en.yml b/decidim-surveys/config/locales/en.yml index 4e92e4b9352df..7f023100ef764 100644 --- a/decidim-surveys/config/locales/en.yml +++ b/decidim-surveys/config/locales/en.yml @@ -72,6 +72,7 @@ en: manage_questions: Questions new_survey: New survey preview: Preview + responses: Responses responses_alert: Delete responses at publish is active for this survey. There are currently %{responses_count} responses that will be destroyed if you continue. title: Actions admin: diff --git a/decidim-surveys/spec/system/admin_manages_surveys_spec.rb b/decidim-surveys/spec/system/admin_manages_surveys_spec.rb index 5f2b474822956..a322132c0f452 100644 --- a/decidim-surveys/spec/system/admin_manages_surveys_spec.rb +++ b/decidim-surveys/spec/system/admin_manages_surveys_spec.rb @@ -342,6 +342,35 @@ end end + context "when the survey has responses or more" do + let!(:question) do + create(:questionnaire_question, questionnaire:) + end + let!(:response) { create(:response, questionnaire:, question:) } + + before do + visit manage_questionnaire_path + end + + it "allows access to responses" do + within "tr", text: decidim_sanitize_translated(survey.title) do + find("button[data-controller='dropdown']").click + expect(page).to have_link("Responses") + end + end + end + + context "when the survey has no responses" do + let!(:question) { create(:questionnaire_question, questionnaire:) } + + it "does not show the Responses button" do + within "tr", text: decidim_sanitize_translated(survey.title) do + find("button[data-controller='dropdown']").click + expect(page).to have_no_link("Responses") + end + end + end + context "when updates the questionnaire" do let(:description) do { @@ -378,6 +407,10 @@ def questionnaire_public_path main_component_path(component) end + def manage_questionnaire_path + Decidim::EngineRouter.admin_proxy(component).surveys_path + end + private def find_nested_form_field(attribute, visible: :visible) From 94b70395968ae837b2927cde14bfa0d4aaae6f54 Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Wed, 17 Dec 2025 11:31:19 +0200 Subject: [PATCH 024/116] Fix process and assembly admin members action logs (#15788) --- .../admin_log/assembly_presenter.rb | 13 ++++++- decidim-assemblies/config/locales/en.yml | 2 ++ ...ers_assemblies_private_space_users_spec.rb | 36 +++++++++++++------ .../participatory_process_presenter.rb | 13 ++++++- .../config/locales/en.yml | 2 ++ ..._participatory_process_members_examples.rb | 18 ++++++++++ 6 files changed, 71 insertions(+), 13 deletions(-) diff --git a/decidim-assemblies/app/presenters/decidim/assemblies/admin_log/assembly_presenter.rb b/decidim-assemblies/app/presenters/decidim/assemblies/admin_log/assembly_presenter.rb index a5064b7e2e763..4d08e8b230271 100644 --- a/decidim-assemblies/app/presenters/decidim/assemblies/admin_log/assembly_presenter.rb +++ b/decidim-assemblies/app/presenters/decidim/assemblies/admin_log/assembly_presenter.rb @@ -59,9 +59,20 @@ def i18n_labels_scope "activemodel.attributes.assembly" end + # i18n-tasks-use t("decidim.admin_log.assembly.create") + # i18n-tasks-use t("decidim.admin_log.assembly.publish") + # i18n-tasks-use t("decidim.admin_log.assembly.unpublish") + # i18n-tasks-use t("decidim.admin_log.assembly.update") + # i18n-tasks-use t("decidim.admin_log.assembly.import") + # i18n-tasks-use t("decidim.admin_log.assembly.export") + # i18n-tasks-use t("decidim.admin_log.assembly.duplicate") + # i18n-tasks-use t("decidim.admin_log.assembly.soft_delete") + # i18n-tasks-use t("decidim.admin_log.assembly.restore") + # i18n-tasks-use t("decidim.admin_log.assembly.publish_all_members") + # i18n-tasks-use t("decidim.admin_log.assembly.unpublish_all_members") def action_string case action - when "create", "publish", "unpublish", "update", "duplicate", "export", "import", "soft_delete", "restore" + when "create", "publish", "unpublish", "update", "duplicate", "export", "import", "soft_delete", "restore", "publish_all_members", "unpublish_all_members" "decidim.admin_log.assembly.#{action}" else super diff --git a/decidim-assemblies/config/locales/en.yml b/decidim-assemblies/config/locales/en.yml index c946b4bbbf0d1..5176901ad487a 100644 --- a/decidim-assemblies/config/locales/en.yml +++ b/decidim-assemblies/config/locales/en.yml @@ -219,9 +219,11 @@ en: export: "%{user_name} exported the %{resource_name} assembly" import: "%{user_name} imported the %{resource_name} assembly" publish: "%{user_name} published the %{resource_name} assembly" + publish_all_members: "%{user_name} published all members of the %{resource_name} assembly" restore: "%{user_name} restored the %{resource_name} assembly" soft_delete: "%{user_name} moved to trash the %{resource_name} assembly" unpublish: "%{user_name} unpublished the %{resource_name} assembly" + unpublish_all_members: "%{user_name} unpublished all members of the %{resource_name} assembly" update: "%{user_name} updated the %{resource_name} assembly" assembly_member: create: "%{user_name} created the %{resource_name} member in the %{space_name} assembly" diff --git a/decidim-assemblies/spec/system/admin/admin_filters_assemblies_private_space_users_spec.rb b/decidim-assemblies/spec/system/admin/admin_filters_assemblies_private_space_users_spec.rb index 1abfd343aa903..fb89bd388dffe 100644 --- a/decidim-assemblies/spec/system/admin/admin_filters_assemblies_private_space_users_spec.rb +++ b/decidim-assemblies/spec/system/admin/admin_filters_assemblies_private_space_users_spec.rb @@ -9,7 +9,7 @@ let!(:user) { create(:user, :admin, :confirmed, organization:) } let(:assembly) { create(:assembly, organization:, private_space: true) } - let!(:invited_user1) { create(:user, name:, organization:) } + let!(:invited_user1) { create(:user, name:, organization:, invitation_sent_at: 1.day.ago, invitation_accepted_at: Time.current) } let!(:invited_member1) { create(:assembly_member, user: invited_user1, privatable_to: assembly) } let!(:invited_user2) { create(:user, email:, organization:) } let!(:invited_member2) { create(:assembly_member, user: invited_user2, privatable_to: assembly) } @@ -19,10 +19,14 @@ let(:resource_controller) { Decidim::Assemblies::Admin::MembersController } - context "when managing private process" do - before do - invited_user1.update!(invitation_sent_at: 1.day.ago, invitation_accepted_at: Time.current) + before do + switch_to_host(organization.host) + login_as user, scope: :user + visit decidim_admin_assemblies.members_path(assembly_slug: assembly.slug) + end + context "when managing private space" do + before do switch_to_host(organization.host) login_as user, scope: :user visit decidim_admin_assemblies.edit_assembly_path(assembly) @@ -38,16 +42,26 @@ context "when managing members in a public process" do let(:assembly) { create(:assembly, organization:, private_space: false) } - before do - invited_user1.update!(invitation_sent_at: 1.day.ago, invitation_accepted_at: Time.current) + it "restricts access" do + expect(page).to have_admin_callout("You are not authorized to perform this action.") + end + end - switch_to_host(organization.host) - login_as user, scope: :user - visit decidim_admin_assemblies.members_path(assembly_slug: assembly.slug) + describe "when publishing all members" do + let!(:member) { create(:member, :unpublished, user:, privatable_to: assembly) } + + it "publishes all members" do + click_on "Publish all" + + sleep(1) + expect(member.reload).to be_published end - it "restricts access" do - expect(page).to have_admin_callout("You are not authorized to perform this action.") + it "displays the correct log message" do + click_on "Publish all" + sleep(1) + visit decidim_admin.root_path + expect(page).to have_content("published all members of the #{translated(assembly.title)} assembly") end end end diff --git a/decidim-participatory_processes/app/presenters/decidim/participatory_processes/admin_log/participatory_process_presenter.rb b/decidim-participatory_processes/app/presenters/decidim/participatory_processes/admin_log/participatory_process_presenter.rb index 7b86278acd7c8..02a9a870d4adc 100644 --- a/decidim-participatory_processes/app/presenters/decidim/participatory_processes/admin_log/participatory_process_presenter.rb +++ b/decidim-participatory_processes/app/presenters/decidim/participatory_processes/admin_log/participatory_process_presenter.rb @@ -35,9 +35,20 @@ def diff_fields_mapping } end + # i18n-tasks-use t("decidim.admin_log.participatory_process.create") + # i18n-tasks-use t("decidim.admin_log.participatory_process.publish") + # i18n-tasks-use t("decidim.admin_log.participatory_process.unpublish") + # i18n-tasks-use t("decidim.admin_log.participatory_process.update") + # i18n-tasks-use t("decidim.admin_log.participatory_process.import") + # i18n-tasks-use t("decidim.admin_log.participatory_process.export") + # i18n-tasks-use t("decidim.admin_log.participatory_process.duplicate") + # i18n-tasks-use t("decidim.admin_log.participatory_process.soft_delete") + # i18n-tasks-use t("decidim.admin_log.participatory_process.restore") + # i18n-tasks-use t("decidim.admin_log.participatory_process.publish_all_members") + # i18n-tasks-use t("decidim.admin_log.participatory_process.unpublish_all_members") def action_string case action - when "create", "publish", "unpublish", "update", "import", "export", "duplicate", "soft_delete", "restore" + when "create", "publish", "unpublish", "update", "import", "export", "duplicate", "soft_delete", "restore", "publish_all_members", "unpublish_all_members" "decidim.admin_log.participatory_process.#{action}" else super diff --git a/decidim-participatory_processes/config/locales/en.yml b/decidim-participatory_processes/config/locales/en.yml index 23ac2eaf2f592..f5da230fea98a 100644 --- a/decidim-participatory_processes/config/locales/en.yml +++ b/decidim-participatory_processes/config/locales/en.yml @@ -283,9 +283,11 @@ en: export: "%{user_name} exported the %{resource_name} participatory process" import: "%{user_name} imported the %{resource_name} participatory process" publish: "%{user_name} published the %{resource_name} participatory process" + publish_all_members: "%{user_name} published all members of the %{resource_name} participatory process" restore: "%{user_name} restored the %{resource_name} participatory process" soft_delete: "%{user_name} trashed the %{resource_name} participatory process" unpublish: "%{user_name} unpublished the %{resource_name} participatory process" + unpublish_all_members: "%{user_name} unpublished all members of the %{resource_name} participatory process" update: "%{user_name} updated the %{resource_name} participatory process" participatory_process_group: create: "%{user_name} created the %{resource_name} participatory process group" diff --git a/decidim-participatory_processes/spec/shared/manage_participatory_process_members_examples.rb b/decidim-participatory_processes/spec/shared/manage_participatory_process_members_examples.rb index 2186797493fa8..0d9bb282fad38 100644 --- a/decidim-participatory_processes/spec/shared/manage_participatory_process_members_examples.rb +++ b/decidim-participatory_processes/spec/shared/manage_participatory_process_members_examples.rb @@ -54,6 +54,24 @@ end end + describe "when publishing all members" do + let!(:member) { create(:member, :unpublished, user:, privatable_to: participatory_process) } + + it "publishes all members" do + click_on "Publish all" + + sleep(1) + expect(member.reload).to be_published + end + + it "displays the correct log message" do + click_on "Publish all" + sleep(1) + visit decidim_admin.root_path + expect(page).to have_content("published all members of the #{translated(participatory_process.title)} participatory process") + end + end + describe "when managing different users" do before do create(:member, user: other_user, privatable_to: participatory_process) From 9b079c251efb6c0897b67d1d17eb9d3f86aab5f2 Mon Sep 17 00:00:00 2001 From: Tom Greenwood <101816158+greenwoodt@users.noreply.github.com> Date: Wed, 17 Dec 2025 14:31:43 +0100 Subject: [PATCH 025/116] Sorting via created_at attribute within spaces (#15810) --- .../app/models/decidim/assembly.rb | 2 +- .../admin/admin_filters_assemblies_spec.rb | 43 +++++++++++++++++++ .../app/models/decidim/conference.rb | 2 +- .../admin/admin_filters_conferences_spec.rb | 43 +++++++++++++++++++ .../app/models/decidim/initiative.rb | 2 +- .../admin/admin_filters_initiatives_spec.rb | 43 +++++++++++++++++++ .../models/decidim/participatory_process.rb | 2 +- ...dmin_filters_participatory_process_spec.rb | 43 +++++++++++++++++++ 8 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 decidim-assemblies/spec/system/admin/admin_filters_assemblies_spec.rb create mode 100644 decidim-conferences/spec/system/admin/admin_filters_conferences_spec.rb create mode 100644 decidim-initiatives/spec/system/admin/admin_filters_initiatives_spec.rb create mode 100644 decidim-participatory_processes/spec/system/admin/admin_filters_participatory_process_spec.rb diff --git a/decidim-assemblies/app/models/decidim/assembly.rb b/decidim-assemblies/app/models/decidim/assembly.rb index 9f718023f3b27..208cc845bae6b 100644 --- a/decidim-assemblies/app/models/decidim/assembly.rb +++ b/decidim-assemblies/app/models/decidim/assembly.rb @@ -169,7 +169,7 @@ def self.ransackable_attributes(auth_object = nil) return base unless auth_object&.admin? - base + %w(published_at private_space parent_id) + base + %w(published_at created_at private_space parent_id) end def self.ransackable_associations(_auth_object = nil) diff --git a/decidim-assemblies/spec/system/admin/admin_filters_assemblies_spec.rb b/decidim-assemblies/spec/system/admin/admin_filters_assemblies_spec.rb new file mode 100644 index 0000000000000..a8c5ec0574306 --- /dev/null +++ b/decidim-assemblies/spec/system/admin/admin_filters_assemblies_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Admin sorting assemblies" do + let(:organization) { create(:organization) } + let(:user) { create(:user, :admin, :confirmed, organization:) } + + let!(:old_assembly) { create(:assembly, title: { en: "Old assembly" }, created_at: 3.weeks.ago, organization:) } + let!(:recent_assembly) { create(:assembly, title: { en: "Recent assembly" }, created_at: 1.day.ago, organization:) } + let!(:newest_assembly) { create(:assembly, title: { en: "Newest assembly" }, created_at: Time.current, organization:) } + + before do + switch_to_host(organization.host) + login_as user, scope: :user + visit decidim_admin_assemblies.assemblies_path + end + + context "when sorting assemblies by their creation" do + it "sorts by created_at descending by default" do + within "table thead" do + click_link "Created at" + end + + titles = page.all("table tbody tr td:first-child") + expect(titles[0].text).to include("Newest assembly") + expect(titles[1].text).to include("Recent assembly") + expect(titles[2].text).to include("Old assembly") + end + + it "sorts by created_at ascending when clicked again" do + within "table thead" do + click_link "Created at" + click_link "Created at" + end + + titles = page.all("table tbody tr td:first-child") + expect(titles[0].text).to include("Old assembly") + expect(titles[1].text).to include("Recent assembly") + expect(titles[2].text).to include("Newest assembly") + end + end +end diff --git a/decidim-conferences/app/models/decidim/conference.rb b/decidim-conferences/app/models/decidim/conference.rb index b48faa15ccbaf..ae978d9d85525 100644 --- a/decidim-conferences/app/models/decidim/conference.rb +++ b/decidim-conferences/app/models/decidim/conference.rb @@ -157,7 +157,7 @@ def self.ransackable_attributes(auth_object = nil) return base unless auth_object&.admin? - base + %w(published_at) + base + %w(published_at created_at) end def self.ransackable_associations(_auth_object = nil) diff --git a/decidim-conferences/spec/system/admin/admin_filters_conferences_spec.rb b/decidim-conferences/spec/system/admin/admin_filters_conferences_spec.rb new file mode 100644 index 0000000000000..e4ba765868f31 --- /dev/null +++ b/decidim-conferences/spec/system/admin/admin_filters_conferences_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Admin sorting conferences" do + let(:organization) { create(:organization) } + let(:user) { create(:user, :admin, :confirmed, organization:) } + + let!(:old_conference) { create(:conference, title: { en: "Old conference" }, created_at: 3.weeks.ago, organization:) } + let!(:recent_conference) { create(:conference, title: { en: "Recent conference" }, created_at: 1.day.ago, organization:) } + let!(:newest_conference) { create(:conference, title: { en: "Newest conference" }, created_at: Time.current, organization:) } + + before do + switch_to_host(organization.host) + login_as user, scope: :user + visit decidim_admin_conferences.conferences_path + end + + context "when sorting conferences by their creation" do + it "sorts by created_at descending by default" do + within "table thead" do + click_link "Created at" + end + + titles = page.all("table tbody tr td:first-child") + expect(titles[0].text).to include("Newest conference") + expect(titles[1].text).to include("Recent conference") + expect(titles[2].text).to include("Old conference") + end + + it "sorts by created_at ascending when clicked again" do + within "table thead" do + click_link "Created at" + click_link "Created at" + end + + titles = page.all("table tbody tr td:first-child") + expect(titles[0].text).to include("Old conference") + expect(titles[1].text).to include("Recent conference") + expect(titles[2].text).to include("Newest conference") + end + end +end diff --git a/decidim-initiatives/app/models/decidim/initiative.rb b/decidim-initiatives/app/models/decidim/initiative.rb index df2118eb55a85..d9a58a9444e43 100644 --- a/decidim-initiatives/app/models/decidim/initiative.rb +++ b/decidim-initiatives/app/models/decidim/initiative.rb @@ -173,7 +173,7 @@ def self.ransackable_attributes(auth_object = nil) return base unless auth_object&.admin? - base + %w(published_at state decidim_area_id type_id) + base + %w(published_at created_at state decidim_area_id type_id) end def self.ransackable_associations(_auth_object = nil) diff --git a/decidim-initiatives/spec/system/admin/admin_filters_initiatives_spec.rb b/decidim-initiatives/spec/system/admin/admin_filters_initiatives_spec.rb new file mode 100644 index 0000000000000..8cae3b71802a9 --- /dev/null +++ b/decidim-initiatives/spec/system/admin/admin_filters_initiatives_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Admin sorting initiatives" do + let(:organization) { create(:organization) } + let(:user) { create(:user, :admin, :confirmed, organization:) } + + let!(:old_initiative) { create(:initiative, title: { en: "Old initiative" }, created_at: 3.weeks.ago, organization:) } + let!(:recent_initiative) { create(:initiative, title: { en: "Recent initiative" }, created_at: 1.day.ago, organization:) } + let!(:newest_initiative) { create(:initiative, title: { en: "Newest initiative" }, created_at: Time.current, organization:) } + + before do + switch_to_host(organization.host) + login_as user, scope: :user + visit decidim_admin_initiatives.initiatives_path + end + + context "when sorting initiatives by their creation" do + it "sorts by created_at descending by default" do + within "table thead" do + click_link "Created at" + end + + titles = page.all("table tbody tr td:first-child") + expect(titles[0].text).to include("Newest initiative") + expect(titles[1].text).to include("Recent initiative") + expect(titles[2].text).to include("Old initiative") + end + + it "sorts by created_at ascending when clicked again" do + within "table thead" do + click_link "Created at" + click_link "Created at" + end + + titles = page.all("table tbody tr td:first-child") + expect(titles[0].text).to include("Old initiative") + expect(titles[1].text).to include("Recent initiative") + expect(titles[2].text).to include("Newest initiative") + end + end +end diff --git a/decidim-participatory_processes/app/models/decidim/participatory_process.rb b/decidim-participatory_processes/app/models/decidim/participatory_process.rb index f612e1d16a28b..a25f3100f0453 100644 --- a/decidim-participatory_processes/app/models/decidim/participatory_process.rb +++ b/decidim-participatory_processes/app/models/decidim/participatory_process.rb @@ -216,7 +216,7 @@ def self.ransackable_attributes(auth_object = nil) base = %w(title short_description description id) return base unless auth_object&.admin? - base + %w(private_space published_at decidim_participatory_process_group_id) + base + %w(private_space published_at created_at decidim_participatory_process_group_id) end def self.ransackable_associations(_auth_object = nil) diff --git a/decidim-participatory_processes/spec/system/admin/admin_filters_participatory_process_spec.rb b/decidim-participatory_processes/spec/system/admin/admin_filters_participatory_process_spec.rb new file mode 100644 index 0000000000000..2992ccb8ecb5c --- /dev/null +++ b/decidim-participatory_processes/spec/system/admin/admin_filters_participatory_process_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Admin sorting participatory processes" do + let(:organization) { create(:organization) } + let(:user) { create(:user, :admin, :confirmed, organization:) } + + let!(:old_process) { create(:participatory_process, title: { en: "Old Process" }, created_at: 3.weeks.ago, organization:) } + let!(:recent_process) { create(:participatory_process, title: { en: "Recent Process" }, created_at: 1.day.ago, organization:) } + let!(:newest_process) { create(:participatory_process, title: { en: "Newest Process" }, created_at: Time.current, organization:) } + + before do + switch_to_host(organization.host) + login_as user, scope: :user + visit decidim_admin_participatory_processes.participatory_processes_path + end + + context "when sorting processes by their creation" do + it "sorts by created_at descending by default" do + within "table thead" do + click_link "Created at" + end + + titles = page.all("table tbody tr td:first-child") + expect(titles[0].text).to include("Newest Process") + expect(titles[1].text).to include("Recent Process") + expect(titles[2].text).to include("Old Process") + end + + it "sorts by created_at ascending when clicked again" do + within "table thead" do + click_link "Created at" + click_link "Created at" + end + + titles = page.all("table tbody tr td:first-child") + expect(titles[0].text).to include("Old Process") + expect(titles[1].text).to include("Recent Process") + expect(titles[2].text).to include("Newest Process") + end + end +end From 243d57353e355b7790123cc28d0a51a044d74e51 Mon Sep 17 00:00:00 2001 From: Leo Storey <123873192+Ginger-Leo@users.noreply.github.com> Date: Wed, 17 Dec 2025 23:40:13 +0200 Subject: [PATCH 026/116] Fix focus trap in modal dialog for sharing (#15807) Co-authored-by: Leo --- decidim-core/app/cells/decidim/share_widget/modal.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/decidim-core/app/cells/decidim/share_widget/modal.erb b/decidim-core/app/cells/decidim/share_widget/modal.erb index 61a0573556414..1c94de87b2df8 100644 --- a/decidim-core/app/cells/decidim/share_widget/modal.erb +++ b/decidim-core/app/cells/decidim/share_widget/modal.erb @@ -1,6 +1,6 @@ <%= decidim_modal id: "socialShare", class: "share-modal" do %>
-

<%= t("share", scope: "decidim.shared.share_modal") %>

+

<%= t("share", scope: "decidim.shared.share_modal") %>

From b16ed4e5382fba2acab8c7c70d4760100cba6d5a Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Thu, 18 Dec 2025 17:41:21 +0200 Subject: [PATCH 027/116] Add errors pages and more examples to API (#15814) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor API documentation * Fix typos * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review * Apply suggestions from code review * Apply suggestions from code review Co-authored-by: Andrés Pereira de Lucena * Apply suggestions from code review Co-authored-by: Andrés Pereira de Lucena * Apply more review recommendations * Apply more review recommendations - error pages * Apply suggestions from code review Co-authored-by: Andrés Pereira de Lucena --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Andrés Pereira de Lucena --- docs/modules/develop/pages/api/index.adoc | 23 +++++ .../api/reference/components/debates.adoc | 9 ++ .../reference/components/debates/close.adoc | 3 + .../reference/components/debates/create.adoc | 3 + .../reference/components/debates/update.adoc | 3 + .../api/reference/components/meetings.adoc | 10 +++ .../reference/components/meetings/close.adoc | 3 + .../reference/components/meetings/create.adoc | 3 + .../reference/components/meetings/update.adoc | 3 + .../components/meetings/withdraw.adoc | 3 + .../api/reference/components/proposals.adoc | 86 ++----------------- .../components/proposals/answer.adoc | 82 ++++++++++++++++++ .../components/proposals/create.adoc | 3 + .../components/proposals/update.adoc | 3 + .../reference/components/proposals/vote.adoc | 3 + .../components/proposals/withdraw.adoc | 3 + .../develop/pages/api/reference/errors.adoc | 22 +++++ .../errors/attribute_validation_error.adoc | 38 ++++++++ .../errors/invalid_locale_error.adoc | 30 +++++++ .../api/reference/errors/locale_error.adoc | 30 +++++++ .../pages/api/reference/errors/not_found.adoc | 30 +++++++ .../reference/errors/permission_not_set.adoc | 46 ++++++++++ .../reference/errors/unauthorized_field.adoc | 30 +++++++ .../errors/unauthorized_mutation.adoc | 30 +++++++ .../reference/errors/unauthorized_object.adoc | 30 +++++++ .../reference/errors/validation_error.adoc | 30 +++++++ 26 files changed, 481 insertions(+), 78 deletions(-) create mode 100644 docs/modules/develop/pages/api/reference/components/debates.adoc create mode 100644 docs/modules/develop/pages/api/reference/components/debates/close.adoc create mode 100644 docs/modules/develop/pages/api/reference/components/debates/create.adoc create mode 100644 docs/modules/develop/pages/api/reference/components/debates/update.adoc create mode 100644 docs/modules/develop/pages/api/reference/components/meetings.adoc create mode 100644 docs/modules/develop/pages/api/reference/components/meetings/close.adoc create mode 100644 docs/modules/develop/pages/api/reference/components/meetings/create.adoc create mode 100644 docs/modules/develop/pages/api/reference/components/meetings/update.adoc create mode 100644 docs/modules/develop/pages/api/reference/components/meetings/withdraw.adoc create mode 100644 docs/modules/develop/pages/api/reference/components/proposals/answer.adoc create mode 100644 docs/modules/develop/pages/api/reference/components/proposals/create.adoc create mode 100644 docs/modules/develop/pages/api/reference/components/proposals/update.adoc create mode 100644 docs/modules/develop/pages/api/reference/components/proposals/vote.adoc create mode 100644 docs/modules/develop/pages/api/reference/components/proposals/withdraw.adoc create mode 100644 docs/modules/develop/pages/api/reference/errors.adoc create mode 100644 docs/modules/develop/pages/api/reference/errors/attribute_validation_error.adoc create mode 100644 docs/modules/develop/pages/api/reference/errors/invalid_locale_error.adoc create mode 100644 docs/modules/develop/pages/api/reference/errors/locale_error.adoc create mode 100644 docs/modules/develop/pages/api/reference/errors/not_found.adoc create mode 100644 docs/modules/develop/pages/api/reference/errors/permission_not_set.adoc create mode 100644 docs/modules/develop/pages/api/reference/errors/unauthorized_field.adoc create mode 100644 docs/modules/develop/pages/api/reference/errors/unauthorized_mutation.adoc create mode 100644 docs/modules/develop/pages/api/reference/errors/unauthorized_object.adoc create mode 100644 docs/modules/develop/pages/api/reference/errors/validation_error.adoc diff --git a/docs/modules/develop/pages/api/index.adoc b/docs/modules/develop/pages/api/index.adoc index 0396de69388f4..be86a699988d0 100644 --- a/docs/modules/develop/pages/api/index.adoc +++ b/docs/modules/develop/pages/api/index.adoc @@ -48,10 +48,33 @@ From now on, we will skip the "query" keyword for the purpose of readability. Yo ** xref:develop:api/authentication.adoc[Authentication] ** xref:develop:api/core-concepts.adoc[Core concepts] +** xref:develop:api/reference/errors.adoc[Error Handling] +*** xref:develop:api/reference/errors/permission_not_set.adoc[Permission Not Set Error - PERMISSION_NOT_SET_ERROR] +*** xref:develop:api/reference/errors/unauthorized_field.adoc[Unauthorized Field Error - UNAUTHORIZED_FIELD_ERROR] +*** xref:develop:api/reference/errors/unauthorized_mutation.adoc[Unauthorized Mutation Error - MUTATION_NOT_AUTHORIZED_ERROR] +*** xref:develop:api/reference/errors/not_found.adoc[Not Found Error - NOT_FOUND_ERROR] +*** xref:develop:api/reference/errors/validation_error.adoc[Validation Error - VALIDATION_ERROR] +*** xref:develop:api/reference/errors/attribute_validation_error.adoc[Attribute Validation Error - ATTRIBUTE_VALIDATION_ERROR] +*** xref:develop:api/reference/errors/locale_error.adoc[Locale Error - LOCALE_ERROR] +*** xref:develop:api/reference/errors/invalid_locale_error.adoc[Invalid Locale Error - INVALID_LOCALE_ERROR] ** API reference by module *** Spaces *** Components +**** xref:develop:api/reference/components/debates.adoc[Debates] +***** xref:develop:api/reference/components/debates/create.adoc[Create Debate] +***** xref:develop:api/reference/components/debates/update.adoc[Update Debate] +***** xref:develop:api/reference/components/debates/close.adoc[Close Debate] +**** xref:develop:api/reference/components/meetings.adoc[Meetings] +***** xref:develop:api/reference/components/meetings/create.adoc[Create Meeting] +***** xref:develop:api/reference/components/meetings/update.adoc[Update Meeting] +***** xref:develop:api/reference/components/meetings/withdraw.adoc[Withdraw Meeting] +***** xref:develop:api/reference/components/meetings/close.adoc[Close Meeting] **** xref:develop:api/reference/components/proposals.adoc[Proposals] +***** xref:develop:api/reference/components/proposals/create.adoc[Create Proposal] +***** xref:develop:api/reference/components/proposals/update.adoc[Update Proposal] +***** xref:develop:api/reference/components/proposals/withdraw.adoc[Withdraw Proposal] +***** xref:develop:api/reference/components/proposals/vote.adoc[Vote Proposal] +***** xref:develop:api/reference/components/proposals/answer.adoc[Answer Proposal] == Usage limits diff --git a/docs/modules/develop/pages/api/reference/components/debates.adoc b/docs/modules/develop/pages/api/reference/components/debates.adoc new file mode 100644 index 0000000000000..5bfb0957be4fb --- /dev/null +++ b/docs/modules/develop/pages/api/reference/components/debates.adoc @@ -0,0 +1,9 @@ += Debates + +In the debates module we are providing a few mutations that we find useful for day-to-day management of Decidim instance outside the admin. + +== For user/participants roles + +* xref:develop:api/reference/components/debates/create.adoc[Create Debate] +* xref:develop:api/reference/components/debates/update.adoc[Update Debate] +* xref:develop:api/reference/components/debates/close.adoc[Close Debate] diff --git a/docs/modules/develop/pages/api/reference/components/debates/close.adoc b/docs/modules/develop/pages/api/reference/components/debates/close.adoc new file mode 100644 index 0000000000000..4077bf21e1ef1 --- /dev/null +++ b/docs/modules/develop/pages/api/reference/components/debates/close.adoc @@ -0,0 +1,3 @@ += Close Debate + +include::admin:partial$under-construction.adoc[] diff --git a/docs/modules/develop/pages/api/reference/components/debates/create.adoc b/docs/modules/develop/pages/api/reference/components/debates/create.adoc new file mode 100644 index 0000000000000..765d6f92e8817 --- /dev/null +++ b/docs/modules/develop/pages/api/reference/components/debates/create.adoc @@ -0,0 +1,3 @@ += Create Debate + +include::admin:partial$under-construction.adoc[] diff --git a/docs/modules/develop/pages/api/reference/components/debates/update.adoc b/docs/modules/develop/pages/api/reference/components/debates/update.adoc new file mode 100644 index 0000000000000..5e03f3a2c52ac --- /dev/null +++ b/docs/modules/develop/pages/api/reference/components/debates/update.adoc @@ -0,0 +1,3 @@ += Update Debate + +include::admin:partial$under-construction.adoc[] diff --git a/docs/modules/develop/pages/api/reference/components/meetings.adoc b/docs/modules/develop/pages/api/reference/components/meetings.adoc new file mode 100644 index 0000000000000..eb9f1079bfd4f --- /dev/null +++ b/docs/modules/develop/pages/api/reference/components/meetings.adoc @@ -0,0 +1,10 @@ += Meetings + +In the meetings module we are providing a few mutations that we find useful for day-to-day management of Decidim instance outside the admin. + +== For user/participants roles + +* xref:develop:api/reference/components/meetings/create.adoc[Create Meeting] +* xref:develop:api/reference/components/meetings/update.adoc[Update Meeting] +* xref:develop:api/reference/components/meetings/withdraw.adoc[Withdraw Meeting] +* xref:develop:api/reference/components/meetings/close.adoc[Close Meeting] diff --git a/docs/modules/develop/pages/api/reference/components/meetings/close.adoc b/docs/modules/develop/pages/api/reference/components/meetings/close.adoc new file mode 100644 index 0000000000000..aa9e1e9a12452 --- /dev/null +++ b/docs/modules/develop/pages/api/reference/components/meetings/close.adoc @@ -0,0 +1,3 @@ += Close Meeting + +include::admin:partial$under-construction.adoc[] diff --git a/docs/modules/develop/pages/api/reference/components/meetings/create.adoc b/docs/modules/develop/pages/api/reference/components/meetings/create.adoc new file mode 100644 index 0000000000000..da498b07c613a --- /dev/null +++ b/docs/modules/develop/pages/api/reference/components/meetings/create.adoc @@ -0,0 +1,3 @@ += Create Meeting + +include::admin:partial$under-construction.adoc[] diff --git a/docs/modules/develop/pages/api/reference/components/meetings/update.adoc b/docs/modules/develop/pages/api/reference/components/meetings/update.adoc new file mode 100644 index 0000000000000..ccad2ce3cd8d1 --- /dev/null +++ b/docs/modules/develop/pages/api/reference/components/meetings/update.adoc @@ -0,0 +1,3 @@ += Update Meeting + +include::admin:partial$under-construction.adoc[] diff --git a/docs/modules/develop/pages/api/reference/components/meetings/withdraw.adoc b/docs/modules/develop/pages/api/reference/components/meetings/withdraw.adoc new file mode 100644 index 0000000000000..492704d61d0e5 --- /dev/null +++ b/docs/modules/develop/pages/api/reference/components/meetings/withdraw.adoc @@ -0,0 +1,3 @@ += Withdraw Meeting + +include::admin:partial$under-construction.adoc[] diff --git a/docs/modules/develop/pages/api/reference/components/proposals.adoc b/docs/modules/develop/pages/api/reference/components/proposals.adoc index 60d14806c4c0c..4435663dd0a9d 100644 --- a/docs/modules/develop/pages/api/reference/components/proposals.adoc +++ b/docs/modules/develop/pages/api/reference/components/proposals.adoc @@ -1,85 +1,15 @@ = Proposals -== Answering Proposals +In the proposals module we are providing a few mutations that we find useful for day-to-day management of Decidim instance outside the admin. -Allows administrators to answer proposals with a status (accepted, rejected, or evaluating). +== For user/participants roles -This is an example on how to use the Answer Proposal mutation. Please refer to the GraphQL documentation for the available fields: +* xref:develop:api/reference/components/proposals/create.adoc[Create Proposal] +* xref:develop:api/reference/components/proposals/update.adoc[Update Proposal] +* xref:develop:api/reference/components/proposals/withdraw.adoc[Withdraw Proposal] +* xref:develop:api/reference/components/proposals/vote.adoc[Vote Proposal] +== For admin roles -- https://nightly.decidim.org/api/docs/input_object/answerinput/[Answer Input] -- https://nightly.decidim.org/api/docs/input_object/proposalattributes[Proposal Attributes] -- https://nightly.decidim.org/api/docs/object/proposal[Proposal Type] - -```graphql -mutation addProposalAnswer($componentId: ID!, $proposalId: ID!, $input: AnswerInput!){ - component(id: $componentId) { - ...on ProposalsMutation{ - proposal(id: $proposalId) { - answer(input: $input) { - // this is the proposal response - } - } - } - } -} -``` - -Example of submitted variables -```graphql -{ - "componentId": 9, - "proposalId": 2, - "input": { - "clientMutationId": "unique-client-id-123", - "attributes": { - "state": "accepted", - "cost": 10.0, - "answerContent": { - "en" => "Some answer in english", - "ca" => "Some answer in Catalan" - } - } - } -} -``` - -=== HTTP Request Example - -Using cURL to make the GraphQL request: - -```bash -curl -X POST https://your-decidim-instance.org/api \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer YOUR_API_TOKEN" \ - -d '{ - "query": "mutation addProposalAnswer($componentId: ID!, $proposalId: ID!, $input: AnswerInput!){ component(id: $componentId) { ...on ProposalsMutation{ proposal(id: $proposalId) { answer(input: $input) { /* this is the proposal response */ } } } } }", - "variables": { - "input": { - "componentId": 9, - "proposalId": 2, - "input": { - "clientMutationId": "unique-client-id-123", - "attributes": { - "state": "accepted", - "cost": 10.0, - "answerContent": { - "en" => "Some answer in english", - "ca" => "Some answer in Catalan" - } - } - } - } - } - }' -``` - -=== Error Handling - -When you are not authorized, or when you are not authenticated, the server will respond with a simple: -```graphql -{ - "data": null -} -``` +* xref:develop:api/reference/components/proposals/answer.adoc[Answer Proposal] diff --git a/docs/modules/develop/pages/api/reference/components/proposals/answer.adoc b/docs/modules/develop/pages/api/reference/components/proposals/answer.adoc new file mode 100644 index 0000000000000..0ef6d67174f8f --- /dev/null +++ b/docs/modules/develop/pages/api/reference/components/proposals/answer.adoc @@ -0,0 +1,82 @@ += Answer Proposal + +Allows administrators to answer proposals with a status (accepted, rejected, or evaluating). + +This is an example on how to use the `addProposalAnswer` mutation. Please refer to the GraphQL documentation for the available fields: + + +- https://nightly.decidim.org/api/docs/input_object/answerinput/[Answer Input] +- https://nightly.decidim.org/api/docs/input_object/proposalattributes[Proposal Attributes] +- https://nightly.decidim.org/api/docs/object/proposal[Proposal Type] + +```graphql +mutation addProposalAnswer($componentId: ID!, $proposalId: ID!, $input: AnswerInput!){ + component(id: $componentId) { + ...on ProposalsMutation{ + proposal(id: $proposalId) { + answer(input: $input) { + // this is the proposal response + } + } + } + } +} +``` + +Example of submitted variables +```graphql +{ + "componentId": 9, + "proposalId": 2, + "input": { + "clientMutationId": "unique-client-id-123", + "attributes": { + "state": "accepted", + "cost": 10.0, + "answerContent": { + "en": "Some answer in English", + "ca": "Some answer in Catalan" + } + } + } +} +``` + +=== HTTP Request Example + +Using cURL to make the GraphQL request: + +```bash +curl -X POST https://your-decidim-instance.org/api \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_API_TOKEN" \ + -d '{ + "query": "mutation addProposalAnswer($componentId: ID!, $proposalId: ID!, $input: AnswerInput!){ component(id: $componentId) { ...on ProposalsMutation{ proposal(id: $proposalId) { answer(input: $input) { /* this is the proposal response */ } } } } }", + "variables": { + "input": { + "componentId": 9, + "proposalId": 2, + "input": { + "clientMutationId": "unique-client-id-123", + "attributes": { + "state": "accepted", + "cost": 10.0, + "answerContent": { + "en": "Some answer in English", + "ca": "Some answer in Catalan" + } + } + } + } + } + }' +``` + +=== Error Handling + +The most frequent errors that can be generated by this mutation are as follows: + + +* xref:develop:api/reference/errors/unauthorized_object.adoc[Unauthorized Object Error - UNAUTHORIZED_OBJECT_ERROR] +* xref:develop:api/reference/errors/not_found.adoc[Not Found Error - NOT_FOUND_ERROR] +* xref:develop:api/reference/errors/attribute_validation_error.adoc[Attribute Validation Error - ATTRIBUTE_VALIDATION_ERROR] diff --git a/docs/modules/develop/pages/api/reference/components/proposals/create.adoc b/docs/modules/develop/pages/api/reference/components/proposals/create.adoc new file mode 100644 index 0000000000000..d2998e60afc87 --- /dev/null +++ b/docs/modules/develop/pages/api/reference/components/proposals/create.adoc @@ -0,0 +1,3 @@ += Create Proposal + +include::admin:partial$under-construction.adoc[] diff --git a/docs/modules/develop/pages/api/reference/components/proposals/update.adoc b/docs/modules/develop/pages/api/reference/components/proposals/update.adoc new file mode 100644 index 0000000000000..733e1987aa225 --- /dev/null +++ b/docs/modules/develop/pages/api/reference/components/proposals/update.adoc @@ -0,0 +1,3 @@ += Update Proposal + +include::admin:partial$under-construction.adoc[] diff --git a/docs/modules/develop/pages/api/reference/components/proposals/vote.adoc b/docs/modules/develop/pages/api/reference/components/proposals/vote.adoc new file mode 100644 index 0000000000000..ab6a8328fc8b3 --- /dev/null +++ b/docs/modules/develop/pages/api/reference/components/proposals/vote.adoc @@ -0,0 +1,3 @@ += Vote Proposal + +include::admin:partial$under-construction.adoc[] diff --git a/docs/modules/develop/pages/api/reference/components/proposals/withdraw.adoc b/docs/modules/develop/pages/api/reference/components/proposals/withdraw.adoc new file mode 100644 index 0000000000000..8b7aab440e85b --- /dev/null +++ b/docs/modules/develop/pages/api/reference/components/proposals/withdraw.adoc @@ -0,0 +1,3 @@ += Withdraw Proposal + +include::admin:partial$under-construction.adoc[] diff --git a/docs/modules/develop/pages/api/reference/errors.adoc b/docs/modules/develop/pages/api/reference/errors.adoc new file mode 100644 index 0000000000000..0acfd284f582f --- /dev/null +++ b/docs/modules/develop/pages/api/reference/errors.adoc @@ -0,0 +1,22 @@ += Error Handling for the API + +Currently, in Decidim we are using some exceptions to handle API responses, aiming to serve both input (receiving data through API) and output (sending data through API). + +At the moment there are 3 categories of exceptions that are being handled: + +== Authorization errors + +* xref:develop:api/reference/errors/permission_not_set.adoc[Permission Not Set Error - PERMISSION_NOT_SET_ERROR], is triggered when there is a problem with the authorization layer. +* xref:develop:api/reference/errors/unauthorized_object.adoc[Unauthorized Object Error - UNAUTHORIZED_OBJECT_ERROR], is triggered when the requesting user does not have access to a certain resource. +* xref:develop:api/reference/errors/unauthorized_field.adoc[Unauthorized Field Error - UNAUTHORIZED_FIELD_ERROR], is triggered when the requesting user does not have access to a certain field or resource. +* xref:develop:api/reference/errors/unauthorized_mutation.adoc[Unauthorized Mutation Error - MUTATION_NOT_AUTHORIZED_ERROR], is triggered when the requesting user tries to access a mutation that cannot be accessed. + +== Validation errors +* xref:develop:api/reference/errors/not_found.adoc[Not Found Error - NOT_FOUND_ERROR], is triggered when the requested record does not exist in database, or is in a state where you should not have access to it (moderated, belongs to an unpublished component, belongs to a private process where the requesting user does not have access) +* xref:develop:api/reference/errors/validation_error.adoc[Validation Error - VALIDATION_ERROR], is triggered when the requesting user performs an action that contains an error. This is equivalent to the flash messages displayed on the Website. +* xref:develop:api/reference/errors/attribute_validation_error.adoc[Attribute Validation Error - ATTRIBUTE_VALIDATION_ERROR], is triggered when the requesting user submits a request to alter a resource that has an error. This is equivalent to the validation errors in the HTTP forms. + +== Internationalization errors + +* xref:develop:api/reference/errors/locale_error.adoc[Locale Error - LOCALE_ERROR], is triggered when there is a problem with the internationalization, for example, a missing translation, a missing interpolation to the output string +* xref:develop:api/reference/errors/invalid_locale_error.adoc[Invalid Locale Error - INVALID_LOCALE_ERROR], is triggered when the provided locale is invalid or not supported by the system, for example when an unknown locale code is used or a locale that is not enabled for the current organization diff --git a/docs/modules/develop/pages/api/reference/errors/attribute_validation_error.adoc b/docs/modules/develop/pages/api/reference/errors/attribute_validation_error.adoc new file mode 100644 index 0000000000000..01d2f5121e41e --- /dev/null +++ b/docs/modules/develop/pages/api/reference/errors/attribute_validation_error.adoc @@ -0,0 +1,38 @@ += Attribute Validation Error - ATTRIBUTE_VALIDATION_ERROR + +This error is triggered when a user submits invalid data into a mutation form. This error is the equivalent of the form validation errors seen in the HTTP version + +The JSON response structure is: +[source, json] +---- +{ + "errors": [ + { + "message": [ + { + "path": ["attributes", "title"], + "message": "cannot be blank" + }, + { + "path": ["attributes", "title"], + "message": "is too short (under 15 characters)"} + ], + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "createProposal" + ], + "extensions": { + "code": "ATTRIBUTE_VALIDATION_ERROR" + } + } + ], + "data": { + "createProposal": null + } +} +---- diff --git a/docs/modules/develop/pages/api/reference/errors/invalid_locale_error.adoc b/docs/modules/develop/pages/api/reference/errors/invalid_locale_error.adoc new file mode 100644 index 0000000000000..bba907120f4ec --- /dev/null +++ b/docs/modules/develop/pages/api/reference/errors/invalid_locale_error.adoc @@ -0,0 +1,30 @@ += Invalid Locale Error - INVALID_LOCALE_ERROR + +This error is issued when the user submits or requests content in a language that is not supported by the platform. + +The JSON response structure is: +[source, json] +---- +{ + "errors": [ + { + "message": "Invalid locale provided", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "createProposal" + ], + "extensions": { + "code": "INVALID_LOCALE_ERROR" + } + } + ], + "data": { + "createProposal": null + } +} +---- diff --git a/docs/modules/develop/pages/api/reference/errors/locale_error.adoc b/docs/modules/develop/pages/api/reference/errors/locale_error.adoc new file mode 100644 index 0000000000000..3ce14da998c19 --- /dev/null +++ b/docs/modules/develop/pages/api/reference/errors/locale_error.adoc @@ -0,0 +1,30 @@ += Locale Error - LOCALE_ERROR + +This error is triggered when the requested language has any internationalization errors, like missing translation string, bad variable replacement (interpolation), or any other I18n errors. + +The JSON response structure is: +[source, json] +---- +{ + "errors": [ + { + "message": "There was an error while internally handling i18n data", + "locations": [ + { + "line": 2, + "column": 39 + } + ], + "path": [ + "createProposal" + ], + "extensions": { + "code": "LOCALE_ERROR" + } + } + ], + "data": { + "createProposal": null + } +} +---- diff --git a/docs/modules/develop/pages/api/reference/errors/not_found.adoc b/docs/modules/develop/pages/api/reference/errors/not_found.adoc new file mode 100644 index 0000000000000..84f64a4017fc0 --- /dev/null +++ b/docs/modules/develop/pages/api/reference/errors/not_found.adoc @@ -0,0 +1,30 @@ += Not Found Error - NOT_FOUND_ERROR + +This error is triggered when the requested record does not exist in the database, or is in a state where you should not have access to it (moderated, belongs to an unpublished component, belongs to a private process where the requesting user does not have access). This is equivalent to 404 HTTP Status code. + +The JSON response structure is: +[source, json] +---- +{ + "errors": [ + { + "message": "ParticipatoryProcess not found", + "extensions": { + "code": "NOT_FOUND_ERROR" + }, + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "participatoryProcess" + ] + } + ], + "data": { + "participatoryProcess": null + } +} +---- diff --git a/docs/modules/develop/pages/api/reference/errors/permission_not_set.adoc b/docs/modules/develop/pages/api/reference/errors/permission_not_set.adoc new file mode 100644 index 0000000000000..aa1845178b38e --- /dev/null +++ b/docs/modules/develop/pages/api/reference/errors/permission_not_set.adoc @@ -0,0 +1,46 @@ += Permission Not Set Error - PERMISSION_NOT_SET_ERROR + +This is an error that should not be seen by end users, as this error is being fired when a request passes through a managed permission handler. + +The JSON response structure is: +[source, json] +---- +{ + "errors": [ + { + "message": "Permission has not been set for this Project", + "locations": [ + { + "line": 3, + "column": 5 + } + ], + "path": [ + "participatoryProcess", + "components", + 0, + "budget", + "projects", + 0 + ], + "extensions": { + "code": "PERMISSION_NOT_SET_ERROR" + } + } + ], + "data": { + "participatoryProcess": { + "components": [ + { + "id": "3", + "budget": { + "projects": [ + null + ] + } + } + ] + } + } +} +---- diff --git a/docs/modules/develop/pages/api/reference/errors/unauthorized_field.adoc b/docs/modules/develop/pages/api/reference/errors/unauthorized_field.adoc new file mode 100644 index 0000000000000..d159a227e94d2 --- /dev/null +++ b/docs/modules/develop/pages/api/reference/errors/unauthorized_field.adoc @@ -0,0 +1,30 @@ += Unauthorized Field Error - UNAUTHORIZED_FIELD_ERROR + +This error is triggered when the requesting user does not have access to a certain field, or a certain resource. + +The JSON response structure is: +[source, json] +---- +{ + "errors": [ + { + "message": "You cannot view or edit answer field on Answer because you do not have permission", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "answer" + ], + "extensions": { + "code": "UNAUTHORIZED_FIELD_ERROR" + } + } + ], + "data": { + "answer": null + } +} +---- diff --git a/docs/modules/develop/pages/api/reference/errors/unauthorized_mutation.adoc b/docs/modules/develop/pages/api/reference/errors/unauthorized_mutation.adoc new file mode 100644 index 0000000000000..26ab12e6cda9b --- /dev/null +++ b/docs/modules/develop/pages/api/reference/errors/unauthorized_mutation.adoc @@ -0,0 +1,30 @@ += Unauthorized Mutation Error - MUTATION_NOT_AUTHORIZED_ERROR + +This error is triggered when a user submits a mutation request to a resource they do not have access to. + +The JSON response structure is: +[source, json] +---- +{ + "errors": [ + { + "message": "You do not have permission to perform this mutation", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "createProposal" + ], + "extensions": { + "code": "MUTATION_NOT_AUTHORIZED_ERROR" + } + } + ], + "data": { + "createProposal": null + } +} +---- diff --git a/docs/modules/develop/pages/api/reference/errors/unauthorized_object.adoc b/docs/modules/develop/pages/api/reference/errors/unauthorized_object.adoc new file mode 100644 index 0000000000000..c63be015ad51e --- /dev/null +++ b/docs/modules/develop/pages/api/reference/errors/unauthorized_object.adoc @@ -0,0 +1,30 @@ += Unauthorized Object Error - UNAUTHORIZED_OBJECT_ERROR + +This error is triggered when the requesting user does not have access to a certain resource. + +The JSON response structure is: +[source, json] +---- +{ + "errors": [ + { + "message": "You cannot view or edit this Result because you do not have permissions", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "vote" + ], + "extensions": { + "code": "UNAUTHORIZED_OBJECT_ERROR" + } + } + ], + "data": { + "result": null + } +} +---- diff --git a/docs/modules/develop/pages/api/reference/errors/validation_error.adoc b/docs/modules/develop/pages/api/reference/errors/validation_error.adoc new file mode 100644 index 0000000000000..d93511c4c60b1 --- /dev/null +++ b/docs/modules/develop/pages/api/reference/errors/validation_error.adoc @@ -0,0 +1,30 @@ += Validation Error - VALIDATION_ERROR + +This error is triggered when the requesting user performs an action that contains an error, for example, when the user tries to publish a resource that is already published. This is equivalent to the flash messages displayed on the Website. + +The JSON response structure is: +[source, json] +---- +{ + "errors": [ + { + "message": "There was a problem voting the proposal.", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "vote" + ], + "extensions": { + "code": "VALIDATION_ERROR" + } + } + ], + "data": { + "vote": null + } +} +---- From 40c07d9c4328770775e4d3266fbc69bd07e9e65e Mon Sep 17 00:00:00 2001 From: stephanie rousset Date: Thu, 18 Dec 2025 16:47:30 +0100 Subject: [PATCH 028/116] fix: add p tag for reference in assembly show --- .../app/views/decidim/assemblies/assemblies/show.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/decidim-assemblies/app/views/decidim/assemblies/assemblies/show.html.erb b/decidim-assemblies/app/views/decidim/assemblies/assemblies/show.html.erb index 6b9799c119e63..fbc902c147981 100644 --- a/decidim-assemblies/app/views/decidim/assemblies/assemblies/show.html.erb +++ b/decidim-assemblies/app/views/decidim/assemblies/assemblies/show.html.erb @@ -34,6 +34,6 @@ edit_link(
- <%= resource_reference(current_participatory_space) %> +

<%= resource_reference(current_participatory_space) %>

From 2fbd1f81b5d2929232a3e29ef8196f03427a20ee Mon Sep 17 00:00:00 2001 From: stephanie rousset Date: Thu, 18 Dec 2025 16:48:23 +0100 Subject: [PATCH 029/116] style: update style to be applied on p instead of span --- decidim-core/app/packs/stylesheets/decidim/_modal_update.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/decidim-core/app/packs/stylesheets/decidim/_modal_update.scss b/decidim-core/app/packs/stylesheets/decidim/_modal_update.scss index e60887cb602dd..4fdb0c7779d99 100644 --- a/decidim-core/app/packs/stylesheets/decidim/_modal_update.scss +++ b/decidim-core/app/packs/stylesheets/decidim/_modal_update.scss @@ -127,7 +127,7 @@ @apply w-full rounded bg-background flex items-center justify-center py-4 [&_img]:object-cover [&_img]:h-[200px]; } - span { + p { @apply text-sm text-gray-2 mx-auto w-full break-all mb-2; } } From b72051b9394e9db24e9d9bf8b0718103149640b1 Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Fri, 19 Dec 2025 08:45:31 +0200 Subject: [PATCH 030/116] Move FormFactory to be accessible from API (#15819) * Move FormFactory to be accessible from API * Add missing method * Apply review recommendation --- .../lib/decidim/api/types/base_mutation.rb | 16 ++++++++++++++++ decidim-core/lib/decidim/core.rb | 1 + .../concerns => lib}/decidim/form_factory.rb | 0 .../concerns => lib}/form_factory_spec.rb | 0 4 files changed, 17 insertions(+) rename decidim-core/{app/controllers/concerns => lib}/decidim/form_factory.rb (100%) rename decidim-core/spec/{controllers/concerns => lib}/form_factory_spec.rb (100%) diff --git a/decidim-api/lib/decidim/api/types/base_mutation.rb b/decidim-api/lib/decidim/api/types/base_mutation.rb index 2a13fe66dde25..a6970dc53dc04 100644 --- a/decidim-api/lib/decidim/api/types/base_mutation.rb +++ b/decidim-api/lib/decidim/api/types/base_mutation.rb @@ -5,6 +5,7 @@ module Api module Types class BaseMutation < GraphQL::Schema::RelayClassicMutation include Decidim::Api::GraphqlPermissions + include Decidim::FormFactory object_class BaseObject field_class Types::BaseField @@ -12,6 +13,13 @@ class BaseMutation < GraphQL::Schema::RelayClassicMutation required_scopes "api:read", "api:write" + def set_locale(locale:, toggle_translations:) + raise I18n::InvalidLocale, "#{locale} is not a valid locale" unless available_locales.include?(locale) + + I18n.locale = locale.presence + RequestStore.store[:toggle_machine_translations] = toggle_translations + end + def current_user context[:current_user] end @@ -23,6 +31,14 @@ def current_component def current_organization context[:current_organization] end + + def available_locales + if current_organization.present? + current_organization.available_locales + else + I18n.available_locales.map(&:to_s) + end + end end end end diff --git a/decidim-core/lib/decidim/core.rb b/decidim-core/lib/decidim/core.rb index d53969c309fa1..e59f148306552 100644 --- a/decidim-core/lib/decidim/core.rb +++ b/decidim-core/lib/decidim/core.rb @@ -132,6 +132,7 @@ module Decidim autoload :ApiResponseHelper, "decidim/api_response_helper" autoload :ResourceHelper, "decidim/resource_helper" autoload :TooltipHelper, "decidim/tooltip_helper" + autoload :FormFactory, "decidim/form_factory" module ParticipatorySpace autoload :HasMembers, "decidim/participatory_space/has_members" diff --git a/decidim-core/app/controllers/concerns/decidim/form_factory.rb b/decidim-core/lib/decidim/form_factory.rb similarity index 100% rename from decidim-core/app/controllers/concerns/decidim/form_factory.rb rename to decidim-core/lib/decidim/form_factory.rb diff --git a/decidim-core/spec/controllers/concerns/form_factory_spec.rb b/decidim-core/spec/lib/form_factory_spec.rb similarity index 100% rename from decidim-core/spec/controllers/concerns/form_factory_spec.rb rename to decidim-core/spec/lib/form_factory_spec.rb From c4faf2a693b3ac122d2201552b6bf543768f144f Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Fri, 19 Dec 2025 08:46:00 +0200 Subject: [PATCH 031/116] Add additional API exceptions (#15789) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor API to raise and catch Decidim::PermissionAction::PermissionNotSetError exceptions * Add Decidim::Api::Errors::MutationNotAuthorizedError * Add Decidim::Api::Errors::ValidationError * Add Decidim::Api::Errors::AttributeValidationError * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix spec * Add test example * Refactor file name * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply review recommendation * Fix spec * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * change the variable name * Add additional I18n related exceptions * Refactor error layer * Fix error class tokens * Fix the initiatives specs * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Running linters * Apply suggestions from code review Co-authored-by: Andrés Pereira de Lucena --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Andrés Pereira de Lucena --- decidim-api/config/locales/en.yml | 3 + .../api/errors/attribute_validation_error.rb | 73 ++++++++++++++++ .../api/errors/invalid_locale_error.rb | 14 ++++ .../lib/decidim/api/errors/locale_error.rb | 14 ++++ .../errors/mutation_not_authorized_error.rb | 14 ++++ .../lib/decidim/api/errors/not_found_error.rb | 3 +- .../api/errors/permission_not_set_error.rb | 3 +- .../api/errors/unauthorized_field_error.rb | 3 +- .../api/errors/unauthorized_object_error.rb | 3 +- .../decidim/api/errors/validation_error.rb | 13 +++ decidim-api/lib/decidim/api/schema.rb | 8 ++ .../lib/decidim/api/test/type_context.rb | 27 +++--- decidim-api/lib/decidim/api/types.rb | 7 +- .../errors/attribute_validation_error_spec.rb | 84 +++++++++++++++++++ decidim-dev/spec/types/errors_spec.rb | 63 ++++++++++++++ .../spec/types/initiative_type_spec.rb | 2 +- 16 files changed, 316 insertions(+), 18 deletions(-) create mode 100644 decidim-api/lib/decidim/api/errors/attribute_validation_error.rb create mode 100644 decidim-api/lib/decidim/api/errors/invalid_locale_error.rb create mode 100644 decidim-api/lib/decidim/api/errors/locale_error.rb create mode 100644 decidim-api/lib/decidim/api/errors/mutation_not_authorized_error.rb create mode 100644 decidim-api/lib/decidim/api/errors/validation_error.rb create mode 100644 decidim-api/spec/lib/decidim/api/errors/attribute_validation_error_spec.rb diff --git a/decidim-api/config/locales/en.yml b/decidim-api/config/locales/en.yml index 51d5113311583..f28fd3c1c73a9 100644 --- a/decidim-api/config/locales/en.yml +++ b/decidim-api/config/locales/en.yml @@ -3,7 +3,10 @@ en: decidim: api: errors: + invalid_locale: Invalid locale provided + locale_argument_error: There was an error while internally handling i18n data not_found: "%{type} not found" permission_not_set: Permission has not been set for this %{type} unauthorized_field: You cannot view or edit %{field} field on %{type} because you do not have permission + unauthorized_mutation: You do not have permission to perform this mutation unauthorized_object: You cannot view or edit this %{type} because you do not have permissions diff --git a/decidim-api/lib/decidim/api/errors/attribute_validation_error.rb b/decidim-api/lib/decidim/api/errors/attribute_validation_error.rb new file mode 100644 index 0000000000000..6a754974bb4cc --- /dev/null +++ b/decidim-api/lib/decidim/api/errors/attribute_validation_error.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +module Decidim + module Api + module Errors + class AttributeValidationError < GraphQL::ExecutionError + def initialize(messages, ast_node: nil, options: nil, extensions: nil) + @ast_node = ast_node + @options = options + @extensions = extensions + + @messages = messages + + message_str = + if messages.is_a?(ActiveModel::Errors) + messages.full_messages.join(", ") + elsif messages.is_a?(Array) + messages.map { |a| a[:message] }.join(", ") + else + messages.to_s + end + super(message_str) + end + + def to_h + hash = {} + if @messages.is_a?(ActiveModel::Errors) + hash["message"] = @messages.map do |error| + # This is the GraphQL argument which corresponds to the validation error: + local_path = ["attributes", error.attribute.to_s.camelize(:lower)] + { + path: local_path, + message: error.message + } + end + end + + hash["message"] = @messages if @messages.is_a?(Array) + + if ast_node + hash["locations"] = [ + { + "line" => ast_node.line, + "column" => ast_node.col + } + ] + end + + hash["path"] = path if path + + hash.merge!(options) if options + + if extensions + hash["extensions"] = extensions.transform_keys do |(key, value), ext| + ext[key.to_s] = value + end + end + + hash.merge!({ "extensions" => { "code" => "ATTRIBUTE_VALIDATION_ERROR" } }) + + hash + end + + def message + return @messages.full_messages.join(", ") if @messages.is_a?(ActiveModel::Errors) + return @messages.map { |a| a[:message] }.join(", ") if @messages.is_a?(Array) + + @messages.to_s + end + end + end + end +end diff --git a/decidim-api/lib/decidim/api/errors/invalid_locale_error.rb b/decidim-api/lib/decidim/api/errors/invalid_locale_error.rb new file mode 100644 index 0000000000000..3f7ed57395b5b --- /dev/null +++ b/decidim-api/lib/decidim/api/errors/invalid_locale_error.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Decidim + module Api + module Errors + # i18n-tasks-use t("decidim.api.errors.invalid_locale") + class InvalidLocaleError < GraphQL::ExecutionError + def to_h + super.merge({ "extensions" => { "code" => "INVALID_LOCALE_ERROR" } }) + end + end + end + end +end diff --git a/decidim-api/lib/decidim/api/errors/locale_error.rb b/decidim-api/lib/decidim/api/errors/locale_error.rb new file mode 100644 index 0000000000000..8302aa2a485b1 --- /dev/null +++ b/decidim-api/lib/decidim/api/errors/locale_error.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Decidim + module Api + module Errors + # i18n-tasks-use t("decidim.api.errors.locale_argument_error") + class LocaleError < GraphQL::ExecutionError + def to_h + super.merge({ "extensions" => { "code" => "LOCALE_ERROR" } }) + end + end + end + end +end diff --git a/decidim-api/lib/decidim/api/errors/mutation_not_authorized_error.rb b/decidim-api/lib/decidim/api/errors/mutation_not_authorized_error.rb new file mode 100644 index 0000000000000..19c513f8a6c30 --- /dev/null +++ b/decidim-api/lib/decidim/api/errors/mutation_not_authorized_error.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Decidim + module Api + module Errors + # i18n-tasks-use t("decidim.api.errors.unauthorized_mutation") + class MutationNotAuthorizedError < GraphQL::ExecutionError + def to_h + super.merge({ "extensions" => { "code" => "MUTATION_NOT_AUTHORIZED_ERROR" } }) + end + end + end + end +end diff --git a/decidim-api/lib/decidim/api/errors/not_found_error.rb b/decidim-api/lib/decidim/api/errors/not_found_error.rb index 1bb1133858e79..bca12968a291b 100644 --- a/decidim-api/lib/decidim/api/errors/not_found_error.rb +++ b/decidim-api/lib/decidim/api/errors/not_found_error.rb @@ -3,9 +3,10 @@ module Decidim module Api module Errors + # i18n-tasks-use t("decidim.api.errors.not_found") class NotFoundError < GraphQL::ExecutionError def to_h - super.merge({ "extensions" => { "code" => "NOT_FOUND" } }) + super.merge({ "extensions" => { "code" => "NOT_FOUND_ERROR" } }) end end end diff --git a/decidim-api/lib/decidim/api/errors/permission_not_set_error.rb b/decidim-api/lib/decidim/api/errors/permission_not_set_error.rb index 340947a119a3b..9242555a8e89f 100644 --- a/decidim-api/lib/decidim/api/errors/permission_not_set_error.rb +++ b/decidim-api/lib/decidim/api/errors/permission_not_set_error.rb @@ -3,9 +3,10 @@ module Decidim module Api module Errors + # i18n-tasks-use t("decidim.api.errors.permission_not_set") class PermissionNotSetError < GraphQL::ExecutionError def to_h - super.merge({ "extensions" => { "code" => "NO_PERMISSION_SET" } }) + super.merge({ "extensions" => { "code" => "PERMISSION_NOT_SET_ERROR" } }) end end end diff --git a/decidim-api/lib/decidim/api/errors/unauthorized_field_error.rb b/decidim-api/lib/decidim/api/errors/unauthorized_field_error.rb index 8f575408f0b36..713f5d4bde55c 100644 --- a/decidim-api/lib/decidim/api/errors/unauthorized_field_error.rb +++ b/decidim-api/lib/decidim/api/errors/unauthorized_field_error.rb @@ -3,9 +3,10 @@ module Decidim module Api module Errors + # i18n-tasks-use t("decidim.api.errors.unauthorized_field") class UnauthorizedFieldError < GraphQL::ExecutionError def to_h - super.merge({ "extensions" => { "code" => "NO_FIELD_PERMISSION" } }) + super.merge({ "extensions" => { "code" => "UNAUTHORIZED_FIELD_ERROR" } }) end end end diff --git a/decidim-api/lib/decidim/api/errors/unauthorized_object_error.rb b/decidim-api/lib/decidim/api/errors/unauthorized_object_error.rb index e39bd60ef5deb..8dff8305a6fc2 100644 --- a/decidim-api/lib/decidim/api/errors/unauthorized_object_error.rb +++ b/decidim-api/lib/decidim/api/errors/unauthorized_object_error.rb @@ -3,9 +3,10 @@ module Decidim module Api module Errors + # i18n-tasks-use t("decidim.api.errors.unauthorized_object") class UnauthorizedObjectError < GraphQL::ExecutionError def to_h - super.merge({ "extensions" => { "code" => "NO_OBJECT_PERMISSION" } }) + super.merge({ "extensions" => { "code" => "UNAUTHORIZED_OBJECT_ERROR" } }) end end end diff --git a/decidim-api/lib/decidim/api/errors/validation_error.rb b/decidim-api/lib/decidim/api/errors/validation_error.rb new file mode 100644 index 0000000000000..ead1bb3239509 --- /dev/null +++ b/decidim-api/lib/decidim/api/errors/validation_error.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Decidim + module Api + module Errors + class ValidationError < GraphQL::ExecutionError + def to_h + super.merge({ "extensions" => { "code" => "VALIDATION_ERROR" } }) + end + end + end + end +end diff --git a/decidim-api/lib/decidim/api/schema.rb b/decidim-api/lib/decidim/api/schema.rb index 13d5cd70b13f0..16903e19b452b 100644 --- a/decidim-api/lib/decidim/api/schema.rb +++ b/decidim-api/lib/decidim/api/schema.rb @@ -30,6 +30,14 @@ def self.unauthorized_field(error) rescue_from(Decidim::PermissionAction::PermissionNotSetError) do |_err, _obj, _args, _ctx, field| raise Decidim::Api::Errors::PermissionNotSetError, I18n.t("decidim.api.errors.permission_not_set", type: field.type.unwrap.graphql_name) end + + rescue_from(I18n::InvalidLocale) do |_err, _obj, _args, _ctx, _field| + raise Decidim::Api::Errors::InvalidLocaleError, I18n.t("decidim.api.errors.invalid_locale") + end + + rescue_from(I18n::ArgumentError) do |err, _obj, _args, _ctx, _field| + raise Decidim::Api::Errors::LocaleError, I18n.t("decidim.api.errors.locale_argument_error", message: err.message) + end end end end diff --git a/decidim-api/lib/decidim/api/test/type_context.rb b/decidim-api/lib/decidim/api/test/type_context.rb index ae6c2c331df97..0e45659141250 100644 --- a/decidim-api/lib/decidim/api/test/type_context.rb +++ b/decidim-api/lib/decidim/api/test/type_context.rb @@ -31,18 +31,21 @@ def raise_proper_error(error) code = error.dig("extensions", "code") - case code - when "NO_PERMISSION_SET" - raise Decidim::Api::Errors::PermissionNotSetError, error["message"] - when "NOT_FOUND" - raise Decidim::Api::Errors::NotFoundError, error["message"] - when "NO_FIELD_PERMISSION" - raise Decidim::Api::Errors::UnauthorizedFieldError, error["message"] - when "NO_OBJECT_PERMISSION" - raise Decidim::Api::Errors::UnauthorizedObjectError, error["message"] - else - raise StandardError, error["message"] - end + # Matches the error code with the Error class + # For instance, if the error code is NOT_FOUND_ERROR then it will raise the "Decidim::Api::Errors::NotFoundError" class + raise "Decidim::Api::Errors::#{code.downcase.classify}".constantize, error["message"] if %w( + LOCALE_ERROR + NOT_FOUND_ERROR + INVALID_LOCALE_ERROR + PERMISSION_NOT_SET_ERROR + ATTRIBUTE_VALIDATION_ERROR + UNAUTHORIZED_FIELD_ERROR + UNAUTHORIZED_OBJECT_ERROR + MUTATION_NOT_AUTHORIZED_ERROR + VALIDATION_ERROR + ).include?(code) + + raise GraphQL::ExecutionError, error["message"] end def execute_query(query, variables) diff --git a/decidim-api/lib/decidim/api/types.rb b/decidim-api/lib/decidim/api/types.rb index 8d16cd75fe078..0899324b07f38 100644 --- a/decidim-api/lib/decidim/api/types.rb +++ b/decidim-api/lib/decidim/api/types.rb @@ -10,10 +10,15 @@ module Api autoload :ComponentMutationType, "decidim/api/component_mutation_type" module Errors - autoload :PermissionNotSetError, "decidim/api/errors/permission_not_set_error" + autoload :LocaleError, "decidim/api/errors/locale_error" + autoload :InvalidLocaleError, "decidim/api/errors/invalid_locale_error" + autoload :AttributeValidationError, "decidim/api/errors/attribute_validation_error" + autoload :MutationNotAuthorizedError, "decidim/api/errors/mutation_not_authorized_error" autoload :NotFoundError, "decidim/api/errors/not_found_error" + autoload :PermissionNotSetError, "decidim/api/errors/permission_not_set_error" autoload :UnauthorizedFieldError, "decidim/api/errors/unauthorized_field_error" autoload :UnauthorizedObjectError, "decidim/api/errors/unauthorized_object_error" + autoload :ValidationError, "decidim/api/errors/validation_error" end module Types diff --git a/decidim-api/spec/lib/decidim/api/errors/attribute_validation_error_spec.rb b/decidim-api/spec/lib/decidim/api/errors/attribute_validation_error_spec.rb new file mode 100644 index 0000000000000..0396c7f37b279 --- /dev/null +++ b/decidim-api/spec/lib/decidim/api/errors/attribute_validation_error_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require "spec_helper" +require "active_model" + +module Decidim + module Api + module Errors + describe AttributeValidationError do + subject { described_class.new(messages) } + let(:messages) { [] } + + context "when initialized with an Array of hashes" do + let(:messages) do + [ + { + path: %w(attributes body), + message: "is too short (under 15 characters)" + }, + { + path: %w(attributes title), + message: "is too long" + } + ] + end + + describe "#to_h" do + it { expect(subject.to_h).to include("extensions" => { "code" => "ATTRIBUTE_VALIDATION_ERROR" }) } + it { expect(subject.to_h).to include("message" => messages) } + end + + describe "#message" do + it { expect(subject.message).to eq("is too short (under 15 characters), is too long") } + end + end + + context "when initialized with ActiveModel::Errors" do + let(:dummy_model_class) do + Class.new do + include ActiveModel::Model + + attr_accessor :body, :title + + validates :body, presence: true + validates :title, presence: true + + def self.name + "DummyModel" + end + end + end + let(:messages) do + model = dummy_model_class.new + model.errors.add(:body, :too_short, count: 15) + model.errors.add(:title, :too_long, count: 1) + model.errors + end + + describe "#to_h" do + it { expect(subject.to_h).to include("extensions" => { "code" => "ATTRIBUTE_VALIDATION_ERROR" }) } + + it { + expect(subject.to_h).to include("message" => [ + { + path: %w(attributes body), + message: "is too short (under 15 characters)" + }, + { + path: %w(attributes title), + message: "is too long (maximum is 1 character)" + } + ]) + } + end + + describe "#message" do + it { expect(subject.message).to include("is too short (under 15 characters)") } + it { expect(subject.message).to include("is too long") } + end + end + end + end + end +end diff --git a/decidim-dev/spec/types/errors_spec.rb b/decidim-dev/spec/types/errors_spec.rb index 62063da1cf875..170615943a778 100644 --- a/decidim-dev/spec/types/errors_spec.rb +++ b/decidim-dev/spec/types/errors_spec.rb @@ -21,6 +21,16 @@ end end + before do + I18n.backend.reload! + I18n.backend.store_translations( + :en, + decidim: { + test: "Test %{name}" + } + ) + end + context "when Decidim::Api::Errors::PermissionNotSetError is raised" do let(:type_class) do Class.new(Decidim::Api::Types::BaseObject) do @@ -37,4 +47,57 @@ def tested_error expect { response }.to raise_error(Decidim::Api::Errors::PermissionNotSetError, /Permission has not been set for this/) end end + + context "when Decidim::Api::Errors::LocaleError is raised because of I18n::MissingInterpolationArgument is raised" do + let(:type_class) do + Class.new(Decidim::Api::Types::BaseObject) do + graphql_name "ErrorTypeTest" + field :tested_error, String, null: false + + def tested_error + I18n.translate!("decidim.test", invalid_interpolation: "Testing Missing interpolation argument") + end + end + end + + it "throws exception" do + expect { response }.to raise_error(Decidim::Api::Errors::LocaleError, "There was an error while internally handling i18n data") + end + end + + context "when Decidim::Api::Errors::LocaleError is raised because of I18n::MissingTranslationData is raised" do + let(:type_class) do + Class.new(Decidim::Api::Types::BaseObject) do + graphql_name "ErrorTypeTest" + field :tested_error, String, null: false + + def tested_error + I18n.translate!("decidim.invalid_translation_key") + end + end + end + + it "throws exception" do + expect { response }.to raise_error(Decidim::Api::Errors::LocaleError, "There was an error while internally handling i18n data") + end + end + + context "when Decidim::Api::Errors::InvalidLocaleError is raised because of I18n::InvalidLocaleError is raised" do + let(:type_class) do + Class.new(Decidim::Api::Types::BaseObject) do + graphql_name "ErrorTypeTest" + field :tested_error, String, null: false + + def tested_error + I18n.with_locale("invalid_locale") do + I18n.translate!("decidim.test", name: "test") + end + end + end + end + + it "throws exception" do + expect { response }.to raise_error(Decidim::Api::Errors::InvalidLocaleError, "Invalid locale provided") + end + end end diff --git a/decidim-initiatives/spec/types/initiative_type_spec.rb b/decidim-initiatives/spec/types/initiative_type_spec.rb index a7e63c592586c..cb08c57c2f3c5 100644 --- a/decidim-initiatives/spec/types/initiative_type_spec.rb +++ b/decidim-initiatives/spec/types/initiative_type_spec.rb @@ -201,7 +201,7 @@ module Initiatives let(:msg) { "Field '#{field}' doesn't exist on type 'Initiative'" } it "has not have a #{field} field" do - expect { response }.to raise_error(an_instance_of(StandardError).and(having_attributes(message: msg))) + expect { response }.to raise_error(an_instance_of(GraphQL::ExecutionError).and(having_attributes(message: msg))) end end end From 8361425e069639f75d70769d08f738d2cad675b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Fri, 19 Dec 2025 09:16:50 +0100 Subject: [PATCH 032/116] Update docs for GitHub Actions configuration (#15824) --- .github/workflows/README.md | 83 +++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 44 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 6b4064b6a917f..d011956bcd854 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -1,46 +1,41 @@ # Decidim GitHub Actions workflows -We use GitHub Actions as CI. - -- `lint_code.yml`: runs the linters for Ruby, JS and ERB files. -- `ci_main.yml`: runs the tests for the main folder -- `ci_core.yml`: runs the tests for the `decidim-core` module. The remaining workflows (except noted) are based on this one. - -Individual workflows with changes: - -- `ci_generators.yml`: `decidim-generators` does not need to create the test_app, so this command is removed. Screenshots uploads and chromedriver setup steps are also not needed for this module and thus removed. We also customize the gems path after running `bundle install`: - -```yml -# ci_generators.yml -- run: bundle install --path vendor/bundle --jobs 4 --retry 3 - name: Install Ruby deps -- run: cp -R vendor/bundle decidim-generators -- run: bundle exec rspec - name: RSpec - working-directory: ${{ env.DECIDIM_MODULE }} -``` - -- `ci_javascript.yml`: Runs tests for the JS files. Tests must run from the project root folder. You will need to install NodeJS and the JS dependencies: - -```yml -- uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} -- run: npm ci - name: Install JS deps -- run: npm run test - name: Test JS files -``` - -- Some specs are split in three workflows, so if we need to retry this particular workflow we do not need to retry all the module suite. For instance proposals: - - - `ci_proposals_system_admin.yml`: Runs the system specs for the admin section - - `ci_proposals_system_public.yml`: Runs the system specs for the public section - - `ci_proposals_unit_tests.yml`: Runs the unit tests - -- `ci_performance_metrics_monitoring.yml`: Runs Lighthouse metrics expectations against the app to detect any performance regression. The expectations can be found in `lighthouse_budget.json`, where a time is defined for each metric: - - - [First Contentful Paint](https://web.dev/first-contentful-paint/): 2 seconds - - [Speed Index](https://web.dev/speed-index/): 4 seconds - - [Time to Interactive](https://web.dev/interactive/): 5 seconds - - [Largest Contentful Paint](https://web.dev/lcp/): 2.5 seconds +We use GitHub Actions as CI with two key optimizations: **workflow splitting** and **composite actions**. + +## Architecture + +### Composite Actions + +- `test_app.yml`: [Reusable workflow](https://docs.github.com/en/actions/using-workflows/reusing-workflows) that provides all common CI setup (Ruby, Node.js, database, Chrome, etc.) +- All `ci_*.yml` workflows use this composite action via `uses: ./.github/workflows/test_app.yml` +- Reduces duplication and simplifies maintenance + +### Workflow Splitting + +Large test suites are split into [parallel workflows](https://docs.github.com/en/actions/using-jobs/using-jobs-in-a-workflow) to reduce execution time: + +## Core Workflows + +- `lint_code.yml`: Lints Ruby, JS, and ERB files +- `ci_main.yml`: Tests for main folder +- `ci_core.yml`: Base template for module testing using `test_app.yml` + +## Special Cases + +- `ci_generators.yml`: No test app needed, uses custom gem path setup +- `ci_javascript.yml`: Runs JS tests from project root with Node.js setup + +## Split Workflows (Parallel Execution) + +Modules with large test suites are split across multiple workflows: + +- Proposals: `ci_proposals_system_admin.yml`, `ci_proposals_system_public.yml`, `ci_proposals_unit_tests.yml` +- Similar patterns for other large modules + +## Performance Monitoring + +- `ci_performance_metrics_monitoring.yml`: Lighthouse CI with budgets: + - [First Contentful Paint](https://web.dev/first-contentful-paint/): 2s + - [Speed Index](https://web.dev/speed-index/): 4s + - [Time to Interactive](https://web.dev/interactive/): 5s + - [Largest Contentful Paint](https://web.dev/lcp/): 2.5s From e2e272be9d5426fdddc4af41b6bcbd3ea33e8262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Fri, 19 Dec 2025 10:22:04 +0100 Subject: [PATCH 033/116] Enable EnforcedShorthandSyntax: always on rubocop configuration (#15822) --- .../destroy_members_follows_job.rb | 2 +- .../destroy_members_follows_job_spec.rb | 2 +- .../app/models/decidim/api/api_user.rb | 2 +- decidim-api/lib/decidim/api/query_type.rb | 2 +- .../decidim/api/sessions_controller_spec.rb | 6 ++--- decidim-api/spec/requests/apiauth_spec.rb | 18 +++++++------- ...admin_manages_assembly_soft_delete_spec.rb | 24 +++++++++---------- .../decidim/blogs/posts_controller.rb | 4 ++-- .../app/forms/decidim/blogs/post_form.rb | 2 +- .../views/decidim/blogs/posts/index.html.erb | 2 +- .../views/decidim/blogs/posts/show.html.erb | 2 +- .../budgets/projects/_budget_summary.html.erb | 2 +- .../spec/system/explore_projects_spec.rb | 2 +- .../lib/decidim/collaborative_texts/seeds.rb | 2 +- .../admin/update_document_spec.rb | 10 ++++---- .../admin/documents_controller_spec.rb | 12 +++++----- .../suggestions_controller_spec.rb | 8 +++---- .../collaborative_texts/document_spec.rb | 2 +- .../collaborative_texts/permissions_spec.rb | 2 +- .../admin_log/document_presenter_spec.rb | 2 +- .../spec/types/integration_schema_spec.rb | 2 +- ...min_manages_conference_soft_delete_spec.rb | 24 +++++++++---------- .../participatory_space_serializer.rb | 2 +- ...23104311_move_cta_to_hero_content_block.rb | 2 +- ...ontent_banner_settings_to_content_block.rb | 2 +- .../spec/helpers/decidim/menu_helper_spec.rb | 2 +- .../spec/lib/oauth/token_generator_spec.rb | 2 +- .../notification_digest_mailer_spec.rb | 4 ++-- .../decidim/stats_presenter_spec.rb | 20 ++++++++-------- .../decidim/inactive_users_query_spec.rb | 6 ++--- decidim-core/spec/system/editor_spec.rb | 2 +- decidim-core/spec/system/link_target_spec.rb | 4 ++-- ...dim_remove_deleted_users_left_data_spec.rb | 2 +- .../fix_deleted_private_follows_spec.rb | 2 +- .../decidim/debates/debates/index.html.erb | 2 +- .../config/rubocop/ruby/configuration.yml | 1 + .../decidim/elections/admin/process_census.rb | 2 +- .../commands/decidim/elections/cast_votes.rb | 6 ++--- .../decidim/elections/uses_votes_booth.rb | 2 +- .../elections/admin/census_controller.rb | 8 +++---- .../elections/admin/elections_controller.rb | 12 +++++----- .../elections/censuses/internal_users_form.rb | 2 +- .../app/models/decidim/elections/vote.rb | 2 +- .../censuses/_internal_users_form.html.erb | 2 +- .../admin/elections/_election-tr.html.erb | 2 +- .../censuses/_internal_users_form.html.erb | 4 ++-- .../censuses/_token_csv_form.html.erb | 2 +- .../elections/elections/index.html.erb | 2 +- .../decidim/elections/elections/show.html.erb | 2 +- .../lib/decidim/elections/engine.rb | 2 +- .../lib/decidim/elections/seeds.rb | 2 +- .../test/vote_controller_examples.rb | 12 +++++----- .../decidim/elections/test/vote_examples.rb | 8 +++---- .../elections/admin/create_election_spec.rb | 4 ++-- .../elections/admin/update_questions_spec.rb | 4 ++-- .../elections/admin/census_controller_spec.rb | 4 ++-- .../census_checks_controller_spec.rb | 8 +++---- .../per_question_votes_controller_spec.rb | 24 +++++++++---------- .../elections/votes_controller_spec.rb | 18 +++++++------- .../elections/admin/question_form_spec.rb | 18 +++++++------- .../elections/admin/questions_form_spec.rb | 2 +- .../elections/admin/token_csv_form_spec.rb | 2 +- .../elections/admin/permissions_spec.rb | 2 +- .../elections/election_presenter_spec.rb | 2 +- .../admin_manages_election_census_spec.rb | 2 +- .../decidim/forms/response_questionnaire.rb | 2 +- .../forms/questionnaires/show.html.erb | 2 +- .../admin/display_condition_form_spec.rb | 2 +- .../admin/exports/_dropdown.html.erb | 2 +- .../promotal_committee.html.erb | 2 +- .../initiatives/progress_notifier_spec.rb | 2 +- .../meetings/admin/mark_as_attendee.rb | 2 +- .../decidim/meetings/update_meeting_event.rb | 4 ++-- .../admin/copy_meeting_fields_spec.rb | 4 ++-- .../spec/commands/join_waitlist_spec.rb | 14 +++++------ .../meetings/registrations_controller_spec.rb | 24 +++++++++---------- .../spec/system/meeting_waiting_list_spec.rb | 6 ++--- ...democratic_quality_stats_presenter_spec.rb | 2 +- ..._participatory_process_soft_delete_spec.rb | 24 +++++++++---------- .../proposals/proposals/index.html.erb | 2 +- .../admin/proposal_answers_controller_spec.rb | 2 +- .../spec/lib/tasks/upgrade/fix_state_spec.rb | 4 ++-- .../spec/types/proposal_mutation_type_spec.rb | 8 +++---- .../surveys/surveys/not_allowed.html.erb | 2 +- .../lib/decidim/surveys/component.rb | 2 +- .../decidim/system/api_users_controller.rb | 6 ++--- .../decidim/system/create_api_user_spec.rb | 2 +- .../system/refresh_api_user_secret_spec.rb | 2 +- .../decidim/system/api_user_form_spec.rb | 4 ++-- .../system/explore_api_credentials_spec.rb | 2 +- .../proposal_answer_templates/index.html.erb | 2 +- 91 files changed, 240 insertions(+), 239 deletions(-) diff --git a/decidim-admin/app/jobs/decidim/admin/participatory_space/destroy_members_follows_job.rb b/decidim-admin/app/jobs/decidim/admin/participatory_space/destroy_members_follows_job.rb index 95da9158bf3a8..efc40670a36db 100644 --- a/decidim-admin/app/jobs/decidim/admin/participatory_space/destroy_members_follows_job.rb +++ b/decidim-admin/app/jobs/decidim/admin/participatory_space/destroy_members_follows_job.rb @@ -19,7 +19,7 @@ def perform(decidim_user_id, space) return if space.respond_to?(:can_participate?) && space.can_participate?(user) - follows = Decidim::Follow.where(user: user) + follows = Decidim::Follow.where(user:) follows.where(followable: space).destroy_all destroy_children_follows(follows, space) diff --git a/decidim-admin/spec/jobs/decidim/admin/participatory_space/destroy_members_follows_job_spec.rb b/decidim-admin/spec/jobs/decidim/admin/participatory_space/destroy_members_follows_job_spec.rb index f4857f822f44d..c5de1536aea5e 100644 --- a/decidim-admin/spec/jobs/decidim/admin/participatory_space/destroy_members_follows_job_spec.rb +++ b/decidim-admin/spec/jobs/decidim/admin/participatory_space/destroy_members_follows_job_spec.rb @@ -11,7 +11,7 @@ module ParticipatorySpace let!(:normal_user) { create(:user, organization:) } let!(:follow) { create(:follow, followable: participatory_space, user: normal_user) } let(:component) { create(:dummy_component, participatory_space:) } - let(:resource) { create(:dummy_resource, component: component, author: user) } + let(:resource) { create(:dummy_resource, component:, author: user) } let!(:followed_resource) { create(:follow, followable: resource, user: normal_user) } context "when assembly is private and non transparent" do diff --git a/decidim-api/app/models/decidim/api/api_user.rb b/decidim-api/app/models/decidim/api/api_user.rb index 9f9cda8c8b76e..211ab0bdbf7c4 100644 --- a/decidim-api/app/models/decidim/api/api_user.rb +++ b/decidim-api/app/models/decidim/api/api_user.rb @@ -54,7 +54,7 @@ def confirmed? end def follows?(followable) - Decidim::Follow.where(user: self, followable: followable).any? + Decidim::Follow.where(user: self, followable:).any? end # Public: whether the user accepts direct messages from another diff --git a/decidim-api/lib/decidim/api/query_type.rb b/decidim-api/lib/decidim/api/query_type.rb index fcb4fc4bd1c83..e037fd3f9eb70 100644 --- a/decidim-api/lib/decidim/api/query_type.rb +++ b/decidim-api/lib/decidim/api/query_type.rb @@ -65,7 +65,7 @@ def users(filter: {}, order: {}) end def participant_details(id: nil, nickname: nil) - participant = Decidim::Core::UserEntityFinder.new.call(object, { id: id, nickname: nickname }, context) + participant = Decidim::Core::UserEntityFinder.new.call(object, { id:, nickname: }, context) return nil unless participant return nil unless Decidim::Core::ParticipantDetailsType.authorized?(participant, context) diff --git a/decidim-api/spec/controllers/decidim/api/sessions_controller_spec.rb b/decidim-api/spec/controllers/decidim/api/sessions_controller_spec.rb index 3487fa3dbd5a4..67c555669644c 100644 --- a/decidim-api/spec/controllers/decidim/api/sessions_controller_spec.rb +++ b/decidim-api/spec/controllers/decidim/api/sessions_controller_spec.rb @@ -9,7 +9,7 @@ let(:organization) { create(:organization) } let(:api_key) { "user_key" } let(:api_secret) { "decidim123456789" } - let!(:user) { create(:api_user, organization: organization, api_key: api_key, api_secret: api_secret) } + let!(:user) { create(:api_user, organization:, api_key:, api_secret:) } let(:params) do { api_user: { @@ -36,7 +36,7 @@ describe "sign in" do it "returns JWT token when credentials are valid" do expect(request.env[Warden::JWTAuth::Hooks::PREPARED_TOKEN_ENV_KEY]).not_to be_present - post :create, params: params + post(:create, params:) expect(response).to have_http_status(:ok) token = request.env[Warden::JWTAuth::Hooks::PREPARED_TOKEN_ENV_KEY] expect(token).to be_present @@ -53,7 +53,7 @@ it "renders resource without JWT token in body when `Tokendispatcher::ENV_KEY` is nil" do request.env[Warden::JWTAuth::Middleware::TokenDispatcher::ENV_KEY] = nil - post :create, params: params + post(:create, params:) expect(request.env[Warden::JWTAuth::Hooks::PREPARED_TOKEN_ENV_KEY]).to be_present parsed_response_body = JSON.parse(response.body) expect(parsed_response_body.has_key?("jwt_token")).to be(false) diff --git a/decidim-api/spec/requests/apiauth_spec.rb b/decidim-api/spec/requests/apiauth_spec.rb index a742445a9bb0b..98a47d99ffd13 100644 --- a/decidim-api/spec/requests/apiauth_spec.rb +++ b/decidim-api/spec/requests/apiauth_spec.rb @@ -15,12 +15,12 @@ context "with api user" do let(:key) { "dummykey123456" } let(:secret) { "decidim123456789" } - let!(:user) { create(:api_user, organization: organization, api_key: key, api_secret: secret) } + let!(:user) { create(:api_user, organization:, api_key: key, api_secret: secret) } let(:params) do { api_user: { - key: key, - secret: secret + key:, + secret: } } end @@ -35,7 +35,7 @@ end it "signs in" do - post sign_in_path, params: params + post(sign_in_path, params:) expect(response.headers["Authorization"]).to be_present expect(response.body["jwt_token"]).to be_present parsed_response_body = JSON.parse(response.body) @@ -51,7 +51,7 @@ end it "signs out" do - post sign_in_path, params: params + post(sign_in_path, params:) expect(response).to have_http_status(:ok) authorization = response.headers["Authorization"] original_count = Decidim::Api::JwtDenylist.count @@ -61,7 +61,7 @@ context "when signed in" do before do - post sign_in_path, params: params + post sign_in_path, params: end it "can use token to post to api" do @@ -78,7 +78,7 @@ context "when not signed in" do it "does not return session details" do - post "/api", params: { query: query } + post "/api", params: { query: } parsed_response = JSON.parse(response.body) expect(parsed_response).to match("data" => { "session" => nil }) end @@ -87,7 +87,7 @@ context "with normal user" do let(:password) { "decidim123456789" } - let!(:user) { create(:user, :confirmed, organization: organization, password:) } + let!(:user) { create(:user, :confirmed, organization:, password:) } let(:params) do { user: { @@ -98,7 +98,7 @@ end it "does not authenticate user" do - post sign_in_path, params: params + post(sign_in_path, params:) parsed_response = JSON.parse(response.body) anonymized_key = parsed_response["api_key"] diff --git a/decidim-assemblies/spec/system/admin/admin_manages_assembly_soft_delete_spec.rb b/decidim-assemblies/spec/system/admin/admin_manages_assembly_soft_delete_spec.rb index b5a22206c81e1..4bda80d5c7440 100644 --- a/decidim-assemblies/spec/system/admin/admin_manages_assembly_soft_delete_spec.rb +++ b/decidim-assemblies/spec/system/admin/admin_manages_assembly_soft_delete_spec.rb @@ -14,12 +14,12 @@ it_behaves_like "manage trashed resource", "assembly" context "when a user is collaborator" do - let!(:assembly) { create(:assembly, organization: organization) } - let!(:collaborator_user) { create(:user, :admin_terms_accepted, :confirmed, organization: organization) } + let!(:assembly) { create(:assembly, organization:) } + let!(:collaborator_user) { create(:user, :admin_terms_accepted, :confirmed, organization:) } let!(:collaborator_role) do create(:assembly_user_role, user: collaborator_user, - assembly: assembly, + assembly:, role: :collaborator) end @@ -36,12 +36,12 @@ end context "when a user is evaluator" do - let!(:assembly) { create(:assembly, organization: organization) } - let!(:evaluator_user) { create(:user, :admin_terms_accepted, :confirmed, organization: organization) } + let!(:assembly) { create(:assembly, organization:) } + let!(:evaluator_user) { create(:user, :admin_terms_accepted, :confirmed, organization:) } let!(:evaluator_role) do create(:assembly_user_role, user: evaluator_user, - assembly: assembly, + assembly:, role: :evaluator) end @@ -58,12 +58,12 @@ end context "when a user is moderator" do - let!(:assembly) { create(:assembly, organization: organization) } - let!(:moderator_user) { create(:user, :admin_terms_accepted, :confirmed, organization: organization) } + let!(:assembly) { create(:assembly, organization:) } + let!(:moderator_user) { create(:user, :admin_terms_accepted, :confirmed, organization:) } let!(:moderator_role) do create(:assembly_user_role, user: moderator_user, - assembly: assembly, + assembly:, role: :moderator) end @@ -80,12 +80,12 @@ end context "when a user is a space admin" do - let!(:assembly) { create(:assembly, organization: organization) } - let!(:admin_user) { create(:user, :admin_terms_accepted, :confirmed, organization: organization) } + let!(:assembly) { create(:assembly, organization:) } + let!(:admin_user) { create(:user, :admin_terms_accepted, :confirmed, organization:) } let!(:admin_role) do create(:assembly_user_role, user: admin_user, - assembly: assembly, + assembly:, role: :admin) end diff --git a/decidim-blogs/app/controllers/decidim/blogs/posts_controller.rb b/decidim-blogs/app/controllers/decidim/blogs/posts_controller.rb index a0c885cfe7624..aeffda5c91004 100644 --- a/decidim-blogs/app/controllers/decidim/blogs/posts_controller.rb +++ b/decidim-blogs/app/controllers/decidim/blogs/posts_controller.rb @@ -26,7 +26,7 @@ def new def create enforce_permission_to :create, :blogpost - @form = form(Decidim::Blogs::PostForm).from_params(params, current_component: current_component) + @form = form(Decidim::Blogs::PostForm).from_params(params, current_component:) CreatePost.call(@form) do on(:ok) do |new_post| @@ -48,7 +48,7 @@ def edit def update enforce_permission_to :update, :blogpost, blogpost: post - @form = form(PostForm).from_params(params, current_component: current_component) + @form = form(PostForm).from_params(params, current_component:) UpdatePost.call(@form, post) do on(:ok) do |post| diff --git a/decidim-blogs/app/forms/decidim/blogs/post_form.rb b/decidim-blogs/app/forms/decidim/blogs/post_form.rb index 9a23a3943f563..c19adec62d439 100644 --- a/decidim-blogs/app/forms/decidim/blogs/post_form.rb +++ b/decidim-blogs/app/forms/decidim/blogs/post_form.rb @@ -48,7 +48,7 @@ def can_set_author end def post - @post ||= Post.find_by(id: id) + @post ||= Post.find_by(id:) end def participatory_space_manifest diff --git a/decidim-blogs/app/views/decidim/blogs/posts/index.html.erb b/decidim-blogs/app/views/decidim/blogs/posts/index.html.erb index c3c33130c29ef..4e61a3b8c66fc 100644 --- a/decidim-blogs/app/views/decidim/blogs/posts/index.html.erb +++ b/decidim-blogs/app/views/decidim/blogs/posts/index.html.erb @@ -1,7 +1,7 @@ <% add_decidim_meta_tags( description: translated_attribute(current_component.participatory_space.try(:description)), title: t("decidim.components.pagination.page_title", - component_name: component_name, + component_name:, current_page: paginate_posts.current_page, total_pages: paginate_posts.total_pages ), url: posts_url, diff --git a/decidim-blogs/app/views/decidim/blogs/posts/show.html.erb b/decidim-blogs/app/views/decidim/blogs/posts/show.html.erb index 259ea2d8c6405..37f5e2428b66b 100644 --- a/decidim-blogs/app/views/decidim/blogs/posts/show.html.erb +++ b/decidim-blogs/app/views/decidim/blogs/posts/show.html.erb @@ -39,7 +39,7 @@ <%= cell "decidim/author", post_presenter.author, from: post, context_actions: [:date], layout: :compact %>
<%= render "decidim/shared/resource_actions", resource: post do %> - <%= render "decidim/blogs/posts/menu_actions", post: post %> + <%= render "decidim/blogs/posts/menu_actions", post: %> <% end %>
diff --git a/decidim-budgets/app/views/decidim/budgets/projects/_budget_summary.html.erb b/decidim-budgets/app/views/decidim/budgets/projects/_budget_summary.html.erb index fe9e4acd4f924..4d8fe7883cad7 100644 --- a/decidim-budgets/app/views/decidim/budgets/projects/_budget_summary.html.erb +++ b/decidim-budgets/app/views/decidim/budgets/projects/_budget_summary.html.erb @@ -1,6 +1,6 @@
" class="budget-summary <%= responsive ? "block md:hidden" : "hidden md:block" %>" data-progress-reference data-safe-url="<%= budget_url(budget) %>"> <% if responsive %> - <%= render partial: "decidim/budgets/projects/order_progress_summary/content_responsive", locals: { focus_mode_origin: focus_mode_origin } %> + <%= render partial: "decidim/budgets/projects/order_progress_summary/content_responsive", locals: { focus_mode_origin: } %> <% else %> <%= render partial: "decidim/budgets/projects/order_progress_summary/content" %> <% end %> diff --git a/decidim-budgets/spec/system/explore_projects_spec.rb b/decidim-budgets/spec/system/explore_projects_spec.rb index 7fa16548097fb..044ecd1564a71 100644 --- a/decidim-budgets/spec/system/explore_projects_spec.rb +++ b/decidim-budgets/spec/system/explore_projects_spec.rb @@ -110,7 +110,7 @@ [-142.15275006889419, 33.33377235135252], [-55.28745034772282, -35.587843900166945] ] - Decidim::Budgets::Project.where(budget: budget).geocoded.each_with_index do |project, index| + Decidim::Budgets::Project.where(budget:).geocoded.each_with_index do |project, index| project.update!(latitude: coordinates[index][0], longitude: coordinates[index][1]) if coordinates[index] end diff --git a/decidim-collaborative_texts/lib/decidim/collaborative_texts/seeds.rb b/decidim-collaborative_texts/lib/decidim/collaborative_texts/seeds.rb index e0866a2fc4512..be0737377fcd2 100644 --- a/decidim-collaborative_texts/lib/decidim/collaborative_texts/seeds.rb +++ b/decidim-collaborative_texts/lib/decidim/collaborative_texts/seeds.rb @@ -29,7 +29,7 @@ def create_component! name: Decidim::Components::Namer.new(participatory_space.organization.available_locales, :collaborative_texts).i18n_name, manifest_name: :collaborative_texts, published_at: Time.current, - participatory_space: participatory_space + participatory_space: } Decidim.traceability.perform_action!( diff --git a/decidim-collaborative_texts/spec/commands/decidim/collaborative_texts/admin/update_document_spec.rb b/decidim-collaborative_texts/spec/commands/decidim/collaborative_texts/admin/update_document_spec.rb index 3cea1fb8dc5bc..fea4327a4f361 100644 --- a/decidim-collaborative_texts/spec/commands/decidim/collaborative_texts/admin/update_document_spec.rb +++ b/decidim-collaborative_texts/spec/commands/decidim/collaborative_texts/admin/update_document_spec.rb @@ -22,7 +22,7 @@ module Admin title:, body:, draft?: draft, - draft: draft, + draft:, accepting_suggestions:, coauthorships: [Decidim::Coauthorship.new(author: organization)] ) @@ -68,11 +68,11 @@ module Admin .with(last_version, user, updated_keys, { extra: { document_id: document.id, - title: title, + title:, version_number: 1 }, resource: { - title: title + title: }, participatory_space: { title: document.participatory_space.title @@ -116,11 +116,11 @@ module Admin .with(Decidim::CollaborativeTexts::Version, user, { document:, body: document.body, draft: true }, { extra: { document_id: document.id, - title: title, + title:, version_number: 2 }, resource: { - title: title + title: }, participatory_space: { title: document.participatory_space.title diff --git a/decidim-collaborative_texts/spec/controllers/decidim/collaborative_texts/admin/documents_controller_spec.rb b/decidim-collaborative_texts/spec/controllers/decidim/collaborative_texts/admin/documents_controller_spec.rb index b454c80814c34..483d81c2785eb 100644 --- a/decidim-collaborative_texts/spec/controllers/decidim/collaborative_texts/admin/documents_controller_spec.rb +++ b/decidim-collaborative_texts/spec/controllers/decidim/collaborative_texts/admin/documents_controller_spec.rb @@ -15,8 +15,8 @@ module Admin let(:document_versions) { [build(:collaborative_text_version)] } let(:params) do { - title: title, - body: body + title:, + body: } end let(:title) { "A nice test document" } @@ -69,7 +69,7 @@ module Admin describe "POST #create" do it "creates a new document and redirects to index" do expect do - post :create, params: params + post :create, params: end.to change(Document, :count).by(1) expect(response).to redirect_to(documents_path) expect(flash[:notice]).to eq("Document successfully created.") @@ -80,7 +80,7 @@ module Admin it "does not create a document" do expect do - post :create, params: params + post :create, params: end.not_to change(Document, :count) expect(response).to render_template(:new) expect(flash.now[:alert]).to eq("There was a problem creating the document.") @@ -98,7 +98,7 @@ module Admin describe "PATCH #update" do it "updates the document and redirects to index" do - patch :update, params: { id: collaborative_text_document.id, title: "Updated Title", body: body } + patch :update, params: { id: collaborative_text_document.id, title: "Updated Title", body: } expect(response).to redirect_to(documents_path) end end @@ -121,7 +121,7 @@ module Admin let(:title) { "" } it "does not update the document settings" do - patch :update_settings, params: { id: collaborative_text_document.id, title: "", body: body } + patch :update_settings, params: { id: collaborative_text_document.id, title: "", body: } expect(response).to render_template(:edit_settings) expect(flash.now[:alert]).to eq("There was a problem updating the document.") end diff --git a/decidim-collaborative_texts/spec/controllers/decidim/collaborative_texts/suggestions_controller_spec.rb b/decidim-collaborative_texts/spec/controllers/decidim/collaborative_texts/suggestions_controller_spec.rb index 730dc04bb30b7..bf1a9103e38e5 100644 --- a/decidim-collaborative_texts/spec/controllers/decidim/collaborative_texts/suggestions_controller_spec.rb +++ b/decidim-collaborative_texts/spec/controllers/decidim/collaborative_texts/suggestions_controller_spec.rb @@ -30,7 +30,7 @@ module CollaborativeTexts describe "GET #index" do it "returns a success response" do - get :index, params: params + get(:index, params:) expect(response).to have_http_status(:ok) body = JSON.parse(response.body) expect(body.first.keys).to contain_exactly("changeset", "createdAt", "id", "profileHtml", "status", "summary", "type") @@ -60,7 +60,7 @@ module CollaborativeTexts let(:first_node) { "1" } it "returns an error when user is not signed in" do - post :create, params: params + post(:create, params:) expect(response).to have_http_status(:unprocessable_entity) body = JSON.parse(response.body) expect(body["message"]).to eq("You are not authorized to perform this action.") @@ -73,7 +73,7 @@ module CollaborativeTexts it "creates a new suggestion" do expect do - post :create, params: params + post :create, params: end.to change(Suggestion, :count).by(1) expect(response).to have_http_status(:ok) body = JSON.parse(response.body) @@ -84,7 +84,7 @@ module CollaborativeTexts let(:first_node) { "" } it "returns an error" do - post :create, params: params + post(:create, params:) expect(response).to have_http_status(:unprocessable_entity) body = JSON.parse(response.body) expect(body["message"]).to eq("There was a problem creating the suggestion. Invalid selected nodes.") diff --git a/decidim-collaborative_texts/spec/models/decidim/collaborative_texts/document_spec.rb b/decidim-collaborative_texts/spec/models/decidim/collaborative_texts/document_spec.rb index 1ff9a5332099a..9ea7444532d8a 100644 --- a/decidim-collaborative_texts/spec/models/decidim/collaborative_texts/document_spec.rb +++ b/decidim-collaborative_texts/spec/models/decidim/collaborative_texts/document_spec.rb @@ -33,7 +33,7 @@ module CollaborativeTexts it "current version points to last created" do document.save! - version = create(:collaborative_text_version, created_at: 1.second.from_now, document: document) + version = create(:collaborative_text_version, created_at: 1.second.from_now, document:) expect(document.reload.document_versions.count).to eq(4) expect(document.document_versions_count).to eq(4) expect(document.current_version).to eq(version) diff --git a/decidim-collaborative_texts/spec/permissions/decidim/collaborative_texts/permissions_spec.rb b/decidim-collaborative_texts/spec/permissions/decidim/collaborative_texts/permissions_spec.rb index 22cc9b757c886..cd3c2e6dd3f74 100644 --- a/decidim-collaborative_texts/spec/permissions/decidim/collaborative_texts/permissions_spec.rb +++ b/decidim-collaborative_texts/spec/permissions/decidim/collaborative_texts/permissions_spec.rb @@ -9,7 +9,7 @@ let(:context) do { current_component: collaborative_text_component, - document: document + document: } end let(:collaborative_text_component) { create(:collaborative_text_component) } diff --git a/decidim-collaborative_texts/spec/presenters/decidim/collaborative_texts/admin_log/document_presenter_spec.rb b/decidim-collaborative_texts/spec/presenters/decidim/collaborative_texts/admin_log/document_presenter_spec.rb index 5078e72d81458..b970af4e0892a 100644 --- a/decidim-collaborative_texts/spec/presenters/decidim/collaborative_texts/admin_log/document_presenter_spec.rb +++ b/decidim-collaborative_texts/spec/presenters/decidim/collaborative_texts/admin_log/document_presenter_spec.rb @@ -6,7 +6,7 @@ module Decidim module CollaborativeTexts module AdminLog describe DocumentPresenter do - let(:action_log) { double("ActionLog", action: action, resource: resource, extra: extra, version: version) } + let(:action_log) { double("ActionLog", action:, resource:, extra:, version:) } let(:resource) { double("Document", document_versions: [document]) } let(:document) { create(:collaborative_text_document, body: "This is and example body") } let(:extra) { { "extra" => { "version_number" => "2", "body" => document.body } } } diff --git a/decidim-collaborative_texts/spec/types/integration_schema_spec.rb b/decidim-collaborative_texts/spec/types/integration_schema_spec.rb index aa03116aea08f..314f5e9548b8a 100644 --- a/decidim-collaborative_texts/spec/types/integration_schema_spec.rb +++ b/decidim-collaborative_texts/spec/types/integration_schema_spec.rb @@ -36,7 +36,7 @@ let!(:current_component) { create(:collaborative_text_component, participatory_space: participatory_process) } let!(:document) { create(:collaborative_text_document, component: current_component, published_at: 2.days.ago) } let!(:document_version) { create(:collaborative_text_version, document:) } - let!(:suggestions) { create_list(:collaborative_text_suggestion, 2, document_version: document_version) } + let!(:suggestions) { create_list(:collaborative_text_suggestion, 2, document_version:) } let(:author) { nil } let(:document_single_result) do { diff --git a/decidim-conferences/spec/system/admin/admin_manages_conference_soft_delete_spec.rb b/decidim-conferences/spec/system/admin/admin_manages_conference_soft_delete_spec.rb index d1fecf196bbf0..6321649f6f4f2 100644 --- a/decidim-conferences/spec/system/admin/admin_manages_conference_soft_delete_spec.rb +++ b/decidim-conferences/spec/system/admin/admin_manages_conference_soft_delete_spec.rb @@ -14,12 +14,12 @@ it_behaves_like "manage trashed resource", "conference" context "when a user is collaborator" do - let!(:conference) { create(:conference, organization: organization) } - let!(:collaborator_user) { create(:user, :admin_terms_accepted, :confirmed, organization: organization) } + let!(:conference) { create(:conference, organization:) } + let!(:collaborator_user) { create(:user, :admin_terms_accepted, :confirmed, organization:) } let!(:collaborator_role) do create(:conference_user_role, user: collaborator_user, - conference: conference, + conference:, role: :collaborator) end @@ -36,12 +36,12 @@ end context "when a user is evaluator" do - let!(:conference) { create(:conference, organization: organization) } - let!(:evaluator_user) { create(:user, :admin_terms_accepted, :confirmed, organization: organization) } + let!(:conference) { create(:conference, organization:) } + let!(:evaluator_user) { create(:user, :admin_terms_accepted, :confirmed, organization:) } let!(:evaluator_role) do create(:conference_user_role, user: evaluator_user, - conference: conference, + conference:, role: :evaluator) end @@ -58,12 +58,12 @@ end context "when a user is moderator" do - let!(:conference) { create(:conference, organization: organization) } - let!(:moderator_user) { create(:user, :admin_terms_accepted, :confirmed, organization: organization) } + let!(:conference) { create(:conference, organization:) } + let!(:moderator_user) { create(:user, :admin_terms_accepted, :confirmed, organization:) } let!(:moderator_role) do create(:conference_user_role, user: moderator_user, - conference: conference, + conference:, role: :moderator) end @@ -80,12 +80,12 @@ end context "when a user is a space admin" do - let!(:conference) { create(:conference, organization: organization) } - let!(:admin_user) { create(:user, :admin_terms_accepted, :confirmed, organization: organization) } + let!(:conference) { create(:conference, organization:) } + let!(:admin_user) { create(:user, :admin_terms_accepted, :confirmed, organization:) } let!(:admin_role) do create(:conference_user_role, user: admin_user, - conference: conference, + conference:, role: :admin) end diff --git a/decidim-core/app/serializers/decidim/exporters/participatory_space_serializer.rb b/decidim-core/app/serializers/decidim/exporters/participatory_space_serializer.rb index 6e02322533747..a74a9c6764cef 100644 --- a/decidim-core/app/serializers/decidim/exporters/participatory_space_serializer.rb +++ b/decidim-core/app/serializers/decidim/exporters/participatory_space_serializer.rb @@ -28,7 +28,7 @@ def serialize short_description: resource.short_description, description: resource.description, promoted: resource.promoted, - component_settings: component_settings + component_settings: } end diff --git a/decidim-core/db/migrate/20250523104311_move_cta_to_hero_content_block.rb b/decidim-core/db/migrate/20250523104311_move_cta_to_hero_content_block.rb index b89ddbcf5c202..fb569cc5d8898 100644 --- a/decidim-core/db/migrate/20250523104311_move_cta_to_hero_content_block.rb +++ b/decidim-core/db/migrate/20250523104311_move_cta_to_hero_content_block.rb @@ -8,7 +8,7 @@ class Organization < ApplicationRecord def up Decidim::ContentBlock.reset_column_information Organization.find_each do |organization| - content_block = Decidim::ContentBlock.find_by(organization: organization, scope_name: :homepage, manifest_name: :hero) + content_block = Decidim::ContentBlock.find_by(organization:, scope_name: :homepage, manifest_name: :hero) settings = {} cta_button_text = organization.cta_button_text || {} settings = cta_button_text.inject(settings) { |acc, (k, v)| acc.update("cta_button_text_#{k}" => v) } diff --git a/decidim-core/db/migrate/20250527122040_move_highlighted_content_banner_settings_to_content_block.rb b/decidim-core/db/migrate/20250527122040_move_highlighted_content_banner_settings_to_content_block.rb index 0d2e1796b84f2..59b29fb108365 100644 --- a/decidim-core/db/migrate/20250527122040_move_highlighted_content_banner_settings_to_content_block.rb +++ b/decidim-core/db/migrate/20250527122040_move_highlighted_content_banner_settings_to_content_block.rb @@ -11,7 +11,7 @@ def up Decidim::ContentBlock.reset_column_information Organization.find_each do |organization| - content_block = Decidim::ContentBlock.find_by(organization: organization, scope_name: :homepage, manifest_name: :highlighted_content_banner) + content_block = Decidim::ContentBlock.find_by(organization:, scope_name: :homepage, manifest_name: :highlighted_content_banner) settings = extract_settings(organization) # We need to do a workaround for getting the image, as ActiveStorage is polymorphic and expects that the `record_type` is the class name of the model diff --git a/decidim-core/spec/helpers/decidim/menu_helper_spec.rb b/decidim-core/spec/helpers/decidim/menu_helper_spec.rb index d47f67ae863e7..3e3089a185201 100644 --- a/decidim-core/spec/helpers/decidim/menu_helper_spec.rb +++ b/decidim-core/spec/helpers/decidim/menu_helper_spec.rb @@ -5,7 +5,7 @@ module Decidim describe MenuHelper do let(:organization) { create(:organization) } - let(:user) { create(:user, organization: organization) } + let(:user) { create(:user, organization:) } let!(:process) { create(:participatory_process, :active, weight: 1, organization:) } let!(:process_two) { create(:participatory_process, :active, weight: 2, organization:) } let!(:process_three) { create(:participatory_process, :active, :promoted, weight: 3, organization:) } diff --git a/decidim-core/spec/lib/oauth/token_generator_spec.rb b/decidim-core/spec/lib/oauth/token_generator_spec.rb index 63c14acaba422..35032bdd2e985 100644 --- a/decidim-core/spec/lib/oauth/token_generator_spec.rb +++ b/decidim-core/spec/lib/oauth/token_generator_spec.rb @@ -13,7 +13,7 @@ module Decidim let(:scopes) { "profile" } let(:options) do { - application: application, + application:, resource_owner_id: user.id, scopes: ::Doorkeeper::OAuth::Scopes.from_string(scopes) } diff --git a/decidim-core/spec/mailers/notification_digest_mailer_spec.rb b/decidim-core/spec/mailers/notification_digest_mailer_spec.rb index feb5d5e3a6a0a..e0016c94efd06 100644 --- a/decidim-core/spec/mailers/notification_digest_mailer_spec.rb +++ b/decidim-core/spec/mailers/notification_digest_mailer_spec.rb @@ -81,7 +81,7 @@ module Decidim context "when the space is private and user has access" do let!(:participatory_space) { create(:participatory_process, :private, organization:) } let(:component) { create(:component, :published, manifest_name: "dummy", participatory_space:) } - let!(:member) { create(:member, privatable_to: participatory_space, user: user) } + let!(:member) { create(:member, privatable_to: participatory_space, user:) } it "displays the notification" do expect(subject.body).to include(test_content) @@ -92,7 +92,7 @@ module Decidim context "when the space is transparent and user has access" do let!(:participatory_space) { create(:assembly, :transparent, :private, organization:) } let(:component) { create(:component, :published, manifest_name: "dummy", participatory_space:) } - let!(:member) { create(:member, privatable_to: participatory_space, user: user) } + let!(:member) { create(:member, privatable_to: participatory_space, user:) } it "displays the notification" do expect(subject.body).to include(test_content) diff --git a/decidim-core/spec/presenters/decidim/stats_presenter_spec.rb b/decidim-core/spec/presenters/decidim/stats_presenter_spec.rb index 00e560398595a..23a364fbde8bc 100644 --- a/decidim-core/spec/presenters/decidim/stats_presenter_spec.rb +++ b/decidim-core/spec/presenters/decidim/stats_presenter_spec.rb @@ -8,14 +8,14 @@ describe "#collection" do let(:priority) { Decidim::StatsRegistry::MEDIUM_PRIORITY } - let(:conditions) { { priority: priority } } + let(:conditions) { { priority: } } before do - allow(presenter).to receive(:all_stats).with(priority: priority).and_return([ - { name: "stat_1", data: [1, 23] }, - { name: "stat_2", data: [0] }, - { name: "stat_3", data: [45] } - ]) + allow(presenter).to receive(:all_stats).with(priority:).and_return([ + { name: "stat_1", data: [1, 23] }, + { name: "stat_2", data: [0] }, + { name: "stat_3", data: [45] } + ]) end it "returns stats with non-empty data" do @@ -31,10 +31,10 @@ end it "sums the data of stats with the same name" do - allow(presenter).to receive(:all_stats).with(priority: priority).and_return([ - { name: "stat_1", data: [12, 3] }, - { name: "stat_1", data: [4] } - ]) + allow(presenter).to receive(:all_stats).with(priority:).and_return([ + { name: "stat_1", data: [12, 3] }, + { name: "stat_1", data: [4] } + ]) result = presenter.collection(priority:) diff --git a/decidim-core/spec/queries/decidim/inactive_users_query_spec.rb b/decidim-core/spec/queries/decidim/inactive_users_query_spec.rb index a0e437559452a..f1665a8a87870 100644 --- a/decidim-core/spec/queries/decidim/inactive_users_query_spec.rb +++ b/decidim-core/spec/queries/decidim/inactive_users_query_spec.rb @@ -15,21 +15,21 @@ let!(:inactive_recent_sign_in) { create(:user, organization:, current_sign_in_at: 400.days.ago, created_at: 400.days.ago, extended_data: {}) } let!(:active_recent_sign_in) { create(:user, organization:, current_sign_in_at: 200.days.ago, created_at: 200.days.ago, extended_data: {}) } let!(:user_reminder_due) do - create(:user, organization: organization, + create(:user, organization:, current_sign_in_at: 294.days.ago, created_at: 400.days.ago, extended_data: { "inactivity_notification" => { "notification_type" => "first", "sent_at" => 23.days.ago } }) end let!(:user_ready_for_removal) do - create(:user, organization: organization, + create(:user, organization:, current_sign_in_at: 400.days.ago, created_at: 400.days.ago, extended_data: { "inactivity_notification" => { "notification_type" => "second", "sent_at" => 40.days.ago } }) end let!(:user_logged_in_after_notification) do - create(:user, organization: organization, + create(:user, organization:, current_sign_in_at: 1.day.ago, created_at: 400.days.ago, extended_data: { "inactivity_notification" => { "notification_type" => "second", "sent_at" => 7.days.ago } }) diff --git a/decidim-core/spec/system/editor_spec.rb b/decidim-core/spec/system/editor_spec.rb index 82d0b3ba3b7c6..9f76c6dcec7de 100644 --- a/decidim-core/spec/system/editor_spec.rb +++ b/decidim-core/spec/system/editor_spec.rb @@ -1274,7 +1274,7 @@ def protect_against_forgery? it "allows selecting resource mentions with a slash" do allow(Decidim::SearchableResource).to receive(:where).with( resource_type: %w(Decidim::Proposals::Proposal), - organization: organization, + organization:, decidim_participatory_space: participatory_space, locale: I18n.locale ).and_return(double( diff --git a/decidim-core/spec/system/link_target_spec.rb b/decidim-core/spec/system/link_target_spec.rb index 7afcad6a162e6..1528cf098407f 100644 --- a/decidim-core/spec/system/link_target_spec.rb +++ b/decidim-core/spec/system/link_target_spec.rb @@ -4,8 +4,8 @@ describe "Admin editor link target remains" do let(:organization) { create(:organization) } - let(:admin) { create(:user, :admin, :confirmed, organization: organization) } - let(:participatory_process) { create(:participatory_process, organization: organization) } + let(:admin) { create(:user, :admin, :confirmed, organization:) } + let(:participatory_process) { create(:participatory_process, organization:) } let(:component) { create(:component, manifest_name: "pages", participatory_space: participatory_process) } before do diff --git a/decidim-core/spec/tasks/upgrade/decidim_remove_deleted_users_left_data_spec.rb b/decidim-core/spec/tasks/upgrade/decidim_remove_deleted_users_left_data_spec.rb index 3c04f02e1fbb4..77c83050339e6 100644 --- a/decidim-core/spec/tasks/upgrade/decidim_remove_deleted_users_left_data_spec.rb +++ b/decidim-core/spec/tasks/upgrade/decidim_remove_deleted_users_left_data_spec.rb @@ -15,7 +15,7 @@ create(:reminder, user: deleted_user) create(:private_export, attached_to: deleted_user) create(:identity, user: deleted_user) - create(:follow, followable: deleted_user, user: user) + create(:follow, followable: deleted_user, user:) create(:follow, followable: user, user: deleted_user) create(:member, user: deleted_user) diff --git a/decidim-core/spec/tasks/upgrade/fix_deleted_private_follows_spec.rb b/decidim-core/spec/tasks/upgrade/fix_deleted_private_follows_spec.rb index 6c7e9b2289f51..98a84e8b34067 100644 --- a/decidim-core/spec/tasks/upgrade/fix_deleted_private_follows_spec.rb +++ b/decidim-core/spec/tasks/upgrade/fix_deleted_private_follows_spec.rb @@ -8,7 +8,7 @@ let(:user) { create(:user, :admin, :confirmed, organization:) } let(:second_user) { create(:user, :confirmed, organization:) } let(:component) { create(:dummy_component, :published, participatory_space:) } - let!(:followable) { create(:dummy_resource, component: component, author: user) } + let!(:followable) { create(:dummy_resource, component:, author: user) } let!(:follow) { create(:follow, user:, followable: participatory_space) } let!(:unwanted_follow) { create(:follow, user: second_user, followable: participatory_space) } let!(:resource_follow) { create(:follow, followable:, user:) } diff --git a/decidim-debates/app/views/decidim/debates/debates/index.html.erb b/decidim-debates/app/views/decidim/debates/debates/index.html.erb index 230ae2850bdb6..7286167642cdd 100644 --- a/decidim-debates/app/views/decidim/debates/debates/index.html.erb +++ b/decidim-debates/app/views/decidim/debates/debates/index.html.erb @@ -1,7 +1,7 @@ <% add_decidim_meta_tags( description: translated_attribute(current_participatory_space.short_description), title: t("decidim.components.pagination.page_title", - component_name: component_name, + component_name:, current_page: paginated_debates.current_page, total_pages: paginated_debates.total_pages ), url: debates_url, diff --git a/decidim-dev/config/rubocop/ruby/configuration.yml b/decidim-dev/config/rubocop/ruby/configuration.yml index f02aa6e69ba2f..b35906036929c 100644 --- a/decidim-dev/config/rubocop/ruby/configuration.yml +++ b/decidim-dev/config/rubocop/ruby/configuration.yml @@ -521,6 +521,7 @@ Style/GuardClause: MinBodyLength: 6 Style/HashSyntax: + EnforcedShorthandSyntax: always EnforcedStyle: no_mixed_keys SupportedStyles: # checks for 1.9 syntax (e.g. {a: 1}) for all symbol keys diff --git a/decidim-elections/app/commands/decidim/elections/admin/process_census.rb b/decidim-elections/app/commands/decidim/elections/admin/process_census.rb index f56e06415240e..9bbf4c67ee5a9 100644 --- a/decidim-elections/app/commands/decidim/elections/admin/process_census.rb +++ b/decidim-elections/app/commands/decidim/elections/admin/process_census.rb @@ -9,7 +9,7 @@ class ProcessCensus < Decidim::Commands::UpdateResource def attributes { census_manifest: resource.census.name, - census_settings: census_settings + census_settings: } end diff --git a/decidim-elections/app/commands/decidim/elections/cast_votes.rb b/decidim-elections/app/commands/decidim/elections/cast_votes.rb index 0f5a93655d823..d3842a8c186c2 100644 --- a/decidim-elections/app/commands/decidim/elections/cast_votes.rb +++ b/decidim-elections/app/commands/decidim/elections/cast_votes.rb @@ -40,11 +40,11 @@ def save_votes! voted_questions.each do |question, responses| raise StandardError, "No responses for question #{question.id}" if responses.blank? - question.votes.where(voter_uid: voter_uid).destroy_all + question.votes.where(voter_uid:).destroy_all responses.each do |response_option| question.votes.create!( - voter_uid: voter_uid, - response_option: response_option + voter_uid:, + response_option: ) end end diff --git a/decidim-elections/app/controllers/concerns/decidim/elections/uses_votes_booth.rb b/decidim-elections/app/controllers/concerns/decidim/elections/uses_votes_booth.rb index 6dd6b9f38c4a7..4561b6e6bfbeb 100644 --- a/decidim-elections/app/controllers/concerns/decidim/elections/uses_votes_booth.rb +++ b/decidim-elections/app/controllers/concerns/decidim/elections/uses_votes_booth.rb @@ -91,7 +91,7 @@ def response_chosen?(response_option) def previous_responses @previous_responses ||= election.questions.to_h do |question| - [question.id.to_s, question.votes.where(voter_uid: voter_uid).pluck(:response_option_id).map(&:to_s)] + [question.id.to_s, question.votes.where(voter_uid:).pluck(:response_option_id).map(&:to_s)] end end end diff --git a/decidim-elections/app/controllers/decidim/elections/admin/census_controller.rb b/decidim-elections/app/controllers/decidim/elections/admin/census_controller.rb index d22ef580641ad..833b58c7ccecb 100644 --- a/decidim-elections/app/controllers/decidim/elections/admin/census_controller.rb +++ b/decidim-elections/app/controllers/decidim/elections/admin/census_controller.rb @@ -11,15 +11,15 @@ class CensusController < Admin::ApplicationController before_action :set_census_manifest, only: [:edit, :update] def edit - enforce_permission_to :update, :census, election: election + enforce_permission_to(:update, :census, election:) - @form = form(election.census.admin_form.constantize).from_params(election.census_settings, election: election) if election.census && election.census.admin_form.present? + @form = form(election.census.admin_form.constantize).from_params(election.census_settings, election:) if election.census && election.census.admin_form.present? end def update - enforce_permission_to :update, :census, election: election + enforce_permission_to(:update, :census, election:) - @form = form(election.census.admin_form.constantize).from_params(params, election: election) if election.census.admin_form.present? + @form = form(election.census.admin_form.constantize).from_params(params, election:) if election.census.admin_form.present? ProcessCensus.call(@form, election) do on(:ok) do diff --git a/decidim-elections/app/controllers/decidim/elections/admin/elections_controller.rb b/decidim-elections/app/controllers/decidim/elections/admin/elections_controller.rb index bddf6d142ed39..2b7da14469687 100644 --- a/decidim-elections/app/controllers/decidim/elections/admin/elections_controller.rb +++ b/decidim-elections/app/controllers/decidim/elections/admin/elections_controller.rb @@ -46,7 +46,7 @@ def edit end def update - enforce_permission_to :update, :election, election: election + enforce_permission_to(:update, :election, election:) @form = form(Decidim::Elections::Admin::ElectionForm).from_params(params, current_component:, election:) @@ -64,7 +64,7 @@ def update end def publish - enforce_permission_to :publish, :election, election: election + enforce_permission_to(:publish, :election, election:) PublishElection.call(election, current_user) do on(:ok) do @@ -80,7 +80,7 @@ def publish end def unpublish - enforce_permission_to :unpublish, :election, election: election + enforce_permission_to(:unpublish, :election, election:) Decidim::Elections::Admin::UnpublishElection.call(election, current_user) do on(:ok) do @@ -96,7 +96,7 @@ def unpublish end def dashboard - enforce_permission_to :dashboard, :election, election: election + enforce_permission_to(:dashboard, :election, election:) respond_to do |format| format.html { render :dashboard } @@ -107,7 +107,7 @@ def dashboard end def update_status - enforce_permission_to :update, :election, election: election + enforce_permission_to(:update, :election, election:) status_action = params[:status_action] UpdateElectionStatus.call(status_action, election) do @@ -123,7 +123,7 @@ def update_status end def toggle_census_check - enforce_permission_to :update, :election, election: election + enforce_permission_to(:update, :election, election:) value = ActiveModel::Type::Boolean.new.cast(params[:allow_census_check_before_start]) election.update!(allow_census_check_before_start: value) diff --git a/decidim-elections/app/forms/decidim/elections/censuses/internal_users_form.rb b/decidim-elections/app/forms/decidim/elections/censuses/internal_users_form.rb index 9f4f67e2695e7..58e1d02cc8941 100644 --- a/decidim-elections/app/forms/decidim/elections/censuses/internal_users_form.rb +++ b/decidim-elections/app/forms/decidim/elections/censuses/internal_users_form.rb @@ -29,7 +29,7 @@ def authorizations [ adapter, Decidim::Verifications::Authorizations.new( - organization: organization, + organization:, user: current_user, name: adapter.name ).first diff --git a/decidim-elections/app/models/decidim/elections/vote.rb b/decidim-elections/app/models/decidim/elections/vote.rb index 1af4fdf945dd5..0c161cfdd34ec 100644 --- a/decidim-elections/app/models/decidim/elections/vote.rb +++ b/decidim-elections/app/models/decidim/elections/vote.rb @@ -31,7 +31,7 @@ def response_belong_to_question def max_votable_options return unless question && response_option - return if question.votes.where.not(id: id).where(voter_uid: voter_uid).count < question.max_votable_options + return if question.votes.where.not(id:).where(voter_uid:).count < question.max_votable_options errors.add(:response_option, :invalid) end diff --git a/decidim-elections/app/views/decidim/elections/admin/censuses/_internal_users_form.html.erb b/decidim-elections/app/views/decidim/elections/admin/censuses/_internal_users_form.html.erb index 3457f04c1fcd0..86b35414e858a 100644 --- a/decidim-elections/app/views/decidim/elections/admin/censuses/_internal_users_form.html.erb +++ b/decidim-elections/app/views/decidim/elections/admin/censuses/_internal_users_form.html.erb @@ -10,7 +10,7 @@
<%= builder.label { builder.check_box + builder.text } %>
;"> - <%= render "decidim/elections/admin/censuses/internal_users_options_form", form: form, handler_name: builder.value %> + <%= render "decidim/elections/admin/censuses/internal_users_options_form", form:, handler_name: builder.value %>
<% end %> diff --git a/decidim-elections/app/views/decidim/elections/admin/elections/_election-tr.html.erb b/decidim-elections/app/views/decidim/elections/admin/elections/_election-tr.html.erb index 282943921e40e..21b3255c510be 100644 --- a/decidim-elections/app/views/decidim/elections/admin/elections/_election-tr.html.erb +++ b/decidim-elections/app/views/decidim/elections/admin/elections/_election-tr.html.erb @@ -25,6 +25,6 @@ <%= election.census&.label %> " class="table-list__actions"> - <%= render partial: "decidim/elections/admin/elections/actions", locals: { election: election, view: view } %> + <%= render partial: "decidim/elections/admin/elections/actions", locals: { election:, view: } %> diff --git a/decidim-elections/app/views/decidim/elections/censuses/_internal_users_form.html.erb b/decidim-elections/app/views/decidim/elections/censuses/_internal_users_form.html.erb index 7a286593fc830..2e1c535d2a928 100644 --- a/decidim-elections/app/views/decidim/elections/censuses/_internal_users_form.html.erb +++ b/decidim-elections/app/views/decidim/elections/censuses/_internal_users_form.html.erb @@ -17,7 +17,7 @@
<% end %> <%= link_to t(".exit_button"), exit_path, class: "button button__secondary button__lg w-full mt-12" %> - <%= render "decidim/elections/censuses/submit_button", form: form, disabled: true %> + <%= render "decidim/elections/censuses/submit_button", form:, disabled: true %> <% else %> <% redirect_url = new_election_vote_path(election) %>
@@ -54,7 +54,7 @@ <% end %> <% end %>
- <%= render "decidim/elections/censuses/submit_button", form: form %> + <%= render "decidim/elections/censuses/submit_button", form: %> <% end %> <% else %> <%= render partial: "decidim/devise/shared/login_boxes", locals: { scope: "decidim.elections.censuses.internal_users" } %> diff --git a/decidim-elections/app/views/decidim/elections/censuses/_token_csv_form.html.erb b/decidim-elections/app/views/decidim/elections/censuses/_token_csv_form.html.erb index 7912c3774865a..c3eb7616cbb84 100644 --- a/decidim-elections/app/views/decidim/elections/censuses/_token_csv_form.html.erb +++ b/decidim-elections/app/views/decidim/elections/censuses/_token_csv_form.html.erb @@ -1,4 +1,4 @@ <%= form.text_field :email, label: t(".email"), placeholder: t(".email_placeholder"), autofocus: true, required: true %> <%= form.text_field :token, label: t(".token"), placeholder: t(".token_placeholder"), required: true %> -<%= render "decidim/elections/censuses/submit_button", form: form %> +<%= render "decidim/elections/censuses/submit_button", form: %> diff --git a/decidim-elections/app/views/decidim/elections/elections/index.html.erb b/decidim-elections/app/views/decidim/elections/elections/index.html.erb index 80f249f8b1743..b9e16b4babfd6 100644 --- a/decidim-elections/app/views/decidim/elections/elections/index.html.erb +++ b/decidim-elections/app/views/decidim/elections/elections/index.html.erb @@ -1,7 +1,7 @@ <% add_decidim_meta_tags( description: translated_attribute(current_participatory_space.short_description), title: t("decidim.components.pagination.page_title", - component_name: component_name, + component_name:, current_page: paginated_elections.current_page, total_pages: paginated_elections.total_pages ), url: elections_url, diff --git a/decidim-elections/app/views/decidim/elections/elections/show.html.erb b/decidim-elections/app/views/decidim/elections/elections/show.html.erb index b0d243dba2187..72ffbc413b70a 100644 --- a/decidim-elections/app/views/decidim/elections/elections/show.html.erb +++ b/decidim-elections/app/views/decidim/elections/elections/show.html.erb @@ -9,7 +9,7 @@ resource_locator(election).edit, :update, :election, - election: election + election: ) %> diff --git a/decidim-elections/lib/decidim/elections/engine.rb b/decidim-elections/lib/decidim/elections/engine.rb index a9497ed99a9e5..39d80c0041b72 100644 --- a/decidim-elections/lib/decidim/elections/engine.rb +++ b/decidim-elections/lib/decidim/elections/engine.rb @@ -47,7 +47,7 @@ class Engine < ::Rails::Engine manifest.voter_form_partial = "decidim/elections/censuses/token_csv_form" manifest.after_update_command = "Decidim::Elections::Admin::Censuses::TokenCsv" manifest.user_query do |election| - Decidim::Elections::Voter.where(election: election) + Decidim::Elections::Voter.where(election:) end end diff --git a/decidim-elections/lib/decidim/elections/seeds.rb b/decidim-elections/lib/decidim/elections/seeds.rb index 97e3ac0984c39..5e75f632b4d19 100644 --- a/decidim-elections/lib/decidim/elections/seeds.rb +++ b/decidim-elections/lib/decidim/elections/seeds.rb @@ -89,7 +89,7 @@ def create_questions_for!(election) def create_voters_for!(election) number_of_records.times do |i| Decidim::Elections::Voter.create!( - election: election, + election:, data: { "email" => "user#{i + 1}@example.org", "token" => SecureRandom.hex(6).upcase diff --git a/decidim-elections/lib/decidim/elections/test/vote_controller_examples.rb b/decidim-elections/lib/decidim/elections/test/vote_controller_examples.rb index 00d3f8692b2f1..fa86c9781fe97 100644 --- a/decidim-elections/lib/decidim/elections/test/vote_controller_examples.rb +++ b/decidim-elections/lib/decidim/elections/test/vote_controller_examples.rb @@ -2,9 +2,9 @@ def do_action(action) if [:show, :confirm, :waiting, :receipt].include?(action) - get action, params: params + get(action, params:) else - patch action, params: params + patch action, params: end end @@ -39,7 +39,7 @@ def do_action(action) shared_examples "an authenticated vote controller" do describe "GET new" do it "renders the new vote form" do - get :new, params: params + get(:new, params:) expect(response).to have_http_status(:ok) expect(assigns(:form)).to be_a(Decidim::Elections::Censuses::InternalUsersForm) expect(subject).to render_template("decidim/elections/votes/new") @@ -52,7 +52,7 @@ def do_action(action) it "redirects to the question path" do expect(controller).to receive(:redirect_to).with(action: :show, id: question) - get :new, params: params + get(:new, params:) expect(controller.send(:session_authenticated?)).to be true expect(response).to render_template("decidim/elections/votes/new") @@ -63,7 +63,7 @@ def do_action(action) describe "POST create" do it "renders the new form with errors when the form is invalid" do expect(controller).to receive(:redirect_to).with(action: :new) - post :create, params: params + post(:create, params:) expect(controller.send(:session_authenticated?)).to be false expect(controller.send(:voter_uid)).to be_nil @@ -77,7 +77,7 @@ def do_action(action) it "creates the session credentials and redirects to form again" do expect(controller).to receive(:redirect_to).with(action: :show, id: question) - post :create, params: params + post(:create, params:) expect(session[:session_attributes]).to be_present expect(controller.send(:session_authenticated?)).to be true diff --git a/decidim-elections/lib/decidim/elections/test/vote_examples.rb b/decidim-elections/lib/decidim/elections/test/vote_examples.rb index 8d82f8be4bdc2..dcf717d30b8d3 100644 --- a/decidim-elections/lib/decidim/elections/test/vote_examples.rb +++ b/decidim-elections/lib/decidim/elections/test/vote_examples.rb @@ -44,7 +44,7 @@ def fill_in_votes click_on "Exit the voting booth" expect(page).to have_current_path(election_path) expect(page).to have_content("You have already voted.") - expect(election.votes.where(voter_uid: voter_uid).size).to eq(3) + expect(election.votes.where(voter_uid:).size).to eq(3) end shared_examples "a votable election" do @@ -163,7 +163,7 @@ def fill_in_votes click_on "Exit the voting booth" expect(page).to have_current_path(election_path) expect(page).to have_content("You have already voted.") - expect(election.votes.where(voter_uid: voter_uid).size).to eq(2) + expect(election.votes.where(voter_uid:).size).to eq(2) end end @@ -186,7 +186,7 @@ def fill_in_votes click_on "Exit the voting booth" expect(page).to have_current_path(election_path) expect(page).to have_content("You have already voted.") - expect(election.votes.where(voter_uid: voter_uid).size).to eq(3) + expect(election.votes.where(voter_uid:).size).to eq(3) end end @@ -214,6 +214,6 @@ def fill_in_votes click_on "Exit the voting booth" expect(page).to have_current_path(election_path) expect(page).to have_content("You have already voted.") - expect(election.votes.where(voter_uid: voter_uid).size).to eq(3) + expect(election.votes.where(voter_uid:).size).to eq(3) end end diff --git a/decidim-elections/spec/commands/decidim/elections/admin/create_election_spec.rb b/decidim-elections/spec/commands/decidim/elections/admin/create_election_spec.rb index 50fcb122e611b..a50d602443b99 100644 --- a/decidim-elections/spec/commands/decidim/elections/admin/create_election_spec.rb +++ b/decidim-elections/spec/commands/decidim/elections/admin/create_election_spec.rb @@ -84,8 +84,8 @@ module Admin Decidim::Elections::Election, current_user, hash_including( - title: title, - description: description, + title:, + description:, start_at:, end_at:, results_availability: "after_end", diff --git a/decidim-elections/spec/commands/decidim/elections/admin/update_questions_spec.rb b/decidim-elections/spec/commands/decidim/elections/admin/update_questions_spec.rb index b1d35796b0490..a4b1b1ccd6734 100644 --- a/decidim-elections/spec/commands/decidim/elections/admin/update_questions_spec.rb +++ b/decidim-elections/spec/commands/decidim/elections/admin/update_questions_spec.rb @@ -25,7 +25,7 @@ module Admin let(:second_question_second_option) { second_question.response_options.second } let(:context_params) do - { current_organization: organization, current_user: current_user } + { current_organization: organization, current_user: } end context "when updating an existing question" do @@ -180,7 +180,7 @@ module Admin context "when the form is invalid" do let(:form) do - double("Form", invalid?: true, current_user: current_user, current_organization: organization, questions: []) + double("Form", invalid?: true, current_user:, current_organization: organization, questions: []) end let(:command) { described_class.new(form, election) } diff --git a/decidim-elections/spec/controllers/decidim/elections/admin/census_controller_spec.rb b/decidim-elections/spec/controllers/decidim/elections/admin/census_controller_spec.rb index 3b5c7b6480f5c..634a57445fe22 100644 --- a/decidim-elections/spec/controllers/decidim/elections/admin/census_controller_spec.rb +++ b/decidim-elections/spec/controllers/decidim/elections/admin/census_controller_spec.rb @@ -38,7 +38,7 @@ module Admin let(:params) { { id: election.id, manifest: :token_csv, file: valid_file } } it "processes the census and redirects with a success message" do - patch :update, params: params + patch(:update, params:) expect(flash[:notice]).to eq(I18n.t("decidim.elections.admin.census.update.success")) expect(response).to redirect_to(dashboard_election_path) @@ -49,7 +49,7 @@ module Admin let(:params) { { id: election.id, manifest: :token_csv, file: invalid_file } } it "renders the edit view with an error message" do - patch :update, params: params + patch(:update, params:) expect(flash[:alert]).to eq(I18n.t("decidim.elections.admin.census.update.error")) expect(response).to render_template(:edit) diff --git a/decidim-elections/spec/controllers/decidim/elections/census_checks_controller_spec.rb b/decidim-elections/spec/controllers/decidim/elections/census_checks_controller_spec.rb index 531918bf2bda7..34569c44bf153 100644 --- a/decidim-elections/spec/controllers/decidim/elections/census_checks_controller_spec.rb +++ b/decidim-elections/spec/controllers/decidim/elections/census_checks_controller_spec.rb @@ -28,7 +28,7 @@ module Elections describe "GET new" do it "renders the census check form" do - get :new, params: params + get(:new, params:) expect(response).to have_http_status(:ok) expect(assigns(:form)).to be_present @@ -40,7 +40,7 @@ module Elections end it "redirects to the success page" do - get :new, params: params + get(:new, params:) expect(response).to redirect_to(census_check_path) end @@ -66,7 +66,7 @@ module Elections describe "GET show" do it "redirects to the form when the session is not authenticated" do - get :show, params: params + get(:show, params:) expect(response).to redirect_to(new_census_check_path) expect(flash[:alert]).to eq(I18n.t("decidim.elections.votes.check_census.failed")) @@ -78,7 +78,7 @@ module Elections end it "renders the success page" do - get :show, params: params + get(:show, params:) expect(response).to have_http_status(:ok) expect(subject).to render_template(:show) diff --git a/decidim-elections/spec/controllers/decidim/elections/per_question_votes_controller_spec.rb b/decidim-elections/spec/controllers/decidim/elections/per_question_votes_controller_spec.rb index abab9d9912189..4ff53c96512f8 100644 --- a/decidim-elections/spec/controllers/decidim/elections/per_question_votes_controller_spec.rb +++ b/decidim-elections/spec/controllers/decidim/elections/per_question_votes_controller_spec.rb @@ -9,7 +9,7 @@ module Elections let(:user) { create(:user, :confirmed, organization: component.organization) } let(:component) { create(:elections_component) } let(:election) { create(:election, :published, :with_internal_users_census, :per_question, :ongoing, component:) } - let!(:existing_vote) { create(:election_vote, question: question, response_option: question.response_options.first, voter_uid: "some-id") } + let!(:existing_vote) { create(:election_vote, question:, response_option: question.response_options.first, voter_uid: "some-id") } let!(:question) { create(:election_question, :with_response_options, :voting_enabled, election:) } let!(:second_question) { create(:election_question, :with_response_options, :voting_enabled, election:) } @@ -49,7 +49,7 @@ module Elections it_behaves_like "a redirect to the waiting room", :show it "renders the voting form" do - get :show, params: params + get(:show, params:) expect(response).to have_http_status(:ok) expect(controller.helpers.question).to eq(question) expect(subject).to render_template(:show) @@ -58,14 +58,14 @@ module Elections it "redirects to the next question if the current question is not enabled" do question.update(voting_enabled_at: nil) expect(controller).to receive(:redirect_to).with(action: :show, id: second_question) - get :show, params: params + get(:show, params:) expect(response).to have_http_status(:ok) end it "redirects to the next question if the current question has published results" do question.update(published_results_at: Time.current) expect(controller).to receive(:redirect_to).with(action: :show, id: second_question) - get :show, params: params + get(:show, params:) expect(response).to have_http_status(:ok) end end @@ -152,7 +152,7 @@ module Elections allow(controller).to receive(:votes_buffer).and_return({ question.id.to_s => [question.response_options.first.id] }) expect(controller).to receive(:redirect_to).with(action: :show, id: second_question) - get :waiting, params: params + get(:waiting, params:) expect(response).to have_http_status(:ok) end @@ -163,7 +163,7 @@ module Elections it "redirects to the non voted question" do expect(controller).to receive(:redirect_to).with(action: :show, id: question) - get :waiting, params: params + get(:waiting, params:) expect(response).to have_http_status(:ok) end @@ -172,13 +172,13 @@ module Elections it "redirects to the non voted question" do expect(controller).to receive(:redirect_to).with(action: :show, id: question) - get :waiting, params: params + get(:waiting, params:) expect(response).to have_http_status(:ok) end it "renders the waiting page if votes_buffer exist" do allow(controller).to receive(:votes_buffer).and_return({ question.id.to_s => [question.response_options.first.id] }) - get :waiting, params: params + get(:waiting, params:) expect(response).to have_http_status(:ok) expect(subject).to render_template(:waiting) end @@ -191,7 +191,7 @@ module Elections allow(controller).to receive(:votes_buffer).and_return({ question.id.to_s => [question.response_options.first.id] }) expect(controller).to receive(:url_for).with(action: :show, id: second_question) - get :waiting, params: params, format: :json + get :waiting, params:, format: :json expect(response).to have_http_status(:ok) expect(JSON.parse(response.body)).to have_key("url") end @@ -215,13 +215,13 @@ module Elections end it "redirects to the election path" do - get :receipt, params: params + get(:receipt, params:) expect(response).to redirect_to(election_path) end context "when the election has votes for the voter UID" do before do - create(:election_vote, voter_uid: session[:voter_uid], question: question, response_option: question.response_options.first) + create(:election_vote, voter_uid: session[:voter_uid], question:, response_option: question.response_options.first) create(:election_vote, voter_uid: session[:voter_uid], question: second_question, response_option: second_question.response_options.first) end @@ -230,7 +230,7 @@ module Elections it "renders the receipt page without clearing session" do expect(controller.send(:votes_buffer)).not_to receive(:clear) expect(controller.send(:session_attributes)).not_to receive(:clear) - get :receipt, params: params + get(:receipt, params:) expect(response).to have_http_status(:ok) expect(subject).to render_template(:receipt) end diff --git a/decidim-elections/spec/controllers/decidim/elections/votes_controller_spec.rb b/decidim-elections/spec/controllers/decidim/elections/votes_controller_spec.rb index c086c74b3366b..1da30d6e346b4 100644 --- a/decidim-elections/spec/controllers/decidim/elections/votes_controller_spec.rb +++ b/decidim-elections/spec/controllers/decidim/elections/votes_controller_spec.rb @@ -9,7 +9,7 @@ module Elections let(:user) { create(:user, :confirmed, organization: component.organization) } let(:component) { create(:elections_component) } let(:election) { create(:election, :published, :with_internal_users_census, :ongoing, component:) } - let!(:existing_vote) { create(:election_vote, question: question, response_option: question.response_options.first, voter_uid: "some-id") } + let!(:existing_vote) { create(:election_vote, question:, response_option: question.response_options.first, voter_uid: "some-id") } let!(:question) { create(:election_question, :with_response_options, :voting_enabled, election:) } let!(:second_question) { create(:election_question, :with_response_options, :voting_enabled, election:) } @@ -52,7 +52,7 @@ module Elections end it "renders the voting form" do - get :show, params: params + get(:show, params:) expect(response).to have_http_status(:ok) expect(controller.helpers.question).to eq(question) expect(subject).to render_template(:show) @@ -158,7 +158,7 @@ module Elections end it "renders the confirmation page" do - get :confirm, params: params + get(:confirm, params:) expect(response).to have_http_status(:ok) expect(subject).to render_template(:confirm) end @@ -184,7 +184,7 @@ module Elections it "casts the votes and redirects to the receipt page" do expect(controller.send(:votes_buffer)).to receive(:clear) expect(controller.send(:session_attributes)).to receive(:clear) - post :cast, params: params + post(:cast, params:) expect(session[:voter_uid]).to eq(user.to_global_id.to_s) expect(response).to redirect_to(receipt_election_votes_path) expect(flash[:notice]).to eq(I18n.t("votes.cast.success", scope: "decidim.elections")) @@ -198,7 +198,7 @@ module Elections end it "redirects to the confirm page if votes are incomplete" do - post :cast, params: params + post(:cast, params:) expect(response).to redirect_to(confirm_election_votes_path) expect(flash[:alert]).to eq(I18n.t("votes.cast.invalid", scope: "decidim.elections")) end @@ -208,7 +208,7 @@ module Elections describe "GET receipt" do it "redirects to the election path" do - get :receipt, params: params + get(:receipt, params:) expect(response).to redirect_to(election_path) end @@ -218,7 +218,7 @@ module Elections end it "redirects to the election path" do - get :receipt, params: params + get(:receipt, params:) expect(response).to redirect_to(election_path) end end @@ -232,13 +232,13 @@ module Elections context "when the election has votes for the voter UID" do before do - create(:election_vote, voter_uid: session[:voter_uid], question: question, response_option: question.response_options.first) + create(:election_vote, voter_uid: session[:voter_uid], question:, response_option: question.response_options.first) end it "renders the receipt page and clears votes buffer" do expect(controller.send(:votes_buffer)).to receive(:clear) expect(controller.send(:session_attributes)).not_to receive(:clear) - get :receipt, params: params + get(:receipt, params:) expect(response).to have_http_status(:ok) expect(subject).to render_template(:receipt) end diff --git a/decidim-elections/spec/forms/decidim/elections/admin/question_form_spec.rb b/decidim-elections/spec/forms/decidim/elections/admin/question_form_spec.rb index 75e3f42235347..ab0d67abc50a7 100644 --- a/decidim-elections/spec/forms/decidim/elections/admin/question_form_spec.rb +++ b/decidim-elections/spec/forms/decidim/elections/admin/question_form_spec.rb @@ -20,10 +20,10 @@ module Admin let(:attributes) do { - body_en: body_en, - description_en: description_en, - question_type: question_type, - response_options: response_options + body_en:, + description_en:, + question_type:, + response_options: } end @@ -58,11 +58,11 @@ module Admin describe "max_choices validation" do let(:attributes) do { - body_en: body_en, - description_en: description_en, - question_type: question_type, - response_options: response_options, - max_choices: max_choices + body_en:, + description_en:, + question_type:, + response_options:, + max_choices: } end diff --git a/decidim-elections/spec/forms/decidim/elections/admin/questions_form_spec.rb b/decidim-elections/spec/forms/decidim/elections/admin/questions_form_spec.rb index 3df91827c8233..98dc848d85b86 100644 --- a/decidim-elections/spec/forms/decidim/elections/admin/questions_form_spec.rb +++ b/decidim-elections/spec/forms/decidim/elections/admin/questions_form_spec.rb @@ -35,7 +35,7 @@ module Admin subject(:form) do described_class.from_params(attributes).with_context( - current_organization: current_organization + current_organization: ) end diff --git a/decidim-elections/spec/forms/decidim/elections/admin/token_csv_form_spec.rb b/decidim-elections/spec/forms/decidim/elections/admin/token_csv_form_spec.rb index fcadf003b446c..e0275029f51eb 100644 --- a/decidim-elections/spec/forms/decidim/elections/admin/token_csv_form_spec.rb +++ b/decidim-elections/spec/forms/decidim/elections/admin/token_csv_form_spec.rb @@ -8,7 +8,7 @@ module Admin module Censuses describe TokenCsvForm do let(:organization) { create(:organization) } - let(:attributes) { { file: file, remove_all: remove_all } } + let(:attributes) { { file:, remove_all: } } let(:remove_all) { false } subject { described_class.new(attributes).with_context(current_organization: organization) } diff --git a/decidim-elections/spec/permissions/decidim/elections/admin/permissions_spec.rb b/decidim-elections/spec/permissions/decidim/elections/admin/permissions_spec.rb index 4175e951d3510..b9a8dbee9c78c 100644 --- a/decidim-elections/spec/permissions/decidim/elections/admin/permissions_spec.rb +++ b/decidim-elections/spec/permissions/decidim/elections/admin/permissions_spec.rb @@ -14,7 +14,7 @@ election: } end - let(:permission_action) { Decidim::PermissionAction.new(scope: scope, action: action_name, subject: action_subject) } + let(:permission_action) { Decidim::PermissionAction.new(scope:, action: action_name, subject: action_subject) } let(:scope) { :admin } let(:action_name) { :foo } let(:action_subject) { :foo } diff --git a/decidim-elections/spec/presenters/decidim/elections/election_presenter_spec.rb b/decidim-elections/spec/presenters/decidim/elections/election_presenter_spec.rb index 11ea9610f36f0..60b72f7b81015 100644 --- a/decidim-elections/spec/presenters/decidim/elections/election_presenter_spec.rb +++ b/decidim-elections/spec/presenters/decidim/elections/election_presenter_spec.rb @@ -13,7 +13,7 @@ module Elections let(:election) do create(:election, - component: component, + component:, title: { en: "Test election" }) end diff --git a/decidim-elections/spec/system/admin/admin_manages_election_census_spec.rb b/decidim-elections/spec/system/admin/admin_manages_election_census_spec.rb index e3518d6cbd15c..94ca9124e8c44 100644 --- a/decidim-elections/spec/system/admin/admin_manages_election_census_spec.rb +++ b/decidim-elections/spec/system/admin/admin_manages_election_census_spec.rb @@ -99,7 +99,7 @@ let(:available_authorizations) { %w(dummy_authorization_handler another_dummy_authorization_handler) } before do - organization.update!(available_authorizations: available_authorizations) + organization.update!(available_authorizations:) end context "when no verification handlers are selected" do diff --git a/decidim-forms/app/commands/decidim/forms/response_questionnaire.rb b/decidim-forms/app/commands/decidim/forms/response_questionnaire.rb index 6f0046c414fe2..3ae31237c4890 100644 --- a/decidim-forms/app/commands/decidim/forms/response_questionnaire.rb +++ b/decidim-forms/app/commands/decidim/forms/response_questionnaire.rb @@ -79,7 +79,7 @@ def build_choices(response, form_response) end def clear_responses! - Response.where(questionnaire: questionnaire, user: current_user, session_token: form.context.session_token, ip_hash: form.context.ip_hash).destroy_all + Response.where(questionnaire:, user: current_user, session_token: form.context.session_token, ip_hash: form.context.ip_hash).destroy_all end def response_questionnaire diff --git a/decidim-forms/app/views/decidim/forms/questionnaires/show.html.erb b/decidim-forms/app/views/decidim/forms/questionnaires/show.html.erb index c2fc3f24d5d01..4da5e4778d96b 100644 --- a/decidim-forms/app/views/decidim/forms/questionnaires/show.html.erb +++ b/decidim-forms/app/views/decidim/forms/questionnaires/show.html.erb @@ -39,7 +39,7 @@ <% else %> <% body = t("decidim.forms.questionnaires.show.questionnaire_responded.body") %> <% end %> - <%= cell("decidim/announcement", { title: t("decidim.forms.questionnaires.show.questionnaire_responded.title"), body: body }) %> + <%= cell("decidim/announcement", { title: t("decidim.forms.questionnaires.show.questionnaire_responded.title"), body: }) %> <% else %> <% if @form.responses_by_step.flatten.empty? %> <%= cell("decidim/announcement", t("decidim.forms.questionnaires.show.empty")) %> diff --git a/decidim-forms/spec/forms/decidim/forms/admin/display_condition_form_spec.rb b/decidim-forms/spec/forms/decidim/forms/admin/display_condition_form_spec.rb index 96eb8fcddecd0..972183d127152 100644 --- a/decidim-forms/spec/forms/decidim/forms/admin/display_condition_form_spec.rb +++ b/decidim-forms/spec/forms/decidim/forms/admin/display_condition_form_spec.rb @@ -112,7 +112,7 @@ module Admin describe "#questions_for_select" do let(:questions_for_select) { subject.questions_for_select(questionnaire, question.id) } - let!(:separator_question) { create(:questionnaire_question, questionnaire: questionnaire, question_type: "separator") } + let!(:separator_question) { create(:questionnaire_question, questionnaire:, question_type: "separator") } it "returns an array of arrays containing translated body, id, and a hash" do expect(questions_for_select.first.first).to eq( diff --git a/decidim-initiatives/app/views/decidim/initiatives/admin/exports/_dropdown.html.erb b/decidim-initiatives/app/views/decidim/initiatives/admin/exports/_dropdown.html.erb index 1d27d0b782ea1..91c292cca779c 100644 --- a/decidim-initiatives/app/views/decidim/initiatives/admin/exports/_dropdown.html.erb +++ b/decidim-initiatives/app/views/decidim/initiatives/admin/exports/_dropdown.html.erb @@ -13,7 +13,7 @@
+
+ <%= form.check_box :has_members, help_text: t(".has_members_help") %> +
+
<%= form.check_box :private_space %>

<%= t(".private_notice") %>

diff --git a/decidim-assemblies/config/locales/en.yml b/decidim-assemblies/config/locales/en.yml index 5176901ad487a..dd02d255f3e66 100644 --- a/decidim-assemblies/config/locales/en.yml +++ b/decidim-assemblies/config/locales/en.yml @@ -26,6 +26,7 @@ en: duration: Duration facebook: Facebook github: GitHub + has_members: This space has members hero_image: Home image import_attachments: Import attachments import_categories: Import categories @@ -249,12 +250,13 @@ en: define_taxonomy_filters: Please define some filters for this participatory space before using this setting. duration: Duration duration_help: If the duration of this assembly is limited, select the end date. Otherwise, it will appear as indefinite. + has_members_help: You will be able to create and publish members images: Images included_at_help: Select the date when this assembly was added to the platform. It does not necessarily have to be the same as the creation date. metadata: Metadata no_taxonomy_filters_found: No taxonomy filters found. other: Other - private_notice: You will be able to manage members after setting it as private + private_notice: Makes the space not visible for visitors and only for members (except if it is also transparent) select_a_created_by: Select a created by select_parent_assembly: Select parent assembly slug_help_html: 'URL slugs are used to generate the URLs that point to this assembly. Only accepts letters, numbers and dashes, and must start with a letter. Example: %{url}' diff --git a/decidim-assemblies/db/migrate/20251205120000_add_has_members_to_decidim_assemblies.rb b/decidim-assemblies/db/migrate/20251205120000_add_has_members_to_decidim_assemblies.rb new file mode 100644 index 0000000000000..02a313a9a5d78 --- /dev/null +++ b/decidim-assemblies/db/migrate/20251205120000_add_has_members_to_decidim_assemblies.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddHasMembersToDecidimAssemblies < ActiveRecord::Migration[7.0] + def change + add_column :decidim_assemblies, :has_members, :boolean, default: false + end +end diff --git a/decidim-assemblies/lib/decidim/assemblies/test/factories.rb b/decidim-assemblies/lib/decidim/assemblies/test/factories.rb index ba2f11e6d8dab..cefe5ad3d9a55 100644 --- a/decidim-assemblies/lib/decidim/assemblies/test/factories.rb +++ b/decidim-assemblies/lib/decidim/assemblies/test/factories.rb @@ -37,6 +37,7 @@ participatory_scope { generate_localized_title(:assembly_participatory_scope, skip_injection:) } participatory_structure { generate_localized_title(:assembly_participatory_structure, skip_injection:) } private_space { false } + has_members { false } purpose_of_action { generate_localized_description(:assembly_purpose_of_action, skip_injection:) } composition { generate_localized_description(:assembly_composition, skip_injection:) } creation_date { 1.month.ago } diff --git a/decidim-assemblies/spec/commands/create_assembly_spec.rb b/decidim-assemblies/spec/commands/create_assembly_spec.rb index c9a899f4d67bd..2c1b8d4ec858d 100644 --- a/decidim-assemblies/spec/commands/create_assembly_spec.rb +++ b/decidim-assemblies/spec/commands/create_assembly_spec.rb @@ -46,6 +46,7 @@ module Decidim::Assemblies organization:, taxonomizations:, parent: nil, + has_members: false, private_space: false, errors:, participatory_processes_ids: related_process_ids, diff --git a/decidim-assemblies/spec/controllers/members_controller_spec.rb b/decidim-assemblies/spec/controllers/members_controller_spec.rb index cb6908b31dc9a..ca92debb9a51b 100644 --- a/decidim-assemblies/spec/controllers/members_controller_spec.rb +++ b/decidim-assemblies/spec/controllers/members_controller_spec.rb @@ -14,7 +14,7 @@ def decidim_assemblies let(:organization) { create(:organization) } - let!(:privatable_to) do + let!(:participatory_space) do create( :assembly, :published, @@ -23,10 +23,10 @@ def decidim_assemblies ) end - let(:destination_path) { decidim_assemblies.assembly_path(privatable_to, locale: I18n.locale) } + let(:destination_path) { decidim_assemblies.assembly_path(participatory_space, locale: I18n.locale) } let(:slug_param) { "assembly_slug" } - let(:slug) { privatable_to.slug } + let(:slug) { participatory_space.slug } it_behaves_like "participatory space members page examples" end diff --git a/decidim-assemblies/spec/forms/assembly_form_spec.rb b/decidim-assemblies/spec/forms/assembly_form_spec.rb index 0097da852d875..0924b5b1705b0 100644 --- a/decidim-assemblies/spec/forms/assembly_form_spec.rb +++ b/decidim-assemblies/spec/forms/assembly_form_spec.rb @@ -42,6 +42,7 @@ module Admin let(:slug) { "slug" } let(:attachment) { upload_test_file(Decidim::Dev.test_file("city.jpeg", "image/jpeg")) } let(:private_space) { true } + let(:has_members) { true } let(:purpose_of_action) do { en: "Purpose of action", @@ -129,6 +130,7 @@ module Admin "banner_image" => attachment, "slug" => slug, "private_space" => private_space, + "has_members" => has_members, "purpose_of_action_en" => purpose_of_action[:en], "purpose_of_action_es" => purpose_of_action[:es], "purpose_of_action_ca" => purpose_of_action[:ca], @@ -165,6 +167,18 @@ module Admin } end + context "when has_members is true" do + let(:has_members) { true } + + it { is_expected.to be_valid } + end + + context "when has_members is false" do + let(:has_members) { false } + + it { is_expected.to be_valid } + end + context "when everything is OK" do it { is_expected.to be_valid } end diff --git a/decidim-assemblies/spec/permissions/decidim/assemblies/permissions_spec.rb b/decidim-assemblies/spec/permissions/decidim/assemblies/permissions_spec.rb index 4247f79eddfe9..ca734f95813db 100644 --- a/decidim-assemblies/spec/permissions/decidim/assemblies/permissions_spec.rb +++ b/decidim-assemblies/spec/permissions/decidim/assemblies/permissions_spec.rb @@ -429,8 +429,8 @@ it_behaves_like "allows any action on subject", :assembly it_behaves_like "allows any action on subject", :assembly_user_role - context "when private assembly" do - let(:assembly) { create(:assembly, organization:, private_space: true) } + context "when assembly has members" do + let(:assembly) { create(:assembly, organization:, has_members: true) } let!(:context) { { current_participatory_space: assembly } } it_behaves_like "allows any action on subject", :space_member @@ -463,8 +463,8 @@ it_behaves_like "allows any action on subject", :assembly it_behaves_like "allows any action on subject", :assembly_user_role - context "when private assembly" do - let(:assembly) { create(:assembly, organization:, private_space: true) } + context "when assembly has members" do + let(:assembly) { create(:assembly, organization:, has_members: true) } let!(:context) { { current_participatory_space: assembly } } it_behaves_like "allows any action on subject", :space_member diff --git a/decidim-assemblies/spec/shared/manage_assembly_members_examples.rb b/decidim-assemblies/spec/shared/manage_assembly_members_examples.rb deleted file mode 100644 index 43f5e14140cd0..0000000000000 --- a/decidim-assemblies/spec/shared/manage_assembly_members_examples.rb +++ /dev/null @@ -1,101 +0,0 @@ -# frozen_string_literal: true - -shared_examples "manage assembly members examples" do - let(:other_user) { create(:user, organization:, email: "my_email@example.org") } - - let!(:assembly_member) { create(:assembly_member, user:, privatable_to: assembly) } - - before do - switch_to_host(organization.host) - login_as user, scope: :user - visit decidim_admin_assemblies.edit_assembly_path(assembly) - within_admin_sidebar_menu do - click_on "Members" - end - end - - it "shows assembly member list" do - within "#members table" do - expect(page).to have_content(assembly_member.user.email) - end - end - - it "creates a new assembly members" do - click_on "New member" - - within ".new_member" do - fill_in :member_name, with: "John Doe" - fill_in :member_email, with: other_user.email - - find("*[type=submit]").click - end - - expect(page).to have_admin_callout("successfully") - - within "#members table" do - expect(page).to have_content(other_user.email) - end - - visit decidim_admin.root_path - expect(page).to have_content("invited #{other_user.name} to be a member") - end - - describe "when import a batch of members from csv" do - it "import a batch of members" do - click_on "Import via CSV" - - # The CSV has no headers - expect(Decidim::Admin::ParticipatorySpace::ImportMemberCsvJob).to receive(:perform_later).once.ordered.with("john.doe@example.org", "John Doe", assembly, user) - expect(Decidim::Admin::ParticipatorySpace::ImportMemberCsvJob).to receive(:perform_later).once.ordered.with("jane.doe@example.org", "Jane Doe", assembly, user) - dynamically_attach_file(:member_csv_import_file, Decidim::Dev.asset("import_members.csv")) - perform_enqueued_jobs { click_on "Upload" } - - expect(page).to have_content("CSV file uploaded successfully") - end - end - - describe "when managing different users" do - before do - create(:assembly_member, user: other_user, privatable_to: assembly) - visit current_path - end - - it "deletes an assembly_member" do - within "#members tr", text: other_user.email do - find("button[data-controller='dropdown']").click - accept_confirm { click_on "Delete" } - end - - expect(page).to have_admin_callout("successfully") - - within "#members table" do - expect(page).to have_no_content(other_user.email) - end - end - - context "when the user has not accepted the invitation" do - before do - form = Decidim::Admin::ParticipatorySpace::MemberForm.from_params( - name: "test", - email: "test@example.org" - ) - - Decidim::Admin::ParticipatorySpace::CreateMember.call( - form, - assembly - ) - - visit current_path - end - - it "resends the invitation to the user" do - within "#members tr", text: "test@example.org" do - find("button[data-controller='dropdown']").click - click_on "Resend invitation" - end - - expect(page).to have_admin_callout("successfully") - end - end - end -end diff --git a/decidim-assemblies/spec/system/admin/admin_filters_assemblies_private_space_users_spec.rb b/decidim-assemblies/spec/system/admin/admin_filters_assemblies_members_spec.rb similarity index 78% rename from decidim-assemblies/spec/system/admin/admin_filters_assemblies_private_space_users_spec.rb rename to decidim-assemblies/spec/system/admin/admin_filters_assemblies_members_spec.rb index fb89bd388dffe..42c8ee733c8bf 100644 --- a/decidim-assemblies/spec/system/admin/admin_filters_assemblies_private_space_users_spec.rb +++ b/decidim-assemblies/spec/system/admin/admin_filters_assemblies_members_spec.rb @@ -2,17 +2,17 @@ require "spec_helper" -describe "Admin filters assemblies private space users" do +describe "Admin filters assemblies members" do include_context "with filterable context" let(:organization) { create(:organization) } let!(:user) { create(:user, :admin, :confirmed, organization:) } - let(:assembly) { create(:assembly, organization:, private_space: true) } + let(:assembly) { create(:assembly, organization:, has_members: true) } let!(:invited_user1) { create(:user, name:, organization:, invitation_sent_at: 1.day.ago, invitation_accepted_at: Time.current) } - let!(:invited_member1) { create(:assembly_member, user: invited_user1, privatable_to: assembly) } + let!(:invited_member1) { create(:assembly_member, user: invited_user1, participatory_space: assembly) } let!(:invited_user2) { create(:user, email:, organization:) } - let!(:invited_member2) { create(:assembly_member, user: invited_user2, privatable_to: assembly) } + let!(:invited_member2) { create(:assembly_member, user: invited_user2, participatory_space: assembly) } let(:name) { "Dummy Name" } let(:email) { "dummy_email@example.org" } @@ -25,7 +25,7 @@ visit decidim_admin_assemblies.members_path(assembly_slug: assembly.slug) end - context "when managing private space" do + context "when managing assembly with members" do before do switch_to_host(organization.host) login_as user, scope: :user @@ -39,8 +39,8 @@ include_examples "searchable participatory space users" end - context "when managing members in a public process" do - let(:assembly) { create(:assembly, organization:, private_space: false) } + context "when trying to manage members and the space does not have members" do + let(:assembly) { create(:assembly, organization:, has_members: false) } it "restricts access" do expect(page).to have_admin_callout("You are not authorized to perform this action.") @@ -48,7 +48,7 @@ end describe "when publishing all members" do - let!(:member) { create(:member, :unpublished, user:, privatable_to: assembly) } + let!(:member) { create(:member, :unpublished, user:, participatory_space: assembly) } it "publishes all members" do click_on "Publish all" diff --git a/decidim-assemblies/spec/system/admin/admin_manages_assemblies_spec.rb b/decidim-assemblies/spec/system/admin/admin_manages_assemblies_spec.rb index 0d131cb42e28e..36fdddf06558f 100644 --- a/decidim-assemblies/spec/system/admin/admin_manages_assemblies_spec.rb +++ b/decidim-assemblies/spec/system/admin/admin_manages_assemblies_spec.rb @@ -12,7 +12,7 @@ let(:model_name) { assembly.class.model_name } context "when conditionally displaying member menu entry" do - let!(:my_space) { create(:assembly, organization:, private_space:) } + let!(:my_space) { create(:assembly, organization:, has_members:) } before do switch_to_host(organization.host) @@ -21,20 +21,20 @@ click_on translated(my_space.title) end - context "when the participatory space is private" do - let(:private_space) { true } + context "when the participatory space has members" do + let(:has_members) { true } - it "hides the member menu entry" do + it "shows the member menu entry" do within_admin_sidebar_menu do expect(page).to have_content("Members") end end end - context "when the participatory space is public" do - let(:private_space) { false } + context "when the participatory space has not members" do + let(:has_members) { false } - it "shows the member menu entry" do + it "hides the member menu entry" do within_admin_sidebar_menu do expect(page).to have_no_content("Members") end diff --git a/decidim-assemblies/spec/system/admin/admin_manages_assembly_members_spec.rb b/decidim-assemblies/spec/system/admin/admin_manages_assembly_members_spec.rb index 29e30dd8ba564..c4d95865f735c 100644 --- a/decidim-assemblies/spec/system/admin/admin_manages_assembly_members_spec.rb +++ b/decidim-assemblies/spec/system/admin/admin_manages_assembly_members_spec.rb @@ -1,11 +1,13 @@ # frozen_string_literal: true require "spec_helper" +require "decidim/admin/test/admin_members_shared_examples" describe "Admin manages assembly members" do let!(:user) { create(:user, :admin, :confirmed, organization:) } let(:organization) { create(:organization) } - let!(:assembly) { create(:assembly, organization:, private_space: true) } + let!(:participatory_space) { create(:assembly, organization:, has_members: true) } + let(:participatory_space_edit_path) { decidim_admin_assemblies.edit_assembly_path(participatory_space) } - it_behaves_like "manage assembly members examples" + it_behaves_like "manage admin members examples" end diff --git a/decidim-assemblies/spec/system/admin/assembly_admin_accesses_admin_sections_spec.rb b/decidim-assemblies/spec/system/admin/assembly_admin_accesses_admin_sections_spec.rb index 3f9cc23a29126..5231f9c8e3ab6 100644 --- a/decidim-assemblies/spec/system/admin/assembly_admin_accesses_admin_sections_spec.rb +++ b/decidim-assemblies/spec/system/admin/assembly_admin_accesses_admin_sections_spec.rb @@ -10,7 +10,7 @@ login_as user, scope: :user end - shared_examples "sees public space menu" do + shared_examples "sees menu without members" do it "can access all sections" do expect(page).to have_content("Info") expect(page).to have_content("Components") @@ -21,7 +21,7 @@ end end - shared_examples "sees private space menu" do + shared_examples "sees menu with members" do it "can access all sections" do expect(page).to have_content("Info") expect(page).to have_content("Components") @@ -41,14 +41,14 @@ end end - context "when is a public assembly" do - it_behaves_like "sees public space menu" + context "when is an assembly without members" do + it_behaves_like "sees menu without members" end - context "when is a private assembly" do - let(:assembly) { create(:assembly, organization:, private_space: true) } + context "when is an assembly with members" do + let(:assembly) { create(:assembly, organization:, has_members: true) } - it_behaves_like "sees private space menu" + it_behaves_like "sees menu with members" end end @@ -59,14 +59,14 @@ visit decidim_admin_assemblies.edit_assembly_path(child_assembly) end - context "when is a public assembly" do - it_behaves_like "sees public space menu" + context "when is an assembly without" do + it_behaves_like "sees menu without members" end - context "when is a private assembly" do - let(:child_assembly) { create(:assembly, parent: assembly, organization:, private_space: true) } + context "when is an assembly with members" do + let(:child_assembly) { create(:assembly, parent: assembly, organization:, has_members: true) } - it_behaves_like "sees private space menu" + it_behaves_like "sees menu with members" end it_behaves_like "assembly admin manage assembly components" diff --git a/decidim-assemblies/spec/system/admin/invite_assembly_admin_spec.rb b/decidim-assemblies/spec/system/admin/invite_assembly_admin_spec.rb index 17459a1c34cd8..53c136fe1b77c 100644 --- a/decidim-assemblies/spec/system/admin/invite_assembly_admin_spec.rb +++ b/decidim-assemblies/spec/system/admin/invite_assembly_admin_spec.rb @@ -6,7 +6,7 @@ describe "Invite assembly administrator" do let(:participatory_space) { create(:assembly) } - let(:private_participatory_space) { create(:assembly, private_space: true) } + let(:members_participatory_space) { create(:assembly, has_members: true) } let(:about_this_space_label) { "About this assembly" } let(:space_admins_label) { "Assembly admins" } let(:space_sidebar_label) { "Assemblies" } diff --git a/decidim-assemblies/spec/system/members_spec.rb b/decidim-assemblies/spec/system/members_spec.rb index e896b5850ab19..b91f69bd35337 100644 --- a/decidim-assemblies/spec/system/members_spec.rb +++ b/decidim-assemblies/spec/system/members_spec.rb @@ -1,123 +1,14 @@ # frozen_string_literal: true require "spec_helper" +require "decidim/core/test/shared_examples/participatory_space_members_shared_examples" describe "Assembly members" do - let(:organization) { create(:organization) } - let(:assembly) { create(:assembly, :with_content_blocks, organization:, blocks_manifests:, private_space: true) } - let(:privatable_to) { assembly } - let(:blocks_manifests) { [] } + let(:assembly) { create(:assembly, :with_content_blocks, organization:, blocks_manifests:, has_members: true) } + let(:participatory_space) { assembly } + let(:participatory_space_homepage_path) { decidim_assemblies.assembly_path(participatory_space, locale: I18n.locale) } + let(:members_path) { decidim_assemblies.assembly_members_path(participatory_space, locale: I18n.locale) } + let(:unexisting_participatory_space_members_path) { decidim_assemblies.assembly_members_path(assembly_slug: 999_999_999, locale: I18n.locale) } - let(:user) { create(:user, organization: privatable_to.organization) } - let(:ceased_user) { create(:user, organization: privatable_to.organization) } - - before do - switch_to_host(organization.host) - end - - context "when there are no assembly members and directly accessing from URL" do - it_behaves_like "a 404 page" do - let(:target_path) { decidim_assemblies.assembly_members_path(assembly, locale: I18n.locale) } - end - end - - context "when there are no assembly members and accessing from the assembly homepage" do - context "and the main data content block is disabled" do - it "the menu nav is not shown" do - visit decidim_assemblies.assembly_path(assembly, locale: I18n.locale) - - expect(page).to have_no_css(".participatory-space__nav-container") - end - end - - context "and the main data content block is enabled" do - let(:blocks_manifests) { ["main_data"] } - - it "the menu link is not shown" do - visit decidim_assemblies.assembly_path(assembly, locale: I18n.locale) - - expect(page).to have_no_content("Members") - end - end - end - - context "when the assembly does not exist" do - it_behaves_like "a 404 page" do - let(:target_path) { decidim_assemblies.assembly_members_path(assembly_slug: 999_999_999, locale: I18n.locale) } - end - end - - context "when there are some assembly members and all are unpublished" do - before do - create(:member, user:, privatable_to:, published: false) - end - - context "and directly accessing from URL" do - it_behaves_like "a 404 page" do - let(:target_path) { decidim_assemblies.assembly_members_path(assembly, locale: I18n.locale) } - end - end - - context "and accessing from the homepage" do - context "and the main data content block is disabled" do - it "the menu nav is not shown" do - visit decidim_assemblies.assembly_path(assembly, locale: I18n.locale) - - expect(page).to have_no_css(".participatory-space__nav-container") - end - end - - context "and the main data content block is enabled" do - let(:blocks_manifests) { ["main_data"] } - - it "the menu link is not shown" do - visit decidim_assemblies.assembly_path(assembly, locale: I18n.locale) - - expect(page).to have_no_content("Members") - end - end - end - end - - context "when there are some published assembly members" do - let!(:member) { create(:member, user:, privatable_to:, published: true) } - let!(:ceased_member) { create(:member, user: ceased_user, privatable_to:, published: false) } - - before do - visit decidim_assemblies.assembly_members_path(assembly, locale: I18n.locale) - end - - context "and accessing from the assembly homepage" do - context "and the main data content block is disabled" do - it "the menu nav is not shown" do - visit decidim_assemblies.assembly_path(assembly, locale: I18n.locale) - - expect(page).to have_no_css(".participatory-space__nav-container") - end - end - - context "and the main data content block is enabled" do - let(:blocks_manifests) { ["main_data"] } - - it "the menu link is shown" do - visit decidim_assemblies.assembly_path(assembly, locale: I18n.locale) - - within ".participatory-space__nav-container" do - expect(page).to have_content("Members") - click_on "Members" - end - - expect(page).to have_current_path decidim_assemblies.assembly_members_path(assembly, locale: I18n.locale) - end - end - - it "lists all the non ceased assembly members" do - within "#assembly_members-grid" do - expect(page).to have_css(".profile__user", count: 1) - - expect(page).to have_no_content(Decidim::ParticipatorySpace::MemberPresenter.new(ceased_member).name) - end - end - end - end + it_behaves_like "participatory space members" end diff --git a/decidim-assemblies/spec/system/private_assemblies_spec.rb b/decidim-assemblies/spec/system/private_assemblies_spec.rb index 350e72dd69bd4..c715c38be55d2 100644 --- a/decidim-assemblies/spec/system/private_assemblies_spec.rb +++ b/decidim-assemblies/spec/system/private_assemblies_spec.rb @@ -9,8 +9,8 @@ let!(:user) { create(:user, :confirmed, organization:) } let!(:other_user) { create(:user, :confirmed, organization:) } let!(:other_user2) { create(:user, :confirmed, organization:) } - let!(:assembly_member) { create(:assembly_member, user: other_user, privatable_to: private_assembly) } - let!(:assembly_member2) { create(:assembly_member, user: other_user2, privatable_to: private_assembly) } + let!(:assembly_member) { create(:assembly_member, user: other_user, participatory_space: private_assembly) } + let!(:assembly_member2) { create(:assembly_member, user: other_user2, participatory_space: private_assembly) } context "when there are private assemblies" do context "and the assembly is transparent" do diff --git a/decidim-budgets/spec/types/integration_schema_spec.rb b/decidim-budgets/spec/types/integration_schema_spec.rb index 29cb3dceae115..7d952c17ce440 100644 --- a/decidim-budgets/spec/types/integration_schema_spec.rb +++ b/decidim-budgets/spec/types/integration_schema_spec.rb @@ -493,7 +493,7 @@ context "when user is member" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } - let!(:member) { create(:assembly_member, user: current_user, privatable_to: participatory_process) } + let!(:member) { create(:assembly_member, user: current_user, participatory_space: participatory_process) } it "is visible" do expect(response["assembly"]["components"].first[lookout_key]).to eq(query_result) @@ -547,7 +547,7 @@ context "when user is member" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } - let!(:member) { create(:assembly_member, user: current_user, privatable_to: participatory_process) } + let!(:member) { create(:assembly_member, user: current_user, participatory_space: participatory_process) } it "should not be visible" do expect(response["assembly"]["components"]).to be_empty diff --git a/decidim-conferences/spec/system/admin/invite_conference_admin_spec.rb b/decidim-conferences/spec/system/admin/invite_conference_admin_spec.rb index dff23ccf1fa7d..f3bbda98a23c1 100644 --- a/decidim-conferences/spec/system/admin/invite_conference_admin_spec.rb +++ b/decidim-conferences/spec/system/admin/invite_conference_admin_spec.rb @@ -15,5 +15,5 @@ include_context "when inviting participatory space users" - it_behaves_like "inviting participatory space admins", check_private_space: false, check_landing_page: false + it_behaves_like "inviting participatory space admins", check_members_page: false, check_landing_page: false end diff --git a/decidim-core/app/models/decidim/participatory_space/member.rb b/decidim-core/app/models/decidim/participatory_space/member.rb index fe86d330ff8ac..1be8ee4ed73f2 100644 --- a/decidim-core/app/models/decidim/participatory_space/member.rb +++ b/decidim-core/app/models/decidim/participatory_space/member.rb @@ -8,13 +8,13 @@ class Member < ApplicationRecord include ParticipatorySpaceUser include Decidim::TranslatableResource - belongs_to :privatable_to, polymorphic: true + belongs_to :participatory_space, polymorphic: true translatable_fields :role delegate :email, :name, to: :user - scope :by_participatory_space, ->(privatable_to) { where(privatable_to_id: privatable_to.id, privatable_to_type: privatable_to.class.to_s) } + scope :by_participatory_space, ->(participatory_space) { where(participatory_space_id: participatory_space.id, participatory_space_type: participatory_space.class.to_s) } scope :published, -> { where(published: true) } def self.user_collection(user) @@ -22,7 +22,7 @@ def self.user_collection(user) end def self.member_ids_for_participatory_spaces(spaces) - joins(:user).where(privatable_to: spaces).distinct.pluck(:decidim_user_id) + joins(:user).where(participatory_space: spaces).distinct.pluck(:decidim_user_id) end def self.export_serializer @@ -47,7 +47,7 @@ def self.ransackable_associations(_auth_object = nil) %w(user) end - def target_space_association = :privatable_to + def target_space_association = :participatory_space end end end diff --git a/decidim-core/config/locales/en.yml b/decidim-core/config/locales/en.yml index ff435fea47134..c01f83b9d3d67 100644 --- a/decidim-core/config/locales/en.yml +++ b/decidim-core/config/locales/en.yml @@ -777,7 +777,7 @@ en: participatory_space_members: created_at: The date and time when this member was created id: The unique identifier of this member - privatable_to: To which space this member belongs + participatory_space: To which space this member belongs published: Wether this member is published or not role: The role that this member has updated_at: The date and time when this member was last updated diff --git a/decidim-core/db/migrate/20251216185133_rename_privatable_to_to_participatory_space_in_members.rb b/decidim-core/db/migrate/20251216185133_rename_privatable_to_to_participatory_space_in_members.rb new file mode 100644 index 0000000000000..cdf6bc82ccb08 --- /dev/null +++ b/decidim-core/db/migrate/20251216185133_rename_privatable_to_to_participatory_space_in_members.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class RenamePrivatableToToParticipatorySpaceInMembers < ActiveRecord::Migration[7.2] + def change + rename_column :decidim_members, :privatable_to_id, :participatory_space_id + rename_column :decidim_members, :privatable_to_type, :participatory_space_type + + rename_index :decidim_members, "space_privatable_to_privatable_id", "index_decidim_members_on_participatory_space" + end +end diff --git a/decidim-core/lib/decidim/core/test/factories.rb b/decidim-core/lib/decidim/core/test/factories.rb index 20ce1007c170c..e1e30f679a768 100644 --- a/decidim-core/lib/decidim/core/test/factories.rb +++ b/decidim-core/lib/decidim/core/test/factories.rb @@ -287,7 +287,7 @@ def generate_title(field = nil, skip_injection:) skip_injection { false } end user - privatable_to { create(:participatory_process, organization: user.organization, skip_injection:) } + participatory_space { create(:participatory_process, organization: user.organization, skip_injection:) } role { generate_localized_title(:role, skip_injection:) } @@ -305,7 +305,7 @@ def generate_title(field = nil, skip_injection:) skip_injection { false } end user - privatable_to { create(:assembly, organization: user.organization, skip_injection:) } + participatory_space { create(:assembly, organization: user.organization, skip_injection:) } end factory :identity, class: "Decidim::Identity" do diff --git a/decidim-core/lib/decidim/core/test/shared_examples/has_members.rb b/decidim-core/lib/decidim/core/test/shared_examples/has_members.rb index 4e6fb0df8f8a5..1259697203423 100644 --- a/decidim-core/lib/decidim/core/test/shared_examples/has_members.rb +++ b/decidim-core/lib/decidim/core/test/shared_examples/has_members.rb @@ -14,7 +14,7 @@ end def create_space_member(space, user = create(:user, organization: space.organization)) - Decidim::ParticipatorySpace::Member.create(privatable_to: space, user:) + Decidim::ParticipatorySpace::Member.create(participatory_space: space, user:) end describe ".public_spaces" do diff --git a/decidim-core/lib/decidim/core/test/shared_examples/participatory_space_members_page_examples.rb b/decidim-core/lib/decidim/core/test/shared_examples/participatory_space_members_page_examples.rb index a7d84f10b6a19..1f9b35611b478 100644 --- a/decidim-core/lib/decidim/core/test/shared_examples/participatory_space_members_page_examples.rb +++ b/decidim-core/lib/decidim/core/test/shared_examples/participatory_space_members_page_examples.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true shared_examples "participatory space members page examples" do - let(:user1) { create(:user, organization: privatable_to.organization) } - let(:user2) { create(:user, organization: privatable_to.organization) } - let(:user3) { create(:user, organization: privatable_to.organization) } + let(:user1) { create(:user, organization: participatory_space.organization) } + let(:user2) { create(:user, organization: participatory_space.organization) } + let(:user3) { create(:user, organization: participatory_space.organization) } before do request.env["decidim.current_organization"] = organization @@ -18,9 +18,9 @@ end context "when participatory space has members" do - let!(:member1) { create(:member, privatable_to:, user: user1, published: true) } - let!(:member2) { create(:member, privatable_to:, user: user2, published: true) } - let!(:non_published) { create(:member, privatable_to:, user: user3, published: false) } + let!(:member1) { create(:member, participatory_space:, user: user1, published: true) } + let!(:member2) { create(:member, participatory_space:, user: user2, published: true) } + let!(:non_published) { create(:member, participatory_space:, user: user3, published: false) } context "when user has permissions" do it "displays list of members" do diff --git a/decidim-core/lib/decidim/core/test/shared_examples/participatory_space_members_shared_examples.rb b/decidim-core/lib/decidim/core/test/shared_examples/participatory_space_members_shared_examples.rb new file mode 100644 index 0000000000000..2f9662df95209 --- /dev/null +++ b/decidim-core/lib/decidim/core/test/shared_examples/participatory_space_members_shared_examples.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +shared_examples "participatory space members" do + let(:blocks_manifests) { [] } + let(:organization) { create(:organization) } + let(:user) { create(:user, organization: participatory_space.organization) } + let(:ceased_user) { create(:user, organization: participatory_space.organization) } + + before do + switch_to_host(organization.host) + end + + context "when there are no members and directly accessing from URL" do + it_behaves_like "a 404 page" do + let(:target_path) { members_path } + end + end + + context "when there are no members and accessing from the space homepage" do + context "and the main data content block is disabled" do + it "the menu nav is not shown" do + visit participatory_space_homepage_path + + expect(page).to have_no_css(".participatory-space__nav-container") + end + end + + context "and the main data content block is enabled" do + let(:blocks_manifests) { ["main_data"] } + + it "the menu link is not shown" do + visit participatory_space_homepage_path + + expect(page).to have_no_content("Members") + end + end + end + + context "when the participatory space does not exist" do + it_behaves_like "a 404 page" do + let(:target_path) { unexisting_participatory_space_members_path } + end + end + + context "when there are some members and all are unpublished" do + before do + create(:member, user:, participatory_space:, published: false) + end + + context "and directly accessing from URL" do + it_behaves_like "a 404 page" do + let(:target_path) { members_path } + end + end + + context "and accessing from the homepage" do + context "and the main data content block is disabled" do + it "the menu nav is not shown" do + visit participatory_space_homepage_path + + expect(page).to have_no_css(".participatory-space__nav-container") + end + end + + context "and the main data content block is enabled" do + let(:blocks_manifests) { ["main_data"] } + + it "the menu link is not shown" do + visit participatory_space_homepage_path + + expect(page).to have_no_content("Members") + end + end + end + end + + context "when there are some published members" do + let!(:member) { create(:member, user:, participatory_space:, published: true) } + let!(:ceased_member) { create(:member, user: ceased_user, participatory_space:, published: false) } + + before do + visit members_path + end + + context "and accessing from the space homepage" do + context "and the main data content block is disabled" do + it "the menu nav is not shown" do + visit participatory_space_homepage_path + + expect(page).to have_no_css(".participatory-space__nav-container") + end + end + + context "and the main data content block is enabled" do + let(:blocks_manifests) { ["main_data"] } + + it "the menu link is shown" do + visit participatory_space_homepage_path + + within ".participatory-space__nav-container" do + expect(page).to have_content("Members") + click_on "Members" + end + + expect(page).to have_current_path members_path + end + end + + it "lists all the non ceased members" do + within "#assembly_members-grid" do + expect(page).to have_css(".profile__user", count: 1) + + expect(page).to have_no_content(Decidim::ParticipatorySpace::MemberPresenter.new(ceased_member).name) + end + end + end + end +end diff --git a/decidim-core/lib/decidim/download_your_data_serializers/download_your_data_member_serializer.rb b/decidim-core/lib/decidim/download_your_data_serializers/download_your_data_member_serializer.rb index 9b87e467ca69a..669cf1c7c1dc5 100644 --- a/decidim-core/lib/decidim/download_your_data_serializers/download_your_data_member_serializer.rb +++ b/decidim-core/lib/decidim/download_your_data_serializers/download_your_data_member_serializer.rb @@ -10,11 +10,11 @@ class DownloadYourDataMemberSerializer < Decidim::Exporters::Serializer def serialize { id: resource.id, - privatable_to: { - id: resource.privatable_to_id, - type: resource.privatable_to_type, - title: resource.privatable_to.title, - slug: resource.privatable_to.slug + participatory_space: { + id: resource.participatory_space_id, + type: resource.participatory_space_type, + title: resource.participatory_space.title, + slug: resource.participatory_space.slug }, created_at: resource.created_at, updated_at: resource.updated_at, diff --git a/decidim-core/lib/decidim/participatory_space/has_members.rb b/decidim-core/lib/decidim/participatory_space/has_members.rb index 3b721a100d6a3..03e87511cde67 100644 --- a/decidim-core/lib/decidim/participatory_space/has_members.rb +++ b/decidim-core/lib/decidim/participatory_space/has_members.rb @@ -12,7 +12,7 @@ module HasMembers included do has_many :members, class_name: "Decidim::ParticipatorySpace::Member", - as: :privatable_to, + as: :participatory_space, dependent: :destroy has_many :users, through: :members, @@ -35,7 +35,7 @@ def self.visible_for(user) end def members_public_page? - private_space && members.published.any? + has_members && members.published.any? end def can_participate?(user) diff --git a/decidim-core/spec/helpers/decidim/menu_helper_spec.rb b/decidim-core/spec/helpers/decidim/menu_helper_spec.rb index 3e3089a185201..06885ca66d827 100644 --- a/decidim-core/spec/helpers/decidim/menu_helper_spec.rb +++ b/decidim-core/spec/helpers/decidim/menu_helper_spec.rb @@ -71,7 +71,7 @@ module Decidim end context "and current_user is member of that process" do - let!(:member) { create(:member, privatable_to: process_two, user:) } + let!(:member) { create(:member, participatory_space: process_two, user:) } it "returns the private process" do expect(helper.menu_highlighted_participatory_process).to eq(process_two) diff --git a/decidim-core/spec/lib/api/functions/participatory_space_list_base_spec.rb b/decidim-core/spec/lib/api/functions/participatory_space_list_base_spec.rb index db026a4a177f4..57358fa6171c4 100644 --- a/decidim-core/spec/lib/api/functions/participatory_space_list_base_spec.rb +++ b/decidim-core/spec/lib/api/functions/participatory_space_list_base_spec.rb @@ -30,7 +30,7 @@ module Decidim::Core context "with a private space participant" do let(:user) { create(:user, :confirmed, organization:) } - let!(:member) { create(:member, privatable_to: private_process, user:) } + let!(:member) { create(:member, participatory_space: private_process, user:) } it "returns all spaces including the private space" do expect(subject).to include(process1, process2, process3, private_process) diff --git a/decidim-core/spec/lib/download_your_data_serializers/download_your_data_member_serializer_spec.rb b/decidim-core/spec/lib/download_your_data_serializers/download_your_data_member_serializer_spec.rb index a0686717cf021..011bdc42c82a1 100644 --- a/decidim-core/spec/lib/download_your_data_serializers/download_your_data_member_serializer_spec.rb +++ b/decidim-core/spec/lib/download_your_data_serializers/download_your_data_member_serializer_spec.rb @@ -14,18 +14,18 @@ module Decidim expect(serialized).to include(id: resource.id) end - it "includes the privatable to" do - expect(serialized[:privatable_to]).to( - include(id: resource.privatable_to_id) + it "includes the participatory space" do + expect(serialized[:participatory_space]).to( + include(id: resource.participatory_space_id) ) - expect(serialized[:privatable_to]).to( - include(type: resource.privatable_to_type) + expect(serialized[:participatory_space]).to( + include(type: resource.participatory_space_type) ) - expect(serialized[:privatable_to]).to( - include(title: resource.privatable_to.title) + expect(serialized[:participatory_space]).to( + include(title: resource.participatory_space.title) ) - expect(serialized[:privatable_to]).to( - include(slug: resource.privatable_to.slug) + expect(serialized[:participatory_space]).to( + include(slug: resource.participatory_space.slug) ) end diff --git a/decidim-core/spec/mailers/notification_digest_mailer_spec.rb b/decidim-core/spec/mailers/notification_digest_mailer_spec.rb index e0016c94efd06..80d2f86c1c45b 100644 --- a/decidim-core/spec/mailers/notification_digest_mailer_spec.rb +++ b/decidim-core/spec/mailers/notification_digest_mailer_spec.rb @@ -81,7 +81,7 @@ module Decidim context "when the space is private and user has access" do let!(:participatory_space) { create(:participatory_process, :private, organization:) } let(:component) { create(:component, :published, manifest_name: "dummy", participatory_space:) } - let!(:member) { create(:member, privatable_to: participatory_space, user:) } + let!(:member) { create(:member, participatory_space:, user:) } it "displays the notification" do expect(subject.body).to include(test_content) @@ -92,7 +92,7 @@ module Decidim context "when the space is transparent and user has access" do let!(:participatory_space) { create(:assembly, :transparent, :private, organization:) } let(:component) { create(:component, :published, manifest_name: "dummy", participatory_space:) } - let!(:member) { create(:member, privatable_to: participatory_space, user:) } + let!(:member) { create(:member, participatory_space:, user:) } it "displays the notification" do expect(subject.body).to include(test_content) diff --git a/decidim-core/spec/models/decidim/participatory_space/member_spec.rb b/decidim-core/spec/models/decidim/participatory_space/member_spec.rb index ec9d084f3bafd..aac19b3afddda 100644 --- a/decidim-core/spec/models/decidim/participatory_space/member_spec.rb +++ b/decidim-core/spec/models/decidim/participatory_space/member_spec.rb @@ -27,7 +27,7 @@ module Decidim::ParticipatorySpace build( :member, user:, - privatable_to: participatory_process + participatory_space: participatory_process ) end diff --git a/decidim-core/spec/queries/decidim/public_activities_spec.rb b/decidim-core/spec/queries/decidim/public_activities_spec.rb index aa91e645fbb4d..83135326bb365 100644 --- a/decidim-core/spec/queries/decidim/public_activities_spec.rb +++ b/decidim-core/spec/queries/decidim/public_activities_spec.rb @@ -18,12 +18,12 @@ # Note that it is possible to add members also to public processes # and assemblies, there is no programming logic forbidding that to happen. [process, assembly, private_process, private_assembly].each do |space| - 10.times { create(:member, user: build(:user, :confirmed, organization:), privatable_to: space) } + 10.times { create(:member, user: build(:user, :confirmed, organization:), participatory_space: space) } end # Add the user to both private spaces - create(:member, user:, privatable_to: private_process) - create(:member, user:, privatable_to: private_assembly) + create(:member, user:, participatory_space: private_process) + create(:member, user:, participatory_space: private_assembly) end describe "#query" do @@ -41,9 +41,9 @@ expect(subject.count).to eq(1) end - context "when the current user has access to the private space" do + context "when the current user is a member of the private space" do before do - create(:member, user: current_user, privatable_to: private_process) + create(:member, user: current_user, participatory_space: private_process) end it "returns also the private comment without duplicates" do diff --git a/decidim-core/spec/tasks/upgrade/fix_deleted_private_follows_spec.rb b/decidim-core/spec/tasks/upgrade/fix_deleted_private_follows_spec.rb index 98a84e8b34067..2e73d45ae4804 100644 --- a/decidim-core/spec/tasks/upgrade/fix_deleted_private_follows_spec.rb +++ b/decidim-core/spec/tasks/upgrade/fix_deleted_private_follows_spec.rb @@ -13,7 +13,7 @@ let!(:unwanted_follow) { create(:follow, user: second_user, followable: participatory_space) } let!(:resource_follow) { create(:follow, followable:, user:) } let!(:resource_unwanted_follow) { create(:follow, followable:, user: second_user) } - let!(:member) { create(:member, user:, privatable_to: participatory_space) } + let!(:member) { create(:member, user:, participatory_space:) } let(:participatory_space) { create(:participatory_process, :published, organization: user.organization) } around do |example| diff --git a/decidim-debates/spec/system/private_space_debate_spec.rb b/decidim-debates/spec/system/private_space_debate_spec.rb index c009bc94f07eb..f451829b576c3 100644 --- a/decidim-debates/spec/system/private_space_debate_spec.rb +++ b/decidim-debates/spec/system/private_space_debate_spec.rb @@ -10,7 +10,7 @@ let(:user) { create(:user, :confirmed, organization:) } let!(:other_user) { create(:user, :confirmed, organization:) } - let!(:member) { create(:member, user: other_user, privatable_to: participatory_space_private) } + let!(:member) { create(:member, user: other_user, participatory_space: participatory_space_private) } let!(:participatory_space) { participatory_space_private } diff --git a/decidim-meetings/app/models/decidim/meetings/meeting.rb b/decidim-meetings/app/models/decidim/meetings/meeting.rb index 9e3de89ae4f88..863d5e536a627 100644 --- a/decidim-meetings/app/models/decidim/meetings/meeting.rb +++ b/decidim-meetings/app/models/decidim/meetings/meeting.rb @@ -122,7 +122,7 @@ class Meeting < Meetings::ApplicationRecord SELECT decidim_components.id FROM decidim_components WHERE CONCAT(decidim_components.participatory_space_id, '-', decidim_components.participatory_space_type) IN - (SELECT CONCAT(decidim_members.privatable_to_id, '-', decidim_members.privatable_to_type) + (SELECT CONCAT(decidim_members.participatory_space_id, '-', decidim_members.participatory_space_type) FROM decidim_members WHERE decidim_members.decidim_user_id = ?) ) " diff --git a/decidim-meetings/spec/controllers/decidim/meetings/meetings_controller_spec.rb b/decidim-meetings/spec/controllers/decidim/meetings/meetings_controller_spec.rb index 9c4afdd024e3e..b2064468fd4a0 100644 --- a/decidim-meetings/spec/controllers/decidim/meetings/meetings_controller_spec.rb +++ b/decidim-meetings/spec/controllers/decidim/meetings/meetings_controller_spec.rb @@ -137,7 +137,7 @@ context "when user is member" do let!(:user) { create(:user, :confirmed, organization:) } - let!(:member) { create(:member, user:, privatable_to: participatory_process) } + let!(:member) { create(:member, user:, participatory_space: participatory_process) } it_behaves_like "having meeting access visibility applied" end diff --git a/decidim-meetings/spec/permissions/decidim/meetings/permissions_spec.rb b/decidim-meetings/spec/permissions/decidim/meetings/permissions_spec.rb index d71cb9c3bea45..32198275ef711 100644 --- a/decidim-meetings/spec/permissions/decidim/meetings/permissions_spec.rb +++ b/decidim-meetings/spec/permissions/decidim/meetings/permissions_spec.rb @@ -232,7 +232,7 @@ let(:user) { admin_user } before do - create(:member, user:, privatable_to: participatory_space) + create(:member, user:, participatory_space:) end it { is_expected.to be true } @@ -248,7 +248,7 @@ context "when user is a space member" do before do - create(:member, user:, privatable_to: participatory_space) + create(:member, user:, participatory_space:) end it { is_expected.to be true } diff --git a/decidim-meetings/spec/system/live_meeting_access_spec.rb b/decidim-meetings/spec/system/live_meeting_access_spec.rb index 852d8a2297848..8fa00301ec931 100644 --- a/decidim-meetings/spec/system/live_meeting_access_spec.rb +++ b/decidim-meetings/spec/system/live_meeting_access_spec.rb @@ -166,7 +166,7 @@ def visit_meeting let(:participatory_space) { assembly } let(:admin) { create(:user, :confirmed, :admin, organization:) } let(:member) { create(:user, :confirmed, organization:) } - let!(:assembly_member) { create(:assembly_member, user: member, privatable_to: assembly) } + let!(:assembly_member) { create(:assembly_member, user: member, participatory_space: assembly) } context "when user is not signed in" do it "does not show the meeting link embedded" do diff --git a/decidim-participatory_processes/app/commands/decidim/participatory_processes/admin/create_participatory_process.rb b/decidim-participatory_processes/app/commands/decidim/participatory_processes/admin/create_participatory_process.rb index 12f2813fe188d..014ef4e175bb7 100644 --- a/decidim-participatory_processes/app/commands/decidim/participatory_processes/admin/create_participatory_process.rb +++ b/decidim-participatory_processes/app/commands/decidim/participatory_processes/admin/create_participatory_process.rb @@ -10,7 +10,7 @@ class CreateParticipatoryProcess < Decidim::Commands::CreateResource fetch_form_attributes :organization, :title, :subtitle, :weight, :slug, :description, :short_description, :promoted, :taxonomizations, :announcement, - :private_space, :developer_group, :local_area, :target, + :has_members, :private_space, :developer_group, :local_area, :target, :participatory_scope, :participatory_structure, :meta_scope, :start_date, :end_date, :participatory_process_group diff --git a/decidim-participatory_processes/app/commands/decidim/participatory_processes/admin/update_participatory_process.rb b/decidim-participatory_processes/app/commands/decidim/participatory_processes/admin/update_participatory_process.rb index 137c27d9f6c29..ad438b628f650 100644 --- a/decidim-participatory_processes/app/commands/decidim/participatory_processes/admin/update_participatory_process.rb +++ b/decidim-participatory_processes/app/commands/decidim/participatory_processes/admin/update_participatory_process.rb @@ -9,7 +9,7 @@ class UpdateParticipatoryProcess < Decidim::Commands::UpdateResource fetch_file_attributes :hero_image fetch_form_attributes :title, :subtitle, :weight, :slug, :promoted, - :taxonomizations, :private_space, :developer_group, :local_area, + :taxonomizations, :has_members, :private_space, :developer_group, :local_area, :target, :participatory_scope, :participatory_structure, :meta_scope, :start_date, :end_date, :participatory_process_group, :announcement diff --git a/decidim-participatory_processes/app/controllers/decidim/participatory_processes/admin/members_controller.rb b/decidim-participatory_processes/app/controllers/decidim/participatory_processes/admin/members_controller.rb index 44586076c7ae8..fd9687fc12690 100644 --- a/decidim-participatory_processes/app/controllers/decidim/participatory_processes/admin/members_controller.rb +++ b/decidim-participatory_processes/app/controllers/decidim/participatory_processes/admin/members_controller.rb @@ -7,14 +7,6 @@ module Admin class MembersController < Decidim::Admin::ApplicationController include Concerns::ParticipatoryProcessAdmin include Decidim::Admin::ParticipatorySpace::Concerns::HasMembers - - def after_destroy_path - members_path(current_participatory_process) - end - - def privatable_to - current_participatory_process - end end end end diff --git a/decidim-participatory_processes/app/controllers/decidim/participatory_processes/admin/members_csv_imports_controller.rb b/decidim-participatory_processes/app/controllers/decidim/participatory_processes/admin/members_csv_imports_controller.rb index 61f2ff60b0427..8959019ac0a27 100644 --- a/decidim-participatory_processes/app/controllers/decidim/participatory_processes/admin/members_csv_imports_controller.rb +++ b/decidim-participatory_processes/app/controllers/decidim/participatory_processes/admin/members_csv_imports_controller.rb @@ -13,7 +13,7 @@ def after_import_path members_path(current_participatory_process) end - def privatable_to + def participatory_space current_participatory_process end end diff --git a/decidim-participatory_processes/app/forms/decidim/participatory_processes/admin/participatory_process_form.rb b/decidim-participatory_processes/app/forms/decidim/participatory_processes/admin/participatory_process_form.rb index 47a2ad106aaae..ebd0c3df3625b 100644 --- a/decidim-participatory_processes/app/forms/decidim/participatory_processes/admin/participatory_process_form.rb +++ b/decidim-participatory_processes/app/forms/decidim/participatory_processes/admin/participatory_process_form.rb @@ -31,6 +31,7 @@ class ParticipatoryProcessForm < Form attribute :related_process_ids, Array[Integer] attribute :weight, Integer, default: 0 + attribute :has_members, Boolean attribute :private_space, Boolean attribute :promoted, Boolean diff --git a/decidim-participatory_processes/app/helpers/decidim/participatory_processes/participatory_process_helper.rb b/decidim-participatory_processes/app/helpers/decidim/participatory_processes/participatory_process_helper.rb index 6ace222284914..7843686b88886 100644 --- a/decidim-participatory_processes/app/helpers/decidim/participatory_processes/participatory_process_helper.rb +++ b/decidim-participatory_processes/app/helpers/decidim/participatory_processes/participatory_process_helper.rb @@ -57,8 +57,8 @@ def process_nav_items(participatory_space) *(if participatory_space.members_public_page? [{ name: t("member_menu_item", scope: "layouts.decidim.participatory_process_navigation"), - url: decidim_participatory_processes.participatory_process_members_path(participatory_space), - active: is_active_link?(decidim_participatory_processes.participatory_process_members_path(participatory_space), :inclusive) + url: decidim_participatory_processes.participatory_process_members_path(participatory_space, locale: current_locale), + active: is_active_link?(decidim_participatory_processes.participatory_process_members_path(participatory_space, locale: current_locale), :inclusive) }] end ) diff --git a/decidim-participatory_processes/app/permissions/decidim/participatory_processes/permissions.rb b/decidim-participatory_processes/app/permissions/decidim/participatory_processes/permissions.rb index 579e2259da0c2..858235a7dc3af 100644 --- a/decidim-participatory_processes/app/permissions/decidim/participatory_processes/permissions.rb +++ b/decidim-participatory_processes/app/permissions/decidim/participatory_processes/permissions.rb @@ -56,7 +56,7 @@ def permissions def user_can_read_members? return unless permission_action.subject == :space_member - return unless process.private_space? + return unless process.has_members? toggle_allow(user.admin? || can_manage_process?(role: :admin) || can_manage_process?(role: :collaborator)) end diff --git a/decidim-participatory_processes/app/views/decidim/participatory_processes/admin/participatory_processes/_form.html.erb b/decidim-participatory_processes/app/views/decidim/participatory_processes/admin/participatory_processes/_form.html.erb index 9842e56191210..730ea78f2c2bf 100644 --- a/decidim-participatory_processes/app/views/decidim/participatory_processes/admin/participatory_processes/_form.html.erb +++ b/decidim-participatory_processes/app/views/decidim/participatory_processes/admin/participatory_processes/_form.html.erb @@ -167,6 +167,10 @@ <% end %>
+
+ <%= form.check_box :has_members, help_text: t(".has_members_help") %> +
+
<%= form.check_box :private_space %>

<%= t(".private_notice") %>

diff --git a/decidim-participatory_processes/config/locales/en.yml b/decidim-participatory_processes/config/locales/en.yml index f5da230fea98a..e0ddd55b283bd 100644 --- a/decidim-participatory_processes/config/locales/en.yml +++ b/decidim-participatory_processes/config/locales/en.yml @@ -15,6 +15,7 @@ en: duplicate_landing_page_blocks: Duplicate landing page blocks duplicate_steps: Duplicate steps end_date: End date + has_members: This space has members hero_image: Home image import_attachments: Import attachments import_categories: Import categories @@ -509,10 +510,11 @@ en: announcement_help: The text you enter here will be shown to the user right below the process information. define_taxonomy_filters: Please define some filters for this participatory space before using this setting. duration: Duration + has_members_help: You will be able to create and publish members images: Images metadata: Metadata no_taxonomy_filters_found: No taxonomy filters found. - private_notice: You will be able to manage members after setting it as private + private_notice: Makes the space not visible for visitors and only for members (except if it is also transparent) related_processes: Related processes select_process_group: Select a process group slug_help_html: 'URL slugs are used to generate the URLs that point to this process. Only accepts letters, numbers and dashes, and must start with a letter. Example: %{url}' diff --git a/decidim-participatory_processes/db/migrate/20251205120001_add_has_members_to_decidim_participatory_processes.rb b/decidim-participatory_processes/db/migrate/20251205120001_add_has_members_to_decidim_participatory_processes.rb new file mode 100644 index 0000000000000..79b210e453082 --- /dev/null +++ b/decidim-participatory_processes/db/migrate/20251205120001_add_has_members_to_decidim_participatory_processes.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddHasMembersToDecidimParticipatoryProcesses < ActiveRecord::Migration[7.0] + def change + add_column :decidim_participatory_processes, :has_members, :boolean, default: false + end +end diff --git a/decidim-participatory_processes/lib/decidim/participatory_processes/test/factories.rb b/decidim-participatory_processes/lib/decidim/participatory_processes/test/factories.rb index 0477c34497aa4..7cbf4bcd8db0e 100644 --- a/decidim-participatory_processes/lib/decidim/participatory_processes/test/factories.rb +++ b/decidim-participatory_processes/lib/decidim/participatory_processes/test/factories.rb @@ -31,6 +31,7 @@ participatory_scope { generate_localized_title(:participatory_process_participatory_scope, skip_injection:) } participatory_structure { generate_localized_title(:participatory_process_participatory_structure, skip_injection:) } announcement { generate_localized_title(:participatory_process_announcement, skip_injection:) } + has_members { false } private_space { false } start_date { Date.current } end_date { 2.months.from_now } diff --git a/decidim-participatory_processes/spec/commands/create_participatory_process_spec.rb b/decidim-participatory_processes/spec/commands/create_participatory_process_spec.rb index a33c6fb1a33a4..09672ab135b8c 100644 --- a/decidim-participatory_processes/spec/commands/create_participatory_process_spec.rb +++ b/decidim-participatory_processes/spec/commands/create_participatory_process_spec.rb @@ -40,6 +40,7 @@ module Decidim::ParticipatoryProcesses current_user:, current_organization: organization, organization:, + has_members: false, private_space: false, taxonomizations:, errors:, diff --git a/decidim-participatory_processes/spec/controllers/admin/members_csv_imports_controller_spec.rb b/decidim-participatory_processes/spec/controllers/admin/members_csv_imports_controller_spec.rb index 1bcf60e9e1586..32957fbc482a7 100644 --- a/decidim-participatory_processes/spec/controllers/admin/members_csv_imports_controller_spec.rb +++ b/decidim-participatory_processes/spec/controllers/admin/members_csv_imports_controller_spec.rb @@ -11,25 +11,25 @@ module Admin let!(:organization) { create(:organization) } let!(:admin) { create(:user, :admin, :confirmed, organization:) } let!(:user) { create(:user, organization:) } - let!(:privatable_to) { create(:participatory_process, organization: user.organization, private_space: true) } - let!(:member) { create(:member, user:, privatable_to:) } + let!(:participatory_space) { create(:participatory_process, organization: user.organization, has_members: true) } + let!(:member) { create(:member, user:, participatory_space:) } before do request.env["decidim.current_organization"] = organization - request.env["decidim.current_participatory_process"] = member.privatable_to + request.env["decidim.current_participatory_process"] = member.participatory_space sign_in admin, scope: :user end it "is routed to" do - delete :destroy_all, params: { participatory_process_slug: member.privatable_to.slug } + delete :destroy_all, params: { participatory_process_slug: member.participatory_space.slug } expect(response).to be_redirect end it "suppress the existing users" do expect do - delete :destroy_all, params: { participatory_process_slug: member.privatable_to.slug, locale: I18n.locale } - end.to change { Decidim::ParticipatorySpace::Member.by_participatory_space(member.privatable_to).count }.by(-1) + delete :destroy_all, params: { participatory_process_slug: member.participatory_space.slug, locale: I18n.locale } + end.to change { Decidim::ParticipatorySpace::Member.by_participatory_space(member.participatory_space).count }.by(-1) end end end diff --git a/decidim-participatory_processes/spec/controllers/members_controller_spec.rb b/decidim-participatory_processes/spec/controllers/members_controller_spec.rb index d82f3a24cf830..bff5a78e2d469 100644 --- a/decidim-participatory_processes/spec/controllers/members_controller_spec.rb +++ b/decidim-participatory_processes/spec/controllers/members_controller_spec.rb @@ -9,11 +9,11 @@ module ParticipatoryProcesses routes { Decidim::ParticipatoryProcesses::Engine.routes } let(:organization) { create(:organization) } - let(:destination_path) { decidim_participatory_processes.participatory_process_path(privatable_to, locale: I18n.locale) } + let(:destination_path) { decidim_participatory_processes.participatory_process_path(participatory_space, locale: I18n.locale) } let(:slug_param) { "participatory_process_slug" } - let(:slug) { privatable_to.slug } + let(:slug) { participatory_space.slug } - let!(:privatable_to) do + let!(:participatory_space) do create( :participatory_process, :published, diff --git a/decidim-participatory_processes/spec/forms/participatory_process_form_spec.rb b/decidim-participatory_processes/spec/forms/participatory_process_form_spec.rb index 04fae369103d8..1ddc2ec3d98f1 100644 --- a/decidim-participatory_processes/spec/forms/participatory_process_form_spec.rb +++ b/decidim-participatory_processes/spec/forms/participatory_process_form_spec.rb @@ -48,6 +48,8 @@ module Admin let(:end_date) { 1.month.from_now } let(:slug) { "slug" } let(:attachment) { upload_test_file(Decidim::Dev.test_file("city.jpeg", "image/jpeg")) } + let(:private_space) { true } + let(:has_members) { true } let(:attributes) do { "participatory_process" => { @@ -68,11 +70,25 @@ module Admin "end_date" => end_date, "hero_image" => attachment, "slug" => slug, + "private_space" => private_space, + "has_members" => has_members, "taxonomies" => [taxonomies.first.id, taxonomies.second.id] } } end + context "when has_members is true" do + let(:has_members) { true } + + it { is_expected.to be_valid } + end + + context "when has_members is false" do + let(:has_members) { false } + + it { is_expected.to be_valid } + end + context "when everything is OK" do it { is_expected.to be_valid } end diff --git a/decidim-participatory_processes/spec/permissions/decidim/participatory_processes/permissions_spec.rb b/decidim-participatory_processes/spec/permissions/decidim/participatory_processes/permissions_spec.rb index 24bc1f66e2efd..ee1e263af58fd 100644 --- a/decidim-participatory_processes/spec/permissions/decidim/participatory_processes/permissions_spec.rb +++ b/decidim-participatory_processes/spec/permissions/decidim/participatory_processes/permissions_spec.rb @@ -422,8 +422,8 @@ it_behaves_like "allows any action on subject", :process_step it_behaves_like "allows any action on subject", :process_user_role - context "when private process" do - let(:process) { create(:participatory_process, organization:, private_space: true) } + context "when process has members" do + let(:process) { create(:participatory_process, organization:, has_members: true) } let!(:context) { { current_participatory_space: process } } it_behaves_like "allows any action on subject", :space_member @@ -447,8 +447,8 @@ it_behaves_like "allows any action on subject", :process_step it_behaves_like "allows any action on subject", :process_user_role - context "when private process" do - let(:process) { create(:participatory_process, organization:, private_space: true) } + context "when process has members" do + let(:process) { create(:participatory_process, organization:, has_members: true) } let!(:context) { { current_participatory_space: process } } it_behaves_like "allows any action on subject", :space_member diff --git a/decidim-participatory_processes/spec/system/admin/admin_filters_participatory_processes_private_space_users_spec.rb b/decidim-participatory_processes/spec/system/admin/admin_filters_participatory_processes_members_spec.rb similarity index 82% rename from decidim-participatory_processes/spec/system/admin/admin_filters_participatory_processes_private_space_users_spec.rb rename to decidim-participatory_processes/spec/system/admin/admin_filters_participatory_processes_members_spec.rb index a5276918cc6ed..b48d8102d36c9 100644 --- a/decidim-participatory_processes/spec/system/admin/admin_filters_participatory_processes_private_space_users_spec.rb +++ b/decidim-participatory_processes/spec/system/admin/admin_filters_participatory_processes_members_spec.rb @@ -2,24 +2,24 @@ require "spec_helper" -describe "Admin filters participatory processes private space users" do +describe "Admin filters participatory processes members" do include_context "with filterable context" let(:organization) { create(:organization) } let!(:user) { create(:user, :admin, :confirmed, organization:) } let!(:invited_user1) { create(:user, name:, organization:) } - let!(:invited_member1) { create(:member, user: invited_user1, privatable_to: participatory_process) } + let!(:invited_member1) { create(:member, user: invited_user1, participatory_space: participatory_process) } let!(:invited_user2) { create(:user, email:, organization:) } - let!(:invited_member2) { create(:member, user: invited_user2, privatable_to: participatory_process) } + let!(:invited_member2) { create(:member, user: invited_user2, participatory_space: participatory_process) } let(:name) { "Dummy Name" } let(:email) { "dummy_email@example.org" } let(:resource_controller) { Decidim::ParticipatoryProcesses::Admin::MembersController } - context "when managing private process" do - let(:participatory_process) { create(:participatory_process, organization:, private_space: true) } + context "when managing process with members" do + let(:participatory_process) { create(:participatory_process, organization:, has_members: true) } before do invited_user1.update!(invitation_sent_at: 1.day.ago, invitation_accepted_at: Time.current) @@ -36,8 +36,8 @@ include_examples "searchable participatory space users" end - context "when managing members in a public process" do - let(:participatory_process) { create(:participatory_process, organization:, private_space: false) } + context "when trying to manage members and the space does not have members" do + let(:participatory_process) { create(:participatory_process, organization:, has_members: false) } before do invited_user1.update!(invitation_sent_at: 1.day.ago, invitation_accepted_at: Time.current) diff --git a/decidim-participatory_processes/spec/system/admin/admin_manages_participatory_process_members_spec.rb b/decidim-participatory_processes/spec/system/admin/admin_manages_participatory_process_members_spec.rb index 22de4997972a6..80212a44298a9 100644 --- a/decidim-participatory_processes/spec/system/admin/admin_manages_participatory_process_members_spec.rb +++ b/decidim-participatory_processes/spec/system/admin/admin_manages_participatory_process_members_spec.rb @@ -1,11 +1,13 @@ # frozen_string_literal: true require "spec_helper" +require "decidim/admin/test/admin_members_shared_examples" describe "Admin manages participatory process members" do let!(:user) { create(:user, :admin, :confirmed, organization:) } let(:organization) { create(:organization) } - let!(:participatory_process) { create(:participatory_process, organization:, private_space: true) } + let!(:participatory_space) { create(:participatory_process, organization:, has_members: true) } + let(:participatory_space_edit_path) { decidim_admin_participatory_processes.edit_participatory_process_path(participatory_space) } - it_behaves_like "manage participatory process members examples" + it_behaves_like "manage admin members examples" end diff --git a/decidim-participatory_processes/spec/system/admin/admin_manages_participatory_processes_spec.rb b/decidim-participatory_processes/spec/system/admin/admin_manages_participatory_processes_spec.rb index be1b467132cc5..2c18b0314bd89 100644 --- a/decidim-participatory_processes/spec/system/admin/admin_manages_participatory_processes_spec.rb +++ b/decidim-participatory_processes/spec/system/admin/admin_manages_participatory_processes_spec.rb @@ -18,8 +18,8 @@ visit decidim_admin_participatory_processes.participatory_processes_path end - context "when conditionally displaying member menu entry" do - let!(:my_space) { create(:participatory_process, organization:, private_space:) } + context "when conditionally displaying members menu entry" do + let!(:my_space) { create(:participatory_process, organization:, has_members:) } before do switch_to_host(organization.host) @@ -28,20 +28,20 @@ click_on translated(my_space.title) end - context "when the participatory process is private" do - let(:private_space) { true } + context "when the participatory process has members" do + let(:has_members) { true } - it "hides the member menu entry" do + it "shows the member menu entry" do within_admin_sidebar_menu do expect(page).to have_content("Members") end end end - context "when the participatory process is public" do - let(:private_space) { false } + context "when the participatory process has no members" do + let(:has_members) { false } - it "shows the member menu entry" do + it "hides the member menu entry" do within_admin_sidebar_menu do expect(page).to have_no_content("Members") end diff --git a/decidim-participatory_processes/spec/system/admin/invite_process_admin_spec.rb b/decidim-participatory_processes/spec/system/admin/invite_process_admin_spec.rb index 3746617f58f27..565d9fd7f6a4a 100644 --- a/decidim-participatory_processes/spec/system/admin/invite_process_admin_spec.rb +++ b/decidim-participatory_processes/spec/system/admin/invite_process_admin_spec.rb @@ -6,7 +6,7 @@ describe "Invite process administrator" do let(:participatory_space) { create(:participatory_process) } - let(:private_participatory_space) { create(:participatory_process, private_space: true) } + let(:members_participatory_space) { create(:participatory_process, has_members: true) } let(:about_this_space_label) { "About this process" } let(:space_admins_label) { "Process admins" } let(:space_sidebar_label) { "Processes" } diff --git a/decidim-participatory_processes/spec/system/members_spec.rb b/decidim-participatory_processes/spec/system/members_spec.rb new file mode 100644 index 0000000000000..27d3a83ba654b --- /dev/null +++ b/decidim-participatory_processes/spec/system/members_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require "spec_helper" +require "decidim/core/test/shared_examples/participatory_space_members_shared_examples" + +describe "Participatory Process members" do + let(:participatory_process) { create(:participatory_process, :with_content_blocks, organization:, blocks_manifests:, has_members: true) } + let(:participatory_space) { participatory_process } + let(:participatory_space_homepage_path) { decidim_participatory_processes.participatory_process_path(participatory_space, locale: I18n.locale) } + let(:members_path) { decidim_participatory_processes.participatory_process_members_path(participatory_space, locale: I18n.locale) } + let(:unexisting_participatory_space_members_path) { decidim_participatory_processes.participatory_process_members_path(participatory_process_slug: 999_999_999, locale: I18n.locale) } + + it_behaves_like "participatory space members" +end diff --git a/decidim-participatory_processes/spec/system/private_participatory_processes_spec.rb b/decidim-participatory_processes/spec/system/private_participatory_processes_spec.rb index 2b50e3de8992d..6765265c7844f 100644 --- a/decidim-participatory_processes/spec/system/private_participatory_processes_spec.rb +++ b/decidim-participatory_processes/spec/system/private_participatory_processes_spec.rb @@ -10,8 +10,8 @@ let!(:user) { create(:user, :confirmed, organization:) } let!(:other_user) { create(:user, :confirmed, organization:) } let!(:other_user2) { create(:user, :confirmed, organization:) } - let!(:member) { create(:member, user: other_user, privatable_to: private_participatory_process) } - let!(:member2) { create(:member, user: other_user2, privatable_to: private_participatory_process) } + let!(:member) { create(:member, user: other_user, participatory_space: private_participatory_process) } + let!(:member2) { create(:member, user: other_user2, participatory_space: private_participatory_process) } context "when there are private participatory processes" do context "and no user is logged in" do diff --git a/decidim-participatory_processes/spec/types/integration_schema_spec.rb b/decidim-participatory_processes/spec/types/integration_schema_spec.rb index 13148113b3fef..19a894b8cb90e 100644 --- a/decidim-participatory_processes/spec/types/integration_schema_spec.rb +++ b/decidim-participatory_processes/spec/types/integration_schema_spec.rb @@ -317,7 +317,7 @@ end context "when the current user is a member" do - let!(:member) { create(:member, privatable_to: private_process, user: current_user) } + let!(:member) { create(:member, participatory_space: private_process, user: current_user) } it "returns all spaces" do expect(response["participatoryProcesses"]).to include( diff --git a/decidim-proposals/spec/system/private_space_proposal_spec.rb b/decidim-proposals/spec/system/private_space_proposal_spec.rb index 72afb0904fc2c..b6ba864615c65 100644 --- a/decidim-proposals/spec/system/private_space_proposal_spec.rb +++ b/decidim-proposals/spec/system/private_space_proposal_spec.rb @@ -7,7 +7,7 @@ let(:user) { create(:user, :confirmed, organization:) } let!(:other_user) { create(:user, :confirmed, organization:) } - let!(:member) { create(:member, user: other_user, privatable_to: participatory_space_private) } + let!(:member) { create(:member, user: other_user, participatory_space: participatory_space_private) } let!(:participatory_space) { participatory_space_private } diff --git a/decidim-surveys/spec/system/private_space_survey_spec.rb b/decidim-surveys/spec/system/private_space_survey_spec.rb index 28ca4ec0dcd0e..e5ca82a3f8b7f 100644 --- a/decidim-surveys/spec/system/private_space_survey_spec.rb +++ b/decidim-surveys/spec/system/private_space_survey_spec.rb @@ -25,7 +25,7 @@ let(:user) { create(:user, :confirmed, organization:) } let!(:another_user) { create(:user, :confirmed, organization:) } - let!(:member) { create(:member, user: another_user, privatable_to: participatory_space_private) } + let!(:member) { create(:member, user: another_user, participatory_space: participatory_space_private) } let!(:questionnaire) { create(:questionnaire, title:, description:) } let!(:survey) { create(:survey, :published, :allow_responses, component:, questionnaire:) } From 134fadbc0b307959ee2590a34f3a38bf2691015b Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Fri, 19 Dec 2025 15:11:56 +0200 Subject: [PATCH 038/116] Write API: withdraw proposals (#15793) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor API to raise and catch Decidim::PermissionAction::PermissionNotSetError exceptions * Add Decidim::Api::Errors::MutationNotAuthorizedError * Add Decidim::Api::Errors::ValidationError * Add Decidim::Api::Errors::AttributeValidationError * Add Decidim::Api::Errors::AttributeValidationError * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix spec * Add test example * Refactor file name * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply review recommendation * Fix spec * Add mutation to withdraw Proposals via API * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * change the variable name * Add additional I18n related exceptions * Refactor error layer * Fix error class tokens * Fix the initiatives specs * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Running linters * Refactor API documentation * Fix typos * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review * Apply suggestions from code review * Update the request example * Fix issues with permissions * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove unecesary comment * Update decidim-proposals/spec/types/withdraw_proposal_type_spec.rb Co-authored-by: Andrés Pereira de Lucena * Apply review recommendations * Update docs/modules/develop/pages/api/reference/components/proposals/withdraw.adoc Co-authored-by: Andrés Pereira de Lucena * Fix spec --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Andrés Pereira de Lucena --- .../decidim/proposals/permissions.rb | 2 +- .../api/mutations/proposal_answer_type.rb | 1 + .../api/mutations/proposal_mutation_type.rb | 1 + .../api/mutations/withdraw_proposal_type.rb | 31 +++++ .../lib/decidim/proposals/api.rb | 1 + .../decidim/proposals/permissions_spec.rb | 6 + .../spec/types/withdraw_proposal_type_spec.rb | 127 ++++++++++++++++++ .../components/proposals/withdraw.adoc | 37 ++++- 8 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 decidim-proposals/lib/decidim/api/mutations/withdraw_proposal_type.rb create mode 100644 decidim-proposals/spec/types/withdraw_proposal_type_spec.rb diff --git a/decidim-proposals/app/permissions/decidim/proposals/permissions.rb b/decidim-proposals/app/permissions/decidim/proposals/permissions.rb index 9d4a274bbb750..91b13ef2ec6b5 100644 --- a/decidim-proposals/app/permissions/decidim/proposals/permissions.rb +++ b/decidim-proposals/app/permissions/decidim/proposals/permissions.rb @@ -79,7 +79,7 @@ def can_edit_proposal? end def can_withdraw_proposal? - toggle_allow(proposal && proposal.authored_by?(user)) + toggle_allow(proposal && !proposal.withdrawn? && proposal.authored_by?(user)) end def can_create_amendment? diff --git a/decidim-proposals/lib/decidim/api/mutations/proposal_answer_type.rb b/decidim-proposals/lib/decidim/api/mutations/proposal_answer_type.rb index f5fbbdc621443..4e124a66fa231 100644 --- a/decidim-proposals/lib/decidim/api/mutations/proposal_answer_type.rb +++ b/decidim-proposals/lib/decidim/api/mutations/proposal_answer_type.rb @@ -33,6 +33,7 @@ def resolve(attributes:) on(:ok) do return object end + on(:invalid) do raise GraphQL::ExecutionError, form.errors.full_messages.join(", ") end diff --git a/decidim-proposals/lib/decidim/api/mutations/proposal_mutation_type.rb b/decidim-proposals/lib/decidim/api/mutations/proposal_mutation_type.rb index 52ae9ffd51cba..ee8497ae3e86b 100644 --- a/decidim-proposals/lib/decidim/api/mutations/proposal_mutation_type.rb +++ b/decidim-proposals/lib/decidim/api/mutations/proposal_mutation_type.rb @@ -11,6 +11,7 @@ class ProposalMutationType < Decidim::Api::Types::BaseObject field :answer, mutation: Decidim::Proposals::ProposalAnswerType, description: "Answers a proposal" field :unvote, mutation: Decidim::Proposals::UnvoteProposalType, description: "Removes a vote from a proposal" field :vote, mutation: Decidim::Proposals::VoteProposalType, description: "Votes a proposal" + field :withdraw, mutation: Decidim::Proposals::WithdrawProposalType, description: "Withdraws a proposal" end end end diff --git a/decidim-proposals/lib/decidim/api/mutations/withdraw_proposal_type.rb b/decidim-proposals/lib/decidim/api/mutations/withdraw_proposal_type.rb new file mode 100644 index 0000000000000..5348094f283fd --- /dev/null +++ b/decidim-proposals/lib/decidim/api/mutations/withdraw_proposal_type.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Decidim + module Proposals + class WithdrawProposalType < Decidim::Api::Types::BaseMutation + graphql_name "WithdrawProposal" + + description "Withdraws a proposal" + type Decidim::Proposals::ProposalType + + def resolve + WithdrawProposal.call(object, current_user) do + on(:ok) do |proposal| + return proposal + end + + on(:has_votes) do + raise Decidim::Api::Errors::ValidationError, I18n.t("proposals.withdraw.errors.has_votes", scope: "decidim") + end + end + end + + def authorized? + raise Decidim::Api::Errors::MutationNotAuthorizedError, I18n.t("decidim.api.errors.unauthorized_mutation") unless super && allowed_to?(:withdraw, :proposal, object, + context) + + true + end + end + end +end diff --git a/decidim-proposals/lib/decidim/proposals/api.rb b/decidim-proposals/lib/decidim/proposals/api.rb index 7e4dc814300e4..757f4b8a3799c 100644 --- a/decidim-proposals/lib/decidim/proposals/api.rb +++ b/decidim-proposals/lib/decidim/proposals/api.rb @@ -13,5 +13,6 @@ module Proposals autoload :AnswerProposalAttributes, "decidim/api/mutations/answer_proposal_attributes" autoload :VoteProposalType, "decidim/api/mutations/vote_proposal_type" autoload :UnvoteProposalType, "decidim/api/mutations/unvote_proposal_type" + autoload :WithdrawProposalType, "decidim/api/mutations/withdraw_proposal_type" end end diff --git a/decidim-proposals/spec/permissions/decidim/proposals/permissions_spec.rb b/decidim-proposals/spec/permissions/decidim/proposals/permissions_spec.rb index 6149b42923f24..550a0f2b44213 100644 --- a/decidim-proposals/spec/permissions/decidim/proposals/permissions_spec.rb +++ b/decidim-proposals/spec/permissions/decidim/proposals/permissions_spec.rb @@ -110,6 +110,12 @@ it { is_expected.to be false } end + + context "when proposal is already withdrawn" do + let(:proposal) { create(:proposal, :withdrawn, component: proposal_component) } + + it { is_expected.to be false } + end end describe "voting" do diff --git a/decidim-proposals/spec/types/withdraw_proposal_type_spec.rb b/decidim-proposals/spec/types/withdraw_proposal_type_spec.rb new file mode 100644 index 0000000000000..6d11bf3dbf4e5 --- /dev/null +++ b/decidim-proposals/spec/types/withdraw_proposal_type_spec.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +require "spec_helper" +require "decidim/api/test/mutation_context" + +module Decidim + module Proposals + describe WithdrawProposalType, type: :graphql do + include_context "with a graphql class mutation" + + let(:root_klass) { ProposalMutationType } + let(:current_organization) { create(:organization, available_locales: [:en]) } + let(:participatory_process) { create(:participatory_process, :with_steps, organization: current_organization) } + let(:proposal_component) { create(:proposal_component, participatory_space: participatory_process) } + let(:author) { create(:user, :confirmed, organization: current_organization) } + let!(:model) { create(:proposal, component: proposal_component, users: [author]) } + let(:component) { model.component } + let(:query) do + <<~GRAPHQL + mutation() { + withdraw(input: {}) { + id + state + withdrawnAt + } + } + GRAPHQL + end + + let(:variables) do + { + input: { + attributes: {} + } + } + end + + describe "withdrawing a proposal" do + context "with proposal author" do + let(:current_user) { author } + let(:user_type) { :user } + + it "withdraws the proposal" do + proposal = response["withdraw"] + expect(proposal).to be_present + expect(proposal["id"]).to eq(model.id.to_s) + expect(model.reload).to be_withdrawn + expect(model.withdrawn_at).to be_present + end + end + + context "with admin user" do + let!(:user_type) { :admin } + + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError exception" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + + context "with api_user that is not the author" do + let!(:user_type) { :api_user } + + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError exception" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + + context "with normal user that is not the author" do + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError exception" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + + context "with api_user that is the author" do + let!(:model) { create(:proposal, component: proposal_component, users: [current_user]) } + let!(:user_type) { :api_user } + + it "withdraws the proposal" do + proposal = response["withdraw"] + expect(proposal).to be_present + expect(proposal["id"]).to eq(model.id.to_s) + expect(model.reload).to be_withdrawn + expect(model.withdrawn_at).to be_present + end + end + end + + context "when proposal has votes" do + let(:current_user) { author } + + before do + model.votes.create!(author: create(:user, :confirmed, organization: current_organization)) + end + + it "does not withdraw the proposal and returns an error" do + expect { response }.to raise_error(Decidim::Api::Errors::ValidationError, "This proposal cannot be withdrawn because it already has votes.") + expect(model.reload).not_to be_withdrawn + expect(model.withdrawn_at).not_to be_present + end + end + + context "when proposal is already withdrawn" do + let!(:model) { create(:proposal, :withdrawn, component: proposal_component, users: [author]) } + let(:current_user) { author } + + it "remains withdrawn and returns an error" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + expect(model.reload).to be_withdrawn + expect(model.withdrawn_at).to be_present + end + end + + context "when proposal is already answered" do + let!(:model) { create(:proposal, :with_answer, component: proposal_component, users: [author]) } + let(:current_user) { author } + + it "can be withdrawn by author" do + proposal = response["withdraw"] + expect(proposal).to be_present + expect(proposal["id"]).to eq(model.id.to_s) + expect(model.reload).to be_withdrawn + expect(model.withdrawn_at).to be_present + end + end + end + end +end diff --git a/docs/modules/develop/pages/api/reference/components/proposals/withdraw.adoc b/docs/modules/develop/pages/api/reference/components/proposals/withdraw.adoc index 8b7aab440e85b..45ba2400d4fee 100644 --- a/docs/modules/develop/pages/api/reference/components/proposals/withdraw.adoc +++ b/docs/modules/develop/pages/api/reference/components/proposals/withdraw.adoc @@ -1,3 +1,38 @@ = Withdraw Proposal -include::admin:partial$under-construction.adoc[] +To use the API to withdraw any proposal that the user has created, you will need to use the following API request. + +[source,graphql] +---- +mutation withdrawProposal($componentId: ID!, $proposalId: ID!){ + component(id: $componentId) { + ...on ProposalsMutation{ + proposal(id: $proposalId) { + withdraw(input: {}) { + id + state + withdrawnAt + } + } + } + } +} +---- + +Example of submitted variables + +[source,json] +---- +{ + "componentId": "9", + "proposalId": "2" +} +---- + +== Error Handling + +The most frequent errors that can be generated by this mutation are as follows: + +* xref:develop:api/reference/errors/unauthorized_mutation.adoc[Unauthorized Mutation Error - MUTATION_NOT_AUTHORIZED_ERROR] +* xref:develop:api/reference/errors/validation_error.adoc[Validation Error - VALIDATION_ERROR] +* xref:develop:api/reference/errors/unauthorized_object.adoc[Unauthorized Object Error - UNAUTHORIZED_OBJECT_ERROR] From 19d5028f71373d95761460277d6e636ffddc2862 Mon Sep 17 00:00:00 2001 From: decidim-bot Date: Sat, 20 Dec 2025 03:10:40 +0100 Subject: [PATCH 039/116] New Crowdin updates (#15782) Co-authored-by: Alexandru Emil Lupu --- decidim-admin/config/locales/ar.yml | 48 +----- decidim-admin/config/locales/bg.yml | 59 +------ decidim-admin/config/locales/bs-BA.yml | 2 - decidim-admin/config/locales/ca-IT.yml | 140 +++++++++-------- decidim-admin/config/locales/ca.yml | 140 +++++++++-------- decidim-admin/config/locales/cs.yml | 75 +-------- decidim-admin/config/locales/de.yml | 75 +-------- decidim-admin/config/locales/el.yml | 47 +----- decidim-admin/config/locales/eo.yml | 2 - decidim-admin/config/locales/es-MX.yml | 140 +++++++++-------- decidim-admin/config/locales/es-PY.yml | 140 +++++++++-------- decidim-admin/config/locales/es.yml | 140 +++++++++-------- decidim-admin/config/locales/eu.yml | 140 +++++++++-------- decidim-admin/config/locales/fi-plain.yml | 145 +++++++++--------- decidim-admin/config/locales/fi.yml | 145 +++++++++--------- decidim-admin/config/locales/fr-CA.yml | 142 ++++++++--------- decidim-admin/config/locales/fr.yml | 142 ++++++++--------- decidim-admin/config/locales/ga-IE.yml | 13 -- decidim-admin/config/locales/gl.yml | 37 +---- decidim-admin/config/locales/hu.yml | 58 +------ decidim-admin/config/locales/id-ID.yml | 22 +-- decidim-admin/config/locales/is-IS.yml | 3 - decidim-admin/config/locales/it.yml | 75 +-------- decidim-admin/config/locales/ja.yml | 143 ++++++++--------- decidim-admin/config/locales/kaa.yml | 8 - decidim-admin/config/locales/ko.yml | 29 ---- decidim-admin/config/locales/lb.yml | 37 +---- decidim-admin/config/locales/lt.yml | 58 +------ decidim-admin/config/locales/lv.yml | 23 +-- decidim-admin/config/locales/nl.yml | 42 +---- decidim-admin/config/locales/no.yml | 42 +---- decidim-admin/config/locales/pl.yml | 59 +------ decidim-admin/config/locales/pt-BR.yml | 59 +------ decidim-admin/config/locales/pt.yml | 37 +---- decidim-admin/config/locales/ro-RO.yml | 58 +------ decidim-admin/config/locales/ru.yml | 21 +-- decidim-admin/config/locales/sk.yml | 23 +-- decidim-admin/config/locales/sq-AL.yml | 14 -- decidim-admin/config/locales/sr-CS.yml | 2 - decidim-admin/config/locales/sv.yml | 100 ++++-------- decidim-admin/config/locales/th-TH.yml | 3 - decidim-admin/config/locales/tr-TR.yml | 61 +------- decidim-admin/config/locales/uk.yml | 13 -- decidim-admin/config/locales/zh-CN.yml | 23 +-- decidim-admin/config/locales/zh-TW.yml | 58 +------ decidim-api/config/locales/am-ET.yml | 1 + decidim-api/config/locales/ar.yml | 1 + decidim-api/config/locales/bg.yml | 1 + decidim-api/config/locales/bn-BD.yml | 1 + decidim-api/config/locales/bs-BA.yml | 1 + decidim-api/config/locales/ca-IT.yml | 12 ++ decidim-api/config/locales/ca.yml | 12 ++ decidim-api/config/locales/cs.yml | 1 + decidim-api/config/locales/da.yml | 1 + decidim-api/config/locales/de.yml | 1 + decidim-api/config/locales/el.yml | 1 + decidim-api/config/locales/eo.yml | 1 + decidim-api/config/locales/es-MX.yml | 12 ++ decidim-api/config/locales/es-PY.yml | 12 ++ decidim-api/config/locales/es.yml | 12 ++ decidim-api/config/locales/et.yml | 1 + decidim-api/config/locales/eu.yml | 12 ++ decidim-api/config/locales/fa-IR.yml | 1 + decidim-api/config/locales/fi-plain.yml | 12 ++ decidim-api/config/locales/fi.yml | 12 ++ decidim-api/config/locales/fr-CA.yml | 9 ++ decidim-api/config/locales/fr.yml | 9 ++ decidim-api/config/locales/ga-IE.yml | 1 + decidim-api/config/locales/gl.yml | 1 + decidim-api/config/locales/gn-PY.yml | 1 + decidim-api/config/locales/he-IL.yml | 1 + decidim-api/config/locales/hr.yml | 1 + decidim-api/config/locales/hu.yml | 1 + decidim-api/config/locales/id-ID.yml | 1 + decidim-api/config/locales/is-IS.yml | 1 + decidim-api/config/locales/it.yml | 1 + decidim-api/config/locales/ja.yml | 9 ++ decidim-api/config/locales/ka-GE.yml | 1 + decidim-api/config/locales/kaa.yml | 1 + decidim-api/config/locales/ko.yml | 1 + decidim-api/config/locales/lb.yml | 1 + decidim-api/config/locales/lo-LA.yml | 1 + decidim-api/config/locales/lt.yml | 1 + decidim-api/config/locales/lv.yml | 1 + decidim-api/config/locales/mt.yml | 1 + decidim-api/config/locales/nl.yml | 1 + decidim-api/config/locales/no.yml | 6 + decidim-api/config/locales/oc-FR.yml | 1 + decidim-api/config/locales/om-ET.yml | 1 + decidim-api/config/locales/pl.yml | 1 + decidim-api/config/locales/pt-BR.yml | 1 + decidim-api/config/locales/pt.yml | 1 + decidim-api/config/locales/ro-RO.yml | 1 + decidim-api/config/locales/ru.yml | 1 + decidim-api/config/locales/si-LK.yml | 1 + decidim-api/config/locales/sk.yml | 1 + decidim-api/config/locales/sl.yml | 1 + decidim-api/config/locales/so-SO.yml | 1 + decidim-api/config/locales/sq-AL.yml | 1 + decidim-api/config/locales/sr-CS.yml | 1 + decidim-api/config/locales/sv.yml | 12 ++ decidim-api/config/locales/sw-KE.yml | 1 + decidim-api/config/locales/th-TH.yml | 1 + decidim-api/config/locales/ti-ER.yml | 1 + decidim-api/config/locales/tr-TR.yml | 1 + decidim-api/config/locales/uk.yml | 1 + decidim-api/config/locales/val-ES.yml | 1 + decidim-api/config/locales/vi.yml | 1 + decidim-api/config/locales/zh-CN.yml | 1 + decidim-api/config/locales/zh-TW.yml | 1 + decidim-assemblies/config/locales/ca-IT.yml | 5 +- decidim-assemblies/config/locales/ca.yml | 5 +- decidim-assemblies/config/locales/cs.yml | 2 - decidim-assemblies/config/locales/de.yml | 2 - decidim-assemblies/config/locales/es-MX.yml | 5 +- decidim-assemblies/config/locales/es-PY.yml | 5 +- decidim-assemblies/config/locales/es.yml | 5 +- decidim-assemblies/config/locales/eu.yml | 5 +- .../config/locales/fi-plain.yml | 8 +- decidim-assemblies/config/locales/fi.yml | 8 +- decidim-assemblies/config/locales/fr-CA.yml | 8 +- decidim-assemblies/config/locales/fr.yml | 8 +- decidim-assemblies/config/locales/he-IL.yml | 1 - decidim-assemblies/config/locales/it.yml | 2 - decidim-assemblies/config/locales/ja.yml | 5 +- decidim-assemblies/config/locales/pt.yml | 2 - decidim-assemblies/config/locales/sv.yml | 4 +- .../config/locales/eu.yml | 2 +- decidim-core/config/locales/ar.yml | 6 - decidim-core/config/locales/bg.yml | 8 - decidim-core/config/locales/ca-IT.yml | 27 ++-- decidim-core/config/locales/ca.yml | 27 ++-- decidim-core/config/locales/cs.yml | 15 -- decidim-core/config/locales/de.yml | 15 -- decidim-core/config/locales/el.yml | 5 - decidim-core/config/locales/es-MX.yml | 29 ++-- decidim-core/config/locales/es-PY.yml | 29 ++-- decidim-core/config/locales/es.yml | 27 ++-- decidim-core/config/locales/eu.yml | 41 +++-- decidim-core/config/locales/fi-plain.yml | 28 ++-- decidim-core/config/locales/fi.yml | 28 ++-- decidim-core/config/locales/fr-CA.yml | 30 ++-- decidim-core/config/locales/fr.yml | 30 ++-- decidim-core/config/locales/gl.yml | 5 - decidim-core/config/locales/hu.yml | 8 - decidim-core/config/locales/id-ID.yml | 5 - decidim-core/config/locales/it.yml | 6 - decidim-core/config/locales/ja.yml | 17 +- decidim-core/config/locales/lb.yml | 6 - decidim-core/config/locales/lt.yml | 8 - decidim-core/config/locales/lv.yml | 5 - decidim-core/config/locales/nl.yml | 6 - decidim-core/config/locales/no.yml | 6 - decidim-core/config/locales/pl.yml | 8 - decidim-core/config/locales/pt-BR.yml | 9 +- decidim-core/config/locales/pt.yml | 6 - decidim-core/config/locales/ro-RO.yml | 13 -- decidim-core/config/locales/ru.yml | 5 - decidim-core/config/locales/sk.yml | 5 - decidim-core/config/locales/sv.yml | 15 -- decidim-core/config/locales/tr-TR.yml | 6 - decidim-core/config/locales/uk.yml | 2 - decidim-core/config/locales/zh-CN.yml | 5 - decidim-core/config/locales/zh-TW.yml | 8 - decidim-dev/config/locales/eu.yml | 2 +- decidim-elections/config/locales/ca-IT.yml | 7 + decidim-elections/config/locales/ca.yml | 7 + decidim-elections/config/locales/es-MX.yml | 7 + decidim-elections/config/locales/es-PY.yml | 7 + decidim-elections/config/locales/es.yml | 7 + decidim-elections/config/locales/eu.yml | 9 +- decidim-elections/config/locales/fi-plain.yml | 7 + decidim-elections/config/locales/fi.yml | 7 + decidim-elections/config/locales/fr-CA.yml | 9 +- decidim-elections/config/locales/fr.yml | 9 +- decidim-elections/config/locales/ja.yml | 6 + decidim-elections/config/locales/sv.yml | 10 ++ decidim-forms/config/locales/ar.yml | 3 - decidim-forms/config/locales/bg.yml | 3 - decidim-forms/config/locales/ca-IT.yml | 4 +- decidim-forms/config/locales/ca.yml | 4 +- decidim-forms/config/locales/cs.yml | 3 - decidim-forms/config/locales/de.yml | 3 - decidim-forms/config/locales/el.yml | 3 - decidim-forms/config/locales/es-MX.yml | 6 +- decidim-forms/config/locales/es-PY.yml | 6 +- decidim-forms/config/locales/es.yml | 4 +- decidim-forms/config/locales/eu.yml | 6 +- decidim-forms/config/locales/fi-plain.yml | 6 +- decidim-forms/config/locales/fi.yml | 4 +- decidim-forms/config/locales/fr-CA.yml | 6 +- decidim-forms/config/locales/fr.yml | 6 +- decidim-forms/config/locales/gl.yml | 3 - decidim-forms/config/locales/hu.yml | 3 - decidim-forms/config/locales/id-ID.yml | 3 - decidim-forms/config/locales/it.yml | 3 - decidim-forms/config/locales/ja.yml | 3 - decidim-forms/config/locales/lb.yml | 3 - decidim-forms/config/locales/lt.yml | 3 - decidim-forms/config/locales/lv.yml | 3 - decidim-forms/config/locales/nl.yml | 3 - decidim-forms/config/locales/no.yml | 3 - decidim-forms/config/locales/pl.yml | 3 - decidim-forms/config/locales/pt-BR.yml | 3 - decidim-forms/config/locales/pt.yml | 3 - decidim-forms/config/locales/ro-RO.yml | 3 - decidim-forms/config/locales/ru.yml | 3 - decidim-forms/config/locales/sk.yml | 3 - decidim-forms/config/locales/sv.yml | 3 - decidim-forms/config/locales/tr-TR.yml | 3 - decidim-forms/config/locales/zh-CN.yml | 3 - decidim-forms/config/locales/zh-TW.yml | 3 - decidim-initiatives/config/locales/eu.yml | 16 +- decidim-meetings/config/locales/eu.yml | 14 +- .../config/locales/ca-IT.yml | 11 +- .../config/locales/ca.yml | 11 +- .../config/locales/cs.yml | 5 - .../config/locales/de.yml | 5 - .../config/locales/es-MX.yml | 11 +- .../config/locales/es-PY.yml | 11 +- .../config/locales/es.yml | 11 +- .../config/locales/eu.yml | 13 +- .../config/locales/fi-plain.yml | 14 +- .../config/locales/fi.yml | 14 +- .../config/locales/fr-CA.yml | 14 +- .../config/locales/fr.yml | 14 +- .../config/locales/ja.yml | 11 +- .../config/locales/sv.yml | 7 +- decidim-proposals/config/locales/eu.yml | 6 +- decidim-surveys/config/locales/ca-IT.yml | 1 + decidim-surveys/config/locales/ca.yml | 1 + decidim-surveys/config/locales/es-MX.yml | 1 + decidim-surveys/config/locales/es-PY.yml | 1 + decidim-surveys/config/locales/es.yml | 1 + decidim-surveys/config/locales/eu.yml | 1 + decidim-surveys/config/locales/fi-plain.yml | 1 + decidim-surveys/config/locales/fi.yml | 1 + decidim-surveys/config/locales/fr-CA.yml | 1 + decidim-surveys/config/locales/fr.yml | 1 + decidim-surveys/config/locales/ja.yml | 1 + decidim-surveys/config/locales/sv.yml | 2 + decidim-system/config/locales/ca-IT.yml | 2 + decidim-system/config/locales/ca.yml | 2 + decidim-system/config/locales/es-MX.yml | 2 + decidim-system/config/locales/es-PY.yml | 2 + decidim-system/config/locales/es.yml | 2 + decidim-system/config/locales/eu.yml | 4 +- decidim-system/config/locales/fi-plain.yml | 2 + decidim-system/config/locales/fi.yml | 2 + decidim-system/config/locales/fr-CA.yml | 2 + decidim-system/config/locales/fr.yml | 2 + decidim-system/config/locales/ja.yml | 2 + decidim-system/config/locales/sv.yml | 2 + decidim-verifications/config/locales/eu.yml | 2 +- 254 files changed, 1542 insertions(+), 2525 deletions(-) create mode 100644 decidim-api/config/locales/am-ET.yml create mode 100644 decidim-api/config/locales/ar.yml create mode 100644 decidim-api/config/locales/bg.yml create mode 100644 decidim-api/config/locales/bn-BD.yml create mode 100644 decidim-api/config/locales/bs-BA.yml create mode 100644 decidim-api/config/locales/ca-IT.yml create mode 100644 decidim-api/config/locales/ca.yml create mode 100644 decidim-api/config/locales/cs.yml create mode 100644 decidim-api/config/locales/da.yml create mode 100644 decidim-api/config/locales/de.yml create mode 100644 decidim-api/config/locales/el.yml create mode 100644 decidim-api/config/locales/eo.yml create mode 100644 decidim-api/config/locales/es-MX.yml create mode 100644 decidim-api/config/locales/es-PY.yml create mode 100644 decidim-api/config/locales/es.yml create mode 100644 decidim-api/config/locales/et.yml create mode 100644 decidim-api/config/locales/eu.yml create mode 100644 decidim-api/config/locales/fa-IR.yml create mode 100644 decidim-api/config/locales/fi-plain.yml create mode 100644 decidim-api/config/locales/fi.yml create mode 100644 decidim-api/config/locales/fr-CA.yml create mode 100644 decidim-api/config/locales/fr.yml create mode 100644 decidim-api/config/locales/ga-IE.yml create mode 100644 decidim-api/config/locales/gl.yml create mode 100644 decidim-api/config/locales/gn-PY.yml create mode 100644 decidim-api/config/locales/he-IL.yml create mode 100644 decidim-api/config/locales/hr.yml create mode 100644 decidim-api/config/locales/hu.yml create mode 100644 decidim-api/config/locales/id-ID.yml create mode 100644 decidim-api/config/locales/is-IS.yml create mode 100644 decidim-api/config/locales/it.yml create mode 100644 decidim-api/config/locales/ja.yml create mode 100644 decidim-api/config/locales/ka-GE.yml create mode 100644 decidim-api/config/locales/kaa.yml create mode 100644 decidim-api/config/locales/ko.yml create mode 100644 decidim-api/config/locales/lb.yml create mode 100644 decidim-api/config/locales/lo-LA.yml create mode 100644 decidim-api/config/locales/lt.yml create mode 100644 decidim-api/config/locales/lv.yml create mode 100644 decidim-api/config/locales/mt.yml create mode 100644 decidim-api/config/locales/nl.yml create mode 100644 decidim-api/config/locales/no.yml create mode 100644 decidim-api/config/locales/oc-FR.yml create mode 100644 decidim-api/config/locales/om-ET.yml create mode 100644 decidim-api/config/locales/pl.yml create mode 100644 decidim-api/config/locales/pt-BR.yml create mode 100644 decidim-api/config/locales/pt.yml create mode 100644 decidim-api/config/locales/ro-RO.yml create mode 100644 decidim-api/config/locales/ru.yml create mode 100644 decidim-api/config/locales/si-LK.yml create mode 100644 decidim-api/config/locales/sk.yml create mode 100644 decidim-api/config/locales/sl.yml create mode 100644 decidim-api/config/locales/so-SO.yml create mode 100644 decidim-api/config/locales/sq-AL.yml create mode 100644 decidim-api/config/locales/sr-CS.yml create mode 100644 decidim-api/config/locales/sv.yml create mode 100644 decidim-api/config/locales/sw-KE.yml create mode 100644 decidim-api/config/locales/th-TH.yml create mode 100644 decidim-api/config/locales/ti-ER.yml create mode 100644 decidim-api/config/locales/tr-TR.yml create mode 100644 decidim-api/config/locales/uk.yml create mode 100644 decidim-api/config/locales/val-ES.yml create mode 100644 decidim-api/config/locales/vi.yml create mode 100644 decidim-api/config/locales/zh-CN.yml create mode 100644 decidim-api/config/locales/zh-TW.yml diff --git a/decidim-admin/config/locales/ar.yml b/decidim-admin/config/locales/ar.yml index 78e1e1258300e..0a35f31dd02b2 100644 --- a/decidim-admin/config/locales/ar.yml +++ b/decidim-admin/config/locales/ar.yml @@ -89,11 +89,6 @@ ar: welcome_notification_body: محتوى إشعار الترحيب welcome_notification_subject: موضوع إشعار الترحيب youtube_handler: مُعرّف حساب يوتيوب - participatory_space_private_user: - email: البريد الإلكتروني - name: الإسم - participatory_space_private_user_csv_import: - file: ملف scope: code: الشفرة name: الاسم @@ -135,10 +130,6 @@ ar: attributes: official_img_footer: allowed_file_content_types: ملف صورة غير صالح - participatory_space_private_user_csv_import: - attributes: - file: - malformed: خطأ في ملفّ الاستيراد، يرجى قراءة التعليمات بعناية والتأكد من أن ترميز الملف هو UTF-8. user_group_csv_verification: attributes: file: @@ -342,17 +333,6 @@ ar: values: 'false': Officialized 'true': غير رسمي - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: تم قبول الدعوة - values: - 'false': لم يتم القبول - 'true': تم قبوله - user_invitation_sent_at_not_null: - label: تم إرسال الدعوة - values: - 'false': لم يتم الارسال - 'true': تم الإرسال private_space_eq: label: خاص values: @@ -444,6 +424,10 @@ ar: explanation: يمكن ترقية المشاركين المدارين إلى مشاركين عاديين. يعني ذلك أنه سيتم دعوتهم إلى التطبيق ولن تتمكن من إدارته مرة أخرى. سيتلقى المشارك المدعو رسالة بريد إلكتروني لقبول دعوتك. new_managed_user_promotion: ترويج مشارك جديد مُدار promote: تروج \ يشجع \ يعزز \ ينمى \ يطور + members_csv_imports: + new: + csv_upload: + title: قم بتحميل ملف CSV الخاص بك menu: admin_log: سجل نشاط المسؤول admins: المدراء @@ -498,8 +482,6 @@ ar: sent_to: أُرسِلت إلى subject: موضوع name: النشرة الإخبارية - participatory_space_private_user: - name: المشاركة الفضاء المشارك الخاص scope: fields: name: اسم @@ -657,28 +639,6 @@ ar: update: error: حدثت مشكلة أثناء تحديث هذه المؤسسة. success: تم تحديث المنظمة بنجاح. - participatory_space_private_users: - create: - error: حدثت مشكلة أثناء إضافة مشارك خاص لهذه المساحة التشاركية. - success: المشاركة الفضاء الفضاء المشترك وصول خلق بنجاح. - destroy: - error: حدثت مشكلة في حذف مشارك خاص لهذه المساحة التشاركية. - success: المشاركة الفضاء الفضاء وصول المشاركين دمرت بنجاح. - index: - title: المشاركة الفضاء المشارك الخاص - new: - create: إنشاء - title: مشارك جديد الفضاء الخاص المشارك. - participatory_space_private_users_csv_imports: - new: - csv_upload: - title: قم بتحميل ملف CSV الخاص بك - destroy: - button: حذف جميع المشاركين الخاصين - empty: ليس لديك أي مشاركين خاصين. - explanation: لديك %{count} مشاركين خاصين. - title: حذف جميع المشاركين الخاصين - upload: حمّل reminders: new: submit: إرسال diff --git a/decidim-admin/config/locales/bg.yml b/decidim-admin/config/locales/bg.yml index 7a99b1d115ed6..f7fe5d100f773 100644 --- a/decidim-admin/config/locales/bg.yml +++ b/decidim-admin/config/locales/bg.yml @@ -89,9 +89,6 @@ bg: welcome_notification_body: Тяло на приветствието welcome_notification_subject: Тема на приветствието youtube_handler: Манипулатор на YouTube - participatory_space_private_user: - email: Имейл - name: Име scope: code: Код name: Име @@ -133,10 +130,6 @@ bg: attributes: official_img_footer: allowed_file_content_types: Невалиден файл на изображението - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Неправилен файл за импортиране, моля, прочетете внимателно инструкциите и се уверете, че файлът е UTF-8 кодиран. user_group_csv_verification: attributes: file: @@ -174,8 +167,6 @@ bg: import: Импортиране newsletter: new: Нов бюлетин - participatory_space_private_user: - new: Нов частен потребител на пространство за участие per_page: За страница send_me_a_test_email: Изпратете ми тестов e-mail share: Сподели @@ -394,17 +385,6 @@ bg: values: 'false': Официализирано 'true': Неофициализирано - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Поканата е приета - values: - 'false': Не се приема - 'true': Прието - user_invitation_sent_at_not_null: - label: Поканата е изпратена - values: - 'false': Не е изпратено - 'true': Изпратено private_space_eq: label: Частни values: @@ -526,6 +506,10 @@ bg: explanation: Управляваните участници могат да бъдат повишавани до стандартни участници. Това означава, че ще бъдат поканени в приложението и повече няма да можете да ги управлявате. Поканеният участник ще получи имейл, за да приеме поканата Ви. new_managed_user_promotion: Ново повишение на управляван участник promote: Повишаване + members_csv_imports: + new: + csv_upload: + title: Качете своя CSV файл menu: admin_log: Регистър на дейността на администраторите admins: Администратори @@ -588,8 +572,6 @@ bg: sent_to: Изпратено до subject: Относно name: Бюлетин - participatory_space_private_user: - name: Пространство за участие на частен участник scope: fields: name: Име @@ -784,39 +766,6 @@ bg: form: add: Добави в листа с позволени title: Лист с позволени външни домейни - participatory_space_private_users: - create: - error: Възникна проблем при добавянето на частен участник за това пространство за участие. - success: Достъпът на частния участник до пространството за участие беше създаден успешно. - destroy: - error: Възникна проблем при изтриването на частен участник за това пространство за участие. - success: Достъпът на частния участник до пространството за участие беше премахнат успешно. - index: - import_via_csv: Импортиране чрез CSV - title: Пространство за участие на частен участник - new: - create: Създаване - title: Ново пространство за участие на частен участник. - participatory_space_private_users_csv_imports: - create: - invalid: Възникна проблем при четенето на CSV файла. Моля, уверете се, че сте следвали инструкциите. - success: Файлът във формат CSV беше качен успешно; изпращаме имейл с покана на участниците. Това може да отнеме известно време. - new: - csv_upload: - title: Качете своя файл във формат CSV - destroy: - button: Изтрийте всички частни участници - confirm: Сигурни ли сте, че искате да изтриете всички частни участници? Това действие е необратимо, няма да можете да ги възстановите. - empty: Нямате частни участници. - explanation: Имате %{count} частни участници. - title: Изтрийте частни участници - example_file: 'Примерен файл:' - explanation: Качете своя файл във формат CSV. Трябва да съдържа две колони — електронната поща в първата колона и името в последната колона от файла (електронна поща, име) — за потребителите, които искате да добавите в пространството за участие, без заглавки. - explanation_example: | - john.doe@example.org%{csv_col_sep}John Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - title: Импортирайте частни участници чрез CSV - upload: Качване reminders: create: error: Възникна проблем при създаването на напомняния. diff --git a/decidim-admin/config/locales/bs-BA.yml b/decidim-admin/config/locales/bs-BA.yml index cdba8743d5763..83991e50e9b21 100644 --- a/decidim-admin/config/locales/bs-BA.yml +++ b/decidim-admin/config/locales/bs-BA.yml @@ -324,8 +324,6 @@ bs: sent_to: Poslato subject: Naslov name: Bilten - participatory_space_private_user: - name: Privatni učesnik prostora za diskusiju scope: fields: name: Ime diff --git a/decidim-admin/config/locales/ca-IT.yml b/decidim-admin/config/locales/ca-IT.yml index f5e6e706dd919..7984095eb7f18 100644 --- a/decidim-admin/config/locales/ca-IT.yml +++ b/decidim-admin/config/locales/ca-IT.yml @@ -33,6 +33,11 @@ ca-IT: help_section: content: Contingut id: ID + member: + email: Correu electrònic + name: Nom + member_csv_import: + file: Arxiu newsletter: body: Cos send_to_all_users: Envia a totes les participants @@ -91,11 +96,6 @@ ca-IT: welcome_notification_body: Cos de la notificació de benvinguda welcome_notification_subject: Assumpte de la notificació de benvinguda youtube_handler: Nom d'usuària de YouTube - participatory_space_private_user: - email: Correu electrònic - name: Nom - participatory_space_private_user_csv_import: - file: Arxiu scope: code: Codi name: Nom @@ -132,6 +132,10 @@ ca-IT: file: Fitxer errors: models: + member_csv_import: + attributes: + file: + malformed: Arxiu d'importació mal formatat, si us plau, llegeix les instruccions curosament i assegura't que l'arxiu està codificat en UTF-8. newsletter: attributes: base: @@ -140,10 +144,6 @@ ca-IT: attributes: official_img_footer: allowed_file_content_types: Fitxer d'imatge no vàlid - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Arxiu d'importació mal formatat, si us plau, llegeix les instruccions curosament i assegura't que l'arxiu està codificat en UTF-8. user_group_csv_verification: attributes: file: @@ -191,12 +191,12 @@ ca-IT: export: Exporta export-selection: Exportar selecció import: Importar + member: + new: Nova membre menu_hidden: Amaga del menú moderate: Gestionar les moderacions newsletter: new: Nou butlletí - participatory_space_private_user: - new: Nou usuari privat de l'espai participatiu per_page: Per pàgina permissions: Gestionar els permisos restore: Restaurar @@ -418,6 +418,17 @@ ca-IT: values: 'false': 'No' 'true': 'Sí' + members: + user_invitation_accepted_at_not_null: + label: Invitació acceptada + values: + 'false': No acceptada + 'true': Acceptada + user_invitation_sent_at_not_null: + label: Invitació enviada + values: + 'false': No enviada + 'true': Enviada moderated_users: reports_reason_eq: label: Motiu de l'informe @@ -433,17 +444,6 @@ ca-IT: values: 'false': Oficialitzada 'true': No oficialitzada - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Invitació acceptada - values: - 'false': No acceptada - 'true': Acceptada - user_invitation_sent_at_not_null: - label: S'ha enviat la invitació - values: - 'false': No enviada - 'true': Enviada private_space_eq: label: Privat values: @@ -576,6 +576,51 @@ ca-IT: explanation: Les participants gestionades es poden promocionar a participants estàndard. Significa que seran convidades al sistema i no podràs tornar a administrar-les. La participant convidada rebrà un correu electrònic per acceptar la vostra invitació. new_managed_user_promotion: Nova promoció de participant promote: Promocionar + members: + create: + error: S'ha produït un error en actualitzar una administradora per a aquest procés participatiu. + success: Accés com a membre creat correctament. + destroy: + error: S'ha produït un error en eliminar una participant participada d'aquest espai participació. + success: Accés com a membre eliminat correctament. + edit: + update: Actualizar + index: + import_via_csv: Importar des de CSV + publish_all: Publicar totes + title: Membre + unpublish_all: Despublicar totes + new: + create: Crear + publish_all: + error: S'ha produït un error en publicar totes les membres d'aquest espai de participació. + success: S'han publicat correctament totes les membres d'aquest espai de participació + unpublish_all: + error: S'ha produït un error en despublicar totes les membres d'aquest espai de participació. + success: S'han despublicat correctament totes les membres d'aquest espai de participació + update: + error: S'ha produït un error en actualitzar la membre per a aquest espai de participació. + success: Membre actualitzada correctament + members_csv_imports: + create: + invalid: S'ha produït un error llegint el fitxer CSV. Si us plau, assegura't d'haver seguit les instruccions. + success: L'arxiu CSV s'ha carregat amb èxit, estem enviant un correu d'invitació a les participants. Això pot trigar una estona. + new: + csv_upload: + title: Puja el fitxer CSV + destroy: + button: Esborrar totes les membres + confirm: Segur que vols esborrar totes les membres? Aquesta acció no es pot desfer, no podràs recuperar-les. + empty: No hi ha membres. + explanation: Hi ha %{count} membres. + title: Elimina membres + example_file: 'Fitxer d''exemple:' + explanation: 'Carrega el teu arxiu CSV. Ha de tenir dues columnes amb l''adreça de correu electrònic a la primera columna i el nom a la segona de les participants que vulguis afegir a l''espai de participació, sense capçaleres. Evita emprar caràcters invàlids com `<>?%&^*#@()[]=+:;"{}\|` al nom d''usuària.' + explanation_example: | + john.doe@example.org%{csv_col_sep}John Doe + jane.doe@example.org%{csv_col_sep}Jane Doe + title: Importar membres via CSV + upload: Carregar menu: admin_log: Registre d'activitat d'administració admins: Administradores @@ -634,6 +679,8 @@ ca-IT: reason: Raó started_at: Va començar el user: Participant + member: + name: Membre newsletter: fields: created_at: Data de creació @@ -642,8 +689,6 @@ ca-IT: sent_to: Enviat a subject: Assumpte name: Butlletí - participatory_space_private_user: - name: Participant d'espai de participació privat scope: fields: name: Nom @@ -918,53 +963,6 @@ ca-IT: form: add: Afegeix a la llista de permesos title: Llistat de dominis externs permesos - participatory_space_private_users: - create: - error: S'ha produït un error en afegir una participant privada a aquest espai de participació. - success: L'accés de la participant a l'espai de participació privat s'ha creat correctament. - destroy: - error: S'ha produït un error en eliminar una participant participada d'aquest espai participatiu. - success: L'accés de la participant a l'espai de participació privat s'ha eliminat correctament. - edit: - title: Editar participant privada de l'espai de participació. - update: Actualitzar - index: - import_via_csv: Importar des de CSV - publish_all: Publicar tot - title: Participant de l'espai participatiu privat - unpublish_all: Despublicar tot - new: - create: Crear - title: Nova participant de l'espai privat. - publish_all: - error: S'ha produït un error en publicar totes les participants privades d'aquest espai de participació. - success: S'han publicat correctament totes les participants privades d'aquest espai de participació - unpublish_all: - error: S'ha produït un error en despublicar totes les participants privades d'aquest espai de participació. - success: S'han despublicat correctament totes les participants privades d'aquest espai de participació - update: - error: S'ha produït un error en actualitzar una participant participada d'aquest espai de participació. - success: S'ha actualitzat correctament l'espai de participació privat - participatory_space_private_users_csv_imports: - create: - invalid: S'ha produït un error llegint el fitxer CSV. Si us plau, assegura't d'haver seguit les instruccions. - success: L'arxiu CSV s'ha carregat amb èxit, estem enviant un correu d'invitació a les participants. Això pot trigar una estona. - new: - csv_upload: - title: Puja el teu fitxer CSV - destroy: - button: Esborrar totes les participants privades - confirm: Segur que vols esborrar totes les participants privades? Aquesta acció no es pot desfer, no podràs recuperar-les. - empty: No hi ha participants privades. - explanation: Hi ha %{count} participant/s privada/ades. - title: Esborrar les participants privades - example_file: 'Fitxer d''exemple:' - explanation: 'Carrega el teu arxiu CSV. Ha de tenir dues columnes amb l''adreça de correu electrònic a la primera columna i el nom a la segona de les participants que vulguis afegir a l''espai de participació, sense capçaleres. Evita emprar caràcters invàlids com `<>?%&^*#@()[]=+:;"{}\|` al nom d''usuària.' - explanation_example: | - john.doe@example.org%{csv_col_sep}John Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - title: Importar participant privades via CSV - upload: Carrega reminders: create: error: Hi ha hagut un problema en crear els recordatoris. diff --git a/decidim-admin/config/locales/ca.yml b/decidim-admin/config/locales/ca.yml index 21afd22f41205..b4dd240f30653 100644 --- a/decidim-admin/config/locales/ca.yml +++ b/decidim-admin/config/locales/ca.yml @@ -33,6 +33,11 @@ ca: help_section: content: Contingut id: ID + member: + email: Correu electrònic + name: Nom + member_csv_import: + file: Arxiu newsletter: body: Cos send_to_all_users: Envia a totes les participants @@ -91,11 +96,6 @@ ca: welcome_notification_body: Cos de la notificació de benvinguda welcome_notification_subject: Assumpte de la notificació de benvinguda youtube_handler: Nom d'usuària de YouTube - participatory_space_private_user: - email: Correu electrònic - name: Nom - participatory_space_private_user_csv_import: - file: Arxiu scope: code: Codi name: Nom @@ -132,6 +132,10 @@ ca: file: Fitxer errors: models: + member_csv_import: + attributes: + file: + malformed: Arxiu d'importació mal formatat, si us plau, llegeix les instruccions curosament i assegura't que l'arxiu està codificat en UTF-8. newsletter: attributes: base: @@ -140,10 +144,6 @@ ca: attributes: official_img_footer: allowed_file_content_types: Fitxer d'imatge no vàlid - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Arxiu d'importació mal formatat, si us plau, llegeix les instruccions curosament i assegura't que l'arxiu està codificat en UTF-8. user_group_csv_verification: attributes: file: @@ -191,12 +191,12 @@ ca: export: Exporta export-selection: Exportar selecció import: Importar + member: + new: Nova membre menu_hidden: Amaga del menú moderate: Gestionar les moderacions newsletter: new: Nou butlletí - participatory_space_private_user: - new: Nou usuari privat de l'espai participatiu per_page: Per pàgina permissions: Gestionar els permisos restore: Restaurar @@ -418,6 +418,17 @@ ca: values: 'false': 'No' 'true': 'Sí' + members: + user_invitation_accepted_at_not_null: + label: Invitació acceptada + values: + 'false': No acceptada + 'true': Acceptada + user_invitation_sent_at_not_null: + label: Invitació enviada + values: + 'false': No enviada + 'true': Enviada moderated_users: reports_reason_eq: label: Motiu de l'informe @@ -433,17 +444,6 @@ ca: values: 'false': Oficialitzada 'true': No oficialitzada - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Invitació acceptada - values: - 'false': No acceptada - 'true': Acceptada - user_invitation_sent_at_not_null: - label: S'ha enviat la invitació - values: - 'false': No enviada - 'true': Enviada private_space_eq: label: Privat values: @@ -576,6 +576,51 @@ ca: explanation: Les participants gestionades es poden promocionar a participants estàndard. Significa que seran convidades al sistema i no podràs tornar a administrar-les. La participant convidada rebrà un correu electrònic per acceptar la vostra invitació. new_managed_user_promotion: Nova promoció de participant promote: Promocionar + members: + create: + error: S'ha produït un error en actualitzar una administradora per a aquest procés participatiu. + success: Accés com a membre creat correctament. + destroy: + error: S'ha produït un error en eliminar una participant participada d'aquest espai participació. + success: Accés com a membre eliminat correctament. + edit: + update: Actualizar + index: + import_via_csv: Importar des de CSV + publish_all: Publicar totes + title: Membre + unpublish_all: Despublicar totes + new: + create: Crear + publish_all: + error: S'ha produït un error en publicar totes les membres d'aquest espai de participació. + success: S'han publicat correctament totes les membres d'aquest espai de participació + unpublish_all: + error: S'ha produït un error en despublicar totes les membres d'aquest espai de participació. + success: S'han despublicat correctament totes les membres d'aquest espai de participació + update: + error: S'ha produït un error en actualitzar la membre per a aquest espai de participació. + success: Membre actualitzada correctament + members_csv_imports: + create: + invalid: S'ha produït un error llegint el fitxer CSV. Si us plau, assegura't d'haver seguit les instruccions. + success: L'arxiu CSV s'ha carregat amb èxit, estem enviant un correu d'invitació a les participants. Això pot trigar una estona. + new: + csv_upload: + title: Puja el fitxer CSV + destroy: + button: Esborrar totes les membres + confirm: Segur que vols esborrar totes les membres? Aquesta acció no es pot desfer, no podràs recuperar-les. + empty: No hi ha membres. + explanation: Hi ha %{count} membres. + title: Elimina membres + example_file: 'Fitxer d''exemple:' + explanation: 'Carrega el teu arxiu CSV. Ha de tenir dues columnes amb l''adreça de correu electrònic a la primera columna i el nom a la segona de les participants que vulguis afegir a l''espai de participació, sense capçaleres. Evita emprar caràcters invàlids com `<>?%&^*#@()[]=+:;"{}\|` al nom d''usuària.' + explanation_example: | + john.doe@example.org%{csv_col_sep}John Doe + jane.doe@example.org%{csv_col_sep}Jane Doe + title: Importar membres via CSV + upload: Carregar menu: admin_log: Registre d'activitat d'administració admins: Administradores @@ -634,6 +679,8 @@ ca: reason: Raó started_at: Va començar el user: Participant + member: + name: Membre newsletter: fields: created_at: Data de creació @@ -642,8 +689,6 @@ ca: sent_to: Enviat a subject: Assumpte name: Butlletí - participatory_space_private_user: - name: Participant d'espai de participació privat scope: fields: name: Nom @@ -918,53 +963,6 @@ ca: form: add: Afegeix a la llista de permesos title: Llistat de dominis externs permesos - participatory_space_private_users: - create: - error: S'ha produït un error en afegir una participant privada a aquest espai de participació. - success: L'accés de la participant a l'espai de participació privat s'ha creat correctament. - destroy: - error: S'ha produït un error en eliminar una participant participada d'aquest espai participatiu. - success: L'accés de la participant a l'espai de participació privat s'ha eliminat correctament. - edit: - title: Editar participant privada de l'espai de participació. - update: Actualitzar - index: - import_via_csv: Importar des de CSV - publish_all: Publicar tot - title: Participant de l'espai participatiu privat - unpublish_all: Despublicar tot - new: - create: Crear - title: Nova participant de l'espai privat. - publish_all: - error: S'ha produït un error en publicar totes les participants privades d'aquest espai de participació. - success: S'han publicat correctament totes les participants privades d'aquest espai de participació - unpublish_all: - error: S'ha produït un error en despublicar totes les participants privades d'aquest espai de participació. - success: S'han despublicat correctament totes les participants privades d'aquest espai de participació - update: - error: S'ha produït un error en actualitzar una participant participada d'aquest espai de participació. - success: S'ha actualitzat correctament l'espai de participació privat - participatory_space_private_users_csv_imports: - create: - invalid: S'ha produït un error llegint el fitxer CSV. Si us plau, assegura't d'haver seguit les instruccions. - success: L'arxiu CSV s'ha carregat amb èxit, estem enviant un correu d'invitació a les participants. Això pot trigar una estona. - new: - csv_upload: - title: Puja el teu fitxer CSV - destroy: - button: Esborrar totes les participants privades - confirm: Segur que vols esborrar totes les participants privades? Aquesta acció no es pot desfer, no podràs recuperar-les. - empty: No hi ha participants privades. - explanation: Hi ha %{count} participant/s privada/ades. - title: Esborrar les participants privades - example_file: 'Fitxer d''exemple:' - explanation: 'Carrega el teu arxiu CSV. Ha de tenir dues columnes amb l''adreça de correu electrònic a la primera columna i el nom a la segona de les participants que vulguis afegir a l''espai de participació, sense capçaleres. Evita emprar caràcters invàlids com `<>?%&^*#@()[]=+:;"{}\|` al nom d''usuària.' - explanation_example: | - john.doe@example.org%{csv_col_sep}John Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - title: Importar participant privades via CSV - upload: Carrega reminders: create: error: Hi ha hagut un problema en crear els recordatoris. diff --git a/decidim-admin/config/locales/cs.yml b/decidim-admin/config/locales/cs.yml index b0103ff991807..768e80a4ee5c9 100644 --- a/decidim-admin/config/locales/cs.yml +++ b/decidim-admin/config/locales/cs.yml @@ -91,11 +91,6 @@ cs: welcome_notification_body: Tělo uvítacího oznámení welcome_notification_subject: Předmět uvítacího oznámení youtube_handler: YouTube handler - participatory_space_private_user: - email: E-mail - name: Název - participatory_space_private_user_csv_import: - file: Soubor scope: code: Kód name: Název @@ -137,10 +132,6 @@ cs: attributes: official_img_footer: allowed_file_content_types: Neplatný soubor s obrázkem - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Chybný importní soubor, přečtěte si pozorně pokyny a ujistěte se, že soubor je kódovaný UTF-8. user_group_csv_verification: attributes: file: @@ -192,8 +183,6 @@ cs: moderate: Spravovat moderace newsletter: new: Nový zpravodaj - participatory_space_private_user: - new: Nový soukromý uživatel participačního prostoru per_page: Na stránku permissions: Správa oprávnění restore: Obnovit @@ -432,17 +421,6 @@ cs: values: 'false': Ověřeno 'true': Neověřeno - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Pozvánka přijata - values: - 'false': Nepřijato - 'true': Přijato - user_invitation_sent_at_not_null: - label: Pozvánka odeslána - values: - 'false': Neodesláno - 'true': Odesláno private_space_eq: label: Soukromé values: @@ -583,6 +561,10 @@ cs: explanation: Spravovaní uživatelé mohou být povýšeni na standardní uživatele. Znamená to, že budou pozváni do aplikace a nebudete je moci znovu předstírat. Pozvaný uživatel obdrží e-mail, ve kterém přijme vaši pozvánku. new_managed_user_promotion: Nová spravovaná podpora pro uživatele promote: Podporovat + members_csv_imports: + new: + csv_upload: + title: Nahrát soubor CSV menu: admin_log: Protokol aktivity správce admins: Administrátoři @@ -649,8 +631,6 @@ cs: sent_to: Odeslána subject: Předmět name: Zpravodaj - participatory_space_private_user: - name: Participační prostor soukromého účastníka scope: fields: name: Název @@ -925,53 +905,6 @@ cs: form: add: Přidat na Seznam povolených title: Seznam povolených externích domén - participatory_space_private_users: - create: - error: Při přidávání soukromého uživatele pro tento participační prostor došlo k chybě. - success: Přístup soukromého účastníka do participativního prostoru byl úspěšně vytvořen. - destroy: - error: Při odstraňování soukromého uživatele tohoto participačního prostoru došlo k chybě. - success: Přístup soukromého účastníka do participativního prostoru byl úspěšně zrušen. - edit: - title: Upravit participační prostor soukromého účastníka. - update: Aktualizovat - index: - import_via_csv: Importovat přes CSV - publish_all: Publikovat vše - title: Participační prostor soukromého účastníka - unpublish_all: Zrušit publikování všech - new: - create: Vytvořit - title: Nový participační prostor soukromého účastníka. - publish_all: - error: U tohoto participativního prostoru byl problém se zveřejněním všech soukromých účastníků. - success: Úspěšné zveřejnění všech soukromých účastníků tohoto participativního prostoru - unpublish_all: - error: U tohoto participativního prostoru byl problém se zrušením zveřejnění všech soukromých účastníků. - success: Úspěšně zneveřejněni všichni soukromí účastníci tohoto participativního prostoru - update: - error: U tohoto participativního prostoru se vyskytl problém s aktualizací soukromého účastníka. - success: Soukromý účastník participativního prostoru úspěšně aktualizován - participatory_space_private_users_csv_imports: - create: - invalid: Při čtení souboru CSV se vyskytl problém. Ujistěte se, že jste dodrželi pokyny. - success: CSV soubor byl úspěšně nahrán, posíláme účastníkům e-mail s pozvánkou. To může chvíli trvat. - new: - csv_upload: - title: Nahrát soubor CSV - destroy: - button: Odstranit všechny soukromé účastníky - confirm: Jste si jisti, že chcete odstranit všechny soukromé účastníky? Tuto akci nelze vrátit zpět, nebudete je moci obnovit. - empty: Nemáte žádné soukromé účastníky. - explanation: Máte %{count} soukromých účastníků. - title: Odstranit soukromé účastníky - example_file: 'Příklad souboru:' - explanation: 'Nahrajte soubor CSV. Musí mít dva sloupce s e-mailem v prvním sloupci souboru a jméno v posledním sloupci souboru uživatelů, které chcete přidat do participačního prostoru, bez hlaviček. Vyhněte se používání neplatných znaků jako `<>?%&^*#@()[]=+:;"{}\|` v uživatelském jméně.' - explanation_example: | - john.doe@example.org%{csv_col_sep}John Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - title: Importovat soukromé účastníky přes CSV - upload: Nahrát reminders: create: error: Při vytváření připomenutí došlo k chybě. diff --git a/decidim-admin/config/locales/de.yml b/decidim-admin/config/locales/de.yml index 7c398e3ab3f97..868eb3cfd79ba 100644 --- a/decidim-admin/config/locales/de.yml +++ b/decidim-admin/config/locales/de.yml @@ -91,11 +91,6 @@ de: welcome_notification_body: Text der Willkommens-Benachrichtigung welcome_notification_subject: Betreff der Willkommens-Benachrichtigung youtube_handler: YouTube-Handler - participatory_space_private_user: - email: E-Mail - name: Name - participatory_space_private_user_csv_import: - file: Datei scope: code: Code name: Name @@ -137,10 +132,6 @@ de: attributes: official_img_footer: allowed_file_content_types: Ungültige Bilddatei - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Fehlerhafte Importdatei. Bitte lesen Sie die Anweisungen sorgfältig durch und stellen Sie sicher, dass die Datei UTF-8 kodiert ist. user_group_csv_verification: attributes: file: @@ -192,8 +183,6 @@ de: moderate: Moderationen verwalten newsletter: new: Neuer Newsletter - participatory_space_private_user: - new: Neuer privater Benutzer per_page: Pro Seite permissions: Berechtigungen verwalten restore: Wiederherstellen @@ -430,17 +419,6 @@ de: values: 'false': Offizialisiert 'true': Nicht offiziell - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Einladung akzeptiert - values: - 'false': Nicht akzeptiert - 'true': Akzeptiert - user_invitation_sent_at_not_null: - label: Einladung versendet - values: - 'false': Nicht versendet - 'true': Versendet private_space_eq: label: Privat values: @@ -573,6 +551,10 @@ de: explanation: Verwaltete Benutzer können zu Standardbenutzern heraufgestuft werden. Das bedeutet, dass sie zu der Anwendung eingeladen werden und nicht in der Lage sind, sie erneut zu repräsentieren. Der eingeladene Benutzer erhält eine E-Mail, um Ihre Einladung anzunehmen. new_managed_user_promotion: Neue verwaltete Benutzerwerbung promote: Fördern + members_csv_imports: + new: + csv_upload: + title: Laden Sie Ihre CSV-Datei hoch menu: admin_log: Admin-Aktivitätsprotokoll admins: Admins @@ -639,8 +621,6 @@ de: sent_to: Gesendet an subject: Gegenstand name: Newsletter - participatory_space_private_user: - name: Participatory Space privater Benutzer scope: fields: name: Name @@ -915,53 +895,6 @@ de: form: add: Zur erlaubten Liste hinzufügen title: Liste erlaubter externer Domains - participatory_space_private_users: - create: - error: Beim Hinzufügen eines privaten Benutzers für diesen partizipativen Bereich ist ein Fehler aufgetreten. - success: Ein privater Zugriff für diesen Beteiligungsbereich wurde erfolgreich für das Konto erstellt. - destroy: - error: Beim Löschen eines privaten Benutzers für diesen partizipativen Bereich ist ein Fehler aufgetreten. - success: Privater Zugriff zum partizipativen Raum erfolgreich gelöscht. - edit: - title: Privates Mitglied des partiziaptiven Bereichs bearbeiten. - update: Aktualisieren - index: - import_via_csv: Aus CSV-Datein importieren - publish_all: Alle veröffentlichen - title: Participatory Space privater Benutzer - unpublish_all: Alle Veröffentlichungen aufheben - new: - create: Erstellen - title: Neuer privater Benutzer des Participatory Space. - publish_all: - error: Es gab ein Problem bei der Veröffentlichung aller privaten Teilnehmenden für diesen partizipativen Bereich. - success: Alle privaten Teilnehmenden erfolgreich für diesen partizipativen Bereich veröffentlicht - unpublish_all: - error: Es gab ein Problem beim Entfernen aller privaten Teilnehmenden für diesen partizipativen Bereich. - success: Alle privaten Teilnehmenden für diesen teilnehmenden Raum erfolgreich zurückgezogen - update: - error: Beim Aktualisieren des privaten Teilnehmenden für diesen Beteiligungsbereich ist ein Problem aufgetreten. - success: Privater Teilnehmende für diesen partizipativen Raum erfolgreich erstellt - participatory_space_private_users_csv_imports: - create: - invalid: Beim Lesen der CSV-Datei ist ein Problem aufgetreten. Bitte stellen Sie sicher, dass Sie den Anweisungen korrekt gefolgt sind. - success: CSV-Datei wurde erfolgreich hochgeladen, wir senden eine Einladungs-E-Mail an die Teilnehmenden. Dies kann eine Weile dauern. - new: - csv_upload: - title: CSV-Datei hochladen - destroy: - button: Alle private Teilnehmende auswählen - confirm: Sind Sie sicher, dass Sie alle private Teilnehmende entfernen möchten? Diese Aktion kann nicht rückgängig gemacht werden. Sie werden sie nicht wiederherstellen können. - empty: Sie haben keine private Teilnehmende. - explanation: Sie haben %{count} private Teilnehmende. - title: Alle private Teilnehmende entfernen - example_file: 'Beispieldatei:' - explanation: 'Laden Sie Ihre CSV-Datei mit den Teilnehmenden, die Sie zum Prozess hinzufügen möchten, hoch. Sie muss zwei Spalten (ohne Kopfzeile) haben, mit der E-Mail-Adresse in der ersten Spalte und dem Namen in der zweiten Spalte der Datei. Verwenden Sie keine ungültige Zeichen wie `<>?%&^*#@()[]=+:;"{}\|` im Namen der Teilnehmenden.' - explanation_example: | - john.doe@example.org%{csv_col_sep}John Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - title: Private Teilnehmende via CSV importieren - upload: Hochladen reminders: create: error: Beim Erstellen dieser Erinnerungen ist ein Problem aufgetreten. diff --git a/decidim-admin/config/locales/el.yml b/decidim-admin/config/locales/el.yml index a47652aa8ffbd..34cfb3c19e248 100644 --- a/decidim-admin/config/locales/el.yml +++ b/decidim-admin/config/locales/el.yml @@ -86,9 +86,6 @@ el: welcome_notification_body: Σώμα ειδοποίησης καλωσορίσματος welcome_notification_subject: Θέμα ειδοποίησης καλωσορίσματος youtube_handler: Πρόγραμμα χειρισμού YouTube - participatory_space_private_user: - email: Email - name: Όνομα scope: code: Κωδικός name: Όνομα @@ -129,10 +126,6 @@ el: attributes: official_img_footer: allowed_file_content_types: Μη έγκυρο αρχείο εικόνας - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Λάθος στο αρχείο εισαγωγής, παρακαλούμε διαβάστε προσεκτικά τις οδηγίες και βεβαιωθείτε ότι το αρχείο είναι κωδικοποιημένο σε UTF-8. user_group_csv_verification: attributes: file: @@ -171,8 +164,6 @@ el: import: Εισαγωγή newsletter: new: Νέο ενημερωτικό δελτίο - participatory_space_private_user: - new: Νέος ιδιωτικός χρήστης συμμετοχικού χώρου per_page: Ανά σελίδα share: Κοινοποίηση user: @@ -357,17 +348,6 @@ el: values: 'false': Επισημοποιήθηκε 'true': Δεν επισημοποιήθηκε - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Πρόσκληση αποδεκτή - values: - 'false': Δεν γίνεται δεκτό - 'true': Αποδοχή - user_invitation_sent_at_not_null: - label: Πρόσκλησης εστάλει - values: - 'false': Δεν έχει σταλεί - 'true': Απεστάλλει private_space_eq: label: Ιδιωτικό values: @@ -485,6 +465,10 @@ el: explanation: Οι διαχειριζόμενοι συμμετέχοντες μπορούν να αναβαθμιστούν σε τυπικούς συμμετέχοντες. Αυτό σημαίνει ότι θα προσκληθούν στην εφαρμογή και δεν θα μπορείτε να τους διαχειριστείτε ξανά. Ο προσκεκλημένος συμμετέχων θα λάβει email για να αποδεχτεί την πρόσκλησή σας. new_managed_user_promotion: Προώθηση νέου διαχειριζόμενου συμμετέχοντα promote: Αναβάθμιση + members_csv_imports: + new: + csv_upload: + title: Αποστολή του αρχείου σας CSV menu: admin_log: Αρχείο καταγραφής δραστηριοτήτων διαχειριστή admins: Διαχειριστές @@ -541,8 +525,6 @@ el: sent_to: Στάλθηκε σε subject: Θέμα name: Ενημερωτικό δελτίο - participatory_space_private_user: - name: Ιδιωτικός συμμετέχων χώρου συμμετοχής scope: fields: name: Όνομα @@ -720,27 +702,6 @@ el: update: error: Υπήρξε ένα πρόβλημα κατά την ενημέρωση αυτού του οργανισμού. success: Ο οργανισμός ενημερώθηκε με επιτυχία. - participatory_space_private_users: - create: - error: Υπήρξε ένα πρόβλημα κατά την προσθήκη ενός ιδιωτικού συμμετέχοντα για αυτόν τον χώρο συμμετοχής. - success: Η πρόσβαση του ιδιωτικού συμμετέχοντα στον χώρο συμμετοχής δημιουργήθηκε με επιτυχία. - destroy: - error: Υπήρξε ένα πρόβλημα κατά τη διαγραφή ενός ιδιωτικού συμμετέχοντα για αυτόν τον χώρο συμμετοχής. - success: Η πρόσβαση του ιδιωτικού συμμετέχοντα στον χώρο συμμετοχής καταστράφηκε με επιτυχία. - index: - import_via_csv: Εισαγωγή μέσω CSV - title: Ιδιωτικός συμμετέχων χώρου συμμετοχής - new: - create: Δημιουργία - title: Νέο ιδιωτικός συμμετέχων χώρου συμμετοχής. - participatory_space_private_users_csv_imports: - create: - success: Το αρχείο CSV μεταφορτώθηκε με επιτυχία. Στέλνουμε πρόσκληση στους συμμετέχοντες μέσω email. Αυτή η διαδικασία μπορεί να χρειαστεί λίγη ώρα. - new: - csv_upload: - title: Αποστολή του αρχείου σας CSV - example_file: 'Παράδειγμα αρχείου:' - upload: Αποστολή reminders: new: submit: Αποστολή diff --git a/decidim-admin/config/locales/eo.yml b/decidim-admin/config/locales/eo.yml index 72441adca2308..c7822411b652b 100644 --- a/decidim-admin/config/locales/eo.yml +++ b/decidim-admin/config/locales/eo.yml @@ -38,8 +38,6 @@ eo: impersonation_log: fields: ended_at: Finita je - participatory_space_private_user: - name: Privata partoprenanto newsletters: select_recipients_to_deliver: all_users_help: Sendu la informilon al ĉiuj konfirmitaj partoprenantoj. diff --git a/decidim-admin/config/locales/es-MX.yml b/decidim-admin/config/locales/es-MX.yml index 67926595a9eb8..bb78e5b7310d8 100644 --- a/decidim-admin/config/locales/es-MX.yml +++ b/decidim-admin/config/locales/es-MX.yml @@ -33,6 +33,11 @@ es-MX: help_section: content: Contenido id: ID + member: + email: Correo electrónico + name: Nombre + member_csv_import: + file: Archivo newsletter: body: Cuerpo send_to_all_users: Enviar a todas las participantes @@ -91,11 +96,6 @@ es-MX: welcome_notification_body: Cuerpo del mensaje de notificación de bienvenida welcome_notification_subject: Asunto del mensaje de notificación de bienvenida youtube_handler: Nombre de YouTube - participatory_space_private_user: - email: Correo electrónico - name: Nombre - participatory_space_private_user_csv_import: - file: Archivo scope: code: Código name: Nombre @@ -132,6 +132,10 @@ es-MX: file: Expediente errors: models: + member_csv_import: + attributes: + file: + malformed: Archivo de importación mal formateado, por favor lea las instrucciones cuidadosamente y asegúrese de que el archivo está codificado en UTF-8. newsletter: attributes: base: @@ -140,10 +144,6 @@ es-MX: attributes: official_img_footer: allowed_file_content_types: Archivo de imagen no válido - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Archivo de importación mal formateado, por favor lea las instrucciones cuidadosamente y asegúrese de que el archivo está codificado en UTF-8. user_group_csv_verification: attributes: file: @@ -191,12 +191,12 @@ es-MX: export: Exportar export-selection: Exportar selección import: Importar + member: + new: Nueva miembro menu_hidden: Ocultar del menú moderate: Gestionar las moderaciones newsletter: new: Nuevo boletín - participatory_space_private_user: - new: Nuevo usuario privado del espacio participativo per_page: Por página permissions: Gestionar los permisos restore: Restaurar @@ -418,6 +418,17 @@ es-MX: values: 'false': 'No' 'true': 'Sí' + members: + user_invitation_accepted_at_not_null: + label: Invitación aceptada + values: + 'false': No aceptada + 'true': Aceptada + user_invitation_sent_at_not_null: + label: Invitación enviada + values: + 'false': No enviada + 'true': Enviada moderated_users: reports_reason_eq: label: Motivo de la denuncia @@ -433,17 +444,6 @@ es-MX: values: 'false': Oficializado 'true': No oficializado - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Invitación aceptada - values: - 'false': No aceptada - 'true': Aceptada - user_invitation_sent_at_not_null: - label: Invitación enviada - values: - 'false': No enviada - 'true': Enviada private_space_eq: label: Privado values: @@ -576,6 +576,51 @@ es-MX: explanation: Los usuarios gestionados pueden ser promovidos a usuarios estándar. Esto significa que serán invitados a la aplicación y no será capaz de suplantarlos de nuevo. El usuario invitado recibirá un correo electrónico para aceptar su invitación. new_managed_user_promotion: Nueva promoción de usuarios gestionados promote: Promocionar + members: + create: + error: Se ha producido un error al agregar una participante privada a este espacio participativo. + success: Acceso como miembro creado correctamente. + destroy: + error: Se ha producido un error al eliminar a una participante privada para este espacio de participación. + success: Acceso como miembro eliminado correctamente. + edit: + update: Actualizar + index: + import_via_csv: Importar vía csv + publish_all: Publicar todas + title: Miembro + unpublish_all: Despublicar todas + new: + create: Crear + publish_all: + error: Se ha producido un error al publicar todas las miembros de este espacio de participación. + success: Se han publicado correctamente todas las miembros de este espacio de participación + unpublish_all: + error: Se ha producido un error al despublicar todas las miembros de este espacio de participación. + success: Se han despublicado correctamente todas las miembros de este espacio de participación + update: + error: Se ha producido un error al actualizar la miembro para este espacio de participación. + success: Miembro actualizada correctamente + members_csv_imports: + create: + invalid: Hubo un problema al leer el archivo CSV. Por favor, asegúrate de haber seguido las instrucciones. + success: El archivo CSV se ha cargado correctamente, estamos enviando un correo de invitación a las participantes. Este proceso puede tardar un poco. + new: + csv_upload: + title: Sube tu archivo CSV + destroy: + button: Borrar todas las miembros + confirm: '¿Seguro que quieres eliminar todas las miembros? Esta acción no se puede deshacer, no podrás recuperarlas.' + empty: No hay miembros. + explanation: Hay %{count} miembros. + title: Eliminar miembros + example_file: 'Archivo de ejemplo:' + explanation: 'Sube tu archivo CSV. Debe tener dos columnas con correo electrónico en la primera columna del archivo y el nombre en la última columna del archivo de las participantes que deseas añadir al espacio participativo, sin encabezados. Evita usar caracteres no válidos como `<>?%&^*#@()[]=+:;"{}\|` en el nombre de usuaria.' + explanation_example: | + john.doe@example.org%{csv_col_sep}John Doe + jane.doe@example.org%{csv_col_sep}Jane Doe + title: Importar miembros mediante CSV + upload: Cargar menu: admin_log: Registro de actividad de administrador admins: Administradores @@ -634,6 +679,8 @@ es-MX: reason: Razón started_at: Empezó a las user: Usuario + member: + name: Miembro newsletter: fields: created_at: Fecha de creación @@ -642,8 +689,6 @@ es-MX: sent_to: Enviado a subject: Asunto name: Boletín - participatory_space_private_user: - name: Usuario privado de espacio participativo scope: fields: name: Nombre @@ -918,53 +963,6 @@ es-MX: form: add: Añadir a la lista de direcciones permitidas title: Lista de dominios externos permitidos - participatory_space_private_users: - create: - error: Hubo un error al agregar un usuario privado para este espacio participativo. - success: El acceso a usuarios privados de espacio participativo creado con éxito. - destroy: - error: Se ha producido un error al eliminar un usuario privado para este espacio participativo. - success: El acceso de usuarios privados del espacio participativo se eliminado con éxito. - edit: - title: Editar participante privada del espacio de participación. - update: Actualizar - index: - import_via_csv: Importar vía csv - publish_all: Publicar todo - title: Usuario privado de espacio participativo. - unpublish_all: Despublicar todo - new: - create: Crear - title: Nuevo usuario privado en el espacio participativo. - publish_all: - error: Se ha producido un error al publicar todas las participantes privadas de este espacio de participación. - success: Se han publicado correctamente todas las participantes privadas de este espacio de participación - unpublish_all: - error: Se ha producido un error al despublicar todas las participantes privadas de este espacio de participación. - success: Se han despublicado correctamente todas las participantes privadas de este espacio de participación - update: - error: Se ha producido un error actualizando una participante privada de este espacio de participación. - success: Se ha actualizado correctamente el espacio de participación privado - participatory_space_private_users_csv_imports: - create: - invalid: Hubo un problema al leer el archivo CSV. Por favor, asegúrate de haber seguido las instrucciones. - success: El archivo CSV se ha cargado correctamente, estamos enviando un correo de invitación a las participantes. Este proceso puede tardar un poco. - new: - csv_upload: - title: Sube tu archivo CSV - destroy: - button: Borrar todas las participantes privadas - confirm: '¿Seguro que quieres eliminar todas las participantes privadas? Esta acción no se puede deshacer, no podrás recuperarlas.' - empty: No hay participantes privadas. - explanation: Hay %{count} participante/s privada/s. - title: Borrar las participantes privadas - example_file: 'Fichero de ejemplo:' - explanation: 'Sube tu archivo CSV. Debe tener dos columnas con correo electrónico en la primera columna del archivo y el nombre en la última columna del archivo de las participantes que deseas añadir al espacio participativo, sin encabezados. Evita usar caracteres no válidos como `<>?%&^*#@()[]=+:;"{}\|` en el nombre de usuaria.' - explanation_example: | - john.doe@example.org%{csv_col_sep}John Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - title: Importar participantes privadas vía CSV - upload: Subir reminders: create: error: Se ha producido un error al crear los recordatorios. diff --git a/decidim-admin/config/locales/es-PY.yml b/decidim-admin/config/locales/es-PY.yml index efec79c30f92b..3da1eab51a8c8 100644 --- a/decidim-admin/config/locales/es-PY.yml +++ b/decidim-admin/config/locales/es-PY.yml @@ -33,6 +33,11 @@ es-PY: help_section: content: Contenido id: ID + member: + email: Correo electrónico + name: Nombre + member_csv_import: + file: Archivo newsletter: body: Cuerpo send_to_all_users: Enviar a todas las participantes @@ -91,11 +96,6 @@ es-PY: welcome_notification_body: Cuerpo del mensaje de notificación de bienvenida welcome_notification_subject: Asunto del mensaje de notificación de bienvenida youtube_handler: Nombre de YouTube - participatory_space_private_user: - email: Correo electrónico - name: Nombre - participatory_space_private_user_csv_import: - file: Archivo scope: code: Código name: Nombre @@ -132,6 +132,10 @@ es-PY: file: Expediente errors: models: + member_csv_import: + attributes: + file: + malformed: Archivo de importación mal formateado, por favor lea las instrucciones cuidadosamente y asegúrese de que el archivo está codificado en UTF-8. newsletter: attributes: base: @@ -140,10 +144,6 @@ es-PY: attributes: official_img_footer: allowed_file_content_types: Archivo de imagen no válido - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Archivo de importación mal formateado, por favor lea las instrucciones cuidadosamente y asegúrese de que el archivo está codificado en UTF-8. user_group_csv_verification: attributes: file: @@ -191,12 +191,12 @@ es-PY: export: Exportar export-selection: Exportar selección import: Importar + member: + new: Nueva miembro menu_hidden: Ocultar del menú moderate: Gestionar las moderaciones newsletter: new: Nuevo boletín - participatory_space_private_user: - new: Nuevo usuario privado del espacio participativo per_page: Por página permissions: Gestionar los permisos restore: Restaurar @@ -418,6 +418,17 @@ es-PY: values: 'false': 'No' 'true': 'Sí' + members: + user_invitation_accepted_at_not_null: + label: Invitación aceptada + values: + 'false': No aceptada + 'true': Aceptada + user_invitation_sent_at_not_null: + label: Invitación enviada + values: + 'false': No enviada + 'true': Enviada moderated_users: reports_reason_eq: label: Motivo de la denuncia @@ -433,17 +444,6 @@ es-PY: values: 'false': Oficializado 'true': No oficializado - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Invitación aceptada - values: - 'false': No aceptada - 'true': Aceptada - user_invitation_sent_at_not_null: - label: Invitación enviada - values: - 'false': No enviada - 'true': Enviada private_space_eq: label: Privado values: @@ -576,6 +576,51 @@ es-PY: explanation: Los usuarios gestionados pueden ser promovidos a usuarios estándar. Esto significa que serán invitados a la aplicación y no será capaz de suplantarlos de nuevo. El usuario invitado recibirá un correo electrónico para aceptar su invitación. new_managed_user_promotion: Nueva promoción de usuarios gestionados promote: Promocionar + members: + create: + error: Se ha producido un error al agregar una participante privada a este espacio participativo. + success: Acceso como miembro creado correctamente. + destroy: + error: Se ha producido un error al eliminar a una participante privada para este espacio de participación. + success: Acceso como miembro eliminado correctamente. + edit: + update: Actualizar + index: + import_via_csv: Importar vía csv + publish_all: Publicar todas + title: Miembro + unpublish_all: Despublicar todas + new: + create: Crear + publish_all: + error: Se ha producido un error al publicar todas las miembros de este espacio de participación. + success: Se han publicado correctamente todas las miembros de este espacio de participación + unpublish_all: + error: Se ha producido un error al despublicar todas las miembros de este espacio de participación. + success: Se han despublicado correctamente todas las miembros de este espacio de participación + update: + error: Se ha producido un error al actualizar la miembro para este espacio de participación. + success: Miembro actualizada correctamente + members_csv_imports: + create: + invalid: Hubo un problema al leer el archivo CSV. Por favor, asegúrate de haber seguido las instrucciones. + success: El archivo CSV se ha cargado correctamente, estamos enviando un correo de invitación a las participantes. Este proceso puede tardar un poco. + new: + csv_upload: + title: Sube tu archivo CSV + destroy: + button: Borrar todas las miembros + confirm: '¿Seguro que quieres eliminar todas las miembros? Esta acción no se puede deshacer, no podrás recuperarlas.' + empty: No hay miembros. + explanation: Hay %{count} miembros. + title: Eliminar miembros + example_file: 'Archivo de ejemplo:' + explanation: 'Sube tu archivo CSV. Debe tener dos columnas con correo electrónico en la primera columna del archivo y el nombre en la última columna del archivo de las participantes que deseas añadir al espacio participativo, sin encabezados. Evita usar caracteres no válidos como `<>?%&^*#@()[]=+:;"{}\|` en el nombre de usuaria.' + explanation_example: | + john.doe@example.org%{csv_col_sep}John Doe + jane.doe@example.org%{csv_col_sep}Jane Doe + title: Importar miembros mediante CSV + upload: Cargar menu: admin_log: Registro de actividad de administrador admins: Administradores @@ -634,6 +679,8 @@ es-PY: reason: Razón started_at: Empezó a las user: Usuario + member: + name: Miembro newsletter: fields: created_at: Fecha de creación @@ -642,8 +689,6 @@ es-PY: sent_to: Enviado a subject: Asunto name: Boletín - participatory_space_private_user: - name: Usuario privado de espacio participativo scope: fields: name: Nombre @@ -918,53 +963,6 @@ es-PY: form: add: Añadir a la lista de direcciones permitidas title: Lista de dominios externos permitidos - participatory_space_private_users: - create: - error: Hubo un error al agregar un usuario privado para este espacio participativo. - success: El acceso a usuarios privados de espacio participativo creado con éxito. - destroy: - error: Se ha producido un error al eliminar un usuario privado para este espacio participativo. - success: El acceso de usuarios privados del espacio participativo se eliminado con éxito. - edit: - title: Editar participante privada del espacio de participación. - update: Actualizar - index: - import_via_csv: Importar vía csv - publish_all: Publicar todo - title: Usuario privado de espacio participativo. - unpublish_all: Despublicar todo - new: - create: Crear - title: Nuevo usuario privado en el espacio participativo. - publish_all: - error: Se ha producido un error al publicar todas las participantes privadas de este espacio de participación. - success: Se han publicado correctamente todas las participantes privadas de este espacio de participación - unpublish_all: - error: Se ha producido un error al despublicar todas las participantes privadas de este espacio de participación. - success: Se han despublicado correctamente todas las participantes privadas de este espacio de participación - update: - error: Se ha producido un error actualizando una participante privada de este espacio de participación. - success: Se ha actualizado correctamente el espacio de participación privado - participatory_space_private_users_csv_imports: - create: - invalid: Hubo un problema al leer el archivo CSV. Por favor, asegúrate de haber seguido las instrucciones. - success: El archivo CSV se ha cargado correctamente, estamos enviando un correo de invitación a las participantes. Este proceso puede tardar un poco. - new: - csv_upload: - title: Sube tu archivo CSV - destroy: - button: Borrar todas las participantes privadas - confirm: '¿Seguro que quieres eliminar todas las participantes privadas? Esta acción no se puede deshacer, no podrás recuperarlas.' - empty: No hay participantes privadas. - explanation: Hay %{count} participante/s privada/s. - title: Borrar las participantes privadas - example_file: 'Fichero de ejemplo:' - explanation: 'Sube tu archivo CSV. Debe tener dos columnas con correo electrónico en la primera columna del archivo y el nombre en la última columna del archivo de las participantes que deseas añadir al espacio participativo, sin encabezados. Evita usar caracteres no válidos como `<>?%&^*#@()[]=+:;"{}\|` en el nombre de usuaria.' - explanation_example: | - john.doe@example.org%{csv_col_sep}John Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - title: Importar participantes privadas vía CSV - upload: Subir reminders: create: error: Se ha producido un error al crear los recordatorios. diff --git a/decidim-admin/config/locales/es.yml b/decidim-admin/config/locales/es.yml index d5dc956249129..f3bd31d64a63c 100644 --- a/decidim-admin/config/locales/es.yml +++ b/decidim-admin/config/locales/es.yml @@ -33,6 +33,11 @@ es: help_section: content: Contenido id: ID + member: + email: Correo electrónico + name: Nombre + member_csv_import: + file: Archivo newsletter: body: Cuerpo send_to_all_users: Enviar a todas las participantes @@ -91,11 +96,6 @@ es: welcome_notification_body: Cuerpo del mensaje de notificación de bienvenida welcome_notification_subject: Asunto del mensaje de notificación de bienvenida youtube_handler: Nombre de YouTube - participatory_space_private_user: - email: Correo electrónico - name: Nombre - participatory_space_private_user_csv_import: - file: Archivo scope: code: Código name: Nombre @@ -132,6 +132,10 @@ es: file: Archivo errors: models: + member_csv_import: + attributes: + file: + malformed: Archivo de importación mal formateado, por favor lea las instrucciones cuidadosamente y asegúrese de que el archivo está codificado en UTF-8. newsletter: attributes: base: @@ -140,10 +144,6 @@ es: attributes: official_img_footer: allowed_file_content_types: Archivo de imagen no válido - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Archivo de importación mal formateado, por favor lea las instrucciones cuidadosamente y asegúrese de que el archivo está codificado en UTF-8. user_group_csv_verification: attributes: file: @@ -191,12 +191,12 @@ es: export: Exportar export-selection: Exportar selección import: Importar + member: + new: Nueva miembro menu_hidden: Ocultar del menú moderate: Gestionar las moderaciones newsletter: new: Nuevo boletín - participatory_space_private_user: - new: Nuevo usuario privado del espacio participativo per_page: Por página permissions: Gestionar los permisos restore: Restaurar @@ -418,6 +418,17 @@ es: values: 'false': 'No' 'true': 'Sí' + members: + user_invitation_accepted_at_not_null: + label: Invitación aceptada + values: + 'false': No aceptada + 'true': Aceptada + user_invitation_sent_at_not_null: + label: Invitación enviada + values: + 'false': No enviada + 'true': Enviada moderated_users: reports_reason_eq: label: Motivo de la denuncia @@ -433,17 +444,6 @@ es: values: 'false': Oficializada 'true': No oficializada - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Invitación aceptada - values: - 'false': No aceptada - 'true': Aceptada - user_invitation_sent_at_not_null: - label: Invitación enviada - values: - 'false': No enviada - 'true': Enviada private_space_eq: label: Privado values: @@ -576,6 +576,51 @@ es: explanation: Las participantes gestionadas se pueden promocionar a participantes estándar. Significa que serán invitadas al sistema y no podrás volver a administrarlas. La participante invitada recibirá un correo electrónico para aceptar tu invitación. new_managed_user_promotion: Nueva promoción de participante promote: Promocionar + members: + create: + error: Se ha producido un error al agregar una participante privada a este espacio participativo. + success: Acceso como miembro creado correctamente. + destroy: + error: Se ha producido un error al eliminar a una participante privada para este espacio de participación. + success: Acceso como miembro eliminado correctamente. + edit: + update: Actualizar + index: + import_via_csv: Importar vía csv + publish_all: Publicar todas + title: Miembro + unpublish_all: Despublicar todas + new: + create: Crear + publish_all: + error: Se ha producido un error al publicar todas las miembros de este espacio de participación. + success: Se han publicado correctamente todas las miembros de este espacio de participación + unpublish_all: + error: Se ha producido un error al despublicar todas las miembros de este espacio de participación. + success: Se han despublicado correctamente todas las miembros de este espacio de participación + update: + error: Se ha producido un error al actualizar la miembro para este espacio de participación. + success: Miembro actualizada correctamente + members_csv_imports: + create: + invalid: Hubo un problema al leer el archivo CSV. Por favor, asegúrate de haber seguido las instrucciones. + success: El archivo CSV se ha cargado correctamente, estamos enviando un correo de invitación a las participantes. Este proceso puede tardar un poco. + new: + csv_upload: + title: Sube tu archivo CSV + destroy: + button: Borrar todas las miembros + confirm: '¿Seguro que quieres eliminar todas las miembros? Esta acción no se puede deshacer, no podrás recuperarlas.' + empty: No hay miembros. + explanation: Hay %{count} miembros. + title: Eliminar miembros + example_file: 'Archivo de ejemplo:' + explanation: 'Sube tu archivo CSV. Debe tener dos columnas con correo electrónico en la primera columna del archivo y el nombre en la última columna del archivo de las participantes que deseas añadir al espacio participativo, sin encabezados. Evita usar caracteres no válidos como `<>?%&^*#@()[]=+:;"{}\|` en el nombre de usuaria.' + explanation_example: | + john.doe@example.org%{csv_col_sep}John Doe + jane.doe@example.org%{csv_col_sep}Jane Doe + title: Importar miembros mediante CSV + upload: Cargar menu: admin_log: Registro de actividad de administración admins: Administradoras @@ -634,6 +679,8 @@ es: reason: Razón started_at: Empezó a las user: Participante + member: + name: Miembro newsletter: fields: created_at: Fecha de creación @@ -642,8 +689,6 @@ es: sent_to: Enviado a subject: Asunto name: Boletín - participatory_space_private_user: - name: Participante de espacio de participación privado scope: fields: name: Nombre @@ -918,53 +963,6 @@ es: form: add: Añadir a la lista de direcciones permitidas title: Lista de dominios externos permitidos - participatory_space_private_users: - create: - error: Se ha producido un error al agregar una participante privada a este espacio participativo. - success: Acceso privado a participantes del espacio participativo creado correctamente. - destroy: - error: Se ha producido un error al eliminar a una participante privada para este espacio participativo. - success: El acceso de la participante al espacio de participación privado se ha eliminado correctamente. - edit: - title: Editar participante privada del espacio de participación. - update: Actualizar - index: - import_via_csv: Importar vía csv - publish_all: Publicar todo - title: Participante de espacio de participación privado - unpublish_all: Despublicar todo - new: - create: Crear - title: Nueva participante del espacio privado. - publish_all: - error: Se ha producido un error al publicar todas las participantes privadas de este espacio de participación. - success: Se han publicado correctamente todas las participantes privadas de este espacio de participación - unpublish_all: - error: Se ha producido un error al despublicar todas las participantes privadas de este espacio de participación. - success: Se han despublicado correctamente todas las participantes privadas de este espacio de participación - update: - error: Se ha producido un error actualizando una participante privada de este espacio de participación. - success: Se ha actualizado correctamente el espacio de participación privado - participatory_space_private_users_csv_imports: - create: - invalid: Hubo un problema al leer el archivo CSV. Por favor, asegúrate de haber seguido las instrucciones. - success: El archivo CSV se ha cargado correctamente, estamos enviando un correo de invitación a las participantes. Este proceso puede tardar un poco. - new: - csv_upload: - title: Sube tu archivo CSV - destroy: - button: Borrar todas las participantes privadas - confirm: '¿Seguro que quieres eliminar todas las participantes privadas? Esta acción no se puede deshacer, no podrás recuperarlas.' - empty: No hay participantes privadas. - explanation: Hay %{count} participante/s privada/s. - title: Borrar las participantes privadas - example_file: 'Fichero de ejemplo:' - explanation: 'Sube tu archivo CSV. Debe tener dos columnas con correo electrónico en la primera columna del archivo y el nombre en la última columna del archivo de las participantes que deseas añadir al espacio participativo, sin encabezados. Evita usar caracteres no válidos como `<>?%&^*#@()[]=+:;"{}\|` en el nombre de usuaria.' - explanation_example: | - john.doe@example.org%{csv_col_sep}John Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - title: Importar participantes privadas vía CSV - upload: Subir reminders: create: error: Se ha producido un error al crear los recordatorios. diff --git a/decidim-admin/config/locales/eu.yml b/decidim-admin/config/locales/eu.yml index 4d9201be4b03a..ecf2e200a57ac 100644 --- a/decidim-admin/config/locales/eu.yml +++ b/decidim-admin/config/locales/eu.yml @@ -33,6 +33,11 @@ eu: help_section: content: Edukia id: ID + member: + email: Helbide elektronikoa + name: Izena + member_csv_import: + file: Fitxategia newsletter: body: Testua send_to_all_users: Bidali parte-hartzaile guztiei @@ -91,11 +96,6 @@ eu: welcome_notification_body: Ongietorri-jakinarazpenaren testua welcome_notification_subject: Ongi etorri jakinarazpenaren mezuaren gaia youtube_handler: YouTubeko kudeatzailea - participatory_space_private_user: - email: Helbide elektronikoa - name: Izena - participatory_space_private_user_csv_import: - file: Fitxategia scope: code: Kodea name: Izena @@ -132,6 +132,10 @@ eu: file: Fitxategia errors: models: + member_csv_import: + attributes: + file: + malformed: Gaizki osatutako inportazio-fitxategia. Mesedez, irakurri arretaz jarraibideen bidez, eta ziurtatu fitxategia UTF-8 kodetuta dagoela. newsletter: attributes: base: @@ -140,10 +144,6 @@ eu: attributes: official_img_footer: allowed_file_content_types: Irudi-fitxategi baliogabea - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Fitxategia akastuna da. Irakurri arretaz jarraibideak eta ziurtatu fitxategia UTF-8an kodifikatuta dagoela. user_group_csv_verification: attributes: file: @@ -191,12 +191,12 @@ eu: export: Esportatu export-selection: Esportatu hautaketa import: Inportatu + member: + new: Beste kide bat menu_hidden: Ezkutatu menutik moderate: Kudeatu moderazioak newsletter: new: Beste buletin bat - participatory_space_private_user: - new: Partaidetza-espazio berria, erabiltzaile pribatua per_page: Orrialdeko permissions: Kudeatu baimenak restore: Berreskuratu @@ -418,6 +418,17 @@ eu: values: 'false': 'Ez' 'true': 'Bai' + members: + user_invitation_accepted_at_not_null: + label: Gonbidapena onartua + values: + 'false': Ez onartua + 'true': Onartua + user_invitation_sent_at_not_null: + label: Gonbidapena bidalita + values: + 'false': Ez da bidali + 'true': Bidalia moderated_users: reports_reason_eq: label: Arrazoiaren berri eman @@ -433,17 +444,6 @@ eu: values: 'false': Ofizializatua 'true': Ez ofizializatua - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Gonbidapena onartua - values: - 'false': Ez onartuta - 'true': Onartuta - user_invitation_sent_at_not_null: - label: Gonbidapena bidali da - values: - 'false': Ez da bidali - 'true': Bidalia private_space_eq: label: Pribatua values: @@ -576,6 +576,51 @@ eu: explanation: Kudeatutako parte-hartzaileak parte-hartzaile estandar mailara igo daitezke. Horrek esan nahi du aplikaziora gonbidatuak izanen direla eta ezingo direla berriro ere ordeztu. Parte-hartzaile gonbidatuak mezu elektroniko bat jasoko du gonbita onartzeko. new_managed_user_promotion: Kudeatutako parte-hartzaileen beste maila-igoera bat promote: Sustatu + members: + create: + error: Arazo bat izan da partaidetza-espazio horretarako kide bat gehitzeko. + success: Kideen sarbidea behar bezala sortu da. + destroy: + error: Arazo bat dago partaidetza-espazio horretarako kide bat ezabatzeko. + success: Kidearen sarbidea behar bezala suntsitu da. + edit: + update: Egunaratu + index: + import_via_csv: CSV fitxategi baten bidez inportatu + publish_all: Denak argitaratu + title: Kidea + unpublish_all: Desargitaratu guztiak + new: + create: Sortu + publish_all: + error: Arazo bat izan da partaidetza-espazio horretarako kide bat gehitzeko. + success: Partaidetza-espazio honetarako kide guztiak arrakastaz argitaratu dira + unpublish_all: + error: Arazo bat dago partaidetza-espazio horretarako kide guztiak desargitaratzeko. + success: Partaidetza-espazio honetako kide guztiak behar bezala ezabatu dira + update: + error: Arazo bat izan da partaidetza-espazio horretarako kidea eguneratzeko. + success: Kidea behar bezala eguneratu da + members_csv_imports: + create: + invalid: Arazo bat izan da CSV fitxategia irakurtzean. Egiaztatu jarraibideak bete dituzula. + success: CSV fitxategia ondo igo da, gonbidapen-mezu elektroniko bat bidaltzen ari gara parte-hartzaileei. Horrek denbora pixka bat iraun dezake. + new: + csv_upload: + title: Igo zure CSV fitxategia + destroy: + button: Kide guztiak ezabatu + confirm: Ziur zaude kide guztiak ezabatu nahi dituzula? Ekintza hori ezin da desegin, ezingo dituzu berreskuratu. + empty: Ez duzu kiderik. + explanation: '%{count} kide dituzu.' + title: Kideak ezabatu + example_file: 'Adibide-fitxategia:' + explanation: 'Igo zure CSV fitxategia. Bi zutabe izan behar ditu, fitxategiko lehen zutabean posta elektronikoa eta parte-hartze gunera gehitu nahi dituzun parte-hartzaileen fitxategiaren azken zutabean izena jarrita, goibururik gabe. Ez erabili baliozko ez diren karaktereak erabiltzaile-izenean, hala nola, "sim"<>?%&^*#@()[]=+:;"{}\|`.' + explanation_example: | + john.doe@example.org%{csv_col_sep}John Doe + jane.doe@example.org%{csv_col_sep}Jane Doe + title: Kidek CSV fitxategi baten bidez + upload: Igo menu: admin_log: Administratzaile jarduera-erregistroa admins: Administratzaileak @@ -634,6 +679,8 @@ eu: reason: Arrazoia started_at: 'Hasiera-ordua:' user: Parte-hartzaileak + member: + name: Kidea newsletter: fields: created_at: Noiz sortua @@ -642,8 +689,6 @@ eu: sent_to: Hona bidalia subject: Gaia name: Buletina - participatory_space_private_user: - name: Partaidetza-espazio pribatuko parte-hartzailea scope: fields: name: Izena @@ -918,53 +963,6 @@ eu: form: add: Gehitu baimendutako helbideen zerrendara title: Baimendutako kanpoko domeinuen zerrenda - participatory_space_private_users: - create: - error: Arazo bat egon da parte-hartzaile pribatu bat gehitzean partaidetza-espazio honetarako. - success: Zuzen sortu da sarbide pribatua partaidetza-espazioko partaideentzat. - destroy: - error: Arazo bat egon da parte-hartzaile pribatu bat ezabatzean partaidetza-espazio honetarako. - success: Behar bezala ezabatua parte-hartzailearen sarbidea partaidetza-espazio pribaturako. - edit: - title: Editatu espazio parte-hartzailea parte-hartzaile pribatua. - update: Eguneratu - index: - import_via_csv: Inportatu CSV bidetik - publish_all: Argitaratu guztiak - title: Partaidetza-espazio pribatuko parte-hartzailea - unpublish_all: Desargitaratu guztiak - new: - create: Sortu - title: Partaidetza-espazio pribatuko parte-hartzaile berria. - publish_all: - error: Arazo bat egon da parte-hartzaile pribatu guztiak argitaratzean partaidetza-espazio honetarako. - success: Parte-hartzaile pribatu guztiak zuzen argitaratu dira partaidetza-espazio honetarako - unpublish_all: - error: Arazo bat egon da parte-hartzaile pribatu guztiak desargiratzean partaidetza-espazio honetarako. - success: Parte-hartzaile pribatu guztiak zuzen desargitaratu dira partaidetza-espazio honetarako - update: - error: Arazo bat egon da parte-hartzaile pribatua partaidetza-espazio horretarako eguneratzean. - success: Partaidetza-espazioa, parte-hartzaile pribatua, arrakastaz eguneratua - participatory_space_private_users_csv_imports: - create: - invalid: Arazo bat egon da CSV fitxategia irakurtzean. Mesedez, ziurtatu jarraibideei kasu egin diezula. - success: CSV fitxategia zuzen igo da, eta parte-hartzaileei gonbidapen-mezu elektroniko bat bidaltzen ari gara. Prozesu horrek denbora behar du. - new: - csv_upload: - title: Igo zure CSV fitxategia - destroy: - button: Ezabatu parte-hartzaile pribatu guztiak - confirm: Ziur zaude parte-hartzaile pribatu guztiak ezabatu nahi dituzula? Ekintza hau ezin da desegin, ezin izango dituzu berreskuratu. - empty: Ez duzu parte-hartzaile pribaturik. - explanation: '%{count} parte-hartzaile pribatu dituzu.' - title: Ezabatu parte-hartzaile pribatuak - example_file: 'Fitxategi eredua:' - explanation: 'Igo zure CSV fitxategia. Bi zutabe izan behar ditu. Lehenengo zutabean posta elektronikoa jarri behar da, eta azken zutabean partaidetza-prozesura gehitu nahi dituzun parte-hartzaileen izena, goibururik gabe. Ez erabili holako karaktererik erabiltzaileen izenetan: `<>?%&^*#@()[]=+:;"{}\|`.' - explanation_example: | - john.doe@example.org%{csv_col_sep}John Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - title: Inportatu parte-hartzaile pribatuak CSV bidez - upload: Igo reminders: create: error: Arazo bat egon da ohartarazpenak sortzean. diff --git a/decidim-admin/config/locales/fi-plain.yml b/decidim-admin/config/locales/fi-plain.yml index 0441ead0dd5af..edd11acd16219 100644 --- a/decidim-admin/config/locales/fi-plain.yml +++ b/decidim-admin/config/locales/fi-plain.yml @@ -33,6 +33,11 @@ fi-pl: help_section: content: Sisältö id: ID + member: + email: Sähköpostiosoite + name: Nimi + member_csv_import: + file: Tiedosto newsletter: body: Runko send_to_all_users: Lähetä kaikille osallistujille @@ -91,11 +96,6 @@ fi-pl: welcome_notification_body: Tervetuloilmoituksen runko welcome_notification_subject: Tervetuloilmoituksen otsikko youtube_handler: YouTube-käsittelijä - participatory_space_private_user: - email: Sähköpostiosoite - name: Nimi - participatory_space_private_user_csv_import: - file: Tiedosto scope: code: Koodi name: Nimi @@ -125,10 +125,17 @@ fi-pl: show_in_footer: Näytä alatunnisteessa title: Otsikko weight: Järjestysnumero + taxonomy: + item_name: Asian nimi + parent_id: Vanhempi user_group_csv_verification: file: Tiedosto errors: models: + member_csv_import: + attributes: + file: + malformed: Virheellinen tuontitiedosto, lue ohjeet huolellisesti ja varmista, että tiedosto on UTF-8 muodossa. newsletter: attributes: base: @@ -137,10 +144,6 @@ fi-pl: attributes: official_img_footer: allowed_file_content_types: Virheellinen kuvatiedosto - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Virheellinen tuontitiedosto, lue ohjeet huolellisesti ja varmista, että tiedosto on UTF-8 muodossa. user_group_csv_verification: attributes: file: @@ -188,12 +191,12 @@ fi-pl: export: Vie export-selection: Vie valitut import: Tuo + member: + new: Uusi jäsen menu_hidden: Piilota valikosta moderate: Moderointien hallinta newsletter: new: Uusi uutiskirje - participatory_space_private_user: - new: Uusi osallistumistilan yksityinen käyttäjä per_page: Per sivu permissions: Käyttöoikeuksien hallinta restore: Palauta @@ -415,6 +418,17 @@ fi-pl: values: 'false': 'Ei' 'true': 'Kyllä' + members: + user_invitation_accepted_at_not_null: + label: Kutsu hyväksytty + values: + 'false': Ei hyäksytty + 'true': Hyväksytty + user_invitation_sent_at_not_null: + label: Kutsu lähetetty + values: + 'false': Ei lähetetty + 'true': Lähetetty moderated_users: reports_reason_eq: label: Ilmoituksen syy @@ -430,17 +444,6 @@ fi-pl: values: 'false': Virallistettu 'true': Ei virallistettu - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Kutsu hyväksytty - values: - 'false': Ei hyäksytty - 'true': Hyväksytty - user_invitation_sent_at_not_null: - label: Kutsu lähetetty - values: - 'false': Ei lähetetty - 'true': Lähetetty private_space_eq: label: Yksityinen values: @@ -573,6 +576,53 @@ fi-pl: explanation: Hallituille käyttäjille voidaan myöntää normaalien käyttäjien oikeudet. Tämä tarkoittaa, että heille lähetetään kutsu hakemusprosessiin ja tämän jälkeen kyseisinä tileinä ei voi enää esiintyä. Kutsutulle käyttäjälle lähetetään sähköposti, jonka avulla hän voi hyväksyä kutsusi. new_managed_user_promotion: Uusi käyttäjätason korotus hallinnoidulle käyttäjälle promote: Korota oikeudet + members: + create: + error: Jäsenen lisääminen tähän osallistumistilaan epäonnistui. + success: Jäsenen pääsyoikeuden lisäys onnistui. + destroy: + error: Osallistumistilan jäsenen poistaminen epäonnistui. + success: Jäsenen pääsyoikeuden poistaminen onnistui. + edit: + title: Muokkaa jäsentä + update: Päivitä + index: + import_via_csv: Tuo CSV-tiedostosta + publish_all: Julkaise kaikki + title: Jäsen + unpublish_all: Lopeta kaikkien julkaisu + new: + create: Luo + title: Uusi jäsen + publish_all: + error: Osallistumistilan jäsenten julkaiseminen epäonnistui. + success: Osallistumistilan jäsenten julkaiseminen onnistui + unpublish_all: + error: Osallistumistilan jäsenten julkaisun peruminen epäonnistui. + success: Osallistumistilan jäsenten julkaisun peruminen onnistui. + update: + error: Osallistumistilan jäsenen päivittäminen epäonnistui. + success: Osallistumistilan jäsenen päivittäminen onnistui. + members_csv_imports: + create: + invalid: CSV-tiedoston lukeminen epäonnistui. Tarkasta, että olet seurannut esitettyjä ohjeita. + success: CSV-tiedoston lataus onnistui. Lähetämme kutsusähköpostin osallistujille. Tämä voi kestää hetken. + new: + csv_upload: + title: Lataa CSV-tiedosto + destroy: + button: Poista kaikki jäsenet + confirm: Haluatko varmasti poistaa kaikki jäsenet? Tätä toimintoa ei voi perua ja poistettuja jäseniä ei voi palauttaa. + empty: Jäseniä ei ole määritetty. + explanation: '%{count} jäsentä.' + title: Poista jäsenet + example_file: 'Esimerkkitiedosto:' + explanation: 'Lataa CSV-tiedosto. Tiedostossa on oltava kaksi saraketta ilman otsikkoriviä. Ensimmäiseen sarakkeeseen määritetään käyttäjän sähköpostiosoite ja toiseen sarakkeeseen käyttäjän nimi, alkaen ensimmäiseltä riviltä. Jokaisesta rivistä luodaan uusi osallistumistilan käyttäjä. Vältä erikoismerkkejä nimessä, kuten `<>?%&^*#@()[]=+:;"{}\|`.' + explanation_example: | + matti.meikalainen@esimerkki.fi%{csv_col_sep}Matti Meikäläinen + minna.meikalainen@esimerkki.fi%{csv_col_sep}Minna Meikäläinen + title: Tuo jäseniä CSV-tiedostosta + upload: Lataa menu: admin_log: Hallintatoimintojen tapahtumaloki admins: Ylläpitäjät @@ -631,6 +681,8 @@ fi-pl: reason: Syy started_at: Aloitettu user: Käyttäjä + member: + name: Jäsen newsletter: fields: created_at: Luonnin ajankohta @@ -639,8 +691,6 @@ fi-pl: sent_to: Vastaanottajat subject: Otsikko name: Uutiskirje - participatory_space_private_user: - name: Osallisuustilan yksityinen käyttäjä scope: fields: name: Nimi @@ -915,53 +965,6 @@ fi-pl: form: add: Lisää sallittujen listalle title: Sallitut ulkoiset verkko-osoitteet - participatory_space_private_users: - create: - error: Yksityisen käyttäjän lisäämisessä tähän osallisuustilaan tapahtui virhe. - success: Osallisuustilaa koskeva yksityisen käyttäjän käyttöoikeus lisätty onnistuneesti. - destroy: - error: Tapahtui virhe poistettaessa osallisuustilan yksityistä käyttäjää. - success: Osallisuustilaa koskeva yksityisen käyttäjän käyttöoikeus tuhottu onnistuneesti. - edit: - title: Muokkaa osallistumistilan yksityistä käyttäjää. - update: Päivitä - index: - import_via_csv: Tuo CSV-tiedostosta - publish_all: Julkaise kaikki - title: Osallisuustilan yksityinen käyttäjä - unpublish_all: Lopeta kaikkien julkaisu - new: - create: Luo - title: Uusi osallisuustilan yksityinen käyttäjä. - publish_all: - error: Osallistumistilan yksityisten käyttäjien julkaiseminen epäonnistui. - success: Osallistumistilan yksityisten käyttäjien julkaiseminen onnistui - unpublish_all: - error: Osallistumistilan yksityisten käyttäjien julkaisun peruminen epäonnistui. - success: Osallistumistilan yksityisten käyttäjien julkaisun peruminen onnistui. - update: - error: Osallistumistilan yksityisen käyttäjän päivittäminen epäonnistui. - success: Osallistumistilan yksityisen käyttäjän päivittäminen onnistui. - participatory_space_private_users_csv_imports: - create: - invalid: CSV-tiedoston lukeminen epäonnistui. Tarkasta, että olet seurannut esitettyjä ohjeita. - success: CSV-tiedosto ladattu onnistuneesti. Lähetämme kutsusähköpostin osallistujille. Tämä voi kestää hetken. - new: - csv_upload: - title: Lataa CSV-tiedosto - destroy: - button: Poista kaikki yksityiset osallistujat - confirm: Oletko varma, että haluat poistaa kaikki yksityiset osallistujat? Tätä toimintoa ei voi peruuttaa ja poistettuja osallistujia ei voi palauttaa. - empty: Tässä osallistumistilassa ei ole yksityisiä osallistujia. - explanation: Tässä osallistumistilassa on %{count} yksityistä osallistujaa. - title: Poista kaikki yksityiset osallistujat - example_file: 'Esimerkkitiedosto:' - explanation: 'Lataa CSV-tiedosto. Tiedostossa on oltava kaksi saraketta ilman otsikkoriviä. Ensimmäiseen sarakkeeseen asetetaan käyttäjän sähköpostiosoite ja toiseen sarakkeeseen käyttäjän nimi, alkaen ensimmäiseltä riviltä. Jokaisesta rivistä luodaan uusi osallistumistilan käyttäjä. Vältä erikoismerkkejä nimessä, kuten `<>?%&^*#@()[]=+:;"{}\|`.' - explanation_example: | - matti.meikalainen@esimerkki.fi%{csv_col_sep}Matti Meikäläinen - minna.meikalainen@esimerkki.fi%{csv_col_sep}Minna Meikäläinen - title: Tuo yksityisiä osallistujia CSV-tiedostosta - upload: Lataa reminders: create: error: Muistutusten luonti epäonnistui. diff --git a/decidim-admin/config/locales/fi.yml b/decidim-admin/config/locales/fi.yml index d9f5cae7357b9..bf8594d49d631 100644 --- a/decidim-admin/config/locales/fi.yml +++ b/decidim-admin/config/locales/fi.yml @@ -33,6 +33,11 @@ fi: help_section: content: Sisältö id: ID + member: + email: Sähköpostiosoite + name: Nimi + member_csv_import: + file: Tiedosto newsletter: body: Runko send_to_all_users: Lähetä kaikille osallistujille @@ -91,11 +96,6 @@ fi: welcome_notification_body: Tervetuloilmoituksen runko welcome_notification_subject: Tervetuloilmoituksen otsikko youtube_handler: YouTube-käsittelijä - participatory_space_private_user: - email: Sähköpostiosoite - name: Nimi - participatory_space_private_user_csv_import: - file: Tiedosto scope: code: Koodi name: Nimi @@ -125,10 +125,17 @@ fi: show_in_footer: Näytä alatunnisteessa title: Otsikko weight: Järjestysnumero + taxonomy: + item_name: Asian nimi + parent_id: Vanhempi user_group_csv_verification: file: Tiedosto errors: models: + member_csv_import: + attributes: + file: + malformed: Virheellinen tuontitiedosto, lue ohjeet huolellisesti ja varmista, että tiedosto on UTF-8 muodossa. newsletter: attributes: base: @@ -137,10 +144,6 @@ fi: attributes: official_img_footer: allowed_file_content_types: Virheellinen kuvatiedosto - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Virheellinen tuontitiedosto, lue ohjeet huolellisesti ja varmista, että tiedosto on UTF-8 muodossa. user_group_csv_verification: attributes: file: @@ -188,12 +191,12 @@ fi: export: Vie export-selection: Vie valitut import: Tuo + member: + new: Uusi jäsen menu_hidden: Piilota valikosta moderate: Moderointien hallinta newsletter: new: Uusi uutiskirje - participatory_space_private_user: - new: Uusi osallistumistilan yksityinen käyttäjä per_page: Per sivu permissions: Käyttöoikeuksien hallinta restore: Palauta @@ -415,6 +418,17 @@ fi: values: 'false': 'Ei' 'true': 'Kyllä' + members: + user_invitation_accepted_at_not_null: + label: Kutsu hyväksytty + values: + 'false': Ei hyäksytty + 'true': Hyväksytty + user_invitation_sent_at_not_null: + label: Kutsu lähetetty + values: + 'false': Ei lähetetty + 'true': Lähetetty moderated_users: reports_reason_eq: label: Ilmoituksen syy @@ -430,17 +444,6 @@ fi: values: 'false': Virallistettu 'true': Ei virallistettu - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Kutsu hyväksytty - values: - 'false': Ei hyäksytty - 'true': Hyväksytty - user_invitation_sent_at_not_null: - label: Kutsu lähetetty - values: - 'false': Ei lähetetty - 'true': Lähetetty private_space_eq: label: Yksityinen values: @@ -573,6 +576,53 @@ fi: explanation: Hallituille käyttäjille voidaan myöntää normaalien käyttäjien oikeudet. Tämä tarkoittaa, että heille lähetetään rekisteröitymiskutsu alustalle ja rekisteröitymisen jälkeen kyseisinä käyttäjinä ei voi enää esiintyä. Kutsutulle käyttäjälle lähetetään sähköposti, jonka avulla he voivat hyväksyä kutsusi. new_managed_user_promotion: Uusi käyttäjätason korotus hallinnoidulle käyttäjälle promote: Korota oikeudet + members: + create: + error: Jäsenen lisääminen tähän osallistumistilaan epäonnistui. + success: Jäsenen pääsyoikeuden lisäys onnistui. + destroy: + error: Osallistumistilan jäsenen poistaminen epäonnistui. + success: Jäsenen pääsyoikeuden poistaminen onnistui. + edit: + title: Muokkaa jäsentä + update: Päivitä + index: + import_via_csv: Tuo CSV-tiedostosta + publish_all: Julkaise kaikki + title: Jäsen + unpublish_all: Lopeta kaikkien julkaisu + new: + create: Luo + title: Uusi jäsen + publish_all: + error: Osallistumistilan jäsenten julkaiseminen epäonnistui. + success: Osallistumistilan jäsenten julkaiseminen onnistui + unpublish_all: + error: Osallistumistilan jäsenten julkaisun peruminen epäonnistui. + success: Osallistumistilan jäsenten julkaisun peruminen onnistui. + update: + error: Osallistumistilan jäsenen päivittäminen epäonnistui. + success: Osallistumistilan jäsenen päivittäminen onnistui. + members_csv_imports: + create: + invalid: CSV-tiedoston lukeminen epäonnistui. Tarkasta, että olet seurannut esitettyjä ohjeita. + success: CSV-tiedoston lataus onnistui. Lähetämme kutsusähköpostin osallistujille. Tämä voi kestää hetken. + new: + csv_upload: + title: Lataa CSV-tiedosto + destroy: + button: Poista kaikki jäsenet + confirm: Haluatko varmasti poistaa kaikki jäsenet? Tätä toimintoa ei voi perua ja poistettuja jäseniä ei voi palauttaa. + empty: Jäseniä ei ole määritetty. + explanation: '%{count} jäsentä.' + title: Poista jäsenet + example_file: 'Esimerkkitiedosto:' + explanation: 'Lataa CSV-tiedosto. Tiedostossa on oltava kaksi saraketta ilman otsikkoriviä. Ensimmäiseen sarakkeeseen määritetään käyttäjän sähköpostiosoite ja toiseen sarakkeeseen käyttäjän nimi, alkaen ensimmäiseltä riviltä. Jokaisesta rivistä luodaan uusi osallistumistilan käyttäjä. Vältä erikoismerkkejä nimessä, kuten `<>?%&^*#@()[]=+:;"{}\|`.' + explanation_example: | + matti.meikalainen@esimerkki.fi%{csv_col_sep}Matti Meikäläinen + minna.meikalainen@esimerkki.fi%{csv_col_sep}Minna Meikäläinen + title: Tuo jäseniä CSV-tiedostosta + upload: Lataa menu: admin_log: Hallintatoimintojen tapahtumaloki admins: Ylläpitäjät @@ -631,6 +681,8 @@ fi: reason: Syy started_at: Aloitettu user: Käyttäjä + member: + name: Jäsen newsletter: fields: created_at: Luonnin ajankohta @@ -639,8 +691,6 @@ fi: sent_to: Vastaanottajat subject: Otsikko name: Uutiskirje - participatory_space_private_user: - name: Osallistumistilan yksityinen käyttäjä scope: fields: name: Nimi @@ -915,53 +965,6 @@ fi: form: add: Lisää sallittujen listalle title: Sallitut ulkoiset verkko-osoitteet - participatory_space_private_users: - create: - error: Yksityisen käyttäjän lisääminen tähän osallistumistilaan epäonnistui. - success: Osallistumistilaa koskeva yksityisen käyttäjän käyttöoikeuden lisäys onnistui. - destroy: - error: Osallistumistilan yksityisen käyttäjän poistaminen epäonnistui. - success: Osallistumistilaa koskeva yksityisen käyttäjän käyttöoikeuden poistaminen onnistui. - edit: - title: Muokkaa osallistumistilan yksityistä käyttäjää. - update: Päivitä - index: - import_via_csv: Tuo CSV-tiedostosta - publish_all: Julkaise kaikki - title: Osallistumistilan yksityinen käyttäjä - unpublish_all: Lopeta kaikkien julkaisu - new: - create: Luo - title: Uusi osallistumistilan yksityinen käyttäjä. - publish_all: - error: Osallistumistilan yksityisten käyttäjien julkaiseminen epäonnistui. - success: Osallistumistilan yksityisten käyttäjien julkaiseminen onnistui - unpublish_all: - error: Osallistumistilan yksityisten käyttäjien julkaisun peruminen epäonnistui. - success: Osallistumistilan yksityisten käyttäjien julkaisun peruminen onnistui. - update: - error: Osallistumistilan yksityisen käyttäjän päivittäminen epäonnistui. - success: Osallistumistilan yksityisen käyttäjän päivittäminen onnistui. - participatory_space_private_users_csv_imports: - create: - invalid: CSV-tiedoston lukeminen epäonnistui. Tarkasta, että olet seurannut esitettyjä ohjeita. - success: CSV-tiedoston lataus onnistui. Lähetämme kutsusähköpostin osallistujille. Tämä voi kestää hetken. - new: - csv_upload: - title: Lataa CSV-tiedosto - destroy: - button: Poista kaikki yksityiset osallistujat - confirm: Oletko varma, että haluat poistaa kaikki yksityiset osallistujat? Tätä toimintoa ei voi perua ja poistettuja osallistujia ei voi palauttaa. - empty: Tässä osallistumistilassa ei ole yksityisiä osallistujia. - explanation: Tässä osallistumistilassa on %{count} yksityistä osallistujaa. - title: Poista kaikki yksityiset osallistujat - example_file: 'Esimerkkitiedosto:' - explanation: 'Lataa CSV-tiedosto. Tiedostossa on oltava kaksi saraketta ilman otsikkoriviä. Ensimmäiseen sarakkeeseen asetetaan käyttäjän sähköpostiosoite ja toiseen sarakkeeseen käyttäjän nimi, alkaen ensimmäiseltä riviltä. Jokaisesta rivistä luodaan uusi osallistumistilan käyttäjä. Vältä erikoismerkkejä nimessä, kuten `<>?%&^*#@()[]=+:;"{}\|`.' - explanation_example: | - matti.meikalainen@esimerkki.fi%{csv_col_sep}Matti Meikäläinen - minna.meikalainen@esimerkki.fi%{csv_col_sep}Minna Meikäläinen - title: Tuo yksityisiä osallistujia CSV-tiedostosta - upload: Lataa reminders: create: error: Muistutusten luonti epäonnistui. diff --git a/decidim-admin/config/locales/fr-CA.yml b/decidim-admin/config/locales/fr-CA.yml index f54972134ee6a..e73a751c9ebf1 100644 --- a/decidim-admin/config/locales/fr-CA.yml +++ b/decidim-admin/config/locales/fr-CA.yml @@ -33,6 +33,11 @@ fr-CA: help_section: content: Contenu id: ID + member: + email: Email + name: Nom + member_csv_import: + file: Fichier newsletter: body: Corps de texte send_to_all_users: Envoyer à tous les participants @@ -90,11 +95,6 @@ fr-CA: welcome_notification_body: Corps du mail de bienvenue welcome_notification_subject: Objet du mail de bienvenue youtube_handler: Gestionnaire YouTube - participatory_space_private_user: - email: Email - name: Nom - participatory_space_private_user_csv_import: - file: Fichier scope: code: Code name: Titre @@ -131,6 +131,10 @@ fr-CA: file: Fichier errors: models: + member_csv_import: + attributes: + file: + malformed: Fichier d'importation mal formé, veuillez lire attentivement les instructions et assurez-vous que le fichier est encodé en UTF-8. newsletter: attributes: base: @@ -139,10 +143,6 @@ fr-CA: attributes: official_img_footer: allowed_file_content_types: Fichier image invalide - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Fichier d'importation mal formé, veuillez lire attentivement les instructions et assurez-vous que le fichier est encodé en UTF-8. user_group_csv_verification: attributes: file: @@ -190,12 +190,12 @@ fr-CA: export: Exporter export-selection: Exporter la sélection import: Importer + member: + new: Nouveau membre menu_hidden: Masquer dans le menu moderate: Gérer les modérations newsletter: new: Nouvelle newsletter - participatory_space_private_user: - new: Nouvel utilisateur privé de l'espace participatif per_page: Par page permissions: Gérer les permissions restore: Restaurer @@ -416,6 +416,17 @@ fr-CA: values: 'false': 'Non' 'true': 'Oui' + members: + user_invitation_accepted_at_not_null: + label: Invitation acceptée + values: + 'false': Non acceptée + 'true': Acceptée + user_invitation_sent_at_not_null: + label: Invitation envoyée + values: + 'false': Non envoyée + 'true': Envoyée moderated_users: reports_reason_eq: label: Raison du signalement @@ -431,17 +442,6 @@ fr-CA: values: 'false': Validé 'true': Non validé - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Invitation acceptée - values: - 'false': Non acceptée - 'true': Acceptée - user_invitation_sent_at_not_null: - label: Invitation envoyée - values: - 'false': Non envoyée - 'true': Envoyée private_space_eq: label: Privé values: @@ -574,6 +574,53 @@ fr-CA: explanation: Les utilisateurs représentés peuvent être promu utilisateurs standard. Cela signifie qu'ils seront invités à s'inscrire sur l'application et vous ne pourrez plus agir à leur place. L'utilisateur recevra un email pour accepter votre invitation. new_managed_user_promotion: Promouvoir un utilisateur représenté en utilisateur standard promote: Promouvoir + members: + create: + error: Une erreur s'est produite lors de l'ajout d'un membre pour cet espace participatif. + success: Accès membre créé avec succès. + destroy: + error: Un problème est survenu lors de la suppression d'un membre pour cet espace participatif. + success: Accès membre supprimé avec succès. + edit: + title: Modifier le membre + update: Mettre à jour + index: + import_via_csv: Importer via CSV + publish_all: Tout publier + title: Membre + unpublish_all: Tout dépublier + new: + create: Créer + title: Nouveau membre + publish_all: + error: Un problème est survenu lors de la publication de tous les membres de cet espace participatif. + success: Tous les membres ont été publiés avec succès pour cet espace participatif + unpublish_all: + error: Une erreur s'est produite lors de la dépublication de tous les membres de cet espace participatif. + success: Tous les membres de cet espace participatif ont été dépubliés avec succès + update: + error: Un problème est survenu lors de la mise à jour du membre pour cet espace participatif. + success: Membre mis à jour avec succès + members_csv_imports: + create: + invalid: Un problème est survenu lors de la lecture du fichier CSV. Veuillez vous assurer que vous avez suivi les instructions. + success: Fichier CSV transféré avec succès, nous envoyons un courriel d'invitation aux participants. Cela peut prendre un certain temps. + new: + csv_upload: + title: Téléchargez votre fichier CSV + destroy: + button: Supprimer tous les membres + confirm: Êtes-vous sûr(e) de vouloir supprimer tous les membres ? Cette action ne peut pas être annulée, vous ne pourrez pas les récupérer. + empty: Vous n'avez aucun membre. + explanation: Vus avez %{count} membres. + title: Supprimer les membres + example_file: 'Fichier d''exemple :' + explanation: 'Téléchargez votre fichier CSV. Il doit comporter deux colonnes, sans en-têtes, avec l''email dans la première colonne et le nom dans la dernière colonne du fichier des utilisateurs que vous voulez ajouter à l''espace participatif. Évitez d''utiliser des caractères invalides comme `<>?%&^*#@()[]=+:;"{}\|` dans le nom d''utilisateur.' + explanation_example: | + john.doe@example.org%{csv_col_sep}John Doe + jane.doe@example.org%{csv_col_sep}Jane Doe + title: Importer des membres via CSV + upload: Charger menu: admin_log: Journal d'activité personnel admins: Administrateurs @@ -632,6 +679,8 @@ fr-CA: reason: Raison started_at: Commencé le user: Utilisateur + member: + name: Membre newsletter: fields: created_at: Créée le @@ -640,8 +689,6 @@ fr-CA: sent_to: Envoyé à subject: Objet name: Bulletin d'information (newsletter) - participatory_space_private_user: - name: Utilisateur de l'espace participatif scope: fields: name: Titre @@ -916,53 +963,6 @@ fr-CA: form: add: Ajouter à la liste autorisée title: Liste des domaines externes autorisés - participatory_space_private_users: - create: - error: Une erreur s'est produite lors de l'ajout d'un utilisateur pour cet espace participatif. - success: L'accès utilisateur à l'espace participatif privé a été créé avec succès. - destroy: - error: Une erreur s'est produite lors de la suppression d'un utilisateur privé pour cet espace participatif. - success: L'accès utilisateur à l'espace participatif privé a été supprimé avec succès. - edit: - title: Modifier le participant privé de l'espace participatif. - update: Mettre à jour - index: - import_via_csv: Importer via csv - publish_all: Tout publier - title: Utilisateur privé de l'espace participatif - unpublish_all: Tout dépublier - new: - create: Créer - title: Nouvel utilisateur privé de l'espace participatif. - publish_all: - error: Une erreur s'est produite lors de la publication de tous les participants privés pour cet espace participatif. - success: Tous les participants privés ont été publiés avec succès pour cet espace participatif - unpublish_all: - error: Une erreur s'est produite lors de la dépublication de tous les participants privés pour cet espace participatif. - success: Tous les participants privés ont été dépubliés avec succès pour cet espace participatif - update: - error: Une erreur s'est produite lors de la mise à jour du participant privé pour cet espace participatif. - success: Participant privé de l'espace participatif mis à jour avec succès - participatory_space_private_users_csv_imports: - create: - invalid: Un problème est survenu lors de la lecture du fichier CSV. Veuillez vous assurer que vous avez suivi les instructions. - success: Le fichier CSV a été téléchargé avec succès. Une invitation par courriel sera envoyée sous peu à chaque participant. Cela peut prendre quelques instants. - new: - csv_upload: - title: Téléchargez votre fichier CSV - destroy: - button: Supprimer tous les utilisateurs privés - confirm: Êtes-vous sûr de vouloir supprimer tous les utilisateurs privés ? Cette action est irréversible, vous ne pourrez pas les récupérer. - empty: Vous n'avez aucun utilisateur privé. - explanation: Vous avez %{count} utilisateurs privés. - title: Supprimer les utilisateurs privés - example_file: 'Fichier d''exemple :' - explanation: 'Téléchargez votre fichier CSV. Il doit avoir deux colonnes avec les emails dans la première colonne, et dans la seconde colonne les noms des utilisateurs que vous souhaitez ajouter à l''espace participatif, sans en-tête. Évitez les caractères spéciaux comme `<>?%&^*#@()[]=+:;"{}\|` dans les noms des utilisateurs.' - explanation_example: | - john.doe@example.org%{csv_col_sep}Jean Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - title: Importer des participants privés via CSV - upload: Télécharger reminders: create: error: Il y a eu un problème lors de la création des rappels. diff --git a/decidim-admin/config/locales/fr.yml b/decidim-admin/config/locales/fr.yml index 61845fa999563..b5f72f245b03f 100644 --- a/decidim-admin/config/locales/fr.yml +++ b/decidim-admin/config/locales/fr.yml @@ -33,6 +33,11 @@ fr: help_section: content: Contenu id: ID + member: + email: Email + name: Nom + member_csv_import: + file: Fichier newsletter: body: Corps de texte send_to_all_users: Envoyer à tous les participants @@ -90,11 +95,6 @@ fr: welcome_notification_body: Corps du mail de bienvenue welcome_notification_subject: Objet du mail de bienvenue youtube_handler: Gestionnaire YouTube - participatory_space_private_user: - email: Email - name: Nom - participatory_space_private_user_csv_import: - file: Fichier scope: code: Code name: Titre @@ -131,6 +131,10 @@ fr: file: Fichier errors: models: + member_csv_import: + attributes: + file: + malformed: Fichier d'importation mal formé, veuillez lire attentivement les instructions et assurez-vous que le fichier est encodé en UTF-8. newsletter: attributes: base: @@ -139,10 +143,6 @@ fr: attributes: official_img_footer: allowed_file_content_types: Fichier image invalide - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Fichier d'importation mal formé, veuillez lire attentivement les instructions et assurez-vous que le fichier est encodé en UTF-8. user_group_csv_verification: attributes: file: @@ -190,12 +190,12 @@ fr: export: Exporter export-selection: Exporter la sélection import: Importer + member: + new: Nouveau membre menu_hidden: Masquer dans le menu moderate: Gérer les modérations newsletter: new: Nouvelle newsletter - participatory_space_private_user: - new: Nouvel utilisateur privé de l'espace participatif per_page: Par page permissions: Gérer les permissions restore: Restaurer @@ -416,6 +416,17 @@ fr: values: 'false': 'Non' 'true': 'Oui' + members: + user_invitation_accepted_at_not_null: + label: Invitation acceptée + values: + 'false': Non acceptée + 'true': Acceptée + user_invitation_sent_at_not_null: + label: Invitation envoyée + values: + 'false': Non envoyée + 'true': Envoyée moderated_users: reports_reason_eq: label: Raison du signalement @@ -431,17 +442,6 @@ fr: values: 'false': Validé 'true': Non validé - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Invitation acceptée - values: - 'false': Non acceptée - 'true': Acceptée - user_invitation_sent_at_not_null: - label: Invitation envoyée - values: - 'false': Non envoyée - 'true': Envoyée private_space_eq: label: Privé values: @@ -574,6 +574,53 @@ fr: explanation: Les utilisateurs représentés peuvent être promu utilisateurs standard. Cela signifie qu'ils seront invités à s'inscrire sur l'application et vous ne pourrez plus agir à leur place. L'utilisateur recevra un email pour accepter votre invitation. new_managed_user_promotion: Promouvoir un utilisateur représenté en utilisateur standard promote: Promouvoir + members: + create: + error: Une erreur s'est produite lors de l'ajout d'un membre pour cet espace participatif. + success: Accès membre créé avec succès. + destroy: + error: Un problème est survenu lors de la suppression d'un membre pour cet espace participatif. + success: Accès membre supprimé avec succès. + edit: + title: Modifier le membre + update: Mettre à jour + index: + import_via_csv: Importer via CSV + publish_all: Tout publier + title: Membre + unpublish_all: Tout dépublier + new: + create: Créer + title: Nouveau membre + publish_all: + error: Un problème est survenu lors de la publication de tous les membres de cet espace participatif. + success: Tous les membres ont été publiés avec succès pour cet espace participatif + unpublish_all: + error: Une erreur s'est produite lors de la dépublication de tous les membres de cet espace participatif. + success: Tous les membres de cet espace participatif ont été dépubliés avec succès + update: + error: Un problème est survenu lors de la mise à jour du membre pour cet espace participatif. + success: Membre mis à jour avec succès + members_csv_imports: + create: + invalid: Un problème est survenu lors de la lecture du fichier CSV. Veuillez vous assurer que vous avez suivi les instructions. + success: Fichier CSV transféré avec succès, nous envoyons un courriel d'invitation aux participants. Cela peut prendre un certain temps. + new: + csv_upload: + title: Téléchargez votre fichier CSV + destroy: + button: Supprimer tous les membres + confirm: Êtes-vous sûr(e) de vouloir supprimer tous les membres ? Cette action ne peut pas être annulée, vous ne pourrez pas les récupérer. + empty: Vous n'avez aucun membre. + explanation: Vus avez %{count} membres. + title: Supprimer les membres + example_file: 'Fichier d''exemple :' + explanation: 'Téléchargez votre fichier CSV. Il doit comporter deux colonnes, sans en-têtes, avec l''email dans la première colonne et le nom dans la dernière colonne du fichier des utilisateurs que vous voulez ajouter à l''espace participatif. Évitez d''utiliser des caractères invalides comme `<>?%&^*#@()[]=+:;"{}\|` dans le nom d''utilisateur.' + explanation_example: | + john.doe@example.org%{csv_col_sep}John Doe + jane.doe@example.org%{csv_col_sep}Jane Doe + title: Importer des membres via CSV + upload: Charger menu: admin_log: Journal d'activité personnel admins: Administrateurs @@ -632,6 +679,8 @@ fr: reason: Raison started_at: Commencé le user: Utilisateur + member: + name: Membre newsletter: fields: created_at: Créée le @@ -640,8 +689,6 @@ fr: sent_to: Envoyé à subject: Objet name: Bulletin d'information (newsletter) - participatory_space_private_user: - name: Utilisateur de l'espace participatif scope: fields: name: Titre @@ -916,53 +963,6 @@ fr: form: add: Ajouter à la liste autorisée title: Liste des domaines externes autorisés - participatory_space_private_users: - create: - error: Une erreur s'est produite lors de l'ajout d'un utilisateur pour cet espace participatif. - success: L'accès utilisateur à l'espace participatif privé a été créé avec succès. - destroy: - error: Une erreur s'est produite lors de la suppression d'un utilisateur privé pour cet espace participatif. - success: L'accès utilisateur à l'espace participatif privé a été supprimé avec succès. - edit: - title: Modifier le participant privé de l'espace participatif. - update: Mettre à jour - index: - import_via_csv: Importer via csv - publish_all: Tout publier - title: Utilisateur privé de l'espace participatif - unpublish_all: Tout dépublier - new: - create: Créer - title: Nouvel utilisateur privé de l'espace participatif. - publish_all: - error: Une erreur s'est produite lors de la publication de tous les participants privés pour cet espace participatif. - success: Tous les participants privés ont été publiés avec succès pour cet espace participatif - unpublish_all: - error: Une erreur s'est produite lors de la dépublication de tous les participants privés pour cet espace participatif. - success: Tous les participants privés ont été dépubliés avec succès pour cet espace participatif - update: - error: Une erreur s'est produite lors de la mise à jour du participant privé pour cet espace participatif. - success: Participant privé de l'espace participatif mis à jour avec succès - participatory_space_private_users_csv_imports: - create: - invalid: Un problème est survenu lors de la lecture du fichier CSV. Veuillez vous assurer que vous avez suivi les instructions. - success: Le fichier CSV a été téléchargé avec succès. Une invitation par courriel sera envoyée sous peu à chaque participant. Cela peut prendre quelques instants. - new: - csv_upload: - title: Téléchargez votre fichier CSV - destroy: - button: Supprimer tous les utilisateurs privés - confirm: Êtes-vous sûr de vouloir supprimer tous les utilisateurs privés ? Cette action est irréversible, vous ne pourrez pas les récupérer. - empty: Vous n'avez aucun utilisateur privé. - explanation: Vous avez %{count} utilisateurs privés. - title: Supprimer les utilisateurs privés - example_file: 'Fichier d''exemple :' - explanation: 'Téléchargez votre fichier CSV. Il doit avoir deux colonnes avec les emails dans la première colonne, et dans la seconde colonne les noms des utilisateurs que vous souhaitez ajouter à l''espace participatif, sans en-tête. Évitez les caractères spéciaux comme `<>?%&^*#@()[]=+:;"{}\|` dans les noms des utilisateurs.' - explanation_example: | - john.doe@example.org%{csv_col_sep}Jean Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - title: Importer des participants privés via CSV - upload: Télécharger reminders: create: error: Il y a eu un problème lors de la création des rappels. diff --git a/decidim-admin/config/locales/ga-IE.yml b/decidim-admin/config/locales/ga-IE.yml index 9fb5d2f081afa..094e97afd281c 100644 --- a/decidim-admin/config/locales/ga-IE.yml +++ b/decidim-admin/config/locales/ga-IE.yml @@ -125,13 +125,6 @@ ga: label: Cineál officialized_at_null: label: Staid - participatory_space_private_users: - user_invitation_accepted_at_not_null: - values: - 'true': Glactha - user_invitation_sent_at_not_null: - values: - 'true': Seolta private_space_eq: label: Príobháideach values: @@ -280,12 +273,6 @@ ga: social_handlers: Sóisialta url: URL youtube: YouTube - participatory_space_private_users: - new: - create: Cruthaigh - participatory_space_private_users_csv_imports: - new: - upload: Uaslódáil resource_permissions: edit: submit: Deimhnigh diff --git a/decidim-admin/config/locales/gl.yml b/decidim-admin/config/locales/gl.yml index 6c6eae640f101..0bf5b472bccac 100644 --- a/decidim-admin/config/locales/gl.yml +++ b/decidim-admin/config/locales/gl.yml @@ -136,8 +136,6 @@ gl: import: Importar newsletter: new: Novo boletín - participatory_space_private_user: - new: Novo usuario privado do espazo participativo per_page: Por páxina share: Compartir user: @@ -295,17 +293,6 @@ gl: values: 'false': Oficializado 'true': Non oficializado - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Invitación aceptada - values: - 'false': Non aceptada - 'true': Aceptada - user_invitation_sent_at_not_null: - label: Invitación enviada - values: - 'false': Sen enviar - 'true': Enviada private_space_eq: label: Privado values: @@ -411,6 +398,10 @@ gl: explanation: Os usuarios xestionados poden ser promocionados a usuarios estándar. Isto significa que serán invitados á aplicación e non poderás suplantarlles de novo. O usuario invitado recibirá un correo electrónico para aceptar a túa invitación. new_managed_user_promotion: Nova promoción de usuarios xestionados promote: Promover + members_csv_imports: + new: + csv_upload: + title: Carga o teu ficheiro CSV menu: admin_log: Rexistro de actividade do administrador admins: Administradores @@ -465,8 +456,6 @@ gl: sent_to: Enviado a subject: Asunto name: Boletín informativo - participatory_space_private_user: - name: Espazo participativo usuario privado scope: fields: name: Nome @@ -627,24 +616,6 @@ gl: update: error: Produciuse un erro ao actualizar esta organización. success: A organización actualizouse con éxito. - participatory_space_private_users: - create: - error: Produciuse un erro engadindo un usuario privado a este espazo participativo. - success: O acceso ao usuario privado do espazo participativo creouse con éxito. - destroy: - error: Produciuse un erro ao eliminar un usuario privado deste espazo participativo. - success: O acceso ao usuario privado do espazo participativo foi destruído con éxito. - index: - import_via_csv: Importar a través de CSV - title: Espazo participativo usuario privado - new: - create: Crear - title: Novo usuario privado do espazo participativo. - participatory_space_private_users_csv_imports: - new: - csv_upload: - title: Carga o teu ficheiro CSV - upload: Subir reminders: create: error: Produciuse un problema ao crear os recordatorios. diff --git a/decidim-admin/config/locales/hu.yml b/decidim-admin/config/locales/hu.yml index a2c7d600bcda1..9ce2a0b95bb0d 100644 --- a/decidim-admin/config/locales/hu.yml +++ b/decidim-admin/config/locales/hu.yml @@ -89,9 +89,6 @@ hu: welcome_notification_body: Üdvözlő értesítés szövegtörzse welcome_notification_subject: Üdvözlő üzenet tárgya youtube_handler: YouTube kezelő - participatory_space_private_user: - email: Email - name: Név scope: code: Kód name: Név @@ -133,10 +130,6 @@ hu: attributes: official_img_footer: allowed_file_content_types: Érvénytelen képfájl - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Hibás importfájl, kérjük, olvassa el figyelmesen az utasításokat, és győződjön meg róla, hogy a fájl UTF-8 kódolású. user_group_csv_verification: attributes: file: @@ -175,8 +168,6 @@ hu: import: Import newsletter: new: Új hírlevél - participatory_space_private_user: - new: Új privát felhasználó a részvételi térben per_page: Oldalanként send_me_a_test_email: Teszt e-mail küldése share: Megosztás @@ -382,17 +373,6 @@ hu: values: 'false': Hivatalos 'true': Nem hivatalos - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Meghívás elfogadva - values: - 'false': Nincs elfogadva - 'true': Elfogadva - user_invitation_sent_at_not_null: - label: Meghívó kiküldve - values: - 'false': Nincs elküldve - 'true': Elküldve private_space_eq: label: Magán values: @@ -510,6 +490,10 @@ hu: explanation: A kezelt résztvevők sztenderd résztvevőkké alakíthatóak. Ez azt jelenti, hogy meghívást kapnak az alkalmazáshoz és nem lehet őket újra kezelni. Erről emailben kapnak értesítést. new_managed_user_promotion: Új résztvevő előléptetése promote: Előléptetés + members_csv_imports: + new: + csv_upload: + title: Töltse fel a CSV fájlját menu: admin_log: Admin tevékenységnapló admins: Adminok @@ -569,8 +553,6 @@ hu: sent_to: Címzett subject: Tárgy name: Hírlevél - participatory_space_private_user: - name: Részvételi tér privát felhasználó scope: fields: name: Név @@ -764,38 +746,6 @@ hu: form: add: Hozzáadás az engedélyezett listához title: Engedélyezett külső domainek listája - participatory_space_private_users: - create: - error: Hiba történt egy privát felhasználó hozzáadása során a részvételi helyhez. - success: Magán résztvevő részvételi helyhez való hozzáférése biztosítva. - destroy: - error: Hiba történt egy privát felhasználó törlésével a részvételi helyen. - success: Privát felhasználó részvételi helyhez való hozzáférése törölve. - index: - import_via_csv: Importálás CSV -ből - title: Részvételi tér privát felhasználója - new: - create: Létrehozás - title: Új részvételi tér privát felhasználója. - participatory_space_private_users_csv_imports: - create: - invalid: Probléma adódott a CSV fájl beolvasásával. Kérjük, győződjön meg róla, hogy követte az utasításokat. - success: A CSV fájl sikeresen feltöltődött, meghívó e-mailt küldünk a résztvevőknek. Ez eltarthat egy ideig. - new: - csv_upload: - title: Töltse fel a CSV fájlját - destroy: - button: Az összes privát felhasználó törlése - confirm: Biztos, hogy törölni szeretné az összes privát résztvevőt? Ezt a műveletet nem lehet visszacsinálni, nem tudja majd visszaállítani őket. - empty: Nincsenek privát résztvevők. - explanation: '%{count} privát résztvevője van.' - title: Privát felhasználók törlése - example_file: 'Példa fájl:' - explanation: 'Töltse fel a CSV-fájlt. Két oszlopnak kell lennie, a fájl első oszlopában az e-mail címmel, a fájl utolsó oszlopában pedig a nevével azoknak a felhasználóknak, akiket hozzá szeretne adni a részvételi térhez, fejlécek nélkül. Kerülje az olyan érvénytelen karakterek használatát, mint a `<>?%&^*#@()[]=+:;"{}\|` a felhasználói névben.' - explanation_example: | - ohn.doe@example.org%{csv_col_sep}John Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - upload: Feltölt reminders: create: error: Hiba történt a bejegyzés létrehozása során. diff --git a/decidim-admin/config/locales/id-ID.yml b/decidim-admin/config/locales/id-ID.yml index f8ba11fe5883b..b14eae5265078 100644 --- a/decidim-admin/config/locales/id-ID.yml +++ b/decidim-admin/config/locales/id-ID.yml @@ -252,6 +252,10 @@ id: explanation: Pengguna yang dikelola dapat dipromosikan ke pengguna standar. Ini berarti mereka akan diundang ke aplikasi dan Anda tidak akan dapat meniru mereka lagi. Pengguna yang diundang akan menerima email untuk menerima undangan Anda. new_managed_user_promotion: Promosi pengguna terkelola baru promote: Memajukan + members_csv_imports: + new: + csv_upload: + title: Unggah file CSV Anda menu: admin_log: Log aktivitas admin admins: Admin @@ -304,8 +308,6 @@ id: sent_at: Dikirim pada subject: Subyek name: Newsletter - participatory_space_private_user: - name: Ruang partisipatif pengguna pribadi scope: fields: name: Nama @@ -394,22 +396,6 @@ id: update: error: Terjadi kesalahan saat memperbarui organisasi ini. success: Organisasi berhasil diperbarui. - participatory_space_private_users: - create: - error: Terjadi kesalahan saat menambahkan pengguna pribadi untuk ruang partisipatif ini. - success: Ruang partisipatif akses pengguna pribadi berhasil dibuat. - destroy: - error: Terjadi kesalahan saat menghapus pengguna pribadi untuk ruang partisipatif ini. - success: Akses pribadi pengguna ruang partisipatif berhasil dihancurkan. - index: - title: Ruang partisipatif pengguna pribadi - new: - create: Membuat - title: Pengguna Pribadi Ruang Partisipatif Baru. - participatory_space_private_users_csv_imports: - new: - csv_upload: - title: Unggah file CSV Anda resource_permissions: edit: submit: Menyerahkan diff --git a/decidim-admin/config/locales/is-IS.yml b/decidim-admin/config/locales/is-IS.yml index 4c018a35817b4..47a18659a6241 100644 --- a/decidim-admin/config/locales/is-IS.yml +++ b/decidim-admin/config/locales/is-IS.yml @@ -359,9 +359,6 @@ is-IS: youtube: Youtube update: success: Skipulag uppfærður með góðum árangri. - participatory_space_private_users: - new: - create: Búa til resource_permissions: edit: title: Breyta heimildum diff --git a/decidim-admin/config/locales/it.yml b/decidim-admin/config/locales/it.yml index 798fa3ebc83b2..ff9989aa948f8 100644 --- a/decidim-admin/config/locales/it.yml +++ b/decidim-admin/config/locales/it.yml @@ -89,11 +89,6 @@ it: welcome_notification_body: Testo del messaggio di Benvenuto welcome_notification_subject: Oggetto della mail con messaggio di Benvenuto youtube_handler: Gestore di YouTube - participatory_space_private_user: - email: Email - name: Nome - participatory_space_private_user_csv_import: - file: File scope: code: Codice name: Nome @@ -135,10 +130,6 @@ it: attributes: official_img_footer: allowed_file_content_types: File immagine non valido - participatory_space_private_user_csv_import: - attributes: - file: - malformed: File di importazione malformato, leggere attentamente le istruzioni e assicurarsi che il file sia codificato UTF-8. user_group_csv_verification: attributes: file: @@ -186,8 +177,6 @@ it: import: Importazione newsletter: new: Nuova newsletter - participatory_space_private_user: - new: Nuovo utente dello spazio partecipativo privato per_page: Per pagina restore: Ripristina send_me_a_test_email: Inviami una email di prova @@ -418,17 +407,6 @@ it: values: 'false': Ufficializzato 'true': Non ufficializzato - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Invito accettato - values: - 'false': Non accettato - 'true': Accettato - user_invitation_sent_at_not_null: - label: Invito inviato - values: - 'false': Non inviato - 'true': Inviato private_space_eq: label: Privato values: @@ -560,6 +538,10 @@ it: explanation: I partecipanti gestiti possono essere promossi a partecipanti standard. Significa che saranno invitati ad effettuare la richiesta e non sarai in grado di gestirli di nuovo. Il partecipante invitato riceverà un'email per accettare il tuo invito. new_managed_user_promotion: Nuova promozione di partecipanti gestiti promote: Promuovere + members_csv_imports: + new: + csv_upload: + title: Carica il tuo file CSV menu: admin_log: Registro delle attività di amministrazione admins: Amministratori @@ -626,8 +608,6 @@ it: sent_to: Inviato a subject: Oggetto name: Newsletter - participatory_space_private_user: - name: Partecipante privato allo spazio partecipativo scope: fields: name: Nome @@ -895,53 +875,6 @@ it: form: add: Aggiungi alla lista consentita title: Elenco di domini esterni consentiti - participatory_space_private_users: - create: - error: Si è verificato un errore durante l'aggiunta di un utente privato per questo spazio partecipativo. - success: Accesso utente privato spazio partecipativo creato con successo. - destroy: - error: Si è verificato un errore durante l'eliminazione di un utente privato per questo spazio partecipativo. - success: Accesso utente privato spazio distruttivo distrutto con successo. - edit: - title: Modifica partecipante privato allo spazio partecipativo. - update: Aggiorna - index: - import_via_csv: Importa da CSV - publish_all: Pubblica tutto - title: Utente privato dello spazio partecipativo - unpublish_all: Annulla la pubblicazione di tutto - new: - create: Creare - title: Nuovo utente privato dello spazio partecipativo. - publish_all: - error: Si è verificato un errore durante l'aggiunta degli utenti privati per questo spazio partecipativo. - success: Ha pubblicato con successo tutti i partecipanti privati per questo spazio partecipativo - unpublish_all: - error: Si è verificato un problema di annullamento della pubblicazione di tutti i partecipanti privati per questo spazio partecipativo. - success: Tutti i partecipanti privati per questo spazio partecipativo sono stati rimossi dalla pubblicazione con successo - update: - error: Si è verificato un errore durante l'aggiornamento dell'utente privato per questo spazio partecipativo. - success: Utente privato dello spazio partecipativo aggiornato con successo - participatory_space_private_users_csv_imports: - create: - invalid: Si è verificato un problema durante la lettura del file CSV. Assicurati di aver seguito le istruzioni. - success: File CSV caricato con successo, stiamo inviando un'email di invito ai partecipanti. Potrebbe richiedere un po' di tempo. - new: - csv_upload: - title: Carica il tuo file CSV - destroy: - button: Elimina tutti i partecipanti privati - confirm: Sei sicuro di voler eliminare tutti i partecipanti privati? Questa azione non può essere annullata, non sarai in grado di recuperarli. - empty: Non hai partecipanti privati. - explanation: Hai %{count} partecipanti privati. - title: Elimina tutti i partecipanti privati - example_file: 'File di esempio:' - explanation: 'Carica il file CSV. Il file deve contenere due colonne (e-mail e nome) senza intestazioni: nella prima sono elencati gli indirizzi e-mail degli utenti che si desidera aggiungere allo spazio partecipativo, nella seconda i loro nomi. Evita di usare caratteri non validi come `<>?%&^*#@()[]=+:;"{}\|` nella colonna dei nomi.' - explanation_example: | - john.doe@example.org%{csv_col_sep}John Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - title: Importa partecipanti privati tramite CSV - upload: Carica File reminders: create: error: Si è verificato un errore durante la creazione dei promemoria. diff --git a/decidim-admin/config/locales/ja.yml b/decidim-admin/config/locales/ja.yml index 31285675954b2..f6904b25eeaf7 100644 --- a/decidim-admin/config/locales/ja.yml +++ b/decidim-admin/config/locales/ja.yml @@ -33,6 +33,11 @@ ja: help_section: content: 内容 id: ID + member: + email: メールアドレス + name: 名前 + member_csv_import: + file: ファイル newsletter: body: 本文 send_to_all_users: 全参加者に送信 @@ -91,11 +96,6 @@ ja: welcome_notification_body: ウェルカム通知本文 welcome_notification_subject: ウェルカム通知の件名 youtube_handler: YouTube ハンドラー - participatory_space_private_user: - email: Eメールアドレス - name: 名前 - participatory_space_private_user_csv_import: - file: ファイル scope: code: コード name: 名前 @@ -125,10 +125,17 @@ ja: show_in_footer: フッターに表示 title: タイトル weight: 順番の位置 + taxonomy: + item_name: アイテム名 + parent_id: 親 user_group_csv_verification: file: ファイル errors: models: + member_csv_import: + attributes: + file: + malformed: インポートファイルの形式が正しくありません。指示内容をよく読み、ファイルがUTF-8でエンコードされていることを確認してください。 newsletter: attributes: base: @@ -137,10 +144,6 @@ ja: attributes: official_img_footer: allowed_file_content_types: 無効な画像ファイル - participatory_space_private_user_csv_import: - attributes: - file: - malformed: インポートファイルの形式が正しくありません。手順を見直して、ファイルがUTF-8でエンコードされていることを確認してください。 user_group_csv_verification: attributes: file: @@ -188,12 +191,12 @@ ja: export: エクスポート export-selection: 選択したものをエクスポート import: インポート + member: + new: 新規メンバー menu_hidden: メニューから隠す moderate: モデレーションの管理 newsletter: new: 新しいニュースレター - participatory_space_private_user: - new: 新しい参加型スペースのプライベートユーザー per_page: ページごと permissions: 権限の管理 restore: 復元 @@ -414,6 +417,17 @@ ja: values: 'false': 'いいえ' 'true': 'はい' + members: + user_invitation_accepted_at_not_null: + label: 招待が承認されました + values: + 'false': 未承認 + 'true': 承認済み + user_invitation_sent_at_not_null: + label: 招待状を送信しました + values: + 'false': 未送信 + 'true': 送信済み moderated_users: reports_reason_eq: label: 理由を報告 @@ -429,17 +443,6 @@ ja: values: 'false': 公式化済み 'true': 公式化されていない - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: 招待が承認されました - values: - 'false': 未承認 - 'true': 承認済み - user_invitation_sent_at_not_null: - label: 招待状の送信 - values: - 'false': 未送信 - 'true': 送信済み private_space_eq: label: プライベート values: @@ -568,6 +571,51 @@ ja: explanation: 管理対象参加者は通常の参加者に昇格することができます。 その場合、彼らはアプリケーションに招待され、あなたはそれらを再び管理することはできなくなります。 招待された参加者は、招待を承諾するためのメールを受け取ります。 new_managed_user_promotion: 新しい管理対象参加者の昇格 promote: プロモート + members: + create: + error: 参加型スペースにメンバーを追加する際に問題が発生しました。 + success: メンバーを作成しました。 + destroy: + error: 参加型スペースのメンバーの削除中に問題が発生しました。 + success: メンバーを削除しました。 + edit: + update: 更新 + index: + import_via_csv: CSV形式でインポート + publish_all: すべて公開 + title: メンバー + unpublish_all: すべて非公開 + new: + create: 作成 + publish_all: + error: 参加スペースのすべてのメンバーを公開する際に問題が発生しました。 + success: 参加スペースのすべてのメンバーを公開しました + unpublish_all: + error: 参加スペースのすべてのメンバーを非公開にする際に問題が発生しました。 + success: 参加スペースのすべてのメンバーを非公開にしました + update: + error: 参加スペースのメンバーを更新する際に問題がありました。 + success: メンバーを更新しました + members_csv_imports: + create: + invalid: CSVファイルの読み込み中に問題が発生しました。指示に従って正しく操作されているかご確認ください。 + success: CSVファイルのアップロードが完了しました。参加者に招待メールを送信中です。完了までしばらくお待ちください。 + new: + csv_upload: + title: CSVファイルをアップロード + destroy: + button: すべてのメンバーを削除 + confirm: すべてのメンバーを削除してもよろしいですか?この操作は元に戻せません。復元することはできません。 + empty: メンバーがいません。 + explanation: '%{count} 名のメンバーがいます。' + title: メンバーを削除 + example_file: 'サンプルファイル:' + explanation: "CSVファイルをアップロードしてください。ファイルには2つの列が必要で、1列目にはメールアドレス、最後の列には参加スペースに追加したいユーザーの名前を入力してください。ヘッダー行は不要です。ユーザー名には、`<>?%&^*#@()[]=+:;\"{}\\|`などの無効な文字を使用しないでください。\n" + explanation_example: | + john.doe@example.org%{csv_col_sep}John Doe + jane.doe@example.org%{csv_col_sep}Jane Doe + title: CSV形式でメンバーをインポート + upload: アップロード menu: admin_log: 管理者アクティビティログ admins: 管理者 @@ -626,6 +674,8 @@ ja: reason: 理由: started_at: 開始日時 user: 参加者 + member: + name: メンバー newsletter: fields: created_at: 作成日時 @@ -634,8 +684,6 @@ ja: sent_to: '送信先:' subject: 件名 name: ニュースレター - participatory_space_private_user: - name: 参加型スペースプライベート参加者 scope: fields: name: 名前 @@ -910,53 +958,6 @@ ja: form: add: 許可されたリストに追加 title: 許可された外部ドメインのリスト - participatory_space_private_users: - create: - error: この参加型スペースにプライベート参加者を追加する際に問題が発生しました。 - success: 参加型スペースのプライベート参加者アクセスが正常に作成されました。 - destroy: - error: この参加型スペースのプライベート参加者を削除する際に問題が発生しました。 - success: 参加型スペースのプライベート参加者アクセスが正常に破棄されました。 - edit: - title: 参加スペースの非公開参加者を編集します。 - update: 更新 - index: - import_via_csv: CSV形式でインポート - publish_all: すべて公開 - title: 参加型スペースプライベート参加者 - unpublish_all: すべて非公開 - new: - create: 作成 - title: 新しい参加型スペースのプライベート参加者。 - publish_all: - error: この参加型スペースのすべてのプライベート参加者を公開する際に問題が発生しました。 - success: この参加型スペースのすべてのプライベート参加者を公開しました - unpublish_all: - error: この参加型スペースのすべてのプライベート参加者を非公開にする際に問題が発生しました。 - success: この参加型スペースのすべてのプライベート参加者を非公開にしました - update: - error: この参加型スペースのプライベート参加者の更新中に問題が発生しました。 - success: 参加型スペースのプライベート参加者を更新しました - participatory_space_private_users_csv_imports: - create: - invalid: CSVファイルの読み込み中に問題が発生しました。手順を確認してください。 - success: CSVファイルは正常にアップロードされました。参加者に招待メールを送信しています。しばらくお待ちください。 - new: - csv_upload: - title: CSVファイルをアップロード - destroy: - button: すべてのプライベート参加者を削除 - confirm: すべてのプライベート参加者を削除してもよろしいですか?この操作は元に戻せず、削除された方を復元することはできません。 - empty: プライベートの参加者はいません。 - explanation: '%{count} 人のプライベート参加者がいます。' - title: プライベート参加者を削除 - example_file: 'サンプルファイル:' - explanation: 'CSV ファイルをアップロードします。 CSVにはヘッダをつけず、最初のカラムにはメールアドレスを、次のカラムにはユーザー名を入れた2カラムのCSVとして、参加スペースに追加したいユーザーの情報を並べてください。 ユーザー名には `<>?%&^*#@()[]=+:;"{}\|`のような無効な文字を使用しないでください。' - explanation_example: | - john.doe@example.org%{csv_col_sep}John Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - title: プライベート参加者をCSV形式でインポート - upload: アップロード reminders: create: error: リマインダー作成中に問題が発生しました。 diff --git a/decidim-admin/config/locales/kaa.yml b/decidim-admin/config/locales/kaa.yml index 0b3071af7cc60..5426820b34eef 100644 --- a/decidim-admin/config/locales/kaa.yml +++ b/decidim-admin/config/locales/kaa.yml @@ -12,8 +12,6 @@ kaa: name: Ataması organization: name: Ataması - participatory_space_private_user: - email: Elektron pochta user_group_csv_verification: file: Fayl activerecord: @@ -144,12 +142,6 @@ kaa: instagram: Instagram url: URL youtube: YouTube - participatory_space_private_users: - new: - create: Jaratıw - participatory_space_private_users_csv_imports: - new: - upload: Júklew reminders: new: submit: Jiberiw diff --git a/decidim-admin/config/locales/ko.yml b/decidim-admin/config/locales/ko.yml index 51900de0a8190..f5487f68cdb53 100644 --- a/decidim-admin/config/locales/ko.yml +++ b/decidim-admin/config/locales/ko.yml @@ -66,9 +66,6 @@ ko: twitter_handler: X handler warning_color: 경고 youtube_handler: YouTube handler - participatory_space_private_user: - email: 이메일 - name: 이름 scope: code: 코드 name: 이름 @@ -106,10 +103,6 @@ ko: attributes: official_img_footer: allowed_file_content_types: 잘못된 이미지 파일 - participatory_space_private_user_csv_import: - attributes: - file: - malformed: 잘못된 형식의 가져오기 파일입니다. 설명서를 잘 읽고 UTF-8이 인코딩되었는지 확인하십시오. user_group_csv_verification: attributes: file: @@ -145,8 +138,6 @@ ko: import: 가져오기 newsletter: new: 새 뉴스레터 - participatory_space_private_user: - new: 새 참여 공간 비공개 사용자 per_page: 페이지 당 send_me_a_test_email: 나에게 테스트 메일 보내기 share: 공유 @@ -306,10 +297,6 @@ ko: label: 유형 officialized_at_null: label: 상태 - participatory_space_private_users: - user_invitation_sent_at_not_null: - values: - 'true': 전송됨 private_space_eq: label: 비공개 values: @@ -429,8 +416,6 @@ ko: sent_to: 다음에 전송하였습니다 subject: 주제 name: 뉴스 레터 - participatory_space_private_user: - name: 참여공간 비공개참여 scope: fields: name: 이름 @@ -531,20 +516,6 @@ ko: hidden: 숨겨짐 show: 표시 title: 참가자의 이메일 주소 보기 - participatory_space_private_users: - new: - create: 생성 - participatory_space_private_users_csv_imports: - new: - csv_upload: - title: CSV 파일 업로드 - destroy: - button: 모든 비공개 참가자 삭제 - confirm: 모든 비공개 참가자를 삭제하시겠습니까? 이 작업은 취소할 수 없습니다. 복구할 수 없습니다. - empty: 비공개 참가자가 없습니다. - explanation: '%{count} 개의 비공개 참가자가 있습니다.' - title: 비공개 참가자 삭제 - example_file: '예제 파일:' reminders: create: error: 리마인더를 만드는 중 문제가 발생했습니다. diff --git a/decidim-admin/config/locales/lb.yml b/decidim-admin/config/locales/lb.yml index ef1b121a60f0b..3bae6f46848b3 100644 --- a/decidim-admin/config/locales/lb.yml +++ b/decidim-admin/config/locales/lb.yml @@ -139,8 +139,6 @@ lb: import: Importieren newsletter: new: Neuer Newsletter - participatory_space_private_user: - new: Neuer privater Benutzer per_page: Pro Seite share: Teilen area_types: @@ -291,17 +289,6 @@ lb: values: 'false': Offizialisiert 'true': Nicht offiziell - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Einladung akzeptiert - values: - 'false': Nicht akzeptiert - 'true': Akzeptiert - user_invitation_sent_at_not_null: - label: Einladung versendet - values: - 'false': Nicht versendet - 'true': Versendet private_space_eq: label: Privat values: @@ -378,6 +365,10 @@ lb: explanation: Verwaltete Benutzer können zu Standardbenutzern heraufgestuft werden. Das bedeutet, dass sie zu der Anwendung eingeladen werden und nicht in der Lage sind, sie erneut zu repräsentieren. Der eingeladene Benutzer erhält eine E-Mail, um Ihre Einladung anzunehmen. new_managed_user_promotion: Neue verwaltete Benutzerwerbung promote: Fördern + members_csv_imports: + new: + csv_upload: + title: Laden Sie Ihre CSV-Datei hoch menu: admin_log: Admin-Aktivitätsprotokoll admins: Admins @@ -432,8 +423,6 @@ lb: sent_to: Gesendet an subject: Gegenstand name: Newsletter - participatory_space_private_user: - name: Participatory Space privater Benutzer scope: fields: name: Name @@ -597,24 +586,6 @@ lb: update: error: Beim Aktualisieren dieser Organisation ist ein Fehler aufgetreten. success: Die Organisation wurde erfolgreich aktualisiert. - participatory_space_private_users: - create: - error: Beim Hinzufügen eines privaten Benutzers für diesen partizipativen Bereich ist ein Fehler aufgetreten. - success: Participatory Space Privater Benutzerzugriff erfolgreich erstellt. - destroy: - error: Beim Löschen eines privaten Benutzers für diesen partizipativen Bereich ist ein Fehler aufgetreten. - success: Participatory Space Privater Benutzerzugriff wurde erfolgreich zerstört. - index: - import_via_csv: Aus CSV-Datein importieren - title: Participatory Space privater Benutzer - new: - create: Erstellen - title: Neuer privater Benutzer des Participatory Space. - participatory_space_private_users_csv_imports: - new: - csv_upload: - title: Laden Sie Ihre CSV-Datei hoch - upload: Hochladen resource_permissions: edit: submit: einreichen diff --git a/decidim-admin/config/locales/lt.yml b/decidim-admin/config/locales/lt.yml index 36d9e2c2459e5..b16095b138b4b 100644 --- a/decidim-admin/config/locales/lt.yml +++ b/decidim-admin/config/locales/lt.yml @@ -86,9 +86,6 @@ lt: welcome_notification_body: Pasisveikinimo pranešimo tekstas welcome_notification_subject: Pasisveikinimo pranešimo antraštė youtube_handler: '„YouTube“ tvarkyklė' - participatory_space_private_user: - email: El. paštas - name: Pavadinimas scope: code: Kodas name: Pavadinimas @@ -129,10 +126,6 @@ lt: attributes: official_img_footer: allowed_file_content_types: Netinkama vaizdo rinkmena - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Blogai suformuotas importo failas, prašome atidžiai perskaityti instrukcijas ir įsitikinkite, kad failas yra UTF-8 koduotėje. user_group_csv_verification: attributes: file: @@ -170,8 +163,6 @@ lt: import: Importuoti newsletter: new: Naujas naujienlaiškis - participatory_space_private_user: - new: Naujas dalyvaujamosios erdvės privatus naudotojas per_page: Per puslapį send_me_a_test_email: Siųsti man bandomąjį el. laišką share: Dalintis @@ -358,17 +349,6 @@ lt: values: 'false': Oficializuota 'true': Neoficializuota - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Pakvietimas priimtas - values: - 'false': Nepriimtas - 'true': Priimtas - user_invitation_sent_at_not_null: - label: Pakvietimas išsiųstas - values: - 'false': Neišsiųstas - 'true': Išsiųstas private_space_eq: label: Privatus values: @@ -494,6 +474,10 @@ lt: explanation: Tvarkomi dalyviai gali būti paaukštinti į standartinius dalyvius. Tai reiškia, kad jie bus pakviesti į platformą ir nebegalėsite jų tvarkyti. Pakviesti naudotojai gaus kvietimą prisijungti el. paštu. new_managed_user_promotion: Naujas tvarkomo naudotojo paaukštinimas promote: Paaukštinti + members_csv_imports: + new: + csv_upload: + title: Įkelkite savo CSV rinkmeną menu: admin_log: Administratoriaus veiklos žurnalas admins: Administratoriai @@ -551,8 +535,6 @@ lt: sent_to: Išsiųsta subject: Tema name: Naujienlaiškis - participatory_space_private_user: - name: Dalivaujamosios erdvės privatus dalyvis scope: fields: name: Vardas @@ -733,38 +715,6 @@ lt: update: error: Atnaujinant šią organizaciją iškilo problema. success: Organizacija atnaujinta. - participatory_space_private_users: - create: - error: Pridedant privatų dalyvį į šia dalyvaujamają erdvę kilo problema. - success: Dalyvaujamosios erdvės privataus dalyvio prieiga buvo sukurta sėkmingai. - destroy: - error: Ištrinant privatų dalyvį iš dalyvaujamosios erdvės kilo problema. - success: Dalyvaujamojo proceso erdvės privataus dalyvio prieiga panaikinta. - index: - import_via_csv: Importuoti iš CSV - title: Dalyvaujamosios erdvės privatus dalyvis - new: - create: Sukurti - title: Naujas dalyvaujamosios erdvės privatus dalyvis. - participatory_space_private_users_csv_imports: - create: - invalid: Skaitant CSV failą kilo problema. Prašau įsitikinkite kad sekėte instrukcijas. - success: CSV rinkmena įkelta, dalyviams siunčiame kvietimą e. laišku. Tai gali užtrukti. - new: - csv_upload: - title: Įkelkite savo CSV rinkmeną - destroy: - button: Ištrinti visus privačius dalyvius - confirm: Ar tikrai norite ištrinti visus privačius dalyvius? Atlikus šį veiksmą nebebus įmanoma sugražinti šių dalyvių į platformą. - empty: Neturite privačių dalyvių. - explanation: Turite %{count} privačių dalyvių. - title: Ištrinti privačius dalyvius - example_file: 'Pavyzdinė rinkmena:' - explanation: 'Įkelkite CSV dokumentą. Jis turi turėti du stulpelius su naudotojų el. paštu pirmajame ir vardu paskutiniąjame. Dokumente neturėtų būti laukų pavadinimų ir šių simbolių naudotojų varduose "<>?%&^*#@()[]=+:;"{}\|".' - explanation_example: | - john.doe@example.org%{csv_col_sep}John Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - upload: Įkelti reminders: create: error: Kuriant priminimus iškilo problema. diff --git a/decidim-admin/config/locales/lv.yml b/decidim-admin/config/locales/lv.yml index dab5bfd694b3d..acb955ed562d1 100644 --- a/decidim-admin/config/locales/lv.yml +++ b/decidim-admin/config/locales/lv.yml @@ -274,6 +274,10 @@ lv: explanation: Pārvaldītos dalībniekus var paaugstināt par standarta dalībniekiem. Tas nozīmē, ka viņi tiks uzaicināti uz lietojumprogrammu un jūs viņus vairs nevarēsiet pārvaldīt. Uzaicinātais dalībnieks saņems e-pastu ar jūsu ielūgumu. new_managed_user_promotion: Jauna pārvaldīta dalībnieka paaugstināšana promote: Paaugstināt + members_csv_imports: + new: + csv_upload: + title: Augšupielādējiet savu CSV failu menu: admin_log: Administratora darbību žurnāls admins: Administratori @@ -326,8 +330,6 @@ lv: sent_to: Nosūtīts subject: Temats name: Informatīvais biļetens - participatory_space_private_user: - name: Līdzdalības telpas privāts dalībnieks scope: fields: name: Nosaukums @@ -442,23 +444,6 @@ lv: update: error: Šīs organizācijas atjaunināšanas laikā radās problēma. success: Organizācija ir veiksmīgi atjaunināta. - participatory_space_private_users: - create: - error: Pievienojot privāto dalībnieku šai līdzdalības telpai, radās problēma. - success: Privāto dalībnieku piekļuve līdzdalības telpai ir veiksmīgi izveidota. - destroy: - error: Dzēšot privāto dalībnieku no šīs līdzdalības telpas, radās problēma. - success: Privāto dalībnieku piekļuve līdzdalības telpai ir veiksmīgi dzēsta. - index: - title: Līdzdalības telpas privāts dalībnieks - new: - create: Izveidot - title: Jauns līdzdalības telpas privāts dalībnieks - participatory_space_private_users_csv_imports: - new: - csv_upload: - title: Augšupielādējiet savu CSV failu - upload: Augšupielādēt resource_permissions: edit: submit: Iesniegt diff --git a/decidim-admin/config/locales/nl.yml b/decidim-admin/config/locales/nl.yml index ad6d30bc45549..21ed0d536ba28 100644 --- a/decidim-admin/config/locales/nl.yml +++ b/decidim-admin/config/locales/nl.yml @@ -139,8 +139,6 @@ nl: import: Importeren newsletter: new: Nieuwe nieuwsbrief - participatory_space_private_user: - new: Nieuwe privégebruiker van de inspraakruimte per_page: Per pagina share: Deel user: @@ -302,17 +300,6 @@ nl: values: 'false': Gevalideerd 'true': Niet gevalideerd - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Uitnodiging geaccepteerd - values: - 'false': Niet geaccepteerd - 'true': Geaccepteerd - user_invitation_sent_at_not_null: - label: Uitnodiging verzonden - values: - 'false': Niet verzonden - 'true': Verzonden private_space_eq: label: Privé values: @@ -425,6 +412,10 @@ nl: explanation: Beheerde deelnemers kunnen worden gepromoveerd tot standaarddeelnemers. Dit betekent dat ze worden uitgenodigd voor de toepassing en dat u ze niet meer kunt beheren. De uitgenodigde deelnemer ontvangt een e-mail om uw uitnodiging te accepteren. new_managed_user_promotion: Nieuwe promotie voor beheerde deelnemers promote: Promoten + members_csv_imports: + new: + csv_upload: + title: Upload uw CSV-bestand menu: admin_log: Admin activiteitenlogboek admins: Admins @@ -479,8 +470,6 @@ nl: sent_to: Verzonden aan subject: Onderwerp name: Nieuwsbrief - participatory_space_private_user: - name: Participerende ruimte privé deelnemer scope: fields: name: Naam @@ -644,29 +633,6 @@ nl: update: error: Er is een fout opgetreden bij het bijwerken van deze organisatie. success: Organisatie is succesvol bijgewerkt. - participatory_space_private_users: - create: - error: Er is een probleem opgetreden bij het toevoegen van een privédeelnemer aan deze deelruimte. - success: De toegang van de privé deelnemer voor de burgerinspraak is succesvol aangemaakt. - destroy: - error: Er was een probleem met het verwijderen van een privé-deelnemer voor deze participatieruimte. - success: De toegang van de privé deelnemer voor burgerinspraak is met succes verwijderd. - index: - import_via_csv: Importeren via csv - title: Participerende ruimte voor privé deelnemer - new: - create: creëren - title: Nieuwe privé deelnemer burgerinspraak. - participatory_space_private_users_csv_imports: - new: - csv_upload: - title: Upload uw CSV-bestand - destroy: - button: Verwijder alle privé deelnemers - empty: Je hebt geen privé deelnemers. - explanation: Je hebt %{count} privédeelnemers. - title: Verwijder privé deelnemer - upload: Upload reminders: create: error: Er is een probleem opgetreden bij het maken van herinneringen. diff --git a/decidim-admin/config/locales/no.yml b/decidim-admin/config/locales/no.yml index afe2affff2bff..e5e3f5871cea1 100644 --- a/decidim-admin/config/locales/no.yml +++ b/decidim-admin/config/locales/no.yml @@ -139,8 +139,6 @@ import: Importer newsletter: new: Nytt nyhetsbrev - participatory_space_private_user: - new: Nytt deltakerområde for privat bruker per_page: Per side share: Del user: @@ -314,17 +312,6 @@ values: 'false': Offisialisert 'true': Ikke offisialisert - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Invitasjon godtatt - values: - 'false': Ikke akseptert - 'true': Godkjent - user_invitation_sent_at_not_null: - label: Invitasjon sendt - values: - 'false': Ikke sendt - 'true': Sendt private_space_eq: label: Privat values: @@ -430,6 +417,10 @@ explanation: Styrte deltakere kan bli promotert til standard deltakere. Dette betyr at de vil bli invitert til applikasjonen og du vil ikke kunne administrere dem lenger. De inviterte deltakerene vil motta en email så de kan akseptere invitasjonen. new_managed_user_promotion: Ny administrert deltaker forfremmelse promote: Promoter + members_csv_imports: + new: + csv_upload: + title: Last opp din CSV fil menu: admin_log: Admin aktivitet logg admins: Adminer @@ -484,8 +475,6 @@ sent_to: Send til subject: Emne name: Nyhetsbrev - participatory_space_private_user: - name: Deltakerområdets private deltaker scope: fields: name: Navn @@ -649,29 +638,6 @@ update: error: Det oppstod et problem med å oppdatere denne organisasjon. success: Organisasjonen ble oppdatert. - participatory_space_private_users: - create: - error: Det oppstod et problem med å legge til en privat deltaker for dette deltakerområdet. - success: Deltakerområde privat deltaker tilgang opprettet. - destroy: - error: Det oppstod et problem med å slette en privat deltaker for dette deltakerområdet. - success: Deltakerområde privat deltaker tilgang ødelagt. - index: - import_via_csv: Importer fra CSV - title: Deltakerområdets private deltaker - new: - create: Opprett - title: Ny deltaker område privat deltaker. - participatory_space_private_users_csv_imports: - new: - csv_upload: - title: Last opp din CSV fil - destroy: - button: Slett alle private deltakere - empty: Du har ingen private deltakere. - explanation: Du har %{count} private deltakere. - title: Slett private deltakere - upload: Last opp reminders: create: error: Det oppsto et problem med å opprette påminnelser. diff --git a/decidim-admin/config/locales/pl.yml b/decidim-admin/config/locales/pl.yml index dceff2ab830ee..48761c99868cd 100644 --- a/decidim-admin/config/locales/pl.yml +++ b/decidim-admin/config/locales/pl.yml @@ -90,9 +90,6 @@ pl: welcome_notification_body: Treść komunikatu powitalnego welcome_notification_subject: Tytuł komunikatu powitalnego youtube_handler: Handler YouTube - participatory_space_private_user: - email: E-mail - name: Nazwa scope: code: Kod name: Nazwa @@ -134,10 +131,6 @@ pl: attributes: official_img_footer: allowed_file_content_types: Nieprawidłowy plik obrazu - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Nieprawidłowy plik importu, przeczytaj uważnie instrukcję i upewnij się, że plik jest kodowany w UTF-8. user_group_csv_verification: attributes: file: @@ -176,8 +169,6 @@ pl: import: Importuj newsletter: new: Nowy newsletter - participatory_space_private_user: - new: Nowy użytkownik prywatny per_page: Na stronę send_me_a_test_email: Wyślij mi testowego e-maila share: Udostępnij @@ -388,17 +379,6 @@ pl: values: 'false': Oficjalny 'true': Nieoficjalny - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Zaakceptowano zaproszenie - values: - 'false': Nie zaakceptowano - 'true': Zaakceptowano - user_invitation_sent_at_not_null: - label: Wysłano zaproszenie - values: - 'false': Nie wysłano - 'true': Wysłano private_space_eq: label: Prywatny values: @@ -527,6 +507,10 @@ pl: explanation: Zarządzani użytkownicy mogą być promowani do standardowych użytkowników. Oznacza to, że zostaną zaproszeni do udziału w aplikacji i nie będą mogli podszywać się pod inne osoby. Zaproszony użytkownik otrzyma wiadomość e-mail, aby zaakceptować zaproszenie. new_managed_user_promotion: Nowa zarządzana promocja użytkowników promote: Awansuj + members_csv_imports: + new: + csv_upload: + title: Prześlij swój plik CSV menu: admin_log: Logi aktywności administratora admins: Administratorzy @@ -587,8 +571,6 @@ pl: sent_to: Wysłano do subject: Temat name: Newsletter - participatory_space_private_user: - name: Użytkownik prywatnej przestrzeni partycypacyjnej scope: fields: name: Nazwa @@ -782,39 +764,6 @@ pl: form: add: Dodaj do listy dozwolonych title: Dozwolone domeny zewnętrzne - participatory_space_private_users: - create: - error: Wystąpił błąd podczas dodawania użytkownika prywatnego do tej przestrzeni partycypacyjnej. - success: Dodano dostęp dla prywatnego użytkownika przestrzeni partycypacyjnej. - destroy: - error: Wystąpił błąd podczas usuwania użytkownika prywatnego z tej przestrzeni partycypacyjnej. - success: Usunięto dostęp dla prywatnego użytkownika w tej przestrzeni partycypacyjnej. - index: - import_via_csv: Importuj z CSV - title: Prywatny użytkownik przestrzeni partycypacyjnej - new: - create: Utwórz - title: Nowy prywatny użytkownik przestrzeni partycypacyjnej. - participatory_space_private_users_csv_imports: - create: - invalid: Wystąpił problem podczas odczytu pliku CSV. Upewnij się, że postępowałeś zgodnie z instrukcjami. - success: Załadowanie pliku CSV powiodło się, wysyłamy e-mail z zaproszeniem do uczestników. To może chwilę potrwać. - new: - csv_upload: - title: Załaduj swój plik CSV - destroy: - button: Usuń wszystkich prywatnych uczestników - confirm: Czy na pewno chcesz usunąć wszystkich uczestników prywatnych? Tej akcji nie można cofnąć, nie będziesz w stanie odzyskać danych. - empty: Nie masz żadnych uczestników prywatnych. - explanation: Masz %{count} uczestników prywatnych. - title: Usuń prywatnych uczestników - example_file: 'Przykładowy plik:' - explanation: 'Załaduj swój plik CSV. Plik musi składać się z dwóch kolumn: w pierwszej kolumnie adresy e-mail, a w ostatniej nazwy użytkowników, których chcesz dodać do przestrzeni dla uczestników. Nie dodawaj nagłówków. Unikaj niepoprawnych znaków, takich jak `<>?%&^*#@()[]=+:;"{}\|` w nazwie użytkownika.' - explanation_example: | - jan.kowalski@przyklad.org%{csv_col_sep}Jan Kowalski - anna.kowalska@przyklad.org%{csv_col_sep}Anna Kowalska - title: Importuj uczestników prywatnych przez CSV - upload: Prześlij reminders: create: error: Wystąpił błąd podczas tworzenia przypomnień. diff --git a/decidim-admin/config/locales/pt-BR.yml b/decidim-admin/config/locales/pt-BR.yml index 10220e1f636ec..46a0a8586de03 100644 --- a/decidim-admin/config/locales/pt-BR.yml +++ b/decidim-admin/config/locales/pt-BR.yml @@ -89,9 +89,6 @@ pt-BR: welcome_notification_body: Corpo de notificação de boas-vindas welcome_notification_subject: Corpo de notificação de boas-vindas youtube_handler: Manipulador do YouTube - participatory_space_private_user: - email: E-mail - name: Nome scope: code: Código name: Nome @@ -133,10 +130,6 @@ pt-BR: attributes: official_img_footer: allowed_file_content_types: Arquivo de imagem inválido - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Arquivo de importação malformado, por favor leia as instruções cuidadosamente e certifique-se de que o arquivo é codificado em UTF-8. user_group_csv_verification: attributes: file: @@ -175,8 +168,6 @@ pt-BR: import: Importar newsletter: new: Novo Boletim Informativo - participatory_space_private_user: - new: Novo espaço participativo de usuário privado per_page: Por página send_me_a_test_email: Enviar um e-mail de teste share: Compartilhar @@ -390,17 +381,6 @@ pt-BR: values: 'false': Oficializado 'true': Não oficializado - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Convite aceito - values: - 'false': Não aceito - 'true': Aceito - user_invitation_sent_at_not_null: - label: Envio de convite - values: - 'false': Não enviado - 'true': Enviado private_space_eq: label: Privado values: @@ -515,6 +495,10 @@ pt-BR: explanation: Os usuários gerenciados podem ser promovidos para usuários padrão. Isso significa que eles serão convidados para o aplicativo e você não poderá representá-los novamente. O usuário convidado receberá um e-mail para aceitar seu convite. new_managed_user_promotion: Nova promoção de usuário gerenciado promote: Promover + members_csv_imports: + new: + csv_upload: + title: Envie seu arquivo CSV menu: admin_log: Log de atividade de administração admins: Admins @@ -574,8 +558,6 @@ pt-BR: sent_to: Enviado para subject: Assunto name: Newsletter - participatory_space_private_user: - name: Espaço participativo usuário particular scope: fields: name: Nome @@ -829,39 +811,6 @@ pt-BR: form: add: Adicionar na lista de permitidos title: Lista de domínios externos permitidos - participatory_space_private_users: - create: - error: Ocorreu um erro ao adicionar um usuário privado para este espaço participativo. - success: Espaço participativo acesso de usuário privado criado com sucesso. - destroy: - error: Houve um erro ao excluir um usuário particular desse espaço participativo. - success: Espaço participativo acesso de usuário privado excluído com sucesso. - index: - import_via_csv: Importar via CSV - title: Espaço participativo usuário particular - new: - create: Criar - title: Novo usuário particular do Espaço Participativo. - participatory_space_private_users_csv_imports: - create: - invalid: Ocorreu um problema ao ler o arquivo CSV. Por favor, certifique-se de ter seguido as instruções. - success: Arquivo CSV enviado com sucesso, estamos enviando um e-mail de convite para os participantes. Isso pode demorar um pouco. - new: - csv_upload: - title: Envie seu arquivo CSV - destroy: - button: Excluir todos os participantes privados - confirm: Tem certeza que deseja excluir todos os participantes privados? Esta ação não pode ser desfeita, não será possível recuperá-los. - empty: Você não tem participantes privados. - explanation: Você tem %{count} participantes privados. - title: Excluir participantes privados - example_file: 'Arquivo de exemplo:' - explanation: 'Envie seu arquivo CSV. Deve ter duas colunas com e-mail na primeira coluna do arquivo e nome na última coluna do arquivo dos usuários que você deseja adicionar ao espaço participativo, sem cabeçalhos. Evite usar caracteres inválidos como `<>?%&^*#@()[]=+:;"{}\├` no nome de usuário.' - explanation_example: | - joao.silva@examplo.org%{csv_col_sep}João da Silva - maria.silva@examplo.org%{csv_col_sep}Maria da Silva - title: Importar participantes privados via CSV - upload: Upload reminders: create: error: Ocorreu um problema ao criar lembretes. diff --git a/decidim-admin/config/locales/pt.yml b/decidim-admin/config/locales/pt.yml index 22814903cf5ab..e17d7510df691 100644 --- a/decidim-admin/config/locales/pt.yml +++ b/decidim-admin/config/locales/pt.yml @@ -139,8 +139,6 @@ pt: import: Importar newsletter: new: Novo boletim informativo - participatory_space_private_user: - new: Novo utilizador privado do espaço participativo per_page: Por página share: Partilhar area_types: @@ -296,17 +294,6 @@ pt: values: 'false': Oficializado 'true': Não oficializado - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Convite aceite - values: - 'false': Não aceite - 'true': Aceite - user_invitation_sent_at_not_null: - label: Convite enviado - values: - 'false': Não enviado - 'true': Enviado private_space_eq: label: Privado values: @@ -390,6 +377,10 @@ pt: explanation: Os participantes geridos podem ser promovidos para participantes padrão. Isto significa que eles serão convidados para a aplicação e não poderá geri-los novamente. O participante convidado receberá um e-mail para aceitar o seu convite. new_managed_user_promotion: Nova promoção de usuário participante gerido promote: Destacar + members_csv_imports: + new: + csv_upload: + title: Envie o seu ficheiro CSV menu: admin_log: Registo de atividade de administração admins: Administradores @@ -446,8 +437,6 @@ pt: sent_to: Enviado para subject: Assunto name: Boletim Informativo - participatory_space_private_user: - name: Participante privado de espaço participativo scope: fields: name: Nome @@ -622,24 +611,6 @@ pt: update: error: Ocorreu um problema ao atualizar esta organização. success: Organização atualizada corretamente. - participatory_space_private_users: - create: - error: Ocorreu um problema ao adicionar um participante privado neste espaço participativo. - success: Acesso ao espaço participativo de participante privado criado corretamente. - destroy: - error: Houve um problema ao eliminar um participante privado deste espaço participativo. - success: Espaço participativo acesso de usuário privado destruído com sucesso. - index: - import_via_csv: Importar via CSV - title: Espaço participativo de participante privado - new: - create: Criar - title: Novo participante privado do Espaço Participativo. - participatory_space_private_users_csv_imports: - new: - csv_upload: - title: Envie o seu ficheiro CSV - upload: Carregar resource_permissions: edit: submit: Submeter diff --git a/decidim-admin/config/locales/ro-RO.yml b/decidim-admin/config/locales/ro-RO.yml index 53722ea7f4527..402be4d0ea2ea 100644 --- a/decidim-admin/config/locales/ro-RO.yml +++ b/decidim-admin/config/locales/ro-RO.yml @@ -87,9 +87,6 @@ ro: welcome_notification_body: Conținutul notificării de bun venit welcome_notification_subject: Subiectul notificării de bun venit youtube_handler: Responsabil YouTube - participatory_space_private_user: - email: E-mail - name: Nume scope: code: Cod name: Nume @@ -131,10 +128,6 @@ ro: attributes: official_img_footer: allowed_file_content_types: Fișier invalid - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Fișier de import malformat, vă rugăm să citiți instrucțiunile cu atenție și asigurați-vă că fișierul este codificat UTF-8. user_group_csv_verification: attributes: file: @@ -172,8 +165,6 @@ ro: import: Importă newsletter: new: Buletin informativ nou - participatory_space_private_user: - new: Nou utilizator privat al spațiului participativ per_page: Pe pagină send_me_a_test_email: Trimite-mi un e-mail de test share: Distribuie @@ -363,17 +354,6 @@ ro: values: 'false': Verificat 'true': Neverificat - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Invitație acceptată - values: - 'false': Neacceptat - 'true': Acceptat - user_invitation_sent_at_not_null: - label: Invitaţie trimisă - values: - 'false': Nu s-a trimis - 'true': Trimis private_space_eq: label: Privat values: @@ -497,6 +477,10 @@ ro: explanation: Participanții gestionați pot fi promovați drept participanți standard. Înseamnă că vor fi invitați în aplicație și nu vei mai putea să îi gestionezi din nou. Participantul invitat va primi un e-mail pentru a accepta invitația ta. new_managed_user_promotion: Promovarea unui nou participant gestionat promote: Promovează + members_csv_imports: + new: + csv_upload: + title: Încarcă fișierul tău CSV menu: admin_log: Jurnal activitate admin admins: Administratori @@ -556,8 +540,6 @@ ro: sent_to: Trimis către subject: Subiect name: Buletin Informativ - participatory_space_private_user: - name: Participant privat pentru spațiul participativ scope: fields: name: Nume @@ -736,38 +718,6 @@ ro: update: error: A apărut o eroare la actualizarea acestei organizații. success: Organizația a fost actualizată cu succes. - participatory_space_private_users: - create: - error: A apărut o problemă la adăugarea unui participant privat pentru acest spațiu participativ. - success: Accesul la spațiul participativ a participantului privat a fost creat cu succes. - destroy: - error: A apărut o eroare la ștergerea unui participant privat pentru acest spațiu participativ. - success: Accesul la spațiul participativ a participantului privat a fost șters cu succes. - index: - import_via_csv: Importă din fișier CSV - title: Participant privat pentru spațiul participativ - new: - create: Crează - title: Participant privat nou pentru spațiul participativ. - participatory_space_private_users_csv_imports: - create: - invalid: A apărut o eroare la citirea fișierului CSV. Vă rugăm să vă asigurați că ați urmat instrucțiunile. - success: Fișier-ul CSV a fost încărcat cu succes, trimitem câte un e-mail participanților cu invitația. Acest lucru ar putea dura puțin timp. - new: - csv_upload: - title: Încarcă fișierul tău CSV - destroy: - button: Șterge toți participanții privați - confirm: Sunteți sigur că doriți să ștergeți toți participanții privați? Această acțiune nu poate fi anulată, și nu îi veți putea recupera. - empty: Nu aveți niciun participant privat. - explanation: Aveți %{count} participanți privați. - title: Ștergere participanți privați - example_file: 'Exemplu de fişier:' - explanation: 'Încărcă fişierul tău CSV. Trebuie să aibă două coloane cu e-mail în prima coloană a fișierului și numele în ultima coloană (e-mail, numele) pentru utilizatorii pe care doriţi să îi adăugaţi în spaţiul participativ, fără antet. Evită folosirea caracterelor invalide, cum ar fi `<>?%&^*#@()[]=+:;"{}\ ` în numele utilizatorului.' - explanation_example: | - ion.popescu@example.org%{csv_col_sep}Ion Popescu - maria.popescu@example.org%{csv_col_sep}Maria Popescu - upload: Încarcă reminders: create: error: A apărut o eroare la crearea de memento-uri. diff --git a/decidim-admin/config/locales/ru.yml b/decidim-admin/config/locales/ru.yml index 498957aabbba1..0129817d608d8 100644 --- a/decidim-admin/config/locales/ru.yml +++ b/decidim-admin/config/locales/ru.yml @@ -258,6 +258,10 @@ ru: explanation: Управляемых участников можно повышать до обычных участников. Это означает, что они будут приглашены в приложение, и вы не сможете выступать в их роли их снова. Приглашенный участник получит электронное письмо, чтобы принять ваше приглашение. new_managed_user_promotion: Повысить управляемого участника в статусе promote: Повысить + members_csv_imports: + new: + csv_upload: + title: Загрузите ваш CSV файл menu: admin_log: Журнал деятельности администратора admins: Администраторы @@ -311,8 +315,6 @@ ru: sent_to: Отправлено subject: Тема name: Рассылка новостей - participatory_space_private_user: - name: Частный участник пространства соучастия scope: fields: name: Имя @@ -410,21 +412,6 @@ ru: update: error: При попытке обновить эту организацию произошла ошибка. success: Организация успешно обновлена. - participatory_space_private_users: - create: - error: При попытке добавить частного участника в это пространство соучастия произошла ошибка. - success: Частным участникам успешно предоставлен доступ к пространству соучастия. - destroy: - success: Успешно отменен доступ частных участников к пространству соучастия. - index: - title: Частный участник пространства соучастия - new: - create: Добавить - title: Добавить частного участника пространства соучастия. - participatory_space_private_users_csv_imports: - new: - csv_upload: - title: Загрузите ваш CSV файл resource_permissions: edit: title: Редактировать права доступа diff --git a/decidim-admin/config/locales/sk.yml b/decidim-admin/config/locales/sk.yml index 1170b1aa5f4e1..6b4deeeb2ff46 100644 --- a/decidim-admin/config/locales/sk.yml +++ b/decidim-admin/config/locales/sk.yml @@ -273,6 +273,10 @@ sk: explanation: Spravovaní účastníci môžu byť povýšení na štandardných účastníkov. To znamená, že budú pozvaní do aplikácie, a už ich nebudete môcť spravovať. Pozvaný účastník obdrží e-mail s Vašou pozvánkou. new_managed_user_promotion: Povýšenie nového spravovaného účastníka promote: Povýšiť + members_csv_imports: + new: + csv_upload: + title: Nahrať súbor CSV menu: admin_log: Záznam aktivity administrátora admins: Administrátori @@ -326,8 +330,6 @@ sk: sent_to: Odoslané subject: Predmet name: Spravodaj - participatory_space_private_user: - name: Účastníci participatívnych procesov scope: fields: name: Meno @@ -446,23 +448,6 @@ sk: update: error: Pri aktualizácii organizácie došlo k chybe. success: Organizácia úspešne aktualizovaná. - participatory_space_private_users: - create: - error: Pri pridávaní súkromného používateľa pre tento participačný priestor došlo k chybe. - success: Participatívny priestor bol sprístupnený súkromnému užívateľovi. - destroy: - error: Pri vymazaní súkromného používateľa pre tento participačný priestor došlo k chybe. - success: Participatívny priestor bol zneprístupnený súkromnému užívateľovi. - index: - title: Súkromný účastník participatívneho priestoru - new: - create: Vytvoriť - title: Nový súkromný účastník participatívneho priestoru. - participatory_space_private_users_csv_imports: - new: - csv_upload: - title: Nahrať súbor CSV - upload: Nahrať resource_permissions: edit: submit: Potvrdiť diff --git a/decidim-admin/config/locales/sq-AL.yml b/decidim-admin/config/locales/sq-AL.yml index 703095d7e3956..cb3a3d11ded78 100644 --- a/decidim-admin/config/locales/sq-AL.yml +++ b/decidim-admin/config/locales/sq-AL.yml @@ -84,9 +84,6 @@ sq: welcome_notification_body: Përmbajtja e njoftimit të mirëseardhjes welcome_notification_subject: Tema e njoftimit të mirëseardhjes youtube_handler: Emri i përdoruesit Youtube - participatory_space_private_user: - email: Email - name: Emri scope: code: Kodi name: Emri @@ -238,17 +235,6 @@ sq: label: Tipi officialized_at_null: label: Gjendja - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Ftesa u pranua - values: - 'false': E papranuar - 'true': Pranuar - user_invitation_sent_at_not_null: - label: Ftesa u dërguan - values: - 'false': Nuk është dërguar - 'true': Dërguar private_space_eq: label: Private values: diff --git a/decidim-admin/config/locales/sr-CS.yml b/decidim-admin/config/locales/sr-CS.yml index 6624b183b1be9..cd7e6b4394d85 100644 --- a/decidim-admin/config/locales/sr-CS.yml +++ b/decidim-admin/config/locales/sr-CS.yml @@ -325,8 +325,6 @@ sr: sent_to: Poslato subject: Naslov name: Bilten - participatory_space_private_user: - name: Privatni učesnik prostora za diskusiju scope: fields: name: Ime diff --git a/decidim-admin/config/locales/sv.yml b/decidim-admin/config/locales/sv.yml index 1354f28b7297b..e6ded36e21757 100644 --- a/decidim-admin/config/locales/sv.yml +++ b/decidim-admin/config/locales/sv.yml @@ -33,6 +33,11 @@ sv: help_section: content: Innehåll id: ID + member: + email: E-post + name: Namn + member_csv_import: + file: Fil newsletter: body: Innehåll send_to_all_users: Skicka till alla deltagare @@ -90,11 +95,6 @@ sv: welcome_notification_body: Välkomstmeddelande welcome_notification_subject: Välkomstmeddelandets ämnesrad youtube_handler: YouTube-namn - participatory_space_private_user: - email: E-post - name: Namn - participatory_space_private_user_csv_import: - file: Fil scope: code: Kod name: Namn @@ -131,6 +131,10 @@ sv: file: Fil errors: models: + member_csv_import: + attributes: + file: + malformed: Felaktig importfil, läs igenom anvisningarna noga och se till att filen är UTF-8 kodad. newsletter: attributes: base: @@ -139,10 +143,6 @@ sv: attributes: official_img_footer: allowed_file_content_types: Ogiltig bildfil - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Fel format, läs igenom instruktionerna noga och se till att filen är UTF-8 kodad. user_group_csv_verification: attributes: file: @@ -190,12 +190,12 @@ sv: export: Exportera export-selection: Exportera markerade import: Importera + member: + new: Ny medlem menu_hidden: Dölj från menyn moderate: Hantera modereringar newsletter: new: Nytt nyhetsbrev - participatory_space_private_user: - new: Ny privat deltagare i deltagarutrymme per_page: Per sida permissions: Hantera behörigheter restore: Återställ @@ -417,6 +417,17 @@ sv: values: 'false': 'Nej' 'true': 'Ja' + members: + user_invitation_accepted_at_not_null: + label: Inbjudan accepterades + values: + 'false': Inte tackat ja + 'true': Tackat ja + user_invitation_sent_at_not_null: + label: Inbjudan har skickats + values: + 'false': Inte skickat + 'true': Skickad moderated_users: reports_reason_eq: label: Anledning till anmälan @@ -432,17 +443,6 @@ sv: values: 'false': Har officiell status 'true': Ej gjord officiell - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Inbjudan accepterades - values: - 'false': Ej godkänd - 'true': Godkänd - user_invitation_sent_at_not_null: - label: Inbjudan skickad - values: - 'false': Ej skickad - 'true': Skickad private_space_eq: label: Privat values: @@ -575,6 +575,13 @@ sv: explanation: Hanterade deltagare kan befordras till vanliga deltagare. Det innebär att de kommer att bli inbjudna till programmet och du kommer inte att kunna hantera dem igen. Den inbjudna deltagaren får ett e-brev om att godkänna inbjudan. new_managed_user_promotion: Befordra ny hanterad användare promote: Befordra + members: + create: + error: Det gick inte att lägga till en deltagare till processen. + members_csv_imports: + new: + csv_upload: + title: Skicka CSV-fil menu: admin_log: Aktivitetslogg admins: Administratörer @@ -640,8 +647,6 @@ sv: sent_to: Skickat till subject: Ämne name: Nyhetsbrev - participatory_space_private_user: - name: Deltagare i privat deltagarutrymme scope: fields: name: Namn @@ -916,53 +921,6 @@ sv: form: add: Lägg till i lista över tillåtna domännamn title: Tillåtna externa domäner - participatory_space_private_users: - create: - error: Det gick inte att lägga till en privat deltagare i deltagarutrymmet. - success: Åtkomst för privat deltagare till deltagarutrymmet har skapats. - destroy: - error: Det gick inte att ta bort en privat deltagare från deltagarutrymmet. - success: Åtkomsten för privat deltagare till deltagarutrymmet har tagits bort. - edit: - title: Redigera privat deltagare i deltagarutrymme. - update: Uppdatera - index: - import_via_csv: Importera via CSV - publish_all: Publicera alla - title: Privat deltagare i deltagarutrymme - unpublish_all: Avpublicera alla - new: - create: Skapa - title: Ny privat deltagare i deltagarutrymme. - publish_all: - error: Det gick inte att publicera alla privata deltagare i deltagarutrymmet. - success: Publicerade alla privata deltagare för deltagarutrymmet - unpublish_all: - error: Det gick inte att avpublicera alla privata deltagare i deltagarutrymmet. - success: Avpublicerade alla privata deltagare för deltagarutrymmet - update: - error: Det gick inte att uppdatera den privata deltagaren i deltagarutrymmet. - success: Privat deltagare i deltagarutrymmet har uppdaterats - participatory_space_private_users_csv_imports: - create: - invalid: Det gick inte att läsa CSV-filen. Kontrollera att du har följt instruktionerna. - success: CSV-filen har laddats upp och vi skickar en inbjudan med e-post till deltagarna. Det kan ta en stund. - new: - csv_upload: - title: Ladda upp din CSV-fil - destroy: - button: Radera alla privata deltagare - confirm: Är du säker på att du vill radera alla privata deltagare? Den här åtgärden kan inte ångras. - empty: Det finns inga privata deltagare. - explanation: Det finns %{count} privata deltagare. - title: Radera privata deltagare - example_file: 'Exempelfil:' - explanation: 'Skicka din CSV-fil. Den behöver ha två kolumner för användare som du vill lägga till, utan rubriker, med e-postadressen i den första kolumnen och namnet i den sista kolumnen. Undvik specialtecken som `<>?%&^*#@()[]=+:;"{}\|` i användarnamn.' - explanation_example: | - jon.andersson@example.org%{csv_col_sep}Jon Andersson - jenny.andersson@example.org%{csv_col_sep}Jenny Andersson - title: Importera privata deltagare från en CSV-fil - upload: Skicka reminders: create: error: Det gick inte att skapa påminnelser. diff --git a/decidim-admin/config/locales/th-TH.yml b/decidim-admin/config/locales/th-TH.yml index 0bd1df7874978..1f8fb82a154a2 100644 --- a/decidim-admin/config/locales/th-TH.yml +++ b/decidim-admin/config/locales/th-TH.yml @@ -45,9 +45,6 @@ th: welcome_notification_body: ยินดีต้อนรับเนื้อหาการแจ้งเตือน welcome_notification_subject: ยินดีต้อนรับเรื่องการแจ้งเตือน youtube_handler: ตัวจัดการ YouTube - participatory_space_private_user: - email: อีเมล - name: ชื่อ scope: code: รหัส name: ชื่อ diff --git a/decidim-admin/config/locales/tr-TR.yml b/decidim-admin/config/locales/tr-TR.yml index 2a0f1186da356..d016e20f947d6 100644 --- a/decidim-admin/config/locales/tr-TR.yml +++ b/decidim-admin/config/locales/tr-TR.yml @@ -90,11 +90,6 @@ tr: welcome_notification_body: Karşılama bildirimi gövdesi welcome_notification_subject: Karşılama bildirimi konusu youtube_handler: YouTube işleyici - participatory_space_private_user: - email: E-Posta - name: Adı - participatory_space_private_user_csv_import: - file: Dosya scope: code: kod name: isim @@ -136,10 +131,6 @@ tr: attributes: official_img_footer: allowed_file_content_types: Geçersiz resim dosyası - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Bozuk içe aktarım dosyası, lütfen yönergeleri dikkatlice gözden geçirin ve dosyanın UTF-8 ile kodlandığından emin olun. user_group_csv_verification: attributes: file: @@ -186,8 +177,6 @@ tr: menu_hidden: Menüden gizle newsletter: new: Yeni bülten - participatory_space_private_user: - new: Yeni katılımcı alanı özel kullanıcısı per_page: Sayfa başına restore: Geri yükle send_me_a_test_email: Bana bir test e-postası gönder @@ -416,17 +405,6 @@ tr: values: 'false': resmileştirilmiş 'true': Resmi değil - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Davet kabul edildi - values: - 'false': Kabul edilmedi - 'true': Kabul Edildi - user_invitation_sent_at_not_null: - label: Davet gönderildi - values: - 'false': Gönderilmedi - 'true': Gönderildi private_space_eq: label: Kişisel values: @@ -552,6 +530,10 @@ tr: explanation: Yönetilen kullanıcılar standart kullanıcılara yükseltilebilir. Bu, uygulamaya davet edilecekleri anlamına gelir ve onları tekrar taklit edemezsiniz. Davet edilen kullanıcı davetinizi kabul etmek için bir e-posta alacak. new_managed_user_promotion: Yeni yönetilen kullanıcı tanıtımı promote: Desteklemek + members_csv_imports: + new: + csv_upload: + title: CSV dosyanızı yükleyin menu: admin_log: Yönetici etkinlik günlüğü admins: Yöneticiler @@ -617,8 +599,6 @@ tr: sent_to: Gönderilen subject: konu name: Bülten - participatory_space_private_user: - name: Katılımcı alan özel kullanıcısı scope: fields: name: isim @@ -841,39 +821,6 @@ tr: form: add: İzin verilenler listesine ekle title: İzin verilen harici alan adları - participatory_space_private_users: - create: - error: Bu katılımcı alan için özel bir kullanıcı eklenirken bir hata oluştu. - success: Katılımcı alan özel kullanıcı erişimi başarıyla oluşturuldu. - destroy: - error: Bu katılımcı alan için özel bir kullanıcı silinirken bir hata oluştu. - success: Katılımcı alan özel kullanıcı erişimi başarıyla yok edildi. - edit: - title: Katılımcı alanı özel katılımcılarını düzenle. - update: Güncelle - index: - import_via_csv: CSV'den içe aktar - publish_all: Tümünü yayımla - title: Katılımcı alan özel kullanıcısı - unpublish_all: Tümünü yayımdan kaldır - new: - create: yaratmak - title: Yeni Katılımcı Uzay özel kullanıcısı. - publish_all: - success: Bu katılımcı alan için özel katılımcılar başarıyla yayınlandı - participatory_space_private_users_csv_imports: - create: - success: CSV dosyası başarıyla yüklendi, katılımcılara bir davet e-postası gönderiyoruz. Bu biraz zaman alabilir. - new: - csv_upload: - title: CSV dosyanızı yükleyin - destroy: - button: Tüm özel kullanıcıları sil - confirm: Tüm özel kullanıcıları silmek istediğinizden emin misiniz? Bu işlem geri alınamaz, silinen katılımcıları geri yükleyemeyeceksiniz. - empty: Özel kullanıcınız yok. - title: Özel Kullanıcıları kaldır - example_file: 'Örnek dosya:' - upload: Yükle reminders: new: submit: Gönder diff --git a/decidim-admin/config/locales/uk.yml b/decidim-admin/config/locales/uk.yml index 3067fc0780b3e..26c1405a55c2b 100644 --- a/decidim-admin/config/locales/uk.yml +++ b/decidim-admin/config/locales/uk.yml @@ -281,8 +281,6 @@ uk: sent_at: 'Надіслане:' subject: Тема name: Розсилання новин - participatory_space_private_user: - name: Приватний учасник простору співучасті scope: fields: name: Ім'я @@ -371,17 +369,6 @@ uk: update: error: При спробі оновити цю організацію сталася помилка. success: Організацію успішно оновлено. - participatory_space_private_users: - create: - error: При спробі додати приватного учасника до цього простору співучасті сталася помилка. - success: Приватним учасникам успішно надано доступ до простору співучасті. - destroy: - success: Успішно скасовано доступ приватних учасників до простору співучасті. - index: - title: Приватний учасник простору співучасті - new: - create: Додати - title: Додати приватного учасника простору співучасті. resource_permissions: edit: title: Редагувати права доступу diff --git a/decidim-admin/config/locales/zh-CN.yml b/decidim-admin/config/locales/zh-CN.yml index 0ee57f734f51a..28028f6f19d77 100644 --- a/decidim-admin/config/locales/zh-CN.yml +++ b/decidim-admin/config/locales/zh-CN.yml @@ -286,6 +286,10 @@ zh-CN: explanation: 管理下的参与者可以提升到标准参与者。 这意味着他们将被邀请到应用程序中,您将无法再次管理他们。 邀请的参与者将收到一封电子邮件来接受您的邀请。 new_managed_user_promotion: 新管理的参与者促销活动 promote: 升级 + members_csv_imports: + new: + csv_upload: + title: 上传您的 CSV 文件 menu: admin_log: 管理员活动日志 admins: 管理员 @@ -339,8 +343,6 @@ zh-CN: sent_to: 发送至 subject: 议 题 name: 通讯 - participatory_space_private_user: - name: 参与性空间私人参与者 scope: fields: name: 名称 @@ -462,23 +464,6 @@ zh-CN: update: error: 更新这个组织时出现问题。 success: 组织更新成功。 - participatory_space_private_users: - create: - error: 在这种参与空间中添加私人参与者时出现了问题。 - success: 参与性空间私人参与者访问成功创建。 - destroy: - error: 删除这个参与空间的私人参与者时出错。 - success: 参与性空间私人参与者访问成功被摧毁。 - index: - title: 参与性空间私人参与者 - new: - create: 创建 - title: 新的参与性空间活动私人参与者。 - participatory_space_private_users_csv_imports: - new: - csv_upload: - title: 上传您的 CSV 文件 - upload: 上传 resource_permissions: edit: submit: 提交 diff --git a/decidim-admin/config/locales/zh-TW.yml b/decidim-admin/config/locales/zh-TW.yml index 485301629fb85..014fe190af880 100644 --- a/decidim-admin/config/locales/zh-TW.yml +++ b/decidim-admin/config/locales/zh-TW.yml @@ -85,9 +85,6 @@ zh-TW: welcome_notification_body: 歡迎通知內容 welcome_notification_subject: 歡迎通知主旨 youtube_handler: YouTube 處理器 - participatory_space_private_user: - email: 電子郵件 - name: 名稱 scope: code: 代碼 name: 名稱 @@ -128,10 +125,6 @@ zh-TW: attributes: official_img_footer: allowed_file_content_types: 圖片有誤 - participatory_space_private_user_csv_import: - attributes: - file: - malformed: 匯入檔案格式有誤,請小心閱讀教學並且確認檔案是使用 UTF-8 編碼 user_group_csv_verification: attributes: file: @@ -169,8 +162,6 @@ zh-TW: import: 匯入 newsletter: new: 新電子報 - participatory_space_private_user: - new: 新參與空間私有使用者 per_page: 每頁 share: 分享 user: @@ -355,17 +346,6 @@ zh-TW: values: 'false': 官方化 'true': 非官方化 - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: 邀請已接受 - values: - 'false': 未接受 - 'true': 已接受 - user_invitation_sent_at_not_null: - label: 已發送邀請 - values: - 'false': 未發送 - 'true': 已送出 private_space_eq: label: 私人的 values: @@ -479,6 +459,10 @@ zh-TW: explanation: 受管理的參與者可以晉升為標準參與者。這意味著他們將受邀加入應用程式,您將無法再對其進行管理。被邀請的參與者將收到一封電子郵件,以接受您的邀請。 new_managed_user_promotion: 晉升新的受管理參與者 promote: 推廣 + members_csv_imports: + new: + csv_upload: + title: 請上傳您的 CSV 檔案。 menu: admin_log: 管理員活動日誌 admins: 管理員 @@ -536,8 +520,6 @@ zh-TW: sent_to: 發送至 subject: 主旨 name: 電子報 - participatory_space_private_user: - name: 參與空間私有參與者 scope: fields: name: 名稱 @@ -715,38 +697,6 @@ zh-TW: update: error: 更新此組織時發生問題。 success: 組織更新成功 - participatory_space_private_users: - create: - error: 在此參與空間中添加私人參與者時出現問題。 - success: 成功建立參與空間的私人參與者訪問權限。 - destroy: - error: 刪除參與空間的私人參與者時發生問題。 - success: 成功刪除參與空間的私人參與者訪問權限。 - index: - import_via_csv: 透過 CSV 匯入 - title: 參與空間私有參與者 - new: - create: 建立 - title: 新的參與空間私人參與者。 - participatory_space_private_users_csv_imports: - create: - invalid: 讀取 CSV 檔案時發生問題。請確保您已按照指示進行操作。 - success: CSV檔案上傳成功,我們正在發送邀請電子郵件給參與者。這可能需要一些時間。 - new: - csv_upload: - title: 請上傳您的 CSV 檔案。 - destroy: - button: 刪除所有私人參與者 - confirm: 您確定要刪除所有私人參與者嗎?此操作無法撤銷,您將無法恢復它們。 - empty: 您沒有私人參與者。 - explanation: 您有 %{count} 位私人參與者。 - title: 刪除私人參與者 - example_file: '示例文件:' - explanation: '請上傳您的 CSV 檔案。檔案的第一欄應為電子郵件,最後一欄應為要添加到參與空間的使用者的名稱,檔案中不應包含標題。請避免在使用者名稱中使用無效字元,例如 <>?%&^*#@()[]=+:;"{}\|。' - explanation_example: | - john.doe@example.org%{csv_col_sep}John Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - upload: 上傳 reminders: create: error: 在創建提醒時遇到問題。 diff --git a/decidim-api/config/locales/am-ET.yml b/decidim-api/config/locales/am-ET.yml new file mode 100644 index 0000000000000..9e7679c0df56d --- /dev/null +++ b/decidim-api/config/locales/am-ET.yml @@ -0,0 +1 @@ +am: diff --git a/decidim-api/config/locales/ar.yml b/decidim-api/config/locales/ar.yml new file mode 100644 index 0000000000000..c257bc08a4aef --- /dev/null +++ b/decidim-api/config/locales/ar.yml @@ -0,0 +1 @@ +ar: diff --git a/decidim-api/config/locales/bg.yml b/decidim-api/config/locales/bg.yml new file mode 100644 index 0000000000000..d0e375da96f10 --- /dev/null +++ b/decidim-api/config/locales/bg.yml @@ -0,0 +1 @@ +bg: diff --git a/decidim-api/config/locales/bn-BD.yml b/decidim-api/config/locales/bn-BD.yml new file mode 100644 index 0000000000000..152c698290639 --- /dev/null +++ b/decidim-api/config/locales/bn-BD.yml @@ -0,0 +1 @@ +bn: diff --git a/decidim-api/config/locales/bs-BA.yml b/decidim-api/config/locales/bs-BA.yml new file mode 100644 index 0000000000000..e9e174462a158 --- /dev/null +++ b/decidim-api/config/locales/bs-BA.yml @@ -0,0 +1 @@ +bs: diff --git a/decidim-api/config/locales/ca-IT.yml b/decidim-api/config/locales/ca-IT.yml new file mode 100644 index 0000000000000..752dd3586d6da --- /dev/null +++ b/decidim-api/config/locales/ca-IT.yml @@ -0,0 +1,12 @@ +--- +ca-IT: + decidim: + api: + errors: + invalid_locale: La configuració regional que s'ha facilitat no és vàlida + locale_argument_error: S'ha produït un error mentre es gestionaven internament les dades i18n + not_found: "%{type} no trobat" + permission_not_set: No s'ha establert el permís per aquest %{type} + unauthorized_field: No pots veure o editar el camp %{field} a %{type} perquè no tens permisos per fer-ho + unauthorized_mutation: No tens permís per realitzar aquesta mutació + unauthorized_object: No pots veure i editar aquest %{type} perquè no tens permisos per fer-ho diff --git a/decidim-api/config/locales/ca.yml b/decidim-api/config/locales/ca.yml new file mode 100644 index 0000000000000..9bd05552d6c06 --- /dev/null +++ b/decidim-api/config/locales/ca.yml @@ -0,0 +1,12 @@ +--- +ca: + decidim: + api: + errors: + invalid_locale: La configuració regional que s'ha facilitat no és vàlida + locale_argument_error: S'ha produït un error mentre es gestionaven internament les dades i18n + not_found: "%{type} no trobat" + permission_not_set: No s'ha establert el permís per aquest %{type} + unauthorized_field: No pots veure o editar el camp %{field} a %{type} perquè no tens permisos per fer-ho + unauthorized_mutation: No tens permís per realitzar aquesta mutació + unauthorized_object: No pots veure i editar aquest %{type} perquè no tens permisos per fer-ho diff --git a/decidim-api/config/locales/cs.yml b/decidim-api/config/locales/cs.yml new file mode 100644 index 0000000000000..11acc57135a6b --- /dev/null +++ b/decidim-api/config/locales/cs.yml @@ -0,0 +1 @@ +cs: diff --git a/decidim-api/config/locales/da.yml b/decidim-api/config/locales/da.yml new file mode 100644 index 0000000000000..347c94d5e3836 --- /dev/null +++ b/decidim-api/config/locales/da.yml @@ -0,0 +1 @@ +da: diff --git a/decidim-api/config/locales/de.yml b/decidim-api/config/locales/de.yml new file mode 100644 index 0000000000000..346523bb60ba7 --- /dev/null +++ b/decidim-api/config/locales/de.yml @@ -0,0 +1 @@ +de: diff --git a/decidim-api/config/locales/el.yml b/decidim-api/config/locales/el.yml new file mode 100644 index 0000000000000..419ec705c3e6d --- /dev/null +++ b/decidim-api/config/locales/el.yml @@ -0,0 +1 @@ +el: diff --git a/decidim-api/config/locales/eo.yml b/decidim-api/config/locales/eo.yml new file mode 100644 index 0000000000000..7599814048735 --- /dev/null +++ b/decidim-api/config/locales/eo.yml @@ -0,0 +1 @@ +eo: diff --git a/decidim-api/config/locales/es-MX.yml b/decidim-api/config/locales/es-MX.yml new file mode 100644 index 0000000000000..a4fa82d23d4ad --- /dev/null +++ b/decidim-api/config/locales/es-MX.yml @@ -0,0 +1,12 @@ +--- +es-MX: + decidim: + api: + errors: + invalid_locale: Se ha proporcionado una configuración regional no válida + locale_argument_error: Se ha producido un error mientras se gestionaban internamente los datos de i18n + not_found: "%{type} no encontrado" + permission_not_set: No se ha establecido el permiso para este %{type} + unauthorized_field: No puedes ver o editar el campo %{field} en %{type} porque no tienes permiso + unauthorized_mutation: No tienes permiso para realizar esta mutación + unauthorized_object: No puedes ver o editar este %{type} porque no tienes permisos diff --git a/decidim-api/config/locales/es-PY.yml b/decidim-api/config/locales/es-PY.yml new file mode 100644 index 0000000000000..dece8e977a39e --- /dev/null +++ b/decidim-api/config/locales/es-PY.yml @@ -0,0 +1,12 @@ +--- +es-PY: + decidim: + api: + errors: + invalid_locale: Se ha proporcionado una configuración regional no válida + locale_argument_error: Se ha producido un error mientras se gestionaban internamente los datos de i18n + not_found: "%{type} no encontrado" + permission_not_set: No se ha establecido el permiso para este %{type} + unauthorized_field: No puedes ver o editar el campo %{field} en %{type} porque no tienes permiso + unauthorized_mutation: No tienes permiso para realizar esta mutación + unauthorized_object: No puedes ver o editar este %{type} porque no tienes permisos diff --git a/decidim-api/config/locales/es.yml b/decidim-api/config/locales/es.yml new file mode 100644 index 0000000000000..814f4a5a64e64 --- /dev/null +++ b/decidim-api/config/locales/es.yml @@ -0,0 +1,12 @@ +--- +es: + decidim: + api: + errors: + invalid_locale: Se ha proporcionado una configuración regional no válida + locale_argument_error: Se ha producido un error mientras se gestionaban internamente los datos de i18n + not_found: "%{type} no encontrado" + permission_not_set: No se ha establecido el permiso para este %{type} + unauthorized_field: No puedes ver o editar el campo %{field} en %{type} porque no tienes permiso + unauthorized_mutation: No tienes permiso para realizar esta mutación + unauthorized_object: No puedes ver o editar este %{type} porque no tienes permisos diff --git a/decidim-api/config/locales/et.yml b/decidim-api/config/locales/et.yml new file mode 100644 index 0000000000000..e020c4ffc443d --- /dev/null +++ b/decidim-api/config/locales/et.yml @@ -0,0 +1 @@ +et: diff --git a/decidim-api/config/locales/eu.yml b/decidim-api/config/locales/eu.yml new file mode 100644 index 0000000000000..ec3152d82fb60 --- /dev/null +++ b/decidim-api/config/locales/eu.yml @@ -0,0 +1,12 @@ +--- +eu: + decidim: + api: + errors: + invalid_locale: Baliozkoa ez den eskualde-konfigurazioa eman da + locale_argument_error: Errorea gertatu da i18n datuen barne-kudeaketa egiten zen bitartean + not_found: "%{mota} ez da aurkitu" + permission_not_set: Ez da baimenik ezarri %{type} honetarako + unauthorized_field: Ezin duzu editatu %{field} eremua hemen %{type} ez daukazulako baimenik + unauthorized_mutation: Ez duzu baimenik aldaketa hau egiteko + unauthorized_object: Ezin duzu ikusi edo editatu %{field} eremua ez daukazulako baimenik diff --git a/decidim-api/config/locales/fa-IR.yml b/decidim-api/config/locales/fa-IR.yml new file mode 100644 index 0000000000000..88215f82cbd5d --- /dev/null +++ b/decidim-api/config/locales/fa-IR.yml @@ -0,0 +1 @@ +fa: diff --git a/decidim-api/config/locales/fi-plain.yml b/decidim-api/config/locales/fi-plain.yml new file mode 100644 index 0000000000000..c5d054ef83604 --- /dev/null +++ b/decidim-api/config/locales/fi-plain.yml @@ -0,0 +1,12 @@ +--- +fi-pl: + decidim: + api: + errors: + invalid_locale: Virheellinen kielivalinta + locale_argument_error: Lokalisointitietojen käsittelyssä tapahtui sisäinen virhetilanne + not_found: "%{type} ei löytynyt" + permission_not_set: Oikeuksia ei ole asetettu tyypille %{type} + unauthorized_field: Sinulla ei ole oikeuksia tarkastella tai muokata kenttää %{field} tyypille %{type} + unauthorized_mutation: Sinulla ei ole oikeutta tämän muutospyynnön suorittamiseen + unauthorized_object: Sinulla ei ole oikeuksia tarkastella tai muokata tyyppiä %{type} diff --git a/decidim-api/config/locales/fi.yml b/decidim-api/config/locales/fi.yml new file mode 100644 index 0000000000000..9b6e8b01dcbae --- /dev/null +++ b/decidim-api/config/locales/fi.yml @@ -0,0 +1,12 @@ +--- +fi: + decidim: + api: + errors: + invalid_locale: Virheellinen kielivalinta + locale_argument_error: Lokalisointitietojen käsittelyssä tapahtui sisäinen virhetilanne + not_found: "%{type} ei löytynyt" + permission_not_set: Oikeuksia ei ole asetettu tyypille %{type} + unauthorized_field: Sinulla ei ole oikeuksia tarkastella tai muokata kenttää %{field} tyypille %{type} + unauthorized_mutation: Sinulla ei ole oikeutta tämän muutospyynnön suorittamiseen + unauthorized_object: Sinulla ei ole oikeuksia tarkastella tai muokata tyyppiä %{type} diff --git a/decidim-api/config/locales/fr-CA.yml b/decidim-api/config/locales/fr-CA.yml new file mode 100644 index 0000000000000..b6f2e88140080 --- /dev/null +++ b/decidim-api/config/locales/fr-CA.yml @@ -0,0 +1,9 @@ +--- +fr-CA: + decidim: + api: + errors: + not_found: "%{type} introuvable" + permission_not_set: La permission n'a pas été définie pour ce %{type} + unauthorized_field: Vous ne pouvez pas afficher ou modifier le champ %{field} sur %{type} car vous n'avez pas la permission + unauthorized_object: Vous ne pouvez pas afficher ou modifier ce %{type} car vous n'avez pas les permissions requises diff --git a/decidim-api/config/locales/fr.yml b/decidim-api/config/locales/fr.yml new file mode 100644 index 0000000000000..46ba0fa4c5177 --- /dev/null +++ b/decidim-api/config/locales/fr.yml @@ -0,0 +1,9 @@ +--- +fr: + decidim: + api: + errors: + not_found: "%{type} introuvable" + permission_not_set: La permission n'a pas été définie pour ce %{type} + unauthorized_field: Vous ne pouvez pas afficher ou modifier le champ %{field} sur %{type} car vous n'avez pas la permission + unauthorized_object: Vous ne pouvez pas afficher ou modifier ce %{type} car vous n'avez pas les permissions requises diff --git a/decidim-api/config/locales/ga-IE.yml b/decidim-api/config/locales/ga-IE.yml new file mode 100644 index 0000000000000..20a9da24e96f1 --- /dev/null +++ b/decidim-api/config/locales/ga-IE.yml @@ -0,0 +1 @@ +ga: diff --git a/decidim-api/config/locales/gl.yml b/decidim-api/config/locales/gl.yml new file mode 100644 index 0000000000000..8ec5fc81c180b --- /dev/null +++ b/decidim-api/config/locales/gl.yml @@ -0,0 +1 @@ +gl: diff --git a/decidim-api/config/locales/gn-PY.yml b/decidim-api/config/locales/gn-PY.yml new file mode 100644 index 0000000000000..bd442b0ad85de --- /dev/null +++ b/decidim-api/config/locales/gn-PY.yml @@ -0,0 +1 @@ +gn: diff --git a/decidim-api/config/locales/he-IL.yml b/decidim-api/config/locales/he-IL.yml new file mode 100644 index 0000000000000..af6fa60a73f2c --- /dev/null +++ b/decidim-api/config/locales/he-IL.yml @@ -0,0 +1 @@ +he: diff --git a/decidim-api/config/locales/hr.yml b/decidim-api/config/locales/hr.yml new file mode 100644 index 0000000000000..f67f33c7e0cfc --- /dev/null +++ b/decidim-api/config/locales/hr.yml @@ -0,0 +1 @@ +hr: diff --git a/decidim-api/config/locales/hu.yml b/decidim-api/config/locales/hu.yml new file mode 100644 index 0000000000000..52314c50c979f --- /dev/null +++ b/decidim-api/config/locales/hu.yml @@ -0,0 +1 @@ +hu: diff --git a/decidim-api/config/locales/id-ID.yml b/decidim-api/config/locales/id-ID.yml new file mode 100644 index 0000000000000..8446cbad90a58 --- /dev/null +++ b/decidim-api/config/locales/id-ID.yml @@ -0,0 +1 @@ +id: diff --git a/decidim-api/config/locales/is-IS.yml b/decidim-api/config/locales/is-IS.yml new file mode 100644 index 0000000000000..1fd0783ddf221 --- /dev/null +++ b/decidim-api/config/locales/is-IS.yml @@ -0,0 +1 @@ +is-IS: diff --git a/decidim-api/config/locales/it.yml b/decidim-api/config/locales/it.yml new file mode 100644 index 0000000000000..85830635a7b97 --- /dev/null +++ b/decidim-api/config/locales/it.yml @@ -0,0 +1 @@ +it: diff --git a/decidim-api/config/locales/ja.yml b/decidim-api/config/locales/ja.yml new file mode 100644 index 0000000000000..ec66879db255d --- /dev/null +++ b/decidim-api/config/locales/ja.yml @@ -0,0 +1,9 @@ +--- +ja: + decidim: + api: + errors: + not_found: "%{type} が見つかりません" + permission_not_set: この %{type} の権限が設定されていません + unauthorized_field: 権限がないため、 %{field} の %{type} フィールドを表示または編集することはできません + unauthorized_object: 権限がないため、この %{type} を表示または編集できません diff --git a/decidim-api/config/locales/ka-GE.yml b/decidim-api/config/locales/ka-GE.yml new file mode 100644 index 0000000000000..57a95cb04703c --- /dev/null +++ b/decidim-api/config/locales/ka-GE.yml @@ -0,0 +1 @@ +ka: diff --git a/decidim-api/config/locales/kaa.yml b/decidim-api/config/locales/kaa.yml new file mode 100644 index 0000000000000..455cb565ea313 --- /dev/null +++ b/decidim-api/config/locales/kaa.yml @@ -0,0 +1 @@ +kaa: diff --git a/decidim-api/config/locales/ko.yml b/decidim-api/config/locales/ko.yml new file mode 100644 index 0000000000000..8a7b3b861deda --- /dev/null +++ b/decidim-api/config/locales/ko.yml @@ -0,0 +1 @@ +ko: diff --git a/decidim-api/config/locales/lb.yml b/decidim-api/config/locales/lb.yml new file mode 100644 index 0000000000000..823df018114f4 --- /dev/null +++ b/decidim-api/config/locales/lb.yml @@ -0,0 +1 @@ +lb: diff --git a/decidim-api/config/locales/lo-LA.yml b/decidim-api/config/locales/lo-LA.yml new file mode 100644 index 0000000000000..27a02bfece429 --- /dev/null +++ b/decidim-api/config/locales/lo-LA.yml @@ -0,0 +1 @@ +lo: diff --git a/decidim-api/config/locales/lt.yml b/decidim-api/config/locales/lt.yml new file mode 100644 index 0000000000000..6c5cb837ac8c1 --- /dev/null +++ b/decidim-api/config/locales/lt.yml @@ -0,0 +1 @@ +lt: diff --git a/decidim-api/config/locales/lv.yml b/decidim-api/config/locales/lv.yml new file mode 100644 index 0000000000000..1be0eabc09156 --- /dev/null +++ b/decidim-api/config/locales/lv.yml @@ -0,0 +1 @@ +lv: diff --git a/decidim-api/config/locales/mt.yml b/decidim-api/config/locales/mt.yml new file mode 100644 index 0000000000000..f7aabc7149a9b --- /dev/null +++ b/decidim-api/config/locales/mt.yml @@ -0,0 +1 @@ +mt: diff --git a/decidim-api/config/locales/nl.yml b/decidim-api/config/locales/nl.yml new file mode 100644 index 0000000000000..f009eadee5442 --- /dev/null +++ b/decidim-api/config/locales/nl.yml @@ -0,0 +1 @@ +nl: diff --git a/decidim-api/config/locales/no.yml b/decidim-api/config/locales/no.yml new file mode 100644 index 0000000000000..205c36aa3438a --- /dev/null +++ b/decidim-api/config/locales/no.yml @@ -0,0 +1,6 @@ +--- +"no": + decidim: + api: + errors: + not_found: "%{type} ikke funnet" diff --git a/decidim-api/config/locales/oc-FR.yml b/decidim-api/config/locales/oc-FR.yml new file mode 100644 index 0000000000000..325b348894124 --- /dev/null +++ b/decidim-api/config/locales/oc-FR.yml @@ -0,0 +1 @@ +oc: diff --git a/decidim-api/config/locales/om-ET.yml b/decidim-api/config/locales/om-ET.yml new file mode 100644 index 0000000000000..05e2e89c3a879 --- /dev/null +++ b/decidim-api/config/locales/om-ET.yml @@ -0,0 +1 @@ +om: diff --git a/decidim-api/config/locales/pl.yml b/decidim-api/config/locales/pl.yml new file mode 100644 index 0000000000000..a8e4dde70d6fb --- /dev/null +++ b/decidim-api/config/locales/pl.yml @@ -0,0 +1 @@ +pl: diff --git a/decidim-api/config/locales/pt-BR.yml b/decidim-api/config/locales/pt-BR.yml new file mode 100644 index 0000000000000..21460cdedbdf8 --- /dev/null +++ b/decidim-api/config/locales/pt-BR.yml @@ -0,0 +1 @@ +pt-BR: diff --git a/decidim-api/config/locales/pt.yml b/decidim-api/config/locales/pt.yml new file mode 100644 index 0000000000000..9cbe1f038722e --- /dev/null +++ b/decidim-api/config/locales/pt.yml @@ -0,0 +1 @@ +pt: diff --git a/decidim-api/config/locales/ro-RO.yml b/decidim-api/config/locales/ro-RO.yml new file mode 100644 index 0000000000000..79dbaa871cae8 --- /dev/null +++ b/decidim-api/config/locales/ro-RO.yml @@ -0,0 +1 @@ +ro: diff --git a/decidim-api/config/locales/ru.yml b/decidim-api/config/locales/ru.yml new file mode 100644 index 0000000000000..ddc9d1e32c29b --- /dev/null +++ b/decidim-api/config/locales/ru.yml @@ -0,0 +1 @@ +ru: diff --git a/decidim-api/config/locales/si-LK.yml b/decidim-api/config/locales/si-LK.yml new file mode 100644 index 0000000000000..b0b50956edd26 --- /dev/null +++ b/decidim-api/config/locales/si-LK.yml @@ -0,0 +1 @@ +si: diff --git a/decidim-api/config/locales/sk.yml b/decidim-api/config/locales/sk.yml new file mode 100644 index 0000000000000..f634a02824398 --- /dev/null +++ b/decidim-api/config/locales/sk.yml @@ -0,0 +1 @@ +sk: diff --git a/decidim-api/config/locales/sl.yml b/decidim-api/config/locales/sl.yml new file mode 100644 index 0000000000000..26c7ce2e31e0e --- /dev/null +++ b/decidim-api/config/locales/sl.yml @@ -0,0 +1 @@ +sl: diff --git a/decidim-api/config/locales/so-SO.yml b/decidim-api/config/locales/so-SO.yml new file mode 100644 index 0000000000000..11720879bac3d --- /dev/null +++ b/decidim-api/config/locales/so-SO.yml @@ -0,0 +1 @@ +so: diff --git a/decidim-api/config/locales/sq-AL.yml b/decidim-api/config/locales/sq-AL.yml new file mode 100644 index 0000000000000..44ddadc95a8f3 --- /dev/null +++ b/decidim-api/config/locales/sq-AL.yml @@ -0,0 +1 @@ +sq: diff --git a/decidim-api/config/locales/sr-CS.yml b/decidim-api/config/locales/sr-CS.yml new file mode 100644 index 0000000000000..9e26af81914fc --- /dev/null +++ b/decidim-api/config/locales/sr-CS.yml @@ -0,0 +1 @@ +sr: diff --git a/decidim-api/config/locales/sv.yml b/decidim-api/config/locales/sv.yml new file mode 100644 index 0000000000000..6e4bfa32b1619 --- /dev/null +++ b/decidim-api/config/locales/sv.yml @@ -0,0 +1,12 @@ +--- +sv: + decidim: + api: + errors: + invalid_locale: Ogiltigt språk angivet + locale_argument_error: Ett fel uppstod när i18n data hanterades internt + not_found: "%{type} hittades inte" + permission_not_set: Behörigheten har inte satts för denna %{type} + unauthorized_field: Du kan inte visa eller redigera %{field} fältet på %{type} eftersom du inte har behörighet + unauthorized_mutation: Du har inte behörighet att utföra denna åtgärd + unauthorized_object: Du kan inte visa eller redigera detta %{type} eftersom du inte har behörigheter diff --git a/decidim-api/config/locales/sw-KE.yml b/decidim-api/config/locales/sw-KE.yml new file mode 100644 index 0000000000000..7bf73465b1380 --- /dev/null +++ b/decidim-api/config/locales/sw-KE.yml @@ -0,0 +1 @@ +sw: diff --git a/decidim-api/config/locales/th-TH.yml b/decidim-api/config/locales/th-TH.yml new file mode 100644 index 0000000000000..a4431912a8299 --- /dev/null +++ b/decidim-api/config/locales/th-TH.yml @@ -0,0 +1 @@ +th: diff --git a/decidim-api/config/locales/ti-ER.yml b/decidim-api/config/locales/ti-ER.yml new file mode 100644 index 0000000000000..39bcd22920e44 --- /dev/null +++ b/decidim-api/config/locales/ti-ER.yml @@ -0,0 +1 @@ +ti: diff --git a/decidim-api/config/locales/tr-TR.yml b/decidim-api/config/locales/tr-TR.yml new file mode 100644 index 0000000000000..077d41667ab93 --- /dev/null +++ b/decidim-api/config/locales/tr-TR.yml @@ -0,0 +1 @@ +tr: diff --git a/decidim-api/config/locales/uk.yml b/decidim-api/config/locales/uk.yml new file mode 100644 index 0000000000000..c256c3246c4a3 --- /dev/null +++ b/decidim-api/config/locales/uk.yml @@ -0,0 +1 @@ +uk: diff --git a/decidim-api/config/locales/val-ES.yml b/decidim-api/config/locales/val-ES.yml new file mode 100644 index 0000000000000..fa70518d04b9b --- /dev/null +++ b/decidim-api/config/locales/val-ES.yml @@ -0,0 +1 @@ +val: diff --git a/decidim-api/config/locales/vi.yml b/decidim-api/config/locales/vi.yml new file mode 100644 index 0000000000000..326506f0b1a36 --- /dev/null +++ b/decidim-api/config/locales/vi.yml @@ -0,0 +1 @@ +vi: diff --git a/decidim-api/config/locales/zh-CN.yml b/decidim-api/config/locales/zh-CN.yml new file mode 100644 index 0000000000000..f0b698bf39eb2 --- /dev/null +++ b/decidim-api/config/locales/zh-CN.yml @@ -0,0 +1 @@ +zh-CN: diff --git a/decidim-api/config/locales/zh-TW.yml b/decidim-api/config/locales/zh-TW.yml new file mode 100644 index 0000000000000..cb82c0526113b --- /dev/null +++ b/decidim-api/config/locales/zh-TW.yml @@ -0,0 +1 @@ +zh-TW: diff --git a/decidim-assemblies/config/locales/ca-IT.yml b/decidim-assemblies/config/locales/ca-IT.yml index ddbd852fb876c..4f5706e934f16 100644 --- a/decidim-assemblies/config/locales/ca-IT.yml +++ b/decidim-assemblies/config/locales/ca-IT.yml @@ -170,8 +170,8 @@ ca-IT: components: Components info: Quant a aquesta assemblea landing_page: Disposició de la pàgina de destinació + members: Membres moderations: Moderacions - private_users: Membres see_assembly: Veure l'assemblea models: assembly: @@ -218,9 +218,11 @@ ca-IT: export: "%{user_name} ha exportat l'assemblea %{resource_name}" import: "%{user_name} ha importat l'assemblea %{resource_name}" publish: "%{user_name} ha publicat l'assemblea %{resource_name}" + publish_all_members: "%{user_name} va publicar a totes les membres de l'assemblea %{resource_name}" restore: "%{user_name} ha restaurat l'assemblea %{resource_name}" soft_delete: "%{user_name} ha mogut a la paperera l'assemblea %{resource_name}" unpublish: "%{user_name} ha despublicat l'assemblea %{resource_name}" + unpublish_all_members: "%{user_name} va despublicar a totes les membres de l'assemblea %{resource_name}" update: "%{user_name} ha actualitzat l'assemblea %{resource_name}" assembly_member: create: "%{user_name} ha afegit el membre %{resource_name} a l'assemblea %{space_name}" @@ -251,7 +253,6 @@ ca-IT: metadata: Metadades no_taxonomy_filters_found: No s'han trobat filtres de taxonomia. other: Altre - private_notice: Podràs administrar les participants privades un cop haguis configurar l'espai com a privat select_a_created_by: Selecciona una creadora select_parent_assembly: Selecciona una assemblea mare slug_help_html: 'Els noms curts d''URL s''utilitzen per generar les URL que apunten a aquesta assemblea. Només accepta lletres, números i guions, i ha de començar amb una lletra. Exemple: %{url}' diff --git a/decidim-assemblies/config/locales/ca.yml b/decidim-assemblies/config/locales/ca.yml index 2142843a6738b..c335ef949ced7 100644 --- a/decidim-assemblies/config/locales/ca.yml +++ b/decidim-assemblies/config/locales/ca.yml @@ -170,8 +170,8 @@ ca: components: Components info: Quant a aquesta assemblea landing_page: Disposició de la pàgina de destinació + members: Membres moderations: Moderacions - private_users: Membres see_assembly: Veure l'assemblea models: assembly: @@ -218,9 +218,11 @@ ca: export: "%{user_name} ha exportat l'assemblea %{resource_name}" import: "%{user_name} ha importat l'assemblea %{resource_name}" publish: "%{user_name} ha publicat l'assemblea %{resource_name}" + publish_all_members: "%{user_name} va publicar a totes les membres de l'assemblea %{resource_name}" restore: "%{user_name} ha restaurat l'assemblea %{resource_name}" soft_delete: "%{user_name} ha mogut a la paperera l'assemblea %{resource_name}" unpublish: "%{user_name} ha despublicat l'assemblea %{resource_name}" + unpublish_all_members: "%{user_name} va despublicar a totes les membres de l'assemblea %{resource_name}" update: "%{user_name} ha actualitzat l'assemblea %{resource_name}" assembly_member: create: "%{user_name} ha afegit el membre %{resource_name} a l'assemblea %{space_name}" @@ -251,7 +253,6 @@ ca: metadata: Metadades no_taxonomy_filters_found: No s'han trobat filtres de taxonomia. other: Altre - private_notice: Podràs administrar les participants privades un cop haguis configurar l'espai com a privat select_a_created_by: Selecciona una creadora select_parent_assembly: Selecciona una assemblea mare slug_help_html: 'Els noms curts d''URL s''utilitzen per generar les URL que apunten a aquesta assemblea. Només accepta lletres, números i guions, i ha de començar amb una lletra. Exemple: %{url}' diff --git a/decidim-assemblies/config/locales/cs.yml b/decidim-assemblies/config/locales/cs.yml index ed44e3e574850..56a84de02924c 100644 --- a/decidim-assemblies/config/locales/cs.yml +++ b/decidim-assemblies/config/locales/cs.yml @@ -177,7 +177,6 @@ cs: info: O tomto shromáždění landing_page: Rozložení vstupní strany moderations: Moderování - private_users: Členové see_assembly: Zobrazit shromáždění models: assembly: @@ -257,7 +256,6 @@ cs: metadata: Metadata no_taxonomy_filters_found: Nebyly nalezeny žádné filtry taxonomie. other: Ostatní - private_notice: Budete moci spravovat soukromé účastníky po nastavení jako soukromé select_a_created_by: Vybrat vytvořeno od select_parent_assembly: Vybrat nadřazené shromáždění slug_help_html: 'URL slugy se používají ke generování adres URL, které odkazují na toto shromáždění. Povolená jsou pouze písmena, číslice a pomlčky a musí začínat písmenem. Příklad: %{url}' diff --git a/decidim-assemblies/config/locales/de.yml b/decidim-assemblies/config/locales/de.yml index 88b10dd7771f5..20d803a0d76b5 100644 --- a/decidim-assemblies/config/locales/de.yml +++ b/decidim-assemblies/config/locales/de.yml @@ -171,7 +171,6 @@ de: info: Über dieses Gremium landing_page: Startseiten-Layout moderations: Moderationen - private_users: Mitglieder see_assembly: Gremium ansehen models: assembly: @@ -251,7 +250,6 @@ de: metadata: Metadaten no_taxonomy_filters_found: Keine Klassifizierungsfilter gefunden. other: Andere - private_notice: Sie werden in der Lage sein, private Teilnehmer zu verwalten, nachdem Sie das Gremium als privat festgelegt haben select_a_created_by: Wählen Sie eine erstellt von aus select_parent_assembly: Übergeordnetes Gremium auswählen slug_help_html: 'URL-Slugs werden zum Generieren der URLs verwendet, die auf dieses Gremium verweisen. Akzeptiert werden nur Buchstaben, Zahlen und Bindestriche und es muss mit einem Buchstaben beginnen. Beispiel: %{url}' diff --git a/decidim-assemblies/config/locales/es-MX.yml b/decidim-assemblies/config/locales/es-MX.yml index 73a438add08fe..8ba66b5bca43d 100644 --- a/decidim-assemblies/config/locales/es-MX.yml +++ b/decidim-assemblies/config/locales/es-MX.yml @@ -170,8 +170,8 @@ es-MX: components: Componentes info: Acerca de esta asamblea landing_page: Disposición de la página de aterrizaje + members: Miembros moderations: Moderaciones - private_users: Miembros see_assembly: Ver la asamblea models: assembly: @@ -218,9 +218,11 @@ es-MX: export: "%{user_name} exportó la asamblea %{resource_name}" import: "%{user_name} importó la asamblea %{resource_name}" publish: "%{user_name} publicó la asamblea %{resource_name}" + publish_all_members: "%{user_name} publicó todas las miembros de la asamblea %{resource_name}" restore: "%{user_name} ha restaurado la asamblea %{resource_name}" soft_delete: "%{user_name} ha movido a la papelera la asamblea %{resource_name}" unpublish: "%{user_name} despublicó la asamblea %{resource_name}" + unpublish_all_members: "%{user_name} despublicó a todas las miembros de la asamblea %{resource_name}" update: "%{user_name} actualizó la asamblea %{resource_name}" assembly_member: create: "%{user_name} creó el miembro %{resource_name} en la asamblea %{space_name}" @@ -251,7 +253,6 @@ es-MX: metadata: Metadatos no_taxonomy_filters_found: No se han encontrado filtros de taxonomía. other: Otro - private_notice: Podrás administrar las participantes privadas después de configurar el espacio como privado select_a_created_by: Selecciona creada por select_parent_assembly: Selecciona asamblea principal slug_help_html: 'Los textos cortos de URL se utilizan para generar las URL que apuntan a esta asamblea. Sólo acepta letras, números y guiones, y debe comenzar con una letra. Ejemplo: %{url}' diff --git a/decidim-assemblies/config/locales/es-PY.yml b/decidim-assemblies/config/locales/es-PY.yml index 53cf928f7b688..a488abe398066 100644 --- a/decidim-assemblies/config/locales/es-PY.yml +++ b/decidim-assemblies/config/locales/es-PY.yml @@ -170,8 +170,8 @@ es-PY: components: Componentes info: Acerca de esta asamblea landing_page: Disposición de la página de aterrizaje + members: Miembros moderations: Moderaciones - private_users: Miembros see_assembly: Ver la asamblea models: assembly: @@ -218,9 +218,11 @@ es-PY: export: "%{user_name} ha exportado la asamblea %{resource_name}" import: "%{user_name} ha importado la asamblea %{resource_name}" publish: "%{user_name} publicó la asamblea %{resource_name}" + publish_all_members: "%{user_name} publicó todas las miembros de la asamblea %{resource_name}" restore: "%{user_name} ha restaurado la asamblea %{resource_name}" soft_delete: "%{user_name} ha movido a la papelera la asamblea %{resource_name}" unpublish: "%{user_name} despublicó la asamblea %{resource_name}" + unpublish_all_members: "%{user_name} despublicó a todas las miembros de la asamblea %{resource_name}" update: "%{user_name} actualizó la asamblea %{resource_name}" assembly_member: create: "%{user_name} creó el miembro %{resource_name} en la asamblea %{space_name}" @@ -251,7 +253,6 @@ es-PY: metadata: Metadatos no_taxonomy_filters_found: No se han encontrado filtros de taxonomía. other: Otro - private_notice: Podrás administrar las participantes privadas después de configurar el espacio como privado select_a_created_by: Selecciona creada por select_parent_assembly: Seleccionar ensamblaje principal slug_help_html: 'Los textos cortos de URL se utilizan para generar las URL que apuntan a esta asamblea. Sólo acepta letras, números y guiones, y debe comenzar con una letra. Ejemplo: %{url}' diff --git a/decidim-assemblies/config/locales/es.yml b/decidim-assemblies/config/locales/es.yml index fff88fefca145..c117ea0f8c1d4 100644 --- a/decidim-assemblies/config/locales/es.yml +++ b/decidim-assemblies/config/locales/es.yml @@ -170,8 +170,8 @@ es: components: Componentes info: Acerca de esta asamblea landing_page: Disposición de la página de aterrizaje + members: Miembros moderations: Moderaciones - private_users: Miembros see_assembly: Ver la asamblea models: assembly: @@ -218,9 +218,11 @@ es: export: "%{user_name} ha exportado la asamblea %{resource_name}" import: "%{user_name} ha importado la asamblea %{resource_name}" publish: "%{user_name} publicó la asamblea %{resource_name}" + publish_all_members: "%{user_name} publicó todas las miembros de la asamblea %{resource_name}" restore: "%{user_name} ha restaurado la asamblea %{resource_name}" soft_delete: "%{user_name} ha movido a la papelera la asamblea %{resource_name}" unpublish: "%{user_name} despublicó la asamblea %{resource_name}" + unpublish_all_members: "%{user_name} despublicó a todas las miembros de la asamblea %{resource_name}" update: "%{user_name} actualizó la asamblea %{resource_name}" assembly_member: create: "%{user_name} creó el miembro %{resource_name} en la asamblea %{space_name}" @@ -251,7 +253,6 @@ es: metadata: Metadatos no_taxonomy_filters_found: No se han encontrado filtros de taxonomía. other: Otro - private_notice: Podrás administrar las participantes privadas después de configurar el espacio como privado select_a_created_by: Selecciona una creadora select_parent_assembly: Selecciona una asamblea madre slug_help_html: 'Los textos cortos de URL se utilizan para generar las URL que apuntan a esta asamblea. Sólo acepta letras, números y guiones, y debe comenzar con una letra. Ejemplo: %{url}' diff --git a/decidim-assemblies/config/locales/eu.yml b/decidim-assemblies/config/locales/eu.yml index a68548bdbeaec..2d03a2854d111 100644 --- a/decidim-assemblies/config/locales/eu.yml +++ b/decidim-assemblies/config/locales/eu.yml @@ -170,8 +170,8 @@ eu: components: Osagaiak info: Batzar honi buruz landing_page: Lurreratze-orrien antolaketa + members: Kideak moderations: Moderazioak - private_users: Kideak see_assembly: Ikusi batzarra models: assembly: @@ -218,9 +218,11 @@ eu: export: "%{user_name} parte-hartzaileak %{resource_name} batzarra esportatu du" import: "%{user_name} parte-hartzaileak %{resource_name} batzarra inportatu du" publish: "%{user_name} parte-hartzaileak %{resource_name} batzarra argitaratu du" + publish_all_members: "%{user_name} parte-hartzaileak %{resource_name} batzarreko kide guztiak argitaratu ditu" restore: "%{user_name} parte-hartzaileak %{resource_name} batzarra berreskuratu du" soft_delete: "%{user_name} parte-hartzaileak %{resource_name} batzarra zaborrontzira eraman du" unpublish: "%{user_name} parte-hartzaileak %{resource_name} batzarra desargitaratu du" + unpublish_all_members: "%{user_name} parte-hartzaileak %{resource_name} batzarreko kide guztiak desargitaratu ditu" update: "%{user_name} parte-hartzaileak %{resource_name} batzarra eguneratu du" assembly_member: create: "%{user_name} parte-hartzaileak %{resource_name} kidea sortu du %{space_name} batzarrean" @@ -251,7 +253,6 @@ eu: metadata: Metadata no_taxonomy_filters_found: Ez da taxonomia-iragazkirik aurkitu. other: Beste bat - private_notice: Parte hartzaile pribatuak kudeatu ahal izango dituzu pribatu gisa ezarri ondoren select_a_created_by: Hautatu egilea select_parent_assembly: Hautatu batzar nagusia slug_help_html: 'URL-ren testu laburrak batzar honetara daramaten URL-ak sortzeko erabiltzen dira. Letrak, zenbakiak eta gidoiak soilik onartzen ditu, eta letra batez hasi behar du. Adibidea: %{url}' diff --git a/decidim-assemblies/config/locales/fi-plain.yml b/decidim-assemblies/config/locales/fi-plain.yml index 3be39c3507b22..8966571717411 100644 --- a/decidim-assemblies/config/locales/fi-plain.yml +++ b/decidim-assemblies/config/locales/fi-plain.yml @@ -25,6 +25,7 @@ fi-pl: duration: Kesto facebook: Facebook github: GitHub + has_members: Tässä tilassa on jäseniä hero_image: Etusivun kuva import_attachments: Tuo liitteitä import_categories: Tuo aihepiirejä @@ -170,8 +171,8 @@ fi-pl: components: Komponentit info: Tietoa tästä ryhmästä landing_page: Laskeutumissivun asettelu + members: Jäsenet moderations: Moderoinnit - private_users: Jäsenet see_assembly: Näytä ryhmä models: assembly: @@ -218,9 +219,11 @@ fi-pl: export: "%{user_name} vei ryhmän %{resource_name}" import: "%{user_name} toi ryhmän %{resource_name}" publish: "%{user_name} julkaisi %{resource_name} ryhmän" + publish_all_members: "%{user_name} julkaisi ryhmän %{resource_name} jäsenet" restore: "%{user_name} palautti ryhmän %{resource_name}" soft_delete: "%{user_name} siirsi ryhmän %{resource_name} roskakoriin" unpublish: "%{user_name} lopetti %{resource_name} ryhmän julkaisemisen" + unpublish_all_members: "%{user_name} perui ryhmän %{resource_name} jäsenten julkaisun" update: "%{user_name} päivitti %{resource_name} ryhmän" assembly_member: create: "%{user_name} loi %{resource_name} jäsenen %{space_name} ryhmässä" @@ -246,12 +249,13 @@ fi-pl: define_taxonomy_filters: Määritä osallistumistilalle suodattimia ennen kuin käytät tätä asetusta. duration: Kesto duration_help: Jos ryhmän kesto on rajoitettu, valitse päättymispäivä. Muussa tapauksessa se näkyy määrittelemättömänä. + has_members_help: Voit luoda ja julkaista jäseniä images: kuvat included_at_help: Valitse päivämäärä, jolloin tämä ryhmä lisättiin alustalle. Sen ei välttämättä tarvitse olla sama kuin luontipäivä. metadata: metadata no_taxonomy_filters_found: Luokittelusuodattimia ei löytynyt. other: muut - private_notice: Voit hallinnoida osallistumistilan yksityisiä käyttäjiä asettamalla osallistumistilan yksityiseksi + private_notice: Piilottaa osallistumistilan verkkosivuston vierailijoiden näkyvistä (ellei osallistumistila ole myös läpinäkyvä) select_a_created_by: Valitse luoja select_parent_assembly: Valitse pääryhmä slug_help_html: 'URL-tunnisteita käytetään tähän ryhmään osoittavien URL-osoitteiden luonnissa. Hyväksyy kirjaimet, numerot ja viivat. Ensimmäinen merkki on oltava kirjain. Esimerkiksi: %{url}' diff --git a/decidim-assemblies/config/locales/fi.yml b/decidim-assemblies/config/locales/fi.yml index 59271c391c40f..637d900d6ab75 100644 --- a/decidim-assemblies/config/locales/fi.yml +++ b/decidim-assemblies/config/locales/fi.yml @@ -25,6 +25,7 @@ fi: duration: Kesto facebook: Facebook github: GitHub + has_members: Tässä tilassa on jäseniä hero_image: Etusivun kuva import_attachments: Tuo liitteitä import_categories: Tuo aihepiirejä @@ -170,8 +171,8 @@ fi: components: Komponentit info: Tietoa tästä ryhmästä landing_page: Laskeutumissivun asettelu + members: Jäsenet moderations: Moderoinnit - private_users: Jäsenet see_assembly: Näytä ryhmä models: assembly: @@ -218,9 +219,11 @@ fi: export: "%{user_name} vei ryhmän %{resource_name}" import: "%{user_name} toi ryhmän %{resource_name}" publish: "%{user_name} julkaisi %{resource_name} ryhmän" + publish_all_members: "%{user_name} julkaisi ryhmän %{resource_name} jäsenet" restore: "%{user_name} palautti ryhmän %{resource_name}" soft_delete: "%{user_name} siirsi ryhmän %{resource_name} roskakoriin" unpublish: "%{user_name} perui ryhmän %{resource_name} julkaisun" + unpublish_all_members: "%{user_name} perui ryhmän %{resource_name} jäsenten julkaisun" update: "%{user_name} päivitti %{resource_name} ryhmän" assembly_member: create: "%{user_name} loi %{resource_name} jäsenen %{space_name} ryhmässä" @@ -246,12 +249,13 @@ fi: define_taxonomy_filters: Määritä osallistumistilalle suodattimia ennen kuin käytät tätä asetusta. duration: Kesto duration_help: Jos ryhmän kesto on rajoitettu, valitse päättymispäivä. Muussa tapauksessa se näkyy määrittelemättömänä. + has_members_help: Voit luoda ja julkaista jäseniä images: Kuvat included_at_help: Valitse päivämäärä, jolloin tämä ryhmä lisättiin alustalle. Sen ei välttämättä tarvitse olla sama kuin luontipäivä. metadata: Metatiedot no_taxonomy_filters_found: Luokittelusuodattimia ei löytynyt. other: Muut - private_notice: Voit hallinnoida osallistumistilan yksityisiä käyttäjiä asettamalla osallistumistilan yksityiseksi + private_notice: Piilottaa osallistumistilan verkkosivuston vierailijoiden näkyvistä (ellei osallistumistila ole myös läpinäkyvä) select_a_created_by: Valitse luoja select_parent_assembly: Valitse pääryhmä slug_help_html: 'URL-tunnisteita käytetään tähän ryhmään osoittavien URL-osoitteiden luonnissa. Hyväksyy kirjaimet, numerot ja viivat. Ensimmäinen merkki on oltava kirjain. Esimerkiksi: %{url}' diff --git a/decidim-assemblies/config/locales/fr-CA.yml b/decidim-assemblies/config/locales/fr-CA.yml index 5b7d3dd91aa70..aefd524c9818c 100644 --- a/decidim-assemblies/config/locales/fr-CA.yml +++ b/decidim-assemblies/config/locales/fr-CA.yml @@ -25,6 +25,7 @@ fr-CA: duration: Durée facebook: Facebook github: GitHub + has_members: Cet espace a des membres hero_image: Image de la page d'accueil import_attachments: Importer les pièces jointes import_categories: Importer les catégories @@ -169,8 +170,8 @@ fr-CA: attachments: Documents liés components: Composants info: À propos de cette assemblée + members: Membres moderations: Modérations - private_users: Membres see_assembly: Voir l'assemblée models: assembly: @@ -217,9 +218,11 @@ fr-CA: export: "%{user_name} a exporté l'assemblée %{resource_name}" import: "%{user_name} a importé l'assemblée %{resource_name}" publish: "%{user_name} a publié l'assemblée %{resource_name}" + publish_all_members: "%{user_name} a publié tous les membres de l'assemblée %{resource_name}" restore: "%{user_name} a restauré l'assemblée %{resource_name}" soft_delete: "%{user_name} a déplacé dans la corbeille l'assemblée %{resource_name}" unpublish: "%{user_name} a dépublié l'assemblée %{resource_name}" + unpublish_all_members: "%{user_name} a dépublié tous les membres de l'assemblée %{resource_name}" update: "%{user_name} a mis à jour l'assemblée %{resource_name}" assembly_member: create: "%{user_name} a créé le membre %{resource_name} membre dans l'assemblée %{space_name}" @@ -245,12 +248,13 @@ fr-CA: define_taxonomy_filters: Veuillez définir des filtres pour cet espace participatif avant d'utiliser ce paramètre. duration: Durée duration_help: Si la durée de cette assemblée est limitée, sélectionnez la date de fin. Sinon sa durée ne sera pas limitée. + has_members_help: Vous pourrez créer et publier des membres images: Images included_at_help: Sélectionnez la date à laquelle cet assemblée a été ajoutée à la plateforme. Elle ne doit pas nécessairement être identique à la date de création. metadata: Métadonnées no_taxonomy_filters_found: Aucun filtre de taxonomie trouvé. other: Autre - private_notice: Vous serez en mesure de gérer les participants privés après l'avoir défini comme privé + private_notice: Rend l'espace non visible pour les visiteurs et visible seulement pour les membres (sauf s'il est également transparent) select_a_created_by: Sélectionnez un créateur select_parent_assembly: Sélectionnez l'assemblée parente slug_help_html: 'Les identifiants d''URL sont utilisés pour générer les URL qui pointent vers cette assemblée. N''accepte que des lettres, des chiffres et des tirets et doit commencer par une lettre. Exemple : %{url}' diff --git a/decidim-assemblies/config/locales/fr.yml b/decidim-assemblies/config/locales/fr.yml index 57698c891462b..98eca80ea2bdc 100644 --- a/decidim-assemblies/config/locales/fr.yml +++ b/decidim-assemblies/config/locales/fr.yml @@ -25,6 +25,7 @@ fr: duration: Durée facebook: Facebook github: GitHub + has_members: Cet espace a des membres hero_image: Image de la page d'accueil import_attachments: Importer les pièces jointes import_categories: Importer les catégories @@ -169,8 +170,8 @@ fr: attachments: Documents liés components: Fonctionnalités info: À propos de cette assemblée + members: Membres moderations: Modérations - private_users: Membres see_assembly: Voir l'assemblée models: assembly: @@ -217,9 +218,11 @@ fr: export: "%{user_name} a exporté l'assemblée %{resource_name}" import: "%{user_name} a importé l'assemblée %{resource_name}" publish: "%{user_name} a publié l'assemblée %{resource_name}" + publish_all_members: "%{user_name} a publié tous les membres de l'assemblée %{resource_name}" restore: "%{user_name} a restauré l'assemblée %{resource_name}" soft_delete: "%{user_name} a déplacé dans la corbeille l'assemblée %{resource_name}" unpublish: "%{user_name} a dépublié l'assemblée %{resource_name}" + unpublish_all_members: "%{user_name} a dépublié tous les membres de l'assemblée %{resource_name}" update: "%{user_name} a mis à jour l'assemblée %{resource_name}" assembly_member: create: "%{user_name} a créé le membre %{resource_name} membre dans l'assemblée %{space_name}" @@ -245,12 +248,13 @@ fr: define_taxonomy_filters: Veuillez définir des filtres pour cet espace participatif avant d'utiliser ce paramètre. duration: Durée duration_help: Si la durée de cette assemblée est limitée, sélectionnez la date de fin. Sinon sa durée ne sera pas limitée. + has_members_help: Vous pourrez créer et publier des membres images: Images included_at_help: Sélectionnez la date à laquelle cet assemblée a été ajoutée à la plateforme. Elle ne doit pas nécessairement être identique à la date de création. metadata: Métadonnées no_taxonomy_filters_found: Aucun filtre de taxonomie trouvé. other: Autre - private_notice: Vous serez en mesure de gérer les participants privés après l'avoir défini comme privé + private_notice: Rend l'espace non visible pour les visiteurs et visible seulement pour les membres (sauf s'il est également transparent) select_a_created_by: Sélectionnez un créateur select_parent_assembly: Sélectionnez l'assemblée parente slug_help_html: 'Les identifiants d''URL sont utilisés pour générer les URL qui pointent vers cette assemblée. N''accepte que des lettres, des chiffres et des tirets et doit commencer par une lettre. Exemple : %{url}' diff --git a/decidim-assemblies/config/locales/he-IL.yml b/decidim-assemblies/config/locales/he-IL.yml index 566dfe31a7643..374c6374daeb8 100644 --- a/decidim-assemblies/config/locales/he-IL.yml +++ b/decidim-assemblies/config/locales/he-IL.yml @@ -151,7 +151,6 @@ he: components: רכיבים info: אודות האסיפה moderations: שינויים - private_users: חברים see_assembly: ראה אסיפה models: assembly: diff --git a/decidim-assemblies/config/locales/it.yml b/decidim-assemblies/config/locales/it.yml index 0ed12b0f96a16..e1e7d875516fb 100644 --- a/decidim-assemblies/config/locales/it.yml +++ b/decidim-assemblies/config/locales/it.yml @@ -159,7 +159,6 @@ it: info: Info su questo spazio landing_page: Layout della pagina di destinazione moderations: Moderazione - private_users: Membri see_assembly: Guarda l'assemblea models: assembly: @@ -239,7 +238,6 @@ it: metadata: Metadati no_taxonomy_filters_found: Nessun filtro trovato. other: Altro - private_notice: Sarai in grado di gestire i partecipanti privati dopo aver impostato lo spazio come privato select_a_created_by: Seleziona un creatore select_parent_assembly: Seleziona l'assemblea madre slug_help_html: 'Gli slug URL vengono utilizzati per generare gli URL che puntano a questa assemblea. Accetta solo lettere, numeri e trattini, e deve iniziare con una lettera. Esempio: %{url}' diff --git a/decidim-assemblies/config/locales/ja.yml b/decidim-assemblies/config/locales/ja.yml index e98a732678599..ea15500ea1d07 100644 --- a/decidim-assemblies/config/locales/ja.yml +++ b/decidim-assemblies/config/locales/ja.yml @@ -167,8 +167,8 @@ ja: components: コンポーネント info: この参加スペースについて landing_page: ランディングページのレイアウト + members: メンバー moderations: モデレーション - private_users: メンバー see_assembly: 参加スペースを見る models: assembly: @@ -215,9 +215,11 @@ ja: export: "%{user_name} は %{resource_name} 参加スペースをエクスポートしました" import: "%{user_name} が %{resource_name} 参加スペースをインポートしました" publish: "%{user_name} が %{resource_name} 参加スペースを公開しました" + publish_all_members: "%{user_name} が参加スペース %{resource_name} のすべてのメンバーを公開しました" restore: "%{user_name} が %{resource_name} 参加スペースを復元しました" soft_delete: "%{user_name} が %{resource_name} 参加スペースをゴミ箱に移動しました" unpublish: "%{user_name} が %{resource_name} 参加スペースを非公開にしました" + unpublish_all_members: "%{user_name} が参加スペース %{resource_name} のすべてのメンバーを非公開にしました" update: "%{user_name} が %{resource_name} 参加スペースを更新しました" assembly_member: create: "%{user_name} が %{resource_name} 参加スペースで %{space_name} メンバーを作成しました" @@ -248,7 +250,6 @@ ja: metadata: メタデータ no_taxonomy_filters_found: タクソノミーフィルタが見つかりません。 other: その他 - private_notice: プライベートとして設定した後、プライベート参加者を管理することができます select_a_created_by: 作成者を選択してください select_parent_assembly: 親参加スペースを選択 slug_help_html: 'URLスラグは、この参加スペースを指すURLを生成するために使用されます。 英字、数字、ハイフンのみを受け付け、英字で始める必要があります。例: %{url}' diff --git a/decidim-assemblies/config/locales/pt.yml b/decidim-assemblies/config/locales/pt.yml index 3749893eb2c09..8481b03253dfe 100644 --- a/decidim-assemblies/config/locales/pt.yml +++ b/decidim-assemblies/config/locales/pt.yml @@ -158,7 +158,6 @@ pt: components: Componentes info: Sobre esta reunião moderations: Moderação - private_users: Membros see_assembly: Veja a reunião models: assembly: @@ -236,7 +235,6 @@ pt: metadata: Metadados no_taxonomy_filters_found: Nenhum filtro de taxonomia encontrado. other: Outros - private_notice: Poderá gerir participantes privados depois de os definir como privados select_a_created_by: Selecione um criado por select_parent_assembly: Selecione a reunião pai slug_help_html: 'São usados slugs de URL para gerar URLs que apontam para esta reunião. Aceita apenas letras, números e traços, e deve começar com uma letra. Exemplo: %{url}' diff --git a/decidim-assemblies/config/locales/sv.yml b/decidim-assemblies/config/locales/sv.yml index 00b214ff379dc..d61b6fef4f34b 100644 --- a/decidim-assemblies/config/locales/sv.yml +++ b/decidim-assemblies/config/locales/sv.yml @@ -170,7 +170,6 @@ sv: components: Komponenter info: Om samrådet moderations: Moderering - private_users: Medlemmar see_assembly: Visa samråd models: assembly: @@ -217,9 +216,11 @@ sv: export: "%{user_name} exporterade samrådet %{resource_name}" import: "%{user_name} importerade samrådet %{resource_name}" publish: "%{user_name} publicerade samrådet %{resource_name}" + publish_all_members: "%{user_name} publicerade samrådet %{resource_name}" restore: "%{user_name} återställde samrådet %{resource_name}" soft_delete: "%{user_name} tog bort samrådet %{resource_name}" unpublish: "%{user_name} avpublicerade samrådet %{resource_name}" + unpublish_all_members: "%{user_name} avpublicerade samrådet %{resource_name}" update: "%{user_name} uppdaterade samrådet %{resource_name}" assembly_member: create: "%{user_name} skapade medlemmen %{resource_name} i samrådet %{space_name}" @@ -250,7 +251,6 @@ sv: metadata: Metadata no_taxonomy_filters_found: Inga filter för kategorier hittades. other: Övrigt - private_notice: Du kommer att kunna hantera medlemmar om samrådet gjorts privat select_a_created_by: Välj en skapad av select_parent_assembly: Välj överordnat samråd slug_help_html: 'URL-slugs används till att generera URL:er till samrådet. Använd bara bokstäver, siffror och bindestreck, och de måste börja med en bokstav. Exempel: %{url}' diff --git a/decidim-collaborative_texts/config/locales/eu.yml b/decidim-collaborative_texts/config/locales/eu.yml index c645ac4c56c73..e51d5676463dc 100644 --- a/decidim-collaborative_texts/config/locales/eu.yml +++ b/decidim-collaborative_texts/config/locales/eu.yml @@ -87,7 +87,7 @@ eu: unpublish: "%{user_name} parte-hartzaileak %{resource_name} testu kolaboratiboa argitaratzeari utzi dio %{space_name} espazioan" update: "%{user_name} parte-hartzaileak %{resource_name} testu kolaboratiboa aldatu du %{space_name} espazioan" suggestion: - create: "%{user_name} parte-hartzaileak %{resource_name} testu kolaboratiboan aldaketak iradoki ditu %{space_name} partaidetza espazioan" + create: "%{user_name} parte-hartzaileak %{resource_name} testu kolaboratiboan aldaketak iradoki ditu %{space_name} partaidetza-espazioan" version: delete: "%{user_name} parte-hartzaileak %{resource_name} testu kolaboratiboaren bertsioa ezabatu du %{space_name} espazioan" update: "%{user_name} parte-hartzaileak %{resource_name} testu kolaboratiboaren bertsioa aldatu du %{space_name} espazioan" diff --git a/decidim-core/config/locales/ar.yml b/decidim-core/config/locales/ar.yml index 9b773fb4ea553..b6656014bca54 100644 --- a/decidim-core/config/locales/ar.yml +++ b/decidim-core/config/locales/ar.yml @@ -202,10 +202,6 @@ ar: organization: update: "قام %{user_name} بتحديث إعدادات المنظمة" update_external_domain: "قام %{user_name} بتحديث النطاقات الخارجية للمنظمة external domains" - participatory_space_private_user: - create: "%{user_name} مدعو %{resource_name} ليكون مشاركًا خاصًا" - create_via_csv: "%{user_name} دعا %{resource_name} عبر CSV ليكون مشاركا خاصا" - delete: "%{user_name} أزال المشارك %{resource_name} كمشارك خاص" scope: create: "%{user_name} خلق نطاق %{resource_name}" create_with_parent: "إنشاء %{user_name} النطاق %{resource_name} داخل النطاق %{parent_scope}" @@ -1136,10 +1132,8 @@ ar: decline: رفض الدعوة hello: مرحبا %{email}، invited_you_as_admin: "%{invited_by} دعاك كمشرف لـ %{application}. يمكنك قبول ذلك من خلال الرابط أدناه." - invited_you_as_private_user: "تلقيت دعوة من %{invited_by} كمشارك خاص لـ %{application}يمكنك قبول ذلك من خلال الرابط أدناه." someone_invited_you: دعاك شخص ما إلى %{application}. يمكنك قبول ذلك من خلال الرابط أدناه. someone_invited_you_as_admin: قام شخص ما بدعوتك كمسؤول لـ %{application}، يمكنك قبولها من خلال الرابط أدناه. - someone_invited_you_as_private_user: قام شخص ما بدعوتك كمشارك خاص بقيمة %{application}، ويمكنك قبولها من خلال الرابط أدناه. subject: تعليمات الدعوة password_change: greeting: مرحبا %{recipient}! diff --git a/decidim-core/config/locales/bg.yml b/decidim-core/config/locales/bg.yml index 966695e798145..6b41cf839b9a6 100644 --- a/decidim-core/config/locales/bg.yml +++ b/decidim-core/config/locales/bg.yml @@ -232,10 +232,6 @@ bg: organization: update: "%{user_name} обнови настройките на организацията" update_external_domain: "%{user_name} актуализира външните домейни на организацията" - participatory_space_private_user: - create: "%{user_name} покани %{resource_name} да бъде частен участник" - create_via_csv: "%{user_name} покани %{resource_name} чрез CSV да бъде частен участник" - delete: "%{user_name} премахна участника %{resource_name} като частен" scope: create: "%{user_name} създаде сферата %{resource_name}" create_with_parent: "%{user_name} създаде сферата %{resource_name} в родителската сфера %{parent_scope}" @@ -1331,17 +1327,13 @@ bg: Ако не искате да приемете поканата, моля, игнорирайте този имейл.
Профилът Ви няма да бъде създаден, докато не отворите връзката по-горе и не зададете своите псевдоним и парола. invited_you_as_admin: "%{invited_by} Ви покани за администратор на %{application}. Можете да приемете чрез връзката по-долу." - invited_you_as_private_user: "%{invited_by} Ви покани като частен участник на %{application}. Можете да приемете чрез връзката по-долу." someone_invited_you: Някой Ви покани в %{application}. Можете да приемете чрез връзката по-долу. someone_invited_you_as_admin: Някой ви покани за администратор на %{application}, можете да приемете чрез връзката по-долу. - someone_invited_you_as_private_user: Някой ви покани частен участник в %{application}, можете да приемете чрез връзката по-долу. subject: Инструкции за покана invite_admin: subject: Получихте покана да управлявате %{organization} invite_collaborator: subject: Получихте покана да сътрудничите на %{organization} - invite_private_user: - subject: Поканени сте в частен процес на сътрудничество в %{organization} organization_admin_invitation_instructions: subject: Получихте покана да управлявате %{organization} password_change: diff --git a/decidim-core/config/locales/ca-IT.yml b/decidim-core/config/locales/ca-IT.yml index ac5e3960be53b..312df21ce7eaa 100644 --- a/decidim-core/config/locales/ca-IT.yml +++ b/decidim-core/config/locales/ca-IT.yml @@ -266,6 +266,10 @@ ca-IT: results: resultats impersonation_log: manage: "%{user_name} ha gestionat a %{resource_name} perquè %{reason}" + member: + create: "%{user_name} ha convidat a %{resource_name} com a membre" + create_via_csv: "%{user_name} ha convidat a %{resource_name} com a membre via CSV" + delete: "%{user_name} ha eliminat a %{resource_name} de membre" moderation: bulk_hide: "%{user_name} ha amagat %{reported_count} continguts" bulk_unhide: "%{user_name} ha tornat a mostrar %{reported_count} continguts" @@ -284,10 +288,6 @@ ca-IT: organization: update: "%{user_name} ha actualitzat la configuració de l'organització" update_external_domain: "%{user_name} ha actualitzat els dominis externs de l'organització" - participatory_space_private_user: - create: "%{user_name} ha convidat %{resource_name} com a participant privada" - create_via_csv: "%{user_name} ha convidat %{resource_name} via CSV per a ser participant privada" - delete: "%{user_name} ha eliminat %{resource_name} com a participant privada" scope: create: "%{user_name} ha creat l'àmbit %{resource_name}" create_with_parent: "%{user_name} ha creat l'àmbit %{resource_name} dins l'àmbit %{parent_scope}" @@ -766,13 +766,12 @@ ca-IT: id: L'identificador únic d'aquesta notificació resource_type: El tipus de recurs amb el qual està relacionat amb la notificació updated_at: La data i hora en què aquesta notificació va ser actualitzada per darrera vegada - participatory_space_private_users: - created_at: La data i l'hora en què es va crear aquesta participant privada - id: L'identificador únic d'aquesta participant privada - privatable_to: A quin espai de participació pertanys aquesta participant privada - published: Si aquesta participant privada està publicada o no - role: El rol que té aquesta participant privada - updated_at: La data i hora en què aquesta participant privada va ser actualitzada per darrera vegada + participatory_space_members: + created_at: La data i l'hora en què es va crear aquesta membre + id: L'identificador únic d'aquesta membre + published: Si aquesta membre està publicada o no + role: El rol que té aquesta membre + updated_at: La data de la darrera actualització d'aquesta membre reports: created_at: La data i hora en què es va crear aquest informe details: Els detalls dels informes de moderació @@ -1838,16 +1837,16 @@ ca-IT: ignore: |- Si no vols acceptar la invitació, si us plau, ignora aquest correu electrònic.
El teu compte no es crearà fins que no accedeixis a l'enllaç anterior i estableixis el teu àlies i contrasenya. invited_you_as_admin: "%{invited_by} t'ha convidat com a administradora de %{application}. Pots acceptar-ho a través de l'enllaç següent." - invited_you_as_private_user: "%{invited_by} t'ha convidat com a participant privada de %{application}. Pots acceptar a través de l'enllaç següent." + invited_you_as_member: "%{invited_by} t'ha convidat com a membre de %{application}. Pots acceptar-ho a través de l'enllaç següent." someone_invited_you: Algú t'ha convidat a %{application}. Pots acceptar-ho a través de l'enllaç següent. someone_invited_you_as_admin: Algú t'ha convidat com a administradora de %{application}, pots acceptar-lo a través de l'enllaç següent. - someone_invited_you_as_private_user: Algú t'ha convidat com a participant privada de %{application}. Pots acceptar a través del següent enllaç. + someone_invited_you_as_member: Algú t'ha convidat com a membre de %{application}, pots acceptar-ho a través de l'enllaç següent. subject: Instruccions d'invitació invite_admin: subject: T'han convidat a administrar %{organization} invite_collaborator: subject: T'han convidat a col·laborar a %{organization} - invite_private_user: + invite_member: subject: T'han convidat a participar en un espai de participació privat a%{organization} organization_admin_invitation_instructions: subject: T'han convidat a administrar %{organization} diff --git a/decidim-core/config/locales/ca.yml b/decidim-core/config/locales/ca.yml index 31d0d886544ea..59b5a1fe59db2 100644 --- a/decidim-core/config/locales/ca.yml +++ b/decidim-core/config/locales/ca.yml @@ -266,6 +266,10 @@ ca: results: resultats impersonation_log: manage: "%{user_name} ha gestionat a %{resource_name} perquè %{reason}" + member: + create: "%{user_name} ha convidat a %{resource_name} com a membre" + create_via_csv: "%{user_name} ha convidat a %{resource_name} com a membre via CSV" + delete: "%{user_name} ha eliminat a %{resource_name} de membre" moderation: bulk_hide: "%{user_name} ha amagat %{reported_count} continguts" bulk_unhide: "%{user_name} ha tornat a mostrar %{reported_count} continguts" @@ -284,10 +288,6 @@ ca: organization: update: "%{user_name} ha actualitzat la configuració de l'organització" update_external_domain: "%{user_name} ha actualitzat els dominis externs de l'organització" - participatory_space_private_user: - create: "%{user_name} ha convidat %{resource_name} com a participant privada" - create_via_csv: "%{user_name} ha convidat %{resource_name} via CSV per a ser participant privada" - delete: "%{user_name} ha eliminat %{resource_name} com a participant privada" scope: create: "%{user_name} ha creat l'àmbit %{resource_name}" create_with_parent: "%{user_name} ha creat l'àmbit %{resource_name} dins l'àmbit %{parent_scope}" @@ -766,13 +766,12 @@ ca: id: L'identificador únic d'aquesta notificació resource_type: El tipus de recurs amb el qual està relacionat amb la notificació updated_at: La data i hora en què aquesta notificació va ser actualitzada per darrera vegada - participatory_space_private_users: - created_at: La data i l'hora en què es va crear aquesta participant privada - id: L'identificador únic d'aquesta participant privada - privatable_to: A quin espai de participació pertanys aquesta participant privada - published: Si aquesta participant privada està publicada o no - role: El rol que té aquesta participant privada - updated_at: La data i hora en què aquesta participant privada va ser actualitzada per darrera vegada + participatory_space_members: + created_at: La data i l'hora en què es va crear aquesta membre + id: L'identificador únic d'aquesta membre + published: Si aquesta membre està publicada o no + role: El rol que té aquesta membre + updated_at: La data de la darrera actualització d'aquesta membre reports: created_at: La data i hora en què es va crear aquest informe details: Els detalls dels informes de moderació @@ -1838,16 +1837,16 @@ ca: ignore: |- Si no vols acceptar la invitació, si us plau, ignora aquest correu electrònic.
El teu compte no es crearà fins que no accedeixis a l'enllaç anterior i estableixis el teu àlies i contrasenya. invited_you_as_admin: "%{invited_by} t'ha convidat com a administradora de %{application}. Pots acceptar-ho a través de l'enllaç següent." - invited_you_as_private_user: "%{invited_by} t'ha convidat com a participant privada de %{application}. Pots acceptar a través de l'enllaç següent." + invited_you_as_member: "%{invited_by} t'ha convidat com a membre de %{application}. Pots acceptar-ho a través de l'enllaç següent." someone_invited_you: Algú t'ha convidat a %{application}. Pots acceptar-ho a través de l'enllaç següent. someone_invited_you_as_admin: Algú t'ha convidat com a administradora de %{application}, pots acceptar-lo a través de l'enllaç següent. - someone_invited_you_as_private_user: Algú t'ha convidat com a participant privada de %{application}. Pots acceptar a través del següent enllaç. + someone_invited_you_as_member: Algú t'ha convidat com a membre de %{application}, pots acceptar-ho a través de l'enllaç següent. subject: Instruccions d'invitació invite_admin: subject: T'han convidat a administrar %{organization} invite_collaborator: subject: T'han convidat a col·laborar a %{organization} - invite_private_user: + invite_member: subject: T'han convidat a participar en un espai de participació privat a%{organization} organization_admin_invitation_instructions: subject: T'han convidat a administrar %{organization} diff --git a/decidim-core/config/locales/cs.yml b/decidim-core/config/locales/cs.yml index 21bcbc147e5f3..66b3555bb473e 100644 --- a/decidim-core/config/locales/cs.yml +++ b/decidim-core/config/locales/cs.yml @@ -306,10 +306,6 @@ cs: organization: update: "%{user_name} aktualizovala nastavení organizace" update_external_domain: "%{user_name} aktualizoval externí domény organizace" - participatory_space_private_user: - create: "%{user_name} pozval %{resource_name} aby se stal soukromým uživatelem" - create_via_csv: "%{user_name} pozval %{resource_name} prostřednictvím CSV k tomu, aby byl soukromým účastníkem" - delete: "%{user_name} uživatele %{resource_name} odstranil jako soukromého uživatele" scope: create: "%{user_name} vytvořil oblast působnosti %{resource_name}" create_with_parent: "%{user_name} vytvořil oblast působnosti %{resource_name} uvnitř oblasti působnosti %{parent_scope}" @@ -803,13 +799,6 @@ cs: id: Jedinečný identifikátor tohoto oznámení resource_type: Typ zdroje, ke kterému se oznámení vztahuje updated_at: Datum a čas, kdy bylo oznámení naposledy aktualizováno - participatory_space_private_users: - created_at: Datum a čas, kdy byl tento soukromý uživatel vytvořen - id: Jedinečný identifikátor tohoto soukromého uživatele - privatable_to: Do kterého prostoru patří tento soukromý uživatel - published: Zda je tento soukromý uživatel zveřejněn, nebo ne - role: Role, kterou má tento soukromý uživatel - updated_at: Datum a čas poslední aktualizace tohoto soukromého uživatele reports: created_at: Datum a čas vytvoření této zprávy details: Podrobnosti této zprávy @@ -1822,17 +1811,13 @@ cs: Pokud pozvánku nechcete přijmout, ignorujte tento e-mail.
Váš účet nebude vytvořen, dokud nekliknete na odkaz výše a nenastavíte si uživatelské jméno a heslo. invited_you_as_admin: "%{invited_by} vás pozval jako administrátora z %{application}. Můžete jej přijmout prostřednictvím níže uvedeného odkazu." - invited_you_as_private_user: "%{invited_by} vás pozval jako soukromého uživatele %{application}. Můžete jej přijmout prostřednictvím níže uvedeného odkazu." someone_invited_you: Někdo vás pozval na %{application}. Můžete jej přijmout prostřednictvím níže uvedeného odkazu. someone_invited_you_as_admin: Někdo Vás pozval jako administrátora %{application}, můžete jej přijmout pomocí níže uvedeného odkazu. - someone_invited_you_as_private_user: Někdo vás pozval jako private_user %{application}, můžete jej přijmout pomocí níže uvedeného odkazu. subject: Pokyny k pozvánkám invite_admin: subject: Byli jste pozváni ke spravování organizace %{organization} invite_collaborator: subject: Byli jste pozváni ke spolupráci na organizaci %{organization} - invite_private_user: - subject: Byli jste pozváni k soukromé účasti na organizaci %{organization} organization_admin_invitation_instructions: subject: Byli jste pozváni ke spravování organizace %{organization} password_change: diff --git a/decidim-core/config/locales/de.yml b/decidim-core/config/locales/de.yml index 1fb4a143bcbcc..673ed5fae773e 100644 --- a/decidim-core/config/locales/de.yml +++ b/decidim-core/config/locales/de.yml @@ -284,10 +284,6 @@ de: organization: update: "%{user_name} hat die Organisationseinstellungen aktualisiert" update_external_domain: "%{user_name} hat die externe Domains der Organisation aktualisiert" - participatory_space_private_user: - create: "%{user_name} hat %{resource_name} als privaten Benutzer eingeladen" - create_via_csv: "%{user_name} hat %{resource_name} per CSV als privaten Teilnehmer eingeladen" - delete: "%{user_name} hat den Benutzer %{resource_name} als privaten Benutzer entfernt" scope: create: "%{user_name} hat den Bereich %{resource_name} erstellt" create_with_parent: "%{user_name} hat den Bereich %{resource_name} im übergeordneten Bereich %{parent_scope} erstellt" @@ -772,13 +768,6 @@ de: id: Die eindeutige Bezeichnung dieser Benachrichtigung resource_type: Der Typ der Ressource, mit der die Benachrichtigung verknüpft ist updated_at: Datum und Uhrzeit, an dem die Benachrichtigung zuletzt aktualisiert wurde - participatory_space_private_users: - created_at: Das Datum und die Uhrzeit, als diese private Teilnahme erstellt wurde - id: Die eindeutige Bezeichnung der privaten Teilnahme - privatable_to: Zu welchem Raum diese private Teilnahme gehört - published: Ob diese private Teilnahme veröffentlicht ist oder nicht - role: Die Rolle, die diese private Teilnahme hat - updated_at: Das Datum und die Uhrzeit, als diese private Teilnahme zuletzt aktualisiert wurde reports: created_at: Das Datum und die Uhrzeit, an der dieser Bericht erstellt wurde details: Die Details dieses Berichts @@ -1816,17 +1805,13 @@ de: Wenn Sie die Einladung nicht annehmen möchten, ignorieren Sie diese E-Mail.
Ihr Konto wird erst erstellt, wenn Sie auf den angegebenen Link zugreifen und Ihren Kontonamen und Ihr Passwort festlegen. invited_you_as_admin: "%{invited_by} hat Sie als Admin von %{application} eingeladen. Sie können die Einladung über den folgenden Link akzeptieren." - invited_you_as_private_user: "%{invited_by} hat Sie als privaten Teilnehmenden zu %{application} eingeladen. Sie können die Einladung über den folgenden Link akzeptieren." someone_invited_you: Jemand hat Sie zu %{application} eingeladen. Sie können die EInladung über den folgenden Link akzeptieren. someone_invited_you_as_admin: Jemand hat Sie als Admin zu %{application} eingeladen. Sie können die Einladung über den folgenden Link akzeptieren. - someone_invited_you_as_private_user: Jemand hat Sie als privaten Teilnehmenden zu %{application} eingeladen. Sie können die Einladung über den folgenden Link akzeptieren. subject: Anweisungen zur Einladung invite_admin: subject: Sie wurden eingeladen %{organization} zu verwalten invite_collaborator: subject: Sie wurden zur Zusammenarbeit auf %{organization} eingeladen - invite_private_user: - subject: Sie wurden zu einem privaten Beteiligungsprozess auf %{organization} eingeladen organization_admin_invitation_instructions: subject: Sie wurden eingeladen %{organization} zu verwalten password_change: diff --git a/decidim-core/config/locales/el.yml b/decidim-core/config/locales/el.yml index 5d32b20ba9a2d..9eaf98a6e2b79 100644 --- a/decidim-core/config/locales/el.yml +++ b/decidim-core/config/locales/el.yml @@ -187,9 +187,6 @@ el: update: "Ο χρήστης %{user_name} ενημέρωσε την εφαρμογή OAuth %{resource_name}" organization: update: "Ο χρήστης %{user_name} ενημέρωσε τις ρυθμίσεις του οργανισμού" - participatory_space_private_user: - create: "Ο χρήστης %{user_name} προσκάλεσε τον χρήστη %{resource_name} να γίνει ιδιωτικός συμμετέχων" - delete: "Ο χρήστης %{user_name} κατάργησε τον συμμετέχοντα %{resource_name} ως ιδιωτικό συμμετέχων" scope: create: "Ο χρήστης %{user_name} δημιούργησε το πεδίο εφαρμογής %{resource_name}" create_with_parent: "Ο χρήστης %{user_name} δημιούργησε το πεδίο εφαρμογής %{resource_name} μέσα στο πεδίο εφαρμογής %{parent_scope}" @@ -990,10 +987,8 @@ el: decline: Απόρριψη πρόσκλησης hello: Γεια σας %{email}, invited_you_as_admin: "Ο χρήστης %{invited_by} σάς προσκάλεσε ως διαχειριστή της εφαρμογής %{application}. Μπορείτε να την αποδεχτείτε μέσω του παρακάτω συνδέσμου." - invited_you_as_private_user: "Ο χρήστης %{invited_by} σάς προσκάλεσε ως ιδιωτικό συμμετέχοντα της εφαρμογής %{application}. Μπορείτε να την αποδεχτείτε μέσω του παρακάτω συνδέσμου." someone_invited_you: Κάποιος σάς προσκάλεσε στην εφαρμογή %{application}. Μπορείτε να την αποδεχτείτε μέσω του παρακάτω συνδέσμου. someone_invited_you_as_admin: Κάποιος σάς προσκάλεσε ως διαχειριστή της εφαρμογής %{application}. Μπορείτε να την αποδεχτείτε μέσω του παρακάτω συνδέσμου. - someone_invited_you_as_private_user: Κάποιος σάς προσκάλεσε ως ιδιωτικό συμμετέχοντα της εφαρμογής %{application}. Μπορείτε να την αποδεχτείτε μέσω του παρακάτω συνδέσμου. subject: Οδηγίες πρόσκλησης password_change: greeting: Γεια σας %{recipient}! diff --git a/decidim-core/config/locales/es-MX.yml b/decidim-core/config/locales/es-MX.yml index cefa402feb4ab..83922eeabe7de 100644 --- a/decidim-core/config/locales/es-MX.yml +++ b/decidim-core/config/locales/es-MX.yml @@ -266,6 +266,10 @@ es-MX: results: resultados impersonation_log: manage: "%{user_name} ha gestionado a %{resource_name} porque %{reason}" + member: + create: "%{user_name} ha invitado a %{resource_name} como miembro" + create_via_csv: "%{user_name} ha invitado a %{resource_name} como miembro vía CSV" + delete: "%{user_name} ha eliminado a la participante %{resource_name} de miembro" moderation: bulk_hide: "%{user_name} ha ocultado %{reported_count} contenidos" bulk_unhide: "%{user_name} ha vuelto a mostrar %{reported_count} contenidos" @@ -284,10 +288,6 @@ es-MX: organization: update: "%{user_name} actualizó la configuración de la organización" update_external_domain: "%{user_name} ha actualizado los dominios externos de la organización" - participatory_space_private_user: - create: "%{user_name} ha invitado a %{resource_name} a ser un usuario privado" - create_via_csv: "%{user_name} invitó a %{resource_name} vía CSV a ser un participante privada" - delete: "%{user_name} ha eliminado el usuario %{resource_name} como usuario privado" scope: create: "%{user_name} creó el ámbito %{resource_name}" create_with_parent: "%{user_name} creó el ámbito %{resource_name} dentro del ámbito %{parent_scope}" @@ -769,13 +769,12 @@ es-MX: id: El identificador único de esta notificación resource_type: El tipo de recurso con el que está relacionada la notificación updated_at: La fecha y hora en que esta notificación fue actualizada por última vez - participatory_space_private_users: - created_at: La fecha y hora en que se creó esta participante privada - id: El identificador único de esta participante privada - privatable_to: A qué espacio de participación pertenece esta participante privada - published: Si esta participante privada está publicado o no - role: El rol tiene que esta participante privada - updated_at: La fecha y hora en que esta participante privada fue actualizada por última vez + participatory_space_members: + created_at: La fecha y hora en que se creó esta miembro + id: El identificador único de esta miembro + published: Si esta miembro está publicada o no + role: El rol tiene que esta miembro + updated_at: La fecha de la última actualización de esta miembro reports: created_at: La fecha y hora en que se creó este informe details: Los detalles de los informes de moderación @@ -1841,17 +1840,17 @@ es-MX: ignore: |- Si no deseas aceptar la invitación, por favor ignora este correo electrónico.
Tu cuenta no se creará hasta que accedas al enlace de arriba y establezcas tu nombre de usuaria y contraseña. invited_you_as_admin: "%{invited_by} te ha invitado como administrador de %{application}. Puedes aceptarlo a través del siguiente enlace." - invited_you_as_private_user: "%{invited_by} te ha invitado como usuario privado de %{application}. Puedes aceptar a través del siguiente enlace." + invited_you_as_member: "%{invited_by} te ha invitado como miembro de %{application}. Puedes aceptarlo a través del siguiente enlace." someone_invited_you: Alguien te ha invitado a %{application}. Puedes aceptar la invitación a través del siguiente enlace. someone_invited_you_as_admin: Alguien te ha invitado como administrador de %{application}, puedes aceptar a través del siguiente enlace. - someone_invited_you_as_private_user: Alguien te ha invitado como usuario_privado de %{application}, puedes aceptarlo a través del siguiente enlace. + someone_invited_you_as_member: Alguien te ha invitado como miembro de %{application}, puedes aceptarlo a través del siguiente enlace. subject: Instrucciones de invitación invite_admin: subject: Te han invitado a administrar %{organization} invite_collaborator: subject: Te han invitado a colaborar en %{organization} - invite_private_user: - subject: Te han invitado a participar en un proceso participativo privado en %{organization} + invite_member: + subject: Te han invitado a participar en un espacio de participación privado en %{organization} organization_admin_invitation_instructions: subject: Te han invitado a administrar %{organization} password_change: diff --git a/decidim-core/config/locales/es-PY.yml b/decidim-core/config/locales/es-PY.yml index ee62da8a9c0e9..2f2b8a05482a9 100644 --- a/decidim-core/config/locales/es-PY.yml +++ b/decidim-core/config/locales/es-PY.yml @@ -266,6 +266,10 @@ es-PY: results: resultados impersonation_log: manage: "%{user_name} ha gestionado a %{resource_name} porque %{reason}" + member: + create: "%{user_name} ha invitado a %{resource_name} como miembro" + create_via_csv: "%{user_name} ha invitado a %{resource_name} como miembro vía CSV" + delete: "%{user_name} ha eliminado a la participante %{resource_name} de miembro" moderation: bulk_hide: "%{user_name} ha ocultado %{reported_count} contenidos" bulk_unhide: "%{user_name} ha vuelto a mostrar %{reported_count} contenidos" @@ -284,10 +288,6 @@ es-PY: organization: update: "%{user_name} actualizó la configuración de la organización" update_external_domain: "%{user_name} ha actualizado los dominios externos de la organización" - participatory_space_private_user: - create: "%{user_name} invitados %{resource_name} para ser un usuario privado" - create_via_csv: "%{user_name} invitó a %{resource_name} vía CSV a ser un participante privada" - delete: "%{user_name} eliminado el usuario %{resource_name} como un usuario privado" scope: create: "%{user_name} creó el ámbito %{resource_name}" create_with_parent: "%{user_name} creó el ámbito %{resource_name} dentro del ámbito %{parent_scope}" @@ -769,13 +769,12 @@ es-PY: id: El identificador único de esta notificación resource_type: El tipo de recurso con el que está relacionada la notificación updated_at: La fecha y hora en que esta notificación fue actualizada por última vez - participatory_space_private_users: - created_at: La fecha y hora en que se creó esta participante privada - id: El identificador único de esta participante privada - privatable_to: A qué espacio de participación pertenece esta participante privada - published: Si esta participante privada está publicado o no - role: El rol tiene que esta participante privada - updated_at: La fecha y hora en que esta participante privada fue actualizada por última vez + participatory_space_members: + created_at: La fecha y hora en que se creó esta miembro + id: El identificador único de esta miembro + published: Si esta miembro está publicada o no + role: El rol tiene que esta miembro + updated_at: La fecha de la última actualización de esta miembro reports: created_at: La fecha y hora en que se creó este informe details: Los detalles de los informes de moderación @@ -1841,17 +1840,17 @@ es-PY: ignore: |- Si no deseas aceptar la invitación, por favor ignora este correo electrónico.
Tu cuenta no se creará hasta que accedas al enlace de arriba y establezcas tu nombre de usuaria y contraseña. invited_you_as_admin: "%{invited_by} te ha invitado como administrador de %{application}. Puedes aceptarlo a través del siguiente enlace." - invited_you_as_private_user: "%{invited_by} te ha invitado como usuario privado de %{application}. Puedes aceptar a través del siguiente enlace." + invited_you_as_member: "%{invited_by} te ha invitado como miembro de %{application}. Puedes aceptarlo a través del siguiente enlace." someone_invited_you: Alguien te ha invitado a %{application}. Puedes aceptar la invitación a través del siguiente enlace. someone_invited_you_as_admin: Alguien te ha invitado como administrador de %{application}, puedes aceptar a través del siguiente enlace. - someone_invited_you_as_private_user: Alguien te ha invitado como usuario_privado de %{application}, puedes aceptarlo a través del siguiente enlace. + someone_invited_you_as_member: Alguien te ha invitado como miembro de %{application}, puedes aceptarlo a través del siguiente enlace. subject: Instrucciones de invitación invite_admin: subject: Te han invitado a administrar %{organization} invite_collaborator: subject: Te han invitado a colaborar en %{organization} - invite_private_user: - subject: Te han invitado a participar en un proceso participativo privado en %{organization} + invite_member: + subject: Te han invitado a participar en un espacio de participación privado en %{organization} organization_admin_invitation_instructions: subject: Te han invitado a administrar %{organization} password_change: diff --git a/decidim-core/config/locales/es.yml b/decidim-core/config/locales/es.yml index cbc6fa42c3acd..3084bbc3521f3 100644 --- a/decidim-core/config/locales/es.yml +++ b/decidim-core/config/locales/es.yml @@ -266,6 +266,10 @@ es: results: resultados impersonation_log: manage: "%{user_name} ha gestionado a %{resource_name} porque %{reason}" + member: + create: "%{user_name} ha invitado a %{resource_name} como miembro" + create_via_csv: "%{user_name} ha invitado a %{resource_name} como miembro vía CSV" + delete: "%{user_name} ha eliminado a la participante %{resource_name} de miembro" moderation: bulk_hide: "%{user_name} ha ocultado %{reported_count} contenidos" bulk_unhide: "%{user_name} ha vuelto a mostrar %{reported_count} contenidos" @@ -284,10 +288,6 @@ es: organization: update: "%{user_name} actualizó la configuración de la organización" update_external_domain: "%{user_name} ha actualizado los dominios externos de la organización" - participatory_space_private_user: - create: "%{user_name} ha invitado a %{resource_name} como participante privada" - create_via_csv: "%{user_name} invitó a %{resource_name} vía CSV a ser un participante privada" - delete: "%{user_name} ha eliminado %{resource_name} como participante privada" scope: create: "%{user_name} creó el ámbito %{resource_name}" create_with_parent: "%{user_name} creó el ámbito %{resource_name} dentro del ámbito %{parent_scope}" @@ -766,13 +766,12 @@ es: id: El identificador único de esta notificación resource_type: El tipo de recurso con el que está relacionada la notificación updated_at: La fecha y hora en que esta notificación fue actualizada por última vez - participatory_space_private_users: - created_at: La fecha y hora en que se creó esta participante privada - id: El identificador único de esta participante privada - privatable_to: A qué espacio de participación pertenece esta participante privada - published: Si esta participante privada está publicado o no - role: El rol tiene que esta participante privada - updated_at: La fecha y hora en que esta participante privada fue actualizada por última vez + participatory_space_members: + created_at: La fecha y hora en que se creó esta miembro + id: El identificador único de esta miembro + published: Si esta miembro está publicada o no + role: El rol tiene que esta miembro + updated_at: La fecha de la última actualización de esta miembro reports: created_at: La fecha y hora en que se creó este informe details: Los detalles de los informes de moderación @@ -1838,16 +1837,16 @@ es: ignore: |- Si no deseas aceptar la invitación, por favor ignora este correo electrónico.
Tu cuenta no se creará hasta que accedas al enlace de arriba y establezcas tu nombre de usuaria y contraseña. invited_you_as_admin: "%{invited_by} te ha invitado como administradora de %{application}. Puedes aceptarlo a través del siguiente enlace." - invited_you_as_private_user: "%{invited_by} te ha invitado como participante privada de %{application}. Puedes aceptar a través del siguiente enlace." + invited_you_as_member: "%{invited_by} te ha invitado como miembro de %{application}. Puedes aceptarlo a través del siguiente enlace." someone_invited_you: Alguien te ha invitado a %{application}. Puedes aceptar la invitación a través del siguiente enlace. someone_invited_you_as_admin: Alguien te ha invitado como administradora de %{application}, puedes aceptar a través del siguiente enlace. - someone_invited_you_as_private_user: Alguien te ha invitado como participante privada de %{application}, puedes aceptar a través del siguiente enlace. + someone_invited_you_as_member: Alguien te ha invitado como miembro de %{application}, puedes aceptarlo a través del siguiente enlace. subject: Instrucciones de invitación invite_admin: subject: Te han invitado a administrar %{organization} invite_collaborator: subject: Te han invitado a colaborar en %{organization} - invite_private_user: + invite_member: subject: Te han invitado a participar en un espacio de participación privado en %{organization} organization_admin_invitation_instructions: subject: Te han invitado a administrar %{organization} diff --git a/decidim-core/config/locales/eu.yml b/decidim-core/config/locales/eu.yml index 4fdad1a1492f7..1550413d9b8ca 100644 --- a/decidim-core/config/locales/eu.yml +++ b/decidim-core/config/locales/eu.yml @@ -45,7 +45,7 @@ eu: email: Zure helbide elektronikoa encrypted_password: Pasahitza locale: Tokikoa - name: Zure Izena + name: Zure izen-abizenak nickname: Ezizena old_password: Egungo pasahitza password: Pasahitza @@ -266,6 +266,10 @@ eu: results: emaitzak impersonation_log: manage: "%{user_name} parte-hartzaileak %{resource_name} kudeatu du arrazoi honengatik %{reason}" + member: + create: "%{user_name} parte-hartzaileak %{resource_name} baliabidera kide izateko gonbidatu du" + create_via_csv: "%{user_name} -k %{resource_name} CSV bidez gonbidatu du kide izateko" + delete: "%{user_name} -k %{resource_name} parte-hartzailea kendu du kide bezala" moderation: bulk_hide: "%{user_name} parte-hartzaileak %{reported_count} baliabide ezkutatu du" bulk_unhide: "%{user_name} parte-hartzaileak berriro erakutsi du %{reported_count} baliabide" @@ -284,10 +288,6 @@ eu: organization: update: "%{user_name} parte-hartzaileak erakundearen ezarpenak eguneratu ditu" update_external_domain: "%{user_name} parte-hartzaileak erakundearen kanpoko domeinuak eguneratu ditu" - participatory_space_private_user: - create: "%{user_name} parte-hartzaileak %{resource_name} gonbidatu du parte-hartzaile pribatua izatera" - create_via_csv: "%{user_name} parte-hartzaileak %{resource_name} gonbidatu du CSV bidez parte-hartzaile pribatua izatera" - delete: "%{user_name} parte-hartzaileak %{resource_name} parte-hartzaile pribatu gisa kendu du" scope: create: "%{user_name} parte-hartzaileak %{resource_name} eremua sortu du" create_with_parent: "%{user_name} parte-hartzaileak %{resource_name} eremua%{parent_scope} eremua barruan sortu du" @@ -729,7 +729,7 @@ eu: connect_your_account_html: 'Konektatu zure kontua saioa hasteko hemen: %{organization}.' publish_content: Argitaratu edukia zuretzat see_email: Ikusi zure helbide elektronikoa - see_name: Ikusi zure izena + see_name: Ikusi zure izena-abizenak see_username: Ikusi zure parte-hartzailearen izena this_application_will_be_able_to: 'Aplikazio horrek honako zeregin hauek egin ahal izango ditu:' this_application_will_not_be_able_to: 'Aplikazio honek ezin izango du:' @@ -767,13 +767,12 @@ eu: id: Jakinarazpen honen identifikatzaile bakarra resource_type: Jakinarazpena zein baliabide motarekin dago lotuta updated_at: Zein datatan eta zein ordutan eguneratu zen jakinarazpena azken aldiz - participatory_space_private_users: - created_at: Zein datatan eta zein ordutan sortu zen parte-hartzaile pribatu hau - id: Parte-hartzaile honen identifikatzaile bakarra - privatable_to: Zein espaziotakoa den parte-hartzaile pribatu hau - published: Ea parte-hartzaile pribatu hau argitaratuta dagoen ala ez - role: Zein rol duen parte-hartzaile pribatu honek - updated_at: Zein datatan eta zein ordutan eguneratu zen parte-hartzaile pribatu hau + participatory_space_members: + created_at: Kide hori sortu zen eguna eta ordua + id: The unique identifier of this member + published: Parte-hartzaile pribatu hori argitaratuta dagoen ala ez + role: Kide horrek duen rola + updated_at: Kide hori azken aldiz eguneratu zen eguna eta ordua reports: created_at: Zein datatan eta zein ordutan sortu zen salaketa hau details: Txosten honen xehetasunak @@ -1565,7 +1564,7 @@ eu: details: Xehetasunak hello: 'Kaixo, %{name}:' manage_moderations: Moderazioak kudeatu - participatory_space: Partaidetza espazioa + participatory_space: Partaidetza-espazioa reason: Arrazoia report_html:

Hurrengo edukiaautomatikoki ezkutatu da

subject: '%{moderator} -k baliabidea ezkutatu du' @@ -1838,17 +1837,17 @@ eu: ignore: |- Gonbidapena onartu nahi ez baduzu, mesedez, ez egin kasurik mezu elektroniko honi. Zure kontua ez da sortuko, goiko estekara sartu eta zure erabiltzaile-izena eta pasahitza ezarri arte. invited_you_as_admin: "%{invited_by} admnisitrariak %{application} administratzaile gisa gonbidatu zaitu. Beheko estekan onartu dezakezu." - invited_you_as_private_user: "%{invited_by} parte-hartzaileak egonbidatu zaitu %{application} ko erabiltzaile pribatu gisa. Beheko estekan onartu dezakezu." + invited_you_as_member: "%{invited_by} k %{application} -ren kide gisa gisa gonbidatu zaitu. Beheko estekan onartu dezakezu." someone_invited_you: Norbaitek %{application} gonbidatu zaitu. Beheko estekan onar dezakezu. someone_invited_you_as_admin: Norbaitek %{application} administratzaile gisa gonbidatu zaitu, beheko estekan onar dezakezu. - someone_invited_you_as_private_user: Norbaitek %{application} ko parte-hartzaile pribatu gisa gonbidatu zaitu, beheko estekan onar dezakezu. + someone_invited_you_as_member: '%{application}-eko kide gisa gonbidatu zaitu norbaitek. Esteka honen bidez onar dezakezu.' subject: Gonbidapenaren argibideak invite_admin: subject: '%{organization} kudeatzeko gonbita jaso duzu' invite_collaborator: subject: 'Gonbita jaso duzu erakunde honekin lankidetzan aritzeko: %{organization}' - invite_private_user: - subject: Gonbidatu zaituzte partaidetza-prozesu pribatu batean %{organization} erakundean + invite_member: + subject: '%{organization} erakundeari buruzko partaidetza prozesu pribatu batera gonbidatu zaituzte' organization_admin_invitation_instructions: subject: '%{organization} erakundea kudeatzeko gonbita jaso duzu' password_change: @@ -1890,13 +1889,13 @@ eu: updated: Pasahitza ondo aldatu da. Saioa hasi duzu. updated_not_active: Pasahitza ondo aldatu da. registrations: - destroyed: Agur! Zure kontua behar bezala bertan behera utzi da. Laster ikusiko dugu berriro. + destroyed: Agur! Zure kontua behar bezala bertan behera utzi da. Laster ikusiko dugu elkar berriro. new: sign_up: Sortu kontu bat signed_up: Ongi etorri! Ondo eman duzu izena. signed_up_but_inactive: Eginda sinatu duzu. Hala ere, ezin izan dugu saioa hasi kontua oraindik ez baita aktibatuta. signed_up_but_locked: Ongi eman duzu izena. Hala ere, ezin duzu saioa hasi zure kontua giltzapetuta dagoelako. - signed_up_but_unconfirmed: Berrespen-esteka duen mezu bat zure helbide elektronikora bidali da. Jarraitu esteka zure kontua aktibatzeko. + signed_up_but_unconfirmed: Berrespen-esteka duen mezu bat bidali da zure helbide elektronikora. Mesedez, jarraitu esteka zure kontua aktibatzeko. update_needs_confirmation: Zure kontua arrakastaz eguneratu duzu, baina helbide elektroniko berria egiaztatu behar dugu. Egiaztatu zure posta elektronikoa eta berretsi esteka jarraitu zure helbide elektroniko berria berresteko. updated: Zure kontua ondo eguneratu da. sessions: @@ -2170,7 +2169,7 @@ eu: profile: Profila title: Parte-hartzailearen ezarpenak locale: - name: Ingelesa + name: Euskara name_with_error: Euskera (akatsa!) number: currency: diff --git a/decidim-core/config/locales/fi-plain.yml b/decidim-core/config/locales/fi-plain.yml index c1eb13f63ddce..ed0b7ddc404dc 100644 --- a/decidim-core/config/locales/fi-plain.yml +++ b/decidim-core/config/locales/fi-plain.yml @@ -266,6 +266,10 @@ fi-pl: results: tulokset impersonation_log: manage: "%{user_name} hallinnoi käyttäjää %{resource_name}, koska %{reason}" + member: + create: "%{user_name} kutsui henkilön %{resource_name} jäseneksi" + create_via_csv: "%{user_name} kutsui henkilön %{resource_name} jäseneksi CSV-tuonnin kautta" + delete: "%{user_name} poisti osallistujan %{resource_name} jäsenyyden" moderation: bulk_hide: "%{user_name} piilotti %{reported_count} kohdetta" bulk_unhide: "%{user_name} palautti %{reported_count} kohdetta näkyviin" @@ -284,10 +288,6 @@ fi-pl: organization: update: "%{user_name} päivitti organisaation asetuksia" update_external_domain: "%{user_name} päivitti organisaation ulkoisia verkkotunnuksia" - participatory_space_private_user: - create: "%{user_name} kutsui %{resource_name} yksityiseksi käyttäjaksi" - create_via_csv: "%{user_name} kutsui yksityisen osallistujan %{resource_name} CSV-tuonnin kautta" - delete: "%{user_name} poisti yksityisen käyttäjän %{resource_name}" scope: create: "%{user_name} loi %{resource_name} teeman" create_with_parent: "%{user_name} loi %{resource_name} teeman %{parent_scope} teeman alle" @@ -769,13 +769,13 @@ fi-pl: id: Ilmoituksen yksilöivä tunniste resource_type: Ilmoituksen kohteen tyyppi updated_at: Ilmoituksen viimeisimmän päivityksen ajankohta - participatory_space_private_users: - created_at: Yksityisen käyttäjän luontiaika - id: Yksityisen käyttäjän yksilöivä tunniste - privatable_to: Mihin osallistumistilaan tämä yksityinen käyttäjä kuuluu - published: Onko yksityinen käyttäjä julkaistu vai ei - role: Yksityisen käyttäjän rooli - updated_at: Yksityisen käyttäjän viimeisimmän päivityksen ajankohta + participatory_space_members: + created_at: Jäsenen luontiaika + id: Jäsenen yksilöivä tunniste + participatory_space: Mihin osallistumistilaan tämä jäsen kuuluu + published: Onko jäsen julkaistu vai ei + role: Jäsenen rooli + updated_at: Jäsenen viimeisimmän päivityksen ajankohta reports: created_at: Ilmoituksen luontiaika details: Ilmoituksen tiedot @@ -1841,16 +1841,16 @@ fi-pl: Jos et halua hyväksyä kutsua, jätä tämä viesti huomioimatta.
Sinulle ei luoda tiliä ennen kuin seuraat yllä olevaa linkkiä ja asetat käyttäjänimen sekä salasanan. invited_you_as_admin: "%{invited_by} on kutsunut sinut hallinnoimaan kohdetta %{application}. Voit hyväksyä sen alla olevan linkin kautta." - invited_you_as_private_user: "%{invited_by} on kutsunut sinut yksityiseksi käyttäjäksi %{application} -nimiseen palveluun. Voit hyväksyä kutsun alla olevan linkin kautta." + invited_you_as_member: "%{invited_by} on kutsunut sinut jäseneksi palveluun %{application}. Voit hyväksyä kutsun alla olevan linkin avulla." someone_invited_you: Joku on kutsunut sinut kohteeseen %{application}. Voit hyväksyä sen alla olevan linkin kautta. someone_invited_you_as_admin: Joku on kutsunut sinut ylläpitämään palvelua %{application}. Voit hyväksyä kutsun alla olevan linkin avulla. - someone_invited_you_as_private_user: Joku on kutsunut sinut yksityiseksi käyttäjäksi %{application} -nimiseen palveluun. Voit hyväksyä kutsun alla olevan linkin kautta. + someone_invited_you_as_member: Joku on kutsunut sinut jäseneksi palveluun %{application}. Voit hyväksyä kutsun alla olevan linkin kautta. subject: Kutsuohjeet invite_admin: subject: Sinut on kutsuttu hallinnoimaan palvelua %{organization} invite_collaborator: subject: Sinut on kutsuttu osallistumaan palvelussa %{organization} - invite_private_user: + invite_member: subject: Sinut on kutsuttu yksityiseen osallistumisprosessiin palvelussa %{organization} organization_admin_invitation_instructions: subject: Sinut on kutsuttu hallinnoimaan palvelua %{organization} diff --git a/decidim-core/config/locales/fi.yml b/decidim-core/config/locales/fi.yml index 7fcb93aaee6dc..81f17955755e0 100644 --- a/decidim-core/config/locales/fi.yml +++ b/decidim-core/config/locales/fi.yml @@ -266,6 +266,10 @@ fi: results: tulokset impersonation_log: manage: "%{user_name} hallinnoi käyttäjää %{resource_name}, koska %{reason}" + member: + create: "%{user_name} kutsui henkilön %{resource_name} jäseneksi" + create_via_csv: "%{user_name} kutsui henkilön %{resource_name} jäseneksi CSV-tuonnin kautta" + delete: "%{user_name} poisti osallistujan %{resource_name} jäsenyyden" moderation: bulk_hide: "%{user_name} piilotti %{reported_count} kohdetta" bulk_unhide: "%{user_name} palautti %{reported_count} kohdetta näkyviin" @@ -284,10 +288,6 @@ fi: organization: update: "%{user_name} päivitti organisaation asetuksia" update_external_domain: "%{user_name} päivitti organisaation ulkoisia verkkotunnuksia" - participatory_space_private_user: - create: "%{user_name} kutsui käyttäjän %{resource_name} yksityiseksi käyttäjäksi" - create_via_csv: "%{user_name} kutsui yksityisen osallistujan %{resource_name} CSV-tuonnin kautta" - delete: "%{user_name} poisti yksityisen käyttäjän %{resource_name}" scope: create: "%{user_name} loi %{resource_name} teeman" create_with_parent: "%{user_name} loi %{resource_name} teeman %{parent_scope} teeman alle" @@ -769,13 +769,13 @@ fi: id: Ilmoituksen yksilöivä tunniste resource_type: Ilmoituksen kohteen tyyppi updated_at: Ilmoituksen viimeisimmän päivityksen ajankohta - participatory_space_private_users: - created_at: Yksityisen käyttäjän luontiaika - id: Yksityisen käyttäjän yksilöivä tunniste - privatable_to: Mihin osallistumistilaan tämä yksityinen käyttäjä kuuluu - published: Onko yksityinen käyttäjä julkaistu vai ei - role: Yksityisen käyttäjän rooli - updated_at: Yksityisen käyttäjän viimeisimmän päivityksen ajankohta + participatory_space_members: + created_at: Jäsenen luontiaika + id: Jäsenen yksilöivä tunniste + participatory_space: Mihin osallistumistilaan tämä jäsen kuuluu + published: Onko jäsen julkaistu vai ei + role: Jäsenen rooli + updated_at: Jäsenen viimeisimmän päivityksen ajankohta reports: created_at: Ilmoituksen luontiaika details: Ilmoituksen tiedot @@ -1841,16 +1841,16 @@ fi: Jos et halua hyväksyä kutsua, jätä tämä viesti huomioimatta.
Sinulle ei luoda tiliä ennen kuin seuraat yllä olevaa linkkiä ja asetat käyttäjänimen sekä salasanan. invited_you_as_admin: "%{invited_by} on kutsunut sinut hallinnoimaan kohdetta %{application}. Voit hyväksyä sen alla olevan linkin kautta." - invited_you_as_private_user: "%{invited_by} on kutsunut sinut yksityiseksi käyttäjäksi %{application} -nimiseen palveluun. Voit hyväksyä kutsun alla olevan linkin kautta." + invited_you_as_member: "%{invited_by} on kutsunut sinut jäseneksi palveluun %{application}. Voit hyväksyä kutsun alla olevan linkin avulla." someone_invited_you: Joku on kutsunut sinut kohteeseen %{application}. Voit hyväksyä sen alla olevan linkin kautta. someone_invited_you_as_admin: Joku on kutsunut sinut ylläpitämään palvelua %{application}. Voit hyväksyä kutsun alla olevan linkin avulla. - someone_invited_you_as_private_user: Joku on kutsunut sinut yksityiseksi käyttäjäksi %{application} -nimiseen palveluun. Voit hyväksyä kutsun alla olevan linkin kautta. + someone_invited_you_as_member: Joku on kutsunut sinut jäseneksi palveluun %{application}. Voit hyväksyä kutsun alla olevan linkin kautta. subject: Kutsuohjeet invite_admin: subject: Sinut on kutsuttu hallinnoimaan palvelua %{organization} invite_collaborator: subject: Sinut on kutsuttu osallistumaan palvelussa %{organization} - invite_private_user: + invite_member: subject: Sinut on kutsuttu yksityiseen osallistumisprosessiin palvelussa %{organization} organization_admin_invitation_instructions: subject: Sinut on kutsuttu hallinnoimaan palvelua %{organization} diff --git a/decidim-core/config/locales/fr-CA.yml b/decidim-core/config/locales/fr-CA.yml index cc9a41afa6466..2edfd9040f55d 100644 --- a/decidim-core/config/locales/fr-CA.yml +++ b/decidim-core/config/locales/fr-CA.yml @@ -250,6 +250,10 @@ fr-CA: results: résultats impersonation_log: manage: "%{user_name} a représenté %{resource_name} parce que %{reason}" + member: + create: "%{user_name} a invité %{resource_name} à devenir membre" + create_via_csv: "%{user_name} a invité %{resource_name} via CSV à devenir membre" + delete: "%{user_name} a supprimé le participant %{resource_name} en tant que membre" moderation: hide: "%{user_name} a caché une ressource de type %{resource_type} dans l'espace %{space_name}" unreport: "%{user_name} a annulé le signalement d'une ressource de type %{resource_type} dans l'espace %{space_name}" @@ -265,10 +269,6 @@ fr-CA: organization: update: "%{user_name} a mis à jour les paramètres de l'organisation" update_external_domain: "%{user_name} a mis à jour les domaines externes de l'organisation" - participatory_space_private_user: - create: "%{user_name} invité %{resource_name} pour être un utilisateur privé" - create_via_csv: "%{user_name} a invité %{resource_name} via CSV à être un utilisateur privé" - delete: "%{user_name} supprimé l'utilisateur %{resource_name} tant qu'utilisateur privé" scope: create: "%{user_name} a créé le périmètre d'application %{resource_name}" create_with_parent: "%{user_name} a créé le périmètre d'application %{resource_name} à l'intérieur du périmètre %{parent_scope}" @@ -704,13 +704,13 @@ fr-CA: updated_at: La date de la dernière mise à jour de cette conversation notifications: extra: Informations supplémentaires sur cette notification - participatory_space_private_users: - created_at: La date et l'heure de création de cet utilisateur privé - id: L'identifiant unique de cet utilisateur privé - privatable_to: À quel espace cet utilisateur privé appartient - published: Que cet utilisateur privé soit publié ou non - role: Le rôle que cet utilisateur privé a - updated_at: La date et l'heure de la dernière mise à jour de cet utilisateur privé + participatory_space_members: + created_at: La date et l'heure de création de ce membre + id: L'identifiant unique de ce membre + participatory_space: À quel espace ce membre appartient + published: Que ce membre soit publié ou non + role: Le rôle que ce membre a + updated_at: La date et l'heure de la dernière mise à jour de ce membre reports: created_at: La date et l'heure de création de ce rapport details: Les détails de ce rapport @@ -1683,17 +1683,17 @@ fr-CA: ignore: |- Si vous n’acceptez pas l’invitation, veuillez ignorer ce courrier électronique.
Votre compte ne sera pas créé tant que vous n’aurez pas accédé au lien ci-dessus et que vous n’aurez pas configuré votre pseudo et votre mot de passe. invited_you_as_admin: "%{invited_by} vous a invité en tant qu'administrateur de %{application}. Vous pouvez accepter cette invitation grâce au lien ci-dessous." - invited_you_as_private_user: "%{invited_by} vous a invité en tant qu'utilisateur privé de %{application}. Vous pouvez l'accepter via le lien ci-dessous." + invited_you_as_member: "%{invited_by} vous a invité en tant que membre de %{application}. Vous pouvez accepter cette invitation via le lien ci-dessous." someone_invited_you: Vous avez été invité à %{application}. Vous pouvez accepter cette invitation grâce au lien ci-dessous. someone_invited_you_as_admin: Vous avez été invité en tant qu'administrateur de %{application}, vous pouvez l'accepter via le lien ci-dessous. - someone_invited_you_as_private_user: Vous avez été invité en tant qu'utilisateur privé de %{application}, vous pouvez l'accepter via le lien ci-dessous. + someone_invited_you_as_member: Quelqu'un vous a invité en tant que membre de %{application}, vous pouvez accepter cette invitation via le lien ci-dessous. subject: Instructions d'invitation invite_admin: subject: Vous avez été invité(e) à gérer %{organization} invite_collaborator: subject: Vous avez été invité à collaborer sur %{organization} - invite_private_user: - subject: Vous avez été invité(e) à une concertation privés sur %{organization} + invite_member: + subject: Vous avez été invité(e) à un processus participatif privé sur %{organization} organization_admin_invitation_instructions: subject: Vous avez été invité(e) à gérer %{organization} password_change: diff --git a/decidim-core/config/locales/fr.yml b/decidim-core/config/locales/fr.yml index e6539e3c563d4..ae9eac29ab679 100644 --- a/decidim-core/config/locales/fr.yml +++ b/decidim-core/config/locales/fr.yml @@ -250,6 +250,10 @@ fr: results: résultats impersonation_log: manage: "%{user_name} a représenté %{resource_name} parce que %{reason}" + member: + create: "%{user_name} a invité %{resource_name} à devenir membre" + create_via_csv: "%{user_name} a invité %{resource_name} via CSV à devenir membre" + delete: "%{user_name} a supprimé le participant %{resource_name} en tant que membre" moderation: hide: "%{user_name} a caché une ressource de type %{resource_type} dans l'espace %{space_name}" unreport: "%{user_name} a annulé le signalement d'une ressource de type %{resource_type} dans l'espace %{space_name}" @@ -265,10 +269,6 @@ fr: organization: update: "%{user_name} a mis à jour les paramètres de l'organisation" update_external_domain: "%{user_name} a mis à jour les domaines externes de l'organisation" - participatory_space_private_user: - create: "%{user_name} invité %{resource_name} pour être un utilisateur privé" - create_via_csv: "%{user_name} a invité %{resource_name} via CSV à être un utilisateur privé" - delete: "%{user_name} supprimé l'utilisateur %{resource_name} tant qu'utilisateur privé" scope: create: "%{user_name} a créé le périmètre d'application %{resource_name}" create_with_parent: "%{user_name} a créé le périmètre d'application %{resource_name} à l'intérieur du périmètre %{parent_scope}" @@ -704,13 +704,13 @@ fr: updated_at: La date de la dernière mise à jour de cette conversation notifications: extra: Informations supplémentaires sur cette notification - participatory_space_private_users: - created_at: La date et l'heure de création de cet utilisateur privé - id: L'identifiant unique de cet utilisateur privé - privatable_to: À quel espace cet utilisateur privé appartient - published: Que cet utilisateur privé soit publié ou non - role: Le rôle que cet utilisateur privé a - updated_at: La date et l'heure de la dernière mise à jour de cet utilisateur privé + participatory_space_members: + created_at: La date et l'heure de création de ce membre + id: L'identifiant unique de ce membre + participatory_space: À quel espace ce membre appartient + published: Que ce membre soit publié ou non + role: Le rôle que ce membre a + updated_at: La date et l'heure de la dernière mise à jour de ce membre reports: created_at: La date et l'heure de création de ce rapport details: Les détails de ce rapport @@ -1683,17 +1683,17 @@ fr: ignore: |- Si vous n’acceptez pas l’invitation, veuillez ignorer ce courrier électronique.
Votre compte ne sera pas créé tant que vous n’aurez pas accédé au lien ci-dessus et que vous n’aurez pas configuré votre pseudo et votre mot de passe. invited_you_as_admin: "%{invited_by} vous a invité en tant qu'administrateur de %{application}. Vous pouvez accepter cette invitation grâce au lien ci-dessous." - invited_you_as_private_user: "%{invited_by} vous a invité en tant qu'utilisateur privé de %{application}. Vous pouvez l'accepter via le lien ci-dessous." + invited_you_as_member: "%{invited_by} vous a invité en tant que membre de %{application}. Vous pouvez accepter cette invitation via le lien ci-dessous." someone_invited_you: Vous avez été invité à %{application}. Vous pouvez accepter cette invitation grâce au lien ci-dessous. someone_invited_you_as_admin: Vous avez été invité en tant qu'administrateur de %{application}, vous pouvez l'accepter via le lien ci-dessous. - someone_invited_you_as_private_user: Vous avez été invité en tant qu'utilisateur privé de %{application}, vous pouvez l'accepter via le lien ci-dessous. + someone_invited_you_as_member: Quelqu'un vous a invité en tant que membre de %{application}, vous pouvez accepter cette invitation via le lien ci-dessous. subject: Instructions d'invitation invite_admin: subject: Vous avez été invité(e) à gérer %{organization} invite_collaborator: subject: Vous avez été invité à collaborer sur %{organization} - invite_private_user: - subject: Vous avez été invité(e) à une concertation privés sur %{organization} + invite_member: + subject: Vous avez été invité(e) à un processus participatif privé sur %{organization} organization_admin_invitation_instructions: subject: Vous avez été invité(e) à gérer %{organization} password_change: diff --git a/decidim-core/config/locales/gl.yml b/decidim-core/config/locales/gl.yml index 5e30ec16bbad4..4df1864fd2b35 100644 --- a/decidim-core/config/locales/gl.yml +++ b/decidim-core/config/locales/gl.yml @@ -109,9 +109,6 @@ gl: update: "%{user_name} actualizou a aplicación %{resource_name} OAuth" organization: update: "%{user_name} actualizou a configuración da organización" - participatory_space_private_user: - create: "%{user_name} invitados %{resource_name} para ser un usuario privado" - delete: "%{user_name} eliminou o usuario %{resource_name} como usuario privado" scope: create: "O %{user_name} creou o alcance %{resource_name}" create_with_parent: "%{user_name} creou o alcance %{resource_name} no ámbito %{parent_scope}" @@ -703,10 +700,8 @@ gl: decline: Declina a invitación hello: Ola %{email}, invited_you_as_admin: "%{invited_by} invitoute como administrador de %{application}. Podes aceptalo a través da seguinte ligazón." - invited_you_as_private_user: "%{invited_by} convidoute como usuario privado de %{application}. Podes aceptalo a través da seguinte ligazón." someone_invited_you: Alguén te invitou a %{application}. Podes aceptalo a través da seguinte ligazón. someone_invited_you_as_admin: Alguén te convidou como administrador de %{application}, podes aceptalo a través da seguinte ligazón. - someone_invited_you_as_private_user: Alguén te convidou como usuario privado de %{application}, podes aceptalo a través da seguinte ligazón. subject: Instrucións de invitación password_change: greeting: Ola %{recipient}! diff --git a/decidim-core/config/locales/hu.yml b/decidim-core/config/locales/hu.yml index dae16c42eb94c..5e739e6f041b5 100644 --- a/decidim-core/config/locales/hu.yml +++ b/decidim-core/config/locales/hu.yml @@ -197,10 +197,6 @@ hu: organization: update: "%{user_name} frissítette a szervezet beállításait" update_external_domain: "%{user_name} frissítette a szervezet külső domainjeit" - participatory_space_private_user: - create: "%{user_name} meghívott %{resource_name} privát felhasználónak" - create_via_csv: "%{user_name} meghívta %{resource_name} önt, hogy privát résztvevő legyen" - delete: "%{user_name} eltávolította a felhasználót %{resource_name} privát felhasználóként" scope: create: "%{user_name} létrehozta a(z) %{resource_name} hatáskört" create_with_parent: "%{user_name} létrehozta a(z) %{resource_name} hatáskört ezen belül: %{parent_scope}" @@ -1213,17 +1209,13 @@ hu: Ha nem szeretné elfogadni a meghívást, hagyja figyelmen kívül ezt az e-mailt.
Fiókját addig nem hozzuk létre, amíg nem használja a fenti hivatkozást, és nem adja meg becenevét és jelszavát. invited_you_as_admin: "%{invited_by} meghívott, mint admint, hogy csatlakozz ehhez: %{application}. Jelezz vissza az alábbi linken." - invited_you_as_private_user: "%{invited_by} meghívott, mint privát felhasználót, hogy csatlakozz ehhez: %{application}. Jelezz vissza az alábbi linken." someone_invited_you: 'Valaki meghívott, hogy csatlakozz ehhez: %{application}. Jelezz vissza az alábbi linken.' someone_invited_you_as_admin: 'Valaki meghívott, mint admint, hogy csatlakozz ehhez: %{application}. Jelezz vissza az alábbi linken.' - someone_invited_you_as_private_user: 'Valaki meghívott, mint privát felhasználót, hogy csatlakozz ehhez: %{application}. Jelezz vissza az alábbi linken.' subject: Meghívó utasítások invite_admin: subject: 'Meghívást kapott, hogy ezt kezelje a következő szervezetet: %{organization}' invite_collaborator: subject: Meghívták együttműködésre a(z) %{organization} szervezetben - invite_private_user: - subject: Meghívtak egy privát folyamatban való részvételre a (%{organization}) szervezetnél organization_admin_invitation_instructions: subject: 'Meghívást kapott, hogy kezelje a következő szervezetet: %{organization}' password_change: diff --git a/decidim-core/config/locales/id-ID.yml b/decidim-core/config/locales/id-ID.yml index 6fce30b52ef4c..1a28d7880f575 100644 --- a/decidim-core/config/locales/id-ID.yml +++ b/decidim-core/config/locales/id-ID.yml @@ -96,9 +96,6 @@ id: update: "%{user_name} memperbarui aplikasi %{resource_name} OAuth" organization: update: "%{user_name} memperbarui pengaturan organisasi" - participatory_space_private_user: - create: "%{user_name} diundang %{resource_name} untuk menjadi pengguna pribadi" - delete: "%{user_name} menghapus pengguna %{resource_name} sebagai pengguna pribadi" scope: create: "%{user_name} menciptakan %{resource_name} ruang lingkup" create_with_parent: "%{user_name} menciptakan %{resource_name} ruang lingkup di dalam %{parent_scope} ruang lingkup" @@ -626,10 +623,8 @@ id: decline: Tolak undangan hello: Halo %{email}, invited_you_as_admin: "%{invited_by} telah mengundang Anda sebagai admin %{application}. Anda dapat menerimanya melalui tautan di bawah ini." - invited_you_as_private_user: "%{invited_by} telah mengundang Anda sebagai pengguna pribadi dari %{application}. Anda dapat menerimanya melalui tautan di bawah ini." someone_invited_you: Seseorang telah mengundang Anda ke %{application}. Anda dapat menerimanya melalui tautan di bawah ini. someone_invited_you_as_admin: Seseorang telah mengundang Anda sebagai admin %{application}, Anda dapat menerimanya melalui tautan di bawah. - someone_invited_you_as_private_user: Seseorang telah mengundang Anda sebagai pengguna pribadi %{application}, Anda dapat menerimanya melalui tautan di bawah ini. subject: Instruksi undangan password_change: greeting: Halo %{recipient}! diff --git a/decidim-core/config/locales/it.yml b/decidim-core/config/locales/it.yml index 57a7b76e469d3..6199d2dde1612 100644 --- a/decidim-core/config/locales/it.yml +++ b/decidim-core/config/locales/it.yml @@ -204,10 +204,6 @@ it: organization: update: "%{user_name} aggiornato le impostazioni dell'organizzazione" update_external_domain: "%{user_name} ha aggiornato i domini esterni dell'organizzazione" - participatory_space_private_user: - create: "%{user_name} invitato %{resource_name} a essere un utente privato" - create_via_csv: "%{user_name} ha invitato %{resource_name} tramite csv a unirsi come partecipante privato" - delete: "%{user_name} rimosso l'utente %{resource_name} come utente privato" scope: create: "%{user_name} ha creato l'ambito %{resource_name}" create_with_parent: "%{user_name} ha creato l'ambito %{resource_name} all'interno dell'ambito %{parent_scope}" @@ -1030,10 +1026,8 @@ it: decline: Rifiuta l'invito hello: Ciao %{email}, invited_you_as_admin: "%{invited_by} ti ha invitato come amministratore di %{application}. Puoi accettare l'invito facendo clic sul link sottostante." - invited_you_as_private_user: "%{invited_by} ti ha invitato come utente privato di %{application}. Puoi accettarlo tramite il link sottostante." someone_invited_you: Qualcuno ti ha invitato a %{application}. Puoi accettare l'invito facendo clic sul link sottostante. someone_invited_you_as_admin: Qualcuno ti ha invitato come amministratore di %{application}. Puoi accettare l'invito facendo clic sul link sottostante. - someone_invited_you_as_private_user: Qualcuno ti ha invitato come private_user di %{application}, puoi accettarlo tramite il link sottostante. subject: Istruzioni di invito password_change: greeting: Ciao %{recipient}! diff --git a/decidim-core/config/locales/ja.yml b/decidim-core/config/locales/ja.yml index 0daeb2cdb32dd..bbc92071386ea 100644 --- a/decidim-core/config/locales/ja.yml +++ b/decidim-core/config/locales/ja.yml @@ -271,10 +271,6 @@ ja: organization: update: "%{user_name} が組織設定を更新しました" update_external_domain: "%{user_name} が組織の外部ドメインを更新しました" - participatory_space_private_user: - create: "%{user_name} が %{resource_name} をプライベート参加者に招待しました" - create_via_csv: "%{user_name} が %{resource_name} をCSV経由でプライベート参加者に招待しました" - delete: "%{user_name} がプライベート参加者の %{resource_name} を削除しました" scope: create: "%{user_name} が %{resource_name} スコープを作成しました" create_with_parent: "%{user_name} は %{resource_name} スコープ内で %{parent_scope} スコープを作成しました" @@ -751,13 +747,6 @@ ja: id: この通知の固有ID resource_type: 通知に関連するリソースの種類 updated_at: 通知の最終更新日時 - participatory_space_private_users: - created_at: プライベートユーザーの作成日時 - id: このプライベートユーザーの固有ID - privatable_to: このプライベートユーザーが所属する領域: - published: このプライベートユーザーが公開されているか否か - role: このプライベートユーザーが持つ役割 - updated_at: プライベートユーザーの最終更新日時 reports: created_at: レポートの作成日時 details: レポートの詳細 @@ -1275,8 +1264,10 @@ ja: action_error: 通知設定の更新に問題が発生しました。 no_notifications: まだ通知はありません。 show: + deleted: コンテンツは著者によって削除されました。 missing_event: この通知は利用できなくなった要素に属しています。破棄してください。 moderated: コンテンツはモデレーションにより非表示になっています。 + not_available: この通知は現在利用できないアイテムに関するものです。破棄できます。 notifications_digest_mailer: header: daily: 日次の通知のダイジェスト @@ -1818,17 +1809,13 @@ ja: ignore: |- 招待を受け入れたくない場合は、このメールを無視してください。
上記のリンクにアクセスし、アカウントIDとパスワードを設定するまで、あなたのアカウントは作成されません。 invited_you_as_admin: "%{invited_by} さんが %{application}の管理者としてあなたを招待しました。以下のリンクから承認できます。" - invited_you_as_private_user: "%{invited_by} が %{application} のプライベート参加者としてあなたを招待しました。以下のリンクから承認できます。" someone_invited_you: '%{application} にあなたを招待しました。以下のリンクから受け入れることができます。' someone_invited_you_as_admin: '%{application} の管理者としてあなたを招待しました。以下のリンクから承認できます。' - someone_invited_you_as_private_user: '%{application} のプライベート参加者としてあなたは招待されています。以下のリンクから承認できます。' subject: 招待の説明 invite_admin: subject: '%{organization} を管理するために招待されています' invite_collaborator: subject: あなたは %{organization} で共同作業に招待されています - invite_private_user: - subject: あなたは %{organization} のプライベート参加プロセスに招待されています organization_admin_invitation_instructions: subject: '%{organization} を管理するために招待されています' password_change: diff --git a/decidim-core/config/locales/lb.yml b/decidim-core/config/locales/lb.yml index 06ff8beeed07a..cea1645fb3d55 100644 --- a/decidim-core/config/locales/lb.yml +++ b/decidim-core/config/locales/lb.yml @@ -118,10 +118,6 @@ lb: update: "%{user_name} huet d'OAuth-Applikation %{resource_name} aktualiséiert" organization: update: "%{user_name} huet d'Organisatiouns-Astellungen aktualiséiert" - participatory_space_private_user: - create: "%{user_name} huet d' %{resource_name} als privaten Benotzer agelueden" - create_via_csv: "%{user_name} huet d'%{resource_name} per CSV als privaten Teilnehmer agelueden" - delete: "%{user_name} huet den Benotzer %{resource_name} als privaten Benotzer geläscht" scope: create: "%{user_name} huet den Beräich %{resource_name} erstallt" create_with_parent: "%{user_name} huet den Beräich %{resource_name} am iwwergeuerdenten Beräich %{parent_scope} erstallt" @@ -886,10 +882,8 @@ lb: decline: Invitatioun refuséieren hello: Moien %{email}, invited_you_as_admin: "%{invited_by} huet dech als Admin vun %{application} aglueden. Du kanns se unhuelen andeems op den Link ënnen gees." - invited_you_as_private_user: "%{invited_by} huet dech als privaten Teilnehmer vun %{application} agelueden. Du déi kanns iwwer den folgenden Link unhuelen." someone_invited_you: Et huet een dech fir d' %{application} agelueden. Du kanns dat unhuelen andeems du op op den Link ënnen gees. someone_invited_you_as_admin: Et huet een dech als Admin op %{application} agelueden, du kanns dat iwwer den Link ënnen unhuelen. - someone_invited_you_as_private_user: Jemand hat dich als private_user von %{application}eingeladen, du kannst es über den folgenden Link akzeptieren. subject: Instruktiounen fir d'Invitatioun password_change: greeting: Moien %{recipient}! diff --git a/decidim-core/config/locales/lt.yml b/decidim-core/config/locales/lt.yml index 7e2f3abc87d5c..5ba6692f34ccd 100644 --- a/decidim-core/config/locales/lt.yml +++ b/decidim-core/config/locales/lt.yml @@ -246,10 +246,6 @@ lt: organization: update: "%{user_name} atnaujino organizacijos nustatymus" update_external_domain: "%{user_name} atnaujino organizacijos išorinius domenus" - participatory_space_private_user: - create: "%{user_name} pakvietė %{resource_name} būti privačiu dalyviu" - create_via_csv: "%{user_name} pakvietė %{resource_name} per CSV tapti privačiu dalyviu" - delete: "%{user_name} pašalino %{resource_name} dalyvį iš privačių dalyvių" scope: create: "%{user_name} sukūrė %{resource_name} sritį" create_with_parent: "%{user_name} sukūrė %{resource_name} sritį %{parent_scope} srityje" @@ -1299,17 +1295,13 @@ lt: Jei nenorite priimti kvietimo, nekreipkite dėmesio į šį e. laišką.
jūsų paskyra nebus sukurta tol, kol nespustelėsite pirmiau pateiktos nuorodos ir nenustatysite savo naudotojo vardo ir slaptažodžio. invited_you_as_admin: "%{invited_by} pakvietė jus kaip %{application} administratorių. Kvietimą galite priimti spustelėję toliau pateikiamą nuorodą." - invited_you_as_private_user: "%{invited_by} pakvietė jus kaip %{application} privatų dalyvį. Kvietimą galite priimti spustelėję toliau pateikiamą nuorodą." someone_invited_you: Kažkas pakvietė jus į %{application}. Kvietimą galite priimti spustelėję toliau pateikiamą nuorodą. someone_invited_you_as_admin: Kažkas pakvietė jus kaip %{application} administratorių. Kvietimą galite priimti spustelėję toliau pateikiamą nuorodą. - someone_invited_you_as_private_user: Kažkas pakvietė jus kaip %{application} privatų dalyvį. Kvietimą galite priimti spustelėję toliau pateikiamą nuorodą. subject: Kvietimo nurodymai invite_admin: subject: Esate pakviestas tvarkyti %{organization} invite_collaborator: subject: Esate pakviesta(-s) bendradarbiauti %{organization} - invite_private_user: - subject: Esate pakviesta(-s) į %{organization} uždarą dalyvaujamąjį procesą organization_admin_invitation_instructions: subject: Esate pakviesta(-s) tvarkyti %{organization} password_change: diff --git a/decidim-core/config/locales/lv.yml b/decidim-core/config/locales/lv.yml index cb5b98ac9c57a..45107a82eb88d 100644 --- a/decidim-core/config/locales/lv.yml +++ b/decidim-core/config/locales/lv.yml @@ -110,9 +110,6 @@ lv: update: "%{user_name} atjaunināja %{resource_name} OAuth lietojumprogrammu" organization: update: "%{user_name} atjaunināja organizācijas iestatījumus" - participatory_space_private_user: - create: "%{user_name} uzaicināja %{resource_name} par privāto dalībnieku" - delete: "%{user_name} noņēma %{resource_name} kā privāto dalībnieku" scope: create: "%{user_name} izveidoja darbības tvērumu %{resource_name}" create_with_parent: "%{user_name} izveidoja darbības tvērumu %{resource_name} darbības tvērumā %{parent_scope}" @@ -736,10 +733,8 @@ lv: decline: Noraidīt ielūgumu hello: Sveiki, %{email}! invited_you_as_admin: "%{invited_by} uzaicināja jūs kā %{application} administratoru. Jūs varat ielūgumu pieņemt, izmantojot turpmāk esošo saiti." - invited_you_as_private_user: "%{invited_by} uzaicināja jūs kā %{application} privātu dalībnieku. Jūs varat ielūgumu pieņemt, izmantojot zemāk esošo saiti." someone_invited_you: Kāds jūs ir uzaicinājis uz %{application}. Jūs varat ielūgumu pieņemt, izmantojot turpmāk esošo saiti. someone_invited_you_as_admin: Kāds ir uzaicinājis jūs kā %{application} administratoru. Jūs varat ielūgumu pieņemt, izmantojot zemāk esošo saiti. - someone_invited_you_as_private_user: Kāds ir uzaicinājis jūs kā %{application} privātu dalībnieku. Jūs varat ielūgumu pieņemt, izmantojot zemāk esošo saiti. subject: Ielūgumu norādījumi password_change: greeting: Sveiki, %{recipient}! diff --git a/decidim-core/config/locales/nl.yml b/decidim-core/config/locales/nl.yml index 628e7378f1ad9..584a3217c5034 100644 --- a/decidim-core/config/locales/nl.yml +++ b/decidim-core/config/locales/nl.yml @@ -125,10 +125,6 @@ nl: update: "%{user_name} heeft de %{resource_name} OAuth-toepassing bijgewerkt" organization: update: "%{user_name} heeft de organisatie-instellingen bijgewerkt" - participatory_space_private_user: - create: "%{user_name} heeft %{resource_name} uitgenodigd om een privé deelnemer te zijn" - create_via_csv: "%{user_name} heeft %{resource_name} uitgenodigd als privé deelnemer" - delete: "%{user_name} heeft de deelnemer %{resource_name} verwijderd als privé-deelnemer" scope: create: "%{user_name} heeft het bereik %{resource_name} gemaakt" create_with_parent: "%{user_name} heeft het bereik %{resource_name} gemaakt binnen het bereik %{parent_scope}" @@ -879,10 +875,8 @@ nl: decline: Uitnodiging afwijzen hello: Hallo %{email}, invited_you_as_admin: "%{invited_by} heeft je uitgenodigd als admin van %{application}. U kunt het accepteren via de onderstaande link." - invited_you_as_private_user: "%{invited_by} heeft je uitgenodigd als een privé deelnemer van %{application}. Accepteren kan via onderstaande link." someone_invited_you: Iemand heeft u uitgenodigd voor %{application}. U kunt het accepteren via de onderstaande link. someone_invited_you_as_admin: Iemand heeft u uitgenodigd als administrator van %{application}, u kunt het accepteren via onderstaande link. - someone_invited_you_as_private_user: Iemand heeft je uitgenodigd als privé-deelnemer van %{application}, je kunt het accepteren via de onderstaande link. subject: Instructies voor de uitnodiging password_change: greeting: Hallo %{recipient}! diff --git a/decidim-core/config/locales/no.yml b/decidim-core/config/locales/no.yml index 81d37093d11a1..feec9af15d660 100644 --- a/decidim-core/config/locales/no.yml +++ b/decidim-core/config/locales/no.yml @@ -127,10 +127,6 @@ update: "%{user_name} oppdaterte %{resource_name} OAuth applikasjonen" organization: update: "%{user_name} oppdaterte organisasjonens innstillinger" - participatory_space_private_user: - create: "%{user_name} inviterte %{resource_name} til å bli en privat deltaker" - create_via_csv: "%{user_name} inviterte %{resource_name} via CSV til å være privat deltaker" - delete: "%{user_name} fjernet deltakeren %{resource_name} som en privat deltaker" scope: create: "%{user_name} laget temaet %{resource_name}" create_with_parent: "%{user_name} lagde %{resource_name} temaet inni %{parent_scope} temaet" @@ -910,10 +906,8 @@ decline: Avvis invitasjon hello: Hei %{email}, invited_you_as_admin: "%{invited_by} har invitert deg som en admin av %{application}. Du kan godta via lenken nedenfor." - invited_you_as_private_user: "%{invited_by} har invitert deg som en privat deltaker av %{application}. Du kan godta via lenken nedenfor." someone_invited_you: Noen har invitert deg til %{application}. Du kan godta det via lenken nedenfor. someone_invited_you_as_admin: Noen har invitert deg som en administrator av %{application}, du kan godta det via lenken nedenfor. - someone_invited_you_as_private_user: Noen har invitert deg som privat deltaker av %{application}, du kan godta det via lenken nedenfor. subject: Invitasjonsinstruksjoner password_change: greeting: Hei %{recipient}! diff --git a/decidim-core/config/locales/pl.yml b/decidim-core/config/locales/pl.yml index db4975782628f..bf6243e2763c1 100644 --- a/decidim-core/config/locales/pl.yml +++ b/decidim-core/config/locales/pl.yml @@ -219,10 +219,6 @@ pl: organization: update: "%{user_name} zaktualizował(a) ustawienia organizacji" update_external_domain: "%{user_name} zaktualizował zewnętrzne domeny organizacji" - participatory_space_private_user: - create: "%{user_name} zaprosił(a) %{resource_name} jako użytkownika prywatnego" - create_via_csv: "%{user_name} zaprosił(a) %{resource_name} jako prywatnego użytkownika poprzez CSV" - delete: "%{user_name} usunął/usunęła użytkownika %{resource_name} jako użytkownika prywatnego" scope: create: "%{user_name} utworzył(a) zakres %{resource_name}" create_with_parent: "%{user_name} utworzył(a) zakres podrzędny %{resource_name} w zakresie %{parent_scope}" @@ -1333,17 +1329,13 @@ pl: Jeżeli nie chcesz zaakceptować zaproszenia, zignoruj ten e-mail.
Twoje konto nie zostanie utworzone, dopóki nie skorzystasz z linku powyżej i nie ustawisz pseudonimu i hasła. invited_you_as_admin: "%{invited_by} zaprasza Cię jako administratora %{application}. Możesz zaakceptować zaproszenie za pomocą poniższego linku." - invited_you_as_private_user: "%{invited_by} zaprasza Cię jako prywatnego użytkownika %{application}. Możesz zaakceptować to zaproszenie za pomocą poniższego linku." someone_invited_you: Ktoś zaprosił Cię do %{application}. Możesz zaakceptować zaproszenie za pomocą poniższego linku. someone_invited_you_as_admin: Ktoś zaprosił Cię jako administratora %{application}, możesz zaakceptować zaproszenie za pomocą poniższego linku. - someone_invited_you_as_private_user: Ktoś zaprosił Cię jako prywatnego użytkownika %{application}, możesz zaakceptować zaproszenie za pomocą poniższego linku. subject: Instrukcje dotyczące zaproszenia invite_admin: subject: Zaproszono Cię do zarządzania %{organization} invite_collaborator: subject: Zaproszono Cię do współpracy przy %{organization} - invite_private_user: - subject: Zostałeś zaproszony do uczestnictwa w prywatnym procesie w %{organization} organization_admin_invitation_instructions: subject: Zaproszono Cię do zarządzania %{organization} password_change: diff --git a/decidim-core/config/locales/pt-BR.yml b/decidim-core/config/locales/pt-BR.yml index 82ea669430d5c..6baa9fb97d622 100644 --- a/decidim-core/config/locales/pt-BR.yml +++ b/decidim-core/config/locales/pt-BR.yml @@ -132,6 +132,9 @@ pt-BR: x_seconds: one: 1 segundo other: "%{count} segundos" + widget: + picker: + time_button: Abrir seletor para %{label} decidim: accessibility: external_link: Link externo @@ -234,10 +237,6 @@ pt-BR: organization: update: "%{user_name} atualizou as configurações da organização" update_external_domain: "%{user_name} atualizou os domínios externos da organização" - participatory_space_private_user: - create: "%{user_name} Convidado %{resource_name} ser um usuário privado" - create_via_csv: "%{user_name} convidou %{resource_name} via CSV para ser um usuário privado" - delete: "%{user_name} removeu o usuário %{resource_name} como usuário particular" scope: create: "%{user_name} criou o âmbito %{resource_name}" create_with_parent: "%{user_name} criou o âmbito %{resource_name} dentro do âmbito %{parent_scope}" @@ -1282,10 +1281,8 @@ pt-BR: decline: Negar convite hello: Olá %{email}, invited_you_as_admin: "%{invited_by} convidou você como administrador de %{application}. Você pode aceitá-lo através do link abaixo." - invited_you_as_private_user: "%{invited_by} convidou você como usuário particular de %{application}. Você pode aceitá-lo através do link abaixo." someone_invited_you: Alguém convidou você para %{application}. Você pode aceitá-lo através do link abaixo. someone_invited_you_as_admin: Alguém convidou você como administrador de %{application}, você pode aceitá-lo através do link abaixo. - someone_invited_you_as_private_user: Alguém convidou você como usuário privado de %{application}, você pode aceitá-lo através do link abaixo. subject: Instruções de convite invite_admin: subject: Você foi convidado a gerenciar %{organization} diff --git a/decidim-core/config/locales/pt.yml b/decidim-core/config/locales/pt.yml index 51156136d98b7..4280aa05fffa6 100644 --- a/decidim-core/config/locales/pt.yml +++ b/decidim-core/config/locales/pt.yml @@ -118,10 +118,6 @@ pt: update: "%{user_name} atualizou a aplicação OAuth %{resource_name}" organization: update: "%{user_name} atualizou as configurações da organização" - participatory_space_private_user: - create: "%{user_name} convidou %{resource_name} para ser um participante privado" - create_via_csv: "%{user_name} convidou %{resource_name} via CSV para ser participante privado" - delete: "%{user_name} removeu o participante %{resource_name} como participante privado" scope: create: "%{user_name} criou o âmbito %{resource_name}" create_with_parent: "%{user_name} criou o âmbito %{resource_name} dentro do âmbito %{parent_scope}" @@ -890,10 +886,8 @@ pt: decline: Recusar convite hello: Olá %{email}, invited_you_as_admin: "%{invited_by} convidou-(a) como administrador(a) de %{application}. Pode aceitar através da hiperligação abaixo." - invited_you_as_private_user: "%{invited_by} convidou-o(a) como participante privado de %{application}. Pode aceitar através da hiperligação abaixo." someone_invited_you: Alguém convidou-o(a) para %{application}. Pode aceitar através da hiperligação abaixo. someone_invited_you_as_admin: Alguém convidou-o(a) como administrador(a) de %{application}, pode aceitar através da hiperligação abaixo. - someone_invited_you_as_private_user: Alguém convidou-o(a) como participante privado de %{application}, pode aceitar através da hiperligação abaixo. subject: Instruções de convite password_change: greeting: Olá %{recipient}! diff --git a/decidim-core/config/locales/ro-RO.yml b/decidim-core/config/locales/ro-RO.yml index e0e5df16640ee..3086fd055fbfe 100644 --- a/decidim-core/config/locales/ro-RO.yml +++ b/decidim-core/config/locales/ro-RO.yml @@ -284,10 +284,6 @@ ro: organization: update: "%{user_name} a actualizat setările organizației" update_external_domain: "%{user_name} a actualizat domeniile externe ale organizației" - participatory_space_private_user: - create: "%{user_name} a invitat pe %{resource_name} să fie un participant privat" - create_via_csv: "%{user_name} a invitat pe %{resource_name} prin upload de CSV să fie un participant privat" - delete: "%{user_name} l-a eliminat pe %{resource_name} din lista de participanți privați" scope: create: "%{user_name} a creat domeniul de interes %{resource_name}" create_with_parent: "%{user_name} a creat subdomeniul de interes %{resource_name} în cadrul domeniului de interes %{parent_scope}" @@ -733,13 +729,6 @@ ro: user: Utilizatorul căruia îi aparține această identitate messaging_conversations: created_at: Data la care a fost creată această conversație - participatory_space_private_users: - created_at: Data și ora la care a fost creat acest utilizator privat - id: Identificatorul unic al acestui utilizator privat - privatable_to: Spațiul căruia îi aparține acest utilizator privat - published: Daca acest utilizator privat este vizibil sau nu - role: Rolul pe care îl are acest utilizator privat - updated_at: Data și ora la care acest utilizator privat a fost actualizat ultima dată reports: created_at: Data și ora la care acest raport a fost creat details: Detaliile acestui raport @@ -1447,10 +1436,8 @@ ro: decline: Refuzați invitația hello: Bună ziua %{email}, invited_you_as_admin: "%{invited_by} v-a invitat ca administrator al %{application}. Îl puteți accepta prin link-ul de mai jos." - invited_you_as_private_user: "%{invited_by} v-a invitat ca participant privat la %{application}. Puteți accepta prin linkul de mai jos." someone_invited_you: Cineva v-a invitat la %{application}. Puteți accepta prin linkul de mai jos. someone_invited_you_as_admin: Cineva v-a invitat să administrați %{application}. Puteți accepta prin linkul de mai jos. - someone_invited_you_as_private_user: Cineva v-a invitat ca participant privat la %{application}, puteți accepta prin intermediul linkului de mai jos. subject: Instrucțiuni pentru invitație invite_admin: subject: Ați fost invitat să gestionați %{organization} diff --git a/decidim-core/config/locales/ru.yml b/decidim-core/config/locales/ru.yml index 30020e782f774..5e7e884658de9 100644 --- a/decidim-core/config/locales/ru.yml +++ b/decidim-core/config/locales/ru.yml @@ -95,9 +95,6 @@ ru: update: "%{user_name} обновил приложение OAuth %{resource_name}" organization: update: "%{user_name} обновил настройки организации" - participatory_space_private_user: - create: "%{user_name} пригласил %{resource_name} стать частным участником" - delete: "%{user_name} удалил участника %{resource_name} как приватного участника" scope: create: "%{user_name} добавил охват %{resource_name}" create_with_parent: "%{user_name} создал охват %{resource_name} внутри охвата %{parent_scope}" @@ -534,10 +531,8 @@ ru: decline: Отклонить приглашение hello: Здравствуйте, %{email}! invited_you_as_admin: "%{invited_by} пригласил вас в качестве администратора %{application}. Вы можете принять это приглашение по ссылке ниже." - invited_you_as_private_user: "%{invited_by} пригласил вас в качестве частного участника %{application}. Вы можете принять это приглашение по ссылке ниже." someone_invited_you: Кто-то пригласил вас в %{application}. Вы можете принять это приглашение по ссылке ниже. someone_invited_you_as_admin: Кто-то пригласил вас в качестве администратора %{application}, вы можете принять это приглашение по ссылке ниже. - someone_invited_you_as_private_user: Кто-то пригласил вас в качестве частного участника %{application}, вы можете принять это приглашение по ссылке ниже. subject: Инструкция по приглашению password_change: greeting: Здравствуйте, %{recipient}! diff --git a/decidim-core/config/locales/sk.yml b/decidim-core/config/locales/sk.yml index 9ea527fd3d76a..d7b20bf6c9b1a 100644 --- a/decidim-core/config/locales/sk.yml +++ b/decidim-core/config/locales/sk.yml @@ -113,9 +113,6 @@ sk: update: "%{user_name} aktualizoval %{resource_name} aplikáciu OAuth" organization: update: "%{user_name} aktualizoval nastavenia organizácie" - participatory_space_private_user: - create: "%{user_name} pozval %{resource_name} aby sa stal súkromným používateľom" - delete: "%{user_name} odstránil užívateľa %{resource_name} ako súkromného účastníka" scope: create: "%{user_name} vytvoril %{resource_name} rozsah" create_with_parent: "%{user_name} vytvoril%{resource_name} rozsah vnútri rozsahu%{parent_scope}" @@ -734,10 +731,8 @@ sk: decline: Odmietnuť pozvaním hello: Dobrý deň%{email}, invited_you_as_admin: "%{invited_by} vás pozval ako administrátora na %{application}. Môžete pozvanie prijať prostredníctvom nižšie uvedeného odkazu." - invited_you_as_private_user: "%{invited_by} vás pozval ako súkromného užívateľa na %{application}. Môžete pozvanie prijať prostredníctvom nižšie uvedeného odkazu." someone_invited_you: Niekto vás pozval na%{application}. Môžete pozvanie prijať prostredníctvom nižšie uvedeného odkazu. someone_invited_you_as_admin: Niekto Vás pozval ako administrátora%{application}, môžete ho prijať pomocou nižšie uvedeného odkazu. - someone_invited_you_as_private_user: Niekto vás pozval ako private_user%{application}, môžete ho prijať pomocou nižšie uvedeného odkazu. subject: Pokyny k pozvánkam password_change: greeting: Dobrý deň, %{recipient}! diff --git a/decidim-core/config/locales/sv.yml b/decidim-core/config/locales/sv.yml index 9ce3516a8d050..89ef0cb1fae45 100644 --- a/decidim-core/config/locales/sv.yml +++ b/decidim-core/config/locales/sv.yml @@ -266,10 +266,6 @@ sv: organization: update: "%{user_name} uppdaterade organisationsinställningarna" update_external_domain: "%{user_name} uppdaterade organisationens externa domäner" - participatory_space_private_user: - create: "%{user_name} uppmanade %{resource_name} att vara en privat deltagare" - create_via_csv: "%{user_name} bjöd in %{resource_name} till att vara en privat deltagare via CSV-fil" - delete: "%{user_name} tog deltagaren %{resource_name} som en privat deltagare" scope: create: "%{user_name} skapade indelningen %{resource_name}" create_with_parent: "%{user_name} skapade indelningen %{resource_name} inom %{parent_scope}" @@ -736,13 +732,6 @@ sv: provider: Leverantören av identiteten messaging_conversations: updated_at: Datum när denna konversation senast uppdaterades - participatory_space_private_users: - created_at: Datum och tid när den privata användaren skapades - id: Unikt id för den privata användaren - privatable_to: Till vilken plats den privata användaren tillhör - published: Om den privata användaren är publicerad eller inte - role: Den privata användarens roll - updated_at: Datum och tid när den privata användaren senast uppdaterades reports: created_at: Datum och tid för när rapporteringen skapades details: Mer information om rapporteringen @@ -1605,17 +1594,13 @@ sv: Om du inte vill godkänna inbjudan kan du ignorera detta e-postmeddelande.
Ditt konto kommer bara att skapas om du öppnar länken ovan och anger ditt användarnamn och lösenord. invited_you_as_admin: "%{invited_by} har bjudit in dig som administratör av %{application}. Du kan acceptera det via länken nedan." - invited_you_as_private_user: "%{invited_by} har bjudit in dig som privat deltagare till %{application}. Du kan godkänna detta via länken nedan." someone_invited_you: Någon har bjudit in dig till %{application}. Du kan acceptera det via länken nedan. someone_invited_you_as_admin: Någon har bjudit in dig som administratör av %{application}. Du kan acceptera det via länken nedan. - someone_invited_you_as_private_user: Någon har bjudit in dig som privatperson till %{application}, du kan godkänna detta via länken nedan. subject: Anvisningar om inbjudningar invite_admin: subject: Du har blivit inbjuden att hantera %{organization} invite_collaborator: subject: Du har blivit inbjuden att samarbeta på %{organization} - invite_private_user: - subject: Du har blivit inbjuden till en privat deltagarprocess på %{organization} organization_admin_invitation_instructions: subject: Du har blivit inbjuden att hantera %{organization} password_change: diff --git a/decidim-core/config/locales/tr-TR.yml b/decidim-core/config/locales/tr-TR.yml index d4676d70fec78..60045d6e90ca9 100644 --- a/decidim-core/config/locales/tr-TR.yml +++ b/decidim-core/config/locales/tr-TR.yml @@ -115,10 +115,6 @@ tr: update: "%{user_name} , %{resource_name} OAuth uygulamasını güncelledi" organization: update: "%{user_name} kuruluş ayarlarını güncelledi" - participatory_space_private_user: - create: "%{user_name}, %{resource_name} adlı kullanıcıyı özel katılımcı olması için davet etti" - create_via_csv: "%{user_name}, %{resource_name} kaynağını CSV aracılığıyla özel bir katılımcı olması için davet etti" - delete: "%{user_name}, katılımcıyı %{resource_name} özel katılımcı özelliğini kaldırdı" scope: create: "%{user_name}, %{resource_name} kapsamı oluşturdu" create_with_parent: "%{user_name}, %{parent_scope} içindeki %{resource_name} kapsamı oluşturdu" @@ -891,10 +887,8 @@ tr: decline: Davet reddet hello: Merhaba %{email} invited_you_as_admin: "%{invited_by} sizi %{application}yönetici olarak davet etti. Aşağıdaki bağlantıdan kabul edebilirsiniz." - invited_you_as_private_user: "%{invited_by} özel bir kullanıcı olarak davet etti %{application}. Aşağıdaki bağlantıdan kabul edebilirsiniz." someone_invited_you: Birisi sizi %{application}davet etti. Aşağıdaki bağlantıdan kabul edebilirsiniz. someone_invited_you_as_admin: Birisi sizi %{application}yönetici olarak davet etti, aşağıdaki bağlantıdan kabul edebilirsiniz. - someone_invited_you_as_private_user: Birisi sizi %{application}private_user olarak davet etti, aşağıdaki bağlantıdan kabul edebilirsiniz. subject: Davet talimatları password_change: greeting: Merhaba %{recipient}! diff --git a/decidim-core/config/locales/uk.yml b/decidim-core/config/locales/uk.yml index baf6902f484a5..9ef0719b56cef 100644 --- a/decidim-core/config/locales/uk.yml +++ b/decidim-core/config/locales/uk.yml @@ -423,10 +423,8 @@ uk: decline: Відхилити запрошення hello: Доброго дня, %{email}%! invited_you_as_admin: "%{invited_by} запросив вас як адміністратора %{application}. Ви можете прийняти це запрошення за посиланням нижче." - invited_you_as_private_user: "%{invited_by} запросив вас як приватного учасника %{application}. Ви можете прийняти це запрошення за посиланням нижче." someone_invited_you: Хтось запросив вас до %{application}. Ви можете прийняти це запрошення за посиланням нижче. someone_invited_you_as_admin: Хтось запросив вас як адміністратора %{application}, ви можете прийняти це запрошення за посиланням нижче. - someone_invited_you_as_private_user: Хтось запросив вас як приватного учасника %{application}, ви можете прийняти це запрошення за посиланням нижче. password_change: greeting: Доброго дня, %{recipient}! subject: Пароль змінено diff --git a/decidim-core/config/locales/zh-CN.yml b/decidim-core/config/locales/zh-CN.yml index d2718f3c68974..fea07ed23454f 100644 --- a/decidim-core/config/locales/zh-CN.yml +++ b/decidim-core/config/locales/zh-CN.yml @@ -108,9 +108,6 @@ zh-CN: update: "%{user_name} 更新了 %{resource_name} OAuth 应用程序" organization: update: "%{user_name} 更新了组织设置" - participatory_space_private_user: - create: "%{user_name} 邀請了 %{resource_name} 作为私人参与者" - delete: "%{user_name} 移除了参与者 %{resource_name} 作为私人参与者" scope: create: "%{user_name} 创建了 %{resource_name} 范围" create_with_parent: "%{user_name} 在 %{parent_scope} 范围内创建了 %{resource_name} 范围" @@ -788,10 +785,8 @@ zh-CN: decline: 拒绝邀请 hello: 您好 %{email}, invited_you_as_admin: "%{invited_by} 邀请您担任了 %{application}的管理员。您可以通过下面的链接接受它。" - invited_you_as_private_user: "%{invited_by} 邀请您作为私有参与者 %{application}。您可以通过下面的链接接受它。" someone_invited_you: 有人邀请您访问 %{application}。您可以通过下面的链接接受它。 someone_invited_you_as_admin: 有人邀请您作为 %{application}的管理员,您可以通过下面的链接接受它。 - someone_invited_you_as_private_user: 有人邀请您作为私有参与者 %{application},您可以通过下面的链接接受它。 subject: 邀请说明 password_change: greeting: 你好 %{recipient}! diff --git a/decidim-core/config/locales/zh-TW.yml b/decidim-core/config/locales/zh-TW.yml index 4fd66b144c3eb..bc0d1a700f6b2 100644 --- a/decidim-core/config/locales/zh-TW.yml +++ b/decidim-core/config/locales/zh-TW.yml @@ -195,10 +195,6 @@ zh-TW: organization: update: "%{user_name} 更新了組織設定" update_external_domain: "%{user_name} 更新了組織外部域名" - participatory_space_private_user: - create: "%{user_name} 邀請了 %{resource_name} 成為私人參與者" - create_via_csv: "%{user_name} 透過 CSV 邀請了 %{resource_name} 成為私人參與者" - delete: "%{user_name} 將參與者 %{resource_name} 從私人參與者中移除" scope: create: "%{user_name} 創建了 %{resource_name} 範圍" create_with_parent: "%{user_name} 在 %{parent_scope} 範圍內創建了 %{resource_name} 範圍" @@ -1185,17 +1181,13 @@ zh-TW: ignore: |- 如果您不想接受邀請,請忽略此電子郵件。
在您使用上述連結設置您的暱稱和密碼之前,您的帳戶不會被創建。 invited_you_as_admin: "%{invited_by} 邀請您成為 %{application} 的管理員。您可以通過下面的連結接受邀請。" - invited_you_as_private_user: "%{invited_by} 邀請您成為 %{application} 的私密參與者。您可以通過下面的連結接受邀請。" someone_invited_you: 有人邀請您加入 %{application}。您可以通過下面的連結接受邀請。 someone_invited_you_as_admin: 有人邀請您成為 %{application} 的管理員,您可以通過下面的連結接受邀請。 - someone_invited_you_as_private_user: 有人邀請您成為 %{application} 的私人參與者,您可以通過下面的連結接受邀請。 subject: 邀請說明 invite_admin: subject: 你已經被邀請成為 %{organization} 的管理者 invite_collaborator: subject: 你已獲邀成為 %{organization} 的協作者 - invite_private_user: - subject: 您已被邀請參加 %{organization} 的私密參與過程。 organization_admin_invitation_instructions: subject: 你已經被邀請成為 %{organization} 的管理者 password_change: diff --git a/decidim-dev/config/locales/eu.yml b/decidim-dev/config/locales/eu.yml index cdf97a105953f..f13064a851e78 100644 --- a/decidim-dev/config/locales/eu.yml +++ b/decidim-dev/config/locales/eu.yml @@ -26,7 +26,7 @@ eu: guided_rich_readonly_html: HTMLlaguntzatestua input desgaiturako readonly_attribute: Soilik irakurtzeko atributua test: Proba bat - test_add: Erantsi testa + test_add: Testa erantsi test_choices: a: A aukera b: B aukera diff --git a/decidim-elections/config/locales/ca-IT.yml b/decidim-elections/config/locales/ca-IT.yml index 1ac5d68f61062..cf88a2147221b 100644 --- a/decidim-elections/config/locales/ca-IT.yml +++ b/decidim-elections/config/locales/ca-IT.yml @@ -81,6 +81,7 @@ ca-IT: questions_table: answer: Respostes percentage: Percentatge + total: Total votes: Vots votes_count: one: 1 vot @@ -254,6 +255,7 @@ ca-IT: active_voting_until: 'Votació activa fins: %{end_date}' check_census_button: Comprova si pots votar check_census_explanation: Aquesta elecció encara no ha començat, però pots comprovar si constes al cens. + edit_vote_button: Edita el vot vote_button: Votar voted: Ja has votat. Pots tornar a votar, això esborrarà el teu vot anterior i, només s'explicarà la teva última votació. votes_count: @@ -266,6 +268,10 @@ ca-IT: per_question: Els resultats estan disponibles per pregunta. Pots veure els resultats de cada pregunta una vegada que s'habiliti la votació i es publiquin els resultats. real_time: Els resultats estan disponibles a temps real. Podeu veure els resultats mentre la votació està en curs. title: Resultats + total: 'TOTAL:' + total_votes: + one: 1 vot + other: "%{count} vots" models: election: fields: @@ -314,6 +320,7 @@ ca-IT: next: Següent receipt: description: Pots tornar a votar en qualsevol moment mentre el període de votació estigui obert. El teu vot anterior serà substituït pel nou. + edit_vote: Edita el teu vot exit_button: Sortir de la cabina de votació title: El teu vot s'ha emès correctament metadata: diff --git a/decidim-elections/config/locales/ca.yml b/decidim-elections/config/locales/ca.yml index 32f5cc1f1f271..1fd3d4edf1777 100644 --- a/decidim-elections/config/locales/ca.yml +++ b/decidim-elections/config/locales/ca.yml @@ -81,6 +81,7 @@ ca: questions_table: answer: Respostes percentage: Percentatge + total: Total votes: Vots votes_count: one: 1 vot @@ -254,6 +255,7 @@ ca: active_voting_until: 'Votació activa fins: %{end_date}' check_census_button: Comprova si pots votar check_census_explanation: Aquesta elecció encara no ha començat, però pots comprovar si constes al cens. + edit_vote_button: Edita el vot vote_button: Votar voted: Ja has votat. Pots tornar a votar, això esborrarà el teu vot anterior i, només s'explicarà la teva última votació. votes_count: @@ -266,6 +268,10 @@ ca: per_question: Els resultats estan disponibles per pregunta. Pots veure els resultats de cada pregunta una vegada que s'habiliti la votació i es publiquin els resultats. real_time: Els resultats estan disponibles a temps real. Podeu veure els resultats mentre la votació està en curs. title: Resultats + total: 'TOTAL:' + total_votes: + one: 1 vot + other: "%{count} vots" models: election: fields: @@ -314,6 +320,7 @@ ca: next: Següent receipt: description: Pots tornar a votar en qualsevol moment mentre el període de votació estigui obert. El teu vot anterior serà substituït pel nou. + edit_vote: Edita el teu vot exit_button: Sortir de la cabina de votació title: El teu vot s'ha emès correctament metadata: diff --git a/decidim-elections/config/locales/es-MX.yml b/decidim-elections/config/locales/es-MX.yml index d7ad2f6ad4b59..53092aa54b98a 100644 --- a/decidim-elections/config/locales/es-MX.yml +++ b/decidim-elections/config/locales/es-MX.yml @@ -81,6 +81,7 @@ es-MX: questions_table: answer: Respuestas percentage: Porcentaje + total: Total votes: Votos votes_count: one: 1 voto @@ -254,6 +255,7 @@ es-MX: active_voting_until: 'Votación activa hasta: %{end_date}' check_census_button: Comprobar si puedo votar check_census_explanation: Esta elección aún no ha comenzado, pero puede comprobar si constas en el censo. + edit_vote_button: Editar el voto vote_button: Votar voted: Ya has votado. Puedes votar de nuevo, esto borrará tu voto anterior y, solo se contará tu última votación. votes_count: @@ -266,6 +268,10 @@ es-MX: per_question: Los resultados están disponibles por pregunta. Puedes ver los resultados de cada pregunta una vez que se habilite la votación y se publiquen los resultados. real_time: Los resultados están disponibles en tiempo real. Puedes ver los resultados mientras la votación está en curso. title: Resultados + total: 'TOTAL:' + total_votes: + one: 1 voto + other: "%{count} votos" models: election: fields: @@ -314,6 +320,7 @@ es-MX: next: Siguiente receipt: description: Puedes votar de nuevo en cualquier momento mientras el período de votación esté abierto. Tu voto anterior será sustituido por el nuevo. + edit_vote: Edita tu voto exit_button: Salir de la cabina de votación title: Tu voto se ha emitido correctamente metadata: diff --git a/decidim-elections/config/locales/es-PY.yml b/decidim-elections/config/locales/es-PY.yml index 6b025b1a4a191..1754505bc2c36 100644 --- a/decidim-elections/config/locales/es-PY.yml +++ b/decidim-elections/config/locales/es-PY.yml @@ -81,6 +81,7 @@ es-PY: questions_table: answer: Respuestas percentage: Porcentaje + total: Total votes: Votos votes_count: one: 1 voto @@ -254,6 +255,7 @@ es-PY: active_voting_until: 'Votación activa hasta: %{end_date}' check_census_button: Comprobar si puedo votar check_census_explanation: Esta elección aún no ha comenzado, pero puede comprobar si constas en el censo. + edit_vote_button: Editar el voto vote_button: Votar voted: Ya has votado. Puedes votar de nuevo, esto borrará tu voto anterior y, solo se contará tu última votación. votes_count: @@ -266,6 +268,10 @@ es-PY: per_question: Los resultados están disponibles por pregunta. Puedes ver los resultados de cada pregunta una vez que se habilite la votación y se publiquen los resultados. real_time: Los resultados están disponibles en tiempo real. Puedes ver los resultados mientras la votación está en curso. title: Resultados + total: 'TOTAL:' + total_votes: + one: 1 voto + other: "%{count} votos" models: election: fields: @@ -314,6 +320,7 @@ es-PY: next: Siguiente receipt: description: Puedes votar de nuevo en cualquier momento mientras el período de votación esté abierto. Tu voto anterior será sustituido por el nuevo. + edit_vote: Edita tu voto exit_button: Salir de la cabina de votación title: Tu voto se ha emitido correctamente metadata: diff --git a/decidim-elections/config/locales/es.yml b/decidim-elections/config/locales/es.yml index 6cfcba3b3f235..70bfec1d1c297 100644 --- a/decidim-elections/config/locales/es.yml +++ b/decidim-elections/config/locales/es.yml @@ -81,6 +81,7 @@ es: questions_table: answer: Respuestas percentage: Porcentaje + total: Total votes: Votos votes_count: one: 1 voto @@ -254,6 +255,7 @@ es: active_voting_until: 'Votación activa hasta: %{end_date}' check_census_button: Comprobar si puedo votar check_census_explanation: Esta elección aún no ha comenzado, pero puede comprobar si constas en el censo. + edit_vote_button: Editar el voto vote_button: Votar voted: Ya has votado. Puedes votar de nuevo, esto borrará tu voto anterior y, solo se contará tu última votación. votes_count: @@ -266,6 +268,10 @@ es: per_question: Los resultados están disponibles por pregunta. Puedes ver los resultados de cada pregunta una vez que se habilite la votación y se publiquen los resultados. real_time: Los resultados están disponibles en tiempo real. Puedes ver los resultados mientras la votación está en curso. title: Resultados + total: 'TOTAL:' + total_votes: + one: 1 voto + other: "%{count} votos" models: election: fields: @@ -314,6 +320,7 @@ es: next: Siguiente receipt: description: Puedes votar de nuevo en cualquier momento mientras el período de votación esté abierto. Tu voto anterior será sustituido por el nuevo. + edit_vote: Edita tu voto exit_button: Salir de la cabina de votación title: Tu voto se ha emitido correctamente metadata: diff --git a/decidim-elections/config/locales/eu.yml b/decidim-elections/config/locales/eu.yml index f07c6557a04b6..a14fde09a4b99 100644 --- a/decidim-elections/config/locales/eu.yml +++ b/decidim-elections/config/locales/eu.yml @@ -81,6 +81,7 @@ eu: questions_table: answer: Erantzunak percentage: Ehunekoa + total: Guztira votes: Babesak votes_count: one: Boto 1 @@ -256,6 +257,7 @@ eu: active_voting_until: 'Bozketa aktibo %{end_date} arte' check_census_button: Begiratu ea botoa eman dezakedan check_census_explanation: Hauteskunde hauek oraindik ez dira hasi, baina erroldan sartuta zauden egiazta dezakezu. + edit_vote_button: Editatu botoak vote_button: Eman babesa voted: Bozkatu duzu. Berriz bozkatu dezakezu, zure azken botoa baino ez da kontuan hartuko. votes_count: @@ -268,6 +270,10 @@ eu: per_question: Emaitzak kontsulta daitezke galderaren arabera. Galdera bakoitzaren emaitzak ikus ditzakezu botoa eman eta emaitzak argitaratu ondoren. real_time: Emaitzak denbora errealean daude eskuragarri. Emaitzak ikus ditzakezu bozketa egiten den bitartean. title: Emaitzak + total: 'GUZTIRA:' + total_votes: + one: Bozka 1 + other: "%{count} bozka" models: election: fields: @@ -315,7 +321,8 @@ eu: max_choices_exceeded: Ezin da aukeratu % {max} aukera baino gehiago. Mesedez, itzuli eta doitu zure aukeraketa. next: Hurrengoa receipt: - description: Edozein unetan eman dezakezu botoa berriro, bozkatzeko epea irekita dagoen bitartean. Zure boto berriak aurrekoa gainidatziko du. + description: Bozkatzeko epea zabalik dagoen bitartean edozein unetan eman daiteke berriro botoa. Zure aurreko botoa berriaz gainidatziko da. + edit_vote: Zure botoa editau exit_button: Irten bozkatzeko kabinatik title: Zure botoak zuzen eman dira metadata: diff --git a/decidim-elections/config/locales/fi-plain.yml b/decidim-elections/config/locales/fi-plain.yml index 5305224a6e71c..91b3daa5b41df 100644 --- a/decidim-elections/config/locales/fi-plain.yml +++ b/decidim-elections/config/locales/fi-plain.yml @@ -81,6 +81,7 @@ fi-pl: questions_table: answer: Vastaukset percentage: Prosentti + total: Yhteensä votes: Äänet votes_count: one: 1 ääni @@ -256,6 +257,7 @@ fi-pl: active_voting_until: 'Äänestysaika päättyy: %{end_date}' check_census_button: Tarkasta, voitko äänestää check_census_explanation: Nämä vaalit eivät ole vielä alkaneet, mutta voit tarkistaa, onko sinut merkitty näiden vaalien henkilötietorekisteriin. + edit_vote_button: Muokkaa ääntä vote_button: Äänestä voted: Olet jo äänestänyt. Voit äänestää uudestaan, ainoastaan viimeinen äänesi otetaan huomioon ääntenlaskennassa. votes_count: @@ -268,6 +270,10 @@ fi-pl: per_question: Tulokset ovat saatavilla kutakin kysymystä kohden. Näet kunkin kysymyksen tulokset, kun äänestys päättyy ja tulokset julkaistaan. real_time: Tulokset näkyvät reaaliaikaisesti. Näet tulokset äänestyksen aikana. title: Tulokset + total: 'YHTEENSÄ:' + total_votes: + one: 1 ääni + other: "%{count} ääntä" models: election: fields: @@ -316,6 +322,7 @@ fi-pl: next: Seuraava receipt: description: Voit äänestää uudestaan äänestyksen ollessa auki. Aikaisempi äänesi mitätöidään ja korvataan antamallasi uudella äänellä. + edit_vote: Muokkaa ääntäsi exit_button: Poistu äänestyskopista title: Äänesi kirjaaminen onnistui metadata: diff --git a/decidim-elections/config/locales/fi.yml b/decidim-elections/config/locales/fi.yml index 2f564b450bd6d..6ed9d74e90177 100644 --- a/decidim-elections/config/locales/fi.yml +++ b/decidim-elections/config/locales/fi.yml @@ -81,6 +81,7 @@ fi: questions_table: answer: Vastaukset percentage: Prosentti + total: Yhteensä votes: Äänet votes_count: one: 1 ääni @@ -256,6 +257,7 @@ fi: active_voting_until: 'Äänestysaika päättyy: %{end_date}' check_census_button: Tarkasta, voitko äänestää check_census_explanation: Nämä vaalit eivät ole vielä alkaneet, mutta voit tarkistaa, onko sinut merkitty näiden vaalien henkilötietorekisteriin. + edit_vote_button: Muokkaa ääntä vote_button: Äänestä voted: Olet jo äänestänyt. Voit äänestää uudestaan, ainoastaan viimeinen äänesi otetaan huomioon ääntenlaskennassa. votes_count: @@ -268,6 +270,10 @@ fi: per_question: Tulokset ovat saatavilla kutakin kysymystä kohden. Näet kunkin kysymyksen tulokset, kun äänestys päättyy ja tulokset julkaistaan. real_time: Tulokset näkyvät reaaliaikaisesti. Näet tulokset äänestyksen aikana. title: Tulokset + total: 'YHTEENSÄ:' + total_votes: + one: 1 ääni + other: "%{count} ääntä" models: election: fields: @@ -316,6 +322,7 @@ fi: next: Seuraava receipt: description: Voit äänestää uudestaan äänestyksen ollessa auki. Aikaisempi äänesi mitätöidään ja korvataan antamallasi uudella äänellä. + edit_vote: Muokkaa ääntäsi exit_button: Poistu äänestyskopista title: Äänesi kirjaaminen onnistui metadata: diff --git a/decidim-elections/config/locales/fr-CA.yml b/decidim-elections/config/locales/fr-CA.yml index 63b0dcb6996ed..8cf5920a8e187 100644 --- a/decidim-elections/config/locales/fr-CA.yml +++ b/decidim-elections/config/locales/fr-CA.yml @@ -66,6 +66,7 @@ fr-CA: title: Questions questions_table: answer: Réponses + total: Total votes: Votes votes_count: one: 1 vote @@ -211,6 +212,7 @@ fr-CA: active_voting_until: 'Vote actif jusqu''au : %{end_date}' check_census_button: Vérifier si je peux voter check_census_explanation: L'élection n'a pas encore commencé, mais vous pouvez vérifier si vous êtes inscrit dans le recensement. + edit_vote_button: Modifier le vote vote_button: Voter voted: Vous avez déjà voté. Vous pouvez voter à nouveau, seul votre dernier vote sera prise en compte. votes_count: @@ -223,6 +225,10 @@ fr-CA: per_question: Les résultats sont disponibles par question. Vous pouvez voir les résultats pour chaque question après que le vote soit activé et que les résultats soient publiés. real_time: Les résultats sont disponibles en temps réel. Vous pouvez voir les résultats en cours de vote. title: Résultats + total: 'TOTAL :' + total_votes: + one: 1 vote + other: "%{count} votes" models: election: fields: @@ -268,7 +274,8 @@ fr-CA: max_choices_exceeded: Vous ne pouvez pas sélectionner plus que %{max} options. Veuillez revenir en arrière et ajuster votre sélection. next: Suivant receipt: - description: Vous pouvez voter à nouveau à tout moment tant que la période de vote est ouverte. Votre vote précédent sera écrasé par le nouveau. + description: Vous pouvez voter à nouveau à tout moment pendant que la période de vote est ouverte. Votre vote précédent sera écrasé par le nouveau. + edit_vote: Modifier votre vote exit_button: Quitter l'isoloir title: Vos votes ont été exprimés avec succès metadata: diff --git a/decidim-elections/config/locales/fr.yml b/decidim-elections/config/locales/fr.yml index f213257812177..5ba33334c4983 100644 --- a/decidim-elections/config/locales/fr.yml +++ b/decidim-elections/config/locales/fr.yml @@ -66,6 +66,7 @@ fr: title: Questions questions_table: answer: Réponses + total: Total votes: Votes votes_count: one: 1 vote @@ -211,6 +212,7 @@ fr: active_voting_until: 'Vote actif jusqu''au : %{end_date}' check_census_button: Vérifier si je peux voter check_census_explanation: L'élection n'a pas encore commencé, mais vous pouvez vérifier si vous êtes inscrit dans le recensement. + edit_vote_button: Modifier le vote vote_button: Voter voted: Vous avez déjà voté. Vous pouvez voter à nouveau, seul votre dernier vote sera prise en compte. votes_count: @@ -223,6 +225,10 @@ fr: per_question: Les résultats sont disponibles par question. Vous pouvez voir les résultats pour chaque question après que le vote soit activé et que les résultats soient publiés. real_time: Les résultats sont disponibles en temps réel. Vous pouvez voir les résultats en cours de vote. title: Résultats + total: 'TOTAL :' + total_votes: + one: 1 vote + other: "%{count} votes" models: election: fields: @@ -268,7 +274,8 @@ fr: max_choices_exceeded: Vous ne pouvez pas sélectionner plus que %{max} options. Veuillez revenir en arrière et ajuster votre sélection. next: Suivant receipt: - description: Vous pouvez voter à nouveau à tout moment tant que la période de vote est ouverte. Votre vote précédent sera écrasé par le nouveau. + description: Vous pouvez voter à nouveau à tout moment pendant que la période de vote est ouverte. Votre vote précédent sera écrasé par le nouveau. + edit_vote: Modifier votre vote exit_button: Quitter l'isoloir title: Vos votes ont été exprimés avec succès metadata: diff --git a/decidim-elections/config/locales/ja.yml b/decidim-elections/config/locales/ja.yml index cb9fa21fa3754..69fa8c3ebafa6 100644 --- a/decidim-elections/config/locales/ja.yml +++ b/decidim-elections/config/locales/ja.yml @@ -80,6 +80,7 @@ ja: questions_table: answer: 回答 percentage: パーセンテージ + total: 合計 votes: 投票 votes_count: other: "%{count} 票" @@ -252,6 +253,7 @@ ja: active_voting_until: '有効な投票まで: %{end_date}' check_census_button: 投票できるかどうかチェック check_census_explanation: この選挙はまだ始まっていませんが、センサスの対象に含まれているかどうか確認できます。 + edit_vote_button: 投票を編集 vote_button: 投票 voted: すでに投票済みです。再度投票することは可能ですが、最後に投票した内容のみが集計対象となります。 votes_count: @@ -263,6 +265,9 @@ ja: per_question: 結果は質問ごとに確認できます。投票が有効化され、結果が公開されると、各質問の結果を確認できるようになります。 real_time: 結果はリアルタイムで確認できます。投票が行われている間も結果を閲覧可能です。 title: 結果 + total: '合計:' + total_votes: + other: "%{count}票" models: election: fields: @@ -311,6 +316,7 @@ ja: next: 次へ receipt: description: 投票期間中はいつでも再投票が可能です。以前の投票結果は新しい投票結果で上書きされます。 + edit_vote: 投票を編集 exit_button: 投票ブースを出る title: 投票は正常に行われました metadata: diff --git a/decidim-elections/config/locales/sv.yml b/decidim-elections/config/locales/sv.yml index 99a5702cb946f..b2f3c5c2eae72 100644 --- a/decidim-elections/config/locales/sv.yml +++ b/decidim-elections/config/locales/sv.yml @@ -62,7 +62,11 @@ sv: title: Frågor questions_table: answer: Svar + total: Totalt votes: Röster + votes_count: + one: 1 röst + other: "%{count} röster" results: publish_button: Publicera resultat title: Resultat @@ -191,6 +195,7 @@ sv: active_voting_until: 'Aktiv röstning till: %{end_date}' check_census_button: Kontrollera om jag kan rösta check_census_explanation: Detta val har ännu inte börjat, men du kan kontrollera om du är med i folkräkningen. + edit_vote_button: Redigera röst vote_button: Rösta voted: Du har redan röstat. Om du röstar igen kommer bara den senaste rösten att räknas. votes_count: @@ -203,6 +208,10 @@ sv: per_question: Resultat finns tillgängliga per fråga. Du kan se resultaten för varje fråga efter att röstning har aktiverats och resultaten är publicerade. real_time: Resultaten är tillgängliga i realtid. Du kan se resultaten medan omröstningen pågår. title: Resultat + total: 'TOTAL:' + total_votes: + one: 1 röst + other: "%{count} röster" models: election: fields: @@ -248,6 +257,7 @@ sv: next: Nästa receipt: description: Du kan rösta igen så länge omröstningsperioden pågår. En ny omröstning kommer att skriva över den gamla. + edit_vote: Redigera din röst exit_button: Lämna valbåset title: Tack, din röst är mottagen metadata: diff --git a/decidim-forms/config/locales/ar.yml b/decidim-forms/config/locales/ar.yml index 7042fd89b4195..96f09c3697ff6 100644 --- a/decidim-forms/config/locales/ar.yml +++ b/decidim-forms/config/locales/ar.yml @@ -44,9 +44,6 @@ ar: show: questionnaire_closed: title: شكل مغلق - questionnaire_for_private_users: - body: النموذج متاح فقط للمستخدمين من القطاع الخاص - title: شكل مغلق tos_agreement: من خلال المشاركة ، فإنك تقبل شروط الخدمة step_navigation: show: diff --git a/decidim-forms/config/locales/bg.yml b/decidim-forms/config/locales/bg.yml index e5dd6a57c5493..b23acd82bb15d 100644 --- a/decidim-forms/config/locales/bg.yml +++ b/decidim-forms/config/locales/bg.yml @@ -94,9 +94,6 @@ bg: of_total_steps: от %{total_steps} questionnaire_closed: title: Формулярът е затворен - questionnaire_for_private_users: - body: Формулярът е налице само за частни потребители - title: Формулярът е затворен questionnaire_js_disabled: body: Някои от функциите на формуляра ще бъдат деактивирани. За да подобрите практическата си работа, моля, активирайте JavaScript в браузъра си. title: JavaScript е деактивиран diff --git a/decidim-forms/config/locales/ca-IT.yml b/decidim-forms/config/locales/ca-IT.yml index 622234cd53531..a612f2352888e 100644 --- a/decidim-forms/config/locales/ca-IT.yml +++ b/decidim-forms/config/locales/ca-IT.yml @@ -188,8 +188,8 @@ ca-IT: questionnaire_closed: body: El formulari està tancat i no es pot respondre. title: S'ha tancat el formulari - questionnaire_for_private_users: - body: El formulari només està disponible per a participants privades + questionnaire_for_members: + body: El formulari només està disponible per a membres title: S'ha tancat el formulari questionnaire_js_disabled: body: Algunes de les característiques d'aquest formulari es deshabilitaran. Per millorar la teva experiència, si us plau, habilita JavaScript al teu navegador. diff --git a/decidim-forms/config/locales/ca.yml b/decidim-forms/config/locales/ca.yml index 4ddaea69b633f..75169a86570e4 100644 --- a/decidim-forms/config/locales/ca.yml +++ b/decidim-forms/config/locales/ca.yml @@ -188,8 +188,8 @@ ca: questionnaire_closed: body: El formulari està tancat i no es pot respondre. title: S'ha tancat el formulari - questionnaire_for_private_users: - body: El formulari només està disponible per a participants privades + questionnaire_for_members: + body: El formulari només està disponible per a membres title: S'ha tancat el formulari questionnaire_js_disabled: body: Algunes de les característiques d'aquest formulari es deshabilitaran. Per millorar la teva experiència, si us plau, habilita JavaScript al teu navegador. diff --git a/decidim-forms/config/locales/cs.yml b/decidim-forms/config/locales/cs.yml index e5a70845220d0..280aec19eea31 100644 --- a/decidim-forms/config/locales/cs.yml +++ b/decidim-forms/config/locales/cs.yml @@ -185,9 +185,6 @@ cs: questionnaire_closed: body: Formulář je uzavřen a nelze jej odpovědět. title: Dotazník byl uzavřen - questionnaire_for_private_users: - body: Dotazník je k dispozici pouze pro soukromé uživatele - title: Dotazník byl uzavřen questionnaire_js_disabled: body: Některé funkce tohoto formuláře budou zakázány. Pro zlepšení použití formuláře povolte JavaScript ve vašem prohlížeči. title: JavaScript je zakázán diff --git a/decidim-forms/config/locales/de.yml b/decidim-forms/config/locales/de.yml index 65ddc6f0bbdd2..e74e2e5c1d137 100644 --- a/decidim-forms/config/locales/de.yml +++ b/decidim-forms/config/locales/de.yml @@ -187,9 +187,6 @@ de: questionnaire_closed: body: Das Formular ist geschlossen und kann nicht beantwortet werden. title: Fragebogen geschlossen - questionnaire_for_private_users: - body: Der Fragebogen ist nur für Privatanwender verfügbar - title: Fragebogen geschlossen questionnaire_js_disabled: body: Manche Funktionen des Formulars werden deaktiviert sein. Aktivieren Sie JavaScript in Ihrem Browser für eine bessere Erfahrung. title: JavaScript ist deaktiviert diff --git a/decidim-forms/config/locales/el.yml b/decidim-forms/config/locales/el.yml index 10b084f6d4d75..a0f6e237cfac7 100644 --- a/decidim-forms/config/locales/el.yml +++ b/decidim-forms/config/locales/el.yml @@ -93,9 +93,6 @@ el: of_total_steps: από %{total_steps} questionnaire_closed: title: Η φόρμα έκλεισε - questionnaire_for_private_users: - body: Η φόρμα είναι διαθέσιμη μόνο για ιδιωτικούς χρήστες - title: Η φόρμα έκλεισε questionnaire_js_disabled: body: Ορισμένες από τις δυνατότητες αυτής της φόρμας θα απενεργοποιηθούν. Για να βελτιώσετε την εμπειρία σας, ενεργοποιήστε την JavaScript στο πρόγραμμα περιήγησης. title: Η JavaScript είναι απενεργοποιημένη diff --git a/decidim-forms/config/locales/es-MX.yml b/decidim-forms/config/locales/es-MX.yml index d59aa33ed3987..0ceccba3b9ecf 100644 --- a/decidim-forms/config/locales/es-MX.yml +++ b/decidim-forms/config/locales/es-MX.yml @@ -188,9 +188,9 @@ es-MX: questionnaire_closed: body: El formulario está cerrado y no puedes responder. title: Cuestionario cerrado - questionnaire_for_private_users: - body: El cuestionario está disponible solo para usuarios privados - title: Cuestionario cerrado + questionnaire_for_members: + body: El formulario está disponible solo para miembros + title: Formulario cerrado questionnaire_js_disabled: body: Algunas de las características de este formulario se desactivarán. Para mejorar tu experiencia, habilita JavaScript en tu navegador. title: Javascript está desactivado diff --git a/decidim-forms/config/locales/es-PY.yml b/decidim-forms/config/locales/es-PY.yml index e7e3ee0e50f2d..524048e44d972 100644 --- a/decidim-forms/config/locales/es-PY.yml +++ b/decidim-forms/config/locales/es-PY.yml @@ -188,9 +188,9 @@ es-PY: questionnaire_closed: body: El formulario está cerrado y no puedes responder. title: Cuestionario cerrado - questionnaire_for_private_users: - body: El cuestionario está disponible solo para usuarios privados - title: Cuestionario cerrado + questionnaire_for_members: + body: El formulario está disponible solo para miembros + title: Formulario cerrado questionnaire_js_disabled: body: Algunas de las características de este formulario se desactivarán. Para mejorar tu experiencia, habilita JavaScript en tu navegador. title: Javascript está desactivado diff --git a/decidim-forms/config/locales/es.yml b/decidim-forms/config/locales/es.yml index 2dfebad4ba786..f1bb3d6296025 100644 --- a/decidim-forms/config/locales/es.yml +++ b/decidim-forms/config/locales/es.yml @@ -188,8 +188,8 @@ es: questionnaire_closed: body: El formulario está cerrado y no puedes responder. title: Formulario cerrado - questionnaire_for_private_users: - body: El formulario está disponible solo para participantes privadas + questionnaire_for_members: + body: El formulario está disponible solo para miembros title: Formulario cerrado questionnaire_js_disabled: body: Algunas de las características de este formulario se desactivarán. Para mejorar tu experiencia, habilita JavaScript en tu navegador. diff --git a/decidim-forms/config/locales/eu.yml b/decidim-forms/config/locales/eu.yml index 3faf10583f609..0ebb559c04ecd 100644 --- a/decidim-forms/config/locales/eu.yml +++ b/decidim-forms/config/locales/eu.yml @@ -29,7 +29,7 @@ eu: help: responses: id: Erantzunaren identifikatzaile bakarra - question: Erantzun zen galdera + question: Erantzun den galdera questionnaire: Erantzun zen galdetegia response: Galderaren erantzuna user: Inkesta erantzun duen parte-hartzailea @@ -188,8 +188,8 @@ eu: questionnaire_closed: body: Galdetegia itxita dago eta ezin da erantzun. title: Galdeketa itxia - questionnaire_for_private_users: - body: Galdetegia parte-hartzaile pribatuentzat soilik dago erabilgarri + questionnaire_for_members: + body: Formularioa bazkideentzat soilik dago eskuragarri title: Galdeketa itxia questionnaire_js_disabled: body: Galdetegi honen ezaugarri batzuk desaktibatu egingo dira. Zure esperientzia hobetzeko, gaitu JavaScript zure nabigatzailean. diff --git a/decidim-forms/config/locales/fi-plain.yml b/decidim-forms/config/locales/fi-plain.yml index 39b2526c63201..aae7287598312 100644 --- a/decidim-forms/config/locales/fi-plain.yml +++ b/decidim-forms/config/locales/fi-plain.yml @@ -188,9 +188,9 @@ fi-pl: questionnaire_closed: body: Kyselylomake on suljettu, eikä siihen voi vastata. title: Kysely on suljettu - questionnaire_for_private_users: - body: Kyselylomake on saatavilla vain yksityisille käyttäjille - title: Kysely on suljettu + questionnaire_for_members: + body: Kyselylomake on nähtävillä vain jäsenille + title: Kyselylomake on suljettu questionnaire_js_disabled: body: Jotkin tämän lomakkeen ominaisuudet eivät ole käytössä. Parantaaksesi käyttökokemustasi, ota JavaScript käyttöön selaimestasi. title: JavaScript ei ole käytössä diff --git a/decidim-forms/config/locales/fi.yml b/decidim-forms/config/locales/fi.yml index 7ccceb939f2ff..a3c7802873318 100644 --- a/decidim-forms/config/locales/fi.yml +++ b/decidim-forms/config/locales/fi.yml @@ -188,8 +188,8 @@ fi: questionnaire_closed: body: Kyselylomake on suljettu, eikä siihen voi vastata. title: Kyselylomake on suljettu - questionnaire_for_private_users: - body: Kyselylomake on nähtävillä vain yksityisille käyttäjille + questionnaire_for_members: + body: Kyselylomake on nähtävillä vain jäsenille title: Kyselylomake on suljettu questionnaire_js_disabled: body: Jotkin tämän lomakkeen ominaisuudet eivät ole käytössä. Parantaaksesi käyttökokemustasi, ota JavaScript käyttöön selaimestasi. diff --git a/decidim-forms/config/locales/fr-CA.yml b/decidim-forms/config/locales/fr-CA.yml index 92c8e581e2cee..e5661ae1cb9fa 100644 --- a/decidim-forms/config/locales/fr-CA.yml +++ b/decidim-forms/config/locales/fr-CA.yml @@ -184,9 +184,9 @@ fr-CA: questionnaire_closed: body: Le questionnaire est fermé et vous ne pouvez pas y répondre. title: Questionnaire fermé - questionnaire_for_private_users: - body: Le questionnaire est disponible uniquement pour les utilisateurs privés - title: Questionnaire fermé + questionnaire_for_members: + body: Le formulaire est disponible uniquement pour les membres + title: Formulaire fermé questionnaire_js_disabled: body: Certaines des fonctionnalités de ce formulaire seront désactivées. Pour améliorer votre expérience, veuillez activer JavaScript dans votre navigateur. title: JavaScript est désactivé diff --git a/decidim-forms/config/locales/fr.yml b/decidim-forms/config/locales/fr.yml index 4a8bd3e64e367..9ee5458700d1e 100644 --- a/decidim-forms/config/locales/fr.yml +++ b/decidim-forms/config/locales/fr.yml @@ -184,9 +184,9 @@ fr: questionnaire_closed: body: Le questionnaire est fermé et vous ne pouvez pas y répondre. title: Questionnaire fermé - questionnaire_for_private_users: - body: Le questionnaire est disponible uniquement pour les utilisateurs privés - title: Questionnaire fermé + questionnaire_for_members: + body: Le formulaire est disponible uniquement pour les membres + title: Formulaire fermé questionnaire_js_disabled: body: Certaines des fonctionnalités de ce formulaire seront désactivées. Pour améliorer votre expérience, veuillez activer JavaScript dans votre navigateur. title: JavaScript est désactivé diff --git a/decidim-forms/config/locales/gl.yml b/decidim-forms/config/locales/gl.yml index e6a033ab862a0..f1b3d9381a59c 100644 --- a/decidim-forms/config/locales/gl.yml +++ b/decidim-forms/config/locales/gl.yml @@ -51,9 +51,6 @@ gl: show: questionnaire_closed: title: Cuestionario pechado - questionnaire_for_private_users: - body: O cuestionario só está dispoñible para usuarios privados - title: Cuestionario pechado tos_agreement: Ao participar aceptas as Condicións de servizo step_navigation: show: diff --git a/decidim-forms/config/locales/hu.yml b/decidim-forms/config/locales/hu.yml index f1dfa868b8571..f72203a7b3d3c 100644 --- a/decidim-forms/config/locales/hu.yml +++ b/decidim-forms/config/locales/hu.yml @@ -54,9 +54,6 @@ hu: show: questionnaire_closed: title: A kérdőív lezárult - questionnaire_for_private_users: - body: A kérdőív csak magán felhasználók számára áll rendelkezésre - title: A kérdőív lezárult tos_agreement: A részvétellel elfogadja az Általános Szerződési Feltételeket step_navigation: show: diff --git a/decidim-forms/config/locales/id-ID.yml b/decidim-forms/config/locales/id-ID.yml index c653067528910..6e17c2da315b3 100644 --- a/decidim-forms/config/locales/id-ID.yml +++ b/decidim-forms/config/locales/id-ID.yml @@ -39,9 +39,6 @@ id: show: questionnaire_closed: title: Formulir ditutup - questionnaire_for_private_users: - body: Formulir ini hanya tersedia untuk pengguna pribadi - title: Formulir ditutup tos_agreement: Dengan berpartisipasi Anda menerima Ketentuan Layanannya step_navigation: show: diff --git a/decidim-forms/config/locales/it.yml b/decidim-forms/config/locales/it.yml index 68022295dfd58..7cdcae34be0c6 100644 --- a/decidim-forms/config/locales/it.yml +++ b/decidim-forms/config/locales/it.yml @@ -79,9 +79,6 @@ it: of_total_steps: di %{total_steps} questionnaire_closed: title: Questionario chiuso - questionnaire_for_private_users: - body: Il questionario è disponibile solo per gli utenti privati - title: Questionario chiuso questionnaire_js_disabled: body: Alcune caratteristiche di questo modulo saranno disabilitate. Per migliorare la tua esperienza, abilita JavaScript nel tuo browser. title: JavaScript è disabilitato diff --git a/decidim-forms/config/locales/ja.yml b/decidim-forms/config/locales/ja.yml index 905932733be8a..9991231dab0cd 100644 --- a/decidim-forms/config/locales/ja.yml +++ b/decidim-forms/config/locales/ja.yml @@ -188,9 +188,6 @@ ja: questionnaire_closed: body: フォームが閉じられているため、応答できません。 title: フォームを閉じました - questionnaire_for_private_users: - body: このフォームはプライベートユーザーのみ利用できます - title: フォームを閉じました questionnaire_js_disabled: body: このフォームのいくつかの機能は無効になります。あなたのエクスペリエンスを向上させるために、ブラウザでJavaScriptを有効にしてください。 title: JavaScriptは無効です diff --git a/decidim-forms/config/locales/lb.yml b/decidim-forms/config/locales/lb.yml index caea150b2c2be..3c6351dd67b9c 100644 --- a/decidim-forms/config/locales/lb.yml +++ b/decidim-forms/config/locales/lb.yml @@ -79,9 +79,6 @@ lb: of_total_steps: von %{total_steps} questionnaire_closed: title: Fragebogen geschlossen - questionnaire_for_private_users: - body: Der Fragebogen ist nur für Privatanwender verfügbar - title: Fragebogen geschlossen questionnaire_js_disabled: body: Manche Funktionen des Formulars werden deaktiviert sein. Aktivieren Sie JavaScript in Ihrem Browser für eine bessere Erfahrung. title: JavaScript ist deaktiviert diff --git a/decidim-forms/config/locales/lt.yml b/decidim-forms/config/locales/lt.yml index 0f394d3f3407a..6505b0049b35f 100644 --- a/decidim-forms/config/locales/lt.yml +++ b/decidim-forms/config/locales/lt.yml @@ -93,9 +93,6 @@ lt: of_total_steps: iš%{total_steps} questionnaire_closed: title: Forma uždaryta - questionnaire_for_private_users: - body: Forma pateikiama tik privatiems naudotojams - title: Forma uždaryta questionnaire_js_disabled: body: Kai kurie šios formos funkcionalumai bus neaktyvūs. Kad pagerintumėte naudojimo patirtį, naršyklėje įjunkite „JavaScript“. title: '„JavaScript“ išjungta' diff --git a/decidim-forms/config/locales/lv.yml b/decidim-forms/config/locales/lv.yml index c5c4e62abfc76..d06bcadf5a419 100644 --- a/decidim-forms/config/locales/lv.yml +++ b/decidim-forms/config/locales/lv.yml @@ -55,9 +55,6 @@ lv: of_total_steps: no %{total_steps} questionnaire_closed: title: Veidlapa slēgta - questionnaire_for_private_users: - body: Veidlapa ir pieejama tikai privātiem lietotājiem - title: Veidlapa slēgta tos_agreement: Piedaloties jūs piekrītat pakalpojumu sniegšanas noteikumiem. step_navigation: show: diff --git a/decidim-forms/config/locales/nl.yml b/decidim-forms/config/locales/nl.yml index d01d3e1fe62d8..64a8798f95f23 100644 --- a/decidim-forms/config/locales/nl.yml +++ b/decidim-forms/config/locales/nl.yml @@ -84,9 +84,6 @@ nl: of_total_steps: van %{total_steps} questionnaire_closed: title: Vragenlijst gesloten - questionnaire_for_private_users: - body: De vragenlijst is alleen beschikbaar voor privé-gebruikers - title: Vragenlijst gesloten questionnaire_js_disabled: body: Sommige functies van dit formulier worden uitgeschakeld. Schakel JavaScript in in uw browser om uw ervaring te verbeteren. title: JavaScript is uitgeschakeld diff --git a/decidim-forms/config/locales/no.yml b/decidim-forms/config/locales/no.yml index 72c3f1c7a7a36..1c59334fa3ef4 100644 --- a/decidim-forms/config/locales/no.yml +++ b/decidim-forms/config/locales/no.yml @@ -87,9 +87,6 @@ of_total_steps: av %{total_steps} questionnaire_closed: title: Skjema lukket - questionnaire_for_private_users: - body: Skjemaet er bare tilgjengelig for private brukere - title: Skjema lukket questionnaire_js_disabled: body: Noen av funksjonene i dette skjemaet vil bli deaktivert. For å forbedre opplevelsen, må du aktivere JavaScript i nettleseren. title: JavaScript deaktivert diff --git a/decidim-forms/config/locales/pl.yml b/decidim-forms/config/locales/pl.yml index 70676065346b6..a20382fa64e02 100644 --- a/decidim-forms/config/locales/pl.yml +++ b/decidim-forms/config/locales/pl.yml @@ -94,9 +94,6 @@ pl: of_total_steps: z %{total_steps} questionnaire_closed: title: Formularz został zamknięty - questionnaire_for_private_users: - body: Formularz jest dostępny tylko dla użytkowników prywatnych - title: Formularz został zamknięty questionnaire_js_disabled: body: Niektóre funkcje tego formularza zostaną wyłączone. Aby strona dostarczała lepszych doświadczeń, włącz obsługę JavaScript w przeglądarce. title: JavaScript jest wyłączony diff --git a/decidim-forms/config/locales/pt-BR.yml b/decidim-forms/config/locales/pt-BR.yml index bb87045f561d8..746b9e60a4f1b 100644 --- a/decidim-forms/config/locales/pt-BR.yml +++ b/decidim-forms/config/locales/pt-BR.yml @@ -93,9 +93,6 @@ pt-BR: of_total_steps: de %{total_steps} questionnaire_closed: title: Questionário fechado - questionnaire_for_private_users: - body: O questionário está disponível apenas para usuários particulares - title: Questionário fechado questionnaire_js_disabled: body: Algumas das funcionalidades deste formulário serão desativadas. Para melhorar sua experiência, por favor ative o JavaScript no seu navegador. title: JavaScript está desativado diff --git a/decidim-forms/config/locales/pt.yml b/decidim-forms/config/locales/pt.yml index 71e0ac8220d36..78b8f9b369bc7 100644 --- a/decidim-forms/config/locales/pt.yml +++ b/decidim-forms/config/locales/pt.yml @@ -79,9 +79,6 @@ pt: of_total_steps: de %{total_steps} questionnaire_closed: title: Questionário encerrado - questionnaire_for_private_users: - body: O questionário está disponível apenas para utilizadores privados - title: Questionário encerrado questionnaire_js_disabled: body: Algumas características deste questionário serão desativadas. Para melhorar a sua experiência, por favor ative o JavaScript no seu navegador. title: JavaScript está desativado diff --git a/decidim-forms/config/locales/ro-RO.yml b/decidim-forms/config/locales/ro-RO.yml index e623d8996bc7a..9c265fc637330 100644 --- a/decidim-forms/config/locales/ro-RO.yml +++ b/decidim-forms/config/locales/ro-RO.yml @@ -121,9 +121,6 @@ ro: of_total_steps: din %{total_steps} questionnaire_closed: title: Formular închis - questionnaire_for_private_users: - body: Chestionarul este disponibil doar pentru utilizatorii privați - title: Chestionar închis questionnaire_js_disabled: body: Unele dintre funcționalitățile acestui chestionar vor fi dezactivate. Pentru a îți îmbunătăți experiența, te rugăm să activezi JavaScript în browser-ul tău. title: JavaScript este dezactivat diff --git a/decidim-forms/config/locales/ru.yml b/decidim-forms/config/locales/ru.yml index ff7c24cf77ddc..00b362d9d4c3f 100644 --- a/decidim-forms/config/locales/ru.yml +++ b/decidim-forms/config/locales/ru.yml @@ -26,9 +26,6 @@ ru: show: questionnaire_closed: title: Форма закрыта - questionnaire_for_private_users: - body: Форма доступна только для частных пользователей - title: Форма закрыта step_navigation: show: submit: Отправить diff --git a/decidim-forms/config/locales/sk.yml b/decidim-forms/config/locales/sk.yml index 011f764cf7e4d..7acf78a16928f 100644 --- a/decidim-forms/config/locales/sk.yml +++ b/decidim-forms/config/locales/sk.yml @@ -39,9 +39,6 @@ sk: show: questionnaire_closed: title: Formulár je uzavretý. - questionnaire_for_private_users: - body: Formulár je otvorený len pre súkromných používateľov - title: Formulár je uzavretý tos_agreement: Účasťou súhlasíte s našimi Podmienkami použitia step_navigation: show: diff --git a/decidim-forms/config/locales/sv.yml b/decidim-forms/config/locales/sv.yml index cf8971b8aec75..0f38b0c136130 100644 --- a/decidim-forms/config/locales/sv.yml +++ b/decidim-forms/config/locales/sv.yml @@ -188,9 +188,6 @@ sv: questionnaire_closed: body: Formuläret är stängt och kan inte besvaras. title: Formuläret är stängt - questionnaire_for_private_users: - body: Formuläret är endast tillgängligt för privata användare - title: Formuläret är stängt questionnaire_js_disabled: body: Vissa av formulärets funktioner kommer att inaktiveras. Aktivera JavaScript i webbläsaren för att förbättra upplevelsen. title: JavaScript är inaktiverat diff --git a/decidim-forms/config/locales/tr-TR.yml b/decidim-forms/config/locales/tr-TR.yml index c3ee5b0b05565..0097feac467a5 100644 --- a/decidim-forms/config/locales/tr-TR.yml +++ b/decidim-forms/config/locales/tr-TR.yml @@ -78,9 +78,6 @@ tr: of_total_steps: '%{total_steps}' questionnaire_closed: title: Form kapatıldı - questionnaire_for_private_users: - body: Form yalnızca özel kullanıcılar tarafından kullanılabilir - title: Form kapatıldı questionnaire_js_disabled: body: Bu formun bazı özellikleri devre dışı bırakılacak. Deneyiminizi iyileştirmek için lütfen tarayıcınızda JavaScript'i etkinleştirin. title: JavaScript devre dışı diff --git a/decidim-forms/config/locales/zh-CN.yml b/decidim-forms/config/locales/zh-CN.yml index 14ce15a99d451..8f5a1d669122a 100644 --- a/decidim-forms/config/locales/zh-CN.yml +++ b/decidim-forms/config/locales/zh-CN.yml @@ -76,9 +76,6 @@ zh-CN: of_total_steps: '%{total_steps}' questionnaire_closed: title: 表单已关闭 - questionnaire_for_private_users: - body: 表单仅供私人用户使用 - title: 表单已关闭 questionnaire_js_disabled: body: 此表单的一些功能将被禁用。为了改善您的体验,请在您的浏览器中启用 JavaScript。 title: JavaScript 已禁用 diff --git a/decidim-forms/config/locales/zh-TW.yml b/decidim-forms/config/locales/zh-TW.yml index a10d704e680f2..701e42cee666d 100644 --- a/decidim-forms/config/locales/zh-TW.yml +++ b/decidim-forms/config/locales/zh-TW.yml @@ -93,9 +93,6 @@ zh-TW: of_total_steps: 的%{total_steps} questionnaire_closed: title: 表單已關閉 - questionnaire_for_private_users: - body: 此表單僅適用於私人使用者 - title: 表單已關閉 questionnaire_js_disabled: body: 此表單的某些功能將被禁用。為了改善您的體驗,請在您的瀏覽器中啟用JavaScript。 title: JavaScript已停用 diff --git a/decidim-initiatives/config/locales/eu.yml b/decidim-initiatives/config/locales/eu.yml index 4969ce7dc21f6..d84f46a6fd2dc 100644 --- a/decidim-initiatives/config/locales/eu.yml +++ b/decidim-initiatives/config/locales/eu.yml @@ -200,7 +200,7 @@ eu: badges: initiatives: conditions: - - Joan zaitez Intiatives-ko partaidetza espaziora + - Joan zaitez Intiatives-ko partaidetza-espaziora - Jarraitu urrats berriak beste ekimen bat sortzeko description: Garaikur hau ekimen berriak martxan jartzean ematen da, beste batzuekin partekatuz horiek lortzeko. description_another: Parte-hartzaile honek %{score} ekimen argitaratzea lortu du. @@ -394,7 +394,7 @@ eu: back: Atzera callout_text_created: Zorionak! Zure ekimena zuzen sortu da. callout_text_validating: Zure ekimena zuzen bidali da balidazio teknikora. - go_to_initiatives: Joan ekimenetara + go_to_initiatives: Ekimenetara joan publish_helper_text_html: "Gogoan izan zure ekimena argitaratzeko teknikoki baliozkotu behar dela, administratzaile batek berrikus dezan.\n" previous_form: back: Back @@ -411,8 +411,8 @@ eu: choose_html: %{title}bat sortu nahi dut consult_existing_initiatives: Kontsultatu lehendik dauden ekimenak continue: Jarraitu - more_information: (Informazio gehiago) - new: Sortu beste ekimen bat + more_information: Informazio gehiago + new: Beste ekimen bat sortu select: Ekimen hau sustatu nahi dut select_initiative_type_help_html: "

Ekimenak bide dira parte-hartzaileek parte har dezaten, erakundeak interes orokorraren aldeko ekintzak egin ditzan. Zein ekimen jarri nahi duzu abian?

" show_less: Gutxiago erakutsi @@ -431,7 +431,7 @@ eu: back: Atzera confirm: Ziur zaude? discard: Kendu ekimena - export_pdf_signatures: Esportatu sinaduren PDF + export_pdf_signatures: Sinadurak PDF batera esportatu export_votes: Sinadurak esportatu reject: Ukatu ekimena title: Ekimena editatu @@ -596,7 +596,7 @@ eu: votes_count: count: one: SIGNATURE - other: sinadurak + other: Sinadurak initiatives_mailer: creation_subject: Zure '%{title}'ekimena sortu egin da initiative_link: @@ -647,7 +647,7 @@ eu: name: Sinaduren Legezko Kudeatzailea states: accepted: Onartua - expired: iraungi + expired: Iraungita unavailable_scope: Eremua ez dago erabilgarri update: error: Arazo bat izan da ekimena eguneratzean. @@ -685,7 +685,7 @@ eu: resources: initiative: actions: - comment: Egin iruzkina + comment: Iruzkindu vote_comment: Bozkatu iruzkina initiatives_type: actions: diff --git a/decidim-meetings/config/locales/eu.yml b/decidim-meetings/config/locales/eu.yml index 123c991def1c9..2d49f96feddec 100644 --- a/decidim-meetings/config/locales/eu.yml +++ b/decidim-meetings/config/locales/eu.yml @@ -162,7 +162,7 @@ eu: comment: Iruzkina join: Hartu parte reply_poll: Erantzun inkesta - vote_comment: Bozkatu iruzkina + vote_comment: Iruzkina bozkatu name: Topaketak settings: global: @@ -339,7 +339,7 @@ eu: responses: Erantzunak invite_join_meeting_mailer: invite: - decline: Deuseztatu gonbidapena + decline: Gonbidapena deuseztatu invited_you_to_join_a_meeting: "%{invited_by} k %{application} topaketan parte hartzera gonbidatu zaitu. Beheko estekan onar dezakezu gobidapena." join: Erregistratu '%{meeting_title}' topaketa invites: @@ -506,7 +506,7 @@ eu: copy_calendar_url_description: Argitaratutako batzar guztiak zure egutegi-aplikazioan edo hornitzailean ikus ditzakezu. Kopiatu eta itsatsi URL hau "Egutegi berria gehitu URL batetik" aukera erabiliz copy_calendar_url_explanation: Mesedez, kontuan izan aukeratutako topaketa multzo bat esportatzen ari zarela eta iragazkiak aktibatuta daudela. Denak esportatu nahi badituzu, lehenengo kendu irakazkiak. copy_calendar_url_message: URL hau zuzen kopiatu da zure arbelean. - export_calendar: Esportatu egutegia + export_calendar: Egutegia esportatu close_meeting_reminder_mailer: close_meeting_reminder: body: "%{meeting_title}" topaketa ixteko dago. Mesedez, gehitu topaketari buruzko txosten bat "Itxi topaketa" botoia erabiliz. @@ -825,10 +825,10 @@ eu: title: Topaketaren izenburua transparent: Topaketaren ikusgarritasuna bisitarientzat type_of_meeting: Topaketa mota - updated_at: Zein datatan eguneratu zen topaketa azken aldiz + updated_at: Zein datatan eguneratu da topaketa azken aldiz url: Topaketaren URLa video_url: Topaketaren bideograbazioa - withdrawn: Topaketa bertan behera geratu zen ala ez + withdrawn: Topaketa bertan behera geratu den ala ez withdrawn_at: Noiz utzi zen bertan behera topaketa hau? participatory_spaces: highlighted_meetings: @@ -838,10 +838,10 @@ eu: resource_links: meetings_through_proposals: meeting_result: 'Topaketaren emaitzak:' - result_meeting: 'Proposamenarekin lotutako topaketak:' + result_meeting: 'Topaketari dagozkion proposamenak:' proposals_from_meeting: meeting_proposal: 'Topaketari dagozkion proposamenak:' - proposal_meeting: 'Proposamenarekin lotutako topaketak:' + proposal_meeting: 'Topaketari dagozkion proposamenak:' statistics: attendees_count: Topaketara bertaratuak meetings_count: Topaketak diff --git a/decidim-participatory_processes/config/locales/ca-IT.yml b/decidim-participatory_processes/config/locales/ca-IT.yml index 797553ae705aa..a4285fde6d061 100644 --- a/decidim-participatory_processes/config/locales/ca-IT.yml +++ b/decidim-participatory_processes/config/locales/ca-IT.yml @@ -115,8 +115,8 @@ ca-IT: components: Components info: Quant a aquest procés landing_page: Disposició de la pàgina de destinació + members: moderations: Moderacions - private_users: Membres process_admins: Administradores del procés steps: Fases del procés models: @@ -282,9 +282,11 @@ ca-IT: export: "%{user_name} ha exportat el procés participatiu %{resource_name}" import: "%{user_name} ha importat el procés participatiu %{resource_name}" publish: "%{user_name} ha publicat el procés participatiu %{resource_name}" + publish_all_members: "%{user_name} va publicar a totes les membres del procés participatiu %{resource_name}" restore: "%{user_name} ha restaurat el procés participatiu \"%{resource_name}\"" soft_delete: "%{user_name} ha enviat a la paperera el procés participatiu \"%{resource_name}\"" unpublish: "%{user_name} ha despublicat el procés participatiu %{resource_name}" + unpublish_all_members: "%{user_name} va despublicar a totes les membres del procés participatiu %{resource_name}" update: "%{user_name} ha actualitzat el procés participatiu %{resource_name}" participatory_process_group: create: "%{user_name} ha creat el grup de processos participatius %{resource_name}" @@ -509,7 +511,6 @@ ca-IT: images: Imatges metadata: Metadades no_taxonomy_filters_found: No s'han trobat filtres de taxonomia. - private_notice: Podràs administrar les membres un cop haguis configurar l'espai com a privat related_processes: Processos relacionats select_process_group: Selecciona un grup de processos slug_help_html: 'Els noms curts d''URL s''utilitzen per generar les URL que apunten a aquest procés. Només accepta lletres, números i guions, i ha de començar amb una lletra. Exemple: %{url}' @@ -558,6 +559,9 @@ ca-IT: title: Processos participatius last_activity: new_participatory_process: 'Nou procés participatiu:' + members: + index: + title: Membres pages: home: highlighted_processes: @@ -604,9 +608,6 @@ ca-IT: type: Tipus show: title: Quant a aquest procés - participatory_space_private_users: - index: - title: Membres show: belongs_to_group: Aquest procés pertany a private_space: Aquest és un procés privat diff --git a/decidim-participatory_processes/config/locales/ca.yml b/decidim-participatory_processes/config/locales/ca.yml index 59080cb0b33d5..e8a1e98ba86b6 100644 --- a/decidim-participatory_processes/config/locales/ca.yml +++ b/decidim-participatory_processes/config/locales/ca.yml @@ -115,8 +115,8 @@ ca: components: Components info: Quant a aquest procés landing_page: Disposició de la pàgina de destinació + members: moderations: Moderacions - private_users: Membres process_admins: Administradores del procés steps: Fases del procés models: @@ -282,9 +282,11 @@ ca: export: "%{user_name} ha exportat el procés participatiu %{resource_name}" import: "%{user_name} ha importat el procés participatiu %{resource_name}" publish: "%{user_name} ha publicat el procés participatiu %{resource_name}" + publish_all_members: "%{user_name} va publicar a totes les membres del procés participatiu %{resource_name}" restore: "%{user_name} ha restaurat el procés participatiu \"%{resource_name}\"" soft_delete: "%{user_name} ha enviat a la paperera el procés participatiu \"%{resource_name}\"" unpublish: "%{user_name} ha despublicat el procés participatiu %{resource_name}" + unpublish_all_members: "%{user_name} va despublicar a totes les membres del procés participatiu %{resource_name}" update: "%{user_name} ha actualitzat el procés participatiu %{resource_name}" participatory_process_group: create: "%{user_name} ha creat el grup de processos participatius %{resource_name}" @@ -509,7 +511,6 @@ ca: images: Imatges metadata: Metadades no_taxonomy_filters_found: No s'han trobat filtres de taxonomia. - private_notice: Podràs administrar les membres un cop haguis configurar l'espai com a privat related_processes: Processos relacionats select_process_group: Selecciona un grup de processos slug_help_html: 'Els noms curts d''URL s''utilitzen per generar les URL que apunten a aquest procés. Només accepta lletres, números i guions, i ha de començar amb una lletra. Exemple: %{url}' @@ -558,6 +559,9 @@ ca: title: Processos participatius last_activity: new_participatory_process: 'Nou procés participatiu:' + members: + index: + title: Membres pages: home: highlighted_processes: @@ -604,9 +608,6 @@ ca: type: Tipus show: title: Quant a aquest procés - participatory_space_private_users: - index: - title: Membres show: belongs_to_group: Aquest procés pertany a private_space: Aquest és un procés privat diff --git a/decidim-participatory_processes/config/locales/cs.yml b/decidim-participatory_processes/config/locales/cs.yml index ee13232b482b6..cbd4e32e732b1 100644 --- a/decidim-participatory_processes/config/locales/cs.yml +++ b/decidim-participatory_processes/config/locales/cs.yml @@ -122,7 +122,6 @@ cs: info: O tomto procesu landing_page: Rozložení vstupní strany moderations: Moderování - private_users: Členové process_admins: Administrátoři procesu steps: Kroky models: @@ -514,7 +513,6 @@ cs: images: Obrázky metadata: Metadata no_taxonomy_filters_found: Nebyly nalezeny žádné filtry taxonomie. - private_notice: Po nastavení jako soukromé budete moci spravovat členy related_processes: Související procesy select_process_group: Vyberte skupinu procesů slug_help_html: 'URL slugy slouží ke generaci adres URL, které odkazují na tento proces. Povolená jsou pouze písmena, číslice a pomlčky a musí začínat písmenem. Příklad: %{url}' @@ -615,9 +613,6 @@ cs: type: Typ show: title: O tomto procesu - participatory_space_private_users: - index: - title: Členové show: belongs_to_group: Tento proces patří do private_space: Jedná se o soukromý proces diff --git a/decidim-participatory_processes/config/locales/de.yml b/decidim-participatory_processes/config/locales/de.yml index c247fcc78ad44..a5f6042561ea4 100644 --- a/decidim-participatory_processes/config/locales/de.yml +++ b/decidim-participatory_processes/config/locales/de.yml @@ -116,7 +116,6 @@ de: info: Über diesen Prozess landing_page: Startseiten-Layout moderations: Moderationen - private_users: Mitglieder process_admins: Benutzer verarbeiten steps: Schritte models: @@ -508,7 +507,6 @@ de: images: Bilder metadata: Metadaten no_taxonomy_filters_found: Keine Klassifizierungsfilter gefunden. - private_notice: Sie können private Teilnehmende verwalten, nachdem Sie das Gremium als "privat" festgelegt haben related_processes: Ähnliche Beteiligungsprozesse select_process_group: Wählen Sie eine Prozessgruppe aus slug_help_html: 'URL-Slugs werden zum Generieren der URLs verwendet, die auf diesen Prozess verweisen. Akzeptiert werden nur Buchstaben, Zahlen und Bindestriche und es muss mit einem Buchstaben beginnen. Beispiel: %{url}' @@ -603,9 +601,6 @@ de: type: Typ show: title: Über diesen Prozess - participatory_space_private_users: - index: - title: Mitglieder show: belongs_to_group: Dieser Prozess gehört zu private_space: Dies ist ein privater Prozess diff --git a/decidim-participatory_processes/config/locales/es-MX.yml b/decidim-participatory_processes/config/locales/es-MX.yml index 6b20aeaf01684..5b44f28bbf701 100644 --- a/decidim-participatory_processes/config/locales/es-MX.yml +++ b/decidim-participatory_processes/config/locales/es-MX.yml @@ -115,8 +115,8 @@ es-MX: components: Componentes info: Acerca de este proceso landing_page: Disposición de la página de aterrizaje + members: Miembros moderations: Moderaciones - private_users: Miembros process_admins: Usuarios del proceso steps: Fases del proceso models: @@ -282,9 +282,11 @@ es-MX: export: "%{user_name} exportó el proceso participativo %{resource_name}" import: "%{user_name} importó el proceso participativo %{resource_name}" publish: "%{user_name} publicó el proceso participativo %{resource_name}" + publish_all_members: "%{user_name} publicó todas las miembros del proceso participativo %{resource_name}" restore: "%{user_name} restauró el proceso participativo \"%{resource_name}\"" soft_delete: "%{user_name} ha enviado a la papelera el proceso participativo \"%{resource_name}\"" unpublish: "%{user_name} despublicó el proceso participativo %{resource_name}" + unpublish_all_members: "%{user_name} despublicó a todas las miembros del proceso participativo %{resource_name}" update: "%{user_name} actualizó el proceso participativo %{resource_name}" participatory_process_group: create: "%{user_name} creó el grupo de procesos participativos de %{resource_name}" @@ -509,7 +511,6 @@ es-MX: images: Imágenes metadata: Metadatos no_taxonomy_filters_found: No se han encontrado filtros de taxonomía. - private_notice: Podrás administrar las miembros después de configurar el espacio como privado related_processes: Procesos relacionados select_process_group: Seleccione un grupo de proceso slug_help_html: 'Los nombres cortos de URL se utilizan para generar las URL que apuntan a esta votación. Sólo acepta letras, números y guiones, y debe comenzar con una letra. Ejemplo: %{url}' @@ -558,6 +559,9 @@ es-MX: title: Procesos participativos last_activity: new_participatory_process: 'Nuevo proceso participativo:' + members: + index: + title: Miembros pages: home: highlighted_processes: @@ -604,9 +608,6 @@ es-MX: type: Tipo show: title: Acerca de este proceso - participatory_space_private_users: - index: - title: Miembros show: belongs_to_group: Este proceso pertenece a private_space: Este es un proceso privado diff --git a/decidim-participatory_processes/config/locales/es-PY.yml b/decidim-participatory_processes/config/locales/es-PY.yml index 2d89e8d262bb1..2a0d5e20d3363 100644 --- a/decidim-participatory_processes/config/locales/es-PY.yml +++ b/decidim-participatory_processes/config/locales/es-PY.yml @@ -115,8 +115,8 @@ es-PY: components: Componentes info: Acerca de este proceso landing_page: Disposición de la página de aterrizaje + members: Miembros moderations: Moderaciones - private_users: Miembros process_admins: Usuarios del proceso steps: Fases del proceso models: @@ -282,9 +282,11 @@ es-PY: export: "%{user_name} ha exportado el proceso participativo %{resource_name}" import: "%{user_name} ha importado el proceso participativo %{resource_name}" publish: "%{user_name} publicó el proceso participativo %{resource_name}" + publish_all_members: "%{user_name} publicó todas las miembros del proceso participativo %{resource_name}" restore: "%{user_name} restauró el proceso participativo \"%{resource_name}\"" soft_delete: "%{user_name} ha enviado a la papelera el proceso participativo \"%{resource_name}\"" unpublish: "%{user_name} despublicó el proceso participativo %{resource_name}" + unpublish_all_members: "%{user_name} despublicó a todas las miembros del proceso participativo %{resource_name}" update: "%{user_name} actualizó el proceso participativo %{resource_name}" participatory_process_group: create: "%{user_name} creó el grupo de procesos participativos de %{resource_name}" @@ -509,7 +511,6 @@ es-PY: images: Imágenes metadata: Metadatos no_taxonomy_filters_found: No se han encontrado filtros de taxonomía. - private_notice: Podrás administrar las miembros después de configurar el espacio como privado related_processes: Procesos relacionados select_process_group: Seleccione un grupo de proceso slug_help_html: 'Los nombres cortos de URL se utilizan para generar las URL que apuntan a esta votación. Sólo acepta letras, números y guiones, y debe comenzar con una letra. Ejemplo: %{url}' @@ -558,6 +559,9 @@ es-PY: title: Procesos participativos last_activity: new_participatory_process: 'Nuevo proceso participativo:' + members: + index: + title: Miembros pages: home: highlighted_processes: @@ -604,9 +608,6 @@ es-PY: type: Tipo show: title: Acerca de este proceso - participatory_space_private_users: - index: - title: Miembros show: belongs_to_group: Este proceso pertenece a private_space: Este es un proceso privado diff --git a/decidim-participatory_processes/config/locales/es.yml b/decidim-participatory_processes/config/locales/es.yml index 0c5ab70e90ed0..c32731f4fadc0 100644 --- a/decidim-participatory_processes/config/locales/es.yml +++ b/decidim-participatory_processes/config/locales/es.yml @@ -115,8 +115,8 @@ es: components: Componentes info: Acerca de este proceso landing_page: Disposición de la página de aterrizaje + members: Miembros moderations: Moderaciones - private_users: Miembros process_admins: Administradoras del proceso steps: Fases del proceso models: @@ -282,9 +282,11 @@ es: export: "%{user_name} ha exportado el proceso participativo %{resource_name}" import: "%{user_name} ha importado el proceso participativo %{resource_name}" publish: "%{user_name} publicó el proceso participativo %{resource_name}" + publish_all_members: "%{user_name} publicó todas las miembros del proceso participativo %{resource_name}" restore: "%{user_name} restauró el proceso participativo \"%{resource_name}\"" soft_delete: "%{user_name} ha enviado a la papelera el proceso participativo \"%{resource_name}\"" unpublish: "%{user_name} despublicó el proceso participativo %{resource_name}" + unpublish_all_members: "%{user_name} despublicó a todas las miembros del proceso participativo %{resource_name}" update: "%{user_name} actualizó el proceso participativo %{resource_name}" participatory_process_group: create: "%{user_name} creó el grupo de procesos participativos de %{resource_name}" @@ -509,7 +511,6 @@ es: images: Imágenes metadata: Metadatos no_taxonomy_filters_found: No se han encontrado filtros de taxonomía. - private_notice: Podrás administrar las miembros después de configurar el espacio como privado related_processes: Procesos relacionados select_process_group: Selecciona un grupo de procesos slug_help_html: 'Los nombres cortos de URL se utilizan para generar las URL que apuntan a esta votación. Sólo acepta letras, números y guiones, y debe comenzar con una letra. Ejemplo: %{url}' @@ -558,6 +559,9 @@ es: title: Procesos participativos last_activity: new_participatory_process: 'Nuevo proceso participativo:' + members: + index: + title: Miembros pages: home: highlighted_processes: @@ -604,9 +608,6 @@ es: type: Tipo show: title: Acerca de este proceso - participatory_space_private_users: - index: - title: Miembros show: belongs_to_group: Este proceso pertenece a private_space: Este es un proceso privado diff --git a/decidim-participatory_processes/config/locales/eu.yml b/decidim-participatory_processes/config/locales/eu.yml index a30b0262c3254..9eef70cd683b6 100644 --- a/decidim-participatory_processes/config/locales/eu.yml +++ b/decidim-participatory_processes/config/locales/eu.yml @@ -78,7 +78,7 @@ eu: other: Partaidetza-prozesuen taldeak decidim/participatory_process_step: one: Step - other: Urratsak + other: Faseak decidim: admin: actions: @@ -115,8 +115,8 @@ eu: components: Osagaiak info: Prozesu honi buruz landing_page: Lurreratze-orrien antolaketa + members: Kideak moderations: Moderazioak - private_users: Kideak process_admins: Prozesuaren parte-hartzaileak steps: Prozesuaren faseak models: @@ -282,9 +282,11 @@ eu: export: "%{user_name} parte-hartzaileak esportatu du %{resource_name} partaidetza-prozesua" import: "%{user_name} parte-hartzaileak inportatu du %{resource_name} partaidetza-prozesua" publish: "%{user_name} parte-hartzaileak argitaratu du %{resource_name} partaidetza-prozesua" + publish_all_members: "%{user_name} parte-hartzaileak %{resource_name} partaidetza-prozesuko kide guztiak argitaratu ditu" restore: "%{user_name} parte-hartzaileak berreskuratu du %{resource_name} partaidetza-prozesua" soft_delete: "%{user_name} parte-hartzaileak zaborrontzira eraman du %{resource_name} partaidetza-prozesua" unpublish: "%{user_name} parte-hartzaileak despublikatu du %{resource_name} partaidetza-prozesua" + unpublish_all_members: "%{user_name} parte-hartzaileak %{resource_name} partaidetza-prozesuko kide guztiak desargitaratu ditu" update: "%{user_name} parte-hartzaileak %{resource_name} partaidetza-prozesua eguneratu du" participatory_process_group: create: "%{user_name} parte-hartzaileak sortu du %{resource_name} partaidetza-prozesuen multzoa" @@ -509,7 +511,6 @@ eu: images: Irudiak metadata: Metadatuak no_taxonomy_filters_found: Ez da taxonomia-iragazkirik aurkitu. - private_notice: Kiak kudeatu ahal izango dituzu pribatu gisa ezarri ondoren related_processes: Elkarrekin lotutako prozesuak select_process_group: Prozesu-talde bat hautatu slug_help_html: 'URLetako testu laburrak erabiltzen dira prozesu honi aurre egiteko URLak sortzeko. Letrak, zenbakiak eta gidoiak soilik onartzen ditu, eta letra batez hasi behar du. Adibidea: %{url}' @@ -558,6 +559,9 @@ eu: title: Partaidetza-prozesuak last_activity: new_participatory_process: 'Beste partaidetza-prozesu bat:' + members: + index: + title: Kideak pages: home: highlighted_processes: @@ -604,9 +608,6 @@ eu: type: Mota show: title: Prozesu honi buruz - participatory_space_private_users: - index: - title: Kideak show: belongs_to_group: Prozesu hau nori dagokio private_space: Prozesu pribatua da diff --git a/decidim-participatory_processes/config/locales/fi-plain.yml b/decidim-participatory_processes/config/locales/fi-plain.yml index ebd8211a98194..837bc76352a2f 100644 --- a/decidim-participatory_processes/config/locales/fi-plain.yml +++ b/decidim-participatory_processes/config/locales/fi-plain.yml @@ -14,6 +14,7 @@ fi-pl: duplicate_landing_page_blocks: Kopioi laskeutumissivun sisältölohkot duplicate_steps: Kopioi vaiheet end_date: Päättymispäivä + has_members: Tässä tilassa on jäseniä hero_image: Etusivun kuva import_attachments: Tuo liitteitä import_categories: Kopioi aihepiirejä @@ -115,8 +116,8 @@ fi-pl: components: Komponentit info: Tietoa tästä prosessista landing_page: Laskeutumissivun asettelu + members: Jäsenet moderations: Moderoinnit - private_users: Jäsenet process_admins: Prosessin käyttäjät steps: Vaiheet models: @@ -282,9 +283,11 @@ fi-pl: export: "%{user_name} vei osallistumisprosessin %{resource_name}" import: "%{user_name} toi osallistumisprosessin %{resource_name}" publish: "%{user_name} julkaisi osallisuusprosessin %{resource_name}" + publish_all_members: "%{user_name} julkaisi osallistumisprosessin %{resource_name} jäsenet" restore: "%{user_name} palautti osallistumisprosessin %{resource_name}" soft_delete: "%{user_name} siirsi osallistumisprosessin %{resource_name} roskakoriin" unpublish: "%{user_name} muutti osallisuusprosessin %{resource_name} julkaisemattomaksi" + unpublish_all_members: "%{user_name} perui osallistumisprosessin %{resource_name} jäsenten julkaisun" update: "%{user_name} päivitti osallisuusprosessin %{resource_name}" participatory_process_group: create: "%{user_name} loi osallisuusprosessien ryhmän %{resource_name}" @@ -506,10 +509,11 @@ fi-pl: announcement_help: Tähän kirjoitettu teksti näkyy käyttäjälle heti prosessitietojen alapuolella. define_taxonomy_filters: Määritä osallistumistilalle suodattimia ennen kuin käytät tätä asetusta. duration: Kesto + has_members_help: Voit luoda ja julkaista jäseniä images: kuvat metadata: metadata no_taxonomy_filters_found: Luokittelusuodattimia ei löytynyt. - private_notice: Voit hallinnoida osallistumistilan yksityisiä käyttäjiä asettamalla osallistumistilan yksityiseksi + private_notice: Piilottaa osallistumistilan verkkosivuston vierailijoiden näkyvistä (ellei osallistumistila ole myös läpinäkyvä) related_processes: Liittyvät prosessit select_process_group: Valitse prosessiryhmä slug_help_html: 'URL-tunnisteita käytetään tuottamaan URL-osoitteet, jotka viittaavat tähän prosessiin. Hyväksyy vain kirjaimet, numerot ja viivat. Arvon on alettava kirjaimella. Esimerkki: %{url}' @@ -558,6 +562,9 @@ fi-pl: title: Osallisuusprosessit last_activity: new_participatory_process: 'Uusi osallistumisprosessi:' + members: + index: + title: Jäsenet pages: home: highlighted_processes: @@ -604,9 +611,6 @@ fi-pl: type: Tyyppi show: title: Tietoa tästä prosessista - participatory_space_private_users: - index: - title: Jäsenet show: belongs_to_group: Tämä prosessi kuuluu ryhmään private_space: Tämä on yksityinen prosessi diff --git a/decidim-participatory_processes/config/locales/fi.yml b/decidim-participatory_processes/config/locales/fi.yml index cc6a07f75cce0..efe4049f57c4d 100644 --- a/decidim-participatory_processes/config/locales/fi.yml +++ b/decidim-participatory_processes/config/locales/fi.yml @@ -14,6 +14,7 @@ fi: duplicate_landing_page_blocks: Kopioi laskeutumissivun sisältölohkot duplicate_steps: Kopioi vaiheet end_date: Päättymispäivä + has_members: Tässä tilassa on jäseniä hero_image: Etusivun kuva import_attachments: Tuo liitteitä import_categories: Kopioi aihepiirejä @@ -115,8 +116,8 @@ fi: components: Komponentit info: Tietoa tästä prosessista landing_page: Laskeutumissivun asettelu + members: Jäsenet moderations: Moderoinnit - private_users: Jäsenet process_admins: Prosessin hallintakäyttäjät steps: Vaiheet models: @@ -282,9 +283,11 @@ fi: export: "%{user_name} vei osallistumisprosessin %{resource_name}" import: "%{user_name} toi osallistumisprosessin %{resource_name}" publish: "%{user_name} julkaisi osallistumisprosessin %{resource_name}" + publish_all_members: "%{user_name} julkaisi osallistumisprosessin %{resource_name} jäsenet" restore: "%{user_name} palautti osallistumisprosessin %{resource_name}" soft_delete: "%{user_name} siirsi osallistumisprosessin %{resource_name} roskakoriin" unpublish: "%{user_name} perui osallistumisprosessin %{resource_name} julkaisun" + unpublish_all_members: "%{user_name} perui osallistumisprosessin %{resource_name} jäsenten julkaisun" update: "%{user_name} päivitti osallistumisprosessin %{resource_name}" participatory_process_group: create: "%{user_name} loi osallistumisprosessien ryhmän %{resource_name}" @@ -506,10 +509,11 @@ fi: announcement_help: Tähän kirjoitettu teksti näkyy käyttäjälle heti prosessitietojen alapuolella. define_taxonomy_filters: Määritä osallistumistilalle suodattimia ennen kuin käytät tätä asetusta. duration: Kesto + has_members_help: Voit luoda ja julkaista jäseniä images: Kuvat metadata: Metatiedot no_taxonomy_filters_found: Luokittelusuodattimia ei löytynyt. - private_notice: Voit hallinnoida osallistumistilan yksityisiä käyttäjiä asettamalla osallistumistilan yksityiseksi + private_notice: Piilottaa osallistumistilan verkkosivuston vierailijoiden näkyvistä (ellei osallistumistila ole myös läpinäkyvä) related_processes: Liittyvät prosessit select_process_group: Valitse prosessiryhmä slug_help_html: 'URL-tunnisteita käytetään tuottamaan URL-osoitteet, jotka viittaavat tähän prosessiin. Hyväksyy vain kirjaimet, numerot ja viivat. Arvon on alettava kirjaimella. Esimerkki: %{url}' @@ -558,6 +562,9 @@ fi: title: Osallistumisprosessit last_activity: new_participatory_process: 'Uusi osallistumisprosessi:' + members: + index: + title: Jäsenet pages: home: highlighted_processes: @@ -604,9 +611,6 @@ fi: type: Tyyppi show: title: Tietoa tästä prosessista - participatory_space_private_users: - index: - title: Jäsenet show: belongs_to_group: Tämä prosessi kuuluu ryhmään private_space: Tämä on yksityinen prosessi diff --git a/decidim-participatory_processes/config/locales/fr-CA.yml b/decidim-participatory_processes/config/locales/fr-CA.yml index ad257689a6cae..ea06b08c0725b 100644 --- a/decidim-participatory_processes/config/locales/fr-CA.yml +++ b/decidim-participatory_processes/config/locales/fr-CA.yml @@ -14,6 +14,7 @@ fr-CA: duplicate_landing_page_blocks: Dupliquer les blocs de page d'accueil duplicate_steps: Dupliquer les étapes end_date: Date de fin + has_members: Cet espace a des membres hero_image: Image de la page d'accueil import_attachments: Importer les pièces jointes import_categories: Importer les catégories @@ -113,8 +114,8 @@ fr-CA: attachments: Documents liés components: Fonctionnalités info: A propos de cette concertation + members: moderations: Modérations - private_users: Membres process_admins: Administrateurs de la concertation steps: Étapes models: @@ -280,7 +281,9 @@ fr-CA: export: "%{user_name} a exporté la concertation %{resource_name}" import: "%{user_name} a importé la concertation %{resource_name}" publish: "%{user_name} a publié la concertation %{resource_name}" + publish_all_members: "%{user_name} a publié tous les membres du processus participatif %{resource_name}" unpublish: "%{user_name} n'a pas publié la concertation %{resource_name}" + unpublish_all_members: "%{user_name} a dépublié tous les membres du processus participatif %{resource_name}" update: "%{user_name} a mis à jour la concertation %{resource_name}" participatory_process_group: create: "%{user_name} a créé le groupe de concertations %{resource_name}" @@ -458,10 +461,11 @@ fr-CA: announcement_help: Le texte saisi ici sera affiché juste en dessous des informations de la concertation. define_taxonomy_filters: Veuillez définir des filtres pour cet espace participatif avant d'utiliser ce paramètre. duration: Durée + has_members_help: Vous pourrez créer et publier des membres images: Images metadata: Métadonnées no_taxonomy_filters_found: Aucun filtre de taxonomie trouvé. - private_notice: Vous serez en mesure de gérer les membres après l'avoir défini comme privé + private_notice: Rend l'espace non visible pour les visiteurs et visible seulement pour les membres (sauf s'il est également transparent) related_processes: Concertations liées select_process_group: Sélectionnez un groupe de concertations slug_help_html: 'Les identifiants d''URL sont utilisés pour générer les URL qui pointent vers cette concertation. N''accepte que des lettres, des chiffres et des tirets et doit commencer par une lettre. Exemple : %{url}' @@ -493,6 +497,9 @@ fr-CA: title: Concertations last_activity: new_participatory_process: 'Nouveau processus participatif:' + members: + index: + title: Membres pages: home: highlighted_processes: @@ -539,9 +546,6 @@ fr-CA: type: Type show: title: A propos de cette concertation - participatory_space_private_users: - index: - title: Membres show: belongs_to_group: Cette concertation appartient à private_space: Ceci est une concertation privée diff --git a/decidim-participatory_processes/config/locales/fr.yml b/decidim-participatory_processes/config/locales/fr.yml index 6e436b78a542a..5de0aa7cfee80 100644 --- a/decidim-participatory_processes/config/locales/fr.yml +++ b/decidim-participatory_processes/config/locales/fr.yml @@ -14,6 +14,7 @@ fr: duplicate_landing_page_blocks: Dupliquer les blocs de page d'accueil duplicate_steps: Dupliquer les étapes end_date: Date de fin + has_members: Cet espace a des membres hero_image: Image de la page d'accueil import_attachments: Importer les pièces jointes import_categories: Importer les catégories @@ -113,8 +114,8 @@ fr: attachments: Documents liés components: Fonctionnalités info: A propos de cette concertation + members: moderations: Modérations - private_users: Membres process_admins: Administrateurs de la concertation steps: Étapes models: @@ -280,7 +281,9 @@ fr: export: "%{user_name} a exporté la concertation %{resource_name}" import: "%{user_name} a importé la concertation %{resource_name}" publish: "%{user_name} a publié la concertation %{resource_name}" + publish_all_members: "%{user_name} a publié tous les membres du processus participatif %{resource_name}" unpublish: "%{user_name} n'a pas publié la concertation %{resource_name}" + unpublish_all_members: "%{user_name} a dépublié tous les membres du processus participatif %{resource_name}" update: "%{user_name} a mis à jour la concertation %{resource_name}" participatory_process_group: create: "%{user_name} a créé le groupe de concertations %{resource_name}" @@ -458,10 +461,11 @@ fr: announcement_help: Le texte saisi ici sera affiché juste en dessous des informations de la concertation. define_taxonomy_filters: Veuillez définir des filtres pour cet espace participatif avant d'utiliser ce paramètre. duration: Durée + has_members_help: Vous pourrez créer et publier des membres images: Images metadata: Métadonnées no_taxonomy_filters_found: Aucun filtre de taxonomie trouvé. - private_notice: Vous serez en mesure de gérer les membres après l'avoir défini comme privé + private_notice: Rend l'espace non visible pour les visiteurs et visible seulement pour les membres (sauf s'il est également transparent) related_processes: Concertations liées select_process_group: Sélectionnez un groupe de concertations slug_help_html: 'Les identifiants d''URL sont utilisés pour générer les URL qui pointent vers cette concertation. N''accepte que des lettres, des chiffres et des tirets et doit commencer par une lettre. Exemple : %{url}' @@ -493,6 +497,9 @@ fr: title: Concertations last_activity: new_participatory_process: 'Nouveau processus participatif:' + members: + index: + title: Membres pages: home: highlighted_processes: @@ -539,9 +546,6 @@ fr: type: Type show: title: A propos de cette concertation - participatory_space_private_users: - index: - title: Membres show: belongs_to_group: Cette concertation appartient à private_space: Ceci est une concertation privée diff --git a/decidim-participatory_processes/config/locales/ja.yml b/decidim-participatory_processes/config/locales/ja.yml index 2881bef1b6f59..bcddfffbbe086 100644 --- a/decidim-participatory_processes/config/locales/ja.yml +++ b/decidim-participatory_processes/config/locales/ja.yml @@ -112,8 +112,8 @@ ja: components: コンポーネント info: このプロセスについて landing_page: ランディングページのレイアウト + members: メンバー moderations: モデレーション - private_users: メンバー process_admins: 参加型プロセス管理者 steps: フェーズ models: @@ -279,9 +279,11 @@ ja: export: "%{user_name} が %{resource_name} の参加プロセスをエクスポートしました" import: "%{user_name} が %{resource_name} の参加プロセスをインポートしました" publish: "%{user_name} が %{resource_name} 参加型プロセスを公開しました" + publish_all_members: "%{user_name} が参加型プロセス %{resource_name} のすべてのメンバーを公開しました" restore: "%{user_name} が %{resource_name} 参加型プロセスを復元しました" soft_delete: "%{user_name} が %{resource_name} 参加型プロセスをゴミ箱に移動しました" unpublish: "%{user_name} が %{resource_name} 参加型プロセスを非公開にしました" + unpublish_all_members: "%{user_name} が参加型プロセス %{resource_name} のすべてのメンバーを非公開にしました" update: "%{user_name} が %{resource_name} の参加型プロセスを更新しました" participatory_process_group: create: "%{user_name} が %{resource_name} 参加型プロセスグループを作成しました" @@ -505,7 +507,6 @@ ja: images: 画像 metadata: メタデータ no_taxonomy_filters_found: タクソノミーフィルタが見つかりません。 - private_notice: プライベートとして設定した後、メンバーを管理することができます related_processes: 関連する参加型プロセス select_process_group: 参加型プロセス グループを選択 slug_help_html: 'URLスラグは、この参加型プロセスを指すURLを生成するために使用されます。英字、数字、ハイフンのみを受け付け、英字で始める必要があります。例: %{url}' @@ -554,6 +555,9 @@ ja: title: 参加型プロセス last_activity: new_participatory_process: '新しい参加型プロセス:' + members: + index: + title: メンバー pages: home: highlighted_processes: @@ -596,9 +600,6 @@ ja: type: 種別 show: title: このプロセスについて - participatory_space_private_users: - index: - title: メンバー show: belongs_to_group: このプロセスの所属グループ private_space: これはプライベートな参加型プロセスです diff --git a/decidim-participatory_processes/config/locales/sv.yml b/decidim-participatory_processes/config/locales/sv.yml index bf84a24e8efc1..736b14c37823a 100644 --- a/decidim-participatory_processes/config/locales/sv.yml +++ b/decidim-participatory_processes/config/locales/sv.yml @@ -114,7 +114,6 @@ sv: components: Komponenter info: Om processen moderations: Moderering - private_users: Medlemmar process_admins: Administratörer för process steps: Steg models: @@ -280,9 +279,11 @@ sv: export: "%{user_name} exporterade processen %{resource_name}" import: "%{user_name} importerade processen %{resource_name}" publish: "%{user_name} publicerade deltagandeprocessen %{resource_name}" + publish_all_members: "%{user_name} publicerade deltagandeprocessen %{resource_name}" restore: "%{user_name} återställde deltagandeprocessen %{resource_name}" soft_delete: "%{user_name} raderade deltagandeprocessen %{resource_name}" unpublish: "%{user_name} avpublicerade deltagandeprocessen %{resource_name}" + unpublish_all_members: "%{user_name} avpublicerade deltagandeprocessen %{resource_name}" update: "%{user_name} uppdaterade deltagandeprocessen %{resource_name}" participatory_process_group: create: "%{user_name} skapade gruppen %{resource_name}" @@ -486,7 +487,6 @@ sv: images: Bilder metadata: Metadata no_taxonomy_filters_found: Inga filter för kategorier hittades. - private_notice: Du kommer att kunna hantera medlemmar om processen gjorts privat related_processes: Relaterade processer select_process_group: Välj en grupp av processer slug_help_html: 'URL-slugs används till att generera URL:er till processen. Använd bara bokstäver, siffror och bindestreck, och de måste börja med en bokstav. Exempel: %{url}' @@ -561,9 +561,6 @@ sv: type: Typ show: title: Om processen - participatory_space_private_users: - index: - title: Medlemmar show: belongs_to_group: Den här processen tillhör private_space: Det här är en privat process diff --git a/decidim-proposals/config/locales/eu.yml b/decidim-proposals/config/locales/eu.yml index 8355b8d9c27cc..ba32c8335f4b7 100644 --- a/decidim-proposals/config/locales/eu.yml +++ b/decidim-proposals/config/locales/eu.yml @@ -386,7 +386,7 @@ eu: badges: accepted_proposals: conditions: - - Aukeratu zure intereseko partaidetza espazioa proposamenak bidaltzeko + - Aukeratu zure intereseko partaidetza-espazioa proposamenak bidaltzeko - Egin daitezkeen proposamenak egiten saiatu. Horrela onartuak izateko aukera gehiago dute. description: Garaikur hau proposamen berriekin aktiboki parte hartzen duzunean eta horiek onartzen direnean ematen da description_another: Parte-hartzaile honek %{score} proposamen onartu ditu. @@ -408,7 +408,7 @@ eu: unearned_own: Oraindik ez duzu proposamenik bozkatu. proposals: conditions: - - Aukeratu zure interesekoa den parte hartzeko espazioa, gaitutako proposamenak aurkeztuta + - Aukeratu zure interesekoa den partaidetza-espazioa, gaitutako proposamenak aurkeztuta - Sortu beste proposamen bat description: Garaikur hau proposamen berriekin modu aktiboan parte hartzen duzunean ematen da. description_another: Parte-hartzaile honek %{score} proposamen sortu ditu. @@ -600,7 +600,7 @@ eu: destroy: success: Egoera behar bezala ezabatua edit: - title: Editatu egoera + title: Egoera editatu update: Eguneratu form: preview: Aurrebistaratu diff --git a/decidim-surveys/config/locales/ca-IT.yml b/decidim-surveys/config/locales/ca-IT.yml index 47e021e9f06c7..6fdc6a8e99931 100644 --- a/decidim-surveys/config/locales/ca-IT.yml +++ b/decidim-surveys/config/locales/ca-IT.yml @@ -71,6 +71,7 @@ ca-IT: manage_questions: Preguntes new_survey: Nova enquesta preview: Previsualitzar + responses: Respostes responses_alert: L'opció d'esborrar les respostes en publicar l'enquesta està activada. Si segueixes, s'esborraran les %{responses_count} existents actualment. title: Accions admin: diff --git a/decidim-surveys/config/locales/ca.yml b/decidim-surveys/config/locales/ca.yml index 511dc34128dbe..2d4c4cc13d05f 100644 --- a/decidim-surveys/config/locales/ca.yml +++ b/decidim-surveys/config/locales/ca.yml @@ -71,6 +71,7 @@ ca: manage_questions: Preguntes new_survey: Nova enquesta preview: Previsualitzar + responses: Respostes responses_alert: L'opció d'esborrar les respostes en publicar l'enquesta està activada. Si segueixes, s'esborraran les %{responses_count} existents actualment. title: Accions admin: diff --git a/decidim-surveys/config/locales/es-MX.yml b/decidim-surveys/config/locales/es-MX.yml index f376da438495a..55594598b9b7c 100644 --- a/decidim-surveys/config/locales/es-MX.yml +++ b/decidim-surveys/config/locales/es-MX.yml @@ -71,6 +71,7 @@ es-MX: manage_questions: Preguntas new_survey: Nueva encuesta preview: Previsualizar + responses: Respuestas responses_alert: La opción de borrar las respuestas al publicar la encuesta está activada. Si sigues, se borrarán las %{responses_count} existentes actualmente. title: Acciones admin: diff --git a/decidim-surveys/config/locales/es-PY.yml b/decidim-surveys/config/locales/es-PY.yml index 5c77b85e08657..4dc5f5a7062eb 100644 --- a/decidim-surveys/config/locales/es-PY.yml +++ b/decidim-surveys/config/locales/es-PY.yml @@ -71,6 +71,7 @@ es-PY: manage_questions: Preguntas new_survey: Nueva encuesta preview: Previsualizar + responses: Respuestas responses_alert: La opción de borrar las respuestas al publicar la encuesta está activada. Si sigues, se borrarán las %{responses_count} existentes actualmente. title: Acciones admin: diff --git a/decidim-surveys/config/locales/es.yml b/decidim-surveys/config/locales/es.yml index 71b369fb78bf7..9f48afc165cdb 100644 --- a/decidim-surveys/config/locales/es.yml +++ b/decidim-surveys/config/locales/es.yml @@ -71,6 +71,7 @@ es: manage_questions: Preguntas new_survey: Nueva encuesta preview: Previsualizar + responses: Respuestas responses_alert: La opción de borrar las respuestas al publicar la encuesta está activada. Si sigues, se borrarán las %{responses_count} existentes actualmente. title: Acciones admin: diff --git a/decidim-surveys/config/locales/eu.yml b/decidim-surveys/config/locales/eu.yml index 775418d225c95..4fd8e1fb6e309 100644 --- a/decidim-surveys/config/locales/eu.yml +++ b/decidim-surveys/config/locales/eu.yml @@ -71,6 +71,7 @@ eu: manage_questions: Galderak new_survey: Beste inkesta bat preview: Aurreikusi + responses: Erantzunak responses_alert: Erantzunak ezabatzea argitalpenean aktibatuta dago inkesta honetarako. Une honetan, erantzunen %{responses_count} suntsitu egingo dira, jarraitzen baduzu. title: Ekintzak admin: diff --git a/decidim-surveys/config/locales/fi-plain.yml b/decidim-surveys/config/locales/fi-plain.yml index 802c2d18fb03f..7ae6b99fd2870 100644 --- a/decidim-surveys/config/locales/fi-plain.yml +++ b/decidim-surveys/config/locales/fi-plain.yml @@ -71,6 +71,7 @@ fi-pl: manage_questions: Kysymykset new_survey: Uusi kysely preview: Esikatsele + responses: Vastaukset responses_alert: Kyselyn vastausten poistaminen julkaisun yhteydessä on otettu käyttöön tälle kyselylle. Kyselyllä on tällä hetkellä %{responses_count} vastausta, jotka poistetaan, mikäli jatkat eteenpäin. title: Toiminnot admin: diff --git a/decidim-surveys/config/locales/fi.yml b/decidim-surveys/config/locales/fi.yml index dc9edd86ce0ed..c4e3df9fd73b8 100644 --- a/decidim-surveys/config/locales/fi.yml +++ b/decidim-surveys/config/locales/fi.yml @@ -71,6 +71,7 @@ fi: manage_questions: Kysymykset new_survey: Uusi kysely preview: Esikatsele + responses: Vastaukset responses_alert: Kyselyn vastausten poistaminen julkaisun yhteydessä on otettu käyttöön tälle kyselylle. Kyselyllä on tällä hetkellä %{responses_count} vastausta, jotka poistetaan, mikäli jatkat eteenpäin. title: Toiminnot admin: diff --git a/decidim-surveys/config/locales/fr-CA.yml b/decidim-surveys/config/locales/fr-CA.yml index cdb5d5f9047b2..f8897e80dc782 100644 --- a/decidim-surveys/config/locales/fr-CA.yml +++ b/decidim-surveys/config/locales/fr-CA.yml @@ -71,6 +71,7 @@ fr-CA: manage_questions: Questions new_survey: Nouvelle enquête preview: Aperçu + responses: Réponses responses_alert: La suppression des réponses lors de la publication est active pour cette enquête. Il y a actuellement %{responses_count} réponses qui seront supprimées si vous continuez. title: Actions admin: diff --git a/decidim-surveys/config/locales/fr.yml b/decidim-surveys/config/locales/fr.yml index 844d8f44d216b..78cfff4071d96 100644 --- a/decidim-surveys/config/locales/fr.yml +++ b/decidim-surveys/config/locales/fr.yml @@ -71,6 +71,7 @@ fr: manage_questions: Questions new_survey: Nouvelle enquête preview: Aperçu + responses: Réponses responses_alert: La suppression des réponses lors de la publication est active pour cette enquête. Il y a actuellement %{responses_count} réponses qui seront supprimées si vous continuez. title: Actions admin: diff --git a/decidim-surveys/config/locales/ja.yml b/decidim-surveys/config/locales/ja.yml index 3c85ea7cbf8dd..8b46d0c9ecd55 100644 --- a/decidim-surveys/config/locales/ja.yml +++ b/decidim-surveys/config/locales/ja.yml @@ -69,6 +69,7 @@ ja: manage_questions: 質問 new_survey: 新しいアンケート preview: プレビュー + responses: 回答 responses_alert: このアンケートでは、公開時に回答を削除する設定が有効になっています。現在%{responses_count}件の回答があり、続行するとそれらは削除されます。 title: アクション admin: diff --git a/decidim-surveys/config/locales/sv.yml b/decidim-surveys/config/locales/sv.yml index 519d2753591b3..f1542d4562a3d 100644 --- a/decidim-surveys/config/locales/sv.yml +++ b/decidim-surveys/config/locales/sv.yml @@ -49,6 +49,8 @@ sv: responses: 'Svar:' surveys_count_tooltip: Antalet tillgängliga undersökningar och svar som samlats in. surveys: + actions: + responses: Svar admin: surveys: new: diff --git a/decidim-system/config/locales/ca-IT.yml b/decidim-system/config/locales/ca-IT.yml index 8467cdf931bd1..9f4cb6b6fa3a9 100644 --- a/decidim-system/config/locales/ca-IT.yml +++ b/decidim-system/config/locales/ca-IT.yml @@ -264,6 +264,7 @@ ca-IT: confirm_resend_invitation: Segur que vols reenviar la invitació? resend_invitation: Reenviar la invitació secondary_hosts_hint: Introdueix cada un d'ells en una nova línia + short_name_hint: Nom curt que es farà servir per a l'Aplicació Web Progressiva (PWA). Ha de tenir un màxim de 12 caràcters. title: Editar l'organització file_upload_settings: content_types: @@ -293,6 +294,7 @@ ca-IT: organization_admin_email_hint: T'enviarem un correu electrònic a aquesta adreça perquè puguis confirmar-la i configurar la teva contrasenya. reference_prefix_hint: El prefix de la referència s'utilitza per identificar de forma única els recursos del conjunt de l'organització. secondary_hosts_hint: Introdueix cada un d'ells en una nova línia. + short_name_hint: Nom curt que es farà servir per a l'Aplicació Web Progressiva (PWA). Ha de tenir un màxim de 12 caràcters. title: Nova organització omniauth_settings: decidim: diff --git a/decidim-system/config/locales/ca.yml b/decidim-system/config/locales/ca.yml index 59b11b0dc8263..a3d4c07e8bb42 100644 --- a/decidim-system/config/locales/ca.yml +++ b/decidim-system/config/locales/ca.yml @@ -264,6 +264,7 @@ ca: confirm_resend_invitation: Segur que vols reenviar la invitació? resend_invitation: Reenviar la invitació secondary_hosts_hint: Introdueix cada un d'ells en una nova línia + short_name_hint: Nom curt que es farà servir per a l'Aplicació Web Progressiva (PWA). Ha de tenir un màxim de 12 caràcters. title: Editar l'organització file_upload_settings: content_types: @@ -293,6 +294,7 @@ ca: organization_admin_email_hint: T'enviarem un correu electrònic a aquesta adreça perquè puguis confirmar-la i configurar la teva contrasenya. reference_prefix_hint: El prefix de la referència s'utilitza per identificar de forma única els recursos del conjunt de l'organització. secondary_hosts_hint: Introdueix cada un d'ells en una nova línia. + short_name_hint: Nom curt que es farà servir per a l'Aplicació Web Progressiva (PWA). Ha de tenir un màxim de 12 caràcters. title: Nova organització omniauth_settings: decidim: diff --git a/decidim-system/config/locales/es-MX.yml b/decidim-system/config/locales/es-MX.yml index 6c1f704353185..1c59c5ae971f5 100644 --- a/decidim-system/config/locales/es-MX.yml +++ b/decidim-system/config/locales/es-MX.yml @@ -265,6 +265,7 @@ es-MX: confirm_resend_invitation: '¿Seguro que quieres reenviar la invitación?' resend_invitation: Reenviar invitación secondary_hosts_hint: Introduce cada uno de ellos en una nueva línea + short_name_hint: Nombre corto utilizado para la Aplicación Web Progresiva (PWA). Debe tener un máximo de 12 caracteres. title: Editar la organización file_upload_settings: content_types: @@ -294,6 +295,7 @@ es-MX: organization_admin_email_hint: Te enviaremos un correo electrónico a esta dirección para que la puedas confirmar y configurar tu contraseña. reference_prefix_hint: El prefijo de referencia se utiliza para identificar de forma única los recursos del conjunto de la organización. secondary_hosts_hint: Introduce cada uno de ellos en una nueva línea. + short_name_hint: Nombre corto utilizado para la Aplicación Web Progresiva (PWA). Debe tener un máximo de 12 caracteres. title: Nueva organización omniauth_settings: decidim: diff --git a/decidim-system/config/locales/es-PY.yml b/decidim-system/config/locales/es-PY.yml index 1e6676bccba3a..b6e4cbcfce52b 100644 --- a/decidim-system/config/locales/es-PY.yml +++ b/decidim-system/config/locales/es-PY.yml @@ -265,6 +265,7 @@ es-PY: confirm_resend_invitation: '¿Seguro que quieres reenviar la invitación?' resend_invitation: Reenviar invitación secondary_hosts_hint: Introduce cada uno de ellos en una nueva línea + short_name_hint: Nombre corto utilizado para la Aplicación Web Progresiva (PWA). Debe tener un máximo de 12 caracteres. title: Editar la organización file_upload_settings: content_types: @@ -294,6 +295,7 @@ es-PY: organization_admin_email_hint: Te enviaremos un correo electrónico a esta dirección para que la puedas confirmar y configurar tu contraseña. reference_prefix_hint: El prefijo de referencia se utiliza para identificar de forma única los recursos del conjunto de la organización. secondary_hosts_hint: Introduce cada uno de ellos en una nueva línea. + short_name_hint: Nombre corto utilizado para la Aplicación Web Progresiva (PWA). Debe tener un máximo de 12 caracteres. title: Nueva organización omniauth_settings: decidim: diff --git a/decidim-system/config/locales/es.yml b/decidim-system/config/locales/es.yml index 4a12d6cc4cb1d..396c5198ebfcc 100644 --- a/decidim-system/config/locales/es.yml +++ b/decidim-system/config/locales/es.yml @@ -265,6 +265,7 @@ es: confirm_resend_invitation: '¿Seguro que quieres reenviar la invitación?' resend_invitation: Reenviar invitación secondary_hosts_hint: Introduce cada uno de ellos en una nueva línea + short_name_hint: Nombre corto utilizado para la Aplicación Web Progresiva (PWA). Debe tener un máximo de 12 caracteres. title: Editar la organización file_upload_settings: content_types: @@ -294,6 +295,7 @@ es: organization_admin_email_hint: Te enviaremos un correo electrónico a esta dirección para que la puedas confirmar y configurar tu contraseña. reference_prefix_hint: El prefijo de referencia se utiliza para identificar de forma única los recursos del conjunto de la organización. secondary_hosts_hint: Introduce cada uno de ellos en una nueva línea. + short_name_hint: Nombre corto utilizado para la Aplicación Web Progresiva (PWA). Debe tener un máximo de 12 caracteres. title: Nueva organización omniauth_settings: decidim: diff --git a/decidim-system/config/locales/eu.yml b/decidim-system/config/locales/eu.yml index 7d014fb663578..b2bd2b44cca50 100644 --- a/decidim-system/config/locales/eu.yml +++ b/decidim-system/config/locales/eu.yml @@ -72,7 +72,7 @@ eu: destroy: success: Administratzailea zuzen ezabatua. edit: - title: Editatu administratzailea + title: Administratzailea editatu update: Eguneratu index: title: Administratzaileak @@ -265,6 +265,7 @@ eu: confirm_resend_invitation: Ziur zaude gonbidapena birbidali nahi duzula? resend_invitation: Birbidali gonbidapena secondary_hosts_hint: Sartu haietako bakoitza lerro berri batean + short_name_hint: Web aplikazio progresiborako erabilitako izen laburra. 12 karaktere izan behar ditu gehienez. title: Editatu erakundea file_upload_settings: content_types: @@ -295,6 +296,7 @@ eu: organization_admin_email_hint: Mezu elektroniko bat bidaliko dugu helbide honetara, baieztatu eta pasahitza jar dezazun. reference_prefix_hint: Erreferentzia-aurrizkia erakunde guztietan baliabideak identifikatzeko erabiltzen da. secondary_hosts_hint: Sartu haietako bakoitza beste lerro batean. + short_name_hint: Web aplikazio progresiborako erabilitako izen laburra. 12 karaktere izan behar ditu gehienez. title: Beste erakunde bat omniauth_settings: decidim: diff --git a/decidim-system/config/locales/fi-plain.yml b/decidim-system/config/locales/fi-plain.yml index 9cde4529a5dda..dd27afda4e6b8 100644 --- a/decidim-system/config/locales/fi-plain.yml +++ b/decidim-system/config/locales/fi-plain.yml @@ -264,6 +264,7 @@ fi-pl: confirm_resend_invitation: Haluatko varmasti lähettää kutsun uudestaan? resend_invitation: Lähetä kutsu uudestaan secondary_hosts_hint: Syötä jokainen niistä omalle rivilleen + short_name_hint: Progressiivisen verkkosovelluksen (PWA) lyhyt nimi. Nimessä voi olla enintään 12 merkkiä. title: Muokkaa organisaatiota file_upload_settings: content_types: @@ -293,6 +294,7 @@ fi-pl: organization_admin_email_hint: Lähetämme tähän sähköpostiosoitteeseen viestin vahvistaaksesi kyseisen sähköpostiosoitteen ja asettaaksesi salasanan käyttäjätilillesi. reference_prefix_hint: Viitetunnisteen avulla tunnistetaan yksilöllisesti resursseja eri organisaatioiden välillä. secondary_hosts_hint: Syötä jokainen niistä omalle rivilleen. + short_name_hint: Progressiivisen verkkosovelluksen (PWA) lyhyt nimi. Nimessä voi olla enintään 12 merkkiä. title: Uusi organisaatio omniauth_settings: decidim: diff --git a/decidim-system/config/locales/fi.yml b/decidim-system/config/locales/fi.yml index 84b7ee32353b8..ab9020a5a11ac 100644 --- a/decidim-system/config/locales/fi.yml +++ b/decidim-system/config/locales/fi.yml @@ -264,6 +264,7 @@ fi: confirm_resend_invitation: Haluatko varmasti lähettää kutsun uudestaan? resend_invitation: Lähetä kutsu uudestaan secondary_hosts_hint: Syötä jokainen niistä omalle rivilleen + short_name_hint: Progressiivisen verkkosovelluksen (PWA) lyhyt nimi. Nimessä voi olla enintään 12 merkkiä. title: Muokkaa organisaatiota file_upload_settings: content_types: @@ -293,6 +294,7 @@ fi: organization_admin_email_hint: Lähetämme tähän sähköpostiosoitteeseen viestin vahvistaaksesi kyseisen sähköpostiosoitteen ja asettaaksesi salasanan käyttäjätilillesi. reference_prefix_hint: Viitetunnisteen avulla tunnistetaan yksilöllisesti resursseja eri organisaatioiden välillä. secondary_hosts_hint: Syötä jokainen niistä omalle rivilleen. + short_name_hint: Progressiivisen verkkosovelluksen (PWA) lyhyt nimi. Nimessä voi olla enintään 12 merkkiä. title: Uusi organisaatio omniauth_settings: decidim: diff --git a/decidim-system/config/locales/fr-CA.yml b/decidim-system/config/locales/fr-CA.yml index 944bdf88204bb..c90769c6bedfb 100644 --- a/decidim-system/config/locales/fr-CA.yml +++ b/decidim-system/config/locales/fr-CA.yml @@ -197,6 +197,7 @@ fr-CA: confirm_resend_invitation: Êtes-vous sûr de vouloir renvoyer l'invitation ? resend_invitation: Renvoyer l'invitation secondary_hosts_hint: Entrez chacun d'eux dans une nouvelle ligne + short_name_hint: Nom court utilisé pour l'application Web progressive. Il doit avoir un maximum de 12 caractères. title: Modifier l'organisation file_upload_settings: content_types: @@ -226,6 +227,7 @@ fr-CA: organization_admin_email_hint: Nous enverrons un e-mail à cette adresse afin que vous puissiez la confirmer et configurer votre mot de passe. reference_prefix_hint: Le préfixe de référence est utilisé pour identifier de manière unique les ressources à travers toutes les organisations. secondary_hosts_hint: Entrez chacun d'eux dans une nouvelle ligne. + short_name_hint: Nom court utilisé pour l'application Web progressive. Il doit avoir un maximum de 12 caractères. title: Nouvelle organisation omniauth_settings: decidim: diff --git a/decidim-system/config/locales/fr.yml b/decidim-system/config/locales/fr.yml index 6052268a4813e..c36f6d4ebae02 100644 --- a/decidim-system/config/locales/fr.yml +++ b/decidim-system/config/locales/fr.yml @@ -197,6 +197,7 @@ fr: confirm_resend_invitation: Êtes-vous sûr de vouloir renvoyer l'invitation ? resend_invitation: Renvoyer l'invitation secondary_hosts_hint: Entrez chacun d'eux dans une nouvelle ligne + short_name_hint: Nom court utilisé pour l'application Web progressive. Il doit avoir un maximum de 12 caractères. title: Modifier l'organisation file_upload_settings: content_types: @@ -226,6 +227,7 @@ fr: organization_admin_email_hint: Nous enverrons un e-mail à cette adresse afin que vous puissiez la confirmer et configurer votre mot de passe. reference_prefix_hint: Le préfixe de référence est utilisé pour identifier de manière unique les ressources à travers toutes les organisations. secondary_hosts_hint: Entrez chacun d'eux dans une nouvelle ligne. + short_name_hint: Nom court utilisé pour l'application Web progressive. Il doit avoir un maximum de 12 caractères. title: Nouvelle organisation omniauth_settings: decidim: diff --git a/decidim-system/config/locales/ja.yml b/decidim-system/config/locales/ja.yml index 1cccb86e4a962..f8fa1c695b988 100644 --- a/decidim-system/config/locales/ja.yml +++ b/decidim-system/config/locales/ja.yml @@ -262,6 +262,7 @@ ja: confirm_resend_invitation: 招待を再送信してもよろしいですか? resend_invitation: 招待を再送信する secondary_hosts_hint: 新しい行にそれぞれ入力してください + short_name_hint: Progressive Web アプリケーションに使用される短い名前。最大 12 文字までです。 title: 組織の編集 file_upload_settings: content_types: @@ -291,6 +292,7 @@ ja: organization_admin_email_hint: このアドレスにメールを送信しますので、ご確認の上、パスワードを設定してください。 reference_prefix_hint: 参照プレフィックスは、すべての組織をまたがってリソースを一意に識別するために使用されます。 secondary_hosts_hint: 新しい行にそれぞれ入力します。 + short_name_hint: Progressive Web アプリケーションに使用される短い名前。最大 12 文字までです。 title: 新しい組織 omniauth_settings: decidim: diff --git a/decidim-system/config/locales/sv.yml b/decidim-system/config/locales/sv.yml index c21948340f528..ec3a282ab577a 100644 --- a/decidim-system/config/locales/sv.yml +++ b/decidim-system/config/locales/sv.yml @@ -46,7 +46,9 @@ sv: hide_secret: Dölj hemlighet secret_can_not_be_shown: API:s hemlighet kan bara ses vid skapande eller kopiering show_secret: Visa hemlighet + shown_secret: Hemligheten är visad api_users: + create: Skapa new: Ny API-användare remove: Ta bort användaren confirm_destroy: Vill du radera detta? diff --git a/decidim-verifications/config/locales/eu.yml b/decidim-verifications/config/locales/eu.yml index 404efb33755d2..58ec9ac11f5db 100644 --- a/decidim-verifications/config/locales/eu.yml +++ b/decidim-verifications/config/locales/eu.yml @@ -182,7 +182,7 @@ eu: index: empty: Ez dago errolda-daturik. Erabili %{import_csv} inportatzeko CSV fitxategi bat. no_sign_in: Saioa inoiz ez hasia - no_user: Ez da aurkitu parte-hartzailerik + no_user: Ez da parte-hartzailerik aurkitu last_login: no_sign_in: Inoiz ez sinatua no_user: Parte-hartzailerik gabe From 027bb456886661c89af69b5ff5856b8bed5daf04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Sat, 20 Dec 2025 23:06:04 +0100 Subject: [PATCH 040/116] Refactor attachments tab in budgets' projects and initiatives (#15831) --- .../decidim/budgets/projects_controller.rb | 32 +++---------------- .../decidim/budgets/projects/show.html.erb | 20 ++---------- .../initiatives/initiatives_controller.rb | 32 +++---------------- .../initiatives/initiatives/show.html.erb | 20 ++---------- 4 files changed, 13 insertions(+), 91 deletions(-) diff --git a/decidim-budgets/app/controllers/decidim/budgets/projects_controller.rb b/decidim-budgets/app/controllers/decidim/budgets/projects_controller.rb index 7d25b6cf20497..4c314f1d72c99 100644 --- a/decidim-budgets/app/controllers/decidim/budgets/projects_controller.rb +++ b/decidim-budgets/app/controllers/decidim/budgets/projects_controller.rb @@ -6,10 +6,11 @@ module Budgets class ProjectsController < Decidim::Budgets::ApplicationController include FilterResource include NeedsCurrentOrder + include Decidim::AttachmentsHelper include Decidim::Budgets::Orderable include Decidim::IconHelper - helper_method :projects, :project, :budget, :all_geocoded_projects, :tabs, :panels, :resource_added? + helper_method :projects, :project, :budget, :all_geocoded_projects, :resource_added?, :tab_panel_items before_action :set_focus_mode_if_voting_open @@ -64,16 +65,8 @@ def show_selected_budgets? voting_finished? && budget.projects.selected.any? end - def tabs - @tabs ||= items.map { |item| item.slice(:id, :text, :icon) } - end - - def panels - @panels ||= items.map { |item| item.slice(:id, :method, :args) } - end - - def items - @items ||= [ + def tab_panel_items + @tab_panel_items ||= [ { enabled: ProjectHistoryCell.new(@project).render?, id: "included_history", @@ -82,22 +75,7 @@ def items method: :cell, args: ["decidim/budgets/project_history", @project] }, - { - enabled: @project.photos.present?, - id: "images", - text: t("decidim.application.photos.photos"), - icon: resource_type_icon_key("images"), - method: :cell, - args: ["decidim/images_panel", @project] - }, - { - enabled: @project.documents.present?, - id: "documents", - text: t("decidim.application.documents.documents"), - icon: resource_type_icon_key("documents"), - method: :cell, - args: ["decidim/documents_panel", @project] - } + *attachments_tab_panel_items(@project) ].select { |item| item[:enabled] } end diff --git a/decidim-budgets/app/views/decidim/budgets/projects/show.html.erb b/decidim-budgets/app/views/decidim/budgets/projects/show.html.erb index bd36576b7c8fd..a4393f5a824aa 100644 --- a/decidim-budgets/app/views/decidim/budgets/projects/show.html.erb +++ b/decidim-budgets/app/views/decidim/budgets/projects/show.html.erb @@ -54,24 +54,8 @@ edit_link(
- <% if tabs.any? %> -
-
    - <% tabs.each_with_index do |tab, i| %> -
  • - -
  • - <% end %> -
- - <% panels.each do |panel| %> -
- <%= send(panel[:method], *panel[:args]) %> -
- <% end %> -
+ <% if tab_panel_items.any? %> + <%= cell "decidim/tab_panels", tab_panel_items %> <% end %>
diff --git a/decidim-initiatives/app/controllers/decidim/initiatives/initiatives_controller.rb b/decidim-initiatives/app/controllers/decidim/initiatives/initiatives_controller.rb index 9f99358b9891f..d717d3a0e2fca 100644 --- a/decidim-initiatives/app/controllers/decidim/initiatives/initiatives_controller.rb +++ b/decidim-initiatives/app/controllers/decidim/initiatives/initiatives_controller.rb @@ -22,6 +22,7 @@ class InitiativesController < Decidim::Initiatives::ApplicationController include InitiativeSlug include FilterResource include Paginable + include Decidim::AttachmentsHelper include Decidim::FormFactory include Decidim::Initiatives::Orderable include TypeSelectorOptions @@ -30,7 +31,7 @@ class InitiativesController < Decidim::Initiatives::ApplicationController include SingleInitiativeType include Decidim::IconHelper - helper_method :collection, :initiatives, :pending_initiatives, :filter, :stats, :tabs, :panels + helper_method :collection, :initiatives, :pending_initiatives, :filter, :stats, :tab_panel_items helper_method :initiative_type, :available_initiative_types before_action :authorize_participatory_space, only: [:show] @@ -176,33 +177,8 @@ def stats @stats ||= InitiativeStatsPresenter.new(initiative: current_initiative) end - def tabs - @tabs ||= items.map { |item| item.slice(:id, :text, :icon) } - end - - def panels - @panels ||= items.map { |item| item.slice(:id, :method, :args) } - end - - def items - @items ||= [ - { - enabled: @current_initiative.photos.present?, - id: "images", - text: t("decidim.application.photos.photos"), - icon: resource_type_icon_key("images"), - method: :cell, - args: ["decidim/images_panel", @current_initiative] - }, - { - enabled: @current_initiative.documents.present?, - id: "documents", - text: t("decidim.application.documents.documents"), - icon: resource_type_icon_key("documents"), - method: :cell, - args: ["decidim/documents_panel", @current_initiative] - } - ].select { |item| item[:enabled] } + def tab_panel_items + @tab_panel_items ||= attachments_tab_panel_items(@current_initiative) end end end diff --git a/decidim-initiatives/app/views/decidim/initiatives/initiatives/show.html.erb b/decidim-initiatives/app/views/decidim/initiatives/initiatives/show.html.erb index 9c8c640906786..d49e021851d98 100644 --- a/decidim-initiatives/app/views/decidim/initiatives/initiatives/show.html.erb +++ b/decidim-initiatives/app/views/decidim/initiatives/initiatives/show.html.erb @@ -180,24 +180,8 @@ edit_link(
- <% if tabs.any? %> -
-
    - <% tabs.each_with_index do |tab, i| %> -
  • - -
  • - <% end %> -
- - <% panels.each do |panel| %> -
- <%= send(panel[:method], *panel[:args]) %> -
- <% end %> -
+ <% if tab_panel_items.any? %> + <%= cell "decidim/tab_panels", tab_panel_items %> <% end %>
From 9c82ca2ce23d03a82a1339def1ba93d117a803d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Sat, 20 Dec 2025 23:12:33 +0100 Subject: [PATCH 041/116] Fix notification from Component publication (#15830) --- decidim-core/app/models/decidim/component.rb | 1 + decidim-core/spec/system/notifications_spec.rb | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/decidim-core/app/models/decidim/component.rb b/decidim-core/app/models/decidim/component.rb index eb2b04d203c04..af7604c5acb66 100644 --- a/decidim-core/app/models/decidim/component.rb +++ b/decidim-core/app/models/decidim/component.rb @@ -108,6 +108,7 @@ def can_participate_in_space?(user) participatory_space.can_participate?(user) end + alias can_participate? can_participate_in_space? def private_non_transparent_space? return false unless participatory_space.respond_to?(:private_space?) diff --git a/decidim-core/spec/system/notifications_spec.rb b/decidim-core/spec/system/notifications_spec.rb index b7fba202a04c7..64e480c7b1ac2 100644 --- a/decidim-core/spec/system/notifications_spec.rb +++ b/decidim-core/spec/system/notifications_spec.rb @@ -111,6 +111,23 @@ end end end + + context "when the notification is from a component" do + let!(:notification) do + create(:notification, user:, + resource: resource.component, + event_name: "decidim.events.components.component_published", + event_class: "Decidim::ComponentPublishedEvent") + end + + before do + page.visit decidim.notifications_path + end + + it "shows the notifications" do + expect(page).to have_css(".notification") + end + end end context "when the notification event has an action" do From fa1611b702964e78dbd9539bfbad1d49f57a3453 Mon Sep 17 00:00:00 2001 From: Tom Greenwood <101816158+greenwoodt@users.noreply.github.com> Date: Sat, 20 Dec 2025 23:36:10 +0100 Subject: [PATCH 042/116] Do not show "more information" modal when there isn't any (#15821) --- .../order_progress_summary/_content.html.erb | 12 +++++---- .../spec/system/admin_manages_budgets_spec.rb | 26 +++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/decidim-budgets/app/views/decidim/budgets/projects/order_progress_summary/_content.html.erb b/decidim-budgets/app/views/decidim/budgets/projects/order_progress_summary/_content.html.erb index d4257b75eebb7..cba6776611e75 100644 --- a/decidim-budgets/app/views/decidim/budgets/projects/order_progress_summary/_content.html.erb +++ b/decidim-budgets/app/views/decidim/budgets/projects/order_progress_summary/_content.html.erb @@ -2,11 +2,13 @@ <%= render partial: "decidim/budgets/projects/order_progress_summary/progress_box" %> <%= render partial: "decidim/budgets/projects/order_progress_summary/progress_box_buttons" %> -
- -
+ <% if cell("decidim/budgets/budget_information_modal", budget).more_information.present? %> +
+ +
+ <% end %>
diff --git a/decidim-budgets/spec/system/admin_manages_budgets_spec.rb b/decidim-budgets/spec/system/admin_manages_budgets_spec.rb index ffb8c68bbea44..12a280ca45d14 100644 --- a/decidim-budgets/spec/system/admin_manages_budgets_spec.rb +++ b/decidim-budgets/spec/system/admin_manages_budgets_spec.rb @@ -175,4 +175,30 @@ it_behaves_like "manage soft deletable resource", "budget" it_behaves_like "manage trashed resource", "budget" end + + describe "more information button" do + context "when budget has more_information content" do + let!(:budget_with_info) { create(:budget, :with_projects, component: current_component) } + + before do + current_component.update!(settings: { more_information_modal: { en: "Additional budget information" } }) + end + + it "displays the more information button" do + visit Decidim::EngineRouter.main_proxy(current_component).budget_projects_path(budget_with_info) + + expect(page).to have_button("More information") + end + end + + context "when budget has no more_information content" do + let!(:budget_without_info) { create(:budget, :with_projects, component: current_component) } + + it "does not display the more information button" do + visit Decidim::EngineRouter.main_proxy(current_component).budget_projects_path(budget_without_info) + + expect(page).to have_no_button("More information") + end + end + end end From 5b78ec5190dd6b4b8d612abaecbcebc8fd31c86f Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Mon, 22 Dec 2025 11:55:47 +0200 Subject: [PATCH 043/116] Fix factories location pages module (#15832) --- decidim-pages/lib/decidim/pages/test/factories.rb | 14 ++++++++++++++ decidim-pages/spec/factories.rb | 14 +------------- 2 files changed, 15 insertions(+), 13 deletions(-) create mode 100644 decidim-pages/lib/decidim/pages/test/factories.rb diff --git a/decidim-pages/lib/decidim/pages/test/factories.rb b/decidim-pages/lib/decidim/pages/test/factories.rb new file mode 100644 index 0000000000000..1a185f196b7fe --- /dev/null +++ b/decidim-pages/lib/decidim/pages/test/factories.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :page_component, parent: :component do + name { Decidim::Components::Namer.new(participatory_space.organization.available_locales, :pages).i18n_name } + manifest_name { :pages } + participatory_space { create(:participatory_process, :with_steps, organization:) } + end + + factory :page, class: "Decidim::Pages::Page" do + body { Decidim::Faker::Localized.wrapped("

", "

") { generate_localized_title } } + component { build(:component, manifest_name: "pages") } + end +end diff --git a/decidim-pages/spec/factories.rb b/decidim-pages/spec/factories.rb index 4ae29a793210c..c9297c89d4afa 100644 --- a/decidim-pages/spec/factories.rb +++ b/decidim-pages/spec/factories.rb @@ -3,16 +3,4 @@ require "decidim/components/namer" require "decidim/core/test/factories" require "decidim/participatory_processes/test/factories" - -FactoryBot.define do - factory :page_component, parent: :component do - name { Decidim::Components::Namer.new(participatory_space.organization.available_locales, :pages).i18n_name } - manifest_name { :pages } - participatory_space { create(:participatory_process, :with_steps, organization:) } - end - - factory :page, class: "Decidim::Pages::Page" do - body { Decidim::Faker::Localized.wrapped("

", "

") { generate_localized_title } } - component { build(:component, manifest_name: "pages") } - end -end +require "decidim/pages/test/factories" From d83c8624d8b42144f92164d419b5d7541b644b27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Mon, 22 Dec 2025 15:57:36 +0100 Subject: [PATCH 044/116] Fix breadcrumb in conferences' program (#15833) --- .../conference_program_controller.rb | 13 ++++++ .../system/conferences_breadcrumbs_spec.rb | 42 +++++++++++++++++++ .../decidim/meetings/meetings_controller.rb | 32 +++++++++++++- 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/decidim-conferences/app/controllers/decidim/conferences/conference_program_controller.rb b/decidim-conferences/app/controllers/decidim/conferences/conference_program_controller.rb index 4e5c24ddd1efa..a97099fcb3c01 100644 --- a/decidim-conferences/app/controllers/decidim/conferences/conference_program_controller.rb +++ b/decidim-conferences/app/controllers/decidim/conferences/conference_program_controller.rb @@ -10,6 +10,8 @@ class ConferenceProgramController < Decidim::Conferences::ApplicationController helper_method :collection, :conference, :meeting_days, :meeting_component + before_action :set_program_breadcrumb_item + def show raise ActionController::RoutingError, "No meetings for this conference " if meetings.blank? @@ -47,6 +49,17 @@ def current_participatory_space def conference current_participatory_space end + + def set_program_breadcrumb_item + return unless meeting_component + + context_breadcrumb_items << { + label: t("conference_program.index.title", scope: "decidim"), + url: decidim_conferences.conference_conference_program_path(current_participatory_space, meeting_component, locale: I18n.locale), + active: true, + resource: meeting_component + } + end end end end diff --git a/decidim-conferences/spec/system/conferences_breadcrumbs_spec.rb b/decidim-conferences/spec/system/conferences_breadcrumbs_spec.rb index 22e97ae41826d..1db071a774907 100644 --- a/decidim-conferences/spec/system/conferences_breadcrumbs_spec.rb +++ b/decidim-conferences/spec/system/conferences_breadcrumbs_spec.rb @@ -31,4 +31,46 @@ expect(page).to have_content(translated(component.name)) end end + + describe "with a program" do + let(:meetings_component) { create(:meeting_component, :published, participatory_space:) } + let!(:meeting) { create(:meeting, :published, latitude:, longitude:, component: meetings_component, start_time: 1.day.from_now) } + + let(:latitude) { 40.7504928941818 } + let(:longitude) { -73.993466492276 } + let(:geocoder_request_url) { "https://nominatim.openstreetmap.org/reverse?accept-language=en&addressdetails=1&format=json&lat=#{latitude}&lon=#{longitude}" } + let(:geocoder_response) { File.read(Decidim::Dev.asset("geocoder_result_osm.json")) } + + before do + stub_request(:get, geocoder_request_url).with( + headers: { + "Accept" => "*/*", + "Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3", + "User-Agent" => "Ruby" + } + ).to_return(body: geocoder_response) + end + + scenario "shows breadcrumb with conference and program" do + visit decidim_conferences.conference_conference_program_path(participatory_space, meetings_component, locale: I18n.locale) + + within ".menu-bar" do + expect(page).to have_content("Conferences") + expect(page).to have_content(translated(participatory_space.title)) + expect(page).to have_content("Program") + end + end + + scenario "shows breadcrumb with conference, program, and meeting" do + visit decidim_conferences.conference_conference_program_path(participatory_space, meetings_component, locale: I18n.locale) + click_on decidim_sanitize_translated(meeting.title) + + within ".menu-bar" do + expect(page).to have_content("Conferences") + expect(page).to have_content(translated(participatory_space.title)) + expect(page).to have_content("Program") + expect(page).to have_content(translated_attribute(meeting.title)) + end + end + end end diff --git a/decidim-meetings/app/controllers/decidim/meetings/meetings_controller.rb b/decidim-meetings/app/controllers/decidim/meetings/meetings_controller.rb index e11828f8bec04..8898ae2e44874 100644 --- a/decidim-meetings/app/controllers/decidim/meetings/meetings_controller.rb +++ b/decidim-meetings/app/controllers/decidim/meetings/meetings_controller.rb @@ -202,14 +202,44 @@ def previous_space nil end + def conference_context? + return false unless Decidim.module_installed?(:conferences) + return false if current_participatory_space.blank? + + current_participatory_space.is_a?(Decidim::Conference) + end + + # Override the add_current_component method when in conference context + # to avoid showing "Meetings" breadcrumb and show "Program" instead + def add_current_component + return {} if conference_context? + + super + end + def add_breadcrumb_item return {} if meeting.blank? - { + breadcrumb = { label: translated_attribute(meeting.title), url: Decidim::EngineRouter.main_proxy(current_component).meeting_path(meeting, locale: current_locale), active: false } + + # If this meeting is being accessed from within a conference program context, + # add program breadcrumb to maintain proper navigation hierarchy + if conference_context? + program_path = decidim_conferences.conference_conference_program_path(current_participatory_space, current_component, locale: I18n.locale) + + context_breadcrumb_items << { + label: t("conference_program.index.title", scope: "decidim"), + url: program_path, + active: false, + resource: current_component + } + end + + breadcrumb end end end From 1b46fd6cdae375e05d7d2831c3dd31b4755c70de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Mon, 22 Dec 2025 17:23:07 +0100 Subject: [PATCH 045/116] Remove identity_selector_dialog.js file (#15842) --- .../src/decidim/identity_selector_dialog.js | 33 ------------------- 1 file changed, 33 deletions(-) delete mode 100644 decidim-core/app/packs/src/decidim/identity_selector_dialog.js diff --git a/decidim-core/app/packs/src/decidim/identity_selector_dialog.js b/decidim-core/app/packs/src/decidim/identity_selector_dialog.js deleted file mode 100644 index e0c35291de937..0000000000000 --- a/decidim-core/app/packs/src/decidim/identity_selector_dialog.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Send a request to select which identity want to use - * NOTE: this should not be done using javascript - * - * @param {HTMLElement} node target node - * @returns {void} - */ -export default function(node = document) { - node.addEventListener("click", ({ target: element }) => { - const { method } = element.dataset - - let attr = "destroy_url"; - - if (method === "POST") { - attr = "create_url"; - } - - const { [attr]: url } = element.dataset - Rails.ajax({ - url: url, - type: method, - success: function() { - if (method === "POST") { - element.classList.add("is-selected") - element.dataset.method = "DELETE" - } else { - element.classList.remove("is-selected") - element.dataset.method = "POST" - } - } - }) - }) -} From b08bdc87104746a24e44a7e08c6bcfe152d5d670 Mon Sep 17 00:00:00 2001 From: Tom Greenwood <101816158+greenwoodt@users.noreply.github.com> Date: Wed, 24 Dec 2025 16:30:46 +0000 Subject: [PATCH 046/116] Fix proposal import with other proposals states in budgets (#15630) Co-authored-by: Alexandru Emil Lupu --- .../admin/import_proposals_to_budgets.rb | 12 ++++++++- .../admin/project_import_proposals_form.rb | 14 ++++++++-- .../admin/proposals_imports/new.html.erb | 14 +++++++--- decidim-budgets/config/locales/en.yml | 3 ++- .../admin/import_proposals_to_budgets_spec.rb | 27 +++++++++++++++++-- .../project_import_proposals_form_spec.rb | 10 +++---- .../import_proposals_to_projects_examples.rb | 2 +- 7 files changed, 67 insertions(+), 15 deletions(-) diff --git a/decidim-budgets/app/commands/decidim/budgets/admin/import_proposals_to_budgets.rb b/decidim-budgets/app/commands/decidim/budgets/admin/import_proposals_to_budgets.rb index 52d4a7450690c..ebeee8d734727 100644 --- a/decidim-budgets/app/commands/decidim/budgets/admin/import_proposals_to_budgets.rb +++ b/decidim-budgets/app/commands/decidim/budgets/admin/import_proposals_to_budgets.rb @@ -68,7 +68,17 @@ def budget_for(original_proposal) end def proposals - Decidim::Proposals::Proposal.where(component: origin_component).published.not_hidden.not_withdrawn.accepted.order(:published_at) + proposals = Decidim::Proposals::Proposal.where(component: origin_component).published.not_hidden.not_withdrawn + + if form.internal_states.present? + if form.internal_states.include?("not_answered") + proposals.not_answered.or(proposals.where(id: proposals.only_status(form.internal_states).pluck(:id))) + else + proposals.only_status(form.internal_states) + end + else + proposals + end.order(:published_at) end def origin_component diff --git a/decidim-budgets/app/forms/decidim/budgets/admin/project_import_proposals_form.rb b/decidim-budgets/app/forms/decidim/budgets/admin/project_import_proposals_form.rb index fe4f1ed29b9d2..f5216b97d2627 100644 --- a/decidim-budgets/app/forms/decidim/budgets/admin/project_import_proposals_form.rb +++ b/decidim-budgets/app/forms/decidim/budgets/admin/project_import_proposals_form.rb @@ -10,10 +10,9 @@ class ProjectImportProposalsForm < Decidim::Form attribute :origin_component_id, Integer attribute :default_budget, Integer - attribute :import_all_accepted_proposals, Boolean + attribute :internal_states, Array[String] validates :origin_component_id, :origin_component, :current_component, presence: true - validates :import_all_accepted_proposals, allow_nil: false, acceptance: true validates :default_budget, presence: true, numericality: { greater_than: 0 } def origin_component @@ -33,6 +32,17 @@ def origin_components_collection def budget @budget ||= context[:budget] end + + def available_states(component_id = nil) + scope = Decidim::Proposals::ProposalState + scope = scope.where(component: Decidim::Component.find(component_id)) if component_id.present? + + states = scope.pluck(:token).uniq.map do |token| + OpenStruct.new(token: token, title: token.humanize) + end + + states + [OpenStruct.new(token: "not_answered", title: I18n.t("decidim.proposals.answers.not_answered"))] + end end end end diff --git a/decidim-budgets/app/views/decidim/budgets/admin/proposals_imports/new.html.erb b/decidim-budgets/app/views/decidim/budgets/admin/proposals_imports/new.html.erb index 9f772c6666f0d..907b43f81de50 100644 --- a/decidim-budgets/app/views/decidim/budgets/admin/proposals_imports/new.html.erb +++ b/decidim-budgets/app/views/decidim/budgets/admin/proposals_imports/new.html.erb @@ -20,9 +20,17 @@
<%= f.number_field :default_budget, label: t(".default_budget") %>
-
- <%= f.check_box :import_all_accepted_proposals, label: t(".import_all_accepted_proposals") %> -
+ <% if @form.available_states.any? %> + + <%= t(".proposal_states_help") %> +
+ <%= f.collection_check_boxes :internal_states, @form.available_states, :token, ->(a) { translated_attribute(a.title) } do |builder| %> +
+ <%= builder.label { builder.check_box + builder.text } %> +
+ <% end %> +
+ <% end %>
diff --git a/decidim-budgets/config/locales/en.yml b/decidim-budgets/config/locales/en.yml index 5f8ca1d601fe7..d4421a1bf919f 100644 --- a/decidim-budgets/config/locales/en.yml +++ b/decidim-budgets/config/locales/en.yml @@ -143,9 +143,10 @@ en: new: create: Import proposals to projects default_budget: Default budget - import_all_accepted_proposals: Import all accepted proposals no_components: There are no other proposal components in this participatory space to import the proposals into projects. origin_component_id: Origin component + proposal_states: Proposal States + proposal_states_help: Select one or more proposal states to import into projects. By selecting none, you will import all published proposals that have not already been imported in other budgets from this component. select_component: Please select a component title: Import proposals to projects reminders: diff --git a/decidim-budgets/spec/commands/decidim/budgets/admin/import_proposals_to_budgets_spec.rb b/decidim-budgets/spec/commands/decidim/budgets/admin/import_proposals_to_budgets_spec.rb index 082187f80b6c2..dff91c0a0bab9 100644 --- a/decidim-budgets/spec/commands/decidim/budgets/admin/import_proposals_to_budgets_spec.rb +++ b/decidim-budgets/spec/commands/decidim/budgets/admin/import_proposals_to_budgets_spec.rb @@ -29,14 +29,14 @@ module Admin current_component:, current_user:, default_budget:, - import_all_accepted_proposals:, + internal_states:, budget:, valid?: valid ) end let(:default_budget) { 1000 } - let(:import_all_accepted_proposals) { true } + let(:internal_states) { ["accepted"] } let(:command) { described_class.new(form) } @@ -65,6 +65,29 @@ module Admin expect { command.call }.to change { Project.where(budget:).count }.by(3) end + context "when importing multiple states" do + let!(:rejected_proposals) { create_list(:proposal, 2, :rejected, component: proposals_component) } + let(:internal_states) { %w(accepted rejected) } + + it "imports proposals from all selected states" do + expect { command.call }.to change { Project.where(budget:).count }.by(5) + end + end + + context "when importing custom states" do + let!(:custom_state) { create(:proposal_state, token: "custom_state", component: proposals_component) } + let!(:custom_state_proposals) do + create_list(:proposal, 2, :published, component: proposals_component).each do |proposal| + proposal.update!(proposal_state: custom_state) + end + end + let(:internal_states) { ["custom_state"] } + + it "imports proposals with custom states" do + expect { command.call }.to change { Project.where(budget:).count }.by(2) + end + end + context "when a proposal was already imported" do let(:second_proposal) { create(:proposal, :accepted, component: proposal.component) } diff --git a/decidim-budgets/spec/forms/decidim/budgets/admin/project_import_proposals_form_spec.rb b/decidim-budgets/spec/forms/decidim/budgets/admin/project_import_proposals_form_spec.rb index 1c67d6c040758..5864d702866e3 100644 --- a/decidim-budgets/spec/forms/decidim/budgets/admin/project_import_proposals_form_spec.rb +++ b/decidim-budgets/spec/forms/decidim/budgets/admin/project_import_proposals_form_spec.rb @@ -12,12 +12,12 @@ module Admin let(:component) { project.component } let(:origin_component) { create(:proposal_component, participatory_space: component.participatory_space) } let(:default_budget) { 1000 } - let(:import_all_accepted_proposals) { true } + let(:internal_states) { %w(accepted rejected) } let(:params) do { origin_component_id: origin_component.try(:id), default_budget:, - import_all_accepted_proposals: + internal_states: } end @@ -44,10 +44,10 @@ module Admin it { is_expected.to be_invalid } end - context "when the import proposals is not accepted" do - let(:import_all_accepted_proposals) { false } + context "when no states are selected" do + let(:internal_states) { [] } - it { is_expected.to be_invalid } + it { is_expected.to be_valid } end describe "origin_component" do diff --git a/decidim-budgets/spec/shared/import_proposals_to_projects_examples.rb b/decidim-budgets/spec/shared/import_proposals_to_projects_examples.rb index 7c179f8c35530..0a4a6404549cc 100644 --- a/decidim-budgets/spec/shared/import_proposals_to_projects_examples.rb +++ b/decidim-budgets/spec/shared/import_proposals_to_projects_examples.rb @@ -14,7 +14,7 @@ within ".import_proposals" do select origin_component.name["en"], from: :proposals_import_origin_component_id fill_in "Default budget", with: default_budget - check :proposals_import_import_all_accepted_proposals + check "Accepted" || "Rejected" end click_on "Import proposals to projects" From 8e6e76a72f5b1868cc0062ce355e22ae8b99a68b Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Wed, 7 Jan 2026 11:58:17 +0200 Subject: [PATCH 047/116] Fix HashSyntax rubocop offence in `project_import_proposals_form.rb` (#15848) --- .../decidim/budgets/admin/project_import_proposals_form.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/decidim-budgets/app/forms/decidim/budgets/admin/project_import_proposals_form.rb b/decidim-budgets/app/forms/decidim/budgets/admin/project_import_proposals_form.rb index f5216b97d2627..2a5efb7f2a258 100644 --- a/decidim-budgets/app/forms/decidim/budgets/admin/project_import_proposals_form.rb +++ b/decidim-budgets/app/forms/decidim/budgets/admin/project_import_proposals_form.rb @@ -38,7 +38,7 @@ def available_states(component_id = nil) scope = scope.where(component: Decidim::Component.find(component_id)) if component_id.present? states = scope.pluck(:token).uniq.map do |token| - OpenStruct.new(token: token, title: token.humanize) + OpenStruct.new(token:, title: token.humanize) end states + [OpenStruct.new(token: "not_answered", title: I18n.t("decidim.proposals.answers.not_answered"))] From 52f4352fcbbad2feed102ed87ba91469b1ae2c16 Mon Sep 17 00:00:00 2001 From: Anna Topalidi <60363870+antopalidi@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:12:31 +0100 Subject: [PATCH 048/116] Fix auto-redirect when question voting closes (#15760) --- .../per_question_votes_controller.rb | 21 +++++++++- .../packs/entrypoints/decidim_elections.js | 1 + .../elections/question_status_checker.js | 42 +++++++++++++++++++ .../per_question_votes/show.html.erb | 2 + .../test/per_question_vote_examples.rb | 33 +++++++++++++++ .../per_question_votes_controller_spec.rb | 41 ++++++++++++++++++ ..._votes_in_an_per_question_election_spec.rb | 9 ++++ 7 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 decidim-elections/app/packs/src/decidim/elections/question_status_checker.js diff --git a/decidim-elections/app/controllers/decidim/elections/per_question_votes_controller.rb b/decidim-elections/app/controllers/decidim/elections/per_question_votes_controller.rb index ebd2bb2d7681f..485fecadcce39 100644 --- a/decidim-elections/app/controllers/decidim/elections/per_question_votes_controller.rb +++ b/decidim-elections/app/controllers/decidim/elections/per_question_votes_controller.rb @@ -16,12 +16,20 @@ class PerQuestionVotesController < Decidim::Elections::ApplicationController end before_action only: [:show, :update] do - redirect_to(**next_vote_step_action) unless question.voting_enabled? + redirect_to(**next_vote_step_action) unless question.voting_enabled? || request.format.json? end # Show the voting form for the given question + # Responds to HTML (render the form) and JSON (check question status for polling) def show enforce_permission_to(:create, :vote, election:) + + respond_to do |format| + format.html + format.json do + render json: question_status_response + end + end end # Saves the vote for the current question and redirect to the next question @@ -86,6 +94,17 @@ def next_vote_step_action { action: :show, id: next_question } end + # Returns JSON response with question voting status for client-side polling. + # Used to redirect users when a question's voting is closed while they are on the voting page. + def question_status_response + if question.voting_enabled? + { voting_enabled: true, redirect_url: nil } + else + redirect_action = next_vote_step_action + { voting_enabled: false, redirect_url: url_for(**redirect_action) } + end + end + def requeue_following_questions election.questions .where("position > ?", question.position) diff --git a/decidim-elections/app/packs/entrypoints/decidim_elections.js b/decidim-elections/app/packs/entrypoints/decidim_elections.js index bd596b1dff83e..7238e2af9520d 100644 --- a/decidim-elections/app/packs/entrypoints/decidim_elections.js +++ b/decidim-elections/app/packs/entrypoints/decidim_elections.js @@ -2,6 +2,7 @@ import "src/decidim/elections/elections.js" import "src/decidim/elections/waiting_room.js" import "src/decidim/elections/live_results_update.js"; import "src/decidim/elections/census_check_visibility.js"; +import "src/decidim/elections/question_status_checker.js"; // Images require.context("../images", true) diff --git a/decidim-elections/app/packs/src/decidim/elections/question_status_checker.js b/decidim-elections/app/packs/src/decidim/elections/question_status_checker.js new file mode 100644 index 0000000000000..0622211a6c764 --- /dev/null +++ b/decidim-elections/app/packs/src/decidim/elections/question_status_checker.js @@ -0,0 +1,42 @@ +/** + * Polls the server to check if the current question is still available for voting. + * When voting is closed, redirects user to the waiting room or next question. + */ +document.addEventListener("DOMContentLoaded", () => { + const votingBooth = document.querySelector("[data-question-status-url]"); + if (!votingBooth) { + return; + } + + const statusUrl = votingBooth.dataset.questionStatusUrl; + if (!statusUrl) { + return; + } + + const checkStatus = async () => { + try { + const response = await fetch(statusUrl, { + method: "GET", + headers: { + "Accept": "application/json", + "Content-Type": "application/json", + "X-Requested-With": "XMLHttpRequest" + } + }); + + if (response.ok) { + const result = await response.json(); + if (!result.voting_enabled && result.redirect_url) { + window.location.href = result.redirect_url; + return; + } + } + } catch (error) { + console.error("[QuestionStatusChecker] Error:", error); + } + + setTimeout(checkStatus, 1000); + }; + + setTimeout(checkStatus, 1000); +}); diff --git a/decidim-elections/app/views/decidim/elections/per_question_votes/show.html.erb b/decidim-elections/app/views/decidim/elections/per_question_votes/show.html.erb index 0d7078b270212..d9162686b30c4 100644 --- a/decidim-elections/app/views/decidim/elections/per_question_votes/show.html.erb +++ b/decidim-elections/app/views/decidim/elections/per_question_votes/show.html.erb @@ -1,3 +1,4 @@ +
<%= form_with url: url_for(action: :show, id: question), method: :patch, local: true do %> <%= question_title(question, :h1, class: "vote_booth-question_title") %>
@@ -28,3 +29,4 @@
<% end %> +
diff --git a/decidim-elections/lib/decidim/elections/test/per_question_vote_examples.rb b/decidim-elections/lib/decidim/elections/test/per_question_vote_examples.rb index 02c98244eb80d..2f789fb9d98cc 100644 --- a/decidim-elections/lib/decidim/elections/test/per_question_vote_examples.rb +++ b/decidim-elections/lib/decidim/elections/test/per_question_vote_examples.rb @@ -172,6 +172,39 @@ end end +shared_examples "a per question votable election with automatic redirect when question closes" do + it "redirects to the waiting room when admin closes voting while user is on question page" do + expect(page).to have_content(translated_attribute(election.title)) + click_on "Vote" + expect(page).to have_current_path(election_vote_path(question1)) + expect(page).to have_content(strip_tags(translated_attribute(question1.description))) + + # Simulate admin closing voting for question1 (publishing results) + question1.update!(published_results_at: Time.current) + + # Wait for JavaScript polling to detect the change and redirect + expect(page).to have_current_path(waiting_election_votes_path, wait: 3) + expect(page).to have_content("Waiting for the next question") + end + + it "redirects to next question when admin closes current question and next is available" do + # Enable question2 from the start + question2.update!(voting_enabled_at: Time.current) + + expect(page).to have_content(translated_attribute(election.title)) + click_on "Vote" + expect(page).to have_current_path(election_vote_path(question1)) + expect(page).to have_content(strip_tags(translated_attribute(question1.description))) + + # Simulate admin closing voting for question1 + question1.update!(published_results_at: Time.current) + + # Wait for JavaScript polling to detect the change and redirect + expect(page).to have_current_path(election_vote_path(question2), wait: 3) + expect(page).to have_content(strip_tags(translated_attribute(question2.description))) + end +end + shared_examples "a per question votable election with edit from receipt" do it "allows editing votes from receipt page" do click_on "Vote" diff --git a/decidim-elections/spec/controllers/decidim/elections/per_question_votes_controller_spec.rb b/decidim-elections/spec/controllers/decidim/elections/per_question_votes_controller_spec.rb index 4ff53c96512f8..2742beff135c9 100644 --- a/decidim-elections/spec/controllers/decidim/elections/per_question_votes_controller_spec.rb +++ b/decidim-elections/spec/controllers/decidim/elections/per_question_votes_controller_spec.rb @@ -68,6 +68,47 @@ module Elections get(:show, params:) expect(response).to have_http_status(:ok) end + + context "when json format is requested" do + it "returns voting_enabled true when question is enabled" do + get :show, params: params.merge(id: question.id), format: :json + expect(response).to have_http_status(:ok) + json_response = JSON.parse(response.body) + expect(json_response["voting_enabled"]).to be(true) + expect(json_response["redirect_url"]).to be_nil + end + + it "returns voting_enabled false with redirect_url when question voting is disabled" do + question.update(voting_enabled_at: nil) + allow(controller).to receive(:url_for).with(action: :show, id: second_question).and_return(second_election_vote_path) + get :show, params: params.merge(id: question.id), format: :json + expect(response).to have_http_status(:ok) + json_response = JSON.parse(response.body) + expect(json_response["voting_enabled"]).to be(false) + expect(json_response["redirect_url"]).to eq(second_election_vote_path) + end + + it "returns redirect_url to next question when current question has published results" do + question.update(published_results_at: Time.current) + allow(controller).to receive(:url_for).with(action: :show, id: second_question).and_return(second_election_vote_path) + get :show, params: params.merge(id: question.id), format: :json + expect(response).to have_http_status(:ok) + json_response = JSON.parse(response.body) + expect(json_response["voting_enabled"]).to be(false) + expect(json_response["redirect_url"]).to eq(second_election_vote_path) + end + + it "returns redirect_url to waiting room when no enabled questions available" do + question.update(published_results_at: Time.current) + second_question.update(voting_enabled_at: nil) + allow(controller).to receive(:url_for).with(action: :waiting).and_return(waiting_election_votes_path) + get :show, params: params.merge(id: question.id), format: :json + expect(response).to have_http_status(:ok) + json_response = JSON.parse(response.body) + expect(json_response["voting_enabled"]).to be(false) + expect(json_response["redirect_url"]).to eq(waiting_election_votes_path) + end + end end end diff --git a/decidim-elections/spec/system/user_votes_in_an_per_question_election_spec.rb b/decidim-elections/spec/system/user_votes_in_an_per_question_election_spec.rb index 3b88aed74897a..75710068b5ad1 100644 --- a/decidim-elections/spec/system/user_votes_in_an_per_question_election_spec.rb +++ b/decidim-elections/spec/system/user_votes_in_an_per_question_election_spec.rb @@ -103,4 +103,13 @@ def election_vote_path(question) expect(page).to have_current_path(new_election_normal_vote_path) end end + + context "when admin closes question voting while user is viewing the question" do + before do + login_as user, scope: :user + visit election_path + end + + it_behaves_like "a per question votable election with automatic redirect when question closes" + end end From 4d9ab0a716a24039bc80558a96d01ddb94127ee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rueda=20L=C3=B3pez=20=28ARU=29?= Date: Wed, 7 Jan 2026 16:40:44 +0100 Subject: [PATCH 049/116] Fix accountability results filtering (#15564) --- .../models/decidim/accountability/result.rb | 4 +--- .../spec/requests/result_search_spec.rb | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/decidim-accountability/app/models/decidim/accountability/result.rb b/decidim-accountability/app/models/decidim/accountability/result.rb index 5a70742441e0a..b35fc43bcc30f 100644 --- a/decidim-accountability/app/models/decidim/accountability/result.rb +++ b/decidim-accountability/app/models/decidim/accountability/result.rb @@ -110,9 +110,7 @@ def self.ransackable_attributes(auth_object = nil) base + %w(created_at progress) end - def self.ransackable_associations(auth_object = nil) - return [] unless auth_object&.admin? - + def self.ransackable_associations(_auth_object = nil) %w(taxonomies status) end diff --git a/decidim-accountability/spec/requests/result_search_spec.rb b/decidim-accountability/spec/requests/result_search_spec.rb index 7a2b8480245c4..efb508835efd6 100644 --- a/decidim-accountability/spec/requests/result_search_spec.rb +++ b/decidim-accountability/spec/requests/result_search_spec.rb @@ -113,5 +113,23 @@ expect(subject).to contain_exactly(result1, result2) end end + + context "when filtering by taxonomy" do + context "when filtering by taxonomy1" do + let(:filter_params) { { taxonomies_part_of_contains: taxonomy1.id } } + + it "returns results with that taxonomy directly assigned" do + expect(subject).to contain_exactly(result1) + end + end + + context "when filtering by taxonomy2" do + let(:filter_params) { { taxonomies_part_of_contains: taxonomy2.id } } + + it "returns results with that taxonomy and results with its child taxonomies" do + expect(subject).to contain_exactly(result2, result4) + end + end + end end end From 535ed01982dc662c960900be9ad098377b131c91 Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Thu, 8 Jan 2026 15:59:03 +0200 Subject: [PATCH 050/116] Write API: close debates (#15800) * Add CloseDebate mutation with input attributes, types and specs Co-authored-by: alecslupu <105683+alecslupu@users.noreply.github.com> * Add comprehensive documentation and fix conclusions handling in mutation Co-authored-by: alecslupu <105683+alecslupu@users.noreply.github.com> * Address code review feedback: extract locale logic and improve documentation Co-authored-by: alecslupu <105683+alecslupu@users.noreply.github.com> * Add Quick Start guide and update debates README with API documentation Co-authored-by: alecslupu <105683+alecslupu@users.noreply.github.com> * Add comprehensive implementation summary document Co-authored-by: alecslupu <105683+alecslupu@users.noreply.github.com> * Fix specs for debates * Fix spelling * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: alecslupu <105683+alecslupu@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../commands/decidim/debates/close_debate.rb | 2 + .../api/mutations/close_debate_attributes.rb | 12 ++ .../api/mutations/close_debate_type.rb | 47 +++++++ .../api/mutations/debate_mutation_type.rb | 14 ++ .../api/mutations/debates_mutation_type.rb | 23 ++++ decidim-debates/lib/decidim/debates/api.rb | 4 + decidim-debates/lib/decidim/debates/engine.rb | 6 + decidim-debates/spec/factories.rb | 2 +- .../spec/types/debate_mutation_type_spec.rb | 129 ++++++++++++++++++ .../reference/components/debates/close.adoc | 47 ++++++- 10 files changed, 284 insertions(+), 2 deletions(-) create mode 100644 decidim-debates/lib/decidim/api/mutations/close_debate_attributes.rb create mode 100644 decidim-debates/lib/decidim/api/mutations/close_debate_type.rb create mode 100644 decidim-debates/lib/decidim/api/mutations/debate_mutation_type.rb create mode 100644 decidim-debates/lib/decidim/api/mutations/debates_mutation_type.rb create mode 100644 decidim-debates/spec/types/debate_mutation_type_spec.rb diff --git a/decidim-debates/app/commands/decidim/debates/close_debate.rb b/decidim-debates/app/commands/decidim/debates/close_debate.rb index 297d1fdff54f7..a9323aec7b049 100644 --- a/decidim-debates/app/commands/decidim/debates/close_debate.rb +++ b/decidim-debates/app/commands/decidim/debates/close_debate.rb @@ -28,6 +28,8 @@ def call attr_reader :form + delegate :debate, to: :form + def close_debate @debate = Decidim.traceability.perform_action!( :close, diff --git a/decidim-debates/lib/decidim/api/mutations/close_debate_attributes.rb b/decidim-debates/lib/decidim/api/mutations/close_debate_attributes.rb new file mode 100644 index 0000000000000..d40707c7729d3 --- /dev/null +++ b/decidim-debates/lib/decidim/api/mutations/close_debate_attributes.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Decidim + module Debates + class CloseDebateAttributes < Decidim::Api::Types::BaseInputObject + graphql_name "CloseDebateAttributes" + description "Attributes for closing a debate" + + argument :conclusions, GraphQL::Types::String, description: "The conclusions for closing the debate", required: true + end + end +end diff --git a/decidim-debates/lib/decidim/api/mutations/close_debate_type.rb b/decidim-debates/lib/decidim/api/mutations/close_debate_type.rb new file mode 100644 index 0000000000000..c8d07d176e914 --- /dev/null +++ b/decidim-debates/lib/decidim/api/mutations/close_debate_type.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Decidim + module Debates + class CloseDebateType < Decidim::Api::Types::BaseMutation + graphql_name "CloseDebate" + + description "Closes a debate" + type Decidim::Debates::DebateType + + argument :attributes, CloseDebateAttributes, description: "Input attributes for closing a debate", required: true + argument :locale, GraphQL::Types::String, "The locale to use for the mutation", required: true + argument :toggle_translations, GraphQL::Types::Boolean, "Whether the user asked to toggle the machine translations or not", required: false, default_value: false + + def resolve(attributes:, locale:, toggle_translations:) + set_locale(locale:, toggle_translations:) + + params = { + id: object.id, + conclusions: attributes.to_h.fetch(:conclusions, "") + } + + form = form(Decidim::Debates::CloseDebateForm).from_params(params) + + Decidim::Debates::CloseDebate.call(form) do + on(:ok) do |debate| + return debate.reload + end + + on(:invalid) do + raise Decidim::Api::Errors::AttributeValidationError, form.errors + end + end + end + + def authorized?(attributes:, locale:, toggle_translations:) + raise Decidim::Api::Errors::MutationNotAuthorizedError, I18n.t("decidim.api.errors.unauthorized_mutation") unless [ + super, + allowed_to?(:close, :debate, object, context), + !object.closed? + ].all? + + true + end + end + end +end diff --git a/decidim-debates/lib/decidim/api/mutations/debate_mutation_type.rb b/decidim-debates/lib/decidim/api/mutations/debate_mutation_type.rb new file mode 100644 index 0000000000000..06227b5b6f06a --- /dev/null +++ b/decidim-debates/lib/decidim/api/mutations/debate_mutation_type.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Decidim + module Debates + class DebateMutationType < Decidim::Api::Types::BaseObject + include Decidim::ApiResponseHelper + + graphql_name "DebateMutation" + description "A debate which includes its available mutations" + + field :close, mutation: Decidim::Debates::CloseDebateType, description: "Closes a debate" + end + end +end diff --git a/decidim-debates/lib/decidim/api/mutations/debates_mutation_type.rb b/decidim-debates/lib/decidim/api/mutations/debates_mutation_type.rb new file mode 100644 index 0000000000000..9341e8c71094e --- /dev/null +++ b/decidim-debates/lib/decidim/api/mutations/debates_mutation_type.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Decidim + module Debates + class DebatesMutationType < Decidim::Core::ComponentType + description "Debates mutations for a component." + + field :debate, type: Decidim::Debates::DebateMutationType, description: "Mutates a debate", null: true do + argument :id, GraphQL::Types::ID, "The ID of the debate", required: true + end + + def debate(id:) + collection.find(id) + end + + private + + def collection + Debate.where(component: object).not_hidden + end + end + end +end diff --git a/decidim-debates/lib/decidim/debates/api.rb b/decidim-debates/lib/decidim/debates/api.rb index 132761788529f..c7b1d01110e55 100644 --- a/decidim-debates/lib/decidim/debates/api.rb +++ b/decidim-debates/lib/decidim/debates/api.rb @@ -4,5 +4,9 @@ module Decidim module Debates autoload :DebateType, "decidim/api/debate_type" autoload :DebatesType, "decidim/api/debates_type" + autoload :DebatesMutationType, "decidim/api/mutations/debates_mutation_type" + autoload :DebateMutationType, "decidim/api/mutations/debate_mutation_type" + autoload :CloseDebateType, "decidim/api/mutations/close_debate_type" + autoload :CloseDebateAttributes, "decidim/api/mutations/close_debate_attributes" end end diff --git a/decidim-debates/lib/decidim/debates/engine.rb b/decidim-debates/lib/decidim/debates/engine.rb index bbffffa296165..e8efdd91fedbc 100644 --- a/decidim-debates/lib/decidim/debates/engine.rb +++ b/decidim-debates/lib/decidim/debates/engine.rb @@ -98,6 +98,12 @@ class Engine < ::Rails::Engine end end end + + initializer "decidim_debates.register_mutations", before: "decidim_api.graphiql" do + Decidim::MutationRegistry.instance.register( + Decidim::Debates::DebatesMutationType + ) + end end end end diff --git a/decidim-debates/spec/factories.rb b/decidim-debates/spec/factories.rb index e03ae1bcd43f9..454c7cba15845 100644 --- a/decidim-debates/spec/factories.rb +++ b/decidim-debates/spec/factories.rb @@ -3,5 +3,5 @@ require "decidim/core/test/factories" require "decidim/comments/test/factories" require "decidim/participatory_processes/test/factories" - +require "decidim/api/test/factories" require "decidim/debates/test/factories" diff --git a/decidim-debates/spec/types/debate_mutation_type_spec.rb b/decidim-debates/spec/types/debate_mutation_type_spec.rb new file mode 100644 index 0000000000000..495dabd7a53fb --- /dev/null +++ b/decidim-debates/spec/types/debate_mutation_type_spec.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +require "spec_helper" +require "decidim/api/test/mutation_context" + +module Decidim + module Debates + describe CloseDebateType, type: :graphql do + include_context "with a graphql class mutation" + + let(:root_klass) { DebateMutationType } + let(:organization) { current_organization } + let(:participatory_process) { create(:participatory_process, :with_steps, organization:) } + let(:debates_component) { create(:component, manifest_name: :debates, participatory_space: participatory_process) } + let(:author) { current_user } + let!(:model) { create(:debate, component: debates_component, author:) } + let(:conclusions) { ::Faker::Lorem.sentence(word_count: 15) } + let(:component) { model.component } + let(:locale) { "en" } + let(:variables) do + { + input: { + locale:, + attributes: { + conclusions: + } + } + } + end + let(:query) do + <<~GRAPHQL + mutation($input: CloseDebateInput!) { + close(input: $input) { + id + conclusions { translation(locale: "en") } + closedAt + } + } + GRAPHQL + end + + shared_examples "close debate mutation examples" do + context "when user cannot close the debate" do + let!(:author) { create(:user, :confirmed, organization: current_organization) } + + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + + context "when user can close the debate" do + it "closes the debate" do + closed = response["close"] + expect(closed).to be_present + expect(closed).to include( + { + "id" => model.id.to_s, + "conclusions" => { + "translation" => conclusions + }, + "closedAt" => model.reload.closed_at.to_time.iso8601 + } + ) + end + end + + context "when validating" do + context "when debate is hidden" do + let!(:model) { create(:debate, :hidden, component: debates_component, author:) } + + it "raises a Decidim::Api::Errors::UnauthorizedObjectError" do + expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedObjectError, "You cannot view or edit this Debate because you do not have permissions") + end + end + + context "when debate is closed" do + let!(:model) { create(:debate, :closed, component: debates_component, author:) } + + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + + context "when conclusions are empty" do + let(:conclusions) { "" } + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::AttributeValidationError, /cannot be blank/) + end + end + + context "when conclusions are too short" do + let(:conclusions) { "Short" } + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::AttributeValidationError, /is too short/) + end + end + + context "with invalid locale" do + let(:locale) { "tlh" } + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::InvalidLocaleError, /Invalid locale provided/) + end + end + end + end + + context "with admin user" do + let!(:user_type) { :admin } + + it_behaves_like "close debate mutation examples" + end + + context "with debate author" do + let!(:user_type) { :user } + + it_behaves_like "close debate mutation examples" + end + + context "with api_user" do + let!(:user_type) { :api_user } + + it_behaves_like "close debate mutation examples" + end + end + end +end diff --git a/docs/modules/develop/pages/api/reference/components/debates/close.adoc b/docs/modules/develop/pages/api/reference/components/debates/close.adoc index 4077bf21e1ef1..fe01c4ebba000 100644 --- a/docs/modules/develop/pages/api/reference/components/debates/close.adoc +++ b/docs/modules/develop/pages/api/reference/components/debates/close.adoc @@ -1,3 +1,48 @@ = Close Debate -include::admin:partial$under-construction.adoc[] +To use the API to close any debate that the user has created, you will need to use the following API request. + +[source,graphql] +---- +mutation closeDebate($componentId: ID!, $debateId: ID!, $input: CloseDebateInput!){ + component(id: $componentId) { + ...on DebatesMutation{ + debate(id: $debateId) { + close(input: $input) { + id + title { + translation(locale: "en") + } + conclusions { + translation(locale: "en") + } + } + } + } + } +} +---- + +Example of submitted variables + +[source,json] +---- +{ + "componentId": "9", + "debateId": "2", + "input": { + "locale": "en", + "attributes": { + "conclusions": "Your closing report" + } + } +} +---- + +== Error Handling + +The most frequent errors that can be generated by this mutation are as follows: + +* xref:develop:api/reference/errors/unauthorized_mutation.adoc[Unauthorized Mutation Error - MUTATION_NOT_AUTHORIZED_ERROR] +* xref:develop:api/reference/errors/attribute_validation_error.adoc[Attribute Validation Error - ATTRIBUTE_VALIDATION_ERROR] +* xref:develop:api/reference/errors/unauthorized_object.adoc[Unauthorized Object Error - UNAUTHORIZED_OBJECT_ERROR] From ca74db9d7c12a6965850814be42ff38b98707d3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Thu, 8 Jan 2026 15:15:01 +0100 Subject: [PATCH 051/116] Fix promoting committee members page in Initiatives' admin (#15844) --- .../admin/committee_requests/index.html.erb | 2 +- ...ages_initiatives_committee_members_spec.rb | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 decidim-initiatives/spec/system/admin/admin_manages_initiatives_committee_members_spec.rb diff --git a/decidim-initiatives/app/views/decidim/initiatives/admin/committee_requests/index.html.erb b/decidim-initiatives/app/views/decidim/initiatives/admin/committee_requests/index.html.erb index 22cf06a2d434b..01eeee78af3be 100644 --- a/decidim-initiatives/app/views/decidim/initiatives/admin/committee_requests/index.html.erb +++ b/decidim-initiatives/app/views/decidim/initiatives/admin/committee_requests/index.html.erb @@ -13,7 +13,7 @@
- <%= decidim_initiatives.new_initiative_committee_request_url(current_initiative) %> + <%= decidim_initiatives.new_initiative_committee_request_url(current_initiative, locale: current_locale) %> <%= icon_link_to "clipboard-line", "#", t(".invite_to_committee_help"), class: "card--list__data__icon invite-users-link" %>
diff --git a/decidim-initiatives/spec/system/admin/admin_manages_initiatives_committee_members_spec.rb b/decidim-initiatives/spec/system/admin/admin_manages_initiatives_committee_members_spec.rb new file mode 100644 index 0000000000000..7fba608da29f1 --- /dev/null +++ b/decidim-initiatives/spec/system/admin/admin_manages_initiatives_committee_members_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "User manages the initiatives committee members page" do + include_context "when admins initiative" + + before do + switch_to_host(organization.host) + login_as user, scope: :user + visit decidim_admin_initiatives.initiatives_path + click_on translated(initiative.title) + end + + context "when the initiative does not have promoting committee" do + let(:initiative_type) { create(:initiatives_type, :promoting_committee_disabled, organization:) } + + it "does not have the committee members section" do + expect(page).to have_no_content("Committee members") + end + end + + context "when the initiative has promoting committee" do + let(:initiative_type) { create(:initiatives_type, :promoting_committee_enabled, organization:) } + + before do + click_on "Committee members" + end + + it "has the committee members section" do + expect(page).to have_content("Committee members") + end + end +end From 565381238f15f9434b037904afdb453012d24a54 Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Fri, 9 Jan 2026 09:05:19 +0200 Subject: [PATCH 052/116] Write API: create meetings (#15796) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add CreateMeeting mutation for decidim-meetings API * Running rubcops * Fix mutation registration * Remove READMEs * Implement meeting creation * Normalize * Fix documentation * Fix enums * Fix spelling * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix pipeline * Fix User type * Apply suggestions from code review Co-authored-by: Andrés Pereira de Lucena * Apply suggestions from code review * Fix dependency location * Remove wrong reference --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Andrés Pereira de Lucena --- decidim-api/lib/decidim/api/test.rb | 1 + .../{ => shared_examples}/mutation_context.rb | 0 .../app/commands/decidim/create_follow.rb | 1 + .../api/interfaces/author_interface.rb | 2 +- .../spec/types/debate_mutation_type_spec.rb | 1 - decidim-meetings/config/locales/en.yml | 2 +- .../decidim/api/iframe_access_level_enum.rb | 13 + .../lib/decidim/api/iframe_embed_type_enum.rb | 14 ++ .../lib/decidim/api/meeting_type.rb | 8 +- .../mutations/create_meeting_attributes.rb | 35 +++ .../api/mutations/create_meeting_type.rb | 63 +++++ .../api/mutations/meetings_mutation_type.rb | 2 + .../lib/decidim/api/registration_type_enum.rb | 13 + .../lib/decidim/api/type_of_meeting_enum.rb | 13 + decidim-meetings/lib/decidim/meetings/api.rb | 9 +- .../lib/decidim/meetings/engine.rb | 12 +- decidim-meetings/spec/factories.rb | 1 + .../api/mutations/create_meeting_type_spec.rb | 234 ++++++++++++++++++ .../spec/types/integration_schema_spec.rb | 6 +- .../spec/types/meeting_mutation_type_spec.rb | 1 - .../spec/types/meeting_type_spec.rb | 6 +- .../mutations/proposal_answer_type_spec.rb | 1 - .../spec/types/unvote_proposal_type_spec.rb | 1 - .../spec/types/vote_proposal_type_spec.rb | 1 - .../spec/types/withdraw_proposal_type_spec.rb | 1 - .../reference/components/meetings/create.adoc | 107 +++++++- 26 files changed, 522 insertions(+), 26 deletions(-) rename decidim-api/lib/decidim/api/test/{ => shared_examples}/mutation_context.rb (100%) create mode 100644 decidim-meetings/lib/decidim/api/iframe_access_level_enum.rb create mode 100644 decidim-meetings/lib/decidim/api/iframe_embed_type_enum.rb create mode 100644 decidim-meetings/lib/decidim/api/mutations/create_meeting_attributes.rb create mode 100644 decidim-meetings/lib/decidim/api/mutations/create_meeting_type.rb create mode 100644 decidim-meetings/lib/decidim/api/registration_type_enum.rb create mode 100644 decidim-meetings/lib/decidim/api/type_of_meeting_enum.rb create mode 100644 decidim-meetings/spec/lib/decidim/api/mutations/create_meeting_type_spec.rb diff --git a/decidim-api/lib/decidim/api/test.rb b/decidim-api/lib/decidim/api/test.rb index 580923a1dff95..e7c57f0b3f470 100644 --- a/decidim-api/lib/decidim/api/test.rb +++ b/decidim-api/lib/decidim/api/test.rb @@ -22,4 +22,5 @@ require "decidim/api/test/shared_examples/taxonomizable_interface_examples" require "decidim/api/test/shared_examples/timestamps_interface_examples" require "decidim/api/test/shared_examples/traceable_interface_examples" +require "decidim/api/test/shared_examples/mutation_context" require "decidim/api/test/type_context" diff --git a/decidim-api/lib/decidim/api/test/mutation_context.rb b/decidim-api/lib/decidim/api/test/shared_examples/mutation_context.rb similarity index 100% rename from decidim-api/lib/decidim/api/test/mutation_context.rb rename to decidim-api/lib/decidim/api/test/shared_examples/mutation_context.rb diff --git a/decidim-core/app/commands/decidim/create_follow.rb b/decidim-core/app/commands/decidim/create_follow.rb index 0be0c622378d5..272b3bcf58aeb 100644 --- a/decidim-core/app/commands/decidim/create_follow.rb +++ b/decidim-core/app/commands/decidim/create_follow.rb @@ -20,6 +20,7 @@ def initialize(form) # Returns nothing. def call return broadcast(:invalid) if form.invalid? + return broadcast(:invalid) unless current_user.is_a?(Decidim::User) create_follow! increment_score diff --git a/decidim-core/lib/decidim/api/interfaces/author_interface.rb b/decidim-core/lib/decidim/api/interfaces/author_interface.rb index e49ea8e2b262d..a82fe64464cbe 100644 --- a/decidim-core/lib/decidim/api/interfaces/author_interface.rb +++ b/decidim-core/lib/decidim/api/interfaces/author_interface.rb @@ -25,7 +25,7 @@ def organization_name end def self.resolve_type(obj, _ctx) - return Decidim::Core::UserType if obj.is_a? Decidim::User + return Decidim::Core::UserType if obj.is_a? Decidim::UserBaseEntity end end end diff --git a/decidim-debates/spec/types/debate_mutation_type_spec.rb b/decidim-debates/spec/types/debate_mutation_type_spec.rb index 495dabd7a53fb..f2808e5bce128 100644 --- a/decidim-debates/spec/types/debate_mutation_type_spec.rb +++ b/decidim-debates/spec/types/debate_mutation_type_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "spec_helper" -require "decidim/api/test/mutation_context" module Decidim module Debates diff --git a/decidim-meetings/config/locales/en.yml b/decidim-meetings/config/locales/en.yml index 82dbc8c729ea3..41bc5970aa729 100644 --- a/decidim-meetings/config/locales/en.yml +++ b/decidim-meetings/config/locales/en.yml @@ -802,7 +802,7 @@ en: follows_count: The number of follows the meeting has id: The unique identifier of the meeting iframe_access_level: The iframe access level of the meeting - iframe_embed_type: The type of iframe embeded in the meeting + iframe_embed_type: The type of iframe embedded in the meeting latitude: The latitude of the meeting location: The location of the meeting location_hints: A hint of the location in which the meeting is taking place diff --git a/decidim-meetings/lib/decidim/api/iframe_access_level_enum.rb b/decidim-meetings/lib/decidim/api/iframe_access_level_enum.rb new file mode 100644 index 0000000000000..2c5f3c628994b --- /dev/null +++ b/decidim-meetings/lib/decidim/api/iframe_access_level_enum.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Decidim + module Meetings + class IframeAccessLevelEnum < Decidim::Api::Types::BaseEnum + description "The iframe access level of the meeting" + + value "ALL", value: "all", description: "All visitors", value_method: false + value "SIGNED_IN", value: "signed_in", description: "Only signed-in participants", value_method: false + value "REGISTERED", value: "registered", description: "Registered participants to this meeting", value_method: false + end + end +end diff --git a/decidim-meetings/lib/decidim/api/iframe_embed_type_enum.rb b/decidim-meetings/lib/decidim/api/iframe_embed_type_enum.rb new file mode 100644 index 0000000000000..2b4d7ddaf113f --- /dev/null +++ b/decidim-meetings/lib/decidim/api/iframe_embed_type_enum.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Decidim + module Meetings + class IframeEmbedTypeEnum < Decidim::Api::Types::BaseEnum + description "The type of iframe embedded in the meeting" + + value "NONE", value: "none", description: "None", value_method: false + value "EMBED_IN_MEETING_PAGE", value: "embed_in_meeting_page", description: "Embed in meeting page", value_method: false + value "OPEN_IN_LIVE_EVENT_PAGE", value: "open_in_live_event_page", description: "Open in live event page", value_method: false + value "OPEN_IN_NEW_TAB", value: "open_in_new_tab", description: "Open URL in a new tab", value_method: false + end + end +end diff --git a/decidim-meetings/lib/decidim/api/meeting_type.rb b/decidim-meetings/lib/decidim/api/meeting_type.rb index 6c3850674d247..86379c71eb37f 100644 --- a/decidim-meetings/lib/decidim/api/meeting_type.rb +++ b/decidim-meetings/lib/decidim/api/meeting_type.rb @@ -32,8 +32,8 @@ class MeetingType < Decidim::Api::Types::BaseObject field :description, Decidim::Core::TranslatedFieldType, "The description of this meeting.", null: true field :end_time, Decidim::Core::DateTimeType, "The time this meeting ends", null: false field :id, GraphQL::Types::ID, "ID of this meeting", null: false - field :iframe_access_level, GraphQL::Types::String, "The iframe access level of the meeting ", null: true - field :iframe_embed_type, GraphQL::Types::String, "The type of displaying of the online meeting URL", null: true + field :iframe_access_level, Decidim::Meetings::IframeAccessLevelEnum, "The iframe access level of the meeting ", null: true + field :iframe_embed_type, Decidim::Meetings::IframeEmbedTypeEnum, "The type of displaying of the online meeting URL", null: true field :location, Decidim::Core::TranslatedFieldType, "The location of this meeting (free format)", null: true field :location_hints, Decidim::Core::TranslatedFieldType, "The location of this meeting (free format)", null: true field :online_meeting_url, GraphQL::Types::String, "The URL of the meeting (when the type is online)", null: false @@ -43,14 +43,14 @@ class MeetingType < Decidim::Api::Types::BaseObject field :registration_form, Decidim::Forms::QuestionnaireType, description: "If registration requires to fill a form, this is the questionnaire", null: true field :registration_form_enabled, GraphQL::Types::Boolean, "Whether the registrations have a form or not", null: false field :registration_terms, Decidim::Core::TranslatedFieldType, "The registration terms", null: true - field :registration_type, GraphQL::Types::String, "The type of registration (disabled, on this platform or different platform)", null: false + field :registration_type, Decidim::Meetings::RegistrationTypeEnum, "The type of registration (disabled, on this platform or different platform)", null: false field :registration_url, GraphQL::Types::String, "URL for the registration form", null: true field :registrations_enabled, GraphQL::Types::Boolean, "Whether the registrations are enabled or not", null: false field :remaining_slots, GraphQL::Types::Int, "Amount of slots available for this meeting", null: true field :start_time, Decidim::Core::DateTimeType, "The time this meeting starts", null: false field :title, Decidim::Core::TranslatedFieldType, "The title of this meeting.", null: false field :transparent, GraphQL::Types::Boolean, "For private meetings, information is public if transparent", null: false - field :type_of_meeting, GraphQL::Types::String, "The type of the meeting (online or in-person)", null: false + field :type_of_meeting, Meetings::TypeOfMeetingEnum, "The type of the meeting (online, hybrid or in-person)", null: false field :url, GraphQL::Types::String, "The URL for this meeting", null: false field :video_url, GraphQL::Types::String, "URL for the video of the session, if any", null: true field :withdrawn, GraphQL::Types::Boolean, "Whether this meeting has been withdrawn or not", method: :withdrawn?, null: true diff --git a/decidim-meetings/lib/decidim/api/mutations/create_meeting_attributes.rb b/decidim-meetings/lib/decidim/api/mutations/create_meeting_attributes.rb new file mode 100644 index 0000000000000..1187a95e2e548 --- /dev/null +++ b/decidim-meetings/lib/decidim/api/mutations/create_meeting_attributes.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Decidim + module Meetings + class CreateMeetingAttributes < Decidim::Api::Types::BaseInputObject + graphql_name "CreateMeetingAttributes" + description "Attributes for creating a meeting" + + argument :address, GraphQL::Types::String, description: "The address of the meeting", required: false + argument :available_slots, GraphQL::Types::Int, description: "Number of available slots for registration", required: false + argument :description, GraphQL::Types::String, description: "The description of the meeting", required: true + argument :end_time, Decidim::Core::DateTimeType, description: "The end time of the meeting", required: true + argument :iframe_access_level, Decidim::Meetings::IframeAccessLevelEnum, + description: "Who can access the iframe: 'all', 'registered', 'signed_in'", required: false + argument :iframe_embed_type, Decidim::Meetings::IframeEmbedTypeEnum, + description: "How to embed the iframe: 'none', 'embed_in_meeting_page', 'open_in_live_event_page', 'open_in_new_tab'", + required: false + argument :latitude, GraphQL::Types::Float, description: "The latitude coordinate", required: false + argument :location, GraphQL::Types::String, description: "The physical location of the meeting", required: false + argument :location_hints, GraphQL::Types::String, description: "Hints about the location", required: false + argument :longitude, GraphQL::Types::Float, description: "The longitude coordinate", required: false + argument :online_meeting_url, GraphQL::Types::String, description: "URL for online meeting", required: false + argument :registration_terms, GraphQL::Types::String, description: "Terms and conditions for registration", required: false + argument :registration_type, Decidim::Meetings::RegistrationTypeEnum, + description: "Type of registration: 'on_this_platform', 'on_different_platform', or 'registration_disabled'", + required: true + argument :registration_url, GraphQL::Types::String, description: "External registration URL", required: false + argument :registrations_enabled, GraphQL::Types::Boolean, description: "Whether registrations are enabled or not", required: false + argument :start_time, Decidim::Core::DateTimeType, description: "The start time of the meeting", required: true + argument :taxonomies, [GraphQL::Types::ID], description: "Array of taxonomy IDs", required: false + argument :title, GraphQL::Types::String, description: "The title of the meeting", required: true + argument :type_of_meeting, Decidim::Meetings::TypeOfMeetingEnum, description: "The type of meeting: 'online', 'in_person', or 'hybrid'", required: true + end + end +end diff --git a/decidim-meetings/lib/decidim/api/mutations/create_meeting_type.rb b/decidim-meetings/lib/decidim/api/mutations/create_meeting_type.rb new file mode 100644 index 0000000000000..4a2989fc6fcac --- /dev/null +++ b/decidim-meetings/lib/decidim/api/mutations/create_meeting_type.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Decidim + module Meetings + class CreateMeetingType < Decidim::Api::Types::BaseMutation + graphql_name "CreateMeeting" + + description "Creates a meeting" + type Decidim::Meetings::MeetingType + + argument :attributes, CreateMeetingAttributes, description: "Input attributes for creating a meeting", required: true + argument :locale, GraphQL::Types::String, "The locale for which to set the meeting texts", required: true + argument :toggle_translations, GraphQL::Types::Boolean, "Whether the user asked to toggle the machine translations or not.", required: false, default_value: false + + def resolve(attributes:, locale:, toggle_translations:) + set_locale(locale:, toggle_translations:) + + params = attributes.to_h.slice( + :address, + :available_slots, + :description, + :end_time, + :latitude, + :longitude, + :location, + :location_hints, + :online_meeting_url, + :registration_terms, + :registration_type, + :registration_url, + :start_time, + :title, + :taxonomies, + :type_of_meeting, + :iframe_access_level, + :iframe_embed_type, + :registrations_enabled + ) + + params[:taxonomies] = Decidim::Taxonomy.where(organization: current_organization, id: params[:taxonomies]).pluck(:id) if params[:taxonomies] + + form = form(Decidim::Meetings::MeetingForm).from_params(params) + + Decidim::Meetings::CreateMeeting.call(form) do + on(:ok) do |meeting| + return meeting.reload + end + on(:invalid) do + raise Decidim::Api::Errors::AttributeValidationError, form.errors + end + end + end + + def authorized?(attributes:, locale:, toggle_translations:) + unless super && allowed_to?(:create, :meeting, Meeting.new(component: current_component), { current_user:, current_component: }) + raise Decidim::Api::Errors::MutationNotAuthorizedError, I18n.t("decidim.api.errors.unauthorized_mutation") + end + + true + end + end + end +end diff --git a/decidim-meetings/lib/decidim/api/mutations/meetings_mutation_type.rb b/decidim-meetings/lib/decidim/api/mutations/meetings_mutation_type.rb index 49cc9116ab878..a133f643bca7a 100644 --- a/decidim-meetings/lib/decidim/api/mutations/meetings_mutation_type.rb +++ b/decidim-meetings/lib/decidim/api/mutations/meetings_mutation_type.rb @@ -3,8 +3,10 @@ module Decidim module Meetings class MeetingsMutationType < Decidim::Core::ComponentType + graphql_name "MeetingsMutation" description "A meetings component with mutations." + field :create_meeting, mutation: Decidim::Meetings::CreateMeetingType, description: "Creates a new meeting" field :meeting, type: Decidim::Meetings::MeetingMutationType, description: "Mutates a meeting", null: true do argument :id, GraphQL::Types::ID, "The ID of the meeting", required: true end diff --git a/decidim-meetings/lib/decidim/api/registration_type_enum.rb b/decidim-meetings/lib/decidim/api/registration_type_enum.rb new file mode 100644 index 0000000000000..0a30c008e5923 --- /dev/null +++ b/decidim-meetings/lib/decidim/api/registration_type_enum.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Decidim + module Meetings + class RegistrationTypeEnum < Decidim::Api::Types::BaseEnum + description "The registration types for meetings" + + value "REGISTRATION_DISABLED", value: "registration_disabled", description: "Registration disabled", value_method: false + value "ON_THIS_PLATFORM", value: "on_this_platform", description: "On this platform", value_method: false + value "ON_DIFFERENT_PLATFORM", value: "on_different_platform", description: "On a different platform", value_method: false + end + end +end diff --git a/decidim-meetings/lib/decidim/api/type_of_meeting_enum.rb b/decidim-meetings/lib/decidim/api/type_of_meeting_enum.rb new file mode 100644 index 0000000000000..26e6dcac357bb --- /dev/null +++ b/decidim-meetings/lib/decidim/api/type_of_meeting_enum.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Decidim + module Meetings + class TypeOfMeetingEnum < Decidim::Api::Types::BaseEnum + description "The types of meetings" + + value "HYBRID", value: "hybrid", description: "Hybrid" + value "IN_PERSON", value: "in_person", description: "In person" + value "ONLINE", value: "online", description: "Online" + end + end +end diff --git a/decidim-meetings/lib/decidim/meetings/api.rb b/decidim-meetings/lib/decidim/meetings/api.rb index f5fc340e8e568..4e273e1ab0b58 100644 --- a/decidim-meetings/lib/decidim/meetings/api.rb +++ b/decidim-meetings/lib/decidim/meetings/api.rb @@ -9,9 +9,16 @@ module Meetings autoload :ServiceType, "decidim/api/service_type" autoload :LinkedResourcesInterface, "decidim/api/linked_resources_interface" autoload :ServicesInterface, "decidim/api/services_interface" - + autoload :CreateMeetingAttributes, "decidim/api/mutations/create_meeting_attributes" + autoload :CreateMeetingType, "decidim/api/mutations/create_meeting_type" autoload :MeetingsMutationType, "decidim/api/mutations/meetings_mutation_type" + autoload :MeetingMutationType, "decidim/api/mutations/meeting_mutation_type" autoload :WithdrawMeetingType, "decidim/api/mutations/withdraw_meeting_type" + + autoload :RegistrationTypeEnum, "decidim/api/registration_type_enum" + autoload :TypeOfMeetingEnum, "decidim/api/type_of_meeting_enum" + autoload :IframeEmbedTypeEnum, "decidim/api/iframe_embed_type_enum" + autoload :IframeAccessLevelEnum, "decidim/api/iframe_access_level_enum" end end diff --git a/decidim-meetings/lib/decidim/meetings/engine.rb b/decidim-meetings/lib/decidim/meetings/engine.rb index 1cd5f1f6b0d2b..f90965d9f82d9 100644 --- a/decidim-meetings/lib/decidim/meetings/engine.rb +++ b/decidim-meetings/lib/decidim/meetings/engine.rb @@ -81,6 +81,12 @@ class Engine < ::Rails::Engine end end + initializer "decidim_meetings.register_mutations", before: "decidim_api.graphiql" do + Decidim::MutationRegistry.instance.register( + Decidim::Meetings::MeetingsMutationType + ) + end + initializer "decidim_meetings.content_processors" do |_app| Decidim.configure do |config| config.content_processors += [:meeting] @@ -171,12 +177,6 @@ class Engine < ::Rails::Engine end end end - - initializer "decidim_meetings.register_mutations", before: "decidim_api.graphiql" do - Decidim::MutationRegistry.instance.register( - Decidim::Meetings::MeetingsMutationType - ) - end end end end diff --git a/decidim-meetings/spec/factories.rb b/decidim-meetings/spec/factories.rb index c56e85f15e5ea..eaa4fcb922fdf 100644 --- a/decidim-meetings/spec/factories.rb +++ b/decidim-meetings/spec/factories.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "decidim/core/test/factories" +require "decidim/api/test/factories" require "decidim/participatory_processes/test/factories" require "decidim/assemblies/test/factories" require "decidim/proposals/test/factories" diff --git a/decidim-meetings/spec/lib/decidim/api/mutations/create_meeting_type_spec.rb b/decidim-meetings/spec/lib/decidim/api/mutations/create_meeting_type_spec.rb new file mode 100644 index 0000000000000..7e3817420157a --- /dev/null +++ b/decidim-meetings/spec/lib/decidim/api/mutations/create_meeting_type_spec.rb @@ -0,0 +1,234 @@ +# frozen_string_literal: true + +require "spec_helper" +require "decidim/api/test" + +module Decidim + module Meetings + describe CreateMeetingType do + include_context "with a graphql class mutation" + + let(:type_class) { Decidim::Meetings::CreateMeetingType } + let(:root_klass) { Decidim::Meetings::MeetingsMutationType } + + let(:current_organization) { create(:organization, available_locales: [:en]) } + let(:organization) { current_organization } + let(:participatory_process) { create(:participatory_process, :published, :with_steps, organization:) } + + let(:locale) { "en" } + let(:translation_locale) { "en" } + + let!(:current_component) do + create(:meeting_component, :published, participatory_space: participatory_process, settings: { + creation_enabled_for_participants: true, + taxonomy_filters: [taxonomy_filter.id] + }) + end + let(:root_taxonomy) { create(:taxonomy, organization:) } + let!(:taxonomy) { create(:taxonomy, parent: root_taxonomy, organization:) } + let(:taxonomy_filter) { create(:taxonomy_filter, root_taxonomy:) } + let!(:taxonomy_filter_item) { create(:taxonomy_filter_item, taxonomy_filter:, taxonomy_item: taxonomy) } + let!(:user) { create(:user, :confirmed, organization:) } + + let(:title) { "More sidewalks and less roads" } + let(:description) { "Cities need more people, not more cars" } + let(:address) { "Carrer de la Pau, 1, Barcelona" } + let(:latitude) { 40.1234 } + let(:longitude) { 2.1234 } + let(:start_time) { 1.day.from_now } + let(:end_time) { start_time + 2.hours } + let(:iframe_embed_type) { "NONE" } + let(:iframe_access_level) { "ALL" } + let(:location) { "Somewhere" } + let(:location_hints) { "Near the main square" } + let(:online_meeting_url) { "https://meets.example.org/abc-def" } + let(:registration_terms) { "By registering you agree to the terms and conditions" } + let(:registration_type) { "ON_THIS_PLATFORM" } + let(:registration_url) { "https://example.org/register" } + let(:registrations_enabled) { true } + let(:type_of_meeting) { "ONLINE" } + + let(:root_value) { current_component } + let(:query) do + <<~GRAPHQL + mutation createMeetings($input: CreateMeetingInput!){ + createMeeting(input: $input) { + id + title { translation(locale: "#{translation_locale}") } + description { translation(locale: "#{translation_locale}") } + address + coordinates { latitude longitude } + publishedAt + author { name } + taxonomies { id } + remainingSlots + location { translation(locale: "#{translation_locale}") } + locationHints { translation(locale: "#{translation_locale}") } + onlineMeetingUrl + registrationTerms { translation(locale: "#{translation_locale}") } + registrationType + registrationUrl + endTime + startTime + typeOfMeeting + registrationsEnabled + } + } + GRAPHQL + end + + let(:attributes) do + { + address:, + availableSlots: 10, + description:, + endTime: end_time.iso8601, + iframeAccessLevel: iframe_access_level, + iframeEmbedType: iframe_embed_type, + latitude:, + location:, + locationHints: location_hints, + longitude:, + onlineMeetingUrl: online_meeting_url, + registrationTerms: registration_terms, + registrationType: registration_type, + registrationUrl: registration_url, + registrationsEnabled: registrations_enabled, + startTime: start_time.iso8601, + taxonomies: [taxonomy.id], + title:, + typeOfMeeting: type_of_meeting + } + end + + let(:variables) do + { + component_id: current_component.id, + input: { + locale:, + attributes: + } + } + end + + before do + stub_geocoding(address, [latitude, longitude]) + end + + context "when validating" do + context "with having invalid locale" do + let(:locale) { "tlh" } + + it "raises an error" do + expect { response }.to raise_error(Api::Errors::InvalidLocaleError, /Invalid locale provided/) + end + end + + context "with having invalid title" do + context "when is missing" do + let(:title) { "" } + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::AttributeValidationError, /cannot be blank/) + end + end + end + + context "with having invalid registration type" do + let(:registration_type) { "INVALID_TYPE" } + + it "raises an error" do + expect { response }.to raise_error(GraphQL::ExecutionError, /to be one of: REGISTRATION_DISABLED, ON_THIS_PLATFORM, ON_DIFFERENT_PLATFORM/) + end + end + + context "with having invalid meeting type" do + let(:type_of_meeting) { "INVALID_TYPE" } + + it "raises an error" do + expect { response }.to raise_error(GraphQL::ExecutionError, /to be one of: HYBRID, IN_PERSON, ONLINE/) + end + end + end + + shared_examples "create meeting mutation examples" do + context "when creation is disabled" do + let!(:current_component) { create(:meeting_component, :published, participatory_space: participatory_process) } + + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + + context "when user is logged in" do + it "creates a new meeting" do + meeting_response = response["createMeeting"] + + expect(meeting_response).to be_present + expect(meeting_response["title"]["translation"]).to eq(title) + expect(meeting_response["description"]["translation"]).to include(description) + expect(meeting_response["publishedAt"]).to be_present + expect(meeting_response["taxonomies"]).to include({ "id" => taxonomy.id.to_s }) + expect(meeting_response["author"]["name"]).to eq(current_user.name) + expect(meeting_response["remainingSlots"]).to eq(10) + expect(meeting_response["location"]).to include({ "translation" => location }) + expect(meeting_response["locationHints"]).to include({ "translation" => location_hints }) + expect(meeting_response["registrationTerms"]).to include({ "translation" => registration_terms }) + + expect(meeting_response["onlineMeetingUrl"]).to eq(online_meeting_url) + expect(meeting_response["registrationType"]).to eq(registration_type) + expect(meeting_response["registrationUrl"]).to eq(registration_url) + expect(meeting_response["endTime"]).to eq(end_time.to_time.iso8601) + expect(meeting_response["startTime"]).to eq(start_time.to_time.iso8601) + expect(meeting_response["typeOfMeeting"]).to eq(type_of_meeting) + expect(meeting_response["registrationsEnabled"]).to eq(registrations_enabled) + + expect(meeting_response["address"]).to eq(address) + expect(meeting_response["coordinates"]).to include( + "latitude" => latitude, + "longitude" => longitude + ) + end + + context "when submitting in one language and requesting in another" do + let(:locale) { "en" } + let(:translation_locale) { "es" } + + it "creates a new meeting" do + meeting_response = response["createMeeting"] + + expect(meeting_response).to be_present + expect(meeting_response["title"]["translation"]).to be_nil + end + end + end + end + + context "with admin user" do + it_behaves_like "create meeting mutation examples" do + let!(:user_type) { :admin } + end + end + + context "with normal user" do + it_behaves_like "create meeting mutation examples" do + let!(:user_type) { :user } + end + end + + context "with api_user" do + it_behaves_like "create meeting mutation examples" do + let!(:user_type) { :api_user } + end + end + + context "when the user is not logged in" do + let(:current_user) { nil } + + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + end + end +end diff --git a/decidim-meetings/spec/types/integration_schema_spec.rb b/decidim-meetings/spec/types/integration_schema_spec.rb index 158d5e676fd6f..a0865a503d358 100644 --- a/decidim-meetings/spec/types/integration_schema_spec.rb +++ b/decidim-meetings/spec/types/integration_schema_spec.rb @@ -150,8 +150,8 @@ "followsCount" => meeting.follows_count, "hasComments" => meeting.comment_threads.size.positive?, "id" => meeting.id.to_s, - "iframeAccessLevel" => meeting.iframe_access_level, - "iframeEmbedType" => meeting.iframe_embed_type, + "iframeAccessLevel" => meeting.iframe_access_level.upcase, + "iframeEmbedType" => meeting.iframe_embed_type.upcase, "location" => { "translation" => meeting.location[locale] }, "locationHints" => { "translation" => meeting.location_hints[locale] }, "privateMeeting" => false, @@ -161,7 +161,7 @@ "reference" => meeting.reference, "registrationForm" => { "id" => meeting.questionnaire.id.to_s }, "registrationTerms" => { "translation" => meeting.registration_terms[locale] }, - "registrationType" => meeting.registration_type, + "registrationType" => meeting.registration_type.upcase, "registrationUrl" => nil, "registrationsEnabled" => false, "remainingSlots" => 0, diff --git a/decidim-meetings/spec/types/meeting_mutation_type_spec.rb b/decidim-meetings/spec/types/meeting_mutation_type_spec.rb index ea34a82c8a51b..0ce4fd1b23081 100644 --- a/decidim-meetings/spec/types/meeting_mutation_type_spec.rb +++ b/decidim-meetings/spec/types/meeting_mutation_type_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "spec_helper" -require "decidim/api/test/mutation_context" module Decidim module Meetings diff --git a/decidim-meetings/spec/types/meeting_type_spec.rb b/decidim-meetings/spec/types/meeting_type_spec.rb index 52a545afff19b..1d7f16ce14489 100644 --- a/decidim-meetings/spec/types/meeting_type_spec.rb +++ b/decidim-meetings/spec/types/meeting_type_spec.rb @@ -111,7 +111,7 @@ module Meetings let(:model) { create(:meeting, :published, component:, iframe_access_level:) } it "displays the field value" do - expect(response["iframeAccessLevel"]).to eq(model.iframe_access_level) + expect(response["iframeAccessLevel"]).to eq(model.iframe_access_level.upcase) end end end @@ -130,7 +130,7 @@ module Meetings let(:model) { create(:meeting, :published, component:, iframe_embed_type:) } it "displays the field value" do - expect(response["iframeEmbedType"]).to eq(model.iframe_embed_type) + expect(response["iframeEmbedType"]).to eq(model.iframe_embed_type.upcase) end end end @@ -144,7 +144,7 @@ module Meetings let(:model) { create(:meeting, :published, component:, registration_type:) } it "displays the field value" do - expect(response["registrationType"]).to eq(model.registration_type) + expect(response["registrationType"]).to eq(model.registration_type.upcase) end end end diff --git a/decidim-proposals/spec/types/mutations/proposal_answer_type_spec.rb b/decidim-proposals/spec/types/mutations/proposal_answer_type_spec.rb index 7681678602429..8cd758c64e3e3 100644 --- a/decidim-proposals/spec/types/mutations/proposal_answer_type_spec.rb +++ b/decidim-proposals/spec/types/mutations/proposal_answer_type_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "spec_helper" -require "decidim/api/test/mutation_context" module Decidim module Proposals diff --git a/decidim-proposals/spec/types/unvote_proposal_type_spec.rb b/decidim-proposals/spec/types/unvote_proposal_type_spec.rb index 66af5a95f8908..651d8f7e26509 100644 --- a/decidim-proposals/spec/types/unvote_proposal_type_spec.rb +++ b/decidim-proposals/spec/types/unvote_proposal_type_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "spec_helper" -require "decidim/api/test/mutation_context" module Decidim module Proposals diff --git a/decidim-proposals/spec/types/vote_proposal_type_spec.rb b/decidim-proposals/spec/types/vote_proposal_type_spec.rb index ce6edb2002dac..0c03ada1a4e86 100644 --- a/decidim-proposals/spec/types/vote_proposal_type_spec.rb +++ b/decidim-proposals/spec/types/vote_proposal_type_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "spec_helper" -require "decidim/api/test/mutation_context" module Decidim module Proposals diff --git a/decidim-proposals/spec/types/withdraw_proposal_type_spec.rb b/decidim-proposals/spec/types/withdraw_proposal_type_spec.rb index 6d11bf3dbf4e5..e602904c5c08d 100644 --- a/decidim-proposals/spec/types/withdraw_proposal_type_spec.rb +++ b/decidim-proposals/spec/types/withdraw_proposal_type_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "spec_helper" -require "decidim/api/test/mutation_context" module Decidim module Proposals diff --git a/docs/modules/develop/pages/api/reference/components/meetings/create.adoc b/docs/modules/develop/pages/api/reference/components/meetings/create.adoc index da498b07c613a..071be3b7cbbe8 100644 --- a/docs/modules/develop/pages/api/reference/components/meetings/create.adoc +++ b/docs/modules/develop/pages/api/reference/components/meetings/create.adoc @@ -1,3 +1,108 @@ = Create Meeting -include::admin:partial$under-construction.adoc[] +== Input attributes + +When you want to add or create data in your meetings component, you need to rely on the `CreateMeetingAttributes` type, which has the following input fields: + +[source] +---- +• address (optional) - String +• available_slots (optional) - Int +• title (required) - String +• description (required) - String +• end_time (required) - DateTime +• latitude (optional) - coordinate +• longitude (optional) - coordinate +• location_hints (optional) - String +• location (optional) - String +• online_meeting_url (optional) - String +• registration_terms (optional) - String +• registration_type (required) - RegistrationTypeType +• registration_url (optional) - String +• registrations_enabled (optional) - Boolean +• start_time (required) - DateTime +• taxonomies (optional) - Taxonomy ids to assign to this meeting +• type_of_meeting (required) - TypeOfMeeting +---- + +You may want to check the following input types: + +* http://nightly.decidim.org/api/docs/enum/typeofmeetingenum/[TypeOfMeetingEnum] +* http://nightly.decidim.org/api/docs/enum/registrationtypeenum/[RegistrationTypeEnum] +* http://nightly.decidim.org/api/docs/enum/iframeembedtypeenum/[IframeEmbedTypeEnum] +* http://nightly.decidim.org/api/docs/enum/iframeaccesslevelenum/[IframeAccessLevelEnum] + +== Sample request + +The `CreateMeeting` mutation allows authenticated users to create new meetings in a Decidim meetings component. This mutation uses the existing `CreateMeeting` command from the controller. + +[source,graphql] +---- +mutation createMeetings($componentId: ID!, $input: CreateMeetingInput!){ + component(id: $componentId) { + ...on MeetingsMutation{ + createMeeting(input: $input) { + id + title { + translation(locale: "en") + } + description { + translation(locale: "en") + } + address + taxonomies{ + name { + translation(locale: "en") + } + children{ + name{ + translation(locale: "en") + } + } + } + } + } + } +} +---- + +Example of submitted variables + +[source,json] +---- +{ + "componentId": 670, + "input": { + "locale": "en", + "attributes": { + "address": "Carrer de la Pau, 1, Barcelona", + "availableSlots": 10, + "description": "Cities need more people, not more cars", + "endTime": "2025-12-24T22:10:22Z", + "iframeAccessLevel": "ALL", + "iframeEmbedType": "NONE", + "latitude": 40.1234, + "location": "Somewhere", + "locationHints": "Near the main square", + "longitude": 2.1234, + "onlineMeetingUrl": "https://meets.example.org/abc-def", + "registrationTerms": "By registering you agree to the terms and conditions", + "registrationType": "ON_THIS_PLATFORM", + "registrationUrl": "https://example.org/register", + "registrationsEnabled": true, + "startTime": "2025-12-24T20:10:22Z", + "taxonomies": [1072], + "title": "More sidewalks and less roads", + "typeOfMeeting": "ONLINE" + } + } +} +---- + +== Error Handling + +The most frequent errors that can be generated by this mutation are as follows: + +* xref:develop:api/reference/errors/unauthorized_mutation.adoc[Unauthorized Mutation Error - MUTATION_NOT_AUTHORIZED_ERROR] +* xref:develop:api/reference/errors/attribute_validation_error.adoc[Attribute Validation Error - ATTRIBUTE_VALIDATION_ERROR] +* xref:develop:api/reference/errors/unauthorized_object.adoc[Unauthorized Object Error - UNAUTHORIZED_OBJECT_ERROR] From e0ccf7c5db984611145cf2fd57baf2bcabe3db80 Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Fri, 9 Jan 2026 10:26:51 +0200 Subject: [PATCH 053/116] Write API: create proposals (#15790) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Write API: create proposals * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review * Apply suggestions from code review Co-authored-by: Andrés Pereira de Lucena * Remove unneeded require --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Andrés Pereira de Lucena --- .../mutations/answer_proposal_attributes.rb | 2 +- .../api/mutations/create_proposal_type.rb | 52 ++++ .../api/mutations/proposal_attributes.rb | 17 ++ .../api/mutations/proposals_mutation_type.rb | 1 + .../lib/decidim/proposals/api.rb | 2 + .../spec/types/create_proposal_type_spec.rb | 235 ++++++++++++++++++ .../components/proposals/create.adoc | 77 +++++- 7 files changed, 384 insertions(+), 2 deletions(-) create mode 100644 decidim-proposals/lib/decidim/api/mutations/create_proposal_type.rb create mode 100644 decidim-proposals/lib/decidim/api/mutations/proposal_attributes.rb create mode 100644 decidim-proposals/spec/types/create_proposal_type_spec.rb diff --git a/decidim-proposals/lib/decidim/api/mutations/answer_proposal_attributes.rb b/decidim-proposals/lib/decidim/api/mutations/answer_proposal_attributes.rb index 8f1dadd8873dc..5f746c25b2142 100644 --- a/decidim-proposals/lib/decidim/api/mutations/answer_proposal_attributes.rb +++ b/decidim-proposals/lib/decidim/api/mutations/answer_proposal_attributes.rb @@ -3,7 +3,7 @@ module Decidim module Proposals class AnswerProposalAttributes < Decidim::Api::Types::BaseInputObject - graphql_name "ProposalAttributes" + graphql_name "AnswerProposalAttributes" description "Attributes of a proposal" argument :answer_content, GraphQL::Types::JSON, description: "The answer feedback for the status for this proposal", required: false diff --git a/decidim-proposals/lib/decidim/api/mutations/create_proposal_type.rb b/decidim-proposals/lib/decidim/api/mutations/create_proposal_type.rb new file mode 100644 index 0000000000000..2ac3c571ccf51 --- /dev/null +++ b/decidim-proposals/lib/decidim/api/mutations/create_proposal_type.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Decidim + module Proposals + class CreateProposalType < Decidim::Api::Types::BaseMutation + graphql_name "CreateProposal" + + description "Creates a proposal" + type Decidim::Proposals::ProposalType + + argument :attributes, ProposalAttributes, description: "Input attributes for the proposal", required: true + argument :locale, GraphQL::Types::String, "The locale for which to set the proposal texts", required: true + argument :toggle_translations, GraphQL::Types::Boolean, "Whether the user asked to toggle the machine translations or not.", required: false, default_value: false + + def resolve(attributes:, locale:, toggle_translations:) + set_locale(locale:, toggle_translations:) + + params = attributes.to_h.slice(:title, :body, :address, :latitude, :longitude, :taxonomies) + + params[:taxonomies] = Decidim::Taxonomy.where(organization: current_organization, id: params[:taxonomies]).pluck(:id) if params[:taxonomies] + + form = form(Decidim::Proposals::ProposalForm).from_params(params) + + Decidim::Proposals::CreateProposal.call(form, current_user) do + on(:ok) do |proposal| + Decidim::Proposals::PublishProposal.call(proposal, current_user) do + on(:ok) do + return proposal.reload + end + + on(:invalid) do + raise Decidim::Api::Errors::ValidationError, I18n.t("proposals.publish.error", scope: "decidim") + end + end + end + + on(:invalid) do + raise Decidim::Api::Errors::AttributeValidationError, form.errors + end + end + end + + def authorized?(attributes:, locale:, toggle_translations:) + unless super && allowed_to?(:create, :proposal, Decidim::Proposals::Proposal.new(component: current_component), { current_user:, current_component: }) + raise Decidim::Api::Errors::MutationNotAuthorizedError, I18n.t("decidim.api.errors.unauthorized_mutation") + end + + true + end + end + end +end diff --git a/decidim-proposals/lib/decidim/api/mutations/proposal_attributes.rb b/decidim-proposals/lib/decidim/api/mutations/proposal_attributes.rb new file mode 100644 index 0000000000000..21c3a7c38052b --- /dev/null +++ b/decidim-proposals/lib/decidim/api/mutations/proposal_attributes.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Decidim + module Proposals + class ProposalAttributes < Decidim::Api::Types::BaseInputObject + graphql_name "ProposalAttributes" + description "Attributes for creating a proposal" + + argument :address, GraphQL::Types::String, description: "Physical address for the proposal", required: false + argument :body, GraphQL::Types::String, description: "The body content of the proposal", required: true + argument :latitude, GraphQL::Types::Float, description: "Latitude coordinate", required: false + argument :longitude, GraphQL::Types::Float, description: "Longitude coordinate", required: false + argument :taxonomies, [GraphQL::Types::ID], description: "Array of taxonomy IDs", required: false + argument :title, GraphQL::Types::String, description: "The title of the proposal", required: true + end + end +end diff --git a/decidim-proposals/lib/decidim/api/mutations/proposals_mutation_type.rb b/decidim-proposals/lib/decidim/api/mutations/proposals_mutation_type.rb index de6d55b84c450..ea7a2093d622b 100644 --- a/decidim-proposals/lib/decidim/api/mutations/proposals_mutation_type.rb +++ b/decidim-proposals/lib/decidim/api/mutations/proposals_mutation_type.rb @@ -5,6 +5,7 @@ module Proposals class ProposalsMutationType < Decidim::Core::ComponentType description "A proposals of a component." + field :create_proposal, mutation: Decidim::Proposals::CreateProposalType, description: "Creates a proposal" field :proposal, type: Decidim::Proposals::ProposalMutationType, description: "Mutates a proposal", null: true do argument :id, GraphQL::Types::ID, "The ID of the proposal", required: true end diff --git a/decidim-proposals/lib/decidim/proposals/api.rb b/decidim-proposals/lib/decidim/proposals/api.rb index 757f4b8a3799c..95a523048c1b6 100644 --- a/decidim-proposals/lib/decidim/proposals/api.rb +++ b/decidim-proposals/lib/decidim/proposals/api.rb @@ -11,6 +11,8 @@ module Proposals autoload :ProposalMutationType, "decidim/api/mutations/proposal_mutation_type" autoload :ProposalAnswerType, "decidim/api/mutations/proposal_answer_type" autoload :AnswerProposalAttributes, "decidim/api/mutations/answer_proposal_attributes" + autoload :CreateProposalType, "decidim/api/mutations/create_proposal_type" + autoload :ProposalAttributes, "decidim/api/mutations/proposal_attributes" autoload :VoteProposalType, "decidim/api/mutations/vote_proposal_type" autoload :UnvoteProposalType, "decidim/api/mutations/unvote_proposal_type" autoload :WithdrawProposalType, "decidim/api/mutations/withdraw_proposal_type" diff --git a/decidim-proposals/spec/types/create_proposal_type_spec.rb b/decidim-proposals/spec/types/create_proposal_type_spec.rb new file mode 100644 index 0000000000000..e08243d65eac2 --- /dev/null +++ b/decidim-proposals/spec/types/create_proposal_type_spec.rb @@ -0,0 +1,235 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Proposals + describe CreateProposalType, type: :graphql do + include_context "with a graphql class mutation" + + let(:type_class) { Decidim::Proposals::CreateProposalType } + let(:root_klass) { Decidim::Proposals::ProposalsMutationType } + + let(:current_organization) { create(:organization, available_locales: [:en]) } + let(:organization) { current_organization } + let(:participatory_process) { create(:participatory_process, :published, :with_steps, organization:) } + let!(:component) { create(:proposal_component, :published, :with_creation_enabled, participatory_space: participatory_process) } + + let(:root_taxonomy) { create(:taxonomy, organization:) } + let!(:taxonomy) { create(:taxonomy, parent: root_taxonomy, organization:) } + let(:taxonomy_filter) { create(:taxonomy_filter, root_taxonomy:) } + let!(:taxonomy_filter_item) { create(:taxonomy_filter_item, taxonomy_filter:, taxonomy_item: taxonomy) } + let!(:user) { create(:user, :confirmed, organization:) } + + let(:address) { "Carrer de la Pau, 1, Barcelona" } + let(:latitude) { 40.1234 } + let(:longitude) { 2.1234 } + + let(:title) { "More sidewalks and less roads" } + let(:body) { "Cities need more people, not more cars" } + let(:locale) { "en" } + let(:translation_locale) { "en" } + + let(:attributes) do + { + title:, + body:, + address:, + latitude:, + longitude:, + taxonomies: [taxonomy_filter.id] + } + end + + let(:variables) do + { + component_id: component.id, + input: { + locale:, + attributes: + } + } + end + + let(:root_value) { component } + let(:query) do + <<~GRAPHQL + mutation createProposal($input: CreateProposalInput!){ + createProposal(input: $input) { + id + title { translation(locale: "#{translation_locale}") } + body { translation(locale: "#{translation_locale}") } + address + publishedAt + author { name } + } + } + GRAPHQL + end + + before do + stub_geocoding(address, [latitude, longitude]) + end + + context "when creating a new proposal" do + context "when the user is not logged in" do + let(:current_user) { nil } + + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + + context "when the user is logged in" do + context "with creation enabled" do + let!(:component) do + create(:proposal_component, + :published, + :with_creation_enabled, + participatory_space: participatory_process, + settings: { + taxonomy_filters: [taxonomy_filter.id] + }) + end + + it "creates a new proposal" do + proposal_response = response["createProposal"] + + expect(proposal_response).to be_present + expect(proposal_response["title"]["translation"]).to eq(title) + expect(proposal_response["body"]["translation"]).to include(body) + expect(proposal_response["publishedAt"]).to be_present + expect(proposal_response["author"]["name"]).to eq(current_user.name) + end + + context "when submitting in one language and requesting in another" do + let(:locale) { "en" } + let(:translation_locale) { "es" } + + it "creates a new proposal" do + proposal_response = response["createProposal"] + + expect(proposal_response).to be_present + expect(proposal_response["title"]["translation"]).to be_nil + end + end + + context "when geocoding is enabled" do + let!(:component) do + create(:proposal_component, + :with_creation_enabled, + :published, + participatory_space: participatory_process, + settings: { + geocoding_enabled: true, + taxonomy_filters: [taxonomy_filter.id] + }) + end + + it "creates a new proposal" do + proposal_response = response["createProposal"] + + expect(proposal_response).to be_present + expect(proposal_response["title"]["translation"]).to eq(title) + expect(proposal_response["body"]["translation"]).to include(body) + expect(proposal_response["address"]).to eq(address) + expect(proposal_response["publishedAt"]).to be_present + expect(proposal_response["author"]["name"]).to eq(current_user.name) + end + end + + context "when the user is not authorized" do + context "and there is only an authorization required" do + before do + permissions = { + create: { + authorization_handlers: { + "dummy_authorization_handler" => { "options" => {} } + } + } + } + + component.update!(permissions:) + end + + it "throws an error if the user does not have a verification method" do + skip("This test is failing, but it is not in the scope of this PR.") + proposal_response = response["createProposal"] + + expect(proposal_response).to be_nil + end + end + + context "and there are more than one authorization required" do + before do + permissions = { + create: { + authorization_handlers: { + "dummy_authorization_handler" => { "options" => {} }, + "another_dummy_authorization_handler" => { "options" => {} } + } + } + } + + component.update!(permissions:) + end + + it "throws an error if the user does not have a verification method" do + skip("This test is failing, but it is not in the scope of this PR.") + + proposal_response = response["createProposal"] + + expect(proposal_response).to be_nil + end + end + end + end + end + + context "when validating" do + context "with having invalid locale" do + let(:locale) { "tlh" } + + it "raises an error" do + expect { response }.to raise_error(Api::Errors::InvalidLocaleError, /Invalid locale provided/) + end + end + + context "with having invalid title" do + context "when is missing" do + let(:title) { "" } + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::AttributeValidationError, /too short/) + end + end + + context "when is too short" do + let(:title) { "Short" } + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::AttributeValidationError, /too short/) + end + end + end + + context "with having invalid body" do + let(:body) { "Short" } + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::AttributeValidationError, /too short/) + end + end + end + + context "when the creating is disabled" do + let!(:component) { create(:proposal_component, participatory_space: participatory_process) } + + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + end + end + end +end diff --git a/docs/modules/develop/pages/api/reference/components/proposals/create.adoc b/docs/modules/develop/pages/api/reference/components/proposals/create.adoc index d2998e60afc87..1141810411bd2 100644 --- a/docs/modules/develop/pages/api/reference/components/proposals/create.adoc +++ b/docs/modules/develop/pages/api/reference/components/proposals/create.adoc @@ -1,3 +1,78 @@ = Create Proposal -include::admin:partial$under-construction.adoc[] +== Input attributes + +When you want to add or create data in your proposals component, you need to rely on the `ProposalAttributes` type, which has the following input fields: + +[source] +---- +• title (required) - 15-150 characters +• body (required) - minimum 15 characters +• address (optional) - physical address (requires geocoding) +• latitude (optional) - coordinate +• longitude (optional) - coordinate +• taxonomies (optional) - Taxonomies ids to assign to this proposal +---- + +== Sample request + +The `CreateProposal` mutation allows authenticated users to create new proposals in a Decidim proposals component. This mutation delegates to the existing `Decidim::Proposals::CreateProposal` command used by the controllers. + +[source,graphql] +---- +mutation createProposal($componentId: ID!, $input: CreateProposalInput!){ + component(id: $componentId) { + ...on ProposalsMutation{ + createProposal(input: $input) { + id + title { + translation(locale: "en") + } + body { + translation(locale: "en") + } + address + taxonomies{ + name { + translation(locale: "en") + } + children{ + name{ + translation(locale: "en") + } + } + } + } + } + } +} +---- + +Example of submitted variables + +[source,json] +---- +{ + "componentId": 9, + "input": { + "locale": "en", + "attributes": { + "title": "Install Bike Lanes on Main Street", + "body": "We propose adding dedicated bike lanes along Main Street to improve cyclist safety and encourage sustainable transportation.", + "address": "Main Street, Barcelona, Spain", + "latitude": 41.3851, + "longitude": 2.1734 + } + } +} +---- + +== Error Handling + +The most frequent errors that can be generated by this mutation are as follows: + +* xref:develop:api/reference/errors/unauthorized_mutation.adoc[Unauthorized Mutation Error - MUTATION_NOT_AUTHORIZED_ERROR] +* xref:develop:api/reference/errors/invalid_locale_error.adoc[Invalid Locale Error - INVALID_LOCALE_ERROR] +* xref:develop:api/reference/errors/validation_error.adoc[Validation Error - VALIDATION_ERROR] +* xref:develop:api/reference/errors/attribute_validation_error.adoc[Attribute Validation Error - ATTRIBUTE_VALIDATION_ERROR] +* xref:develop:api/reference/errors/unauthorized_object.adoc[Unauthorized Object Error - UNAUTHORIZED_OBJECT_ERROR] From d08f4749dd3e807052be45d8f5a98f2fe9eeb208 Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Fri, 9 Jan 2026 11:15:33 +0200 Subject: [PATCH 054/116] Fix error when there are too many aliases in GraphQL API (#15867) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Configure how many aliases a GraphQL could have * Add error class to interpret the server outcome * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review * Refactor alias list * Fix spelling error * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update decidim-api/lib/decidim/api.rb Co-authored-by: Andrés Pereira de Lucena --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Andrés Pereira de Lucena --- decidim-api/config/locales/en.yml | 1 + decidim-api/lib/decidim/api.rb | 5 ++ decidim-api/lib/decidim/api/alias_analyzer.rb | 23 +++++++ .../api/errors/too_many_aliases_error.rb | 15 +++++ decidim-api/lib/decidim/api/schema.rb | 2 + .../lib/decidim/api/test/type_context.rb | 1 + decidim-api/lib/decidim/api/types.rb | 2 + .../spec/lib/decidim/api/schema_spec.rb | 62 +++++++++++++++++++ .../config/rubocop/graphql/configuration.yml | 1 + .../pages/environment_variables.adoc | 5 ++ 10 files changed, 117 insertions(+) create mode 100644 decidim-api/lib/decidim/api/alias_analyzer.rb create mode 100644 decidim-api/lib/decidim/api/errors/too_many_aliases_error.rb create mode 100644 decidim-api/spec/lib/decidim/api/schema_spec.rb diff --git a/decidim-api/config/locales/en.yml b/decidim-api/config/locales/en.yml index f28fd3c1c73a9..a2d051d61558a 100644 --- a/decidim-api/config/locales/en.yml +++ b/decidim-api/config/locales/en.yml @@ -7,6 +7,7 @@ en: locale_argument_error: There was an error while internally handling i18n data not_found: "%{type} not found" permission_not_set: Permission has not been set for this %{type} + too_many_aliases_error: Too many aliases used. You have used %{size} aliases, but %{limit} are allowed. unauthorized_field: You cannot view or edit %{field} field on %{type} because you do not have permission unauthorized_mutation: You do not have permission to perform this mutation unauthorized_object: You cannot view or edit this %{type} because you do not have permissions diff --git a/decidim-api/lib/decidim/api.rb b/decidim-api/lib/decidim/api.rb index 2cd1d429ce3d6..c73c16934bc6e 100644 --- a/decidim-api/lib/decidim/api.rb +++ b/decidim-api/lib/decidim/api.rb @@ -23,6 +23,11 @@ module Api Decidim::Env.new("API_SCHEMA_MAX_COMPLEXITY", 5000).to_i end + # defines how many aliases are permitted in a query + config_accessor :max_aliases do + Decidim::Env.new("API_SCHEMA_MAX_ALIASES", 5).to_i + end + # defines the schema max_depth to configure GraphQL query max_depth config_accessor :schema_max_depth do Decidim::Env.new("API_SCHEMA_MAX_DEPTH", 15).to_i diff --git a/decidim-api/lib/decidim/api/alias_analyzer.rb b/decidim-api/lib/decidim/api/alias_analyzer.rb new file mode 100644 index 0000000000000..c4d5a2c11103c --- /dev/null +++ b/decidim-api/lib/decidim/api/alias_analyzer.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Decidim + module Api + class AliasAnalyzer < GraphQL::Analysis::AST::Analyzer + def initialize(query) + super + + @aliases = Set.new + end + + def on_enter_field(node, _parent, _visitor) + @aliases.add(node.alias) if node.alias.present? + end + + def result + if @aliases.size > Decidim::Api.max_aliases + Errors::TooManyAliasesError.new(I18n.t("decidim.api.errors.too_many_aliases_error", size: @aliases.size, limit: Decidim::Api.max_aliases)) + end + end + end + end +end diff --git a/decidim-api/lib/decidim/api/errors/too_many_aliases_error.rb b/decidim-api/lib/decidim/api/errors/too_many_aliases_error.rb new file mode 100644 index 0000000000000..167e0a7e285e5 --- /dev/null +++ b/decidim-api/lib/decidim/api/errors/too_many_aliases_error.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Decidim + module Api + module Errors + # i18n-tasks-use t("decidim.api.errors.too_many_aliases_error") + + class TooManyAliasesError < GraphQL::AnalysisError + def to_h + super.merge({ "extensions" => { "code" => "TOO_MANY_ALIASES_ERROR" } }) + end + end + end + end +end diff --git a/decidim-api/lib/decidim/api/schema.rb b/decidim-api/lib/decidim/api/schema.rb index 16903e19b452b..7193f1927f2e3 100644 --- a/decidim-api/lib/decidim/api/schema.rb +++ b/decidim-api/lib/decidim/api/schema.rb @@ -11,6 +11,8 @@ class Schema < GraphQL::Schema max_depth Decidim::Api.schema_max_depth max_complexity Decidim::Api.schema_max_complexity + query_analyzer AliasAnalyzer + orphan_types(Api.orphan_types) def self.unauthorized_object(error) diff --git a/decidim-api/lib/decidim/api/test/type_context.rb b/decidim-api/lib/decidim/api/test/type_context.rb index 0e45659141250..a590ea8713fac 100644 --- a/decidim-api/lib/decidim/api/test/type_context.rb +++ b/decidim-api/lib/decidim/api/test/type_context.rb @@ -43,6 +43,7 @@ def raise_proper_error(error) UNAUTHORIZED_OBJECT_ERROR MUTATION_NOT_AUTHORIZED_ERROR VALIDATION_ERROR + TOO_MANY_ALIASES_ERROR ).include?(code) raise GraphQL::ExecutionError, error["message"] diff --git a/decidim-api/lib/decidim/api/types.rb b/decidim-api/lib/decidim/api/types.rb index 0899324b07f38..b9e108c39d422 100644 --- a/decidim-api/lib/decidim/api/types.rb +++ b/decidim-api/lib/decidim/api/types.rb @@ -2,6 +2,7 @@ module Decidim module Api + autoload :AliasAnalyzer, "decidim/api/alias_analyzer" autoload :QueryType, "decidim/api/query_type" autoload :MutationType, "decidim/api/mutation_type" autoload :Schema, "decidim/api/schema" @@ -11,6 +12,7 @@ module Api module Errors autoload :LocaleError, "decidim/api/errors/locale_error" + autoload :TooManyAliasesError, "decidim/api/errors/too_many_aliases_error" autoload :InvalidLocaleError, "decidim/api/errors/invalid_locale_error" autoload :AttributeValidationError, "decidim/api/errors/attribute_validation_error" autoload :MutationNotAuthorizedError, "decidim/api/errors/mutation_not_authorized_error" diff --git a/decidim-api/spec/lib/decidim/api/schema_spec.rb b/decidim-api/spec/lib/decidim/api/schema_spec.rb new file mode 100644 index 0000000000000..52be74e9e4b8a --- /dev/null +++ b/decidim-api/spec/lib/decidim/api/schema_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require "spec_helper" +require "decidim/api/test" + +module Decidim + module Api + describe Schema do + include_context "with a graphql class type" + let(:type_class) { Decidim::Api::QueryType } + + context "when restricting number of aliases" do + let!(:query) do + %({ + invalidAlias0 : __typename + invalidAlias1 : __typename + invalidAlias2 : __typename + invalidAlias3 : __typename + invalidAlias4 : __typename + invalidAlias5 : __typename + }) + end + + it "raises an error" do + expect { response }.to raise_error(Errors::TooManyAliasesError, "Too many aliases used. You have used 6 aliases, but 5 are allowed.") + end + + context "when using a custom value" do + around do |example| + aliases = Decidim::Api.max_aliases + + # 5 is the default value, we have 6 aliases in the above query definition, and we just set a higher number + Decidim::Api.max_aliases = 10 + example.run + + Decidim::Api.max_aliases = aliases + end + + it "runs successfully" do + expect(response).to include("invalidAlias0" => "Query") + end + end + end + + context "when allowing number of aliases" do + let!(:query) do + %({ + invalidAlias0 : __typename + invalidAlias1 : __typename + invalidAlias2 : __typename + invalidAlias3 : __typename + invalidAlias4 : __typename + }) + end + + it "runs successfully" do + expect(response).to include("invalidAlias0" => "Query") + end + end + end + end +end diff --git a/decidim-dev/config/rubocop/graphql/configuration.yml b/decidim-dev/config/rubocop/graphql/configuration.yml index 11b7860cf4a37..9867c4d7839bc 100644 --- a/decidim-dev/config/rubocop/graphql/configuration.yml +++ b/decidim-dev/config/rubocop/graphql/configuration.yml @@ -18,6 +18,7 @@ GraphQL/ObjectDescription: - "**/lib/decidim/api/graphql_permissions.rb" - "**/lib/decidim/api/functions/*" - "**/lib/decidim/api/errors/*" + - "decidim-api/lib/decidim/api/alias_analyzer.rb" - "spec/**/*" - "test/**/*" diff --git a/docs/modules/configure/pages/environment_variables.adoc b/docs/modules/configure/pages/environment_variables.adoc index bf796cdf8d132..129d468a23fd9 100644 --- a/docs/modules/configure/pages/environment_variables.adoc +++ b/docs/modules/configure/pages/environment_variables.adoc @@ -781,6 +781,11 @@ Read more about this at xref:services:pdf.adoc[PDF signing] service page. | false | No +| API_SCHEMA_MAX_ALIASES +| This Environment variable instructs Decidim how many aliases should be allowed in a GraphQL query +| 5 +| No + |PROPOSALS_PARTICIPATORY_SPACE_HIGHLIGHTED_PROPOSALS_LIMIT |Number of proposals to be shown in blocks with highlighted content across different participatory spaces. |4 From ed13c7c9e5509d269525b158998e514f5fa4cab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Fri, 9 Jan 2026 11:38:07 +0100 Subject: [PATCH 055/116] Rename createMeetings to createMeeting in GraphQL API mutation (#15874) --- .../lib/decidim/api/mutations/create_meeting_type_spec.rb | 2 +- .../pages/api/reference/components/meetings/create.adoc | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/decidim-meetings/spec/lib/decidim/api/mutations/create_meeting_type_spec.rb b/decidim-meetings/spec/lib/decidim/api/mutations/create_meeting_type_spec.rb index 7e3817420157a..d6194a5c703ea 100644 --- a/decidim-meetings/spec/lib/decidim/api/mutations/create_meeting_type_spec.rb +++ b/decidim-meetings/spec/lib/decidim/api/mutations/create_meeting_type_spec.rb @@ -51,7 +51,7 @@ module Meetings let(:root_value) { current_component } let(:query) do <<~GRAPHQL - mutation createMeetings($input: CreateMeetingInput!){ + mutation createMeeting($input: CreateMeetingInput!){ createMeeting(input: $input) { id title { translation(locale: "#{translation_locale}") } diff --git a/docs/modules/develop/pages/api/reference/components/meetings/create.adoc b/docs/modules/develop/pages/api/reference/components/meetings/create.adoc index 071be3b7cbbe8..341775b04c6f1 100644 --- a/docs/modules/develop/pages/api/reference/components/meetings/create.adoc +++ b/docs/modules/develop/pages/api/reference/components/meetings/create.adoc @@ -38,7 +38,7 @@ The `CreateMeeting` mutation allows authenticated users to create new meetings i [source,graphql] ---- -mutation createMeetings($componentId: ID!, $input: CreateMeetingInput!){ +mutation createMeeting($componentId: ID!, $input: CreateMeetingInput!){ component(id: $componentId) { ...on MeetingsMutation{ createMeeting(input: $input) { @@ -46,7 +46,7 @@ mutation createMeetings($componentId: ID!, $input: CreateMeetingInput!){ title { translation(locale: "en") } - description { + description { translation(locale: "en") } address @@ -62,7 +62,7 @@ mutation createMeetings($componentId: ID!, $input: CreateMeetingInput!){ } } } - } + } } ---- From 3aac27fca29431a4296d28a8dd0f5c23b37c2043 Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Fri, 9 Jan 2026 14:11:52 +0200 Subject: [PATCH 056/116] Show promoted space image in the menu (#15860) * Placeholder image isn't shown in the menu * Apply code review requirements * Change aspect ratio from 3/1 to 21/9 --- .../app/packs/stylesheets/decidim/_header.scss | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/decidim-core/app/packs/stylesheets/decidim/_header.scss b/decidim-core/app/packs/stylesheets/decidim/_header.scss index e37d30f6e0d56..a14a6473ad43d 100644 --- a/decidim-core/app/packs/stylesheets/decidim/_header.scss +++ b/decidim-core/app/packs/stylesheets/decidim/_header.scss @@ -125,6 +125,12 @@ header { } } + .card__highlight-img { + svg { + @apply w-full h-full; + } + } + &__dropdown { @apply flex items-end absolute top-0 right-0 z-30 h-screen; @@ -629,7 +635,11 @@ header { @apply flex-col ring-transparent rounded-lg shadow-[1px_4px_8px_3px_rgba(0,0,0,0.15)]; &-img { - @apply w-full aspect-[3/1] rounded-tl-lg rounded-tr-lg; + @apply w-full aspect-[21/9] rounded-none rounded-tl-lg rounded-tr-lg; + + svg { + @apply fill-primary; + } } &-text { From b78b5dd70e1b4f574f5c0ec476228056e0b144fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Fri, 9 Jan 2026 13:19:43 +0100 Subject: [PATCH 057/116] Remove unecessary inclusions of admin assets on initiatives frontend page (#15843) --- .../create_initiative/_share_committee_link.html.erb | 2 -- .../decidim/initiatives/initiatives/_committee_members.html.erb | 2 -- 2 files changed, 4 deletions(-) diff --git a/decidim-initiatives/app/views/decidim/initiatives/create_initiative/_share_committee_link.html.erb b/decidim-initiatives/app/views/decidim/initiatives/create_initiative/_share_committee_link.html.erb index e53dc782cad53..e5bc4af825d35 100644 --- a/decidim-initiatives/app/views/decidim/initiatives/create_initiative/_share_committee_link.html.erb +++ b/decidim-initiatives/app/views/decidim/initiatives/create_initiative/_share_committee_link.html.erb @@ -19,5 +19,3 @@
- -<%= append_javascript_pack_tag "decidim_initiatives_admin" %> diff --git a/decidim-initiatives/app/views/decidim/initiatives/initiatives/_committee_members.html.erb b/decidim-initiatives/app/views/decidim/initiatives/initiatives/_committee_members.html.erb index be6c45acd0fa5..519e5bce9a859 100644 --- a/decidim-initiatives/app/views/decidim/initiatives/initiatives/_committee_members.html.erb +++ b/decidim-initiatives/app/views/decidim/initiatives/initiatives/_committee_members.html.erb @@ -51,5 +51,3 @@
<% end %>
- -<%= append_javascript_pack_tag "decidim_initiatives_admin" %> From bf81bab206e427525a3b4e5bf7125c1565403f88 Mon Sep 17 00:00:00 2001 From: Tom Greenwood <101816158+greenwoodt@users.noreply.github.com> Date: Fri, 9 Jan 2026 13:21:04 +0100 Subject: [PATCH 058/116] Bump tailwindcss from v3.4.1 to v3.4.19 (#15873) --- package-lock.json | 90 ++++++++++++++++++++------------- packages/webpacker/package.json | 2 +- 2 files changed, 55 insertions(+), 37 deletions(-) diff --git a/package-lock.json b/package-lock.json index 15f3d72490f40..3da4a01bdab1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13703,7 +13703,9 @@ } }, "node_modules/jiti": { - "version": "1.21.0", + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -13974,10 +13976,15 @@ } }, "node_modules/lilconfig": { - "version": "2.1.0", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", "license": "MIT", "engines": { - "node": ">=10" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/lines-and-columns": { @@ -15581,7 +15588,9 @@ } }, "node_modules/postcss": { - "version": "8.4.33", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -15598,9 +15607,9 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -16071,13 +16080,6 @@ } } }, - "node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, "node_modules/postcss-loader": { "version": "7.3.4", "license": "MIT", @@ -16200,24 +16202,34 @@ } }, "node_modules/postcss-nested": { - "version": "6.0.1", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "postcss-selector-parser": "^6.0.11" + "postcss-selector-parser": "^6.1.1" }, "engines": { "node": ">=12.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, "peerDependencies": { "postcss": "^8.2.14" } }, "node_modules/postcss-nested/node_modules/postcss-selector-parser": { - "version": "6.0.15", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -18316,7 +18328,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -19254,31 +19268,33 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.1", + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", - "chokidar": "^3.5.3", + "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", - "fast-glob": "^3.3.0", + "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.19.1", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", @@ -19299,7 +19315,9 @@ } }, "node_modules/tailwindcss/node_modules/postcss-selector-parser": { - "version": "6.0.15", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", diff --git a/packages/webpacker/package.json b/packages/webpacker/package.json index 6a6c5d7240557..e02642699448f 100644 --- a/packages/webpacker/package.json +++ b/packages/webpacker/package.json @@ -37,7 +37,7 @@ "shakapacker": "~8.3.0", "source-map-loader": "^4.0.1", "style-loader": "^3.3.3", - "tailwindcss": "^3.4.1", + "tailwindcss": "^3.4.19", "terser-webpack-plugin": "^5.3.9", "webpack": "^5.88.1", "webpack-assets-manifest": "^5.1.0", From 3be49f7d92c8e170e2d87f0ab28ae2cfcea3ac39 Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Fri, 9 Jan 2026 15:03:10 +0200 Subject: [PATCH 059/116] Disable introspection for regular users (#15865) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Disable introspection for regular users * Add introspection specs * Fix specs * refactor the specs * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Add spec for not logged in user * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Andrés Pereira de Lucena * Revert include_examples for a better approach * Apply suggestions from code review Co-authored-by: Andrés Pereira de Lucena * Apply code review recommendations --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Andrés Pereira de Lucena --- .../decidim/api/queries_controller.rb | 3 +- decidim-api/config/locales/en.yml | 1 + decidim-api/lib/decidim/api.rb | 9 +- .../lib/decidim/api/decidim_introspection.rb | 51 +++++++++++ .../errors/introspection_disabled_error.rb | 14 ++++ decidim-api/lib/decidim/api/schema.rb | 1 + .../lib/decidim/api/test/component_context.rb | 1 + .../lib/decidim/api/test/type_context.rb | 84 ++++++++++++++++++- decidim-api/lib/decidim/api/types.rb | 2 + .../controllers/queries_controller_spec.rb | 4 +- .../spec/types/integration_schema_spec.rb | 1 + .../spec/types/integration_schema_spec.rb | 1 + .../config/rubocop/graphql/configuration.yml | 1 + .../spec/types/integration_schema_spec.rb | 1 + .../spec/types/integration_schema_spec.rb | 1 + .../pages/environment_variables.adoc | 7 +- 16 files changed, 176 insertions(+), 6 deletions(-) create mode 100644 decidim-api/lib/decidim/api/decidim_introspection.rb create mode 100644 decidim-api/lib/decidim/api/errors/introspection_disabled_error.rb diff --git a/decidim-api/app/controllers/decidim/api/queries_controller.rb b/decidim-api/app/controllers/decidim/api/queries_controller.rb index cd9c810063808..1b7e2a5b38da9 100644 --- a/decidim-api/app/controllers/decidim/api/queries_controller.rb +++ b/decidim-api/app/controllers/decidim/api/queries_controller.rb @@ -30,7 +30,8 @@ def context { current_organization:, current_user: api_user, - scopes: api_scopes + scopes: api_scopes, + can_introspect: Decidim::Api.enable_anonymous_introspection || api_user&.admin? } end diff --git a/decidim-api/config/locales/en.yml b/decidim-api/config/locales/en.yml index a2d051d61558a..c173fa06debae 100644 --- a/decidim-api/config/locales/en.yml +++ b/decidim-api/config/locales/en.yml @@ -3,6 +3,7 @@ en: decidim: api: errors: + introspection_disabled: Introspection is disabled for this request invalid_locale: Invalid locale provided locale_argument_error: There was an error while internally handling i18n data not_found: "%{type} not found" diff --git a/decidim-api/lib/decidim/api.rb b/decidim-api/lib/decidim/api.rb index c73c16934bc6e..11f9beec80ec2 100644 --- a/decidim-api/lib/decidim/api.rb +++ b/decidim-api/lib/decidim/api.rb @@ -37,12 +37,19 @@ module Api Decidim::Env.new("DECIDIM_API_DISCLOSE_SYSTEM_VERSION").present? end - # Public Setting that can make the API authentication necessary in order to + # makes the API authentication necessary in order to access it # access it. config_accessor :force_api_authentication do Decidim::Env.new("DECIDIM_API_FORCE_API_AUTHENTICATION", nil).present? end + # allows anonymous introspection queries + # If you are not sure, leave it set to false. In this way only administrator users will be able to access the introspection query. + # Otherwise, anyone can access it, causing security issues. + config_accessor :enable_anonymous_introspection do + Decidim::Env.new("DECIDIM_API_ENABLE_ANONYMOUS_INTROSPECTION", nil).present? + end + # The expiration time of the JWT tokens, after which issued token will # expire. Recommended to match the value of # `DECIDIM_OAUTH_ACCESS_TOKEN_EXPIRES_IN`. diff --git a/decidim-api/lib/decidim/api/decidim_introspection.rb b/decidim-api/lib/decidim/api/decidim_introspection.rb new file mode 100644 index 0000000000000..136c7043040d4 --- /dev/null +++ b/decidim-api/lib/decidim/api/decidim_introspection.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Decidim + module Api + module DecidimIntrospection + module FieldVisibility + extend ActiveSupport::Concern + + included do + def self.visible?(context) + raise Errors::IntrospectionDisabledError, I18n.t("decidim.api.errors.introspection_disabled") unless context[:can_introspect] == true + + super + end + end + end + + class SchemaType < GraphQL::Introspection::SchemaType + include FieldVisibility + end + + class TypeType < GraphQL::Introspection::TypeType + include FieldVisibility + end + + class DirectiveType < GraphQL::Introspection::DirectiveType + include FieldVisibility + end + + class DirectiveLocationEnum < GraphQL::Introspection::DirectiveLocationEnum + include FieldVisibility + end + + class EnumValueType < GraphQL::Introspection::EnumValueType + include FieldVisibility + end + + class FieldType < GraphQL::Introspection::FieldType + include FieldVisibility + end + + class InputValueType < GraphQL::Introspection::InputValueType + include FieldVisibility + end + + class TypeKindEnum < GraphQL::Introspection::TypeKindEnum + include FieldVisibility + end + end + end +end diff --git a/decidim-api/lib/decidim/api/errors/introspection_disabled_error.rb b/decidim-api/lib/decidim/api/errors/introspection_disabled_error.rb new file mode 100644 index 0000000000000..13a497a194695 --- /dev/null +++ b/decidim-api/lib/decidim/api/errors/introspection_disabled_error.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Decidim + module Api + module Errors + # i18n-tasks-use t("decidim.api.errors.introspection_disabled") + class IntrospectionDisabledError < GraphQL::ExecutionError + def to_h + super.merge({ "extensions" => { "code" => "INTROSPECTION_DISABLED_ERROR" } }) + end + end + end + end +end diff --git a/decidim-api/lib/decidim/api/schema.rb b/decidim-api/lib/decidim/api/schema.rb index 7193f1927f2e3..f8151b7e6dcc7 100644 --- a/decidim-api/lib/decidim/api/schema.rb +++ b/decidim-api/lib/decidim/api/schema.rb @@ -4,6 +4,7 @@ module Decidim module Api # Main GraphQL schema for decidim's API. class Schema < GraphQL::Schema + introspection(DecidimIntrospection) mutation(MutationType) query(QueryType) diff --git a/decidim-api/lib/decidim/api/test/component_context.rb b/decidim-api/lib/decidim/api/test/component_context.rb index 9bf033f2af518..9a4b389b35342 100644 --- a/decidim-api/lib/decidim/api/test/component_context.rb +++ b/decidim-api/lib/decidim/api/test/component_context.rb @@ -2,6 +2,7 @@ shared_context "with a graphql decidim component" do include_context "with a graphql class type" + include_examples "when the introspection is disabled" let(:schema) { Decidim::Api::Schema } diff --git a/decidim-api/lib/decidim/api/test/type_context.rb b/decidim-api/lib/decidim/api/test/type_context.rb index a590ea8713fac..5756e79025bf0 100644 --- a/decidim-api/lib/decidim/api/test/type_context.rb +++ b/decidim-api/lib/decidim/api/test/type_context.rb @@ -15,6 +15,7 @@ let(:type_class) { described_class } let(:variables) { {} } let(:root_value) { model } + let(:can_introspect) { Decidim::Api.enable_anonymous_introspection || current_user&.admin? } let(:schema) do klass = type_class @@ -44,6 +45,7 @@ def raise_proper_error(error) MUTATION_NOT_AUTHORIZED_ERROR VALIDATION_ERROR TOO_MANY_ALIASES_ERROR + INTROSPECTION_DISABLED_ERROR ).include?(code) raise GraphQL::ExecutionError, error["message"] @@ -57,7 +59,8 @@ def execute_query(query, variables) current_organization:, current_user:, current_component:, - scopes: api_scopes + scopes: api_scopes, + can_introspect: }, variables: ) @@ -68,6 +71,85 @@ def execute_query(query, variables) end end +shared_examples "when the introspection is disabled" do + shared_examples "check introspection behavior" do + context "and the user is not authenticated" do + let!(:current_user) { nil } + + it "raises an Decidim::Api::Errors::IntrospectionDisabledError" do + expect { response }.to raise_error(Decidim::Api::Errors::IntrospectionDisabledError, "Introspection is disabled for this request") + end + end + + context "and the user is not an admin" do + let!(:current_user) { create(:user, :confirmed, organization: current_organization) } + + it "raises an Decidim::Api::Errors::IntrospectionDisabledError" do + expect { response }.to raise_error(Decidim::Api::Errors::IntrospectionDisabledError, "Introspection is disabled for this request") + end + end + + context "and the user is an admin" do + let!(:current_user) { create(:user, :confirmed, :admin, organization: current_organization) } + + it "runs successfully" do + expect { response }.not_to raise_error + end + end + + context "and the setting is true" do + before do + allow(Decidim::Api).to receive(:enable_anonymous_introspection).and_return(true) + end + + it "runs successfully" do + expect { response }.not_to raise_error + end + end + + context "and the setting is false" do + before do + allow(Decidim::Api).to receive(:enable_anonymous_introspection).and_return(false) + end + it "raises an Decidim::Api::Errors::IntrospectionDisabledError" do + expect { response }.to raise_error(Decidim::Api::Errors::IntrospectionDisabledError, "Introspection is disabled for this request") + end + end + end + + context "when requesting the schema introspection" do + let(:query) do + %( query { __schema { types { fields { type { fields { type { fields { type { fields { type { name } } } } } } } } } } } ) + end + + it_behaves_like "check introspection behavior" + end + + context "when requesting the type introspection" do + let(:query) do + %( query CircularIntrospection { + __type(name: "User") { + fields { + type { + fields { + type { + fields { + type { + name + } + } + } + } + } + } + } +} ) + end + + it_behaves_like "check introspection behavior" + end +end + shared_context "with a graphql scalar class type" do include_context "with a graphql class type" diff --git a/decidim-api/lib/decidim/api/types.rb b/decidim-api/lib/decidim/api/types.rb index b9e108c39d422..f25b286ad1e2d 100644 --- a/decidim-api/lib/decidim/api/types.rb +++ b/decidim-api/lib/decidim/api/types.rb @@ -2,6 +2,7 @@ module Decidim module Api + autoload :DecidimIntrospection, "decidim/api/decidim_introspection" autoload :AliasAnalyzer, "decidim/api/alias_analyzer" autoload :QueryType, "decidim/api/query_type" autoload :MutationType, "decidim/api/mutation_type" @@ -11,6 +12,7 @@ module Api autoload :ComponentMutationType, "decidim/api/component_mutation_type" module Errors + autoload :IntrospectionDisabledError, "decidim/api/errors/introspection_disabled_error" autoload :LocaleError, "decidim/api/errors/locale_error" autoload :TooManyAliasesError, "decidim/api/errors/too_many_aliases_error" autoload :InvalidLocaleError, "decidim/api/errors/invalid_locale_error" diff --git a/decidim-api/spec/controllers/queries_controller_spec.rb b/decidim-api/spec/controllers/queries_controller_spec.rb index c872dce37154c..d2c2eb65fbf3a 100644 --- a/decidim-api/spec/controllers/queries_controller_spec.rb +++ b/decidim-api/spec/controllers/queries_controller_spec.rb @@ -29,10 +29,10 @@ module Api end it "executes a query" do - post :create, params: { query: "{ __schema { queryType { name } } }" } + post :create, params: { query: "{ organization { name { translations { locale text } } } }" } parsed_response = JSON.parse(response.body)["data"] - expect(parsed_response["__schema"]["queryType"]["name"]).to eq("Query") + expect(parsed_response["organization"]["name"]["translations"]).to include("locale" => "en", "text" => translated(organization.name)) end context "with force sign in enabled" do diff --git a/decidim-assemblies/spec/types/integration_schema_spec.rb b/decidim-assemblies/spec/types/integration_schema_spec.rb index 2762812f16ab5..506a9bf3b697c 100644 --- a/decidim-assemblies/spec/types/integration_schema_spec.rb +++ b/decidim-assemblies/spec/types/integration_schema_spec.rb @@ -191,6 +191,7 @@ ) end + include_examples "when the introspection is disabled" describe "valid query" do it "executes successfully" do expect { response }.not_to raise_error diff --git a/decidim-conferences/spec/types/integration_schema_spec.rb b/decidim-conferences/spec/types/integration_schema_spec.rb index 3e3ae4df8daf6..532f084d93929 100644 --- a/decidim-conferences/spec/types/integration_schema_spec.rb +++ b/decidim-conferences/spec/types/integration_schema_spec.rb @@ -131,6 +131,7 @@ ) end + include_examples "when the introspection is disabled" describe "valid query" do it "executes successfully" do expect { response }.not_to raise_error diff --git a/decidim-dev/config/rubocop/graphql/configuration.yml b/decidim-dev/config/rubocop/graphql/configuration.yml index 9867c4d7839bc..76a99a52eb481 100644 --- a/decidim-dev/config/rubocop/graphql/configuration.yml +++ b/decidim-dev/config/rubocop/graphql/configuration.yml @@ -19,6 +19,7 @@ GraphQL/ObjectDescription: - "**/lib/decidim/api/functions/*" - "**/lib/decidim/api/errors/*" - "decidim-api/lib/decidim/api/alias_analyzer.rb" + - "decidim-api/lib/decidim/api/decidim_introspection.rb" - "spec/**/*" - "test/**/*" diff --git a/decidim-initiatives/spec/types/integration_schema_spec.rb b/decidim-initiatives/spec/types/integration_schema_spec.rb index 8f52953a84abb..bf4641c52b459 100644 --- a/decidim-initiatives/spec/types/integration_schema_spec.rb +++ b/decidim-initiatives/spec/types/integration_schema_spec.rb @@ -161,6 +161,7 @@ ) end + include_examples "when the introspection is disabled" describe "valid query" do it "executes successfully" do expect { response }.not_to raise_error diff --git a/decidim-participatory_processes/spec/types/integration_schema_spec.rb b/decidim-participatory_processes/spec/types/integration_schema_spec.rb index 19a894b8cb90e..4f5fd46dcad8d 100644 --- a/decidim-participatory_processes/spec/types/integration_schema_spec.rb +++ b/decidim-participatory_processes/spec/types/integration_schema_spec.rb @@ -245,6 +245,7 @@ ) end + include_examples "when the introspection is disabled" describe "valid query" do it "executes successfully" do expect { response }.not_to raise_error diff --git a/docs/modules/configure/pages/environment_variables.adoc b/docs/modules/configure/pages/environment_variables.adoc index 129d468a23fd9..3c038d4a037bb 100644 --- a/docs/modules/configure/pages/environment_variables.adoc +++ b/docs/modules/configure/pages/environment_variables.adoc @@ -777,7 +777,12 @@ Read more about this at xref:services:pdf.adoc[PDF signing] service page. |No | DECIDIM_API_DISCLOSE_SYSTEM_VERSION -| This Environment variable instructs decidim whether to display or not the exact version of the application. By default, this is set to false to avoid explicit exposure of Decidim version. +| Instructs the system whether to display or not the exact version of the application. By default, this is set to false to avoid explicit exposure of Decidim version. +| false +| No + +| DECIDIM_API_ENABLE_ANONYMOUS_INTROSPECTION +| Allows the system to accept introspection queries, which can lead to a security issue. We recommend avoiding enabling this variable in production environments. When it is disabled, only organization admin users will be able to run introspection queries. | false | No From ded12f879971244521aa61a432e39dcd62acda92 Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Fri, 9 Jan 2026 16:54:51 +0200 Subject: [PATCH 060/116] Write API: close meetings (#15798) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Write API: close meetings * Fix code suggestions from Copilot * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply code suggestions to validate attendee_count * Running rubocops * Remove unneeded require * Apply suggestions from code review Co-authored-by: Andrés Pereira de Lucena --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Andrés Pereira de Lucena --- .../api/mutations/close_meeting_attributes.rb | 17 ++ .../api/mutations/close_meeting_type.rb | 57 +++++ .../api/mutations/meeting_mutation_type.rb | 1 + decidim-meetings/lib/decidim/meetings/api.rb | 2 + .../spec/types/close_meeting_type_spec.rb | 211 ++++++++++++++++++ .../reference/components/meetings/close.adoc | 47 +++- 6 files changed, 334 insertions(+), 1 deletion(-) create mode 100644 decidim-meetings/lib/decidim/api/mutations/close_meeting_attributes.rb create mode 100644 decidim-meetings/lib/decidim/api/mutations/close_meeting_type.rb create mode 100644 decidim-meetings/spec/types/close_meeting_type_spec.rb diff --git a/decidim-meetings/lib/decidim/api/mutations/close_meeting_attributes.rb b/decidim-meetings/lib/decidim/api/mutations/close_meeting_attributes.rb new file mode 100644 index 0000000000000..adcccb0589a0b --- /dev/null +++ b/decidim-meetings/lib/decidim/api/mutations/close_meeting_attributes.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Decidim + module Meetings + class CloseMeetingAttributes < Decidim::Api::Types::BaseInputObject + graphql_name "CloseMeetingAttributes" + description "Attributes for closing a meeting" + + argument :attendees_count, GraphQL::Types::Int, description: "Number of attendees", required: true + argument :closed_at, Decidim::Core::DateTimeType, + description: "Optional custom date and time when the meeting was closed. If omitted, the meeting will be closed at the current time (as in the UI). +This field is only available via the API to support integrations (for example, importing or synchronizing meetings with externally managed closure times).", required: false + argument :closing_report, GraphQL::Types::String, description: "The closing report for the meeting", required: true + argument :proposal_ids, [GraphQL::Types::ID], description: "IDs of proposals to link to the meeting", required: false + end + end +end diff --git a/decidim-meetings/lib/decidim/api/mutations/close_meeting_type.rb b/decidim-meetings/lib/decidim/api/mutations/close_meeting_type.rb new file mode 100644 index 0000000000000..f73c74b8f120c --- /dev/null +++ b/decidim-meetings/lib/decidim/api/mutations/close_meeting_type.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Decidim + module Meetings + class CloseMeetingType < Decidim::Api::Types::BaseMutation + graphql_name "CloseMeeting" + + description "Closes a meeting" + type Decidim::Meetings::MeetingType + + argument :attributes, CloseMeetingAttributes, description: "Input attributes for closing a meeting", required: true + argument :locale, GraphQL::Types::String, "The locale to use for the mutation", required: true + argument :toggle_translations, GraphQL::Types::Boolean, "Whether the user asked to toggle the machine translations or not", required: false, default_value: false + + def resolve(attributes:, locale:, toggle_translations:) + set_locale(locale:, toggle_translations:) + + closing_report = attributes.to_h.fetch(:closing_report, object.closing_report) + attendees_count = attributes.to_h.fetch(:attendees_count, object.attendees_count) + proposal_ids = Array(attributes.to_h.fetch(:proposal_ids, [])).map(&:to_i) + closed_at = attributes.to_h.fetch(:closed_at, Time.current) + + params = { + closing_report:, + attendees_count:, + proposal_ids:, + closed_at:, + proposals: object.sibling_scope(:proposals) + } + + form = form(Decidim::Meetings::CloseMeetingForm).from_params(params) + + CloseMeeting.call(form, object) do + on(:ok) do + return object.reload + end + + on(:invalid) do + raise Decidim::Api::Errors::AttributeValidationError, form.errors + end + end + end + + def authorized?(attributes:, locale:, toggle_translations:) + raise Decidim::Api::Errors::MutationNotAuthorizedError, I18n.t("decidim.api.errors.unauthorized_mutation") unless [ + super, + allowed_to?(:close, :meeting, object, context), + object.published?, + object.closed? == false, + object.withdrawn? == false + ].all? + + true + end + end + end +end diff --git a/decidim-meetings/lib/decidim/api/mutations/meeting_mutation_type.rb b/decidim-meetings/lib/decidim/api/mutations/meeting_mutation_type.rb index e9db4dfd12833..ec5479cf49189 100644 --- a/decidim-meetings/lib/decidim/api/mutations/meeting_mutation_type.rb +++ b/decidim-meetings/lib/decidim/api/mutations/meeting_mutation_type.rb @@ -8,6 +8,7 @@ class MeetingMutationType < Decidim::Api::Types::BaseObject graphql_name "MeetingMutation" description "A meeting which includes its available mutations" + field :close, mutation: Decidim::Meetings::CloseMeetingType, description: "Closes a meeting" field :withdraw, mutation: Decidim::Meetings::WithdrawMeetingType, description: "Withdraws a meeting" end end diff --git a/decidim-meetings/lib/decidim/meetings/api.rb b/decidim-meetings/lib/decidim/meetings/api.rb index 4e273e1ab0b58..e869fb1feb94f 100644 --- a/decidim-meetings/lib/decidim/meetings/api.rb +++ b/decidim-meetings/lib/decidim/meetings/api.rb @@ -14,6 +14,8 @@ module Meetings autoload :MeetingsMutationType, "decidim/api/mutations/meetings_mutation_type" autoload :MeetingMutationType, "decidim/api/mutations/meeting_mutation_type" + autoload :CloseMeetingType, "decidim/api/mutations/close_meeting_type" + autoload :CloseMeetingAttributes, "decidim/api/mutations/close_meeting_attributes" autoload :WithdrawMeetingType, "decidim/api/mutations/withdraw_meeting_type" autoload :RegistrationTypeEnum, "decidim/api/registration_type_enum" diff --git a/decidim-meetings/spec/types/close_meeting_type_spec.rb b/decidim-meetings/spec/types/close_meeting_type_spec.rb new file mode 100644 index 0000000000000..83246555eceab --- /dev/null +++ b/decidim-meetings/spec/types/close_meeting_type_spec.rb @@ -0,0 +1,211 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Meetings + describe CloseMeetingType, type: :graphql do + include_context "with a graphql class mutation" + + let(:root_klass) { MeetingMutationType } + let(:organization) { current_organization } + let(:participatory_process) { create(:participatory_process, :with_steps, organization:) } + let(:meetings_component) { create(:meeting_component, participatory_space: participatory_process) } + let(:author) { current_user } + let!(:model) { create(:meeting, :published, component: meetings_component, author:, end_time:, start_time:) } + let(:attendees_count) { 10 } + let(:closing_report) { "My meeting closing report" } + let(:component) { model.component } + let(:proposal_ids) { [] } + let(:locale) { "en" } + let(:end_time) { 1.day.ago } + let(:start_time) { 2.days.ago } + + let(:variables) do + { + input: { + locale:, + attributes: { + closingReport: closing_report, + attendeesCount: attendees_count, + proposalIds: proposal_ids + } + } + } + end + + let(:query) do + <<~GRAPHQL + mutation($input: CloseMeetingInput!) { + close(input: $input) { + id + closed + attendeesCount + closingReport { translation(locale: "en") } + closedAt + } + } + GRAPHQL + end + + shared_examples "manage meeting mutation examples" do + context "when meeting is in the future" do + let(:end_time) { 1.day.from_now } + + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + + context "when meeting can be closed" do + it "closes the meeting" do + close = response["close"] + expect(close).to be_present + expect(close).to include( + { + "id" => model.id.to_s, + "closed" => true, + "attendeesCount" => attendees_count, + "closingReport" => { + "translation" => closing_report + }, + "closedAt" => model.reload.closed_at.to_time.iso8601 + } + ) + expect(model.reload).to be_closed + end + + context "with linked proposals" do + let!(:proposals_component) { create(:proposal_component, participatory_space: participatory_process) } + let!(:proposal1) { create(:proposal, component: proposals_component) } + let!(:proposal2) { create(:proposal, component: proposals_component) } + let!(:proposal_ids) { [proposal1.id, proposal2.id].map(&:to_s) } + + it "closes the meeting and links proposals" do + close = response["close"] + expect(close).to be_present + + expect(model.reload).to be_closed + + # Verify proposals are linked + expect(model.reload.linked_resources(:proposals, "proposals_from_meeting").pluck(:id)).to match_array(proposal_ids.collect(&:to_i)) + end + end + end + end + + context "with admin user" do + it_behaves_like "manage meeting mutation examples" do + let!(:user_type) { :admin } + end + end + + context "with normal user" do + it_behaves_like "manage meeting mutation examples" + end + + context "with api_user" do + it_behaves_like "manage meeting mutation examples" do + let!(:user_type) { :api_user } + end + end + + context "when validating" do + context "with issues on attendees_count" do + context "when the value is not sent" do + let(:variables) do + { + input: { + locale:, + attributes: { + closingReport: closing_report, + proposalIds: proposal_ids + } + } + } + end + + it "raises an error" do + expect { response }.to raise_error(::GraphQL::ExecutionError, /Expected value to not be null/) + end + end + + context "when the value is not an integer" do + let(:attendees_count) { "not_an_integer" } + + it "raises an error" do + expect { response }.to raise_error(::GraphQL::ExecutionError, /Could not coerce value "not_an_integer" to Int/) + end + end + + context "when the value is less than 0" do + let(:attendees_count) { -1 } + + it "raises an error" do + expect { response }.to raise_error(Api::Errors::AttributeValidationError, /must be greater than or equal to 0/) + end + end + + context "when the value is larger than 1000" do + let(:attendees_count) { 1000 } + + it "raises an error" do + expect { response }.to raise_error(Api::Errors::AttributeValidationError, /must be less than or equal to 999/) + end + end + end + + context "with an invalid locale" do + let(:locale) { "tlh" } + + it "raises an error" do + expect { response }.to raise_error(Api::Errors::InvalidLocaleError, /Invalid locale provided/) + end + end + + context "when having an invalid closing_report" do + context "when is missing" do + let(:closing_report) { "" } + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::AttributeValidationError, /cannot be blank/) + end + end + end + end + + context "when anonymous user" do + let(:current_user) { nil } + let(:author) { create(:user, :confirmed, organization: current_organization) } + + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + + context "when meeting is not published" do + let!(:model) { create(:meeting, component: meetings_component, author:, end_time:, start_time:) } + + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + + context "when meeting is closed" do + let!(:model) { create(:meeting, :published, :closed_with_minutes, component: meetings_component, author:) } + + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + + context "when meeting is withdrawn" do + let!(:model) { create(:meeting, :withdrawn, :closed_with_minutes, component: meetings_component, author:) } + + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + end + end +end diff --git a/docs/modules/develop/pages/api/reference/components/meetings/close.adoc b/docs/modules/develop/pages/api/reference/components/meetings/close.adoc index aa9e1e9a12452..d59c74f43a8d9 100644 --- a/docs/modules/develop/pages/api/reference/components/meetings/close.adoc +++ b/docs/modules/develop/pages/api/reference/components/meetings/close.adoc @@ -1,3 +1,48 @@ = Close Meeting -include::admin:partial$under-construction.adoc[] +To use the API to close any meeting that the user is authorized to close, you will need to use the following API request. + +[source,graphql] +---- +mutation closeMeeting($componentId: ID!, $meetingId: ID!, $input: CloseMeetingInput!){ + component(id: $componentId) { + ...on MeetingsMutation{ + meeting(id: $meetingId) { + close(input: $input) { + id + closed + attendeesCount + closingReport { translation(locale: "en") } + closedAt + } + } + } + } +} +---- + +Example of submitted variables + +[source,json] +---- +{ + "componentId": "9", + "meetingId": "2", + "input": { + "locale": "en", + "attributes": { + "closingReport": "Your closing report", + "attendeesCount": 12, + "proposalIds": ["1","2","3"] + } + } +} +---- + +== Error Handling + +The most frequent errors that can be generated by this mutation are as follows: + +* xref:develop:api/reference/errors/unauthorized_mutation.adoc[Unauthorized Mutation Error - MUTATION_NOT_AUTHORIZED_ERROR] +* xref:develop:api/reference/errors/attribute_validation_error.adoc[Attribute Validation Error - ATTRIBUTE_VALIDATION_ERROR] +* xref:develop:api/reference/errors/unauthorized_object.adoc[Unauthorized Object Error - UNAUTHORIZED_OBJECT_ERROR] From 9b1dc8aae1b5144d9b063c7b662975e70bcf5bcb Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Mon, 12 Jan 2026 11:22:42 +0200 Subject: [PATCH 061/116] Avoid GraphQL circular query (recursion limit) (#15866) * Avoid GraphQL Circular Query * Normalize locales * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix specs * Add individual spec * Rename DecidimIntrspection to IntrospectionAnalyzer * Apply review request recommendations * Fix locales --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- decidim-api/config/locales/en.yml | 1 + .../errors/recursion_limit_exceeded_error.rb | 14 +++ ...ospection.rb => introspection_analyzer.rb} | 2 +- .../lib/decidim/api/recursion_analyzer.rb | 74 ++++++++++++++++ decidim-api/lib/decidim/api/schema.rb | 7 +- .../lib/decidim/api/test/type_context.rb | 9 +- decidim-api/lib/decidim/api/types.rb | 4 +- .../decidim/api/recursion_analyzer_spec.rb | 85 +++++++++++++++++++ .../config/rubocop/graphql/configuration.yml | 4 +- .../spec/types/integration_schema_spec.rb | 29 +++++++ 10 files changed, 217 insertions(+), 12 deletions(-) create mode 100644 decidim-api/lib/decidim/api/errors/recursion_limit_exceeded_error.rb rename decidim-api/lib/decidim/api/{decidim_introspection.rb => introspection_analyzer.rb} (97%) create mode 100644 decidim-api/lib/decidim/api/recursion_analyzer.rb create mode 100644 decidim-api/spec/lib/decidim/api/recursion_analyzer_spec.rb diff --git a/decidim-api/config/locales/en.yml b/decidim-api/config/locales/en.yml index c173fa06debae..547206d45b7f9 100644 --- a/decidim-api/config/locales/en.yml +++ b/decidim-api/config/locales/en.yml @@ -8,6 +8,7 @@ en: locale_argument_error: There was an error while internally handling i18n data not_found: "%{type} not found" permission_not_set: Permission has not been set for this %{type} + recursion_limit_exceeded_error: Too many recursions detected in query too_many_aliases_error: Too many aliases used. You have used %{size} aliases, but %{limit} are allowed. unauthorized_field: You cannot view or edit %{field} field on %{type} because you do not have permission unauthorized_mutation: You do not have permission to perform this mutation diff --git a/decidim-api/lib/decidim/api/errors/recursion_limit_exceeded_error.rb b/decidim-api/lib/decidim/api/errors/recursion_limit_exceeded_error.rb new file mode 100644 index 0000000000000..37daaf161b4e6 --- /dev/null +++ b/decidim-api/lib/decidim/api/errors/recursion_limit_exceeded_error.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Decidim + module Api + module Errors + # i18n-tasks-use t("decidim.api.errors.recursion_limit_exceeded_error") + class RecursionLimitExceededError < GraphQL::AnalysisError + def to_h + super.merge({ "extensions" => { "code" => "RECURSION_LIMIT_EXCEEDED_ERROR" } }) + end + end + end + end +end diff --git a/decidim-api/lib/decidim/api/decidim_introspection.rb b/decidim-api/lib/decidim/api/introspection_analyzer.rb similarity index 97% rename from decidim-api/lib/decidim/api/decidim_introspection.rb rename to decidim-api/lib/decidim/api/introspection_analyzer.rb index 136c7043040d4..bb71a209053bf 100644 --- a/decidim-api/lib/decidim/api/decidim_introspection.rb +++ b/decidim-api/lib/decidim/api/introspection_analyzer.rb @@ -2,7 +2,7 @@ module Decidim module Api - module DecidimIntrospection + module IntrospectionAnalyzer module FieldVisibility extend ActiveSupport::Concern diff --git a/decidim-api/lib/decidim/api/recursion_analyzer.rb b/decidim-api/lib/decidim/api/recursion_analyzer.rb new file mode 100644 index 0000000000000..471c7a9b9c18b --- /dev/null +++ b/decidim-api/lib/decidim/api/recursion_analyzer.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +# This analyzer checks for too many recursions in GraphQL queries. +# Copyright (c) GitLab B.V. +# License: MIT Expat license +# This content of the class was copied from the GitLab repository +# @see https://gitlab.com/gitlab-org/gitlab/-/blob/f59f7aa0d86f07496e68abf7172edd703669e7bd/lib/gitlab/graphql/query_analyzers/ast/recursion_analyzer.rb +# To which I have modified the result format to be compatible with decidim-api. + +module Decidim + module Api + class RecursionAnalyzer < GraphQL::Analysis::AST::Analyzer + IGNORED_FIELDS = %w(node edges nodes ofType).freeze + RECURSION_THRESHOLD = 2 + + def initialize(query) + super + + @node_visits = {} + @recurring_fields = {} + end + + def on_enter_field(node, _parent, visitor) + return if skip_node?(node, visitor) + + node_name = node.name + node_visits[node_name] ||= 0 + node_visits[node_name] += 1 + + times_encountered = @node_visits[node_name] + recurring_fields[node_name] = times_encountered if recursion_too_deep?(node_name, times_encountered) + end + + # Visitors are all defined on the AST::Analyzer base class + # We override them for custom analyzers. + def on_leave_field(node, _parent, visitor) + return if skip_node?(node, visitor) + + node_name = node.name + node_visits[node_name] ||= 0 + node_visits[node_name] -= 1 + end + + def result + @recurring_fields = @recurring_fields.select { |k, v| recursion_too_deep?(k, v) } + + Decidim::Api::Errors::RecursionLimitExceededError.new I18n.t("decidim.api.errors.recursion_limit_exceeded_error") if @recurring_fields.any? + end + + private + + attr_reader :node_visits, :recurring_fields + + def recursion_too_deep?(node_name, times_encountered) + return false if IGNORED_FIELDS.include?(node_name) + + times_encountered > recursion_threshold + end + + def skip_node?(node, visitor) + # We do not want to count skipped fields or fields + # inside fragment definitions + return false if visitor.skipping? || visitor.visiting_fragment_definition? + + !node.is_a?(GraphQL::Language::Nodes::Field) || node.selections.empty? + end + + # Separated into a method to allow overriding or customization of the recursion limit. + def recursion_threshold + RECURSION_THRESHOLD + end + end + end +end diff --git a/decidim-api/lib/decidim/api/schema.rb b/decidim-api/lib/decidim/api/schema.rb index f8151b7e6dcc7..1bed442722f2a 100644 --- a/decidim-api/lib/decidim/api/schema.rb +++ b/decidim-api/lib/decidim/api/schema.rb @@ -4,16 +4,17 @@ module Decidim module Api # Main GraphQL schema for decidim's API. class Schema < GraphQL::Schema - introspection(DecidimIntrospection) mutation(MutationType) query(QueryType) + introspection(IntrospectionAnalyzer) + query_analyzer RecursionAnalyzer + query_analyzer AliasAnalyzer + default_max_page_size Decidim::Api.schema_max_per_page max_depth Decidim::Api.schema_max_depth max_complexity Decidim::Api.schema_max_complexity - query_analyzer AliasAnalyzer - orphan_types(Api.orphan_types) def self.unauthorized_object(error) diff --git a/decidim-api/lib/decidim/api/test/type_context.rb b/decidim-api/lib/decidim/api/test/type_context.rb index 5756e79025bf0..a25a4806733ad 100644 --- a/decidim-api/lib/decidim/api/test/type_context.rb +++ b/decidim-api/lib/decidim/api/test/type_context.rb @@ -46,6 +46,7 @@ def raise_proper_error(error) VALIDATION_ERROR TOO_MANY_ALIASES_ERROR INTROSPECTION_DISABLED_ERROR + RECURSION_LIMIT_EXCEEDED_ERROR ).include?(code) raise GraphQL::ExecutionError, error["message"] @@ -119,7 +120,7 @@ def execute_query(query, variables) context "when requesting the schema introspection" do let(:query) do - %( query { __schema { types { fields { type { fields { type { fields { type { fields { type { name } } } } } } } } } } } ) + %( query { __schema { types { fields { type { fields { type { name } } } } } } } ) end it_behaves_like "check introspection behavior" @@ -133,11 +134,7 @@ def execute_query(query, variables) type { fields { type { - fields { - type { - name - } - } + name } } } diff --git a/decidim-api/lib/decidim/api/types.rb b/decidim-api/lib/decidim/api/types.rb index f25b286ad1e2d..74e5e0e9cd0d6 100644 --- a/decidim-api/lib/decidim/api/types.rb +++ b/decidim-api/lib/decidim/api/types.rb @@ -2,8 +2,9 @@ module Decidim module Api - autoload :DecidimIntrospection, "decidim/api/decidim_introspection" + autoload :IntrospectionAnalyzer, "decidim/api/introspection_analyzer" autoload :AliasAnalyzer, "decidim/api/alias_analyzer" + autoload :RecursionAnalyzer, "decidim/api/recursion_analyzer" autoload :QueryType, "decidim/api/query_type" autoload :MutationType, "decidim/api/mutation_type" autoload :Schema, "decidim/api/schema" @@ -23,6 +24,7 @@ module Errors autoload :UnauthorizedFieldError, "decidim/api/errors/unauthorized_field_error" autoload :UnauthorizedObjectError, "decidim/api/errors/unauthorized_object_error" autoload :ValidationError, "decidim/api/errors/validation_error" + autoload :RecursionLimitExceededError, "decidim/api/errors/recursion_limit_exceeded_error" end module Types diff --git a/decidim-api/spec/lib/decidim/api/recursion_analyzer_spec.rb b/decidim-api/spec/lib/decidim/api/recursion_analyzer_spec.rb new file mode 100644 index 0000000000000..33e8f3595c7e1 --- /dev/null +++ b/decidim-api/spec/lib/decidim/api/recursion_analyzer_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require "spec_helper" +require "decidim/api/test" + +module Decidim + module Api + module FooBar + class DummyModel + def initialize(id) + @id = id + end + + attr_reader :id + + def child + self.class.new(id + 1) + end + + def parent + self.class.new(id + 1) + end + end + + class DummyType < GraphQL::Schema::Object + description "Dummy type" + field :child, ::Decidim::Api::FooBar::DummyType, "Dummy child field" + field :id, Integer, "Dummy ID", null: false + field :parent, ::Decidim::Api::FooBar::DummyType, "Dummy parent field" + end + + class Query < GraphQL::Schema::Object + description "The query root of this schema" + field :parent, ::Decidim::Api::FooBar::DummyType, "Dummy field", null: false + + def parent + DummyModel.new(0) + end + end + end + + describe RecursionAnalyzer do + include_context "with a graphql class type" do + let(:schema) do + Class.new(GraphQL::Schema) do + query ::Decidim::Api::FooBar::Query + query_analyzer Decidim::Api::RecursionAnalyzer + end + end + end + + let(:model) do + Decidim::Api::FooBar::DummyModel + end + + context "when a recursion is detected" do + let!(:query) do + %( + query { + parent { child { parent { child { parent { child { id } } } } } } + } + ) + end + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::RecursionLimitExceededError) + end + end + + context "when a recursion is not detected" do + let!(:query) do + %( + query { + parent { child { parent { child { id } } } } + } + ) + end + + it "raises an error" do + expect { response }.not_to raise_error + end + end + end + end +end diff --git a/decidim-dev/config/rubocop/graphql/configuration.yml b/decidim-dev/config/rubocop/graphql/configuration.yml index 76a99a52eb481..af48798184769 100644 --- a/decidim-dev/config/rubocop/graphql/configuration.yml +++ b/decidim-dev/config/rubocop/graphql/configuration.yml @@ -19,7 +19,9 @@ GraphQL/ObjectDescription: - "**/lib/decidim/api/functions/*" - "**/lib/decidim/api/errors/*" - "decidim-api/lib/decidim/api/alias_analyzer.rb" - - "decidim-api/lib/decidim/api/decidim_introspection.rb" + - "decidim-api/lib/decidim/api/introspection_analyzer.rb" + - "decidim-api/lib/decidim/api/recursion_analyzer.rb" + - "decidim-api/spec/lib/decidim/api/recursion_analyzer_spec.rb" - "spec/**/*" - "test/**/*" diff --git a/decidim-meetings/spec/types/integration_schema_spec.rb b/decidim-meetings/spec/types/integration_schema_spec.rb index a0865a503d358..27f21ab6b8307 100644 --- a/decidim-meetings/spec/types/integration_schema_spec.rb +++ b/decidim-meetings/spec/types/integration_schema_spec.rb @@ -220,6 +220,35 @@ end end + context "when query exceeds recursion threshold" do + let(:component_fragment) do + %( + fragment fooComponent on Meetings { + meeting(id: #{meeting.id}){ + agenda { + items { + agenda { + items { + agenda { + items { id } + } + } + } + } + } + } + } + ) + end + + it "raises error Decidim::Api::Errors::TooManyRecursionsError" do + expect { response }.to raise_error( + Decidim::Api::Errors::RecursionLimitExceededError, + I18n.t("decidim.api.errors.recursion_limit_exceeded_error") + ) + end + end + describe "valid connection query" do let(:component_fragment) do %( From c7effe183def134bbe0454d505fc9c78124943c8 Mon Sep 17 00:00:00 2001 From: Tom Greenwood <101816158+greenwoodt@users.noreply.github.com> Date: Mon, 12 Jan 2026 14:45:03 +0100 Subject: [PATCH 062/116] Replace image processing with imagemagick to libvips (#15670) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Andrés Pereira de Lucena Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Alexandru Emil Lupu --- .github/actions/spelling/expect.txt | 1 + .github/workflows/ci_generators.yml | 4 ++ .../ci_performance_metrics_monitoring.yml | 4 ++ .github/workflows/ci_production_check.yml | 4 ++ .github/workflows/test_app.yml | 4 ++ Gemfile.lock | 11 ++-- RELEASE_NOTES.md | 19 +++++- .../decidim/organization_favicon_uploader.rb | 13 ++-- .../uploader_image_dimensions_validator.rb | 20 +++--- decidim-core/decidim-core.gemspec | 2 +- decidim-core/lib/decidim/core/engine.rb | 7 -- .../controllers/favicon_controller_spec.rb | 2 +- .../spec/models/decidim/attachment_spec.rb | 2 +- .../spec/models/decidim/organization_spec.rb | 66 +++++-------------- decidim-core/spec/system/editor_spec.rb | 23 ++++--- .../organization_favicon_uploader_spec.rb | 4 +- ...ploader_image_dimensions_validator_spec.rb | 45 ++++++------- decidim-generators/Gemfile.lock | 11 ++-- .../system/amendable/amend_proposal_spec.rb | 1 + .../spec/system/proposal_show_spec.rb | 1 + docs/modules/install/pages/manual.adoc | 3 +- 21 files changed, 118 insertions(+), 129 deletions(-) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index e1d7d086eb790..f4ffadced5a2c 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -467,6 +467,7 @@ libpq libreadline libreoffice libretranslate +libvips libyaml Liddel lighthouserc diff --git a/.github/workflows/ci_generators.yml b/.github/workflows/ci_generators.yml index 47a62c64943e7..e5a4bb759b829 100644 --- a/.github/workflows/ci_generators.yml +++ b/.github/workflows/ci_generators.yml @@ -80,6 +80,10 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 1 + - run: | + sudo apt-get update && sudo apt-get install libvips libvips-tools + name: Install libvips + shell: "bash" - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ env.RUBY_VERSION }} diff --git a/.github/workflows/ci_performance_metrics_monitoring.yml b/.github/workflows/ci_performance_metrics_monitoring.yml index 0374d66465636..f4c8596d19a81 100644 --- a/.github/workflows/ci_performance_metrics_monitoring.yml +++ b/.github/workflows/ci_performance_metrics_monitoring.yml @@ -64,6 +64,10 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 1 + - run: | + sudo apt-get update && sudo apt-get install libvips libvips-tools + name: Install libvips + shell: "bash" - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ env.RUBY_VERSION }} diff --git a/.github/workflows/ci_production_check.yml b/.github/workflows/ci_production_check.yml index cd151012b8923..0850a2569659a 100644 --- a/.github/workflows/ci_production_check.yml +++ b/.github/workflows/ci_production_check.yml @@ -54,6 +54,10 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 1 + - run: | + sudo apt-get update && sudo apt-get install libvips libvips-tools + name: Install libvips + shell: "bash" - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ env.RUBY_VERSION }} diff --git a/.github/workflows/test_app.yml b/.github/workflows/test_app.yml index 69849a2b5b2f5..94783b8815a5f 100644 --- a/.github/workflows/test_app.yml +++ b/.github/workflows/test_app.yml @@ -70,6 +70,10 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 1 + - run: | + sudo apt-get update && sudo apt-get install libvips libvips-tools + name: Install libvips + shell: "bash" - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ inputs.ruby_version }} diff --git a/Gemfile.lock b/Gemfile.lock index 9a7d810772349..f45053a0bb009 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -83,7 +83,6 @@ PATH kaminari (~> 1.2, >= 1.2.1) loofah (~> 2.19, >= 2.19.1) mime-types (>= 1.16, < 4.0) - mini_magick (~> 4.9) net-smtp (~> 0.5.0) nokogiri (~> 1.16, >= 1.16.2) omniauth (~> 2.0) @@ -104,6 +103,7 @@ PATH redis (~> 4.1) request_store (~> 1.7.0) rqrcode (~> 2.2.0) + ruby-vips (~> 2.2) rubyXL (~> 3.4) rubyzip (~> 2.0) shakapacker (~> 8.3.0) @@ -492,8 +492,8 @@ GEM ice_cube (~> 0.16) ostruct ice_cube (0.17.0) - image_processing (1.13.0) - mini_magick (>= 4.9.5, < 5) + image_processing (1.14.0) + mini_magick (>= 4.9.5, < 6) ruby-vips (>= 2.0.17, < 3) invisible_captcha (0.13.0) rails (>= 3.2.0) @@ -547,7 +547,8 @@ GEM logger mime-types-data (~> 3.2015) mime-types-data (3.2025.0722) - mini_magick (4.13.2) + mini_magick (5.3.1) + logger mini_mime (1.1.5) minitest (5.25.5) msgpack (1.8.0) @@ -808,7 +809,7 @@ GEM rubocop (~> 1.72) yard ruby-progressbar (1.13.0) - ruby-vips (2.2.4) + ruby-vips (2.2.5) ffi (~> 1.12) logger rubyXL (3.4.33) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 32d3a4f13b831..63207c34cd006 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -32,6 +32,7 @@ gem "decidim-dev", github: "decidim/decidim" ### 1.3. Run these commands ```console +sudo apt install libvips libvips-tools # or the alternative installation process for your operating system. See "3.5. Replace image processing with imagemagick to libvips" bundle update decidim bin/rails decidim:upgrade bin/rails db:migrate @@ -175,7 +176,23 @@ Back in [#15534](https://github.com/decidim/decidim/pull/15534) we upgraded webp You can read more about this change on PR [#15534](https://github.com/decidim/decidim/pull/15534), [#15674](https://github.com/decidim/decidim/pull/15674). -### 3.5. [[TITLE OF THE ACTION]] +### 3.5. Replace ImageMagick with libvips for image processing + +We have upgraded our image processor within the application to libvips for speed and low memory usage. + +Support for `.ico` favicon files has been removed. Applications that relied on ICO favicons must migrate to one of the supported Libvips image formats. + +In order to install please run the following command: + +```bash +sudo apt install libvips libvips-tools +``` + +This works for Ubuntu Linux, other operating systems would need to do other command/package. + +You can read more about this change on PR [#15670](https://github.com/decidim/decidim/pull/15670). + +### 3.6. [[TITLE OF THE ACTION]] You can read more about this change on PR [#XXXX](https://github.com/decidim/decidim/pull/XXXX). diff --git a/decidim-core/app/uploaders/decidim/organization_favicon_uploader.rb b/decidim-core/app/uploaders/decidim/organization_favicon_uploader.rb index 7778bb3f4cb27..68f62e9326957 100644 --- a/decidim-core/app/uploaders/decidim/organization_favicon_uploader.rb +++ b/decidim-core/app/uploaders/decidim/organization_favicon_uploader.rb @@ -7,7 +7,8 @@ class OrganizationFaviconUploader < ImageUploader huge: 512, big: 192, medium: 180, - small: 32 + small: 32, + favicon: 256 }.freeze set_variants do @@ -16,17 +17,11 @@ class OrganizationFaviconUploader < ImageUploader resize_and_pad: [value, value], format: :png } - end.merge( - favicon: { - resize_and_pad: [256, 256], - define: "icon:auto-resize=16,24,32,48,64,72,96,128,256", - format: :ico - } - ) + end end def extension_allowlist - %w(png jpg jpeg webp ico) + %w(png jpg jpeg webp) end end end diff --git a/decidim-core/app/validators/uploader_image_dimensions_validator.rb b/decidim-core/app/validators/uploader_image_dimensions_validator.rb index bfd26fab16349..9a9af8d49ddaa 100644 --- a/decidim-core/app/validators/uploader_image_dimensions_validator.rb +++ b/decidim-core/app/validators/uploader_image_dimensions_validator.rb @@ -3,7 +3,7 @@ # This validator checks when the files to be uploaded are images and the attached uploader's # has enabled dimensions validation that the image dimensions are below the # limit defined by the uploader -require "mini_magick" +require "vips" class UploaderImageDimensionsValidator < ActiveModel::Validations::FileContentTypeValidator def validate_each(record, attribute, value) @@ -32,14 +32,12 @@ def validate_image_size(record, attribute, file, uploader) # avoid reckless users that upload images with too many pixels. # # See https://hackerone.com/reports/390 - record.errors.add attribute, I18n.t("decidim.errors.files.file_resolution_too_large") if image.dimensions.any? { |dimension| dimension > uploader.max_image_height_or_width } - rescue MiniMagick::Error + max_dimension = [image.width, image.height].max + record.errors.add attribute, I18n.t("decidim.errors.files.file_resolution_too_large") if max_dimension > uploader.max_image_height_or_width + rescue Vips::Error # The error may happen because of many reasons but most commonly the image - # exceeds the default maximum dimensions set for ImageMagick when the - # `identify` command fails to identify the image. - # - # To relax ImageMagick default limits, please refer to: - # https://imagemagick.org/script/security-policy.php + # exceeds the default maximum dimensions set for libvips when the image + # fails to load. # # Note that the error can also happen because of other reasons than only # the image dimensions being too large. But as we do not really know the @@ -51,11 +49,11 @@ def extract_image(file) return unless file.try(:content_type).to_s.start_with?("image") if uploaded_file?(file) - MiniMagick::Image.new(file.path, File.extname(file.original_filename)) + Vips::Image.new_from_file(file.path) elsif file.is_a?(ActiveStorage::Attached) && file.blob.persisted? - MiniMagick::Image.read(file.blob.download, File.extname(file.blob.filename.to_s)) + Vips::Image.new_from_buffer(file.blob.download, "") end - rescue ActiveStorage::FileNotFoundError, MiniMagick::Invalid + rescue ActiveStorage::FileNotFoundError, Vips::Error # Although the blob is persisted, the file is not available to download and analyze # after committing the record nil diff --git a/decidim-core/decidim-core.gemspec b/decidim-core/decidim-core.gemspec index c0b85c30f2b5a..4655b70fa8670 100644 --- a/decidim-core/decidim-core.gemspec +++ b/decidim-core/decidim-core.gemspec @@ -59,7 +59,6 @@ Gem::Specification.new do |s| s.add_dependency "kaminari", "~> 1.2", ">= 1.2.1" s.add_dependency "loofah", "~> 2.19", ">= 2.19.1" s.add_dependency "mime-types", ">= 1.16", "< 4.0" - s.add_dependency "mini_magick", "~> 4.9" s.add_dependency "net-smtp", "~> 0.5.0" s.add_dependency "nokogiri", "~> 1.16", ">= 1.16.2" s.add_dependency "omniauth", "~> 2.0" @@ -80,6 +79,7 @@ Gem::Specification.new do |s| s.add_dependency "redis", "~> 4.1" s.add_dependency "request_store", "~> 1.7.0" s.add_dependency "rqrcode", "~> 2.2.0" + s.add_dependency "ruby-vips", "~> 2.2" s.add_dependency "rubyXL", "~> 3.4" s.add_dependency "rubyzip", "~> 2.0" s.add_dependency "shakapacker", "~> 8.3.0" diff --git a/decidim-core/lib/decidim/core/engine.rb b/decidim-core/lib/decidim/core/engine.rb index ffa5b3726c457..abdcabc70de93 100644 --- a/decidim-core/lib/decidim/core/engine.rb +++ b/decidim-core/lib/decidim/core/engine.rb @@ -241,13 +241,6 @@ class Engine < ::Rails::Engine ENV["SHAKAPACKER_CONFIG"] = Decidim::Shakapacker.configuration.configuration_file end - # Rails 7.0 default is vips, but - # The `:mini_magick` option is not deprecated; it is fine to keep using it. - # And we are going to use it while migrating rails application - initializer "decidim_core.active_storage_variant_processor" do |app| - app.config.active_storage.variant_processor = :mini_magick - end - initializer "decidim_core.setup_i18n" do |app| app.config.i18n.available_locales = Decidim.available_locales app.config.i18n.default_locale = Decidim.default_locale diff --git a/decidim-core/spec/controllers/favicon_controller_spec.rb b/decidim-core/spec/controllers/favicon_controller_spec.rb index 3502eec6088dd..6e09f8aa5fd6e 100644 --- a/decidim-core/spec/controllers/favicon_controller_spec.rb +++ b/decidim-core/spec/controllers/favicon_controller_spec.rb @@ -36,7 +36,7 @@ module Decidim end context "when the organization has a favicon" do - let(:favicon_path) { Decidim::Dev.asset("icon.ico") } + let(:favicon_path) { Decidim::Dev.asset("icon.png") } before do organization.favicon.attach(io: File.open(favicon_path), filename: File.basename(favicon_path)) diff --git a/decidim-core/spec/models/decidim/attachment_spec.rb b/decidim-core/spec/models/decidim/attachment_spec.rb index 5686bb0498dd0..403cb428efd89 100644 --- a/decidim-core/spec/models/decidim/attachment_spec.rb +++ b/decidim-core/spec/models/decidim/attachment_spec.rb @@ -47,7 +47,7 @@ module Decidim it "shows the correct error" do expect(subject.valid?).to be(false) - expect(subject.errors[:file]).to contain_exactly("File cannot be processed") + expect(subject.errors[:file]).to contain_exactly("File resolution is too large") end end end diff --git a/decidim-core/spec/models/decidim/organization_spec.rb b/decidim-core/spec/models/decidim/organization_spec.rb index 965c9bbfa7e0c..23cc58360278e 100644 --- a/decidim-core/spec/models/decidim/organization_spec.rb +++ b/decidim-core/spec/models/decidim/organization_spec.rb @@ -180,14 +180,6 @@ module Decidim expect(subject.favicon_ico.blob).to be_a(ActiveStorage::Blob) end end - - context "when the favicon is image/vnd.microsoft.icon" do - let(:favicon_path) { Decidim::Dev.asset("icon.ico") } - - it "returns the favicon itself" do - expect(subject.favicon_ico).to be(subject.favicon) - end - end end describe "favicon variants" do @@ -198,9 +190,8 @@ module Decidim end let(:image) do - MiniMagick::Image.open( - ActiveStorage::Blob.service.path_for(variant.key), - File.extname(variant.filename.to_s) + Vips::Image.new_from_file( + ActiveStorage::Blob.service.path_for(variant.key) ) end end @@ -216,37 +207,9 @@ module Decidim context "with favicon variant" do include_context "with processed variant", :favicon - let(:identified) { image.identify.split("\n") } - - it "creates the correct ICO variant" do - expect(variant.content_type).to eq("image/vnd.microsoft.icon") - expect(variant.filename.to_s).to eq("#{File.basename(favicon_path, ".*")}.ico") - end - - it "converts all ICO sizes" do - # Example output: - # /tmp/mini_magick20220101-123456-abcdef.ico[0] ICO 16x16 16x16+0+0 8-bit sRGB 0.020u 0:00.000 - # /tmp/mini_magick20220101-123456-abcdef.ico[1] ICO 24x24 24x24+0+0 8-bit sRGB 0.010u 0:00.000 - # /tmp/mini_magick20220101-123456-abcdef.ico[2] ICO 32x32 32x32+0+0 8-bit sRGB 0.000u 0:00.000 - # /tmp/mini_magick20220101-123456-abcdef.ico[3] ICO 48x48 48x48+0+0 8-bit sRGB 0.000u 0:00.000 - # /tmp/mini_magick20220101-123456-abcdef.ico[4] ICO 64x64 64x64+0+0 8-bit sRGB 0.000u 0:00.000 - # /tmp/mini_magick20220101-123456-abcdef.ico[5] ICO 72x72 72x72+0+0 8-bit sRGB 0.000u 0:00.000 - # /tmp/mini_magick20220101-123456-abcdef.ico[6] ICO 96x96 96x96+0+0 8-bit sRGB 0.000u 0:00.000 - # /tmp/mini_magick20220101-123456-abcdef.ico[7] ICO 128x128 128x128+0+0 8-bit sRGB 0.000u 0:00.000 - # /tmp/mini_magick20220101-123456-abcdef.ico[8] PNG 256x256 256x256+0+0 8-bit sRGB 179438B 0.000u 0:00.000 - [ - /\.ico\[0\] ICO 16x16 16x16\+0\+0 8-bit sRGB 0.[0-9]{3}u 0:[0-9]{2}\.[0-9]{3}$/, - /\.ico\[1\] ICO 24x24 24x24\+0\+0 8-bit sRGB 0.[0-9]{3}u 0:[0-9]{2}\.[0-9]{3}$/, - /\.ico\[2\] ICO 32x32 32x32\+0\+0 8-bit sRGB 0.[0-9]{3}u 0:[0-9]{2}\.[0-9]{3}$/, - /\.ico\[3\] ICO 48x48 48x48\+0\+0 8-bit sRGB 0.[0-9]{3}u 0:[0-9]{2}\.[0-9]{3}$/, - /\.ico\[4\] ICO 64x64 64x64\+0\+0 8-bit sRGB 0.[0-9]{3}u 0:[0-9]{2}\.[0-9]{3}$/, - /\.ico\[5\] ICO 72x72 72x72\+0\+0 8-bit sRGB 0.[0-9]{3}u 0:[0-9]{2}\.[0-9]{3}$/, - /\.ico\[6\] ICO 96x96 96x96\+0\+0 8-bit sRGB 0.[0-9]{3}u 0:[0-9]{2}\.[0-9]{3}$/, - /\.ico\[7\] ICO 128x128 128x128\+0\+0 8-bit sRGB 0.[0-9]{3}u 0:[0-9]{2}\.[0-9]{3}$/, - /\.ico\[8\] PNG 256x256 256x256\+0\+0 8-bit sRGB 179438B 0.[0-9]{3}u 0:[0-9]{2}\.[0-9]{3}$/ - ].each_with_index do |regexp, idx| - expect(identified[idx]).to match(regexp) - end + it "creates the correct png variant" do + expect(variant.content_type).to eq("image/png") + expect(variant.filename.to_s).to eq("#{File.basename(favicon_path, ".*")}.png") end end @@ -259,21 +222,22 @@ module Decidim end it "converts it the image correct dimensions" do - expect(image.dimensions).to eq([32, 32]) + expect(image.width).to eq(32) + expect(image.height).to eq(32) end end - end - context "with an ICO file" do - let(:favicon_path) { Decidim::Dev.asset("icon.ico") } + context "with a HEIC format" do + let(:favicon_path) { Decidim::Dev.asset("icon.HEIC") } - it_behaves_like "creates correct favicon variants" - end + it_behaves_like "creates correct favicon variants" + end - context "with a PNG file" do - let(:favicon_path) { Decidim::Dev.asset("icon.png") } + context "with a JPEG format" do + let(:favicon_path) { Decidim::Dev.asset("avatar.jpeg") } - it_behaves_like "creates correct favicon variants" + it_behaves_like "creates correct favicon variants" + end end end diff --git a/decidim-core/spec/system/editor_spec.rb b/decidim-core/spec/system/editor_spec.rb index 9f76c6dcec7de..91d958e63a912 100644 --- a/decidim-core/spec/system/editor_spec.rb +++ b/decidim-core/spec/system/editor_spec.rb @@ -1119,7 +1119,9 @@ def protect_against_forgery? context "when resizing an image" do let(:image) { create(:editor_image, organization:) } let(:image_src) { image.attached_uploader(:file).path } - let(:dimensions) { MiniMagick::Image.read(image.file.blob.download).dimensions } + let(:vips_image) { Vips::Image.new_from_buffer(image.file.blob.download, "") } + let(:width) { vips_image.width } + let(:height) { vips_image.height } let(:editor_content) do <<~HTML
@@ -1137,30 +1139,27 @@ def protect_against_forgery? context "with right side controls" do it "allows resizing the image" do drag("[data-image-resizer-control='top-right']", mode:, direction: "left", amount: 100) - expect_value(%(
Test
)) + expect_value(%(
Test
)) drag("[data-image-resizer-control='bottom-right']", mode:, direction: "right", amount: 50) - expect_value(%(
Test
)) + expect_value(%(
Test
)) end it "removes the width attribute when resizing back to original width or above it" do drag("[data-image-resizer-control='top-right']", mode:, direction: "left", amount: 100) - expect_value(%(
Test
)) + expect_value(%(
Test
)) drag("[data-image-resizer-control='bottom-right']", mode:, direction: "right", amount: 100) expect_value(%(
Test
)) drag("[data-image-resizer-control='top-right']", mode:, direction: "left", amount: 100) - expect_value(%(
Test
)) + expect_value(%(
Test
)) drag("[data-image-resizer-control='bottom-right']", mode:, direction: "right", amount: 500) expect_value(%(
Test
)) end it "shows and updates image sizes" do - width = dimensions[0] - height = dimensions[1] - expect(page).to have_css("[data-image-resizer-dimension-value='#{width}']", visible: :all) expect(page).to have_css("[data-image-resizer-dimension-value='#{height}']", visible: :all) @@ -1173,21 +1172,21 @@ def protect_against_forgery? context "with left side controls" do it "allows resizing the image" do drag("[data-image-resizer-control='bottom-left']", mode:, direction: "right", amount: 100) - expect_value(%(
Test
)) + expect_value(%(
Test
)) drag("[data-image-resizer-control='top-left']", mode:, direction: "left", amount: 50) - expect_value(%(
Test
)) + expect_value(%(
Test
)) end it "removes the width attribute when resizing back to original width or above it" do drag("[data-image-resizer-control='bottom-left']", mode:, direction: "right", amount: 100) - expect_value(%(
Test
)) + expect_value(%(
Test
)) drag("[data-image-resizer-control='top-left']", mode:, direction: "left", amount: 100) expect_value(%(
Test
)) drag("[data-image-resizer-control='bottom-left']", mode:, direction: "right", amount: 100) - expect_value(%(
Test
)) + expect_value(%(
Test
)) drag("[data-image-resizer-control='top-left']", mode:, direction: "left", amount: 500) expect_value(%(
Test
)) diff --git a/decidim-core/spec/uploaders/organization_favicon_uploader_spec.rb b/decidim-core/spec/uploaders/organization_favicon_uploader_spec.rb index d0c487688f8fa..0d89c764b86d3 100644 --- a/decidim-core/spec/uploaders/organization_favicon_uploader_spec.rb +++ b/decidim-core/spec/uploaders/organization_favicon_uploader_spec.rb @@ -14,7 +14,7 @@ module Decidim subject { uploader.extension_allowlist } it "returns the custom allowed extensions" do - expect(subject).to eq(%w(png jpg jpeg webp ico)) + expect(subject).to eq(%w(png jpg jpeg webp)) end end @@ -22,7 +22,7 @@ module Decidim subject { uploader.content_type_allowlist } it "returns the correct MIME types" do - expect(subject).to eq(%w(image/png image/jpeg image/webp image/vnd.microsoft.icon)) + expect(subject).to eq(%w(image/png image/jpeg image/webp)) end end end diff --git a/decidim-core/spec/validators/uploader_image_dimensions_validator_spec.rb b/decidim-core/spec/validators/uploader_image_dimensions_validator_spec.rb index d95ec7824a8e7..2385f34db7be5 100644 --- a/decidim-core/spec/validators/uploader_image_dimensions_validator_spec.rb +++ b/decidim-core/spec/validators/uploader_image_dimensions_validator_spec.rb @@ -41,13 +41,13 @@ def self.attached_config expect(subject.attached_uploader(:upload).validable_dimensions).to be(true) end - # Ensure MiniMagic is called so that the validations are actually run for + # Ensure Vips is called so that the validations are actually run for # the same reason as above. - it "calls MiniMagick" do + it "calls Vips" do if type == :blob - expect(MiniMagick::Image).to receive(:read).and_call_original + expect(Vips::Image).to receive(:read).and_call_original else - expect(MiniMagick::Image).to receive(:new).and_call_original + expect(Vips::Image).to receive(:new).and_call_original end subject.valid? @@ -67,19 +67,6 @@ def self.attached_config it_behaves_like "valid image type" end - - context "with an ICO" do - let(:filename) { "icon.ico" } - let(:content_type) { "image/vnd.microsoft.icon" } - - it_behaves_like "valid image type" - - context "without an extension" do - let(:blob_filename) { "icon_ico" } - - it_behaves_like "valid image type" - end - end end context "when the file is an Rack::Test::UploadedFile" do @@ -149,15 +136,29 @@ def upload=(blob) let(:upload) { Decidim::Dev.test_file("avatar.jpg", "image/jpeg") } let(:uploader) { record.attached_uploader(:upload) } - context "when MiniMagick fails to process the image" do + context "when image dimensions exceed maximum allowed" do + let(:image) { instance_double(Vips::Image, width: 10_000, height: 8_000) } + + before do + allow(Vips::Image).to receive(:new_from_file).and_return(image) + allow(uploader).to receive(:max_image_height_or_width).and_return(5_000) + end + + it "adds the correct error" do + expect { subject }.not_to raise_error + expect(record.errors[:upload]).to contain_exactly("File resolution is too large") + end + end + + context "when Vips fails to process the image" do let(:image) do - MiniMagick::Image.new(upload.path, File.extname(upload.original_filename)) + Vips::Image.new_from_file(upload.path) end before do - allow(MiniMagick::Image).to receive(:new).and_return(image) - allow(image).to receive(:dimensions).and_raise( - MiniMagick::Error.new( + allow(Vips::Image).to receive(:new_from_file).and_return(image) + allow(image).to receive(:width).and_raise( + Vips::Error.new( <<~ERR.strip identify-im6.q16: unable to open image `%w': No such file or directory @ error/blob.c/OpenBlob/2924. identify-im6.q16: no decode delegate for this image format `' @ error/constitute.c/ReadImage/575. diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index 9e6e228b876aa..65c17283c45ad 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -83,7 +83,6 @@ PATH kaminari (~> 1.2, >= 1.2.1) loofah (~> 2.19, >= 2.19.1) mime-types (>= 1.16, < 4.0) - mini_magick (~> 4.9) net-smtp (~> 0.5.0) nokogiri (~> 1.16, >= 1.16.2) omniauth (~> 2.0) @@ -104,6 +103,7 @@ PATH redis (~> 4.1) request_store (~> 1.7.0) rqrcode (~> 2.2.0) + ruby-vips (~> 2.2) rubyXL (~> 3.4) rubyzip (~> 2.0) shakapacker (~> 8.3.0) @@ -488,8 +488,8 @@ GEM ice_cube (~> 0.16) ostruct ice_cube (0.17.0) - image_processing (1.13.0) - mini_magick (>= 4.9.5, < 5) + image_processing (1.14.0) + mini_magick (>= 4.9.5, < 6) ruby-vips (>= 2.0.17, < 3) invisible_captcha (0.13.0) rails (>= 3.2.0) @@ -543,7 +543,8 @@ GEM logger mime-types-data (~> 3.2015) mime-types-data (3.2025.0722) - mini_magick (4.13.2) + mini_magick (5.3.1) + logger mini_mime (1.1.5) minitest (5.25.5) msgpack (1.8.0) @@ -802,7 +803,7 @@ GEM rubocop (~> 1.72) yard ruby-progressbar (1.13.0) - ruby-vips (2.2.4) + ruby-vips (2.2.5) ffi (~> 1.12) logger rubyXL (3.4.33) diff --git a/decidim-proposals/spec/system/amendable/amend_proposal_spec.rb b/decidim-proposals/spec/system/amendable/amend_proposal_spec.rb index 70225c228b4c4..5cc5c2ffdbf8e 100644 --- a/decidim-proposals/spec/system/amendable/amend_proposal_spec.rb +++ b/decidim-proposals/spec/system/amendable/amend_proposal_spec.rb @@ -210,6 +210,7 @@ expect(page).to have_content("Log in") switch_to_host(component.organization.host) login_as user, scope: :user + sleep 1 visit proposal_path expect(page).to have_content(proposal_title) find("#dropdown-trigger-resource-#{proposal.id}").click diff --git a/decidim-proposals/spec/system/proposal_show_spec.rb b/decidim-proposals/spec/system/proposal_show_spec.rb index 2f775e3297852..8a56b37e5419d 100644 --- a/decidim-proposals/spec/system/proposal_show_spec.rb +++ b/decidim-proposals/spec/system/proposal_show_spec.rb @@ -27,6 +27,7 @@ def visit_proposal describe "extra admin link" do before do login_as user, scope: :user + sleep 1 visit current_path end diff --git a/docs/modules/install/pages/manual.adoc b/docs/modules/install/pages/manual.adoc index a8cdc06e545e6..ed282ee57439f 100644 --- a/docs/modules/install/pages/manual.adoc +++ b/docs/modules/install/pages/manual.adoc @@ -8,6 +8,7 @@ In order to develop on decidim, you will need: * *NodeJS* 22.14.x * *Npm* 10.9.x * *ImageMagick* +* *LibVips* * *Chrome* browser and https://sites.google.com/a/chromium.org/chromedriver/[chromedriver] (if you need to run specs/tests) The compatibility between the different versions of the components is the following: @@ -67,7 +68,7 @@ Next, we need to install the `decidim` gem with its dependencies: [source,bash] ---- -sudo apt install -y libicu-dev imagemagick +sudo apt install -y libicu-dev imagemagick libvips libvips-tools gem install decidim ---- From d1157b4b82ac1c4729f9975e7afc7b684413997d Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Wed, 14 Jan 2026 10:40:41 +0200 Subject: [PATCH 063/116] Write API: create debates (#15801) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add CreateDebate mutation with input types and specs Co-authored-by: alecslupu <105683+alecslupu@users.noreply.github.com> * Add comprehensive usage documentation for CreateDebate mutation Co-authored-by: alecslupu <105683+alecslupu@users.noreply.github.com> * Add README for mutations directory Co-authored-by: alecslupu <105683+alecslupu@users.noreply.github.com> * Add implementation summary document Co-authored-by: alecslupu <105683+alecslupu@users.noreply.github.com> * Add quick reference guide Co-authored-by: alecslupu <105683+alecslupu@users.noreply.github.com> * Fix creation of debates * Fix ephemeral user in debates * Add documentation * Fix user types * Revert permissions * Remove unneeded require * Patch the documentation * Fix debate mutation type * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Andrés Pereira de Lucena --------- Co-authored-by: alecslupu <105683+alecslupu@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Andrés Pereira de Lucena --- .../app/models/decidim/api/api_user.rb | 4 + .../app/commands/decidim/create_follow.rb | 7 +- .../decidim/debates/permissions.rb | 2 + .../api/mutations/create_debate_type.rb | 44 +++++ .../api/mutations/debate_attributes.rb | 14 ++ .../api/mutations/debates_mutation_type.rb | 1 + decidim-debates/lib/decidim/debates/api.rb | 3 + .../spec/types/create_debate_type_spec.rb | 162 ++++++++++++++++++ .../reference/components/debates/create.adoc | 57 +++++- 9 files changed, 288 insertions(+), 6 deletions(-) create mode 100644 decidim-debates/lib/decidim/api/mutations/create_debate_type.rb create mode 100644 decidim-debates/lib/decidim/api/mutations/debate_attributes.rb create mode 100644 decidim-debates/spec/types/create_debate_type_spec.rb diff --git a/decidim-api/app/models/decidim/api/api_user.rb b/decidim-api/app/models/decidim/api/api_user.rb index 211ab0bdbf7c4..32b30df7fee5d 100644 --- a/decidim-api/app/models/decidim/api/api_user.rb +++ b/decidim-api/app/models/decidim/api/api_user.rb @@ -77,6 +77,10 @@ def admin_terms_accepted? def needs_password_update? false end + + def ephemeral? + extended_data["ephemeral"] + end end end end diff --git a/decidim-core/app/commands/decidim/create_follow.rb b/decidim-core/app/commands/decidim/create_follow.rb index 272b3bcf58aeb..523234116c2b6 100644 --- a/decidim-core/app/commands/decidim/create_follow.rb +++ b/decidim-core/app/commands/decidim/create_follow.rb @@ -33,13 +33,10 @@ def call attr_reader :follow, :form def create_follow! - @follow = Follow.find_by( + @follow = Follow.where( followable: form.followable, user: current_user - ) || Follow.create!( - followable: form.followable, - user: current_user - ) + ).first_or_create! end def increment_score diff --git a/decidim-debates/app/permissions/decidim/debates/permissions.rb b/decidim-debates/app/permissions/decidim/debates/permissions.rb index d67ee4bb7a496..3670a9fde8e3d 100644 --- a/decidim-debates/app/permissions/decidim/debates/permissions.rb +++ b/decidim-debates/app/permissions/decidim/debates/permissions.rb @@ -31,6 +31,8 @@ def permissions private def can_create_debate? + return false unless user + authorized?(:create) && current_settings&.creation_enabled? && component.participatory_space.can_participate?(user) end diff --git a/decidim-debates/lib/decidim/api/mutations/create_debate_type.rb b/decidim-debates/lib/decidim/api/mutations/create_debate_type.rb new file mode 100644 index 0000000000000..3d89f7343733c --- /dev/null +++ b/decidim-debates/lib/decidim/api/mutations/create_debate_type.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Decidim + module Debates + class CreateDebateType < Decidim::Api::Types::BaseMutation + graphql_name "CreateDebate" + + description "Creates a debate" + type Decidim::Debates::DebateType + + argument :attributes, DebateAttributes, description: "Input attributes of a debate", required: true + argument :locale, GraphQL::Types::String, "The locale for which to set the debate texts", required: true + argument :toggle_translations, GraphQL::Types::Boolean, "Whether the user asked to toggle the machine translations or not.", required: false, default_value: false + + def resolve(attributes:, locale:, toggle_translations:) + set_locale(locale:, toggle_translations:) + + params = attributes.to_h.slice(:title, :description) + + params[:taxonomies] = Decidim::Taxonomy.where(organization: current_organization, id: attributes.to_h.fetch(:taxonomies, [])).pluck(:id) + + form = form(Decidim::Debates::DebateForm).from_params(params) + + Decidim::Debates::CreateDebate.call(form) do + on(:ok) do |debate| + return debate.reload + end + + on(:invalid) do + raise Decidim::Api::Errors::AttributeValidationError, form.errors + end + end + end + + def authorized?(attributes:, locale:, toggle_translations:) + unless super && allowed_to?(:create, :debate, Debate.new(component: current_component), { current_user:, current_component: }) + raise Decidim::Api::Errors::MutationNotAuthorizedError, I18n.t("decidim.api.errors.unauthorized_mutation") + end + + true + end + end + end +end diff --git a/decidim-debates/lib/decidim/api/mutations/debate_attributes.rb b/decidim-debates/lib/decidim/api/mutations/debate_attributes.rb new file mode 100644 index 0000000000000..d6dae4fe31af0 --- /dev/null +++ b/decidim-debates/lib/decidim/api/mutations/debate_attributes.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Decidim + module Debates + class DebateAttributes < Decidim::Api::Types::BaseInputObject + graphql_name "DebateAttributes" + description "Attributes of a debate" + + argument :description, GraphQL::Types::String, description: "The description of the debate", required: true + argument :taxonomies, [GraphQL::Types::ID], description: "Array of taxonomy IDs", required: false + argument :title, GraphQL::Types::String, description: "The title of the debate", required: true + end + end +end diff --git a/decidim-debates/lib/decidim/api/mutations/debates_mutation_type.rb b/decidim-debates/lib/decidim/api/mutations/debates_mutation_type.rb index 9341e8c71094e..02a79fe792a2b 100644 --- a/decidim-debates/lib/decidim/api/mutations/debates_mutation_type.rb +++ b/decidim-debates/lib/decidim/api/mutations/debates_mutation_type.rb @@ -5,6 +5,7 @@ module Debates class DebatesMutationType < Decidim::Core::ComponentType description "Debates mutations for a component." + field :create_debate, mutation: Decidim::Debates::CreateDebateType, description: "Creates a debate" field :debate, type: Decidim::Debates::DebateMutationType, description: "Mutates a debate", null: true do argument :id, GraphQL::Types::ID, "The ID of the debate", required: true end diff --git a/decidim-debates/lib/decidim/debates/api.rb b/decidim-debates/lib/decidim/debates/api.rb index c7b1d01110e55..c6046ae8d9929 100644 --- a/decidim-debates/lib/decidim/debates/api.rb +++ b/decidim-debates/lib/decidim/debates/api.rb @@ -4,9 +4,12 @@ module Decidim module Debates autoload :DebateType, "decidim/api/debate_type" autoload :DebatesType, "decidim/api/debates_type" + autoload :DebatesMutationType, "decidim/api/mutations/debates_mutation_type" autoload :DebateMutationType, "decidim/api/mutations/debate_mutation_type" autoload :CloseDebateType, "decidim/api/mutations/close_debate_type" autoload :CloseDebateAttributes, "decidim/api/mutations/close_debate_attributes" + autoload :CreateDebateType, "decidim/api/mutations/create_debate_type" + autoload :DebateAttributes, "decidim/api/mutations/debate_attributes" end end diff --git a/decidim-debates/spec/types/create_debate_type_spec.rb b/decidim-debates/spec/types/create_debate_type_spec.rb new file mode 100644 index 0000000000000..7d495bfe3ec14 --- /dev/null +++ b/decidim-debates/spec/types/create_debate_type_spec.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true + +require "spec_helper" +require "decidim/api/test" + +module Decidim + module Debates + describe CreateDebateType, type: :graphql do + include_context "with a graphql class mutation" + + let(:type_class) { Decidim::Debates::CreateDebateType } + let(:root_klass) { Decidim::Debates::DebatesMutationType } + let(:root_value) { current_component } + + let(:current_organization) { create(:organization, available_locales: [:en]) } + let(:organization) { current_organization } + let(:participatory_process) { create(:participatory_process, :published, :with_steps, organization:) } + let!(:current_component) do + create(:debates_component, :published, :with_creation_enabled, participatory_space: participatory_process, settings: { + taxonomy_filters: [taxonomy_filter.id] + }) + end + + let(:title) { "Should every organization use Decidim?" } + let(:description) { "Add your comments on whether Decidim is useful for every organization." } + + let(:root_taxonomy) { create(:taxonomy, organization:) } + let!(:taxonomy) { create(:taxonomy, parent: root_taxonomy, organization:) } + let(:taxonomy_filter) { create(:taxonomy_filter, root_taxonomy:) } + let!(:taxonomy_filter_item) { create(:taxonomy_filter_item, taxonomy_filter:, taxonomy_item: taxonomy) } + let!(:taxonomies) { [taxonomy.id] } + + let(:locale) { "en" } + let(:translation_locale) { "en" } + + let(:attributes) do + { + title:, + description:, + taxonomies: + } + end + + let(:variables) do + { + component_id: current_component.id, + input: { + locale:, + attributes: + } + } + end + + let(:query) do + <<~GRAPHQL + mutation createDebate($input: CreateDebateInput!) { + createDebate(input: $input) { + id + title { translation(locale: "en") } + description { translation(locale: "en") } + taxonomies { + name { translation(locale: "en") } + } + author { id } + } + } + GRAPHQL + end + + shared_examples "create debate mutation examples" do + context "when creation is enabled" do + it "creates a debate" do + debate = response["createDebate"] + expect(debate).to be_present + expect(debate["id"]).to be_present + expect(debate["title"]["translation"]).to eq(title) + expect(debate["description"]["translation"]).to eq(description) + end + + it "associates taxonomies with the debate" do + debate = response["createDebate"] + expect(debate["taxonomies"]).to be_present + expect(debate["taxonomies"].length).to eq(1) + expect(debate["taxonomies"].first["name"]["translation"]).to eq(taxonomy.name["en"]) + end + + context "without taxonomies" do + let(:taxonomies) { [] } + + it "creates a debate without taxonomies" do + debate = response["createDebate"] + expect(debate).to be_present + expect(debate["taxonomies"]).to be_empty + end + end + end + end + + context "when the user is not logged in" do + let!(:current_user) { nil } + + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + + context "with admin user" do + it_behaves_like "create debate mutation examples" do + let!(:user_type) { :admin } + end + end + + context "with normal user" do + it_behaves_like "create debate mutation examples" do + let!(:user_type) { :user } + end + end + + context "with api_user" do + it_behaves_like "create debate mutation examples" do + let!(:user_type) { :api_user } + end + end + + context "when creation is not enabled" do + let(:current_component) { create(:debates_component, :published, participatory_space: participatory_process) } + + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + + context "when validating" do + let!(:user_type) { :user } + + context "with having invalid locale" do + let(:locale) { "tlh" } + + it "raises an error" do + expect { response }.to raise_error(Api::Errors::InvalidLocaleError, /Invalid locale provided/) + end + end + + context "and title is missing" do + let(:title) { "" } + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::AttributeValidationError, /cannot be blank/) + end + end + + context "and description is missing" do + let(:description) { "" } + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::AttributeValidationError, /cannot be blank/) + end + end + end + end + end +end diff --git a/docs/modules/develop/pages/api/reference/components/debates/create.adoc b/docs/modules/develop/pages/api/reference/components/debates/create.adoc index 765d6f92e8817..9b8c5c480c364 100644 --- a/docs/modules/develop/pages/api/reference/components/debates/create.adoc +++ b/docs/modules/develop/pages/api/reference/components/debates/create.adoc @@ -1,3 +1,58 @@ = Create Debate +== Input attributes -include::admin:partial$under-construction.adoc[] +When you want to add or create data in your debates component, you need to rely on the `DebateAttributes` type, which has the following input fields: + +[source] +---- +• title (required) - String +• description (required) - String +• taxonomies (optional) - Taxonomy ids to assign to this debate +---- + +== Sample request + +The `CreateDebate` mutation allows authenticated users to create new debates in a Decidim debates component. This mutation uses the existing `CreateDebate` command from the controller. + +[source,graphql] +---- +mutation createDebate($componentId: ID!, $input: CreateDebateInput!){ + component(id: $componentId) { + ...on DebatesMutation{ + createDebate(input: $input) { + id + title { translation(locale: "en") } + description { translation(locale: "en") } + taxonomies { + name { translation(locale: "en") } + } + } + } + } +} +---- + +Example of submitted variables + +[source,json] +---- +{ + "componentId": 5, + "input": { + "locale": "en", + "attributes": { + "description": "Cities need more people, not more cars", + "taxonomies": [11], + "title": "More sidewalks and less roads" + } + } +} +---- + +== Error Handling + +The most frequent errors that can be generated by this mutation are as follows: + +* xref:develop:api/reference/errors/unauthorized_mutation.adoc[Unauthorized Mutation Error - MUTATION_NOT_AUTHORIZED_ERROR] +* xref:develop:api/reference/errors/attribute_validation_error.adoc[Attribute Validation Error - ATTRIBUTE_VALIDATION_ERROR] +* xref:develop:api/reference/errors/unauthorized_object.adoc[Unauthorized Object Error - UNAUTHORIZED_OBJECT_ERROR] From 781fa423066761b148b08e5d95591807e445f36d Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Wed, 14 Jan 2026 12:30:01 +0200 Subject: [PATCH 064/116] Rename set_component_breadcrumb_item to set_breadcrumb_items (#15859) --- .../controllers/decidim/admin/components/base_controller.rb | 4 ++-- .../app/controllers/decidim/components/base_controller.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/decidim-admin/app/controllers/decidim/admin/components/base_controller.rb b/decidim-admin/app/controllers/decidim/admin/components/base_controller.rb index faf46a8ba2e07..eaff40b1f4161 100644 --- a/decidim-admin/app/controllers/decidim/admin/components/base_controller.rb +++ b/decidim-admin/app/controllers/decidim/admin/components/base_controller.rb @@ -31,7 +31,7 @@ class BaseController < Decidim::Admin::ApplicationController enforce_permission_to :read, :component, component: current_component end - before_action :set_component_breadcrumb_item + before_action :set_breadcrumb_items def permissions_context super.merge( @@ -68,7 +68,7 @@ def skip_manage_component_permission false end - def set_component_breadcrumb_item + def set_breadcrumb_items context_breadcrumb_items << { label: t("components", scope: "decidim.admin.menu"), url: parent_path, diff --git a/decidim-core/app/controllers/decidim/components/base_controller.rb b/decidim-core/app/controllers/decidim/components/base_controller.rb index b07f684295f15..da45894553027 100644 --- a/decidim-core/app/controllers/decidim/components/base_controller.rb +++ b/decidim-core/app/controllers/decidim/components/base_controller.rb @@ -35,7 +35,7 @@ class BaseController < Decidim::ApplicationController before_action :redirect_unless_feature_private - before_action :set_component_breadcrumb_item + before_action :set_breadcrumb_items def current_participatory_space request.env["decidim.current_participatory_space"] @@ -66,7 +66,7 @@ def redirect_unless_feature_private raise ActionController::RoutingError, "Not Found" unless current_user_can_visit_space? end - def set_component_breadcrumb_item + def set_breadcrumb_items context_breadcrumb_items << add_current_component context_breadcrumb_items << add_parent_breadcrumb_item context_breadcrumb_items << add_breadcrumb_item From 37ffce3630a9d6157e7928d97ffd4237760f0537 Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Wed, 14 Jan 2026 13:27:21 +0200 Subject: [PATCH 065/116] Write API: update meetings (#15797) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add CreateMeeting mutation for decidim-meetings API * Add UpdateMeeting mutation with input types and comprehensive documentation Co-authored-by: alecslupu <105683+alecslupu@users.noreply.github.com> * Register UpdateMeeting mutation in API and improve form handling Co-authored-by: alecslupu <105683+alecslupu@users.noreply.github.com> * Fix authorization and test spec structure for UpdateMeeting mutation Co-authored-by: alecslupu <105683+alecslupu@users.noreply.github.com> * Add comprehensive testing guide and implementation summary Co-authored-by: alecslupu <105683+alecslupu@users.noreply.github.com> * Fix syntax error in authorization method Co-authored-by: alecslupu <105683+alecslupu@users.noreply.github.com> * Fix authorization method signature based on code review Co-authored-by: alecslupu <105683+alecslupu@users.noreply.github.com> * Running rubcops * Fix mutation registration * Remove READMEs * Implement meeting creation * Normalize * Fix documentation * Fix enums * Fix spelling * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix pipeline * Fix update meetings * Fix specs * Remove unused test * Patch the documentation * Apply code review recommendations * Apply suggestions from code review Co-authored-by: Andrés Pereira de Lucena * Running linters * Apply suggestions from code review Co-authored-by: Andrés Pereira de Lucena * Apply code review recommendation * Apply suggestions from code review Co-authored-by: Andrés Pereira de Lucena --------- Co-authored-by: alecslupu <105683+alecslupu@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Andrés Pereira de Lucena --- .../api/errors/attribute_validation_error.rb | 2 +- .../errors/attribute_validation_error_spec.rb | 2 +- .../forms/decidim/meetings/meeting_form.rb | 2 +- .../api/mutations/create_meeting_type.rb | 14 +- ...ng_attributes.rb => meeting_attributes.rb} | 6 +- .../api/mutations/meeting_mutation_type.rb | 1 + .../api/mutations/update_meeting_type.rb | 69 +++++ decidim-meetings/lib/decidim/meetings/api.rb | 15 +- .../create_meeting_type_spec.rb | 0 .../spec/types/update_meeting_type_spec.rb | 293 ++++++++++++++++++ .../reference/components/meetings/create.adoc | 2 +- .../reference/components/meetings/update.adoc | 94 +++++- 12 files changed, 479 insertions(+), 21 deletions(-) rename decidim-meetings/lib/decidim/api/mutations/{create_meeting_attributes.rb => meeting_attributes.rb} (93%) create mode 100644 decidim-meetings/lib/decidim/api/mutations/update_meeting_type.rb rename decidim-meetings/spec/{lib/decidim/api/mutations => types}/create_meeting_type_spec.rb (100%) create mode 100644 decidim-meetings/spec/types/update_meeting_type_spec.rb diff --git a/decidim-api/lib/decidim/api/errors/attribute_validation_error.rb b/decidim-api/lib/decidim/api/errors/attribute_validation_error.rb index 6a754974bb4cc..0225d6fde18b0 100644 --- a/decidim-api/lib/decidim/api/errors/attribute_validation_error.rb +++ b/decidim-api/lib/decidim/api/errors/attribute_validation_error.rb @@ -63,7 +63,7 @@ def to_h def message return @messages.full_messages.join(", ") if @messages.is_a?(ActiveModel::Errors) - return @messages.map { |a| a[:message] }.join(", ") if @messages.is_a?(Array) + return @messages.map { |a| [a[:path].last, a[:message]].join(": ") }.join(", ") if @messages.is_a?(Array) @messages.to_s end diff --git a/decidim-api/spec/lib/decidim/api/errors/attribute_validation_error_spec.rb b/decidim-api/spec/lib/decidim/api/errors/attribute_validation_error_spec.rb index 0396c7f37b279..c57f2155c3a23 100644 --- a/decidim-api/spec/lib/decidim/api/errors/attribute_validation_error_spec.rb +++ b/decidim-api/spec/lib/decidim/api/errors/attribute_validation_error_spec.rb @@ -30,7 +30,7 @@ module Errors end describe "#message" do - it { expect(subject.message).to eq("is too short (under 15 characters), is too long") } + it { expect(subject.message).to eq("body: is too short (under 15 characters), title: is too long") } end end diff --git a/decidim-meetings/app/forms/decidim/meetings/meeting_form.rb b/decidim-meetings/app/forms/decidim/meetings/meeting_form.rb index 2dace5dc9a0a3..336c090d0b784 100644 --- a/decidim-meetings/app/forms/decidim/meetings/meeting_form.rb +++ b/decidim-meetings/app/forms/decidim/meetings/meeting_form.rb @@ -23,7 +23,7 @@ class MeetingForm < ::Decidim::Meetings::BaseMeetingForm validates :title, presence: true, etiquette: true validates :description, presence: true, etiquette: true validates :type_of_meeting, presence: true - validates :location, presence: true, if: ->(form) { form.in_person_meeting? || form.hybrid_meeting? } + validates :location, presence: true, if: ->(form) { form.needs_address? } validates :online_meeting_url, presence: true, url: true, if: ->(form) { form.online_meeting? || form.hybrid_meeting? } validates :registration_type, presence: true validates :available_slots, numericality: { greater_than_or_equal_to: 0 }, presence: true, if: ->(form) { form.on_this_platform? } diff --git a/decidim-meetings/lib/decidim/api/mutations/create_meeting_type.rb b/decidim-meetings/lib/decidim/api/mutations/create_meeting_type.rb index 4a2989fc6fcac..2417000ccb9e6 100644 --- a/decidim-meetings/lib/decidim/api/mutations/create_meeting_type.rb +++ b/decidim-meetings/lib/decidim/api/mutations/create_meeting_type.rb @@ -8,7 +8,7 @@ class CreateMeetingType < Decidim::Api::Types::BaseMutation description "Creates a meeting" type Decidim::Meetings::MeetingType - argument :attributes, CreateMeetingAttributes, description: "Input attributes for creating a meeting", required: true + argument :attributes, MeetingAttributes, description: "Input attributes for creating a meeting", required: true argument :locale, GraphQL::Types::String, "The locale for which to set the meeting texts", required: true argument :toggle_translations, GraphQL::Types::Boolean, "Whether the user asked to toggle the machine translations or not.", required: false, default_value: false @@ -20,21 +20,21 @@ def resolve(attributes:, locale:, toggle_translations:) :available_slots, :description, :end_time, + :iframe_access_level, + :iframe_embed_type, :latitude, - :longitude, :location, :location_hints, + :longitude, :online_meeting_url, :registration_terms, :registration_type, :registration_url, + :registrations_enabled, :start_time, - :title, :taxonomies, - :type_of_meeting, - :iframe_access_level, - :iframe_embed_type, - :registrations_enabled + :title, + :type_of_meeting ) params[:taxonomies] = Decidim::Taxonomy.where(organization: current_organization, id: params[:taxonomies]).pluck(:id) if params[:taxonomies] diff --git a/decidim-meetings/lib/decidim/api/mutations/create_meeting_attributes.rb b/decidim-meetings/lib/decidim/api/mutations/meeting_attributes.rb similarity index 93% rename from decidim-meetings/lib/decidim/api/mutations/create_meeting_attributes.rb rename to decidim-meetings/lib/decidim/api/mutations/meeting_attributes.rb index 1187a95e2e548..326e01453953f 100644 --- a/decidim-meetings/lib/decidim/api/mutations/create_meeting_attributes.rb +++ b/decidim-meetings/lib/decidim/api/mutations/meeting_attributes.rb @@ -2,9 +2,9 @@ module Decidim module Meetings - class CreateMeetingAttributes < Decidim::Api::Types::BaseInputObject - graphql_name "CreateMeetingAttributes" - description "Attributes for creating a meeting" + class MeetingAttributes < Decidim::Api::Types::BaseInputObject + graphql_name "MeetingAttributes" + description "Attributes for creating or updating a meeting" argument :address, GraphQL::Types::String, description: "The address of the meeting", required: false argument :available_slots, GraphQL::Types::Int, description: "Number of available slots for registration", required: false diff --git a/decidim-meetings/lib/decidim/api/mutations/meeting_mutation_type.rb b/decidim-meetings/lib/decidim/api/mutations/meeting_mutation_type.rb index ec5479cf49189..83f75df16a998 100644 --- a/decidim-meetings/lib/decidim/api/mutations/meeting_mutation_type.rb +++ b/decidim-meetings/lib/decidim/api/mutations/meeting_mutation_type.rb @@ -9,6 +9,7 @@ class MeetingMutationType < Decidim::Api::Types::BaseObject description "A meeting which includes its available mutations" field :close, mutation: Decidim::Meetings::CloseMeetingType, description: "Closes a meeting" + field :update, mutation: Decidim::Meetings::UpdateMeetingType, description: "Updates a meeting" field :withdraw, mutation: Decidim::Meetings::WithdrawMeetingType, description: "Withdraws a meeting" end end diff --git a/decidim-meetings/lib/decidim/api/mutations/update_meeting_type.rb b/decidim-meetings/lib/decidim/api/mutations/update_meeting_type.rb new file mode 100644 index 0000000000000..87d01d2927657 --- /dev/null +++ b/decidim-meetings/lib/decidim/api/mutations/update_meeting_type.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +module Decidim + module Meetings + class UpdateMeetingType < Decidim::Api::Types::BaseMutation + graphql_name "UpdateMeeting" + + description "Updates a meeting" + type Decidim::Meetings::MeetingType + + argument :attributes, MeetingAttributes, description: "Input attributes for updating a meeting", required: true + argument :locale, GraphQL::Types::String, "The locale for which to set the meeting texts", required: true + argument :toggle_translations, GraphQL::Types::Boolean, "Whether the user asked to toggle the machine translations or not.", required: false, default_value: false + + def resolve(attributes:, locale:, toggle_translations:) + set_locale(locale:, toggle_translations:) + + params = extract_from(attributes) + params[:taxonomies] = Decidim::Taxonomy.where(organization: current_organization, id: params[:taxonomies]).pluck(:id) if params[:taxonomies] + + form = form(Decidim::Meetings::MeetingForm).from_params(params) + + UpdateMeeting.call(form, object) do + on(:ok) do |meeting| + return meeting.reload + end + + on(:invalid) do + raise Decidim::Api::Errors::AttributeValidationError, form.errors + end + end + end + + def authorized?(attributes:, locale:, toggle_translations:) + raise Decidim::Api::Errors::MutationNotAuthorizedError, I18n.t("decidim.api.errors.unauthorized_mutation") unless super && allowed_to?(:update, :meeting, object, context) + + true + end + + private + + def extract_from(attributes) + attributes = attributes.to_h.compact + + { + address: attributes.fetch(:address, object.address), + available_slots: attributes.fetch(:available_slots, object.available_slots), + description: attributes.fetch(:description, translated_attribute(object.description)), + end_time: attributes.fetch(:end_time, object.end_time), + iframe_access_level: attributes.fetch(:iframe_access_level, object.iframe_access_level), + iframe_embed_type: attributes.fetch(:iframe_embed_type, object.iframe_embed_type), + latitude: attributes.fetch(:latitude, object.latitude), + location: attributes.fetch(:location, translated_attribute(object.location)), + location_hints: attributes.fetch(:location_hints, translated_attribute(object.location_hints)), + longitude: attributes.fetch(:longitude, object.longitude), + online_meeting_url: attributes.fetch(:online_meeting_url, object.online_meeting_url), + registration_terms: attributes.fetch(:registration_terms, translated_attribute(object.registration_terms)), + registration_type: attributes.fetch(:registration_type, object.registration_type), + registration_url: attributes.fetch(:registration_url, object.registration_url), + registrations_enabled: attributes.fetch(:registrations_enabled, object.registrations_enabled), + start_time: attributes.fetch(:start_time, object.start_time), + taxonomies: attributes.fetch(:taxonomies, object.taxonomies.pluck(:id)), + title: attributes.fetch(:title, translated_attribute(object.title)), + type_of_meeting: attributes.fetch(:type_of_meeting, object.type_of_meeting) + } + end + end + end +end diff --git a/decidim-meetings/lib/decidim/meetings/api.rb b/decidim-meetings/lib/decidim/meetings/api.rb index e869fb1feb94f..f48ac9ab3b6f9 100644 --- a/decidim-meetings/lib/decidim/meetings/api.rb +++ b/decidim-meetings/lib/decidim/meetings/api.rb @@ -7,20 +7,23 @@ module Meetings autoload :MeetingType, "decidim/api/meeting_type" autoload :MeetingsType, "decidim/api/meetings_type" autoload :ServiceType, "decidim/api/service_type" + autoload :LinkedResourcesInterface, "decidim/api/linked_resources_interface" autoload :ServicesInterface, "decidim/api/services_interface" - autoload :CreateMeetingAttributes, "decidim/api/mutations/create_meeting_attributes" + + autoload :MeetingAttributes, "decidim/api/mutations/meeting_attributes" + autoload :CloseMeetingAttributes, "decidim/api/mutations/close_meeting_attributes" + autoload :CreateMeetingType, "decidim/api/mutations/create_meeting_type" + autoload :CloseMeetingType, "decidim/api/mutations/close_meeting_type" autoload :MeetingsMutationType, "decidim/api/mutations/meetings_mutation_type" - autoload :MeetingMutationType, "decidim/api/mutations/meeting_mutation_type" - autoload :CloseMeetingType, "decidim/api/mutations/close_meeting_type" - autoload :CloseMeetingAttributes, "decidim/api/mutations/close_meeting_attributes" + autoload :UpdateMeetingType, "decidim/api/mutations/update_meeting_type" autoload :WithdrawMeetingType, "decidim/api/mutations/withdraw_meeting_type" + autoload :IframeAccessLevelEnum, "decidim/api/iframe_access_level_enum" + autoload :IframeEmbedTypeEnum, "decidim/api/iframe_embed_type_enum" autoload :RegistrationTypeEnum, "decidim/api/registration_type_enum" autoload :TypeOfMeetingEnum, "decidim/api/type_of_meeting_enum" - autoload :IframeEmbedTypeEnum, "decidim/api/iframe_embed_type_enum" - autoload :IframeAccessLevelEnum, "decidim/api/iframe_access_level_enum" end end diff --git a/decidim-meetings/spec/lib/decidim/api/mutations/create_meeting_type_spec.rb b/decidim-meetings/spec/types/create_meeting_type_spec.rb similarity index 100% rename from decidim-meetings/spec/lib/decidim/api/mutations/create_meeting_type_spec.rb rename to decidim-meetings/spec/types/create_meeting_type_spec.rb diff --git a/decidim-meetings/spec/types/update_meeting_type_spec.rb b/decidim-meetings/spec/types/update_meeting_type_spec.rb new file mode 100644 index 0000000000000..19cc2c17a5108 --- /dev/null +++ b/decidim-meetings/spec/types/update_meeting_type_spec.rb @@ -0,0 +1,293 @@ +# frozen_string_literal: true + +require "spec_helper" +require "decidim/api/test" + +module Decidim + module Meetings + describe UpdateMeetingType, type: :graphql do + include_context "with a graphql class mutation" + + let(:root_klass) { MeetingMutationType } + let(:current_organization) { create(:organization, available_locales: [:en]) } + let(:participatory_process) { create(:participatory_process, :with_steps, organization: current_organization) } + let(:meeting_component) do + create(:meeting_component, :published, participatory_space: participatory_process, settings: { + creation_enabled_for_participants: true, + taxonomy_filters: [taxonomy_filter.id] + }) + end + let!(:model) { create(:meeting, :published, component: meeting_component, author: current_user) } + let(:title) { "Updated Meeting Title" } + let(:description) { "Updated meeting description" } + let(:location) { "Updated location" } + let(:start_time) { (2.days.from_now).to_time.iso8601 } + let(:end_time) { (2.days.from_now + 2.hours).to_time.iso8601 } + let(:address) { "Updated address, 123" } + let(:latitude) { 41.1234 } + let(:longitude) { 2.5678 } + let(:registration_type) { "ON_THIS_PLATFORM" } + let(:available_slots) { 50 } + let(:type_of_meeting) { "ONLINE" } + let(:online_meeting_url) { "https://meet.example.org/updated-meeting" } + let(:registration_terms) { "Updated registration terms" } + let(:root_taxonomy) { create(:taxonomy, organization: current_organization) } + let!(:taxonomy) { create(:taxonomy, parent: root_taxonomy, organization: current_organization) } + let(:taxonomy_filter) { create(:taxonomy_filter, root_taxonomy:) } + let!(:taxonomy_filter_item) { create(:taxonomy_filter_item, taxonomy_filter:, taxonomy_item: taxonomy) } + let(:locale) { "en" } + + let(:variables) do + { + input: { + locale:, + attributes: { + address:, + availableSlots: available_slots, + description:, + endTime: end_time, + latitude:, + location:, + longitude:, + onlineMeetingUrl: online_meeting_url, + registrationTerms: registration_terms, + registrationType: registration_type, + startTime: start_time, + taxonomies: [taxonomy.id], + title:, + typeOfMeeting: type_of_meeting + } + } + } + end + + let(:query) do + <<~GRAPHQL + mutation($input: UpdateMeetingInput!) { + update(input: $input) { + address + description { translation(locale: "en") } + endTime + id + location { translation(locale: "en") } + registrationType + startTime + taxonomies { id } + title { translation(locale: "en") } + } + } + GRAPHQL + end + + shared_examples "update meeting mutation examples" do + context "when user has permission to update" do + it "the meeting" do + meeting_response = response["update"] + expect(meeting_response).to be_present + expect(meeting_response).to include( + { + "id" => model.id.to_s, + "title" => { + "translation" => title + }, + "description" => { + "translation" => description + }, + "location" => { + "translation" => location + }, + "address" => address, + "registrationType" => registration_type + } + ) + + expect(meeting_response["taxonomies"]).to include({ "id" => taxonomy.id.to_s }) + + # Verify the meeting was actually updated in the database + model.reload + expect(model.title["en"]).to eq(title) + expect(model.description["en"]).to eq(description) + expect(model.address).to eq(address) + end + + context "when validating" do + context "with having invalid locale" do + let(:locale) { "tlh" } + + it "raises an error" do + expect { response }.to raise_error(Api::Errors::InvalidLocaleError, /Invalid locale provided/) + end + end + + context "with invalid title" do + let(:title) { "" } + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::AttributeValidationError, /cannot be blank/) + end + end + + context "with invalid description" do + let(:description) { "" } + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::AttributeValidationError, /cannot be blank/) + end + end + + context "with invalid start_time" do + context "and is empty" do + let(:start_time) { "" } + + it "raises an error" do + expect { response }.to raise_error(ArgumentError) + end + end + + context "and is invalid" do + let(:start_time) { 3.days.from_now.to_time.iso8601 } + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::AttributeValidationError, /must be after/) + end + end + end + + context "with invalid end_time" do + context "and is empty" do + let(:end_time) { "" } + + it "raises an error" do + expect { response }.to raise_error(ArgumentError) + end + end + + context "and is invalid" do + let(:end_time) { (2.days.ago + 2.hours).to_time.iso8601 } + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::AttributeValidationError, /must be before/) + end + end + end + + context "with empty fields" do + context "when location" do + let(:location) { "" } + + context "when online_meeting_url is required" do + let(:type_of_meeting) { "IN_PERSON" } + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::AttributeValidationError, /cannot be blank/) + end + end + + context "when online_meeting_url is optional" do + let(:type_of_meeting) { "ONLINE" } + + it "saves the data" do + expect(response["update"]).to include({ "title" => { "translation" => title } }) + end + end + end + + context "when address" do + let(:address) { "" } + + context "when online_meeting_url is required" do + let(:type_of_meeting) { "IN_PERSON" } + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::AttributeValidationError, /cannot be blank/) + end + end + + context "when online_meeting_url is optional" do + let(:type_of_meeting) { "ONLINE" } + + it "saves the data" do + expect(response["update"]).to include({ "title" => { "translation" => title } }) + end + end + end + end + + context "with empty online_meeting_url" do + let(:online_meeting_url) { "" } + + context "when online_meeting_url is required" do + let(:type_of_meeting) { "ONLINE" } + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::AttributeValidationError, /cannot be blank/) + end + end + + context "when online_meeting_url is optional" do + let(:type_of_meeting) { "IN_PERSON" } + + it "saves the data" do + expect(response["update"]).to include({ "title" => { "translation" => title } }) + end + end + end + end + + context "with partial update" do + let(:variables) do + { + input: { + locale:, + attributes: { + description:, + endTime: end_time, + onlineMeetingUrl: online_meeting_url, + registrationType: registration_type, + startTime: start_time, + taxonomies: [taxonomy.id], + title: "Only title updated", + typeOfMeeting: type_of_meeting + } + } + } + end + + it "updates only specified fields" do + meeting_response = response["update"] + expect(meeting_response).to be_present + expect(meeting_response["title"]["translation"]).to eq("Only title updated") + end + end + end + end + + context "with user who owns the meeting" do + it_behaves_like "update meeting mutation examples" do + let!(:user_type) { :user } + end + end + + context "with admin user" do + it_behaves_like "update meeting mutation examples" do + let!(:user_type) { :admin } + end + end + + context "with api_user" do + it_behaves_like "update meeting mutation examples" do + let!(:user_type) { :api_user } + end + end + + context "with user who does not own the meeting" do + let(:other_user) { create(:user, :confirmed, organization: current_organization) } + let!(:model) { create(:meeting, component: meeting_component, author: other_user) } + + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + end + end +end diff --git a/docs/modules/develop/pages/api/reference/components/meetings/create.adoc b/docs/modules/develop/pages/api/reference/components/meetings/create.adoc index 341775b04c6f1..414e147bbf258 100644 --- a/docs/modules/develop/pages/api/reference/components/meetings/create.adoc +++ b/docs/modules/develop/pages/api/reference/components/meetings/create.adoc @@ -2,7 +2,7 @@ == Input attributes -When you want to add or create data in your meetings component, you need to rely on the `CreateMeetingAttributes` type, which has the following input fields: +When you want to add or create data in your meetings component, you need to rely on the `MeetingAttributes` type, which has the following input fields: [source] ---- diff --git a/docs/modules/develop/pages/api/reference/components/meetings/update.adoc b/docs/modules/develop/pages/api/reference/components/meetings/update.adoc index ccad2ce3cd8d1..2ec3063c7b021 100644 --- a/docs/modules/develop/pages/api/reference/components/meetings/update.adoc +++ b/docs/modules/develop/pages/api/reference/components/meetings/update.adoc @@ -1,3 +1,95 @@ = Update Meeting -include::admin:partial$under-construction.adoc[] +== Input attributes + +When you want to update your meetings component, you need to rely on the `MeetingAttributes` type, which has the following input fields: + +[source] +---- +• address (optional) - String +• available_slots (optional) - Int +• description (required) - String +• end_time (required) - DateTime +• latitude (optional) - coordinate +• longitude (optional) - coordinate +• location_hints (optional) - String +• location (optional) - String +• online_meeting_url (optional) - String +• registration_terms (optional) - String +• registration_type (required) - RegistrationTypeType +• registration_url (optional) - String +• registrations_enabled (optional) - Boolean +• start_time (required) - DateTime +• taxonomies (optional) - Taxonomy ids to assign to this meeting +• title (required) - String +• type_of_meeting (required) - TypeOfMeeting +---- + +You may want to check the following input types: + +* http://nightly.decidim.org/api/docs/enum/typeofmeetingenum/[TypeOfMeetingEnum] +* http://nightly.decidim.org/api/docs/enum/registrationtypeenum/[RegistrationTypeEnum] +* http://nightly.decidim.org/api/docs/enum/iframeembedtypeenum/[IframeEmbedTypeEnum] +* http://nightly.decidim.org/api/docs/enum/iframeaccesslevelenum/[IframeAccessLevelEnum] + +== Sample request + +The `UpdateMeeting` mutation allows authenticated users to update meetings in a Decidim meetings component. This mutation uses the existing `UpdateMeeting` command from the controller. + +[source,graphql] +---- +mutation updateMeeting($componentId: ID!, $meetingId: ID!, $input: UpdateMeetingInput!){ + component(id: $componentId) { + ...on MeetingsMutation{ + meeting(id: $meetingId) { + update(input: $input) { + id + title { translation(locale: "en") } + } + } + } + } +} +---- + +Example of submitted variables + +[source,json] +---- +{ + "componentId": 670, + "meetingId": "2", + "input": { + "locale": "en", + "attributes": { + "address": "Carrer de la Pau, 1, Barcelona", + "availableSlots": 10, + "description": "Cities need more people, not more cars", + "endTime": "2025-12-24T22:10:22Z", + "iframeAccessLevel": "ALL", + "iframeEmbedType": "NONE", + "latitude": 40.1234, + "location": "Somewhere", + "locationHints": "Near the main square", + "longitude": 2.1234, + "onlineMeetingUrl": "https://meets.example.org/abc-def", + "registrationTerms": "By registering you agree to the terms and conditions", + "registrationType": "ON_THIS_PLATFORM", + "registrationUrl": "https://example.org/register", + "registrationsEnabled": true, + "startTime": "2025-12-24T20:10:22Z", + "taxonomies": [1072], + "title": "More sidewalks and less roads", + "typeOfMeeting": "ONLINE" + } + } +} +---- + +== Error Handling + +The most frequent errors that can be generated by this mutation are as follows: + +* xref:develop:api/reference/errors/unauthorized_mutation.adoc[Unauthorized Mutation Error - MUTATION_NOT_AUTHORIZED_ERROR] +* xref:develop:api/reference/errors/attribute_validation_error.adoc[Attribute Validation Error - ATTRIBUTE_VALIDATION_ERROR] +* xref:develop:api/reference/errors/unauthorized_object.adoc[Unauthorized Object Error - UNAUTHORIZED_OBJECT_ERROR] From b9637093bd372700cd35ed30eaa97ff8c71df351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Wed, 14 Jan 2026 14:02:50 +0100 Subject: [PATCH 066/116] Remove gitpod configuration (#15888) --- .gitpod.Dockerfile | 46 ------------------------------------ .gitpod.yml | 59 ---------------------------------------------- 2 files changed, 105 deletions(-) delete mode 100644 .gitpod.Dockerfile delete mode 100644 .gitpod.yml diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile deleted file mode 100644 index bf356c10a5ec4..0000000000000 --- a/.gitpod.Dockerfile +++ /dev/null @@ -1,46 +0,0 @@ -FROM gitpod/workspace-base:latest -USER root - -# Install PostgreSQL -ENV PGVERSION=14 -RUN apt-get update \ - && apt-get install -y postgresql postgresql-client postgresql-server-dev-${PGVERSION} libpq-dev - -# Setup the database user env vars, drop the default database cluster and change the folders to the workspace -# This makes it possible to persist the database within the workspace -ENV DATABASE_USERNAME=decidim -ENV DATABASE_PASSWORD=development -RUN pg_dropcluster $PGVERSION main \ - && rm -rf /etc/postgresql /var/lib/postgresql \ - && ln -s /workspace/etc/postgresql /etc/postgresql \ - && ln -s /workspace/var/lib/postgresql /var/lib/postgresql \ - && chown -h postgres:postgres /etc/postgresql /var/lib/postgresql - -#### User space #### -USER gitpod - -# Install nvm and Node -ENV NODE_VERSION=22.14.0 -RUN curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | PROFILE=/dev/null bash \ - && bash -c ". .nvm/nvm.sh \ - && nvm install v${NODE_VERSION} \ - && nvm alias default v${NODE_VERSION} \ - && npm install -g npm yarn node-gyp" \ - && echo ". ~/.nvm/nvm.sh" >> ~/.bashrc.d/50-node - -# Install rbenv and Ruby -ENV RUBY_VERSION=3.4.7 -ENV WORKSPACE_GEM_HOME=/workspace/.gem -RUN sudo apt-get install -y build-essential curl git zlib1g-dev libssl-dev \ - libreadline-dev libyaml-dev libxml2-dev libxslt-dev -RUN git clone https://github.com/rbenv/rbenv.git ~/.rbenv \ - && echo 'eval "$(~/.rbenv/bin/rbenv init - sh)"' >> ~/.bashrc.d/60-ruby \ - && eval "$(~/.rbenv/bin/rbenv init - sh)" \ - && git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build \ - && git clone https://github.com/rbenv/rbenv-vars.git "$(rbenv root)"/plugins/rbenv-vars \ - && rbenv install $RUBY_VERSION && rbenv global $RUBY_VERSION \ - && echo "export GEM_PATH=\"${WORKSPACE_GEM_HOME}:$(gem env home)\"" >> ~/.bashrc.d/60-ruby \ - && echo "export GEM_HOME=\"${WORKSPACE_GEM_HOME}\"" >> ~/.bashrc.d/60-ruby \ - && echo 'export RAILS_DEVELOPMENT_HOST=${RAILS_DEVELOPMENT_HOST:-"3000-${GITPOD_WORKSPACE_ID}.${GITPOD_WORKSPACE_CLUSTER_HOST}"}' >> ~/.bashrc.d/70-decidim \ - && echo 'export DECIDIM_FORCE_SSL=${DECIDIM_FORCE_SSL:-true}' >> ~/.bashrc.d/70-decidim \ - && echo 'export DECIDIM_SERVICE_WORKER_ENABLED=${DECIDIM_SERVICE_WORKER_ENABLED:-true}' >> ~/.bashrc.d/70-decidim diff --git a/.gitpod.yml b/.gitpod.yml deleted file mode 100644 index 3c2fb03e8cb46..0000000000000 --- a/.gitpod.yml +++ /dev/null @@ -1,59 +0,0 @@ -image: - file: .gitpod.Dockerfile - -tasks: - - name: Development app - init: | - ( - [ $(sudo pg_lsclusters -h | wc -l) -eq 0 ] && ( - sudo mkdir -p /workspace/etc/postgresql /workspace/var/lib/postgresql && - sudo chown -R postgres:postgres /workspace/etc/postgresql /workspace/var/lib/postgresql && - sudo pg_createcluster $PGVERSION main - ) || echo "Database exists" - ) && - sudo service postgresql start && - ( - [ $(sudo su postgres -c "psql -At -c \"SELECT COUNT(usename) FROM pg_user WHERE usename = '${DATABASE_USERNAME}'\"") -eq 0 ] && ( - sudo su postgres -c "psql -c 'CREATE USER $DATABASE_USERNAME SUPERUSER'" && - sudo su postgres -c "psql -c \"ALTER ROLE $DATABASE_USERNAME WITH PASSWORD '${DATABASE_PASSWORD}'\"" - ) || echo "Database user exists" - ) && - mkdir -p .vscode && - echo '{"workbench.startupEditor": "none"}' > .vscode/settings.json && - bundle install --jobs 4 && - bundle exec rake development_app && - echo 'Rails.application.config.hosts << ENV.fetch("RAILS_DEVELOPMENT_HOST", "")' > development_app/config/initializers/gitpod.rb && - echo 'Rails.application.config.action_mailer.default_url_options = { protocol: "https" }' >> development_app/config/initializers/gitpod.rb && - cd development_app && - ./bin/rails decidim:pwa:generate_vapid_keys | grep VAPID_ >> ../.rbenv-vars && - echo "Compiling assets, please wait a moment..." && - ./bin/shakapacker - command: | - sudo service postgresql start && - { [ $(basename $PWD) == "development_app" ] || cd development_app ; } && - ./bin/rails runner 'Decidim::Organization.first.update!(host: ENV.fetch("RAILS_DEVELOPMENT_HOST", "localhost"))' && - ./bin/rails s -b 0.0.0.0 - -ports: - - name: Web App - description: The main application web server - port: 3000 - onOpen: open-preview - visibility: public - - name: Shakapacker - description: The shakapacker dev server for asset reloading - port: 3035 - onOpen: ignore - visibility: public - - name: Database - description: PostgreSQL database server - port: 5432 - onOpen: ignore - visibility: private - -github: - prebuilds: - addCheck: true - master: false - pullRequests: true - pullRequestsFromForks: true From f6cde106b64a5bf60b2b14e36897dd2552aa7f68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Wed, 14 Jan 2026 14:06:51 +0100 Subject: [PATCH 067/116] Add DPG Alliance badge (#15889) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/actions/spelling/expect.txt | 1 + README.adoc | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index f4ffadced5a2c..75c21ccd49347 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -226,6 +226,7 @@ douban downvoted downvotes downvoting +DPG draggables dragula drinkform diff --git a/README.adoc b/README.adoc index 5dee4a70c07ed..d3e03cccf3fcb 100644 --- a/README.adoc +++ b/README.adoc @@ -29,10 +29,10 @@ image:https://img.shields.io/gem/dt/decidim.svg[Gem,link=https://rubygems.org/ge image:https://img.shields.io/github/contributors/decidim/decidim.svg[GitHub contributors,link=https://github.com/decidim/decidim/graphs/contributors] image:https://img.shields.io/matrix/decidimdevs:matrix.org[Matrix,link=https://matrix.to/#/#decidimdevs:matrix.org] image:https://codecov.io/gh/decidim/decidim/branch/develop/graph/badge.svg[codecov,link=https://codecov.io/gh/decidim/decidim] -image:https://api.codeclimate.com/v1/badges/ad8fa445086e491486b6/maintainability[Maintainability,link=https://codeclimate.com/github/decidim/decidim/maintainability] -image:https://d322cqt584bo4o.cloudfront.net/decidim/localized.svg[Crowdin,link=https://crowdin.com/project/decidim] +image:https://qlty.sh/gh/decidim/projects/decidim/maintainability.svg[Maintainability,link=https://qlty.sh/gh/decidim/projects/decidim] image:https://opencollective.com/decidim/tiers/badge.svg[https://opencollective.com/decidim] image:http://img.shields.io/badge/yard-docs-blue.svg[Yard Docs,link=http://rubydoc.info/github/decidim/decidim/develop] +image:https://img.shields.io/badge/Verified-DPG-3333AB?logo=data:image/svg%2bxml;base64,PHN2ZyB3aWR0aD0iMzEiIGhlaWdodD0iMzMiIHZpZXdCb3g9IjAgMCAzMSAzMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0LjIwMDggMjEuMzY3OEwxMC4xNzM2IDE4LjAxMjRMMTEuNTIxOSAxNi40MDAzTDEzLjk5MjggMTguNDU5TDE5LjYyNjkgMTIuMjExMUwyMS4xOTA5IDEzLjYxNkwxNC4yMDA4IDIxLjM2NzhaTTI0LjYyNDEgOS4zNTEyN0wyNC44MDcxIDMuMDcyOTdMMTguODgxIDUuMTg2NjJMMTUuMzMxNCAtMi4zMzA4MmUtMDVMMTEuNzgyMSA1LjE4NjYyTDUuODU2MDEgMy4wNzI5N0w2LjAzOTA2IDkuMzUxMjdMMCAxMS4xMTc3TDMuODQ1MjEgMTYuMDg5NUwwIDIxLjA2MTJMNi4wMzkwNiAyMi44Mjc3TDUuODU2MDEgMjkuMTA2TDExLjc4MjEgMjYuOTkyM0wxNS4zMzE0IDMyLjE3OUwxOC44ODEgMjYuOTkyM0wyNC44MDcxIDI5LjEwNkwyNC42MjQxIDIyLjgyNzdMMzAuNjYzMSAyMS4wNjEyTDI2LjgxNzYgMTYuMDg5NUwzMC42NjMxIDExLjExNzdMMjQuNjI0MSA5LjM1MTI3WiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==[DPG Badge,link=https://digitalpublicgoods.net/r/decidim] ++++

From 5fcebb815444ca1f16ee21f570e26078da86283f Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Wed, 14 Jan 2026 15:12:17 +0200 Subject: [PATCH 068/116] Add documentation for the new exceptions (#15868) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add documentation for the new exceptions * Rename the recusion error * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Andrés Pereira de Lucena * Add additional details * Apply suggestions from code review Co-authored-by: Andrés Pereira de Lucena --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Andrés Pereira de Lucena --- .../develop/pages/api/reference/errors.adoc | 9 +++++++- .../errors/introspection_disabled_error.adoc | 19 +++++++++++++++ .../recursion_limit_exceeded_error.adoc | 19 +++++++++++++++ .../errors/too_many_aliases_error.adoc | 23 +++++++++++++++++++ 4 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 docs/modules/develop/pages/api/reference/errors/introspection_disabled_error.adoc create mode 100644 docs/modules/develop/pages/api/reference/errors/recursion_limit_exceeded_error.adoc create mode 100644 docs/modules/develop/pages/api/reference/errors/too_many_aliases_error.adoc diff --git a/docs/modules/develop/pages/api/reference/errors.adoc b/docs/modules/develop/pages/api/reference/errors.adoc index 0acfd284f582f..1e7a6a3f489d4 100644 --- a/docs/modules/develop/pages/api/reference/errors.adoc +++ b/docs/modules/develop/pages/api/reference/errors.adoc @@ -2,7 +2,13 @@ Currently, in Decidim we are using some exceptions to handle API responses, aiming to serve both input (receiving data through API) and output (sending data through API). -At the moment there are 3 categories of exceptions that are being handled: +At the moment there are 4 categories of exceptions that are being handled: + +== System Errors + +* xref:develop:api/reference/errors/too_many_aliases_error.adoc[Too many aliases error - TOO_MANY_ALIASES_ERROR] - is triggered when your query uses too many aliases. +* xref:develop:api/reference/errors/recursion_limit_exceeded_error.adoc[Recursion limit error - RECURSION_LIMIT_EXCEEDED_ERROR] - is triggered when your query uses a GraphQL circular reference for more than two times. +* xref:develop:api/reference/errors/introspection_disabled_error.adoc[Introspection disabled error - INTROSPECTION_DISABLED_ERROR] - is triggered when a user attempts to run introspection and they do not have the required permissions. == Authorization errors @@ -12,6 +18,7 @@ At the moment there are 3 categories of exceptions that are being handled: * xref:develop:api/reference/errors/unauthorized_mutation.adoc[Unauthorized Mutation Error - MUTATION_NOT_AUTHORIZED_ERROR], is triggered when the requesting user tries to access a mutation that cannot be accessed. == Validation errors + * xref:develop:api/reference/errors/not_found.adoc[Not Found Error - NOT_FOUND_ERROR], is triggered when the requested record does not exist in database, or is in a state where you should not have access to it (moderated, belongs to an unpublished component, belongs to a private process where the requesting user does not have access) * xref:develop:api/reference/errors/validation_error.adoc[Validation Error - VALIDATION_ERROR], is triggered when the requesting user performs an action that contains an error. This is equivalent to the flash messages displayed on the Website. * xref:develop:api/reference/errors/attribute_validation_error.adoc[Attribute Validation Error - ATTRIBUTE_VALIDATION_ERROR], is triggered when the requesting user submits a request to alter a resource that has an error. This is equivalent to the validation errors in the HTTP forms. diff --git a/docs/modules/develop/pages/api/reference/errors/introspection_disabled_error.adoc b/docs/modules/develop/pages/api/reference/errors/introspection_disabled_error.adoc new file mode 100644 index 0000000000000..9c3b64a7ac3a8 --- /dev/null +++ b/docs/modules/develop/pages/api/reference/errors/introspection_disabled_error.adoc @@ -0,0 +1,19 @@ += Introspection disabled error - INTROSPECTION_DISABLED_ERROR + +This error is triggered when a user tries to run introspections, and they do not have the required permissions. By default, Decidim API allows administrators to run introspection queries but restricts them for regular users due to security concerns. + +The JSON response structure is: + +[source, json] +---- +{ + "errors": [ + { + "message": "Introspection is disabled for this request", + "extensions": { + "code": "INTROSPECTION_DISABLED_ERROR" + } + } + ] +} +---- diff --git a/docs/modules/develop/pages/api/reference/errors/recursion_limit_exceeded_error.adoc b/docs/modules/develop/pages/api/reference/errors/recursion_limit_exceeded_error.adoc new file mode 100644 index 0000000000000..a9e1cf08d95db --- /dev/null +++ b/docs/modules/develop/pages/api/reference/errors/recursion_limit_exceeded_error.adoc @@ -0,0 +1,19 @@ += Recursion limit error - RECURSION_LIMIT_EXCEEDED_ERROR + +This error is triggered when your query uses a GraphQL circular reference for more than two times. By default, Decidim API restricts the circular reference calls due to security and performance issues. + +The JSON response structure is: + +[source, json] +---- +{ + "errors": [ + { + "message": "Too many recursions detected in query", + "extensions": { + "code": "RECURSION_LIMIT_EXCEEDED_ERROR" + } + } + ] +} +---- diff --git a/docs/modules/develop/pages/api/reference/errors/too_many_aliases_error.adoc b/docs/modules/develop/pages/api/reference/errors/too_many_aliases_error.adoc new file mode 100644 index 0000000000000..a28c9cc571d92 --- /dev/null +++ b/docs/modules/develop/pages/api/reference/errors/too_many_aliases_error.adoc @@ -0,0 +1,23 @@ += Too many Aliases error - TOO_MANY_ALIASES_ERROR + +This error is triggered when your query has too many aliases. +To avoid this, you should do any of the following: + +- Simplify the query +- If you are unable, ask for the implementer of the instance to change the `API_SCHEMA_MAX_ALIASES` environment variable. (xref:configure:environment_variables.adoc[Environment Variables page]) + +The JSON response structure is: + +[source, json] +---- +{ + "errors": [ + { + "message": "Too many aliases used. You have used 51 aliases, but 5 are allowed.", + "extensions": { + "code": "TOO_MANY_ALIASES_ERROR" + } + } + ] +} +---- From acf878e126b3eaa9fe1a9436cd2e9c404c690008 Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Thu, 15 Jan 2026 14:52:57 +0200 Subject: [PATCH 069/116] Write API: update proposals (#15795) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor API to raise and catch Decidim::PermissionAction::PermissionNotSetError exceptions * Add Decidim::Api::Errors::MutationNotAuthorizedError * Add Decidim::Api::Errors::ValidationError * Add Decidim::Api::Errors::AttributeValidationError * Add mutation to create Proposals via API * Add mutation to update Proposals via API # Conflicts: # docs/modules/develop/pages/api/proposals/mutations.adoc * Add Decidim::Api::Errors::AttributeValidationError * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix spec * Add test example * Refactor file name * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply review recommendation * Fix spec * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * change the variable name * Implement mutation arguments * Add additional I18n related exceptions * Refactor error layer * Fix error class tokens * Fix the initiatives specs * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Running linters * Refactor form * Add locale data * Add locale specific tests * Refactor API documentation * Fix typos * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Move FormFactory to be accessible from API * Apply suggestions from code review * Apply suggestions from code review * Add additional specs * Apply review recommendation * Fix attribute retrieval * Formatting * Fix specs * Fix spec * Running rubocop * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Add specs for taxonomies * Apply suggestions from code review * Apply suggestions from code review Co-authored-by: Andrés Pereira de Lucena * Fix indentation --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Andrés Pereira de Lucena --- .../mutations/answer_proposal_attributes.rb | 2 +- .../api/mutations/proposal_mutation_type.rb | 1 + .../api/mutations/update_proposal_type.rb | 53 +++++ .../lib/decidim/proposals/api.rb | 1 + .../spec/types/update_proposal_type_spec.rb | 220 ++++++++++++++++++ .../components/proposals/create.adoc | 4 +- .../components/proposals/update.adoc | 65 +++++- 7 files changed, 342 insertions(+), 4 deletions(-) create mode 100644 decidim-proposals/lib/decidim/api/mutations/update_proposal_type.rb create mode 100644 decidim-proposals/spec/types/update_proposal_type_spec.rb diff --git a/decidim-proposals/lib/decidim/api/mutations/answer_proposal_attributes.rb b/decidim-proposals/lib/decidim/api/mutations/answer_proposal_attributes.rb index 5f746c25b2142..1722f61643161 100644 --- a/decidim-proposals/lib/decidim/api/mutations/answer_proposal_attributes.rb +++ b/decidim-proposals/lib/decidim/api/mutations/answer_proposal_attributes.rb @@ -4,7 +4,7 @@ module Decidim module Proposals class AnswerProposalAttributes < Decidim::Api::Types::BaseInputObject graphql_name "AnswerProposalAttributes" - description "Attributes of a proposal" + description "Attributes for answering a proposal" argument :answer_content, GraphQL::Types::JSON, description: "The answer feedback for the status for this proposal", required: false argument :cost, GraphQL::Types::Float, description: "Estimated cost of the proposal", required: false diff --git a/decidim-proposals/lib/decidim/api/mutations/proposal_mutation_type.rb b/decidim-proposals/lib/decidim/api/mutations/proposal_mutation_type.rb index ee8497ae3e86b..52d0768348aca 100644 --- a/decidim-proposals/lib/decidim/api/mutations/proposal_mutation_type.rb +++ b/decidim-proposals/lib/decidim/api/mutations/proposal_mutation_type.rb @@ -10,6 +10,7 @@ class ProposalMutationType < Decidim::Api::Types::BaseObject field :answer, mutation: Decidim::Proposals::ProposalAnswerType, description: "Answers a proposal" field :unvote, mutation: Decidim::Proposals::UnvoteProposalType, description: "Removes a vote from a proposal" + field :update, mutation: Decidim::Proposals::UpdateProposalType, description: "Updates a proposal" field :vote, mutation: Decidim::Proposals::VoteProposalType, description: "Votes a proposal" field :withdraw, mutation: Decidim::Proposals::WithdrawProposalType, description: "Withdraws a proposal" end diff --git a/decidim-proposals/lib/decidim/api/mutations/update_proposal_type.rb b/decidim-proposals/lib/decidim/api/mutations/update_proposal_type.rb new file mode 100644 index 0000000000000..130e9419d8e18 --- /dev/null +++ b/decidim-proposals/lib/decidim/api/mutations/update_proposal_type.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module Decidim + module Proposals + class UpdateProposalType < Decidim::Api::Types::BaseMutation + graphql_name "UpdateProposal" + + description "Updates a proposal" + type Decidim::Proposals::ProposalType + + argument :attributes, ProposalAttributes, description: "Input attributes for updating a proposal", required: true + argument :locale, GraphQL::Types::String, "The locale for which to get the proposals texts", required: true + argument :toggle_translations, GraphQL::Types::Boolean, "Whether the user asked to toggle the machine translations or not.", required: true, default_value: false + + def resolve(attributes:, locale:, toggle_translations:) + set_locale(locale:, toggle_translations:) + + params = extract_from(attributes) + + form = form(Decidim::Proposals::ProposalForm).from_params(params) + + UpdateProposal.call(form, current_user, object) do + on(:ok) do |proposal| + return proposal.reload + end + + on(:invalid) do + raise Decidim::Api::Errors::AttributeValidationError, form.errors + end + end + end + + def authorized?(attributes:, locale:, toggle_translations:) + raise Decidim::Api::Errors::MutationNotAuthorizedError, I18n.t("decidim.api.errors.unauthorized_mutation") unless super && allowed_to?(:edit, :proposal, object, context) + + true + end + + private + + def extract_from(attributes) + title = attributes.to_h.fetch(:title, translated_attribute(object.title)) + body = attributes.to_h.fetch(:body, translated_attribute(object.body)) + taxonomies = Decidim::Taxonomy.where(organization: current_organization, id: attributes.to_h.fetch(:taxonomies, object.taxonomies)).pluck(:id) + address = attributes.to_h.fetch(:address, object.address) + latitude = attributes.to_h.fetch(:latitude, object.latitude) + longitude = attributes.to_h.fetch(:longitude, object.longitude) + + { title:, body:, address:, latitude:, longitude:, taxonomies: } + end + end + end +end diff --git a/decidim-proposals/lib/decidim/proposals/api.rb b/decidim-proposals/lib/decidim/proposals/api.rb index 95a523048c1b6..60f2c04f9171d 100644 --- a/decidim-proposals/lib/decidim/proposals/api.rb +++ b/decidim-proposals/lib/decidim/proposals/api.rb @@ -16,5 +16,6 @@ module Proposals autoload :VoteProposalType, "decidim/api/mutations/vote_proposal_type" autoload :UnvoteProposalType, "decidim/api/mutations/unvote_proposal_type" autoload :WithdrawProposalType, "decidim/api/mutations/withdraw_proposal_type" + autoload :UpdateProposalType, "decidim/api/mutations/update_proposal_type" end end diff --git a/decidim-proposals/spec/types/update_proposal_type_spec.rb b/decidim-proposals/spec/types/update_proposal_type_spec.rb new file mode 100644 index 0000000000000..fed4ec627cc82 --- /dev/null +++ b/decidim-proposals/spec/types/update_proposal_type_spec.rb @@ -0,0 +1,220 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Proposals + describe UpdateProposalType, type: :graphql do + include_context "with a graphql class mutation" + + let(:root_taxonomy) { create(:taxonomy, organization:) } + let!(:taxonomy) { create(:taxonomy, parent: root_taxonomy, organization:) } + let(:taxonomy_filter) { create(:taxonomy_filter, root_taxonomy:) } + let!(:taxonomy_filter_item) { create(:taxonomy_filter_item, taxonomy_filter:, taxonomy_item: taxonomy) } + let!(:taxonomies) { [taxonomy.id] } + + let(:type_class) { Decidim::Proposals::UpdateProposalType } + let(:root_klass) { ProposalMutationType } + let(:organization) { create(:organization, available_locales: [:en, :ca, :es]) } + let(:current_organization) { organization } + let(:participatory_process) { create(:participatory_process, :with_steps, organization:) } + let(:proposal_component) { create(:proposal_component, participatory_space: participatory_process, settings: { taxonomy_filters: [taxonomy_filter.id] }) } + let(:current_component) { proposal_component } + let(:author) { create(:user, organization:) } + let!(:model) { create(:proposal, component: proposal_component, users: [author]) } + let(:root_value) { model } + let(:new_title) { "Updated proposal title for testing" } + let(:new_body) { "This is an updated body content for the proposal that meets the minimum length requirements." } + let(:component) { model.component } + let(:locale) { "en" } + let(:variables) do + { + input: { + locale:, + attributes: { + title: new_title, + body: new_body, + taxonomies: + } + } + } + end + let(:query) do + <<~GRAPHQL + mutation($input: UpdateProposalInput!) { + updateProposal(input: $input) { + id + title { translation(locale: "#{locale}") } + body { translation(locale: "#{locale}") } + address + taxonomies { id } + } + } + GRAPHQL + end + + before do + I18n.locale = "en" + end + + shared_examples "update proposal mutation examples" do + context "when user is not authorized" do + let!(:current_user) { nil } + + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError exception" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + + context "when user is authorized" do + context "with valid attributes" do + context "when requesting a different locale" do + let!(:model) { create(:proposal, title: { "en" => "Original title", "ca" => "Títol original" }, component: proposal_component, users: [author]) } + let(:locale) { "ca" } + + it "updates only the language" do + update = response["updateProposal"] + expect(update).to be_present + expect(update["title"]).to include({ "translation" => new_title }) + + expect(model.reload.title).to include({ "ca" => new_title }) + end + end + + it "updates the proposal" do + update = response["updateProposal"] + + expect(update).to be_present + expect(update).to include( + { + "id" => model.id.to_s, + "title" => { + "translation" => new_title + }, + "body" => { + "translation" => new_body + } + } + ) + expect(update["taxonomies"]).to include({ "id" => taxonomy.id.to_s }) + end + + context "with address and coordinates" do + let(:address) { "Carrer de la Pau, 1, Barcelona" } + let(:latitude) { 41.3851 } + let(:longitude) { 2.1734 } + let(:variables) do + { + input: { + locale:, + attributes: { + title: new_title, + body: new_body, + address:, + latitude:, + longitude: + } + } + } + end + + it "updates the proposal with location data" do + update = response["updateProposal"] + expect(update).to be_present + expect(update).to include( + { + "id" => model.id.to_s, + "address" => address + } + ) + end + end + end + + context "with invalid attributes" do + let(:new_title) { "short" } + let(:new_body) { "x" } + + it "returns an error" do + expect { response }.to raise_error(StandardError) + end + end + end + end + + context "with proposal author" do + let!(:current_user) { author } + + it_behaves_like "update proposal mutation examples" do + let!(:user_type) { :user } + end + + context "with having invalid locale" do + let(:locale) { "tlh" } + + it "raises an error" do + expect { response }.to raise_error(Api::Errors::InvalidLocaleError, /Invalid locale provided/) + end + end + + context "with invalid attributes" do + context "with invalid title" do + context "when is missing" do + let(:new_title) { "" } + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::AttributeValidationError, /too short/) + end + end + + context "when is too short" do + let(:new_title) { "Short" } + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::AttributeValidationError, /too short/) + end + end + + context "when is all small" do + let(:new_title) { "Updated proposal title for testing".downcase } + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::AttributeValidationError, /must start with a capital letter/) + end + end + end + + context "with invalid body" do + let(:new_body) { "Short" } + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::AttributeValidationError, /too short/) + end + end + end + end + + context "with admin user" do + let!(:user_type) { :admin } + + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError exception" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + + context "with normal user (not author)" do + it "raises an Decidim::Api::Errors::MutationNotAuthorizedError exception" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + + context "with api_user" do + let!(:current_user) { author } + + it_behaves_like "update proposal mutation examples" do + let!(:user_type) { :api_user } + end + end + end + end +end diff --git a/docs/modules/develop/pages/api/reference/components/proposals/create.adoc b/docs/modules/develop/pages/api/reference/components/proposals/create.adoc index 1141810411bd2..a1e770432f04c 100644 --- a/docs/modules/develop/pages/api/reference/components/proposals/create.adoc +++ b/docs/modules/develop/pages/api/reference/components/proposals/create.adoc @@ -28,7 +28,7 @@ mutation createProposal($componentId: ID!, $input: CreateProposalInput!){ title { translation(locale: "en") } - body { + body { translation(locale: "en") } address @@ -44,7 +44,7 @@ mutation createProposal($componentId: ID!, $input: CreateProposalInput!){ } } } - } + } } ---- diff --git a/docs/modules/develop/pages/api/reference/components/proposals/update.adoc b/docs/modules/develop/pages/api/reference/components/proposals/update.adoc index 733e1987aa225..5d81fe5c384cf 100644 --- a/docs/modules/develop/pages/api/reference/components/proposals/update.adoc +++ b/docs/modules/develop/pages/api/reference/components/proposals/update.adoc @@ -1,3 +1,66 @@ = Update Proposal -include::admin:partial$under-construction.adoc[] +== Input attributes + +When you want to update data in your proposals component, you need to rely on the `ProposalAttributes` type, which has the following input fields: + +``` +• title (required) - 15-150 characters +• body (required) - minimum 15 characters +• address (optional) - physical address (requires geocoding) +• latitude (optional) - coordinate +• longitude (optional) - coordinate +• taxonomies (optional) - Taxonomies ids to assign to this proposal +``` + +== Sample request + +The `UpdateProposal` mutation allows authenticated users to update proposals in a Decidim component. This mutation delegates to the existing `Decidim::Proposals::UpdateProposal` command. + +[source,graphql] +---- +mutation updateProposal($componentId: ID!,$proposalId: ID!, $input: UpdateProposalInput!){ + component(id: $componentId) { + ...on ProposalsMutation{ + proposal(id: $proposalId) { + update(input: $input) { + id + title { translation(locale: "en") } + body { translation(locale: "en") } + } + } + } + } +} +---- + +Example of submitted variables + +[source,json] +---- +{ + "componentId": 9, + "proposalId": "140", + "input": { + "locale": "en", + "attributes": { + "title": "Install Bike Lanes on Main Street", + "body": "We propose adding dedicated bike lanes along Main Street to improve cyclist safety and encourage sustainable transportation.", + "address": "Main Street, Barcelona, Spain", + "latitude": 41.3851, + "longitude": 2.1734, + "taxonomies": [123, 456] + } + } +} +---- + +== Error Handling + +The most frequent errors that can be generated by this mutation are as follows: + +* xref:develop:api/reference/errors/unauthorized_mutation.adoc[Unauthorized Mutation Error - MUTATION_NOT_AUTHORIZED_ERROR] +* xref:develop:api/reference/errors/invalid_locale_error.adoc[Invalid Locale Error - INVALID_LOCALE_ERROR] +* xref:develop:api/reference/errors/validation_error.adoc[Validation Error - VALIDATION_ERROR] +* xref:develop:api/reference/errors/attribute_validation_error.adoc[Attribute Validation Error - ATTRIBUTE_VALIDATION_ERROR] +* xref:develop:api/reference/errors/unauthorized_object.adoc[Unauthorized Object Error - UNAUTHORIZED_OBJECT_ERROR] From 71ea61dba531ac766bbef13e7cfc8b0ba3021f3e Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Thu, 15 Jan 2026 15:07:47 +0200 Subject: [PATCH 070/116] Make results searcheable (#15881) --- .../models/decidim/accountability/result.rb | 16 ++-- .../db/data/20260113140600_reindex_results.rb | 13 +++ .../lib/decidim/accountability/component.rb | 14 +++- .../spec/db/data/reindex_results_spec.rb | 83 +++++++++++++++++++ .../decidim/accountability/component_spec.rb | 36 ++++++++ 5 files changed, 154 insertions(+), 8 deletions(-) create mode 100644 decidim-accountability/db/data/20260113140600_reindex_results.rb create mode 100644 decidim-accountability/spec/db/data/reindex_results_spec.rb diff --git a/decidim-accountability/app/models/decidim/accountability/result.rb b/decidim-accountability/app/models/decidim/accountability/result.rb index b35fc43bcc30f..a0ecbd20993a0 100644 --- a/decidim-accountability/app/models/decidim/accountability/result.rb +++ b/decidim-accountability/app/models/decidim/accountability/result.rb @@ -39,13 +39,15 @@ class Result < Accountability::ApplicationRecord after_save :update_parent_progress, if: -> { parent_id.present? } - searchable_fields( - scope_id: :decidim_scope_id, - participatory_space: { component: :participatory_space }, - A: :title, - D: :description, - datetime: :start_date - ) + searchable_fields({ + scope_id: :decidim_scope_id, + participatory_space: { component: :participatory_space }, + A: :title, + D: :description, + datetime: :start_date + }, + index_on_create: ->(result) { result.visible? }, + index_on_update: ->(result) { result.visible? }) geocoded_by :address diff --git a/decidim-accountability/db/data/20260113140600_reindex_results.rb b/decidim-accountability/db/data/20260113140600_reindex_results.rb new file mode 100644 index 0000000000000..297b7f6eb7695 --- /dev/null +++ b/decidim-accountability/db/data/20260113140600_reindex_results.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class ReindexResults < ActiveRecord::Migration[7.2] + def up + Decidim::Component.where(manifest_name: :accountability).find_each do |component| + component.manifest.run_hooks(:publish, component) + end + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/decidim-accountability/lib/decidim/accountability/component.rb b/decidim-accountability/lib/decidim/accountability/component.rb index f07e7c87126d7..d7287877013b8 100644 --- a/decidim-accountability/lib/decidim/accountability/component.rb +++ b/decidim-accountability/lib/decidim/accountability/component.rb @@ -15,6 +15,18 @@ raise StandardError, "Cannot remove this component" if Decidim::Accountability::Result.where(component: instance).any? end + component.on(:publish) do |instance| + Decidim::Accountability::Result.where(component: instance).find_in_batches(batch_size: 10) do |batch| + Decidim::UpdateSearchIndexesJob.perform_later(batch) + end + end + + component.on(:unpublish) do |instance| + Decidim::Accountability::Result.where(component: instance).find_in_batches(batch_size: 10) do |batch| + Decidim::RemoveSearchIndexesJob.perform_later(batch) + end + end + # These actions permissions can be configured in the admin panel component.actions = %w(comment vote_comment) @@ -22,7 +34,7 @@ resource.model_class_name = "Decidim::Accountability::Result" resource.template = "decidim/accountability/results/linked_results" resource.card = "decidim/accountability/result" - resource.searchable = false + resource.searchable = true resource.actions = %w(comment vote_comment) end diff --git a/decidim-accountability/spec/db/data/reindex_results_spec.rb b/decidim-accountability/spec/db/data/reindex_results_spec.rb new file mode 100644 index 0000000000000..47352e2f4be42 --- /dev/null +++ b/decidim-accountability/spec/db/data/reindex_results_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require "spec_helper" +require "./db/data/20260113140600_reindex_results" + +describe ReindexResults do + let(:migrator) do + described_class.new.tap do |m| + m.verbose = false + end + end + + describe "#up" do + context "when the component is published" do + let(:component) { create(:accountability_component, published_at: Time.zone.now) } + + context "and there are results" do + let!(:results) { create_list(:result, 2, component:) } + + it "those are added to index" do + Decidim::SearchableResource.delete_all + + expect(Decidim::SearchableResource.where(resource: results)).to be_empty + perform_enqueued_jobs { migrator.migrate(:up) } + expect(Decidim::SearchableResource.where(resource: results)).not_to be_empty + # 3 languages multiplied by 2 results + expect(Decidim::SearchableResource.where(resource: results).count).to eq(6) + end + end + + context "and there are deleted results" do + let!(:results) { create_list(:result, 2, component:) } + + it "those are added to index" do + results.map(&:destroy) + Decidim::SearchableResource.delete_all + + expect(Decidim::SearchableResource.where(resource: results)).to be_empty + perform_enqueued_jobs { migrator.migrate(:up) } + expect(Decidim::SearchableResource.where(resource: results)).to be_empty + end + end + + context "and there are no results" do + let!(:results) { [] } + + it "does not reindex the results" do + Decidim::SearchableResource.delete_all + + expect(Decidim::SearchableResource.where(resource: results)).to be_empty + perform_enqueued_jobs { migrator.migrate(:up) } + expect(Decidim::SearchableResource.where(resource: results)).to be_empty + end + end + end + + context "when the component is not published" do + let(:component) { create(:accountability_component, published_at: nil) } + let!(:results) { create_list(:result, 2, component:) } + + it "does not reindex the results" do + Decidim::SearchableResource.delete_all + + expect(Decidim::SearchableResource.where(resource: results)).to be_empty + perform_enqueued_jobs { migrator.migrate(:up) } + expect(Decidim::SearchableResource.where(resource: results)).to be_empty + end + end + + context "when the component is deleted" do + let(:component) { create(:accountability_component, published_at: Time.zone.now, deleted_at: Time.zone.now) } + let!(:results) { create_list(:result, 2, component:) } + + it "does not reindex the results" do + Decidim::SearchableResource.delete_all + + expect(Decidim::SearchableResource.where(resource: results)).to be_empty + perform_enqueued_jobs { migrator.migrate(:up) } + expect(Decidim::SearchableResource.where(resource: results)).to be_empty + end + end + end +end diff --git a/decidim-accountability/spec/lib/decidim/accountability/component_spec.rb b/decidim-accountability/spec/lib/decidim/accountability/component_spec.rb index bde48b30bc2d3..ea20635c60264 100644 --- a/decidim-accountability/spec/lib/decidim/accountability/component_spec.rb +++ b/decidim-accountability/spec/lib/decidim/accountability/component_spec.rb @@ -21,4 +21,40 @@ it_behaves_like "has mandatory config setting", :comments_max_length end end + + describe "hooks" do + let!(:results) { create_list(:result, 5, component:) } + + describe "publish" do + let(:component) { create(:accountability_component, published_at: nil) } + + it "adds the results to search index" do + expect(Decidim::SearchableResource.where(resource: results)).to be_empty + component.publish! + + perform_enqueued_jobs do + component.manifest.run_hooks(:publish, component) + end + + expect(Decidim::SearchableResource.where(resource: results)).to be_present + # 3 languages multiplied by 5 results + expect(Decidim::SearchableResource.where(resource: results).count).to eq(15) + end + end + + describe "unpublish" do + it "removes the results from search index" do + # 3 languages multiplied by 5 results + expect(Decidim::SearchableResource.where(resource: results).count).to eq(15) + expect(Decidim::SearchableResource.where(resource: results)).to be_present + component.unpublish! + + perform_enqueued_jobs do + component.manifest.run_hooks(:publish, component) + end + + expect(Decidim::SearchableResource.where(resource: results)).to be_empty + end + end + end end From f6ad7a8185997cc7ef9f1dbadbe9e4d413e8bf14 Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Thu, 15 Jan 2026 17:17:12 +0200 Subject: [PATCH 071/116] Add component hooks documentation (#15883) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Tom Greenwood <101816158+greenwoodt@users.noreply.github.com> --- docs/modules/develop/pages/components.adoc | 68 +++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/docs/modules/develop/pages/components.adoc b/docs/modules/develop/pages/components.adoc index 135825f4719d6..809b6cf6c54ea 100644 --- a/docs/modules/develop/pages/components.adoc +++ b/docs/modules/develop/pages/components.adoc @@ -47,7 +47,7 @@ Decidim.register_component(:my_component) do |component| # along with its hooks, so you can decide to halt the transaction by raising # an exception. # - # Valid hook names are :create and :destroy. + # Please refer to the section below to understand the component hooks component.on(:create) do |component| MyComponent::DoSomething.with(component) end @@ -154,6 +154,72 @@ Each setting should have one or more translation texts related for the admin zon * `decidim.components.[component_name].settings.[global|step].[attribute_name]_help`: Additional text with help for the setting use. * `decidim.components.[component_name].settings.[global|step].[attribute_name]_readonly`: Additional text for the setting when it is readonly. + +== Life Cycle + +The following hooks are being fired when an action is being done in the admin web interface: + +- `:create` - A new component is being created +- `:publish` - A component is being published +- `:unpublish` - A component is being unpublished +- `:update` - A component is being updated +- `:permission_update` - The component permissions are being changed +- `:duplicate` - A component is being copied + +[source,ruby] +---- +# :my_component is the unique name of the component that will be globally registered. +Decidim.register_component(:my_component) do |my_component| + my_component.on(:create) do |component| + # In the officially provided libraries we are using this hook to create additional required resources, like the default proposal states. + MyComponent::DoSomething.with(component) + end + + my_component.on(:publish) do |component| + # In the officially provided libraries we are using this hook to add items to the search index + MyComponent::DoSomething.with(component) + end + + my_component.on(:unpublish) do |component| + # In the officially provided libraries we are using this hook to remove items from the search index + MyComponent::DoSomething.with(component) + end + my_component.on(:update) do |component| + MyComponent::DoSomething.with(component) + end + + my_component.on(:permission_update) do |component| + MyComponent::DoSomething.with(component) + end + + my_component.on(:duplicate) do |new_component, old_component| + MyComponent::DoSomething.with(new_component) + MyComponent::DoSomethingElse.with(old_component) + end +end +---- + +In some cases, you could define your own component hooks like the following: + +[source,ruby] +---- +# :my_component is the unique name of the component that will be globally registered. +Decidim.register_component(:my_component) do |my_component| + my_component.on(:my_action) do |params| + MyComponent::DoSomething.with_the(params) + end +end +---- + +Then you could call it in your codebase as the following: + +[source,ruby] +---- +# ... Some code that you may have +@my_resource.component.run_hooks(:my_action, params) +# ... Some other code you may need +---- + == Fixtures This sections explains how to add dummy content to a development application. From 235d404372b7685e2d32d2fd5a3627008e89dce4 Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Fri, 16 Jan 2026 16:49:42 +0200 Subject: [PATCH 072/116] Fix accountability pipeline (#15896) --- .../spec/db/data/reindex_results_spec.rb | 14 +++++++++----- .../lib/decidim/accountability/component_spec.rb | 8 ++++++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/decidim-accountability/spec/db/data/reindex_results_spec.rb b/decidim-accountability/spec/db/data/reindex_results_spec.rb index 47352e2f4be42..95ff7a095cb97 100644 --- a/decidim-accountability/spec/db/data/reindex_results_spec.rb +++ b/decidim-accountability/spec/db/data/reindex_results_spec.rb @@ -10,6 +10,10 @@ end end + before do + clear_enqueued_jobs + end + describe "#up" do context "when the component is published" do let(:component) { create(:accountability_component, published_at: Time.zone.now) } @@ -21,7 +25,7 @@ Decidim::SearchableResource.delete_all expect(Decidim::SearchableResource.where(resource: results)).to be_empty - perform_enqueued_jobs { migrator.migrate(:up) } + perform_enqueued_jobs(only: Decidim::UpdateSearchIndexesJob) { migrator.migrate(:up) } expect(Decidim::SearchableResource.where(resource: results)).not_to be_empty # 3 languages multiplied by 2 results expect(Decidim::SearchableResource.where(resource: results).count).to eq(6) @@ -36,7 +40,7 @@ Decidim::SearchableResource.delete_all expect(Decidim::SearchableResource.where(resource: results)).to be_empty - perform_enqueued_jobs { migrator.migrate(:up) } + perform_enqueued_jobs(only: Decidim::UpdateSearchIndexesJob) { migrator.migrate(:up) } expect(Decidim::SearchableResource.where(resource: results)).to be_empty end end @@ -48,7 +52,7 @@ Decidim::SearchableResource.delete_all expect(Decidim::SearchableResource.where(resource: results)).to be_empty - perform_enqueued_jobs { migrator.migrate(:up) } + perform_enqueued_jobs(only: Decidim::UpdateSearchIndexesJob) { migrator.migrate(:up) } expect(Decidim::SearchableResource.where(resource: results)).to be_empty end end @@ -62,7 +66,7 @@ Decidim::SearchableResource.delete_all expect(Decidim::SearchableResource.where(resource: results)).to be_empty - perform_enqueued_jobs { migrator.migrate(:up) } + perform_enqueued_jobs(only: Decidim::UpdateSearchIndexesJob) { migrator.migrate(:up) } expect(Decidim::SearchableResource.where(resource: results)).to be_empty end end @@ -75,7 +79,7 @@ Decidim::SearchableResource.delete_all expect(Decidim::SearchableResource.where(resource: results)).to be_empty - perform_enqueued_jobs { migrator.migrate(:up) } + perform_enqueued_jobs(only: Decidim::UpdateSearchIndexesJob) { migrator.migrate(:up) } expect(Decidim::SearchableResource.where(resource: results)).to be_empty end end diff --git a/decidim-accountability/spec/lib/decidim/accountability/component_spec.rb b/decidim-accountability/spec/lib/decidim/accountability/component_spec.rb index ea20635c60264..f8f2b0f6e0d4c 100644 --- a/decidim-accountability/spec/lib/decidim/accountability/component_spec.rb +++ b/decidim-accountability/spec/lib/decidim/accountability/component_spec.rb @@ -25,6 +25,10 @@ describe "hooks" do let!(:results) { create_list(:result, 5, component:) } + before do + clear_enqueued_jobs + end + describe "publish" do let(:component) { create(:accountability_component, published_at: nil) } @@ -32,7 +36,7 @@ expect(Decidim::SearchableResource.where(resource: results)).to be_empty component.publish! - perform_enqueued_jobs do + perform_enqueued_jobs(only: Decidim::UpdateSearchIndexesJob) do component.manifest.run_hooks(:publish, component) end @@ -49,7 +53,7 @@ expect(Decidim::SearchableResource.where(resource: results)).to be_present component.unpublish! - perform_enqueued_jobs do + perform_enqueued_jobs(only: Decidim::UpdateSearchIndexesJob) do component.manifest.run_hooks(:publish, component) end From fe6d9f443f52ba311145946d01998c9f852c92ed Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Mon, 19 Jan 2026 10:30:01 +0200 Subject: [PATCH 073/116] Remove unused component hook - before_destroy (#15882) --- .../lib/decidim/accountability/component.rb | 4 --- decidim-blogs/lib/decidim/blogs/component.rb | 4 --- .../lib/decidim/budgets/component.rb | 4 --- .../lib/decidim/debates/component.rb | 4 --- .../lib/decidim/elections/component.rb | 4 --- .../lib/decidim/component/component.rb.erb | 4 --- .../lib/decidim/meetings/component.rb | 4 --- .../lib/decidim/proposals/component.rb | 4 --- .../lib/decidim/surveys/component.rb | 7 ----- .../lib/decidim/surveys/component_spec.rb | 26 ------------------- 10 files changed, 65 deletions(-) diff --git a/decidim-accountability/lib/decidim/accountability/component.rb b/decidim-accountability/lib/decidim/accountability/component.rb index d7287877013b8..848db42b5443c 100644 --- a/decidim-accountability/lib/decidim/accountability/component.rb +++ b/decidim-accountability/lib/decidim/accountability/component.rb @@ -11,10 +11,6 @@ component.permissions_class_name = "Decidim::Accountability::Permissions" component.query_type = "Decidim::Accountability::AccountabilityType" - component.on(:before_destroy) do |instance| - raise StandardError, "Cannot remove this component" if Decidim::Accountability::Result.where(component: instance).any? - end - component.on(:publish) do |instance| Decidim::Accountability::Result.where(component: instance).find_in_batches(batch_size: 10) do |batch| Decidim::UpdateSearchIndexesJob.perform_later(batch) diff --git a/decidim-blogs/lib/decidim/blogs/component.rb b/decidim-blogs/lib/decidim/blogs/component.rb index c1e80ce8090b2..fa8b60e928c0e 100644 --- a/decidim-blogs/lib/decidim/blogs/component.rb +++ b/decidim-blogs/lib/decidim/blogs/component.rb @@ -9,10 +9,6 @@ component.query_type = "Decidim::Blogs::BlogsType" - component.on(:before_destroy) do |instance| - raise StandardError, "Cannot remove this component" if Decidim::Blogs::Post.where(component: instance).any? - end - component.on(:publish) do |instance| Decidim::Blogs::Post.where(component: instance).find_in_batches(batch_size: 100) do |batch| Decidim::UpdateSearchIndexesJob.perform_later(batch) diff --git a/decidim-budgets/lib/decidim/budgets/component.rb b/decidim-budgets/lib/decidim/budgets/component.rb index 597b7971ab720..fc77f38657f92 100644 --- a/decidim-budgets/lib/decidim/budgets/component.rb +++ b/decidim-budgets/lib/decidim/budgets/component.rb @@ -17,10 +17,6 @@ component.actions = %w(vote comment vote_comment) - component.on(:before_destroy) do |instance| - raise StandardError, "Cannot remove this component" if Decidim::Budgets::Budget.where(component: instance).any? - end - component.on(:publish) do |instance| Decidim::Budgets::Budget.where(component: instance).find_each do |budget| Decidim::UpdateSearchIndexesJob.perform_later([budget]) diff --git a/decidim-debates/lib/decidim/debates/component.rb b/decidim-debates/lib/decidim/debates/component.rb index f638b5e7280ed..037a158c38454 100644 --- a/decidim-debates/lib/decidim/debates/component.rb +++ b/decidim-debates/lib/decidim/debates/component.rb @@ -12,10 +12,6 @@ component.newsletter_participant_entities = ["Decidim::Debates::Debate"] - component.on(:before_destroy) do |instance| - raise StandardError, "Cannot remove this component" if Decidim::Debates::Debate.where(component: instance).any? - end - component.on(:publish) do |instance| Decidim::Debates::Debate.where(component: instance).find_in_batches(batch_size: 100) do |batch| Decidim::UpdateSearchIndexesJob.perform_later(batch) diff --git a/decidim-elections/lib/decidim/elections/component.rb b/decidim-elections/lib/decidim/elections/component.rb index 9c6836fda5854..4b0e1bc14411c 100644 --- a/decidim-elections/lib/decidim/elections/component.rb +++ b/decidim-elections/lib/decidim/elections/component.rb @@ -10,10 +10,6 @@ component.actions = %w() - component.on(:before_destroy) do |instance| - raise StandardError, "Cannot remove this component" if Decidim::Elections::Election.where(component: instance).any? - end - component.register_stat :elections_count, primary: true, priority: Decidim::StatsRegistry::HIGH_PRIORITY do |components, _start_at, _end_at| elections = Decidim::Elections::Election.where(component: components).not_hidden elections.count diff --git a/decidim-generators/lib/decidim/generators/component_templates/lib/decidim/component/component.rb.erb b/decidim-generators/lib/decidim/generators/component_templates/lib/decidim/component/component.rb.erb index fd06a07836cea..9d8854ed5502b 100644 --- a/decidim-generators/lib/decidim/generators/component_templates/lib/decidim/component/component.rb.erb +++ b/decidim-generators/lib/decidim/generators/component_templates/lib/decidim/component/component.rb.erb @@ -7,10 +7,6 @@ Decidim.register_component(:<%= component_name %>) do |component| component.admin_engine = Decidim::<%= component_module_name %>::AdminEngine component.icon = "decidim/<%= component_name %>/icon.svg" - # component.on(:before_destroy) do |instance| - # # Code executed before removing the component - # end - # These actions permissions can be configured in the admin panel # component.actions = %w() diff --git a/decidim-meetings/lib/decidim/meetings/component.rb b/decidim-meetings/lib/decidim/meetings/component.rb index fb9ed2ba1079a..cd4248af91f83 100644 --- a/decidim-meetings/lib/decidim/meetings/component.rb +++ b/decidim-meetings/lib/decidim/meetings/component.rb @@ -14,10 +14,6 @@ "Decidim::Meetings::Meeting" ] - component.on(:before_destroy) do |instance| - raise StandardError, "Cannot remove this component" if Decidim::Meetings::Meeting.where(component: instance).any? - end - component.on(:publish) do |instance| Decidim::Meetings::Meeting.where(component: instance).find_in_batches(batch_size: 100) do |batch| Decidim::UpdateSearchIndexesJob.perform_later(batch) diff --git a/decidim-proposals/lib/decidim/proposals/component.rb b/decidim-proposals/lib/decidim/proposals/component.rb index c9c91403ce78f..fc3ff3b0033c5 100644 --- a/decidim-proposals/lib/decidim/proposals/component.rb +++ b/decidim-proposals/lib/decidim/proposals/component.rb @@ -7,10 +7,6 @@ component.icon = "media/images/decidim_proposals.svg" component.icon_key = "chat-new-line" - component.on(:before_destroy) do |instance| - raise "Cannot destroy this component when there are proposals" if Decidim::Proposals::Proposal.where(component: instance).any? - end - component.on(:create) do |instance| admin_user = GlobalID::Locator.locate(instance.versions.first.whodunnit) Decidim::Proposals.create_default_states!(instance, admin_user) diff --git a/decidim-surveys/lib/decidim/surveys/component.rb b/decidim-surveys/lib/decidim/surveys/component.rb index 0348c79bbcd93..bbe2c85187e43 100644 --- a/decidim-surveys/lib/decidim/surveys/component.rb +++ b/decidim-surveys/lib/decidim/surveys/component.rb @@ -16,13 +16,6 @@ component.newsletter_participant_entities = ["Decidim::Forms::Response"] - component.on(:before_destroy) do |instance| - survey = Decidim::Surveys::Survey.find_by(decidim_component_id: instance.id) - survey_responses_for_component = Decidim::Forms::Response.where(questionnaire: survey.questionnaire) - - raise "Cannot destroy this component when there are survey responses" if survey_responses_for_component.any? - end - component.register_resource(:survey) do |resource| resource.model_class_name = "Decidim::Surveys::Survey" resource.card = "decidim/surveys/survey" diff --git a/decidim-surveys/spec/lib/decidim/surveys/component_spec.rb b/decidim-surveys/spec/lib/decidim/surveys/component_spec.rb index 33fa5ff2e8c47..d3a461adc9683 100644 --- a/decidim-surveys/spec/lib/decidim/surveys/component_spec.rb +++ b/decidim-surveys/spec/lib/decidim/surveys/component_spec.rb @@ -8,32 +8,6 @@ let(:component) { create(:surveys_component) } let(:new_component) { create(:surveys_component) } - describe "before_destroy hooks" do - context "when there are no responses" do - before do - create(:survey, component:) - end - - it "does not raise any error" do - expect { subject.manifest.run_hooks(:before_destroy, subject) }.not_to raise_error - end - end - - context "with responses" do - before do - survey = create(:survey, component:) - create(:response, questionnaire: survey.questionnaire) - end - - it "raises an error" do - expect { subject.manifest.run_hooks(:before_destroy, subject) }.to raise_error( - RuntimeError, - "Cannot destroy this component when there are survey responses" - ) - end - end - end - context "when copying component" do it "does not raise any error" do expect { subject.manifest.run_hooks(:copy, old_component: component, new_component:) }.not_to raise_error From 17771549307cb821418a1546952738fc3749aa7f Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Mon, 19 Jan 2026 10:59:09 +0200 Subject: [PATCH 074/116] Align ProposalAnswerType mutation with latest changes (#15827) * Align Proposal Answers with latest changes * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../decidim/api/mutations/proposal_answer_type.rb | 15 ++++++--------- .../types/mutations/proposal_answer_type_spec.rb | 8 ++++---- .../reference/components/proposals/answer.adoc | 2 +- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/decidim-proposals/lib/decidim/api/mutations/proposal_answer_type.rb b/decidim-proposals/lib/decidim/api/mutations/proposal_answer_type.rb index 4e124a66fa231..e92f8acd37a2b 100644 --- a/decidim-proposals/lib/decidim/api/mutations/proposal_answer_type.rb +++ b/decidim-proposals/lib/decidim/api/mutations/proposal_answer_type.rb @@ -21,13 +21,7 @@ def resolve(attributes:) execution_period: object.execution_period ) - form = Decidim::Proposals::Admin::ProposalAnswerForm.from_params( - params - ).with_context( - current_component: object.component, - current_user:, - current_organization: current_user.organization - ) + form = form(Decidim::Proposals::Admin::ProposalAnswerForm).from_params(params) Admin::AnswerProposal.call(form, object) do on(:ok) do @@ -35,13 +29,16 @@ def resolve(attributes:) end on(:invalid) do - raise GraphQL::ExecutionError, form.errors.full_messages.join(", ") + raise Decidim::Api::Errors::AttributeValidationError, form.errors end end end def authorized?(attributes:) - super && allowed_to?(:create, :proposal_answer, object, context, scope: :admin) + authorized = super && allowed_to?(:create, :proposal_answer, object, context, scope: :admin) + raise Decidim::Api::Errors::MutationNotAuthorizedError, I18n.t("decidim.api.errors.unauthorized_mutation") unless authorized + + true end def current_user diff --git a/decidim-proposals/spec/types/mutations/proposal_answer_type_spec.rb b/decidim-proposals/spec/types/mutations/proposal_answer_type_spec.rb index 8cd758c64e3e3..b5569227f40a8 100644 --- a/decidim-proposals/spec/types/mutations/proposal_answer_type_spec.rb +++ b/decidim-proposals/spec/types/mutations/proposal_answer_type_spec.rb @@ -63,8 +63,8 @@ module Proposals shared_examples "manage proposal answer mutation examples" do context "when proposal answering disabled" do - it "throws Decidim::Api::Errors::UnauthorizedFieldError" do - expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedFieldError, "You cannot view or edit answer field on Answer because you do not have permission") + it "throws Decidim::Api::Errors::MutationNotAuthorizedError" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") end end @@ -125,8 +125,8 @@ module Proposals end context "with normal user" do - it "throws Decidim::Api::Errors::UnauthorizedFieldError" do - expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedFieldError, "You cannot view or edit answer field on Answer because you do not have permission") + it "throws Decidim::Api::Errors::MutationNotAuthorizedError" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") end end diff --git a/docs/modules/develop/pages/api/reference/components/proposals/answer.adoc b/docs/modules/develop/pages/api/reference/components/proposals/answer.adoc index 0ef6d67174f8f..2dfcb8593e585 100644 --- a/docs/modules/develop/pages/api/reference/components/proposals/answer.adoc +++ b/docs/modules/develop/pages/api/reference/components/proposals/answer.adoc @@ -76,7 +76,7 @@ curl -X POST https://your-decidim-instance.org/api \ The most frequent errors that can be generated by this mutation are as follows: - +* xref:develop:api/reference/errors/unauthorized_mutation.adoc[Unauthorized Mutation Error - MUTATION_NOT_AUTHORIZED_ERROR] * xref:develop:api/reference/errors/unauthorized_object.adoc[Unauthorized Object Error - UNAUTHORIZED_OBJECT_ERROR] * xref:develop:api/reference/errors/not_found.adoc[Not Found Error - NOT_FOUND_ERROR] * xref:develop:api/reference/errors/attribute_validation_error.adoc[Attribute Validation Error - ATTRIBUTE_VALIDATION_ERROR] From 3cb646b4c3679abb254753a9d446d28d90065421 Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Mon, 19 Jan 2026 14:23:22 +0200 Subject: [PATCH 075/116] Fix inconsistent tab behavior in the elements of the breadcrumb when focus (#15855) --- decidim-core/app/packs/stylesheets/decidim/_header.scss | 2 +- .../decidim/header/_menu_breadcrumb_desktop.html.erb | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/decidim-core/app/packs/stylesheets/decidim/_header.scss b/decidim-core/app/packs/stylesheets/decidim/_header.scss index a14a6473ad43d..4220755fb78f5 100644 --- a/decidim-core/app/packs/stylesheets/decidim/_header.scss +++ b/decidim-core/app/packs/stylesheets/decidim/_header.scss @@ -354,7 +354,7 @@ header { } &__dropdown-wrapper { - @apply flex items-center cursor-pointer underline focus:backdrop-brightness-75 focus:outline-none; + @apply flex items-center cursor-pointer underline; } &__dropdown-content { diff --git a/decidim-core/app/views/layouts/decidim/header/_menu_breadcrumb_desktop.html.erb b/decidim-core/app/views/layouts/decidim/header/_menu_breadcrumb_desktop.html.erb index 423dcc62a71ab..7d7bab41fdad3 100644 --- a/decidim-core/app/views/layouts/decidim/header/_menu_breadcrumb_desktop.html.erb +++ b/decidim-core/app/views/layouts/decidim/header/_menu_breadcrumb_desktop.html.erb @@ -1,6 +1,4 @@ -

+<%= link_to decidim.root_url, class: "menu-bar__breadcrumb-desktop__dropdown-wrapper menu-bar__breadcrumb-desktop__dropdown-trigger" do %> + <%= t("decidim.menu.home") %> +<% end %> <%= render partial: "layouts/decidim/header/menu_breadcrumb_items" %> From 3965dd2271e858423817f410f3e430ee70009d4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Mon, 19 Jan 2026 13:44:06 +0100 Subject: [PATCH 076/116] Fix devcontainer configuration (#15887) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Alexandru Emil Lupu --- .devcontainer/Dockerfile | 11 +++---- .devcontainer/compose.yaml | 44 +++++++++++++++++++++++++ .devcontainer/devcontainer.json | 51 ++++++++++++++++++----------- .devcontainer/docker-compose.yml | 27 --------------- .github/actions/spelling/expect.txt | 2 ++ .inch.yml | 5 --- bin/setup | 35 ++++++++++++++++++++ 7 files changed, 118 insertions(+), 57 deletions(-) create mode 100644 .devcontainer/compose.yaml delete mode 100644 .devcontainer/docker-compose.yml delete mode 100644 .inch.yml create mode 100755 bin/setup diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index bde812b3c811a..be516fd8db2bc 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,8 +1,7 @@ -FROM decidim/decidim:0.22.0-dev +ARG RUBY_VERSION=3.4.7 +FROM ghcr.io/rails/devcontainer/images/ruby:$RUBY_VERSION -RUN apt-get update && apt-get install -y \ - vim +RUN sudo apt-get update && \ + sudo apt-get install -y libicu-dev -ENV EDITOR=vim - -RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v1.1.1/zsh-in-docker.sh)" +ENV BINDING="0.0.0.0" diff --git a/.devcontainer/compose.yaml b/.devcontainer/compose.yaml new file mode 100644 index 0000000000000..cd4f33f2082e1 --- /dev/null +++ b/.devcontainer/compose.yaml @@ -0,0 +1,44 @@ +name: "decidim" + +services: + rails-app: + build: + context: .. + dockerfile: .devcontainer/Dockerfile + volumes: + - ../:/workspaces/decidim:cached + - bundle-cache:/usr/local/bundle + - node-modules-cache:/workspaces/decidim/node_modules + command: sleep infinity + depends_on: + - selenium + - redis + - postgres + + selenium: + image: selenium/standalone-chromium + restart: unless-stopped + + redis: + image: redis:7.2 + restart: unless-stopped + volumes: + - redis-data:/data + + postgres: + image: postgres:16.1 + restart: unless-stopped + networks: + - default + volumes: + - postgres-data:/var/lib/postgresql/data + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + +volumes: + redis-data: + postgres-data: + bundle-cache: + node-modules-cache: + diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 4544c22e09232..3f08732500f9a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,21 +1,34 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: -// https://github.com/microsoft/vscode-dev-containers/tree/v0.140.1/containers/ruby-rails { - "name": "Ruby on Rails", - "workspaceFolder": "/workspace", - "service": "rails", - "dockerComposeFile": [ - "docker-compose.yml" - ], - "settings": { - "terminal.integrated.shell.linux": "/bin/zsh" - }, - "extensions": [ - "rebornix.Ruby", - "wingrunr21.vscode-ruby", - "misogi.ruby-rubocop" - ], - "forwardPorts": [ - 3000 - ], + "name": "decidim", + "dockerComposeFile": "compose.yaml", + "service": "rails-app", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + "features": { + "ghcr.io/devcontainers/features/github-cli:1": {}, + "ghcr.io/devcontainers/features/docker-outside-of-docker:1": { + "moby": false + }, + "ghcr.io/devcontainers/features/node:1": { + "version": "22.14.0" + }, + "ghcr.io/rails/devcontainer/features/activestorage": {}, + "ghcr.io/rails/devcontainer/features/postgres-client": {} + }, + "containerEnv": { + "CAPYBARA_SERVER_PORT": "45678", + "SELENIUM_HOST": "selenium", + "REDIS_URL": "redis://redis:6379/1", + "DECIDIM_SPAM_DETECTION_BACKEND_RESOURCE_URL": "redis://redis:6379/2", + "DECIDIM_SPAM_DETECTION_BACKEND_USER_REDIS_URL": "redis://redis:6379/3", + "DATABASE_HOST": "postgres", + "DATABASE_USERNAME": "postgres", + "DATABASE_PASSWORD": "postgres" + }, + "appPort": ["3000:3000"], + "forwardPorts": [ + 3000, + 5432, + 6379 + ], + "postCreateCommand": "bin/setup --skip-server" } \ No newline at end of file diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml deleted file mode 100644 index d2ad07c963e28..0000000000000 --- a/.devcontainer/docker-compose.yml +++ /dev/null @@ -1,27 +0,0 @@ - -version: "3" - -services: - rails: - build: - context: . - dockerfile: Dockerfile - depends_on: - - db - command: /bin/sh -c "while sleep 1000; do :; done" - environment: - DATABASE_HOST: db - DATABASE_USERNAME: postgres - DATABASE_PASSWORD: postgres - db: - image: postgres:9.6 - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - PGDATA: /var/lib/postgresql/data/pgdata - restart: always - volumes: - - db:/var/lib/postgresql/data - -volumes: - db: \ No newline at end of file diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 75c21ccd49347..95410aea9edf4 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -534,6 +534,7 @@ misogi mlat mlon Mobi +moby modals modernizrrc modestbranding @@ -659,6 +660,7 @@ PGVERSION Phargo phoe pinterest +pids Placeholdit plantuml plataforma diff --git a/.inch.yml b/.inch.yml deleted file mode 100644 index c1392d007c695..0000000000000 --- a/.inch.yml +++ /dev/null @@ -1,5 +0,0 @@ -files: - included: - - lib/**/*.rb - - decidim-*/{app,lib}/**/*.rb - - decidim-*/{app,lib}/**/*.js diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000000000..1f2d4b7525c44 --- /dev/null +++ b/bin/setup @@ -0,0 +1,35 @@ +#!/usr/bin/env ruby +require "fileutils" + +APP_ROOT = File.expand_path("..", __dir__) +APP_NAME = "decidim-development-app" + +def system!(*args) + system(*args, exception: true) +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts "== Installing dependencies ==" + system! "gem install bundler --conservative" + system("bundle check") || system!("bundle install") + system("npm install") + + puts "\n== Preparing decidim development app ==" + system! "bundle exec rake development_app" unless Dir.exist?("development_app") + + puts "\n== Preparing database ==" + system("bin/rails db:create") + + if File.exist?("development_app/tmp/pids/server.pid") + puts "\n== Removing the leftover pid file ==" + system("rm development_app/tmp/pids/server.pid") + end + + puts "\n== Starting application server ==" + system! "bin/dev" +end + From 7bbb2bb28603b3ea3fd86cc91922b4ca28c4caf4 Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Mon, 19 Jan 2026 15:05:21 +0200 Subject: [PATCH 077/116] Fix search bar icon gets overwritten when search phrase is too long (#15858) * Fix searchbox padding * Apply copilot changes --- decidim-core/app/packs/stylesheets/decidim/_forms.scss | 6 +++++- decidim-core/app/packs/stylesheets/decidim/_header.scss | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/decidim-core/app/packs/stylesheets/decidim/_forms.scss b/decidim-core/app/packs/stylesheets/decidim/_forms.scss index 701746de9f606..2763f412c23c7 100644 --- a/decidim-core/app/packs/stylesheets/decidim/_forms.scss +++ b/decidim-core/app/packs/stylesheets/decidim/_forms.scss @@ -1,11 +1,15 @@ #form-search-mobile { - @apply py-1.5 px-4 bg-gray-5 rounded-lg border border-gray outline outline-1 outline-transparent; + @apply py-1.5 px-4 pr-8 bg-gray-5 rounded-lg border border-gray outline outline-1 outline-transparent; input[type="text"] { @apply bg-gray-5; } } +#input-search-page { + @apply w-full px-4 pr-8; +} + .form-defaults { /* text-like inputs */ input[type="date"], diff --git a/decidim-core/app/packs/stylesheets/decidim/_header.scss b/decidim-core/app/packs/stylesheets/decidim/_header.scss index 4220755fb78f5..e5edbee2c88c8 100644 --- a/decidim-core/app/packs/stylesheets/decidim/_header.scss +++ b/decidim-core/app/packs/stylesheets/decidim/_header.scss @@ -73,7 +73,7 @@ header { } input[type="text"] { - @apply block bg-transparent w-full px-4 py-2 h-full; + @apply block bg-transparent w-full pl-4 pr-10 py-2 h-full; &::placeholder { @apply text-neutral-500 text-sm ltr:pl-0 rtl:pr-0; From cfff6af249ff778e42e90e69658336f61f4728fe Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Mon, 19 Jan 2026 15:06:03 +0200 Subject: [PATCH 078/116] Write API: update debates (#15802) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add UpdateDebate mutation with input types and specs Co-authored-by: alecslupu <105683+alecslupu@users.noreply.github.com> * Add comprehensive documentation for UpdateDebate mutation Co-authored-by: alecslupu <105683+alecslupu@users.noreply.github.com> * Fix trailing whitespace in update_debate_type.rb Co-authored-by: alecslupu <105683+alecslupu@users.noreply.github.com> * Add implementation summary document Co-authored-by: alecslupu <105683+alecslupu@users.noreply.github.com> * Align update debate mutation with the latest changes * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Andrés Pereira de Lucena --------- Co-authored-by: alecslupu <105683+alecslupu@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Andrés Pereira de Lucena --- .../api/mutations/debate_mutation_type.rb | 1 + .../api/mutations/update_debate_type.rb | 57 +++++++ decidim-debates/lib/decidim/debates/api.rb | 1 + ...type_spec.rb => close_debate_type_spec.rb} | 0 .../spec/types/update_debate_type_spec.rb | 160 ++++++++++++++++++ .../reference/components/debates/update.adoc | 62 ++++++- 6 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 decidim-debates/lib/decidim/api/mutations/update_debate_type.rb rename decidim-debates/spec/types/{debate_mutation_type_spec.rb => close_debate_type_spec.rb} (100%) create mode 100644 decidim-debates/spec/types/update_debate_type_spec.rb diff --git a/decidim-debates/lib/decidim/api/mutations/debate_mutation_type.rb b/decidim-debates/lib/decidim/api/mutations/debate_mutation_type.rb index 06227b5b6f06a..aad97d4f93325 100644 --- a/decidim-debates/lib/decidim/api/mutations/debate_mutation_type.rb +++ b/decidim-debates/lib/decidim/api/mutations/debate_mutation_type.rb @@ -9,6 +9,7 @@ class DebateMutationType < Decidim::Api::Types::BaseObject description "A debate which includes its available mutations" field :close, mutation: Decidim::Debates::CloseDebateType, description: "Closes a debate" + field :update, mutation: Decidim::Debates::UpdateDebateType, description: "Updates a debate" end end end diff --git a/decidim-debates/lib/decidim/api/mutations/update_debate_type.rb b/decidim-debates/lib/decidim/api/mutations/update_debate_type.rb new file mode 100644 index 0000000000000..56700a49f81c8 --- /dev/null +++ b/decidim-debates/lib/decidim/api/mutations/update_debate_type.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Decidim + module Debates + class UpdateDebateType < Decidim::Api::Types::BaseMutation + graphql_name "UpdateDebate" + + description "Updates a debate" + type Decidim::Debates::DebateType + + argument :attributes, DebateAttributes, description: "input attributes of a debate", required: true + argument :locale, GraphQL::Types::String, "The locale for which to set the debate texts", required: true + argument :toggle_translations, GraphQL::Types::Boolean, "Whether the user asked to toggle the machine translations or not.", required: false, default_value: false + + def resolve(attributes:, locale:, toggle_translations:) + set_locale(locale:, toggle_translations:) + + params = extract_from(attributes) + params[:taxonomies] = Decidim::Taxonomy.where(organization: current_organization, id: params[:taxonomies]).pluck(:id) if params[:taxonomies] + + form = form(Decidim::Debates::DebateForm).from_params(params) + + UpdateDebate.call(form, object) do + on(:ok) do + return object.reload + end + + on(:invalid) do + raise Decidim::Api::Errors::AttributeValidationError, form.errors + end + end + end + + def authorized?(attributes:, locale:, toggle_translations:) + raise Decidim::Api::Errors::MutationNotAuthorizedError, I18n.t("decidim.api.errors.unauthorized_mutation") unless super && allowed_to?(:edit, :debate, object, context) + + true + end + + private + + def extract_from(attributes) + attributes = attributes.to_h.compact + + title = attributes.fetch(:title, translated_attribute(object.title)) + description = attributes.fetch(:description, translated_attribute(object.description)) + taxonomies = attributes.fetch(:taxonomies, object.taxonomies.pluck(:id)) + + { + title:, + description:, + taxonomies: + } + end + end + end +end diff --git a/decidim-debates/lib/decidim/debates/api.rb b/decidim-debates/lib/decidim/debates/api.rb index c6046ae8d9929..1cae0927b1beb 100644 --- a/decidim-debates/lib/decidim/debates/api.rb +++ b/decidim-debates/lib/decidim/debates/api.rb @@ -11,5 +11,6 @@ module Debates autoload :CloseDebateAttributes, "decidim/api/mutations/close_debate_attributes" autoload :CreateDebateType, "decidim/api/mutations/create_debate_type" autoload :DebateAttributes, "decidim/api/mutations/debate_attributes" + autoload :UpdateDebateType, "decidim/api/mutations/update_debate_type" end end diff --git a/decidim-debates/spec/types/debate_mutation_type_spec.rb b/decidim-debates/spec/types/close_debate_type_spec.rb similarity index 100% rename from decidim-debates/spec/types/debate_mutation_type_spec.rb rename to decidim-debates/spec/types/close_debate_type_spec.rb diff --git a/decidim-debates/spec/types/update_debate_type_spec.rb b/decidim-debates/spec/types/update_debate_type_spec.rb new file mode 100644 index 0000000000000..ff6714e30e83b --- /dev/null +++ b/decidim-debates/spec/types/update_debate_type_spec.rb @@ -0,0 +1,160 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Debates + describe UpdateDebateType, type: :graphql do + include_context "with a graphql class mutation" + + let(:root_klass) { DebateMutationType } + let(:participatory_process) { create(:participatory_process, :with_steps, organization: current_organization) } + let!(:current_component) do + create(:debates_component, :published, :with_creation_enabled, participatory_space: participatory_process, settings: { + taxonomy_filters: [taxonomy_filter.id] + }) + end + let!(:model) { create(:debate, author: current_user, component: current_component) } + let(:title) { "Updated title" } + let(:description) { "Updated description" } + + let(:root_taxonomy) { create(:taxonomy, organization: current_organization) } + let!(:taxonomy) { create(:taxonomy, parent: root_taxonomy, organization: current_organization) } + let(:taxonomy_filter) { create(:taxonomy_filter, root_taxonomy:) } + let!(:taxonomy_filter_item) { create(:taxonomy_filter_item, taxonomy_filter:, taxonomy_item: taxonomy) } + let!(:taxonomies) { [taxonomy.id] } + + let(:locale) { "en" } + let(:translation_locale) { "en" } + + let(:attributes) do + { + title:, + description:, + taxonomies: + } + end + + let(:variables) do + { + input: { + locale:, + attributes: + } + } + end + let(:query) do + <<~GRAPHQL + mutation($input: UpdateDebateInput!) { + update(input: $input) { + id + title { translation(locale: "en") } + description { translation(locale: "en") } + taxonomies { + name { translation(locale: "en") } + } + } + } + GRAPHQL + end + + shared_examples "manage debate mutation examples" do + context "when user is the author" do + it "updates the debate" do + update = response["update"] + expect(update).to be_present + expect(update).to include( + { + "id" => model.id.to_s, + "title" => { + "translation" => title + }, + "description" => { + "translation" => description + } + } + ) + end + + it "associates taxonomies with the debate" do + debate = response["update"] + expect(debate["taxonomies"]).to be_present + expect(debate["taxonomies"].length).to eq(1) + expect(debate["taxonomies"].first["name"]["translation"]).to eq(taxonomy.name["en"]) + end + + context "without taxonomies" do + let(:taxonomies) { [] } + + it "updates a debate without taxonomies" do + debate = response["update"] + expect(debate).to be_present + expect(debate["taxonomies"]).to be_empty + end + end + end + end + + context "when the user is not logged in" do + let!(:current_user) { nil } + let!(:model) { create(:debate, component: current_component) } + + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + + context "when owner is not the current user" do + let!(:model) { create(:debate, component: current_component) } + + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + + context "with admin user" do + it_behaves_like "manage debate mutation examples" do + let!(:user_type) { :admin } + end + end + + context "with normal user" do + it_behaves_like "manage debate mutation examples" + end + + context "with api_user" do + it_behaves_like "manage debate mutation examples" do + let!(:user_type) { :api_user } + end + end + + context "when validating" do + let!(:user_type) { :user } + + context "with having invalid locale" do + let(:locale) { "tlh" } + + it "raises an error" do + expect { response }.to raise_error(Api::Errors::InvalidLocaleError, /Invalid locale provided/) + end + end + + context "and title is missing" do + let(:title) { "" } + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::AttributeValidationError, /cannot be blank/) + end + end + + context "and description is missing" do + let(:description) { "" } + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::AttributeValidationError, /cannot be blank/) + end + end + end + end + end +end diff --git a/docs/modules/develop/pages/api/reference/components/debates/update.adoc b/docs/modules/develop/pages/api/reference/components/debates/update.adoc index 5e03f3a2c52ac..9236618cb7628 100644 --- a/docs/modules/develop/pages/api/reference/components/debates/update.adoc +++ b/docs/modules/develop/pages/api/reference/components/debates/update.adoc @@ -1,3 +1,63 @@ = Update Debate -include::admin:partial$under-construction.adoc[] +== Input attributes + +When you want to update data in your debates component, you need to rely on the `DebateAttributes` type, which has the following input fields: + +[source] +---- +• title (required) - String +• description (required) - String +• taxonomies (optional) - Taxonomy ids to assign to this debate +---- + +== Sample request + +The `UpdateDebate` mutation allows authenticated users to update debates in a Decidim debates component. This mutation uses the existing `UpdateDebate` command from the controller. + +[source,graphql] +---- +mutation updateDebate($componentId: ID!,$debateId: ID!, $input: UpdateDebateInput!){ + component(id: $componentId) { + ...on DebatesMutation{ + debate(id: $debateId){ + update(input: $input) { + id + title { translation(locale: "en") } + description { translation(locale: "en") } + taxonomies { + name { translation(locale: "en") } + } + } + } + } + } +} +---- + +Example of submitted variables + +[source,json] +---- +{ + "componentId": "5", + "debateId": "9", + "input": { + "locale": "en", + "attributes": { + "description": "Cities need more people, not more cars", + "taxonomies": [11], + "title": "More sidewalks and less roads" + } + } +} +---- + +== Error Handling + +The most frequent errors that can be generated by this mutation are as follows: + +* xref:develop:api/reference/errors/unauthorized_mutation.adoc[Unauthorized Mutation Error - MUTATION_NOT_AUTHORIZED_ERROR] +* xref:develop:api/reference/errors/attribute_validation_error.adoc[Attribute Validation Error - ATTRIBUTE_VALIDATION_ERROR] +* xref:develop:api/reference/errors/unauthorized_object.adoc[Unauthorized Object Error - UNAUTHORIZED_OBJECT_ERROR] + From 2a8e91bb3cdaae95f9a9d1afe97724fa16057a3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Wed, 21 Jan 2026 08:44:58 +0100 Subject: [PATCH 079/116] Fix validation error in user name regular expression (#15919) Co-authored-by: Hadrien Froger --- .../app/models/decidim/user_base_entity.rb | 2 +- .../models/decidim/user_base_entity_spec.rb | 77 ++++++++++++++++--- 2 files changed, 68 insertions(+), 11 deletions(-) diff --git a/decidim-core/app/models/decidim/user_base_entity.rb b/decidim-core/app/models/decidim/user_base_entity.rb index f69d8048d7633..4532c4dfde91b 100644 --- a/decidim-core/app/models/decidim/user_base_entity.rb +++ b/decidim-core/app/models/decidim/user_base_entity.rb @@ -20,7 +20,7 @@ class UserBaseEntity < ApplicationRecord has_one :blocking, class_name: "Decidim::UserBlock", foreign_key: :id, primary_key: :block_id, dependent: :destroy # Regex for name & nickname format validations - REGEXP_NAME = /\A(?!.*[<>?%&\^*#@()\[\]=+:;"{}\\|])/ + REGEXP_NAME = /\A(?!.*[<>?%&\^*#@()\[\]=+:;"{}\\|\n\r])/m REGEXP_NICKNAME = /\A[a-z0-9_-]+\z/ has_one_attached :avatar diff --git a/decidim-core/spec/models/decidim/user_base_entity_spec.rb b/decidim-core/spec/models/decidim/user_base_entity_spec.rb index 837af9b96a132..e3860f91acf18 100644 --- a/decidim-core/spec/models/decidim/user_base_entity_spec.rb +++ b/decidim-core/spec/models/decidim/user_base_entity_spec.rb @@ -9,20 +9,77 @@ module Decidim let(:organization) { create(:organization) } let(:user) { build(:user, organization:) } - let(:user_followed) { create(:user, organization: user.organization) } - let(:public_resource) { create(:dummy_resource, :published) } - let(:user_blocked) { create(:user, organization: user.organization, blocked: true) } - - before do - create(:follow, user:, followable: user_followed) - create(:follow, user:, followable: public_resource) - create(:follow, user:, followable: user_blocked) - end - describe "public followings" do + let(:user_followed) { create(:user, organization: user.organization) } + let(:public_resource) { create(:dummy_resource, :published) } + let(:user_blocked) { create(:user, organization: user.organization, blocked: true) } + + before do + user.save! + create(:follow, user:, followable: user_followed) + create(:follow, user:, followable: public_resource) + create(:follow, user:, followable: user_blocked) + end + it "return all the things followed unless the blocked users" do expect(subject.public_followings).to contain_exactly(public_resource, user_followed) end end + + describe "#validates :name" do + context "when name is John Doe" do + let(:user) { build(:user, organization:, name: "John Doe") } + + it "is valid" do + expect(user).to be_valid + end + end + + context "when name contains newlines" do + let(:user) { build(:user, organization:, name: "John\n") } + + it "is invalid" do + expect(user).not_to be_valid + expect(user.errors[:name]).to be_present + end + end + + context "when name contains carriage return" do + let(:user) { build(:user, organization:, name: "John\r") } + + it "is invalid" do + expect(user).not_to be_valid + expect(user.errors[:name]).to be_present + end + end + end + + describe "#validates :nickname" do + context "when nickname is john_doe" do + let(:user) { build(:user, organization:, nickname: "john_doe") } + + it "is valid" do + expect(user).to be_valid + end + end + + context "when nickname contains newlines" do + let(:user) { build(:user, organization:, nickname: "john\n") } + + it "is invalid" do + expect(user).not_to be_valid + expect(user.errors[:nickname]).to be_present + end + end + + context "when nickname contains carriage return" do + let(:user) { build(:user, organization:, nickname: "john\r") } + + it "is invalid" do + expect(user).not_to be_valid + expect(user.errors[:nickname]).to be_present + end + end + end end end From 8a1d8126935e1b3ce0e0d4b2bf337519dc55cb64 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 09:34:26 +0100 Subject: [PATCH 080/116] Bump qltysh/qlty-action to v2 (#15913) Co-authored-by: Alexandru Emil Lupu Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/dependabot.yml | 1 - .github/workflows/ci_generators.yml | 2 +- .github/workflows/test_app.yml | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 404bc5482f75b..6e330aa87f69a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -72,4 +72,3 @@ updates: - "dependencies" - "type: internal" - "decidim-generators" - diff --git a/.github/workflows/ci_generators.yml b/.github/workflows/ci_generators.yml index e5a4bb759b829..78dcca0e329bf 100644 --- a/.github/workflows/ci_generators.yml +++ b/.github/workflows/ci_generators.yml @@ -115,7 +115,7 @@ jobs: working-directory: ${{ env.DECIDIM_MODULE }} env: SIMPLECOV: "true" - - uses: qltysh/qlty-action/coverage@v1 + - uses: qltysh/qlty-action/coverage@v2 with: token: ${{secrets.QLTY_COVERAGE_TOKEN}} verbose: true diff --git a/.github/workflows/test_app.yml b/.github/workflows/test_app.yml index 94783b8815a5f..3ca1e3be37b10 100644 --- a/.github/workflows/test_app.yml +++ b/.github/workflows/test_app.yml @@ -130,7 +130,7 @@ jobs: SIMPLECOV: "true" SHAKAPACKER_RUNTIME_COMPILE: "false" NODE_ENV: "test" - - uses: qltysh/qlty-action/coverage@v1 + - uses: qltysh/qlty-action/coverage@v2 with: token: ${{secrets.QLTY_COVERAGE_TOKEN}} files: coverage/coverage.xml From b04406bb511146f2bcc4d92779c7d1c69affbcdf Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 09:37:10 +0100 Subject: [PATCH 081/116] Add comprehensive AI coding agent instructions for Decidim development (#15187) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial plan * Add comprehensive GitHub Copilot instructions with validated timings and NEVER CANCEL warnings Co-authored-by: andreslucena <717367+andreslucena@users.noreply.github.com> * Use rbenv for Ruby installation to avoid permission issues and vendor/bundle workarounds Co-authored-by: andreslucena <717367+andreslucena@users.noreply.github.com> * Update instructions to use development_app with rbenv, add release notes guidance, clean gitignore Co-authored-by: andreslucena <717367+andreslucena@users.noreply.github.com> * Split agent documentation files and support multiple vibe coding software * Use only AGENTS.md * Fix spelling error * 📝 qlty fmt * Fix markdownlint offenses in .ai/app-directories.md * Fix markdownlint offenses in .ai/documentation.md * Add more examples on how to run single examples on rspec * Add drop database command * Document data migration practices and examples Added guidelines for data migrations using the data-migrate gem, including best practices and examples. Suggested on code-review * Fix time of total module run and also mentiond docs on tests and parallel tests * Add capybara instructions with Google Chrome * Fix spelling * 📝 qlty fmt * Add ChromeDriver requirements to system tests documentation Co-authored-by: alecslupu <105683+alecslupu@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: andreslucena <717367+andreslucena@users.noreply.github.com> Co-authored-by: Fran Bolívar Co-authored-by: qltysh[bot] <168846912+qltysh[bot]@users.noreply.github.com> Co-authored-by: Andrés Pereira de Lucena Co-authored-by: Andrés Pereira de Lucena Co-authored-by: alecslupu <105683+alecslupu@users.noreply.github.com> --- .ai/app-directories.md | 47 +++++++ .ai/build-pipeline-integration.md | 113 ++++++++++++++++ .ai/development-workflow.md | 206 ++++++++++++++++++++++++++++++ .ai/documentation.md | 109 ++++++++++++++++ .ai/important-notes.md | 9 ++ .ai/introduction.md | 20 +++ .ai/linting.md | 39 ++++++ .ai/locales.md | 3 + .ai/requirements.md | 11 ++ .ai/testing.md | 80 ++++++++++++ .qlty/qlty.toml | 3 + AGENTS.md | 3 + 12 files changed, 643 insertions(+) create mode 100644 .ai/app-directories.md create mode 100644 .ai/build-pipeline-integration.md create mode 100644 .ai/development-workflow.md create mode 100644 .ai/documentation.md create mode 100644 .ai/important-notes.md create mode 100644 .ai/introduction.md create mode 100644 .ai/linting.md create mode 100644 .ai/locales.md create mode 100644 .ai/requirements.md create mode 100644 .ai/testing.md create mode 100644 AGENTS.md diff --git a/.ai/app-directories.md b/.ai/app-directories.md new file mode 100644 index 0000000000000..c7cab2055a6cd --- /dev/null +++ b/.ai/app-directories.md @@ -0,0 +1,47 @@ +# App directories + +As decidim is a gem for Ruby on Rails you may find the usual rails directories: controllers, models, etc. but we also have other kind of directories. + +## Standard Rails + +| Directory | Description | Technology | +|--------------|----------------------------------|------------------------| +| controllers/ | HTTP request handlers | Rails ActionController | +| models/ | ActiveRecord models and entities | Rails ActiveRecord | +| views/ | ERB templates for rendering | Rails ERB | +| helpers/ | View helper methods | Rails ActionView | +| mailers/ | Email sending classes | Rails ActionMailer | +| jobs/ | Background job classes | Rails ActiveJob | + +## Beyond Standard Rails + +| Directory | Description | Technology | +|--------------|-------------------------------------------------------------|-------------------------------------------------------------------| +| commands/ | Business logic encapsulating use cases (Command Pattern) | Custom Decidim base class (inspired by Rectify gem). | +| forms/ | Form objects for data validation and transformation | Custom Form base class with Decidim::Attributes | +| cells/ | Reusable view components | Trailblazer::Cells gem | +| events/ | Activity logging, notifications, event triggering | Custom Event classes (SimpleEvent, NotificationEvent, EmailEvent) | +| permissions/ | Authorization and permission checking logic | Custom Decidim permission system | +| queries/ | Database query objects for complex queries | Custom Query base class | +| presenters/ | Decorator classes for view-specific formatting | SimpleDelegator-based | +| services/ | Stateless utility and business service classes | Plain Ruby classes | +| validators/ | Custom validation classes | Rails Custom Validators | +| serializers/ | JSON/XML serialization for API responses | Custom serializers | +| uploaders/ | File upload handling | Custom validations for ActiveStorage | +| constraints/ | Rails routing constraints | Custom constraint classes | +| resolvers/ | GraphQL data resolution | Custom resolver classes | +| scrubbers/ | HTML content sanitization | Rails::HTML::Scrubber | +| packs/ | JavaScript/CSS entry points and assets | Shakapacker | +| assets/ | Static assets (some modules) | Asset pipeline | + +## Key Architectural Patterns + +**IMPORTANT: You must follow the existing patterns.** + +1. **Command Pattern**: Commands in `app/commands/` encapsulate single use cases and broadcast events +2. **Form Objects**: Forms in `app/forms/` handle validation separately from models +3. **Cells**: Component-based views using `Trailblazer::Cells` for reusable UI +4. **Query Objects**: Complex database queries isolated in `app/queries/` +5. **Events**: Events trigger notifications, logs, and side effects +6. **Permission System**: Scope-based authorization with action/subject model +7. **Content Block System**: Customizable page sections via manifests diff --git a/.ai/build-pipeline-integration.md b/.ai/build-pipeline-integration.md new file mode 100644 index 0000000000000..4e06c35cf7ffb --- /dev/null +++ b/.ai/build-pipeline-integration.md @@ -0,0 +1,113 @@ +# Build Pipeline Integration + +**CI Requirements:** The `.github/workflows/` contain the production CI setup: + +- Tests run on Ubuntu +- Requires PostgreSQL and Redis services +- Uses specific Ruby and Node versions +- Runs parallel tests across multiple modules +- **Each module CI can timeout at 30-60 minutes - NEVER CANCEL** + +## Pre-Commit Checklist + +Before committing, ALWAYS run: + +```bash +npm run lint +bundle exec rubocop +bundle exec erblint --lint-all +bundle exec i18n-tasks normalize --locales en +bundle exec rspec +npm run test +``` + +## Troubleshooting + +### JavaScript Lint Failures + +```bash +# View errors +npm run lint + +# Auto-fix what can be fixed +npm run lint -- --fix +``` + +### Ruby Lint Failures (RuboCop) + +```bash +# View errors with details +bundle exec rubocop + +# Auto-fix safe corrections +bundle exec rubocop -a + +# Auto-fix including unsafe corrections (review changes afterward) +bundle exec rubocop -A +``` + +### ERB Lint Failures + +```bash +# View errors +bundle exec erblint --lint-all + +# Auto-fix +bundle exec erblint --lint-all --autocorrect +``` + +### CSS/SCSS Formatting Issues + +```bash +# Check issues +npm run stylelint +npm run prettier + +# Auto-fix +npm run prettify +``` + +### Translation Key Issues + +```bash +# Normalize and sort keys (only English) +bundle exec i18n-tasks normalize --locales en + +# Find missing keys +bundle exec i18n-tasks missing + +# Find unused keys +bundle exec i18n-tasks unused +``` + +### Test Failures + +**IMPORTANT:** When a test fails, always ask the user whether you should fix the test or fix the code that the test is validating. Do not assume which approach is correct. + +1. **Read the error message carefully** - RSpec provides detailed failure information + +2. **Run the specific failing test** to iterate faster: + +```bash +cd decidim- +bundle exec rspec spec/path/to/failing_spec.rb:LINE_NUMBER +``` + +- You can pass multiple line numbers: file.rb:12:34 +- For failures in shared contexts/examples, always run the concrete example using its file:line from the failure output. +- Alternatively, run by example description: + +```bash +bundle exec rspec spec/path/to/failing_spec.rb -e "example description" +``` + +1. **Check for flaky tests** - If a test passes when run individually but fails in the suite, it may be a test isolation issue + +2. **Reset the test database** if you suspect data issues: + +```bash +cd spec/decidim_dummy_app +bin/rails db:reset RAILS_ENV=test +``` + +1. **Check for missing dependencies** - Run `bundle install` and `npm install` if tests fail with load errors diff --git a/.ai/development-workflow.md b/.ai/development-workflow.md new file mode 100644 index 0000000000000..7f5831174c65a --- /dev/null +++ b/.ai/development-workflow.md @@ -0,0 +1,206 @@ +# Development Workflow + +## Creating a Development App + +As decidim is a gem, we need to create a rails application to test it. That's why we have the `development_app`. When you generate it, you create a rails application with the decidim gem using the local files. You only need to generate it once. So if the directory already exists you don't need to generate it again unless a reset is required. + +You should not change anything inside this development app as it's a local directory that won't be persisted. + +```bash +# Install dependencies first (if not already done) +bundle install +npm install + +# Create development app for active development +bundle exec rake development_app +cd development_app +bin/dev # Starts Rails server with webpack dev server +``` + +## Key Development Files and Locations + +**Gem Structure:** Each `decidim-*` directory is a separate gem: + +- `decidim-core/` - Main framework and shared components +- `decidim-admin/` - Administrative interface +- `decidim-proposals/` - Proposal management component +- `decidim-participatory_processes/` - Process management +- `decidim-assemblies/` - Assembly management +- `decidim-meetings/` - Meeting management +- `decidim-surveys/` - Survey component +- And many more... + +**Important Files:** + +- `Rakefile` - Main build tasks and gem management +- `Gemfile` - Root dependency specification +- `package.json` - JavaScript dependencies and scripts +- `.github/workflows/` - CI/CD pipeline definitions +- `docs/` - Comprehensive documentation in AsciiDoc format + +**JavaScript Assets:** Located in each gem's `app/packs/` directory +**Stylesheets:** Located in each gem's `app/packs/stylesheets/` directory + +## Common Development Tasks + +Running the development server: + +```bash +cd development_app +bin/dev # Starts Rails + webpack dev server +# Access at http://localhost:3000 +# Admin panel: http://localhost:3000/admin (after creating admin user) +``` + +Database operations: + +```bash +cd development_app +bin/rails db:drop # Drop database +bin/rails db:create # Create database +bin/rails db:migrate # Run migrations +bin/rails db:seed # Load sample data +bin/rails db:reset # Reset and reseed database +``` + +Asset compilation: + +```bash +cd development_app +bin/rails assets:precompile +``` + +## Database Migrations + +When creating new features that require database changes, migrations belong in the appropriate `decidim-*` module, not in the development app. + +### Creating a Migration + +```bash +cd decidim- +bin/rails generate migration AddFieldToTableName field_name:type +``` + +### Migration File Location + +Migrations are stored in each gem's `db/migrate/` directory: + +```text +decidim-/ +└── db/ + └── migrate/ + └── YYYYMMDDHHMMSS_migration_name.rb +``` + +### Applying Migrations + +After creating a migration, regenerate the development or test app to apply it: + +```bash +# For development +bundle exec rake development_app + +# For testing +bundle exec rake test_app +``` + +Or apply migrations directly in an existing app: + +```bash +cd development_app # or spec/decidim_dummy_app +bin/rails decidim:upgrade +bin/rails db:migrate +``` + +### Migration Best Practices + +- Use reversible migrations when possible +- Add indexes for foreign keys and frequently queried columns +- Use `change_column_null` with a default value for non-nullable columns +- Test migrations in both directions: `bin/rails db:migrate` and `bin/rails db:rollback` + +## Data Migrations (data-migrate) + +Decidim uses the `data-migrate` gem for data changes that should not live in schema migrations (e.g. backfilling data, transforming existing records, one-off fixes). + +Use data migrations when: + +- Modifying existing data +- Backfilling new columns +- Migrating values between columns or tables +- Fixing production data inconsistencies + +Do **not** use schema migrations for these cases. + +### Creating a Data Migration + +From the appropriate decidim-* module: + +```bash +cd decidim- bin/rails generate data_migration BackfillSomething +``` + +This creates a file under: + +```text +decidim-/ +└── db/ + └── data/ + └── YYYYMMDDHHMMSS_backfill_something.rb +``` + +### Running Data Migrations + +In a development or test app: + +```bash +bin/rails data:migrate +``` + +To check status: + +```bash +bin/rails data:migrate:status +``` + +### Data Migration Best Practices + +- Never reference application models directly. +- Define a minimal ActiveRecord::Base class inside the migration. +- Always pin the table name to avoid breakage if models change: + +```ruby +class LegacyProposal < ActiveRecord::Base + self.table_name = "decidim_proposals_proposals" +end +``` + +- Avoid callbacks, validations, and scopes. +- Make migrations idempotent (safe to re-run). +- Prefer find_each for large datasets. +- Keep data migrations small and focused. + +### Example Pattern + +```ruby +class BackfillPublishedAt < ActiveRecord::Migration[6.1] + class Proposal < ActiveRecord::Base + self.table_name = "decidim_proposals_proposals" + end + + def up + Proposal.where(published_at: nil).find_each do |proposal| + proposal.update_column(:published_at, proposal.created_at) + end + end + + def down + # no-op (data migrations are usually irreversible) + end +end +``` + +### When in Doubt + +- Schema change? → regular migration +- Data change? → data-migrate diff --git a/.ai/documentation.md b/.ai/documentation.md new file mode 100644 index 0000000000000..6125e15926ac1 --- /dev/null +++ b/.ai/documentation.md @@ -0,0 +1,109 @@ +# Documentation + +We have comprehensive documentation in `docs/` using AsciiDoc format. When making changes, check if related documentation needs updating. + +## When to Read Which Docs + +### `docs/modules/install/` - Installation Guide + +**Read when:** Setting up Decidim, troubleshooting installation, updating versions, deploying to production. + +| File | Content | +|-----------------------|-------------------------------------------------------| +| `index.adoc` | Overview, creating apps, scheduled tasks, seed data | +| `manual.adoc` | Step-by-step installation (Ruby, PostgreSQL, Node.js) | +| `checklist.adoc` | Production deployment checklist | +| `update.adoc` | Updating Decidim versions, compatibility matrix | +| `empty-database.adoc` | Setup without seed data | + +### `docs/modules/develop/` - Developer Guide + +**Read when:** Developing features, understanding architecture, writing tests, contributing to core. + +| File | Content | +|------------------------------|----------------------------------------------------| +| `guide.adoc` | Entry point for development | +| `guide_architecture.adoc` | C4 diagrams, system architecture | +| `guide_conventions.adoc` | GitFlow, branch naming, commit messages | +| `modules.adoc` | Creating external modules | +| `components.adoc` | Creating components (manifests, settings, exports) | +| `testing.adoc` | RSpec, Jest, parallel testing | +| `permissions.adoc` | Permission system, adding actions | +| `notifications.adoc` | Events, email/notification generation | +| `content_blocks.adoc` | Registering content blocks | +| `api.adoc` | GraphQL API | +| `view_models_aka_cells.adoc` | Cells pattern | + +**`docs/modules/develop/pages/classes/`** - Class patterns: + +| File | Pattern | +|--------------------|-------------------------------------------------| +| `commands.adoc` | Command pattern (Create/Update/DestroyResource) | +| `forms.adoc` | Form objects (Decidim::Form) | +| `cells.adoc` | View components (Decidim::ViewModel) | +| `events.adoc` | Event classes for notifications | +| `permissions.adoc` | Permission classes | +| `queries.adoc` | Query objects | +| `presenters.adoc` | ResourcePresenter, AdminLogPresenter | +| `jobs.adoc` | ActiveJob background jobs | +| `mailers.adoc` | Mailers with locale handling | +| `controllers.adoc` | Controller patterns | +| `models.adoc` | ActiveRecord models and concerns | + +### `docs/modules/configure/` - Configuration Guide + +**Read when:** Configuring Decidim options, environment variables, system panel. + +| File | Content | +|------------------------------|------------------------------------------| +| `index.adoc` | Configuration overview, CLI flags | +| `initializer.adoc` | All Decidim.configure options | +| `system.adoc` | System panel, multi-tenant organizations | +| `environment_variables.adoc` | Environment variable reference | + +### `docs/modules/services/` - External Services + +**Read when:** Integrating maps, email, storage, or other external services. + +| File | Content | +|--------------------------|---------------------------------------| +| `activejob.adoc` | Background jobs (Sidekiq, DelayedJob) | +| `activestorage.adoc` | File storage (S3, GCS, Azure) | +| `maps.adoc` | Maps/geocoding (HERE Maps, OSM) | +| `smtp.adoc` | Email server configuration | +| `sms.adoc` | SMS gateway for verification | +| `social_providers.adoc` | OAuth providers | +| `etherpad.adoc` | Real-time collaborative editing | +| `aitools.adoc` | AI tools integration | + +### `docs/modules/customize/` - Customization Guide + +**Read when:** Customizing appearance, overriding behavior, extending functionality. + +| File | Content | +|----------------------|---------------------------------------------------| +| `code.adoc` | Monkey patching, decorators, modules | +| `views.adoc` | Overriding views (filename method, Deface, cells) | +| `styles.adoc` | CSS (Tailwind, SCSS, organization colors) | +| `javascript.adoc` | Custom JavaScript | +| `authorizations.adoc`| Custom verification handlers | +| `menu.adoc` | Navigation menu customization | +| `localization.adoc` | Translations | + +## Quick Reference by Task + +| Task | Read | +|------------------------------|----------------------------------------------------------------------| +| Creating a new command | `develop/pages/classes/commands.adoc` | +| Creating a new form | `develop/pages/classes/forms.adoc` | +| Adding a new cell | `develop/pages/classes/cells.adoc` | +| Adding notifications/events | `develop/notifications.adoc`, `develop/pages/classes/events.adoc` | +| Adding permissions | `develop/permissions.adoc`, `develop/pages/classes/permissions.adoc` | +| Creating a module | `develop/modules.adoc` | +| Creating a component | `develop/components.adoc` | +| Overriding views | `customize/views.adoc` | +| Customizing styles | `customize/styles.adoc` | +| Running tests | `develop/testing.adoc` | +| Configuring maps | `services/maps.adoc` | +| Configuring storage | `services/activestorage.adoc` | +| Production deployment | `install/checklist.adoc` | diff --git a/.ai/important-notes.md b/.ai/important-notes.md new file mode 100644 index 0000000000000..9fb1c4ce3e9f7 --- /dev/null +++ b/.ai/important-notes.md @@ -0,0 +1,9 @@ +# Important Notes + +- **NEVER CANCEL** builds or tests that take more than 2 minutes - builds can take 3+ minutes, full test suites for a module 60+ minutes +- Always use the exact Ruby and Node versions specified in `.ruby-version` and `.node-version`. +- The development app (`rake development_app`) is the primary way to create a working Decidim application for development +- Each `decidim-*` directory is an independent gem with its own tests and dependencies +- Always run full validation scenarios after making changes to ensure functionality works end-to-end +- **Changes to decidim-generators**: When making changes to `decidim-generators` that affect application configuration (files like `config/application.rb`, `config/environments/*`, etc) or other generated files, also document these changes in `RELEASE_NOTES.md` +- Read more about testing (like how to run parallel tests) at @docs/modules/develop/pages/testing.adoc diff --git a/.ai/introduction.md b/.ai/introduction.md new file mode 100644 index 0000000000000..0557e7a648cdd --- /dev/null +++ b/.ai/introduction.md @@ -0,0 +1,20 @@ +# Introduction + +[Decidim](https://decidim.org) is a free/libre, open-source digital platform for citizen participation and democratic governance. It enables citizens, organizations, and public institutions to self-organize democratically at any scale. + +## Key Features + +- Strategic planning +- Participatory processes +- Assemblies +- Initiatives and citizen consultations +- Participatory budgeting +- Networked communication + +## Who Uses Decidim + +Hundreds of organizations globally use Decidim, including city governments (Barcelona, NYC, Helsinki), the European Commission, the French Senate and National Assembly, Mexico City, the Brazilian federal government, and universities. + +## Technical Overview + +Decidim is a Ruby on Rails application with JavaScript frontend components. The codebase consists of multiple gem modules (`decidim-core`, `decidim-admin`, `decidim-proposals`, etc.), each handling specific functionality. The platform prioritizes transparency, traceability, security, and privacy. diff --git a/.ai/linting.md b/.ai/linting.md new file mode 100644 index 0000000000000..3ef6e245954e4 --- /dev/null +++ b/.ai/linting.md @@ -0,0 +1,39 @@ +# Linting + +- JavaScript linting: + +```bash +npm run lint +# Expected time: ~6 seconds +# May show warnings about React version and import paths - this is normal +``` + +- Ruby linting: + +```bash +bundle exec rubocop +# Expected time: ~1.5 seconds +# Use --parallel for faster execution +# Use -a flag for auto-correction +``` + +- ERB linting: + +```bash +bundle exec erblint --lint-all +# use --autocorrect flag for auto-correction +``` + +- CSS/SCSS linting: + +```bash +npm run stylelint +npm run prettier +# Use npm run prettify to fix formatting +``` + +- Normalize and sort translation keys: + +```bash +bundle exec i18n-tasks normalize --locales en +``` diff --git a/.ai/locales.md b/.ai/locales.md new file mode 100644 index 0000000000000..17e7b0b1f2481 --- /dev/null +++ b/.ai/locales.md @@ -0,0 +1,3 @@ +# Locales + +The locales are managed by Crowdin. When you are developing you must only take care of the `en.yml` files, do not change the other locales. diff --git a/.ai/requirements.md b/.ai/requirements.md new file mode 100644 index 0000000000000..ece4c484c3b80 --- /dev/null +++ b/.ai/requirements.md @@ -0,0 +1,11 @@ +# Requirements + +In order to run this project locally we need: + +- Ruby (you can check the version in the `.ruby-version` file) +- Node.js (you can check in `.node-version` file) +- PostgreSQL (any recent version) +- Redis server (required for background jobs) +- Google Chrome (required for system tests using Capybara) + +If any of these requirements is not installed, refer to the official installation guide: diff --git a/.ai/testing.md b/.ai/testing.md new file mode 100644 index 0000000000000..7d052a552779c --- /dev/null +++ b/.ai/testing.md @@ -0,0 +1,80 @@ +# Testing + +We use RSpec as technology for testing. + +## Creating the Test App + +As decidim is a gem, we need to create a rails application to run the tests. That's why we have the `spec/decidim_dummy_app`. When you generate it, you create a rails application with the decidim gem using the local files. You only need to generate it once. So if the directory already exists you don't need to generate it again unless a reset is required. + +You should not change anything inside this test app as it's a local directory that won't be persisted. + +```bash +# Install dependencies first (if not already done) +bundle install +npm install + +# Create test app (generates spec/decidim_dummy_app) +bundle exec rake test_app +``` + +## Running tests + +**NEVER CANCEL TESTS** - They may take several minutes to complete. + +- Main test suite (1 minute 49 seconds - NEVER CANCEL): + +```bash +bundle exec rspec +# Expected time: ~109 seconds - NEVER CANCEL +# Set timeout to 300+ seconds +``` + +- JavaScript tests (13 seconds): + +```bash +npm run test +# Expected time: ~13 seconds +# Some vendor test failures are expected (shakapacker dependencies) +``` + +- Individual module tests: + +```bash +bundle exec rake test_core # Test decidim-core +bundle exec rake test_admin # Test decidim-admin +bundle exec rake test_proposals # Test decidim-proposals +# Each module test can take 10-30 minutes - NEVER CANCEL +``` + +- Individual test: + +```bash +# You need to access the module where the test belong +# and run the rspec from there. For example: +cd decidim-core +bundle exec rspec spec/system/account_spec.rb +``` + +## Creating or updating tests + +When you create or update any of the components described in `.ai/app-directories.md` you should create or update unit tests for them. + +We also have `system` specs for integration specs. We shouldn't test all the scenarios there. Only the most relevant ones. + +You can make use of the `shared_examples`, `shared_context`, etc. directives to avoid repeating code in your test files. + +## System Tests Requirements + +Some specs (especially `spec/system`) use Capybara with a real browser. + +Requirements for running system tests locally: + +- Google Chrome must be installed and available in `PATH` +- Google ChromeDriver should also be available +- ChromeDriver should have the same version number as Google Chrome +- The test app must be generated (`spec/decidim_dummy_app`) +- Tests are executed against the dummy app; do not modify it manually + +If Chrome is missing, system specs will fail with driver or browser-related errors. + +Chrome is required both locally and in CI for running system tests. diff --git a/.qlty/qlty.toml b/.qlty/qlty.toml index f4c56c8aad8d3..31baa2e234a5b 100644 --- a/.qlty/qlty.toml +++ b/.qlty/qlty.toml @@ -102,9 +102,12 @@ name = "markdownlint" [[plugin]] name = "rubocop" +version = "1.78.0" +mode = "disabled" [[plugin]] name = "osv-scanner" [[plugin]] name = "stylelint" +mode = "disabled" diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000000..4ecee6f1ca310 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,3 @@ +**MANDATORY**: At the start of every session, you MUST read ALL `*.md` files in the `.ai` directory before proceeding with any task. + +**ALWAYS reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.** From 304038b7e7a14cee6fc3aee633f91f47fc3ba18a Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Wed, 21 Jan 2026 11:37:28 +0200 Subject: [PATCH 082/116] Show the breadcrumb in medium resolutions (700px) (#15863) * On some resolutions (700 pixels width) the breadcrumb isn't shown * Update decidim-core/app/packs/stylesheets/decidim/_header.scss Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../controllers/sticky_header/controller.js | 2 +- .../packs/stylesheets/decidim/_header.scss | 22 +++++++++---------- .../layouts/decidim/header/_menu.html.erb | 2 +- ...l.erb => _menu_breadcrumb_mobile.html.erb} | 0 4 files changed, 13 insertions(+), 13 deletions(-) rename decidim-core/app/views/layouts/decidim/header/{_menu_breadcrumb_mobile_tablet.html.erb => _menu_breadcrumb_mobile.html.erb} (100%) diff --git a/decidim-core/app/packs/src/decidim/controllers/sticky_header/controller.js b/decidim-core/app/packs/src/decidim/controllers/sticky_header/controller.js index 98e0abb00dac0..d21194f9954e7 100644 --- a/decidim-core/app/packs/src/decidim/controllers/sticky_header/controller.js +++ b/decidim-core/app/packs/src/decidim/controllers/sticky_header/controller.js @@ -41,7 +41,7 @@ export default class extends Controller { const menuBarContainer = document.querySelector("#menu-bar-container"); // Calculate margin based on screen size and sticky header height - const marginTop = this.isMaxScreenSize("sm") + const marginTop = this.isMaxScreenSize("md") ? this.element.offsetHeight : 0; diff --git a/decidim-core/app/packs/stylesheets/decidim/_header.scss b/decidim-core/app/packs/stylesheets/decidim/_header.scss index e5edbee2c88c8..56d39d3cb34b7 100644 --- a/decidim-core/app/packs/stylesheets/decidim/_header.scss +++ b/decidim-core/app/packs/stylesheets/decidim/_header.scss @@ -98,7 +98,7 @@ header { } &__menu-mobile { - @apply lg:hidden flex flex-row-reverse items-center gap-x-3; + @apply sm:hidden flex flex-row-reverse items-center gap-x-3; } &__links-desktop, @@ -107,7 +107,7 @@ header { } &__links-desktop { - @apply hidden lg:flex items-center justify-between text-center divide-x-2 divide-gray-3 ml-auto [&>*]:px-4 xl:[&>*]:px-6 first:[&>*]:pl-0 last:[&>*]:pr-0; + @apply hidden sm:flex items-center justify-between text-center divide-x-2 divide-gray-3 ml-auto [&>*]:px-4 xl:[&>*]:px-6 first:[&>*]:pl-0 last:[&>*]:pr-0; &__item { @apply flex items-center gap-x-2 text-secondary px-2 py-1 rounded; @@ -136,7 +136,7 @@ header { .menu-bar { &__main-dropdown { - @apply hidden lg:flex flex-col h-full md:p-8; + @apply hidden sm:flex flex-col h-full sm:p-8; &__left { @apply w-full; @@ -147,7 +147,7 @@ header { @apply w-full; li { - @apply py-3 md:py-3.5 border-b border-t-0 last:border-0 border-gray-3 #{!important}; + @apply py-3 sm:py-3.5 border-b border-t-0 last:border-0 border-gray-3 #{!important}; a { @apply no-underline; @@ -319,16 +319,16 @@ header { /* overwrite default dropdown styles */ [id*="dropdown-menu"] { - @apply py-0 mx-0 w-full lg:w-fit; + @apply py-0 mx-0 w-full sm:w-fit; &[aria-hidden="true"] { - @apply md:hidden; + @apply sm:hidden; } } } .menu-bar { - @apply container h-full flex justify-between items-center lg:relative last-of-type:[&>svg]:hidden; + @apply container h-full flex justify-between items-center sm:relative last-of-type:[&>svg]:hidden; &__container { @apply bg-white relative h-14 flex justify-between; @@ -542,10 +542,10 @@ header { } &__dropdown-menu { - @apply w-full md:w-1/4 px-4 md:px-0 pt-0 pb-3 md:py-3 divide-y divide-gray-3 text-[var(--secondary)]; + @apply w-full sm:w-1/4 px-4 sm:px-0 pt-0 pb-3 sm:py-3 divide-y divide-gray-3 text-[var(--secondary)]; > * { - @apply py-3 md:py-3.5 md:px-2; + @apply py-3 sm:py-3.5 sm:px-2; } a { @@ -570,10 +570,10 @@ header { } &__main-dropdown { - @apply bg-white flex flex-row rounded-b shadow-lg text-black w-full lg:w-[530px] h-screen md:h-auto; + @apply bg-white flex flex-row rounded-b shadow-lg text-black w-full sm:w-[530px] h-screen md:h-auto; &__left { - @apply p-4 md:py-8 md:px-0 space-y-5 hidden md:block md:w-3/4; + @apply p-4 sm:py-8 sm:px-0 space-y-5 hidden sm:block sm:w-3/4; &-top { @apply border-b-4 border-gray-3 pb-3; diff --git a/decidim-core/app/views/layouts/decidim/header/_menu.html.erb b/decidim-core/app/views/layouts/decidim/header/_menu.html.erb index 975925e6e8e1e..85a85115aa939 100644 --- a/decidim-core/app/views/layouts/decidim/header/_menu.html.erb +++ b/decidim-core/app/views/layouts/decidim/header/_menu.html.erb @@ -13,7 +13,7 @@ <%# secondary dropdown mobile-tablet %>
diff --git a/decidim-core/app/views/layouts/decidim/header/_menu_breadcrumb_mobile_tablet.html.erb b/decidim-core/app/views/layouts/decidim/header/_menu_breadcrumb_mobile.html.erb similarity index 100% rename from decidim-core/app/views/layouts/decidim/header/_menu_breadcrumb_mobile_tablet.html.erb rename to decidim-core/app/views/layouts/decidim/header/_menu_breadcrumb_mobile.html.erb From ea32040c76e05376b07a00258e928eb7c3df94c6 Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Wed, 21 Jan 2026 12:36:00 +0200 Subject: [PATCH 083/116] Fix accountability pipeline (#15922) --- decidim-accountability/spec/db/data/reindex_results_spec.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/decidim-accountability/spec/db/data/reindex_results_spec.rb b/decidim-accountability/spec/db/data/reindex_results_spec.rb index 95ff7a095cb97..c331052df5090 100644 --- a/decidim-accountability/spec/db/data/reindex_results_spec.rb +++ b/decidim-accountability/spec/db/data/reindex_results_spec.rb @@ -23,9 +23,12 @@ it "those are added to index" do Decidim::SearchableResource.delete_all + clear_enqueued_jobs expect(Decidim::SearchableResource.where(resource: results)).to be_empty - perform_enqueued_jobs(only: Decidim::UpdateSearchIndexesJob) { migrator.migrate(:up) } + expect { migrator.migrate(:up) }.to have_enqueued_job(Decidim::UpdateSearchIndexesJob).at_least(1).times + + perform_enqueued_jobs(only: Decidim::UpdateSearchIndexesJob) expect(Decidim::SearchableResource.where(resource: results)).not_to be_empty # 3 languages multiplied by 2 results expect(Decidim::SearchableResource.where(resource: results).count).to eq(6) From 893683a5d7fd3b568eda79424d01988c5a020332 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 13:15:21 +0100 Subject: [PATCH 084/116] Bump actions/labeler to v6 (#15911) Co-authored-by: Alexandru Emil Lupu Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/lint_pr_format.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint_pr_format.yml b/.github/workflows/lint_pr_format.yml index 499a5ffe5f538..56d7e6c359f80 100644 --- a/.github/workflows/lint_pr_format.yml +++ b/.github/workflows/lint_pr_format.yml @@ -41,7 +41,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/labeler@v5 + - uses: actions/labeler@v6 with: dot: 'true' repo-token: "${{ secrets.GITHUB_TOKEN }}" From b5a94b30df5be4d5ea6df55e4895dbb58e6e6974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Wed, 21 Jan 2026 16:34:28 +0100 Subject: [PATCH 085/116] Make the numbers in seeds non-harcoded and respect SLOW_SEEDS (#15926) --- .../lib/decidim/accountability/seeds.rb | 4 ++-- .../app/models/decidim/comments/seed.rb | 18 ++++++++++++++---- decidim-core/lib/decidim/seeds.rb | 17 +++++++++++++++++ .../lib/decidim/initiatives/seeds.rb | 2 +- .../lib/decidim/proposals/seeds.rb | 2 +- decidim-surveys/lib/decidim/surveys/seeds.rb | 6 +++--- 6 files changed, 38 insertions(+), 11 deletions(-) diff --git a/decidim-accountability/lib/decidim/accountability/seeds.rb b/decidim-accountability/lib/decidim/accountability/seeds.rb index 74974e0da6435..8fdabcb570a42 100644 --- a/decidim-accountability/lib/decidim/accountability/seeds.rb +++ b/decidim-accountability/lib/decidim/accountability/seeds.rb @@ -43,7 +43,7 @@ def create_component! end def create_statuses!(component:) - 5.times do |i| + config_value(:accountability_statuses_count).times do |i| Decidim::Accountability::Status.create!( component:, name: Decidim::Faker::Localized.word, @@ -61,7 +61,7 @@ def create_taxonomies! parent_taxonomy = root_taxonomy.children.sample || create_taxonomy!(name: ::Faker::Lorem.sentence(word_count: 5), parent: root_taxonomy) taxonomies = [parent_taxonomy] - 2.times do + config_value(:accountability_taxonomies_count).times do taxonomies << if parent_taxonomy.children.count > 1 parent_taxonomy.children.sample else diff --git a/decidim-comments/app/models/decidim/comments/seed.rb b/decidim-comments/app/models/decidim/comments/seed.rb index 0302a877b0718..04a765653addc 100644 --- a/decidim-comments/app/models/decidim/comments/seed.rb +++ b/decidim-comments/app/models/decidim/comments/seed.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "decidim/seeds" + module Decidim module Comments # A comment can belong to many Commentable models. This class is responsible @@ -20,16 +22,16 @@ def comments_for(resource) @organization = resource.organization - rand(0..6).times do + rand(0..config_value(:comments_count)).times do comment1 = create_comment(resource) NewCommentNotificationCreator.new(comment1, []).create - if [true, false].sample + if rand < config_value(:comments_nested_probability) comment2 = create_comment(comment1, resource) NewCommentNotificationCreator.new(comment2, []).create end - next if [true, false].sample + next if rand < config_value(:comments_vote_skip_probability) create_votes(comment1) if comment1 create_votes(comment2) if comment2 @@ -40,6 +42,14 @@ def comments_for(resource) attr_reader :organization + def config_value(key) + slow_seeds? ? Decidim::Seeds::SEEDS_CONFIG[key][:slow] : Decidim::Seeds::SEEDS_CONFIG[key][:fast] + end + + def slow_seeds? + Decidim::Env.new("SLOW_SEEDS").present? + end + # Creates a comment for a given resource. # # @private @@ -74,7 +84,7 @@ def create_comment(resource, root_commentable = nil) # # @return nil def create_votes(comment) - rand(0..12).times do + rand(0..config_value(:comments_votes_count)).times do author = random_user next if CommentVote.where(comment:, author:).any? diff --git a/decidim-core/lib/decidim/seeds.rb b/decidim-core/lib/decidim/seeds.rb index 0e3042274bb6a..3820b8451abb7 100644 --- a/decidim-core/lib/decidim/seeds.rb +++ b/decidim-core/lib/decidim/seeds.rb @@ -7,6 +7,19 @@ module Decidim # Base class to be inherited from the different modules' seeds classes class Seeds + SEEDS_CONFIG = { + comments_count: { slow: 6, fast: 2 }, + comments_nested_probability: { slow: 0.5, fast: 0.2 }, + comments_vote_skip_probability: { slow: 0.5, fast: 0.7 }, + comments_votes_count: { slow: 12, fast: 3 }, + surveys_responses_count: { slow: 200, fast: 20 }, + surveys_response_options_count: { slow: 3, fast: 2 }, + surveys_matrix_rows_count: { slow: 3, fast: 2 }, + initiatives_votes_count: { slow: 50, fast: 10 }, + accountability_statuses_count: { slow: 5, fast: 3 }, + accountability_taxonomies_count: { slow: 2, fast: 1 } + }.freeze + protected def slow_seeds? @@ -17,6 +30,10 @@ def number_of_records slow_seeds? ? rand(3..5) : 1 end + def config_value(key) + slow_seeds? ? SEEDS_CONFIG[key][:slow] : SEEDS_CONFIG[key][:fast] + end + def organization @organization ||= Decidim::Organization.first end diff --git a/decidim-initiatives/lib/decidim/initiatives/seeds.rb b/decidim-initiatives/lib/decidim/initiatives/seeds.rb index ce6dd6f85bf30..6e7847590bd34 100644 --- a/decidim-initiatives/lib/decidim/initiatives/seeds.rb +++ b/decidim-initiatives/lib/decidim/initiatives/seeds.rb @@ -93,7 +93,7 @@ def create_initiative!(state:) def create_initiative_votes!(initiative:) users = [] - rand(50).times do + rand(0..config_value(:initiatives_votes_count)).times do author = (Decidim::User.all - users).sample initiative.votes.create!(author:, scope: initiative.scope, hash_id: SecureRandom.hex) users << author diff --git a/decidim-proposals/lib/decidim/proposals/seeds.rb b/decidim-proposals/lib/decidim/proposals/seeds.rb index 7254c3892d281..bb176f950587b 100644 --- a/decidim-proposals/lib/decidim/proposals/seeds.rb +++ b/decidim-proposals/lib/decidim/proposals/seeds.rb @@ -17,7 +17,7 @@ def call Decidim::Proposals.create_default_states!(component, admin_user) - number_of_records = slow_seeds? ? 10 : rand(25..50) + number_of_records = slow_seeds? ? rand(25..50) : rand(5..10) (5..number_of_records).to_a.sample.times do |n| proposal = create_proposal!(component:) diff --git a/decidim-surveys/lib/decidim/surveys/seeds.rb b/decidim-surveys/lib/decidim/surveys/seeds.rb index 8f67ae902d6e5..8622bd4e326cb 100644 --- a/decidim-surveys/lib/decidim/surveys/seeds.rb +++ b/decidim-surveys/lib/decidim/surveys/seeds.rb @@ -20,7 +20,7 @@ def call next if questionnaire.questionnaire_for.allow_responses - rand(200).times { create_responses!(questionnaire:) } + rand(0..config_value(:surveys_responses_count)).times { create_responses!(questionnaire:) } end end @@ -90,7 +90,7 @@ def create_questions!(questionnaire:) position: index + 2 ) - 3.times do + config_value(:surveys_response_options_count).times do question.response_options.create!(body: Decidim::Faker::Localized.sentence) end @@ -114,7 +114,7 @@ def create_questions!(questionnaire:) position: index ) - 3.times do |position| + config_value(:surveys_matrix_rows_count).times do |position| question.response_options.create!(body: Decidim::Faker::Localized.sentence) question.matrix_rows.create!(body: Decidim::Faker::Localized.sentence, position:) end From 4c1a2b6c1c9b4c06e5f5f2e23112414e151e6157 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 08:40:56 +0100 Subject: [PATCH 086/116] Bump github/codeql-action to v4 (#15909) Co-authored-by: Alexandru Emil Lupu Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index febf5cfff946e..1221c447454b5 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,11 +58,11 @@ jobs: name: Tune NPM configuration shell: "bash" - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 with: category: "/language:${{matrix.language}}" From 510d68122551a02c3f42f3b81d92bde3d8420f68 Mon Sep 17 00:00:00 2001 From: Tom Greenwood <101816158+greenwoodt@users.noreply.github.com> Date: Fri, 23 Jan 2026 15:05:35 +0100 Subject: [PATCH 087/116] Fix quality indicator content block (#15897) * Added the locale definition to info_url within the cell. * New test space for the cell added for the quaility indicators block creation with locale * Added more elements of the HTML to rspec test of quality indiactors --- .../democratic_quality_stats_cell.rb | 2 +- .../democratic_quality_stats_cell_spec.rb | 61 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 decidim-participatory_processes/spec/cells/decidim/participatory_processes/content_blocks/democratic_quality_stats_cell_spec.rb diff --git a/decidim-participatory_processes/app/cells/decidim/participatory_processes/content_blocks/democratic_quality_stats_cell.rb b/decidim-participatory_processes/app/cells/decidim/participatory_processes/content_blocks/democratic_quality_stats_cell.rb index 1d44c4f6bd2b1..c3766e7dc3056 100644 --- a/decidim-participatory_processes/app/cells/decidim/participatory_processes/content_blocks/democratic_quality_stats_cell.rb +++ b/decidim-participatory_processes/app/cells/decidim/participatory_processes/content_blocks/democratic_quality_stats_cell.rb @@ -21,7 +21,7 @@ def presenter end def info_url - decidim.page_path("democratic-quality-indicators") + decidim.page_path("democratic-quality-indicators", locale: I18n.locale) end end end diff --git a/decidim-participatory_processes/spec/cells/decidim/participatory_processes/content_blocks/democratic_quality_stats_cell_spec.rb b/decidim-participatory_processes/spec/cells/decidim/participatory_processes/content_blocks/democratic_quality_stats_cell_spec.rb new file mode 100644 index 0000000000000..74c004c53ad1d --- /dev/null +++ b/decidim-participatory_processes/spec/cells/decidim/participatory_processes/content_blocks/democratic_quality_stats_cell_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe Decidim::ParticipatoryProcesses::ContentBlocks::DemocraticQualityStatsCell, type: :cell do + subject { cell(described_class, content_block, context: { resource: }) } + + let(:organization) { create(:organization) } + let(:resource) { create(:participatory_process, organization:) } + let(:content_block) { create(:content_block, organization:, manifest_name: :democratic_quality_stats, scope_name: :participatory_process_homepage, scoped_resource_id: resource.id) } + let(:html) { subject.call } + + controller Decidim::ParticipatoryProcesses::ParticipatoryProcessesController + + before do + allow(controller).to receive(:current_organization).and_return(organization) + end + + describe "#info_url" do + it "generates the correct page path with locale" do + I18n.with_locale(:en) do + expect(subject.send(:info_url)).to eq("/en/pages/democratic-quality-indicators") + end + end + + it "uses the current locale" do + I18n.with_locale(:es) do + expect(subject.send(:info_url)).to eq("/es/pages/democratic-quality-indicators") + end + end + end + + it "renders the democratic quality stats section" do + expect(html).to have_css("section#democratic_quality_stats") + end + + it "renders the quality indicator section title" do + expect(html).to have_css("h2.home__section-title") + end + + it "renders the global score indicator section" do + expect(html).to have_content("Global score") + end + + it "renders the automatic metrics section title" do + expect(html).to have_content("Automatic metrics") + end + + it "renders the automatic metrics indicators section" do + expect(html).to have_content("Citizen influence") + expect(html).to have_content("Hybridization") + expect(html).to have_content("Responsiveness") + expect(html).to have_content("Traceability") + end + + it "includes a link to democratic quality indicators page" do + I18n.with_locale(:en) do + expect(html).to have_link(href: "/en/pages/democratic-quality-indicators") + end + end +end From b4f0f0691578b4e8c692873f88fcde17253dfddc Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Mon, 26 Jan 2026 11:44:18 +0200 Subject: [PATCH 088/116] Fix username handling exceptions (#15928) * Fix username handling exceptions * Refactor previous iteration * Fix username handling exceptions * Fix comments specs * Patch ConferenceSpeakerPresenter --- .../admin/moderations/reports_helper.rb | 2 +- ...ters_participatory_space_users_examples.rb | 2 +- ...rator_manages_assembly_moderations_spec.rb | 3 +- .../admin/conference_speaker_presenter.rb | 10 ++++++- .../decidim/welcome_notification_event.rb | 2 +- .../presenters/decidim/log/user_presenter.rb | 3 +- .../app/presenters/decidim/user_presenter.rb | 7 ++++- .../lib/decidim/core/test/factories.rb | 8 ++++++ .../test/shared_examples/comments_examples.rb | 28 ++++++++++++++++++- .../presenters/decidim/user_presenter_spec.rb | 14 ++++++++++ .../spec/system/authentication_spec.rb | 2 +- .../dev/test/rspec_support/comments.rb | 2 +- 12 files changed, 73 insertions(+), 10 deletions(-) diff --git a/decidim-admin/app/helpers/decidim/admin/moderations/reports_helper.rb b/decidim-admin/app/helpers/decidim/admin/moderations/reports_helper.rb index ba7b387487108..eec346b0f4fff 100644 --- a/decidim-admin/app/helpers/decidim/admin/moderations/reports_helper.rb +++ b/decidim-admin/app/helpers/decidim/admin/moderations/reports_helper.rb @@ -19,7 +19,7 @@ def reportable_author_name(reportable) when User content_tag :li do link_to current_or_new_conversation_path_with(author), target: "_blank", rel: "noopener" do - "#{author.name} #{icon "mail-send-line"}".html_safe + "#{author.presenter.name} #{icon "mail-send-line"}".html_safe end end when Decidim::Meetings::Meeting diff --git a/decidim-admin/lib/decidim/admin/test/filters_participatory_space_users_examples.rb b/decidim-admin/lib/decidim/admin/test/filters_participatory_space_users_examples.rb index c6eb525ed6d95..dc43a289f55cb 100644 --- a/decidim-admin/lib/decidim/admin/test/filters_participatory_space_users_examples.rb +++ b/decidim-admin/lib/decidim/admin/test/filters_participatory_space_users_examples.rb @@ -5,7 +5,7 @@ apply_filter(label, value) within ".table-list tbody" do - expect(page).to have_content(compare_with) + expect(page).to have_content(compare_with.gsub("\n", " ")) expect(page).to have_css("tr", count: 1) end end diff --git a/decidim-assemblies/spec/system/admin/assembly_moderator_manages_assembly_moderations_spec.rb b/decidim-assemblies/spec/system/admin/assembly_moderator_manages_assembly_moderations_spec.rb index a6e4c89791a8f..3dbf99e422e35 100644 --- a/decidim-assemblies/spec/system/admin/assembly_moderator_manages_assembly_moderations_spec.rb +++ b/decidim-assemblies/spec/system/admin/assembly_moderator_manages_assembly_moderations_spec.rb @@ -6,7 +6,8 @@ include_context "when assembly moderator administrating an assembly" let(:current_component) { create(:component, participatory_space: assembly) } - let!(:reportables) { create_list(:dummy_resource, 2, component: current_component) } + let(:author) { create(:user, :malicious, :confirmed, organization: current_component.organization) } + let!(:reportables) { create_list(:dummy_resource, 2, author:, component: current_component) } let(:participatory_space_path) do decidim_admin_assemblies.moderations_path(assembly) end diff --git a/decidim-conferences/app/presenters/decidim/admin/conference_speaker_presenter.rb b/decidim-conferences/app/presenters/decidim/admin/conference_speaker_presenter.rb index f41e77d451c5e..d6ee463a4ec85 100644 --- a/decidim-conferences/app/presenters/decidim/admin/conference_speaker_presenter.rb +++ b/decidim-conferences/app/presenters/decidim/admin/conference_speaker_presenter.rb @@ -8,11 +8,19 @@ module Admin class ConferenceSpeakerPresenter < SimpleDelegator def name if user - "#{user.name} (#{Decidim::UserPresenter.new(user).nickname})" + "#{user.name} (#{user.nickname})" else full_name end end + + private + + def user + @user ||= if (user = __getobj__.user.presence) + Decidim::UserPresenter.new(user) + end + end end end end diff --git a/decidim-core/app/events/decidim/welcome_notification_event.rb b/decidim-core/app/events/decidim/welcome_notification_event.rb index d29f4dfcb76ea..687e6fd49ce5f 100644 --- a/decidim-core/app/events/decidim/welcome_notification_event.rb +++ b/decidim-core/app/events/decidim/welcome_notification_event.rb @@ -43,7 +43,7 @@ def resource_title def interpolate(template) template - .gsub("{{name}}", user.name) + .gsub("{{name}}", user.presenter.name) .gsub("{{organization}}", organization_name(organization)) .gsub("{{help_url}}", url_helpers.pages_url(host: organization.host, locale: I18n.locale)) .gsub("{{badges_url}}", url_helpers.gamification_badges_url(host: organization.host)) diff --git a/decidim-core/app/presenters/decidim/log/user_presenter.rb b/decidim-core/app/presenters/decidim/log/user_presenter.rb index 969d620b2dac2..9804a3c58e711 100644 --- a/decidim-core/app/presenters/decidim/log/user_presenter.rb +++ b/decidim-core/app/presenters/decidim/log/user_presenter.rb @@ -10,6 +10,7 @@ module Log # overwrite `BasePresenter#user_presenter` to return your custom user presenter. # The only requirement for custom renderers is that they should respond to `present`. class UserPresenter + include Decidim::SanitizeHelper # Public: Initializes the presenter. # # user - An instance of Decidim::User @@ -60,7 +61,7 @@ def present_user # # Returns an HTML-safe String. def present_user_name - extra["name"].html_safe + decidim_sanitize_translated(extra["name"]).html_safe end # Private: Presents the nickname of the user performing the action. diff --git a/decidim-core/app/presenters/decidim/user_presenter.rb b/decidim-core/app/presenters/decidim/user_presenter.rb index 14286dd10dc5d..bb49475bfa40f 100644 --- a/decidim-core/app/presenters/decidim/user_presenter.rb +++ b/decidim-core/app/presenters/decidim/user_presenter.rb @@ -6,7 +6,12 @@ module Decidim # class UserPresenter < SimpleDelegator include ActionView::Helpers::UrlHelper - include Decidim::TranslatableAttributes + include Decidim::SanitizeHelper + + # name sanitized + def name + decidim_sanitize_translated(__getobj__.name) + end # # nickname presented in a twitter-like style diff --git a/decidim-core/lib/decidim/core/test/factories.rb b/decidim-core/lib/decidim/core/test/factories.rb index e1e30f679a768..85ec7176743da 100644 --- a/decidim-core/lib/decidim/core/test/factories.rb +++ b/decidim-core/lib/decidim/core/test/factories.rb @@ -210,6 +210,14 @@ def generate_title(field = nil, skip_injection:) previous_passwords { [] } extended_data { {} } + trait :malicious do + after :create do |user| + # rubocop:disable Rails/SkipsModelValidations + user.update_column(:name, "user_#{user.id}\n") + # rubocop:enable Rails/SkipsModelValidations + end + end + trait :confirmed do confirmed_at { Time.current } end diff --git a/decidim-core/lib/decidim/core/test/shared_examples/comments_examples.rb b/decidim-core/lib/decidim/core/test/shared_examples/comments_examples.rb index c8a195f154a3a..518558417ef14 100644 --- a/decidim-core/lib/decidim/core/test/shared_examples/comments_examples.rb +++ b/decidim-core/lib/decidim/core/test/shared_examples/comments_examples.rb @@ -13,6 +13,32 @@ expect_no_js_errors end + context "when user name is improperly formatted" do + let!(:user) { create(:user, :malicious, :confirmed, organization:) } + + before do + # rubocop:disable Rails/SkipsModelValidations + comments.each do |comment| + comment.author.update_column(:name, "user_#{comment.author.id}\n") if comment.author.is_a?(Decidim::UserBaseEntity) + end + # rubocop:enable Rails/SkipsModelValidations + end + + it "properly displays the user name" do + login_as user, scope: :user + visit resource_path + + within "#add-comment-anchor" do + within "form#new_comment_for_#{commentable.commentable_type.demodulize}_#{commentable.id}" do + expect(page).to have_css("p.comment__as-author-name") + within "p.comment__as-author-name" do + expect(page).to have_content("user_#{user.id} alert('name')") + end + end + end + end + end + it "shows the list of comments for the resource" do visit resource_path @@ -21,7 +47,7 @@ within "#comments" do comments.each do |comment| - expect(page).to have_content comment.author.name + expect(page).to have_content decidim_sanitize_translated(comment.author.name).gsub("\n", " ") expect(page).to have_content comment.body.values.first end end diff --git a/decidim-core/spec/presenters/decidim/user_presenter_spec.rb b/decidim-core/spec/presenters/decidim/user_presenter_spec.rb index 903dfbcf34527..84c9e36512636 100644 --- a/decidim-core/spec/presenters/decidim/user_presenter_spec.rb +++ b/decidim-core/spec/presenters/decidim/user_presenter_spec.rb @@ -7,6 +7,20 @@ module Decidim let(:presenter) { described_class.new(user) } let(:user) { build(:user) } + describe "name" do + subject { presenter.name } + + context "when is valid" do + it { is_expected.to eq(user.name) } + end + + context "when is not valid" do + let(:user) { build(:user, name: "John\r") } + + it { is_expected.to eq("John\ralert('name')") } + end + end + describe "#nickname" do subject { presenter.nickname } diff --git a/decidim-core/spec/system/authentication_spec.rb b/decidim-core/spec/system/authentication_spec.rb index 000f4f0c73ee2..52655fddcda95 100644 --- a/decidim-core/spec/system/authentication_spec.rb +++ b/decidim-core/spec/system/authentication_spec.rb @@ -415,7 +415,7 @@ end context "when confirming the account" do - let!(:user) { create(:user, organization:) } + let!(:user) { create(:user, :malicious, organization:) } before do perform_enqueued_jobs { user.confirm } diff --git a/decidim-dev/lib/decidim/dev/test/rspec_support/comments.rb b/decidim-dev/lib/decidim/dev/test/rspec_support/comments.rb index d2f3a3a5dcbdb..968a0c8bd65a8 100644 --- a/decidim-dev/lib/decidim/dev/test/rspec_support/comments.rb +++ b/decidim-dev/lib/decidim/dev/test/rspec_support/comments.rb @@ -3,7 +3,7 @@ module CommentsHelpers def have_comment_from(user, text, opts = {}) within "#comments" do - have_content(user.name, **opts).and have_content(text, **opts) + have_content(decidim_sanitize_translated(user.name).gsub("\n", " "), **opts).and have_content(text, **opts) end end From c086ac36657df3a9c4692bce278ea70b54f16ec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Tue, 27 Jan 2026 08:04:24 +0100 Subject: [PATCH 089/116] Add devcontainer CLI wrapper script (#15916) Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .devcontainer/Dockerfile | 8 +- .devcontainer/compose.yaml | 7 +- .devcontainer/devcontainer.json | 6 +- .github/actions/spelling/expect.txt | 2 + .gitignore | 1 + bin/devcontainer | 175 ++++++++++++++++++++++++++++ bin/setup | 68 ++++++++++- 7 files changed, 252 insertions(+), 15 deletions(-) create mode 100755 bin/devcontainer diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index be516fd8db2bc..c6203667f3dd5 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,7 +1,11 @@ ARG RUBY_VERSION=3.4.7 FROM ghcr.io/rails/devcontainer/images/ruby:$RUBY_VERSION -RUN sudo apt-get update && \ - sudo apt-get install -y libicu-dev +RUN wget -q -O /tmp/google-chrome-key.pub https://dl-ssl.google.com/linux/linux_signing_key.pub \ + && sudo gpg --dearmor -o /usr/share/keyrings/google-chrome-keyring.gpg /tmp/google-chrome-key.pub \ + && echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome-keyring.gpg] http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee /etc/apt/sources.list.d/google-chrome.list \ + && sudo apt-get update \ + && sudo apt-get install -y google-chrome-stable libicu-dev \ + && sudo rm -rf /var/lib/apt/lists/* /tmp/google-chrome-key.pub ENV BINDING="0.0.0.0" diff --git a/.devcontainer/compose.yaml b/.devcontainer/compose.yaml index cd4f33f2082e1..ed22d700847e3 100644 --- a/.devcontainer/compose.yaml +++ b/.devcontainer/compose.yaml @@ -9,16 +9,13 @@ services: - ../:/workspaces/decidim:cached - bundle-cache:/usr/local/bundle - node-modules-cache:/workspaces/decidim/node_modules + ports: + - "${DEVCONTAINER_APP_PORT:-3000}:3000" command: sleep infinity depends_on: - - selenium - redis - postgres - selenium: - image: selenium/standalone-chromium - restart: unless-stopped - redis: image: redis:7.2 restart: unless-stopped diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 3f08732500f9a..bceb49495bd92 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,7 +2,7 @@ "name": "decidim", "dockerComposeFile": "compose.yaml", "service": "rails-app", - "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + "workspaceFolder": "/workspaces/decidim", "features": { "ghcr.io/devcontainers/features/github-cli:1": {}, "ghcr.io/devcontainers/features/docker-outside-of-docker:1": { @@ -15,8 +15,6 @@ "ghcr.io/rails/devcontainer/features/postgres-client": {} }, "containerEnv": { - "CAPYBARA_SERVER_PORT": "45678", - "SELENIUM_HOST": "selenium", "REDIS_URL": "redis://redis:6379/1", "DECIDIM_SPAM_DETECTION_BACKEND_RESOURCE_URL": "redis://redis:6379/2", "DECIDIM_SPAM_DETECTION_BACKEND_USER_REDIS_URL": "redis://redis:6379/3", @@ -31,4 +29,4 @@ 6379 ], "postCreateCommand": "bin/setup --skip-server" -} \ No newline at end of file +} diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 95410aea9edf4..e1fe10eb61d43 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -139,6 +139,7 @@ cobertura codecov codefor CODEOWNERS +codespace codeql coditramuntana commentables @@ -222,6 +223,7 @@ doesnot doggotrainer Dokku Dota +dotfiles douban downvoted downvotes diff --git a/.gitignore b/.gitignore index c3bd03e19df05..cc1d4ed7569ed 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ decidim-packs packages/**/package-lock.json temporary_changelog.md +.devcontainer/devcontainer.local.* diff --git a/bin/devcontainer b/bin/devcontainer new file mode 100755 index 0000000000000..6c975c41f5a7d --- /dev/null +++ b/bin/devcontainer @@ -0,0 +1,175 @@ +#!/usr/bin/env bash + +# Configuration +CONTAINER_NAME="decidim-rails-app" +SCRIPT_NAME=$(basename "$0") +CONTAINERS_TO_STOP=("decidim-rails-app-1" "decidim-selenium-1" "decidim-postgres-1" "decidim-redis-1") +DEVCONTAINER_DIR=".devcontainer" +DEVCONTAINER_JSON="$DEVCONTAINER_DIR/devcontainer.json" +DEVCONTAINER_LOCAL_JSON="$DEVCONTAINER_DIR/devcontainer.local.json" +DEVCONTAINER_BACKUP_JSON="$DEVCONTAINER_DIR/devcontainer.json.backup" + +# Function to use local config if it exists +# +# This allows having a file with local instructions for your personal developer environment at +# .devcontainer/devcontainer.local.json +# So, for instance, you can have +# +# ```json +# { +# "postCreateCommand": ".devcontainer/devcontainer.local.postCreateCommand.bash" +# } +# ``` +# +# And there you can add your own packages, dotfiles, etc. Do not forget to also call `bin/setup` +# at the end of your script. +# +use_local_config() { + if [ -f "$DEVCONTAINER_LOCAL_JSON" ]; then + # Check if jq is available + if command -v jq &> /dev/null; then + echo "Merging devcontainer.local.json with devcontainer.json..." >&2 + + # Backup original + cp "$DEVCONTAINER_JSON" "$DEVCONTAINER_BACKUP_JSON" + + # Merge configs: base config with local overrides + # jq -s '.[0] * .[1]' merges two objects, with the second overriding the first + if jq -s '.[0] * .[1]' "$DEVCONTAINER_BACKUP_JSON" "$DEVCONTAINER_LOCAL_JSON" > "$DEVCONTAINER_JSON"; then + return 0 + else + echo "Error: Failed to merge JSON configs. Restoring backup..." >&2 + mv "$DEVCONTAINER_BACKUP_JSON" "$DEVCONTAINER_JSON" + return 1 + fi + else + echo "Warning: jq not found. Install jq to use devcontainer.local.json." >&2 + return 1 + fi + fi + return 1 +} + +# Function to restore original devcontainer config +restore_devcontainer_config() { + if [ -f "$DEVCONTAINER_BACKUP_JSON" ]; then + mv "$DEVCONTAINER_BACKUP_JSON" "$DEVCONTAINER_JSON" + fi +} + +# Check if devcontainer CLI is installed +if ! command -v devcontainer &> /dev/null; then + echo "devcontainer CLI not found. Installing..." + npm install -g @devcontainers/cli + + # Check if installation was successful + if ! command -v devcontainer &> /dev/null; then + echo "Error: Failed to install devcontainer CLI" + exit 1 + fi + echo "devcontainer CLI installed successfully" +fi + +# Handle commands that don't need container ID first +case "$1" in + up) + use_local_config + devcontainer up --workspace-folder=. + status=$? + restore_devcontainer_config + exit "$status" + ;; + down) + echo "Stopping containers..." + for container in "${CONTAINERS_TO_STOP[@]}"; do + if docker ps -a --format "{{.Names}}" | grep -q "^${container}$"; then + echo "Stopping $container..." + docker stop "$container" + else + echo "Container $container not found, skipping..." + fi + done + restore_devcontainer_config + exit 0 + ;; + rebuild) + echo "Removing development_app directory..." + rm -rf development_app/ + use_local_config + devcontainer up --workspace-folder=. --remove-existing-container + status=$? + restore_devcontainer_config + exit "$status" + ;; + rm) + echo "Removing all containers and cleaning up..." + for container in "${CONTAINERS_TO_STOP[@]}"; do + if docker ps -a --format "{{.Names}}" | grep -q "^${container}$"; then + echo "Removing $container..." + docker rm -f "$container" + else + echo "Container $container not found, skipping..." + fi + done + echo "Cleaning up development_app directory..." + rm -rf development_app/ + restore_devcontainer_config + echo "Cleanup complete!" + exit 0 + ;; + ps) + echo "Checking container status..." + echo "" + found_any=false + for container in "${CONTAINERS_TO_STOP[@]}"; do + if docker ps -a --format "{{.Names}}" | grep -q "^${container}$"; then + found_any=true + status=$(docker ps -a --format "{{.Names}}\t{{.Status}}" | grep "^${container}\s" | cut -f2) + echo " $container: $status" + fi + done + if [ "$found_any" = false ]; then + echo " No containers found" + fi + exit 0 + ;; + ""| help | --help | -h) + echo "Usage: $SCRIPT_NAME " + echo "" + echo "Container Management:" + echo " up - Start the devcontainer" + echo " down - Stop all containers" + echo " rebuild - Rebuild the devcontainer" + echo " rm - Remove all containers and clean up" + echo " ps - Check container status" + echo "" + echo "Execute Commands:" + echo " $SCRIPT_NAME Execute any command in the container" + echo "" + echo "Examples:" + echo " $SCRIPT_NAME dev - Run bin/dev" + echo " $SCRIPT_NAME rails console - Run bin/rails console" + echo " $SCRIPT_NAME bundle install - Run bin/bundle install" + echo " $SCRIPT_NAME bundle exec rake test_app - Generates the test_app" + echo " $SCRIPT_NAME rspec decidim-budgets/spec/models/order_spec.rb - Runs the specs for the budgets' order model" + exit 0 + ;; +esac + +# Get the container ID for decidim-rails-app +container_id=$(docker ps --filter "name=$CONTAINER_NAME" --format "{{.ID}}" | head -n 1) + +# Check if container was found +if [ -z "$container_id" ]; then + echo "Error: $CONTAINER_NAME container not found. Start it with '$SCRIPT_NAME up'" + exit 1 +fi + +# Check if the command exists in ./bin directory +if [ -f "./bin/$1" ] && [ -x "./bin/$1" ]; then + # If it's a script in bin/, prepend bin/ to the path + devcontainer exec --container-id "$container_id" --workspace-folder=. "bin/$@" +else + # Otherwise execute the command as-is + devcontainer exec --container-id "$container_id" --workspace-folder=. "$@" +fi diff --git a/bin/setup b/bin/setup index 1f2d4b7525c44..e472a0a696a9e 100755 --- a/bin/setup +++ b/bin/setup @@ -1,11 +1,66 @@ #!/usr/bin/env ruby require "fileutils" +require "json" APP_ROOT = File.expand_path("..", __dir__) APP_NAME = "decidim-development-app" -def system!(*args) - system(*args, exception: true) +def system!(env = {}, *args) + if args.empty? + # env is actually the command + system(env, exception: true) + else + system(env, *args, exception: true) + end +end + +class CodespacesConfig + ENV_FILE = "/workspaces/.codespaces/shared/environment-variables.json" + APP_CONFIG = "development_app/config/application.rb" + + def self.running? + File.exist?(ENV_FILE) + end + + def initialize + @hostname = nil + end + + def hostname + @hostname ||= fetch_hostname + end + + def env_vars + hostname ? { "DECIDIM_HOST" => hostname } : {} + end + + def configure_application! + return unless hostname + return unless File.exist?(APP_CONFIG) + + config_content = File.read(APP_CONFIG) + return if config_content.match?(/^\s+config\.hosts/) + + updated_content = config_content.sub( + /(config\.load_defaults\s+[\d.]+\s*\n)/, + "\\1 config.hosts << \"#{hostname}\"\n" + ) + + File.write(APP_CONFIG, updated_content) + puts "\n== Configured Codespaces host: #{hostname} ==" + end + + private + + def fetch_hostname + return nil unless File.exist?(ENV_FILE) + + env_data = JSON.parse(File.read(ENV_FILE)) + codespace_name = env_data["CODESPACE_NAME"] + return nil unless codespace_name + + "#{codespace_name}-3000.app.github.dev" + end end FileUtils.chdir APP_ROOT do @@ -19,7 +74,13 @@ FileUtils.chdir APP_ROOT do system("npm install") puts "\n== Preparing decidim development app ==" - system! "bundle exec rake development_app" unless Dir.exist?("development_app") + if CodespacesConfig.running? + codespaces = CodespacesConfig.new + system!(codespaces.env_vars, "bundle exec rake development_app") unless Dir.exist?("development_app") + codespaces.configure_application! + else + system!("bundle exec rake development_app") unless Dir.exist?("development_app") + end puts "\n== Preparing database ==" system("bin/rails db:create") @@ -32,4 +93,3 @@ FileUtils.chdir APP_ROOT do puts "\n== Starting application server ==" system! "bin/dev" end - From 2ca7edf056d5e6235973d8b87541697487aa7114 Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Tue, 27 Jan 2026 10:03:11 +0200 Subject: [PATCH 090/116] Refactor API based comments (#15794) * Refactor API to raise and catch Decidim::PermissionAction::PermissionNotSetError exceptions * Add Decidim::Api::Errors::MutationNotAuthorizedError * Add Decidim::Api::Errors::ValidationError * Add Decidim::Api::Errors::AttributeValidationError * Refactor API based comments * Adjust specs * Remove unusued locale --- .../lib/decidim/api/types/base_object.rb | 12 ++++++++ .../lib/decidim/api/project_type.rb | 4 +-- .../spec/types/integration_schema_spec.rb | 28 +++++++++---------- .../decidim/comments/vote_comment_resolver.rb | 25 ----------------- .../lib/decidim/api/comment_mutation_type.rb | 16 ++++++++--- 5 files changed, 40 insertions(+), 45 deletions(-) delete mode 100644 decidim-comments/app/resolvers/decidim/comments/vote_comment_resolver.rb diff --git a/decidim-api/lib/decidim/api/types/base_object.rb b/decidim-api/lib/decidim/api/types/base_object.rb index 96214353c8fd3..4fc315f8265af 100644 --- a/decidim-api/lib/decidim/api/types/base_object.rb +++ b/decidim-api/lib/decidim/api/types/base_object.rb @@ -10,6 +10,18 @@ class BaseObject < GraphQL::Schema::Object field_class Types::BaseField required_scopes "api:read" + + def current_user + context[:current_user] + end + + def current_component + context[:current_component] + end + + def current_organization + context[:current_organization] + end end end end diff --git a/decidim-budgets/lib/decidim/api/project_type.rb b/decidim-budgets/lib/decidim/api/project_type.rb index d293ed9371458..f2df243583d8e 100644 --- a/decidim-budgets/lib/decidim/api/project_type.rb +++ b/decidim-budgets/lib/decidim/api/project_type.rb @@ -49,8 +49,8 @@ def self.authorized?(object, context) context[:project] = object chain = [ - allowed_to?(:read, :project, object, context), - object.visible? + object.visible?, + allowed_to?(:read, :project, object, context) ].all? super && chain diff --git a/decidim-budgets/spec/types/integration_schema_spec.rb b/decidim-budgets/spec/types/integration_schema_spec.rb index 7d952c17ce440..1399a58d16993 100644 --- a/decidim-budgets/spec/types/integration_schema_spec.rb +++ b/decidim-budgets/spec/types/integration_schema_spec.rb @@ -469,25 +469,25 @@ it "is visible" do expect(response["assembly"]["components"].first[lookout_key]).to eq(query_result.except("projects")) end + end - context "and requests projects that is not supposed to see" do - let!(:current_user) { nil } + context "when user is visitor and requests projects that is not supposed to see" do + let!(:current_user) { nil } - let(:component_fragment) do - %( - fragment fooComponent on Budgets { - budget(id: #{budget.id}) { + let(:component_fragment) do + %( + fragment fooComponent on Budgets { + budget(id: #{budget.id}) { + id + projects { id - projects { - id - } } - }) - end + } + }) + end - it "throws Decidim::Api::Errors::UnauthorizedObjectError" do - expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedObjectError, "You cannot view or edit this Project because you do not have permissions") - end + it "throws Decidim::Api::Errors::UnauthorizedObjectError" do + expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedObjectError, "You cannot view or edit this Project because you do not have permissions") end end diff --git a/decidim-comments/app/resolvers/decidim/comments/vote_comment_resolver.rb b/decidim-comments/app/resolvers/decidim/comments/vote_comment_resolver.rb deleted file mode 100644 index 2c9384e6123d4..0000000000000 --- a/decidim-comments/app/resolvers/decidim/comments/vote_comment_resolver.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -module Decidim - module Comments - # A GraphQL resolver to handle `upVote` and `downVote` mutations - # It creates a vote for a comment by the current user. - class VoteCommentResolver - def initialize(options = { weight: 1 }) - @weight = options[:weight] - end - - def call(obj, _args, ctx) - Decidim::Comments::VoteComment.call(obj, ctx[:current_user], weight: @weight) do - on(:ok) do |comment| - return comment - end - - on(:invalid) do - raise GraphQL::ExecutionError, I18n.t("votes.create.error", scope: "decidim.comments") - end - end - end - end - end -end diff --git a/decidim-comments/lib/decidim/api/comment_mutation_type.rb b/decidim-comments/lib/decidim/api/comment_mutation_type.rb index eb18b16042da3..7a5d590665607 100644 --- a/decidim-comments/lib/decidim/api/comment_mutation_type.rb +++ b/decidim-comments/lib/decidim/api/comment_mutation_type.rb @@ -10,12 +10,20 @@ class CommentMutationType < Decidim::Api::Types::BaseObject field :id, GraphQL::Types::ID, "The Comment's unique ID", null: false field :up_vote, Decidim::Comments::CommentType, "The comment that is upvoted", null: true - def up_vote(args: {}) - VoteCommentResolver.new(weight: 1).call(object, args, context) + def up_vote(_args: {}) + Decidim::Comments::VoteComment.call(object, current_user, weight: 1) do + on(:ok) do |comment| + return comment + end + end end - def down_vote(args: {}) - VoteCommentResolver.new(weight: -1).call(object, args, context) + def down_vote(_args: {}) + Decidim::Comments::VoteComment.call(object, current_user, weight: -1) do + on(:ok) do |comment| + return comment + end + end end end end From bd5ab265fb387ade4c9188bd42c8bdabaf5dc4fe Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Tue, 27 Jan 2026 11:00:35 +0200 Subject: [PATCH 091/116] Set fixed image for language selector on mobile (#15864) * This menu, it should be left aligned and not centered * The height of the search bar and languages is not the same (desktop) * Keep the same margin, 16 pixels (mobile) * Refactor menu * Fix Search box height * Fix specs, run linters * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove important on padding * Refactor selectors * Apply review recommendation * Fix layout * Fix language list width for mobile * Apply review recommendation --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../packs/stylesheets/decidim/_dropdown.scss | 36 ------- .../packs/stylesheets/decidim/_footer.scss | 36 ------- .../packs/stylesheets/decidim/_header.scss | 98 +++---------------- .../decidim/_language_chooser.scss | 79 +++++++++++++++ .../stylesheets/decidim/application.scss | 1 + .../header/_focus_mode_back_button.html.erb | 5 +- .../layouts/decidim/header/_main.html.erb | 13 ++- .../header/_main_language_chooser.html.erb | 16 +-- .../header/_main_links_desktop.html.erb | 3 - .../decidim/header/_main_menu_mobile.html.erb | 2 +- ...u_breadcrumb_main_dropdown_mobile.html.erb | 4 +- .../header/_mobile_language_choose.html.erb | 26 ----- .../spec/system/mobile_header_spec.rb | 2 +- .../decidim/dev/test/rspec_support/helpers.rb | 2 +- 14 files changed, 118 insertions(+), 205 deletions(-) create mode 100644 decidim-core/app/packs/stylesheets/decidim/_language_chooser.scss delete mode 100644 decidim-core/app/views/layouts/decidim/header/_mobile_language_choose.html.erb diff --git a/decidim-core/app/packs/stylesheets/decidim/_dropdown.scss b/decidim-core/app/packs/stylesheets/decidim/_dropdown.scss index 604983164afd0..79f94b80aaa8c 100644 --- a/decidim-core/app/packs/stylesheets/decidim/_dropdown.scss +++ b/decidim-core/app/packs/stylesheets/decidim/_dropdown.scss @@ -66,42 +66,6 @@ @apply md:hidden; } -[data-target="dropdown-menu-language-chooser"] { - & > span { - @apply block font-semibold text-black text-xs; - } - - > svg { - @apply w-5 h-6 flex-none text-xs font-normal text-black fill-current; - } - - & > ul { - @apply w-6 bg-gray; - - > li { - @apply w-6 bg-gray; - } - } -} - -[data-target="dropdown-menu-language-chooser-mobile"] { - & > span { - @apply block text-black font-normal text-sm; - } - - > svg { - @apply w-fit h-6 flex-none text-xs font-normal text-black fill-current px-2; - } - - & > ul { - @apply w-6 bg-gray; - - > li { - @apply w-6 bg-gray; - } - } -} - .dropdown { @apply absolute border-2 border-gray-3 rounded-md min-w-max p-1 drop-shadow-md text-left z-10 after:content-[''] after:absolute after:left-0 after:top-0 after:w-full after:h-full after:bg-white; diff --git a/decidim-core/app/packs/stylesheets/decidim/_footer.scss b/decidim-core/app/packs/stylesheets/decidim/_footer.scss index 6fcf8478c0e4c..2544d21ebfd23 100644 --- a/decidim-core/app/packs/stylesheets/decidim/_footer.scss +++ b/decidim-core/app/packs/stylesheets/decidim/_footer.scss @@ -14,42 +14,6 @@ footer { &__down { @apply border-t border-black flex flex-wrap items-center gap-6 container py-6 text-white; } - - &__language { - @apply absolute top-full left-0 bg-white rounded w-full; - - &-container { - @apply relative; - } - - &-trigger { - @apply flex items-center gap-1 border border-white rounded py-1.5 px-2 cursor-pointer text-md font-semibold; - } - } - - /* overwrite default dropdown styles */ - [data-target*="dropdown"] { - > svg { - @apply w-4 h-4 text-white fill-current last-of-type:block last-of-type:ml-auto; - } - - &[aria-expanded="true"] > svg:first-of-type { - @apply block; - } - - > span { - @apply block text-white; - } - } - - /* overwrite default dropdown styles */ - [id*="dropdown-menu"] { - @apply py-0 mx-0 w-full; - - &[aria-hidden="true"] { - @apply md:hidden; - } - } } .mini-footer { diff --git a/decidim-core/app/packs/stylesheets/decidim/_header.scss b/decidim-core/app/packs/stylesheets/decidim/_header.scss index 56d39d3cb34b7..d5bc13f99fda3 100644 --- a/decidim-core/app/packs/stylesheets/decidim/_header.scss +++ b/decidim-core/app/packs/stylesheets/decidim/_header.scss @@ -69,7 +69,7 @@ header { @apply hidden md:block col-span-2 col-start-5 xl:col-start-4; form { - @apply block relative rounded text-md border border-neutral-200 outline outline-1 outline-transparent rounded bg-background-2 leading-relaxed; + @apply block relative rounded text-md border border-neutral-200 outline outline-1 outline-transparent rounded bg-background-2 leading-none; } input[type="text"] { @@ -98,16 +98,16 @@ header { } &__menu-mobile { - @apply sm:hidden flex flex-row-reverse items-center gap-x-3; + @apply lg:hidden flex flex-row-reverse items-center gap-x-3; } &__links-desktop, > *:last-child:not(.main-bar__back-button) { - @apply col-span-1 md:col-start-8 lg:col-start-8 lg:col-span-5 justify-self-end; + @apply col-span-1 lg:col-start-8 lg:col-start-8 lg:col-span-5 justify-self-end; } &__links-desktop { - @apply hidden sm:flex items-center justify-between text-center divide-x-2 divide-gray-3 ml-auto [&>*]:px-4 xl:[&>*]:px-6 first:[&>*]:pl-0 last:[&>*]:pr-0; + @apply hidden lg:flex items-center justify-between text-center divide-x-2 divide-gray-3 ml-auto [&>*]:px-4 xl:[&>*]:px-6 first:[&>*]:pl-0 last:[&>*]:pr-0; &__item { @apply flex items-center gap-x-2 text-secondary px-2 py-1 rounded; @@ -194,7 +194,7 @@ header { } svg + span { - @apply text-md leading-[22px] first-letter:uppercase font-semibold; + @apply text-md leading-[22px] first-letter:uppercase font-semibold whitespace-nowrap; } } @@ -319,7 +319,7 @@ header { /* overwrite default dropdown styles */ [id*="dropdown-menu"] { - @apply py-0 mx-0 w-full sm:w-fit; + @apply py-0 mx-0 w-full; &[aria-hidden="true"] { @apply sm:hidden; @@ -465,82 +465,6 @@ header { } } - &__language-chooser { - @apply absolute top-full left-0 rounded w-full bg-gray-5; - - &-desktop { - @apply border-r border-gray-6 pr-10; - - &.focus-mode { - @apply border-none pr-0; - } - } - - &-desktop, - &-mobile { - & button { - @apply border border-neutral-300 rounded px-4 py-2 gap-x-2; - - & > span, - & > svg:first-of-type { - @apply text-neutral-500; - - display: block !important; - } - - & > span.mobile-holder { - @apply items-center gap-x-1; - - display: flex !important; - } - - & > span { - @apply text-sm font-normal; - } - - & > svg:last-of-type { - @apply text-gray-2 ml-1; - - display: block !important; - } - } - - #dropdown-menu-language-chooser { - @apply absolute top-full bg-white shadow-[1px_4px_8px_3px_rgba(0,0,0,0.15)] p-2; - - ul { - @apply flex flex-col space-y-2; - - li:hover { - @apply rounded; - } - - .is-active { - @apply bg-neutral-200 rounded; - - &:hover { - @apply bg-[var(--secondary)]; - } - } - } - } - - #dropdown-menu-language-chooser-mobile { - @apply overflow-scroll h-[50vh]; - - ul { - .is-active { - @apply bg-neutral-200 rounded; - - &:hover { - @apply bg-[var(--secondary)]; - } - } - } - } - } - } - &__dropdown-menu { @apply w-full sm:w-1/4 px-4 sm:px-0 pt-0 pb-3 sm:py-3 divide-y divide-gray-3 text-[var(--secondary)]; @@ -570,7 +494,7 @@ header { } &__main-dropdown { - @apply bg-white flex flex-row rounded-b shadow-lg text-black w-full sm:w-[530px] h-screen md:h-auto; + @apply bg-white flex flex-row rounded-b shadow-lg text-black w-full lg:w-[530px] h-screen lg:h-auto; &__left { @apply p-4 sm:py-8 sm:px-0 space-y-5 hidden sm:block sm:w-3/4; @@ -584,14 +508,14 @@ header { @apply w-full px-4; &-menu { - @apply w-full md:w-[100%] mt-0 grid md:grid-cols-2 gap-x-6 text-secondary; + @apply w-full lg:w-[100%] mt-0 grid lg:grid-cols-2 gap-x-6 text-secondary; > * { - @apply py-3 md:py-3.5 border-b last:border-0 border-gray-3; + @apply py-3 lg:py-3.5 border-b last:border-0 border-gray-3; /* since the grid has 2 columns, remove the border for these last 2 columns */ &:nth-last-child(-n + 2) { - @apply md:border-0; + @apply lg:border-0; } } @@ -610,7 +534,7 @@ header { } &__bottom { - @apply hidden md:flex; + @apply hidden lg:flex; &-right { @apply mr-2 mb-2; diff --git a/decidim-core/app/packs/stylesheets/decidim/_language_chooser.scss b/decidim-core/app/packs/stylesheets/decidim/_language_chooser.scss new file mode 100644 index 0000000000000..2c1290d644870 --- /dev/null +++ b/decidim-core/app/packs/stylesheets/decidim/_language_chooser.scss @@ -0,0 +1,79 @@ +header { + .main-bar { + &__language-chooser { + &-desktop { + @apply hidden md:block; + } + + > div[id*="dropdown-menu"] { + @apply bg-white shadow-[1px_4px_8px_3px_rgba(0,0,0,0.15)] overflow-auto max-h-[50vh] p-2 sm:w-fit; + } + + &.focus-mode { + @apply border-none pr-0; + } + + &-trigger { + @apply inline-flex border border-neutral-300 rounded px-4 py-[9px] gap-x-2 flex-row; + + &[id*="dropdown-menu"] { + @apply py-[9px]; + } + + & > span, + & > svg:first-of-type { + @apply text-neutral-500; + } + + & > svg:last-of-type, + & > svg:first-of-type { + @apply block; + } + + & > svg:last-of-type { + @apply text-gray-2 ml-1; + } + + svg + span { + @apply text-sm font-normal; + } + } + + &-holder { + @apply items-center gap-x-1 flex; + } + + &-list { + @apply flex flex-col space-y-2; + } + + &-item { + @apply text-black text-left text-md hover:bg-secondary hover:text-white; + + &:hover { + @apply rounded; + } + + &-active { + @apply bg-neutral-200 rounded; + + &:hover { + @apply bg-[var(--secondary)]; + } + } + } + + & > div { + @apply absolute; + } + } + } + + .menu-bar__language-chooser-mobile { + .main-bar__language-chooser { + & > div[id*="dropdown-menu"] { + @apply w-full relative; + } + } + } +} diff --git a/decidim-core/app/packs/stylesheets/decidim/application.scss b/decidim-core/app/packs/stylesheets/decidim/application.scss index 7a85caabde943..bb71da580380e 100644 --- a/decidim-core/app/packs/stylesheets/decidim/application.scss +++ b/decidim-core/app/packs/stylesheets/decidim/application.scss @@ -9,6 +9,7 @@ // On the other hand, the following styles match with specific routes @use "stylesheets/decidim/header"; @use "stylesheets/decidim/footer"; +@use "stylesheets/decidim/language_chooser"; @use "stylesheets/decidim/login"; @use "stylesheets/decidim/pages"; @use "stylesheets/decidim/notifications"; diff --git a/decidim-core/app/views/layouts/decidim/header/_focus_mode_back_button.html.erb b/decidim-core/app/views/layouts/decidim/header/_focus_mode_back_button.html.erb index 9d027b5f2d97f..a0e8cf78ca94b 100644 --- a/decidim-core/app/views/layouts/decidim/header/_focus_mode_back_button.html.erb +++ b/decidim-core/app/views/layouts/decidim/header/_focus_mode_back_button.html.erb @@ -9,15 +9,14 @@ <% end %> -