From 818064267c15eb673bda777e5be80174fc00edbf Mon Sep 17 00:00:00 2001 From: Karol Date: Sun, 18 Aug 2019 12:53:13 +0200 Subject: [PATCH 01/49] rename table, add fields to link table --- ...04802_rename_code_harbor_links_to_codeharbor_links.rb | 5 +++++ ...sh_url_client_id_client_secret_to_codeharbor_links.rb | 7 +++++++ db/schema.rb | 9 ++++++--- 3 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20190818104802_rename_code_harbor_links_to_codeharbor_links.rb create mode 100644 db/migrate/20190818104954_add_push_url_client_id_client_secret_to_codeharbor_links.rb diff --git a/db/migrate/20190818104802_rename_code_harbor_links_to_codeharbor_links.rb b/db/migrate/20190818104802_rename_code_harbor_links_to_codeharbor_links.rb new file mode 100644 index 00000000..5c7ba063 --- /dev/null +++ b/db/migrate/20190818104802_rename_code_harbor_links_to_codeharbor_links.rb @@ -0,0 +1,5 @@ +class RenameCodeHarborLinksToCodeharborLinks < ActiveRecord::Migration[5.2] + def change + rename_table :code_harbor_links, :codeharbor_links + end +end diff --git a/db/migrate/20190818104954_add_push_url_client_id_client_secret_to_codeharbor_links.rb b/db/migrate/20190818104954_add_push_url_client_id_client_secret_to_codeharbor_links.rb new file mode 100644 index 00000000..9d041be6 --- /dev/null +++ b/db/migrate/20190818104954_add_push_url_client_id_client_secret_to_codeharbor_links.rb @@ -0,0 +1,7 @@ +class AddPushUrlClientIdClientSecretToCodeharborLinks < ActiveRecord::Migration[5.2] + def change + add_column :codeharbor_links, :push_url, :string + add_column :codeharbor_links, :client_id, :string + add_column :codeharbor_links, :client_secret, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index feb96e00..ec6a7366 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_02_13_131802) do +ActiveRecord::Schema.define(version: 2019_08_18_104954) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -28,12 +28,15 @@ ActiveRecord::Schema.define(version: 2019_02_13_131802) do t.index ["user_type", "user_id"], name: "index_anomaly_notifications_on_user_type_and_user_id" end - create_table "code_harbor_links", force: :cascade do |t| + create_table "codeharbor_links", force: :cascade do |t| t.string "oauth2token", limit: 255 t.datetime "created_at" t.datetime "updated_at" t.integer "user_id" - t.index ["user_id"], name: "index_code_harbor_links_on_user_id" + t.string "push_url" + t.string "client_id" + t.string "client_secret" + t.index ["user_id"], name: "index_codeharbor_links_on_user_id" end create_table "comments", force: :cascade do |t| From 017644c4a5551416a26bb81d8d23555725e78692 Mon Sep 17 00:00:00 2001 From: Karol Date: Tue, 20 Aug 2019 18:37:17 +0200 Subject: [PATCH 02/49] implementation of import wip --- Gemfile | 1 + Gemfile.lock | 10 +++ app/controllers/exercises_controller.rb | 59 +++++++------ app/helpers/code_harbor_links_helper.rb | 2 - app/models/code_harbor_link.rb | 13 --- app/models/codeharbor_link.rb | 12 +++ app/models/exercise.rb | 4 +- app/policies/code_harbor_link_policy.rb | 3 - app/policies/codeharbor_link_policy.rb | 3 + .../convert_task_to_exercise.rb | 82 +++++++++++++++++++ app/services/proforma_service/import.rb | 58 +++++++++++++ app/services/service_base.rb | 7 ++ app/views/application/_navigation.html.slim | 2 +- config/environments/development.rb | 6 +- config/routes.rb | 2 +- 15 files changed, 212 insertions(+), 52 deletions(-) delete mode 100644 app/helpers/code_harbor_links_helper.rb delete mode 100644 app/models/code_harbor_link.rb create mode 100644 app/models/codeharbor_link.rb delete mode 100644 app/policies/code_harbor_link_policy.rb create mode 100644 app/policies/codeharbor_link_policy.rb create mode 100644 app/services/proforma_service/convert_task_to_exercise.rb create mode 100644 app/services/proforma_service/import.rb create mode 100644 app/services/service_base.rb diff --git a/Gemfile b/Gemfile index 484c18a1..47006ec7 100644 --- a/Gemfile +++ b/Gemfile @@ -36,6 +36,7 @@ gem 'webpacker' gem 'rest-client' gem 'rubyzip' gem 'mnemosyne-ruby' +gem 'proforma', path: '../proforma' gem 'whenever', require: false gem 'rails-timeago' diff --git a/Gemfile.lock b/Gemfile.lock index b0ee3759..7c80f248 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,6 +7,15 @@ GIT rack (>= 1.5.0) websocket (>= 1.1.0) +PATH + remote: ../proforma + specs: + proforma (0.1.0) + activemodel (~> 5.2.3) + activesupport (~> 5.2.3) + nokogiri (~> 1.10.2) + rubyzip (~> 1.2.2) + GEM remote: https://rubygems.org/ specs: @@ -430,6 +439,7 @@ DEPENDENCIES nyan-cat-formatter pagedown-bootstrap-rails pg + proforma! pry-byebug puma pundit diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 5505ef56..b5bde4bc 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -108,27 +108,34 @@ class ExercisesController < ApplicationController end def import_proforma_xml - begin - user = user_for_oauth2_request() - exercise = Exercise.new - request_body = request.body.read - exercise.from_proforma_xml(request_body) - exercise.user = user - saved = exercise.save - if saved - render :text => 'SUCCESS', :status => 200 - else - logger.info(exercise.errors.full_messages) - render :text => 'Invalid exercise', :status => 400 - end - rescue => error - if error.class == Hash - render :text => error.message, :status => error.status - else - raise error - render :text => '', :status => 500 - end + # begin + # user = user_for_oauth2_request + # exercise = Exercise.new + # request_body = request.body.read # needs to be some kind of a zip file + + tempfile = Tempfile.new('codeharbor_import.zip') + tempfile.write request.body.read.force_encoding('UTF-8') + tempfile.rewind + + exercise = ProformaService::Import.call(zip: tempfile, user: user_for_oauth2_request) + # exercise.from_proforma_xml(request_body) + # exercise.user = user + # saved = exercise.save + if exercise.save + # render text: 'SUCCESS', status: 200 + render json: {status: 201} + else + logger.info(exercise.errors.full_messages) + render json: {status: 400} end + # rescue => error + # if error.class == Hash + # render :text => error.message, :status => error.status + # else + # raise error + # render :text => '', :status => 500 + # end + # end end def user_for_oauth2_request @@ -142,7 +149,7 @@ class ExercisesController < ApplicationController raise ({status: 401, message: 'No token in Authorization header'}) end - user = user_by_code_harbor_token(oauth2Token) + user = user_by_codeharbor_token(oauth2Token) if user == nil raise ({status: 401, message: 'Unknown OAuth2 token'}) end @@ -151,13 +158,11 @@ class ExercisesController < ApplicationController end private :user_for_oauth2_request - def user_by_code_harbor_token(oauth2Token) - link = CodeHarborLink.where(:oauth2token => oauth2Token)[0] - if link != nil - return link.user - end + def user_by_codeharbor_token(oauth2_token) + link = CodeharborLink.where(oauth2token: oauth2_token)[0] + link&.user end - private :user_by_code_harbor_token + private :user_by_codeharbor_token def exercise_params params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :hide_file_tree, :allow_file_creation, :allow_auto_completion, :title, :expected_difficulty, files_attributes: file_attributes, :tag_ids => []).merge(user_id: current_user.id, user_type: current_user.class.name) if params[:exercise].present? diff --git a/app/helpers/code_harbor_links_helper.rb b/app/helpers/code_harbor_links_helper.rb deleted file mode 100644 index d8e92ddf..00000000 --- a/app/helpers/code_harbor_links_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module CodeHarborLinksHelper -end diff --git a/app/models/code_harbor_link.rb b/app/models/code_harbor_link.rb deleted file mode 100644 index 338461d1..00000000 --- a/app/models/code_harbor_link.rb +++ /dev/null @@ -1,13 +0,0 @@ -class CodeHarborLink < ApplicationRecord - validates :oauth2token, presence: true - validates :user_id, presence: true - - belongs_to :internal_user, foreign_key: :user_id - alias_method :user, :internal_user - alias_method :user=, :internal_user= - - def to_s - oauth2token - end - -end diff --git a/app/models/codeharbor_link.rb b/app/models/codeharbor_link.rb new file mode 100644 index 00000000..5bb1fba1 --- /dev/null +++ b/app/models/codeharbor_link.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class CodeharborLink < ApplicationRecord + validates :oauth2token, presence: true + validates :user_id, presence: true + + belongs_to :user, foreign_key: :user_id, class_name: 'InternalUser' + + def to_s + oauth2token + end +end diff --git a/app/models/exercise.rb b/app/models/exercise.rb index 3f4414e8..37d60476 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -31,7 +31,7 @@ class Exercise < ApplicationRecord validate :valid_main_file? validates :description, presence: true - validates :execution_environment_id, presence: true + # validates :execution_environment_id, presence: true # TODO make this conditional - but based on what? validates :public, boolean_presence: true validates :title, presence: true validates :token, presence: true, uniqueness: true @@ -49,7 +49,7 @@ class Exercise < ApplicationRecord 0 end end - + def finishers_percentage if users.distinct.count != 0 (100.0 / users.distinct.count * finishers.count).round(2) diff --git a/app/policies/code_harbor_link_policy.rb b/app/policies/code_harbor_link_policy.rb deleted file mode 100644 index 8726c22a..00000000 --- a/app/policies/code_harbor_link_policy.rb +++ /dev/null @@ -1,3 +0,0 @@ -class CodeHarborLinkPolicy < AdminOnlyPolicy - -end diff --git a/app/policies/codeharbor_link_policy.rb b/app/policies/codeharbor_link_policy.rb new file mode 100644 index 00000000..13c3676c --- /dev/null +++ b/app/policies/codeharbor_link_policy.rb @@ -0,0 +1,3 @@ +class CodeharborLinkPolicy < AdminOnlyPolicy + +end diff --git a/app/services/proforma_service/convert_task_to_exercise.rb b/app/services/proforma_service/convert_task_to_exercise.rb new file mode 100644 index 00000000..f2d26b45 --- /dev/null +++ b/app/services/proforma_service/convert_task_to_exercise.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +module ProformaService + class ConvertTaskToExercise < ServiceBase + def initialize(task:, user:, exercise: nil) + @task = task + @user = user + @exercise = exercise || Exercise.new + end + + def execute + import_exercise + @exercise + end + + private + + def import_exercise + @exercise.assign_attributes( + user: @user, + title: @task.title, + description: @task.description, + instructions: @task.internal_description, + # exercise_files: task_files.values, + # execution_environment: execution_environment, + # tests: tests, + # state_list: @exercise.persisted? ? 'updated' : 'new' + ) + end + + def task_files + @task_files ||= Hash[ + @task.all_files.reject { |file| file.id == 'ms-placeholder-file' }.map do |task_file| + [task_file.id, exercise_file_from_task_file(task_file)] + end + ] + end + + def exercise_file_from_task_file(task_file) + ExerciseFile.new({ + full_file_name: task_file.filename, + read_only: task_file.usage_by_lms.in?(%w[display download]), + hidden: task_file.visible == 'no', + role: task_file.internal_description + }.tap do |params| + if task_file.binary + params[:attachment] = file_base64(task_file) + params[:attachment_file_name] = task_file.filename + params[:attachment_content_type] = task_file.mimetype + else + params[:content] = task_file.content + end + end) + end + + def file_base64(file) + "data:#{file.mimetype || 'image/jpeg'};base64,#{Base64.encode64(file.content)}" + end + + def tests + @task.tests.map do |test_object| + Test.new( + feedback_message: test_object.meta_data['feedback-message'], + testing_framework: TestingFramework.where( + name: test_object.meta_data['testing-framework'], + version: test_object.meta_data['testing-framework-version'] + ).first_or_initialize, + exercise_file: test_file(test_object) + ) + end + end + + def test_file(test_object) + task_files.delete(test_object.files.first.id).tap { |file| file.purpose = 'test' } + end + + def execution_environment + ExecutionEnvironment.last + # ExecutionEnvironment.where(language: @task.proglang[:name], version: @task.proglang[:version]).first_or_initialize + end + end +end diff --git a/app/services/proforma_service/import.rb b/app/services/proforma_service/import.rb new file mode 100644 index 00000000..a4558870 --- /dev/null +++ b/app/services/proforma_service/import.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module ProformaService + class Import < ServiceBase + def initialize(zip:, user:) + @zip = zip + @user = user + end + + def execute + if single_task? + importer = Proforma::Importer.new(@zip) + @task = importer.perform + exercise = ConvertTaskToExercise.call(task: @task, user: @user) + exercise.save! + + exercise + else + import_multi + end + end + + private + + def import_multi + Zip::File.open(@zip.path) do |zip_file| + zip_files = zip_file.filter { |entry| entry.name.match?(/\.zip$/) } + begin + zip_files.map! do |entry| + store_zip_entry_in_tempfile entry + end + zip_files.map do |proforma_file| + Import.call(zip: proforma_file, user: @user) + end + ensure + zip_files.each(&:unlink) + end + end + end + + def store_zip_entry_in_tempfile(entry) + tempfile = Tempfile.new(entry.name) + tempfile.write entry.get_input_stream.read.force_encoding('UTF-8') + tempfile.rewind + tempfile + end + + def single_task? + filenames = Zip::File.open(@zip.path) do |zip_file| + zip_file.map(&:name) + end + + filenames.select { |f| f[/\.xml$/] }.any? + # rescue Zip::Error + # raise Proforma::InvalidZip + end + end +end diff --git a/app/services/service_base.rb b/app/services/service_base.rb new file mode 100644 index 00000000..39010bb7 --- /dev/null +++ b/app/services/service_base.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class ServiceBase + def self.call(*args) + new(*args).execute + end +end diff --git a/app/views/application/_navigation.html.slim b/app/views/application/_navigation.html.slim index 31115189..738dfc9d 100644 --- a/app/views/application/_navigation.html.slim +++ b/app/views/application/_navigation.html.slim @@ -19,5 +19,5 @@ models: [ErrorTemplate, ErrorTemplateAttribute], cached: true) = render('navigation_submenu', title: t('navigation.sections.files'), models: [FileType, FileTemplate], cached: true) - = render('navigation_submenu', title: t('navigation.sections.integrations'), models: [Consumer, CodeHarborLink], + = render('navigation_submenu', title: t('navigation.sections.integrations'), models: [Consumer], cached: true) diff --git a/config/environments/development.rb b/config/environments/development.rb index b91a19b5..ba0880a5 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -3,15 +3,15 @@ Rails.application.configure do config.webpacker.check_yarn_integrity = true # Settings specified here will take precedence over those in config/application.rb. - config.web_console.whitelisted_ips = '192.168.0.0/16' - + config.web_console.whitelisted_ips = '192.168.0.0/16' + # In the development environment your application's code is reloaded on # every request. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.cache_classes = false # Do not eager load code on boot. - config.eager_load = false + config.eager_load = true # Show full error reports. config.consider_all_requests_local = true diff --git a/config/routes.rb b/config/routes.rb index 42cee5a3..23ed0984 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -13,7 +13,7 @@ Rails.application.routes.draw do get 'by_file_type/:file_type_id', as: :by_file_type, action: :by_file_type end end - resources :code_harbor_links + # resources :code_harbor_links resources :request_for_comments do member do get :mark_as_solved, defaults: { format: :json } From 2af93ea308f233985cbc6bdbaba3b083a309d0b9 Mon Sep 17 00:00:00 2001 From: Karol Date: Wed, 21 Aug 2019 18:27:42 +0200 Subject: [PATCH 03/49] implement file import --- app/models/exercise.rb | 2 +- .../convert_task_to_exercise.rb | 28 ++++++++++++++----- app/views/exercises/show.html.slim | 2 +- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/app/models/exercise.rb b/app/models/exercise.rb index 37d60476..05e69dcf 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -9,7 +9,7 @@ class Exercise < ApplicationRecord after_initialize :generate_token after_initialize :set_default_values - belongs_to :execution_environment + belongs_to :execution_environment, optional: true has_many :submissions has_and_belongs_to_many :proxy_exercises diff --git a/app/services/proforma_service/convert_task_to_exercise.rb b/app/services/proforma_service/convert_task_to_exercise.rb index f2d26b45..ee5f5a78 100644 --- a/app/services/proforma_service/convert_task_to_exercise.rb +++ b/app/services/proforma_service/convert_task_to_exercise.rb @@ -21,19 +21,33 @@ module ProformaService title: @task.title, description: @task.description, instructions: @task.internal_description, - # exercise_files: task_files.values, - # execution_environment: execution_environment, + files: task_files # tests: tests, + # execution_environment: execution_environment, # state_list: @exercise.persisted? ? 'updated' : 'new' ) end def task_files - @task_files ||= Hash[ - @task.all_files.reject { |file| file.id == 'ms-placeholder-file' }.map do |task_file| - [task_file.id, exercise_file_from_task_file(task_file)] - end - ] + @task.all_files.map do |file| + CodeOcean::File.new( + context: @exercise, + file_type: FileType.find_by(file_extension: File.extname(file.filename)), + hidden: file.visible == 'no', + name: File.basename(file.filename, '.*'), + read_only: file.usage_by_lms != 'edit', + # native_file: somehting something, + role: file.internal_description.underscore.gsub(' ', '_'), + # feedback_message: #if file is testfilethingy take that message, + # weight: see above, + path: File.dirname(file.filename) + ) + end + # @task_files ||= Hash[ + # @task.all_files.reject { |file| file.id == 'ms-placeholder-file' }.map do |task_file| + # [task_file.id, exercise_file_from_task_file(task_file)] + # end + # ] end def exercise_file_from_task_file(task_file) diff --git a/app/views/exercises/show.html.slim b/app/views/exercises/show.html.slim index e2f7ada9..b9fd33bc 100644 --- a/app/views/exercises/show.html.slim +++ b/app/views/exercises/show.html.slim @@ -12,7 +12,7 @@ h1 = row(label: 'exercise.title', value: @exercise.title) = row(label: 'exercise.user', value: link_to_if(policy(@exercise.author).show?, @exercise.author, @exercise.author)) = row(label: 'exercise.description', value: render_markdown(@exercise.description), class: 'm-0') -= row(label: 'exercise.execution_environment', value: link_to_if(policy(@exercise.execution_environment).show?, @exercise.execution_environment, @exercise.execution_environment)) += row(label: 'exercise.execution_environment', value: link_to_if(@exercise.execution_environment && policy(@exercise.execution_environment).show?, @exercise.execution_environment, @exercise.execution_environment)) /= row(label: 'exercise.instructions', value: render_markdown(@exercise.instructions)) = row(label: 'exercise.maximum_score', value: @exercise.maximum_score) = row(label: 'exercise.public', value: @exercise.public?) From aafb3f21df7f7f944ef4c7986be798d1fe06afda Mon Sep 17 00:00:00 2001 From: Karol Date: Thu, 22 Aug 2019 18:37:47 +0200 Subject: [PATCH 04/49] file import wip, "native" missing --- app/controllers/exercises_controller.rb | 4 +- .../convert_task_to_exercise.rb | 106 ++++++++---------- 2 files changed, 51 insertions(+), 59 deletions(-) diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index b5bde4bc..131b1315 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -123,10 +123,10 @@ class ExercisesController < ApplicationController # saved = exercise.save if exercise.save # render text: 'SUCCESS', status: 200 - render json: {status: 201} + render json: {}, status: 201 else logger.info(exercise.errors.full_messages) - render json: {status: 400} + render json: {}, status: 400 end # rescue => error # if error.class == Hash diff --git a/app/services/proforma_service/convert_task_to_exercise.rb b/app/services/proforma_service/convert_task_to_exercise.rb index ee5f5a78..994684d0 100644 --- a/app/services/proforma_service/convert_task_to_exercise.rb +++ b/app/services/proforma_service/convert_task_to_exercise.rb @@ -21,76 +21,68 @@ module ProformaService title: @task.title, description: @task.description, instructions: @task.internal_description, - files: task_files + files: files # tests: tests, # execution_environment: execution_environment, # state_list: @exercise.persisted? ? 'updated' : 'new' ) end - def task_files - @task.all_files.map do |file| - CodeOcean::File.new( - context: @exercise, - file_type: FileType.find_by(file_extension: File.extname(file.filename)), - hidden: file.visible == 'no', - name: File.basename(file.filename, '.*'), - read_only: file.usage_by_lms != 'edit', - # native_file: somehting something, - role: file.internal_description.underscore.gsub(' ', '_'), - # feedback_message: #if file is testfilethingy take that message, - # weight: see above, - path: File.dirname(file.filename) - ) - end - # @task_files ||= Hash[ - # @task.all_files.reject { |file| file.id == 'ms-placeholder-file' }.map do |task_file| - # [task_file.id, exercise_file_from_task_file(task_file)] - # end - # ] + def files + test_files + task_files.values end - def exercise_file_from_task_file(task_file) - ExerciseFile.new({ - full_file_name: task_file.filename, - read_only: task_file.usage_by_lms.in?(%w[display download]), - hidden: task_file.visible == 'no', - role: task_file.internal_description - }.tap do |params| - if task_file.binary - params[:attachment] = file_base64(task_file) - params[:attachment_file_name] = task_file.filename - params[:attachment_content_type] = task_file.mimetype - else - params[:content] = task_file.content + def test_files + @task.tests.map do |test_object| + task_files.delete(test_object.files.first.id).tap do |file| + file.weight = 1.0 + file.feedback_message = test_object.meta_data['feedback-message'] end - end) + end + end + + def task_files + @task_files ||= Hash[ + @task.all_files.reject { |file| file.id == 'ms-placeholder-file' }.map do |task_file| + [task_file.id, codeocean_file_from_task_file(task_file)] + end + ] + end + + def codeocean_file_from_task_file(file) + + CodeOcean::File.new( + context: @exercise, + content: file.content, + file_type: FileType.find_by(file_extension: File.extname(file.filename)), + hidden: file.visible == 'no', + name: File.basename(file.filename, '.*'), + read_only: file.usage_by_lms != 'edit', + # native_file: somehting something, uploader something + role: file.internal_description.underscore.gsub(' ', '_'), + # feedback_message: file.purpose == 'test' ? file.test.feedback_message : nil, + # weight: file.test? ? 1.0 : nil, + path: File.dirname(file.filename) + ) + + # ExerciseFile.new({ + # full_file_name: task_file.filename, + # read_only: task_file.usage_by_lms.in?(%w[display download]), + # hidden: task_file.visible == 'no', + # role: task_file.internal_description + # }.tap do |params| + # if task_file.binary + # params[:attachment] = file_base64(task_file) + # params[:attachment_file_name] = task_file.filename + # params[:attachment_content_type] = task_file.mimetype + # else + # params[:content] = task_file.content + # end + # end) end def file_base64(file) "data:#{file.mimetype || 'image/jpeg'};base64,#{Base64.encode64(file.content)}" end - - def tests - @task.tests.map do |test_object| - Test.new( - feedback_message: test_object.meta_data['feedback-message'], - testing_framework: TestingFramework.where( - name: test_object.meta_data['testing-framework'], - version: test_object.meta_data['testing-framework-version'] - ).first_or_initialize, - exercise_file: test_file(test_object) - ) - end - end - - def test_file(test_object) - task_files.delete(test_object.files.first.id).tap { |file| file.purpose = 'test' } - end - - def execution_environment - ExecutionEnvironment.last - # ExecutionEnvironment.where(language: @task.proglang[:name], version: @task.proglang[:version]).first_or_initialize - end end end From ecabd9d05c25e039b2c4d59e60a518486a7ebc94 Mon Sep 17 00:00:00 2001 From: Karol Date: Fri, 23 Aug 2019 07:33:43 +0200 Subject: [PATCH 05/49] file import with binary file --- .../convert_task_to_exercise.rb | 37 ++++--------------- lib/file_io.rb | 11 ++++++ 2 files changed, 19 insertions(+), 29 deletions(-) create mode 100644 lib/file_io.rb diff --git a/app/services/proforma_service/convert_task_to_exercise.rb b/app/services/proforma_service/convert_task_to_exercise.rb index 994684d0..ddeb9030 100644 --- a/app/services/proforma_service/convert_task_to_exercise.rb +++ b/app/services/proforma_service/convert_task_to_exercise.rb @@ -22,9 +22,6 @@ module ProformaService description: @task.description, instructions: @task.internal_description, files: files - # tests: tests, - # execution_environment: execution_environment, - # state_list: @exercise.persisted? ? 'updated' : 'new' ) end @@ -50,39 +47,21 @@ module ProformaService end def codeocean_file_from_task_file(file) - - CodeOcean::File.new( + CodeOcean::File.new({ context: @exercise, - content: file.content, file_type: FileType.find_by(file_extension: File.extname(file.filename)), hidden: file.visible == 'no', name: File.basename(file.filename, '.*'), read_only: file.usage_by_lms != 'edit', - # native_file: somehting something, uploader something role: file.internal_description.underscore.gsub(' ', '_'), - # feedback_message: file.purpose == 'test' ? file.test.feedback_message : nil, - # weight: file.test? ? 1.0 : nil, path: File.dirname(file.filename) - ) - - # ExerciseFile.new({ - # full_file_name: task_file.filename, - # read_only: task_file.usage_by_lms.in?(%w[display download]), - # hidden: task_file.visible == 'no', - # role: task_file.internal_description - # }.tap do |params| - # if task_file.binary - # params[:attachment] = file_base64(task_file) - # params[:attachment_file_name] = task_file.filename - # params[:attachment_content_type] = task_file.mimetype - # else - # params[:content] = task_file.content - # end - # end) - end - - def file_base64(file) - "data:#{file.mimetype || 'image/jpeg'};base64,#{Base64.encode64(file.content)}" + }.tap do |params| + if file.binary + params[:native_file] = FileIO.new(file.content.force_encoding('UTF-8'), File.basename(file.filename)) + else + params[:content] = file.content + end + end) end end end diff --git a/lib/file_io.rb b/lib/file_io.rb new file mode 100644 index 00000000..39627b99 --- /dev/null +++ b/lib/file_io.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# stole from: https://makandracards.com/makandra/50526-fileio-writing-strings-as-carrierwave-uploads +class FileIO < StringIO + def initialize(stream, filename) + super(stream) + @original_filename = filename + end + + attr_reader :original_filename +end From ec48d1f4473365d7d7b585198fe11d58a714a172 Mon Sep 17 00:00:00 2001 From: Karol Date: Mon, 26 Aug 2019 19:06:52 +0200 Subject: [PATCH 06/49] readding codeharbor_link views and actions --- .../code_harbor_links_controller.rb | 68 ------------------- .../codeharbor_links_controller.rb | 55 +++++++++++++++ app/controllers/exercises_controller.rb | 17 ----- app/models/user.rb | 1 + app/policies/codeharbor_link_policy.rb | 30 +++++++- app/views/application/_navigation.html.slim | 2 +- app/views/code_harbor_links/_form.html.slim | 6 -- app/views/code_harbor_links/edit.html.slim | 3 - app/views/code_harbor_links/index.html.slim | 18 ----- app/views/code_harbor_links/new.html.slim | 3 - app/views/code_harbor_links/show.html.slim | 7 -- app/views/codeharbor_links/_form.html.slim | 6 ++ app/views/codeharbor_links/edit.html.slim | 3 + app/views/codeharbor_links/index.html.slim | 18 +++++ app/views/codeharbor_links/new.html.slim | 3 + app/views/codeharbor_links/show.html.slim | 7 ++ app/views/internal_users/show.html.slim | 6 ++ config/locales/de.yml | 2 +- config/locales/en.yml | 6 +- config/routes.rb | 2 +- 20 files changed, 136 insertions(+), 127 deletions(-) delete mode 100644 app/controllers/code_harbor_links_controller.rb create mode 100644 app/controllers/codeharbor_links_controller.rb delete mode 100644 app/views/code_harbor_links/_form.html.slim delete mode 100644 app/views/code_harbor_links/edit.html.slim delete mode 100644 app/views/code_harbor_links/index.html.slim delete mode 100644 app/views/code_harbor_links/new.html.slim delete mode 100644 app/views/code_harbor_links/show.html.slim create mode 100644 app/views/codeharbor_links/_form.html.slim create mode 100644 app/views/codeharbor_links/edit.html.slim create mode 100644 app/views/codeharbor_links/index.html.slim create mode 100644 app/views/codeharbor_links/new.html.slim create mode 100644 app/views/codeharbor_links/show.html.slim diff --git a/app/controllers/code_harbor_links_controller.rb b/app/controllers/code_harbor_links_controller.rb deleted file mode 100644 index a4736f26..00000000 --- a/app/controllers/code_harbor_links_controller.rb +++ /dev/null @@ -1,68 +0,0 @@ -class CodeHarborLinksController < ApplicationController - include CommonBehavior - before_action :set_code_harbor_link, only: [:show, :edit, :update, :destroy] - - def authorize! - authorize(@code_harbor_link || @code_harbor_links) - end - private :authorize! - - # GET /code_harbor_links - # GET /code_harbor_links.json - def index - @code_harbor_links = CodeHarborLink.where(user_id: current_user.id).paginate(page: params[:page]) - authorize! - end - - # GET /code_harbor_links/1 - # GET /code_harbor_links/1.json - def show - authorize! - end - - # GET /code_harbor_links/new - def new - @code_harbor_link = CodeHarborLink.new - authorize! - end - - # GET /code_harbor_links/1/edit - def edit - authorize! - end - - # POST /code_harbor_links - # POST /code_harbor_links.json - def create - @code_harbor_link = CodeHarborLink.new(code_harbor_link_params) - @code_harbor_link.user = current_user - authorize! - create_and_respond(object: @code_harbor_link) - end - - # PATCH/PUT /code_harbor_links/1 - # PATCH/PUT /code_harbor_links/1.json - def update - update_and_respond(object: @code_harbor_link, params: code_harbor_link_params) - authorize! - end - - # DELETE /code_harbor_links/1 - # DELETE /code_harbor_links/1.json - def destroy - destroy_and_respond(object: @code_harbor_link) - end - - private - # Use callbacks to share common setup or constraints between actions. - def set_code_harbor_link - @code_harbor_link = CodeHarborLink.find(params[:id]) - @code_harbor_link.user = current_user - authorize! - end - - # Never trust parameters from the scary internet, only allow the white list through. - def code_harbor_link_params - params.require(:code_harbor_link).permit(:oauth2token) - end -end diff --git a/app/controllers/codeharbor_links_controller.rb b/app/controllers/codeharbor_links_controller.rb new file mode 100644 index 00000000..1eee74df --- /dev/null +++ b/app/controllers/codeharbor_links_controller.rb @@ -0,0 +1,55 @@ +class CodeharborLinksController < ApplicationController + include CommonBehavior + before_action :set_codeharbor_link, only: [:show, :edit, :update, :destroy] + + def authorize! + authorize(@codeharbor_link || @codeharbor_links) + end + private :authorize! + + def index + @codeharbor_links = CodeharborLink.where(user_id: current_user.id).paginate(page: params[:page]) + authorize! + end + + def show + authorize! + end + + def new + @codeharbor_link = CodeharborLink.new + authorize! + end + + def edit + authorize! + end + + def create + @codeharbor_link = CodeharborLink.new(codeharbor_link_params) + @codeharbor_link.user = current_user + authorize! + create_and_respond(object: @codeharbor_link) + end + + def update + update_and_respond(object: @codeharbor_link, params: codeharbor_link_params) + authorize! + end + + def destroy + destroy_and_respond(object: @codeharbor_link) + end + + private + + def set_codeharbor_link + @codeharbor_link = CodeharborLink.find(params[:id]) + @codeharbor_link.user = current_user + authorize! + end + + def codeharbor_link_params + params.require(:codeharbor_link).permit(:push_url, :oauth2token, :client_id, :client_secret) + end +end diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 131b1315..33e06fa9 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -108,34 +108,17 @@ class ExercisesController < ApplicationController end def import_proforma_xml - # begin - # user = user_for_oauth2_request - # exercise = Exercise.new - # request_body = request.body.read # needs to be some kind of a zip file - tempfile = Tempfile.new('codeharbor_import.zip') tempfile.write request.body.read.force_encoding('UTF-8') tempfile.rewind exercise = ProformaService::Import.call(zip: tempfile, user: user_for_oauth2_request) - # exercise.from_proforma_xml(request_body) - # exercise.user = user - # saved = exercise.save if exercise.save - # render text: 'SUCCESS', status: 200 render json: {}, status: 201 else logger.info(exercise.errors.full_messages) render json: {}, status: 400 end - # rescue => error - # if error.class == Hash - # render :text => error.message, :status => error.status - # else - # raise error - # render :text => '', :status => 500 - # end - # end end def user_for_oauth2_request diff --git a/app/models/user.rb b/app/models/user.rb index a9c9dab2..0315e761 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -13,6 +13,7 @@ class User < ApplicationRecord has_many :user_proxy_exercise_exercises, as: :user has_many :user_exercise_interventions, as: :user has_many :interventions, through: :user_exercise_interventions + has_one :codeharbor_link accepts_nested_attributes_for :user_proxy_exercise_exercises diff --git a/app/policies/codeharbor_link_policy.rb b/app/policies/codeharbor_link_policy.rb index 13c3676c..e28e3af7 100644 --- a/app/policies/codeharbor_link_policy.rb +++ b/app/policies/codeharbor_link_policy.rb @@ -1,3 +1,31 @@ -class CodeharborLinkPolicy < AdminOnlyPolicy +# frozen_string_literal: true +class CodeharborLinkPolicy < ApplicationPolicy + def index? + teacher? + end + + def create? + teacher? + end + + def show? + teacher? + end + + def edit? + teacher? + end + + def destroy? + teacher? + end + + def new? + teacher? + end + + def update? + teacher? + end end diff --git a/app/views/application/_navigation.html.slim b/app/views/application/_navigation.html.slim index 738dfc9d..c4626795 100644 --- a/app/views/application/_navigation.html.slim +++ b/app/views/application/_navigation.html.slim @@ -19,5 +19,5 @@ models: [ErrorTemplate, ErrorTemplateAttribute], cached: true) = render('navigation_submenu', title: t('navigation.sections.files'), models: [FileType, FileTemplate], cached: true) - = render('navigation_submenu', title: t('navigation.sections.integrations'), models: [Consumer], + = render('navigation_submenu', title: t('navigation.sections.integrations'), models: [Consumer, CodeharborLink], cached: true) diff --git a/app/views/code_harbor_links/_form.html.slim b/app/views/code_harbor_links/_form.html.slim deleted file mode 100644 index f7b449ee..00000000 --- a/app/views/code_harbor_links/_form.html.slim +++ /dev/null @@ -1,6 +0,0 @@ -= form_for(@code_harbor_link) do |f| - = render('shared/form_errors', object: @code_harbor_link) - .form-group - = f.label(:oauth2token) - = f.text_field(:oauth2token, class: 'form-control', required: true) - .actions = render('shared/submit_button', f: f, object: @code_harbor_link) diff --git a/app/views/code_harbor_links/edit.html.slim b/app/views/code_harbor_links/edit.html.slim deleted file mode 100644 index d1c7ea8f..00000000 --- a/app/views/code_harbor_links/edit.html.slim +++ /dev/null @@ -1,3 +0,0 @@ -h1 = @code_harbor_link - -= render('form') diff --git a/app/views/code_harbor_links/index.html.slim b/app/views/code_harbor_links/index.html.slim deleted file mode 100644 index c07e43da..00000000 --- a/app/views/code_harbor_links/index.html.slim +++ /dev/null @@ -1,18 +0,0 @@ -h1 = CodeHarborLink.model_name.human(count: 2) - -.table-responsive - table.table - thead - tr - th = t('activerecord.attributes.code_harbor_link.oauth2token') - th colspan=3 = t('shared.actions') - tbody - - @code_harbor_links.each do |code_harbor_link| - tr - td = link_to_if(policy(code_harbor_link).show?, code_harbor_link.oauth2token, code_harbor_link) - td = link_to(t('shared.show'), code_harbor_link) if policy(code_harbor_link).show? - td = link_to(t('shared.edit'), edit_code_harbor_link_path(code_harbor_link)) if policy(code_harbor_link).edit? - td = link_to(t('shared.destroy'), code_harbor_link, data: {confirm: t('shared.confirm_destroy')}, method: :delete) if policy(code_harbor_link).destroy? - -= render('shared/pagination', collection: @code_harbor_links) -p = render('shared/new_button', model: CodeHarborLink) diff --git a/app/views/code_harbor_links/new.html.slim b/app/views/code_harbor_links/new.html.slim deleted file mode 100644 index ef19a3e6..00000000 --- a/app/views/code_harbor_links/new.html.slim +++ /dev/null @@ -1,3 +0,0 @@ -h1 = t('shared.new_model', model: CodeHarborLink.model_name.human) - -= render('form') diff --git a/app/views/code_harbor_links/show.html.slim b/app/views/code_harbor_links/show.html.slim deleted file mode 100644 index e4a29f75..00000000 --- a/app/views/code_harbor_links/show.html.slim +++ /dev/null @@ -1,7 +0,0 @@ -h1 - = @code_harbor_link - = render('shared/edit_button', object: @code_harbor_link) - -- %w[oauth2token].each do |attribute| - = row(label: "code_harbor_link.#{attribute}") do - = content_tag(:input, nil, class: 'form-control', readonly: true, value: @code_harbor_link.send(attribute)) diff --git a/app/views/codeharbor_links/_form.html.slim b/app/views/codeharbor_links/_form.html.slim new file mode 100644 index 00000000..de351ec7 --- /dev/null +++ b/app/views/codeharbor_links/_form.html.slim @@ -0,0 +1,6 @@ += form_for(@codeharbor_link) do |f| + = render('shared/form_errors', object: @codeharbor_link) + .form-group + = f.label(:oauth2token) + = f.text_field(:oauth2token, class: 'form-control', required: true) + .actions = render('shared/submit_button', f: f, object: @codeharbor_link) diff --git a/app/views/codeharbor_links/edit.html.slim b/app/views/codeharbor_links/edit.html.slim new file mode 100644 index 00000000..6cb4a21b --- /dev/null +++ b/app/views/codeharbor_links/edit.html.slim @@ -0,0 +1,3 @@ +h1 = @codeharbor_link + += render('form') diff --git a/app/views/codeharbor_links/index.html.slim b/app/views/codeharbor_links/index.html.slim new file mode 100644 index 00000000..be6833c9 --- /dev/null +++ b/app/views/codeharbor_links/index.html.slim @@ -0,0 +1,18 @@ +h1 = CodeharborLink.model_name.human(count: 2) + +.table-responsive + table.table + thead + tr + th = t('activerecord.attributes.codeharbor_link.oauth2token') + th colspan=3 = t('shared.actions') + tbody + - @codeharbor_links.each do |codeharbor_link| + tr + td = link_to_if(policy(codeharbor_link).show?, codeharbor_link.oauth2token, codeharbor_link) + td = link_to(t('shared.show'), codeharbor_link) if policy(codeharbor_link).show? + td = link_to(t('shared.edit'), edit_codeharbor_link_path(codeharbor_link)) if policy(codeharbor_link).edit? + td = link_to(t('shared.destroy'), codeharbor_link, data: {confirm: t('shared.confirm_destroy')}, method: :delete) if policy(codeharbor_link).destroy? + += render('shared/pagination', collection: @codeharbor_links) +p = render('shared/new_button', model: CodeharborLink) diff --git a/app/views/codeharbor_links/new.html.slim b/app/views/codeharbor_links/new.html.slim new file mode 100644 index 00000000..c3038281 --- /dev/null +++ b/app/views/codeharbor_links/new.html.slim @@ -0,0 +1,3 @@ +h1 = t('shared.new_model', model: CodeharborLink.model_name.human) + += render('form') diff --git a/app/views/codeharbor_links/show.html.slim b/app/views/codeharbor_links/show.html.slim new file mode 100644 index 00000000..b17c86e7 --- /dev/null +++ b/app/views/codeharbor_links/show.html.slim @@ -0,0 +1,7 @@ +h1 + = @codeharbor_link + = render('shared/edit_button', object: @codeharbor_link) + +- %w[oauth2token].each do |attribute| + = row(label: "codeharbor_link.#{attribute}") do + = content_tag(:input, nil, class: 'form-control', readonly: true, value: @codeharbor_link.send(attribute)) diff --git a/app/views/internal_users/show.html.slim b/app/views/internal_users/show.html.slim index 167443f6..da5620ff 100644 --- a/app/views/internal_users/show.html.slim +++ b/app/views/internal_users/show.html.slim @@ -7,3 +7,9 @@ h1 = row(label: 'internal_user.consumer', value: @user.consumer ? link_to_if(policy(@user.consumer).show?, @user.consumer, @user.consumer) : nil) = row(label: 'internal_user.role', value: t("users.roles.#{@user.role}")) = row(label: 'internal_user.activated', value: @user.activated?) + +- if @user.codeharbor_link.nil? + + = row(label: 'codeharbor_link.new', value: link_to(t('codeharbor_link.new'), codeharbor_link_new_path, class: 'btn btn-secondary')) +- else + = row(label: 'codeharbor_link.edit', value: link_to(t('codeharbor_link.edit'), edit_codeharbor_link_path(@user.codeharbor_link), class: 'btn btn-secondary')) diff --git a/config/locales/de.yml b/config/locales/de.yml index f36e5cbb..6ae6b37c 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1,7 +1,7 @@ de: activerecord: attributes: - code_harbor_link: + codeharbor_link: oauth2token: OAuth2 Token consumer: name: Name diff --git a/config/locales/en.yml b/config/locales/en.yml index e9d30ec0..90f1f69e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,7 +1,7 @@ en: activerecord: attributes: - code_harbor_link: + codeharbor_link: oauth2token: OAuth2 Token consumer: name: Name @@ -240,6 +240,10 @@ en: consumers: show: link: Consumer + codeharbor_link: + profile_label: Codeharbor Link + new: Create link to Codeharbor + edit: Edit existing link execution_environments: form: hints: diff --git a/config/routes.rb b/config/routes.rb index 23ed0984..0949dea9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -13,7 +13,7 @@ Rails.application.routes.draw do get 'by_file_type/:file_type_id', as: :by_file_type, action: :by_file_type end end - # resources :code_harbor_links + resources :codeharbor_links resources :request_for_comments do member do get :mark_as_solved, defaults: { format: :json } From 8c306669af7f6f7ff4e96d04d7fffe62e06998cd Mon Sep 17 00:00:00 2001 From: Karol Date: Tue, 27 Aug 2019 18:33:21 +0200 Subject: [PATCH 07/49] codeharbor_links editable through own profile --- app/assets/javascripts/codeharbor_link.js | 49 +++++++++++++++++++ .../codeharbor_links_controller.rb | 20 ++++---- app/policies/codeharbor_link_policy.rb | 24 ++++----- app/views/application/_navigation.html.slim | 2 +- app/views/codeharbor_links/_form.html.slim | 26 +++++++++- app/views/codeharbor_links/edit.html.slim | 2 +- app/views/codeharbor_links/index.html.slim | 18 ------- app/views/codeharbor_links/show.html.slim | 7 --- app/views/internal_users/show.html.slim | 9 ++-- config/locales/en.yml | 13 +++++ config/routes.rb | 2 +- 11 files changed, 116 insertions(+), 56 deletions(-) create mode 100644 app/assets/javascripts/codeharbor_link.js delete mode 100644 app/views/codeharbor_links/index.html.slim delete mode 100644 app/views/codeharbor_links/show.html.slim diff --git a/app/assets/javascripts/codeharbor_link.js b/app/assets/javascripts/codeharbor_link.js new file mode 100644 index 00000000..bf8c74e1 --- /dev/null +++ b/app/assets/javascripts/codeharbor_link.js @@ -0,0 +1,49 @@ +$(document).on('turbolinks:load', function() { + $('[data-toggle="tooltip"]').tooltip(); + if($.isController('codeharbor_links')) { + if ($('.edit_codeharbor_link, .new_codeharbor_link').isPresent()) { + + var replace = (function(string) { + var d = getDate(); + return string.replace(/[xy]/g, function (c) { + var r = (d + Math.random() * 16) % 16 | 0; + d = Math.floor(d / 16); + return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); + }); + }); + + var getDate = (function () { + var d = new Date().getTime(); + if (typeof performance !== 'undefined' && typeof performance.now === 'function') { + d += performance.now(); //use high-precision timer if available + } + return d + }); + + var generateUUID = (function () { // Public Domain/MIT + // var d = getDate(); + return replace('xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'); + }); + + var generateRandomHex32 = (function () { + // var d = getDate(); + return replace(Array(32).join('x')); + }); + + + + $('.generate-client-id').on('click', function () { + $('.client-id').val(generateUUID()); + }); + + $('.generate-client-secret').on('click', function () { + $('.client-secret').val(generateRandomHex32()); + }); + + $('.generate-oauth2-token').on('click', function () { + $('.oauth2-token').val(generateRandomHex32()) + }); + } + } +}); + diff --git a/app/controllers/codeharbor_links_controller.rb b/app/controllers/codeharbor_links_controller.rb index 1eee74df..ddff149f 100644 --- a/app/controllers/codeharbor_links_controller.rb +++ b/app/controllers/codeharbor_links_controller.rb @@ -7,14 +7,14 @@ class CodeharborLinksController < ApplicationController end private :authorize! - def index - @codeharbor_links = CodeharborLink.where(user_id: current_user.id).paginate(page: params[:page]) - authorize! - end + # def index + # @codeharbor_links = CodeharborLink.where(user_id: current_user.id).paginate(page: params[:page]) + # authorize! + # end - def show - authorize! - end + # def show + # authorize! + # end def new @codeharbor_link = CodeharborLink.new @@ -29,16 +29,16 @@ class CodeharborLinksController < ApplicationController @codeharbor_link = CodeharborLink.new(codeharbor_link_params) @codeharbor_link.user = current_user authorize! - create_and_respond(object: @codeharbor_link) + create_and_respond(object: @codeharbor_link, path: proc { @codeharbor_link.user }) end def update - update_and_respond(object: @codeharbor_link, params: codeharbor_link_params) + update_and_respond(object: @codeharbor_link, params: codeharbor_link_params, path: @codeharbor_link.user) authorize! end def destroy - destroy_and_respond(object: @codeharbor_link) + destroy_and_respond(object: @codeharbor_link, path: @codeharbor_link.user) end private diff --git a/app/policies/codeharbor_link_policy.rb b/app/policies/codeharbor_link_policy.rb index e28e3af7..e83f9b75 100644 --- a/app/policies/codeharbor_link_policy.rb +++ b/app/policies/codeharbor_link_policy.rb @@ -2,6 +2,14 @@ class CodeharborLinkPolicy < ApplicationPolicy def index? + false + end + + def show? + false + end + + def new? teacher? end @@ -9,23 +17,15 @@ class CodeharborLinkPolicy < ApplicationPolicy teacher? end - def show? - teacher? - end - def edit? teacher? end - def destroy? - teacher? - end - - def new? - teacher? - end - def update? teacher? end + + def destroy? + teacher? + end end diff --git a/app/views/application/_navigation.html.slim b/app/views/application/_navigation.html.slim index c4626795..738dfc9d 100644 --- a/app/views/application/_navigation.html.slim +++ b/app/views/application/_navigation.html.slim @@ -19,5 +19,5 @@ models: [ErrorTemplate, ErrorTemplateAttribute], cached: true) = render('navigation_submenu', title: t('navigation.sections.files'), models: [FileType, FileTemplate], cached: true) - = render('navigation_submenu', title: t('navigation.sections.integrations'), models: [Consumer, CodeharborLink], + = render('navigation_submenu', title: t('navigation.sections.integrations'), models: [Consumer], cached: true) diff --git a/app/views/codeharbor_links/_form.html.slim b/app/views/codeharbor_links/_form.html.slim index de351ec7..fac3c05c 100644 --- a/app/views/codeharbor_links/_form.html.slim +++ b/app/views/codeharbor_links/_form.html.slim @@ -1,6 +1,28 @@ = form_for(@codeharbor_link) do |f| = render('shared/form_errors', object: @codeharbor_link) + .form-group + = f.label(:push_url) + = f.text_field :push_url, data: {toggle: 'tooltip', placement: 'bottom'}, title: t('codeharbor_link.info.push_url'), class: 'form-control' .form-group = f.label(:oauth2token) - = f.text_field(:oauth2token, class: 'form-control', required: true) - .actions = render('shared/submit_button', f: f, object: @codeharbor_link) + .input-group + = f.text_field(:oauth2token, data: {toggle: 'tooltip', placement: 'bottom'}, title: t('codeharbor_link.info.token'), class: 'form-control oauth2-token') + .input-group-btn + = button_tag t('codeharbor_link.generate'), type: 'button', class: 'generate-oauth2-token btn btn-default' + .field-element.form-group + = f.label(:client_id) + .input-group + = f.text_field(:client_id, data: {toggle: 'tooltip', placement: 'bottom'}, title: t('codeharbor_link.info.client_id'), class: 'form-control client-id') + .input-group-btn + = button_tag t('codeharbor_link.generate'), type: 'button', class: 'generate-client-id btn btn-default' + .field-element.form-group + = f.label(:client_secret) + .input-group + = f.text_field(:client_secret, data: {toggle: 'tooltip', placement: 'bottom'}, title: t('codeharbor_link.info.client_secret'), class: 'form-control client-secret') + .input-group-btn + = button_tag t('codeharbor_link.generate'), type: 'button', class: 'generate-client-secret btn btn-default' + .actions + = render('shared/submit_button', f: f, object: @codeharbor_link) + - if @codeharbor_link.persisted? + = link_to(t('codeharbor_link.delete'), codeharbor_link_path(@codeharbor_link), data: {confirm: t('shared.confirm_destroy')}, method: :delete, class: 'btn btn-danger pull-right') + diff --git a/app/views/codeharbor_links/edit.html.slim b/app/views/codeharbor_links/edit.html.slim index 6cb4a21b..43a92c7e 100644 --- a/app/views/codeharbor_links/edit.html.slim +++ b/app/views/codeharbor_links/edit.html.slim @@ -1,3 +1,3 @@ -h1 = @codeharbor_link +h1 = 'Codeharbor link' = render('form') diff --git a/app/views/codeharbor_links/index.html.slim b/app/views/codeharbor_links/index.html.slim deleted file mode 100644 index be6833c9..00000000 --- a/app/views/codeharbor_links/index.html.slim +++ /dev/null @@ -1,18 +0,0 @@ -h1 = CodeharborLink.model_name.human(count: 2) - -.table-responsive - table.table - thead - tr - th = t('activerecord.attributes.codeharbor_link.oauth2token') - th colspan=3 = t('shared.actions') - tbody - - @codeharbor_links.each do |codeharbor_link| - tr - td = link_to_if(policy(codeharbor_link).show?, codeharbor_link.oauth2token, codeharbor_link) - td = link_to(t('shared.show'), codeharbor_link) if policy(codeharbor_link).show? - td = link_to(t('shared.edit'), edit_codeharbor_link_path(codeharbor_link)) if policy(codeharbor_link).edit? - td = link_to(t('shared.destroy'), codeharbor_link, data: {confirm: t('shared.confirm_destroy')}, method: :delete) if policy(codeharbor_link).destroy? - -= render('shared/pagination', collection: @codeharbor_links) -p = render('shared/new_button', model: CodeharborLink) diff --git a/app/views/codeharbor_links/show.html.slim b/app/views/codeharbor_links/show.html.slim deleted file mode 100644 index b17c86e7..00000000 --- a/app/views/codeharbor_links/show.html.slim +++ /dev/null @@ -1,7 +0,0 @@ -h1 - = @codeharbor_link - = render('shared/edit_button', object: @codeharbor_link) - -- %w[oauth2token].each do |attribute| - = row(label: "codeharbor_link.#{attribute}") do - = content_tag(:input, nil, class: 'form-control', readonly: true, value: @codeharbor_link.send(attribute)) diff --git a/app/views/internal_users/show.html.slim b/app/views/internal_users/show.html.slim index da5620ff..6e876ec9 100644 --- a/app/views/internal_users/show.html.slim +++ b/app/views/internal_users/show.html.slim @@ -8,8 +8,9 @@ h1 = row(label: 'internal_user.role', value: t("users.roles.#{@user.role}")) = row(label: 'internal_user.activated', value: @user.activated?) -- if @user.codeharbor_link.nil? += row(label: 'codeharbor_link.profile_label', value: @user.codeharbor_link.nil? ? link_to(t('codeharbor_link.new'), new_codeharbor_link_path, class: 'btn btn-secondary') : link_to(t('codeharbor_link.edit'), edit_codeharbor_link_path(@user.codeharbor_link), class: 'btn btn-secondary')) - = row(label: 'codeharbor_link.new', value: link_to(t('codeharbor_link.new'), codeharbor_link_new_path, class: 'btn btn-secondary')) -- else - = row(label: 'codeharbor_link.edit', value: link_to(t('codeharbor_link.edit'), edit_codeharbor_link_path(@user.codeharbor_link), class: 'btn btn-secondary')) +/ - if @user.codeharbor_link.nil? +/ = row(label: 'codeharbor_link.profile_label', value: link_to(t('codeharbor_link.new'), codeharbor_link_new_path, class: 'btn btn-secondary')) +/ - else +/ = row(label: 'codeharbor_link.profile_label', value: link_to(t('codeharbor_link.edit'), edit_codeharbor_link_path(@user.codeharbor_link), class: 'btn btn-secondary')) diff --git a/config/locales/en.yml b/config/locales/en.yml index 90f1f69e..ff8d8bb1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -241,9 +241,22 @@ en: show: link: Consumer codeharbor_link: + generate: Generate + info: + push_url: | + The address on CodeHarbor side your exercise can be exported to. If you don't know what to write here, ask an admin. + name: | + Can be anything to Identify your account link. + token: | + Will be used to authenticate your export request. Has to be the same on both sides. + client_id: + Will be sent with your token. Can be automatically generated. + client_secret: + Will be sent with your token. Can be automatically generated. profile_label: Codeharbor Link new: Create link to Codeharbor edit: Edit existing link + delete: Remove Codeharbor link execution_environments: form: hints: diff --git a/config/routes.rb b/config/routes.rb index 0949dea9..9310e946 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -13,7 +13,7 @@ Rails.application.routes.draw do get 'by_file_type/:file_type_id', as: :by_file_type, action: :by_file_type end end - resources :codeharbor_links + resources :codeharbor_links, only: %i[new create edit update destroy] resources :request_for_comments do member do get :mark_as_solved, defaults: { format: :json } From c006bc3dc82f344248363824391688d068901e14 Mon Sep 17 00:00:00 2001 From: Karol Date: Thu, 29 Aug 2019 18:31:32 +0200 Subject: [PATCH 08/49] wip exercise to task implementation --- app/assets/javascripts/codeharbor_link.js | 3 - app/controllers/exercises_controller.rb | 34 +++-- .../convert_exercise_to_task.rb | 129 ++++++++++++++++++ .../convert_task_to_exercise.rb | 2 +- app/services/proforma_service/export_task.rb | 15 ++ 5 files changed, 166 insertions(+), 17 deletions(-) create mode 100644 app/services/proforma_service/convert_exercise_to_task.rb create mode 100644 app/services/proforma_service/export_task.rb diff --git a/app/assets/javascripts/codeharbor_link.js b/app/assets/javascripts/codeharbor_link.js index bf8c74e1..7ada3756 100644 --- a/app/assets/javascripts/codeharbor_link.js +++ b/app/assets/javascripts/codeharbor_link.js @@ -21,17 +21,14 @@ $(document).on('turbolinks:load', function() { }); var generateUUID = (function () { // Public Domain/MIT - // var d = getDate(); return replace('xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'); }); var generateRandomHex32 = (function () { - // var d = getDate(); return replace(Array(32).join('x')); }); - $('.generate-client-id').on('click', function () { $('.client-id').val(generateUUID()); }); diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 33e06fa9..b285454d 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -107,6 +107,19 @@ class ExercisesController < ApplicationController @feedbacks = @exercise.user_exercise_feedbacks.paginate(page: params[:page]) end + def push_proforma_xml + codeharbor_link = CodeharborLink.find(params[:account_link]) + oauth2_client = OAuth2::Client.new(codeharbor_link.client_id, codeharbor_link.client_secret, url: codeharbor_link.push_url, ssl: {verify: false}) + oauth2token = codeharbor_link[:oauth2token] + token = OAuth2::AccessToken.from_hash(oauth2_client, access_token: oauth2token) + + # xml_generator = Proforma::XmlGenerator.new + xml_document = xml_generator.generate_xml(@exercise) + request = token.post(codeharbor_link.push_url, body: xml_document, headers: {'Content-Type' => 'text/xml'}) + puts request + redirect_to @exercise, notice: t('exercises.push_proforma_xml.notice', link: codeharbor_link.push_url) + end + def import_proforma_xml tempfile = Tempfile.new('codeharbor_import.zip') tempfile.write request.body.read.force_encoding('UTF-8') @@ -122,22 +135,17 @@ class ExercisesController < ApplicationController end def user_for_oauth2_request - authorizationHeader = request.headers['Authorization'] - if authorizationHeader == nil - raise ({status: 401, message: 'No Authorization header'}) - end + authorization_header = request.headers['Authorization'] + raise(status: 401, message: 'No Authorization header') if authorization_header.nil? - oauth2Token = authorizationHeader.split(' ')[1] - if oauth2Token == nil || oauth2Token.size == 0 - raise ({status: 401, message: 'No token in Authorization header'}) - end + oauth2_token = authorization_header.split(' ')[1] + raise(status: 401, message: 'No token in Authorization header') if oauth2_token.nil? || oauth2_token.size.zero? - user = user_by_codeharbor_token(oauth2Token) - if user == nil - raise ({status: 401, message: 'Unknown OAuth2 token'}) - end + user = user_by_codeharbor_token(oauth2_token) - return user + raise(status: 401, message: 'Unknown OAuth2 token') if user.nil? + + user end private :user_for_oauth2_request diff --git a/app/services/proforma_service/convert_exercise_to_task.rb b/app/services/proforma_service/convert_exercise_to_task.rb new file mode 100644 index 00000000..11c4b36c --- /dev/null +++ b/app/services/proforma_service/convert_exercise_to_task.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +require 'mimemagic' + +module ProformaService + class ConvertExerciseToTask < ServiceBase + def initialize(exercise: nil) + @exercise = exercise + end + + def execute + create_task + end + + private + + def create_task + Proforma::Task.new( + { + title: @exercise.title, + description: @exercise.description, + internal_description: @exercise.instructions, + + # proglang: proglang, + files: task_files, + # tests: tests, + # uuid: @exercise.uuid, + # parent_uuid: parent_uuid, + # language: primary_description.language, + # model_solutions: model_solutions + }.compact + ) + end + + def parent_uuid + @exercise.clone_relations.first&.origin&.uuid + end + + def primary_description + @exercise.descriptions.select(&:primary?).first + end + + def proglang + {name: @exercise.execution_environment.language, version: @exercise.execution_environment.version} + end + + def model_solutions + @exercise.exercise_files.filter { |file| file.role == 'Reference Implementation' }.map do |file| + Proforma::ModelSolution.new( + id: "ms-#{file.id}", + files: [ + Proforma::TaskFile.new( + id: file.id, + content: file.content, + filename: file.full_file_name, + used_by_grader: false, + usage_by_lms: 'display', + visible: 'delayed', + binary: false, + internal_description: file.role + ) + ] + ) + end + end + + def tests + @exercise.tests.map do |test| + Proforma::Test.new( + id: test.id, + title: test.exercise_file.name, + files: test_file(test.exercise_file), + meta_data: { + 'feedback-message' => test.feedback_message, + 'testing-framework' => test.testing_framework&.name, + 'testing-framework-version' => test.testing_framework&.version + }.compact + ) + end + end + + def test_file(file) + [Proforma::TaskFile.new( + id: file.id, + content: file.content, + filename: file.full_file_name, + used_by_grader: true, + visible: file.hidden ? 'no' : 'yes', + binary: false, + internal_description: file.role || 'Teacher-defined Test' + )] + end + + def task_files + @exercise.files + .filter { |file| !file.role.in? %w[reference_implementation teacher_defined_test] }.map do |file| + task_file(file) + end + end + + def task_file(file) + Proforma::TaskFile.new( + { + id: file.id, + filename: file.path.present? && file.path != '.' ? ::File.join(file.path, file.name_with_extension) : file.name_with_extension, + usage_by_lms: file.read_only ? 'display' : 'edit', + visible: file.hidden ? 'no' : 'yes', + internal_description: file.role || 'regular_file' + }.tap do |params| + if file.native_file.present? + file = ::File.new(file.native_file.file.path, 'r') + params[:content] = file.read + params[:used_by_grader] = false + params[:binary] = true + params[:mimetype] = MimeMagic.by_magic(file).type + else + params[:content] = file.content + params[:used_by_grader] = true + params[:binary] = false + end + end + ) + end + + def attachment_content(file) + Paperclip.io_adapters.for(file.attachment).read + end + end +end diff --git a/app/services/proforma_service/convert_task_to_exercise.rb b/app/services/proforma_service/convert_task_to_exercise.rb index ddeb9030..75af90a8 100644 --- a/app/services/proforma_service/convert_task_to_exercise.rb +++ b/app/services/proforma_service/convert_task_to_exercise.rb @@ -53,7 +53,7 @@ module ProformaService hidden: file.visible == 'no', name: File.basename(file.filename, '.*'), read_only: file.usage_by_lms != 'edit', - role: file.internal_description.underscore.gsub(' ', '_'), + role: file.internal_description, path: File.dirname(file.filename) }.tap do |params| if file.binary diff --git a/app/services/proforma_service/export_task.rb b/app/services/proforma_service/export_task.rb new file mode 100644 index 00000000..72679e23 --- /dev/null +++ b/app/services/proforma_service/export_task.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module ProformaService + class ExportTask < ServiceBase + def initialize(exercise: nil) + @exercise = exercise + end + + def execute + @task = ConvertExerciseToTask.call(exercise: @exercise) + exporter = Proforma::Exporter.new(@task) + exporter.perform + end + end +end From 3c65565b8c1c2601c4ccbf15b5d6bb6e19e03c58 Mon Sep 17 00:00:00 2001 From: Karol Date: Mon, 2 Sep 2019 19:03:50 +0200 Subject: [PATCH 09/49] enable export to codeharbor --- Gemfile | 1 + Gemfile.lock | 3 + app/controllers/exercises_controller.rb | 33 +++++--- app/policies/exercise_policy.rb | 2 +- .../exercise_service/push_external.rb | 28 +++++++ .../convert_exercise_to_task.rb | 82 ++++++++----------- .../convert_task_to_exercise.rb | 3 +- app/views/exercises/index.html.slim | 1 + app/views/exercises/show.html.slim | 1 + config/locales/en.yml | 2 + config/routes.rb | 1 + .../20190830142809_add_uuid_to_exercise.rb | 5 ++ db/schema.rb | 3 +- 13 files changed, 104 insertions(+), 61 deletions(-) create mode 100644 app/services/exercise_service/push_external.rb create mode 100644 db/migrate/20190830142809_add_uuid_to_exercise.rb diff --git a/Gemfile b/Gemfile index 47006ec7..c4d5897e 100644 --- a/Gemfile +++ b/Gemfile @@ -50,6 +50,7 @@ group :development, :staging do gem 'capistrano-rails' gem 'capistrano-rvm' gem 'capistrano-upload-config' + gem 'pry-rails' gem 'rack-mini-profiler' gem 'rubocop', require: false gem 'rubocop-rspec' diff --git a/Gemfile.lock b/Gemfile.lock index 7c80f248..2bfd7a2d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -226,6 +226,8 @@ GEM pry-byebug (3.7.0) byebug (~> 11.0) pry (~> 0.10) + pry-rails (0.3.9) + pry (>= 0.10.4) public_suffix (3.0.3) puma (3.12.1) pundit (2.0.1) @@ -441,6 +443,7 @@ DEPENDENCIES pg proforma! pry-byebug + pry-rails puma pundit rack-mini-profiler diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index b285454d..1421db5a 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -7,7 +7,7 @@ class ExercisesController < ApplicationController before_action :handle_file_uploads, only: [:create, :update] before_action :set_execution_environments, only: [:create, :edit, :new, :update] - before_action :set_exercise_and_authorize, only: MEMBER_ACTIONS + [:clone, :implement, :working_times, :intervention, :search, :run, :statistics, :submit, :reload, :feedback, :study_group_dashboard] + before_action :set_exercise_and_authorize, only: MEMBER_ACTIONS + [:push_proforma_xml, :clone, :implement, :working_times, :intervention, :search, :run, :statistics, :submit, :reload, :feedback, :study_group_dashboard] before_action :set_external_user_and_authorize, only: [:statistics] before_action :set_file_types, only: [:create, :edit, :new, :update] before_action :set_course_token, only: [:implement] @@ -108,16 +108,29 @@ class ExercisesController < ApplicationController end def push_proforma_xml - codeharbor_link = CodeharborLink.find(params[:account_link]) - oauth2_client = OAuth2::Client.new(codeharbor_link.client_id, codeharbor_link.client_secret, url: codeharbor_link.push_url, ssl: {verify: false}) - oauth2token = codeharbor_link[:oauth2token] - token = OAuth2::AccessToken.from_hash(oauth2_client, access_token: oauth2token) + # codeharbor_link = current_user.codeharbor_link # CodeharborLink.find(params[:account_link]) - # xml_generator = Proforma::XmlGenerator.new - xml_document = xml_generator.generate_xml(@exercise) - request = token.post(codeharbor_link.push_url, body: xml_document, headers: {'Content-Type' => 'text/xml'}) - puts request - redirect_to @exercise, notice: t('exercises.push_proforma_xml.notice', link: codeharbor_link.push_url) + error = ExerciseService::PushExternal.call( + zip: ProformaService::ExportTask.call(exercise: @exercise), + codeharbor_link: current_user.codeharbor_link + ) + if error.nil? + redirect_to exercises_path, notice: 'klappt' # t('controllers.exercise.push_external_notice', account_link: account_link.readable) + # redirect_to @exercise, notice: 'klappt' # t('controllers.exercise.push_external_notice', account_link: account_link.readable) + else + # logger.debug(error) + redirect_to exercises_path, alert: 'klappt nicht' # t('controllers.account_links.not_working', account_link: account_link.readable) + # redirect_to @exercise, alert: 'klappt nicht' # t('controllers.account_links.not_working', account_link: account_link.readable) + end + # oauth2_client = OAuth2::Client.new(codeharbor_link.client_id, codeharbor_link.client_secret, url: codeharbor_link.push_url, ssl: {verify: false}) + # oauth2token = codeharbor_link[:oauth2token] + # token = OAuth2::AccessToken.from_hash(oauth2_client, access_token: oauth2token) + + # # xml_generator = Proforma::XmlGenerator.new + # xml_document = xml_generator.generate_xml(@exercise) + # request = token.post(codeharbor_link.push_url, body: xml_document, headers: {'Content-Type' => 'text/xml'}) + # puts request + # redirect_to @exercise, notice: t('exercises.push_proforma_xml.notice', link: codeharbor_link.push_url) end def import_proforma_xml diff --git a/app/policies/exercise_policy.rb b/app/policies/exercise_policy.rb index 662349fe..8a5679ee 100644 --- a/app/policies/exercise_policy.rb +++ b/app/policies/exercise_policy.rb @@ -7,7 +7,7 @@ class ExercisePolicy < AdminOrAuthorPolicy define_method(action) { admin? || teacher? } end - [:clone?, :destroy?, :edit?, :statistics?, :update?, :feedback?].each do |action| + [:clone?, :destroy?, :edit?, :statistics?, :update?, :feedback?, :push_proforma_xml?].each do |action| define_method(action) { admin? || author? } end diff --git a/app/services/exercise_service/push_external.rb b/app/services/exercise_service/push_external.rb new file mode 100644 index 00000000..562ca57f --- /dev/null +++ b/app/services/exercise_service/push_external.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module ExerciseService + class PushExternal < ServiceBase + CODEHARBOR_PUSH_LINK = 'http://localhost:3001/import_exercise' + def initialize(zip:, codeharbor_link:) + @zip = zip + @codeharbor_link = codeharbor_link + end + + def execute + oauth2_client = OAuth2::Client.new(@codeharbor_link.client_id, @codeharbor_link.client_secret, site: CODEHARBOR_PUSH_LINK) + oauth2_token = @codeharbor_link[:oauth2token] + token = OAuth2::AccessToken.from_hash(oauth2_client, access_token: oauth2_token) + body = @zip.string + begin + token.post( + CODEHARBOR_PUSH_LINK, + body: body, + headers: {'Content-Type' => 'application/zip', 'Content-Length' => body.length.to_s} + ) + return nil + rescue StandardError => e + return e + end + end + end +end diff --git a/app/services/proforma_service/convert_exercise_to_task.rb b/app/services/proforma_service/convert_exercise_to_task.rb index 11c4b36c..1af8ac62 100644 --- a/app/services/proforma_service/convert_exercise_to_task.rb +++ b/app/services/proforma_service/convert_exercise_to_task.rb @@ -4,6 +4,8 @@ require 'mimemagic' module ProformaService class ConvertExerciseToTask < ServiceBase + DEFAULT_LANGUAGE = 'de' + def initialize(exercise: nil) @exercise = exercise end @@ -20,75 +22,63 @@ module ProformaService title: @exercise.title, description: @exercise.description, internal_description: @exercise.instructions, - # proglang: proglang, files: task_files, - # tests: tests, - # uuid: @exercise.uuid, + tests: tests, + uuid: uuid, # parent_uuid: parent_uuid, - # language: primary_description.language, - # model_solutions: model_solutions + language: DEFAULT_LANGUAGE, + model_solutions: model_solutions }.compact ) end - def parent_uuid - @exercise.clone_relations.first&.origin&.uuid - end - - def primary_description - @exercise.descriptions.select(&:primary?).first - end - - def proglang - {name: @exercise.execution_environment.language, version: @exercise.execution_environment.version} + def uuid + @exercise.update(uuid: SecureRandom.uuid) if @exercise.uuid.nil? + @exercise.uuid end def model_solutions - @exercise.exercise_files.filter { |file| file.role == 'Reference Implementation' }.map do |file| + @exercise.files.filter { |file| file.role == 'reference_implementation' }.map do |file| Proforma::ModelSolution.new( id: "ms-#{file.id}", - files: [ - Proforma::TaskFile.new( - id: file.id, - content: file.content, - filename: file.full_file_name, - used_by_grader: false, - usage_by_lms: 'display', - visible: 'delayed', - binary: false, - internal_description: file.role - ) - ] + files: model_solution_file(file) ) end end + def model_solution_file(file) + [ + task_file(file).tap do |ms_file| + ms_file.used_by_grader = false + ms_file.usage_by_lms = 'display' + ms_file.visible = 'delayed' + end + ] + end + def tests - @exercise.tests.map do |test| + @exercise.files.filter { |file| file.role == 'teacher_defined_test' }.map do |file| Proforma::Test.new( - id: test.id, - title: test.exercise_file.name, - files: test_file(test.exercise_file), + id: file.id, + title: file.name, + files: test_file(file), meta_data: { - 'feedback-message' => test.feedback_message, - 'testing-framework' => test.testing_framework&.name, - 'testing-framework-version' => test.testing_framework&.version + 'feedback-message' => file.feedback_message + # 'testing-framework' => test.testing_framework&.name, + # 'testing-framework-version' => test.testing_framework&.version }.compact ) end end def test_file(file) - [Proforma::TaskFile.new( - id: file.id, - content: file.content, - filename: file.full_file_name, - used_by_grader: true, - visible: file.hidden ? 'no' : 'yes', - binary: false, - internal_description: file.role || 'Teacher-defined Test' - )] + [ + task_file(file).tap do |t_file| + t_file.used_by_grader = true + t_file.internal_description = 'teacher_defined_test' + end + ] end def task_files @@ -121,9 +111,5 @@ module ProformaService end ) end - - def attachment_content(file) - Paperclip.io_adapters.for(file.attachment).read - end end end diff --git a/app/services/proforma_service/convert_task_to_exercise.rb b/app/services/proforma_service/convert_task_to_exercise.rb index 75af90a8..f42c06da 100644 --- a/app/services/proforma_service/convert_task_to_exercise.rb +++ b/app/services/proforma_service/convert_task_to_exercise.rb @@ -21,7 +21,8 @@ module ProformaService title: @task.title, description: @task.description, instructions: @task.internal_description, - files: files + files: files, + uuid: @task.uuid ) end diff --git a/app/views/exercises/index.html.slim b/app/views/exercises/index.html.slim index 9c26ab6f..bdec864c 100644 --- a/app/views/exercises/index.html.slim +++ b/app/views/exercises/index.html.slim @@ -46,6 +46,7 @@ h1 = Exercise.model_name.human(count: 2) li = link_to(t('activerecord.models.user_exercise_feedback.other'), feedback_exercise_path(exercise), class: 'dropdown-item') if policy(exercise).feedback? li = link_to(t('shared.destroy'), exercise, data: {confirm: t('shared.confirm_destroy')}, method: :delete, class: 'dropdown-item') if policy(exercise).destroy? li = link_to(t('.clone'), clone_exercise_path(exercise), data: {confirm: t('shared.confirm_destroy')}, method: :post, class: 'dropdown-item') if policy(exercise).clone? + li = link_to(t('exercises.export_codeharbor'), push_proforma_xml_exercise_path(exercise), data: {confirm: 'PUSHPUSH?'}, method: :post, class: 'dropdown-item') if policy(exercise).push_proforma_xml? = render('shared/pagination', collection: @exercises) p = render('shared/new_button', model: Exercise) diff --git a/app/views/exercises/show.html.slim b/app/views/exercises/show.html.slim index b9fd33bc..b75417c7 100644 --- a/app/views/exercises/show.html.slim +++ b/app/views/exercises/show.html.slim @@ -20,6 +20,7 @@ h1 = row(label: 'exercise.allow_file_creation', value: @exercise.allow_file_creation?) = row(label: 'exercise.allow_auto_completion', value: @exercise.allow_auto_completion?) = row(label: 'exercise.difficulty', value: @exercise.expected_difficulty) += row(label: 'exercise.uuid', value: @exercise.uuid) = row(label: 'exercise.tags', value: @exercise.exercise_tags.map{|et| "#{et.tag.name} (#{et.factor})"}.sort.join(", ")) = row(label: 'exercise.embedding_parameters', class: 'mb-4') do = content_tag(:input, nil, class: 'form-control mb-4', readonly: true, value: embedding_parameters(@exercise)) diff --git a/config/locales/en.yml b/config/locales/en.yml index ff8d8bb1..790e8393 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -44,6 +44,7 @@ en: allow_file_creation: "Allow file creation" difficulty: Difficulty token: "Exercise Token" + uuid: UUID proxy_exercise: title: Title files_count: Exercises Count @@ -315,6 +316,7 @@ en: request_for_comments_sent: "Request for comments sent." editor_file_tree: file_root: Files + export_codeharbor: Export to Codeharbor file_form: hints: feedback_message: This message is used as a hint for failing tests. diff --git a/config/routes.rb b/config/routes.rb index 9310e946..9277c4f0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -81,6 +81,7 @@ Rails.application.routes.draw do post :search get :statistics get :feedback + post :push_proforma_xml get :reload post :submit get 'study_group_dashboard/:study_group_id', to: 'exercises#study_group_dashboard' diff --git a/db/migrate/20190830142809_add_uuid_to_exercise.rb b/db/migrate/20190830142809_add_uuid_to_exercise.rb new file mode 100644 index 00000000..4dfa6e5c --- /dev/null +++ b/db/migrate/20190830142809_add_uuid_to_exercise.rb @@ -0,0 +1,5 @@ +class AddUuidToExercise < ActiveRecord::Migration[5.2] + def change + add_column :exercises, :uuid, :uuid + end +end diff --git a/db/schema.rb b/db/schema.rb index ec6a7366..d86763dd 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_08_18_104954) do +ActiveRecord::Schema.define(version: 2019_08_30_142809) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -153,6 +153,7 @@ ActiveRecord::Schema.define(version: 2019_08_18_104954) do t.boolean "allow_file_creation" t.boolean "allow_auto_completion", default: false t.integer "expected_difficulty", default: 1 + t.uuid "uuid" t.index ["id"], name: "index_exercises_on_id" end From 973cc43f4c35dc47d4b8cdfc7db16cda1837f1d9 Mon Sep 17 00:00:00 2001 From: Karol Date: Tue, 3 Sep 2019 15:26:28 +0200 Subject: [PATCH 10/49] self-review --- .../codeharbor_links_controller.rb | 24 +++++-------- app/controllers/exercises_controller.rb | 35 +++++-------------- app/models/codeharbor_link.rb | 1 - .../exercise_service/push_external.rb | 2 +- app/views/exercises/index.html.slim | 2 +- config/locales/en.yml | 5 ++- 6 files changed, 22 insertions(+), 47 deletions(-) diff --git a/app/controllers/codeharbor_links_controller.rb b/app/controllers/codeharbor_links_controller.rb index ddff149f..732d897e 100644 --- a/app/controllers/codeharbor_links_controller.rb +++ b/app/controllers/codeharbor_links_controller.rb @@ -1,20 +1,8 @@ +# frozen_string_literal: true + class CodeharborLinksController < ApplicationController include CommonBehavior - before_action :set_codeharbor_link, only: [:show, :edit, :update, :destroy] - - def authorize! - authorize(@codeharbor_link || @codeharbor_links) - end - private :authorize! - - # def index - # @codeharbor_links = CodeharborLink.where(user_id: current_user.id).paginate(page: params[:page]) - # authorize! - # end - - # def show - # authorize! - # end + before_action :set_codeharbor_link, only: %i[show edit update destroy] def new @codeharbor_link = CodeharborLink.new @@ -33,8 +21,8 @@ class CodeharborLinksController < ApplicationController end def update - update_and_respond(object: @codeharbor_link, params: codeharbor_link_params, path: @codeharbor_link.user) authorize! + update_and_respond(object: @codeharbor_link, params: codeharbor_link_params, path: @codeharbor_link.user) end def destroy @@ -43,6 +31,10 @@ class CodeharborLinksController < ApplicationController private + def authorize! + authorize @codeharbor_link + end + def set_codeharbor_link @codeharbor_link = CodeharborLink.find(params[:id]) @codeharbor_link.user = current_user diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 1421db5a..9e1c057d 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -108,29 +108,15 @@ class ExercisesController < ApplicationController end def push_proforma_xml - # codeharbor_link = current_user.codeharbor_link # CodeharborLink.find(params[:account_link]) - error = ExerciseService::PushExternal.call( zip: ProformaService::ExportTask.call(exercise: @exercise), codeharbor_link: current_user.codeharbor_link ) if error.nil? - redirect_to exercises_path, notice: 'klappt' # t('controllers.exercise.push_external_notice', account_link: account_link.readable) - # redirect_to @exercise, notice: 'klappt' # t('controllers.exercise.push_external_notice', account_link: account_link.readable) + redirect_to exercises_path, notice: t('exercises.export_codeharbor.success') else - # logger.debug(error) - redirect_to exercises_path, alert: 'klappt nicht' # t('controllers.account_links.not_working', account_link: account_link.readable) - # redirect_to @exercise, alert: 'klappt nicht' # t('controllers.account_links.not_working', account_link: account_link.readable) + redirect_to exercises_path, alert: t('exercises.export_codeharbor.fail') end - # oauth2_client = OAuth2::Client.new(codeharbor_link.client_id, codeharbor_link.client_secret, url: codeharbor_link.push_url, ssl: {verify: false}) - # oauth2token = codeharbor_link[:oauth2token] - # token = OAuth2::AccessToken.from_hash(oauth2_client, access_token: oauth2token) - - # # xml_generator = Proforma::XmlGenerator.new - # xml_document = xml_generator.generate_xml(@exercise) - # request = token.post(codeharbor_link.push_url, body: xml_document, headers: {'Content-Type' => 'text/xml'}) - # puts request - # redirect_to @exercise, notice: t('exercises.push_proforma_xml.notice', link: codeharbor_link.push_url) end def import_proforma_xml @@ -138,7 +124,10 @@ class ExercisesController < ApplicationController tempfile.write request.body.read.force_encoding('UTF-8') tempfile.rewind - exercise = ProformaService::Import.call(zip: tempfile, user: user_for_oauth2_request) + user = user_for_oauth2_request + return render json: {}, status: 401 if user.nil? + + exercise = ProformaService::Import.call(zip: tempfile, user: user) if exercise.save render json: {}, status: 201 else @@ -149,16 +138,8 @@ class ExercisesController < ApplicationController def user_for_oauth2_request authorization_header = request.headers['Authorization'] - raise(status: 401, message: 'No Authorization header') if authorization_header.nil? - - oauth2_token = authorization_header.split(' ')[1] - raise(status: 401, message: 'No token in Authorization header') if oauth2_token.nil? || oauth2_token.size.zero? - - user = user_by_codeharbor_token(oauth2_token) - - raise(status: 401, message: 'Unknown OAuth2 token') if user.nil? - - user + oauth2_token = authorization_header&.split(' ')&.second + user_by_codeharbor_token(oauth2_token) end private :user_for_oauth2_request diff --git a/app/models/codeharbor_link.rb b/app/models/codeharbor_link.rb index 5bb1fba1..18f2da02 100644 --- a/app/models/codeharbor_link.rb +++ b/app/models/codeharbor_link.rb @@ -2,7 +2,6 @@ class CodeharborLink < ApplicationRecord validates :oauth2token, presence: true - validates :user_id, presence: true belongs_to :user, foreign_key: :user_id, class_name: 'InternalUser' diff --git a/app/services/exercise_service/push_external.rb b/app/services/exercise_service/push_external.rb index 562ca57f..dfa54468 100644 --- a/app/services/exercise_service/push_external.rb +++ b/app/services/exercise_service/push_external.rb @@ -2,7 +2,7 @@ module ExerciseService class PushExternal < ServiceBase - CODEHARBOR_PUSH_LINK = 'http://localhost:3001/import_exercise' + CODEHARBOR_PUSH_LINK = Rails.env.production? ? 'https://codeharbor.openhpi.de/import_exercise' : 'http://localhost:3001/import_exercise' def initialize(zip:, codeharbor_link:) @zip = zip @codeharbor_link = codeharbor_link diff --git a/app/views/exercises/index.html.slim b/app/views/exercises/index.html.slim index bdec864c..2b854d22 100644 --- a/app/views/exercises/index.html.slim +++ b/app/views/exercises/index.html.slim @@ -46,7 +46,7 @@ h1 = Exercise.model_name.human(count: 2) li = link_to(t('activerecord.models.user_exercise_feedback.other'), feedback_exercise_path(exercise), class: 'dropdown-item') if policy(exercise).feedback? li = link_to(t('shared.destroy'), exercise, data: {confirm: t('shared.confirm_destroy')}, method: :delete, class: 'dropdown-item') if policy(exercise).destroy? li = link_to(t('.clone'), clone_exercise_path(exercise), data: {confirm: t('shared.confirm_destroy')}, method: :post, class: 'dropdown-item') if policy(exercise).clone? - li = link_to(t('exercises.export_codeharbor'), push_proforma_xml_exercise_path(exercise), data: {confirm: 'PUSHPUSH?'}, method: :post, class: 'dropdown-item') if policy(exercise).push_proforma_xml? + li = link_to(t('exercises.export_codeharbor.label'), push_proforma_xml_exercise_path(exercise), method: :post, class: 'dropdown-item') if policy(exercise).push_proforma_xml? = render('shared/pagination', collection: @exercises) p = render('shared/new_button', model: Exercise) diff --git a/config/locales/en.yml b/config/locales/en.yml index 790e8393..6b8b243f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -316,7 +316,10 @@ en: request_for_comments_sent: "Request for comments sent." editor_file_tree: file_root: Files - export_codeharbor: Export to Codeharbor + export_codeharbor: + label: Export to Codeharbor + success: Successfully pushed the exercise to CodeHarbor. + fail: Failed to push the exercise to CodeHarbor. file_form: hints: feedback_message: This message is used as a hint for failing tests. From a7f2d7da34c533991200e71ad9f2d748c3ecc600 Mon Sep 17 00:00:00 2001 From: Karol Date: Thu, 5 Sep 2019 17:41:02 +0200 Subject: [PATCH 11/49] small refactoring --- app/views/codeharbor_links/edit.html.slim | 2 +- app/views/internal_users/show.html.slim | 5 ----- config/environments/development.rb | 6 +++--- db/schema.rb | 3 ++- lib/file_io.rb | 2 +- 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/app/views/codeharbor_links/edit.html.slim b/app/views/codeharbor_links/edit.html.slim index 43a92c7e..1ccf2a3b 100644 --- a/app/views/codeharbor_links/edit.html.slim +++ b/app/views/codeharbor_links/edit.html.slim @@ -1,3 +1,3 @@ -h1 = 'Codeharbor link' +h1 = CodeharborLink.model_name.human = render('form') diff --git a/app/views/internal_users/show.html.slim b/app/views/internal_users/show.html.slim index 6e876ec9..71a16383 100644 --- a/app/views/internal_users/show.html.slim +++ b/app/views/internal_users/show.html.slim @@ -9,8 +9,3 @@ h1 = row(label: 'internal_user.activated', value: @user.activated?) = row(label: 'codeharbor_link.profile_label', value: @user.codeharbor_link.nil? ? link_to(t('codeharbor_link.new'), new_codeharbor_link_path, class: 'btn btn-secondary') : link_to(t('codeharbor_link.edit'), edit_codeharbor_link_path(@user.codeharbor_link), class: 'btn btn-secondary')) - -/ - if @user.codeharbor_link.nil? -/ = row(label: 'codeharbor_link.profile_label', value: link_to(t('codeharbor_link.new'), codeharbor_link_new_path, class: 'btn btn-secondary')) -/ - else -/ = row(label: 'codeharbor_link.profile_label', value: link_to(t('codeharbor_link.edit'), edit_codeharbor_link_path(@user.codeharbor_link), class: 'btn btn-secondary')) diff --git a/config/environments/development.rb b/config/environments/development.rb index ba0880a5..b91a19b5 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -3,15 +3,15 @@ Rails.application.configure do config.webpacker.check_yarn_integrity = true # Settings specified here will take precedence over those in config/application.rb. - config.web_console.whitelisted_ips = '192.168.0.0/16' - + config.web_console.whitelisted_ips = '192.168.0.0/16' + # In the development environment your application's code is reloaded on # every request. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.cache_classes = false # Do not eager load code on boot. - config.eager_load = true + config.eager_load = false # Show full error reports. config.consider_all_requests_local = true diff --git a/db/schema.rb b/db/schema.rb index d86763dd..1f82e621 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_08_30_142809) do +ActiveRecord::Schema.define(version: 2019_09_05_152630) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -154,6 +154,7 @@ ActiveRecord::Schema.define(version: 2019_08_30_142809) do t.boolean "allow_auto_completion", default: false t.integer "expected_difficulty", default: 1 t.uuid "uuid" + t.string "import_checksum" t.index ["id"], name: "index_exercises_on_id" end diff --git a/lib/file_io.rb b/lib/file_io.rb index 39627b99..b19ef187 100644 --- a/lib/file_io.rb +++ b/lib/file_io.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# stole from: https://makandracards.com/makandra/50526-fileio-writing-strings-as-carrierwave-uploads +# stolen from: https://makandracards.com/makandra/50526-fileio-writing-strings-as-carrierwave-uploads class FileIO < StringIO def initialize(stream, filename) super(stream) From 55e49f01f27be9bfd3953d92f52aecbdc3528632 Mon Sep 17 00:00:00 2001 From: Karol Date: Thu, 5 Sep 2019 17:41:12 +0200 Subject: [PATCH 12/49] add import checksum --- app/services/proforma_service/convert_task_to_exercise.rb | 3 ++- .../20190905152630_add_import_checksum_to_exercises.rb | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20190905152630_add_import_checksum_to_exercises.rb diff --git a/app/services/proforma_service/convert_task_to_exercise.rb b/app/services/proforma_service/convert_task_to_exercise.rb index f42c06da..64d0119f 100644 --- a/app/services/proforma_service/convert_task_to_exercise.rb +++ b/app/services/proforma_service/convert_task_to_exercise.rb @@ -22,7 +22,8 @@ module ProformaService description: @task.description, instructions: @task.internal_description, files: files, - uuid: @task.uuid + uuid: @task.uuid, + import_checksum: @task.checksum ) end diff --git a/db/migrate/20190905152630_add_import_checksum_to_exercises.rb b/db/migrate/20190905152630_add_import_checksum_to_exercises.rb new file mode 100644 index 00000000..26a5d365 --- /dev/null +++ b/db/migrate/20190905152630_add_import_checksum_to_exercises.rb @@ -0,0 +1,5 @@ +class AddImportChecksumToExercises < ActiveRecord::Migration[5.2] + def change + add_column :exercises, :import_checksum, :string + end +end From 568796ef869a37a35667c67e1eed4c63850ebe58 Mon Sep 17 00:00:00 2001 From: Karol Date: Thu, 5 Sep 2019 18:33:53 +0200 Subject: [PATCH 13/49] submit checksum on export --- app/services/proforma_service/convert_exercise_to_task.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/services/proforma_service/convert_exercise_to_task.rb b/app/services/proforma_service/convert_exercise_to_task.rb index 1af8ac62..e22aa709 100644 --- a/app/services/proforma_service/convert_exercise_to_task.rb +++ b/app/services/proforma_service/convert_exercise_to_task.rb @@ -28,7 +28,8 @@ module ProformaService uuid: uuid, # parent_uuid: parent_uuid, language: DEFAULT_LANGUAGE, - model_solutions: model_solutions + model_solutions: model_solutions, + import_checksum: @exercise.import_checksum }.compact ) end From 49d438cef8ed7ad0c1119f70414dd5019b7fdfff Mon Sep 17 00:00:00 2001 From: Karol Date: Tue, 24 Sep 2019 18:43:38 +0200 Subject: [PATCH 14/49] add endpoint to check for exercise uuid --- app/controllers/exercises_controller.rb | 15 ++++++++++++--- config/routes.rb | 1 + 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 9e1c057d..750028c0 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -12,9 +12,9 @@ class ExercisesController < ApplicationController before_action :set_file_types, only: [:create, :edit, :new, :update] before_action :set_course_token, only: [:implement] - skip_before_action :verify_authenticity_token, only: [:import_proforma_xml] - skip_after_action :verify_authorized, only: [:import_proforma_xml] - skip_after_action :verify_policy_scoped, only: [:import_proforma_xml], raise: false + skip_before_action :verify_authenticity_token, only: [:import_proforma_xml, :import_uuid_check] + skip_after_action :verify_authorized, only: [:import_proforma_xml, :import_uuid_check] + skip_after_action :verify_policy_scoped, only: [:import_proforma_xml, :import_uuid_check], raise: false def authorize! authorize(@exercise || @exercises) @@ -119,6 +119,15 @@ class ExercisesController < ApplicationController end end + def import_uuid_check + uuid = params[:uuid] + exercise = Exercise.find_by(uuid: uuid) + + return render json: {exercise_found: false, message: 'no_exercise'} if exercise.nil? + + render json: {exercise_found: true, message: 'exercise found, be careful when overwriting or else!'} + end + def import_proforma_xml tempfile = Tempfile.new('codeharbor_import.zip') tempfile.write request.body.read.force_encoding('UTF-8') diff --git a/config/routes.rb b/config/routes.rb index 9277c4f0..52d33d3c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -67,6 +67,7 @@ Rails.application.routes.draw do end post '/import_proforma_xml' => 'exercises#import_proforma_xml' + post '/import_uuid_check' => 'exercises#import_uuid_check' resources :exercises do collection do From 6a296cbe65a7df5fa3352716f9fdc28cfece79fa Mon Sep 17 00:00:00 2001 From: Karol Date: Mon, 7 Oct 2019 18:50:07 +0200 Subject: [PATCH 15/49] fix exercise_file duplication bug --- app/controllers/exercises_controller.rb | 15 ++++++++------- app/models/code_ocean/file.rb | 1 + app/models/exercise.rb | 2 +- app/services/proforma_service/import.rb | 8 ++++++-- config/locales/en.yml | 4 +++- 5 files changed, 19 insertions(+), 11 deletions(-) diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 750028c0..8a9ed527 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -123,7 +123,7 @@ class ExercisesController < ApplicationController uuid = params[:uuid] exercise = Exercise.find_by(uuid: uuid) - return render json: {exercise_found: false, message: 'no_exercise'} if exercise.nil? + return render json: {exercise_found: false, message: t('exercises.export_codeharbor.check.no_exercise')} if exercise.nil? render json: {exercise_found: true, message: 'exercise found, be careful when overwriting or else!'} end @@ -136,13 +136,14 @@ class ExercisesController < ApplicationController user = user_for_oauth2_request return render json: {}, status: 401 if user.nil? - exercise = ProformaService::Import.call(zip: tempfile, user: user) - if exercise.save - render json: {}, status: 201 - else - logger.info(exercise.errors.full_messages) - render json: {}, status: 400 + exercise = nil + ActiveRecord::Base.transaction do + exercise = ::ProformaService::Import.call(zip: tempfile, user: user) + exercise.save! + return render json: {}, status: 201 end + logger.info(exercise.errors.full_messages) + render json: {}, status: 400 end def user_for_oauth2_request diff --git a/app/models/code_ocean/file.rb b/app/models/code_ocean/file.rb index a68b585b..91ad6e54 100644 --- a/app/models/code_ocean/file.rb +++ b/app/models/code_ocean/file.rb @@ -7,6 +7,7 @@ module CodeOcean def validate(record) existing_files = File.where(name: record.name, path: record.path, file_type_id: record.file_type_id, context_id: record.context_id, context_type: record.context_type).to_a + unless existing_files.empty? if (not record.context.is_a?(Exercise)) || (record.context.new_record?) record.errors[:base] << 'Duplicate' diff --git a/app/models/exercise.rb b/app/models/exercise.rb index 05e69dcf..f975c45e 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -35,13 +35,13 @@ class Exercise < ApplicationRecord validates :public, boolean_presence: true validates :title, presence: true validates :token, presence: true, uniqueness: true + validates_uniqueness_of :uuid @working_time_statistics = nil attr_reader :working_time_statistics MAX_EXERCISE_FEEDBACKS = 20 - def average_percentage if average_score and maximum_score != 0.0 and submissions.exists?(cause: 'submit') (average_score / maximum_score * 100).round(2) diff --git a/app/services/proforma_service/import.rb b/app/services/proforma_service/import.rb index a4558870..7ab31055 100644 --- a/app/services/proforma_service/import.rb +++ b/app/services/proforma_service/import.rb @@ -11,8 +11,12 @@ module ProformaService if single_task? importer = Proforma::Importer.new(@zip) @task = importer.perform - exercise = ConvertTaskToExercise.call(task: @task, user: @user) - exercise.save! + + exercise = Exercise.find_by(uuid: @task.uuid) + exercise_files = exercise&.files&.to_a + + exercise = ConvertTaskToExercise.call(task: @task, user: @user, exercise: exercise) + exercise_files&.each(&:destroy) # feels suboptimal exercise else diff --git a/config/locales/en.yml b/config/locales/en.yml index 6b8b243f..af6f191d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -317,9 +317,11 @@ en: editor_file_tree: file_root: Files export_codeharbor: + check: + no_exercise: No exercise found + fail: Failed to push the exercise to CodeHarbor. label: Export to Codeharbor success: Successfully pushed the exercise to CodeHarbor. - fail: Failed to push the exercise to CodeHarbor. file_form: hints: feedback_message: This message is used as a hint for failing tests. From d6d8a803ae5cf1e8dc6687e1d6711698f7caca6e Mon Sep 17 00:00:00 2001 From: Karol Date: Tue, 8 Oct 2019 16:39:55 +0200 Subject: [PATCH 16/49] use proformas git repo --- Gemfile | 2 +- Gemfile.lock | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Gemfile b/Gemfile index c4d5897e..90052eff 100644 --- a/Gemfile +++ b/Gemfile @@ -36,7 +36,7 @@ gem 'webpacker' gem 'rest-client' gem 'rubyzip' gem 'mnemosyne-ruby' -gem 'proforma', path: '../proforma' +gem 'proforma', git: 'git://github.com/openHPI/proforma.git' # use version not master gem 'whenever', require: false gem 'rails-timeago' diff --git a/Gemfile.lock b/Gemfile.lock index 2bfd7a2d..6388bfde 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,13 @@ +GIT + remote: git://github.com/openHPI/proforma.git + revision: 54a5bdf2bafebb548998c945ee68d099d85108df + specs: + proforma (0.1.0) + activemodel (~> 5.2.3) + activesupport (~> 5.2.3) + nokogiri (~> 1.10.2) + rubyzip (>= 1.2.2, < 2.1.0) + GIT remote: https://github.com/gosukiwi/tubesock revision: 86a5ca4f7d3c3a7b9a727ad91df3b9b4912eda39 @@ -7,15 +17,6 @@ GIT rack (>= 1.5.0) websocket (>= 1.1.0) -PATH - remote: ../proforma - specs: - proforma (0.1.0) - activemodel (~> 5.2.3) - activesupport (~> 5.2.3) - nokogiri (~> 1.10.2) - rubyzip (~> 1.2.2) - GEM remote: https://rubygems.org/ specs: From 64f6f088f5ba50e81f71f6686c21a7c9ff0bd4bc Mon Sep 17 00:00:00 2001 From: Karol Date: Tue, 8 Oct 2019 18:31:29 +0200 Subject: [PATCH 17/49] add warnings --- app/controllers/exercises_controller.rb | 4 ++-- config/locales/en.yml | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 8a9ed527..d3a5b31c 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -123,9 +123,9 @@ class ExercisesController < ApplicationController uuid = params[:uuid] exercise = Exercise.find_by(uuid: uuid) - return render json: {exercise_found: false, message: t('exercises.export_codeharbor.check.no_exercise')} if exercise.nil? + return render json: {exercise_found: false, message: t('exercises.import_codeharbor.check.no_exercise')} if exercise.nil? - render json: {exercise_found: true, message: 'exercise found, be careful when overwriting or else!'} + render json: {exercise_found: true, message: t('exercises.import_codeharbor.check.exercise_found')} end def import_proforma_xml diff --git a/config/locales/en.yml b/config/locales/en.yml index af6f191d..493dd631 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -316,9 +316,11 @@ en: request_for_comments_sent: "Request for comments sent." editor_file_tree: file_root: Files - export_codeharbor: + import_codeharbor: check: - no_exercise: No exercise found + no_exercise: No corresponding exercise found on Codeocean. Pushing this exercise will create a new one on Codeocean, which will be linked to this one on Codeharbor, any changes to either one can be pushed to the respective other platform. + exercise_found: A corresponding exercise has been found on Codeocean. You can either
  • Create a new exercise as a duplicate of this one Codeharbor and push it to Codeocean, using the "Create new" button.
  • Overwrite the exercise on Codeocean, by pushing all changes. Only use "Overwrite" for bugfixes or very small changes - it will alter and may break published exercises.
