.list-group h4#exercise_caption.list-group-item-heading data-comment-exercise-url=create_comment_exercise_request_for_comment_path 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 - puts 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+?|\[.*?\]))+?,?)+}/) - 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.teacher_defined_test? } - 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 + '/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 + '/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 = '/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, '
'); } function replaceNewlineTags(commentText) { // display original line breaks as \n: return commentText.replace(/
/g, '\n'); } function generateCommentHtmlContent(comments) { var htmlContent = ''; comments.forEach(function(comment, index) { var commentText = preprocess(comment.text); if (index !== 0) { htmlContent += '
' } htmlContent += '\
\
\
' + preprocess(comment.username) + '
\
' + comment.date + '
\
\ \ #{{ t('request_for_comments.comment_edited') }} \
\
\
' + commentText + '
\ \
\ \ \
\
'; }); 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 += '' .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: '/comments', 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: "/comments/" + 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: "/comments/" + 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: "/comments" }); 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: "/subscriptions.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: '/subscriptions/' + subscriptionId + '/unsubscribe.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('
#{ t('comments.deleted') }
'); }); }); 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') }); }