Split BadgeRewarder into multiple services (#11902)

This commit is contained in:
Michael Kohl 2020-12-23 21:55:06 +07:00 committed by GitHub
parent 01f7dbd24f
commit 63cf95d811
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 616 additions and 421 deletions

View file

@ -1,151 +0,0 @@
module BadgeRewarder
YEARS = { 1 => "one", 2 => "two", 3 => "three", 4 => "four", 5 => "five", 6 => "six", 7 => "seven" }.freeze
REPOSITORIES = [
"forem/forem",
"forem/forem-browser-extension",
"thepracticaldev/DEV-Android",
"thepracticaldev/DEV-ios",
].freeze
LONGEST_STREAK_WEEKS = 16
LONGEST_STREAK_MESSAGE = "16 weeks! You've achieved the longest writing " \
"streak possible. This makes you eligible for special quests in the future. Keep up the amazing contributions to" \
" our community!".freeze
MINIMUM_QUALITY = -25
def self.award_yearly_club_badges
total_years = Time.current.year - SiteConfig.community_copyright_start_year.to_i
(1..total_years).each do |i|
message = "Happy #{SiteConfig.community_name} birthday! " \
"Can you believe it's been #{i} #{'year'.pluralize(i)} already?!"
badge = Badge.find_by(slug: "#{YEARS[i]}-year-club")
next unless badge
User.registered.where("created_at < ? AND created_at > ?", i.year.ago, i.year.ago - 2.days).find_each do |user|
achievement = BadgeAchievement.create(
user_id: user.id,
badge_id: badge.id,
rewarding_context_message_markdown: message,
)
user.save if achievement.valid?
end
end
end
def self.award_beloved_comment_badges(comment_count = 24)
badge_id = Badge.find_by(slug: "beloved-comment")&.id
return unless badge_id
Comment.includes(:user).where("public_reactions_count > ?", comment_count).find_each do |comment|
message = "You're famous! [This is the comment](#{URL.comment(comment)}) for which you are being recognized. 😄"
achievement = BadgeAchievement.create(
user_id: comment.user_id,
badge_id: badge_id,
rewarding_context_message_markdown: message,
)
comment.user.save if achievement.valid?
end
end
def self.award_top_seven_badges(usernames, message_markdown = "Congrats!!!")
award_badges(usernames, "top-7", message_markdown)
end
def self.award_fab_five_badges(usernames, message_markdown = "Congrats!!!")
award_badges(usernames, "fab-5", message_markdown)
end
def self.award_contributor_badges(usernames, message_markdown = "Thank you so much for your contributions!")
award_badges(usernames, "dev-contributor", message_markdown)
end
def self.award_tag_badges
Tag.where.not(badge_id: nil).find_each do |tag|
past_winner_user_ids = BadgeAchievement.where(badge_id: tag.badge_id).pluck(:user_id)
winning_article = Article.where("score > 100")
.published
.where.not(user_id: past_winner_user_ids)
.order(score: :desc)
.where("published_at > ?", 7.5.days.ago) # More than seven days, to have some wiggle room.
.cached_tagged_with(tag).first
if winning_article
award_badges(
[winning_article.user.username],
tag.badge.slug,
"Congratulations on posting the most beloved [##{tag.name}](#{URL.tag(tag)}) post " \
"from the past seven days! " \
"Your winning post was [#{winning_article.title}](#{URL.article(winning_article)}). " \
"(You can only win once per badge-eligible tag)",
)
end
end
end
def self.award_contributor_badges_from_github(since = 1.day.ago, msg = "Thank you so much for your contributions!")
badge = Badge.find_by(slug: "dev-contributor")
return unless badge
REPOSITORIES.each do |repo|
commits = Github::OauthClient.new.commits(repo, since: since.utc.iso8601)
authors_uids = commits.map { |commit| commit.author.id }
Identity.github.where(uid: authors_uids).find_each do |i|
BadgeAchievement.where(user_id: i.user_id, badge_id: badge.id).first_or_create(
rewarding_context_message_markdown: msg,
)
end
end
end
def self.award_four_week_streak_badge
award_streak_badge(4)
end
def self.award_eight_week_streak_badge
award_streak_badge(8)
end
def self.award_sixteen_week_streak_badge
award_streak_badge(16)
end
def self.award_streak_badge(num_weeks)
# No credit for super low quality
article_user_ids = Article.published
.where("published_at > ? AND score > ?", 1.week.ago, MINIMUM_QUALITY)
.pluck(:user_id)
message = if num_weeks == LONGEST_STREAK_WEEKS
LONGEST_STREAK_MESSAGE
else
"Congrats on achieving this streak! Consistent writing is hard. " \
"The next streak badge you can get is the #{num_weeks * 2} Week Badge. 😉"
end
users = User.where(id: article_user_ids).where("articles_count >= ?", num_weeks)
usernames = []
users.find_each do |user|
count = 0
num_weeks.times do |i|
num = i + 1
count += 1 if user.articles.published.where("published_at > ? AND published_at < ?", num.weeks.ago,
(num - 1).weeks.ago).any?
end
usernames << user.username if count >= num_weeks
end
award_badges(usernames, "#{num_weeks}-week-streak", message)
end
def self.award_badges(usernames, slug, message_markdown)
badge_id = Badge.find_by(slug: slug)&.id
return unless badge_id
User.registered.where(username: usernames).find_each do |user|
BadgeAchievement.create(
user_id: user.id,
badge_id: badge_id,
rewarding_context_message_markdown: message_markdown,
)
user.save
end
end
end

View file

@ -18,6 +18,10 @@ class Badge < ApplicationRecord
"/badge/#{slug}"
end
def self.id_for_slug(slug)
select(:id).find_by(slug: slug)&.id
end
private
def generate_slug

View file

@ -0,0 +1,15 @@
module Badges
class Award
def self.call(user_relation, slug, message_markdown)
return unless (badge_id = Badge.id_for_slug(slug))
user_relation.find_each do |user|
achievement = user.badge_achievements.create(
badge_id: badge_id,
rewarding_context_message_markdown: message_markdown,
)
user.touch if achievement.persisted?
end
end
end
end

View file

@ -0,0 +1,38 @@
module Badges
class AwardBelovedComment
BADGE_SLUG = "beloved-comment".freeze
MESSAGE_TEMPLATE =
"You're famous! " \
"[This is the comment](%<comment>s) for which you're being recognized. 😄".freeze
def self.call(comment_count = 25)
new(comment_count).call
end
def initialize(comment_count)
@comment_count = comment_count
end
def call
return unless (badge_id = Badge.id_for_slug(BADGE_SLUG))
Comment.includes(:user).where(public_reactions_count: comment_count..).find_each do |comment|
achievement = BadgeAchievement.create(
user_id: comment.user_id,
badge_id: badge_id,
rewarding_context_message_markdown: generate_message(comment),
)
comment.user.touch if achievement.valid?
end
end
private
attr_reader :comment_count
def generate_message(comment)
format(MESSAGE_TEMPLATE, comment: URL.comment(comment))
end
end
end

View file

@ -0,0 +1,13 @@
module Badges
class AwardContributor
BADGE_SLUG = "dev-contributor".freeze
def self.call(usernames, message_markdown = "Thank you so much for your contributions!")
::Badges::Award.call(
User.where(username: usernames),
BADGE_SLUG,
message_markdown,
)
end
end
end

View file

@ -0,0 +1,27 @@
module Badges
class AwardContributorFromGithub
BADGE_SLUG = "dev-contributor".freeze
REPOSITORIES = [
"forem/forem",
"forem/forem-browser-extension",
"forem/DEV-Android",
"forem/DEV-ios",
].freeze
def self.call(since = 1.day.ago, msg = "Thank you so much for your contributions!")
return unless (badge_id = Badge.id_for_slug(BADGE_SLUG))
REPOSITORIES.each do |repo|
commits = Github::OauthClient.new.commits(repo, since: since.utc.iso8601)
authors_uids = commits.map { |commit| commit.author.id }
Identity.github.where(uid: authors_uids).find_each do |i|
BadgeAchievement
.where(user_id: i.user_id, badge_id: badge_id)
.first_or_create(rewarding_context_message_markdown: msg)
end
end
end
end
end

View file

@ -0,0 +1,7 @@
module Badges
class AwardEightWeekStreak
def self.call
::Badges::AwardStreak.call(weeks: 8)
end
end
end

View file

@ -0,0 +1,14 @@
module Badges
class AwardFabFive
BADGE_SLUG = "fab-5".freeze
DEFAULT_MESSAGE = "Congrats!!!".freeze
def self.call(usernames, message_markdown = DEFAULT_MESSAGE)
::Badges::Award.call(
User.where(username: usernames),
BADGE_SLUG,
message_markdown,
)
end
end
end

View file

@ -0,0 +1,7 @@
module Badges
class AwardFourWeekStreak
def self.call
::Badges::AwardStreak.call(weeks: 4)
end
end
end

View file

@ -0,0 +1,7 @@
module Badges
class AwardSixteenWeekStreak
def self.call
::Badges::AwardStreak.call(weeks: 16)
end
end
end

View file

@ -0,0 +1,61 @@
module Badges
class AwardStreak
LONGEST_STREAK_WEEKS = 16
LONGEST_STREAK_MESSAGE = "16 weeks! You've achieved the longest writing " \
"streak possible. This makes you eligible for special quests in the future. " \
"Keep up the amazing contributions to our community!".freeze
MINIMUM_QUALITY = -25
MESSAGE_TEMPLATE =
"Congrats on achieving this streak! Consistent writing is hard. " \
"The next streak badge you can get is the %<weeks>d Week Badge. 😉".freeze
def self.call(weeks:)
new(weeks: weeks).call
end
def initialize(weeks:)
@weeks = weeks
end
def call
badge_slug = "#{weeks}-week-streak"
return unless (badge_id = Badge.id_for_slug(badge_slug))
users = User.where(id: article_user_ids).where("articles_count >= ?", weeks)
users.find_each do |user|
count = weeks.times.count { |i| published_x_weeks_ago?(user, i + 1) }
next unless count >= weeks
user.badge_achievements.create(
badge_id: badge_id,
rewarding_context_message_markdown: generate_message,
)
end
end
private
attr_reader :weeks
# No credit for super low quality
def article_user_ids
Article.published
.where("published_at > ? AND score > ?", 1.week.ago, MINIMUM_QUALITY)
.pluck(:user_id)
end
def published_x_weeks_ago?(user, num)
user.articles.published
.where("published_at > ? AND published_at < ?", num.weeks.ago, (num - 1).weeks.ago).any?
end
def generate_message
return LONGEST_STREAK_MESSAGE if weeks == LONGEST_STREAK_WEEKS
format(MESSAGE_TEMPLATE, weeks: weeks * 2)
end
end
end

View file

@ -0,0 +1,45 @@
module Badges
class AwardTag
MESSAGE_TEMPLATE =
"Congratulations on posting the most beloved [#%<tag_name>s](%<tag_url>s) post " \
"from the past seven days! " \
"Your winning post was [%<article_title>s](%<article_url>s). " \
"(You can only win once per badge-eligible tag)".freeze
def self.call
new.call
end
def call
Tag.where.not(badge_id: nil).find_each do |tag|
past_winner_user_ids = BadgeAchievement.where(badge_id: tag.badge_id).pluck(:user_id)
winning_article = Article.where("score > 100")
.published
.where.not(user_id: past_winner_user_ids)
.order(score: :desc)
.where("published_at > ?", 7.5.days.ago) # More than seven days, to have some wiggle room.
.cached_tagged_with(tag).first
next unless winning_article
user = winning_article.user
achievement = user.badge_achievements.create(
badge_id: tag.badge_id,
rewarding_context_message_markdown: generate_message(tag, winning_article),
)
user.touch if achievement.persisted?
end
end
private
def generate_message(tag, winning_article)
format(
MESSAGE_TEMPLATE,
tag_name: tag.name,
tag_url: URL.tag(tag),
article_title: winning_article.title,
article_url: URL.article(winning_article),
)
end
end
end

View file

@ -0,0 +1,14 @@
module Badges
class AwardTopSeven
BADGE_SLUG = "top-7".freeze
DEFAULT_MESSAGE = "Congrats!!!".freeze
def self.call(usernames, message_markdown = DEFAULT_MESSAGE)
::Badges::Award.call(
User.where(username: usernames),
BADGE_SLUG,
message_markdown,
)
end
end
end

View file

@ -0,0 +1,38 @@
module Badges
class AwardYearlyClub
YEARS = {
1 => "one",
2 => "two",
3 => "three",
4 => "four",
5 => "five",
6 => "six",
7 => "seven"
}.freeze
MESSAGE_TEMPLATE =
"Happy #{SiteConfig.community_name} birthday! " \
"Can you believe it's been %<years>d %<noun>s already?!".freeze
def self.call
new.call
end
def call
total_years = Time.current.year - SiteConfig.community_copyright_start_year.to_i
(1..total_years).each do |i|
::Badges::Award.call(
User.registered.where(created_at: i.year.ago - 2.days...i.year.ago),
"#{YEARS[i]}-year-club",
generate_message(i),
)
end
end
private
def generate_message(years)
format(MESSAGE_TEMPLATE, years: years, noun: "year".pluralize(years))
end
end
end

View file

@ -5,10 +5,14 @@ module BadgeAchievements
sidekiq_options queue: :high_priority, retry: 10
def perform(usernames, badge_slug, message)
if BadgeRewarder.respond_to?(badge_slug)
BadgeRewarder.public_send(badge_slug)
if (award_class = "Badges::#{badge_slug.classify}".safe_constantize)
award_class.call
else
BadgeRewarder.award_badges(usernames, badge_slug, message)
Badges::Award.call(
User.where(username: usernames),
badge_slug,
message,
)
end
end
end

View file

@ -21,49 +21,49 @@ award_yearly_club_badges:
class: "BadgeAchievements::BadgeAwardWorker"
args:
- ""
- award_yearly_club_badges
- award_yearly_club
- ""
award_beloved_comment_badges:
cron: "5 */12 * * *" # 5 min past every 12th hour UTC
class: "BadgeAchievements::BadgeAwardWorker"
args:
- ""
- award_beloved_comment_badges
- award_beloved_comment
- ""
award_four_week_streak_badge:
cron: "10 */12 * * *" # 10 min past every 12th hour UTC
class: "BadgeAchievements::BadgeAwardWorker"
args:
- ""
- award_four_week_streak_badge
- award_four_week_streak
- ""
award_eight_week_streak_badge:
cron: "15 */12 * * *" # 15 min past every 12th hour UTC
class: "BadgeAchievements::BadgeAwardWorker"
args:
- ""
- award_eight_week_streak_badge
- award_eight_week_streak
- ""
award_sixteen_week_streak_badge:
cron: "20 */12 * * *" # 20 min past every 12th hour UTC
class: "BadgeAchievements::BadgeAwardWorker"
args:
- ""
- award_sixteen_week_streak_badge
- award_sixteen_week_streak
- ""
award_weekly_tag_badges:
cron: "0 11 * * 4" # 11 am UTC every Thursday
class: "BadgeAchievements::BadgeAwardWorker"
args:
- ""
- award_tag_badges
- award_tag
- ""
award_contributor_badges_from_github:
cron: "20 * * * *" # every hour, 20 min after the hour
class: "BadgeAchievements::BadgeAwardWorker"
args:
- ""
- award_contributor_badges_from_github
- award_contributor_from_github
- ""
remove_old_html_variant_data:
cron: "10 * * * *" # every hour, 10 min after the hour

View file

@ -2,7 +2,7 @@
task :award_top_seven_badges, [:arg1] => :environment do |_t, args|
usernames = args[:arg1].split
puts "Awarding top-7 badges to #{usernames}"
BadgeRewarder.award_top_seven_badges(usernames)
Badges::AwardTopSeven.call(usernames)
puts "Done!"
end
@ -10,7 +10,7 @@ end
task :award_contributor_badges, [:arg1] => :environment do |_t, args|
usernames = args[:arg1].split
puts "Awarding dev-contributor badges to #{usernames}"
BadgeRewarder.award_contributor_badges(usernames)
Badges::AwardContributor.call(usernames)
puts "Done!"
end
@ -18,6 +18,6 @@ end
task :award_fab_five_badges, [:arg1] => :environment do |_t, args|
usernames = args[:arg1].split
puts "Awarding fab 5 badges to #{usernames}"
BadgeRewarder.award_fab_five_badges(usernames)
Badges::AwardFabFive.call(usernames)
puts "Done!"
end

View file

@ -1,249 +0,0 @@
require "rails_helper"
RSpec.describe BadgeRewarder, type: :labor do
describe "::award_yearly_club_badges" do
before do
stub_const("#{described_class}::YEARS", BadgeRewarder::YEARS.slice(1, 2, 3))
allow(ApplicationConfig).to receive(:[])
allow(SiteConfig).to receive(:community_copyright_start_year).and_return(3.years.ago.year)
create(:badge, title: "one-year-club")
create(:badge, title: "two-year-club")
create(:badge, title: "three-year-club")
create(:badge, title: "heysddssdhey")
end
it "rewards birthday badge to birthday folks who registered a year ago" do
user = create(:user, created_at: 366.days.ago)
newer_user = create(:user, created_at: 6.days.ago)
older_user = create(:user, created_at: 390.days.ago)
described_class.award_yearly_club_badges
expect(user.badge_achievements.size).to eq(1)
expect(newer_user.badge_achievements.size).to eq(0)
expect(older_user.badge_achievements.size).to eq(0)
end
it "rewards 2-year birthday badge to birthday folks who registered 2 years ago" do
user = create(:user, created_at: 731.days.ago)
newer_user = create(:user, created_at: 6.days.ago)
older_user = create(:user, created_at: 800.days.ago)
described_class.award_yearly_club_badges
expect(user.badge_achievements.size).to eq(1)
expect(newer_user.badge_achievements.size).to eq(0)
expect(older_user.badge_achievements.size).to eq(0)
end
it "rewards 3-year birthday badge to birthday folks who registered 3 years ago" do
user = create(:user, created_at: 1096.days.ago)
newer_user = create(:user, created_at: 6.days.ago)
older_user = create(:user, created_at: 1200.days.ago)
described_class.award_yearly_club_badges
expect(user.badge_achievements.size).to eq(1)
expect(newer_user.badge_achievements.size).to eq(0)
expect(older_user.badge_achievements.size).to eq(0)
end
end
describe "::award_beloved_comments" do
it "rewards beloved comment to folks who have a qualifying comment" do
create(:badge, title: "Beloved comment", slug: "beloved-comment")
comment = create(:comment, commentable: create(:article))
comment.update(public_reactions_count: 30)
described_class.award_beloved_comment_badges
expect(BadgeAchievement.count).to eq(1)
end
it "does not reward beloved comment to non-qualifying comment" do
create(:badge, title: "Beloved comment", slug: "beloved-comment")
create(:comment, commentable: create(:article))
described_class.award_beloved_comment_badges
expect(BadgeAchievement.count).to eq(0)
end
end
it "rewards top seven badge to users" do
badge = create(:badge, title: "Top 7")
user = create(:user)
user_other = create(:user)
described_class.award_top_seven_badges([user.username, user_other.username])
expect(BadgeAchievement.where(badge_id: badge.id).size).to eq(2)
end
it "rewards fab five badge to users" do
badge = create(:badge, title: "Fab 5")
user = create(:user)
user_other = create(:user)
described_class.award_fab_five_badges([user.username, user_other.username])
expect(BadgeAchievement.where(badge_id: badge.id).size).to eq(2)
end
it "rewards contributor badges" do
badge = create(:badge, title: "Dev Contributor")
user = create(:user)
user_other = create(:user)
described_class.award_contributor_badges([user.username, user_other.username])
expect(BadgeAchievement.where(badge_id: badge.id).size).to eq(2)
end
describe "::award_four_week_streak_badge" do
it "calls award_streak_badge with argument 4" do
allow(described_class).to receive(:award_streak_badge)
described_class.award_four_week_streak_badge
expect(described_class).to have_received(:award_streak_badge).with(4)
end
end
describe "::award_eight_week_streak_badge" do
it "calls award_streak_badge with argument 8" do
allow(described_class).to receive(:award_streak_badge)
described_class.award_eight_week_streak_badge
expect(described_class).to have_received(:award_streak_badge).with(8)
end
end
describe "::award_sixteen_week_streak_badge" do
it "calls award_streak_badge with argument 16" do
allow(described_class).to receive(:award_streak_badge)
described_class.award_sixteen_week_streak_badge
expect(described_class).to have_received(:award_streak_badge).with(16)
end
end
describe "::award_streak_badge" do
it "rewards badge to users with four straight weeks of articles" do
create(:badge, title: "4 Week Streak", slug: "4-week-streak")
user = create(:user)
create(:article, user: user, published: true, published_at: 26.days.ago)
create(:article, user: user, published: true, published_at: 19.days.ago)
create(:article, user: user, published: true, published_at: 12.days.ago)
create(:article, user: user, published: true, published_at: 5.days.ago)
described_class.award_streak_badge(4)
expect(user.badges.size).to eq(1)
end
it "does not reward the badge to not qualified users" do
create(:badge, title: "4 Week Streak", slug: "4-week-streak")
user = create(:user)
create(:article, user: user, published: true, published_at: 26.days.ago)
create(:article, user: user, published: true, published_at: 19.days.ago)
create(:article, user: user, published: true, published_at: 5.days.ago)
described_class.award_streak_badge(4)
expect(user.badges.size).to eq(0)
end
end
describe "::award_contributor_badges_from_github", vcr: true do
let(:user) { create(:user, :with_identity, identities: ["github"]) }
let(:badge) { create(:badge, title: "DEV Contributor") }
before do
badge
omniauth_mock_github_payload
stub_const("BadgeRewarder::REPOSITORIES", ["rust-lang/rust"])
user.identities.github.update_all(uid: "3372342")
end
it "awards contributor badge" do
expect do
Timecop.freeze("2020-05-15T13:49:20Z") do
VCR.use_cassette("github_client_commits_contributor_badge") do
described_class.award_contributor_badges_from_github
end
end
end.to change(user.badge_achievements, :count).by(1)
end
it "awards contributor badge once" do
expect do
Timecop.freeze("2020-05-15T13:49:20Z") do
VCR.use_cassette("github_client_commits_contributor_badge_twice") do
described_class.award_contributor_badges_from_github
described_class.award_contributor_badges_from_github
end
end
end.to change(user.badge_achievements, :count).by(1)
end
end
describe "::award_tag_badges" do
let(:user) { create(:user) }
let(:second_user) { create(:user) }
let(:third_user) { create(:user) }
let(:article) { create(:article, user_id: user.id) }
let(:second_article) { create(:article, user_id: second_user.id) }
let(:third_article) { create(:article, user_id: third_user.id) }
let(:badge) { create(:badge) }
let(:tag) { create(:tag, badge_id: badge.id) }
it "awards badge if qualifying article by score and tagged appropriately" do
article.update_columns(cached_tag_list: tag.name, score: 101)
described_class.award_tag_badges
expect(user.badge_achievements.size).to eq(1)
expect(user.badge_achievements.last.badge_id).to eq(badge.id)
end
it "renders html for message" do
article.update_columns(cached_tag_list: tag.name, score: 101)
described_class.award_tag_badges
expect(user.badge_achievements.size).to eq(1)
expect(user.badge_achievements.last.rewarding_context_message).to include("<a ")
expect(user.badge_achievements.last.rewarding_context_message).to include(ApplicationConfig["APP_DOMAIN"])
expect(user.badge_achievements.last.rewarding_context_message).to include(article.title)
expect(user.badge_achievements.last.rewarding_context_message).to include(article.path)
end
it "does not award badge if qualifying article by score but not tagged appropriately" do
article.update_columns(cached_tag_list: "differenttag", score: 101)
described_class.award_tag_badges
expect(user.badge_achievements.size).to eq(0)
end
it "does not award badge if qualifying article by score but not from past week" do
article.update_columns(published_at: 8.days.ago, cached_tag_list: tag.name, score: 333)
described_class.award_tag_badges
expect(user.badge_achievements.size).to eq(0)
end
it "does not award badge if tagged appropriately but not published" do
article.update_columns(cached_tag_list: tag.name, score: 101, published: false)
described_class.award_tag_badges
expect(user.badge_achievements.size).to eq(0)
end
it "does not award badge to user who has previously won" do
article.update_columns(cached_tag_list: tag.name, score: 201)
second_article.update_columns(cached_tag_list: tag.name, score: 150)
third_article.update_columns(cached_tag_list: tag.name, score: 120)
described_class.award_tag_badges
expect(user.reload.badge_achievements.size).to eq(1)
expect(second_user.reload.badge_achievements.size).to eq(0)
described_class.award_tag_badges
expect(user.reload.badge_achievements.size).to eq(1)
expect(second_user.reload.badge_achievements.size).to eq(1)
expect(third_user.reload.badge_achievements.size).to eq(0)
described_class.award_tag_badges
expect(user.reload.badge_achievements.size).to eq(1)
expect(second_user.reload.badge_achievements.size).to eq(1)
expect(third_user.reload.badge_achievements.size).to eq(1)
end
end
describe "::award_badges" do
let!(:badge) { create(:badge, title: "one-year-club") }
let(:user) { create(:user) }
let(:user2) { create(:user) }
it "awards badges" do
expect do
described_class.award_badges([user.username, user2.username], "one-year-club", "Congrats on a badge!")
end.to change(BadgeAchievement, :count).by(2)
end
it "creates correct badge acheivements" do
described_class.award_badges([user.username, user2.username], "one-year-club", "Congrats on a badge!")
expect(user.badge_achievements.pluck(:badge_id)).to eq([badge.id])
expect(user2.badge_achievements.pluck(:rewarding_context_message_markdown)).to eq(["Congrats on a badge!"])
end
end
end

View file

@ -18,6 +18,18 @@ RSpec.describe Badge, type: :model do
end
end
describe "class methods" do
describe ".id_for_slug" do
it "returns the id of an existing slug" do
expect(described_class.id_for_slug(badge.slug)).to eq badge.id
end
it "returns nil for non-existing slugs" do
expect(described_class.id_for_slug("ohnoes")).to be_nil
end
end
end
context "when callbacks are triggered after save" do
let!(:badge) { create(:badge) }

View file

@ -0,0 +1,24 @@
require "rails_helper"
RSpec.describe Badges::AwardBelovedComment, type: :service do
let!(:comment) { create(:comment, commentable: create(:article)) }
before do
create(:badge, title: "Beloved comment", slug: "beloved-comment")
end
describe ".call" do
it "awards beloved comment to folks who have a qualifying comment" do
comment.update(public_reactions_count: 25)
expect do
described_class.call
end.to change(BadgeAchievement, :count).by(1)
end
it "does not reward beloved comment to non-qualifying comment" do
expect do
described_class.call
end.not_to change(BadgeAchievement, :count)
end
end
end

View file

@ -0,0 +1,36 @@
require "rails_helper"
RSpec.describe Badges::AwardContributorFromGithub, type: :service, vcr: true do
let(:user) { create(:user, :with_identity, identities: ["github"]) }
let(:badge) { create(:badge, title: "DEV Contributor") }
before do
badge
omniauth_mock_github_payload
stub_const("#{described_class}::REPOSITORIES", ["rust-lang/rust"])
user.identities.github.update_all(uid: "3372342")
end
it "awards contributor badge" do
expect do
Timecop.freeze("2020-05-15T13:49:20Z") do
VCR.use_cassette("github_client_commits_contributor_badge") do
described_class.call
end
end
end.to change(user.badge_achievements, :count).by(1)
end
it "awards contributor badge once" do
expect do
Timecop.freeze("2020-05-15T13:49:20Z") do
VCR.use_cassette("github_client_commits_contributor_badge_twice") do
described_class.call
described_class.call
end
end
end.to change(user.badge_achievements, :count).by(1)
end
end

View file

@ -0,0 +1,13 @@
require "rails_helper"
RSpec.describe Badges::AwardContributor, type: :service do
it "awards contributor badge to users" do
create(:badge, title: "DEV Contributor")
user = create(:user)
other_user = create(:user)
expect do
described_class.call([user.username, other_user.username])
end.to change(BadgeAchievement, :count).by(2)
end
end

View file

@ -0,0 +1,9 @@
require "rails_helper"
RSpec.describe Badges::AwardEightWeekStreak, type: :service do
it "calls Badges::AwardStreak with argument 8" do
allow(Badges::AwardStreak).to receive(:call)
described_class.call
expect(Badges::AwardStreak).to have_received(:call).with(weeks: 8)
end
end

View file

@ -0,0 +1,13 @@
require "rails_helper"
RSpec.describe Badges::AwardFabFive, type: :service do
it "awards fab five badge to users" do
create(:badge, title: "Fab 5")
user = create(:user)
other_user = create(:user)
expect do
described_class.call([user.username, other_user.username])
end.to change(BadgeAchievement, :count).by(2)
end
end

View file

@ -0,0 +1,9 @@
require "rails_helper"
RSpec.describe Badges::AwardFourWeekStreak, type: :service do
it "calls Badges::AwardStreak with argument 4" do
allow(Badges::AwardStreak).to receive(:call)
described_class.call
expect(Badges::AwardStreak).to have_received(:call).with(weeks: 4)
end
end

View file

@ -0,0 +1,9 @@
require "rails_helper"
RSpec.describe Badges::AwardSixteenWeekStreak, type: :service do
it "calls Badges::AwardStreak with argument 16" do
allow(Badges::AwardStreak).to receive(:call)
described_class.call
expect(Badges::AwardStreak).to have_received(:call).with(weeks: 16)
end
end

View file

@ -0,0 +1,22 @@
require "rails_helper"
RSpec.describe Badges::Award, type: :service do
describe ".call" do
let!(:badge) { create(:badge, title: "one-year-club") }
let!(:user) { create(:user) }
let!(:user2) { create(:user) }
it "awards badges" do
expect do
described_class.call(User.all, "one-year-club", "Congrats on a badge!")
end.to change(BadgeAchievement, :count).by(2)
end
it "creates correct badge achievements" do
described_class.call(User.all, "one-year-club", "Congrats on a badge!")
expect(user.badge_achievements.pluck(:badge_id)).to eq([badge.id])
expect(user2.badge_achievements.pluck(:rewarding_context_message_markdown))
.to eq(["Congrats on a badge!"])
end
end
end

View file

@ -0,0 +1,25 @@
require "rails_helper"
RSpec.describe Badges::AwardStreak, type: :service do
let(:user) { create(:user) }
before do
create(:badge, title: "4 Week Streak", slug: "4-week-streak")
create(:article, user: user, published: true, published_at: 26.days.ago)
create(:article, user: user, published: true, published_at: 19.days.ago)
create(:article, user: user, published: true, published_at: 5.days.ago)
end
it "awards badge to users with four straight weeks of articles" do
create(:article, user: user, published: true, published_at: 12.days.ago)
expect do
described_class.call(weeks: 4)
end.to change { user.reload.badges.size }.by(1)
end
it "does not award the badge to not qualified users" do
expect do
described_class.call(weeks: 4)
end.not_to change { user.reload.badges.size }
end
end

View file

@ -0,0 +1,64 @@
require "rails_helper"
RSpec.describe Badges::AwardTag, type: :service do
let(:user) { create(:user) }
let(:second_user) { create(:user) }
let(:third_user) { create(:user) }
let(:article) { create(:article, user_id: user.id) }
let(:second_article) { create(:article, user_id: second_user.id) }
let(:third_article) { create(:article, user_id: third_user.id) }
let(:badge) { create(:badge) }
let(:tag) { create(:tag, badge_id: badge.id) }
it "awards badge if qualifying article by score and tagged appropriately" do
article.update_columns(cached_tag_list: tag.name, score: 101)
described_class.call
expect(user.badge_achievements.size).to eq(1)
expect(user.badge_achievements.last.badge_id).to eq(badge.id)
end
it "renders html for message" do
article.update_columns(cached_tag_list: tag.name, score: 101)
described_class.call
expect(user.badge_achievements.size).to eq(1)
expect(user.badge_achievements.last.rewarding_context_message).to include("<a ")
expect(user.badge_achievements.last.rewarding_context_message).to include(ApplicationConfig["APP_DOMAIN"])
expect(user.badge_achievements.last.rewarding_context_message).to include(article.title)
expect(user.badge_achievements.last.rewarding_context_message).to include(article.path)
end
it "does not award badge if qualifying article by score but not tagged appropriately" do
article.update_columns(cached_tag_list: "differenttag", score: 101)
described_class.call
expect(user.badge_achievements.size).to eq(0)
end
it "does not award badge if qualifying article by score but not from past week" do
article.update_columns(published_at: 8.days.ago, cached_tag_list: tag.name, score: 333)
described_class.call
expect(user.badge_achievements.size).to eq(0)
end
it "does not award badge if tagged appropriately but not published" do
article.update_columns(cached_tag_list: tag.name, score: 101, published: false)
described_class.call
expect(user.badge_achievements.size).to eq(0)
end
it "does not award badge to user who has previously won" do
article.update_columns(cached_tag_list: tag.name, score: 201)
second_article.update_columns(cached_tag_list: tag.name, score: 150)
third_article.update_columns(cached_tag_list: tag.name, score: 120)
described_class.call
expect(user.reload.badge_achievements.size).to eq(1)
expect(second_user.reload.badge_achievements.size).to eq(0)
described_class.call
expect(user.reload.badge_achievements.size).to eq(1)
expect(second_user.reload.badge_achievements.size).to eq(1)
expect(third_user.reload.badge_achievements.size).to eq(0)
described_class.call
expect(user.reload.badge_achievements.size).to eq(1)
expect(second_user.reload.badge_achievements.size).to eq(1)
expect(third_user.reload.badge_achievements.size).to eq(1)
end
end

View file

@ -0,0 +1,13 @@
require "rails_helper"
RSpec.describe Badges::AwardTopSeven, type: :service do
it "awards top seven badge to users" do
create(:badge, title: "Top 7")
user = create(:user)
other_user = create(:user)
expect do
described_class.call([user.username, other_user.username])
end.to change(BadgeAchievement, :count).by(2)
end
end

View file

@ -0,0 +1,51 @@
require "rails_helper"
RSpec.describe Badges::AwardYearlyClub, type: :service do
describe ".call" do
before do
stub_const("#{described_class}::YEARS", described_class::YEARS.slice(1, 2, 3))
allow(ApplicationConfig).to receive(:[])
allow(SiteConfig).to receive(:community_copyright_start_year).and_return(3.years.ago.year)
create(:badge, title: "one-year-club")
create(:badge, title: "two-year-club")
create(:badge, title: "three-year-club")
create(:badge, title: "heysddssdhey")
end
it "awards birthday badge to birthday folks who registered a year ago" do
user = create(:user, created_at: 366.days.ago)
newer_user = create(:user, created_at: 6.days.ago)
older_user = create(:user, created_at: 390.days.ago)
described_class.call
expect(user.badge_achievements.size).to eq(1)
expect(newer_user.badge_achievements.size).to eq(0)
expect(older_user.badge_achievements.size).to eq(0)
end
it "rewards 2-year birthday badge to birthday folks who registered 2 years ago" do
user = create(:user, created_at: 731.days.ago)
newer_user = create(:user, created_at: 6.days.ago)
older_user = create(:user, created_at: 800.days.ago)
described_class.call
expect(user.badge_achievements.size).to eq(1)
expect(newer_user.badge_achievements.size).to eq(0)
expect(older_user.badge_achievements.size).to eq(0)
end
it "rewards 3-year birthday badge to birthday folks who registered 3 years ago" do
user = create(:user, created_at: 1096.days.ago)
newer_user = create(:user, created_at: 6.days.ago)
older_user = create(:user, created_at: 1200.days.ago)
described_class.call
expect(user.badge_achievements.size).to eq(1)
expect(newer_user.badge_achievements.size).to eq(0)
expect(older_user.badge_achievements.size).to eq(0)
end
end
end

View file

@ -2,31 +2,32 @@ require "rails_helper"
RSpec.describe BadgeAchievements::BadgeAwardWorker, type: :worker do
let(:worker) { subject }
let(:badge_rewarder) { BadgeRewarder }
# passing in a random badge_achievement_id argument since the worker itself won't be executed
include_examples "#enqueues_on_correct_queue", "high_priority", [456]
describe "#perform_now" do
before do
allow(badge_rewarder).to receive(:award_badges)
allow(Badges::Award).to receive(:call)
end
context "with badge achievement" do
it "sends badge email" do
worker.perform("jess", "test", "yo")
relation = User.none
allow(User).to receive(:where).with(username: ["jess"]).and_return(relation)
worker.perform(["jess"], "test", "yo")
expect(badge_rewarder).to have_received(:award_badges).with("jess", "test", "yo")
expect(Badges::Award).to have_received(:call).with(relation, "test", "yo")
end
end
context "with predefined badge_slug" do
it "sends badge email" do
allow(badge_rewarder).to receive(:award_yearly_club_badges)
worker.perform("", "award_yearly_club_badges", "")
allow(Badges::AwardYearlyClub).to receive(:call)
worker.perform([], "award_yearly_club", "")
expect(badge_rewarder).to have_received(:award_yearly_club_badges)
expect(badge_rewarder).not_to have_received(:award_badges)
expect(Badges::AwardYearlyClub).to have_received(:call)
expect(Badges::Award).not_to have_received(:call)
end
end
end