+ export_codeharbor: fail: Failed to push the exercise to CodeHarbor. label: Export to Codeharbor success: Successfully pushed the exercise to CodeHarbor. From 7b2f61e602e0a4293bd9d2edd5a6cdf78d325b5a Mon Sep 17 00:00:00 2001 From: Karol Date: Tue, 8 Oct 2019 18:32:02 +0200 Subject: [PATCH 18/49] add unpublished field to exercise --- db/migrate/20191008163045_add_unpublished_to_exercise.rb | 5 +++++ db/schema.rb | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20191008163045_add_unpublished_to_exercise.rb diff --git a/db/migrate/20191008163045_add_unpublished_to_exercise.rb b/db/migrate/20191008163045_add_unpublished_to_exercise.rb new file mode 100644 index 00000000..3ba9a996 --- /dev/null +++ b/db/migrate/20191008163045_add_unpublished_to_exercise.rb @@ -0,0 +1,5 @@ +class AddUnpublishedToExercise < ActiveRecord::Migration[5.2] + def change + add_column :exercises, :unpublished, :boolean + end +end diff --git a/db/schema.rb b/db/schema.rb index 1f82e621..cc7d022a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_09_05_152630) do +ActiveRecord::Schema.define(version: 2019_10_08_163045) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -155,6 +155,7 @@ ActiveRecord::Schema.define(version: 2019_09_05_152630) do t.integer "expected_difficulty", default: 1 t.uuid "uuid" t.string "import_checksum" + t.boolean "unpublished" t.index ["id"], name: "index_exercises_on_id" end From 45ceacd34b578809ef87a6c1f261e2ae1bbb6475 Mon Sep 17 00:00:00 2001 From: Karol Date: Tue, 8 Oct 2019 18:44:30 +0200 Subject: [PATCH 19/49] update migration --- db/migrate/20191008163045_add_unpublished_to_exercise.rb | 2 +- db/schema.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/db/migrate/20191008163045_add_unpublished_to_exercise.rb b/db/migrate/20191008163045_add_unpublished_to_exercise.rb index 3ba9a996..56bc215d 100644 --- a/db/migrate/20191008163045_add_unpublished_to_exercise.rb +++ b/db/migrate/20191008163045_add_unpublished_to_exercise.rb @@ -1,5 +1,5 @@ class AddUnpublishedToExercise < ActiveRecord::Migration[5.2] def change - add_column :exercises, :unpublished, :boolean + add_column :exercises, :unpublished, :boolean, default: false end end diff --git a/db/schema.rb b/db/schema.rb index cc7d022a..34d1802a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -155,7 +155,7 @@ ActiveRecord::Schema.define(version: 2019_10_08_163045) do t.integer "expected_difficulty", default: 1 t.uuid "uuid" t.string "import_checksum" - t.boolean "unpublished" + t.boolean "unpublished", default: false t.index ["id"], name: "index_exercises_on_id" end From 27ef0d45ddc986580aecf8a5a58ba9a2714a3564 Mon Sep 17 00:00:00 2001 From: Karol Date: Tue, 8 Oct 2019 18:44:45 +0200 Subject: [PATCH 20/49] add unpublished validation --- app/models/exercise.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/exercise.rb b/app/models/exercise.rb index f975c45e..71ec0afb 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -31,8 +31,9 @@ class Exercise < ApplicationRecord validate :valid_main_file? validates :description, presence: true - # validates :execution_environment_id, presence: true # TODO make this conditional - but based on what? + validates :execution_environment_id, presence: true, if: -> { !unpublished? } # TODO make this conditional - but based on what? validates :public, boolean_presence: true + validates :unpublished, boolean_presence: true validates :title, presence: true validates :token, presence: true, uniqueness: true validates_uniqueness_of :uuid From 87798212ada38fc32ee2cca23e8eab5e89fc4727 Mon Sep 17 00:00:00 2001 From: Karol Date: Fri, 11 Oct 2019 16:28:50 +0200 Subject: [PATCH 21/49] add unpublished to views --- app/assets/javascripts/exercises.js.erb | 31 +++++++++++++++++++++++++ app/controllers/exercises_controller.rb | 3 ++- app/models/exercise.rb | 2 +- app/views/exercises/_form.html.slim | 8 +++++-- app/views/exercises/show.html.slim | 3 ++- config/locales/en.yml | 3 +++ 6 files changed, 45 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/exercises.js.erb b/app/assets/javascripts/exercises.js.erb index efe110d2..8eb64f4f 100644 --- a/app/assets/javascripts/exercises.js.erb +++ b/app/assets/javascripts/exercises.js.erb @@ -208,6 +208,35 @@ $(document).on('turbolinks:load', function() { } }); }; + var old_execution_environment = $('#exercise_execution_environment_id').val(); + var observeExecutionEnvironment = function() { + $('#exercise_execution_environment_id').on('change', function(){ + new_execution_environment = $('#exercise_execution_environment_id').val(); + + if(new_execution_environment == '' && !$('#exercise_unpublished').prop('checked')){ + if(confirm('<%= I18n.t('exercises.form.unpublish_warning') %>')){ + $('#exercise_unpublished').prop('checked', true); + } else { + return $('#exercise_execution_environment_id').val(old_execution_environment).trigger("chosen:updated"); + } + } + old_execution_environment = new_execution_environment; + }); + }; + + var observeUnpublishedState = function() { + $('#exercise_unpublished').on('change', function(){ + if($('#exercise_unpublished').prop('checked')){ + if(!confirm('<%= I18n.t('exercises.form.unpublish_warning') %>')){ + $('#exercise_unpublished').prop('checked', false); + } + } else if($('#exercise_execution_environment_id').val() == '') { + alert('<%= I18n.t('exercises.form.no_execution_environment_selected') %>'); + $('#exercise_unpublished').prop('checked', true); + } + }) + + }; var overrideTextareaTabBehavior = function() { $('.form-group textarea[name$="[content]"]').on('keydown', function(event) { @@ -271,6 +300,8 @@ $(document).on('turbolinks:load', function() { enableInlineFileCreation(); inferFileAttributes(); observeFileRoleChanges(); + observeExecutionEnvironment(); + observeUnpublishedState(); overrideTextareaTabBehavior(); } else if ($('#files.jstree').isPresent()) { var fileTypeSelect = $('#code_ocean_file_file_type_id'); diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index d3a5b31c..d3d98675 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -160,7 +160,7 @@ class ExercisesController < ApplicationController private :user_by_codeharbor_token def exercise_params - params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :hide_file_tree, :allow_file_creation, :allow_auto_completion, :title, :expected_difficulty, files_attributes: file_attributes, :tag_ids => []).merge(user_id: current_user.id, user_type: current_user.class.name) if params[:exercise].present? + params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :unpublished, :hide_file_tree, :allow_file_creation, :allow_auto_completion, :title, :expected_difficulty, files_attributes: file_attributes, :tag_ids => []).merge(user_id: current_user.id, user_type: current_user.class.name) if params[:exercise].present? end private :exercise_params @@ -182,6 +182,7 @@ class ExercisesController < ApplicationController private :handle_file_uploads def implement + redirect_to(@exercise, alert: t('exercises.implement.unpublished')) if @exercise.unpublished? # TODO TESTESTEST redirect_to(@exercise, alert: t('exercises.implement.no_files')) unless @exercise.files.visible.exists? user_solved_exercise = @exercise.has_user_solved(current_user) count_interventions_today = UserExerciseIntervention.where(user: current_user).where("created_at >= ?", Time.zone.now.beginning_of_day).count diff --git a/app/models/exercise.rb b/app/models/exercise.rb index 71ec0afb..bf4364db 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -31,7 +31,7 @@ class Exercise < ApplicationRecord validate :valid_main_file? validates :description, presence: true - validates :execution_environment_id, presence: true, if: -> { !unpublished? } # TODO make this conditional - but based on what? + validates :execution_environment_id, presence: true, if: -> { !unpublished? } validates :public, boolean_presence: true validates :unpublished, boolean_presence: true validates :title, presence: true diff --git a/app/views/exercises/_form.html.slim b/app/views/exercises/_form.html.slim index b6d0980b..d9ad42f6 100644 --- a/app/views/exercises/_form.html.slim +++ b/app/views/exercises/_form.html.slim @@ -11,7 +11,7 @@ = f.pagedown :description, input_html: { preview: true, rows: 10 } .form-group = f.label(:execution_environment_id) - = f.collection_select(:execution_environment_id, @execution_environments, :id, :name, {}, class: 'form-control') + = f.collection_select(:execution_environment_id, @execution_environments, :id, :name, {include_blank: 'None'}, class: 'form-control') /.form-group = f.label(:instructions) = f.hidden_field(:instructions) @@ -20,6 +20,10 @@ label.form-check-label = f.check_box(:public, class: 'form-check-input') = t('activerecord.attributes.exercise.public') + .form-check + label.form-check-label + = f.check_box(:unpublished, class: 'form-check-input') + = t('activerecord.attributes.exercise.unpublished') .form-check label.form-check-label = f.check_box(:hide_file_tree, class: 'form-check-input') @@ -66,4 +70,4 @@ ul#dummies.d-none = f.fields_for(:files, CodeOcean::File.new, child_index: 'index') do |files_form| = render('file_form', f: files_form) - .actions = render('shared/submit_button', f: f, object: @exercise) \ No newline at end of file + .actions = render('shared/submit_button', f: f, object: @exercise) diff --git a/app/views/exercises/show.html.slim b/app/views/exercises/show.html.slim index b75417c7..cd56fff1 100644 --- a/app/views/exercises/show.html.slim +++ b/app/views/exercises/show.html.slim @@ -16,6 +16,7 @@ h1 /= row(label: 'exercise.instructions', value: render_markdown(@exercise.instructions)) = row(label: 'exercise.maximum_score', value: @exercise.maximum_score) = row(label: 'exercise.public', value: @exercise.public?) += row(label: 'exercise.unpublished', value: @exercise.unpublished?) = row(label: 'exercise.hide_file_tree', value: @exercise.hide_file_tree?) = row(label: 'exercise.allow_file_creation', value: @exercise.allow_file_creation?) = row(label: 'exercise.allow_auto_completion', value: @exercise.allow_auto_completion?) @@ -23,7 +24,7 @@ h1 = row(label: 'exercise.uuid', value: @exercise.uuid) = row(label: 'exercise.tags', value: @exercise.exercise_tags.map{|et| "#{et.tag.name} (#{et.factor})"}.sort.join(", ")) = row(label: 'exercise.embedding_parameters', class: 'mb-4') do - = content_tag(:input, nil, class: 'form-control mb-4', readonly: true, value: embedding_parameters(@exercise)) + = content_tag(:input, nil, class: 'form-control mb-4', readonly: true, value: @exercise.unpublished? ? 'Exercise is unpublished' : embedding_parameters(@exercise)) h2.mt-4 = t('activerecord.attributes.exercise.files') diff --git a/config/locales/en.yml b/config/locales/en.yml index 493dd631..881af49e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -45,6 +45,7 @@ en: difficulty: Difficulty token: "Exercise Token" uuid: UUID + unpublished: Unpublished proxy_exercise: title: Title files_count: Exercises Count @@ -332,6 +333,8 @@ en: add_file: Add file tags: "Tags" click_to_collapse: "Click to expand/collapse..." + unpublish_warning: This will unpublish the exercise. Any student trying to implement it will get an error message, until it is published again. + no_execution_environment_selected: Select an execution environment before publishing the exercise. implement: alert: text: 'Your browser does not support features required for using %{application_name}. Please access %{application_name} using a modern browser.' From 8e5debd2e4f8eb38beac35115183e9725f4e49d9 Mon Sep 17 00:00:00 2001 From: Karol Date: Mon, 14 Oct 2019 15:57:53 +0200 Subject: [PATCH 22/49] default to unpublished on import --- app/services/proforma_service/convert_task_to_exercise.rb | 2 +- app/views/exercises/show.html.slim | 2 +- config/locales/en.yml | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/services/proforma_service/convert_task_to_exercise.rb b/app/services/proforma_service/convert_task_to_exercise.rb index 64d0119f..0264494f 100644 --- a/app/services/proforma_service/convert_task_to_exercise.rb +++ b/app/services/proforma_service/convert_task_to_exercise.rb @@ -5,7 +5,7 @@ module ProformaService def initialize(task:, user:, exercise: nil) @task = task @user = user - @exercise = exercise || Exercise.new + @exercise = exercise || Exercise.new(unpublished: true) end def execute diff --git a/app/views/exercises/show.html.slim b/app/views/exercises/show.html.slim index cd56fff1..2aaa2993 100644 --- a/app/views/exercises/show.html.slim +++ b/app/views/exercises/show.html.slim @@ -24,7 +24,7 @@ h1 = row(label: 'exercise.uuid', value: @exercise.uuid) = row(label: 'exercise.tags', value: @exercise.exercise_tags.map{|et| "#{et.tag.name} (#{et.factor})"}.sort.join(", ")) = row(label: 'exercise.embedding_parameters', class: 'mb-4') do - = content_tag(:input, nil, class: 'form-control mb-4', readonly: true, value: @exercise.unpublished? ? 'Exercise is unpublished' : embedding_parameters(@exercise)) + = content_tag(:input, nil, class: 'form-control mb-4', readonly: true, value: @exercise.unpublished? ? t('exercises.show.is_unpublished') : embedding_parameters(@exercise)) h2.mt-4 = t('activerecord.attributes.exercise.files') diff --git a/config/locales/en.yml b/config/locales/en.yml index 881af49e..8f32054a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -383,6 +383,8 @@ en: test_files: Test Files feedback: Feedback study_group_dashboard: Live Dashboard + show: + is_unpublished: Exercise is unpublished statistics: average_score: Average Score final_submissions: Final Submissions From 9c009ee4ecbd732e04b080671addc794fa29efa1 Mon Sep 17 00:00:00 2001 From: Karol Date: Mon, 14 Oct 2019 17:49:49 +0200 Subject: [PATCH 23/49] add functionality, when user is not authorized --- app/controllers/exercises_controller.rb | 6 +++++- config/locales/en.yml | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index d3d98675..f007f443 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -120,12 +120,16 @@ class ExercisesController < ApplicationController end def import_uuid_check + user = user_for_oauth2_request + return render json: {}, status: 401 if user.nil? + uuid = params[:uuid] exercise = Exercise.find_by(uuid: uuid) return render json: {exercise_found: false, message: t('exercises.import_codeharbor.check.no_exercise')} if exercise.nil? + return render json: {exercise_found: true, update_right: false, message: t('exercises.import_codeharbor.check.exercise_found_no_right')} unless ExercisePolicy.new(user, exercise).update? - render json: {exercise_found: true, message: t('exercises.import_codeharbor.check.exercise_found')} + render json: {exercise_found: true, update_right: true, message: t('exercises.import_codeharbor.check.exercise_found')} end def import_proforma_xml diff --git a/config/locales/en.yml b/config/locales/en.yml index 8f32054a..f67c6c43 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -320,7 +320,8 @@ en: import_codeharbor: check: no_exercise: No corresponding exercise found on Codeocean. Pushing this exercise will create a new one on Codeocean, which will be linked to this one on Codeharbor, any changes to either one can be pushed to the respective other platform. - exercise_found: A corresponding exercise has been found on Codeocean. You can either
  • Create a new exercise as a duplicate of this one Codeharbor and push it to Codeocean, using the "Create new" button.
  • Overwrite the exercise on Codeocean, by pushing all changes. Only use "Overwrite" for bugfixes or very small changes - it will alter and may break published exercises.
