2019-05-02 17:17:27 -05:00
# frozen_string_literal: true
2014-03-05 06:52:20 -06:00
class BadgeGranter
2021-04-28 13:05:45 -05:00
class GrantError < StandardError
end
2014-03-05 06:52:20 -06:00
2020-05-18 04:22:39 -05:00
def self . disable_queue
2020-05-22 23:56:13 -05:00
@queue_disabled = true
2020-05-18 04:22:39 -05:00
end
def self . enable_queue
2020-05-22 23:56:13 -05:00
@queue_disabled = false
2020-05-18 04:22:39 -05:00
end
2014-03-05 06:52:20 -06:00
def initialize ( badge , user , opts = { } )
@badge , @user , @opts = badge , user , opts
@granted_by = opts [ :granted_by ] || Discourse . system_user
2014-06-17 01:29:49 -05:00
@post_id = opts [ :post_id ]
2014-03-05 06:52:20 -06:00
end
def self . grant ( badge , user , opts = { } )
BadgeGranter . new ( badge , user , opts ) . grant
end
2021-07-14 21:53:26 -05:00
def self . enqueue_mass_grant_for_users (
badge ,
emails : [ ] ,
usernames : [ ] ,
ensure_users_have_badge_once : true
)
emails = emails . map ( & :downcase )
usernames = usernames . map ( & :downcase )
usernames_map_to_ids = { }
emails_map_to_ids = { }
if usernames . size > 0
usernames_map_to_ids = User . where ( username_lower : usernames ) . pluck ( :username_lower , :id ) . to_h
end
if emails . size > 0
emails_map_to_ids = User . with_email ( emails ) . pluck ( " LOWER(user_emails.email) " , :id ) . to_h
end
count_per_user = { }
unmatched = Set . new
( usernames + emails ) . each do | entry |
id = usernames_map_to_ids [ entry ] || emails_map_to_ids [ entry ]
if id . blank?
unmatched << entry
next
end
if ensure_users_have_badge_once
count_per_user [ id ] = 1
else
count_per_user [ id ] || = 0
count_per_user [ id ] += 1
end
end
existing_owners_ids = [ ]
if ensure_users_have_badge_once
existing_owners_ids = UserBadge . where ( badge : badge ) . distinct . pluck ( :user_id )
end
count_per_user . each do | user_id , count |
next if ensure_users_have_badge_once && existing_owners_ids . include? ( user_id )
Jobs . enqueue ( :mass_award_badge , user : user_id , badge : badge . id , count : count )
end
2020-01-13 08:20:26 -06:00
2021-07-14 21:53:26 -05:00
{
unmatched_entries : unmatched . to_a ,
matched_users_count : count_per_user . size ,
unmatched_entries_count : unmatched . size ,
}
end
2020-01-13 08:20:26 -06:00
2021-07-14 21:53:26 -05:00
def self . mass_grant ( badge , user , count : )
return if ! badge . enabled?
raise ArgumentError . new ( " count can't be less than 1 " ) if count < 1
UserBadge . transaction do
DB . exec (
<< ~ SQL * count ,
INSERT INTO user_badges
( granted_at , created_at , granted_by_id , user_id , badge_id , seq )
VALUES
(
:now ,
:now ,
:system ,
:user_id ,
:badge_id ,
COALESCE ( (
SELECT MAX ( seq ) + 1
FROM user_badges
WHERE badge_id = :badge_id AND user_id = :user_id
) , 0 )
) ;
SQL
now : Time . zone . now ,
system : Discourse . system_user . id ,
user_id : user . id ,
badge_id : badge . id ,
2023-01-09 06:20:10 -06:00
)
2024-06-27 12:10:59 -05:00
notification = send_notification ( user . id , user . username , user . effective_locale , badge )
2020-01-13 08:20:26 -06:00
2021-07-14 21:53:26 -05:00
DB . exec ( << ~ SQL , notification_id : notification . id , user_id : user . id , badge_id : badge . id )
UPDATE user_badges
SET notification_id = :notification_id
WHERE notification_id IS NULL AND user_id = :user_id AND badge_id = :badge_id
SQL
2020-01-14 08:39:20 -06:00
UserBadge . update_featured_ranks! ( user . id )
2020-01-13 08:20:26 -06:00
end
end
2014-03-05 06:52:20 -06:00
def grant
return if @granted_by && ! Guardian . new ( @granted_by ) . can_grant_badges? ( @user )
2020-12-22 08:54:51 -06:00
return unless @badge . present? && @badge . enabled?
2021-01-18 14:12:38 -06:00
return if @user . blank?
2014-03-05 06:52:20 -06:00
2014-07-04 02:40:44 -05:00
find_by = { badge_id : @badge . id , user_id : @user . id }
find_by [ :post_id ] = @post_id if @badge . multiple_grant?
user_badge = UserBadge . find_by ( find_by )
2014-03-05 06:52:20 -06:00
2014-06-27 14:02:09 -05:00
if user_badge . nil? || ( @badge . multiple_grant? && @post_id . nil? )
2014-04-14 00:58:27 -05:00
UserBadge . transaction do
2014-08-10 18:21:06 -05:00
seq = 0
if @badge . multiple_grant?
seq = UserBadge . where ( badge : @badge , user : @user ) . maximum ( :seq )
seq = ( seq || - 1 ) + 1
end
user_badge =
UserBadge . create! (
badge : @badge ,
user : @user ,
2014-06-17 01:29:49 -05:00
granted_by : @granted_by ,
2016-04-07 12:49:44 -05:00
granted_at : @opts [ :created_at ] || Time . now ,
2014-08-10 18:21:06 -05:00
post_id : @post_id ,
seq : seq ,
)
2014-03-05 06:52:20 -06:00
2020-10-05 09:58:35 -05:00
return unless SiteSetting . enable_badges
2014-04-14 00:58:27 -05:00
if @granted_by != Discourse . system_user
StaffActionLogger . new ( @granted_by ) . log_badge_grant ( user_badge )
end
2014-04-16 14:59:45 -05:00
2021-04-26 02:41:51 -05:00
skip_new_user_tips = @user . user_option . skip_new_user_tips
unless self . class . suppress_notification? ( @badge , user_badge . granted_at , skip_new_user_tips )
2020-10-05 09:58:35 -05:00
notification =
self . class . send_notification ( @user . id , @user . username , @user . effective_locale , @badge )
user_badge . update! ( notification_id : notification . id )
2014-05-04 13:15:38 -05:00
end
2014-03-19 14:30:12 -05:00
end
2014-03-05 06:52:20 -06:00
end
user_badge
end
2014-03-19 14:30:12 -05:00
def self . revoke ( user_badge , options = { } )
2014-03-05 06:52:20 -06:00
UserBadge . transaction do
user_badge . destroy!
2014-03-19 14:30:12 -05:00
if options [ :revoked_by ]
StaffActionLogger . new ( options [ :revoked_by ] ) . log_badge_revoke ( user_badge )
end
2014-04-17 22:10:53 -05:00
2019-11-07 23:34:24 -06:00
# If the user's title is the same as the badge name OR the custom badge name, remove their title.
custom_badge_name =
TranslationOverride . find_by ( translation_key : user_badge . badge . translation_key ) & . value
user_title_is_badge_name = user_badge . user . title == user_badge . badge . name
user_title_is_custom_badge_name =
custom_badge_name . present? && user_badge . user . title == custom_badge_name
if user_title_is_badge_name || user_title_is_custom_badge_name
if options [ :revoked_by ]
StaffActionLogger . new ( options [ :revoked_by ] ) . log_title_revoke (
user_badge . user ,
revoke_reason : " user title was same as revoked badge name or custom badge name " ,
previous_value : user_badge . user . title ,
)
end
2014-04-17 22:10:53 -05:00
user_badge . user . title = nil
user_badge . user . save!
end
2014-03-05 06:52:20 -06:00
end
end
2020-01-23 11:14:58 -06:00
def self . revoke_all ( badge )
custom_badge_names =
TranslationOverride . where ( translation_key : badge . translation_key ) . pluck ( :value )
2023-01-09 06:20:10 -06:00
2020-01-23 11:14:58 -06:00
users =
User . joins ( :user_badges ) . where ( user_badges : { badge_id : badge . id } ) . where ( title : badge . name )
users =
users . or (
User . joins ( :user_badges ) . where ( title : custom_badge_names ) ,
) unless custom_badge_names . empty?
users . update_all ( title : nil )
UserBadge . where ( badge : badge ) . delete_all
end
2016-08-10 12:24:01 -05:00
def self . queue_badge_grant ( type , opt )
2020-05-22 23:56:13 -05:00
return if ! SiteSetting . enable_badges || @queue_disabled
2014-07-22 20:42:24 -05:00
payload = nil
case type
when Badge :: Trigger :: PostRevision
post = opt [ :post ]
payload = { type : " PostRevision " , post_ids : [ post . id ] }
when Badge :: Trigger :: UserChange
user = opt [ :user ]
payload = { type : " UserChange " , user_ids : [ user . id ] }
when Badge :: Trigger :: TrustLevelChange
user = opt [ :user ]
payload = { type : " TrustLevelChange " , user_ids : [ user . id ] }
when Badge :: Trigger :: PostAction
action = opt [ :post_action ]
payload = { type : " PostAction " , post_ids : [ action . post_id , action . related_post_id ] . compact! }
end
2019-12-03 03:05:53 -06:00
Discourse . redis . lpush queue_key , payload . to_json if payload
2014-05-04 13:15:38 -05:00
end
2014-07-22 20:42:24 -05:00
def self . clear_queue!
2019-12-03 03:05:53 -06:00
Discourse . redis . del queue_key
2014-07-22 20:42:24 -05:00
end
def self . process_queue!
limit = 1000
items = [ ]
2019-12-03 03:05:53 -06:00
while limit > 0 && item = Discourse . redis . lpop ( queue_key )
2014-07-22 20:42:24 -05:00
items << JSON . parse ( item )
limit -= 1
end
2016-08-10 12:24:01 -05:00
items = items . group_by { | i | i [ " type " ] }
2014-07-22 20:42:24 -05:00
items . each do | type , list |
2016-08-10 12:24:01 -05:00
post_ids = list . flat_map { | i | i [ " post_ids " ] } . compact . uniq
user_ids = list . flat_map { | i | i [ " user_ids " ] } . compact . uniq
2014-07-22 20:42:24 -05:00
2024-05-27 05:27:13 -05:00
next if post_ids . blank? && user_ids . blank?
2016-08-10 12:24:01 -05:00
2015-03-28 20:36:05 -05:00
find_by_type ( type ) . each { | badge | backfill ( badge , post_ids : post_ids , user_ids : user_ids ) }
2014-07-22 20:42:24 -05:00
end
end
def self . find_by_type ( type )
2019-01-04 08:17:54 -06:00
Badge . where ( trigger : " Badge::Trigger:: #{ type } " . constantize )
2014-07-22 20:42:24 -05:00
end
def self . queue_key
2020-04-30 01:48:34 -05:00
" badge_queue "
2014-07-22 20:42:24 -05:00
end
FEATURE: Badge query validation, preview results, and EXPLAIN
Upon saving a badge or requesting a badge result preview,
BadgeGranter.contract_checks! will examine the provided badge SQL for
some contractual obligations - namely, the returned columns and use of
trigger parameters.
Saving the badge is wrapped in a transaction to make this easier, by
raising ActiveRecord::Rollback on a detected violation.
On the client, a modal view is added for the badge query sample run
results, named admin-badge-preview.
The preview action is moved up to the route.
The save action, on failure, triggers a 'saveError' action (also in the
route).
The preview action gains a new parameter, 'explain', which will give the
output of an EXPLAIN query for the badge sql, which can be used by forum
admins to estimate the cost of their badge queries.
The preview link is replaced by two links, one which omits (false) and
includes (true) the EXPLAIN query.
The Badge.save() method is amended to propogate errors.
Badge::Trigger gets some utility methods for use in the
BadgeGranter.contract_checks! method.
Additionally, extra checks outside of BadgeGranter.contract_checks! are
added in the preview() method, to cover cases of null granted_at
columns.
An uninitialized variable path is removed in the backfill() method.
TODO - it would be nice to be able to get the actual names of all
columns the provided query returns, so we could give more errors
2014-08-25 17:17:29 -05:00
# Options:
# :target_posts - whether the badge targets posts
# :trigger - the Badge::Trigger id
def self . contract_checks! ( sql , opts = { } )
2019-01-04 08:17:54 -06:00
return if sql . blank?
FEATURE: Badge query validation, preview results, and EXPLAIN
Upon saving a badge or requesting a badge result preview,
BadgeGranter.contract_checks! will examine the provided badge SQL for
some contractual obligations - namely, the returned columns and use of
trigger parameters.
Saving the badge is wrapped in a transaction to make this easier, by
raising ActiveRecord::Rollback on a detected violation.
On the client, a modal view is added for the badge query sample run
results, named admin-badge-preview.
The preview action is moved up to the route.
The save action, on failure, triggers a 'saveError' action (also in the
route).
The preview action gains a new parameter, 'explain', which will give the
output of an EXPLAIN query for the badge sql, which can be used by forum
admins to estimate the cost of their badge queries.
The preview link is replaced by two links, one which omits (false) and
includes (true) the EXPLAIN query.
The Badge.save() method is amended to propogate errors.
Badge::Trigger gets some utility methods for use in the
BadgeGranter.contract_checks! method.
Additionally, extra checks outside of BadgeGranter.contract_checks! are
added in the preview() method, to cover cases of null granted_at
columns.
An uninitialized variable path is removed in the backfill() method.
TODO - it would be nice to be able to get the actual names of all
columns the provided query returns, so we could give more errors
2014-08-25 17:17:29 -05:00
if Badge :: Trigger . uses_post_ids? ( opts [ :trigger ] )
2014-10-07 18:26:18 -05:00
unless sql . match ( / :post_ids / )
raise (
" Contract violation: \n Query triggers on posts, but does not reference the ':post_ids' array " ,
)
2023-01-09 06:20:10 -06:00
end
2014-10-07 18:26:18 -05:00
if sql . match ( / :user_ids / )
raise " Contract violation: \n Query triggers on posts, but references the ':user_ids' array "
2023-01-09 06:20:10 -06:00
end
FEATURE: Badge query validation, preview results, and EXPLAIN
Upon saving a badge or requesting a badge result preview,
BadgeGranter.contract_checks! will examine the provided badge SQL for
some contractual obligations - namely, the returned columns and use of
trigger parameters.
Saving the badge is wrapped in a transaction to make this easier, by
raising ActiveRecord::Rollback on a detected violation.
On the client, a modal view is added for the badge query sample run
results, named admin-badge-preview.
The preview action is moved up to the route.
The save action, on failure, triggers a 'saveError' action (also in the
route).
The preview action gains a new parameter, 'explain', which will give the
output of an EXPLAIN query for the badge sql, which can be used by forum
admins to estimate the cost of their badge queries.
The preview link is replaced by two links, one which omits (false) and
includes (true) the EXPLAIN query.
The Badge.save() method is amended to propogate errors.
Badge::Trigger gets some utility methods for use in the
BadgeGranter.contract_checks! method.
Additionally, extra checks outside of BadgeGranter.contract_checks! are
added in the preview() method, to cover cases of null granted_at
columns.
An uninitialized variable path is removed in the backfill() method.
TODO - it would be nice to be able to get the actual names of all
columns the provided query returns, so we could give more errors
2014-08-25 17:17:29 -05:00
end
2019-01-04 08:17:54 -06:00
FEATURE: Badge query validation, preview results, and EXPLAIN
Upon saving a badge or requesting a badge result preview,
BadgeGranter.contract_checks! will examine the provided badge SQL for
some contractual obligations - namely, the returned columns and use of
trigger parameters.
Saving the badge is wrapped in a transaction to make this easier, by
raising ActiveRecord::Rollback on a detected violation.
On the client, a modal view is added for the badge query sample run
results, named admin-badge-preview.
The preview action is moved up to the route.
The save action, on failure, triggers a 'saveError' action (also in the
route).
The preview action gains a new parameter, 'explain', which will give the
output of an EXPLAIN query for the badge sql, which can be used by forum
admins to estimate the cost of their badge queries.
The preview link is replaced by two links, one which omits (false) and
includes (true) the EXPLAIN query.
The Badge.save() method is amended to propogate errors.
Badge::Trigger gets some utility methods for use in the
BadgeGranter.contract_checks! method.
Additionally, extra checks outside of BadgeGranter.contract_checks! are
added in the preview() method, to cover cases of null granted_at
columns.
An uninitialized variable path is removed in the backfill() method.
TODO - it would be nice to be able to get the actual names of all
columns the provided query returns, so we could give more errors
2014-08-25 17:17:29 -05:00
if Badge :: Trigger . uses_user_ids? ( opts [ :trigger ] )
2014-10-07 18:26:18 -05:00
unless sql . match ( / :user_ids / )
raise " Contract violation: \n Query triggers on users, but does not reference the ':user_ids' array "
2023-01-09 06:20:10 -06:00
end
2014-10-07 18:26:18 -05:00
if sql . match ( / :post_ids / )
raise " Contract violation: \n Query triggers on users, but references the ':post_ids' array "
2023-01-09 06:20:10 -06:00
end
FEATURE: Badge query validation, preview results, and EXPLAIN
Upon saving a badge or requesting a badge result preview,
BadgeGranter.contract_checks! will examine the provided badge SQL for
some contractual obligations - namely, the returned columns and use of
trigger parameters.
Saving the badge is wrapped in a transaction to make this easier, by
raising ActiveRecord::Rollback on a detected violation.
On the client, a modal view is added for the badge query sample run
results, named admin-badge-preview.
The preview action is moved up to the route.
The save action, on failure, triggers a 'saveError' action (also in the
route).
The preview action gains a new parameter, 'explain', which will give the
output of an EXPLAIN query for the badge sql, which can be used by forum
admins to estimate the cost of their badge queries.
The preview link is replaced by two links, one which omits (false) and
includes (true) the EXPLAIN query.
The Badge.save() method is amended to propogate errors.
Badge::Trigger gets some utility methods for use in the
BadgeGranter.contract_checks! method.
Additionally, extra checks outside of BadgeGranter.contract_checks! are
added in the preview() method, to cover cases of null granted_at
columns.
An uninitialized variable path is removed in the backfill() method.
TODO - it would be nice to be able to get the actual names of all
columns the provided query returns, so we could give more errors
2014-08-25 17:17:29 -05:00
end
2019-01-04 08:17:54 -06:00
FEATURE: Badge query validation, preview results, and EXPLAIN
Upon saving a badge or requesting a badge result preview,
BadgeGranter.contract_checks! will examine the provided badge SQL for
some contractual obligations - namely, the returned columns and use of
trigger parameters.
Saving the badge is wrapped in a transaction to make this easier, by
raising ActiveRecord::Rollback on a detected violation.
On the client, a modal view is added for the badge query sample run
results, named admin-badge-preview.
The preview action is moved up to the route.
The save action, on failure, triggers a 'saveError' action (also in the
route).
The preview action gains a new parameter, 'explain', which will give the
output of an EXPLAIN query for the badge sql, which can be used by forum
admins to estimate the cost of their badge queries.
The preview link is replaced by two links, one which omits (false) and
includes (true) the EXPLAIN query.
The Badge.save() method is amended to propogate errors.
Badge::Trigger gets some utility methods for use in the
BadgeGranter.contract_checks! method.
Additionally, extra checks outside of BadgeGranter.contract_checks! are
added in the preview() method, to cover cases of null granted_at
columns.
An uninitialized variable path is removed in the backfill() method.
TODO - it would be nice to be able to get the actual names of all
columns the provided query returns, so we could give more errors
2014-08-25 17:17:29 -05:00
if opts [ :trigger ] && ! Badge :: Trigger . is_none? ( opts [ :trigger ] )
2014-10-07 18:26:18 -05:00
unless sql . match ( / :backfill / )
raise " Contract violation: \n Query is triggered, but does not reference the ':backfill' parameter. \n (Hint: if :backfill is TRUE, you should ignore the :post_ids/:user_ids) "
2023-01-09 06:20:10 -06:00
end
FEATURE: Badge query validation, preview results, and EXPLAIN
Upon saving a badge or requesting a badge result preview,
BadgeGranter.contract_checks! will examine the provided badge SQL for
some contractual obligations - namely, the returned columns and use of
trigger parameters.
Saving the badge is wrapped in a transaction to make this easier, by
raising ActiveRecord::Rollback on a detected violation.
On the client, a modal view is added for the badge query sample run
results, named admin-badge-preview.
The preview action is moved up to the route.
The save action, on failure, triggers a 'saveError' action (also in the
route).
The preview action gains a new parameter, 'explain', which will give the
output of an EXPLAIN query for the badge sql, which can be used by forum
admins to estimate the cost of their badge queries.
The preview link is replaced by two links, one which omits (false) and
includes (true) the EXPLAIN query.
The Badge.save() method is amended to propogate errors.
Badge::Trigger gets some utility methods for use in the
BadgeGranter.contract_checks! method.
Additionally, extra checks outside of BadgeGranter.contract_checks! are
added in the preview() method, to cover cases of null granted_at
columns.
An uninitialized variable path is removed in the backfill() method.
TODO - it would be nice to be able to get the actual names of all
columns the provided query returns, so we could give more errors
2014-08-25 17:17:29 -05:00
end
# TODO these three conditions have a lot of false negatives
if opts [ :target_posts ]
2014-10-07 18:26:18 -05:00
unless sql . match ( / post_id / )
raise " Contract violation: \n Query targets posts, but does not return a 'post_id' column "
2023-01-09 06:20:10 -06:00
end
FEATURE: Badge query validation, preview results, and EXPLAIN
Upon saving a badge or requesting a badge result preview,
BadgeGranter.contract_checks! will examine the provided badge SQL for
some contractual obligations - namely, the returned columns and use of
trigger parameters.
Saving the badge is wrapped in a transaction to make this easier, by
raising ActiveRecord::Rollback on a detected violation.
On the client, a modal view is added for the badge query sample run
results, named admin-badge-preview.
The preview action is moved up to the route.
The save action, on failure, triggers a 'saveError' action (also in the
route).
The preview action gains a new parameter, 'explain', which will give the
output of an EXPLAIN query for the badge sql, which can be used by forum
admins to estimate the cost of their badge queries.
The preview link is replaced by two links, one which omits (false) and
includes (true) the EXPLAIN query.
The Badge.save() method is amended to propogate errors.
Badge::Trigger gets some utility methods for use in the
BadgeGranter.contract_checks! method.
Additionally, extra checks outside of BadgeGranter.contract_checks! are
added in the preview() method, to cover cases of null granted_at
columns.
An uninitialized variable path is removed in the backfill() method.
TODO - it would be nice to be able to get the actual names of all
columns the provided query returns, so we could give more errors
2014-08-25 17:17:29 -05:00
end
2019-01-04 08:17:54 -06:00
2014-10-07 18:26:18 -05:00
unless sql . match ( / user_id / )
raise " Contract violation: \n Query does not return a 'user_id' column "
2023-01-09 06:20:10 -06:00
end
2014-10-07 18:26:18 -05:00
unless sql . match ( / granted_at / )
raise " Contract violation: \n Query does not return a 'granted_at' column "
2023-01-09 06:20:10 -06:00
end
2014-10-07 18:26:18 -05:00
if sql . match ( / ; \ s* \ z / )
raise " Contract violation: \n Query ends with a semicolon. Remove the semicolon; your sql will be used in a subquery. "
2023-01-09 06:20:10 -06:00
end
FEATURE: Badge query validation, preview results, and EXPLAIN
Upon saving a badge or requesting a badge result preview,
BadgeGranter.contract_checks! will examine the provided badge SQL for
some contractual obligations - namely, the returned columns and use of
trigger parameters.
Saving the badge is wrapped in a transaction to make this easier, by
raising ActiveRecord::Rollback on a detected violation.
On the client, a modal view is added for the badge query sample run
results, named admin-badge-preview.
The preview action is moved up to the route.
The save action, on failure, triggers a 'saveError' action (also in the
route).
The preview action gains a new parameter, 'explain', which will give the
output of an EXPLAIN query for the badge sql, which can be used by forum
admins to estimate the cost of their badge queries.
The preview link is replaced by two links, one which omits (false) and
includes (true) the EXPLAIN query.
The Badge.save() method is amended to propogate errors.
Badge::Trigger gets some utility methods for use in the
BadgeGranter.contract_checks! method.
Additionally, extra checks outside of BadgeGranter.contract_checks! are
added in the preview() method, to cover cases of null granted_at
columns.
An uninitialized variable path is removed in the backfill() method.
TODO - it would be nice to be able to get the actual names of all
columns the provided query returns, so we could give more errors
2014-08-25 17:17:29 -05:00
end
# Options:
# :target_posts - whether the badge targets posts
# :trigger - the Badge::Trigger id
# :explain - return the EXPLAIN query
2014-07-24 03:28:09 -05:00
def self . preview ( sql , opts = { } )
2014-08-08 18:33:00 -05:00
params = { user_ids : [ ] , post_ids : [ ] , backfill : true }
2014-08-12 21:25:56 -05:00
FEATURE: Badge query validation, preview results, and EXPLAIN
Upon saving a badge or requesting a badge result preview,
BadgeGranter.contract_checks! will examine the provided badge SQL for
some contractual obligations - namely, the returned columns and use of
trigger parameters.
Saving the badge is wrapped in a transaction to make this easier, by
raising ActiveRecord::Rollback on a detected violation.
On the client, a modal view is added for the badge query sample run
results, named admin-badge-preview.
The preview action is moved up to the route.
The save action, on failure, triggers a 'saveError' action (also in the
route).
The preview action gains a new parameter, 'explain', which will give the
output of an EXPLAIN query for the badge sql, which can be used by forum
admins to estimate the cost of their badge queries.
The preview link is replaced by two links, one which omits (false) and
includes (true) the EXPLAIN query.
The Badge.save() method is amended to propogate errors.
Badge::Trigger gets some utility methods for use in the
BadgeGranter.contract_checks! method.
Additionally, extra checks outside of BadgeGranter.contract_checks! are
added in the preview() method, to cover cases of null granted_at
columns.
An uninitialized variable path is removed in the backfill() method.
TODO - it would be nice to be able to get the actual names of all
columns the provided query returns, so we could give more errors
2014-08-25 17:17:29 -05:00
BadgeGranter . contract_checks! ( sql , opts )
2014-08-12 21:25:56 -05:00
# hack to allow for params, otherwise sanitizer will trigger sprintf
2020-03-27 13:16:14 -05:00
count_sql = << ~ SQL
SELECT COUNT ( * ) count
FROM (
#{sql}
) q
WHERE :backfill = :backfill
SQL
2018-06-20 02:48:02 -05:00
grant_count = DB . query_single ( count_sql , params ) . first . to_i
2014-07-24 03:28:09 -05:00
2019-01-04 08:17:54 -06:00
grants_sql =
if opts [ :target_posts ]
<< ~ SQL
SELECT u . id , u . username , q . post_id , t . title , q . granted_at
2020-03-27 13:16:14 -05:00
FROM (
#{sql}
) q
2019-01-04 08:17:54 -06:00
JOIN users u on u . id = q . user_id
2014-07-24 03:28:09 -05:00
LEFT JOIN badge_posts p on p . id = q . post_id
2014-07-24 03:38:27 -05:00
LEFT JOIN topics t on t . id = p . topic_id
2019-01-04 08:17:54 -06:00
WHERE :backfill = :backfill
LIMIT 10
SQL
else
<< ~ SQL
SELECT u . id , u . username , q . granted_at
2020-03-27 13:16:14 -05:00
FROM (
#{sql}
) q
2019-01-04 08:17:54 -06:00
JOIN users u on u . id = q . user_id
WHERE :backfill = :backfill
LIMIT 10
SQL
end
2014-07-24 03:28:09 -05:00
FEATURE: Badge query validation, preview results, and EXPLAIN
Upon saving a badge or requesting a badge result preview,
BadgeGranter.contract_checks! will examine the provided badge SQL for
some contractual obligations - namely, the returned columns and use of
trigger parameters.
Saving the badge is wrapped in a transaction to make this easier, by
raising ActiveRecord::Rollback on a detected violation.
On the client, a modal view is added for the badge query sample run
results, named admin-badge-preview.
The preview action is moved up to the route.
The save action, on failure, triggers a 'saveError' action (also in the
route).
The preview action gains a new parameter, 'explain', which will give the
output of an EXPLAIN query for the badge sql, which can be used by forum
admins to estimate the cost of their badge queries.
The preview link is replaced by two links, one which omits (false) and
includes (true) the EXPLAIN query.
The Badge.save() method is amended to propogate errors.
Badge::Trigger gets some utility methods for use in the
BadgeGranter.contract_checks! method.
Additionally, extra checks outside of BadgeGranter.contract_checks! are
added in the preview() method, to cover cases of null granted_at
columns.
An uninitialized variable path is removed in the backfill() method.
TODO - it would be nice to be able to get the actual names of all
columns the provided query returns, so we could give more errors
2014-08-25 17:17:29 -05:00
query_plan = nil
2018-06-19 01:13:14 -05:00
# HACK: active record sanitization too flexible, force it to go down the sanitization path that cares not for % stuff
# note mini_sql uses AR sanitizer at the moment (review if changed)
query_plan = DB . query_hash ( " EXPLAIN #{ sql } /*:backfill*/ " , params ) if opts [ :explain ]
FEATURE: Badge query validation, preview results, and EXPLAIN
Upon saving a badge or requesting a badge result preview,
BadgeGranter.contract_checks! will examine the provided badge SQL for
some contractual obligations - namely, the returned columns and use of
trigger parameters.
Saving the badge is wrapped in a transaction to make this easier, by
raising ActiveRecord::Rollback on a detected violation.
On the client, a modal view is added for the badge query sample run
results, named admin-badge-preview.
The preview action is moved up to the route.
The save action, on failure, triggers a 'saveError' action (also in the
route).
The preview action gains a new parameter, 'explain', which will give the
output of an EXPLAIN query for the badge sql, which can be used by forum
admins to estimate the cost of their badge queries.
The preview link is replaced by two links, one which omits (false) and
includes (true) the EXPLAIN query.
The Badge.save() method is amended to propogate errors.
Badge::Trigger gets some utility methods for use in the
BadgeGranter.contract_checks! method.
Additionally, extra checks outside of BadgeGranter.contract_checks! are
added in the preview() method, to cover cases of null granted_at
columns.
An uninitialized variable path is removed in the backfill() method.
TODO - it would be nice to be able to get the actual names of all
columns the provided query returns, so we could give more errors
2014-08-25 17:17:29 -05:00
2018-06-19 01:13:14 -05:00
sample = DB . query ( grants_sql , params )
2014-07-24 03:28:09 -05:00
FEATURE: Badge query validation, preview results, and EXPLAIN
Upon saving a badge or requesting a badge result preview,
BadgeGranter.contract_checks! will examine the provided badge SQL for
some contractual obligations - namely, the returned columns and use of
trigger parameters.
Saving the badge is wrapped in a transaction to make this easier, by
raising ActiveRecord::Rollback on a detected violation.
On the client, a modal view is added for the badge query sample run
results, named admin-badge-preview.
The preview action is moved up to the route.
The save action, on failure, triggers a 'saveError' action (also in the
route).
The preview action gains a new parameter, 'explain', which will give the
output of an EXPLAIN query for the badge sql, which can be used by forum
admins to estimate the cost of their badge queries.
The preview link is replaced by two links, one which omits (false) and
includes (true) the EXPLAIN query.
The Badge.save() method is amended to propogate errors.
Badge::Trigger gets some utility methods for use in the
BadgeGranter.contract_checks! method.
Additionally, extra checks outside of BadgeGranter.contract_checks! are
added in the preview() method, to cover cases of null granted_at
columns.
An uninitialized variable path is removed in the backfill() method.
TODO - it would be nice to be able to get the actual names of all
columns the provided query returns, so we could give more errors
2014-08-25 17:17:29 -05:00
sample . each do | result |
2018-06-19 01:13:14 -05:00
unless User . exists? ( id : result . id )
raise " Query returned a non-existent user ID: \n #{ result . id } "
2023-01-09 06:20:10 -06:00
end
2018-06-19 01:13:14 -05:00
unless result . granted_at
raise " Query did not return a badge grant time \n (Try using 'current_timestamp granted_at') "
2023-01-09 06:20:10 -06:00
end
FEATURE: Badge query validation, preview results, and EXPLAIN
Upon saving a badge or requesting a badge result preview,
BadgeGranter.contract_checks! will examine the provided badge SQL for
some contractual obligations - namely, the returned columns and use of
trigger parameters.
Saving the badge is wrapped in a transaction to make this easier, by
raising ActiveRecord::Rollback on a detected violation.
On the client, a modal view is added for the badge query sample run
results, named admin-badge-preview.
The preview action is moved up to the route.
The save action, on failure, triggers a 'saveError' action (also in the
route).
The preview action gains a new parameter, 'explain', which will give the
output of an EXPLAIN query for the badge sql, which can be used by forum
admins to estimate the cost of their badge queries.
The preview link is replaced by two links, one which omits (false) and
includes (true) the EXPLAIN query.
The Badge.save() method is amended to propogate errors.
Badge::Trigger gets some utility methods for use in the
BadgeGranter.contract_checks! method.
Additionally, extra checks outside of BadgeGranter.contract_checks! are
added in the preview() method, to cover cases of null granted_at
columns.
An uninitialized variable path is removed in the backfill() method.
TODO - it would be nice to be able to get the actual names of all
columns the provided query returns, so we could give more errors
2014-08-25 17:17:29 -05:00
if opts [ :target_posts ]
2018-06-19 01:13:14 -05:00
raise " Query did not return a post ID " unless result . post_id
2024-05-27 05:27:13 -05:00
if Post . exists? ( result . post_id ) . blank?
2018-06-19 01:13:14 -05:00
raise " Query returned a non-existent post ID: \n #{ result . post_id } "
2023-01-09 06:20:10 -06:00
end
FEATURE: Badge query validation, preview results, and EXPLAIN
Upon saving a badge or requesting a badge result preview,
BadgeGranter.contract_checks! will examine the provided badge SQL for
some contractual obligations - namely, the returned columns and use of
trigger parameters.
Saving the badge is wrapped in a transaction to make this easier, by
raising ActiveRecord::Rollback on a detected violation.
On the client, a modal view is added for the badge query sample run
results, named admin-badge-preview.
The preview action is moved up to the route.
The save action, on failure, triggers a 'saveError' action (also in the
route).
The preview action gains a new parameter, 'explain', which will give the
output of an EXPLAIN query for the badge sql, which can be used by forum
admins to estimate the cost of their badge queries.
The preview link is replaced by two links, one which omits (false) and
includes (true) the EXPLAIN query.
The Badge.save() method is amended to propogate errors.
Badge::Trigger gets some utility methods for use in the
BadgeGranter.contract_checks! method.
Additionally, extra checks outside of BadgeGranter.contract_checks! are
added in the preview() method, to cover cases of null granted_at
columns.
An uninitialized variable path is removed in the backfill() method.
TODO - it would be nice to be able to get the actual names of all
columns the provided query returns, so we could give more errors
2014-08-25 17:17:29 -05:00
end
end
{ grant_count : grant_count , sample : sample , query_plan : query_plan }
2014-07-24 03:28:09 -05:00
rescue = > e
FEATURE: Badge query validation, preview results, and EXPLAIN
Upon saving a badge or requesting a badge result preview,
BadgeGranter.contract_checks! will examine the provided badge SQL for
some contractual obligations - namely, the returned columns and use of
trigger parameters.
Saving the badge is wrapped in a transaction to make this easier, by
raising ActiveRecord::Rollback on a detected violation.
On the client, a modal view is added for the badge query sample run
results, named admin-badge-preview.
The preview action is moved up to the route.
The save action, on failure, triggers a 'saveError' action (also in the
route).
The preview action gains a new parameter, 'explain', which will give the
output of an EXPLAIN query for the badge sql, which can be used by forum
admins to estimate the cost of their badge queries.
The preview link is replaced by two links, one which omits (false) and
includes (true) the EXPLAIN query.
The Badge.save() method is amended to propogate errors.
Badge::Trigger gets some utility methods for use in the
BadgeGranter.contract_checks! method.
Additionally, extra checks outside of BadgeGranter.contract_checks! are
added in the preview() method, to cover cases of null granted_at
columns.
An uninitialized variable path is removed in the backfill() method.
TODO - it would be nice to be able to get the actual names of all
columns the provided query returns, so we could give more errors
2014-08-25 17:17:29 -05:00
{ errors : e . message }
2014-07-24 03:28:09 -05:00
end
2024-10-15 21:09:07 -05:00
MAX_ITEMS_FOR_DELTA = 200
2014-07-22 20:42:24 -05:00
def self . backfill ( badge , opts = nil )
2014-09-02 15:12:27 -05:00
return unless SiteSetting . enable_badges
2016-08-10 12:24:01 -05:00
return unless badge . enabled
2024-05-27 05:27:13 -05:00
return if badge . query . blank?
2014-07-03 02:29:44 -05:00
FEATURE: Badge query validation, preview results, and EXPLAIN
Upon saving a badge or requesting a badge result preview,
BadgeGranter.contract_checks! will examine the provided badge SQL for
some contractual obligations - namely, the returned columns and use of
trigger parameters.
Saving the badge is wrapped in a transaction to make this easier, by
raising ActiveRecord::Rollback on a detected violation.
On the client, a modal view is added for the badge query sample run
results, named admin-badge-preview.
The preview action is moved up to the route.
The save action, on failure, triggers a 'saveError' action (also in the
route).
The preview action gains a new parameter, 'explain', which will give the
output of an EXPLAIN query for the badge sql, which can be used by forum
admins to estimate the cost of their badge queries.
The preview link is replaced by two links, one which omits (false) and
includes (true) the EXPLAIN query.
The Badge.save() method is amended to propogate errors.
Badge::Trigger gets some utility methods for use in the
BadgeGranter.contract_checks! method.
Additionally, extra checks outside of BadgeGranter.contract_checks! are
added in the preview() method, to cover cases of null granted_at
columns.
An uninitialized variable path is removed in the backfill() method.
TODO - it would be nice to be able to get the actual names of all
columns the provided query returns, so we could give more errors
2014-08-25 17:17:29 -05:00
post_ids = user_ids = nil
2014-07-22 20:42:24 -05:00
post_ids = opts [ :post_ids ] if opts
user_ids = opts [ :user_ids ] if opts
2014-08-08 18:33:00 -05:00
# safeguard fall back to full backfill if more than 200
2019-01-04 08:17:54 -06:00
if ( post_ids && post_ids . size > MAX_ITEMS_FOR_DELTA ) ||
( user_ids && user_ids . size > MAX_ITEMS_FOR_DELTA )
2014-08-08 18:33:00 -05:00
post_ids = nil
user_ids = nil
end
2019-01-04 08:17:54 -06:00
post_ids = nil if post_ids . blank?
user_ids = nil if user_ids . blank?
FEATURE: Badge query validation, preview results, and EXPLAIN
Upon saving a badge or requesting a badge result preview,
BadgeGranter.contract_checks! will examine the provided badge SQL for
some contractual obligations - namely, the returned columns and use of
trigger parameters.
Saving the badge is wrapped in a transaction to make this easier, by
raising ActiveRecord::Rollback on a detected violation.
On the client, a modal view is added for the badge query sample run
results, named admin-badge-preview.
The preview action is moved up to the route.
The save action, on failure, triggers a 'saveError' action (also in the
route).
The preview action gains a new parameter, 'explain', which will give the
output of an EXPLAIN query for the badge sql, which can be used by forum
admins to estimate the cost of their badge queries.
The preview link is replaced by two links, one which omits (false) and
includes (true) the EXPLAIN query.
The Badge.save() method is amended to propogate errors.
Badge::Trigger gets some utility methods for use in the
BadgeGranter.contract_checks! method.
Additionally, extra checks outside of BadgeGranter.contract_checks! are
added in the preview() method, to cover cases of null granted_at
columns.
An uninitialized variable path is removed in the backfill() method.
TODO - it would be nice to be able to get the actual names of all
columns the provided query returns, so we could give more errors
2014-08-25 17:17:29 -05:00
2014-08-07 19:00:10 -05:00
full_backfill = ! user_ids && ! post_ids
post_clause = badge . target_posts ? " AND (q.post_id = ub.post_id OR NOT :multiple_grant) " : " "
2014-07-03 02:29:44 -05:00
post_id_field = badge . target_posts ? " q.post_id " : " NULL "
2019-01-04 08:17:54 -06:00
sql = << ~ SQL
DELETE FROM user_badges
2020-03-27 13:16:14 -05:00
WHERE id IN (
SELECT ub . id
FROM user_badges ub
LEFT JOIN (
#{badge.query}
) q ON q . user_id = ub . user_id
#{post_clause}
WHERE ub . badge_id = :id AND q . user_id IS NULL
)
2019-01-04 08:17:54 -06:00
SQL
2014-07-03 02:29:44 -05:00
2018-06-19 01:13:14 -05:00
if badge . auto_revoke && full_backfill
DB . exec (
sql ,
id : badge . id ,
post_ids : [ - 1 ] ,
user_ids : [ - 2 ] ,
backfill : true ,
multiple_grant : true , # cheat here, cause we only run on backfill and are deleting
)
2023-01-09 06:20:10 -06:00
end
2018-06-19 01:13:14 -05:00
sql = << ~ SQL
WITH w as (
2020-04-20 12:03:25 -05:00
INSERT INTO user_badges ( badge_id , user_id , granted_at , granted_by_id , created_at , post_id )
SELECT :id , q . user_id , q . granted_at , - 1 , current_timestamp , #{post_id_field}
2020-03-27 13:16:14 -05:00
FROM (
#{badge.query}
) q
2019-01-04 08:17:54 -06:00
LEFT JOIN user_badges ub ON ub . badge_id = :id AND ub . user_id = q . user_id
2018-06-19 01:13:14 -05:00
#{post_clause}
2019-01-04 08:17:54 -06:00
/ *where* /
ON CONFLICT DO NOTHING
RETURNING id , user_id , granted_at
2018-06-19 01:13:14 -05:00
)
2021-04-26 02:41:51 -05:00
SELECT w . * , username , locale , ( u . admin OR u . moderator ) AS staff , uo . skip_new_user_tips
2019-01-04 08:17:54 -06:00
FROM w
JOIN users u on u . id = w . user_id
2021-04-26 02:41:51 -05:00
JOIN user_options uo ON uo . user_id = w . user_id
2018-06-19 01:13:14 -05:00
SQL
builder = DB . build ( sql )
2019-01-04 08:17:54 -06:00
builder . where ( " ub.badge_id IS NULL AND q.user_id > 0 " )
2014-08-27 03:02:13 -05:00
if ( post_ids || user_ids ) && ! badge . query . include? ( " :backfill " )
Rails . logger . warn " Your triggered badge query for #{ badge . name } does not include the :backfill param, skipping! "
return
end
2019-01-04 08:17:54 -06:00
if post_ids && ! badge . query . include? ( " :post_ids " )
2014-08-27 03:02:13 -05:00
Rails . logger . warn " Your triggered badge query for #{ badge . name } does not include the :post_ids param, skipping! "
return
end
2019-01-04 08:17:54 -06:00
if user_ids && ! badge . query . include? ( " :user_ids " )
2014-08-27 03:02:13 -05:00
Rails . logger . warn " Your triggered badge query for #{ badge . name } does not include the :user_ids param, skipping! "
return
end
2014-08-07 19:00:10 -05:00
2018-06-19 01:13:14 -05:00
builder
. query (
id : badge . id ,
multiple_grant : badge . multiple_grant ,
backfill : full_backfill ,
post_ids : post_ids || [ - 2 ] ,
user_ids : user_ids || [ - 2 ] ,
2023-01-09 06:20:10 -06:00
)
2018-06-19 01:13:14 -05:00
. each do | row |
2021-04-26 02:41:51 -05:00
next if suppress_notification? ( badge , row . granted_at , row . skip_new_user_tips )
2019-01-04 08:17:54 -06:00
next if row . staff && badge . awarded_for_trust_level?
2020-01-13 08:20:26 -06:00
notification = send_notification ( row . user_id , row . username , row . locale , badge )
2022-07-21 20:06:02 -05:00
UserBadge . trigger_user_badge_granted_event ( badge . id , row . user_id )
2014-07-07 02:55:25 -05:00
2018-06-19 01:13:14 -05:00
DB . exec (
" UPDATE user_badges SET notification_id = :notification_id WHERE id = :id " ,
notification_id : notification . id ,
id : row . id ,
)
2014-07-07 02:55:25 -05:00
end
2014-07-03 02:29:44 -05:00
badge . reset_grant_count!
2019-01-04 08:17:54 -06:00
rescue = > e
2021-04-28 13:05:45 -05:00
raise GrantError , " Failed to backfill ' #{ badge . name } ' badge: #{ opts } . Reason: #{ e . message } "
2014-07-01 07:00:31 -05:00
end
2014-10-07 18:26:18 -05:00
def self . revoke_ungranted_titles!
2018-06-19 01:13:14 -05:00
DB . exec << ~ SQL
2020-08-19 16:23:31 -05:00
UPDATE users u
SET title = ''
FROM user_profiles up
WHERE u . title IS NOT NULL
AND u . title < > ''
AND up . user_id = u . id
AND up . granted_title_badge_id IS NOT NULL
AND NOT EXISTS (
SELECT 1
FROM badges b
JOIN user_badges ub ON ub . user_id = u . id AND ub . badge_id = b . id
WHERE b . id = up . granted_title_badge_id
AND b . allow_title
AND b . enabled
2018-06-19 01:13:14 -05:00
)
SQL
2020-08-19 16:23:31 -05:00
DB . exec << ~ SQL
UPDATE user_profiles up
2023-03-08 06:37:20 -06:00
SET granted_title_badge_id = NULL
2020-08-19 16:23:31 -05:00
FROM users u
WHERE up . user_id = u . id
AND ( u . title IS NULL OR u . title = '' )
2023-03-08 06:37:20 -06:00
AND up . granted_title_badge_id IS NOT NULL
2020-08-19 16:23:31 -05:00
SQL
2014-10-07 18:26:18 -05:00
end
2020-01-20 16:08:48 -06:00
def self . notification_locale ( locale )
use_default_locale = ! SiteSetting . allow_user_locale || locale . blank?
use_default_locale ? SiteSetting . default_locale : locale
end
2020-01-13 08:20:26 -06:00
2020-01-20 16:08:48 -06:00
def self . send_notification ( user_id , username , locale , badge )
2022-05-05 08:09:41 -05:00
I18n . with_locale ( notification_locale ( locale ) ) do
2020-01-13 08:20:26 -06:00
Notification . create! (
user_id : user_id ,
notification_type : Notification . types [ :granted_badge ] ,
data : {
badge_id : badge . id ,
badge_name : badge . display_name ,
badge_slug : badge . slug ,
badge_title : badge . allow_title ,
username : username ,
} . to_json ,
)
end
end
2021-04-26 02:41:51 -05:00
def self . suppress_notification? ( badge , granted_at , skip_new_user_tips )
is_old_bronze_badge = badge . badge_type_id == BadgeType :: Bronze && granted_at < 2 . days . ago
skip_beginner_badge = skip_new_user_tips && badge . for_beginners?
is_old_bronze_badge || skip_beginner_badge
end
2014-03-05 06:52:20 -06:00
end