Merge pull request #423 from openHPI/implement_codeharbor_interface
Implement codeharbor interface
This commit is contained in:
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'))
|
||||
|
Reference in New Issue
Block a user