Merge branch 'master' into john-graph

# Conflicts:
#	Gemfile
This commit is contained in:
John Geiger
2016-05-12 16:02:38 +02:00
36 changed files with 322 additions and 316 deletions

View File

@ -13,6 +13,7 @@ $(function() {
var THEME = 'ace/theme/textmate';
var REMEMBER_TAB = false;
var AUTOSAVE_INTERVAL = 15 * 1000;
var REQUEST_FOR_COMMENTS_DELAY = 3 * 60 * 1000;
var NONE = 0;
var WEBSOCKET = 1;
var SERVER_SEND_EVENT = 2;
@ -110,18 +111,6 @@ $(function() {
var createSubmission = function(initiator, filter, callback) {
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({
data: {
submission: {
@ -129,7 +118,7 @@ $(function() {
exercise_id: $('#editor').data('exercise-id'),
files_attributes: (filter || _.identity)(collectFiles())
},
annotations_arr: annotations_arr
annotations_arr: []
},
dataType: 'json',
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
toggleButtonStates();
@ -256,8 +244,6 @@ $(function() {
}
};
var getPanelClass = function(result) {
if (result.stderr && !result.score) {
return 'panel-danger';
@ -425,7 +411,6 @@ $(function() {
session.setUseWrapMode(true);
var file_id = $(element).data('id');
//setAnnotations(editor, file_id);
/*
* Register event handlers
@ -434,17 +419,6 @@ $(function() {
// editor itself
editor.on("paste", handlePasteEvent);
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
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() {
$(document).on('click', '#results a', showOutput);
$(document).on('keypress', handleKeyPress);
@ -612,6 +434,7 @@ $(function() {
initializeFileTreeButtons();
initializeWorkflowButtons();
initializeWorkspaceButtons();
initializeRequestForComments()
};
var initializeFileTree = function() {
@ -654,6 +477,20 @@ $(function() {
$('#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() {
return 'binary' in active_frame.data();
};
@ -667,7 +504,7 @@ $(function() {
filename: filename,
id: fileId
};
}
};
var isActiveFileRenderable = function() {
return 'renderable' in active_frame.data();
@ -737,10 +574,14 @@ $(function() {
// output_mode_is_streaming = false;
//}
if (!colorize) {
var stream = _.sortBy([output.stderr || '', output.stdout || ''], function(stream) {
return stream.length;
})[1];
element.append(stream);
if(output.stdout != ''){
element.append(output.stdout)
}
if(output.stderr != ''){
element.append('There was an error: StdErr: ' + output.stderr);
}
} else if (output.stderr) {
element.addClass('text-warning').append(output.stderr);
} else if (output.stdout) {
@ -796,10 +637,16 @@ $(function() {
var payload = {
user: {
resource_uuid: $('#editor').data('user-id')
type: 'User',
uuid: $('#editor').data('user-id')
},
verb: {
type: eventName
},
resource: {
type: 'page',
uuid: document.location.href
},
verb: eventName,
resource: {},
timestamp: new Date().toISOString(),
with_result: {},
in_context: contextData
@ -1119,7 +966,6 @@ $(function() {
$('#run').toggle(isActiveFileRunnable() && !running);
$('#stop').toggle(isActiveFileStoppable());
$('#test').toggle(isActiveFileTestable());
$('#request-for-comments').toggle(isActiveFileSubmission() && !isActiveFileBinary());
};
var initWebsocketConnection = function(url) {
@ -1302,10 +1148,11 @@ $(function() {
}
};
var requestComments = function(e) {
var requestComments = function() {
var user_id = $('#editor').data('user-id')
var exercise_id = $('#editor').data('exercise-id')
var file_id = $('.editor').data('id')
var question = $('#question').val();
$.ajax({
method: 'POST',
@ -1314,6 +1161,7 @@ $(function() {
request_for_comment: {
exercise_id: exercise_id,
file_id: file_id,
question: question,
"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(3i)":27,
@ -1322,13 +1170,13 @@ $(function() {
}
}
}).done(function() {
hideSpinner()
$.flash.success({ text: 'Request for comments sent!' })
})
hideSpinner();
$.flash.success({ text: $('#askForCommentsButton').data('message-success') })
}).error(ajaxError);
showSpinner($('#request-for-comments'))
// hide button until next submission is created
$('#request-for-comments').toggle(false);
$('#comment-modal').modal('hide');
var button = $('.requestCommentsButton');
button.fadeOut();
}
var initializeCodePilot = function() {

View File

@ -88,3 +88,10 @@ button i.fa-spin {
color: #777;
font-size: 0.8em;
}
.requestCommentsButton {
position: relative;
margin-top: -50px;
margin-right: 25px;
float: right;
}

View File

@ -14,6 +14,21 @@ module CodeOcean
create_and_respond(object: @file, path: proc { implement_exercise_path(@file.context.exercise, tab: 2) })
end
def create_and_respond(options = {})
@object = options[:object]
respond_to do |format|
if @object.save
yield if block_given?
path = options[:path].try(:call) || @object
respond_with_valid_object(format, notice: t('shared.object_created', model: @object.class.model_name.human), path: path, status: :created)
else
filename = (@object.path || '') + '/' + (@object.name || '') + (@object.file_type.file_extension || '')
format.html { redirect_to(options[:path]); flash[:danger] = t('files.error.filename', name: filename) }
format.json { render(json: @object.errors, status: :unprocessable_entity) }
end
end
end
def destroy
@file = CodeOcean::File.find(params[:id])
authorize!

View File

@ -12,41 +12,16 @@ class CommentsController < ApplicationController
# GET /comments
# GET /comments.json
def index
#@comments = Comment.all
#if admin, show all comments.
#check whether user is the author of the passed file_id, if so, show all comments. otherwise, only show comments of the file-author and own comments
file = CodeOcean::File.find(params[:file_id])
#there might be no submission yet, so dont use find
submission = Submission.find_by(id: file.context_id)
if submission
is_admin = false
user_id = current_user.id
# if we have an internal user, check whether he is an admin
if not current_user.respond_to? :external_id
is_admin = current_user.role == 'admin'
end
if(is_admin || user_id == submission.user_id)
# fetch all comments for this file
@comments = Comment.where(file_id: params[:file_id])
else
# fetch comments of the current user
#@comments = Comment.where(file_id: params[:file_id], user_id: user_id)
# fetch comments of file-author and the current user
@comments = Comment.where(file_id: params[:file_id], user_id: [user_id, submission.user_id])
end
#add names to comments
# if the user is internal, set the name
@comments = Comment.where(file_id: params[:file_id])
@comments.map{|comment|
comment.username = comment.user.name
# alternative: # if the user is external, fetch the displayname from xikolo
# Xikolo::UserClient.get(comment.user_id.to_s)[:display_name]
comment.username = comment.user.displayname
}
else
@comments = Comment.all.limit(0) #we need an empty relation here
@comments = []
end
authorize!
end
@ -111,14 +86,13 @@ class CommentsController < ApplicationController
end
def destroy
@comments = Comment.where(file_id: params[:file_id], row: params[:row])
@comments.delete_all
@comments = Comment.where(file_id: params[:file_id], row: params[:row], user: current_user)
@comments.each { |comment| authorize comment; comment.destroy }
respond_to do |format|
#format.html { redirect_to comments_url, notice: 'Comments were successfully destroyed.' }
format.html { head :no_content, notice: 'Comments were successfully destroyed.' }
format.json { head :no_content }
end
authorize!
end
private

View File

@ -119,7 +119,7 @@ class ExercisesController < ApplicationController
private :user_by_code_harbor_token
def exercise_params
params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :hide_file_tree, :team_id, :title, files_attributes: file_attributes).merge(user_id: current_user.id, user_type: current_user.class.name)
params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :hide_file_tree, :allow_file_creation, :team_id, :title, files_attributes: file_attributes).merge(user_id: current_user.id, user_type: current_user.class.name)
end
private :exercise_params

View File

@ -70,6 +70,6 @@ class RequestForCommentsController < ApplicationController
# Never trust parameters from the scary internet, only allow the white list through.
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

View File

@ -124,7 +124,7 @@ class SubmissionsController < ApplicationController
tubesock.onmessage do |data|
Rails.logger.info(Time.now.getutc.to_s + ": Client sending: " + data)
# Check wether the client send a JSON command and kill container
# Check whether the client send a JSON command and kill container
# if the command is 'exit', send it to docker otherwise.
begin
parsed = JSON.parse(data)

View File

@ -2,6 +2,19 @@ require File.expand_path('../../../uploaders/file_uploader', __FILE__)
require File.expand_path('../../../../lib/active_model/validations/boolean_presence_validator', __FILE__)
module CodeOcean
class FileNameValidator < ActiveModel::Validator
def validate(record)
existing_files = File.where(name: record.name, path: record.path, file_type_id: record.file_type_id,
context_id: record.context_id, context_type: record.context_type).to_a
unless existing_files.empty?
if (not record.context.is_a?(Exercise)) || (record.context.new_record?)
record.errors[:base] << 'Duplicate'
end
end
end
end
class File < ActiveRecord::Base
include DefaultValues
@ -44,6 +57,8 @@ module CodeOcean
validates :weight, if: :teacher_defined_test?, numericality: true, presence: true
validates :weight, absence: true, unless: :teacher_defined_test?
validates_with FileNameValidator, fields: [:name, :path, :file_type_id]
ROLES.each do |role|
define_method("#{role}?") { self.role == role }
end

View File

@ -3,4 +3,13 @@ class ExternalUser < ActiveRecord::Base
validates :consumer_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

View File

@ -21,4 +21,9 @@ class InternalUser < ActiveRecord::Base
def teacher?
role == 'teacher'
end
def displayname
name
end
end

View File

@ -25,8 +25,12 @@ class RequestForComment < ActiveRecord::Base
limit 1").first
end
def to_s
"RFC-" + self.id.to_s
end
private
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, question, 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
end
end

View File

@ -8,7 +8,11 @@ class CommentPolicy < ApplicationPolicy
everyone
end
[:new?, :show?, :destroy?].each do |action|
def show?
everyone
end
[:new?, :destroy?].each do |action|
define_method(action) { admin? || author? }
end

View File

@ -1,19 +1,17 @@
- if current_user
- if current_user.internal_user?
li.dropdown
a.dropdown-toggle data-toggle='dropdown' href='#'
i.fa.fa-user
= current_user
span.caret
ul.dropdown-menu role='menu'
li.dropdown
a.dropdown-toggle data-toggle='dropdown' href='#'
i.fa.fa-user
= current_user
span.caret
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('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)
- else
li
p.navbar-text
i.fa.fa-user
= current_user
- else
li = link_to(sign_in_path) do
i.fa.fa-sign-in

View File

@ -38,4 +38,4 @@
= 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('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')

View File

@ -2,9 +2,9 @@
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: t('exercises.editor.requestComments'))
- if @exercise.allow_file_creation?
= 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('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'))
= render('editor_button', classes: 'btn-block btn-primary btn-sm', icon: 'fa fa-download', id: 'download', label: t('exercises.editor.download'))

View File

@ -13,3 +13,7 @@
- else
.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
button.btn.btn-primary.requestCommentsButton type='button'
i.fa.fa-comment-o
= t('exercises.editor.requestComments')

View File

@ -28,6 +28,10 @@
label
= f.check_box(:hide_file_tree)
= t('activerecord.attributes.exercise.hide_file_tree')
.checkbox
label
= f.check_box(:allow_file_creation)
= t('activerecord.attributes.exercise.allow_file_creation')
h2 = t('activerecord.attributes.exercise.files')
ul#files.list-unstyled.panel-group
= f.fields_for :files do |files_form|

View File

@ -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')

View File

@ -28,7 +28,7 @@ h1 = Exercise.model_name.human(count: 2)
tr data-id=exercise.id
td = exercise.title
td = link_to_if(policy(exercise.author).show?, exercise.author, exercise.author)
td = link_to_if(policy(exercise.execution_environment).show?, exercise.execution_environment, exercise.execution_environment)
td = link_to_if(exercise.execution_environment && policy(exercise.execution_environment).show?, exercise.execution_environment, exercise.execution_environment)
td = exercise.files.teacher_defined_tests.count
td = exercise.maximum_score
td.public data-value=exercise.public? = symbol_for(exercise.public?)

View File

@ -16,6 +16,7 @@ h1
= row(label: 'exercise.maximum_score', value: @exercise.maximum_score)
= row(label: 'exercise.public', value: @exercise.public?)
= row(label: 'exercise.hide_file_tree', value: @exercise.hide_file_tree?)
= row(label: 'exercise.allow_file_creation', value: @exercise.allow_file_creation?)
= row(label: 'exercise.embedding_parameters') do
= content_tag(:input, nil, class: 'form-control', readonly: true, value: embedding_parameters(@exercise))

View File

@ -22,7 +22,7 @@ html lang='en'
span.icon-bar
span.icon-bar
span.icon-bar
a.navbar-brand href=root_path
.navbar-brand
i.fa.fa-code
= application_name
#navbar-collapse.collapse.navbar-collapse

View File

@ -1,19 +1,22 @@
h1 = RequestForComment.model_name.human(count: 2)
.table-responsive
table.table
table.table.sortable
thead
tr
th = t('activerecord.attributes.request_for_comments.exercise')
th = t('activerecord.attributes.request_for_comments.execution_environment')
th = t('activerecord.attributes.request_for_comments.question')
th = t('activerecord.attributes.request_for_comments.username')
th = t('activerecord.attributes.request_for_comments.requested_at')
tbody
- @request_for_comments.each do |request_for_comment|
tr data-id=request_for_comment.id
td = link_to(request_for_comment.exercise.title, request_for_comment)
td = request_for_comment.exercise.execution_environment
td = request_for_comment.user.name
td = request_for_comment.requested_at
- if request_for_comment.has_attribute?(:question) && request_for_comment.question
td = truncate(request_for_comment.question, length: 200)
- else
td = '-'
td = request_for_comment.user.displayname
td = t('shared.time.before', time: distance_of_time_in_words_to_now(request_for_comment.requested_at))
= render('shared/pagination', collection: @request_for_comments)

View File

@ -14,6 +14,13 @@
%>
<%= user %> | <%= @request_for_comment.requested_at %>
</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>
<!--
@ -25,27 +32,22 @@ do not put a carriage return in the line below. it will be present in the presen
</div>
<% end %>
<%= render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.dialogtitle'), template: 'exercises/_comment_dialogcontent') %>
<script type="text/javascript">
var commentitor = $('.editor');
var userid = commentitor.data('user-id');
commentitor.each(function (index, editor) {
currentEditor = ace.edit(editor);
var currentEditor = ace.edit(editor);
currentEditor.setReadOnly(true);
setAnnotations(currentEditor, $(editor).data('file-id'));
currentEditor.on("guttermousedown", handleSidebarClick);
});
function setAnnotations(editor, fileid) {
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 session = editor.getSession();
var jqrequest = $.ajax({
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){
$.each(response, function(index, comment) {
comment.className = "code-ocean_comment"
comment.className = "code-ocean_comment";
comment.text = comment.username + ": " + comment.text
})
});
editor.getSession().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())
})
session.setAnnotations(response);
})
}
function addComment(editor, fileid) {
var commentInput = $(editor.container).find('#commentInput');
var comment = commentInput.val()
var line = commentInput.data('line')
function hasCommentsInRow(editor, row){
return editor.getSession().getAnnotations().some(function(element) {
return element.row === row;
})
}
if (line == '' || comment == '') {
return
} else {
line = parseInt(line) - 1
}
function getCommentsForRow(editor, row){
return editor.getSession().getAnnotations().filter(function(element) {
return element.row === row;
})
}
$.ajax({
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: {
comment: {
user_id: userid,
file_id: fileid,
row: line,
file_id: file_id,
row: row,
column: 0,
text: comment
text: commenttext
}
},
dataType: 'json',
method: 'POST',
url: "/comments"
}).done(setAnnotations(editor, fileid))
$('.ace_gutter-cell').popover('hide')
});
jqxhr.done(function(response){
setAnnotations(editor, file_id);
});
jqxhr.fail(ajaxError);
}
function handleSidebarClick(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();
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>
<style>
#commentitor, .ace_gutter, .ace_gutter-layer { overflow: visible }
.popover { width: 400px; max-width: none; }
</style>