0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-09 21:45:25 +08:00
discourse/plugins/discourse-calendar/spec/integration/recurrence_spec.rb
Régis Hanol b4643f7151
FEATURE: Better RSVPs on recurring events (#39892)
For recurring events, members can now choose whether their "Going" RSVP
applies to a single upcoming instance or to every following instance in
the series. Previously, a "Going" RSVP silently persisted forever across
all future recurrences with no way to opt out short of leaving the
event.

The recurring `Going` button is now a dropdown with two options:

- "This event" (default): the RSVP applies only to the next occurrence
and is cleared when the recurrence advances.
- "This event and all following events": the RSVP persists across every
future occurrence until the member changes it.

Non-recurring events are unchanged and still expose a plain `Going`
button.

Implementation:

- New `recurring` boolean column on `discourse_post_event_invitees`,
defaulting to false.
- `Invitee#recurring=` coerces to a strict boolean and a `before_save`
callback enforces that only `going` rows can be recurring, so the
invariant is owned by the model.
- `Event#reset_invitee_notifications` (called on recurrence advance) now
also clears `going` rows that are not marked recurring, restoring the
original pre-regression "reset on each occurrence" behavior as the
default while keeping the opt-in persistent mode intact.
- `CreateInvitee` / `UpdateInvitee` services accept a `recurring` param
threaded through from the controller. The `InviteeSerializer` exposes
`recurring` so the UI can reflect the current selection.
- The participant chip and the trigger button swap between the `check`
and `arrows-rotate` icons depending on the invitee's `recurring` flag.

Ref - t/144138
2026-05-18 10:42:09 +02:00

166 lines
4.8 KiB
Ruby
Vendored

# frozen_string_literal: true
describe "discourse_post_event_recurrence" do
let(:user_1) { Fabricate(:user, admin: true) }
let(:topic_1) { Fabricate(:topic, user: user_1) }
let(:post_1) { Fabricate(:post, topic: topic_1) }
let(:starts_at) { Time.zone.parse("2020-09-10 19:00") }
let(:post_event_1) do
Fabricate(
:event,
post: post_1,
original_starts_at: starts_at,
original_ends_at: starts_at + 1.hour,
)
end
before do
freeze_time(starts_at + 1.minute)
SiteSetting.calendar_enabled = true
SiteSetting.discourse_post_event_enabled = true
end
it "delete previous notifications before creating a new one for invites" do
going_user = Fabricate(:user)
DiscoursePostEvent::Invitee.create_attendance!(going_user.id, post_event_1.id, :going)
post_event_1.update!(original_starts_at: starts_at + 10.minutes)
post_event_1.set_next_date
post_event_1.update!(original_starts_at: starts_at - 10.minutes)
post_event_1.set_next_date
expect(
going_user
.notifications
.where(notification_type: Notification.types[:event_invitation])
.count,
).to eq(1)
end
describe "every_month" do
before { post_event_1.update!(recurrence: "every_month") }
it "sets the next month at the same weekday" do
post_event_1.set_next_date
expect(post_event_1.starts_at).to eq_time(Time.zone.parse("2020-10-08 19:00"))
end
end
describe "every_week" do
before { post_event_1.update!(recurrence: "every_week") }
it "sets the next week at the same weekday" do
post_event_1.set_next_date
expect(post_event_1.starts_at).to eq_time(Time.zone.parse("2020-09-17 19:00"))
end
end
describe "every_two_weeks" do
before { post_event_1.update!(recurrence: "every_two_weeks") }
it "sets in two weeks at the same weekday" do
post_event_1.set_next_date
expect(post_event_1.starts_at).to eq_time(Time.zone.parse("2020-09-24 19:00"))
end
end
describe "every_four_weeks" do
before { post_event_1.update!(recurrence: "every_four_weeks") }
it "sets in four weeks at the same weekday" do
post_event_1.set_next_date
expect(post_event_1.starts_at).to eq_time(Time.zone.parse("2020-10-08 19:00"))
end
end
describe "every_day" do
before { post_event_1.update!(recurrence: "every_day") }
it "sets the next day" do
post_event_1.set_next_date
expect(post_event_1.starts_at).to eq_time(Time.zone.parse("2020-09-11 19:00"))
end
end
describe "every_weekday" do
before do
post_event_1.update!(
original_starts_at: Time.zone.parse("2020-09-11 19:00"),
original_ends_at: Time.zone.parse("2020-09-11 19:00") + 1.hour,
recurrence: "every_weekday",
)
end
it "sets the next day" do
freeze_time(post_event_1.original_starts_at + 1.minute)
post_event_1.set_next_date
expect(post_event_1.starts_at).to eq_time(Time.zone.parse("2020-09-14 19:00"))
end
end
context "when the event has a timezone" do
describe "every_month" do
before { post_event_1.update!(recurrence: "every_month", timezone: "America/New_York") }
it "sets the next month at the same weekday" do
freeze_time(starts_at + 1.day)
post_event_1.set_next_date
expect(post_event_1.starts_at).to eq_time(Time.zone.parse("2020-10-08 19:00"))
end
end
end
describe "resetting invitees on recurrence advance" do
fab!(:going_once_user, :user)
fab!(:going_recurring_user, :user)
before { post_event_1.update!(recurrence: "every_week") }
it "clears non-recurring going invitees and keeps recurring ones" do
DiscoursePostEvent::Invitee.create_attendance!(going_once_user.id, post_event_1.id, :going)
DiscoursePostEvent::Invitee.create_attendance!(
going_recurring_user.id,
post_event_1.id,
:going,
recurring: true,
)
post_event_1.set_next_date
once = post_event_1.invitees.find_by(user_id: going_once_user.id)
recurring = post_event_1.invitees.find_by(user_id: going_recurring_user.id)
expect(once.status).to be_nil
expect(recurring.status).to eq(DiscoursePostEvent::Invitee.statuses[:going])
expect(recurring.recurring).to eq(true)
end
end
context "when the recurring event is closed" do
before { post_event_1.update!(recurrence: "every_week", closed: true) }
it "does not generate the next occurrence" do
initial_starts_at = post_event_1.starts_at
post_event_1.set_next_date
expect(post_event_1.starts_at).to eq_time(initial_starts_at)
end
it "does not schedule a topic bump" do
post_event_1.update!(reminders: "bumpTopic.10.minutes")
expect { post_event_1.set_topic_bump }.not_to change {
Jobs::DiscoursePostEventBumpTopic.jobs.size
}
end
end
end