+ exercise_found: A corresponding exercise has been found on Codeocean. You can either
  • Create a new exercise as a duplicate of this one on Codeharbor and push it to Codeocean, using the "Create new" button.
  • Overwrite the exercise on Codeocean, by pushing all changes. Only use "Overwrite" for bugfixes or very small changes - it will alter and may break published exercises.
+ exercise_found_no_right: A corresponding exercise has been found on Codeocean, but you don't have the rights to edit it. You can only
  • Create a new exercise as a duplicate of this one on Codeharbor and push it to Codeocean, using the "Create new" button.
export_codeharbor: fail: Failed to push the exercise to CodeHarbor. label: Export to Codeharbor From 4ab78c170e24de5db1871745f24399dccaa4e402 Mon Sep 17 00:00:00 2001 From: Karol Date: Wed, 16 Oct 2019 19:19:28 +0200 Subject: [PATCH 24/49] add uuid check --- Gemfile | 1 + Gemfile.lock | 1 + app/assets/javascripts/exercises.js.erb | 44 +++++++++++++++++++ app/assets/stylesheets/exercises.css.scss | 27 ++++++++++++ app/controllers/exercises_controller.rb | 44 ++++++++++++++++++- app/policies/exercise_policy.rb | 2 +- app/views/exercises/_export_actions.html.slim | 20 +++++++++ .../exercises/_export_dialogcontent.html.slim | 5 +++ app/views/exercises/index.html.slim | 4 +- config/locales/en.yml | 1 + config/routes.rb | 3 +- ...nt_id_client_secret_to_codeharbor_links.rb | 7 --- ..._rename_oauth2token_in_codeharbor_links.rb | 6 +++ db/schema.rb | 4 +- 14 files changed, 155 insertions(+), 14 deletions(-) create mode 100644 app/views/exercises/_export_actions.html.slim create mode 100644 app/views/exercises/_export_dialogcontent.html.slim delete mode 100644 db/migrate/20190818104954_add_push_url_client_id_client_secret_to_codeharbor_links.rb create mode 100644 db/migrate/20190818104954_add_push_url_rename_oauth2token_in_codeharbor_links.rb diff --git a/Gemfile b/Gemfile index 90052eff..056ce5d7 100644 --- a/Gemfile +++ b/Gemfile @@ -36,6 +36,7 @@ gem 'webpacker' gem 'rest-client' gem 'rubyzip' gem 'mnemosyne-ruby' +gem 'faraday' gem 'proforma', git: 'git://github.com/openHPI/proforma.git' # use version not master gem 'whenever', require: false gem 'rails-timeago' diff --git a/Gemfile.lock b/Gemfile.lock index 6388bfde..4f353f77 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -426,6 +426,7 @@ DEPENDENCIES docker-api eventmachine (= 1.0.9.1) factory_bot_rails + faraday faye-websocket forgery headless diff --git a/app/assets/javascripts/exercises.js.erb b/app/assets/javascripts/exercises.js.erb index 8eb64f4f..26feb6aa 100644 --- a/app/assets/javascripts/exercises.js.erb +++ b/app/assets/javascripts/exercises.js.erb @@ -238,6 +238,48 @@ $(document).on('turbolinks:load', function() { }; + var observeExportStartButtons = function(){ + $('.export-start').on('click', function(e){ + e.preventDefault(); + $('#export-modal').modal({ + height: 250 + }); + $('#export-modal').modal('show'); + exportExerciseStart($(this).attr('data-exercise-id')); + }); + } + + var exportExerciseStart = function(exerciseID) { + var $exerciseDiv = $('#export-exercise'); + // var accountLinkID = $exerciseDiv.attr('data-account-link'); + var $messageDiv = $exerciseDiv.children('.export-message'); + var $actionsDiv = $exerciseDiv.children('.export-exercise-actions'); + + $messageDiv.html('requesting status'); + $actionsDiv.html('spinning'); + + return $.ajax({ + type: 'POST', + url: '/exercises/' + exerciseID + '/export_external_check', + // data: { + // account_link: accountLinkID + // }, + dataType: 'json', + success: function(response) { + if (response.error) { + $messageDiv.html(response.error); + $actionsDiv.html('Retry?'); + } + $messageDiv.html(response.message); + return $actionsDiv.html(response.actions); + }, + error: function(a, b, c) { + return alert('error:' + c); + } + }); + }; + + var overrideTextareaTabBehavior = function() { $('.form-group textarea[name$="[content]"]').on('keydown', function(event) { if (event.which === TAB_KEY_CODE) { @@ -293,6 +335,7 @@ $(document).on('turbolinks:load', function() { // ignore tags table since it is in the dom before other tables if ($('table:not(#tags-table)').isPresent()) { enableBatchUpdate(); + observeExportStartButtons(); } else if ($('.edit_exercise, .new_exercise').isPresent()) { execution_environments = $('form').data('execution-environments'); file_types = $('form').data('file-types'); @@ -303,6 +346,7 @@ $(document).on('turbolinks:load', function() { observeExecutionEnvironment(); observeUnpublishedState(); overrideTextareaTabBehavior(); + } else if ($('#files.jstree').isPresent()) { var fileTypeSelect = $('#code_ocean_file_file_type_id'); fileTypeSelect.on("change", function() {updateFileTemplates(fileTypeSelect.val())}); diff --git a/app/assets/stylesheets/exercises.css.scss b/app/assets/stylesheets/exercises.css.scss index ce3195c1..bc611d17 100644 --- a/app/assets/stylesheets/exercises.css.scss +++ b/app/assets/stylesheets/exercises.css.scss @@ -176,3 +176,30 @@ a.file-heading { } } } + +#export-modal { + .modal-content { + min-height: unset; + height: 300px; + } +} + +#export-exercise{ + display: flex; +} + +.export-message { + flex-grow: 1; + font-size: 12px; + padding-right: 5px; +} + +.export-exercise-actions { + max-width: 110px; + min-width: 110px; +} + +.export-button { + font-size: 12px; + width: 100%; +} diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index f007f443..580c8511 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -7,7 +7,7 @@ class ExercisesController < ApplicationController before_action :handle_file_uploads, only: [:create, :update] before_action :set_execution_environments, only: [:create, :edit, :new, :update] - before_action :set_exercise_and_authorize, only: MEMBER_ACTIONS + [:push_proforma_xml, :clone, :implement, :working_times, :intervention, :search, :run, :statistics, :submit, :reload, :feedback, :study_group_dashboard] + before_action :set_exercise_and_authorize, only: MEMBER_ACTIONS + [:push_proforma_xml, :clone, :implement, :working_times, :intervention, :search, :run, :statistics, :submit, :reload, :feedback, :study_group_dashboard, :export_external_check] before_action :set_external_user_and_authorize, only: [:statistics] before_action :set_file_types, only: [:create, :edit, :new, :update] before_action :set_course_token, only: [:implement] @@ -119,6 +119,48 @@ class ExercisesController < ApplicationController end end + def export_external_check + @codeharbor_link = current_user.codeharbor_link + url = 'http://localhost:3001/import_uuid_check' + + conn = Faraday.new(url: url) do |faraday| + faraday.options[:open_timeout] = 5 + faraday.options[:timeout] = 5 + + faraday.adapter Faraday.default_adapter + end + + error = false + response_hash = {} + message = '' + begin + response = conn.post do |req| + req.headers['Content-Type'] = 'application/json' + req.headers['Authorization'] = 'Bearer ' + @codeharbor_link.api_key + req.body = {uuid: @exercise.uuid}.to_json + end + response_hash = JSON.parse(response.body, symbolize_names: true) + message = response_hash[:message] + rescue Faraday::ClientError + message = 'an error occured' + error = true + end + + render json: { + message: message, + actions: render_to_string( + partial: 'export_actions', + locals: { + exercise: @exercise, + exercise_found: response_hash[:exercise_found], + update_right: response_hash[:update_right], + error: error + } + ) + + }, status: 200 + end + def import_uuid_check user = user_for_oauth2_request return render json: {}, status: 401 if user.nil? diff --git a/app/policies/exercise_policy.rb b/app/policies/exercise_policy.rb index 8a5679ee..e0a8d2a5 100644 --- a/app/policies/exercise_policy.rb +++ b/app/policies/exercise_policy.rb @@ -7,7 +7,7 @@ class ExercisePolicy < AdminOrAuthorPolicy define_method(action) { admin? || teacher? } end - [:clone?, :destroy?, :edit?, :statistics?, :update?, :feedback?, :push_proforma_xml?].each do |action| + [:clone?, :destroy?, :edit?, :statistics?, :update?, :feedback?, :push_proforma_xml?, :export_external_check?].each do |action| define_method(action) { admin? || author? } end diff --git a/app/views/exercises/_export_actions.html.slim b/app/views/exercises/_export_actions.html.slim new file mode 100644 index 00000000..4274ddc4 --- /dev/null +++ b/app/views/exercises/_export_actions.html.slim @@ -0,0 +1,20 @@ +- if error + = button_tag type: 'button', class:'btn btn-primary pull-right export-button', onclick: "exportExerciseStart(#{exercise.id})" do + i.fa.fa-refresh.confirm-icon + = ' Retry' +- else + - if exercise_found + - if update_right + = button_tag type: 'button', class:'btn btn-primary pull-right export-action export-button', data: {'export-type' => 'export'} do + i.fa.fa-check.confirm-icon + = ' Overwrite' + = button_tag type: 'button', class:'btn btn-primary pull-right export-action export-button', data: {'export-type' => 'create_new'} do + i.fa.fa-check.confirm-icon-alt + = ' Create new' + - else + = button_tag type: 'button', class:'btn btn-primary pull-right export-action export-button', data: {'export-type' => 'export'} do + i.fa.fa-check.confirm-icon + = ' Export' += button_tag type: 'submit', class:'btn btn-secondary pull-right export-button', data: {dismiss: 'modal'} do + i.fa.fa-remove.abort-icon + = ' Abort' diff --git a/app/views/exercises/_export_dialogcontent.html.slim b/app/views/exercises/_export_dialogcontent.html.slim new file mode 100644 index 00000000..92066666 --- /dev/null +++ b/app/views/exercises/_export_dialogcontent.html.slim @@ -0,0 +1,5 @@ +#export-exercise + .export-message + = 'This should not be seen' + .export-exercise-actions + = 'This neither' diff --git a/app/views/exercises/index.html.slim b/app/views/exercises/index.html.slim index 2b854d22..22004850 100644 --- a/app/views/exercises/index.html.slim +++ b/app/views/exercises/index.html.slim @@ -46,7 +46,9 @@ h1 = Exercise.model_name.human(count: 2) li = link_to(t('activerecord.models.user_exercise_feedback.other'), feedback_exercise_path(exercise), class: 'dropdown-item') if policy(exercise).feedback? li = link_to(t('shared.destroy'), exercise, data: {confirm: t('shared.confirm_destroy')}, method: :delete, class: 'dropdown-item') if policy(exercise).destroy? li = link_to(t('.clone'), clone_exercise_path(exercise), data: {confirm: t('shared.confirm_destroy')}, method: :post, class: 'dropdown-item') if policy(exercise).clone? - li = link_to(t('exercises.export_codeharbor.label'), push_proforma_xml_exercise_path(exercise), method: :post, class: 'dropdown-item') if policy(exercise).push_proforma_xml? + li = link_to(t('exercises.export_codeharbor.label'), '', class: 'dropdown-item export-start', data: {'exercise-id' => exercise.id}) if policy(exercise).push_proforma_xml? = render('shared/pagination', collection: @exercises) p = render('shared/new_button', model: Exercise) + += render('shared/modal', id: 'export-modal', title: t('exercises.export_codeharbor.dialogtitle'), template: 'exercises/_export_dialogcontent') diff --git a/config/locales/en.yml b/config/locales/en.yml index f67c6c43..8cb2d110 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -326,6 +326,7 @@ en: fail: Failed to push the exercise to CodeHarbor. label: Export to Codeharbor success: Successfully pushed the exercise to CodeHarbor. + dialogtitle: Export to Codeharbor file_form: hints: feedback_message: This message is used as a hint for failing tests. diff --git a/config/routes.rb b/config/routes.rb index 52d33d3c..388fc175 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -82,10 +82,11 @@ Rails.application.routes.draw do post :search get :statistics get :feedback - post :push_proforma_xml get :reload post :submit get 'study_group_dashboard/:study_group_id', to: 'exercises#study_group_dashboard' + post :push_proforma_xml + post :export_external_check end end diff --git a/db/migrate/20190818104954_add_push_url_client_id_client_secret_to_codeharbor_links.rb b/db/migrate/20190818104954_add_push_url_client_id_client_secret_to_codeharbor_links.rb deleted file mode 100644 index 9d041be6..00000000 --- a/db/migrate/20190818104954_add_push_url_client_id_client_secret_to_codeharbor_links.rb +++ /dev/null @@ -1,7 +0,0 @@ -class AddPushUrlClientIdClientSecretToCodeharborLinks < ActiveRecord::Migration[5.2] - def change - add_column :codeharbor_links, :push_url, :string - add_column :codeharbor_links, :client_id, :string - add_column :codeharbor_links, :client_secret, :string - end -end diff --git a/db/migrate/20190818104954_add_push_url_rename_oauth2token_in_codeharbor_links.rb b/db/migrate/20190818104954_add_push_url_rename_oauth2token_in_codeharbor_links.rb new file mode 100644 index 00000000..d3a3c01f --- /dev/null +++ b/db/migrate/20190818104954_add_push_url_rename_oauth2token_in_codeharbor_links.rb @@ -0,0 +1,6 @@ +class AddPushUrlRenameOauth2tokenInCodeharborLinks < ActiveRecord::Migration[5.2] + def change + add_column :codeharbor_links, :push_url, :string + rename_column :codeharbor_links, :oauth2token, :api_key + end +end diff --git a/db/schema.rb b/db/schema.rb index 34d1802a..6cd22c8f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -29,13 +29,11 @@ ActiveRecord::Schema.define(version: 2019_10_08_163045) do end create_table "codeharbor_links", force: :cascade do |t| - t.string "oauth2token", limit: 255 + t.string "api_key", limit: 255 t.datetime "created_at" t.datetime "updated_at" t.integer "user_id" t.string "push_url" - t.string "client_id" - t.string "client_secret" t.index ["user_id"], name: "index_codeharbor_links_on_user_id" end From 7e7be4721ae4d2ac44bd914b6f40b86a7f1aae2a Mon Sep 17 00:00:00 2001 From: Karol Date: Sun, 20 Oct 2019 11:02:57 +0200 Subject: [PATCH 25/49] wip multi-step export --- app/assets/javascripts/exercises.js.erb | 48 ++++++++++++--- app/assets/stylesheets/exercises.css.scss | 18 ++++++ app/controllers/exercises_controller.rb | 60 ++++++++++++++----- app/policies/exercise_policy.rb | 2 +- .../exercise_service/push_external.rb | 23 +++---- app/views/exercises/_export_actions.html.slim | 22 ++++--- config/locales/en.yml | 3 + config/routes.rb | 1 + 8 files changed, 132 insertions(+), 45 deletions(-) diff --git a/app/assets/javascripts/exercises.js.erb b/app/assets/javascripts/exercises.js.erb index 26feb6aa..89e5cf3a 100644 --- a/app/assets/javascripts/exercises.js.erb +++ b/app/assets/javascripts/exercises.js.erb @@ -208,6 +208,7 @@ $(document).on('turbolinks:load', function() { } }); }; + var old_execution_environment = $('#exercise_execution_environment_id').val(); var observeExecutionEnvironment = function() { $('#exercise_execution_environment_id').on('change', function(){ @@ -238,20 +239,25 @@ $(document).on('turbolinks:load', function() { }; - var observeExportStartButtons = function(){ + var observeExportButtons = function(){ $('.export-start').on('click', function(e){ e.preventDefault(); $('#export-modal').modal({ height: 250 }); $('#export-modal').modal('show'); - exportExerciseStart($(this).attr('data-exercise-id')); + exportExerciseStart($(this).data().exerciseId); + }); + $('body').on('click', '.export-retry-button', function(){ + exportExerciseStart($(this).data().exerciseId); + }); + $('body').on('click', '.export-action', function(){ + exportExerciseConfirm($(this).data().exerciseId, $(this).data().exportType); }); } var exportExerciseStart = function(exerciseID) { var $exerciseDiv = $('#export-exercise'); - // var accountLinkID = $exerciseDiv.attr('data-account-link'); var $messageDiv = $exerciseDiv.children('.export-message'); var $actionsDiv = $exerciseDiv.children('.export-exercise-actions'); @@ -261,9 +267,6 @@ $(document).on('turbolinks:load', function() { return $.ajax({ type: 'POST', url: '/exercises/' + exerciseID + '/export_external_check', - // data: { - // account_link: accountLinkID - // }, dataType: 'json', success: function(response) { if (response.error) { @@ -279,6 +282,37 @@ $(document).on('turbolinks:load', function() { }); }; + var exportExerciseConfirm = function(exerciseID, pushType) { + var $exerciseDiv = $('#export-exercise'); + var $messageDiv = $exerciseDiv.children('.export-message'); + var $actionsDiv = $exerciseDiv.children('.export-exercise-actions'); + + return $.ajax({ + type: 'POST', + url: '/exercises/' + exerciseID + '/export_external_confirm', + data: { + push_type: pushType + }, + dataType: 'json', + success: function(response) { + $messageDiv.html(response.message) + $actionsDiv.html(response.actions); + + if(response.status == 'success') { + $messageDiv.addClass('export-success'); + setTimeout((function() { + $('#export-modal').modal('hide'); + $messageDiv.html('').removeClass('export-success'); + }), 3000); + } else { + $messageDiv.addClass('export-failure'); + } + }, + error: function(a, b, c) { + return alert('error:' + c); + } + }); + }; var overrideTextareaTabBehavior = function() { $('.form-group textarea[name$="[content]"]').on('keydown', function(event) { @@ -335,7 +369,7 @@ $(document).on('turbolinks:load', function() { // ignore tags table since it is in the dom before other tables if ($('table:not(#tags-table)').isPresent()) { enableBatchUpdate(); - observeExportStartButtons(); + observeExportButtons(); } else if ($('.edit_exercise, .new_exercise').isPresent()) { execution_environments = $('form').data('execution-environments'); file_types = $('form').data('file-types'); diff --git a/app/assets/stylesheets/exercises.css.scss b/app/assets/stylesheets/exercises.css.scss index bc611d17..1208c846 100644 --- a/app/assets/stylesheets/exercises.css.scss +++ b/app/assets/stylesheets/exercises.css.scss @@ -192,6 +192,14 @@ a.file-heading { flex-grow: 1; font-size: 12px; padding-right: 5px; + word-wrap: break-word; +} +.export-message + :empty { + max-width: 100%; +} + +.export-exercise-actions:empty { + display: none; } .export-exercise-actions { @@ -203,3 +211,13 @@ a.file-heading { font-size: 12px; width: 100%; } + +.export-success { + color: darkgreen; + font-size: 12pt; + font-weight: 600; +} + +.export-failure { + color: darkred; +} diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 580c8511..17effa54 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -7,14 +7,14 @@ class ExercisesController < ApplicationController before_action :handle_file_uploads, only: [:create, :update] before_action :set_execution_environments, only: [:create, :edit, :new, :update] - before_action :set_exercise_and_authorize, only: MEMBER_ACTIONS + [:push_proforma_xml, :clone, :implement, :working_times, :intervention, :search, :run, :statistics, :submit, :reload, :feedback, :study_group_dashboard, :export_external_check] + before_action :set_exercise_and_authorize, only: MEMBER_ACTIONS + [:push_proforma_xml, :clone, :implement, :working_times, :intervention, :search, :run, :statistics, :submit, :reload, :feedback, :study_group_dashboard, :export_external_check, :export_external_confirm] before_action :set_external_user_and_authorize, only: [:statistics] before_action :set_file_types, only: [:create, :edit, :new, :update] before_action :set_course_token, only: [:implement] - skip_before_action :verify_authenticity_token, only: [:import_proforma_xml, :import_uuid_check] - skip_after_action :verify_authorized, only: [:import_proforma_xml, :import_uuid_check] - skip_after_action :verify_policy_scoped, only: [:import_proforma_xml, :import_uuid_check], raise: false + skip_before_action :verify_authenticity_token, only: [:import_proforma_xml, :import_uuid_check, :export_external_confirm] + skip_after_action :verify_authorized, only: [:import_proforma_xml, :import_uuid_check, :export_external_confirm] + skip_after_action :verify_policy_scoped, only: [:import_proforma_xml, :import_uuid_check, :export_external_confirm], raise: false def authorize! authorize(@exercise || @exercises) @@ -141,8 +141,8 @@ class ExercisesController < ApplicationController end response_hash = JSON.parse(response.body, symbolize_names: true) message = response_hash[:message] - rescue Faraday::ClientError - message = 'an error occured' + rescue Faraday::Error => e + message = t('exercises.export_codeharbor.error', message: e.message) error = true end @@ -154,15 +154,45 @@ class ExercisesController < ApplicationController exercise: @exercise, exercise_found: response_hash[:exercise_found], update_right: response_hash[:update_right], - error: error + error: error, + exported: false } ) }, status: 200 end + def export_external_confirm + push_type = params[:push_type] + + return render :fail unless %w[create_new export].include? push_type + + @exercise.uuid = SecureRandom.uuid if push_type == 'create_new' + + error = ExerciseService::PushExternal.call( + zip: ProformaService::ExportTask.call(exercise: @exercise), + codeharbor_link: current_user.codeharbor_link + ) + if error.nil? + render json: { + status: 'success', + message: t('exercises.export_codeharbor.successfully_exported', id: @exercise.id, title: @exercise.title), + actions: render_to_string(partial: 'export_actions', locals: {exercise: @exercise, exported: true, error: error}) + } + # @exercise, notice: t('controllers.exercise.push_external_notice', account_link: account_link.readable) + else + # logger.debug(error) + render json: { + status: 'fail', + message: t('exercises.export_codeharbor.export_failed', id: @exercise.id, title: @exercise.title, error: error), + actions: render_to_string(partial: 'export_actions', locals: {exercise: @exercise, exported: true, error: error}) + } + # redirect_to @exercise, alert: t('controllers.account_links.not_working', account_link: account_link.readable) + end + end + def import_uuid_check - user = user_for_oauth2_request + user = user_from_api_key return render json: {}, status: 401 if user.nil? uuid = params[:uuid] @@ -179,7 +209,7 @@ class ExercisesController < ApplicationController tempfile.write request.body.read.force_encoding('UTF-8') tempfile.rewind - user = user_for_oauth2_request + user = user_from_api_key return render json: {}, status: 401 if user.nil? exercise = nil @@ -192,15 +222,15 @@ class ExercisesController < ApplicationController render json: {}, status: 400 end - def user_for_oauth2_request + def user_from_api_key authorization_header = request.headers['Authorization'] - oauth2_token = authorization_header&.split(' ')&.second - user_by_codeharbor_token(oauth2_token) + api_key = authorization_header&.split(' ')&.second + user_by_codeharbor_token(api_key) end - private :user_for_oauth2_request + private :user_from_api_key - def user_by_codeharbor_token(oauth2_token) - link = CodeharborLink.where(oauth2token: oauth2_token)[0] + def user_by_codeharbor_token(api_key) + link = CodeharborLink.find_by_api_key(api_key) link&.user end private :user_by_codeharbor_token diff --git a/app/policies/exercise_policy.rb b/app/policies/exercise_policy.rb index e0a8d2a5..3d286271 100644 --- a/app/policies/exercise_policy.rb +++ b/app/policies/exercise_policy.rb @@ -7,7 +7,7 @@ class ExercisePolicy < AdminOrAuthorPolicy define_method(action) { admin? || teacher? } end - [:clone?, :destroy?, :edit?, :statistics?, :update?, :feedback?, :push_proforma_xml?, :export_external_check?].each do |action| + [:clone?, :destroy?, :edit?, :statistics?, :update?, :feedback?, :push_proforma_xml?, :export_external_check?, :export_external_confirm?].each do |action| define_method(action) { admin? || author? } end diff --git a/app/services/exercise_service/push_external.rb b/app/services/exercise_service/push_external.rb index dfa54468..3c72ac22 100644 --- a/app/services/exercise_service/push_external.rb +++ b/app/services/exercise_service/push_external.rb @@ -9,19 +9,22 @@ module ExerciseService end def execute - oauth2_client = OAuth2::Client.new(@codeharbor_link.client_id, @codeharbor_link.client_secret, site: CODEHARBOR_PUSH_LINK) - oauth2_token = @codeharbor_link[:oauth2token] - token = OAuth2::AccessToken.from_hash(oauth2_client, access_token: oauth2_token) body = @zip.string begin - token.post( - CODEHARBOR_PUSH_LINK, - body: body, - headers: {'Content-Type' => 'application/zip', 'Content-Length' => body.length.to_s} - ) - return nil + conn = Faraday.new(url: CODEHARBOR_PUSH_LINK) do |faraday| + faraday.adapter Faraday.default_adapter + end + + response = conn.post do |request| + request.headers['Content-Type'] = 'application/zip' + request.headers['Content-Length'] = body.length.to_s + request.headers['Authorization'] = 'Bearer ' + @codeharbor_link.api_key + request.body = body + end + + return response.success? ? nil : response.body rescue StandardError => e - return e + return e.message end end end diff --git a/app/views/exercises/_export_actions.html.slim b/app/views/exercises/_export_actions.html.slim index 4274ddc4..6b5a0554 100644 --- a/app/views/exercises/_export_actions.html.slim +++ b/app/views/exercises/_export_actions.html.slim @@ -1,20 +1,18 @@ - if error - = button_tag type: 'button', class:'btn btn-primary pull-right export-button', onclick: "exportExerciseStart(#{exercise.id})" do + = button_tag type: 'button', class:'btn btn-primary pull-right export-button export-retry-button', data: {exercise_id: exercise.id} do i.fa.fa-refresh.confirm-icon = ' Retry' - else - - if exercise_found + - unless exported - if update_right - = button_tag type: 'button', class:'btn btn-primary pull-right export-action export-button', data: {'export-type' => 'export'} do + = button_tag type: 'button', class:'btn btn-primary pull-right export-action export-button', data: {exercise_id: exercise.id, export_type: 'export'} do i.fa.fa-check.confirm-icon - = ' Overwrite' - = button_tag type: 'button', class:'btn btn-primary pull-right export-action export-button', data: {'export-type' => 'create_new'} do - i.fa.fa-check.confirm-icon-alt - = ' Create new' - - else - = button_tag type: 'button', class:'btn btn-primary pull-right export-action export-button', data: {'export-type' => 'export'} do - i.fa.fa-check.confirm-icon - = ' Export' + = ' Export' + - else + = button_tag type: 'button', class:'btn btn-primary pull-right export-action export-button', data: {exercise_id: exercise.id, export_type: 'create_new'} do + i.fa.fa-check.confirm-icon + = ' Create new' + = button_tag type: 'submit', class:'btn btn-secondary pull-right export-button', data: {dismiss: 'modal'} do i.fa.fa-remove.abort-icon - = ' Abort' + = exported ? ' Close' : ' Abort' diff --git a/config/locales/en.yml b/config/locales/en.yml index 8cb2d110..609f6550 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -327,6 +327,9 @@ en: label: Export to Codeharbor success: Successfully pushed the exercise to CodeHarbor. dialogtitle: Export to Codeharbor + successfully_exported: 'Exercise has successfully been exported.
ID: %{id}
Title: %{title}' + export_failed: 'Export has failed.
ID: %{id}
Title: %{title}

