Skip to content

Commit 1136443

Browse files
committed
feat: Allow users to report comments
Comments can be used to potentially harass users who request comments. A button was added to report a message as spam. Resolves #2715
1 parent e58a2eb commit 1136443

26 files changed

+310
-61
lines changed

app/assets/javascripts/request_for_comments.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ $(document).on('turbo-migration:load', function () {
116116
<button class="action-edit btn btn-sm btn-warning">' + I18n.t('shared.edit') + '</button> \
117117
<button class="action-delete btn btn-sm btn-danger">' + I18n.t('shared.destroy') + '</button> \
118118
</div> \
119+
<div class="text-warning' + (comment.reportable ? '' : ' d-none') + '"> \
120+
<button class="action-report btn btn-light btn-sm">' + I18n.t('shared.report') + '</button> \
121+
</div> \
119122
</div>';
120123
});
121124
return htmlContent;
@@ -166,6 +169,17 @@ $(document).on('turbo-migration:load', function () {
166169
})
167170
}
168171

172+
function reportComment(commentId, callback) {
173+
const jqxhr = $.ajax({
174+
type: 'POST',
175+
url: Routes.report_comment_path(commentId)
176+
});
177+
jqxhr.done(function () {
178+
callback();
179+
});
180+
jqxhr.fail(ajaxError);
181+
}
182+
169183
function deleteComment(commentId, editor, file_id, callback) {
170184
const jqxhr = $.ajax({
171185
type: 'DELETE',
@@ -313,6 +327,17 @@ $(document).on('turbo-migration:load', function () {
313327
const container = otherComments.find('.container');
314328
container.html(htmlContent);
315329

330+
const reportButtons = container.find('.action-report');
331+
reportButtons.on('click', function (event) {
332+
const button = $(event.target);
333+
const parent = $(button).parent().parent();
334+
const commentId = parent.data('comment-id');
335+
336+
reportComment(commentId, function () {
337+
parent.html('<div class="comment-reported">' + I18n.t('comments.reported') + '</div>');
338+
});
339+
});
340+
316341
const deleteButtons = container.find('.action-delete');
317342
deleteButtons.on('click', function (event) {
318343
const button = $(event.target);

app/controllers/comments_controller.rb

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# frozen_string_literal: true
22

33
class CommentsController < ApplicationController
4-
before_action :set_comment, only: %i[show update destroy]
4+
before_action :set_comment, only: %i[show update destroy report]
55

66
def authorize!
77
authorize(@comment || @comments)
@@ -15,12 +15,6 @@ def index
1515
submission = Submission.find_by(id: file.context_id)
1616
if submission
1717
@comments = Comment.where(file_id: params[:file_id])
18-
@comments.map do |comment|
19-
comment.username = comment.user.displayname
20-
comment.date = comment.created_at.strftime('%d.%m.%Y %k:%M')
21-
comment.updated = (comment.created_at != comment.updated_at)
22-
comment.editable = policy(comment).edit?
23-
end
2418
else
2519
@comments = []
2620
end
@@ -67,6 +61,15 @@ def destroy
6761
head :no_content
6862
end
6963

64+
# POST /comments/1/report.json
65+
def report
66+
authorize!
67+
68+
ReportMailer.with(reported_content: @comment).report_content.deliver_later
69+
70+
head :no_content
71+
end
72+
7073
private
7174

7275
# Use callbacks to share common setup or constraints between actions.

app/controllers/request_for_comments_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ def report
168168

169169
ReportMailer.with(reported_content: @request_for_comment).report_content.deliver_later
170170

171-
redirect_to @request_for_comment, notice: t('.report.reported'), status: :see_other
171+
redirect_to @request_for_comment, notice: t('.reported'), status: :see_other
172172
end
173173

174174
private

app/mailers/report_mailer.rb

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,8 @@ class ReportMailer < ApplicationMailer
44
default to: CodeOcean::Config.new(:code_ocean).read.dig(:content_moderation, :report_emails)
55

66
def report_content
7-
@reported_content = params.fetch(:reported_content)
8-
study_group = @reported_content.submission.study_group
9-
exercise = @reported_content.exercise
7+
@spam_report = SpamReport.new(reported_content: params.fetch(:reported_content))
108

11-
# NOTE: This implementation assumes the course URL is static and does not vary per user.
12-
# This is currently valid for a majority of use cases. However, in dynamic scenarios (such as
13-
# content trees in openHPI used in conjunction with A/B/n testing) this assumption may no longer hold true.
14-
@course_url = LtiParameter.find_by(study_group:, exercise:)&.lti_parameters&.[]('launch_presentation_return_url')
15-
16-
mail(subject: I18n.t('report_mailer.report_content.subject', content_name: @reported_content.model_name.human))
9+
mail(subject: I18n.t('report_mailer.report_content.subject', human_model_name: @spam_report.human_model_name))
1710
end
1811
end

app/models/comment.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ class Comment < ApplicationRecord
55
include Creation
66
include ActionCableHelper
77

8-
attr_accessor :username, :date, :updated, :editable
9-
108
belongs_to :file, class_name: 'CodeOcean::File'
119
has_one :submission, through: :file, source: :context, source_type: 'Submission'
1210
has_one :request_for_comment, through: :submission

app/models/spam_report.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# frozen_string_literal: true
2+
3+
class SpamReport
4+
def initialize(reported_content:)
5+
unless [Comment, RequestForComment].include?(reported_content.class)
6+
raise("#{reported_content.model_name} is not configured for spam reports.")
7+
end
8+
9+
@reported_content = reported_content
10+
end
11+
12+
# NOTE: This implementation assumes the course URL is static and does not vary per user.
13+
# This is currently valid for a majority of use cases. However, in dynamic scenarios (such as
14+
# content trees in openHPI used in conjunction with A/B/n testing) this assumption may no
15+
# longer hold true.
16+
def course_url = lti_parameters['launch_presentation_return_url']
17+
18+
def human_model_name = reported_content.model_name.human
19+
20+
def reported_message
21+
case reported_content
22+
when RequestForComment
23+
reported_content.question
24+
when Comment
25+
reported_content.text
26+
end
27+
end
28+
29+
def related_request_for_comment
30+
case reported_content
31+
when RequestForComment
32+
reported_content
33+
when Comment
34+
RequestForComment.find_by!(file: reported_content.file)
35+
end
36+
end
37+
38+
private
39+
40+
attr_reader :reported_content
41+
42+
def lti_parameters = LtiParameter.find_by(study_group:, exercise:)&.lti_parameters || {}
43+
44+
def study_group
45+
reported_content.submission.study_group
46+
end
47+
48+
def exercise
49+
related_request_for_comment.exercise
50+
end
51+
end

app/policies/comment_policy.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# frozen_string_literal: true
22

33
class CommentPolicy < ApplicationPolicy
4+
REPORT_RECEIVER_CONFIGURED = CodeOcean::Config.new(:code_ocean).read.dig(:content_moderation, :report_emails).present?
5+
46
def create?
57
everyone
68
end
@@ -16,4 +18,8 @@ def show?
1618
def index?
1719
everyone
1820
end
21+
22+
def report?
23+
REPORT_RECEIVER_CONFIGURED && everyone && !author?
24+
end
1925
end
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# frozen_string_literal: true
22

33
json.array!(@comments) do |comment|
4-
json.extract! comment, :id, :user_id, :file_id, :row, :column, :text, :username, :date, :updated, :editable
4+
json.extract! comment, :id, :user_id, :file_id, :row, :column, :text
5+
json.username comment.user.displayname
6+
json.date comment.created_at.strftime('%d.%m.%Y %k:%M')
7+
json.updated(comment.created_at != comment.updated_at)
8+
json.editable policy(comment).edit?
9+
json.reportable policy(comment).report?
510
json.url comment_url(comment, format: :json)
611
end
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
h3 = t('.prolog')
2-
blockquote style="white-space: pre-wrap;" = @reported_content.question
2+
blockquote style="white-space: pre-wrap;" = @spam_report.reported_message
33
p = t('.take_action')
4-
p = link_to(request_for_comment_url(@reported_content), request_for_comment_url(@reported_content))
5-
- if @course_url.present?
6-
p = link_to(t('.authentication'), @course_url)
4+
p = link_to(request_for_comment_url(@spam_report.related_request_for_comment), request_for_comment_url(@spam_report.related_request_for_comment))
5+
- if @spam_report.course_url.present?
6+
p = link_to(t('.authentication'), @spam_report.course_url)
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
== t('.prolog')
22
== "\n\n"
3-
== @reported_content.question.lines.map { "> #{it}" }.join
3+
== @spam_report.reported_message.lines.map { "> #{it}" }.join
44
== "\n\n"
55
== t('.take_action')
66
== "\n\n"
7-
== request_for_comment_url(@reported_content)
7+
== request_for_comment_url(@spam_report.related_request_for_comment)
88
== "\n\n"
9-
- if @course_url.present?
9+
- if @spam_report.course_url.present?
1010
== t('.authentication')
1111
== "\n\n"
12-
== @course_url
12+
== @spam_report.course_url

0 commit comments

Comments
 (0)