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) {
this.publishCodeOceanEvent("codeocean_editor_paste", {
text: pasteObject.text,
codeocean_user_id: $('#editor').data('user-id'),
exercise: $('#editor').data('exercise-id'),
file_id: "1"
});
}
},
@ -390,9 +390,7 @@ configureEditors: function () {
var payload = {
user: {
type: 'User',
uuid: $('#editor').data('user-id'),
external_id: $('#editor').data('user-external-id')
uuid: $('#editor').data('user-external-id')
},
verb: {
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
this.toggleButtonStates();
this.updateSaveStateLabel();
},
/**
@ -200,12 +202,15 @@ CodeOceanEditorSubmissions = {
}
},
autosave: function () {
updateSaveStateLabel: function() {
var date = new Date();
var autosaveLabel = $(this.autosaveLabel);
autosaveLabel.parent().css("visibility", "visible");
autosaveLabel.text(date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds());
autosaveLabel.text(date.toLocaleTimeString());
},
autosave: function () {
this.autosaveTimer = null;
this.createSubmission($('#autosave'), null);
}

View File

@ -17,3 +17,50 @@
width: 100%;
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.map{|comment|
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
@comments = []

View File

@ -11,14 +11,45 @@ class RequestForCommentsController < ApplicationController
# GET /request_for_comments
# GET /request_for_comments.json
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])
authorize!
end
def get_my_comment_requests
@search = RequestForComment.where(user_id: current_user.id).order('created_at DESC').search(params[:q])
@request_for_comments = @search.result.paginate(page: params[:page])
@search = RequestForComment
.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'
end

View File

@ -1,7 +1,7 @@
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.
include Creation
attr_accessor :username
attr_accessor :username, :date, :updated
belongs_to :file, class_name: 'CodeOcean::File'
belongs_to :user, polymorphic: true

View File

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

View File

@ -10,6 +10,7 @@
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)
li = link_to(t('request_for_comments.index.get_my_rfc_activity'), my_rfc_activity_path)
- if current_user.internal_user?
li = link_to(t('sessions.destroy.link'), sign_out_path, method: :delete)
- else

View File

@ -1,4 +1,4 @@
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)
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"
th = t('activerecord.attributes.request_for_comments.username')
th = t('activerecord.attributes.request_for_comments.requested_at')
th = t('activerecord.attributes.request_for_comments.last_update')
tbody
- @request_for_comments.each do |request_for_comment|
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.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.last_comment.nil? ? request_for_comment.updated_at : request_for_comment.last_comment))
= render('shared/pagination', collection: @request_for_comments)

View File

@ -64,6 +64,8 @@
</h5>
</div>
<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.
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);
// set editor mode (used for syntax highlighting
currentEditor.getSession().setMode($(editor).data('mode'));
currentEditor.getSession().setOption("useWorker", false);
setAnnotations(currentEditor, $(editor).data('file-id'));
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) {
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){
$.each(response, function(index, comment) {
comment.className = "code-ocean_comment";
// comments need to be sorted to cluster them per line
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(comment.text.includes("\n") || comment.text.includes("\t")){
comment.text = comment.username + ": " + comment.text;
} else {
comment.text = comment.username + ": " + stringDivider(comment.text, 80, "\n\t\t");
comment.text = comment.username + ": " + stringDivider(comment.text, 80, "\n");
}
});
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) {
cleanupPopovers();
var jqxhr = $.ajax({
type: 'DELETE',
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){
cleanupPopovers();
var jqxhr = $.ajax({
data: {
comment: {

View File

@ -94,6 +94,7 @@ de:
requested_at: Angefragezeitpunkt
question: "Frage"
close: "Fenster schließen"
last_update: "Letzte Aktivität"
submission:
cause: Anlass
code: Code
@ -460,7 +461,7 @@ de:
Thank you for helping other users on CodeOcean!
<br>
This mail was automatically sent by CodeOcean. <br>
subject: "%{author} sagt danke!"
subject: "%{author} sagt Danke!"
request_for_comments:
click_here: Zum Kommentieren auf die Seitenleiste klicken!
comments: Kommentare
@ -472,6 +473,8 @@ de:
index:
get_my_comment_requests: Meine 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."
mark_as_solved: "Diese Frage als beantwortet markieren"
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:"
send_thank_you_note: "Senden"
cancel_thank_you_note: "Nichts senden"
comment_edited: "bearbeitet"
sessions:
create:
failure: Fehlerhafte E-Mail oder Passwort.

View File

@ -115,6 +115,7 @@ en:
requested_at: Request Date
question: "Question"
close: Close window
last_update: "Last Update"
submission:
cause: Cause
code: Code
@ -493,6 +494,8 @@ en:
index:
all: All 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."
mark_as_solved: "Mark this question as answered"
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:"
send_thank_you_note: "Send"
cancel_thank_you_note: "Don't send"
comment_edited: "edited"
sessions:
create:
failure: Invalid email or password.

View File

@ -27,6 +27,7 @@ Rails.application.routes.draw do
end
end
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'
put '/comments', to: 'comments#update'