Error: %{error}' + error: 'An error occurred while contacting Codeharbor
Error: %{message}' file_form: hints: feedback_message: This message is used as a hint for failing tests. diff --git a/config/routes.rb b/config/routes.rb index 388fc175..6b744055 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -87,6 +87,7 @@ Rails.application.routes.draw do get 'study_group_dashboard/:study_group_id', to: 'exercises#study_group_dashboard' post :push_proforma_xml post :export_external_check + post :export_external_confirm end end From 9512fe4a79fa996fe5d68f9eabc7dba17ffaf159 Mon Sep 17 00:00:00 2001 From: Karol Date: Sun, 20 Oct 2019 16:20:04 +0200 Subject: [PATCH 26/49] add check_uuid_url to codeharbor_link --- app/assets/javascripts/codeharbor_link.js | 13 +---- app/assets/javascripts/exercises.js.erb | 11 ++-- app/assets/stylesheets/exercises.css.scss | 7 ++- .../codeharbor_links_controller.rb | 5 +- app/controllers/exercises_controller.rb | 56 ++++++------------- app/models/codeharbor_link.rb | 6 +- app/policies/exercise_policy.rb | 2 +- .../exercise_service/push_external.rb | 3 +- app/views/codeharbor_links/_form.html.slim | 21 ++----- app/views/exercises/index.html.slim | 2 +- config/locales/en.yml | 7 ++- config/routes.rb | 1 - ..._rename_oauth2token_in_codeharbor_links.rb | 1 + db/schema.rb | 1 + 14 files changed, 51 insertions(+), 85 deletions(-) diff --git a/app/assets/javascripts/codeharbor_link.js b/app/assets/javascripts/codeharbor_link.js index 7ada3756..4cd49ece 100644 --- a/app/assets/javascripts/codeharbor_link.js +++ b/app/assets/javascripts/codeharbor_link.js @@ -28,17 +28,8 @@ $(document).on('turbolinks:load', function() { return replace(Array(32).join('x')); }); - - $('.generate-client-id').on('click', function () { - $('.client-id').val(generateUUID()); - }); - - $('.generate-client-secret').on('click', function () { - $('.client-secret').val(generateRandomHex32()); - }); - - $('.generate-oauth2-token').on('click', function () { - $('.oauth2-token').val(generateRandomHex32()) + $('.generate-api_key').on('click', function () { + $('.api_key').val(generateRandomHex32()) }); } } diff --git a/app/assets/javascripts/exercises.js.erb b/app/assets/javascripts/exercises.js.erb index 89e5cf3a..95b5539e 100644 --- a/app/assets/javascripts/exercises.js.erb +++ b/app/assets/javascripts/exercises.js.erb @@ -261,18 +261,16 @@ $(document).on('turbolinks:load', function() { var $messageDiv = $exerciseDiv.children('.export-message'); var $actionsDiv = $exerciseDiv.children('.export-exercise-actions'); - $messageDiv.html('requesting status'); - $actionsDiv.html('spinning'); + $messageDiv.removeClass('export-failure'); + + $messageDiv.html('<%= I18n.t('exercises.export_codeharbor.checking_codeharbor') %>'); + $actionsDiv.html('
'); return $.ajax({ type: 'POST', url: '/exercises/' + exerciseID + '/export_external_check', dataType: 'json', success: function(response) { - if (response.error) { - $messageDiv.html(response.error); - $actionsDiv.html('Retry?'); - } $messageDiv.html(response.message); return $actionsDiv.html(response.actions); }, @@ -380,7 +378,6 @@ $(document).on('turbolinks:load', function() { observeExecutionEnvironment(); observeUnpublishedState(); overrideTextareaTabBehavior(); - } else if ($('#files.jstree').isPresent()) { var fileTypeSelect = $('#code_ocean_file_file_type_id'); fileTypeSelect.on("change", function() {updateFileTemplates(fileTypeSelect.val())}); diff --git a/app/assets/stylesheets/exercises.css.scss b/app/assets/stylesheets/exercises.css.scss index 1208c846..e5118ffc 100644 --- a/app/assets/stylesheets/exercises.css.scss +++ b/app/assets/stylesheets/exercises.css.scss @@ -179,8 +179,11 @@ a.file-heading { #export-modal { .modal-content { - min-height: unset; - height: 300px; + min-height: 300px; + } + + .modal-body { + overflow: auto; } } diff --git a/app/controllers/codeharbor_links_controller.rb b/app/controllers/codeharbor_links_controller.rb index 732d897e..8c25df66 100644 --- a/app/controllers/codeharbor_links_controller.rb +++ b/app/controllers/codeharbor_links_controller.rb @@ -5,7 +5,8 @@ class CodeharborLinksController < ApplicationController before_action :set_codeharbor_link, only: %i[show edit update destroy] def new - @codeharbor_link = CodeharborLink.new + base_url = CodeOcean::Config.new(:code_ocean).read[:codeharbor][:url] + @codeharbor_link = CodeharborLink.new(push_url: base_url + '/import_exercise', check_uuid_url: base_url + '/import_uuid_check') authorize! end @@ -42,6 +43,6 @@ class CodeharborLinksController < ApplicationController end def codeharbor_link_params - params.require(:codeharbor_link).permit(:push_url, :oauth2token, :client_id, :client_secret) + params.require(:codeharbor_link).permit(:push_url, :check_uuid_url, :api_key) end end diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 17effa54..6df096fe 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -7,7 +7,7 @@ class ExercisesController < ApplicationController before_action :handle_file_uploads, only: [:create, :update] before_action :set_execution_environments, only: [:create, :edit, :new, :update] - before_action :set_exercise_and_authorize, only: MEMBER_ACTIONS + [:push_proforma_xml, :clone, :implement, :working_times, :intervention, :search, :run, :statistics, :submit, :reload, :feedback, :study_group_dashboard, :export_external_check, :export_external_confirm] + before_action :set_exercise_and_authorize, only: MEMBER_ACTIONS + [:clone, :implement, :working_times, :intervention, :search, :run, :statistics, :submit, :reload, :feedback, :study_group_dashboard, :export_external_check, :export_external_confirm] before_action :set_external_user_and_authorize, only: [:statistics] before_action :set_file_types, only: [:create, :edit, :new, :update] before_action :set_course_token, only: [:implement] @@ -107,54 +107,37 @@ class ExercisesController < ApplicationController @feedbacks = @exercise.user_exercise_feedbacks.paginate(page: params[:page]) end - def push_proforma_xml - error = ExerciseService::PushExternal.call( - zip: ProformaService::ExportTask.call(exercise: @exercise), - codeharbor_link: current_user.codeharbor_link - ) - if error.nil? - redirect_to exercises_path, notice: t('exercises.export_codeharbor.success') - else - redirect_to exercises_path, alert: t('exercises.export_codeharbor.fail') - end - end - def export_external_check @codeharbor_link = current_user.codeharbor_link - url = 'http://localhost:3001/import_uuid_check' - - conn = Faraday.new(url: url) do |faraday| + conn = Faraday.new(url: @codeharbor_link.check_uuid_url) do |faraday| faraday.options[:open_timeout] = 5 faraday.options[:timeout] = 5 faraday.adapter Faraday.default_adapter end - error = false - response_hash = {} - message = '' - begin - response = conn.post do |req| - req.headers['Content-Type'] = 'application/json' - req.headers['Authorization'] = 'Bearer ' + @codeharbor_link.api_key - req.body = {uuid: @exercise.uuid}.to_json - end - response_hash = JSON.parse(response.body, symbolize_names: true) - message = response_hash[:message] - rescue Faraday::Error => e - message = t('exercises.export_codeharbor.error', message: e.message) - error = true - end + codeharbor_check = begin + response = conn.post do |req| + req.headers['Content-Type'] = 'application/json' + req.headers['Authorization'] = 'Bearer ' + @codeharbor_link.api_key + req.body = {uuid: @exercise.uuid}.to_json + end + response_hash = JSON.parse(response.body, symbolize_names: true) + + {error: false}.merge(response_hash.slice(:message, :exercise_found, :update_right)) + rescue Faraday::Error => e + {error: true, message: t('exercises.export_codeharbor.error', message: e.message)} + end render json: { - message: message, + message: codeharbor_check[:message], actions: render_to_string( partial: 'export_actions', locals: { exercise: @exercise, - exercise_found: response_hash[:exercise_found], - update_right: response_hash[:update_right], - error: error, + exercise_found: codeharbor_check[:exercise_found], + update_right: codeharbor_check[:update_right], + error: codeharbor_check[:error], exported: false } ) @@ -179,15 +162,12 @@ class ExercisesController < ApplicationController message: t('exercises.export_codeharbor.successfully_exported', id: @exercise.id, title: @exercise.title), actions: render_to_string(partial: 'export_actions', locals: {exercise: @exercise, exported: true, error: error}) } - # @exercise, notice: t('controllers.exercise.push_external_notice', account_link: account_link.readable) else - # logger.debug(error) render json: { status: 'fail', message: t('exercises.export_codeharbor.export_failed', id: @exercise.id, title: @exercise.title, error: error), actions: render_to_string(partial: 'export_actions', locals: {exercise: @exercise, exported: true, error: error}) } - # redirect_to @exercise, alert: t('controllers.account_links.not_working', account_link: account_link.readable) end end diff --git a/app/models/codeharbor_link.rb b/app/models/codeharbor_link.rb index 18f2da02..b35dc6cf 100644 --- a/app/models/codeharbor_link.rb +++ b/app/models/codeharbor_link.rb @@ -1,11 +1,13 @@ # frozen_string_literal: true class CodeharborLink < ApplicationRecord - validates :oauth2token, presence: true + validates :push_url, presence: true + validates :check_uuid_url, presence: true + validates :api_key, presence: true belongs_to :user, foreign_key: :user_id, class_name: 'InternalUser' def to_s - oauth2token + id.to_s end end diff --git a/app/policies/exercise_policy.rb b/app/policies/exercise_policy.rb index 3d286271..ad305e11 100644 --- a/app/policies/exercise_policy.rb +++ b/app/policies/exercise_policy.rb @@ -7,7 +7,7 @@ class ExercisePolicy < AdminOrAuthorPolicy define_method(action) { admin? || teacher? } end - [:clone?, :destroy?, :edit?, :statistics?, :update?, :feedback?, :push_proforma_xml?, :export_external_check?, :export_external_confirm?].each do |action| + [:clone?, :destroy?, :edit?, :statistics?, :update?, :feedback?, :export_external_check?, :export_external_confirm?].each do |action| define_method(action) { admin? || author? } end diff --git a/app/services/exercise_service/push_external.rb b/app/services/exercise_service/push_external.rb index 3c72ac22..b0d8723f 100644 --- a/app/services/exercise_service/push_external.rb +++ b/app/services/exercise_service/push_external.rb @@ -2,7 +2,6 @@ module ExerciseService class PushExternal < ServiceBase - CODEHARBOR_PUSH_LINK = Rails.env.production? ? 'https://codeharbor.openhpi.de/import_exercise' : 'http://localhost:3001/import_exercise' def initialize(zip:, codeharbor_link:) @zip = zip @codeharbor_link = codeharbor_link @@ -11,7 +10,7 @@ module ExerciseService def execute body = @zip.string begin - conn = Faraday.new(url: CODEHARBOR_PUSH_LINK) do |faraday| + conn = Faraday.new(url: @codeharbor_link.push_url) do |faraday| faraday.adapter Faraday.default_adapter end diff --git a/app/views/codeharbor_links/_form.html.slim b/app/views/codeharbor_links/_form.html.slim index fac3c05c..a07afe1a 100644 --- a/app/views/codeharbor_links/_form.html.slim +++ b/app/views/codeharbor_links/_form.html.slim @@ -4,23 +4,14 @@ = f.label(:push_url) = f.text_field :push_url, data: {toggle: 'tooltip', placement: 'bottom'}, title: t('codeharbor_link.info.push_url'), class: 'form-control' .form-group - = f.label(:oauth2token) + = f.label(:check_uuid_url) + = f.text_field :check_uuid_url, data: {toggle: 'tooltip', placement: 'bottom'}, title: t('codeharbor_link.info.check_uuid_url'), class: 'form-control' + .form-group + = f.label(:api_key) .input-group - = f.text_field(:oauth2token, data: {toggle: 'tooltip', placement: 'bottom'}, title: t('codeharbor_link.info.token'), class: 'form-control oauth2-token') + = f.text_field(:api_key, data: {toggle: 'tooltip', placement: 'bottom'}, title: t('codeharbor_link.info.token'), class: 'form-control api_key') .input-group-btn - = button_tag t('codeharbor_link.generate'), type: 'button', class: 'generate-oauth2-token btn btn-default' - .field-element.form-group - = f.label(:client_id) - .input-group - = f.text_field(:client_id, data: {toggle: 'tooltip', placement: 'bottom'}, title: t('codeharbor_link.info.client_id'), class: 'form-control client-id') - .input-group-btn - = button_tag t('codeharbor_link.generate'), type: 'button', class: 'generate-client-id btn btn-default' - .field-element.form-group - = f.label(:client_secret) - .input-group - = f.text_field(:client_secret, data: {toggle: 'tooltip', placement: 'bottom'}, title: t('codeharbor_link.info.client_secret'), class: 'form-control client-secret') - .input-group-btn - = button_tag t('codeharbor_link.generate'), type: 'button', class: 'generate-client-secret btn btn-default' + = button_tag t('codeharbor_link.generate'), type: 'button', class: 'generate-api_key btn btn-default' .actions = render('shared/submit_button', f: f, object: @codeharbor_link) - if @codeharbor_link.persisted? diff --git a/app/views/exercises/index.html.slim b/app/views/exercises/index.html.slim index 22004850..30af9b9c 100644 --- a/app/views/exercises/index.html.slim +++ b/app/views/exercises/index.html.slim @@ -46,7 +46,7 @@ h1 = Exercise.model_name.human(count: 2) li = link_to(t('activerecord.models.user_exercise_feedback.other'), feedback_exercise_path(exercise), class: 'dropdown-item') if policy(exercise).feedback? li = link_to(t('shared.destroy'), exercise, data: {confirm: t('shared.confirm_destroy')}, method: :delete, class: 'dropdown-item') if policy(exercise).destroy? li = link_to(t('.clone'), clone_exercise_path(exercise), data: {confirm: t('shared.confirm_destroy')}, method: :post, class: 'dropdown-item') if policy(exercise).clone? - li = link_to(t('exercises.export_codeharbor.label'), '', class: 'dropdown-item export-start', data: {'exercise-id' => exercise.id}) if policy(exercise).push_proforma_xml? + li = link_to(t('exercises.export_codeharbor.label'), '', class: 'dropdown-item export-start', data: {'exercise-id' => exercise.id}) if policy(exercise).export_external_confirm? = render('shared/pagination', collection: @exercises) p = render('shared/new_button', model: Exercise) diff --git a/config/locales/en.yml b/config/locales/en.yml index 609f6550..fb9f2e55 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -246,7 +246,9 @@ en: generate: Generate info: push_url: | - The address on CodeHarbor side your exercise can be exported to. If you don't know what to write here, ask an admin. + The url from Codeharbor where your exercise can be exported to. If you don't know what to write here, ask an admin. + check_uuid_url: | + The url from Codeharbor where we can check if the exercise already exists. If you don't know what to write here, ask an admin. name: | Can be anything to Identify your account link. token: | @@ -323,13 +325,12 @@ en: exercise_found: A corresponding exercise has been found on Codeocean. You can either
  • Create a new exercise as a duplicate of this one on Codeharbor and push it to Codeocean, using the "Create new" button.
  • Overwrite the exercise on Codeocean, by pushing all changes. Only use "Overwrite" for bugfixes or very small changes - it will alter and may break published exercises.
