FEATURE: allow advanced badge options in admin screen

clean up serializer, allow simplistic preview
This commit is contained in:
Sam 2014-07-24 18:28:09 +10:00
parent 469e74316b
commit ec03d135fa
14 changed files with 206 additions and 32 deletions

View file

@ -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')
});

View file

@ -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);
}
},

View file

@ -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);
}
});

View file

@ -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"}}
</div>
<div>
<label for="description">{{i18n admin.badges.description}}</label>
{{#if controller.canEditDescription}}
@ -65,6 +65,40 @@
{{/if}}
</div>
<div>
<label for="query">{{i18n admin.badges.query}}</label>
{{textarea name="query" value=query disabled=readOnly}}
</div>
{{#if hasQuery}}
<a href="/admin/badges/preview" {{action preview this}}>{{i18n admin.badges.preview}}</a>
<div>
<span>
{{input type="checkbox" checked=auto_revoke disabled=readOnly}}
{{i18n admin.badges.auto_revoke}}
</span>
</div>
<div>
<span>
{{input type="checkbox" checked=target_posts disabled=readOnly}}
{{i18n admin.badges.target_posts}}
</span>
</div>
<div>
<label for="trigger">{{i18n admin.badges.trigger}}</label>
{{view Ember.Select name="trigger" value=trigger
content=controller.badgeTriggers
optionValuePath="content.id"
optionLabelPath="content.name"
disabled=readOnly}}
</div>
{{/if}}
<div>
<span>
{{input type="checkbox" checked=allow_title disabled=readOnly}}
@ -86,6 +120,13 @@
</span>
</div>
<div>
<span>
{{input type="checkbox" checked=show_posts disabled=readOnly}}
{{i18n admin.badges.show_posts}}
</span>
</div>
<div>
<span>
{{input type="checkbox" checked=enabled}}

View file

@ -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'));

View file

@ -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|

View file

@ -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
#

View file

@ -0,0 +1,3 @@
class AdminBadgeSerializer < BadgeSerializer
attributes :query, :trigger, :target_posts, :auto_revoke, :show_posts
end

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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");
});