mirror of
https://github.com/discourse/discourse.git
synced 2024-11-29 06:03:43 +08:00
FEATURE: Two new badges - First Emoji and First Mention
This commit is contained in:
parent
56e47c8d7e
commit
c30d327b77
|
@ -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
14
app/models/user_first.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
12
db/migrate/20160405172827_create_user_firsts.rb
Normal file
12
db/migrate/20160405172827_create_user_firsts.rb
Normal 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
242
lib/badge_queries.rb
Normal 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
|
|
@ -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
|
||||
|
||||
|
|
33
spec/models/user_firsts_spec.rb
Normal file
33
spec/models/user_firsts_spec.rb
Normal 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
|
Loading…
Reference in New Issue
Block a user