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 = {
requestComments: function () {
var user_id = $('#editor').data('user-id');
@ -83,6 +81,8 @@ CodeOceanEditorRequestForComments = {
}).done(function () {
this.hideSpinner();
$.flash.success({text: $('#askForCommentsButton').data('message-success')});
// trigger a run
this.runSubmission.call(this, submission);
}.bind(this)).error(this.ajaxError.bind(this));
};

View File

@ -142,19 +142,21 @@ CodeOceanEditorSubmissions = {
* Execution-Logic
*/
runCode: function(event) {
event.preventDefault();
if ($('#run').is(':visible')) {
this.createSubmission('#run', null, function(response) {
//Run part starts here
$('#stop').data('url', response.stop_url);
this.running = true;
this.showSpinner($('#run'));
$('#score_div').addClass('hidden');
this.toggleButtonStates();
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.initializeSocketForRunning(url);
}.bind(this));
}
event.preventDefault();
if ($('#run').is(':visible')) {
this.createSubmission('#run', null, this.runSubmission.bind(this));
}
},
runSubmission: function (submission) {
//Run part starts here
$('#stop').data('url', submission.stop_url);
this.running = true;
this.showSpinner($('#run'));
$('#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) {

View File

@ -1,7 +1,98 @@
#commentitor {
margin-bottom: 2rem;
height: 600px;
background-color:#f9f9f9
.rfc {
h5 {
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 {
@ -11,6 +102,10 @@
border: solid lightgrey 1px;
background-color: rgba(20, 180, 20, 0.2);
border-radius: 4px;
button {
margin-right: 10px;
}
}
#thank-you-note {
@ -18,6 +113,12 @@
height: 200px;
}
#commentitor {
margin-bottom: 2rem;
height: 600px;
background-color:#f9f9f9
}
.ace_tooltip {
display: none !important;
}

View File

@ -42,6 +42,14 @@ div.positive-result {
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 {
border-radius: 50%;
background-color: #ff2600;

View File

@ -9,7 +9,7 @@ module SubmissionScoring
assessment = assessor.assess(output)
passed = ((assessment[:passed] == assessment[:count]) and (assessment[:score] > 0))
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!(filename: file.name_with_extension, message: feedback_message(file, output[:score]), weight: file.weight)
end

View File

@ -1,4 +1,5 @@
class RequestForCommentsController < ApplicationController
include SubmissionScoring
before_action :set_request_for_comment, only: [:show, :edit, :update, :destroy, :mark_as_solved, :set_thank_you_note]
skip_after_action :verify_authorized
@ -107,6 +108,10 @@ class RequestForCommentsController < ApplicationController
@request_for_comment = RequestForComment.new(request_for_comment_params)
respond_to do |format|
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 }
else
format.html { render :new }

View File

@ -13,8 +13,12 @@ class SubmissionsController < ApplicationController
before_action :set_mime_type, only: [:download_file, :render_file]
skip_before_action :verify_authenticity_token, only: [:download_file, :render_file]
def max_message_buffer_size
500
def max_run_output_buffer_size
if(@submission.cause == 'requestComments')
5000
else
500
end
end
def authorize!
@ -196,7 +200,7 @@ class SubmissionsController < ApplicationController
end
def handle_message(message, tubesock, container)
@message_buffer ||= ""
@run_output ||= ""
# Handle special commands first
if (/^#exit/.match(message))
# 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
@docker_client.exit_container(container)
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
# Filter out information about run_command, test_command, user or working directory
run_command = @submission.execution_environment.run_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))
@message_buffer += message if @message_buffer.size <= max_message_buffer_size
parse_message(message, 'stdout', tubesock)
end
end
end
def parse_message(message, output_stream, socket, recursive = true)
parsed = '';
begin
parsed = JSON.parse(message)
if(parsed.class == Hash && parsed.key?('cmd'))
@ -256,13 +260,16 @@ class SubmissionsController < ApplicationController
socket.send_data JSON.dump(parsed)
Rails.logger.info('parse_message sent: ' + JSON.dump(parsed))
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
def save_run_output
if !@message_buffer.blank?
@message_buffer = @message_buffer[(0..max_message_buffer_size-1)] # trim the string to max_message_buffer_size chars
Testrun.create(file: @file, submission: @submission, output: @message_buffer)
if !@run_output.blank?
@run_output = @run_output[(0..max_run_output_buffer_size-1)] # trim the string to max_message_buffer_size chars
Testrun.create(file: @file, cause: 'run', submission: @submission, output: @run_output)
end
end

View File

@ -54,8 +54,10 @@ h1 = "#{@exercise} (external user #{@external_user})"
-submission_or_intervention.testruns.each do |run|
- if run.passed
.unit-test-result.positive-result title=run.output
- else
- elsif run.failed
.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
-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

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">
<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>
<% end %>
<%= link_to(@request_for_comment.exercise.title, [:implement, @request_for_comment.exercise]) %>
@ -9,61 +9,75 @@
<%
user = @request_for_comment.user
submission = @request_for_comment.submission
testruns = Testrun.where(:submission_id => @request_for_comment.submission)
%>
<%= user.displayname %> | <%= @request_for_comment.created_at.localtime %>
</p>
<h5>
<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>
<div class="rfc">
<div class="description">
<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>
<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>
<%= t('activerecord.attributes.exercise.description') %>
</h5>
<% end %>
<h5>
<u><%= t('request_for_comments.howto_title') %></u><br> <%= render_markdown(t('request_for_comments.howto')) %>
</h5>
<div class="text">
<span class="fa fa-chevron-up collapse-button"></span>
<%= render_markdown(@request_for_comment.exercise.description) %>
</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>
<hr>
<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();
});
$('.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) {

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>
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.
howto_title: 'Anleitung:'
howto_title: 'Anleitung'
index:
get_my_comment_requests: Meine Kommentaranfragen
all: "Alle Kommentaranfragen"
@ -517,6 +517,9 @@ de:
modal_title: "Einen Kommentar in Zeile ${line} hinzufügen"
click_for_more_comments: "Klicken um ${numComments} weitere Kommentare zu sehen..."
subscribe_to_author: "Bei neuen Kommentaren des Autors per E-Mail benachrichtigt werden"
no_output: "Keine Ausgabe."
runtime_output: "Programmausgabe"
test_results: "Testergebnisse"
sessions:
create:
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>
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.
howto_title: 'How to comment:'
howto_title: 'How to comment'
index:
all: All Requests for Comments
get_my_comment_requests: My Requests for Comments
@ -517,6 +517,9 @@ en:
modal_title: "Add a comment to line ${line}"
click_for_more_comments: "Click to view ${numComments} more comments..."
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:
create:
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.datetime "created_at"
t.datetime "updated_at"
t.string "cause"
end
create_table "user_exercise_feedbacks", force: :cascade do |t|