FEATURE: Two new badges - First Emoji and First Mention

This commit is contained in:
Robin Ward 2016-04-05 15:12:02 -04:00
parent 56e47c8d7e
commit c30d327b77
9 changed files with 380 additions and 255 deletions

View File

@ -1,16 +1,22 @@
class Badge < ActiveRecord::Base
# badge ids
# NOTE: These badge ids are not in order! They are grouped logically. When picking an id
# search for it.
Welcome = 5
NicePost = 6
GoodPost = 7
GreatPost = 8
Autobiographer = 9
Editor = 10
FirstLike = 11
FirstShare = 12
FirstFlag = 13
FirstLink = 14
FirstQuote = 15
FirstMention = 40
FirstEmoji = 41
ReadGuidelines = 16
Reader = 17
NiceTopic = 18
@ -58,13 +64,14 @@ class Badge < ActiveRecord::Base
PostRevision = 2
TrustLevelChange = 4
UserChange = 8
PostProcessed = 16
def self.is_none?(trigger)
[None].include? trigger
end
def self.uses_user_ids?(trigger)
[TrustLevelChange, UserChange].include? trigger
[TrustLevelChange, UserChange, PostProcessed].include? trigger
end
def self.uses_post_ids?(trigger)
@ -72,240 +79,6 @@ class Badge < ActiveRecord::Base
end
end
module Queries
Reader = <<SQL
SELECT id user_id, current_timestamp granted_at
FROM users
WHERE id IN
(
SELECT pt.user_id
FROM post_timings pt
JOIN badge_posts b ON b.post_number = pt.post_number AND
b.topic_id = pt.topic_id
JOIN topics t ON t.id = pt.topic_id
LEFT JOIN user_badges ub ON ub.badge_id = 17 AND ub.user_id = pt.user_id
WHERE ub.id IS NULL AND t.posts_count > 100
GROUP BY pt.user_id, pt.topic_id, t.posts_count
HAVING count(*) >= t.posts_count
)
SQL
ReadGuidelines = <<SQL
SELECT user_id, read_faq granted_at
FROM user_stats
WHERE read_faq IS NOT NULL AND (user_id IN (:user_ids) OR :backfill)
SQL
FirstQuote = <<SQL
SELECT ids.user_id, q.post_id, q.created_at granted_at
FROM
(
SELECT p1.user_id, MIN(q1.id) id
FROM quoted_posts q1
JOIN badge_posts p1 ON p1.id = q1.post_id
JOIN badge_posts p2 ON p2.id = q1.quoted_post_id
WHERE (:backfill OR ( p1.id IN (:post_ids) ))
GROUP BY p1.user_id
) ids
JOIN quoted_posts q ON q.id = ids.id
SQL
FirstLink = <<SQL
SELECT l.user_id, l.post_id, l.created_at granted_at
FROM
(
SELECT MIN(l1.id) id
FROM topic_links l1
JOIN badge_posts p1 ON p1.id = l1.post_id
JOIN badge_posts p2 ON p2.id = l1.link_post_id
WHERE NOT reflection AND p1.topic_id <> p2.topic_id AND not quote AND
(:backfill OR ( p1.id in (:post_ids) ))
GROUP BY l1.user_id
) ids
JOIN topic_links l ON l.id = ids.id
SQL
FirstShare = <<SQL
SELECT views.user_id, i2.post_id, i2.created_at granted_at
FROM
(
SELECT i.user_id, MIN(i.id) i_id
FROM incoming_links i
JOIN badge_posts p on p.id = i.post_id
WHERE i.user_id IS NOT NULL
GROUP BY i.user_id
) as views
JOIN incoming_links i2 ON i2.id = views.i_id
SQL
FirstFlag = <<SQL
SELECT pa1.user_id, pa1.created_at granted_at, pa1.post_id
FROM (
SELECT pa.user_id, min(pa.id) id
FROM post_actions pa
JOIN badge_posts p on p.id = pa.post_id
WHERE post_action_type_id IN (#{PostActionType.flag_types.values.join(",")}) AND
(:backfill OR pa.post_id IN (:post_ids) )
GROUP BY pa.user_id
) x
JOIN post_actions pa1 on pa1.id = x.id
SQL
FirstLike = <<SQL
SELECT pa1.user_id, pa1.created_at granted_at, pa1.post_id
FROM (
SELECT pa.user_id, min(pa.id) id
FROM post_actions pa
JOIN badge_posts p on p.id = pa.post_id
WHERE post_action_type_id = 2 AND
(:backfill OR pa.post_id IN (:post_ids) )
GROUP BY pa.user_id
) x
JOIN post_actions pa1 on pa1.id = x.id
SQL
# Incorrect, but good enough - (earlies post edited vs first edit)
Editor = <<SQL
SELECT p.user_id, min(p.id) post_id, min(p.created_at) granted_at
FROM badge_posts p
WHERE p.self_edits > 0 AND
(:backfill OR p.id IN (:post_ids) )
GROUP BY p.user_id
SQL
Welcome = <<SQL
SELECT p.user_id, min(post_id) post_id, min(pa.created_at) granted_at
FROM post_actions pa
JOIN badge_posts p on p.id = pa.post_id
WHERE post_action_type_id = 2 AND
(:backfill OR pa.post_id IN (:post_ids) )
GROUP BY p.user_id
SQL
Autobiographer = <<SQL
SELECT u.id user_id, current_timestamp granted_at
FROM users u
JOIN user_profiles up on u.id = up.user_id
WHERE bio_raw IS NOT NULL AND LENGTH(TRIM(bio_raw)) > #{Badge::AutobiographerMinBioLength} AND
uploaded_avatar_id IS NOT NULL AND
(:backfill OR u.id IN (:user_ids) )
SQL
# member for a year + has posted at least once during that year
OneYearAnniversary = <<-SQL
SELECT u.id AS user_id, MIN(u.created_at + interval '1 year') AS granted_at
FROM users u
JOIN posts p ON p.user_id = u.id
WHERE u.id > 0
AND u.active
AND NOT u.blocked
AND u.created_at + interval '1 year' < now()
AND p.deleted_at IS NULL
AND NOT p.hidden
AND p.created_at + interval '1 year' > now()
AND (:backfill OR u.id IN (:user_ids))
GROUP BY u.id
HAVING COUNT(p.id) > 0
SQL
def self.invite_badge(count,trust_level)
"
SELECT u.id user_id, current_timestamp granted_at
FROM users u
WHERE u.id IN (
SELECT invited_by_id
FROM invites i
JOIN users u2 ON u2.id = i.user_id
WHERE i.deleted_at IS NULL AND u2.active AND u2.trust_level >= #{trust_level.to_i} AND not u2.blocked
GROUP BY invited_by_id
HAVING COUNT(*) >= #{count.to_i}
) AND u.active AND NOT u.blocked AND u.id > 0 AND
(:backfill OR u.id IN (:user_ids) )
"
end
def self.like_badge(count, is_topic)
# we can do better with dates, but its hard work
"
SELECT p.user_id, p.id post_id, p.updated_at granted_at
FROM badge_posts p
WHERE #{is_topic ? "p.post_number = 1" : "p.post_number > 1" } AND p.like_count >= #{count.to_i} AND
(:backfill OR p.id IN (:post_ids) )
"
end
def self.trust_level(level)
# we can do better with dates, but its hard work figuring this out historically
"
SELECT u.id user_id, current_timestamp granted_at FROM users u
WHERE trust_level >= #{level.to_i} AND (
:backfill OR u.id IN (:user_ids)
)
"
end
def self.sharing_badge(count)
<<SQL
SELECT views.user_id, i2.post_id, current_timestamp granted_at
FROM
(
SELECT i.user_id, MIN(i.id) i_id
FROM incoming_links i
JOIN badge_posts p on p.id = i.post_id
WHERE i.user_id IS NOT NULL
GROUP BY i.user_id,i.post_id
HAVING COUNT(*) > #{count}
) as views
JOIN incoming_links i2 ON i2.id = views.i_id
SQL
end
def self.linking_badge(count)
<<-SQL
SELECT tl.user_id, post_id, current_timestamp granted_at
FROM topic_links tl
JOIN posts p ON p.id = post_id AND p.deleted_at IS NULL
JOIN topics t ON t.id = p.topic_id AND t.deleted_at IS NULL AND t.archetype <> 'private_message'
WHERE NOT tl.internal
AND tl.clicks >= #{count}
GROUP BY tl.user_id, tl.post_id
SQL
end
def self.liked_posts(post_count, like_count)
<<-SQL
SELECT p.user_id, current_timestamp AS granted_at
FROM posts AS p
WHERE p.like_count >= #{like_count}
AND (:backfill OR p.user_id IN (:user_ids))
GROUP BY p.user_id
HAVING count(*) > #{post_count}
SQL
end
def self.like_rate_limit(count)
<<-SQL
SELECT gdl.user_id, current_timestamp AS granted_at
FROM given_daily_likes AS gdl
WHERE gdl.limit_reached
AND (:backfill OR gdl.user_id IN (:user_ids))
GROUP BY gdl.user_id
HAVING COUNT(*) >= #{count}
SQL
end
def self.liked_back(likes_received, likes_given)
<<-SQL
SELECT us.user_id, current_timestamp AS granted_at
FROM user_stats AS us
WHERE us.likes_received >= #{likes_received}
AND us.likes_given >= #{likes_given}
AND (:backfill OR us.user_id IN (:user_ids))
SQL
end
end
belongs_to :badge_type
belongs_to :badge_grouping
@ -433,6 +206,7 @@ SQL
end
protected
def ensure_not_system
unless id
self.id = [Badge.maximum(:id) + 1, 100].max

