diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index c659d61fc1dd0..fd824e5e21cac 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -318,6 +318,13 @@ ignore_unused: - decidim.meetings.meetings.show.withdraw_btn_hint - decidim.meetings.meetings.show.withdraw_confirmation_html - decidim.meetings.meetings.show.withdraw_meeting + - decidim.meetings.meetings.show.cancel_waitlist + - decidim.meetings.meetings.show.join_waitlist + - decidim.meetings.meetings.show.leave + - decidim.meetings.meetings.show.registration_confirmation + - decidim.meetings.meetings.show.registration_title + - decidim.meetings.meetings.show.waitlist_confirmation + - decidim.meetings.meetings.show.waitlist_title - decidim.proposals.proposals.show.withdraw_btn_hint - decidim.proposals.proposals.show.withdraw_confirmation_html - decidim.proposals.proposals.show.withdraw_proposal diff --git a/decidim-meetings/app/cells/decidim/meetings/cancel_registration_meeting_button/cancelation_modal.erb b/decidim-meetings/app/cells/decidim/meetings/cancel_registration_meeting_button/cancelation_modal.erb index 2d49bb2bd3892..4f14fd1dc6f25 100644 --- a/decidim-meetings/app/cells/decidim/meetings/cancel_registration_meeting_button/cancelation_modal.erb +++ b/decidim-meetings/app/cells/decidim/meetings/cancel_registration_meeting_button/cancelation_modal.erb @@ -2,10 +2,13 @@ <%= decidim_form_for(registration_form, url: meeting_registration_path(model), method: :delete) do |form| %>
<%= icon "door-open-line" %> -

<%= t("leave", scope: "decidim.meetings.meetings.show") %>

+

+ <%= i18n_modal_title %> +

- -
<%= t("leave_confirmation", scope: "decidim.meetings.meetings.show") %>
+
+ <%= i18n_modal_confirmation_text %> +
@@ -13,7 +16,7 @@ <%= t("close", scope: "decidim.shared.flag_modal") %>
diff --git a/decidim-meetings/app/cells/decidim/meetings/cancel_registration_meeting_button/show.erb b/decidim-meetings/app/cells/decidim/meetings/cancel_registration_meeting_button/show.erb index d4b8ee70387db..ecef0e2c772ed 100644 --- a/decidim-meetings/app/cells/decidim/meetings/cancel_registration_meeting_button/show.erb +++ b/decidim-meetings/app/cells/decidim/meetings/cancel_registration_meeting_button/show.erb @@ -1,3 +1,4 @@ +<%= waiting_list_info %> <% unless options[:hide_modal] %> <%= render :cancelation_modal %> <% end %> @@ -7,6 +8,6 @@ class: button_classes, data: { "dialog-open": "meeting-cancelation-confirm-#{model.id}" } ) do %> - <%= t("leave", scope: "decidim.meetings.meetings.show") %> + <%= cancel_button_text %> <%= icon icon_name %> <% end %> diff --git a/decidim-meetings/app/cells/decidim/meetings/cancel_registration_meeting_button_cell.rb b/decidim-meetings/app/cells/decidim/meetings/cancel_registration_meeting_button_cell.rb index c4a568be0c679..28103c9183268 100644 --- a/decidim-meetings/app/cells/decidim/meetings/cancel_registration_meeting_button_cell.rb +++ b/decidim-meetings/app/cells/decidim/meetings/cancel_registration_meeting_button_cell.rb @@ -13,12 +13,48 @@ def show render end + def waiting_list_info + cell("decidim/announcement", t("waitlist_info", scope: "decidim.meetings.meetings.show")) if registration_status == "on_waiting_list" + end + private def current_component model.component end + def registration_status + model.registrations.find_by(user: current_user)&.status + end + + def action_keys + if registration_status == "on_waiting_list" + { + button: "cancel_waitlist", + modal_title: "waitlist_title", + modal_confirmation: "waitlist_confirmation" + } + else + { + button: "leave", + modal_title: "registration_title", + modal_confirmation: "registration_confirmation" + } + end + end + + def cancel_button_text + I18n.t(action_keys[:button], scope: "decidim.meetings.meetings.show") + end + + def i18n_modal_title + I18n.t(action_keys[:modal_title], scope: "decidim.meetings.meetings.show") + end + + def i18n_modal_confirmation_text + I18n.t(action_keys[:modal_confirmation], scope: "decidim.meetings.meetings.show") + end + def button_classes "button button__sm button__transparent-secondary w-full" end diff --git a/decidim-meetings/app/cells/decidim/meetings/join_meeting_button/registration_modal.erb b/decidim-meetings/app/cells/decidim/meetings/join_meeting_button/registration_modal.erb index 9c2536cf18068..87f4ce6948368 100644 --- a/decidim-meetings/app/cells/decidim/meetings/join_meeting_button/registration_modal.erb +++ b/decidim-meetings/app/cells/decidim/meetings/join_meeting_button/registration_modal.erb @@ -1,8 +1,9 @@ -<%= decidim_modal id: "meeting-registration-confirm-#{model.id}", class: "meeting__registration-modal" do %> - <%= decidim_form_for(registration_form, url: meeting_registration_path(model), method: :post) do |form| %> +<% action = model.has_available_slots? ? "registration" : "waitlist" %> +<%= decidim_modal id: "meeting-#{action}-confirm-#{model.id}", class: "meeting__registration-modal" do %> + <%= decidim_form_for(registration_form, url: model.has_available_slots? ? meeting_registration_path(model) : join_waitlist_meeting_registration_path(model), method: :post) do |form| %>
<%= icon "login-circle-line" %> -