exercise_found_no_right: A corresponding exercise has been found on Codeocean, but you don't have the rights to edit it. You can only
  • Create a new exercise as a duplicate of this one on Codeharbor and push it to Codeocean, using the "Create new" button.
export_codeharbor: - fail: Failed to push the exercise to CodeHarbor. label: Export to Codeharbor - success: Successfully pushed the exercise to CodeHarbor. dialogtitle: Export to Codeharbor successfully_exported: 'Exercise has successfully been exported.
ID: %{id}
Title: %{title}' export_failed: 'Export has failed.
ID: %{id}
Title: %{title}

Error: %{error}' error: 'An error occurred while contacting Codeharbor
Error: %{message}' + checking_codeharbor: Checking whether exercise exists on Codeharbor. file_form: hints: feedback_message: This message is used as a hint for failing tests. diff --git a/config/routes.rb b/config/routes.rb index 6b744055..5f562661 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -85,7 +85,6 @@ Rails.application.routes.draw do get :reload post :submit get 'study_group_dashboard/:study_group_id', to: 'exercises#study_group_dashboard' - post :push_proforma_xml post :export_external_check post :export_external_confirm end diff --git a/db/migrate/20190818104954_add_push_url_rename_oauth2token_in_codeharbor_links.rb b/db/migrate/20190818104954_add_push_url_rename_oauth2token_in_codeharbor_links.rb index d3a3c01f..24dd9137 100644 --- a/db/migrate/20190818104954_add_push_url_rename_oauth2token_in_codeharbor_links.rb +++ b/db/migrate/20190818104954_add_push_url_rename_oauth2token_in_codeharbor_links.rb @@ -1,6 +1,7 @@ class AddPushUrlRenameOauth2tokenInCodeharborLinks < ActiveRecord::Migration[5.2] def change add_column :codeharbor_links, :push_url, :string + add_column :codeharbor_links, :check_uuid_url, :string rename_column :codeharbor_links, :oauth2token, :api_key end end diff --git a/db/schema.rb b/db/schema.rb index 6cd22c8f..fcd4e86b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -34,6 +34,7 @@ ActiveRecord::Schema.define(version: 2019_10_08_163045) do t.datetime "updated_at" t.integer "user_id" t.string "push_url" + t.string "check_uuid_url" t.index ["user_id"], name: "index_codeharbor_links_on_user_id" end From f51dde4ef7781b8df9a3a419c2a9c8e12ed2c8f8 Mon Sep 17 00:00:00 2001 From: Karol Date: Mon, 21 Oct 2019 18:03:56 +0200 Subject: [PATCH 27/49] translations and whitespaces --- app/assets/javascripts/exercises.js.erb | 1 - app/controllers/exercises_controller.rb | 1 - app/models/code_ocean/file.rb | 1 - app/services/proforma_service/import.rb | 4 ++-- app/views/exercises/_export_actions.html.slim | 8 ++++---- app/views/exercises/_export_dialogcontent.html.slim | 2 -- app/views/exercises/_form.html.slim | 2 +- config/locales/en.yml | 7 +++++++ 8 files changed, 14 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/exercises.js.erb b/app/assets/javascripts/exercises.js.erb index 95b5539e..08d119ad 100644 --- a/app/assets/javascripts/exercises.js.erb +++ b/app/assets/javascripts/exercises.js.erb @@ -236,7 +236,6 @@ $(document).on('turbolinks:load', function() { $('#exercise_unpublished').prop('checked', true); } }) - }; var observeExportButtons = function(){ diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 6df096fe..48ac4dbd 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -141,7 +141,6 @@ class ExercisesController < ApplicationController exported: false } ) - }, status: 200 end diff --git a/app/models/code_ocean/file.rb b/app/models/code_ocean/file.rb index 91ad6e54..a68b585b 100644 --- a/app/models/code_ocean/file.rb +++ b/app/models/code_ocean/file.rb @@ -7,7 +7,6 @@ module CodeOcean def validate(record) existing_files = File.where(name: record.name, path: record.path, file_type_id: record.file_type_id, context_id: record.context_id, context_type: record.context_type).to_a - unless existing_files.empty? if (not record.context.is_a?(Exercise)) || (record.context.new_record?) record.errors[:base] << 'Duplicate' diff --git a/app/services/proforma_service/import.rb b/app/services/proforma_service/import.rb index 7ab31055..b8ec628b 100644 --- a/app/services/proforma_service/import.rb +++ b/app/services/proforma_service/import.rb @@ -55,8 +55,8 @@ module ProformaService end filenames.select { |f| f[/\.xml$/] }.any? - # rescue Zip::Error - # raise Proforma::InvalidZip + rescue Zip::Error + raise Proforma::InvalidZip end end end diff --git a/app/views/exercises/_export_actions.html.slim b/app/views/exercises/_export_actions.html.slim index 6b5a0554..0008670d 100644 --- a/app/views/exercises/_export_actions.html.slim +++ b/app/views/exercises/_export_actions.html.slim @@ -1,18 +1,18 @@ - if error = button_tag type: 'button', class:'btn btn-primary pull-right export-button export-retry-button', data: {exercise_id: exercise.id} do i.fa.fa-refresh.confirm-icon - = ' Retry' + = t('exercises.export_codeharbor.buttons.retry') - else - unless exported - if update_right = button_tag type: 'button', class:'btn btn-primary pull-right export-action export-button', data: {exercise_id: exercise.id, export_type: 'export'} do i.fa.fa-check.confirm-icon - = ' Export' + = t('exercises.export_codeharbor.buttons.export') - else = button_tag type: 'button', class:'btn btn-primary pull-right export-action export-button', data: {exercise_id: exercise.id, export_type: 'create_new'} do i.fa.fa-check.confirm-icon - = ' Create new' + = t('exercises.export_codeharbor.buttons.create_new') = button_tag type: 'submit', class:'btn btn-secondary pull-right export-button', data: {dismiss: 'modal'} do i.fa.fa-remove.abort-icon - = exported ? ' Close' : ' Abort' + = exported ? t('exercises.export_codeharbor.buttons.close') : t('exercises.export_codeharbor.buttons.abort') diff --git a/app/views/exercises/_export_dialogcontent.html.slim b/app/views/exercises/_export_dialogcontent.html.slim index 92066666..bae04880 100644 --- a/app/views/exercises/_export_dialogcontent.html.slim +++ b/app/views/exercises/_export_dialogcontent.html.slim @@ -1,5 +1,3 @@ #export-exercise .export-message - = 'This should not be seen' .export-exercise-actions - = 'This neither' diff --git a/app/views/exercises/_form.html.slim b/app/views/exercises/_form.html.slim index d9ad42f6..dcf31568 100644 --- a/app/views/exercises/_form.html.slim +++ b/app/views/exercises/_form.html.slim @@ -11,7 +11,7 @@ = f.pagedown :description, input_html: { preview: true, rows: 10 } .form-group = f.label(:execution_environment_id) - = f.collection_select(:execution_environment_id, @execution_environments, :id, :name, {include_blank: 'None'}, class: 'form-control') + = f.collection_select(:execution_environment_id, @execution_environments, :id, :name, {include_blank: t('exercises.form.none')}, class: 'form-control') /.form-group = f.label(:instructions) = f.hidden_field(:instructions) diff --git a/config/locales/en.yml b/config/locales/en.yml index fb9f2e55..77d79d4f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -331,6 +331,12 @@ en: export_failed: 'Export has failed.
ID: %{id}
Title: %{title}

Error: %{error}' error: 'An error occurred while contacting Codeharbor
Error: %{message}' checking_codeharbor: Checking whether exercise exists on Codeharbor. + buttons: + retry: ' Retry' + export: ' Export' + create_new: ' Create new' + close: ' Close' + abort: ' Abort' file_form: hints: feedback_message: This message is used as a hint for failing tests. @@ -341,6 +347,7 @@ en: click_to_collapse: "Click to expand/collapse..." unpublish_warning: This will unpublish the exercise. Any student trying to implement it will get an error message, until it is published again. no_execution_environment_selected: Select an execution environment before publishing the exercise. + none: None implement: alert: text: 'Your browser does not support features required for using %{application_name}. Please access %{application_name} using a modern browser.' From 8767b183cf56a73c966a69c685626e9d9ffcc6a9 Mon Sep 17 00:00:00 2001 From: Karol Date: Tue, 22 Oct 2019 18:41:06 +0200 Subject: [PATCH 28/49] rename action, translation fixes --- app/controllers/exercises_controller.rb | 9 ++++----- config/locales/en.yml | 2 +- config/routes.rb | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 48ac4dbd..d2a90f98 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -12,9 +12,9 @@ class ExercisesController < ApplicationController before_action :set_file_types, only: [:create, :edit, :new, :update] before_action :set_course_token, only: [:implement] - skip_before_action :verify_authenticity_token, only: [:import_proforma_xml, :import_uuid_check, :export_external_confirm] - skip_after_action :verify_authorized, only: [:import_proforma_xml, :import_uuid_check, :export_external_confirm] - skip_after_action :verify_policy_scoped, only: [:import_proforma_xml, :import_uuid_check, :export_external_confirm], raise: false + skip_before_action :verify_authenticity_token, only: [:import_exercise, :import_uuid_check, :export_external_confirm] + skip_after_action :verify_authorized, only: [:import_exercise, :import_uuid_check, :export_external_confirm] + skip_after_action :verify_policy_scoped, only: [:import_exercise, :import_uuid_check, :export_external_confirm], raise: false def authorize! authorize(@exercise || @exercises) @@ -36,7 +36,6 @@ class ExercisesController < ApplicationController } end - def experimental_course?(course_token) experimental_courses.has_value?(course_token) end @@ -183,7 +182,7 @@ class ExercisesController < ApplicationController render json: {exercise_found: true, update_right: true, message: t('exercises.import_codeharbor.check.exercise_found')} end - def import_proforma_xml + def import_exercise tempfile = Tempfile.new('codeharbor_import.zip') tempfile.write request.body.read.force_encoding('UTF-8') tempfile.rewind diff --git a/config/locales/en.yml b/config/locales/en.yml index 77d79d4f..a80ea006 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -330,7 +330,7 @@ en: successfully_exported: 'Exercise has successfully been exported.
ID: %{id}
Title: %{title}' export_failed: 'Export has failed.
ID: %{id}
Title: %{title}

Error: %{error}' error: 'An error occurred while contacting Codeharbor
Error: %{message}' - checking_codeharbor: Checking whether exercise exists on Codeharbor. + checking_codeharbor: Checking if the exercise exists on Codeharbor. buttons: retry: ' Retry' export: ' Export' diff --git a/config/routes.rb b/config/routes.rb index 5f562661..7f552b2d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -66,7 +66,7 @@ Rails.application.routes.draw do end end - post '/import_proforma_xml' => 'exercises#import_proforma_xml' + post '/import_exercise' => 'exercises#import_exercise' post '/import_uuid_check' => 'exercises#import_uuid_check' resources :exercises do From c0a0b44c4dfdc5192d66b56a297a427fa52c9bec Mon Sep 17 00:00:00 2001 From: Karol Date: Thu, 24 Oct 2019 18:07:51 +0200 Subject: [PATCH 29/49] fix translations --- config/locales/en.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index a80ea006..b0a7ccd3 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -332,11 +332,11 @@ en: error: 'An error occurred while contacting Codeharbor
Error: %{message}' checking_codeharbor: Checking if the exercise exists on Codeharbor. buttons: - retry: ' Retry' - export: ' Export' - create_new: ' Create new' - close: ' Close' - abort: ' Abort' + retry: Retry + export: Export + create_new: Create new + close: Close + abort: Abort file_form: hints: feedback_message: This message is used as a hint for failing tests. From 3912caab1cecfdadbb77504714891bfad77af33a Mon Sep 17 00:00:00 2001 From: Karol Date: Fri, 25 Oct 2019 16:25:57 +0200 Subject: [PATCH 30/49] support better errorhandling for codeharbor --- Gemfile | 2 +- Gemfile.lock | 19 +++++------ app/controllers/exercises_controller.rb | 29 ++++------------ .../exercise_service/check_external.rb | 34 +++++++++++++++++++ .../exercise_service/push_external.rb | 14 +++++--- config/locales/en.yml | 3 ++ 6 files changed, 63 insertions(+), 38 deletions(-) create mode 100644 app/services/exercise_service/check_external.rb diff --git a/Gemfile b/Gemfile index 056ce5d7..02789f5b 100644 --- a/Gemfile +++ b/Gemfile @@ -37,7 +37,7 @@ gem 'rest-client' gem 'rubyzip' gem 'mnemosyne-ruby' gem 'faraday' -gem 'proforma', git: 'git://github.com/openHPI/proforma.git' # use version not master +gem 'proforma', path: '../proforma' #, git: 'git://github.com/openHPI/proforma.git' # use version not master gem 'whenever', require: false gem 'rails-timeago' diff --git a/Gemfile.lock b/Gemfile.lock index 4f353f77..862b636c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,13 +1,3 @@ -GIT - remote: git://github.com/openHPI/proforma.git - revision: 54a5bdf2bafebb548998c945ee68d099d85108df - specs: - proforma (0.1.0) - activemodel (~> 5.2.3) - activesupport (~> 5.2.3) - nokogiri (~> 1.10.2) - rubyzip (>= 1.2.2, < 2.1.0) - GIT remote: https://github.com/gosukiwi/tubesock revision: 86a5ca4f7d3c3a7b9a727ad91df3b9b4912eda39 @@ -17,6 +7,15 @@ GIT rack (>= 1.5.0) websocket (>= 1.1.0) +PATH + remote: ../proforma + specs: + proforma (0.1.0) + activemodel (~> 5.2.3) + activesupport (~> 5.2.3) + nokogiri (~> 1.10.2) + rubyzip (>= 1.2.2, < 2.1.0) + GEM remote: https://rubygems.org/ specs: diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index d2a90f98..77f35ba4 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -107,26 +107,7 @@ class ExercisesController < ApplicationController end def export_external_check - @codeharbor_link = current_user.codeharbor_link - conn = Faraday.new(url: @codeharbor_link.check_uuid_url) do |faraday| - faraday.options[:open_timeout] = 5 - faraday.options[:timeout] = 5 - - faraday.adapter Faraday.default_adapter - end - - codeharbor_check = begin - response = conn.post do |req| - req.headers['Content-Type'] = 'application/json' - req.headers['Authorization'] = 'Bearer ' + @codeharbor_link.api_key - req.body = {uuid: @exercise.uuid}.to_json - end - response_hash = JSON.parse(response.body, symbolize_names: true) - - {error: false}.merge(response_hash.slice(:message, :exercise_found, :update_right)) - rescue Faraday::Error => e - {error: true, message: t('exercises.export_codeharbor.error', message: e.message)} - end + codeharbor_check = ExerciseService::CheckExternal.call(uuid: @exercise.uuid, codeharbor_link: current_user.codeharbor_link) render json: { message: codeharbor_check[:message], @@ -196,8 +177,12 @@ class ExercisesController < ApplicationController exercise.save! return render json: {}, status: 201 end - logger.info(exercise.errors.full_messages) - render json: {}, status: 400 + # logger.info(exercise.errors.full_messages) + # render json: {}, status: 400 + rescue Proforma::ProformaError + render json: t('exercises.export_codeharbor.export_errors.invalid'), status: 400 + rescue StandardError + render json: t('exercises.export_codeharbor.export_errors.internal_error'), status: 500 end def user_from_api_key diff --git a/app/services/exercise_service/check_external.rb b/app/services/exercise_service/check_external.rb new file mode 100644 index 00000000..1cce7614 --- /dev/null +++ b/app/services/exercise_service/check_external.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module ExerciseService + class CheckExternal < ServiceBase + def initialize(uuid:, codeharbor_link:) + @uuid = uuid + @codeharbor_link = codeharbor_link + end + + def execute + response = connection.post do |req| + req.headers['Content-Type'] = 'application/json' + req.headers['Authorization'] = 'Bearer ' + @codeharbor_link.api_key + req.body = {uuid: @uuid}.to_json + end + response_hash = JSON.parse(response.body, symbolize_names: true) + + {error: false}.merge(response_hash.slice(:message, :exercise_found, :update_right)) + rescue Faraday::Error => e + {error: true, message: t('exercises.export_exercise.error', message: e.message)} + end + + private + + def connection + Faraday.new(url: @codeharbor_link.check_uuid_url) do |faraday| + faraday.options[:open_timeout] = 5 + faraday.options[:timeout] = 5 + + faraday.adapter Faraday.default_adapter + end + end + end +end diff --git a/app/services/exercise_service/push_external.rb b/app/services/exercise_service/push_external.rb index b0d8723f..93dacb81 100644 --- a/app/services/exercise_service/push_external.rb +++ b/app/services/exercise_service/push_external.rb @@ -10,11 +10,7 @@ module ExerciseService def execute body = @zip.string begin - conn = Faraday.new(url: @codeharbor_link.push_url) do |faraday| - faraday.adapter Faraday.default_adapter - end - - response = conn.post do |request| + response = connection.post do |request| request.headers['Content-Type'] = 'application/zip' request.headers['Content-Length'] = body.length.to_s request.headers['Authorization'] = 'Bearer ' + @codeharbor_link.api_key @@ -26,5 +22,13 @@ module ExerciseService return e.message end end + + private + + def connection + Faraday.new(url: @codeharbor_link.push_url) do |faraday| + faraday.adapter Faraday.default_adapter + end + end end end diff --git a/config/locales/en.yml b/config/locales/en.yml index b0a7ccd3..138fa28c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -337,6 +337,9 @@ en: create_new: Create new close: Close abort: Abort + export_errors: + invalid: Invalid exercise + internal_error: An internal error occurred on Codeharbor while importing the exercise. file_form: hints: feedback_message: This message is used as a hint for failing tests. From 94026dcedf160fd5add35ae7f2d4e945e47369c3 Mon Sep 17 00:00:00 2001 From: Karol Date: Tue, 3 Dec 2019 17:49:45 +0100 Subject: [PATCH 31/49] update proforma gem --- Gemfile | 2 +- Gemfile.lock | 8 +++++--- app/controllers/exercises_controller.rb | 3 +-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index 02789f5b..84da2b1a 100644 --- a/Gemfile +++ b/Gemfile @@ -37,7 +37,7 @@ gem 'rest-client' gem 'rubyzip' gem 'mnemosyne-ruby' gem 'faraday' -gem 'proforma', path: '../proforma' #, git: 'git://github.com/openHPI/proforma.git' # use version not master +gem 'proforma', git: 'https://github.com/openHPI/proforma.git', tag: 'v0.3.2' gem 'whenever', require: false gem 'rails-timeago' diff --git a/Gemfile.lock b/Gemfile.lock index 862b636c..42a75329 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,10 +7,12 @@ GIT rack (>= 1.5.0) websocket (>= 1.1.0) -PATH - remote: ../proforma +GIT + remote: https://github.com/openHPI/proforma.git + revision: 6784ace5bb4449fd53f419fa1eb40dfc04e8f086 + tag: v0.3.2 specs: - proforma (0.1.0) + proforma (0.3.2) activemodel (~> 5.2.3) activesupport (~> 5.2.3) nokogiri (~> 1.10.2) diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 77f35ba4..1687f4ab 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -177,11 +177,10 @@ class ExercisesController < ApplicationController exercise.save! return render json: {}, status: 201 end - # logger.info(exercise.errors.full_messages) - # render json: {}, status: 400 rescue Proforma::ProformaError render json: t('exercises.export_codeharbor.export_errors.invalid'), status: 400 rescue StandardError + logger.info(exercise.errors.full_messages) render json: t('exercises.export_codeharbor.export_errors.internal_error'), status: 500 end From 5625fa63b06aebf14349e652c4b73c70e300d06c Mon Sep 17 00:00:00 2001 From: Karol Date: Fri, 6 Dec 2019 17:25:00 +0100 Subject: [PATCH 32/49] add controller specs --- Gemfile | 1 + Gemfile.lock | 9 + .../codeharbor_links_controller.rb | 2 +- app/controllers/exercises_controller.rb | 6 +- .../exercise_service/check_external.rb | 2 +- config/locales/en.yml | 2 +- .../codeharbor_links_controller_spec.rb | 93 ++++++++ spec/controllers/exercises_controller_spec.rb | 199 ++++++++++++++++++ spec/factories/codeharbor_link.rb | 8 + spec/spec_helper.rb | 2 + spec/support/expectations/has_content.rb | 9 + 11 files changed, 326 insertions(+), 7 deletions(-) create mode 100644 spec/controllers/codeharbor_links_controller_spec.rb create mode 100644 spec/factories/codeharbor_link.rb create mode 100644 spec/support/expectations/has_content.rb diff --git a/Gemfile b/Gemfile index f3626183..f980a082 100644 --- a/Gemfile +++ b/Gemfile @@ -74,4 +74,5 @@ group :test do gem 'rspec-autotest' gem 'rspec-rails' gem 'simplecov', require: false + gem 'webmock' end diff --git a/Gemfile.lock b/Gemfile.lock index 853a2f87..fb93dbf4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -128,6 +128,8 @@ GEM chronic (0.10.2) coderay (1.1.2) concurrent-ruby (1.1.5) + crack (0.4.3) + safe_yaml (~> 1.0.0) crass (1.0.5) database_cleaner (1.7.0) debug_inspector (0.0.3) @@ -156,6 +158,7 @@ GEM forgery (0.7.0) globalid (0.4.2) activesupport (>= 4.2.0) + hashdiff (1.0.0) headless (2.3.1) highline (2.0.3) http-accept (1.7.0) @@ -348,6 +351,7 @@ GEM json (~> 2.1) structured_warnings (~> 0.3) rubyzip (2.0.0) + safe_yaml (1.0.5) sass-rails (6.0.0) sassc-rails (~> 2.1, >= 2.1.1) sassc (2.2.1) @@ -409,6 +413,10 @@ GEM activemodel (>= 5.0) bindex (>= 0.4.0) railties (>= 5.0) + webmock (3.7.6) + addressable (>= 2.3.6) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) webpacker (4.2.0) activesupport (>= 4.2) rack-proxy (>= 0.6.1) @@ -491,6 +499,7 @@ DEPENDENCIES turbolinks uglifier web-console + webmock webpacker whenever diff --git a/app/controllers/codeharbor_links_controller.rb b/app/controllers/codeharbor_links_controller.rb index 8c25df66..802448f6 100644 --- a/app/controllers/codeharbor_links_controller.rb +++ b/app/controllers/codeharbor_links_controller.rb @@ -18,7 +18,7 @@ class CodeharborLinksController < ApplicationController @codeharbor_link = CodeharborLink.new(codeharbor_link_params) @codeharbor_link.user = current_user authorize! - create_and_respond(object: @codeharbor_link, path: proc { @codeharbor_link.user }) + create_and_respond(object: @codeharbor_link, path: -> { @codeharbor_link.user }) end def update diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 468d18ac..f0dd0f5a 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -123,14 +123,13 @@ class ExercisesController < ApplicationController def export_external_check codeharbor_check = ExerciseService::CheckExternal.call(uuid: @exercise.uuid, codeharbor_link: current_user.codeharbor_link) - render json: { message: codeharbor_check[:message], actions: render_to_string( partial: 'export_actions', locals: { exercise: @exercise, - exercise_found: codeharbor_check[:exercise_found], + # exercise_found: codeharbor_check[:exercise_found], update_right: codeharbor_check[:update_right], error: codeharbor_check[:error], exported: false @@ -142,7 +141,7 @@ class ExercisesController < ApplicationController def export_external_confirm push_type = params[:push_type] - return render :fail unless %w[create_new export].include? push_type + return render json: {}, status: 500 unless %w[create_new export].include? push_type @exercise.uuid = SecureRandom.uuid if push_type == 'create_new' @@ -195,7 +194,6 @@ class ExercisesController < ApplicationController rescue Proforma::ProformaError render json: t('exercises.export_codeharbor.export_errors.invalid'), status: 400 rescue StandardError - logger.info(exercise.errors.full_messages) render json: t('exercises.export_codeharbor.export_errors.internal_error'), status: 500 end diff --git a/app/services/exercise_service/check_external.rb b/app/services/exercise_service/check_external.rb index 1cce7614..1aac945b 100644 --- a/app/services/exercise_service/check_external.rb +++ b/app/services/exercise_service/check_external.rb @@ -17,7 +17,7 @@ module ExerciseService {error: false}.merge(response_hash.slice(:message, :exercise_found, :update_right)) rescue Faraday::Error => e - {error: true, message: t('exercises.export_exercise.error', message: e.message)} + {error: true, message: I18n.t('exercises.export_codeharbor.error', message: e.message)} end private diff --git a/config/locales/en.yml b/config/locales/en.yml index 76091159..8a0707e1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -327,7 +327,7 @@ en: export_codeharbor: label: Export to Codeharbor dialogtitle: Export to Codeharbor - successfully_exported: 'Exercise has successfully been exported.
ID: %{id}
Title: %{title}' + successfully_exported: 'Exercise has been successfully exported.
ID: %{id}
Title: %{title}' export_failed: 'Export has failed.
ID: %{id}
Title: %{title}

