From 588b4701a741efb62cb983b202d4f82d45826388 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Wed, 20 Apr 2016 00:18:34 +0200 Subject: [PATCH 01/11] Fixed not shown online notifications in safari. --- app/assets/stylesheets/zammad.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/stylesheets/zammad.scss b/app/assets/stylesheets/zammad.scss index ac4904416..4f8afe681 100644 --- a/app/assets/stylesheets/zammad.scss +++ b/app/assets/stylesheets/zammad.scss @@ -3735,7 +3735,6 @@ footer { } .popover-content { - flex: 1; padding-left: 0; padding-right: 0; margin-bottom: 0; From fcdf62cd137fc721a3f380de2f60a15fb6d2f46d Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Thu, 21 Apr 2016 09:36:25 +0200 Subject: [PATCH 02/11] Added sipgate integration. --- .../_integration/sipgate_io.coffee | 156 +++++++++++++++ .../app/controllers/widget/http_log.coffee | 13 +- .../app/views/integration/sipgate.jst.eco | 78 ++++++++ .../app/views/widget/http_log.jst.eco | 2 +- app/controllers/application_controller.rb | 61 +++++- .../integration/sipgate_controller.rb | 115 +++++++++++ config/routes/integration_sipgate.rb | 6 + .../20160421000001_add_sipgate_integration.rb | 37 ++++ db/seeds.rb | 33 ++++ test/integration/sipgate_controller_test.rb | 184 ++++++++++++++++++ 10 files changed, 677 insertions(+), 8 deletions(-) create mode 100644 app/assets/javascripts/app/controllers/_integration/sipgate_io.coffee create mode 100644 app/assets/javascripts/app/views/integration/sipgate.jst.eco create mode 100644 app/controllers/integration/sipgate_controller.rb create mode 100644 config/routes/integration_sipgate.rb create mode 100644 db/migrate/20160421000001_add_sipgate_integration.rb create mode 100644 test/integration/sipgate_controller_test.rb diff --git a/app/assets/javascripts/app/controllers/_integration/sipgate_io.coffee b/app/assets/javascripts/app/controllers/_integration/sipgate_io.coffee new file mode 100644 index 000000000..7baecf237 --- /dev/null +++ b/app/assets/javascripts/app/controllers/_integration/sipgate_io.coffee @@ -0,0 +1,156 @@ +class Index extends App.ControllerIntegrationBase + featureIntegration: 'sipgate_integration' + featureName: 'sipgate.io' + featureConfig: 'sipgate_config' + description: [ + ['This service shows you contacts of incoming calls and a caller list in realtime.'] + ['Also caller id of outbound calls can be changed.'] + ] + + render: => + super + new Form( + el: @$('.js-form') + ) + + new App.HttpLog( + el: @$('.js-log') + facility: 'sipgate.io' + ) + +class Form extends App.Controller + events: + 'submit form': 'update' + 'click .js-inboundBlockCallerId .js-add': 'addInboundBlockCallerId' + 'click .js-outboundRouting .js-add': 'addOutboundRouting' + 'click .js-inboundBlockCallerId .js-remove': 'removeInboundBlockCallerId' + 'click .js-outboundRouting .js-remove': 'removeOutboundRouting' + + constructor: -> + super + + # check authentication + return if !@authenticate() + + @subscribeId = App.Setting.subscribe(@render, initFetch: true, clear: false) + + currentConfig: -> + config = App.Setting.get('sipgate_config') + if !config.outbound + config.outbound = {} + if !config.outbound.routing_table + config.outbound.routing_table = [] + if !config.inbound + config.inbound = {} + if !config.inbound.block_caller_ids + config.inbound.block_caller_ids = [] + config + + setConfig: (value) -> + App.Setting.set('sipgate_config', value) + + render: => + @config = @currentConfig() + + @html App.view('integration/sipgate')( + config: @config + ) + + updateCurrentConfig: => + config = @config + cleanupInput = @cleanupInput + + # default caller_id + default_caller_id = @$('input[name=default_caller_id]').val() + config.outbound.default_caller_id = cleanupInput(default_caller_id) + + # routing table + config.outbound.routing_table = [] + @$('.js-outboundRouting .js-row').each(-> + dest = cleanupInput($(@).find('input[name="dest"]').val()) + caller_id = cleanupInput($(@).find('input[name="caller_id"]').val()) + note = $(@).find('input[name="note"]').val() + config.outbound.routing_table.push { + dest: dest + caller_id: caller_id + note: note + } + ) + + # blocked caller ids + config.inbound.block_caller_ids = [] + @$('.js-inboundBlockCallerId .js-row').each(-> + caller_id = $(@).find('input[name="caller_id"]').val() + note = $(@).find('input[name="note"]').val() + config.inbound.block_caller_ids.push { + caller_id: cleanupInput(caller_id) + note: note + } + ) + + @config = config + + update: (e) => + e.preventDefault() + @updateCurrentConfig() + @setConfig(@config) + + cleanupInput: (value) -> + return value if !value + value.replace(/\s/g, '').trim() + + addInboundBlockCallerId: (e) => + e.preventDefault() + @updateCurrentConfig() + element = $(e.currentTarget).closest('tr') + caller_id = element.find('input[name="caller_id"]').val() + note = element.find('input[name="note"]').val() + @config.inbound.block_caller_ids.push { + caller_id: @cleanupInput(caller_id) + note: note + } + @setConfig(@config) + @render() + + addOutboundRouting: (e) => + e.preventDefault() + @updateCurrentConfig() + element = $(e.currentTarget).closest('tr') + dest = @cleanupInput(element.find('input[name="dest"]').val()) + caller_id = @cleanupInput(element.find('input[name="caller_id"]').val()) + note = element.find('input[name="note"]').val() + @config.outbound.routing_table.push { + dest: dest + caller_id: caller_id + note: note + } + @setConfig(@config) + @render() + + removeInboundBlockCallerId: (e) => + e.preventDefault() + @updateCurrentConfig() + element = $(e.currentTarget).closest('tr') + element.remove() + + removeOutboundRouting: (e) => + e.preventDefault() + @updateCurrentConfig() + element = $(e.currentTarget).closest('tr') + element.remove() + +class State + @current: -> + App.Setting.get('sipgate_integration') + +App.Config.set( + 'IntegrationSipgate' + { + name: 'sipgate.io' + target: '#system/integration/sipgate' + description: 'VoIP services provide.' + controller: Index + state: State + } + 'NavBarIntegrations' +) diff --git a/app/assets/javascripts/app/controllers/widget/http_log.coffee b/app/assets/javascripts/app/controllers/widget/http_log.coffee index 01951edad..4a625b3bd 100644 --- a/app/assets/javascripts/app/controllers/widget/http_log.coffee +++ b/app/assets/javascripts/app/controllers/widget/http_log.coffee @@ -9,22 +9,23 @@ class App.HttpLog extends App.Controller fetch: => @ajax( - id: 'http_logs' - type: 'GET' - url: "#{@apiPath}/http_logs/#{@facility}" + id: 'http_logs' + type: 'GET' + url: "#{@apiPath}/http_logs/#{@facility}" data: limit: @limit || 50 processData: true success: (data) => - @records = data - @render() + if !@records[0] || (data[0] && @records[0] && data[0].updated_at isnt @records[0].updated_at) + @records = data + @render() + @delay(@fetch, 20000) ) render: => @html App.view('widget/http_log')( records: @records ) - #@delay(message, 2000) show: (e) => e.preventDefault() diff --git a/app/assets/javascripts/app/views/integration/sipgate.jst.eco b/app/assets/javascripts/app/views/integration/sipgate.jst.eco new file mode 100644 index 000000000..a21d2c380 --- /dev/null +++ b/app/assets/javascripts/app/views/integration/sipgate.jst.eco @@ -0,0 +1,78 @@ +
+ +

<%- @T('Inbound') %>

+ +

<%- @T('Blocked caller ids based on sender caller id.') %> + +

+ + + + + +<% for row in @config.inbound.block_caller_ids: %> + + + +
<%- @T('Caller id to block') %> + <%- @T('Note') %> + <%- @T('Action') %> +
+ +
<%- @Icon('trash') %> <%- @T('Remove') %>
+<% end %> +
+ +
<%- @Icon('plus-small') %> <%- @T('Add') %>
+
+
+ +

<%- @T('Outbound') %>

+ +

<%- @T('Set caller id of outbound calls based on destination caller id.') %> + +

+ + + + + +<% for row in @config.outbound.routing_table: %> + + + +
<%- @T('Destination caller id') %> + <%- @T('Set outbound caller id') %> + <%- @T('Note') %> + <%- @T('Action') %> +
+ + +
<%- @Icon('trash') %> <%- @T('Remove') %>
+<% end %> +
+ + +
<%- @Icon('plus-small') %> <%- @T('Add') %>
+
+
+ +

<%- @T('Default caller id.') %> + +

+ + + + + + + +
<%- @T('Default caller id') %> + <%- @T('Note') %> +
+ <%- @T('Default caller id for outbound calls.') %> +
+
+ + +
\ No newline at end of file diff --git a/app/assets/javascripts/app/views/widget/http_log.jst.eco b/app/assets/javascripts/app/views/widget/http_log.jst.eco index 4f3790b12..349bab12e 100644 --- a/app/assets/javascripts/app/views/widget/http_log.jst.eco +++ b/app/assets/javascripts/app/views/widget/http_log.jst.eco @@ -1,6 +1,6 @@
-<%- @T('Recent logs') %> +

<%- @T('Recent logs') %>

diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 51c705347..27dfe7ccd 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -6,6 +6,7 @@ class ApplicationController < ActionController::Base helper_method :current_user, :authentication_check, :config_frontend, + :http_log_config, :role?, :model_create_render, :model_update_render, @@ -18,7 +19,7 @@ class ApplicationController < ActionController::Base before_action :cors_preflight_check after_action :user_device_update, :set_access_control_headers - after_action :trigger_events + after_action :trigger_events, :http_log # For all responses in this controller, return the CORS access control headers. def set_access_control_headers @@ -47,6 +48,10 @@ class ApplicationController < ActionController::Base false end + def http_log_config(config) + @http_log_support = config + end + private # execute events @@ -98,6 +103,60 @@ class ApplicationController < ActionController::Base session[:user_agent] = request.env['HTTP_USER_AGENT'] end + # log http access + def http_log + return if !@http_log_support + + # request + request_data = { + content: '', + content_type: request.headers['Content-Type'], + content_encoding: request.headers['Content-Encoding'], + source: request.headers['User-Agent'] || request.headers['Server'], + } + request.headers.each {|key, value| + next if key[0, 5] != 'HTTP_' + request_data[:content] += if key == 'HTTP_COOKIE' + "#{key}: xxxxx\n" + else + "#{key}: #{value}\n" + end + } + body = request.body.read + if body + request_data[:content] += "\n" + body + end + request_data[:content] = request_data[:content].slice(0, 8000) + + # response + response_data = { + code: response.status = response.code, + content: '', + content_type: nil, + content_encoding: nil, + source: nil, + } + response.headers.each {|key, value| + response_data[:content] += "#{key}: #{value}\n" + } + body = response.body + if body + response_data[:content] += "\n" + body + end + response_data[:content] = response_data[:content].slice(0, 8000) + record = { + direction: 'in', + facility: @http_log_support[:facility], + url: url_for(only_path: false, overwrite_params: {}), + status: response.status, + ip: request.remote_ip, + request: request_data, + response: response_data, + method: request.method, + } + HttpLog.create(record) + end + # user device recent action update def user_device_update diff --git a/app/controllers/integration/sipgate_controller.rb b/app/controllers/integration/sipgate_controller.rb new file mode 100644 index 000000000..721240765 --- /dev/null +++ b/app/controllers/integration/sipgate_controller.rb @@ -0,0 +1,115 @@ +require 'builder' + +class Integration::SipgateController < ApplicationController + before_action { http_log_config facility: 'sipgate.io' } + + # notify about inbound call / block inbound call + def in + return if feature_disabled + + config = Setting.get('sipgate_config') + config_inbound = config[:inbound] + block_caller_ids = config_inbound[:block_caller_ids] + + if params['event'] == 'newCall' + + # check if call need to be blocked + block_caller_ids.each {|item| + next unless item[:caller_id] == params['from'] + xml = Builder::XmlMarkup.new(indent: 2) + xml.instruct! + content = xml.Response('onHangup' => in_url, 'onAnswer' => in_url) do + xml.Reject('reason' => 'busy') + end + + send_data content, type: 'application/xml; charset=UTF-8;' + + params['Reject'] = 'busy' + Sessions.broadcast( + event: 'sipgate.io', + data: params + ) + return true + } + end + + xml = Builder::XmlMarkup.new(indent: 2) + xml.instruct! + content = xml.Response('onHangup' => in_url, 'onAnswer' => in_url) + + send_data content, type: 'application/xml; charset=UTF-8;' + + # search for caller + Sessions.broadcast( + event: 'sipgate.io', + data: params + ) + + end + + # set caller id of outbound call + def out + return if feature_disabled + + config = Setting.get('sipgate_config') + config_outbound = config[:outbound][:routing_table] + default_caller_id = config[:outbound][:default_caller_id] + + xml = Builder::XmlMarkup.new(indent: 2) + xml.instruct! + + # set callerId + content = nil + to = params[:to] + if to + config_outbound.each {|row| + dest = row[:dest].gsub(/\*/, '.+?') + next if to !~ /^#{dest}$/ + content = xml.Response('onHangup' => in_url, 'onAnswer' => in_url) do + xml.Dial(callerId: row[:caller_id]) { xml.Number(params[:to]) } + end + break + } + if !content && default_caller_id + content = xml.Response('onHangup' => in_url, 'onAnswer' => in_url) do + xml.Dial(callerId: default_caller_id) { xml.Number(params[:to]) } + end + end + else + content = xml.Response('onHangup' => in_url, 'onAnswer' => in_url) + end + + send_data content, + type: 'application/xml; charset=UTF-8;' + + # notify about outbound call + Sessions.broadcast( + event: 'sipgate.io:out', + data: params + ) + end + + private + + def feature_disabled + if !Setting.get('sipgate_integration') + render( + json: {}, + status: :unauthorized + ) + return true + end + false + end + + def base_url + http_type = Setting.get('http_type') + fqdn = Setting.get('fqdn') + + "#{http_type}://#{fqdn}/api/v1/sipgate" + end + + def in_url + "#{base_url}/in" + end +end diff --git a/config/routes/integration_sipgate.rb b/config/routes/integration_sipgate.rb new file mode 100644 index 000000000..04d4d8f91 --- /dev/null +++ b/config/routes/integration_sipgate.rb @@ -0,0 +1,6 @@ +Zammad::Application.routes.draw do + + match '/api/v1/sipgate/in', to: 'integration/sipgate#in', via: :post + match '/api/v1/sipgate/out', to: 'integration/sipgate#out', via: :post + +end diff --git a/db/migrate/20160421000001_add_sipgate_integration.rb b/db/migrate/20160421000001_add_sipgate_integration.rb new file mode 100644 index 000000000..2a046670a --- /dev/null +++ b/db/migrate/20160421000001_add_sipgate_integration.rb @@ -0,0 +1,37 @@ +class AddSipgateIntegration < ActiveRecord::Migration + def up + Setting.create_if_not_exists( + title: 'sipgate.io integration', + name: 'sipgate_integration', + area: 'Integration::Switch', + description: 'Define if sipgate.io (http://www.sipgate.io) is enabled or not.', + options: { + form: [ + { + display: '', + null: true, + name: 'sipgate_integration', + tag: 'boolean', + options: { + true => 'yes', + false => 'no', + }, + }, + ], + }, + state: false, + preferences: { prio: 1 }, + frontend: false + ) + Setting.create_if_not_exists( + title: 'sipgate.io config', + name: 'sipgate_config', + area: 'Integration::Sipgate', + description: 'Define the sipgate.io config.', + options: {}, + state: {}, + frontend: false, + preferences: { prio: 2 }, + ) + end +end diff --git a/db/seeds.rb b/db/seeds.rb index b0b2556e9..9f29ba6d5 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1822,6 +1822,39 @@ Setting.create_if_not_exists( frontend: false, preferences: { prio: 2 }, ) +Setting.create_if_not_exists( + title: 'sipgate.io integration', + name: 'sipgate_integration', + area: 'Integration::Switch', + description: 'Define if sipgate.io (http://www.sipgate.io) is enabled or not.', + options: { + form: [ + { + display: '', + null: true, + name: 'sipgate_integration', + tag: 'boolean', + options: { + true => 'yes', + false => 'no', + }, + }, + ], + }, + state: false, + preferences: { prio: 1 }, + frontend: false +) +Setting.create_if_not_exists( + title: 'sipgate.io config', + name: 'sipgate_config', + area: 'Integration::Sipgate', + description: 'Define the sipgate.io config.', + options: {}, + state: {}, + frontend: false, + preferences: { prio: 2 }, +) signature = Signature.create_if_not_exists( id: 1, diff --git a/test/integration/sipgate_controller_test.rb b/test/integration/sipgate_controller_test.rb new file mode 100644 index 000000000..cb102e066 --- /dev/null +++ b/test/integration/sipgate_controller_test.rb @@ -0,0 +1,184 @@ +# encoding: utf-8 +require 'test_helper' +require 'rexml/document' + +class SipgateControllerTest < ActionDispatch::IntegrationTest + setup do + + Setting.create_or_update( + title: 'sipgate.io integration', + name: 'sipgate_integration', + area: 'Integration::Switch', + description: 'Define if sipgate.io (http://www.sipgate.io) is enabled or not.', + options: { + form: [ + { + display: '', + null: true, + name: 'sipgate_integration', + tag: 'boolean', + options: { + true => 'yes', + false => 'no', + }, + }, + ], + }, + state: true, + preferences: { prio: 1 }, + frontend: false + ) + Setting.create_or_update( + title: 'sipgate.io config', + name: 'sipgate_config', + area: 'Integration::Sipgate', + description: 'Define the sipgate.io config.', + options: {}, + state: { + outbound: { + routing_table: [ + { + dest: '41*', + caller_id: '41715880339000', + }, + { + dest: '491714000000', + caller_id: '41715880339000', + }, + ], + default_caller_id: '4930777000000', + }, + inbound: { + block_caller_ids: [ + { + caller_id: '491715000000', + note: 'some note', + } + ], + notify_user_ids: { + 2 => true, + 4 => false, + }, + } + }, + frontend: false, + preferences: { prio: 2 }, + ) + + end + + test 'basic call' do + + # inbound - I + params = 'event=newCall&direction=in&from=4912347114711&to=4930600000000&callId=4991155921769858278&user%5B%5D=user+1&user%5B%5D=user+2' + post '/api/v1/sipgate/in', params + assert_response(200) + on_hangup = nil + on_answer = nil + content = @response.body + response = REXML::Document.new(content) + response.elements.each('Response') do |element| + on_hangup = element.attributes['onHangup'] + on_answer = element.attributes['onAnswer'] + end + assert_equal('http://zammad.example.com/api/v1/sipgate/in', on_hangup) + assert_equal('http://zammad.example.com/api/v1/sipgate/in', on_answer) + + # inbound - II - block caller + params = 'event=newCall&direction=in&from=491715000000&to=4930600000000&callId=4991155921769858278&user%5B%5D=user+1&user%5B%5D=user+2' + post '/api/v1/sipgate/in', params + assert_response(200) + on_hangup = nil + on_answer = nil + content = @response.body + response = REXML::Document.new(content) + response.elements.each('Response') do |element| + on_hangup = element.attributes['onHangup'] + on_answer = element.attributes['onAnswer'] + end + assert_equal('http://zammad.example.com/api/v1/sipgate/in', on_hangup) + assert_equal('http://zammad.example.com/api/v1/sipgate/in', on_answer) + reason = nil + response.elements.each('Response/Reject') do |element| + reason = element.attributes['reason'] + end + assert_equal('busy', reason) + + # outbound - I - set default_caller_id + params = 'event=newCall&direction=out&from=4930600000000&to=4912347114711&callId=8621106404543334274&user%5B%5D=user+1' + post '/api/v1/sipgate/out', params + assert_response(200) + on_hangup = nil + on_answer = nil + caller_id = nil + number_to_dail = nil + content = @response.body + response = REXML::Document.new(content) + response.elements.each('Response') do |element| + on_hangup = element.attributes['onHangup'] + on_answer = element.attributes['onAnswer'] + end + response.elements.each('Response/Dial') do |element| + caller_id = element.attributes['callerId'] + end + response.elements.each('Response/Dial/Number') do |element| + number_to_dail = element.text + end + assert_equal('4930777000000', caller_id) + assert_equal('4912347114711', number_to_dail) + assert_equal('http://zammad.example.com/api/v1/sipgate/in', on_hangup) + assert_equal('http://zammad.example.com/api/v1/sipgate/in', on_answer) + + # outbound - II - set caller_id based on routing_table by explicite number + params = 'event=newCall&direction=out&from=4930600000000&to=491714000000&callId=8621106404543334274&user%5B%5D=user+1' + post '/api/v1/sipgate/out', params + assert_response(200) + on_hangup = nil + on_answer = nil + caller_id = nil + number_to_dail = nil + content = @response.body + response = REXML::Document.new(content) + response.elements.each('Response') do |element| + on_hangup = element.attributes['onHangup'] + on_answer = element.attributes['onAnswer'] + end + response.elements.each('Response/Dial') do |element| + caller_id = element.attributes['callerId'] + end + response.elements.each('Response/Dial/Number') do |element| + number_to_dail = element.text + end + assert_equal('41715880339000', caller_id) + assert_equal('491714000000', number_to_dail) + assert_equal('http://zammad.example.com/api/v1/sipgate/in', on_hangup) + assert_equal('http://zammad.example.com/api/v1/sipgate/in', on_answer) + + # outbound - III - set caller_id based on routing_table by 41* + params = 'event=newCall&direction=out&from=4930600000000&to=4147110000000&callId=8621106404543334274&user%5B%5D=user+1' + post '/api/v1/sipgate/out', params + assert_response(200) + on_hangup = nil + on_answer = nil + caller_id = nil + number_to_dail = nil + content = @response.body + response = REXML::Document.new(content) + response.elements.each('Response') do |element| + on_hangup = element.attributes['onHangup'] + on_answer = element.attributes['onAnswer'] + end + response.elements.each('Response/Dial') do |element| + caller_id = element.attributes['callerId'] + end + response.elements.each('Response/Dial/Number') do |element| + number_to_dail = element.text + end + assert_equal('41715880339000', caller_id) + assert_equal('4147110000000', number_to_dail) + assert_equal('http://zammad.example.com/api/v1/sipgate/in', on_hangup) + assert_equal('http://zammad.example.com/api/v1/sipgate/in', on_answer) + + end + +end From 30b05bd991fb449fadd538804b4b3e45e51e83fc Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Thu, 21 Apr 2016 15:43:13 +0200 Subject: [PATCH 03/11] Improved error handling. --- .../integration/sipgate_controller.rb | 33 ++++++++++++------- test/integration/sipgate_controller_test.rb | 13 ++++++++ 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/app/controllers/integration/sipgate_controller.rb b/app/controllers/integration/sipgate_controller.rb index 721240765..8006b8635 100644 --- a/app/controllers/integration/sipgate_controller.rb +++ b/app/controllers/integration/sipgate_controller.rb @@ -5,11 +5,11 @@ class Integration::SipgateController < ApplicationController # notify about inbound call / block inbound call def in - return if feature_disabled + return if !configured? config = Setting.get('sipgate_config') - config_inbound = config[:inbound] - block_caller_ids = config_inbound[:block_caller_ids] + config_inbound = config[:inbound] || {} + block_caller_ids = config_inbound[:block_caller_ids] || [] if params['event'] == 'newCall' @@ -49,7 +49,7 @@ class Integration::SipgateController < ApplicationController # set caller id of outbound call def out - return if feature_disabled + return if !configured? config = Setting.get('sipgate_config') config_outbound = config[:outbound][:routing_table] @@ -91,15 +91,26 @@ class Integration::SipgateController < ApplicationController private - def feature_disabled + def configured? if !Setting.get('sipgate_integration') - render( - json: {}, - status: :unauthorized - ) - return true + xml_error('Feature is disable, please contact your admin to enable it!') + return false end - false + config = Setting.get('sipgate_config') + if !config || !config[:inbound] || !config[:outbound] + xml_error('Feature not configured, please contact your admin!') + return false + end + true + end + + def xml_error(error) + xml = Builder::XmlMarkup.new(indent: 2) + xml.instruct! + content = xml.Response() do + xml.Error(error) + end + send_data content, type: 'application/xml; charset=UTF-8;', status: '500' end def base_url diff --git a/test/integration/sipgate_controller_test.rb b/test/integration/sipgate_controller_test.rb index cb102e066..fa9432433 100644 --- a/test/integration/sipgate_controller_test.rb +++ b/test/integration/sipgate_controller_test.rb @@ -179,6 +179,19 @@ class SipgateControllerTest < ActionDispatch::IntegrationTest assert_equal('http://zammad.example.com/api/v1/sipgate/in', on_hangup) assert_equal('http://zammad.example.com/api/v1/sipgate/in', on_answer) + # no config + Setting.set('sipgate_config', {}) + params = 'event=newCall&direction=in&from=4912347114711&to=4930600000000&callId=4991155921769858278&user%5B%5D=user+1&user%5B%5D=user+2' + post '/api/v1/sipgate/in', params + assert_response(500) + error = nil + content = @response.body + response = REXML::Document.new(content) + response.elements.each('Response/Error') do |element| + error = element.text + end + assert_equal('Feature not configured, please contact your admin!', error) + end end From 3a9f3ba5f942c60a5ba409cd64d82c39aa6783fe Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Thu, 21 Apr 2016 17:11:10 +0200 Subject: [PATCH 04/11] Take also . in url for http log facility. --- app/controllers/integration/sipgate_controller.rb | 3 +-- config/routes/http_log.rb | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/controllers/integration/sipgate_controller.rb b/app/controllers/integration/sipgate_controller.rb index 8006b8635..450317584 100644 --- a/app/controllers/integration/sipgate_controller.rb +++ b/app/controllers/integration/sipgate_controller.rb @@ -79,8 +79,7 @@ class Integration::SipgateController < ApplicationController content = xml.Response('onHangup' => in_url, 'onAnswer' => in_url) end - send_data content, - type: 'application/xml; charset=UTF-8;' + send_data content, type: 'application/xml; charset=UTF-8;' # notify about outbound call Sessions.broadcast( diff --git a/config/routes/http_log.rb b/config/routes/http_log.rb index 2dad26384..0258934e0 100644 --- a/config/routes/http_log.rb +++ b/config/routes/http_log.rb @@ -2,7 +2,7 @@ Zammad::Application.routes.draw do api_path = Rails.configuration.api_path match api_path + '/http_logs', to: 'http_logs#index', via: :get - match api_path + '/http_logs/:facility', to: 'http_logs#index', via: :get + match api_path + '/http_logs/:facility', to: 'http_logs#index', via: :get, constraints: { facility: /.*/ } match api_path + '/http_logs', to: 'http_logs#create', via: :post end From 3cf3bc919fdc1e4e816c832c79539d059071f082 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Thu, 21 Apr 2016 17:11:55 +0200 Subject: [PATCH 05/11] Updated gems. --- Gemfile.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index f568c61ef..7141ed216 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -45,7 +45,7 @@ GEM ast (2.2.0) autoprefixer-rails (6.3.6) execjs - biz (1.5.1) + biz (1.5.2) clavius (~> 1.0) tzinfo browser (2.0.3) @@ -129,7 +129,7 @@ GEM inflection (1.0.0) json (1.8.3) jwt (1.5.1) - koala (2.2.0) + koala (2.3.0) addressable faraday multi_json @@ -199,7 +199,7 @@ GEM coderay (~> 1.1.0) method_source (~> 0.8.1) slop (~> 3.4) - puma (3.3.0) + puma (3.4.0) rack (1.6.4) rack-livereload (0.3.16) rack @@ -269,8 +269,8 @@ GEM simplecov (>= 0.4.1) slack-notifier (1.5.1) slop (3.6.0) - spring (1.6.4) - sprockets (3.5.2) + spring (1.7.1) + sprockets (3.6.0) concurrent-ruby (~> 1.0) rack (> 1, < 3) sprockets-rails (3.0.4) From 2d93b435bada9251a0cfb62e758f39bb33e836e6 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Thu, 21 Apr 2016 20:49:30 +0200 Subject: [PATCH 06/11] Reset filters to empty for postmaster filter. --- app/models/channel/email_parser.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/channel/email_parser.rb b/app/models/channel/email_parser.rb index 797b06cb7..3ed371242 100644 --- a/app/models/channel/email_parser.rb +++ b/app/models/channel/email_parser.rb @@ -490,6 +490,7 @@ retrns Observer::Transaction.commit # run postmaster post filter + filters = {} Setting.where(area: 'Postmaster::PostFilter').order(:name).each {|setting| filters[setting.name] = Kernel.const_get(Setting.get(setting.name)) } From e56ed8eecadcff392ca86be85546b0aa4e59ff07 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Fri, 22 Apr 2016 08:55:10 +0200 Subject: [PATCH 07/11] Moved signature detection to transaction module. --- .../ticket/article/signature_detection.rb | 33 ---- .../signature_detection/background_job.rb | 9 - app/models/observer/transaction.rb | 161 +++++++++--------- app/models/transaction/notification.rb | 10 +- app/models/transaction/signature_detection.rb | 58 +++++++ app/models/transaction/slack.rb | 10 +- config/application.rb | 1 - ...160422000001_update_signature_detection.rb | 13 ++ db/seeds.rb | 9 + lib/signature_detection.rb | 5 +- test/unit/email_signatur_detection_test.rb | 5 +- test/unit/ticket_notification_test.rb | 20 +-- 12 files changed, 192 insertions(+), 142 deletions(-) delete mode 100644 app/models/observer/ticket/article/signature_detection.rb delete mode 100644 app/models/observer/ticket/article/signature_detection/background_job.rb create mode 100644 app/models/transaction/signature_detection.rb create mode 100644 db/migrate/20160422000001_update_signature_detection.rb diff --git a/app/models/observer/ticket/article/signature_detection.rb b/app/models/observer/ticket/article/signature_detection.rb deleted file mode 100644 index a3b520215..000000000 --- a/app/models/observer/ticket/article/signature_detection.rb +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/ - -require 'signature_detection' - -class Observer::Ticket::Article::SignatureDetection < ActiveRecord::Observer - observe 'ticket::_article' - - def before_create(record) - - # return if we run import mode - return if Setting.get('import_mode') - - # if sender is not customer, do not change anything - sender = Ticket::Article::Sender.lookup(id: record.sender_id) - return if !sender - return if sender['name'] != 'Customer' - - # set email attributes - type = Ticket::Article::Type.lookup(id: record.type_id) - return if type['name'] != 'email' - - # add queue job to update current signature of user id - Delayed::Job.enqueue(Observer::Ticket::Article::SignatureDetection::BackgroundJob.new(record.created_by_id)) - - # user - user = User.lookup(id: record.created_by_id) - return if !user - return if !user.preferences - return if !user.preferences[:signature_detection] - - record.preferences[:signature_detection] = SignatureDetection.find_signature_line(user.preferences[:signature_detection], record.body) - end -end diff --git a/app/models/observer/ticket/article/signature_detection/background_job.rb b/app/models/observer/ticket/article/signature_detection/background_job.rb deleted file mode 100644 index 83eecbf23..000000000 --- a/app/models/observer/ticket/article/signature_detection/background_job.rb +++ /dev/null @@ -1,9 +0,0 @@ -class Observer::Ticket::Article::SignatureDetection::BackgroundJob - def initialize(id) - @user_id = id - end - - def perform - SignatureDetection.rebuild_user(@user_id) - end -end diff --git a/app/models/observer/transaction.rb b/app/models/observer/transaction.rb index 5644a23c8..8f403d38e 100644 --- a/app/models/observer/transaction.rb +++ b/app/models/observer/transaction.rb @@ -1,7 +1,7 @@ # Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/ class Observer::Transaction < ActiveRecord::Observer - observe :ticket, 'ticket::_article' + observe :ticket, 'ticket::_article', :user, :organization def self.commit(params = {}) @@ -28,10 +28,10 @@ class Observer::Transaction < ActiveRecord::Observer # get uniq objects list_objects = get_uniq_changes(list) - list_objects.each {|_id, item| - - # send background job - Delayed::Job.enqueue(Transaction::BackgroundJob.new(item, params)) + list_objects.each {|_object, objects| + objects.each {|_id, item| + Delayed::Job.enqueue(Transaction::BackgroundJob.new(item, params)) + } } end @@ -40,33 +40,37 @@ class Observer::Transaction < ActiveRecord::Observer result = get_uniq_changes(events) result = { - 1 => { - object: 'Ticket', - type: 'create', - ticket_id: 123, - article_id: 123, - }, - 9 => { - object: 'Ticket', - type: 'update', - ticket_id: 123, - changes: { - attribute1: [before, now], - attribute2: [before, now], - } + 'Ticket' => + 1 => { + object: 'Ticket', + type: 'create', + object_id: 123, + article_id: 123, + }, + 9 => { + object: 'Ticket', + type: 'update', + object_id: 123, + changes: { + attribute1: [before, now], + attribute2: [before, now], + }, + }, }, } result = { - 9 => { - object: 'Ticket', - type: 'update', - ticket_id: 123, - article_id: 123, - changes: { - attribute1: [before, now], - attribute2: [before, now], - } + 'Ticket' => + 9 => { + object: 'Ticket', + type: 'update', + object_id: 123, + article_id: 123, + changes: { + attribute1: [before, now], + attribute2: [before, now], + }, + }, }, } @@ -76,57 +80,59 @@ class Observer::Transaction < ActiveRecord::Observer list_objects = {} events.each { |event| - # get current state of objects - if event[:name] == 'Ticket::Article' + # simulate article create as ticket update + article = nil + if event[:object] == 'Ticket::Article' article = Ticket::Article.lookup(id: event[:id]) - - # next if article is already deleted next if !article + next if event[:type] == 'update' - ticket = article.ticket - if !list_objects[ticket.id] - list_objects[ticket.id] = {} + # set new event infos + ticket = Ticket.lookup(id: article.ticket_id) + event[:object] = 'Ticket' + event[:id] = ticket.id + event[:type] = 'update' + event[:changes] = nil + end + + # get current state of objects + object = Kernel.const_get(event[:object]).lookup(id: event[:id]) + + # next if object is already deleted + next if !object + + if !list_objects[event[:object]] + list_objects[event[:object]] = {} + end + if !list_objects[event[:object]][object.id] + list_objects[event[:object]][object.id] = {} + end + store = list_objects[event[:object]][object.id] + store[:object] = event[:object] + store[:object_id] = object.id + + if !store[:type] || store[:type] == 'update' + store[:type] = event[:type] + end + + # merge changes + if event[:changes] + if !store[:changes] + store[:changes] = event[:changes] + else + event[:changes].each {|key, value| + if !store[:changes][key] + store[:changes][key] = value + else + store[:changes][key][1] = value[1] + end + } end - list_objects[ticket.id][:object] = 'Ticket' - list_objects[ticket.id][:article_id] = article.id - list_objects[ticket.id][:ticket_id] = ticket.id + end - if !list_objects[ticket.id][:type] - list_objects[ticket.id][:type] = 'update' - end - - elsif event[:name] == 'Ticket' - ticket = Ticket.lookup(id: event[:id]) - - # next if ticket is already deleted - next if !ticket - - if !list_objects[ticket.id] - list_objects[ticket.id] = {} - end - list_objects[ticket.id][:object] = 'Ticket' - list_objects[ticket.id][:ticket_id] = ticket.id - - if !list_objects[ticket.id][:type] || list_objects[ticket.id][:type] == 'update' - list_objects[ticket.id][:type] = event[:type] - end - - # merge changes - if event[:changes] - if !list_objects[ticket.id][:changes] - list_objects[ticket.id][:changes] = event[:changes] - else - event[:changes].each {|key, value| - if !list_objects[ticket.id][:changes][key] - list_objects[ticket.id][:changes][key] = value - else - list_objects[ticket.id][:changes][key][1] = value[1] - end - } - end - end - else - raise "unknown object for integration #{event[:name]}" + # remember article id if exists + if article + store[:article_id] = article.id end } list_objects @@ -138,7 +144,7 @@ class Observer::Transaction < ActiveRecord::Observer return if Setting.get('import_mode') e = { - name: record.class.name, + object: record.class.name, type: 'create', data: record, id: record.id, @@ -151,9 +157,6 @@ class Observer::Transaction < ActiveRecord::Observer # return if we run import mode return if Setting.get('import_mode') - # ignore updates on articles / we just want send integrations on ticket updates - return if record.class.name == 'Ticket::Article' - # ignore certain attributes real_changes = {} record.changes.each {|key, value| @@ -173,7 +176,7 @@ class Observer::Transaction < ActiveRecord::Observer return if real_changes.empty? e = { - name: record.class.name, + object: record.class.name, type: 'update', data: record, changes: real_changes, diff --git a/app/models/transaction/notification.rb b/app/models/transaction/notification.rb index b28d06b55..5c9cca837 100644 --- a/app/models/transaction/notification.rb +++ b/app/models/transaction/notification.rb @@ -6,7 +6,7 @@ class Transaction::Notification { object: 'Ticket', type: 'update', - ticket_id: 123, + object_id: 123, via_web: true, changes: { 'attribute1' => [before, now], @@ -21,9 +21,15 @@ class Transaction::Notification end def perform + + # return if we run import mode + return if Setting.get('import_mode') + + return if @item[:object] != 'Ticket' + return if @params[:disable_notification] - ticket = Ticket.find(@item[:ticket_id]) + ticket = Ticket.find(@item[:object_id]) if @item[:article_id] article = Ticket::Article.find(@item[:article_id]) end diff --git a/app/models/transaction/signature_detection.rb b/app/models/transaction/signature_detection.rb new file mode 100644 index 000000000..3c14b74e5 --- /dev/null +++ b/app/models/transaction/signature_detection.rb @@ -0,0 +1,58 @@ +# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/ +require 'signature_detection' + +class Transaction::SignatureDetection + +=begin + { + object: 'Ticket', + type: 'update', + object_id: 123, + via_web: true, + changes: { + 'attribute1' => [before, now], + 'attribute2' => [before, now], + } + }, +=end + + def initialize(item, params = {}) + @item = item + @params = params + end + + def perform + + # return if we run import mode + return if Setting.get('import_mode') + + return if @item[:type] != 'create' + return if @item[:object] != 'Ticket' + + ticket = Ticket.lookup(id: @item[:object_id]) + return if !ticket + article = ticket.articles.first + return if !article + + # if sender is not customer, do not change anything + sender = Ticket::Article::Sender.lookup(id: article.sender_id) + return if !sender + return if sender['name'] != 'Customer' + + # set email attributes + type = Ticket::Article::Type.lookup(id: article.type_id) + return if type['name'] != 'email' + + # add queue job to update current signature of user id + SignatureDetection.rebuild_user(article.created_by_id) + + # user + user = User.lookup(id: article.created_by_id) + return if !user + return if !user.preferences + return if !user.preferences[:signature_detection] + article.preferences[:signature_detection] = SignatureDetection.find_signature_line(user.preferences[:signature_detection], article.body) + article.save + end + +end diff --git a/app/models/transaction/slack.rb b/app/models/transaction/slack.rb index 23138b83a..a4c331b00 100644 --- a/app/models/transaction/slack.rb +++ b/app/models/transaction/slack.rb @@ -6,14 +6,14 @@ class Transaction::Slack backend = Transaction::Slack.new( object: 'Ticket', type: 'create', - ticket_id: 1, + object_id: 1, ) backend.perform { object: 'Ticket', type: 'update', - ticket_id: 123, + object_id: 123, via_web: true, changes: { 'attribute1' => [before, now], @@ -27,6 +27,10 @@ backend.perform end def perform + + # return if we run import mode + return if Setting.get('import_mode') + return if @item[:object] != 'Ticket' return if !Setting.get('slack_integration') @@ -34,7 +38,7 @@ backend.perform return if !config return if !config['items'] - ticket = Ticket.find(@item[:ticket_id]) + ticket = Ticket.find(@item[:object_id]) if @item[:article_id] article = Ticket::Article.find(@item[:article_id]) end diff --git a/config/application.rb b/config/application.rb index 4228be027..e783fae9a 100644 --- a/config/application.rb +++ b/config/application.rb @@ -30,7 +30,6 @@ module Zammad 'observer::_ticket::_article::_communicate_email', 'observer::_ticket::_article::_communicate_facebook', 'observer::_ticket::_article::_communicate_twitter', - 'observer::_ticket::_article::_signature_detection', 'observer::_ticket::_reset_new_state', 'observer::_ticket::_escalation_calculation', 'observer::_ticket::_ref_object_touch', diff --git a/db/migrate/20160422000001_update_signature_detection.rb b/db/migrate/20160422000001_update_signature_detection.rb new file mode 100644 index 000000000..33ba97d08 --- /dev/null +++ b/db/migrate/20160422000001_update_signature_detection.rb @@ -0,0 +1,13 @@ +class UpdateSignatureDetection < ActiveRecord::Migration + def up + Setting.create_if_not_exists( + title: 'Define transaction backend.', + name: '1000_signature_detection', + area: 'Transaction::Backend', + description: 'Define the transaction backend to detect customers signature in email.', + options: {}, + state: 'Transaction::SignatureDetection', + frontend: false + ) + end +end diff --git a/db/seeds.rb b/db/seeds.rb index 9f29ba6d5..ae9c440ea 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1778,6 +1778,15 @@ Setting.create_if_not_exists( state: 'Transaction::Notification', frontend: false ) +Setting.create_if_not_exists( + title: 'Define transaction backend.', + name: '1000_signature_detection', + area: 'Transaction::Backend', + description: 'Define the transaction backend to detect customers signature in email.', + options: {}, + state: 'Transaction::SignatureDetection', + frontend: false +) Setting.create_if_not_exists( title: 'Define transaction backend.', name: '6000_slack_webhook', diff --git a/lib/signature_detection.rb b/lib/signature_detection.rb index 2a21dda43..9b4187a9c 100644 --- a/lib/signature_detection.rb +++ b/lib/signature_detection.rb @@ -131,7 +131,6 @@ returns =end def self.by_user_id(user_id) - type = Ticket::Article::Type.lookup(name: 'email') sender = Ticket::Article::Sender.lookup(name: 'Customer') article_bodies = [] @@ -142,7 +141,7 @@ returns article_bodies.push article.body } - find_signature( article_bodies ) + find_signature(article_bodies) end =begin @@ -167,7 +166,7 @@ returns =begin -rebuild signature for user +rebuild signature detection for user SignatureDetection.rebuild_user(user_id) diff --git a/test/unit/email_signatur_detection_test.rb b/test/unit/email_signatur_detection_test.rb index 9d02fa785..c8844760c 100644 --- a/test/unit/email_signatur_detection_test.rb +++ b/test/unit/email_signatur_detection_test.rb @@ -73,6 +73,7 @@ class EmailSignaturDetectionTest < ActiveSupport::TestCase ticket1, article1, user1, mail = Channel::EmailParser.new.process({}, raw_email) assert(ticket1) assert(article1) + Delayed::Worker.new.work_off # process email II file = File.open("#{Rails.root}/test/fixtures/email_signature_detection/client_a_2.txt", 'rb') @@ -80,8 +81,6 @@ class EmailSignaturDetectionTest < ActiveSupport::TestCase ticket2, article2, user2, mail = Channel::EmailParser.new.process({}, raw_email) assert(ticket2) assert(article2) - - # process background jobs (user signature detection & article signature detection) Delayed::Worker.new.work_off # check if user2 has a signature_detection value @@ -94,8 +93,10 @@ class EmailSignaturDetectionTest < ActiveSupport::TestCase ticket3, article3, user3, mail = Channel::EmailParser.new.process({}, raw_email) assert(ticket3) assert(article3) + Delayed::Worker.new.work_off # check if article3 has a signature_detection value + article3 = Ticket::Article.find(article3.id) assert_equal(article3.preferences[:signature_detection], 6) # relbuild all diff --git a/test/unit/ticket_notification_test.rb b/test/unit/ticket_notification_test.rb index 4ae1a26a4..4de9a0d46 100644 --- a/test/unit/ticket_notification_test.rb +++ b/test/unit/ticket_notification_test.rb @@ -904,11 +904,11 @@ class TicketNotificationTest < ActiveSupport::TestCase list = EventBuffer.list('transaction') list_objects = Observer::Transaction.get_uniq_changes(list) - assert_equal('some notification event test 1', list_objects[ticket1.id][:changes]['title'][0]) - assert_equal('some notification event test 1 - #2', list_objects[ticket1.id][:changes]['title'][1]) - assert_not(list_objects[ticket1.id][:changes]['priority']) - assert_equal(2, list_objects[ticket1.id][:changes]['priority_id'][0]) - assert_equal(3, list_objects[ticket1.id][:changes]['priority_id'][1]) + assert_equal('some notification event test 1', list_objects['Ticket'][ticket1.id][:changes]['title'][0]) + assert_equal('some notification event test 1 - #2', list_objects['Ticket'][ticket1.id][:changes]['title'][1]) + assert_not(list_objects['Ticket'][ticket1.id][:changes]['priority']) + assert_equal(2, list_objects['Ticket'][ticket1.id][:changes]['priority_id'][0]) + assert_equal(3, list_objects['Ticket'][ticket1.id][:changes]['priority_id'][1]) # update ticket attributes ticket1.title = "#{ticket1.title} - #3" @@ -918,11 +918,11 @@ class TicketNotificationTest < ActiveSupport::TestCase list = EventBuffer.list('transaction') list_objects = Observer::Transaction.get_uniq_changes(list) - assert_equal('some notification event test 1', list_objects[ticket1.id][:changes]['title'][0]) - assert_equal('some notification event test 1 - #2 - #3', list_objects[ticket1.id][:changes]['title'][1]) - assert_not(list_objects[ticket1.id][:changes]['priority']) - assert_equal(2, list_objects[ticket1.id][:changes]['priority_id'][0]) - assert_equal(1, list_objects[ticket1.id][:changes]['priority_id'][1]) + assert_equal('some notification event test 1', list_objects['Ticket'][ticket1.id][:changes]['title'][0]) + assert_equal('some notification event test 1 - #2 - #3', list_objects['Ticket'][ticket1.id][:changes]['title'][1]) + assert_not(list_objects['Ticket'][ticket1.id][:changes]['priority']) + assert_equal(2, list_objects['Ticket'][ticket1.id][:changes]['priority_id'][0]) + assert_equal(1, list_objects['Ticket'][ticket1.id][:changes]['priority_id'][1]) end From 30acbec315116c841dc2890c1cd87cb8bf0a245b Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Fri, 22 Apr 2016 09:58:28 +0200 Subject: [PATCH 08/11] Moved to ne generic params. --- app/models/ticket.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/ticket.rb b/app/models/ticket.rb index 05530bdf8..30a38888c 100644 --- a/app/models/ticket.rb +++ b/app/models/ticket.rb @@ -183,7 +183,7 @@ returns Transaction::BackgroundJob.run( object: 'Ticket', type: 'reminder_reached', - ticket_id: ticket.id, + object_id: ticket.id, article_id: ticket.articles.last.id, ) @@ -223,7 +223,7 @@ returns Transaction::BackgroundJob.run( object: 'Ticket', type: 'escalation', - ticket_id: ticket.id, + object_id: ticket.id, article_id: ticket.articles.last.id, ) result.push ticket @@ -234,7 +234,7 @@ returns Transaction::BackgroundJob.run( object: 'Ticket', type: 'escalation_warning', - ticket_id: ticket.id, + object_id: ticket.id, article_id: ticket.articles.last.id, ) result.push ticket From 4646aca8401e82786f69e6385d63279ade294ebf Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Sat, 23 Apr 2016 11:04:33 +0200 Subject: [PATCH 09/11] Added cti call log ui. --- .../javascripts/app/controllers/cti.coffee | 114 ++++++++++++ .../javascripts/app/views/cti/index.jst.eco | 28 +++ app/controllers/application_controller.rb | 3 +- .../integration/sipgate_controller.rb | 121 +++++++++---- app/controllers/users_controller.rb | 2 +- app/models/cti/log.rb | 78 +++++++++ config/routes/integration_sipgate.rb | 1 + db/migrate/20160422000003_create_cti_log.rb | 26 +++ db/seeds.rb | 7 + test/integration/sipgate_controller_test.rb | 162 ++++++++++++++++-- 10 files changed, 492 insertions(+), 50 deletions(-) create mode 100644 app/assets/javascripts/app/controllers/cti.coffee create mode 100644 app/assets/javascripts/app/views/cti/index.jst.eco create mode 100644 app/models/cti/log.rb create mode 100644 db/migrate/20160422000003_create_cti_log.rb diff --git a/app/assets/javascripts/app/controllers/cti.coffee b/app/assets/javascripts/app/controllers/cti.coffee new file mode 100644 index 000000000..08b09ec27 --- /dev/null +++ b/app/assets/javascripts/app/controllers/cti.coffee @@ -0,0 +1,114 @@ +class App.CTI extends App.Controller + constructor: -> + super + + @meta = + active: false + + preferences = @Session.get('preferences') || {} + @meta.active = preferences.cti || false + + @load() + + App.Event.bind( + 'cti_event' + (data) => + console.log('cti_event', data) + if data.state is 'newCall' + console.log('notify') + @notify(data) + 'cti_event' + ) + App.Event.bind( + 'cti_list_push' + (data) => + @list = data + @render() + 'cti_list_push' + ) + + # fetch data, render view + load: -> + @ajax( + id: 'cti_log' + type: 'GET' + url: "#{@apiPath}/cti/log" + success: (data) => + @list = data + @render() + ) + + notify: (data) -> + console.log(data) + #return if ! + if data.state is 'newCall' && data.direction is 'in' + App.Event.trigger 'notify', { + type: 'notice' + msg: App.i18n.translateContent('Call from %s for %s', data.from, data.to) + timeout: 2500 + } + + featureActive: => + return true + if @Config.get('sipgate_integration') + return true + false + + render: -> + if !@isRole('CTI') + @renderScreenUnauthorized(objectName: 'CTI') + return + + @html App.view('cti/index')( + list: @list + ) + + @updateNavMenu() + + show: (params) => + @title 'CTI', true + @navupdate '#cti' + + counter: -> + counter = 0 + + switch: (state = undefined) => + + # read state + if state is undefined + return @meta.active + + @meta.active = state + + # update user preferences + @ajax( + id: 'preferences' + type: 'PUT' + url: "#{@apiPath}/users/preferences" + data: JSON.stringify(user: {cti: state}) + processData: true + ) + + updateNavMenu: => + delay = -> + App.Event.trigger('menu:render') + @delay(delay, 200, 'updateNavMenu') + +class CTIRouter extends App.ControllerPermanent + constructor: (params) -> + super + + # check authentication + return if !@authenticate(false, 'CTI') + + App.TaskManager.execute( + key: 'CTI' + controller: 'CTI' + params: {} + show: true + persistent: true + ) + +App.Config.set('cti', CTIRouter, 'Routes') +App.Config.set('CTI', { controller: 'CTI', authentication: true }, 'permanentTask') +App.Config.set('CTI', { prio: 1300, parent: '', name: 'Phone', target: '#cti', key: 'CTI', shown: false, role: ['CTI'], class: 'phone' }, 'NavBar') diff --git a/app/assets/javascripts/app/views/cti/index.jst.eco b/app/assets/javascripts/app/views/cti/index.jst.eco new file mode 100644 index 000000000..d1f660668 --- /dev/null +++ b/app/assets/javascripts/app/views/cti/index.jst.eco @@ -0,0 +1,28 @@ +
+

<%- @T('Caller log') %>

+ +
+
+ + + + + + + + + + + <% for item in @list: %> + + + + + + + + <% end %> + +
<%- @T('From') %><%- @T('To') %><%- @T('State') %><%- @T('Comment') %><%- @T('Time') %>
<%= item.from %><%= item.to %><%= item.state %><%= item.comment %><%- @humanTime(item.created_at) %>
+
+ diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 27dfe7ccd..37a12b9d3 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -287,12 +287,11 @@ class ApplicationController < ActionController::Base } end - def authentication_check(auth_param = {} ) + def authentication_check(auth_param = {}) result = authentication_check_only(auth_param) # check if basic_auth fallback is possible if auth_param[:basic_auth_promt] && result[:auth] == false - return request_http_basic_authentication end diff --git a/app/controllers/integration/sipgate_controller.rb b/app/controllers/integration/sipgate_controller.rb index 450317584..0cbf6f518 100644 --- a/app/controllers/integration/sipgate_controller.rb +++ b/app/controllers/integration/sipgate_controller.rb @@ -1,54 +1,60 @@ +# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/ + require 'builder' class Integration::SipgateController < ApplicationController - before_action { http_log_config facility: 'sipgate.io' } + + # list current caller log + def index + return if !authentication_check + return if deny_if_not_role('CTI') + list = Cti::Log.order('created_at DESC').limit(60) + render json: list + end # notify about inbound call / block inbound call def in + http_log_config facility: 'sipgate.io' return if !configured? - config = Setting.get('sipgate_config') - config_inbound = config[:inbound] || {} - block_caller_ids = config_inbound[:block_caller_ids] || [] - if params['event'] == 'newCall' + config = Setting.get('sipgate_config') + config_inbound = config[:inbound] || {} + block_caller_ids = config_inbound[:block_caller_ids] || [] + # check if call need to be blocked block_caller_ids.each {|item| next unless item[:caller_id] == params['from'] xml = Builder::XmlMarkup.new(indent: 2) xml.instruct! - content = xml.Response('onHangup' => in_url, 'onAnswer' => in_url) do + content = xml.Response(onHangup: url, onAnswer: url) do xml.Reject('reason' => 'busy') end send_data content, type: 'application/xml; charset=UTF-8;' - params['Reject'] = 'busy' - Sessions.broadcast( - event: 'sipgate.io', - data: params - ) + #params['Reject'] = 'busy' + params['comment'] = 'reject, busy' + if params['user'] + params['comment'] = "#{params['user']} -> reject, busy" + end + update_log(params) return true } end + update_log(params) + xml = Builder::XmlMarkup.new(indent: 2) xml.instruct! - content = xml.Response('onHangup' => in_url, 'onAnswer' => in_url) - + content = xml.Response(onHangup: url, onAnswer: url) send_data content, type: 'application/xml; charset=UTF-8;' - - # search for caller - Sessions.broadcast( - event: 'sipgate.io', - data: params - ) - end # set caller id of outbound call def out + http_log_config facility: 'sipgate.io' return if !configured? config = Setting.get('sipgate_config') @@ -61,31 +67,32 @@ class Integration::SipgateController < ApplicationController # set callerId content = nil to = params[:to] + from = nil if to config_outbound.each {|row| dest = row[:dest].gsub(/\*/, '.+?') next if to !~ /^#{dest}$/ - content = xml.Response('onHangup' => in_url, 'onAnswer' => in_url) do - xml.Dial(callerId: row[:caller_id]) { xml.Number(params[:to]) } + from = row[:caller_id] + content = xml.Response(onHangup: url, onAnswer: url) do + xml.Dial(callerId: from) { xml.Number(params[:to]) } end break } if !content && default_caller_id - content = xml.Response('onHangup' => in_url, 'onAnswer' => in_url) do + from = default_caller_id + content = xml.Response(onHangup: url, onAnswer: url) do xml.Dial(callerId: default_caller_id) { xml.Number(params[:to]) } end end else - content = xml.Response('onHangup' => in_url, 'onAnswer' => in_url) + content = xml.Response(onHangup: url, onAnswer: url) end send_data content, type: 'application/xml; charset=UTF-8;' - - # notify about outbound call - Sessions.broadcast( - event: 'sipgate.io:out', - data: params - ) + if from + params['from'] = from + end + update_log(params) end private @@ -103,23 +110,69 @@ class Integration::SipgateController < ApplicationController true end + def update_log(params) + + user = params['user'] + if params['user'] && params['user'].class == Array + user = params['user'].join(', ') + end + from_comment = nil + to_comment = nil + if params['direction'] == 'in' + to_comment = user + else + from_comment = user + end + comment = nil + if params['cause'] + comment = params['cause'] + end + + if params['event'] == 'newCall' + Cti::Log.create( + direction: params['direction'], + from: params['from'], + from_comment: from_comment, + to: params['to'], + to_comment: to_comment, + call_id: params['callId'], + comment: comment, + state: params['event'], + ) + elsif params['event'] == 'answer' + log = Cti::Log.find_by(call_id: params['callId']) + raise "No such call_id #{params['callId']}" if !log + log.state = 'answer' + log.comment = comment + log.save + elsif params['event'] == 'hangup' + log = Cti::Log.find_by(call_id: params['callId']) + raise "No such call_id #{params['callId']}" if !log + log.state = 'hangup' + log.comment = comment + log.save + else + raise "Unknown event #{params['event']}" + end + + end + def xml_error(error) xml = Builder::XmlMarkup.new(indent: 2) xml.instruct! content = xml.Response() do xml.Error(error) end - send_data content, type: 'application/xml; charset=UTF-8;', status: '500' + send_data content, type: 'application/xml; charset=UTF-8;', status: 422 end def base_url http_type = Setting.get('http_type') fqdn = Setting.get('fqdn') - "#{http_type}://#{fqdn}/api/v1/sipgate" end - def in_url - "#{base_url}/in" + def url + "#{base_url}/#{params['direction']}" end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index a2c727aae..072fb7e0a 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -85,7 +85,7 @@ class UsersController < ApplicationController group_ids = [] role_ids = [] if count <= 2 - Role.where(name: [ Z_ROLENAME_ADMIN, 'Agent', 'Chat']).each { |role| + Role.where(name: [ Z_ROLENAME_ADMIN, 'Agent', 'Chat', 'CTI']).each { |role| role_ids.push role.id } Group.all().each { |group| diff --git a/app/models/cti/log.rb b/app/models/cti/log.rb new file mode 100644 index 000000000..bf97b138a --- /dev/null +++ b/app/models/cti/log.rb @@ -0,0 +1,78 @@ +module Cti + class Log < ApplicationModel + self.table_name = 'cti_logs' + + after_create :push_event, :push_caller_list + after_update :push_event, :push_caller_list + after_destroy :push_event, :push_caller_list + +=begin + + Cti::Log.create( + direction: 'in', + from: '007', + from_comment: '', + to: '008', + to_comment: '', + call_id: '1', + comment: '', + state: 'newCall', + ) + + Cti::Log.create( + direction: 'in', + from: '007', + from_comment: '', + to: '008', + to_comment: '', + call_id: '2', + comment: '', + state: 'answer', + ) + + Cti::Log.create( + direction: 'in', + from: '009', + from_comment: '', + to: '010', + to_comment: '', + call_id: '3', + comment: '', + state: 'hangup', + ) + +=end + + def push_event + users = User.of_role('CTI') + users.each {|user| + + # send notify about event + Sessions.send_to( + user.id, + { + event: 'cti_event', + data: self, + }, + ) + } + end + + def push_caller_list + list = Cti::Log.order('created_at DESC').limit(60) + + users = User.of_role('CTI') + users.each {|user| + + # send notify on create/update/delete + Sessions.send_to( + user.id, + { + event: 'cti_list_push', + data: list, + }, + ) + } + end + end +end diff --git a/config/routes/integration_sipgate.rb b/config/routes/integration_sipgate.rb index 04d4d8f91..d569898d2 100644 --- a/config/routes/integration_sipgate.rb +++ b/config/routes/integration_sipgate.rb @@ -1,5 +1,6 @@ Zammad::Application.routes.draw do + match '/api/v1/cti/log', to: 'integration/sipgate#index', via: :get match '/api/v1/sipgate/in', to: 'integration/sipgate#in', via: :post match '/api/v1/sipgate/out', to: 'integration/sipgate#out', via: :post diff --git a/db/migrate/20160422000003_create_cti_log.rb b/db/migrate/20160422000003_create_cti_log.rb new file mode 100644 index 000000000..3bbfb0b61 --- /dev/null +++ b/db/migrate/20160422000003_create_cti_log.rb @@ -0,0 +1,26 @@ +class CreateCtiLog < ActiveRecord::Migration + def up + create_table :cti_logs do |t| + t.string :direction, limit: 20, null: false + t.string :state, limit: 20, null: false + t.string :from, limit: 100, null: false + t.string :from_comment, limit: 250, null: true + t.string :to, limit: 100, null: false + t.string :to_comment, limit: 250, null: true + t.string :call_id, limit: 250, null: false + t.string :comment, limit: 500, null: true + t.timestamps null: false + end + add_index :cti_logs, [:call_id], unique: true + add_index :cti_logs, [:direction] + add_index :cti_logs, [:from] + + Role.create_if_not_exists( + name: 'CTI', + note: 'Access to CTI feature.', + updated_by_id: 1, + created_by_id: 1 + ) + + end +end diff --git a/db/seeds.rb b/db/seeds.rb index ae9c440ea..00a85194e 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1915,6 +1915,13 @@ Role.create_if_not_exists( updated_by_id: 1, created_by_id: 1 ) +Role.create_if_not_exists( + id: 6, + name: 'CTI', + note: 'Access to CTI feature.', + updated_by_id: 1, + created_by_id: 1 +) Group.create_if_not_exists( id: 1, diff --git a/test/integration/sipgate_controller_test.rb b/test/integration/sipgate_controller_test.rb index fa9432433..98fb66ecc 100644 --- a/test/integration/sipgate_controller_test.rb +++ b/test/integration/sipgate_controller_test.rb @@ -5,6 +5,8 @@ require 'rexml/document' class SipgateControllerTest < ActionDispatch::IntegrationTest setup do + Cti::Log.destroy_all + Setting.create_or_update( title: 'sipgate.io integration', name: 'sipgate_integration', @@ -65,12 +67,27 @@ class SipgateControllerTest < ActionDispatch::IntegrationTest preferences: { prio: 2 }, ) + groups = Group.where(name: 'Users') + roles = Role.where(name: 'Agent') + agent = User.create_or_update( + login: 'cti-agent@example.com', + firstname: 'E', + lastname: 'S', + email: 'cti-agent@example.com', + password: 'agentpw', + active: true, + roles: roles, + groups: groups, + updated_by_id: 1, + created_by_id: 1, + ) + end test 'basic call' do # inbound - I - params = 'event=newCall&direction=in&from=4912347114711&to=4930600000000&callId=4991155921769858278&user%5B%5D=user+1&user%5B%5D=user+2' + params = 'event=newCall&direction=in&from=4912347114711&to=4930600000000&callId=4991155921769858278-1&user%5B%5D=user+1&user%5B%5D=user+2' post '/api/v1/sipgate/in', params assert_response(200) on_hangup = nil @@ -85,7 +102,7 @@ class SipgateControllerTest < ActionDispatch::IntegrationTest assert_equal('http://zammad.example.com/api/v1/sipgate/in', on_answer) # inbound - II - block caller - params = 'event=newCall&direction=in&from=491715000000&to=4930600000000&callId=4991155921769858278&user%5B%5D=user+1&user%5B%5D=user+2' + params = 'event=newCall&direction=in&from=491715000000&to=4930600000000&callId=4991155921769858278-2&user%5B%5D=user+1&user%5B%5D=user+2' post '/api/v1/sipgate/in', params assert_response(200) on_hangup = nil @@ -105,7 +122,7 @@ class SipgateControllerTest < ActionDispatch::IntegrationTest assert_equal('busy', reason) # outbound - I - set default_caller_id - params = 'event=newCall&direction=out&from=4930600000000&to=4912347114711&callId=8621106404543334274&user%5B%5D=user+1' + params = 'event=newCall&direction=out&from=4930600000000&to=4912347114711&callId=8621106404543334274-3&user%5B%5D=user+1' post '/api/v1/sipgate/out', params assert_response(200) on_hangup = nil @@ -126,11 +143,11 @@ class SipgateControllerTest < ActionDispatch::IntegrationTest end assert_equal('4930777000000', caller_id) assert_equal('4912347114711', number_to_dail) - assert_equal('http://zammad.example.com/api/v1/sipgate/in', on_hangup) - assert_equal('http://zammad.example.com/api/v1/sipgate/in', on_answer) + assert_equal('http://zammad.example.com/api/v1/sipgate/out', on_hangup) + assert_equal('http://zammad.example.com/api/v1/sipgate/out', on_answer) # outbound - II - set caller_id based on routing_table by explicite number - params = 'event=newCall&direction=out&from=4930600000000&to=491714000000&callId=8621106404543334274&user%5B%5D=user+1' + params = 'event=newCall&direction=out&from=4930600000000&to=491714000000&callId=8621106404543334274-4&user%5B%5D=user+1' post '/api/v1/sipgate/out', params assert_response(200) on_hangup = nil @@ -151,11 +168,11 @@ class SipgateControllerTest < ActionDispatch::IntegrationTest end assert_equal('41715880339000', caller_id) assert_equal('491714000000', number_to_dail) - assert_equal('http://zammad.example.com/api/v1/sipgate/in', on_hangup) - assert_equal('http://zammad.example.com/api/v1/sipgate/in', on_answer) + assert_equal('http://zammad.example.com/api/v1/sipgate/out', on_hangup) + assert_equal('http://zammad.example.com/api/v1/sipgate/out', on_answer) # outbound - III - set caller_id based on routing_table by 41* - params = 'event=newCall&direction=out&from=4930600000000&to=4147110000000&callId=8621106404543334274&user%5B%5D=user+1' + params = 'event=newCall&direction=out&from=4930600000000&to=4147110000000&callId=8621106404543334274-5&user%5B%5D=user+1' post '/api/v1/sipgate/out', params assert_response(200) on_hangup = nil @@ -176,14 +193,14 @@ class SipgateControllerTest < ActionDispatch::IntegrationTest end assert_equal('41715880339000', caller_id) assert_equal('4147110000000', number_to_dail) - assert_equal('http://zammad.example.com/api/v1/sipgate/in', on_hangup) - assert_equal('http://zammad.example.com/api/v1/sipgate/in', on_answer) + assert_equal('http://zammad.example.com/api/v1/sipgate/out', on_hangup) + assert_equal('http://zammad.example.com/api/v1/sipgate/out', on_answer) # no config Setting.set('sipgate_config', {}) - params = 'event=newCall&direction=in&from=4912347114711&to=4930600000000&callId=4991155921769858278&user%5B%5D=user+1&user%5B%5D=user+2' + params = 'event=newCall&direction=in&from=4912347114711&to=4930600000000&callId=4991155921769858278-6&user%5B%5D=user+1&user%5B%5D=user+2' post '/api/v1/sipgate/in', params - assert_response(500) + assert_response(422) error = nil content = @response.body response = REXML::Document.new(content) @@ -194,4 +211,123 @@ class SipgateControllerTest < ActionDispatch::IntegrationTest end + test 'log call' do + + # outbound - I - new call + params = 'event=newCall&direction=out&from=4930600000000&to=4912347114711&callId=1234567890-1&user%5B%5D=user+1' + post '/api/v1/sipgate/out', params + assert_response(200) + log = Cti::Log.find_by(call_id: '1234567890-1') + assert(log) + assert_equal('4930777000000', log.from) + assert_equal('4912347114711', log.to) + assert_equal('out', log.direction) + assert_equal('user 1', log.from_comment) + assert_equal(nil, log.comment) + assert_equal('newCall', log.state) + + # outbound - I - hangup by agent + params = 'event=hangup&direction=out&callId=1234567890-1&cause=cancel' + post '/api/v1/sipgate/out', params + assert_response(200) + log = Cti::Log.find_by(call_id: '1234567890-1') + assert(log) + assert_equal('4930777000000', log.from) + assert_equal('4912347114711', log.to) + assert_equal('out', log.direction) + assert_equal('user 1', log.from_comment) + assert_equal('cancel', log.comment) + assert_equal('hangup', log.state) + + # outbound - II - new call + params = 'event=newCall&direction=out&from=4930600000000&to=4912347114711&callId=1234567890-2&user%5B%5D=user+1' + post '/api/v1/sipgate/out', params + assert_response(200) + log = Cti::Log.find_by(call_id: '1234567890-2') + assert(log) + assert_equal('4930777000000', log.from) + assert_equal('4912347114711', log.to) + assert_equal('out', log.direction) + assert_equal('user 1', log.from_comment) + assert_equal(nil, log.comment) + assert_equal('newCall', log.state) + + # outbound - II - answer by customer + params = 'event=answer&direction=out&callId=1234567890-2&from=4930600000000&to=4912347114711' + post '/api/v1/sipgate/out', params + assert_response(200) + log = Cti::Log.find_by(call_id: '1234567890-2') + assert(log) + assert_equal('4930777000000', log.from) + assert_equal('4912347114711', log.to) + assert_equal('out', log.direction) + assert_equal('user 1', log.from_comment) + assert_equal(nil, log.comment) + assert_equal('answer', log.state) + + # outbound - II - hangup by customer + params = 'event=hangup&direction=out&callId=1234567890-2&cause=normalClearing&from=4930600000000&to=4912347114711' + post '/api/v1/sipgate/out', params + assert_response(200) + log = Cti::Log.find_by(call_id: '1234567890-2') + assert(log) + assert_equal('4930777000000', log.from) + assert_equal('4912347114711', log.to) + assert_equal('out', log.direction) + assert_equal('user 1', log.from_comment) + assert_equal('normalClearing', log.comment) + assert_equal('hangup', log.state) + + # inbound - I - new call + params = 'event=newCall&direction=in&to=4930600000000&from=4912347114711&callId=1234567890-3&user%5B%5D=user+1' + post '/api/v1/sipgate/in', params + assert_response(200) + log = Cti::Log.find_by(call_id: '1234567890-3') + assert(log) + assert_equal('4930600000000', log.to) + assert_equal('4912347114711', log.from) + assert_equal('in', log.direction) + assert_equal('user 1', log.to_comment) + assert_equal(nil, log.comment) + assert_equal('newCall', log.state) + + # inbound - I - answer by customer + params = 'event=answer&direction=in&callId=1234567890-3&to=4930600000000&from=4912347114711' + post '/api/v1/sipgate/in', params + assert_response(200) + log = Cti::Log.find_by(call_id: '1234567890-3') + assert(log) + assert_equal('4930600000000', log.to) + assert_equal('4912347114711', log.from) + assert_equal('in', log.direction) + assert_equal('user 1', log.to_comment) + assert_equal(nil, log.comment) + assert_equal('answer', log.state) + + # inbound - I - hangup by customer + params = 'event=hangup&direction=in&callId=1234567890-3&cause=normalClearing&to=4930600000000&from=4912347114711' + post '/api/v1/sipgate/in', params + assert_response(200) + log = Cti::Log.find_by(call_id: '1234567890-3') + assert(log) + assert_equal('4930600000000', log.to) + assert_equal('4912347114711', log.from) + assert_equal('in', log.direction) + assert_equal('user 1', log.to_comment) + assert_equal('normalClearing', log.comment) + assert_equal('hangup', log.state) + + get '/api/v1/cti/log' + assert_response(401) + + headers = { 'ACCEPT' => 'application/json', 'CONTENT_TYPE' => 'application/json' } + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('cti-agent@example.com', 'agentpw') + get '/api/v1/cti/log', {}, headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(result.class, Array) + assert_equal(3, result.count) + + end + end From 9602597e1c620c48816834a41206f8a94a759e8b Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Sat, 23 Apr 2016 11:20:40 +0200 Subject: [PATCH 10/11] Fixed migration for initial setup. --- app/assets/javascripts/app/views/cti/index.jst.eco | 4 ++-- db/migrate/20160422000003_create_cti_log.rb | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/app/views/cti/index.jst.eco b/app/assets/javascripts/app/views/cti/index.jst.eco index d1f660668..7aba99b57 100644 --- a/app/assets/javascripts/app/views/cti/index.jst.eco +++ b/app/assets/javascripts/app/views/cti/index.jst.eco @@ -15,8 +15,8 @@ <% for item in @list: %> - <%= item.from %> - <%= item.to %> + <%= item.from %><% if item.from_comment: %> (<%= item.from_comment %>)<% end %> + <%= item.to %><% if item.to_comment: %> (<%= item.to_comment %>)<% end %> <%= item.state %> <%= item.comment %> <%- @humanTime(item.created_at) %> diff --git a/db/migrate/20160422000003_create_cti_log.rb b/db/migrate/20160422000003_create_cti_log.rb index 3bbfb0b61..4f145cd32 100644 --- a/db/migrate/20160422000003_create_cti_log.rb +++ b/db/migrate/20160422000003_create_cti_log.rb @@ -15,6 +15,9 @@ class CreateCtiLog < ActiveRecord::Migration add_index :cti_logs, [:direction] add_index :cti_logs, [:from] + # return if it's a new setup + return if !Setting.find_by(name: 'system_init_done') + Role.create_if_not_exists( name: 'CTI', note: 'Access to CTI feature.', From 2d9ceaf220e697644ca4134528f2112d288e66f3 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Sat, 23 Apr 2016 11:31:08 +0200 Subject: [PATCH 11/11] Applied new role count (added CTI role). --- test/integration/zendesk_import_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/zendesk_import_test.rb b/test/integration/zendesk_import_test.rb index a68fa24c5..1fab59be1 100644 --- a/test/integration/zendesk_import_test.rb +++ b/test/integration/zendesk_import_test.rb @@ -46,7 +46,7 @@ class ZendeskImportTest < ActiveSupport::TestCase test 'check counts' do assert_equal( 143, User.count, 'users' ) assert_equal( 3, Group.count, 'groups' ) - assert_equal( 5, Role.count, 'roles' ) + assert_equal( 6, Role.count, 'roles' ) assert_equal( 2, Organization.count, 'organizations' ) assert_equal( 143, Ticket.count, 'tickets' ) assert_equal( 151, Ticket::Article.count, 'ticket articles' )