<%= t("join", scope: "decidim.meetings.meetings.show") %>

+

<%= t(model.has_available_slots? ? "join" : "join_waitlist", scope: "decidim.meetings.meetings.show") %>

@@ -13,7 +14,7 @@
-
+
+ <%= form.check_box :waitlist_enabled, help_text: t(".waitlist_enabled_help") %> +
+
<%= form.check_box :customize_registration_email %>
diff --git a/decidim-meetings/app/views/decidim/meetings/meetings/_meeting_aside.html.erb b/decidim-meetings/app/views/decidim/meetings/meetings/_meeting_aside.html.erb index f34a63c227c75..466cf77f651b2 100644 --- a/decidim-meetings/app/views/decidim/meetings/meetings/_meeting_aside.html.erb +++ b/decidim-meetings/app/views/decidim/meetings/meetings/_meeting_aside.html.erb @@ -6,6 +6,12 @@ <% end %> +<% if meeting.waitlist_enabled? && !meeting.has_available_slots? && meeting.can_be_joined_by?(current_user) %> +
+ <%= cell "decidim/meetings/join_waitlist_button", meeting, hide_modal: local_assigns[:hide_modal] %> +
+<% end %> + <% if (meeting.closed? && meeting.closing_visible?) || (registration.present? && registration.meeting.component.settings.registration_code_enabled) || (meeting.services.any?) %>
<% if meeting.closed? && meeting.closing_visible? %> @@ -35,7 +41,7 @@ <% end %> <% end %> - <% if registration.present? && registration.meeting.component.settings.registration_code_enabled %> + <% if registration.present? && registration.meeting.component.settings.registration_code_enabled && registration.registered? %> <%= render layout: "decidim/meetings/layouts/aside_block", locals: { emoji: "coupon-line" } do %>

<%= registration_code_help_text %>

