Add group messages support to admin UI, and add a ‘type’ field for further improvements

This commit is contained in:
David Taylor 2017-08-01 15:20:00 +01:00
parent d437634f61
commit 209daf7801
14 changed files with 124 additions and 47 deletions

View File

@ -118,7 +118,7 @@ class DiscourseChat::ChatController < ApplicationController
def create_rule def create_rule
begin begin
hash = params.require(:rule).permit(:channel_id, :filter, :group_id, :category_id, tags:[]) hash = params.require(:rule).permit(:channel_id, :type, :filter, :group_id, :category_id, tags:[])
rule = DiscourseChat::Rule.new(hash) rule = DiscourseChat::Rule.new(hash)
@ -135,7 +135,7 @@ class DiscourseChat::ChatController < ApplicationController
def update_rule def update_rule
begin begin
rule = DiscourseChat::Rule.find(params[:id].to_i) rule = DiscourseChat::Rule.find(params[:id].to_i)
hash = params.require(:rule).permit(:filter, :group_id, :category_id, tags:[]) hash = params.require(:rule).permit(:type, :filter, :group_id, :category_id, tags:[])
if not rule.update(hash) if not rule.update(hash)
raise Discourse::InvalidParameters, 'Rule is not valid' raise Discourse::InvalidParameters, 'Rule is not valid'

View File

@ -59,7 +59,7 @@ class DiscourseChat::Channel < DiscourseChat::PluginModel
end end
def rules def rules
DiscourseChat::Rule.with_channel_id(id) DiscourseChat::Rule.with_channel_id(id).order_by_precedence
end end
scope :with_provider, ->(provider) { where("value::json->>'provider'=?", provider)} scope :with_provider, ->(provider) { where("value::json->>'provider'=?", provider)}

View File