14
app/models/user_first.rb Normal file
View File

@ -0,0 +1,14 @@
class UserFirst < ActiveRecord::Base
def self.types
@types ||= Enum.new(used_emoji: 1, mentioned_user: 2)
end
def self.create_for(user_id, type, post_id=nil)
create!(user_id: user_id, first_type: types[type], post_id: post_id)
true
rescue PG::UniqueViolation, ActiveRecord::RecordNotUnique
# Violating the index just means the user already did it
false
end
end

View File

@ -102,6 +102,12 @@ class BadgeGranter
type: "PostAction",
post_ids: [action.post_id, action.related_post_id].compact!
}
when Badge::Trigger::PostProcessed
user = opt[:user]
payload = {
type: "PostProcessed",
user_ids: [user.id]
}
end
$redis.lpush queue_key, payload.to_json if payload

View File

@ -2738,6 +2738,7 @@ en:
post_revision: "When a user edits or creates a post"
trust_level_change: "When a user changes trust level"
user_change: "When a user is edited or created"
post_processed: "After a post is processed"
preview:
link_text: "Preview granted badges"
plan_text: "Preview with query plan"

View File

@ -1,3 +1,4 @@
require 'badge_queries'
BadgeGrouping.seed do |g|
g.id = BadgeGrouping::GettingStarted
@ -50,7 +51,7 @@ trust_level_badges.each do |spec|
b.id = spec[:id]
b.default_name = spec[:name]
b.badge_type_id = spec[:type]
b.query = Badge::Queries.trust_level(spec[:id])
b.query = BadgeQueries.trust_level(spec[:id])
b.default_badge_grouping_id = BadgeGrouping::TrustLevel
b.trigger = Badge::Trigger::TrustLevelChange
@ -68,7 +69,7 @@ Badge.seed do |b|
b.multiple_grant = false
b.target_posts = false
b.show_posts = false
b.query = Badge::Queries::Reader
b.query = BadgeQueries::Reader
b.default_badge_grouping_id = BadgeGrouping::GettingStarted
b.auto_revoke = false
b.system = true
@ -81,7 +82,7 @@ Badge.seed do |b|
b.multiple_grant = false
b.target_posts = false
b.show_posts = false
b.query = Badge::Queries::ReadGuidelines
b.query = BadgeQueries::ReadGuidelines
b.default_badge_grouping_id = BadgeGrouping::GettingStarted
b.trigger = Badge::Trigger::UserChange
b.system = true
@ -94,7 +95,7 @@ Badge.seed do |b|
b.multiple_grant = false
b.target_posts = true
b.show_posts = true
b.query = Badge::Queries::FirstLink
b.query = BadgeQueries::FirstLink
b.default_badge_grouping_id = BadgeGrouping::GettingStarted
b.trigger = Badge::Trigger::PostRevision
b.system = true
@ -107,7 +108,7 @@ Badge.seed do |b|
b.multiple_grant = false
b.target_posts = true
b.show_posts = true
b.query = Badge::Queries::FirstQuote
b.query = BadgeQueries::FirstQuote
b.default_badge_grouping_id = BadgeGrouping::GettingStarted
b.trigger = Badge::Trigger::PostRevision
b.system = true
@ -120,7 +121,7 @@ Badge.seed do |b|
b.multiple_grant = false
b.target_posts = true
b.show_posts = true
b.query = Badge::Queries::FirstLike
b.query = BadgeQueries::FirstLike
b.default_badge_grouping_id = BadgeGrouping::GettingStarted
b.trigger = Badge::Trigger::PostAction
b.system = true
@ -133,7 +134,7 @@ Badge.seed do |b|
b.multiple_grant = false
b.target_posts = true
b.show_posts = false
b.query = Badge::Queries::FirstFlag
b.query = BadgeQueries::FirstFlag
b.badge_grouping_id = BadgeGrouping::GettingStarted
b.default_badge_grouping_id = BadgeGrouping::GettingStarted
b.trigger = Badge::Trigger::PostAction
@ -154,7 +155,7 @@ end
b.multiple_grant = false
b.target_posts = false
b.show_posts = false
b.query = Badge::Queries.invite_badge(count,trust_level)
b.query = BadgeQueries.invite_badge(count,trust_level)
b.default_badge_grouping_id = BadgeGrouping::Community
# daily is good enough
b.trigger = Badge::Trigger::None
@ -170,7 +171,7 @@ Badge.seed do |b|
b.multiple_grant = false
b.target_posts = true
b.show_posts = true
b.query = Badge::Queries::FirstShare
b.query = BadgeQueries::FirstShare
b.default_badge_grouping_id = BadgeGrouping::GettingStarted
# don't trigger for now, its too expensive
b.trigger = Badge::Trigger::None
@ -191,7 +192,7 @@ end
b.multiple_grant = true
b.target_posts = true
b.show_posts = true
b.query = Badge::Queries.sharing_badge(count)
b.query = BadgeQueries.sharing_badge(count)
b.default_badge_grouping_id = BadgeGrouping::Community
# don't trigger for now, its too expensive
b.trigger = Badge::Trigger::None
@ -206,7 +207,7 @@ Badge.seed do |b|
b.multiple_grant = false
b.target_posts = true
b.show_posts = true
b.query = Badge::Queries::Welcome
b.query = BadgeQueries::Welcome
b.default_badge_grouping_id = BadgeGrouping::Community
b.trigger = Badge::Trigger::PostAction
b.system = true
@ -217,7 +218,7 @@ Badge.seed do |b|
b.default_name = "Autobiographer"
b.badge_type_id = BadgeType::Bronze
b.multiple_grant = false
b.query = Badge::Queries::Autobiographer
b.query = BadgeQueries::Autobiographer
b.default_badge_grouping_id = BadgeGrouping::GettingStarted
b.trigger = Badge::Trigger::UserChange
b.system = true
@ -228,7 +229,7 @@ Badge.seed do |b|
b.default_name = "Editor"
b.badge_type_id = BadgeType::Bronze
b.multiple_grant = false
b.query = Badge::Queries::Editor
b.query = BadgeQueries::Editor
b.badge_grouping_id = BadgeGrouping::GettingStarted
b.default_badge_grouping_id = BadgeGrouping::GettingStarted
b.trigger = Badge::Trigger::PostRevision
@ -254,7 +255,7 @@ like_badges.each do |spec|
b.multiple_grant = true
b.target_posts = true
b.show_posts = true
b.query = Badge::Queries.like_badge(Badge.like_badge_counts[spec[:id]], spec[:topic])
b.query = BadgeQueries.like_badge(Badge.like_badge_counts[spec[:id]], spec[:topic])
b.default_badge_grouping_id = BadgeGrouping::Posting
b.trigger = Badge::Trigger::PostAction
b.system = true
@ -266,7 +267,7 @@ Badge.seed do |b|
b.default_name = "Anniversary"
b.default_icon = "fa-clock-o"
b.badge_type_id = BadgeType::Silver
b.query = Badge::Queries::OneYearAnniversary
b.query = BadgeQueries::OneYearAnniversary
b.default_badge_grouping_id = BadgeGrouping::Community
b.trigger = Badge::Trigger::None
b.auto_revoke = false
@ -286,7 +287,7 @@ end
b.multiple_grant = true
b.target_posts = true
b.show_posts = true
b.query = Badge::Queries.linking_badge(count)
b.query = BadgeQueries.linking_badge(count)
b.badge_grouping_id = BadgeGrouping::Posting
b.default_badge_grouping_id = BadgeGrouping::Posting
# don't trigger for now, its too expensive
@ -307,7 +308,7 @@ end
b.default_name = name
b.default_icon = "fa-heart"
b.badge_type_id = level
b.query = Badge::Queries.liked_posts(post_count, like_count)
b.query = BadgeQueries.liked_posts(post_count, like_count)
b.default_badge_grouping_id = BadgeGrouping::Community
b.trigger = Badge::Trigger::None
b.auto_revoke = false
@ -327,7 +328,7 @@ end
b.default_name = name
b.default_icon = "fa-heart"
b.badge_type_id = level
b.query = Badge::Queries.liked_back(count, ratio)
b.query = BadgeQueries.liked_back(count, ratio)
b.badge_grouping_id = BadgeGrouping::Community
b.default_badge_grouping_id = BadgeGrouping::Community
b.trigger = Badge::Trigger::None
@ -348,7 +349,7 @@ end
b.default_name = name
b.default_icon = "fa-heart"
b.badge_type_id = level
b.query = Badge::Queries.like_rate_limit(count)
b.query = BadgeQueries.like_rate_limit(count)
b.badge_grouping_id = BadgeGrouping::Community
b.default_badge_grouping_id = BadgeGrouping::Community
b.trigger = Badge::Trigger::None
@ -357,6 +358,31 @@ end
end
end
Badge.seed do |b|
b.id = Badge::FirstMention
b.default_name = "First Mention"
b.badge_type_id = BadgeType::Bronze
b.multiple_grant = false
b.target_posts = true
b.show_posts = true
b.query = BadgeQueries.has_user_first(:mentioned_user)
b.default_badge_grouping_id = BadgeGrouping::GettingStarted
b.trigger = Badge::Trigger::PostProcessed
b.system = true
end
Badge.seed do |b|
b.id = Badge::FirstEmoji
b.default_name = "First Emoji"
b.badge_type_id = BadgeType::Bronze
b.multiple_grant = false
b.target_posts = true
b.show_posts = true
b.query = BadgeQueries.has_user_first(:used_emoji)
b.default_badge_grouping_id = BadgeGrouping::GettingStarted
b.trigger = Badge::Trigger::PostProcessed
b.system = true
end
Badge.where("NOT system AND id < 100").each do |badge|
new_id = [Badge.maximum(:id) + 1, 100].max

