<%= cell("decidim/represent_user_group", form) %>
diff --git a/decidim-meetings/app/cells/decidim/meetings/join_waitlist_button/show.erb b/decidim-meetings/app/cells/decidim/meetings/join_waitlist_button/show.erb
new file mode 100644
index 0000000000000..b03f1b92ed6c9
--- /dev/null
+++ b/decidim-meetings/app/cells/decidim/meetings/join_waitlist_button/show.erb
@@ -0,0 +1,16 @@
+<% if model.registration_form_enabled? %>
+ <%= action_authorized_link_to(
+ :join_waitlist,
+ i18n_join_waitlist_text,
+ join_waitlist_meeting_registration_path(model),
+ class: button_classes
+ ) %>
+<% else %>
+ <%= action_authorized_button_to(
+ :join_waitlist,
+ i18n_join_waitlist_text,
+ "#",
+ class: button_classes,
+ data: { open: current_user.present? ? "meeting-waitlist-confirm-#{model.id}" : "loginModal" }
+ ) %>
+<% end %>
diff --git a/decidim-meetings/app/cells/decidim/meetings/join_waitlist_button_cell.rb b/decidim-meetings/app/cells/decidim/meetings/join_waitlist_button_cell.rb
new file mode 100644
index 0000000000000..028c5a5adae4f
--- /dev/null
+++ b/decidim-meetings/app/cells/decidim/meetings/join_waitlist_button_cell.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Decidim
+ module Meetings
+ # This cell renders the button to join a waitlist.
+ class JoinWaitlistButtonCell < Decidim::ViewModel
+ include MeetingCellsHelper
+
+ def show
+ return unless model.waitlist_enabled? && !model.has_available_slots? && model.can_be_joined_by?(current_user)
+ return if model.has_registration_for?(current_user)
+
+ render
+ end
+
+ private
+
+ def current_component
+ model.component
+ end
+
+ def button_classes
+ "button expanded secondary small mt-s"
+ end
+
+ def i18n_join_waitlist_text
+ return if !model.waitlist_enabled? && model.has_available_slots?
+
+ I18n.t("add_to_waitlist", scope: "decidim.meetings.meetings.show")
+ end
+
+ def icon_name
+ "clockwise-line"
+ end
+
+ def registration_form
+ @registration_form ||= Decidim::Meetings::JoinMeetingForm.new
+ end
+ end
+ end
+end
diff --git a/decidim-meetings/app/commands/decidim/meetings/admin/copy_meeting.rb b/decidim-meetings/app/commands/decidim/meetings/admin/copy_meeting.rb
index b683d64b23e57..5764e779ca32b 100644
--- a/decidim-meetings/app/commands/decidim/meetings/admin/copy_meeting.rb
+++ b/decidim-meetings/app/commands/decidim/meetings/admin/copy_meeting.rb
@@ -82,6 +82,7 @@ def fields_from_meeting
available_slots: meeting.available_slots,
registration_terms: meeting.registration_terms,
reserved_slots: meeting.reserved_slots,
+ waitlist_enabled: meeting.waitlist_enabled,
customize_registration_email: meeting.customize_registration_email,
registration_form_enabled: meeting.registration_form_enabled,
registration_email_custom_content: meeting.registration_email_custom_content
diff --git a/decidim-meetings/app/commands/decidim/meetings/admin/update_registrations.rb b/decidim-meetings/app/commands/decidim/meetings/admin/update_registrations.rb
index 987a790a2ccaa..d0e878ed86f4e 100644
--- a/decidim-meetings/app/commands/decidim/meetings/admin/update_registrations.rb
+++ b/decidim-meetings/app/commands/decidim/meetings/admin/update_registrations.rb
@@ -40,6 +40,7 @@ def update_meeting_registrations
meeting.available_slots = form.available_slots
meeting.reserved_slots = form.reserved_slots
meeting.registration_terms = form.registration_terms
+ meeting.waitlist_enabled = form.waitlist_enabled
meeting.customize_registration_email = form.customize_registration_email
meeting.registration_email_custom_content = form.registration_email_custom_content if form.customize_registration_email
end
diff --git a/decidim-meetings/app/commands/decidim/meetings/join_waitlist.rb b/decidim-meetings/app/commands/decidim/meetings/join_waitlist.rb
new file mode 100644
index 0000000000000..eae23785a2c08
--- /dev/null
+++ b/decidim-meetings/app/commands/decidim/meetings/join_waitlist.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+module Decidim
+ module Meetings
+ # This command is executed when the user joins a waitlist for a meeting.
+ class JoinWaitlist < Decidim::Command
+ # Initializes a JoinWaitlist Command.
+ #
+ # meeting - The current instance of the meeting to be joined.
+ # user - The user joining the waitlist.
+ # registration_form - A form object with params; can be a questionnaire.
+ def initialize(meeting, user, registration_form)
+ @meeting = meeting
+ @user = user
+ @user_group = Decidim::UserGroup.find_by(id: registration_form.user_group_id)
+ @registration_form = registration_form
+ end
+
+ # Joins the waitlist for the meeting if valid.
+ #
+ # Broadcasts :ok if successful, :invalid otherwise.
+ def call
+ return broadcast(:invalid) unless can_join_waitlist?
+ return broadcast(:invalid_form) unless registration_form.valid?
+ return broadcast(:invalid) if answer_questionnaire == :invalid
+
+ meeting.with_lock do
+ create_waitlist_entry
+ send_waitlist_notification
+ end
+ follow_meeting
+ broadcast(:ok)
+ end
+
+ private
+
+ attr_reader :meeting, :user, :user_group, :registration, :registration_form
+
+ def can_join_waitlist?
+ meeting.waitlist_enabled? &&
+ !meeting.registrations.exists?(user: user) &&
+ !meeting.has_available_slots?
+ end
+
+ def create_waitlist_entry
+ @registration = Decidim::Meetings::Registration.create!(
+ meeting: meeting,
+ user: user,
+ user_group: user_group,
+ public_participation: registration_form.public_participation,
+ status: :on_waiting_list
+ )
+ end
+
+ def send_waitlist_notification
+ Decidim::EventsManager.publish(
+ event: "decidim.events.meetings.meeting_waitlist_added",
+ event_class: Decidim::Meetings::MeetingRegistrationNotificationEvent,
+ resource: meeting,
+ affected_users: [user]
+ )
+ end
+
+ def answer_questionnaire
+ return unless questionnaire?
+
+ Decidim::Forms::AnswerQuestionnaire.call(registration_form, user, meeting.questionnaire) do
+ on(:ok) do
+ return :valid
+ end
+
+ on(:invalid) do
+ return :invalid
+ end
+ end
+ end
+
+ def questionnaire?
+ registration_form.model_name == "questionnaire"
+ end
+
+ def follow_meeting
+ Decidim::CreateFollow.call(follow_form, user)
+ end
+
+ def follow_form
+ Decidim::FollowForm
+ .from_params(followable_gid: meeting.to_signed_global_id.to_s)
+ .with_context(current_user: user)
+ end
+ end
+ end
+end
diff --git a/decidim-meetings/app/commands/decidim/meetings/leave_meeting.rb b/decidim-meetings/app/commands/decidim/meetings/leave_meeting.rb
index c592030d8e164..5873555f487d3 100644
--- a/decidim-meetings/app/commands/decidim/meetings/leave_meeting.rb
+++ b/decidim-meetings/app/commands/decidim/meetings/leave_meeting.rb
@@ -24,6 +24,7 @@ def call
destroy_registration
destroy_questionnaire_answers
decrement_score
+ move_from_waitlist!
end
broadcast(:ok)
end
@@ -50,6 +51,30 @@ def destroy_questionnaire_answers
def decrement_score
Decidim::Gamification.decrement_score(@user, :attended_meetings)
end
+
+ def send_email_confirmation(registration, user, meeting)
+ Decidim::Meetings::RegistrationMailer.confirmation(user, meeting, registration).deliver_later
+ end
+
+ def move_from_waitlist!
+ return unless @meeting.remaining_slots.positive?
+
+ on_waiting_list_user = @meeting.registrations.on_waiting_list.order(:created_at).first
+ return unless on_waiting_list_user
+
+ on_waiting_list_user.update!(status: :registered)
+ send_email_confirmation(on_waiting_list_user, on_waiting_list_user.user, @meeting)
+
+ Decidim::EventsManager.publish(
+ event: "decidim.events.meetings.meeting_registration_confirmed",
+ event_class: Decidim::Meetings::MeetingRegistrationNotificationEvent,
+ resource: @meeting,
+ affected_users: [on_waiting_list_user.user],
+ extra: {
+ registration_code: on_waiting_list_user.code
+ }
+ )
+ end
end
end
end
diff --git a/decidim-meetings/app/controllers/decidim/meetings/registrations_controller.rb b/decidim-meetings/app/controllers/decidim/meetings/registrations_controller.rb
index 7ce8d6f305cc6..3d72a4a5b682f 100644
--- a/decidim-meetings/app/controllers/decidim/meetings/registrations_controller.rb
+++ b/decidim-meetings/app/controllers/decidim/meetings/registrations_controller.rb
@@ -11,20 +11,23 @@ def answer
@form = form(Decidim::Forms::QuestionnaireForm).from_params(params, session_token: session_token)
- JoinMeeting.call(meeting, current_user, @form) do
+ waitlist = ActiveModel::Type::Boolean.new.cast(params[:waitlist])
+ command = waitlist ? JoinWaitlist : JoinMeeting
+
+ command.call(meeting, current_user, @form) do
on(:ok) do
- flash[:notice] = I18n.t("registrations.create.success", scope: "decidim.meetings")
+ flash[:notice] = I18n.t("registrations.#{waitlist ? "waitlist" : "create"}.success", scope: "decidim.meetings")
redirect_to after_answer_path
end
on(:invalid) do
- flash.now[:alert] = I18n.t("registrations.create.invalid", scope: "decidim.meetings")
- render template: "decidim/forms/questionnaires/show"
+ flash.now[:alert] = I18n.t("registrations.#{waitlist ? "waitlist" : "create"}.invalid", scope: "decidim.meetings")
+ render template: "decidim/forms/questionnaires/show", status: :unprocessable_entity
end
on(:invalid_form) do
flash.now[:alert] = I18n.t("answer.invalid", scope: i18n_flashes_scope)
- render template: "decidim/forms/questionnaires/show"
+ render template: "decidim/forms/questionnaires/show", status: :unprocessable_entity
end
end
end
@@ -47,17 +50,37 @@ def create
end
end
+ def join_waitlist
+ enforce_permission_to(:join_waitlist, :meeting, meeting: meeting)
+
+ @form = JoinMeetingForm.from_params(params).with_context(current_user: current_user)
+
+ JoinWaitlist.call(meeting, current_user, @form) do
+ on(:ok) do
+ flash[:notice] = I18n.t("registrations.waitlist.success", scope: "decidim.meetings")
+ redirect_after_path
+ end
+
+ on(:invalid) do
+ flash.now[:alert] = I18n.t("registrations.waitlist.invalid", scope: "decidim.meetings")
+ redirect_after_path
+ end
+ end
+ end
+
def destroy
enforce_permission_to :leave, :meeting, meeting: meeting
+ status = registration.status
+
LeaveMeeting.call(meeting, current_user) do
on(:ok) do
- flash[:notice] = I18n.t("registrations.destroy.success", scope: "decidim.meetings")
+ flash[:notice] = I18n.t("registrations.destroy.#{status}.success", scope: "decidim.meetings")
redirect_after_path
end
on(:invalid) do
- flash.now[:alert] = I18n.t("registrations.destroy.invalid", scope: "decidim.meetings")
+ flash.now[:alert] = I18n.t("registrations.destroy.#{status}.invalid", scope: "decidim.meetings")
redirect_after_path
end
end
@@ -80,7 +103,9 @@ def decline_invitation
end
def allow_answers?
- meeting.registrations_enabled? && meeting.registration_form_enabled? && meeting.has_available_slots?
+ return false unless meeting.registrations_enabled? && meeting.registration_form_enabled?
+
+ meeting.has_available_slots? || (meeting.waitlist_enabled? && request.path.include?("join_waitlist"))
end
def after_answer_path
@@ -90,7 +115,7 @@ def after_answer_path
# You can implement this method in your controller to change the URL
# where the questionnaire will be submitted.
def update_url
- answer_meeting_registration_path(meeting_id: meeting.id)
+ answer_meeting_registration_path(meeting_id: meeting.id, waitlist: params[:waitlist] || request.path.include?("join_waitlist"))
end
def questionnaire_for
@@ -103,6 +128,10 @@ def meeting
@meeting ||= Meeting.where(component: current_component).find(params[:meeting_id])
end
+ def registration
+ @registration ||= meeting.registrations.find_by(user: current_user)
+ end
+
def redirect_after_path
redirect_to meeting_path(meeting)
end
diff --git a/decidim-meetings/app/forms/decidim/meetings/admin/meeting_registrations_form.rb b/decidim-meetings/app/forms/decidim/meetings/admin/meeting_registrations_form.rb
index dccfb96e12d03..0737722bc587c 100644
--- a/decidim-meetings/app/forms/decidim/meetings/admin/meeting_registrations_form.rb
+++ b/decidim-meetings/app/forms/decidim/meetings/admin/meeting_registrations_form.rb
@@ -14,6 +14,7 @@ class MeetingRegistrationsForm < Decidim::Form
attribute :customize_registration_email, Boolean
attribute :available_slots, Integer
attribute :reserved_slots, Integer
+ attribute :waitlist_enabled, :boolean, default: false
translatable_attribute :registration_terms, String
translatable_attribute :registration_email_custom_content, String
@@ -21,6 +22,7 @@ class MeetingRegistrationsForm < Decidim::Form
validates :registration_terms, translatable_presence: true, if: ->(form) { form.registrations_enabled? }
validates :available_slots, :reserved_slots, presence: true, if: ->(form) { form.registrations_enabled? }
validates :available_slots, numericality: { greater_than_or_equal_to: 0 }, if: ->(form) { form.registrations_enabled? && form.available_slots.present? }
+ validates :waitlist_enabled, inclusion: { in: [true, false] }
validates :reserved_slots, numericality: { greater_than_or_equal_to: 0 }, if: ->(form) { form.registrations_enabled? }
validates :reserved_slots, numericality: { less_than_or_equal_to: :available_slots }, if: lambda { |form|
form.registrations_enabled? &&
diff --git a/decidim-meetings/app/models/decidim/meetings/meeting.rb b/decidim-meetings/app/models/decidim/meetings/meeting.rb
index dc4089cb47bb0..decafb8000b48 100644
--- a/decidim-meetings/app/models/decidim/meetings/meeting.rb
+++ b/decidim-meetings/app/models/decidim/meetings/meeting.rb
@@ -196,11 +196,11 @@ def emendation?
def has_available_slots?
return true if available_slots.zero?
- (available_slots - reserved_slots) > registrations.count
+ (available_slots - reserved_slots) > registrations.registered.count
end
def remaining_slots
- available_slots - reserved_slots - registrations.count
+ available_slots - reserved_slots - registrations.registered.count
end
def has_registration_for?(user)
diff --git a/decidim-meetings/app/models/decidim/meetings/registration.rb b/decidim-meetings/app/models/decidim/meetings/registration.rb
index 090c7b8c2dd5c..a5e647ee68ea6 100644
--- a/decidim-meetings/app/models/decidim/meetings/registration.rb
+++ b/decidim-meetings/app/models/decidim/meetings/registration.rb
@@ -12,11 +12,15 @@ class Registration < Meetings::ApplicationRecord
validates :user, uniqueness: { scope: :meeting }
validates :code, uniqueness: { allow_blank: true, scope: :meeting }
- validates :code, presence: true, on: :create
+ validates :code, presence: true, on: :create, if: -> { status == "registered" }
+
+ enum status: { registered: "registered", on_waiting_list: "on_waiting_list" }
before_validation :generate_code, on: :create
scope :public_participant, -> { where(decidim_user_group_id: nil, public_participation: true) }
+ scope :registered, -> { where(status: :registered) }
+ scope :waiting_list, -> { where(status: :waiting_list).order(:created_at) }
def self.user_collection(user)
where(decidim_user_id: user.id)
diff --git a/decidim-meetings/app/permissions/decidim/meetings/permissions.rb b/decidim-meetings/app/permissions/decidim/meetings/permissions.rb
index 7263fad5c5e35..789891e8b4444 100644
--- a/decidim-meetings/app/permissions/decidim/meetings/permissions.rb
+++ b/decidim-meetings/app/permissions/decidim/meetings/permissions.rb
@@ -26,6 +26,8 @@ def permissions
case permission_action.action
when :join
toggle_allow(can_join_meeting?)
+ when :join_waitlist
+ toggle_allow(can_join_waitlist?)
when :leave
toggle_allow(can_leave_meeting?)
when :decline_invitation
@@ -72,6 +74,13 @@ def can_join_meeting?
authorized?(:join, resource: meeting)
end
+ def can_join_waitlist?
+ meeting.waitlist_enabled? &&
+ !meeting.has_available_slots? &&
+ !meeting.has_registration_for?(user) &&
+ authorized?(:join_waitlist, resource: meeting)
+ end
+
def can_leave_meeting?
meeting.registrations_enabled?
end
diff --git a/decidim-meetings/app/views/decidim/meetings/admin/registrations/_form.html.erb b/decidim-meetings/app/views/decidim/meetings/admin/registrations/_form.html.erb
index ff3ced798e5fb..cb26394516054 100644
--- a/decidim-meetings/app/views/decidim/meetings/admin/registrations/_form.html.erb
+++ b/decidim-meetings/app/views/decidim/meetings/admin/registrations/_form.html.erb
@@ -41,6 +41,11 @@
<%= t(".reserved_slots_help") %>
<%= form.check_box :customize_registration_email, :"data-toggle" => "customize_registration_email-div" %>
diff --git a/decidim-meetings/app/views/decidim/meetings/meetings/show.html.erb b/decidim-meetings/app/views/decidim/meetings/meetings/show.html.erb
index ecf5f9e3e83f5..770a36db54c4f 100644
--- a/decidim-meetings/app/views/decidim/meetings/meetings/show.html.erb
+++ b/decidim-meetings/app/views/decidim/meetings/meetings/show.html.erb
@@ -46,6 +46,9 @@ edit_link(
<%= cell("decidim/date_range", { start: meeting.start_time, end: meeting.end_time }) %>
<%= cell "decidim/meetings/join_meeting_button", meeting, big_button: true, show_remaining_slots: true %>
+ <% if meeting.waitlist_enabled? && !meeting.has_available_slots? && meeting.can_be_joined_by?(current_user) %>
+ <%= cell "decidim/meetings/join_waitlist_button", meeting %>
+ <% end %>
<%= render partial: "decidim/shared/follow_button", locals: { followable: meeting, large: false } %>