From ed43808131ba686657f6e219a658b192be95e160 Mon Sep 17 00:00:00 2001 From: Quentin Champenois <26109239+Quentinchampenois@users.noreply.github.com> Date: Fri, 22 Nov 2024 11:15:20 +0100 Subject: [PATCH] release: 2.5.0 (#616) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * bump: Bump custom proposal states (#599) * Feat: custom sort for processes (#596) * feat: add custom sort for processesdepending on new variable * feat: update locales files * feat: update seeds * test: add controller tests for assemblies and processes * chore: update i18n config for unused keys * docs: update overrides * refactor: update env variable after review * Fix form initiatives (#600) * fix: XSS vulnerability with img on initiative form and model * test: add tests for new validation * docs: update overrides section * fix: interference from added extends with migration * style: update with rubocop * fix: ActiveRecord::NoDatabaseError * fix: trying to fix again interference * fix: update initiative fomr extends and modify admin initiative controller * refactor: update with rubocop * fix: validation in initiative_form extends and update test * docs: update overrides section in overloads.md * fix: Update OVERLOADS.md --------- Co-authored-by: Quentin Champenois <26109239+Quentinchampenois@users.noreply.github.com> * bump: Decidim-Awesome to last commit (#607) * feat: Bump decidim-awesome to last commit * fix: Fix migration that has been changed since first implementation * fix: Add Referrer-Policy to strict (#613) * fix: Flash message on proposal limit per user reached (#609) * fix error message displaying when reaching proposition add limit * add test * add keys in ignore_missing keys * fix: Add block reported user task (#614) * feat: Add module decidim-cleaner (#597) * feat: Add decidim-cleaner * refactor: Comment env var by default --------- Co-authored-by: Quentin Champenois <26109239+Quentinchampenois@users.noreply.github.com> * Install GuestMeetingRegistration module (#615) * Install GuestMeetingRegistration module * Update registration module * feat: Add Sendethics possibility to the sms gateway (#605) Co-authored-by: Lucie Grau <87868063+luciegrau@users.noreply.github.com> * Bump: Phone authorization handler module (#623) * backport: remove sentry (#622) * backport: Use cdn (#624) * backport: self hosted cdn * backport: update js files content * fix: Missing image in survey question (#621) * fix: update condition to not empty input value if image is present * test: add system test to check for input value * test: update check for image * test: update img check again * test: last update check img * test: update * test: another update * test: if img is present * test: update other test to avoid ambiguous selector error * test: update to see if image is presnet after save * fix: override editor js in decidim_awesome * test: update system test * fix: Questions order in survey export (#618) * chore: update after pull * fix: order questions by position in serializer * test: add test for question order * refactor: update test * style: remove empty line * bump: Guest meeting registration module (#625) * Bump: Guest meeting registration module * refactor(Gemfile): Remove ref reference in Gemfile --------- Co-authored-by: Quentin Champenois * feat: add decypted private body to extra fields (#608) * feat: add new column to proposal extra fields * feat: add callback to proposal extra field model * test: add test for proposal extra field model * feat: add rake task to update existing data * test: add test for new rake task * feat: add proposal extra field model extends to config * chore: update rubocop rules * refactor: update task and test * create the job file * update rake task * lint code * add tests file * fix rspec * clean double specs * update spec * update spec * update syntax test with a context instade of only 'it' * lint code by removing useless line in job spec file * update spec * add more context in spec file * lint code * update rake tasks test --------- Co-authored-by: barbara oliveira Co-authored-by: Lucie Grau <87868063+luciegrau@users.noreply.github.com> Co-authored-by: Quentin Champenois <26109239+Quentinchampenois@users.noreply.github.com> * feat: Clear minio s3 bucket (#612) * feat(Docker): Add minio service * feat(rake): Add new tasks to cleanup s3 bucket * fix: Add S3 purge rake task * fix: S3 Bucket endpoint for docker local * fix(rake): Active storage clear orphans job * fix(sidekiq): Add sidekiq configuration * fix: Logger for active_storage.rake job * fix: Prevent duplicated ActiveRecord Query * fix: Prevent error on PP sort with end_date nil (#626) * fix: update to handle processes without start_date or end_date * fix: update sort in controllers * test: update test with process without end date * refactor: optimize queries in controllers and update tests * refactor: update sort processes in controllers --------- Co-authored-by: Lucie Grau <87868063+luciegrau@users.noreply.github.com> * bump: Module Spam Detection to 4.1.2 (#630) * feat: Author notification on proposal publication (#620) * add notification with eventmanager * base to watch the CI and see files on github * fix translation key & notififaction displaying * add send_pubication_notification to right file * start test rspec * continuing rspec * potential final test file * fix: Merge proposal command and anonymous proposals * fix: Change ProposalPublishedEvent to SimpleEvent * fix: Proposal Published Event * fix: Push FR locales * test: Add specs for proposal_published_event * update methode & file name * adjust trad key * adjust test file & update name of test file * lint * lint fr trad key * lint * update trad key order * lint * correct trad key link in method * correst rspec * fix rspec * fix: Notification small title * lint(rubocop): Fix offenses * ci: Exclude BeEq Rubocop rule * clean * fix CI * delete test file * add ignore trad key in i118n-tasks.yml to fix CI * update text syntaxe via trad key * add '' in fr trad key --------- Co-authored-by: Quentin Champenois * revert: "fix: Flash message on proposal limit per user reached (#609)" (#634) This reverts commit 28003b5d45a89c7233596c4b751b1dabdf7690aa. --------- Co-authored-by: Guillaume MORET <90462045+AyakorK@users.noreply.github.com> Co-authored-by: stephanierousset <61418966+Stef-Rousset@users.noreply.github.com> Co-authored-by: ’Barbara Oliveira <143180473+BarbaraOliveira13@users.noreply.github.com> Co-authored-by: Alexandru Emil Lupu Co-authored-by: Lucie Grau <87868063+luciegrau@users.noreply.github.com> Co-authored-by: barbara oliveira --- .env-example | 28 +- .overloads | 1 - .rubocop_rails.yml | 6 + Gemfile | 9 +- Gemfile.lock | 43 +- OVERLOADS.md | 13 + app.json | 3 +- .../decidim/proposals/publish_proposal.rb | 126 ++ .../assemblies/assemblies_controller.rb | 111 ++ .../admin/initiatives_controller.rb | 2 + .../participatory_processes_controller.rb | 164 +++ .../author_confirmation_proposal_event.rb | 15 + .../decidim/assemblies/assemblies_helper.rb | 99 ++ app/jobs/active_storage_clear_orphans_job.rb | 39 + app/jobs/private_body_decrypt_job.rb | 18 + .../admin/custom_fields_builder.js | 79 ++ .../decidim/decidim_awesome/editors/editor.js | 214 +++ .../forms/custom_fields_renderer.js | 225 ++++ app/packs/src/decidim/editor.js | 152 +++ app/services/decidim/sms_gateway_service.rb | 32 +- .../layouts/decidim/_head_extra.html.erb | 9 - config/application.rb | 8 +- config/i18n-tasks.yml | 4 + config/initializers/awesome_defaults.rb | 10 + config/initializers/sentry.rb | 5 - config/locales/en.yml | 18 + config/locales/fr.yml | 18 + config/secrets.yml | 8 +- config/sidekiq.yml | 16 + config/storage.yml | 2 +- ...d_warning_date_to_users.decidim_cleaner.rb | 8 + ..._private_fields.decidim_decidim_awesome.rb | 30 + ...ate_fields_date.decidim_decidim_awesome.rb | 21 + ...ix_reference_for_all_resources.decidim.rb} | 3 +- ...ed_private_body_to_proposal_extra_field.rb | 11 + ...ings.decidim_guest_meeting_registration.rb | 13 + ...ings.decidim_guest_meeting_registration.rb | 8 + ...ests.decidim_guest_meeting_registration.rb | 18 + ...ests.decidim_guest_meeting_registration.rb | 9 + ...ings.decidim_guest_meeting_registration.rb | 8 + ...ests.decidim_guest_meeting_registration.rb | 8 + ...ests.decidim_guest_meeting_registration.rb | 8 + ...ings.decidim_guest_meeting_registration.rb | 8 + ...ings.decidim_guest_meeting_registration.rb | 11 + db/schema.rb | 45 +- db/seeds.rb | 85 ++ docker-compose.local.yml | 43 +- lib/decidim_app/sentry_setup.rb | 57 - .../initiatives/initiative_form_extends.rb | 19 + .../forms/user_answers_serializer_extend.rb | 14 + .../proposal_extra_field_extends.rb | 18 + lib/tasks/active_storage.rake | 26 + lib/tasks/set_decrypted_private_body.rake | 12 + package-lock.json | 152 +-- package.json | 14 +- .../controllers/assemblies_controller_spec.rb | 211 +++ .../admin/initiatives_controller_spec.rb | 632 +++++++++ ...participatory_processes_controller_spec.rb | 108 ++ spec/factories.rb | 2 +- spec/forms/initiative_form_spec.rb | 52 + spec/jobs/private_body_decrypt_job_spec.rb | 58 + spec/lib/decidim_app/sentry_setup_spec.rb | 129 -- .../set_decrypted_private_body_task_spec.rb | 39 + .../proposal_extra_field_spec.rb | 298 +++++ .../forms/user_answers_serializer_spec.rb | 7 + .../decidim/sms_gateway_service_spec.rb | 226 ++++ ...manages_survey_question_with_image_spec.rb | 54 + ...uestion_in_questionnaire_templates_spec.rb | 22 +- ...dmin_manages_organization_cleaning_spec.rb | 34 + yarn.lock | 1143 +++++++++++------ 70 files changed, 4372 insertions(+), 769 deletions(-) create mode 100644 app/commands/decidim/proposals/publish_proposal.rb create mode 100644 app/controllers/decidim/assemblies/assemblies_controller.rb create mode 100644 app/controllers/decidim/participatory_processes/participatory_processes_controller.rb create mode 100644 app/events/decidim/proposals/author_confirmation_proposal_event.rb create mode 100644 app/helpers/decidim/assemblies/assemblies_helper.rb create mode 100644 app/jobs/active_storage_clear_orphans_job.rb create mode 100644 app/jobs/private_body_decrypt_job.rb create mode 100644 app/packs/src/decidim/decidim_awesome/admin/custom_fields_builder.js create mode 100644 app/packs/src/decidim/decidim_awesome/editors/editor.js create mode 100644 app/packs/src/decidim/decidim_awesome/forms/custom_fields_renderer.js create mode 100644 app/packs/src/decidim/editor.js create mode 100644 config/initializers/awesome_defaults.rb delete mode 100644 config/initializers/sentry.rb create mode 100644 db/migrate/20240926120128_add_warning_date_to_users.decidim_cleaner.rb create mode 100644 db/migrate/20241009105926_add_decidim_awesome_proposal_private_fields.decidim_decidim_awesome.rb create mode 100644 db/migrate/20241009105927_add_decidim_awesome_proposal_private_fields_date.decidim_decidim_awesome.rb rename db/migrate/{20180611126841_fix_reference_for_all_resources.decidim.rb => 20241010092645_fix_reference_for_all_resources.decidim.rb} (90%) create mode 100644 db/migrate/20241011092707_add_decrypted_private_body_to_proposal_extra_field.rb create mode 100644 db/migrate/20241018101341_create_guest_meeting_registration_settings.decidim_guest_meeting_registration.rb create mode 100644 db/migrate/20241018101342_add_confirmation_settings.decidim_guest_meeting_registration.rb create mode 100644 db/migrate/20241018101343_create_guest_meeting_registration_registration_requests.decidim_guest_meeting_registration.rb create mode 100644 db/migrate/20241018101344_add_confirmation_to_register_requests.decidim_guest_meeting_registration.rb create mode 100644 db/migrate/20241018101345_add_cancellation_settings.decidim_guest_meeting_registration.rb create mode 100644 db/migrate/20241018101346_add_cancellation_to_register_requests.decidim_guest_meeting_registration.rb create mode 100644 db/migrate/20241018101347_add_session_token_to_register_requests.decidim_guest_meeting_registration.rb create mode 100644 db/migrate/20241018101348_add_disable_normal_account_to_register_settings.decidim_guest_meeting_registration.rb create mode 100644 db/migrate/20241028094242_add_fields_to_meetings.decidim_guest_meeting_registration.rb delete mode 100644 lib/decidim_app/sentry_setup.rb create mode 100644 lib/extends/forms/decidim/initiatives/initiative_form_extends.rb create mode 100644 lib/extends/models/decidim/decidim_awesome/proposal_extra_field_extends.rb create mode 100644 lib/tasks/active_storage.rake create mode 100644 lib/tasks/set_decrypted_private_body.rake create mode 100644 spec/controllers/assemblies_controller_spec.rb create mode 100644 spec/controllers/decidim/initiatives/admin/initiatives_controller_spec.rb create mode 100644 spec/forms/initiative_form_spec.rb create mode 100644 spec/jobs/private_body_decrypt_job_spec.rb delete mode 100644 spec/lib/decidim_app/sentry_setup_spec.rb create mode 100644 spec/lib/tasks/decidim_app/set_decrypted_private_body_task_spec.rb create mode 100644 spec/models/decidim/decidim_awesome/proposal_extra_field_spec.rb create mode 100644 spec/services/decidim/sms_gateway_service_spec.rb create mode 100644 spec/system/admin/admin_manages_survey_question_with_image_spec.rb create mode 100644 spec/system/admin_manages_organization_cleaning_spec.rb diff --git a/.env-example b/.env-example index 3642eaf4e2..14c215885a 100644 --- a/.env-example +++ b/.env-example @@ -5,7 +5,6 @@ AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= SECRET_KEY_BASE= GEOCODER_LOOKUP_API_KEY= -SENTRY_DSN= HELP_SCOUT_BEACON_ID_MAIN= BACKUP_ENABLED= BACKUP_S3SYNC_ENABLED= @@ -48,8 +47,6 @@ AVAILABLE_LOCALES="fr,en" # Force SSL - binary (default: 1) # Can be disable for reverse proxy setup FORCE_SSL=1 -SENTRY_SIDEKIQ_SAMPLE_RATE=0.1 -SENTRY_SAMPLE_RATE=0.5 FRIENDLY_SIGNUP_OVERRIDE_PASSWORDS=1 FRIENDLY_SIGNUP_INSTANT_VALIDATION=1 FRIENDLY_SIGNUP_HIDE_NICKNAME=1 @@ -93,13 +90,34 @@ RAILS_LOG_LEVEL=warn # SMS_GATEWAY_PASSWORD= ## Set to replace the organization name # SMS_GATEWAY_PLATFORM="hashimoto.local" +## In case you're using Sendethics service +SMS_GATEWAY_MB_API_KEY= +SMS_GATEWAY_MB_ACCOUNT_ID= #Timeout for the unsubscribe link of the newsletter -#NEWSLETTERS_UNSUBSCRIBE_TIMEOUT= +# NEWSLETTERS_UNSUBSCRIBE_TIMEOUT= # Redirect to the TOS page after signup (default: true) # DECIDIM_HALF_SIGNUP_SHOW_TOS_PAGE_AFTER_SIGNUP=true # Automatically save AH metadata to user extended data # Format : comma separated list of auhtorization handler names -# AUTO_EXPORT_AUTHORIZATIONS_DATA_TO_USER_DATA_ENABLED_FOR="authorization1,authorization2" \ No newline at end of file +# AUTO_EXPORT_AUTHORIZATIONS_DATA_TO_USER_DATA_ENABLED_FOR="authorization1,authorization2" + +# Delay until a user is considered inactive and receive a warning email (in days, default: 365) +# DECIDIM_CLEANER_INACTIVE_USERS_MAIL= + +# Delay until a user is deleted after receiving an email (in days, default: 30) +# DECIDIM_CLEANER_DELETE_INACTIVE_USERS= + +# Delay until an admin log is deleted (in days, default: 365) +# DECIDIM_CLEANER_DELETE_ADMIN_LOGS= + +# Delay until user's versions are deleted after the user deletion (in days, default: 30) +# DECIDIM_CLEANER_DELETE_DELETED_USERS_DATA= + +# Delay until deleted authorization's versions are deleted after the authorization creation (in days, default: 30) +# DECIDIM_CLEANER_DELETE_DELETED_AUTHORIZATIONS_DATA= + +# Sort participatory processes by date +SORT_PROCESSES_BY_DATE=false diff --git a/.overloads b/.overloads index 385fe4ffee..3f9b1a6fbb 100644 --- a/.overloads +++ b/.overloads @@ -58,7 +58,6 @@ config/initializers/filter_parameter_logging.rb config/initializers/inflections.rb config/initializers/mime_types.rb config/initializers/new_framework_defaults_5_2.rb -config/initializers/sentry.rb config/initializers/social_share_button.rb config/initializers/wrap_parameters.rb config/locales/decidim-awesome/fr.yml diff --git a/.rubocop_rails.yml b/.rubocop_rails.yml index 68f4c40d64..b0e45aa030 100644 --- a/.rubocop_rails.yml +++ b/.rubocop_rails.yml @@ -89,6 +89,8 @@ Rails/SkipsModelValidations: Enabled: true Exclude: - db/migrate/*.rb + - lib/extends/models/decidim/decidim_awesome/proposal_extra_field_extends.rb + - spec/lib/tasks/decidim_app/set_decrypted_private_body_task_spec.rb Rails/Validation: Include: @@ -107,3 +109,7 @@ RSpec/MultipleMemoizedHelpers: RSpec/AnyInstance: Enabled: false + +RSpec/BeEq: + Exclude: + - spec/events/decidim/proposals/author_confirmation_proposal_event_spec.rb \ No newline at end of file diff --git a/Gemfile b/Gemfile index 84e14123f5..a33f8c0198 100644 --- a/Gemfile +++ b/Gemfile @@ -23,8 +23,9 @@ gem "decidim-anonymous_proposals", DECIDIM_ANONYMOUS_PROPOSALS_VERSION gem "decidim-budget_category_voting", git: "https://github.com/alecslupu-pfa/decidim-budget_category_voting.git", branch: DECIDIM_BRANCH gem "decidim-cache_cleaner" gem "decidim-category_enhanced", "~> 0.0.1" +gem "decidim-cleaner" gem "decidim-custom_proposal_states", git: "https://github.com/alecslupu-pfa/decidim-module-custom_proposal_states", branch: DECIDIM_BRANCH -gem "decidim-decidim_awesome", git: "https://github.com/decidim-ice/decidim-module-decidim_awesome", branch: "main" +gem "decidim-decidim_awesome", git: "https://github.com/decidim-ice/decidim-module-decidim_awesome", branch: DECIDIM_BRANCH gem "decidim-extended_socio_demographic_authorization_handler", git: "https://github.com/OpenSourcePolitics/decidim-module-extended_socio_demographic_authorization_handler.git", branch: DECIDIM_BRANCH gem "decidim-extra_user_fields", git: "https://github.com/OpenSourcePolitics/decidim-module-extra_user_fields.git", branch: "temp/twilio-compatibility-0.27" @@ -34,10 +35,11 @@ gem "decidim-half_signup", git: "https://github.com/OpenSourcePolitics/decidim-m gem "decidim-homepage_interactive_map", git: "https://github.com/OpenSourcePolitics/decidim-module-homepage_interactive_map.git", branch: DECIDIM_BRANCH gem "decidim-phone_authorization_handler", git: "https://github.com/OpenSourcePolitics/decidim-module_phone_authorization_handler", branch: "release/0.27-stable" gem "decidim-simple_proposal", git: "https://github.com/OpenSourcePolitics/decidim-module-simple_proposal", branch: DECIDIM_BRANCH -gem "decidim-spam_detection", git: "https://github.com/OpenSourcePolitics/decidim-spam_detection.git", tag: "4.1.1" +gem "decidim-spam_detection", git: "https://github.com/OpenSourcePolitics/decidim-spam_detection.git", tag: "4.1.2" gem "decidim-survey_multiple_answers", git: "https://github.com/OpenSourcePolitics/decidim-module-survey_multiple_answers" gem "decidim-term_customizer", git: "https://github.com/OpenSourcePolitics/decidim-module-term_customizer.git", branch: "fix/email_with_precompile" +gem "decidim-guest_meeting_registration", git: "https://github.com/alecslupu-pfa/guest-meeting-registration.git", branch: DECIDIM_BRANCH # Omniauth gems gem "omniauth-france_connect", git: "https://github.com/OpenSourcePolitics/omniauth-france_connect" gem "omniauth_openid_connect" @@ -84,9 +86,6 @@ group :production do gem "health_check", "~> 3.1" gem "lograge" gem "sendgrid-ruby" - gem "sentry-rails" - gem "sentry-ruby" - gem "sentry-sidekiq" gem "sidekiq", "~> 6.0" gem "sidekiq-scheduler", "~> 5.0" end diff --git a/Gemfile.lock b/Gemfile.lock index 8477846bb4..f8408c2dbd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -100,7 +100,7 @@ GIT GIT remote: https://github.com/OpenSourcePolitics/decidim-module_phone_authorization_handler - revision: a3e77fb29e9a19793b3ff8b5d2273d41fac0919b + revision: 885122479e7fb9d8294dcf4c4d4f2d34e978b3c6 branch: release/0.27-stable specs: decidim-phone_authorization_handler (1.0.0) @@ -108,10 +108,10 @@ GIT GIT remote: https://github.com/OpenSourcePolitics/decidim-spam_detection.git - revision: fb2ee4624b728ce6f73603bfb84eda1d9b4e04d4 - tag: 4.1.1 + revision: 5e4f92f19b903228b8349fb002d735e900d63ed4 + tag: 4.1.2 specs: - decidim-spam_detection (4.1.1) + decidim-spam_detection (4.1.2) decidim-core (~> 0.27.0) GIT @@ -143,7 +143,7 @@ GIT GIT remote: https://github.com/alecslupu-pfa/decidim-module-custom_proposal_states - revision: 66bc4d1a9f00eb66356e583365597e737e1d6917 + revision: 848eb550d44d9bebc9e72c458c4e3aab79203d9e branch: release/0.27-stable specs: decidim-custom_proposal_states (0.27.5) @@ -151,12 +151,22 @@ GIT decidim-proposals (~> 0.27) deface (>= 1.9) +GIT + remote: https://github.com/alecslupu-pfa/guest-meeting-registration.git + revision: 7b3af0d34d053cc430080e483cd6d1e48dcc0f32 + branch: release/0.27-stable + specs: + decidim-guest_meeting_registration (0.27.7) + decidim-core (~> 0.27) + decidim-meetings (~> 0.27) + deface (>= 1.9) + GIT remote: https://github.com/decidim-ice/decidim-module-decidim_awesome - revision: 058af7db47737e3ca108ac8e08efd5ec55d67a44 - branch: main + revision: b9aae42bc835485edec5887cb02062caaaf64ed1 + branch: release/0.27-stable specs: - decidim-decidim_awesome (0.10.2) + decidim-decidim_awesome (0.10.3) decidim-admin (>= 0.26.0, < 0.28) decidim-core (>= 0.26.0, < 0.28) deface (>= 1.5) @@ -416,6 +426,8 @@ GEM decidim-admin (~> 0.27.0) decidim-core (~> 0.27.0) deface (>= 1.9) + decidim-cleaner (3.1.0) + decidim-core (~> 0.27.0) decidim-comments (0.27.4) decidim-core (= 0.27.4) redcarpet (~> 3.5, >= 3.5.1) @@ -1023,14 +1035,6 @@ GEM semantic_range (3.0.0) sendgrid-ruby (6.7.0) ruby_http_client (~> 3.4) - sentry-rails (5.16.1) - railties (>= 5.0) - sentry-ruby (~> 5.16.1) - sentry-ruby (5.16.1) - concurrent-ruby (~> 1.0, >= 1.0.2) - sentry-sidekiq (5.16.1) - sentry-ruby (~> 5.16.1) - sidekiq (>= 3.0) seven_zip_ruby (1.3.0) sidekiq (6.5.12) connection_pool (>= 2.2.5, < 3) @@ -1170,6 +1174,7 @@ DEPENDENCIES decidim-budgets_booth! decidim-cache_cleaner decidim-category_enhanced (~> 0.0.1) + decidim-cleaner decidim-conferences (~> 0.27.0) decidim-custom_proposal_states! decidim-decidim_awesome! @@ -1178,6 +1183,7 @@ DEPENDENCIES decidim-extra_user_fields! decidim-friendly_signup! decidim-gallery! + decidim-guest_meeting_registration! decidim-half_signup! decidim-homepage_interactive_map! decidim-initiatives (~> 0.27.0) @@ -1208,9 +1214,6 @@ DEPENDENCIES rack-attack (~> 6.6) rubocop-faker sendgrid-ruby - sentry-rails - sentry-ruby - sentry-sidekiq sidekiq (~> 6.0) sidekiq-scheduler (~> 5.0) spring (~> 2.0) @@ -1223,4 +1226,4 @@ RUBY VERSION ruby 3.0.6p216 BUNDLED WITH - 2.4.9 + 2.5.22 diff --git a/OVERLOADS.md b/OVERLOADS.md index 95d0041245..7e9b14d431 100644 --- a/OVERLOADS.md +++ b/OVERLOADS.md @@ -2,6 +2,19 @@ * `app/cells/decidim/version_cell.rb` This override the default `VersionCell` from `decidim-core`, by adding sanitization for `version_number` to prevent XSS attacks. +* `app/controllers/decidim/assemblies/assemblies_controller.rb` +This override the default `AssembliesController` from `decidim-assemblies`, by adding custom sort for assembly_participatory_processes + +* `app/helpers/decidim/assemblies/assemblies_helper.rb` +This override the default `AssembliesHelpler` from `decidim-assemblies`, by adding custom html for sorted assembly_participatory_processes + +* `app/controllers/decidim/participatory_processes/participatory_processes_controller.rb` +This override the default `ParticipatoryProcessesController` from `decidim-participatory_processes`, by adding custom sort for participatory_processes + +## Initiative form +* `lib/extends/forms/decidim/initiatives/initiative_form_extends.rb` +This adds a validation to form's description. + ## Proposal's draft (Decidim awesome overrides 0.26.7) * `app/views/decidim/proposals/collaborative_drafts/_edit_form_fields.html.erb` diff --git a/app.json b/app.json index 1d321aed7b..611b2d2ad6 100644 --- a/app.json +++ b/app.json @@ -7,8 +7,7 @@ "heroku-redis:hobby-dev", "memcachedcloud:30", "newrelic:wayne", - "sendgrid:starter", - "sentry:f1" + "sendgrid:starter" ], "scripts": { "postdeploy":"rake db:schema:load db:migrate" diff --git a/app/commands/decidim/proposals/publish_proposal.rb b/app/commands/decidim/proposals/publish_proposal.rb new file mode 100644 index 0000000000..a637eaed4f --- /dev/null +++ b/app/commands/decidim/proposals/publish_proposal.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +module Decidim + module Proposals + # A command with all the business logic when a user publishes a draft proposal. + class PublishProposal < Decidim::Command + include Decidim::AnonymousProposals::AnonymousBehaviorCommandsConcern + + # Public: Initializes the command. + # + # proposal - The proposal to publish. + # current_user - The current user. + # override: decidim-module-anonymous_proposals/app/commands/decidim/anonymous_proposals/publish_proposal_command_overrides.rb + def initialize(proposal, current_user) + @proposal = proposal + @is_anonymous = allow_anonymous_proposals? && (current_user.blank? || proposal.authored_by?(anonymous_group)) + set_current_user(current_user) + end + + # Executes the command. Broadcasts these events: + # + # - :ok when everything is valid and the proposal is published. + # - :invalid if the proposal's author is not the current user. + # + # Returns nothing. + def call + return broadcast(:invalid) unless @proposal.authored_by?(@current_user) + + transaction do + publish_proposal + increment_scores + send_notification + send_notification_to_participatory_space + send_publication_notification + end + + broadcast(:ok, @proposal) + end + + private + + # This will be the PaperTrail version that is + # shown in the version control feature (1 of 1) + # + # For an attribute to appear in the new version it has to be reset + # and reassigned, as PaperTrail only keeps track of object CHANGES. + def publish_proposal + title = reset(:title) + body = reset(:body) + + Decidim.traceability.perform_action!( + "publish", + @proposal, + @current_user, + visibility: "public-only" + ) do + @proposal.update title: title, body: body, published_at: Time.current + end + end + + # Reset the attribute to an empty string and return the old value + def reset(attribute) + attribute_value = @proposal[attribute] + PaperTrail.request(enabled: false) do + # rubocop:disable Rails/SkipsModelValidations + @proposal.update_attribute attribute, "" + # rubocop:enable Rails/SkipsModelValidations + end + attribute_value + end + + def send_notification + return if @proposal.coauthorships.empty? + + Decidim::EventsManager.publish( + event: "decidim.events.proposals.proposal_published", + event_class: Decidim::Proposals::PublishProposalEvent, + resource: @proposal, + followers: coauthors_followers + ) + end + + def send_publication_notification + Decidim::EventsManager.publish( + event: "decidim.events.proposals.author_confirmation_proposal_event", + event_class: Decidim::Proposals::AuthorConfirmationProposalEvent, + resource: @proposal, + affected_users: [@proposal.creator_identity], + extra: { force_email: true }, + force_send: true + ) + end + + def send_notification_to_participatory_space + Decidim::EventsManager.publish( + event: "decidim.events.proposals.proposal_published", + event_class: Decidim::Proposals::PublishProposalEvent, + resource: @proposal, + followers: @proposal.participatory_space.followers - coauthors_followers, + extra: { + participatory_space: true + } + ) + end + + def coauthors_followers + @coauthors_followers ||= @proposal.authors.flat_map(&:followers) + end + + def increment_scores + @proposal.coauthorships.find_each do |coauthorship| + if coauthorship.user_group + Decidim::Gamification.increment_score(coauthorship.user_group, :proposals) + else + Decidim::Gamification.increment_score(coauthorship.author, :proposals) + end + end + end + + # override: decidim-module-anonymous_proposals/app/commands/decidim/anonymous_proposals/publish_proposal_command_overrides.rb + def component + @component ||= @proposal.component + end + end + end +end diff --git a/app/controllers/decidim/assemblies/assemblies_controller.rb b/app/controllers/decidim/assemblies/assemblies_controller.rb new file mode 100644 index 0000000000..c409bde9a2 --- /dev/null +++ b/app/controllers/decidim/assemblies/assemblies_controller.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +module Decidim + module Assemblies + # A controller that holds the logic to show Assemblies in a public layout. + class AssembliesController < Decidim::Assemblies::ApplicationController + include ParticipatorySpaceContext + participatory_space_layout only: :show + include FilterResource + + helper_method :parent_assemblies, :promoted_assemblies, :stats, :assembly_participatory_processes, :current_assemblies_settings + + def index + enforce_permission_to :list, :assembly + + respond_to do |format| + format.html do + raise ActionController::RoutingError, "Not Found" if published_assemblies.none? + + render "index" + end + + format.js do + raise ActionController::RoutingError, "Not Found" if published_assemblies.none? + + render "index" + end + + format.json do + render json: published_assemblies.query.includes(:children).where(parent: nil).collect { |assembly| + { + name: assembly.title[I18n.locale.to_s], + children: assembly.children.collect do |child| + { + name: child.title[I18n.locale.to_s], + children: child.children.collect { |child_of_child| { name: child_of_child.title[I18n.locale.to_s] } } + } + end + } + } + end + end + end + + def show + enforce_permission_to :read, :assembly, assembly: current_participatory_space + end + + private + + def search_collection + Assembly.where(organization: current_organization).published.visible_for(current_user) + end + + def default_filter_params + { + with_scope: nil, + with_area: nil, + type_id_eq: nil + } + end + + def current_participatory_space + return unless params[:slug] + + @current_participatory_space ||= OrganizationAssemblies.new(current_organization).query.where(slug: params[:slug]).or( + OrganizationAssemblies.new(current_organization).query.where(id: params[:slug]) + ).first! + end + + def published_assemblies + @published_assemblies ||= OrganizationPublishedAssemblies.new(current_organization, current_user) + end + + def promoted_assemblies + @promoted_assemblies ||= published_assemblies | PromotedAssemblies.new + end + + def parent_assemblies + search.result.parent_assemblies.order(weight: :asc, promoted: :desc) + end + + def stats + @stats ||= AssemblyStatsPresenter.new(assembly: current_participatory_space) + end + + def assembly_participatory_processes + if Rails.application.secrets.dig(:decidim, :participatory_processes, :sort_by_date) == false + @assembly_participatory_processes ||= @current_participatory_space.linked_participatory_space_resources(:participatory_processes, "included_participatory_processes") + else + @assembly_participatory_processes = @current_participatory_space.linked_participatory_space_resources(:participatory_processes, "included_participatory_processes") + @active_processes ||= @assembly_participatory_processes.select(&:active?) + sorted_by_date = { + active: @active_processes.reject { |process| process.end_date.nil? }.sort_by(&:end_date) + processes_without_end_date(@active_processes), + future: @assembly_participatory_processes.upcoming.sort_by(&:start_date), + past: @assembly_participatory_processes.past.sort_by(&:end_date).reverse + } + @assembly_participatory_processes = sorted_by_date + end + end + + def current_assemblies_settings + @current_assemblies_settings ||= Decidim::AssembliesSetting.find_or_create_by(decidim_organization_id: current_organization.id) + end + + def processes_without_end_date(processes) + processes.select { |process| process.end_date.nil? } + end + end + end +end diff --git a/app/controllers/decidim/initiatives/admin/initiatives_controller.rb b/app/controllers/decidim/initiatives/admin/initiatives_controller.rb index 06fbfba3e2..2f4a2a6ec8 100644 --- a/app/controllers/decidim/initiatives/admin/initiatives_controller.rb +++ b/app/controllers/decidim/initiatives/admin/initiatives_controller.rb @@ -34,6 +34,8 @@ def edit initiative: current_initiative ) @form.attachment = form_attachment_model + # "sanitize" the translated description, if the value is a hash (for machine_translation key) we don't modify it + @form.description.transform_values! { |v| v.instance_of?(String) ? v.gsub(/on\w+=("|')/, "nothing") : v } render layout: "decidim/admin/initiative" end diff --git a/app/controllers/decidim/participatory_processes/participatory_processes_controller.rb b/app/controllers/decidim/participatory_processes/participatory_processes_controller.rb new file mode 100644 index 0000000000..72b7cf0f25 --- /dev/null +++ b/app/controllers/decidim/participatory_processes/participatory_processes_controller.rb @@ -0,0 +1,164 @@ +# frozen_string_literal: true + +module Decidim + module ParticipatoryProcesses + # A controller that holds the logic to show ParticipatoryProcesses in a + # public layout. + class ParticipatoryProcessesController < Decidim::ParticipatoryProcesses::ApplicationController + include ParticipatorySpaceContext + participatory_space_layout only: [:show, :all_metrics] + include FilterResource + + helper_method :collection, + :promoted_collection, + :participatory_processes, + :stats, + :metrics, + :participatory_process_group, + :default_date_filter, + :related_processes, + :linked_assemblies + + def index + raise ActionController::RoutingError, "Not Found" if published_processes.none? + + enforce_permission_to :list, :process + enforce_permission_to :list, :process_group + end + + def show + enforce_permission_to :read, :process, process: current_participatory_space + end + + def all_metrics + if current_participatory_space.show_statistics + enforce_permission_to :read, :process, process: current_participatory_space + else + render status: :not_found + end + end + + private + + def search_collection + ParticipatoryProcess.where(organization: current_organization).published.visible_for(current_user).includes(:area) + end + + def default_filter_params + { + with_scope: nil, + with_area: nil, + with_type: nil, + with_date: default_date_filter + } + end + + def organization_participatory_processes + @organization_participatory_processes ||= OrganizationParticipatoryProcesses.new(current_organization).query + end + + def current_participatory_space + return unless params["slug"] + + @current_participatory_space ||= organization_participatory_processes.where(slug: params["slug"]).or( + organization_participatory_processes.where(id: params["slug"]) + ).first! + end + + def published_processes + @published_processes ||= OrganizationPublishedParticipatoryProcesses.new(current_organization, current_user) + end + + def promoted_participatory_processes + @promoted_participatory_processes ||= published_processes | PromotedParticipatoryProcesses.new + end + + def promoted_participatory_process_groups + @promoted_participatory_process_groups ||= OrganizationPromotedParticipatoryProcessGroups.new(current_organization) + end + + def promoted_collection + @promoted_collection ||= promoted_participatory_processes.query + promoted_participatory_process_groups.query + end + + def collection + @collection ||= participatory_processes + participatory_process_groups + end + + def filtered_processes + search.result + end + + def participatory_processes + @participatory_processes ||= filtered_processes.groupless.includes(attachments: :file_attachment) + return @participatory_processes if Rails.application.secrets.dig(:decidim, :participatory_processes, :sort_by_date) == false + + custom_sort(search.with_date) + end + + def participatory_process_groups + @participatory_process_groups ||= OrganizationParticipatoryProcessGroups.new(current_organization).query + .where(id: filtered_processes.grouped.group_ids) + end + + def stats + @stats ||= ParticipatoryProcessStatsPresenter.new(participatory_process: current_participatory_space) + end + + def metrics + @metrics ||= ParticipatoryProcessMetricChartsPresenter.new(participatory_process: current_participatory_space, view_context: view_context) + end + + def participatory_process_group + @participatory_process_group ||= current_participatory_space.participatory_process_group + end + + def default_date_filter + return "active" if published_processes.any?(&:active?) + return "upcoming" if published_processes.any?(&:upcoming?) + return "past" if published_processes.any?(&:past?) + + "all" + end + + def related_processes + @related_processes ||= + current_participatory_space + .linked_participatory_space_resources(:participatory_processes, "related_processes") + .published + .all + end + + def linked_assemblies + @linked_assemblies ||= current_participatory_space.linked_participatory_space_resources(:assembly, "included_participatory_processes").public_spaces + end + + def custom_sort(date) + case date + when "active" + @participatory_processes.reject { |process| process.end_date.nil? }.sort_by(&:end_date) + processes_without_end_date(@participatory_processes) + when "past" + @participatory_processes.sort_by(&:end_date).reverse + when "upcoming" + @participatory_processes.sort_by(&:start_date) + when "all" + @participatory_processes = sort_all_processes + else + @participatory_processes + end + end + + def sort_all_processes + @actives_processes ||= @participatory_processes.select(&:active?) + actives = @actives_processes.reject { |process| process.end_date.nil? }.sort_by(&:end_date) + processes_without_end_date(@actives_processes) + pasts = @participatory_processes.select(&:past?).sort_by(&:end_date).reverse + upcomings = @participatory_processes.select(&:upcoming?).sort_by(&:start_date) + (actives + upcomings + pasts) + end + + def processes_without_end_date(processes) + processes.select { |process| process.end_date.nil? } + end + end + end +end diff --git a/app/events/decidim/proposals/author_confirmation_proposal_event.rb b/app/events/decidim/proposals/author_confirmation_proposal_event.rb new file mode 100644 index 0000000000..22745650a4 --- /dev/null +++ b/app/events/decidim/proposals/author_confirmation_proposal_event.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Decidim + module Proposals + class AuthorConfirmationProposalEvent < Decidim::Events::SimpleEvent + def self.model_name + ActiveModel::Name.new(self, nil, I18n.t("decidim.events.proposals.author_confirmation_proposal_event.email_subject")) + end + + def resource_title + translated_attribute(resource.title) + end + end + end +end diff --git a/app/helpers/decidim/assemblies/assemblies_helper.rb b/app/helpers/decidim/assemblies/assemblies_helper.rb new file mode 100644 index 0000000000..4da2bb6aa9 --- /dev/null +++ b/app/helpers/decidim/assemblies/assemblies_helper.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +module Decidim + module Assemblies + # Helpers related to the Assemblies layout. + module AssembliesHelper + include Decidim::ResourceHelper + include Decidim::AttachmentsHelper + include Decidim::IconHelper + include Decidim::WidgetUrlsHelper + include Decidim::SanitizeHelper + include Decidim::ResourceReferenceHelper + include Decidim::FiltersHelper + include FilterAssembliesHelper + + # Public: Returns the characteristics of an assembly in a readable format like + # "title: close, no public, no transparent and is restricted to the members of the assembly" + def participatory_processes_for_assembly(assembly_participatory_processes) + if Rails.application.secrets.dig(:decidim, :participatory_processes, :sort_by_date) == true + sorted_participatory_processes_for_assembly(assembly_participatory_processes) + else + html = "" + html += %(
).html_safe + html += %(

#{t("assemblies.show.related_participatory_processes", scope: "decidim")}

).html_safe + html += %(
).html_safe + assembly_participatory_processes.each do |assembly_participatory_process| + html += render partial: "decidim/participatory_processes/participatory_process", locals: { participatory_process: assembly_participatory_process } + end + html += %(
).html_safe + html += %(
).html_safe + html.html_safe + end + end + + def sorted_participatory_processes_for_assembly(assembly_participatory_processes) + return if assembly_participatory_processes.values.all?(&:empty?) + + html = "" + html += %(
).html_safe + html += %(

#{t("assemblies.show.related_participatory_processes", scope: "decidim")} ).html_safe + + assembly_participatory_processes.each do |type, processes| + next if processes.empty? + + html += %( + #{t("assemblies.show.#{type}_assembly_participatory_processes_mini", scope: "decidim")} + (#{processes.count}) ).html_safe + end + + html += %(

).html_safe + html += %(
).html_safe + + assembly_participatory_processes.each do |type, processes| + next if processes.empty? + + html += %(
+ #{t("assemblies.show.#{type}_assembly_participatory_processes", scope: "decidim")}
).html_safe + html += %(
).html_safe + processes.each do |process| + html += render partial: "decidim/participatory_processes/participatory_process", locals: { participatory_process: process } + end + html += %(
).html_safe + end + html += %(
).html_safe + html += %(
).html_safe + html.html_safe + end + + def assembly_features(assembly) + html = "".html_safe + html += "#{translated_attribute(assembly.title)}: ".html_safe + html += t("assemblies.show.private_space", scope: "decidim").to_s.html_safe + html += ", #{t("assemblies.show.is_transparent.#{assembly.is_transparent}", scope: "decidim")}".html_safe if assembly.is_transparent? + html += " #{decidim_sanitize_editor translated_attribute(assembly.special_features)}".html_safe + html.html_safe + end + + def social_handler_links(assembly) + html = "".html_safe + if Decidim::Assembly::SOCIAL_HANDLERS.any? { |h| assembly.try("#{h}_handler").present? } + html += "".html_safe + end + + html.html_safe + end + end + end +end diff --git a/app/jobs/active_storage_clear_orphans_job.rb b/app/jobs/active_storage_clear_orphans_job.rb new file mode 100644 index 0000000000..efa7554111 --- /dev/null +++ b/app/jobs/active_storage_clear_orphans_job.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +class ActiveStorageClearOrphansJob < ApplicationJob + include ActionView::Helpers::NumberHelper + queue_as :default + + def perform(**args) + limit = args[:limit] || 10_000 + Rails.logger.info "Looking for orphan blobs in S3... (limit: #{limit})" + objects = ActiveStorage::Blob.service.bucket.objects + Rails.logger.info "Total files: #{objects.size}" + + current_iteration = 0 + sum = 0 + orphans_count = 0 + objects.each do |obj| + break if current_iteration >= limit + + current_iteration += 1 + next if ActiveStorage::Blob.exists?(key: obj.key) + + sum += delete_object(obj) + orphans_count += 1 + end + + Rails.logger.info "Size: #{number_to_human_size(sum)} in #{orphans_count} files" + Rails.logger.info "Configuration limit is #{limit} files" + Rails.logger.info "Terminated task... " + end + + private + + def delete_object(obj) + Rails.logger.info "Removing orphan: #{obj.key}" + size = obj.size + obj.delete + size + end +end diff --git a/app/jobs/private_body_decrypt_job.rb b/app/jobs/private_body_decrypt_job.rb new file mode 100644 index 0000000000..275f8bf62f --- /dev/null +++ b/app/jobs/private_body_decrypt_job.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class PrivateBodyDecryptJob < ApplicationJob + queue_as :default + + def perform + extra_fields = Decidim::DecidimAwesome::ProposalExtraField.where(decrypted_private_body: nil).where.not(private_body: nil) + return unless extra_fields.any? + + Rails.logger.info "Extra fields to update: #{extra_fields.size}" + count = 0 + extra_fields.find_each do |extra_field| + extra_field.update(decrypted_private_body: extra_field.private_body.to_s) + count += 1 if extra_field.decrypted_private_body_previous_change.present? + end + Rails.logger.info "Extra fields updated: #{count}" + end +end diff --git a/app/packs/src/decidim/decidim_awesome/admin/custom_fields_builder.js b/app/packs/src/decidim/decidim_awesome/admin/custom_fields_builder.js new file mode 100644 index 0000000000..c426e9645a --- /dev/null +++ b/app/packs/src/decidim/decidim_awesome/admin/custom_fields_builder.js @@ -0,0 +1,79 @@ +require("formBuilder/dist/form-builder.min.js") +import "src/decidim/decidim_awesome/forms/rich_text_plugin" + +window.CustomFieldsBuilders = window.CustomFieldsBuilders || []; + +$(() => { + $(".awesome-edit-config .proposal_custom_fields_editor").each((_idx, el) => { + const key = $(el).closest(".proposal_custom_fields_container").data("key"); + const configVar = $(el).closest(".proposal_custom_fields_container").data("var"); + // DOCS: https://formbuilder.online/docs + window.CustomFieldsBuilders.push({ + el: el, + key: key, + var: configVar, + config: { + i18n: { + locale: "fr-FR", + location: "https://decidim.storage.opensourcepolitics.eu/osp-cdn/form_builder/1.1.0" + }, + formData: $(`input[name="config[${configVar}][${key}]"]`).val(), + disableFields: ["button", "file"], + disabledActionButtons: ["save", "data", "clear"], + disabledAttrs: [ + "access", + "inline", + "className" + ], + controlOrder: [ + "text", + "textarea", + "number", + "date", + "checkbox-group", + "radio-group", + "select", + "autocomplete", + "header", + "paragraph" + ], + disabledSubtypes: { + // default color as it generate hashtags in decidim (TODO: fix hashtag generator with this) + text: ["color"], + // disable default wysiwyg editors as they present problems + textarea: ["tinymce", "quill"] + } + }, + instance: null + }); + }); + + $(document).on("formBuilder.create", (_event, idx, list) => { + if (!list[idx]) { + return; + } + + $(list[idx].el).formBuilder(list[idx].config).promise.then(function(res) { + list[idx].instance = res; + // Attach to DOM + list[idx].el.FormBuilder = res; + // remove spinner + $(list[idx].el).find(".loading-spinner").remove(); + // for external use + $(document).trigger("formBuilder.created", [list[idx]]); + if (idx < list.length) { + $(document).trigger("formBuilder.create", [idx + 1, list]); + } + }); + }); + + if (window.CustomFieldsBuilders.length) { + $(document).trigger("formBuilder.create", [0, window.CustomFieldsBuilders]); + } + + $("form.awesome-edit-config").on("submit", () => { + window.CustomFieldsBuilders.forEach((builder) => { + $(`input[name="config[${builder.var}][${builder.key}]"]`).val(builder.instance.actions.getData("json")); + }); + }); +}); diff --git a/app/packs/src/decidim/decidim_awesome/editors/editor.js b/app/packs/src/decidim/decidim_awesome/editors/editor.js new file mode 100644 index 0000000000..7bec3c9396 --- /dev/null +++ b/app/packs/src/decidim/decidim_awesome/editors/editor.js @@ -0,0 +1,214 @@ +/* eslint-disable require-jsdoc, func-style */ + +/* +* Since version 0.25 we follow a different strategy and opt to destroy and override completely the original editor +* That's because editors are instantiated directly instead of creating a global function to instantiate them +*/ + +import lineBreakButtonHandler from "src/decidim/editor/linebreak_module" +import InscrybMDE from "inscrybmde" +import Europa from "europa" +import "inline-attachment/src/inline-attachment"; +import "inline-attachment/src/codemirror-4.inline-attachment"; +import "inline-attachment/src/jquery.inline-attachment"; +import hljs from "highlight.js"; +import "highlight.js/styles/github.css"; +import "src/decidim/editor/clipboard_override" +import "src/decidim/vendor/image-resize.min" +import "src/decidim/vendor/image-upload.min" +import { marked } from "marked"; + +const DecidimAwesome = window.DecidimAwesome || {}; +const quillFormats = ["bold", "italic", "link", "underline", "header", "list", "video", "image", "alt", "break", "width", "style", "code", "blockquote", "indent"]; + +// A tricky way to destroy the quill editor +export function destroyQuillEditor(container) { + if (container) { + const content = $(container).find(".ql-editor").html(); + $(container).html(content); + $(container).siblings(".ql-toolbar").remove(); + $(container).find("*[class*='ql-']").removeClass((index, className) => (className.match(/(^|\s)ql-\S+/g) || []).join(" ")); + $(container).removeClass((index, className) => (className.match(/(^|\s)ql-\S+/g) || []).join(" ")); + if ($(container).next().is("p.help-text")) { + $(container).next().remove(); + } + } + else { + console.error(`editor [${container}] not exists`); + } +} + +export function createQuillEditor(container) { + const toolbar = $(container).data("toolbar"); + const disabled = $(container).data("disabled"); + const allowedEmptyContentSelector = "iframe,img"; + + let quillToolbar = [ + ["bold", "italic", "underline", "linebreak"], + [{ list: "ordered" }, { list: "bullet" }], + ["link", "clean"], + ["code", "blockquote"], + [{ "indent": "-1"}, { "indent": "+1" }] + ]; + + let addImage = false; + + if (toolbar === "full") { + quillToolbar = [ + [{ header: [2, 3, 4, 5, 6, false] }], + ...quillToolbar + ]; + if (DecidimAwesome.allow_images_in_full_editor) { + quillToolbar.push(["video", "image"]); + addImage = true; + } else { + quillToolbar.push(["video"]); + } + } else if (toolbar === "basic") { + if (DecidimAwesome.allow_images_in_small_editor) { + quillToolbar.push(["video", "image"]); + addImage = true; + } else { + quillToolbar.push(["video"]); + } + } else if (DecidimAwesome.allow_images_in_small_editor) { + quillToolbar.push(["image"]); + addImage = true; + } + + let modules = { + linebreak: {}, + toolbar: { + container: quillToolbar, + handlers: { + "linebreak": lineBreakButtonHandler + } + } + }; + + const $input = $(container).siblings('input[type="hidden"]'); + container.innerHTML = $input.val() || ""; + const token = $('meta[name="csrf-token"]').attr("content"); + if (addImage) { + modules.imageResize = { + modules: ["Resize", "DisplaySize"] + } + modules.imageUpload = { + url: DecidimAwesome.editor_uploader_path, + method: "POST", + name: "image", + withCredentials: false, + headers: { "X-CSRF-Token": token }, + callbackOK: (serverResponse, next) => { + $("div.ql-toolbar").last().removeClass("editor-loading") + next(serverResponse.url); + }, + callbackKO: (serverError) => { + $("div.ql-toolbar").last().removeClass("editor-loading") + let msg = serverError && serverError.body; + try { + msg = JSON.parse(msg).message; + } catch (evt) { console.error("Parsing error", evt); } + console.error(`Image upload error: ${msg}`); + let $p = $(`

