Skip to content

add direct import of proforma zip #2867

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 81 additions & 11 deletions app/assets/javascripts/exercises.js
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,6 @@ $(document).on('turbolinks:load', function () {
var observeExportButtons = function () {
$('.export-start').on('click', function (e) {
e.preventDefault();
new bootstrap.Modal($('#export-modal')).show();
exportExerciseStart($(this).data().exerciseId);
});
body_selector.on('click', '.export-retry-button', function () {
Expand All @@ -356,11 +355,11 @@ $(document).on('turbolinks:load', function () {
}

var exportExerciseStart = function (exerciseID) {
const $exerciseDiv = $('#export-exercise');
const $messageDiv = $exerciseDiv.children('.export-message');
const $actionsDiv = $exerciseDiv.children('.export-exercise-actions');
const $exerciseDiv = $('#exercise-transfer');
const $messageDiv = $exerciseDiv.children('.transfer-message');
const $actionsDiv = $exerciseDiv.children('.transfer-exercise-actions');

$messageDiv.removeClass('export-failure');
$messageDiv.removeClass('transfer-failure');

$messageDiv.html(I18n.t('exercises.export_codeharbor.checking_codeharbor'));
$actionsDiv.html('<div class="spinner-border"></div>');
Expand All @@ -380,9 +379,9 @@ $(document).on('turbolinks:load', function () {
};

var exportExerciseConfirm = function (exerciseID) {
const $exerciseDiv = $('#export-exercise');
const $messageDiv = $exerciseDiv.children('.export-message');
const $actionsDiv = $exerciseDiv.children('.export-exercise-actions');
const $exerciseDiv = $('#exercise-transfer');
const $messageDiv = $exerciseDiv.children('.transfer-message');
const $actionsDiv = $exerciseDiv.children('.transfer-exercise-actions');

return $.ajax({
type: 'POST',
Expand All @@ -395,11 +394,11 @@ $(document).on('turbolinks:load', function () {
if (response.status === 'success') {
$messageDiv.addClass('export-success');
setTimeout((function () {
bootstrap.Modal.getInstance($('#export-modal'))?.hide();
bootstrap.Modal.getInstance($('#transfer-modal'))?.hide();
$messageDiv.html('').removeClass('export-success');
}), 3000);
} else {
$messageDiv.addClass('export-failure');
$messageDiv.addClass('transfer-failure');
}
},
error: function (a, b, c) {
Expand All @@ -408,6 +407,76 @@ $(document).on('turbolinks:load', function () {
});
};

var observeImportButtons = function () {
const $exerciseDiv = $('#exercise-transfer');
const $messageDiv = $exerciseDiv.children('.transfer-message');
const $actionsDiv = $exerciseDiv.children('.transfer-exercise-actions');

$('.import-start').on('click', function (e) {
e.preventDefault();
importExerciseStart();
});
body_selector.on('change', '#proforma-file', async function () {
const file = event.target.files[0];
const formData = new FormData();
formData.append('file', file);

return $.ajax({
type: 'POST',
url: Routes.import_start_exercises_path(),
data: formData,
processData: false,
contentType: false,

success: function (response) {
if(response.status === 'failure')
$messageDiv.addClass('transfer-failure');
else
$messageDiv.removeClass('transfer-failure');
$messageDiv.html(response.message);
return $actionsDiv.html(response.actions);
},
error: function (a, b, c) {
return alert(`error: ${c}`);
}
});
});
body_selector.on('click', '.import-action', async function () {
let fileId = $(this).attr('data-file-id')
let importType = $(this).attr('data-import-type')
importExerciseConfirm(fileId, importType)
});
}
var importExerciseStart = function () {
const $exerciseDiv = $('#exercise-transfer');
const $messageDiv = $exerciseDiv.children('.transfer-message');
const $actionsDiv = $exerciseDiv.children('.transfer-exercise-actions');

$messageDiv.removeClass('transfer-failure');
$messageDiv.html(I18n.t('exercises.import_proforma.dialog.start'));
$actionsDiv.html(`<label for="proforma-file" class="btn btn-primary">${I18n.t('exercises.import_start.upload_file')}</label><input type="file" id="proforma-file" name="proforma-file" accept=".zip,application/zip" >`);
}

var importExerciseConfirm = function (fileId, importType) {
const $exerciseDiv = $('#exercise-transfer');
const $messageDiv = $exerciseDiv.children('.transfer-message');
const $actionsDiv = $exerciseDiv.children('.transfer-exercise-actions');

$.ajax({
type: 'POST',
url: Routes.import_confirm_exercises_path(),
data: {file_id: fileId, import_type: importType},
dataType: 'json',

success: function (response) {
$messageDiv.html(response.message);
return $actionsDiv.html(response.actions);
},
error: function (a, b, c) {
return alert(`error: ${c}`);
}
});
}
var overrideTextareaTabBehavior = function () {
$('.mb-3 textarea[name$="[content]"]').on('keydown', function (event) {
if (event.which === TAB_KEY_CODE) {
Expand Down Expand Up @@ -463,6 +532,7 @@ $(document).on('turbolinks:load', function () {
if ($('table:not(#tags-table)').isPresent()) {
enableBatchUpdate();
observeExportButtons();
observeImportButtons();
} else if ($('.edit_exercise, .new_exercise').isPresent()) {
const form_selector = $('form');
execution_environments = form_selector.data('execution-environments');
Expand Down Expand Up @@ -499,4 +569,4 @@ $(document).on('turbolinks:load', function () {
}


});
});
18 changes: 9 additions & 9 deletions app/assets/stylesheets/exercises.css.scss
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ a.file-heading {
}
}

#export-modal {
#transfer-modal {
.modal-content {
min-height: 300px;
}
Expand All @@ -189,27 +189,27 @@ a.file-heading {
}
}

#export-exercise{
#exercise-transfer{
display: flex;
}

.export-message {
.transfer-message {
flex-grow: 1;
font-size: 12px;
padding-right: 5px;
word-wrap: break-word;
}
.export-message + :empty {
.transfer-message + :empty {
max-width: 100%;
}

.export-exercise-actions:empty {
.transfer-exercise-actions:empty {
display: none;
}

.export-exercise-actions {
max-width: 110px;
min-width: 110px;
.transfer-exercise-actions {
max-width: 140px;
min-width: 140px;
}

.export-button {
Expand All @@ -223,6 +223,6 @@ a.file-heading {
font-weight: 600;
}

.export-failure {
.transfer-failure {
color: var(--bs-danger);
}
80 changes: 72 additions & 8 deletions app/controllers/exercises_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class ExercisesController < ApplicationController

skip_before_action :verify_authenticity_token, only: %i[import_task import_uuid_check]
skip_before_action :require_fully_authenticated_user!, only: %i[import_task import_uuid_check]
skip_after_action :verify_authorized, only: %i[import_task import_uuid_check]
skip_after_action :verify_authorized, only: %i[import_task import_uuid_check import_start import_confirm]
skip_after_action :verify_policy_scoped, only: %i[import_task import_uuid_check], raise: false

rescue_from Pundit::NotAuthorizedError, with: :not_authorized_for_exercise
Expand Down Expand Up @@ -150,13 +150,66 @@ def import_uuid_check
user = user_from_api_key
return render json: {}, status: :unauthorized if user.nil?

uuid = params[:uuid]
exercise = Exercise.find_by(uuid:)
render json: uuid_check(user:, uuid: params[:uuid])
end

def import_start
zip_file = params[:file]
unless zip_file.is_a?(ActionDispatch::Http::UploadedFile)
return render json: {status: 'failure', message: t('.choose_file_error')}
end

return render json: {uuid_found: false} if exercise.nil?
return render json: {uuid_found: true, update_right: false} unless ExercisePolicy.new(user, exercise).update?
uuid = ProformaService::UuidFromZip.call(zip: zip_file)
exists, updatable = uuid_check(user: current_user, uuid:).values_at(:uuid_found, :update_right)

render json: {uuid_found: true, update_right: true}
uploader = ProformaZipUploader.new
uploader.cache!(params[:file])

message = if exists && updatable
t('.exercise_exists_and_is_updatable')
elsif exists
t('.exercise_exists_and_is_not_updatable')
else
t('.exercise_is_importable')
end

render json: {
status: 'success',
message:,
actions: render_to_string(partial: 'import_actions',
locals: {exercise: @exercise, imported: false, exists:, updatable:, file_id: uploader.cache_name}),
}
rescue ProformaXML::InvalidZip => e
render json: {
status: 'failure',
message: t('.error', message: e.message),
}
end

def import_confirm
uploader = ProformaZipUploader.new
uploader.retrieve_from_cache!(params[:file_id])
exercise = ::ProformaService::Import.call(zip: uploader.file, user: current_user)
exercise.save!

render json: {
status: 'success',
message: t('.success'),
actions: render_to_string(partial: 'import_actions', locals: {exercise:, imported: true}),
}
rescue ProformaXML::ProformaError, ActiveRecord::RecordInvalid => e
render json: {
status: 'failure',
message: t('.error', error: e.message),
actions: '',
}
rescue StandardError => e
Sentry.capture_exception(e)
render json: {
status: 'failure',
message: t('exercises.import_proforma.import_errors.internal_error'),
actions: '',
}
end

def import_task
Expand All @@ -175,10 +228,10 @@ def import_task
rescue ProformaXML::ExerciseNotOwned
render json: {}, status: :unauthorized
rescue ProformaXML::ProformaError
render json: t('exercises.import_codeharbor.import_errors.invalid'), status: :bad_request
render json: t('exercises.import_proforma.import_errors.invalid'), status: :bad_request
rescue StandardError => e
Sentry.capture_exception(e)
render json: t('exercises.import_codeharbor.import_errors.internal_error'), status: :internal_server_error
render json: t('exercises.import_proforma.import_errors.internal_error'), status: :internal_server_error
end

def user_from_api_key
Expand Down Expand Up @@ -572,4 +625,15 @@ def study_group_dashboard

@graph_data = @exercise.get_working_times_for_study_group(@study_group_id)
end

private

def uuid_check(user:, uuid:)
exercise = Exercise.find_by(uuid:)

return {uuid_found: false} if exercise.nil?
return {uuid_found: true, update_right: false} unless ExercisePolicy.new(user, exercise).update?

{uuid_found: true, update_right: true}
end
end
5 changes: 5 additions & 0 deletions app/errors/proformaxml/invalid_zip.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

module ProformaXML
class InvalidZip < ApplicationError; end
end
2 changes: 1 addition & 1 deletion app/services/proforma_service/convert_task_to_exercise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def import_task
@exercise.assign_attributes(
user: @user,
title: @task.title,
description: @task.description,
description: @task.description.presence || @task.title,
public: string_to_bool(extract_meta_data(@task.meta_data&.dig('meta-data'), 'public')) || false,
hide_file_tree: string_to_bool(extract_meta_data(@task.meta_data&.dig('meta-data'), 'hide_file_tree')) || false,
allow_file_creation: string_to_bool(extract_meta_data(@task.meta_data&.dig('meta-data'), 'allow_file_creation')) || false,
Expand Down
5 changes: 4 additions & 1 deletion app/services/proforma_service/import.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

module ProformaService
class Import < ServiceBase
def initialize(zip:, user:)
def initialize(zip:, user:, import_type: 'import')
super()
@zip = zip
@user = user
@import_type = import_type
end

def execute
Expand All @@ -23,6 +24,8 @@ def execute
private

def base_exercise
return Exercise.new(uuid: SecureRandom.uuid, unpublished: true) if @import_type == 'create_new'

exercise = Exercise.find_by(uuid: @task.uuid)
if exercise
raise ProformaXML::ExerciseNotOwned unless ExercisePolicy.new(@user, exercise).update?
Expand Down
33 changes: 33 additions & 0 deletions app/services/proforma_service/uuid_from_zip.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

module ProformaService
class UuidFromZip < ServiceBase
def initialize(zip:)
super()
@zip = zip
end

def execute
if xml_exists_in_zip?
importer = ProformaXML::Importer.new(zip: @zip)
import_result = importer.perform
task = import_result
task.uuid
end
rescue Zip::Error
raise ProformaXML::InvalidZip.new I18n.t('exercises.import_proforma.import_errors.invalid_zip')
end

private

def xml_exists_in_zip?
filenames = Zip::File.open(@zip.path) do |zip_file|
zip_file.map(&:name)
end

return true if filenames.any? {|f| f[/\.xml$/] }

raise ProformaXML::InvalidZip.new I18n.t('exercises.import_proforma.import_errors.no_xml_found')
end
end
end
7 changes: 7 additions & 0 deletions app/uploaders/proforma_zip_uploader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class ProformaZipUploader < CarrierWave::Uploader::Base
def filename
SecureRandom.uuid
end
end
3 changes: 0 additions & 3 deletions app/views/exercises/_export_dialogcontent.html.slim

This file was deleted.

Loading