Error: %{error}' error: 'An error occurred while contacting Codeharbor
Error: %{message}' checking_codeharbor: Checking if the exercise exists on Codeharbor. diff --git a/spec/controllers/codeharbor_links_controller_spec.rb b/spec/controllers/codeharbor_links_controller_spec.rb new file mode 100644 index 00000000..b057c32f --- /dev/null +++ b/spec/controllers/codeharbor_links_controller_spec.rb @@ -0,0 +1,93 @@ +require 'rails_helper' + +describe CodeharborLinksController do + let(:user) { FactoryBot.create(:teacher) } + before(:each) { allow(controller).to receive(:current_user).and_return(user) } + + describe 'GET #new' do + before { get :new } + + expect_status(200) + end + + describe 'GET #edit' do + let(:codeharbor_link) { FactoryBot.create(:codeharbor_link, user: user) } + + before { get :edit, params: { id: codeharbor_link.id } } + + expect_status(200) + end + + describe 'POST #create' do + let(:post_request) { post :create, params: {codeharbor_link: params} } + let(:params) { {push_url: 'http://foo.bar/push', check_uuid_url: 'http://foo.bar/check', api_key: 'api_key'} } + + it 'creates a codeharbor_link' do + expect { post_request }.to change(CodeharborLink, :count).by(1) + end + + it 'redirects to user show' do + expect(post_request).to redirect_to(user) + end + + context 'with invalid params' do + let(:params) { {push_url: '', check_uuid_url: '', api_key: ''} } + + it 'does not create a codeharbor_link' do + expect { post_request }.not_to change(CodeharborLink, :count) + end + + it 'redirects to user show' do + post_request + expect(response).to render_template(:new) + end + end + end + + describe 'PUT #update' do + let(:codeharbor_link) { FactoryBot.create(:codeharbor_link, user: user) } + let(:put_request) { patch :update, params: {id: codeharbor_link.id, codeharbor_link: params} } + let(:params) { {push_url: 'http://foo.bar/push', check_uuid_url: 'http://foo.bar/check', api_key: 'api_key'} } + + it 'updates push_url' do + expect { put_request }.to change { codeharbor_link.reload.push_url }.to('http://foo.bar/push') + end + it 'updates check_uuid_url' do + expect { put_request }.to change { codeharbor_link.reload.check_uuid_url }.to('http://foo.bar/check') + end + it 'updates api_key' do + expect { put_request }.to change { codeharbor_link.reload.api_key }.to('api_key') + end + + it 'redirects to user show' do + expect(put_request).to redirect_to(user) + end + + context 'with invalid params' do + let(:params) { {push_url: '', check_uuid_url: '', api_key: ''} } + + it 'does not change codeharbor_link' do + expect { put_request }.not_to change { codeharbor_link.reload.attributes } + end + + it 'redirects to user show' do + put_request + expect(response).to render_template(:edit) + end + end + end + + describe 'DELETE #destroy' do + let!(:codeharbor_link) { FactoryBot.create(:codeharbor_link, user: user) } + let(:destroy_request) { delete :destroy, params: {id: codeharbor_link.id} } + + it 'deletes codeharbor_link' do + expect { destroy_request }.to change(CodeharborLink, :count).by(-1) + end + + it 'redirects to user show' do + expect(destroy_request).to redirect_to(user) + end + end +end + diff --git a/spec/controllers/exercises_controller_spec.rb b/spec/controllers/exercises_controller_spec.rb index 424a5290..3c5289c0 100644 --- a/spec/controllers/exercises_controller_spec.rb +++ b/spec/controllers/exercises_controller_spec.rb @@ -313,4 +313,203 @@ describe ExercisesController do expect_template(:edit) end end + + RSpec::Matchers.define_negated_matcher :not_include, :include + # RSpec::Support::ObjectFormatter.default_instance.max_formatted_output_length = 99999 + + describe 'POST #export_external_check' do + render_views + + let(:post_request) { post :export_external_check, params: { id: exercise.id } } + let!(:codeharbor_link) { FactoryBot.create(:codeharbor_link, user: user) } + let(:external_check_hash) { {message: message, exercise_found: true, update_right: update_right, error: error} } + let(:message) { 'message' } + let(:update_right) { true } + let(:error) {} + + before { allow(ExerciseService::CheckExternal).to receive(:call).with(uuid: exercise.uuid, codeharbor_link: codeharbor_link).and_return(external_check_hash) } + + it 'renders the correct contents as json' do + post_request + expect(JSON.parse(response.body).symbolize_keys[:message]).to eq('message') + expect(JSON.parse(response.body).symbolize_keys[:actions]).to( + include('button').and(include('Abort').and(include('Export'))) + ) + expect(JSON.parse(response.body).symbolize_keys[:actions]).to( + not_include('Retry').and(not_include('Hide')) + ) + end + + context 'when there is an error' do + let(:error) { 'error' } + + it 'renders the correct contents as json' do + post_request + expect(JSON.parse(response.body).symbolize_keys[:message]).to eq('message') + expect(JSON.parse(response.body).symbolize_keys[:actions]).to( + include('button').and(include('Abort')).and(include('Retry')) + ) + expect(JSON.parse(response.body).symbolize_keys[:actions]).to( + not_include('Create new').and(not_include('Export')).and(not_include('Hide')) + ) + end + end + + context 'when update_right is false' do + let(:update_right) { false } + + it 'renders the correct contents as json' do + post_request + expect(JSON.parse(response.body).symbolize_keys[:message]).to eq('message') + expect(JSON.parse(response.body).symbolize_keys[:actions]).to( + include('button').and(include('Abort')).and(include('Create new')) + ) + expect(JSON.parse(response.body).symbolize_keys[:actions]).to( + not_include('Retry').and(not_include('Export')).and(not_include('Hide')) + ) + end + end + end + + describe '#export_external_confirm' do + render_views + + let!(:codeharbor_link) { FactoryBot.create(:codeharbor_link, user: user) } + let(:post_request) { post :export_external_confirm, params: {push_type: push_type, id: exercise.id, codeharbor_link: codeharbor_link.id} } + let(:push_type) { 'create_new' } + let(:error) {} + let(:zip) { 'zip' } + + before do + allow(ProformaService::ExportTask).to receive(:call).with(exercise: exercise).and_return(zip) + allow(ExerciseService::PushExternal).to receive(:call).with(zip: zip, codeharbor_link: codeharbor_link).and_return(error) + end + + it 'renders correct response' do + post_request + + expect(response).to have_http_status(:success) + expect(JSON.parse(response.body).symbolize_keys[:message]).to(include('successfully exported')) + expect(JSON.parse(response.body).symbolize_keys[:status]).to(eql('success')) + expect(JSON.parse(response.body).symbolize_keys[:actions]).to(include('button').and(include('Close'))) + expect(JSON.parse(response.body).symbolize_keys[:actions]).to(not_include('Retry').and(not_include('Abort'))) + end + + context 'when an error occurs' do + let(:error) { 'exampleerror' } + + it 'renders correct response' do + post_request + expect(response).to have_http_status(:success) + expect(JSON.parse(response.body).symbolize_keys[:message]).to(include('failed').and(include('exampleerror'))) + expect(JSON.parse(response.body).symbolize_keys[:status]).to(eql('fail')) + expect(JSON.parse(response.body).symbolize_keys[:actions]).to(include('button').and(include('Retry')).and(include('Close'))) + expect(JSON.parse(response.body).symbolize_keys[:actions]).to(not_include('Abort')) + end + end + + context 'without push_type' do + let(:push_type) {} + + it 'responds with status 500' do + post_request + expect(response).to have_http_status(:internal_server_error) + end + end + end + + describe '#import_uuid_check' do + let(:exercise) { FactoryBot.create(:dummy, uuid: SecureRandom.uuid) } + let!(:codeharbor_link) { FactoryBot.create(:codeharbor_link, user: user) } + let(:uuid) { exercise.reload.uuid } + let(:post_request) { post :import_uuid_check, params: {uuid: uuid} } + let(:headers) { {'Authorization' => "Bearer #{codeharbor_link.api_key}"} } + + before { request.headers.merge! headers } + + it 'renders correct response' do + post_request + expect(response).to have_http_status(:success) + + expect(JSON.parse(response.body).symbolize_keys[:exercise_found]).to be true + expect(JSON.parse(response.body).symbolize_keys[:update_right]).to be true + expect(JSON.parse(response.body).symbolize_keys[:message]).to(include('has been found').and(include('Overwrite'))) + end + + context 'when api_key is incorrect' do + let(:headers) { {'Authorization' => 'Bearer XXXXXX'} } + + it 'renders correct response' do + post_request + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when the user is cannot update the exercise' do + let(:codeharbor_link) { FactoryBot.create(:codeharbor_link, api_key: 'anotherkey') } + + it 'renders correct response' do + post_request + expect(response).to have_http_status(:success) + + expect(JSON.parse(response.body).symbolize_keys[:exercise_found]).to be true + expect(JSON.parse(response.body).symbolize_keys[:update_right]).to be false + expect(JSON.parse(response.body).symbolize_keys[:message]).to(include('has been found').and(not_include('Overwrite'))) + end + end + + context 'when the searched exercise does not exist' do + let(:uuid) { 'anotheruuid' } + + it 'renders correct response' do + post_request + expect(response).to have_http_status(:success) + + expect(JSON.parse(response.body).symbolize_keys[:exercise_found]).to be false + expect(JSON.parse(response.body).symbolize_keys[:message]).to(include('No corresponding exercise')) + end + end + end + + describe 'POST #import_exercise' do + let(:codeharbor_link) { FactoryBot.create(:codeharbor_link, user: user) } + let(:imported_exercise) { exercise } + let(:post_request) { post :import_exercise, body: zip_file_content } + let(:zip_file_content) { 'zipped task xml' } + let(:headers) { {'Authorization' => "Bearer #{codeharbor_link.api_key}"} } + + before do + request.headers.merge! headers + allow(ProformaService::Import).to receive(:call).and_return(imported_exercise) + end + + it 'responds with correct status code' do + post_request + expect(response).to have_http_status(:created) + end + + it 'calls service' do + post_request + expect(ProformaService::Import).to have_received(:call).with(zip: be_a(Tempfile).and(has_content(zip_file_content)), user: user) + end + + context 'when import fails with ProformaError' do + before { allow(ProformaService::Import).to receive(:call).and_raise(Proforma::PreImportValidationError) } + + it 'responds with correct status code' do + post_request + expect(response).to have_http_status(:bad_request) + end + end + + context 'when import fails due to another error' do + before { allow(ProformaService::Import).to receive(:call).and_raise(StandardError) } + + it 'responds with correct status code' do + post_request + expect(response).to have_http_status(:internal_server_error) + end + end + end + end diff --git a/spec/factories/codeharbor_link.rb b/spec/factories/codeharbor_link.rb new file mode 100644 index 00000000..c960eb9a --- /dev/null +++ b/spec/factories/codeharbor_link.rb @@ -0,0 +1,8 @@ +FactoryBot.define do + factory :codeharbor_link do + user { build(:teacher) } + push_url { 'http://push.url' } + check_uuid_url { 'http://check-uuid.url' } + sequence(:api_key) { |n| "api_key#{n}" } + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f13a3edb..570c271b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -20,6 +20,8 @@ unless RUBY_PLATFORM == 'java' SimpleCov.start('rails') end +require 'webmock/rspec' + RSpec.configure do |config| # These two settings work together to allow you to limit a spec run # to individual examples or groups you care about by tagging them with diff --git a/spec/support/expectations/has_content.rb b/spec/support/expectations/has_content.rb new file mode 100644 index 00000000..e0f5f60c --- /dev/null +++ b/spec/support/expectations/has_content.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'rspec/expectations' + +RSpec::Matchers.define :has_content do |actual_content| + match do |file| + file.read == actual_content + end +end From c89ee6c10247ee53c04eae848f7849240fd70852 Mon Sep 17 00:00:00 2001 From: Karol Date: Sat, 7 Dec 2019 13:11:48 +0100 Subject: [PATCH 33/49] model and policy specs --- Gemfile | 1 + Gemfile.lock | 3 ++ app/models/exercise.rb | 4 +- app/policies/codeharbor_link_policy.rb | 14 +++--- spec/models/codeharbor_link_spec.rb | 16 +++++++ spec/models/exercise_spec.rb | 28 ++++++++++-- spec/policies/codeharbor_link_policy_spec.rb | 45 ++++++++++++++++++++ spec/policies/exercise_policy_spec.rb | 4 +- spec/rails_helper.rb | 7 +++ 9 files changed, 109 insertions(+), 13 deletions(-) create mode 100644 spec/models/codeharbor_link_spec.rb create mode 100644 spec/policies/codeharbor_link_policy_spec.rb diff --git a/Gemfile b/Gemfile index f980a082..4bdc5e8d 100644 --- a/Gemfile +++ b/Gemfile @@ -73,6 +73,7 @@ group :test do gem 'nyan-cat-formatter' gem 'rspec-autotest' gem 'rspec-rails' + gem 'shoulda-matchers' gem 'simplecov', require: false gem 'webmock' end diff --git a/Gemfile.lock b/Gemfile.lock index fb93dbf4..e3afa6a6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -365,6 +365,8 @@ GEM selenium-webdriver (3.142.6) childprocess (>= 0.5, < 4.0) rubyzip (>= 1.2.2) + shoulda-matchers (4.1.1) + activesupport (>= 4.2.0) simplecov (0.17.1) docile (~> 1.1) json (>= 1.8, < 3) @@ -491,6 +493,7 @@ DEPENDENCIES rubyzip sass-rails selenium-webdriver + shoulda-matchers simplecov slim-rails sorcery diff --git a/app/models/exercise.rb b/app/models/exercise.rb index 71ffd97a..86b402d8 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -31,12 +31,12 @@ class Exercise < ApplicationRecord validate :valid_main_file? validates :description, presence: true - validates :execution_environment_id, presence: true, if: -> { !unpublished? } + validates :execution_environment, presence: true, if: -> { !unpublished? } validates :public, boolean_presence: true validates :unpublished, boolean_presence: true validates :title, presence: true validates :token, presence: true, uniqueness: true - validates_uniqueness_of :uuid + validates_uniqueness_of :uuid, if: -> { uuid.present? } @working_time_statistics = nil attr_reader :working_time_statistics diff --git a/app/policies/codeharbor_link_policy.rb b/app/policies/codeharbor_link_policy.rb index e83f9b75..21b1dab2 100644 --- a/app/policies/codeharbor_link_policy.rb +++ b/app/policies/codeharbor_link_policy.rb @@ -1,5 +1,3 @@ -# frozen_string_literal: true - class CodeharborLinkPolicy < ApplicationPolicy def index? false @@ -18,14 +16,20 @@ class CodeharborLinkPolicy < ApplicationPolicy end def edit? - teacher? + owner? end def update? - teacher? + owner? end def destroy? - teacher? + owner? + end + + private + + def owner? + @record.reload.user == @user end end diff --git a/spec/models/codeharbor_link_spec.rb b/spec/models/codeharbor_link_spec.rb new file mode 100644 index 00000000..df1ddba4 --- /dev/null +++ b/spec/models/codeharbor_link_spec.rb @@ -0,0 +1,16 @@ +require 'rails_helper' + +describe CodeharborLink do + it { is_expected.to validate_presence_of(:check_uuid_url) } + it { is_expected.to validate_presence_of(:push_url) } + it { is_expected.to validate_presence_of(:api_key) } + it { is_expected.to belong_to(:user) } + + describe '#to_s' do + subject { codeharbor_link.to_s } + + let(:codeharbor_link) { FactoryBot.create(:codeharbor_link) } + + it { is_expected.to eql codeharbor_link.id.to_s } + end +end diff --git a/spec/models/exercise_spec.rb b/spec/models/exercise_spec.rb index edfbd1cc..3576d3a5 100644 --- a/spec/models/exercise_spec.rb +++ b/spec/models/exercise_spec.rb @@ -22,10 +22,6 @@ describe Exercise do expect(exercise.errors[:description]).to be_present end - it 'validates the presence of an execution environment' do - expect(exercise.errors[:execution_environment_id]).to be_present - end - it 'validates the presence of the public flag' do expect(exercise.errors[:public]).to be_present exercise.update(public: false) @@ -45,6 +41,30 @@ describe Exercise do expect(exercise.errors[:user_type]).to be_present end + context 'when exercise is unpublished' do + subject { FactoryBot.build(:dummy, unpublished: true) } + + it { is_expected.not_to validate_presence_of(:execution_environment) } + end + + context 'when exercise is not unpublished' do + subject { FactoryBot.build(:dummy, unpublished: false) } + + it { is_expected.to validate_presence_of(:execution_environment) } + end + + context 'with uuid' do + subject { FactoryBot.build(:dummy, uuid: SecureRandom.uuid) } + + it { is_expected.to validate_uniqueness_of(:uuid).case_insensitive } + end + + context 'without uuid' do + subject { FactoryBot.build(:dummy, uuid: nil) } + + it { is_expected.not_to validate_uniqueness_of(:uuid) } + end + describe '#average_percentage' do let(:exercise) { FactoryBot.create(:fibonacci) } diff --git a/spec/policies/codeharbor_link_policy_spec.rb b/spec/policies/codeharbor_link_policy_spec.rb new file mode 100644 index 00000000..9569dd90 --- /dev/null +++ b/spec/policies/codeharbor_link_policy_spec.rb @@ -0,0 +1,45 @@ +require 'rails_helper' + +describe CodeharborLinkPolicy do + subject(:policy) { described_class } + + let(:codeharbor_link) { FactoryBot.create(:codeharbor_link) } + + %i[index? show?].each do |action| + permissions(action) do + it 'does not grant access any user' do + %i[external_user teacher admin].each do |factory_name| + expect(policy).not_to permit(FactoryBot.create(factory_name), codeharbor_link) + end + end + end + end + + %i[new? create?].each do |action| + permissions(action) do + it 'grants access to teachers' do + expect(policy).to permit(FactoryBot.create(:teacher), codeharbor_link) + end + + it 'does not grant access to all other users' do + %i[external_user admin].each do |factory_name| + expect(policy).not_to permit(FactoryBot.create(factory_name), codeharbor_link) + end + end + end + end + + %i[destroy? edit? update?].each do |action| + permissions(action) do + it 'grants access to the owner of the link' do + expect(policy).to permit(codeharbor_link.user, codeharbor_link) + end + + it 'does not grant access to arbitrary users' do + %i[external_user admin teacher].each do |factory_name| + expect(policy).not_to permit(FactoryBot.create(factory_name), codeharbor_link) + end + end + end + end +end diff --git a/spec/policies/exercise_policy_spec.rb b/spec/policies/exercise_policy_spec.rb index 4b4acd4e..4de6a9f4 100644 --- a/spec/policies/exercise_policy_spec.rb +++ b/spec/policies/exercise_policy_spec.rb @@ -4,7 +4,7 @@ describe ExercisePolicy do subject { described_class } let(:exercise) { FactoryBot.build(:dummy) } - + permissions :batch_update? do it 'grants access to admins only' do expect(subject).to permit(FactoryBot.build(:admin), exercise) @@ -30,7 +30,7 @@ let(:exercise) { FactoryBot.build(:dummy) } end end - [:clone?, :destroy?, :edit?, :update?].each do |action| + [:clone?, :destroy?, :edit?, :update?, :export_external_check?, :export_external_confirm?].each do |action| permissions(action) do it 'grants access to admins' do expect(subject).to permit(FactoryBot.build(:admin), exercise) diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index faf011d2..735761ff 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -39,3 +39,10 @@ RSpec.configure do |config| # https://relishapp.com/rspec/rspec-rails/docs config.infer_spec_type_from_file_location! end + +Shoulda::Matchers.configure do |config| + config.integrate do |with| + with.test_framework :rspec + with.library :rails + end +end From 46e7853465ea01d69e306db0261baf25076eadeb Mon Sep 17 00:00:00 2001 From: Karol Date: Mon, 9 Dec 2019 20:35:49 +0100 Subject: [PATCH 34/49] specs for services --- Gemfile | 1 + Gemfile.lock | 3 + .../exercise_service/check_external.rb | 4 +- .../exercise_service/push_external.rb | 4 +- .../convert_exercise_to_task.rb | 4 - .../convert_task_to_exercise.rb | 5 +- app/services/proforma_service/import.rb | 13 +- config/locales/en.yml | 2 +- spec/factories/code_ocean/file.rb | 18 + .../exercise_service/check_external_spec.rb | 76 ++++ .../exercise_service/push_external_spec.rb | 64 +++ .../convert_exercise_to_task_spec.rb | 198 +++++++++ .../convert_task_to_exercise_spec.rb | 377 ++++++++++++++++++ .../proforma_service/export_task_spec.rb | 41 ++ spec/services/proforma_service/import_spec.rb | 190 +++++++++ spec/support/expectations/equal_exercise.rb | 42 ++ 16 files changed, 1029 insertions(+), 13 deletions(-) create mode 100644 spec/services/exercise_service/check_external_spec.rb create mode 100644 spec/services/exercise_service/push_external_spec.rb create mode 100644 spec/services/proforma_service/convert_exercise_to_task_spec.rb create mode 100644 spec/services/proforma_service/convert_task_to_exercise_spec.rb create mode 100644 spec/services/proforma_service/export_task_spec.rb create mode 100644 spec/services/proforma_service/import_spec.rb create mode 100644 spec/support/expectations/equal_exercise.rb diff --git a/Gemfile b/Gemfile index 4bdc5e8d..6ca05b58 100644 --- a/Gemfile +++ b/Gemfile @@ -72,6 +72,7 @@ group :test do gem 'database_cleaner' gem 'nyan-cat-formatter' gem 'rspec-autotest' + gem 'rspec-collection_matchers' gem 'rspec-rails' gem 'shoulda-matchers' gem 'simplecov', require: false diff --git a/Gemfile.lock b/Gemfile.lock index e3afa6a6..8ba86613 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -318,6 +318,8 @@ GEM rspec-mocks (~> 3.9.0) rspec-autotest (1.0.2) rspec-core (>= 2.99.0.beta1, < 4.0.0) + rspec-collection_matchers (1.1.3) + rspec-expectations (>= 2.99.0.beta1) rspec-core (3.9.0) rspec-support (~> 3.9.0) rspec-expectations (3.9.0) @@ -486,6 +488,7 @@ DEPENDENCIES ransack rest-client rspec-autotest + rspec-collection_matchers rspec-rails rubocop rubocop-rspec diff --git a/app/services/exercise_service/check_external.rb b/app/services/exercise_service/check_external.rb index 1aac945b..a7add85d 100644 --- a/app/services/exercise_service/check_external.rb +++ b/app/services/exercise_service/check_external.rb @@ -16,8 +16,8 @@ module ExerciseService response_hash = JSON.parse(response.body, symbolize_names: true) {error: false}.merge(response_hash.slice(:message, :exercise_found, :update_right)) - rescue Faraday::Error => e - {error: true, message: I18n.t('exercises.export_codeharbor.error', message: e.message)} + rescue Faraday::Error, JSON::ParserError + {error: true, message: I18n.t('exercises.export_codeharbor.error')} end private diff --git a/app/services/exercise_service/push_external.rb b/app/services/exercise_service/push_external.rb index 93dacb81..3410a7ba 100644 --- a/app/services/exercise_service/push_external.rb +++ b/app/services/exercise_service/push_external.rb @@ -17,9 +17,9 @@ module ExerciseService request.body = body end - return response.success? ? nil : response.body + response.success? ? nil : response.body rescue StandardError => e - return e.message + e.message end end diff --git a/app/services/proforma_service/convert_exercise_to_task.rb b/app/services/proforma_service/convert_exercise_to_task.rb index e22aa709..7a44c5aa 100644 --- a/app/services/proforma_service/convert_exercise_to_task.rb +++ b/app/services/proforma_service/convert_exercise_to_task.rb @@ -22,11 +22,9 @@ module ProformaService title: @exercise.title, description: @exercise.description, internal_description: @exercise.instructions, - # proglang: proglang, files: task_files, tests: tests, uuid: uuid, - # parent_uuid: parent_uuid, language: DEFAULT_LANGUAGE, model_solutions: model_solutions, import_checksum: @exercise.import_checksum @@ -66,8 +64,6 @@ module ProformaService files: test_file(file), meta_data: { 'feedback-message' => file.feedback_message - # 'testing-framework' => test.testing_framework&.name, - # 'testing-framework-version' => test.testing_framework&.version }.compact ) end diff --git a/app/services/proforma_service/convert_task_to_exercise.rb b/app/services/proforma_service/convert_task_to_exercise.rb index 0264494f..f87a9354 100644 --- a/app/services/proforma_service/convert_task_to_exercise.rb +++ b/app/services/proforma_service/convert_task_to_exercise.rb @@ -22,7 +22,6 @@ module ProformaService description: @task.description, instructions: @task.internal_description, files: files, - uuid: @task.uuid, import_checksum: @task.checksum ) end @@ -56,10 +55,10 @@ module ProformaService name: File.basename(file.filename, '.*'), read_only: file.usage_by_lms != 'edit', role: file.internal_description, - path: File.dirname(file.filename) + path: File.dirname(file.filename).in?(['.', '']) ? nil : File.dirname(file.filename) }.tap do |params| if file.binary - params[:native_file] = FileIO.new(file.content.force_encoding('UTF-8'), File.basename(file.filename)) + params[:native_file] = FileIO.new(file.content.dup.force_encoding('UTF-8'), File.basename(file.filename)) else params[:content] = file.content end diff --git a/app/services/proforma_service/import.rb b/app/services/proforma_service/import.rb index b8ec628b..4fa0676e 100644 --- a/app/services/proforma_service/import.rb +++ b/app/services/proforma_service/import.rb @@ -12,7 +12,7 @@ module ProformaService importer = Proforma::Importer.new(@zip) @task = importer.perform - exercise = Exercise.find_by(uuid: @task.uuid) + exercise = base_exercise exercise_files = exercise&.files&.to_a exercise = ConvertTaskToExercise.call(task: @task, user: @user, exercise: exercise) @@ -26,6 +26,17 @@ module ProformaService private + def base_exercise + exercise = Exercise.find_by(uuid: @task.uuid) + if exercise + return exercise if ExercisePolicy.new(@user, exercise).update? + + return Exercise.new(uuid: SecureRandom.uuid, unpublished: true) + end + + Exercise.new(uuid: @task.uuid || SecureRandom.uuid, unpublished: true) + end + def import_multi Zip::File.open(@zip.path) do |zip_file| zip_files = zip_file.filter { |entry| entry.name.match?(/\.zip$/) } diff --git a/config/locales/en.yml b/config/locales/en.yml index 8a0707e1..6211c3e1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -329,7 +329,7 @@ en: dialogtitle: Export to Codeharbor successfully_exported: 'Exercise has been successfully exported.
ID: %{id}
Title: %{title}' export_failed: 'Export has failed.
ID: %{id}
Title: %{title}

