Merge pull request #128 from openHPI/rfc_run_assess_messages

Show test results and program output on RfC page
This commit is contained in:
rteusner
2017-09-27 16:05:00 +02:00
committed by GitHub
15 changed files with 267 additions and 81 deletions

View File

@@ -59,8 +59,6 @@ CodeOceanEditorCodePilot = {
} }
}; };
//Request for comments does currently not work on staging platform (no relative root_url used here).
//To fix this rely on ruby routes
CodeOceanEditorRequestForComments = { CodeOceanEditorRequestForComments = {
requestComments: function () { requestComments: function () {
var user_id = $('#editor').data('user-id'); var user_id = $('#editor').data('user-id');
@@ -83,6 +81,8 @@ CodeOceanEditorRequestForComments = {
}).done(function () { }).done(function () {
this.hideSpinner(); this.hideSpinner();
$.flash.success({text: $('#askForCommentsButton').data('message-success')}); $.flash.success({text: $('#askForCommentsButton').data('message-success')});
// trigger a run
this.runSubmission.call(this, submission);
}.bind(this)).error(this.ajaxError.bind(this)); }.bind(this)).error(this.ajaxError.bind(this));
}; };

View File

@@ -142,19 +142,21 @@ CodeOceanEditorSubmissions = {
* Execution-Logic * Execution-Logic
*/ */
runCode: function(event) { runCode: function(event) {
event.preventDefault(); event.preventDefault();
if ($('#run').is(':visible')) { if ($('#run').is(':visible')) {
this.createSubmission('#run', null, function(response) { this.createSubmission('#run', null, this.runSubmission.bind(this));
//Run part starts here }
$('#stop').data('url', response.stop_url); },
this.running = true;
this.showSpinner($('#run')); runSubmission: function (submission) {
$('#score_div').addClass('hidden'); //Run part starts here
this.toggleButtonStates(); $('#stop').data('url', submission.stop_url);
var url = response.run_url.replace(this.FILENAME_URL_PLACEHOLDER, this.active_file.filename.replace(/#$/,'')); // remove # if it is the last character, this is not part of the filename and just an anchor this.running = true;
this.initializeSocketForRunning(url); this.showSpinner($('#run'));
}.bind(this)); $('#score_div').addClass('hidden');
} this.toggleButtonStates();
var url = submission.run_url.replace(this.FILENAME_URL_PLACEHOLDER, this.active_file.filename.replace(/#$/,'')); // remove # if it is the last character, this is not part of the filename and just an anchor
this.initializeSocketForRunning(url);
}, },
saveCode: function(event) { saveCode: function(event) {

View File

@@ -1,7 +1,98 @@
#commentitor { .rfc {
margin-bottom: 2rem;
height: 600px; h5 {
background-color:#f9f9f9 color: #008CBA;
}
.text {
font-size: larger;
}
.text.collapsed {
max-height: 50px;
overflow-y: hidden;
}
.collapse-button {
position: relative;
float: right;
margin-top: 5px;
margin-right: 5px;
cursor: pointer;
}
.description {
.text {
padding: 5px;
background-color: #FAFAFA;
border: 1px solid #CCCCCC;
}
}
.question {
.text {
font-weight: bold;
}
}
.testruns {
.text {
padding: 5px;
background-color: #FAFAFA;
border: 1px solid #CCCCCC;
}
pre {
background-color: inherit;
border: none;
}
}
}
.testrun-assess-results {
display: flex;
.result {
margin-right: 10px;
width: 10px;
height: 10px;
}
.passed {
border-radius: 50%;
background-color: #8efa00;
-webkit-box-shadow: 0 0 11px 1px rgba(44,222,0,1);
-moz-box-shadow: 0 0 11px 1px rgba(44,222,0,1);
box-shadow: 0 0 11px 1px rgba(44,222,0,1);
}
.unknown {
border-radius: 50%;
background-color: #ffca00;
-webkit-box-shadow: 0 0 11px 1px rgb(255, 202, 0);
-moz-box-shadow: 0 0 11px 1px rgb(255, 202, 0);
box-shadow: 0 0 11px 1px rgb(255, 202, 0);
}
.failed {
border-radius: 50%;
background-color: #ff2600;
-webkit-box-shadow: 0 0 11px 1px rgba(222,0,0,1);
-moz-box-shadow: 0 0 11px 1px rgba(222,0,0,1);
box-shadow: 0 0 11px 1px rgba(222,0,0,1);
}
}
#mark-as-solved-button {
margin-top: 20px;
} }
#thank-you-container { #thank-you-container {
@@ -11,6 +102,10 @@
border: solid lightgrey 1px; border: solid lightgrey 1px;
background-color: rgba(20, 180, 20, 0.2); background-color: rgba(20, 180, 20, 0.2);
border-radius: 4px; border-radius: 4px;
button {
margin-right: 10px;
}
} }
#thank-you-note { #thank-you-note {
@@ -18,6 +113,12 @@
height: 200px; height: 200px;
} }
#commentitor {
margin-bottom: 2rem;
height: 600px;
background-color:#f9f9f9
}
.ace_tooltip { .ace_tooltip {
display: none !important; display: none !important;
} }

