Merge pull request #423 from openHPI/implement_codeharbor_interface

Implement codeharbor interface
This commit is contained in:
MrSerth
2019-12-20 10:53:42 +01:00
committed by GitHub
68 changed files with 2393 additions and 195 deletions

View 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())
});
}
}
});

View File

@@ -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');

View File

@@ -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;
}

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -0,0 +1,5 @@
# frozen_string_literal: true
module Proforma
class ExerciseNotOwned < StandardError; end
end

View File

@@ -1,2 +0,0 @@
module CodeHarborLinksHelper
end

View File

@@ -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

View 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

View File

@@ -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)

View File

@@ -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

View File

@@ -1,3 +0,0 @@
class CodeHarborLinkPolicy < AdminOnlyPolicy
end

View 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

View File

@@ -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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1,7 @@
# frozen_string_literal: true
class ServiceBase
def self.call(*args)
new(*args).execute
end
end

View File

@@ -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)

View File

@@ -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)

View File

@@ -1,3 +0,0 @@
h1 = @code_harbor_link
= render('form')

View File

@@ -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)

View File

@@ -1,3 +0,0 @@
h1 = t('shared.new_model', model: CodeHarborLink.model_name.human)
= render('form')

View File

@@ -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))

View 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')

View File

@@ -0,0 +1,3 @@
h1 = CodeharborLink.model_name.human
= render('form')

View File

@@ -0,0 +1,3 @@
h1 = t('shared.new_model', model: CodeharborLink.model_name.human)
= render('form')

View 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')

View File

@@ -0,0 +1,3 @@
#export-exercise
.export-message
.export-exercise-actions

View File

@@ -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)

View File

@@ -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')

View File

@@ -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')

View File

@@ -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'))