Error: %{error}' - error: 'An error occurred while contacting Codeharbor
Error: %{message}' + error: 'An error occurred while contacting Codeharbor' checking_codeharbor: Checking if the exercise exists on Codeharbor. buttons: retry: Retry diff --git a/spec/factories/code_ocean/file.rb b/spec/factories/code_ocean/file.rb index 186ad1d1..64245e97 100644 --- a/spec/factories/code_ocean/file.rb +++ b/spec/factories/code_ocean/file.rb @@ -10,6 +10,24 @@ module CodeOcean name { SecureRandom.hex } read_only { false } role { 'main_file' } + + trait(:image) do + association :file_type, factory: :dot_png + name { 'poster' } + native_file { Rack::Test::UploadedFile.new(Rails.root.join('db', 'seeds', 'audio_video', 'poster.png'), 'image/png') } + end + end + + factory :test_file, class: CodeOcean::File do + content { '' } + association :context, factory: :dummy + association :file_type, factory: :dot_rb + hidden { true } + name { SecureRandom.hex } + read_only { true } + role { 'teacher_defined_test' } + feedback_message { 'feedback_message' } + weight { 1 } end end end diff --git a/spec/services/exercise_service/check_external_spec.rb b/spec/services/exercise_service/check_external_spec.rb new file mode 100644 index 00000000..ea7874c6 --- /dev/null +++ b/spec/services/exercise_service/check_external_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe ExerciseService::CheckExternal do + describe '.new' do + subject(:export_service) { described_class.new(uuid: uuid, codeharbor_link: codeharbor_link) } + + let(:uuid) { SecureRandom.uuid } + let(:codeharbor_link) { FactoryBot.build(:codeharbor_link) } + + it 'assigns uuid' do + expect(export_service.instance_variable_get(:@uuid)).to be uuid + end + + it 'assigns codeharbor_link' do + expect(export_service.instance_variable_get(:@codeharbor_link)).to be codeharbor_link + end + end + + describe '#execute' do + subject(:check_external_service) { described_class.call(uuid: uuid, codeharbor_link: codeharbor_link) } + + let(:uuid) { SecureRandom.uuid } + let(:codeharbor_link) { FactoryBot.build(:codeharbor_link) } + let(:response) { {}.to_json } + + before { stub_request(:post, codeharbor_link.check_uuid_url).to_return(body: response) } + + it 'calls the correct url' do + expect(check_external_service).to have_requested(:post, codeharbor_link.check_uuid_url) + end + + it 'submits the correct headers' do + expect(check_external_service).to have_requested(:post, codeharbor_link.check_uuid_url) + .with(headers: {content_type: 'application/json', authorization: "Bearer #{codeharbor_link.api_key}"}) + end + + it 'submits the correct body' do + expect(check_external_service).to have_requested(:post, codeharbor_link.check_uuid_url) + .with(body: {uuid: uuid}.to_json) + end + + context 'when response contains a JSON with expected keys' do + let(:response) { {message: 'message', exercise_found: true, update_right: true}.to_json } + + it 'returns the correct hash' do + expect(check_external_service).to eql(error: false, message: 'message', exercise_found: true, update_right: true) + end + + context 'with different values' do + let(:response) { {message: 'message', exercise_found: false, update_right: false}.to_json } + + it 'returns the correct hash' do + expect(check_external_service).to eql(error: false, message: 'message', exercise_found: false, update_right: false) + end + end + end + + context 'when response does not contain JSON' do + let(:response) { 'foo' } + + it 'returns the correct hash' do + expect(check_external_service).to eql(error: true, message: I18n.t('exercises.export_codeharbor.error')) + end + end + + context 'when the request fails' do + before { allow(Faraday).to receive(:new).and_raise(Faraday::Error, 'error') } + + it 'returns the correct hash' do + expect(check_external_service).to eql(error: true, message: I18n.t('exercises.export_codeharbor.error')) + end + end + end +end diff --git a/spec/services/exercise_service/push_external_spec.rb b/spec/services/exercise_service/push_external_spec.rb new file mode 100644 index 00000000..20c07a0a --- /dev/null +++ b/spec/services/exercise_service/push_external_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ExerciseService::PushExternal do + describe '.new' do + subject(:push_external) { described_class.new(zip: zip, codeharbor_link: codeharbor_link) } + + let(:zip) { ProformaService::ExportTask.call(exercise: FactoryBot.build(:dummy)) } + let(:codeharbor_link) { FactoryBot.build(:codeharbor_link) } + + it 'assigns zip' do + expect(push_external.instance_variable_get(:@zip)).to be zip + end + + it 'assigns codeharbor_link' do + expect(push_external.instance_variable_get(:@codeharbor_link)).to be codeharbor_link + end + end + + describe '#execute' do + subject(:push_external) { described_class.call(zip: zip, codeharbor_link: codeharbor_link) } + + let(:zip) { ProformaService::ExportTask.call(exercise: FactoryBot.build(:dummy)) } + let(:codeharbor_link) { FactoryBot.build(:codeharbor_link) } + let(:status) { 200 } + let(:response) { '' } + + before { stub_request(:post, codeharbor_link.push_url).to_return(status: status, body: response) } + + it 'calls the correct url' do + expect(push_external).to have_requested(:post, codeharbor_link.push_url) + end + + it 'submits the correct headers' do + expect(push_external).to have_requested(:post, codeharbor_link.push_url) + .with(headers: {content_type: 'application/zip', + authorization: "Bearer #{codeharbor_link.api_key}", + content_length: zip.string.length}) + end + + it 'submits the correct body' do + expect(push_external).to have_requested(:post, codeharbor_link.push_url) + .with(body: zip.string) + end + + context 'when response status is success' do + it { is_expected.to be nil } + + context 'when response status is 500' do + let(:status) { 500 } + let(:response) { 'an error occured' } + + it { is_expected.to be response } + end + end + + context 'when an error occurs' do + before { allow(Faraday).to receive(:new).and_raise(StandardError) } + + it { is_expected.not_to be nil } + end + end +end diff --git a/spec/services/proforma_service/convert_exercise_to_task_spec.rb b/spec/services/proforma_service/convert_exercise_to_task_spec.rb new file mode 100644 index 00000000..ae2976d1 --- /dev/null +++ b/spec/services/proforma_service/convert_exercise_to_task_spec.rb @@ -0,0 +1,198 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ProformaService::ConvertExerciseToTask do + describe '.new' do + subject(:convert_to_task) { described_class.new(exercise: exercise) } + + let(:exercise) { FactoryBot.build(:dummy) } + + it 'assigns exercise' do + expect(convert_to_task.instance_variable_get(:@exercise)).to be exercise + end + end + + describe '#execute' do + subject(:task) { convert_to_task.execute } + + let(:convert_to_task) { described_class.new(exercise: exercise) } + let(:exercise) do + FactoryBot.create(:dummy, + instructions: 'instruction', + uuid: SecureRandom.uuid, + files: files + tests) + end + let(:files) { [] } + let(:tests) { [] } + + it 'creates a task with all basic attributes' do + expect(task).to have_attributes( + title: exercise.title, + description: exercise.description, + internal_description: exercise.instructions, + # proglang: { + # name: exercise.execution_environment.language, + # version: exercise.execution_environment.version + # }, + uuid: exercise.uuid, + language: described_class::DEFAULT_LANGUAGE, + # parent_uuid: exercise.clone_relations.first&.origin&.uuid, + files: [], + tests: [], + model_solutions: [], + import_checksum: exercise.import_checksum + ) + end + + context 'when exercise has a mainfile' do + let(:files) { [file] } + let(:file) { FactoryBot.build(:file) } + + it 'creates a task-file with the correct attributes' do + expect(task.files.first).to have_attributes( + id: file.id, + content: file.content, + filename: file.name_with_extension, + used_by_grader: true, + usage_by_lms: 'edit', + visible: 'yes', + binary: false, + internal_description: 'main_file' + ) + end + end + + context 'when exercise has a regular file' do + let(:files) { [file] } + let(:file) { FactoryBot.build(:file, role: 'regular_file', hidden: hidden, read_only: read_only) } + let(:hidden) { true } + let(:read_only) { true } + + it 'creates a task-file with the correct attributes' do + expect(task.files.first).to have_attributes( + id: file.id, + content: file.content, + filename: file.name_with_extension, + used_by_grader: true, + usage_by_lms: 'display', + visible: 'no', + binary: false, + internal_description: 'regular_file' + ) + end + + context 'when file is not hidden' do + let(:hidden) { false } + + it 'creates a task-file with the correct attributes' do + expect(task.files.first).to have_attributes(visible: 'yes') + end + end + + context 'when file is not read_only' do + let(:read_only) { false } + + it 'creates a task-file with the correct attributes' do + expect(task.files.first).to have_attributes(usage_by_lms: 'edit') + end + end + + context 'when file has an attachment' do + let(:file) { FactoryBot.build(:file, :image, role: 'regular_file') } + + it 'creates a task-file with the correct attributes' do + expect(task.files.first).to have_attributes( + used_by_grader: false, + binary: true, + mimetype: 'image/png' + ) + end + end + end + + context 'when exercise has a file with role reference implementation' do + let(:files) { [file] } + let(:file) { FactoryBot.build(:file, role: 'reference_implementation') } + + it 'creates a task with one model-solution' do + expect(task.model_solutions).to have(1).item + end + + it 'creates a model-solution with one file' do + expect(task.model_solutions.first).to have_attributes( + id: "ms-#{file.id}", + files: have(1).item + ) + end + + it 'creates a model-solution with one file with correct attributes' do + expect(task.model_solutions.first.files.first).to have_attributes( + id: file.id, + content: file.content, + filename: file.name_with_extension, + used_by_grader: false, + usage_by_lms: 'display', + visible: 'delayed', + binary: false, + internal_description: 'reference_implementation' + ) + end + end + + context 'when exercise has multiple files with role reference implementation' do + let(:files) { FactoryBot.build_list(:file, 2, role: 'reference_implementation') } + + it 'creates a task with two model-solutions' do + expect(task.model_solutions).to have(2).items + end + end + + context 'when exercise has a test' do + let(:tests) { [test_file] } + let(:test_file) { FactoryBot.build(:test_file) } + # let(:file) { FactoryBot.build(:codeharbor_test_file) } + + it 'creates a task with one test' do + expect(task.tests).to have(1).item + end + + it 'creates a test with one file' do + expect(task.tests.first).to have_attributes( + id: test_file.id, + title: test_file.name, + files: have(1).item, + meta_data: {'feedback-message' => test_file.feedback_message} + ) + end + + it 'creates a test with one file with correct attributes' do + expect(task.tests.first.files.first).to have_attributes( + id: test_file.id, + content: test_file.content, + filename: test_file.name_with_extension, + used_by_grader: true, + visible: 'no', + binary: false, + internal_description: 'teacher_defined_test' + ) + end + + context 'when exercise_file is not hidden' do + let(:test_file) { FactoryBot.create(:test_file, hidden: false) } + + it 'creates the test file with the correct attribute' do + expect(task.tests.first.files.first).to have_attributes(visible: 'yes') + end + end + end + + context 'when exercise has multiple tests' do + let(:tests) { FactoryBot.build_list(:test_file, 2) } + + it 'creates a task with two tests' do + expect(task.tests).to have(2).items + end + end + end +end diff --git a/spec/services/proforma_service/convert_task_to_exercise_spec.rb b/spec/services/proforma_service/convert_task_to_exercise_spec.rb new file mode 100644 index 00000000..4f0a3b8c --- /dev/null +++ b/spec/services/proforma_service/convert_task_to_exercise_spec.rb @@ -0,0 +1,377 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe ProformaService::ConvertTaskToExercise do + describe '.new' do + subject(:convert_to_exercise_service) { described_class.new(task: task, user: user, exercise: exercise) } + + let(:task) { Proforma::Task.new } + let(:user) { FactoryBot.build(:teacher) } + let(:exercise) { FactoryBot.build(:dummy) } + + it 'assigns task' do + expect(convert_to_exercise_service.instance_variable_get(:@task)).to be task + end + + it 'assigns user' do + expect(convert_to_exercise_service.instance_variable_get(:@user)).to be user + end + + it 'assigns exercise' do + expect(convert_to_exercise_service.instance_variable_get(:@exercise)).to be exercise + end + end + + describe '#execute' do + subject(:convert_to_exercise_service) { described_class.call(task: task, user: user, exercise: exercise) } + + before { FactoryBot.create(:dot_txt) } + + let(:task) do + Proforma::Task.new( + title: 'title', + description: 'description', + internal_description: 'internal_description', + proglang: {name: 'proglang-name', version: 'proglang-version'}, + uuid: 'uuid', + parent_uuid: 'parent_uuid', + language: 'language', + model_solutions: model_solutions, + files: files, + tests: tests, + import_checksum: 'import_checksum', + checksum: 'checksum' + ) + end + let(:user) { FactoryBot.create(:teacher) } + let(:files) { [] } + let(:tests) { [] } + let(:model_solutions) { [] } + let(:exercise) {} + + it 'creates an exercise with the correct attributes' do + expect(convert_to_exercise_service).to have_attributes( + title: 'title', + description: 'description', + instructions: 'internal_description', + execution_environment: be_blank, + uuid: be_blank, + unpublished: true, + user: user, + files: be_empty + ) + end + + context 'when task has a file' do + let(:files) { [file] } + let(:file) do + Proforma::TaskFile.new( + id: 'id', + content: content, + filename: 'filename.txt', + used_by_grader: 'used_by_grader', + visible: 'yes', + usage_by_lms: usage_by_lms, + binary: binary, + internal_description: 'regular_file', + mimetype: mimetype + ) + end + let(:usage_by_lms) { 'display' } + let(:mimetype) { 'mimetype' } + let(:binary) { false } + let(:content) { 'content' } + + it 'creates an exercise with a file that has the correct attributes' do + expect(convert_to_exercise_service.files.first).to have_attributes( + content: 'content', + name: 'filename', + role: 'regular_file', + hidden: false, + read_only: true, + file_type: be_a(FileType).and(have_attributes(file_extension: '.txt')) + ) + end + + it 'creates a new Exercise on save' do + expect { convert_to_exercise_service.save! }.to change(Exercise, :count).by(1) + end + + context 'when file is very large' do + let(:content) { 'test' * 10**5 } + + it 'creates an exercise with a file that has the correct attributes' do + expect(convert_to_exercise_service.files.first).to have_attributes(content: content) + end + end + + context 'when file is binary' do + let(:mimetype) { 'image/png' } + let(:binary) { true } + + it 'creates an exercise with a file with attachment and the correct attributes' do + expect(convert_to_exercise_service.files.first.native_file).to be_present + end + end + + context 'when usage_by_lms is edit' do + let(:usage_by_lms) { 'edit' } + + it 'creates an exercise with a file with correct attributes' do + expect(convert_to_exercise_service.files.first).to have_attributes(read_only: false) + end + end + + context 'when file is a model-solution-placeholder (needed by proforma until issue #5 is resolved)' do + let(:file) { Proforma::TaskFile.new(id: 'ms-placeholder-file') } + + it 'leaves exercise_files empty' do + expect(convert_to_exercise_service.files).to be_empty + end + end + end + + context 'when task has a model-solution' do + let(:model_solutions) { [model_solution] } + let(:model_solution) do + Proforma::ModelSolution.new( + id: 'ms-id', + files: ms_files + ) + end + let(:ms_files) { [ms_file] } + let(:ms_file) do + Proforma::TaskFile.new( + id: 'ms-file', + content: 'content', + filename: 'filename.txt', + used_by_grader: 'used_by_grader', + visible: 'yes', + usage_by_lms: 'display', + binary: false, + internal_description: 'reference_implementation' + ) + end + + it 'creates an exercise with a file with role Reference Implementation' do + expect(convert_to_exercise_service.files.first).to have_attributes( + role: 'reference_implementation' + ) + end + + context 'when task has two model-solutions' do + let(:model_solutions) { [model_solution, model_solution2] } + let(:model_solution2) do + Proforma::ModelSolution.new( + id: 'ms-id-2', + files: ms_files_2 + ) + end + let(:ms_files_2) { [ms_file_2] } + let(:ms_file_2) do + Proforma::TaskFile.new( + id: 'ms-file-2', + content: 'content', + filename: 'filename.txt', + used_by_grader: 'used_by_grader', + visible: 'yes', + usage_by_lms: 'display', + binary: false, + internal_description: 'reference_implementation' + ) + end + + it 'creates an exercise with two files with role Reference Implementation' do + expect(convert_to_exercise_service.files).to have(2).items.and(all(have_attributes(role: 'reference_implementation'))) + end + end + end + + context 'when task has a test' do + let(:tests) { [test] } + let(:test) do + Proforma::Test.new( + id: 'test-id', + title: 'title', + description: 'description', + internal_description: 'internal_description', + test_type: 'test_type', + files: test_files, + meta_data: { + 'feedback-message' => 'feedback-message', + 'testing-framework' => 'testing-framework', + 'testing-framework-version' => 'testing-framework-version' + } + ) + end + + let(:test_files) { [test_file] } + let(:test_file) do + Proforma::TaskFile.new( + id: 'test_file_id', + content: 'testfile-content', + filename: 'testfile.txt', + used_by_grader: 'yes', + visible: 'no', + usage_by_lms: 'display', + binary: false, + internal_description: 'teacher_defined_test' + ) + end + + it 'creates an exercise with a test' do + expect(convert_to_exercise_service.files.select { |file| file.role == 'teacher_defined_test' }).to have(1).item + end + + it 'creates an exercise with a test with correct attributes' do + expect(convert_to_exercise_service.files.select { |file| file.role == 'teacher_defined_test' }.first).to have_attributes( + feedback_message: 'feedback-message', + content: 'testfile-content', + name: 'testfile', + role: 'teacher_defined_test', + hidden: true, + read_only: true, + file_type: be_a(FileType).and(have_attributes(file_extension: '.txt')) + ) + end + + context 'when task has multiple tests' do + let(:tests) { [test, test2] } + let(:test2) do + Proforma::Test.new( + files: test_files2, + meta_data: { + 'feedback-message' => 'feedback-message', + 'testing-framework' => 'testing-framework', + 'testing-framework-version' => 'testing-framework-version' + } + ) + end + let(:test_files2) { [test_file2] } + let(:test_file2) do + Proforma::TaskFile.new( + id: 'test_file_id2', + content: 'testfile-content', + filename: 'testfile.txt', + used_by_grader: 'yes', + visible: 'no', + usage_by_lms: 'display', + binary: false, + internal_description: 'teacher_defined_test' + ) + end + + it 'creates an exercise with two test' do + expect(convert_to_exercise_service.files.select { |file| file.role == 'teacher_defined_test' }).to have(2).items + end + end + end + + context 'when exercise is set' do + let(:exercise) do + FactoryBot.create( + :files, + title: 'exercise-title', + description: 'exercise-description', + instructions: 'exercise-instruction' + ) + end + + before { exercise.reload } + + it 'assigns all values to given exercise' do + convert_to_exercise_service.save + expect(exercise.reload).to have_attributes( + id: exercise.id, + title: task.title, + description: task.description, + instructions: task.internal_description, + execution_environment: exercise.execution_environment, + uuid: exercise.uuid, + user: exercise.user, + files: be_empty + ) + end + + it 'does not create a new Exercise on save' do + expect { convert_to_exercise_service.save }.not_to change(Exercise, :count) + end + + context 'with file, model solution and test' do + let(:files) { [file] } + let(:file) do + Proforma::TaskFile.new( + id: 'id', + content: 'content', + filename: 'filename.txt', + used_by_grader: 'used_by_grader', + visible: 'yes', + usage_by_lms: 'display', + binary: false, + internal_description: 'regular_file' + ) + end + let(:tests) { [test] } + let(:test) do + Proforma::Test.new( + id: 'test-id', + title: 'title', + description: 'description', + internal_description: 'regular_file', + test_type: 'test_type', + files: test_files, + meta_data: { + 'feedback-message' => 'feedback-message', + 'testing-framework' => 'testing-framework', + 'testing-framework-version' => 'testing-framework-version' + } + ) + end + let(:test_files) { [test_file] } + let(:test_file) do + Proforma::TaskFile.new( + id: 'test_file_id', + content: 'testfile-content', + filename: 'testfile.txt', + used_by_grader: 'yes', + visible: 'no', + usage_by_lms: 'display', + binary: false, + internal_description: 'teacher_defined_test' + ) + end + let(:model_solutions) { [model_solution] } + let(:model_solution) do + Proforma::ModelSolution.new( + id: 'ms-id', + files: ms_files + ) + end + let(:ms_files) { [ms_file] } + let(:ms_file) do + Proforma::TaskFile.new( + id: 'ms-file', + content: 'ms-content', + filename: 'filename.txt', + used_by_grader: 'used_by_grader', + visible: 'yes', + usage_by_lms: 'display', + binary: false, + internal_description: 'reference_implementation' + ) + end + + it 'assigns all values to given exercise' do + expect(convert_to_exercise_service).to have_attributes( + id: exercise.id, + files: have(3).items + .and(include(have_attributes(content: 'ms-content', role: 'reference_implementation'))) + .and(include(have_attributes(content: 'content', role: 'regular_file'))) + .and(include(have_attributes(content: 'testfile-content', role: 'teacher_defined_test'))) + ) + end + end + end + end +end diff --git a/spec/services/proforma_service/export_task_spec.rb b/spec/services/proforma_service/export_task_spec.rb new file mode 100644 index 00000000..157094de --- /dev/null +++ b/spec/services/proforma_service/export_task_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe ProformaService::ExportTask do + describe '.new' do + subject(:export_task) { described_class.new(exercise: exercise) } + + let(:exercise) { FactoryBot.build(:dummy) } + + it 'assigns exercise' do + expect(export_task.instance_variable_get(:@exercise)).to be exercise + end + + context 'without exercise' do + subject(:export_task) { described_class.new } + + it 'assigns exercise' do + expect(export_task.instance_variable_get(:@exercise)).to be nil + end + end + end + + describe '#execute' do + subject(:export_task) { described_class.call(exercise: exercise) } + + let(:task) { Proforma::Task.new } + let(:exercise) { FactoryBot.build(:dummy) } + let(:exporter) { instance_double('Proforma::Exporter', perform: 'zip') } + + before do + allow(ProformaService::ConvertExerciseToTask).to receive(:call).with(exercise: exercise).and_return(task) + allow(Proforma::Exporter).to receive(:new).with(task).and_return(exporter) + end + + it do + export_task + expect(exporter).to have_received(:perform) + end + end +end diff --git a/spec/services/proforma_service/import_spec.rb b/spec/services/proforma_service/import_spec.rb new file mode 100644 index 00000000..118284f1 --- /dev/null +++ b/spec/services/proforma_service/import_spec.rb @@ -0,0 +1,190 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe ProformaService::Import do + describe '.new' do + subject(:import_service) { described_class.new(zip: zip, user: user) } + + let(:zip) { Tempfile.new('proforma_test_zip_file') } + let(:user) { FactoryBot.build(:teacher) } + + it 'assigns zip' do + expect(import_service.instance_variable_get(:@zip)).to be zip + end + + it 'assigns user' do + expect(import_service.instance_variable_get(:@user)).to be user + end + end + + describe '#execute' do + subject(:import_service) { described_class.call(zip: zip_file, user: import_user) } + + let(:user) { FactoryBot.create(:teacher) } + let(:import_user) { user } + let(:zip_file) { Tempfile.new('proforma_test_zip_file') } + let(:exercise) do + FactoryBot.create(:dummy, + instructions: 'instruction', + execution_environment: execution_environment, + files: files + tests, + uuid: uuid, + user: user) + end + + let(:uuid) {} + let(:execution_environment) { FactoryBot.build(:java) } + let(:files) { [] } + let(:tests) { [] } + let(:exporter) { ProformaService::ExportTask.call(exercise: exercise.reload).string } + + before do + zip_file.write(exporter) + zip_file.rewind + end + + it { is_expected.to be_an_equal_exercise_as exercise } + + it 'sets the correct user as owner of the exercise' do + expect(import_service.user).to be user + end + + it 'sets the uuid' do + expect(import_service.uuid).not_to be_blank + end + + context 'when no exercise exists' do + before { exercise.destroy } + + it { is_expected.to be_valid } + + it 'sets the correct user as owner of the exercise' do + expect(import_service.user).to be user + end + + it 'sets the uuid' do + expect(import_service.uuid).not_to be_blank + end + + context 'when task has a uuid' do + let(:uuid) { SecureRandom.uuid } + + it 'sets the uuid' do + expect(import_service.uuid).to eql uuid + end + end + end + + context 'when exercise has a mainfile' do + let(:files) { [file] } + let(:file) { FactoryBot.build(:file) } + + it { is_expected.to be_an_equal_exercise_as exercise } + + context 'when the mainfile is very large' do + let(:file) { FactoryBot.build(:file, content: 'test' * 10**5) } + + it { is_expected.to be_an_equal_exercise_as exercise } + end + end + + context 'when exercise has a regular file' do + let(:files) { [file] } + let(:file) { FactoryBot.build(:file, role: 'regular_file') } + + it { is_expected.to be_an_equal_exercise_as exercise } + + context 'when file has an attachment' do + let(:file) { FactoryBot.build(:file, :image, role: 'regular_file') } + + it { is_expected.to be_an_equal_exercise_as exercise } + end + end + + context 'when exercise has a file with role reference implementation' do + let(:files) { [file] } + let(:file) { FactoryBot.build(:file, role: 'reference_implementation', read_only: true) } + + it { is_expected.to be_an_equal_exercise_as exercise } + end + + context 'when exercise has multiple files with role reference implementation' do + let(:files) { FactoryBot.build_list(:file, 2, role: 'reference_implementation', read_only: true) } + + it { is_expected.to be_an_equal_exercise_as exercise } + end + + context 'when exercise has a test' do + let(:tests) { [test] } + let(:test) { FactoryBot.build(:test_file) } + + it { is_expected.to be_an_equal_exercise_as exercise } + end + + context 'when exercise has multiple tests' do + let(:tests) { FactoryBot.build_list(:test_file, 2) } + + it { is_expected.to be_an_equal_exercise_as exercise } + end + + # context 'when zip contains multiple tasks' do + # let(:exporter) { ProformaService::ExportTasks.call(exercises: [exercise, exercise2]).string } + + # let(:exercise2) do + # FactoryBot.create(:dummy, + # instruction: 'instruction2', + # execution_environment: execution_environment, + # exercise_files: [], + # tests: [], + # user: user) + # end + + # it 'imports the exercises from zip containing multiple zips' do + # expect(import_service).to all be_an(Exercise) + # end + + # it 'imports the zip exactly how they were exported' do + # expect(import_service).to all be_an_equal_exercise_as(exercise).or be_an_equal_exercise_as(exercise2) + # end + + # context 'when a exercise has files and tests' do + # let(:files) { [FactoryBot.build(:file), FactoryBot.build(:file, role: 'regular_file')] } + # let(:tests) { FactoryBot.build_list(:codeharbor_test, 2) } + + # it 'imports the zip exactly how the were exported' do + # expect(import_service).to all be_an_equal_exercise_as(exercise).or be_an_equal_exercise_as(exercise2) + # end + # end + # end + + context 'when task in zip has a different uuid' do + let(:uuid) { SecureRandom.uuid } + let(:new_uuid) { SecureRandom.uuid } + + before do + exercise.update(uuid: new_uuid) + end + + it 'creates a new Exercise' do + expect(import_service.id).not_to be exercise.id + end + end + + context 'when task in zip has the same uuid and nothing has changed' do + let(:uuid) { SecureRandom.uuid } + + it 'updates the old Exercise' do + expect(import_service.id).to be exercise.id + end + + context 'when another user imports the exercise' do + let(:import_user) { FactoryBot.create(:teacher) } + + it 'creates a new Exercise' do + expect(import_service.id).not_to be exercise.id + end + end + end + end +end diff --git a/spec/support/expectations/equal_exercise.rb b/spec/support/expectations/equal_exercise.rb new file mode 100644 index 00000000..df21ec3d --- /dev/null +++ b/spec/support/expectations/equal_exercise.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'rspec/expectations' + +RSpec::Matchers.define :be_an_equal_exercise_as do |exercise| + match do |actual| + equal?(actual, exercise) + end + failure_message do |actual| + "#{actual.inspect} is not equal to \n#{exercise.inspect}. \nLast checked attribute: #{@last_checked}" + end + + def equal?(object, other) + return false unless object.class == other.class + return attributes_equal?(object, other) if object.is_a?(ApplicationRecord) + return array_equal?(object, other) if object.is_a?(Array) || object.is_a?(ActiveRecord::Associations::CollectionProxy) + + object == other + end + + def attributes_equal?(object, other) + other_attributes = attributes_and_associations(other) + attributes_and_associations(object).each do |k, v| + @last_checked = "#{k}: \n\"#{v}\" vs \n\"#{other_attributes[k]}\"" + return false unless equal?(other_attributes[k], v) + end + true + end + + def array_equal?(object, other) + return true if object == other # for [] + return false if object.length != other.length + + object.to_a.product(other.to_a).map { |k, v| equal?(k, v) }.any? + end + + def attributes_and_associations(object) + object.attributes.dup.tap do |attributes| + attributes[:files] = object.files if defined? object.files + end.except('id', 'created_at', 'updated_at', 'exercise_id', 'uuid', 'import_checksum') + end +end From 4fd440b1f6994c7bf711811dd6197688ddb3d5d8 Mon Sep 17 00:00:00 2001 From: Karol Date: Mon, 9 Dec 2019 20:50:42 +0100 Subject: [PATCH 35/49] reenable webrequests in specs --- spec/rails_helper.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 735761ff..8de4fc82 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -46,3 +46,5 @@ Shoulda::Matchers.configure do |config| with.library :rails end end + +WebMock.allow_net_connect! From 7abc952e754c5a443b851839875767e4e24c4077 Mon Sep 17 00:00:00 2001 From: Karol Date: Mon, 9 Dec 2019 21:03:44 +0100 Subject: [PATCH 36/49] update config for travis --- config/code_ocean.yml.travis | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config/code_ocean.yml.travis b/config/code_ocean.yml.travis index 28485afc..c4a2b410 100644 --- a/config/code_ocean.yml.travis +++ b/config/code_ocean.yml.travis @@ -2,4 +2,6 @@ test: flowr: enabled: false code_pilot: - enabled: false \ No newline at end of file + enabled: false + codeharbor: + url: http://test.url From 06053d437df486fa1637dd7e0397f024473a7034 Mon Sep 17 00:00:00 2001 From: Karol Date: Tue, 10 Dec 2019 16:37:36 +0100 Subject: [PATCH 37/49] add specs for nil paths --- .../convert_task_to_exercise_spec.rb | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/spec/services/proforma_service/convert_task_to_exercise_spec.rb b/spec/services/proforma_service/convert_task_to_exercise_spec.rb index 4f0a3b8c..1d66d32f 100644 --- a/spec/services/proforma_service/convert_task_to_exercise_spec.rb +++ b/spec/services/proforma_service/convert_task_to_exercise_spec.rb @@ -69,7 +69,7 @@ describe ProformaService::ConvertTaskToExercise do Proforma::TaskFile.new( id: 'id', content: content, - filename: 'filename.txt', + filename: "#{path}filename.txt", used_by_grader: 'used_by_grader', visible: 'yes', usage_by_lms: usage_by_lms, @@ -82,6 +82,7 @@ describe ProformaService::ConvertTaskToExercise do let(:mimetype) { 'mimetype' } let(:binary) { false } let(:content) { 'content' } + let(:path) {} it 'creates an exercise with a file that has the correct attributes' do expect(convert_to_exercise_service.files.first).to have_attributes( @@ -90,7 +91,8 @@ describe ProformaService::ConvertTaskToExercise do role: 'regular_file', hidden: false, read_only: true, - file_type: be_a(FileType).and(have_attributes(file_extension: '.txt')) + file_type: be_a(FileType).and(have_attributes(file_extension: '.txt')), + path: nil ) end @@ -98,6 +100,22 @@ describe ProformaService::ConvertTaskToExercise do expect { convert_to_exercise_service.save! }.to change(Exercise, :count).by(1) end + context 'when path is folder/' do + let(:path) { 'folder/' } + + it 'creates an exercise with a file that has the correct path' do + expect(convert_to_exercise_service.files.first).to have_attributes(path: 'folder') + end + end + + context 'when path is ./' do + let(:path) { './' } + + it 'creates an exercise with a file that has the correct path' do + expect(convert_to_exercise_service.files.first).to have_attributes(path: nil) + end + end + context 'when file is very large' do let(:content) { 'test' * 10**5 } From 17aa44a4447db5fbb7cd464df28f118503cc4790 Mon Sep 17 00:00:00 2001 From: Karol Date: Tue, 10 Dec 2019 17:23:25 +0100 Subject: [PATCH 38/49] fix cognitive complexity --- .../convert_exercise_to_task.rb | 42 ++++++++++--------- .../convert_task_to_exercise.rb | 16 +++---- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/app/services/proforma_service/convert_exercise_to_task.rb b/app/services/proforma_service/convert_exercise_to_task.rb index 7a44c5aa..ce861120 100644 --- a/app/services/proforma_service/convert_exercise_to_task.rb +++ b/app/services/proforma_service/convert_exercise_to_task.rb @@ -86,27 +86,29 @@ module ProformaService end def task_file(file) - Proforma::TaskFile.new( - { - id: file.id, - filename: file.path.present? && file.path != '.' ? ::File.join(file.path, file.name_with_extension) : file.name_with_extension, - usage_by_lms: file.read_only ? 'display' : 'edit', - visible: file.hidden ? 'no' : 'yes', - internal_description: file.role || 'regular_file' - }.tap do |params| - if file.native_file.present? - file = ::File.new(file.native_file.file.path, 'r') - params[:content] = file.read - params[:used_by_grader] = false - params[:binary] = true - params[:mimetype] = MimeMagic.by_magic(file).type - else - params[:content] = file.content - params[:used_by_grader] = true - params[:binary] = false - end - end + task_file = Proforma::TaskFile.new( + id: file.id, + filename: file.path.present? && file.path != '.' ? ::File.join(file.path, file.name_with_extension) : file.name_with_extension, + usage_by_lms: file.read_only ? 'display' : 'edit', + visible: file.hidden ? 'no' : 'yes', + internal_description: file.role || 'regular_file' ) + add_content_to_task_file(file, task_file) + task_file + end + + def add_content_to_task_file(file, task_file) + if file.native_file.present? + file = ::File.new(file.native_file.file.path, 'r') + task_file.content = file.read + task_file.used_by_grader = false + task_file.binary = true + task_file.mimetype = MimeMagic.by_magic(file).type + else + task_file.content = file.content + task_file.used_by_grader = true + task_file.binary = false + end end end end diff --git a/app/services/proforma_service/convert_task_to_exercise.rb b/app/services/proforma_service/convert_task_to_exercise.rb index f87a9354..6cb89a2c 100644 --- a/app/services/proforma_service/convert_task_to_exercise.rb +++ b/app/services/proforma_service/convert_task_to_exercise.rb @@ -48,7 +48,7 @@ module ProformaService end def codeocean_file_from_task_file(file) - CodeOcean::File.new({ + codeocean_file = CodeOcean::File.new( context: @exercise, file_type: FileType.find_by(file_extension: File.extname(file.filename)), hidden: file.visible == 'no', @@ -56,13 +56,13 @@ module ProformaService read_only: file.usage_by_lms != 'edit', role: file.internal_description, path: File.dirname(file.filename).in?(['.', '']) ? nil : File.dirname(file.filename) - }.tap do |params| - if file.binary - params[:native_file] = FileIO.new(file.content.dup.force_encoding('UTF-8'), File.basename(file.filename)) - else - params[:content] = file.content - end - end) + ) + if file.binary + codeocean_file.native_file = FileIO.new(file.content.dup.force_encoding('UTF-8'), File.basename(file.filename)) + else + codeocean_file.content = file.content + end + codeocean_file end end end From 8ba764044a20c167d6e5f8937b38f07c77a47788 Mon Sep 17 00:00:00 2001 From: Karol Date: Tue, 10 Dec 2019 17:34:02 +0100 Subject: [PATCH 39/49] fix cognitive complexity --- app/services/proforma_service/convert_exercise_to_task.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/services/proforma_service/convert_exercise_to_task.rb b/app/services/proforma_service/convert_exercise_to_task.rb index ce861120..27836f91 100644 --- a/app/services/proforma_service/convert_exercise_to_task.rb +++ b/app/services/proforma_service/convert_exercise_to_task.rb @@ -88,7 +88,7 @@ module ProformaService def task_file(file) task_file = Proforma::TaskFile.new( id: file.id, - filename: file.path.present? && file.path != '.' ? ::File.join(file.path, file.name_with_extension) : file.name_with_extension, + filename: filename(file), usage_by_lms: file.read_only ? 'display' : 'edit', visible: file.hidden ? 'no' : 'yes', internal_description: file.role || 'regular_file' @@ -97,6 +97,10 @@ module ProformaService task_file end + def filename(file) + file.path.present? && file.path != '.' ? ::File.join(file.path, file.name_with_extension) : file.name_with_extension + end + def add_content_to_task_file(file, task_file) if file.native_file.present? file = ::File.new(file.native_file.file.path, 'r') From eb7a4d5933615a89bf8139c4d578c13d19e96e95 Mon Sep 17 00:00:00 2001 From: Karol Date: Thu, 12 Dec 2019 19:19:47 +0100 Subject: [PATCH 40/49] add german translations --- app/controllers/exercises_controller.rb | 4 +-- app/views/codeharbor_links/_form.html.slim | 2 +- config/locales/de.yml | 38 ++++++++++++++++++++++ config/locales/en.yml | 21 ++++-------- 4 files changed, 47 insertions(+), 18 deletions(-) diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index f0dd0f5a..5c4e4455 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -192,9 +192,9 @@ class ExercisesController < ApplicationController return render json: {}, status: 201 end rescue Proforma::ProformaError - render json: t('exercises.export_codeharbor.export_errors.invalid'), status: 400 + render json: t('exercises.import_codeharbor.import_errors.invalid'), status: 400 rescue StandardError - render json: t('exercises.export_codeharbor.export_errors.internal_error'), status: 500 + render json: t('exercises.import_codeharbor.import_errors.internal_error'), status: 500 end def user_from_api_key diff --git a/app/views/codeharbor_links/_form.html.slim b/app/views/codeharbor_links/_form.html.slim index a07afe1a..f7b84ddc 100644 --- a/app/views/codeharbor_links/_form.html.slim +++ b/app/views/codeharbor_links/_form.html.slim @@ -9,7 +9,7 @@ .form-group = f.label(:api_key) .input-group - = f.text_field(:api_key, data: {toggle: 'tooltip', placement: 'bottom'}, title: t('codeharbor_link.info.token'), class: 'form-control api_key') + = f.text_field(:api_key, data: {toggle: 'tooltip', placement: 'bottom'}, title: t('codeharbor_link.info.api_key'), class: 'form-control api_key') .input-group-btn = button_tag t('codeharbor_link.generate'), type: 'button', class: 'generate-api_key btn btn-default' .actions diff --git a/config/locales/de.yml b/config/locales/de.yml index 16dc3b10..230b433f 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -44,6 +44,8 @@ de: allow_file_creation: "Dateierstellung erlauben" difficulty: Schwierigkeitsgrad token: "Aufgaben-Token" + uuid: UUID + unpublished: Deaktiviert proxy_exercise: title: Title files_count: Anzahl der Aufgaben @@ -240,6 +242,16 @@ de: consumers: show: link: Konsument + codeharbor_link: + generate: Generieren + info: + push_url: Die Url von Codeharbor zu der die Aufgabe exportiert werden soll. Bei Unklarheiten bitte an einen Admin wenden. + check_uuid_url: Die Url von Codeharbor an der geprüft werden kann, ob die Aufgabe schon existiert. Bei Unklarheiten bitte an einen Admin wenden. + api_key: Wird zum Authentifizieren gegenüber Codeharbor genutzt. Auf Codeharbor muss der gleiche Key eingetragen sein. + profile_label: Codeharbor Link + new: Neue Codeharbor Link anlegen + edit: Codeharbor Link bearbeiten + delete: Codeharbor Link entfernen execution_environments: form: hints: @@ -298,6 +310,27 @@ de: request_for_comments_sent: "Kommentaranfrage gesendet." editor_file_tree: file_root: Dateien + import_codeharbor: + check: + no_exercise: Auf Codeharbor wurde keine entsprechende Aufgabe gefunden. Mit dem Export der Aufgabe wird eine neue auf Codeharbor angelegt, die mit dieser verbunden ist. Anschließend können Veränderungen an der Aufgabe von beiden Systemen aus jeweils in das andere Übertragen werden. + exercise_found: Auf Codeharbor wurde eine entsprechende Aufgabe gefunden. Mit dem Export der Aufgabe werden alle Veränderungen die auf Codeocean vorgenommen wurden, exportiert und die Aufgabe auf Codeharbor überschrieben. + exercise_found_no_right: Auf Codeharbor wurde eine entsprechende Aufgabe gefunden, Sie haben aber keine Rechte sie zu bearbeiten. Bitte wenden Sie sich an einen Admin, wenn Sie denken, dass Sie die Rechte dazu besitzen sollten. + import_errors: + invalid: Fehlerhafte Aufgabe + internal_error: Beim Import der Aufgabe ist ein interner Fehler aufgetreten. + export_codeharbor: + label: Zu Codeharbor exportieren + dialogtitle: Zu Codeharbor exportieren + successfully_exported: 'Aufgabe wurde erfolgreich exportiert.
ID: %{id}
Title: %{title}' + export_failed: 'Export ist fehlgeschlagen.
ID: %{id}
Title: %{title}

Error: %{error}' + error: Es ist ein Fehler bei der Kommunikation mit Codeharbor aufgetreten. + checking_codeharbor: Es wird geprüft, ob auf Codeharbor eine korrespondierende Aufgabe gefunden werden kann. + buttons: + retry: Erneut probieren + export: Export + create_new: Neu erstellen + close: Schließen + abort: Abbrechen file_form: hints: feedback_message: Diese Nachricht wird als Tipp zu fehlschlagenden Tests angezeigt. @@ -306,6 +339,9 @@ de: add_file: Datei hinzufügen tags: "Tags" click_to_collapse: "Zum Aus-/Einklappen hier klicken..." + unpublish_warning: Mit dieser Aktion wird die Aufgabe deaktiviert. Jeder Student, der versucht sie zu implementieren wird eine Fehlermeldung bekommen, bis die Aufgabe wieder aktiviert wurde. + no_execution_environment_selected: Bitte eine Ausführungsumgebung auswählen, bevor die Aufgabe aktiviert wird. + none: Keine implement: alert: text: 'Ihr Browser unterstützt nicht alle Funktionalitäten, die %{application_name} benötigt. Bitte nutzen Sie einen modernen Browser, um %{application_name} zu besuchen.' @@ -355,6 +391,8 @@ de: feedback: Feedback requests_for_comments: Kommentaranfragen study_group_dashboard: Live Dashboard + show: + is_unpublished: Aufgabe ist deaktiviert statistics: average_score: Durchschnittliche Punktzahl final_submissions: Finale Abgaben diff --git a/config/locales/en.yml b/config/locales/en.yml index 6211c3e1..a8b45784 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -245,18 +245,9 @@ en: codeharbor_link: generate: Generate info: - push_url: | - The url from Codeharbor where your exercise can be exported to. If you don't know what to write here, ask an admin. - check_uuid_url: | - The url from Codeharbor where we can check if the exercise already exists. If you don't know what to write here, ask an admin. - name: | - Can be anything to Identify your account link. - token: | - Will be used to authenticate your export request. Has to be the same on both sides. - client_id: - Will be sent with your token. Can be automatically generated. - client_secret: - Will be sent with your token. Can be automatically generated. + push_url: The url from Codeharbor where your exercise can be exported to. If you don't know what to write here, ask an admin. + check_uuid_url: The url from Codeharbor where we can check if the exercise already exists. If you don't know what to write here, ask an admin. + api_key: Will be used to authenticate your export request. Has to be the same on both sides. profile_label: Codeharbor Link new: Create link to Codeharbor edit: Edit existing link @@ -321,9 +312,9 @@ en: file_root: Files import_codeharbor: check: - no_exercise: No corresponding exercise found on Codeocean. Pushing this exercise will create a new one on Codeocean, which will be linked to this one on Codeharbor, any changes to either one can be pushed to the respective other platform. - exercise_found: A corresponding exercise has been found on Codeocean. You can either
  • Create a new exercise as a duplicate of this one on Codeharbor and push it to Codeocean, using the "Create new" button.
  • Overwrite the exercise on Codeocean, by pushing all changes. Only use "Overwrite" for bugfixes or very small changes - it will alter and may break published exercises.
- exercise_found_no_right: A corresponding exercise has been found on Codeocean, but you don't have the rights to edit it. You can only
  • Create a new exercise as a duplicate of this one on Codeharbor and push it to Codeocean, using the "Create new" button.
