2014-03-05 07:52:20 -05:00
class BadgeGranter
2017-07-27 21:20:09 -04:00
def initialize ( badge , user , opts = { } )
2014-03-05 07:52:20 -05:00
@badge , @user , @opts = badge , user , opts
@granted_by = opts [ :granted_by ] || Discourse . system_user
2014-06-17 02:29:49 -04:00
@post_id = opts [ :post_id ]
2014-03-05 07:52:20 -05:00
end
2017-07-27 21:20:09 -04:00
def self . grant ( badge , user , opts = { } )
2014-03-05 07:52:20 -05:00
BadgeGranter . new ( badge , user , opts ) . grant
end
def grant
2017-07-27 21:20:09 -04:00
return if @granted_by && ! Guardian . new ( @granted_by ) . can_grant_badges? ( @user )
2016-08-19 15:16:37 -04:00
return unless @badge . enabled?
2014-03-05 07:52:20 -05:00
2014-07-04 03:40:44 -04:00
find_by = { badge_id : @badge . id , user_id : @user . id }
if @badge . multiple_grant?
find_by [ :post_id ] = @post_id
end
user_badge = UserBadge . find_by ( find_by )
2014-03-05 07:52:20 -05:00
2014-06-27 15:02:09 -04:00
if user_badge . nil? || ( @badge . multiple_grant? && @post_id . nil? )
2014-04-14 01:58:27 -04:00
UserBadge . transaction do
2014-08-10 19:21:06 -04: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 02:29:49 -04:00
granted_by : @granted_by ,
2016-04-07 13:49:44 -04:00
granted_at : @opts [ :created_at ] || Time . now ,
2014-08-10 19:21:06 -04:00
post_id : @post_id ,
seq : seq )
2014-03-05 07:52:20 -05:00
2017-04-28 12:20:05 -04:00
return unless SiteSetting . enable_badges
2014-04-14 01:58:27 -04:00
if @granted_by != Discourse . system_user
StaffActionLogger . new ( @granted_by ) . log_badge_grant ( user_badge )
end
2014-04-16 15:59:45 -04:00
2014-05-04 14:15:38 -04:00
if SiteSetting . enable_badges?
2016-04-12 08:08:38 -04:00
unless @badge . badge_type_id == BadgeType :: Bronze && user_badge . granted_at < 2 . days . ago
I18n . with_locale ( @user . effective_locale ) do
notification = @user . notifications . create (
notification_type : Notification . types [ :granted_badge ] ,
data : { badge_id : @badge . id ,
badge_name : @badge . display_name ,
badge_slug : @badge . slug ,
2017-07-27 21:20:09 -04:00
username : @user . username } . to_json
2016-04-12 08:08:38 -04:00
)
user_badge . update_attributes notification_id : notification . id
end
2015-09-23 16:52:43 -04:00
end
2014-05-04 14:15:38 -04:00
end
2014-03-19 15:30:12 -04:00
end
2014-03-05 07:52:20 -05:00
end
user_badge
end
2017-07-27 21:20:09 -04:00
def self . revoke ( user_badge , options = { } )
2014-03-05 07:52:20 -05:00
UserBadge . transaction do
user_badge . destroy!
2014-03-19 15:30:12 -04:00
if options [ :revoked_by ]
StaffActionLogger . new ( options [ :revoked_by ] ) . log_badge_revoke ( user_badge )
end
2014-04-17 23:10:53 -04:00
# If the user's title is the same as the badge name, remove their title.
if user_badge . user . title == user_badge . badge . name
user_badge . user . title = nil
user_badge . user . save!
end
2014-03-05 07:52:20 -05:00
end
end
2016-08-10 13:24:01 -04:00
def self . queue_badge_grant ( type , opt )
2014-09-02 16:12:27 -04:00
return unless SiteSetting . enable_badges
2014-07-22 21:42:24 -04: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
$redis . lpush queue_key , payload . to_json if payload
2014-05-04 14:15:38 -04:00
end
2014-07-22 21:42:24 -04:00
def self . clear_queue!
$redis . del queue_key
end
def self . process_queue!
limit = 1000
items = [ ]
while limit > 0 && item = $redis . lpop ( queue_key )
items << JSON . parse ( item )
limit -= 1
end
2016-08-10 13:24:01 -04:00
items = items . group_by { | i | i [ " type " ] }
2014-07-22 21:42:24 -04:00
items . each do | type , list |
2016-08-10 13:24:01 -04: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 21:42:24 -04:00
next unless post_ids . present? || user_ids . present?
2016-08-10 13:24:01 -04:00
find_by_type ( type ) . each do | badge |
2015-03-28 21:36:05 -04:00
backfill ( badge , post_ids : post_ids , user_ids : user_ids )
2016-08-10 13:24:01 -04:00
end
2014-07-22 21:42:24 -04:00
end
end
def self . find_by_type ( type )
id = " Badge::Trigger:: #{ type } " . constantize
Badge . where ( trigger : id )
end
def self . queue_key
" badge_queue " . freeze
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 18:17:29 -04:00
# Options:
# :target_posts - whether the badge targets posts
# :trigger - the Badge::Trigger id
def self . contract_checks! ( sql , opts = { } )
return unless sql . present?
if Badge :: Trigger . uses_post_ids? ( opts [ :trigger ] )
2014-10-07 19:26:18 -04:00
raise ( " Contract violation: \n Query triggers on posts, but does not reference the ':post_ids' array " ) unless sql . match ( / :post_ids / )
raise " Contract violation: \n Query triggers on posts, but references the ':user_ids' array " if sql . match ( / :user_ids / )
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 18:17:29 -04:00
end
if Badge :: Trigger . uses_user_ids? ( opts [ :trigger ] )
2014-10-07 19:26:18 -04:00
raise " Contract violation: \n Query triggers on users, but does not reference the ':user_ids' array " unless sql . match ( / :user_ids / )
raise " Contract violation: \n Query triggers on users, but references the ':post_ids' array " if sql . match ( / :post_ids / )
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 18:17:29 -04:00
end
if opts [ :trigger ] && ! Badge :: Trigger . is_none? ( opts [ :trigger ] )
2014-10-07 19:26:18 -04:00
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) " unless sql . match ( / :backfill / )
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 18:17:29 -04:00
end
# TODO these three conditions have a lot of false negatives
if opts [ :target_posts ]
2014-10-07 19:26:18 -04:00
raise " Contract violation: \n Query targets posts, but does not return a 'post_id' column " unless sql . match ( / post_id / )
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 18:17:29 -04:00
end
2014-10-07 19:26:18 -04:00
raise " Contract violation: \n Query does not return a 'user_id' column " unless sql . match ( / user_id / )
raise " Contract violation: \n Query does not return a 'granted_at' column " unless sql . match ( / granted_at / )
raise " Contract violation: \n Query ends with a semicolon. Remove the semicolon; your sql will be used in a subquery. " if sql . match ( / ; \ s* \ z / )
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 18:17:29 -04:00
end
# Options:
# :target_posts - whether the badge targets posts
# :trigger - the Badge::Trigger id
# :explain - return the EXPLAIN query
2014-07-24 04:28:09 -04:00
def self . preview ( sql , opts = { } )
2017-07-27 21:20:09 -04:00
params = { user_ids : [ ] , post_ids : [ ] , backfill : true }
2014-08-12 22:25:56 -04: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 18:17:29 -04:00
BadgeGranter . contract_checks! ( sql , opts )
2014-08-12 22:25:56 -04:00
# hack to allow for params, otherwise sanitizer will trigger sprintf
count_sql = " SELECT COUNT(*) count FROM ( #{ sql } ) q WHERE :backfill = :backfill "
grant_count = SqlBuilder . map_exec ( OpenStruct , count_sql , params ) . first . count . to_i
2014-07-24 04:28:09 -04:00
grants_sql =
if opts [ :target_posts ]
2017-07-27 21:20:09 -04:00
" SELECT u.id, u.username, q.post_id, t.title, q.granted_at
FROM ( #{sql}) q
JOIN users u on u . id = q . user_id
LEFT JOIN badge_posts p on p . id = q . post_id
LEFT JOIN topics t on t . id = p . topic_id
WHERE :backfill = :backfill
LIMIT 10 "
2014-07-24 04:28:09 -04:00
else
2017-07-27 21:20:09 -04:00
" SELECT u.id, u.username, q.granted_at
FROM ( #{sql}) q
JOIN users u on u . id = q . user_id
WHERE :backfill = :backfill
LIMIT 10 "
2014-07-24 04:28:09 -04: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 18:17:29 -04:00
query_plan = nil
2015-05-28 02:06:22 -04:00
# HACK: active record is weird, force it to go down the sanitization path that cares not for % stuff
query_plan = ActiveRecord :: Base . exec_sql ( " 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 18:17:29 -04:00
2014-08-08 19:33:00 -04:00
sample = SqlBuilder . map_exec ( OpenStruct , grants_sql , params ) . map ( & :to_h )
2014-07-24 04:28:09 -04: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 18:17:29 -04:00
sample . each do | result |
raise " Query returned a non-existent user ID: \n #{ result [ :id ] } " unless User . find ( result [ :id ] ) . present?
raise " Query did not return a badge grant time \n (Try using 'current_timestamp granted_at') " unless result [ :granted_at ]
if opts [ :target_posts ]
raise " Query did not return a post ID " unless result [ :post_id ]
raise " Query returned a non-existent post ID: \n #{ result [ :post_id ] } " unless Post . find ( result [ :post_id ] ) . present?
end
end
2017-07-27 21:20:09 -04:00
{ grant_count : grant_count , sample : sample , query_plan : query_plan }
2014-07-24 04:28:09 -04:00
rescue = > e
2017-07-27 21:20:09 -04:00
{ errors : e . message }
2014-07-24 04:28:09 -04:00
end
2017-06-22 06:55:58 -04:00
MAX_ITEMS_FOR_DELTA || = 200
2017-07-27 21:20:09 -04:00
def self . backfill ( badge , opts = nil )
2014-09-02 16:12:27 -04:00
return unless SiteSetting . enable_badges
2016-08-10 13:24:01 -04:00
return unless badge . enabled
return unless badge . query . present?
2014-07-03 03:29:44 -04: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 18:17:29 -04:00
post_ids = user_ids = nil
2014-07-22 21:42:24 -04:00
post_ids = opts [ :post_ids ] if opts
user_ids = opts [ :user_ids ] if opts
2014-08-08 19:33:00 -04:00
# safeguard fall back to full backfill if more than 200
if ( post_ids && post_ids . length > MAX_ITEMS_FOR_DELTA ) ||
( user_ids && user_ids . length > MAX_ITEMS_FOR_DELTA )
post_ids = nil
user_ids = nil
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 18:17:29 -04:00
post_ids = nil unless post_ids . present?
user_ids = nil unless user_ids . present?
2014-08-07 20:00:10 -04: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 03:29:44 -04:00
post_id_field = badge . target_posts ? " q.post_id " : " NULL "
sql = " DELETE FROM user_badges
WHERE id in (
SELECT ub . id
FROM user_badges ub
LEFT JOIN ( #{badge.query} ) q
ON q . user_id = ub . user_id
#{post_clause}
2014-07-05 04:32:06 -04:00
WHERE ub . badge_id = :id AND q . user_id IS NULL
2014-07-03 03:29:44 -04:00
) "
2014-08-07 20:00:10 -04:00
Badge . exec_sql ( 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
) if badge . auto_revoke && full_backfill
2014-07-03 03:29:44 -04:00
2016-02-01 02:26:45 -05:00
sql = " WITH w as (
INSERT INTO user_badges ( badge_id , user_id , granted_at , granted_by_id , post_id )
2014-07-03 03:29:44 -04:00
SELECT :id , q . user_id , q . granted_at , - 1 , #{post_id_field}
FROM ( #{badge.query} ) q
LEFT JOIN user_badges ub ON
2014-07-05 04:32:06 -04:00
ub . badge_id = :id AND ub . user_id = q . user_id
2014-07-03 03:29:44 -04:00
#{post_clause}
2014-07-22 21:42:24 -04:00
/ *where* /
2014-07-07 03:55:25 -04:00
RETURNING id , user_id , granted_at
2016-02-01 02:26:45 -05:00
)
2017-01-11 11:25:04 -05:00
select w . * , username , locale , ( u . admin OR u . moderator ) AS staff FROM w
2016-02-01 02:26:45 -05:00
JOIN users u on u . id = w . user_id
2014-07-07 03:55:25 -04:00
"
2014-07-03 03:29:44 -04:00
2014-07-07 03:55:25 -04:00
builder = SqlBuilder . new ( sql )
2014-07-22 21:42:24 -04:00
builder . where ( " ub.badge_id IS NULL AND q.user_id <> -1 " )
2014-08-27 04:02:13 -04: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
if ( post_ids && ! badge . query . include? ( " :post_ids " ) )
Rails . logger . warn " Your triggered badge query for #{ badge . name } does not include the :post_ids param, skipping! "
return
end
if ( user_ids && ! badge . query . include? ( " :user_ids " ) )
Rails . logger . warn " Your triggered badge query for #{ badge . name } does not include the :user_ids param, skipping! "
return
end
2014-08-07 20:00:10 -04:00
builder . map_exec ( OpenStruct , id : badge . id ,
multiple_grant : badge . multiple_grant ,
backfill : full_backfill ,
post_ids : post_ids || [ - 2 ] ,
user_ids : user_ids || [ - 2 ] ) . each do | row |
2014-07-07 03:55:25 -04:00
# old bronze badges do not matter
2017-07-27 21:20:09 -04:00
next if badge . badge_type_id == ( BadgeType :: Bronze ) && row . granted_at < ( 2 . days . ago )
2014-07-07 03:55:25 -04:00
2016-09-29 17:55:41 -04:00
# Try to use user locale in the badge notification if possible without too much resources
2017-07-27 21:20:09 -04:00
notification_locale =
if SiteSetting . allow_user_locale && row . locale . present?
row . locale
else
SiteSetting . default_locale
end
2016-09-29 17:55:41 -04:00
2016-10-11 18:14:32 -04:00
# Make this variable in this scope
notification = nil
2017-01-11 11:25:04 -05:00
next if ( row . staff && badge . awarded_for_trust_level? )
2017-01-10 12:18:48 -05:00
2016-09-29 17:55:41 -04:00
I18n . with_locale ( notification_locale ) do
notification = Notification . create! (
user_id : row . user_id ,
notification_type : Notification . types [ :granted_badge ] ,
data : {
badge_id : badge . id ,
badge_name : badge . display_name ,
badge_slug : badge . slug ,
username : row . username
2017-07-27 21:20:09 -04:00
} . to_json )
2016-09-29 17:55:41 -04:00
end
2014-07-07 03:55:25 -04:00
Badge . exec_sql ( " UPDATE user_badges SET notification_id = :notification_id WHERE id = :id " ,
notification_id : notification . id ,
id : row . id
)
end
2014-07-03 03:29:44 -04:00
badge . reset_grant_count!
2016-03-23 22:10:53 -04:00
rescue = > ex
2016-06-06 02:32:14 -04:00
Rails . logger . error ( " Failed to backfill ' #{ badge . name } ' badge: #{ opts } " )
2016-03-23 22:10:53 -04:00
raise ex
2014-07-01 08:00:31 -04:00
end
2014-10-07 19:26:18 -04:00
def self . revoke_ungranted_titles!
Badge . exec_sql ( " UPDATE users SET title = ''
WHERE NOT title IS NULL AND
title < > '' AND
EXISTS (
SELECT 1
FROM user_profiles
WHERE user_id = users . id AND badge_granted_title
) AND
title NOT IN (
SELECT name
FROM badges
2014-10-09 07:00:27 -04:00
WHERE allow_title AND enabled AND
badges . id IN ( SELECT badge_id FROM user_badges ub where ub . user_id = users . id )
2014-10-07 19:26:18 -04:00
)
" )
end
2014-03-05 07:52:20 -05:00
end