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

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
91 changes: 81 additions & 10 deletions app/assets/javascripts/exercises.js
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ $(document).on('turbolinks:load', function () {
var observeExportButtons = function () {
$('.export-start').on('click', function (e) {
e.preventDefault();
new bootstrap.Modal($('#export-modal')).show();
new bootstrap.Modal($('#transfer-modal')).show();
exportExerciseStart($(this).data().exerciseId);
});
body_selector.on('click', '.export-retry-button', function () {
Expand All @@ -356,11 +356,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 +380,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 +395,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 +408,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();
new bootstrap.Modal($('#transfer-modal')).show();
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) {
$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 () {
// e.preventDefault();
new bootstrap.Modal($('#transfer-modal')).show();

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">Upload file</label><input type="file" id="proforma-file" name="proforma-file">');
}

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 +533,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
14 changes: 7 additions & 7 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,25 +189,25 @@ 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 {
.transfer-exercise-actions {
max-width: 110px;
min-width: 110px;
}
Expand All @@ -223,6 +223,6 @@ a.file-heading {
font-weight: 600;
}

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

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,68 @@
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 ProformaError::InvalidZip => e
render json: {

Check warning on line 183 in app/controllers/exercises_controller.rb

View check run for this annotation

Codecov / codecov/patch

app/controllers/exercises_controller.rb#L183

Added line #L183 was not covered by tests
status: 'failure',
message: e.message,
actions: render_to_string(partial: 'import_actions',
locals: {exercise: @exercise, imported: false, exists:, updatable:, file_id: uploader.cache_name}),
}
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 +230,10 @@
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 +627,15 @@

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

14 changes: 14 additions & 0 deletions app/views/exercises/_import_actions.html.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
- if imported
= link_to t('exercises.import_proforma.button.show_exercise'), exercise, class: 'btn btn-light btn-sm float-end show-action import-export-button', target: '_blank', rel: 'noopener noreferrer'
- elsif exists && updatable
= button_tag type: 'button', class: 'btn btn-light btn-sm float-end import-action import-export-button', data: {'import-type' => 'import', 'file-id' => file_id} do
i.fa-solid.fa-check.confirm-icon.export-button-icon
= t('exercises.import_proforma.button.overwrite')
- elsif exists
= button_tag type: 'button', class: 'btn btn-light btn-sm float-end import-action import-export-button', data: {'import-type' => 'create_new', 'file-id' => file_id} do
i.fa-solid.fa-check.confirm-icon-alt.export-button-icon
= t('exercises.import_proforma.button.import_copy')
- else
= button_tag type: 'button', class: 'btn btn-light btn-sm float-end import-action import-export-button', data: {'import-type' => 'import', 'file-id' => file_id} do
i.fa-solid.fa-check.confirm-icon.export-button-icon
= t('exercises.import_proforma.button.import')
3 changes: 3 additions & 0 deletions app/views/exercises/_transfer_dialogcontent.html.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#exercise-transfer
.transfer-message
.transfer-exercise-actions
Loading