<%= registration.code %>
diff --git a/decidim-meetings/config/locales/en.yml b/decidim-meetings/config/locales/en.yml index 1b6e07e1f396f..4ab076bcbab2b 100644 --- a/decidim-meetings/config/locales/en.yml +++ b/decidim-meetings/config/locales/en.yml @@ -43,6 +43,7 @@ en: title: Title transparent: Transparent type_of_meeting: Type + waitlist_enabled: Waitlist enabled meeting_agenda: title: Title visible: Visible @@ -231,6 +232,8 @@ en: email_outro: You have received this notification because you are following the "%{resource_title}" meeting. You can unfollow it from the previous link. email_subject: The "%{resource_title}" meeting was updated notification_title: The %{resource_title} meeting was updated. + meeting_waitlist_added: + notification_title: You have been added to the waiting list for the %{resource_title} meeting. registration_code_validated: email_intro: Your registration code "%{registration_code}" for the "%{resource_title}" meeting has been validated. email_outro: You have received this notification because your registration code for the "%{resource_title}" meeting has been validated. @@ -431,6 +434,7 @@ en: reserved_slots_help: Leave it to 0 if you do not have reserved slots. reserved_slots_less_than: Must be less than or equal to %{count} title: Registrations + waitlist_enabled_help: If enabled, the user will be able to join the waitlist when the available slots are full. update: invalid: There was a problem saving the registration settings. success: Meeting registrations settings successfully saved. @@ -592,11 +596,13 @@ en: cancel: Cancel confirm: Confirm show: + add_to_waitlist: Join the waiting list attendees: Attendees count + cancel_waitlist: Leave the waiting list contributions: Contributions count join: Register + join_waitlist: Join the waiting list leave: Cancel your registration - leave_confirmation: Are you sure you want to cancel your registration for this meeting? link_available_soon: Link available soon link_closed: The link to join the meeting will be available a few minutes before it starts live_event: This meeting is happening right now @@ -606,13 +612,18 @@ en: organizations: Attending organizations redirect_notice: This meeting is part of another space, so you have been moved to %{current_space_name}.
If you prefer, you can go back to %{previous_space_name}. registration_code_help_text: Your registration code + registration_confirmation: Are you sure you want to cancel your registration for this meeting? registration_state: validated: VALIDATED validation_pending: VALIDATION PENDING + registration_title: Cancel your registration remaining_slots: one: "%{count} slot remaining" other: "%{count} slots remaining" visit_finished: View past meeting + waitlist_confirmation: Are you sure you want to cancel your waitlist registration for this meeting? + waitlist_info: You have joined the waiting list for this meeting. If a spot for this meeting becomes available, you will register automatically and receive a notification. + waitlist_title: Cancel your waitlist registration withdraw_btn_hint: You can withdraw your meeting if you change your mind. The meeting is not deleted, it will appear in the list of withdrawn meetings. withdraw_confirmation_html: Are you sure you want to withdraw this meeting?

