diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8777124c0..2f359a147 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v2 - with: + with: submodules: false - name: Setup git submodules run: | @@ -29,7 +29,7 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v2 - with: + with: submodules: false - name: Setup git submodules run: | @@ -47,7 +47,7 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v2 - with: + with: submodules: false - name: Setup git submodules run: | @@ -65,7 +65,7 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v2 - with: + with: submodules: false - name: Setup git submodules run: | @@ -83,7 +83,7 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v2 - with: + with: submodules: false - name: Setup git submodules run: | @@ -101,7 +101,7 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v2 - with: + with: submodules: false - name: Setup git submodules run: | @@ -112,4 +112,4 @@ jobs: - name: Setup database & test env run: docker compose run web bin/rake db:create db:schema:load RAILS_ENV=test - name: Run spec_integration tests - run: docker compose run web bin/spec_integration \ No newline at end of file + run: docker compose run web bin/spec_integration diff --git a/Gemfile b/Gemfile index 7b413d3cb..5b82c5eef 100644 --- a/Gemfile +++ b/Gemfile @@ -13,9 +13,9 @@ gem 'bootstrap', '~> 4.6' # Held back to 4.6 gem 'cancancan', '~> 3.6', '>= 3.6.1' gem 'doorkeeper', '~> 5.7', '>= 5.7.1' gem 'doorkeeper-openid_connect', '~> 1.8', '>= 1.8.9' +gem 'faraday', '~> 2.14' gem 'file_validators', '~> 3.0' # Used to validate organization logo gem 'font-awesome-rails', '~> 4.7', '>= 4.7.0.8' -gem 'google_drive', '~> 3.0', '>= 3.0.7', require: false gem 'gravtastic', '~> 3.2', '>= 3.2.6' # Used to display user avatars gem 'image_processing', '~> 1.12', '>= 1.12.2' # Used by active_storage to make logo 100x100 on the fly gem 'logstasher', '~> 2.1', '>= 2.1.5' diff --git a/Gemfile.lock b/Gemfile.lock index 8f3137e0e..91fee7c49 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -130,7 +130,6 @@ GEM database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) date (3.3.4) - declarative (0.0.20) diff-lcs (1.5.1) docile (1.4.1) domain_name (0.6.20240107) @@ -148,29 +147,12 @@ GEM factory_bot_rails (6.4.3) factory_bot (~> 6.4) railties (>= 5.0.0) - faraday (1.10.3) - faraday-em_http (~> 1.0) - faraday-em_synchrony (~> 1.0) - faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0) - faraday-multipart (~> 1.0) - faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.0) - faraday-patron (~> 1.0) - faraday-rack (~> 1.0) - faraday-retry (~> 1.0) - ruby2_keywords (>= 0.0.4) - faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.0) - faraday-excon (1.1.0) - faraday-httpclient (1.0.1) - faraday-multipart (1.0.4) - multipart-post (~> 2) - faraday-net_http (1.0.2) - faraday-net_http_persistent (1.2.0) - faraday-patron (1.0.0) - faraday-rack (1.0.0) - faraday-retry (1.0.3) + faraday (2.14.0) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-net_http (3.4.2) + net-http (~> 0.5) ffi (1.17.0) ffi-compiler (1.3.2) ffi (>= 1.15.5) @@ -182,36 +164,11 @@ GEM railties (>= 3.2, < 8.0) globalid (1.2.1) activesupport (>= 6.1) - google-apis-core (0.11.3) - addressable (~> 2.5, >= 2.5.1) - googleauth (>= 0.16.2, < 2.a) - httpclient (>= 2.8.1, < 3.a) - mini_mime (~> 1.0) - representable (~> 3.0) - retriable (>= 2.0, < 4.a) - rexml - google-apis-drive_v3 (0.46.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-sheets_v4 (0.26.0) - google-apis-core (>= 0.11.0, < 2.a) - google_drive (3.0.7) - google-apis-drive_v3 (>= 0.5.0, < 1.0.0) - google-apis-sheets_v4 (>= 0.4.0, < 1.0.0) - googleauth (>= 0.5.0, < 1.0.0) - nokogiri (>= 1.5.3, < 2.0.0) - googleauth (0.17.1) - faraday (>= 0.17.3, < 2.0) - jwt (>= 1.4, < 3.0) - memoist (~> 0.16) - multi_json (~> 1.11) - os (>= 0.9, < 2.0) - signet (~> 0.15) gravtastic (3.2.6) hiredis (0.6.3) http-accept (1.7.0) http-cookie (1.0.6) domain_name (~> 0.5) - httpclient (2.8.3) i18n (1.14.5) concurrent-ruby (~> 1.0) image_processing (1.13.0) @@ -250,7 +207,6 @@ GEM net-smtp marcel (1.0.4) matrix (0.4.2) - memoist (0.16.2) method_source (1.1.0) mime-types (3.5.2) mime-types-data (~> 3.2015) @@ -265,11 +221,12 @@ GEM mini_mime (1.1.5) minitest (5.24.1) multi_json (1.15.0) - multipart-post (2.4.1) mustermann (3.0.0) ruby2_keywords (~> 0.0.1) mutex_m (0.2.0) natcmp (1.4.3) + net-http (0.8.0) + uri (>= 0.11.1) net-imap (0.4.14) date net-protocol @@ -294,7 +251,6 @@ GEM racc (~> 1.4) nokogiri (1.16.7-x86_64-linux) racc (~> 1.4) - os (1.1.4) parallel (1.25.1) parser (3.3.4.0) ast (~> 2.4.1) @@ -386,10 +342,6 @@ GEM regexp_parser (2.9.2) reline (0.5.9) io-console (~> 0.5) - representable (3.2.0) - declarative (< 0.1.0) - trailblazer-option (>= 0.1.1, < 0.2.0) - uber (< 0.2.0) request_store (1.7.0) rack (>= 1.4) responders (3.1.1) @@ -400,7 +352,6 @@ GEM http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 4.0) netrc (~> 0.8) - retriable (3.1.2) rexml (3.3.2) strscan rspec (3.13.0) @@ -478,11 +429,6 @@ GEM sprockets (> 3.0) sprockets-rails tilt - signet (0.19.0) - addressable (~> 2.8) - faraday (>= 0.17.5, < 3.a) - jwt (>= 1.5, < 3.0) - multi_json (~> 1.10) simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) @@ -511,13 +457,12 @@ GEM thor (1.3.1) tilt (2.4.0) timeout (0.4.1) - trailblazer-option (0.1.2) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - uber (0.1.0) uglifier (4.2.0) execjs (>= 0.3.0, < 3) unicode-display_width (2.5.0) + uri (1.1.1) webrick (1.8.1) websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) @@ -557,9 +502,9 @@ DEPENDENCIES doorkeeper (~> 5.7, >= 5.7.1) doorkeeper-openid_connect (~> 1.8, >= 1.8.9) factory_bot_rails (~> 6.4, >= 6.4.3) + faraday (~> 2.14) file_validators (~> 3.0) font-awesome-rails (~> 4.7, >= 4.7.0.8) - google_drive (~> 3.0, >= 3.0.7) gravtastic (~> 3.2, >= 3.2.6) hiredis (~> 0.6.3) image_processing (~> 1.12, >= 1.12.2) diff --git a/app/controllers/api/v8/apidocs_controller.rb b/app/controllers/api/v8/apidocs_controller.rb index e06e3e909..ff3dfc918 100644 --- a/app/controllers/api/v8/apidocs_controller.rb +++ b/app/controllers/api/v8/apidocs_controller.rb @@ -62,6 +62,13 @@ class ApidocsController < ActionController::Base key :required, true key :type, :integer end + parameter :path_user_email do + key :name, :user_email + key :in, :path + key :description, "User's email" + key :required, true + key :type, :string + end parameter :path_exercise_id do key :name, :exercise_id key :in, :path diff --git a/app/controllers/api/v8/users_controller.rb b/app/controllers/api/v8/users_controller.rb index 88c129160..ec0357d57 100644 --- a/app/controllers/api/v8/users_controller.rb +++ b/app/controllers/api/v8/users_controller.rb @@ -56,6 +56,54 @@ class UsersController < Api::V8::BaseController end end + swagger_path '/api/v8/users/{user_id}/set_password_managed_by_courses_mooc_fi' do + operation :post do + key :description, 'Sets the boolean password_managed_by_courses_mooc_fi for the user with the given id to true.' + key :operationId, 'setPasswordManagedByCoursesMoocFi' + key :produces, ['application/json'] + key :tags, ['user'] + parameter '$ref': '#/parameters/user_id' + response 403, '$ref': '#/responses/error' + response 404, '$ref': '#/responses/error' + response 200 do + key :description, "status 'ok' and sets the boolean password_managed_by_courses_mooc_fi to true" + schema do + key :title, :status + key :required, [:status] + property :status, type: :string, example: 'Password managed by courses.mooc.fi set to true and password deleted.' + end + end + end + end + + swagger_path '/api/v8/users/get_user_with_email?email={email}' do + operation :get do + key :description, "Returns the user's id as upstream_id, user's courses.mooc.fi-id as id, email, first name and last name by user email" + key :operationId, 'getUserInformationByEmail' + key :produces, ['application/json'] + key :tags, ['user'] + parameter '$ref': '#/parameters/user_email' + response 403, '$ref': '#/responses/error' + response 404, '$ref': '#/responses/error' + response 200 do + key :description, "User's courses.mooc.fi-id as id, email, first name, last name and id as upstream_id as json" + key :content, 'application/json' + schema do + key :title, :user + key :required, [:user] + key :type, :object + property :id, type: :string, description: "User's courses.mooc.fi user id", example: 'ABCD1234-5678-EFGH-IJ90-ABC123DEF456' + property :email, type: :string, description: 'User email', example: 'user@example.com' + property :first_name, type: :string, description: 'User first name', example: 'John' + property :last_name, type: :string, description: 'User last name', example: 'Doe' + property :upstream_id, type: :integer, description: "User's user id in TMC database", example: 123 + end + end + end + end + + skip_authorization_check only: %i[set_password_managed_by_courses_mooc_fi] + def show unauthorize_guest! if current_user.guest? user = current_user @@ -103,19 +151,13 @@ def create set_extra_data if BannedEmail.banned?(@user.email) - return render json: { - success: true, - message: 'User created.' - } + return render json: build_success_response(params[:include_id]) end if @user.errors.empty? && @user.save # TODO: Whitelist origins UserMailer.email_confirmation(@user, params[:origin], params[:language]).deliver_now - render json: { - success: true, - message: 'User created.' - } + render json: build_success_response(params[:include_id]) else errors = @user.errors errors[:username] = errors.delete(:login) if errors.key?(:login) @@ -149,6 +191,67 @@ def update }, status: :bad_request end + def destroy + unauthorize_guest! if current_user.guest? + + user = User.find(params[:id]) + authorize! :destroy, user + + User.transaction do + if user.destroy + RecentlyChangedUserDetail.where(username: user.login).delete_all + RecentlyChangedUserDetail.deleted.create!( + new_value: true, + email: user.email, + username: user.login, + user_id: user.id + ) + Doorkeeper::AccessToken.where(resource_owner_id: user.id).delete_all + + render json: { success: true, message: 'User deleted.' } + else + render json: { success: false, errors: user.errors }, status: :bad_request + end + end + end + + def set_password_managed_by_courses_mooc_fi + only_admins! + + User.transaction do + user = User.find_by!(id: params[:id]) + user.password_managed_by_courses_mooc_fi = true + user.password_hash = nil + user.salt = nil + user.argon_hash = nil + user.courses_mooc_fi_user_id = params[:courses_mooc_fi_user_id] + raise ActiveRecord::Rollback if !user.errors.empty? || !user.save + return render json: { + status: 'Password managed by courses.mooc.fi set to true and password deleted.' + } + end + render json: { + errors: @user.errors + }, status: :bad_request + end + + def get_user_with_email + unauthorize_guest! if current_user.guest? + + user = User.find_by!(email: params[:email]) + authorize! :read, user + + name = UserFieldValue.where(user_id: user.id, field_name: ['first_name', 'last_name']).pluck(:field_name, :value).to_h + + render json: { + id: user.courses_mooc_fi_user_id, + email: user.email, + first_name: name['first_name'], + last_name: name['last_name'], + upstream_id: user.id, + } + end + private def set_email user_params = params[:user] @@ -189,6 +292,10 @@ def set_password end def maybe_update_password + if @user.password_managed_by_courses_mooc_fi && @user.courses_mooc_fi_user_id.present? + return @user.update_password_via_courses_mooc_fi(@user.courses_mooc_fi_user_id, user_params[:old_password], user_params[:password]) + end + if params[:old_password].present? && params[:password].present? if !@user.has_password?(params[:old_password]) @user.errors.add(:old_password, 'incorrect') @@ -229,6 +336,17 @@ def set_extra_data(eager_save = false) datum.save! if eager_save end end + + def build_success_response(include_id = false) + response = { + success: true, + message: 'User created.', + } + if include_id + response[:id] = @user.id + end + response + end end end end diff --git a/app/controllers/password_reset_keys_controller.rb b/app/controllers/password_reset_keys_controller.rb index 5ab403593..dfeff5906 100644 --- a/app/controllers/password_reset_keys_controller.rb +++ b/app/controllers/password_reset_keys_controller.rb @@ -39,18 +39,29 @@ def destroy return render action: :show, status: :forbidden end - @user.password = params[:password] - if @user.save - @key.destroy - flash[:success] = 'Your password has been reset.' - redirect_to root_path + if @user.password_managed_by_courses_mooc_fi + success = @user.update_password_via_courses_mooc_fi(nil, params[:password]) + if success + @key.destroy + flash[:success] = 'Your password has been reset.' + redirect_to root_path + else + 'Failed to reset password.' + end else - flash.now[:alert] = if @user.errors[:password] - 'Password ' + @user.errors[:password].join(', ') + @user.password = params[:password] + if @user.save + @key.destroy + flash[:success] = 'Your password has been reset.' + redirect_to root_path else - 'Failed to set password' + flash.now[:alert] = if @user.errors[:password] + 'Password ' + @user.errors[:password].join(', ') + else + 'Failed to set password' + end + render action: :show, status: :forbidden end - render action: :show, status: :forbidden end end diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb index 1915ac4c9..21e8f2801 100644 --- a/app/controllers/settings_controller.rb +++ b/app/controllers/settings_controller.rb @@ -86,6 +86,10 @@ def set_email end def maybe_update_password(user, user_params) + if user.password_managed_by_courses_mooc_fi && user.courses_mooc_fi_user_id.present? + return user.update_password_via_courses_mooc_fi(user.courses_mooc_fi_user_id, user_params[:old_password], user_params[:password]) + end + if user_params[:old_password].present? || user_params[:password].present? if !user.has_password?(user_params[:old_password]) user.errors.add(:old_password, 'incorrect') diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index d7e2246d4..be012b747 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -179,6 +179,10 @@ def set_password end def maybe_update_password(user, user_params) + if user.password_managed_by_courses_mooc_fi && user.courses_mooc_fi_user_id.present? + return user.update_password_via_courses_mooc_fi(user.courses_mooc_fi_user_id, user_params[:old_password], user_params[:password]) + end + if user_params[:old_password].present? || user_params[:password].present? if !user.has_password?(user_params[:old_password]) user.errors.add(:old_password, 'incorrect') diff --git a/app/helpers/points_helper.rb b/app/helpers/points_helper.rb index fdc0ee595..91348bbee 100644 --- a/app/helpers/points_helper.rb +++ b/app/helpers/points_helper.rb @@ -9,19 +9,6 @@ def github_repo_url_to_project_page_url(url) end end - def gdocs_notifications(notifications) - ret = "' - ret - end - def points_list(points) points.to_a.natsort.map { |pt| h(pt) }.join('  ').html_safe end diff --git a/app/models/course.rb b/app/models/course.rb index 5669e13ae..e50a89ca2 100644 --- a/app/models/course.rb +++ b/app/models/course.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require 'gdocs_export' require 'system_commands' require 'date_and_time_utils' @@ -292,10 +291,6 @@ def gdocs_sheets(exercises = nil) exercises.map(&:gdocs_sheet).reject(&:nil?).uniq end - def refresh_gdocs_worksheet(sheetname) - GDocsExport.refresh_course_worksheet_points self, sheetname - end - def self.cache_root "#{FileStore.root}/course" end diff --git a/app/models/user.rb b/app/models/user.rb index 721e5c041..14b451fc2 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -146,9 +146,101 @@ def self.authenticate(login, submitted_password) user = find_by(login: login) user ||= find_by('lower(email) = ?', login.downcase) return nil if user.nil? + + if user.password_managed_by_courses_mooc_fi && user.courses_mooc_fi_user_id.present? + return user if user.authenticate_via_courses_mooc_fi(submitted_password) + return nil + end + user if user.has_password?(submitted_password) end + + def authenticate_via_courses_mooc_fi(submitted_password) + auth_url = SiteSetting.value('courses_mooc_fi_auth_url') + + conn = Faraday.new do |f| + f.request :json + f.response :json + end + + response = conn.post(auth_url) do |req| + req.headers['Content-Type'] = 'application/json' + req.headers['Accept'] = 'application/json' + req.headers['Authorization'] = Base64.decode64( + Rails.application.secrets.tmc_server_secret_for_communicating_to_secret_project + ) + + req.body = { + user_id: courses_mooc_fi_user_id, + password: submitted_password + } + end + + response.body == true + + rescue Faraday::ClientError => e + status = e.response&.dig(:status) + + if status == 401 || status == 403 + return false + end + + Rails.logger.error("Authentication via courses.mooc.fi error: #{e.response}") + raise + + rescue => e + Rails.logger.error("Unexpected error during authentication via courses.mooc.fi: #{e.message}") + raise + end + + + + def update_password_via_courses_mooc_fi(old_password, new_password) + update_url = SiteSetting.value('courses_mooc_fi_update_password_url') + + conn = Faraday.new do |f| + f.request :json + f.response :json + end + + begin + response = conn.post(update_url) do |req| + req.headers['Content-Type'] = 'application/json' + req.headers['Accept'] = 'application/json' + req.headers['Authorization'] = Base64.decode64( + Rails.application.secrets.tmc_server_secret_for_communicating_to_secret_project + ) + + req.body = { + user_id: self.courses_mooc_fi_user_id, + old_password: old_password, + new_password: new_password + } + end + + data = response.body + + unless data == true + raise "Updating password via courses.mooc.fi failed for user with courses.mooc.fi-user-id #{self.courses_mooc_fi_user_id}" + end + + true + + rescue Faraday::ClientError => e + Rails.logger.error( + "Updating password via courses.mooc.fi failed for user with courses.mooc.fi-user-id #{self.courses_mooc_fi_user_id}: #{e.response}" + ) + false + + rescue => e + Rails.logger.error( + "Unexpected error updating password via courses.mooc.fi for user with courses.mooc.fi-user-id #{self.courses_mooc_fi_user_id}: #{e.message}" + ) + false + end + end + def password_reset_key action_tokens.find { |t| t.action == 'reset_password' } end diff --git a/app/views/points/refresh_gdocs.html.erb b/app/views/points/refresh_gdocs.html.erb deleted file mode 100644 index 6f9e66d26..000000000 --- a/app/views/points/refresh_gdocs.html.erb +++ /dev/null @@ -1,7 +0,0 @@ -

