diff --git a/.ai/app-directories.md b/.ai/app-directories.md new file mode 100644 index 0000000000000..c7cab2055a6cd --- /dev/null +++ b/.ai/app-directories.md @@ -0,0 +1,47 @@ +# App directories + +As decidim is a gem for Ruby on Rails you may find the usual rails directories: controllers, models, etc. but we also have other kind of directories. + +## Standard Rails + +| Directory | Description | Technology | +|--------------|----------------------------------|------------------------| +| controllers/ | HTTP request handlers | Rails ActionController | +| models/ | ActiveRecord models and entities | Rails ActiveRecord | +| views/ | ERB templates for rendering | Rails ERB | +| helpers/ | View helper methods | Rails ActionView | +| mailers/ | Email sending classes | Rails ActionMailer | +| jobs/ | Background job classes | Rails ActiveJob | + +## Beyond Standard Rails + +| Directory | Description | Technology | +|--------------|-------------------------------------------------------------|-------------------------------------------------------------------| +| commands/ | Business logic encapsulating use cases (Command Pattern) | Custom Decidim base class (inspired by Rectify gem). | +| forms/ | Form objects for data validation and transformation | Custom Form base class with Decidim::Attributes | +| cells/ | Reusable view components | Trailblazer::Cells gem | +| events/ | Activity logging, notifications, event triggering | Custom Event classes (SimpleEvent, NotificationEvent, EmailEvent) | +| permissions/ | Authorization and permission checking logic | Custom Decidim permission system | +| queries/ | Database query objects for complex queries | Custom Query base class | +| presenters/ | Decorator classes for view-specific formatting | SimpleDelegator-based | +| services/ | Stateless utility and business service classes | Plain Ruby classes | +| validators/ | Custom validation classes | Rails Custom Validators | +| serializers/ | JSON/XML serialization for API responses | Custom serializers | +| uploaders/ | File upload handling | Custom validations for ActiveStorage | +| constraints/ | Rails routing constraints | Custom constraint classes | +| resolvers/ | GraphQL data resolution | Custom resolver classes | +| scrubbers/ | HTML content sanitization | Rails::HTML::Scrubber | +| packs/ | JavaScript/CSS entry points and assets | Shakapacker | +| assets/ | Static assets (some modules) | Asset pipeline | + +## Key Architectural Patterns + +**IMPORTANT: You must follow the existing patterns.** + +1. **Command Pattern**: Commands in `app/commands/` encapsulate single use cases and broadcast events +2. **Form Objects**: Forms in `app/forms/` handle validation separately from models +3. **Cells**: Component-based views using `Trailblazer::Cells` for reusable UI +4. **Query Objects**: Complex database queries isolated in `app/queries/` +5. **Events**: Events trigger notifications, logs, and side effects +6. **Permission System**: Scope-based authorization with action/subject model +7. **Content Block System**: Customizable page sections via manifests diff --git a/.ai/build-pipeline-integration.md b/.ai/build-pipeline-integration.md new file mode 100644 index 0000000000000..4e06c35cf7ffb --- /dev/null +++ b/.ai/build-pipeline-integration.md @@ -0,0 +1,113 @@ +# Build Pipeline Integration + +**CI Requirements:** The `.github/workflows/` contain the production CI setup: + +- Tests run on Ubuntu +- Requires PostgreSQL and Redis services +- Uses specific Ruby and Node versions +- Runs parallel tests across multiple modules +- **Each module CI can timeout at 30-60 minutes - NEVER CANCEL** + +## Pre-Commit Checklist + +Before committing, ALWAYS run: + +```bash +npm run lint +bundle exec rubocop +bundle exec erblint --lint-all +bundle exec i18n-tasks normalize --locales en +bundle exec rspec +npm run test +``` + +## Troubleshooting + +### JavaScript Lint Failures + +```bash +# View errors +npm run lint + +# Auto-fix what can be fixed +npm run lint -- --fix +``` + +### Ruby Lint Failures (RuboCop) + +```bash +# View errors with details +bundle exec rubocop + +# Auto-fix safe corrections +bundle exec rubocop -a + +# Auto-fix including unsafe corrections (review changes afterward) +bundle exec rubocop -A +``` + +### ERB Lint Failures + +```bash +# View errors +bundle exec erblint --lint-all + +# Auto-fix +bundle exec erblint --lint-all --autocorrect +``` + +### CSS/SCSS Formatting Issues + +```bash +# Check issues +npm run stylelint +npm run prettier + +# Auto-fix +npm run prettify +``` + +### Translation Key Issues + +```bash +# Normalize and sort keys (only English) +bundle exec i18n-tasks normalize --locales en + +# Find missing keys +bundle exec i18n-tasks missing + +# Find unused keys +bundle exec i18n-tasks unused +``` + +### Test Failures + +**IMPORTANT:** When a test fails, always ask the user whether you should fix the test or fix the code that the test is validating. Do not assume which approach is correct. + +1. **Read the error message carefully** - RSpec provides detailed failure information + +2. **Run the specific failing test** to iterate faster: + +```bash +cd decidim- +bundle exec rspec spec/path/to/failing_spec.rb:LINE_NUMBER +``` + +- You can pass multiple line numbers: file.rb:12:34 +- For failures in shared contexts/examples, always run the concrete example using its file:line from the failure output. +- Alternatively, run by example description: + +```bash +bundle exec rspec spec/path/to/failing_spec.rb -e "example description" +``` + +1. **Check for flaky tests** - If a test passes when run individually but fails in the suite, it may be a test isolation issue + +2. **Reset the test database** if you suspect data issues: + +```bash +cd spec/decidim_dummy_app +bin/rails db:reset RAILS_ENV=test +``` + +1. **Check for missing dependencies** - Run `bundle install` and `npm install` if tests fail with load errors diff --git a/.ai/development-workflow.md b/.ai/development-workflow.md new file mode 100644 index 0000000000000..7f5831174c65a --- /dev/null +++ b/.ai/development-workflow.md @@ -0,0 +1,206 @@ +# Development Workflow + +## Creating a Development App + +As decidim is a gem, we need to create a rails application to test it. That's why we have the `development_app`. When you generate it, you create a rails application with the decidim gem using the local files. You only need to generate it once. So if the directory already exists you don't need to generate it again unless a reset is required. + +You should not change anything inside this development app as it's a local directory that won't be persisted. + +```bash +# Install dependencies first (if not already done) +bundle install +npm install + +# Create development app for active development +bundle exec rake development_app +cd development_app +bin/dev # Starts Rails server with webpack dev server +``` + +## Key Development Files and Locations + +**Gem Structure:** Each `decidim-*` directory is a separate gem: + +- `decidim-core/` - Main framework and shared components +- `decidim-admin/` - Administrative interface +- `decidim-proposals/` - Proposal management component +- `decidim-participatory_processes/` - Process management +- `decidim-assemblies/` - Assembly management +- `decidim-meetings/` - Meeting management +- `decidim-surveys/` - Survey component +- And many more... + +**Important Files:** + +- `Rakefile` - Main build tasks and gem management +- `Gemfile` - Root dependency specification +- `package.json` - JavaScript dependencies and scripts +- `.github/workflows/` - CI/CD pipeline definitions +- `docs/` - Comprehensive documentation in AsciiDoc format + +**JavaScript Assets:** Located in each gem's `app/packs/` directory +**Stylesheets:** Located in each gem's `app/packs/stylesheets/` directory + +## Common Development Tasks + +Running the development server: + +```bash +cd development_app +bin/dev # Starts Rails + webpack dev server +# Access at http://localhost:3000 +# Admin panel: http://localhost:3000/admin (after creating admin user) +``` + +Database operations: + +```bash +cd development_app +bin/rails db:drop # Drop database +bin/rails db:create # Create database +bin/rails db:migrate # Run migrations +bin/rails db:seed # Load sample data +bin/rails db:reset # Reset and reseed database +``` + +Asset compilation: + +```bash +cd development_app +bin/rails assets:precompile +``` + +## Database Migrations + +When creating new features that require database changes, migrations belong in the appropriate `decidim-*` module, not in the development app. + +### Creating a Migration + +```bash +cd decidim- +bin/rails generate migration AddFieldToTableName field_name:type +``` + +### Migration File Location + +Migrations are stored in each gem's `db/migrate/` directory: + +```text +decidim-/ +└── db/ + └── migrate/ + └── YYYYMMDDHHMMSS_migration_name.rb +``` + +### Applying Migrations + +After creating a migration, regenerate the development or test app to apply it: + +```bash +# For development +bundle exec rake development_app + +# For testing +bundle exec rake test_app +``` + +Or apply migrations directly in an existing app: + +```bash +cd development_app # or spec/decidim_dummy_app +bin/rails decidim:upgrade +bin/rails db:migrate +``` + +### Migration Best Practices + +- Use reversible migrations when possible +- Add indexes for foreign keys and frequently queried columns +- Use `change_column_null` with a default value for non-nullable columns +- Test migrations in both directions: `bin/rails db:migrate` and `bin/rails db:rollback` + +## Data Migrations (data-migrate) + +Decidim uses the `data-migrate` gem for data changes that should not live in schema migrations (e.g. backfilling data, transforming existing records, one-off fixes). + +Use data migrations when: + +- Modifying existing data +- Backfilling new columns +- Migrating values between columns or tables +- Fixing production data inconsistencies + +Do **not** use schema migrations for these cases. + +### Creating a Data Migration + +From the appropriate decidim-* module: + +```bash +cd decidim- bin/rails generate data_migration BackfillSomething +``` + +This creates a file under: + +```text +decidim-/ +└── db/ + └── data/ + └── YYYYMMDDHHMMSS_backfill_something.rb +``` + +### Running Data Migrations + +In a development or test app: + +```bash +bin/rails data:migrate +``` + +To check status: + +```bash +bin/rails data:migrate:status +``` + +### Data Migration Best Practices + +- Never reference application models directly. +- Define a minimal ActiveRecord::Base class inside the migration. +- Always pin the table name to avoid breakage if models change: + +```ruby +class LegacyProposal < ActiveRecord::Base + self.table_name = "decidim_proposals_proposals" +end +``` + +- Avoid callbacks, validations, and scopes. +- Make migrations idempotent (safe to re-run). +- Prefer find_each for large datasets. +- Keep data migrations small and focused. + +### Example Pattern + +```ruby +class BackfillPublishedAt < ActiveRecord::Migration[6.1] + class Proposal < ActiveRecord::Base + self.table_name = "decidim_proposals_proposals" + end + + def up + Proposal.where(published_at: nil).find_each do |proposal| + proposal.update_column(:published_at, proposal.created_at) + end + end + + def down + # no-op (data migrations are usually irreversible) + end +end +``` + +### When in Doubt + +- Schema change? → regular migration +- Data change? → data-migrate diff --git a/.ai/documentation.md b/.ai/documentation.md new file mode 100644 index 0000000000000..6125e15926ac1 --- /dev/null +++ b/.ai/documentation.md @@ -0,0 +1,109 @@ +# Documentation + +We have comprehensive documentation in `docs/` using AsciiDoc format. When making changes, check if related documentation needs updating. + +## When to Read Which Docs + +### `docs/modules/install/` - Installation Guide + +**Read when:** Setting up Decidim, troubleshooting installation, updating versions, deploying to production. + +| File | Content | +|-----------------------|-------------------------------------------------------| +| `index.adoc` | Overview, creating apps, scheduled tasks, seed data | +| `manual.adoc` | Step-by-step installation (Ruby, PostgreSQL, Node.js) | +| `checklist.adoc` | Production deployment checklist | +| `update.adoc` | Updating Decidim versions, compatibility matrix | +| `empty-database.adoc` | Setup without seed data | + +### `docs/modules/develop/` - Developer Guide + +**Read when:** Developing features, understanding architecture, writing tests, contributing to core. + +| File | Content | +|------------------------------|----------------------------------------------------| +| `guide.adoc` | Entry point for development | +| `guide_architecture.adoc` | C4 diagrams, system architecture | +| `guide_conventions.adoc` | GitFlow, branch naming, commit messages | +| `modules.adoc` | Creating external modules | +| `components.adoc` | Creating components (manifests, settings, exports) | +| `testing.adoc` | RSpec, Jest, parallel testing | +| `permissions.adoc` | Permission system, adding actions | +| `notifications.adoc` | Events, email/notification generation | +| `content_blocks.adoc` | Registering content blocks | +| `api.adoc` | GraphQL API | +| `view_models_aka_cells.adoc` | Cells pattern | + +**`docs/modules/develop/pages/classes/`** - Class patterns: + +| File | Pattern | +|--------------------|-------------------------------------------------| +| `commands.adoc` | Command pattern (Create/Update/DestroyResource) | +| `forms.adoc` | Form objects (Decidim::Form) | +| `cells.adoc` | View components (Decidim::ViewModel) | +| `events.adoc` | Event classes for notifications | +| `permissions.adoc` | Permission classes | +| `queries.adoc` | Query objects | +| `presenters.adoc` | ResourcePresenter, AdminLogPresenter | +| `jobs.adoc` | ActiveJob background jobs | +| `mailers.adoc` | Mailers with locale handling | +| `controllers.adoc` | Controller patterns | +| `models.adoc` | ActiveRecord models and concerns | + +### `docs/modules/configure/` - Configuration Guide + +**Read when:** Configuring Decidim options, environment variables, system panel. + +| File | Content | +|------------------------------|------------------------------------------| +| `index.adoc` | Configuration overview, CLI flags | +| `initializer.adoc` | All Decidim.configure options | +| `system.adoc` | System panel, multi-tenant organizations | +| `environment_variables.adoc` | Environment variable reference | + +### `docs/modules/services/` - External Services + +**Read when:** Integrating maps, email, storage, or other external services. + +| File | Content | +|--------------------------|---------------------------------------| +| `activejob.adoc` | Background jobs (Sidekiq, DelayedJob) | +| `activestorage.adoc` | File storage (S3, GCS, Azure) | +| `maps.adoc` | Maps/geocoding (HERE Maps, OSM) | +| `smtp.adoc` | Email server configuration | +| `sms.adoc` | SMS gateway for verification | +| `social_providers.adoc` | OAuth providers | +| `etherpad.adoc` | Real-time collaborative editing | +| `aitools.adoc` | AI tools integration | + +### `docs/modules/customize/` - Customization Guide + +**Read when:** Customizing appearance, overriding behavior, extending functionality. + +| File | Content | +|----------------------|---------------------------------------------------| +| `code.adoc` | Monkey patching, decorators, modules | +| `views.adoc` | Overriding views (filename method, Deface, cells) | +| `styles.adoc` | CSS (Tailwind, SCSS, organization colors) | +| `javascript.adoc` | Custom JavaScript | +| `authorizations.adoc`| Custom verification handlers | +| `menu.adoc` | Navigation menu customization | +| `localization.adoc` | Translations | + +## Quick Reference by Task + +| Task | Read | +|------------------------------|----------------------------------------------------------------------| +| Creating a new command | `develop/pages/classes/commands.adoc` | +| Creating a new form | `develop/pages/classes/forms.adoc` | +| Adding a new cell | `develop/pages/classes/cells.adoc` | +| Adding notifications/events | `develop/notifications.adoc`, `develop/pages/classes/events.adoc` | +| Adding permissions | `develop/permissions.adoc`, `develop/pages/classes/permissions.adoc` | +| Creating a module | `develop/modules.adoc` | +| Creating a component | `develop/components.adoc` | +| Overriding views | `customize/views.adoc` | +| Customizing styles | `customize/styles.adoc` | +| Running tests | `develop/testing.adoc` | +| Configuring maps | `services/maps.adoc` | +| Configuring storage | `services/activestorage.adoc` | +| Production deployment | `install/checklist.adoc` | diff --git a/.ai/important-notes.md b/.ai/important-notes.md new file mode 100644 index 0000000000000..9fb1c4ce3e9f7 --- /dev/null +++ b/.ai/important-notes.md @@ -0,0 +1,9 @@ +# Important Notes + +- **NEVER CANCEL** builds or tests that take more than 2 minutes - builds can take 3+ minutes, full test suites for a module 60+ minutes +- Always use the exact Ruby and Node versions specified in `.ruby-version` and `.node-version`. +- The development app (`rake development_app`) is the primary way to create a working Decidim application for development +- Each `decidim-*` directory is an independent gem with its own tests and dependencies +- Always run full validation scenarios after making changes to ensure functionality works end-to-end +- **Changes to decidim-generators**: When making changes to `decidim-generators` that affect application configuration (files like `config/application.rb`, `config/environments/*`, etc) or other generated files, also document these changes in `RELEASE_NOTES.md` +- Read more about testing (like how to run parallel tests) at @docs/modules/develop/pages/testing.adoc diff --git a/.ai/introduction.md b/.ai/introduction.md new file mode 100644 index 0000000000000..0557e7a648cdd --- /dev/null +++ b/.ai/introduction.md @@ -0,0 +1,20 @@ +# Introduction + +[Decidim](https://decidim.org) is a free/libre, open-source digital platform for citizen participation and democratic governance. It enables citizens, organizations, and public institutions to self-organize democratically at any scale. + +## Key Features + +- Strategic planning +- Participatory processes +- Assemblies +- Initiatives and citizen consultations +- Participatory budgeting +- Networked communication + +## Who Uses Decidim + +Hundreds of organizations globally use Decidim, including city governments (Barcelona, NYC, Helsinki), the European Commission, the French Senate and National Assembly, Mexico City, the Brazilian federal government, and universities. + +## Technical Overview + +Decidim is a Ruby on Rails application with JavaScript frontend components. The codebase consists of multiple gem modules (`decidim-core`, `decidim-admin`, `decidim-proposals`, etc.), each handling specific functionality. The platform prioritizes transparency, traceability, security, and privacy. diff --git a/.ai/linting.md b/.ai/linting.md new file mode 100644 index 0000000000000..3ef6e245954e4 --- /dev/null +++ b/.ai/linting.md @@ -0,0 +1,39 @@ +# Linting + +- JavaScript linting: + +```bash +npm run lint +# Expected time: ~6 seconds +# May show warnings about React version and import paths - this is normal +``` + +- Ruby linting: + +```bash +bundle exec rubocop +# Expected time: ~1.5 seconds +# Use --parallel for faster execution +# Use -a flag for auto-correction +``` + +- ERB linting: + +```bash +bundle exec erblint --lint-all +# use --autocorrect flag for auto-correction +``` + +- CSS/SCSS linting: + +```bash +npm run stylelint +npm run prettier +# Use npm run prettify to fix formatting +``` + +- Normalize and sort translation keys: + +```bash +bundle exec i18n-tasks normalize --locales en +``` diff --git a/.ai/locales.md b/.ai/locales.md new file mode 100644 index 0000000000000..17e7b0b1f2481 --- /dev/null +++ b/.ai/locales.md @@ -0,0 +1,3 @@ +# Locales + +The locales are managed by Crowdin. When you are developing you must only take care of the `en.yml` files, do not change the other locales. diff --git a/.ai/requirements.md b/.ai/requirements.md new file mode 100644 index 0000000000000..ece4c484c3b80 --- /dev/null +++ b/.ai/requirements.md @@ -0,0 +1,11 @@ +# Requirements + +In order to run this project locally we need: + +- Ruby (you can check the version in the `.ruby-version` file) +- Node.js (you can check in `.node-version` file) +- PostgreSQL (any recent version) +- Redis server (required for background jobs) +- Google Chrome (required for system tests using Capybara) + +If any of these requirements is not installed, refer to the official installation guide: diff --git a/.ai/testing.md b/.ai/testing.md new file mode 100644 index 0000000000000..7d052a552779c --- /dev/null +++ b/.ai/testing.md @@ -0,0 +1,80 @@ +# Testing + +We use RSpec as technology for testing. + +## Creating the Test App + +As decidim is a gem, we need to create a rails application to run the tests. That's why we have the `spec/decidim_dummy_app`. When you generate it, you create a rails application with the decidim gem using the local files. You only need to generate it once. So if the directory already exists you don't need to generate it again unless a reset is required. + +You should not change anything inside this test app as it's a local directory that won't be persisted. + +```bash +# Install dependencies first (if not already done) +bundle install +npm install + +# Create test app (generates spec/decidim_dummy_app) +bundle exec rake test_app +``` + +## Running tests + +**NEVER CANCEL TESTS** - They may take several minutes to complete. + +- Main test suite (1 minute 49 seconds - NEVER CANCEL): + +```bash +bundle exec rspec +# Expected time: ~109 seconds - NEVER CANCEL +# Set timeout to 300+ seconds +``` + +- JavaScript tests (13 seconds): + +```bash +npm run test +# Expected time: ~13 seconds +# Some vendor test failures are expected (shakapacker dependencies) +``` + +- Individual module tests: + +```bash +bundle exec rake test_core # Test decidim-core +bundle exec rake test_admin # Test decidim-admin +bundle exec rake test_proposals # Test decidim-proposals +# Each module test can take 10-30 minutes - NEVER CANCEL +``` + +- Individual test: + +```bash +# You need to access the module where the test belong +# and run the rspec from there. For example: +cd decidim-core +bundle exec rspec spec/system/account_spec.rb +``` + +## Creating or updating tests + +When you create or update any of the components described in `.ai/app-directories.md` you should create or update unit tests for them. + +We also have `system` specs for integration specs. We shouldn't test all the scenarios there. Only the most relevant ones. + +You can make use of the `shared_examples`, `shared_context`, etc. directives to avoid repeating code in your test files. + +## System Tests Requirements + +Some specs (especially `spec/system`) use Capybara with a real browser. + +Requirements for running system tests locally: + +- Google Chrome must be installed and available in `PATH` +- Google ChromeDriver should also be available +- ChromeDriver should have the same version number as Google Chrome +- The test app must be generated (`spec/decidim_dummy_app`) +- Tests are executed against the dummy app; do not modify it manually + +If Chrome is missing, system specs will fail with driver or browser-related errors. + +Chrome is required both locally and in CI for running system tests. diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index bde812b3c811a..c6203667f3dd5 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,8 +1,11 @@ -FROM decidim/decidim:0.22.0-dev +ARG RUBY_VERSION=3.4.7 +FROM ghcr.io/rails/devcontainer/images/ruby:$RUBY_VERSION -RUN apt-get update && apt-get install -y \ - vim +RUN wget -q -O /tmp/google-chrome-key.pub https://dl-ssl.google.com/linux/linux_signing_key.pub \ + && sudo gpg --dearmor -o /usr/share/keyrings/google-chrome-keyring.gpg /tmp/google-chrome-key.pub \ + && echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome-keyring.gpg] http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee /etc/apt/sources.list.d/google-chrome.list \ + && sudo apt-get update \ + && sudo apt-get install -y google-chrome-stable libicu-dev \ + && sudo rm -rf /var/lib/apt/lists/* /tmp/google-chrome-key.pub -ENV EDITOR=vim - -RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v1.1.1/zsh-in-docker.sh)" +ENV BINDING="0.0.0.0" diff --git a/.devcontainer/compose.yaml b/.devcontainer/compose.yaml new file mode 100644 index 0000000000000..ed22d700847e3 --- /dev/null +++ b/.devcontainer/compose.yaml @@ -0,0 +1,41 @@ +name: "decidim" + +services: + rails-app: + build: + context: .. + dockerfile: .devcontainer/Dockerfile + volumes: + - ../:/workspaces/decidim:cached + - bundle-cache:/usr/local/bundle + - node-modules-cache:/workspaces/decidim/node_modules + ports: + - "${DEVCONTAINER_APP_PORT:-3000}:3000" + command: sleep infinity + depends_on: + - redis + - postgres + + redis: + image: redis:7.2 + restart: unless-stopped + volumes: + - redis-data:/data + + postgres: + image: postgres:16.1 + restart: unless-stopped + networks: + - default + volumes: + - postgres-data:/var/lib/postgresql/data + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + +volumes: + redis-data: + postgres-data: + bundle-cache: + node-modules-cache: + diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 4544c22e09232..bceb49495bd92 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,21 +1,32 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: -// https://github.com/microsoft/vscode-dev-containers/tree/v0.140.1/containers/ruby-rails { - "name": "Ruby on Rails", - "workspaceFolder": "/workspace", - "service": "rails", - "dockerComposeFile": [ - "docker-compose.yml" - ], - "settings": { - "terminal.integrated.shell.linux": "/bin/zsh" - }, - "extensions": [ - "rebornix.Ruby", - "wingrunr21.vscode-ruby", - "misogi.ruby-rubocop" - ], - "forwardPorts": [ - 3000 - ], -} \ No newline at end of file + "name": "decidim", + "dockerComposeFile": "compose.yaml", + "service": "rails-app", + "workspaceFolder": "/workspaces/decidim", + "features": { + "ghcr.io/devcontainers/features/github-cli:1": {}, + "ghcr.io/devcontainers/features/docker-outside-of-docker:1": { + "moby": false + }, + "ghcr.io/devcontainers/features/node:1": { + "version": "22.14.0" + }, + "ghcr.io/rails/devcontainer/features/activestorage": {}, + "ghcr.io/rails/devcontainer/features/postgres-client": {} + }, + "containerEnv": { + "REDIS_URL": "redis://redis:6379/1", + "DECIDIM_SPAM_DETECTION_BACKEND_RESOURCE_URL": "redis://redis:6379/2", + "DECIDIM_SPAM_DETECTION_BACKEND_USER_REDIS_URL": "redis://redis:6379/3", + "DATABASE_HOST": "postgres", + "DATABASE_USERNAME": "postgres", + "DATABASE_PASSWORD": "postgres" + }, + "appPort": ["3000:3000"], + "forwardPorts": [ + 3000, + 5432, + 6379 + ], + "postCreateCommand": "bin/setup --skip-server" +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml deleted file mode 100644 index d2ad07c963e28..0000000000000 --- a/.devcontainer/docker-compose.yml +++ /dev/null @@ -1,27 +0,0 @@ - -version: "3" - -services: - rails: - build: - context: . - dockerfile: Dockerfile - depends_on: - - db - command: /bin/sh -c "while sleep 1000; do :; done" - environment: - DATABASE_HOST: db - DATABASE_USERNAME: postgres - DATABASE_PASSWORD: postgres - db: - image: postgres:9.6 - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - PGDATA: /var/lib/postgresql/data/pgdata - restart: always - volumes: - - db:/var/lib/postgresql/data - -volumes: - db: \ No newline at end of file diff --git a/.github/actions/spelling/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..81873d492bbd3 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -139,6 +139,7 @@ cobertura codecov codefor CODEOWNERS +codespace codeql coditramuntana commentables @@ -175,6 +176,7 @@ cuenta Cugat customise Customising +CVEs cweek cyclomatic danline @@ -222,10 +224,12 @@ doesnot doggotrainer Dokku Dota +dotfiles douban downvoted downvotes downvoting +DPG draggables dragula drinkform @@ -281,6 +285,8 @@ evanfuture evt Exampledocument exitstatus +Existente +EXISTINGCITY faketoken Fal fcell @@ -417,6 +423,7 @@ isbn iseq isready Italiano +itemabcd ivan Jaskolski Jaume @@ -465,6 +472,7 @@ libpq libreadline libreoffice libretranslate +libvips libyaml Liddel lighthouserc @@ -530,6 +538,7 @@ misogi mlat mlon Mobi +moby modals modernizrrc modestbranding @@ -547,6 +556,7 @@ msword multichoice multifield multitenant +mycity myengine mypass myprovider @@ -639,7 +649,7 @@ pamplona pandoc paramxx partcipatory -participatoryspaceprivateusers +participatoryspace patrimonigracia Peguera pepito @@ -654,6 +664,7 @@ PGVERSION Phargo phoe pinterest +pids Placeholdit plantuml plataforma diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 404bc5482f75b..6e330aa87f69a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -72,4 +72,3 @@ updates: - "dependencies" - "type: internal" - "decidim-generators" - diff --git a/.github/workflows/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/ci_generators.yml b/.github/workflows/ci_generators.yml index 47a62c64943e7..18dcb36985c0a 100644 --- a/.github/workflows/ci_generators.yml +++ b/.github/workflows/ci_generators.yml @@ -80,6 +80,10 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 1 + - run: | + sudo apt-get update && sudo apt-get install libvips libvips-tools + name: Install libvips + shell: "bash" - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ env.RUBY_VERSION }} @@ -90,14 +94,14 @@ jobs: - name: Get npm cache directory path id: npm-cache-dir-path run: echo "dir=$(npm get cache)-${{ env.DECIDIM_MODULE }}" >> $GITHUB_OUTPUT - - uses: actions/cache@v4 + - uses: actions/cache@v5 id: npm-cache with: path: ${{ steps.npm-cache-dir-path.outputs.dir }} key: npm-${{ hashFiles('**/package-lock.json') }} restore-keys: | npm- - - uses: actions/cache@v4 + - uses: actions/cache@v5 id: app-cache with: path: ./Gemfile.lock @@ -111,7 +115,7 @@ jobs: working-directory: ${{ env.DECIDIM_MODULE }} env: SIMPLECOV: "true" - - uses: qltysh/qlty-action/coverage@v1 + - uses: qltysh/qlty-action/coverage@v2 with: token: ${{secrets.QLTY_COVERAGE_TOKEN}} verbose: true diff --git a/.github/workflows/ci_javascript.yml b/.github/workflows/ci_javascript.yml index ee6d704ae79d0..d97f85a23e4c9 100644 --- a/.github/workflows/ci_javascript.yml +++ b/.github/workflows/ci_javascript.yml @@ -46,7 +46,7 @@ jobs: - name: Get npm cache directory path id: npm-cache-dir-path run: echo "dir=$(npm get cache)-javascript" >> $GITHUB_OUTPUT - - uses: actions/cache@v4 + - uses: actions/cache@v5 id: npm-cache with: path: ${{ steps.npm-cache-dir-path.outputs.dir }} diff --git a/.github/workflows/ci_main.yml b/.github/workflows/ci_main.yml index a06f3b490fc81..2c9f3299dd88c 100644 --- a/.github/workflows/ci_main.yml +++ b/.github/workflows/ci_main.yml @@ -42,7 +42,7 @@ jobs: - name: Get npm cache directory path id: npm-cache-dir-path run: echo "dir=$(npm get cache)-main" >> $GITHUB_OUTPUT - - uses: actions/cache@v4 + - uses: actions/cache@v5 id: npm-cache with: path: ${{ steps.npm-cache-dir-path.outputs.dir }} diff --git a/.github/workflows/ci_performance_metrics_monitoring.yml b/.github/workflows/ci_performance_metrics_monitoring.yml index 0374d66465636..d452c89bf1b7b 100644 --- a/.github/workflows/ci_performance_metrics_monitoring.yml +++ b/.github/workflows/ci_performance_metrics_monitoring.yml @@ -64,6 +64,10 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 1 + - run: | + sudo apt-get update && sudo apt-get install libvips libvips-tools + name: Install libvips + shell: "bash" - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ env.RUBY_VERSION }} @@ -74,7 +78,7 @@ jobs: - name: Get npm cache directory path id: npm-cache-dir-path run: echo "dir=$(npm get cache)-${{ env.DECIDIM_MODULE }}" >> $GITHUB_OUTPUT - - uses: actions/cache@v4 + - uses: actions/cache@v5 id: npm-cache with: path: ${{ steps.npm-cache-dir-path.outputs.dir }} diff --git a/.github/workflows/ci_production_check.yml b/.github/workflows/ci_production_check.yml index cd151012b8923..edd4d73a3a39d 100644 --- a/.github/workflows/ci_production_check.yml +++ b/.github/workflows/ci_production_check.yml @@ -54,6 +54,10 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 1 + - run: | + sudo apt-get update && sudo apt-get install libvips libvips-tools + name: Install libvips + shell: "bash" - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ env.RUBY_VERSION }} @@ -64,7 +68,7 @@ jobs: - name: Get npm cache directory path id: npm-cache-dir-path run: echo "dir=$(npm get cache)-${{ env.DECIDIM_MODULE }}" >> $GITHUB_OUTPUT - - uses: actions/cache@v4 + - uses: actions/cache@v5 id: npm-cache with: path: ${{ steps.npm-cache-dir-path.outputs.dir }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index febf5cfff946e..1221c447454b5 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,11 +58,11 @@ jobs: name: Tune NPM configuration shell: "bash" - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/lint_code.yml b/.github/workflows/lint_code.yml index 2cf9e5e939cde..1877404000630 100644 --- a/.github/workflows/lint_code.yml +++ b/.github/workflows/lint_code.yml @@ -54,7 +54,7 @@ jobs: - name: Get npm cache directory path id: npm-cache-dir-path run: echo "dir=$(npm get cache)-lint" >> $GITHUB_OUTPUT - - uses: actions/cache@v4 + - uses: actions/cache@v5 id: npm-cache with: path: ${{ steps.npm-cache-dir-path.outputs.dir }} diff --git a/.github/workflows/lint_pr_format.yml b/.github/workflows/lint_pr_format.yml index 499a5ffe5f538..56d7e6c359f80 100644 --- a/.github/workflows/lint_pr_format.yml +++ b/.github/workflows/lint_pr_format.yml @@ -41,7 +41,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/labeler@v5 + - uses: actions/labeler@v6 with: dot: 'true' repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/test_app.yml b/.github/workflows/test_app.yml index 58132b6431b16..3ca1e3be37b10 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 @@ -70,6 +70,10 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 1 + - run: | + sudo apt-get update && sudo apt-get install libvips libvips-tools + name: Install libvips + shell: "bash" - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ inputs.ruby_version }} @@ -126,7 +130,7 @@ jobs: SIMPLECOV: "true" SHAKAPACKER_RUNTIME_COMPILE: "false" NODE_ENV: "test" - - uses: qltysh/qlty-action/coverage@v1 + - uses: qltysh/qlty-action/coverage@v2 with: token: ${{secrets.QLTY_COVERAGE_TOKEN}} files: coverage/coverage.xml diff --git a/.gitignore b/.gitignore index c3bd03e19df05..cc1d4ed7569ed 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ decidim-packs packages/**/package-lock.json temporary_changelog.md +.devcontainer/devcontainer.local.* diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile deleted file mode 100644 index bf356c10a5ec4..0000000000000 --- a/.gitpod.Dockerfile +++ /dev/null @@ -1,46 +0,0 @@ -FROM gitpod/workspace-base:latest -USER root - -# Install PostgreSQL -ENV PGVERSION=14 -RUN apt-get update \ - && apt-get install -y postgresql postgresql-client postgresql-server-dev-${PGVERSION} libpq-dev - -# Setup the database user env vars, drop the default database cluster and change the folders to the workspace -# This makes it possible to persist the database within the workspace -ENV DATABASE_USERNAME=decidim -ENV DATABASE_PASSWORD=development -RUN pg_dropcluster $PGVERSION main \ - && rm -rf /etc/postgresql /var/lib/postgresql \ - && ln -s /workspace/etc/postgresql /etc/postgresql \ - && ln -s /workspace/var/lib/postgresql /var/lib/postgresql \ - && chown -h postgres:postgres /etc/postgresql /var/lib/postgresql - -#### User space #### -USER gitpod - -# Install nvm and Node -ENV NODE_VERSION=22.14.0 -RUN curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | PROFILE=/dev/null bash \ - && bash -c ". .nvm/nvm.sh \ - && nvm install v${NODE_VERSION} \ - && nvm alias default v${NODE_VERSION} \ - && npm install -g npm yarn node-gyp" \ - && echo ". ~/.nvm/nvm.sh" >> ~/.bashrc.d/50-node - -# Install rbenv and Ruby -ENV RUBY_VERSION=3.4.7 -ENV WORKSPACE_GEM_HOME=/workspace/.gem -RUN sudo apt-get install -y build-essential curl git zlib1g-dev libssl-dev \ - libreadline-dev libyaml-dev libxml2-dev libxslt-dev -RUN git clone https://github.com/rbenv/rbenv.git ~/.rbenv \ - && echo 'eval "$(~/.rbenv/bin/rbenv init - sh)"' >> ~/.bashrc.d/60-ruby \ - && eval "$(~/.rbenv/bin/rbenv init - sh)" \ - && git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build \ - && git clone https://github.com/rbenv/rbenv-vars.git "$(rbenv root)"/plugins/rbenv-vars \ - && rbenv install $RUBY_VERSION && rbenv global $RUBY_VERSION \ - && echo "export GEM_PATH=\"${WORKSPACE_GEM_HOME}:$(gem env home)\"" >> ~/.bashrc.d/60-ruby \ - && echo "export GEM_HOME=\"${WORKSPACE_GEM_HOME}\"" >> ~/.bashrc.d/60-ruby \ - && echo 'export RAILS_DEVELOPMENT_HOST=${RAILS_DEVELOPMENT_HOST:-"3000-${GITPOD_WORKSPACE_ID}.${GITPOD_WORKSPACE_CLUSTER_HOST}"}' >> ~/.bashrc.d/70-decidim \ - && echo 'export DECIDIM_FORCE_SSL=${DECIDIM_FORCE_SSL:-true}' >> ~/.bashrc.d/70-decidim \ - && echo 'export DECIDIM_SERVICE_WORKER_ENABLED=${DECIDIM_SERVICE_WORKER_ENABLED:-true}' >> ~/.bashrc.d/70-decidim diff --git a/.gitpod.yml b/.gitpod.yml deleted file mode 100644 index 3c2fb03e8cb46..0000000000000 --- a/.gitpod.yml +++ /dev/null @@ -1,59 +0,0 @@ -image: - file: .gitpod.Dockerfile - -tasks: - - name: Development app - init: | - ( - [ $(sudo pg_lsclusters -h | wc -l) -eq 0 ] && ( - sudo mkdir -p /workspace/etc/postgresql /workspace/var/lib/postgresql && - sudo chown -R postgres:postgres /workspace/etc/postgresql /workspace/var/lib/postgresql && - sudo pg_createcluster $PGVERSION main - ) || echo "Database exists" - ) && - sudo service postgresql start && - ( - [ $(sudo su postgres -c "psql -At -c \"SELECT COUNT(usename) FROM pg_user WHERE usename = '${DATABASE_USERNAME}'\"") -eq 0 ] && ( - sudo su postgres -c "psql -c 'CREATE USER $DATABASE_USERNAME SUPERUSER'" && - sudo su postgres -c "psql -c \"ALTER ROLE $DATABASE_USERNAME WITH PASSWORD '${DATABASE_PASSWORD}'\"" - ) || echo "Database user exists" - ) && - mkdir -p .vscode && - echo '{"workbench.startupEditor": "none"}' > .vscode/settings.json && - bundle install --jobs 4 && - bundle exec rake development_app && - echo 'Rails.application.config.hosts << ENV.fetch("RAILS_DEVELOPMENT_HOST", "")' > development_app/config/initializers/gitpod.rb && - echo 'Rails.application.config.action_mailer.default_url_options = { protocol: "https" }' >> development_app/config/initializers/gitpod.rb && - cd development_app && - ./bin/rails decidim:pwa:generate_vapid_keys | grep VAPID_ >> ../.rbenv-vars && - echo "Compiling assets, please wait a moment..." && - ./bin/shakapacker - command: | - sudo service postgresql start && - { [ $(basename $PWD) == "development_app" ] || cd development_app ; } && - ./bin/rails runner 'Decidim::Organization.first.update!(host: ENV.fetch("RAILS_DEVELOPMENT_HOST", "localhost"))' && - ./bin/rails s -b 0.0.0.0 - -ports: - - name: Web App - description: The main application web server - port: 3000 - onOpen: open-preview - visibility: public - - name: Shakapacker - description: The shakapacker dev server for asset reloading - port: 3035 - onOpen: ignore - visibility: public - - name: Database - description: PostgreSQL database server - port: 5432 - onOpen: ignore - visibility: private - -github: - prebuilds: - addCheck: true - master: false - pullRequests: true - pullRequestsFromForks: true diff --git a/.inch.yml b/.inch.yml deleted file mode 100644 index c1392d007c695..0000000000000 --- a/.inch.yml +++ /dev/null @@ -1,5 +0,0 @@ -files: - included: - - lib/**/*.rb - - decidim-*/{app,lib}/**/*.rb - - decidim-*/{app,lib}/**/*.js diff --git a/.qlty/qlty.toml b/.qlty/qlty.toml index f4c56c8aad8d3..31baa2e234a5b 100644 --- a/.qlty/qlty.toml +++ b/.qlty/qlty.toml @@ -102,9 +102,12 @@ name = "markdownlint" [[plugin]] name = "rubocop" +version = "1.78.0" +mode = "disabled" [[plugin]] name = "osv-scanner" [[plugin]] name = "stylelint" +mode = "disabled" diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000000..4ecee6f1ca310 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,3 @@ +**MANDATORY**: At the start of every session, you MUST read ALL `*.md` files in the `.ai` directory before proceeding with any task. + +**ALWAYS reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.** diff --git a/Gemfile.lock b/Gemfile.lock index fcc840f633463..f45053a0bb009 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -83,7 +83,6 @@ PATH kaminari (~> 1.2, >= 1.2.1) loofah (~> 2.19, >= 2.19.1) mime-types (>= 1.16, < 4.0) - mini_magick (~> 4.9) net-smtp (~> 0.5.0) nokogiri (~> 1.16, >= 1.16.2) omniauth (~> 2.0) @@ -104,6 +103,7 @@ PATH redis (~> 4.1) request_store (~> 1.7.0) rqrcode (~> 2.2.0) + ruby-vips (~> 2.2) rubyXL (~> 3.4) rubyzip (~> 2.0) shakapacker (~> 8.3.0) @@ -492,8 +492,8 @@ GEM ice_cube (~> 0.16) ostruct ice_cube (0.17.0) - image_processing (1.13.0) - mini_magick (>= 4.9.5, < 5) + image_processing (1.14.0) + mini_magick (>= 4.9.5, < 6) ruby-vips (>= 2.0.17, < 3) invisible_captcha (0.13.0) rails (>= 3.2.0) @@ -547,7 +547,8 @@ GEM logger mime-types-data (~> 3.2015) mime-types-data (3.2025.0722) - mini_magick (4.13.2) + mini_magick (5.3.1) + logger mini_mime (1.1.5) minitest (5.25.5) msgpack (1.8.0) @@ -808,7 +809,7 @@ GEM rubocop (~> 1.72) yard ruby-progressbar (1.13.0) - ruby-vips (2.2.4) + ruby-vips (2.2.5) ffi (~> 1.12) logger rubyXL (3.4.33) @@ -914,6 +915,7 @@ GEM PLATFORMS arm64-darwin-23 + arm64-darwin-25 x86_64-linux DEPENDENCIES diff --git a/README.adoc b/README.adoc index 5dee4a70c07ed..d3e03cccf3fcb 100644 --- a/README.adoc +++ b/README.adoc @@ -29,10 +29,10 @@ image:https://img.shields.io/gem/dt/decidim.svg[Gem,link=https://rubygems.org/ge image:https://img.shields.io/github/contributors/decidim/decidim.svg[GitHub contributors,link=https://github.com/decidim/decidim/graphs/contributors] image:https://img.shields.io/matrix/decidimdevs:matrix.org[Matrix,link=https://matrix.to/#/#decidimdevs:matrix.org] image:https://codecov.io/gh/decidim/decidim/branch/develop/graph/badge.svg[codecov,link=https://codecov.io/gh/decidim/decidim] -image:https://api.codeclimate.com/v1/badges/ad8fa445086e491486b6/maintainability[Maintainability,link=https://codeclimate.com/github/decidim/decidim/maintainability] -image:https://d322cqt584bo4o.cloudfront.net/decidim/localized.svg[Crowdin,link=https://crowdin.com/project/decidim] +image:https://qlty.sh/gh/decidim/projects/decidim/maintainability.svg[Maintainability,link=https://qlty.sh/gh/decidim/projects/decidim] image:https://opencollective.com/decidim/tiers/badge.svg[https://opencollective.com/decidim] image:http://img.shields.io/badge/yard-docs-blue.svg[Yard Docs,link=http://rubydoc.info/github/decidim/decidim/develop] +image:https://img.shields.io/badge/Verified-DPG-3333AB?logo=data:image/svg%2bxml;base64,PHN2ZyB3aWR0aD0iMzEiIGhlaWdodD0iMzMiIHZpZXdCb3g9IjAgMCAzMSAzMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0LjIwMDggMjEuMzY3OEwxMC4xNzM2IDE4LjAxMjRMMTEuNTIxOSAxNi40MDAzTDEzLjk5MjggMTguNDU5TDE5LjYyNjkgMTIuMjExMUwyMS4xOTA5IDEzLjYxNkwxNC4yMDA4IDIxLjM2NzhaTTI0LjYyNDEgOS4zNTEyN0wyNC44MDcxIDMuMDcyOTdMMTguODgxIDUuMTg2NjJMMTUuMzMxNCAtMi4zMzA4MmUtMDVMMTEuNzgyMSA1LjE4NjYyTDUuODU2MDEgMy4wNzI5N0w2LjAzOTA2IDkuMzUxMjdMMCAxMS4xMTc3TDMuODQ1MjEgMTYuMDg5NUwwIDIxLjA2MTJMNi4wMzkwNiAyMi44Mjc3TDUuODU2MDEgMjkuMTA2TDExLjc4MjEgMjYuOTkyM0wxNS4zMzE0IDMyLjE3OUwxOC44ODEgMjYuOTkyM0wyNC44MDcxIDI5LjEwNkwyNC42MjQxIDIyLjgyNzdMMzAuNjYzMSAyMS4wNjEyTDI2LjgxNzYgMTYuMDg5NUwzMC42NjMxIDExLjExNzdMMjQuNjI0MSA5LjM1MTI3WiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==[DPG Badge,link=https://digitalpublicgoods.net/r/decidim] ++++

diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index f116a7bea1227..63207c34cd006 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -32,6 +32,7 @@ gem "decidim-dev", github: "decidim/decidim" ### 1.3. Run these commands ```console +sudo apt install libvips libvips-tools # or the alternative installation process for your operating system. See "3.5. Replace image processing with imagemagick to libvips" bundle update decidim bin/rails decidim:upgrade bin/rails db:migrate @@ -94,7 +95,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 +105,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 +130,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). @@ -165,7 +176,23 @@ Back in [#15534](https://github.com/decidim/decidim/pull/15534) we upgraded webp You can read more about this change on PR [#15534](https://github.com/decidim/decidim/pull/15534), [#15674](https://github.com/decidim/decidim/pull/15674). -### 3.5. [[TITLE OF THE ACTION]] +### 3.5. Replace ImageMagick with libvips for image processing + +We have upgraded our image processor within the application to libvips for speed and low memory usage. + +Support for `.ico` favicon files has been removed. Applications that relied on ICO favicons must migrate to one of the supported Libvips image formats. + +In order to install please run the following command: + +```bash +sudo apt install libvips libvips-tools +``` + +This works for Ubuntu Linux, other operating systems would need to do other command/package. + +You can read more about this change on PR [#15670](https://github.com/decidim/decidim/pull/15670). + +### 3.6. [[TITLE OF THE ACTION]] You can read more about this change on PR [#XXXX](https://github.com/decidim/decidim/pull/XXXX). diff --git a/bin/devcontainer b/bin/devcontainer new file mode 100755 index 0000000000000..6c975c41f5a7d --- /dev/null +++ b/bin/devcontainer @@ -0,0 +1,175 @@ +#!/usr/bin/env bash + +# Configuration +CONTAINER_NAME="decidim-rails-app" +SCRIPT_NAME=$(basename "$0") +CONTAINERS_TO_STOP=("decidim-rails-app-1" "decidim-selenium-1" "decidim-postgres-1" "decidim-redis-1") +DEVCONTAINER_DIR=".devcontainer" +DEVCONTAINER_JSON="$DEVCONTAINER_DIR/devcontainer.json" +DEVCONTAINER_LOCAL_JSON="$DEVCONTAINER_DIR/devcontainer.local.json" +DEVCONTAINER_BACKUP_JSON="$DEVCONTAINER_DIR/devcontainer.json.backup" + +# Function to use local config if it exists +# +# This allows having a file with local instructions for your personal developer environment at +# .devcontainer/devcontainer.local.json +# So, for instance, you can have +# +# ```json +# { +# "postCreateCommand": ".devcontainer/devcontainer.local.postCreateCommand.bash" +# } +# ``` +# +# And there you can add your own packages, dotfiles, etc. Do not forget to also call `bin/setup` +# at the end of your script. +# +use_local_config() { + if [ -f "$DEVCONTAINER_LOCAL_JSON" ]; then + # Check if jq is available + if command -v jq &> /dev/null; then + echo "Merging devcontainer.local.json with devcontainer.json..." >&2 + + # Backup original + cp "$DEVCONTAINER_JSON" "$DEVCONTAINER_BACKUP_JSON" + + # Merge configs: base config with local overrides + # jq -s '.[0] * .[1]' merges two objects, with the second overriding the first + if jq -s '.[0] * .[1]' "$DEVCONTAINER_BACKUP_JSON" "$DEVCONTAINER_LOCAL_JSON" > "$DEVCONTAINER_JSON"; then + return 0 + else + echo "Error: Failed to merge JSON configs. Restoring backup..." >&2 + mv "$DEVCONTAINER_BACKUP_JSON" "$DEVCONTAINER_JSON" + return 1 + fi + else + echo "Warning: jq not found. Install jq to use devcontainer.local.json." >&2 + return 1 + fi + fi + return 1 +} + +# Function to restore original devcontainer config +restore_devcontainer_config() { + if [ -f "$DEVCONTAINER_BACKUP_JSON" ]; then + mv "$DEVCONTAINER_BACKUP_JSON" "$DEVCONTAINER_JSON" + fi +} + +# Check if devcontainer CLI is installed +if ! command -v devcontainer &> /dev/null; then + echo "devcontainer CLI not found. Installing..." + npm install -g @devcontainers/cli + + # Check if installation was successful + if ! command -v devcontainer &> /dev/null; then + echo "Error: Failed to install devcontainer CLI" + exit 1 + fi + echo "devcontainer CLI installed successfully" +fi + +# Handle commands that don't need container ID first +case "$1" in + up) + use_local_config + devcontainer up --workspace-folder=. + status=$? + restore_devcontainer_config + exit "$status" + ;; + down) + echo "Stopping containers..." + for container in "${CONTAINERS_TO_STOP[@]}"; do + if docker ps -a --format "{{.Names}}" | grep -q "^${container}$"; then + echo "Stopping $container..." + docker stop "$container" + else + echo "Container $container not found, skipping..." + fi + done + restore_devcontainer_config + exit 0 + ;; + rebuild) + echo "Removing development_app directory..." + rm -rf development_app/ + use_local_config + devcontainer up --workspace-folder=. --remove-existing-container + status=$? + restore_devcontainer_config + exit "$status" + ;; + rm) + echo "Removing all containers and cleaning up..." + for container in "${CONTAINERS_TO_STOP[@]}"; do + if docker ps -a --format "{{.Names}}" | grep -q "^${container}$"; then + echo "Removing $container..." + docker rm -f "$container" + else + echo "Container $container not found, skipping..." + fi + done + echo "Cleaning up development_app directory..." + rm -rf development_app/ + restore_devcontainer_config + echo "Cleanup complete!" + exit 0 + ;; + ps) + echo "Checking container status..." + echo "" + found_any=false + for container in "${CONTAINERS_TO_STOP[@]}"; do + if docker ps -a --format "{{.Names}}" | grep -q "^${container}$"; then + found_any=true + status=$(docker ps -a --format "{{.Names}}\t{{.Status}}" | grep "^${container}\s" | cut -f2) + echo " $container: $status" + fi + done + if [ "$found_any" = false ]; then + echo " No containers found" + fi + exit 0 + ;; + ""| help | --help | -h) + echo "Usage: $SCRIPT_NAME " + echo "" + echo "Container Management:" + echo " up - Start the devcontainer" + echo " down - Stop all containers" + echo " rebuild - Rebuild the devcontainer" + echo " rm - Remove all containers and clean up" + echo " ps - Check container status" + echo "" + echo "Execute Commands:" + echo " $SCRIPT_NAME Execute any command in the container" + echo "" + echo "Examples:" + echo " $SCRIPT_NAME dev - Run bin/dev" + echo " $SCRIPT_NAME rails console - Run bin/rails console" + echo " $SCRIPT_NAME bundle install - Run bin/bundle install" + echo " $SCRIPT_NAME bundle exec rake test_app - Generates the test_app" + echo " $SCRIPT_NAME rspec decidim-budgets/spec/models/order_spec.rb - Runs the specs for the budgets' order model" + exit 0 + ;; +esac + +# Get the container ID for decidim-rails-app +container_id=$(docker ps --filter "name=$CONTAINER_NAME" --format "{{.ID}}" | head -n 1) + +# Check if container was found +if [ -z "$container_id" ]; then + echo "Error: $CONTAINER_NAME container not found. Start it with '$SCRIPT_NAME up'" + exit 1 +fi + +# Check if the command exists in ./bin directory +if [ -f "./bin/$1" ] && [ -x "./bin/$1" ]; then + # If it's a script in bin/, prepend bin/ to the path + devcontainer exec --container-id "$container_id" --workspace-folder=. "bin/$@" +else + # Otherwise execute the command as-is + devcontainer exec --container-id "$container_id" --workspace-folder=. "$@" +fi diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000000000..e472a0a696a9e --- /dev/null +++ b/bin/setup @@ -0,0 +1,95 @@ +#!/usr/bin/env ruby +require "fileutils" +require "json" + +APP_ROOT = File.expand_path("..", __dir__) +APP_NAME = "decidim-development-app" + +def system!(env = {}, *args) + if args.empty? + # env is actually the command + system(env, exception: true) + else + system(env, *args, exception: true) + end +end + +class CodespacesConfig + ENV_FILE = "/workspaces/.codespaces/shared/environment-variables.json" + APP_CONFIG = "development_app/config/application.rb" + + def self.running? + File.exist?(ENV_FILE) + end + + def initialize + @hostname = nil + end + + def hostname + @hostname ||= fetch_hostname + end + + def env_vars + hostname ? { "DECIDIM_HOST" => hostname } : {} + end + + def configure_application! + return unless hostname + return unless File.exist?(APP_CONFIG) + + config_content = File.read(APP_CONFIG) + return if config_content.match?(/^\s+config\.hosts/) + + updated_content = config_content.sub( + /(config\.load_defaults\s+[\d.]+\s*\n)/, + "\\1 config.hosts << \"#{hostname}\"\n" + ) + + File.write(APP_CONFIG, updated_content) + puts "\n== Configured Codespaces host: #{hostname} ==" + end + + private + + def fetch_hostname + return nil unless File.exist?(ENV_FILE) + + env_data = JSON.parse(File.read(ENV_FILE)) + codespace_name = env_data["CODESPACE_NAME"] + return nil unless codespace_name + + "#{codespace_name}-3000.app.github.dev" + end +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts "== Installing dependencies ==" + system! "gem install bundler --conservative" + system("bundle check") || system!("bundle install") + system("npm install") + + puts "\n== Preparing decidim development app ==" + if CodespacesConfig.running? + codespaces = CodespacesConfig.new + system!(codespaces.env_vars, "bundle exec rake development_app") unless Dir.exist?("development_app") + codespaces.configure_application! + else + system!("bundle exec rake development_app") unless Dir.exist?("development_app") + end + + puts "\n== Preparing database ==" + system("bin/rails db:create") + + if File.exist?("development_app/tmp/pids/server.pid") + puts "\n== Removing the leftover pid file ==" + system("rm development_app/tmp/pids/server.pid") + end + + puts "\n== Starting application server ==" + system! "bin/dev" +end 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/app/models/decidim/accountability/result.rb b/decidim-accountability/app/models/decidim/accountability/result.rb index 5a70742441e0a..a0ecbd20993a0 100644 --- a/decidim-accountability/app/models/decidim/accountability/result.rb +++ b/decidim-accountability/app/models/decidim/accountability/result.rb @@ -39,13 +39,15 @@ class Result < Accountability::ApplicationRecord after_save :update_parent_progress, if: -> { parent_id.present? } - searchable_fields( - scope_id: :decidim_scope_id, - participatory_space: { component: :participatory_space }, - A: :title, - D: :description, - datetime: :start_date - ) + searchable_fields({ + scope_id: :decidim_scope_id, + participatory_space: { component: :participatory_space }, + A: :title, + D: :description, + datetime: :start_date + }, + index_on_create: ->(result) { result.visible? }, + index_on_update: ->(result) { result.visible? }) geocoded_by :address @@ -110,9 +112,7 @@ def self.ransackable_attributes(auth_object = nil) base + %w(created_at progress) end - def self.ransackable_associations(auth_object = nil) - return [] unless auth_object&.admin? - + def self.ransackable_associations(_auth_object = nil) %w(taxonomies status) end diff --git a/decidim-accountability/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..8c0904651f9e3 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: @@ -357,7 +358,7 @@ ja: url: この結果が見つかるURL participatory_spaces: highlighted_results: - see_all: すべての結果を見る (%{count}) + see_all: すべての結果を見る resource_links: included_projects: result_project: この結果に含まれるプロジェクト diff --git a/decidim-accountability/config/locales/ro-RO.yml b/decidim-accountability/config/locales/ro-RO.yml index 9745c775239be..44bfcafae8beb 100644 --- a/decidim-accountability/config/locales/ro-RO.yml +++ b/decidim-accountability/config/locales/ro-RO.yml @@ -1,6 +1,10 @@ ro: activemodel: attributes: + milestone: + description: Descriere + entry_date: Data + title: Titlu result: decidim_accountability_status_id: Stare decidim_category_id: Categorie @@ -9,7 +13,7 @@ ro: end_date: Data de sfârșit progress: Progres start_date: Data de început - subresults: Subrezultate + subresults: Rezultate title: Titlu updated_at: Ultima actualizare status: @@ -25,7 +29,11 @@ ro: decidim: accountability: result: - budget_text: Acest rezultat %{link} a fost inclus + budget_text: Rezultatul %{link} a fost inclus + meetings_ids: 'În cadrul acestei ședințe: %{link}' + project_ids: 'A fost inclus în acest proiect: %{link}' + proposal_ids: 'A fost adăugat la această propunere: %{link}' + text: 'A fost adăugat la acest rezultat: %{link}' decidim/accountability/result: one: Rezultat few: Rezultate @@ -33,46 +41,147 @@ ro: decidim: accountability: actions: + add_milestone: Adaugați obiectiv nou + add_result: Adăugați rezultat + confirm_delete_result: Sunteți sigur că doriți să eliminați acest rezultat? confirm_destroy: Sunteți sigur că doriți să ștergeți %{name}? + deleted_results_info: Rezultatele șterse pot fi restaurate din gunoi. destroy: Ștergeți - edit: Actualizare + edit: Modificați + import: Importați rezultate dintr-o altă componentă import_csv: Importați rezultate dintr-un fișier CSV + new_milestone: Reper nou new_result: Rezultat nou new_status: Stare nouă preview: Previzualizare title: Acțiuni + view_deleted_results: Vizualizați rezultatele eliminate admin: exports: result_comments: Comentarii results: Rezultate + import_components: + create: + invalid: A apărut o problemă la importul rezultatelor, vă rugăm să urmați instrucțiunile cu atenție și să vă asigurați că ați selectat proiecte pentru implementare. + filters: + new_items_projects: + one: Va fi importat 1 proiect selectat + few: "Vor fi importate %{count} proiecte selectate" + other: "Vor fi importate %{count} proiecte selectate" + new_items_proposals: + one: O propunere va fi importată + few: "Vor fi importate %{count} propuneri" + other: "Vor fi importate %{count} propuneri" + proposal_state: Starea propunerilor + select_state: Selectați starea + form: + create: Import + no_components: Nu există componente în acest spațiu participativ de importat. + origin_component_id: Componenta de origine + select_component: Selectează o componentă + new: + success: + one: 1 rezultat în așteptare pentru a fi importat. Veți fi notificat prin e-mail, odată ce importul a fost finalizat. + few: "%{count} rezultate în așteptare pentru a fi importate. Veți fi notificat prin e-mail, odată ce importul a fost finalizat." + other: "%{count} rezultate în așteptare pentru a fi importate. Veți fi notificat prin e-mail, odată ce importul a fost finalizat." + title: Importați rezultate dintr-o altă componentă import_results: new: download_export: Descărcați Exportul în format CSV import: Importați + info: | +

Vă recomandăm să urmați acești pași:

+
    +
  1. Creați stările pentru rezultatele pe care doriți să le adăugați
  2. +
  3. Creați cel puțin un rezultat manual prin intermediul acestui panou de administrare înainte de a utiliza Importul, pentru a înțelege mai bine formatul și ce trebuie să completați.
  4. +
  5. %{link_export_csv}
  6. +
  7. Efectuați modificările local. Puteți modifica doar următoarele coloane ale fișierului CSV (restul vor fi ignorate):
  8. +
      +
    • taxonomii/id-uri: ID-uri pentru taxonomii (dacă sunt mai multe, separați-le prin virgulă)
    • +
    • parent/id: ID-ul părintelui (pentru rezultatele corelate). Opțional
    • +
    • title/en: Titlu în limba engleză. Acesta va depinde de configurația lingvistică a platformei dvs.
    • +
    • description/en: Descriere în limba engleză. Aceasta va depinde de configurația lingvistică a platformei dvs.
    • +
    • start_date: data la care rezultatul începe execuția (format AAAA-LL-ZZ)
    • +
    • end_date: data la care rezultatul se termină execuția (format AAAA-LL-ZZ)
    • +
    • status/id: ID-ul stării pentru acest rezultat
    • +
    • progress: Procentul (de la 0 la 100) al execuției
    • +
    • proposals_ids: ID-ul intern al propunerilor aferente (separate prin virgulă). Se convertește automat în proposal_url
    • +
    + +
title: Importați rezultate dintr-un fișier CSV imports: create: invalid: A apărut o problemă la importul rezultatelor. + success: Importarea fișierului a început. În câteva minute veți primi un e-mail cu rezultatul importului. + milestones: + create: + invalid: A apărut o problemă la crearea acestui reper. + success: Reperul a fost creat cu succes. + destroy: + success: Reperul a fost eliminat cu succes. + edit: + title: Modificați obiectivul + update: Actualizați reperul + index: + title: Repere + new: + create: Creați un reper + title: Reper nou + update: + invalid: A apărut o problemă la actualizarea acestui reper. + success: Reperul a fost actualizat cu succes. models: result: name: Rezultat status: name: Stare results: + bulk_actions: + dates_form: + change_dates: Schimbați data + end_date: Dată de sfârșit + start_date: Dată de începere + dropdown: + actions: Acțiuni + change_dates: Schimbați datele + change_status: Schimbați starea + change_taxonomies: Schimbați taxonomia + status_form: + change_status: Schimbați starea + status: Stare + submit_buttons: + cancel: Anulați + taxonomies_form: + change_taxonomies: Schimbați taxonomia create: invalid: A apărut o problemă la crearea acestui rezultat. success: Rezultatul a fost creat. edit: - title: Actualizare rezultat - update: Actualizare rezultat + title: Modificați rezultatul + update: Actualizați rezultatul index: + selected: Selectat title: Rezultate + manage_trash: + title: Rezultate eliminate new: create: Creați un rezultat nou title: Rezultat nou update: invalid: A apărut o eroare la actualizarea acestui rezultat. success: Rezultatul a fost actualizat. + update_dates: + invalid: A apărut o eroare la actualizarea datelor acestui rezultat + success: Datele rezultatului actualizate cu succes + update_status: + invalid: A apărut o eroare la actualizarea stării rezultatelor + success: Starea rezultatelor a fost actualizată + update_taxonomies: + invalid: Nu s-au putut actualiza taxonomiile %{taxonomies} pentru rezultatele %{results} + select_a_result: Selectați un rezultat + select_a_taxonomy: Selectați o taxonomie + success: Actualizare cu succes a taxonomiilor %{taxonomies} pentru rezultatele %{results} shared: subnav: statuses: Stări @@ -83,55 +192,77 @@ ro: destroy: success: Starea a fost ștearsă. edit: - title: Actualizare stare - update: Actualizare stare + title: Modificați stare + update: Actualizați stare index: title: Stări new: - create: Creează stare + create: Creați stare title: Stare nouă update: invalid: A apărut o eroare la actualizarea acestei stări. success: Starea a fost actualizată. admin_log: + milestones: + create: "%{user_name} a creat reperul %{resource_name}" + delete: "%{user_name} a eliminat reperul %{resource_name}" + update: "%{user_name} a actualizat reperul %{resource_name}" result: create: "%{user_name} a creat rezultatul %{resource_name} în %{space_name}" delete: "%{user_name} a șters rezultatul %{resource_name} din %{space_name}" + restore: "%{user_name} a restaurat rezultatul %{resource_name} din %{space_name}" + soft_delete: "%{user_name} a mutat la gunoi rezultatul %{resource_name} din %{space_name}" update: "%{user_name} a actualizat rezultatul %{resource_name} din %{space_name}" status: - create: "%{user_name} a creat starea resursei %{resource_name}" - delete: "%{user_name} a șters starea resursei %{resource_name}" - update: "%{user_name} a actualizat starea %{resource_name}" + create: "%{user_name} a creat o stare %{resource_name}" + delete: "%{user_name} a eliminat starea resursei %{resource_name}" + update: "%{user_name} a actualizat starea resursei %{resource_name}" value_types: parent_presenter: - not_found: 'Elementul-părinte nu a fost găsit în baza de date (ID: %{id})' + not_found: 'Elementul părinte nu a fost găsit în baza de date (ID: %{id})' content_blocks: highlighted_results: results: Rezultate + creation: + text: Rezultatul a fost creat import_mailer: import: errors: Erori errors_present: A apărut o problemă la importul rezultatelor. row_number: Rând - subject: Importare cu succes a rezultatelor + subject: Import cu succes a rezultatelor success: Rezultate importate. Puteți revizui rezultatele în interfața de administrare. import_projects_mailer: import: added_projects: one: Un rezultat a fost importat din proiecte. - few: "Rezultatele au fost importate din proiecte." + few: "%{count} rezultate au fost importate din proiecte." other: "%{count} rezultate au fost importate din proiecte." subject: Importul de proiecte a avut succes + success: Proiecte importate cu succes ca rezultate în componenta %{component_name}. Puteți analiza rezultatele în interfața de administrare. + import_proposals_mailer: + import: + added_proposals: + one: Un rezultat a fost importat din propuneri + few: "%{count} rezultate au fost importate din propuneri." + other: "%{count} rezultate au fost importate din propuneri." + subject: Propunerile au fost importate cu succes + success: Propuneri importate cu succes ca rezultate în componenta %{component_name}. Puteți analiza rezultatele în interfața de administrare. last_activity: new_result: 'Rezultat nou:' models: + milestone: + fields: + entry_date: Data + title: Titlu result: fields: created_at: Creat - end_date: Data de sfârșit + end_date: Dată de sfârșit progress: Progres - start_date: Data de început + start_date: Dată de începere status: Stare + taxonomies: Taxonomii title: Titlu status: fields: @@ -146,12 +277,14 @@ ro: few: "%{count} rezultate" other: "%{count} rezultate" home_header: - global_status: Stare execuție globală + global_status: Stare globală de execuție + milestones: + title: Repere no_results: Nu există proiecte root_taxonomies: title: 'Vizualizare după:' search: - search: Caută acțiuni + search: Cautați acțiuni show: stats: back_to_resource: Înapoi la rezultat @@ -160,37 +293,86 @@ ro: results: status_id_eq: label: Stare + taxonomies_part_of_contains: + label: Taxonomie + tooltips: + deleted_results_info: Nu puteți elimina acest rezultat components: accountability: actions: comment: Comentariu + vote_comment: Votați comentariul name: Responsabilitate settings: global: + clear_all: Eliminați tot comments_enabled: Comentarii activate comments_max_length: Lungimea maximă a comentariilor (Lăsați 0 pentru valoarea implicită) default_taxonomy: Taxonomie implicită default_taxonomy_help: Selectați ce taxonomie doriți să afișați implicit. Dacă nu este selectată nicio taxonomie, rezultatele vor fi afișate într-un format de listă. - display_progress_enabled: Afișare progres + define_taxonomy_filters: Vă rugăm să definiți niște filtre pentru acest spațiu participativ înainte de a utiliza această setare. + display_progress_enabled: Afișați progres + geocoding_enabled: Hărți activate intro: Introducere + no_taxonomy_filters_found: Nu am găsit nici un filtru de taxonomie. + taxonomy_filters: Selectați filtrele pentru componentă + taxonomy_filters_add: Adaugați filtru step: comments_blocked: Comentarii blocate visualization: Vizualizare + download_your_data: + show: + result_comments: Export al comentariilor rezultatelor + results: Export rezultate events: accountability: proposal_linked: email_intro: 'Propunerea "%{proposal_title}" a fost inclusă într-un rezultat. O puteți vedea de pe această pagină:' - email_outro: Ați primit această notificare deoarece urmați "%{proposal_title}". Puteți înceta să primiți notificări urmând linkul anterior. - email_subject: O actualizare la %{proposal_title} + email_outro: Ați primit această notificare deoarece urmăriți "%{proposal_title}". Puteți înceta să primiți notificări urmând linkul anterior. + email_subject: O actualizare a %{proposal_title} notification_title: Propunerea %{proposal_title} a fost inclusă în rezultatul %{resource_title}. result_progress_updated: email_intro: 'Rezultatul "%{resource_title}", care include propunerea "%{proposal_title}", este acum %{progress}% complet. Îl puteți vedea de pe această pagină:' - email_outro: Ai primit această notificare deoarece urmărești „%{proposal_title}” și această propunere este inclusă în rezultatul „%{resource_title}”. Poți înceta să primești notificări urmând link-ul anterior. - email_subject: O actualizare la progresul %{resource_title} + email_outro: Ați primit această notificare deoarece urmăriți „%{proposal_title}”, iar această propunere este inclusă în rezultatul „%{resource_title}”. Puteți înceta să primiți notificări urmând link-ul anterior. + email_subject: O actualizare privind progresul a %{resource_title} notification_title: Rezultatul %{resource_title}, care include propunerea %{proposal_title}, este acum %{progress}% complet. + open_data: + help: + result_comments: + alignment: În cazul în care acest comentariu a fost favorabil, contrar sau neutru + author: Numele participantului care a făcut acest comentariu + body: Comentariul în sine + commentable_id: Id-ul unic al comentariului + commentable_type: Tipul resursei comentate (dacă este întâlnire, propunere etc.) + created_at: Data când acest comentariu a fost creat + depth: Locul în care acest comentariu este în arborele de comentarii (dacă este un răspuns sau un răspuns la un răspuns) + id: Id-ul unic al comentariului + locale: Localizarea (limba) pe care participantul a folosit-o atunci când a lăsat acest comentariu + root_commentable_url: URL-ul resursei unde a fost lăsat acest comentariu + results: + address: Adresa rezultatului (dacă există) + children_count: Numărul rezultatelor copii + comments_count: Numărul de comentarii pe care le are acest rezultat + component: Componenta căreia îi aparține rezultatul + created_at: Data când acest rezultat a fost creat + description: Descrierea rezultatului + end_date: Data la care încetează este terminat rezultatul și execuția încetează + id: Identificatorul unic al acestui rezultat + latitude: Latitudinea rezultatului în cazul în care acesta are o localizare fizică + longitude: Longitudinea rezultatului în cazul în care are o localizare fizică + parent: Rezultatul părinte (dacă există) al rezultatului + progress: Procentul de execuție al rezultatului + proposal_urls: URL-urile propunerilor care sunt incluse în acest rezultat + reference: Referința unică a rezultatului + start_date: Data la care începe execuția rezultatului + status: Starea actuală a acestui rezultat + taxonomies: Taxonomiile rezultatului + title: Titlul rezultatului + updated_at: Ultima dată când acest rezultat a fost actualizat + url: URL-ul unde acest rezultat poate fi găsit participatory_spaces: highlighted_results: - see_all: Vezi toate rezultatele + see_all: Vedeți toate rezultatele resource_links: included_projects: result_project: Proiecte incluse în acest rezultat 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/db/data/20260113140600_reindex_results.rb b/decidim-accountability/db/data/20260113140600_reindex_results.rb new file mode 100644 index 0000000000000..297b7f6eb7695 --- /dev/null +++ b/decidim-accountability/db/data/20260113140600_reindex_results.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class ReindexResults < ActiveRecord::Migration[7.2] + def up + Decidim::Component.where(manifest_name: :accountability).find_each do |component| + component.manifest.run_hooks(:publish, component) + end + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/decidim-accountability/lib/decidim/accountability/component.rb b/decidim-accountability/lib/decidim/accountability/component.rb index f07e7c87126d7..848db42b5443c 100644 --- a/decidim-accountability/lib/decidim/accountability/component.rb +++ b/decidim-accountability/lib/decidim/accountability/component.rb @@ -11,8 +11,16 @@ component.permissions_class_name = "Decidim::Accountability::Permissions" component.query_type = "Decidim::Accountability::AccountabilityType" - component.on(:before_destroy) do |instance| - raise StandardError, "Cannot remove this component" if Decidim::Accountability::Result.where(component: instance).any? + component.on(:publish) do |instance| + Decidim::Accountability::Result.where(component: instance).find_in_batches(batch_size: 10) do |batch| + Decidim::UpdateSearchIndexesJob.perform_later(batch) + end + end + + component.on(:unpublish) do |instance| + Decidim::Accountability::Result.where(component: instance).find_in_batches(batch_size: 10) do |batch| + Decidim::RemoveSearchIndexesJob.perform_later(batch) + end end # These actions permissions can be configured in the admin panel @@ -22,7 +30,7 @@ resource.model_class_name = "Decidim::Accountability::Result" resource.template = "decidim/accountability/results/linked_results" resource.card = "decidim/accountability/result" - resource.searchable = false + resource.searchable = true resource.actions = %w(comment vote_comment) end diff --git a/decidim-accountability/lib/decidim/accountability/seeds.rb b/decidim-accountability/lib/decidim/accountability/seeds.rb index 74974e0da6435..8fdabcb570a42 100644 --- a/decidim-accountability/lib/decidim/accountability/seeds.rb +++ b/decidim-accountability/lib/decidim/accountability/seeds.rb @@ -43,7 +43,7 @@ def create_component! end def create_statuses!(component:) - 5.times do |i| + config_value(:accountability_statuses_count).times do |i| Decidim::Accountability::Status.create!( component:, name: Decidim::Faker::Localized.word, @@ -61,7 +61,7 @@ def create_taxonomies! parent_taxonomy = root_taxonomy.children.sample || create_taxonomy!(name: ::Faker::Lorem.sentence(word_count: 5), parent: root_taxonomy) taxonomies = [parent_taxonomy] - 2.times do + config_value(:accountability_taxonomies_count).times do taxonomies << if parent_taxonomy.children.count > 1 parent_taxonomy.children.sample else diff --git a/decidim-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/db/data/reindex_results_spec.rb b/decidim-accountability/spec/db/data/reindex_results_spec.rb new file mode 100644 index 0000000000000..c331052df5090 --- /dev/null +++ b/decidim-accountability/spec/db/data/reindex_results_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require "spec_helper" +require "./db/data/20260113140600_reindex_results" + +describe ReindexResults do + let(:migrator) do + described_class.new.tap do |m| + m.verbose = false + end + end + + before do + clear_enqueued_jobs + end + + describe "#up" do + context "when the component is published" do + let(:component) { create(:accountability_component, published_at: Time.zone.now) } + + context "and there are results" do + let!(:results) { create_list(:result, 2, component:) } + + it "those are added to index" do + Decidim::SearchableResource.delete_all + clear_enqueued_jobs + + expect(Decidim::SearchableResource.where(resource: results)).to be_empty + expect { migrator.migrate(:up) }.to have_enqueued_job(Decidim::UpdateSearchIndexesJob).at_least(1).times + + perform_enqueued_jobs(only: Decidim::UpdateSearchIndexesJob) + expect(Decidim::SearchableResource.where(resource: results)).not_to be_empty + # 3 languages multiplied by 2 results + expect(Decidim::SearchableResource.where(resource: results).count).to eq(6) + end + end + + context "and there are deleted results" do + let!(:results) { create_list(:result, 2, component:) } + + it "those are added to index" do + results.map(&:destroy) + Decidim::SearchableResource.delete_all + + expect(Decidim::SearchableResource.where(resource: results)).to be_empty + perform_enqueued_jobs(only: Decidim::UpdateSearchIndexesJob) { migrator.migrate(:up) } + expect(Decidim::SearchableResource.where(resource: results)).to be_empty + end + end + + context "and there are no results" do + let!(:results) { [] } + + it "does not reindex the results" do + Decidim::SearchableResource.delete_all + + expect(Decidim::SearchableResource.where(resource: results)).to be_empty + perform_enqueued_jobs(only: Decidim::UpdateSearchIndexesJob) { migrator.migrate(:up) } + expect(Decidim::SearchableResource.where(resource: results)).to be_empty + end + end + end + + context "when the component is not published" do + let(:component) { create(:accountability_component, published_at: nil) } + let!(:results) { create_list(:result, 2, component:) } + + it "does not reindex the results" do + Decidim::SearchableResource.delete_all + + expect(Decidim::SearchableResource.where(resource: results)).to be_empty + perform_enqueued_jobs(only: Decidim::UpdateSearchIndexesJob) { migrator.migrate(:up) } + expect(Decidim::SearchableResource.where(resource: results)).to be_empty + end + end + + context "when the component is deleted" do + let(:component) { create(:accountability_component, published_at: Time.zone.now, deleted_at: Time.zone.now) } + let!(:results) { create_list(:result, 2, component:) } + + it "does not reindex the results" do + Decidim::SearchableResource.delete_all + + expect(Decidim::SearchableResource.where(resource: results)).to be_empty + perform_enqueued_jobs(only: Decidim::UpdateSearchIndexesJob) { migrator.migrate(:up) } + expect(Decidim::SearchableResource.where(resource: results)).to be_empty + end + end + end +end diff --git a/decidim-accountability/spec/lib/decidim/accountability/component_spec.rb b/decidim-accountability/spec/lib/decidim/accountability/component_spec.rb index bde48b30bc2d3..f8f2b0f6e0d4c 100644 --- a/decidim-accountability/spec/lib/decidim/accountability/component_spec.rb +++ b/decidim-accountability/spec/lib/decidim/accountability/component_spec.rb @@ -21,4 +21,44 @@ it_behaves_like "has mandatory config setting", :comments_max_length end end + + describe "hooks" do + let!(:results) { create_list(:result, 5, component:) } + + before do + clear_enqueued_jobs + end + + describe "publish" do + let(:component) { create(:accountability_component, published_at: nil) } + + it "adds the results to search index" do + expect(Decidim::SearchableResource.where(resource: results)).to be_empty + component.publish! + + perform_enqueued_jobs(only: Decidim::UpdateSearchIndexesJob) do + component.manifest.run_hooks(:publish, component) + end + + expect(Decidim::SearchableResource.where(resource: results)).to be_present + # 3 languages multiplied by 5 results + expect(Decidim::SearchableResource.where(resource: results).count).to eq(15) + end + end + + describe "unpublish" do + it "removes the results from search index" do + # 3 languages multiplied by 5 results + expect(Decidim::SearchableResource.where(resource: results).count).to eq(15) + expect(Decidim::SearchableResource.where(resource: results)).to be_present + component.unpublish! + + perform_enqueued_jobs(only: Decidim::UpdateSearchIndexesJob) do + component.manifest.run_hooks(:publish, component) + end + + expect(Decidim::SearchableResource.where(resource: results)).to be_empty + end + end + end end diff --git a/decidim-accountability/spec/requests/result_search_spec.rb b/decidim-accountability/spec/requests/result_search_spec.rb index 7a2b8480245c4..efb508835efd6 100644 --- a/decidim-accountability/spec/requests/result_search_spec.rb +++ b/decidim-accountability/spec/requests/result_search_spec.rb @@ -113,5 +113,23 @@ expect(subject).to contain_exactly(result1, result2) end end + + context "when filtering by taxonomy" do + context "when filtering by taxonomy1" do + let(:filter_params) { { taxonomies_part_of_contains: taxonomy1.id } } + + it "returns results with that taxonomy directly assigned" do + expect(subject).to contain_exactly(result1) + end + end + + context "when filtering by taxonomy2" do + let(:filter_params) { { taxonomies_part_of_contains: taxonomy2.id } } + + it "returns results with that taxonomy and results with its child taxonomies" do + expect(subject).to contain_exactly(result2, result4) + end + end + end end end 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/content_blocks/update_content_block.rb b/decidim-admin/app/commands/decidim/admin/content_blocks/update_content_block.rb index 3d4921ed17c2b..33b2867d990da 100644 --- a/decidim-admin/app/commands/decidim/admin/content_blocks/update_content_block.rb +++ b/decidim-admin/app/commands/decidim/admin/content_blocks/update_content_block.rb @@ -6,7 +6,7 @@ module ContentBlocks # This command gets called when a content block is updated from the admin # panel. class UpdateContentBlock < Decidim::Command - attr_reader :form, :content_block, :scope + attr_reader :form, :content_block, :scope, :attachments_to_purge # Public: Initializes the command. # @@ -17,6 +17,7 @@ def initialize(form, content_block, scope) @form = form @content_block = content_block @scope = scope + @attachments_to_purge = [] end # Public: Updates the content block settings and its attachments. @@ -55,6 +56,8 @@ def call return broadcast(:invalid) unless images_valid + purge_attachment + broadcast(:ok, content_block) end @@ -67,14 +70,20 @@ def update_content_block_settings def update_content_block_images content_block.manifest.images.each do |image_config| image_name = image_config[:name] - if form.images[image_name] content_block.images_container.send("#{image_name}=", form.images[image_name]) - elsif form.images[:"remove_#{image_name}"] == "1" + elsif form.images[:"remove_#{image_name}"] + @attachments_to_purge << content_block.images_container.send(image_name.to_s) content_block.images_container.send("#{image_name}=", nil) end end end + + def purge_attachment + attachments_to_purge.each do |attachment_to_purge| + attachment_to_purge.purge if attachment_to_purge.respond_to?(:purge) + end + end 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..32ad418fa1f7f --- /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:, + participatory_space: @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..4bcf5ac44fa43 --- /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.participatory_space.respond_to?(:private_space?) + return unless resource.participatory_space.private_space? + return if resource.participatory_space.respond_to?(:is_transparent) && resource.participatory_space.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.participatory_space) + 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/components/base_controller.rb b/decidim-admin/app/controllers/decidim/admin/components/base_controller.rb index faf46a8ba2e07..eaff40b1f4161 100644 --- a/decidim-admin/app/controllers/decidim/admin/components/base_controller.rb +++ b/decidim-admin/app/controllers/decidim/admin/components/base_controller.rb @@ -31,7 +31,7 @@ class BaseController < Decidim::Admin::ApplicationController enforce_permission_to :read, :component, component: current_component end - before_action :set_component_breadcrumb_item + before_action :set_breadcrumb_items def permissions_context super.merge( @@ -68,7 +68,7 @@ def skip_manage_component_permission false end - def set_component_breadcrumb_item + def set_breadcrumb_items context_breadcrumb_items << { label: t("components", scope: "decidim.admin.menu"), url: parent_path, diff --git a/decidim-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..4d0e713998dd4 --- /dev/null +++ b/decidim-admin/app/controllers/decidim/admin/participatory_space/concerns/has_members.rb @@ -0,0 +1,165 @@ +# 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. + # + # It takes the current_participatory_space that is defined + # in the controller, so there is no need to define any method + module HasMembers + extend ActiveSupport::Concern + + included do + include Decidim::Admin::ParticipatorySpace::Concerns::MembersFilterable + helper PaginateHelper + helper_method :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({}) + 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) + + 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) + + 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 participatory_space. + # + # It can be redefined at controller level if you need to redirect elsewhere. + def after_destroy_path + members_path(current_participatory_space) + 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 ||= current_participatory_space + .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..f8e2e45f0bdb5 --- /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 `participatory_space` method that + # returns an instance of the model to relate the member to. + module HasMembersCsvImport + extend ActiveSupport::Concern + + included do + helper_method :participatory_space + + def new + enforce_permission_to :csv_import, :space_member + @form = form(MemberCsvImportForm).from_params({}, participatory_space:) + @count = Decidim::ParticipatorySpace::Member.by_participatory_space(participatory_space).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, participatory_space:) + + 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(participatory_space).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 participatory_space. + # + # It can be redefined at controller level if you need to redirect elsewhere. + def after_import_path + participatory_space + 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 participatory_space + 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/helpers/decidim/admin/moderations/reports_helper.rb b/decidim-admin/app/helpers/decidim/admin/moderations/reports_helper.rb index ba7b387487108..eec346b0f4fff 100644 --- a/decidim-admin/app/helpers/decidim/admin/moderations/reports_helper.rb +++ b/decidim-admin/app/helpers/decidim/admin/moderations/reports_helper.rb @@ -19,7 +19,7 @@ def reportable_author_name(reportable) when User content_tag :li do link_to current_or_new_conversation_path_with(author), target: "_blank", rel: "noopener" do - "#{author.name} #{icon "mail-send-line"}".html_safe + "#{author.presenter.name} #{icon "mail-send-line"}".html_safe end end when Decidim::Meetings::Meeting diff --git a/decidim-admin/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..efb86b4084475 --- /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, participatory_space, current_user) + return if email.blank? || user_name.blank? + + params = { + name: user_name, + email: email.downcase.strip + } + member_form = MemberForm.from_params(params, participatory_space:) + .with_context( + current_user:, + current_participatory_space: participatory_space + ) + + CreateMember.call(member_form, participatory_space, via_csv: true) + end + end + end + end +end diff --git a/decidim-admin/app/queries/decidim/admin/newsletter_recipients.rb b/decidim-admin/app/queries/decidim/admin/newsletter_recipients.rb index 697ce9f9bf04c..e74b6c589a422 100644 --- a/decidim-admin/app/queries/decidim/admin/newsletter_recipients.rb +++ b/decidim-admin/app/queries/decidim/admin/newsletter_recipients.rb @@ -129,7 +129,7 @@ def private_member_ids return unless @form.send_to_private_members return [] if private_spaces.blank? - Decidim::ParticipatorySpacePrivateUser.private_user_ids_for_participatory_spaces(private_spaces) + Decidim::ParticipatorySpace::Member.member_ids_for_participatory_spaces(private_spaces) end end end diff --git a/decidim-admin/app/views/decidim/admin/participatory_space_private_users/_form.html.erb b/decidim-admin/app/views/decidim/admin/members/_form.html.erb similarity index 100% rename from decidim-admin/app/views/decidim/admin/participatory_space_private_users/_form.html.erb rename to decidim-admin/app/views/decidim/admin/members/_form.html.erb diff --git a/decidim-admin/app/views/decidim/admin/participatory_space_private_users/edit.html.erb b/decidim-admin/app/views/decidim/admin/members/edit.html.erb similarity index 60% rename from decidim-admin/app/views/decidim/admin/participatory_space_private_users/edit.html.erb rename to decidim-admin/app/views/decidim/admin/members/edit.html.erb index c3bfd1492cf1f..494738f61b422 100644 --- a/decidim-admin/app/views/decidim/admin/participatory_space_private_users/edit.html.erb +++ b/decidim-admin/app/views/decidim/admin/members/edit.html.erb @@ -7,8 +7,8 @@
- <%= decidim_form_for(@form, url: participatory_space_private_user_path(current_participatory_space, @private_user), html: { class: "form-defaults form edit_participatory_space_private_user" }) do |f| %> - <%= render partial: "decidim/admin/participatory_space_private_users/form", object: f %> + <%= decidim_form_for(@form, url: member_path(current_participatory_space, @member), html: { class: "form-defaults form edit_member" }) do |f| %> + <%= render partial: "decidim/admin/members/form", object: f %>
<%= f.submit t(".update"), class: "button button__sm button__secondary" %> diff --git a/decidim-admin/app/views/decidim/admin/participatory_space_private_users/index.html.erb b/decidim-admin/app/views/decidim/admin/members/index.html.erb similarity index 55% rename from decidim-admin/app/views/decidim/admin/participatory_space_private_users/index.html.erb rename to decidim-admin/app/views/decidim/admin/members/index.html.erb index ee68d751bde8b..bfb33b5d93305 100644 --- a/decidim-admin/app/views/decidim/admin/participatory_space_private_users/index.html.erb +++ b/decidim-admin/app/views/decidim/admin/members/index.html.erb @@ -1,18 +1,18 @@ <% add_decidim_page_title(t(".title")) %> -
+

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

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

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

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

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

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

diff --git a/decidim-admin/config/locales/ar.yml b/decidim-admin/config/locales/ar.yml index 78e1e1258300e..0a35f31dd02b2 100644 --- a/decidim-admin/config/locales/ar.yml +++ b/decidim-admin/config/locales/ar.yml @@ -89,11 +89,6 @@ ar: welcome_notification_body: محتوى إشعار الترحيب welcome_notification_subject: موضوع إشعار الترحيب youtube_handler: مُعرّف حساب يوتيوب - participatory_space_private_user: - email: البريد الإلكتروني - name: الإسم - participatory_space_private_user_csv_import: - file: ملف scope: code: الشفرة name: الاسم @@ -135,10 +130,6 @@ ar: attributes: official_img_footer: allowed_file_content_types: ملف صورة غير صالح - participatory_space_private_user_csv_import: - attributes: - file: - malformed: خطأ في ملفّ الاستيراد، يرجى قراءة التعليمات بعناية والتأكد من أن ترميز الملف هو UTF-8. user_group_csv_verification: attributes: file: @@ -342,17 +333,6 @@ ar: values: 'false': Officialized 'true': غير رسمي - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: تم قبول الدعوة - values: - 'false': لم يتم القبول - 'true': تم قبوله - user_invitation_sent_at_not_null: - label: تم إرسال الدعوة - values: - 'false': لم يتم الارسال - 'true': تم الإرسال private_space_eq: label: خاص values: @@ -444,6 +424,10 @@ ar: explanation: يمكن ترقية المشاركين المدارين إلى مشاركين عاديين. يعني ذلك أنه سيتم دعوتهم إلى التطبيق ولن تتمكن من إدارته مرة أخرى. سيتلقى المشارك المدعو رسالة بريد إلكتروني لقبول دعوتك. new_managed_user_promotion: ترويج مشارك جديد مُدار promote: تروج \ يشجع \ يعزز \ ينمى \ يطور + members_csv_imports: + new: + csv_upload: + title: قم بتحميل ملف CSV الخاص بك menu: admin_log: سجل نشاط المسؤول admins: المدراء @@ -498,8 +482,6 @@ ar: sent_to: أُرسِلت إلى subject: موضوع name: النشرة الإخبارية - participatory_space_private_user: - name: المشاركة الفضاء المشارك الخاص scope: fields: name: اسم @@ -657,28 +639,6 @@ ar: update: error: حدثت مشكلة أثناء تحديث هذه المؤسسة. success: تم تحديث المنظمة بنجاح. - participatory_space_private_users: - create: - error: حدثت مشكلة أثناء إضافة مشارك خاص لهذه المساحة التشاركية. - success: المشاركة الفضاء الفضاء المشترك وصول خلق بنجاح. - destroy: - error: حدثت مشكلة في حذف مشارك خاص لهذه المساحة التشاركية. - success: المشاركة الفضاء الفضاء وصول المشاركين دمرت بنجاح. - index: - title: المشاركة الفضاء المشارك الخاص - new: - create: إنشاء - title: مشارك جديد الفضاء الخاص المشارك. - participatory_space_private_users_csv_imports: - new: - csv_upload: - title: قم بتحميل ملف CSV الخاص بك - destroy: - button: حذف جميع المشاركين الخاصين - empty: ليس لديك أي مشاركين خاصين. - explanation: لديك %{count} مشاركين خاصين. - title: حذف جميع المشاركين الخاصين - upload: حمّل reminders: new: submit: إرسال diff --git a/decidim-admin/config/locales/bg.yml b/decidim-admin/config/locales/bg.yml index 7a99b1d115ed6..f7fe5d100f773 100644 --- a/decidim-admin/config/locales/bg.yml +++ b/decidim-admin/config/locales/bg.yml @@ -89,9 +89,6 @@ bg: welcome_notification_body: Тяло на приветствието welcome_notification_subject: Тема на приветствието youtube_handler: Манипулатор на YouTube - participatory_space_private_user: - email: Имейл - name: Име scope: code: Код name: Име @@ -133,10 +130,6 @@ bg: attributes: official_img_footer: allowed_file_content_types: Невалиден файл на изображението - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Неправилен файл за импортиране, моля, прочетете внимателно инструкциите и се уверете, че файлът е UTF-8 кодиран. user_group_csv_verification: attributes: file: @@ -174,8 +167,6 @@ bg: import: Импортиране newsletter: new: Нов бюлетин - participatory_space_private_user: - new: Нов частен потребител на пространство за участие per_page: За страница send_me_a_test_email: Изпратете ми тестов e-mail share: Сподели @@ -394,17 +385,6 @@ bg: values: 'false': Официализирано 'true': Неофициализирано - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Поканата е приета - values: - 'false': Не се приема - 'true': Прието - user_invitation_sent_at_not_null: - label: Поканата е изпратена - values: - 'false': Не е изпратено - 'true': Изпратено private_space_eq: label: Частни values: @@ -526,6 +506,10 @@ bg: explanation: Управляваните участници могат да бъдат повишавани до стандартни участници. Това означава, че ще бъдат поканени в приложението и повече няма да можете да ги управлявате. Поканеният участник ще получи имейл, за да приеме поканата Ви. new_managed_user_promotion: Ново повишение на управляван участник promote: Повишаване + members_csv_imports: + new: + csv_upload: + title: Качете своя CSV файл menu: admin_log: Регистър на дейността на администраторите admins: Администратори @@ -588,8 +572,6 @@ bg: sent_to: Изпратено до subject: Относно name: Бюлетин - participatory_space_private_user: - name: Пространство за участие на частен участник scope: fields: name: Име @@ -784,39 +766,6 @@ bg: form: add: Добави в листа с позволени title: Лист с позволени външни домейни - participatory_space_private_users: - create: - error: Възникна проблем при добавянето на частен участник за това пространство за участие. - success: Достъпът на частния участник до пространството за участие беше създаден успешно. - destroy: - error: Възникна проблем при изтриването на частен участник за това пространство за участие. - success: Достъпът на частния участник до пространството за участие беше премахнат успешно. - index: - import_via_csv: Импортиране чрез CSV - title: Пространство за участие на частен участник - new: - create: Създаване - title: Ново пространство за участие на частен участник. - participatory_space_private_users_csv_imports: - create: - invalid: Възникна проблем при четенето на CSV файла. Моля, уверете се, че сте следвали инструкциите. - success: Файлът във формат CSV беше качен успешно; изпращаме имейл с покана на участниците. Това може да отнеме известно време. - new: - csv_upload: - title: Качете своя файл във формат CSV - destroy: - button: Изтрийте всички частни участници - confirm: Сигурни ли сте, че искате да изтриете всички частни участници? Това действие е необратимо, няма да можете да ги възстановите. - empty: Нямате частни участници. - explanation: Имате %{count} частни участници. - title: Изтрийте частни участници - example_file: 'Примерен файл:' - explanation: Качете своя файл във формат CSV. Трябва да съдържа две колони — електронната поща в първата колона и името в последната колона от файла (електронна поща, име) — за потребителите, които искате да добавите в пространството за участие, без заглавки. - explanation_example: | - john.doe@example.org%{csv_col_sep}John Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - title: Импортирайте частни участници чрез CSV - upload: Качване reminders: create: error: Възникна проблем при създаването на напомняния. diff --git a/decidim-admin/config/locales/bs-BA.yml b/decidim-admin/config/locales/bs-BA.yml index cdba8743d5763..83991e50e9b21 100644 --- a/decidim-admin/config/locales/bs-BA.yml +++ b/decidim-admin/config/locales/bs-BA.yml @@ -324,8 +324,6 @@ bs: sent_to: Poslato subject: Naslov name: Bilten - participatory_space_private_user: - name: Privatni učesnik prostora za diskusiju scope: fields: name: Ime diff --git a/decidim-admin/config/locales/ca-IT.yml b/decidim-admin/config/locales/ca-IT.yml index 214daf8c77306..8a6f091817dda 100644 --- a/decidim-admin/config/locales/ca-IT.yml +++ b/decidim-admin/config/locales/ca-IT.yml @@ -33,6 +33,11 @@ ca-IT: help_section: content: Contingut id: ID + member: + email: Correu electrònic + name: Nom + member_csv_import: + file: Arxiu newsletter: body: Cos send_to_all_users: Envia a totes les participants @@ -91,11 +96,6 @@ ca-IT: welcome_notification_body: Cos de la notificació de benvinguda welcome_notification_subject: Assumpte de la notificació de benvinguda youtube_handler: Nom d'usuària de YouTube - participatory_space_private_user: - email: Correu electrònic - name: Nom - participatory_space_private_user_csv_import: - file: Arxiu scope: code: Codi name: Nom @@ -125,10 +125,17 @@ 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: models: + member_csv_import: + attributes: + file: + malformed: Arxiu d'importació mal formatat, si us plau, llegeix les instruccions curosament i assegura't que l'arxiu està codificat en UTF-8. newsletter: attributes: base: @@ -137,10 +144,6 @@ ca-IT: attributes: official_img_footer: allowed_file_content_types: Fitxer d'imatge no vàlid - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Arxiu d'importació mal formatat, si us plau, llegeix les instruccions curosament i assegura't que l'arxiu està codificat en UTF-8. user_group_csv_verification: attributes: file: @@ -188,12 +191,12 @@ ca-IT: export: Exporta export-selection: Exportar selecció import: Importar + member: + new: Nova membre menu_hidden: Amaga del menú moderate: Gestionar les moderacions newsletter: new: Nou butlletí - participatory_space_private_user: - new: Nou usuari privat de l'espai participatiu per_page: Per pàgina permissions: Gestionar els permisos restore: Restaurar @@ -415,6 +418,17 @@ ca-IT: values: 'false': 'No' 'true': 'Sí' + members: + user_invitation_accepted_at_not_null: + label: Invitació acceptada + values: + 'false': No acceptada + 'true': Acceptada + user_invitation_sent_at_not_null: + label: Invitació enviada + values: + 'false': No enviada + 'true': Enviada moderated_users: reports_reason_eq: label: Motiu de l'informe @@ -430,17 +444,6 @@ ca-IT: values: 'false': Oficialitzada 'true': No oficialitzada - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Invitació acceptada - values: - 'false': No acceptada - 'true': Acceptada - user_invitation_sent_at_not_null: - label: S'ha enviat la invitació - values: - 'false': No enviada - 'true': Enviada private_space_eq: label: Privat values: @@ -573,6 +576,53 @@ ca-IT: explanation: Les participants gestionades es poden promocionar a participants estàndard. Significa que seran convidades al sistema i no podràs tornar a administrar-les. La participant convidada rebrà un correu electrònic per acceptar la vostra invitació. new_managed_user_promotion: Nova promoció de participant promote: Promocionar + members: + create: + error: S'ha produït un error en actualitzar una administradora per a aquest procés participatiu. + success: Accés com a membre creat correctament. + destroy: + error: S'ha produït un error en eliminar una participant participada d'aquest espai participació. + success: Accés com a membre eliminat correctament. + edit: + title: Editar membre + update: Actualizar + index: + import_via_csv: Importar des de CSV + publish_all: Publicar totes + title: Membre + unpublish_all: Despublicar totes + new: + create: Crear + title: Nova membre + publish_all: + error: S'ha produït un error en publicar totes les membres d'aquest espai de participació. + success: S'han publicat correctament totes les membres d'aquest espai de participació + unpublish_all: + error: S'ha produït un error en despublicar totes les membres d'aquest espai de participació. + success: S'han despublicat correctament totes les membres d'aquest espai de participació + update: + error: S'ha produït un error en actualitzar la membre per a aquest espai de participació. + success: Membre actualitzada correctament + members_csv_imports: + create: + invalid: S'ha produït un error llegint el fitxer CSV. Si us plau, assegura't d'haver seguit les instruccions. + success: L'arxiu CSV s'ha carregat amb èxit, estem enviant un correu d'invitació a les participants. Això pot trigar una estona. + new: + csv_upload: + title: Puja el fitxer CSV + destroy: + button: Esborrar totes les membres + confirm: Segur que vols esborrar totes les membres? Aquesta acció no es pot desfer, no podràs recuperar-les. + empty: No hi ha membres. + explanation: Hi ha %{count} membres. + title: Elimina membres + example_file: 'Fitxer d''exemple:' + explanation: 'Carrega el teu arxiu CSV. Ha de tenir dues columnes amb l''adreça de correu electrònic a la primera columna i el nom a la segona de les participants que vulguis afegir a l''espai de participació, sense capçaleres. Evita emprar caràcters invàlids com `<>?%&^*#@()[]=+:;"{}\|` al nom d''usuària.' + explanation_example: | + john.doe@example.org%{csv_col_sep}John Doe + jane.doe@example.org%{csv_col_sep}Jane Doe + title: Importar membres via CSV + upload: Carregar menu: admin_log: Registre d'activitat d'administració admins: Administradores @@ -631,6 +681,8 @@ ca-IT: reason: Raó started_at: Va començar el user: Participant + member: + name: Membre newsletter: fields: created_at: Data de creació @@ -639,8 +691,6 @@ ca-IT: sent_to: Enviat a subject: Assumpte name: Butlletí - participatory_space_private_user: - name: Participant d'espai de participació privat scope: fields: name: Nom @@ -915,53 +965,6 @@ ca-IT: form: add: Afegeix a la llista de permesos title: Llistat de dominis externs permesos - participatory_space_private_users: - create: - error: S'ha produït un error en afegir una participant privada a aquest espai de participació. - success: L'accés de la participant a l'espai de participació privat s'ha creat correctament. - destroy: - error: S'ha produït un error en eliminar una participant participada d'aquest espai participatiu. - success: L'accés de la participant a l'espai de participació privat s'ha eliminat correctament. - edit: - title: Editar participant privada de l'espai de participació. - update: Actualitzar - index: - import_via_csv: Importar des de CSV - publish_all: Publicar tot - title: Participant de l'espai participatiu privat - unpublish_all: Despublicar tot - new: - create: Crear - title: Nova participant de l'espai privat. - publish_all: - error: S'ha produït un error en publicar totes les participants privades d'aquest espai de participació. - success: S'han publicat correctament totes les participants privades d'aquest espai de participació - unpublish_all: - error: S'ha produït un error en despublicar totes les participants privades d'aquest espai de participació. - success: S'han despublicat correctament totes les participants privades d'aquest espai de participació - update: - error: S'ha produït un error en actualitzar una participant participada d'aquest espai de participació. - success: S'ha actualitzat correctament l'espai de participació privat - participatory_space_private_users_csv_imports: - create: - invalid: S'ha produït un error llegint el fitxer CSV. Si us plau, assegura't d'haver seguit les instruccions. - success: L'arxiu CSV s'ha carregat amb èxit, estem enviant un correu d'invitació a les participants. Això pot trigar una estona. - new: - csv_upload: - title: Puja el teu fitxer CSV - destroy: - button: Esborrar totes les participants privades - confirm: Segur que vols esborrar totes les participants privades? Aquesta acció no es pot desfer, no podràs recuperar-les. - empty: No hi ha participants privades. - explanation: Hi ha %{count} participant/s privada/ades. - title: Esborrar les participants privades - example_file: 'Fitxer d''exemple:' - explanation: 'Carrega el teu arxiu CSV. Ha de tenir dues columnes amb l''adreça de correu electrònic a la primera columna i el nom a la segona de les participants que vulguis afegir a l''espai de participació, sense capçaleres. Evita emprar caràcters invàlids com `<>?%&^*#@()[]=+:;"{}\|` al nom d''usuària.' - explanation_example: | - john.doe@example.org%{csv_col_sep}John Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - title: Importar participant privades via CSV - upload: Carrega reminders: create: error: Hi ha hagut un problema en crear els recordatoris. diff --git a/decidim-admin/config/locales/ca.yml b/decidim-admin/config/locales/ca.yml index 660ecbdc5bfcd..911639283ebbc 100644 --- a/decidim-admin/config/locales/ca.yml +++ b/decidim-admin/config/locales/ca.yml @@ -33,6 +33,11 @@ ca: help_section: content: Contingut id: ID + member: + email: Correu electrònic + name: Nom + member_csv_import: + file: Arxiu newsletter: body: Cos send_to_all_users: Envia a totes les participants @@ -91,11 +96,6 @@ ca: welcome_notification_body: Cos de la notificació de benvinguda welcome_notification_subject: Assumpte de la notificació de benvinguda youtube_handler: Nom d'usuària de YouTube - participatory_space_private_user: - email: Correu electrònic - name: Nom - participatory_space_private_user_csv_import: - file: Arxiu scope: code: Codi name: Nom @@ -125,10 +125,17 @@ 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: models: + member_csv_import: + attributes: + file: + malformed: Arxiu d'importació mal formatat, si us plau, llegeix les instruccions curosament i assegura't que l'arxiu està codificat en UTF-8. newsletter: attributes: base: @@ -137,10 +144,6 @@ ca: attributes: official_img_footer: allowed_file_content_types: Fitxer d'imatge no vàlid - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Arxiu d'importació mal formatat, si us plau, llegeix les instruccions curosament i assegura't que l'arxiu està codificat en UTF-8. user_group_csv_verification: attributes: file: @@ -188,12 +191,12 @@ ca: export: Exporta export-selection: Exportar selecció import: Importar + member: + new: Nova membre menu_hidden: Amaga del menú moderate: Gestionar les moderacions newsletter: new: Nou butlletí - participatory_space_private_user: - new: Nou usuari privat de l'espai participatiu per_page: Per pàgina permissions: Gestionar els permisos restore: Restaurar @@ -415,6 +418,17 @@ ca: values: 'false': 'No' 'true': 'Sí' + members: + user_invitation_accepted_at_not_null: + label: Invitació acceptada + values: + 'false': No acceptada + 'true': Acceptada + user_invitation_sent_at_not_null: + label: Invitació enviada + values: + 'false': No enviada + 'true': Enviada moderated_users: reports_reason_eq: label: Motiu de l'informe @@ -430,17 +444,6 @@ ca: values: 'false': Oficialitzada 'true': No oficialitzada - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Invitació acceptada - values: - 'false': No acceptada - 'true': Acceptada - user_invitation_sent_at_not_null: - label: S'ha enviat la invitació - values: - 'false': No enviada - 'true': Enviada private_space_eq: label: Privat values: @@ -573,6 +576,53 @@ ca: explanation: Les participants gestionades es poden promocionar a participants estàndard. Significa que seran convidades al sistema i no podràs tornar a administrar-les. La participant convidada rebrà un correu electrònic per acceptar la vostra invitació. new_managed_user_promotion: Nova promoció de participant promote: Promocionar + members: + create: + error: S'ha produït un error en actualitzar una administradora per a aquest procés participatiu. + success: Accés com a membre creat correctament. + destroy: + error: S'ha produït un error en eliminar una participant participada d'aquest espai participació. + success: Accés com a membre eliminat correctament. + edit: + title: Editar membre + update: Actualizar + index: + import_via_csv: Importar des de CSV + publish_all: Publicar totes + title: Membre + unpublish_all: Despublicar totes + new: + create: Crear + title: Nova membre + publish_all: + error: S'ha produït un error en publicar totes les membres d'aquest espai de participació. + success: S'han publicat correctament totes les membres d'aquest espai de participació + unpublish_all: + error: S'ha produït un error en despublicar totes les membres d'aquest espai de participació. + success: S'han despublicat correctament totes les membres d'aquest espai de participació + update: + error: S'ha produït un error en actualitzar la membre per a aquest espai de participació. + success: Membre actualitzada correctament + members_csv_imports: + create: + invalid: S'ha produït un error llegint el fitxer CSV. Si us plau, assegura't d'haver seguit les instruccions. + success: L'arxiu CSV s'ha carregat amb èxit, estem enviant un correu d'invitació a les participants. Això pot trigar una estona. + new: + csv_upload: + title: Puja el fitxer CSV + destroy: + button: Esborrar totes les membres + confirm: Segur que vols esborrar totes les membres? Aquesta acció no es pot desfer, no podràs recuperar-les. + empty: No hi ha membres. + explanation: Hi ha %{count} membres. + title: Elimina membres + example_file: 'Fitxer d''exemple:' + explanation: 'Carrega el teu arxiu CSV. Ha de tenir dues columnes amb l''adreça de correu electrònic a la primera columna i el nom a la segona de les participants que vulguis afegir a l''espai de participació, sense capçaleres. Evita emprar caràcters invàlids com `<>?%&^*#@()[]=+:;"{}\|` al nom d''usuària.' + explanation_example: | + john.doe@example.org%{csv_col_sep}John Doe + jane.doe@example.org%{csv_col_sep}Jane Doe + title: Importar membres via CSV + upload: Carregar menu: admin_log: Registre d'activitat d'administració admins: Administradores @@ -631,6 +681,8 @@ ca: reason: Raó started_at: Va començar el user: Participant + member: + name: Membre newsletter: fields: created_at: Data de creació @@ -639,8 +691,6 @@ ca: sent_to: Enviat a subject: Assumpte name: Butlletí - participatory_space_private_user: - name: Participant d'espai de participació privat scope: fields: name: Nom @@ -915,53 +965,6 @@ ca: form: add: Afegeix a la llista de permesos title: Llistat de dominis externs permesos - participatory_space_private_users: - create: - error: S'ha produït un error en afegir una participant privada a aquest espai de participació. - success: L'accés de la participant a l'espai de participació privat s'ha creat correctament. - destroy: - error: S'ha produït un error en eliminar una participant participada d'aquest espai participatiu. - success: L'accés de la participant a l'espai de participació privat s'ha eliminat correctament. - edit: - title: Editar participant privada de l'espai de participació. - update: Actualitzar - index: - import_via_csv: Importar des de CSV - publish_all: Publicar tot - title: Participant de l'espai participatiu privat - unpublish_all: Despublicar tot - new: - create: Crear - title: Nova participant de l'espai privat. - publish_all: - error: S'ha produït un error en publicar totes les participants privades d'aquest espai de participació. - success: S'han publicat correctament totes les participants privades d'aquest espai de participació - unpublish_all: - error: S'ha produït un error en despublicar totes les participants privades d'aquest espai de participació. - success: S'han despublicat correctament totes les participants privades d'aquest espai de participació - update: - error: S'ha produït un error en actualitzar una participant participada d'aquest espai de participació. - success: S'ha actualitzat correctament l'espai de participació privat - participatory_space_private_users_csv_imports: - create: - invalid: S'ha produït un error llegint el fitxer CSV. Si us plau, assegura't d'haver seguit les instruccions. - success: L'arxiu CSV s'ha carregat amb èxit, estem enviant un correu d'invitació a les participants. Això pot trigar una estona. - new: - csv_upload: - title: Puja el teu fitxer CSV - destroy: - button: Esborrar totes les participants privades - confirm: Segur que vols esborrar totes les participants privades? Aquesta acció no es pot desfer, no podràs recuperar-les. - empty: No hi ha participants privades. - explanation: Hi ha %{count} participant/s privada/ades. - title: Esborrar les participants privades - example_file: 'Fitxer d''exemple:' - explanation: 'Carrega el teu arxiu CSV. Ha de tenir dues columnes amb l''adreça de correu electrònic a la primera columna i el nom a la segona de les participants que vulguis afegir a l''espai de participació, sense capçaleres. Evita emprar caràcters invàlids com `<>?%&^*#@()[]=+:;"{}\|` al nom d''usuària.' - explanation_example: | - john.doe@example.org%{csv_col_sep}John Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - title: Importar participant privades via CSV - upload: Carrega reminders: create: error: Hi ha hagut un problema en crear els recordatoris. diff --git a/decidim-admin/config/locales/cs.yml b/decidim-admin/config/locales/cs.yml index a5966825647b8..42b53cba8a5db 100644 --- a/decidim-admin/config/locales/cs.yml +++ b/decidim-admin/config/locales/cs.yml @@ -33,8 +33,14 @@ cs: help_section: content: Obsah id: ID + member: + email: E-mail + name: Název + member_csv_import: + file: Soubor 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 @@ -90,11 +96,6 @@ cs: welcome_notification_body: Tělo uvítacího oznámení welcome_notification_subject: Předmět uvítacího oznámení youtube_handler: YouTube handler - participatory_space_private_user: - email: E-mail - name: Název - participatory_space_private_user_csv_import: - file: Soubor scope: code: Kód name: Název @@ -124,10 +125,17 @@ cs: show_in_footer: Zobrazit v zápatí title: Titul weight: Pozice v řazení + taxonomy: + item_name: Název položky + parent_id: Nadřazený user_group_csv_verification: file: Soubor errors: models: + member_csv_import: + attributes: + file: + malformed: Chybný importní soubor, přečtěte si pozorně pokyny a ujistěte se, že soubor je kódovaný UTF-8. newsletter: attributes: base: @@ -136,10 +144,6 @@ cs: attributes: official_img_footer: allowed_file_content_types: Neplatný soubor s obrázkem - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Chybný importní soubor, přečtěte si pozorně pokyny a ujistěte se, že soubor je kódovaný UTF-8. user_group_csv_verification: attributes: file: @@ -187,12 +191,12 @@ cs: export: Exportovat export-selection: Exportovat výběr import: Importovat + member: + new: Nový člen menu_hidden: Skrýt v menu moderate: Spravovat moderace newsletter: new: Nový zpravodaj - participatory_space_private_user: - new: Nový soukromý uživatel participačního prostoru per_page: Na stránku permissions: Správa oprávnění restore: Obnovit @@ -292,11 +296,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 +398,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}" @@ -413,6 +420,17 @@ cs: values: 'false': 'Ne' 'true': 'Ano' + members: + user_invitation_accepted_at_not_null: + label: Pozvánka přijata + values: + 'false': Nepřijato + 'true': Přijato + user_invitation_sent_at_not_null: + label: Pozvánka odeslána + values: + 'false': Neodesláno + 'true': Odesláno moderated_users: reports_reason_eq: label: Důvod hlášení @@ -428,17 +446,6 @@ cs: values: 'false': Ověřeno 'true': Neověřeno - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Pozvánka přijata - values: - 'false': Nepřijato - 'true': Přijato - user_invitation_sent_at_not_null: - label: Pozvánka odeslána - values: - 'false': Neodesláno - 'true': Odesláno private_space_eq: label: Soukromé values: @@ -454,6 +461,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 +483,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 +570,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. @@ -574,6 +586,37 @@ cs: explanation: Spravovaní uživatelé mohou být povýšeni na standardní uživatele. Znamená to, že budou pozváni do aplikace a nebudete je moci znovu předstírat. Pozvaný uživatel obdrží e-mail, ve kterém přijme vaši pozvánku. new_managed_user_promotion: Nová spravovaná podpora pro uživatele promote: Podporovat + members: + create: + error: Při přidávání člena do tohoto participačního prostoru došlo k chybě. + success: Přístup člena byl úspěšně vytvořen. + destroy: + error: Při mazání člena pro tento participační prostor došlo k chybě. + edit: + title: Upravit člena + update: Aktualizovat + index: + import_via_csv: Importovat přes CSV + publish_all: Publikovat vše + title: Člen + unpublish_all: Zrušit publikování všech + new: + create: Vytvořit + title: Nový člen + publish_all: + error: Při publikování všech členů tohoto participačního prostoru došlo k chybě. + members_csv_imports: + new: + csv_upload: + title: Nahrát soubor CSV + destroy: + button: Odstranit všechny členy + confirm: Opravdu chcete odstranit všechny členy? Tuto akci nelze vrátit zpět, nebudete ji moci obnovit. + empty: Nemáte žádné členy. + title: Smazat členy + example_file: 'Příklad souboru:' + title: Importovat členy přes CSV + upload: Nahrát menu: admin_log: Protokol aktivity správce admins: Administrátoři @@ -632,6 +675,8 @@ cs: reason: Důvod started_at: Začalo v user: Účastník + member: + name: Člen newsletter: fields: created_at: Vytvořeno v @@ -640,8 +685,6 @@ cs: sent_to: Odeslána subject: Předmět name: Zpravodaj - participatory_space_private_user: - name: Participační prostor soukromého účastníka scope: fields: name: Název @@ -690,6 +733,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 +760,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 @@ -914,53 +959,6 @@ cs: form: add: Přidat na Seznam povolených title: Seznam povolených externích domén - participatory_space_private_users: - create: - error: Při přidávání soukromého uživatele pro tento participační prostor došlo k chybě. - success: Přístup soukromého účastníka do participativního prostoru byl úspěšně vytvořen. - destroy: - error: Při odstraňování soukromého uživatele tohoto participačního prostoru došlo k chybě. - success: Přístup soukromého účastníka do participativního prostoru byl úspěšně zrušen. - edit: - title: Upravit participační prostor soukromého účastníka. - update: Aktualizovat - index: - import_via_csv: Importovat přes CSV - publish_all: Publikovat vše - title: Participační prostor soukromého účastníka - unpublish_all: Zrušit publikování všech - new: - create: Vytvořit - title: Nový participační prostor soukromého účastníka. - publish_all: - error: U tohoto participativního prostoru byl problém se zveřejněním všech soukromých účastníků. - success: Úspěšné zveřejnění všech soukromých účastníků tohoto participativního prostoru - unpublish_all: - error: U tohoto participativního prostoru byl problém se zrušením zveřejnění všech soukromých účastníků. - success: Úspěšně zneveřejněni všichni soukromí účastníci tohoto participativního prostoru - update: - error: U tohoto participativního prostoru se vyskytl problém s aktualizací soukromého účastníka. - success: Soukromý účastník participativního prostoru úspěšně aktualizován - participatory_space_private_users_csv_imports: - create: - invalid: Při čtení souboru CSV se vyskytl problém. Ujistěte se, že jste dodrželi pokyny. - success: CSV soubor byl úspěšně nahrán, posíláme účastníkům e-mail s pozvánkou. To může chvíli trvat. - new: - csv_upload: - title: Nahrát soubor CSV - destroy: - button: Odstranit všechny soukromé účastníky - confirm: Jste si jisti, že chcete odstranit všechny soukromé účastníky? Tuto akci nelze vrátit zpět, nebudete je moci obnovit. - empty: Nemáte žádné soukromé účastníky. - explanation: Máte %{count} soukromých účastníků. - title: Odstranit soukromé účastníky - example_file: 'Příklad souboru:' - explanation: 'Nahrajte soubor CSV. Musí mít dva sloupce s e-mailem v prvním sloupci souboru a jméno v posledním sloupci souboru uživatelů, které chcete přidat do participačního prostoru, bez hlaviček. Vyhněte se používání neplatných znaků jako `<>?%&^*#@()[]=+:;"{}\|` v uživatelském jméně.' - explanation_example: | - john.doe@example.org%{csv_col_sep}John Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - title: Importovat soukromé účastníky přes CSV - upload: Nahrát reminders: create: error: Při vytváření připomenutí došlo k chybě. @@ -1242,6 +1240,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 +1271,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 +1286,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..5970fd6cb3893 100644 --- a/decidim-admin/config/locales/de.yml +++ b/decidim-admin/config/locales/de.yml @@ -33,8 +33,14 @@ de: help_section: content: Inhalt id: ID + member: + email: E-Mail + name: Name + member_csv_import: + file: Datei 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 @@ -90,11 +96,6 @@ de: welcome_notification_body: Text der Willkommens-Benachrichtigung welcome_notification_subject: Betreff der Willkommens-Benachrichtigung youtube_handler: YouTube-Handler - participatory_space_private_user: - email: E-Mail - name: Name - participatory_space_private_user_csv_import: - file: Datei scope: code: Code name: Name @@ -124,10 +125,17 @@ de: show_in_footer: In der Fußzeile anzeigen title: Titel weight: Reihenfolge + taxonomy: + item_name: Elementbezeichnung + parent_id: Übergeordnetes Element user_group_csv_verification: file: Datei errors: models: + member_csv_import: + attributes: + file: + malformed: Fehlerhafte Importdatei. Bitte lesen Sie die Anweisungen sorgfältig durch und stellen Sie sicher, dass die Datei UTF-8 kodiert ist. newsletter: attributes: base: @@ -136,10 +144,6 @@ de: attributes: official_img_footer: allowed_file_content_types: Ungültige Bilddatei - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Fehlerhafte Importdatei. Bitte lesen Sie die Anweisungen sorgfältig durch und stellen Sie sicher, dass die Datei UTF-8 kodiert ist. user_group_csv_verification: attributes: file: @@ -187,12 +191,12 @@ de: export: Exportieren export-selection: Auswahl exportieren import: Importieren + member: + new: Neues Mitglied menu_hidden: Im Menü ausblenden moderate: Moderationen verwalten newsletter: new: Neuer Newsletter - participatory_space_private_user: - new: Neuer privater Benutzer per_page: Pro Seite permissions: Berechtigungen verwalten restore: Wiederherstellen @@ -292,11 +296,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 +396,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}" @@ -411,6 +418,17 @@ de: values: 'false': 'Nein' 'true': 'Ja' + members: + user_invitation_accepted_at_not_null: + label: Einladung akzeptiert + values: + 'false': Nicht akzeptiert + 'true': Akzeptiert + user_invitation_sent_at_not_null: + label: Einladung versendet + values: + 'false': Nicht versendet + 'true': Versendet moderated_users: reports_reason_eq: label: Grund der Meldung @@ -426,17 +444,6 @@ de: values: 'false': Offizialisiert 'true': Nicht offiziell - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Einladung akzeptiert - values: - 'false': Nicht akzeptiert - 'true': Akzeptiert - user_invitation_sent_at_not_null: - label: Einladung versendet - values: - 'false': Nicht versendet - 'true': Versendet private_space_eq: label: Privat values: @@ -474,6 +481,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: @@ -568,6 +576,53 @@ de: explanation: Verwaltete Benutzer können zu Standardbenutzern heraufgestuft werden. Das bedeutet, dass sie zu der Anwendung eingeladen werden und nicht in der Lage sind, sie erneut zu repräsentieren. Der eingeladene Benutzer erhält eine E-Mail, um Ihre Einladung anzunehmen. new_managed_user_promotion: Neue verwaltete Benutzerwerbung promote: Fördern + members: + create: + error: Beim Hinzufügen eines Mitglieds zu diesem partizipativen Bereich ist ein Fehler aufgetreten. + success: Zugriff für das Mitglied erfolgreich erstellt. + destroy: + error: Beim Entfernen eines Mitglieds zu diesem partizipativen Bereich ist ein Fehler aufgetreten. + success: Zugriff für das Mitglied erfolgreich gelöscht. + edit: + title: Mitglied bearbeiten + update: Aktualisieren + index: + import_via_csv: Aus CSV importieren + publish_all: Alle veröffentlichen + title: Mitglied + unpublish_all: Alle Veröffentlichungen aufheben + new: + create: Erstellen + title: Neues Mitglied + publish_all: + error: Beim Veröffentlichen der Mitglieder in diesem partizipativen Bereich ist ein Fehler aufgetreten. + success: Alle Mitglieder für diesen partizipativen Bereich erfolgreich veröffentlicht + unpublish_all: + error: Beim Aufheben der Veröffentlichen der Mitglieder in diesem partizipativen Bereich ist ein Fehler aufgetreten. + success: Die Veröffentlichung der Mitglieder für diesen partizipativen Bereich wurde erfolgreich aufgehoben + update: + error: Beim Aktualisieren eines Mitglieds in diesem partizipativen Bereich ist ein Fehler aufgetreten. + success: Mitglied wurde erfolgreich aktualisiert + members_csv_imports: + create: + invalid: Beim Lesen der CSV-Datei ist ein Problem aufgetreten. Bitte stellen Sie sicher, dass Sie den Anweisungen korrekt gefolgt sind. + success: CSV-Datei wurde erfolgreich hochgeladen. Wir senden eine Einladungs-E-Mail an die Teilnehmenden. Dies kann eine Weile dauern. + new: + csv_upload: + title: Laden Sie Ihre CSV-Datei hoch + destroy: + button: Alle Mitglieder löschen + confirm: Sind Sie sicher, dass Sie alle Mitglieder entfernen möchten? Diese Aktion kann nicht rückgängig gemacht werden. Sie werden sie nicht wiederherstellen können. + empty: Sie haben noch keine Mitglieder. + explanation: Sie haben %{count} Mitglieder. + title: Mitglieder löschen + example_file: 'Beispieldatei:' + explanation: 'Laden Sie Ihre CSV-Datei mit den Teilnehmenden, die Sie zum Prozess hinzufügen möchten, hoch. Sie muss zwei Spalten (ohne Kopfzeile) haben, mit der E-Mail-Adresse in der ersten Spalte und dem Namen in der zweiten Spalte der Datei. Verwenden Sie keine ungültige Zeichen wie `<>?%&^*#@()[]=+:;"{}\|` im Namen der Teilnehmenden.' + explanation_example: | + john.doe@example.org%{csv_col_sep}John Doe + jane.doe@example.org%{csv_col_sep}Jane Doe + title: Mitglieder via CSV importieren + upload: Hochladen menu: admin_log: Admin-Aktivitätsprotokoll admins: Admins @@ -626,6 +681,8 @@ de: reason: Grund started_at: Fing an bei user: Benutzer + member: + name: Mitglied newsletter: fields: created_at: Erstellt am @@ -634,8 +691,6 @@ de: sent_to: Gesendet an subject: Gegenstand name: Newsletter - participatory_space_private_user: - name: Participatory Space privater Benutzer scope: fields: name: Name @@ -910,53 +965,6 @@ de: form: add: Zur erlaubten Liste hinzufügen title: Liste erlaubter externer Domains - participatory_space_private_users: - create: - error: Beim Hinzufügen eines privaten Benutzers für diesen partizipativen Bereich ist ein Fehler aufgetreten. - success: Ein privater Zugriff für diesen Beteiligungsbereich wurde erfolgreich für das Konto erstellt. - destroy: - error: Beim Löschen eines privaten Benutzers für diesen partizipativen Bereich ist ein Fehler aufgetreten. - success: Privater Zugriff zum partizipativen Raum erfolgreich gelöscht. - edit: - title: Privates Mitglied des partiziaptiven Bereichs bearbeiten. - update: Aktualisieren - index: - import_via_csv: Aus CSV-Datein importieren - publish_all: Alle veröffentlichen - title: Participatory Space privater Benutzer - unpublish_all: Alle Veröffentlichungen aufheben - new: - create: Erstellen - title: Neuer privater Benutzer des Participatory Space. - publish_all: - error: Es gab ein Problem bei der Veröffentlichung aller privaten Teilnehmenden für diesen partizipativen Bereich. - success: Alle privaten Teilnehmenden erfolgreich für diesen partizipativen Bereich veröffentlicht - unpublish_all: - error: Es gab ein Problem beim Entfernen aller privaten Teilnehmenden für diesen partizipativen Bereich. - success: Alle privaten Teilnehmenden für diesen teilnehmenden Raum erfolgreich zurückgezogen - update: - error: Beim Aktualisieren des privaten Teilnehmenden für diesen Beteiligungsbereich ist ein Problem aufgetreten. - success: Privater Teilnehmende für diesen partizipativen Raum erfolgreich erstellt - participatory_space_private_users_csv_imports: - create: - invalid: Beim Lesen der CSV-Datei ist ein Problem aufgetreten. Bitte stellen Sie sicher, dass Sie den Anweisungen korrekt gefolgt sind. - success: CSV-Datei wurde erfolgreich hochgeladen, wir senden eine Einladungs-E-Mail an die Teilnehmenden. Dies kann eine Weile dauern. - new: - csv_upload: - title: CSV-Datei hochladen - destroy: - button: Alle private Teilnehmende auswählen - confirm: Sind Sie sicher, dass Sie alle private Teilnehmende entfernen möchten? Diese Aktion kann nicht rückgängig gemacht werden. Sie werden sie nicht wiederherstellen können. - empty: Sie haben keine private Teilnehmende. - explanation: Sie haben %{count} private Teilnehmende. - title: Alle private Teilnehmende entfernen - example_file: 'Beispieldatei:' - explanation: 'Laden Sie Ihre CSV-Datei mit den Teilnehmenden, die Sie zum Prozess hinzufügen möchten, hoch. Sie muss zwei Spalten (ohne Kopfzeile) haben, mit der E-Mail-Adresse in der ersten Spalte und dem Namen in der zweiten Spalte der Datei. Verwenden Sie keine ungültige Zeichen wie `<>?%&^*#@()[]=+:;"{}\|` im Namen der Teilnehmenden.' - explanation_example: | - john.doe@example.org%{csv_col_sep}John Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - title: Private Teilnehmende via CSV importieren - upload: Hochladen reminders: create: error: Beim Erstellen dieser Erinnerungen ist ein Problem aufgetreten. @@ -967,7 +975,7 @@ de: submit: Senden resource_permissions: edit: - submit: einreichen + submit: Absenden title: Berechtigungen bearbeiten options_form: ephemeral_warning: Um diese Autorisierungsmethode zu aktivieren, müssen Sie alle bestehenden Autorisierungen ?%&^*#@()[]=+:;"{}\|` 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/eo.yml b/decidim-admin/config/locales/eo.yml index 72441adca2308..c7822411b652b 100644 --- a/decidim-admin/config/locales/eo.yml +++ b/decidim-admin/config/locales/eo.yml @@ -38,8 +38,6 @@ eo: impersonation_log: fields: ended_at: Finita je - participatory_space_private_user: - name: Privata partoprenanto newsletters: select_recipients_to_deliver: all_users_help: Sendu la informilon al ĉiuj konfirmitaj partoprenantoj. diff --git a/decidim-admin/config/locales/es-MX.yml b/decidim-admin/config/locales/es-MX.yml index bf9df930afd23..3d8948dffba26 100644 --- a/decidim-admin/config/locales/es-MX.yml +++ b/decidim-admin/config/locales/es-MX.yml @@ -33,6 +33,11 @@ es-MX: help_section: content: Contenido id: ID + member: + email: Correo electrónico + name: Nombre + member_csv_import: + file: Archivo newsletter: body: Cuerpo send_to_all_users: Enviar a todas las participantes @@ -91,11 +96,6 @@ es-MX: welcome_notification_body: Cuerpo del mensaje de notificación de bienvenida welcome_notification_subject: Asunto del mensaje de notificación de bienvenida youtube_handler: Nombre de YouTube - participatory_space_private_user: - email: Correo electrónico - name: Nombre - participatory_space_private_user_csv_import: - file: Archivo scope: code: Código name: Nombre @@ -125,10 +125,17 @@ 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: models: + member_csv_import: + attributes: + file: + malformed: Archivo de importación mal formateado, por favor lea las instrucciones cuidadosamente y asegúrese de que el archivo está codificado en UTF-8. newsletter: attributes: base: @@ -137,10 +144,6 @@ es-MX: attributes: official_img_footer: allowed_file_content_types: Archivo de imagen no válido - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Archivo de importación mal formateado, por favor lea las instrucciones cuidadosamente y asegúrese de que el archivo está codificado en UTF-8. user_group_csv_verification: attributes: file: @@ -188,12 +191,12 @@ es-MX: export: Exportar export-selection: Exportar selección import: Importar + member: + new: Nueva miembro menu_hidden: Ocultar del menú moderate: Gestionar las moderaciones newsletter: new: Nuevo boletín - participatory_space_private_user: - new: Nuevo usuario privado del espacio participativo per_page: Por página permissions: Gestionar los permisos restore: Restaurar @@ -415,6 +418,17 @@ es-MX: values: 'false': 'No' 'true': 'Sí' + members: + user_invitation_accepted_at_not_null: + label: Invitación aceptada + values: + 'false': No aceptada + 'true': Aceptada + user_invitation_sent_at_not_null: + label: Invitación enviada + values: + 'false': No enviada + 'true': Enviada moderated_users: reports_reason_eq: label: Motivo de la denuncia @@ -430,17 +444,6 @@ es-MX: values: 'false': Oficializado 'true': No oficializado - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Invitación aceptada - values: - 'false': No aceptada - 'true': Aceptada - user_invitation_sent_at_not_null: - label: Invitación enviada - values: - 'false': No enviada - 'true': Enviada private_space_eq: label: Privado values: @@ -573,6 +576,53 @@ es-MX: explanation: Los usuarios gestionados pueden ser promovidos a usuarios estándar. Esto significa que serán invitados a la aplicación y no será capaz de suplantarlos de nuevo. El usuario invitado recibirá un correo electrónico para aceptar su invitación. new_managed_user_promotion: Nueva promoción de usuarios gestionados promote: Promocionar + members: + create: + error: Se ha producido un error al agregar una participante privada a este espacio participativo. + success: Acceso como miembro creado correctamente. + destroy: + error: Se ha producido un error al eliminar a una participante privada para este espacio de participación. + success: Acceso como miembro eliminado correctamente. + edit: + title: Editar miembro + update: Actualizar + index: + import_via_csv: Importar vía csv + publish_all: Publicar todas + title: Miembro + unpublish_all: Despublicar todas + new: + create: Crear + title: Nueva miembro + publish_all: + error: Se ha producido un error al publicar todas las miembros de este espacio de participación. + success: Se han publicado correctamente todas las miembros de este espacio de participación + unpublish_all: + error: Se ha producido un error al despublicar todas las miembros de este espacio de participación. + success: Se han despublicado correctamente todas las miembros de este espacio de participación + update: + error: Se ha producido un error al actualizar la miembro para este espacio de participación. + success: Miembro actualizada correctamente + members_csv_imports: + create: + invalid: Hubo un problema al leer el archivo CSV. Por favor, asegúrate de haber seguido las instrucciones. + success: El archivo CSV se ha cargado correctamente, estamos enviando un correo de invitación a las participantes. Este proceso puede tardar un poco. + new: + csv_upload: + title: Sube tu archivo CSV + destroy: + button: Borrar todas las miembros + confirm: '¿Seguro que quieres eliminar todas las miembros? Esta acción no se puede deshacer, no podrás recuperarlas.' + empty: No hay miembros. + explanation: Hay %{count} miembros. + title: Eliminar miembros + example_file: 'Archivo de ejemplo:' + explanation: 'Sube tu archivo CSV. Debe tener dos columnas con correo electrónico en la primera columna del archivo y el nombre en la última columna del archivo de las participantes que deseas añadir al espacio participativo, sin encabezados. Evita usar caracteres no válidos como `<>?%&^*#@()[]=+:;"{}\|` en el nombre de usuaria.' + explanation_example: | + john.doe@example.org%{csv_col_sep}John Doe + jane.doe@example.org%{csv_col_sep}Jane Doe + title: Importar miembros mediante CSV + upload: Cargar menu: admin_log: Registro de actividad de administrador admins: Administradores @@ -631,6 +681,8 @@ es-MX: reason: Razón started_at: Empezó a las user: Usuario + member: + name: Miembro newsletter: fields: created_at: Fecha de creación @@ -639,8 +691,6 @@ es-MX: sent_to: Enviado a subject: Asunto name: Boletín - participatory_space_private_user: - name: Usuario privado de espacio participativo scope: fields: name: Nombre @@ -915,53 +965,6 @@ es-MX: form: add: Añadir a la lista de direcciones permitidas title: Lista de dominios externos permitidos - participatory_space_private_users: - create: - error: Hubo un error al agregar un usuario privado para este espacio participativo. - success: El acceso a usuarios privados de espacio participativo creado con éxito. - destroy: - error: Se ha producido un error al eliminar un usuario privado para este espacio participativo. - success: El acceso de usuarios privados del espacio participativo se eliminado con éxito. - edit: - title: Editar participante privada del espacio de participación. - update: Actualizar - index: - import_via_csv: Importar vía csv - publish_all: Publicar todo - title: Usuario privado de espacio participativo. - unpublish_all: Despublicar todo - new: - create: Crear - title: Nuevo usuario privado en el espacio participativo. - publish_all: - error: Se ha producido un error al publicar todas las participantes privadas de este espacio de participación. - success: Se han publicado correctamente todas las participantes privadas de este espacio de participación - unpublish_all: - error: Se ha producido un error al despublicar todas las participantes privadas de este espacio de participación. - success: Se han despublicado correctamente todas las participantes privadas de este espacio de participación - update: - error: Se ha producido un error actualizando una participante privada de este espacio de participación. - success: Se ha actualizado correctamente el espacio de participación privado - participatory_space_private_users_csv_imports: - create: - invalid: Hubo un problema al leer el archivo CSV. Por favor, asegúrate de haber seguido las instrucciones. - success: El archivo CSV se ha cargado correctamente, estamos enviando un correo de invitación a las participantes. Este proceso puede tardar un poco. - new: - csv_upload: - title: Sube tu archivo CSV - destroy: - button: Borrar todas las participantes privadas - confirm: '¿Seguro que quieres eliminar todas las participantes privadas? Esta acción no se puede deshacer, no podrás recuperarlas.' - empty: No hay participantes privadas. - explanation: Hay %{count} participante/s privada/s. - title: Borrar las participantes privadas - example_file: 'Fichero de ejemplo:' - explanation: 'Sube tu archivo CSV. Debe tener dos columnas con correo electrónico en la primera columna del archivo y el nombre en la última columna del archivo de las participantes que deseas añadir al espacio participativo, sin encabezados. Evita usar caracteres no válidos como `<>?%&^*#@()[]=+:;"{}\|` en el nombre de usuaria.' - explanation_example: | - john.doe@example.org%{csv_col_sep}John Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - title: Importar participantes privadas vía CSV - upload: Subir reminders: create: error: Se ha producido un error al crear los recordatorios. diff --git a/decidim-admin/config/locales/es-PY.yml b/decidim-admin/config/locales/es-PY.yml index a600e0380bba1..79c14842467f1 100644 --- a/decidim-admin/config/locales/es-PY.yml +++ b/decidim-admin/config/locales/es-PY.yml @@ -33,6 +33,11 @@ es-PY: help_section: content: Contenido id: ID + member: + email: Correo electrónico + name: Nombre + member_csv_import: + file: Archivo newsletter: body: Cuerpo send_to_all_users: Enviar a todas las participantes @@ -91,11 +96,6 @@ es-PY: welcome_notification_body: Cuerpo del mensaje de notificación de bienvenida welcome_notification_subject: Asunto del mensaje de notificación de bienvenida youtube_handler: Nombre de YouTube - participatory_space_private_user: - email: Correo electrónico - name: Nombre - participatory_space_private_user_csv_import: - file: Archivo scope: code: Código name: Nombre @@ -125,10 +125,17 @@ 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: models: + member_csv_import: + attributes: + file: + malformed: Archivo de importación mal formateado, por favor lea las instrucciones cuidadosamente y asegúrese de que el archivo está codificado en UTF-8. newsletter: attributes: base: @@ -137,10 +144,6 @@ es-PY: attributes: official_img_footer: allowed_file_content_types: Archivo de imagen no válido - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Archivo de importación mal formateado, por favor lea las instrucciones cuidadosamente y asegúrese de que el archivo está codificado en UTF-8. user_group_csv_verification: attributes: file: @@ -188,12 +191,12 @@ es-PY: export: Exportar export-selection: Exportar selección import: Importar + member: + new: Nueva miembro menu_hidden: Ocultar del menú moderate: Gestionar las moderaciones newsletter: new: Nuevo boletín - participatory_space_private_user: - new: Nuevo usuario privado del espacio participativo per_page: Por página permissions: Gestionar los permisos restore: Restaurar @@ -415,6 +418,17 @@ es-PY: values: 'false': 'No' 'true': 'Sí' + members: + user_invitation_accepted_at_not_null: + label: Invitación aceptada + values: + 'false': No aceptada + 'true': Aceptada + user_invitation_sent_at_not_null: + label: Invitación enviada + values: + 'false': No enviada + 'true': Enviada moderated_users: reports_reason_eq: label: Motivo de la denuncia @@ -430,17 +444,6 @@ es-PY: values: 'false': Oficializado 'true': No oficializado - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Invitación aceptada - values: - 'false': No aceptada - 'true': Aceptada - user_invitation_sent_at_not_null: - label: Invitación enviada - values: - 'false': No enviada - 'true': Enviada private_space_eq: label: Privado values: @@ -573,6 +576,53 @@ es-PY: explanation: Los usuarios gestionados pueden ser promovidos a usuarios estándar. Esto significa que serán invitados a la aplicación y no será capaz de suplantarlos de nuevo. El usuario invitado recibirá un correo electrónico para aceptar su invitación. new_managed_user_promotion: Nueva promoción de usuarios gestionados promote: Promocionar + members: + create: + error: Se ha producido un error al agregar una participante privada a este espacio participativo. + success: Acceso como miembro creado correctamente. + destroy: + error: Se ha producido un error al eliminar a una participante privada para este espacio de participación. + success: Acceso como miembro eliminado correctamente. + edit: + title: Editar miembro + update: Actualizar + index: + import_via_csv: Importar vía csv + publish_all: Publicar todas + title: Miembro + unpublish_all: Despublicar todas + new: + create: Crear + title: Nueva miembro + publish_all: + error: Se ha producido un error al publicar todas las miembros de este espacio de participación. + success: Se han publicado correctamente todas las miembros de este espacio de participación + unpublish_all: + error: Se ha producido un error al despublicar todas las miembros de este espacio de participación. + success: Se han despublicado correctamente todas las miembros de este espacio de participación + update: + error: Se ha producido un error al actualizar la miembro para este espacio de participación. + success: Miembro actualizada correctamente + members_csv_imports: + create: + invalid: Hubo un problema al leer el archivo CSV. Por favor, asegúrate de haber seguido las instrucciones. + success: El archivo CSV se ha cargado correctamente, estamos enviando un correo de invitación a las participantes. Este proceso puede tardar un poco. + new: + csv_upload: + title: Sube tu archivo CSV + destroy: + button: Borrar todas las miembros + confirm: '¿Seguro que quieres eliminar todas las miembros? Esta acción no se puede deshacer, no podrás recuperarlas.' + empty: No hay miembros. + explanation: Hay %{count} miembros. + title: Eliminar miembros + example_file: 'Archivo de ejemplo:' + explanation: 'Sube tu archivo CSV. Debe tener dos columnas con correo electrónico en la primera columna del archivo y el nombre en la última columna del archivo de las participantes que deseas añadir al espacio participativo, sin encabezados. Evita usar caracteres no válidos como `<>?%&^*#@()[]=+:;"{}\|` en el nombre de usuaria.' + explanation_example: | + john.doe@example.org%{csv_col_sep}John Doe + jane.doe@example.org%{csv_col_sep}Jane Doe + title: Importar miembros mediante CSV + upload: Cargar menu: admin_log: Registro de actividad de administrador admins: Administradores @@ -631,6 +681,8 @@ es-PY: reason: Razón started_at: Empezó a las user: Usuario + member: + name: Miembro newsletter: fields: created_at: Fecha de creación @@ -639,8 +691,6 @@ es-PY: sent_to: Enviado a subject: Asunto name: Boletín - participatory_space_private_user: - name: Usuario privado de espacio participativo scope: fields: name: Nombre @@ -915,53 +965,6 @@ es-PY: form: add: Añadir a la lista de direcciones permitidas title: Lista de dominios externos permitidos - participatory_space_private_users: - create: - error: Hubo un error al agregar un usuario privado para este espacio participativo. - success: El acceso a usuarios privados de espacio participativo creado con éxito. - destroy: - error: Se ha producido un error al eliminar un usuario privado para este espacio participativo. - success: El acceso de usuarios privados del espacio participativo se eliminado con éxito. - edit: - title: Editar participante privada del espacio de participación. - update: Actualizar - index: - import_via_csv: Importar vía csv - publish_all: Publicar todo - title: Usuario privado de espacio participativo. - unpublish_all: Despublicar todo - new: - create: Crear - title: Nuevo usuario privado en el espacio participativo. - publish_all: - error: Se ha producido un error al publicar todas las participantes privadas de este espacio de participación. - success: Se han publicado correctamente todas las participantes privadas de este espacio de participación - unpublish_all: - error: Se ha producido un error al despublicar todas las participantes privadas de este espacio de participación. - success: Se han despublicado correctamente todas las participantes privadas de este espacio de participación - update: - error: Se ha producido un error actualizando una participante privada de este espacio de participación. - success: Se ha actualizado correctamente el espacio de participación privado - participatory_space_private_users_csv_imports: - create: - invalid: Hubo un problema al leer el archivo CSV. Por favor, asegúrate de haber seguido las instrucciones. - success: El archivo CSV se ha cargado correctamente, estamos enviando un correo de invitación a las participantes. Este proceso puede tardar un poco. - new: - csv_upload: - title: Sube tu archivo CSV - destroy: - button: Borrar todas las participantes privadas - confirm: '¿Seguro que quieres eliminar todas las participantes privadas? Esta acción no se puede deshacer, no podrás recuperarlas.' - empty: No hay participantes privadas. - explanation: Hay %{count} participante/s privada/s. - title: Borrar las participantes privadas - example_file: 'Fichero de ejemplo:' - explanation: 'Sube tu archivo CSV. Debe tener dos columnas con correo electrónico en la primera columna del archivo y el nombre en la última columna del archivo de las participantes que deseas añadir al espacio participativo, sin encabezados. Evita usar caracteres no válidos como `<>?%&^*#@()[]=+:;"{}\|` en el nombre de usuaria.' - explanation_example: | - john.doe@example.org%{csv_col_sep}John Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - title: Importar participantes privadas vía CSV - upload: Subir reminders: create: error: Se ha producido un error al crear los recordatorios. diff --git a/decidim-admin/config/locales/es.yml b/decidim-admin/config/locales/es.yml index fc007682585c5..7af7a77c7c0a5 100644 --- a/decidim-admin/config/locales/es.yml +++ b/decidim-admin/config/locales/es.yml @@ -33,6 +33,11 @@ es: help_section: content: Contenido id: ID + member: + email: Correo electrónico + name: Nombre + member_csv_import: + file: Archivo newsletter: body: Cuerpo send_to_all_users: Enviar a todas las participantes @@ -91,11 +96,6 @@ es: welcome_notification_body: Cuerpo del mensaje de notificación de bienvenida welcome_notification_subject: Asunto del mensaje de notificación de bienvenida youtube_handler: Nombre de YouTube - participatory_space_private_user: - email: Correo electrónico - name: Nombre - participatory_space_private_user_csv_import: - file: Archivo scope: code: Código name: Nombre @@ -125,10 +125,17 @@ 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: models: + member_csv_import: + attributes: + file: + malformed: Archivo de importación mal formateado, por favor lea las instrucciones cuidadosamente y asegúrese de que el archivo está codificado en UTF-8. newsletter: attributes: base: @@ -137,10 +144,6 @@ es: attributes: official_img_footer: allowed_file_content_types: Archivo de imagen no válido - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Archivo de importación mal formateado, por favor lea las instrucciones cuidadosamente y asegúrese de que el archivo está codificado en UTF-8. user_group_csv_verification: attributes: file: @@ -188,12 +191,12 @@ es: export: Exportar export-selection: Exportar selección import: Importar + member: + new: Nueva miembro menu_hidden: Ocultar del menú moderate: Gestionar las moderaciones newsletter: new: Nuevo boletín - participatory_space_private_user: - new: Nuevo usuario privado del espacio participativo per_page: Por página permissions: Gestionar los permisos restore: Restaurar @@ -415,6 +418,17 @@ es: values: 'false': 'No' 'true': 'Sí' + members: + user_invitation_accepted_at_not_null: + label: Invitación aceptada + values: + 'false': No aceptada + 'true': Aceptada + user_invitation_sent_at_not_null: + label: Invitación enviada + values: + 'false': No enviada + 'true': Enviada moderated_users: reports_reason_eq: label: Motivo de la denuncia @@ -430,17 +444,6 @@ es: values: 'false': Oficializada 'true': No oficializada - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Invitación aceptada - values: - 'false': No aceptada - 'true': Aceptada - user_invitation_sent_at_not_null: - label: Invitación enviada - values: - 'false': No enviada - 'true': Enviada private_space_eq: label: Privado values: @@ -573,6 +576,53 @@ es: explanation: Las participantes gestionadas se pueden promocionar a participantes estándar. Significa que serán invitadas al sistema y no podrás volver a administrarlas. La participante invitada recibirá un correo electrónico para aceptar tu invitación. new_managed_user_promotion: Nueva promoción de participante promote: Promocionar + members: + create: + error: Se ha producido un error al agregar una participante privada a este espacio participativo. + success: Acceso como miembro creado correctamente. + destroy: + error: Se ha producido un error al eliminar a una participante privada para este espacio de participación. + success: Acceso como miembro eliminado correctamente. + edit: + title: Editar miembro + update: Actualizar + index: + import_via_csv: Importar vía csv + publish_all: Publicar todas + title: Miembro + unpublish_all: Despublicar todas + new: + create: Crear + title: Nueva miembro + publish_all: + error: Se ha producido un error al publicar todas las miembros de este espacio de participación. + success: Se han publicado correctamente todas las miembros de este espacio de participación + unpublish_all: + error: Se ha producido un error al despublicar todas las miembros de este espacio de participación. + success: Se han despublicado correctamente todas las miembros de este espacio de participación + update: + error: Se ha producido un error al actualizar la miembro para este espacio de participación. + success: Miembro actualizada correctamente + members_csv_imports: + create: + invalid: Hubo un problema al leer el archivo CSV. Por favor, asegúrate de haber seguido las instrucciones. + success: El archivo CSV se ha cargado correctamente, estamos enviando un correo de invitación a las participantes. Este proceso puede tardar un poco. + new: + csv_upload: + title: Sube tu archivo CSV + destroy: + button: Borrar todas las miembros + confirm: '¿Seguro que quieres eliminar todas las miembros? Esta acción no se puede deshacer, no podrás recuperarlas.' + empty: No hay miembros. + explanation: Hay %{count} miembros. + title: Eliminar miembros + example_file: 'Archivo de ejemplo:' + explanation: 'Sube tu archivo CSV. Debe tener dos columnas con correo electrónico en la primera columna del archivo y el nombre en la última columna del archivo de las participantes que deseas añadir al espacio participativo, sin encabezados. Evita usar caracteres no válidos como `<>?%&^*#@()[]=+:;"{}\|` en el nombre de usuaria.' + explanation_example: | + john.doe@example.org%{csv_col_sep}John Doe + jane.doe@example.org%{csv_col_sep}Jane Doe + title: Importar miembros mediante CSV + upload: Cargar menu: admin_log: Registro de actividad de administración admins: Administradoras @@ -631,6 +681,8 @@ es: reason: Razón started_at: Empezó a las user: Participante + member: + name: Miembro newsletter: fields: created_at: Fecha de creación @@ -639,8 +691,6 @@ es: sent_to: Enviado a subject: Asunto name: Boletín - participatory_space_private_user: - name: Participante de espacio de participación privado scope: fields: name: Nombre @@ -915,53 +965,6 @@ es: form: add: Añadir a la lista de direcciones permitidas title: Lista de dominios externos permitidos - participatory_space_private_users: - create: - error: Se ha producido un error al agregar una participante privada a este espacio participativo. - success: Acceso privado a participantes del espacio participativo creado correctamente. - destroy: - error: Se ha producido un error al eliminar a una participante privada para este espacio participativo. - success: El acceso de la participante al espacio de participación privado se ha eliminado correctamente. - edit: - title: Editar participante privada del espacio de participación. - update: Actualizar - index: - import_via_csv: Importar vía csv - publish_all: Publicar todo - title: Participante de espacio de participación privado - unpublish_all: Despublicar todo - new: - create: Crear - title: Nueva participante del espacio privado. - publish_all: - error: Se ha producido un error al publicar todas las participantes privadas de este espacio de participación. - success: Se han publicado correctamente todas las participantes privadas de este espacio de participación - unpublish_all: - error: Se ha producido un error al despublicar todas las participantes privadas de este espacio de participación. - success: Se han despublicado correctamente todas las participantes privadas de este espacio de participación - update: - error: Se ha producido un error actualizando una participante privada de este espacio de participación. - success: Se ha actualizado correctamente el espacio de participación privado - participatory_space_private_users_csv_imports: - create: - invalid: Hubo un problema al leer el archivo CSV. Por favor, asegúrate de haber seguido las instrucciones. - success: El archivo CSV se ha cargado correctamente, estamos enviando un correo de invitación a las participantes. Este proceso puede tardar un poco. - new: - csv_upload: - title: Sube tu archivo CSV - destroy: - button: Borrar todas las participantes privadas - confirm: '¿Seguro que quieres eliminar todas las participantes privadas? Esta acción no se puede deshacer, no podrás recuperarlas.' - empty: No hay participantes privadas. - explanation: Hay %{count} participante/s privada/s. - title: Borrar las participantes privadas - example_file: 'Fichero de ejemplo:' - explanation: 'Sube tu archivo CSV. Debe tener dos columnas con correo electrónico en la primera columna del archivo y el nombre en la última columna del archivo de las participantes que deseas añadir al espacio participativo, sin encabezados. Evita usar caracteres no válidos como `<>?%&^*#@()[]=+:;"{}\|` en el nombre de usuaria.' - explanation_example: | - john.doe@example.org%{csv_col_sep}John Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - title: Importar participantes privadas vía CSV - upload: Subir reminders: create: error: Se ha producido un error al crear los recordatorios. diff --git a/decidim-admin/config/locales/eu.yml b/decidim-admin/config/locales/eu.yml index 8bb5f29f864c1..819cb98d72fb8 100644 --- a/decidim-admin/config/locales/eu.yml +++ b/decidim-admin/config/locales/eu.yml @@ -33,6 +33,11 @@ eu: help_section: content: Edukia id: ID + member: + email: Helbide elektronikoa + name: Izena + member_csv_import: + file: Fitxategia newsletter: body: Testua send_to_all_users: Bidali parte-hartzaile guztiei @@ -91,11 +96,6 @@ eu: welcome_notification_body: Ongietorri-jakinarazpenaren testua welcome_notification_subject: Ongi etorri jakinarazpenaren mezuaren gaia youtube_handler: YouTubeko kudeatzailea - participatory_space_private_user: - email: Helbide elektronikoa - name: Izena - participatory_space_private_user_csv_import: - file: Fitxategia scope: code: Kodea name: Izena @@ -125,10 +125,17 @@ 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: models: + member_csv_import: + attributes: + file: + malformed: Gaizki osatutako inportazio-fitxategia. Mesedez, irakurri arretaz jarraibideen bidez, eta ziurtatu fitxategia UTF-8 kodetuta dagoela. newsletter: attributes: base: @@ -137,10 +144,6 @@ eu: attributes: official_img_footer: allowed_file_content_types: Irudi-fitxategi baliogabea - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Fitxategia akastuna da. Irakurri arretaz jarraibideak eta ziurtatu fitxategia UTF-8an kodifikatuta dagoela. user_group_csv_verification: attributes: file: @@ -188,12 +191,12 @@ eu: export: Esportatu export-selection: Esportatu hautaketa import: Inportatu + member: + new: Beste kide bat menu_hidden: Ezkutatu menutik moderate: Kudeatu moderazioak newsletter: new: Beste buletin bat - participatory_space_private_user: - new: Partaidetza-espazio berria, erabiltzaile pribatua per_page: Orrialdeko permissions: Kudeatu baimenak restore: Berreskuratu @@ -415,6 +418,17 @@ eu: values: 'false': 'Ez' 'true': 'Bai' + members: + user_invitation_accepted_at_not_null: + label: Gonbidapena onartua + values: + 'false': Ez onartua + 'true': Onartua + user_invitation_sent_at_not_null: + label: Gonbidapena bidalita + values: + 'false': Ez da bidali + 'true': Bidalia moderated_users: reports_reason_eq: label: Arrazoiaren berri eman @@ -430,17 +444,6 @@ eu: values: 'false': Ofizializatua 'true': Ez ofizializatua - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Gonbidapena onartua - values: - 'false': Ez onartuta - 'true': Onartuta - user_invitation_sent_at_not_null: - label: Gonbidapena bidali da - values: - 'false': Ez da bidali - 'true': Bidalia private_space_eq: label: Pribatua values: @@ -573,6 +576,53 @@ eu: explanation: Kudeatutako parte-hartzaileak parte-hartzaile estandar mailara igo daitezke. Horrek esan nahi du aplikaziora gonbidatuak izanen direla eta ezingo direla berriro ere ordeztu. Parte-hartzaile gonbidatuak mezu elektroniko bat jasoko du gonbita onartzeko. new_managed_user_promotion: Kudeatutako parte-hartzaileen beste maila-igoera bat promote: Sustatu + members: + create: + error: Arazo bat izan da partaidetza-espazio horretarako kide bat gehitzeko. + success: Kideen sarbidea behar bezala sortu da. + destroy: + error: Arazo bat dago partaidetza-espazio horretarako kide bat ezabatzeko. + success: Kidearen sarbidea behar bezala suntsitu da. + edit: + title: Kidea aldatu + update: Egunaratu + index: + import_via_csv: CSV fitxategi baten bidez inportatu + publish_all: Denak argitaratu + title: Kidea + unpublish_all: Desargitaratu guztiak + new: + create: Sortu + title: Beste kide bat + publish_all: + error: Arazo bat izan da partaidetza-espazio horretarako kide bat gehitzeko. + success: Partaidetza-espazio honetarako kide guztiak arrakastaz argitaratu dira + unpublish_all: + error: Arazo bat dago partaidetza-espazio horretarako kide guztiak desargitaratzeko. + success: Partaidetza-espazio honetako kide guztiak behar bezala ezabatu dira + update: + error: Arazo bat izan da partaidetza-espazio horretarako kidea eguneratzeko. + success: Kidea behar bezala eguneratu da + members_csv_imports: + create: + invalid: Arazo bat izan da CSV fitxategia irakurtzean. Egiaztatu jarraibideak bete dituzula. + success: CSV fitxategia ondo igo da, gonbidapen-mezu elektroniko bat bidaltzen ari gara parte-hartzaileei. Horrek denbora pixka bat iraun dezake. + new: + csv_upload: + title: Igo zure CSV fitxategia + destroy: + button: Kide guztiak ezabatu + confirm: Ziur zaude kide guztiak ezabatu nahi dituzula? Ekintza hori ezin da desegin, ezingo dituzu berreskuratu. + empty: Ez duzu kiderik. + explanation: '%{count} kide dituzu.' + title: Kideak ezabatu + example_file: 'Adibide-fitxategia:' + explanation: 'Igo zure CSV fitxategia. Bi zutabe izan behar ditu, fitxategiko lehen zutabean posta elektronikoa eta parte-hartze gunera gehitu nahi dituzun parte-hartzaileen fitxategiaren azken zutabean izena jarrita, goibururik gabe. Ez erabili baliozko ez diren karaktereak erabiltzaile-izenean, hala nola, "sim"<>?%&^*#@()[]=+:;"{}\|`.' + explanation_example: | + john.doe@example.org%{csv_col_sep}John Doe + jane.doe@example.org%{csv_col_sep}Jane Doe + title: Kidek CSV fitxategi baten bidez + upload: Igo menu: admin_log: Administratzaile jarduera-erregistroa admins: Administratzaileak @@ -631,6 +681,8 @@ eu: reason: Arrazoia started_at: 'Hasiera-ordua:' user: Parte-hartzaileak + member: + name: Kidea newsletter: fields: created_at: Noiz sortua @@ -639,8 +691,6 @@ eu: sent_to: Hona bidalia subject: Gaia name: Buletina - participatory_space_private_user: - name: Partaidetza-espazio pribatuko parte-hartzailea scope: fields: name: Izena @@ -915,53 +965,6 @@ eu: form: add: Gehitu baimendutako helbideen zerrendara title: Baimendutako kanpoko domeinuen zerrenda - participatory_space_private_users: - create: - error: Arazo bat egon da parte-hartzaile pribatu bat gehitzean partaidetza-espazio honetarako. - success: Zuzen sortu da sarbide pribatua partaidetza-espazioko partaideentzat. - destroy: - error: Arazo bat egon da parte-hartzaile pribatu bat ezabatzean partaidetza-espazio honetarako. - success: Behar bezala ezabatua parte-hartzailearen sarbidea partaidetza-espazio pribaturako. - edit: - title: Editatu espazio parte-hartzailea parte-hartzaile pribatua. - update: Eguneratu - index: - import_via_csv: Inportatu CSV bidetik - publish_all: Argitaratu guztiak - title: Partaidetza-espazio pribatuko parte-hartzailea - unpublish_all: Desargitaratu guztiak - new: - create: Sortu - title: Partaidetza-espazio pribatuko parte-hartzaile berria. - publish_all: - error: Arazo bat egon da parte-hartzaile pribatu guztiak argitaratzean partaidetza-espazio honetarako. - success: Parte-hartzaile pribatu guztiak zuzen argitaratu dira partaidetza-espazio honetarako - unpublish_all: - error: Arazo bat egon da parte-hartzaile pribatu guztiak desargiratzean partaidetza-espazio honetarako. - success: Parte-hartzaile pribatu guztiak zuzen desargitaratu dira partaidetza-espazio honetarako - update: - error: Arazo bat egon da parte-hartzaile pribatua partaidetza-espazio horretarako eguneratzean. - success: Partaidetza-espazioa, parte-hartzaile pribatua, arrakastaz eguneratua - participatory_space_private_users_csv_imports: - create: - invalid: Arazo bat egon da CSV fitxategia irakurtzean. Mesedez, ziurtatu jarraibideei kasu egin diezula. - success: CSV fitxategia zuzen igo da, eta parte-hartzaileei gonbidapen-mezu elektroniko bat bidaltzen ari gara. Prozesu horrek denbora behar du. - new: - csv_upload: - title: Igo zure CSV fitxategia - destroy: - button: Ezabatu parte-hartzaile pribatu guztiak - confirm: Ziur zaude parte-hartzaile pribatu guztiak ezabatu nahi dituzula? Ekintza hau ezin da desegin, ezin izango dituzu berreskuratu. - empty: Ez duzu parte-hartzaile pribaturik. - explanation: '%{count} parte-hartzaile pribatu dituzu.' - title: Ezabatu parte-hartzaile pribatuak - example_file: 'Fitxategi eredua:' - explanation: 'Igo zure CSV fitxategia. Bi zutabe izan behar ditu. Lehenengo zutabean posta elektronikoa jarri behar da, eta azken zutabean partaidetza-prozesura gehitu nahi dituzun parte-hartzaileen izena, goibururik gabe. Ez erabili holako karaktererik erabiltzaileen izenetan: `<>?%&^*#@()[]=+:;"{}\|`.' - explanation_example: | - john.doe@example.org%{csv_col_sep}John Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - title: Inportatu parte-hartzaile pribatuak CSV bidez - upload: Igo reminders: create: error: Arazo bat egon da ohartarazpenak sortzean. diff --git a/decidim-admin/config/locales/fi-plain.yml b/decidim-admin/config/locales/fi-plain.yml index ad15d7a023ba4..edd11acd16219 100644 --- a/decidim-admin/config/locales/fi-plain.yml +++ b/decidim-admin/config/locales/fi-plain.yml @@ -33,8 +33,14 @@ fi-pl: help_section: content: Sisältö id: ID + member: + email: Sähköpostiosoite + name: Nimi + member_csv_import: + file: Tiedosto newsletter: body: Runko + send_to_all_users: Lähetä kaikille osallistujille send_to_followers: Lähetä seuraajille send_to_participants: Lähetä osallistujille subject: Otsikko @@ -90,11 +96,6 @@ fi-pl: welcome_notification_body: Tervetuloilmoituksen runko welcome_notification_subject: Tervetuloilmoituksen otsikko youtube_handler: YouTube-käsittelijä - participatory_space_private_user: - email: Sähköpostiosoite - name: Nimi - participatory_space_private_user_csv_import: - file: Tiedosto scope: code: Koodi name: Nimi @@ -124,10 +125,17 @@ fi-pl: show_in_footer: Näytä alatunnisteessa title: Otsikko weight: Järjestysnumero + taxonomy: + item_name: Asian nimi + parent_id: Vanhempi user_group_csv_verification: file: Tiedosto errors: models: + member_csv_import: + attributes: + file: + malformed: Virheellinen tuontitiedosto, lue ohjeet huolellisesti ja varmista, että tiedosto on UTF-8 muodossa. newsletter: attributes: base: @@ -136,10 +144,6 @@ fi-pl: attributes: official_img_footer: allowed_file_content_types: Virheellinen kuvatiedosto - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Virheellinen tuontitiedosto, lue ohjeet huolellisesti ja varmista, että tiedosto on UTF-8 muodossa. user_group_csv_verification: attributes: file: @@ -187,12 +191,12 @@ fi-pl: export: Vie export-selection: Vie valitut import: Tuo + member: + new: Uusi jäsen menu_hidden: Piilota valikosta moderate: Moderointien hallinta newsletter: new: Uusi uutiskirje - participatory_space_private_user: - new: Uusi osallistumistilan yksityinen käyttäjä per_page: Per sivu permissions: Käyttöoikeuksien hallinta restore: Palauta @@ -292,11 +296,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 +396,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}" @@ -411,6 +418,17 @@ fi-pl: values: 'false': 'Ei' 'true': 'Kyllä' + members: + user_invitation_accepted_at_not_null: + label: Kutsu hyväksytty + values: + 'false': Ei hyäksytty + 'true': Hyväksytty + user_invitation_sent_at_not_null: + label: Kutsu lähetetty + values: + 'false': Ei lähetetty + 'true': Lähetetty moderated_users: reports_reason_eq: label: Ilmoituksen syy @@ -426,17 +444,6 @@ fi-pl: values: 'false': Virallistettu 'true': Ei virallistettu - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Kutsu hyväksytty - values: - 'false': Ei hyäksytty - 'true': Hyväksytty - user_invitation_sent_at_not_null: - label: Kutsu lähetetty - values: - 'false': Ei lähetetty - 'true': Lähetetty private_space_eq: label: Yksityinen values: @@ -474,6 +481,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: @@ -568,6 +576,53 @@ fi-pl: explanation: Hallituille käyttäjille voidaan myöntää normaalien käyttäjien oikeudet. Tämä tarkoittaa, että heille lähetetään kutsu hakemusprosessiin ja tämän jälkeen kyseisinä tileinä ei voi enää esiintyä. Kutsutulle käyttäjälle lähetetään sähköposti, jonka avulla hän voi hyväksyä kutsusi. new_managed_user_promotion: Uusi käyttäjätason korotus hallinnoidulle käyttäjälle promote: Korota oikeudet + members: + create: + error: Jäsenen lisääminen tähän osallistumistilaan epäonnistui. + success: Jäsenen pääsyoikeuden lisäys onnistui. + destroy: + error: Osallistumistilan jäsenen poistaminen epäonnistui. + success: Jäsenen pääsyoikeuden poistaminen onnistui. + edit: + title: Muokkaa jäsentä + update: Päivitä + index: + import_via_csv: Tuo CSV-tiedostosta + publish_all: Julkaise kaikki + title: Jäsen + unpublish_all: Lopeta kaikkien julkaisu + new: + create: Luo + title: Uusi jäsen + publish_all: + error: Osallistumistilan jäsenten julkaiseminen epäonnistui. + success: Osallistumistilan jäsenten julkaiseminen onnistui + unpublish_all: + error: Osallistumistilan jäsenten julkaisun peruminen epäonnistui. + success: Osallistumistilan jäsenten julkaisun peruminen onnistui. + update: + error: Osallistumistilan jäsenen päivittäminen epäonnistui. + success: Osallistumistilan jäsenen päivittäminen onnistui. + members_csv_imports: + create: + invalid: CSV-tiedoston lukeminen epäonnistui. Tarkasta, että olet seurannut esitettyjä ohjeita. + success: CSV-tiedoston lataus onnistui. Lähetämme kutsusähköpostin osallistujille. Tämä voi kestää hetken. + new: + csv_upload: + title: Lataa CSV-tiedosto + destroy: + button: Poista kaikki jäsenet + confirm: Haluatko varmasti poistaa kaikki jäsenet? Tätä toimintoa ei voi perua ja poistettuja jäseniä ei voi palauttaa. + empty: Jäseniä ei ole määritetty. + explanation: '%{count} jäsentä.' + title: Poista jäsenet + example_file: 'Esimerkkitiedosto:' + explanation: 'Lataa CSV-tiedosto. Tiedostossa on oltava kaksi saraketta ilman otsikkoriviä. Ensimmäiseen sarakkeeseen määritetään käyttäjän sähköpostiosoite ja toiseen sarakkeeseen käyttäjän nimi, alkaen ensimmäiseltä riviltä. Jokaisesta rivistä luodaan uusi osallistumistilan käyttäjä. Vältä erikoismerkkejä nimessä, kuten `<>?%&^*#@()[]=+:;"{}\|`.' + explanation_example: | + matti.meikalainen@esimerkki.fi%{csv_col_sep}Matti Meikäläinen + minna.meikalainen@esimerkki.fi%{csv_col_sep}Minna Meikäläinen + title: Tuo jäseniä CSV-tiedostosta + upload: Lataa menu: admin_log: Hallintatoimintojen tapahtumaloki admins: Ylläpitäjät @@ -626,6 +681,8 @@ fi-pl: reason: Syy started_at: Aloitettu user: Käyttäjä + member: + name: Jäsen newsletter: fields: created_at: Luonnin ajankohta @@ -634,8 +691,6 @@ fi-pl: sent_to: Vastaanottajat subject: Otsikko name: Uutiskirje - participatory_space_private_user: - name: Osallisuustilan yksityinen käyttäjä scope: fields: name: Nimi @@ -910,53 +965,6 @@ fi-pl: form: add: Lisää sallittujen listalle title: Sallitut ulkoiset verkko-osoitteet - participatory_space_private_users: - create: - error: Yksityisen käyttäjän lisäämisessä tähän osallisuustilaan tapahtui virhe. - success: Osallisuustilaa koskeva yksityisen käyttäjän käyttöoikeus lisätty onnistuneesti. - destroy: - error: Tapahtui virhe poistettaessa osallisuustilan yksityistä käyttäjää. - success: Osallisuustilaa koskeva yksityisen käyttäjän käyttöoikeus tuhottu onnistuneesti. - edit: - title: Muokkaa osallistumistilan yksityistä käyttäjää. - update: Päivitä - index: - import_via_csv: Tuo CSV-tiedostosta - publish_all: Julkaise kaikki - title: Osallisuustilan yksityinen käyttäjä - unpublish_all: Lopeta kaikkien julkaisu - new: - create: Luo - title: Uusi osallisuustilan yksityinen käyttäjä. - publish_all: - error: Osallistumistilan yksityisten käyttäjien julkaiseminen epäonnistui. - success: Osallistumistilan yksityisten käyttäjien julkaiseminen onnistui - unpublish_all: - error: Osallistumistilan yksityisten käyttäjien julkaisun peruminen epäonnistui. - success: Osallistumistilan yksityisten käyttäjien julkaisun peruminen onnistui. - update: - error: Osallistumistilan yksityisen käyttäjän päivittäminen epäonnistui. - success: Osallistumistilan yksityisen käyttäjän päivittäminen onnistui. - participatory_space_private_users_csv_imports: - create: - invalid: CSV-tiedoston lukeminen epäonnistui. Tarkasta, että olet seurannut esitettyjä ohjeita. - success: CSV-tiedosto ladattu onnistuneesti. Lähetämme kutsusähköpostin osallistujille. Tämä voi kestää hetken. - new: - csv_upload: - title: Lataa CSV-tiedosto - destroy: - button: Poista kaikki yksityiset osallistujat - confirm: Oletko varma, että haluat poistaa kaikki yksityiset osallistujat? Tätä toimintoa ei voi peruuttaa ja poistettuja osallistujia ei voi palauttaa. - empty: Tässä osallistumistilassa ei ole yksityisiä osallistujia. - explanation: Tässä osallistumistilassa on %{count} yksityistä osallistujaa. - title: Poista kaikki yksityiset osallistujat - example_file: 'Esimerkkitiedosto:' - explanation: 'Lataa CSV-tiedosto. Tiedostossa on oltava kaksi saraketta ilman otsikkoriviä. Ensimmäiseen sarakkeeseen asetetaan käyttäjän sähköpostiosoite ja toiseen sarakkeeseen käyttäjän nimi, alkaen ensimmäiseltä riviltä. Jokaisesta rivistä luodaan uusi osallistumistilan käyttäjä. Vältä erikoismerkkejä nimessä, kuten `<>?%&^*#@()[]=+:;"{}\|`.' - explanation_example: | - matti.meikalainen@esimerkki.fi%{csv_col_sep}Matti Meikäläinen - minna.meikalainen@esimerkki.fi%{csv_col_sep}Minna Meikäläinen - title: Tuo yksityisiä osallistujia CSV-tiedostosta - upload: Lataa reminders: create: error: Muistutusten luonti epäonnistui. @@ -1265,6 +1273,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..bf8594d49d631 100644 --- a/decidim-admin/config/locales/fi.yml +++ b/decidim-admin/config/locales/fi.yml @@ -33,8 +33,14 @@ fi: help_section: content: Sisältö id: ID + member: + email: Sähköpostiosoite + name: Nimi + member_csv_import: + file: Tiedosto newsletter: body: Runko + send_to_all_users: Lähetä kaikille osallistujille send_to_followers: Lähetä seuraajille send_to_participants: Lähetä osallistujille subject: Otsikko @@ -90,11 +96,6 @@ fi: welcome_notification_body: Tervetuloilmoituksen runko welcome_notification_subject: Tervetuloilmoituksen otsikko youtube_handler: YouTube-käsittelijä - participatory_space_private_user: - email: Sähköpostiosoite - name: Nimi - participatory_space_private_user_csv_import: - file: Tiedosto scope: code: Koodi name: Nimi @@ -124,10 +125,17 @@ fi: show_in_footer: Näytä alatunnisteessa title: Otsikko weight: Järjestysnumero + taxonomy: + item_name: Asian nimi + parent_id: Vanhempi user_group_csv_verification: file: Tiedosto errors: models: + member_csv_import: + attributes: + file: + malformed: Virheellinen tuontitiedosto, lue ohjeet huolellisesti ja varmista, että tiedosto on UTF-8 muodossa. newsletter: attributes: base: @@ -136,10 +144,6 @@ fi: attributes: official_img_footer: allowed_file_content_types: Virheellinen kuvatiedosto - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Virheellinen tuontitiedosto, lue ohjeet huolellisesti ja varmista, että tiedosto on UTF-8 muodossa. user_group_csv_verification: attributes: file: @@ -187,12 +191,12 @@ fi: export: Vie export-selection: Vie valitut import: Tuo + member: + new: Uusi jäsen menu_hidden: Piilota valikosta moderate: Moderointien hallinta newsletter: new: Uusi uutiskirje - participatory_space_private_user: - new: Uusi osallistumistilan yksityinen käyttäjä per_page: Per sivu permissions: Käyttöoikeuksien hallinta restore: Palauta @@ -292,11 +296,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 +396,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}" @@ -411,6 +418,17 @@ fi: values: 'false': 'Ei' 'true': 'Kyllä' + members: + user_invitation_accepted_at_not_null: + label: Kutsu hyväksytty + values: + 'false': Ei hyäksytty + 'true': Hyväksytty + user_invitation_sent_at_not_null: + label: Kutsu lähetetty + values: + 'false': Ei lähetetty + 'true': Lähetetty moderated_users: reports_reason_eq: label: Ilmoituksen syy @@ -426,17 +444,6 @@ fi: values: 'false': Virallistettu 'true': Ei virallistettu - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Kutsu hyväksytty - values: - 'false': Ei hyäksytty - 'true': Hyväksytty - user_invitation_sent_at_not_null: - label: Kutsu lähetetty - values: - 'false': Ei lähetetty - 'true': Lähetetty private_space_eq: label: Yksityinen values: @@ -474,6 +481,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: @@ -568,6 +576,53 @@ fi: explanation: Hallituille käyttäjille voidaan myöntää normaalien käyttäjien oikeudet. Tämä tarkoittaa, että heille lähetetään rekisteröitymiskutsu alustalle ja rekisteröitymisen jälkeen kyseisinä käyttäjinä ei voi enää esiintyä. Kutsutulle käyttäjälle lähetetään sähköposti, jonka avulla he voivat hyväksyä kutsusi. new_managed_user_promotion: Uusi käyttäjätason korotus hallinnoidulle käyttäjälle promote: Korota oikeudet + members: + create: + error: Jäsenen lisääminen tähän osallistumistilaan epäonnistui. + success: Jäsenen pääsyoikeuden lisäys onnistui. + destroy: + error: Osallistumistilan jäsenen poistaminen epäonnistui. + success: Jäsenen pääsyoikeuden poistaminen onnistui. + edit: + title: Muokkaa jäsentä + update: Päivitä + index: + import_via_csv: Tuo CSV-tiedostosta + publish_all: Julkaise kaikki + title: Jäsen + unpublish_all: Lopeta kaikkien julkaisu + new: + create: Luo + title: Uusi jäsen + publish_all: + error: Osallistumistilan jäsenten julkaiseminen epäonnistui. + success: Osallistumistilan jäsenten julkaiseminen onnistui + unpublish_all: + error: Osallistumistilan jäsenten julkaisun peruminen epäonnistui. + success: Osallistumistilan jäsenten julkaisun peruminen onnistui. + update: + error: Osallistumistilan jäsenen päivittäminen epäonnistui. + success: Osallistumistilan jäsenen päivittäminen onnistui. + members_csv_imports: + create: + invalid: CSV-tiedoston lukeminen epäonnistui. Tarkasta, että olet seurannut esitettyjä ohjeita. + success: CSV-tiedoston lataus onnistui. Lähetämme kutsusähköpostin osallistujille. Tämä voi kestää hetken. + new: + csv_upload: + title: Lataa CSV-tiedosto + destroy: + button: Poista kaikki jäsenet + confirm: Haluatko varmasti poistaa kaikki jäsenet? Tätä toimintoa ei voi perua ja poistettuja jäseniä ei voi palauttaa. + empty: Jäseniä ei ole määritetty. + explanation: '%{count} jäsentä.' + title: Poista jäsenet + example_file: 'Esimerkkitiedosto:' + explanation: 'Lataa CSV-tiedosto. Tiedostossa on oltava kaksi saraketta ilman otsikkoriviä. Ensimmäiseen sarakkeeseen määritetään käyttäjän sähköpostiosoite ja toiseen sarakkeeseen käyttäjän nimi, alkaen ensimmäiseltä riviltä. Jokaisesta rivistä luodaan uusi osallistumistilan käyttäjä. Vältä erikoismerkkejä nimessä, kuten `<>?%&^*#@()[]=+:;"{}\|`.' + explanation_example: | + matti.meikalainen@esimerkki.fi%{csv_col_sep}Matti Meikäläinen + minna.meikalainen@esimerkki.fi%{csv_col_sep}Minna Meikäläinen + title: Tuo jäseniä CSV-tiedostosta + upload: Lataa menu: admin_log: Hallintatoimintojen tapahtumaloki admins: Ylläpitäjät @@ -626,6 +681,8 @@ fi: reason: Syy started_at: Aloitettu user: Käyttäjä + member: + name: Jäsen newsletter: fields: created_at: Luonnin ajankohta @@ -634,8 +691,6 @@ fi: sent_to: Vastaanottajat subject: Otsikko name: Uutiskirje - participatory_space_private_user: - name: Osallistumistilan yksityinen käyttäjä scope: fields: name: Nimi @@ -910,53 +965,6 @@ fi: form: add: Lisää sallittujen listalle title: Sallitut ulkoiset verkko-osoitteet - participatory_space_private_users: - create: - error: Yksityisen käyttäjän lisääminen tähän osallistumistilaan epäonnistui. - success: Osallistumistilaa koskeva yksityisen käyttäjän käyttöoikeuden lisäys onnistui. - destroy: - error: Osallistumistilan yksityisen käyttäjän poistaminen epäonnistui. - success: Osallistumistilaa koskeva yksityisen käyttäjän käyttöoikeuden poistaminen onnistui. - edit: - title: Muokkaa osallistumistilan yksityistä käyttäjää. - update: Päivitä - index: - import_via_csv: Tuo CSV-tiedostosta - publish_all: Julkaise kaikki - title: Osallistumistilan yksityinen käyttäjä - unpublish_all: Lopeta kaikkien julkaisu - new: - create: Luo - title: Uusi osallistumistilan yksityinen käyttäjä. - publish_all: - error: Osallistumistilan yksityisten käyttäjien julkaiseminen epäonnistui. - success: Osallistumistilan yksityisten käyttäjien julkaiseminen onnistui - unpublish_all: - error: Osallistumistilan yksityisten käyttäjien julkaisun peruminen epäonnistui. - success: Osallistumistilan yksityisten käyttäjien julkaisun peruminen onnistui. - update: - error: Osallistumistilan yksityisen käyttäjän päivittäminen epäonnistui. - success: Osallistumistilan yksityisen käyttäjän päivittäminen onnistui. - participatory_space_private_users_csv_imports: - create: - invalid: CSV-tiedoston lukeminen epäonnistui. Tarkasta, että olet seurannut esitettyjä ohjeita. - success: CSV-tiedoston lataus onnistui. Lähetämme kutsusähköpostin osallistujille. Tämä voi kestää hetken. - new: - csv_upload: - title: Lataa CSV-tiedosto - destroy: - button: Poista kaikki yksityiset osallistujat - confirm: Oletko varma, että haluat poistaa kaikki yksityiset osallistujat? Tätä toimintoa ei voi perua ja poistettuja osallistujia ei voi palauttaa. - empty: Tässä osallistumistilassa ei ole yksityisiä osallistujia. - explanation: Tässä osallistumistilassa on %{count} yksityistä osallistujaa. - title: Poista kaikki yksityiset osallistujat - example_file: 'Esimerkkitiedosto:' - explanation: 'Lataa CSV-tiedosto. Tiedostossa on oltava kaksi saraketta ilman otsikkoriviä. Ensimmäiseen sarakkeeseen asetetaan käyttäjän sähköpostiosoite ja toiseen sarakkeeseen käyttäjän nimi, alkaen ensimmäiseltä riviltä. Jokaisesta rivistä luodaan uusi osallistumistilan käyttäjä. Vältä erikoismerkkejä nimessä, kuten `<>?%&^*#@()[]=+:;"{}\|`.' - explanation_example: | - matti.meikalainen@esimerkki.fi%{csv_col_sep}Matti Meikäläinen - minna.meikalainen@esimerkki.fi%{csv_col_sep}Minna Meikäläinen - title: Tuo yksityisiä osallistujia CSV-tiedostosta - upload: Lataa reminders: create: error: Muistutusten luonti epäonnistui. @@ -1265,6 +1273,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..e73a751c9ebf1 100644 --- a/decidim-admin/config/locales/fr-CA.yml +++ b/decidim-admin/config/locales/fr-CA.yml @@ -33,6 +33,11 @@ fr-CA: help_section: content: Contenu id: ID + member: + email: Email + name: Nom + member_csv_import: + file: Fichier newsletter: body: Corps de texte send_to_all_users: Envoyer à tous les participants @@ -90,11 +95,6 @@ fr-CA: welcome_notification_body: Corps du mail de bienvenue welcome_notification_subject: Objet du mail de bienvenue youtube_handler: Gestionnaire YouTube - participatory_space_private_user: - email: Email - name: Nom - participatory_space_private_user_csv_import: - file: Fichier scope: code: Code name: Titre @@ -124,10 +124,17 @@ 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: models: + member_csv_import: + attributes: + file: + malformed: Fichier d'importation mal formé, veuillez lire attentivement les instructions et assurez-vous que le fichier est encodé en UTF-8. newsletter: attributes: base: @@ -136,10 +143,6 @@ fr-CA: attributes: official_img_footer: allowed_file_content_types: Fichier image invalide - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Fichier d'importation mal formé, veuillez lire attentivement les instructions et assurez-vous que le fichier est encodé en UTF-8. user_group_csv_verification: attributes: file: @@ -187,12 +190,12 @@ fr-CA: export: Exporter export-selection: Exporter la sélection import: Importer + member: + new: Nouveau membre menu_hidden: Masquer dans le menu moderate: Gérer les modérations newsletter: new: Nouvelle newsletter - participatory_space_private_user: - new: Nouvel utilisateur privé de l'espace participatif per_page: Par page permissions: Gérer les permissions restore: Restaurer @@ -413,6 +416,17 @@ fr-CA: values: 'false': 'Non' 'true': 'Oui' + members: + user_invitation_accepted_at_not_null: + label: Invitation acceptée + values: + 'false': Non acceptée + 'true': Acceptée + user_invitation_sent_at_not_null: + label: Invitation envoyée + values: + 'false': Non envoyée + 'true': Envoyée moderated_users: reports_reason_eq: label: Raison du signalement @@ -428,17 +442,6 @@ fr-CA: values: 'false': Validé 'true': Non validé - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Invitation acceptée - values: - 'false': Non acceptée - 'true': Acceptée - user_invitation_sent_at_not_null: - label: Invitation envoyée - values: - 'false': Non envoyée - 'true': Envoyée private_space_eq: label: Privé values: @@ -571,6 +574,53 @@ fr-CA: explanation: Les utilisateurs représentés peuvent être promu utilisateurs standard. Cela signifie qu'ils seront invités à s'inscrire sur l'application et vous ne pourrez plus agir à leur place. L'utilisateur recevra un email pour accepter votre invitation. new_managed_user_promotion: Promouvoir un utilisateur représenté en utilisateur standard promote: Promouvoir + members: + create: + error: Une erreur s'est produite lors de l'ajout d'un membre pour cet espace participatif. + success: Accès membre créé avec succès. + destroy: + error: Un problème est survenu lors de la suppression d'un membre pour cet espace participatif. + success: Accès membre supprimé avec succès. + edit: + title: Modifier le membre + update: Mettre à jour + index: + import_via_csv: Importer via CSV + publish_all: Tout publier + title: Membre + unpublish_all: Tout dépublier + new: + create: Créer + title: Nouveau membre + publish_all: + error: Un problème est survenu lors de la publication de tous les membres de cet espace participatif. + success: Tous les membres ont été publiés avec succès pour cet espace participatif + unpublish_all: + error: Une erreur s'est produite lors de la dépublication de tous les membres de cet espace participatif. + success: Tous les membres de cet espace participatif ont été dépubliés avec succès + update: + error: Un problème est survenu lors de la mise à jour du membre pour cet espace participatif. + success: Membre mis à jour avec succès + members_csv_imports: + create: + invalid: Un problème est survenu lors de la lecture du fichier CSV. Veuillez vous assurer que vous avez suivi les instructions. + success: Fichier CSV transféré avec succès, nous envoyons un courriel d'invitation aux participants. Cela peut prendre un certain temps. + new: + csv_upload: + title: Téléchargez votre fichier CSV + destroy: + button: Supprimer tous les membres + confirm: Êtes-vous sûr(e) de vouloir supprimer tous les membres ? Cette action ne peut pas être annulée, vous ne pourrez pas les récupérer. + empty: Vous n'avez aucun membre. + explanation: Vus avez %{count} membres. + title: Supprimer les membres + example_file: 'Fichier d''exemple :' + explanation: 'Téléchargez votre fichier CSV. Il doit comporter deux colonnes, sans en-têtes, avec l''email dans la première colonne et le nom dans la dernière colonne du fichier des utilisateurs que vous voulez ajouter à l''espace participatif. Évitez d''utiliser des caractères invalides comme `<>?%&^*#@()[]=+:;"{}\|` dans le nom d''utilisateur.' + explanation_example: | + john.doe@example.org%{csv_col_sep}John Doe + jane.doe@example.org%{csv_col_sep}Jane Doe + title: Importer des membres via CSV + upload: Charger menu: admin_log: Journal d'activité personnel admins: Administrateurs @@ -629,6 +679,8 @@ fr-CA: reason: Raison started_at: Commencé le user: Utilisateur + member: + name: Membre newsletter: fields: created_at: Créée le @@ -637,8 +689,6 @@ fr-CA: sent_to: Envoyé à subject: Objet name: Bulletin d'information (newsletter) - participatory_space_private_user: - name: Utilisateur de l'espace participatif scope: fields: name: Titre @@ -913,53 +963,6 @@ fr-CA: form: add: Ajouter à la liste autorisée title: Liste des domaines externes autorisés - participatory_space_private_users: - create: - error: Une erreur s'est produite lors de l'ajout d'un utilisateur pour cet espace participatif. - success: L'accès utilisateur à l'espace participatif privé a été créé avec succès. - destroy: - error: Une erreur s'est produite lors de la suppression d'un utilisateur privé pour cet espace participatif. - success: L'accès utilisateur à l'espace participatif privé a été supprimé avec succès. - edit: - title: Modifier le participant privé de l'espace participatif. - update: Mettre à jour - index: - import_via_csv: Importer via csv - publish_all: Tout publier - title: Utilisateur privé de l'espace participatif - unpublish_all: Tout dépublier - new: - create: Créer - title: Nouvel utilisateur privé de l'espace participatif. - publish_all: - error: Une erreur s'est produite lors de la publication de tous les participants privés pour cet espace participatif. - success: Tous les participants privés ont été publiés avec succès pour cet espace participatif - unpublish_all: - error: Une erreur s'est produite lors de la dépublication de tous les participants privés pour cet espace participatif. - success: Tous les participants privés ont été dépubliés avec succès pour cet espace participatif - update: - error: Une erreur s'est produite lors de la mise à jour du participant privé pour cet espace participatif. - success: Participant privé de l'espace participatif mis à jour avec succès - participatory_space_private_users_csv_imports: - create: - invalid: Un problème est survenu lors de la lecture du fichier CSV. Veuillez vous assurer que vous avez suivi les instructions. - success: Le fichier CSV a été téléchargé avec succès. Une invitation par courriel sera envoyée sous peu à chaque participant. Cela peut prendre quelques instants. - new: - csv_upload: - title: Téléchargez votre fichier CSV - destroy: - button: Supprimer tous les utilisateurs privés - confirm: Êtes-vous sûr de vouloir supprimer tous les utilisateurs privés ? Cette action est irréversible, vous ne pourrez pas les récupérer. - empty: Vous n'avez aucun utilisateur privé. - explanation: Vous avez %{count} utilisateurs privés. - title: Supprimer les utilisateurs privés - example_file: 'Fichier d''exemple :' - explanation: 'Téléchargez votre fichier CSV. Il doit avoir deux colonnes avec les emails dans la première colonne, et dans la seconde colonne les noms des utilisateurs que vous souhaitez ajouter à l''espace participatif, sans en-tête. Évitez les caractères spéciaux comme `<>?%&^*#@()[]=+:;"{}\|` dans les noms des utilisateurs.' - explanation_example: | - john.doe@example.org%{csv_col_sep}Jean Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - title: Importer des participants privés via CSV - upload: Télécharger reminders: create: error: Il y a eu un problème lors de la création des rappels. diff --git a/decidim-admin/config/locales/fr.yml b/decidim-admin/config/locales/fr.yml index 45583581c9155..b5f72f245b03f 100644 --- a/decidim-admin/config/locales/fr.yml +++ b/decidim-admin/config/locales/fr.yml @@ -33,6 +33,11 @@ fr: help_section: content: Contenu id: ID + member: + email: Email + name: Nom + member_csv_import: + file: Fichier newsletter: body: Corps de texte send_to_all_users: Envoyer à tous les participants @@ -90,11 +95,6 @@ fr: welcome_notification_body: Corps du mail de bienvenue welcome_notification_subject: Objet du mail de bienvenue youtube_handler: Gestionnaire YouTube - participatory_space_private_user: - email: Email - name: Nom - participatory_space_private_user_csv_import: - file: Fichier scope: code: Code name: Titre @@ -124,10 +124,17 @@ 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: models: + member_csv_import: + attributes: + file: + malformed: Fichier d'importation mal formé, veuillez lire attentivement les instructions et assurez-vous que le fichier est encodé en UTF-8. newsletter: attributes: base: @@ -136,10 +143,6 @@ fr: attributes: official_img_footer: allowed_file_content_types: Fichier image invalide - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Fichier d'importation mal formé, veuillez lire attentivement les instructions et assurez-vous que le fichier est encodé en UTF-8. user_group_csv_verification: attributes: file: @@ -187,12 +190,12 @@ fr: export: Exporter export-selection: Exporter la sélection import: Importer + member: + new: Nouveau membre menu_hidden: Masquer dans le menu moderate: Gérer les modérations newsletter: new: Nouvelle newsletter - participatory_space_private_user: - new: Nouvel utilisateur privé de l'espace participatif per_page: Par page permissions: Gérer les permissions restore: Restaurer @@ -413,6 +416,17 @@ fr: values: 'false': 'Non' 'true': 'Oui' + members: + user_invitation_accepted_at_not_null: + label: Invitation acceptée + values: + 'false': Non acceptée + 'true': Acceptée + user_invitation_sent_at_not_null: + label: Invitation envoyée + values: + 'false': Non envoyée + 'true': Envoyée moderated_users: reports_reason_eq: label: Raison du signalement @@ -428,17 +442,6 @@ fr: values: 'false': Validé 'true': Non validé - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Invitation acceptée - values: - 'false': Non acceptée - 'true': Acceptée - user_invitation_sent_at_not_null: - label: Invitation envoyée - values: - 'false': Non envoyée - 'true': Envoyée private_space_eq: label: Privé values: @@ -571,6 +574,53 @@ fr: explanation: Les utilisateurs représentés peuvent être promu utilisateurs standard. Cela signifie qu'ils seront invités à s'inscrire sur l'application et vous ne pourrez plus agir à leur place. L'utilisateur recevra un email pour accepter votre invitation. new_managed_user_promotion: Promouvoir un utilisateur représenté en utilisateur standard promote: Promouvoir + members: + create: + error: Une erreur s'est produite lors de l'ajout d'un membre pour cet espace participatif. + success: Accès membre créé avec succès. + destroy: + error: Un problème est survenu lors de la suppression d'un membre pour cet espace participatif. + success: Accès membre supprimé avec succès. + edit: + title: Modifier le membre + update: Mettre à jour + index: + import_via_csv: Importer via CSV + publish_all: Tout publier + title: Membre + unpublish_all: Tout dépublier + new: + create: Créer + title: Nouveau membre + publish_all: + error: Un problème est survenu lors de la publication de tous les membres de cet espace participatif. + success: Tous les membres ont été publiés avec succès pour cet espace participatif + unpublish_all: + error: Une erreur s'est produite lors de la dépublication de tous les membres de cet espace participatif. + success: Tous les membres de cet espace participatif ont été dépubliés avec succès + update: + error: Un problème est survenu lors de la mise à jour du membre pour cet espace participatif. + success: Membre mis à jour avec succès + members_csv_imports: + create: + invalid: Un problème est survenu lors de la lecture du fichier CSV. Veuillez vous assurer que vous avez suivi les instructions. + success: Fichier CSV transféré avec succès, nous envoyons un courriel d'invitation aux participants. Cela peut prendre un certain temps. + new: + csv_upload: + title: Téléchargez votre fichier CSV + destroy: + button: Supprimer tous les membres + confirm: Êtes-vous sûr(e) de vouloir supprimer tous les membres ? Cette action ne peut pas être annulée, vous ne pourrez pas les récupérer. + empty: Vous n'avez aucun membre. + explanation: Vus avez %{count} membres. + title: Supprimer les membres + example_file: 'Fichier d''exemple :' + explanation: 'Téléchargez votre fichier CSV. Il doit comporter deux colonnes, sans en-têtes, avec l''email dans la première colonne et le nom dans la dernière colonne du fichier des utilisateurs que vous voulez ajouter à l''espace participatif. Évitez d''utiliser des caractères invalides comme `<>?%&^*#@()[]=+:;"{}\|` dans le nom d''utilisateur.' + explanation_example: | + john.doe@example.org%{csv_col_sep}John Doe + jane.doe@example.org%{csv_col_sep}Jane Doe + title: Importer des membres via CSV + upload: Charger menu: admin_log: Journal d'activité personnel admins: Administrateurs @@ -629,6 +679,8 @@ fr: reason: Raison started_at: Commencé le user: Utilisateur + member: + name: Membre newsletter: fields: created_at: Créée le @@ -637,8 +689,6 @@ fr: sent_to: Envoyé à subject: Objet name: Bulletin d'information (newsletter) - participatory_space_private_user: - name: Utilisateur de l'espace participatif scope: fields: name: Titre @@ -913,53 +963,6 @@ fr: form: add: Ajouter à la liste autorisée title: Liste des domaines externes autorisés - participatory_space_private_users: - create: - error: Une erreur s'est produite lors de l'ajout d'un utilisateur pour cet espace participatif. - success: L'accès utilisateur à l'espace participatif privé a été créé avec succès. - destroy: - error: Une erreur s'est produite lors de la suppression d'un utilisateur privé pour cet espace participatif. - success: L'accès utilisateur à l'espace participatif privé a été supprimé avec succès. - edit: - title: Modifier le participant privé de l'espace participatif. - update: Mettre à jour - index: - import_via_csv: Importer via csv - publish_all: Tout publier - title: Utilisateur privé de l'espace participatif - unpublish_all: Tout dépublier - new: - create: Créer - title: Nouvel utilisateur privé de l'espace participatif. - publish_all: - error: Une erreur s'est produite lors de la publication de tous les participants privés pour cet espace participatif. - success: Tous les participants privés ont été publiés avec succès pour cet espace participatif - unpublish_all: - error: Une erreur s'est produite lors de la dépublication de tous les participants privés pour cet espace participatif. - success: Tous les participants privés ont été dépubliés avec succès pour cet espace participatif - update: - error: Une erreur s'est produite lors de la mise à jour du participant privé pour cet espace participatif. - success: Participant privé de l'espace participatif mis à jour avec succès - participatory_space_private_users_csv_imports: - create: - invalid: Un problème est survenu lors de la lecture du fichier CSV. Veuillez vous assurer que vous avez suivi les instructions. - success: Le fichier CSV a été téléchargé avec succès. Une invitation par courriel sera envoyée sous peu à chaque participant. Cela peut prendre quelques instants. - new: - csv_upload: - title: Téléchargez votre fichier CSV - destroy: - button: Supprimer tous les utilisateurs privés - confirm: Êtes-vous sûr de vouloir supprimer tous les utilisateurs privés ? Cette action est irréversible, vous ne pourrez pas les récupérer. - empty: Vous n'avez aucun utilisateur privé. - explanation: Vous avez %{count} utilisateurs privés. - title: Supprimer les utilisateurs privés - example_file: 'Fichier d''exemple :' - explanation: 'Téléchargez votre fichier CSV. Il doit avoir deux colonnes avec les emails dans la première colonne, et dans la seconde colonne les noms des utilisateurs que vous souhaitez ajouter à l''espace participatif, sans en-tête. Évitez les caractères spéciaux comme `<>?%&^*#@()[]=+:;"{}\|` dans les noms des utilisateurs.' - explanation_example: | - john.doe@example.org%{csv_col_sep}Jean Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - title: Importer des participants privés via CSV - upload: Télécharger reminders: create: error: Il y a eu un problème lors de la création des rappels. diff --git a/decidim-admin/config/locales/ga-IE.yml b/decidim-admin/config/locales/ga-IE.yml index 9fb5d2f081afa..094e97afd281c 100644 --- a/decidim-admin/config/locales/ga-IE.yml +++ b/decidim-admin/config/locales/ga-IE.yml @@ -125,13 +125,6 @@ ga: label: Cineál officialized_at_null: label: Staid - participatory_space_private_users: - user_invitation_accepted_at_not_null: - values: - 'true': Glactha - user_invitation_sent_at_not_null: - values: - 'true': Seolta private_space_eq: label: Príobháideach values: @@ -280,12 +273,6 @@ ga: social_handlers: Sóisialta url: URL youtube: YouTube - participatory_space_private_users: - new: - create: Cruthaigh - participatory_space_private_users_csv_imports: - new: - upload: Uaslódáil resource_permissions: edit: submit: Deimhnigh diff --git a/decidim-admin/config/locales/gl.yml b/decidim-admin/config/locales/gl.yml index 6c6eae640f101..0bf5b472bccac 100644 --- a/decidim-admin/config/locales/gl.yml +++ b/decidim-admin/config/locales/gl.yml @@ -136,8 +136,6 @@ gl: import: Importar newsletter: new: Novo boletín - participatory_space_private_user: - new: Novo usuario privado do espazo participativo per_page: Por páxina share: Compartir user: @@ -295,17 +293,6 @@ gl: values: 'false': Oficializado 'true': Non oficializado - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Invitación aceptada - values: - 'false': Non aceptada - 'true': Aceptada - user_invitation_sent_at_not_null: - label: Invitación enviada - values: - 'false': Sen enviar - 'true': Enviada private_space_eq: label: Privado values: @@ -411,6 +398,10 @@ gl: explanation: Os usuarios xestionados poden ser promocionados a usuarios estándar. Isto significa que serán invitados á aplicación e non poderás suplantarlles de novo. O usuario invitado recibirá un correo electrónico para aceptar a túa invitación. new_managed_user_promotion: Nova promoción de usuarios xestionados promote: Promover + members_csv_imports: + new: + csv_upload: + title: Carga o teu ficheiro CSV menu: admin_log: Rexistro de actividade do administrador admins: Administradores @@ -465,8 +456,6 @@ gl: sent_to: Enviado a subject: Asunto name: Boletín informativo - participatory_space_private_user: - name: Espazo participativo usuario privado scope: fields: name: Nome @@ -627,24 +616,6 @@ gl: update: error: Produciuse un erro ao actualizar esta organización. success: A organización actualizouse con éxito. - participatory_space_private_users: - create: - error: Produciuse un erro engadindo un usuario privado a este espazo participativo. - success: O acceso ao usuario privado do espazo participativo creouse con éxito. - destroy: - error: Produciuse un erro ao eliminar un usuario privado deste espazo participativo. - success: O acceso ao usuario privado do espazo participativo foi destruído con éxito. - index: - import_via_csv: Importar a través de CSV - title: Espazo participativo usuario privado - new: - create: Crear - title: Novo usuario privado do espazo participativo. - participatory_space_private_users_csv_imports: - new: - csv_upload: - title: Carga o teu ficheiro CSV - upload: Subir reminders: create: error: Produciuse un problema ao crear os recordatorios. diff --git a/decidim-admin/config/locales/hu.yml b/decidim-admin/config/locales/hu.yml index a2c7d600bcda1..9ce2a0b95bb0d 100644 --- a/decidim-admin/config/locales/hu.yml +++ b/decidim-admin/config/locales/hu.yml @@ -89,9 +89,6 @@ hu: welcome_notification_body: Üdvözlő értesítés szövegtörzse welcome_notification_subject: Üdvözlő üzenet tárgya youtube_handler: YouTube kezelő - participatory_space_private_user: - email: Email - name: Név scope: code: Kód name: Név @@ -133,10 +130,6 @@ hu: attributes: official_img_footer: allowed_file_content_types: Érvénytelen képfájl - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Hibás importfájl, kérjük, olvassa el figyelmesen az utasításokat, és győződjön meg róla, hogy a fájl UTF-8 kódolású. user_group_csv_verification: attributes: file: @@ -175,8 +168,6 @@ hu: import: Import newsletter: new: Új hírlevél - participatory_space_private_user: - new: Új privát felhasználó a részvételi térben per_page: Oldalanként send_me_a_test_email: Teszt e-mail küldése share: Megosztás @@ -382,17 +373,6 @@ hu: values: 'false': Hivatalos 'true': Nem hivatalos - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Meghívás elfogadva - values: - 'false': Nincs elfogadva - 'true': Elfogadva - user_invitation_sent_at_not_null: - label: Meghívó kiküldve - values: - 'false': Nincs elküldve - 'true': Elküldve private_space_eq: label: Magán values: @@ -510,6 +490,10 @@ hu: explanation: A kezelt résztvevők sztenderd résztvevőkké alakíthatóak. Ez azt jelenti, hogy meghívást kapnak az alkalmazáshoz és nem lehet őket újra kezelni. Erről emailben kapnak értesítést. new_managed_user_promotion: Új résztvevő előléptetése promote: Előléptetés + members_csv_imports: + new: + csv_upload: + title: Töltse fel a CSV fájlját menu: admin_log: Admin tevékenységnapló admins: Adminok @@ -569,8 +553,6 @@ hu: sent_to: Címzett subject: Tárgy name: Hírlevél - participatory_space_private_user: - name: Részvételi tér privát felhasználó scope: fields: name: Név @@ -764,38 +746,6 @@ hu: form: add: Hozzáadás az engedélyezett listához title: Engedélyezett külső domainek listája - participatory_space_private_users: - create: - error: Hiba történt egy privát felhasználó hozzáadása során a részvételi helyhez. - success: Magán résztvevő részvételi helyhez való hozzáférése biztosítva. - destroy: - error: Hiba történt egy privát felhasználó törlésével a részvételi helyen. - success: Privát felhasználó részvételi helyhez való hozzáférése törölve. - index: - import_via_csv: Importálás CSV -ből - title: Részvételi tér privát felhasználója - new: - create: Létrehozás - title: Új részvételi tér privát felhasználója. - participatory_space_private_users_csv_imports: - create: - invalid: Probléma adódott a CSV fájl beolvasásával. Kérjük, győződjön meg róla, hogy követte az utasításokat. - success: A CSV fájl sikeresen feltöltődött, meghívó e-mailt küldünk a résztvevőknek. Ez eltarthat egy ideig. - new: - csv_upload: - title: Töltse fel a CSV fájlját - destroy: - button: Az összes privát felhasználó törlése - confirm: Biztos, hogy törölni szeretné az összes privát résztvevőt? Ezt a műveletet nem lehet visszacsinálni, nem tudja majd visszaállítani őket. - empty: Nincsenek privát résztvevők. - explanation: '%{count} privát résztvevője van.' - title: Privát felhasználók törlése - example_file: 'Példa fájl:' - explanation: 'Töltse fel a CSV-fájlt. Két oszlopnak kell lennie, a fájl első oszlopában az e-mail címmel, a fájl utolsó oszlopában pedig a nevével azoknak a felhasználóknak, akiket hozzá szeretne adni a részvételi térhez, fejlécek nélkül. Kerülje az olyan érvénytelen karakterek használatát, mint a `<>?%&^*#@()[]=+:;"{}\|` a felhasználói névben.' - explanation_example: | - ohn.doe@example.org%{csv_col_sep}John Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - upload: Feltölt reminders: create: error: Hiba történt a bejegyzés létrehozása során. diff --git a/decidim-admin/config/locales/id-ID.yml b/decidim-admin/config/locales/id-ID.yml index f8ba11fe5883b..b14eae5265078 100644 --- a/decidim-admin/config/locales/id-ID.yml +++ b/decidim-admin/config/locales/id-ID.yml @@ -252,6 +252,10 @@ id: explanation: Pengguna yang dikelola dapat dipromosikan ke pengguna standar. Ini berarti mereka akan diundang ke aplikasi dan Anda tidak akan dapat meniru mereka lagi. Pengguna yang diundang akan menerima email untuk menerima undangan Anda. new_managed_user_promotion: Promosi pengguna terkelola baru promote: Memajukan + members_csv_imports: + new: + csv_upload: + title: Unggah file CSV Anda menu: admin_log: Log aktivitas admin admins: Admin @@ -304,8 +308,6 @@ id: sent_at: Dikirim pada subject: Subyek name: Newsletter - participatory_space_private_user: - name: Ruang partisipatif pengguna pribadi scope: fields: name: Nama @@ -394,22 +396,6 @@ id: update: error: Terjadi kesalahan saat memperbarui organisasi ini. success: Organisasi berhasil diperbarui. - participatory_space_private_users: - create: - error: Terjadi kesalahan saat menambahkan pengguna pribadi untuk ruang partisipatif ini. - success: Ruang partisipatif akses pengguna pribadi berhasil dibuat. - destroy: - error: Terjadi kesalahan saat menghapus pengguna pribadi untuk ruang partisipatif ini. - success: Akses pribadi pengguna ruang partisipatif berhasil dihancurkan. - index: - title: Ruang partisipatif pengguna pribadi - new: - create: Membuat - title: Pengguna Pribadi Ruang Partisipatif Baru. - participatory_space_private_users_csv_imports: - new: - csv_upload: - title: Unggah file CSV Anda resource_permissions: edit: submit: Menyerahkan diff --git a/decidim-admin/config/locales/is-IS.yml b/decidim-admin/config/locales/is-IS.yml index 4c018a35817b4..47a18659a6241 100644 --- a/decidim-admin/config/locales/is-IS.yml +++ b/decidim-admin/config/locales/is-IS.yml @@ -359,9 +359,6 @@ is-IS: youtube: Youtube update: success: Skipulag uppfærður með góðum árangri. - participatory_space_private_users: - new: - create: Búa til resource_permissions: edit: title: Breyta heimildum diff --git a/decidim-admin/config/locales/it.yml b/decidim-admin/config/locales/it.yml index 798fa3ebc83b2..ff9989aa948f8 100644 --- a/decidim-admin/config/locales/it.yml +++ b/decidim-admin/config/locales/it.yml @@ -89,11 +89,6 @@ it: welcome_notification_body: Testo del messaggio di Benvenuto welcome_notification_subject: Oggetto della mail con messaggio di Benvenuto youtube_handler: Gestore di YouTube - participatory_space_private_user: - email: Email - name: Nome - participatory_space_private_user_csv_import: - file: File scope: code: Codice name: Nome @@ -135,10 +130,6 @@ it: attributes: official_img_footer: allowed_file_content_types: File immagine non valido - participatory_space_private_user_csv_import: - attributes: - file: - malformed: File di importazione malformato, leggere attentamente le istruzioni e assicurarsi che il file sia codificato UTF-8. user_group_csv_verification: attributes: file: @@ -186,8 +177,6 @@ it: import: Importazione newsletter: new: Nuova newsletter - participatory_space_private_user: - new: Nuovo utente dello spazio partecipativo privato per_page: Per pagina restore: Ripristina send_me_a_test_email: Inviami una email di prova @@ -418,17 +407,6 @@ it: values: 'false': Ufficializzato 'true': Non ufficializzato - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Invito accettato - values: - 'false': Non accettato - 'true': Accettato - user_invitation_sent_at_not_null: - label: Invito inviato - values: - 'false': Non inviato - 'true': Inviato private_space_eq: label: Privato values: @@ -560,6 +538,10 @@ it: explanation: I partecipanti gestiti possono essere promossi a partecipanti standard. Significa che saranno invitati ad effettuare la richiesta e non sarai in grado di gestirli di nuovo. Il partecipante invitato riceverà un'email per accettare il tuo invito. new_managed_user_promotion: Nuova promozione di partecipanti gestiti promote: Promuovere + members_csv_imports: + new: + csv_upload: + title: Carica il tuo file CSV menu: admin_log: Registro delle attività di amministrazione admins: Amministratori @@ -626,8 +608,6 @@ it: sent_to: Inviato a subject: Oggetto name: Newsletter - participatory_space_private_user: - name: Partecipante privato allo spazio partecipativo scope: fields: name: Nome @@ -895,53 +875,6 @@ it: form: add: Aggiungi alla lista consentita title: Elenco di domini esterni consentiti - participatory_space_private_users: - create: - error: Si è verificato un errore durante l'aggiunta di un utente privato per questo spazio partecipativo. - success: Accesso utente privato spazio partecipativo creato con successo. - destroy: - error: Si è verificato un errore durante l'eliminazione di un utente privato per questo spazio partecipativo. - success: Accesso utente privato spazio distruttivo distrutto con successo. - edit: - title: Modifica partecipante privato allo spazio partecipativo. - update: Aggiorna - index: - import_via_csv: Importa da CSV - publish_all: Pubblica tutto - title: Utente privato dello spazio partecipativo - unpublish_all: Annulla la pubblicazione di tutto - new: - create: Creare - title: Nuovo utente privato dello spazio partecipativo. - publish_all: - error: Si è verificato un errore durante l'aggiunta degli utenti privati per questo spazio partecipativo. - success: Ha pubblicato con successo tutti i partecipanti privati per questo spazio partecipativo - unpublish_all: - error: Si è verificato un problema di annullamento della pubblicazione di tutti i partecipanti privati per questo spazio partecipativo. - success: Tutti i partecipanti privati per questo spazio partecipativo sono stati rimossi dalla pubblicazione con successo - update: - error: Si è verificato un errore durante l'aggiornamento dell'utente privato per questo spazio partecipativo. - success: Utente privato dello spazio partecipativo aggiornato con successo - participatory_space_private_users_csv_imports: - create: - invalid: Si è verificato un problema durante la lettura del file CSV. Assicurati di aver seguito le istruzioni. - success: File CSV caricato con successo, stiamo inviando un'email di invito ai partecipanti. Potrebbe richiedere un po' di tempo. - new: - csv_upload: - title: Carica il tuo file CSV - destroy: - button: Elimina tutti i partecipanti privati - confirm: Sei sicuro di voler eliminare tutti i partecipanti privati? Questa azione non può essere annullata, non sarai in grado di recuperarli. - empty: Non hai partecipanti privati. - explanation: Hai %{count} partecipanti privati. - title: Elimina tutti i partecipanti privati - example_file: 'File di esempio:' - explanation: 'Carica il file CSV. Il file deve contenere due colonne (e-mail e nome) senza intestazioni: nella prima sono elencati gli indirizzi e-mail degli utenti che si desidera aggiungere allo spazio partecipativo, nella seconda i loro nomi. Evita di usare caratteri non validi come `<>?%&^*#@()[]=+:;"{}\|` nella colonna dei nomi.' - explanation_example: | - john.doe@example.org%{csv_col_sep}John Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - title: Importa partecipanti privati tramite CSV - upload: Carica File reminders: create: error: Si è verificato un errore durante la creazione dei promemoria. diff --git a/decidim-admin/config/locales/ja.yml b/decidim-admin/config/locales/ja.yml index 4b8dcfef4ca01..61c0034ee41e5 100644 --- a/decidim-admin/config/locales/ja.yml +++ b/decidim-admin/config/locales/ja.yml @@ -33,8 +33,14 @@ ja: help_section: content: 内容 id: ID + member: + email: メールアドレス + name: 名前 + member_csv_import: + file: ファイル newsletter: body: 本文 + send_to_all_users: 全参加者に送信 send_to_followers: フォロワーに送信 send_to_participants: 参加者に送信 subject: 件名 @@ -90,11 +96,6 @@ ja: welcome_notification_body: ウェルカム通知本文 welcome_notification_subject: ウェルカム通知の件名 youtube_handler: YouTube ハンドラー - participatory_space_private_user: - email: Eメールアドレス - name: 名前 - participatory_space_private_user_csv_import: - file: ファイル scope: code: コード name: 名前 @@ -124,10 +125,17 @@ ja: show_in_footer: フッターに表示 title: タイトル weight: 順番の位置 + taxonomy: + item_name: アイテム名 + parent_id: 親 user_group_csv_verification: file: ファイル errors: models: + member_csv_import: + attributes: + file: + malformed: インポートファイルの形式が正しくありません。指示内容をよく読み、ファイルがUTF-8でエンコードされていることを確認してください。 newsletter: attributes: base: @@ -136,10 +144,6 @@ ja: attributes: official_img_footer: allowed_file_content_types: 無効な画像ファイル - participatory_space_private_user_csv_import: - attributes: - file: - malformed: インポートファイルの形式が正しくありません。手順を見直して、ファイルがUTF-8でエンコードされていることを確認してください。 user_group_csv_verification: attributes: file: @@ -187,12 +191,12 @@ ja: export: エクスポート export-selection: 選択したものをエクスポート import: インポート + member: + new: 新規メンバー menu_hidden: メニューから隠す moderate: モデレーションの管理 newsletter: new: 新しいニュースレター - participatory_space_private_user: - new: 新しい参加型スペースのプライベートユーザー per_page: ページごと permissions: 権限の管理 restore: 復元 @@ -292,11 +296,13 @@ ja: block_user: bulk_new: action: アカウントをブロックして理由を送信 + already_reported_html: この操作を続行すると、参加者のコンテンツもすべて非表示になります。 description: ユーザーをブロックすると、そのアカウントは利用できなくなります。ユーザーのブロックを解除することを検討する方法に関するガイドラインを判定通知に含めることができます。 justification: 判定理由 title: ユーザーをブロック new: action: アカウントをブロックして理由を送信 + already_reported_html: この操作を続行すると、参加者のコンテンツもすべて非表示になります。 description: ユーザーをブロックすると、そのアカウントは利用できなくなります。ユーザーのブロックを解除することを検討する方法に関するガイドラインを判定通知に含めることができます。 justification: 判定理由 title: ユーザー %{name} をブロックする @@ -389,6 +395,7 @@ ja: form: domain_too_short: ドメインが短すぎます update: + error: 許可された外部ドメインのリストを更新できませんでした。 success: 許可された外部ドメインのリストを更新しました。 exports: export_as: "%{name} を %{export_format} 形式で取得" @@ -410,6 +417,17 @@ ja: values: 'false': 'いいえ' 'true': 'はい' + members: + user_invitation_accepted_at_not_null: + label: 招待が承認されました + values: + 'false': 未承認 + 'true': 承認済み + user_invitation_sent_at_not_null: + label: 招待状を送信しました + values: + 'false': 未送信 + 'true': 送信済み moderated_users: reports_reason_eq: label: 理由を報告 @@ -425,17 +443,6 @@ ja: values: 'false': 公式化済み 'true': 公式化されていない - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: 招待が承認されました - values: - 'false': 未承認 - 'true': 承認済み - user_invitation_sent_at_not_null: - label: 招待状の送信 - values: - 'false': 未送信 - 'true': 送信済み private_space_eq: label: プライベート values: @@ -473,6 +480,7 @@ ja: import_csv: explanation: 'ファイルのガイダンス:' message_1: CSVファイルがサポートされています + message_2: "電子メールデータを含む.csvファイル" help_sections: error: ヘルプセクションの更新中に問題が発生しました。 form: @@ -563,6 +571,53 @@ ja: explanation: 管理対象参加者は通常の参加者に昇格することができます。 その場合、彼らはアプリケーションに招待され、あなたはそれらを再び管理することはできなくなります。 招待された参加者は、招待を承諾するためのメールを受け取ります。 new_managed_user_promotion: 新しい管理対象参加者の昇格 promote: プロモート + members: + create: + error: 参加型スペースにメンバーを追加する際に問題が発生しました。 + success: メンバーを作成しました。 + destroy: + error: 参加型スペースのメンバーの削除中に問題が発生しました。 + success: メンバーを削除しました。 + edit: + title: メンバーを編集 + update: 更新 + index: + import_via_csv: CSV形式でインポート + publish_all: すべて公開 + title: メンバー + unpublish_all: すべて非公開 + new: + create: 作成 + title: 新規メンバー + publish_all: + error: 参加スペースのすべてのメンバーを公開する際に問題が発生しました。 + success: 参加スペースのすべてのメンバーを公開しました + unpublish_all: + error: 参加スペースのすべてのメンバーを非公開にする際に問題が発生しました。 + success: 参加スペースのすべてのメンバーを非公開にしました + update: + error: 参加スペースのメンバーを更新する際に問題がありました。 + success: メンバーを更新しました + members_csv_imports: + create: + invalid: CSVファイルの読み込み中に問題が発生しました。指示に従って正しく操作されているかご確認ください。 + success: CSVファイルのアップロードが完了しました。参加者に招待メールを送信中です。完了までしばらくお待ちください。 + new: + csv_upload: + title: CSVファイルをアップロード + destroy: + button: すべてのメンバーを削除 + confirm: すべてのメンバーを削除してもよろしいですか?この操作は元に戻せません。復元することはできません。 + empty: メンバーがいません。 + explanation: '%{count} 名のメンバーがいます。' + title: メンバーを削除 + example_file: 'サンプルファイル:' + explanation: "CSVファイルをアップロードしてください。ファイルには2つの列が必要で、1列目にはメールアドレス、最後の列には参加スペースに追加したいユーザーの名前を入力してください。ヘッダー行は不要です。ユーザー名には、`<>?%&^*#@()[]=+:;\"{}\\|`などの無効な文字を使用しないでください。\n" + explanation_example: | + john.doe@example.org%{csv_col_sep}John Doe + jane.doe@example.org%{csv_col_sep}Jane Doe + title: CSV形式でメンバーをインポート + upload: アップロード menu: admin_log: 管理者アクティビティログ admins: 管理者 @@ -621,6 +676,8 @@ ja: reason: 理由: started_at: 開始日時 user: 参加者 + member: + name: メンバー newsletter: fields: created_at: 作成日時 @@ -629,8 +686,6 @@ ja: sent_to: '送信先:' subject: 件名 name: ニュースレター - participatory_space_private_user: - name: 参加型スペースプライベート参加者 scope: fields: name: 名前 @@ -905,53 +960,6 @@ ja: form: add: 許可されたリストに追加 title: 許可された外部ドメインのリスト - participatory_space_private_users: - create: - error: この参加型スペースにプライベート参加者を追加する際に問題が発生しました。 - success: 参加型スペースのプライベート参加者アクセスが正常に作成されました。 - destroy: - error: この参加型スペースのプライベート参加者を削除する際に問題が発生しました。 - success: 参加型スペースのプライベート参加者アクセスが正常に破棄されました。 - edit: - title: 参加スペースの非公開参加者を編集します。 - update: 更新 - index: - import_via_csv: CSV形式でインポート - publish_all: すべて公開 - title: 参加型スペースプライベート参加者 - unpublish_all: すべて非公開 - new: - create: 作成 - title: 新しい参加型スペースのプライベート参加者。 - publish_all: - error: この参加型スペースのすべてのプライベート参加者を公開する際に問題が発生しました。 - success: この参加型スペースのすべてのプライベート参加者を公開しました - unpublish_all: - error: この参加型スペースのすべてのプライベート参加者を非公開にする際に問題が発生しました。 - success: この参加型スペースのすべてのプライベート参加者を非公開にしました - update: - error: この参加型スペースのプライベート参加者の更新中に問題が発生しました。 - success: 参加型スペースのプライベート参加者を更新しました - participatory_space_private_users_csv_imports: - create: - invalid: CSVファイルの読み込み中に問題が発生しました。手順を確認してください。 - success: CSVファイルは正常にアップロードされました。参加者に招待メールを送信しています。しばらくお待ちください。 - new: - csv_upload: - title: CSVファイルをアップロード - destroy: - button: すべてのプライベート参加者を削除 - confirm: すべてのプライベート参加者を削除してもよろしいですか?この操作は元に戻せず、削除された方を復元することはできません。 - empty: プライベートの参加者はいません。 - explanation: '%{count} 人のプライベート参加者がいます。' - title: プライベート参加者を削除 - example_file: 'サンプルファイル:' - explanation: 'CSV ファイルをアップロードします。 CSVにはヘッダをつけず、最初のカラムにはメールアドレスを、次のカラムにはユーザー名を入れた2カラムのCSVとして、参加スペースに追加したいユーザーの情報を並べてください。 ユーザー名には `<>?%&^*#@()[]=+:;"{}\|`のような無効な文字を使用しないでください。' - explanation_example: | - john.doe@example.org%{csv_col_sep}John Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - title: プライベート参加者をCSV形式でインポート - upload: アップロード reminders: create: error: リマインダー作成中に問題が発生しました。 @@ -1257,6 +1265,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/kaa.yml b/decidim-admin/config/locales/kaa.yml index 0b3071af7cc60..5426820b34eef 100644 --- a/decidim-admin/config/locales/kaa.yml +++ b/decidim-admin/config/locales/kaa.yml @@ -12,8 +12,6 @@ kaa: name: Ataması organization: name: Ataması - participatory_space_private_user: - email: Elektron pochta user_group_csv_verification: file: Fayl activerecord: @@ -144,12 +142,6 @@ kaa: instagram: Instagram url: URL youtube: YouTube - participatory_space_private_users: - new: - create: Jaratıw - participatory_space_private_users_csv_imports: - new: - upload: Júklew reminders: new: submit: Jiberiw diff --git a/decidim-admin/config/locales/ko.yml b/decidim-admin/config/locales/ko.yml index 51900de0a8190..f5487f68cdb53 100644 --- a/decidim-admin/config/locales/ko.yml +++ b/decidim-admin/config/locales/ko.yml @@ -66,9 +66,6 @@ ko: twitter_handler: X handler warning_color: 경고 youtube_handler: YouTube handler - participatory_space_private_user: - email: 이메일 - name: 이름 scope: code: 코드 name: 이름 @@ -106,10 +103,6 @@ ko: attributes: official_img_footer: allowed_file_content_types: 잘못된 이미지 파일 - participatory_space_private_user_csv_import: - attributes: - file: - malformed: 잘못된 형식의 가져오기 파일입니다. 설명서를 잘 읽고 UTF-8이 인코딩되었는지 확인하십시오. user_group_csv_verification: attributes: file: @@ -145,8 +138,6 @@ ko: import: 가져오기 newsletter: new: 새 뉴스레터 - participatory_space_private_user: - new: 새 참여 공간 비공개 사용자 per_page: 페이지 당 send_me_a_test_email: 나에게 테스트 메일 보내기 share: 공유 @@ -306,10 +297,6 @@ ko: label: 유형 officialized_at_null: label: 상태 - participatory_space_private_users: - user_invitation_sent_at_not_null: - values: - 'true': 전송됨 private_space_eq: label: 비공개 values: @@ -429,8 +416,6 @@ ko: sent_to: 다음에 전송하였습니다 subject: 주제 name: 뉴스 레터 - participatory_space_private_user: - name: 참여공간 비공개참여 scope: fields: name: 이름 @@ -531,20 +516,6 @@ ko: hidden: 숨겨짐 show: 표시 title: 참가자의 이메일 주소 보기 - participatory_space_private_users: - new: - create: 생성 - participatory_space_private_users_csv_imports: - new: - csv_upload: - title: CSV 파일 업로드 - destroy: - button: 모든 비공개 참가자 삭제 - confirm: 모든 비공개 참가자를 삭제하시겠습니까? 이 작업은 취소할 수 없습니다. 복구할 수 없습니다. - empty: 비공개 참가자가 없습니다. - explanation: '%{count} 개의 비공개 참가자가 있습니다.' - title: 비공개 참가자 삭제 - example_file: '예제 파일:' reminders: create: error: 리마인더를 만드는 중 문제가 발생했습니다. diff --git a/decidim-admin/config/locales/lb.yml b/decidim-admin/config/locales/lb.yml index ef1b121a60f0b..3bae6f46848b3 100644 --- a/decidim-admin/config/locales/lb.yml +++ b/decidim-admin/config/locales/lb.yml @@ -139,8 +139,6 @@ lb: import: Importieren newsletter: new: Neuer Newsletter - participatory_space_private_user: - new: Neuer privater Benutzer per_page: Pro Seite share: Teilen area_types: @@ -291,17 +289,6 @@ lb: values: 'false': Offizialisiert 'true': Nicht offiziell - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Einladung akzeptiert - values: - 'false': Nicht akzeptiert - 'true': Akzeptiert - user_invitation_sent_at_not_null: - label: Einladung versendet - values: - 'false': Nicht versendet - 'true': Versendet private_space_eq: label: Privat values: @@ -378,6 +365,10 @@ lb: explanation: Verwaltete Benutzer können zu Standardbenutzern heraufgestuft werden. Das bedeutet, dass sie zu der Anwendung eingeladen werden und nicht in der Lage sind, sie erneut zu repräsentieren. Der eingeladene Benutzer erhält eine E-Mail, um Ihre Einladung anzunehmen. new_managed_user_promotion: Neue verwaltete Benutzerwerbung promote: Fördern + members_csv_imports: + new: + csv_upload: + title: Laden Sie Ihre CSV-Datei hoch menu: admin_log: Admin-Aktivitätsprotokoll admins: Admins @@ -432,8 +423,6 @@ lb: sent_to: Gesendet an subject: Gegenstand name: Newsletter - participatory_space_private_user: - name: Participatory Space privater Benutzer scope: fields: name: Name @@ -597,24 +586,6 @@ lb: update: error: Beim Aktualisieren dieser Organisation ist ein Fehler aufgetreten. success: Die Organisation wurde erfolgreich aktualisiert. - participatory_space_private_users: - create: - error: Beim Hinzufügen eines privaten Benutzers für diesen partizipativen Bereich ist ein Fehler aufgetreten. - success: Participatory Space Privater Benutzerzugriff erfolgreich erstellt. - destroy: - error: Beim Löschen eines privaten Benutzers für diesen partizipativen Bereich ist ein Fehler aufgetreten. - success: Participatory Space Privater Benutzerzugriff wurde erfolgreich zerstört. - index: - import_via_csv: Aus CSV-Datein importieren - title: Participatory Space privater Benutzer - new: - create: Erstellen - title: Neuer privater Benutzer des Participatory Space. - participatory_space_private_users_csv_imports: - new: - csv_upload: - title: Laden Sie Ihre CSV-Datei hoch - upload: Hochladen resource_permissions: edit: submit: einreichen diff --git a/decidim-admin/config/locales/lt.yml b/decidim-admin/config/locales/lt.yml index 36d9e2c2459e5..b16095b138b4b 100644 --- a/decidim-admin/config/locales/lt.yml +++ b/decidim-admin/config/locales/lt.yml @@ -86,9 +86,6 @@ lt: welcome_notification_body: Pasisveikinimo pranešimo tekstas welcome_notification_subject: Pasisveikinimo pranešimo antraštė youtube_handler: '„YouTube“ tvarkyklė' - participatory_space_private_user: - email: El. paštas - name: Pavadinimas scope: code: Kodas name: Pavadinimas @@ -129,10 +126,6 @@ lt: attributes: official_img_footer: allowed_file_content_types: Netinkama vaizdo rinkmena - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Blogai suformuotas importo failas, prašome atidžiai perskaityti instrukcijas ir įsitikinkite, kad failas yra UTF-8 koduotėje. user_group_csv_verification: attributes: file: @@ -170,8 +163,6 @@ lt: import: Importuoti newsletter: new: Naujas naujienlaiškis - participatory_space_private_user: - new: Naujas dalyvaujamosios erdvės privatus naudotojas per_page: Per puslapį send_me_a_test_email: Siųsti man bandomąjį el. laišką share: Dalintis @@ -358,17 +349,6 @@ lt: values: 'false': Oficializuota 'true': Neoficializuota - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Pakvietimas priimtas - values: - 'false': Nepriimtas - 'true': Priimtas - user_invitation_sent_at_not_null: - label: Pakvietimas išsiųstas - values: - 'false': Neišsiųstas - 'true': Išsiųstas private_space_eq: label: Privatus values: @@ -494,6 +474,10 @@ lt: explanation: Tvarkomi dalyviai gali būti paaukštinti į standartinius dalyvius. Tai reiškia, kad jie bus pakviesti į platformą ir nebegalėsite jų tvarkyti. Pakviesti naudotojai gaus kvietimą prisijungti el. paštu. new_managed_user_promotion: Naujas tvarkomo naudotojo paaukštinimas promote: Paaukštinti + members_csv_imports: + new: + csv_upload: + title: Įkelkite savo CSV rinkmeną menu: admin_log: Administratoriaus veiklos žurnalas admins: Administratoriai @@ -551,8 +535,6 @@ lt: sent_to: Išsiųsta subject: Tema name: Naujienlaiškis - participatory_space_private_user: - name: Dalivaujamosios erdvės privatus dalyvis scope: fields: name: Vardas @@ -733,38 +715,6 @@ lt: update: error: Atnaujinant šią organizaciją iškilo problema. success: Organizacija atnaujinta. - participatory_space_private_users: - create: - error: Pridedant privatų dalyvį į šia dalyvaujamają erdvę kilo problema. - success: Dalyvaujamosios erdvės privataus dalyvio prieiga buvo sukurta sėkmingai. - destroy: - error: Ištrinant privatų dalyvį iš dalyvaujamosios erdvės kilo problema. - success: Dalyvaujamojo proceso erdvės privataus dalyvio prieiga panaikinta. - index: - import_via_csv: Importuoti iš CSV - title: Dalyvaujamosios erdvės privatus dalyvis - new: - create: Sukurti - title: Naujas dalyvaujamosios erdvės privatus dalyvis. - participatory_space_private_users_csv_imports: - create: - invalid: Skaitant CSV failą kilo problema. Prašau įsitikinkite kad sekėte instrukcijas. - success: CSV rinkmena įkelta, dalyviams siunčiame kvietimą e. laišku. Tai gali užtrukti. - new: - csv_upload: - title: Įkelkite savo CSV rinkmeną - destroy: - button: Ištrinti visus privačius dalyvius - confirm: Ar tikrai norite ištrinti visus privačius dalyvius? Atlikus šį veiksmą nebebus įmanoma sugražinti šių dalyvių į platformą. - empty: Neturite privačių dalyvių. - explanation: Turite %{count} privačių dalyvių. - title: Ištrinti privačius dalyvius - example_file: 'Pavyzdinė rinkmena:' - explanation: 'Įkelkite CSV dokumentą. Jis turi turėti du stulpelius su naudotojų el. paštu pirmajame ir vardu paskutiniąjame. Dokumente neturėtų būti laukų pavadinimų ir šių simbolių naudotojų varduose "<>?%&^*#@()[]=+:;"{}\|".' - explanation_example: | - john.doe@example.org%{csv_col_sep}John Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - upload: Įkelti reminders: create: error: Kuriant priminimus iškilo problema. diff --git a/decidim-admin/config/locales/lv.yml b/decidim-admin/config/locales/lv.yml index dab5bfd694b3d..acb955ed562d1 100644 --- a/decidim-admin/config/locales/lv.yml +++ b/decidim-admin/config/locales/lv.yml @@ -274,6 +274,10 @@ lv: explanation: Pārvaldītos dalībniekus var paaugstināt par standarta dalībniekiem. Tas nozīmē, ka viņi tiks uzaicināti uz lietojumprogrammu un jūs viņus vairs nevarēsiet pārvaldīt. Uzaicinātais dalībnieks saņems e-pastu ar jūsu ielūgumu. new_managed_user_promotion: Jauna pārvaldīta dalībnieka paaugstināšana promote: Paaugstināt + members_csv_imports: + new: + csv_upload: + title: Augšupielādējiet savu CSV failu menu: admin_log: Administratora darbību žurnāls admins: Administratori @@ -326,8 +330,6 @@ lv: sent_to: Nosūtīts subject: Temats name: Informatīvais biļetens - participatory_space_private_user: - name: Līdzdalības telpas privāts dalībnieks scope: fields: name: Nosaukums @@ -442,23 +444,6 @@ lv: update: error: Šīs organizācijas atjaunināšanas laikā radās problēma. success: Organizācija ir veiksmīgi atjaunināta. - participatory_space_private_users: - create: - error: Pievienojot privāto dalībnieku šai līdzdalības telpai, radās problēma. - success: Privāto dalībnieku piekļuve līdzdalības telpai ir veiksmīgi izveidota. - destroy: - error: Dzēšot privāto dalībnieku no šīs līdzdalības telpas, radās problēma. - success: Privāto dalībnieku piekļuve līdzdalības telpai ir veiksmīgi dzēsta. - index: - title: Līdzdalības telpas privāts dalībnieks - new: - create: Izveidot - title: Jauns līdzdalības telpas privāts dalībnieks - participatory_space_private_users_csv_imports: - new: - csv_upload: - title: Augšupielādējiet savu CSV failu - upload: Augšupielādēt resource_permissions: edit: submit: Iesniegt diff --git a/decidim-admin/config/locales/nl.yml b/decidim-admin/config/locales/nl.yml index ad6d30bc45549..21ed0d536ba28 100644 --- a/decidim-admin/config/locales/nl.yml +++ b/decidim-admin/config/locales/nl.yml @@ -139,8 +139,6 @@ nl: import: Importeren newsletter: new: Nieuwe nieuwsbrief - participatory_space_private_user: - new: Nieuwe privégebruiker van de inspraakruimte per_page: Per pagina share: Deel user: @@ -302,17 +300,6 @@ nl: values: 'false': Gevalideerd 'true': Niet gevalideerd - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Uitnodiging geaccepteerd - values: - 'false': Niet geaccepteerd - 'true': Geaccepteerd - user_invitation_sent_at_not_null: - label: Uitnodiging verzonden - values: - 'false': Niet verzonden - 'true': Verzonden private_space_eq: label: Privé values: @@ -425,6 +412,10 @@ nl: explanation: Beheerde deelnemers kunnen worden gepromoveerd tot standaarddeelnemers. Dit betekent dat ze worden uitgenodigd voor de toepassing en dat u ze niet meer kunt beheren. De uitgenodigde deelnemer ontvangt een e-mail om uw uitnodiging te accepteren. new_managed_user_promotion: Nieuwe promotie voor beheerde deelnemers promote: Promoten + members_csv_imports: + new: + csv_upload: + title: Upload uw CSV-bestand menu: admin_log: Admin activiteitenlogboek admins: Admins @@ -479,8 +470,6 @@ nl: sent_to: Verzonden aan subject: Onderwerp name: Nieuwsbrief - participatory_space_private_user: - name: Participerende ruimte privé deelnemer scope: fields: name: Naam @@ -644,29 +633,6 @@ nl: update: error: Er is een fout opgetreden bij het bijwerken van deze organisatie. success: Organisatie is succesvol bijgewerkt. - participatory_space_private_users: - create: - error: Er is een probleem opgetreden bij het toevoegen van een privédeelnemer aan deze deelruimte. - success: De toegang van de privé deelnemer voor de burgerinspraak is succesvol aangemaakt. - destroy: - error: Er was een probleem met het verwijderen van een privé-deelnemer voor deze participatieruimte. - success: De toegang van de privé deelnemer voor burgerinspraak is met succes verwijderd. - index: - import_via_csv: Importeren via csv - title: Participerende ruimte voor privé deelnemer - new: - create: creëren - title: Nieuwe privé deelnemer burgerinspraak. - participatory_space_private_users_csv_imports: - new: - csv_upload: - title: Upload uw CSV-bestand - destroy: - button: Verwijder alle privé deelnemers - empty: Je hebt geen privé deelnemers. - explanation: Je hebt %{count} privédeelnemers. - title: Verwijder privé deelnemer - upload: Upload reminders: create: error: Er is een probleem opgetreden bij het maken van herinneringen. diff --git a/decidim-admin/config/locales/no.yml b/decidim-admin/config/locales/no.yml index afe2affff2bff..e5e3f5871cea1 100644 --- a/decidim-admin/config/locales/no.yml +++ b/decidim-admin/config/locales/no.yml @@ -139,8 +139,6 @@ import: Importer newsletter: new: Nytt nyhetsbrev - participatory_space_private_user: - new: Nytt deltakerområde for privat bruker per_page: Per side share: Del user: @@ -314,17 +312,6 @@ values: 'false': Offisialisert 'true': Ikke offisialisert - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Invitasjon godtatt - values: - 'false': Ikke akseptert - 'true': Godkjent - user_invitation_sent_at_not_null: - label: Invitasjon sendt - values: - 'false': Ikke sendt - 'true': Sendt private_space_eq: label: Privat values: @@ -430,6 +417,10 @@ explanation: Styrte deltakere kan bli promotert til standard deltakere. Dette betyr at de vil bli invitert til applikasjonen og du vil ikke kunne administrere dem lenger. De inviterte deltakerene vil motta en email så de kan akseptere invitasjonen. new_managed_user_promotion: Ny administrert deltaker forfremmelse promote: Promoter + members_csv_imports: + new: + csv_upload: + title: Last opp din CSV fil menu: admin_log: Admin aktivitet logg admins: Adminer @@ -484,8 +475,6 @@ sent_to: Send til subject: Emne name: Nyhetsbrev - participatory_space_private_user: - name: Deltakerområdets private deltaker scope: fields: name: Navn @@ -649,29 +638,6 @@ update: error: Det oppstod et problem med å oppdatere denne organisasjon. success: Organisasjonen ble oppdatert. - participatory_space_private_users: - create: - error: Det oppstod et problem med å legge til en privat deltaker for dette deltakerområdet. - success: Deltakerområde privat deltaker tilgang opprettet. - destroy: - error: Det oppstod et problem med å slette en privat deltaker for dette deltakerområdet. - success: Deltakerområde privat deltaker tilgang ødelagt. - index: - import_via_csv: Importer fra CSV - title: Deltakerområdets private deltaker - new: - create: Opprett - title: Ny deltaker område privat deltaker. - participatory_space_private_users_csv_imports: - new: - csv_upload: - title: Last opp din CSV fil - destroy: - button: Slett alle private deltakere - empty: Du har ingen private deltakere. - explanation: Du har %{count} private deltakere. - title: Slett private deltakere - upload: Last opp reminders: create: error: Det oppsto et problem med å opprette påminnelser. diff --git a/decidim-admin/config/locales/pl.yml b/decidim-admin/config/locales/pl.yml index dceff2ab830ee..48761c99868cd 100644 --- a/decidim-admin/config/locales/pl.yml +++ b/decidim-admin/config/locales/pl.yml @@ -90,9 +90,6 @@ pl: welcome_notification_body: Treść komunikatu powitalnego welcome_notification_subject: Tytuł komunikatu powitalnego youtube_handler: Handler YouTube - participatory_space_private_user: - email: E-mail - name: Nazwa scope: code: Kod name: Nazwa @@ -134,10 +131,6 @@ pl: attributes: official_img_footer: allowed_file_content_types: Nieprawidłowy plik obrazu - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Nieprawidłowy plik importu, przeczytaj uważnie instrukcję i upewnij się, że plik jest kodowany w UTF-8. user_group_csv_verification: attributes: file: @@ -176,8 +169,6 @@ pl: import: Importuj newsletter: new: Nowy newsletter - participatory_space_private_user: - new: Nowy użytkownik prywatny per_page: Na stronę send_me_a_test_email: Wyślij mi testowego e-maila share: Udostępnij @@ -388,17 +379,6 @@ pl: values: 'false': Oficjalny 'true': Nieoficjalny - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Zaakceptowano zaproszenie - values: - 'false': Nie zaakceptowano - 'true': Zaakceptowano - user_invitation_sent_at_not_null: - label: Wysłano zaproszenie - values: - 'false': Nie wysłano - 'true': Wysłano private_space_eq: label: Prywatny values: @@ -527,6 +507,10 @@ pl: explanation: Zarządzani użytkownicy mogą być promowani do standardowych użytkowników. Oznacza to, że zostaną zaproszeni do udziału w aplikacji i nie będą mogli podszywać się pod inne osoby. Zaproszony użytkownik otrzyma wiadomość e-mail, aby zaakceptować zaproszenie. new_managed_user_promotion: Nowa zarządzana promocja użytkowników promote: Awansuj + members_csv_imports: + new: + csv_upload: + title: Prześlij swój plik CSV menu: admin_log: Logi aktywności administratora admins: Administratorzy @@ -587,8 +571,6 @@ pl: sent_to: Wysłano do subject: Temat name: Newsletter - participatory_space_private_user: - name: Użytkownik prywatnej przestrzeni partycypacyjnej scope: fields: name: Nazwa @@ -782,39 +764,6 @@ pl: form: add: Dodaj do listy dozwolonych title: Dozwolone domeny zewnętrzne - participatory_space_private_users: - create: - error: Wystąpił błąd podczas dodawania użytkownika prywatnego do tej przestrzeni partycypacyjnej. - success: Dodano dostęp dla prywatnego użytkownika przestrzeni partycypacyjnej. - destroy: - error: Wystąpił błąd podczas usuwania użytkownika prywatnego z tej przestrzeni partycypacyjnej. - success: Usunięto dostęp dla prywatnego użytkownika w tej przestrzeni partycypacyjnej. - index: - import_via_csv: Importuj z CSV - title: Prywatny użytkownik przestrzeni partycypacyjnej - new: - create: Utwórz - title: Nowy prywatny użytkownik przestrzeni partycypacyjnej. - participatory_space_private_users_csv_imports: - create: - invalid: Wystąpił problem podczas odczytu pliku CSV. Upewnij się, że postępowałeś zgodnie z instrukcjami. - success: Załadowanie pliku CSV powiodło się, wysyłamy e-mail z zaproszeniem do uczestników. To może chwilę potrwać. - new: - csv_upload: - title: Załaduj swój plik CSV - destroy: - button: Usuń wszystkich prywatnych uczestników - confirm: Czy na pewno chcesz usunąć wszystkich uczestników prywatnych? Tej akcji nie można cofnąć, nie będziesz w stanie odzyskać danych. - empty: Nie masz żadnych uczestników prywatnych. - explanation: Masz %{count} uczestników prywatnych. - title: Usuń prywatnych uczestników - example_file: 'Przykładowy plik:' - explanation: 'Załaduj swój plik CSV. Plik musi składać się z dwóch kolumn: w pierwszej kolumnie adresy e-mail, a w ostatniej nazwy użytkowników, których chcesz dodać do przestrzeni dla uczestników. Nie dodawaj nagłówków. Unikaj niepoprawnych znaków, takich jak `<>?%&^*#@()[]=+:;"{}\|` w nazwie użytkownika.' - explanation_example: | - jan.kowalski@przyklad.org%{csv_col_sep}Jan Kowalski - anna.kowalska@przyklad.org%{csv_col_sep}Anna Kowalska - title: Importuj uczestników prywatnych przez CSV - upload: Prześlij reminders: create: error: Wystąpił błąd podczas tworzenia przypomnień. diff --git a/decidim-admin/config/locales/pt-BR.yml b/decidim-admin/config/locales/pt-BR.yml index 10220e1f636ec..3af647846f8a5 100644 --- a/decidim-admin/config/locales/pt-BR.yml +++ b/decidim-admin/config/locales/pt-BR.yml @@ -33,8 +33,14 @@ pt-BR: help_section: content: Conteúdo id: ID + member: + email: Email + name: Nome + member_csv_import: + file: Arquivo newsletter: body: Corpo + send_to_all_users: Enviar para todos os participantes send_to_followers: Enviar para seguidores send_to_participants: Enviar para participantes subject: Assunto @@ -47,7 +53,8 @@ pt-BR: customize_welcome_notification: Personalizar notificação de boas-vindas default_locale: Idioma padrão description: Descrição - enable_omnipresent_banner: Mostrar bandeira omnipresente + enable_machine_translations: Ativar traduções automáticas + enable_omnipresent_banner: Mostrar bandeira onipresente enable_participatory_space_filters: Habilitar filtros do espaço participativo facebook_handler: Manipulador do Facebook favicon: Ícone @@ -89,9 +96,6 @@ pt-BR: welcome_notification_body: Corpo de notificação de boas-vindas welcome_notification_subject: Corpo de notificação de boas-vindas youtube_handler: Manipulador do YouTube - participatory_space_private_user: - email: E-mail - name: Nome scope: code: Código name: Nome @@ -121,10 +125,17 @@ pt-BR: show_in_footer: Mostrar no rodapé title: Título weight: Posição do pedido + taxonomy: + item_name: Nome do item + parent_id: Parente user_group_csv_verification: file: Arquivo errors: models: + member_csv_import: + attributes: + file: + malformed: Arquivo de importação malformado, por favor leia as instruções cuidadosamente e certifique-se de que o arquivo é codificado em UTF-8. newsletter: attributes: base: @@ -133,10 +144,6 @@ pt-BR: attributes: official_img_footer: allowed_file_content_types: Arquivo de imagem inválido - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Arquivo de importação malformado, por favor leia as instruções cuidadosamente e certifique-se de que o arquivo é codificado em UTF-8. user_group_csv_verification: attributes: file: @@ -163,25 +170,43 @@ pt-BR: decidim: admin: actions: + actions: Ações + actions_label: Ações para %{resource} add: Adicionar attachment: new: Novo anexo attachment_collection: new: Nova pasta de anexos + attachment_collections: Adicionar pasta + attachments: Adicionar anexo browse: Navegar configure: Configurar + confirm_delete_component: | + Você tem certeza que quer excluir esse componente?
+ Este componente contém:
+
    +
  • %{resources_count} recurso
  • +
+ Se você trocar de ideia, pode restaurar-lá mais tarde. export: Exportar export-selection: Exportar selecionado import: Importar + member: + new: Novo membro + menu_hidden: Ocultar do menu + moderate: Gerenciar moderações newsletter: new: Novo Boletim Informativo - participatory_space_private_user: - new: Novo espaço participativo de usuário privado per_page: Por página + permissions: Gerenciar permissões + restore: Restaurar send_me_a_test_email: Enviar um e-mail de teste share: Compartilhar + share_tokens: Compartilhar link + soft_delete: Mover para a lixeira user: new: Novo administrador + view_deleted_components: Ver componentes excluídos admin_terms_of_service: accept: error: Houve um erro ao aceitar os termos de serviço de administrador. @@ -215,6 +240,7 @@ pt-BR: create: error: Ocorreu um erro ao criar uma nova área. success: Área criada com sucesso. + deprecated: Lembre-se de que as áreas estão obsoletas e serão removidas em versões futuras. Por favor, use taxonomias em vez disso. O único módulo atualmente usando áreas é o módulo de iniciativas. destroy: has_spaces: Essa área tem espaços dependentes. Por favor, certifique-se de que nenhum Espaço Participativo referencia essa área antes de excluí-lo. success: Área excluída com sucesso. @@ -268,8 +294,15 @@ pt-BR: no_results: Nenhum resultado encontrado search_prompt: Digite, no mínimo, três caracteres para pesquisar. block_user: + bulk_new: + action: Bloquear conta e enviar justificativa + already_reported_html: Ao continuar com esta ação, você também ocultará o conteúdo de todos os participantes. + description: Bloquear um usuário irá tornar sua conta inutilizável. Você pode fornecer em sua justificação quaisquer diretrizes sobre como você poderia considerar desbloquear o usuário. + justification: Justificativa + title: Bloquear usuários new: action: Bloquear conta e enviar justificativa + already_reported_html: Ao continuar com esta ação, você também ocultará o conteúdo de todos os participantes. description: Bloquear um usuário irá tornar sua conta inutilizável. Você pode fornecer em sua justificação quaisquer diretrizes sobre como você poderia considerar desbloquear o usuário. justification: Justificativa title: Bloquear usuário %{name} @@ -301,6 +334,8 @@ pt-BR: name: Nome do componente type: Tipo de componente visibility: Visibilidade + manage_trash: + title: Componentes excluídos new: add: Adicionar componente title: 'Adicionar componente: %{name}' @@ -319,11 +354,14 @@ pt-BR: conflicts: attempts: Tentativas 'false': 'Não' + index: + text: Pesquisar por email, nome ou apelido do usuário atual managed_user_name: Usuário gerenciado solved: Resolvido title: Conflitos de verificação transfer: email: E-mail + error: Ocorreu um erro ao transferir o participante atual para o participante gerenciado. name: Nome reason: Motivo success: A transferência atual foi concluída com sucesso. @@ -332,6 +370,7 @@ pt-BR: user_name: Usuário content_blocks: create: + error: Ocorreu um problema ao criar o bloco de conteúdo. success: Bloco de conteúdo criado com sucesso. destroy: error: Ocorreu um problema ao tentar excluir esse bloco de conteúdo. @@ -345,6 +384,9 @@ pt-BR: update: Atualizar dashboard: pending_moderations: + announcement: + one: Existem %{count} moderações pendentes + other: Existem %{count} moderações pendentes goto_moderation: Ir para moderações globais title: Moderações pendentes show: @@ -354,6 +396,7 @@ pt-BR: form: domain_too_short: Domínio muito curto update: + error: Falha ao atualizar lista de domínios externos permitidos. success: Lista do domínio externo permitido atualizada com sucesso. exports: export_as: "%{name} como %{export_format}" @@ -375,6 +418,17 @@ pt-BR: values: 'false': 'Não' 'true': 'Sim' + members: + user_invitation_accepted_at_not_null: + label: Convite aceito + values: + 'false': Não aceito + 'true': Aceito + user_invitation_sent_at_not_null: + label: Convite enviado + values: + 'false': Não enviado + 'true': Enviado moderated_users: reports_reason_eq: label: Razão da denúncia @@ -390,17 +444,6 @@ pt-BR: values: 'false': Oficializado 'true': Não oficializado - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Convite aceito - values: - 'false': Não aceito - 'true': Aceito - user_invitation_sent_at_not_null: - label: Envio de convite - values: - 'false': Não enviado - 'true': Enviado private_space_eq: label: Privado values: @@ -411,9 +454,14 @@ pt-BR: values: 'false': Publicado 'true': Não publicado + remove_all: Apagar tudo search_label: Buscar search_placeholder: + name_or_nickname_or_email_cont: Pesquise %{collection} por email, nome ou apelido report_count_eq: Número de denúncias + reported_id_string_or_reported_content_cont: Pesquisar %{collection} por identificação reportável ou conteúdo + title_cont: Pesquisar %{collection} por título + user_name_or_user_nickname_or_user_email_cont: Pesquise %{collection} por email, nome ou apelido state_eq: label: Estado values: @@ -421,12 +469,19 @@ pt-BR: pending: Pendente rejected: Rejeitado verified: Verificado + taxonomies: + taxonomy_id_eq: + label: Taxonomia forms: file_help: import: explanation: 'Orientação para o arquivo:' message_1: Arquivos CSV, JSON e Excel (xlsx) são suportados message_2: Para arquivos CSV, o separador entre colunas deve ser um ponto e vírgula (";") + import_csv: + explanation: 'Orientação para o arquivo:' + message_1: Arquivos CSV são suportados + message_2: "Arquivo.csv com dados de email" help_sections: error: Houve um erro ao atualizar as seções de ajuda. form: @@ -485,6 +540,9 @@ pt-BR: other: Encontrado um erro no arquivo de importação para registros com os números de ordem %{indexes}. missing_headers: detail: Por favor, verifique se o arquivo contém colunas obrigatórias. + message: + one: 'Faltam as seguintes colunas: %{columns}.' + other: Faltam colunas %{columns}. error: Houve um problema durante a importação. example_error: Ocorreu um erro ao criar um exemplo para o tipo fornecido. new: @@ -502,10 +560,13 @@ pt-BR: logs: filters: participatory_space: Pesquisar espaço participativo + text: Pesquisar por email, nome ou apelido do usuário user: Usuário logs_list: no_logs_yet: Ainda não há registros. no_matching_logs: Não há nenhum registro com os filtros da pesquisa fornecidos. Tente alterá-los e tente novamente. + manage_trash: + deleted_items_warning: Você está visualizando itens excluídos no momento. Para fazer qualquer edição ou alterações, você deve primeiro restaurá-los. managed_users: promotion: error: Ocorreu um erro ao promover o usuário gerenciado. @@ -515,17 +576,67 @@ pt-BR: explanation: Os usuários gerenciados podem ser promovidos para usuários padrão. Isso significa que eles serão convidados para o aplicativo e você não poderá representá-los novamente. O usuário convidado receberá um e-mail para aceitar seu convite. new_managed_user_promotion: Nova promoção de usuário gerenciado promote: Promover + members: + create: + error: Ocorreu um erro ao adicionar um membro neste espaço participativo. + success: Acesso de membro criado com sucesso. + destroy: + error: Ocorreu um erro ao excluir um membro deste espaço participativo. + success: Acesso de membro destruído com sucesso. + edit: + title: Editar membro + update: Atualizar + index: + import_via_csv: Importar via CSV + publish_all: Publicar tudo + title: Membro + unpublish_all: Despublicar tudo + new: + create: Criar + title: Novo membro + publish_all: + error: Ocorreu um erro ao publicar todos os membros para este espaço participativo. + success: Todos os membros desta comunidade participativa foram publicados com sucesso + unpublish_all: + error: Ocorreu um erro ao cancelar a publicação de todos os membros para este espaço participativo. + success: Todos os membros desta comunidade participativa foram despublicados com sucesso + update: + error: Ocorreu um erro ao atualizar o membro para este espaço participativo. + success: Membro atualizado com sucesso + members_csv_imports: + create: + invalid: Houve um problema ao ler o arquivo CSV. Por favor, certifique-se de ter seguido as instruções. + success: Arquivo CSV carregado com sucesso, estamos enviando um email de convite para os participantes. Isso pode demorar um pouco. + new: + csv_upload: + title: Envie seu arquivo CSV + destroy: + button: Excluir todos os membros + confirm: Tem certeza que deseja apagar todos os membros? Esta ação não pode ser desfeita. Não poderá recuperá-los. + empty: Você não tem membros. + explanation: Você tem %{count} membros. + title: Apagar membros + example_file: 'Arquivo de exemplo:' + explanation: 'Envie seu arquivo CSV. Deve ter duas colunas com email na primeira coluna do arquivo e nome na última coluna do arquivo dos usuários que você deseja adicionar ao espaço participativo, sem cabeçalhos. Evite usar caracteres inválidos como `<>?%&^*#@()[]=+:;"{}\├` no nome de usuário.' + explanation_example: | + joao.silva@exemplo.org%{csv_col_sep}João da Silva + joana.silva@exemplo.org%{csv_col_sep}Joana da Silva + title: Importar membros via CSV + upload: Carregar menu: admin_log: Log de atividade de administração admins: Admins appearance: Aparência area_types: Tipos de área areas: Áreas + components: Componentes configuration: Configuração content: Conteúdo relatado external_domain_allowlist: Domínios externos permitidos help_sections: Seções de ajuda + homepage: Layout da página inicial impersonations: Imitações + insights: Insights manage: Gerenciar moderation: Moderadores globais newsletters: Newsletters @@ -535,8 +646,12 @@ pt-BR: scopes: Âmbitos see_site: Ver site settings: Configurações + share_tokens: Acessar links static_page_topics: Tópicos static_pages: Páginas + statistics: Estatísticas + taxonomies: Taxonomias + taxonomy_filters: Filtros de taxonomia users: Usuários models: area: @@ -566,6 +681,8 @@ pt-BR: reason: Motivo started_at: Começou em user: Usuário + member: + name: Membro newsletter: fields: created_at: Criado em @@ -574,8 +691,6 @@ pt-BR: sent_to: Enviado para subject: Assunto name: Newsletter - participatory_space_private_user: - name: Espaço participativo usuário particular scope: fields: name: Nome @@ -624,6 +739,7 @@ pt-BR: update_moderated_user_button: Anular denúncias de usuários title: Ações unblock: Desbloquear usuário + unreport: Desfazer a denúncia cancel: Cancelar name: Nome nickname: Apelido @@ -647,8 +763,10 @@ pt-BR: title: Ocultar update_moderation_button: Ocultar os recursos selecionados unhide: + title: Desfazer o ocultar update_moderation_button: Desocultar os recursos selecionados unreport: + title: Desfazer a denúncia update_moderation_button: Anular denúncia dos recursos selecionados cancel: Cancelar selected: selecionado @@ -776,6 +894,7 @@ pt-BR: index: actions: Ações badge: Selo + block: Bloquear created_at: Criado em name: Nome nickname: Apelido @@ -784,7 +903,10 @@ pt-BR: officialized: Oficializado reofficialize: Reorientar reports: Denúncias + send_message: Enviar mensagem + show_email: Mostrar email status: Status + unblock: Desbloquear unofficialize: Não oficializar new: badge: Emblema de oficialização @@ -807,13 +929,27 @@ pt-BR: form: admin_terms_of_service: Termos de serviço do administrador basic_configuration: Configuração básica + colors: + choose_color: Escolha a cor primária + colors_title: Cores da organização + colors_warning_html: Atenção! Mudar estas cores pode quebrar contrastes de acessibilidade. Você pode verificar o contraste da sua escolha com
verificador de contraste WebAIM ou outras ferramentas similares. + explanation: Esta ferramenta ajuda você a escolher um esquema de cores, composto de tons igualmente espaçados ao redor da roda de cor que será usado no site da organização. + legend_html: Cores principais da aplicação, com base no algoritmo Triadic. A cor secundária é auto-calculada a partir da cor primária e da saturação que você escolheu. + saturation: Saturação + title: Seletor de cores + update_suggested_colors: Atualizar cores sugeridas + extra_features: Recursos adicionais facebook: Facebook github: GitHub instagram: Instagram + logos: + organization_logos: Logotipos da organização + preview: Pré-visualização rich_text_editor_in_public_views_help: Em algumas áreas de texto, os participantes poderão inserir algumas tags HTML usando o editor de texto rico. social_handlers: Social twitter: X url: URL + welcome_notification: Notificações de Boas-Vindas youtube: YouTube update: error: Ocorreu um erro ao atualizar essa organização. @@ -829,39 +965,6 @@ pt-BR: form: add: Adicionar na lista de permitidos title: Lista de domínios externos permitidos - participatory_space_private_users: - create: - error: Ocorreu um erro ao adicionar um usuário privado para este espaço participativo. - success: Espaço participativo acesso de usuário privado criado com sucesso. - destroy: - error: Houve um erro ao excluir um usuário particular desse espaço participativo. - success: Espaço participativo acesso de usuário privado excluído com sucesso. - index: - import_via_csv: Importar via CSV - title: Espaço participativo usuário particular - new: - create: Criar - title: Novo usuário particular do Espaço Participativo. - participatory_space_private_users_csv_imports: - create: - invalid: Ocorreu um problema ao ler o arquivo CSV. Por favor, certifique-se de ter seguido as instruções. - success: Arquivo CSV enviado com sucesso, estamos enviando um e-mail de convite para os participantes. Isso pode demorar um pouco. - new: - csv_upload: - title: Envie seu arquivo CSV - destroy: - button: Excluir todos os participantes privados - confirm: Tem certeza que deseja excluir todos os participantes privados? Esta ação não pode ser desfeita, não será possível recuperá-los. - empty: Você não tem participantes privados. - explanation: Você tem %{count} participantes privados. - title: Excluir participantes privados - example_file: 'Arquivo de exemplo:' - explanation: 'Envie seu arquivo CSV. Deve ter duas colunas com e-mail na primeira coluna do arquivo e nome na última coluna do arquivo dos usuários que você deseja adicionar ao espaço participativo, sem cabeçalhos. Evite usar caracteres inválidos como `<>?%&^*#@()[]=+:;"{}\├` no nome de usuário.' - explanation_example: | - joao.silva@examplo.org%{csv_col_sep}João da Silva - maria.silva@examplo.org%{csv_col_sep}Maria da Silva - title: Importar participantes privados via CSV - upload: Upload reminders: create: error: Ocorreu um problema ao criar lembretes. @@ -874,6 +977,8 @@ pt-BR: edit: submit: Enviar title: Editar permissões + options_form: + ephemeral_warning: Para habilitar este método de autorização, você precisa revogar todas as autorizações existentes. Isto minimiza o risco de conflitos de verificação. Todas as autorizações deste tipo serão revogadas automaticamente na organização se você enviar alterações adicionando este método ou modifiyng sua configuração depois de adicionada. update: success: Permissões atualizadas com sucesso. scope_types: @@ -895,6 +1000,7 @@ pt-BR: create: error: Ocorreu um erro ao criar um novo âmbito. success: Âmbito criado com sucesso. + deprecated: Lembre-se de que os escopos são obsoletos e serão removidos em versões futuras. Por favor use taxonomias em vez disso. O único módulo que usa escopos atualmente é o módulo de iniciativas. destroy: success: Âmbito excluído com sucesso. edit: @@ -909,8 +1015,38 @@ pt-BR: success: Âmbito atualizado com sucesso share_tokens: actions: + confirm_destroy: Tem certeza de que deseja excluir este acesso? + copy_link: Copiar o link destroy: Excluir + edit: Editar + preview: Pré-visualização + create: + invalid: Ocorreu um erro ao gerar o link de acesso. + success: Link de acesso criado com sucesso. + destroy: + error: Ocorreu um erro ao destruir o link de acesso. + success: Link de acesso excluído com sucesso. + edit: + title: 'Editar links de acesso para: %{name}' + update: Atualizar + form: + automatic: Automático + custom: Personalizar + custom_expiration: Expiração personalizada + custom_token: Palavra customizada + expires_at: Válido até + 'false': 'Não' + never_expire: Nunca + registered_only: Apenas registrado? + token: Chave de acesso + 'true': 'Sim' index: + back_to_share_tokens: Voltar aos links de acesso + copied: Link de acesso copiado! + copy_message: O texto foi copiado com sucesso para área de transferência. + create_new_token: Crie seu primeiro link de acesso! + empty_html: Não há nenhum link de acesso ativo. %{new_token_link} + never: Nunca new_share_token_button: Novo link de acesso share_tokens_help_html: | Crie e compartilhe um link de acesso para permitir que outros visualizem este recurso não publicado. @@ -949,6 +1085,8 @@ pt-BR: error: Houve um erro ao atualizar este tópico. success: Tópico atualizado com sucesso static_pages: + actions: + view: Visualizar create: error: Ocorreu um erro ao criar uma nova página. success: Página criada com sucesso. @@ -976,6 +1114,9 @@ pt-BR: taxonomies: actions: actions: Ações + destroy: Excluir + edit: Editar + filters: Aplicar filtros breadcrumb: edit: Editar taxonomia new: Nova taxonomia @@ -1032,15 +1173,46 @@ pt-BR: back: Retornar title: Editar filtro de taxonomia update: Atualizar filtro de taxonomia + form: + all: Tudo + items: + one: "Item %{count} selecionado" + other: "Itens %{count} selecionados" + name_help: O título do filtro a ser exibido no frontend. + no_items: Não há itens disponíveis para esta taxonomia. Você pode criar itens nas configurações de configuração da taxonomia. + space_filter: Disponível como filtro para + index: + description: Um filtro taxonômico permite aos administradores ordenar e filtrar espaços participativos com base em uma taxonomia. Por exemplo, adicione um filtro de taxonomia aos processos de classificação por âmbito geográfico. + empty: Atualmente não há filtros de taxonomia. Crie uma lista de filtros de taxonomia aqui para classificar e filtrar espaços participativos com base em uma taxonomia. + new_filter: Novo filtro new: back: Voltar create: Criar filtro de taxonomia title: Novo filtro de taxonomia table: actions: Ações + components_count: Componentes usando isto + confirm_destroy: 'Tem certeza que deseja excluir o filtro %{name}? NOTA: Recursos que estão usando este filtro não serão afetados, mas eles perderão a referência a este filtro.' destroy: Deletar edit: Editar + internal_name: Rótulo Interno name: + update: + error: Ocorreu um erro ao atualizar este filtro de taxonomia. + success: Filtro de taxonomia atualizado com sucesso. + taxonomy_filters_selector: + new: + add_new_items_html: Se você precisa definir um novo filtro ou item, faça isso através das configurações ou clicando aqui. + items_count: "{count} itens" + save: Salvar + show: + items_count: "{count} itens" + remove: Excluir + taxonomies_select: + filters: Filtros + select_filter: Selecione um filtro + select_taxonomy: Selecione uma taxonomia + taxonomies: Taxonomias taxonomy_items: edit: title: Editar item em %{taxonomy} @@ -1065,8 +1237,19 @@ pt-BR: participants: Comercial scope_types: Tipos de âmbito scopes: Âmbitos + statistics: Estatísticas + taxonomies: Taxonomias + taxonomy_filters: Filtros de taxonomia para "%{taxonomy}" users: Administradores + tooltips: + cannot_destroy_taxonomy_filter: Não é possível destruir este filtro de taxonomia + cannot_edit_taxonomy_filter: Não é possível editar este filtro de taxonomia + deleted_attachment_collections_info: Não é possível excluir esta pasta porque ela possui anexos. + deleted_component_info: Este componente só pode ser excluído se o status for 'Não publicado'. trash_management: + restore: + invalid: Houve um problema ao restaurar %{resource_name}. + success: "%{resource_name} restaurado com sucesso." soft_delete: invalid: Houve um problema ao remover %{resource_name}. success: "%{resource_name} removido com sucesso." @@ -1090,6 +1273,7 @@ pt-BR: last_day: Último dia last_month: Último mês last_week: Última semana + no_users_count_statistics_yet: Ainda não há nenhuma estatística de participantes. participants: Participantes forms: errors: @@ -1101,14 +1285,34 @@ pt-BR: hidden: Oculto hide: Ocultar not_hidden: Não oculto + parent_hidden: Você não pode exibir este recurso porque seu pai ainda está oculto. title: Ações + unhide: Desfazer o ocultar + unreport: Desfazer a denúncia admin: reportable: + bulk_action: + hide: + failed: 'Houve um problema ao esconder alguns recursos: %{errored}' + invalid: Nenhuma moderação selecionada. + success: Recursos ocultos com sucesso. + ignore: + invalid: Nenhuma moderação de usuários selecionada. + success: Participantes não reportados com sucesso. + unhide: + failed: 'Houve um problema ao exibir alguns recursos: %{errored}' + invalid: Nenhuma moderação selecionada. + success: Recursos desocultados com sucesso. + unreport: + failed: 'Houve um problema não reportando alguns recursos: %{errored}' + invalid: Nenhuma moderação selecionada. + success: Recursos não reportados com sucesso. hide: invalid: Ocorreu um problema ao ocultar o recurso. success: O recurso foi ocultado com sucesso. unhide: invalid: Houve um problema ao exibir o recurso. + parent_invalid: Ocorreu um problema ao exibir o recurso. O pai não está visível. success: Recurso com sucesso exibido. unreport: invalid: Tem havido um problema não relatando o recurso. @@ -1139,5 +1343,7 @@ pt-BR: admin: global_moderations: title: Moderações globais + insights: + title: Insights taxonomy_filters_selector: title: Filtros diff --git a/decidim-admin/config/locales/pt.yml b/decidim-admin/config/locales/pt.yml index 22814903cf5ab..e17d7510df691 100644 --- a/decidim-admin/config/locales/pt.yml +++ b/decidim-admin/config/locales/pt.yml @@ -139,8 +139,6 @@ pt: import: Importar newsletter: new: Novo boletim informativo - participatory_space_private_user: - new: Novo utilizador privado do espaço participativo per_page: Por página share: Partilhar area_types: @@ -296,17 +294,6 @@ pt: values: 'false': Oficializado 'true': Não oficializado - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Convite aceite - values: - 'false': Não aceite - 'true': Aceite - user_invitation_sent_at_not_null: - label: Convite enviado - values: - 'false': Não enviado - 'true': Enviado private_space_eq: label: Privado values: @@ -390,6 +377,10 @@ pt: explanation: Os participantes geridos podem ser promovidos para participantes padrão. Isto significa que eles serão convidados para a aplicação e não poderá geri-los novamente. O participante convidado receberá um e-mail para aceitar o seu convite. new_managed_user_promotion: Nova promoção de usuário participante gerido promote: Destacar + members_csv_imports: + new: + csv_upload: + title: Envie o seu ficheiro CSV menu: admin_log: Registo de atividade de administração admins: Administradores @@ -446,8 +437,6 @@ pt: sent_to: Enviado para subject: Assunto name: Boletim Informativo - participatory_space_private_user: - name: Participante privado de espaço participativo scope: fields: name: Nome @@ -622,24 +611,6 @@ pt: update: error: Ocorreu um problema ao atualizar esta organização. success: Organização atualizada corretamente. - participatory_space_private_users: - create: - error: Ocorreu um problema ao adicionar um participante privado neste espaço participativo. - success: Acesso ao espaço participativo de participante privado criado corretamente. - destroy: - error: Houve um problema ao eliminar um participante privado deste espaço participativo. - success: Espaço participativo acesso de usuário privado destruído com sucesso. - index: - import_via_csv: Importar via CSV - title: Espaço participativo de participante privado - new: - create: Criar - title: Novo participante privado do Espaço Participativo. - participatory_space_private_users_csv_imports: - new: - csv_upload: - title: Envie o seu ficheiro CSV - upload: Carregar resource_permissions: edit: submit: Submeter diff --git a/decidim-admin/config/locales/ro-RO.yml b/decidim-admin/config/locales/ro-RO.yml index 53722ea7f4527..619598822962c 100644 --- a/decidim-admin/config/locales/ro-RO.yml +++ b/decidim-admin/config/locales/ro-RO.yml @@ -87,9 +87,6 @@ ro: welcome_notification_body: Conținutul notificării de bun venit welcome_notification_subject: Subiectul notificării de bun venit youtube_handler: Responsabil YouTube - participatory_space_private_user: - email: E-mail - name: Nume scope: code: Cod name: Nume @@ -131,14 +128,10 @@ ro: attributes: official_img_footer: allowed_file_content_types: Fișier invalid - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Fișier de import malformat, vă rugăm să citiți instrucțiunile cu atenție și asigurați-vă că fișierul este codificat UTF-8. user_group_csv_verification: attributes: file: - malformed: Fişier de import malformat, vă rugăm să citiţi instrucţiunile cu atenţie şi asiguraţi-vă că fişierul este codificat UTF-8. + malformed: Fișier de import malformat, vă rugăm să citiți instrucțiunile cu atenție și asigurați-vă că fișierul este codificat UTF-8. new_import: attributes: file: @@ -172,8 +165,6 @@ ro: import: Importă newsletter: new: Buletin informativ nou - participatory_space_private_user: - new: Nou utilizator privat al spațiului participativ per_page: Pe pagină send_me_a_test_email: Trimite-mi un e-mail de test share: Distribuie @@ -363,17 +354,6 @@ ro: values: 'false': Verificat 'true': Neverificat - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Invitație acceptată - values: - 'false': Neacceptat - 'true': Acceptat - user_invitation_sent_at_not_null: - label: Invitaţie trimisă - values: - 'false': Nu s-a trimis - 'true': Trimis private_space_eq: label: Privat values: @@ -497,6 +477,15 @@ ro: explanation: Participanții gestionați pot fi promovați drept participanți standard. Înseamnă că vor fi invitați în aplicație și nu vei mai putea să îi gestionezi din nou. Participantul invitat va primi un e-mail pentru a accepta invitația ta. new_managed_user_promotion: Promovarea unui nou participant gestionat promote: Promovează + members: + edit: + title: Editare membru + new: + title: Membru nou + members_csv_imports: + new: + csv_upload: + title: Încarcă fișierul tău CSV menu: admin_log: Jurnal activitate admin admins: Administratori @@ -556,8 +545,6 @@ ro: sent_to: Trimis către subject: Subiect name: Buletin Informativ - participatory_space_private_user: - name: Participant privat pentru spațiul participativ scope: fields: name: Nume @@ -633,7 +620,7 @@ ro: title: Detalii raportare newsletter_templates: index: - preview_template: Previzualizează + preview_template: Previzualizați title: Modele de buletine informative use_template: Utilizează acest model show: @@ -682,7 +669,7 @@ ro: send: no_recipients: Niciun destinatar pentru această selecție. show: - preview: Previzualizează + preview: Previzualizați select_recipients_to_deliver: Selectează destinatarii pentru trimitere subject: Subiect update: @@ -736,38 +723,6 @@ ro: update: error: A apărut o eroare la actualizarea acestei organizații. success: Organizația a fost actualizată cu succes. - participatory_space_private_users: - create: - error: A apărut o problemă la adăugarea unui participant privat pentru acest spațiu participativ. - success: Accesul la spațiul participativ a participantului privat a fost creat cu succes. - destroy: - error: A apărut o eroare la ștergerea unui participant privat pentru acest spațiu participativ. - success: Accesul la spațiul participativ a participantului privat a fost șters cu succes. - index: - import_via_csv: Importă din fișier CSV - title: Participant privat pentru spațiul participativ - new: - create: Crează - title: Participant privat nou pentru spațiul participativ. - participatory_space_private_users_csv_imports: - create: - invalid: A apărut o eroare la citirea fișierului CSV. Vă rugăm să vă asigurați că ați urmat instrucțiunile. - success: Fișier-ul CSV a fost încărcat cu succes, trimitem câte un e-mail participanților cu invitația. Acest lucru ar putea dura puțin timp. - new: - csv_upload: - title: Încarcă fișierul tău CSV - destroy: - button: Șterge toți participanții privați - confirm: Sunteți sigur că doriți să ștergeți toți participanții privați? Această acțiune nu poate fi anulată, și nu îi veți putea recupera. - empty: Nu aveți niciun participant privat. - explanation: Aveți %{count} participanți privați. - title: Ștergere participanți privați - example_file: 'Exemplu de fişier:' - explanation: 'Încărcă fişierul tău CSV. Trebuie să aibă două coloane cu e-mail în prima coloană a fișierului și numele în ultima coloană (e-mail, numele) pentru utilizatorii pe care doriţi să îi adăugaţi în spaţiul participativ, fără antet. Evită folosirea caracterelor invalide, cum ar fi `<>?%&^*#@()[]=+:;"{}\ ` în numele utilizatorului.' - explanation_example: | - ion.popescu@example.org%{csv_col_sep}Ion Popescu - maria.popescu@example.org%{csv_col_sep}Maria Popescu - upload: Încarcă reminders: create: error: A apărut o eroare la crearea de memento-uri. @@ -899,6 +854,7 @@ ro: dashboard: Panou de administrare impersonatable_users: Participanți gestionabili impersonations: Gestionarea participanților + menu: Meniu panel: Administrator participants: Participanți scope_types: Tipuri de domeniu diff --git a/decidim-admin/config/locales/ru.yml b/decidim-admin/config/locales/ru.yml index 498957aabbba1..0129817d608d8 100644 --- a/decidim-admin/config/locales/ru.yml +++ b/decidim-admin/config/locales/ru.yml @@ -258,6 +258,10 @@ ru: explanation: Управляемых участников можно повышать до обычных участников. Это означает, что они будут приглашены в приложение, и вы не сможете выступать в их роли их снова. Приглашенный участник получит электронное письмо, чтобы принять ваше приглашение. new_managed_user_promotion: Повысить управляемого участника в статусе promote: Повысить + members_csv_imports: + new: + csv_upload: + title: Загрузите ваш CSV файл menu: admin_log: Журнал деятельности администратора admins: Администраторы @@ -311,8 +315,6 @@ ru: sent_to: Отправлено subject: Тема name: Рассылка новостей - participatory_space_private_user: - name: Частный участник пространства соучастия scope: fields: name: Имя @@ -410,21 +412,6 @@ ru: update: error: При попытке обновить эту организацию произошла ошибка. success: Организация успешно обновлена. - participatory_space_private_users: - create: - error: При попытке добавить частного участника в это пространство соучастия произошла ошибка. - success: Частным участникам успешно предоставлен доступ к пространству соучастия. - destroy: - success: Успешно отменен доступ частных участников к пространству соучастия. - index: - title: Частный участник пространства соучастия - new: - create: Добавить - title: Добавить частного участника пространства соучастия. - participatory_space_private_users_csv_imports: - new: - csv_upload: - title: Загрузите ваш CSV файл resource_permissions: edit: title: Редактировать права доступа diff --git a/decidim-admin/config/locales/sk.yml b/decidim-admin/config/locales/sk.yml index 1170b1aa5f4e1..6b4deeeb2ff46 100644 --- a/decidim-admin/config/locales/sk.yml +++ b/decidim-admin/config/locales/sk.yml @@ -273,6 +273,10 @@ sk: explanation: Spravovaní účastníci môžu byť povýšení na štandardných účastníkov. To znamená, že budú pozvaní do aplikácie, a už ich nebudete môcť spravovať. Pozvaný účastník obdrží e-mail s Vašou pozvánkou. new_managed_user_promotion: Povýšenie nového spravovaného účastníka promote: Povýšiť + members_csv_imports: + new: + csv_upload: + title: Nahrať súbor CSV menu: admin_log: Záznam aktivity administrátora admins: Administrátori @@ -326,8 +330,6 @@ sk: sent_to: Odoslané subject: Predmet name: Spravodaj - participatory_space_private_user: - name: Účastníci participatívnych procesov scope: fields: name: Meno @@ -446,23 +448,6 @@ sk: update: error: Pri aktualizácii organizácie došlo k chybe. success: Organizácia úspešne aktualizovaná. - participatory_space_private_users: - create: - error: Pri pridávaní súkromného používateľa pre tento participačný priestor došlo k chybe. - success: Participatívny priestor bol sprístupnený súkromnému užívateľovi. - destroy: - error: Pri vymazaní súkromného používateľa pre tento participačný priestor došlo k chybe. - success: Participatívny priestor bol zneprístupnený súkromnému užívateľovi. - index: - title: Súkromný účastník participatívneho priestoru - new: - create: Vytvoriť - title: Nový súkromný účastník participatívneho priestoru. - participatory_space_private_users_csv_imports: - new: - csv_upload: - title: Nahrať súbor CSV - upload: Nahrať resource_permissions: edit: submit: Potvrdiť diff --git a/decidim-admin/config/locales/sq-AL.yml b/decidim-admin/config/locales/sq-AL.yml index 703095d7e3956..cb3a3d11ded78 100644 --- a/decidim-admin/config/locales/sq-AL.yml +++ b/decidim-admin/config/locales/sq-AL.yml @@ -84,9 +84,6 @@ sq: welcome_notification_body: Përmbajtja e njoftimit të mirëseardhjes welcome_notification_subject: Tema e njoftimit të mirëseardhjes youtube_handler: Emri i përdoruesit Youtube - participatory_space_private_user: - email: Email - name: Emri scope: code: Kodi name: Emri @@ -238,17 +235,6 @@ sq: label: Tipi officialized_at_null: label: Gjendja - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Ftesa u pranua - values: - 'false': E papranuar - 'true': Pranuar - user_invitation_sent_at_not_null: - label: Ftesa u dërguan - values: - 'false': Nuk është dërguar - 'true': Dërguar private_space_eq: label: Private values: diff --git a/decidim-admin/config/locales/sr-CS.yml b/decidim-admin/config/locales/sr-CS.yml index 6624b183b1be9..cd7e6b4394d85 100644 --- a/decidim-admin/config/locales/sr-CS.yml +++ b/decidim-admin/config/locales/sr-CS.yml @@ -325,8 +325,6 @@ sr: sent_to: Poslato subject: Naslov name: Bilten - participatory_space_private_user: - name: Privatni učesnik prostora za diskusiju scope: fields: name: Ime diff --git a/decidim-admin/config/locales/sv.yml b/decidim-admin/config/locales/sv.yml index 69100c45997e5..e6ded36e21757 100644 --- a/decidim-admin/config/locales/sv.yml +++ b/decidim-admin/config/locales/sv.yml @@ -33,6 +33,11 @@ sv: help_section: content: Innehåll id: ID + member: + email: E-post + name: Namn + member_csv_import: + file: Fil newsletter: body: Innehåll send_to_all_users: Skicka till alla deltagare @@ -90,11 +95,6 @@ sv: welcome_notification_body: Välkomstmeddelande welcome_notification_subject: Välkomstmeddelandets ämnesrad youtube_handler: YouTube-namn - participatory_space_private_user: - email: E-post - name: Namn - participatory_space_private_user_csv_import: - file: Fil scope: code: Kod name: Namn @@ -124,10 +124,17 @@ 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: models: + member_csv_import: + attributes: + file: + malformed: Felaktig importfil, läs igenom anvisningarna noga och se till att filen är UTF-8 kodad. newsletter: attributes: base: @@ -136,10 +143,6 @@ sv: attributes: official_img_footer: allowed_file_content_types: Ogiltig bildfil - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Fel format, läs igenom instruktionerna noga och se till att filen är UTF-8 kodad. user_group_csv_verification: attributes: file: @@ -187,12 +190,12 @@ sv: export: Exportera export-selection: Exportera markerade import: Importera + member: + new: Ny medlem menu_hidden: Dölj från menyn moderate: Hantera modereringar newsletter: new: Nytt nyhetsbrev - participatory_space_private_user: - new: Ny privat deltagare i deltagarutrymme per_page: Per sida permissions: Hantera behörigheter restore: Återställ @@ -414,6 +417,17 @@ sv: values: 'false': 'Nej' 'true': 'Ja' + members: + user_invitation_accepted_at_not_null: + label: Inbjudan accepterades + values: + 'false': Inte tackat ja + 'true': Tackat ja + user_invitation_sent_at_not_null: + label: Inbjudan har skickats + values: + 'false': Inte skickat + 'true': Skickad moderated_users: reports_reason_eq: label: Anledning till anmälan @@ -429,17 +443,6 @@ sv: values: 'false': Har officiell status 'true': Ej gjord officiell - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Inbjudan accepterades - values: - 'false': Ej godkänd - 'true': Godkänd - user_invitation_sent_at_not_null: - label: Inbjudan skickad - values: - 'false': Ej skickad - 'true': Skickad private_space_eq: label: Privat values: @@ -572,6 +575,13 @@ sv: explanation: Hanterade deltagare kan befordras till vanliga deltagare. Det innebär att de kommer att bli inbjudna till programmet och du kommer inte att kunna hantera dem igen. Den inbjudna deltagaren får ett e-brev om att godkänna inbjudan. new_managed_user_promotion: Befordra ny hanterad användare promote: Befordra + members: + create: + error: Det gick inte att lägga till en deltagare till processen. + members_csv_imports: + new: + csv_upload: + title: Skicka CSV-fil menu: admin_log: Aktivitetslogg admins: Administratörer @@ -637,8 +647,6 @@ sv: sent_to: Skickat till subject: Ämne name: Nyhetsbrev - participatory_space_private_user: - name: Deltagare i privat deltagarutrymme scope: fields: name: Namn @@ -913,53 +921,6 @@ sv: form: add: Lägg till i lista över tillåtna domännamn title: Tillåtna externa domäner - participatory_space_private_users: - create: - error: Det gick inte att lägga till en privat deltagare i deltagarutrymmet. - success: Åtkomst för privat deltagare till deltagarutrymmet har skapats. - destroy: - error: Det gick inte att ta bort en privat deltagare från deltagarutrymmet. - success: Åtkomsten för privat deltagare till deltagarutrymmet har tagits bort. - edit: - title: Redigera privat deltagare i deltagarutrymme. - update: Uppdatera - index: - import_via_csv: Importera via CSV - publish_all: Publicera alla - title: Privat deltagare i deltagarutrymme - unpublish_all: Avpublicera alla - new: - create: Skapa - title: Ny privat deltagare i deltagarutrymme. - publish_all: - error: Det gick inte att publicera alla privata deltagare i deltagarutrymmet. - success: Publicerade alla privata deltagare för deltagarutrymmet - unpublish_all: - error: Det gick inte att avpublicera alla privata deltagare i deltagarutrymmet. - success: Avpublicerade alla privata deltagare för deltagarutrymmet - update: - error: Det gick inte att uppdatera den privata deltagaren i deltagarutrymmet. - success: Privat deltagare i deltagarutrymmet har uppdaterats - participatory_space_private_users_csv_imports: - create: - invalid: Det gick inte att läsa CSV-filen. Kontrollera att du har följt instruktionerna. - success: CSV-filen har laddats upp och vi skickar en inbjudan med e-post till deltagarna. Det kan ta en stund. - new: - csv_upload: - title: Ladda upp din CSV-fil - destroy: - button: Radera alla privata deltagare - confirm: Är du säker på att du vill radera alla privata deltagare? Den här åtgärden kan inte ångras. - empty: Det finns inga privata deltagare. - explanation: Det finns %{count} privata deltagare. - title: Radera privata deltagare - example_file: 'Exempelfil:' - explanation: 'Skicka din CSV-fil. Den behöver ha två kolumner för användare som du vill lägga till, utan rubriker, med e-postadressen i den första kolumnen och namnet i den sista kolumnen. Undvik specialtecken som `<>?%&^*#@()[]=+:;"{}\|` i användarnamn.' - explanation_example: | - jon.andersson@example.org%{csv_col_sep}Jon Andersson - jenny.andersson@example.org%{csv_col_sep}Jenny Andersson - title: Importera privata deltagare från en CSV-fil - upload: Skicka reminders: create: error: Det gick inte att skapa påminnelser. diff --git a/decidim-admin/config/locales/th-TH.yml b/decidim-admin/config/locales/th-TH.yml index 0bd1df7874978..1f8fb82a154a2 100644 --- a/decidim-admin/config/locales/th-TH.yml +++ b/decidim-admin/config/locales/th-TH.yml @@ -45,9 +45,6 @@ th: welcome_notification_body: ยินดีต้อนรับเนื้อหาการแจ้งเตือน welcome_notification_subject: ยินดีต้อนรับเรื่องการแจ้งเตือน youtube_handler: ตัวจัดการ YouTube - participatory_space_private_user: - email: อีเมล - name: ชื่อ scope: code: รหัส name: ชื่อ diff --git a/decidim-admin/config/locales/tr-TR.yml b/decidim-admin/config/locales/tr-TR.yml index 70d22c1a67775..7ffdcf0fb0e27 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 @@ -89,11 +90,6 @@ tr: welcome_notification_body: Karşılama bildirimi gövdesi welcome_notification_subject: Karşılama bildirimi konusu youtube_handler: YouTube işleyici - participatory_space_private_user: - email: E-Posta - name: Adı - participatory_space_private_user_csv_import: - file: Dosya scope: code: kod name: isim @@ -135,10 +131,6 @@ tr: attributes: official_img_footer: allowed_file_content_types: Geçersiz resim dosyası - participatory_space_private_user_csv_import: - attributes: - file: - malformed: Bozuk içe aktarım dosyası, lütfen yönergeleri dikkatlice gözden geçirin ve dosyanın UTF-8 ile kodlandığından emin olun. user_group_csv_verification: attributes: file: @@ -165,6 +157,7 @@ tr: decidim: admin: actions: + actions_label: '%{resource} için işlemler' add: Eklemek attachment: new: Yeni ek @@ -181,10 +174,9 @@ 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: - new: Yeni katılımcı alanı özel kullanıcısı per_page: Sayfa başına restore: Geri yükle send_me_a_test_email: Bana bir test e-postası gönder @@ -281,6 +273,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 +293,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 +369,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}" @@ -392,6 +387,7 @@ tr: 'false': 'Hayır' 'true': 'Evet' last_sign_in_at_present: + label: Daha önce giriş yaptınız mı values: 'false': 'Hayır' 'true': 'Evet' @@ -410,17 +406,6 @@ tr: values: 'false': resmileştirilmiş 'true': Resmi değil - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: Davet kabul edildi - values: - 'false': Kabul edilmedi - 'true': Kabul Edildi - user_invitation_sent_at_not_null: - label: Davet gönderildi - values: - 'false': Gönderilmedi - 'true': Gönderildi private_space_eq: label: Kişisel values: @@ -433,6 +418,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 +440,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 +495,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 +508,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. @@ -539,6 +531,15 @@ tr: explanation: Yönetilen kullanıcılar standart kullanıcılara yükseltilebilir. Bu, uygulamaya davet edilecekleri anlamına gelir ve onları tekrar taklit edemezsiniz. Davet edilen kullanıcı davetinizi kabul etmek için bir e-posta alacak. new_managed_user_promotion: Yeni yönetilen kullanıcı tanıtımı promote: Desteklemek + members: + create: + error: Bu katılımcı alan için bir üye eklenirken hata oluştu. + update: + error: Bu katılımcı alan için bir üyeyi güncellerken bir hata oluştu. + members_csv_imports: + new: + csv_upload: + title: CSV dosyanızı yükleyin menu: admin_log: Yönetici etkinlik günlüğü admins: Yöneticiler @@ -550,6 +551,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 @@ -603,8 +605,6 @@ tr: sent_to: Gönderilen subject: konu name: Bülten - participatory_space_private_user: - name: Katılımcı alan özel kullanıcısı scope: fields: name: isim @@ -683,6 +683,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 +696,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 +732,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 +775,7 @@ tr: officialized: resmileştirilmiş reofficialize: Reofficialize reports: Raporlar + send_message: Mesaj Gönder status: durum unofficialize: Unofficialize new: @@ -789,9 +797,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 @@ -811,32 +827,6 @@ tr: form: add: İzin verilenler listesine ekle title: İzin verilen harici alan adları - participatory_space_private_users: - create: - error: Bu katılımcı alan için özel bir kullanıcı eklenirken bir hata oluştu. - success: Katılımcı alan özel kullanıcı erişimi başarıyla oluşturuldu. - destroy: - error: Bu katılımcı alan için özel bir kullanıcı silinirken bir hata oluştu. - success: Katılımcı alan özel kullanıcı erişimi başarıyla yok edildi. - edit: - title: Katılımcı alanı özel katılımcılarını düzenle. - update: Güncelle - index: - import_via_csv: CSV'den içe aktar - publish_all: Tümünü yayımla - title: Katılımcı alan özel kullanıcısı - unpublish_all: Tümünü yayımdan kaldır - new: - create: yaratmak - title: Yeni Katılımcı Uzay özel kullanıcısı. - participatory_space_private_users_csv_imports: - new: - csv_upload: - title: CSV dosyanızı yükleyin - destroy: - title: Özel Kullanıcıları kaldır - example_file: 'Örnek dosya:' - upload: Yükle reminders: new: submit: Gönder @@ -877,14 +867,19 @@ 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 custom: Özel + custom_token: Kelimeyi özelleştir expires_at: Son Geçerlilik Tarihi 'false': 'Hayır' never_expire: Asla @@ -893,6 +888,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 @@ -916,18 +924,45 @@ tr: destroy: success: Sayfa başarıyla yok edildi edit: + last_notable_change: 'Son önemli değişiklik: %{tos_version_formatted}' title: Sayfayı düzenle update: Güncelleştirme form: none: Yok + slug_help_html: 'Burada tam bağlantıları değil, kısmi yolları kullanınız. Harfleri, sayıları, kesik çizgileri, taksimleri kabul eder ve bir harf ile başlamak zorundadır. Örnek: %{url}' index: last_notable_change: Son önemli değişiklik 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 +976,14 @@ tr: participants: Kullanıcılar scope_types: Kapsam türleri scopes: kapsamları + taxonomy_filters: '"%{taxonomy} için kategori filtreleri' + 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. + trash_management: + restore: + invalid: '%{resource_name} geri yüklenirken bir hata oluştu.' users: create: error: Bu kullanıcıyı davet ederken bir hata oluştu. @@ -954,13 +997,19 @@ 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 + forms: + errors: + impersonate_user: + reason: Yönetilmeyen bir katılımcıyla ilgili bir işlem yaparken gerekçe belirtilmesi zorunludur. moderations: actions: expand: Genişlet @@ -970,6 +1019,17 @@ tr: title: Eylemler admin: reportable: + bulk_action: + hide: + failed: 'Bazı kaynakları gizlerken bir hata oluştu: %{errored}' + invalid: Kolaylaştırıcı seçilmedi. + success: Kaynaklar başarıyla gizlendi. + ignore: + invalid: Kullanıcı kolaylaştırıcısı seçilmedi. + success: Katılımcıların bildirilmesi başarıyla kaldırıldı. + unreport: + failed: 'Bazı kaynakların bildirilmesini kaldırırken bir hata oluştu: %{errored}' + success: Kaynakların engellenmesi başarıyla kaldırıldı. hide: invalid: Kaynağı gizleyen bir sorun oluştu. success: Kaynak başarıyla gizlendi. @@ -984,6 +1044,7 @@ tr: fields: hidden_at: Gizli participatory_space: Katılımcı alanı + report_count: Bildirilme sayısı reportable_type: Türü reported_content_url: Raporlanan içerik URL'si visit_url: URL'yi ziyaret et diff --git a/decidim-admin/config/locales/uk.yml b/decidim-admin/config/locales/uk.yml index 3067fc0780b3e..26c1405a55c2b 100644 --- a/decidim-admin/config/locales/uk.yml +++ b/decidim-admin/config/locales/uk.yml @@ -281,8 +281,6 @@ uk: sent_at: 'Надіслане:' subject: Тема name: Розсилання новин - participatory_space_private_user: - name: Приватний учасник простору співучасті scope: fields: name: Ім'я @@ -371,17 +369,6 @@ uk: update: error: При спробі оновити цю організацію сталася помилка. success: Організацію успішно оновлено. - participatory_space_private_users: - create: - error: При спробі додати приватного учасника до цього простору співучасті сталася помилка. - success: Приватним учасникам успішно надано доступ до простору співучасті. - destroy: - success: Успішно скасовано доступ приватних учасників до простору співучасті. - index: - title: Приватний учасник простору співучасті - new: - create: Додати - title: Додати приватного учасника простору співучасті. resource_permissions: edit: title: Редагувати права доступу diff --git a/decidim-admin/config/locales/zh-CN.yml b/decidim-admin/config/locales/zh-CN.yml index 0ee57f734f51a..28028f6f19d77 100644 --- a/decidim-admin/config/locales/zh-CN.yml +++ b/decidim-admin/config/locales/zh-CN.yml @@ -286,6 +286,10 @@ zh-CN: explanation: 管理下的参与者可以提升到标准参与者。 这意味着他们将被邀请到应用程序中,您将无法再次管理他们。 邀请的参与者将收到一封电子邮件来接受您的邀请。 new_managed_user_promotion: 新管理的参与者促销活动 promote: 升级 + members_csv_imports: + new: + csv_upload: + title: 上传您的 CSV 文件 menu: admin_log: 管理员活动日志 admins: 管理员 @@ -339,8 +343,6 @@ zh-CN: sent_to: 发送至 subject: 议 题 name: 通讯 - participatory_space_private_user: - name: 参与性空间私人参与者 scope: fields: name: 名称 @@ -462,23 +464,6 @@ zh-CN: update: error: 更新这个组织时出现问题。 success: 组织更新成功。 - participatory_space_private_users: - create: - error: 在这种参与空间中添加私人参与者时出现了问题。 - success: 参与性空间私人参与者访问成功创建。 - destroy: - error: 删除这个参与空间的私人参与者时出错。 - success: 参与性空间私人参与者访问成功被摧毁。 - index: - title: 参与性空间私人参与者 - new: - create: 创建 - title: 新的参与性空间活动私人参与者。 - participatory_space_private_users_csv_imports: - new: - csv_upload: - title: 上传您的 CSV 文件 - upload: 上传 resource_permissions: edit: submit: 提交 diff --git a/decidim-admin/config/locales/zh-TW.yml b/decidim-admin/config/locales/zh-TW.yml index 485301629fb85..014fe190af880 100644 --- a/decidim-admin/config/locales/zh-TW.yml +++ b/decidim-admin/config/locales/zh-TW.yml @@ -85,9 +85,6 @@ zh-TW: welcome_notification_body: 歡迎通知內容 welcome_notification_subject: 歡迎通知主旨 youtube_handler: YouTube 處理器 - participatory_space_private_user: - email: 電子郵件 - name: 名稱 scope: code: 代碼 name: 名稱 @@ -128,10 +125,6 @@ zh-TW: attributes: official_img_footer: allowed_file_content_types: 圖片有誤 - participatory_space_private_user_csv_import: - attributes: - file: - malformed: 匯入檔案格式有誤,請小心閱讀教學並且確認檔案是使用 UTF-8 編碼 user_group_csv_verification: attributes: file: @@ -169,8 +162,6 @@ zh-TW: import: 匯入 newsletter: new: 新電子報 - participatory_space_private_user: - new: 新參與空間私有使用者 per_page: 每頁 share: 分享 user: @@ -355,17 +346,6 @@ zh-TW: values: 'false': 官方化 'true': 非官方化 - participatory_space_private_users: - user_invitation_accepted_at_not_null: - label: 邀請已接受 - values: - 'false': 未接受 - 'true': 已接受 - user_invitation_sent_at_not_null: - label: 已發送邀請 - values: - 'false': 未發送 - 'true': 已送出 private_space_eq: label: 私人的 values: @@ -479,6 +459,10 @@ zh-TW: explanation: 受管理的參與者可以晉升為標準參與者。這意味著他們將受邀加入應用程式,您將無法再對其進行管理。被邀請的參與者將收到一封電子郵件,以接受您的邀請。 new_managed_user_promotion: 晉升新的受管理參與者 promote: 推廣 + members_csv_imports: + new: + csv_upload: + title: 請上傳您的 CSV 檔案。 menu: admin_log: 管理員活動日誌 admins: 管理員 @@ -536,8 +520,6 @@ zh-TW: sent_to: 發送至 subject: 主旨 name: 電子報 - participatory_space_private_user: - name: 參與空間私有參與者 scope: fields: name: 名稱 @@ -715,38 +697,6 @@ zh-TW: update: error: 更新此組織時發生問題。 success: 組織更新成功 - participatory_space_private_users: - create: - error: 在此參與空間中添加私人參與者時出現問題。 - success: 成功建立參與空間的私人參與者訪問權限。 - destroy: - error: 刪除參與空間的私人參與者時發生問題。 - success: 成功刪除參與空間的私人參與者訪問權限。 - index: - import_via_csv: 透過 CSV 匯入 - title: 參與空間私有參與者 - new: - create: 建立 - title: 新的參與空間私人參與者。 - participatory_space_private_users_csv_imports: - create: - invalid: 讀取 CSV 檔案時發生問題。請確保您已按照指示進行操作。 - success: CSV檔案上傳成功,我們正在發送邀請電子郵件給參與者。這可能需要一些時間。 - new: - csv_upload: - title: 請上傳您的 CSV 檔案。 - destroy: - button: 刪除所有私人參與者 - confirm: 您確定要刪除所有私人參與者嗎?此操作無法撤銷,您將無法恢復它們。 - empty: 您沒有私人參與者。 - explanation: 您有 %{count} 位私人參與者。 - title: 刪除私人參與者 - example_file: '示例文件:' - explanation: '請上傳您的 CSV 檔案。檔案的第一欄應為電子郵件,最後一欄應為要添加到參與空間的使用者的名稱,檔案中不應包含標題。請避免在使用者名稱中使用無效字元,例如 <>?%&^*#@()[]=+:;"{}\|。' - explanation_example: | - john.doe@example.org%{csv_col_sep}John Doe - jane.doe@example.org%{csv_col_sep}Jane Doe - upload: 上傳 reminders: create: error: 在創建提醒時遇到問題。 diff --git a/decidim-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/lib/decidim/admin/test/admin_members_shared_examples.rb b/decidim-admin/lib/decidim/admin/test/admin_members_shared_examples.rb new file mode 100644 index 0000000000000..302177142176f --- /dev/null +++ b/decidim-admin/lib/decidim/admin/test/admin_members_shared_examples.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +shared_examples "manage admin members examples" do + let(:other_user) { create(:user, organization:, email: "my_email@example.org") } + + let!(:member) { create(:member, user:, participatory_space:) } + + before do + switch_to_host(organization.host) + login_as user, scope: :user + visit participatory_space_edit_path + within_admin_sidebar_menu do + click_on "Members" + end + end + + it "shows the member list" do + within "#members table" do + expect(page).to have_content(member.user.email) + end + end + + it "creates a new member" do + click_on "New member" + + within ".new_member" do + fill_in :member_name, with: "John Doe" + fill_in :member_email, with: other_user.email + + find("*[type=submit]").click + end + + expect(page).to have_admin_callout("successfully") + + within "#members table" do + expect(page).to have_content(other_user.email) + end + + visit decidim_admin.root_path + expect(page).to have_content("invited #{other_user.name} to be a member") + end + + describe "when import a batch of members from csv" do + it "import a batch of members" do + click_on "Import via CSV" + + # The CSV has no headers + expect(Decidim::Admin::ParticipatorySpace::ImportMemberCsvJob).to receive(:perform_later).once.ordered.with("john.doe@example.org", "John Doe", participatory_space, user) + expect(Decidim::Admin::ParticipatorySpace::ImportMemberCsvJob).to receive(:perform_later).once.ordered.with("jane.doe@example.org", "Jane Doe", participatory_space, user) + dynamically_attach_file(:member_csv_import_file, Decidim::Dev.asset("import_members.csv")) + perform_enqueued_jobs { click_on "Upload" } + + expect(page).to have_content("CSV file uploaded successfully") + end + end + + describe "when publishing all members" do + let!(:member) { create(:member, :unpublished, user:, participatory_space:) } + + it "publishes all members" do + click_on "Publish all" + + sleep(1) + expect(member.reload).to be_published + end + + it "displays the correct log message" do + click_on "Publish all" + sleep(1) + visit decidim_admin.root_path + expect(page).to have_content("published all members of the #{translated(participatory_space.title)}") + end + end + + describe "when managing different users" do + before do + create(:member, user: other_user, participatory_space:) + visit current_path + end + + it "deletes a member" do + within "#members tr", text: other_user.email do + find("button[data-controller='dropdown']").click + accept_confirm { click_on "Delete" } + end + + expect(page).to have_admin_callout("successfully") + + within "#members table" do + expect(page).to have_no_content(other_user.email) + end + end + + context "when the user has not accepted the invitation" do + before do + form = Decidim::Admin::ParticipatorySpace::MemberForm.from_params( + name: "test", + email: "test@example.org" + ) + + Decidim::Admin::ParticipatorySpace::CreateMember.call( + form, + participatory_space + ) + + visit current_path + end + + it "resends the invitation to the user" do + within "#members tr", text: "test@example.org" do + find("button[data-controller='dropdown']").click + click_on "Resend invitation" + end + + expect(page).to have_admin_callout("successfully") + end + end + end +end diff --git a/decidim-admin/lib/decidim/admin/test/filters_participatory_space_users_examples.rb b/decidim-admin/lib/decidim/admin/test/filters_participatory_space_users_examples.rb index c6eb525ed6d95..dc43a289f55cb 100644 --- a/decidim-admin/lib/decidim/admin/test/filters_participatory_space_users_examples.rb +++ b/decidim-admin/lib/decidim/admin/test/filters_participatory_space_users_examples.rb @@ -5,7 +5,7 @@ apply_filter(label, value) within ".table-list tbody" do - expect(page).to have_content(compare_with) + expect(page).to have_content(compare_with.gsub("\n", " ")) expect(page).to have_css("tr", count: 1) end end diff --git a/decidim-admin/lib/decidim/admin/test/invite_participatory_space_admins_shared_examples.rb b/decidim-admin/lib/decidim/admin/test/invite_participatory_space_admins_shared_examples.rb index 706ad2f45c945..4a9c8f55317a6 100644 --- a/decidim-admin/lib/decidim/admin/test/invite_participatory_space_admins_shared_examples.rb +++ b/decidim-admin/lib/decidim/admin/test/invite_participatory_space_admins_shared_examples.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true -shared_examples "inviting participatory space admins" do |check_private_space: true, check_landing_page: true| +shared_examples "inviting participatory space admins" do |check_members_page: true, check_landing_page: true| let(:role) { "Administrator" } before do switch_to_host organization.host end - shared_examples "sees public space menu" do + shared_examples "sees space without members menu" do it "can access all sections" do within_admin_sidebar_menu do expect(page).to have_content(about_this_space_label) @@ -16,13 +16,13 @@ expect(page).to have_content("Components") expect(page).to have_content("Attachments") expect(page).to have_content(space_admins_label) - expect(page).to have_no_content("Members") if participatory_space.respond_to?(:private_space) + expect(page).to have_no_content("Members") if participatory_space.respond_to?(:has_members) expect(page).to have_content("Moderations") end end end - shared_examples "sees private space menu" do + shared_examples "sees space with members menu" do it "can access all sections" do within_admin_sidebar_menu do expect(page).to have_content(about_this_space_label) @@ -31,7 +31,7 @@ expect(page).to have_content("Components") expect(page).to have_content("Attachments") expect(page).to have_content(space_admins_label) - expect(page).to have_content("Members") if participatory_space.respond_to?(:private_space) + expect(page).to have_content("Members") if participatory_space.respond_to?(:has_members) expect(page).to have_content("Moderations") end end @@ -98,14 +98,14 @@ end context "and is a public space" do - it_behaves_like "sees public space menu" + it_behaves_like "sees space without members menu" end - if check_private_space - context "and is a private space" do - let(:participatory_space) { private_participatory_space } + if check_members_page + context "and is a space with members" do + let(:participatory_space) { members_participatory_space } - it_behaves_like "sees private space menu" + it_behaves_like "sees space with members menu" end end end @@ -163,14 +163,14 @@ end context "and is a public space" do - it_behaves_like "sees public space menu" + it_behaves_like "sees space without members menu" end - if check_private_space - context "and is a private space" do - let(:participatory_space) { private_participatory_space } + if check_members_page + context "and is a space with members" do + let(:participatory_space) { members_participatory_space } - it_behaves_like "sees private space menu" + it_behaves_like "sees space with members menu" end end end diff --git a/decidim-admin/lib/decidim/admin/test/manage_attachments_examples.rb b/decidim-admin/lib/decidim/admin/test/manage_attachments_examples.rb index 10187414c7b9a..c83994d04ec39 100644 --- a/decidim-admin/lib/decidim/admin/test/manage_attachments_examples.rb +++ b/decidim-admin/lib/decidim/admin/test/manage_attachments_examples.rb @@ -3,6 +3,7 @@ shared_examples "manage attachments examples" do context "when processing attachments" do let!(:attachment) { create(:attachment, attached_to:, attachment_collection:) } + let!(:attachment_with_link) { create(:attachment, :with_link, attached_to:, attachment_collection:) } before do visit current_path @@ -182,6 +183,17 @@ expect(page).to have_no_content(translated(attachment.title, locale: :en)) end + it "can delete an attachment with a link" do + within "tr", text: translated(attachment_with_link.title) do + find("button[data-controller='dropdown']").click + accept_confirm { click_on "Delete" } + end + + expect(page).to have_admin_callout("Attachment destroyed successfully") + + expect(page).to have_no_content(translated(attachment_with_link.title, locale: :en)) + end + it "can update an attachment" do within "#attachments" do within "tr", text: translated(attachment.title) do diff --git a/decidim-admin/spec/commands/decidim/admin/content_blocks/update_content_block_spec.rb b/decidim-admin/spec/commands/decidim/admin/content_blocks/update_content_block_spec.rb index 0a574e82c1304..838028dcc2fdf 100644 --- a/decidim-admin/spec/commands/decidim/admin/content_blocks/update_content_block_spec.rb +++ b/decidim-admin/spec/commands/decidim/admin/content_blocks/update_content_block_spec.rb @@ -116,6 +116,17 @@ module Decidim::Admin::ContentBlocks }.with_indifferent_access end + it "purges the background image attachment" do + attachment = content_block.images_container.background_image + + expect(attachment).to receive(:purge).and_call_original + + subject.call + content_block.reload + + expect(content_block.images_container.background_image.attached?).to be false + end + it "deletes the attachment" do expect do subject.call 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..8b6579a43008f 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.participatory_space = 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 75% 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..1ca15f31bb0c5 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,21 +2,21 @@ require "spec_helper" -module Decidim::Admin - describe CreateParticipatorySpacePrivateUser do - subject { described_class.new(form, privatable_to, via_csv:) } +module Decidim::Admin::ParticipatorySpace + describe CreateMember do + subject { described_class.new(form, participatory_space, via_csv:) } let(:via_csv) { false } - let(:privatable_to) { create(:participatory_process) } + let(:participatory_space) { create(:participatory_process) } let!(:email) { "my_email@example.org" } let!(:name) { "Weird Guy" } - 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) } + let!(:user) { create(:user, email: "my_email@example.org", organization: participatory_space.organization) } + let!(:current_user) { create(:user, email: "some_email@example.org", organization: participatory_space.organization) } let(:role) { generate_localized_title(:role) } 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,23 +112,23 @@ 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 context "when email is input with case-insensitive letters" do - let!(:admin) { create(:user, :admin, email: "admin@example.org", organization: privatable_to.organization) } + let!(:admin) { create(:user, :admin, email: "admin@example.org", organization: participatory_space.organization) } let!(:email) { "Admin@example.org" } 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..7859c4e7400d6 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, participatory_space: 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, participatory_space: 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/participatory_space/publish_all_members_spec.rb b/decidim-admin/spec/commands/decidim/admin/participatory_space/publish_all_members_spec.rb new file mode 100644 index 0000000000000..d241c88dd6324 --- /dev/null +++ b/decidim-admin/spec/commands/decidim/admin/participatory_space/publish_all_members_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim::Admin::ParticipatorySpace + describe PublishAllMembers do + subject { described_class.new(participatory_space, current_user) } + + let!(:participatory_space) { create(:participatory_process) } + let!(:user) { create(:user, email: "my_email@example.org", organization: participatory_space.organization) } + let!(:member) { create(:member, :unpublished, user:, participatory_space:, role:) } + let(:role) { generate_localized_title(:role) } + let(:current_user) { create(:user, email: "admin@example.org", organization: participatory_space.organization) } + + it "updates the published attribute" do + subject.call + + expect(member.reload.published).to be(true) + end + + it "creates an action log" do + expect { subject.call }.to change(Decidim::ActionLog, :count).by(1) + end + end +end diff --git a/decidim-admin/spec/commands/decidim/admin/participatory_space/unpublish_all_members_spec.rb b/decidim-admin/spec/commands/decidim/admin/participatory_space/unpublish_all_members_spec.rb new file mode 100644 index 0000000000000..8976dca26ad9d --- /dev/null +++ b/decidim-admin/spec/commands/decidim/admin/participatory_space/unpublish_all_members_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim::Admin::ParticipatorySpace + describe UnpublishAllMembers do + subject { described_class.new(participatory_space, current_user) } + + let!(:participatory_space) { create(:participatory_process) } + let!(:user) { create(:user, email: "my_email@example.org", organization: participatory_space.organization) } + let!(:member) { create(:member, :published, user:, participatory_space:, role:) } + let(:role) { generate_localized_title(:role) } + let(:current_user) { create(:user, email: "admin@example.org", organization: participatory_space.organization) } + + it "updates the published attribute" do + subject.call + + expect(member.reload.published).to be(false) + end + + it "creates an action log" do + expect { subject.call }.to change(Decidim::ActionLog, :count).by(1) + end + end +end 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 59% 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..6bb7c8f56c56c 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,14 +2,14 @@ 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!(: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) } + let!(:participatory_space) { create(:participatory_process) } + let!(:member) { create(:member, :unpublished, user:, role:) } + let!(:user) { create(:user, email: "my_email@example.org", organization: participatory_space.organization) } + let!(:current_user) { create(:user, email: "some_email@example.org", organization: participatory_space.organization) } let(:form) do double( @@ -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/commands/decidim/admin/publish_all_participatory_space_private_users_spec.rb b/decidim-admin/spec/commands/decidim/admin/publish_all_participatory_space_private_users_spec.rb deleted file mode 100644 index 409887aee336e..0000000000000 --- a/decidim-admin/spec/commands/decidim/admin/publish_all_participatory_space_private_users_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -module Decidim::Admin - describe PublishAllParticipatorySpacePrivateUsers 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(: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) - end - - it "creates an action log" do - expect { subject.call }.to change(Decidim::ActionLog, :count).by(1) - end - end -end diff --git a/decidim-admin/spec/commands/decidim/admin/unpublish_all_participatory_space_private_users_spec.rb b/decidim-admin/spec/commands/decidim/admin/unpublish_all_participatory_space_private_users_spec.rb deleted file mode 100644 index dbe01ab48bb50..0000000000000 --- a/decidim-admin/spec/commands/decidim/admin/unpublish_all_participatory_space_private_users_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -module Decidim::Admin - describe UnpublishAllParticipatorySpacePrivateUsers 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(: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) - end - - it "creates an action log" do - expect { subject.call }.to change(Decidim::ActionLog, :count).by(1) - 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..0a6a52c36d045 --- /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(:participatory_space) { 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, participatory_space, 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..54d2970463561 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, participatory_space: 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_attachments_spec.rb b/decidim-admin/spec/system/admin_manages_attachments_spec.rb new file mode 100644 index 0000000000000..57879fc266b4e --- /dev/null +++ b/decidim-admin/spec/system/admin_manages_attachments_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim::Admin + describe "Admin manages attachments" do + let(:organization) { create(:organization) } + let!(:participatory_process) { create(:participatory_process, organization:) } + let!(:admin) { create(:user, :admin, :confirmed, organization:) } + let!(:attachment) { create(:attachment, attached_to: participatory_process) } + + before do + switch_to_host(organization.host) + login_as admin, scope: :user + visit decidim_admin_participatory_processes.edit_participatory_process_path(participatory_process) + end + + context "when managing attachments" do + it "will persist picture when error is present" do + within_admin_sidebar_menu do + click_on "Attachments" + end + + within "#attachments" do + click_on "New attachment" + end + + within ".new_attachment" do + fill_in_i18n( + :attachment_title, + "#attachment-title-tabs", + en: "", + es: "", + ca: "" + ) + end + + within ".new_attachment" do + find_by_id("trigger-file").click + end + + dynamically_attach_file(:attachment_file, Decidim::Dev.asset("city.jpeg")) + + within ".new_attachment" do + find("*[type=submit]").click + end + + within ".new_attachment" do + find("*[type=submit]").click + end + + expect(page).to have_css("img[src*='city.jpeg']") + end + end + end +end diff --git a/decidim-admin/spec/system/admin_manages_newsletters_spec.rb b/decidim-admin/spec/system/admin_manages_newsletters_spec.rb index 04520b06392c1..d0c2e22c77878 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.participatory_space = 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/admin_manages_organization_spec.rb b/decidim-admin/spec/system/admin_manages_organization_spec.rb index 86293461ba9b3..efabd2af563e4 100644 --- a/decidim-admin/spec/system/admin_manages_organization_spec.rb +++ b/decidim-admin/spec/system/admin_manages_organization_spec.rb @@ -429,18 +429,18 @@ )["innerHTML"]).to eq("#{terms_content}

Another paragraph

".gsub("\n", "")) end - it "deletes empty list item when pressing backspace and starts new paragraph" do - find('#organization_admin_terms_of_service_body_en div[contenteditable="true"].ProseMirror').native.send_keys [:enter, :backspace, :enter], "Another paragraph" + it "allows removing empty list item with backspace" do + find('#organization_admin_terms_of_service_body_en div[contenteditable="true"].ProseMirror').native.send_keys [:enter, "test", :left, :left, :left, :left, :backspace] expect(find( "#organization-admin_terms_of_service_body-tabs-admin_terms_of_service_body-panel-0 .editor .ProseMirror" - )["innerHTML"]).to eq("#{terms_content}

Another paragraph

".gsub("\n", "")) + )["innerHTML"]).to eq("

Paragraph

  • List item 1

  • List item 2

  • List item 3test

".gsub("\n", "")) end it "deletes linebreaks (and smartbreaks) using the backspace" do find('#organization_admin_terms_of_service_body_en div[contenteditable="true"].ProseMirror').native.send_keys [:enter, :enter, :enter, :backspace, :backspace, :backspace, :backspace] expect(find( "#organization-admin_terms_of_service_body-tabs-admin_terms_of_service_body-panel-0 .editor .ProseMirror" - )["innerHTML"]).to eq(terms_content.to_s.gsub("\n", "")) + )["innerHTML"]).to eq("

Paragraph

  • List item 1

  • List item 2

  • List item

".gsub("\n", "")) end it "keeps right cursor position when using the backspace" do @@ -448,14 +448,14 @@ find('#organization_admin_terms_of_service_body_en div[contenteditable="true"].ProseMirror').native.send_keys [:enter, :backspace, :backspace, "a"] expect(find( "#organization-admin_terms_of_service_body-tabs-admin_terms_of_service_body-panel-0 .editor .ProseMirror" - )["innerHTML"]).to eq("

Paragraph

  • List item 1

  • List item 2

  • List item 3

  • abc

".to_s.gsub("\n", "")) + )["innerHTML"]).to eq("

Paragraph

  • List item 1

  • List item 2

  • List item 3abc

".to_s.gsub("\n", "")) end it "keeps right format when using the backspace" do find('#organization_admin_terms_of_service_body_en div[contenteditable="true"].ProseMirror').native.send_keys [:enter, :backspace, "abc", :left, :left, :left, :backspace] expect(find( "#organization-admin_terms_of_service_body-tabs-admin_terms_of_service_body-panel-0 .editor .ProseMirror" - )["innerHTML"]).to eq("

Paragraph

  • List item 1

  • List item 2

  • List item 3abc

".to_s.gsub("\n", "")) + )["innerHTML"]).to eq("

Paragraph

  • List item 1

  • List item 2

  • List item abc

".to_s.gsub("\n", "")) end it "keeps right cursor position when using backspace after empty list item" do @@ -463,7 +463,7 @@ find('#organization_admin_terms_of_service_body_en div[contenteditable="true"].ProseMirror').native.send_keys [:enter, :enter, :enter, :backspace, :backspace, :backspace, :backspace, :backspace, :backspace, "a"] expect(find( "#organization-admin_terms_of_service_body-tabs-admin_terms_of_service_body-panel-0 .editor .ProseMirror" - )["innerHTML"]).to eq("

Paragraph

  • List item 1

  • List item 2

  • List item 3

  • abcd

".to_s.gsub("\n", "")) + )["innerHTML"]).to eq("

Paragraph

  • List item 1

  • List item 2

  • List itemabcd

".to_s.gsub("\n", "")) end it "keeps right cursor position when using backspace after list item with text" do @@ -471,7 +471,7 @@ find('#organization_admin_terms_of_service_body_en div[contenteditable="true"].ProseMirror').native.send_keys [:enter, :backspace, :backspace, :enter, :enter, :backspace, :backspace, :backspace, :backspace, "b"] expect(find( "#organization-admin_terms_of_service_body-tabs-admin_terms_of_service_body-panel-0 .editor .ProseMirror" - )["innerHTML"]).to eq("

Paragraph

  • List item 1

  • List item 2

  • List item 3

  • abcd

".to_s.gsub("\n", "")) + )["innerHTML"]).to eq("

Paragraph

  • List item 1

  • List item 2

  • List item bcd

".to_s.gsub("\n", "")) end it "does not delete characters below when pressing backspace" do 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 69% 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..8be1fb11dfe3f 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,11 +2,11 @@ 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:) } - let(:assembly) { create(:assembly, organization:, private_space: true) } + let(:assembly) { create(:assembly, organization:, has_members: true) } before do switch_to_host(organization.host) @@ -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, participatory_space: 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 51% rename from decidim-admin/spec/system/participatory_space_private_user_spec.rb rename to decidim-admin/spec/system/member_spec.rb index ac1babe7a3816..2afe0b3eec5e7 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(:assembly) { create(:assembly, organization:, has_members: 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, participatory_space: 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/controllers/decidim/api/queries_controller.rb b/decidim-api/app/controllers/decidim/api/queries_controller.rb index cd9c810063808..1b7e2a5b38da9 100644 --- a/decidim-api/app/controllers/decidim/api/queries_controller.rb +++ b/decidim-api/app/controllers/decidim/api/queries_controller.rb @@ -30,7 +30,8 @@ def context { current_organization:, current_user: api_user, - scopes: api_scopes + scopes: api_scopes, + can_introspect: Decidim::Api.enable_anonymous_introspection || api_user&.admin? } end diff --git a/decidim-api/app/models/decidim/api/api_user.rb b/decidim-api/app/models/decidim/api/api_user.rb index 9f9cda8c8b76e..32b30df7fee5d 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 @@ -77,6 +77,10 @@ def admin_terms_accepted? def needs_password_update? false end + + def ephemeral? + extended_data["ephemeral"] + end end end end diff --git a/decidim-api/config/locales/am-ET.yml b/decidim-api/config/locales/am-ET.yml new file mode 100644 index 0000000000000..9e7679c0df56d --- /dev/null +++ b/decidim-api/config/locales/am-ET.yml @@ -0,0 +1 @@ +am: diff --git a/decidim-api/config/locales/ar.yml b/decidim-api/config/locales/ar.yml new file mode 100644 index 0000000000000..c257bc08a4aef --- /dev/null +++ b/decidim-api/config/locales/ar.yml @@ -0,0 +1 @@ +ar: diff --git a/decidim-api/config/locales/bg.yml b/decidim-api/config/locales/bg.yml new file mode 100644 index 0000000000000..d0e375da96f10 --- /dev/null +++ b/decidim-api/config/locales/bg.yml @@ -0,0 +1 @@ +bg: diff --git a/decidim-api/config/locales/bn-BD.yml b/decidim-api/config/locales/bn-BD.yml new file mode 100644 index 0000000000000..152c698290639 --- /dev/null +++ b/decidim-api/config/locales/bn-BD.yml @@ -0,0 +1 @@ +bn: diff --git a/decidim-api/config/locales/bs-BA.yml b/decidim-api/config/locales/bs-BA.yml new file mode 100644 index 0000000000000..e9e174462a158 --- /dev/null +++ b/decidim-api/config/locales/bs-BA.yml @@ -0,0 +1 @@ +bs: diff --git a/decidim-api/config/locales/ca-IT.yml b/decidim-api/config/locales/ca-IT.yml new file mode 100644 index 0000000000000..d932361e33a01 --- /dev/null +++ b/decidim-api/config/locales/ca-IT.yml @@ -0,0 +1,15 @@ +--- +ca-IT: + decidim: + api: + errors: + introspection_disabled: La introspecció està desactivada per a aquesta sol·licitud + invalid_locale: La configuració regional que s'ha facilitat no és vàlida + locale_argument_error: S'ha produït un error mentre es gestionaven internament les dades i18n + not_found: "%{type} no trobat" + permission_not_set: No s'ha establert el permís per aquest %{type} + recursion_limit_exceeded_error: S'han detectat massa consultes recurrents + too_many_aliases_error: S'han fet servir massa àlies (nicknames). Has fet servir %{size} àlies, però es permet un màxim de %{limit}. + unauthorized_field: No pots veure o editar el camp %{field} a %{type} perquè no tens permisos per fer-ho + unauthorized_mutation: No tens permís per realitzar aquesta mutació + unauthorized_object: No pots veure i editar aquest %{type} perquè no tens permisos per fer-ho diff --git a/decidim-api/config/locales/ca.yml b/decidim-api/config/locales/ca.yml new file mode 100644 index 0000000000000..843d5a6d4122f --- /dev/null +++ b/decidim-api/config/locales/ca.yml @@ -0,0 +1,15 @@ +--- +ca: + decidim: + api: + errors: + introspection_disabled: La introspecció està desactivada per a aquesta sol·licitud + invalid_locale: La configuració regional que s'ha facilitat no és vàlida + locale_argument_error: S'ha produït un error mentre es gestionaven internament les dades i18n + not_found: "%{type} no trobat" + permission_not_set: No s'ha establert el permís per aquest %{type} + recursion_limit_exceeded_error: S'han detectat massa consultes recurrents + too_many_aliases_error: S'han fet servir massa àlies (nicknames). Has fet servir %{size} àlies, però es permet un màxim de %{limit}. + unauthorized_field: No pots veure o editar el camp %{field} a %{type} perquè no tens permisos per fer-ho + unauthorized_mutation: No tens permís per realitzar aquesta mutació + unauthorized_object: No pots veure i editar aquest %{type} perquè no tens permisos per fer-ho diff --git a/decidim-api/config/locales/cs.yml b/decidim-api/config/locales/cs.yml new file mode 100644 index 0000000000000..4bdd973689027 --- /dev/null +++ b/decidim-api/config/locales/cs.yml @@ -0,0 +1,6 @@ +--- +cs: + decidim: + api: + errors: + unauthorized_mutation: Nemáte oprávnění k provedení této mutace diff --git a/decidim-api/config/locales/da.yml b/decidim-api/config/locales/da.yml new file mode 100644 index 0000000000000..347c94d5e3836 --- /dev/null +++ b/decidim-api/config/locales/da.yml @@ -0,0 +1 @@ +da: diff --git a/decidim-api/config/locales/de.yml b/decidim-api/config/locales/de.yml new file mode 100644 index 0000000000000..346523bb60ba7 --- /dev/null +++ b/decidim-api/config/locales/de.yml @@ -0,0 +1 @@ +de: diff --git a/decidim-api/config/locales/el.yml b/decidim-api/config/locales/el.yml new file mode 100644 index 0000000000000..419ec705c3e6d --- /dev/null +++ b/decidim-api/config/locales/el.yml @@ -0,0 +1 @@ +el: diff --git a/decidim-api/config/locales/en.yml b/decidim-api/config/locales/en.yml new file mode 100644 index 0000000000000..547206d45b7f9 --- /dev/null +++ b/decidim-api/config/locales/en.yml @@ -0,0 +1,15 @@ +--- +en: + decidim: + api: + errors: + introspection_disabled: Introspection is disabled for this request + invalid_locale: Invalid locale provided + locale_argument_error: There was an error while internally handling i18n data + not_found: "%{type} not found" + permission_not_set: Permission has not been set for this %{type} + recursion_limit_exceeded_error: Too many recursions detected in query + too_many_aliases_error: Too many aliases used. You have used %{size} aliases, but %{limit} are allowed. + unauthorized_field: You cannot view or edit %{field} field on %{type} because you do not have permission + unauthorized_mutation: You do not have permission to perform this mutation + unauthorized_object: You cannot view or edit this %{type} because you do not have permissions diff --git a/decidim-api/config/locales/eo.yml b/decidim-api/config/locales/eo.yml new file mode 100644 index 0000000000000..7599814048735 --- /dev/null +++ b/decidim-api/config/locales/eo.yml @@ -0,0 +1 @@ +eo: diff --git a/decidim-api/config/locales/es-MX.yml b/decidim-api/config/locales/es-MX.yml new file mode 100644 index 0000000000000..f6f64603d886d --- /dev/null +++ b/decidim-api/config/locales/es-MX.yml @@ -0,0 +1,15 @@ +--- +es-MX: + decidim: + api: + errors: + introspection_disabled: La introspección está desactivada para esta solicitud + invalid_locale: Se ha proporcionado una configuración regional no válida + locale_argument_error: Se ha producido un error mientras se gestionaban internamente los datos de i18n + not_found: "%{type} no encontrado" + permission_not_set: No se ha establecido el permiso para este %{type} + recursion_limit_exceeded_error: Se han detectado demasiadas consultas recurrentes + too_many_aliases_error: Has utilizado demasiados alias (nicknames). Has usado %{size} alias, pero se permite un máximo de %{limit}. + unauthorized_field: No puedes ver o editar el campo %{field} en %{type} porque no tienes permiso + unauthorized_mutation: No tienes permiso para realizar esta mutación + unauthorized_object: No puedes ver o editar este %{type} porque no tienes permisos diff --git a/decidim-api/config/locales/es-PY.yml b/decidim-api/config/locales/es-PY.yml new file mode 100644 index 0000000000000..6bfd67722d80b --- /dev/null +++ b/decidim-api/config/locales/es-PY.yml @@ -0,0 +1,15 @@ +--- +es-PY: + decidim: + api: + errors: + introspection_disabled: La introspección está desactivada para esta solicitud + invalid_locale: Se ha proporcionado una configuración regional no válida + locale_argument_error: Se ha producido un error mientras se gestionaban internamente los datos de i18n + not_found: "%{type} no encontrado" + permission_not_set: No se ha establecido el permiso para este %{type} + recursion_limit_exceeded_error: Se han detectado demasiadas consultas recurrentes + too_many_aliases_error: Has utilizado demasiados alias (nicknames). Has usado %{size} alias, pero se permite un máximo de %{limit}. + unauthorized_field: No puedes ver o editar el campo %{field} en %{type} porque no tienes permiso + unauthorized_mutation: No tienes permiso para realizar esta mutación + unauthorized_object: No puedes ver o editar este %{type} porque no tienes permisos diff --git a/decidim-api/config/locales/es.yml b/decidim-api/config/locales/es.yml new file mode 100644 index 0000000000000..cadabfbf65857 --- /dev/null +++ b/decidim-api/config/locales/es.yml @@ -0,0 +1,15 @@ +--- +es: + decidim: + api: + errors: + introspection_disabled: La introspección está desactivada para esta solicitud + invalid_locale: Se ha proporcionado una configuración regional no válida + locale_argument_error: Se ha producido un error mientras se gestionaban internamente los datos de i18n + not_found: "%{type} no encontrado" + permission_not_set: No se ha establecido el permiso para este %{type} + recursion_limit_exceeded_error: Se han detectado demasiadas consultas recurrentes + too_many_aliases_error: Has utilizado demasiados alias (nicknames). Has usado %{size} alias, pero se permite un máximo de %{limit}. + unauthorized_field: No puedes ver o editar el campo %{field} en %{type} porque no tienes permiso + unauthorized_mutation: No tienes permiso para realizar esta mutación + unauthorized_object: No puedes ver o editar este %{type} porque no tienes permisos diff --git a/decidim-api/config/locales/et.yml b/decidim-api/config/locales/et.yml new file mode 100644 index 0000000000000..e020c4ffc443d --- /dev/null +++ b/decidim-api/config/locales/et.yml @@ -0,0 +1 @@ +et: diff --git a/decidim-api/config/locales/eu.yml b/decidim-api/config/locales/eu.yml new file mode 100644 index 0000000000000..415c358e3dfd6 --- /dev/null +++ b/decidim-api/config/locales/eu.yml @@ -0,0 +1,13 @@ +--- +eu: + decidim: + api: + errors: + invalid_locale: Eskualde-konfigurazioa ez da baliozkoa + locale_argument_error: Akats bat bat gertatu da i18n datuak barnetik aldatzean + not_found: "%{type} ez da aurkitu" + permission_not_set: Ez da baimenik ezarri %{type} honetarako + too_many_aliases_error: Ezizen gehiegi erabiltzen dira. %{size} ezizen erabili dituzu, baina %{limit} onartuta daude. + unauthorized_field: Ezin da %{field} %{type} eremua %{type} motan ikusi edo editatu, baimenik ez duzulako + unauthorized_mutation: Aldaketa hau egiteko baimenik ez duzu + unauthorized_object: Ezin da %{type} hau ikusi edo editatu, baimenik ez duzulako diff --git a/decidim-api/config/locales/fa-IR.yml b/decidim-api/config/locales/fa-IR.yml new file mode 100644 index 0000000000000..88215f82cbd5d --- /dev/null +++ b/decidim-api/config/locales/fa-IR.yml @@ -0,0 +1 @@ +fa: diff --git a/decidim-api/config/locales/fi-plain.yml b/decidim-api/config/locales/fi-plain.yml new file mode 100644 index 0000000000000..a7ac5699bfb5c --- /dev/null +++ b/decidim-api/config/locales/fi-plain.yml @@ -0,0 +1,15 @@ +--- +fi-pl: + decidim: + api: + errors: + introspection_disabled: Rajapintamallin tarkastelu ei ole käytössä tälle pyynnölle + invalid_locale: Virheellinen kielivalinta + locale_argument_error: Lokalisointitietojen käsittelyssä tapahtui sisäinen virhetilanne + not_found: "%{type} ei löytynyt" + permission_not_set: Oikeuksia ei ole asetettu tyypille %{type} + recursion_limit_exceeded_error: Rajapintakyselyssä on liikaa rekursiivisia kyselyitä + too_many_aliases_error: Rajapintakysely käyttää liian monta alias-nimitystä. Kyselyssä on %{size} aliasta, mutta maksimissaan %{limit} on sallittu. + unauthorized_field: Sinulla ei ole oikeuksia tarkastella tai muokata kenttää %{field} tyypille %{type} + unauthorized_mutation: Sinulla ei ole oikeutta tämän muutospyynnön suorittamiseen + unauthorized_object: Sinulla ei ole oikeuksia tarkastella tai muokata tyyppiä %{type} diff --git a/decidim-api/config/locales/fi.yml b/decidim-api/config/locales/fi.yml new file mode 100644 index 0000000000000..228a7b95ab05d --- /dev/null +++ b/decidim-api/config/locales/fi.yml @@ -0,0 +1,15 @@ +--- +fi: + decidim: + api: + errors: + introspection_disabled: Rajapintamallin tarkastelu ei ole käytössä tälle pyynnölle + invalid_locale: Virheellinen kielivalinta + locale_argument_error: Lokalisointitietojen käsittelyssä tapahtui sisäinen virhetilanne + not_found: "%{type} ei löytynyt" + permission_not_set: Oikeuksia ei ole asetettu tyypille %{type} + recursion_limit_exceeded_error: Rajapintakyselyssä on liikaa rekursiivisia kyselyitä + too_many_aliases_error: Rajapintakysely käyttää liian monta alias-nimitystä. Kyselyssä on %{size} aliasta, mutta maksimissaan %{limit} on sallittu. + unauthorized_field: Sinulla ei ole oikeuksia tarkastella tai muokata kenttää %{field} tyypille %{type} + unauthorized_mutation: Sinulla ei ole oikeutta tämän muutospyynnön suorittamiseen + unauthorized_object: Sinulla ei ole oikeuksia tarkastella tai muokata tyyppiä %{type} diff --git a/decidim-api/config/locales/fr-CA.yml b/decidim-api/config/locales/fr-CA.yml new file mode 100644 index 0000000000000..340e6dde27fed --- /dev/null +++ b/decidim-api/config/locales/fr-CA.yml @@ -0,0 +1,12 @@ +--- +fr-CA: + decidim: + api: + errors: + introspection_disabled: L'introspection est désactivée pour cette requête + not_found: "%{type} introuvable" + permission_not_set: La permission n'a pas été définie pour ce %{type} + recursion_limit_exceeded_error: Trop de récursions détectées dans la requête + too_many_aliases_error: Trop d'alias utilisés. Vous avez utilisé %{size} alias, mais seulement %{limit} sont autorisés. + unauthorized_field: Vous ne pouvez pas afficher ou modifier le champ %{field} sur %{type} car vous n'avez pas la permission + unauthorized_object: Vous ne pouvez pas afficher ou modifier ce %{type} car vous n'avez pas les permissions requises diff --git a/decidim-api/config/locales/fr.yml b/decidim-api/config/locales/fr.yml new file mode 100644 index 0000000000000..3e1c4fff08919 --- /dev/null +++ b/decidim-api/config/locales/fr.yml @@ -0,0 +1,12 @@ +--- +fr: + decidim: + api: + errors: + introspection_disabled: L'introspection est désactivée pour cette requête + not_found: "%{type} introuvable" + permission_not_set: La permission n'a pas été définie pour ce %{type} + recursion_limit_exceeded_error: Trop de récursions détectées dans la requête + too_many_aliases_error: Trop d'alias utilisés. Vous avez utilisé %{size} alias, mais seulement %{limit} sont autorisés. + unauthorized_field: Vous ne pouvez pas afficher ou modifier le champ %{field} sur %{type} car vous n'avez pas la permission + unauthorized_object: Vous ne pouvez pas afficher ou modifier ce %{type} car vous n'avez pas les permissions requises diff --git a/decidim-api/config/locales/ga-IE.yml b/decidim-api/config/locales/ga-IE.yml new file mode 100644 index 0000000000000..20a9da24e96f1 --- /dev/null +++ b/decidim-api/config/locales/ga-IE.yml @@ -0,0 +1 @@ +ga: diff --git a/decidim-api/config/locales/gl.yml b/decidim-api/config/locales/gl.yml new file mode 100644 index 0000000000000..8ec5fc81c180b --- /dev/null +++ b/decidim-api/config/locales/gl.yml @@ -0,0 +1 @@ +gl: diff --git a/decidim-api/config/locales/gn-PY.yml b/decidim-api/config/locales/gn-PY.yml new file mode 100644 index 0000000000000..bd442b0ad85de --- /dev/null +++ b/decidim-api/config/locales/gn-PY.yml @@ -0,0 +1 @@ +gn: diff --git a/decidim-api/config/locales/he-IL.yml b/decidim-api/config/locales/he-IL.yml new file mode 100644 index 0000000000000..af6fa60a73f2c --- /dev/null +++ b/decidim-api/config/locales/he-IL.yml @@ -0,0 +1 @@ +he: diff --git a/decidim-api/config/locales/hr.yml b/decidim-api/config/locales/hr.yml new file mode 100644 index 0000000000000..f67f33c7e0cfc --- /dev/null +++ b/decidim-api/config/locales/hr.yml @@ -0,0 +1 @@ +hr: diff --git a/decidim-api/config/locales/hu.yml b/decidim-api/config/locales/hu.yml new file mode 100644 index 0000000000000..52314c50c979f --- /dev/null +++ b/decidim-api/config/locales/hu.yml @@ -0,0 +1 @@ +hu: diff --git a/decidim-api/config/locales/id-ID.yml b/decidim-api/config/locales/id-ID.yml new file mode 100644 index 0000000000000..8446cbad90a58 --- /dev/null +++ b/decidim-api/config/locales/id-ID.yml @@ -0,0 +1 @@ +id: diff --git a/decidim-api/config/locales/is-IS.yml b/decidim-api/config/locales/is-IS.yml new file mode 100644 index 0000000000000..1fd0783ddf221 --- /dev/null +++ b/decidim-api/config/locales/is-IS.yml @@ -0,0 +1 @@ +is-IS: diff --git a/decidim-api/config/locales/it.yml b/decidim-api/config/locales/it.yml new file mode 100644 index 0000000000000..85830635a7b97 --- /dev/null +++ b/decidim-api/config/locales/it.yml @@ -0,0 +1 @@ +it: diff --git a/decidim-api/config/locales/ja.yml b/decidim-api/config/locales/ja.yml new file mode 100644 index 0000000000000..9fc6647b434a8 --- /dev/null +++ b/decidim-api/config/locales/ja.yml @@ -0,0 +1,15 @@ +--- +ja: + decidim: + api: + errors: + introspection_disabled: このリクエストのイントロスペクションは無効です + invalid_locale: 無効なロケールが指定されました + locale_argument_error: I18n データの内部処理中にエラーが発生しました + not_found: "%{type} が見つかりません" + permission_not_set: この %{type} の権限が設定されていません + recursion_limit_exceeded_error: クエリで再帰が検出された回数が多すぎます + too_many_aliases_error: エイリアスが多すぎます。 %{size} 件のエイリアスを使用していますが、許可されているのは %{limit} 件までです。 + unauthorized_field: 権限がないため、 %{field} の %{type} フィールドを表示または編集することはできません + unauthorized_mutation: この変更を実行する権限がありません + unauthorized_object: 権限がないため、この %{type} を表示または編集できません diff --git a/decidim-api/config/locales/ka-GE.yml b/decidim-api/config/locales/ka-GE.yml new file mode 100644 index 0000000000000..57a95cb04703c --- /dev/null +++ b/decidim-api/config/locales/ka-GE.yml @@ -0,0 +1 @@ +ka: diff --git a/decidim-api/config/locales/kaa.yml b/decidim-api/config/locales/kaa.yml new file mode 100644 index 0000000000000..455cb565ea313 --- /dev/null +++ b/decidim-api/config/locales/kaa.yml @@ -0,0 +1 @@ +kaa: diff --git a/decidim-api/config/locales/ko.yml b/decidim-api/config/locales/ko.yml new file mode 100644 index 0000000000000..8a7b3b861deda --- /dev/null +++ b/decidim-api/config/locales/ko.yml @@ -0,0 +1 @@ +ko: diff --git a/decidim-api/config/locales/lb.yml b/decidim-api/config/locales/lb.yml new file mode 100644 index 0000000000000..823df018114f4 --- /dev/null +++ b/decidim-api/config/locales/lb.yml @@ -0,0 +1 @@ +lb: diff --git a/decidim-api/config/locales/lo-LA.yml b/decidim-api/config/locales/lo-LA.yml new file mode 100644 index 0000000000000..27a02bfece429 --- /dev/null +++ b/decidim-api/config/locales/lo-LA.yml @@ -0,0 +1 @@ +lo: diff --git a/decidim-api/config/locales/lt.yml b/decidim-api/config/locales/lt.yml new file mode 100644 index 0000000000000..6c5cb837ac8c1 --- /dev/null +++ b/decidim-api/config/locales/lt.yml @@ -0,0 +1 @@ +lt: diff --git a/decidim-api/config/locales/lv.yml b/decidim-api/config/locales/lv.yml new file mode 100644 index 0000000000000..1be0eabc09156 --- /dev/null +++ b/decidim-api/config/locales/lv.yml @@ -0,0 +1 @@ +lv: diff --git a/decidim-api/config/locales/mt.yml b/decidim-api/config/locales/mt.yml new file mode 100644 index 0000000000000..f7aabc7149a9b --- /dev/null +++ b/decidim-api/config/locales/mt.yml @@ -0,0 +1 @@ +mt: diff --git a/decidim-api/config/locales/nl.yml b/decidim-api/config/locales/nl.yml new file mode 100644 index 0000000000000..f009eadee5442 --- /dev/null +++ b/decidim-api/config/locales/nl.yml @@ -0,0 +1 @@ +nl: diff --git a/decidim-api/config/locales/no.yml b/decidim-api/config/locales/no.yml new file mode 100644 index 0000000000000..9296b1e8d48ee --- /dev/null +++ b/decidim-api/config/locales/no.yml @@ -0,0 +1,7 @@ +--- +"no": + decidim: + api: + errors: + not_found: "%{type} ikke funnet" + recursion_limit_exceeded_error: For mange rekursjoner oppdaget i spørringen diff --git a/decidim-api/config/locales/oc-FR.yml b/decidim-api/config/locales/oc-FR.yml new file mode 100644 index 0000000000000..325b348894124 --- /dev/null +++ b/decidim-api/config/locales/oc-FR.yml @@ -0,0 +1 @@ +oc: diff --git a/decidim-api/config/locales/om-ET.yml b/decidim-api/config/locales/om-ET.yml new file mode 100644 index 0000000000000..05e2e89c3a879 --- /dev/null +++ b/decidim-api/config/locales/om-ET.yml @@ -0,0 +1 @@ +om: diff --git a/decidim-api/config/locales/pl.yml b/decidim-api/config/locales/pl.yml new file mode 100644 index 0000000000000..a8e4dde70d6fb --- /dev/null +++ b/decidim-api/config/locales/pl.yml @@ -0,0 +1 @@ +pl: diff --git a/decidim-api/config/locales/pt-BR.yml b/decidim-api/config/locales/pt-BR.yml new file mode 100644 index 0000000000000..720068427f5e7 --- /dev/null +++ b/decidim-api/config/locales/pt-BR.yml @@ -0,0 +1,15 @@ +--- +pt-BR: + decidim: + api: + errors: + introspection_disabled: A inspeção está desabilitada para esta requisição + invalid_locale: Localidade fornecida inválida + locale_argument_error: Houve um erro ao lidar internamente com dados do i18n + not_found: "%{type} Não encontrado" + permission_not_set: A permissão não foi definida para este %{type} + recursion_limit_exceeded_error: Muitas recursões detectadas na consulta + too_many_aliases_error: Foram utilizados muitos aliases. Você usou %{size} aliases, mas são permitidos apenas %{limit}. + unauthorized_field: Você não pode visualizar ou editar o campo %{field} em %{type} porque não tem permissão + unauthorized_mutation: Você não tem permissão para realizar esta mutação + unauthorized_object: Você não pode visualizar ou editar este %{type} porque não possui permissões diff --git a/decidim-api/config/locales/pt.yml b/decidim-api/config/locales/pt.yml new file mode 100644 index 0000000000000..9cbe1f038722e --- /dev/null +++ b/decidim-api/config/locales/pt.yml @@ -0,0 +1 @@ +pt: diff --git a/decidim-api/config/locales/ro-RO.yml b/decidim-api/config/locales/ro-RO.yml new file mode 100644 index 0000000000000..2d4c1838d1164 --- /dev/null +++ b/decidim-api/config/locales/ro-RO.yml @@ -0,0 +1,15 @@ +--- +ro: + decidim: + api: + errors: + introspection_disabled: Introspection este dezactivat pentru această solicitare + invalid_locale: Limba furnizată nu este validă + locale_argument_error: A apărut o eroare la procesarea datelor privind traducerea + not_found: "%{type} nu a fost găsit" + permission_not_set: Permisiunile nu au fost setate pentru acest %{type} + recursion_limit_exceeded_error: Prea multe recursii detectate în interogare + too_many_aliases_error: Prea multe aliasuri folosite. Ați folosit aliasuri %{size}, dar %{limit} sunt permise. + unauthorized_field: Nu puteți vizualiza sau actualiza câmpul %{field} pentru %{type} deoarece nu aveți permisiuni + unauthorized_mutation: Nu aveți permisiunile necesare pentru a efectua această acțiune + unauthorized_object: Nu puteți vizualiza sau actualiza %{type} deoarece nu aveți permisiuni diff --git a/decidim-api/config/locales/ru.yml b/decidim-api/config/locales/ru.yml new file mode 100644 index 0000000000000..ddc9d1e32c29b --- /dev/null +++ b/decidim-api/config/locales/ru.yml @@ -0,0 +1 @@ +ru: diff --git a/decidim-api/config/locales/si-LK.yml b/decidim-api/config/locales/si-LK.yml new file mode 100644 index 0000000000000..b0b50956edd26 --- /dev/null +++ b/decidim-api/config/locales/si-LK.yml @@ -0,0 +1 @@ +si: diff --git a/decidim-api/config/locales/sk.yml b/decidim-api/config/locales/sk.yml new file mode 100644 index 0000000000000..f634a02824398 --- /dev/null +++ b/decidim-api/config/locales/sk.yml @@ -0,0 +1 @@ +sk: diff --git a/decidim-api/config/locales/sl.yml b/decidim-api/config/locales/sl.yml new file mode 100644 index 0000000000000..26c7ce2e31e0e --- /dev/null +++ b/decidim-api/config/locales/sl.yml @@ -0,0 +1 @@ +sl: diff --git a/decidim-api/config/locales/so-SO.yml b/decidim-api/config/locales/so-SO.yml new file mode 100644 index 0000000000000..11720879bac3d --- /dev/null +++ b/decidim-api/config/locales/so-SO.yml @@ -0,0 +1 @@ +so: diff --git a/decidim-api/config/locales/sq-AL.yml b/decidim-api/config/locales/sq-AL.yml new file mode 100644 index 0000000000000..44ddadc95a8f3 --- /dev/null +++ b/decidim-api/config/locales/sq-AL.yml @@ -0,0 +1 @@ +sq: diff --git a/decidim-api/config/locales/sr-CS.yml b/decidim-api/config/locales/sr-CS.yml new file mode 100644 index 0000000000000..9e26af81914fc --- /dev/null +++ b/decidim-api/config/locales/sr-CS.yml @@ -0,0 +1 @@ +sr: diff --git a/decidim-api/config/locales/sv.yml b/decidim-api/config/locales/sv.yml new file mode 100644 index 0000000000000..6e4bfa32b1619 --- /dev/null +++ b/decidim-api/config/locales/sv.yml @@ -0,0 +1,12 @@ +--- +sv: + decidim: + api: + errors: + invalid_locale: Ogiltigt språk angivet + locale_argument_error: Ett fel uppstod när i18n data hanterades internt + not_found: "%{type} hittades inte" + permission_not_set: Behörigheten har inte satts för denna %{type} + unauthorized_field: Du kan inte visa eller redigera %{field} fältet på %{type} eftersom du inte har behörighet + unauthorized_mutation: Du har inte behörighet att utföra denna åtgärd + unauthorized_object: Du kan inte visa eller redigera detta %{type} eftersom du inte har behörigheter diff --git a/decidim-api/config/locales/sw-KE.yml b/decidim-api/config/locales/sw-KE.yml new file mode 100644 index 0000000000000..7bf73465b1380 --- /dev/null +++ b/decidim-api/config/locales/sw-KE.yml @@ -0,0 +1 @@ +sw: diff --git a/decidim-api/config/locales/th-TH.yml b/decidim-api/config/locales/th-TH.yml new file mode 100644 index 0000000000000..a4431912a8299 --- /dev/null +++ b/decidim-api/config/locales/th-TH.yml @@ -0,0 +1 @@ +th: diff --git a/decidim-api/config/locales/ti-ER.yml b/decidim-api/config/locales/ti-ER.yml new file mode 100644 index 0000000000000..39bcd22920e44 --- /dev/null +++ b/decidim-api/config/locales/ti-ER.yml @@ -0,0 +1 @@ +ti: diff --git a/decidim-api/config/locales/tr-TR.yml b/decidim-api/config/locales/tr-TR.yml new file mode 100644 index 0000000000000..077d41667ab93 --- /dev/null +++ b/decidim-api/config/locales/tr-TR.yml @@ -0,0 +1 @@ +tr: diff --git a/decidim-api/config/locales/uk.yml b/decidim-api/config/locales/uk.yml new file mode 100644 index 0000000000000..c256c3246c4a3 --- /dev/null +++ b/decidim-api/config/locales/uk.yml @@ -0,0 +1 @@ +uk: diff --git a/decidim-api/config/locales/val-ES.yml b/decidim-api/config/locales/val-ES.yml new file mode 100644 index 0000000000000..fa70518d04b9b --- /dev/null +++ b/decidim-api/config/locales/val-ES.yml @@ -0,0 +1 @@ +val: diff --git a/decidim-api/config/locales/vi.yml b/decidim-api/config/locales/vi.yml new file mode 100644 index 0000000000000..326506f0b1a36 --- /dev/null +++ b/decidim-api/config/locales/vi.yml @@ -0,0 +1 @@ +vi: diff --git a/decidim-api/config/locales/zh-CN.yml b/decidim-api/config/locales/zh-CN.yml new file mode 100644 index 0000000000000..f0b698bf39eb2 --- /dev/null +++ b/decidim-api/config/locales/zh-CN.yml @@ -0,0 +1 @@ +zh-CN: diff --git a/decidim-api/config/locales/zh-TW.yml b/decidim-api/config/locales/zh-TW.yml new file mode 100644 index 0000000000000..cb82c0526113b --- /dev/null +++ b/decidim-api/config/locales/zh-TW.yml @@ -0,0 +1 @@ +zh-TW: diff --git a/decidim-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.rb b/decidim-api/lib/decidim/api.rb index 2cd1d429ce3d6..11f9beec80ec2 100644 --- a/decidim-api/lib/decidim/api.rb +++ b/decidim-api/lib/decidim/api.rb @@ -23,6 +23,11 @@ module Api Decidim::Env.new("API_SCHEMA_MAX_COMPLEXITY", 5000).to_i end + # defines how many aliases are permitted in a query + config_accessor :max_aliases do + Decidim::Env.new("API_SCHEMA_MAX_ALIASES", 5).to_i + end + # defines the schema max_depth to configure GraphQL query max_depth config_accessor :schema_max_depth do Decidim::Env.new("API_SCHEMA_MAX_DEPTH", 15).to_i @@ -32,12 +37,19 @@ module Api Decidim::Env.new("DECIDIM_API_DISCLOSE_SYSTEM_VERSION").present? end - # Public Setting that can make the API authentication necessary in order to + # makes the API authentication necessary in order to access it # access it. config_accessor :force_api_authentication do Decidim::Env.new("DECIDIM_API_FORCE_API_AUTHENTICATION", nil).present? end + # allows anonymous introspection queries + # If you are not sure, leave it set to false. In this way only administrator users will be able to access the introspection query. + # Otherwise, anyone can access it, causing security issues. + config_accessor :enable_anonymous_introspection do + Decidim::Env.new("DECIDIM_API_ENABLE_ANONYMOUS_INTROSPECTION", nil).present? + end + # The expiration time of the JWT tokens, after which issued token will # expire. Recommended to match the value of # `DECIDIM_OAUTH_ACCESS_TOKEN_EXPIRES_IN`. diff --git a/decidim-api/lib/decidim/api/alias_analyzer.rb b/decidim-api/lib/decidim/api/alias_analyzer.rb new file mode 100644 index 0000000000000..c4d5a2c11103c --- /dev/null +++ b/decidim-api/lib/decidim/api/alias_analyzer.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Decidim + module Api + class AliasAnalyzer < GraphQL::Analysis::AST::Analyzer + def initialize(query) + super + + @aliases = Set.new + end + + def on_enter_field(node, _parent, _visitor) + @aliases.add(node.alias) if node.alias.present? + end + + def result + if @aliases.size > Decidim::Api.max_aliases + Errors::TooManyAliasesError.new(I18n.t("decidim.api.errors.too_many_aliases_error", size: @aliases.size, limit: Decidim::Api.max_aliases)) + end + end + end + end +end diff --git a/decidim-api/lib/decidim/api/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..0225d6fde18b0 --- /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[:path].last, a[:message]].join(": ") }.join(", ") if @messages.is_a?(Array) + + @messages.to_s + end + end + end + end +end diff --git a/decidim-api/lib/decidim/api/errors/introspection_disabled_error.rb b/decidim-api/lib/decidim/api/errors/introspection_disabled_error.rb new file mode 100644 index 0000000000000..13a497a194695 --- /dev/null +++ b/decidim-api/lib/decidim/api/errors/introspection_disabled_error.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Decidim + module Api + module Errors + # i18n-tasks-use t("decidim.api.errors.introspection_disabled") + class IntrospectionDisabledError < GraphQL::ExecutionError + def to_h + super.merge({ "extensions" => { "code" => "INTROSPECTION_DISABLED_ERROR" } }) + end + end + end + end +end diff --git a/decidim-api/lib/decidim/api/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/recursion_limit_exceeded_error.rb b/decidim-api/lib/decidim/api/errors/recursion_limit_exceeded_error.rb new file mode 100644 index 0000000000000..37daaf161b4e6 --- /dev/null +++ b/decidim-api/lib/decidim/api/errors/recursion_limit_exceeded_error.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Decidim + module Api + module Errors + # i18n-tasks-use t("decidim.api.errors.recursion_limit_exceeded_error") + class RecursionLimitExceededError < GraphQL::AnalysisError + def to_h + super.merge({ "extensions" => { "code" => "RECURSION_LIMIT_EXCEEDED_ERROR" } }) + end + end + end + end +end diff --git a/decidim-api/lib/decidim/api/errors/too_many_aliases_error.rb b/decidim-api/lib/decidim/api/errors/too_many_aliases_error.rb new file mode 100644 index 0000000000000..167e0a7e285e5 --- /dev/null +++ b/decidim-api/lib/decidim/api/errors/too_many_aliases_error.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Decidim + module Api + module Errors + # i18n-tasks-use t("decidim.api.errors.too_many_aliases_error") + + class TooManyAliasesError < GraphQL::AnalysisError + def to_h + super.merge({ "extensions" => { "code" => "TOO_MANY_ALIASES_ERROR" } }) + end + end + end + end +end diff --git a/decidim-api/lib/decidim/api/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/introspection_analyzer.rb b/decidim-api/lib/decidim/api/introspection_analyzer.rb new file mode 100644 index 0000000000000..bb71a209053bf --- /dev/null +++ b/decidim-api/lib/decidim/api/introspection_analyzer.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Decidim + module Api + module IntrospectionAnalyzer + module FieldVisibility + extend ActiveSupport::Concern + + included do + def self.visible?(context) + raise Errors::IntrospectionDisabledError, I18n.t("decidim.api.errors.introspection_disabled") unless context[:can_introspect] == true + + super + end + end + end + + class SchemaType < GraphQL::Introspection::SchemaType + include FieldVisibility + end + + class TypeType < GraphQL::Introspection::TypeType + include FieldVisibility + end + + class DirectiveType < GraphQL::Introspection::DirectiveType + include FieldVisibility + end + + class DirectiveLocationEnum < GraphQL::Introspection::DirectiveLocationEnum + include FieldVisibility + end + + class EnumValueType < GraphQL::Introspection::EnumValueType + include FieldVisibility + end + + class FieldType < GraphQL::Introspection::FieldType + include FieldVisibility + end + + class InputValueType < GraphQL::Introspection::InputValueType + include FieldVisibility + end + + class TypeKindEnum < GraphQL::Introspection::TypeKindEnum + include FieldVisibility + end + end + end +end diff --git a/decidim-api/lib/decidim/api/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/recursion_analyzer.rb b/decidim-api/lib/decidim/api/recursion_analyzer.rb new file mode 100644 index 0000000000000..471c7a9b9c18b --- /dev/null +++ b/decidim-api/lib/decidim/api/recursion_analyzer.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +# This analyzer checks for too many recursions in GraphQL queries. +# Copyright (c) GitLab B.V. +# License: MIT Expat license +# This content of the class was copied from the GitLab repository +# @see https://gitlab.com/gitlab-org/gitlab/-/blob/f59f7aa0d86f07496e68abf7172edd703669e7bd/lib/gitlab/graphql/query_analyzers/ast/recursion_analyzer.rb +# To which I have modified the result format to be compatible with decidim-api. + +module Decidim + module Api + class RecursionAnalyzer < GraphQL::Analysis::AST::Analyzer + IGNORED_FIELDS = %w(node edges nodes ofType).freeze + RECURSION_THRESHOLD = 2 + + def initialize(query) + super + + @node_visits = {} + @recurring_fields = {} + end + + def on_enter_field(node, _parent, visitor) + return if skip_node?(node, visitor) + + node_name = node.name + node_visits[node_name] ||= 0 + node_visits[node_name] += 1 + + times_encountered = @node_visits[node_name] + recurring_fields[node_name] = times_encountered if recursion_too_deep?(node_name, times_encountered) + end + + # Visitors are all defined on the AST::Analyzer base class + # We override them for custom analyzers. + def on_leave_field(node, _parent, visitor) + return if skip_node?(node, visitor) + + node_name = node.name + node_visits[node_name] ||= 0 + node_visits[node_name] -= 1 + end + + def result + @recurring_fields = @recurring_fields.select { |k, v| recursion_too_deep?(k, v) } + + Decidim::Api::Errors::RecursionLimitExceededError.new I18n.t("decidim.api.errors.recursion_limit_exceeded_error") if @recurring_fields.any? + end + + private + + attr_reader :node_visits, :recurring_fields + + def recursion_too_deep?(node_name, times_encountered) + return false if IGNORED_FIELDS.include?(node_name) + + times_encountered > recursion_threshold + end + + def skip_node?(node, visitor) + # We do not want to count skipped fields or fields + # inside fragment definitions + return false if visitor.skipping? || visitor.visiting_fragment_definition? + + !node.is_a?(GraphQL::Language::Nodes::Field) || node.selections.empty? + end + + # Separated into a method to allow overriding or customization of the recursion limit. + def recursion_threshold + RECURSION_THRESHOLD + end + end + end +end diff --git a/decidim-api/lib/decidim/api/schema.rb b/decidim-api/lib/decidim/api/schema.rb index 8a0905419bece..1bed442722f2a 100644 --- a/decidim-api/lib/decidim/api/schema.rb +++ b/decidim-api/lib/decidim/api/schema.rb @@ -7,11 +7,41 @@ class Schema < GraphQL::Schema mutation(MutationType) query(QueryType) + introspection(IntrospectionAnalyzer) + query_analyzer RecursionAnalyzer + query_analyzer AliasAnalyzer + default_max_page_size Decidim::Api.schema_max_per_page max_depth Decidim::Api.schema_max_depth max_complexity Decidim::Api.schema_max_complexity 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.rb b/decidim-api/lib/decidim/api/test.rb index 580923a1dff95..e7c57f0b3f470 100644 --- a/decidim-api/lib/decidim/api/test.rb +++ b/decidim-api/lib/decidim/api/test.rb @@ -22,4 +22,5 @@ require "decidim/api/test/shared_examples/taxonomizable_interface_examples" require "decidim/api/test/shared_examples/timestamps_interface_examples" require "decidim/api/test/shared_examples/traceable_interface_examples" +require "decidim/api/test/shared_examples/mutation_context" require "decidim/api/test/type_context" diff --git a/decidim-api/lib/decidim/api/test/component_context.rb b/decidim-api/lib/decidim/api/test/component_context.rb index dd1ce779078b6..9a4b389b35342 100644 --- a/decidim-api/lib/decidim/api/test/component_context.rb +++ b/decidim-api/lib/decidim/api/test/component_context.rb @@ -2,6 +2,7 @@ shared_context "with a graphql decidim component" do include_context "with a graphql class type" + include_examples "when the introspection is disabled" let(:schema) { Decidim::Api::Schema } @@ -42,25 +43,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 +84,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 +127,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, participatory_space: 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, participatory_space: participatory_process) } it_behaves_like "graphQL visible resource" end @@ -142,7 +151,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 +169,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 +185,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, participatory_space: participatory_process) } it_behaves_like "graphQL hidden component" end end @@ -240,7 +249,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, participatory_space: participatory_process) } it_behaves_like "graphQL visible resource" end @@ -258,13 +267,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 +285,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 +300,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, participatory_space: participatory_process) } it_behaves_like "graphQL hidden component" end end @@ -308,38 +317,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, participatory_space: participatory_process) } it_behaves_like "graphQL visible resource" end end @@ -352,36 +361,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, participatory_space: 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 +406,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, participatory_space: 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 +449,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, participatory_space: 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/mutation_context.rb b/decidim-api/lib/decidim/api/test/shared_examples/mutation_context.rb similarity index 100% rename from decidim-api/lib/decidim/api/test/mutation_context.rb rename to decidim-api/lib/decidim/api/test/shared_examples/mutation_context.rb diff --git a/decidim-api/lib/decidim/api/test/type_context.rb b/decidim-api/lib/decidim/api/test/type_context.rb index dd1b52c7ddf7d..a25a4806733ad 100644 --- a/decidim-api/lib/decidim/api/test/type_context.rb +++ b/decidim-api/lib/decidim/api/test/type_context.rb @@ -15,6 +15,7 @@ let(:type_class) { described_class } let(:variables) { {} } let(:root_value) { model } + let(:can_introspect) { Decidim::Api.enable_anonymous_introspection || current_user&.admin? } let(:schema) do klass = type_class @@ -28,6 +29,29 @@ 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 + TOO_MANY_ALIASES_ERROR + INTROSPECTION_DISABLED_ERROR + RECURSION_LIMIT_EXCEEDED_ERROR + ).include?(code) + + raise GraphQL::ExecutionError, error["message"] + end + def execute_query(query, variables) result = schema.execute( query, @@ -36,17 +60,93 @@ def execute_query(query, variables) current_organization:, current_user:, current_component:, - scopes: api_scopes + scopes: api_scopes, + can_introspect: }, 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 end +shared_examples "when the introspection is disabled" do + shared_examples "check introspection behavior" do + context "and the user is not authenticated" do + let!(:current_user) { nil } + + it "raises an Decidim::Api::Errors::IntrospectionDisabledError" do + expect { response }.to raise_error(Decidim::Api::Errors::IntrospectionDisabledError, "Introspection is disabled for this request") + end + end + + context "and the user is not an admin" do + let!(:current_user) { create(:user, :confirmed, organization: current_organization) } + + it "raises an Decidim::Api::Errors::IntrospectionDisabledError" do + expect { response }.to raise_error(Decidim::Api::Errors::IntrospectionDisabledError, "Introspection is disabled for this request") + end + end + + context "and the user is an admin" do + let!(:current_user) { create(:user, :confirmed, :admin, organization: current_organization) } + + it "runs successfully" do + expect { response }.not_to raise_error + end + end + + context "and the setting is true" do + before do + allow(Decidim::Api).to receive(:enable_anonymous_introspection).and_return(true) + end + + it "runs successfully" do + expect { response }.not_to raise_error + end + end + + context "and the setting is false" do + before do + allow(Decidim::Api).to receive(:enable_anonymous_introspection).and_return(false) + end + it "raises an Decidim::Api::Errors::IntrospectionDisabledError" do + expect { response }.to raise_error(Decidim::Api::Errors::IntrospectionDisabledError, "Introspection is disabled for this request") + end + end + end + + context "when requesting the schema introspection" do + let(:query) do + %( query { __schema { types { fields { type { fields { type { name } } } } } } } ) + end + + it_behaves_like "check introspection behavior" + end + + context "when requesting the type introspection" do + let(:query) do + %( query CircularIntrospection { + __type(name: "User") { + fields { + type { + fields { + type { + name + } + } + } + } + } +} ) + end + + it_behaves_like "check introspection behavior" + end +end + shared_context "with a graphql scalar class type" do include_context "with a graphql class type" diff --git a/decidim-api/lib/decidim/api/types.rb b/decidim-api/lib/decidim/api/types.rb index d179b78ccf7bc..74e5e0e9cd0d6 100644 --- a/decidim-api/lib/decidim/api/types.rb +++ b/decidim-api/lib/decidim/api/types.rb @@ -2,6 +2,9 @@ module Decidim module Api + autoload :IntrospectionAnalyzer, "decidim/api/introspection_analyzer" + autoload :AliasAnalyzer, "decidim/api/alias_analyzer" + autoload :RecursionAnalyzer, "decidim/api/recursion_analyzer" autoload :QueryType, "decidim/api/query_type" autoload :MutationType, "decidim/api/mutation_type" autoload :Schema, "decidim/api/schema" @@ -9,6 +12,21 @@ module Api autoload :GraphqlPermissions, "decidim/api/graphql_permissions" autoload :ComponentMutationType, "decidim/api/component_mutation_type" + module Errors + autoload :IntrospectionDisabledError, "decidim/api/errors/introspection_disabled_error" + autoload :LocaleError, "decidim/api/errors/locale_error" + autoload :TooManyAliasesError, "decidim/api/errors/too_many_aliases_error" + autoload :InvalidLocaleError, "decidim/api/errors/invalid_locale_error" + 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" + autoload :RecursionLimitExceededError, "decidim/api/errors/recursion_limit_exceeded_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/lib/decidim/api/types/base_object.rb b/decidim-api/lib/decidim/api/types/base_object.rb index 96214353c8fd3..4fc315f8265af 100644 --- a/decidim-api/lib/decidim/api/types/base_object.rb +++ b/decidim-api/lib/decidim/api/types/base_object.rb @@ -10,6 +10,18 @@ class BaseObject < GraphQL::Schema::Object field_class Types::BaseField required_scopes "api:read" + + def current_user + context[:current_user] + end + + def current_component + context[:current_component] + end + + def current_organization + context[:current_organization] + end end end end diff --git a/decidim-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/controllers/queries_controller_spec.rb b/decidim-api/spec/controllers/queries_controller_spec.rb index c872dce37154c..d2c2eb65fbf3a 100644 --- a/decidim-api/spec/controllers/queries_controller_spec.rb +++ b/decidim-api/spec/controllers/queries_controller_spec.rb @@ -29,10 +29,10 @@ module Api end it "executes a query" do - post :create, params: { query: "{ __schema { queryType { name } } }" } + post :create, params: { query: "{ organization { name { translations { locale text } } } }" } parsed_response = JSON.parse(response.body)["data"] - expect(parsed_response["__schema"]["queryType"]["name"]).to eq("Query") + expect(parsed_response["organization"]["name"]["translations"]).to include("locale" => "en", "text" => translated(organization.name)) end context "with force sign in enabled" do diff --git a/decidim-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..c57f2155c3a23 --- /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("body: is too short (under 15 characters), title: 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/lib/decidim/api/recursion_analyzer_spec.rb b/decidim-api/spec/lib/decidim/api/recursion_analyzer_spec.rb new file mode 100644 index 0000000000000..33e8f3595c7e1 --- /dev/null +++ b/decidim-api/spec/lib/decidim/api/recursion_analyzer_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require "spec_helper" +require "decidim/api/test" + +module Decidim + module Api + module FooBar + class DummyModel + def initialize(id) + @id = id + end + + attr_reader :id + + def child + self.class.new(id + 1) + end + + def parent + self.class.new(id + 1) + end + end + + class DummyType < GraphQL::Schema::Object + description "Dummy type" + field :child, ::Decidim::Api::FooBar::DummyType, "Dummy child field" + field :id, Integer, "Dummy ID", null: false + field :parent, ::Decidim::Api::FooBar::DummyType, "Dummy parent field" + end + + class Query < GraphQL::Schema::Object + description "The query root of this schema" + field :parent, ::Decidim::Api::FooBar::DummyType, "Dummy field", null: false + + def parent + DummyModel.new(0) + end + end + end + + describe RecursionAnalyzer do + include_context "with a graphql class type" do + let(:schema) do + Class.new(GraphQL::Schema) do + query ::Decidim::Api::FooBar::Query + query_analyzer Decidim::Api::RecursionAnalyzer + end + end + end + + let(:model) do + Decidim::Api::FooBar::DummyModel + end + + context "when a recursion is detected" do + let!(:query) do + %( + query { + parent { child { parent { child { parent { child { id } } } } } } + } + ) + end + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::RecursionLimitExceededError) + end + end + + context "when a recursion is not detected" do + let!(:query) do + %( + query { + parent { child { parent { child { id } } } } + } + ) + end + + it "raises an error" do + expect { response }.not_to raise_error + end + end + end + end +end diff --git a/decidim-api/spec/lib/decidim/api/schema_spec.rb b/decidim-api/spec/lib/decidim/api/schema_spec.rb new file mode 100644 index 0000000000000..52be74e9e4b8a --- /dev/null +++ b/decidim-api/spec/lib/decidim/api/schema_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require "spec_helper" +require "decidim/api/test" + +module Decidim + module Api + describe Schema do + include_context "with a graphql class type" + let(:type_class) { Decidim::Api::QueryType } + + context "when restricting number of aliases" do + let!(:query) do + %({ + invalidAlias0 : __typename + invalidAlias1 : __typename + invalidAlias2 : __typename + invalidAlias3 : __typename + invalidAlias4 : __typename + invalidAlias5 : __typename + }) + end + + it "raises an error" do + expect { response }.to raise_error(Errors::TooManyAliasesError, "Too many aliases used. You have used 6 aliases, but 5 are allowed.") + end + + context "when using a custom value" do + around do |example| + aliases = Decidim::Api.max_aliases + + # 5 is the default value, we have 6 aliases in the above query definition, and we just set a higher number + Decidim::Api.max_aliases = 10 + example.run + + Decidim::Api.max_aliases = aliases + end + + it "runs successfully" do + expect(response).to include("invalidAlias0" => "Query") + end + end + end + + context "when allowing number of aliases" do + let!(:query) do + %({ + invalidAlias0 : __typename + invalidAlias1 : __typename + invalidAlias2 : __typename + invalidAlias3 : __typename + invalidAlias4 : __typename + }) + end + + it "runs successfully" do + expect(response).to include("invalidAlias0" => "Query") + end + end + end + end +end diff --git a/decidim-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/cells/decidim/assemblies/assembly_dropdown_metadata_cell.rb b/decidim-assemblies/app/cells/decidim/assemblies/assembly_dropdown_metadata_cell.rb deleted file mode 100644 index cbc110438f66a..0000000000000 --- a/decidim-assemblies/app/cells/decidim/assemblies/assembly_dropdown_metadata_cell.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -module Decidim - module Assemblies - class AssemblyDropdownMetadataCell < Decidim::ParticipatorySpaceDropdownMetadataCell - include AssembliesHelper - include Decidim::ComponentPathHelper - include ActiveLinkTo - - def decidim_assemblies - Decidim::Assemblies::Engine.routes.url_helpers - end - - private - - def nav_items_method = :assembly_nav_items - end - end -end diff --git a/decidim-assemblies/app/commands/decidim/assemblies/admin/create_assembly.rb b/decidim-assemblies/app/commands/decidim/assemblies/admin/create_assembly.rb index d201fe1f641ac..52cb80784de57 100644 --- a/decidim-assemblies/app/commands/decidim/assemblies/admin/create_assembly.rb +++ b/decidim-assemblies/app/commands/decidim/assemblies/admin/create_assembly.rb @@ -14,7 +14,7 @@ class CreateAssembly < Decidim::Commands::CreateResource :participatory_structure, :meta_scope, :purpose_of_action, :composition, :creation_date, :created_by, :created_by_other, :duration, :included_at, :closing_date, :closing_date_reason, :internal_organisation, - :is_transparent, :special_features, :twitter_handler, :facebook_handler, + :has_members, :is_transparent, :special_features, :twitter_handler, :facebook_handler, :instagram_handler, :youtube_handler, :github_handler protected diff --git a/decidim-assemblies/app/commands/decidim/assemblies/admin/update_assembly.rb b/decidim-assemblies/app/commands/decidim/assemblies/admin/update_assembly.rb index c44489b8d440d..ed8eef471e827 100644 --- a/decidim-assemblies/app/commands/decidim/assemblies/admin/update_assembly.rb +++ b/decidim-assemblies/app/commands/decidim/assemblies/admin/update_assembly.rb @@ -13,7 +13,7 @@ class UpdateAssembly < Decidim::Commands::UpdateResource :target, :participatory_scope, :participatory_structure, :meta_scope, :purpose_of_action, :composition, :creation_date, :created_by, :created_by_other, :duration, :included_at, :closing_date, :closing_date_reason, - :internal_organisation, :is_transparent, :special_features, :twitter_handler, :announcement, + :internal_organisation, :has_members, :is_transparent, :special_features, :twitter_handler, :announcement, :facebook_handler, :instagram_handler, :youtube_handler, :github_handler, :weight private diff --git a/decidim-assemblies/app/controllers/concerns/decidim/assemblies/assembly_breadcrumb.rb b/decidim-assemblies/app/controllers/concerns/decidim/assemblies/assembly_breadcrumb.rb index fd41748d057e9..a6621e6232c28 100644 --- a/decidim-assemblies/app/controllers/concerns/decidim/assemblies/assembly_breadcrumb.rb +++ b/decidim-assemblies/app/controllers/concerns/decidim/assemblies/assembly_breadcrumb.rb @@ -12,14 +12,11 @@ def current_participatory_space_breadcrumb_item return {} if current_participatory_space.blank? return super unless current_participatory_space.is_a?(Decidim::Assembly) - dropdown_cell = current_participatory_space_manifest.breadcrumb_cell - items = current_participatory_space.ancestors.map do |participatory_space| { label: participatory_space.title, url: Decidim::ResourceLocatorPresenter.new(participatory_space).path, active: false, - dropdown_cell:, resource: participatory_space } end diff --git a/decidim-assemblies/app/controllers/decidim/assemblies/admin/members_controller.rb b/decidim-assemblies/app/controllers/decidim/assemblies/admin/members_controller.rb new file mode 100644 index 0000000000000..1bea18136b568 --- /dev/null +++ b/decidim-assemblies/app/controllers/decidim/assemblies/admin/members_controller.rb @@ -0,0 +1,14 @@ +# 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 + 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..58fc249e28433 --- /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 participatory_space + 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/forms/decidim/assemblies/admin/assembly_form.rb b/decidim-assemblies/app/forms/decidim/assemblies/admin/assembly_form.rb index 654693398d58d..87428d57fbbd6 100644 --- a/decidim-assemblies/app/forms/decidim/assemblies/admin/assembly_form.rb +++ b/decidim-assemblies/app/forms/decidim/assemblies/admin/assembly_form.rb @@ -45,6 +45,7 @@ class AssemblyForm < Form attribute :participatory_processes_ids, Array[Integer] attribute :weight, Integer, default: 0 + attribute :has_members, Boolean attribute :is_transparent, Boolean attribute :promoted, Boolean attribute :private_space, Boolean diff --git a/decidim-assemblies/app/forms/decidim/assemblies/admin/assembly_import_form.rb b/decidim-assemblies/app/forms/decidim/assemblies/admin/assembly_import_form.rb index f3231df335986..4dc60fd62a8ed 100644 --- a/decidim-assemblies/app/forms/decidim/assemblies/admin/assembly_import_form.rb +++ b/decidim-assemblies/app/forms/decidim/assemblies/admin/assembly_import_form.rb @@ -31,6 +31,7 @@ class AssemblyImportForm < Form attribute :import_components, Boolean, default: true attribute :document, Decidim::Attributes::Blob + validates :document, presence: true validates :document, file_content_type: { allow: ACCEPTED_TYPES.values } validates :slug, presence: true, format: { with: Decidim::Assembly.slug_format } validates :title, translatable_presence: true 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

+
+ <%= form.check_box :has_members, help_text: t(".has_members_help") %> +
+
<%= form.check_box :private_space %>

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

diff --git a/decidim-assemblies/app/views/decidim/assemblies/admin/assembly_imports/_form.html.erb b/decidim-assemblies/app/views/decidim/assemblies/admin/assembly_imports/_form.html.erb index 186ef2fe2a463..b86fc95ee64e1 100644 --- a/decidim-assemblies/app/views/decidim/assemblies/admin/assembly_imports/_form.html.erb +++ b/decidim-assemblies/app/views/decidim/assemblies/admin/assembly_imports/_form.html.erb @@ -1,34 +1,38 @@ -<%= append_javascript_pack_tag "decidim_assemblies_admin" %> -
+
+ <%= cell("decidim/announcement", { body: t("decidim.assemblies.admin.new_import.help_html") }, callout_class: "info") %> +
+
<%= form.translated :text_field, :title, autofocus: true, aria: { label: :title } %>
+
<%= form.text_field :slug, help_text: t(".slug_help_html", url: decidim_form_slug_url(:assemblies, form.object.slug)) %>
+
- <%= form.upload :document, button_class: "button button__sm button__transparent-secondary" %> + <%= form.upload :document, label: t(".document_legend"), button_class: "button button__sm button__transparent-secondary", help_i18n_scope: "decidim.forms.file_help.import_file", help_i18n_messages: ["message_1"] %>
-
-
-
- <%= t("assembly_imports.new.select", scope: "decidim.admin") %> + +
+
+ <%= t("assembly_imports.new.select", scope: "decidim.admin") %> +
+
+
+
+
+ <%= form.check_box :import_attachments %>
-
-
-
- <%= form.check_box :import_attachments %> -
-
- <%= form.check_box :import_components %> -
-
+
+ <%= form.check_box :import_components %>
+
diff --git a/decidim-assemblies/app/views/decidim/assemblies/admin/assembly_imports/new.html.erb b/decidim-assemblies/app/views/decidim/assemblies/admin/assembly_imports/new.html.erb index d2f911ad4dcc8..b5d593aa340de 100644 --- a/decidim-assemblies/app/views/decidim/assemblies/admin/assembly_imports/new.html.erb +++ b/decidim-assemblies/app/views/decidim/assemblies/admin/assembly_imports/new.html.erb @@ -1,4 +1,5 @@ <% add_decidim_page_title(t("assembly_imports.new.title", scope: "decidim.admin")) %> +

<%= t("assembly_imports.new.title", scope: "decidim.admin") %> diff --git a/decidim-assemblies/app/views/decidim/assemblies/assemblies/show.html.erb b/decidim-assemblies/app/views/decidim/assemblies/assemblies/show.html.erb index 6b9799c119e63..fbc902c147981 100644 --- a/decidim-assemblies/app/views/decidim/assemblies/assemblies/show.html.erb +++ b/decidim-assemblies/app/views/decidim/assemblies/assemblies/show.html.erb @@ -34,6 +34,6 @@ edit_link(

- <%= resource_reference(current_participatory_space) %> +

<%= resource_reference(current_participatory_space) %>

diff --git a/decidim-assemblies/app/views/decidim/assemblies/participatory_space_private_users/index.html.erb b/decidim-assemblies/app/views/decidim/assemblies/members/index.html.erb similarity index 88% rename from decidim-assemblies/app/views/decidim/assemblies/participatory_space_private_users/index.html.erb rename to decidim-assemblies/app/views/decidim/assemblies/members/index.html.erb index 0709e746b3033..3fb3b7d068099 100644 --- a/decidim-assemblies/app/views/decidim/assemblies/participatory_space_private_users/index.html.erb +++ b/decidim-assemblies/app/views/decidim/assemblies/members/index.html.erb @@ -4,7 +4,7 @@ resource: current_participatory_space) %> <% edit_link( - decidim_admin_assemblies.participatory_space_private_users_path(current_participatory_space.slug), + decidim_admin_assemblies.members_path(current_participatory_space.slug), :update, :assembly, assembly: current_participatory_space diff --git a/decidim-assemblies/config/locales/ca-IT.yml b/decidim-assemblies/config/locales/ca-IT.yml index ddbd852fb876c..3c6b1551f8dcd 100644 --- a/decidim-assemblies/config/locales/ca-IT.yml +++ b/decidim-assemblies/config/locales/ca-IT.yml @@ -25,6 +25,7 @@ ca-IT: duration: Durada facebook: Facebook github: GitHub + has_members: Aquest espai de participació té membres hero_image: Imatge de portada import_attachments: Importar arxius adjunts import_categories: Importar categories @@ -170,8 +171,8 @@ ca-IT: components: Components info: Quant a aquesta assemblea landing_page: Disposició de la pàgina de destinació + members: Membres moderations: Moderacions - private_users: Membres see_assembly: Veure l'assemblea models: assembly: @@ -218,9 +219,11 @@ ca-IT: export: "%{user_name} ha exportat l'assemblea %{resource_name}" import: "%{user_name} ha importat l'assemblea %{resource_name}" publish: "%{user_name} ha publicat l'assemblea %{resource_name}" + publish_all_members: "%{user_name} va publicar a totes les membres de l'assemblea %{resource_name}" restore: "%{user_name} ha restaurat l'assemblea %{resource_name}" soft_delete: "%{user_name} ha mogut a la paperera l'assemblea %{resource_name}" unpublish: "%{user_name} ha despublicat l'assemblea %{resource_name}" + unpublish_all_members: "%{user_name} va despublicar a totes les membres de l'assemblea %{resource_name}" update: "%{user_name} ha actualitzat l'assemblea %{resource_name}" assembly_member: create: "%{user_name} ha afegit el membre %{resource_name} a l'assemblea %{space_name}" @@ -246,12 +249,13 @@ ca-IT: define_taxonomy_filters: Si us plau, defineix algunes filtres per aquest espai de participació abans de fer servir aquesta configuració. duration: Durada duration_help: Si la durada d'aquesta assemblea és limitada, selecciona la data de finalització. En cas contrari, apareixerà com a indefinida. + has_members_help: Podràs crear i publicar membres images: Imatges included_at_help: Selecciona la data en què es va afegir aquesta assemblea a la plataforma. No necessàriament ha de ser la mateixa que la data de creació. metadata: Metadades no_taxonomy_filters_found: No s'han trobat filtres de taxonomia. other: Altre - private_notice: Podràs administrar les participants privades un cop haguis configurar l'espai com a privat + private_notice: Fa que l'espai de participació no sigui visible per a les visitants i només ho sigui per a les usuàries membre (a no ser que també sigui transparent) select_a_created_by: Selecciona una creadora select_parent_assembly: Selecciona una assemblea mare slug_help_html: 'Els noms curts d''URL s''utilitzen per generar les URL que apunten a aquesta assemblea. Només accepta lletres, números i guions, i ha de començar amb una lletra. Exemple: %{url}' diff --git a/decidim-assemblies/config/locales/ca.yml b/decidim-assemblies/config/locales/ca.yml index 2142843a6738b..6fb12f83ef8aa 100644 --- a/decidim-assemblies/config/locales/ca.yml +++ b/decidim-assemblies/config/locales/ca.yml @@ -25,6 +25,7 @@ ca: duration: Durada facebook: Facebook github: GitHub + has_members: Aquest espai de participació té membres hero_image: Imatge de portada import_attachments: Importar arxius adjunts import_categories: Importar categories @@ -170,8 +171,8 @@ ca: components: Components info: Quant a aquesta assemblea landing_page: Disposició de la pàgina de destinació + members: Membres moderations: Moderacions - private_users: Membres see_assembly: Veure l'assemblea models: assembly: @@ -218,9 +219,11 @@ ca: export: "%{user_name} ha exportat l'assemblea %{resource_name}" import: "%{user_name} ha importat l'assemblea %{resource_name}" publish: "%{user_name} ha publicat l'assemblea %{resource_name}" + publish_all_members: "%{user_name} va publicar a totes les membres de l'assemblea %{resource_name}" restore: "%{user_name} ha restaurat l'assemblea %{resource_name}" soft_delete: "%{user_name} ha mogut a la paperera l'assemblea %{resource_name}" unpublish: "%{user_name} ha despublicat l'assemblea %{resource_name}" + unpublish_all_members: "%{user_name} va despublicar a totes les membres de l'assemblea %{resource_name}" update: "%{user_name} ha actualitzat l'assemblea %{resource_name}" assembly_member: create: "%{user_name} ha afegit el membre %{resource_name} a l'assemblea %{space_name}" @@ -246,12 +249,13 @@ ca: define_taxonomy_filters: Si us plau, defineix algunes filtres per aquest espai de participació abans de fer servir aquesta configuració. duration: Durada duration_help: Si la durada d'aquesta assemblea és limitada, selecciona la data de finalització. En cas contrari, apareixerà com a indefinida. + has_members_help: Podràs crear i publicar membres images: Imatges included_at_help: Selecciona la data en què es va afegir aquesta assemblea a la plataforma. No necessàriament ha de ser la mateixa que la data de creació. metadata: Metadades no_taxonomy_filters_found: No s'han trobat filtres de taxonomia. other: Altre - private_notice: Podràs administrar les participants privades un cop haguis configurar l'espai com a privat + private_notice: Fa que l'espai de participació no sigui visible per a les visitants i només ho sigui per a les usuàries membre (a no ser que també sigui transparent) select_a_created_by: Selecciona una creadora select_parent_assembly: Selecciona una assemblea mare slug_help_html: 'Els noms curts d''URL s''utilitzen per generar les URL que apunten a aquesta assemblea. Només accepta lletres, números i guions, i ha de començar amb una lletra. Exemple: %{url}' diff --git a/decidim-assemblies/config/locales/cs.yml b/decidim-assemblies/config/locales/cs.yml index 9888a825e1b3c..29e12336e2084 100644 --- a/decidim-assemblies/config/locales/cs.yml +++ b/decidim-assemblies/config/locales/cs.yml @@ -176,8 +176,8 @@ cs: components: Komponenty info: O tomto shromáždění landing_page: Rozložení vstupní strany + members: Členové moderations: Moderování - private_users: Členové see_assembly: Zobrazit shromáždění models: assembly: @@ -257,7 +257,6 @@ cs: metadata: Metadata no_taxonomy_filters_found: Nebyly nalezeny žádné filtry taxonomie. other: Ostatní - private_notice: Budete moci spravovat soukromé účastníky po nastavení jako soukromé select_a_created_by: Vybrat vytvořeno od select_parent_assembly: Vybrat nadřazené shromáždění slug_help_html: 'URL slugy se používají ke generování adres URL, které odkazují na toto shromáždění. Povolená jsou pouze písmena, číslice a pomlčky a musí začínat písmenem. Příklad: %{url}' @@ -390,6 +389,7 @@ cs: assembly_type: Typ shromáždění closing_date: Datum ukončení shromáždění closing_date_reason: Proč bylo shromáždění uzavřeno + component_settings: Nastavení komponenty shromáždění composition: Složení shromáždění created_at: Datum, kdy byl tento prostor vytvořen created_by: Kdo vytvořil toto shromáždění diff --git a/decidim-assemblies/config/locales/de.yml b/decidim-assemblies/config/locales/de.yml index 88b10dd7771f5..4e109b33e4b7e 100644 --- a/decidim-assemblies/config/locales/de.yml +++ b/decidim-assemblies/config/locales/de.yml @@ -25,6 +25,7 @@ de: duration: Aktiv bis facebook: Facebook github: GitHub + has_members: Dieser Bereich hat Mitglieder hero_image: Hauptbild import_attachments: Anhänge importieren import_categories: Kategorien importieren @@ -170,8 +171,8 @@ de: components: Komponenten info: Über dieses Gremium landing_page: Startseiten-Layout + members: Mitglieder moderations: Moderationen - private_users: Mitglieder see_assembly: Gremium ansehen models: assembly: @@ -218,9 +219,11 @@ de: export: "%{user_name} hat das Gremium %{resource_name} exportiert" import: "%{user_name} hat das Gremium %{resource_name} importiert" publish: "%{user_name} hat das Gremium %{resource_name} veröffentlicht" + publish_all_members: "%{user_name} hat alle Mitglieder des Gremiums %{resource_name} veröffentlicht" restore: "%{user_name} hat das Gremium %{resource_name} wiederhergestellt" soft_delete: "%{user_name} hat das Gremium %{resource_name} in den Papierkorb verschoben" unpublish: "%{user_name} hat das Gremium %{resource_name} auf \"unveröffentlicht\" gesetzt" + unpublish_all_members: "%{user_name} hat die Veröffentlichung aller Mitglieder des Gremiums %{resource_name} aufgehoben" update: "%{user_name} hat das Gremium %{resource_name} aktualisiert" assembly_member: create: "%{user_name} hat das Mitglied %{resource_name} im Gremium %{space_name} erstellt" @@ -246,12 +249,12 @@ de: define_taxonomy_filters: Bitte definieren Sie einige Filter für diesen partizipativen Bereich, bevor Sie diese Einstellung verwenden. duration: Aktiv bis duration_help: Wenn die Dauer dieses Gremiums begrenzt ist, dann wählen Sie hier das Enddatum aus. Andernfalls wird das Datum als unbestimmt angezeigt. + has_members_help: Sie können Mitglieder erstellen und veröffentlichen images: Bilder included_at_help: Wählen Sie das Datum aus, an dem dieses Gremium zur Plattform hinzugefügt wurde. Das Datum muss nicht zwingend mit dem Gründungsdatum übereinstimmen. metadata: Metadaten no_taxonomy_filters_found: Keine Klassifizierungsfilter gefunden. other: Andere - private_notice: Sie werden in der Lage sein, private Teilnehmer zu verwalten, nachdem Sie das Gremium als privat festgelegt haben select_a_created_by: Wählen Sie eine erstellt von aus select_parent_assembly: Übergeordnetes Gremium auswählen slug_help_html: 'URL-Slugs werden zum Generieren der URLs verwendet, die auf dieses Gremium verweisen. Akzeptiert werden nur Buchstaben, Zahlen und Bindestriche und es muss mit einem Buchstaben beginnen. Beispiel: %{url}' diff --git a/decidim-assemblies/config/locales/en.yml b/decidim-assemblies/config/locales/en.yml index 4252a87199e40..32e03672f6ed0 100644 --- a/decidim-assemblies/config/locales/en.yml +++ b/decidim-assemblies/config/locales/en.yml @@ -26,6 +26,7 @@ en: duration: Duration facebook: Facebook github: GitHub + has_members: This space has members hero_image: Home image import_attachments: Import attachments import_categories: Import categories @@ -171,8 +172,8 @@ en: components: Components info: About this assembly landing_page: Landing page layout + members: Members moderations: Moderations - private_users: Members see_assembly: See assembly models: assembly: @@ -219,9 +220,11 @@ en: export: "%{user_name} exported the %{resource_name} assembly" import: "%{user_name} imported the %{resource_name} assembly" publish: "%{user_name} published the %{resource_name} assembly" + publish_all_members: "%{user_name} published all members of the %{resource_name} assembly" restore: "%{user_name} restored the %{resource_name} assembly" soft_delete: "%{user_name} moved to trash the %{resource_name} assembly" unpublish: "%{user_name} unpublished the %{resource_name} assembly" + unpublish_all_members: "%{user_name} unpublished all members of the %{resource_name} assembly" update: "%{user_name} updated the %{resource_name} assembly" assembly_member: create: "%{user_name} created the %{resource_name} member in the %{space_name} assembly" @@ -247,12 +250,13 @@ en: define_taxonomy_filters: Please define some filters for this participatory space before using this setting. duration: Duration duration_help: If the duration of this assembly is limited, select the end date. Otherwise, it will appear as indefinite. + has_members_help: You will be able to create and publish members images: Images included_at_help: Select the date when this assembly was added to the platform. It does not necessarily have to be the same as the creation date. metadata: Metadata no_taxonomy_filters_found: No taxonomy filters found. other: Other - private_notice: You will be able to manage private participants after setting it as private + private_notice: Makes the space not visible for visitors and only for members (except if it is also transparent) select_a_created_by: Select a created by select_parent_assembly: Select parent assembly slug_help_html: 'URL slugs are used to generate the URLs that point to this assembly. Only accepts letters, numbers and dashes, and must start with a letter. Example: %{url}' @@ -265,6 +269,7 @@ en: slug_help_html: 'URL slugs are used to generate the URLs that point to this assembly. Only accepts letters, numbers and dashes, and must start with a letter. Example: %{url}' assembly_imports: form: + document_legend: Add a document slug_help_html: 'URL slugs are used to generate the URLs that point to this assembly. Only accepts letters, numbers and dashes, and must start with a letter. Example: %{url}' assembly_members: form: @@ -277,6 +282,7 @@ en: new_import: accepted_types: json: JSON + help_html: This import feature allows you to create a new assembly from an exported JSON file. You can export an assembly from another organization or from this same organization. The imported assembly will include its settings, components, and attachments (if selected). assemblies: description: area_name: Area diff --git a/decidim-assemblies/config/locales/es-MX.yml b/decidim-assemblies/config/locales/es-MX.yml index 73a438add08fe..f9d5873461912 100644 --- a/decidim-assemblies/config/locales/es-MX.yml +++ b/decidim-assemblies/config/locales/es-MX.yml @@ -25,6 +25,7 @@ es-MX: duration: Duración facebook: Facebook github: GitHub + has_members: Este espacio de participación tiene miembros hero_image: Imagen de portada import_attachments: Importar archivos adjuntos import_categories: Importar categorias @@ -170,8 +171,8 @@ es-MX: components: Componentes info: Acerca de esta asamblea landing_page: Disposición de la página de aterrizaje + members: Miembros moderations: Moderaciones - private_users: Miembros see_assembly: Ver la asamblea models: assembly: @@ -218,9 +219,11 @@ es-MX: export: "%{user_name} exportó la asamblea %{resource_name}" import: "%{user_name} importó la asamblea %{resource_name}" publish: "%{user_name} publicó la asamblea %{resource_name}" + publish_all_members: "%{user_name} publicó todas las miembros de la asamblea %{resource_name}" restore: "%{user_name} ha restaurado la asamblea %{resource_name}" soft_delete: "%{user_name} ha movido a la papelera la asamblea %{resource_name}" unpublish: "%{user_name} despublicó la asamblea %{resource_name}" + unpublish_all_members: "%{user_name} despublicó a todas las miembros de la asamblea %{resource_name}" update: "%{user_name} actualizó la asamblea %{resource_name}" assembly_member: create: "%{user_name} creó el miembro %{resource_name} en la asamblea %{space_name}" @@ -246,12 +249,13 @@ es-MX: define_taxonomy_filters: Por favor, define algunos filtros para este espacio de participación antes de utilizar esta configuración. duration: Duración duration_help: Si la duración de esta asamblea es limitada, selecciona la fecha de finalización. De lo contrario, aparecerá como indefinida. + has_members_help: Podrás crear y publicar miembros images: Imágenes included_at_help: Selecciona la fecha en la que se añadió esta asamblea a la plataforma. No es necesario que sea la misma que la fecha de creación. metadata: Metadatos no_taxonomy_filters_found: No se han encontrado filtros de taxonomía. other: Otro - private_notice: Podrás administrar las participantes privadas después de configurar el espacio como privado + private_notice: Hace que el espacio de participación no sea visible para las visitantes y sólo lo sea para las usuarias miembro (excepto si también es transparente) select_a_created_by: Selecciona creada por select_parent_assembly: Selecciona asamblea principal slug_help_html: 'Los textos cortos de URL se utilizan para generar las URL que apuntan a esta asamblea. Sólo acepta letras, números y guiones, y debe comenzar con una letra. Ejemplo: %{url}' diff --git a/decidim-assemblies/config/locales/es-PY.yml b/decidim-assemblies/config/locales/es-PY.yml index 53cf928f7b688..e5b7b98d5f0a3 100644 --- a/decidim-assemblies/config/locales/es-PY.yml +++ b/decidim-assemblies/config/locales/es-PY.yml @@ -25,6 +25,7 @@ es-PY: duration: Duración facebook: Facebook github: GitHub + has_members: Este espacio de participación tiene miembros hero_image: Imagen de portada import_attachments: Importar archivos adjuntos import_categories: Importar categorias @@ -170,8 +171,8 @@ es-PY: components: Componentes info: Acerca de esta asamblea landing_page: Disposición de la página de aterrizaje + members: Miembros moderations: Moderaciones - private_users: Miembros see_assembly: Ver la asamblea models: assembly: @@ -218,9 +219,11 @@ es-PY: export: "%{user_name} ha exportado la asamblea %{resource_name}" import: "%{user_name} ha importado la asamblea %{resource_name}" publish: "%{user_name} publicó la asamblea %{resource_name}" + publish_all_members: "%{user_name} publicó todas las miembros de la asamblea %{resource_name}" restore: "%{user_name} ha restaurado la asamblea %{resource_name}" soft_delete: "%{user_name} ha movido a la papelera la asamblea %{resource_name}" unpublish: "%{user_name} despublicó la asamblea %{resource_name}" + unpublish_all_members: "%{user_name} despublicó a todas las miembros de la asamblea %{resource_name}" update: "%{user_name} actualizó la asamblea %{resource_name}" assembly_member: create: "%{user_name} creó el miembro %{resource_name} en la asamblea %{space_name}" @@ -246,12 +249,13 @@ es-PY: define_taxonomy_filters: Por favor, define algunos filtros para este espacio de participación antes de utilizar esta configuración. duration: Duración duration_help: Si la duración de esta asamblea es limitada, selecciona la fecha de finalización. De lo contrario, aparecerá como indefinida. + has_members_help: Podrás crear y publicar miembros images: Imágenes included_at_help: Selecciona la fecha en la que se añadió esta asamblea a la plataforma. No necesariamente tiene que ser la misma que la fecha de creación. metadata: Metadatos no_taxonomy_filters_found: No se han encontrado filtros de taxonomía. other: Otro - private_notice: Podrás administrar las participantes privadas después de configurar el espacio como privado + private_notice: Hace que el espacio de participación no sea visible para las visitantes y sólo lo sea para las usuarias miembro (excepto si también es transparente) select_a_created_by: Selecciona creada por select_parent_assembly: Seleccionar ensamblaje principal slug_help_html: 'Los textos cortos de URL se utilizan para generar las URL que apuntan a esta asamblea. Sólo acepta letras, números y guiones, y debe comenzar con una letra. Ejemplo: %{url}' diff --git a/decidim-assemblies/config/locales/es.yml b/decidim-assemblies/config/locales/es.yml index fff88fefca145..9b43b2f0e3f7b 100644 --- a/decidim-assemblies/config/locales/es.yml +++ b/decidim-assemblies/config/locales/es.yml @@ -25,6 +25,7 @@ es: duration: Duración facebook: Facebook github: GitHub + has_members: Este espacio de participación tiene miembros hero_image: Imagen de portada import_attachments: Importar archivos adjuntos import_categories: Importar categorias @@ -170,8 +171,8 @@ es: components: Componentes info: Acerca de esta asamblea landing_page: Disposición de la página de aterrizaje + members: Miembros moderations: Moderaciones - private_users: Miembros see_assembly: Ver la asamblea models: assembly: @@ -218,9 +219,11 @@ es: export: "%{user_name} ha exportado la asamblea %{resource_name}" import: "%{user_name} ha importado la asamblea %{resource_name}" publish: "%{user_name} publicó la asamblea %{resource_name}" + publish_all_members: "%{user_name} publicó todas las miembros de la asamblea %{resource_name}" restore: "%{user_name} ha restaurado la asamblea %{resource_name}" soft_delete: "%{user_name} ha movido a la papelera la asamblea %{resource_name}" unpublish: "%{user_name} despublicó la asamblea %{resource_name}" + unpublish_all_members: "%{user_name} despublicó a todas las miembros de la asamblea %{resource_name}" update: "%{user_name} actualizó la asamblea %{resource_name}" assembly_member: create: "%{user_name} creó el miembro %{resource_name} en la asamblea %{space_name}" @@ -246,12 +249,13 @@ es: define_taxonomy_filters: Por favor, define algunos filtros para este espacio de participación antes de utilizar esta configuración. duration: Duración duration_help: Si la duración de esta asamblea es limitada, selecciona la fecha de finalización. De lo contrario, aparecerá como indefinida. + has_members_help: Podrás crear y publicar miembros images: Imágenes included_at_help: Selecciona la fecha en la que se añadió esta asamblea a la plataforma. No necesariamente tiene que ser la misma que la fecha de creación. metadata: Metadatos no_taxonomy_filters_found: No se han encontrado filtros de taxonomía. other: Otro - private_notice: Podrás administrar las participantes privadas después de configurar el espacio como privado + private_notice: Hace que el espacio de participación no sea visible para las visitantes y sólo lo sea para las usuarias miembro (excepto si también es transparente) select_a_created_by: Selecciona una creadora select_parent_assembly: Selecciona una asamblea madre slug_help_html: 'Los textos cortos de URL se utilizan para generar las URL que apuntan a esta asamblea. Sólo acepta letras, números y guiones, y debe comenzar con una letra. Ejemplo: %{url}' diff --git a/decidim-assemblies/config/locales/eu.yml b/decidim-assemblies/config/locales/eu.yml index a68548bdbeaec..beec6d7e9ce39 100644 --- a/decidim-assemblies/config/locales/eu.yml +++ b/decidim-assemblies/config/locales/eu.yml @@ -25,6 +25,7 @@ eu: duration: Iraupena facebook: Facebook github: GitHub + has_members: Partaidetza-espazio honek kideak ditu hero_image: Hasierako orriko irudia import_attachments: Inportatu erantsitako fitxategiak import_categories: Inportatu kategoriak @@ -170,8 +171,8 @@ eu: components: Osagaiak info: Batzar honi buruz landing_page: Lurreratze-orrien antolaketa + members: Kideak moderations: Moderazioak - private_users: Kideak see_assembly: Ikusi batzarra models: assembly: @@ -218,9 +219,11 @@ eu: export: "%{user_name} parte-hartzaileak %{resource_name} batzarra esportatu du" import: "%{user_name} parte-hartzaileak %{resource_name} batzarra inportatu du" publish: "%{user_name} parte-hartzaileak %{resource_name} batzarra argitaratu du" + publish_all_members: "%{user_name} parte-hartzaileak %{resource_name} batzarreko kide guztiak argitaratu ditu" restore: "%{user_name} parte-hartzaileak %{resource_name} batzarra berreskuratu du" soft_delete: "%{user_name} parte-hartzaileak %{resource_name} batzarra zaborrontzira eraman du" unpublish: "%{user_name} parte-hartzaileak %{resource_name} batzarra desargitaratu du" + unpublish_all_members: "%{user_name} parte-hartzaileak %{resource_name} batzarreko kide guztiak desargitaratu ditu" update: "%{user_name} parte-hartzaileak %{resource_name} batzarra eguneratu du" assembly_member: create: "%{user_name} parte-hartzaileak %{resource_name} kidea sortu du %{space_name} batzarrean" @@ -246,12 +249,13 @@ eu: define_taxonomy_filters: Mesedez, eszenatoki hau erabili aurretik, zehaztu espazio parte-hartzaile honetarako iragazki batzuk. duration: Iraupena duration_help: Batzar honen iraupena mugatua bada, hautatu amaiera-data. Bestela, mugagabea izango da. + has_members_help: Kideak sortu eta argitaratu ahal izango dituzu images: Irudiak included_at_help: Hautatu zein egunetan gehitu zen batzar hori plataformara. Ez du zertan sortze-data bera izan. metadata: Metadata no_taxonomy_filters_found: Ez da taxonomia-iragazkirik aurkitu. other: Beste bat - private_notice: Parte hartzaile pribatuak kudeatu ahal izango dituzu pribatu gisa ezarri ondoren + private_notice: Bisitarientzat eta bazkideentzat solikik (gardena bada izan ezik) ez da partaidetza-espazioa ikusten select_a_created_by: Hautatu egilea select_parent_assembly: Hautatu batzar nagusia slug_help_html: 'URL-ren testu laburrak batzar honetara daramaten URL-ak sortzeko erabiltzen dira. Letrak, zenbakiak eta gidoiak soilik onartzen ditu, eta letra batez hasi behar du. Adibidea: %{url}' diff --git a/decidim-assemblies/config/locales/fi-plain.yml b/decidim-assemblies/config/locales/fi-plain.yml index 6daad0218b102..8966571717411 100644 --- a/decidim-assemblies/config/locales/fi-plain.yml +++ b/decidim-assemblies/config/locales/fi-plain.yml @@ -25,6 +25,7 @@ fi-pl: duration: Kesto facebook: Facebook github: GitHub + has_members: Tässä tilassa on jäseniä hero_image: Etusivun kuva import_attachments: Tuo liitteitä import_categories: Tuo aihepiirejä @@ -170,8 +171,8 @@ fi-pl: components: Komponentit info: Tietoa tästä ryhmästä landing_page: Laskeutumissivun asettelu + members: Jäsenet moderations: Moderoinnit - private_users: Jäsenet see_assembly: Näytä ryhmä models: assembly: @@ -218,9 +219,11 @@ fi-pl: export: "%{user_name} vei ryhmän %{resource_name}" import: "%{user_name} toi ryhmän %{resource_name}" publish: "%{user_name} julkaisi %{resource_name} ryhmän" + publish_all_members: "%{user_name} julkaisi ryhmän %{resource_name} jäsenet" restore: "%{user_name} palautti ryhmän %{resource_name}" soft_delete: "%{user_name} siirsi ryhmän %{resource_name} roskakoriin" unpublish: "%{user_name} lopetti %{resource_name} ryhmän julkaisemisen" + unpublish_all_members: "%{user_name} perui ryhmän %{resource_name} jäsenten julkaisun" update: "%{user_name} päivitti %{resource_name} ryhmän" assembly_member: create: "%{user_name} loi %{resource_name} jäsenen %{space_name} ryhmässä" @@ -246,12 +249,13 @@ fi-pl: define_taxonomy_filters: Määritä osallistumistilalle suodattimia ennen kuin käytät tätä asetusta. duration: Kesto duration_help: Jos ryhmän kesto on rajoitettu, valitse päättymispäivä. Muussa tapauksessa se näkyy määrittelemättömänä. + has_members_help: Voit luoda ja julkaista jäseniä images: kuvat included_at_help: Valitse päivämäärä, jolloin tämä ryhmä lisättiin alustalle. Sen ei välttämättä tarvitse olla sama kuin luontipäivä. metadata: metadata no_taxonomy_filters_found: Luokittelusuodattimia ei löytynyt. other: muut - private_notice: Voit hallinnoida osallistumistilan yksityisiä käyttäjiä asettamalla osallistumistilan yksityiseksi + private_notice: Piilottaa osallistumistilan verkkosivuston vierailijoiden näkyvistä (ellei osallistumistila ole myös läpinäkyvä) select_a_created_by: Valitse luoja select_parent_assembly: Valitse pääryhmä slug_help_html: 'URL-tunnisteita käytetään tähän ryhmään osoittavien URL-osoitteiden luonnissa. Hyväksyy kirjaimet, numerot ja viivat. Ensimmäinen merkki on oltava kirjain. Esimerkiksi: %{url}' @@ -384,6 +388,7 @@ fi-pl: assembly_type: Ryhmän tyyppi closing_date: Ryhmän sulkemispäivä closing_date_reason: Miksi tämä ryhmä suljettiin + component_settings: Ryhmän komponenttiasetukset composition: Ryhmän kokoonpano created_at: Tilan luontiaika created_by: Kuka tämän ryhmän on luonut diff --git a/decidim-assemblies/config/locales/fi.yml b/decidim-assemblies/config/locales/fi.yml index bff1b363cea02..637d900d6ab75 100644 --- a/decidim-assemblies/config/locales/fi.yml +++ b/decidim-assemblies/config/locales/fi.yml @@ -25,6 +25,7 @@ fi: duration: Kesto facebook: Facebook github: GitHub + has_members: Tässä tilassa on jäseniä hero_image: Etusivun kuva import_attachments: Tuo liitteitä import_categories: Tuo aihepiirejä @@ -170,8 +171,8 @@ fi: components: Komponentit info: Tietoa tästä ryhmästä landing_page: Laskeutumissivun asettelu + members: Jäsenet moderations: Moderoinnit - private_users: Jäsenet see_assembly: Näytä ryhmä models: assembly: @@ -218,9 +219,11 @@ fi: export: "%{user_name} vei ryhmän %{resource_name}" import: "%{user_name} toi ryhmän %{resource_name}" publish: "%{user_name} julkaisi %{resource_name} ryhmän" + publish_all_members: "%{user_name} julkaisi ryhmän %{resource_name} jäsenet" restore: "%{user_name} palautti ryhmän %{resource_name}" soft_delete: "%{user_name} siirsi ryhmän %{resource_name} roskakoriin" unpublish: "%{user_name} perui ryhmän %{resource_name} julkaisun" + unpublish_all_members: "%{user_name} perui ryhmän %{resource_name} jäsenten julkaisun" update: "%{user_name} päivitti %{resource_name} ryhmän" assembly_member: create: "%{user_name} loi %{resource_name} jäsenen %{space_name} ryhmässä" @@ -246,12 +249,13 @@ fi: define_taxonomy_filters: Määritä osallistumistilalle suodattimia ennen kuin käytät tätä asetusta. duration: Kesto duration_help: Jos ryhmän kesto on rajoitettu, valitse päättymispäivä. Muussa tapauksessa se näkyy määrittelemättömänä. + has_members_help: Voit luoda ja julkaista jäseniä images: Kuvat included_at_help: Valitse päivämäärä, jolloin tämä ryhmä lisättiin alustalle. Sen ei välttämättä tarvitse olla sama kuin luontipäivä. metadata: Metatiedot no_taxonomy_filters_found: Luokittelusuodattimia ei löytynyt. other: Muut - private_notice: Voit hallinnoida osallistumistilan yksityisiä käyttäjiä asettamalla osallistumistilan yksityiseksi + private_notice: Piilottaa osallistumistilan verkkosivuston vierailijoiden näkyvistä (ellei osallistumistila ole myös läpinäkyvä) select_a_created_by: Valitse luoja select_parent_assembly: Valitse pääryhmä slug_help_html: 'URL-tunnisteita käytetään tähän ryhmään osoittavien URL-osoitteiden luonnissa. Hyväksyy kirjaimet, numerot ja viivat. Ensimmäinen merkki on oltava kirjain. Esimerkiksi: %{url}' @@ -384,6 +388,7 @@ fi: assembly_type: Ryhmän tyyppi closing_date: Ryhmän sulkemispäivä closing_date_reason: Miksi tämä ryhmä suljettiin + component_settings: Ryhmän komponenttiasetukset composition: Ryhmän kokoonpano created_at: Tilan luontiaika created_by: Kuka tämän ryhmän on luonut diff --git a/decidim-assemblies/config/locales/fr-CA.yml b/decidim-assemblies/config/locales/fr-CA.yml index 5b7d3dd91aa70..aefd524c9818c 100644 --- a/decidim-assemblies/config/locales/fr-CA.yml +++ b/decidim-assemblies/config/locales/fr-CA.yml @@ -25,6 +25,7 @@ fr-CA: duration: Durée facebook: Facebook github: GitHub + has_members: Cet espace a des membres hero_image: Image de la page d'accueil import_attachments: Importer les pièces jointes import_categories: Importer les catégories @@ -169,8 +170,8 @@ fr-CA: attachments: Documents liés components: Composants info: À propos de cette assemblée + members: Membres moderations: Modérations - private_users: Membres see_assembly: Voir l'assemblée models: assembly: @@ -217,9 +218,11 @@ fr-CA: export: "%{user_name} a exporté l'assemblée %{resource_name}" import: "%{user_name} a importé l'assemblée %{resource_name}" publish: "%{user_name} a publié l'assemblée %{resource_name}" + publish_all_members: "%{user_name} a publié tous les membres de l'assemblée %{resource_name}" restore: "%{user_name} a restauré l'assemblée %{resource_name}" soft_delete: "%{user_name} a déplacé dans la corbeille l'assemblée %{resource_name}" unpublish: "%{user_name} a dépublié l'assemblée %{resource_name}" + unpublish_all_members: "%{user_name} a dépublié tous les membres de l'assemblée %{resource_name}" update: "%{user_name} a mis à jour l'assemblée %{resource_name}" assembly_member: create: "%{user_name} a créé le membre %{resource_name} membre dans l'assemblée %{space_name}" @@ -245,12 +248,13 @@ fr-CA: define_taxonomy_filters: Veuillez définir des filtres pour cet espace participatif avant d'utiliser ce paramètre. duration: Durée duration_help: Si la durée de cette assemblée est limitée, sélectionnez la date de fin. Sinon sa durée ne sera pas limitée. + has_members_help: Vous pourrez créer et publier des membres images: Images included_at_help: Sélectionnez la date à laquelle cet assemblée a été ajoutée à la plateforme. Elle ne doit pas nécessairement être identique à la date de création. metadata: Métadonnées no_taxonomy_filters_found: Aucun filtre de taxonomie trouvé. other: Autre - private_notice: Vous serez en mesure de gérer les participants privés après l'avoir défini comme privé + private_notice: Rend l'espace non visible pour les visiteurs et visible seulement pour les membres (sauf s'il est également transparent) select_a_created_by: Sélectionnez un créateur select_parent_assembly: Sélectionnez l'assemblée parente slug_help_html: 'Les identifiants d''URL sont utilisés pour générer les URL qui pointent vers cette assemblée. N''accepte que des lettres, des chiffres et des tirets et doit commencer par une lettre. Exemple : %{url}' diff --git a/decidim-assemblies/config/locales/fr.yml b/decidim-assemblies/config/locales/fr.yml index 57698c891462b..98eca80ea2bdc 100644 --- a/decidim-assemblies/config/locales/fr.yml +++ b/decidim-assemblies/config/locales/fr.yml @@ -25,6 +25,7 @@ fr: duration: Durée facebook: Facebook github: GitHub + has_members: Cet espace a des membres hero_image: Image de la page d'accueil import_attachments: Importer les pièces jointes import_categories: Importer les catégories @@ -169,8 +170,8 @@ fr: attachments: Documents liés components: Fonctionnalités info: À propos de cette assemblée + members: Membres moderations: Modérations - private_users: Membres see_assembly: Voir l'assemblée models: assembly: @@ -217,9 +218,11 @@ fr: export: "%{user_name} a exporté l'assemblée %{resource_name}" import: "%{user_name} a importé l'assemblée %{resource_name}" publish: "%{user_name} a publié l'assemblée %{resource_name}" + publish_all_members: "%{user_name} a publié tous les membres de l'assemblée %{resource_name}" restore: "%{user_name} a restauré l'assemblée %{resource_name}" soft_delete: "%{user_name} a déplacé dans la corbeille l'assemblée %{resource_name}" unpublish: "%{user_name} a dépublié l'assemblée %{resource_name}" + unpublish_all_members: "%{user_name} a dépublié tous les membres de l'assemblée %{resource_name}" update: "%{user_name} a mis à jour l'assemblée %{resource_name}" assembly_member: create: "%{user_name} a créé le membre %{resource_name} membre dans l'assemblée %{space_name}" @@ -245,12 +248,13 @@ fr: define_taxonomy_filters: Veuillez définir des filtres pour cet espace participatif avant d'utiliser ce paramètre. duration: Durée duration_help: Si la durée de cette assemblée est limitée, sélectionnez la date de fin. Sinon sa durée ne sera pas limitée. + has_members_help: Vous pourrez créer et publier des membres images: Images included_at_help: Sélectionnez la date à laquelle cet assemblée a été ajoutée à la plateforme. Elle ne doit pas nécessairement être identique à la date de création. metadata: Métadonnées no_taxonomy_filters_found: Aucun filtre de taxonomie trouvé. other: Autre - private_notice: Vous serez en mesure de gérer les participants privés après l'avoir défini comme privé + private_notice: Rend l'espace non visible pour les visiteurs et visible seulement pour les membres (sauf s'il est également transparent) select_a_created_by: Sélectionnez un créateur select_parent_assembly: Sélectionnez l'assemblée parente slug_help_html: 'Les identifiants d''URL sont utilisés pour générer les URL qui pointent vers cette assemblée. N''accepte que des lettres, des chiffres et des tirets et doit commencer par une lettre. Exemple : %{url}' diff --git a/decidim-assemblies/config/locales/he-IL.yml b/decidim-assemblies/config/locales/he-IL.yml index 566dfe31a7643..374c6374daeb8 100644 --- a/decidim-assemblies/config/locales/he-IL.yml +++ b/decidim-assemblies/config/locales/he-IL.yml @@ -151,7 +151,6 @@ he: components: רכיבים info: אודות האסיפה moderations: שינויים - private_users: חברים see_assembly: ראה אסיפה models: assembly: diff --git a/decidim-assemblies/config/locales/it.yml b/decidim-assemblies/config/locales/it.yml index 0ed12b0f96a16..e1e7d875516fb 100644 --- a/decidim-assemblies/config/locales/it.yml +++ b/decidim-assemblies/config/locales/it.yml @@ -159,7 +159,6 @@ it: info: Info su questo spazio landing_page: Layout della pagina di destinazione moderations: Moderazione - private_users: Membri see_assembly: Guarda l'assemblea models: assembly: @@ -239,7 +238,6 @@ it: metadata: Metadati no_taxonomy_filters_found: Nessun filtro trovato. other: Altro - private_notice: Sarai in grado di gestire i partecipanti privati dopo aver impostato lo spazio come privato select_a_created_by: Seleziona un creatore select_parent_assembly: Seleziona l'assemblea madre slug_help_html: 'Gli slug URL vengono utilizzati per generare gli URL che puntano a questa assemblea. Accetta solo lettere, numeri e trattini, e deve iniziare con una lettera. Esempio: %{url}' diff --git a/decidim-assemblies/config/locales/ja.yml b/decidim-assemblies/config/locales/ja.yml index e85ef4440c259..65ed99b3b2ce9 100644 --- a/decidim-assemblies/config/locales/ja.yml +++ b/decidim-assemblies/config/locales/ja.yml @@ -25,6 +25,7 @@ ja: duration: 持続期間 facebook: Facebook github: GitHub + has_members: このスペースにはメンバーがいます hero_image: ホーム画像 import_attachments: 添付ファイルをインポート import_categories: カテゴリをインポート @@ -167,8 +168,8 @@ ja: components: コンポーネント info: この参加スペースについて landing_page: ランディングページのレイアウト + members: メンバー moderations: モデレーション - private_users: メンバー see_assembly: 参加スペースを見る models: assembly: @@ -215,9 +216,11 @@ ja: export: "%{user_name} は %{resource_name} 参加スペースをエクスポートしました" import: "%{user_name} が %{resource_name} 参加スペースをインポートしました" publish: "%{user_name} が %{resource_name} 参加スペースを公開しました" + publish_all_members: "%{user_name} が参加スペース %{resource_name} のすべてのメンバーを公開しました" restore: "%{user_name} が %{resource_name} 参加スペースを復元しました" soft_delete: "%{user_name} が %{resource_name} 参加スペースをゴミ箱に移動しました" unpublish: "%{user_name} が %{resource_name} 参加スペースを非公開にしました" + unpublish_all_members: "%{user_name} が参加スペース %{resource_name} のすべてのメンバーを非公開にしました" update: "%{user_name} が %{resource_name} 参加スペースを更新しました" assembly_member: create: "%{user_name} が %{resource_name} 参加スペースで %{space_name} メンバーを作成しました" @@ -243,12 +246,13 @@ ja: define_taxonomy_filters: この設定を使用する前に、参加型スペースのフィルターをいくつか定義してください。 duration: 持続期間 duration_help: この参加スペースの持続時間が限られている場合は、終了日を選択します。それ以外の場合は、不定義として表示されます。 + has_members_help: メンバーを作成し、公開することができます images: 画像 included_at_help: この参加スペースがプラットフォームに追加された日付を選択します。必ずしも作成日と同じである必要はありません。 metadata: メタデータ no_taxonomy_filters_found: タクソノミーフィルタが見つかりません。 other: その他 - private_notice: プライベートとして設定した後、プライベート参加者を管理することができます + private_notice: 訪問者にはスペースを非表示にし、メンバーのみ表示します(透明化している場合を除きます) select_a_created_by: 作成者を選択してください select_parent_assembly: 親参加スペースを選択 slug_help_html: 'URLスラグは、この参加スペースを指すURLを生成するために使用されます。 英字、数字、ハイフンのみを受け付け、英字で始める必要があります。例: %{url}' @@ -307,7 +311,7 @@ ja: extra_data: name: 種別と期間 highlighted_assemblies: - name: ハイライトされた参加スペース + name: 注目の参加スペース related_assemblies: name: 関連する参加スペース created_by: @@ -381,6 +385,7 @@ ja: assembly_type: 参加スペースの種別 closing_date: 参加スペースの終了日 closing_date_reason: 参加スペースが終了した理由 + component_settings: 参加スペースのコンポーネント設定 composition: 参加スペースの構成 created_at: このスペースの作成日時 created_by: この参加スペースの作成者 @@ -434,7 +439,7 @@ ja: more_info: 詳細情報 take_part: 参加する index: - promoted_assemblies: ハイライトされた参加スペース + promoted_assemblies: 注目の参加スペース metadata: children_item: other: "%{count} 個の参加スペース" diff --git a/decidim-assemblies/config/locales/pt-BR.yml b/decidim-assemblies/config/locales/pt-BR.yml index 8f9425ec60f19..6edb2bb34a09c 100644 --- a/decidim-assemblies/config/locales/pt-BR.yml +++ b/decidim-assemblies/config/locales/pt-BR.yml @@ -19,15 +19,20 @@ pt-BR: developer_group: Grupo promotor document: Documento domain: Domínio + duplicate_categories: Categorias duplicadas + duplicate_components: Componentes duplicados + duplicate_features: Recursos duplicados duration: Duração facebook: Facebook github: GitHub + has_members: Este espaço tem membros hero_image: Imagem inicial import_attachments: Importar anexos import_categories: Importar categorias import_components: Importar componentes included_at: Incluído em instagram: Instagram + internal_organisation: Organização interna is_transparent: É transparente local_area: Área da organização meta_scope: Metadados do escopo @@ -83,12 +88,15 @@ pt-BR: decidim: admin: actions: + confirm_delete_assembly: Tem certeza de que deseja excluir esta assembleia? Se mudar de ideia, você pode restaurá-lo mais tarde. import_assembly: Importar new_assembly: Nova assembleia new_assembly_user_role: Novo administrador da assembleia + view_deleted_assemblies: Ver assembleias excluídas assemblies: create: error: Ocorreu um erro ao criar uma nova assembleia. + success: Assembléia criada com sucesso. Agora você pode adicionar componentes e configurá-la. edit: update: Atualizar index: @@ -96,12 +104,23 @@ pt-BR: public: Público published: Publicados unpublished: Despublicado + manage_trash: + title: Assembléias excluídas new: create: Criar title: Nova assembleia update: error: Ocorreu um erro ao atualizar esta assembleia. success: Assembleia atualizada com sucesso. + assemblies_duplicates: + create: + error: Ocorreu um erro ao duplicar esta assembleia. + success: Assembléia duplicada com sucesso. + assembly_duplicates: + new: + duplicate: Duplicado + select: Selecione quais dados você gostaria de duplicar + title: Duplicar assembléia assembly_imports: create: error: Ocorreu um erro ao importar esta assembleia. @@ -151,6 +170,8 @@ pt-BR: attachments: Anexos components: Componentes info: Sobre essa assambleia + landing_page: Layout página principal + members: Membros moderations: Moderação see_assembly: Ver assembleia models: @@ -172,6 +193,7 @@ pt-BR: vice_president: Vice presidente assembly_user_role: fields: + actions: Ações email: O email name: Nome role: Cargo @@ -179,10 +201,17 @@ pt-BR: roles: admin: Administrador collaborator: Colaborador + evaluator: Avaliador moderator: Moderador + taxonomy_filters: + space_filter_for: + assemblies: Todas as assembléias titles: assemblies: Assembleias + assemblies_deleted: Assembleias excluídas assemblies_types: Tipos de assembleia + tooltips: + deleted_assemblies_info: Uma assembleia só pode ser excluída se o status for "Não publicada". admin_log: assembly: create: "%{user_name} criou a assembleia %{resource_name}" @@ -190,7 +219,11 @@ pt-BR: export: "%{user_name} exportou a assembleia %{resource_name}" import: "%{user_name} importou a assembleia %{resource_name}" publish: "%{user_name} publicou a assembleia %{resource_name}" + publish_all_members: "%{user_name} publicou todos os membros da assembleia %{resource_name}" + restore: "%{user_name} restaurou a assembleia %{resource_name}" + soft_delete: "%{user_name} movido para a lixeira da assembleia %{resource_name}" unpublish: "%{user_name} não desplublicou a assembleia %{resource_name}" + unpublish_all_members: "%{user_name} não publicado todos os membros da assembleia %{resource_name}" update: "%{user_name} atualizou a assembleia %{resource_name}" assembly_member: create: "%{user_name} criou o membro %{resource_name} na assembleia %{space_name}" @@ -213,23 +246,33 @@ pt-BR: assemblies: form: announcement_help: O texto que você inserir aqui será mostrado ao usuário logo abaixo das informações de assembleia. + define_taxonomy_filters: Por favor, defina alguns filtros para este espaço participativo antes de usar esta configuração. duration: Duração duration_help: Se a duração dessa assembleia for limitada, selecione a data final. Caso contrário, aparecerá como indefinido. + has_members_help: Você conseguirá criar e publicar membros images: Imagens included_at_help: Selecione a data em que a assambleia foi adicionada à plataforma. Não precisa necessariamente ser o mesmo que a data de criação. metadata: Metadados + no_taxonomy_filters_found: Nenhum filtro de taxonomia encontrado. other: De outros + private_notice: Torna o espaço invisível para visitantes e exclusivo para membros (exceto se também for transparente) select_a_created_by: Selecione um criado por select_parent_assembly: Selecione a assembleia mãe slug_help_html: 'Os slugs de URL são usados ​​para gerar os URLs que apontam para essa assembleia. Apenas aceita letras, números e traços, e deve começar com uma carta. Exemplo: %{url}' social_handlers: Social + taxonomies: Taxonomias title: Informação geral + visibility: Visibilidade + assembly_duplicates: + form: + slug_help_html: 'Os slugs de URL são usados para gerar os URLs que apontam para essa assembleia. Aceita apenas letras, números e traços e deve começar com uma letra. Exemplo: %{url}' assembly_imports: form: slug_help_html: 'Os slugs de URL são usados ​​para gerar os URLs que apontam para essa assembleia. Apenas aceita letras, números e traços, e deve começar com uma carta. Exemplo: %{url}' assembly_members: form: explanation: 'Orientação para a imagem:' + image_guide: Preferencialmente, uma imagem de retrato que não tem nenhum texto. non_user_avatar_help: Você deve receber o consentimento das pessoas antes de publicá-las como membro. content_blocks: highlighted_assemblies: @@ -307,6 +350,9 @@ pt-BR: assembly_members: index: title: Membros + download_your_data: + show: + assemblies: Exportação de assembléias events: assemblies: create_assembly_member: @@ -334,11 +380,61 @@ pt-BR: not_found: 'O tipo de assembleia não foi encontrado no banco de dados (ID: %{id})' menu: assemblies: Assembleias + open_data: + help: + assemblies: + announcement: A informação de anúncio (chamada) + area: A área da assembleia + assembly_type: O tipo de assembleia + closing_date: A data de fechamento da assembleia + closing_date_reason: Porque a assembleia foi fechada + component_settings: As configurações do componente da assembleia + composition: A composição da assembleia + created_at: A data em que este espaço foi criado + created_by: Quem criou esta assembleia + created_by_other: Outro criador da assembleia + creation_date: A data de criação desta assembleia + decidim_scope_id: O escopo da assembleia + description: Uma longa descrição da assembleia + developer_group: O grupo de desenvolvedores da assembleia + duration: A duração da assembleia + facebook_handler: Manipulador de mídia social para Facebook + follows_count: O número de usuário seguindo este espaço + github_handler: Manipulador de mídia social para GitHub + id: O identificador único desta assembleia + included_at: A data de quando a assembleia foi incluído + instagram_handler: Manipulador de mídia social para Instagram + internal_organisation: A organização interna desta assembleia + is_transparent: Onde a assembleia é transparente ou não + local_area: A área local da assembleia + meta_scope: Os metadados do escopo da assembleia + participatory_scope: O escopo participativo da assembleia + participatory_structure: A estrutura participativa da assembleia + promoted: Se a assembleia é promovida ou não + published_at: A data que esse espaço foi publicado + purpose_of_action: O propósito da ação da assembleia + reference: A referência única do espaço + remote_banner_image_url: A URL da imagem do banner da assembleia + remote_hero_image_url: URL da imagem do herói assembleia + scope: O escopo da assembleia + scopes_enabled: Se os escopos estão habilitados ou não + short_description: Uma breve descrição da assembleia + slug: O slug da assembleia(usado para fins de identificação, para a URL) + special_features: Quais características especiais esta assembleia tem + subtitle: O subtítulo da assembleia + target: O alvo da assembleia + taxonomies: As taxonomias do projeto + title: Título da assembleia + twitter_handler: Manipulador de mídia social para Twitter + updated_at: A última data em que este espaço foi atualizado + url: A URL do espaço + youtube_handler: Manipulador de mídia social para o YouTube participatory_processes: show: related_assemblies: Assembleias Relacionadas statistics: assemblies_count: Assembleias + assemblies_count_tooltip: O número de assembleias públicas onde são tomadas decisões colectivas. layouts: decidim: assemblies: diff --git a/decidim-assemblies/config/locales/pt.yml b/decidim-assemblies/config/locales/pt.yml index 3749893eb2c09..8481b03253dfe 100644 --- a/decidim-assemblies/config/locales/pt.yml +++ b/decidim-assemblies/config/locales/pt.yml @@ -158,7 +158,6 @@ pt: components: Componentes info: Sobre esta reunião moderations: Moderação - private_users: Membros see_assembly: Veja a reunião models: assembly: @@ -236,7 +235,6 @@ pt: metadata: Metadados no_taxonomy_filters_found: Nenhum filtro de taxonomia encontrado. other: Outros - private_notice: Poderá gerir participantes privados depois de os definir como privados select_a_created_by: Selecione um criado por select_parent_assembly: Selecione a reunião pai slug_help_html: 'São usados slugs de URL para gerar URLs que apontam para esta reunião. Aceita apenas letras, números e traços, e deve começar com uma letra. Exemplo: %{url}' diff --git a/decidim-assemblies/config/locales/ro-RO.yml b/decidim-assemblies/config/locales/ro-RO.yml index 4940ce0d6dfc8..46f5fe2c5d500 100644 --- a/decidim-assemblies/config/locales/ro-RO.yml +++ b/decidim-assemblies/config/locales/ro-RO.yml @@ -22,6 +22,7 @@ ro: duration: Durată facebook: Facebook github: GitHub + has_members: Acest spațiu are membri hero_image: Imagine pentru pagina principală import_attachments: Importă atașamente import_categories: Importă categorii @@ -203,10 +204,12 @@ ro: announcement_help: Textul pe care îl introduceți aici va fi prezentat utilizatorului sub informațiile despre grupul de lucru. duration: Durată duration_help: În cazul în care durata acestui grup de lucru este limitată, selectaţi data de încheiere. În caz contrar, va apărea ca nelimitată. + has_members_help: Veți putea crea și publica membri images: Imagini included_at_help: Selectați data la care acest grup de lucru a fost adăugat în platformă. Nu trebuie să fie neapărat aceeași dată cu data creării. metadata: Metadate other: Altul + private_notice: Face spațiul invizibil pentru vizitatori și vizibil doar pentru membri (cu excepția cazului în care este de asemenea transparent) select_a_created_by: Selectați după persoana care a creat select_parent_assembly: Selectați grupul de lucru părinte social_handlers: Rețele de socializare @@ -321,6 +324,11 @@ ro: take_part: Participă index: promoted_assemblies: Grupuri de lucru evidențiate + metadata: + children_item: + one: "%{count} grup de lucru" + few: "%{count} grupuri de lucru" + other: "%{count} grup de lucru" order_by_assemblies: assemblies: one: "%{count} grup de lucru" diff --git a/decidim-assemblies/config/locales/sv.yml b/decidim-assemblies/config/locales/sv.yml index 00b214ff379dc..d61b6fef4f34b 100644 --- a/decidim-assemblies/config/locales/sv.yml +++ b/decidim-assemblies/config/locales/sv.yml @@ -170,7 +170,6 @@ sv: components: Komponenter info: Om samrådet moderations: Moderering - private_users: Medlemmar see_assembly: Visa samråd models: assembly: @@ -217,9 +216,11 @@ sv: export: "%{user_name} exporterade samrådet %{resource_name}" import: "%{user_name} importerade samrådet %{resource_name}" publish: "%{user_name} publicerade samrådet %{resource_name}" + publish_all_members: "%{user_name} publicerade samrådet %{resource_name}" restore: "%{user_name} återställde samrådet %{resource_name}" soft_delete: "%{user_name} tog bort samrådet %{resource_name}" unpublish: "%{user_name} avpublicerade samrådet %{resource_name}" + unpublish_all_members: "%{user_name} avpublicerade samrådet %{resource_name}" update: "%{user_name} uppdaterade samrådet %{resource_name}" assembly_member: create: "%{user_name} skapade medlemmen %{resource_name} i samrådet %{space_name}" @@ -250,7 +251,6 @@ sv: metadata: Metadata no_taxonomy_filters_found: Inga filter för kategorier hittades. other: Övrigt - private_notice: Du kommer att kunna hantera medlemmar om samrådet gjorts privat select_a_created_by: Välj en skapad av select_parent_assembly: Välj överordnat samråd slug_help_html: 'URL-slugs används till att generera URL:er till samrådet. Använd bara bokstäver, siffror och bindestreck, och de måste börja med en bokstav. Exempel: %{url}' diff --git a/decidim-assemblies/config/locales/tr-TR.yml b/decidim-assemblies/config/locales/tr-TR.yml index b7a91b2c30e00..a87b20af85fff 100644 --- a/decidim-assemblies/config/locales/tr-TR.yml +++ b/decidim-assemblies/config/locales/tr-TR.yml @@ -132,6 +132,8 @@ tr: attachment_files: Dosyalar attachments: Ekler components: Bileşenler + info: Bu meclis hakkında + landing_page: moderations: Yönetim models: assembly: @@ -162,6 +164,8 @@ tr: titles: assemblies: Kurullar assemblies_types: Kurul türleri + tooltips: + deleted_assemblies_info: Bir meclis yalnızca eğer statüsü "Yayınlanmamış" ise silinebilir. admin_log: assembly: create: "%{user_name} %{resource_name} kurulu oluşturdu" @@ -193,11 +197,17 @@ tr: duration_help: Bu kurulun süresi sınırlıysa, bitiş tarihini seçin. Aksi takdirde belirsiz görünecektir. images: Resimler metadata: Meta veri + no_taxonomy_filters_found: Kategori filtresi bulunamadı. other: Diğer select_a_created_by: tarafından oluşturulanı seçin select_parent_assembly: Bağlı olduğu kurulu seçin social_handlers: Sosyal title: Genel bilgi + visibility: Görünürlük + assembly_members: + form: + explanation: 'Görüntü kılavuzu:' + non_user_avatar_help: Üye olarak yayınlamadan önce kişilerin onayını almalısınız. content_blocks: highlighted_assemblies: max_results: Gösterilecek maksimum öğe miktarı @@ -244,6 +254,7 @@ tr: assemblies: create_assembly_member: email_intro: %{resource_name} kurulun bir yöneticisi sizi üyelerinden biri olarak ekledi. + email_outro: Bu bildirimi bir toplantıya davet edildiğiniz için aldınız. Katkıda bulunmak için kurul sayfasını kontrol edin! email_subject: '%{resource_name} kuruluna üye olmak için davet edildiniz!' notification_title: %{resource_name} Kurulu üyesi olarak kaydoldunuz. Katkıda bulunmak için kurul sayfasını kontrol edin! assembly: @@ -266,6 +277,10 @@ tr: not_found: 'Kurul türü veritabanında bulunamadı (ID: %{id})' menu: assemblies: Kurullar + open_data: + help: + assemblies: + decidim_scope_id: Meclisin kapsamı participatory_processes: show: related_assemblies: İlgili kurullar diff --git a/decidim-assemblies/db/migrate/20251205120000_add_has_members_to_decidim_assemblies.rb b/decidim-assemblies/db/migrate/20251205120000_add_has_members_to_decidim_assemblies.rb new file mode 100644 index 0000000000000..02a313a9a5d78 --- /dev/null +++ b/decidim-assemblies/db/migrate/20251205120000_add_has_members_to_decidim_assemblies.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddHasMembersToDecidimAssemblies < ActiveRecord::Migration[7.0] + def change + add_column :decidim_assemblies, :has_members, :boolean, default: false + end +end diff --git a/decidim-assemblies/lib/decidim/assemblies/admin_engine.rb b/decidim-assemblies/lib/decidim/assemblies/admin_engine.rb index 966a16ce0a028..37537257603c2 100644 --- a/decidim-assemblies/lib/decidim/assemblies/admin_engine.rb +++ b/decidim-assemblies/lib/decidim/assemblies/admin_engine.rb @@ -81,12 +81,12 @@ class AdminEngine < ::Rails::Engine resources :reports, controller: "moderations/reports", only: [:index, :show] end - resources :participatory_space_private_users, controller: "participatory_space_private_users" do + resources :members, controller: "members" do member do - post :resend_invitation, to: "participatory_space_private_users#resend_invitation" + post :resend_invitation, to: "members#resend_invitation" end collection do - resource :participatory_space_private_users_csv_imports, only: [:new, :create], path: "csv_import" do + resource :members_csv_imports, only: [:new, :create], path: "csv_import" do delete :destroy_all end post :publish_all diff --git a/decidim-assemblies/lib/decidim/assemblies/engine.rb b/decidim-assemblies/lib/decidim/assemblies/engine.rb index ff6da75075f89..ebd08fba721d4 100644 --- a/decidim-assemblies/lib/decidim/assemblies/engine.rb +++ b/decidim-assemblies/lib/decidim/assemblies/engine.rb @@ -27,7 +27,7 @@ class Engine < ::Rails::Engine }, constraints: { assembly_id: /[0-9]+/ } resources :assemblies, only: [:index, :show], param: :slug, path: "assemblies" do - resources :participatory_space_private_users, only: :index, path: "members" + resources :members, only: :index, path: "members" end scope "/assemblies/:assembly_slug/f/:component_id" do @@ -96,8 +96,8 @@ class Engine < ::Rails::Engine Decidim.view_hooks.register(:user_profile_bottom, priority: Decidim::ViewHooks::MEDIUM_PRIORITY) do |view_context| assemblies = OrganizationPublishedAssemblies.new(view_context.current_organization, view_context.current_user) .query.distinct - .joins(:participatory_space_private_users) - .merge(Decidim::ParticipatorySpacePrivateUser.where(user: view_context.profile_holder)) + .joins(:members) + .merge(Decidim::ParticipatorySpace::Member.where(user: view_context.profile_holder)) .reorder(title: :asc) next unless assemblies.any? diff --git a/decidim-assemblies/lib/decidim/assemblies/menu.rb b/decidim-assemblies/lib/decidim/assemblies/menu.rb index d632409a09ae2..2ebbf1d1b54c1 100644 --- a/decidim-assemblies/lib/decidim/assemblies/menu.rb +++ b/decidim-assemblies/lib/decidim/assemblies/menu.rb @@ -122,11 +122,11 @@ def self.register_admin_assembly_menu! icon_name: "user-settings-line", if: allowed_to?(:read, :assembly_user_role, assembly: current_participatory_space) - menu.add_item :participatory_space_private_users, - I18n.t("private_users", scope: "decidim.admin.menu.assemblies_submenu"), - decidim_admin_assemblies.participatory_space_private_users_path(current_participatory_space), - icon_name: "spy-line", - if: allowed_to?(:read, :space_private_user, current_participatory_space:) + menu.add_item :members, + I18n.t("members", scope: "decidim.admin.menu.assemblies_submenu"), + decidim_admin_assemblies.members_path(current_participatory_space), + icon_name: "user-settings-line", + if: allowed_to?(:read, :space_member, current_participatory_space:) menu.add_item :moderations, I18n.t("moderations", scope: "decidim.admin.menu.assemblies_submenu"), diff --git a/decidim-assemblies/lib/decidim/assemblies/participatory_space.rb b/decidim-assemblies/lib/decidim/assemblies/participatory_space.rb index 140a4ea33bec8..b3b4248de2bb6 100644 --- a/decidim-assemblies/lib/decidim/assemblies/participatory_space.rb +++ b/decidim-assemblies/lib/decidim/assemblies/participatory_space.rb @@ -13,8 +13,6 @@ participatory_space.query_type = "Decidim::Assemblies::AssemblyType" - participatory_space.breadcrumb_cell = "decidim/assemblies/assembly_dropdown_metadata" - participatory_space.register_resource(:assembly) do |resource| resource.model_class_name = "Decidim::Assembly" resource.card = "decidim/assemblies/assembly" diff --git a/decidim-assemblies/lib/decidim/assemblies/test/factories.rb b/decidim-assemblies/lib/decidim/assemblies/test/factories.rb index ba2f11e6d8dab..cefe5ad3d9a55 100644 --- a/decidim-assemblies/lib/decidim/assemblies/test/factories.rb +++ b/decidim-assemblies/lib/decidim/assemblies/test/factories.rb @@ -37,6 +37,7 @@ participatory_scope { generate_localized_title(:assembly_participatory_scope, skip_injection:) } participatory_structure { generate_localized_title(:assembly_participatory_structure, skip_injection:) } private_space { false } + has_members { false } purpose_of_action { generate_localized_description(:assembly_purpose_of_action, skip_injection:) } composition { generate_localized_description(:assembly_composition, skip_injection:) } creation_date { 1.month.ago } diff --git a/decidim-assemblies/spec/cells/decidim/assemblies/assembly_dropdown_metadata_cell_spec.rb b/decidim-assemblies/spec/cells/decidim/assemblies/assembly_dropdown_metadata_cell_spec.rb deleted file mode 100644 index 4953a9f7ef426..0000000000000 --- a/decidim-assemblies/spec/cells/decidim/assemblies/assembly_dropdown_metadata_cell_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -require "decidim/core/test/shared_examples/participatory_space_dropdown_metadata_cell_examples" - -module Decidim::Assemblies - describe AssemblyDropdownMetadataCell, type: :cell do - controller Decidim::ApplicationController - - subject { cell("decidim/assemblies/assembly_dropdown_metadata", model).call } - - let(:model) { create(:assembly) } - - include_examples "participatory space dropdown metadata cell" - end -end diff --git a/decidim-assemblies/spec/commands/create_assembly_spec.rb b/decidim-assemblies/spec/commands/create_assembly_spec.rb index c9a899f4d67bd..2c1b8d4ec858d 100644 --- a/decidim-assemblies/spec/commands/create_assembly_spec.rb +++ b/decidim-assemblies/spec/commands/create_assembly_spec.rb @@ -46,6 +46,7 @@ module Decidim::Assemblies organization:, taxonomizations:, parent: nil, + has_members: false, private_space: false, errors:, participatory_processes_ids: related_process_ids, diff --git a/decidim-assemblies/spec/controllers/participatory_space_private_users_controller_spec.rb b/decidim-assemblies/spec/controllers/members_controller_spec.rb similarity index 80% rename from decidim-assemblies/spec/controllers/participatory_space_private_users_controller_spec.rb rename to decidim-assemblies/spec/controllers/members_controller_spec.rb index fbb67ca018d62..ca92debb9a51b 100644 --- a/decidim-assemblies/spec/controllers/participatory_space_private_users_controller_spec.rb +++ b/decidim-assemblies/spec/controllers/members_controller_spec.rb @@ -5,7 +5,7 @@ module Decidim module Assemblies - describe ParticipatorySpacePrivateUsersController do + describe MembersController do routes { Decidim::Assemblies::Engine.routes } def decidim_assemblies @@ -14,7 +14,7 @@ def decidim_assemblies let(:organization) { create(:organization) } - let!(:privatable_to) do + let!(:participatory_space) do create( :assembly, :published, @@ -23,10 +23,10 @@ def decidim_assemblies ) end - let(:destination_path) { decidim_assemblies.assembly_path(privatable_to, locale: I18n.locale) } + let(:destination_path) { decidim_assemblies.assembly_path(participatory_space, locale: I18n.locale) } let(:slug_param) { "assembly_slug" } - let(:slug) { privatable_to.slug } + let(:slug) { participatory_space.slug } it_behaves_like "participatory space members page examples" end diff --git a/decidim-assemblies/spec/forms/assembly_form_spec.rb b/decidim-assemblies/spec/forms/assembly_form_spec.rb index 0097da852d875..0924b5b1705b0 100644 --- a/decidim-assemblies/spec/forms/assembly_form_spec.rb +++ b/decidim-assemblies/spec/forms/assembly_form_spec.rb @@ -42,6 +42,7 @@ module Admin let(:slug) { "slug" } let(:attachment) { upload_test_file(Decidim::Dev.test_file("city.jpeg", "image/jpeg")) } let(:private_space) { true } + let(:has_members) { true } let(:purpose_of_action) do { en: "Purpose of action", @@ -129,6 +130,7 @@ module Admin "banner_image" => attachment, "slug" => slug, "private_space" => private_space, + "has_members" => has_members, "purpose_of_action_en" => purpose_of_action[:en], "purpose_of_action_es" => purpose_of_action[:es], "purpose_of_action_ca" => purpose_of_action[:ca], @@ -165,6 +167,18 @@ module Admin } end + context "when has_members is true" do + let(:has_members) { true } + + it { is_expected.to be_valid } + end + + context "when has_members is false" do + let(:has_members) { false } + + it { is_expected.to be_valid } + end + context "when everything is OK" do it { is_expected.to be_valid } end diff --git a/decidim-assemblies/spec/forms/decidim/assemblies/admin/assembly_import_form_spec.rb b/decidim-assemblies/spec/forms/decidim/assemblies/admin/assembly_import_form_spec.rb new file mode 100644 index 0000000000000..8c5d47ed2470d --- /dev/null +++ b/decidim-assemblies/spec/forms/decidim/assemblies/admin/assembly_import_form_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Assemblies + module Admin + describe AssemblyImportForm do + subject { form } + + let(:organization) { create(:organization) } + let(:document) { upload_test_file(Decidim::Dev.asset("assemblies.json"), content_type: "application/json", return_blob: true) } + let(:slug) { "imported-assembly-slug" } + let(:title) do + { + en: "Imported Assembly", + es: "Asamblea Importada", + ca: "Assemblea Importada" + } + end + + let(:params) do + { + slug:, + title_en: title[:en], + title_es: title[:es], + title_ca: title[:ca], + document:, + import_steps: true, + import_attachments: true, + import_components: true + } + end + + let(:form) do + described_class.from_params(params).with_context( + current_organization: organization + ) + end + + context "when everything is OK" do + it { is_expected.to be_valid } + end + + context "when document is missing" do + let(:document) { nil } + + it { is_expected.to be_invalid } + + it "adds an error on document" do + form.valid? + expect(form.errors[:document]).not_to be_empty + end + end + + context "when document content type is not valid" do + let(:document) { upload_test_file(Decidim::Dev.test_file("city.jpeg", "image/jpeg"), return_blob: true) } + + it { is_expected.to be_invalid } + + it "adds an error on document" do + form.valid? + expect(form.errors[:document]).not_to be_empty + end + end + + context "when slug is missing" do + let(:slug) { nil } + + it { is_expected.to be_invalid } + end + + context "when slug is not valid" do + let(:slug) { "123-invalid-slug" } + + it { is_expected.to be_invalid } + end + + context "when slug is not unique" do + before do + create(:assembly, slug:, organization:) + end + + it { is_expected.to be_invalid } + + it "adds an error on slug" do + form.valid? + expect(form.errors[:slug]).not_to be_empty + end + end + + context "when default language in title is missing" do + let(:title) do + { + ca: "Assemblea Importada" + } + end + + it { is_expected.to be_invalid } + end + end + end + end +end diff --git a/decidim-assemblies/spec/lib/query_extensions_spec.rb b/decidim-assemblies/spec/lib/query_extensions_spec.rb index 9262797848e0b..c412b88da37d6 100644 --- a/decidim-assemblies/spec/lib/query_extensions_spec.rb +++ b/decidim-assemblies/spec/lib/query_extensions_spec.rb @@ -38,8 +38,8 @@ module Core let!(:assembly) { create(:assembly) } let(:id) { assembly.id } - it "returns nil" do - expect(response["assembly"]).to be_nil + it_behaves_like "graphQL not found space" do + let(:space_type) { "assembly" } end end end diff --git a/decidim-assemblies/spec/permissions/decidim/assemblies/permissions_spec.rb b/decidim-assemblies/spec/permissions/decidim/assemblies/permissions_spec.rb index a7f93add8eec4..ca734f95813db 100644 --- a/decidim-assemblies/spec/permissions/decidim/assemblies/permissions_spec.rb +++ b/decidim-assemblies/spec/permissions/decidim/assemblies/permissions_spec.rb @@ -429,11 +429,11 @@ it_behaves_like "allows any action on subject", :assembly it_behaves_like "allows any action on subject", :assembly_user_role - context "when private assembly" do - let(:assembly) { create(:assembly, organization:, private_space: true) } + context "when assembly has members" do + let(:assembly) { create(:assembly, organization:, has_members: true) } let!(:context) { { current_participatory_space: assembly } } - it_behaves_like "allows any action on subject", :space_private_user + it_behaves_like "allows any action on subject", :space_member end end @@ -463,11 +463,11 @@ it_behaves_like "allows any action on subject", :assembly it_behaves_like "allows any action on subject", :assembly_user_role - context "when private assembly" do - let(:assembly) { create(:assembly, organization:, private_space: true) } + context "when assembly has members" do + let(:assembly) { create(:assembly, organization:, has_members: true) } let!(:context) { { current_participatory_space: assembly } } - it_behaves_like "allows any action on subject", :space_private_user + it_behaves_like "allows any action on subject", :space_member end end end diff --git a/decidim-assemblies/spec/shared/manage_assembly_private_users_examples.rb b/decidim-assemblies/spec/shared/manage_assembly_private_users_examples.rb deleted file mode 100644 index 7810cc6a06cda..0000000000000 --- a/decidim-assemblies/spec/shared/manage_assembly_private_users_examples.rb +++ /dev/null @@ -1,101 +0,0 @@ -# frozen_string_literal: true - -shared_examples "manage assembly private users examples" do - let(:other_user) { create(:user, organization:, email: "my_email@example.org") } - - let!(:assembly_private_user) { create(:assembly_private_user, user:, privatable_to: assembly) } - - before do - switch_to_host(organization.host) - login_as user, scope: :user - visit decidim_admin_assemblies.edit_assembly_path(assembly) - within_admin_sidebar_menu do - click_on "Members" - end - end - - it "shows assembly private user list" do - within "#private_users table" do - expect(page).to have_content(assembly_private_user.user.email) - end - end - - it "creates a new assembly private users" do - click_on "New participatory space private user" - - within ".new_participatory_space_private_user" do - fill_in :participatory_space_private_user_name, with: "John Doe" - fill_in :participatory_space_private_user_email, with: other_user.email - - find("*[type=submit]").click - end - - expect(page).to have_admin_callout("successfully") - - within "#private_users table" do - expect(page).to have_content(other_user.email) - end - - visit decidim_admin.root_path - expect(page).to have_content("invited #{other_user.name} to be a private participant") - end - - describe "when import a batch of private users from csv" do - it "import a batch of participatory space private users" do - click_on "Import via CSV" - - # The CSV has no headers - expect(Decidim::Admin::ImportParticipatorySpacePrivateUserCsvJob).to receive(:perform_later).once.ordered.with("john.doe@example.org", "John Doe", assembly, user) - expect(Decidim::Admin::ImportParticipatorySpacePrivateUserCsvJob).to receive(:perform_later).once.ordered.with("jane.doe@example.org", "Jane Doe", assembly, user) - dynamically_attach_file(:participatory_space_private_user_csv_import_file, Decidim::Dev.asset("import_participatory_space_private_users.csv")) - perform_enqueued_jobs { click_on "Upload" } - - expect(page).to have_content("CSV file uploaded successfully") - end - end - - describe "when managing different users" do - before do - create(:assembly_private_user, user: other_user, privatable_to: assembly) - visit current_path - end - - it "deletes an assembly_private_user" do - within "#private_users tr", text: other_user.email do - find("button[data-controller='dropdown']").click - accept_confirm { click_on "Delete" } - end - - expect(page).to have_admin_callout("successfully") - - within "#private_users table" do - expect(page).to have_no_content(other_user.email) - end - end - - context "when the user has not accepted the invitation" do - before do - form = Decidim::Admin::ParticipatorySpacePrivateUserForm.from_params( - name: "test", - email: "test@example.org" - ) - - Decidim::Admin::CreateParticipatorySpacePrivateUser.call( - form, - assembly - ) - - visit current_path - end - - it "resends the invitation to the user" do - within "#private_users tr", text: "test@example.org" do - find("button[data-controller='dropdown']").click - click_on "Resend invitation" - end - - expect(page).to have_admin_callout("successfully") - end - end - end -end diff --git a/decidim-assemblies/spec/system/admin/admin_filters_assemblies_members_spec.rb b/decidim-assemblies/spec/system/admin/admin_filters_assemblies_members_spec.rb new file mode 100644 index 0000000000000..42c8ee733c8bf --- /dev/null +++ b/decidim-assemblies/spec/system/admin/admin_filters_assemblies_members_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Admin filters assemblies members" do + include_context "with filterable context" + + let(:organization) { create(:organization) } + let!(:user) { create(:user, :admin, :confirmed, organization:) } + let(:assembly) { create(:assembly, organization:, has_members: true) } + + let!(:invited_user1) { create(:user, name:, organization:, invitation_sent_at: 1.day.ago, invitation_accepted_at: Time.current) } + let!(:invited_member1) { create(:assembly_member, user: invited_user1, participatory_space: assembly) } + let!(:invited_user2) { create(:user, email:, organization:) } + let!(:invited_member2) { create(:assembly_member, user: invited_user2, participatory_space: assembly) } + + let(:name) { "Dummy Name" } + let(:email) { "dummy_email@example.org" } + + let(:resource_controller) { Decidim::Assemblies::Admin::MembersController } + + before do + switch_to_host(organization.host) + login_as user, scope: :user + visit decidim_admin_assemblies.members_path(assembly_slug: assembly.slug) + end + + context "when managing assembly with members" do + before do + switch_to_host(organization.host) + login_as user, scope: :user + visit decidim_admin_assemblies.edit_assembly_path(assembly) + within_admin_sidebar_menu do + click_on "Members" + end + end + + include_examples "filterable participatory space users" + include_examples "searchable participatory space users" + end + + context "when trying to manage members and the space does not have members" do + let(:assembly) { create(:assembly, organization:, has_members: false) } + + it "restricts access" do + expect(page).to have_admin_callout("You are not authorized to perform this action.") + end + end + + describe "when publishing all members" do + let!(:member) { create(:member, :unpublished, user:, participatory_space: assembly) } + + it "publishes all members" do + click_on "Publish all" + + sleep(1) + expect(member.reload).to be_published + end + + it "displays the correct log message" do + click_on "Publish all" + sleep(1) + visit decidim_admin.root_path + expect(page).to have_content("published all members of the #{translated(assembly.title)} assembly") + end + end +end diff --git a/decidim-assemblies/spec/system/admin/admin_filters_assemblies_private_space_users_spec.rb b/decidim-assemblies/spec/system/admin/admin_filters_assemblies_private_space_users_spec.rb deleted file mode 100644 index cdf0951924173..0000000000000 --- a/decidim-assemblies/spec/system/admin/admin_filters_assemblies_private_space_users_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe "Admin filters assemblies private space users" do - include_context "with filterable context" - - let(:organization) { create(:organization) } - let!(:user) { create(:user, :admin, :confirmed, organization:) } - let(:assembly) { create(:assembly, organization:, private_space: true) } - - let!(:invited_user1) { create(:user, name:, organization:) } - let!(:invited_private_user1) { create(:assembly_private_user, user: invited_user1, privatable_to: assembly) } - let!(:invited_user2) { create(:user, email:, organization:) } - let!(:invited_private_user2) { create(:assembly_private_user, user: invited_user2, privatable_to: assembly) } - - let(:name) { "Dummy Name" } - let(:email) { "dummy_email@example.org" } - - let(:resource_controller) { Decidim::Assemblies::Admin::ParticipatorySpacePrivateUsersController } - - context "when managing private process" do - before do - invited_user1.update!(invitation_sent_at: 1.day.ago, invitation_accepted_at: Time.current) - - switch_to_host(organization.host) - login_as user, scope: :user - visit decidim_admin_assemblies.edit_assembly_path(assembly) - within_admin_sidebar_menu do - click_on "Members" - end - end - - include_examples "filterable participatory space users" - include_examples "searchable participatory space users" - end - - context "when managing private users in a public process" do - let(:assembly) { create(:assembly, organization:, private_space: false) } - - before do - invited_user1.update!(invitation_sent_at: 1.day.ago, invitation_accepted_at: Time.current) - - switch_to_host(organization.host) - login_as user, scope: :user - visit decidim_admin_assemblies.participatory_space_private_users_path(assembly_slug: assembly.slug) - end - - it "restricts access" do - expect(page).to have_admin_callout("You are not authorized to perform this action.") - end - end -end diff --git a/decidim-assemblies/spec/system/admin/admin_filters_assemblies_spec.rb b/decidim-assemblies/spec/system/admin/admin_filters_assemblies_spec.rb new file mode 100644 index 0000000000000..a8c5ec0574306 --- /dev/null +++ b/decidim-assemblies/spec/system/admin/admin_filters_assemblies_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Admin sorting assemblies" do + let(:organization) { create(:organization) } + let(:user) { create(:user, :admin, :confirmed, organization:) } + + let!(:old_assembly) { create(:assembly, title: { en: "Old assembly" }, created_at: 3.weeks.ago, organization:) } + let!(:recent_assembly) { create(:assembly, title: { en: "Recent assembly" }, created_at: 1.day.ago, organization:) } + let!(:newest_assembly) { create(:assembly, title: { en: "Newest assembly" }, created_at: Time.current, organization:) } + + before do + switch_to_host(organization.host) + login_as user, scope: :user + visit decidim_admin_assemblies.assemblies_path + end + + context "when sorting assemblies by their creation" do + it "sorts by created_at descending by default" do + within "table thead" do + click_link "Created at" + end + + titles = page.all("table tbody tr td:first-child") + expect(titles[0].text).to include("Newest assembly") + expect(titles[1].text).to include("Recent assembly") + expect(titles[2].text).to include("Old assembly") + end + + it "sorts by created_at ascending when clicked again" do + within "table thead" do + click_link "Created at" + click_link "Created at" + end + + titles = page.all("table tbody tr td:first-child") + expect(titles[0].text).to include("Old assembly") + expect(titles[1].text).to include("Recent assembly") + expect(titles[2].text).to include("Newest assembly") + end + end +end diff --git a/decidim-assemblies/spec/system/admin/admin_imports_assembly_spec.rb b/decidim-assemblies/spec/system/admin/admin_imports_assembly_spec.rb index 1103ea69c6537..1d4ca58eb7f2b 100644 --- a/decidim-assemblies/spec/system/admin/admin_imports_assembly_spec.rb +++ b/decidim-assemblies/spec/system/admin/admin_imports_assembly_spec.rb @@ -11,6 +11,18 @@ visit decidim_admin_assemblies.assemblies_path end + context "when viewing the import page" do + before do + within_admin_menu do + click_on "Import" + end + end + + it "displays the import help text" do + expect(page).to have_content("This import feature allows you to create a new assembly from an exported JSON file") + end + end + context "when importing the assembly with basic fields" do before do stub_get_request_with_format( diff --git a/decidim-assemblies/spec/system/admin/admin_manages_assemblies_spec.rb b/decidim-assemblies/spec/system/admin/admin_manages_assemblies_spec.rb index 8335a9dd69ab0..36fdddf06558f 100644 --- a/decidim-assemblies/spec/system/admin/admin_manages_assemblies_spec.rb +++ b/decidim-assemblies/spec/system/admin/admin_manages_assemblies_spec.rb @@ -11,8 +11,8 @@ let(:resource_controller) { Decidim::Assemblies::Admin::AssembliesController } let(:model_name) { assembly.class.model_name } - context "when conditionally displaying private user menu entry" do - let!(:my_space) { create(:assembly, organization:, private_space:) } + context "when conditionally displaying member menu entry" do + let!(:my_space) { create(:assembly, organization:, has_members:) } before do switch_to_host(organization.host) @@ -21,20 +21,20 @@ click_on translated(my_space.title) end - context "when the participatory space is private" do - let(:private_space) { true } + context "when the participatory space has members" do + let(:has_members) { true } - it "hides the private user menu entry" do + it "shows the member menu entry" do within_admin_sidebar_menu do expect(page).to have_content("Members") end end end - context "when the participatory space is public" do - let(:private_space) { false } + context "when the participatory space has not members" do + let(:has_members) { false } - it "shows the private user menu entry" do + it "hides the member menu entry" do within_admin_sidebar_menu do expect(page).to have_no_content("Members") end diff --git a/decidim-assemblies/spec/system/admin/admin_manages_assembly_attachments_spec.rb b/decidim-assemblies/spec/system/admin/admin_manages_assembly_attachments_spec.rb index 50070178677bc..3eda2c02cf7f1 100644 --- a/decidim-assemblies/spec/system/admin/admin_manages_assembly_attachments_spec.rb +++ b/decidim-assemblies/spec/system/admin/admin_manages_assembly_attachments_spec.rb @@ -17,5 +17,44 @@ end end - it_behaves_like "manage attachments examples" + it_behaves_like "manage attachments examples" do + context "when checking notifications" do + it "successfully displays the notification" do + create(:follow, user:, followable: attached_to) + + click_on "New attachment" + + within ".new_attachment" do + fill_in_i18n( + :attachment_title, + "#attachment-title-tabs", + en: "Very Important Document", + es: "Documento Muy Importante", + ca: "Document Molt Important" + ) + + fill_in_i18n( + :attachment_description, + "#attachment-description-tabs", + en: "This document contains important information", + es: "Este documento contiene información importante", + ca: "Aquest document conté informació important" + ) + end + + dynamically_attach_file(:attachment_file, Decidim::Dev.asset("Exampledocument.pdf")) + + within ".new_attachment" do + find("*[type=submit]").click + end + + expect(page).to have_admin_callout("successfully") + + wait_enqueued_jobs do + visit decidim.notifications_path + expect(page).to have_content("A new document has been added to") + end + end + end + end end diff --git a/decidim-assemblies/spec/system/admin/admin_manages_assembly_members_spec.rb b/decidim-assemblies/spec/system/admin/admin_manages_assembly_members_spec.rb new file mode 100644 index 0000000000000..c4d95865f735c --- /dev/null +++ b/decidim-assemblies/spec/system/admin/admin_manages_assembly_members_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require "spec_helper" +require "decidim/admin/test/admin_members_shared_examples" + +describe "Admin manages assembly members" do + let!(:user) { create(:user, :admin, :confirmed, organization:) } + let(:organization) { create(:organization) } + let!(:participatory_space) { create(:assembly, organization:, has_members: true) } + let(:participatory_space_edit_path) { decidim_admin_assemblies.edit_assembly_path(participatory_space) } + + it_behaves_like "manage admin members examples" +end diff --git a/decidim-assemblies/spec/system/admin/admin_manages_assembly_private_users_spec.rb b/decidim-assemblies/spec/system/admin/admin_manages_assembly_private_users_spec.rb deleted file mode 100644 index 54a5d44ce98d4..0000000000000 --- a/decidim-assemblies/spec/system/admin/admin_manages_assembly_private_users_spec.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe "Admin manages assembly private users" do - let!(:user) { create(:user, :admin, :confirmed, organization:) } - let(:organization) { create(:organization) } - let!(:assembly) { create(:assembly, organization:, private_space: true) } - - it_behaves_like "manage assembly private users examples" -end diff --git a/decidim-assemblies/spec/system/admin/admin_manages_assembly_soft_delete_spec.rb b/decidim-assemblies/spec/system/admin/admin_manages_assembly_soft_delete_spec.rb index b5a22206c81e1..4bda80d5c7440 100644 --- a/decidim-assemblies/spec/system/admin/admin_manages_assembly_soft_delete_spec.rb +++ b/decidim-assemblies/spec/system/admin/admin_manages_assembly_soft_delete_spec.rb @@ -14,12 +14,12 @@ it_behaves_like "manage trashed resource", "assembly" context "when a user is collaborator" do - let!(:assembly) { create(:assembly, organization: organization) } - let!(:collaborator_user) { create(:user, :admin_terms_accepted, :confirmed, organization: organization) } + let!(:assembly) { create(:assembly, organization:) } + let!(:collaborator_user) { create(:user, :admin_terms_accepted, :confirmed, organization:) } let!(:collaborator_role) do create(:assembly_user_role, user: collaborator_user, - assembly: assembly, + assembly:, role: :collaborator) end @@ -36,12 +36,12 @@ end context "when a user is evaluator" do - let!(:assembly) { create(:assembly, organization: organization) } - let!(:evaluator_user) { create(:user, :admin_terms_accepted, :confirmed, organization: organization) } + let!(:assembly) { create(:assembly, organization:) } + let!(:evaluator_user) { create(:user, :admin_terms_accepted, :confirmed, organization:) } let!(:evaluator_role) do create(:assembly_user_role, user: evaluator_user, - assembly: assembly, + assembly:, role: :evaluator) end @@ -58,12 +58,12 @@ end context "when a user is moderator" do - let!(:assembly) { create(:assembly, organization: organization) } - let!(:moderator_user) { create(:user, :admin_terms_accepted, :confirmed, organization: organization) } + let!(:assembly) { create(:assembly, organization:) } + let!(:moderator_user) { create(:user, :admin_terms_accepted, :confirmed, organization:) } let!(:moderator_role) do create(:assembly_user_role, user: moderator_user, - assembly: assembly, + assembly:, role: :moderator) end @@ -80,12 +80,12 @@ end context "when a user is a space admin" do - let!(:assembly) { create(:assembly, organization: organization) } - let!(:admin_user) { create(:user, :admin_terms_accepted, :confirmed, organization: organization) } + let!(:assembly) { create(:assembly, organization:) } + let!(:admin_user) { create(:user, :admin_terms_accepted, :confirmed, organization:) } let!(:admin_role) do create(:assembly_user_role, user: admin_user, - assembly: assembly, + assembly:, role: :admin) end diff --git a/decidim-assemblies/spec/system/admin/assembly_admin_accesses_admin_sections_spec.rb b/decidim-assemblies/spec/system/admin/assembly_admin_accesses_admin_sections_spec.rb index 3f9cc23a29126..5231f9c8e3ab6 100644 --- a/decidim-assemblies/spec/system/admin/assembly_admin_accesses_admin_sections_spec.rb +++ b/decidim-assemblies/spec/system/admin/assembly_admin_accesses_admin_sections_spec.rb @@ -10,7 +10,7 @@ login_as user, scope: :user end - shared_examples "sees public space menu" do + shared_examples "sees menu without members" do it "can access all sections" do expect(page).to have_content("Info") expect(page).to have_content("Components") @@ -21,7 +21,7 @@ end end - shared_examples "sees private space menu" do + shared_examples "sees menu with members" do it "can access all sections" do expect(page).to have_content("Info") expect(page).to have_content("Components") @@ -41,14 +41,14 @@ end end - context "when is a public assembly" do - it_behaves_like "sees public space menu" + context "when is an assembly without members" do + it_behaves_like "sees menu without members" end - context "when is a private assembly" do - let(:assembly) { create(:assembly, organization:, private_space: true) } + context "when is an assembly with members" do + let(:assembly) { create(:assembly, organization:, has_members: true) } - it_behaves_like "sees private space menu" + it_behaves_like "sees menu with members" end end @@ -59,14 +59,14 @@ visit decidim_admin_assemblies.edit_assembly_path(child_assembly) end - context "when is a public assembly" do - it_behaves_like "sees public space menu" + context "when is an assembly without" do + it_behaves_like "sees menu without members" end - context "when is a private assembly" do - let(:child_assembly) { create(:assembly, parent: assembly, organization:, private_space: true) } + context "when is an assembly with members" do + let(:child_assembly) { create(:assembly, parent: assembly, organization:, has_members: true) } - it_behaves_like "sees private space menu" + it_behaves_like "sees menu with members" end it_behaves_like "assembly admin manage assembly components" diff --git a/decidim-assemblies/spec/system/admin/assembly_moderator_manages_assembly_moderations_spec.rb b/decidim-assemblies/spec/system/admin/assembly_moderator_manages_assembly_moderations_spec.rb index a6e4c89791a8f..3dbf99e422e35 100644 --- a/decidim-assemblies/spec/system/admin/assembly_moderator_manages_assembly_moderations_spec.rb +++ b/decidim-assemblies/spec/system/admin/assembly_moderator_manages_assembly_moderations_spec.rb @@ -6,7 +6,8 @@ include_context "when assembly moderator administrating an assembly" let(:current_component) { create(:component, participatory_space: assembly) } - let!(:reportables) { create_list(:dummy_resource, 2, component: current_component) } + let(:author) { create(:user, :malicious, :confirmed, organization: current_component.organization) } + let!(:reportables) { create_list(:dummy_resource, 2, author:, component: current_component) } let(:participatory_space_path) do decidim_admin_assemblies.moderations_path(assembly) end diff --git a/decidim-assemblies/spec/system/admin/invite_assembly_admin_spec.rb b/decidim-assemblies/spec/system/admin/invite_assembly_admin_spec.rb index 17459a1c34cd8..53c136fe1b77c 100644 --- a/decidim-assemblies/spec/system/admin/invite_assembly_admin_spec.rb +++ b/decidim-assemblies/spec/system/admin/invite_assembly_admin_spec.rb @@ -6,7 +6,7 @@ describe "Invite assembly administrator" do let(:participatory_space) { create(:assembly) } - let(:private_participatory_space) { create(:assembly, private_space: true) } + let(:members_participatory_space) { create(:assembly, has_members: true) } let(:about_this_space_label) { "About this assembly" } let(:space_admins_label) { "Assembly admins" } let(:space_sidebar_label) { "Assemblies" } diff --git a/decidim-assemblies/spec/system/assembly_breadcrumb_spec.rb b/decidim-assemblies/spec/system/assemblies_breadcrumbs_spec.rb similarity index 99% rename from decidim-assemblies/spec/system/assembly_breadcrumb_spec.rb rename to decidim-assemblies/spec/system/assemblies_breadcrumbs_spec.rb index bd826e5802827..491d5563bca50 100644 --- a/decidim-assemblies/spec/system/assembly_breadcrumb_spec.rb +++ b/decidim-assemblies/spec/system/assemblies_breadcrumbs_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -describe "Assembly Breadcrumb" do +describe "Assemblies Breadcrumb" do let(:organization) { create(:organization) } let(:parent_assembly) { create(:assembly, :published, organization:) } let(:child_assembly) { create(:assembly, :published, organization:, parent: parent_assembly) } diff --git a/decidim-assemblies/spec/system/members_spec.rb b/decidim-assemblies/spec/system/members_spec.rb new file mode 100644 index 0000000000000..b91f69bd35337 --- /dev/null +++ b/decidim-assemblies/spec/system/members_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require "spec_helper" +require "decidim/core/test/shared_examples/participatory_space_members_shared_examples" + +describe "Assembly members" do + let(:assembly) { create(:assembly, :with_content_blocks, organization:, blocks_manifests:, has_members: true) } + let(:participatory_space) { assembly } + let(:participatory_space_homepage_path) { decidim_assemblies.assembly_path(participatory_space, locale: I18n.locale) } + let(:members_path) { decidim_assemblies.assembly_members_path(participatory_space, locale: I18n.locale) } + let(:unexisting_participatory_space_members_path) { decidim_assemblies.assembly_members_path(assembly_slug: 999_999_999, locale: I18n.locale) } + + it_behaves_like "participatory space members" +end diff --git a/decidim-assemblies/spec/system/participatory_space_private_users_spec.rb b/decidim-assemblies/spec/system/participatory_space_private_users_spec.rb deleted file mode 100644 index 17a026b5e0ec4..0000000000000 --- a/decidim-assemblies/spec/system/participatory_space_private_users_spec.rb +++ /dev/null @@ -1,123 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe "Assembly private users" do - let(:organization) { create(:organization) } - let(:assembly) { create(:assembly, :with_content_blocks, organization:, blocks_manifests:, private_space: true) } - let(:privatable_to) { assembly } - let(:blocks_manifests) { [] } - - let(:user) { create(:user, organization: privatable_to.organization) } - let(:ceased_user) { create(:user, organization: privatable_to.organization) } - - before do - switch_to_host(organization.host) - end - - context "when there are no assembly members and directly accessing from URL" do - it_behaves_like "a 404 page" do - let(:target_path) { decidim_assemblies.assembly_participatory_space_private_users_path(assembly, locale: I18n.locale) } - end - end - - context "when there are no assembly members and accessing from the assembly homepage" do - context "and the main data content block is disabled" do - it "the menu nav is not shown" do - visit decidim_assemblies.assembly_path(assembly, locale: I18n.locale) - - expect(page).to have_no_css(".participatory-space__nav-container") - end - end - - context "and the main data content block is enabled" do - let(:blocks_manifests) { ["main_data"] } - - it "the menu link is not shown" do - visit decidim_assemblies.assembly_path(assembly, locale: I18n.locale) - - expect(page).to have_no_content("Members") - end - end - end - - context "when the assembly does not exist" do - it_behaves_like "a 404 page" do - let(:target_path) { decidim_assemblies.assembly_participatory_space_private_users_path(assembly_slug: 999_999_999, locale: I18n.locale) } - end - end - - context "when there are some assembly members and all are unpublished" do - before do - create(:participatory_space_private_user, user:, privatable_to:, published: false) - end - - context "and directly accessing from URL" do - it_behaves_like "a 404 page" do - let(:target_path) { decidim_assemblies.assembly_participatory_space_private_users_path(assembly, locale: I18n.locale) } - end - end - - context "and accessing from the homepage" do - context "and the main data content block is disabled" do - it "the menu nav is not shown" do - visit decidim_assemblies.assembly_path(assembly, locale: I18n.locale) - - expect(page).to have_no_css(".participatory-space__nav-container") - end - end - - context "and the main data content block is enabled" do - let(:blocks_manifests) { ["main_data"] } - - it "the menu link is not shown" do - visit decidim_assemblies.assembly_path(assembly, locale: I18n.locale) - - expect(page).to have_no_content("Members") - end - end - end - end - - context "when there are some published assembly members" do - let!(:private_user) { create(:participatory_space_private_user, user:, privatable_to:, published: true) } - let!(:ceased_private_user) { create(:participatory_space_private_user, user: ceased_user, privatable_to:, published: false) } - - before do - visit decidim_assemblies.assembly_participatory_space_private_users_path(assembly, locale: I18n.locale) - end - - context "and accessing from the assembly homepage" do - context "and the main data content block is disabled" do - it "the menu nav is not shown" do - visit decidim_assemblies.assembly_path(assembly, locale: I18n.locale) - - expect(page).to have_no_css(".participatory-space__nav-container") - end - end - - context "and the main data content block is enabled" do - let(:blocks_manifests) { ["main_data"] } - - it "the menu link is shown" do - visit decidim_assemblies.assembly_path(assembly, locale: I18n.locale) - - within ".participatory-space__nav-container" do - expect(page).to have_content("Members") - click_on "Members" - end - - expect(page).to have_current_path decidim_assemblies.assembly_participatory_space_private_users_path(assembly, locale: I18n.locale) - end - end - - it "lists all the non ceased assembly members" do - within "#assembly_members-grid" do - expect(page).to have_css(".profile__user", count: 1) - - expect(page).to have_no_content(Decidim::ParticipatorySpacePrivateUserPresenter.new(ceased_private_user).name) - end - end - end - end -end diff --git a/decidim-assemblies/spec/system/private_assemblies_spec.rb b/decidim-assemblies/spec/system/private_assemblies_spec.rb index e51c3d216ff11..c715c38be55d2 100644 --- a/decidim-assemblies/spec/system/private_assemblies_spec.rb +++ b/decidim-assemblies/spec/system/private_assemblies_spec.rb @@ -9,8 +9,8 @@ let!(:user) { create(:user, :confirmed, organization:) } let!(:other_user) { create(:user, :confirmed, organization:) } let!(:other_user2) { create(:user, :confirmed, organization:) } - let!(:assembly_private_user) { create(:assembly_private_user, user: other_user, privatable_to: private_assembly) } - let!(:assembly_private_user2) { create(:assembly_private_user, user: other_user2, privatable_to: private_assembly) } + let!(:assembly_member) { create(:assembly_member, user: other_user, participatory_space: private_assembly) } + let!(:assembly_member2) { create(:assembly_member, user: other_user2, participatory_space: private_assembly) } context "when there are private assemblies" do context "and the assembly is transparent" do @@ -37,7 +37,7 @@ end context "when user is logged in" do - context "when is not an assembly private user" do + context "when is not an assembly member" do before do switch_to_host(organization.host) login_as user, scope: :user @@ -98,7 +98,7 @@ end end - context "when user is logged in and is not an assembly private user" do + context "when user is logged in and is not an assembly member" do context "when the user is not admin" do before do switch_to_host(organization.host) @@ -155,7 +155,7 @@ end end - context "when user is logged in and is an assembly private user" do + context "when user is logged in and is an assembly member" do before do switch_to_host(organization.host) login_as other_user, scope: :user diff --git a/decidim-assemblies/spec/types/integration_schema_spec.rb b/decidim-assemblies/spec/types/integration_schema_spec.rb index 2762812f16ab5..506a9bf3b697c 100644 --- a/decidim-assemblies/spec/types/integration_schema_spec.rb +++ b/decidim-assemblies/spec/types/integration_schema_spec.rb @@ -191,6 +191,7 @@ ) end + include_examples "when the introspection is disabled" describe "valid query" do it "executes successfully" do expect { response }.not_to raise_error diff --git a/decidim-blogs/app/controllers/decidim/blogs/posts_controller.rb b/decidim-blogs/app/controllers/decidim/blogs/posts_controller.rb index a0c885cfe7624..aeffda5c91004 100644 --- a/decidim-blogs/app/controllers/decidim/blogs/posts_controller.rb +++ b/decidim-blogs/app/controllers/decidim/blogs/posts_controller.rb @@ -26,7 +26,7 @@ def new def create enforce_permission_to :create, :blogpost - @form = form(Decidim::Blogs::PostForm).from_params(params, current_component: current_component) + @form = form(Decidim::Blogs::PostForm).from_params(params, current_component:) CreatePost.call(@form) do on(:ok) do |new_post| @@ -48,7 +48,7 @@ def edit def update enforce_permission_to :update, :blogpost, blogpost: post - @form = form(PostForm).from_params(params, current_component: current_component) + @form = form(PostForm).from_params(params, current_component:) UpdatePost.call(@form, post) do on(:ok) do |post| diff --git a/decidim-blogs/app/forms/decidim/blogs/post_form.rb b/decidim-blogs/app/forms/decidim/blogs/post_form.rb index 9a23a3943f563..c19adec62d439 100644 --- a/decidim-blogs/app/forms/decidim/blogs/post_form.rb +++ b/decidim-blogs/app/forms/decidim/blogs/post_form.rb @@ -48,7 +48,7 @@ def can_set_author end def post - @post ||= Post.find_by(id: id) + @post ||= Post.find_by(id:) end def participatory_space_manifest diff --git a/decidim-blogs/app/views/decidim/blogs/posts/index.html.erb b/decidim-blogs/app/views/decidim/blogs/posts/index.html.erb index c3c33130c29ef..4e61a3b8c66fc 100644 --- a/decidim-blogs/app/views/decidim/blogs/posts/index.html.erb +++ b/decidim-blogs/app/views/decidim/blogs/posts/index.html.erb @@ -1,7 +1,7 @@ <% add_decidim_meta_tags( description: translated_attribute(current_component.participatory_space.try(:description)), title: t("decidim.components.pagination.page_title", - component_name: component_name, + component_name:, current_page: paginate_posts.current_page, total_pages: paginate_posts.total_pages ), url: posts_url, diff --git a/decidim-blogs/app/views/decidim/blogs/posts/show.html.erb b/decidim-blogs/app/views/decidim/blogs/posts/show.html.erb index 259ea2d8c6405..37f5e2428b66b 100644 --- a/decidim-blogs/app/views/decidim/blogs/posts/show.html.erb +++ b/decidim-blogs/app/views/decidim/blogs/posts/show.html.erb @@ -39,7 +39,7 @@ <%= cell "decidim/author", post_presenter.author, from: post, context_actions: [:date], layout: :compact %>
<%= render "decidim/shared/resource_actions", resource: post do %> - <%= render "decidim/blogs/posts/menu_actions", post: post %> + <%= render "decidim/blogs/posts/menu_actions", post: %> <% end %>
diff --git a/decidim-blogs/config/locales/cs.yml b/decidim-blogs/config/locales/cs.yml index 8125c6a3e8400..4320b8cef0208 100644 --- a/decidim-blogs/config/locales/cs.yml +++ b/decidim-blogs/config/locales/cs.yml @@ -111,6 +111,7 @@ cs: destroy: Smazat like: Líbí se mi update: Aktualizovat + vote_comment: Komentář hlasování name: Blog settings: global: diff --git a/decidim-blogs/config/locales/fi.yml b/decidim-blogs/config/locales/fi.yml index 273118d593726..c954c630a1c4c 100644 --- a/decidim-blogs/config/locales/fi.yml +++ b/decidim-blogs/config/locales/fi.yml @@ -12,7 +12,7 @@ fi: models: decidim/blogs/post: one: Artikkeli - other: Artikkelia + other: Artikkelit decidim: admin: admin_log: diff --git a/decidim-blogs/config/locales/ja.yml b/decidim-blogs/config/locales/ja.yml index c794a68e776e7..c2741bde08503 100644 --- a/decidim-blogs/config/locales/ja.yml +++ b/decidim-blogs/config/locales/ja.yml @@ -105,6 +105,7 @@ ja: destroy: 削除 like: いいね update: 更新 + vote_comment: コメントに投票 name: ブログ settings: global: diff --git a/decidim-blogs/config/locales/pt-BR.yml b/decidim-blogs/config/locales/pt-BR.yml index d3bff9a68ee5f..b0e3bd6703b44 100644 --- a/decidim-blogs/config/locales/pt-BR.yml +++ b/decidim-blogs/config/locales/pt-BR.yml @@ -7,30 +7,43 @@ pt-BR: published_at: Horário da publicação title: Título models: - decidim/blogs/create_post_event: Nova postagem no blog + decidim/blogs/create_post_event: Nova postagem no ‘blog’ activerecord: models: decidim/blogs/post: one: Postagem other: Postagens decidim: + admin: + admin_log: + changeset: + posts: Postagens + tooltips: + deleted_posts_info: Não é possível excluir este post blogs: actions: author_id: Criar publicação como + confirm_delete_post: Deseja mesmo excluir este post? + deleted_posts_info: Postagens excluídas podem ser restaurados da lixeira. edit: Editar new: Nova postagem title: Ações + view_deleted_posts: Ver propostas excluídas admin: posts: create: invalid: Houve um problema ao criar esta postagem. success: Post criado com sucesso. + destroy: + success: Postagem excluída com sucesso. edit: save: Atualizar title: Editar publicação index: not_published_yet: Não publicado. title: Postagens + manage_trash: + title: Postagens excluídas new: create: Criar title: Criar post @@ -39,9 +52,11 @@ pt-BR: success: Post salvo com sucesso. admin_log: post: - create: "%{user_name} criou a publicação no blog %{resource_name} do %{space_name}" - delete: "O %{user_name} excluiu a publicação no blog %{resource_name} do %{space_name}" - update: "%{user_name} atualizou a publicação no blog %{resource_name} do %{space_name}" + create: "%{user_name} Criou a publicação no ‘blog’ %{resource_name} do %{space_name}" + delete: "O %{user_name} excluiu a publicação no ‘blog’ %{resource_name} do %{space_name}" + restore: "%{user_name} restaurou a postagem do blog %{resource_name} no %{space_name}" + soft_delete: "%{user_name} moveu a postagem do blog %{resource_name} no %{space_name} para a lixeira" + update: "%{user_name} Atualizou a publicação no ‘blog’ %{resource_name} do %{space_name}" content_blocks: highlighted_posts: last_published: Último publicado @@ -56,29 +71,60 @@ pt-BR: body: Corpo official_blog_post: Publicação oficial published_at: Horário da publicação + taxonomies: Taxonomias title: Título posts: + edit: + add_attachments: Adicionar anexos + attachment_legend: Adicionar um documento ou uma imagem + back: Voltar à postagem + button: Atualizar + edit_attachments: Editar anexos + title: Editar postagem + form: + author_id: Autor + body: Corpo + title: Título index: count: one: "%{count} publicação" other: "%{count} publicações" empty: Não existem publicações ainda. + new_post: Nova postagem + menu_actions: + button_destroy: Excluir postagem + button_destroy_confirm: Tem certeza que deseja excluir esta postagem? + button_edit: Editar postagem + new: + back: Voltar às postagens + button: Criar + title: Criar nova postagem components: blogs: actions: comment: Comentário create: Criar destroy: Excluir + like: Curti update: Atualizar + vote_comment: Vote no comentário name: Blog settings: global: announcement: Anúncio + attachments_allowed: Permitir anexos comments_enabled: Comentários ativados comments_max_length: Tamanho máximo de comentários (deixe 0 para o valor padrão) + creation_enabled_for_participants: Os usuários podem criar postagens + define_taxonomy_filters: Por favor, defina alguns filtros para este espaço participativo antes de usar esta configuração. + no_taxonomy_filters_found: Nenhum filtro de taxonomia encontrado. + taxonomy_filters: Selecione filtros para o componente + taxonomy_filters_add: Adicionar filtro step: announcement: Anúncio comments_blocked: Comentários bloqueados + likes_blocked: Curtidas bloqueadas + likes_enabled: Curtidas ativadas events: blogs: post_created: @@ -86,5 +132,33 @@ pt-BR: email_outro: Você recebeu esta notificação porque está seguindo "%{participatory_space_title}". Você pode deixar de segui-lo no link anterior. email_subject: Nova postagem publicada em %{participatory_space_title} notification_title: A postagem %{resource_title} foi publicada em %{participatory_space_title} + open_data: + help: + post_comments: + alignment: Se este comentário foi um favorito, contra ou neutro + author: O nome do usuário que fez este comentário + body: O comentário em si + commentable_id: A identificação única do comentável + commentable_type: O tipo do comentário (se foi um resultado, uma proposta, etc.) + created_at: A data em que este comentário foi criado + depth: O lugar onde este comentário está nos três comentários (se for uma resposta ou uma resposta de uma resposta) + id: O ID deste comentário + locale: O idioma (localidade) que o participante tinha ao deixar este comentário + root_commentable_url: O URL do recurso relacionado a este comentário + posts: + author: Informações do autor + body: O corpo da postagem + comments_count: O número de comentários que esta publicação recebeu + component: O componente ao qual a postagem pertence + created_at: A data em que esta publicação foi criada + follows_count: O número de seguidores que esta publicação tem + id: O identificador único desta publicação + likes_count: O número de curtidas que esta publicação tem + participatory_space: A que espaço (por exemplo, Processo Participativo ou Assembleia) pertence esta publicação + published_at: A data em que esta postagem foi publicada + title: O título da postagem + updated_at: Última data em que esta publicação foi atualizada + url: O URL para esta postagem statistics: posts_count: Postagens + posts_count_tooltip: O número de atualizações ou posts de ‘blog’, publicados. diff --git a/decidim-blogs/config/locales/ro-RO.yml b/decidim-blogs/config/locales/ro-RO.yml index 81a658a9117aa..6b3462daedb9f 100644 --- a/decidim-blogs/config/locales/ro-RO.yml +++ b/decidim-blogs/config/locales/ro-RO.yml @@ -19,6 +19,8 @@ ro: admin_log: changeset: posts: Postări + tooltips: + deleted_posts_info: Nu se poate șterge această postare blogs: actions: author_id: Creeați postare ca @@ -105,7 +107,9 @@ ro: comment: Comentați create: Creați destroy: Ștergeți + like: Apreciere update: Actualizați + vote_comment: Votați comentariul name: Blog settings: global: @@ -121,6 +125,8 @@ ro: step: announcement: Anunț comments_blocked: Comentarii blocate + likes_blocked: Aprecieri blocate + likes_enabled: Aprecieri activate events: blogs: post_created: @@ -149,6 +155,7 @@ ro: created_at: Data la care acestă postare a fost creată follows_count: Numărul de urmăritori al acestei postări id: Identificatorul unic al acestei postări + likes_count: Numărul de aprecieri pe care le are această postare participatory_space: Cărui spațiu (de exemplu, proces participativ sau adunare) aparține această postare published_at: Data la care acestă postare a fost publicată title: Titlul postării diff --git a/decidim-blogs/lib/decidim/api/post_type.rb b/decidim-blogs/lib/decidim/api/post_type.rb index 5e83e70cff8bd..894eb6c470f77 100644 --- a/decidim-blogs/lib/decidim/api/post_type.rb +++ b/decidim-blogs/lib/decidim/api/post_type.rb @@ -33,8 +33,6 @@ def self.authorized?(object, context) ].all? super && chain - rescue Decidim::PermissionAction::PermissionNotSetError - false end end end diff --git a/decidim-blogs/lib/decidim/blogs/component.rb b/decidim-blogs/lib/decidim/blogs/component.rb index c1e80ce8090b2..fa8b60e928c0e 100644 --- a/decidim-blogs/lib/decidim/blogs/component.rb +++ b/decidim-blogs/lib/decidim/blogs/component.rb @@ -9,10 +9,6 @@ component.query_type = "Decidim::Blogs::BlogsType" - component.on(:before_destroy) do |instance| - raise StandardError, "Cannot remove this component" if Decidim::Blogs::Post.where(component: instance).any? - end - component.on(:publish) do |instance| Decidim::Blogs::Post.where(component: instance).find_in_batches(batch_size: 100) do |batch| Decidim::UpdateSearchIndexesJob.perform_later(batch) diff --git a/decidim-blogs/spec/system/blogs_breadcrumbs_spec.rb b/decidim-blogs/spec/system/blogs_breadcrumbs_spec.rb new file mode 100644 index 0000000000000..d3d4dd0b6bf99 --- /dev/null +++ b/decidim-blogs/spec/system/blogs_breadcrumbs_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Blogs Breadcrumb" do + include_context "with a component" + let(:manifest_name) { "blogs" } + let!(:post) { create(:post, component:) } + + before do + visit_component + end + + describe "index" do + it "shows the correct information in breadcrumb (space, component)" do + within(".menu-bar") do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + end + end + end + + describe "show" do + it "shows the correct information in breadcrumb (space, component, post)" do + click_on translated(post.title) + + within(".menu-bar") do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + expect(page).to have_content(translated(post.title)) + end + end + end +end diff --git a/decidim-blogs/spec/system/explore_posts_spec.rb b/decidim-blogs/spec/system/explore_posts_spec.rb index 9cba82f5d0380..babc8cdd00eb2 100644 --- a/decidim-blogs/spec/system/explore_posts_spec.rb +++ b/decidim-blogs/spec/system/explore_posts_spec.rb @@ -37,10 +37,13 @@ visit_component end - it "shows the component name in the sidebar" do + it "shows the correct information in breadcrumb" do within(".menu-bar") do expect(page).to have_content(translated(component.name)) end + end + + it "shows the component name in the sidebar" do within("aside") do expect(page).to have_content(translated(component.name)) end @@ -89,10 +92,6 @@ within ".author__name" do expect(page).to have_content("Official") end - within(".menu-bar") do - expect(page).to have_content(translated(component.name)) - expect(page).to have_content(translated(post.title)) - end end end diff --git a/decidim-blogs/spec/types/blogs_type_spec.rb b/decidim-blogs/spec/types/blogs_type_spec.rb index 5a9ae0829a6e9..3c0ae899a9e9f 100644 --- a/decidim-blogs/spec/types/blogs_type_spec.rb +++ b/decidim-blogs/spec/types/blogs_type_spec.rb @@ -39,8 +39,8 @@ module Blogs context "when the post does not belong to the component" do let!(:post) { create(:post, component: create(:post_component)) } - it "returns null" do - expect(response["post"]).to be_nil + it "raises error" do + expect { response }.to raise_error(Decidim::Api::Errors::NotFoundError, "Post not found") end end end diff --git a/decidim-blogs/spec/types/post_type_spec.rb b/decidim-blogs/spec/types/post_type_spec.rb index 294b106257278..26c47eeb54cc9 100644 --- a/decidim-blogs/spec/types/post_type_spec.rb +++ b/decidim-blogs/spec/types/post_type_spec.rb @@ -17,6 +17,12 @@ module Blogs include_examples "likeable interface" include_examples "followable interface" + shared_examples "unauthorized Post" do + it "throws Decidim::Api::Errors::UnauthorizedObjectError" do + expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedObjectError, "You cannot view or edit this Post because you do not have permissions") + end + end + describe "id" do let(:query) { "{ id }" } @@ -55,9 +61,7 @@ module Blogs let(:model) { create(:post, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Post" end context "when participatory space is private but transparent" do @@ -77,9 +81,7 @@ module Blogs let(:model) { create(:post, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Post" end context "when component is not published" do @@ -87,9 +89,7 @@ module Blogs let(:model) { create(:post, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Post" end context "when post is moderated" do @@ -97,9 +97,7 @@ module Blogs let(:query) { "{ id }" } let(:root_value) { model.reload } - it "returns all the required fields" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Post" end context "when post is not published" do @@ -107,9 +105,7 @@ module Blogs let(:model) { create(:post, published_at: nil, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Post" end end end diff --git a/decidim-budgets/app/commands/decidim/budgets/admin/import_proposals_to_budgets.rb b/decidim-budgets/app/commands/decidim/budgets/admin/import_proposals_to_budgets.rb index 52d4a7450690c..ebeee8d734727 100644 --- a/decidim-budgets/app/commands/decidim/budgets/admin/import_proposals_to_budgets.rb +++ b/decidim-budgets/app/commands/decidim/budgets/admin/import_proposals_to_budgets.rb @@ -68,7 +68,17 @@ def budget_for(original_proposal) end def proposals - Decidim::Proposals::Proposal.where(component: origin_component).published.not_hidden.not_withdrawn.accepted.order(:published_at) + proposals = Decidim::Proposals::Proposal.where(component: origin_component).published.not_hidden.not_withdrawn + + if form.internal_states.present? + if form.internal_states.include?("not_answered") + proposals.not_answered.or(proposals.where(id: proposals.only_status(form.internal_states).pluck(:id))) + else + proposals.only_status(form.internal_states) + end + else + proposals + end.order(:published_at) end def origin_component diff --git a/decidim-budgets/app/controllers/decidim/budgets/projects_controller.rb b/decidim-budgets/app/controllers/decidim/budgets/projects_controller.rb index 9f0508765e23c..4c314f1d72c99 100644 --- a/decidim-budgets/app/controllers/decidim/budgets/projects_controller.rb +++ b/decidim-budgets/app/controllers/decidim/budgets/projects_controller.rb @@ -6,10 +6,11 @@ module Budgets class ProjectsController < Decidim::Budgets::ApplicationController include FilterResource include NeedsCurrentOrder + include Decidim::AttachmentsHelper include Decidim::Budgets::Orderable include Decidim::IconHelper - helper_method :projects, :project, :budget, :all_geocoded_projects, :tabs, :panels, :resource_added? + helper_method :projects, :project, :budget, :all_geocoded_projects, :resource_added?, :tab_panel_items before_action :set_focus_mode_if_voting_open @@ -64,16 +65,8 @@ def show_selected_budgets? voting_finished? && budget.projects.selected.any? end - def tabs - @tabs ||= items.map { |item| item.slice(:id, :text, :icon) } - end - - def panels - @panels ||= items.map { |item| item.slice(:id, :method, :args) } - end - - def items - @items ||= [ + def tab_panel_items + @tab_panel_items ||= [ { enabled: ProjectHistoryCell.new(@project).render?, id: "included_history", @@ -82,24 +75,31 @@ def items method: :cell, args: ["decidim/budgets/project_history", @project] }, - { - enabled: @project.photos.present?, - id: "images", - text: t("decidim.application.photos.photos"), - icon: resource_type_icon_key("images"), - method: :cell, - args: ["decidim/images_panel", @project] - }, - { - enabled: @project.documents.present?, - id: "documents", - text: t("decidim.application.documents.documents"), - icon: resource_type_icon_key("documents"), - method: :cell, - args: ["decidim/documents_panel", @project] - } + *attachments_tab_panel_items(@project) ].select { |item| item[:enabled] } end + + def add_breadcrumb_item + return {} if project.blank? + + { + label: translated_attribute(project.title), + url: Decidim::EngineRouter.main_proxy(current_component).budget_project_url(budget, project, locale: current_locale), + active: false, + resource: project + } + end + + def add_parent_breadcrumb_item + return {} if budget.blank? + + { + label: translated_attribute(budget.title), + url: Decidim::EngineRouter.main_proxy(current_component).budget_projects_url(budget, locale: current_locale), + active: false, + resource: budget + } + end end end end diff --git a/decidim-budgets/app/forms/decidim/budgets/admin/project_import_proposals_form.rb b/decidim-budgets/app/forms/decidim/budgets/admin/project_import_proposals_form.rb index fe4f1ed29b9d2..2a5efb7f2a258 100644 --- a/decidim-budgets/app/forms/decidim/budgets/admin/project_import_proposals_form.rb +++ b/decidim-budgets/app/forms/decidim/budgets/admin/project_import_proposals_form.rb @@ -10,10 +10,9 @@ class ProjectImportProposalsForm < Decidim::Form attribute :origin_component_id, Integer attribute :default_budget, Integer - attribute :import_all_accepted_proposals, Boolean + attribute :internal_states, Array[String] validates :origin_component_id, :origin_component, :current_component, presence: true - validates :import_all_accepted_proposals, allow_nil: false, acceptance: true validates :default_budget, presence: true, numericality: { greater_than: 0 } def origin_component @@ -33,6 +32,17 @@ def origin_components_collection def budget @budget ||= context[:budget] end + + def available_states(component_id = nil) + scope = Decidim::Proposals::ProposalState + scope = scope.where(component: Decidim::Component.find(component_id)) if component_id.present? + + states = scope.pluck(:token).uniq.map do |token| + OpenStruct.new(token:, title: token.humanize) + end + + states + [OpenStruct.new(token: "not_answered", title: I18n.t("decidim.proposals.answers.not_answered"))] + end end end end diff --git a/decidim-budgets/app/views/decidim/budgets/admin/proposals_imports/new.html.erb b/decidim-budgets/app/views/decidim/budgets/admin/proposals_imports/new.html.erb index 9f772c6666f0d..907b43f81de50 100644 --- a/decidim-budgets/app/views/decidim/budgets/admin/proposals_imports/new.html.erb +++ b/decidim-budgets/app/views/decidim/budgets/admin/proposals_imports/new.html.erb @@ -20,9 +20,17 @@
<%= f.number_field :default_budget, label: t(".default_budget") %>
-
- <%= f.check_box :import_all_accepted_proposals, label: t(".import_all_accepted_proposals") %> -
+ <% if @form.available_states.any? %> + + <%= t(".proposal_states_help") %> +
+ <%= f.collection_check_boxes :internal_states, @form.available_states, :token, ->(a) { translated_attribute(a.title) } do |builder| %> +
+ <%= builder.label { builder.check_box + builder.text } %> +
+ <% end %> +
+ <% end %>
diff --git a/decidim-budgets/app/views/decidim/budgets/projects/_budget_summary.html.erb b/decidim-budgets/app/views/decidim/budgets/projects/_budget_summary.html.erb index fe9e4acd4f924..4d8fe7883cad7 100644 --- a/decidim-budgets/app/views/decidim/budgets/projects/_budget_summary.html.erb +++ b/decidim-budgets/app/views/decidim/budgets/projects/_budget_summary.html.erb @@ -1,6 +1,6 @@
" class="budget-summary <%= responsive ? "block md:hidden" : "hidden md:block" %>" data-progress-reference data-safe-url="<%= budget_url(budget) %>"> <% if responsive %> - <%= render partial: "decidim/budgets/projects/order_progress_summary/content_responsive", locals: { focus_mode_origin: focus_mode_origin } %> + <%= render partial: "decidim/budgets/projects/order_progress_summary/content_responsive", locals: { focus_mode_origin: } %> <% else %> <%= render partial: "decidim/budgets/projects/order_progress_summary/content" %> <% end %> diff --git a/decidim-budgets/app/views/decidim/budgets/projects/order_progress_summary/_content.html.erb b/decidim-budgets/app/views/decidim/budgets/projects/order_progress_summary/_content.html.erb index d4257b75eebb7..cba6776611e75 100644 --- a/decidim-budgets/app/views/decidim/budgets/projects/order_progress_summary/_content.html.erb +++ b/decidim-budgets/app/views/decidim/budgets/projects/order_progress_summary/_content.html.erb @@ -2,11 +2,13 @@ <%= render partial: "decidim/budgets/projects/order_progress_summary/progress_box" %> <%= render partial: "decidim/budgets/projects/order_progress_summary/progress_box_buttons" %> -
- -
+ <% if cell("decidim/budgets/budget_information_modal", budget).more_information.present? %> +
+ +
+ <% end %>
diff --git a/decidim-budgets/app/views/decidim/budgets/projects/show.html.erb b/decidim-budgets/app/views/decidim/budgets/projects/show.html.erb index bd36576b7c8fd..a4393f5a824aa 100644 --- a/decidim-budgets/app/views/decidim/budgets/projects/show.html.erb +++ b/decidim-budgets/app/views/decidim/budgets/projects/show.html.erb @@ -54,24 +54,8 @@ edit_link(
- <% if tabs.any? %> -
-
    - <% tabs.each_with_index do |tab, i| %> -
  • - -
  • - <% end %> -
- - <% panels.each do |panel| %> -
- <%= send(panel[:method], *panel[:args]) %> -
- <% end %> -
+ <% if tab_panel_items.any? %> + <%= cell "decidim/tab_panels", tab_panel_items %> <% end %>
diff --git a/decidim-budgets/config/locales/bg.yml b/decidim-budgets/config/locales/bg.yml index bcfdb15b3299d..f25c399bc2a07 100644 --- a/decidim-budgets/config/locales/bg.yml +++ b/decidim-budgets/config/locales/bg.yml @@ -107,7 +107,6 @@ bg: new: create: Импортиране на предложения в проекти default_budget: Бюджет по подразбиране - import_all_accepted_proposals: Импортиране на всички приети предложения no_components: Няма други компоненти на предложения в това пространство за участие за импортиране на предложенията в проекти. origin_component_id: Компонент за произход select_component: Моля, изберете компонент diff --git a/decidim-budgets/config/locales/ca-IT.yml b/decidim-budgets/config/locales/ca-IT.yml index 201c10001bf2c..0f8111422d786 100644 --- a/decidim-budgets/config/locales/ca-IT.yml +++ b/decidim-budgets/config/locales/ca-IT.yml @@ -142,9 +142,10 @@ ca-IT: new: create: Importa propostes a projectes default_budget: Pressupost per defecte - import_all_accepted_proposals: Importar totes les propostes acceptades no_components: No hi ha cap component de propostes en aquest espai participatiu per importar les propostes a projectes. origin_component_id: Component d'origen + proposal_states: Estats de resposta a la proposta + proposal_states_help: Selecciona un o més estats de resposta de les propostes a importar com a projectes. Si no seleccioneu cap estat de resposta, s'importaran totes les propostes publicades que encara no hagin estat importades a altres pressupostos d'aquest mateix component. select_component: Selecciona un component title: Importa propostes a projectes reminders: diff --git a/decidim-budgets/config/locales/ca.yml b/decidim-budgets/config/locales/ca.yml index 3b3abbbbe4f4f..0fa323df97509 100644 --- a/decidim-budgets/config/locales/ca.yml +++ b/decidim-budgets/config/locales/ca.yml @@ -142,9 +142,10 @@ ca: new: create: Importa propostes a projectes default_budget: Pressupost per defecte - import_all_accepted_proposals: Importar totes les propostes acceptades no_components: No hi ha cap component de propostes en aquest espai participatiu per importar les propostes a projectes. origin_component_id: Component d'origen + proposal_states: Estats de resposta a la proposta + proposal_states_help: Selecciona un o més estats de resposta de les propostes a importar com a projectes. Si no seleccioneu cap estat de resposta, s'importaran totes les propostes publicades que encara no hagin estat importades a altres pressupostos d'aquest mateix component. select_component: Selecciona un component title: Importa propostes a projectes reminders: diff --git a/decidim-budgets/config/locales/cs.yml b/decidim-budgets/config/locales/cs.yml index 49d004a5beaef..ee132992646ef 100644 --- a/decidim-budgets/config/locales/cs.yml +++ b/decidim-budgets/config/locales/cs.yml @@ -146,7 +146,6 @@ cs: new: create: Návrhy na projekty default_budget: Výchozí rozpočet - import_all_accepted_proposals: Importovat všechny přijaté návrhy no_components: V tomto participativním prostoru neexistují jiné komponenty návrhu, které by mohly importovat návrhy do projektů. origin_component_id: Původ komponenty select_component: Vyberte součást @@ -303,6 +302,8 @@ cs: dynamic_help: minimum_reached: Dosáhli jste minima, abyste mohli hlasovat minimum: Minimum + minimum_projects_rule: + description: "Vyberte alespoň %{minimum_number} projektů, které chcete a hlasujete podle vašich preferencí." orders: highest_cost: Nejvyšší náklady label: Seřadit projekty podle diff --git a/decidim-budgets/config/locales/de.yml b/decidim-budgets/config/locales/de.yml index f8073ef1aab84..687a579e5a6e8 100644 --- a/decidim-budgets/config/locales/de.yml +++ b/decidim-budgets/config/locales/de.yml @@ -142,7 +142,6 @@ de: new: create: Importieren Sie Vorschläge in Projekte default_budget: Standardbudget - import_all_accepted_proposals: Alle akzeptierten Vorschläge importieren no_components: Es gibt keine weiteren Vorschlagskomponenten in diesem partizipativen Raum, um die Vorschläge in Projekte zu importieren. origin_component_id: Ursprungskomponente select_component: Bitte wählen Sie eine Komponente aus @@ -307,8 +306,8 @@ de: remove: Projekt %{resource_name} aus Ihrer Abstimmung entfernen. selected: Ausgewählt votes: - one: Abstimmung - other: Abstimmungen + one: Stimme + other: Stimmen you_voted: Sie haben dafür gestimmt project_budget_button: add: Zur Abstimmung hinzufügen @@ -326,7 +325,7 @@ de: prompt: Budget auswählen vote_reminder_mailer: vote_reminder: - email_budgets: 'Bereiche, in denen Sie eine unvollendete Abstimmung haben:' + email_budgets: 'In diesen Bereichen haben Sie die Abstimmung noch nicht abgeschlossen:' email_intro: Sie haben Ihre begonnene Abstimmung zur Verteilung des partizipativen Budgets noch nicht abgeschlossen. email_link: Mit der Abstimmung fortfahren email_outro: Denken Sie daran, die Abstimmung vollständig abzuschließen. Wählen Sie dazu den gewünschten Vorschlag oder die gewünschten Vorschläge aus, denen Sie Ihre Stimme geben möchten. Bestätigen Sie anschliessend Ihre Auswahl, indem Sie auf "Abstimmen" klicken. diff --git a/decidim-budgets/config/locales/el.yml b/decidim-budgets/config/locales/el.yml index 6bdc9621d9913..d4543193e15c3 100644 --- a/decidim-budgets/config/locales/el.yml +++ b/decidim-budgets/config/locales/el.yml @@ -95,7 +95,6 @@ el: new: create: Εισαγωγή προτάσεων σε έργα default_budget: Προεπιλεγμένος προϋπολογισμός - import_all_accepted_proposals: Εισαγωγή όλων των αποδεχθείσων προτάσεων no_components: Δεν υπάρχουν άλλα στοιχεία προτάσεων σε αυτόν τον χώρο συμμετοχής για εισαγωγή των προτάσεων σε έργα. origin_component_id: Στοιχείο καταγωγής select_component: Επιλέξτε ένα στοιχείο diff --git a/decidim-budgets/config/locales/en.yml b/decidim-budgets/config/locales/en.yml index 5f8ca1d601fe7..d4421a1bf919f 100644 --- a/decidim-budgets/config/locales/en.yml +++ b/decidim-budgets/config/locales/en.yml @@ -143,9 +143,10 @@ en: new: create: Import proposals to projects default_budget: Default budget - import_all_accepted_proposals: Import all accepted proposals no_components: There are no other proposal components in this participatory space to import the proposals into projects. origin_component_id: Origin component + proposal_states: Proposal States + proposal_states_help: Select one or more proposal states to import into projects. By selecting none, you will import all published proposals that have not already been imported in other budgets from this component. select_component: Please select a component title: Import proposals to projects reminders: diff --git a/decidim-budgets/config/locales/es-MX.yml b/decidim-budgets/config/locales/es-MX.yml index 37beb0083178d..55721d2b44e35 100644 --- a/decidim-budgets/config/locales/es-MX.yml +++ b/decidim-budgets/config/locales/es-MX.yml @@ -142,9 +142,10 @@ es-MX: new: create: Importar propuestas a proyectos default_budget: Presupuesto por defecto - import_all_accepted_proposals: Importar todas las propuestas aceptadas no_components: No hay otros componentes de la propuesta en este espacio participativo para importar las propuestas en los proyectos. origin_component_id: Componente de origen + proposal_states: Estados de repuesta a la propuesta + proposal_states_help: Selecciona uno o más estados de respuesta de las propuestas a importar como proyectos. Si no seleccionas ningún estado de respuesta, se importaran todas las propuestas publicadas que no hayan sido importadas aún a otros presupuestos de este mismo componente. select_component: Por favor seleccione un componente title: Importar propuestas a proyectos reminders: diff --git a/decidim-budgets/config/locales/es-PY.yml b/decidim-budgets/config/locales/es-PY.yml index 07f8cde89d1f1..a01be4863c46a 100644 --- a/decidim-budgets/config/locales/es-PY.yml +++ b/decidim-budgets/config/locales/es-PY.yml @@ -142,9 +142,10 @@ es-PY: new: create: Importar propuestas a proyectos default_budget: Presupuesto por defecto - import_all_accepted_proposals: Importar todas las propuestas aceptadas no_components: No hay otros componentes de la propuesta en este espacio participativo para importar las propuestas en los proyectos. origin_component_id: Componente de origen + proposal_states: Estados de repuesta a la propuesta + proposal_states_help: Selecciona uno o más estados de respuesta de las propuestas a importar como proyectos. Si no seleccionas ningún estado de respuesta, se importaran todas las propuestas publicadas que no hayan sido importadas aún a otros presupuestos de este mismo componente. select_component: Por favor seleccione un componente title: Importar propuestas a proyectos reminders: diff --git a/decidim-budgets/config/locales/es.yml b/decidim-budgets/config/locales/es.yml index 8b7cf69b083de..004c036dcde1c 100644 --- a/decidim-budgets/config/locales/es.yml +++ b/decidim-budgets/config/locales/es.yml @@ -142,9 +142,10 @@ es: new: create: Importar propuestas a proyectos default_budget: Presupuesto por defecto - import_all_accepted_proposals: Importar todas las propuestas aceptadas no_components: No hay otros componentes de la propuesta en este espacio participativo para importar las propuestas en los proyectos. origin_component_id: Componente de origen + proposal_states: Estados de repuesta a la propuesta + proposal_states_help: Selecciona uno o más estados de respuesta de las propuestas a importar como proyectos. Si no seleccionas ningún estado de respuesta, se importaran todas las propuestas publicadas que no hayan sido importadas aún a otros presupuestos de este mismo componente. select_component: Por favor seleccione un componente title: Importar propuestas a proyectos reminders: diff --git a/decidim-budgets/config/locales/eu.yml b/decidim-budgets/config/locales/eu.yml index 7df9582cdc6e6..e70d4e257cb53 100644 --- a/decidim-budgets/config/locales/eu.yml +++ b/decidim-budgets/config/locales/eu.yml @@ -142,9 +142,10 @@ eu: new: create: Inportatu proiektuetarako proposamenak default_budget: Aurrekontu lehenetsia - import_all_accepted_proposals: Inportatu onartutako proposamen guztiak no_components: Ez dago proposamen osagairik parte hartzeko espazio honetan proposamenak proiektuetara inportatzeko. origin_component_id: Jatorrizko osagaia + proposal_states: Proposamenen egoera + proposal_states_help: Hautatu proposamen-egoera bat edo gehiago, proiektuetara inportatzeko. Bat ere hautatzen ez baduzu, osagai honetako beste aurrekontu batzuetan inportatu ez diren argitaratutako proposamen guztiak inportatuko dituzu. select_component: Meesedez, hautatu osagaia title: Inportatu proiektuetarako proposamenak reminders: diff --git a/decidim-budgets/config/locales/fi-plain.yml b/decidim-budgets/config/locales/fi-plain.yml index 1061c2eccbaec..af6bf93fc3c40 100644 --- a/decidim-budgets/config/locales/fi-plain.yml +++ b/decidim-budgets/config/locales/fi-plain.yml @@ -142,9 +142,10 @@ fi-pl: new: create: Tuo ehdotuksia suunnitelmiin default_budget: Oletusbudjetti - import_all_accepted_proposals: Tuo kaikki hyväksytyt ehdotukset no_components: Tässä osallisuustilassa ei ole ole muita ehdotuskomponentteja, joista voitaisiin tuoda ehdotuksia suunnitelmiin. origin_component_id: Lähdekomponentti + proposal_states: Ehdotusten tilat + proposal_states_help: Valitse yksi tai useampi ehdotusten tila, joiden perusteella ehdotuksia tuodaan projekteiksi. Mikäli et valitse yhtään tilaa, kaikki julkaistut ehdotukset tuodaan, jos niitä ei ole aikaisemmin tuotu tämän komponentin budjetteihin. select_component: Valitse komponentti title: Tuo ehdotuksia projekteiksi reminders: diff --git a/decidim-budgets/config/locales/fi.yml b/decidim-budgets/config/locales/fi.yml index 74fa6abcd09cc..3c76e6380f3c4 100644 --- a/decidim-budgets/config/locales/fi.yml +++ b/decidim-budgets/config/locales/fi.yml @@ -142,9 +142,10 @@ fi: new: create: Tuo ehdotuksia projekteiksi default_budget: Oletusbudjetti - import_all_accepted_proposals: Tuo kaikki hyväksytyt ehdotukset no_components: Tässä osallistumistilassa ei ole ole muita ehdotuskomponentteja, joista voitaisiin tuoda ehdotuksia projekteiksi. origin_component_id: Lähdekomponentti + proposal_states: Ehdotusten tilat + proposal_states_help: Valitse yksi tai useampi ehdotusten tila, joiden perusteella ehdotuksia tuodaan projekteiksi. Mikäli et valitse yhtään tilaa, kaikki julkaistut ehdotukset tuodaan, jos niitä ei ole aikaisemmin tuotu tämän komponentin budjetteihin. select_component: Valitse komponentti title: Tuo ehdotuksia projekteiksi reminders: diff --git a/decidim-budgets/config/locales/fr-CA.yml b/decidim-budgets/config/locales/fr-CA.yml index d01b681714295..70a70272606d7 100644 --- a/decidim-budgets/config/locales/fr-CA.yml +++ b/decidim-budgets/config/locales/fr-CA.yml @@ -136,9 +136,10 @@ fr-CA: new: create: Importer des propositions dans des projets default_budget: Budget par défaut - import_all_accepted_proposals: Importer toutes les propositions acceptées no_components: Il n'y a pas d'autres modules de proposition dans cet espace participatif pour importer les propositions dans des projets. origin_component_id: Composant d'origine + proposal_states: Statuts de proposition + proposal_states_help: Sélectionnez un ou plusieurs statuts de proposition à importer dans les projets. Si vous n'en sélectionnez aucun, vous importerez toutes les propositions publiées qui n'ont pas déjà été importées dans d'autres budgets à partir de cette fonctionnalité. select_component: Veuillez sélectionner un module title: Importer des propositions dans des projets reminders: diff --git a/decidim-budgets/config/locales/fr.yml b/decidim-budgets/config/locales/fr.yml index 3cecf6f208df9..f6311b01d9287 100644 --- a/decidim-budgets/config/locales/fr.yml +++ b/decidim-budgets/config/locales/fr.yml @@ -136,9 +136,10 @@ fr: new: create: Importer des propositions dans des projets default_budget: Budget par défaut - import_all_accepted_proposals: Importer toutes les propositions acceptées no_components: Il n'y a pas d'autres modules de proposition dans cet espace participatif pour importer les propositions dans des projets. origin_component_id: Composant d'origine + proposal_states: Statuts de proposition + proposal_states_help: Sélectionnez un ou plusieurs statuts de proposition à importer dans les projets. Si vous n'en sélectionnez aucun, vous importerez toutes les propositions publiées qui n'ont pas déjà été importées dans d'autres budgets à partir de cette fonctionnalité. select_component: Veuillez sélectionner un module title: Importer des propositions dans des projets reminders: diff --git a/decidim-budgets/config/locales/hu.yml b/decidim-budgets/config/locales/hu.yml index f5a59346c5226..7feb3bcadd82d 100644 --- a/decidim-budgets/config/locales/hu.yml +++ b/decidim-budgets/config/locales/hu.yml @@ -103,7 +103,6 @@ hu: new: create: Javaslatok importálása projektekbe default_budget: Alapértelmezett költségvetés - import_all_accepted_proposals: Minden elfogadott javaslat importálása no_components: Ebben a részvételi térben nincs más javaslat-összetevő a javaslatok projektekbe való importálásához. origin_component_id: A komponens származása select_component: Válassz ki egy elemet diff --git a/decidim-budgets/config/locales/it.yml b/decidim-budgets/config/locales/it.yml index 4f1c3bd162f0a..010a436baa194 100644 --- a/decidim-budgets/config/locales/it.yml +++ b/decidim-budgets/config/locales/it.yml @@ -100,6 +100,8 @@ it: order: status: title: OK, il tuo voto è stato acquisito. + order_pdf: + title: Ok, il tuo voto è stato acquisito. order_summary_mailer: order_summary: selected_projects: 'I progetti che hai selezionato sono:' diff --git a/decidim-budgets/config/locales/ja.yml b/decidim-budgets/config/locales/ja.yml index 3ad5109ac8a3e..30d181087d3a7 100644 --- a/decidim-budgets/config/locales/ja.yml +++ b/decidim-budgets/config/locales/ja.yml @@ -140,9 +140,10 @@ ja: new: create: プロジェクトへの提案をインポート default_budget: 既定の予算 - import_all_accepted_proposals: すべての承認済みの提案をインポート no_components: この参加型スペースには、プロジェクトにインポートするための他の提案コンポーネントはありません。 origin_component_id: 元のコンポーネント + proposal_states: 提案状態 + proposal_states_help: プロジェクトにインポートする提案状態を1つ以上選択してください。何も選択しない場合、このコンポーネントの提案のうち、公開済みかつ他の予算にインポートされていないすべての提案をインポートします。 select_component: コンポーネントを選択してください title: プロジェクトに提案をインポート reminders: @@ -328,6 +329,7 @@ ja: actions: comment: コメント vote: 投票 + vote_comment: コメントに投票 name: 予算 settings: global: diff --git a/decidim-budgets/config/locales/lt.yml b/decidim-budgets/config/locales/lt.yml index efcd45f7cfa31..c1bc17510c53b 100644 --- a/decidim-budgets/config/locales/lt.yml +++ b/decidim-budgets/config/locales/lt.yml @@ -97,7 +97,6 @@ lt: new: create: Importuoti pasiūlymus į projektus default_budget: Numatytasis biudžetas - import_all_accepted_proposals: Importuoti visus priimtus projektus no_components: Nėra kitų pasiūlymų komponentų šioje dalyvaujamojoje erdvėje, kuriuos būtų galima importuoti į projektus. origin_component_id: Originalusis komponentas select_component: Pasirinkite komponentą diff --git a/decidim-budgets/config/locales/nl.yml b/decidim-budgets/config/locales/nl.yml index beabb767cac4f..95c3bdc461555 100644 --- a/decidim-budgets/config/locales/nl.yml +++ b/decidim-budgets/config/locales/nl.yml @@ -73,7 +73,6 @@ nl: new: create: Voorstellen importeren in projecten default_budget: Standaard budget - import_all_accepted_proposals: Alle geaccepteerde voorstellen importeren no_components: Er zijn geen andere voorstelonderdelen in deze participatieruimte om voorstellen in projecten te kunnen importeren. origin_component_id: Oorsprong component select_component: Selecteer een component diff --git a/decidim-budgets/config/locales/pl.yml b/decidim-budgets/config/locales/pl.yml index 8c5a5e7a6e00d..c8020543b85de 100644 --- a/decidim-budgets/config/locales/pl.yml +++ b/decidim-budgets/config/locales/pl.yml @@ -106,7 +106,6 @@ pl: new: create: Importuj propozycje do projektów default_budget: Domyślny budżet - import_all_accepted_proposals: Importuj wszystkie zaakceptowane propozycje no_components: W tej przestrzeni partycypacyjnej nie ma innych komponentów do zaimportowania propozycji jako projektów. origin_component_id: Źródłowy komponent select_component: Wybierz komponent diff --git a/decidim-budgets/config/locales/pt-BR.yml b/decidim-budgets/config/locales/pt-BR.yml index 0934a1b10d774..851fe209234b3 100644 --- a/decidim-budgets/config/locales/pt-BR.yml +++ b/decidim-budgets/config/locales/pt-BR.yml @@ -17,6 +17,10 @@ pt-BR: scope_id: Escopo activerecord: models: + decidim: + budgets: + project: + text: 'Foi adicionado a este orçamento: %{link}' decidim/budgets/budget: one: Orçamento other: Orçamentos @@ -25,6 +29,9 @@ pt-BR: other: Projetos decidim: admin: + admin_log: + changeset: + projects: Projetos filters: projects: selected_at_null: @@ -32,15 +39,25 @@ pt-BR: values: 'false': Selecionado para implementação 'true': Não selecionado para implementação + taxonomies_part_of_contains: + label: Taxonomia + tooltips: + deleted_projects_info: Não é possível excluir este projeto budgets: actions: + confirm_delete_budget: Tem certeza de que deseja excluir este orçamento? + confirm_delete_project: Tem certeza que deseja excluir este projeto? + deleted_budgets_info: Orçamentos excluídos podem ser restaurados da lixeira. edit: Editar + edit_projects: Adicionar projetos import: Importar propostas para os projetos new_budget: Novo orçamento new_project: Novo projeto preview: Pré-visualização send_voting_reminders: Enviar lembretes de votação title: Ações + view_deleted_budgets: Ver orçamentos excluídos + view_deleted_projects: Ver projetos excluídos admin: budgets: create: @@ -55,6 +72,8 @@ pt-BR: title: Orçamentos users_with_finished_orders: Usuários com votos finalizados users_with_pending_orders: Usuários com votos pendentes + manage_trash: + title: Orçamentos excluídos new: create: Criar orçamento title: Novo orçamento @@ -78,6 +97,7 @@ pt-BR: cancel: Cancelar change_budget: Alterar orçamento change_selected: Alterar estado selecionado + change_taxonomies: Alterar taxonomias deselect_implementation: Desselecionar da implementação finished_orders: Votos finais pending_orders: Votações pendentes @@ -89,6 +109,8 @@ pt-BR: title: Projetos update: Atualizar update_budget_button: Atualizar orçamento do projeto + manage_trash: + title: Projetos excluídos new: create: Criar title: Novo projeto @@ -96,10 +118,23 @@ pt-BR: invalid: Houve um problema ao atualizar este projeto. success: Projeto atualizado com sucesso. update_budget: + invalid: 'Esses projetos já estão no mesmo orçamento ou seus orçamentos são mais que o máximo permitido: %{errored}.' select_a_project: Por favor, selecione um projeto. + success: 'Projetos atualizados com sucesso para o orçamento %{subject_name}: %{successful}.' update_selected: + invalid: + selected: 'Estes projetos já foram selecionados para a implementação: %{errored}.' + unselected: 'Estes projetos já foram desselecionados da implementação: %{errored}.' select_a_project: Por favor, selecione um projeto. select_a_selection: Por favor, selecione um estado de implementação. + success: + selected: 'Estes projetos foram selecionados com sucesso para a implementação: %{successful}.' + unselected: 'Estes projetos foram desselecionados da implementação com sucesso: %{successful}.' + update_taxonomies: + invalid: 'Taxonomias %{taxonomies} já estão atribuídas a esses projetos: %{errored}.' + select_a_project: Por favor, selecione um projeto. + select_a_taxonomy: Por favor, selecione uma taxonomia. + success: 'Projetos atualizados com sucesso para as taxonomias %{taxonomies}: %{successful}.' proposals_imports: create: invalid: Houve um problema ao importar as propostas para projetos. @@ -107,9 +142,10 @@ pt-BR: new: create: Importar propostas para projetos default_budget: Orçamento padrão - import_all_accepted_proposals: Importar todas as propostas aceitas no_components: Não há outros componentes da proposta neste espaço participativo para importar as propostas para projetos. origin_component_id: Componente de origem + proposal_states: Estados da proposta + proposal_states_help: Selecione um ou mais estados da proposta para importar para os projetos. Selecionando nenhuma, você importará todas as propostas publicadas que não foram já importadas para outros orçamentos deste componente. select_component: Selecione um componente title: Importar propostas para os projetos reminders: @@ -122,10 +158,14 @@ pt-BR: budget: create: "%{user_name} criou o orçamento %{resource_name} no espaço %{space_name}" delete: "%{user_name} excluiu o orçamento %{resource_name} no espaço %{space_name}" + restore: "%{user_name} restaurou o orçamento %{resource_name} em %{space_name}" + soft_delete: "%{user_name} moveu o orçamento %{resource_name} em %{space_name} para a lixeira" update: "%{user_name} atualizou o orçamento %{resource_name} no espaço %{space_name}" project: create: "%{user_name} criou o projeto %{resource_name} no espaço %{space_name}" delete: "%{user_name} excluiu o projeto %{resource_name} no espaço %{space_name}" + restore: "%{user_name} restaurou o projeto %{resource_name} em %{space_name}" + soft_delete: "%{user_name} moveu o projeto %{resource_name} em %{space_name} para a lixeira" update: "%{user_name} atualizou o projeto %{resource_name} no espaço %{space_name}" budget_information_modal: back_to: Voltar para %{component_name} @@ -137,6 +177,7 @@ pt-BR: cancel_order: more_than_one: exclua seu voto em %{name} e comece de novo only_one: exclua seu voto e comece novamente. + completed: Concluído count: one: "Orçamento em %{count}" other: "%{count} Orçamentos" @@ -144,6 +185,7 @@ pt-BR: finished_message: Você terminou o processo de votação. Obrigado por participar! highlighted_cta: Votar em %{name} if_change_opinion: Se você mudou de ideia, você pode + incomplete: Incompleto orders: highest_cost: Custo mais alto label: Ordenar orçamentos por @@ -151,10 +193,14 @@ pt-BR: random: Ordem aleatória progress: Terminar votação remove_vote: Remover voto + see_projects: Ver projetos + see_results: Ver resultados show: Ver projetos vote: Voto voted_budgets: Orçamentos votados voted_on: Você votou em %{links}. + creation: + text: Eles foram adicionados a este orçamento last_activity: new_vote_at: Nova votação sobre orçamentação em limit_announcement: @@ -169,11 +215,26 @@ pt-BR: project: fields: map: Mapa + taxonomies: Taxonomias title: Título order: status: + continue_voting: Continuar votando + download_vote: Baixe seu voto + pending_to_vote_budgets: + one: Você pode votar em outro orçamento + other: Você pode votar em outros orçamentos + share_text: "Acabei de votar nos meus projetos favoritos em %{space_name}! 🎉 Vote agora: %{component_url}" + share_vote: Compartilhe seu voto title: Seu voto foi aceito com sucesso + view_votes: + one: Visualizar voto + other: Ver votos + votes_count: + one: Seu voto para %{budget_name} já foi registrado. + other: Seus votos %{count} para %{budget_name} já foram registrados. order_pdf: + text: Você votou em %{space_name}, para os seguintes projetos title: Seu voto foi aceito com sucesso. order_summary_mailer: order_summary: @@ -181,6 +242,7 @@ pt-BR: subject: Você votou no espaço participativo %{space_name} voted_on_space: Você votou no orçamento %{budget_name} para o espaço participativo %{space_name}. projects: + back_to_budgets: Voltar aos orçamentos budget_confirm: are_you_sure: Se mudar de ideia, você pode mudar de voto mais tarde. cancel: Cancelar @@ -198,6 +260,7 @@ pt-BR: title: Máximo de projetos excedido budget_summary: are_you_sure: Deseja mesmo cancelar o seu voto? + cancel_order: Exclua seu voto checked_out: description: Você já votou no orçamento. Se você mudou de ideia, você pode apagar seu voto %{cancel_link}. title: Votação do orçamento concluída @@ -225,6 +288,14 @@ pt-BR: dynamic_help: minimum_reached: Você atingiu o mínimo para poder votar minimum: Mínimo + minimum_projects_rule: + description: "Selecione pelo menos %{minimum_number} projetos você quer e vote de acordo com suas preferências." + projects_rule: + description: "Selecione no mínimo %{minimum_number} e no máximo %{maximum_number} projetos que você deseja e vote de acordo com suas preferências.." + projects_rule_maximum_only: + description: "Selecione até %{maximum_number} projetos você quer e vote de acordo com suas preferências." + vote_threshold_percent_rule: + description: "Atribua pelo menos %{minimum_budget} aos projetos que você deseja e vote de acordo com suas preferências." orders: highest_cost: Custo mais alto label: Ordenar projetos por @@ -249,8 +320,10 @@ pt-BR: added: Adicionado all: Todos projects_for: Projetos para %{name} + select_projects: Adicionar projetos show: budget: Orçamento + votes: Votos prompt: Selecione o orçamento vote_reminder_mailer: vote_reminder: @@ -266,12 +339,15 @@ pt-BR: actions: comment: Comentar vote: Voto + vote_comment: Vote no comentário name: Orçamentos settings: global: announcement: Anúncio + clear_all: Limpar tudo comments_enabled: Comentários ativados comments_max_length: Tamanho máximo de comentários (deixe 0 para o valor padrão) + define_taxonomy_filters: Por favor, defina alguns filtros para este espaço participativo antes de usar esta configuração. form: errors: budget_voting_rule_only_one: Apenas uma regra de votação deve estar habilitada. @@ -279,8 +355,11 @@ pt-BR: geocoding_enabled: Mapas ativados landing_page_content: Página inicial dos orçamentos more_information_modal: Modal de mais informação + no_taxonomy_filters_found: Nenhum filtro de taxonomia encontrado. projects_per_page: Projetos por página resources_permissions_enabled: Permissões de ações podem ser definidas para cada projeto + taxonomy_filters: Selecione filtros para o componente + taxonomy_filters_add: Adicionar filtro title: Título total_budget: Orçamento total vote_minimum_budget_projects_number: Número mínimo de projetos para votar @@ -290,6 +369,11 @@ pt-BR: vote_selected_projects_maximum: Quantidade máxima de projetos a serem selecionados vote_selected_projects_minimum: Quantidade mínima de projetos a serem selecionados vote_threshold_percent: Porcentagem do limiar de voto + voting_rule: Regra de votação + voting_rule_choices: + minimum_projects: Número mínimo de projetos para votação + selected_projects: Número mínimo e máximo de projetos a serem votados + threshold_percent: Porcentagem de orçamento mínimo workflow: Fluxo de Trabalho workflow_choices: all: 'Votar em todos: permite que os participantes votem em todos os orçamentos.' @@ -308,6 +392,43 @@ pt-BR: disabled: Votação desativada enabled: Votação habilitada finished: Votação finalizada + download_your_data: + help: + orders: + budget: O orçamento com o qual a ordem está relacionada + checked_out_at: A data e hora em que o pedido foi verificado + component: O componente com o qual a ordem está relacionada + created_at: A data e hora em que o pedido foi criado + id: O identificador exclusivo do pedido + projects: Os projetos em que a ordem foi votada + updated_at: A data e hora em que o pedido foi atualizado + show: + projects: Exportação de projetos + open_data: + help: + projects: + address: O endereço do projeto (se houver) + budget: Dados relativos ao orçamento (por exemplo, "2021 orçamento") do projeto + budget_amount: O montante total do orçamento para este projeto + comments: O número de comentários que este projeto recebeu + component: O componente ao qual o projeto pertence + confirmed_votes: O número de votos confirmados que este projeto recebeu + created_at: Data e hora em que o projeto foi criado + description: A descrição do projeto + follows_count: O número de seguir o projeto tem + id: O identificador exclusivo do projeto + latitude: A latitude do projeto, caso tenha uma localização física + longitude: A longitude do projeto, caso ele tenha um local físico + participatory_space: A que espaço (por exemplo, Processo participativo, ou montagem) este projeto pertence + reference: A referência exclusiva do projeto + related_proposal_titles: Os títulos das propostas relacionadas + related_proposal_urls: As URLs das propostas relacionadas + related_proposals: As propostas relacionadas a este projeto + selected_at: O momento em que o projeto foi selecionado + taxonomies: As taxonomias do projeto + title: O título do projeto + updated_at: A última data em que o projeto foi atualizado + url: A URL do projeto orders: checkout: error: Ocorreu um erro ao processar seu voto. @@ -319,5 +440,7 @@ pt-BR: project_proposal: Propostas incluídas neste projeto statistics: orders_count: Suportes + projects_count: Orçamentos + projects_count_tooltip: O número de projectos de orçamentação participativa e o total de votos expressos sobre eles. index: confirmed_orders_count: Contagem de votos diff --git a/decidim-budgets/config/locales/ro-RO.yml b/decidim-budgets/config/locales/ro-RO.yml index bdc78da938abc..87a25adfd788c 100644 --- a/decidim-budgets/config/locales/ro-RO.yml +++ b/decidim-budgets/config/locales/ro-RO.yml @@ -77,6 +77,8 @@ ro: new: create: Importă propuneri în proiecte no_components: Nu există alte componente de propuneri în acest spațiu participativ pentru a importa propunerile în proiecte. + proposal_states: Stări ale propunerii + proposal_states_help: Selectați una sau mai multe stări de propunere pentru a importa în proiecte. Prin neselectarea vreuneia, veți importa toate propunerile publicate care nu au fost deja importate în alte bugete din această componentă. select_component: Te rugăm selectează o componentă admin_log: budget: diff --git a/decidim-budgets/config/locales/sv.yml b/decidim-budgets/config/locales/sv.yml index b769923634516..2fe190c1bd81a 100644 --- a/decidim-budgets/config/locales/sv.yml +++ b/decidim-budgets/config/locales/sv.yml @@ -142,7 +142,6 @@ sv: new: create: Importera förslag till projekt default_budget: Förvald budget - import_all_accepted_proposals: Importera alla godkända förslag no_components: Det finns inga andra förslagskomponenter med förslag i deltagarutrymmet som kan importeras till projekt. origin_component_id: Ursprungskomponent select_component: Välj en komponent diff --git a/decidim-budgets/config/locales/zh-TW.yml b/decidim-budgets/config/locales/zh-TW.yml index 6f0ed34f4df41..87214182a0f52 100644 --- a/decidim-budgets/config/locales/zh-TW.yml +++ b/decidim-budgets/config/locales/zh-TW.yml @@ -94,7 +94,6 @@ zh-TW: new: create: 將提案匯入專案 default_budget: 預設預算 - import_all_accepted_proposals: 導入所有已接受的提案 no_components: 在此參與空間中沒有其他提案元件可以將提案匯入到專案中。 origin_component_id: 原始元件 select_component: 請選擇一個組件 diff --git a/decidim-budgets/lib/decidim/api/budget_type.rb b/decidim-budgets/lib/decidim/api/budget_type.rb index 0e08730102dcd..1f3de24edeb81 100644 --- a/decidim-budgets/lib/decidim/api/budget_type.rb +++ b/decidim-budgets/lib/decidim/api/budget_type.rb @@ -23,8 +23,6 @@ def url def self.authorized?(object, context) super && object.visible? - rescue Decidim::PermissionAction::PermissionNotSetError - false end end end diff --git a/decidim-budgets/lib/decidim/api/budgets_type.rb b/decidim-budgets/lib/decidim/api/budgets_type.rb index f2e4ff44503dc..f54ed84eaf134 100644 --- a/decidim-budgets/lib/decidim/api/budgets_type.rb +++ b/decidim-budgets/lib/decidim/api/budgets_type.rb @@ -15,8 +15,8 @@ def budgets Budget.where(component: object).includes(:component) end - def budget(**args) - Budget.where(component: object).find_by(id: args[:id]) + def budget(id:) + Decidim::Core::ComponentFinderBase.new(model_class: Budget).call(object, { id: }, context) end end end diff --git a/decidim-budgets/lib/decidim/api/project_type.rb b/decidim-budgets/lib/decidim/api/project_type.rb index f26390c3a70e3..f2df243583d8e 100644 --- a/decidim-budgets/lib/decidim/api/project_type.rb +++ b/decidim-budgets/lib/decidim/api/project_type.rb @@ -49,13 +49,11 @@ def self.authorized?(object, context) context[:project] = object chain = [ - allowed_to?(:read, :project, object, context), - object.visible? + object.visible?, + allowed_to?(:read, :project, object, context) ].all? super && chain - rescue Decidim::PermissionAction::PermissionNotSetError - false end end end diff --git a/decidim-budgets/lib/decidim/budgets/component.rb b/decidim-budgets/lib/decidim/budgets/component.rb index 597b7971ab720..fc77f38657f92 100644 --- a/decidim-budgets/lib/decidim/budgets/component.rb +++ b/decidim-budgets/lib/decidim/budgets/component.rb @@ -17,10 +17,6 @@ component.actions = %w(vote comment vote_comment) - component.on(:before_destroy) do |instance| - raise StandardError, "Cannot remove this component" if Decidim::Budgets::Budget.where(component: instance).any? - end - component.on(:publish) do |instance| Decidim::Budgets::Budget.where(component: instance).find_each do |budget| Decidim::UpdateSearchIndexesJob.perform_later([budget]) diff --git a/decidim-budgets/spec/commands/decidim/budgets/admin/import_proposals_to_budgets_spec.rb b/decidim-budgets/spec/commands/decidim/budgets/admin/import_proposals_to_budgets_spec.rb index 082187f80b6c2..dff91c0a0bab9 100644 --- a/decidim-budgets/spec/commands/decidim/budgets/admin/import_proposals_to_budgets_spec.rb +++ b/decidim-budgets/spec/commands/decidim/budgets/admin/import_proposals_to_budgets_spec.rb @@ -29,14 +29,14 @@ module Admin current_component:, current_user:, default_budget:, - import_all_accepted_proposals:, + internal_states:, budget:, valid?: valid ) end let(:default_budget) { 1000 } - let(:import_all_accepted_proposals) { true } + let(:internal_states) { ["accepted"] } let(:command) { described_class.new(form) } @@ -65,6 +65,29 @@ module Admin expect { command.call }.to change { Project.where(budget:).count }.by(3) end + context "when importing multiple states" do + let!(:rejected_proposals) { create_list(:proposal, 2, :rejected, component: proposals_component) } + let(:internal_states) { %w(accepted rejected) } + + it "imports proposals from all selected states" do + expect { command.call }.to change { Project.where(budget:).count }.by(5) + end + end + + context "when importing custom states" do + let!(:custom_state) { create(:proposal_state, token: "custom_state", component: proposals_component) } + let!(:custom_state_proposals) do + create_list(:proposal, 2, :published, component: proposals_component).each do |proposal| + proposal.update!(proposal_state: custom_state) + end + end + let(:internal_states) { ["custom_state"] } + + it "imports proposals with custom states" do + expect { command.call }.to change { Project.where(budget:).count }.by(2) + end + end + context "when a proposal was already imported" do let(:second_proposal) { create(:proposal, :accepted, component: proposal.component) } diff --git a/decidim-budgets/spec/forms/decidim/budgets/admin/project_import_proposals_form_spec.rb b/decidim-budgets/spec/forms/decidim/budgets/admin/project_import_proposals_form_spec.rb index 1c67d6c040758..5864d702866e3 100644 --- a/decidim-budgets/spec/forms/decidim/budgets/admin/project_import_proposals_form_spec.rb +++ b/decidim-budgets/spec/forms/decidim/budgets/admin/project_import_proposals_form_spec.rb @@ -12,12 +12,12 @@ module Admin let(:component) { project.component } let(:origin_component) { create(:proposal_component, participatory_space: component.participatory_space) } let(:default_budget) { 1000 } - let(:import_all_accepted_proposals) { true } + let(:internal_states) { %w(accepted rejected) } let(:params) do { origin_component_id: origin_component.try(:id), default_budget:, - import_all_accepted_proposals: + internal_states: } end @@ -44,10 +44,10 @@ module Admin it { is_expected.to be_invalid } end - context "when the import proposals is not accepted" do - let(:import_all_accepted_proposals) { false } + context "when no states are selected" do + let(:internal_states) { [] } - it { is_expected.to be_invalid } + it { is_expected.to be_valid } end describe "origin_component" do diff --git a/decidim-budgets/spec/shared/import_proposals_to_projects_examples.rb b/decidim-budgets/spec/shared/import_proposals_to_projects_examples.rb index 7c179f8c35530..0a4a6404549cc 100644 --- a/decidim-budgets/spec/shared/import_proposals_to_projects_examples.rb +++ b/decidim-budgets/spec/shared/import_proposals_to_projects_examples.rb @@ -14,7 +14,7 @@ within ".import_proposals" do select origin_component.name["en"], from: :proposals_import_origin_component_id fill_in "Default budget", with: default_budget - check :proposals_import_import_all_accepted_proposals + check "Accepted" || "Rejected" end click_on "Import proposals to projects" diff --git a/decidim-budgets/spec/system/admin_manages_budgets_spec.rb b/decidim-budgets/spec/system/admin_manages_budgets_spec.rb index ffb8c68bbea44..12a280ca45d14 100644 --- a/decidim-budgets/spec/system/admin_manages_budgets_spec.rb +++ b/decidim-budgets/spec/system/admin_manages_budgets_spec.rb @@ -175,4 +175,30 @@ it_behaves_like "manage soft deletable resource", "budget" it_behaves_like "manage trashed resource", "budget" end + + describe "more information button" do + context "when budget has more_information content" do + let!(:budget_with_info) { create(:budget, :with_projects, component: current_component) } + + before do + current_component.update!(settings: { more_information_modal: { en: "Additional budget information" } }) + end + + it "displays the more information button" do + visit Decidim::EngineRouter.main_proxy(current_component).budget_projects_path(budget_with_info) + + expect(page).to have_button("More information") + end + end + + context "when budget has no more_information content" do + let!(:budget_without_info) { create(:budget, :with_projects, component: current_component) } + + it "does not display the more information button" do + visit Decidim::EngineRouter.main_proxy(current_component).budget_projects_path(budget_without_info) + + expect(page).to have_no_button("More information") + end + end + end end diff --git a/decidim-budgets/spec/system/budgets_breadcrumbs_spec.rb b/decidim-budgets/spec/system/budgets_breadcrumbs_spec.rb new file mode 100644 index 0000000000000..a491d85ed8048 --- /dev/null +++ b/decidim-budgets/spec/system/budgets_breadcrumbs_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Budgets Breadcrumb" do + let(:organization) { create(:organization) } + let(:participatory_space) { create(:participatory_process, :with_steps, :published, organization:, title: { "en" => "Participatory space" }) } + let(:component) { create(:budgets_component, :published, :with_votes_disabled, participatory_space:, name: { "en" => "Component" }) } + let(:budget) { create(:budget, component:, title: { "en" => "Budget" }) } + let!(:project) { create(:project, budget:, title: { "en" => "Project" }) } + let(:router) { Decidim::EngineRouter.main_proxy(component) } + + before do + switch_to_host(organization.host) + end + + context "when visiting the budgets index page" do + it "shows the correct information in breadcrumb (space, component)" do + visit router.root_path(locale: I18n.locale) + + within ".menu-bar" do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + end + end + end + + context "when visiting single budget page" do + it "shows the correct information in breadcrumb (space, component, budget)" do + visit router.budget_path(budget, locale: I18n.locale) + + within ".menu-bar" do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + expect(page).to have_content(translated(budget.title)) + end + end + end + + context "when visiting single project page" do + it "shows the correct information in breadcrumb (space, component, budget, project)" do + visit router.budget_project_path(budget, project, locale: I18n.locale) + + within ".menu-bar" do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + expect(page).to have_content(translated(budget.title)) + expect(page).to have_content(translated(project.title)) + end + end + end +end diff --git a/decidim-budgets/spec/system/explore_projects_spec.rb b/decidim-budgets/spec/system/explore_projects_spec.rb index 7fa16548097fb..044ecd1564a71 100644 --- a/decidim-budgets/spec/system/explore_projects_spec.rb +++ b/decidim-budgets/spec/system/explore_projects_spec.rb @@ -110,7 +110,7 @@ [-142.15275006889419, 33.33377235135252], [-55.28745034772282, -35.587843900166945] ] - Decidim::Budgets::Project.where(budget: budget).geocoded.each_with_index do |project, index| + Decidim::Budgets::Project.where(budget:).geocoded.each_with_index do |project, index| project.update!(latitude: coordinates[index][0], longitude: coordinates[index][1]) if coordinates[index] end diff --git a/decidim-budgets/spec/types/budget_type_spec.rb b/decidim-budgets/spec/types/budget_type_spec.rb index 40b169938a459..006f869243dfc 100644 --- a/decidim-budgets/spec/types/budget_type_spec.rb +++ b/decidim-budgets/spec/types/budget_type_spec.rb @@ -10,7 +10,7 @@ module Budgets let(:model) { create(:budget, :with_projects) } it_behaves_like "traceable interface" do - let(:author) { create(:user, :admin, organization: model.component.organization) } + let(:author) { create(:user, :admin, :confirmed, organization: model.component.organization) } end describe "id" do diff --git a/decidim-budgets/spec/types/budgets_type_spec.rb b/decidim-budgets/spec/types/budgets_type_spec.rb index 46b62664470c9..1aba5e98f8454 100644 --- a/decidim-budgets/spec/types/budgets_type_spec.rb +++ b/decidim-budgets/spec/types/budgets_type_spec.rb @@ -40,8 +40,8 @@ module Budgets context "when the budget does not belong to the component" do let!(:budget) { create(:budget, component: create(:budgets_component)) } - it "returns null" do - expect(response["budget"]).to be_nil + it "raises error" do + expect { response }.to raise_error(Decidim::Api::Errors::NotFoundError, "Budget not found") end end end diff --git a/decidim-budgets/spec/types/integration_schema_spec.rb b/decidim-budgets/spec/types/integration_schema_spec.rb index d164389d7087d..1399a58d16993 100644 --- a/decidim-budgets/spec/types/integration_schema_spec.rb +++ b/decidim-budgets/spec/types/integration_schema_spec.rb @@ -123,6 +123,12 @@ } end + shared_examples "unauthorized Budget" do + it "throws Decidim::Api::Errors::UnauthorizedObjectError" do + expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedObjectError, "You cannot view or edit this Budget because you do not have permissions") + end + end + describe "commentable" do let(:component_fragment) { nil } @@ -275,8 +281,33 @@ context "when user is visitor" do let!(:current_user) { nil } + let(:component_fragment) do + %( + fragment fooComponent on Budgets { + budget(id: #{budget.id}) { + createdAt + description { + translation(locale:"#{locale}") + } + id + title { + translation(locale:"#{locale}") + } + total_budget + updatedAt + url + versions { + id + } + versionsCount + weight + } + } + ) + end + it "should be visible" do - expect(response["participatoryProcess"]["components"].first[lookout_key]).to eq(query_result.merge("projects" => [nil, nil])) + expect(response["participatoryProcess"]["components"].first[lookout_key]).to eq(query_result.except("projects")) end end @@ -295,16 +326,14 @@ context "when the user is admin" do let!(:current_user) { create(:user, :admin, :confirmed, organization: current_organization) } - it "should not be visible" do - expect(response["participatoryProcess"]["components"].first[lookout_key]).to be_nil - end + it_behaves_like "unauthorized Budget" end context "when user is visitor" do let!(:current_user) { nil } it "should not be visible" do - expect(response["participatoryProcess"]["components"].first).to be_nil + expect(response["participatoryProcess"]["components"]).to be_empty end end @@ -312,7 +341,7 @@ let!(:current_user) { create(:user, :confirmed, organization: current_organization) } it "should not be visible" do - expect(response["participatoryProcess"]["components"].first).to be_nil + expect(response["participatoryProcess"]["components"]).to be_empty end end end @@ -327,25 +356,19 @@ context "when the user is admin" do let!(:current_user) { create(:user, :admin, :confirmed, organization: current_organization) } - it "should not be visible" do - expect(response["participatoryProcess"]["components"].first[lookout_key]).to be_nil - end + it_behaves_like "unauthorized Budget" end context "when user is visitor" do let!(:current_user) { nil } - it "should not be visible" do - expect(response["participatoryProcess"]).to be_nil - end + it_behaves_like "graphQL not found space" end context "when user is normal user" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } - it "should not be visible" do - expect(response["participatoryProcess"]).to be_nil - end + it_behaves_like "graphQL not found space" end end @@ -355,25 +378,19 @@ context "when the user is admin" do let!(:current_user) { create(:user, :admin, :confirmed, organization: current_organization) } - it "should not be visible" do - expect(response["participatoryProcess"]["components"].first[lookout_key]).to be_nil - end + it_behaves_like "unauthorized Budget" end context "when user is visitor" do let!(:current_user) { nil } - it "should not be visible" do - expect(response["participatoryProcess"]).to be_nil - end + it_behaves_like "graphQL not found space" end context "when user is normal user" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } - it "should not be visible" do - expect(response["participatoryProcess"]).to be_nil - end + it_behaves_like "graphQL not found space" end end end @@ -421,18 +438,62 @@ end end end - context "when user is visitor" do let!(:current_user) { nil } + let(:component_fragment) do + %( + fragment fooComponent on Budgets { + budget(id: #{budget.id}) { + createdAt + description { + translation(locale:"#{locale}") + } + id + title { + translation(locale:"#{locale}") + } + total_budget + updatedAt + url + versions { + id + } + versionsCount + weight + } + } + ) + end + it "is visible" do - expect(response["assembly"]["components"].first[lookout_key]).to eq(query_result.merge("projects" => [nil, nil])) + expect(response["assembly"]["components"].first[lookout_key]).to eq(query_result.except("projects")) + end + end + + context "when user is visitor and requests projects that is not supposed to see" do + let!(:current_user) { nil } + + let(:component_fragment) do + %( + fragment fooComponent on Budgets { + budget(id: #{budget.id}) { + id + projects { + id + } + } + }) + end + + it "throws Decidim::Api::Errors::UnauthorizedObjectError" do + expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedObjectError, "You cannot view or edit this Project because you do not have permissions") end end 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, participatory_space: participatory_process) } it "is visible" do expect(response["assembly"]["components"].first[lookout_key]).to eq(query_result) @@ -454,9 +515,7 @@ context "when the user is admin" do let!(:current_user) { create(:user, :admin, :confirmed, organization: current_organization) } - it "is visible" do - expect(response["assembly"]["components"].first[lookout_key]).to be_nil - end + it_behaves_like "unauthorized Budget" end %w(admin collaborator evaluator).each do |role| @@ -465,16 +524,17 @@ let!(:role) { create(:assembly_user_role, assembly: participatory_process, user: current_user, role:) } it "is visible" do - expect(response["assembly"]["components"].first[lookout_key]).to be_nil + expect(response["assembly"]["components"]).to be_empty end end end + context "when the user is space moderator" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } let!(:role) { create(:assembly_user_role, assembly: participatory_process, user: current_user, role: "moderator") } it "is visible" do - expect(response["assembly"]["components"].first).to be_nil + expect(response["assembly"]["components"]).to be_empty end end @@ -482,15 +542,15 @@ let!(:current_user) { nil } it "should not be visible" do - expect(response["assembly"]["components"].first).to be_nil + expect(response["assembly"]["components"]).to be_empty end context "when user is member" do 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, participatory_space: participatory_process) } it "should not be visible" do - expect(response["assembly"]["components"].first).to be_nil + expect(response["assembly"]["components"]).to be_empty end end end @@ -499,7 +559,7 @@ let!(:current_user) { create(:user, :confirmed, organization: current_organization) } it "should not be visible" do - expect(response["assembly"]["components"].first).to be_nil + expect(response["assembly"]["components"]).to be_empty end end end @@ -514,25 +574,19 @@ context "when the user is admin" do let!(:current_user) { create(:user, :admin, :confirmed, organization: current_organization) } - it "should not be visible" do - expect(response["participatoryProcess"]["components"].first[lookout_key]).to be_nil - end + it_behaves_like "unauthorized Budget" end context "when user is visitor" do let!(:current_user) { nil } - it "should not be visible" do - expect(response["participatoryProcess"]).to be_nil - end + it_behaves_like "graphQL not found space" end context "when user is normal user" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } - it "should not be visible" do - expect(response["participatoryProcess"]).to be_nil - end + it_behaves_like "graphQL not found space" end end @@ -542,25 +596,19 @@ context "when the user is admin" do let!(:current_user) { create(:user, :admin, :confirmed, organization: current_organization) } - it "should not be visible" do - expect(response["participatoryProcess"]["components"].first[lookout_key]).to be_nil - end + it_behaves_like "unauthorized Budget" end context "when user is visitor" do let!(:current_user) { nil } - it "should not be visible" do - expect(response["participatoryProcess"]).to be_nil - end + it_behaves_like "graphQL not found space" end context "when user is normal user" do let!(:current_user) { create(:user, :confirmed, organization: current_organization) } - it "should not be visible" do - expect(response["participatoryProcess"]).to be_nil - end + it_behaves_like "graphQL not found space" end end end diff --git a/decidim-budgets/spec/types/project_type_spec.rb b/decidim-budgets/spec/types/project_type_spec.rb index 94448418103db..cd19a363da937 100644 --- a/decidim-budgets/spec/types/project_type_spec.rb +++ b/decidim-budgets/spec/types/project_type_spec.rb @@ -21,6 +21,12 @@ module Budgets include_examples "referable interface" include_examples "followable interface" + shared_examples "unauthorized Project" do + it "throws Decidim::Api::Errors::UnauthorizedObjectError" do + expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedObjectError, "You cannot view or edit this Project because you do not have permissions") + end + end + describe "id" do let(:query) { "{ id }" } @@ -155,9 +161,7 @@ module Budgets let(:model) { create(:project, budget:) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Project" end context "when participatory space is not published" do @@ -167,9 +171,7 @@ module Budgets let(:model) { create(:project, budget:) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Project" end context "when component is not published" do @@ -177,9 +179,7 @@ module Budgets let(:model) { create(:project, component:) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Project" end context "when budget is not visible" do @@ -189,10 +189,11 @@ module Budgets let(:query) { "{ id }" } let(:root_value) { model.reload } - it "returns all the required fields" do + before do allow(model).to receive(:visible?).and_return(false) - expect(response).to be_nil end + + it_behaves_like "unauthorized Project" end end end diff --git a/decidim-collaborative_texts/config/locales/en.yml b/decidim-collaborative_texts/config/locales/en.yml index 58991fd8369c3..b74f98097e0dd 100644 --- a/decidim-collaborative_texts/config/locales/en.yml +++ b/decidim-collaborative_texts/config/locales/en.yml @@ -16,6 +16,11 @@ en: body: Body draft: Draft version_number: Version number + activerecord: + models: + decidim/collaborative_texts/document: + one: Collaborative text + other: Collaborative texts decidim: admin: tooltips: diff --git a/decidim-collaborative_texts/config/locales/eu.yml b/decidim-collaborative_texts/config/locales/eu.yml index c645ac4c56c73..e51d5676463dc 100644 --- a/decidim-collaborative_texts/config/locales/eu.yml +++ b/decidim-collaborative_texts/config/locales/eu.yml @@ -87,7 +87,7 @@ eu: unpublish: "%{user_name} parte-hartzaileak %{resource_name} testu kolaboratiboa argitaratzeari utzi dio %{space_name} espazioan" update: "%{user_name} parte-hartzaileak %{resource_name} testu kolaboratiboa aldatu du %{space_name} espazioan" suggestion: - create: "%{user_name} parte-hartzaileak %{resource_name} testu kolaboratiboan aldaketak iradoki ditu %{space_name} partaidetza espazioan" + create: "%{user_name} parte-hartzaileak %{resource_name} testu kolaboratiboan aldaketak iradoki ditu %{space_name} partaidetza-espazioan" version: delete: "%{user_name} parte-hartzaileak %{resource_name} testu kolaboratiboaren bertsioa ezabatu du %{space_name} espazioan" update: "%{user_name} parte-hartzaileak %{resource_name} testu kolaboratiboaren bertsioa aldatu du %{space_name} espazioan" diff --git a/decidim-collaborative_texts/config/locales/pt-BR.yml b/decidim-collaborative_texts/config/locales/pt-BR.yml index 21460cdedbdf8..bcfb6eb3afbb0 100644 --- a/decidim-collaborative_texts/config/locales/pt-BR.yml +++ b/decidim-collaborative_texts/config/locales/pt-BR.yml @@ -1 +1,158 @@ +--- pt-BR: + activemodel: + attributes: + collaborative_texts: + document: + body: Corpo + draft: Rascunho + title: Título + version_number: Número da versão + suggestion: + nodes: DOM nodes + original: Texto original + replace: Substituição + version: + body: Corpo + draft: Rascunho + version_number: Número da versão + decidim: + admin: + tooltips: + deleted_collaborative_texts_info: Não é possível excluir este documento + collaborative_texts: + actions: + confirm_delete_document: Tem certeza de que deseja excluir este documento? + deleted_document_info: Documento excluído pode ser restaurado da lixeira. + edit: Editar + manage: Configurar + new: Novo texto + title: Ações + view_deleted_documents: Visualizar documentos excluídos + admin: + documents: + create: + invalid: Houve um problema ao criar o documento. + success: Documento criado com sucesso. + draft_options: + accepting_suggestions: Habilitar sugestões na próxima versão + accepting_suggestions_help: Habilitar as sugestões iniciarão uma nova rodada de sugestões para este documento. As sugestões atuais não ficarão visíveis na nova versão. + draft: Versão do rascunho + draft_help_html: Uma versão rascunho não é pública e pode ser editada pelos administradores. Remova o status de rascunho para tornar este conteúdo público. Observe que, enquanto a versão rascunho estiver ativa, os participantes verão a versão anterior e não poderão fazer sugestões. + edit: + document_has_suggestions_html: Este documento contém sugestões e não pode ser editado diretamente. Por favor, aceite ou rejeite as sugestões para gerar uma nova versão de rascunho que possa ser editada. + draft: Versão do rascunho + previous_versions: Versões anteriores + public_version: Versão pública + title: Editar textos colaborativos + update: Atualização + version: Versão %{version_number}, criada em %{created_at} recebeu sugestões %{suggestions_count}. + edit_settings: + title: Configurar textos colaborativos + update: Atualizar + index: + title: Textos colaborativos + manage_trash: + title: Documentos excluídos + new: + create: Criar + title: Criar textos colaborativos + non_draft_options: + create_draft: Descartar sugestões e criar uma nova versão de rascunho + draft_help_html: Uma versão rascunho não é pública e pode ser editada pelos administradores. Remova o status de rascunho para tornar este conteúdo público. Observe que, enquanto a versão rascunho estiver ativa, os participantes verão a versão anterior e não poderão fazer sugestões. + enable_suggestions: Ativar sugestões + enable_suggestions_help: Permitir sugestões permitirá que os participantes façam sugestões nesta versão. + publish: + invalid: Ocorreu um erro ao publicar este documento. + success: Documento publicado com sucesso. + unpublish: + invalid: Ocorreu um erro ao cancelar a publicação deste documento. + success: Documento despublicado com sucesso. + update: + invalid: Houve um problema ao atualizar o documento. + success: Documento atualizado com sucesso. + update_settings: + invalid: Houve um problema ao atualizar o documento. + success: Documento atualizado com sucesso. + index: + published: Publicado + unpublished: Despublicado + admin_log: + document: + create: "%{user_name} criou o texto colaborativo %{resource_name} em %{space_name}" + delete: "%{user_name} excluiu o texto colaborativo %{resource_name} do %{space_name}" + publish: "%{user_name} publicou o texto colaborativo %{resource_name} em %{space_name}" + restore: "%{user_name} restaurou o texto colaborativo %{resource_name} em %{space_name}" + soft_delete: "%{user_name} moveu o texto colaborativo %{resource_name} em %{space_name} para a lixeira" + unpublish: "%{user_name} despublicou o texto colaborativo %{resource_name} em %{space_name}" + update: "%{user_name} atualizou o texto colaborativo %{resource_name} em %{space_name}" + suggestion: + create: "%{user_name} sugeriu alterações ao texto colaborativo %{resource_name} em %{space_name}" + version: + delete: "%{user_name} excluiu a versão do texto colaborativo %{resource_name} do %{space_name}" + update: "%{user_name} atualizou a versão do texto colaborativo %{resource_name} em %{space_name}" + document: + apply: Aplicar + cancel: Cancelar + consolidate: + confirm: Esta ação consolidará todas as sugestões aceites em uma nova versão do documento. Sugestões não aceitas serão transferidas para a nova versão para uma nova revisão. Esta ação irá manter as sugestões habilitadas status. + new: Consolidar sugestões aceitas + consolidate_counter: Consolidar irá mesclar as sugestões %{applied} aplicadas e mover as sugestões %{pending} pendentes para a nova versão. + controls_label: Controles de sugestão + draft_counter: O rascunho irá mesclar as sugestões %{applied} aplicadas e descartar as sugestões %{pending} pendentes. + index: Índice + restore: Restaurar + rollout: + confirm: Esta ação criará uma nova versão rascunho do documento e você será redirecionado para a página de edição para refinos finais. A versão atual permanecerá pública e as sugestões serão desativadas. + invalid: Ocorreu um erro ao criar uma nova versão do documento (%{errors}). + new: Elabore uma nova versão + save: Sugerir alterações + status: + accepting_suggestions: Para sugerir alterações, selecione ou clique duas vezes no texto que deseja editar e clique no botão 'Sugerir alterações'. + draft: Este documento está atualmente em revisão pelos administradores. As sugestões não são permitidas neste momento, mas você pode ver as sugestões feitas por outros participantes. + not_accepting_suggestions: Sugestões não são permitidas no momento. + selection_active: Uma seleção está ativa. Cancele a seleção atual para fazer uma nova. + suggestions_count: "Sugestões de %{count}" + toggle: Mostrar/ocultar sugestões + documents: + index: + count: + one: "%{count} texto colaborativo" + other: "%{count} textos colaborativos" + empty: Ainda não há textos colaborativos. + show: + contributors: Colaboradores + models: + collaborative_text: + fields: + published: Publicados + suggestions: Aceitar sugestões + title: Título + suggestion: + add_html: Adicionar: %{text} + remove_html: Excluir: %{text} + replace_html: Substituir: %{text} + suggestions: + create: + invalid: Houve um problema ao criar a sugestão. + success: Sugestão criada com sucesso. + errors: + blank_changeset: O conjunto de alterações não pode ficar em branco. + invalid_nodes: Nós selecionados inválidos. + components: + collaborative_texts: + actions: + create: Criar + destroy: Destruir + update: Atualizar + name: Textos colaborativos + settings: + global: + announcement: Anúncio + comments_blocked: Comentários bloqueados + step: + announcement: Anúncio + statistics: + all_collaborative_texts_count: Todos os textos colaborativos + collaborative_texts_count: Textos colaborativos + collaborative_texts_count_tooltip: Número de textos colaborativos diff --git a/decidim-collaborative_texts/config/locales/ro-RO.yml b/decidim-collaborative_texts/config/locales/ro-RO.yml index 9a46abf5eb629..52791944bf5b2 100644 --- a/decidim-collaborative_texts/config/locales/ro-RO.yml +++ b/decidim-collaborative_texts/config/locales/ro-RO.yml @@ -5,17 +5,26 @@ ro: collaborative_texts: document: body: Conținut + draft: Schiță title: Titlu version_number: Număr versiune + suggestion: + original: Text original + replace: Înlocuire version: body: Conținut + draft: Ciornă version_number: Număr versiune decidim: + admin: + tooltips: + deleted_collaborative_texts_info: Nu puteți șterge acest document collaborative_texts: actions: confirm_delete_document: Sigur doriți să ștergeți acest document? deleted_document_info: Documentul șters poate fi restaurat din gunoi. edit: Modificați + manage: Configurare new: Text nou title: Acțiuni view_deleted_documents: Vizualizați documentele șterse @@ -24,7 +33,14 @@ ro: create: invalid: A apărut o problemă la crearea documentului. success: Documentul a fost creat cu succes. + draft_options: + accepting_suggestions: Activați sugestiile în versiunea următoare + accepting_suggestions_help: Activarea sugestiilor va începe o nouă rundă de sugestii pentru acest document. Sugestiile actuale nu vor fi vizibile în noua versiune. + draft: Versiune ciornă edit: + draft: Versiune ciornă + previous_versions: Versiuni anterioare + public_version: Versiune publică title: Actualizați texte colaborative update: Actualizați edit_settings: @@ -37,6 +53,8 @@ ro: new: create: Creați title: Creați texte colaborative + non_draft_options: + enable_suggestions: Activați sugestiile publish: invalid: A apărut o problemă la publicarea acestui document. success: Documentul a fost publicat cu succes. @@ -49,20 +67,32 @@ ro: update_settings: invalid: A apărut o eroare la actualizarea documentului. success: Document actualizat cu succes. + index: + published: Publicate + unpublished: Nepublicate admin_log: document: - create: "%{user_name} a creat postarea %{resource_name} în %{space_name}" - delete: "%{user_name} a creat postarea %{resource_name} în %{space_name}" - publish: "%{user_name} a creat postarea %{resource_name} în %{space_name}" + create: "%{user_name} a creat textul colaborativ %{resource_name} în %{space_name}" + delete: "%{user_name} a eliminat textul colaborativ %{resource_name} în %{space_name}" + publish: "%{user_name} a publicat textul colaborativ %{resource_name} în %{space_name}" restore: "%{user_name} a restaurat postarea %{resource_name} din %{space_name}" - soft_delete: "%{user_name} a mutat postarea %{resource_name} din %{space_name}" + soft_delete: "%{user_name} a mutat la gunoi textul colaborativ %{resource_name} din %{space_name}" unpublish: "%{user_name} a nepublicat %{resource_name} în %{space_name}" - update: "%{user_name} a actualizat postarea %{resource_name} în %{space_name}" + update: "%{user_name} a actualizat textul colaborativ %{resource_name} în %{space_name}" version: - delete: "%{user_name} a șters versiunea text colaborativă %{resource_name} din %{space_name}" - update: "%{user_name} a actualizat versiunea text colaborativ %{resource_name} în %{space_name}" + delete: "%{user_name} a eliminat versiunea de text colaborativ %{resource_name} din %{space_name}" + update: "%{user_name} a actualizat versiunea de text colaborativ %{resource_name} în %{space_name}" document: - cancel: Anulează + cancel: Anulați + consolidate: + confirm: Această acțiune va consolida toate sugestiile acceptate într-o nouă versiune a documentului. Sugestiile neacceptate vor fi transferate noii versiuni pentru a fi revizuite în continuare. Această acțiune va menține starea sugestiilor activate. + new: Consolidați sugestiile acceptate + controls_label: Controale privind sugestiile + draft_counter: Ciorna va fuziona %{applied} sugestii aplicate și va anula %{pending} sugestii în așteptare. + index: Index + rollout: + new: Creați o nouă versiune ca ciornă + save: Sugerați modificări documents: index: count: diff --git a/decidim-collaborative_texts/lib/decidim/collaborative_texts/engine.rb b/decidim-collaborative_texts/lib/decidim/collaborative_texts/engine.rb index 563e68ebf837a..853be3d5daee0 100644 --- a/decidim-collaborative_texts/lib/decidim/collaborative_texts/engine.rb +++ b/decidim-collaborative_texts/lib/decidim/collaborative_texts/engine.rb @@ -15,6 +15,11 @@ class Engine < ::Rails::Engine get "/", to: redirect("documents", status: 301) end + initializer "decidim_collaborative_texts.register_icons" do + Decidim.icons.register(name: "Decidim::CollaborativeTexts::Document", icon: "draft-line", description: "Collaborative texts", category: "activity", + engine: :collaborative_texts) + end + initializer "decidim_collaborative_texts.data_migrate", after: "decidim_core.data_migrate" do DataMigrate.configure do |config| config.data_migrations_path << root.join("db/data").to_s diff --git a/decidim-collaborative_texts/lib/decidim/collaborative_texts/seeds.rb b/decidim-collaborative_texts/lib/decidim/collaborative_texts/seeds.rb index e0866a2fc4512..be0737377fcd2 100644 --- a/decidim-collaborative_texts/lib/decidim/collaborative_texts/seeds.rb +++ b/decidim-collaborative_texts/lib/decidim/collaborative_texts/seeds.rb @@ -29,7 +29,7 @@ def create_component! name: Decidim::Components::Namer.new(participatory_space.organization.available_locales, :collaborative_texts).i18n_name, manifest_name: :collaborative_texts, published_at: Time.current, - participatory_space: participatory_space + participatory_space: } Decidim.traceability.perform_action!( diff --git a/decidim-collaborative_texts/spec/commands/decidim/collaborative_texts/admin/update_document_spec.rb b/decidim-collaborative_texts/spec/commands/decidim/collaborative_texts/admin/update_document_spec.rb index 3cea1fb8dc5bc..fea4327a4f361 100644 --- a/decidim-collaborative_texts/spec/commands/decidim/collaborative_texts/admin/update_document_spec.rb +++ b/decidim-collaborative_texts/spec/commands/decidim/collaborative_texts/admin/update_document_spec.rb @@ -22,7 +22,7 @@ module Admin title:, body:, draft?: draft, - draft: draft, + draft:, accepting_suggestions:, coauthorships: [Decidim::Coauthorship.new(author: organization)] ) @@ -68,11 +68,11 @@ module Admin .with(last_version, user, updated_keys, { extra: { document_id: document.id, - title: title, + title:, version_number: 1 }, resource: { - title: title + title: }, participatory_space: { title: document.participatory_space.title @@ -116,11 +116,11 @@ module Admin .with(Decidim::CollaborativeTexts::Version, user, { document:, body: document.body, draft: true }, { extra: { document_id: document.id, - title: title, + title:, version_number: 2 }, resource: { - title: title + title: }, participatory_space: { title: document.participatory_space.title diff --git a/decidim-collaborative_texts/spec/controllers/decidim/collaborative_texts/admin/documents_controller_spec.rb b/decidim-collaborative_texts/spec/controllers/decidim/collaborative_texts/admin/documents_controller_spec.rb index b454c80814c34..483d81c2785eb 100644 --- a/decidim-collaborative_texts/spec/controllers/decidim/collaborative_texts/admin/documents_controller_spec.rb +++ b/decidim-collaborative_texts/spec/controllers/decidim/collaborative_texts/admin/documents_controller_spec.rb @@ -15,8 +15,8 @@ module Admin let(:document_versions) { [build(:collaborative_text_version)] } let(:params) do { - title: title, - body: body + title:, + body: } end let(:title) { "A nice test document" } @@ -69,7 +69,7 @@ module Admin describe "POST #create" do it "creates a new document and redirects to index" do expect do - post :create, params: params + post :create, params: end.to change(Document, :count).by(1) expect(response).to redirect_to(documents_path) expect(flash[:notice]).to eq("Document successfully created.") @@ -80,7 +80,7 @@ module Admin it "does not create a document" do expect do - post :create, params: params + post :create, params: end.not_to change(Document, :count) expect(response).to render_template(:new) expect(flash.now[:alert]).to eq("There was a problem creating the document.") @@ -98,7 +98,7 @@ module Admin describe "PATCH #update" do it "updates the document and redirects to index" do - patch :update, params: { id: collaborative_text_document.id, title: "Updated Title", body: body } + patch :update, params: { id: collaborative_text_document.id, title: "Updated Title", body: } expect(response).to redirect_to(documents_path) end end @@ -121,7 +121,7 @@ module Admin let(:title) { "" } it "does not update the document settings" do - patch :update_settings, params: { id: collaborative_text_document.id, title: "", body: body } + patch :update_settings, params: { id: collaborative_text_document.id, title: "", body: } expect(response).to render_template(:edit_settings) expect(flash.now[:alert]).to eq("There was a problem updating the document.") end diff --git a/decidim-collaborative_texts/spec/controllers/decidim/collaborative_texts/suggestions_controller_spec.rb b/decidim-collaborative_texts/spec/controllers/decidim/collaborative_texts/suggestions_controller_spec.rb index 730dc04bb30b7..bf1a9103e38e5 100644 --- a/decidim-collaborative_texts/spec/controllers/decidim/collaborative_texts/suggestions_controller_spec.rb +++ b/decidim-collaborative_texts/spec/controllers/decidim/collaborative_texts/suggestions_controller_spec.rb @@ -30,7 +30,7 @@ module CollaborativeTexts describe "GET #index" do it "returns a success response" do - get :index, params: params + get(:index, params:) expect(response).to have_http_status(:ok) body = JSON.parse(response.body) expect(body.first.keys).to contain_exactly("changeset", "createdAt", "id", "profileHtml", "status", "summary", "type") @@ -60,7 +60,7 @@ module CollaborativeTexts let(:first_node) { "1" } it "returns an error when user is not signed in" do - post :create, params: params + post(:create, params:) expect(response).to have_http_status(:unprocessable_entity) body = JSON.parse(response.body) expect(body["message"]).to eq("You are not authorized to perform this action.") @@ -73,7 +73,7 @@ module CollaborativeTexts it "creates a new suggestion" do expect do - post :create, params: params + post :create, params: end.to change(Suggestion, :count).by(1) expect(response).to have_http_status(:ok) body = JSON.parse(response.body) @@ -84,7 +84,7 @@ module CollaborativeTexts let(:first_node) { "" } it "returns an error" do - post :create, params: params + post(:create, params:) expect(response).to have_http_status(:unprocessable_entity) body = JSON.parse(response.body) expect(body["message"]).to eq("There was a problem creating the suggestion. Invalid selected nodes.") diff --git a/decidim-collaborative_texts/spec/models/decidim/collaborative_texts/document_spec.rb b/decidim-collaborative_texts/spec/models/decidim/collaborative_texts/document_spec.rb index 1ff9a5332099a..9ea7444532d8a 100644 --- a/decidim-collaborative_texts/spec/models/decidim/collaborative_texts/document_spec.rb +++ b/decidim-collaborative_texts/spec/models/decidim/collaborative_texts/document_spec.rb @@ -33,7 +33,7 @@ module CollaborativeTexts it "current version points to last created" do document.save! - version = create(:collaborative_text_version, created_at: 1.second.from_now, document: document) + version = create(:collaborative_text_version, created_at: 1.second.from_now, document:) expect(document.reload.document_versions.count).to eq(4) expect(document.document_versions_count).to eq(4) expect(document.current_version).to eq(version) diff --git a/decidim-collaborative_texts/spec/permissions/decidim/collaborative_texts/permissions_spec.rb b/decidim-collaborative_texts/spec/permissions/decidim/collaborative_texts/permissions_spec.rb index 22cc9b757c886..cd3c2e6dd3f74 100644 --- a/decidim-collaborative_texts/spec/permissions/decidim/collaborative_texts/permissions_spec.rb +++ b/decidim-collaborative_texts/spec/permissions/decidim/collaborative_texts/permissions_spec.rb @@ -9,7 +9,7 @@ let(:context) do { current_component: collaborative_text_component, - document: document + document: } end let(:collaborative_text_component) { create(:collaborative_text_component) } diff --git a/decidim-collaborative_texts/spec/presenters/decidim/collaborative_texts/admin_log/document_presenter_spec.rb b/decidim-collaborative_texts/spec/presenters/decidim/collaborative_texts/admin_log/document_presenter_spec.rb index 5078e72d81458..b970af4e0892a 100644 --- a/decidim-collaborative_texts/spec/presenters/decidim/collaborative_texts/admin_log/document_presenter_spec.rb +++ b/decidim-collaborative_texts/spec/presenters/decidim/collaborative_texts/admin_log/document_presenter_spec.rb @@ -6,7 +6,7 @@ module Decidim module CollaborativeTexts module AdminLog describe DocumentPresenter do - let(:action_log) { double("ActionLog", action: action, resource: resource, extra: extra, version: version) } + let(:action_log) { double("ActionLog", action:, resource:, extra:, version:) } let(:resource) { double("Document", document_versions: [document]) } let(:document) { create(:collaborative_text_document, body: "This is and example body") } let(:extra) { { "extra" => { "version_number" => "2", "body" => document.body } } } diff --git a/decidim-collaborative_texts/spec/system/collaborative_tests_breadcrumbs_spec.rb b/decidim-collaborative_texts/spec/system/collaborative_tests_breadcrumbs_spec.rb new file mode 100644 index 0000000000000..23e02bf5cc047 --- /dev/null +++ b/decidim-collaborative_texts/spec/system/collaborative_tests_breadcrumbs_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "CollaborativeTexts Breadcrumb" do + include_context "with a component" + let(:manifest_name) { "collaborative_texts" } + let!(:component) do + create(:collaborative_text_component, + manifest:, + participatory_space: participatory_process) + end + let!(:document) { create(:collaborative_text_document, :with_body, :published, component:) } + + before do + visit_component + end + + describe "index" do + it "shows the correct information in breadcrumb (space, component)" do + within(".menu-bar") do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + end + end + end + + describe "show" do + it "shows the correct information in breadcrumb (space, component, document)" do + click_on document.title + + within(".menu-bar") do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + expect(page).to have_content(translated(document.title)) + end + end + end +end diff --git a/decidim-collaborative_texts/spec/system/user_sends_suggestions_spec.rb b/decidim-collaborative_texts/spec/system/user_sends_suggestions_spec.rb index 13bdf4b263e25..e12a570f1bc38 100644 --- a/decidim-collaborative_texts/spec/system/user_sends_suggestions_spec.rb +++ b/decidim-collaborative_texts/spec/system/user_sends_suggestions_spec.rb @@ -46,9 +46,6 @@ end it "lists all the documents" do - within(".menu-bar") do - expect(page).to have_content(translated(component.name)) - end within("aside") do expect(page).to have_content(translated(component.name)) end @@ -60,11 +57,6 @@ it "shows the document details" do click_on document.title - within(".menu-bar") do - expect(page).to have_content(translated(component.name)) - expect(page).to have_content(translated(document.title)) - end - expect(page).to have_content(translated(document.title)) within("aside") do expect(page).to have_content("Index") diff --git a/decidim-collaborative_texts/spec/types/document_type_spec.rb b/decidim-collaborative_texts/spec/types/document_type_spec.rb index 7961837fdfad8..6cb15cef4d6e0 100644 --- a/decidim-collaborative_texts/spec/types/document_type_spec.rb +++ b/decidim-collaborative_texts/spec/types/document_type_spec.rb @@ -13,6 +13,12 @@ module CollaborativeTexts include_examples "traceable interface" include_examples "timestamps interface" + shared_examples "unauthorized Document" do + it "throws Decidim::Api::Errors::UnauthorizedObjectError" do + expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedObjectError, "You cannot view or edit this CollaborativeText because you do not have permissions") + end + end + describe "id" do let(:query) { "{ id }" } @@ -93,9 +99,7 @@ module CollaborativeTexts let(:model) { create(:collaborative_text_document, :published, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Document" end context "when participatory space is private but transparent" do @@ -115,9 +119,7 @@ module CollaborativeTexts let(:model) { create(:collaborative_text_document, :published, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Document" end context "when component is not published" do @@ -125,9 +127,7 @@ module CollaborativeTexts let(:model) { create(:collaborative_text_document, :published, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Document" end context "when document is not published" do @@ -135,9 +135,7 @@ module CollaborativeTexts let(:model) { create(:collaborative_text_document, :published, published_at: nil, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Document" end end end diff --git a/decidim-collaborative_texts/spec/types/documents_type_spec.rb b/decidim-collaborative_texts/spec/types/documents_type_spec.rb index ad8b42dd8ec39..cad1e4492b98d 100644 --- a/decidim-collaborative_texts/spec/types/documents_type_spec.rb +++ b/decidim-collaborative_texts/spec/types/documents_type_spec.rb @@ -39,8 +39,8 @@ module CollaborativeTexts context "when the document does not belong to the component" do let!(:document) { create(:collaborative_text_document, :published, component: create(:collaborative_text_component)) } - it "returns null" do - expect(response["collaborativeText"]).to be_nil + it "raises error" do + expect { response }.to raise_error(Decidim::Api::Errors::NotFoundError, "CollaborativeText not found") end end end diff --git a/decidim-collaborative_texts/spec/types/integration_schema_spec.rb b/decidim-collaborative_texts/spec/types/integration_schema_spec.rb index aa03116aea08f..314f5e9548b8a 100644 --- a/decidim-collaborative_texts/spec/types/integration_schema_spec.rb +++ b/decidim-collaborative_texts/spec/types/integration_schema_spec.rb @@ -36,7 +36,7 @@ let!(:current_component) { create(:collaborative_text_component, participatory_space: participatory_process) } let!(:document) { create(:collaborative_text_document, component: current_component, published_at: 2.days.ago) } let!(:document_version) { create(:collaborative_text_version, document:) } - let!(:suggestions) { create_list(:collaborative_text_suggestion, 2, document_version: document_version) } + let!(:suggestions) { create_list(:collaborative_text_suggestion, 2, document_version:) } let(:author) { nil } let(:document_single_result) do { diff --git a/decidim-comments/app/cells/decidim/comments/comment/alignment_badge.erb b/decidim-comments/app/cells/decidim/comments/comment/alignment_badge.erb index 9f11f2a607000..a8197fb093859 100644 --- a/decidim-comments/app/cells/decidim/comments/comment/alignment_badge.erb +++ b/decidim-comments/app/cells/decidim/comments/comment/alignment_badge.erb @@ -1 +1 @@ - + diff --git a/decidim-comments/app/cells/decidim/comments/comment_s_cell.rb b/decidim-comments/app/cells/decidim/comments/comment_s_cell.rb index 001cb75b5ef1c..4e035c23d1f05 100644 --- a/decidim-comments/app/cells/decidim/comments/comment_s_cell.rb +++ b/decidim-comments/app/cells/decidim/comments/comment_s_cell.rb @@ -12,7 +12,7 @@ class CommentSCell < Decidim::CardSCell private def title - resource_link_text + sanitize(translated_attribute(model.body)) end def resource_path diff --git a/decidim-comments/app/models/decidim/comments/seed.rb b/decidim-comments/app/models/decidim/comments/seed.rb index 0302a877b0718..04a765653addc 100644 --- a/decidim-comments/app/models/decidim/comments/seed.rb +++ b/decidim-comments/app/models/decidim/comments/seed.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "decidim/seeds" + module Decidim module Comments # A comment can belong to many Commentable models. This class is responsible @@ -20,16 +22,16 @@ def comments_for(resource) @organization = resource.organization - rand(0..6).times do + rand(0..config_value(:comments_count)).times do comment1 = create_comment(resource) NewCommentNotificationCreator.new(comment1, []).create - if [true, false].sample + if rand < config_value(:comments_nested_probability) comment2 = create_comment(comment1, resource) NewCommentNotificationCreator.new(comment2, []).create end - next if [true, false].sample + next if rand < config_value(:comments_vote_skip_probability) create_votes(comment1) if comment1 create_votes(comment2) if comment2 @@ -40,6 +42,14 @@ def comments_for(resource) attr_reader :organization + def config_value(key) + slow_seeds? ? Decidim::Seeds::SEEDS_CONFIG[key][:slow] : Decidim::Seeds::SEEDS_CONFIG[key][:fast] + end + + def slow_seeds? + Decidim::Env.new("SLOW_SEEDS").present? + end + # Creates a comment for a given resource. # # @private @@ -74,7 +84,7 @@ def create_comment(resource, root_commentable = nil) # # @return nil def create_votes(comment) - rand(0..12).times do + rand(0..config_value(:comments_votes_count)).times do author = random_user next if CommentVote.where(comment:, author:).any? diff --git a/decidim-comments/app/resolvers/decidim/comments/vote_comment_resolver.rb b/decidim-comments/app/resolvers/decidim/comments/vote_comment_resolver.rb deleted file mode 100644 index aeccb5ef96e0a..0000000000000 --- a/decidim-comments/app/resolvers/decidim/comments/vote_comment_resolver.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -module Decidim - module Comments - # A GraphQL resolver to handle `upVote` and `downVote` mutations - # It creates a vote for a comment by the current user. - class VoteCommentResolver - def initialize(options = { weight: 1 }) - @weight = options[:weight] - end - - def call(obj, _args, ctx) - Decidim::Comments::VoteComment.call(obj, ctx[:current_user], weight: @weight) do - on(:ok) do |comment| - return comment - end - on(:invalid) do - return GraphQL::ExecutionError.new(I18n.t("votes.create.error", scope: "decidim.comments")) - end - end - end - end - end -end diff --git a/decidim-comments/config/locales/pt-BR.yml b/decidim-comments/config/locales/pt-BR.yml index 1ecdfde9eade4..6eee23d65daee 100644 --- a/decidim-comments/config/locales/pt-BR.yml +++ b/decidim-comments/config/locales/pt-BR.yml @@ -20,20 +20,38 @@ pt-BR: changeset: comments: Comentários comments: + admin: + shared: + availability_fields: + enabled: Comentários ativados + end_time: Comentários ativados até + start_time: Comentários ativados de + comment_thread: + accessibility_label: Tópico de comentário iniciado por %{full_name} em %{date} comments: create: error: Houveram erros ao criar o comentário. + delete: + error: O comentário não pode ser excluído. update: error: Houve um erro ao atualizar o comentário. + comments_title: Comentário + last_activity: + new_comment: 'Novo comentário:' votes: create: error: Houve erros ao votar o comentário. components: add_comment_form: + account_message: Faça o acesso ou crie uma conta para adicionar o seu comentário. + add_comment: Adicionar comentário form: body: label: Comente placeholder: O que você pensa sobre isso? + form_error: O texto é obrigatório e não pode ser maior que %{length} caracteres. + submit_reply: Publicar resposta + submit_root_comment: Publicar comentário opinion: label: Sua opinião sobre este tópico negative: Negativo @@ -49,12 +67,22 @@ pt-BR: alignment: against: Contra in_favor: A favor + answers: + one: "%{count} resposta" + other: "%{count} respostas" + cancel_reply: Cancelar resposta + comment_label: Comentário %{comment_id} + comment_label_reply: Comentário %{comment_id} (responder ao comentário %{parent_comment_id}) confirm_destroy: Tem certeza que deseja excluir este comentário? + controls_label: Controles de comentários delete: Deletar deleted_at: Comentário excluído em %{date} deleted_user: Usuário excluído edit: Editar edited: Editado + hide_replies: + one: Ocultar resposta + other: Ocultar {count} respostas moderated_at: Comentário moderado em %{date} reply: Resposta report: @@ -64,6 +92,7 @@ pt-BR: description: Este conteúdo é impróprio? details: Comentários adicionais reasons: + does_not_belong: Contém atividades ilegais, ameaças suicidas, informações pessoais ou outra coisa que você acha que não pertence ao %{organization_name}. offensive: Contém racismo, sexismo, insultos, ataques pessoais, ameaças de morte, pedidos de suicídio ou qualquer tipo de discurso de ódio. spam: Contém clickbait, publicidade, fraudes ou script bots. title: Reportar conteúdo impróprio @@ -71,6 +100,7 @@ pt-BR: one: Mostrar resposta other: Mostrar %{count} respostas single_comment_link_title: Obter o link + sort_by: 'Classificar por: ' comment_order_selector: order: best_rated: Melhores avaliações @@ -79,15 +109,23 @@ pt-BR: recent: Recente title: 'Ordenar por:' comments: + against: Contra blocked_comments_for_unauthorized_user_warning: Você precisa ser verificado para comentar neste momento, mas pode ler os anteriores. blocked_comments_for_user_warning: Você não pode comentar neste momento, mas pode ler os anteriores. + blocked_comments_warning: Comentários estão atualmente desativados, somente administradores podem responder ou postar novos. comment_details_title: Detalhes do comentário + in_favor: A favor loading: Carregando comentários ... + single_comment_warning: Ver todos os comentários single_comment_warning_title: Você está vendo um único comentário title: one: "%{count} comentário" other: "%{count} comentários" + top_comment_label: Mais votado down_vote_button: + label: + one: Botão "Não gostei". %{count} não gostei + other: Botão "Não gostei". %{count} não gostaram text: Eu discordo deste comentário edit_comment_modal_form: close: Fechar @@ -98,7 +136,29 @@ pt-BR: submit: Enviar title: Editar seu comentário up_vote_button: + label: + one: Botão "Gostei". %{count} gostei + other: Botão "Gostei". %{count} gostaram text: Eu concordo com este comentário + download_your_data: + help: + comment_votes: + comment: A identificação do comentário votado + created_at: A data em que esta votação foi criada + id: A identificação da votação + updated_at: A data da última atualização desta votação + weight: O peso da votação (1 a favor do voto, -1 contra o voto) + comments: + alignment: Se este comentário foi um favorito, contra ou neutro + author: O nome do usuário que fez este comentário + body: O comentário em si + commentable_id: A identificação única do cometário + commentable_type: O tipo do comentário (se for um resultado, uma proposta, etc.) + created_at: A data em que este comentário foi criado + depth: O lugar onde este comentário está nos três comentários (se for uma resposta ou uma resposta de uma resposta) + id: O identificador para este comentário + locale: A localidade (linguagem) que o usuário teve ao deixar este comentário + root_commentable_url: A URL do recurso ligado a este comentário events: comments: comment_by_followed_user: @@ -131,3 +191,6 @@ pt-BR: email_outro: Você recebeu esta notificação porque você foi mencionado em %{resource_title}. email_subject: Você foi mencionado em %{resource_title} notification_title: Você foi mencionado em %{resource_title} por %{author_name} %{author_nickname} + errors: + messages: + cannot_have_comments: não pode ter comentários diff --git a/decidim-comments/config/locales/ro-RO.yml b/decidim-comments/config/locales/ro-RO.yml index 06a0cf8bc4d1c..84c10de28a215 100644 --- a/decidim-comments/config/locales/ro-RO.yml +++ b/decidim-comments/config/locales/ro-RO.yml @@ -129,6 +129,10 @@ ro: other: "%{count} comentarii" top_comment_label: Cele mai votate down_vote_button: + label: + one: Buton dezaprobare. O dezaprobare + few: Buton dezaprobare. %{count} dezaprobări + other: Buton dezaprobare. %{count} dezaprobări text: Nu sunt de acord cu acest comentariu edit_comment_modal_form: close: Închideți @@ -139,6 +143,10 @@ ro: submit: Actualizați title: Modificați comentariul dumneavoastră up_vote_button: + label: + one: Buton apreciere. %{count} apreciază + few: Buton apreciere. %{count} apreciază + other: Buton apreciere. %{count} apreciază text: Sunt de acord cu acest comentariu download_your_data: help: diff --git a/decidim-comments/config/locales/sv.yml b/decidim-comments/config/locales/sv.yml index 45fd4ac806ed1..4dde695e5caf1 100644 --- a/decidim-comments/config/locales/sv.yml +++ b/decidim-comments/config/locales/sv.yml @@ -48,7 +48,7 @@ sv: form: body: label: Kommentera - placeholder: Vad tycker du om detta? + placeholder: Vad tycker du? form_error: Texten är obligatorisk och får inte vara längre än %{length} tecken. submit_reply: Publicera svar submit_root_comment: Publicera kommentar @@ -103,10 +103,10 @@ sv: sort_by: 'Sortera efter: ' comment_order_selector: order: - best_rated: Bästa betyg + best_rated: Högst betyg most_discussed: Mest diskuterade - older: Äldsta - recent: Senaste + older: Gamla + recent: Nya title: 'Sortera efter:' comments: against: Mot diff --git a/decidim-comments/lib/decidim/api/comment_mutation_type.rb b/decidim-comments/lib/decidim/api/comment_mutation_type.rb index eb18b16042da3..7a5d590665607 100644 --- a/decidim-comments/lib/decidim/api/comment_mutation_type.rb +++ b/decidim-comments/lib/decidim/api/comment_mutation_type.rb @@ -10,12 +10,20 @@ class CommentMutationType < Decidim::Api::Types::BaseObject field :id, GraphQL::Types::ID, "The Comment's unique ID", null: false field :up_vote, Decidim::Comments::CommentType, "The comment that is upvoted", null: true - def up_vote(args: {}) - VoteCommentResolver.new(weight: 1).call(object, args, context) + def up_vote(_args: {}) + Decidim::Comments::VoteComment.call(object, current_user, weight: 1) do + on(:ok) do |comment| + return comment + end + end end - def down_vote(args: {}) - VoteCommentResolver.new(weight: -1).call(object, args, context) + def down_vote(_args: {}) + Decidim::Comments::VoteComment.call(object, current_user, weight: -1) do + on(:ok) do |comment| + return comment + end + end end end end diff --git a/decidim-comments/lib/decidim/api/commentable_mutation_type.rb b/decidim-comments/lib/decidim/api/commentable_mutation_type.rb index 7ce370b8a8a58..0fcada3818574 100644 --- a/decidim-comments/lib/decidim/api/commentable_mutation_type.rb +++ b/decidim-comments/lib/decidim/api/commentable_mutation_type.rb @@ -22,6 +22,10 @@ def add_comment(body:, alignment: nil) on(:ok) do |comment| return comment end + + on(:invalid) do + raise GraphQL::ExecutionError, t("create.error", scope: "decidim.comments.comments") + end end end end diff --git a/decidim-comments/spec/system/search_comments_spec.rb b/decidim-comments/spec/system/search_comments_spec.rb index 4a898f2c3e744..93a610dcf3681 100644 --- a/decidim-comments/spec/system/search_comments_spec.rb +++ b/decidim-comments/spec/system/search_comments_spec.rb @@ -16,5 +16,22 @@ searchables << comment end + context "when there is a link in the comment search result" do + let(:search_input_selector) { "input#input-search" } + + before do + create(:comment, body: "Here is an interesting link: https://github.com/decidim", commentable:) + visit decidim.root_path + field = find(search_input_selector) + field.set "Here is an interesting" + send_keys(:enter) + end + + it "does not allow clickable link" do + expect(page).to have_no_link(href: "https://github.com/decidim") + expect(page).to have_text("Here is an interesting link: https://github.com/decidim") + end + end + include_examples "searchable results" end diff --git a/decidim-comments/spec/types/comment_type_spec.rb b/decidim-comments/spec/types/comment_type_spec.rb index d65bacb7473ab..d2ba11f4faa9f 100644 --- a/decidim-comments/spec/types/comment_type_spec.rb +++ b/decidim-comments/spec/types/comment_type_spec.rb @@ -11,6 +11,12 @@ module Comments let(:model) { create(:comment) } let(:sgid) { double("sgid", to_s: "1234") } + shared_examples "unauthorized Comment" do + it "throws Decidim::Api::Errors::UnauthorizedObjectError" do + expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedObjectError, "You cannot view or edit this Comment because you do not have permissions") + end + end + context "when participatory space is unpublished" do let(:participatory_space) { create(:assembly, :unpublished) } let(:component) { create(:dummy_component, :published, participatory_space:) } @@ -20,9 +26,7 @@ module Comments let(:model) { create(:comment, commentable:) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Comment" end context "when participatory space is private and transparent" do @@ -45,9 +49,7 @@ module Comments let(:model) { create(:comment, commentable:) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Comment" end context "when component is unpublished" do @@ -57,9 +59,7 @@ module Comments let(:model) { create(:comment, commentable:) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Comment" end context "when resource is unpublished" do @@ -68,9 +68,7 @@ module Comments let(:model) { create(:comment, commentable:) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Comment" end context "when resource is moderated" do @@ -80,27 +78,21 @@ module Comments let(:model) { create(:comment, commentable:) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Comment" end describe "deleted comment" do let(:model) { create(:comment, :deleted) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Comment" end describe "moderated comment" do let(:model) { create(:comment, :moderated) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Comment" end describe "author" do diff --git a/decidim-conferences/app/cells/decidim/conferences/conference_dropdown_metadata_cell.rb b/decidim-conferences/app/cells/decidim/conferences/conference_dropdown_metadata_cell.rb deleted file mode 100644 index 829fed3742275..0000000000000 --- a/decidim-conferences/app/cells/decidim/conferences/conference_dropdown_metadata_cell.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -module Decidim - module Conferences - class ConferenceDropdownMetadataCell < Decidim::ParticipatorySpaceDropdownMetadataCell - include ConferenceHelper - include Decidim::ComponentPathHelper - include ActiveLinkTo - - def decidim_conferences - Decidim::Conferences::Engine.routes.url_helpers - end - - private - - def nav_items_method = :conference_nav_items - end - end -end diff --git a/decidim-conferences/app/controllers/decidim/conferences/conference_program_controller.rb b/decidim-conferences/app/controllers/decidim/conferences/conference_program_controller.rb index 4e5c24ddd1efa..a97099fcb3c01 100644 --- a/decidim-conferences/app/controllers/decidim/conferences/conference_program_controller.rb +++ b/decidim-conferences/app/controllers/decidim/conferences/conference_program_controller.rb @@ -10,6 +10,8 @@ class ConferenceProgramController < Decidim::Conferences::ApplicationController helper_method :collection, :conference, :meeting_days, :meeting_component + before_action :set_program_breadcrumb_item + def show raise ActionController::RoutingError, "No meetings for this conference " if meetings.blank? @@ -47,6 +49,17 @@ def current_participatory_space def conference current_participatory_space end + + def set_program_breadcrumb_item + return unless meeting_component + + context_breadcrumb_items << { + label: t("conference_program.index.title", scope: "decidim"), + url: decidim_conferences.conference_conference_program_path(current_participatory_space, meeting_component, locale: I18n.locale), + active: true, + resource: meeting_component + } + end end end end diff --git a/decidim-conferences/app/models/decidim/conference.rb b/decidim-conferences/app/models/decidim/conference.rb index b48faa15ccbaf..ae978d9d85525 100644 --- a/decidim-conferences/app/models/decidim/conference.rb +++ b/decidim-conferences/app/models/decidim/conference.rb @@ -157,7 +157,7 @@ def self.ransackable_attributes(auth_object = nil) return base unless auth_object&.admin? - base + %w(published_at) + base + %w(published_at created_at) end def self.ransackable_associations(_auth_object = nil) diff --git a/decidim-conferences/app/presenters/decidim/admin/conference_speaker_presenter.rb b/decidim-conferences/app/presenters/decidim/admin/conference_speaker_presenter.rb index f41e77d451c5e..d6ee463a4ec85 100644 --- a/decidim-conferences/app/presenters/decidim/admin/conference_speaker_presenter.rb +++ b/decidim-conferences/app/presenters/decidim/admin/conference_speaker_presenter.rb @@ -8,11 +8,19 @@ module Admin class ConferenceSpeakerPresenter < SimpleDelegator def name if user - "#{user.name} (#{Decidim::UserPresenter.new(user).nickname})" + "#{user.name} (#{user.nickname})" else full_name end end + + private + + def user + @user ||= if (user = __getobj__.user.presence) + Decidim::UserPresenter.new(user) + end + end end end end diff --git a/decidim-conferences/config/locales/cs.yml b/decidim-conferences/config/locales/cs.yml index 49f00704fa60d..41520df4c54fc 100644 --- a/decidim-conferences/config/locales/cs.yml +++ b/decidim-conferences/config/locales/cs.yml @@ -632,6 +632,7 @@ cs: open_data: help: conferences: + component_settings: Nastavení komponent v konferenčním prostoru created_at: Datum, kdy byl tento prostor vytvořen decidim_scope_id: Rozsah konference description: Dlouhý popis konference diff --git a/decidim-conferences/config/locales/fi-plain.yml b/decidim-conferences/config/locales/fi-plain.yml index 8fb676a94b9e0..74c65f2c96147 100644 --- a/decidim-conferences/config/locales/fi-plain.yml +++ b/decidim-conferences/config/locales/fi-plain.yml @@ -622,6 +622,7 @@ fi-pl: open_data: help: conferences: + component_settings: Konferenssin komponenttiasetukset created_at: Tilan luontiaika decidim_scope_id: Konferenssin teema description: Konferenssin pitkä kuvaus diff --git a/decidim-conferences/config/locales/fi.yml b/decidim-conferences/config/locales/fi.yml index 6f67310e6bfa9..3cfc05873cbc9 100644 --- a/decidim-conferences/config/locales/fi.yml +++ b/decidim-conferences/config/locales/fi.yml @@ -622,6 +622,7 @@ fi: open_data: help: conferences: + component_settings: Konferenssin komponenttiasetukset created_at: Tilan luontiaika decidim_scope_id: Konferenssin teema description: Konferenssin pitkä kuvaus diff --git a/decidim-conferences/config/locales/ja.yml b/decidim-conferences/config/locales/ja.yml index 64b14a53698ec..76a038fad586d 100644 --- a/decidim-conferences/config/locales/ja.yml +++ b/decidim-conferences/config/locales/ja.yml @@ -617,6 +617,7 @@ ja: open_data: help: conferences: + component_settings: カンファレンススペースのコンポーネント設定 created_at: このスペースが作成された日時 decidim_scope_id: カンファレンスのスコープ description: カンファレンスの詳しい説明 diff --git a/decidim-conferences/config/locales/pt-BR.yml b/decidim-conferences/config/locales/pt-BR.yml index fce64732802ea..f322fdfb87d4e 100644 --- a/decidim-conferences/config/locales/pt-BR.yml +++ b/decidim-conferences/config/locales/pt-BR.yml @@ -2,10 +2,14 @@ pt-BR: activemodel: attributes: conference: + assemblies_ids: Assembleias Relacionadas available_slots: Vagas disponíveis banner_image: Imagem de banner decidim_scope_id: Escopo description: Descrição + duplicate_categories: Categorias duplicadas + duplicate_components: Componentes duplicados + duplicate_features: Recursos duplicados end_date: Data final hero_image: Imagem inicial location: Localização @@ -55,11 +59,14 @@ pt-BR: personal_url: URL pessoal position: Posição short_bio: Perfil curto + twitter_handle: X handler user_id: Usuário conference_user_role: email: O email name: Nome role: Função + partner: + logo: Logotipo errors: models: conference_registration_invite: @@ -81,6 +88,7 @@ pt-BR: admin: actions: confirm: confirme + confirm_delete_conference: Tem certeza de que deseja excluir esta conferência? Se mudar de ideia, você pode restaurá-la mais tarde. new_conference: Nova conferência new_conference_user_role: Novo usuário da conferência new_media_link: Novo link de mídia @@ -88,6 +96,12 @@ pt-BR: new_registration_type: Novo tipo de registro new_speaker: Novo Pregador send_diplomas: Envie certificados de participação + view_deleted_conferences: Ver conferências excluídas + conference_duplicates: + new: + duplicate: Duplicado + select: Selecione quais dados você gostaria de duplicar + title: Conferência duplicada conference_publications: create: error: Ocorreu um erro ao publicar esta conferência. @@ -106,11 +120,13 @@ pt-BR: destroy: success: Alto-falante excluído com sucesso para esta conferência. edit: + title: Atualize o orador da conferência update: Atualizar index: conference_speakers_title: Alto-falantes da conferência new: create: Crio + title: Novo orador da conferência publish: invalid: Ocorreu um problema ao publicar esta reunião. success: Alto-falante excluído com sucesso para esta conferência. @@ -127,11 +143,13 @@ pt-BR: destroy: success: Usuário removido com sucesso desta conferência. edit: + title: Atualize o usuário da conferência update: Atualizar index: conference_admins_title: Administradores da conferência new: create: Crio + title: Novo administrador da conferência update: error: Houve um erro ao atualizar um usuário para esta conferência. success: Usuário atualizado com sucesso para esta conferência. @@ -144,25 +162,45 @@ pt-BR: exports: registrations: Inscrições form: + duration: Duração images: Fotos + metadata: Metadados + registrations: Inscrições + related_spaces: Espaços relacionados title: Informação geral + visibility: Visibilidade index: published: Publicados unpublished: Despublicado + manage_trash: + title: Conferências excluídas new: create: Crio + title: Nova conferência update: error: Houve um erro ao atualizar esta conferência. success: Conferência atualizada com sucesso. + conferences_duplicates: + create: + error: Houve um erro ao duplicar esta conferência. + success: Conferência duplicada com sucesso. media_links: create: error: Ocorreu um erro ao criar um novo link de mídia. + success: Link de mídia criado com sucesso. + destroy: + success: Link de mídia excluído com sucesso. edit: + title: Atualizar link de mídia update: Atualizar + index: + media_links_title: Links de mídia new: create: Criar + title: Criar link de mídia update: error: Houve um erro ao atualizar este link de mídia. + success: Link de mídia atualizado com sucesso. menu: conferences: Conferências conferences_submenu: @@ -173,24 +211,32 @@ pt-BR: conference_admins: Administradores da conferência conference_invites: Convites conference_speakers: caixas de som + diploma: Certificado de participação info: Links sobre esta conferência + media_links: Links de mídia moderations: Moderações partners: Parceiros + registration_types: Tipos de registro registrations: Inscrições + see_conference: Ver conferência + user_registrations: Inscrições de usuários models: conference: fields: + actions: Ações created_at: Criado em published: Publicados title: Título conference_speaker: fields: + actions: Ações affiliation: Afiliação full_name: Nome completo position: Posição name: Conferência Palestrante conference_user_role: fields: + actions: Ações email: O email name: Nome role: Função @@ -198,14 +244,18 @@ pt-BR: roles: admin: Administrador collaborator: Colaborador + evaluator: Avaliador moderator: Moderador media_link: fields: + actions: Ações date: Encontro link: Link title: Título + name: Link da mídia partner: fields: + actions: Ações link: Link logo: Logotipo name: Nome @@ -216,6 +266,7 @@ pt-BR: main_promotor: Principais promotores registration_type: fields: + actions: Ações conference_meetings: Reuniões da conferência price: Preço registrations_count: Contagem de registros @@ -229,6 +280,7 @@ pt-BR: destroy: success: Alto-falante excluído com sucesso para esta conferência. edit: + title: Atualizar parceiro update: Atualizar new: create: Criar @@ -250,6 +302,7 @@ pt-BR: destroy: success: Tipo de registro removido com sucesso desta conferência. edit: + title: Atualizar tipo de registro update: Atualizar new: create: Crio @@ -257,13 +310,21 @@ pt-BR: update: error: Ocorreu um erro ao atualizar um tipo de registro para esta conferência. success: Tipo de registro atualizado com sucesso para esta conferência. + taxonomy_filters: + space_filter_for: + conferences: Todas as conferências titles: conferences: Conferências + conferences_deleted: Conferências excluídas + tooltips: + deleted_conferences_info: As conferências só podem ser excluídas se o status for "Não publicado". admin_log: conference: create: "%{user_name} criou a conferência %{resource_name}" publish: "%{user_name} publicou a conferencia%{resource_name}" + restore: "%{user_name} restaurou a conferência %{resource_name}" send_conference_diplomas: "%{user_name} enviou certificados de participação para os %{resource_name} participantes da conferência" + soft_delete: "%{user_name} movido para a lixeira da conferência %{resource_name}" unpublish: "%{user_name} despublicou a conferencia%{resource_name}" update: "%{user_name} Atualizou a conferência %{resource_name}" update_diploma: "%{user_name} atualizou a configuração dos certificados de participação para %{resource_name} conferência" @@ -300,6 +361,9 @@ pt-BR: title: caixas de som conferences: admin: + conference_duplicates: + form: + slug_help_html: 'Os slugs de URL são usados para gerar as URLs que apontam para esta conferência. Aceita apenas letras, números e traços e deve começar com uma letra. Exemplo: %{url}' conference_invites: create: error: Houve um problema ao convidar o usuário para participar da conferência. @@ -331,16 +395,20 @@ pt-BR: conferences: form: available_slots_help: Deixe-o em 0 se você tiver slots ilimitados disponíveis. + define_taxonomy_filters: Por favor, defina alguns filtros para este espaço participativo antes de usar esta configuração. + no_taxonomy_filters_found: Nenhum filtro de taxonomia encontrado. registrations_count: one: Houve 1 registro. other: Houve %{count} inscrições. slug_help_html: 'Os slugs de URL são usados para gerar as URLs que apontam para esta conferência. Aceita apenas letras, números e traços e deve começar com uma letra. Exemplo: %{url}' + taxonomies: Taxonomias content_blocks: highlighted_conferences: max_results: Quantidade máxima de elementos para mostrar diplomas: edit: save: Salve  + title: Certificado de participação invite_join_conference_mailer: invite: decline: Recusar convite '%{conference_title}' @@ -360,6 +428,7 @@ pt-BR: diploma_html: Você encontrará o certificado de participação para a conferência %{title} nos anexos. diploma_user: attendance_verified_by: Atendimento verificado por + certificate_of_attendance: Certificado de participação certificate_of_attendance_description: Certifica-se que %{user} participou e participou nos %{title} realizados em %{location} em %{start} - %{end} send_diploma: error: Houve um problema ao enviar os certificados de participação da conferência. @@ -373,6 +442,8 @@ pt-BR: no_slots_available: Não há slots disponíveis registration: Cadastro conference_program: + program_item: + other_category: Outros show: program: Programa conference_registration_mailer: @@ -406,6 +477,11 @@ pt-BR: conference_speaker_cell: personal_url: personal_website: Site pessoal + conference_speakers: + index: + speakers: + one: Palestrante + other: caixas de som conferences: partners: collaborators: Parceiros @@ -424,6 +500,8 @@ pt-BR: name: Conferências destacadas index: title: Conferências + last_activity: + new_conference: 'Nova conferência:' mailer: conference_registration_mailer: confirmation: @@ -450,6 +528,7 @@ pt-BR: sent: Enviei conference_registration: fields: + actions: Ações email: O email name: Nome registration_type: Tipo de Registro @@ -474,6 +553,25 @@ pt-BR: objectives: Objetivos related_assemblies: Assembleias Relacionadas related_participatory_processes: Processos participativos relacionados + download_your_data: + help: + conference_invites: + accepted_at: A data em que o convite de conferência foi aceito + conference: A conferência para onde este convite foi enviado + confirmed_at: A data em que este convite foi confirmado + created_at: A data em que este convite foi criado + id: O identificador único do convite de conferência + registration_type: O tipo de registro para esta conferência que foi enviado + rejected_at: A data em que o convite para conferência foi rejeitado + sent_at: A data em que o convite desta conferência foi enviado + updated_at: A data em que este convite foi atualizado pela última vez + conference_registrations: + conference: A conferência a que isto pertence + confirmed_at: A data em que este registro foi confirmado + created_at: A data em que esta inscrição foi criada + id: O identificador exclusivo dos registros de conferência + registration_type: O tipo de registro que pertence a + updated_at: A data da última atualização desta inscrição events: conferences: conference_registration_confirmed: @@ -521,8 +619,36 @@ pt-BR: title: Mídia e Links menu: conferences: Conferências + open_data: + help: + conferences: + component_settings: Configurações do componente do espaço de conferência + created_at: A data em que esta comunidade foi criada + decidim_scope_id: O escopo da conferência + description: Uma longa descrição da conferência + end_date: A data em que esta conferência termina. + follows_count: O número de usuários que seguem este espaço + id: O identificador único dessa conferência + location: Local da conferência. Onde esta conferência será realizada. + objectives: Quais são os objetivos desta conferência. Qual é a meta. + promoted: Se a conferência é promovida ou não + published_at: A data em que esta comunidade foi publicada + reference: A referência única do espaço + remote_banner_image_url: A URL da imagem do banner da conferência + remote_hero_image_url: URL da imagem do herói da conferência + scope: O escopo da conferência + scopes_enabled: Clima os escopos estão ativados ou não + short_description: Uma breve descrição da conferência + slogan: O slogan para esta conferência + slug: O slug de conferência (usado para fins de identificação, para a URL) + start_date: A data de início desta conferência. + taxonomies: As taxonomias da conferência + title: O título da conferência + updated_at: A última data em que este espaço foi atualizado + url: A URL do espaço statistics: conferences_count: Conferências + conferences_count_tooltip: O número de conferências publicadas. devise: mailer: join_conference: diff --git a/decidim-conferences/config/locales/ro-RO.yml b/decidim-conferences/config/locales/ro-RO.yml index 18957f0f884c7..f7b3eb156c09f 100644 --- a/decidim-conferences/config/locales/ro-RO.yml +++ b/decidim-conferences/config/locales/ro-RO.yml @@ -174,12 +174,12 @@ ro: attachment_files: Fișiere attachments: Atașamente components: Componente - conference_admins: Administratori ai conferinţei + conference_admins: Administratori ai conferinței conference_invites: Invitații conference_speakers: Vorbitori diploma: Certificat de participare media_links: Link-uri media - moderations: Moderatii + moderations: Moderări partners: Parteneri registration_types: Tipuri de înregistrare registrations: Înregistrări diff --git a/decidim-conferences/lib/decidim/api/conference_registration_type_type.rb b/decidim-conferences/lib/decidim/api/conference_registration_type_type.rb index a4c6062292abd..54916546008e1 100644 --- a/decidim-conferences/lib/decidim/api/conference_registration_type_type.rb +++ b/decidim-conferences/lib/decidim/api/conference_registration_type_type.rb @@ -22,8 +22,6 @@ def self.authorized?(object, context) ].all? super && chain - rescue Decidim::PermissionAction::PermissionNotSetError - false end end end diff --git a/decidim-conferences/lib/decidim/api/conference_speaker_type.rb b/decidim-conferences/lib/decidim/api/conference_speaker_type.rb index 2992a2a024184..239890b9393c0 100644 --- a/decidim-conferences/lib/decidim/api/conference_speaker_type.rb +++ b/decidim-conferences/lib/decidim/api/conference_speaker_type.rb @@ -30,8 +30,6 @@ def self.authorized?(object, context) ].all? super && chain - rescue Decidim::PermissionAction::PermissionNotSetError - false end end end diff --git a/decidim-conferences/lib/decidim/conferences/participatory_space.rb b/decidim-conferences/lib/decidim/conferences/participatory_space.rb index b2cd8e49fced6..59bc60725c8a9 100644 --- a/decidim-conferences/lib/decidim/conferences/participatory_space.rb +++ b/decidim-conferences/lib/decidim/conferences/participatory_space.rb @@ -17,8 +17,6 @@ participatory_space.query_type = "Decidim::Conferences::ConferenceType" - participatory_space.breadcrumb_cell = "decidim/conferences/conference_dropdown_metadata" - participatory_space.register_resource(:conference) do |resource| resource.model_class_name = "Decidim::Conference" resource.card = "decidim/conferences/conference" diff --git a/decidim-conferences/spec/cells/decidim/conferences/conference_dropdown_metadata_cell_spec.rb b/decidim-conferences/spec/cells/decidim/conferences/conference_dropdown_metadata_cell_spec.rb deleted file mode 100644 index 1f58b121eb134..0000000000000 --- a/decidim-conferences/spec/cells/decidim/conferences/conference_dropdown_metadata_cell_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -require "decidim/core/test/shared_examples/participatory_space_dropdown_metadata_cell_examples" - -module Decidim::Conferences - describe ConferenceDropdownMetadataCell, type: :cell do - controller Decidim::ApplicationController - - subject { cell("decidim/conferences/conference_dropdown_metadata", model).call } - - let(:model) { create(:conference) } - - include_examples "participatory space dropdown metadata cell" - end -end diff --git a/decidim-conferences/spec/lib/decidim/conferences/query_extensions_spec.rb b/decidim-conferences/spec/lib/decidim/conferences/query_extensions_spec.rb index 741da0678c957..2ebb49d9f1aa7 100644 --- a/decidim-conferences/spec/lib/decidim/conferences/query_extensions_spec.rb +++ b/decidim-conferences/spec/lib/decidim/conferences/query_extensions_spec.rb @@ -38,8 +38,8 @@ module Conferences let!(:conference) { create(:conference) } let(:id) { conference.id } - it "returns nil" do - expect(response["conference"]).to be_nil + it_behaves_like "graphQL not found space" do + let(:space_type) { "conference" } end end end diff --git a/decidim-conferences/spec/system/admin/admin_filters_conferences_spec.rb b/decidim-conferences/spec/system/admin/admin_filters_conferences_spec.rb new file mode 100644 index 0000000000000..e4ba765868f31 --- /dev/null +++ b/decidim-conferences/spec/system/admin/admin_filters_conferences_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Admin sorting conferences" do + let(:organization) { create(:organization) } + let(:user) { create(:user, :admin, :confirmed, organization:) } + + let!(:old_conference) { create(:conference, title: { en: "Old conference" }, created_at: 3.weeks.ago, organization:) } + let!(:recent_conference) { create(:conference, title: { en: "Recent conference" }, created_at: 1.day.ago, organization:) } + let!(:newest_conference) { create(:conference, title: { en: "Newest conference" }, created_at: Time.current, organization:) } + + before do + switch_to_host(organization.host) + login_as user, scope: :user + visit decidim_admin_conferences.conferences_path + end + + context "when sorting conferences by their creation" do + it "sorts by created_at descending by default" do + within "table thead" do + click_link "Created at" + end + + titles = page.all("table tbody tr td:first-child") + expect(titles[0].text).to include("Newest conference") + expect(titles[1].text).to include("Recent conference") + expect(titles[2].text).to include("Old conference") + end + + it "sorts by created_at ascending when clicked again" do + within "table thead" do + click_link "Created at" + click_link "Created at" + end + + titles = page.all("table tbody tr td:first-child") + expect(titles[0].text).to include("Old conference") + expect(titles[1].text).to include("Recent conference") + expect(titles[2].text).to include("Newest conference") + end + end +end diff --git a/decidim-conferences/spec/system/admin/admin_manages_conference_soft_delete_spec.rb b/decidim-conferences/spec/system/admin/admin_manages_conference_soft_delete_spec.rb index d1fecf196bbf0..6321649f6f4f2 100644 --- a/decidim-conferences/spec/system/admin/admin_manages_conference_soft_delete_spec.rb +++ b/decidim-conferences/spec/system/admin/admin_manages_conference_soft_delete_spec.rb @@ -14,12 +14,12 @@ it_behaves_like "manage trashed resource", "conference" context "when a user is collaborator" do - let!(:conference) { create(:conference, organization: organization) } - let!(:collaborator_user) { create(:user, :admin_terms_accepted, :confirmed, organization: organization) } + let!(:conference) { create(:conference, organization:) } + let!(:collaborator_user) { create(:user, :admin_terms_accepted, :confirmed, organization:) } let!(:collaborator_role) do create(:conference_user_role, user: collaborator_user, - conference: conference, + conference:, role: :collaborator) end @@ -36,12 +36,12 @@ end context "when a user is evaluator" do - let!(:conference) { create(:conference, organization: organization) } - let!(:evaluator_user) { create(:user, :admin_terms_accepted, :confirmed, organization: organization) } + let!(:conference) { create(:conference, organization:) } + let!(:evaluator_user) { create(:user, :admin_terms_accepted, :confirmed, organization:) } let!(:evaluator_role) do create(:conference_user_role, user: evaluator_user, - conference: conference, + conference:, role: :evaluator) end @@ -58,12 +58,12 @@ end context "when a user is moderator" do - let!(:conference) { create(:conference, organization: organization) } - let!(:moderator_user) { create(:user, :admin_terms_accepted, :confirmed, organization: organization) } + let!(:conference) { create(:conference, organization:) } + let!(:moderator_user) { create(:user, :admin_terms_accepted, :confirmed, organization:) } let!(:moderator_role) do create(:conference_user_role, user: moderator_user, - conference: conference, + conference:, role: :moderator) end @@ -80,12 +80,12 @@ end context "when a user is a space admin" do - let!(:conference) { create(:conference, organization: organization) } - let!(:admin_user) { create(:user, :admin_terms_accepted, :confirmed, organization: organization) } + let!(:conference) { create(:conference, organization:) } + let!(:admin_user) { create(:user, :admin_terms_accepted, :confirmed, organization:) } let!(:admin_role) do create(:conference_user_role, user: admin_user, - conference: conference, + conference:, role: :admin) end diff --git a/decidim-conferences/spec/system/admin/invite_conference_admin_spec.rb b/decidim-conferences/spec/system/admin/invite_conference_admin_spec.rb index dff23ccf1fa7d..f3bbda98a23c1 100644 --- a/decidim-conferences/spec/system/admin/invite_conference_admin_spec.rb +++ b/decidim-conferences/spec/system/admin/invite_conference_admin_spec.rb @@ -15,5 +15,5 @@ include_context "when inviting participatory space users" - it_behaves_like "inviting participatory space admins", check_private_space: false, check_landing_page: false + it_behaves_like "inviting participatory space admins", check_members_page: false, check_landing_page: false end diff --git a/decidim-conferences/spec/system/conference_breadcrumb_spec.rb b/decidim-conferences/spec/system/conference_breadcrumb_spec.rb deleted file mode 100644 index b55a1c4579ce2..0000000000000 --- a/decidim-conferences/spec/system/conference_breadcrumb_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe "Conference Breadcrumb" do - let(:organization) { create(:organization) } - let(:participatory_space) { create(:conference, :published, organization:) } - let(:component) { create(:proposal_component, :published, participatory_space:) } - let(:router) { Decidim::EngineRouter.main_proxy(component) } - let!(:proposal) { create(:proposal, :published, component:) } - - before do - switch_to_host(organization.host) - end - - scenario "shows breadcrumb with only conference" do - visit decidim_conferences.conference_path(participatory_space, locale: I18n.locale) - - within ".menu-bar" do - expect(page).to have_content("Conferences") - expect(page).to have_content(translated(participatory_space.title)) - end - end - - scenario "shows breadcrumb with conference and component" do - visit router.root_path - - within ".menu-bar" do - expect(page).to have_content("Conferences") - expect(page).to have_content(translated(participatory_space.title)) - expect(page).to have_content(translated(component.name)) - end - end -end diff --git a/decidim-conferences/spec/system/conferences_breadcrumbs_spec.rb b/decidim-conferences/spec/system/conferences_breadcrumbs_spec.rb new file mode 100644 index 0000000000000..1db071a774907 --- /dev/null +++ b/decidim-conferences/spec/system/conferences_breadcrumbs_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Conferences Breadcrumb" do + let(:organization) { create(:organization) } + let(:participatory_space) { create(:conference, :published, organization:) } + let(:component) { create(:proposal_component, :published, participatory_space:) } + let(:router) { Decidim::EngineRouter.main_proxy(component) } + let!(:proposal) { create(:proposal, :published, component:) } + + before do + switch_to_host(organization.host) + end + + scenario "shows breadcrumb with only conference" do + visit decidim_conferences.conference_path(participatory_space, locale: I18n.locale) + + within ".menu-bar" do + expect(page).to have_content("Conferences") + expect(page).to have_content(translated(participatory_space.title)) + end + end + + scenario "shows breadcrumb with conference and component" do + visit router.root_path + + within ".menu-bar" do + expect(page).to have_content("Conferences") + expect(page).to have_content(translated(participatory_space.title)) + expect(page).to have_content(translated(component.name)) + end + end + + describe "with a program" do + let(:meetings_component) { create(:meeting_component, :published, participatory_space:) } + let!(:meeting) { create(:meeting, :published, latitude:, longitude:, component: meetings_component, start_time: 1.day.from_now) } + + let(:latitude) { 40.7504928941818 } + let(:longitude) { -73.993466492276 } + let(:geocoder_request_url) { "https://nominatim.openstreetmap.org/reverse?accept-language=en&addressdetails=1&format=json&lat=#{latitude}&lon=#{longitude}" } + let(:geocoder_response) { File.read(Decidim::Dev.asset("geocoder_result_osm.json")) } + + before do + stub_request(:get, geocoder_request_url).with( + headers: { + "Accept" => "*/*", + "Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3", + "User-Agent" => "Ruby" + } + ).to_return(body: geocoder_response) + end + + scenario "shows breadcrumb with conference and program" do + visit decidim_conferences.conference_conference_program_path(participatory_space, meetings_component, locale: I18n.locale) + + within ".menu-bar" do + expect(page).to have_content("Conferences") + expect(page).to have_content(translated(participatory_space.title)) + expect(page).to have_content("Program") + end + end + + scenario "shows breadcrumb with conference, program, and meeting" do + visit decidim_conferences.conference_conference_program_path(participatory_space, meetings_component, locale: I18n.locale) + click_on decidim_sanitize_translated(meeting.title) + + within ".menu-bar" do + expect(page).to have_content("Conferences") + expect(page).to have_content(translated(participatory_space.title)) + expect(page).to have_content("Program") + expect(page).to have_content(translated_attribute(meeting.title)) + end + end + end +end diff --git a/decidim-conferences/spec/types/conference_type_spec.rb b/decidim-conferences/spec/types/conference_type_spec.rb index d12a166b469e3..121de1003b4bb 100644 --- a/decidim-conferences/spec/types/conference_type_spec.rb +++ b/decidim-conferences/spec/types/conference_type_spec.rb @@ -204,8 +204,8 @@ module Conferences context "when registrations are disabled" do let(:registrations_enabled) { false } - it "does not return any registration type" do - expect(response["registrationTypes"]).to eq([nil]) + it "throws Decidim::Api::Errors::UnauthorizedObjectError" do + expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedObjectError, "You cannot view or edit this ConferenceRegistrationType because you do not have permissions") end end diff --git a/decidim-conferences/spec/types/integration_schema_spec.rb b/decidim-conferences/spec/types/integration_schema_spec.rb index 3e3ae4df8daf6..532f084d93929 100644 --- a/decidim-conferences/spec/types/integration_schema_spec.rb +++ b/decidim-conferences/spec/types/integration_schema_spec.rb @@ -131,6 +131,7 @@ ) end + include_examples "when the introspection is disabled" describe "valid query" do it "executes successfully" do expect { response }.not_to raise_error diff --git a/decidim-core/app/cells/decidim/attachments_file_tab/show.erb b/decidim-core/app/cells/decidim/attachments_file_tab/show.erb index 05bf98426f02e..22ce3b4998dfc 100644 --- a/decidim-core/app/cells/decidim/attachments_file_tab/show.erb +++ b/decidim-core/app/cells/decidim/attachments_file_tab/show.erb @@ -1,3 +1,3 @@
- <%= form.upload :file, button_class: "button button__sm button__transparent-secondary" %> + <%= form.upload :file, attachments: form.object.file.present? ? [form.object.file] : [], button_class: "button button__sm button__transparent-secondary" %>
diff --git a/decidim-core/app/cells/decidim/content_blocks/menu_breadcrumb_last_activity/show.erb b/decidim-core/app/cells/decidim/content_blocks/menu_breadcrumb_last_activity/show.erb deleted file mode 100644 index cc547b6dd2b8c..0000000000000 --- a/decidim-core/app/cells/decidim/content_blocks/menu_breadcrumb_last_activity/show.erb +++ /dev/null @@ -1,10 +0,0 @@ -
- <%= cell( - "decidim/activities", - valid_activities, - **activities_options - ) %> -
-<%= link_to last_activities_path, class: "mt-2.5 button button__text-secondary" do %> - <%= t("decidim.content_blocks.last_activity.name") %><%= icon "arrow-right-line" %> -<% end %> diff --git a/decidim-core/app/cells/decidim/content_blocks/menu_breadcrumb_last_activity_cell.rb b/decidim-core/app/cells/decidim/content_blocks/menu_breadcrumb_last_activity_cell.rb deleted file mode 100644 index 0b69b1fec531f..0000000000000 --- a/decidim-core/app/cells/decidim/content_blocks/menu_breadcrumb_last_activity_cell.rb +++ /dev/null @@ -1,49 +0,0 @@ -# frozen_string_literal: true - -module Decidim - module ContentBlocks - # A cell to be rendered as a content block with the latest activities performed - # in a Decidim Organization. - class MenuBreadcrumbLastActivityCell < LastActivityCell - def show - return if current_user.blank? && current_organization&.force_users_to_authenticate_before_access_organization - - super - end - - private - - def activities - @activities ||= if model.is_a?(Decidim::Organization) - Decidim::LastActivity.new(model).query - else - Decidim::ParticipatorySpaceLastActivity.new(model).query - end.limit(activities_to_show * 6) - end - - # A MD5 hash of model attributes is needed because - # it ensures the cache version value will always be the same size - def cache_hash - hash = [] - hash << "decidim/content_blocks/menu_breadcrumb_last_activity" - hash << id_prefix - hash << Digest::SHA256.hexdigest(valid_activities.map(&:cache_key_with_version).to_s) - hash << I18n.locale.to_s - - hash.join(Decidim.cache_key_separator) - end - - def activities_options - @activities_options ||= { id_prefix: }.merge(options.slice(:hide_participatory_space)) - end - - def id_prefix - @id_prefix ||= options[:id_prefix] || model.respond_to?(:to_gid) ? model.to_gid.to_param : "menu-breadcrumb" - end - - def activities_to_show - 4 - end - end - end -end diff --git a/decidim-core/app/cells/decidim/content_blocks/participatory_space_metadata/content.erb b/decidim-core/app/cells/decidim/content_blocks/participatory_space_metadata/content.erb index 54b23bfe7bd43..0c1da81bef8d9 100644 --- a/decidim-core/app/cells/decidim/content_blocks/participatory_space_metadata/content.erb +++ b/decidim-core/app/cells/decidim/content_blocks/participatory_space_metadata/content.erb @@ -2,9 +2,9 @@ <% metadata_valued_items.each do |item| %> <% end %>
diff --git a/decidim-core/app/cells/decidim/participatory_space_private_user/show.erb b/decidim-core/app/cells/decidim/member/show.erb similarity index 100% rename from decidim-core/app/cells/decidim/participatory_space_private_user/show.erb rename to decidim-core/app/cells/decidim/member/show.erb diff --git a/decidim-core/app/cells/decidim/participatory_space_private_user_cell.rb b/decidim-core/app/cells/decidim/member_cell.rb similarity index 86% rename from decidim-core/app/cells/decidim/participatory_space_private_user_cell.rb rename to decidim-core/app/cells/decidim/member_cell.rb index cd6aac893d571..a4427a667a36f 100644 --- a/decidim-core/app/cells/decidim/participatory_space_private_user_cell.rb +++ b/decidim-core/app/cells/decidim/member_cell.rb @@ -2,7 +2,7 @@ module Decidim # This cell renders the card for an instance of an Assembly member - class ParticipatorySpacePrivateUserCell < Decidim::ViewModel + class MemberCell < Decidim::ViewModel property :name property :role property :nickname diff --git a/decidim-core/app/cells/decidim/participatory_space_dropdown_metadata/links.erb b/decidim-core/app/cells/decidim/participatory_space_dropdown_metadata/links.erb deleted file mode 100644 index ab02418703794..0000000000000 --- a/decidim-core/app/cells/decidim/participatory_space_dropdown_metadata/links.erb +++ /dev/null @@ -1,9 +0,0 @@ -<% if nav_items.present? %> - <% nav_items.each do |item| %> -
  • - <%= link_to item[:url] do %> - <%= item[:name] %><%= icon "arrow-right-line" %> - <% end %> -
  • - <% end %> -<% end %> diff --git a/decidim-core/app/cells/decidim/participatory_space_dropdown_metadata/metadata.erb b/decidim-core/app/cells/decidim/participatory_space_dropdown_metadata/metadata.erb deleted file mode 100644 index 71ff053bdbb89..0000000000000 --- a/decidim-core/app/cells/decidim/participatory_space_dropdown_metadata/metadata.erb +++ /dev/null @@ -1 +0,0 @@ - diff --git a/decidim-core/app/cells/decidim/participatory_space_dropdown_metadata/show.erb b/decidim-core/app/cells/decidim/participatory_space_dropdown_metadata/show.erb deleted file mode 100644 index 478ec9c6ab7d1..0000000000000 --- a/decidim-core/app/cells/decidim/participatory_space_dropdown_metadata/show.erb +++ /dev/null @@ -1,11 +0,0 @@ - diff --git a/decidim-core/app/cells/decidim/participatory_space_dropdown_metadata_cell.rb b/decidim-core/app/cells/decidim/participatory_space_dropdown_metadata_cell.rb deleted file mode 100644 index bbdb481dad922..0000000000000 --- a/decidim-core/app/cells/decidim/participatory_space_dropdown_metadata_cell.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -module Decidim - class ParticipatorySpaceDropdownMetadataCell < Decidim::ViewModel - private - - def nav_items_method = nil - - def nav_items - return [] if nav_items_method.blank? - return [] if (nav_items = try(nav_items_method, model)).blank? - - nav_items - end - - def title - decidim_escape_translated(model.try(:title) || model.try(:name) || "") - end - - def id - return "#{model.id}-mobile" if options[:mobile] - - model.id - end - end -end diff --git a/decidim-core/app/cells/decidim/share_widget/modal.erb b/decidim-core/app/cells/decidim/share_widget/modal.erb index 61a0573556414..1c94de87b2df8 100644 --- a/decidim-core/app/cells/decidim/share_widget/modal.erb +++ b/decidim-core/app/cells/decidim/share_widget/modal.erb @@ -1,6 +1,6 @@ <%= decidim_modal id: "socialShare", class: "share-modal" do %>
    -

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

    +

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

    diff --git a/decidim-core/app/cells/decidim/statistic/show.erb b/decidim-core/app/cells/decidim/statistic/show.erb index 31ba229089d2b..9bb0bb460dabf 100644 --- a/decidim-core/app/cells/decidim/statistic/show.erb +++ b/decidim-core/app/cells/decidim/statistic/show.erb @@ -1,9 +1,9 @@
    -
    +

    <%= stat_title %> -

    +

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

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

    +

    <% else %>

    <% end %> -
    +

    <%= stat_number %> -

    +

    diff --git a/decidim-core/app/cells/decidim/upload_modal/files.erb b/decidim-core/app/cells/decidim/upload_modal/files.erb index d56cabe7df66b..651da3aacd0b2 100644 --- a/decidim-core/app/cells/decidim/upload_modal/files.erb +++ b/decidim-core/app/cells/decidim/upload_modal/files.erb @@ -1,9 +1,9 @@
    - <%= label %> + <%= options[:paragraph] == true ? paragraph : label %> <% if options[:help_text].present? %> - <%= options[:help_text] %> +

    <%= options[:help_text] %>

    <% end %> <%# NOTE: this block is about wrapping a default image for the avatar with the new styles, @@ -22,24 +22,28 @@
    <% attachments.each do |attachment| %> <% next if [Array, Hash].any? { |klass| attachment.is_a? klass } %> + <% is_persisted_attachment = attachment.is_a?(Decidim::Attachment) && attachment.persisted? %> + <% attachment_blob = blob(attachment) %> -
    - <% if file_attachment_path(attachment) && blob(attachment).image? %> +
    data-attachment-id="<%= attachment.id %>"<% end %> data-title="<%= title_for(attachment) %>" data-filename="<%= file_name_for(attachment) %>" data-state="uploaded" data-hidden-field="<%= attachment_blob&.signed_id %>"> + <% if file_attachment_path(attachment) && attachment_blob&.image? %>
    <%= image_tag(file_attachment_path(attachment), alt: "") %>
    <% elsif uploader_default_image_path(attribute).present? %>
    <%= image_tag uploader_default_image_path(attribute) %>
    <% end %> <% if has_title? %> - <%= title_for(attachment) %> - <%= form.hidden_field attribute, multiple: true, value: attachment.id, id: attachment.id %> +

    <%= title_for(attachment) %>

    <% else %> - <% if blob(attachment).image? %> - <%= title_for(attachment) %> + <% if attachment_blob&.image? %> +

    <%= title_for(attachment) %>

    <% else %> <%= link_to title_for(attachment), file_attachment_path(attachment), class: "w-full break-all mb-2" %> <% end %> <% end %> + <% if attachment_blob.present? %> + <%= form.hidden_field attribute, value: attachment_blob.signed_id, id: "hidden_#{attribute}_#{attachment_blob.id}" %> + <% end %>
    <% end %>
    diff --git a/decidim-core/app/cells/decidim/upload_modal_cell.rb b/decidim-core/app/cells/decidim/upload_modal_cell.rb index 6f743d00ae465..055d4d1df8081 100644 --- a/decidim-core/app/cells/decidim/upload_modal_cell.rb +++ b/decidim-core/app/cells/decidim/upload_modal_cell.rb @@ -30,6 +30,10 @@ def label form.send(:custom_label, attribute, options[:label], { required: required?, for: nil }) end + def paragraph + form.send(:custom_paragraph, attribute, options[:label], { required: required? }) + end + def button_label return button_edit_label if attachments.count.positive? @@ -71,13 +75,16 @@ def required? end # By default FoundationRailsHelper adds form errors next to input, but since input is in the modal - # and modal is hidden by default, we need to add an additional validation field to the form. + # and modal is hidden by default, we add a hidden checkbox field to handle HTML5 validation. # This should only be necessary when file is required by the form. + # Note: Validation errors are now displayed in the main form area, not inside the modal. def input_validation_field object_name = form.object.present? ? "#{form.object.model_name.param_key}[#{add_attribute}_validation]" : "#{add_attribute}_validation" - input = check_box_tag object_name, 1, attachments.present?, class: "reset-defaults", hidden: true, label: false, required: required? - message = form.send(:abide_error_element, add_attribute) + form.send(:error_and_help_text, add_attribute) - input + message + check_box_tag object_name, 1, attachments.present?, class: "reset-defaults", hidden: true, label: false, required: required?, id: validation_field_id + end + + def validation_field_id + "#{attribute}_validation" end def explanation diff --git a/decidim-core/app/commands/decidim/create_follow.rb b/decidim-core/app/commands/decidim/create_follow.rb index 0be0c622378d5..523234116c2b6 100644 --- a/decidim-core/app/commands/decidim/create_follow.rb +++ b/decidim-core/app/commands/decidim/create_follow.rb @@ -20,6 +20,7 @@ def initialize(form) # Returns nothing. def call return broadcast(:invalid) if form.invalid? + return broadcast(:invalid) unless current_user.is_a?(Decidim::User) create_follow! increment_score @@ -32,13 +33,10 @@ def call attr_reader :follow, :form def create_follow! - @follow = Follow.find_by( + @follow = Follow.where( followable: form.followable, user: current_user - ) || Follow.create!( - followable: form.followable, - user: current_user - ) + ).first_or_create! end def increment_score diff --git a/decidim-core/app/commands/decidim/destroy_account.rb b/decidim-core/app/commands/decidim/destroy_account.rb index 6199f2ffbf6cb..8ad711b72fc0d 100644 --- a/decidim-core/app/commands/decidim/destroy_account.rb +++ b/decidim-core/app/commands/decidim/destroy_account.rb @@ -28,7 +28,7 @@ def call destroy_user_badges destroy_user_likes destroy_user_reports - destroy_participatory_space_private_user + destroy_member delegate_destroy_to_participatory_spaces end @@ -101,8 +101,8 @@ def destroy_follows Decidim::Follow.where(user: current_user).find_each(&:destroy) end - def destroy_participatory_space_private_user - Decidim::ParticipatorySpacePrivateUser.where(user: current_user).find_each(&:destroy) + def destroy_member + Decidim::ParticipatorySpace::Member.where(user: current_user).find_each(&:destroy) end def delegate_destroy_to_participatory_spaces diff --git a/decidim-core/app/controllers/concerns/decidim/has_members_page.rb b/decidim-core/app/controllers/concerns/decidim/has_members_page.rb deleted file mode 100644 index f0ff4f9ebd135..0000000000000 --- a/decidim-core/app/controllers/concerns/decidim/has_members_page.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -require "active_support/concern" - -module Decidim - module HasMembersPage - extend ActiveSupport::Concern - - included do - helper_method :collection - - private - - def can_visit_index? - current_user_can_visit_space? && current_participatory_space.members_public_page? - end - - def members - @members ||= current_participatory_space.participatory_space_private_users.published - end - - alias_method :collection, :members - end - end -end diff --git a/decidim-core/app/controllers/concerns/decidim/participatory_space/has_members_page.rb b/decidim-core/app/controllers/concerns/decidim/participatory_space/has_members_page.rb new file mode 100644 index 0000000000000..edda01621be6c --- /dev/null +++ b/decidim-core/app/controllers/concerns/decidim/participatory_space/has_members_page.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "active_support/concern" + +module Decidim + module ParticipatorySpace + module HasMembersPage + extend ActiveSupport::Concern + + included do + helper_method :collection + + private + + def can_visit_index? + current_user_can_visit_space? && current_participatory_space.members_public_page? + end + + def members + @members ||= current_participatory_space.members.published + end + + alias_method :collection, :members + end + end + end +end diff --git a/decidim-core/app/controllers/concerns/decidim/participatory_space_context.rb b/decidim-core/app/controllers/concerns/decidim/participatory_space_context.rb index 931f7fda3e75c..b4e604f49095d 100644 --- a/decidim-core/app/controllers/concerns/decidim/participatory_space_context.rb +++ b/decidim-core/app/controllers/concerns/decidim/participatory_space_context.rb @@ -36,8 +36,6 @@ def current_participatory_space # title of the space (mandatory). # * url - The url of the resource (optional). # * active - Whether the item is active (optional). - # * dropdown_cell - When this value is present is used to generate a dropdown - # associated to the item (optional). # * resource - The resource of the item. This value is passed to the # dropdown cell, so it is mandatory if the dropdown cell is # present. @@ -48,7 +46,6 @@ def current_participatory_space_breadcrumb_item label: current_participatory_space.title, url: Decidim::ResourceLocatorPresenter.new(current_participatory_space).path, active: true, - dropdown_cell: current_participatory_space_manifest.breadcrumb_cell, resource: current_participatory_space } end diff --git a/decidim-core/app/controllers/decidim/components/base_controller.rb b/decidim-core/app/controllers/decidim/components/base_controller.rb index b07f684295f15..da45894553027 100644 --- a/decidim-core/app/controllers/decidim/components/base_controller.rb +++ b/decidim-core/app/controllers/decidim/components/base_controller.rb @@ -35,7 +35,7 @@ class BaseController < Decidim::ApplicationController before_action :redirect_unless_feature_private - before_action :set_component_breadcrumb_item + before_action :set_breadcrumb_items def current_participatory_space request.env["decidim.current_participatory_space"] @@ -66,7 +66,7 @@ def redirect_unless_feature_private raise ActionController::RoutingError, "Not Found" unless current_user_can_visit_space? end - def set_component_breadcrumb_item + def set_breadcrumb_items context_breadcrumb_items << add_current_component context_breadcrumb_items << add_parent_breadcrumb_item context_breadcrumb_items << add_breadcrumb_item diff --git a/decidim-core/app/events/decidim/welcome_notification_event.rb b/decidim-core/app/events/decidim/welcome_notification_event.rb index d29f4dfcb76ea..687e6fd49ce5f 100644 --- a/decidim-core/app/events/decidim/welcome_notification_event.rb +++ b/decidim-core/app/events/decidim/welcome_notification_event.rb @@ -43,7 +43,7 @@ def resource_title def interpolate(template) template - .gsub("{{name}}", user.name) + .gsub("{{name}}", user.presenter.name) .gsub("{{organization}}", organization_name(organization)) .gsub("{{help_url}}", url_helpers.pages_url(host: organization.host, locale: I18n.locale)) .gsub("{{badges_url}}", url_helpers.gamification_badges_url(host: organization.host)) diff --git a/decidim-core/app/helpers/decidim/amendments_helper.rb b/decidim-core/app/helpers/decidim/amendments_helper.rb index 1873619b9dc9d..29fa088e71bd2 100644 --- a/decidim-core/app/helpers/decidim/amendments_helper.rb +++ b/decidim-core/app/helpers/decidim/amendments_helper.rb @@ -17,9 +17,9 @@ def emendation_announcement_for(emendation) end # Checks if the user can participate in a participatory space - # based on its settings related with Decidim::HasPrivateUsers. + # based on its settings related with Decidim::ParticipatorySpace::HasMembers. def can_participate_in_private_space? - return true unless current_participatory_space.class.included_modules.include?(HasPrivateUsers) + return true unless current_participatory_space.class.included_modules.include?(Decidim::ParticipatorySpace::HasMembers) current_participatory_space.can_participate?(current_user) end diff --git a/decidim-core/app/helpers/decidim/menu_helper.rb b/decidim-core/app/helpers/decidim/menu_helper.rb index d84f6aeab96b5..d31e9aaf0ea6e 100644 --- a/decidim-core/app/helpers/decidim/menu_helper.rb +++ b/decidim-core/app/helpers/decidim/menu_helper.rb @@ -69,10 +69,10 @@ def menu_highlighted_participatory_process # The queries already include the order by weight Decidim::ParticipatoryProcesses::OrganizationParticipatoryProcesses.new(current_organization) | Decidim::ParticipatoryProcesses::PromotedParticipatoryProcesses.new - ).select(&:published?).map { |process| remove_private_space_if_not_private_user(process) }&.compact&.first + ).select(&:published?).map { |process| remove_private_space_if_not_member(process) }&.compact&.first end - def remove_private_space_if_not_private_user(process) + def remove_private_space_if_not_member(process) return nil if process.private_space == true && !process.can_participate?(current_user) process diff --git a/decidim-core/app/models/decidim/attachment.rb b/decidim-core/app/models/decidim/attachment.rb index d13e5ce54dec3..35d84fea9712c 100644 --- a/decidim-core/app/models/decidim/attachment.rb +++ b/decidim-core/app/models/decidim/attachment.rb @@ -9,7 +9,7 @@ class Attachment < ApplicationRecord include Traceable before_save :set_content_type_and_size, if: :attached? - before_validation :set_link_content_type_and_size, if: :link? + before_validation :set_link_content_type_and_size, if: :editable_link? translatable_fields :title, :description belongs_to :attachment_collection, class_name: "Decidim::AttachmentCollection", optional: true @@ -69,6 +69,13 @@ def link? link.present? end + # Whether this attachment is a link that can be edited or not. + # + # Returns Boolean. + def editable_link? + !destroyed? && !frozen? && link? + end + # Whether this attachment has a file or not. # # Returns Boolean. @@ -130,5 +137,12 @@ def set_link_content_type_and_size def self.log_presenter_class_for(_log) Decidim::AdminLog::AttachmentPresenter end + + def can_participate?(user) + return true unless attached_to + return true unless attached_to.respond_to?(:can_participate?) + + attached_to.can_participate?(user) + end end end diff --git a/decidim-core/app/models/decidim/component.rb b/decidim-core/app/models/decidim/component.rb index eb2b04d203c04..af7604c5acb66 100644 --- a/decidim-core/app/models/decidim/component.rb +++ b/decidim-core/app/models/decidim/component.rb @@ -108,6 +108,7 @@ def can_participate_in_space?(user) participatory_space.can_participate?(user) end + alias can_participate? can_participate_in_space? def private_non_transparent_space? return false unless participatory_space.respond_to?(:private_space?) diff --git a/decidim-core/app/models/decidim/participatory_space/member.rb b/decidim-core/app/models/decidim/participatory_space/member.rb new file mode 100644 index 0000000000000..1be8ee4ed73f2 --- /dev/null +++ b/decidim-core/app/models/decidim/participatory_space/member.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module Decidim + module ParticipatorySpace + # This class gives a given User access to a given private Member + class Member < ApplicationRecord + include Decidim::DownloadYourData + include ParticipatorySpaceUser + include Decidim::TranslatableResource + + belongs_to :participatory_space, polymorphic: true + + translatable_fields :role + + delegate :email, :name, to: :user + + scope :by_participatory_space, ->(participatory_space) { where(participatory_space_id: participatory_space.id, participatory_space_type: participatory_space.class.to_s) } + scope :published, -> { where(published: true) } + + def self.user_collection(user) + where(decidim_user_id: user.id) + end + + def self.member_ids_for_participatory_spaces(spaces) + joins(:user).where(participatory_space: spaces).distinct.pluck(:decidim_user_id) + end + + def self.export_serializer + Decidim::DownloadYourDataSerializers::DownloadYourDataMemberSerializer + end + + def self.log_presenter_class_for(_log) + Decidim::AdminLog::ParticipatorySpace::MemberPresenter + end + + ransacker :invitation_sent_at do + Arel.sql(%{("invitation_sent_at")::text}) + end + + def self.ransackable_attributes(auth_object = nil) + return [] unless auth_object&.admin? + + %w(name nickname email invitation_accepted_at last_sign_in_at invitation_sent_at role) + end + + def self.ransackable_associations(_auth_object = nil) + %w(user) + end + + def target_space_association = :participatory_space + end + end +end diff --git a/decidim-core/app/models/decidim/participatory_space_private_user.rb b/decidim-core/app/models/decidim/participatory_space_private_user.rb deleted file mode 100644 index cd8676f037dd2..0000000000000 --- a/decidim-core/app/models/decidim/participatory_space_private_user.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -module Decidim - # This class gives a given User access to a given private ParticipatorySpacePrivateUser - class ParticipatorySpacePrivateUser < ApplicationRecord - include Decidim::DownloadYourData - include ParticipatorySpaceUser - include Decidim::TranslatableResource - - belongs_to :privatable_to, polymorphic: true - - translatable_fields :role - - delegate :email, :name, to: :user - - scope :by_participatory_space, ->(privatable_to) { where(privatable_to_id: privatable_to.id, privatable_to_type: privatable_to.class.to_s) } - scope :published, -> { where(published: true) } - - def self.user_collection(user) - where(decidim_user_id: user.id) - end - - def self.private_user_ids_for_participatory_spaces(spaces) - joins(:user).where(privatable_to: spaces).distinct.pluck(:decidim_user_id) - end - - def self.export_serializer - Decidim::DownloadYourDataSerializers::DownloadYourDataParticipatorySpacePrivateUserSerializer - end - - def self.log_presenter_class_for(_log) - Decidim::AdminLog::ParticipatorySpacePrivateUserPresenter - end - - ransacker :invitation_sent_at do - Arel.sql(%{("invitation_sent_at")::text}) - end - - def self.ransackable_attributes(auth_object = nil) - return [] unless auth_object&.admin? - - %w(name nickname email invitation_accepted_at last_sign_in_at invitation_sent_at role) - end - - def self.ransackable_associations(_auth_object = nil) - %w(user) - end - - def target_space_association = :privatable_to - end -end diff --git a/decidim-core/app/models/decidim/user_base_entity.rb b/decidim-core/app/models/decidim/user_base_entity.rb index f69d8048d7633..4532c4dfde91b 100644 --- a/decidim-core/app/models/decidim/user_base_entity.rb +++ b/decidim-core/app/models/decidim/user_base_entity.rb @@ -20,7 +20,7 @@ class UserBaseEntity < ApplicationRecord has_one :blocking, class_name: "Decidim::UserBlock", foreign_key: :id, primary_key: :block_id, dependent: :destroy # Regex for name & nickname format validations - REGEXP_NAME = /\A(?!.*[<>?%&\^*#@()\[\]=+:;"{}\\|])/ + REGEXP_NAME = /\A(?!.*[<>?%&\^*#@()\[\]=+:;"{}\\|\n\r])/m REGEXP_NICKNAME = /\A[a-z0-9_-]+\z/ has_one_attached :avatar diff --git a/decidim-core/app/packs/src/decidim/controllers/assign_role/assign_role.test.js b/decidim-core/app/packs/src/decidim/controllers/assign_role/assign_role.test.js new file mode 100644 index 0000000000000..5d0602dbc2936 --- /dev/null +++ b/decidim-core/app/packs/src/decidim/controllers/assign_role/assign_role.test.js @@ -0,0 +1,80 @@ +/* global global, jest */ + +import { Application } from "@hotwired/stimulus"; +import AssignRoleController from "src/decidim/controllers/assign_role/controller"; + +describe("AssignRoleController", () => { + let application = null; + let element = null; + let controller = null; + + beforeEach(() => { + document.body.innerHTML = ` +
    + `; + + application = Application.start(); + application.register("assign-role", AssignRoleController); + + element = document.querySelector('[data-controller="assign-role"]'); + + return new Promise((resolve) => { + setTimeout(() => { + controller = application.getControllerForElementAndIdentifier(element, "assign-role"); + resolve(); + }, 0); + }); + }); + + afterEach(() => { + application.stop(); + document.body.innerHTML = ""; + jest.useRealTimers(); + jest.restoreAllMocks(); + }); + + describe("connect", () => { + it("sets the role attribute after the delay", () => { + controller.disconnect(); + jest.useFakeTimers(); + + controller.connect(); + + expect(element.getAttribute("role")).toBeNull(); + jest.advanceTimersByTime(300); + expect(element.getAttribute("role")).toBe("navigation"); + }); + + it("returns early when no data-role is provided", () => { + controller.disconnect(); + + element.dataset.role = ""; + const setTimeoutSpy = jest.spyOn(global, "setTimeout"); + jest.useFakeTimers(); + + controller.connect(); + + expect(element.getAttribute("role")).toBeNull(); + expect(setTimeoutSpy).not.toHaveBeenCalled(); + jest.advanceTimersByTime(300); + expect(element.getAttribute("role")).toBeNull(); + }); + }); + + describe("disconnect", () => { + it("clears any pending timeout", () => { + controller.disconnect(); + jest.useFakeTimers(); + + const clearTimeoutSpy = jest.spyOn(global, "clearTimeout"); + + controller.connect(); + controller.disconnect(); + + expect(clearTimeoutSpy).toHaveBeenCalled(); + + jest.advanceTimersByTime(300); + expect(element.getAttribute("role")).toBeNull(); + }); + }); +}); diff --git a/decidim-core/app/packs/src/decidim/controllers/assign_role/controller.js b/decidim-core/app/packs/src/decidim/controllers/assign_role/controller.js new file mode 100644 index 0000000000000..9d5769ab80b92 --- /dev/null +++ b/decidim-core/app/packs/src/decidim/controllers/assign_role/controller.js @@ -0,0 +1,27 @@ +import { Controller } from "@hotwired/stimulus" + +/** + * This controller is used to set a role attribute for any element where is being assigned. + * It requires a data-role attribute with the value of the role attribute to be set. + * We are using to change the value "menu" of role attribute set by a11y on div dropdown-menu-account and + * dropdown-menu-account-mobile which are inappropriate for accessibility + */ +export default class extends Controller { + connect() { + const role = this.element.dataset.role + + if (!role) { + return + } + + this.timeoutId = setTimeout(() => { + this.element.setAttribute("role", role) + }, 300) + } + + disconnect() { + if (this.timeoutId) { + clearTimeout(this.timeoutId) + } + } +} diff --git a/decidim-core/app/packs/src/decidim/controllers/main_menu/controller.js b/decidim-core/app/packs/src/decidim/controllers/main_menu/controller.js new file mode 100644 index 0000000000000..f7b5fb7836c4f --- /dev/null +++ b/decidim-core/app/packs/src/decidim/controllers/main_menu/controller.js @@ -0,0 +1,115 @@ +import { Controller } from "@hotwired/stimulus" + +const OPEN_DELAY_MS = 50 + +/** + * Main menu dropdown controller and traps page scroll while the menu is open. + * + * Expected markup: + * - The controller element has a `data-target` attribute with the menu container id. + * - The menu container uses `aria-hidden="true|false"` for visibility. + * - An optional close button exists with id `main-dropdown-summary-desktop-close`. + */ +export default class extends Controller { + connect() { + this.menuButton = this.element + this.menuContainer = document.getElementById(this.element.dataset.target) + this.closeButton = document.getElementById(this.element.dataset.closeButton) + + if (!this.menuContainer) { + return; + } + + this.handleContainerClick = this.handleContainerClick.bind(this) + this.handleButtonClick = this.handleButtonClick.bind(this) + this.handleKeydown = this.handleKeydown.bind(this) + this.handleCloseButtonClick = this.handleCloseButtonClick.bind(this) + + this.menuButton.addEventListener("click", this.handleButtonClick) + this.menuContainer.addEventListener("click", this.handleContainerClick) + + + document.addEventListener("keydown", this.handleKeydown) + if (this.closeButton) { + this.closeButton.addEventListener("click", this.handleCloseButtonClick) + } + } + + disconnect() { + if (!this.menuContainer) { + return; + } + + this.menuButton.removeEventListener("click", this.handleButtonClick) + this.menuContainer.removeEventListener("click", this.handleContainerClick) + document.removeEventListener("keydown", this.handleKeydown) + if (this.closeButton) { + this.closeButton.removeEventListener("click", this.handleCloseButtonClick) + } + if (!this.isHidden()) { + this.closeMenu(); + } + } + + + handleContainerClick(event) { + if (this.isHidden()) { + return; + } + if (event.target !== this.menuContainer) { + return; + } + this.closeMenu() + } + + handleButtonClick() { + if (!this.isHidden()) { + return; + } + + setTimeout(() => { + this.openMenu() + window.scrollTo({ top: 0, behavior: "smooth" }) + }, OPEN_DELAY_MS) + } + + handleKeydown(event) { + if (event.key !== "Escape") { + return; + } + if (this.isHidden()) { + return; + } + + this.closeMenu() + } + + handleCloseButtonClick() { + if (this.isHidden()) { + return; + } + + this.closeMenu(); + } + + isHidden() { + return this.menuContainer.getAttribute("aria-hidden") === "true" + } + + openMenu() { + if (typeof this.previousBodyOverflow === "undefined") { + this.previousBodyOverflow = document.body.style.overflow; + } + document.body.style.overflow = "hidden" + this.element.setAttribute("aria-expanded", "true") + this.menuContainer.setAttribute("aria-hidden", "false") + this.menuContainer.setAttribute("aria-modal", "true") + } + + closeMenu() { + document.body.style.overflow = this.previousBodyOverflow ?? "" + this.element.setAttribute("aria-expanded", "false") + this.menuContainer.setAttribute("aria-hidden", "true") + this.menuContainer.removeAttribute("aria-modal") + } +} diff --git a/decidim-core/app/packs/src/decidim/controllers/main_menu/main_menu.test.js b/decidim-core/app/packs/src/decidim/controllers/main_menu/main_menu.test.js new file mode 100644 index 0000000000000..39d38bd82cfbc --- /dev/null +++ b/decidim-core/app/packs/src/decidim/controllers/main_menu/main_menu.test.js @@ -0,0 +1,185 @@ +/* global jest */ +import { Application } from "@hotwired/stimulus" +import MainMenuController from "src/decidim/controllers/main_menu/controller" + +describe("MainMenuController", () => { + let application = null + let controller = null + let menuButton = null + let menuContainer = null + let closeButton = null + + const buildDom = () => { + document.body.innerHTML = ` + + + + ` + } + + const startController = () => new Promise((resolve) => { + setTimeout(() => { + controller = application.getControllerForElementAndIdentifier(menuButton, "main-menu") + resolve() + }, 0) + }) + + beforeEach(() => { + application = Application.start() + application.register("main-menu", MainMenuController) + buildDom() + + menuButton = document.querySelector('[data-controller="main-menu"]') + menuContainer = document.getElementById("main-menu-container") + closeButton = document.getElementById("main-menu-close") + + jest.spyOn(window, "scrollTo").mockImplementation(() => {}) + + return startController() + }) + + afterEach(() => { + application.stop() + document.body.innerHTML = "" + jest.useRealTimers() + jest.restoreAllMocks() + }) + + describe("connect", () => { + it("binds expected event listeners", () => { + const buttonSpy = jest.spyOn(menuButton, "addEventListener") + const documentSpy = jest.spyOn(document, "addEventListener") + const closeSpy = jest.spyOn(closeButton, "addEventListener") + + controller.disconnect() + controller.connect() + + expect(buttonSpy).toHaveBeenCalledWith("click", controller.handleButtonClick) + expect(documentSpy).toHaveBeenCalledWith("keydown", controller.handleKeydown) + expect(closeSpy).toHaveBeenCalledWith("click", controller.handleCloseButtonClick) + }) + + it("returns early when menu container is missing", async () => { + document.body.innerHTML = ` + + ` + + application.stop() + application = Application.start() + application.register("main-menu", MainMenuController) + + const missingButton = document.querySelector('[data-controller="main-menu"]') + const missingController = await new Promise((resolve) => { + setTimeout(() => { + resolve(application.getControllerForElementAndIdentifier(missingButton, "main-menu")) + }, 0) + }) + + expect(missingController.menuContainer).toBeNull() + expect(() => missingController.disconnect()).not.toThrow() + }) + }) + + describe("handleButtonClick", () => { + it("opens the menu after the delay and scrolls to top", () => { + jest.useFakeTimers() + document.body.style.overflow = "scroll" + + controller.handleButtonClick() + jest.advanceTimersByTime(50) + + expect(menuButton.getAttribute("aria-expanded")).toBe("true") + expect(menuContainer.getAttribute("aria-hidden")).toBe("false") + expect(document.body.style.overflow).toBe("hidden") + expect(window.scrollTo).toHaveBeenCalledWith({ top: 0, behavior: "smooth" }) + }) + + it("does nothing when menu is already open", () => { + jest.useFakeTimers() + controller.openMenu() + + controller.handleButtonClick() + jest.advanceTimersByTime(50) + + expect(window.scrollTo).not.toHaveBeenCalled() + expect(menuContainer.getAttribute("aria-hidden")).toBe("false") + }) + }) + + describe("handleKeydown", () => { + it("closes the menu on Escape", () => { + document.body.style.overflow = "scroll" + controller.openMenu() + + controller.handleKeydown({ key: "Escape" }) + + expect(menuButton.getAttribute("aria-expanded")).toBe("false") + expect(menuContainer.getAttribute("aria-hidden")).toBe("true") + expect(document.body.style.overflow).toBe("scroll") + }) + + it("ignores non-escape keys", () => { + controller.openMenu() + + controller.handleKeydown({ key: "Enter" }) + + expect(menuContainer.getAttribute("aria-hidden")).toBe("false") + }) + }) + + describe("handleContainerClick", () => { + it("closes the menu when clicking the container", () => { + controller.openMenu() + + controller.handleContainerClick({ target: menuContainer }) + + expect(menuContainer.getAttribute("aria-hidden")).toBe("true") + }) + + it("does not close the menu when clicking inside the container", () => { + const childItem = document.getElementById("main-menu-item") + controller.openMenu() + + controller.handleContainerClick({ target: childItem }) + + expect(menuContainer.getAttribute("aria-hidden")).toBe("false") + }) + }) + + describe("handleCloseButtonClick", () => { + it("closes the menu when open", () => { + controller.openMenu() + + controller.handleCloseButtonClick() + + expect(menuContainer.getAttribute("aria-hidden")).toBe("true") + }) + }) + + describe("disconnect", () => { + it("removes listeners and closes the menu if open", () => { + const buttonSpy = jest.spyOn(menuButton, "removeEventListener") + const documentSpy = jest.spyOn(document, "removeEventListener") + const closeSpy = jest.spyOn(closeButton, "removeEventListener") + + document.body.style.overflow = "scroll" + controller.openMenu() + controller.disconnect() + + expect(buttonSpy).toHaveBeenCalledWith("click", controller.handleButtonClick) + expect(documentSpy).toHaveBeenCalledWith("keydown", controller.handleKeydown) + expect(closeSpy).toHaveBeenCalledWith("click", controller.handleCloseButtonClick) + expect(menuContainer.getAttribute("aria-hidden")).toBe("true") + expect(document.body.style.overflow).toBe("scroll") + }) + }) +}) diff --git a/decidim-core/app/packs/src/decidim/controllers/sticky_header/controller.js b/decidim-core/app/packs/src/decidim/controllers/sticky_header/controller.js deleted file mode 100644 index 98e0abb00dac0..0000000000000 --- a/decidim-core/app/packs/src/decidim/controllers/sticky_header/controller.js +++ /dev/null @@ -1,116 +0,0 @@ -import { Controller } from "@hotwired/stimulus" -import { screens } from "tailwindcss/defaultTheme" - -export default class extends Controller { - connect() { - this.prevScroll = window.scrollY; - - // Set the initial margin for the menu bar container - this.fixMenuBarContainerMargin(); - - // Attach event listeners for page load and window resize events - this.setupEventListeners(); - - // Set up the scroll event handler for sticky header behavior - this.setupScrollHandler(); - } - - /** - * Determines if the current screen size matches or is smaller than the specified breakpoint. - * Uses Tailwind CSS screen breakpoints for responsive behavior. - * - * @param {string} key - The Tailwind CSS screen breakpoint key (e.g., 'md', 'lg') - * @returns {boolean} True if the screen is at or below the specified breakpoint - */ - isMaxScreenSize(key) { - return window.matchMedia(`(max-width: ${screens[key]})`).matches; - } - - /** - * Dynamically adjusts the top margin of the menu bar container to accommodate - * the sticky header height. This prevents content from being hidden behind - * the fixed sticky header. - * - * The margin is only applied on mobile devices (screens smaller than 'md' breakpoint) - * to ensure proper spacing when multiple header elements are present, such as - * omnipresent banner, admin bar, and offline banner. - * @returns {void} - */ - fixMenuBarContainerMargin() { - // Locate the menu bar container element - const menuBarContainer = document.querySelector("#menu-bar-container"); - - // Calculate margin based on screen size and sticky header height - const marginTop = this.isMaxScreenSize("sm") - ? this.element.offsetHeight - : 0; - - // Apply the calculated margin to the menu bar container - if (menuBarContainer) { - menuBarContainer.style.marginTop = `${marginTop}px`; - } - } - - /** - * Sets up event listeners for page navigation and window resize events. - * Ensures the sticky header behaves correctly after page transitions - * and when the window is resized. - * @returns {void} - */ - setupEventListeners() { - // Handle window resizes events to recalculate margins for responsive behavior - window.addEventListener("resize", () => { - this.fixMenuBarContainerMargin(); - }); - } - - /** - * Sets up the scroll event handler that manages the sticky header visibility - * based on a scroll direction. Only initializes if the sticky header element exists. - * @returns {void} - */ - setupScrollHandler() { - // Attach scroll event listener for sticky header show/hide behavior - document.addEventListener("scroll", () => { - this.handleScroll(); - }); - } - - /** - * Handles scroll events to show or hide the sticky header based on scroll direction. - * The header is shown when scrolling up or near the top of the page, - * and hidden when scrolling down to maximize content visibility. - * - * Uses a scroll threshold of 5 pixels to prevent excessive toggling - * from minor scroll movements. - * @returns {void} - */ - handleScroll() { - // Continuously adjust menu bar margin to handle dynamic content changes - this.fixMenuBarContainerMargin(); - - // Check if the main bar element is visible (has offsetParent when visible) - const header = document.getElementById("main-bar")?.offsetParent; - - // Only proceed if header is visible and sticky header is in fixed position - if (header && window.getComputedStyle(this.element).position === "fixed") { - const currentScroll = window.scrollY; - const goingDown = this.prevScroll > currentScroll; - const change = Math.abs(this.prevScroll - currentScroll); - - // Apply scroll threshold to prevent excessive header toggling - if (change > 5) { - // Show header when scrolling up or when near the top of the page - if (goingDown || currentScroll < this.element.offsetHeight) { - this.element.style.top = "0"; - } else { - // Hide header when scrolling down by moving it above the viewport - this.element.style.top = `-${this.element.offsetHeight}px`; - } - - // Update previous scroll position for next comparison - this.prevScroll = currentScroll; - } - } - } -} diff --git a/decidim-core/app/packs/src/decidim/controllers/sticky_header/sticky_header.test.js b/decidim-core/app/packs/src/decidim/controllers/sticky_header/sticky_header.test.js deleted file mode 100644 index 1a34be107d02a..0000000000000 --- a/decidim-core/app/packs/src/decidim/controllers/sticky_header/sticky_header.test.js +++ /dev/null @@ -1,320 +0,0 @@ -/* eslint max-lines: ["error", 320] */ -/* global jest */ - -import { Application } from "@hotwired/stimulus" -import StickyHeaderController from "src/decidim/controllers/sticky_header/controller"; - -// Mock Tailwind CSS screens -jest.mock("tailwindcss/defaultTheme", () => ({ - screens: { - md: "768px", - lg: "1024px", - xl: "1280px" - } -})); - -describe("StickyHeader", () => { - let mockStickyHeaderElement = null; - let mockMenuBarContainer = null; - let mockMainBar = null; - let application = null; - let controller = null; - - beforeEach(() => { - application = Application.start(); - application.register("sticky-header", StickyHeaderController); - // Reset DOM - document.body.innerHTML = ""; - - // Create mock elements - mockStickyHeaderElement = document.createElement("div"); - mockStickyHeaderElement.setAttribute("data-controller", "sticky-header"); - mockStickyHeaderElement.style.position = "fixed"; - Reflect.defineProperty(mockStickyHeaderElement, "offsetHeight", { - value: 60, - writable: true - }); - - mockMenuBarContainer = document.createElement("div"); - mockMenuBarContainer.id = "menu-bar-container"; - - mockMainBar = document.createElement("div"); - mockMainBar.id = "main-bar"; - Reflect.defineProperty(mockMainBar, "offsetParent", { - value: mockMainBar, - writable: true - }); - - // Append elements to DOM - document.body.appendChild(mockStickyHeaderElement); - document.body.appendChild(mockMenuBarContainer); - document.body.appendChild(mockMainBar); - - // Mock window properties - Reflect.defineProperty(window, "scrollY", { - value: 0, - writable: true - }); - - // Mock window.matchMedia - window.matchMedia = jest.fn().mockImplementation((query) => ({ - matches: Boolean(query.includes("768px")), - media: query, - onchange: null, - addListener: jest.fn(), - removeListener: jest.fn(), - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn() - })); - - // Mock getComputedStyle - window.getComputedStyle = jest.fn().mockReturnValue({ - position: "fixed" - }); - - // Spy on event listeners - jest.spyOn(document, "addEventListener"); - jest.spyOn(window, "addEventListener"); - - // Wait for the controller to be connected - return new Promise((resolve) => { - setTimeout(() => { - controller = application.getControllerForElementAndIdentifier(mockStickyHeaderElement, "sticky-header"); - resolve(); - }, 0); - }); - }); - - afterEach(() => { - jest.clearAllMocks(); - document.body.innerHTML = ""; - }); - - describe("connect", () => { - it("should initialize with correct default values", () => { - window.scrollY = 100; - - controller.disconnect(); - controller.connect(); - - expect(controller.prevScroll).toBe(100); - expect(controller.element).toBe(mockStickyHeaderElement); - }); - - it("should call all setup methods", () => { - const fixMarginSpy = jest.spyOn(controller, "fixMenuBarContainerMargin"); - const setupListenersSpy = jest.spyOn(controller, "setupEventListeners"); - const setupScrollSpy = jest.spyOn(controller, "setupScrollHandler"); - - controller.disconnect(); - controller.connect(); - - expect(fixMarginSpy).toHaveBeenCalled(); - expect(setupListenersSpy).toHaveBeenCalled(); - expect(setupScrollSpy).toHaveBeenCalled(); - }); - }); - - describe("isMaxScreenSize", () => { - it("should return true for screens at or below breakpoint", () => { - window.matchMedia.mockReturnValue({ matches: true }); - - const result = controller.isMaxScreenSize("md"); - - expect(result).toBe(true); - expect(window.matchMedia).toHaveBeenCalledWith("(max-width: 768px)"); - }); - - it("should return false for screens above breakpoint", () => { - window.matchMedia.mockReturnValue({ matches: false }); - - const result = controller.isMaxScreenSize("md"); - - expect(result).toBe(false); - }); - - it("should handle different breakpoint keys", () => { - controller.isMaxScreenSize("lg"); - expect(window.matchMedia).toHaveBeenCalledWith("(max-width: 1024px)"); - }); - }); - - describe("fixMenuBarContainerMargin", () => { - - it("should set margin on mobile devices", () => { - window.matchMedia.mockReturnValue({ matches: true }); - - controller.fixMenuBarContainerMargin(); - - expect(mockMenuBarContainer.style.marginTop).toBe("60px"); - }); - - it("should not set margin on desktop devices", () => { - window.matchMedia.mockReturnValue({ matches: false }); - - controller.fixMenuBarContainerMargin(); - - expect(mockMenuBarContainer.style.marginTop).toBe("0px"); - }); - - it("should handle missing menu bar container", () => { - mockMenuBarContainer.remove(); - - expect(() => controller.fixMenuBarContainerMargin()).not.toThrow(); - }); - }); - - describe("setupEventListeners", () => { - it("should add resize event listener", () => { - controller.setupEventListeners(); - - expect(window.addEventListener).toHaveBeenCalledWith( - "resize", - expect.any(Function) - ); - }); - - it("should call fixMenuBarContainerMargin on resize", () => { - const fixMarginSpy = jest.spyOn(controller, "fixMenuBarContainerMargin"); - controller.setupEventListeners(); - - // Simulate resize event - const resizeHandler = window.addEventListener.mock.calls.find( - (call) => call[0] === "resize" - )[1]; - resizeHandler(); - - expect(fixMarginSpy).toHaveBeenCalled(); - }); - }); - - describe("setupScrollHandler", () => { - it("should add scroll event listener when sticky header exists", () => { - controller.setupScrollHandler(); - - expect(document.addEventListener).toHaveBeenCalledWith( - "scroll", - expect.any(Function) - ); - }); - - it("should call handleScroll on scroll event", () => { - const handleScrollSpy = jest.spyOn(controller, "handleScroll"); - controller.setupScrollHandler(); - - // Simulate scroll event - const scrollHandler = document.addEventListener.mock.calls.find( - (call) => call[0] === "scroll" - )[1]; - scrollHandler(); - - expect(handleScrollSpy).toHaveBeenCalled(); - }); - }); - - describe("handleScroll", () => { - beforeEach(() => { - controller.prevScroll = 0; - }); - - it("should call fixMenuBarContainerMargin", () => { - const fixMarginSpy = jest.spyOn(controller, "fixMenuBarContainerMargin"); - - controller.handleScroll(); - - expect(fixMarginSpy).toHaveBeenCalled(); - }); - - it("should return early if main bar has no offsetParent", () => { - Reflect.defineProperty(mockMainBar, "offsetParent", { value: null }); - - controller.handleScroll(); - - expect(mockStickyHeaderElement.style.top).toBe(""); - }); - - it("should return early if sticky header is not fixed", () => { - window.getComputedStyle.mockReturnValue({ position: "static" }); - - controller.handleScroll(); - - expect(mockStickyHeaderElement.style.top).toBe(""); - }); - - it("should show header when scrolling up", () => { - window.scrollY = 50; - controller.prevScroll = 100; - - controller.handleScroll(); - - expect(mockStickyHeaderElement.style.top).toBe("0px"); - expect(controller.prevScroll).toBe(50); - }); - - it("should hide header when scrolling down", () => { - window.scrollY = 150; - controller.prevScroll = 100; - - controller.handleScroll(); - - expect(mockStickyHeaderElement.style.top).toBe("-60px"); - expect(controller.prevScroll).toBe(150); - }); - - it("should show header when near top of page", () => { - // Less than offsetHeight (60) - window.scrollY = 30; - controller.prevScroll = 100; - - controller.handleScroll(); - - expect(mockStickyHeaderElement.style.top).toBe("0px"); - }); - - it("should not change header position for small scroll changes", () => { - // Change of 3 pixels (less than threshold of 5) - window.scrollY = 103; - controller.prevScroll = 100; - - controller.handleScroll(); - - expect(mockStickyHeaderElement.style.top).toBe(""); - // Should not update - expect(controller.prevScroll).toBe(100); - }); - - it("should handle missing main bar element", () => { - mockMainBar.remove(); - - expect(() => controller.handleScroll()).not.toThrow(); - }); - }); - - describe("integration tests", () => { - it("should properly initialize and handle scroll events", () => { - // Simulate scrolling down - window.scrollY = 200; - controller.handleScroll(); - expect(mockStickyHeaderElement.style.top).toBe("-60px"); - - // Simulate scrolling up - window.scrollY = 100; - controller.handleScroll(); - expect(mockStickyHeaderElement.style.top).toBe("0px"); - }); - - it("should handle responsive behavior correctly", () => { - // Mobile view - window.matchMedia.mockReturnValue({ matches: true }); - controller.disconnect(); - controller.connect(); - expect(mockMenuBarContainer.style.marginTop).toBe("60px"); - - // Desktop view - window.matchMedia.mockReturnValue({ matches: false }); - controller.fixMenuBarContainerMargin(); - expect(mockMenuBarContainer.style.marginTop).toBe("0px"); - }); - }); -}); diff --git a/decidim-core/app/packs/src/decidim/direct_uploads/upload_field.js b/decidim-core/app/packs/src/decidim/direct_uploads/upload_field.js index 02027cac228b0..8f4fd49f81b4d 100644 --- a/decidim-core/app/packs/src/decidim/direct_uploads/upload_field.js +++ b/decidim-core/app/packs/src/decidim/direct_uploads/upload_field.js @@ -79,7 +79,7 @@ const updateActiveUploads = (modal) => { const template = `
    ${(/image/).test(file.type) && "
    " || ""} - ${escapeHtml(title)} +

    ${escapeHtml(title)}

    ${hidden}
    ` diff --git a/decidim-core/app/packs/src/decidim/dropdown_menu.js b/decidim-core/app/packs/src/decidim/dropdown_menu.js deleted file mode 100644 index bb3ce7068eb82..0000000000000 --- a/decidim-core/app/packs/src/decidim/dropdown_menu.js +++ /dev/null @@ -1,79 +0,0 @@ -// changes the value "menu" of role attribute set by a11y on div dropdown-menu-account and -// dropdown-menu-account-mobile which are inappropriate for accessibility -document.addEventListener("turbo:load", () => { - const dropdownDiv = document.querySelector("#dropdown-menu-account"); - const dropdownMobileDiv = document.querySelector("#dropdown-menu-account-mobile"); - if (dropdownDiv) { - setTimeout(() => { - dropdownDiv.setAttribute("role", "dialog") - dropdownMobileDiv.setAttribute("role", "dialog") - }, 300) - } - const triggerButtonMobile = document.querySelector("#dropdown-trigger-links-mobile"); - if (triggerButtonMobile) { - triggerButtonMobile.addEventListener("click", () => { - dropdownMobileDiv.setAttribute("aria-modal", "true") - }) - } -}); - -const setMenuOpacity = (opacity) => { - const content = document.getElementById("content"); - const footer = document.querySelector("footer"); - const menuBar = document.getElementById("menu-bar-container"); - - if (content) { - content.style.opacity = opacity; - } - if (footer) { - footer.style.opacity = opacity; - } - if (menuBar) { - menuBar.style.opacity = opacity; - } -} - -const menuContainer = document.getElementById("dropdown-menu-main-desktop"); -const menuButton = document.getElementById("main-dropdown-summary-desktop"); - -if (menuButton && menuContainer) { - menuButton.addEventListener("click", function () { - const isHidden = menuContainer.getAttribute("aria-hidden") === "true"; - if (!isHidden) { - return; - } - setTimeout(() => { - setMenuOpacity("0.3"); - document.body.style.overflow = "hidden"; - menuContainer.setAttribute("aria-hidden", "false"); - window.scrollTo({ top: 0, behavior: "smooth" }); - }, 50); - }); -} - -if (menuContainer) { - document.addEventListener("keydown", function (event) { - if (event.key === "Escape") { - const isOpen = menuContainer.getAttribute("aria-hidden") === "true"; - - if (isOpen) { - menuContainer.setAttribute("aria-hidden", "true"); - setMenuOpacity("1"); - document.body.style.overflow = "scroll"; - } - } - }) - - document.addEventListener("click", function (event) { - const isOpen = menuContainer.getAttribute("aria-hidden") === "false"; - const closeMenuButton = document.getElementById("main-dropdown-summary-desktop-close"); - const clickedInsideMenu = menuContainer.contains(event.target); - const clickCloseButton = closeMenuButton && closeMenuButton.contains(event.target); - - if (isOpen && (!clickedInsideMenu || clickCloseButton)) { - menuContainer.setAttribute("aria-hidden", "true"); - setMenuOpacity("1"); - document.body.style.overflow = "scroll"; - } - }); -} diff --git a/decidim-core/app/packs/src/decidim/editor/common/suggestion.js b/decidim-core/app/packs/src/decidim/editor/common/suggestion.js index 0d8aea65d160b..b3c8b3821d94d 100644 --- a/decidim-core/app/packs/src/decidim/editor/common/suggestion.js +++ b/decidim-core/app/packs/src/decidim/editor/common/suggestion.js @@ -183,7 +183,7 @@ export const createSuggestionRenderer = (node, { itemConverter } = {}) => () => export const createNodeView = (self) => { return ({ node }) => { const dom = document.createElement("span"); - dom.textContent = self.options.renderLabel({ options: self.options, node }); + dom.textContent = self.options.renderText({ options: self.options, node }); const { id, label } = node.attrs; dom.dataset.suggestion = node.type.name; diff --git a/decidim-core/app/packs/src/decidim/editor/extensions/decidim_kit/index.js b/decidim-core/app/packs/src/decidim/editor/extensions/decidim_kit/index.js index 04228ee11bbef..a640c51c65ecf 100644 --- a/decidim-core/app/packs/src/decidim/editor/extensions/decidim_kit/index.js +++ b/decidim-core/app/packs/src/decidim/editor/extensions/decidim_kit/index.js @@ -43,7 +43,10 @@ export default Extension.create({ heading: false, bold: false, orderedList: false, - codeBlock: false + codeBlock: false, + link: false, + underline: false, + trailingNode: false }), CharacterCount.configure(this.options.characterCount), Link.configure({ openOnClick: false, ...this.options.link }), diff --git a/decidim-core/app/packs/src/decidim/editor/extensions/image/index.js b/decidim-core/app/packs/src/decidim/editor/extensions/image/index.js index 5df5399c3821b..69f0ed0b882bd 100644 --- a/decidim-core/app/packs/src/decidim/editor/extensions/image/index.js +++ b/decidim-core/app/packs/src/decidim/editor/extensions/image/index.js @@ -172,13 +172,16 @@ export default Image.extend({ return true; }, - handleDoubleClick() { - if (!editor.isActive("image")) { - return false; + handleDoubleClick(view, pos) { + const { state } = view; + const node = state.doc.nodeAt(pos); + + if (node && node.type.name === "image") { + editor.chain().focus().imageDialog().run(); + return true; } - editor.chain().focus().imageDialog().run(); - return true; + return false; }, handleDOMEvents: { diff --git a/decidim-core/app/packs/src/decidim/editor/extensions/link/index.js b/decidim-core/app/packs/src/decidim/editor/extensions/link/index.js index 6e46e9df576f4..964ae9b17823c 100644 --- a/decidim-core/app/packs/src/decidim/editor/extensions/link/index.js +++ b/decidim-core/app/packs/src/decidim/editor/extensions/link/index.js @@ -33,6 +33,14 @@ export default Link.extend({ } }, + renderHTML({ HTMLAttributes }) { + const attrs = { ...HTMLAttributes }; + if (attrs.target === "") { + Reflect.deleteProperty(attrs, "target"); + } + return ["a", attrs, 0]; + }, + addCommands() { const i18n = getDictionary("editor.extensions.link"); diff --git a/decidim-core/app/packs/src/decidim/editor/extensions/mention/index.js b/decidim-core/app/packs/src/decidim/editor/extensions/mention/index.js index 103abf955e051..1d6f503b28fb4 100644 --- a/decidim-core/app/packs/src/decidim/editor/extensions/mention/index.js +++ b/decidim-core/app/packs/src/decidim/editor/extensions/mention/index.js @@ -32,11 +32,10 @@ export default Mention.extend({ return { ...parentOptions, - renderLabel({ node }) { - // The labels are formed based on the nicknames returned by the API - // which already contain the suggestion character, so there is no need - // to display it twice. - return `${node.attrs.label ?? node.attrs.id}` + renderText({ node }) { + // renderText is used to create the DOM representation + const label = node.attrs.label ?? node.attrs.id; + return label; }, suggestion: { ...parentOptions?.suggestion, @@ -59,6 +58,20 @@ export default Mention.extend({ }; }, + renderHTML({ node }) { + // renderHTML is used for visual rendering getHTML() + const label = node.attrs.label ?? node.attrs.id; + return [ + "span", + { + "data-type": "mention", + "data-id": node.attrs.id, + "data-label": node.attrs.label + }, + label + ]; + }, + addNodeView() { return createNodeView(this); } diff --git a/decidim-core/app/packs/src/decidim/editor/extensions/mention_resource/index.js b/decidim-core/app/packs/src/decidim/editor/extensions/mention_resource/index.js index 0fe40a6b35377..4df8d17d24f58 100644 --- a/decidim-core/app/packs/src/decidim/editor/extensions/mention_resource/index.js +++ b/decidim-core/app/packs/src/decidim/editor/extensions/mention_resource/index.js @@ -27,18 +27,14 @@ const searchResources = async (queryText) => { export default Mention.extend({ name: "mentionResource", - addOptions() { const options = this.parent?.(); const suggestion = options?.suggestion; - return { ...options, - renderLabel({ node }) { - // The labels are formed based on the titles returned by the API - // which already contain the suggestion character, so there is no need - // to display it twice. - return `${node.attrs.label ?? node.attrs.id}` + renderText({ node }) { + // renderText is used to create the DOM representation + return node.attrs.label ?? node.attrs.id; }, suggestion: { ...suggestion, @@ -63,6 +59,19 @@ export default Mention.extend({ }; }, + renderHTML({ node }) { + // renderHTML is used for visual rendering getHTML() + return [ + "span", + { + "data-type": "mentionResource", + "data-id": node.attrs.id, + "data-label": node.attrs.label + }, + node.attrs.label ?? node.attrs.id + ]; + }, + addNodeView() { return createNodeView(this); } diff --git a/decidim-core/app/packs/src/decidim/editor/test/extensions/decidim_kit.test.js b/decidim-core/app/packs/src/decidim/editor/test/extensions/decidim_kit.test.js index 1e5d8350c53d5..b5516e2ac6856 100644 --- a/decidim-core/app/packs/src/decidim/editor/test/extensions/decidim_kit.test.js +++ b/decidim-core/app/packs/src/decidim/editor/test/extensions/decidim_kit.test.js @@ -32,15 +32,24 @@ describe("DecidimKit", () => { "code", "doc", "dropCursor", - "gapCursor", "hardBreak", - "history", + "paragraph", + "link", + "decidimKit", + "starterKit", "horizontalRule", "italic", "listItem", - "paragraph", "strike", - "text" + "text", + "characterCount", + "bold", + "dialog", + "indent", + "orderedList", + "codeBlock", + "underline", + "heading" ].forEach((name) => expect(extensions).toContain(name)); }); diff --git a/decidim-core/app/packs/src/decidim/editor/test/extensions/heading.test.js b/decidim-core/app/packs/src/decidim/editor/test/extensions/heading.test.js index 89dd4a7a99562..807ee4c8ad3d6 100644 --- a/decidim-core/app/packs/src/decidim/editor/test/extensions/heading.test.js +++ b/decidim-core/app/packs/src/decidim/editor/test/extensions/heading.test.js @@ -51,6 +51,6 @@ describe("Heading", () => { editorElement.focus(); await updateContent(editorElement, "# "); - expect(editor.getHTML()).toMatchHtml("

    "); + expect(editor.getHTML()).toMatchHtml("

    #

    "); }); }); diff --git a/decidim-core/app/packs/src/decidim/editor/test/extensions/image.test.js b/decidim-core/app/packs/src/decidim/editor/test/extensions/image.test.js index d2248318a4396..260b6393c98e2 100644 --- a/decidim-core/app/packs/src/decidim/editor/test/extensions/image.test.js +++ b/decidim-core/app/packs/src/decidim/editor/test/extensions/image.test.js @@ -28,24 +28,7 @@ describe("Image", () => { let uploadFilePath = "/path/to/image.jpg"; let uploadDialogElement = null; let editorInnerHTML = (dim, src, alt) => { - return ` -
    -
    - - - - -
    - - × - -
    -
    - ${alt} -
    -
    -
    - ` + return `
    ×
    ${alt}
    `; } const updateFile = async (path, alt) => { @@ -63,6 +46,10 @@ describe("Image", () => { await sleep(0); } + const normalizeHTML = (html) => { + return html.replace(/


    <\/p>/g, "").replace(/

    <\/p>/g, ""); + }; + beforeEach(() => { document.body.innerHTML = ""; @@ -109,7 +96,8 @@ describe("Image", () => { it("editing setting the image through the dialog", async () => { editorElement.focus(); await updateContent(editorElement, - '

    Test text
    ' + '
    Test text
    ', + editor ); editor.commands.imageDialog(); @@ -117,7 +105,7 @@ describe("Image", () => { await updateFile("/path/to/image_updated.jpg", "Updated text") expect(editorElement.classList.contains("dialog-open")).toBe(false); - expect(editorElement.innerHTML).toMatchHtml(editorInnerHTML("null", "/path/to/image_updated.jpg", "Updated text")); + expect(normalizeHTML(editorElement.innerHTML)).toMatchHtml(editorInnerHTML("null", "/path/to/image_updated.jpg", "Updated text")); expect(editor.getHTML()).toMatchHtml(`
    Updated text
    `); @@ -126,16 +114,26 @@ describe("Image", () => { it("allows double clicking the image", async () => { editorElement.focus(); await updateContent(editorElement, - '
    Test text
    ' + '
    Test text
    ', + editor ); jest.spyOn(uploadDialogElement.dialog, "open"); // Position calculations do not work with JSDom / Jest - editor.view.posAtCoords = jest.fn().mockReturnValue({ pos: 1, inside: -1 }); - editorElement.dispatchEvent(new MouseEvent("mousedown", { button: 0, clientX: 10, clientY: 10 })); - editorElement.dispatchEvent(new MouseEvent("mousedown", { button: 0, clientX: 10, clientY: 10 })); - await updateFile("/path/to/image_updated.jpg", "Updated text") + editor.view.posAtCoords = jest.fn().mockReturnValue({ pos: 0, inside: 0 }); + + const mockEvent = new MouseEvent("dblclick", { + button: 0, + clientX: 10, + clientY: 10 + }); + + editor.view.someProp("handleDoubleClick", (click) => click(editor.view, 0, mockEvent)); + + await sleep(0); + + await updateFile("/path/to/image_updated.jpg", "Updated text"); expect(uploadDialogElement.dialog.open).toHaveBeenCalled(); expect(editor.getHTML()).toMatchHtml(` @@ -283,9 +281,9 @@ describe("Image", () => { }); editorElement.focus(); - await updateContent(editorElement, - '
    Test text
    ' - ); + + editor.commands.setImage({ src: "/path/to/image.jpg", alt: "Test text" }); + await sleep(0); }); describe("with mouse", () => behavesLikeImageResizer("mouse")); diff --git a/decidim-core/app/packs/src/decidim/editor/test/extensions/mention.test.js b/decidim-core/app/packs/src/decidim/editor/test/extensions/mention.test.js index fa9199179d60b..5b8b645dd0b67 100644 --- a/decidim-core/app/packs/src/decidim/editor/test/extensions/mention.test.js +++ b/decidim-core/app/packs/src/decidim/editor/test/extensions/mention.test.js @@ -64,6 +64,10 @@ describe("Mention", () => { let editor = null; let editorElement = null; + const normalizeHTML = (html) => { + return html.replace(/\s*data-mention-suggestion-char="[^"]*"/g, ""); + }; + beforeEach(() => { document.body.innerHTML = ""; @@ -108,7 +112,7 @@ describe("Mention", () => { expect(editorElement.innerHTML).toEqual( '

    @johndoe (John Doe)

    ' ); - expect(editor.getHTML()).toEqual( + expect(normalizeHTML(editor.getHTML())).toEqual( '

    @johndoe (John Doe)

    ' ); }); @@ -122,7 +126,7 @@ describe("Mention", () => { expect(editorElement.innerHTML).toEqual( '

    @johndoe (John Doe)

    ' ); - expect(editor.getHTML()).toEqual( + expect(normalizeHTML(editor.getHTML())).toEqual( '

    @johndoe (John Doe)

    ' ); }); diff --git a/decidim-core/app/packs/src/decidim/editor/test/extensions/video_embed.test.js b/decidim-core/app/packs/src/decidim/editor/test/extensions/video_embed.test.js index cf2ca9a98341e..af2f3e3e4cc82 100644 --- a/decidim-core/app/packs/src/decidim/editor/test/extensions/video_embed.test.js +++ b/decidim-core/app/packs/src/decidim/editor/test/extensions/video_embed.test.js @@ -9,6 +9,10 @@ describe("VideoEmbed", () => { let editor = null; let editorElement = null; + const normalizeHTML = (html) => { + return html.replace(/


    <\/p>/g, "").replace(/

    <\/p>/g, ""); + }; + beforeEach(() => { document.body.innerHTML = ""; @@ -35,7 +39,7 @@ describe("VideoEmbed", () => {

    `); - expect(editor.getHTML()).toMatchHtml(` + expect(normalizeHTML(editor.getHTML())).toMatchHtml(`
    @@ -52,21 +56,21 @@ describe("VideoEmbed", () => {
    - `); + `, editor); editor.commands.setVideo({ src: "https://www.youtube.com/watch?v=zhMMW0TENNA", title: "Free Open-Source" }); - expect(editorElement.innerHTML).toMatchHtml(` + expect(normalizeHTML(editorElement.innerHTML)).toMatchHtml(`
    `); - expect(editor.getHTML()).toMatchHtml(` + expect(normalizeHTML(editor.getHTML())).toMatchHtml(`
    @@ -83,7 +87,7 @@ describe("VideoEmbed", () => {
    - `); + `, editor); editor.commands.videoEmbedDialog(); expect(editorElement.classList.contains("dialog-open")).toBe(true); @@ -94,7 +98,7 @@ describe("VideoEmbed", () => { dialog.querySelector("[data-dialog-actions] button[data-action='save']").click(); await sleep(50); - expect(editor.getHTML()).toMatchHtml(` + expect(normalizeHTML(editor.getHTML())).toMatchHtml(`
    @@ -113,7 +117,7 @@ describe("VideoEmbed", () => { dialog.querySelector("[data-dialog-actions] button[data-action='save']").click(); await sleep(50); - expect(editor.getHTML()).toMatchHtml(` + expect(normalizeHTML(editor.getHTML())).toMatchHtml(`
    @@ -125,7 +129,7 @@ describe("VideoEmbed", () => { it("allows pasting a YouTube video", async () => { await pasteContent(editorElement, "https://www.youtube.com/watch?v=f6JMgJAQ2tc"); - expect(editor.getHTML()).toMatchHtml(` + expect(normalizeHTML(editor.getHTML())).toMatchHtml(`
    @@ -137,7 +141,7 @@ describe("VideoEmbed", () => { it("allows pasting a Vimeo video", async () => { await pasteContent(editorElement, "https://vimeo.com/312909656"); - expect(editor.getHTML()).toMatchHtml(` + expect(normalizeHTML(editor.getHTML())).toMatchHtml(`
    @@ -154,13 +158,21 @@ describe("VideoEmbed", () => {
    - `); + `, editor); + + // Position calculations do not work with JSDom / Jest. + editor.view.posAtCoords = jest.fn().mockReturnValue({ pos: 0, inside: 0 }); + + // Manually trigger the handleDoubleClick handler directly. + const mockEvent = new MouseEvent("dblclick", { + bubbles: true, + cancelable: true, + clientX: 10, + clientY: 10 + }); - // Position calculations do not work with JSDom / Jest - editor.view.posAtCoords = jest.fn().mockReturnValue({ pos: 1, inside: -1 }); + editor.view.someProp("handleDoubleClick", (click) => click(editor.view, 0, mockEvent)); - editorElement.dispatchEvent(new MouseEvent("mousedown", { clientX: 10, clientY: 10 })); - editorElement.dispatchEvent(new MouseEvent("mousedown", { clientX: 10, clientY: 10 })); await sleep(0); expect(editorElement.classList.contains("dialog-open")).toBe(true); @@ -172,7 +184,7 @@ describe("VideoEmbed", () => { dialog.querySelector("[data-dialog-actions] button[data-action='save']").click(); await sleep(50); - expect(editor.getHTML()).toMatchHtml(` + expect(normalizeHTML(editor.getHTML())).toMatchHtml(`
    diff --git a/decidim-core/app/packs/src/decidim/editor/test/helpers.js b/decidim-core/app/packs/src/decidim/editor/test/helpers.js index 6adb94b227f7a..4644c076b7dd7 100644 --- a/decidim-core/app/packs/src/decidim/editor/test/helpers.js +++ b/decidim-core/app/packs/src/decidim/editor/test/helpers.js @@ -76,16 +76,14 @@ Object.assign(Range.prototype, { export const sleep = async (time) => new Promise((resolve) => setTimeout(resolve, time)); -export const updateContent = async (editable, content) => { - // ProseMirror listens has a mutation observer which listens to the changes on - // the contenteditable element. This forces us to manually change the - // `innerHTML` of the element to cause a change event to be triggered for the - // editor. - editable.innerHTML = content; - - // We need to wait for the mutation observer to complete its logic. It has a - // timeout of 20 milliseconds by default which is the minimum amount we need - // to wait. +export const updateContent = async (editable, content, editor) => { + if (editor) { + // Allows the proper use of TipTap's setContent method + editor.commands.setContent(content); + } else { + // Fallback for cases where editor is not available + editable.innerHTML = content; + } await sleep(50); }; diff --git a/decidim-core/app/packs/src/decidim/editor/test/toolbar/full.test.js b/decidim-core/app/packs/src/decidim/editor/test/toolbar/full.test.js index 72da4bf9a5848..1279edb157461 100644 --- a/decidim-core/app/packs/src/decidim/editor/test/toolbar/full.test.js +++ b/decidim-core/app/packs/src/decidim/editor/test/toolbar/full.test.js @@ -53,6 +53,12 @@ describe("full toolbar", () => { }); describe("videoEmbed", () => { + + const normalizeHTML = (html) => { + return html.replace(/\s*data-mention-suggestion-char="[^"]*"/g, ""). + replace(/


    <\/p>/g, ""); + }; + it("creates a new video embed with the provided details at the end of the selection", async () => { await setContent("Hello, world!"); prosemirror.focus(); @@ -69,7 +75,7 @@ describe("full toolbar", () => { // handled await sleep(0); - expect(prosemirror.innerHTML).toMatchHtml( + expect(normalizeHTML(prosemirror.innerHTML)).toMatchHtml( `

    Hello, world!

    @@ -112,7 +118,7 @@ describe("full toolbar", () => { // handled await sleep(0); - expect(prosemirror.innerHTML).toMatchHtml( + expect(normalizeHTML(prosemirror.innerHTML)).toMatchHtml( `

    Hello, world!

    @@ -148,6 +154,11 @@ describe("full toolbar", () => { window.Decidim.currentDialogs[dialog] = dialogEl.dialog; } + const normalizeHTML = (html) => { + return html.replace(/\s*data-mention-suggestion-char="[^"]*"/g, ""). + replace(/


    <\/p>/g, ""); + }; + beforeEach(() => { const csrf = document.createElement("meta"); csrf.setAttribute("name", "csrf-token") @@ -174,7 +185,7 @@ describe("full toolbar", () => { await sleep(0); - expect(prosemirror.innerHTML).toMatchHtml( + expect(normalizeHTML(prosemirror.innerHTML)).toMatchHtml( `

    Hello, world!

    @@ -218,7 +229,7 @@ describe("full toolbar", () => { await sleep(0); - expect(prosemirror.innerHTML).toMatchHtml( + expect(normalizeHTML(prosemirror.innerHTML)).toMatchHtml( `

    Hello, world!

    diff --git a/decidim-core/app/packs/src/decidim/editor/test/toolbar/shared/behaves_like_basic_block.js b/decidim-core/app/packs/src/decidim/editor/test/toolbar/shared/behaves_like_basic_block.js index 0a3f008cfa871..1459da54ea38a 100644 --- a/decidim-core/app/packs/src/decidim/editor/test/toolbar/shared/behaves_like_basic_block.js +++ b/decidim-core/app/packs/src/decidim/editor/test/toolbar/shared/behaves_like_basic_block.js @@ -5,13 +5,17 @@ import contextHelpers from "src/decidim/editor/test/toolbar/shared/context"; export default (ctx) => { const { getControl, setContent } = contextHelpers(ctx); + const normalizeHTML = (html) => { + return html.replace(/


    <\/p>$/g, "").trim(); + }; + describe("codeBlock", () => { it("creates a new code block", async () => { await setContent("Hello, world!"); selectContent(ctx.prosemirror); getControl("codeBlock").click(); - expect(ctx.prosemirror.innerHTML).toEqual("

    Hello, world!
    "); + expect(normalizeHTML(ctx.prosemirror.innerHTML)).toEqual("
    Hello, world!
    "); }); it("makes existing code block content as normal text", async () => { @@ -20,7 +24,7 @@ export default (ctx) => { selectContent(ctx.prosemirror, "pre code"); getControl("codeBlock").click(); - expect(ctx.prosemirror.innerHTML).toEqual("

    Hello, world!

    "); + expect(normalizeHTML(ctx.prosemirror.innerHTML)).toEqual("

    Hello, world!

    "); }); }); @@ -30,7 +34,7 @@ export default (ctx) => { selectContent(ctx.prosemirror); getControl("blockquote").click(); - expect(ctx.prosemirror.innerHTML).toEqual("

    Hello, world!

    "); + expect(normalizeHTML(ctx.prosemirror.innerHTML)).toEqual("

    Hello, world!

    "); }); it("makes existing blockquote content as normal text", async () => { @@ -39,7 +43,7 @@ export default (ctx) => { selectContent(ctx.prosemirror, "blockquote p"); getControl("blockquote").click(); - expect(ctx.prosemirror.innerHTML).toEqual("

    Hello, world!

    "); + expect(normalizeHTML(ctx.prosemirror.innerHTML)).toEqual("

    Hello, world!

    "); }); }); }; diff --git a/decidim-core/app/packs/src/decidim/editor/test/toolbar/shared/behaves_like_basic_formatting.js b/decidim-core/app/packs/src/decidim/editor/test/toolbar/shared/behaves_like_basic_formatting.js index f0356e0298a2a..4a53714e62644 100644 --- a/decidim-core/app/packs/src/decidim/editor/test/toolbar/shared/behaves_like_basic_formatting.js +++ b/decidim-core/app/packs/src/decidim/editor/test/toolbar/shared/behaves_like_basic_formatting.js @@ -3,6 +3,10 @@ import contextHelpers from "src/decidim/editor/test/toolbar/shared/context"; export default (ctx) => { const { getControl, setContent } = contextHelpers(ctx); + const normalizeHTML = (html) => { + return html.replace(/


    <\/p>$/g, "").trim(); + }; + describe("hardBreak", () => { it("creates a new line break at the cursor position", async () => { await setContent("Hello, world!"); @@ -11,7 +15,7 @@ export default (ctx) => { // Note that the "tailingBreak" is only ProseMirror's internal element // to place the cursor at the correct location. - expect(ctx.prosemirror.innerHTML).toEqual('

    Hello, world!

    '); + expect(normalizeHTML(ctx.prosemirror.innerHTML)).toEqual('

    Hello, world!

    '); }); }); }; diff --git a/decidim-core/app/packs/src/decidim/editor/test/toolbar/shared/behaves_like_basic_indent.js b/decidim-core/app/packs/src/decidim/editor/test/toolbar/shared/behaves_like_basic_indent.js index 45399a48334a4..e2ffbcb84cf20 100644 --- a/decidim-core/app/packs/src/decidim/editor/test/toolbar/shared/behaves_like_basic_indent.js +++ b/decidim-core/app/packs/src/decidim/editor/test/toolbar/shared/behaves_like_basic_indent.js @@ -5,6 +5,10 @@ import contextHelpers from "src/decidim/editor/test/toolbar/shared/context"; export default (ctx) => { const { getControl, setContent } = contextHelpers(ctx); + const normalizeHTML = (html) => { + return html.replace(/


    <\/p>$/g, "").trim(); + }; + describe("indent:indent", () => { it("indents the existing content", async () => { await setContent("Hello, world!"); @@ -13,7 +17,7 @@ export default (ctx) => { ctrl.click(); ctrl.click(); - expect(ctx.prosemirror.innerHTML).toEqual('

    Hello, world!

    '); + expect(normalizeHTML(ctx.prosemirror.innerHTML)).toEqual('

    Hello, world!

    '); }); it("indents a list item correctly", async () => { @@ -21,7 +25,7 @@ export default (ctx) => { selectContent(ctx.prosemirror, "ul li:nth-child(2) p"); getControl("indent:indent").click(); - expect(ctx.prosemirror.innerHTML).toEqual( + expect(normalizeHTML(ctx.prosemirror.innerHTML)).toEqual( "
    • First item

      • Second item

    " ); }); @@ -35,7 +39,7 @@ export default (ctx) => { ctrl.click(); ctrl.click(); - expect(ctx.prosemirror.innerHTML).toEqual("

    Hello, world!

    "); + expect(normalizeHTML(ctx.prosemirror.innerHTML)).toEqual("

    Hello, world!

    "); }); it("outdents a list item correctly", async () => { @@ -43,7 +47,7 @@ export default (ctx) => { selectContent(ctx.prosemirror, "ul li ul li p"); getControl("indent:outdent").click(); - expect(ctx.prosemirror.innerHTML).toEqual( + expect(normalizeHTML(ctx.prosemirror.innerHTML)).toEqual( "
    • First item

    • Second item

    " ); }); @@ -53,7 +57,7 @@ export default (ctx) => { selectContent(ctx.prosemirror, "ul li:nth-child(2) p"); getControl("indent:outdent").click(); - expect(ctx.prosemirror.innerHTML).toEqual( + expect(normalizeHTML(ctx.prosemirror.innerHTML)).toEqual( "
    • First item

    • Second item

    " ); }); diff --git a/decidim-core/app/packs/src/decidim/editor/test/toolbar/shared/behaves_like_basic_link.js b/decidim-core/app/packs/src/decidim/editor/test/toolbar/shared/behaves_like_basic_link.js index 018e067f8be0b..124089b8fefec 100644 --- a/decidim-core/app/packs/src/decidim/editor/test/toolbar/shared/behaves_like_basic_link.js +++ b/decidim-core/app/packs/src/decidim/editor/test/toolbar/shared/behaves_like_basic_link.js @@ -5,6 +5,10 @@ import contextHelpers from "src/decidim/editor/test/toolbar/shared/context"; export default (ctx) => { const { getControl, setContent } = contextHelpers(ctx); + const normalizeHTML = (html) => { + return html.replace(/


    <\/p>$/g, "").trim(); + }; + describe("link", () => { it("creates a link for the selected text", async () => { await setContent("Hello, world!"); @@ -25,7 +29,7 @@ export default (ctx) => { // handled await sleep(0); - expect(ctx.prosemirror.innerHTML).toEqual( + expect(normalizeHTML(ctx.prosemirror.innerHTML)).toEqual( '

    Hello, world!

    ' ); }); diff --git a/decidim-core/app/packs/src/decidim/editor/test/toolbar/shared/behaves_like_basic_list.js b/decidim-core/app/packs/src/decidim/editor/test/toolbar/shared/behaves_like_basic_list.js index 40c1076c59309..3e18b3a762194 100644 --- a/decidim-core/app/packs/src/decidim/editor/test/toolbar/shared/behaves_like_basic_list.js +++ b/decidim-core/app/packs/src/decidim/editor/test/toolbar/shared/behaves_like_basic_list.js @@ -5,13 +5,17 @@ import contextHelpers from "src/decidim/editor/test/toolbar/shared/context"; export default (ctx) => { const { getControl, setContent } = contextHelpers(ctx); + const normalizeHTML = (html) => { + return html.replace(/


    <\/p>$/g, "").trim(); + }; + describe("orderedList", () => { it("creates a new ordered list", async () => { await setContent("Hello, world!"); ctx.prosemirror.focus(); getControl("orderedList").click(); - expect(ctx.prosemirror.innerHTML).toEqual("

    1. Hello, world!

    "); + expect(normalizeHTML(ctx.prosemirror.innerHTML)).toEqual("
    1. Hello, world!

    "); }); it("makes existing ordered list as normal text", async () => { @@ -21,7 +25,7 @@ export default (ctx) => { selectContent(ctx.prosemirror, "ol li p"); getControl("orderedList").click(); - expect(ctx.prosemirror.innerHTML).toEqual("

    Hello, world!

    "); + expect(normalizeHTML(ctx.prosemirror.innerHTML)).toEqual("

    Hello, world!

    "); }); }); @@ -31,7 +35,7 @@ export default (ctx) => { ctx.prosemirror.focus(); getControl("bulletList").click(); - expect(ctx.prosemirror.innerHTML).toEqual("
    • Hello, world!

    "); + expect(normalizeHTML(ctx.prosemirror.innerHTML)).toEqual("
    • Hello, world!

    "); }); it("makes existing bullet list as normal text", async () => { @@ -41,7 +45,7 @@ export default (ctx) => { selectContent(ctx.prosemirror, "ul li p"); getControl("bulletList").click(); - expect(ctx.prosemirror.innerHTML).toEqual("

    Hello, world!

    "); + expect(normalizeHTML(ctx.prosemirror.innerHTML)).toEqual("

    Hello, world!

    "); }); }); }; diff --git a/decidim-core/app/packs/src/decidim/editor/test/toolbar/shared/behaves_like_basic_styling.js b/decidim-core/app/packs/src/decidim/editor/test/toolbar/shared/behaves_like_basic_styling.js index 4b3e3e3215b84..0cd5f69112424 100644 --- a/decidim-core/app/packs/src/decidim/editor/test/toolbar/shared/behaves_like_basic_styling.js +++ b/decidim-core/app/packs/src/decidim/editor/test/toolbar/shared/behaves_like_basic_styling.js @@ -5,13 +5,17 @@ import contextHelpers from "src/decidim/editor/test/toolbar/shared/context"; export default (ctx) => { const { getControl, setContent } = contextHelpers(ctx); + const normalizeHTML = (html) => { + return html.replace(/


    <\/p>$/g, "").trim(); + }; + describe("bold", () => { it("makes text bold", async () => { await setContent("Hello, world!"); selectContent(ctx.prosemirror); getControl("bold").click(); - expect(ctx.prosemirror.innerHTML).toEqual("

    Hello, world!

    "); + expect(normalizeHTML(ctx.prosemirror.innerHTML)).toEqual("

    Hello, world!

    "); }); it("makes already bolded text normal", async () => { @@ -20,7 +24,7 @@ export default (ctx) => { selectContent(ctx.prosemirror, "p strong"); getControl("bold").click(); - expect(ctx.prosemirror.innerHTML).toEqual("

    Hello, world!

    "); + expect(normalizeHTML(ctx.prosemirror.innerHTML)).toEqual("

    Hello, world!

    "); }); }); @@ -30,7 +34,7 @@ export default (ctx) => { selectContent(ctx.prosemirror); getControl("italic").click(); - expect(ctx.prosemirror.innerHTML).toEqual("

    Hello, world!

    "); + expect(normalizeHTML(ctx.prosemirror.innerHTML)).toEqual("

    Hello, world!

    "); }); it("makes already italic text normal", async () => { @@ -39,7 +43,7 @@ export default (ctx) => { selectContent(ctx.prosemirror, "p em"); getControl("italic").click(); - expect(ctx.prosemirror.innerHTML).toEqual("

    Hello, world!

    "); + expect(normalizeHTML(ctx.prosemirror.innerHTML)).toEqual("

    Hello, world!

    "); }); }); @@ -49,7 +53,7 @@ export default (ctx) => { selectContent(ctx.prosemirror); getControl("underline").click(); - expect(ctx.prosemirror.innerHTML).toEqual("

    Hello, world!

    "); + expect(normalizeHTML(ctx.prosemirror.innerHTML)).toEqual("

    Hello, world!

    "); }); it("makes already underlined text normal", async () => { @@ -58,7 +62,7 @@ export default (ctx) => { selectContent(ctx.prosemirror, "p u"); getControl("underline").click(); - expect(ctx.prosemirror.innerHTML).toEqual("

    Hello, world!

    "); + expect(normalizeHTML(ctx.prosemirror.innerHTML)).toEqual("

    Hello, world!

    "); }); }); @@ -71,7 +75,7 @@ export default (ctx) => { getControl("common:eraseStyles").click(); - expect(ctx.prosemirror.innerHTML).toEqual("

    Hello, world!

    ") + expect(normalizeHTML(ctx.prosemirror.innerHTML)).toEqual("

    Hello, world!

    ") }); it("makes ordered list content as normal text", async () => { @@ -80,7 +84,7 @@ export default (ctx) => { getControl("common:eraseStyles").click(); - expect(ctx.prosemirror.innerHTML).toEqual("

    Hello, world!

    ") + expect(normalizeHTML(ctx.prosemirror.innerHTML)).toEqual("

    Hello, world!

    ") }); it("makes bullet list content as normal text", async () => { @@ -89,7 +93,7 @@ export default (ctx) => { getControl("common:eraseStyles").click(); - expect(ctx.prosemirror.innerHTML).toEqual("

    Hello, world!

    ") + expect(normalizeHTML(ctx.prosemirror.innerHTML)).toEqual("

    Hello, world!

    ") }); }); }; diff --git a/decidim-core/app/packs/src/decidim/editor/test/toolbar/shared/behaves_like_content_styling.js b/decidim-core/app/packs/src/decidim/editor/test/toolbar/shared/behaves_like_content_styling.js index 93b676fb1b92e..2f3828dde464e 100644 --- a/decidim-core/app/packs/src/decidim/editor/test/toolbar/shared/behaves_like_content_styling.js +++ b/decidim-core/app/packs/src/decidim/editor/test/toolbar/shared/behaves_like_content_styling.js @@ -5,6 +5,10 @@ import contextHelpers from "src/decidim/editor/test/toolbar/shared/context"; export default (ctx) => { const { getControl, setContent } = contextHelpers(ctx); + const normalizeHTML = (html) => { + return html.replace(/


    <\/p>$/g, "").trim(); + }; + describe("heading", () => { const levels = ["2", "3", "4", "5", "6"]; let selectValue = (value) => { @@ -21,7 +25,7 @@ export default (ctx) => { selectValue(level); const tag = `h${level}`; - expect(ctx.prosemirror.innerHTML).toEqual(`<${tag}>Hello, world!`); + expect(normalizeHTML(ctx.prosemirror.innerHTML)).toEqual(`<${tag}>Hello, world!`); }); }); @@ -33,7 +37,7 @@ export default (ctx) => { selectValue("normal"); - expect(ctx.prosemirror.innerHTML).toEqual("

    Hello, world!

    "); + expect(normalizeHTML(ctx.prosemirror.innerHTML)).toEqual("

    Hello, world!

    "); }); }); }); diff --git a/decidim-core/app/packs/src/decidim/identity_selector_dialog.js b/decidim-core/app/packs/src/decidim/identity_selector_dialog.js deleted file mode 100644 index e0c35291de937..0000000000000 --- a/decidim-core/app/packs/src/decidim/identity_selector_dialog.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Send a request to select which identity want to use - * NOTE: this should not be done using javascript - * - * @param {HTMLElement} node target node - * @returns {void} - */ -export default function(node = document) { - node.addEventListener("click", ({ target: element }) => { - const { method } = element.dataset - - let attr = "destroy_url"; - - if (method === "POST") { - attr = "create_url"; - } - - const { [attr]: url } = element.dataset - Rails.ajax({ - url: url, - type: method, - success: function() { - if (method === "POST") { - element.classList.add("is-selected") - element.dataset.method = "DELETE" - } else { - element.classList.remove("is-selected") - element.dataset.method = "POST" - } - } - }) - }) -} diff --git a/decidim-core/app/packs/src/decidim/index.js b/decidim-core/app/packs/src/decidim/index.js index 249e15daed5b3..92c5fc8962af2 100644 --- a/decidim-core/app/packs/src/decidim/index.js +++ b/decidim-core/app/packs/src/decidim/index.js @@ -36,7 +36,6 @@ import "src/decidim/results_listing" import "src/decidim/data_consent" import "src/decidim/sw" import "src/decidim/attachments" -import "src/decidim/dropdown_menu" import "src/decidim/callout" // local deps that require initialization diff --git a/decidim-core/app/packs/stylesheets/decidim/_dropdown.scss b/decidim-core/app/packs/stylesheets/decidim/_dropdown.scss index 604983164afd0..79f94b80aaa8c 100644 --- a/decidim-core/app/packs/stylesheets/decidim/_dropdown.scss +++ b/decidim-core/app/packs/stylesheets/decidim/_dropdown.scss @@ -66,42 +66,6 @@ @apply md:hidden; } -[data-target="dropdown-menu-language-chooser"] { - & > span { - @apply block font-semibold text-black text-xs; - } - - > svg { - @apply w-5 h-6 flex-none text-xs font-normal text-black fill-current; - } - - & > ul { - @apply w-6 bg-gray; - - > li { - @apply w-6 bg-gray; - } - } -} - -[data-target="dropdown-menu-language-chooser-mobile"] { - & > span { - @apply block text-black font-normal text-sm; - } - - > svg { - @apply w-fit h-6 flex-none text-xs font-normal text-black fill-current px-2; - } - - & > ul { - @apply w-6 bg-gray; - - > li { - @apply w-6 bg-gray; - } - } -} - .dropdown { @apply absolute border-2 border-gray-3 rounded-md min-w-max p-1 drop-shadow-md text-left z-10 after:content-[''] after:absolute after:left-0 after:top-0 after:w-full after:h-full after:bg-white; diff --git a/decidim-core/app/packs/stylesheets/decidim/_footer.scss b/decidim-core/app/packs/stylesheets/decidim/_footer.scss index 6fcf8478c0e4c..2544d21ebfd23 100644 --- a/decidim-core/app/packs/stylesheets/decidim/_footer.scss +++ b/decidim-core/app/packs/stylesheets/decidim/_footer.scss @@ -14,42 +14,6 @@ footer { &__down { @apply border-t border-black flex flex-wrap items-center gap-6 container py-6 text-white; } - - &__language { - @apply absolute top-full left-0 bg-white rounded w-full; - - &-container { - @apply relative; - } - - &-trigger { - @apply flex items-center gap-1 border border-white rounded py-1.5 px-2 cursor-pointer text-md font-semibold; - } - } - - /* overwrite default dropdown styles */ - [data-target*="dropdown"] { - > svg { - @apply w-4 h-4 text-white fill-current last-of-type:block last-of-type:ml-auto; - } - - &[aria-expanded="true"] > svg:first-of-type { - @apply block; - } - - > span { - @apply block text-white; - } - } - - /* overwrite default dropdown styles */ - [id*="dropdown-menu"] { - @apply py-0 mx-0 w-full; - - &[aria-hidden="true"] { - @apply md:hidden; - } - } } .mini-footer { diff --git a/decidim-core/app/packs/stylesheets/decidim/_forms.scss b/decidim-core/app/packs/stylesheets/decidim/_forms.scss index 701746de9f606..2763f412c23c7 100644 --- a/decidim-core/app/packs/stylesheets/decidim/_forms.scss +++ b/decidim-core/app/packs/stylesheets/decidim/_forms.scss @@ -1,11 +1,15 @@ #form-search-mobile { - @apply py-1.5 px-4 bg-gray-5 rounded-lg border border-gray outline outline-1 outline-transparent; + @apply py-1.5 px-4 pr-8 bg-gray-5 rounded-lg border border-gray outline outline-1 outline-transparent; input[type="text"] { @apply bg-gray-5; } } +#input-search-page { + @apply w-full px-4 pr-8; +} + .form-defaults { /* text-like inputs */ input[type="date"], diff --git a/decidim-core/app/packs/stylesheets/decidim/_header.scss b/decidim-core/app/packs/stylesheets/decidim/_header.scss index e37d30f6e0d56..8ba8cecf918e5 100644 --- a/decidim-core/app/packs/stylesheets/decidim/_header.scss +++ b/decidim-core/app/packs/stylesheets/decidim/_header.scss @@ -28,14 +28,10 @@ header { #flash-messages-container { @apply mt-40 md:mt-0; } - - #menu-bar-container { - @apply mt-[140px] md:mt-0; - } } - #sticky-header-container { - @apply fixed w-full top-0 z-40 bg-white md:relative transition-top duration-300; + #header-container { + @apply w-full z-40 bg-white; } .main-bar { @@ -69,11 +65,11 @@ header { @apply hidden md:block col-span-2 col-start-5 xl:col-start-4; form { - @apply block relative rounded text-md border border-neutral-200 outline outline-1 outline-transparent rounded bg-background-2 leading-relaxed; + @apply block relative rounded text-md border border-neutral-200 outline outline-1 outline-transparent rounded bg-background-2 leading-none; } input[type="text"] { - @apply block bg-transparent w-full px-4 py-2 h-full; + @apply block bg-transparent w-full pl-4 pr-10 py-2 h-full; &::placeholder { @apply text-neutral-500 text-sm ltr:pl-0 rtl:pr-0; @@ -103,7 +99,7 @@ header { &__links-desktop, > *:last-child:not(.main-bar__back-button) { - @apply col-span-1 md:col-start-8 lg:col-start-8 lg:col-span-5 justify-self-end; + @apply col-span-1 lg:col-start-8 lg:col-start-8 lg:col-span-5 justify-self-end; } &__links-desktop { @@ -125,12 +121,18 @@ header { } } + .card__highlight-img { + svg { + @apply w-full h-full; + } + } + &__dropdown { - @apply flex items-end absolute top-0 right-0 z-30 h-screen; + @apply flex items-end absolute top-0 right-0 bg-[rgba(0,0,0,0.25)] transition duration-300 z-30 h-screen cursor-auto; .menu-bar { &__main-dropdown { - @apply hidden lg:flex flex-col h-full md:p-8; + @apply hidden sm:flex flex-col h-full sm:p-8; &__left { @apply w-full; @@ -141,7 +143,7 @@ header { @apply w-full; li { - @apply py-3 md:py-3.5 border-b border-t-0 last:border-0 border-gray-3 #{!important}; + @apply py-3 sm:py-3.5 border-b border-t-0 last:border-0 border-gray-3 #{!important}; a { @apply no-underline; @@ -188,7 +190,7 @@ header { } svg + span { - @apply text-md leading-[22px] first-letter:uppercase font-semibold; + @apply text-md leading-[22px] first-letter:uppercase font-semibold whitespace-nowrap; } } @@ -313,16 +315,16 @@ header { /* overwrite default dropdown styles */ [id*="dropdown-menu"] { - @apply py-0 mx-0 w-full lg:w-fit; + @apply py-0 mx-0 w-full; &[aria-hidden="true"] { - @apply md:hidden; + @apply sm:hidden; } } } .menu-bar { - @apply container h-full flex justify-between items-center lg:relative last-of-type:[&>svg]:hidden; + @apply container h-full flex justify-between items-center sm:relative last-of-type:[&>svg]:hidden; &__container { @apply bg-white relative h-14 flex justify-between; @@ -348,7 +350,7 @@ header { } &__dropdown-wrapper { - @apply flex items-center cursor-pointer underline focus:backdrop-brightness-75 focus:outline-none; + @apply flex items-center cursor-pointer underline; } &__dropdown-content { @@ -459,87 +461,11 @@ header { } } - &__language-chooser { - @apply absolute top-full left-0 rounded w-full bg-gray-5; - - &-desktop { - @apply border-r border-gray-6 pr-10; - - &.focus-mode { - @apply border-none pr-0; - } - } - - &-desktop, - &-mobile { - & button { - @apply border border-neutral-300 rounded px-4 py-2 gap-x-2; - - & > span, - & > svg:first-of-type { - @apply text-neutral-500; - - display: block !important; - } - - & > span.mobile-holder { - @apply items-center gap-x-1; - - display: flex !important; - } - - & > span { - @apply text-sm font-normal; - } - - & > svg:last-of-type { - @apply text-gray-2 ml-1; - - display: block !important; - } - } - - #dropdown-menu-language-chooser { - @apply absolute top-full bg-white shadow-[1px_4px_8px_3px_rgba(0,0,0,0.15)] p-2; - - ul { - @apply flex flex-col space-y-2; - - li:hover { - @apply rounded; - } - - .is-active { - @apply bg-neutral-200 rounded; - - &:hover { - @apply bg-[var(--secondary)]; - } - } - } - } - - #dropdown-menu-language-chooser-mobile { - @apply overflow-scroll h-[50vh]; - - ul { - .is-active { - @apply bg-neutral-200 rounded; - - &:hover { - @apply bg-[var(--secondary)]; - } - } - } - } - } - } - &__dropdown-menu { - @apply w-full md:w-1/4 px-4 md:px-0 pt-0 pb-3 md:py-3 divide-y divide-gray-3 text-[var(--secondary)]; + @apply w-full sm:w-1/4 px-4 sm:px-0 pt-0 pb-3 sm:py-3 divide-y divide-gray-3 text-[var(--secondary)]; > * { - @apply py-3 md:py-3.5 md:px-2; + @apply py-3 sm:py-3.5 sm:px-2; } a { @@ -564,10 +490,10 @@ header { } &__main-dropdown { - @apply bg-white flex flex-row rounded-b shadow-lg text-black w-full lg:w-[530px] h-screen md:h-auto; + @apply bg-white flex flex-row rounded-b shadow-lg text-black w-full lg:w-[530px] h-screen lg:h-auto; &__left { - @apply p-4 md:py-8 md:px-0 space-y-5 hidden md:block md:w-3/4; + @apply p-4 sm:py-8 sm:px-0 space-y-5 hidden sm:block sm:w-3/4; &-top { @apply border-b-4 border-gray-3 pb-3; @@ -578,14 +504,14 @@ header { @apply w-full px-4; &-menu { - @apply w-full md:w-[100%] mt-0 grid md:grid-cols-2 gap-x-6 text-secondary; + @apply w-full lg:w-[100%] mt-0 grid lg:grid-cols-2 gap-x-6 text-secondary; > * { - @apply py-3 md:py-3.5 border-b last:border-0 border-gray-3; + @apply py-3 lg:py-3.5 border-b last:border-0 border-gray-3; /* since the grid has 2 columns, remove the border for these last 2 columns */ &:nth-last-child(-n + 2) { - @apply md:border-0; + @apply lg:border-0; } } @@ -604,7 +530,7 @@ header { } &__bottom { - @apply hidden md:flex; + @apply hidden lg:flex; &-right { @apply mr-2 mb-2; @@ -629,7 +555,11 @@ header { @apply flex-col ring-transparent rounded-lg shadow-[1px_4px_8px_3px_rgba(0,0,0,0.15)]; &-img { - @apply w-full aspect-[3/1] rounded-tl-lg rounded-tr-lg; + @apply w-full aspect-[21/9] rounded-none rounded-tl-lg rounded-tr-lg; + + svg { + @apply fill-primary; + } } &-text { diff --git a/decidim-core/app/packs/stylesheets/decidim/_language_chooser.scss b/decidim-core/app/packs/stylesheets/decidim/_language_chooser.scss new file mode 100644 index 0000000000000..59046ec48ae5f --- /dev/null +++ b/decidim-core/app/packs/stylesheets/decidim/_language_chooser.scss @@ -0,0 +1,79 @@ +header { + .main-bar { + &__language-chooser { + &-desktop { + @apply hidden md:block; + } + + > div[id*="dropdown-menu"] { + @apply bg-white shadow-[1px_4px_8px_3px_rgba(0,0,0,0.15)] overflow-auto max-h-[50vh] p-2 sm:w-fit z-[100]; + } + + &.focus-mode { + @apply border-none pr-0; + } + + &-trigger { + @apply inline-flex border border-neutral-300 rounded px-4 py-[9px] gap-x-2 flex-row; + + &[id*="dropdown-menu"] { + @apply py-[9px]; + } + + & > span, + & > svg:first-of-type { + @apply text-neutral-500; + } + + & > svg:last-of-type, + & > svg:first-of-type { + @apply block; + } + + & > svg:last-of-type { + @apply text-gray-2 ml-1; + } + + svg + span { + @apply text-sm font-normal; + } + } + + &-holder { + @apply items-center gap-x-1 flex; + } + + &-list { + @apply flex flex-col space-y-2; + } + + &-item { + @apply text-black text-left text-md hover:bg-secondary hover:text-white; + + &:hover { + @apply rounded; + } + + &-active { + @apply bg-neutral-200 rounded; + + &:hover { + @apply bg-[var(--secondary)]; + } + } + } + + & > div { + @apply absolute; + } + } + } + + .menu-bar__language-chooser-mobile { + .main-bar__language-chooser { + & > div[id*="dropdown-menu"] { + @apply w-full relative; + } + } + } +} diff --git a/decidim-core/app/packs/stylesheets/decidim/_modal_update.scss b/decidim-core/app/packs/stylesheets/decidim/_modal_update.scss index e60887cb602dd..4fdb0c7779d99 100644 --- a/decidim-core/app/packs/stylesheets/decidim/_modal_update.scss +++ b/decidim-core/app/packs/stylesheets/decidim/_modal_update.scss @@ -127,7 +127,7 @@ @apply w-full rounded bg-background flex items-center justify-center py-4 [&_img]:object-cover [&_img]:h-[200px]; } - span { + p { @apply text-sm text-gray-2 mx-auto w-full break-all mb-2; } } diff --git a/decidim-core/app/packs/stylesheets/decidim/application.scss b/decidim-core/app/packs/stylesheets/decidim/application.scss index 7a85caabde943..bb71da580380e 100644 --- a/decidim-core/app/packs/stylesheets/decidim/application.scss +++ b/decidim-core/app/packs/stylesheets/decidim/application.scss @@ -9,6 +9,7 @@ // On the other hand, the following styles match with specific routes @use "stylesheets/decidim/header"; @use "stylesheets/decidim/footer"; +@use "stylesheets/decidim/language_chooser"; @use "stylesheets/decidim/login"; @use "stylesheets/decidim/pages"; @use "stylesheets/decidim/notifications"; diff --git a/decidim-core/app/presenters/decidim/admin_log/participatory_space/member_presenter.rb b/decidim-core/app/presenters/decidim/admin_log/participatory_space/member_presenter.rb new file mode 100644 index 0000000000000..a86d0687fcbdc --- /dev/null +++ b/decidim-core/app/presenters/decidim/admin_log/participatory_space/member_presenter.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Decidim + module AdminLog + module ParticipatorySpace + # This class holds the logic to present a `Decidim::ParticipatorySpace::MemberPresenter` + # for the `AdminLog` log. + # + # Usage should be automatic and you should not need to call this class + # directly, but here is an example: + # + # action_log = Decidim::ActionLog.last + # view_helpers # => this comes from the views + # MemberPresenter.new(action_log, view_helpers).present + class MemberPresenter < Decidim::Log::BasePresenter + private + + def diff_fields_mapping + { + name: :string, + email: :string + } + end + + def action_string + case action + when "create", "create_via_csv", "delete" + "decidim.admin_log.member.#{action}" + else + super + end + end + + def i18n_labels_scope + "activemodel.attributes.member" + end + end + end + end +end diff --git a/decidim-core/app/presenters/decidim/admin_log/participatory_space_private_user_presenter.rb b/decidim-core/app/presenters/decidim/admin_log/participatory_space_private_user_presenter.rb deleted file mode 100644 index 27787e840f560..0000000000000 --- a/decidim-core/app/presenters/decidim/admin_log/participatory_space_private_user_presenter.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -module Decidim - module AdminLog - # This class holds the logic to present a `Decidim::ParticipatorySpacePrivateUserPresenter` - # for the `AdminLog` log. - # - # Usage should be automatic and you should not need to call this class - # directly, but here is an example: - # - # action_log = Decidim::ActionLog.last - # view_helpers # => this comes from the views - # ParticipatorySpacePrivateUserPresenter.new(action_log, view_helpers).present - class ParticipatorySpacePrivateUserPresenter < Decidim::Log::BasePresenter - private - - def diff_fields_mapping - { - name: :string, - email: :string - } - end - - def action_string - case action - when "create", "create_via_csv", "delete" - "decidim.admin_log.participatory_space_private_user.#{action}" - else - super - end - end - - def i18n_labels_scope - "activemodel.attributes.participatory_space_private_user" - end - end - end -end diff --git a/decidim-core/app/presenters/decidim/breadcrumb_root_menu_item_presenter.rb b/decidim-core/app/presenters/decidim/breadcrumb_root_menu_item_presenter.rb index e7e9e49cbbd39..94748cb2ffee1 100644 --- a/decidim-core/app/presenters/decidim/breadcrumb_root_menu_item_presenter.rb +++ b/decidim-core/app/presenters/decidim/breadcrumb_root_menu_item_presenter.rb @@ -9,14 +9,14 @@ class BreadcrumbRootMenuItemPresenter < MenuItemPresenter def render content_tag :li, class: link_wrapper_classes do - output = [arrow_link(label, url, link_options)] + output = [root_link(label, url, link_options)] output.push(@view.send(:simple_menu, **@menu_item.submenu).render) if @menu_item.submenu safe_join(output) end end - def arrow_link(text, url, args = {}) + def root_link(text, url, args = {}) link_to url, class: args.with_indifferent_access[:class] do "#{text}".html_safe end diff --git a/decidim-core/app/presenters/decidim/log/user_presenter.rb b/decidim-core/app/presenters/decidim/log/user_presenter.rb index 969d620b2dac2..9804a3c58e711 100644 --- a/decidim-core/app/presenters/decidim/log/user_presenter.rb +++ b/decidim-core/app/presenters/decidim/log/user_presenter.rb @@ -10,6 +10,7 @@ module Log # overwrite `BasePresenter#user_presenter` to return your custom user presenter. # The only requirement for custom renderers is that they should respond to `present`. class UserPresenter + include Decidim::SanitizeHelper # Public: Initializes the presenter. # # user - An instance of Decidim::User @@ -60,7 +61,7 @@ def present_user # # Returns an HTML-safe String. def present_user_name - extra["name"].html_safe + decidim_sanitize_translated(extra["name"]).html_safe end # Private: Presents the nickname of the user performing the action. diff --git a/decidim-core/app/presenters/decidim/organization_presenter.rb b/decidim-core/app/presenters/decidim/organization_presenter.rb index 1b542a8afdb70..7ad70c9e25d77 100644 --- a/decidim-core/app/presenters/decidim/organization_presenter.rb +++ b/decidim-core/app/presenters/decidim/organization_presenter.rb @@ -7,6 +7,10 @@ def html_name translated_attribute(name).html_safe end + def html_short_name + translated_attribute(short_name).html_safe + end + def translated_description ActionView::Base.full_sanitizer.sanitize(translated_attribute(description)).html_safe end diff --git a/decidim-core/app/presenters/decidim/participatory_space/member_presenter.rb b/decidim-core/app/presenters/decidim/participatory_space/member_presenter.rb new file mode 100644 index 0000000000000..c36faa25b1a81 --- /dev/null +++ b/decidim-core/app/presenters/decidim/participatory_space/member_presenter.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Decidim + module ParticipatorySpace + # + # Decorator for participatory space members + # + class MemberPresenter < SimpleDelegator + delegate :profile_url, to: :user, allow_nil: true + + def name + user ? user.name : full_name + end + + def nickname + user.nickname if user + end + + def avatar_url(variant = nil) + return user.avatar_url(variant) if user.present? + + non_user_avatar_path(variant) + end + + def non_user_avatar_path(variant = nil) + return non_user_avatar.default_url(variant) unless non_user_avatar.attached? + + non_user_avatar.path(variant:) + end + + def non_user_avatar + attached_uploader(:non_user_avatar) + end + + def deleted? + false + end + + private + + def user + @user ||= if (user = __getobj__.user.presence) + Decidim::UserPresenter.new(user) + end + end + end + end +end diff --git a/decidim-core/app/presenters/decidim/participatory_space_private_user_presenter.rb b/decidim-core/app/presenters/decidim/participatory_space_private_user_presenter.rb deleted file mode 100644 index ab086c9095f29..0000000000000 --- a/decidim-core/app/presenters/decidim/participatory_space_private_user_presenter.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -module Decidim - # - # Decorator for assembly members - # - class ParticipatorySpacePrivateUserPresenter < SimpleDelegator - delegate :profile_url, to: :user, allow_nil: true - - def name - user ? user.name : full_name - end - - def nickname - user.nickname if user - end - - def avatar_url(variant = nil) - return user.avatar_url(variant) if user.present? - - non_user_avatar_path(variant) - end - - def non_user_avatar_path(variant = nil) - return non_user_avatar.default_url(variant) unless non_user_avatar.attached? - - non_user_avatar.path(variant:) - end - - def non_user_avatar - attached_uploader(:non_user_avatar) - end - - def deleted? - false - end - - private - - def user - @user ||= if (user = __getobj__.user.presence) - Decidim::UserPresenter.new(user) - end - end - end -end diff --git a/decidim-core/app/presenters/decidim/user_presenter.rb b/decidim-core/app/presenters/decidim/user_presenter.rb index 14286dd10dc5d..bb49475bfa40f 100644 --- a/decidim-core/app/presenters/decidim/user_presenter.rb +++ b/decidim-core/app/presenters/decidim/user_presenter.rb @@ -6,7 +6,12 @@ module Decidim # class UserPresenter < SimpleDelegator include ActionView::Helpers::UrlHelper - include Decidim::TranslatableAttributes + include Decidim::SanitizeHelper + + # name sanitized + def name + decidim_sanitize_translated(__getobj__.name) + end # # nickname presented in a twitter-like style diff --git a/decidim-core/app/queries/decidim/last_activity.rb b/decidim-core/app/queries/decidim/last_activity.rb index b54d2bf7f30e8..5025ad8a2d0db 100644 --- a/decidim-core/app/queries/decidim/last_activity.rb +++ b/decidim-core/app/queries/decidim/last_activity.rb @@ -79,7 +79,7 @@ def filter_spaces(query) Decidim.participatory_space_manifests.map do |manifest| klass = manifest.model_class_name.constantize - condition = if klass.include?(Decidim::HasPrivateUsers) + condition = if klass.include?(Decidim::ParticipatorySpace::HasMembers) Arel.sql( <<~SQL.squish ( diff --git a/decidim-core/app/serializers/decidim/exporters/participatory_space_serializer.rb b/decidim-core/app/serializers/decidim/exporters/participatory_space_serializer.rb index 6e02322533747..a74a9c6764cef 100644 --- a/decidim-core/app/serializers/decidim/exporters/participatory_space_serializer.rb +++ b/decidim-core/app/serializers/decidim/exporters/participatory_space_serializer.rb @@ -28,7 +28,7 @@ def serialize short_description: resource.short_description, description: resource.description, promoted: resource.promoted, - component_settings: component_settings + component_settings: } end diff --git a/decidim-core/app/uploaders/decidim/organization_favicon_uploader.rb b/decidim-core/app/uploaders/decidim/organization_favicon_uploader.rb index 7778bb3f4cb27..68f62e9326957 100644 --- a/decidim-core/app/uploaders/decidim/organization_favicon_uploader.rb +++ b/decidim-core/app/uploaders/decidim/organization_favicon_uploader.rb @@ -7,7 +7,8 @@ class OrganizationFaviconUploader < ImageUploader huge: 512, big: 192, medium: 180, - small: 32 + small: 32, + favicon: 256 }.freeze set_variants do @@ -16,17 +17,11 @@ class OrganizationFaviconUploader < ImageUploader resize_and_pad: [value, value], format: :png } - end.merge( - favicon: { - resize_and_pad: [256, 256], - define: "icon:auto-resize=16,24,32,48,64,72,96,128,256", - format: :ico - } - ) + end end def extension_allowlist - %w(png jpg jpeg webp ico) + %w(png jpg jpeg webp) end end end diff --git a/decidim-core/app/validators/uploader_image_dimensions_validator.rb b/decidim-core/app/validators/uploader_image_dimensions_validator.rb index bfd26fab16349..9a9af8d49ddaa 100644 --- a/decidim-core/app/validators/uploader_image_dimensions_validator.rb +++ b/decidim-core/app/validators/uploader_image_dimensions_validator.rb @@ -3,7 +3,7 @@ # This validator checks when the files to be uploaded are images and the attached uploader's # has enabled dimensions validation that the image dimensions are below the # limit defined by the uploader -require "mini_magick" +require "vips" class UploaderImageDimensionsValidator < ActiveModel::Validations::FileContentTypeValidator def validate_each(record, attribute, value) @@ -32,14 +32,12 @@ def validate_image_size(record, attribute, file, uploader) # avoid reckless users that upload images with too many pixels. # # See https://hackerone.com/reports/390 - record.errors.add attribute, I18n.t("decidim.errors.files.file_resolution_too_large") if image.dimensions.any? { |dimension| dimension > uploader.max_image_height_or_width } - rescue MiniMagick::Error + max_dimension = [image.width, image.height].max + record.errors.add attribute, I18n.t("decidim.errors.files.file_resolution_too_large") if max_dimension > uploader.max_image_height_or_width + rescue Vips::Error # The error may happen because of many reasons but most commonly the image - # exceeds the default maximum dimensions set for ImageMagick when the - # `identify` command fails to identify the image. - # - # To relax ImageMagick default limits, please refer to: - # https://imagemagick.org/script/security-policy.php + # exceeds the default maximum dimensions set for libvips when the image + # fails to load. # # Note that the error can also happen because of other reasons than only # the image dimensions being too large. But as we do not really know the @@ -51,11 +49,11 @@ def extract_image(file) return unless file.try(:content_type).to_s.start_with?("image") if uploaded_file?(file) - MiniMagick::Image.new(file.path, File.extname(file.original_filename)) + Vips::Image.new_from_file(file.path) elsif file.is_a?(ActiveStorage::Attached) && file.blob.persisted? - MiniMagick::Image.read(file.blob.download, File.extname(file.blob.filename.to_s)) + Vips::Image.new_from_buffer(file.blob.download, "") end - rescue ActiveStorage::FileNotFoundError, MiniMagick::Invalid + rescue ActiveStorage::FileNotFoundError, Vips::Error # Although the blob is persisted, the file is not available to download and analyze # after committing the record nil diff --git a/decidim-core/app/views/decidim/manifests/show.json.erb b/decidim-core/app/views/decidim/manifests/show.json.erb index 2c3b6d412af13..7b3ed0e32981b 100644 --- a/decidim-core/app/views/decidim/manifests/show.json.erb +++ b/decidim-core/app/views/decidim/manifests/show.json.erb @@ -1,5 +1,6 @@ { "name": "<%= organization_params.html_name %>", + "short_name": "<%= organization_params.html_short_name %>", "lang": "<%= organization_params.default_locale %>", "description": "<%= organization_params.translated_description %>", "display": "<%= organization_params.pwa_display %>", diff --git a/decidim-core/app/views/decidim/participatory_space/members/_member.html.erb b/decidim-core/app/views/decidim/participatory_space/members/_member.html.erb new file mode 100644 index 0000000000000..8cd1bdbf22597 --- /dev/null +++ b/decidim-core/app/views/decidim/participatory_space/members/_member.html.erb @@ -0,0 +1 @@ +<%= cell "decidim/member", Decidim::ParticipatorySpace::MemberPresenter.new(member) %> diff --git a/decidim-core/app/views/decidim/participatory_space_private_users/_participatory_space_private_user.html.erb b/decidim-core/app/views/decidim/participatory_space_private_users/_participatory_space_private_user.html.erb deleted file mode 100644 index 69f415e14ccb2..0000000000000 --- a/decidim-core/app/views/decidim/participatory_space_private_users/_participatory_space_private_user.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= cell "decidim/participatory_space_private_user", Decidim::ParticipatorySpacePrivateUserPresenter.new(participatory_space_private_user) %> diff --git a/decidim-core/app/views/devise/mailer/invite_private_user.html.erb b/decidim-core/app/views/devise/mailer/invite_member.html.erb similarity index 81% rename from decidim-core/app/views/devise/mailer/invite_private_user.html.erb rename to decidim-core/app/views/devise/mailer/invite_member.html.erb index 3694d05a6827d..382aebadde83d 100644 --- a/decidim-core/app/views/devise/mailer/invite_private_user.html.erb +++ b/decidim-core/app/views/devise/mailer/invite_member.html.erb @@ -2,9 +2,9 @@ diff --git a/decidim-core/app/views/devise/mailer/invite_private_user.text.erb b/decidim-core/app/views/devise/mailer/invite_member.text.erb similarity index 68% rename from decidim-core/app/views/devise/mailer/invite_private_user.text.erb rename to decidim-core/app/views/devise/mailer/invite_member.text.erb index 8f572859214a1..aeab6dba9d470 100644 --- a/decidim-core/app/views/devise/mailer/invite_private_user.text.erb +++ b/decidim-core/app/views/devise/mailer/invite_member.text.erb @@ -1,9 +1,9 @@ <%= t("devise.mailer.invitation_instructions.hello", email: @resource.name) %> <% if @resource.invited_by.present? %> -<%= t("devise.mailer.invitation_instructions.invited_you_as_private_user", invited_by: @resource.invited_by.name, application: organization_name(@resource.organization)) %> +<%= t("devise.mailer.invitation_instructions.invited_you_as_member", invited_by: @resource.invited_by.name, application: organization_name(@resource.organization)) %> <% else %> -<%= t("devise.mailer.invitation_instructions.someone_invited_you_as_private_user", application: organization_name(@resource.organization)) %> +<%= t("devise.mailer.invitation_instructions.someone_invited_you_as_member", application: organization_name(@resource.organization)) %> <% end %> <%= accept_invitation_url(@resource, invitation_token: @token, host: @resource.organization.host) %> diff --git a/decidim-core/app/views/layouts/decidim/_wrapper.html.erb b/decidim-core/app/views/layouts/decidim/_wrapper.html.erb index 067a78c7d2c30..ffc18cf5b6fae 100644 --- a/decidim-core/app/views/layouts/decidim/_wrapper.html.erb +++ b/decidim-core/app/views/layouts/decidim/_wrapper.html.erb @@ -12,7 +12,7 @@ end
    > -
    +
    <%= render partial: "layouts/decidim/impersonation_warning" %> <%= render partial: "layouts/decidim/omnipresent_banner" %> <%= render partial: "layouts/decidim/offline_banner" %> diff --git a/decidim-core/app/views/layouts/decidim/header/_focus_mode_back_button.html.erb b/decidim-core/app/views/layouts/decidim/header/_focus_mode_back_button.html.erb index 9d027b5f2d97f..a0e8cf78ca94b 100644 --- a/decidim-core/app/views/layouts/decidim/header/_focus_mode_back_button.html.erb +++ b/decidim-core/app/views/layouts/decidim/header/_focus_mode_back_button.html.erb @@ -9,15 +9,14 @@ <% end %>
    -
    - <%= f.label t(".select_states") %> - <%= f.collection_check_boxes :states, @form.states_collection, :token, :title do |b| %> -
    - <%= b.label { b.check_box + translated_attribute(b.text) } %> + + <% if @form.available_states.any? %> +
    + <%= f.collection_check_boxes :states, @form.available_states, :token, ->(a) { translated_attribute(a.title) } do |builder| %> +
    + <%= builder.label { builder.check_box + builder.text } %> +
    + <% end %>
    <% end %>
    @@ -29,9 +33,6 @@
    <%= f.check_box :keep_answers %>
    -
    - <%= f.check_box :import_proposals %> -
    diff --git a/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_edit_form_fields.html.erb b/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_edit_form_fields.html.erb index a01db0fd69890..ecb6049f3bbd6 100644 --- a/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_edit_form_fields.html.erb +++ b/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_edit_form_fields.html.erb @@ -21,5 +21,6 @@ button_edit_label: t("decidim.proposals.collaborative_drafts.new.edit_file"), button_class: "button button__lg button__transparent-secondary w-full", help_text: t("attachment_legend", scope: "decidim.proposals.collaborative_drafts.edit"), - help_i18n_scope: "decidim.forms.file_help.file" %> + help_i18n_scope: "decidim.forms.file_help.file", + paragraph: true %> <% end %> diff --git a/decidim-proposals/app/views/decidim/proposals/proposals/_edit_form_fields.html.erb b/decidim-proposals/app/views/decidim/proposals/proposals/_edit_form_fields.html.erb index df4e316399256..90175af59b412 100644 --- a/decidim-proposals/app/views/decidim/proposals/proposals/_edit_form_fields.html.erb +++ b/decidim-proposals/app/views/decidim/proposals/proposals/_edit_form_fields.html.erb @@ -22,7 +22,6 @@ <%= filter_taxonomy_items_select_field form, :taxonomies, filter %> <% end %> <% end %> - <% if component_settings.attachments_allowed? && (new_proposal || @proposal) %> <%= form.attachment :documents, multiple: true, @@ -31,5 +30,6 @@ button_edit_label: t("decidim.proposals.proposals.edit.edit_attachments"), button_class: "button button__lg button__transparent-secondary w-full", help_i18n_scope: "decidim.forms.file_help.file", - help_text: t("attachment_legend", scope: "decidim.proposals.proposals.edit") %> + help_text: t("attachment_legend", scope: "decidim.proposals.proposals.edit"), + paragraph: true %> <% end %> diff --git a/decidim-proposals/app/views/decidim/proposals/proposals/_votes_count.html.erb b/decidim-proposals/app/views/decidim/proposals/proposals/_votes_count.html.erb index df48911abd243..fde49c5ff07b8 100644 --- a/decidim-proposals/app/views/decidim/proposals/proposals/_votes_count.html.erb +++ b/decidim-proposals/app/views/decidim/proposals/proposals/_votes_count.html.erb @@ -1,4 +1,4 @@ -<% if !current_settings.votes_hidden? && (current_component.participatory_space.can_participate?(current_user) || current_user.admin?) %> +<% if !current_settings.votes_hidden? && (current_component.participatory_space.can_participate?(current_user) || current_user&.admin?) %> <% if component_settings.participatory_texts_enabled? && from_proposals_list %> <%= render partial: "decidim/proposals/proposals/participatory_texts/proposal_votes_count", locals: { proposal:, from_proposals_list: true } %> <% else %> diff --git a/decidim-proposals/app/views/decidim/proposals/proposals/index.html.erb b/decidim-proposals/app/views/decidim/proposals/proposals/index.html.erb index 9371bda1c8c23..af5bce51a3f85 100644 --- a/decidim-proposals/app/views/decidim/proposals/proposals/index.html.erb +++ b/decidim-proposals/app/views/decidim/proposals/proposals/index.html.erb @@ -1,7 +1,7 @@ <% add_decidim_meta_tags( description: translated_attribute(current_participatory_space.short_description), title: t("decidim.components.pagination.page_title", - component_name: component_name, + component_name:, current_page: @proposals.current_page, total_pages: @proposals.total_pages ), url: proposals_url, diff --git a/decidim-proposals/config/locales/cs.yml b/decidim-proposals/config/locales/cs.yml index e8eadad2e6715..7379b5124f4dc 100644 --- a/decidim-proposals/config/locales/cs.yml +++ b/decidim-proposals/config/locales/cs.yml @@ -150,6 +150,8 @@ cs: values: state_not_published: Nezodpovězeno state_published: Odpovězeno + tooltips: + deleted_proposals_info: Tento návrh nelze odstranit application: geocoding: not_configured: Geocoding není nakonfigurován! @@ -333,6 +335,8 @@ cs: email_outro: Toto oznámení jste obdrželi, protože jste sledovali %{participatory_space_title}. Po předchozím propojení můžete přestat přijímat oznámení. email_subject: Návrhy jsou nyní k dispozici v %{participatory_space_title} notification_title: Nyní můžete předložit nové návrhy v %{participatory_space_title}. + liking_enabled: + email_outro: Obdrželi jste toto oznámení, protože sledujete %{participatory_space_title}. Můžete přestat přijímat oznámení na předchozím odkazu. proposal_mentioned: email_intro: Váš návrh "%{mentioned_proposal_title}" byl zmíněn v této skupině v komentářích. email_outro: Toto oznámení jste obdrželi, protože jste autorem položky "%{resource_title}". diff --git a/decidim-proposals/config/locales/de.yml b/decidim-proposals/config/locales/de.yml index 0d58a4e0df184..2dd7dd2303e3d 100644 --- a/decidim-proposals/config/locales/de.yml +++ b/decidim-proposals/config/locales/de.yml @@ -110,8 +110,8 @@ de: one: Anmerkung other: Anmerkungen decidim/proposals/proposal_vote: - one: Abstimmung - other: Abstimmungen + one: Stimme + other: Stimmen decidim: admin: admin_log: diff --git a/decidim-proposals/config/locales/en.yml b/decidim-proposals/config/locales/en.yml index 0b7db59b1814c..28639a99f3f93 100644 --- a/decidim-proposals/config/locales/en.yml +++ b/decidim-proposals/config/locales/en.yml @@ -53,7 +53,7 @@ en: file: File proposals_import: import_proposals: Import proposals - keep_answers: Keep state and answers + keep_answers: Keep statuses and answers keep_authors: Keep original authors errors: models: @@ -690,7 +690,7 @@ en: create: Import proposals no_components: There are no other proposal components in this participatory space to import the proposals from. select_component: Please select a component - select_states: Check the status of the proposals to import + select_states: Import only proposals with these statuses. If none are selected, all proposals will be imported. title: Import proposals from another component proposals_merges: create: diff --git a/decidim-proposals/config/locales/eu.yml b/decidim-proposals/config/locales/eu.yml index 6d27b4da114a2..fdacb074060ca 100644 --- a/decidim-proposals/config/locales/eu.yml +++ b/decidim-proposals/config/locales/eu.yml @@ -386,7 +386,7 @@ eu: badges: accepted_proposals: conditions: - - Aukeratu zure intereseko partaidetza espazioa proposamenak bidaltzeko + - Aukeratu zure intereseko partaidetza-espazioa proposamenak bidaltzeko - Egin daitezkeen proposamenak egiten saiatu. Horrela onartuak izateko aukera gehiago dute. description: Garaikur hau proposamen berriekin aktiboki parte hartzen duzunean eta horiek onartzen direnean ematen da description_another: Parte-hartzaile honek %{score} proposamen onartu ditu. @@ -408,7 +408,7 @@ eu: unearned_own: Oraindik ez duzu proposamenik bozkatu. proposals: conditions: - - Aukeratu zure interesekoa den parte hartzeko espazioa, gaitutako proposamenak aurkeztuta + - Aukeratu zure interesekoa den partaidetza-espazioa, gaitutako proposamenak aurkeztuta - Sortu beste proposamen bat description: Garaikur hau proposamen berriekin modu aktiboan parte hartzen duzunean ematen da. description_another: Parte-hartzaile honek %{score} proposamen sortu ditu. @@ -600,7 +600,7 @@ eu: destroy: success: Egoera behar bezala ezabatua edit: - title: Editatu egoera + title: Egoera editatu update: Eguneratu form: preview: Aurrebistaratu @@ -721,7 +721,7 @@ eu: accepted: Onartuta evaluating: Ebaluatzen not_answered: Erantzun gabe - rejected: Ukatua + rejected: Bazteruta withdrawn: Kendu application_helper: filter_origin_values: @@ -1000,7 +1000,7 @@ eu: already_voted_hover: Kendu babesa maximum_votes_reached: Babesen muga lortua no_votes_remaining: Ez da gelditzen babesik - vote: Alde egin + vote: Babesa eman votes_blocked: Eman botoa votes_count: count: @@ -1023,7 +1023,7 @@ eu: title: Parte hartzeko arauak vote_limit: description: '%{limit} proposameni eman ahal diozu babesa.' - votes: Gainerako %{number} babes + votes: '%{number} babes emateke' wizard_aside: back: Atzera back_from_step_1: Itzuli proposamenetara diff --git a/decidim-proposals/config/locales/pt-BR.yml b/decidim-proposals/config/locales/pt-BR.yml index 4a9573c2c268c..af098b17e92ec 100644 --- a/decidim-proposals/config/locales/pt-BR.yml +++ b/decidim-proposals/config/locales/pt-BR.yml @@ -9,13 +9,21 @@ pt-BR: scope_id: Escopo state: Estado title: Título + evaluation_assignment: + admin_log: + evaluator_role_id: Nome do avaliador + import_participatory_text: + document: Documento proposal: address: Endereço answer: Responda answered_at: Respondido em body: Corpo + decidim_proposals_proposal_state_id: Estado decidim_scope_id: Escopo has_address: Tem endereço + latitude: Latitude + longitude: Longitude scope_id: Escopo state: Estado title: Título @@ -24,8 +32,24 @@ pt-BR: cost: Custo cost_report: Relatório de custos execution_period: Período de execução + proposal_state: + announcement_title: Título da resposta + bg_color: Cor de fundo + colors: + blue: Azul + gray: Cinza + green: Verde + orange: Laranja + pink: Rosa + purple: Roxo + red: Vermelho + yellow: Amarelo + text_color: Cor do texto proposals_copy: + copy_proposals: Entendo que isso importará todas as propostas do componente selecionado para o componente atual e que essa ação não pode ser desfeita. origin_component_id: Componente para copiar as propostas de + proposals_file_import: + file: Arquivo proposals_import: import_proposals: Importar propostas keep_answers: Manter o estado e as respostas @@ -47,13 +71,35 @@ pt-BR: identical: E o título não pode ser idêntico title: identical: E o corpo não pode ser idêntico + proposals_merge: + attributes: + base: + not_official: Não são oficiais + voted: Já recebeu votos ou curtidas + proposals_split: + attributes: + base: + not_official: Não são oficiais + voted: Já recebeu votos ou curtidas models: + decidim/proposals/admin/update_proposal_taxonomies_event: Taxonomias da proposta alteradas decidim/proposals/creation_enabled_event: Criação de proposta ativada + decidim/proposals/liking_enabled_event: Curtir propostas ativado decidim/proposals/proposal_mentioned_event: Proposta mencionada decidim/proposals/publish_proposal_event: Proposta publicada decidim/proposals/voting_enabled_event: Proposta de votação ativada activerecord: models: + decidim: + proposals: + proposal: + budget_text: A proposta %{link} foi criada + import_from_proposal_text: 'Foi tornado esta proposta: %{link}' + import_to_proposal_text: A proposta %{link} foi criada + merge_from_proposal_text: 'Foi tornado esta proposta: %{link}' + merge_to_proposal_text: 'Esta proposta foi criada: %{link}' + split_from_proposal_text: 'Foi tornado esta proposta: %{link}' + split_to_proposal_text: 'Esta proposta foi criada: %{link}' decidim/proposals/collaborative_draft: one: Rascunho colaborativo other: Rascunhos colaborativos @@ -68,13 +114,20 @@ pt-BR: other: Votos decidim: admin: + admin_log: + changeset: + proposals: Propostas filters: proposals: + evaluator_role_ids_has: + label: Atribuído ao avaliador is_emendation_true: label: Tipo values: 'false': Propostas 'true': Emendas + proposal_state_id_eq: + label: Estado state_eq: label: Estado values: @@ -85,14 +138,27 @@ pt-BR: rejected: Rejeitado validating: Validação técnica withdrawn: Retirado + taxonomies_part_of_contains: + label: Taxonomia with_any_state: + label: Respondido values: state_not_published: Não respondido + state_published: Respondido + tooltips: + cannot_edit_proposal_info: Não é possível editar esta proposta, pois ela foi criada por um participante ou já recebeu votos + deleted_proposal_states_info: Não é possível excluir o estado desta proposta porque há propostas atribuídas a ela + deleted_proposals_info: Não é possível excluir esta proposta + application: + geocoding: + not_configured: Geocodificação não está configurado! components: proposals: actions: amend: Alterar comment: Comentar + create: Criar uma proposta + like: Curtir vote: Voto vote_comment: Votar no documento withdraw: retirar o @@ -104,15 +170,36 @@ pt-BR: amendments_wizard_help_text: Texto de ajuda do Assistente announcement: Anúncio attachments_allowed: Permitir anexos + attachments_allowed_help: Ao ativar esta opção, as propostas serão predefinidas no modo de grade, e a primeira imagem aparecerá no cartão. + can_accumulate_votes_beyond_threshold: Pode acumular votos além do limite + clear_all: Limpar tudo collaborative_drafts_enabled: Rascunhos colaborativos ativados + collaborative_drafts_enabled_help: O recurso de Rascunhos colaborativos será removido no Decidim v0.32. As organizações que o usam, podem alternar para o novo recurso de co-autoria de proposta. comments_enabled: Comentários ativados comments_max_length: Tamanho máximo de comentários (deixe 0 para o valor padrão) + default_sort_order: Classificação de proposta padrão + default_sort_order_help: Automático significa que, se os votos estiverem habilitados, as propostas serão mostradas por ordem aleatória, e se as votações estiverem bloqueadas, serão ordenadas pela maioria das votações. default_sort_order_options: + automatic: Automático most_commented: Mais comentado + most_followed: Mais seguido + most_liked: Mais curtido + most_voted: Mais votado + random: Aleatório + recent: Recente + with_more_authors: Com mais autores + define_taxonomy_filters: Por favor, defina alguns filtros para este espaço participativo antes de usar esta configuração. + edit_time: As propostas podem ser editadas pelos autores antes que esse tempo decorra + edit_time_units: + days: Dias + hours: Horas + minutes: Minutos + geocoding_enabled: Mapas ativados minimum_votes_per_user: Mínimo de votos por usuário new_proposal_body_template: Novo modelo de corpo da proposta new_proposal_body_template_help: Você pode definir o texto pré-preenchido que as novas propostas terão new_proposal_help_text: Novo texto de ajuda da proposta + no_taxonomy_filters_found: Nenhum filtro de taxonomia encontrado. official_proposals_enabled: Proposta oficial habilitada participatory_texts_enabled: Textos participativos habilitados participatory_texts_enabled_readonly: Não é possível interagir com esta configuração se houver propostas existentes. Por Favor, crie um novo componente `Propostas` se você quiser habilitar esta funcionalidade ou descartar todas as propostas importadas no menu `Textos Participatórios` se você quiser desativá-la. @@ -121,11 +208,19 @@ pt-BR: proposal_edit_time_choices: infinite: Permitir a edição de propostas por um período infinito de tempo limited: Permitir a edição de propostas em um período de tempo específico + proposal_edit_time_unit_options: + days: Dias + hours: Horas + minutes: Minutos proposal_length: Comprimento máximo do corpo da proposta proposal_limit: Limite da proposta por usuário proposal_wizard_step_1_help_text: Assistente de propostas "Criar" passo ajuda texto + proposal_wizard_step_2_help_text: Texto de ajuda da etapa "Publicar" do assistente de propostas resources_permissions_enabled: Permissões de ações podem ser definidas para cada proposta + taxonomy_filters: Selecione os filtros para o componente + taxonomy_filters_add: Adicionar filtro threshold_per_proposal: Limiar por proposta + vote_limit: Limite de votos por participante step: amendment_creation_enabled: Criação de alteração ativada amendment_creation_enabled_help: O usuário pode alterar propostas. @@ -143,18 +238,66 @@ pt-BR: comments_blocked: Comentários bloqueados creation_enabled: Os participantes podem criar propostas creation_enabled_readonly: Essa configuração é desativada quando você ativa a funcionalidade de textos participativos. Para enviar propostas como texto participativo, clique no botão de textos participativos e siga as instruções. + default_sort_order: Classificação de propostas padrão + default_sort_order_help: O termo "automático" significa que, se as votações estiverem ativadas, as propostas serão exibidas em ordem aleatória e, se as votações estiverem bloqueadas, serão ordenadas pelas mais votadas. default_sort_order_options: + automatic: Automático most_commented: Mais comentado + most_followed: Mais seguido + most_liked: Mais curtido + most_voted: Mais votado + random: Aleatório + recent: Recente + with_more_authors: Com mais autores + likes_blocked: Curtidas bloqueadas + likes_enabled: Curtidas ativadas proposal_answering_enabled: Resposta de proposta ativada publish_answers_immediately: Publicar respostas da proposta imediatamente + publish_answers_immediately_help_html: 'Observe que, se você responder a alguma proposta sem essa opção ativada, precisará publicá-la manualmente, selecionando-a e usando a ação de publicação. Para obter mais informações sobre como isso funciona, consulte página de documentação das respostas às propostas.' votes_blocked: Votos bloqueados votes_enabled: Votos ativados votes_hidden: Votos ocultos (se os votos estiverem ativados, verificar isso esconderá o número de votos) + download_your_data: + show: + proposal_comments: Exportação de comentários da proposta + proposals: Exportação de propostas events: proposals: + accepted_coauthorship: + notification_title: Você foi adicionado como um co-autor da proposta %{resource_title}. admin: + proposal_assigned_to_evaluator: + email_intro: Você foi designado como avaliador da proposta "%{resource_title}". Isso significa que você foi incumbido de dar comentário e uma resposta adequada nos próximos dias. Confira no painel de administração. + email_outro: Você recebeu esta notificação porque pode avaliar a proposta. + email_subject: Você foi atribuído como um avaliador da proposta %{resource_title}. + notification_title: Você foi atribuído como um avaliador da proposta %{resource_title}. Confira no painel de administração. proposal_note_created: + email_intro: '%{author_name} criou uma nota privada em %{resource_title}. Confira no painel de administração.' + email_outro: Você recebeu esta notificação porque pode avaliar a proposta. email_subject: Alguém deixou uma nota na proposta %{resource_title}. + notification_title: %{author_name} %{author_nickname} criou uma nota privada em %{resource_title}. Confira em o painel de administração. + proposal_note_mentioned: + email_intro: Você foi mencionado em uma nota privada em"%{resource_title}" por "%{author_name}"%{author_nickname}". Confira a painel de administração. + email_outro: Você recebeu esta notificação porque você foi mencionado em uma nota privada. + email_subject: Alguém mencionou você numa nota sobre a proposta %{resource_title}. + notification_title: Você foi mencionado em uma nota privada em %{resource_title} por %{author_name} %{author_nickname}. Confira a o painel de administração. + proposal_note_replied: + email_intro: '%{author_name} respondeu sua nota privada em %{resource_title}. Confira a painel de administração.' + email_outro: Você recebeu esta notificação porque você é o autor da nota. + email_subject: "%{author_name} respondeu a sua publicação privada em %{resource_title}." + notification_title: %{author_name} %{author_nickname} respondeu a sua nota privada em %{resource_title}. Confira a o painel de administração. + coauthor_accepted_invite: + notification_title: %{coauthor_name} aceitou seu convite para se tornar um co-autor da proposta %{resource_title}. + coauthor_invited: + actions: + accept: Aceitar + decline: Recusar + email_intro: 'Você foi convidado para ser um co-autor da proposta "%{resource_title}". Você pode aceitar ou recusar o convite nesta página:' + email_outro: Você recebeu esta notificação porque o autor da proposta quer reconhecer suas contribuições tornando-se um co-autor. + email_subject: Você foi convidado para ser um co-autor da proposta "%{resource_title}" + notification_title: %{author_name} gostaria de convidá-lo como co-autor da proposta %{resource_title}. + coauthor_rejected_invite: + notification_title: %{coauthor_name} recusou seu convite para se tornar um co-autor da proposta %{resource_title}. collaborative_draft_access_accepted: email_intro: '%{requester_name} foi aceito para acessar como colaborador do rascunho colaborativo %{resource_title}.' email_outro: Você recebeu esta notificação porque é colaborador de %{resource_title}. @@ -189,11 +332,22 @@ pt-BR: email_intro: 'Agora você pode criar novas propostas em %{participatory_space_title}! Comece a participar nesta página:' email_outro: Você recebeu esta notificação porque está seguindo %{participatory_space_title}. Você pode parar de receber notificações após o link anterior. email_subject: Propostas agora disponíveis em %{participatory_space_title} + notification_title: Agora você pode apresentar novas propostas em %{participatory_space_title}. + liking_enabled: + email_intro: 'Você pode curtir propostas em %{participatory_space_title}! Comece a participar nesta página:' + email_outro: Você recebeu esta notificação porque está seguindo %{participatory_space_title}. Você pode parar de receber notificações após o link anterior. + email_subject: A curtida nas propostas começou por %{participatory_space_title} + notification_title: Agora você pode começar acurtir propostas em %{participatory_space_title}. proposal_mentioned: email_intro: Sua proposta "%{mentioned_proposal_title}" foi mencionada neste espaço nos comentários. email_outro: Você recebeu esta notificação porque é um autor de "%{resource_title}". email_subject: Sua proposta "%{mentioned_proposal_title}" foi mencionada notification_title: Sua proposta "%{mentioned_proposal_title}" foi mencionada neste espaço nos comentários. + proposal_merged: + email_intro: 'Um administrador mesclou sua proposta "%{resource_title}", confira nesta página:' + email_outro: Você recebeu esta notificação porque você é o autor da proposta. + email_subject: A proposta %{resource_title} foi mesclada + notification_title: A proposta %{resource_title} foi mesclada por um administrador. proposal_published: email_intro: '%{author_name} %{author_nickname}, que você está seguindo, publicou uma nova proposta chamada "%{resource_title}". Confira e contribua:' email_outro: Você recebeu esta notificação porque está seguindo %{author_nickname}. Você pode parar de receber notificações após o link anterior. @@ -205,9 +359,28 @@ pt-BR: email_subject: Nova proposta "%{resource_title}" adicionada a %{participatory_space_title} notification_title: A proposta %{resource_title} foi adicionada a %{participatory_space_title} por %{author}. notification_title_official: A proposta oficial %{resource_title} foi adicionada à %{participatory_space_title}. + proposal_state_changed: + affected_user: + email_intro: 'A proposta "%{resource_title}" mudou seu estado para "%{state}". Você pode ler a resposta nesta página:' + email_outro: Você recebeu esta notificação porque é um autor de "%{resource_title}". + email_subject: Sua proposta mudou seu estado (%{state}) + notification_title: Sua proposta %{resource_title} mudou seu estado para "%{state}". + follower: + email_intro: 'A proposta "%{resource_title}" mudou seu estado para "%{state}". Você pode ler a resposta nesta página:' + email_outro: Você recebeu esta notificação porque está seguindo "%{resource_title}". Você pode ignorá-lo do link anterior. + email_subject: Uma proposta que você está seguindo mudou seu estado (%{state}) + notification_title: A proposta %{resource_title} mudou seu estado para "%{state}". + proposal_update_taxonomies: + email_intro: 'Um administrador atualizou as taxonomias de sua proposta "%{resource_title}", confira nesta página:' + email_outro: Você recebeu esta notificação porque você é o autor da proposta. + email_subject: As taxonomias da proposta %{resource_title} foram atualizadas + notification_title: As taxonomias %{resource_title} da proposta foram atualizadas por um administrador. + rejected_coauthorship: + notification_title: Você recusou o convite de %{author_name} para se tornar um co-autor da proposta %{resource_title}. voting_enabled: email_intro: 'Você pode votar propostas em %{participatory_space_title}! Comece a participar nesta página:' email_outro: Você recebeu esta notificação porque está seguindo %{participatory_space_title}. Você pode parar de receber notificações após o link anterior. + email_subject: A votação da proposta começou para %{participatory_space_title} notification_title: Agora você pode iniciar propostas de votação em %{participatory_space_title} gamification: badges: @@ -225,7 +398,14 @@ pt-BR: proposal_votes: conditions: - Navegue e passe algum tempo lendo as propostas de outras pessoas - - + - Vote nas propostas que você gosta ou acha interessantes + description: Este selo é concedido quando votam nas propostas das outras pessoas. + description_another: Este usuário votou nas propostas %{score}. + description_own: Você votou nas propostas %{score}. + name: Votos da proposta + next_level_in: Vote em mais %{score} propostas para alcançar o próximo nível! + unearned_another: Este usuário ainda não votou em nenhuma proposta. + unearned_own: Você ainda não votou em nenhuma proposta. proposals: conditions: - Escolha o espaço de participação de seu interesse com o envio de propostas ativadas @@ -235,26 +415,121 @@ pt-BR: description_own: Você criou %{score} propostas. name: Propostas next_level_in: Crie mais %{score} propostas para alcançar o próximo nível! + unearned_another: Este usuário ainda não criou nenhuma proposta. unearned_own: Você não criou nenhuma proposta ainda. + open_data: + help: + proposal_comments: + alignment: Se este comentário foi um favorito, contra ou neutro + author: O nome do usuário que fez este comentário + body: O comentário em si + commentable_id: A identificação única do comentável + commentable_type: O tipo do comentável (se for um resultado, uma proposta, etc.) + created_at: A data em que este comentário foi criado + depth: O lugar onde este comentário está nos três comentários (se for uma resposta ou uma resposta de uma resposta) + id: O Id para este comentário + locale: A localidade (linguagem) que o usuário teve ao deixar este comentário + root_commentable_url: A URL do recurso ligado a este comentário + proposals: + address: O endereço da proposta caso a proposta tenha um local físico + answer: A resposta à proposta no caso de ela ter sido respondida + answered_at: A data em que essa proposta foi respondida + attachments: O número de anexos que essa proposta tem + author: Os dados para o autor desta proposta + body: O corpo da proposta + coauthorships_count: O número de coautorias que a proposta representa + comments: O número de comentários que essa proposta tem + component: O componente ao qual a proposta pertence + cost: O custo total da proposta em questão + cost_report: Um relatório de custos para a proposta + created_at: A data em que a proposta foi criada + created_in_meeting: Se a proposta foi criada em uma reunião + execution_period: O período em que a proposta correu do início ao fim + follows_count: O número de seguidores que essa proposta tem + id: O identificador único para a proposta + is_amend: Esta proposta está adiando outra proposta + latitude: A latitude da proposta no caso de ter uma localização física + likes: O número de curtidas ("curtidas") que essa proposta tem + longitude: A longitude da proposta no caso de ela ter um local físico + meeting_urls: As URLs das reuniões onde esta proposta foi apresentada ou discutida + original_proposal: A referência à proposta original se esta for uma alteração + participatory_space: A qual espaço (por exemplo, processo participativo, ou Assembléia) esta proposta pertence + published_at: A data em que esta proposta foi publicada + reference: O identificador único do recurso nesta plataforma + related_proposals: As propostas relacionadas a esta proposta + state: 'O status desta proposta (ex: "Aceito")' + state_published_at: Um registro de tempo do estado da proposta quando publicado + taxonomies: As taxonomias a que esta proposta pertence + title: O título da proposta + updated_at: Data da última atualização da proposta + url: A URL onde esta proposta pode ser encontrada + votes: O número de votos que esta proposta tem + withdrawn: Se essa proposta foi retirada + withdrawn_at: Quando essa proposta foi retirada participatory_spaces: highlighted_proposals: see_all: Ver todos proposals: actions: answer_proposal: Responder proposta + cancel_coauthor_invitation: Cancelar convite de co-autor + cancel_coauthor_invitation_confirm: Tem certeza de que deseja cancelar o convite de co-autor? + delete_proposal_state_confirm: Tem certeza que deseja excluir este estado? + destroy: Excluir estado edit_proposal: Editar proposta + edit_proposal_state: Editar estado import: Importar de outro componente + mark_as_coauthor: Marcar como co-autor + mark_as_coauthor_confirm: Tem certeza que deseja marcar este usuário como um co-autor? O destinatário receberá uma notificação para aceitar ou recusar o convite. new: Nova proposta + new_proposal_state: Novo status participatory_texts: Textos participativos show: Mostrar proposta title: Ações admin: actions: + confirm_delete_proposal: Tem certeza que deseja excluir esta proposta? + deleted_proposals_info: Propostas excluídas podem ser restauradas do lixo. preview: Previsualização + view_deleted_proposals: Ver propostas excluídas + deprecation_warning_collaborative_drafts_html: "⚠️ Aviso de descontinuação. O recurso de Rascunhos Colaborativos será removido na versão 0.32 do Decidim. As organizações que o utilizavam poderão migrar para o novo recurso de coautoria de propostas." + evaluation_assignments: + create: + invalid: Houve um problema ao atribuir propostas a um avaliador. + success: Propostas atribuídas a um avaliador com sucesso. + delete: + invalid: Houve um problema ao desatribuir propostas de um avaliador. + success: Avaliador desatribuído das propostas com sucesso. exports: proposal_comments: Comentários proposals: Propostas + import_proposals_mailer: + notify_failure: + body: Houve um problema ao importar propostas do componente %{origin_component_name} para o componente %{target_component_name}. + subject: Houve um erro ao importar propostas + notify_success: + added_proposals: + one: Uma proposta foi importada. + other: "%{count} propostas foram importadas." + body: Propostas importadas com sucesso do componente %{origin_component_name} para o componente %{target_component_name}. Você pode rever os resultados na interface de administração. + subject: As propostas foram importadas com sucesso imports: + help: + answers: | + O documento de importação deve conter os seguintes nomes de coluna, no caso de arquivos CSV ou Excel, ou nomes de chave, no caso de arquivos JSON (outras colunas serão ignoradas): +
      +
    • id: Id da proposta a ser respondida
    • +
    • estado: Uma das opções "aceito", "avaliando" ou "rejeitado"
    • +
    • resposta/en: Resposta no idioma inglês. Isso dependerá da configuração de idioma de sua plataforma.
    • +
    + proposals: | + O arquivo deve ter os seguintes nomes de coluna em caso de arquivos CSV ou Excel. ou nomes de chaves no caso dos arquivos JSON: +
      +
    • titulo/en: Título na língua inglesa. Isto dependerá da configuração de idioma da sua plataforma.
    • +
    • corpo/en: corpo em inglês. Isto dependerá da configuração de linguagem da sua plataforma.
    • +
    • escopo/id: Id do Escopo
    • +
    • categoria/id: Id das Taxonomias (se houver mais de uma, separe com vírgula)
    • +
    label: answers: Importar respostas de um arquivo proposals: Importar propostas de um arquivo @@ -262,7 +537,11 @@ pt-BR: answers: one: resposta na proposta other: respostas na proposta + proposals: + one: proposta + other: propostas title: + answers: Importar respostas de proposta de um arquivo proposals: Importar propostas de um arquivo models: proposal: @@ -288,8 +567,12 @@ pt-BR: md: Markdown odt: ODT bottom_hint: "(Você será capaz de visualizar e classificar as seções do documento)" + document_legend: 'Adicione um documento menor que 2MB, cada seção até que 3 níveis de profundidade sejam analisados em propostas. Formatos suportados são: %{valid_mime_types}' title: ADICIONAR DOCUMENTO upload_document: Carregar documento + publish: + invalid: Ocorreu um erro ao publicar propostas. + success: Todas as propostas foram publicadas. sections: article: "Artigo" section: "Seção: %{title}" @@ -301,11 +584,44 @@ pt-BR: answer_proposal: Responda title: Resposta para a proposta %{title} proposal_notes: + create: + error: Houve um problema ao criar está nota de proposta. + success: Nota de proposta criada com sucesso. form: note: Nota submit: Enviar + reply: + error: Houve um problema ao criar está resposta de nota de proposta. + success: Resposta de nota de proposta criada com sucesso. title: Notas privadas + proposal_states: + create: + error: Erro ao criar estado + success: Status criado com sucesso + destroy: + success: Status excluído com sucesso + edit: + title: Editar status + update: Atualização + form: + preview: Pré-visualizar + index: + title: Status + new: + create: Criar + title: Novo status + update: + error: Erro ao atualizar status + success: Status atualizado com sucesso proposals: + answer: + bulk_answer_error: Propostas com IDs [%{proposals}] não puderam ser respondidas devido erros aplicando o modelo "%{template}". Você pode verificar que o modelo de resposta corresponde ao formato esperado para este componente, aplicando-o individualmente. + bulk_answer_success: '%{count} propostas serão respondidas usando o modelo "%{template}". Por favor, aguarde alguns minutos e atualize a página para ver as atualizações.' + invalid: Houve erros ao responder a esta proposta. + success: Proposta respondida com sucesso. + create: + invalid: Houve erros ao criar esta proposta. + success: Proposta criada com sucesso. edit: title: Atualizar proposta update: Atualizar @@ -314,44 +630,91 @@ pt-BR: select_a_meeting: Selecione uma reunião index: actions: Ações + apply_answer_template: Aplicar modelo de resposta + assign_to_evaluator: Atribuir ao avaliador + assign_to_evaluator_button: Atribuir cancel: Cancelar + change_taxonomies: Alterar taxonomias merge: Junte-se a um novo + merge_button: Criar + no_templates_available: Nenhum modelo disponível publish: Publicar publish_answers: Publicar respostas select_component: Selecionar componente + select_evaluators: Selecione um ou mais avaliadores selected: selecionado split: Dividir propostas split_button: Dividir + statuses: Status title: Propostas + unassign_from_evaluator: Desatribuir do avaliador + unassign_from_evaluator_button: Desatribuir update: Atualizar + manage_trash: + title: Propostas excluídas new: create: Criar title: Criar proposta + publish_answers: + number_of_proposals: Respostas para a proposta %{number} serão publicadas? + select_a_proposal: Por favor, selecione uma proposta. show: amendments_count: As alterações contam + assigned_evaluators: Avaliadores atribuídos body: Corpo comments_count: Contagem de Comentários documents: Documentos + evaluators: Avaliadores + likes: Curtidas + likes_count: Contagem de curtidas + link: Ver a proposta + n_more_likes: + one: e mais 1 + other: e mais %{count} photos: Fotos ranking: "%{ranking} de %{total}" related_meetings: Reuniões relacionadas remove_assignment: Remover Atribuição + remove_assignment_confirmation: Tem certeza que deseja remover o avaliador desta proposta? + votes_count: Contagem de votos + update_taxonomies: + invalid: 'Essas propostas já tinham as taxonomias %{taxonomies}: %{proposals}.' + select_a_proposal: Por favor, selecione uma proposta. + select_a_taxonomy: Por favor, selecione uma taxonomia. + success: 'Propostas atualizadas com sucesso para as taxonomias %{taxonomies}: %{proposals}.' proposals_imports: + create: + invalid: Houve um problema ao importar as propostas. + success: O processo de importação começou. Vamos informar você assim que terminar. new: create: Propostas de importação no_components: Não há outros componentes da proposta neste espaço participativo para importar as propostas. select_component: Selecione um componente select_states: Verifique os estados das propostas para importar + title: Importar propostas de outro componente proposals_merges: create: + invalid: 'Houve um problema ao mesclar as propostas selecionadas porque algumas delas:' success: Fundiu com sucesso as propostas em uma nova. + form: + created_in_meeting: Esta proposta vem de uma reunião + select_a_meeting: Selecione uma reunião + new: + title: Mesclar em um novo proposals_splits: create: + invalid: 'Houve um problema ao dividir as propostas selecionadas porque algumas delas:' success: Dividiu com sucesso as propostas em novas. admin_log: + evaluation_assignment: + create: "%{user_name} atribuiu a proposta %{resource_name} a um avaliador" + delete: "%{user_name} não atribuiu um avaliador da proposta %{proposal_title}" proposal: answer: "%{user_name} respondeu a proposta %{resource_name} no espaço %{space_name}" + create: "%{user_name} criou a proposta %{resource_name} a partir da fusão de propostas %{merged_count} em %{space_name}" publish_answer: "%{user_name} publicou a resposta para a proposta %{resource_name} no espaço %{space_name}" + restore: "%{user_name} restaurou a proposta %{resource_name} no espaço %{space_name}" + soft_delete: "%{user_name} movido para a lixeira da proposta %{resource_name} no espaço %{space_name}" update: "%{user_name} atualizou a proposta oficial %{resource_name} no espaço %{space_name}" proposal_note: create: "%{user_name} deixou uma nota privada na proposta %{resource_name} no espaço %{space_name}" @@ -379,6 +742,7 @@ pt-BR: publish: error: Houve erros ao publicar o rascunho colaborativo. irreversible_action_modal: + body: Depois de publicar o rascunho como uma proposta, ele não será mais editável.. A proposta não aceitará novos autores ou contribuições. cancel: Cancelar ok: Publicar como uma proposta title: A ação a seguir é irreversível @@ -386,6 +750,7 @@ pt-BR: withdraw: error: Houve erros ao fechar o rascunho colaborativo. irreversible_action_modal: + body: Depois de fechar o rascunho, ele não será mais editável. O rascunho não aceitará novos autores ou contribuições. cancel: Cancelar ok: Retirar o rascunho colaborativo title: A ação a seguir é irreversível @@ -394,9 +759,12 @@ pt-BR: error: Houve um problema ao criar este rascunho colaborativo. success: Rascunho colaborativo criado com sucesso. edit: + attachment_legend: Adicionar um documento ou uma imagem back: Costas send: Enviar title: Editar rascunho colaborativo + empty: Ainda não há rascunhos colaborativos + empty_filters: Não há nenhum rascunho colaborativo com este critério filters: all: Todos amendment: Emendas @@ -415,8 +783,10 @@ pt-BR: count: one: "%{count} rascunho colaborativo" other: "%{count} rascunho colaborativo" + name: Rascunhos colaborativos new: add_file: Adicionar arquivo + edit_file: Editar arquivo send: Continuar new_collaborative_draft_button: new_collaborative_draft: Novo rascunho colaborativo @@ -428,20 +798,25 @@ pt-BR: requests: accepted_request: error: Não foi possível aceitar como colaborador. Tente novamente mais tarde. + success: "@%{user} foi aceito como colaborador com sucesso." access_requested: error: Sua solicitação não pôde ser concluída. Tente novamente mais tarde. + success: Sua solicitação para colaborar foi enviada com sucesso. collaboration_requests: accept_request: Aceitar reject_request: Rejeitar title: Pedidos de colaboração rejected_request: error: Não foi possível rejeitá-lo como colaborador. Tente novamente mais tarde. + success: "@%{user} foi rejeitado como colaborador com sucesso." show: + edit: Editar final_proposal: proposta final final_proposal_help_text: Este rascunho está terminado. Você pode ver a proposta final concluída hidden_authors_count: one: e mais %{count} pessoa other: e mais %{count} pessoas + info-message: Este é um rascunho colaborativo de uma proposta. Isso significa que você pode ajudar os autores a moldar a proposta usando a seção de comentários abaixo ou aprimorá-la diretamente solicitando acesso para editá-la. Assim que os autores concederem o acesso, você poderá fazer alterações neste rascunho. publish: Publicar publish_info: Publicar esta versão do rascunho ou published_proposal: proposta publicada @@ -465,38 +840,86 @@ pt-BR: create: error: Ocorreu erros ao salvar a proposta. success: Proposta criada com sucesso. Salvo como um rascunho. + creation: + imported_text: E assim surgiu esta proposta + merged_text: Eles se tornaram esta proposta + splitted_text: E assim surgiu esta proposta + text: Esta proposta foi criada destroy_draft: error: Houve erros ao excluir o rascunho da proposta. success: O rascunho da proposta foi excluído com sucesso. + exit_modal: + cancel: Cancelar + exit: Continuar + message: Você precisa dar %{number} votos a mais entre as diferentes propostas para que seus votos sejam considerados. + title: Lembre-se que ainda lhe restam %{number} votos + forms: + errors: + device_not_supported: Seu dispositivo não é compatível com serviços de localização. Digite o endereço manualmente. + no_device_location: Desculpe, não foi possível detectar sua localização. Por favor, insira o endereço manualmente. + use_my_location: Usar minha localização atual + invite_coauthors: + cancel: + error: Houve um problema ao cancelar o convite para co-autor. + success: Convite para co-autor cancelado com sucesso. + create: + error: Houve um problema ao convidar o co-autor. + success: "%{author_name} Foi convidado com sucesso como co-autor." + destroy: + error: Houve um problema ao recusar o convite. + success: O convite foi recusado. + update: + error: Ocorreu um erro ao aceitar o convite. + success: O convite foi aceito. + last_activity: + new_proposal: 'Nova proposta:' + proposal_updated: 'Proposta atualizada:' models: proposal: fields: comments: Comentários + evaluator: Avaliador + evaluators: Avaliadores id: ID notes: Notas official_proposal: Proposta oficial published_answer: Resposta publicada published_at: Publicado em state: Estado + taxonomies: Taxonomias title: Título votes: Votos + proposal_state: + css_class: Classe CSS + title: Status + new: + limit_reached: Você não pode criar novas propostas, pois excedeu o limite. participatory_text_proposal: alternative_title: Não há textos participativos no momento buttons: amend: Alterar comment: Comente + proposal_votes: + create: + error: Houve um problema ao votar na proposta. proposals: dynamic_map_instructions: description: As coordenadas serão atualizadas quando clicar no botão 'visualizar'. No entanto, o endereço não muda. instructions: Você pode mover o ponto no mapa. edit: + add_attachments: Adicionar anexos + attachment_legend: Adicionar um documento ou uma imagem back: Voltar + edit_attachments: Editar anexos send: Enviar title: Editar proposta edit_draft: discard: Descarte este rascunho discard_confirmation: Deseja mesmo descartar este rascunho de proposta? send: Visualização + title: Editar rascunho da proposta + edit_form_fields: + marker_added: Marcador adicionado ao mapa. filters: activity: Minha atividade all: Tudo @@ -507,21 +930,29 @@ pt-BR: scope: Escopo search: Pesquisa state: Estado + taxonomy_filters: Filtros type: Tipo voted: Votado index: + click_here: Veja todas as propostas collaborative_drafts_list: Acesse rascunhos colaborativos count: one: "%{count} proposta" other: "%{count} propostas" + grid_mode: Modo grade + list_mode: Modo de lista new_proposal: Nova proposta + see_all: Veja todas as propostas see_all_withdrawn: Veja todas as propostas retiradas + text_banner: Você está vendo a lista de propostas retiradas por seus autores. %{go_back_link}. new: send: Continuar + title: Criar nova proposta orders: label: 'Ordene propostas por:' most_commented: Mais comentados most_followed: Mais seguidos + most_liked: Mais curtidas most_voted: Mais votados random: Aleatório recent: Recente @@ -540,10 +971,17 @@ pt-BR: other: Você poderá editar esta proposta durante o primeiro %{count} minutos após a publicação da proposta. Uma vez que esta janela de tempo passa, você não poderá editar a proposta. publish: Publicar title: Publique sua proposta + proposals: + empty: Ainda não há propostas. + empty_filters: Não há propostas com estes critérios. show: answer: Responda changes_at_title: Alteração para "%{title}" + edit_proposal: Editar estimated_cost: Custo estimado + hidden_likes_count: + one: e mais %{count} pessoa + other: e mais %{count} pessoas link_to_collaborative_draft_help_text: Esta proposta é o resultado de um rascunho colaborativo. Revise o histórico link_to_collaborative_draft_text: Veja o rascunho colaborativo link_to_promoted_emendation_help_text: Esta proposta é uma emenda promovida @@ -553,27 +991,52 @@ pt-BR: proposal_accepted_reason: 'Esta proposta foi aceita porque:' proposal_in_evaluation_reason: Esta proposta está sendo avaliada proposal_rejected_reason: 'Esta proposta foi rejeitada porque:' + withdraw_btn_hint: Você pode retirar sua proposta se mudar de ideia, desde que não tenha recebido nenhum voto. A proposta não é excluída, ela aparecerá na lista de propostas retiradas. withdraw_confirmation_html: Tem certeza que deseja retirar esta proposta?

    Esta ação não pode ser cancelada! + withdraw_proposal: Retirar update: title: Atualizar proposta vote_button: + already_voted: Votado + already_voted_hover: Retirar voto maximum_votes_reached: Limite de votação atingido no_votes_remaining: Não há votos restantes vote: Voto + votes_blocked: Voto + votes_count: + count: + one: Voto + other: Votos voting_rules: + already_vote: + description: Lembre-se de que você ainda tem que votar %{number} entre diferentes propostas para que os seus votos sejam levados em consideração. + see_other_proposals: Veja outras propostas + title: Você tem%{number} votos restantes + can_accumulate_votes_beyond_threshold: + description: Cada proposta pode acumular mais de %{limit} votos + minimum_votes_per_user: + description: Você tem que distribuir um mínimo de %{votes} votos entre diferentes propostas para que os seus votos sejam levados em conta. proposal_limit: description: Você pode criar até %{limit} propostas. + success: Seus votos foram aceitos com sucesso + threshold_per_proposal: + description: Para serem validadas, as propostas precisam chegar aos %{limit} votos. + title: Regras de participação vote_limit: description: Você pode votar até %{limit} propostas. + votes: '%{number} votos restantes' wizard_aside: back: Costas back_from_step_1: Voltar às propostas + back_from_step_2: Voltar à edição wizard_steps: current_step: Etapa atual step_1: Crie sua proposta + step_2: Publique sua proposta title: Etapas de criação proposta proposals_picker: choose_proposals: Escolher propostas + no_proposals: Nenhuma proposta corresponde aos seus critérios de pesquisa ou não há propostas. publish: error: Houve erros ao publicar a proposta. success: Proposta publicada com sucesso. @@ -588,6 +1051,9 @@ pt-BR: versions: index: title: Versões + withdraw: + errors: + has_votes: Esta proposta não pode ser retirada porque já tem votos. resource_links: copied_from_component: proposal_proposal: Propostas relacionadas @@ -597,6 +1063,9 @@ pt-BR: proposal_project: 'Proposta que aparece nesses projetos:' proposal_result: 'Proposta que aparece nesses resultados:' statistics: + participatory_space_proposals_count: Propostas proposals_accepted: Propostas aceitas proposals_count: Propostas + proposals_count_tooltip: O número total de propostas enviadas e votos expressos sobre elas. + votes: 'Votos:' votes_count: Votos diff --git a/decidim-proposals/config/locales/ro-RO.yml b/decidim-proposals/config/locales/ro-RO.yml index a802bd71dc61f..ec78aba93511f 100644 --- a/decidim-proposals/config/locales/ro-RO.yml +++ b/decidim-proposals/config/locales/ro-RO.yml @@ -236,6 +236,11 @@ ro: conditions: - Răsfoiește paginile și petrece puțin timp citind propunerile altora - + description_another: Acest participant a votat %{score} propuneri. + description_own: Ați votat %{score} propuneri. + name: Voturi pentru propunere + unearned_another: Acest participant nu a votat încă nicio propunere. + unearned_own: Încă nu ați votat nicio propunere. proposals: conditions: - Alege spațiul de participare unde e activă opțiunea de a trimite propuneri conform domeniului tău de interes @@ -245,6 +250,7 @@ ro: description_own: Ai creat %{score} propuneri. name: Propuneri next_level_in: Creează încă %{score} propuneri pentru a atinge nivelul următor! + unearned_another: Acest participant nu a creat nicio propunere încă. unearned_own: Nu ai creat încă nicio propunere. participatory_spaces: highlighted_proposals: @@ -518,6 +524,8 @@ ro: invite_coauthors: create: success: "%{author_name} a fost invitat cu succes să fie coautor." + last_activity: + new_proposal: 'Propunere nouă:' models: proposal: fields: diff --git a/decidim-proposals/config/locales/sv.yml b/decidim-proposals/config/locales/sv.yml index 99befa96a9ba9..3d5cdb5c4423e 100644 --- a/decidim-proposals/config/locales/sv.yml +++ b/decidim-proposals/config/locales/sv.yml @@ -178,8 +178,8 @@ sv: default_sort_order_help: Automatiskt betyder att om röstning är aktiverat kommer förslagen sorteras slumpmässigt, och om röstning är avstängt kommer de att sorteras efter mest valda. default_sort_order_options: automatic: Automatisk - most_commented: Mest kommenterade - most_followed: Mest följda + most_commented: Flest kommentarer + most_followed: Flest följare most_liked: Mest gillade most_voted: Flest röster random: Slumpvis @@ -192,7 +192,7 @@ sv: hours: Timmar minutes: Minuter geocoding_enabled: Aktivera visning på karta - minimum_votes_per_user: Minsta antal röster per användare + minimum_votes_per_user: Minsta antal röster per deltagare new_proposal_body_template: Innehållsmall för nytt förslag new_proposal_body_template_help: Du kan ange den förifyllda texten för nya förslag new_proposal_help_text: Hjälptext om nya förslag @@ -210,14 +210,14 @@ sv: hours: Timmar minutes: Minuter proposal_length: Maximal längd på förslagets innehåll - proposal_limit: Förslagsgräns per deltagare + proposal_limit: Max antal lagda förslag per deltagare proposal_wizard_step_1_help_text: Hjälptext till steget "Skapa" i förslagsguiden proposal_wizard_step_2_help_text: Hjälptext för steget ”Publicera” i förslagsguiden resources_permissions_enabled: Behörigheter kan ställas in för varje förslag taxonomy_filters: Välj filter för komponenten taxonomy_filters_add: Lägg till filter - threshold_per_proposal: Minst antal stöd per förslag - vote_limit: Antal röster per deltagare + threshold_per_proposal: Antal röster ett förslag behöver få för att gå vidare + vote_limit: Max antal röster per deltagare step: amendment_creation_enabled: Skapa ändringar är aktiverat amendment_creation_enabled_help: Deltagare kan ändra förslag. @@ -239,8 +239,8 @@ sv: default_sort_order_help: Automatiskt betyder att om röstning är aktiverat kommer förslagen sorteras slumpmässigt, och om röstning är avstängt kommer de att sorteras efter mest valda. default_sort_order_options: automatic: Automatisk - most_commented: Mest kommenterade - most_followed: Mest följda + most_commented: Flest kommentarer + most_followed: Flest följare most_liked: Mest gillade most_voted: Flest röster random: Slumpvis @@ -251,8 +251,8 @@ sv: proposal_answering_enabled: Aktivera svar på förslag publish_answers_immediately: Publicera svar på förslag omedelbart publish_answers_immediately_help_html: 'Tänk på att om du svarar på förslag som inte är aktiverade, kommer du att behöva publicera dem manuellt genom att välja dem och använda åtgärden för publicering. För mer information om hur det här fungerar, se proposals'' answers documentation page.' - votes_blocked: Röster blockerade - votes_enabled: Röster aktiverade + votes_blocked: Stäng av röstning + votes_enabled: Aktivera röstning votes_hidden: Dölj antal röster (om röster är aktiverade, kommer antalet röster att döljas) download_your_data: show: @@ -296,17 +296,17 @@ sv: coauthor_rejected_invite: notification_title: %{coauthor_name} har avböjt din inbjudan att bli medförfattare till förslaget %{resource_title}. collaborative_draft_access_accepted: - email_intro: '%{requester_name} har blivit accepterad som medskapare till %{resource_title} det gemensamma utkastet.' - email_outro: Du har fått det här meddelandet eftersom du är en medskapare av %{resource_title}. + email_intro: '%{requester_name} har blivit godkänd som medförfattare till det gemensamma utkastet %{resource_title}.' + email_outro: Du har fått det här meddelandet eftersom du är författare till %{resource_title}. email_subject: "%{requester_name} har fått tillgång till och är accepterad som medskapare till %{resource_title}." notification_title: %{requester_name} %{requester_nickname} har fått tillgång till och accepterats som medskapare till det gemensamma utkastet %{resource_title}. collaborative_draft_access_rejected: - email_intro: '%{requester_name} har avvisats som medskapare av %{resource_title} samarbetsutkastet.' + email_intro: '%{requester_name} har avvisats som medförfattare till %{resource_title}.' email_outro: Du har fått det här meddelandet eftersom du är en medskapare av %{resource_title}. email_subject: "%{requester_name} har avvisats från att få tillgång till det gemensamma utkastet %{resource_title} som medskapare." notification_title: %{requester_name} %{requester_nickname} har avvisats från att få tillgång till det gemensamma utkastet %{resource_title} som medskapare. collaborative_draft_access_requested: - email_intro: '%{requester_name} begärde åtkomst som medskapare. Du kan acceptera eller avvisa begäran från %{resource_title}, sidan för det gemensamma utkastet.' + email_intro: '%{requester_name} har begärt att bli medförfattare. Du kan acceptera eller avvisa begäran från sidan för det gemensamma utkastet, %{resource_title}.' email_outro: Du har fått det här meddelandet eftersom du är en medskapare av %{resource_title}. email_subject: "%{requester_name} begärde tillgång att bidra till %{resource_title}." notification_title: %{requester_name} %{requester_nickname} begärde tillgång för att få bidra till det gemensamma utkastet %{resource_title}. Godkänn eller avvisa begäran. @@ -446,7 +446,10 @@ sv: title: Åtgärder admin: actions: + confirm_delete_proposal: Är du säker på att du vill ta bort förslaget? + deleted_proposals_info: Borttagna förslag kan återställas från papperskorgen. preview: Förhandsvisa + view_deleted_proposals: Visa borttagna förslag evaluation_assignments: create: invalid: 'Problem: Utvärderaren kunde inte få förslag.' @@ -562,6 +565,7 @@ sv: assign_to_evaluator: Tilldelad en utvärderare assign_to_evaluator_button: Tilldelad cancel: Avbryt + change_taxonomies: Ändra kategorier merge: Slå samman till ett nytt no_templates_available: Ingen mall tillgänglig publish: Publicera @@ -653,15 +657,15 @@ sv: error: Det gick inte att publicera det gemensamma utkastet. irreversible_action_modal: body: Efter att utkastet har publicerats som ett förslag går det inte längre att redigera utkastet. Förslaget kommer inte acceptera nya författare eller tillägg. - cancel: Dra tillbaka + cancel: Avbryt ok: Publicera som ett förslag title: Följande åtgärd kan inte ångras success: Det gemensamma utkastet har publicerats som ett förslag. withdraw: error: Det gick inte att stänga det gemensamma utkastet. irreversible_action_modal: - body: Efter att utkastet har publicerats som ett förslag går det inte längre att redigera utkastet. Förslaget kommer inte acceptera nya författare eller tillägg. - cancel: Dra tillbaka + body: Efter att utkastet dragits tillbaka går det inte längre att redigera. + cancel: Avbryt ok: Dra tillbaka det gemensamma utkastet title: Det går inte att ångra denna åtgärd success: Det gemensamma utkastet har dragits tillbaka. @@ -696,7 +700,7 @@ sv: new: add_file: Lägg till fil edit_file: Redigera fil - send: Fortsätt + send: Spara new_collaborative_draft_button: new_collaborative_draft: Nytt gemensamt utkast orders: @@ -719,6 +723,7 @@ sv: error: Kunde inte avvisas som samarbetspartner, försök igen senare. success: "@%{user} har avvisats som medskapare." show: + edit: Redigera final_proposal: Färdigt förslag final_proposal_help_text: Detta utkast är klart. Du kan se det slutliga förslaget hidden_authors_count: @@ -778,6 +783,7 @@ sv: published_answer: Publicerat svar published_at: Publicerad state: Status + taxonomies: Kategorier title: Titel votes: Röster proposal_state: @@ -798,6 +804,8 @@ sv: description: Koordinaterna uppdateras när du klickar på knappen 'Förhandsgranska'. Adressen ändras dock inte. instructions: Du kan flytta markören på kartan. edit: + add_attachments: Lägg till bilagor + attachment_legend: Lägg till ett dokument eller en bild back: Tillbaka send: Skicka title: Redigera förslag @@ -819,7 +827,7 @@ sv: search: Sök state: Status type: Typ - voted: Röstade + voted: Röstat på index: click_here: Se alla förslag collaborative_drafts_list: Visa gemensamma utkast @@ -837,10 +845,10 @@ sv: title: Skapa ett nytt förslag orders: label: 'Ordna förslag efter:' - most_commented: Mest kommenterade - most_followed: Mest följda + most_commented: Flest kommentarer + most_followed: Flest följare most_liked: Mest gillade - most_voted: Mest röstade + most_voted: Flest röster random: Slumpvis recent: Senaste with_more_authors: Med flera författare @@ -864,6 +872,7 @@ sv: show: answer: Svara changes_at_title: Ändring till %{title} + edit_proposal: Redigera estimated_cost: Beräknad kostnad hidden_likes_count: one: och %{count} person till @@ -882,7 +891,7 @@ sv: update: title: Uppdatera förslag vote_button: - already_voted: Röstade + already_voted: Röstat already_voted_hover: Dra tillbaka röst maximum_votes_reached: Röstningsgränsen uppnådd no_votes_remaining: Inga röster kvar @@ -893,11 +902,15 @@ sv: other: Röster voting_rules: can_accumulate_votes_beyond_threshold: - description: Varje förslag kan få mer än %{limit} röster + description: Förslag kan få fler än gränsen på %{limit} röster + minimum_votes_per_user: + description: Du behöver rösta på %{votes} till förslag för att din röst ska räknas. proposal_limit: description: Du kan skapa upp till %{limit} förslag. + success: Din röst har tagits emot threshold_per_proposal: description: Förslag behöver få minst %{limit} röster för accepteras. + title: Regler för omröstning vote_limit: description: Du kan stödja upp till %{limit} förslag. votes: Saknar %{number} röster diff --git a/decidim-proposals/lib/decidim/api/mutations/answer_proposal_attributes.rb b/decidim-proposals/lib/decidim/api/mutations/answer_proposal_attributes.rb index 8f1dadd8873dc..1722f61643161 100644 --- a/decidim-proposals/lib/decidim/api/mutations/answer_proposal_attributes.rb +++ b/decidim-proposals/lib/decidim/api/mutations/answer_proposal_attributes.rb @@ -3,8 +3,8 @@ module Decidim module Proposals class AnswerProposalAttributes < Decidim::Api::Types::BaseInputObject - graphql_name "ProposalAttributes" - description "Attributes of a proposal" + graphql_name "AnswerProposalAttributes" + description "Attributes for answering a proposal" argument :answer_content, GraphQL::Types::JSON, description: "The answer feedback for the status for this proposal", required: false argument :cost, GraphQL::Types::Float, description: "Estimated cost of the proposal", required: false diff --git a/decidim-proposals/lib/decidim/api/mutations/create_proposal_type.rb b/decidim-proposals/lib/decidim/api/mutations/create_proposal_type.rb new file mode 100644 index 0000000000000..2ac3c571ccf51 --- /dev/null +++ b/decidim-proposals/lib/decidim/api/mutations/create_proposal_type.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Decidim + module Proposals + class CreateProposalType < Decidim::Api::Types::BaseMutation + graphql_name "CreateProposal" + + description "Creates a proposal" + type Decidim::Proposals::ProposalType + + argument :attributes, ProposalAttributes, description: "Input attributes for the proposal", required: true + argument :locale, GraphQL::Types::String, "The locale for which to set the proposal texts", required: true + argument :toggle_translations, GraphQL::Types::Boolean, "Whether the user asked to toggle the machine translations or not.", required: false, default_value: false + + def resolve(attributes:, locale:, toggle_translations:) + set_locale(locale:, toggle_translations:) + + params = attributes.to_h.slice(:title, :body, :address, :latitude, :longitude, :taxonomies) + + params[:taxonomies] = Decidim::Taxonomy.where(organization: current_organization, id: params[:taxonomies]).pluck(:id) if params[:taxonomies] + + form = form(Decidim::Proposals::ProposalForm).from_params(params) + + Decidim::Proposals::CreateProposal.call(form, current_user) do + on(:ok) do |proposal| + Decidim::Proposals::PublishProposal.call(proposal, current_user) do + on(:ok) do + return proposal.reload + end + + on(:invalid) do + raise Decidim::Api::Errors::ValidationError, I18n.t("proposals.publish.error", scope: "decidim") + end + end + end + + on(:invalid) do + raise Decidim::Api::Errors::AttributeValidationError, form.errors + end + end + end + + def authorized?(attributes:, locale:, toggle_translations:) + unless super && allowed_to?(:create, :proposal, Decidim::Proposals::Proposal.new(component: current_component), { current_user:, current_component: }) + raise Decidim::Api::Errors::MutationNotAuthorizedError, I18n.t("decidim.api.errors.unauthorized_mutation") + end + + true + end + end + end +end diff --git a/decidim-proposals/lib/decidim/api/mutations/proposal_answer_type.rb b/decidim-proposals/lib/decidim/api/mutations/proposal_answer_type.rb index c9c8f99388229..e92f8acd37a2b 100644 --- a/decidim-proposals/lib/decidim/api/mutations/proposal_answer_type.rb +++ b/decidim-proposals/lib/decidim/api/mutations/proposal_answer_type.rb @@ -21,32 +21,24 @@ def resolve(attributes:) execution_period: object.execution_period ) - form = Decidim::Proposals::Admin::ProposalAnswerForm.from_params( - params - ).with_context( - current_component: object.component, - current_user:, - current_organization: current_user.organization - ) + form = form(Decidim::Proposals::Admin::ProposalAnswerForm).from_params(params) Admin::AnswerProposal.call(form, object) do on(:ok) do return object end + on(:invalid) do - return GraphQL::ExecutionError.new( - form.errors.full_messages.join(", ") - ) + raise Decidim::Api::Errors::AttributeValidationError, form.errors end - - GraphQL::ExecutionError.new( - I18n.t("decidim.proposals.admin.proposals.answer.invalid") - ) end end def authorized?(attributes:) - super && allowed_to?(:create, :proposal_answer, object, context, scope: :admin) + authorized = super && allowed_to?(:create, :proposal_answer, object, context, scope: :admin) + raise Decidim::Api::Errors::MutationNotAuthorizedError, I18n.t("decidim.api.errors.unauthorized_mutation") unless authorized + + true end def current_user diff --git a/decidim-proposals/lib/decidim/api/mutations/proposal_attributes.rb b/decidim-proposals/lib/decidim/api/mutations/proposal_attributes.rb new file mode 100644 index 0000000000000..21c3a7c38052b --- /dev/null +++ b/decidim-proposals/lib/decidim/api/mutations/proposal_attributes.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Decidim + module Proposals + class ProposalAttributes < Decidim::Api::Types::BaseInputObject + graphql_name "ProposalAttributes" + description "Attributes for creating a proposal" + + argument :address, GraphQL::Types::String, description: "Physical address for the proposal", required: false + argument :body, GraphQL::Types::String, description: "The body content of the proposal", required: true + argument :latitude, GraphQL::Types::Float, description: "Latitude coordinate", required: false + argument :longitude, GraphQL::Types::Float, description: "Longitude coordinate", required: false + argument :taxonomies, [GraphQL::Types::ID], description: "Array of taxonomy IDs", required: false + argument :title, GraphQL::Types::String, description: "The title of the proposal", required: true + end + end +end diff --git a/decidim-proposals/lib/decidim/api/mutations/proposal_mutation_type.rb b/decidim-proposals/lib/decidim/api/mutations/proposal_mutation_type.rb index 741b0f23d8fa8..52d0768348aca 100644 --- a/decidim-proposals/lib/decidim/api/mutations/proposal_mutation_type.rb +++ b/decidim-proposals/lib/decidim/api/mutations/proposal_mutation_type.rb @@ -9,6 +9,10 @@ class ProposalMutationType < Decidim::Api::Types::BaseObject description "a proposal which includes its available mutations" field :answer, mutation: Decidim::Proposals::ProposalAnswerType, description: "Answers a proposal" + field :unvote, mutation: Decidim::Proposals::UnvoteProposalType, description: "Removes a vote from a proposal" + field :update, mutation: Decidim::Proposals::UpdateProposalType, description: "Updates a proposal" + field :vote, mutation: Decidim::Proposals::VoteProposalType, description: "Votes a proposal" + field :withdraw, mutation: Decidim::Proposals::WithdrawProposalType, description: "Withdraws a proposal" end end end diff --git a/decidim-proposals/lib/decidim/api/mutations/proposals_mutation_type.rb b/decidim-proposals/lib/decidim/api/mutations/proposals_mutation_type.rb index de6d55b84c450..ea7a2093d622b 100644 --- a/decidim-proposals/lib/decidim/api/mutations/proposals_mutation_type.rb +++ b/decidim-proposals/lib/decidim/api/mutations/proposals_mutation_type.rb @@ -5,6 +5,7 @@ module Proposals class ProposalsMutationType < Decidim::Core::ComponentType description "A proposals of a component." + field :create_proposal, mutation: Decidim::Proposals::CreateProposalType, description: "Creates a proposal" field :proposal, type: Decidim::Proposals::ProposalMutationType, description: "Mutates a proposal", null: true do argument :id, GraphQL::Types::ID, "The ID of the proposal", required: true end diff --git a/decidim-proposals/lib/decidim/api/mutations/unvote_proposal_type.rb b/decidim-proposals/lib/decidim/api/mutations/unvote_proposal_type.rb new file mode 100644 index 0000000000000..da61492eec6e5 --- /dev/null +++ b/decidim-proposals/lib/decidim/api/mutations/unvote_proposal_type.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Decidim + module Proposals + class UnvoteProposalType < Decidim::Api::Types::BaseMutation + graphql_name "UnvoteProposal" + + description "Removes a vote from a proposal" + type Decidim::Proposals::ProposalType + + def resolve + UnvoteProposal.call(object, current_user) do + on(:ok) do + return object.reload + end + end + end + + def authorized? + raise Decidim::Api::Errors::MutationNotAuthorizedError, I18n.t("decidim.api.errors.unauthorized_mutation") unless super && allowed_to?(:unvote, :proposal, object, context) + + true + end + end + end +end diff --git a/decidim-proposals/lib/decidim/api/mutations/update_proposal_type.rb b/decidim-proposals/lib/decidim/api/mutations/update_proposal_type.rb new file mode 100644 index 0000000000000..130e9419d8e18 --- /dev/null +++ b/decidim-proposals/lib/decidim/api/mutations/update_proposal_type.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module Decidim + module Proposals + class UpdateProposalType < Decidim::Api::Types::BaseMutation + graphql_name "UpdateProposal" + + description "Updates a proposal" + type Decidim::Proposals::ProposalType + + argument :attributes, ProposalAttributes, description: "Input attributes for updating a proposal", required: true + argument :locale, GraphQL::Types::String, "The locale for which to get the proposals texts", required: true + argument :toggle_translations, GraphQL::Types::Boolean, "Whether the user asked to toggle the machine translations or not.", required: true, default_value: false + + def resolve(attributes:, locale:, toggle_translations:) + set_locale(locale:, toggle_translations:) + + params = extract_from(attributes) + + form = form(Decidim::Proposals::ProposalForm).from_params(params) + + UpdateProposal.call(form, current_user, object) do + on(:ok) do |proposal| + return proposal.reload + end + + on(:invalid) do + raise Decidim::Api::Errors::AttributeValidationError, form.errors + end + end + end + + def authorized?(attributes:, locale:, toggle_translations:) + raise Decidim::Api::Errors::MutationNotAuthorizedError, I18n.t("decidim.api.errors.unauthorized_mutation") unless super && allowed_to?(:edit, :proposal, object, context) + + true + end + + private + + def extract_from(attributes) + title = attributes.to_h.fetch(:title, translated_attribute(object.title)) + body = attributes.to_h.fetch(:body, translated_attribute(object.body)) + taxonomies = Decidim::Taxonomy.where(organization: current_organization, id: attributes.to_h.fetch(:taxonomies, object.taxonomies)).pluck(:id) + address = attributes.to_h.fetch(:address, object.address) + latitude = attributes.to_h.fetch(:latitude, object.latitude) + longitude = attributes.to_h.fetch(:longitude, object.longitude) + + { title:, body:, address:, latitude:, longitude:, taxonomies: } + end + end + end +end diff --git a/decidim-proposals/lib/decidim/api/mutations/vote_proposal_type.rb b/decidim-proposals/lib/decidim/api/mutations/vote_proposal_type.rb new file mode 100644 index 0000000000000..84725799d62f0 --- /dev/null +++ b/decidim-proposals/lib/decidim/api/mutations/vote_proposal_type.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Decidim + module Proposals + class VoteProposalType < Decidim::Api::Types::BaseMutation + graphql_name "VoteProposal" + + description "Votes a proposal" + type Decidim::Proposals::ProposalType + + def resolve + VoteProposal.call(object, current_user) do + on(:ok) do + return object.reload + end + + on(:invalid) do + raise Decidim::Api::Errors::ValidationError, I18n.t("proposal_votes.create.error", scope: "decidim.proposals") + end + end + end + + def authorized? + raise Decidim::Api::Errors::MutationNotAuthorizedError, I18n.t("decidim.api.errors.unauthorized_mutation") unless super && allowed_to?(:vote, :proposal, object, context) + + true + end + end + end +end diff --git a/decidim-proposals/lib/decidim/api/mutations/withdraw_proposal_type.rb b/decidim-proposals/lib/decidim/api/mutations/withdraw_proposal_type.rb new file mode 100644 index 0000000000000..5348094f283fd --- /dev/null +++ b/decidim-proposals/lib/decidim/api/mutations/withdraw_proposal_type.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Decidim + module Proposals + class WithdrawProposalType < Decidim::Api::Types::BaseMutation + graphql_name "WithdrawProposal" + + description "Withdraws a proposal" + type Decidim::Proposals::ProposalType + + def resolve + WithdrawProposal.call(object, current_user) do + on(:ok) do |proposal| + return proposal + end + + on(:has_votes) do + raise Decidim::Api::Errors::ValidationError, I18n.t("proposals.withdraw.errors.has_votes", scope: "decidim") + end + end + end + + def authorized? + raise Decidim::Api::Errors::MutationNotAuthorizedError, I18n.t("decidim.api.errors.unauthorized_mutation") unless super && allowed_to?(:withdraw, :proposal, object, + context) + + true + end + end + end +end diff --git a/decidim-proposals/lib/decidim/api/proposal_type.rb b/decidim-proposals/lib/decidim/api/proposal_type.rb index ce8af12ecc1ac..209d4f382c8da 100644 --- a/decidim-proposals/lib/decidim/api/proposal_type.rb +++ b/decidim-proposals/lib/decidim/api/proposal_type.rb @@ -97,8 +97,6 @@ def self.authorized?(object, context) ].all? super && chain - rescue Decidim::PermissionAction::PermissionNotSetError - false end private diff --git a/decidim-proposals/lib/decidim/proposals/api.rb b/decidim-proposals/lib/decidim/proposals/api.rb index 6b889c6346851..60f2c04f9171d 100644 --- a/decidim-proposals/lib/decidim/proposals/api.rb +++ b/decidim-proposals/lib/decidim/proposals/api.rb @@ -11,5 +11,11 @@ module Proposals autoload :ProposalMutationType, "decidim/api/mutations/proposal_mutation_type" autoload :ProposalAnswerType, "decidim/api/mutations/proposal_answer_type" autoload :AnswerProposalAttributes, "decidim/api/mutations/answer_proposal_attributes" + autoload :CreateProposalType, "decidim/api/mutations/create_proposal_type" + autoload :ProposalAttributes, "decidim/api/mutations/proposal_attributes" + autoload :VoteProposalType, "decidim/api/mutations/vote_proposal_type" + autoload :UnvoteProposalType, "decidim/api/mutations/unvote_proposal_type" + autoload :WithdrawProposalType, "decidim/api/mutations/withdraw_proposal_type" + autoload :UpdateProposalType, "decidim/api/mutations/update_proposal_type" end end diff --git a/decidim-proposals/lib/decidim/proposals/component.rb b/decidim-proposals/lib/decidim/proposals/component.rb index c9c91403ce78f..fc3ff3b0033c5 100644 --- a/decidim-proposals/lib/decidim/proposals/component.rb +++ b/decidim-proposals/lib/decidim/proposals/component.rb @@ -7,10 +7,6 @@ component.icon = "media/images/decidim_proposals.svg" component.icon_key = "chat-new-line" - component.on(:before_destroy) do |instance| - raise "Cannot destroy this component when there are proposals" if Decidim::Proposals::Proposal.where(component: instance).any? - end - component.on(:create) do |instance| admin_user = GlobalID::Locator.locate(instance.versions.first.whodunnit) Decidim::Proposals.create_default_states!(instance, admin_user) diff --git a/decidim-proposals/lib/decidim/proposals/seeds.rb b/decidim-proposals/lib/decidim/proposals/seeds.rb index 7254c3892d281..bb176f950587b 100644 --- a/decidim-proposals/lib/decidim/proposals/seeds.rb +++ b/decidim-proposals/lib/decidim/proposals/seeds.rb @@ -17,7 +17,7 @@ def call Decidim::Proposals.create_default_states!(component, admin_user) - number_of_records = slow_seeds? ? 10 : rand(25..50) + number_of_records = slow_seeds? ? rand(25..50) : rand(5..10) (5..number_of_records).to_a.sample.times do |n| proposal = create_proposal!(component:) diff --git a/decidim-proposals/spec/controllers/decidim/proposals/admin/proposal_answers_controller_spec.rb b/decidim-proposals/spec/controllers/decidim/proposals/admin/proposal_answers_controller_spec.rb index 690ff0259d2df..b62dcca51d10f 100644 --- a/decidim-proposals/spec/controllers/decidim/proposals/admin/proposal_answers_controller_spec.rb +++ b/decidim-proposals/spec/controllers/decidim/proposals/admin/proposal_answers_controller_spec.rb @@ -64,7 +64,7 @@ module Admin context "when the update is successful." do it "renders ProposalsAdmin#index view" do - post :update, params: params + post(:update, params:) expect(response).to have_http_status(:found) expect(subject).to redirect_to(proposals_path) end diff --git a/decidim-proposals/spec/forms/decidim/proposals/admin/proposals_import_form_spec.rb b/decidim-proposals/spec/forms/decidim/proposals/admin/proposals_import_form_spec.rb index 5e510c51b22da..d26f8dd19bfdb 100644 --- a/decidim-proposals/spec/forms/decidim/proposals/admin/proposals_import_form_spec.rb +++ b/decidim-proposals/spec/forms/decidim/proposals/admin/proposals_import_form_spec.rb @@ -12,13 +12,11 @@ module Admin let(:component) { proposal.component } let(:origin_component) { create(:proposal_component, participatory_space: component.participatory_space) } let(:states) { %w(accepted) } - let(:import_proposals) { true } let(:params) do { states:, keep_authors: false, - origin_component_id: origin_component.try(:id), - import_proposals: + origin_component_id: origin_component.try(:id) } end @@ -51,10 +49,10 @@ module Admin it { is_expected.to be_invalid } end - context "when the import proposals is not accepted" do - let(:import_proposals) { false } + context "when importing from multiple states" do + let(:states) { %w(accepted rejected) } - it { is_expected.to be_invalid } + it { is_expected.to be_valid } end describe "states" do diff --git a/decidim-proposals/spec/lib/tasks/upgrade/fix_state_spec.rb b/decidim-proposals/spec/lib/tasks/upgrade/fix_state_spec.rb index 5f1e7c13af21c..0c763ee380797 100644 --- a/decidim-proposals/spec/lib/tasks/upgrade/fix_state_spec.rb +++ b/decidim-proposals/spec/lib/tasks/upgrade/fix_state_spec.rb @@ -11,7 +11,7 @@ before do proposal_state = proposal.proposal_state = Decidim::Proposals::ProposalState.where(component: component2).first - proposal.update!(proposal_state: proposal_state) + proposal.update!(proposal_state:) end it "does not throw an exception" do @@ -24,7 +24,7 @@ before do proposal_state = proposal.proposal_state = Decidim::Proposals::ProposalState.where(component: component2).first - proposal.update!(proposal_state: proposal_state) + proposal.update!(proposal_state:) end it "sets the state of the correct component" do diff --git a/decidim-proposals/spec/permissions/decidim/proposals/permissions_spec.rb b/decidim-proposals/spec/permissions/decidim/proposals/permissions_spec.rb index 6149b42923f24..550a0f2b44213 100644 --- a/decidim-proposals/spec/permissions/decidim/proposals/permissions_spec.rb +++ b/decidim-proposals/spec/permissions/decidim/proposals/permissions_spec.rb @@ -110,6 +110,12 @@ it { is_expected.to be false } end + + context "when proposal is already withdrawn" do + let(:proposal) { create(:proposal, :withdrawn, component: proposal_component) } + + it { is_expected.to be false } + end end describe "voting" do diff --git a/decidim-proposals/spec/shared/import_proposals_examples.rb b/decidim-proposals/spec/shared/import_proposals_examples.rb index 12588014bcc65..3ecf56bbb2ddc 100644 --- a/decidim-proposals/spec/shared/import_proposals_examples.rb +++ b/decidim-proposals/spec/shared/import_proposals_examples.rb @@ -75,7 +75,6 @@ def fill_form(keep_authors: false) select origin_component.name["en"], from: "Origin component" check "Accepted" check "Keep original authors" if keep_authors - check "Import proposals" end click_on "Import proposals" diff --git a/decidim-proposals/spec/shared/proposal_mutation_examples.rb b/decidim-proposals/spec/shared/proposal_mutation_examples.rb deleted file mode 100644 index dcfb02f94d840..0000000000000 --- a/decidim-proposals/spec/shared/proposal_mutation_examples.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -shared_examples "manage proposal mutation examples" do - context "when proposal answering disabled" do - it "does not answer the proposal" do - expect(response["answer"]).to be_nil - end - end - - context "when proposal answering enabled" do - let!(:proposal_answering_enabled) { true } - - it "answers the proposal but not costs" do - answer = response["answer"] - expect(answer).to be_present - expect(answer).to include( - { - "id" => model.id.to_s, - "state" => state, - "answer" => { - "translation" => answer_content[:en] - }, - "cost" => nil, - "costReport" => nil, - "executionPeriod" => nil, - "answeredAt" => model.reload.answered_at.to_time.iso8601 - } - ) - end - - context "with enabled answering with cost" do - let!(:proposal_answers_with_costs?) { true } - - it "answers the proposal and adds the cost" do - answer = response["answer"] - - expect(answer).to be_present - expect(answer).to include( - { - "id" => model.id.to_s, - "state" => state, - "answer" => { - "translation" => answer_content[:en] - }, - "cost" => "€ 1,234.00", - "costReport" => { - "translation" => cost_report[:en] - }, - "executionPeriod" => { - "translation" => execution_period[:en] - }, - "answeredAt" => model.reload.answered_at.to_time.iso8601 - } - ) - end - end - end -end diff --git a/decidim-proposals/spec/system/admin/admin_edits_proposal_spec.rb b/decidim-proposals/spec/system/admin/admin_edits_proposal_spec.rb index 5418552ded89a..0560038a3c702 100644 --- a/decidim-proposals/spec/system/admin/admin_edits_proposal_spec.rb +++ b/decidim-proposals/spec/system/admin/admin_edits_proposal_spec.rb @@ -112,12 +112,19 @@ let!(:document) { create(:attachment, :with_pdf, attached_to: proposal) } - it "can be remove attachment" do + it "can remove attachment" do visit_component_admin within "tr", text: translated_attribute(proposal.title) do find("button[data-controller='dropdown']").click click_on "Edit proposal" end + + click_on("Edit attachments") + within "li[data-filename='#{document.file.blob.filename}']" do + click_on("Remove") + end + click_on("Save") + within ".item__edit-form" do click_on "Update" end @@ -129,7 +136,7 @@ find("button[data-controller='dropdown']").click click_on "Edit proposal" end - expect(page).to have_no_content("Current file") + expect(page).to have_no_content(document.file.blob.filename) end it "can attach a file" do diff --git a/decidim-proposals/spec/system/amendable/amend_proposal_spec.rb b/decidim-proposals/spec/system/amendable/amend_proposal_spec.rb index 5945cf5644ecb..5cc5c2ffdbf8e 100644 --- a/decidim-proposals/spec/system/amendable/amend_proposal_spec.rb +++ b/decidim-proposals/spec/system/amendable/amend_proposal_spec.rb @@ -167,7 +167,7 @@ expect(page).to have_no_css("#amend-button") end - context "when a private user is logged in" do + context "when a member is logged in" do let!(:user) { create(:user, :confirmed, organization: component.organization) } before do @@ -210,6 +210,7 @@ expect(page).to have_content("Log in") switch_to_host(component.organization.host) login_as user, scope: :user + sleep 1 visit proposal_path expect(page).to have_content(proposal_title) find("#dropdown-trigger-resource-#{proposal.id}").click diff --git a/decidim-proposals/spec/system/private_space_proposal_spec.rb b/decidim-proposals/spec/system/private_space_proposal_spec.rb index fd46e50dcf45e..b6ba864615c65 100644 --- a/decidim-proposals/spec/system/private_space_proposal_spec.rb +++ b/decidim-proposals/spec/system/private_space_proposal_spec.rb @@ -7,7 +7,7 @@ let(:user) { create(:user, :confirmed, organization:) } let!(:other_user) { create(:user, :confirmed, organization:) } - let!(:participatory_space_private_user) { create(:participatory_space_private_user, user: other_user, privatable_to: participatory_space_private) } + let!(:member) { create(:member, user: other_user, participatory_space: participatory_space_private) } let!(:participatory_space) { participatory_space_private } @@ -33,10 +33,32 @@ def visit_component expect(page).to have_no_link("New proposal") end end + + context "when the component has votes enabled and the proposal has votes" do + let!(:proposal) { create(:proposal, :official, :with_votes, component:) } + + before do + component.default_step_settings = component.default_step_settings.to_h.merge({ votes_enabled: true }) + component.save! + end + + context "when accessing the proposal page" do + let(:target_path) { Decidim::ResourceLocatorPresenter.new(proposal).path } + + before do + visit target_path + end + + it "can access the page but cannot see the votes" do + expect(page).to have_content(proposal.title["en"]) + expect(page).to have_no_content("Votes") + end + end + end end context "when the user is logged in" do - context "and is private user space" do + context "and is member space" do before do login_as other_user, scope: :user end @@ -50,7 +72,7 @@ def visit_component end end - context "and is not private user space" do + context "and is not member space" do before do login_as user, scope: :user end @@ -82,7 +104,7 @@ def visit_component end context "when the user is logged in" do - context "and is private user space" do + context "and is member space" do before do login_as other_user, scope: :user end @@ -103,7 +125,7 @@ def visit_component end end - context "and is not private user space" do + context "and is not member space" do let(:target_path) { main_component_path(component) } before do diff --git a/decidim-proposals/spec/system/proposal_show_spec.rb b/decidim-proposals/spec/system/proposal_show_spec.rb index 60e99f5cf1200..8a56b37e5419d 100644 --- a/decidim-proposals/spec/system/proposal_show_spec.rb +++ b/decidim-proposals/spec/system/proposal_show_spec.rb @@ -27,6 +27,7 @@ def visit_proposal describe "extra admin link" do before do login_as user, scope: :user + sleep 1 visit current_path end @@ -65,9 +66,6 @@ def visit_proposal end it "successfully shows the page" do - within(".menu-bar") do - expect(page).to have_content(translated(component.name)) - end expect(page).to have_content("Deleted participant") end end diff --git a/decidim-proposals/spec/system/proposals_breadcrumbs_spec.rb b/decidim-proposals/spec/system/proposals_breadcrumbs_spec.rb new file mode 100644 index 0000000000000..4a49f10d05730 --- /dev/null +++ b/decidim-proposals/spec/system/proposals_breadcrumbs_spec.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Proposals Breadcrumb" do + include_context "with a component" + + let(:organization) { create(:organization) } + let(:participatory_space) { create(:participatory_process, :with_steps, :published, organization:, title: { "en" => "Participatory space" }) } + let(:component) { create(:proposal_component, :published, :with_amendments_enabled, participatory_space:, name: { "en" => "Component" }) } + let(:proposal) { create(:proposal, component:, title: { "en" => "Proposal" }) } + let(:router) { Decidim::EngineRouter.main_proxy(component) } + + before do + switch_to_host(organization.host) + end + + describe "index" do + it "shows the correct information in breadcrumb (space, component)" do + visit router.root_path(locale: I18n.locale) + + within ".menu-bar" do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + end + end + end + + describe "show" do + it "shows the correct information in breadcrumb (space, component, proposal)" do + visit router.proposal_path(proposal, locale: I18n.locale) + + within ".menu-bar" do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + expect(page).to have_content(translated(proposal.title)) + end + end + + context "when it is an official proposal" do + let(:content) { generate_localized_title } + let!(:official_proposal) { create(:proposal, :official, body: content, component:) } + let!(:official_proposal_title) { translated(official_proposal.title) } + + before do + visit_component + click_on official_proposal_title + end + + it "shows the correct information in breadcrumb (space, component, proposal)" do + within(".menu-bar") do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + expect(page).to have_content(translated(official_proposal.title)) + end + end + end + end + + describe "versions", versioning: true do + let!(:amendment) { create(:amendment, amendable: proposal, emendation:) } + let!(:emendation) { create(:proposal, body: { en: "Amended One liner body" }, component:) } + + let(:form) do + Decidim::Amendable::ReviewForm.from_params( + id: amendment.id, + amendable_gid: proposal.to_sgid.to_s, + emendation_gid: emendation.to_sgid.to_s, + emendation_params: { title: emendation.title, body: emendation.body } + ) + end + let(:command) { Decidim::Amendable::Accept.new(form) } + + before do + visit router.proposal_path(proposal, locale: I18n.locale) + command.call + click_on "see other versions" + click_on("Version 2 of 2") + end + + it "shows the correct information in breadcrumb (space, component, proposal)" do + within(".menu-bar") do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + expect(page).to have_content(translated(proposal.reload.title)) + end + end + end + + context "when visiting single amendment page", versioning: true do + let!(:emendation) { create(:proposal, title: { en: "Amended Long enough title" }, component:) } + let!(:amendment) { create(:amendment, amendable: proposal, emendation:) } + let(:form) do + Decidim::Amendable::ReviewForm.from_params( + id: amendment.id, + amendable_gid: proposal.to_sgid.to_s, + emendation_gid: emendation.to_sgid.to_s, + emendation_params: { title: emendation.title, body: emendation.body } + ) + end + let(:command) { Decidim::Amendable::Accept.new(form) } + + before do + component.update!(settings: { amendments_enabled: true }) + command.call + end + + it "shows the correct information in breadcrumb (space, component, amendment)" do + visit router.proposal_path(emendation, locale: I18n.locale) + + within ".menu-bar" do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + expect(page).to have_content(translated(emendation.title)) + expect(page).to have_content("Amendment") + end + end + end +end diff --git a/decidim-proposals/spec/system/proposals_spec.rb b/decidim-proposals/spec/system/proposals_spec.rb index a3b78bd55fb55..0c483739de399 100644 --- a/decidim-proposals/spec/system/proposals_spec.rb +++ b/decidim-proposals/spec/system/proposals_spec.rb @@ -89,11 +89,6 @@ end it "shows the author as official" do - within(".menu-bar") do - expect(page).to have_content(translated(component.name)) - expect(page).to have_content(translated(official_proposal.title)) - end - expect(page).to have_content("Official proposal") end diff --git a/decidim-proposals/spec/system/proposals_versions_spec.rb b/decidim-proposals/spec/system/proposals_versions_spec.rb index 34896c771ac05..1d428ad0c8258 100644 --- a/decidim-proposals/spec/system/proposals_versions_spec.rb +++ b/decidim-proposals/spec/system/proposals_versions_spec.rb @@ -130,11 +130,6 @@ visit current_path click_on("Version 3 of 3") - within(".menu-bar") do - expect(page).to have_content(translated(component.name)) - expect(page).to have_content(translated(proposal.reload.title)) - end - within "#diff-for-state" do expect(page).to have_content("State") within ".diff > ul > .ins" do diff --git a/decidim-proposals/spec/types/create_proposal_type_spec.rb b/decidim-proposals/spec/types/create_proposal_type_spec.rb new file mode 100644 index 0000000000000..e08243d65eac2 --- /dev/null +++ b/decidim-proposals/spec/types/create_proposal_type_spec.rb @@ -0,0 +1,235 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Proposals + describe CreateProposalType, type: :graphql do + include_context "with a graphql class mutation" + + let(:type_class) { Decidim::Proposals::CreateProposalType } + let(:root_klass) { Decidim::Proposals::ProposalsMutationType } + + let(:current_organization) { create(:organization, available_locales: [:en]) } + let(:organization) { current_organization } + let(:participatory_process) { create(:participatory_process, :published, :with_steps, organization:) } + let!(:component) { create(:proposal_component, :published, :with_creation_enabled, participatory_space: participatory_process) } + + let(:root_taxonomy) { create(:taxonomy, organization:) } + let!(:taxonomy) { create(:taxonomy, parent: root_taxonomy, organization:) } + let(:taxonomy_filter) { create(:taxonomy_filter, root_taxonomy:) } + let!(:taxonomy_filter_item) { create(:taxonomy_filter_item, taxonomy_filter:, taxonomy_item: taxonomy) } + let!(:user) { create(:user, :confirmed, organization:) } + + let(:address) { "Carrer de la Pau, 1, Barcelona" } + let(:latitude) { 40.1234 } + let(:longitude) { 2.1234 } + + let(:title) { "More sidewalks and less roads" } + let(:body) { "Cities need more people, not more cars" } + let(:locale) { "en" } + let(:translation_locale) { "en" } + + let(:attributes) do + { + title:, + body:, + address:, + latitude:, + longitude:, + taxonomies: [taxonomy_filter.id] + } + end + + let(:variables) do + { + component_id: component.id, + input: { + locale:, + attributes: + } + } + end + + let(:root_value) { component } + let(:query) do + <<~GRAPHQL + mutation createProposal($input: CreateProposalInput!){ + createProposal(input: $input) { + id + title { translation(locale: "#{translation_locale}") } + body { translation(locale: "#{translation_locale}") } + address + publishedAt + author { name } + } + } + GRAPHQL + end + + before do + stub_geocoding(address, [latitude, longitude]) + end + + context "when creating a new proposal" do + context "when the user is not logged in" do + let(:current_user) { nil } + + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + + context "when the user is logged in" do + context "with creation enabled" do + let!(:component) do + create(:proposal_component, + :published, + :with_creation_enabled, + participatory_space: participatory_process, + settings: { + taxonomy_filters: [taxonomy_filter.id] + }) + end + + it "creates a new proposal" do + proposal_response = response["createProposal"] + + expect(proposal_response).to be_present + expect(proposal_response["title"]["translation"]).to eq(title) + expect(proposal_response["body"]["translation"]).to include(body) + expect(proposal_response["publishedAt"]).to be_present + expect(proposal_response["author"]["name"]).to eq(current_user.name) + end + + context "when submitting in one language and requesting in another" do + let(:locale) { "en" } + let(:translation_locale) { "es" } + + it "creates a new proposal" do + proposal_response = response["createProposal"] + + expect(proposal_response).to be_present + expect(proposal_response["title"]["translation"]).to be_nil + end + end + + context "when geocoding is enabled" do + let!(:component) do + create(:proposal_component, + :with_creation_enabled, + :published, + participatory_space: participatory_process, + settings: { + geocoding_enabled: true, + taxonomy_filters: [taxonomy_filter.id] + }) + end + + it "creates a new proposal" do + proposal_response = response["createProposal"] + + expect(proposal_response).to be_present + expect(proposal_response["title"]["translation"]).to eq(title) + expect(proposal_response["body"]["translation"]).to include(body) + expect(proposal_response["address"]).to eq(address) + expect(proposal_response["publishedAt"]).to be_present + expect(proposal_response["author"]["name"]).to eq(current_user.name) + end + end + + context "when the user is not authorized" do + context "and there is only an authorization required" do + before do + permissions = { + create: { + authorization_handlers: { + "dummy_authorization_handler" => { "options" => {} } + } + } + } + + component.update!(permissions:) + end + + it "throws an error if the user does not have a verification method" do + skip("This test is failing, but it is not in the scope of this PR.") + proposal_response = response["createProposal"] + + expect(proposal_response).to be_nil + end + end + + context "and there are more than one authorization required" do + before do + permissions = { + create: { + authorization_handlers: { + "dummy_authorization_handler" => { "options" => {} }, + "another_dummy_authorization_handler" => { "options" => {} } + } + } + } + + component.update!(permissions:) + end + + it "throws an error if the user does not have a verification method" do + skip("This test is failing, but it is not in the scope of this PR.") + + proposal_response = response["createProposal"] + + expect(proposal_response).to be_nil + end + end + end + end + end + + context "when validating" do + context "with having invalid locale" do + let(:locale) { "tlh" } + + it "raises an error" do + expect { response }.to raise_error(Api::Errors::InvalidLocaleError, /Invalid locale provided/) + end + end + + context "with having invalid title" do + context "when is missing" do + let(:title) { "" } + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::AttributeValidationError, /too short/) + end + end + + context "when is too short" do + let(:title) { "Short" } + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::AttributeValidationError, /too short/) + end + end + end + + context "with having invalid body" do + let(:body) { "Short" } + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::AttributeValidationError, /too short/) + end + end + end + + context "when the creating is disabled" do + let!(:component) { create(:proposal_component, participatory_space: participatory_process) } + + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + end + end + end +end diff --git a/decidim-proposals/spec/types/mutations/proposal_answer_type_spec.rb b/decidim-proposals/spec/types/mutations/proposal_answer_type_spec.rb new file mode 100644 index 0000000000000..b5569227f40a8 --- /dev/null +++ b/decidim-proposals/spec/types/mutations/proposal_answer_type_spec.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Proposals + describe ProposalAnswerType, type: :graphql do + include_context "with a graphql class mutation" + + let(:root_klass) { ProposalMutationType } + let(:organization) { create(:organization, available_locales: [:en]) } + let(:participatory_process) { create(:participatory_process, :with_steps, organization:) } + let(:proposal_component) { create(:proposal_component, participatory_space: participatory_process) } + let!(:model) { create(:proposal, component: proposal_component) } + let(:state) { %w(accepted evaluating rejected).sample } + let(:answer_content) { Decidim::Faker::Localized.sentence(word_count: 3) } + let(:proposal_answering_enabled) { false } + let(:proposal_answers_with_costs?) { false } + let(:cost_report) { Decidim::Faker::Localized.sentence(word_count: 3) } + let(:component) { model.component } + let(:execution_period) { Decidim::Faker::Localized.sentence(word_count: 3) } + let(:cost) { 123_4 } + let(:variables) do + { + input: { + attributes: { + state:, + answerContent: answer_content, + cost:, + costReport: cost_report, + executionPeriod: execution_period + } + } + } + end + let(:query) do + <<~GRAPHQL + mutation($input: AnswerInput!) { + answer(input: $input) { + id + answer { translation(locale: "en") } + state + cost + costReport { translation(locale: "en") } + executionPeriod { translation(locale: "en") } + answeredAt + } + } + GRAPHQL + end + + before do + component.update!( + settings: { proposal_answering_enabled: }, + step_settings: { + component.participatory_space.active_step.id => { + proposal_answering_enabled:, + answers_with_costs: proposal_answers_with_costs? + } + } + ) + end + + shared_examples "manage proposal answer mutation examples" do + context "when proposal answering disabled" do + it "throws Decidim::Api::Errors::MutationNotAuthorizedError" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + + context "when proposal answering enabled" do + let!(:proposal_answering_enabled) { true } + + it "answers the proposal but not costs" do + answer = response["answer"] + expect(answer).to be_present + expect(answer).to include( + { + "id" => model.id.to_s, + "state" => state, + "answer" => { + "translation" => answer_content[:en] + }, + "cost" => nil, + "costReport" => nil, + "executionPeriod" => nil, + "answeredAt" => model.reload.answered_at.to_time.iso8601 + } + ) + end + + context "with enabled answering with cost" do + let!(:proposal_answers_with_costs?) { true } + + it "answers the proposal and adds the cost" do + answer = response["answer"] + + expect(answer).to be_present + expect(answer).to include( + { + "id" => model.id.to_s, + "state" => state, + "answer" => { + "translation" => answer_content[:en] + }, + "cost" => "€ 1,234.00", + "costReport" => { + "translation" => cost_report[:en] + }, + "executionPeriod" => { + "translation" => execution_period[:en] + }, + "answeredAt" => model.reload.answered_at.to_time.iso8601 + } + ) + end + end + end + end + + context "with admin user" do + it_behaves_like "manage proposal answer mutation examples" do + let!(:user_type) { :admin } + end + end + + context "with normal user" do + it "throws Decidim::Api::Errors::MutationNotAuthorizedError" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + + context "with api_user" do + it_behaves_like "manage proposal answer mutation examples" do + let!(:user_type) { :api_user } + end + end + end + end +end diff --git a/decidim-proposals/spec/types/proposal_mutation_type_spec.rb b/decidim-proposals/spec/types/proposal_mutation_type_spec.rb deleted file mode 100644 index 31f11a0653a70..0000000000000 --- a/decidim-proposals/spec/types/proposal_mutation_type_spec.rb +++ /dev/null @@ -1,84 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" -require "decidim/api/test/mutation_context" - -module Decidim - module Proposals - describe ProposalAnswerType, type: :graphql do - include_context "with a graphql class mutation" - - let(:root_klass) { ProposalMutationType } - let(:organization) { create(:organization, available_locales: [:en]) } - let(:participatory_process) { create(:participatory_process, :with_steps, organization:) } - let(:proposal_component) { create(:proposal_component, participatory_space: participatory_process) } - let!(:model) { create(:proposal, component: proposal_component) } - let(:state) { %w(accepted evaluating rejected).sample } - let(:answer_content) { Decidim::Faker::Localized.sentence(word_count: 3) } - let(:proposal_answering_enabled) { false } - let(:proposal_answers_with_costs?) { false } - let(:cost_report) { Decidim::Faker::Localized.sentence(word_count: 3) } - let(:component) { model.component } - let(:execution_period) { Decidim::Faker::Localized.sentence(word_count: 3) } - let(:cost) { 123_4 } - let(:variables) do - { - input: { - attributes: { - state: state, - answerContent: answer_content, - cost: cost, - costReport: cost_report, - executionPeriod: execution_period - } - } - } - end - let(:query) do - <<~GRAPHQL - mutation($input: AnswerInput!) { - answer(input: $input) { - id - answer { translation(locale: "en") } - state - cost - costReport { translation(locale: "en") } - executionPeriod { translation(locale: "en") } - answeredAt - } - } - GRAPHQL - end - - before do - component.update!( - settings: { proposal_answering_enabled: proposal_answering_enabled }, - step_settings: { - component.participatory_space.active_step.id => { - proposal_answering_enabled: proposal_answering_enabled, - answers_with_costs: proposal_answers_with_costs? - } - } - ) - end - - context "with admin user" do - it_behaves_like "manage proposal mutation examples" do - let!(:user_type) { :admin } - end - end - - context "with normal user" do - it "returns nil" do - expect(response["answer"]).to be_nil - end - end - - context "with api_user" do - it_behaves_like "manage proposal mutation examples" do - let!(:user_type) { :api_user } - end - end - end - end -end diff --git a/decidim-proposals/spec/types/proposal_type_spec.rb b/decidim-proposals/spec/types/proposal_type_spec.rb index 68115fc8f63f9..7b4447837d1c3 100644 --- a/decidim-proposals/spec/types/proposal_type_spec.rb +++ b/decidim-proposals/spec/types/proposal_type_spec.rb @@ -26,6 +26,12 @@ module Proposals include_examples "localizable interface" include_examples "followable interface" + shared_examples "unauthorized Proposal" do + it "throws Decidim::Api::Errors::UnauthorizedObjectError" do + expect { response }.to raise_error(Decidim::Api::Errors::UnauthorizedObjectError, "You cannot view or edit this Proposal because you do not have permissions") + end + end + describe "id" do let(:query) { "{ id }" } @@ -275,9 +281,7 @@ module Proposals let(:model) { create(:proposal, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Proposal" end context "when participatory space is private but transparent" do @@ -297,9 +301,7 @@ module Proposals let(:model) { create(:proposal, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Proposal" end context "when component is not published" do @@ -307,9 +309,7 @@ module Proposals let(:model) { create(:proposal, component: current_component) } let(:query) { "{ id }" } - it "returns nothing" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Proposal" end context "when proposal is moderated" do @@ -317,9 +317,7 @@ module Proposals let(:query) { "{ id }" } let(:root_value) { model.reload } - it "returns all the required fields" do - expect(response).to be_nil - end + it_behaves_like "unauthorized Proposal" end end end diff --git a/decidim-proposals/spec/types/proposals_type_spec.rb b/decidim-proposals/spec/types/proposals_type_spec.rb index aab72b587bf09..6184f3fdb5e29 100644 --- a/decidim-proposals/spec/types/proposals_type_spec.rb +++ b/decidim-proposals/spec/types/proposals_type_spec.rb @@ -58,16 +58,16 @@ module Proposals context "when the proposal does not belong to the component" do let!(:proposal) { create(:proposal, component: create(:proposal_component)) } - it "returns null" do - expect(response["proposal"]).to be_nil + it "raises error" do + expect { response }.to raise_error(Decidim::Api::Errors::NotFoundError, "Proposal not found") end end context "when the proposal is not published" do let!(:proposal) { create(:proposal, :draft, component: model) } - it "returns null" do - expect(response["proposal"]).to be_nil + it "raises error" do + expect { response }.to raise_error(Decidim::Api::Errors::NotFoundError, "Proposal not found") end end end diff --git a/decidim-proposals/spec/types/unvote_proposal_type_spec.rb b/decidim-proposals/spec/types/unvote_proposal_type_spec.rb new file mode 100644 index 0000000000000..651d8f7e26509 --- /dev/null +++ b/decidim-proposals/spec/types/unvote_proposal_type_spec.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Proposals + describe UnvoteProposalType, type: :graphql do + include_context "with a graphql class mutation" + + let(:root_klass) { ProposalMutationType } + let(:current_organization) { create(:organization, available_locales: [:en]) } + let(:participatory_process) { create(:participatory_process, :with_steps, organization: current_organization) } + let(:proposal_component) do + create(:proposal_component, + :with_votes_enabled, + participatory_space: participatory_process) + end + let!(:model) { create(:proposal, component: proposal_component) } + let(:component) { model.component } + let(:query) do + <<~GRAPHQL + mutation { + unvote(input: {}) { + id + voteCount + } + } + GRAPHQL + end + let(:variables) do + { + input: { + attributes: {} + } + } + end + + context "with a normal user" do + let(:user_type) { :user } + + context "when the user has voted" do + before do + create(:proposal_vote, proposal: model, author: current_user) + end + + it "removes the vote from the proposal" do + expect do + expect(response["unvote"]).not_to be_nil + end.to change(ProposalVote, :count).by(-1) + end + + it "returns the proposal with updated vote count" do + unvote = response["unvote"] + expect(unvote).to be_present + expect(unvote["id"]).to eq(model.id.to_s) + expect(unvote["voteCount"]).to eq(0) + end + end + + context "when the user has not voted" do + it "does not change vote count" do + expect do + response + end.not_to change(ProposalVote, :count) + end + + it "returns the proposal" do + unvote = response["unvote"] + expect(unvote).to be_present + expect(unvote["id"]).to eq(model.id.to_s) + expect(unvote["voteCount"]).to eq(0) + end + end + + context "when votes are disabled" do + let(:proposal_component) do + create(:proposal_component, + :with_votes_disabled, + participatory_space: participatory_process) + end + + before do + create(:proposal_vote, proposal: model, author: current_user) + end + + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError exception" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + end + + context "with an unauthenticated user" do + let(:current_user) { nil } + + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError exception" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + end + end +end diff --git a/decidim-proposals/spec/types/update_proposal_type_spec.rb b/decidim-proposals/spec/types/update_proposal_type_spec.rb new file mode 100644 index 0000000000000..fed4ec627cc82 --- /dev/null +++ b/decidim-proposals/spec/types/update_proposal_type_spec.rb @@ -0,0 +1,220 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Proposals + describe UpdateProposalType, type: :graphql do + include_context "with a graphql class mutation" + + let(:root_taxonomy) { create(:taxonomy, organization:) } + let!(:taxonomy) { create(:taxonomy, parent: root_taxonomy, organization:) } + let(:taxonomy_filter) { create(:taxonomy_filter, root_taxonomy:) } + let!(:taxonomy_filter_item) { create(:taxonomy_filter_item, taxonomy_filter:, taxonomy_item: taxonomy) } + let!(:taxonomies) { [taxonomy.id] } + + let(:type_class) { Decidim::Proposals::UpdateProposalType } + let(:root_klass) { ProposalMutationType } + let(:organization) { create(:organization, available_locales: [:en, :ca, :es]) } + let(:current_organization) { organization } + let(:participatory_process) { create(:participatory_process, :with_steps, organization:) } + let(:proposal_component) { create(:proposal_component, participatory_space: participatory_process, settings: { taxonomy_filters: [taxonomy_filter.id] }) } + let(:current_component) { proposal_component } + let(:author) { create(:user, organization:) } + let!(:model) { create(:proposal, component: proposal_component, users: [author]) } + let(:root_value) { model } + let(:new_title) { "Updated proposal title for testing" } + let(:new_body) { "This is an updated body content for the proposal that meets the minimum length requirements." } + let(:component) { model.component } + let(:locale) { "en" } + let(:variables) do + { + input: { + locale:, + attributes: { + title: new_title, + body: new_body, + taxonomies: + } + } + } + end + let(:query) do + <<~GRAPHQL + mutation($input: UpdateProposalInput!) { + updateProposal(input: $input) { + id + title { translation(locale: "#{locale}") } + body { translation(locale: "#{locale}") } + address + taxonomies { id } + } + } + GRAPHQL + end + + before do + I18n.locale = "en" + end + + shared_examples "update proposal mutation examples" do + context "when user is not authorized" do + let!(:current_user) { nil } + + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError exception" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + + context "when user is authorized" do + context "with valid attributes" do + context "when requesting a different locale" do + let!(:model) { create(:proposal, title: { "en" => "Original title", "ca" => "Títol original" }, component: proposal_component, users: [author]) } + let(:locale) { "ca" } + + it "updates only the language" do + update = response["updateProposal"] + expect(update).to be_present + expect(update["title"]).to include({ "translation" => new_title }) + + expect(model.reload.title).to include({ "ca" => new_title }) + end + end + + it "updates the proposal" do + update = response["updateProposal"] + + expect(update).to be_present + expect(update).to include( + { + "id" => model.id.to_s, + "title" => { + "translation" => new_title + }, + "body" => { + "translation" => new_body + } + } + ) + expect(update["taxonomies"]).to include({ "id" => taxonomy.id.to_s }) + end + + context "with address and coordinates" do + let(:address) { "Carrer de la Pau, 1, Barcelona" } + let(:latitude) { 41.3851 } + let(:longitude) { 2.1734 } + let(:variables) do + { + input: { + locale:, + attributes: { + title: new_title, + body: new_body, + address:, + latitude:, + longitude: + } + } + } + end + + it "updates the proposal with location data" do + update = response["updateProposal"] + expect(update).to be_present + expect(update).to include( + { + "id" => model.id.to_s, + "address" => address + } + ) + end + end + end + + context "with invalid attributes" do + let(:new_title) { "short" } + let(:new_body) { "x" } + + it "returns an error" do + expect { response }.to raise_error(StandardError) + end + end + end + end + + context "with proposal author" do + let!(:current_user) { author } + + it_behaves_like "update proposal mutation examples" do + let!(:user_type) { :user } + end + + context "with having invalid locale" do + let(:locale) { "tlh" } + + it "raises an error" do + expect { response }.to raise_error(Api::Errors::InvalidLocaleError, /Invalid locale provided/) + end + end + + context "with invalid attributes" do + context "with invalid title" do + context "when is missing" do + let(:new_title) { "" } + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::AttributeValidationError, /too short/) + end + end + + context "when is too short" do + let(:new_title) { "Short" } + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::AttributeValidationError, /too short/) + end + end + + context "when is all small" do + let(:new_title) { "Updated proposal title for testing".downcase } + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::AttributeValidationError, /must start with a capital letter/) + end + end + end + + context "with invalid body" do + let(:new_body) { "Short" } + + it "raises an error" do + expect { response }.to raise_error(Decidim::Api::Errors::AttributeValidationError, /too short/) + end + end + end + end + + context "with admin user" do + let!(:user_type) { :admin } + + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError exception" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + + context "with normal user (not author)" do + it "raises an Decidim::Api::Errors::MutationNotAuthorizedError exception" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + + context "with api_user" do + let!(:current_user) { author } + + it_behaves_like "update proposal mutation examples" do + let!(:user_type) { :api_user } + end + end + end + end +end diff --git a/decidim-proposals/spec/types/vote_proposal_type_spec.rb b/decidim-proposals/spec/types/vote_proposal_type_spec.rb new file mode 100644 index 0000000000000..0c03ada1a4e86 --- /dev/null +++ b/decidim-proposals/spec/types/vote_proposal_type_spec.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Proposals + describe VoteProposalType, type: :graphql do + include_context "with a graphql class mutation" + + let(:root_klass) { ProposalMutationType } + let(:current_organization) { create(:organization, available_locales: [:en]) } + let(:participatory_process) { create(:participatory_process, :with_steps, organization: current_organization) } + let(:proposal_component) do + create(:proposal_component, + :with_votes_enabled, + participatory_space: participatory_process) + end + let!(:model) { create(:proposal, component: proposal_component) } + let(:component) { model.component } + let(:query) do + <<~GRAPHQL + mutation { + vote(input: {}) { + id + voteCount + } + } + GRAPHQL + end + + let(:variables) do + { + input: { + attributes: {} + } + } + end + + context "with a normal user" do + let(:user_type) { :user } + + context "when votes are enabled" do + it "votes the proposal" do + expect do + expect(response["vote"]).not_to be_nil + end.to change(ProposalVote, :count).by(1) + end + + it "returns the proposal with updated vote count" do + vote = response["vote"] + expect(vote).to be_present + expect(vote["id"]).to eq(model.id.to_s) + expect(vote["voteCount"]).to eq(1) + end + end + + context "when the user has already voted" do + before do + create(:proposal_vote, proposal: model, author: current_user) + end + + it "raises a Decidim::Api::Errors::ValidationError exception" do + expect { response }.to raise_error(Decidim::Api::Errors::ValidationError, "There was a problem voting the proposal.") + end + end + + context "when votes are disabled" do + let(:proposal_component) do + create(:proposal_component, + :with_votes_disabled, + participatory_space: participatory_process) + end + + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError exception" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + + context "when the proposal has reached maximum votes" do + before do + allow(model).to receive(:maximum_votes_reached?).and_return(true) + allow(model).to receive(:can_accumulate_votes_beyond_threshold).and_return(false) + end + + it "raises a Decidim::Api::Errors::ValidationError exception" do + expect { response }.to raise_error(Decidim::Api::Errors::ValidationError, "There was a problem voting the proposal.") + end + end + end + + context "with an unauthenticated user" do + let(:current_user) { nil } + + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError exception" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + end + end +end diff --git a/decidim-proposals/spec/types/withdraw_proposal_type_spec.rb b/decidim-proposals/spec/types/withdraw_proposal_type_spec.rb new file mode 100644 index 0000000000000..e602904c5c08d --- /dev/null +++ b/decidim-proposals/spec/types/withdraw_proposal_type_spec.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Proposals + describe WithdrawProposalType, type: :graphql do + include_context "with a graphql class mutation" + + let(:root_klass) { ProposalMutationType } + let(:current_organization) { create(:organization, available_locales: [:en]) } + let(:participatory_process) { create(:participatory_process, :with_steps, organization: current_organization) } + let(:proposal_component) { create(:proposal_component, participatory_space: participatory_process) } + let(:author) { create(:user, :confirmed, organization: current_organization) } + let!(:model) { create(:proposal, component: proposal_component, users: [author]) } + let(:component) { model.component } + let(:query) do + <<~GRAPHQL + mutation() { + withdraw(input: {}) { + id + state + withdrawnAt + } + } + GRAPHQL + end + + let(:variables) do + { + input: { + attributes: {} + } + } + end + + describe "withdrawing a proposal" do + context "with proposal author" do + let(:current_user) { author } + let(:user_type) { :user } + + it "withdraws the proposal" do + proposal = response["withdraw"] + expect(proposal).to be_present + expect(proposal["id"]).to eq(model.id.to_s) + expect(model.reload).to be_withdrawn + expect(model.withdrawn_at).to be_present + end + end + + context "with admin user" do + let!(:user_type) { :admin } + + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError exception" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + + context "with api_user that is not the author" do + let!(:user_type) { :api_user } + + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError exception" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + + context "with normal user that is not the author" do + it "raises a Decidim::Api::Errors::MutationNotAuthorizedError exception" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + end + end + + context "with api_user that is the author" do + let!(:model) { create(:proposal, component: proposal_component, users: [current_user]) } + let!(:user_type) { :api_user } + + it "withdraws the proposal" do + proposal = response["withdraw"] + expect(proposal).to be_present + expect(proposal["id"]).to eq(model.id.to_s) + expect(model.reload).to be_withdrawn + expect(model.withdrawn_at).to be_present + end + end + end + + context "when proposal has votes" do + let(:current_user) { author } + + before do + model.votes.create!(author: create(:user, :confirmed, organization: current_organization)) + end + + it "does not withdraw the proposal and returns an error" do + expect { response }.to raise_error(Decidim::Api::Errors::ValidationError, "This proposal cannot be withdrawn because it already has votes.") + expect(model.reload).not_to be_withdrawn + expect(model.withdrawn_at).not_to be_present + end + end + + context "when proposal is already withdrawn" do + let!(:model) { create(:proposal, :withdrawn, component: proposal_component, users: [author]) } + let(:current_user) { author } + + it "remains withdrawn and returns an error" do + expect { response }.to raise_error(Decidim::Api::Errors::MutationNotAuthorizedError, "You do not have permission to perform this mutation") + expect(model.reload).to be_withdrawn + expect(model.withdrawn_at).to be_present + end + end + + context "when proposal is already answered" do + let!(:model) { create(:proposal, :with_answer, component: proposal_component, users: [author]) } + let(:current_user) { author } + + it "can be withdrawn by author" do + proposal = response["withdraw"] + expect(proposal).to be_present + expect(proposal["id"]).to eq(model.id.to_s) + expect(model.reload).to be_withdrawn + expect(model.withdrawn_at).to be_present + end + end + end + end +end diff --git a/decidim-surveys/app/views/decidim/surveys/admin/surveys/index.html.erb b/decidim-surveys/app/views/decidim/surveys/admin/surveys/index.html.erb index af902c996941e..308db89b9beaf 100644 --- a/decidim-surveys/app/views/decidim/surveys/admin/surveys/index.html.erb +++ b/decidim-surveys/app/views/decidim/surveys/admin/surveys/index.html.erb @@ -61,10 +61,18 @@ + <% if survey.questionnaire.responses.any? %> + + <% end %>
    diff --git a/decidim-surveys/app/views/decidim/surveys/surveys/not_allowed.html.erb b/decidim-surveys/app/views/decidim/surveys/surveys/not_allowed.html.erb index a65594c9f8492..aca95331f4a20 100644 --- a/decidim-surveys/app/views/decidim/surveys/surveys/not_allowed.html.erb +++ b/decidim-surveys/app/views/decidim/surveys/surveys/not_allowed.html.erb @@ -17,6 +17,6 @@ <%= render partial: "decidim/shared/component_announcement" if current_component.manifest_name == "surveys" %>
    <% body = t("decidim.forms.questionnaires.show.questionnaire_responded.body") %> - <%= cell("decidim/announcement", { title: t("decidim.forms.questionnaires.show.questionnaire_responded.title"), body: body }) %> + <%= cell("decidim/announcement", { title: t("decidim.forms.questionnaires.show.questionnaire_responded.title"), body: }) %>
    <% end %> diff --git a/decidim-surveys/config/locales/ca-IT.yml b/decidim-surveys/config/locales/ca-IT.yml index 47e021e9f06c7..6fdc6a8e99931 100644 --- a/decidim-surveys/config/locales/ca-IT.yml +++ b/decidim-surveys/config/locales/ca-IT.yml @@ -71,6 +71,7 @@ ca-IT: manage_questions: Preguntes new_survey: Nova enquesta preview: Previsualitzar + responses: Respostes responses_alert: L'opció d'esborrar les respostes en publicar l'enquesta està activada. Si segueixes, s'esborraran les %{responses_count} existents actualment. title: Accions admin: diff --git a/decidim-surveys/config/locales/ca.yml b/decidim-surveys/config/locales/ca.yml index 511dc34128dbe..2d4c4cc13d05f 100644 --- a/decidim-surveys/config/locales/ca.yml +++ b/decidim-surveys/config/locales/ca.yml @@ -71,6 +71,7 @@ ca: manage_questions: Preguntes new_survey: Nova enquesta preview: Previsualitzar + responses: Respostes responses_alert: L'opció d'esborrar les respostes en publicar l'enquesta està activada. Si segueixes, s'esborraran les %{responses_count} existents actualment. title: Accions admin: diff --git a/decidim-surveys/config/locales/cs.yml b/decidim-surveys/config/locales/cs.yml index cda656ab0872e..831d6329d8439 100644 --- a/decidim-surveys/config/locales/cs.yml +++ b/decidim-surveys/config/locales/cs.yml @@ -44,6 +44,11 @@ cs: email_outro: Toto oznámení jste obdrželi, protože jste sledovali %{participatory_space_title}. Po předchozím propojení můžete přestat přijímat oznámení. email_subject: Nový průzkum v %{participatory_space_title} notification_title: Průzkum %{resource_title} v %{participatory_space_title} je nyní otevřený. + open_data: + help: + published_survey_user_responses: + body: Obsah odpovědi + created_at: Časové razítko, kdy byla odpověď vytvořena statistics: responses: 'Odpovědi:' responses_count: Odpovědi diff --git a/decidim-surveys/config/locales/en.yml b/decidim-surveys/config/locales/en.yml index 4e92e4b9352df..7f023100ef764 100644 --- a/decidim-surveys/config/locales/en.yml +++ b/decidim-surveys/config/locales/en.yml @@ -72,6 +72,7 @@ en: manage_questions: Questions new_survey: New survey preview: Preview + responses: Responses responses_alert: Delete responses at publish is active for this survey. There are currently %{responses_count} responses that will be destroyed if you continue. title: Actions admin: diff --git a/decidim-surveys/config/locales/es-MX.yml b/decidim-surveys/config/locales/es-MX.yml index f376da438495a..55594598b9b7c 100644 --- a/decidim-surveys/config/locales/es-MX.yml +++ b/decidim-surveys/config/locales/es-MX.yml @@ -71,6 +71,7 @@ es-MX: manage_questions: Preguntas new_survey: Nueva encuesta preview: Previsualizar + responses: Respuestas responses_alert: La opción de borrar las respuestas al publicar la encuesta está activada. Si sigues, se borrarán las %{responses_count} existentes actualmente. title: Acciones admin: diff --git a/decidim-surveys/config/locales/es-PY.yml b/decidim-surveys/config/locales/es-PY.yml index 5c77b85e08657..4dc5f5a7062eb 100644 --- a/decidim-surveys/config/locales/es-PY.yml +++ b/decidim-surveys/config/locales/es-PY.yml @@ -71,6 +71,7 @@ es-PY: manage_questions: Preguntas new_survey: Nueva encuesta preview: Previsualizar + responses: Respuestas responses_alert: La opción de borrar las respuestas al publicar la encuesta está activada. Si sigues, se borrarán las %{responses_count} existentes actualmente. title: Acciones admin: diff --git a/decidim-surveys/config/locales/es.yml b/decidim-surveys/config/locales/es.yml index 71b369fb78bf7..9f48afc165cdb 100644 --- a/decidim-surveys/config/locales/es.yml +++ b/decidim-surveys/config/locales/es.yml @@ -71,6 +71,7 @@ es: manage_questions: Preguntas new_survey: Nueva encuesta preview: Previsualizar + responses: Respuestas responses_alert: La opción de borrar las respuestas al publicar la encuesta está activada. Si sigues, se borrarán las %{responses_count} existentes actualmente. title: Acciones admin: diff --git a/decidim-surveys/config/locales/eu.yml b/decidim-surveys/config/locales/eu.yml index 775418d225c95..4fd8e1fb6e309 100644 --- a/decidim-surveys/config/locales/eu.yml +++ b/decidim-surveys/config/locales/eu.yml @@ -71,6 +71,7 @@ eu: manage_questions: Galderak new_survey: Beste inkesta bat preview: Aurreikusi + responses: Erantzunak responses_alert: Erantzunak ezabatzea argitalpenean aktibatuta dago inkesta honetarako. Une honetan, erantzunen %{responses_count} suntsitu egingo dira, jarraitzen baduzu. title: Ekintzak admin: diff --git a/decidim-surveys/config/locales/fi-plain.yml b/decidim-surveys/config/locales/fi-plain.yml index 802c2d18fb03f..7ae6b99fd2870 100644 --- a/decidim-surveys/config/locales/fi-plain.yml +++ b/decidim-surveys/config/locales/fi-plain.yml @@ -71,6 +71,7 @@ fi-pl: manage_questions: Kysymykset new_survey: Uusi kysely preview: Esikatsele + responses: Vastaukset responses_alert: Kyselyn vastausten poistaminen julkaisun yhteydessä on otettu käyttöön tälle kyselylle. Kyselyllä on tällä hetkellä %{responses_count} vastausta, jotka poistetaan, mikäli jatkat eteenpäin. title: Toiminnot admin: diff --git a/decidim-surveys/config/locales/fi.yml b/decidim-surveys/config/locales/fi.yml index dc9edd86ce0ed..c4e3df9fd73b8 100644 --- a/decidim-surveys/config/locales/fi.yml +++ b/decidim-surveys/config/locales/fi.yml @@ -71,6 +71,7 @@ fi: manage_questions: Kysymykset new_survey: Uusi kysely preview: Esikatsele + responses: Vastaukset responses_alert: Kyselyn vastausten poistaminen julkaisun yhteydessä on otettu käyttöön tälle kyselylle. Kyselyllä on tällä hetkellä %{responses_count} vastausta, jotka poistetaan, mikäli jatkat eteenpäin. title: Toiminnot admin: diff --git a/decidim-surveys/config/locales/fr-CA.yml b/decidim-surveys/config/locales/fr-CA.yml index cdb5d5f9047b2..f8897e80dc782 100644 --- a/decidim-surveys/config/locales/fr-CA.yml +++ b/decidim-surveys/config/locales/fr-CA.yml @@ -71,6 +71,7 @@ fr-CA: manage_questions: Questions new_survey: Nouvelle enquête preview: Aperçu + responses: Réponses responses_alert: La suppression des réponses lors de la publication est active pour cette enquête. Il y a actuellement %{responses_count} réponses qui seront supprimées si vous continuez. title: Actions admin: diff --git a/decidim-surveys/config/locales/fr.yml b/decidim-surveys/config/locales/fr.yml index 844d8f44d216b..78cfff4071d96 100644 --- a/decidim-surveys/config/locales/fr.yml +++ b/decidim-surveys/config/locales/fr.yml @@ -71,6 +71,7 @@ fr: manage_questions: Questions new_survey: Nouvelle enquête preview: Aperçu + responses: Réponses responses_alert: La suppression des réponses lors de la publication est active pour cette enquête. Il y a actuellement %{responses_count} réponses qui seront supprimées si vous continuez. title: Actions admin: diff --git a/decidim-surveys/config/locales/ja.yml b/decidim-surveys/config/locales/ja.yml index 0c8d28cb85a9c..8b46d0c9ecd55 100644 --- a/decidim-surveys/config/locales/ja.yml +++ b/decidim-surveys/config/locales/ja.yml @@ -46,6 +46,17 @@ ja: email_outro: '%{participatory_space_title}をフォローしているため、この通知を受け取りました。前のリンクに続く通知の受信を停止することができます。' email_subject: '%{participatory_space_title} での新しいアンケート' notification_title: %{resource_title}の %{participatory_space_title}のアンケート が公開されました。 + open_data: + help: + published_survey_user_responses: + body: 回答の内容 + created_at: 回答が作成されたタイムスタンプ + id: アンケート回答の固有ID + ip_hash: プライバシー保護のためにハッシュ化された回答者のIPアドレス + question: 回答された質問 + registered: 登録済みの参加者 + unregistered: 未登録の参加者 + user_status: 回答を提出したユーザーのステータス statistics: responses: '応答:' responses_count: 回答 @@ -58,6 +69,7 @@ ja: manage_questions: 質問 new_survey: 新しいアンケート preview: プレビュー + responses: 回答 responses_alert: このアンケートでは、公開時に回答を削除する設定が有効になっています。現在%{responses_count}件の回答があり、続行するとそれらは削除されます。 title: アクション admin: diff --git a/decidim-surveys/config/locales/pt-BR.yml b/decidim-surveys/config/locales/pt-BR.yml index e9a130dcb5586..db6973703d66f 100644 --- a/decidim-surveys/config/locales/pt-BR.yml +++ b/decidim-surveys/config/locales/pt-BR.yml @@ -8,10 +8,30 @@ pt-BR: decidim/surveys/survey: one: Enquete other: Enquetes + decidim/surveys/survey_response: + one: Resposta + other: Respostas decidim: + admin: + actions: + confirm_unpublish_survey: Tem certeza de que deseja remover esta pesquisa da publicação? + see_survey: Veja a pesquisa + admin_log: + changeset: + surveys: Pesquisas + menu: + surveys_menu: + main: Principal + questions: Questões + responses: Respostas + settings: Configurações components: surveys: + actions: + respond: Responder + name: Pesquisas settings: + announcement: Avisos global: announcement: Anúncio step: @@ -28,11 +48,131 @@ pt-BR: email_outro: Você recebeu esta notificação porque está seguindo %{participatory_space_title}. Você pode parar de receber notificações após o link anterior. email_subject: Uma nova pesquisa em %{participatory_space_title} notification_title: A pesquisa %{resource_title} em %{participatory_space_title} está agora aberta. + open_data: + help: + published_survey_user_responses: + body: O conteúdo da resposta + created_at: Carimbo de data/hora em que a resposta foi criada + id: Identificador único para a resposta da pesquisa + ip_hash: Endereço IP criptografado do respondente para fins de privacidade + question: A pergunta respondida + registered: Participante inscrito + unregistered: Participante não registrado + user_status: '''Estado'' do usuário que enviou a resposta' + statistics: + responses: 'Respostas:' + responses_count: Respostas + surveys_count_tooltip: O número de pesquisas disponíveis e respostas coletadas. surveys: + actions: + confirm_destroy: Tem certeza de que deseja excluir isso? + destroy: Destruir + edit: Editar + manage_questions: Questões + new_survey: Nova pesquisa + preview: Pré-visualização + responses: Respostas + responses_alert: A opção "Excluir respostas na publicação" está ativa para esta pesquisa. Atualmente, existem %{responses_count} respostas excluídas se você continuar. + title: Ações admin: + exports: + survey_user_responses: Respostas dos participantes da pesquisa + publish_responses: + index: + description: |- + Ao publicar as respostas das perguntas, você as tornará visíveis ao público. + Você pode selecionar as respostas que deseja publicar clicando na caixa de seleção ao lado de cada pergunta. + Você só pode publicar os seguintes tipos de pergunta: "Opção única", "Opção múltipla", "Matriz (opção única)", "Matriz (opção múltipla)" e "Classificação". + responses: + one: "%{count} Resposta" + other: "%{count} Respostas" + status: + not_published: Não publicado + published: Publicado + title: Publicar respostas + questions: + surveys: + edit: + title: Questões + responses: + index: + no_responses: Ainda não há respostas + title: "%{total} Respostas totais" + show: + title: 'Resposta #%{número}' + settings: + surveys: + edit: + title: Configurações surveys: + create: + invalid: Ocorreu um problema ao criar a pesquisa. + success: Pesquisa criada com sucesso. + destroy: + success: Pesquisa excluída com sucesso. edit: title: Editar questionário + index: + title: Pesquisas + new: + title: Nova pesquisa + publish: + invalid: Houve um problema na publicação desta pesquisa. + success: Pesquisa publicada com sucesso. + unpublish: + invalid: Houve um problema ao cancelar a publicação desta pesquisa. + success: Pesquisa não publicada com sucesso. update: invalid: Houve erros ao salvar a pesquisa. success: Pesquisa salva com sucesso. + admin_log: + survey: + create: "%{user_name} Criou a pesquisa %{resource_name} no espaço %{space_name} como uma pesquisa" + delete: "%{user_name} Excluiu a pesquisa %{resource_name} no espaço %{space_name} como uma pesquisa" + publish: "%{user_name} Publicou a pesquisa %{resource_name} no espaço %{space_name}" + unpublish: "%{user_name} Deixou de publicar a pesquisa %{resource_name} no espaço %{space_name}" + update: "%{user_name} Atualizou a pesquisa %{resource_name} no espaço %{space_name}" + directory: + surveys: + index: + surveys: Pesquisas + last_activity: + new_survey: 'Nova pesquisa:' + models: + survey: + fields: + published: Publicado + questions: Questões + responses: Respostas + status: Estado + title: Titulo + published: + published: Publicado + unpublished: Não publicado + status: + closed: Fechado + open: Aberto + survey_confirmation_mailer: + confirmation: + body: Você respondeu com sucesso à pesquisa %{questionnaire_title} no prazo de %{participatory_space} + subject: Confirmação de resposta ao questionário %{questionnaire_title} + export_name: Respostas da pesquisa + surveys: + count: + surveys_count: + one: "%{count} Pesquisa" + other: "%{count} Pesquisas" + filters: + all: Todos + state_values: + closed: Fechado + open: Aberto + no_surveys_warning: Nenhuma pesquisa corresponde aos seus critérios de busca ou não há nenhuma pesquisa disponível. + response: + invalid: Houve um problema ao responder à pesquisa. + spam_detected: Ocorreu um problema ao preencher o formulário. Talvez você tenha respondido muito rápido. Pode tentar novamente? + success: A pesquisa foi respondida com sucesso. + show: + closed: Fechado + open: Aberto + questions: Questões diff --git a/decidim-surveys/config/locales/ro-RO.yml b/decidim-surveys/config/locales/ro-RO.yml index aadd2076d9158..f306ae2930c2f 100644 --- a/decidim-surveys/config/locales/ro-RO.yml +++ b/decidim-surveys/config/locales/ro-RO.yml @@ -9,50 +9,174 @@ ro: one: Sondaj few: Sondaje other: Sondaje + decidim/surveys/survey_response: + one: Răspuns + few: Răspunsuri + other: Răspunsuri decidim: admin: + actions: + confirm_unpublish_survey: Sunteți sigur că doriți să anulați publicarea acestui chestionar? + see_survey: Vedeți sondajul + admin_log: + changeset: + surveys: sondaje menu: surveys_menu: main: Principal questions: Întrebări + responses: Răspunsuri settings: Setări components: surveys: + actions: + respond: Răspundeți + name: Sondaje settings: + announcement: Anunț global: - announcement: Anunţ + announcement: Anunț step: - announcement: Anunţ + announcement: Anunț events: surveys: survey_closed: email_intro: Sondajul %{resource_title} din %{participatory_space_title} a fost închis. - email_outro: Ai primit această notificare deoarece urmărești „%{participatory_space_title}”. Poți anula aceste notificări de la link-ul anterior. - email_subject: În %{participatory_space_title} s-a încheiat un sondaj + email_outro: Ați primit această notificare deoarece urmăriți %{participatory_space_title}. Puteți înceta să primiți notificări urmând linkul anterior. + email_subject: Un sondaj s-a încheiat în %{participatory_space_title} notification_title: Sondajul %{resource_title} din %{participatory_space_title} s-a încheiat. survey_opened: - email_intro: 'Sondajul %{resource_title} din %{participatory_space_title} este acum deschis. Poți participa la acesta din această pagină:' - email_outro: Ai primit această notificare deoarece urmărești „%{participatory_space_title}”. Poți anula aceste notificări de la link-ul anterior. - email_subject: Un nou sondaj în %{participatory_space_title} - notification_title: Sondajul %{resource_title} din %{participatory_space_title} este acum deschis. + email_intro: 'Sondajul %{resource_title} din %{participatory_space_title} a început. Poți participa la acesta din această pagină:' + email_outro: Ați primit această notificare deoarece urmărești „%{participatory_space_title}”. Puteți anula aceste notificări de la link-ul anterior. + email_subject: Un sondaj nou în %{participatory_space_title} + notification_title: Sondajul %{resource_title} din %{participatory_space_title} a început. + open_data: + help: + published_survey_user_responses: + body: Conținutul răspunsului + created_at: Data și ora când a fost creat răspunsul + id: Identificator unic pentru răspunsurile sondajului + ip_hash: Semnătura adresei IP a respondentului pentru confidențialitate + question: Întrebarea la care s-a răspuns + registered: Participant înregistrat + unregistered: Participant neînregistrat + user_status: Starea participantului care a trimis răspunsul statistics: responses: 'Răspunsuri:' + responses_count: Răspunsuri surveys_count_tooltip: Numărul de sondaje disponibile și răspunsuri care au fost colectate. surveys: actions: + confirm_destroy: Sunteți sigur că doriți să eliminați asta? + destroy: Eliminați + edit: Modificați manage_questions: Întrebări + new_survey: Sondaj nou + preview: Previzualizați + responses: Răspunsuri + responses_alert: Ștergerea răspunsurilor la publicare este activă pentru acest sondaj. Există în prezent %{responses_count} răspunsuri care vor fi eliminate în momentul publicării. + title: Acțiuni admin: + exports: + survey_user_responses: Răspunsuri ale participantului la sondaj + publish_responses: + index: + description: |- + Prin publicarea răspunsurilor la întrebări, acestea vor fi vizibile pentru public. + Puteți selecta răspunsurile pe care doriți să le publicați făcând clic pe caseta de selectare de lângă fiecare întrebare. + Puteți publica numai următoarele tipuri de întrebări: "opțiune unică", "opțiune multiplă", "matrice (opțiune unică)", "matrice (opțiune multiplă)" și "Sortare". + responses: + one: "%{count} răspuns" + few: "%{count} răspunsuri" + other: "%{count} răspunsuri" + status: + not_published: Nepublicat + published: Publicat + title: Publicați răspunsuri questions: surveys: edit: title: Întrebări + responses: + index: + no_responses: Nu există răspunsuri încă + title: "%{total} răspunsuri în total" + show: + title: 'Răspuns #%{number}' settings: surveys: edit: title: Setări surveys: + create: + invalid: A apărut o eroare la crearea sondajului. + success: Sondajul a fost creat cu succes. + destroy: + success: Sondajul a fost eliminat cu succes. + edit: + title: Modificați sondajul + index: + title: Sondaje + new: + title: Sondaj nou + publish: + invalid: A apărut o problemă la publicarea acestui sondaj. + success: Sondajul a fost publicat cu succes. + unpublish: + invalid: A apărut o problemă la anularea publicării acestui sondaj. + success: Sondajul a fost retras cu succes de la publicare. update: - invalid: A apărut o eroare la salvarea sondajului. - success: Sondajul a fost salvat cu succes. + invalid: A apărut o eroare la actualizarea sondajului. + success: Sondajul a fost actualizat cu succes. + admin_log: + survey: + create: "%{user_name} a creat sondajul %{resource_name} în spațiul %{space_name}" + delete: "%{user_name} a eliminat sondajul %{resource_name} în spațiul %{space_name}" + publish: "%{user_name} a publicat sondajul %{resource_name} în spațiul participativ %{space_name}" + unpublish: "%{user_name} a anulat publicarea sondajului %{resource_name} din spațiul participativ %{space_name}" + update: "%{user_name} a actualizat sondajul %{resource_name} din spațiul %{space_name}" + directory: + surveys: + index: + surveys: Sondaje last_activity: new_survey: 'Sondaj nou:' + models: + survey: + fields: + published: Publicat + questions: Întrebări + responses: Răspunsuri + status: Stare + title: Titlu + published: + published: Publicat + unpublished: Nepublicat + status: + closed: Închis + open: Deschis + survey_confirmation_mailer: + confirmation: + body: Ați răspuns cu succes la sondajul %{questionnaire_title} din %{participatory_space} + subject: Confirmarea răspunsurilor la sondajul %{questionnaire_tile} + export_name: Răspunsuri la sondaj + surveys: + count: + surveys_count: + one: "%{count} sondaj" + few: "%{count} sondaje" + other: "%{count} sondaje" + filters: + all: Toate + state_values: + closed: Închise + open: Deschise + no_surveys_warning: Nici un sondaj nu corespunde criteriilor de căutare sau nu există nici un sondaj deschis. + response: + invalid: A apărut o problemă la completarea sondajului. + spam_detected: A apărut o problemă la completarea formularului. Poate că ați fost prea rapid(ă), puteți încerca din nou? + success: Răspunsul pentru sondaj a fost înregistrat cu succes. + show: + closed: Închise + open: Deschise + questions: întrebări diff --git a/decidim-surveys/config/locales/sv.yml b/decidim-surveys/config/locales/sv.yml index 519d2753591b3..d1747f97fedb2 100644 --- a/decidim-surveys/config/locales/sv.yml +++ b/decidim-surveys/config/locales/sv.yml @@ -17,6 +17,7 @@ sv: surveys: actions: respond: Besvara + name: Enkäter settings: global: announcement: Meddelande @@ -47,8 +48,10 @@ sv: user_status: Status för användaren som skickade svaret statistics: responses: 'Svar:' - surveys_count_tooltip: Antalet tillgängliga undersökningar och svar som samlats in. + surveys_count_tooltip: Antalet enkäter och svar som samlats in. surveys: + actions: + responses: Svar admin: surveys: new: diff --git a/decidim-surveys/lib/decidim/api/survey_type.rb b/decidim-surveys/lib/decidim/api/survey_type.rb index 520da3b766667..d925c69a1b49c 100644 --- a/decidim-surveys/lib/decidim/api/survey_type.rb +++ b/decidim-surveys/lib/decidim/api/survey_type.rb @@ -27,8 +27,6 @@ def self.authorized?(object, context) context[:current_settings] = object.component.current_settings super - rescue Decidim::PermissionAction::PermissionNotSetError - false end end end diff --git a/decidim-surveys/lib/decidim/api/surveys_type.rb b/decidim-surveys/lib/decidim/api/surveys_type.rb index b7727082cab53..89ae53eb48146 100644 --- a/decidim-surveys/lib/decidim/api/surveys_type.rb +++ b/decidim-surveys/lib/decidim/api/surveys_type.rb @@ -15,8 +15,8 @@ def surveys Survey.where(component: object).includes(:component) end - def survey(**args) - Survey.where(component: object).find_by(id: args[:id]) + def survey(id:) + Survey.published.where(component: object).find(id) end end end diff --git a/decidim-surveys/lib/decidim/surveys/component.rb b/decidim-surveys/lib/decidim/surveys/component.rb index f2f89ea4ae30a..bbe2c85187e43 100644 --- a/decidim-surveys/lib/decidim/surveys/component.rb +++ b/decidim-surveys/lib/decidim/surveys/component.rb @@ -16,13 +16,6 @@ component.newsletter_participant_entities = ["Decidim::Forms::Response"] - component.on(:before_destroy) do |instance| - survey = Decidim::Surveys::Survey.find_by(decidim_component_id: instance.id) - survey_responses_for_component = Decidim::Forms::Response.where(questionnaire: survey.questionnaire) - - raise "Cannot destroy this component when there are survey responses" if survey_responses_for_component.any? - end - component.register_resource(:survey) do |resource| resource.model_class_name = "Decidim::Surveys::Survey" resource.card = "decidim/surveys/survey" @@ -73,7 +66,7 @@ component.exports :published_survey_user_responses do |exports| exports.collection do |component| - survey = Decidim::Surveys::Survey.find_by(component: component) + survey = Decidim::Surveys::Survey.find_by(component:) Decidim::Forms::Response .joins(:question) diff --git a/decidim-surveys/lib/decidim/surveys/seeds.rb b/decidim-surveys/lib/decidim/surveys/seeds.rb index 8f67ae902d6e5..8622bd4e326cb 100644 --- a/decidim-surveys/lib/decidim/surveys/seeds.rb +++ b/decidim-surveys/lib/decidim/surveys/seeds.rb @@ -20,7 +20,7 @@ def call next if questionnaire.questionnaire_for.allow_responses - rand(200).times { create_responses!(questionnaire:) } + rand(0..config_value(:surveys_responses_count)).times { create_responses!(questionnaire:) } end end @@ -90,7 +90,7 @@ def create_questions!(questionnaire:) position: index + 2 ) - 3.times do + config_value(:surveys_response_options_count).times do question.response_options.create!(body: Decidim::Faker::Localized.sentence) end @@ -114,7 +114,7 @@ def create_questions!(questionnaire:) position: index ) - 3.times do |position| + config_value(:surveys_matrix_rows_count).times do |position| question.response_options.create!(body: Decidim::Faker::Localized.sentence) question.matrix_rows.create!(body: Decidim::Faker::Localized.sentence, position:) end diff --git a/decidim-surveys/spec/lib/decidim/surveys/component_spec.rb b/decidim-surveys/spec/lib/decidim/surveys/component_spec.rb index 33fa5ff2e8c47..d3a461adc9683 100644 --- a/decidim-surveys/spec/lib/decidim/surveys/component_spec.rb +++ b/decidim-surveys/spec/lib/decidim/surveys/component_spec.rb @@ -8,32 +8,6 @@ let(:component) { create(:surveys_component) } let(:new_component) { create(:surveys_component) } - describe "before_destroy hooks" do - context "when there are no responses" do - before do - create(:survey, component:) - end - - it "does not raise any error" do - expect { subject.manifest.run_hooks(:before_destroy, subject) }.not_to raise_error - end - end - - context "with responses" do - before do - survey = create(:survey, component:) - create(:response, questionnaire: survey.questionnaire) - end - - it "raises an error" do - expect { subject.manifest.run_hooks(:before_destroy, subject) }.to raise_error( - RuntimeError, - "Cannot destroy this component when there are survey responses" - ) - end - end - end - context "when copying component" do it "does not raise any error" do expect { subject.manifest.run_hooks(:copy, old_component: component, new_component:) }.not_to raise_error diff --git a/decidim-surveys/spec/system/admin_manages_surveys_spec.rb b/decidim-surveys/spec/system/admin_manages_surveys_spec.rb index 5f2b474822956..a322132c0f452 100644 --- a/decidim-surveys/spec/system/admin_manages_surveys_spec.rb +++ b/decidim-surveys/spec/system/admin_manages_surveys_spec.rb @@ -342,6 +342,35 @@ end end + context "when the survey has responses or more" do + let!(:question) do + create(:questionnaire_question, questionnaire:) + end + let!(:response) { create(:response, questionnaire:, question:) } + + before do + visit manage_questionnaire_path + end + + it "allows access to responses" do + within "tr", text: decidim_sanitize_translated(survey.title) do + find("button[data-controller='dropdown']").click + expect(page).to have_link("Responses") + end + end + end + + context "when the survey has no responses" do + let!(:question) { create(:questionnaire_question, questionnaire:) } + + it "does not show the Responses button" do + within "tr", text: decidim_sanitize_translated(survey.title) do + find("button[data-controller='dropdown']").click + expect(page).to have_no_link("Responses") + end + end + end + context "when updates the questionnaire" do let(:description) do { @@ -378,6 +407,10 @@ def questionnaire_public_path main_component_path(component) end + def manage_questionnaire_path + Decidim::EngineRouter.admin_proxy(component).surveys_path + end + private def find_nested_form_field(attribute, visible: :visible) diff --git a/decidim-surveys/spec/system/private_space_survey_spec.rb b/decidim-surveys/spec/system/private_space_survey_spec.rb index 4e8ddd8b7978e..e5ca82a3f8b7f 100644 --- a/decidim-surveys/spec/system/private_space_survey_spec.rb +++ b/decidim-surveys/spec/system/private_space_survey_spec.rb @@ -25,7 +25,7 @@ let(:user) { create(:user, :confirmed, organization:) } let!(:another_user) { create(:user, :confirmed, organization:) } - let!(:participatory_space_private_user) { create(:participatory_space_private_user, user: another_user, privatable_to: participatory_space_private) } + let!(:member) { create(:member, user: another_user, participatory_space: participatory_space_private) } let!(:questionnaire) { create(:questionnaire, title:, description:) } let!(:survey) { create(:survey, :published, :allow_responses, component:, questionnaire:) } @@ -65,7 +65,7 @@ def visit_component end context "when the user is logged in" do - context "and is private user space" do + context "and is member space" do before do login_as another_user, scope: :user end @@ -92,7 +92,7 @@ def visit_component end end - context "and is not private user space" do + context "and is not member space" do before do login_as user, scope: :user end @@ -103,7 +103,7 @@ def visit_component expect(page).to have_i18n_content(questionnaire.title) expect(page).to have_i18n_content(questionnaire.description) - expect(page).to have_content "The form is available only for private users" + expect(page).to have_content "The form is available only for members" expect(page).to have_content "Form closed" expect(page).to have_css(".button[disabled]") @@ -128,7 +128,7 @@ def visit_component end context "when the user is logged in" do - context "and is private user space" do + context "and is member space" do before do login_as another_user, scope: :user end @@ -157,7 +157,7 @@ def visit_component end end - context "and is not private user space" do + context "and is not member space" do let(:target_path) { main_component_path(component) } before do diff --git a/decidim-surveys/spec/system/survey_spec.rb b/decidim-surveys/spec/system/survey_spec.rb index 30039b8d83f7d..a4e2f14d90a61 100644 --- a/decidim-surveys/spec/system/survey_spec.rb +++ b/decidim-surveys/spec/system/survey_spec.rb @@ -39,10 +39,6 @@ it "does not allow responding the survey" do visit_component - within(".menu-bar") do - expect(page).to have_content(translated(component.name)) - end - choose "All" expect(page).to have_i18n_content(questionnaire.title) @@ -104,11 +100,6 @@ choose "All" click_on translated_attribute(questionnaire.title) - within(".menu-bar") do - expect(page).to have_content(translated(component.name)) - expect(page).to have_content(translated(questionnaire.title)) - end - # does not show the charts if not published expect(page.html).not_to include('new Chartkick["ColumnChart"]("chart-1"') expect(page.html).not_to include('new Chartkick["ColumnChart"]("chart-2"') diff --git a/decidim-surveys/spec/system/surveys_breadcrumbs_spec.rb b/decidim-surveys/spec/system/surveys_breadcrumbs_spec.rb new file mode 100644 index 0000000000000..9c71f04bb39a1 --- /dev/null +++ b/decidim-surveys/spec/system/surveys_breadcrumbs_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Surveys Breadcrumb" do + include_context "with a component" + + let(:manifest_name) { "surveys" } + let(:title) do + { + "en" => "Survey's title", + "ca" => "Títol de l'enquesta'", + "es" => "Título de la encuesta" + } + end + let!(:questionnaire) { create(:questionnaire, title:) } + let!(:survey) { create(:survey, :published, component:, questionnaire:) } + + context "when the survey does not allow responses" do + it "shows the correct information in breadcrumb (space, component)" do + visit_component + + within(".menu-bar") do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + end + end + end + + context "when the survey has questions' responses published" do + let(:question_single_option) { create(:questionnaire_question, :with_response_options, position: 0, question_type: "single_option", questionnaire:) } + + before do + 10.times do + response = create(:response, question: question_single_option, questionnaire:) + response_option = question_single_option.response_options.sample + create(:response_choice, response_option:, response:, matrix_row: nil) + end + end + + it "shows the correct information in breadcrumb (space, component, questionnaire)" do + visit_component + choose "All" + click_on translated_attribute(questionnaire.title) + + within(".menu-bar") do + expect(page).to have_content(translated(component.participatory_space.title)) + expect(page).to have_content(translated(component.name)) + expect(page).to have_content(translated(questionnaire.title)) + end + end + end +end diff --git a/decidim-surveys/spec/types/integration_schema_spec.rb b/decidim-surveys/spec/types/integration_schema_spec.rb index 91472d1118763..5f27b5ffa8032 100644 --- a/decidim-surveys/spec/types/integration_schema_spec.rb +++ b/decidim-surveys/spec/types/integration_schema_spec.rb @@ -62,7 +62,7 @@ let(:component_type) { "Surveys" } let!(:current_component) { create(:surveys_component, participatory_space: participatory_process) } - let!(:survey) { create(:survey, component: current_component) } + let!(:survey) { create(:survey, :published, component: current_component) } let!(:questionnaire) { create(:questionnaire, :with_questions, questionnaire_for: survey) } let(:survey_single_result) do diff --git a/decidim-surveys/spec/types/surveys_type_spec.rb b/decidim-surveys/spec/types/surveys_type_spec.rb index 4913a741d69f8..cf8a5aa4a3732 100644 --- a/decidim-surveys/spec/types/surveys_type_spec.rb +++ b/decidim-surveys/spec/types/surveys_type_spec.rb @@ -12,8 +12,8 @@ module Surveys it_behaves_like "a component query type" describe "surveys" do - let!(:component_surveys) { create_list(:survey, 2, component: model) } - let!(:other_surveys) { create_list(:survey, 2) } + let!(:component_surveys) { create_list(:survey, 2, :published, component: model) } + let!(:other_surveys) { create_list(:survey, 2, :published) } let(:query) { "{ surveys { edges { node { id } } } }" } @@ -29,7 +29,7 @@ module Surveys let(:variables) { { id: survey.id.to_s } } context "when the survey belongs to the component" do - let!(:survey) { create(:survey, component: model) } + let!(:survey) { create(:survey, :published, component: model) } it "finds the survey" do expect(response["survey"]["id"]).to eq(survey.id.to_s) @@ -37,10 +37,10 @@ module Surveys end context "when the survey does not belong to the component" do - let!(:survey) { create(:survey, component: create(:surveys_component)) } + let!(:survey) { create(:survey, :published, component: create(:surveys_component)) } - it "returns null" do - expect(response["survey"]).to be_nil + it "raises error" do + expect { response }.to raise_error(Decidim::Api::Errors::NotFoundError, "Survey not found") end end end diff --git a/decidim-system/app/commands/decidim/system/create_organization.rb b/decidim-system/app/commands/decidim/system/create_organization.rb index f48d3c286e097..ee466d9fece43 100644 --- a/decidim-system/app/commands/decidim/system/create_organization.rb +++ b/decidim-system/app/commands/decidim/system/create_organization.rb @@ -58,6 +58,7 @@ def event_arguments def create_organization Decidim::Organization.create!( name: { form.default_locale => form.name }, + short_name: { form.default_locale => form.short_name }, host: form.host, secondary_hosts: form.clean_secondary_hosts, reference_prefix: form.reference_prefix, diff --git a/decidim-system/app/controllers/decidim/system/api_users_controller.rb b/decidim-system/app/controllers/decidim/system/api_users_controller.rb index 890aefd0e3938..09f2e91a9f9e5 100644 --- a/decidim-system/app/controllers/decidim/system/api_users_controller.rb +++ b/decidim-system/app/controllers/decidim/system/api_users_controller.rb @@ -29,7 +29,7 @@ def update RefreshApiUserSecret.call(api_user, current_admin) do on(:ok) do |secret| flash[:notice] = I18n.t("api_user.refresh.success", scope: "decidim.system", user: api_user.api_key) - session[:api_user] = { id: api_user.id, secret: secret } + session[:api_user] = { id: api_user.id, secret: } redirect_to action: :index end @@ -41,11 +41,11 @@ def update end def create - @form = ::Decidim::System::ApiUserForm.from_params(params.merge!(name: params[:admin][:name], organization: organization)) + @form = ::Decidim::System::ApiUserForm.from_params(params.merge!(name: params[:admin][:name], organization:)) CreateApiUser.call(@form, current_admin) do on(:ok) do |api_user, secret| flash[:notice] = I18n.t("api_user.create.success", scope: "decidim.system", user: api_user.api_key) - session[:api_user] = { id: api_user.id, secret: secret } + session[:api_user] = { id: api_user.id, secret: } redirect_to action: :index end diff --git a/decidim-system/app/forms/decidim/system/base_organization_form.rb b/decidim-system/app/forms/decidim/system/base_organization_form.rb index e9ae695ed5d48..5550fa6ae704e 100644 --- a/decidim-system/app/forms/decidim/system/base_organization_form.rb +++ b/decidim-system/app/forms/decidim/system/base_organization_form.rb @@ -59,9 +59,12 @@ class BaseOrganizationForm < Form jsonb_attribute :omniauth_settings, OMNIATH_PROVIDERS_ATTRIBUTES validates :host, :users_registration_mode, presence: true + validates :users_registration_mode, inclusion: { in: Decidim::Organization.users_registration_modes } + validate :validate_organization_uniqueness + validate :validate_short_name_uniqueness + validate :validate_short_name_format validate :validate_secret_key_base_for_encryption - validates :users_registration_mode, inclusion: { in: Decidim::Organization.users_registration_modes } def map_model(model) self.default_locale = model.default_locale @@ -123,6 +126,14 @@ def validate_secret_key_base_for_encryption def validate_organization_uniqueness raise "#{self.class.name} is expected to implement #validate_organization_uniqueness" end + + def validate_short_name_uniqueness + raise "#{self.class.name} is expected to implement #validate_short_name_uniqueness" + end + + def validate_short_name_format + raise "#{self.class.name} is expected to implement #validate_short_name_format" + end end end end diff --git a/decidim-system/app/forms/decidim/system/register_organization_form.rb b/decidim-system/app/forms/decidim/system/register_organization_form.rb index 169a34db7ad8f..ab099af92e6a3 100644 --- a/decidim-system/app/forms/decidim/system/register_organization_form.rb +++ b/decidim-system/app/forms/decidim/system/register_organization_form.rb @@ -11,6 +11,7 @@ class RegisterOrganizationForm < BaseOrganizationForm mimic :organization attribute :name, String + attribute :short_name, String attribute :organization_admin_email, String attribute :organization_admin_name, String @@ -22,6 +23,7 @@ class RegisterOrganizationForm < BaseOrganizationForm validates :organization_admin_email, :organization_admin_name, :name, :reference_prefix, presence: true validates :name, presence: true + validates :short_name, presence: true validates :available_locales, presence: true validates :default_locale, presence: true validates :default_locale, inclusion: { in: :available_locales } @@ -43,6 +45,28 @@ def validate_organization_uniqueness errors.add(:name, :taken) if organization_names.include?(name&.downcase) errors.add(:host, :taken) if Decidim::Organization.where(host:).where.not(id:).exists? end + + def validate_short_name_uniqueness + base_query = Decidim::Organization.pluck(:short_name) + + organization_short_names = [] + + base_query.each do |value| + organization_short_names += value.except("machine_translations").values + organization_short_names += value.fetch("machine_translations", {}).values + end + + organization_short_names = organization_short_names.map(&:downcase) + + errors.add(:short_name, :taken) if organization_short_names.include?(short_name&.downcase) + end + + def validate_short_name_format + return if short_name.blank? + + errors.add(:short_name, :too_short, count: 3) if short_name.length < 3 + errors.add(:short_name, :too_long, count: 12) if short_name.length > 12 + end end end end diff --git a/decidim-system/app/forms/decidim/system/update_organization_form.rb b/decidim-system/app/forms/decidim/system/update_organization_form.rb index 8a3ba712dfc74..04f1a029ea671 100644 --- a/decidim-system/app/forms/decidim/system/update_organization_form.rb +++ b/decidim-system/app/forms/decidim/system/update_organization_form.rb @@ -8,8 +8,11 @@ module System # class UpdateOrganizationForm < BaseOrganizationForm translatable_attribute :name, String + translatable_attribute :short_name, String validate :validate_organization_name_presence + validate :validate_organization_short_name_presence + validate :validate_short_name_format private @@ -18,6 +21,11 @@ def validate_organization_name_presence errors.add(translated_attr, :blank) if send(translated_attr).blank? end + def validate_organization_short_name_presence + translated_attr = :"short_name_#{current_organization.try(:default_locale) || Decidim.default_locale.to_s}" + errors.add(translated_attr, :blank) if send(translated_attr).blank? + end + def validate_organization_uniqueness base_query = persisted? ? Decidim::Organization.where.not(id:).all : Decidim::Organization.all @@ -33,11 +41,40 @@ def validate_organization_uniqueness name.each do |language, value| next if value.is_a?(Hash) - errors.add("name_#{language}", :taken) if organization_names.include?(value.downcase) + errors.add("name_#{language}", :taken) if organization_names.include?(value&.downcase) end errors.add(:host, :taken) if Decidim::Organization.where(host:).where.not(id:).exists? end + + def validate_short_name_uniqueness + base_query = persisted? ? Decidim::Organization.where.not(id:).all : Decidim::Organization.all + + organization_short_names = [] + + base_query.pluck(:short_name).each do |value| + organization_short_names += value.except("machine_translations").values + organization_short_names += value.fetch("machine_translations", {}).values + end + + organization_short_names = organization_short_names.map(&:downcase).compact_blank + + short_name.each do |language, value| + next if value.is_a?(Hash) + + errors.add("short_name_#{language}", :taken) if organization_short_names.include?(value&.downcase) + end + end + + def validate_short_name_format + short_name.each do |language, value| + next if value.is_a?(Hash) + next if value.blank? + + errors.add("short_name_#{language}", :too_short, count: 3) if value.length < 3 + errors.add("short_name_#{language}", :too_long, count: 12) if value.length > 12 + end + end end end end diff --git a/decidim-system/app/views/decidim/system/organizations/edit.html.erb b/decidim-system/app/views/decidim/system/organizations/edit.html.erb index f44edd1ac0012..da5085c6457e0 100644 --- a/decidim-system/app/views/decidim/system/organizations/edit.html.erb +++ b/decidim-system/app/views/decidim/system/organizations/edit.html.erb @@ -10,6 +10,10 @@ <%= f.translated :text_field, :name, autofocus: true %>
    +
    + <%= f.translated :text_field, :short_name, help_text: t(".short_name_hint") %> +
    + <%= f.text_field :host %> <%= f.text_area :secondary_hosts, help_text: t(".secondary_hosts_hint") %> diff --git a/decidim-system/app/views/decidim/system/organizations/new.html.erb b/decidim-system/app/views/decidim/system/organizations/new.html.erb index 25f19ccf0b457..9ce3a24d0b66e 100644 --- a/decidim-system/app/views/decidim/system/organizations/new.html.erb +++ b/decidim-system/app/views/decidim/system/organizations/new.html.erb @@ -8,6 +8,8 @@
    <%= f.text_field :name, autofocus: true %> + <%= f.text_field :short_name, help_text: t(".short_name_hint") %> + <%= f.text_field :reference_prefix, help_text: t(".reference_prefix_hint") %> <%= f.text_field :host %> diff --git a/decidim-system/config/locales/ca-IT.yml b/decidim-system/config/locales/ca-IT.yml index 8467cdf931bd1..9f4cb6b6fa3a9 100644 --- a/decidim-system/config/locales/ca-IT.yml +++ b/decidim-system/config/locales/ca-IT.yml @@ -264,6 +264,7 @@ ca-IT: confirm_resend_invitation: Segur que vols reenviar la invitació? resend_invitation: Reenviar la invitació secondary_hosts_hint: Introdueix cada un d'ells en una nova línia + short_name_hint: Nom curt que es farà servir per a l'Aplicació Web Progressiva (PWA). Ha de tenir un màxim de 12 caràcters. title: Editar l'organització file_upload_settings: content_types: @@ -293,6 +294,7 @@ ca-IT: organization_admin_email_hint: T'enviarem un correu electrònic a aquesta adreça perquè puguis confirmar-la i configurar la teva contrasenya. reference_prefix_hint: El prefix de la referència s'utilitza per identificar de forma única els recursos del conjunt de l'organització. secondary_hosts_hint: Introdueix cada un d'ells en una nova línia. + short_name_hint: Nom curt que es farà servir per a l'Aplicació Web Progressiva (PWA). Ha de tenir un màxim de 12 caràcters. title: Nova organització omniauth_settings: decidim: diff --git a/decidim-system/config/locales/ca.yml b/decidim-system/config/locales/ca.yml index 59b11b0dc8263..a3d4c07e8bb42 100644 --- a/decidim-system/config/locales/ca.yml +++ b/decidim-system/config/locales/ca.yml @@ -264,6 +264,7 @@ ca: confirm_resend_invitation: Segur que vols reenviar la invitació? resend_invitation: Reenviar la invitació secondary_hosts_hint: Introdueix cada un d'ells en una nova línia + short_name_hint: Nom curt que es farà servir per a l'Aplicació Web Progressiva (PWA). Ha de tenir un màxim de 12 caràcters. title: Editar l'organització file_upload_settings: content_types: @@ -293,6 +294,7 @@ ca: organization_admin_email_hint: T'enviarem un correu electrònic a aquesta adreça perquè puguis confirmar-la i configurar la teva contrasenya. reference_prefix_hint: El prefix de la referència s'utilitza per identificar de forma única els recursos del conjunt de l'organització. secondary_hosts_hint: Introdueix cada un d'ells en una nova línia. + short_name_hint: Nom curt que es farà servir per a l'Aplicació Web Progressiva (PWA). Ha de tenir un màxim de 12 caràcters. title: Nova organització omniauth_settings: decidim: diff --git a/decidim-system/config/locales/en.yml b/decidim-system/config/locales/en.yml index 83a2cebf65400..f474807dc447d 100644 --- a/decidim-system/config/locales/en.yml +++ b/decidim-system/config/locales/en.yml @@ -270,6 +270,7 @@ en: confirm_resend_invitation: Are you sure you want to resend the invitation? resend_invitation: Resend invitation secondary_hosts_hint: Enter each one of them in a new line + short_name_hint: Short name used for the Progressive Web Application. It must have 12 characters maximum. title: Edit organization file_upload_settings: content_types: @@ -299,6 +300,7 @@ en: organization_admin_email_hint: We will send an email to this address so you can confirm it and set up your password. reference_prefix_hint: The reference prefix is used to uniquely identify resources across all organization. secondary_hosts_hint: Enter each one of them in a new line. + short_name_hint: Short name used for the Progressive Web Application. It must have 12 characters maximum. title: New organization omniauth_settings: decidim: diff --git a/decidim-system/config/locales/es-MX.yml b/decidim-system/config/locales/es-MX.yml index 6c1f704353185..1c59c5ae971f5 100644 --- a/decidim-system/config/locales/es-MX.yml +++ b/decidim-system/config/locales/es-MX.yml @@ -265,6 +265,7 @@ es-MX: confirm_resend_invitation: '¿Seguro que quieres reenviar la invitación?' resend_invitation: Reenviar invitación secondary_hosts_hint: Introduce cada uno de ellos en una nueva línea + short_name_hint: Nombre corto utilizado para la Aplicación Web Progresiva (PWA). Debe tener un máximo de 12 caracteres. title: Editar la organización file_upload_settings: content_types: @@ -294,6 +295,7 @@ es-MX: organization_admin_email_hint: Te enviaremos un correo electrónico a esta dirección para que la puedas confirmar y configurar tu contraseña. reference_prefix_hint: El prefijo de referencia se utiliza para identificar de forma única los recursos del conjunto de la organización. secondary_hosts_hint: Introduce cada uno de ellos en una nueva línea. + short_name_hint: Nombre corto utilizado para la Aplicación Web Progresiva (PWA). Debe tener un máximo de 12 caracteres. title: Nueva organización omniauth_settings: decidim: diff --git a/decidim-system/config/locales/es-PY.yml b/decidim-system/config/locales/es-PY.yml index 1e6676bccba3a..b6e4cbcfce52b 100644 --- a/decidim-system/config/locales/es-PY.yml +++ b/decidim-system/config/locales/es-PY.yml @@ -265,6 +265,7 @@ es-PY: confirm_resend_invitation: '¿Seguro que quieres reenviar la invitación?' resend_invitation: Reenviar invitación secondary_hosts_hint: Introduce cada uno de ellos en una nueva línea + short_name_hint: Nombre corto utilizado para la Aplicación Web Progresiva (PWA). Debe tener un máximo de 12 caracteres. title: Editar la organización file_upload_settings: content_types: @@ -294,6 +295,7 @@ es-PY: organization_admin_email_hint: Te enviaremos un correo electrónico a esta dirección para que la puedas confirmar y configurar tu contraseña. reference_prefix_hint: El prefijo de referencia se utiliza para identificar de forma única los recursos del conjunto de la organización. secondary_hosts_hint: Introduce cada uno de ellos en una nueva línea. + short_name_hint: Nombre corto utilizado para la Aplicación Web Progresiva (PWA). Debe tener un máximo de 12 caracteres. title: Nueva organización omniauth_settings: decidim: diff --git a/decidim-system/config/locales/es.yml b/decidim-system/config/locales/es.yml index 4a12d6cc4cb1d..396c5198ebfcc 100644 --- a/decidim-system/config/locales/es.yml +++ b/decidim-system/config/locales/es.yml @@ -265,6 +265,7 @@ es: confirm_resend_invitation: '¿Seguro que quieres reenviar la invitación?' resend_invitation: Reenviar invitación secondary_hosts_hint: Introduce cada uno de ellos en una nueva línea + short_name_hint: Nombre corto utilizado para la Aplicación Web Progresiva (PWA). Debe tener un máximo de 12 caracteres. title: Editar la organización file_upload_settings: content_types: @@ -294,6 +295,7 @@ es: organization_admin_email_hint: Te enviaremos un correo electrónico a esta dirección para que la puedas confirmar y configurar tu contraseña. reference_prefix_hint: El prefijo de referencia se utiliza para identificar de forma única los recursos del conjunto de la organización. secondary_hosts_hint: Introduce cada uno de ellos en una nueva línea. + short_name_hint: Nombre corto utilizado para la Aplicación Web Progresiva (PWA). Debe tener un máximo de 12 caracteres. title: Nueva organización omniauth_settings: decidim: diff --git a/decidim-system/config/locales/eu.yml b/decidim-system/config/locales/eu.yml index 7d014fb663578..b2bd2b44cca50 100644 --- a/decidim-system/config/locales/eu.yml +++ b/decidim-system/config/locales/eu.yml @@ -72,7 +72,7 @@ eu: destroy: success: Administratzailea zuzen ezabatua. edit: - title: Editatu administratzailea + title: Administratzailea editatu update: Eguneratu index: title: Administratzaileak @@ -265,6 +265,7 @@ eu: confirm_resend_invitation: Ziur zaude gonbidapena birbidali nahi duzula? resend_invitation: Birbidali gonbidapena secondary_hosts_hint: Sartu haietako bakoitza lerro berri batean + short_name_hint: Web aplikazio progresiborako erabilitako izen laburra. 12 karaktere izan behar ditu gehienez. title: Editatu erakundea file_upload_settings: content_types: @@ -295,6 +296,7 @@ eu: organization_admin_email_hint: Mezu elektroniko bat bidaliko dugu helbide honetara, baieztatu eta pasahitza jar dezazun. reference_prefix_hint: Erreferentzia-aurrizkia erakunde guztietan baliabideak identifikatzeko erabiltzen da. secondary_hosts_hint: Sartu haietako bakoitza beste lerro batean. + short_name_hint: Web aplikazio progresiborako erabilitako izen laburra. 12 karaktere izan behar ditu gehienez. title: Beste erakunde bat omniauth_settings: decidim: diff --git a/decidim-system/config/locales/fi-plain.yml b/decidim-system/config/locales/fi-plain.yml index 9cde4529a5dda..dd27afda4e6b8 100644 --- a/decidim-system/config/locales/fi-plain.yml +++ b/decidim-system/config/locales/fi-plain.yml @@ -264,6 +264,7 @@ fi-pl: confirm_resend_invitation: Haluatko varmasti lähettää kutsun uudestaan? resend_invitation: Lähetä kutsu uudestaan secondary_hosts_hint: Syötä jokainen niistä omalle rivilleen + short_name_hint: Progressiivisen verkkosovelluksen (PWA) lyhyt nimi. Nimessä voi olla enintään 12 merkkiä. title: Muokkaa organisaatiota file_upload_settings: content_types: @@ -293,6 +294,7 @@ fi-pl: organization_admin_email_hint: Lähetämme tähän sähköpostiosoitteeseen viestin vahvistaaksesi kyseisen sähköpostiosoitteen ja asettaaksesi salasanan käyttäjätilillesi. reference_prefix_hint: Viitetunnisteen avulla tunnistetaan yksilöllisesti resursseja eri organisaatioiden välillä. secondary_hosts_hint: Syötä jokainen niistä omalle rivilleen. + short_name_hint: Progressiivisen verkkosovelluksen (PWA) lyhyt nimi. Nimessä voi olla enintään 12 merkkiä. title: Uusi organisaatio omniauth_settings: decidim: diff --git a/decidim-system/config/locales/fi.yml b/decidim-system/config/locales/fi.yml index 84b7ee32353b8..ab9020a5a11ac 100644 --- a/decidim-system/config/locales/fi.yml +++ b/decidim-system/config/locales/fi.yml @@ -264,6 +264,7 @@ fi: confirm_resend_invitation: Haluatko varmasti lähettää kutsun uudestaan? resend_invitation: Lähetä kutsu uudestaan secondary_hosts_hint: Syötä jokainen niistä omalle rivilleen + short_name_hint: Progressiivisen verkkosovelluksen (PWA) lyhyt nimi. Nimessä voi olla enintään 12 merkkiä. title: Muokkaa organisaatiota file_upload_settings: content_types: @@ -293,6 +294,7 @@ fi: organization_admin_email_hint: Lähetämme tähän sähköpostiosoitteeseen viestin vahvistaaksesi kyseisen sähköpostiosoitteen ja asettaaksesi salasanan käyttäjätilillesi. reference_prefix_hint: Viitetunnisteen avulla tunnistetaan yksilöllisesti resursseja eri organisaatioiden välillä. secondary_hosts_hint: Syötä jokainen niistä omalle rivilleen. + short_name_hint: Progressiivisen verkkosovelluksen (PWA) lyhyt nimi. Nimessä voi olla enintään 12 merkkiä. title: Uusi organisaatio omniauth_settings: decidim: diff --git a/decidim-system/config/locales/fr-CA.yml b/decidim-system/config/locales/fr-CA.yml index 944bdf88204bb..c90769c6bedfb 100644 --- a/decidim-system/config/locales/fr-CA.yml +++ b/decidim-system/config/locales/fr-CA.yml @@ -197,6 +197,7 @@ fr-CA: confirm_resend_invitation: Êtes-vous sûr de vouloir renvoyer l'invitation ? resend_invitation: Renvoyer l'invitation secondary_hosts_hint: Entrez chacun d'eux dans une nouvelle ligne + short_name_hint: Nom court utilisé pour l'application Web progressive. Il doit avoir un maximum de 12 caractères. title: Modifier l'organisation file_upload_settings: content_types: @@ -226,6 +227,7 @@ fr-CA: organization_admin_email_hint: Nous enverrons un e-mail à cette adresse afin que vous puissiez la confirmer et configurer votre mot de passe. reference_prefix_hint: Le préfixe de référence est utilisé pour identifier de manière unique les ressources à travers toutes les organisations. secondary_hosts_hint: Entrez chacun d'eux dans une nouvelle ligne. + short_name_hint: Nom court utilisé pour l'application Web progressive. Il doit avoir un maximum de 12 caractères. title: Nouvelle organisation omniauth_settings: decidim: diff --git a/decidim-system/config/locales/fr.yml b/decidim-system/config/locales/fr.yml index 6052268a4813e..c36f6d4ebae02 100644 --- a/decidim-system/config/locales/fr.yml +++ b/decidim-system/config/locales/fr.yml @@ -197,6 +197,7 @@ fr: confirm_resend_invitation: Êtes-vous sûr de vouloir renvoyer l'invitation ? resend_invitation: Renvoyer l'invitation secondary_hosts_hint: Entrez chacun d'eux dans une nouvelle ligne + short_name_hint: Nom court utilisé pour l'application Web progressive. Il doit avoir un maximum de 12 caractères. title: Modifier l'organisation file_upload_settings: content_types: @@ -226,6 +227,7 @@ fr: organization_admin_email_hint: Nous enverrons un e-mail à cette adresse afin que vous puissiez la confirmer et configurer votre mot de passe. reference_prefix_hint: Le préfixe de référence est utilisé pour identifier de manière unique les ressources à travers toutes les organisations. secondary_hosts_hint: Entrez chacun d'eux dans une nouvelle ligne. + short_name_hint: Nom court utilisé pour l'application Web progressive. Il doit avoir un maximum de 12 caractères. title: Nouvelle organisation omniauth_settings: decidim: diff --git a/decidim-system/config/locales/ja.yml b/decidim-system/config/locales/ja.yml index 1cccb86e4a962..f8fa1c695b988 100644 --- a/decidim-system/config/locales/ja.yml +++ b/decidim-system/config/locales/ja.yml @@ -262,6 +262,7 @@ ja: confirm_resend_invitation: 招待を再送信してもよろしいですか? resend_invitation: 招待を再送信する secondary_hosts_hint: 新しい行にそれぞれ入力してください + short_name_hint: Progressive Web アプリケーションに使用される短い名前。最大 12 文字までです。 title: 組織の編集 file_upload_settings: content_types: @@ -291,6 +292,7 @@ ja: organization_admin_email_hint: このアドレスにメールを送信しますので、ご確認の上、パスワードを設定してください。 reference_prefix_hint: 参照プレフィックスは、すべての組織をまたがってリソースを一意に識別するために使用されます。 secondary_hosts_hint: 新しい行にそれぞれ入力します。 + short_name_hint: Progressive Web アプリケーションに使用される短い名前。最大 12 文字までです。 title: 新しい組織 omniauth_settings: decidim: diff --git a/decidim-system/config/locales/pt-BR.yml b/decidim-system/config/locales/pt-BR.yml index 10c730c8ca8ff..953cd3f12cf31 100644 --- a/decidim-system/config/locales/pt-BR.yml +++ b/decidim-system/config/locales/pt-BR.yml @@ -7,6 +7,9 @@ pt-BR: organization_name: Organização organization_url: URL da organização redirect_uri: Redirecionar URI + refresh_tokens: Atualizar tokens + refresh_tokens_enabled: Atualizar tokens ativados + scopes: Escopos disponíveis organization: address: Nome do host SMTP from_email: Endereço de E-mail @@ -31,17 +34,43 @@ pt-BR: attributes: redirect_uri: must_be_ssl: O URI de redirecionamento deve ser um URI com SSL + organization: + attributes: + password: + secret_key: Você precisa definir a variável de ambiente SECRET_KEY_BASE para ser capaz de salvar este campo decidim: system: actions: + api_user: + hidden_secret: O segredo está oculto + hide_secret: Ocultar Segredo + secret_can_not_be_shown: o segredo de API só pode ser visto ou copiado após a criação + show_secret: Mostrar segredo + shown_secret: O segredo é mostrado + api_users: + create: Criar + new: Novo usuário da API + remove: Remover usuário confirm_destroy: Deseja mesmo excluir isso? + confirm_refresh_api_secret: Tem certeza que deseja atualizar o segredo para este usuário da API? Depois da atualização, o segredo antigo será inválido e não poderá mais ser usado. + confirm_remove_api_user: Você tem certeza que deseja remover este usuário da API? + copied: Copiado + copy_secret: Copiar segredo + copy_secret_clarification: Copiar segredo para área de transferência destroy: Excluir edit: Editar + new_admin: Novo administrador + new_oauth_application: Novo aplicação OAUTH + new_organization: Nova organização + refresh_secret: Atualizar segredo save: Salvar title: Ações admins: create: error: Ocorreu um erro ao criar um novo administrador. + success: Administrador criado com sucesso. + destroy: + success: Administrador excluído com sucesso. edit: title: Edit admin update: Atualizar @@ -52,12 +81,35 @@ pt-BR: title: Novo administrador update: error: Ocorreu um erro ao atualizar este administrador. + success: Administrador atualizado com sucesso. + api_user: + create: + error: Criação de usuário da API falhou. + success: Usuário de API criado com sucesso. O segredo do usuário só é visível após a criação. Por favor, copie e armazene-o com segurança antes de sair desta página! + destroy: + success: Usuário de API excluído com sucesso. + refresh: + error: Algo deu errado! Por favor, tente novamente mais tarde. + success: Segredo atualizado com sucesso. O segredo para o usuário só é visível após a criação. Por favor, copie e armazene-o com segurança antes de sair desta página! + api_users: + index: + explanation_html: | +

    Os usuários da API permitem que os desenvolvedores de integração adicionem contas de usuário de máquina administrativas ao sistema. Esses usuários de máquina podem atuar como administradores na plataforma para realizar atualizações que os administradores normais realizariam por meio do painel de administração. Um caso de uso típico é uma plataforma de gerenciamento externa onde um script automatizado executaria tarefas administrativas na plataforma sem que um usuário real precisasse se autenticar no sistema. Por exemplo, um sistema de automação pode querer enviar respostas a propostas, caso em que precisamos de um usuário de integração para executar essa tarefa dentro desta plataforma.

    +

    A principal diferença entre essas contas e as contas de administrador normais é que esses usuários são destinados ao uso por máquinas e, portanto, seus requisitos de senha são diferentes. Esses usuários não precisam atualizar suas senhas regularmente e a alteração da senha da conta pode ocorrer de forma gerenciada quando necessário.

    +

    Essas credenciais são destinadas ao uso por máquinas. Os aplicativos voltados para o participante devem ser integrados com aplicativos OAuth.

    + manage: Gerenciar usuários da API + new: + select_organization: Selecione sua organização + title: Criar novo usuário de API dashboard: show: + admins: Administradores current_organizations: Organizações atuais + system_checks: Verificações de sistema default_pages: placeholders: content: Adicione conteúdo significativo à página estática %{page} no painel do administrador. + summary: Por favor, adicione um resumo significativo à página estática %{page} no painel do administrador. title: Título padrão para %{page} terms-of-service: Termos de serviço devise: @@ -77,8 +129,9 @@ pt-BR: sign_up: Criar uma conta menu: admins: Administradores + api_credentials: Credenciais de API dashboard: painel de controle - oauth_applications: Aplicações OAuth + oauth_applications: Aplicativos OAuth organizations: Organizações models: admin: @@ -87,6 +140,15 @@ pt-BR: email: E-mail validations: email_uniqueness: outro administrador com o mesmo e-mail já existe + api_user: + fields: + created_at: Criado em + key: Chave + name: Nome + organization: Organização + secret: Segredo + validations: + name_uniqueness: Um usuário de API com este nome já existe. oauth_application: fields: created_at: Criado em @@ -96,6 +158,7 @@ pt-BR: actions: save_and_invite: Criar organização e convidar admin fields: + content_security_policy: Política de segurança de conteúdo created_at: Criado em file_upload_settings: Configurações de upload de arquivo name: Nome @@ -112,6 +175,26 @@ pt-BR: save: Salvar title: Editar aplicativo form: + application_type: + confidential: + explanation: Aplicações que conseguem autenticar de forma segura com o servidor de autorização, por exemplo, conseguindo manter seus clientes registrados seguros. Normalmente um aplicativo executando em um servidor onde o segredo do cliente é armazenado. + name: Confidencial + public: + explanation: Aplicativos que são incapazes de usar segredos de cliente registrados, como aplicativos executados em um navegador ou em um dispositivo móvel. + name: Público + application_type_help_html: 'O tipo de cliente OAuth conforme definido por RFC 6749 Seção 2.1. Os clientes públicos precisam implementar o fluxo PKCE, conforme definido pela RFC 7636. ' + refresh_tokens_help_html: 'Os tokens de atualização são úteis caso o token precise de um tempo de vida mais longo do que o que é atribuído para o token de acesso. Note que os tokens de atualização devem ser usados cuidadosamente porque eles podem enfraquecer a segurança de seus usuários. ' + scopes_explanation: + "api:read": Permite que o usuário realize operações de consulta por meio da API GraphQL. + "api:write": Permite que o usuário execute operações de mutação através da API do GraphQL. + profile: Fornece acesso aos detalhes do próprio perfil do usuário. Apenas este escopo é necessário no caso de a aplicação OAuth ser usada apenas como um provedor de identidade ou um serviço de autenticação. + user: Capacidade de agir como o usuário conectado com o token fornecido durante chamadas para a API do GraphQL. Isto é necessário para todas as operações de api:write ou api:read que precisam de informação sobre o usuário atual. + scopes_help_html: | + Os escopos selecionados estarão disponíveis para os aplicativos conectados solicitarem durante o processo de autorização. O usuário será apresentado detalhes sobre a solicitação de autorização quando ele entrar usando OAuth. +
    + Diferentes escopos fornecem diferentes recursos para o usuário autenticado usando os tokens de acesso concedidos. +
    + Nota: Se você só precisar autenticar os usuários em seus aplicativos externos, você só precisa do escopo do perfil. Aplicações interagindo com a API Decidim também exigem outros escopos. select_organization: Selecione uma organização index: confirm_delete: Tem certeza de que deseja excluir este aplicativo? @@ -129,8 +212,60 @@ pt-BR: create: error: Ocorreu um erro ao criar uma nova organização. error_invitation: Ocorreu um erro ao criar uma nova organização. Revise o nome do administrador da organização. + success_html: | +

    + Organização criada com sucesso. + +

    +
      +
    1. Talvez seja necessário atualizar o código do seu aplicativo, pois para permitir solicitações para %{host} você precisa adicionar o seguinte à sua configuração de ambiente (ou seja, config/environment/production.rb) ou ao seu config/application.rb: +

      config.hosts << "%{host}"

      +
    2. +
    3. + Após isso, você poderá acessar sua plataforma através de http://%{host} +
    4. +
    5. + Enviamos um e-mail para %{email} que você precisa confirmar. +
    6. +
    + csp_settings: + connect_src: Conectar src + connect_src_hint: | + A diretiva connect-src restringe os URLs que podem ser carregados usando elementos