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 }