View File

@ -0,0 +1,12 @@
class CreateUserFirsts < ActiveRecord::Migration
def change
create_table :user_firsts, force: true do |t|
t.integer :user_id, null: false
t.integer :first_type, null: false
t.integer :post_id
t.datetime :created_at, null: false
end
add_index :user_firsts, [:user_id, :first_type], unique: true
end
end

242
lib/badge_queries.rb Normal file
View File

@ -0,0 +1,242 @@
module BadgeQueries
Reader = <<SQL
SELECT id user_id, current_timestamp granted_at
FROM users
WHERE id IN
(
SELECT pt.user_id
FROM post_timings pt
JOIN badge_posts b ON b.post_number = pt.post_number AND
b.topic_id = pt.topic_id
JOIN topics t ON t.id = pt.topic_id
LEFT JOIN user_badges ub ON ub.badge_id = 17 AND ub.user_id = pt.user_id
WHERE ub.id IS NULL AND t.posts_count > 100
GROUP BY pt.user_id, pt.topic_id, t.posts_count
HAVING count(*) >= t.posts_count
)
SQL
ReadGuidelines = <<SQL
SELECT user_id, read_faq granted_at
FROM user_stats
WHERE read_faq IS NOT NULL AND (user_id IN (:user_ids) OR :backfill)
SQL
FirstQuote = <<SQL
SELECT ids.user_id, q.post_id, q.created_at granted_at
FROM
(
SELECT p1.user_id, MIN(q1.id) id
FROM quoted_posts q1
JOIN badge_posts p1 ON p1.id = q1.post_id
JOIN badge_posts p2 ON p2.id = q1.quoted_post_id
WHERE (:backfill OR ( p1.id IN (:post_ids) ))
GROUP BY p1.user_id
) ids
JOIN quoted_posts q ON q.id = ids.id
SQL
FirstLink = <<SQL
SELECT l.user_id, l.post_id, l.created_at granted_at
FROM
(
SELECT MIN(l1.id) id
FROM topic_links l1
JOIN badge_posts p1 ON p1.id = l1.post_id
JOIN badge_posts p2 ON p2.id = l1.link_post_id
WHERE NOT reflection AND p1.topic_id <> p2.topic_id AND not quote AND
(:backfill OR ( p1.id in (:post_ids) ))
GROUP BY l1.user_id
) ids
JOIN topic_links l ON l.id = ids.id
SQL
FirstShare = <<SQL
SELECT views.user_id, i2.post_id, i2.created_at granted_at
FROM
(
SELECT i.user_id, MIN(i.id) i_id
FROM incoming_links i
JOIN badge_posts p on p.id = i.post_id
WHERE i.user_id IS NOT NULL
GROUP BY i.user_id
) as views
JOIN incoming_links i2 ON i2.id = views.i_id
SQL
FirstFlag = <<SQL
SELECT pa1.user_id, pa1.created_at granted_at, pa1.post_id
FROM (
SELECT pa.user_id, min(pa.id) id
FROM post_actions pa
JOIN badge_posts p on p.id = pa.post_id
WHERE post_action_type_id IN (#{PostActionType.flag_types.values.join(",")}) AND
(:backfill OR pa.post_id IN (:post_ids) )
GROUP BY pa.user_id
) x
JOIN post_actions pa1 on pa1.id = x.id
SQL
FirstLike = <<SQL
SELECT pa1.user_id, pa1.created_at granted_at, pa1.post_id
FROM (
SELECT pa.user_id, min(pa.id) id
FROM post_actions pa
JOIN badge_posts p on p.id = pa.post_id
WHERE post_action_type_id = 2 AND
(:backfill OR pa.post_id IN (:post_ids) )
GROUP BY pa.user_id
) x
JOIN post_actions pa1 on pa1.id = x.id
SQL
# Incorrect, but good enough - (earlies post edited vs first edit)
Editor = <<SQL
SELECT p.user_id, min(p.id) post_id, min(p.created_at) granted_at
FROM badge_posts p
WHERE p.self_edits > 0 AND
(:backfill OR p.id IN (:post_ids) )
GROUP BY p.user_id
SQL
Welcome = <<SQL
SELECT p.user_id, min(post_id) post_id, min(pa.created_at) granted_at
FROM post_actions pa
JOIN badge_posts p on p.id = pa.post_id
WHERE post_action_type_id = 2 AND
(:backfill OR pa.post_id IN (:post_ids) )
GROUP BY p.user_id
SQL
Autobiographer = <<SQL
SELECT u.id user_id, current_timestamp granted_at
FROM users u
JOIN user_profiles up on u.id = up.user_id
WHERE bio_raw IS NOT NULL AND LENGTH(TRIM(bio_raw)) > #{Badge::AutobiographerMinBioLength} AND
uploaded_avatar_id IS NOT NULL AND
(:backfill OR u.id IN (:user_ids) )
SQL
# member for a year + has posted at least once during that year
OneYearAnniversary = <<-SQL
SELECT u.id AS user_id, MIN(u.created_at + interval '1 year') AS granted_at
FROM users u
JOIN posts p ON p.user_id = u.id
WHERE u.id > 0
AND u.active
AND NOT u.blocked
AND u.created_at + interval '1 year' < now()
AND p.deleted_at IS NULL
AND NOT p.hidden
AND p.created_at + interval '1 year' > now()
AND (:backfill OR u.id IN (:user_ids))
GROUP BY u.id
HAVING COUNT(p.id) > 0
SQL
def self.invite_badge(count,trust_level)
"
SELECT u.id user_id, current_timestamp granted_at
FROM users u
WHERE u.id IN (
SELECT invited_by_id
FROM invites i
JOIN users u2 ON u2.id = i.user_id
WHERE i.deleted_at IS NULL AND u2.active AND u2.trust_level >= #{trust_level.to_i} AND not u2.blocked
GROUP BY invited_by_id
HAVING COUNT(*) >= #{count.to_i}
) AND u.active AND NOT u.blocked AND u.id > 0 AND
(:backfill OR u.id IN (:user_ids) )
"
end
def self.like_badge(count, is_topic)
# we can do better with dates, but its hard work
"
SELECT p.user_id, p.id post_id, p.updated_at granted_at
FROM badge_posts p
WHERE #{is_topic ? "p.post_number = 1" : "p.post_number > 1" } AND p.like_count >= #{count.to_i} AND
(:backfill OR p.id IN (:post_ids) )
"
end
def self.trust_level(level)
# we can do better with dates, but its hard work figuring this out historically
"
SELECT u.id user_id, current_timestamp granted_at FROM users u
WHERE trust_level >= #{level.to_i} AND (
:backfill OR u.id IN (:user_ids)
)
"
end
def self.sharing_badge(count)
<<SQL
SELECT views.user_id, i2.post_id, current_timestamp granted_at
FROM
(
SELECT i.user_id, MIN(i.id) i_id
FROM incoming_links i
JOIN badge_posts p on p.id = i.post_id
WHERE i.user_id IS NOT NULL
GROUP BY i.user_id,i.post_id
HAVING COUNT(*) > #{count}
) as views
JOIN incoming_links i2 ON i2.id = views.i_id
SQL
end
def self.linking_badge(count)
<<-SQL
SELECT tl.user_id, post_id, current_timestamp granted_at
FROM topic_links tl
JOIN posts p ON p.id = post_id AND p.deleted_at IS NULL
JOIN topics t ON t.id = p.topic_id AND t.deleted_at IS NULL AND t.archetype <> 'private_message'
WHERE NOT tl.internal
AND tl.clicks >= #{count}
GROUP BY tl.user_id, tl.post_id
SQL
end
def self.liked_posts(post_count, like_count)
<<-SQL
SELECT p.user_id, current_timestamp AS granted_at
FROM posts AS p
WHERE p.like_count >= #{like_count}
AND (:backfill OR p.user_id IN (:user_ids))
GROUP BY p.user_id
HAVING count(*) > #{post_count}
SQL
end
def self.like_rate_limit(count)
<<-SQL
SELECT gdl.user_id, current_timestamp AS granted_at
FROM given_daily_likes AS gdl
WHERE gdl.limit_reached
AND (:backfill OR gdl.user_id IN (:user_ids))
GROUP BY gdl.user_id
HAVING COUNT(*) >= #{count}
SQL
end
def self.liked_back(likes_received, likes_given)
<<-SQL
SELECT us.user_id, current_timestamp AS granted_at
FROM user_stats AS us
WHERE us.likes_received >= #{likes_received}
AND us.likes_given >= #{likes_given}
AND (:backfill OR us.user_id IN (:user_ids))
SQL
end
def self.has_user_first(type)
<<SQL
SELECT uf.user_id, uf.post_id, uf.created_at AS granted_at
FROM user_firsts AS uf
WHERE (:backfill OR uf.user_id IN (:user_ids))
AND uf.first_type = #{UserFirst.types[type]}
SQL
end
end

