diff --git a/.github/actions/spelling/excludes.txt b/.github/actions/spelling/excludes.txt index 28c5f8f988d94..9801f099f9e43 100644 --- a/.github/actions/spelling/excludes.txt +++ b/.github/actions/spelling/excludes.txt @@ -101,6 +101,7 @@ ^\Qdecidim-core/app/views/layouts/decidim/_edit_link.html.erb\E$ ^\Qdecidim-core/lib/decidim/db/common-passwords.txt\E$ ^\Qdecidim-core/spec/mailers/decidim_devise_mailer_spec.rb\E$ +^\Qdecidim-core/spec/db/data/add_short_name_to_organizations_spec.rb\E$ ^\Qdecidim-dev/lib/decidim/dev/assets/assemblies.json\E$ ^\Qdecidim-dev/lib/decidim/dev/assets/base64_content.html\E$ ^\Qdecidim-dev/lib/decidim/dev/assets/empty_file.csv\E$ diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 66d3cf4d9aa30..e1d7d086eb790 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -281,6 +281,8 @@ evanfuture evt Exampledocument exitstatus +Existente +EXISTINGCITY faketoken Fal fcell @@ -547,6 +549,7 @@ msword multichoice multifield multitenant +mycity myengine mypass myprovider @@ -639,7 +642,7 @@ pamplona pandoc paramxx partcipatory -participatoryspaceprivateusers +participatoryspace patrimonigracia Peguera pepito 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 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/.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 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/RELEASE_NOTES.md b/RELEASE_NOTES.md index f116a7bea1227..32d3a4f13b831 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -94,7 +94,7 @@ At the moment we are adding this gem so we can start doing data migrations for f You can read more about this change on PR [#15501](https://github.com/decidim/decidim/pull/15501). -#### 2.4. Fix gitignore for ServiceWorker related files +### 2.4. Fix gitignore for ServiceWorker related files We detected a bug where some dynamic files are not added to the gitignore, so they could be committed to the repository. For fixing it, you need to add them to your gitignore file: @@ -104,7 +104,17 @@ echo "/public/sw.js*" >> .gitignore You can read more about this change on PR [#15601](https://github.com/decidim/decidim/pull/15601). -### 2.5. Add locale to the url +### 2.5. Data migration for organization short_name + +A new data migration has been added to populate the `short_name` field for existing organizations. This field is required for the PWA (Progressive Web App) manifest to properly display the application name on mobile devices' home screens. + +The migration automatically generates a short_name for each organization based on its name by removing spaces and truncating to 12 characters maximum. Organizations with names that result in less than 3 characters after processing will not have a short_name set and will need to be configured manually through the admin panel. + +This migration runs automatically when executing `bin/rails data:migrate` as part of the upgrade process. + +You can read more about this change on PR [#15729](https://github.com/decidim/decidim/pull/15729). + +### 2.6. Add locale to the url For a long time Decidim has been using internally the user browser to detect the language of the user. This has been changed to use the locale of the url instead. @@ -119,7 +129,7 @@ It also enables the users of multi language platforms to share the links to the You can read more about this change on PR [#14432](https://github.com/decidim/decidim/pull/14432). -### 2.6. [[TITLE OF THE ACTION]] +### 2.7. [[TITLE OF THE ACTION]] You can read more about this change on PR [#XXXX](https://github.com/decidim/decidim/pull/XXXX). 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-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-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/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-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-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-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..efc40670a36db --- /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:) + 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 @@
| "> - <%= 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"> - |
<%= 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 @@<%= t(".explanation") %>
<%= t(".example_file") %>
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/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/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-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..c5de1536aea5e --- /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:, 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_invite_spec.rb b/decidim-admin/spec/system/admin_invite_spec.rb index 7f920ad6d8daf..e37b3ead355f9 100644 --- a/decidim-admin/spec/system/admin_invite_spec.rb +++ b/decidim-admin/spec/system/admin_invite_spec.rb @@ -10,6 +10,7 @@ let(:params) do { name: "Gotham City", + short_name: "GothamCity", reference_prefix: "JKR", host: "decide.lvh.me", organization_admin_name: "Fiorello Henry La Guardia", 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/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/config/locales/en.yml b/decidim-api/config/locales/en.yml new file mode 100644 index 0000000000000..f28fd3c1c73a9 --- /dev/null +++ b/decidim-api/config/locales/en.yml @@ -0,0 +1,12 @@ +--- +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/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/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/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 new file mode 100644 index 0000000000000..bca12968a291b --- /dev/null +++ b/decidim-api/lib/decidim/api/errors/not_found_error.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +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_ERROR" } }) + end + end + 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 new file mode 100644 index 0000000000000..9242555a8e89f --- /dev/null +++ b/decidim-api/lib/decidim/api/errors/permission_not_set_error.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +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" => "PERMISSION_NOT_SET_ERROR" } }) + end + end + 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..713f5d4bde55c --- /dev/null +++ b/decidim-api/lib/decidim/api/errors/unauthorized_field_error.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +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" => "UNAUTHORIZED_FIELD_ERROR" } }) + 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..8dff8305a6fc2 --- /dev/null +++ b/decidim-api/lib/decidim/api/errors/unauthorized_object_error.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +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" => "UNAUTHORIZED_OBJECT_ERROR" } }) + end + end + 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/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/query_type.rb b/decidim-api/lib/decidim/api/query_type.rb index cbce0ddee80a3..e037fd3f9eb70 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:, 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-api/lib/decidim/api/schema.rb b/decidim-api/lib/decidim/api/schema.rb index 8a0905419bece..16903e19b452b 100644 --- a/decidim-api/lib/decidim/api/schema.rb +++ b/decidim-api/lib/decidim/api/schema.rb @@ -12,6 +12,32 @@ class Schema < GraphQL::Schema max_complexity Decidim::Api.schema_max_complexity 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 + + 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/component_context.rb b/decidim-api/lib/decidim/api/test/component_context.rb index dd1ce779078b6..5a9c4f3362947 100644 --- a/decidim-api/lib/decidim/api/test/component_context.rb +++ b/decidim-api/lib/decidim/api/test/component_context.rb @@ -42,25 +42,33 @@ end end -shared_examples "with resource visibility" do - let(:process_space_factory) { :participatory_process } +shared_examples "graphQL not found space" do 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) - end + it "should not be visible" do + expect { response }.to raise_error(Decidim::Api::Errors::NotFoundError, "#{space_type.classify} not found") end +end - shared_examples "graphQL hidden space" do - it "should not be visible" do - expect(response[space_type]).to be_nil +shared_examples "with resource visibility" do + let(:process_space_factory) { :participatory_process } + let(:space_type) { "participatoryProcess" } + + 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 @@ -75,7 +83,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 @@ -118,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 @@ -142,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 @@ -160,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 @@ -176,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 @@ -240,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 @@ -258,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 @@ -276,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 @@ -291,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 @@ -308,38 +316,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 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 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 @@ -352,36 +360,36 @@ 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) } + 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 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,38 +405,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" + let!(:member) { create(:member, user: current_user, privatable_to: participatory_process) } + 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 +448,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" + let!(:member) { create(:member, user: current_user, privatable_to: participatory_process) } + 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..0e45659141250 100644 --- a/decidim-api/lib/decidim/api/test/type_context.rb +++ b/decidim-api/lib/decidim/api/test/type_context.rb @@ -28,6 +28,26 @@ execute_query query, variables.stringify_keys end + def raise_proper_error(error) + code = error.dig("extensions", "code") + + # 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) result = schema.execute( query, @@ -41,7 +61,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..0899324b07f38 100644 --- a/decidim-api/lib/decidim/api/types.rb +++ b/decidim-api/lib/decidim/api/types.rb @@ -9,6 +9,18 @@ module Api autoload :GraphqlPermissions, "decidim/api/graphql_permissions" autoload :ComponentMutationType, "decidim/api/component_mutation_type" + module Errors + 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 autoload :BaseArgument, "decidim/api/types/base_argument" autoload :BaseEnum, "decidim/api/types/base_enum" diff --git a/decidim-api/lib/decidim/api/types/base_mutation.rb b/decidim-api/lib/decidim/api/types/base_mutation.rb index 2a0d2a44588b3..a6970dc53dc04 100644 --- a/decidim-api/lib/decidim/api/types/base_mutation.rb +++ b/decidim-api/lib/decidim/api/types/base_mutation.rb @@ -5,12 +5,40 @@ module Api module Types class BaseMutation < GraphQL::Schema::RelayClassicMutation include Decidim::Api::GraphqlPermissions + include Decidim::FormFactory object_class BaseObject field_class Types::BaseField input_object_class BaseInputObject 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 + + def current_component + context[:current_component] + end + + 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-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/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-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-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/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..208cc845bae6b 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 @@ -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/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
<%= t(".description") %>