@ -2,18 +2,22 @@ class DiscourseChat::Rule < DiscourseChat::PluginModel
KEY_PREFIX = 'rule:' KEY_PREFIX = 'rule:'
# Setup ActiveRecord::Store to use the JSON field to read/write these values # Setup ActiveRecord::Store to use the JSON field to read/write these values
store :value, accessors: [ :channel_id, :group_id, :category_id, :tags, :filter ], coder: JSON store :value, accessors: [ :channel_id, :type, :group_id, :category_id, :tags, :filter ], coder: JSON
after_initialize :init_filter after_initialize :init_filter
def init_filter def init_filter
self.filter ||= 'watch' self.filter ||= 'watch'
self.type ||= 'normal'
end end
validates :filter, :inclusion => { :in => %w(watch follow mute), validates :filter, :inclusion => { :in => %w(watch follow mute),
:message => "%{value} is not a valid filter" } :message => "%{value} is not a valid filter" }
validate :channel_valid?, :category_and_group_valid?, :tags_valid? validates :type, :inclusion => { :in => %w(normal group_message),
:message => "%{value} is not a valid filter" }
validate :channel_valid?, :category_valid?, :group_valid?, :tags_valid?
def channel_valid? def channel_valid?
# Validate channel # Validate channel
@ -22,19 +26,12 @@ class DiscourseChat::Rule < DiscourseChat::PluginModel
end end
end end
def category_and_group_valid? def category_valid?
if category_id and group_id if type != 'normal' && !category_id.nil?
errors.add(:category_id, "cannot be specified in addition to group_id") errors.add(:category_id, "cannot be specified for that type of rule")
return
end end
if group_id return unless type == 'normal'
# Validate group
if not Group.where(id: group_id).exists?
errors.add(:group_id, "#{group_id} is not a valid group id")
end
return
end
# Validate category # Validate category
if not (category_id.nil? or Category.where(id: category_id).exists?) if not (category_id.nil? or Category.where(id: category_id).exists?)
@ -42,6 +39,19 @@ class DiscourseChat::Rule < DiscourseChat::PluginModel
end end
end end
def group_valid?
if type == 'normal' && !group_id.nil?
errors.add(:group_id, "cannot be specified for that type of rule")
end
return if type == 'normal'
# Validate group
if not Group.where(id: group_id).exists?
errors.add(:group_id, "#{group_id} is not a valid group id")
end
end
def tags_valid? def tags_valid?
# Validate tags # Validate tags
return if tags.nil? return if tags.nil?
@ -81,6 +91,8 @@ class DiscourseChat::Rule < DiscourseChat::PluginModel
self.channel_id = val.id self.channel_id = val.id
end end
scope :with_type, ->(type) { where("value::json->>'type'=?", type.to_s)}
scope :with_channel, ->(channel) { with_channel_id(channel.id) } scope :with_channel, ->(channel) { with_channel_id(channel.id) }
scope :with_channel_id, ->(channel_id) { where("value::json->>'channel_id'=?", channel_id.to_s)} scope :with_channel_id, ->(channel_id) { where("value::json->>'channel_id'=?", channel_id.to_s)}
@ -88,6 +100,11 @@ class DiscourseChat::Rule < DiscourseChat::PluginModel
scope :with_group_ids, ->(group_id) { where("value::json->>'group_id' IN (?)", group_id.map(&:to_s))} scope :with_group_ids, ->(group_id) { where("value::json->>'group_id' IN (?)", group_id.map(&:to_s))}
scope :order_by_precedence, ->{ order("CASE scope :order_by_precedence, ->{ order("CASE
WHEN value::json->>'type' = 'group_mention' THEN 1
WHEN value::json->>'type' = 'group_message' THEN 2
ELSE 3
END",
"CASE
WHEN value::json->>'filter' = 'mute' THEN 1 WHEN value::json->>'filter' = 'mute' THEN 1
WHEN value::json->>'filter' = 'watch' THEN 2 WHEN value::json->>'filter' = 'watch' THEN 2
WHEN value::json->>'filter' = 'follow' THEN 3 WHEN value::json->>'filter' = 'follow' THEN 3

View File

@ -1,5 +1,5 @@
class DiscourseChat::RuleSerializer < ApplicationSerializer class DiscourseChat::RuleSerializer < ApplicationSerializer
attributes :id, :channel_id, :group_id, :group_name, :category_id, :tags, :filter attributes :id, :channel_id, :type, :group_id, :group_name, :category_id, :tags, :filter
def group_name def group_name
if object.group_id if object.group_id

View File

@ -25,7 +25,7 @@ module DiscourseChat
if topic.archetype == Archetype.private_message if topic.archetype == Archetype.private_message
group_ids_with_access = topic.topic_allowed_groups.pluck(:group_id) group_ids_with_access = topic.topic_allowed_groups.pluck(:group_id)
return if group_ids_with_access.empty? return if group_ids_with_access.empty?
matching_rules = DiscourseChat::Rule.with_group_ids(group_ids_with_access) matching_rules = DiscourseChat::Rule.with_type('group_message').with_group_ids(group_ids_with_access)
else else
matching_rules = DiscourseChat::Rule.with_category_id(topic.category_id) matching_rules = DiscourseChat::Rule.with_category_id(topic.category_id)
if topic.category # Also load the rules for the wildcard category if topic.category # Also load the rules for the wildcard category

View File

@ -37,12 +37,12 @@ export default Ember.Controller.extend({
createRule(channel){ createRule(channel){
this.set('modalShowing', true); this.set('modalShowing', true);
var model = {rule: this.store.createRecord('rule',{channel_id: channel.id}), channel:channel, provider:this.get('model.provider')}; var model = {rule: this.store.createRecord('rule',{channel_id: channel.id}), channel:channel, provider:this.get('model.provider'), groups:this.get('model.groups')};
showModal('admin-plugins-chat-edit-rule', { model: model, admin: true }); showModal('admin-plugins-chat-edit-rule', { model: model, admin: true });
}, },
editRule(rule, channel){ editRule(rule, channel){
this.set('modalShowing', true); this.set('modalShowing', true);
var model = {rule: rule, channel:channel, provider:this.get('model.provider')}; var model = {rule: rule, channel:channel, provider:this.get('model.provider'), groups:this.get('model.groups')};
showModal('admin-plugins-chat-edit-rule', { model: model, admin: true }); showModal('admin-plugins-chat-edit-rule', { model: model, admin: true });
}, },

View File

@ -3,9 +3,9 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality';
import { ajax } from 'discourse/lib/ajax'; import { ajax } from 'discourse/lib/ajax';
import { extractError } from 'discourse/lib/ajax-error'; import { extractError } from 'discourse/lib/ajax-error';
import InputValidation from 'discourse/models/input-validation'; import InputValidation from 'discourse/models/input-validation';
import computed from "ember-addons/ember-computed-decorators";
export default Ember.Controller.extend(ModalFunctionality, { export default Ember.Controller.extend(ModalFunctionality, {
setupKeydown: function() { setupKeydown: function() {
Ember.run.schedule('afterRender', () => { Ember.run.schedule('afterRender', () => {
$('#chat_integration_edit_channel_modal').keydown(e => { $('#chat_integration_edit_channel_modal').keydown(e => {
@ -16,11 +16,15 @@ export default Ember.Controller.extend(ModalFunctionality, {
}); });
}.on('init'), }.on('init'),
saveDisabled: function(){ saveDisabled: function(){
return false; return false;
}.property(), }.property(),
@computed('model.rule.type')
showCategory: function(type){
return (type == "normal")
},
actions: { actions: {
cancel: function(){ cancel: function(){
this.send('closeModal'); this.send('closeModal');

View File

@ -9,12 +9,29 @@ export default RestModel.extend({
{ id: 'mute', name: I18n.t('chat_integration.filter.mute'), icon: 'times-circle' } { id: 'mute', name: I18n.t('chat_integration.filter.mute'), icon: 'times-circle' }
], ],
available_types: [
{ id: 'normal', name: I18n.t('chat_integration.type.normal')},
{ id: 'group_message', name: I18n.t('chat_integration.type.group_message')},
{ id: 'group_mention', name: I18n.t('chat_integration.type.group_mention')}
],
category_id: null, category_id: null,
tags: null, tags: null,
channel_id: null, channel_id: null,
filter: 'watch', filter: 'watch',
type: 'normal',
error_key: null, error_key: null,
removeUnneededInfo: function(){
const type=this.get('type');
if(type=='normal'){
this.set('group_id', null);
}else{
this.set('category_id', null);
}
}.observes('type'),
@computed('category_id') @computed('category_id')
category(categoryId) { category(categoryId) {
if (categoryId){ if (categoryId){
@ -30,12 +47,12 @@ export default RestModel.extend({
}, },
updateProperties() { updateProperties() {
var prop_names = ['category_id','group_id','tags','filter']; var prop_names = ['type','category_id','group_id','tags','filter'];
return this.getProperties(prop_names); return this.getProperties(prop_names);
}, },
createProperties() { createProperties() {
var prop_names = ['channel_id', 'category_id','group_id','tags','filter']; var prop_names = ['type','channel_id', 'category_id','group_id','tags','filter'];
return this.getProperties(prop_names); return this.getProperties(prop_names);
} }

View File

@ -1,11 +1,15 @@
import { ajax } from 'discourse/lib/ajax'; import { ajax } from 'discourse/lib/ajax';
import Group from 'discourse/models/group';
export default Discourse.Route.extend({ export default Discourse.Route.extend({
model(params, transition) { model(params, transition) {
return Ember.RSVP.hash({ return Ember.RSVP.hash({
channels: this.store.findAll('channel', {provider: params.provider}), channels: this.store.findAll('channel', {provider: params.provider}),
provider: this.modelFor("admin-plugins-chat").findBy('id',params.provider) provider: this.modelFor("admin-plugins-chat").findBy('id',params.provider),
groups: Group.findAll().then(groups => {
return groups.filter(g => !g.get('automatic'));
})
}).then(value => { }).then(value => {
value.channels.forEach(channel => { value.channels.forEach(channel => {
channel.set('rules', channel.rules.map(rule => { channel.set('rules', channel.rules.map(rule => {

View File

@ -25,6 +25,17 @@
<td></td> <td></td>
</tr> </tr>
<tr class="input">
<td class="label"><label for='filter'>{{i18n "chat_integration.edit_rule_modal.type"}}</label></td>
<td>
{{combo-box name="type" content=model.rule.available_types value=model.rule.type}}
</td>
</tr>
<tr class="instructions">
<td></td>
<td><label>{{i18n 'chat_integration.edit_rule_modal.instructions.type'}}</label></td>
</tr>
<tr class="input"> <tr class="input">
<td class="label"><label for='filter'>{{i18n "chat_integration.edit_rule_modal.filter"}}</label></td> <td class="label"><label for='filter'>{{i18n "chat_integration.edit_rule_modal.filter"}}</label></td>
<td> <td>
@ -36,6 +47,7 @@
<td><label>{{i18n 'chat_integration.edit_rule_modal.instructions.filter'}}</label></td> <td><label>{{i18n 'chat_integration.edit_rule_modal.instructions.filter'}}</label></td>
</tr> </tr>
{{#if showCategory}}
<tr class="input"> <tr class="input">
<td class="label"><label for='category'>{{i18n "chat_integration.edit_rule_modal.category"}}</label></td> <td class="label"><label for='category'>{{i18n "chat_integration.edit_rule_modal.category"}}</label></td>
<td> <td>
@ -52,6 +64,20 @@
<td></td> <td></td>
<td><label>{{i18n 'chat_integration.edit_rule_modal.instructions.category'}}</label></td> <td><label>{{i18n 'chat_integration.edit_rule_modal.instructions.category'}}</label></td>
</tr> </tr>
{{else}}
<tr class="input">
<td class="label"><label for='group'>{{i18n "chat_integration.edit_rule_modal.group"}}</label></td>
<td>
{{combo-box content=model.groups valueAttribute="id" value=model.rule.group_id none="chat_integration.choose_group"}}
</td>
</tr>
<tr class="instructions">
<td></td>
<td><label>{{i18n 'chat_integration.edit_rule_modal.instructions.group'}}</label></td>
</tr>
{{/if}}
{{#if siteSettings.tagging_enabled}} {{#if siteSettings.tagging_enabled}}
<tr class="input"> <tr class="input">

View File

@ -22,6 +22,10 @@ en:
close: "Close" close: "Close"
error: "An unknown error occured while sending the message. Check the site logs for more information." error: "An unknown error occured while sending the message. Check the site logs for more information."
success: "Message sent successfully" success: "Message sent successfully"
type:
normal: Normal
group_message: Group Message
group_mention: Group Mention
filter: filter:
mute: 'Mute' mute: 'Mute'
follow: 'First post only' follow: 'First post only'
@ -45,13 +49,16 @@ en:
save: Save Rule save: Save Rule
cancel: Cancel cancel: Cancel
provider: Provider provider: Provider
type: Type
channel: Channel channel: Channel
filter: Filter filter: Filter
category: Category category: Category
group: Group
tags: Tags tags: Tags
instructions: instructions:
filter: "Notification level. Mute overrides other matching rules." filter: "Notification level. Mute overrides other matching rules."
category: "This rule will only apply to topics in the specified category." category: "This rule will only apply to topics in the specified category."
group: "This rule will apply to posts referencing this group"
tags: "If specified, this rule will only apply to topics which have at least one of these tags." tags: "If specified, this rule will only apply to topics which have at least one of these tags."
provider: provider:

View File

@ -105,7 +105,7 @@ describe 'Chat Controller', type: :request do
"provider" => 'dummy', "provider" => 'dummy',
"data" => {}, "data" => {},
"error_key" => nil, "error_key" => nil,
"rules" => [{"id" => rule.id, "filter" => "follow", "channel_id" => channel.id, "category_id" => category.id, "tags" => [tag.name]}] "rules" => [{"id" => rule.id, "type" => 'normal', "group_name" => nil, "group_id" => nil, "filter" => "follow", "channel_id" => channel.id, "category_id" => category.id, "tags" => [tag.name]}]
) )
end end

View File

@ -116,8 +116,8 @@ RSpec.describe DiscourseChat::Rule do
it 'can be filtered by group' do it 'can be filtered by group' do
group1 = Fabricate(:group) group1 = Fabricate(:group)
group2 = Fabricate(:group) group2 = Fabricate(:group)
rule2 = DiscourseChat::Rule.create(channel:channel, group_id: group1.id) rule2 = DiscourseChat::Rule.create!(channel:channel, type:'group_message', group_id: group1.id)
rule3 = DiscourseChat::Rule.create(channel:channel, group_id: group2.id) rule3 = DiscourseChat::Rule.create!(channel:channel, type:'group_message', group_id: group2.id)
expect(DiscourseChat::Rule.all.length).to eq(3) expect(DiscourseChat::Rule.all.length).to eq(3)
@ -161,12 +161,14 @@ RSpec.describe DiscourseChat::Rule do
rule.group_id = group.id rule.group_id = group.id
expect(rule.valid?).to eq(false) expect(rule.valid?).to eq(false)
rule.category_id = nil rule.category_id = nil
rule.type = "group_message"
expect(rule.valid?).to eq(true) expect(rule.valid?).to eq(true)
end end
it 'validates group correctly' do it 'validates group correctly' do
rule.category_id = nil rule.category_id = nil
rule.group_id = group.id rule.group_id = group.id
rule.type = "group_message"
expect(rule.valid?).to eq(true) expect(rule.valid?).to eq(true)
rule.group_id = -99 rule.group_id = -99
expect(rule.valid?).to eq(false) expect(rule.valid?).to eq(false)

View File

@ -110,7 +110,7 @@ RSpec.describe DiscourseChat::Manager do
it "should work for group pms" do it "should work for group pms" do
DiscourseChat::Rule.create!(channel: chan1, filter: 'watch' ) # Wildcard watch DiscourseChat::Rule.create!(channel: chan1, filter: 'watch' ) # Wildcard watch
DiscourseChat::Rule.create!(channel: chan2, filter: 'watch', group_id: group.id ) # Group watch DiscourseChat::Rule.create!(channel: chan2, type: 'group_message', filter: 'watch', group_id: group.id ) # Group watch
private_post = Fabricate(:private_message_post) private_post = Fabricate(:private_message_post)
private_post.topic.invite_group(Fabricate(:user), group) private_post.topic.invite_group(Fabricate(:user), group)
@ -122,8 +122,8 @@ RSpec.describe DiscourseChat::Manager do
it "should work for pms with multiple groups" do it "should work for pms with multiple groups" do
group2 = Fabricate(:group) group2 = Fabricate(:group)
DiscourseChat::Rule.create!(channel: chan1, filter: 'watch', group_id: group.id ) DiscourseChat::Rule.create!(channel: chan1, type: 'group_message', filter: 'watch', group_id: group.id )
DiscourseChat::Rule.create!(channel: chan2, filter: 'watch', group_id: group2.id ) DiscourseChat::Rule.create!(channel: chan2, type: 'group_message', filter: 'watch', group_id: group2.id )
private_post = Fabricate(:private_message_post) private_post = Fabricate(:private_message_post)
private_post.topic.invite_group(Fabricate(:user), group) private_post.topic.invite_group(Fabricate(:user), group)