View File

@ -28,6 +28,23 @@ class CookedPostProcessor
post_process_oneboxes
optimize_urls
pull_hotlinked_images(bypass_bump)
create_firsts
end
end
def create_firsts
created = false
if @doc.css("img.emoji").size > 0
created |= UserFirst.create_for(@post.user_id, :used_emoji, @post.id)
end
if @doc.css("span.mention, a.mention").size > 0
created |= UserFirst.create_for(@post.user_id, :mentioned_user, @post.id)
end
if created
BadgeGranter.queue_badge_grant(Badge::Trigger::PostProcessed, user: @post.user)
end
end

View File

@ -0,0 +1,33 @@
require 'rails_helper'
describe UserFirst do
let(:user) { Fabricate(:user) }
context "#create_for" do
it "doesn't raise an error on duplicate" do
expect(UserFirst.create_for(user.id, :used_emoji)).to eq(true)
expect(UserFirst.create_for(user.id, :used_emoji)).to eq(false)
end
end
it "creates one the first time a user posts an emoji" do
post = PostCreator.create(user, title: "this topic is about candy", raw: "time to eat some sweet :candy: mmmm")
uf = UserFirst.where(user_id: user.id, first_type: UserFirst.types[:used_emoji]).first
expect(uf).to be_present
expect(uf.post_id).to eq(post.id)
end
context 'mentioning' do
let(:codinghorror) { Fabricate(:codinghorror) }
it "creates one the first time a user mentions another" do
post = PostCreator.create(user, title: "gonna mention another user", raw: "what is up @codinghorror?")
uf = UserFirst.where(user_id: user.id, first_type: UserFirst.types[:mentioned_user]).first
expect(uf).to be_present
expect(uf.post_id).to eq(post.id)
end
end
end