Refreshed Google Docs worksheet

-<%= link_to 'back to worksheet points', course_point_path(@course) %> -<% unless @notifications.empty? %> -

Notifications:

- <%= raw(gdocs_notifications(@notifications)) %> -<% end %> - diff --git a/app/views/points/show.html.erb b/app/views/points/show.html.erb index c146475e0..b1c37836a 100644 --- a/app/views/points/show.html.erb +++ b/app/views/points/show.html.erb @@ -10,10 +10,6 @@ <% else %> <%= link_to('Sort by default order', organization_course_point_path(@organization, @course, @sheetname, show_attempted: params[:show_attempted]), class: "btn btn-primary") %> <% end %> - - <% if can? :refresh_gdocs_spreadsheet, @course %> - <% link_to 'Refresh Google Docs worksheet', refresh_gdocs_organization_course_point_path(@organization, @course, @sheetname), class: "btn btn-primary" %> - <% end %> diff --git a/config/initializers/gdocs_config.rb.example b/config/initializers/gdocs_config.rb.example deleted file mode 100644 index ef913419e..000000000 --- a/config/initializers/gdocs_config.rb.example +++ /dev/null @@ -1,4 +0,0 @@ -#TODO: move to config/site.yml -#TmcServer::Application.config.gdocs_username = 'username@gmail.com' -#TmcServer::Application.config.gdocs_password = 'password' - diff --git a/config/routes.rb b/config/routes.rb index f3c7c23b1..77da25dff 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -55,10 +55,12 @@ resources :recently_changed_user_details, only: :index end - resources :users, only: %i[show create update] do + resources :users, only: %i[show create update destroy] do resources :request_deletion, only: [:create], module: :users resources :assistantships, module: :users, only: :index resources :teacherships, module: :users, only: :index + post :set_password_managed_by_courses_mooc_fi, on: :member + get :get_user_with_email, on: :collection end resources :user_app_datum, only: [:index] @@ -201,11 +203,7 @@ post 'toggle_submission_result_visibility' end - resources :points, only: %i[index show] do - member do - get 'refresh_gdocs' - end - end + resources :points, only: %i[index show] resources :exercises, only: [:index] do collection do diff --git a/db/migrate/20250711214850_add_password_managed_by_courses_mooc_fi_to_users.rb b/db/migrate/20250711214850_add_password_managed_by_courses_mooc_fi_to_users.rb new file mode 100644 index 000000000..0f70f37af --- /dev/null +++ b/db/migrate/20250711214850_add_password_managed_by_courses_mooc_fi_to_users.rb @@ -0,0 +1,5 @@ +class AddPasswordManagedByCoursesMoocFiToUsers < ActiveRecord::Migration[7.1] + def change + add_column :users, :password_managed_by_courses_mooc_fi, :boolean, default: false, null: false + end +end diff --git a/db/migrate/20250723122117_add_courses_mooc_fi_user_id_to_users.rb b/db/migrate/20250723122117_add_courses_mooc_fi_user_id_to_users.rb new file mode 100644 index 000000000..8c478a373 --- /dev/null +++ b/db/migrate/20250723122117_add_courses_mooc_fi_user_id_to_users.rb @@ -0,0 +1,6 @@ +class AddCoursesMoocFiUserIdToUsers < ActiveRecord::Migration[7.1] + def change + add_column :users, :courses_mooc_fi_user_id, :string + add_index :users, :courses_mooc_fi_user_id, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 32be56cfd..af328f129 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,8 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2024_03_04_085436) do - +ActiveRecord::Schema[7.1].define(version: 2025_07_23_122117) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -83,8 +82,8 @@ create_table "banned_emails", force: :cascade do |t| t.string "email", null: false - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end create_table "certificates", id: :serial, force: :cascade do |t| @@ -333,8 +332,8 @@ create_table "organization_memberships", force: :cascade do |t| t.bigint "user_id" t.bigint "organization_id" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.index ["organization_id"], name: "index_organization_memberships_on_organization_id" t.index ["user_id", "organization_id"], name: "index_organization_memberships_on_user_id_and_organization_id", unique: true t.index ["user_id"], name: "index_organization_memberships_on_user_id" @@ -539,7 +538,10 @@ t.boolean "legitimate_student", default: true, null: false t.boolean "email_verified", default: false, null: false t.string "argon_hash" + t.boolean "password_managed_by_courses_mooc_fi", default: false, null: false + t.string "courses_mooc_fi_user_id" t.index "lower(email)", name: "index_user_email_lowercase", unique: true + t.index ["courses_mooc_fi_user_id"], name: "index_users_on_courses_mooc_fi_user_id", unique: true t.index ["login"], name: "index_users_on_login", unique: true end diff --git a/lib/gdocs_export.rb b/lib/gdocs_export.rb deleted file mode 100644 index ad14d2944..000000000 --- a/lib/gdocs_export.rb +++ /dev/null @@ -1,127 +0,0 @@ -# frozen_string_literal: true - -require 'google_drive' - -module GDocsExport - def self.authenticate(notifications) - notifications << 'gdocs_username undefined' && (return nil) unless - TmcServer::Application.config.gdocs_username - notifications << 'gdocs_password undefined' && (return nil) unless - TmcServer::Application.config.gdocs_password - - GoogleDrive.login( - TmcServer::Application.config.gdocs_username, - TmcServer::Application.config.gdocs_password - ) - end - - def self.refresh_course_worksheet_points(course, sheetname) - return ["sheetname #{sheetname} not found"] unless - course.gdocs_sheets.include?(sheetname) - - notifications = [] - gsession = authenticate notifications - refresh_course_worksheet notifications, gsession, course, sheetname - notifications - end - - def self.refresh_course_worksheet(notifications, gsession, course, sheetname) - ss = find_course_spreadsheet gsession, course - update_worksheet notifications, ss, course, sheetname - rescue Exception => e - notifications << "exception: #{e.message}" - end - - def self.worksheet_points(notifications, ws, course, sheetname) - points = AvailablePoint.course_sheet_points_list(course, sheetname).map(&:name) - points.each_with_object([]) do |point, result| - if point_col(ws, point) < 0 - notifications << "point #{point} not found on sheet #{sheetname}" - else - result << point - end - end - end - - def self.worksheet_students(notifications, ws, course, sheetname) - students = User.course_sheet_students(course, sheetname) - students.each_with_object([]) do |student, result| - if student_row(ws, student.login) < 0 - notifications << "student #{student.login} not found on sheet " + - sheetname - else - result << student - end - end - end - - def self.update_worksheet(notifications, ss, course, sheetname) - ws = ss.worksheets.find { |w| w.title == sheetname } - notifications << ["worksheet #{sheetname} not found"] && return unless ws - - students = worksheet_students notifications, ws, course, sheetname - points = worksheet_points notifications, ws, course, sheetname - - write_points(ws, course, students, points) - ws.save - - notifications - end - - def self.write_points(ws, course, students, points) - students.each do |student| - row = student_row ws, student.login - raise "student #{student.login} not found" if row < 0 - awarded = AwardedPoint.course_user_sheet_points(course, student, ws.title).map(&:name) - points.each do |point| - next unless awarded.include? point - col = point_col ws, point - raise "point #{point.name} not found" if col < 0 - ws[row, col] = '1' if ws[row, col] != '1' - end - end - end - - def self.point_col(ws, point_name) - (points_begin..ws.num_cols).each do |col| - return col if ws[header_row, col] == point_name - return col if ws[header_row, col] == "'#{point_name}" - end - -1 - end - - def self.student_row(ws, student_name) - stripped = strip_leading_zeroes(student_name) - return -1 if stripped.empty? - - (header_row + 1..ws.num_rows).each do |row| - cell = ws[row, student_col] - break if cell =~ /^=counta/ - return row if cell == student_name || cell == stripped - end - -1 - end - - def self.strip_leading_zeroes(s) - s.gsub(/^0*/, '') - end - - def self.find_course_spreadsheet(gsession, course) - raise 'spreadsheet_key undefined' unless course.spreadsheet_key - ss = gsession.spreadsheet_by_key course.spreadsheet_key - raise 'spreadsheet not found' unless ss - ss - end - - def self.student_col - 3 - end - - def self.header_row - 2 - end - - def self.points_begin - 6 - end -end diff --git a/spec/factories.rb b/spec/factories.rb index 3d0872957..8f0c4c416 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -31,6 +31,7 @@ def make_repo_for_course_template sequence(:email) { |n| "user#{n}@example.com" } sequence(:email_verified) { |n| 'false' } administrator { false } + password_managed_by_courses_mooc_fi { false } end factory :verified_user, class: User do @@ -39,6 +40,7 @@ def make_repo_for_course_template sequence(:email) { |n| "ver_user#{n}@example.com" } sequence(:email_verified) { |n| 'true' } administrator { false } + password_managed_by_courses_mooc_fi { false } end factory :admin, class: User do @@ -47,6 +49,7 @@ def make_repo_for_course_template sequence(:email) { |n| "admin#{n}@example.com" } sequence(:email_verified) { |n| 'true' } administrator { true } + password_managed_by_courses_mooc_fi { false } end factory :course, class: Course do diff --git a/spec/lib/gdocs_export_spec.rb b/spec/lib/gdocs_export_spec.rb deleted file mode 100644 index 2acf7bdb0..000000000 --- a/spec/lib/gdocs_export_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require 'gdocs_export' - -describe GDocsExport, gdocs: true do - before :all do - @session = GDocsExport.authenticate [] - expect(@session).not_to be_nil - @fixture1 = '0AnEpZul37faOdE1rc3lib0RLdjc5UXk2bk56a1lyWlE' - end - - before :each do - @course = FactoryBot.create(:course) - end - - describe 'finding stuff' do - before :each do - @course.spreadsheet_key = @fixture1 - @ss = GDocsExport.find_course_spreadsheet @session, @course - @ws = @ss.worksheets.find { |w| w.title == '1' } - end - - it 'should find students present in the fixture' do - expect(GDocsExport.student_row(@ws, '13816074')).not_to eq(-1) - expect(GDocsExport.student_row(@ws, '13284062')).not_to eq(-1) - end - end - - describe 'refreshing points' do - it 'should not find a spreadsheet if spreadsheet_key is nil' do - @course.spreadsheet_key = nil - notifications = [] - GDocsExport.refresh_course_spreadsheet notifications, @session, @course - expect(notifications).to include('exception: spreadsheet_key undefined') - end - - it 'should find fixture1' do - @course.spreadsheet_key = @fixture1 - notifications = [] - GDocsExport.refresh_course_spreadsheet notifications, @session, @course - expect(notifications).to be_empty - end - end -end