From 870ade4936bc1eadddd454f7cf80894eb48703ef Mon Sep 17 00:00:00 2001 From: AyakorK Date: Fri, 12 Apr 2024 11:32:11 +0200 Subject: [PATCH] fix: Replace parse with parse_with_processor to fix the issue --- config/application.rb | 4 + .../meetings/admin/create_meeting_extends.rb | 69 ++++ .../meetings/admin/update_meeting_extends.rb | 61 ++++ .../meetings/create_meeting_extends.rb | 68 ++++ .../meetings/update_meeting_extends.rb | 60 ++++ .../meetings/admin/create_meeting_spec.rb | 192 +++++++++++ .../meetings/admin/update_meeting_spec.rb | 299 ++++++++++++++++++ spec/commands/meetings/create_meeting_spec.rb | 231 ++++++++++++++ spec/commands/meetings/update_meeting_spec.rb | 263 +++++++++++++++ 9 files changed, 1247 insertions(+) create mode 100644 lib/extends/commands/decidim/meetings/admin/create_meeting_extends.rb create mode 100644 lib/extends/commands/decidim/meetings/admin/update_meeting_extends.rb create mode 100644 lib/extends/commands/decidim/meetings/create_meeting_extends.rb create mode 100644 lib/extends/commands/decidim/meetings/update_meeting_extends.rb create mode 100644 spec/commands/meetings/admin/create_meeting_spec.rb create mode 100644 spec/commands/meetings/admin/update_meeting_spec.rb create mode 100644 spec/commands/meetings/create_meeting_spec.rb create mode 100644 spec/commands/meetings/update_meeting_spec.rb diff --git a/config/application.rb b/config/application.rb index db1a953f..4bdee961 100644 --- a/config/application.rb +++ b/config/application.rb @@ -48,6 +48,10 @@ class Application < Rails::Application config.after_initialize do require "extends/controllers/decidim/devise/sessions_controller_extends" require "extends/lib/decidim/dependency_resolver_extends" + require "extends/commands/decidim/meetings/admin/update_meeting_extends" + require "extends/commands/decidim/meetings/admin/create_meeting_extends" + require "extends/commands/decidim/meetings/update_meeting_extends" + require "extends/commands/decidim/meetings/create_meeting_extends" Decidim::GraphiQL::Rails.config.tap do |config| config.initial_query = "{\n deployment {\n version\n branch\n remote\n upToDate\n currentCommit\n latestCommit\n locallyModified\n }\n}".html_safe diff --git a/lib/extends/commands/decidim/meetings/admin/create_meeting_extends.rb b/lib/extends/commands/decidim/meetings/admin/create_meeting_extends.rb new file mode 100644 index 00000000..f0626da8 --- /dev/null +++ b/lib/extends/commands/decidim/meetings/admin/create_meeting_extends.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +module Decidim + module Meetings + module Admin + module CreateMeetingExtends + def call + return broadcast(:invalid) if @form.invalid? + + transaction do + create_meeting! + create_services! + end + + create_follow_form_resource(form.current_user) + broadcast(:ok, meeting) + end + + private + + def create_meeting! + parsed_title = Decidim::ContentProcessor.parse_with_processor(:hashtag, form.title, current_organization: form.current_organization).rewrite + parsed_description = Decidim::ContentProcessor.parse_with_processor(:hashtag, form.description, current_organization: form.current_organization).rewrite + + params = { + scope: form.scope, + category: form.category, + title: parsed_title, + description: parsed_description, + end_time: form.end_time, + start_time: form.start_time, + online_meeting_url: form.online_meeting_url, + registration_type: form.registration_type, + registration_url: form.registration_url, + type_of_meeting: form.clean_type_of_meeting, + address: form.address, + latitude: form.latitude, + longitude: form.longitude, + location: form.location, + location_hints: form.location_hints, + private_meeting: form.private_meeting, + transparent: form.transparent, + author: form.current_organization, + registration_terms: form.current_component.settings.default_registration_terms, + registrations_enabled: form.registrations_enabled, + component: form.current_component, + questionnaire: Decidim::Forms::Questionnaire.new, + iframe_embed_type: form.iframe_embed_type, + comments_enabled: form.comments_enabled, + comments_start_time: form.comments_start_time, + comments_end_time: form.comments_end_time, + iframe_access_level: form.iframe_access_level + } + + @meeting = Decidim.traceability.create!( + Decidim::Meetings::Meeting, + form.current_user, + params, + visibility: "all" + ) + end + end + end + end +end + +Decidim::Meetings::Admin::CreateMeeting.class_eval do + prepend Decidim::Meetings::Admin::CreateMeetingExtends +end diff --git a/lib/extends/commands/decidim/meetings/admin/update_meeting_extends.rb b/lib/extends/commands/decidim/meetings/admin/update_meeting_extends.rb new file mode 100644 index 00000000..a89550f3 --- /dev/null +++ b/lib/extends/commands/decidim/meetings/admin/update_meeting_extends.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module Decidim + module Meetings + module Admin + module UpdateMeetingExtends + def call + return broadcast(:invalid) if form.invalid? + + transaction do + update_meeting! + send_notification if should_notify_followers? + schedule_upcoming_meeting_notification if meeting.published? && start_time_changed? + update_services! + end + + broadcast(:ok, meeting) + end + + private + + def update_meeting! + parsed_title = Decidim::ContentProcessor.parse_with_processor(:hashtag, form.title, current_organization: form.current_organization).rewrite + parsed_description = Decidim::ContentProcessor.parse_with_processor(:hashtag, form.description, current_organization: form.current_organization).rewrite + + Decidim.traceability.update!( + meeting, + form.current_user, + scope: form.scope, + category: form.category, + title: parsed_title, + description: parsed_description, + end_time: form.end_time, + start_time: form.start_time, + online_meeting_url: form.online_meeting_url, + registration_type: form.registration_type, + registration_url: form.registration_url, + registrations_enabled: form.registrations_enabled, + type_of_meeting: form.clean_type_of_meeting, + address: form.address, + latitude: form.latitude, + longitude: form.longitude, + location: form.location, + location_hints: form.location_hints, + private_meeting: form.private_meeting, + transparent: form.transparent, + iframe_embed_type: form.iframe_embed_type, + comments_enabled: form.comments_enabled, + comments_start_time: form.comments_start_time, + comments_end_time: form.comments_end_time, + iframe_access_level: form.iframe_access_level + ) + end + end + end + end +end + +Decidim::Meetings::Admin::UpdateMeeting.class_eval do + prepend Decidim::Meetings::Admin::UpdateMeetingExtends +end diff --git a/lib/extends/commands/decidim/meetings/create_meeting_extends.rb b/lib/extends/commands/decidim/meetings/create_meeting_extends.rb new file mode 100644 index 00000000..1dcd9e39 --- /dev/null +++ b/lib/extends/commands/decidim/meetings/create_meeting_extends.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module Decidim + module Meetings + module CreateMeetingExtends + def call + return broadcast(:invalid) if form.invalid? + + transaction do + create_meeting! + schedule_upcoming_meeting_notification + send_notification + end + + create_follow_form_resource(form.current_user) + broadcast(:ok, meeting) + end + + private + + def create_meeting! + parsed_title = Decidim::ContentProcessor.parse_with_processor(:hashtag, form.title, current_organization: form.current_organization).rewrite + parsed_description = Decidim::ContentProcessor.parse_with_processor(:hashtag, form.description, current_organization: form.current_organization).rewrite + + params = { + scope: form.scope, + category: form.category, + title: { I18n.locale => parsed_title }, + description: { I18n.locale => parsed_description }, + end_time: form.end_time, + start_time: form.start_time, + address: form.address, + latitude: form.latitude, + longitude: form.longitude, + location: { I18n.locale => form.location }, + location_hints: { I18n.locale => form.location_hints }, + author: form.current_user, + decidim_user_group_id: form.user_group_id, + online_meeting_url: form.online_meeting_url, + registration_type: form.registration_type, + registration_url: form.registration_url, + available_slots: form.available_slots, + registration_terms: { I18n.locale => form.registration_terms }, + registrations_enabled: form.registrations_enabled, + type_of_meeting: form.clean_type_of_meeting, + component: form.current_component, + published_at: Time.current, + iframe_embed_type: form.iframe_embed_type, + iframe_access_level: form.iframe_access_level + } + + @meeting = Decidim.traceability.create!( + Meeting, + form.current_user, + params, + visibility: "public-only" + ) + Decidim.traceability.perform_action!(:publish, meeting, form.current_user, visibility: "all") do + meeting.publish! + end + end + end + end +end + +Decidim::Meetings::CreateMeeting.class_eval do + prepend Decidim::Meetings::CreateMeetingExtends +end diff --git a/lib/extends/commands/decidim/meetings/update_meeting_extends.rb b/lib/extends/commands/decidim/meetings/update_meeting_extends.rb new file mode 100644 index 00000000..2497bc96 --- /dev/null +++ b/lib/extends/commands/decidim/meetings/update_meeting_extends.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module Decidim + module Meetings + module UpdateMeetingExtends + def call + return broadcast(:invalid) if form.invalid? + + transaction do + update_meeting! + send_notification if should_notify_followers? + schedule_upcoming_meeting_notification if start_time_changed? + end + + broadcast(:ok, meeting) + end + + private + + def update_meeting! + parsed_title = Decidim::ContentProcessor.parse_with_processor(:hashtag, form.title, current_organization: form.current_organization).rewrite + parsed_description = Decidim::ContentProcessor.parse_with_processor(:hashtag, form.description, current_organization: form.current_organization).rewrite + + Decidim.traceability.update!( + meeting, + form.current_user, + { + scope: form.scope, + category: form.category, + title: { I18n.locale => parsed_title }, + description: { I18n.locale => parsed_description }, + end_time: form.end_time, + start_time: form.start_time, + address: form.address, + latitude: form.latitude, + longitude: form.longitude, + location: { I18n.locale => form.location }, + location_hints: { I18n.locale => form.location_hints }, + author: form.current_user, + decidim_user_group_id: form.user_group_id, + registration_type: form.registration_type, + registration_url: form.registration_url, + available_slots: form.available_slots, + registration_terms: { I18n.locale => form.registration_terms }, + registrations_enabled: form.registrations_enabled, + type_of_meeting: form.clean_type_of_meeting, + online_meeting_url: form.online_meeting_url, + iframe_embed_type: form.iframe_embed_type, + iframe_access_level: form.iframe_access_level + }, + visibility: "public-only" + ) + end + end + end +end + +Decidim::Meetings::UpdateMeeting.class_eval do + prepend Decidim::Meetings::UpdateMeetingExtends +end diff --git a/spec/commands/meetings/admin/create_meeting_spec.rb b/spec/commands/meetings/admin/create_meeting_spec.rb new file mode 100644 index 00000000..c91c1e99 --- /dev/null +++ b/spec/commands/meetings/admin/create_meeting_spec.rb @@ -0,0 +1,192 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim::Meetings + describe Admin::CreateMeeting do + subject { described_class.new(form) } + + let(:organization) { create :organization, available_locales: [:en] } + let(:current_user) { create :user, :admin, :confirmed, organization: organization } + let(:participatory_process) { create :participatory_process, organization: organization } + let(:current_component) { create :component, participatory_space: participatory_process, manifest_name: "meetings" } + let(:scope) { create :scope, organization: organization } + let(:category) { create :category, participatory_space: participatory_process } + let(:address) { "address" } + let(:invalid) { false } + let(:latitude) { 40.1234 } + let(:longitude) { 2.1234 } + let(:start_time) { 1.day.from_now } + let(:private_meeting) { false } + let(:transparent) { true } + let(:transparent_type) { "transparent" } + let(:type_of_meeting) { "online" } + let(:online_meeting_url) { "http://decidim.org" } + let(:registration_url) { "http://decidim.org" } + let(:registration_type) { "on_this_platform" } + let(:registrations_enabled) { true } + let(:iframe_embed_type) { "embed_in_meeting_page" } + let(:iframe_access_level) { "all" } + let(:services) do + [ + { + "title" => { "en" => "First service" }, + "description" => { "en" => "First description" } + }, + { + "title" => { "en" => "Second service" }, + "description" => { "en" => "Second description" } + } + ] + end + let(:services_to_persist) do + services.map { |service| Admin::MeetingServiceForm.from_params(service) } + end + let(:description) { { en: "description" } } + + let(:form) do + double( + invalid?: invalid, + title: { en: "title" }, + description: description, + location: { en: "location" }, + location_hints: { en: "location_hints" }, + start_time: start_time, + end_time: 1.day.from_now + 1.hour, + address: address, + latitude: latitude, + longitude: longitude, + scope: scope, + category: category, + private_meeting: private_meeting, + transparent: transparent, + services_to_persist: services_to_persist, + current_user: current_user, + current_component: current_component, + current_organization: organization, + registration_type: registration_type, + registration_url: registration_url, + registrations_enabled: registrations_enabled, + clean_type_of_meeting: type_of_meeting, + online_meeting_url: online_meeting_url, + iframe_embed_type: iframe_embed_type, + comments_enabled: true, + comments_start_time: nil, + comments_end_time: nil, + iframe_access_level: iframe_access_level + ) + end + + context "when the form is not valid" do + let(:invalid) { true } + + it "is not valid" do + expect { subject.call }.to broadcast(:invalid) + end + end + + context "when everything is ok" do + let(:meeting) { Meeting.last } + + it "creates the meeting" do + expect { subject.call }.to change(Meeting, :count).by(1) + end + + it "sets the scope" do + subject.call + expect(meeting.scope).to eq scope + end + + it "sets the category" do + subject.call + expect(meeting.category).to eq category + end + + it "sets the author" do + subject.call + expect(meeting.author).to eq organization + end + + it "sets the registration enabled flag" do + subject.call + expect(meeting.registrations_enabled).to eq registrations_enabled + end + + it "sets the component" do + subject.call + expect(meeting.component).to eq current_component + end + + it "sets the longitude and latitude" do + subject.call + last_meeting = Meeting.last + expect(last_meeting.latitude).to eq(latitude) + expect(last_meeting.longitude).to eq(longitude) + end + + it "sets the services" do + subject.call + + meeting.services.each_with_index do |service, index| + expect(service.title).to eq(services[index]["title"]) + expect(service.description).to eq(services[index]["description"]) + end + end + + it "sets the questionnaire for registrations" do + subject.call + expect(meeting.questionnaire).to be_a(Decidim::Forms::Questionnaire) + end + + it "is created as unpublished" do + subject.call + + expect(meeting).not_to be_published + end + + it "makes the user follow the meeting" do + expect { subject.call }.to change(Decidim::Follow, :count).by(1) + expect(meeting.reload.followers).to include(current_user) + end + + it "sets iframe_embed_type" do + subject.call + + expect(meeting.iframe_embed_type).to eq(iframe_embed_type) + end + + it "sets iframe_access_level" do + subject.call + + expect(meeting.iframe_access_level).to eq(iframe_access_level) + end + + it "traces the action", versioning: true do + expect(Decidim.traceability) + .to receive(:create!) + .with(Meeting, current_user, kind_of(Hash), visibility: "all") + .and_call_original + + expect { subject.call }.to change(Decidim::ActionLog, :count) + action_log = Decidim::ActionLog.last + expect(action_log.version).to be_present + end + + context "when a link to a proposal is added in the description of the current platform" do + let(:component) { create(:extended_proposal_component, :with_creation_enabled, :with_attachments_allowed) } + let(:proposal) { create(:proposal, component: component) } + let(:proposal_path) { Decidim::ResourceLocatorPresenter.new(proposal).path } + let(:english_description) { "This is a description with a link to a proposal: [#{proposal.title}](#{proposal_path})" } + let(:description) { { en: english_description } } + + before do + subject.call + end + + it "maintains the same description after changes" do + expect(meeting.description["en"]).to include(english_description) + end + end + end + end +end diff --git a/spec/commands/meetings/admin/update_meeting_spec.rb b/spec/commands/meetings/admin/update_meeting_spec.rb new file mode 100644 index 00000000..f23900d4 --- /dev/null +++ b/spec/commands/meetings/admin/update_meeting_spec.rb @@ -0,0 +1,299 @@ +# frozen_string_literal: true + +require "spec_helper" +require "decidim/meetings/test/notifications_handling" + +module Decidim::Meetings + describe Admin::UpdateMeeting do + subject { described_class.new(form, meeting) } + + let(:meeting) { create(:meeting, :published) } + let(:organization) { meeting.component.organization } + let(:scope) { create :scope, organization: organization } + let(:category) { create :category, participatory_space: meeting.component.participatory_space } + let(:address) { meeting.address } + let(:invalid) { false } + let(:latitude) { 40.1234 } + let(:longitude) { 2.1234 } + let(:service_objects) { build_list(:service, 2) } + let(:services) do + service_objects.map(&:attributes) + end + let(:services_to_persist) do + services.map { |service| Admin::MeetingServiceForm.from_params(service) } + end + let(:user) { create :user, :admin, organization: organization } + let(:private_meeting) { false } + let(:transparent) { true } + let(:type_of_meeting) { "online" } + let(:online_meeting_url) { "http://decidim.org" } + let(:registration_url) { "http://decidim.org" } + let(:registration_type) { "on_this_platform" } + let(:registrations_enabled) { true } + let(:iframe_embed_type) { "none" } + let(:iframe_access_level) { nil } + let(:description) { { en: "description" } } + + let(:form) do + double( + invalid?: invalid, + title: { en: "title" }, + description: description, + location: { en: "location" }, + location_hints: { en: "location_hints" }, + start_time: 1.day.from_now, + end_time: 1.day.from_now + 1.hour, + scope: scope, + category: category, + address: address, + latitude: latitude, + longitude: longitude, + private_meeting: private_meeting, + transparent: transparent, + services_to_persist: services_to_persist, + current_user: user, + current_organization: organization, + registration_type: registration_type, + registration_url: registration_url, + registrations_enabled: registrations_enabled, + clean_type_of_meeting: type_of_meeting, + online_meeting_url: online_meeting_url, + iframe_embed_type: iframe_embed_type, + comments_enabled: true, + comments_start_time: nil, + comments_end_time: nil, + iframe_access_level: iframe_access_level + ) + end + + context "when the form is not valid" do + let(:invalid) { true } + + it "is not valid" do + expect { subject.call }.to broadcast(:invalid) + end + end + + context "when everything is ok" do + it "updates the meeting" do + subject.call + expect(translated(meeting.title)).to eq "title" + end + + it "sets the scope" do + subject.call + expect(meeting.scope).to eq scope + end + + it "sets the category" do + subject.call + expect(meeting.category).to eq category + end + + it "sets the latitude and longitude" do + subject.call + expect(meeting.latitude).to eq(latitude) + expect(meeting.longitude).to eq(longitude) + end + + it "sets the author" do + subject.call + expect(meeting.author).to eq organization + end + + it "sets the registration enabled flag" do + subject.call + expect(meeting.registrations_enabled).to eq registrations_enabled + end + + it "sets the services" do + subject.call + meeting.services.each_with_index do |service, index| + expect(service.title).to eq(service_objects[index].title) + expect(service.description).to eq(service_objects[index].description) + end + end + + it "sets iframe_access_level" do + subject.call + + expect(meeting.iframe_access_level).to eq(iframe_access_level) + end + + it "traces the action", versioning: true do + expect(Decidim.traceability) + .to receive(:update!) + .with(meeting, user, kind_of(Hash)) + .and_call_original + + expect { subject.call }.to change(Decidim::ActionLog, :count) + action_log = Decidim::ActionLog.last + expect(action_log.version).to be_present + end + + describe "events" do + let!(:follow) { create :follow, followable: meeting, user: user } + let(:title) { meeting.title } + let(:start_time) { meeting.start_time } + let(:end_time) { meeting.end_time } + let(:address) { meeting.address } + let(:form) do + double( + invalid?: false, + title: title, + description: meeting.description, + location: meeting.location, + location_hints: meeting.location_hints, + start_time: start_time, + end_time: end_time, + scope: meeting.scope, + category: meeting.category, + address: address, + latitude: meeting.latitude, + longitude: meeting.longitude, + private_meeting: private_meeting, + transparent: transparent, + services_to_persist: services_to_persist, + current_user: user, + current_organization: organization, + registration_type: registration_type, + registration_url: registration_url, + registrations_enabled: registrations_enabled, + clean_type_of_meeting: type_of_meeting, + online_meeting_url: online_meeting_url, + iframe_embed_type: iframe_embed_type, + comments_enabled: true, + comments_start_time: nil, + comments_end_time: nil, + iframe_access_level: iframe_access_level + ) + end + + context "when nothing changes" do + it "doesn't notify the change" do + expect(Decidim::EventsManager) + .not_to receive(:publish) + + subject.call + end + end + + context "when a non-important attribute changes" do + let(:title) do + { + "en" => "Title updated" + } + end + + it "doesn't notify the change" do + expect(Decidim::EventsManager) + .not_to receive(:publish) + + subject.call + end + + it "doesn't schedule the upcoming meeting notification job" do + expect(UpcomingMeetingNotificationJob) + .not_to receive(:perform_later) + + subject.call + end + end + + context "when the start time changes" do + let(:start_time) { meeting.start_time - 1.day } + + it "notifies the change" do + expect(Decidim::EventsManager) + .to receive(:publish) + .with( + event: "decidim.events.meetings.meeting_updated", + event_class: UpdateMeetingEvent, + resource: meeting, + followers: [user] + ) + + subject.call + end + + it_behaves_like "emits an upcoming notificaton" do + let(:future_start_date) { 1.day.from_now + Decidim::Meetings.upcoming_meeting_notification } + let(:past_start_date) { 1.day.ago } + end + end + + context "when the end time changes" do + let(:end_time) { meeting.start_time + 1.day } + + it "notifies the change" do + expect(Decidim::EventsManager) + .to receive(:publish) + .with( + event: "decidim.events.meetings.meeting_updated", + event_class: UpdateMeetingEvent, + resource: meeting, + followers: [user] + ) + + subject.call + end + end + + context "when the address changes" do + let(:address) { "some address" } + + it "notifies the change" do + expect(Decidim::EventsManager) + .to receive(:publish) + .with( + event: "decidim.events.meetings.meeting_updated", + event_class: UpdateMeetingEvent, + resource: meeting, + followers: [user] + ) + + subject.call + end + end + + context "when the meeting is unpublished" do + let(:meeting) { create(:meeting) } + + context "when the start time changes" do + let(:start_time) { meeting.start_time - 1.day } + + it "doesn't notify the change" do + expect(Decidim::EventsManager) + .not_to receive(:publish) + + subject.call + end + + it "doesn't schedule the upcoming meeting notification job" do + expect(UpcomingMeetingNotificationJob) + .not_to receive(:perform_later) + + subject.call + end + end + end + end + + context "when a link to a proposal is added in the description of the current platform" do + let(:component) { create(:extended_proposal_component, :with_creation_enabled, :with_attachments_allowed) } + let(:proposal) { create(:proposal, component: component) } + let(:proposal_path) { Decidim::ResourceLocatorPresenter.new(proposal).path } + let(:english_description) { "This is a description with a link to a proposal: [#{proposal.title}](#{proposal_path})" } + let(:description) { { en: english_description } } + + before do + subject.call + end + + it "maintains the same description after changes" do + expect(meeting.description["en"]).to include(english_description) + end + end + end + end +end diff --git a/spec/commands/meetings/create_meeting_spec.rb b/spec/commands/meetings/create_meeting_spec.rb new file mode 100644 index 00000000..84a115b4 --- /dev/null +++ b/spec/commands/meetings/create_meeting_spec.rb @@ -0,0 +1,231 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim::Meetings + describe CreateMeeting do + subject { described_class.new(form) } + + let(:organization) { create :organization, available_locales: [:en] } + let(:current_user) { create :user, :admin, :confirmed, organization: organization } + let(:participatory_process) { create :participatory_process, organization: organization } + let(:current_component) { create :component, participatory_space: participatory_process, manifest_name: "meetings" } + let(:scope) { create :scope, organization: organization } + let(:category) { create :category, participatory_space: participatory_process } + let(:address) { "address" } + let(:invalid) { false } + let(:latitude) { 40.1234 } + let(:longitude) { 2.1234 } + let(:start_time) { 1.day.from_now } + let(:user_group_id) { nil } + let(:type_of_meeting) { "online" } + let(:registration_url) { "http://decidim.org" } + let(:online_meeting_url) { "http://decidim.org" } + let(:iframe_embed_type) { "embed_in_meeting_page" } + let(:iframe_access_level) { "all" } + let(:registration_type) { "on_this_platform" } + let(:registrations_enabled) { true } + let(:available_slots) { 0 } + let(:registration_terms) { Faker::Lorem.sentence(word_count: 3) } + let(:description) { Faker::Lorem.sentence(word_count: 3) } + let(:form) do + double( + invalid?: invalid, + title: Faker::Lorem.sentence(word_count: 1), + description: description, + location: Faker::Lorem.sentence(word_count: 2), + location_hints: Faker::Lorem.sentence(word_count: 3), + start_time: start_time, + end_time: start_time + 2.hours, + address: address, + latitude: latitude, + longitude: longitude, + scope: scope, + category: category, + user_group_id: user_group_id, + current_user: current_user, + current_component: current_component, + current_organization: organization, + registration_type: registration_type, + available_slots: available_slots, + registration_url: registration_url, + registration_terms: registration_terms, + registrations_enabled: registrations_enabled, + clean_type_of_meeting: type_of_meeting, + online_meeting_url: online_meeting_url, + iframe_embed_type: iframe_embed_type, + iframe_access_level: iframe_access_level + ) + end + + context "when the form is not valid" do + let(:invalid) { true } + + it "is not valid" do + expect { subject.call }.to broadcast(:invalid) + end + end + + context "when everything is ok" do + let(:meeting) { Meeting.last } + + it "creates and publishes the meeting and log both actions" do + subject.call + meeting.reload + expect(meeting).to be_published + expect { subject.call }.to change(Meeting, :count).by(1) + expect { subject.call }.to change(Decidim::ActionLog, :count).by(2) + end + + it "makes the user follow the meeting" do + expect { subject.call }.to change(Decidim::Follow, :count).by(1) + expect(meeting.reload.followers).to include(current_user) + end + + it "sets the scope" do + subject.call + expect(meeting.scope).to eq scope + end + + it "sets the category" do + subject.call + expect(meeting.category).to eq category + end + + it "sets the registration_terms" do + subject.call + expect(meeting.registration_terms).to eq("en" => registration_terms) + end + + it "sets the registrations_enabled flag" do + subject.call + expect(meeting.registrations_enabled).to eq registrations_enabled + end + + it "sets the component" do + subject.call + expect(meeting.component).to eq current_component + end + + it "sets the longitude and latitude" do + subject.call + last_meeting = Meeting.last + expect(last_meeting.latitude).to eq(latitude) + expect(last_meeting.longitude).to eq(longitude) + end + + it "is created as published" do + subject.call + + expect(meeting).to be_published + end + + it "sets iframe_embed_type" do + subject.call + + expect(meeting.iframe_embed_type).to eq(iframe_embed_type) + end + + context "when the author is a user_group" do + let(:user_group) { create :user_group, :verified, users: [current_user], organization: organization } + let(:user_group_id) { user_group.id } + + it "sets the user_group as the author" do + subject.call + expect(meeting.author).to eq current_user + expect(meeting.normalized_author).to eq user_group + end + end + + context "when the author is a user" do + it "sets the user as the author" do + subject.call + expect(meeting.author).to eq current_user + expect(meeting.normalized_author).to eq current_user + end + end + + it "traces the action", versioning: true do + expect(Decidim.traceability) + .to receive(:create!) + .with(Meeting, current_user, kind_of(Hash), visibility: "public-only") + .and_call_original + + expect { subject.call }.to change(Decidim::ActionLog, :count) + action_log = Decidim::ActionLog.last + expect(action_log.version).to be_present + end + + it "schedules a upcoming meeting notification job 48h before start time" do + meeting = instance_double(Meeting, id: 1, start_time: start_time, participatory_space: participatory_process) + allow(Decidim.traceability) + .to receive(:create!) + .and_return(meeting) + + expect(meeting).to receive(:valid?) + expect(meeting).to receive(:publish!) + allow(meeting).to receive(:to_signed_global_id).and_return "gid://Decidim::Meetings::Meeting/1" + + allow(UpcomingMeetingNotificationJob) + .to receive(:generate_checksum).and_return "1234" + + expect(UpcomingMeetingNotificationJob) + .to receive_message_chain(:set, :perform_later) # rubocop:disable RSpec/MessageChain + .with(set: start_time - Decidim::Meetings.upcoming_meeting_notification).with(1, "1234") + + allow(Decidim::EventsManager).to receive(:publish).and_return(true) + + subject.call + end + + it "doesn't schedule an upcoming meeting notification if start time is in the past" do + meeting = instance_double(Meeting, id: 1, start_time: 2.days.ago, participatory_space: participatory_process) + allow(Decidim.traceability) + .to receive(:create!) + .and_return(meeting) + + expect(meeting).to receive(:valid?) + expect(meeting).to receive(:publish!) + allow(meeting).to receive(:to_signed_global_id).and_return "gid://Decidim::Meetings::Meeting/1" + + expect(UpcomingMeetingNotificationJob).not_to receive(:generate_checksum) + expect(UpcomingMeetingNotificationJob).not_to receive(:set) + + allow(Decidim::EventsManager).to receive(:publish).and_return(true) + + subject.call + end + + it "sends a notification to the participatory space followers" do + follower = create(:user, organization: organization) + create(:follow, followable: participatory_process, user: follower) + + expect(Decidim::EventsManager) + .to receive(:publish) + .with( + event: "decidim.events.meetings.meeting_created", + event_class: Decidim::Meetings::CreateMeetingEvent, + resource: kind_of(Meeting), + followers: [follower] + ) + + subject.call + end + + context "when a link to a proposal is added in the description of the current platform" do + let(:component) { create(:extended_proposal_component, :with_creation_enabled, :with_attachments_allowed) } + let(:proposal) { create(:proposal, component: component) } + let(:proposal_path) { Decidim::ResourceLocatorPresenter.new(proposal).path } + let(:description) { "This is a description with a link to a proposal: [#{proposal.title}](#{proposal_path})" } + + before do + subject.call + end + + it "maintains the same description after changes" do + expect(meeting.description["en"]).to include(description) + end + end + end + end +end diff --git a/spec/commands/meetings/update_meeting_spec.rb b/spec/commands/meetings/update_meeting_spec.rb new file mode 100644 index 00000000..d0fa73a4 --- /dev/null +++ b/spec/commands/meetings/update_meeting_spec.rb @@ -0,0 +1,263 @@ +# frozen_string_literal: true + +require "spec_helper" +require "decidim/meetings/test/notifications_handling" + +module Decidim::Meetings + describe UpdateMeeting do + subject { described_class.new(form, current_user, meeting) } + + let(:meeting) { create :meeting } + let(:organization) { meeting.component.organization } + let(:current_user) { create :user, :confirmed, organization: organization } + let(:participatory_process) { meeting.component.participatory_space } + let(:current_component) { meeting.component } + let(:scope) { create :scope, organization: organization } + let(:category) { create :category, participatory_space: participatory_process } + let(:address) { "address" } + let(:invalid) { false } + let(:latitude) { 40.1234 } + let(:longitude) { 2.1234 } + let(:start_time) { 1.day.from_now } + let(:user_group_id) { nil } + let(:type_of_meeting) { "online" } + let(:online_meeting_url) { "http://decidim.org" } + let(:registration_type) { "on_this_platform" } + let(:available_slots) { 0 } + let(:registration_url) { "http://decidim.org" } + let(:iframe_embed_type) { "none" } + let(:iframe_access_level) { nil } + let(:description) { "The meeting description text" } + let(:form) do + double( + invalid?: invalid, + title: "The meeting title", + description: description, + location: "The meeting location text", + location_hints: "The meeting location hint text", + start_time: 1.day.from_now, + end_time: 1.day.from_now + 1.hour, + scope: scope, + category: category, + address: address, + latitude: latitude, + longitude: longitude, + user_group_id: user_group_id, + current_user: current_user, + current_organization: organization, + registration_type: registration_type, + available_slots: available_slots, + registration_url: registration_url, + registration_terms: "The meeting registration terms", + registrations_enabled: true, + clean_type_of_meeting: type_of_meeting, + online_meeting_url: online_meeting_url, + iframe_embed_type: iframe_embed_type, + iframe_access_level: iframe_access_level + ) + end + + context "when the form is not valid" do + let(:invalid) { true } + + it "is not valid" do + expect { subject.call }.to broadcast(:invalid) + end + end + + context "when everything is ok" do + it "updates the meeting" do + subject.call + + expect(meeting.title).to include("en" => "The meeting title") + expect(meeting.description).to include("en" => "The meeting description text") + end + + it "sets the scope" do + subject.call + expect(meeting.scope).to eq scope + end + + it "sets the category" do + subject.call + expect(meeting.category).to eq category + end + + it "sets the latitude and longitude" do + subject.call + expect(meeting.latitude).to eq(latitude) + expect(meeting.longitude).to eq(longitude) + end + + context "when the author is a user_group" do + let(:user_group) { create :user_group, :verified, users: [current_user], organization: organization } + let(:user_group_id) { user_group.id } + + it "sets the user_group as the author" do + subject.call + expect(meeting.author).to eq current_user + expect(meeting.normalized_author).to eq user_group + end + end + + context "when the author is a user" do + it "sets the user as the author" do + subject.call + expect(meeting.author).to eq current_user + expect(meeting.normalized_author).to eq current_user + end + end + + it "traces the action", versioning: true do + expect(Decidim.traceability) + .to receive(:update!) + .with(meeting, current_user, kind_of(Hash), visibility: "public-only") + .and_call_original + + expect { subject.call }.to change(Decidim::ActionLog, :count) + action_log = Decidim::ActionLog.last + expect(action_log.version).to be_present + expect(action_log.version.event).to eq "update" + end + + describe "events" do + let!(:follow) { create :follow, followable: meeting, user: current_user } + let(:title) { meeting.title } + let(:start_time) { meeting.start_time } + let(:end_time) { meeting.end_time } + let(:address) { meeting.address } + let(:form) do + double( + invalid?: false, + title: title, + description: meeting.description, + location: meeting.location, + location_hints: meeting.location_hints, + start_time: start_time, + end_time: end_time, + scope: meeting.scope, + category: meeting.category, + address: address, + latitude: meeting.latitude, + longitude: meeting.longitude, + user_group_id: user_group_id, + services_to_persist: [], + current_user: current_user, + current_organization: organization, + registration_type: registration_type, + available_slots: available_slots, + registration_url: registration_url, + registration_terms: meeting.registration_terms, + registrations_enabled: true, + clean_type_of_meeting: type_of_meeting, + online_meeting_url: online_meeting_url, + iframe_embed_type: iframe_embed_type, + iframe_access_level: iframe_access_level + ) + end + + context "when nothing changes" do + it "doesn't notify the change" do + expect(Decidim::EventsManager) + .not_to receive(:publish) + + subject.call + end + end + + context "when a non-important attribute changes" do + let(:title) do + { + "en" => "Title updated" + } + end + + it "doesn't notify the change" do + expect(Decidim::EventsManager) + .not_to receive(:publish) + + subject.call + end + + it "doesn't schedule the upcoming meeting notification job" do + expect(UpcomingMeetingNotificationJob) + .not_to receive(:perform_later) + + subject.call + end + end + + context "when the start time changes" do + let(:start_time) { meeting.start_time - 1.day } + + it "notifies the change" do + expect(Decidim::EventsManager) + .to receive(:publish) + .with( + event: "decidim.events.meetings.meeting_updated", + event_class: UpdateMeetingEvent, + resource: meeting, + followers: [current_user] + ) + + subject.call + end + + it_behaves_like "emits an upcoming notificaton" do + let(:future_start_date) { 1.day.from_now + Decidim::Meetings.upcoming_meeting_notification } + let(:past_start_date) { 1.day.ago } + end + end + + context "when the end time changes" do + let(:end_time) { meeting.start_time + 1.day } + + it "notifies the change" do + expect(Decidim::EventsManager) + .to receive(:publish) + .with( + event: "decidim.events.meetings.meeting_updated", + event_class: UpdateMeetingEvent, + resource: meeting, + followers: [current_user] + ) + + subject.call + end + end + + context "when the address changes" do + let(:address) { "some address" } + + it "notifies the change" do + expect(Decidim::EventsManager) + .to receive(:publish) + .with( + event: "decidim.events.meetings.meeting_updated", + event_class: UpdateMeetingEvent, + resource: meeting, + followers: [current_user] + ) + + subject.call + end + end + end + end + + context "when a link to a proposal is added in the description of the current platform" do + let(:component) { create(:extended_proposal_component, :with_creation_enabled, :with_attachments_allowed) } + let(:proposal) { create(:proposal, component: component) } + let(:proposal_path) { Decidim::ResourceLocatorPresenter.new(proposal).path } + let(:description) { "This is a description with a link to a proposal: [#{proposal.title}](#{proposal_path})" } + + before do + subject.call + end + + it "maintains the same description after changes" do + expect(meeting.description["en"]).to include(description) + end + end + end +end