View File

@@ -42,6 +42,14 @@ div.positive-result {
box-shadow: 0px 0px 11px 1px rgba(44,222,0,1); box-shadow: 0px 0px 11px 1px rgba(44,222,0,1);
} }
div.unknown-result {
border-radius: 50%;
background-color: #ffca00;
-webkit-box-shadow: 0px 0px 11px 1px rgb(255, 202, 0);
-moz-box-shadow: 0px 0px 11px 1px rgb(255, 202, 0);
box-shadow: 0px 0px 11px 1px rgb(255, 202, 0);
}
div.negative-result { div.negative-result {
border-radius: 50%; border-radius: 50%;
background-color: #ff2600; background-color: #ff2600;

View File

@@ -9,7 +9,7 @@ module SubmissionScoring
assessment = assessor.assess(output) assessment = assessor.assess(output)
passed = ((assessment[:passed] == assessment[:count]) and (assessment[:score] > 0)) passed = ((assessment[:passed] == assessment[:count]) and (assessment[:score] > 0))
testrun_output = passed ? nil : output[:stderr] testrun_output = passed ? nil : output[:stderr]
Testrun.new(submission: submission, file: file, passed: passed, output: testrun_output).save Testrun.new(submission: submission, cause: 'assess', file: file, passed: passed, output: testrun_output).save
output.merge!(assessment) output.merge!(assessment)
output.merge!(filename: file.name_with_extension, message: feedback_message(file, output[:score]), weight: file.weight) output.merge!(filename: file.name_with_extension, message: feedback_message(file, output[:score]), weight: file.weight)
end end

View File

@@ -1,4 +1,5 @@
class RequestForCommentsController < ApplicationController class RequestForCommentsController < ApplicationController
include SubmissionScoring
before_action :set_request_for_comment, only: [:show, :edit, :update, :destroy, :mark_as_solved, :set_thank_you_note] before_action :set_request_for_comment, only: [:show, :edit, :update, :destroy, :mark_as_solved, :set_thank_you_note]
skip_after_action :verify_authorized skip_after_action :verify_authorized
@@ -107,6 +108,10 @@ class RequestForCommentsController < ApplicationController
@request_for_comment = RequestForComment.new(request_for_comment_params) @request_for_comment = RequestForComment.new(request_for_comment_params)
respond_to do |format| respond_to do |format|
if @request_for_comment.save if @request_for_comment.save
# create thread here and execute tests. A run is triggered from the frontend and does not need to be handled here.
Thread.new do
score_submission(@request_for_comment.submission)
end
format.json { render :show, status: :created, location: @request_for_comment } format.json { render :show, status: :created, location: @request_for_comment }
else else
format.html { render :new } format.html { render :new }

View File

@@ -13,8 +13,12 @@ class SubmissionsController < ApplicationController
before_action :set_mime_type, only: [:download_file, :render_file] before_action :set_mime_type, only: [:download_file, :render_file]
skip_before_action :verify_authenticity_token, only: [:download_file, :render_file] skip_before_action :verify_authenticity_token, only: [:download_file, :render_file]
def max_message_buffer_size def max_run_output_buffer_size
500 if(@submission.cause == 'requestComments')
5000
else
500
end
end end
def authorize! def authorize!
@@ -196,7 +200,7 @@ class SubmissionsController < ApplicationController
end end
def handle_message(message, tubesock, container) def handle_message(message, tubesock, container)
@message_buffer ||= "" @run_output ||= ""
# Handle special commands first # Handle special commands first
if (/^#exit/.match(message)) if (/^#exit/.match(message))
# Just call exit_container on the docker_client. # Just call exit_container on the docker_client.
@@ -205,19 +209,19 @@ class SubmissionsController < ApplicationController
# kill_socket is called in the "on close handler" of the websocket to the container # kill_socket is called in the "on close handler" of the websocket to the container
@docker_client.exit_container(container) @docker_client.exit_container(container)
elsif /^#timeout/.match(message) elsif /^#timeout/.match(message)
@message_buffer = 'timeout: ' + @message_buffer # add information that this run timed out to the buffer @run_output = 'timeout: ' + @run_output # add information that this run timed out to the buffer
else else
# Filter out information about run_command, test_command, user or working directory # Filter out information about run_command, test_command, user or working directory
run_command = @submission.execution_environment.run_command % command_substitutions(params[:filename]) run_command = @submission.execution_environment.run_command % command_substitutions(params[:filename])
test_command = @submission.execution_environment.test_command % command_substitutions(params[:filename]) test_command = @submission.execution_environment.test_command % command_substitutions(params[:filename])
if !(/root|workspace|#{run_command}|#{test_command}/.match(message)) if !(/root|workspace|#{run_command}|#{test_command}/.match(message))
@message_buffer += message if @message_buffer.size <= max_message_buffer_size
parse_message(message, 'stdout', tubesock) parse_message(message, 'stdout', tubesock)
end end
end end
end end
def parse_message(message, output_stream, socket, recursive = true) def parse_message(message, output_stream, socket, recursive = true)
parsed = '';
begin begin
parsed = JSON.parse(message) parsed = JSON.parse(message)
if(parsed.class == Hash && parsed.key?('cmd')) if(parsed.class == Hash && parsed.key?('cmd'))
@@ -256,13 +260,16 @@ class SubmissionsController < ApplicationController
socket.send_data JSON.dump(parsed) socket.send_data JSON.dump(parsed)
Rails.logger.info('parse_message sent: ' + JSON.dump(parsed)) Rails.logger.info('parse_message sent: ' + JSON.dump(parsed))
end end
ensure
# save the data that was send to the run_output if there is enough space left. this will be persisted as a testrun with cause "run"
@run_output += JSON.dump(parsed) if @run_output.size <= max_run_output_buffer_size
end end
end end
def save_run_output def save_run_output
if !@message_buffer.blank? if !@run_output.blank?
@message_buffer = @message_buffer[(0..max_message_buffer_size-1)] # trim the string to max_message_buffer_size chars @run_output = @run_output[(0..max_run_output_buffer_size-1)] # trim the string to max_message_buffer_size chars
Testrun.create(file: @file, submission: @submission, output: @message_buffer) Testrun.create(file: @file, cause: 'run', submission: @submission, output: @run_output)
end end
end end

View File

@@ -54,8 +54,10 @@ h1 = "#{@exercise} (external user #{@external_user})"
-submission_or_intervention.testruns.each do |run| -submission_or_intervention.testruns.each do |run|
- if run.passed - if run.passed
.unit-test-result.positive-result title=run.output .unit-test-result.positive-result title=run.output
- else - elsif run.failed
.unit-test-result.negative-result title=run.output .unit-test-result.negative-result title=run.output
- else
.unit-test-result.unknown-result title=run.output
td = Time.at(deltas[1..index].inject(:+)).utc.strftime("%H:%M:%S") if index > 0 td = Time.at(deltas[1..index].inject(:+)).utc.strftime("%H:%M:%S") if index > 0
-working_times_until.push((Time.at(deltas[1..index].inject(:+)).utc.strftime("%H:%M:%S") if index > 0)) -working_times_until.push((Time.at(deltas[1..index].inject(:+)).utc.strftime("%H:%M:%S") if index > 0))
- elsif submission_or_intervention.is_a? UserExerciseIntervention - elsif submission_or_intervention.is_a? UserExerciseIntervention

View File

@@ -0,0 +1,9 @@
br
h4 Admin Menu
h5
ul
li = link_to "User's current status of this exercise", statistics_external_user_exercise_path(id: @request_for_comment.exercise_id, external_user_id: @request_for_comment.user_id)
li = link_to "All exercises of this user", statistics_external_user_path(id: @request_for_comment.user_id)
ul
li = link_to "Implement the exercise yourself", implement_exercise_path(id: @request_for_comment.exercise_id)
li = link_to "Show the exercise", exercise_path(id: @request_for_comment.exercise_id)

View File

@@ -0,0 +1,7 @@
button.btn.btn-primary#mark-as-solved-button = t('request_for_comments.mark_as_solved')
#thank-you-container
p = t('request_for_comments.write_a_thank_you_node')
textarea#thank-you-note
button.btn.btn-primary#send-thank-you-note = t('request_for_comments.send_thank_you_note')
button.btn.btn-default#cancel-thank-you-note = t('request_for_comments.cancel_thank_you_note')

View File

@@ -1,6 +1,6 @@
<div class="list-group"> <div class="list-group">
<h4 id ="exercise_caption" class="list-group-item-heading" data-exercise-id="<%=@request_for_comment.exercise.id%>" data-comment-exercise-url="<%=create_comment_exercise_request_for_comment_path%>" data-rfc-id = "<%= @request_for_comment.id %>" > <h4 id ="exercise_caption" class="list-group-item-heading" data-exercise-id="<%=@request_for_comment.exercise.id%>" data-comment-exercise-url="<%=create_comment_exercise_request_for_comment_path%>" data-rfc-id = "<%= @request_for_comment.id %>" >
<% if (@request_for_comment.solved?) %> <% if @request_for_comment.solved? %>
<span class="fa fa-check" aria-hidden="true"></span> <span class="fa fa-check" aria-hidden="true"></span>
<% end %> <% end %>
<%= link_to(@request_for_comment.exercise.title, [:implement, @request_for_comment.exercise]) %> <%= link_to(@request_for_comment.exercise.title, [:implement, @request_for_comment.exercise]) %>
@@ -9,61 +9,75 @@
<% <%
user = @request_for_comment.user user = @request_for_comment.user
submission = @request_for_comment.submission submission = @request_for_comment.submission
testruns = Testrun.where(:submission_id => @request_for_comment.submission)
%> %>
<%= user.displayname %> | <%= @request_for_comment.created_at.localtime %> <%= user.displayname %> | <%= @request_for_comment.created_at.localtime %>
</p> </p>
<div class="rfc">
<h5> <div class="description">
<u><%= t('activerecord.attributes.exercise.description') %>:</u> <%= render_markdown(@request_for_comment.exercise.description) %>
</h5>
<h5>
<% if @request_for_comment.question and not @request_for_comment.question == '' %>
<u><%= t('activerecord.attributes.request_for_comments.question')%>:</u> "<%= @request_for_comment.question %>"
<% else %>
<u><%= t('activerecord.attributes.request_for_comments.question')%>:</u> <%= t('request_for_comments.no_question') %>
<% end %>
</h5>
<% if (policy(@request_for_comment).mark_as_solved? and not @request_for_comment.solved?) %>
<button class="btn btn-primary" id="mark-as-solved-button">
<%= t('request_for_comments.mark_as_solved') %>
</button>
<div id="thank-you-container">
<p>
<%= t('request_for_comments.write_a_thank_you_node') %>
</p>
<textarea id="thank-you-note"></textarea>
<button class="btn btn-primary" id="send-thank-you-note">
<%= t('request_for_comments.send_thank_you_note') %>
</button>
<button class="btn btn-default" id="cancel-thank-you-note">
<%= t('request_for_comments.cancel_thank_you_note') %>
</button>
</div>
<% end %>
<% if @current_user.admin? && user.is_a?(ExternalUser) %>
<br>
<br>
<h4>Admin Menu</h4>
<h5> <h5>
<ul> <%= t('activerecord.attributes.exercise.description') %>
<li><%= link_to "User's current status of this exercise", statistics_external_user_exercise_path(id: @request_for_comment.exercise_id, external_user_id: @request_for_comment.user_id) %></li>
<li><%= link_to "All exercises of this user", statistics_external_user_path(id: @request_for_comment.user_id) %></li> <br>
<li><%= link_to "Implement the exercise yourself", implement_exercise_path(id: @request_for_comment.exercise_id) %> </li>
<li><%= link_to "Show the exercise", exercise_path(id: @request_for_comment.exercise_id) %> </li>
</ul>
</h5> </h5>
<% end %> <div class="text">
<h5> <span class="fa fa-chevron-up collapse-button"></span>
<u><%= t('request_for_comments.howto_title') %></u><br> <%= render_markdown(t('request_for_comments.howto')) %> <%= render_markdown(@request_for_comment.exercise.description) %>
</h5> </div>
</div>
<div class="question">
<h5>
<%= t('activerecord.attributes.request_for_comments.question')%>
</h5>
<div class="text">
<%= @request_for_comment.question or t('request_for_comments.no_question')%>
</div>
</div>
<% if policy(@request_for_comment).mark_as_solved? and not @request_for_comment.solved? %>
<%= render('mark_as_solved') %>
<% end %>
<% if testruns.size > 0 %>
<div class="testruns">
<% output_runs = testruns.select { |run| run.cause == 'run' } %>
<% if output_runs.size > 0 %>
<h5><%= t('request_for_comments.runtime_output') %></h5>
<div class="testrun-output text">
<span class="fa fa-chevron-up collapse-button"></span>
<% output_runs.each do |testrun| %>
<pre><%= testrun.try(:output) or t('request_for_comments.no_output') %></pre>
<% end %>
</div>
<% end %>
<% assess_runs = testruns.select { |run| run.cause == 'assess' } %>
<% if assess_runs.size > 0 %>
<h5><%= t('request_for_comments.test_results') %></h5>
<div class="testrun-assess-results">
<% assess_runs.each do |testrun| %>
<div class="result <%= testrun.passed ? 'passed' : 'failed' %>" title="<%= testrun.output %>"></div>
<% end %>
</div>
<% end %>
</div>
<% end %>
<% if @current_user.admin? && user.is_a?(ExternalUser) %>
<%= render('admin_menu') %>
<% end %>
<hr>
<div class="howto">
<h5>
<%= t('request_for_comments.howto_title') %>
</h5>
<div class="text">
<%= render_markdown(t('request_for_comments.howto')) %>
</div>
</div>
</div>
</div> </div>
<hr>
<div class="hidden sanitizer"></div> <div class="hidden sanitizer"></div>
<!-- <!--
@@ -128,6 +142,12 @@ also, all settings from the rails model needed for the editor configuration in t
thankYouContainer.hide(); 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 // set file paths for ace
var ACE_FILES_PATH = '/assets/ace/'; var ACE_FILES_PATH = '/assets/ace/';
_.each(['modePath', 'themePath', 'workerPath'], function(attribute) { _.each(['modePath', 'themePath', 'workerPath'], function(attribute) {

View File

@@ -497,7 +497,7 @@ de:
Um Kommentare zu einer Programmzeile hinzuzufügen, kann einfach auf die jeweilige Zeilennummer auf der linken Seite geklickt werden. <br> Um Kommentare zu einer Programmzeile hinzuzufügen, kann einfach auf die jeweilige Zeilennummer auf der linken Seite geklickt werden. <br>
Es öffnet sich ein Textfeld, in dem der Kommentar eingetragen werden kann. <br> Es öffnet sich ein Textfeld, in dem der Kommentar eingetragen werden kann. <br>
Mit "Kommentar abschicken" wird der Kommentar dann gesichert und taucht als Sprechblase neben der Zeile auf. Mit "Kommentar abschicken" wird der Kommentar dann gesichert und taucht als Sprechblase neben der Zeile auf.
howto_title: 'Anleitung:' howto_title: 'Anleitung'
index: index:
get_my_comment_requests: Meine Kommentaranfragen get_my_comment_requests: Meine Kommentaranfragen
all: "Alle Kommentaranfragen" all: "Alle Kommentaranfragen"
@@ -517,6 +517,9 @@ de:
modal_title: "Einen Kommentar in Zeile ${line} hinzufügen" modal_title: "Einen Kommentar in Zeile ${line} hinzufügen"
click_for_more_comments: "Klicken um ${numComments} weitere Kommentare zu sehen..." click_for_more_comments: "Klicken um ${numComments} weitere Kommentare zu sehen..."
subscribe_to_author: "Bei neuen Kommentaren des Autors per E-Mail benachrichtigt werden" subscribe_to_author: "Bei neuen Kommentaren des Autors per E-Mail benachrichtigt werden"
no_output: "Keine Ausgabe."
runtime_output: "Programmausgabe"
test_results: "Testergebnisse"
sessions: sessions:
create: create:
failure: Fehlerhafte E-Mail oder Passwort. failure: Fehlerhafte E-Mail oder Passwort.

View File

@@ -497,7 +497,7 @@ en:
To leave comments to a specific code line, click on the respective line number. <br> To leave comments to a specific code line, click on the respective line number. <br>
Enter your comment in the popup and save it by clicking "Comment this". <br> Enter your comment in the popup and save it by clicking "Comment this". <br>
Your comment will show up next to the line number as a speech bubble symbol. Your comment will show up next to the line number as a speech bubble symbol.
howto_title: 'How to comment:' howto_title: 'How to comment'
index: index:
all: All Requests for Comments all: All Requests for Comments
get_my_comment_requests: My Requests for Comments get_my_comment_requests: My Requests for Comments
@@ -517,6 +517,9 @@ en:
modal_title: "Add a comment to line ${line}" modal_title: "Add a comment to line ${line}"
click_for_more_comments: "Click to view ${numComments} more comments..." click_for_more_comments: "Click to view ${numComments} more comments..."
subscribe_to_author: "Receive E-Mail notifications for new comments of the original author" subscribe_to_author: "Receive E-Mail notifications for new comments of the original author"
no_output: "No output."
runtime_output: "Runtime Output"
test_results: "Test Results"
sessions: sessions:
create: create:
failure: Invalid email or password. failure: Invalid email or password.

View File

@@ -0,0 +1,18 @@
class AddCauseToTestruns < ActiveRecord::Migration
def up
add_column :testruns, :cause, :string
Testrun.reset_column_information
Testrun.all.each{ |testrun|
if(testrun.submission.nil?)
say_with_time "#{testrun.id} has no submission" do end
else
testrun.cause = testrun.submission.cause
testrun.save
end
}
end
def down
remove_column :testruns, :cause
end
end

View File

@@ -304,6 +304,7 @@ ActiveRecord::Schema.define(version: 20170920145852) do
t.integer "submission_id" t.integer "submission_id"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.string "cause"
end end
create_table "user_exercise_feedbacks", force: :cascade do |t| create_table "user_exercise_feedbacks", force: :cascade do |t|