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