diff --git a/app/controllers/users/enrollment_controller.rb b/app/controllers/users/enrollment_controller.rb index 3176159621..aae431501b 100644 --- a/app/controllers/users/enrollment_controller.rb +++ b/app/controllers/users/enrollment_controller.rb @@ -104,28 +104,36 @@ def student_role? def remove set_course_and_user return if @user.nil? - # For events controlled by Event Center, only non-student roles - # can be changed on the Dashboard. Student role is handled - # via WikimediaEventCenterController. - if @course.controlled_by_event_center? && student_role? - render json: { message: I18n.t('courses.controlled_by_event_center') }, status: :unauthorized - return - end - @course_user = CoursesUsers.find_by(user: @user, course: @course, - role: enroll_params[:role]) - if @course_user.nil? # This will happen if the user was already removed. - render 'users', formats: :json - return - end + ensure_role_is_authorized { return } + ensure_course_user_exists { return } remove_assignment_templates + make_disenrollment_edits + @course_user.destroy # destroying the course_user also destroys associated Assignments. render 'users', formats: :json update_course_page_and_assignment_talk_templates end + # For events controlled by Event Center, only non-student roles + # can be changed on the Dashboard. Student role is handled + # via WikimediaEventCenterController. + def ensure_role_is_authorized + return unless @course.controlled_by_event_center? && student_role? + render json: { message: I18n.t('courses.controlled_by_event_center') }, status: :unauthorized + yield + end + + def ensure_course_user_exists + @course_user = CoursesUsers.find_by(user: @user, course: @course, + role: enroll_params[:role]) + return unless @course_user.nil? # This will happen if the user was already removed. + render 'users', formats: :json + yield + end + # If the user has Assignments, update article talk pages to remove them from # the assignment templates. def remove_assignment_templates @@ -136,6 +144,15 @@ def remove_assignment_templates end end + # Remove enrollment templates from user page and user talk page. + def make_disenrollment_edits + return unless student_role? + # for students only, remove templates from userpage and user talk page + DisenrollFromCourseWorker.schedule_edits(course: @course, + editing_user: current_user, + disenrolling_user: @user) + end + ################## # Finding a user # ################## diff --git a/app/workers/disenroll_from_course_worker.rb b/app/workers/disenroll_from_course_worker.rb new file mode 100644 index 0000000000..aadb644fc8 --- /dev/null +++ b/app/workers/disenroll_from_course_worker.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require_dependency "#{Rails.root}/lib/wiki_course_edits" + +class DisenrollFromCourseWorker + include Sidekiq::Worker + sidekiq_options lock: :until_executed + + def self.schedule_edits(course:, editing_user:, disenrolling_user:) + perform_async(course.id, editing_user.id, disenrolling_user.id) + end + + def perform(course_id, editing_user_id, disenrolling_user_id) + course = Course.find(course_id) + editing_user = User.find(editing_user_id) + disenrolling_user = User.find(disenrolling_user_id) + WikiCourseEdits.new(action: :disenroll_from_course, + course:, + current_user: editing_user, + disenrolling_user:) + WikiCourseEdits.new(action: :update_course, + course:, + current_user: editing_user) + end +end diff --git a/lib/wiki_course_edits.rb b/lib/wiki_course_edits.rb index 2d317f5cf7..0b00d61022 100644 --- a/lib/wiki_course_edits.rb +++ b/lib/wiki_course_edits.rb @@ -70,6 +70,25 @@ def enroll_in_course(enrolling_user:) add_template_to_sandbox end + # Removes existing template from the disenrolling student's userpage, and + # also from their talk page + def disenroll_from_course(disenrolling_user:) + generator = WikiUserpageOutput.new(@course) + + @disenrolling_user = disenrolling_user + # Remove existing template from the user page + template = generator.enrollment_template + user_page = "User:#{@disenrolling_user.username}" + summary = generator.disenrollment_summary + remove_content_from_page(user_page, template, summary) + + # Remove existing template from the user's talk page + talk_template = generator.enrollment_talk_template + talk_page = "User_talk:#{disenrolling_user.username}" + talk_summary = "removing {{#{template_name(@templates, 'user_talk')}}}" + remove_content_from_page(talk_page, talk_template, talk_summary) + end + # Updates the assignment template for every Assignment for the course. # Usually, this is done incrementally so that a call to this method will only # update the assignments that were changed in the action that triggered it. @@ -114,7 +133,7 @@ def validate(action) yield unless @course.wiki_course_page_enabled? when :update_assignments, :remove_assignment yield unless @course.assignment_edits_enabled? - when :enroll_in_course + when :enroll_in_course, :disenroll_from_course yield unless @course.enrollment_edits_enabled? end end @@ -163,6 +182,16 @@ def add_template_to_sandbox sandbox_template:, current_user: @current_user) end + def remove_content_from_page(page_title, content, summary) + initial_page_content = @wiki_api.get_page_content(page_title) + + # If content to remove does not exist on initial page, then there is nothing + # to remove + return unless initial_page_content&.include?(content) + new_page_content = initial_page_content.gsub(/#{Regexp.quote(content)}(\n)?/, '') + @wiki_editor.post_whole_page(@current_user, page_title, new_page_content, summary) + end + def repost_with_sanitized_links(wiki_title, wiki_text, summary, spamlist) bad_links = spamlist['matches'] bad_links = bad_links.values if bad_links.is_a?(Hash) diff --git a/lib/wiki_userpage_output.rb b/lib/wiki_userpage_output.rb index 7e812a318d..104b02ca53 100644 --- a/lib/wiki_userpage_output.rb +++ b/lib/wiki_userpage_output.rb @@ -39,6 +39,15 @@ def sandbox_template(dashboard_url) "{{#{dashboard_url} sandbox#{course_type_param}}}" end + def disenrollment_summary + case @course.type + when 'FellowsCohort' + "User is no longer participating in #{@course.slug}." + else + "User has disenrolled in [[#{@course.wiki_title}]]." + end + end + private def course_page_param diff --git a/spec/lib/wiki_course_edits_spec.rb b/spec/lib/wiki_course_edits_spec.rb index 5e609b5aa3..d6c14b2f22 100644 --- a/spec/lib/wiki_course_edits_spec.rb +++ b/spec/lib/wiki_course_edits_spec.rb @@ -9,15 +9,22 @@ let(:course) { create(:course, id: 1, submitted: true, home_wiki:, slug:) } let(:user) { create(:user) } let(:enrolling_user) { create(:user, username: 'Belajane41') } + let(:disenrolling_user) { create(:user, username: 'Belajane41') } # rubocop:disable Layout/LineLength let(:user_page_content) do - '{{dashboard.wikiedu.org student editor | course = [[Wikipedia:Wiki_Ed/Missouri_SandT/History_of_Science_(Fall_2019)]] | slug = Missouri_SandT/History_of_Science_(Fall_2019) }}' + "{{dashboard.wikiedu.org student editor | course = [[Wikipedia:Wiki_Ed/Missouri_SandT/History_of_Science_(Fall_2019)]] | slug = Missouri_SandT/History_of_Science_(Fall_2019) }}\nAny other user page content" + end + let(:user_page_talk_content) do + "{{dashboard.wikiedu.org talk course link | course = [[Wikipedia:Wiki_Ed/Missouri_SandT/History_of_Science_(Fall_2019)]] | slug = Missouri_SandT/History_of_Science_(Fall_2019) }}\nAny other user talk page content" end # rubocop:enable Layout/LineLength let(:user_template) { WikiUserpageOutput.new(course).enrollment_template } let(:talk_template) { WikiUserpageOutput.new(course).enrollment_talk_template } let(:sandbox_template) { WikiUserpageOutput.new(course).sandbox_template(ENV['dashboard_url']) } + let(:user_page_content_without_enrollment) { '{{a user page content}}' } + let(:user_talk_page_content_without_enrollment) { '{{a user talk page content}}' } + before do stub_oauth_edit end @@ -217,6 +224,73 @@ end end + describe '#disenroll_from_course' do + it 'respects the enrollment_edits_enabled edit_settings flag' do + course.update(flags: { 'edit_settings' => { 'enrollment_edits_enabled' => false } }) + allow_any_instance_of(WikiApi).to receive(:get_page_content).and_return( + user_page_content, + user_page_talk_content + ) + expect_any_instance_of(WikiEdits).not_to receive(:post_whole_page) + described_class.new(action: :disenroll_from_course, + course:, + current_user: user, + disenrolling_user:) + end + + it 'does nothing if get_page_content returns nil' do + allow_any_instance_of(WikiApi).to receive(:get_page_content).and_return(nil) + expect_any_instance_of(WikiEdits).not_to receive(:post_whole_page) + described_class.new(action: :disenroll_from_course, + course:, + current_user: user, + disenrolling_user:) + end + + it 'does nothing if page content does not include templates' do + allow_any_instance_of(WikiApi).to receive(:get_page_content).and_return( + user_page_content_without_enrollment, + user_talk_page_content_without_enrollment + ) + expect_any_instance_of(WikiEdits).not_to receive(:post_whole_page) + described_class.new(action: :disenroll_from_course, + course:, + current_user: user, + disenrolling_user:) + end + + it 'removes enrollment template from user page if it exists' do + allow_any_instance_of(WikiApi).to receive(:get_page_content).and_return( + user_page_content, + user_talk_page_content_without_enrollment + ) + expect_any_instance_of(WikiEdits).to receive(:post_whole_page).with( + user, 'User:Belajane41', 'Any other user page content', + 'User has disenrolled in [[Wikipedia:Wiki_Ed/Missouri_SandT/'\ + 'History_of_Science_(Fall_2019)]].' + ) + described_class.new(action: :disenroll_from_course, + course:, + current_user: user, + disenrolling_user:) + end + + it 'removes enrollment template from user talk page if it exists' do + allow_any_instance_of(WikiApi).to receive(:get_page_content).and_return( + user_page_content_without_enrollment, + user_page_talk_content + ) + expect_any_instance_of(WikiEdits).to receive(:post_whole_page).with( + user, 'User_talk:Belajane41', 'Any other user talk page content', + 'removing {{dashboard.wikiedu.org talk course link}}' + ) + described_class.new(action: :disenroll_from_course, + course:, + current_user: user, + disenrolling_user:) + end + end + describe '#update_assignments' do let(:instructor) { create(:user, username: 'Instructor') } let(:selfie) { create(:article, title: 'Selfie') }