Files
codeocean/app/views/request_for_comments/show.html.slim
Sebastian Serth 237c225732 Add support for running CodeOcean under a subpath
* Also refactor (JavaScript) routes
2021-07-06 19:33:55 +02:00

478 lines
16 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

.list-group
h4#exercise_caption.list-group-item-heading data-exercise-id="#{@request_for_comment.exercise.id}" data-rfc-id="#{@request_for_comment.id}"
- if @request_for_comment.solved?
span.fa.fa-check aria-hidden="true"
= link_to_if(policy(@request_for_comment.exercise).show?, @request_for_comment.exercise.title, [:implement, @request_for_comment.exercise])
p.list-group-item-text
- user = @request_for_comment.user
- submission = @request_for_comment.submission
- testruns = Testrun.where(:submission_id => @request_for_comment.submission)
= link_to_if(policy(user).show?, user.displayname, user)
| | #{@request_for_comment.created_at.localtime}
- if @request_for_comment.submission.study_group.present? && policy(@request_for_comment.submission).show_study_group?
= ' | '
= link_to_if(policy(@request_for_comment.submission.study_group).show?, @request_for_comment.submission.study_group, @request_for_comment.submission.study_group)
.rfc
.description
h5
= t('activerecord.attributes.exercise.description')
.text
span.fa.fa-chevron-up.collapse-button
= render_markdown(@request_for_comment.exercise.description)
.question
h5.mt-4
= t('activerecord.attributes.request_for_comments.question')
.text
- question = @request_for_comment.question
= question.blank? ? t('request_for_comments.no_question') : question
- if policy(@request_for_comment).mark_as_solved? and not @request_for_comment.solved?
= render('mark_as_solved')
- if testruns.size > 0
.testruns
- output_runs = testruns.select {|run| run.cause == 'run'}
- if output_runs.size > 0
h5.mt-4= t('request_for_comments.runtime_output')
.collapsed.testrun-output.text
span.fa.fa-chevron-down.collapse-button
- output_runs.each do |testrun|
- output = testrun.try(:output)
- if output
- Sentry.set_extras(output: output)
- begin
- Timeout::timeout(2) do
// (?:\\"|.) is required to correctly identify " within the output.
// The outer (?: |\d+?) is used to correctly identify integers within the JSON
- messages = output.scan(/{(?:(?:"(?:\\"|.)+?":(?:"(?:\\"|.)*?"|-?\d+?|\[.*?\]|null))+?,?)+}/)
- messages.map! {|el| JSON.parse(el)}
- messages.keep_if {|message| message['cmd'] == 'write'}
- messages.map! {|message| message['data']}
- output = messages.join ''
- rescue Timeout::Error
pre= output or t('request_for_comments.no_output')
- assess_runs = testruns.select {|run| run.cause == 'assess' }
- unless @current_user.admin?
- assess_runs = assess_runs.select {|run| run.file.present? ? run.file.teacher_defined_test? : true }
- if assess_runs.size > 0
h5.mt-4= t('request_for_comments.test_results')
.testrun-assess-results
- assess_runs.each do |testrun|
.testrun-container
div class=("result #{testrun.passed ? 'passed' : 'failed'}")
.collapsed.testrun-output.text
span.fa.fa-chevron-down.collapse-button
pre= testrun.output or t('request_for_comments.no_output')
- if @current_user.admin? && user.is_a?(ExternalUser)
= render('admin_menu')
hr/
.howto
h5.mt-4
= t('request_for_comments.howto_title')
.text
= render_markdown(t('request_for_comments.howto'))
.d-none.sanitizer
/!
| do not put a carriage return in the line below. it will be present in the presentation of the source code, otherwise.
| also, all settings from the rails model needed for the editor configuration in the JavaScript are attached to the editor as data attributes here.
- submission.files.each do |file|
= (file.path or "") + "/" + file.name + file.file_type.file_extension
br/
|   
i.fa.fa-arrow-down aria-hidden="true"
= t('request_for_comments.click_here')
#commentitor.editor data-file-id="#{file.id}" data-mode="#{file.file_type.editor_mode}" data-read-only="true"
= file.content
= render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.dialogtitle'), template: 'exercises/_comment_dialogcontent')
javascript:
$('.modal-content').draggable({
handle: '.modal-header'
}).resizable({
autoHide: true
});
var solvedButton = $('#mark-as-solved-button');
var addCommentExerciseButton = $('#addCommentExerciseButton');
var thankYouContainer = $('#thank-you-container');
solvedButton.on('click', function(){
$.ajax({
dataType: 'json',
method: 'GET',
url: '//' + location.host + location.pathname + '/mark_as_solved'
}).done(function(response){
if(response.solved){
solvedButton.removeClass('btn-primary');
solvedButton.addClass('btn-success');
solvedButton.html("#{t('request_for_comments.solved')}");
solvedButton.off('click');
thankYouContainer.show();
}
});
});
$('#send-thank-you-note').on('click', function () {
var value = $('#thank-you-note').val();
if (value) {
$.ajax({
dataType: 'json',
method: 'POST',
url: '//' + location.host + location.pathname + '/set_thank_you_note',
data: {
note: value
}
}).done(function() {
thankYouContainer.hide();
});
}
});
$('#cancel-thank-you-note').on('click', function () {
thankYouContainer.hide();
});
$('.text > .collapse-button').on('click', function(e) {
$(this).toggleClass('fa-chevron-down');
$(this).toggleClass('fa-chevron-up');
$(this).parent().toggleClass('collapsed');
});
// set file paths for ace
var ACE_FILES_PATH = "#{Rails.application.config.relative_url_root.chomp('/')}/assets/ace/";
_.each(['modePath', 'themePath', 'workerPath'], function(attribute) {
ace.config.set(attribute, ACE_FILES_PATH);
});
var commentitor = $('.editor');
commentitor.each(function (index, editor) {
var currentEditor = ace.edit(editor);
currentEditor.setReadOnly(true);
// set editor mode (used for syntax highlighting
currentEditor.getSession().setMode($(editor).data('mode'));
currentEditor.getSession().setOption("useWorker", false);
currentEditor.commentVisualsByLine = {};
setAnnotations(currentEditor, $(editor).data('file-id'));
currentEditor.on("guttermousedown", handleSidebarClick);
currentEditor.on("guttermousemove", showPopover);
});
function preprocess(commentText) {
// sanitize comments to deal with XSS attacks:
commentText = $('div.sanitizer').text(commentText).html();
// display original line breaks:
return commentText.replace(/\n/g, '<br>');
}
function replaceNewlineTags(commentText) {
// display original line breaks as \n:
return commentText.replace(/<br>/g, '\n');
}
function generateCommentHtmlContent(comments) {
var htmlContent = '';
comments.forEach(function(comment, index) {
var commentText = preprocess(comment.text);
if (index !== 0) {
htmlContent += '<div class="comment-divider"></div>'
}
htmlContent += '\
<div class="comment" data-comment-id=' + comment.id + '> \
<div class="comment-header"> \
<div class="comment-username">' + preprocess(comment.username) + '</div> \
<div class="comment-date">' + comment.date + '</div> \
<div class="comment-updated' + (comment.updated ? '' : ' d-none') + '"> \
<i class="fa fa-pencil" aria-hidden="true"></i> \
#{{ t('request_for_comments.comment_edited') }} \
</div> \
</div> \
<div class="comment-content">' + commentText + '</div> \
<textarea class="comment-editor">' + commentText + '</textarea> \
<div class="comment-actions' + (comment.editable ? '' : ' d-none') + '"> \
<button class="action-edit btn btn-sm btn-warning">#{ t('shared.edit') }</button> \
<button class="action-delete btn btn-sm btn-danger">#{ t('shared.destroy') }</button> \
</div> \
</div>';
});
return htmlContent;
}
function buildPopover(comments, where) {
// only display the newest three comments in preview
var maxComments = 3;
var htmlContent = generateCommentHtmlContent(comments.reverse().slice(0, maxComments));
if (comments.length > maxComments) {
// add a hint that there are more comments than shown here
htmlContent += '<div class="popover-footer">#{ t('request_for_comments.click_for_more_comments') }</div>'
.replace('${numComments}', String(comments.length - maxComments));
}
where.popover({
content: htmlContent,
html: true, // necessary to style comments. XSS is not possible due to comment pre-processing (sanitizing)
trigger: 'manual', // can only be triggered by $(where).popover('show' | 'hide')
container: 'body'
});
}
function setAnnotations(editor, fileid) {
var session = editor.getSession();
var jqrequest = $.ajax({
dataType: 'json',
method: 'GET',
url: Routes.comments_path(),
data: {
file_id: fileid
}
});
jqrequest.done(function(response){
$.each(response, function(index, comment) {
comment.className = 'code-ocean_comment';
});
session.setAnnotations(response);
});
}
function getCommentsForRow(editor, row){
return editor.getSession().getAnnotations().filter(function(element) {
return element.row === row;
})
}
function deleteComment(commentId, editor, file_id, callback) {
var jqxhr = $.ajax({
type: 'DELETE',
url: Routes.comments_path(commentId)
});
jqxhr.done(function () {
setAnnotations(editor, file_id);
callback();
});
jqxhr.fail(ajaxError);
}
function updateComment(commentId, text, editor, file_id, callback) {
var jqxhr = $.ajax({
type: 'PATCH',
url: Routes.comments_path(commentId),
data: {
comment: {
text: text
}
}
});
jqxhr.done(function () {
setAnnotations(editor, file_id);
callback();
});
jqxhr.fail(ajaxError);
}
function createComment(file_id, row, editor, commenttext){
var jqxhr = $.ajax({
data: {
comment: {
file_id: file_id,
row: row,
column: 0,
text: commenttext,
request_id: $('h4#exercise_caption').data('rfc-id')
}
},
dataType: 'json',
method: 'POST',
url: Routes.comments_path()
});
jqxhr.done(function(){
setAnnotations(editor, file_id);
});
jqxhr.fail(ajaxError);
}
function subscribeToRFC(subscriptionType, checkbox){
checkbox.attr("disabled", true);
var jqxhr = $.ajax({
data: {
subscription: {
request_for_comment_id: $('h4#exercise_caption').data('rfc-id'),
subscription_type: subscriptionType
}
},
dataType: 'json',
method: 'POST',
url: Routes.subscriptions_path({format: 'json'})
});
jqxhr.done(function(subscription) {
checkbox.data('subscription', subscription.id);
checkbox.attr("disabled", false);
});
jqxhr.fail(function(response) {
checkbox.prop('checked', false);
checkbox.attr("disabled", false);
ajaxError(response);
});
}
function unsubscribeFromRFC(checkbox) {
checkbox.attr("disabled", true);
var subscriptionId = checkbox.data('subscription');
var jqxhr = $.ajax({
url: Routes.unsubscribe_subscription_path(subscriptionId, {format: 'json'})
});
jqxhr.done(function(response) {
checkbox.prop('checked', false);
checkbox.data('subscription', null);
checkbox.attr("disabled", false);
$.flash.success({text: response.message});
});
jqxhr.fail(function(response) {
checkbox.prop('checked', true);
checkbox.attr("disabled", false);
ajaxError(response);
});
}
var lastRow = null;
var lastTarget = null;
function showPopover(e) {
var target = e.domEvent.target;
var row = e.getDocumentPosition().row;
if (target.className.indexOf('ace_gutter-cell') === -1 || lastRow === row) {
return;
}
if (lastTarget === target) {
// sometimes the row gets updated before the DOM event target, so we need to wait for it to change
return;
}
lastRow = row;
var editor = e.editor;
var comments = getCommentsForRow(editor, row);
buildPopover(comments, $(target));
lastTarget = target;
$(target).popover('show');
$(target).on('mouseleave', function () {
$(this).off('mouseleave');
$(this).popover('dispose');
});
}
$('.ace_gutter').on('mouseleave', function () {
lastRow = null;
lastTarget = null;
});
function handleSidebarClick(e) {
var target = e.domEvent.target;
if (target.className.indexOf('ace_gutter-cell') === -1) return;
var editor = e.editor;
var fileid = $(editor.container).data('file-id');
var row = e.getDocumentPosition().row;
e.stop();
$('.modal-title').text("#{ t('request_for_comments.modal_title') }".replace('${line}', row + 1));
var commentModal = $('#comment-modal');
var otherComments = commentModal.find('#otherComments');
var htmlContent = generateCommentHtmlContent(getCommentsForRow(editor, row));
if (htmlContent) {
otherComments.show();
var container = otherComments.find('.container');
container.html(htmlContent);
var deleteButtons = container.find('.action-delete');
deleteButtons.on('click', function (event) {
var button = $(event.target);
var parent = $(button).parent().parent();
var commentId = parent.data('comment-id');
deleteComment(commentId, editor, fileid, function () {
parent.html('<div class="comment-removed">#{ t('comments.deleted') }</div>');
});
});
var editButtons = container.find('.action-edit');
editButtons.on('click', function (event) {
var button = $(event.target);
var parent = $(button).parent().parent();
var commentId = parent.data('comment-id');
var currentlyEditing = button.data('editing');
var deleteButton = parent.find('.action-delete');
var commentContent = parent.find('.comment-content');
var commentEditor = parent.find('textarea.comment-editor');
var commentUpdated = parent.find('.comment-updated');
if (currentlyEditing) {
updateComment(commentId, commentEditor.val(), editor, fileid, function () {
button.text("#{ t('shared.edit') }");
button.data('editing', false);
commentContent.html(preprocess(commentEditor.val()));
deleteButton.show();
commentContent.show();
commentEditor.hide();
commentUpdated.removeClass('d-none');
});
} else {
button.text("#{ t('comments.save_update') }");
button.data('editing', true);
deleteButton.hide();
commentContent.hide();
commentEditor.val(replaceNewlineTags(commentEditor.val()));
commentEditor.show();
}
});
} else {
otherComments.hide();
}
var subscribeCheckbox = commentModal.find('#subscribe');
subscribeCheckbox.prop('checked', subscribeCheckbox.data('subscription'));
subscribeCheckbox.off('change');
subscribeCheckbox.on('change', function() {
if (this.checked) {
subscribeToRFC('author', $(this));
} else {
unsubscribeFromRFC($(this));
}
});
var addCommentButton = commentModal.find('#addCommentButton');
addCommentButton.off('click');
addCommentButton.on('click', function(){
var commentTextarea = commentModal.find('#myComment > textarea');
var commenttext = commentTextarea.val();
if (commenttext !== "") {
createComment(fileid, row, editor, commenttext);
commentTextarea.val('') ;
commentModal.modal('hide');
}
});
commentModal.modal('show');
}
function ajaxError(response) {
var message = ((response || {}).responseJSON || {}).message || '';
$.flash.danger({
text: message.length > 0 ? message : $('#flash').data('message-failure')
});
}