diff --git a/app/services/copy_course.rb b/app/services/copy_course.rb new file mode 100644 index 0000000000..31ff911acb --- /dev/null +++ b/app/services/copy_course.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +#= Copy course from another server dashboard +class CopyCourse + def initialize(url:) + @url = url + end + + def make_copy + @course_data = retrieve_course_data + copy_main_course_data + add_tracked_wikis + @cat_data = retrieve_categories_data + copy_tracked_categories_data + @users_data = retrieve_users_data + copy_users_data + return { course: @course, error: nil } + rescue StandardError => e + return { course: nil, error: e.message } + rescue ActiveRecord::RecordInvalid => e + return { course: nil, error: "Error copying data to db: #{e.message}" } + end + + private + + def copy_main_course_data + # Extract the attributes we want to copy + params_to_copy = %w[school title term description start end subject slug timeline_start + timeline_end type flags] + copied_data = {} + params_to_copy.each { |p| copied_data[p] = @course_data[p] } + @home_wiki = Wiki.get_or_create(language: @course_data['home_wiki']['language'], + project: @course_data['home_wiki']['project']) + copied_data['home_wiki_id'] = @home_wiki.id + copied_data['passcode'] = 'passcode' # set an arbitrary passcode + if copied_data['flags'].key?('update_logs') + copied_data['flags']['update_logs'] = + fix_update_logs_parsing(copied_data['flags']['update_logs']) + end + # Create the course + @course = Course.create!(copied_data) + end + + # When parsing update_logs from flags, keys are set as strings instead of integers + # This causes problems, so we need to force the keys to be integers. + def fix_update_logs_parsing(update_logs) + update_logs.transform_keys(&:to_i) + end + + def add_tracked_wikis + @course_data['wikis'].each do |wiki_hash| + wiki = Wiki.get_or_create(language: wiki_hash['language'], project: wiki_hash['project']) + next if wiki.id == @home_wiki.id # home wiki was automatically added already + @course.wikis << wiki + end + end + + def copy_tracked_categories_data + @cat_data.each do |cat_hash| + wiki = Wiki.get_or_create(language: cat_hash['wiki']['language'], + project: cat_hash['wiki']['project']) + cat = Category.find_or_create_by!( + depth: cat_hash['depth'], + source: cat_hash['source'], + name: cat_hash['name'], + wiki: + ) + @course.categories << cat + end + end + + def copy_users_data + @users_data.each do |user_hash| + user = User.find_or_create_by!(username: user_hash['username']) + CoursesUsers.create!(user_id: user.id, role: user_hash['role'], course_id: @course.id) + end + end + + def get_request(path) + uri = URI(@url + path) + response = Net::HTTP.get_response(uri) + raise "Error getting data from #{uri}" unless response.is_a?(Net::HTTPSuccess) + response + end + + def retrieve_course_data + response = get_request('/course.json') + JSON.parse(response.body)['course'] + end + + def retrieve_categories_data + response = get_request('/categories.json') + JSON.parse(response.body)['course']['categories'] + end + + def retrieve_users_data + response = get_request('/users.json') + JSON.parse(response.body)['course']['users'] + end +end diff --git a/spec/services/copy_course_spec.rb b/spec/services/copy_course_spec.rb new file mode 100644 index 0000000000..049a2bfc0f --- /dev/null +++ b/spec/services/copy_course_spec.rb @@ -0,0 +1,196 @@ +# frozen_string_literal: true +require 'rails_helper' + +describe CopyCourse do + let(:url_base) { 'https://dashboard.wikiedu.org/courses/' } + let(:existent_prod_course_slug) do + 'University_of_South_Carolina/Invertebrate_Zoology_(Spring_2022)' + end + let(:course_url) { url_base + existent_prod_course_slug + '/course.json' } + let(:categories_url) { url_base + existent_prod_course_slug + '/categories.json' } + let(:users_url) { url_base + existent_prod_course_slug + '/users.json' } + let(:course_response_body) do + '{ + "course": { + "id": 15907, + "slug": "University_of_South_Carolina/Invertebrate_Zoology_(Spring_2022)", + "school": "University of South Carolina", + "title": "Invertebrate Zoology", + "term": "Spring 2022", + "start": "2022-01-11T00:00:00.000Z", + "end": "2022-04-30T23:59:59.000Z", + "type": "ClassroomProgramCourse", + "home_wiki": { + "id": 1, + "language": "en", + "project": "wikipedia" + }, + "flags": { + "update_logs": { + "28763": { + "start_time": "2022-05-26T16:13:42.177+00:00", + "end_time": "2022-05-26T16:13:46.648+00:00", + "sentry_tag_uuid": "4c0a1b87-da5a-4e82-8f40-3d560690cdb2", + "error_count": 0 + } + } + }, + "wikis": [ + { + "language": "en", + "project": "wikipedia" + } + ] + } + }' + end + let(:categories_response_body) do + '{ + "course": { + "categories": [ + { + "name": "Category 0", + "depth": 0, + "source": "Source 0", + "wiki": { + "id": 1, + "language": "en", + "project": "wikipedia" + } + } + ] + } + }' + end + let(:users_response_body) do + '{ + "course": { + "users": [ + { + "role": 1, + "id": 28451264, + "username": "Joshua Stone" + }, + { + "role": 4, + "id": 22694295, + "username": "Helaine (Wiki Ed)" + }, + { + "role": 0, + "id": 28515697, + "username": "CharlieJ385" + }, + { + "role": 0, + "id": 28515751, + "username": "Diqi Yan" + } + ] + } + }' + end + let(:subject) do + service = described_class.new(url: url_base + existent_prod_course_slug) + service.make_copy + end + + describe '#make_copy' do + it 'returns an error if /course.json request fails' do + stub_request(:get, course_url) + .to_return(status: 404, body: '', headers: {}) + + result = subject + expect(result[:error]).to eq("Error getting data from #{course_url}") + expect(result[:course]).to be_nil + end + + it 'returns an error if /categories.json request fails' do + # Stub the response to the course request + stub_request(:get, course_url) + .to_return(status: 200, body: course_response_body, headers: {}) + # Stub the response to the categories request + stub_request(:get, categories_url) + .to_return(status: 404, body: '', headers: {}) + + result = subject + expect(result[:error]).to eq("Error getting data from #{categories_url}") + expect(result[:course]).to be_nil + end + + it 'returns an error if /users.json request fails' do + # Stub the response to the course request + stub_request(:get, course_url) + .to_return(status: 200, body: course_response_body, headers: {}) + + # Stub the response to the categories request + stub_request(:get, categories_url) + .to_return(status: 200, body: categories_response_body, headers: {}) + + # Stub the response to the users request + stub_request(:get, users_url) + .to_return(status: 404, body: '', headers: {}) + + result = subject + expect(result[:error]).to eq("Error getting data from #{users_url}") + expect(result[:course]).to be_nil + end + + it 'course, categories, and users are created if no error' do + # Stub the response to the course request + stub_request(:get, course_url) + .to_return(status: 200, body: course_response_body, headers: {}) + + # Stub the response to the categories request + stub_request(:get, categories_url) + .to_return(status: 200, body: categories_response_body, headers: {}) + + # Stub the response to the users request + stub_request(:get, users_url) + .to_return(status: 200, body: users_response_body, headers: {}) + result = subject + + # No error was returned + expect(result[:error]).to be_nil + # Course returned is not nil + expect(result[:course]).not_to be_nil + + # The course was created + expect(Course.exists?(slug: existent_prod_course_slug)).to eq(true) + + # Course users were created + course = Course.find_by(slug: existent_prod_course_slug) + + expect(course.instructors.length).to eq(1) + expect(course.instructors.first.username).to eq('Joshua Stone') + + expect(course.staff.length).to eq(1) + expect(course.staff.first.username).to eq('Helaine (Wiki Ed)') + + expect(course.students.length).to eq(2) + expect(course.students.first.username).to eq('CharlieJ385') + expect(course.students.second.username).to eq('Diqi Yan') + + # Wiki exists + expect(Wiki.exists?(language: 'en', project: 'wikipedia')).to eq(true) + + # Category was created + expect(Category.exists?(name: 'Category 0', depth: 0, source: 'Source 0', + wiki: 1)).to eq(true) + + # Category course was created + expect(course.categories.length).to eq(1) + + # Users were created + expect(User.exists?(username: 'Joshua Stone')).to eq(true) + expect(User.exists?(username: 'Helaine (Wiki Ed)')).to eq(true) + expect(User.exists?(username: 'CharlieJ385')).to eq(true) + expect(User.exists?(username: 'Diqi Yan')).to eq(true) + + # Update logs were correctly created + course.flags['update_logs'].each do |key, _value| + expect(key).to be_a(Integer) + end + end + end +end