merged qa into codeocean

This commit is contained in:
Nicholas Wittstruck
2015-03-27 22:08:53 +01:00
22 changed files with 267 additions and 95 deletions

View File

@ -110,7 +110,7 @@ GEM
excon (>= 0.38.0)
json
erubis (2.7.0)
excon (0.44.4)
excon (0.45.0)
execjs (2.4.0)
factory_girl (4.5.0)
activesupport (>= 3.0.0)

View File

@ -16,6 +16,7 @@ $(function() {
var active_file = undefined;
var active_frame = undefined;
var running = false;
var qa_api = undefined;
var flowrResultHtml = '<div class="panel panel-default"><div id="{{headingId}}" role="tab" class="panel-heading"><h4 class="panel-title"><a data-toggle="collapse" data-parent="#flowrHint" href="#{{collapseId}}" aria-expanded="true" aria-controls="{{collapseId}}"></a></h4></div><div id="{{collapseId}}" role="tabpanel" aria-labelledby="{{headingId}}" class="panel-collapse collapse"><div class="panel-body"></div></div></div>'
@ -128,22 +129,9 @@ $(function() {
var evaluateCodeWithStreamedResponse = function(url, callback) {
var event_source = new EventSource(url);
event_source.addEventListener('close', function(event) {
event_source.close();
hideSpinner();
running = false;
toggleButtonStates();
if (JSON.parse(event.data).code !== 200) {
ajaxError();
showTab(1);
}
if (qa_api) {
qa_api.executeCommand('syncOutput', [chunkBuffer]);
chunkBuffer = [{streamedResponse: true}];
}
});
event_source.addEventListener('error', ajaxError);
event_source.addEventListener('close', closeEventSource);
event_source.addEventListener('error', closeEventSource);
event_source.addEventListener('hint', renderHint);
event_source.addEventListener('info', storeContainerInformation);
event_source.addEventListener('output', callback);
@ -154,11 +142,20 @@ $(function() {
event_source.addEventListener('close', handleStderrOutputForFlowr);
}
if (qa_api) {
event_source.addEventListener('close', handleStreamedResponseForCodePilot);
}
event_source.addEventListener('status', function(event) {
showStatus(JSON.parse(event.data));
});
};
var handleStreamedResponseForCodePilot = function(event) {
qa_api.executeCommand('syncOutput', [chunkBuffer]);
chunkBuffer = [{streamedResponse: true}];
}
var evaluateCodeWithoutStreamedResponse = function(url, callback) {
var jqxhr = ajax({
method: 'GET',
@ -277,9 +274,9 @@ $(function() {
var handleTestResponse = function(response) {
clearOutput();
printOutput(response[0], false, 0);
if (qa_api) {
qa_api.executeCommand('syncOutput', [response]);
}
if (qa_api) {
qa_api.executeCommand('syncOutput', [response]);
}
showStatus(response[0]);
showTab(2);
};
@ -347,7 +344,7 @@ $(function() {
commentModal.find('#removeAllButton').off('click')
commentModal.find('#addCommentButton').on('click', function(e){
var user_id = 18
var user_id = element.data('user-id')
var commenttext = commentModal.find('textarea').val()
if (commenttext !== "") {
@ -357,7 +354,7 @@ $(function() {
})
commentModal.find('#removeAllButton').on('click', function(e){
var user_id = 18;
var user_id = element.data('user-id')
deleteComment(user_id,file_id,row,editor);
commentModal.modal('hide')
})
@ -380,10 +377,7 @@ $(function() {
}
var setAnnotations = function (editor, file_id){
var session = editor.getSession();
// Retrieve comments for file and set them as annotations
var session = editor.getSession()
var url = "/comments";
var jqrequest = $.ajax({
@ -468,7 +462,7 @@ $(function() {
url: '/comments',
data: {
id: annotation.id,
user_id: 18,
user_id: $('#editor').data('user-id'),
comment: {
row: annotation.row,
text: annotation.text
@ -504,6 +498,7 @@ $(function() {
$('#create-file').on('click', showFileDialog);
$('#destroy-file').on('click', confirmDestroy);
$('#download').on('click', downloadCode);
$('#request-for-comments').on('click', requestComments)
};
var initializeTooltips = function() {
@ -529,15 +524,17 @@ $(function() {
return 'executable' in active_frame.data();
};
var setActiveFile = function (filename, fileId) {
active_file = {
filename: filename,
id: fileId
};
}
var isActiveFileRenderable = function() {
return 'renderable' in active_frame.data();
};
var setActiveFile = function (filename, fileId) {
active_file = {
filename: filename,
id: fileId
};
}
var isActiveFileRenderable = function() {
return 'renderable' in active_frame.data();
};
var isActiveFileRunnable = function() {
return isActiveFileExecutable() && ['main_file', 'user_defined_file'].includes(active_frame.data('role'));
};
@ -566,7 +563,7 @@ $(function() {
panel.find('.row .col-sm-9').eq(3).find('a').attr('href', '#output-' + index);
};
var chunkBuffer = [{streamedResponse: true}];
var chunkBuffer = [{streamedResponse: true}];
var printChunk = function(event) {
var output = JSON.parse(event.data);
@ -612,10 +609,17 @@ $(function() {
$('#results ul').first().html('');
$('.test-count .number').html(response.length);
clearOutput();
_.each(response, function(result, index) {
printOutput(result, false, index);
printScoringResult(result, index);
});
if (_.some(response, function(result) {
return result.status === 'timeout';
})) {
showTimeoutMessage();
}
if (qa_api) {
// send test response to QA
qa_api.executeCommand('syncOutput', [response]);
@ -743,7 +747,7 @@ $(function() {
var showFirstFile = function() {
var frame = $('.frame[data-role="main_file"]').isPresent() ? $('.frame[data-role="main_file"]') : $('.frame').first();
var file_id = frame.find('.editor').data('file-id');
setActiveFile(frame.data('filename'), file_id);
setActiveFile(frame.data('filename'), file_id);
$('#files').jstree().select_node(file_id);
showFrame(frame);
toggleButtonStates();
@ -778,10 +782,7 @@ $(function() {
var showStatus = function(output) {
if (output.status === 'timeout') {
$.flash.danger({
icon: ['fa', 'fa-clock-o'],
text: $('#editor').data('message-timeout')
});
showTimeoutMessage();
} else if (output.stderr) {
$.flash.danger({
icon: ['fa', 'fa-bug'],
@ -799,6 +800,13 @@ $(function() {
$('a[data-toggle="tab"]').eq(index || 0).tab('show');
};
var showTimeoutMessage = function() {
$.flash.danger({
icon: ['fa', 'fa-clock-o'],
text: $('#editor').data('message-timeout')
});
};
var showWorkspaceTab = function(event) {
event.preventDefault();
showTab(1);
@ -875,53 +883,55 @@ $(function() {
$('#test').toggle(isActiveFileTestable());
};
if ($('#editor').isPresent()) {
var qa_api;
if ($('#questions-column').isPresent() && QaApi.isBrowserSupported()) {
$('#editor-column').addClass('col-md-8').removeClass('col-md-10');
$('#questions-column').addClass('col-md-3');
var requestComments = function(e) {
var user_id = $('#editor').data('user-id')
var exercise_id = $('#editor').data('exercise-id')
var file_id = $('.editor').data('file-id')
var node = document.getElementById('questions-holder');
var url = $('#questions-holder').data('url');
var qa_api = new QaApi(node, url);
}
configureEditors();
initializeEditors(qa_api);
initializeEventHandlers();
initializeFileTree();
initializeTooltips();
renderScore();
showMainFile();
showRequestedTab();
$.ajax({
method: 'POST',
url: '/request_for_comments',
data: {
request_for_comment: {
requestorid: user_id,
exerciseid: exercise_id,
fileid: file_id,
"requested_at(1i)": 2015,
"requested_at(2i)":3,
"requested_at(3i)":27,
"requested_at(4i)":17,
"requested_at(5i)":06
}
}
})
}
var stderrOutput = ''
var handleStderrOutputForFlowr = function(event) {
var json = JSON.parse(event.data);
var initializeCodePilot = function() {
if ($('#questions-column').isPresent() && QaApi.isBrowserSupported()) {
$('#editor-column').addClass('col-md-8').removeClass('col-md-10');
$('#questions-column').addClass('col-md-3');
if (json.stderr) {
stderrOutput += json.stderr;
} else if (json.code) {
var flowrHintBody = $('#flowrHint .panel-body')
var node = document.getElementById('questions-holder');
var url = $('#questions-holder').data('url');
jQuery.getJSON(flowrUrl + '&query=' + escape(stderrOutput), function(data) {
for (var question in data.queryResults) {
// replace everything, not only one occurence
var collapsibleTileHtml = flowrResultHtml.replace(/{{collapseId}}/g, 'collapse-' + question).replace(/{{headingId}}/g, 'heading-' + question)
var resultTile = $(collapsibleTileHtml)
resultTile.find('h4 > a').text(data.queryResults[question].title)
resultTile.find('.panel-body').append($(data.queryResults[question].body))
flowrHintBody.append(resultTile)
qa_api = new QaApi(node, url);
}
$('#flowrHint').fadeIn()
})
stderrOutput = ''
}
};
if ($('#editor').isPresent()) {
if (isBrowserSupported()) {
initializeCodePilot();
$('.score, #development-environment').show();
configureEditors();
initializeEditors();
initializeEventHandlers();
initializeFileTree();
initializeTooltips();
renderScore();
showFirstFile();
showRequestedTab();
} else {
$('#alert').show();
}
}
});

View File

@ -86,6 +86,12 @@ class ExercisesController < ApplicationController
@submission = current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first
@files = (@submission ? @submission.collect_files : @exercise.files).select(&:visible).sort_by(&:name_with_extension)
@paths = collect_paths(@files)
if current_user.respond_to? :external_id
@user_id = current_user.external_id
else
@user_id = '00000001-3100-4444-9999-000000000001'
end
end
def index

View File

@ -0,0 +1,62 @@
class RequestForCommentsController < ApplicationController
before_action :set_request_for_comment, only: [:show, :edit, :update, :destroy]
skip_after_action :verify_authorized
# GET /request_for_comments
# GET /request_for_comments.json
def index
@request_for_comments = RequestForComment.all
end
# GET /request_for_comments/1
# GET /request_for_comments/1.json
def show
end
# GET /request_for_comments/new
def new
@request_for_comment = RequestForComment.new
end
# GET /request_for_comments/1/edit
def edit
end
# POST /request_for_comments
# POST /request_for_comments.json
def create
@request_for_comment = RequestForComment.new(request_for_comment_params)
respond_to do |format|
if @request_for_comment.save
format.json { render :show, status: :created, location: @request_for_comment }
else
format.html { render :new }
format.json { render json: @request_for_comment.errors, status: :unprocessable_entity }
end
end
end
# DELETE /request_for_comments/1
# DELETE /request_for_comments/1.json
def destroy
@request_for_comment.destroy
respond_to do |format|
format.html { redirect_to request_for_comments_url, notice: 'Request for comment was successfully destroyed.' }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_request_for_comment
@request_for_comment = RequestForComment.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def request_for_comment_params
params.require(:request_for_comment).permit(:requestorid, :exerciseid, :fileid, :requested_at)
end
end

View File

@ -0,0 +1,2 @@
module RequestForCommentsHelper
end

View File

@ -0,0 +1,7 @@
class RequestForComment < ActiveRecord::Base
before_create :set_requested_timestamp
def set_requested_timestamp
self.requested_at = Time.now
end
end

View File

@ -1,4 +1,4 @@
#editor.row data-exercise-id=exercise.id data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-errors-url=execution_environment_errors_path(exercise.execution_environment) data-submissions-url=submissions_path
#editor.row data-exercise-id=exercise.id data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-errors-url=execution_environment_errors_path(exercise.execution_environment) data-submissions-url=submissions_path data-user-id=@current_user.id
.col-sm-3 = render('editor_file_tree', files: @files)
#frames.col-sm-9
- @files.each do |file|

View File

@ -5,5 +5,6 @@ hr
= render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-cause' => 'file'}, icon: 'fa fa-plus', id: 'create-file', label: t('exercises.editor.create_file'))
= render('editor_button', classes: 'btn-block btn-warning btn-sm', data: {:'data-cause' => 'file', :'data-message-confirm' => t('shared.confirm_destroy')}, icon: 'fa fa-times', id: 'destroy-file', label: t('exercises.editor.destroy_file'))
= render('editor_button', classes: 'btn-block btn-primary btn-sm', icon: 'fa fa-download', id: 'download', label: t('exercises.editor.download'))
= render('editor_button', classes: 'btn-block btn-primary btn-sm', icon: 'fa fa-bullhorn', id: 'request-for-comments', label: 'Request comments')
= render('shared/modal', id: 'modal-file', template: 'code_ocean/files/_form', title: t('exercises.editor.create_file'))

View File

@ -11,4 +11,5 @@
- else
= link_to(file.native_file.file.name_with_extension, file.native_file.url)
- else
.editor data-file-id=file.ancestor_id data-indent-size=file.file_type.indent_size data-mode=file.file_type.editor_mode data-read-only=file.read_only = file.content
.editor-content.hidden data-file-id=file.ancestor_id = file.content
.editor data-file-id=file.ancestor_id data-indent-size=file.file_type.indent_size data-mode=file.file_type.editor_mode data-read-only=file.read_only

View File

@ -40,4 +40,4 @@ h1 = Exercise.model_name.human(count: 2)
td = link_to(t('shared.statistics'), statistics_exercise_path(exercise))
= render('shared/pagination', collection: @exercises)
p = render('shared/new_button', model: Exercise)
p = render('shared/new_button', model: Exercise)

View File

@ -34,6 +34,12 @@ html lang='en'
.container data-controller=controller_name
= render('breadcrumbs')
= render('flash')
= yield
- if (controller_name == "exercises" && action_name == "implement")
.container-fluid
= yield
- else
.container
= yield
- template_variables = {execution_environment: @exercise.execution_environment} if action_name == 'implement'
= render('shared/modal', classes: 'modal-lg', id: 'modal-help', template: 'application/help', template_variables: template_variables, title: t('shared.help.headline'))

View File

@ -0,0 +1,33 @@
<%= form_for(@request_for_comment) do |f| %>
<% if @request_for_comment.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@request_for_comment.errors.count, "error") %> prohibited this request_for_comment from being saved:</h2>
<ul>
<% @request_for_comment.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :requestorid %><br>
<%= f.number_field :requestorid %>
</div>
<div class="field">
<%= f.label :exerciseid %><br>
<%= f.number_field :exerciseid %>
</div>
<div class="field">
<%= f.label :fileid %><br>
<%= f.number_field :fileid %>
</div>
<div class="field">
<%= f.label :requested_at %><br>
<%= f.datetime_select :requested_at %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>

View File

@ -0,0 +1,12 @@
<h1>Listing comment requests</h1>
<div class="list-group">
<% @request_for_comments.each do |request_for_comment| %>
<a href="<%= request_for_comment_path(request_for_comment) %>" class="list-group-item">
<h4 class="list-group-item-heading"><%= Exercise.find(request_for_comment.exerciseid) %></h4>
<p class="list-group-item-text">
<%= InternalUser.find(request_for_comment.requestorid) %> | <%= request_for_comment.requested_at %>
</p>
</a>
<% end %>
</div>

View File

@ -0,0 +1,4 @@
json.array!(@request_for_comments) do |request_for_comment|
json.extract! request_for_comment, :id, :requestorid, :exerciseid, :fileid, :requested_at
json.url request_for_comment_url(request_for_comment, format: :json)
end

View File

@ -0,0 +1,9 @@
<div class="list-group">
<h4 class="list-group-item-heading"><%= Exercise.find(@request_for_comment.exerciseid) %></h4>
<p class="list-group-item-text">
<%= InternalUser.find(@request_for_comment.requestorid) %> | <%= @request_for_comment.requested_at %>
</p>
</div>
<div id='editor' class='editor' data-read-only='true' data-file-id='<%=@request_for_comment.fileid%>'>
</div>

View File

@ -0,0 +1 @@
json.extract! @request_for_comment, :id, :requestorid, :exerciseid, :fileid, :requested_at, :created_at, :updated_at

View File

@ -180,7 +180,7 @@ de:
stop: Stoppen
submit: Code zur Bewertung abgeben
test: Testen
timeout: 'Ihr Code benötigte länger als die erlaubten %{permitted_execution_time} Sekunden, um zu terminieren.'
timeout: 'Ihr Code hat die erlaubte Ausführungszeit von %{permitted_execution_time} Sekunden überschritten.'
tooltips:
save: Ihr Code wird automatisch gespeichert, wann immer Sie eine Datei herunterladen, ausführen oder testen. Explizites Speichern ist also selten notwendig.
editor_file_tree:

View File

@ -180,7 +180,7 @@ en:
stop: Stop
submit: Submit Code For Assessment
test: Test
timeout: 'Your code took longer than the permitted %{permitted_execution_time} seconds to run.'
timeout: 'Your code exceeded the permitted execution time of %{permitted_execution_time} seconds.'
tooltips:
save: Your code is automatically saved whenever you download, run, or test it. Therefore, explicitly saving is rarely necessary.
editor_file_tree:

View File

@ -1,6 +1,7 @@
FILENAME_REGEXP = /[\w\.]+/ unless Kernel.const_defined?(:FILENAME_REGEXP)
Rails.application.routes.draw do
resources :request_for_comments
resources :comments, except: [:destroy] do
collection do
delete :destroy

View File

@ -0,0 +1,12 @@
class CreateRequestForComments < ActiveRecord::Migration
def change
create_table :request_for_comments do |t|
t.integer :requestorid, :null => false
t.integer :exerciseid, :null => false
t.integer :fileid, :null => false
t.timestamp :requested_at
t.timestamps
end
end
end

View File

@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20150317115338) do
ActiveRecord::Schema.define(version: 20150327141740) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -165,6 +165,15 @@ ActiveRecord::Schema.define(version: 20150317115338) do
add_index "internal_users_teams", ["internal_user_id"], name: "index_internal_users_teams_on_internal_user_id", using: :btree
add_index "internal_users_teams", ["team_id"], name: "index_internal_users_teams_on_team_id", using: :btree
create_table "request_for_comments", force: true do |t|
t.integer "requestorid", null: false
t.integer "exerciseid", null: false
t.integer "fileid", null: false
t.datetime "requested_at"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "submissions", force: true do |t|
t.integer "exercise_id"
t.float "score"

View File

@ -5,11 +5,7 @@
var buildFlash = function(options) {
if (options.text) {
var container = options.container;
var html = '';
if (options.icon) {
html += '<i class="' + options.icon.join(' ') + '">&nbsp;';
}
html += options.text;
var html = (options.icon ? '<i class="' + options.icon.join(' ') + '"></i>' : '') + options.text;
container.html(html);
showFlashes();
}