Merge pull request #125 from openHPI/rework-comment-modal
Rework comment modal
This commit is contained in:
@ -14,6 +14,7 @@
|
||||
//
|
||||
//= require ace/ace
|
||||
//= require chosen.jquery.min
|
||||
//= require jquery-ui.min
|
||||
//= require d3
|
||||
//= require jquery.turbolinks
|
||||
//= require jquery_ujs
|
||||
|
@ -22,45 +22,132 @@
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
p.comment {
|
||||
width: 400px;
|
||||
.modal-content {
|
||||
min-height: 512px;
|
||||
min-width: 360px;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.modal-body {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
#otherComments {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.container {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.popover-header {
|
||||
.comment {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
padding-bottom: 10px;
|
||||
margin: auto;
|
||||
min-width: 200px;
|
||||
|
||||
.comment-header {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
padding-bottom: 10px;
|
||||
margin: auto;
|
||||
|
||||
.comment-username {
|
||||
font-weight: bold;
|
||||
width: 60%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.comment-date {
|
||||
text-align: right;
|
||||
color: #008cba;
|
||||
margin-left: 60%;
|
||||
font-size: x-small;
|
||||
}
|
||||
|
||||
.comment-updated {
|
||||
text-align: right;
|
||||
margin-left: 60%;
|
||||
font-size: x-small;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
word-wrap: break-word;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.comment-editor {
|
||||
display: none;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.comment-actions {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
.comment-divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: #008cba;
|
||||
overflow: hidden;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#otherComments {
|
||||
h5 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #cccccc;
|
||||
padding: 15px;
|
||||
|
||||
.comment-removed {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.comment-actions {
|
||||
display: flex;
|
||||
|
||||
button {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input#subscribe {
|
||||
margin-top: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
#myComment {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
textarea {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.popover-footer {
|
||||
color: #008cba;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
class CommentsController < ApplicationController
|
||||
before_action :set_comment, only: [:show, :edit, :update, :destroy_by_id]
|
||||
before_action :set_comment, only: [:show, :edit, :update, :destroy]
|
||||
|
||||
# to disable authorization check: comment the line below back in
|
||||
# skip_after_action :verify_authorized
|
||||
@ -21,6 +21,7 @@ class CommentsController < ApplicationController
|
||||
comment.username = comment.user.displayname
|
||||
comment.date = comment.created_at.strftime('%d.%m.%Y %k:%M')
|
||||
comment.updated = (comment.created_at != comment.updated_at)
|
||||
comment.editable = comment.user == current_user
|
||||
}
|
||||
else
|
||||
@comments = []
|
||||
@ -50,12 +51,14 @@ class CommentsController < ApplicationController
|
||||
def create
|
||||
@comment = Comment.new(comment_params_without_request_id)
|
||||
|
||||
if comment_params[:request_id]
|
||||
UserMailer.got_new_comment(@comment, RequestForComment.find(comment_params[:request_id]), current_user).deliver_now
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
if @comment.save
|
||||
if comment_params[:request_id]
|
||||
request_for_comment = RequestForComment.find(comment_params[:request_id])
|
||||
send_mail_to_author @comment, request_for_comment
|
||||
send_mail_to_subscribers @comment, request_for_comment
|
||||
end
|
||||
|
||||
format.html { redirect_to @comment, notice: 'Comment was successfully created.' }
|
||||
format.json { render :show, status: :created, location: @comment }
|
||||
else
|
||||
@ -83,7 +86,8 @@ class CommentsController < ApplicationController
|
||||
|
||||
# DELETE /comments/1
|
||||
# DELETE /comments/1.json
|
||||
def destroy_by_id
|
||||
def destroy
|
||||
authorize!
|
||||
@comment.destroy
|
||||
respond_to do |format|
|
||||
format.html { head :no_content, notice: 'Comment was successfully destroyed.' }
|
||||
@ -91,30 +95,42 @@ class CommentsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@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
|
||||
end
|
||||
|
||||
private
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
def set_comment
|
||||
@comment = Comment.find(params[:id])
|
||||
end
|
||||
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
def set_comment
|
||||
@comment = Comment.find(params[:id])
|
||||
end
|
||||
|
||||
def comment_params_without_request_id
|
||||
comment_params.except :request_id
|
||||
end
|
||||
|
||||
# Never trust parameters from the scary internet, only allow the white list through.
|
||||
def comment_params
|
||||
#params.require(:comment).permit(:user_id, :file_id, :row, :column, :text)
|
||||
# fuer production mode, damit böse menschen keine falsche user_id uebergeben:
|
||||
params.require(:comment).permit(:file_id, :row, :column, :text, :request_id).merge(user_id: current_user.id, user_type: current_user.class.name)
|
||||
# Never trust parameters from the scary internet, only allow the white list through.
|
||||
def comment_params
|
||||
#params.require(:comment).permit(:user_id, :file_id, :row, :column, :text)
|
||||
# fuer production mode, damit böse menschen keine falsche user_id uebergeben:
|
||||
params.require(:comment).permit(:file_id, :row, :column, :text, :request_id).merge(user_id: current_user.id, user_type: current_user.class.name)
|
||||
end
|
||||
|
||||
def send_mail_to_author(comment, request_for_comment)
|
||||
if current_user != request_for_comment.user
|
||||
UserMailer.got_new_comment(comment, request_for_comment, current_user).deliver_now
|
||||
end
|
||||
end
|
||||
|
||||
def send_mail_to_subscribers(comment, request_for_comment)
|
||||
request_for_comment.commenters.each do |commenter|
|
||||
subscriptions = Subscription.where(
|
||||
:request_for_comment_id => request_for_comment.id,
|
||||
:user_id => commenter.id, :user_type => commenter.class.name)
|
||||
subscriptions.each do |subscription|
|
||||
if (subscription.subscription_type == 'author' and current_user == request_for_comment.user) or subscription.subscription_type == 'all'
|
||||
if subscription.user != current_user
|
||||
UserMailer.got_new_comment_for_subscription(comment, subscription, current_user).deliver_now
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -68,11 +68,8 @@ class RequestForCommentsController < ApplicationController
|
||||
def set_thank_you_note
|
||||
authorize!
|
||||
@request_for_comment.thank_you_note = params[:note]
|
||||
commenters = []
|
||||
@request_for_comment.comments.distinct.to_a.each {|comment|
|
||||
commenters.append comment.user
|
||||
}
|
||||
commenters = commenters.uniq {|user| user.id}
|
||||
|
||||
commenters = @request_for_comment.commenters
|
||||
commenters.each {|commenter| UserMailer.send_thank_you_note(@request_for_comment, commenter).deliver_now}
|
||||
|
||||
respond_to do |format|
|
||||
|
61
app/controllers/subscriptions_controller.rb
Normal file
61
app/controllers/subscriptions_controller.rb
Normal file
@ -0,0 +1,61 @@
|
||||
class SubscriptionsController < ApplicationController
|
||||
|
||||
def authorize!
|
||||
authorize(@subscription || @subscriptions)
|
||||
end
|
||||
private :authorize!
|
||||
|
||||
# POST /subscriptions.json
|
||||
def create
|
||||
@subscription = Subscription.new(subscription_params)
|
||||
respond_to do |format|
|
||||
if @subscription.save
|
||||
format.json { render json: @subscription, status: :created }
|
||||
else
|
||||
format.json { render json: @subscription.errors, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
authorize!
|
||||
end
|
||||
|
||||
# DELETE /subscriptions/1
|
||||
# DELETE /subscriptions/1.json
|
||||
def destroy
|
||||
begin
|
||||
@subscription = Subscription.find(params[:id])
|
||||
rescue
|
||||
skip_authorization
|
||||
respond_to do |format|
|
||||
format.html { redirect_to request_for_comments_url, alert: t('subscriptions.subscription_not_existent') }
|
||||
format.json { render json: {message: t('subscriptions.subscription_not_existent')}, status: :not_found }
|
||||
end
|
||||
else
|
||||
authorize!
|
||||
rfc = @subscription.try(:request_for_comment)
|
||||
if @subscription.destroy
|
||||
respond_to do |format|
|
||||
format.html { redirect_to request_for_comment_url(rfc), notice: t('subscriptions.successfully_unsubscribed') }
|
||||
format.json { render json: {message: t('subscriptions.successfully_unsubscribed')}, status: :ok}
|
||||
end
|
||||
else
|
||||
respond_to do |format|
|
||||
format.html { redirect_to request_for_comment_url(rfc), :flash => { :danger => t('shared.message_failure') } }
|
||||
format.json { render json: {message: t('shared.message_failure')}, status: :internal_server_error}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def set_subscription
|
||||
@subscription = Subscription.find(params[:id])
|
||||
authorize!
|
||||
end
|
||||
private :set_subscription
|
||||
|
||||
def subscription_params
|
||||
current_user_id = current_user.try(:id)
|
||||
current_user_class_name = current_user.try(:class).try(:name)
|
||||
params[:subscription].permit(:request_for_comment_id, :subscription_type).merge(user_id: current_user_id, user_type: current_user_class_name)
|
||||
end
|
||||
private :subscription_params
|
||||
end
|
@ -21,6 +21,15 @@ class UserMailer < ActionMailer::Base
|
||||
mail(subject: t('mailers.user_mailer.got_new_comment.subject', commenting_user_displayname: @commenting_user_displayname), to: request_for_comment.user.email)
|
||||
end
|
||||
|
||||
def got_new_comment_for_subscription(comment, subscription, from_user)
|
||||
@receiver_displayname = subscription.user.displayname
|
||||
@author_displayname = from_user.displayname
|
||||
@comment_text = comment.text
|
||||
@rfc_link = request_for_comment_url(subscription.request_for_comment)
|
||||
@unsubscribe_link = unsubscribe_subscription_url(subscription)
|
||||
mail(subject: t('mailers.user_mailer.got_new_comment_for_subscription.subject', author_displayname: @author_displayname), to: subscription.user.email)
|
||||
end
|
||||
|
||||
def send_thank_you_note(request_for_comments, receiver)
|
||||
@receiver_displayname = receiver.displayname
|
||||
@author = request_for_comments.user.displayname
|
||||
|
@ -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, :date, :updated
|
||||
attr_accessor :username, :date, :updated, :editable
|
||||
|
||||
belongs_to :file, class_name: 'CodeOcean::File'
|
||||
belongs_to :user, polymorphic: true
|
||||
|
@ -5,6 +5,7 @@ class RequestForComment < ActiveRecord::Base
|
||||
belongs_to :file, class_name: 'CodeOcean::File'
|
||||
|
||||
has_many :comments, through: :submission
|
||||
has_many :subscriptions
|
||||
|
||||
scope :unsolved, -> { where(solved: [false, nil]) }
|
||||
|
||||
@ -37,6 +38,14 @@ class RequestForComment < ActiveRecord::Base
|
||||
submission.files.map { |file| file.comments.size}.sum
|
||||
end
|
||||
|
||||
def commenters
|
||||
commenters = []
|
||||
comments.distinct.to_a.each {|comment|
|
||||
commenters.append comment.user
|
||||
}
|
||||
commenters.uniq {|user| user.id}
|
||||
end
|
||||
|
||||
def to_s
|
||||
"RFC-" + self.id.to_s
|
||||
end
|
||||
|
4
app/models/subscription.rb
Normal file
4
app/models/subscription.rb
Normal file
@ -0,0 +1,4 @@
|
||||
class Subscription < ActiveRecord::Base
|
||||
belongs_to :user, polymorphic: true
|
||||
belongs_to :request_for_comment
|
||||
end
|
@ -12,14 +12,10 @@ class CommentPolicy < ApplicationPolicy
|
||||
everyone
|
||||
end
|
||||
|
||||
[:new?, :destroy?, :update?].each do |action|
|
||||
[:new?, :destroy?, :update?, :edit?].each do |action|
|
||||
define_method(action) { admin? || author? }
|
||||
end
|
||||
|
||||
def edit?
|
||||
admin?
|
||||
end
|
||||
|
||||
def index?
|
||||
everyone
|
||||
end
|
||||
|
18
app/policies/subscription_policy.rb
Normal file
18
app/policies/subscription_policy.rb
Normal file
@ -0,0 +1,18 @@
|
||||
class SubscriptionPolicy < ApplicationPolicy
|
||||
def create?
|
||||
everyone
|
||||
end
|
||||
|
||||
def destroy?
|
||||
author? || admin?
|
||||
end
|
||||
|
||||
def show_error?
|
||||
everyone
|
||||
end
|
||||
|
||||
def author?
|
||||
@user == @record.user
|
||||
end
|
||||
private :author?
|
||||
end
|
@ -1,4 +1,4 @@
|
||||
json.array!(@comments) do |comment|
|
||||
json.extract! comment, :id, :user_id, :file_id, :row, :column, :text, :username, :date, :updated
|
||||
json.extract! comment, :id, :user_id, :file_id, :row, :column, :text, :username, :date, :updated, :editable
|
||||
json.url comment_url(comment, format: :json)
|
||||
end
|
||||
|
@ -1,9 +1,11 @@
|
||||
h5 =t('exercises.implement.comment.addyours')
|
||||
|
||||
textarea.form-control(style='resize:none;')
|
||||
#otherComments
|
||||
h5 =t('exercises.implement.comment.others')
|
||||
pre#otherCommentsTextfield
|
||||
p = ''
|
||||
button#addCommentButton.btn.btn-block.btn-primary(type='button') =t('exercises.implement.comment.addCommentButton')
|
||||
button#removeAllButton.btn.btn-block.btn-warning(type='button') =t('exercises.implement.comment.removeAllOnLine')
|
||||
.container
|
||||
label
|
||||
input#subscribe type='checkbox' title=t('request_for_comments.subscribe_to_author') data-subscription=Subscription.where(user: current_user, request_for_comment_id: @request_for_comment.id).try(:first).try(:id)
|
||||
= t('request_for_comments.subscribe_to_author')
|
||||
|
||||
#myComment
|
||||
h5 =t('exercises.implement.comment.addyours')
|
||||
textarea.form-control
|
||||
button#addCommentButton.btn.btn-block.btn-primary(type='button') =t('exercises.implement.comment.addCommentButton')
|
@ -81,8 +81,13 @@ also, all settings from the rails model needed for the editor configuration in t
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
$('.modal-content').draggable({
|
||||
handle: '.modal-header'
|
||||
}).resizable({
|
||||
autoHide: true
|
||||
});
|
||||
|
||||
var solvedButton = $('#mark-as-solved-button');
|
||||
var commentOnExerciseButton = $('#comment-exercise-button');
|
||||
var addCommentExerciseButton = $('#addCommentExerciseButton');
|
||||
|
||||
var thankYouContainer = $('#thank-you-container');
|
||||
@ -138,102 +143,82 @@ also, all settings from the rails model needed for the editor configuration in t
|
||||
currentEditor.getSession().setMode($(editor).data('mode'));
|
||||
currentEditor.getSession().setOption("useWorker", false);
|
||||
|
||||
currentEditor.commentVisualsByLine = {};
|
||||
setAnnotations(currentEditor, $(editor).data('file-id'));
|
||||
currentEditor.on("guttermousedown", handleSidebarClick);
|
||||
currentEditor.on("guttermousemove", showPopover);
|
||||
});
|
||||
|
||||
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 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 generateCommentHtmlContent(comments) {
|
||||
var htmlContent = '';
|
||||
comments.forEach(function(comment, index) {
|
||||
var commentText = preprocess(comment.text);
|
||||
if (index !== 0) {
|
||||
htmlContent += '<div class="comment-divider"></div>'
|
||||
}
|
||||
htmlContent += '\
|
||||
<div class="comment" data-comment-id=' + comment.id + '> \
|
||||
<div class="comment-header"> \
|
||||
<div class="comment-username">' + preprocess(comment.username) + '</div> \
|
||||
<div class="comment-date">' + comment.date + '</div> \
|
||||
<div class="comment-updated' + (comment.updated ? '' : ' hidden') + '"> \
|
||||
<i class="fa fa-pencil" aria-hidden="true"></i> \
|
||||
<%= t('request_for_comments.comment_edited') %> \
|
||||
</div> \
|
||||
</div> \
|
||||
<div class="comment-content">' + commentText + '</div> \
|
||||
<textarea class="comment-editor">' + commentText + '</textarea> \
|
||||
<div class="comment-actions' + (comment.editable ? '' : ' hidden') + '"> \
|
||||
<button class="action-edit btn btn-xs btn-warning"><%= t('shared.edit') %></button> \
|
||||
<button class="action-delete btn btn-xs btn-danger"><%= t('shared.destroy') %></button> \
|
||||
</div> \
|
||||
</div>';
|
||||
});
|
||||
return htmlContent;
|
||||
}
|
||||
|
||||
function buildPopover(comments, where) {
|
||||
// only display the newest three comments in preview
|
||||
var maxComments = 3;
|
||||
var htmlContent = generateCommentHtmlContent(comments.reverse().slice(0, maxComments));
|
||||
if (comments.length > maxComments) {
|
||||
// add a hint that there are more comments than shown here
|
||||
htmlContent += '<div class="popover-footer"><%= t('request_for_comments.click_for_more_comments') %></div>'
|
||||
.replace('${numComments}', String(comments.length - maxComments));
|
||||
}
|
||||
where.popover({
|
||||
content: htmlContent,
|
||||
html: true, // necessary to style comments. XSS is not possible due to comment pre-processing (sanitizing)
|
||||
trigger: 'manual', // can only be triggered by $(where).popover('show' | 'hide')
|
||||
container: 'body'
|
||||
});
|
||||
}
|
||||
|
||||
function setAnnotations(editor, fileid) {
|
||||
var session = editor.getSession();
|
||||
|
||||
var jqrequest = $.ajax({
|
||||
dataType: 'json',
|
||||
method: 'GET',
|
||||
url: '/comments',
|
||||
data: {
|
||||
file_id: fileid
|
||||
}
|
||||
dataType: 'json',
|
||||
method: 'GET',
|
||||
url: '/comments',
|
||||
data: {
|
||||
file_id: fileid
|
||||
}
|
||||
});
|
||||
|
||||
jqrequest.done(function(response){
|
||||
// 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");
|
||||
}
|
||||
});
|
||||
session.setAnnotations(response);
|
||||
})
|
||||
}
|
||||
|
||||
function hasCommentsInRow(editor, row){
|
||||
return editor.getSession().getAnnotations().some(function(element) {
|
||||
return element.row === row;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function getCommentsForRow(editor, row){
|
||||
@ -242,23 +227,36 @@ also, all settings from the rails model needed for the editor configuration in t
|
||||
})
|
||||
}
|
||||
|
||||
function deleteComment(file_id, row, editor) {
|
||||
cleanupPopovers();
|
||||
function deleteComment(commentId, editor, file_id, callback) {
|
||||
var jqxhr = $.ajax({
|
||||
type: 'DELETE',
|
||||
url: "/comments",
|
||||
data: {
|
||||
row: row,
|
||||
file_id: file_id }
|
||||
url: "/comments/" + commentId
|
||||
});
|
||||
jqxhr.done(function (response) {
|
||||
jqxhr.done(function () {
|
||||
setAnnotations(editor, file_id);
|
||||
callback();
|
||||
});
|
||||
jqxhr.fail(ajaxError);
|
||||
}
|
||||
|
||||
function updateComment(commentId, text, editor, file_id, callback) {
|
||||
var jqxhr = $.ajax({
|
||||
type: 'PATCH',
|
||||
url: "/comments/" + commentId,
|
||||
data: {
|
||||
comment: {
|
||||
text: text
|
||||
}
|
||||
}
|
||||
});
|
||||
jqxhr.done(function () {
|
||||
setAnnotations(editor, file_id);
|
||||
callback();
|
||||
});
|
||||
jqxhr.fail(ajaxError);
|
||||
}
|
||||
|
||||
function createComment(file_id, row, editor, commenttext){
|
||||
cleanupPopovers();
|
||||
var jqxhr = $.ajax({
|
||||
data: {
|
||||
comment: {
|
||||
@ -273,71 +271,173 @@ also, all settings from the rails model needed for the editor configuration in t
|
||||
method: 'POST',
|
||||
url: "/comments"
|
||||
});
|
||||
jqxhr.done(function(response){
|
||||
jqxhr.done(function(){
|
||||
setAnnotations(editor, file_id);
|
||||
});
|
||||
jqxhr.fail(ajaxError);
|
||||
}
|
||||
|
||||
function createCommentOnExercise(file_id, row, editor, commenttext){
|
||||
var jqxhr = $.ajax({
|
||||
data: {
|
||||
comment: {
|
||||
file_id: file_id,
|
||||
row: row,
|
||||
column: 0,
|
||||
text: commenttext,
|
||||
request_id: $('h4#exercise_caption').data('rfc-id')
|
||||
}
|
||||
},
|
||||
dataType: 'json',
|
||||
method: 'POST',
|
||||
url: "/comments"
|
||||
});
|
||||
jqxhr.done(function(response){
|
||||
setAnnotations(editor, file_id);
|
||||
});
|
||||
jqxhr.fail(ajaxError);
|
||||
function subscribeToRFC(subscriptionType, checkbox){
|
||||
checkbox.attr("disabled", true);
|
||||
var jqxhr = $.ajax({
|
||||
data: {
|
||||
subscription: {
|
||||
request_for_comment_id: $('h4#exercise_caption').data('rfc-id'),
|
||||
subscription_type: subscriptionType
|
||||
}
|
||||
},
|
||||
dataType: 'json',
|
||||
method: 'POST',
|
||||
url: "/subscriptions.json"
|
||||
});
|
||||
jqxhr.done(function(subscription) {
|
||||
checkbox.data('subscription', subscription.id);
|
||||
checkbox.attr("disabled", false);
|
||||
});
|
||||
jqxhr.fail(function(response) {
|
||||
checkbox.prop('checked', false);
|
||||
checkbox.attr("disabled", false);
|
||||
ajaxError(response);
|
||||
});
|
||||
}
|
||||
|
||||
function unsubscribeFromRFC(checkbox) {
|
||||
checkbox.attr("disabled", true);
|
||||
var subscriptionId = checkbox.data('subscription');
|
||||
var jqxhr = $.ajax({
|
||||
url: '/subscriptions/' + subscriptionId + '/unsubscribe.json'
|
||||
});
|
||||
jqxhr.done(function(response) {
|
||||
checkbox.prop('checked', false);
|
||||
checkbox.data('subscription', null);
|
||||
checkbox.attr("disabled", false);
|
||||
$.flash.success({text: response.message});
|
||||
});
|
||||
jqxhr.fail(function(response) {
|
||||
checkbox.prop('checked', true);
|
||||
checkbox.attr("disabled", false);
|
||||
ajaxError(response);
|
||||
});
|
||||
}
|
||||
|
||||
var lastRow = null;
|
||||
var lastTarget = null;
|
||||
function showPopover(e) {
|
||||
var target = e.domEvent.target;
|
||||
var row = e.getDocumentPosition().row;
|
||||
|
||||
if (target.className.indexOf('ace_gutter-cell') === -1 || lastRow === row) {
|
||||
return;
|
||||
}
|
||||
if (lastTarget === target) {
|
||||
// sometimes the row gets updated before the DOM event target, so we need to wait for it to change
|
||||
return;
|
||||
}
|
||||
lastRow = row;
|
||||
|
||||
var editor = e.editor;
|
||||
var comments = getCommentsForRow(editor, row);
|
||||
buildPopover(comments, $(target));
|
||||
lastTarget = target;
|
||||
|
||||
$(target).popover('show');
|
||||
$(target).on('mouseleave', function () {
|
||||
$(this).off('mouseleave');
|
||||
$(this).popover('destroy');
|
||||
});
|
||||
}
|
||||
|
||||
$('.ace_gutter').on('mouseleave', function () {
|
||||
lastRow = null;
|
||||
lastTarget = null;
|
||||
});
|
||||
|
||||
function handleSidebarClick(e) {
|
||||
var target = e.domEvent.target;
|
||||
var editor = e.editor;
|
||||
if (target.className.indexOf('ace_gutter-cell') === -1) return;
|
||||
|
||||
if (target.className.indexOf("ace_gutter-cell") == -1) return;
|
||||
var editor = e.editor;
|
||||
var fileid = $(editor.container).data('file-id');
|
||||
|
||||
var row = e.getDocumentPosition().row;
|
||||
e.stop();
|
||||
$('.modal-title').text('<%= t('request_for_comments.modal_title') %>'.replace('${line}', row + 1));
|
||||
|
||||
var commentModal = $('#comment-modal');
|
||||
|
||||
if (hasCommentsInRow(editor, row)) {
|
||||
var rowComments = getCommentsForRow(editor, row);
|
||||
var comments = _.pluck(rowComments, 'text').join('\n');
|
||||
commentModal.find('#otherComments').show();
|
||||
commentModal.find('#otherCommentsTextfield').text(comments);
|
||||
var otherComments = commentModal.find('#otherComments');
|
||||
var htmlContent = generateCommentHtmlContent(getCommentsForRow(editor, row));
|
||||
if (htmlContent) {
|
||||
otherComments.show();
|
||||
var container = otherComments.find('.container');
|
||||
container.html(htmlContent);
|
||||
|
||||
var deleteButtons = container.find('.action-delete');
|
||||
deleteButtons.on('click', function (event) {
|
||||
var button = $(event.target);
|
||||
var parent = $(button).parent().parent();
|
||||
var commentId = parent.data('comment-id');
|
||||
|
||||
deleteComment(commentId, editor, fileid, function () {
|
||||
parent.html('<div class="comment-removed"><%= t('comments.deleted') %></div>');
|
||||
});
|
||||
});
|
||||
|
||||
var editButtons = container.find('.action-edit');
|
||||
editButtons.on('click', function (event) {
|
||||
var button = $(event.target);
|
||||
var parent = $(button).parent().parent();
|
||||
var commentId = parent.data('comment-id');
|
||||
var currentlyEditing = button.data('editing');
|
||||
|
||||
var deleteButton = parent.find('.action-delete');
|
||||
var commentContent = parent.find('.comment-content');
|
||||
var commentEditor = parent.find('textarea.comment-editor');
|
||||
var commentUpdated = parent.find('.comment-updated');
|
||||
|
||||
if (currentlyEditing) {
|
||||
updateComment(commentId, commentEditor.val(), editor, fileid, function () {
|
||||
button.text('<%= t('shared.edit') %>');
|
||||
button.data('editing', false);
|
||||
commentContent.text(commentEditor.val());
|
||||
deleteButton.show();
|
||||
commentContent.show();
|
||||
commentEditor.hide();
|
||||
commentUpdated.removeClass('hidden');
|
||||
});
|
||||
} else {
|
||||
button.text('<%= t('comments.save_update') %>');
|
||||
button.data('editing', true);
|
||||
deleteButton.hide();
|
||||
commentContent.hide();
|
||||
commentEditor.show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
commentModal.find('#otherComments').hide();
|
||||
otherComments.hide();
|
||||
}
|
||||
|
||||
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.find('textarea').val('') ;
|
||||
commentModal.modal('hide');
|
||||
var subscribeCheckbox = commentModal.find('#subscribe');
|
||||
subscribeCheckbox.prop('checked', subscribeCheckbox.data('subscription'));
|
||||
subscribeCheckbox.off('change');
|
||||
subscribeCheckbox.on('change', function() {
|
||||
if (this.checked) {
|
||||
subscribeToRFC('author', $(this));
|
||||
} else {
|
||||
unsubscribeFromRFC($(this));
|
||||
}
|
||||
});
|
||||
|
||||
commentModal.find('#removeAllButton').on('click', function(e){
|
||||
var file_id = $(editor.container).data('file-id');
|
||||
deleteComment(file_id, row, editor);
|
||||
commentModal.modal('hide');
|
||||
var addCommentButton = commentModal.find('#addCommentButton');
|
||||
addCommentButton.off('click');
|
||||
addCommentButton.on('click', function(){
|
||||
var commentTextarea = commentModal.find('#myComment > textarea');
|
||||
var commenttext = commentTextarea.val();
|
||||
if (commenttext !== "") {
|
||||
createComment(fileid, row, editor, commenttext);
|
||||
commentTextarea.val('') ;
|
||||
commentModal.modal('hide');
|
||||
}
|
||||
});
|
||||
|
||||
commentModal.modal('show');
|
||||
@ -351,19 +451,4 @@ also, all settings from the rails model needed for the editor configuration in t
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function stringDivider(str, width, spaceReplacer) {
|
||||
if (str.length>width) {
|
||||
var p=width;
|
||||
for (;p>0 && str[p]!=' ';p--) {
|
||||
}
|
||||
if (p>0) {
|
||||
var left = str.substring(0, p);
|
||||
var right = str.substring(p+1);
|
||||
return left + spaceReplacer + stringDivider(right, width, spaceReplacer);
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
@ -0,0 +1,7 @@
|
||||
== t('mailers.user_mailer.got_new_comment_for_subscription.body',
|
||||
receiver_displayname: @receiver_displayname, link_to_comment: link_to(@rfc_link, @rfc_link),
|
||||
unsubscribe_link: link_to(@unsubscribe_link, @unsubscribe_link),
|
||||
author_displayname: @author_displayname,
|
||||
comment_text: @comment_text,
|
||||
link_my_comments: link_to(t('request_for_comments.index.get_my_comment_requests'), my_request_for_comments_url),
|
||||
link_all_comments: link_to(t('request_for_comments.index.all'), request_for_comments_url) )
|
@ -159,6 +159,9 @@ de:
|
||||
user_exercise_feedback:
|
||||
one: Feedback
|
||||
other: Feedback
|
||||
comment:
|
||||
one: Kommentar
|
||||
other: Kommentare
|
||||
errors:
|
||||
messages:
|
||||
together: 'muss zusammen mit %{attribute} definiert werden'
|
||||
@ -449,13 +452,48 @@ de:
|
||||
<br>
|
||||
This mail was automatically sent by CodeOcean. <br>
|
||||
subject: "%{author} sagt Danke!"
|
||||
got_new_comment_for_subscription:
|
||||
body: |
|
||||
English version below <br>
|
||||
_________________________<br>
|
||||
<br>
|
||||
Hallo %{receiver_displayname}, <br>
|
||||
<br>
|
||||
es gibt einen neuen Kommentar von %{author_displayname} zu einer Kommentaranfrage auf CodeOcean, die Sie abonniert haben. <br>
|
||||
<br>
|
||||
%{author_displayname} schreibt: %{comment_text}<br>
|
||||
<br>
|
||||
Sie finden die Kommentaranfrage hier: %{link_to_comment} <br>
|
||||
<br>
|
||||
Falls Sie beim Klick auf diesen Link eine Fehlermeldung erhalten, dass Sie nicht berechtigt wären diese Aktion auszuführen, öffnen Sie bitte eine beliebige Programmieraufgabe aus einem Kurs heraus und klicken den Link danach noch einmal.<br>
|
||||
<br>
|
||||
Wenn Sie keine weiteren Benachrichtigungen zu dieser Anfrage erhalten möchten, klicken Sie bitte hier: %{unsubscribe_link}
|
||||
<br>
|
||||
Diese Mail wurde automatisch von CodeOcean verschickt.<br>
|
||||
<br>
|
||||
_________________________<br>
|
||||
<br>
|
||||
Dear %{receiver_displayname}, <br>
|
||||
<br>
|
||||
you received a new comment from %{author_displayname} to a request for comments on CodeOcean that you have subscribed to. <br>
|
||||
<br>
|
||||
%{author_displayname} wrote: %{comment_text} <br>
|
||||
<br>
|
||||
You can find the request for comments here: %{link_to_comment} <br>
|
||||
<br>
|
||||
If you receive an error that you are not authorized to perform this action when clicking the link, please log-in through any course exercise beforehand and click the link again. <br>
|
||||
<br>
|
||||
If you don't want to be notified about further comments, please click here: %{unsubscribe_link}
|
||||
<br>
|
||||
This mail was automatically sent by CodeOcean. <br>
|
||||
subject: "%{author_displayname} hat einen neuen Kommentar in einer Diskussion veröffentlicht, die Sie abonniert haben."
|
||||
request_for_comments:
|
||||
click_here: Zum Kommentieren auf die Seitenleiste klicken!
|
||||
comments: Kommentare
|
||||
howto: |
|
||||
Um Kommentare zu einer Programmzeile hinzuzufügen, kann einfach auf die jeweilige Zeilennummer auf der linken Seite geklickt werden. <br>
|
||||
Es öffnet sich ein Textfeld, in dem der Kommentar eingetragen werden kann. <br>
|
||||
Mit "Kommentieren" wird der Kommentar dann gesichert und taucht als Sprechblase neben der Zeile auf.
|
||||
Mit "Kommentar abschicken" wird der Kommentar dann gesichert und taucht als Sprechblase neben der Zeile auf.
|
||||
howto_title: 'Anleitung:'
|
||||
index:
|
||||
get_my_comment_requests: Meine Kommentaranfragen
|
||||
@ -473,6 +511,9 @@ de:
|
||||
send_thank_you_note: "Senden"
|
||||
cancel_thank_you_note: "Nichts senden"
|
||||
comment_edited: "bearbeitet"
|
||||
modal_title: "Einen Kommentar in Zeile ${line} hinzufügen"
|
||||
click_for_more_comments: "Klicken um ${numComments} weitere Kommentare zu sehen..."
|
||||
subscribe_to_author: "Bei neuen Kommentaren des Autors per E-Mail benachrichtigt werden"
|
||||
sessions:
|
||||
create:
|
||||
failure: Fehlerhafte E-Mail oder Passwort.
|
||||
@ -587,4 +628,10 @@ de:
|
||||
estimated_time_20_to_30: "zwischen 20 und 30 Minuten"
|
||||
estimated_time_more_30: "mehr als 30 Minuten"
|
||||
working_time: "Geschätze Bearbeitungszeit für diese Aufgabe:"
|
||||
comments:
|
||||
deleted: "Gelöscht"
|
||||
save_update: "Speichern"
|
||||
subscriptions:
|
||||
successfully_unsubscribed: "Ihr Abonnement für weitere Kommentare auf dieser Kommentaranfrage wurde erfolgreich beendet."
|
||||
subscription_not_existent: "Das Abonnement, von dem Sie sich abmelden wollen, existiert nicht."
|
||||
|
||||
|
@ -159,6 +159,9 @@ en:
|
||||
user_exercise_feedback:
|
||||
one: Feedback
|
||||
other: Feedback
|
||||
comment:
|
||||
one: Comment
|
||||
other: Comments
|
||||
errors:
|
||||
messages:
|
||||
together: 'has to be set along with %{attribute}'
|
||||
@ -449,6 +452,41 @@ en:
|
||||
<br>
|
||||
This mail was automatically sent by CodeOcean. <br>
|
||||
subject: "%{author} says thank you!"
|
||||
got_new_comment_for_subscription:
|
||||
body: |
|
||||
English version below <br>
|
||||
_________________________<br>
|
||||
<br>
|
||||
Hallo %{receiver_displayname}, <br>
|
||||
<br>
|
||||
es gibt einen neuen Kommentar von %{author_displayname} zu einer Kommentaranfrage auf CodeOcean, die Sie abonniert haben. <br>
|
||||
<br>
|
||||
%{author_displayname} schreibt: %{comment_text}<br>
|
||||
<br>
|
||||
Sie finden die Kommentaranfrage hier: %{link_to_comment} <br>
|
||||
<br>
|
||||
Falls Sie beim Klick auf diesen Link eine Fehlermeldung erhalten, dass Sie nicht berechtigt wären diese Aktion auszuführen, öffnen Sie bitte eine beliebige Programmieraufgabe aus einem Kurs heraus und klicken den Link danach noch einmal.<br>
|
||||
<br>
|
||||
Wenn Sie keine weiteren Benachrichtigungen zu dieser Anfrage erhalten möchten, klicken Sie bitte hier: %{unsubscribe_link}
|
||||
<br>
|
||||
Diese Mail wurde automatisch von CodeOcean verschickt.<br>
|
||||
<br>
|
||||
_________________________<br>
|
||||
<br>
|
||||
Dear %{receiver_displayname}, <br>
|
||||
<br>
|
||||
you received a new comment from %{author_displayname} to a request for comments on CodeOcean that you have subscribed to. <br>
|
||||
<br>
|
||||
%{author_displayname} wrote: %{comment_text} <br>
|
||||
<br>
|
||||
You can find the request for comments here: %{link_to_comment} <br>
|
||||
<br>
|
||||
If you receive an error that you are not authorized to perform this action when clicking the link, please log-in through any course exercise beforehand and click the link again. <br>
|
||||
<br>
|
||||
If you don't want to be notified about further comments, please click here: %{unsubscribe_link}
|
||||
<br>
|
||||
This mail was automatically sent by CodeOcean. <br>
|
||||
subject: "%{author_displayname} has posted a new comment to a discussion you subscribed to on CodeOcean."
|
||||
request_for_comments:
|
||||
click_here: Click on this sidebar to comment!
|
||||
comments: Comments
|
||||
@ -473,6 +511,9 @@ en:
|
||||
send_thank_you_note: "Send"
|
||||
cancel_thank_you_note: "Don't send"
|
||||
comment_edited: "edited"
|
||||
modal_title: "Add a comment to line ${line}"
|
||||
click_for_more_comments: "Click to view ${numComments} more comments..."
|
||||
subscribe_to_author: "Receive E-Mail notifications for new comments of the original author"
|
||||
sessions:
|
||||
create:
|
||||
failure: Invalid email or password.
|
||||
@ -587,3 +628,9 @@ en:
|
||||
estimated_time_20_to_30: "between 20 and 30 minutes"
|
||||
estimated_time_more_30: "more than 30 minutes"
|
||||
working_time: "Estimated time working on this exercise:"
|
||||
comments:
|
||||
deleted: "Deleted"
|
||||
save_update: "Save"
|
||||
subscriptions:
|
||||
successfully_unsubscribed: "You successfully unsubscribed from this Request for Comment"
|
||||
subscription_not_existent: "The subscription you want to unsubscribe from does not exist."
|
||||
|
@ -14,17 +14,19 @@ Rails.application.routes.draw do
|
||||
post :set_thank_you_note
|
||||
end
|
||||
end
|
||||
resources :comments, except: [:destroy] do
|
||||
collection do
|
||||
delete :destroy
|
||||
end
|
||||
end
|
||||
resources :comments
|
||||
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'
|
||||
|
||||
resources :subscriptions do
|
||||
member do
|
||||
get :unsubscribe, to: 'subscriptions#destroy'
|
||||
end
|
||||
end
|
||||
|
||||
root to: 'application#welcome'
|
||||
|
||||
namespace :admin do
|
||||
|
11
db/migrate/20170906124500_create_subscriptions.rb
Normal file
11
db/migrate/20170906124500_create_subscriptions.rb
Normal file
@ -0,0 +1,11 @@
|
||||
class CreateSubscriptions < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :subscriptions do |t|
|
||||
t.belongs_to :user, polymorphic: true
|
||||
t.references :request_for_comment
|
||||
t.string :type
|
||||
|
||||
t.timestamps null: false
|
||||
end
|
||||
end
|
||||
end
|
5
db/migrate/20170913054203_rename_subscription_type.rb
Normal file
5
db/migrate/20170913054203_rename_subscription_type.rb
Normal file
@ -0,0 +1,5 @@
|
||||
class RenameSubscriptionType < ActiveRecord::Migration
|
||||
def change
|
||||
rename_column :subscriptions, :type, :subscription_type
|
||||
end
|
||||
end
|
11
db/schema.rb
11
db/schema.rb
@ -11,7 +11,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20170608141612) do
|
||||
ActiveRecord::Schema.define(version: 20170913054203) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
@ -281,6 +281,15 @@ ActiveRecord::Schema.define(version: 20170608141612) do
|
||||
add_index "submissions", ["exercise_id"], name: "index_submissions_on_exercise_id", using: :btree
|
||||
add_index "submissions", ["user_id"], name: "index_submissions_on_user_id", using: :btree
|
||||
|
||||
create_table "subscriptions", force: :cascade do |t|
|
||||
t.integer "user_id"
|
||||
t.string "user_type"
|
||||
t.integer "request_for_comment_id"
|
||||
t.string "subscription_type"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
create_table "tags", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.datetime "created_at"
|
||||
|
7
test/controllers/subscription_controller_test.rb
Normal file
7
test/controllers/subscription_controller_test.rb
Normal file
@ -0,0 +1,7 @@
|
||||
require 'test_helper'
|
||||
|
||||
class SubscriptionControllerTest < ActionController::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
7
test/factories/subscriptions.rb
Normal file
7
test/factories/subscriptions.rb
Normal file
@ -0,0 +1,7 @@
|
||||
FactoryGirl.define do
|
||||
factory :subscription do
|
||||
user nil
|
||||
request_for_comments nil
|
||||
type ""
|
||||
end
|
||||
end
|
7
test/models/subscription_test.rb
Normal file
7
test/models/subscription_test.rb
Normal file
@ -0,0 +1,7 @@
|
||||
require 'test_helper'
|
||||
|
||||
class SubscriptionTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
8
vendor/assets/javascripts/jquery-ui.min.js
vendored
Normal file
8
vendor/assets/javascripts/jquery-ui.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
6
vendor/assets/stylesheets/jquery-ui.min.css
vendored
Normal file
6
vendor/assets/stylesheets/jquery-ui.min.css
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
/*! jQuery UI - v1.12.1 - 2017-09-05
|
||||
* http://jqueryui.com
|
||||
* Includes: draggable.css, core.css, resizable.css, selectable.css, sortable.css
|
||||
* Copyright jQuery Foundation and other contributors; Licensed MIT */
|
||||
|
||||
.ui-draggable-handle{-ms-touch-action:none;touch-action:none}.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable{-ms-touch-action:none;touch-action:none}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-sortable-handle{-ms-touch-action:none;touch-action:none}
|
5
vendor/assets/stylesheets/jquery-ui.structure.min.css
vendored
Normal file
5
vendor/assets/stylesheets/jquery-ui.structure.min.css
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/*! jQuery UI - v1.12.1 - 2017-09-05
|
||||
* http://jqueryui.com
|
||||
* Copyright jQuery Foundation and other contributors; Licensed MIT */
|
||||
|
||||
.ui-draggable-handle{-ms-touch-action:none;touch-action:none}.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable{-ms-touch-action:none;touch-action:none}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-sortable-handle{-ms-touch-action:none;touch-action:none}
|
Reference in New Issue
Block a user