${msg}

`); + $(container).after($p) + setTimeout(() => { + $p.fadeOut(1000, () => { + $p.destroy(); + }); + }, 3000); + }, + checkBeforeSend: (file, next) => { + $("div.ql-toolbar").last().addClass("editor-loading") + next(file); + } + } + } + const quill = new Quill(container, { + modules: modules, + formats: quillFormats, + theme: "snow" + }); + + if (disabled) { + quill.disable(); + } + + quill.on("text-change", () => { + const text = quill.getText(); + + // Triggers CustomEvent with the cursor position + // It is required in input_mentions.js + let event = new CustomEvent("quill-position", { + detail: quill.getSelection() + }); + container.dispatchEvent(event); + + if ((text === "\n" || text === "\n\n") && quill.root.querySelectorAll(allowedEmptyContentSelector).length === 0 + && !$input.val().match(/img/)) { + $input.val(""); + } else { + const emptyParagraph = "


"; + const cleanHTML = quill.root.innerHTML.replace( + new RegExp(`^${emptyParagraph}|${emptyParagraph}$`, "g"), + "" + ); + $input.val(cleanHTML); + } + }); + // After editor is ready, linebreak_module deletes two extraneous new lines + quill.emitter.emit("editor-ready"); + + if (addImage) { + const text = $(container).data("dragAndDropHelpText") || DecidimAwesome.texts.drag_and_drop_image; + $(container).after(`

${text}

`); + } + + // After editor is ready, linebreak_module deletes two extraneous new lines + quill.emitter.emit("editor-ready"); + + return quill; +} + +export function createMarkdownEditor(container) { + const text = DecidimAwesome.texts.drag_and_drop_image; + const token = $('meta[name="csrf-token"]').attr("content"); + const $input = $(container).siblings('input[type="hidden"]'); + const $faker = $('