Merge branch 'develop' of git.znuny.com:zammad/zammad into develop

This commit is contained in:
Martin Edenhofer 2018-04-13 16:26:11 +02:00
commit 221c8a0538
7 changed files with 279 additions and 80 deletions

View file

@ -235,7 +235,15 @@ class EmailReply extends App.Controller
articleTypes
@setArticleType: (type, ticket, ui, signaturePosition) ->
@setArticleTypePre: (type, ticket, ui, signaturePosition) ->
# remove old signature
if type isnt 'email'
ui.$('[data-name=body] [data-signature=true]').remove()
return
@setArticleTypePost: (type, ticket, ui, signaturePosition) ->
return if type isnt 'email'
# detect current signature (use current group_id, if not set, use ticket.group_id)
ticketCurrent = App.Ticket.fullLocal(ticket.id)
@ -249,7 +257,7 @@ class EmailReply extends App.Controller
signature = App.Signature.find(group.signature_id)
# add/replace signature
if signature && signature.body && type is 'email'
if signature && signature.body
# if signature has changed, remove it
signature_id = ui.$('[data-signature=true]').data('signature-id')
@ -271,15 +279,6 @@ class EmailReply extends App.Controller
body.append(signature)
ui.$('[data-name=body]').replaceWith(body)
# remove old signature
else
ui.$('[data-name=body] [data-signature=true]').remove()
if type isnt 'email'
ui.$('[name=to]').val('')
ui.$('[name=cc]').val('')
ui.$('[name=subject]').val('')
@validation: (type, params, ui) ->
return true if type isnt 'email'

View file

@ -61,7 +61,7 @@ class TelegramReply
}
articleTypes
@setArticleType: (type, ticket, ui) ->
@setArticleTypePost: (type, ticket, ui) ->
return if type isnt 'telegram personal-message'
rawHTML = ui.$('[data-name=body]').html()
cleanHTML = App.Utils.htmlRemoveRichtext(rawHTML)

View file