This action cannot be cancelled! withdraw_meeting: Withdraw @@ -697,8 +708,15 @@ en: invalid: There was a problem declining the invitation. success: You have declined the invitation successfully. destroy: - invalid: There was a problem leaving this meeting. - success: You have left the meeting successfully. + on_waiting_list: + invalid: There was a problem leaving the waiting list. + success: You have left the waiting list successfully. + registered: + invalid: There was a problem leaving this meeting. + success: You have left the meeting successfully. + waitlist: + invalid: There was a problem joining the waiting list. + success: You have joined the waiting list successfully. type_of_meeting: hybrid: Hybrid in_person: In person diff --git a/decidim-meetings/db/migrate/20250214110525_add_waitlist_enabled_to_decidim_meetings.rb b/decidim-meetings/db/migrate/20250214110525_add_waitlist_enabled_to_decidim_meetings.rb new file mode 100644 index 0000000000000..c0707d4366d61 --- /dev/null +++ b/decidim-meetings/db/migrate/20250214110525_add_waitlist_enabled_to_decidim_meetings.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddWaitlistEnabledToDecidimMeetings < ActiveRecord::Migration[7.0] + def change + add_column :decidim_meetings_meetings, :waitlist_enabled, :boolean, default: false, null: false + end +end diff --git a/decidim-meetings/db/migrate/20250214113208_add_status_to_registrations_to_decidim_meetings_registrations.rb b/decidim-meetings/db/migrate/20250214113208_add_status_to_registrations_to_decidim_meetings_registrations.rb new file mode 100644 index 0000000000000..4bcce525e5e8c --- /dev/null +++ b/decidim-meetings/db/migrate/20250214113208_add_status_to_registrations_to_decidim_meetings_registrations.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class AddStatusToRegistrationsToDecidimMeetingsRegistrations < ActiveRecord::Migration[7.0] + def change + add_column :decidim_meetings_registrations, :status, :string, default: "registered", null: false + add_index :decidim_meetings_registrations, :status + end +end diff --git a/decidim-meetings/lib/decidim/meetings/engine.rb b/decidim-meetings/lib/decidim/meetings/engine.rb index 3531cc8d9c56d..0e70e0940c9d5 100644 --- a/decidim-meetings/lib/decidim/meetings/engine.rb +++ b/decidim-meetings/lib/decidim/meetings/engine.rb @@ -26,6 +26,8 @@ class Engine < ::Rails::Engine get :decline_invitation get :join, action: :show post :answer + get :join_waitlist, action: :show + post :join_waitlist end end resources :versions, only: [:show] @@ -71,6 +73,7 @@ class Engine < ::Rails::Engine Decidim.icons.register(name: "bill-line", icon: "bill-line", category: "system", description: "", engine: :meetings) Decidim.icons.register(name: "add-box-line", icon: "add-box-line", category: "system", description: "", engine: :meetings) Decidim.icons.register(name: "calendar-close-line", icon: "calendar-close-line", category: "system", description: "", engine: :meetings) + Decidim.icons.register(name: "clockwise-line", icon: "clockwise-line", category: "system", description: "", engine: :meetings) end initializer "decidim_meetings.content_processors" do |_app| diff --git a/decidim-meetings/lib/decidim/meetings/test/factories.rb b/decidim-meetings/lib/decidim/meetings/test/factories.rb index 7ad7fbfbde41b..1ee2b74b9e113 100644 --- a/decidim-meetings/lib/decidim/meetings/test/factories.rb +++ b/decidim-meetings/lib/decidim/meetings/test/factories.rb @@ -44,6 +44,7 @@ registration_terms { generate_localized_description(:meeting_registration_terms, skip_injection:) } registration_type { :on_this_platform } type_of_meeting { :in_person } + waitlist_enabled { false } component { build(:meeting_component) } iframe_access_level { :all } iframe_embed_type { :none } diff --git a/decidim-meetings/spec/commands/admin/update_registrations_spec.rb b/decidim-meetings/spec/commands/admin/update_registrations_spec.rb index 5858b5145a096..0567d50677732 100644 --- a/decidim-meetings/spec/commands/admin/update_registrations_spec.rb +++ b/decidim-meetings/spec/commands/admin/update_registrations_spec.rb @@ -28,6 +28,7 @@ module Decidim::Meetings registration_form_enabled:, available_slots:, reserved_slots:, + waitlist_enabled: true, customize_registration_email:, registration_email_custom_content:, registration_terms:, @@ -57,6 +58,7 @@ module Decidim::Meetings expect(meeting.customize_registration_email).to be true expect(meeting.registration_email_custom_content).to eq(registration_email_custom_content) expect(translated(meeting.registration_terms)).to eq "A legal text" + expect(meeting.waitlist_enabled).to be true end end diff --git a/decidim-meetings/spec/commands/join_waitlist_spec.rb b/decidim-meetings/spec/commands/join_waitlist_spec.rb new file mode 100644 index 0000000000000..b9c7d8fa008f1 --- /dev/null +++ b/decidim-meetings/spec/commands/join_waitlist_spec.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim::Meetings + describe JoinWaitlist do + subject { described_class.new(meeting, form) } + + let(:organization) { create(:organization) } + let(:participatory_process) { create(:participatory_process, organization: organization) } + let(:component) { create(:component, manifest_name: :meetings, participatory_space: participatory_process) } + let(:available_slots) { 2 } + let(:waitlist_enabled) { true } + let(:meeting) do + create(:meeting, + component: component, + waitlist_enabled: waitlist_enabled, + registrations_enabled: true, + available_slots: available_slots) + end + + let(:user) { create(:user, :confirmed, organization: organization, notifications_sending_frequency: "none") } + let(:user_group) { create(:user_group) } + let(:form_params) { { user_group_id: user_group.id } } + let(:form) do + Decidim::Meetings::JoinMeetingForm.from_params(form_params).with_context(current_user: user) + end + + let(:waitlist_notification) do + { + event: "decidim.events.meetings.meeting_waitlist_added", + event_class: MeetingRegistrationNotificationEvent, + resource: meeting, + affected_users: [user] + } + end + + context "when all conditions are met" do + before do + create_list(:registration, available_slots, meeting: meeting, status: :registered) + end + + it "broadcasts ok" do + expect { subject.call }.to broadcast(:ok) + end + + it "creates a waitlist registration with correct attributes" do + expect { subject.call }.to change(Registration, :count).by(1) + last_registration = Registration.last + expect(last_registration.user).to eq(user) + expect(last_registration.meeting).to eq(meeting) + expect(last_registration.status).to eq("on_waiting_list") + end + + it "publishes waitlist notification" do + expect(Decidim::EventsManager).to receive(:publish).with(waitlist_notification) + subject.call + end + end + + context "when waitlist is disabled" do + let(:waitlist_enabled) { false } + + before do + create_list(:registration, available_slots, meeting: meeting, status: :registered) + end + + it "broadcasts invalid" do + expect { subject.call }.to broadcast(:invalid) + end + end + + context "when the user is already registered" do + before do + create(:registration, meeting: meeting, user: user, status: :registered) + end + + it "broadcasts invalid" do + expect { subject.call }.to broadcast(:invalid) + end + end + + context "when meeting has available slots" do + it "broadcasts invalid" do + expect { subject.call }.to broadcast(:invalid) + end + end + + context "when the form is invalid" do + before do + create_list(:registration, available_slots, meeting: meeting, status: :registered) + allow(form).to receive(:valid?).and_return(false) + end + + it "broadcasts invalid_form" do + expect { subject.call }.to broadcast(:invalid_form) + end + end + end +end diff --git a/decidim-meetings/spec/controllers/decidim/meetings/registrations_controller_spec.rb b/decidim-meetings/spec/controllers/decidim/meetings/registrations_controller_spec.rb new file mode 100644 index 0000000000000..554df129ec6fb --- /dev/null +++ b/decidim-meetings/spec/controllers/decidim/meetings/registrations_controller_spec.rb @@ -0,0 +1,168 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim::Meetings + describe RegistrationsController do + routes { Decidim::Meetings::Engine.routes } + + let(:organization) { create(:organization) } + let(:user) { create(:user, :confirmed, organization:) } + let(:participatory_process) { create(:participatory_process, organization:) } + let(:component) { create(:meeting_component, participatory_space: participatory_process) } + let(:meeting) { create(:meeting, :published, component:, registrations_enabled: true, available_slots: 10) } + + before do + request.env["decidim.current_organization"] = organization + request.env["decidim.current_participatory_space"] = participatory_process + request.env["decidim.current_component"] = component + end + + describe "POST create" do + let(:params) { { meeting_id: meeting.id } } + + context "when user is authenticated" do + before { sign_in user } + + context "with available slots" do + it "creates registration and redirects" do + expect do + post :create, params: params + end.to change(Registration, :count).by(1) + + expect(flash[:notice]).to eq(I18n.t("registrations.create.success", scope: "decidim.meetings")) + expect(response).to redirect_to(meeting_path(meeting)) + end + end + + context "when no available slots" do + let!(:registrations) { create_list(:registration, 10, meeting: meeting) } + + it "shows error message" do + post :create, params: params + + expect(flash[:alert]).to eq(I18n.t("registrations.create.invalid", scope: "decidim.meetings")) + expect(response).to redirect_to(meeting_path(meeting)) + end + end + end + + context "when user not authenticated" do + it "redirects to login" do + post :create, params: params + expect(response).to redirect_to("/users/sign_in") + end + end + end + + describe "POST answer" do + let(:questionnaire) { create(:questionnaire, :with_questions, questionnaire_for: meeting) } + let(:question) { questionnaire.questions.first } + let(:params) do + { + meeting_id: meeting.id, + waitlist: waitlist, + questionnaire: { + responses: [ + { + body: "Answer", + question_id: question.id + } + ], + tos_agreement: true + } + } + end + let(:waitlist) { false } + + before do + sign_in user + meeting.update!( + registrations_enabled: true, + registration_form_enabled: true, + questionnaire: questionnaire + ) + end + + context "with valid params" do + context "when joining directly" do + it "answers questionnaire and redirects" do + expect do + post :answer, params: params + end.to change { meeting.registrations.count }.by(1) + + expect(flash[:notice]).to eq(I18n.t("registrations.create.success", scope: "decidim.meetings")) + expect(response).to redirect_to(meeting_path(meeting)) + end + end + + context "when joining waitlist" do + let(:meeting) { create(:meeting, component:, available_slots: 10, waitlist_enabled: true) } + let!(:registrations) { create_list(:registration, 10, meeting: meeting) } + let(:waitlist) { true } + + it "adds user to waitlist and redirects" do + expect do + post :answer, params: params + end.to change { meeting.registrations.where(status: :on_waiting_list).count }.by(1) + + expect(flash[:notice]).to eq(I18n.t("registrations.waitlist.success", scope: "decidim.meetings")) + expect(response).to redirect_to(meeting_path(meeting)) + end + end + end + + context "with invalid params" do + let(:params) do + { + meeting_id: meeting.id, + waitlist: false, + questionnaire: { responses: [] } + } + end + + it "shows error message" do + post :answer, params: params + + expect(flash[:alert]).to eq(I18n.t("answer.invalid", scope: "decidim.forms.questionnaires")) + expect(response).to render_template("decidim/forms/questionnaires/show") + end + end + end + + describe "POST join_waitlist" do + let(:meeting) { create(:meeting, component:, available_slots: 10, waitlist_enabled: true) } + let!(:registrations) { create_list(:registration, 10, meeting: meeting) } + let(:params) { { meeting_id: meeting.id } } + + before { sign_in user } + + context "when meeting has no available slots" do + it "adds user to waitlist" do + expect do + post :join_waitlist, params: params + end.to change(Registration.on_waiting_list, :count).by(1) + + expect(flash[:notice]).to eq(I18n.t("registrations.waitlist.success", scope: "decidim.meetings")) + expect(response).to redirect_to(meeting_path(meeting)) + end + end + end + + describe "DELETE destroy" do + let!(:registration) { create(:registration, meeting:, user:) } + let(:params) { { meeting_id: meeting.id } } + + before { sign_in user } + + it "destroys registration" do + expect do + delete :destroy, params: params + end.to change(Registration, :count).by(-1) + + expect(flash[:notice]).to match(/successfully/) + expect(response).to redirect_to(meeting_path(meeting)) + end + end + end +end diff --git a/decidim-meetings/spec/system/meeting_registrations_spec.rb b/decidim-meetings/spec/system/meeting_registrations_spec.rb index 6366ffcf5837f..fd3389986ac96 100644 --- a/decidim-meetings/spec/system/meeting_registrations_spec.rb +++ b/decidim-meetings/spec/system/meeting_registrations_spec.rb @@ -14,6 +14,7 @@ let(:registrations_enabled) { true } let(:registration_form_enabled) { false } let(:available_slots) { 20 } + let(:waitlist_enabled) { true } let(:registration_terms) do { en: "A legal text", @@ -30,6 +31,10 @@ def questionnaire_public_path Decidim::EngineRouter.main_proxy(component).join_meeting_registration_path(meeting_id: meeting.id) end + def questionnaire_waitlist_public_path + Decidim::EngineRouter.main_proxy(component).join_waitlist_meeting_registration_path(meeting_id: meeting.id) + end + def see_questionnaire_questions; end before do @@ -38,6 +43,7 @@ def see_questionnaire_questions; end registrations_enabled:, registration_form_enabled:, available_slots:, + waitlist_enabled:, registration_terms: ) end @@ -52,6 +58,12 @@ def see_questionnaire_questions; end expect(page).to have_no_text("20 slots remaining") end + it "the waitlist button is not visible" do + visit_meeting + + expect(page).to have_no_content("Join the waiting list") + end + context "and registration form is also enabled" do let(:registration_form_enabled) { true } @@ -83,6 +95,14 @@ def see_questionnaire_questions; end expect(page).to have_text("0 slots remaining") end + context "and waitlist is enabled" do + it "the waitlist button is visible" do + visit_meeting + + expect(page).to have_content("Join the waiting list") + end + end + context "and registration form is enabled" do let(:registration_form_enabled) { true } @@ -100,6 +120,15 @@ def see_questionnaire_questions; end expect(page).to have_content("The form is closed and cannot be answered") end + + it "can answer the waitlist form" do + visit questionnaire_waitlist_public_path + + expect(page).to have_i18n_content(questionnaire.title) + expect(page).to have_i18n_content(questionnaire.description, strip_tags: true) + expect(page).to have_i18n_content(question.body) + expect(page).to have_css(".form.answer-questionnaire") + end end end diff --git a/decidim-meetings/spec/system/meeting_waiting_list_spec.rb b/decidim-meetings/spec/system/meeting_waiting_list_spec.rb new file mode 100644 index 0000000000000..fd522ec31a864 --- /dev/null +++ b/decidim-meetings/spec/system/meeting_waiting_list_spec.rb @@ -0,0 +1,257 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Meeting waiting list" do + include_context "with a component" + let(:manifest_name) { "meetings" } + + let!(:questionnaire) { create(:questionnaire) } + let!(:question) { create(:questionnaire_question, questionnaire:, position: 0) } + let!(:meeting) { create(:meeting, :published, component:, questionnaire:, waitlist_enabled:) } + let!(:user) { create(:user, :confirmed, organization:) } + let(:registrations_enabled) { true } + let(:registration_form_enabled) { false } + let(:available_slots) { 10 } + let(:waitlist_enabled) { true } + let(:registration_terms) do + { + en: "A legal text", + es: "Un texto legal", + ca: "Un text legal" + } + end + + def visit_meeting + visit resource_locator(meeting).path + end + + before do + stub_geocoding_coordinates([meeting.latitude, meeting.longitude]) + meeting.update!( + registrations_enabled:, + registration_form_enabled:, + available_slots:, + registration_terms: + ) + end + + context "when the registration form is enabled" do + let(:registration_form_enabled) { true } + + context "when the waitlist is enabled" do + context "when the meeting has available slots" do + before do + visit_meeting + login_as user, scope: :user + end + + it "does not show the join waitlist button" do + expect(page).to have_no_content("Join the waiting list") + end + end + + context "when the meeting has no available slots" do + before do + create_list(:registration, available_slots, meeting: meeting) + end + + context "when the user is not logged in" do + before do + visit_meeting + end + + it "shows the join waitlist button" do + expect(page).to have_content("Join the waiting list") + expect(page).to have_no_content("You have joined the waiting list for this meeting.") + end + + it "shows the login modal when clicking the join waitlist button" do + click_on "Join the waiting list" + expect(page).to have_css("#loginModal", visible: :visible) + expect(page).to have_content("Please log in") + end + end + + context "when the user is logged in" do + before do + login_as user, scope: :user + visit_meeting + end + + it "shows the join waitlist button" do + expect(page).to have_content("Join the waiting list") + end + + it "shows the join waitlist registration form when clicking the join waitlist button" do + click_on "Join the waiting list" + expect(page).to have_i18n_content(questionnaire.title) + expect(page).to have_i18n_content(questionnaire.description, strip_tags: true) + expect(page).to have_i18n_content(question.body) + expect(page).to have_css(".form.answer-questionnaire") + end + + it "can join the waitlist" do + click_on "Join the waiting list" + fill_in question.body["en"], with: "My first answer" + check "questionnaire_tos_agreement" + accept_confirm do + click_on "Submit" + end + expect(page).to have_content("You have joined the waiting list successfully.") + end + end + end + end + + context "when the waitlist is disabled" do + let(:waitlist_enabled) { false } + + context "when the meeting has available slots" do + before do + visit_meeting + login_as user, scope: :user + end + + it "does not show the join waitlist button" do + expect(page).to have_no_content("Join the waiting list") + expect(page).to have_no_content("You have joined the waiting list for this meeting.") + end + end + + context "when the meeting has no available slots" do + before do + create_list(:registration, available_slots, meeting: meeting) + end + + context "when the user is not logged in" do + before do + visit_meeting + end + + it "does not show the join waitlist button" do + expect(page).to have_no_content("Join the waiting list") + expect(page).to have_no_content("You have joined the waiting list for this meeting.") + end + end + + context "when the user is logged in" do + before do + login_as user, scope: :user + visit_meeting + end + + it "does not show the join waitlist button" do + expect(page).to have_no_content("Join the waiting list") + expect(page).to have_no_content("You have joined the waiting list for this meeting.") + end + end + end + end + end + + context "when the registration form is disabled" do + context "when the waitlist is enabled" do + context "when the meeting has available slots" do + before do + visit_meeting + login_as user, scope: :user + end + + it "does not show the join waitlist button" do + expect(page).to have_no_content("Join the waiting list") + expect(page).to have_no_content("You have joined the waiting list for this meeting.") + end + end + + context "when the meeting has no available slots" do + before do + create_list(:registration, available_slots, meeting: meeting) + login_as user, scope: :user + visit_meeting + end + + it "shows the join waitlist button" do + expect(page).to have_content("Join the waiting list") + expect(page).to have_no_content("You have joined the waiting list for this meeting.") + end + + it "can join the waitlist" do + click_on "Join the waiting list" + + within "#meeting-waitlist-confirm-#{meeting.id}" do + expect(page).to have_content "Join the waiting list" + click_on "Confirm" + end + + expect(page).to have_content("You have joined the waiting list successfully.") + expect(page).to have_content("You have joined the waiting list for this meeting.") + end + end + end + + context "when the waitlist is disabled" do + let(:waitlist_enabled) { false } + + context "when the meeting has available slots" do + before do + visit_meeting + login_as user, scope: :user + end + + it "does not show the join waitlist button" do + expect(page).to have_no_content("Join the waiting list") + end + end + + context "when the meeting has no available slots" do + before do + create_list(:registration, available_slots, meeting: meeting) + login_as user, scope: :user + visit_meeting + end + + it "does not show the join waitlist button" do + expect(page).to have_no_content("Join the waiting list") + end + end + end + end + + def leave_meeting(user) + login_as user, scope: :user + visit_meeting + click_on "Cancel your registration" + logout :user + end + + context "when the meeting is full and a user cancels their registration" do + context "and there are users on the waiting list" do + let!(:registrations) { create_list(:registration, available_slots, meeting: meeting) } + let(:users_on_waitlist) { create_list(:user, 5, :confirmed, organization:) } + let(:first_waitlist_user) { users_on_waitlist.first } + + let!(:waitlist_entries) do + users_on_waitlist.map.with_index do |user, index| + create(:registration, meeting: meeting, user: user, status: "on_waiting_list", created_at: Time.current - index.minutes) + end + end + + let(:earliest_waitlist_entry) { waitlist_entries.min_by(&:created_at) } + let(:earliest_waitlist_user) { earliest_waitlist_entry.user } + + before do + perform_enqueued_jobs { Decidim::Meetings::LeaveMeeting.call(meeting, registrations.first.user) } + login_as earliest_waitlist_user, scope: :user + end + + it "displays the registration confirmation" do + visit_meeting + email = last_email + expect(page).to have_content("Your registration code") + expect(email.subject).to eq("Your meeting's registration has been confirmed") + expect(email.to).to eq([earliest_waitlist_user.email]) + end + end + end +end