+ no_exercise: No corresponding exercise found on Codeharbor. Pushing this exercise will create a new exercise on Codeharbor, which will be linked to this one on Codeocean. Any changes to either one can be pushed to the respective other platform. + exercise_found: 'A corresponding exercise has been found on Codeharbor. You can export the exercise to transfer all changes made on Codeocean to Codeharbor. Careful: This will overwrite all potential changes made on Codeharbor.' + exercise_found_no_right: A corresponding exercise has been found on Codeharbor, but you don't have the rights to edit it. Please contact an Admin if you think you should be able to edit the exercise on Codeharbor. export_codeharbor: label: Export to Codeharbor dialogtitle: Export to Codeharbor From f68091638590609e092e2c897e2518aaf5db3a00 Mon Sep 17 00:00:00 2001 From: Karol Date: Fri, 13 Dec 2019 16:43:19 +0100 Subject: [PATCH 41/49] remove create_new functionality, when exercise exists on CH but is not editable --- app/assets/javascripts/exercises.js.erb | 7 ++----- app/controllers/exercises_controller.rb | 15 ++++++------- .../exercise_service/check_external.rb | 7 ++++++- app/views/exercises/_export_actions.html.slim | 8 ++----- config/locales/de.yml | 9 ++++---- config/locales/en.yml | 15 +++++++------ spec/controllers/exercises_controller_spec.rb | 21 ++++--------------- .../exercise_service/check_external_spec.rb | 18 +++++++++++----- 8 files changed, 44 insertions(+), 56 deletions(-) diff --git a/app/assets/javascripts/exercises.js.erb b/app/assets/javascripts/exercises.js.erb index 08d119ad..66c8dfb6 100644 --- a/app/assets/javascripts/exercises.js.erb +++ b/app/assets/javascripts/exercises.js.erb @@ -251,7 +251,7 @@ $(document).on('turbolinks:load', function() { exportExerciseStart($(this).data().exerciseId); }); $('body').on('click', '.export-action', function(){ - exportExerciseConfirm($(this).data().exerciseId, $(this).data().exportType); + exportExerciseConfirm($(this).data().exerciseId); }); } @@ -279,7 +279,7 @@ $(document).on('turbolinks:load', function() { }); }; - var exportExerciseConfirm = function(exerciseID, pushType) { + var exportExerciseConfirm = function(exerciseID) { var $exerciseDiv = $('#export-exercise'); var $messageDiv = $exerciseDiv.children('.export-message'); var $actionsDiv = $exerciseDiv.children('.export-exercise-actions'); @@ -287,9 +287,6 @@ $(document).on('turbolinks:load', function() { return $.ajax({ type: 'POST', url: '/exercises/' + exerciseID + '/export_external_confirm', - data: { - push_type: pushType - }, dataType: 'json', success: function(response) { $messageDiv.html(response.message) diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 5c4e4455..805c4268 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -129,7 +129,7 @@ class ExercisesController < ApplicationController partial: 'export_actions', locals: { exercise: @exercise, - # exercise_found: codeharbor_check[:exercise_found], + exercise_found: codeharbor_check[:exercise_found], update_right: codeharbor_check[:update_right], error: codeharbor_check[:error], exported: false @@ -139,11 +139,7 @@ class ExercisesController < ApplicationController end def export_external_confirm - push_type = params[:push_type] - - return render json: {}, status: 500 unless %w[create_new export].include? push_type - - @exercise.uuid = SecureRandom.uuid if push_type == 'create_new' + @exercise.uuid = SecureRandom.uuid if @exercise.uuid.nil? error = ExerciseService::PushExternal.call( zip: ProformaService::ExportTask.call(exercise: @exercise), @@ -155,6 +151,7 @@ class ExercisesController < ApplicationController message: t('exercises.export_codeharbor.successfully_exported', id: @exercise.id, title: @exercise.title), actions: render_to_string(partial: 'export_actions', locals: {exercise: @exercise, exported: true, error: error}) } + @exercise.save else render json: { status: 'fail', @@ -171,10 +168,10 @@ class ExercisesController < ApplicationController uuid = params[:uuid] exercise = Exercise.find_by(uuid: uuid) - return render json: {exercise_found: false, message: t('exercises.import_codeharbor.check.no_exercise')} if exercise.nil? - return render json: {exercise_found: true, update_right: false, message: t('exercises.import_codeharbor.check.exercise_found_no_right')} unless ExercisePolicy.new(user, exercise).update? + return render json: {exercise_found: false} if exercise.nil? + return render json: {exercise_found: true, update_right: false} unless ExercisePolicy.new(user, exercise).update? - render json: {exercise_found: true, update_right: true, message: t('exercises.import_codeharbor.check.exercise_found')} + render json: {exercise_found: true, update_right: true} end def import_exercise diff --git a/app/services/exercise_service/check_external.rb b/app/services/exercise_service/check_external.rb index a7add85d..7bfb12e3 100644 --- a/app/services/exercise_service/check_external.rb +++ b/app/services/exercise_service/check_external.rb @@ -14,8 +14,13 @@ module ExerciseService req.body = {uuid: @uuid}.to_json end response_hash = JSON.parse(response.body, symbolize_names: true) + message = if response_hash[:exercise_found] + response_hash[:update_right] ? I18n.t('exercises.export_codeharbor.check.exercise_found') : I18n.t('exercises.export_codeharbor.check.exercise_found_no_right') + else + I18n.t('exercises.export_codeharbor.check.no_exercise') + end - {error: false}.merge(response_hash.slice(:message, :exercise_found, :update_right)) + {error: false, message: message}.merge(response_hash.slice(:exercise_found, :update_right)) rescue Faraday::Error, JSON::ParserError {error: true, message: I18n.t('exercises.export_codeharbor.error')} end diff --git a/app/views/exercises/_export_actions.html.slim b/app/views/exercises/_export_actions.html.slim index 0008670d..0484d2b2 100644 --- a/app/views/exercises/_export_actions.html.slim +++ b/app/views/exercises/_export_actions.html.slim @@ -4,14 +4,10 @@ = t('exercises.export_codeharbor.buttons.retry') - else - unless exported - - if update_right - = button_tag type: 'button', class:'btn btn-primary pull-right export-action export-button', data: {exercise_id: exercise.id, export_type: 'export'} do + - if !exercise_found || update_right + = button_tag type: 'button', class:'btn btn-primary pull-right export-action export-button', data: {exercise_id: exercise.id} do i.fa.fa-check.confirm-icon = t('exercises.export_codeharbor.buttons.export') - - else - = button_tag type: 'button', class:'btn btn-primary pull-right export-action export-button', data: {exercise_id: exercise.id, export_type: 'create_new'} do - i.fa.fa-check.confirm-icon - = t('exercises.export_codeharbor.buttons.create_new') = button_tag type: 'submit', class:'btn btn-secondary pull-right export-button', data: {dismiss: 'modal'} do i.fa.fa-remove.abort-icon diff --git a/config/locales/de.yml b/config/locales/de.yml index 230b433f..2b5985ac 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -311,10 +311,6 @@ de: editor_file_tree: file_root: Dateien import_codeharbor: - check: - no_exercise: Auf Codeharbor wurde keine entsprechende Aufgabe gefunden. Mit dem Export der Aufgabe wird eine neue auf Codeharbor angelegt, die mit dieser verbunden ist. Anschließend können Veränderungen an der Aufgabe von beiden Systemen aus jeweils in das andere Übertragen werden. - exercise_found: Auf Codeharbor wurde eine entsprechende Aufgabe gefunden. Mit dem Export der Aufgabe werden alle Veränderungen die auf Codeocean vorgenommen wurden, exportiert und die Aufgabe auf Codeharbor überschrieben. - exercise_found_no_right: Auf Codeharbor wurde eine entsprechende Aufgabe gefunden, Sie haben aber keine Rechte sie zu bearbeiten. Bitte wenden Sie sich an einen Admin, wenn Sie denken, dass Sie die Rechte dazu besitzen sollten. import_errors: invalid: Fehlerhafte Aufgabe internal_error: Beim Import der Aufgabe ist ein interner Fehler aufgetreten. @@ -328,9 +324,12 @@ de: buttons: retry: Erneut probieren export: Export - create_new: Neu erstellen close: Schließen abort: Abbrechen + check: + no_exercise: Auf Codeharbor wurde keine entsprechende Aufgabe gefunden. Mit dem Export der Aufgabe wird eine neue auf Codeharbor angelegt, die mit dieser verbunden ist. Anschließend können Veränderungen an der Aufgabe von beiden Systemen aus jeweils in das andere Übertragen werden. + exercise_found: Auf Codeharbor wurde eine entsprechende Aufgabe gefunden. Mit dem Export der Aufgabe werden alle Veränderungen, die auf Codeocean vorgenommen wurden, exportiert und die Aufgabe auf Codeharbor überschrieben. + exercise_found_no_right: Auf Codeharbor wurde eine entsprechende Aufgabe gefunden, Sie haben aber keine Rechte sie zu bearbeiten. Bitte wenden Sie sich an einen Admin, wenn Sie denken, dass Sie die Rechte dazu besitzen sollten. file_form: hints: feedback_message: Diese Nachricht wird als Tipp zu fehlschlagenden Tests angezeigt. diff --git a/config/locales/en.yml b/config/locales/en.yml index a8b45784..84c4f89f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -311,10 +311,9 @@ en: editor_file_tree: file_root: Files import_codeharbor: - check: - no_exercise: No corresponding exercise found on Codeharbor. Pushing this exercise will create a new exercise on Codeharbor, which will be linked to this one on Codeocean. Any changes to either one can be pushed to the respective other platform. - exercise_found: 'A corresponding exercise has been found on Codeharbor. You can export the exercise to transfer all changes made on Codeocean to Codeharbor. Careful: This will overwrite all potential changes made on Codeharbor.' - exercise_found_no_right: A corresponding exercise has been found on Codeharbor, but you don't have the rights to edit it. Please contact an Admin if you think you should be able to edit the exercise on Codeharbor. + import_errors: + invalid: Invalid exercise + internal_error: An internal error occurred on Codeharbor while importing the exercise. export_codeharbor: label: Export to Codeharbor dialogtitle: Export to Codeharbor @@ -325,12 +324,12 @@ en: buttons: retry: Retry export: Export - create_new: Create new close: Close abort: Abort - export_errors: - invalid: Invalid exercise - internal_error: An internal error occurred on Codeharbor while importing the exercise. + check: + no_exercise: No corresponding exercise found on Codeharbor. Pushing this exercise will create a new exercise on Codeharbor, which will be linked to this one on Codeocean. Any changes to either one can be pushed to the respective other platform. + exercise_found: 'A corresponding exercise has been found on Codeharbor. You can export the exercise to transfer all changes made on Codeocean to Codeharbor. Careful: This will overwrite all potential changes made on Codeharbor.' + exercise_found_no_right: A corresponding exercise has been found on Codeharbor, but you don't have the rights to edit it. Please contact an Admin if you think you should be able to edit the exercise on Codeharbor. file_form: hints: feedback_message: This message is used as a hint for failing tests. diff --git a/spec/controllers/exercises_controller_spec.rb b/spec/controllers/exercises_controller_spec.rb index 3c5289c0..2b5cf523 100644 --- a/spec/controllers/exercises_controller_spec.rb +++ b/spec/controllers/exercises_controller_spec.rb @@ -350,7 +350,7 @@ describe ExercisesController do include('button').and(include('Abort')).and(include('Retry')) ) expect(JSON.parse(response.body).symbolize_keys[:actions]).to( - not_include('Create new').and(not_include('Export')).and(not_include('Hide')) + not_include('Export').and(not_include('Hide')) ) end end @@ -362,7 +362,7 @@ describe ExercisesController do post_request expect(JSON.parse(response.body).symbolize_keys[:message]).to eq('message') expect(JSON.parse(response.body).symbolize_keys[:actions]).to( - include('button').and(include('Abort')).and(include('Create new')) + include('button').and(include('Abort')) ) expect(JSON.parse(response.body).symbolize_keys[:actions]).to( not_include('Retry').and(not_include('Export')).and(not_include('Hide')) @@ -375,8 +375,7 @@ describe ExercisesController do render_views let!(:codeharbor_link) { FactoryBot.create(:codeharbor_link, user: user) } - let(:post_request) { post :export_external_confirm, params: {push_type: push_type, id: exercise.id, codeharbor_link: codeharbor_link.id} } - let(:push_type) { 'create_new' } + let(:post_request) { post :export_external_confirm, params: {id: exercise.id, codeharbor_link: codeharbor_link.id} } let(:error) {} let(:zip) { 'zip' } @@ -407,15 +406,6 @@ describe ExercisesController do expect(JSON.parse(response.body).symbolize_keys[:actions]).to(not_include('Abort')) end end - - context 'without push_type' do - let(:push_type) {} - - it 'responds with status 500' do - post_request - expect(response).to have_http_status(:internal_server_error) - end - end end describe '#import_uuid_check' do @@ -433,7 +423,6 @@ describe ExercisesController do expect(JSON.parse(response.body).symbolize_keys[:exercise_found]).to be true expect(JSON.parse(response.body).symbolize_keys[:update_right]).to be true - expect(JSON.parse(response.body).symbolize_keys[:message]).to(include('has been found').and(include('Overwrite'))) end context 'when api_key is incorrect' do @@ -445,7 +434,7 @@ describe ExercisesController do end end - context 'when the user is cannot update the exercise' do + context 'when the user cannot update the exercise' do let(:codeharbor_link) { FactoryBot.create(:codeharbor_link, api_key: 'anotherkey') } it 'renders correct response' do @@ -454,7 +443,6 @@ describe ExercisesController do expect(JSON.parse(response.body).symbolize_keys[:exercise_found]).to be true expect(JSON.parse(response.body).symbolize_keys[:update_right]).to be false - expect(JSON.parse(response.body).symbolize_keys[:message]).to(include('has been found').and(not_include('Overwrite'))) end end @@ -466,7 +454,6 @@ describe ExercisesController do expect(response).to have_http_status(:success) expect(JSON.parse(response.body).symbolize_keys[:exercise_found]).to be false - expect(JSON.parse(response.body).symbolize_keys[:message]).to(include('No corresponding exercise')) end end end diff --git a/spec/services/exercise_service/check_external_spec.rb b/spec/services/exercise_service/check_external_spec.rb index ea7874c6..f5ee4b97 100644 --- a/spec/services/exercise_service/check_external_spec.rb +++ b/spec/services/exercise_service/check_external_spec.rb @@ -42,17 +42,25 @@ describe ExerciseService::CheckExternal do end context 'when response contains a JSON with expected keys' do - let(:response) { {message: 'message', exercise_found: true, update_right: true}.to_json } + let(:response) { {exercise_found: true, update_right: true}.to_json } it 'returns the correct hash' do - expect(check_external_service).to eql(error: false, message: 'message', exercise_found: true, update_right: true) + expect(check_external_service).to eql(error: false, message: I18n.t('exercises.export_codeharbor.check.exercise_found'), exercise_found: true, update_right: true) end - context 'with different values' do - let(:response) { {message: 'message', exercise_found: false, update_right: false}.to_json } + context 'with exercise_found: false and no update_right' do + let(:response) { {exercise_found: false}.to_json } it 'returns the correct hash' do - expect(check_external_service).to eql(error: false, message: 'message', exercise_found: false, update_right: false) + expect(check_external_service).to eql(error: false, message: I18n.t('exercises.export_codeharbor.check.no_exercise'), exercise_found: false) + end + end + + context 'with exercise_found: true and update_right: false' do + let(:response) { {exercise_found: true, update_right: false}.to_json } + + it 'returns the correct hash' do + expect(check_external_service).to eql(error: false, message: I18n.t('exercises.export_codeharbor.check.exercise_found_no_right'), exercise_found: true, update_right: false) end end end From 1ddd6e19f51276d0bc1ee1d052aae0207df113c3 Mon Sep 17 00:00:00 2001 From: Karol Date: Sat, 14 Dec 2019 12:34:55 +0100 Subject: [PATCH 42/49] add transaction spec --- spec/controllers/exercises_controller_spec.rb | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/spec/controllers/exercises_controller_spec.rb b/spec/controllers/exercises_controller_spec.rb index 2b5cf523..ae41de10 100644 --- a/spec/controllers/exercises_controller_spec.rb +++ b/spec/controllers/exercises_controller_spec.rb @@ -460,7 +460,7 @@ describe ExercisesController do describe 'POST #import_exercise' do let(:codeharbor_link) { FactoryBot.create(:codeharbor_link, user: user) } - let(:imported_exercise) { exercise } + let!(:imported_exercise) { FactoryBot.create(:fibonacci) } let(:post_request) { post :import_exercise, body: zip_file_content } let(:zip_file_content) { 'zipped task xml' } let(:headers) { {'Authorization' => "Bearer #{codeharbor_link.api_key}"} } @@ -497,6 +497,13 @@ describe ExercisesController do expect(response).to have_http_status(:internal_server_error) end end - end + context 'when the imported exercise is invalid' do + before { allow(ProformaService::Import).to receive(:call) { imported_exercise.tap { |e| e.files = [] }.tap { |e| e.title = nil } } } + + it 'responds with correct status code' do + expect { post_request }.not_to(change { imported_exercise.reload.files.count }) + end + end + end end From da8d31279cb4c10b620562c07e525468e5af2f60 Mon Sep 17 00:00:00 2001 From: Karol Date: Mon, 16 Dec 2019 17:38:32 +0100 Subject: [PATCH 43/49] review points --- app/models/user.rb | 2 +- app/policies/codeharbor_link_policy.rb | 4 +- app/services/proforma_service/import.rb | 8 +--- spec/policies/codeharbor_link_policy_spec.rb | 8 ++-- spec/services/proforma_service/import_spec.rb | 40 ++++--------------- 5 files changed, 16 insertions(+), 46 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 0315e761..686456f4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -13,7 +13,7 @@ class User < ApplicationRecord has_many :user_proxy_exercise_exercises, as: :user has_many :user_exercise_interventions, as: :user has_many :interventions, through: :user_exercise_interventions - has_one :codeharbor_link + has_one :codeharbor_link, dependent: :destroy accepts_nested_attributes_for :user_proxy_exercise_exercises diff --git a/app/policies/codeharbor_link_policy.rb b/app/policies/codeharbor_link_policy.rb index 21b1dab2..3dd4ff54 100644 --- a/app/policies/codeharbor_link_policy.rb +++ b/app/policies/codeharbor_link_policy.rb @@ -8,11 +8,11 @@ class CodeharborLinkPolicy < ApplicationPolicy end def new? - teacher? + teacher? || admin? end def create? - teacher? + teacher? || admin? end def edit? diff --git a/app/services/proforma_service/import.rb b/app/services/proforma_service/import.rb index 4fa0676e..9864b31e 100644 --- a/app/services/proforma_service/import.rb +++ b/app/services/proforma_service/import.rb @@ -28,13 +28,9 @@ module ProformaService def base_exercise exercise = Exercise.find_by(uuid: @task.uuid) - if exercise - return exercise if ExercisePolicy.new(@user, exercise).update? + return exercise if exercise && ExercisePolicy.new(@user, exercise).update? - return Exercise.new(uuid: SecureRandom.uuid, unpublished: true) - end - - Exercise.new(uuid: @task.uuid || SecureRandom.uuid, unpublished: true) + Exercise.new(uuid: @task.uuid, unpublished: true) end def import_multi diff --git a/spec/policies/codeharbor_link_policy_spec.rb b/spec/policies/codeharbor_link_policy_spec.rb index 9569dd90..dcb6ace3 100644 --- a/spec/policies/codeharbor_link_policy_spec.rb +++ b/spec/policies/codeharbor_link_policy_spec.rb @@ -18,13 +18,13 @@ describe CodeharborLinkPolicy do %i[new? create?].each do |action| permissions(action) do it 'grants access to teachers' do - expect(policy).to permit(FactoryBot.create(:teacher), codeharbor_link) + %i[teacher admin].each do |factory_name| + expect(policy).to permit(FactoryBot.create(factory_name), codeharbor_link) + end end it 'does not grant access to all other users' do - %i[external_user admin].each do |factory_name| - expect(policy).not_to permit(FactoryBot.create(factory_name), codeharbor_link) - end + expect(policy).not_to permit(FactoryBot.create(:external_user), codeharbor_link) end end end diff --git a/spec/services/proforma_service/import_spec.rb b/spec/services/proforma_service/import_spec.rb index 118284f1..6f83c4bf 100644 --- a/spec/services/proforma_service/import_spec.rb +++ b/spec/services/proforma_service/import_spec.rb @@ -128,42 +128,14 @@ describe ProformaService::Import do it { is_expected.to be_an_equal_exercise_as exercise } end - # context 'when zip contains multiple tasks' do - # let(:exporter) { ProformaService::ExportTasks.call(exercises: [exercise, exercise2]).string } - - # let(:exercise2) do - # FactoryBot.create(:dummy, - # instruction: 'instruction2', - # execution_environment: execution_environment, - # exercise_files: [], - # tests: [], - # user: user) - # end - - # it 'imports the exercises from zip containing multiple zips' do - # expect(import_service).to all be_an(Exercise) - # end - - # it 'imports the zip exactly how they were exported' do - # expect(import_service).to all be_an_equal_exercise_as(exercise).or be_an_equal_exercise_as(exercise2) - # end - - # context 'when a exercise has files and tests' do - # let(:files) { [FactoryBot.build(:file), FactoryBot.build(:file, role: 'regular_file')] } - # let(:tests) { FactoryBot.build_list(:codeharbor_test, 2) } - - # it 'imports the zip exactly how the were exported' do - # expect(import_service).to all be_an_equal_exercise_as(exercise).or be_an_equal_exercise_as(exercise2) - # end - # end - # end - context 'when task in zip has a different uuid' do let(:uuid) { SecureRandom.uuid } let(:new_uuid) { SecureRandom.uuid } + let(:imported_exercise) { import_service } before do exercise.update(uuid: new_uuid) + imported_exercise.save! end it 'creates a new Exercise' do @@ -173,16 +145,18 @@ describe ProformaService::Import do context 'when task in zip has the same uuid and nothing has changed' do let(:uuid) { SecureRandom.uuid } + let(:imported_exercise) { import_service } it 'updates the old Exercise' do - expect(import_service.id).to be exercise.id + imported_exercise.save! + expect(imported_exercise.id).to be exercise.id end context 'when another user imports the exercise' do let(:import_user) { FactoryBot.create(:teacher) } - it 'creates a new Exercise' do - expect(import_service.id).not_to be exercise.id + it 'raises a validation error' do + expect { imported_exercise.save! } .to raise_error ActiveRecord::RecordInvalid end end end From 7d4c4a4494ea04173383768406052e38d22c1917 Mon Sep 17 00:00:00 2001 From: Karol Date: Mon, 16 Dec 2019 17:43:47 +0100 Subject: [PATCH 44/49] remove checksum --- app/services/proforma_service/convert_exercise_to_task.rb | 3 +-- app/services/proforma_service/convert_task_to_exercise.rb | 3 +-- .../20190905152630_add_import_checksum_to_exercises.rb | 5 ----- db/schema.rb | 1 - .../proforma_service/convert_exercise_to_task_spec.rb | 3 +-- .../proforma_service/convert_task_to_exercise_spec.rb | 4 +--- spec/support/expectations/equal_exercise.rb | 2 +- 7 files changed, 5 insertions(+), 16 deletions(-) delete mode 100644 db/migrate/20190905152630_add_import_checksum_to_exercises.rb diff --git a/app/services/proforma_service/convert_exercise_to_task.rb b/app/services/proforma_service/convert_exercise_to_task.rb index 27836f91..b628b04d 100644 --- a/app/services/proforma_service/convert_exercise_to_task.rb +++ b/app/services/proforma_service/convert_exercise_to_task.rb @@ -26,8 +26,7 @@ module ProformaService tests: tests, uuid: uuid, language: DEFAULT_LANGUAGE, - model_solutions: model_solutions, - import_checksum: @exercise.import_checksum + model_solutions: model_solutions }.compact ) end diff --git a/app/services/proforma_service/convert_task_to_exercise.rb b/app/services/proforma_service/convert_task_to_exercise.rb index 6cb89a2c..7f49fa40 100644 --- a/app/services/proforma_service/convert_task_to_exercise.rb +++ b/app/services/proforma_service/convert_task_to_exercise.rb @@ -21,8 +21,7 @@ module ProformaService title: @task.title, description: @task.description, instructions: @task.internal_description, - files: files, - import_checksum: @task.checksum + files: files ) end diff --git a/db/migrate/20190905152630_add_import_checksum_to_exercises.rb b/db/migrate/20190905152630_add_import_checksum_to_exercises.rb deleted file mode 100644 index 26a5d365..00000000 --- a/db/migrate/20190905152630_add_import_checksum_to_exercises.rb +++ /dev/null @@ -1,5 +0,0 @@ -class AddImportChecksumToExercises < ActiveRecord::Migration[5.2] - def change - add_column :exercises, :import_checksum, :string - end -end diff --git a/db/schema.rb b/db/schema.rb index fcd4e86b..5c3ffdf7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -153,7 +153,6 @@ ActiveRecord::Schema.define(version: 2019_10_08_163045) do t.boolean "allow_auto_completion", default: false t.integer "expected_difficulty", default: 1 t.uuid "uuid" - t.string "import_checksum" t.boolean "unpublished", default: false t.index ["id"], name: "index_exercises_on_id" end diff --git a/spec/services/proforma_service/convert_exercise_to_task_spec.rb b/spec/services/proforma_service/convert_exercise_to_task_spec.rb index ae2976d1..f6fe6b7d 100644 --- a/spec/services/proforma_service/convert_exercise_to_task_spec.rb +++ b/spec/services/proforma_service/convert_exercise_to_task_spec.rb @@ -40,8 +40,7 @@ RSpec.describe ProformaService::ConvertExerciseToTask do # parent_uuid: exercise.clone_relations.first&.origin&.uuid, files: [], tests: [], - model_solutions: [], - import_checksum: exercise.import_checksum + model_solutions: [] ) end diff --git a/spec/services/proforma_service/convert_task_to_exercise_spec.rb b/spec/services/proforma_service/convert_task_to_exercise_spec.rb index 1d66d32f..ad41de3e 100644 --- a/spec/services/proforma_service/convert_task_to_exercise_spec.rb +++ b/spec/services/proforma_service/convert_task_to_exercise_spec.rb @@ -39,9 +39,7 @@ describe ProformaService::ConvertTaskToExercise do language: 'language', model_solutions: model_solutions, files: files, - tests: tests, - import_checksum: 'import_checksum', - checksum: 'checksum' + tests: tests ) end let(:user) { FactoryBot.create(:teacher) } diff --git a/spec/support/expectations/equal_exercise.rb b/spec/support/expectations/equal_exercise.rb index df21ec3d..8f72955c 100644 --- a/spec/support/expectations/equal_exercise.rb +++ b/spec/support/expectations/equal_exercise.rb @@ -37,6 +37,6 @@ RSpec::Matchers.define :be_an_equal_exercise_as do |exercise| def attributes_and_associations(object) object.attributes.dup.tap do |attributes| attributes[:files] = object.files if defined? object.files - end.except('id', 'created_at', 'updated_at', 'exercise_id', 'uuid', 'import_checksum') + end.except('id', 'created_at', 'updated_at', 'exercise_id', 'uuid') end end From 2afbcacb1a4cbeb1244d64ea5c9e23c7fb455312 Mon Sep 17 00:00:00 2001 From: Karol Date: Wed, 18 Dec 2019 17:06:00 +0100 Subject: [PATCH 45/49] update proforma gem --- Gemfile | 2 +- Gemfile.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index 526aa919..0bed2524 100644 --- a/Gemfile +++ b/Gemfile @@ -37,7 +37,7 @@ gem 'rest-client' gem 'rubyzip' gem 'mnemosyne-ruby' gem 'faraday' -gem 'proforma', git: 'https://github.com/openHPI/proforma.git', tag: 'v0.3.2' +gem 'proforma', git: 'https://github.com/openHPI/proforma.git', tag: 'v0.4' gem 'whenever', require: false gem 'rails-timeago' diff --git a/Gemfile.lock b/Gemfile.lock index 75ee23c2..9e0da505 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,12 +9,12 @@ GIT GIT remote: https://github.com/openHPI/proforma.git - revision: 6784ace5bb4449fd53f419fa1eb40dfc04e8f086 - tag: v0.3.2 + revision: b09c9fcc1fdb314fc1fb1396e7d0bb2d70e376c5 + tag: v0.4 specs: - proforma (0.3.2) - activemodel (~> 5.2.3) - activesupport (~> 5.2.3) + proforma (0.4) + activemodel (>= 5.2.3, < 6.1.0) + activesupport (>= 5.2.3, < 6.1.0) nokogiri (~> 1.10.2) rubyzip (>= 1.2.2, < 2.1.0) From 12c76b2fe4e04606b4c6573cc61ad126a68298ff Mon Sep 17 00:00:00 2001 From: Karol Date: Wed, 18 Dec 2019 17:06:31 +0100 Subject: [PATCH 46/49] reduced perceived complexity --- app/services/exercise_service/check_external.rb | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/app/services/exercise_service/check_external.rb b/app/services/exercise_service/check_external.rb index 7bfb12e3..cf4a818e 100644 --- a/app/services/exercise_service/check_external.rb +++ b/app/services/exercise_service/check_external.rb @@ -13,20 +13,23 @@ module ExerciseService req.headers['Authorization'] = 'Bearer ' + @codeharbor_link.api_key req.body = {uuid: @uuid}.to_json end - response_hash = JSON.parse(response.body, symbolize_names: true) - message = if response_hash[:exercise_found] - response_hash[:update_right] ? I18n.t('exercises.export_codeharbor.check.exercise_found') : I18n.t('exercises.export_codeharbor.check.exercise_found_no_right') - else - I18n.t('exercises.export_codeharbor.check.no_exercise') - end + response_hash = JSON.parse(response.body, symbolize_names: true).slice(:exercise_found, :update_right) - {error: false, message: message}.merge(response_hash.slice(:exercise_found, :update_right)) + {error: false, message: message(response_hash[:exercise_found], response_hash[:update_right])}.merge(response_hash) rescue Faraday::Error, JSON::ParserError {error: true, message: I18n.t('exercises.export_codeharbor.error')} end private + def message(exercise_found, update_right) + if exercise_found + update_right ? I18n.t('exercises.export_codeharbor.check.exercise_found') : I18n.t('exercises.export_codeharbor.check.exercise_found_no_right') + else + I18n.t('exercises.export_codeharbor.check.no_exercise') + end + end + def connection Faraday.new(url: @codeharbor_link.check_uuid_url) do |faraday| faraday.options[:open_timeout] = 5 From f49cd0bed48190cfa2e076a4d59cc2a45f1d82f4 Mon Sep 17 00:00:00 2001 From: Karol Date: Wed, 18 Dec 2019 17:52:34 +0100 Subject: [PATCH 47/49] forbid users to import an exercise they do not have access to (previously a new one was created) --- app/controllers/exercises_controller.rb | 2 ++ app/errors/proforma/exercise_not_owned.rb | 5 +++++ app/services/proforma_service/import.rb | 8 ++++++-- spec/controllers/exercises_controller_spec.rb | 9 +++++++++ spec/services/proforma_service/import_spec.rb | 4 ++-- 5 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 app/errors/proforma/exercise_not_owned.rb diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 805c4268..b08cecf3 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -188,6 +188,8 @@ class ExercisesController < ApplicationController exercise.save! return render json: {}, status: 201 end + rescue Proforma::ExerciseNotOwned + render json: {}, status: 401 rescue Proforma::ProformaError render json: t('exercises.import_codeharbor.import_errors.invalid'), status: 400 rescue StandardError diff --git a/app/errors/proforma/exercise_not_owned.rb b/app/errors/proforma/exercise_not_owned.rb new file mode 100644 index 00000000..afe96364 --- /dev/null +++ b/app/errors/proforma/exercise_not_owned.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module Proforma + class ExerciseNotOwned < StandardError; end +end diff --git a/app/services/proforma_service/import.rb b/app/services/proforma_service/import.rb index 9864b31e..9a2e6317 100644 --- a/app/services/proforma_service/import.rb +++ b/app/services/proforma_service/import.rb @@ -28,9 +28,13 @@ module ProformaService def base_exercise exercise = Exercise.find_by(uuid: @task.uuid) - return exercise if exercise && ExercisePolicy.new(@user, exercise).update? + if exercise + raise Proforma::ExerciseNotOwned unless ExercisePolicy.new(@user, exercise).update? - Exercise.new(uuid: @task.uuid, unpublished: true) + exercise + else + Exercise.new(uuid: @task.uuid, unpublished: true) + end end def import_multi diff --git a/spec/controllers/exercises_controller_spec.rb b/spec/controllers/exercises_controller_spec.rb index ae41de10..7a687e38 100644 --- a/spec/controllers/exercises_controller_spec.rb +++ b/spec/controllers/exercises_controller_spec.rb @@ -489,6 +489,15 @@ describe ExercisesController do end end + context 'when import fails with ExerciseNotOwned' do + before { allow(ProformaService::Import).to receive(:call).and_raise(Proforma::ExerciseNotOwned) } + + it 'responds with correct status code' do + post_request + expect(response).to have_http_status(:unauthorized) + end + end + context 'when import fails due to another error' do before { allow(ProformaService::Import).to receive(:call).and_raise(StandardError) } diff --git a/spec/services/proforma_service/import_spec.rb b/spec/services/proforma_service/import_spec.rb index 6f83c4bf..d674a68d 100644 --- a/spec/services/proforma_service/import_spec.rb +++ b/spec/services/proforma_service/import_spec.rb @@ -155,8 +155,8 @@ describe ProformaService::Import do context 'when another user imports the exercise' do let(:import_user) { FactoryBot.create(:teacher) } - it 'raises a validation error' do - expect { imported_exercise.save! } .to raise_error ActiveRecord::RecordInvalid + it 'raises a proforma error' do + expect { imported_exercise.save! } .to raise_error Proforma::ExerciseNotOwned end end end From 9670246e752d65ee22e928f647e505f220e2d93d Mon Sep 17 00:00:00 2001 From: Karol Date: Wed, 18 Dec 2019 18:41:06 +0100 Subject: [PATCH 48/49] rake task to export public exercises --- lib/tasks/export_public_exercises.rake | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 lib/tasks/export_public_exercises.rake diff --git a/lib/tasks/export_public_exercises.rake b/lib/tasks/export_public_exercises.rake new file mode 100644 index 00000000..aa82c8d1 --- /dev/null +++ b/lib/tasks/export_public_exercises.rake @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +namespace :export_exercises do + desc 'exports all public exercises to codeharbor' + task :public, [:codeharbor_link_id] => [:environment] do |_, args| + codeharbor_link = CodeharborLink.find(args.codeharbor_link_id) + + Exercise.where(public: true).each do |exercise| + puts "Exporting exercise\# #{exercise.id}" + error = ExerciseService::PushExternal.call( + zip: ProformaService::ExportTask.call(exercise: exercise), + codeharbor_link: codeharbor_link + ) + if error.nil? + puts "Successfully exported exercise\# #{exercise.id}" + else + puts "An error occured during export of exercise\# #{exercise.id}: #{error}" + end + end + end +end From 2432678ea120e9f1a1db8a6650426f2e598ab37c Mon Sep 17 00:00:00 2001 From: Karol Date: Thu, 19 Dec 2019 17:49:53 +0100 Subject: [PATCH 49/49] make rake task more verbose --- lib/tasks/export_public_exercises.rake | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/tasks/export_public_exercises.rake b/lib/tasks/export_public_exercises.rake index aa82c8d1..162c36ed 100644 --- a/lib/tasks/export_public_exercises.rake +++ b/lib/tasks/export_public_exercises.rake @@ -4,18 +4,24 @@ namespace :export_exercises do desc 'exports all public exercises to codeharbor' task :public, [:codeharbor_link_id] => [:environment] do |_, args| codeharbor_link = CodeharborLink.find(args.codeharbor_link_id) - + successful_exports = [] + failed_exports = [] Exercise.where(public: true).each do |exercise| - puts "Exporting exercise\# #{exercise.id}" + puts "Exporting exercise \##{exercise.id}" error = ExerciseService::PushExternal.call( zip: ProformaService::ExportTask.call(exercise: exercise), codeharbor_link: codeharbor_link ) if error.nil? + successful_exports << exercise.id puts "Successfully exported exercise\# #{exercise.id}" else + failed_exports << exercise.id puts "An error occured during export of exercise\# #{exercise.id}: #{error}" end end + + puts "successful exports: count: #{successful_exports.count} \nlist: #{successful_exports}" + puts "failed exports: count: #{failed_exports.count} \nlist: #{failed_exports}" end end