Merge remote-tracking branch 'origin/master' into error-info

This commit is contained in:
Maximilian Grundke
2017-08-23 14:51:53 +02:00
14 changed files with 176 additions and 17 deletions

View File

@ -155,9 +155,9 @@ configureEditors: function () {
if (!same) { if (!same) {
this.publishCodeOceanEvent("codeocean_editor_paste", { this.publishCodeOceanEvent("codeocean_editor_paste", {
text: pasteObject.text, text: pasteObject.text,
codeocean_user_id: $('#editor').data('user-id'),
exercise: $('#editor').data('exercise-id'), exercise: $('#editor').data('exercise-id'),
file_id: "1" file_id: "1"
}); });
} }
}, },
@ -390,9 +390,7 @@ configureEditors: function () {
var payload = { var payload = {
user: { user: {
type: 'User', uuid: $('#editor').data('user-external-id')
uuid: $('#editor').data('user-id'),
external_id: $('#editor').data('user-external-id')
}, },
verb: { verb: {
type: eventName type: eventName

View File

@ -75,6 +75,8 @@ CodeOceanEditorSubmissions = {
} }
// 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
this.toggleButtonStates(); this.toggleButtonStates();
this.updateSaveStateLabel();
}, },
/** /**
@ -200,12 +202,15 @@ CodeOceanEditorSubmissions = {
} }
}, },
autosave: function () { updateSaveStateLabel: function() {
var date = new Date(); var date = new Date();
var autosaveLabel = $(this.autosaveLabel); var autosaveLabel = $(this.autosaveLabel);
autosaveLabel.parent().css("visibility", "visible"); autosaveLabel.parent().css("visibility", "visible");
autosaveLabel.text(date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds()); autosaveLabel.text(date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds());
autosaveLabel.text(date.toLocaleTimeString()); autosaveLabel.text(date.toLocaleTimeString());
},
autosave: function () {
this.autosaveTimer = null; this.autosaveTimer = null;
this.createSubmission($('#autosave'), null); this.createSubmission($('#autosave'), null);
} }

View File

@ -17,3 +17,50 @@
width: 100%; width: 100%;
height: 200px; height: 200px;
} }
.ace_tooltip {
display: none !important;
}
p.comment {
width: 400px;
}
.popover-header {
width: 100%;
overflow: hidden;
padding-bottom: 10px;
margin: auto;
}
.popover-username {
font-weight: bold;
width: 60%;
float: left;
}
.popover-date {
text-align: right;
color: #008cba;
margin-left: 60%;
font-size: x-small;
}
.popover-updated {
text-align: right;
margin-left: 60%;
font-size: x-small;
}
.popover-comment {
word-wrap: break-word;
margin-bottom: 10px;
}
.popover-divider {
width: 100%;
height: 1px;
background-color: #008cba;
overflow: hidden;
margin-bottom: 10px;
}

View File

@ -19,6 +19,8 @@ class CommentsController < ApplicationController
@comments = Comment.where(file_id: params[:file_id]) @comments = Comment.where(file_id: params[:file_id])
@comments.map{|comment| @comments.map{|comment|
comment.username = comment.user.displayname comment.username = comment.user.displayname
comment.date = comment.created_at.strftime('%d.%m.%Y %k:%M')
comment.updated = (comment.created_at != comment.updated_at)
} }
else else
@comments = [] @comments = []

View File

@ -11,14 +11,45 @@ class RequestForCommentsController < ApplicationController
# GET /request_for_comments # GET /request_for_comments
# GET /request_for_comments.json # GET /request_for_comments.json
def index def index
@search = RequestForComment.last_per_user(2).search(params[:q]) @search = RequestForComment
.last_per_user(2)
.joins('join "submissions" s on s.id = request_for_comments.submission_id
left outer join "files" f on f.context_id = s.id
left outer join "comments" on comments.file_id = f.id')
.group('request_for_comments.id, request_for_comments.user_id, request_for_comments.exercise_id,
request_for_comments.file_id, request_for_comments.question, request_for_comments.created_at,
request_for_comments.updated_at, request_for_comments.user_type, request_for_comments.solved,
request_for_comments.submission_id, request_for_comments.row_number') # ugly, but rails wants it this way
.select('request_for_comments.*, max(comments.updated_at) as last_comment')
.search(params[:q])
@request_for_comments = @search.result.order('created_at DESC').paginate(page: params[:page]) @request_for_comments = @search.result.order('created_at DESC').paginate(page: params[:page])
authorize! authorize!
end end
def get_my_comment_requests def get_my_comment_requests
@search = RequestForComment.where(user_id: current_user.id).order('created_at DESC').search(params[:q]) @search = RequestForComment
@request_for_comments = @search.result.paginate(page: params[:page]) .where(user_id: current_user.id)
.joins('join "submissions" s on s.id = request_for_comments.submission_id
left outer join "files" f on f.context_id = s.id
left outer join "comments" on comments.file_id = f.id')
.group('request_for_comments.id')
.select('request_for_comments.*, max(comments.updated_at) as last_comment')
.search(params[:q])
@request_for_comments = @search.result.order('created_at DESC').paginate(page: params[:page])
render 'index'
end
def get_rfcs_with_my_comments
@search = RequestForComment
.joins(:comments) # we don't need to outer join here, because we know the user has commented on these
.where(comments: {user_id: current_user.id})
.joins('join "submissions" s on s.id = request_for_comments.submission_id
left outer join "files" f on f.context_id = s.id
left outer join "comments" as c on c.file_id = f.id')
.group('request_for_comments.id')
.select('request_for_comments.*, max(c.updated_at) as last_comment')
.search(params[:q])
@request_for_comments = @search.result.order('last_comment DESC').paginate(page: params[:page])
render 'index' render 'index'
end end

View File

@ -1,7 +1,7 @@
class Comment < ActiveRecord::Base class Comment < ActiveRecord::Base
# inherit the creation module: encapsulates that this is a polymorphic user, offers some aliases and makes sure that all necessary attributes are set. # inherit the creation module: encapsulates that this is a polymorphic user, offers some aliases and makes sure that all necessary attributes are set.
include Creation include Creation
attr_accessor :username attr_accessor :username, :date, :updated
belongs_to :file, class_name: 'CodeOcean::File' belongs_to :file, class_name: 'CodeOcean::File'
belongs_to :user, polymorphic: true belongs_to :user, polymorphic: true

View File

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

View File

@ -10,6 +10,7 @@
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.all'), request_for_comments_path)
li = link_to(t('request_for_comments.index.get_my_comment_requests'), my_request_for_comments_path) li = link_to(t('request_for_comments.index.get_my_comment_requests'), my_request_for_comments_path)
li = link_to(t('request_for_comments.index.get_my_rfc_activity'), my_rfc_activity_path)
- if current_user.internal_user? - 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 - else

View File

@ -1,4 +1,4 @@
json.array!(@comments) do |comment| json.array!(@comments) do |comment|
json.extract! comment, :id, :user_id, :file_id, :row, :column, :text, :username json.extract! comment, :id, :user_id, :file_id, :row, :column, :text, :username, :date, :updated
json.url comment_url(comment, format: :json) json.url comment_url(comment, format: :json)
end end

View File

@ -20,6 +20,7 @@ h1 = RequestForComment.model_name.human(count: 2)
i class="fa fa-comment" aria-hidden="true" title = t('request_for_comments.comments') align="center" i class="fa fa-comment" aria-hidden="true" title = t('request_for_comments.comments') align="center"
th = t('activerecord.attributes.request_for_comments.username') th = t('activerecord.attributes.request_for_comments.username')
th = t('activerecord.attributes.request_for_comments.requested_at') th = t('activerecord.attributes.request_for_comments.requested_at')
th = t('activerecord.attributes.request_for_comments.last_update')
tbody tbody
- @request_for_comments.each do |request_for_comment| - @request_for_comments.each do |request_for_comment|
tr data-id=request_for_comment.id tr data-id=request_for_comment.id
@ -36,5 +37,6 @@ h1 = RequestForComment.model_name.human(count: 2)
td = request_for_comment.comments_count td = request_for_comment.comments_count
td = request_for_comment.user.displayname td = request_for_comment.user.displayname
td = t('shared.time.before', time: distance_of_time_in_words_to_now(request_for_comment.created_at)) td = t('shared.time.before', time: distance_of_time_in_words_to_now(request_for_comment.created_at))
td = t('shared.time.before', time: distance_of_time_in_words_to_now(request_for_comment.last_comment.nil? ? request_for_comment.updated_at : request_for_comment.last_comment))
= render('shared/pagination', collection: @request_for_comments) = render('shared/pagination', collection: @request_for_comments)

View File

@ -64,6 +64,8 @@
</h5> </h5>
</div> </div>
<hr> <hr>
<div class="hidden sanitizer"></div>
<!-- <!--
do not put a carriage return in the line below. it will be present in the presentation of the source code, otherwise. do not put a carriage return in the line below. it will be present in the presentation of the source code, otherwise.
also, all settings from the rails model needed for the editor configuration in the JavaScript are attached to the editor as data attributes here. also, all settings from the rails model needed for the editor configuration in the JavaScript are attached to the editor as data attributes here.
@ -134,11 +136,24 @@ also, all settings from the rails model needed for the editor configuration in t
currentEditor.setReadOnly(true); currentEditor.setReadOnly(true);
// set editor mode (used for syntax highlighting // set editor mode (used for syntax highlighting
currentEditor.getSession().setMode($(editor).data('mode')); currentEditor.getSession().setMode($(editor).data('mode'));
currentEditor.getSession().setOption("useWorker", false);
setAnnotations(currentEditor, $(editor).data('file-id')); setAnnotations(currentEditor, $(editor).data('file-id'));
currentEditor.on("guttermousedown", handleSidebarClick); currentEditor.on("guttermousedown", handleSidebarClick);
}); });
function cleanupPopovers() {
// remove all possible popovers
$('.editor > .ace_gutter > .ace_gutter-layer > .ace_gutter-cell').popover('destroy');
}
function preprocess(commentText) {
// sanitize comments to deal with XSS attacks:
commentText = $('div.sanitizer').text(commentText).html();
// display original line breaks:
return commentText.replace(/\n/g, '<br>');
}
function setAnnotations(editor, fileid) { function setAnnotations(editor, fileid) {
var session = editor.getSession(); var session = editor.getSession();
@ -152,18 +167,65 @@ also, all settings from the rails model needed for the editor configuration in t
}); });
jqrequest.done(function(response){ jqrequest.done(function(response){
$.each(response, function(index, comment) { // comments need to be sorted to cluster them per line
comment.className = "code-ocean_comment"; var comments = response.slice().sort(function (a, b) {
return a.row - b.row;
});
while (comments.length > 0) {
// new cluster of comments
var cluster = [];
var clusterRow = comments[0].row;
// now collect all comments on this line
while (comments.length > 0 && comments[0].row === clusterRow) {
cluster.push(comments.shift());
}
// sort the comments by creation date
cluster = cluster.sort(function (a, b) {
return a.id - b.id;
});
// build the markup for the current line's popover
var popupContent = '';
cluster.forEach(function(comment, index) {
if (index !== 0) {
popupContent += '<div class="popover-divider"></div>'
}
popupContent += '<p class="comment">';
popupContent += '<div class="popover-header">' +
'<div class="popover-username">' + preprocess(comment.username) + '</div>' +
'<div class="popover-date">' + comment.date + '</div>';
if (comment.updated) {
popupContent += '<div class="popover-updated">' +
'<i class="fa fa-pencil" aria-hidden="true"></i>' +
'<%= t('request_for_comments.comment_edited') %>' +
'</div>'
}
popupContent += '</div>';
popupContent += '<div class="popover-comment">' + preprocess(comment.text) + '</div>';
popupContent += '</p>';
});
// attach the popover to the ace sidebar (where the comment icon is displayed)
var icon = $('*[data-file-id="' + fileid + '"]') // the editor for this file
.find('.ace_gutter > .ace_gutter-layer') // the sidebar
.find('div:nth-child(' + (clusterRow + 1) + ')'); // the correct line
icon.popover({
content: popupContent,
html: true, // necessary to style comments. XSS is not possible due to comment pre-processing (sanitizing)
trigger: 'hover',
container: 'body'
});
}
$.each(response, function(index, comment) {
comment.className = 'code-ocean_comment';
// if we have tabs or carriage returns in the comment, just add the name and leave it as it is. otherwise: format! // if we have tabs or carriage returns in the comment, just add the name and leave it as it is. otherwise: format!
if(comment.text.includes("\n") || comment.text.includes("\t")){ if(comment.text.includes("\n") || comment.text.includes("\t")){
comment.text = comment.username + ": " + comment.text; comment.text = comment.username + ": " + comment.text;
} else { } else {
comment.text = comment.username + ": " + stringDivider(comment.text, 80, "\n\t\t"); comment.text = comment.username + ": " + stringDivider(comment.text, 80, "\n");
} }
}); });
session.setAnnotations(response); session.setAnnotations(response);
}) })
} }
@ -181,6 +243,7 @@ also, all settings from the rails model needed for the editor configuration in t
} }
function deleteComment(file_id, row, editor) { function deleteComment(file_id, row, editor) {
cleanupPopovers();
var jqxhr = $.ajax({ var jqxhr = $.ajax({
type: 'DELETE', type: 'DELETE',
url: "/comments", url: "/comments",
@ -195,6 +258,7 @@ also, all settings from the rails model needed for the editor configuration in t
} }
function createComment(file_id, row, editor, commenttext){ function createComment(file_id, row, editor, commenttext){
cleanupPopovers();
var jqxhr = $.ajax({ var jqxhr = $.ajax({
data: { data: {
comment: { comment: {

View File

@ -94,6 +94,7 @@ de:
requested_at: Angefragezeitpunkt requested_at: Angefragezeitpunkt
question: "Frage" question: "Frage"
close: "Fenster schließen" close: "Fenster schließen"
last_update: "Letzte Aktivität"
submission: submission:
cause: Anlass cause: Anlass
code: Code code: Code
@ -460,7 +461,7 @@ de:
Thank you for helping other users on CodeOcean! Thank you for helping other users on CodeOcean!
<br> <br>
This mail was automatically sent by CodeOcean. <br> This mail was automatically sent by CodeOcean. <br>
subject: "%{author} sagt danke!" subject: "%{author} sagt Danke!"
request_for_comments: request_for_comments:
click_here: Zum Kommentieren auf die Seitenleiste klicken! click_here: Zum Kommentieren auf die Seitenleiste klicken!
comments: Kommentare comments: Kommentare
@ -472,6 +473,8 @@ de:
index: index:
get_my_comment_requests: Meine Kommentaranfragen get_my_comment_requests: Meine Kommentaranfragen
all: "Alle Kommentaranfragen" all: "Alle Kommentaranfragen"
get_rfcs_with_my_comments: Kommentaranfragen die ich kommentiert habe
get_my_rfc_activity: "Meine Kommentaraktivität"
no_question: "Der Autor hat keine Frage zu dieser Anfrage gestellt." no_question: "Der Autor hat keine Frage zu dieser Anfrage gestellt."
mark_as_solved: "Diese Frage als beantwortet markieren" mark_as_solved: "Diese Frage als beantwortet markieren"
show_all: "Alle Anfragen anzeigen" show_all: "Alle Anfragen anzeigen"
@ -482,6 +485,7 @@ de:
write_a_thank_you_node: "Wenn Sie möchten, können Sie sich bei allen Mitstudenten, die Ihnen bei der Beantwortung Ihrer Frage geholfen haben, bedanken:" write_a_thank_you_node: "Wenn Sie möchten, können Sie sich bei allen Mitstudenten, die Ihnen bei der Beantwortung Ihrer Frage geholfen haben, bedanken:"
send_thank_you_note: "Senden" send_thank_you_note: "Senden"
cancel_thank_you_note: "Nichts senden" cancel_thank_you_note: "Nichts senden"
comment_edited: "bearbeitet"
sessions: sessions:
create: create:
failure: Fehlerhafte E-Mail oder Passwort. failure: Fehlerhafte E-Mail oder Passwort.

View File

@ -115,6 +115,7 @@ en:
requested_at: Request Date requested_at: Request Date
question: "Question" question: "Question"
close: Close window close: Close window
last_update: "Last Update"
submission: submission:
cause: Cause cause: Cause
code: Code code: Code
@ -493,6 +494,8 @@ en:
index: index:
all: All Requests for Comments all: All Requests for Comments
get_my_comment_requests: My Requests for Comments get_my_comment_requests: My Requests for Comments
get_rfcs_with_my_comments: Requests for Comments I have commented on
get_my_rfc_activity: "My Comment Activity"
no_question: "The author did not enter a question for this request." no_question: "The author did not enter a question for this request."
mark_as_solved: "Mark this question as answered" mark_as_solved: "Mark this question as answered"
show_all: "All requests" show_all: "All requests"
@ -503,6 +506,7 @@ en:
write_a_thank_you_node: "If you want, you can write a thank you note to all your commenters:" write_a_thank_you_node: "If you want, you can write a thank you note to all your commenters:"
send_thank_you_note: "Send" send_thank_you_note: "Send"
cancel_thank_you_note: "Don't send" cancel_thank_you_note: "Don't send"
comment_edited: "edited"
sessions: sessions:
create: create:
failure: Invalid email or password. failure: Invalid email or password.

View File

@ -27,6 +27,7 @@ Rails.application.routes.draw do
end end
end end
get '/my_request_for_comments', as: 'my_request_for_comments', to: 'request_for_comments#get_my_comment_requests' get '/my_request_for_comments', as: 'my_request_for_comments', to: 'request_for_comments#get_my_comment_requests'
get '/my_rfc_activity', as: 'my_rfc_activity', to: 'request_for_comments#get_rfcs_with_my_comments'
delete '/comment_by_id', to: 'comments#destroy_by_id' delete '/comment_by_id', to: 'comments#destroy_by_id'
put '/comments', to: 'comments#update' put '/comments', to: 'comments#update'