@ -116,7 +116,7 @@ class TwitterReply
else
articleNew.to = article.from
if !articleNew.to
if !articleNew.to && customer && customer.accounts
articleNew.to = customer.accounts['twitter'].username || customer.accounts['twitter'].uid
App.Event.trigger('ui::ticket::setArticleType', {
@ -169,9 +169,23 @@ class TwitterReply
textLength = ui.maxTextLength - App.Utils.textLengthWithUrl(params.body)
return false if textLength < 0
# check if recipient exists
if _.isEmpty(params.to)
new App.ControllerModal(
head: 'Text missing'
buttonCancel: 'Cancel'
buttonCancelClass: 'btn--danger'
buttonSubmit: false
message: 'Need recipient in "To".'
shown: true
small: true
container: ui.el.closest('.content')
)
return false
true
@setArticleType: (type, ticket, ui) ->
@setArticleTypePost: (type, ticket, ui) ->
return if type isnt 'twitter status' && type isnt 'twitter direct-message'
rawHTML = ui.$('[data-name=body]').html()
cleanHTML = App.Utils.htmlRemoveRichtext(rawHTML)

View file

@ -51,6 +51,8 @@ class App.TicketZoomArticleNew extends App.Controller
@bind('ui::ticket::setArticleType', (data) =>
return if data.ticket.id.toString() isnt @ticket_id.toString()
@setArticleTypePre(data.type.name, data.signaturePosition)
@openTextarea(null, true)
for key, value of data.article
if key is 'body'
@ -58,8 +60,7 @@ class App.TicketZoomArticleNew extends App.Controller
else
@$("[name=\"#{key}\"]").val(value).trigger('change')
# preselect article type
@setArticleType(data.type.name, data.signaturePosition)
@setArticleTypePost(data.type.name, data.signaturePosition)
# set focus into field
if data.focus
@ -120,11 +121,8 @@ class App.TicketZoomArticleNew extends App.Controller
App.Delay.set(a, 500, undefined, 'tags')
setPossibleArticleTypes: =>
actionConfig = App.Config.get('TicketZoomArticleAction')
keys = _.keys(actionConfig).sort()
@articleTypes = []
for key in keys
config = actionConfig[key]
for config in @actions()
if config && config.articleTypes
@articleTypes = config.articleTypes(@articleTypes, @ticket, @)
@ -166,7 +164,8 @@ class App.TicketZoomArticleNew extends App.Controller
isCustomer: @permissionCheck('ticket.customer')
internalSelector: @internalSelector
)
@setArticleType(@type)
@setArticleTypePre(@type)
@setArticleTypePost(@type)
new App.WidgetAvatar(
el: @$('.js-avatar')
@ -278,10 +277,7 @@ class App.TicketZoomArticleNew extends App.Controller
params.internal = false
# backend based validation
actionConfig = App.Config.get('TicketZoomArticleAction')
keys = _.keys(actionConfig).sort()
for key in keys
config = actionConfig[key]
for config in @actions()
if config && config.params
params = config.params(params.type, params, @)
@ -323,10 +319,7 @@ class App.TicketZoomArticleNew extends App.Controller
return false
# backend based validation
actionConfig = App.Config.get('TicketZoomArticleAction')
keys = _.keys(actionConfig).sort()
for key in keys
config = actionConfig[key]
for config in @actions()
if config && config.validation
return false if !config.validation(params.type, params, @)
@ -350,10 +343,11 @@ class App.TicketZoomArticleNew extends App.Controller
selectArticleType: (event) =>
event.stopPropagation()
articleTypeToSet = $(event.target).closest('.pop-selectable').data('value')
@setArticleType(articleTypeToSet)
@setArticleTypePre(articleTypeToSet)
@hideSelectableArticleType()
@setArticleTypePost(articleTypeToSet)
$(window).off 'click.ticket-zoom-select-type'
$(window).off('click.ticket-zoom-select-type')
@tokanice()
hideSelectableArticleType: =>
@ -374,8 +368,14 @@ class App.TicketZoomArticleNew extends App.Controller
@$('[name=internal]').val('')
setArticleType: (type, signaturePosition = 'bottom') =>
setArticleTypePre: (type, signaturePosition = 'bottom') =>
wasScrolledToBottom = @isScrolledToBottom()
# reset old params
if type isnt @type
for key in ['to', 'cc', 'bcc', 'subject', 'in_reply_to']
@$("[name=#{key}]").val('').trigger('change')
@type = type
@$('[name=type]').val(type).trigger('change')
@articleNewEdit.attr('data-type', type)
@ -395,13 +395,6 @@ class App.TicketZoomArticleNew extends App.Controller
else
@setArticleInternal(false)
actionConfig = App.Config.get('TicketZoomArticleAction')
keys = _.keys(actionConfig).sort()
for key in keys
localConfig = actionConfig[key]
if localConfig && localConfig.setArticleType
localConfig.setArticleType(@type, @ticket, @, signaturePosition)
# show/hide attributes/features
@maxTextLength = undefined
@warningTextLength = undefined
@ -438,6 +431,11 @@ class App.TicketZoomArticleNew extends App.Controller
@scrollToBottom() if wasScrolledToBottom
setArticleTypePost: (type, signaturePosition = 'bottom') =>
for localConfig in @actions()
if localConfig && localConfig.setArticleTypePost
localConfig.setArticleTypePost(@type, @ticket, @, signaturePosition)
isScrolledToBottom: ->
return @el.scrollParent().scrollTop() + @el.scrollParent().height() is @el.scrollParent().prop('scrollHeight')
@ -615,3 +613,13 @@ class App.TicketZoomArticleNew extends App.Controller
if element.find('.attachment').length == 0
element.empty()
)
actions: ->
actionConfig = App.Config.get('TicketZoomArticleAction')
keys = _.keys(actionConfig).sort()
actions = []
for key in keys
localConfig = actionConfig[key]
if localConfig
actions.push localConfig
actions

View file

@ -23,6 +23,7 @@ class Observer::Ticket::Article::CommunicateTwitter < ActiveRecord::Observer
type = Ticket::Article::Type.lookup(id: record.type_id)
return if type['name'] !~ /\Atwitter/i
raise Exceptions::UnprocessableEntity, 'twitter to: parameter is missing' if record.to.blank? && type['name'] == 'twitter direct-message'
Delayed::Job.enqueue(Observer::Ticket::Article::CommunicateTwitter::BackgroundJob.new(record.id))
end

View file

@ -3,36 +3,7 @@ require 'browser_test_helper'
class TwitterBrowserTest < TestCase
def test_add_config
# app config
if !ENV['TWITTER_BT_CONSUMER_KEY']
raise "ERROR: Need TWITTER_BT_CONSUMER_KEY - hint TWITTER_BT_CONSUMER_KEY='1234'"
end
consumer_key = ENV['TWITTER_BT_CONSUMER_KEY']
if !ENV['TWITTER_BT_CONSUMER_SECRET']
raise "ERROR: Need TWITTER_BT_CONSUMER_SECRET - hint TWITTER_BT_CONSUMER_SECRET='1234'"
end
consumer_secret = ENV['TWITTER_BT_CONSUMER_SECRET']
if !ENV['TWITTER_BT_USER_LOGIN']
raise "ERROR: Need TWITTER_BT_USER_LOGIN - hint TWITTER_BT_USER_LOGIN='1234'"
end
twitter_user_login = ENV['TWITTER_BT_USER_LOGIN']
if !ENV['TWITTER_BT_USER_PW']
raise "ERROR: Need TWITTER_BT_USER_PW - hint TWITTER_BT_USER_PW='1234'"
end
twitter_user_pw = ENV['TWITTER_BT_USER_PW']
if !ENV['TWITTER_BT_CUSTOMER_TOKEN']
raise "ERROR: Need TWITTER_BT_CUSTOMER_TOKEN - hint TWITTER_BT_CUSTOMER_TOKEN='1234'"
end
twitter_customer_token = ENV['TWITTER_BT_CUSTOMER_TOKEN']
if !ENV['TWITTER_BT_CUSTOMER_TOKEN_SECRET']
raise "ERROR: Need TWITTER_BT_CUSTOMER_TOKEN_SECRET - hint TWITTER_BT_CUSTOMER_TOKEN_SECRET='1234'"
end
twitter_customer_token_secret = ENV['TWITTER_BT_CUSTOMER_TOKEN_SECRET']
twitter_config
hash = "#sweet#{hash_gen}"
@ -51,7 +22,7 @@ class TwitterBrowserTest < TestCase
sleep 2
set(
css: '.content.active .modal [name=consumer_key]',
value: consumer_key,
value: twitter_config[:consumer_key],
)
set(
css: '.content.active .modal [name=consumer_secret]',
@ -66,7 +37,7 @@ class TwitterBrowserTest < TestCase
set(
css: '.content.active .modal [name=consumer_secret]',
value: consumer_secret,
value: twitter_config[:consumer_secret],
)
click(css: '.content.active .modal .js-submit')
@ -95,7 +66,7 @@ class TwitterBrowserTest < TestCase
set(
css: '.content.active .modal [name=consumer_secret]',
value: consumer_secret,
value: twitter_config[:consumer_secret],
)
click(css: '.content.active .modal .js-submit')
@ -115,12 +86,12 @@ class TwitterBrowserTest < TestCase
set(
css: '#username_or_email',
value: twitter_user_login,
value: twitter_config[:twitter_user_login],
no_click: true, # <label> other element would receive the click
)
set(
css: '#password',
value: twitter_user_pw,
value: twitter_config[:twitter_user_pw],
no_click: true, # <label> other element would receive the click
)
click(css: '#allow')
@ -139,6 +110,7 @@ class TwitterBrowserTest < TestCase
click(css: '.content.active .modal .js-searchTermAdd')
set(css: '.content.active .modal [name="search::term"]', value: hash)
select(css: '.content.active .modal [name="search::group_id"]', value: 'Users')
select(css: '.content.active .modal [name="direct_messages::group_id"]', value: 'Users')
click(css: '.content.active .modal .js-submit')
modal_disappear
@ -148,7 +120,7 @@ class TwitterBrowserTest < TestCase
)
watch_for(
css: '.content.active',
value: "@#{twitter_user_login}",
value: "@#{twitter_config[:twitter_user_login]}",
)
exists(
css: '.content.active .main .action:nth-child(1)'
@ -177,7 +149,7 @@ class TwitterBrowserTest < TestCase
)
watch_for(
css: '.content.active',
value: "@#{twitter_user_login}",
value: "@#{twitter_config[:twitter_user_login]}",
)
exists(
css: '.content.active .main .action:nth-child(1)'
@ -191,10 +163,10 @@ class TwitterBrowserTest < TestCase
# start tweet from customer
client = Twitter::REST::Client.new do |config|
config.consumer_key = consumer_key
config.consumer_secret = consumer_secret
config.access_token = twitter_customer_token
config.access_token_secret = twitter_customer_token_secret
config.consumer_key = twitter_config[:consumer_key]
config.consumer_secret = twitter_config[:consumer_secret]
config.access_token = twitter_config[:twitter_customer_token]
config.access_token_secret = twitter_config[:twitter_customer_token_secret]
end
text = "Today #{rand_word}... #{hash} #{hash_gen}"
@ -272,6 +244,100 @@ class TwitterBrowserTest < TestCase
end
def reply_direct_message
twitter_config
@browser = browser_instance
login(
username: 'master@example.com',
password: 'test',
url: browser_url,
auto_wizard: true,
)
tasks_close_all()
client = Twitter::REST::Client.new do |config|
config.consumer_key = twitter_config[:consumer_key]
config.consumer_secret = twitter_config[:consumer_secret]
config.access_token = twitter_config[:twitter_customer_token]
config.access_token_secret = twitter_config[:twitter_customer_token_secret]
end
text = "Today #{rand_word}... #{hash} #{hash_gen}"
tweet = client.create_direct_message(
"@#{twitter_config[:twitter_user_login]}",
text,
)
# watch till tweet is in app
click(text: 'Overviews')
# enable full overviews
execute(
js: '$(".content.active .sidebar").css("display", "block")',
)
click(text: 'Unassigned & Open')
watch_for(
css: '.content.active',
value: hash,
timeout: 36,
)
ticket_open_by_title(
title: hash,
)
# reply via app
click(css: '.content.active [data-type="twitterStatusReply"]')
ticket_update(
data: {
body: '@dzucker6 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890',
},
do_not_submit: true,
)
click(
css: '.content.active .js-submit',
)
sleep 10
click(
css: '.content.active .js-reset',
)
sleep 2
match_not(
css: '.content.active',
value: '1234567890',
)
click(css: '.content.active [data-type="twitterStatusReply"]')
sleep 2
re_hash = "#{hash}re#{rand(99_999)}"
ticket_update(
data: {
body: "@dzucker6 #{rand_word} reply #{re_hash} #{rand(999_999)}",
},
)
sleep 20
match(
css: '.content.active .ticket-article',
value: re_hash,
)
# watch till tweet reached customer
sleep 10
text = nil
client.search(re_hash, result_type: 'mixed').collect do |local_tweet|
text = local_tweet.text
end
assert(text)
end
def hash_gen
(0...10).map { ('a'..'z').to_a[rand(26)] }.join + rand(999).to_s
end
@ -298,4 +364,44 @@ class TwitterBrowserTest < TestCase
words[rand(words.length)]
end
def twitter_config
# app config
if !ENV['TWITTER_BT_CONSUMER_KEY']
raise "ERROR: Need TWITTER_BT_CONSUMER_KEY - hint TWITTER_BT_CONSUMER_KEY='1234'"
end
consumer_key = ENV['TWITTER_BT_CONSUMER_KEY']
if !ENV['TWITTER_BT_CONSUMER_SECRET']
raise "ERROR: Need TWITTER_BT_CONSUMER_SECRET - hint TWITTER_BT_CONSUMER_SECRET='1234'"
end
consumer_secret = ENV['TWITTER_BT_CONSUMER_SECRET']
if !ENV['TWITTER_BT_USER_LOGIN']
raise "ERROR: Need TWITTER_BT_USER_LOGIN - hint TWITTER_BT_USER_LOGIN='1234'"
end
twitter_user_login = ENV['TWITTER_BT_USER_LOGIN']
if !ENV['TWITTER_BT_USER_PW']
raise "ERROR: Need TWITTER_BT_USER_PW - hint TWITTER_BT_USER_PW='1234'"
end
twitter_user_pw = ENV['TWITTER_BT_USER_PW']
if !ENV['TWITTER_BT_CUSTOMER_TOKEN']
raise "ERROR: Need TWITTER_BT_CUSTOMER_TOKEN - hint TWITTER_BT_CUSTOMER_TOKEN='1234'"
end
twitter_customer_token = ENV['TWITTER_BT_CUSTOMER_TOKEN']
if !ENV['TWITTER_BT_CUSTOMER_TOKEN_SECRET']
raise "ERROR: Need TWITTER_BT_CUSTOMER_TOKEN_SECRET - hint TWITTER_BT_CUSTOMER_TOKEN_SECRET='1234'"
end
twitter_customer_token_secret = ENV['TWITTER_BT_CUSTOMER_TOKEN_SECRET']
hash = {
consumer_key: consumer_key,
consumer_secret: consumer_secret,
twitter_user_login: twitter_user_login,
twitter_user_pw: twitter_user_pw,
twitter_customer_token: twitter_customer_token,
twitter_customer_token_secret: twitter_customer_token_secret
}
end
end

View file

@ -427,6 +427,75 @@ class TwitterTest < ActiveSupport::TestCase
assert_equal('ok', channel.status_in)
end
test 'c new by direct message outbound without required parameters' do
# cleanup direct messages of system
client = Twitter::REST::Client.new do |config|
config.consumer_key = consumer_key
config.consumer_secret = consumer_secret
config.access_token = system_token
config.access_token_secret = system_token_secret
end
dms = client.direct_messages(count: 100)
dms.each do |dm|
client.destroy_direct_message(dm.id)
end
client = Twitter::REST::Client.new(
consumer_key: consumer_key,
consumer_secret: consumer_secret,
access_token: customer_token,
access_token_secret: customer_token_secret
)
dms = client.direct_messages(count: 100)
dms.each do |dm|
client.destroy_direct_message(dm.id)
end
hash = "#citheo44 #{hash_gen}"
text = "How about #{rand_word} the details? #{hash} - #{'Long' * 50}"
dm = client.create_direct_message(
system_login_without_at,
text,
)
assert(dm, "dm with ##{hash} created")
# fetch check system account
sleep 15
article = nil
1.times do
Channel.fetch
# check if ticket and article has been created
article = Ticket::Article.find_by(message_id: dm.id)
break if article
sleep 10
end
assert(article, "inbound article '#{text}' created")
assert_equal(customer_login, article.from, 'ticket article from')
assert_equal(text, article.body, 'ticket article body')
ticket = article.ticket
assert(ticket, 'ticket of inbound article exists')
assert(ticket.articles, 'ticket.articles exists')
assert_equal(1, ticket.articles.count, 'ticket article inbound count')
assert_equal(ticket.state.name, 'closed')
# reply via ticket
reply = assert_raises(Exceptions::UnprocessableEntity) do
Ticket::Article.create!(
ticket_id: ticket.id,
in_reply_to: '123456789',
body: "Will call you later #{rand_word}!",
type: Ticket::Article::Type.find_by(name: 'twitter direct-message'),
sender: Ticket::Article::Sender.find_by(name: 'Agent'),
internal: false,
updated_by_id: 1,
created_by_id: 1,
)
end
assert_equal('twitter to: parameter is missing', reply.message)
end
test 'd track_retweets enabled' do
# enable track_retweets
@ -538,6 +607,7 @@ class TwitterTest < ActiveSupport::TestCase
ActiveRecord::Base.connection.query_cache.clear
sleep 10
end
assert(article, "article from customer with text '#{text}' message_id '#{tweet.id}' created")
assert_equal(customer_login, article.from, 'ticket article from')
assert_nil(article.to, 'ticket article to')
@ -768,6 +838,7 @@ class TwitterTest < ActiveSupport::TestCase
)
article = nil
5.times do
Channel.fetch
Scheduler.worker(true)
article = Ticket::Article.find_by(message_id: tweet.id)
break if article