Merge pull request #50 from openHPI/request-for-comments-enhancements
Request for comments enhancements
This commit is contained in:
@ -13,6 +13,7 @@ $(function() {
|
|||||||
var THEME = 'ace/theme/textmate';
|
var THEME = 'ace/theme/textmate';
|
||||||
var REMEMBER_TAB = false;
|
var REMEMBER_TAB = false;
|
||||||
var AUTOSAVE_INTERVAL = 15 * 1000;
|
var AUTOSAVE_INTERVAL = 15 * 1000;
|
||||||
|
var REQUEST_FOR_COMMENTS_DELAY = 3 * 60 * 1000;
|
||||||
var NONE = 0;
|
var NONE = 0;
|
||||||
var WEBSOCKET = 1;
|
var WEBSOCKET = 1;
|
||||||
var SERVER_SEND_EVENT = 2;
|
var SERVER_SEND_EVENT = 2;
|
||||||
@ -110,18 +111,6 @@ $(function() {
|
|||||||
|
|
||||||
var createSubmission = function(initiator, filter, callback) {
|
var createSubmission = function(initiator, filter, callback) {
|
||||||
showSpinner(initiator);
|
showSpinner(initiator);
|
||||||
|
|
||||||
var annotations_arr = [];
|
|
||||||
|
|
||||||
$('.editor').each(function(index, element) {
|
|
||||||
var editor = ace.edit(element);
|
|
||||||
var cleaned_annotations = editor.getSession().getAnnotations();
|
|
||||||
for(var i = cleaned_annotations.length-1; i>=0; --i){
|
|
||||||
cleaned_annotations[i].text = cleaned_annotations[i].text.replace(cleaned_annotations[i].username + ": ", "");
|
|
||||||
}
|
|
||||||
annotations_arr = annotations_arr.concat(editor.getSession().getAnnotations());
|
|
||||||
});
|
|
||||||
|
|
||||||
var jqxhr = ajax({
|
var jqxhr = ajax({
|
||||||
data: {
|
data: {
|
||||||
submission: {
|
submission: {
|
||||||
@ -129,7 +118,7 @@ $(function() {
|
|||||||
exercise_id: $('#editor').data('exercise-id'),
|
exercise_id: $('#editor').data('exercise-id'),
|
||||||
files_attributes: (filter || _.identity)(collectFiles())
|
files_attributes: (filter || _.identity)(collectFiles())
|
||||||
},
|
},
|
||||||
annotations_arr: annotations_arr
|
annotations_arr: []
|
||||||
},
|
},
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -169,7 +158,6 @@ $(function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setAnnotations(editors[i], $(editors[i].container).data('id'));
|
|
||||||
}
|
}
|
||||||
// toggle button states (it might be the case that the request for comments button has to be enabled
|
// toggle button states (it might be the case that the request for comments button has to be enabled
|
||||||
toggleButtonStates();
|
toggleButtonStates();
|
||||||
@ -256,8 +244,6 @@ $(function() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var getPanelClass = function(result) {
|
var getPanelClass = function(result) {
|
||||||
if (result.stderr && !result.score) {
|
if (result.stderr && !result.score) {
|
||||||
return 'panel-danger';
|
return 'panel-danger';
|
||||||
@ -425,7 +411,6 @@ $(function() {
|
|||||||
session.setUseWrapMode(true);
|
session.setUseWrapMode(true);
|
||||||
|
|
||||||
var file_id = $(element).data('id');
|
var file_id = $(element).data('id');
|
||||||
//setAnnotations(editor, file_id);
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Register event handlers
|
* Register event handlers
|
||||||
@ -434,17 +419,6 @@ $(function() {
|
|||||||
// editor itself
|
// editor itself
|
||||||
editor.on("paste", handlePasteEvent);
|
editor.on("paste", handlePasteEvent);
|
||||||
editor.on("copy", handleCopyEvent);
|
editor.on("copy", handleCopyEvent);
|
||||||
editor.on("guttermousedown", handleSidebarClick);
|
|
||||||
|
|
||||||
/* // alternative:
|
|
||||||
editor.on("guttermousedown", function(e) {
|
|
||||||
handleSidebarClick(e);
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
//session
|
|
||||||
session.on('annotationRemoval', handleAnnotationRemoval);
|
|
||||||
session.on('annotationChange', handleAnnotationChange);
|
|
||||||
|
|
||||||
// listener for autosave
|
// listener for autosave
|
||||||
session.on("change", function (deltaObject) {
|
session.on("change", function (deltaObject) {
|
||||||
@ -453,158 +427,6 @@ $(function() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
var hasCommentsInRow = function (editor, row){
|
|
||||||
return editor.getSession().getAnnotations().some(function(element) {
|
|
||||||
return element.row === row;
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
var getCommentsForRow = function (editor, row){
|
|
||||||
return editor.getSession().getAnnotations().filter(function(element) {
|
|
||||||
return element.row === row;
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
var setAnnotations = function (editor, file_id){
|
|
||||||
var session = editor.getSession();
|
|
||||||
var url = "/comments";
|
|
||||||
|
|
||||||
var jqrequest = $.ajax({
|
|
||||||
dataType: 'json',
|
|
||||||
method: 'GET',
|
|
||||||
url: url,
|
|
||||||
data: {
|
|
||||||
file_id: file_id
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
jqrequest.done(function(response){
|
|
||||||
setAnnotationsCallback(response, session);
|
|
||||||
});
|
|
||||||
jqrequest.fail(ajaxError);
|
|
||||||
};
|
|
||||||
|
|
||||||
var setAnnotationsCallback = function (response, session) {
|
|
||||||
var annotations = response;
|
|
||||||
|
|
||||||
// add classname and the username in front of each comment
|
|
||||||
$.each(annotations, function(index, comment){
|
|
||||||
comment.className = "code-ocean_comment";
|
|
||||||
comment.text = comment.username + ": " + comment.text;
|
|
||||||
});
|
|
||||||
|
|
||||||
session.setAnnotations(annotations);
|
|
||||||
}
|
|
||||||
|
|
||||||
var deleteComment = function (file_id, row, editor) {
|
|
||||||
var jqxhr = $.ajax({
|
|
||||||
type: 'DELETE',
|
|
||||||
url: "/comments",
|
|
||||||
data: {
|
|
||||||
row: row,
|
|
||||||
file_id: file_id }
|
|
||||||
});
|
|
||||||
jqxhr.done(function (response) {
|
|
||||||
setAnnotations(editor, file_id);
|
|
||||||
});
|
|
||||||
jqxhr.fail(ajaxError);
|
|
||||||
}
|
|
||||||
|
|
||||||
var createComment = function (file_id, row, editor, commenttext){
|
|
||||||
var jqxhr = $.ajax({
|
|
||||||
data: {
|
|
||||||
comment: {
|
|
||||||
file_id: file_id,
|
|
||||||
row: row,
|
|
||||||
column: 0,
|
|
||||||
text: commenttext
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dataType: 'json',
|
|
||||||
method: 'POST',
|
|
||||||
url: "/comments"
|
|
||||||
});
|
|
||||||
jqxhr.done(function(response){
|
|
||||||
setAnnotations(editor, file_id);
|
|
||||||
});
|
|
||||||
jqxhr.fail(ajaxError);
|
|
||||||
};
|
|
||||||
|
|
||||||
var handleAnnotationRemoval = function(removedAnnotations) {
|
|
||||||
removedAnnotations.forEach(function(annotation) {
|
|
||||||
$.ajax({
|
|
||||||
method: 'DELETE',
|
|
||||||
url: '/comment_by_id',
|
|
||||||
data: {
|
|
||||||
id: annotation.id,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
var handleAnnotationChange = function(changedAnnotations) {
|
|
||||||
changedAnnotations.forEach(function(annotation) {
|
|
||||||
$.ajax({
|
|
||||||
method: 'PUT',
|
|
||||||
url: '/comments',
|
|
||||||
data: {
|
|
||||||
id: annotation.id,
|
|
||||||
user_id: $('#editor').data('user-id'),
|
|
||||||
comment: {
|
|
||||||
row: annotation.row,
|
|
||||||
text: annotation.text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
// Code for clicks on gutter / sidepanel
|
|
||||||
var handleSidebarClick = function(e) {
|
|
||||||
var target = e.domEvent.target;
|
|
||||||
var editor = e.editor;
|
|
||||||
|
|
||||||
if (target.className.indexOf("ace_gutter-cell") == -1) return;
|
|
||||||
if (!editor.isFocused()) return;
|
|
||||||
if (e.clientX > 25 + target.getBoundingClientRect().left) return;
|
|
||||||
|
|
||||||
var row = e.getDocumentPosition().row;
|
|
||||||
e.stop();
|
|
||||||
|
|
||||||
var commentModal = $('#comment-modal');
|
|
||||||
|
|
||||||
if (hasCommentsInRow(editor, row)) {
|
|
||||||
var rowComments = getCommentsForRow(editor, row);
|
|
||||||
var comments = _.pluck(rowComments, 'text').join('\n');
|
|
||||||
commentModal.find('#other-comments').text(comments);
|
|
||||||
} else {
|
|
||||||
commentModal.find('#other-comments').text('none');
|
|
||||||
}
|
|
||||||
|
|
||||||
commentModal.find('#addCommentButton').off('click');
|
|
||||||
commentModal.find('#removeAllButton').off('click');
|
|
||||||
|
|
||||||
commentModal.find('#addCommentButton').on('click', function(e){
|
|
||||||
var commenttext = commentModal.find('textarea').val();
|
|
||||||
// attention: use id of data attribute here, not file-id (file-id is the original file)
|
|
||||||
var file_id = $(editor.container).data('id');
|
|
||||||
|
|
||||||
if (commenttext !== "") {
|
|
||||||
createComment(file_id, row, editor, commenttext);
|
|
||||||
commentModal.modal('hide');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
commentModal.find('#removeAllButton').on('click', function(e){
|
|
||||||
// attention: use id of data attribute here, not file-id (file-id is the original file)
|
|
||||||
var file_id = $(editor.container).data('id');
|
|
||||||
deleteComment(file_id,row, editor);
|
|
||||||
commentModal.modal('hide');
|
|
||||||
});
|
|
||||||
|
|
||||||
commentModal.modal('show');
|
|
||||||
};
|
|
||||||
|
|
||||||
var initializeEventHandlers = function() {
|
var initializeEventHandlers = function() {
|
||||||
$(document).on('click', '#results a', showOutput);
|
$(document).on('click', '#results a', showOutput);
|
||||||
$(document).on('keypress', handleKeyPress);
|
$(document).on('keypress', handleKeyPress);
|
||||||
@ -612,6 +434,7 @@ $(function() {
|
|||||||
initializeFileTreeButtons();
|
initializeFileTreeButtons();
|
||||||
initializeWorkflowButtons();
|
initializeWorkflowButtons();
|
||||||
initializeWorkspaceButtons();
|
initializeWorkspaceButtons();
|
||||||
|
initializeRequestForComments()
|
||||||
};
|
};
|
||||||
|
|
||||||
var initializeFileTree = function() {
|
var initializeFileTree = function() {
|
||||||
@ -654,6 +477,20 @@ $(function() {
|
|||||||
$('#start-over').on('click', confirmReset);
|
$('#start-over').on('click', confirmReset);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var initializeRequestForComments = function () {
|
||||||
|
var button = $('#requestCommentsButton');
|
||||||
|
button.hide();
|
||||||
|
button.on('click', function() {
|
||||||
|
$('#comment-modal').modal('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#askForCommentsButton').on('click', requestComments);
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
button.fadeIn();
|
||||||
|
}, REQUEST_FOR_COMMENTS_DELAY);
|
||||||
|
};
|
||||||
|
|
||||||
var isActiveFileBinary = function() {
|
var isActiveFileBinary = function() {
|
||||||
return 'binary' in active_frame.data();
|
return 'binary' in active_frame.data();
|
||||||
};
|
};
|
||||||
@ -667,7 +504,7 @@ $(function() {
|
|||||||
filename: filename,
|
filename: filename,
|
||||||
id: fileId
|
id: fileId
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
var isActiveFileRenderable = function() {
|
var isActiveFileRenderable = function() {
|
||||||
return 'renderable' in active_frame.data();
|
return 'renderable' in active_frame.data();
|
||||||
@ -1125,7 +962,6 @@ $(function() {
|
|||||||
$('#run').toggle(isActiveFileRunnable() && !running);
|
$('#run').toggle(isActiveFileRunnable() && !running);
|
||||||
$('#stop').toggle(isActiveFileStoppable());
|
$('#stop').toggle(isActiveFileStoppable());
|
||||||
$('#test').toggle(isActiveFileTestable());
|
$('#test').toggle(isActiveFileTestable());
|
||||||
$('#request-for-comments').toggle(isActiveFileSubmission() && !isActiveFileBinary());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var initWebsocketConnection = function(url) {
|
var initWebsocketConnection = function(url) {
|
||||||
@ -1308,10 +1144,11 @@ $(function() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var requestComments = function(e) {
|
var requestComments = function() {
|
||||||
var user_id = $('#editor').data('user-id')
|
var user_id = $('#editor').data('user-id')
|
||||||
var exercise_id = $('#editor').data('exercise-id')
|
var exercise_id = $('#editor').data('exercise-id')
|
||||||
var file_id = $('.editor').data('id')
|
var file_id = $('.editor').data('id')
|
||||||
|
var question = $('#question').val();
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -1320,6 +1157,7 @@ $(function() {
|
|||||||
request_for_comment: {
|
request_for_comment: {
|
||||||
exercise_id: exercise_id,
|
exercise_id: exercise_id,
|
||||||
file_id: file_id,
|
file_id: file_id,
|
||||||
|
question: question,
|
||||||
"requested_at(1i)": 2015, // these are the timestamp values that the request handler demands
|
"requested_at(1i)": 2015, // these are the timestamp values that the request handler demands
|
||||||
"requested_at(2i)":3, // they could be random here, because the timestamp is updated on serverside anyway
|
"requested_at(2i)":3, // they could be random here, because the timestamp is updated on serverside anyway
|
||||||
"requested_at(3i)":27,
|
"requested_at(3i)":27,
|
||||||
@ -1328,13 +1166,13 @@ $(function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).done(function() {
|
}).done(function() {
|
||||||
hideSpinner()
|
hideSpinner();
|
||||||
$.flash.success({ text: 'Request for comments sent!' })
|
$.flash.success({ text: $('#askForCommentsButton').data('message-success') })
|
||||||
})
|
}).error(ajaxError);
|
||||||
|
|
||||||
showSpinner($('#request-for-comments'))
|
$('#comment-modal').modal('hide');
|
||||||
// hide button until next submission is created
|
var button = $('#requestCommentsButton');
|
||||||
$('#request-for-comments').toggle(false);
|
button.fadeOut();
|
||||||
}
|
}
|
||||||
|
|
||||||
var initializeCodePilot = function() {
|
var initializeCodePilot = function() {
|
||||||
|
@ -88,3 +88,10 @@ button i.fa-spin {
|
|||||||
color: #777;
|
color: #777;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#requestCommentsButton {
|
||||||
|
position: relative;
|
||||||
|
margin-top: -50px;
|
||||||
|
margin-right: 25px;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
@ -41,7 +41,7 @@ class CommentsController < ApplicationController
|
|||||||
# if the user is internal, set the name
|
# if the user is internal, set the name
|
||||||
|
|
||||||
@comments.map{|comment|
|
@comments.map{|comment|
|
||||||
comment.username = comment.user.name
|
comment.username = comment.user.displayname
|
||||||
# alternative: # if the user is external, fetch the displayname from xikolo
|
# alternative: # if the user is external, fetch the displayname from xikolo
|
||||||
# Xikolo::UserClient.get(comment.user_id.to_s)[:display_name]
|
# Xikolo::UserClient.get(comment.user_id.to_s)[:display_name]
|
||||||
}
|
}
|
||||||
@ -111,14 +111,13 @@ class CommentsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@comments = Comment.where(file_id: params[:file_id], row: params[:row])
|
@comments = Comment.where(file_id: params[:file_id], row: params[:row], user: current_user)
|
||||||
@comments.delete_all
|
@comments.each { |comment| authorize comment; comment.destroy }
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
#format.html { redirect_to comments_url, notice: 'Comments were successfully destroyed.' }
|
#format.html { redirect_to comments_url, notice: 'Comments were successfully destroyed.' }
|
||||||
format.html { head :no_content, notice: 'Comments were successfully destroyed.' }
|
format.html { head :no_content, notice: 'Comments were successfully destroyed.' }
|
||||||
format.json { head :no_content }
|
format.json { head :no_content }
|
||||||
end
|
end
|
||||||
authorize!
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -70,6 +70,6 @@ class RequestForCommentsController < ApplicationController
|
|||||||
|
|
||||||
# Never trust parameters from the scary internet, only allow the white list through.
|
# Never trust parameters from the scary internet, only allow the white list through.
|
||||||
def request_for_comment_params
|
def request_for_comment_params
|
||||||
params.require(:request_for_comment).permit(:exercise_id, :file_id, :requested_at).merge(user_id: current_user.id, user_type: current_user.class.name)
|
params.require(:request_for_comment).permit(:exercise_id, :file_id, :question, :requested_at).merge(user_id: current_user.id, user_type: current_user.class.name)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -3,4 +3,13 @@ class ExternalUser < ActiveRecord::Base
|
|||||||
|
|
||||||
validates :consumer_id, presence: true
|
validates :consumer_id, presence: true
|
||||||
validates :external_id, presence: true
|
validates :external_id, presence: true
|
||||||
|
|
||||||
|
def displayname
|
||||||
|
result = name
|
||||||
|
if(consumer.name == 'openHPI')
|
||||||
|
result = Xikolo::UserClient.get(external_id.to_s)[:display_name]
|
||||||
|
end
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -21,4 +21,9 @@ class InternalUser < ActiveRecord::Base
|
|||||||
def teacher?
|
def teacher?
|
||||||
role == 'teacher'
|
role == 'teacher'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def displayname
|
||||||
|
name
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -25,6 +25,10 @@ class RequestForComment < ActiveRecord::Base
|
|||||||
limit 1").first
|
limit 1").first
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
"RFC-" + self.id.to_s
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def self.row_number_user_sql
|
def self.row_number_user_sql
|
||||||
select("id, user_id, exercise_id, file_id, requested_at, created_at, updated_at, user_type, row_number() OVER (PARTITION BY user_id ORDER BY created_at DESC) as row_number").to_sql
|
select("id, user_id, exercise_id, file_id, requested_at, created_at, updated_at, user_type, row_number() OVER (PARTITION BY user_id ORDER BY created_at DESC) as row_number").to_sql
|
||||||
|
@ -1,19 +1,17 @@
|
|||||||
- if current_user
|
- if current_user
|
||||||
- if current_user.internal_user?
|
|
||||||
li.dropdown
|
li.dropdown
|
||||||
a.dropdown-toggle data-toggle='dropdown' href='#'
|
a.dropdown-toggle data-toggle='dropdown' href='#'
|
||||||
i.fa.fa-user
|
i.fa.fa-user
|
||||||
= current_user
|
= current_user
|
||||||
span.caret
|
span.caret
|
||||||
ul.dropdown-menu role='menu'
|
ul.dropdown-menu role='menu'
|
||||||
|
- if current_user.internal_user?
|
||||||
li = link_to(t('consumers.show.link'), current_user.consumer) if current_user.consumer
|
li = link_to(t('consumers.show.link'), current_user.consumer) if current_user.consumer
|
||||||
li = link_to(t('internal_users.show.link'), current_user)
|
li = link_to(t('internal_users.show.link'), current_user)
|
||||||
|
li = link_to(t('request_for_comments.index.all'), request_for_comments_path)
|
||||||
|
li = link_to(t('request_for_comments.index.get_my_comment_requests'), my_request_for_comments_path)
|
||||||
|
- if current_user.internal_user?
|
||||||
li = link_to(t('sessions.destroy.link'), sign_out_path, method: :delete)
|
li = link_to(t('sessions.destroy.link'), sign_out_path, method: :delete)
|
||||||
- else
|
|
||||||
li
|
|
||||||
p.navbar-text
|
|
||||||
i.fa.fa-user
|
|
||||||
= current_user
|
|
||||||
- else
|
- else
|
||||||
li = link_to(sign_in_path) do
|
li = link_to(sign_in_path) do
|
||||||
i.fa.fa-sign-in
|
i.fa.fa-sign-in
|
||||||
|
@ -38,4 +38,4 @@
|
|||||||
= t('exercises.editor.test')
|
= t('exercises.editor.test')
|
||||||
= render('editor_button', data: {:'data-placement' => 'top', :'data-tooltip' => true}, icon: 'fa fa-trophy', id: 'assess', label: t('exercises.editor.score'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + s'))
|
= render('editor_button', data: {:'data-placement' => 'top', :'data-tooltip' => true}, icon: 'fa fa-trophy', id: 'assess', label: t('exercises.editor.score'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + s'))
|
||||||
|
|
||||||
= render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.dialogtitle'), template: 'exercises/_comment_dialogcontent')
|
= render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent')
|
@ -5,6 +5,5 @@ 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-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-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-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: t('exercises.editor.requestComments'))
|
|
||||||
|
|
||||||
= render('shared/modal', id: 'modal-file', template: 'code_ocean/files/_form', title: t('exercises.editor.create_file'))
|
= render('shared/modal', id: 'modal-file', template: 'code_ocean/files/_form', title: t('exercises.editor.create_file'))
|
||||||
|
@ -13,3 +13,7 @@
|
|||||||
- else
|
- else
|
||||||
.editor-content.hidden data-file-id=file.ancestor_id = 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 data-id=file.id
|
.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 data-id=file.id
|
||||||
|
|
||||||
|
button.btn.btn-primary id='requestCommentsButton' type='button'
|
||||||
|
i.fa.fa-comment-o
|
||||||
|
= t('exercises.editor.requestComments')
|
@ -0,0 +1,4 @@
|
|||||||
|
h5 = t('exercises.implement.comment.question')
|
||||||
|
textarea.form-control#question(style='resize:none;')
|
||||||
|
p = ''
|
||||||
|
button#askForCommentsButton.btn.btn-block.btn-primary(type='button' data-message-success=t('exercises.editor.request_for_comments_sent')) =t('exercises.implement.comment.request')
|
@ -1,7 +1,7 @@
|
|||||||
h1 = RequestForComment.model_name.human(count: 2)
|
h1 = RequestForComment.model_name.human(count: 2)
|
||||||
|
|
||||||
.table-responsive
|
.table-responsive
|
||||||
table.table
|
table.table.sortable
|
||||||
thead
|
thead
|
||||||
tr
|
tr
|
||||||
th = t('activerecord.attributes.request_for_comments.exercise')
|
th = t('activerecord.attributes.request_for_comments.exercise')
|
||||||
@ -13,7 +13,7 @@ h1 = RequestForComment.model_name.human(count: 2)
|
|||||||
tr data-id=request_for_comment.id
|
tr data-id=request_for_comment.id
|
||||||
td = link_to(request_for_comment.exercise.title, request_for_comment)
|
td = link_to(request_for_comment.exercise.title, request_for_comment)
|
||||||
td = request_for_comment.exercise.execution_environment
|
td = request_for_comment.exercise.execution_environment
|
||||||
td = request_for_comment.user.name
|
td = request_for_comment.user.displayname
|
||||||
td = request_for_comment.requested_at
|
td = request_for_comment.requested_at
|
||||||
|
|
||||||
= render('shared/pagination', collection: @request_for_comments)
|
= render('shared/pagination', collection: @request_for_comments)
|
@ -14,6 +14,13 @@
|
|||||||
%>
|
%>
|
||||||
<%= user %> | <%= @request_for_comment.requested_at %>
|
<%= user %> | <%= @request_for_comment.requested_at %>
|
||||||
</p>
|
</p>
|
||||||
|
<h5>
|
||||||
|
<% if @request_for_comment.question and not @request_for_comment.question == '' %>
|
||||||
|
<%= t('activerecord.attributes.request_for_comments.question')%>: "<%= @request_for_comment.question %>"
|
||||||
|
<% else %>
|
||||||
|
<%= t('request_for_comments.no_question') %>
|
||||||
|
<% end %>
|
||||||
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
@ -25,27 +32,22 @@ do not put a carriage return in the line below. it will be present in the presen
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<%= render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.dialogtitle'), template: 'exercises/_comment_dialogcontent') %>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var commentitor = $('.editor');
|
var commentitor = $('.editor');
|
||||||
var userid = commentitor.data('user-id');
|
var userid = commentitor.data('user-id');
|
||||||
|
|
||||||
commentitor.each(function (index, editor) {
|
commentitor.each(function (index, editor) {
|
||||||
currentEditor = ace.edit(editor);
|
var currentEditor = ace.edit(editor);
|
||||||
currentEditor.setReadOnly(true);
|
currentEditor.setReadOnly(true);
|
||||||
|
|
||||||
setAnnotations(currentEditor, $(editor).data('file-id'));
|
setAnnotations(currentEditor, $(editor).data('file-id'));
|
||||||
|
currentEditor.on("guttermousedown", handleSidebarClick);
|
||||||
});
|
});
|
||||||
|
|
||||||
function setAnnotations(editor, fileid) {
|
function setAnnotations(editor, fileid) {
|
||||||
var session = editor.getSession()
|
var session = editor.getSession();
|
||||||
|
|
||||||
var inputHtml = ''
|
|
||||||
inputHtml += '<div class="input-group">'
|
|
||||||
inputHtml += '<input type="text" class="form-control" id="commentInput"'
|
|
||||||
inputHtml += 'placeholder="I\'d suggest a variable here" required>'
|
|
||||||
inputHtml += '<span class="input-group-btn"><button id="submitComment"'
|
|
||||||
inputHtml += 'class="btn btn-default"><%= t("exercises.implement.comment.addComment") %>!</button></span>'
|
|
||||||
inputHtml += '</div>'
|
|
||||||
|
|
||||||
var jqrequest = $.ajax({
|
var jqrequest = $.ajax({
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
@ -58,57 +60,109 @@ do not put a carriage return in the line below. it will be present in the presen
|
|||||||
|
|
||||||
jqrequest.done(function(response){
|
jqrequest.done(function(response){
|
||||||
$.each(response, function(index, comment) {
|
$.each(response, function(index, comment) {
|
||||||
comment.className = "code-ocean_comment"
|
comment.className = "code-ocean_comment";
|
||||||
comment.text = comment.username + ": " + comment.text
|
comment.text = comment.username + ": " + comment.text
|
||||||
})
|
});
|
||||||
|
|
||||||
editor.getSession().setAnnotations(response)
|
session.setAnnotations(response);
|
||||||
|
|
||||||
$(editor.container).find('.ace_gutter-cell').popover({
|
|
||||||
title: 'Add a comment',
|
|
||||||
html: true,
|
|
||||||
content: inputHtml,
|
|
||||||
position: 'right',
|
|
||||||
trigger: 'focus click'
|
|
||||||
}).on('shown.bs.popover', function() {
|
|
||||||
var container = $(editor.container)
|
|
||||||
container.find('#commentInput').focus()
|
|
||||||
container.find('#submitComment').click(_.partial(addComment, editor, fileid));
|
|
||||||
container.find('#commentInput').data('line', $(this).text())
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function addComment(editor, fileid) {
|
function hasCommentsInRow(editor, row){
|
||||||
var commentInput = $(editor.container).find('#commentInput');
|
return editor.getSession().getAnnotations().some(function(element) {
|
||||||
var comment = commentInput.val()
|
return element.row === row;
|
||||||
var line = commentInput.data('line')
|
})
|
||||||
|
|
||||||
if (line == '' || comment == '') {
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
line = parseInt(line) - 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$.ajax({
|
function getCommentsForRow(editor, row){
|
||||||
|
return editor.getSession().getAnnotations().filter(function(element) {
|
||||||
|
return element.row === row;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteComment(file_id, row, editor) {
|
||||||
|
var jqxhr = $.ajax({
|
||||||
|
type: 'DELETE',
|
||||||
|
url: "/comments",
|
||||||
|
data: {
|
||||||
|
row: row,
|
||||||
|
file_id: file_id }
|
||||||
|
});
|
||||||
|
jqxhr.done(function (response) {
|
||||||
|
setAnnotations(editor, file_id);
|
||||||
|
});
|
||||||
|
jqxhr.fail(ajaxError);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createComment(file_id, row, editor, commenttext){
|
||||||
|
var jqxhr = $.ajax({
|
||||||
data: {
|
data: {
|
||||||
comment: {
|
comment: {
|
||||||
user_id: userid,
|
file_id: file_id,
|
||||||
file_id: fileid,
|
row: row,
|
||||||
row: line,
|
|
||||||
column: 0,
|
column: 0,
|
||||||
text: comment
|
text: commenttext
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: "/comments"
|
url: "/comments"
|
||||||
}).done(setAnnotations(editor, fileid))
|
});
|
||||||
|
jqxhr.done(function(response){
|
||||||
$('.ace_gutter-cell').popover('hide')
|
setAnnotations(editor, file_id);
|
||||||
|
});
|
||||||
|
jqxhr.fail(ajaxError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleSidebarClick(e) {
|
||||||
|
var target = e.domEvent.toElement;
|
||||||
|
var editor = e.editor;
|
||||||
|
|
||||||
|
if (target.className.indexOf("ace_gutter-cell") == -1) return;
|
||||||
|
//if (!editor.isFocused()) return;
|
||||||
|
//if (e.clientX > 25 + target.getBoundingClientRect().left) return;
|
||||||
|
|
||||||
|
|
||||||
|
var row = e.getDocumentPosition().row;
|
||||||
|
e.stop();
|
||||||
|
|
||||||
|
var commentModal = $('#comment-modal');
|
||||||
|
|
||||||
|
if (hasCommentsInRow(editor, row)) {
|
||||||
|
var rowComments = getCommentsForRow(editor, row);
|
||||||
|
var comments = _.pluck(rowComments, 'text').join('\n');
|
||||||
|
commentModal.find('#other-comments').text(comments);
|
||||||
|
} else {
|
||||||
|
commentModal.find('#other-comments').text('none');
|
||||||
|
}
|
||||||
|
|
||||||
|
commentModal.find('#addCommentButton').off('click');
|
||||||
|
commentModal.find('#removeAllButton').off('click');
|
||||||
|
|
||||||
|
commentModal.find('#addCommentButton').on('click', function(e){
|
||||||
|
var commenttext = commentModal.find('textarea').val();
|
||||||
|
var file_id = $(editor.container).data('file-id');
|
||||||
|
|
||||||
|
if (commenttext !== "") {
|
||||||
|
createComment(file_id, row, editor, commenttext);
|
||||||
|
commentModal.modal('hide');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
commentModal.find('#removeAllButton').on('click', function(e){
|
||||||
|
var file_id = $(editor.container).data('file-id');
|
||||||
|
deleteComment(file_id, row, editor);
|
||||||
|
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')
|
||||||
|
});
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
<style>
|
|
||||||
#commentitor, .ace_gutter, .ace_gutter-layer { overflow: visible }
|
|
||||||
.popover { width: 400px; max-width: none; }
|
|
||||||
</style>
|
|
||||||
|
@ -81,6 +81,7 @@ de:
|
|||||||
execution_environment: Sprache
|
execution_environment: Sprache
|
||||||
username: Benutzername
|
username: Benutzername
|
||||||
requested_at: Angefragezeitpunkt
|
requested_at: Angefragezeitpunkt
|
||||||
|
question: "Frage"
|
||||||
submission:
|
submission:
|
||||||
cause: Anlass
|
cause: Anlass
|
||||||
code: Code
|
code: Code
|
||||||
@ -209,6 +210,7 @@ de:
|
|||||||
timeout: 'Ausführung gestoppt. Ihr Code hat die erlaubte Ausführungszeit von %{permitted_execution_time} Sekunden überschritten.'
|
timeout: 'Ausführung gestoppt. Ihr Code hat die erlaubte Ausführungszeit von %{permitted_execution_time} Sekunden überschritten.'
|
||||||
tooltips:
|
tooltips:
|
||||||
save: Ihr Code wird automatisch gespeichert, wann immer Sie eine Datei herunterladen, ausführen oder testen. Explizites Speichern ist also selten notwendig.
|
save: Ihr Code wird automatisch gespeichert, wann immer Sie eine Datei herunterladen, ausführen oder testen. Explizites Speichern ist also selten notwendig.
|
||||||
|
request_for_comments_sent: "Kommentaranfrage gesendet."
|
||||||
editor_file_tree:
|
editor_file_tree:
|
||||||
file_root: Dateien
|
file_root: Dateien
|
||||||
file_form:
|
file_form:
|
||||||
@ -243,8 +245,10 @@ de:
|
|||||||
others: Andere Kommentare auf dieser Zeile
|
others: Andere Kommentare auf dieser Zeile
|
||||||
addyours: Fügen Sie Ihren Kommentar hinzu
|
addyours: Fügen Sie Ihren Kommentar hinzu
|
||||||
addComment: Kommentieren
|
addComment: Kommentieren
|
||||||
removeAllOnLine: Alle Kommentare auf dieser Zeile löschen
|
removeAllOnLine: Meine Kommentare auf dieser Zeile löschen
|
||||||
listing: Die neuesten Kommentaranfragen
|
listing: Die neuesten Kommentaranfragen
|
||||||
|
request: "Kommentaranfrage stellen"
|
||||||
|
question: "Bitte beschreiben Sie kurz ihre Problem oder nennen Sie den Programmteil, zu dem sie Feedback wünschen."
|
||||||
index:
|
index:
|
||||||
clone: Duplizieren
|
clone: Duplizieren
|
||||||
implement: Implementieren
|
implement: Implementieren
|
||||||
@ -322,6 +326,8 @@ de:
|
|||||||
request_for_comments:
|
request_for_comments:
|
||||||
index:
|
index:
|
||||||
get_my_comment_requests: Meine Kommentaranfragen
|
get_my_comment_requests: Meine Kommentaranfragen
|
||||||
|
all: "Alle Kommentaranfragen"
|
||||||
|
no_question: "Der Autor hat keine Frage zu dieser Anfrage gestellt."
|
||||||
sessions:
|
sessions:
|
||||||
create:
|
create:
|
||||||
failure: Fehlerhafte E-Mail oder Passwort.
|
failure: Fehlerhafte E-Mail oder Passwort.
|
||||||
|
@ -81,6 +81,7 @@ en:
|
|||||||
execution_environment: Language
|
execution_environment: Language
|
||||||
username: Username
|
username: Username
|
||||||
requested_at: Request Date
|
requested_at: Request Date
|
||||||
|
question: "Question"
|
||||||
submission:
|
submission:
|
||||||
cause: Cause
|
cause: Cause
|
||||||
code: Code
|
code: Code
|
||||||
@ -209,6 +210,7 @@ en:
|
|||||||
timeout: 'Execution stopped. Your code exceeded the permitted execution time of %{permitted_execution_time} seconds.'
|
timeout: 'Execution stopped. Your code exceeded the permitted execution time of %{permitted_execution_time} seconds.'
|
||||||
tooltips:
|
tooltips:
|
||||||
save: Your code is automatically saved whenever you download, run, or test it. Therefore, explicitly saving is rarely necessary.
|
save: Your code is automatically saved whenever you download, run, or test it. Therefore, explicitly saving is rarely necessary.
|
||||||
|
request_for_comments_sent: "Request for comments sent."
|
||||||
editor_file_tree:
|
editor_file_tree:
|
||||||
file_root: Files
|
file_root: Files
|
||||||
file_form:
|
file_form:
|
||||||
@ -243,8 +245,10 @@ en:
|
|||||||
others: Other comments on this line
|
others: Other comments on this line
|
||||||
addyours: Add your comment
|
addyours: Add your comment
|
||||||
addComment: Comment this
|
addComment: Comment this
|
||||||
removeAllOnLine: Remove all comments on this line
|
removeAllOnLine: Remove my comments on this line
|
||||||
listing: Listing the newest comment requests
|
listing: Listing the newest comment requests
|
||||||
|
request: "Request Comments"
|
||||||
|
question: "Please shortly describe your problem or the program part you would like to get feedback for."
|
||||||
index:
|
index:
|
||||||
clone: Duplicate
|
clone: Duplicate
|
||||||
implement: Implement
|
implement: Implement
|
||||||
@ -321,7 +325,9 @@ en:
|
|||||||
subject: Password reset instructions
|
subject: Password reset instructions
|
||||||
request_for_comments:
|
request_for_comments:
|
||||||
index:
|
index:
|
||||||
|
all: All Requests for Comments
|
||||||
get_my_comment_requests: My Requests for Comments
|
get_my_comment_requests: My Requests for Comments
|
||||||
|
no_question: "The author did not enter a question for this request."
|
||||||
sessions:
|
sessions:
|
||||||
create:
|
create:
|
||||||
failure: Invalid email or password.
|
failure: Invalid email or password.
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
class AddQuestionToRequestForComments < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
add_column :request_for_comments, :question, :text
|
||||||
|
end
|
||||||
|
end
|
@ -11,7 +11,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 20160302133540) do
|
ActiveRecord::Schema.define(version: 20160426114951) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
@ -190,6 +190,7 @@ ActiveRecord::Schema.define(version: 20160302133540) do
|
|||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
t.datetime "updated_at"
|
t.datetime "updated_at"
|
||||||
t.string "user_type"
|
t.string "user_type"
|
||||||
|
t.text "question"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "submissions", force: true do |t|
|
create_table "submissions", force: true do |t|
|
||||||
|
Reference in New Issue
Block a user