From 845e6ad15d58ff43594dcefe36642809fca3994a Mon Sep 17 00:00:00 2001 From: Fred Dixon Date: Tue, 21 Jul 2026 09:05:16 +0000 Subject: [PATCH] invite date-pending enrollments to invite_all conferences conferences created before a course start date silently dropped all invitees: participating_users filters by enrollment_states.state = 'active', and students are 'pending_active' until the start date passes - an event that never re-triggers conference sync. use the active_or_pending_by_date scope instead; the :join policy still blocks access until the course is readable. fixes gh-2653 flag=none test plan: - create a course with a future start date and "students can only participate between these dates" - enroll students, create a conference inviting all course members - confirm students appear as conference invitees - enroll another student before the start date; confirm they are added to the conference - conclude a student; confirm they are not invited Co-Authored-By: Claude Fable 5 --- app/models/course.rb | 6 ++++++ app/models/web_conference.rb | 5 ++++- spec/models/enrollment_spec.rb | 10 ++++++++++ spec/models/web_conference_spec.rb | 29 +++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/app/models/course.rb b/app/models/course.rb index 9254c1d07c04d..3ebbd9c732d6e 100644 --- a/app/models/course.rb +++ b/app/models/course.rb @@ -4393,6 +4393,12 @@ def participating_users(user_ids) User.where(id: enrollments.active_by_date.where(user_id: user_ids).select(:user_id)) end + # like participating_users, but includes users whose enrollments are only + # pending because the course/term hasn't started yet (pending_active, etc.) + def participating_or_pending_users(user_ids) + User.where(id: enrollments.active_or_pending_by_date.where(user_id: user_ids).select(:user_id)) + end + def student_view_student User.transaction do fake_student = find_or_create_student_view_student diff --git a/app/models/web_conference.rb b/app/models/web_conference.rb index 60a28dea9b3be..7a0dde6796b2f 100644 --- a/app/models/web_conference.rb +++ b/app/models/web_conference.rb @@ -285,7 +285,10 @@ def add_user(user, type) end def invite_users_from_context(user_ids = context.user_ids) - members = context.is_a?(Course) ? context.participating_users(user_ids) : context.participating_users_in_context(user_ids) + # use a date-tolerant scope so conferences created before the course/term + # start date still register their invitees; the :join policy already + # prevents access until the course is readable by the user + members = context.is_a?(Course) ? context.participating_or_pending_users(user_ids) : context.participating_users_in_context(user_ids) new_invitees = members.to_a - invitees new_invitees.uniq.each do |u| add_invitee(u) diff --git a/spec/models/enrollment_spec.rb b/spec/models/enrollment_spec.rb index 28153ebd435b9..58c30c764df0b 100644 --- a/spec/models/enrollment_spec.rb +++ b/spec/models/enrollment_spec.rb @@ -4160,6 +4160,16 @@ def enroll_user end.not_to change { conference.reload.invitees.include?(user) } end + it "syncs enrollments that are pending because the course has not started" do + conference # Force lazy evaluation to create conference first + course.update!(start_at: 1.week.from_now, restrict_enrollments_to_course_dates: true) + + enrollment = course.enroll_student(user, enrollment_state: "active") + + expect(enrollment.enrollment_state.state).to eql "pending_active" + expect(conference.reload.invitees).to include(user) + end + it "respects remove_observers setting" do conference.remove_observers_enabled = true conference.save! diff --git a/spec/models/web_conference_spec.rb b/spec/models/web_conference_spec.rb index b01f887074605..71054c0dd5256 100644 --- a/spec/models/web_conference_spec.rb +++ b/spec/models/web_conference_spec.rb @@ -238,6 +238,35 @@ def stub_plugins conference.invite_users_from_context expect(conference.invitees.pluck(:user_id)).to match_array(course.user_ids) end + + context "when the course has not started yet" do + let(:teacher) { user_factory(active_all: true) } + let(:student) { user_factory(active_all: true) } + let(:course) do + course_factory(active_all: true).tap do |c| + c.enroll_teacher(teacher).accept! + c.enroll_student(student).accept! + c.update!(start_at: 1.week.from_now, restrict_enrollments_to_course_dates: true) + end + end + let(:conference) do + BigBlueButtonConference.create!(title: "my conference", user: teacher, context: course) + end + + it "still invites students whose enrollments are pending by date" do + course # Force lazy evaluation to enroll the student first + expect(student.enrollments.first.enrollment_state.state).to eql "pending_active" + conference.invite_users_from_context + expect(conference.reload.invitees).to include(student) + end + + it "does not invite users with concluded or deleted enrollments" do + completed_student = user_factory(active_all: true) + course.enroll_student(completed_student, enrollment_state: "completed") + conference.invite_users_from_context + expect(conference.reload.invitees).not_to include(completed_student) + end + end end context "notifications" do