diff --git a/app/assets/javascripts/admin/controllers/admin_badge_controller.js b/app/assets/javascripts/admin/controllers/admin_badge_controller.js index 3fb21eab684..084e5a8aab3 100644 --- a/app/assets/javascripts/admin/controllers/admin_badge_controller.js +++ b/app/assets/javascripts/admin/controllers/admin_badge_controller.js @@ -8,8 +8,6 @@ @module Discourse **/ -var RESERVED_BADGE_COUNT = 100; - Discourse.AdminBadgeController = Discourse.ObjectController.extend({ /** Whether this badge has been selected. @@ -33,5 +31,5 @@ Discourse.AdminBadgeController = Discourse.ObjectController.extend({ @property readOnly @type {Boolean} **/ - readOnly: Ember.computed.lt('model.id', RESERVED_BADGE_COUNT) + readOnly: Ember.computed.alias('model.system') }); diff --git a/app/assets/javascripts/admin/controllers/admin_badges_controller.js b/app/assets/javascripts/admin/controllers/admin_badges_controller.js index baac6bf7fb7..6edc4c712a5 100644 --- a/app/assets/javascripts/admin/controllers/admin_badges_controller.js +++ b/app/assets/javascripts/admin/controllers/admin_badges_controller.js @@ -77,6 +77,21 @@ Discourse.AdminBadgesController = Ember.ArrayController.extend({ actions: { + preview: function(badge) { + // TODO wire modal and localize + Discourse.ajax('/admin/badges/preview.json', { + method: 'post', + data: {sql: badge.query, target_posts: !!badge.target_posts} + }).then(function(json){ + if(json.error){ + bootbox.alert(json.error); + } else { + bootbox.alert(json.grant_count + " badges to be assigned"); + } + }); + + }, + /** Create a new badge and select it. @@ -107,7 +122,21 @@ Discourse.AdminBadgesController = Ember.ArrayController.extend({ **/ save: function() { if (!this.get('disableSave')) { - this.get('selectedItem').save(); + var fields = ['allow_title', 'multiple_grant', + 'listable', 'auto_revoke', + 'enabled', 'show_posts', + 'target_posts', 'name', 'description', + 'icon', 'query', 'badge_grouping_id', + 'trigger']; + + if(this.get('selectedItem.system')){ + var protectedFields = this.get('protectedSystemFields'); + fields = _.filter(fields, function(f){ + return !_.include(protectedFields,f); + }); + } + + this.get('selectedItem').save(fields); } }, diff --git a/app/assets/javascripts/admin/routes/admin_badges_route.js b/app/assets/javascripts/admin/routes/admin_badges_route.js index 30da3e52de9..6ac596b5c3f 100644 --- a/app/assets/javascripts/admin/routes/admin_badges_route.js +++ b/app/assets/javascripts/admin/routes/admin_badges_route.js @@ -1,18 +1,16 @@ Discourse.AdminBadgesRoute = Discourse.Route.extend({ - - model: function() { - return Discourse.Badge.findAll(); - }, - - setupController: function(controller, model) { - // TODO build into findAll - Discourse.ajax('/admin/badges/groupings').then(function(json) { + setupController: function(controller) { + Discourse.ajax('/admin/badges.json').then(function(json){ controller.set('badgeGroupings', json.badge_groupings); - }); - Discourse.ajax('/admin/badges/types').then(function(json) { controller.set('badgeTypes', json.badge_types); + controller.set('protectedSystemFields', json.admin_badges.protected_system_fields); + var triggers = []; + _.each(json.admin_badges.triggers,function(v,k){ + triggers.push({id: v, name: I18n.t('admin.badges.trigger_type.'+k)}); + }); + controller.set('badgeTriggers', triggers); + controller.set('model', Discourse.Badge.createFromJson(json)); }); - controller.set('model', model); } }); diff --git a/app/assets/javascripts/admin/templates/badges.js.handlebars b/app/assets/javascripts/admin/templates/badges.js.handlebars index 41759554d65..e5cf7e2f511 100644 --- a/app/assets/javascripts/admin/templates/badges.js.handlebars +++ b/app/assets/javascripts/admin/templates/badges.js.handlebars @@ -52,10 +52,10 @@ {{view Ember.Select name="badge_grouping_id" value=badge_grouping_id content=controller.badgeGroupings optionValuePath="content.id" - optionLabelPath="content.name" - disabled=readOnly}} + optionLabelPath="content.name"}} +
{{#if controller.canEditDescription}} @@ -65,6 +65,40 @@ {{/if}}
+
+ + {{textarea name="query" value=query disabled=readOnly}} +
+ + {{#if hasQuery}} + + {{i18n admin.badges.preview}} + +
+ + {{input type="checkbox" checked=auto_revoke disabled=readOnly}} + {{i18n admin.badges.auto_revoke}} + +
+ +
+ + {{input type="checkbox" checked=target_posts disabled=readOnly}} + {{i18n admin.badges.target_posts}} + +
+ +
+ + {{view Ember.Select name="trigger" value=trigger + content=controller.badgeTriggers + optionValuePath="content.id" + optionLabelPath="content.name" + disabled=readOnly}} +
+ + {{/if}} +
{{input type="checkbox" checked=allow_title disabled=readOnly}} @@ -86,6 +120,13 @@
+
+ + {{input type="checkbox" checked=show_posts disabled=readOnly}} + {{i18n admin.badges.show_posts}} + +
+
{{input type="checkbox" checked=enabled}} diff --git a/app/assets/javascripts/discourse/models/badge.js b/app/assets/javascripts/discourse/models/badge.js index fa0a13ff729..64fd1ec8870 100644 --- a/app/assets/javascripts/discourse/models/badge.js +++ b/app/assets/javascripts/discourse/models/badge.js @@ -15,6 +15,11 @@ Discourse.Badge = Discourse.Model.extend({ **/ newBadge: Em.computed.none('id'), + hasQuery: function(){ + var query = this.get('query'); + return query && query.trim().length > 0; + }.property('query'), + /** @private @@ -100,7 +105,7 @@ Discourse.Badge = Discourse.Model.extend({ @method save @returns {Promise} A promise that resolves to the updated `Discourse.Badge` **/ - save: function() { + save: function(fields) { this.set('savingStatus', I18n.t('saving')); this.set('saving', true); @@ -114,19 +119,23 @@ Discourse.Badge = Discourse.Model.extend({ requestType = "PUT"; } + var boolFields = ['allow_title', 'multiple_grant', + 'listable', 'auto_revoke', + 'enabled', 'show_posts', + 'target_posts' ]; + + var data = {}; + fields.forEach(function(field){ + var d = self.get(field); + if(_.include(boolFields, field)) { + d = !!d; + } + data[field] = d; + }); + return Discourse.ajax(url, { type: requestType, - data: { - name: this.get('name'), - description: this.get('description'), - badge_type_id: this.get('badge_type_id'), - allow_title: !!this.get('allow_title'), - multiple_grant: !!this.get('multiple_grant'), - listable: !!this.get('listable'), - enabled: !!this.get('enabled'), - icon: this.get('icon'), - badge_grouping_id: this.get('badge_grouping_id') - } + data: data }).then(function(json) { self.updateFromJson(json); self.set('savingStatus', I18n.t('saved')); diff --git a/app/controllers/admin/badges_controller.rb b/app/controllers/admin/badges_controller.rb index d75bd66d07e..186f4e44b3f 100644 --- a/app/controllers/admin/badges_controller.rb +++ b/app/controllers/admin/badges_controller.rb @@ -1,4 +1,20 @@ class Admin::BadgesController < Admin::AdminController + + def index + data = { + badge_types: BadgeType.all.to_a, + badge_groupings: BadgeGrouping.all.to_a, + badges: Badge.all.to_a, + protected_system_fields: Badge.protected_system_fields, + triggers: Badge.trigger_hash + } + render_serialized(OpenStruct.new(data), AdminBadgesSerializer) + end + + def preview + render json: BadgeGranter.preview(params[:sql], target_posts: params[:target_posts] == "true") + end + def badge_types badge_types = BadgeType.all.to_a render_serialized(badge_types, BadgeTypeSerializer, root: "badge_types") @@ -35,7 +51,9 @@ class Admin::BadgesController < Admin::AdminController end def update_badge_from_params(badge) - allowed = [:icon, :name, :description, :badge_type_id, :allow_title, :multiple_grant, :listable, :enabled, :badge_grouping_id] + allowed = Badge.column_names.map(&:to_sym) + allowed -= [:id, :created_at, :updated_at, :grant_count] + allowed -= Badge.protected_system_fields if badge.system? params.permit(*allowed) allowed.each do |key| diff --git a/app/models/badge.rb b/app/models/badge.rb index 51333529afb..35d3796ad8e 100644 --- a/app/models/badge.rb +++ b/app/models/badge.rb @@ -17,7 +17,16 @@ class Badge < ActiveRecord::Base # other consts AutobiographerMinBioLength = 10 + def self.trigger_hash + Hash[*( + Badge::Trigger.constants.map{|k| + [k.to_s.underscore, Badge::Trigger.const_get(k)] + }.flatten + )] + end + module Trigger + None = 0 PostAction = 1 PostRevision = 2 TrustLevelChange = 4 @@ -169,6 +178,11 @@ SQL scope :enabled, ->{ where(enabled: true) } + # fields that can not be edited on system badges + def self.protected_system_fields + [:badge_type_id, :multiple_grant, :target_posts, :show_posts, :query, :trigger, :auto_revoke, :listable] + end + def self.trust_level_badge_ids (1..4).to_a @@ -191,6 +205,10 @@ SQL !self.multiple_grant? end + def system? + id < 100 + end + def default_name=(val) self.name ||= val end @@ -224,6 +242,7 @@ end # auto_revoke :boolean default(TRUE), not null # badge_grouping_id :integer default(5), not null # trigger :integer +# show_posts :boolean default(FALSE), not null # # Indexes # diff --git a/app/serializers/admin_badge_serializer.rb b/app/serializers/admin_badge_serializer.rb new file mode 100644 index 00000000000..83cccb0c234 --- /dev/null +++ b/app/serializers/admin_badge_serializer.rb @@ -0,0 +1,3 @@ +class AdminBadgeSerializer < BadgeSerializer + attributes :query, :trigger, :target_posts, :auto_revoke, :show_posts +end diff --git a/app/serializers/admin_badges_serializer.rb b/app/serializers/admin_badges_serializer.rb new file mode 100644 index 00000000000..eedf6fd464d --- /dev/null +++ b/app/serializers/admin_badges_serializer.rb @@ -0,0 +1,14 @@ +class AdminBadgesSerializer < ApplicationSerializer + attributes :protected_system_fields, :triggers + has_many :badges, serializer: AdminBadgeSerializer + has_many :badge_groupings + has_many :badge_types + + def protected_system_fields + object.protected_system_fields + end + + def triggers + object.triggers + end +end diff --git a/app/serializers/badge_serializer.rb b/app/serializers/badge_serializer.rb index 60a0262189d..1059f9de244 100644 --- a/app/serializers/badge_serializer.rb +++ b/app/serializers/badge_serializer.rb @@ -1,5 +1,10 @@ class BadgeSerializer < ApplicationSerializer attributes :id, :name, :description, :grant_count, :allow_title, - :multiple_grant, :icon, :listable, :enabled, :badge_grouping_id + :multiple_grant, :icon, :listable, :enabled, :badge_grouping_id, + :system has_one :badge_type + + def system + object.system? + end end diff --git a/app/services/badge_granter.rb b/app/services/badge_granter.rb index 3c5f9c663e8..4a7026ea1c0 100644 --- a/app/services/badge_granter.rb +++ b/app/services/badge_granter.rb @@ -122,6 +122,32 @@ class BadgeGranter "badge_queue".freeze end + def self.preview(sql, opts = {}) + count_sql = "SELECT COUNT(*) count FROM (#{sql}) q" + grant_count = SqlBuilder.map_exec(OpenStruct, count_sql).first.count + + grants_sql = + if opts[:target_posts] + "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 = q.topic_id + LIMIT 10" + else + "SELECT u.id, u.username, q.granted_at + FROM(#{sql}) q + JOIN users u on u.id = q.user_id + LIMIT 10" + end + + sample = SqlBuilder.map_exec(OpenStruct, grants_sql).map(&:to_h) + + {grant_count: grant_count, sample: sample} + rescue => e + {error: e.to_s} + end + def self.backfill(badge, opts=nil) return unless badge.query.present? && badge.enabled diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 0753b1387e8..7749d3d35e7 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1907,6 +1907,18 @@ en: listable: Show badge on the public badges page enabled: Enable badge icon: Icon + query: Badge Query (SQL) + target_posts: Query targets posts + auto_revoke: Run revocation query daily + show_posts: Show post granting badge on badge page + preview: Preview badge + trigger: Trigger + trigger_type: + none: "Update daily" + post_action: "When a user acts on post" + 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" lightbox: download: "download" diff --git a/config/routes.rb b/config/routes.rb index e96276eede6..c38ca989259 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -147,6 +147,7 @@ Discourse::Application.routes.draw do collection do get "types" => "badges#badge_types" get "groupings" => "badges#badge_groupings" + post "preview" => "badges#preview" end end diff --git a/test/javascripts/models/badge_test.js b/test/javascripts/models/badge_test.js index f04279d974b..cd25d79c39f 100644 --- a/test/javascripts/models/badge_test.js +++ b/test/javascripts/models/badge_test.js @@ -63,7 +63,8 @@ test('updateFromJson', function() { test('save', function() { this.stub(Discourse, 'ajax').returns(Ember.RSVP.resolve({})); var badge = Discourse.Badge.create({name: "New Badge", description: "This is a new badge.", badge_type_id: 1}); - badge.save(); + // TODO: clean API + badge.save(["name", "description", "badge_type_id"]); ok(Discourse.ajax.calledOnce, "saved badge"); });