FEATURE: Warn a user when they have few likes remaining

This commit is contained in:
Robin Ward 2016-03-18 11:17:51 -04:00
parent 1fba835d4f
commit 5d4ee2ca1d
9 changed files with 75 additions and 17 deletions

View file

@ -66,6 +66,10 @@ Discourse.Ajax = Em.Mixin.create({
});
}
if (args.returnXHR) {
data = { result: data, xhr };
}
Ember.run(null, resolve, data);
};

View file

@ -18,10 +18,7 @@ export default RestModel.extend({
},
togglePromise(post) {
if (!this.get('acted')) {
return this.act(post).then(() => true);
}
return this.undo(post).then(() => false);
return this.get('acted') ? this.undo(post) : this.act(post);
},
toggle(post) {
@ -64,11 +61,15 @@ export default RestModel.extend({
message: opts.message,
take_action: opts.takeAction,
flag_topic: this.get('flagTopic') ? true : false
}
}).then(function(result) {
},
returnXHR: true,
}).then(function(data) {
if (!self.get('flagTopic')) {
return post.updateActionsSummary(result);
post.updateActionsSummary(data.result);
}
const remaining = parseInt(data.xhr.getResponseHeader('Discourse-Actions-Remaining') || 0);
const max = parseInt(data.xhr.getResponseHeader('Discourse-Actions-Max') || 0);
return { acted: true, remaining, max };
}).catch(function(error) {
popupAjaxError(error);
self.removeAction(post);
@ -83,7 +84,10 @@ export default RestModel.extend({
return Discourse.ajax("/post_actions/" + post.get('id'), {
type: 'DELETE',
data: { post_action_type_id: this.get('id') }
}).then(result => post.updateActionsSummary(result));
}).then(result => {
post.updateActionsSummary(result);
return { acted: false };
});
},
deferFlags(post) {

View file

@ -423,7 +423,27 @@ export default createWidget('post', {
const likeAction = post.get('likeAction');
if (likeAction && likeAction.get('canToggle')) {
return likeAction.togglePromise(post);
return likeAction.togglePromise(post).then(result => this._warnIfClose(result));
}
},
_warnIfClose(result) {
if (!result || !result.acted) { return; }
const kvs = this.keyValueStore;
const lastWarnedLikes = kvs.get('lastWarnedLikes');
// only warn once per day
const yesterday = new Date().getTime() - 1000 * 60 * 60 * 24;
if (lastWarnedLikes && parseInt(lastWarnedLikes) > yesterday) {
return;
}
const { remaining, max } = result;
const threshold = Math.ceil(max * 0.1);
if (remaining === threshold) {
bootbox.alert(I18n.t('post.few_likes_left'));
kvs.set({ key: 'lastWarnedLikes', value: new Date().getTime() });
}
},

View file

@ -119,6 +119,7 @@ export default class Widget {
this.currentUser = container.lookup('current-user:main');
this.store = container.lookup('store:main');
this.appEvents = container.lookup('app-events:main');
this.keyValueStore = container.lookup('key-value-store:main');
if (this.name) {
const custom = _customSettings[this.name];

View file

@ -21,6 +21,12 @@ class PostActionsController < ApplicationController
else
# We need to reload or otherwise we are showing the old values on the front end
@post.reload
if @post_action_type_id == PostActionType.types[:like]
limiter = post_action.post_action_rate_limiter
response.headers['Discourse-Actions-Remaining'] = limiter.remaining.to_s
response.headers['Discourse-Actions-Max'] = limiter.max.to_s
end
render_post_json(@post, _add_raw = false)
end
end

View file

@ -1511,6 +1511,8 @@ en:
archetypes:
save: 'Save Options'
few_likes_left: "Warning: You have few likes left to give today. Use them wisely!"
controls:
reply: "begin composing a reply to this post"
like: "like this post"

View file

@ -67,6 +67,15 @@ class RateLimiter
$redis.lpop(@key)
end
def remaining
return @max if @user && @user.staff?
arr = $redis.lrange(@key, 0, @max) || []
t0 = Time.now.to_i
arr.reject! {|a| (t0 - a.to_i) > @secs}
@max - arr.size
end
private
def seconds_to_wait

View file

@ -39,6 +39,16 @@ describe RateLimiter do
end
end
context "remaining" do
it "updates correctly" do
expect(rate_limiter.remaining).to eq(2)
rate_limiter.performed!
expect(rate_limiter.remaining).to eq(1)
rate_limiter.performed!
expect(rate_limiter.remaining).to eq(0)
end
end
context "multiple calls" do
before do
rate_limiter.performed!
@ -47,6 +57,7 @@ describe RateLimiter do
it "returns false for can_perform when the limit has been hit" do
expect(rate_limiter.can_perform?).to eq(false)
expect(rate_limiter.remaining).to eq(0)
end
it "raises an error the third time called" do
@ -54,10 +65,10 @@ describe RateLimiter do
end
context "as an admin/moderator" do
it "returns true for can_perform if the user is an admin" do
user.admin = true
expect(rate_limiter.can_perform?).to eq(true)
expect(rate_limiter.remaining).to eq(2)
end
it "doesn't raise an error when an admin performs the task" do
@ -74,8 +85,6 @@ describe RateLimiter do
user.moderator = true
expect { rate_limiter.performed! }.not_to raise_error
end
end
context "rollback!" do
@ -90,7 +99,6 @@ describe RateLimiter do
it "raises no error now that there is room" do
expect { rate_limiter.performed! }.not_to raise_error
end
end
end

View file

@ -12,6 +12,10 @@ describe PostAction do
let(:second_post) { Fabricate(:post, topic_id: post.topic_id) }
let(:bookmark) { PostAction.new(user_id: post.user_id, post_action_type_id: PostActionType.types[:bookmark] , post_id: post.id) }
def value_for(user_id, dt)
GivenDailyLike.find_for(user_id, dt).pluck(:likes_given)[0] || 0
end
describe "rate limits" do
it "limits redo/undo" do
@ -172,7 +176,7 @@ describe PostAction do
# we need this to test it
TopicUser.change(codinghorror, post.topic, posted: true)
expect(GivenDailyLike.value_for(moderator.id, Date.today)).to eq(0)
expect(value_for(moderator.id, Date.today)).to eq(0)
PostAction.act(moderator, post, PostActionType.types[:like])
PostAction.act(codinghorror, second_post, PostActionType.types[:like])
@ -180,7 +184,7 @@ describe PostAction do
post.topic.reload
expect(post.topic.like_count).to eq(2)
expect(GivenDailyLike.value_for(moderator.id, Date.today)).to eq(1)
expect(value_for(moderator.id, Date.today)).to eq(1)
tu = TopicUser.get(post.topic, codinghorror)
expect(tu.liked).to be true
@ -251,7 +255,7 @@ describe PostAction do
expect(post.like_score).to eq(1)
post.topic.reload
expect(post.topic.like_count).to eq(1)
expect(GivenDailyLike.value_for(codinghorror.id, Date.today)).to eq(1)
expect(value_for(codinghorror.id, Date.today)).to eq(1)
# When a staff member likes it
PostAction.act(moderator, post, PostActionType.types[:like])
@ -264,7 +268,7 @@ describe PostAction do
post.reload
expect(post.like_count).to eq(1)
expect(post.like_score).to eq(3)
expect(GivenDailyLike.value_for(codinghorror.id, Date.today)).to eq(0)
expect(value_for(codinghorror.id, Date.today)).to eq(0)
PostAction.remove_act(moderator, post, PostActionType.types[:like])
post.reload