Skip to content

Commit 0103086

Browse files
authored
Merge pull request #1766 from codidact/0valt/1289/flag-validations
Flagging improvements
2 parents 097fdce + 4965b39 commit 0103086

File tree

9 files changed

+153
-55
lines changed

9 files changed

+153
-55
lines changed

app/assets/javascripts/flags.js

Lines changed: 20 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
$(() => {
2-
$(document).on('click', '.flag-link', (ev) => {
2+
$(document).on('click', '.flag-link', async (ev) => {
33
ev.preventDefault();
44
const self = $(ev.target);
55
const isCommentFlag = self.hasClass('js-comment-flag');
@@ -21,6 +21,10 @@ $(() => {
2121
}
2222

2323
const postId = self.data('post-id');
24+
25+
/**
26+
* @type {QPixelFlagData}
27+
*/
2428
const data = {
2529
'flag_type': (reason !== -1) ? reason : null,
2630
'post_id': postId,
@@ -29,47 +33,26 @@ $(() => {
2933
};
3034

3135
if (requiresDetails && data['reason'].length < 1) {
32-
QPixel.createNotification('danger',
33-
'Details are required for this flag type - please enter a message.');
36+
QPixel.createNotification('danger', 'Details are required for this flag type - please enter a message.');
3437
return;
3538
}
3639

37-
const responseType = isCommentFlag ? null : activeRadio.data('response-type');
40+
const closeFlagModal = () => {
41+
self.parents('.js-flag-box').removeClass('is-active');
42+
$(`#flag-comment-${postId}`).removeClass('is-active');
43+
};
3844

39-
$.ajax({
40-
'type': 'POST',
41-
'url': '/flags/new',
42-
'data': data,
43-
// TODO: review
44-
// 'target': self
45-
})
46-
.done((response) => {
47-
if(response.status !== 'success') {
48-
QPixel.createNotification('danger', '<strong>Failed:</strong> ' + response.message);
49-
}
50-
else {
51-
const messages = {
52-
comment: `<strong>Thanks!</strong> Your flag has been added as a comment for the author to review.`
53-
};
54-
const defaultMessage = `<strong>Thanks!</strong> We will review your flag.`;
55-
QPixel.createNotification('success', messages[responseType] || defaultMessage);
56-
$(`#flag-post-${postId}`).val('');
57-
}
58-
self.parents('.js-flag-box').removeClass('is-active');
59-
$(`#flag-comment-${postId}`).removeClass('is-active');
45+
try {
46+
const response = await QPixel.flag(data);
6047

61-
})
62-
.fail((jqXHR, _textStatus, _errorThrown) => {
63-
let message = jqXHR.status;
64-
try {
65-
message = JSON.parse(jqXHR.responseText)['message'];
66-
}
67-
finally {
68-
QPixel.createNotification('danger', '<strong>Failed:</strong> ' + message);
69-
}
70-
self.parents('.js-flag-box').removeClass('is-active');
71-
$(`#flag-comment-${postId}`).removeClass('is-active');
72-
});
48+
QPixel.handleJSONResponse(response, (data) => {
49+
QPixel.createNotification('success', data.message);
50+
$(`#flag-post-${postId}`).val('');
51+
}, closeFlagModal);
52+
} catch(e) {
53+
console.warn(`[flags/new] API error:`, e);
54+
QPixel.createNotification('danger', 'Failed to flag.');
55+
}
7356
});
7457

7558
$('.js-start-escalate').on('click', (ev) => {

app/assets/javascripts/qpixel_api.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -443,13 +443,25 @@ window.QPixel = {
443443
return content;
444444
},
445445

446-
handleJSONResponse: (data, onSuccess) => {
446+
handleJSONResponse: (data, onSuccess, onFinally) => {
447447
if (data.status === 'success') {
448-
onSuccess(data)
448+
onSuccess(data);
449449
}
450450
else {
451451
QPixel.createNotification('danger', data.message);
452452
}
453+
454+
onFinally?.(data);
455+
},
456+
457+
flag: async (flag) => {
458+
const resp = await QPixel.fetchJSON(`/flags/new`, { ...flag }, {
459+
headers: { 'Accept': 'application/json' }
460+
});
461+
462+
const data = await resp.json();
463+
464+
return data;
453465
},
454466

455467
deleteComment: async (id) => {

app/controllers/flags_controller.rb

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@ def new
1414
max_flags_per_day = SiteSetting[current_user.privilege?('unrestricted') ? 'RL_Flags' : 'RL_NewUserFlags']
1515

1616
if recent_flags >= max_flags_per_day
17-
flag_limit_msg = 'Thank you. Flags from people like you help us keep this site clean. ' \
18-
"However, you have reached your daily flag limit of #{max_flags_per_day} " \
19-
'flags. Please come back tomorrow to continue flagging.'
17+
flag_limit_msg = I18n.t('flags.errors.rate_limited', count: max_flags_per_day)
2018

2119
AuditLog.rate_limit_log(event_type: 'flag', related: Post.find(params[:post_id]), user: current_user,
2220
comment: "limit: #{max_flags_per_day}\n\ntype:#{type}\ncomment:\n#{params[:reason].to_i}")
@@ -27,16 +25,19 @@ def new
2725

2826
if type&.name == "needs author's attention"
2927
create_as_feedback_comment Post.find(params[:post_id]), current_user, params[:reason]
30-
render json: { status: 'success' }, status: :created
28+
render json: { status: 'success', message: I18n.t('flags.success.create_author_attention') },
29+
status: :created
3130
return
3231
end
3332

3433
@flag = Flag.new(post_flag_type: type, reason: params[:reason], post_id: params[:post_id],
3534
post_type: params[:post_type], user: current_user)
3635
if @flag.save
37-
render json: { status: 'success' }, status: :created
36+
render json: { status: 'success', message: I18n.t('flags.success.create_generic') },
37+
status: :created
3838
else
39-
render json: { status: 'failed', message: 'Flag failed to save.' }, status: :internal_server_error
39+
render json: { status: 'failed', message: I18n.t('flags.errors.create_generic') },
40+
status: :bad_request
4041
end
4142
end
4243

app/models/flag.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,18 @@ class Flag < ApplicationRecord
2121

2222
scope :escalated, -> { where(escalated: true) }
2323

24+
validate :maximum_reason_length
25+
2426
# Checks if the flag is confidential as per its type
2527
# @return [Boolean] check result
2628
def confidential?
2729
post_flag_type&.confidential || false
2830
end
31+
32+
def maximum_reason_length
33+
max_len = SiteSetting['MaxFlagReasonLength'] || 1000
34+
if reason.length > [max_len, 1000].min
35+
errors.add(:reason, "can't be more than #{max_len} characters")
36+
end
37+
end
2938
end
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
en:
2+
flags:
3+
errors:
4+
create_generic: >
5+
Failed to raise the flag.
6+
rate_limited: >
7+
Thank you. Flags from people like you help us keep this site clean.
8+
However, you have reached your daily flag limit of %{count} flags.
9+
Please come back tomorrow to continue flagging.
10+
success:
11+
create_generic: >
12+
<strong>Thanks!</strong> We will review your flag.
13+
create_author_attention: >
14+
<strong>Thanks!</strong> Your flag was added as a comment for the author to review.

db/seeds/site_settings.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,13 @@
143143
description: >
144144
A link to the full legal code of the license specified in ContentLicenseName.
145145
146+
- name: MaxFlagReasonLength
147+
value: 1000
148+
value_type: integer
149+
category: SiteDetails
150+
description: >
151+
The maximum number of characters a single flag's reason may contain. Default is 1000
152+
146153
- name: MaxTagLength
147154
value: 35
148155
value_type: integer

global.d.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,13 @@ type QPixelComment = {
129129
references_comment_id: string | null
130130
}
131131

132+
type QPixelFlagData = {
133+
flag_type: number | null
134+
post_id: string
135+
post_type: 'Comment' | 'Post'
136+
reason?: string
137+
}
138+
132139
interface GetThreadContentOptions {
133140
inline?: boolean,
134141
showDeleted?: boolean
@@ -315,8 +322,11 @@ interface QPixel {
315322
* Processes JSON responses from QPixel API
316323
* @param data
317324
* @param onSuccess callback to call for successful requests
325+
* @param onFinally callback to call for all requests
318326
*/
319-
handleJSONResponse?: <T extends QPixelResponseJSON>(data: T, onSuccess: (data: T) => void) => void
327+
handleJSONResponse?: <T extends QPixelResponseJSON>(data: T,
328+
onSuccess: (data: T) => void,
329+
onFinally?: (data: T) => void) => void
320330

321331
/**
322332
* Attempts to delete a comment
@@ -337,7 +347,14 @@ interface QPixel {
337347
* @param id id of the comment thread to lock
338348
* @returns result of the operation
339349
*/
340-
lockThread?: (id: string) => Promise<QPixelResponseJSON>;
350+
lockThread?: (id: string) => Promise<QPixelResponseJSON>
351+
352+
/**
353+
* Attempts to raise a flag
354+
* @param flag new flag data
355+
* @returns result of the operation
356+
*/
357+
flag?: (flag: QPixelFlagData) => Promise<QPixelResponseJSON>
341358

342359
// qpixel_dom
343360
DOM?: QPixelDOM;

test/controllers/flags_controller_test.rb

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,56 @@ class FlagsControllerTest < ActionController::TestCase
55

66
test 'should create new post flag' do
77
sign_in users(:standard_user)
8-
post :new, params: { reason: 'ABCDEF GHIJKL MNOPQR STUVWX YZ', post_id: posts(:answer_two).id, post_type: 'Post' }
98

10-
assert_not_nil assigns(:flag)
11-
assert_not_nil assigns(:flag).post
12-
assert_equal 'success', JSON.parse(response.body)['status']
9+
try_create_flag(posts(:answer_two), reason: 'testing post flags')
10+
1311
assert_response(:created)
12+
13+
@flag = assigns(:flag)
14+
assert_not_nil @flag
15+
assert_not_nil @flag.post
16+
assert_equal 'success', JSON.parse(response.body)['status']
17+
assert_equal I18n.t('flags.success.create_generic'), JSON.parse(response.body)['message']
1418
end
1519

1620
test 'should create new comment flag' do
1721
sign_in users(:standard_user)
18-
post :new, params: { reason: 'ABCDEF GHIJKL MNOPQR STUVWX YZ', post_id: comments(:one).id, post_type: 'Comment' }
1922

20-
assert_not_nil assigns(:flag)
21-
assert_not_nil assigns(:flag).post
22-
assert_equal 'success', JSON.parse(response.body)['status']
23+
try_create_flag(comments(:one), reason: 'testing comment flags')
24+
2325
assert_response(:created)
26+
27+
@flag = assigns(:flag)
28+
assert_not_nil @flag
29+
assert_not_nil @flag.post
30+
assert_equal 'success', JSON.parse(response.body)['status']
31+
end
32+
33+
test 'should fail to create invalid flags' do
34+
sign_in users(:standard_user)
35+
36+
try_create_flag(posts(:question_one), reason: 'a' * 1001)
37+
38+
assert_response(:bad_request)
39+
assert_equal 'failed', JSON.parse(response.body)['status']
40+
assert_equal I18n.t('flags.errors.create_generic'), JSON.parse(response.body)['message']
41+
end
42+
43+
test 'should fail to create flags for rate-limited users' do
44+
{
45+
RL_NewUserFlags: users(:basic_user),
46+
RL_Flags: users(:standard_user)
47+
}.each_pair do |setting, user|
48+
sign_in(user)
49+
50+
SiteSetting[setting] = 0
51+
52+
try_create_flag(posts(:question_one), reason: 'testing flag rate limits')
53+
54+
assert_response(:forbidden)
55+
assert_equal 'failed', JSON.parse(response.body)['status']
56+
assert_equal I18n.t('flags.errors.rate_limited', count: 0), JSON.parse(response.body)['message']
57+
end
2458
end
2559

2660
test 'should retrieve flag queue' do
@@ -120,6 +154,11 @@ class FlagsControllerTest < ActionController::TestCase
120154

121155
private
122156

157+
def try_create_flag(target, **opts)
158+
post :new, params: { post_id: target.id,
159+
post_type: target.is_a?(Post) ? 'Post' : 'Comment' }.merge(opts)
160+
end
161+
123162
def try_resolve_flag(flag, result: nil, message: nil)
124163
post :resolve, params: { id: flag.id, result: result, message: message }
125164
end

test/models/flag_test.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,20 @@ class FlagTest < ActiveSupport::TestCase
1414
assert_equal normal.confidential?, false
1515
assert_equal secret.confidential?, true
1616
end
17+
18+
test 'flags should be correctly validated' do
19+
SiteSetting['MaxFlagReasonLength'] = 500
20+
21+
common_attributes = {
22+
post: posts(:question_one),
23+
user: users(:standard_user)
24+
}
25+
26+
too_long = Flag.new(reason: 'a' * 1000, **common_attributes)
27+
assert_not too_long.valid?
28+
assert too_long.errors[:reason].any?
29+
30+
valid = Flag.new(reason: 'my reasons are my own', **common_attributes)
31+
assert valid.valid?
32+
end
1733
end

0 commit comments

Comments
 (0)