Merge pull request #423 from openHPI/implement_codeharbor_interface
Implement codeharbor interface
This commit is contained in:
6
Gemfile
6
Gemfile
@ -36,6 +36,8 @@ gem 'webpacker'
|
||||
gem 'rest-client'
|
||||
gem 'rubyzip'
|
||||
gem 'mnemosyne-ruby'
|
||||
gem 'faraday'
|
||||
gem 'proforma', git: 'https://github.com/openHPI/proforma.git', tag: 'v0.4'
|
||||
gem 'whenever', require: false
|
||||
gem 'rails-timeago'
|
||||
|
||||
@ -49,6 +51,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'
|
||||
@ -69,6 +72,9 @@ 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
|
||||
gem 'webmock'
|
||||
end
|
||||
|
31
Gemfile.lock
31
Gemfile.lock
@ -7,6 +7,17 @@ GIT
|
||||
rack (>= 1.5.0)
|
||||
websocket (>= 1.1.0)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/openHPI/proforma.git
|
||||
revision: b09c9fcc1fdb314fc1fb1396e7d0bb2d70e376c5
|
||||
tag: v0.4
|
||||
specs:
|
||||
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)
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
@ -117,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)
|
||||
@ -145,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)
|
||||
@ -233,6 +247,8 @@ GEM
|
||||
pry-byebug (3.7.0)
|
||||
byebug (~> 11.0)
|
||||
pry (~> 0.10)
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (4.0.1)
|
||||
puma (4.3.1)
|
||||
nio4r (~> 2.0)
|
||||
@ -302,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)
|
||||
@ -335,6 +353,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)
|
||||
@ -348,6 +367,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)
|
||||
@ -396,6 +417,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)
|
||||
@ -433,6 +458,7 @@ DEPENDENCIES
|
||||
docker-api
|
||||
eventmachine (= 1.0.9.1)
|
||||
factory_bot_rails
|
||||
faraday
|
||||
faye-websocket
|
||||
forgery
|
||||
headless
|
||||
@ -449,7 +475,9 @@ DEPENDENCIES
|
||||
nyan-cat-formatter
|
||||
pagedown-bootstrap-rails
|
||||
pg
|
||||
proforma!
|
||||
pry-byebug
|
||||
pry-rails
|
||||
puma
|
||||
pundit
|
||||
rack-mini-profiler
|
||||
@ -460,6 +488,7 @@ DEPENDENCIES
|
||||
ransack
|
||||
rest-client
|
||||
rspec-autotest
|
||||
rspec-collection_matchers
|
||||
rspec-rails
|
||||
rubocop
|
||||
rubocop-rspec
|
||||
@ -467,6 +496,7 @@ DEPENDENCIES
|
||||
rubyzip
|
||||
sass-rails
|
||||
selenium-webdriver
|
||||
shoulda-matchers
|
||||
simplecov
|
||||
slim-rails
|
||||
sorcery
|
||||
@ -475,6 +505,7 @@ DEPENDENCIES
|
||||
turbolinks
|
||||
uglifier
|
||||
web-console
|
||||
webmock
|
||||
webpacker
|
||||
whenever
|
||||
|
||||
|
37
app/assets/javascripts/codeharbor_link.js
Normal file
37
app/assets/javascripts/codeharbor_link.js
Normal file
@ -0,0 +1,37 @@
|
||||
$(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
|
||||
return replace('xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx');
|
||||
});
|
||||
|
||||
var generateRandomHex32 = (function () {
|
||||
return replace(Array(32).join('x'));
|
||||
});
|
||||
|
||||
$('.generate-api_key').on('click', function () {
|
||||
$('.api_key').val(generateRandomHex32())
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -209,6 +209,105 @@ $(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 observeExportButtons = function(){
|
||||
$('.export-start').on('click', function(e){
|
||||
e.preventDefault();
|
||||
$('#export-modal').modal({
|
||||
height: 250
|
||||
});
|
||||
$('#export-modal').modal('show');
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
var exportExerciseStart = function(exerciseID) {
|
||||
var $exerciseDiv = $('#export-exercise');
|
||||
var $messageDiv = $exerciseDiv.children('.export-message');
|
||||
var $actionsDiv = $exerciseDiv.children('.export-exercise-actions');
|
||||
|
||||
$messageDiv.removeClass('export-failure');
|
||||
|
||||
$messageDiv.html('<%= I18n.t('exercises.export_codeharbor.checking_codeharbor') %>');
|
||||
$actionsDiv.html('<div class="spinner-border"></div>');
|
||||
|
||||
return $.ajax({
|
||||
type: 'POST',
|
||||
url: '/exercises/' + exerciseID + '/export_external_check',
|
||||
dataType: 'json',
|
||||
success: function(response) {
|
||||
$messageDiv.html(response.message);
|
||||
return $actionsDiv.html(response.actions);
|
||||
},
|
||||
error: function(a, b, c) {
|
||||
return alert('error:' + c);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var exportExerciseConfirm = function(exerciseID) {
|
||||
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',
|
||||
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) {
|
||||
if (event.which === TAB_KEY_CODE) {
|
||||
@ -264,6 +363,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();
|
||||
observeExportButtons();
|
||||
} else if ($('.edit_exercise, .new_exercise').isPresent()) {
|
||||
execution_environments = $('form').data('execution-environments');
|
||||
file_types = $('form').data('file-types');
|
||||
@ -271,6 +371,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');
|
||||
|
@ -176,3 +176,51 @@ a.file-heading {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#export-modal {
|
||||
.modal-content {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
#export-exercise{
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.export-message {
|
||||
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 {
|
||||
max-width: 110px;
|
||||
min-width: 110px;
|
||||
}
|
||||
|
||||
.export-button {
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.export-success {
|
||||
color: darkgreen;
|
||||
font-size: 12pt;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.export-failure {
|
||||
color: darkred;
|
||||
}
|
||||
|
@ -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
|
48
app/controllers/codeharbor_links_controller.rb
Normal file
48
app/controllers/codeharbor_links_controller.rb
Normal file
@ -0,0 +1,48 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CodeharborLinksController < ApplicationController
|
||||
include CommonBehavior
|
||||
before_action :set_codeharbor_link, only: %i[show edit update destroy]
|
||||
|
||||
def 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
|
||||
|
||||
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, path: -> { @codeharbor_link.user })
|
||||
end
|
||||
|
||||
def update
|
||||
authorize!
|
||||
update_and_respond(object: @codeharbor_link, params: codeharbor_link_params, path: @codeharbor_link.user)
|
||||
end
|
||||
|
||||
def destroy
|
||||
destroy_and_respond(object: @codeharbor_link, path: @codeharbor_link.user)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authorize!
|
||||
authorize @codeharbor_link
|
||||
end
|
||||
|
||||
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, :check_uuid_url, :api_key)
|
||||
end
|
||||
end
|
@ -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 + [:clone, :implement, :working_times, :intervention, :search, :run, :statistics, :submit, :reload, :feedback, :requests_for_comments, :study_group_dashboard]
|
||||
before_action :set_exercise_and_authorize, only: MEMBER_ACTIONS + [:clone, :implement, :working_times, :intervention, :search, :run, :statistics, :submit, :reload, :feedback, :requests_for_comments, :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]
|
||||
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_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
|
||||
@ -122,60 +121,96 @@ class ExercisesController < ApplicationController
|
||||
render 'request_for_comments/index'
|
||||
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
|
||||
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],
|
||||
update_right: codeharbor_check[:update_right],
|
||||
error: codeharbor_check[:error],
|
||||
exported: false
|
||||
}
|
||||
)
|
||||
}, status: 200
|
||||
end
|
||||
|
||||
def export_external_confirm
|
||||
@exercise.uuid = SecureRandom.uuid if @exercise.uuid.nil?
|
||||
|
||||
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.save
|
||||
else
|
||||
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})
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def user_for_oauth2_request
|
||||
authorizationHeader = request.headers['Authorization']
|
||||
if authorizationHeader == nil
|
||||
raise ({status: 401, message: 'No Authorization header'})
|
||||
end
|
||||
def import_uuid_check
|
||||
user = user_from_api_key
|
||||
return render json: {}, status: 401 if user.nil?
|
||||
|
||||
oauth2Token = authorizationHeader.split(' ')[1]
|
||||
if oauth2Token == nil || oauth2Token.size == 0
|
||||
raise ({status: 401, message: 'No token in Authorization header'})
|
||||
end
|
||||
uuid = params[:uuid]
|
||||
exercise = Exercise.find_by(uuid: uuid)
|
||||
|
||||
user = user_by_code_harbor_token(oauth2Token)
|
||||
if user == nil
|
||||
raise ({status: 401, message: 'Unknown OAuth2 token'})
|
||||
end
|
||||
return render json: {exercise_found: false} if exercise.nil?
|
||||
return render json: {exercise_found: true, update_right: false} unless ExercisePolicy.new(user, exercise).update?
|
||||
|
||||
return user
|
||||
render json: {exercise_found: true, update_right: true}
|
||||
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
|
||||
def import_exercise
|
||||
tempfile = Tempfile.new('codeharbor_import.zip')
|
||||
tempfile.write request.body.read.force_encoding('UTF-8')
|
||||
tempfile.rewind
|
||||
|
||||
user = user_from_api_key
|
||||
return render json: {}, status: 401 if user.nil?
|
||||
|
||||
exercise = nil
|
||||
ActiveRecord::Base.transaction do
|
||||
exercise = ::ProformaService::Import.call(zip: tempfile, user: user)
|
||||
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
|
||||
render json: t('exercises.import_codeharbor.import_errors.internal_error'), status: 500
|
||||
end
|
||||
private :user_by_code_harbor_token
|
||||
|
||||
def user_from_api_key
|
||||
authorization_header = request.headers['Authorization']
|
||||
api_key = authorization_header&.split(' ')&.second
|
||||
user_by_codeharbor_token(api_key)
|
||||
end
|
||||
private :user_from_api_key
|
||||
|
||||
def user_by_codeharbor_token(api_key)
|
||||
link = CodeharborLink.find_by_api_key(api_key)
|
||||
link&.user
|
||||
end
|
||||
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
|
||||
|
||||
@ -197,6 +232,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
|
||||
|
5
app/errors/proforma/exercise_not_owned.rb
Normal file
5
app/errors/proforma/exercise_not_owned.rb
Normal file
@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Proforma
|
||||
class ExerciseNotOwned < StandardError; end
|
||||
end
|
@ -1,2 +0,0 @@
|
||||
module CodeHarborLinksHelper
|
||||
end
|
@ -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
|
13
app/models/codeharbor_link.rb
Normal file
13
app/models/codeharbor_link.rb
Normal file
@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CodeharborLink < ApplicationRecord
|
||||
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
|
||||
id.to_s
|
||||
end
|
||||
end
|
@ -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
|
||||
@ -31,17 +31,18 @@ class Exercise < ApplicationRecord
|
||||
|
||||
validate :valid_main_file?
|
||||
validates :description, presence: true
|
||||
validates :execution_environment_id, presence: true
|
||||
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, if: -> { uuid.present? }
|
||||
|
||||
@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)
|
||||
@ -49,7 +50,7 @@ class Exercise < ApplicationRecord
|
||||
0
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def finishers_percentage
|
||||
if users.distinct.count != 0
|
||||
(100.0 / users.distinct.count * finishers.count).round(2)
|
||||
|
@ -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, dependent: :destroy
|
||||
accepts_nested_attributes_for :user_proxy_exercise_exercises
|
||||
|
||||
|
||||
|
@ -1,3 +0,0 @@
|
||||
class CodeHarborLinkPolicy < AdminOnlyPolicy
|
||||
|
||||
end
|
35
app/policies/codeharbor_link_policy.rb
Normal file
35
app/policies/codeharbor_link_policy.rb
Normal file
@ -0,0 +1,35 @@
|
||||
class CodeharborLinkPolicy < ApplicationPolicy
|
||||
def index?
|
||||
false
|
||||
end
|
||||
|
||||
def show?
|
||||
false
|
||||
end
|
||||
|
||||
def new?
|
||||
teacher? || admin?
|
||||
end
|
||||
|
||||
def create?
|
||||
teacher? || admin?
|
||||
end
|
||||
|
||||
def edit?
|
||||
owner?
|
||||
end
|
||||
|
||||
def update?
|
||||
owner?
|
||||
end
|
||||
|
||||
def destroy?
|
||||
owner?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def owner?
|
||||
@record.reload.user == @user
|
||||
end
|
||||
end
|
@ -7,7 +7,7 @@ class ExercisePolicy < AdminOrAuthorPolicy
|
||||
define_method(action) { admin? || teacher? }
|
||||
end
|
||||
|
||||
[:clone?, :destroy?, :edit?, :update?].each do |action|
|
||||
[:clone?, :destroy?, :edit?, :update?, :export_external_check?, :export_external_confirm?].each do |action|
|
||||
define_method(action) { admin? || teacher_in_study_group || author? }
|
||||
end
|
||||
|
||||
|
42
app/services/exercise_service/check_external.rb
Normal file
42
app/services/exercise_service/check_external.rb
Normal file
@ -0,0 +1,42 @@
|
||||
# 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).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
|
||||
faraday.options[:timeout] = 5
|
||||
|
||||
faraday.adapter Faraday.default_adapter
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
34
app/services/exercise_service/push_external.rb
Normal file
34
app/services/exercise_service/push_external.rb
Normal file
@ -0,0 +1,34 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ExerciseService
|
||||
class PushExternal < ServiceBase
|
||||
def initialize(zip:, codeharbor_link:)
|
||||
@zip = zip
|
||||
@codeharbor_link = codeharbor_link
|
||||
end
|
||||
|
||||
def execute
|
||||
body = @zip.string
|
||||
begin
|
||||
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
|
||||
request.body = body
|
||||
end
|
||||
|
||||
response.success? ? nil : response.body
|
||||
rescue StandardError => e
|
||||
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
|
117
app/services/proforma_service/convert_exercise_to_task.rb
Normal file
117
app/services/proforma_service/convert_exercise_to_task.rb
Normal file
@ -0,0 +1,117 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'mimemagic'
|
||||
|
||||
module ProformaService
|
||||
class ConvertExerciseToTask < ServiceBase
|
||||
DEFAULT_LANGUAGE = 'de'
|
||||
|
||||
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,
|
||||
files: task_files,
|
||||
tests: tests,
|
||||
uuid: uuid,
|
||||
language: DEFAULT_LANGUAGE,
|
||||
model_solutions: model_solutions
|
||||
}.compact
|
||||
)
|
||||
end
|
||||
|
||||
def uuid
|
||||
@exercise.update(uuid: SecureRandom.uuid) if @exercise.uuid.nil?
|
||||
@exercise.uuid
|
||||
end
|
||||
|
||||
def model_solutions
|
||||
@exercise.files.filter { |file| file.role == 'reference_implementation' }.map do |file|
|
||||
Proforma::ModelSolution.new(
|
||||
id: "ms-#{file.id}",
|
||||
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.files.filter { |file| file.role == 'teacher_defined_test' }.map do |file|
|
||||
Proforma::Test.new(
|
||||
id: file.id,
|
||||
title: file.name,
|
||||
files: test_file(file),
|
||||
meta_data: {
|
||||
'feedback-message' => file.feedback_message
|
||||
}.compact
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def test_file(file)
|
||||
[
|
||||
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
|
||||
@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)
|
||||
task_file = Proforma::TaskFile.new(
|
||||
id: file.id,
|
||||
filename: filename(file),
|
||||
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 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')
|
||||
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
|
67
app/services/proforma_service/convert_task_to_exercise.rb
Normal file
67
app/services/proforma_service/convert_task_to_exercise.rb
Normal file
@ -0,0 +1,67 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ProformaService
|
||||
class ConvertTaskToExercise < ServiceBase
|
||||
def initialize(task:, user:, exercise: nil)
|
||||
@task = task
|
||||
@user = user
|
||||
@exercise = exercise || Exercise.new(unpublished: true)
|
||||
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,
|
||||
files: files
|
||||
)
|
||||
end
|
||||
|
||||
def files
|
||||
test_files + task_files.values
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
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 = 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',
|
||||
role: file.internal_description,
|
||||
path: File.dirname(file.filename).in?(['.', '']) ? nil : File.dirname(file.filename)
|
||||
)
|
||||
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
|
15
app/services/proforma_service/export_task.rb
Normal file
15
app/services/proforma_service/export_task.rb
Normal file
@ -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
|
73
app/services/proforma_service/import.rb
Normal file
73
app/services/proforma_service/import.rb
Normal file
@ -0,0 +1,73 @@
|
||||
# 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 = base_exercise
|
||||
exercise_files = exercise&.files&.to_a
|
||||
|
||||
exercise = ConvertTaskToExercise.call(task: @task, user: @user, exercise: exercise)
|
||||
exercise_files&.each(&:destroy) # feels suboptimal
|
||||
|
||||
exercise
|
||||
else
|
||||
import_multi
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def base_exercise
|
||||
exercise = Exercise.find_by(uuid: @task.uuid)
|
||||
if exercise
|
||||
raise Proforma::ExerciseNotOwned unless ExercisePolicy.new(@user, exercise).update?
|
||||
|
||||
exercise
|
||||
else
|
||||
Exercise.new(uuid: @task.uuid, unpublished: true)
|
||||
end
|
||||
end
|
||||
|
||||
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
|
7
app/services/service_base.rb
Normal file
7
app/services/service_base.rb
Normal file
@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ServiceBase
|
||||
def self.call(*args)
|
||||
new(*args).execute
|
||||
end
|
||||
end
|
@ -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)
|
||||
|
@ -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)
|
@ -1,3 +0,0 @@
|
||||
h1 = @code_harbor_link
|
||||
|
||||
= render('form')
|
@ -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)
|
@ -1,3 +0,0 @@
|
||||
h1 = t('shared.new_model', model: CodeHarborLink.model_name.human)
|
||||
|
||||
= render('form')
|
@ -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))
|
19
app/views/codeharbor_links/_form.html.slim
Normal file
19
app/views/codeharbor_links/_form.html.slim
Normal file
@ -0,0 +1,19 @@
|
||||
= 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(: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(: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
|
||||
= 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')
|
||||
|
3
app/views/codeharbor_links/edit.html.slim
Normal file
3
app/views/codeharbor_links/edit.html.slim
Normal file
@ -0,0 +1,3 @@
|
||||
h1 = CodeharborLink.model_name.human
|
||||
|
||||
= render('form')
|
3
app/views/codeharbor_links/new.html.slim
Normal file
3
app/views/codeharbor_links/new.html.slim
Normal file
@ -0,0 +1,3 @@
|
||||
h1 = t('shared.new_model', model: CodeharborLink.model_name.human)
|
||||
|
||||
= render('form')
|
14
app/views/exercises/_export_actions.html.slim
Normal file
14
app/views/exercises/_export_actions.html.slim
Normal file
@ -0,0 +1,14 @@
|
||||
- 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
|
||||
= t('exercises.export_codeharbor.buttons.retry')
|
||||
- else
|
||||
- unless exported
|
||||
- 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')
|
||||
|
||||
= button_tag type: 'submit', class:'btn btn-secondary pull-right export-button', data: {dismiss: 'modal'} do
|
||||
i.fa.fa-remove.abort-icon
|
||||
= exported ? t('exercises.export_codeharbor.buttons.close') : t('exercises.export_codeharbor.buttons.abort')
|
3
app/views/exercises/_export_dialogcontent.html.slim
Normal file
3
app/views/exercises/_export_dialogcontent.html.slim
Normal file
@ -0,0 +1,3 @@
|
||||
#export-exercise
|
||||
.export-message
|
||||
.export-exercise-actions
|
@ -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: t('exercises.form.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)
|
||||
.actions = render('shared/submit_button', f: f, object: @exercise)
|
||||
|
@ -47,6 +47,9 @@ h1 = Exercise.model_name.human(count: 2)
|
||||
li = link_to(t('activerecord.models.request_for_comment.other'), requests_for_comments_exercise_path(exercise), class: 'dropdown-item') if policy(exercise).requests_for_comments?
|
||||
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).export_external_confirm?
|
||||
|
||||
= 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')
|
||||
|
@ -12,17 +12,19 @@ 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?)
|
||||
= 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?)
|
||||
= 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))
|
||||
= 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')
|
||||
|
||||
|
@ -7,3 +7,5 @@ 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?)
|
||||
|
||||
= 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'))
|
||||
|
@ -2,4 +2,6 @@ test:
|
||||
flowr:
|
||||
enabled: false
|
||||
code_pilot:
|
||||
enabled: false
|
||||
enabled: false
|
||||
codeharbor:
|
||||
url: http://test.url
|
||||
|
@ -1,7 +1,7 @@
|
||||
de:
|
||||
activerecord:
|
||||
attributes:
|
||||
code_harbor_link:
|
||||
codeharbor_link:
|
||||
oauth2token: OAuth2 Token
|
||||
consumer:
|
||||
name: Name
|
||||
@ -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,26 @@ de:
|
||||
request_for_comments_sent: "Kommentaranfrage gesendet."
|
||||
editor_file_tree:
|
||||
file_root: Dateien
|
||||
import_codeharbor:
|
||||
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.<br>ID: %{id}<br>Title: %{title}'
|
||||
export_failed: 'Export ist fehlgeschlagen.<br>ID: %{id}<br>Title: %{title}<br><br>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
|
||||
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.
|
||||
@ -306,6 +338,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 +390,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
|
||||
|
@ -1,7 +1,7 @@
|
||||
en:
|
||||
activerecord:
|
||||
attributes:
|
||||
code_harbor_link:
|
||||
codeharbor_link:
|
||||
oauth2token: OAuth2 Token
|
||||
consumer:
|
||||
name: Name
|
||||
@ -44,6 +44,8 @@ en:
|
||||
allow_file_creation: "Allow file creation"
|
||||
difficulty: Difficulty
|
||||
token: "Exercise Token"
|
||||
uuid: UUID
|
||||
unpublished: Unpublished
|
||||
proxy_exercise:
|
||||
title: Title
|
||||
files_count: Exercises Count
|
||||
@ -240,6 +242,16 @@ en:
|
||||
consumers:
|
||||
show:
|
||||
link: Consumer
|
||||
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.
|
||||
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
|
||||
delete: Remove Codeharbor link
|
||||
execution_environments:
|
||||
form:
|
||||
hints:
|
||||
@ -298,6 +310,26 @@ en:
|
||||
request_for_comments_sent: "Request for comments sent."
|
||||
editor_file_tree:
|
||||
file_root: Files
|
||||
import_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
|
||||
successfully_exported: 'Exercise has been successfully exported.<br>ID: %{id}<br>Title: %{title}'
|
||||
export_failed: 'Export has failed.<br>ID: %{id}<br>Title: %{title}<br><br>Error: %{error}'
|
||||
error: 'An error occurred while contacting Codeharbor'
|
||||
checking_codeharbor: Checking if the exercise exists on Codeharbor.
|
||||
buttons:
|
||||
retry: Retry
|
||||
export: Export
|
||||
close: Close
|
||||
abort: Abort
|
||||
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.
|
||||
@ -306,6 +338,9 @@ 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.
|
||||
none: None
|
||||
implement:
|
||||
alert:
|
||||
text: 'Your browser does not support features required for using %{application_name}. Please access %{application_name} using a modern browser.'
|
||||
@ -355,6 +390,8 @@ en:
|
||||
feedback: Feedback
|
||||
requests_for_comments: Requests for Comments
|
||||
study_group_dashboard: Live Dashboard
|
||||
show:
|
||||
is_unpublished: Exercise is unpublished
|
||||
statistics:
|
||||
average_score: Average Score
|
||||
final_submissions: Final Submissions
|
||||
|
@ -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, only: %i[new create edit update destroy]
|
||||
resources :request_for_comments do
|
||||
member do
|
||||
get :mark_as_solved, defaults: { format: :json }
|
||||
@ -66,7 +66,8 @@ 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
|
||||
collection do
|
||||
@ -85,6 +86,8 @@ Rails.application.routes.draw do
|
||||
get :reload
|
||||
post :submit
|
||||
get 'study_group_dashboard/:study_group_id', to: 'exercises#study_group_dashboard'
|
||||
post :export_external_check
|
||||
post :export_external_confirm
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -0,0 +1,5 @@
|
||||
class RenameCodeHarborLinksToCodeharborLinks < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
rename_table :code_harbor_links, :codeharbor_links
|
||||
end
|
||||
end
|
@ -0,0 +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
|
5
db/migrate/20190830142809_add_uuid_to_exercise.rb
Normal file
5
db/migrate/20190830142809_add_uuid_to_exercise.rb
Normal file
@ -0,0 +1,5 @@
|
||||
class AddUuidToExercise < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_column :exercises, :uuid, :uuid
|
||||
end
|
||||
end
|
5
db/migrate/20191008163045_add_unpublished_to_exercise.rb
Normal file
5
db/migrate/20191008163045_add_unpublished_to_exercise.rb
Normal file
@ -0,0 +1,5 @@
|
||||
class AddUnpublishedToExercise < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_column :exercises, :unpublished, :boolean, default: false
|
||||
end
|
||||
end
|
12
db/schema.rb
12
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_10_08_163045) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
@ -28,12 +28,14 @@ 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|
|
||||
t.string "oauth2token", limit: 255
|
||||
create_table "codeharbor_links", force: :cascade do |t|
|
||||
t.string "api_key", 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 "check_uuid_url"
|
||||
t.index ["user_id"], name: "index_codeharbor_links_on_user_id"
|
||||
end
|
||||
|
||||
create_table "comments", force: :cascade do |t|
|
||||
@ -150,6 +152,8 @@ ActiveRecord::Schema.define(version: 2019_02_13_131802) do
|
||||
t.boolean "allow_file_creation"
|
||||
t.boolean "allow_auto_completion", default: false
|
||||
t.integer "expected_difficulty", default: 1
|
||||
t.uuid "uuid"
|
||||
t.boolean "unpublished", default: false
|
||||
t.index ["id"], name: "index_exercises_on_id"
|
||||
end
|
||||
|
||||
|
11
lib/file_io.rb
Normal file
11
lib/file_io.rb
Normal file
@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# stolen 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
|
27
lib/tasks/export_public_exercises.rake
Normal file
27
lib/tasks/export_public_exercises.rake
Normal file
@ -0,0 +1,27 @@
|
||||
# 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)
|
||||
successful_exports = []
|
||||
failed_exports = []
|
||||
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?
|
||||
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
|
93
spec/controllers/codeharbor_links_controller_spec.rb
Normal file
93
spec/controllers/codeharbor_links_controller_spec.rb
Normal file
@ -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
|
||||
|
@ -313,4 +313,206 @@ 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('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'))
|
||||
)
|
||||
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: {id: exercise.id, codeharbor_link: codeharbor_link.id} }
|
||||
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
|
||||
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
|
||||
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 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
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #import_exercise' do
|
||||
let(:codeharbor_link) { FactoryBot.create(:codeharbor_link, user: user) }
|
||||
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}"} }
|
||||
|
||||
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 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) }
|
||||
|
||||
it 'responds with correct status code' do
|
||||
post_request
|
||||
expect(response).to have_http_status(:internal_server_error)
|
||||
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
|
||||
|
@ -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
|
||||
|
8
spec/factories/codeharbor_link.rb
Normal file
8
spec/factories/codeharbor_link.rb
Normal file
@ -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
|
16
spec/models/codeharbor_link_spec.rb
Normal file
16
spec/models/codeharbor_link_spec.rb
Normal file
@ -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
|
@ -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) }
|
||||
|
||||
|
45
spec/policies/codeharbor_link_policy_spec.rb
Normal file
45
spec/policies/codeharbor_link_policy_spec.rb
Normal file
@ -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
|
||||
%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
|
||||
expect(policy).not_to permit(FactoryBot.create(:external_user), codeharbor_link)
|
||||
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
|
@ -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)
|
||||
|
@ -39,3 +39,12 @@ 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
|
||||
|
||||
WebMock.allow_net_connect!
|
||||
|
84
spec/services/exercise_service/check_external_spec.rb
Normal file
84
spec/services/exercise_service/check_external_spec.rb
Normal file
@ -0,0 +1,84 @@
|
||||
# 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) { {exercise_found: true, update_right: true}.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'), exercise_found: true, update_right: true)
|
||||
end
|
||||
|
||||
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: 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
|
||||
|
||||
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
|
64
spec/services/exercise_service/push_external_spec.rb
Normal file
64
spec/services/exercise_service/push_external_spec.rb
Normal file
@ -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
|
197
spec/services/proforma_service/convert_exercise_to_task_spec.rb
Normal file
197
spec/services/proforma_service/convert_exercise_to_task_spec.rb
Normal file
@ -0,0 +1,197 @@
|
||||
# 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: []
|
||||
)
|
||||
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
|
393
spec/services/proforma_service/convert_task_to_exercise_spec.rb
Normal file
393
spec/services/proforma_service/convert_task_to_exercise_spec.rb
Normal file
@ -0,0 +1,393 @@
|
||||
# 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
|
||||
)
|
||||
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: "#{path}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' }
|
||||
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(
|
||||
content: 'content',
|
||||
name: 'filename',
|
||||
role: 'regular_file',
|
||||
hidden: false,
|
||||
read_only: true,
|
||||
file_type: be_a(FileType).and(have_attributes(file_extension: '.txt')),
|
||||
path: nil
|
||||
)
|
||||
end
|
||||
|
||||
it 'creates a new Exercise on save' 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 }
|
||||
|
||||
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
|
41
spec/services/proforma_service/export_task_spec.rb
Normal file
41
spec/services/proforma_service/export_task_spec.rb
Normal file
@ -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
|
164
spec/services/proforma_service/import_spec.rb
Normal file
164
spec/services/proforma_service/import_spec.rb
Normal file
@ -0,0 +1,164 @@
|
||||
# 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 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
|
||||
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 }
|
||||
let(:imported_exercise) { import_service }
|
||||
|
||||
it 'updates the old Exercise' do
|
||||
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 'raises a proforma error' do
|
||||
expect { imported_exercise.save! } .to raise_error Proforma::ExerciseNotOwned
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -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
|
||||
|
42
spec/support/expectations/equal_exercise.rb
Normal file
42
spec/support/expectations/equal_exercise.rb
Normal file
@ -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')
|
||||
end
|
||||
end
|
9
spec/support/expectations/has_content.rb
Normal file
9
spec/support/expectations/has_content.rb
Normal file
@ -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
|
Reference in New Issue
Block a user