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

This commit is contained in:
André Bauer 2017-07-08 15:37:00 +02:00
commit 8cb2eb406f
62 changed files with 2117 additions and 537 deletions

View file

@ -3,7 +3,7 @@ before_script:
- which ruby - which ruby
- env - env
- test -n "$RNAME" && script/build/test_db_config.sh - test -n "$RNAME" && script/build/test_db_config.sh
- test -n "$RNAME" && bundle install - test -n "$RNAME" && bundle install --jobs 8
stages: stages:
- pre - pre
@ -229,7 +229,7 @@ test:integration:slack:
- rake db:create - rake db:create
- rake db:migrate - rake db:migrate
- echo "gem 'slack-api'" >> Gemfile.local - echo "gem 'slack-api'" >> Gemfile.local
- bundle install - bundle install --jobs 8
- ruby -I test test/integration/slack_test.rb - ruby -I test test/integration/slack_test.rb
- rake db:drop - rake db:drop
@ -281,6 +281,7 @@ test:integration:es_mysql:
- ruby -I test/ test/integration/elasticsearch_test.rb - ruby -I test/ test/integration/elasticsearch_test.rb
- ruby -I test/ test/controllers/search_controller_test.rb - ruby -I test/ test/controllers/search_controller_test.rb
- ruby -I test/ test/integration/report_test.rb - ruby -I test/ test/integration/report_test.rb
- ruby -I test/ test/controllers/form_controller_test.rb
- rake db:drop - rake db:drop
test:integration:es_postgresql: test:integration:es_postgresql:
@ -297,6 +298,7 @@ test:integration:es_postgresql:
- ruby -I test/ test/integration/elasticsearch_test.rb - ruby -I test/ test/integration/elasticsearch_test.rb
- ruby -I test/ test/controllers/search_controller_test.rb - ruby -I test/ test/controllers/search_controller_test.rb
- ruby -I test/ test/integration/report_test.rb - ruby -I test/ test/integration/report_test.rb
- ruby -I test/ test/controllers/form_controller_test.rb
- rake db:drop - rake db:drop
test:integration:zendesk_mysql: test:integration:zendesk_mysql:
@ -427,7 +429,7 @@ test:browser:integration:api_client_ruby:
- script/build/test_startup.sh $RAILS_ENV $BROWSER_PORT $WS_PORT 1 - script/build/test_startup.sh $RAILS_ENV $BROWSER_PORT $WS_PORT 1
- git clone git@github.com:zammad/zammad-api-client-ruby.git || script/build/test_shutdown.sh $RAILS_ENV $BROWSER_PORT $WS_PORT 1 1 - git clone git@github.com:zammad/zammad-api-client-ruby.git || script/build/test_shutdown.sh $RAILS_ENV $BROWSER_PORT $WS_PORT 1 1
- cd zammad-api-client-ruby - cd zammad-api-client-ruby
- bundle install - bundle install --jobs 8
- export TEST_URL=http://$IP:$BROWSER_PORT - export TEST_URL=http://$IP:$BROWSER_PORT
- rspec || (cd .. && script/build/test_shutdown.sh $RAILS_ENV $BROWSER_PORT $WS_PORT 1 1) - rspec || (cd .. && script/build/test_shutdown.sh $RAILS_ENV $BROWSER_PORT $WS_PORT 1 1)
- cd .. && script/build/test_shutdown.sh $RAILS_ENV $BROWSER_PORT $WS_PORT 0 1 - cd .. && script/build/test_shutdown.sh $RAILS_ENV $BROWSER_PORT $WS_PORT 0 1

View file

@ -7,6 +7,7 @@ notifications:
env: env:
- DB=mysql - DB=mysql
- DB=postgresql - DB=postgresql
- BUNDLE_JOBS=8
addons: addons:
postgresql: "9.4" postgresql: "9.4"
apt: apt:

View file

@ -45,7 +45,7 @@ gem 'twitter'
gem 'telegramAPI' gem 'telegramAPI'
gem 'koala' gem 'koala'
gem 'mail' gem 'mail'
gem 'email_verifier' gem 'valid_email2'
gem 'htmlentities' gem 'htmlentities'
gem 'mime-types' gem 'mime-types'

View file

@ -93,7 +93,6 @@ GEM
delayed_job (>= 3.0, < 5) delayed_job (>= 3.0, < 5)
diff-lcs (1.2.5) diff-lcs (1.2.5)
diffy (3.1.0) diffy (3.1.0)
dnsruby (1.59.3)
docile (1.1.5) docile (1.1.5)
domain_name (0.5.20170404) domain_name (0.5.20170404)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
@ -107,8 +106,6 @@ GEM
em-websocket (0.5.1) em-websocket (0.5.1)
eventmachine (>= 0.12.9) eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0) http_parser.rb (~> 0.6.0)
email_verifier (0.1.0)
dnsruby (>= 1.5)
equalizer (0.0.10) equalizer (0.0.10)
erubis (2.7.0) erubis (2.7.0)
eventmachine (1.2.3) eventmachine (1.2.3)
@ -403,6 +400,9 @@ GEM
unicorn (5.2.0) unicorn (5.2.0)
kgio (~> 2.6) kgio (~> 2.6)
raindrops (~> 0.7) raindrops (~> 0.7)
valid_email2 (1.2.17)
activemodel (>= 3.2)
mail (~> 2.5)
webmock (2.3.2) webmock (2.3.2)
addressable (>= 2.3.6) addressable (>= 2.3.6)
crack (>= 0.3.2) crack (>= 0.3.2)
@ -439,7 +439,6 @@ DEPENDENCIES
doorkeeper doorkeeper
eco eco
em-websocket em-websocket
email_verifier
eventmachine eventmachine
execjs execjs
factory_girl_rails factory_girl_rails
@ -491,6 +490,7 @@ DEPENDENCIES
twitter twitter
uglifier uglifier
unicorn unicorn
valid_email2
webmock webmock
writeexcel writeexcel
zendesk_api zendesk_api

View file

@ -531,8 +531,8 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
# base # base
configureAttributesBase = [ configureAttributesBase = [
{ name: 'realname', display: 'Organization & Department Name', tag: 'input', type: 'text', limit: 160, null: false, placeholder: 'Organization Support', autocomplete: 'new-password' }, { name: 'realname', display: 'Organization & Department Name', tag: 'input', type: 'text', limit: 160, null: false, placeholder: 'Organization Support', autocomplete: 'off' },
{ name: 'email', display: 'Email', tag: 'input', type: 'email', limit: 120, null: false, placeholder: 'support@example.com', autocapitalize: false, autocomplete: 'new-password' }, { name: 'email', display: 'Email', tag: 'input', type: 'email', limit: 120, null: false, placeholder: 'support@example.com', autocapitalize: false, autocomplete: 'off' },
{ name: 'password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', single: true }, { name: 'password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', single: true },
{ name: 'group_id', display: 'Destination Group', tag: 'select', null: false, relation: 'Group', nulloption: true }, { name: 'group_id', display: 'Destination Group', tag: 'select', null: false, relation: 'Group', nulloption: true },
] ]
@ -562,7 +562,7 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
configureAttributesInbound = [ configureAttributesInbound = [
{ name: 'adapter', display: 'Type', tag: 'select', multiple: false, null: false, options: @channelDriver.email.inbound }, { name: 'adapter', display: 'Type', tag: 'select', multiple: false, null: false, options: @channelDriver.email.inbound },
{ name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false }, { name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false },
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', }, { name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autocomplete: 'off', },
{ name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', single: true }, { name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', single: true },
{ name: 'options::ssl', display: 'SSL', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, default: true, translate: true, item_class: 'formGroup--halfSize' }, { name: 'options::ssl', display: 'SSL', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, default: true, translate: true, item_class: 'formGroup--halfSize' },
{ name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false, default: '993', item_class: 'formGroup--halfSize' }, { name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false, default: '993', item_class: 'formGroup--halfSize' },
@ -616,7 +616,7 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
if adapter is 'smtp' if adapter is 'smtp'
configureAttributesOutbound = [ configureAttributesOutbound = [
{ name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autofocus: true }, { name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autofocus: true },
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password', }, { name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, autocomplete: 'off', },
{ name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password', single: true }, { name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password', single: true },
{ name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false }, { name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false },
] ]
@ -930,7 +930,7 @@ class App.ChannelEmailNotificationWizard extends App.WizardModal
if adapter is 'smtp' if adapter is 'smtp'
configureAttributesOutbound = [ configureAttributesOutbound = [
{ name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autofocus: true }, { name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autofocus: true },
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password' }, { name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, autocomplete: 'off' },
{ name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password', single: true }, { name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password', single: true },
{ name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false }, { name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false },
] ]

View file

@ -3,12 +3,14 @@ class App.ChannelForm extends App.ControllerSubContent
requiredPermission: 'admin.channel_formular' requiredPermission: 'admin.channel_formular'
header: 'Form' header: 'Form'
events: events:
'change form.js-params': 'updateParams' 'change form.js-paramsDesigner': 'updateParamsDesigner'
'keyup form.js-params': 'updateParams' 'keyup form.js-paramsDesigner': 'updateParamsDesigner'
'change .js-formSetting input': 'toggleFormSetting' 'change .js-formSetting input': 'toggleFormSetting'
'change .js-paramsSetting select': 'updateGroup'
elements: elements:
'.js-paramsBlock': 'paramsBlock' '.js-paramsBlock': 'paramsBlock'
'.js-paramsSetting': 'paramsSetting'
'.js-formSetting input': 'formSetting' '.js-formSetting input': 'formSetting'
constructor: -> constructor: ->
@ -20,22 +22,38 @@ class App.ChannelForm extends App.ControllerSubContent
render: => render: =>
setting = App.Setting.get('form_ticket_create') setting = App.Setting.get('form_ticket_create')
@html App.view('channel/form')(
element = $(App.view('channel/form')(
baseurl: window.location.origin baseurl: window.location.origin
formSetting: setting formSetting: setting
))
group_id = App.Setting.get('form_ticket_create_group_id')
selection = App.UiElement.select.render(
name: 'group_id'
multiple: false
null: false
relation: 'Group'
nulloption: false
value: group_id
#class: 'form-control--small'
) )
console.log('s', element.find('.js-groupSelector'), selection)
element.find('.js-groupSelector').html(selection)
@html element
@paramsBlock.each (i, block) -> @paramsBlock.each (i, block) ->
hljs.highlightBlock block hljs.highlightBlock block
@updateParams() @updateParamsDesigner()
updateParams: -> updateParamsDesigner: ->
quote = (string) -> quote = (string) ->
string = string.replace('\'', '\\\'') string = string.replace('\'', '\\\'')
.replace(/\</g, '&lt;') .replace(/\</g, '&lt;')
.replace(/\>/g, '&gt;') .replace(/\>/g, '&gt;')
params = @formParam(@$('.js-params')) params = @formParam(@$('.js-paramsDesigner'))
paramString = '' paramString = ''
for key, value of params for key, value of params
if value != '' if value != ''
@ -63,4 +81,8 @@ class App.ChannelForm extends App.ControllerSubContent
value = @formSetting.prop('checked') value = @formSetting.prop('checked')
App.Setting.set('form_ticket_create', value) App.Setting.set('form_ticket_create', value)
updateGroup: =>
value = @paramsSetting.find('[name=group_id]').val()
App.Setting.set('form_ticket_create_group_id', value)
App.Config.set('Form', { prio: 2000, name: 'Form', parent: '#channels', target: '#channels/form', controller: App.ChannelForm, permission: ['admin.formular'] }, 'NavBarAdmin') App.Config.set('Form', { prio: 2000, name: 'Form', parent: '#channels', target: '#channels/form', controller: App.ChannelForm, permission: ['admin.formular'] }, 'NavBarAdmin')

View file

@ -404,7 +404,7 @@ class App.TicketZoom extends App.Controller
) )
new App.TicketZoomOverviewNavigator( new App.TicketZoomOverviewNavigator(
el: elLocal.find('.overview-navigator') el: elLocal.find('.js-overviewNavigatorContainer')
ticket_id: @ticket_id ticket_id: @ticket_id
overview_id: @overview_id overview_id: @overview_id
) )
@ -412,13 +412,13 @@ class App.TicketZoom extends App.Controller
new App.TicketZoomTitle( new App.TicketZoomTitle(
object_id: @ticket_id object_id: @ticket_id
overview_id: @overview_id overview_id: @overview_id
el: elLocal.find('.ticket-title') el: elLocal.find('.js-ticketTitleContainer')
task_key: @task_key task_key: @task_key
) )
new App.TicketZoomMeta( new App.TicketZoomMeta(
object_id: @ticket_id object_id: @ticket_id
el: elLocal.find('.ticket-meta') el: elLocal.find('.js-ticketMetaContainer')
) )
@attributeBar = new App.TicketZoomAttributeBar( @attributeBar = new App.TicketZoomAttributeBar(
@ -445,7 +445,12 @@ class App.TicketZoom extends App.Controller
) )
@highligher = new App.TicketZoomHighlighter( @highligher = new App.TicketZoomHighlighter(
el: elLocal.find('.highlighter') el: elLocal.find('.js-highlighterContainer')
ticket_id: @ticket_id
)
new App.TicketZoomSetting(
el: elLocal.find('.js-settingContainer')
ticket_id: @ticket_id ticket_id: @ticket_id
) )
@ -557,14 +562,16 @@ class App.TicketZoom extends App.Controller
return if !@ticket return if !@ticket
currentStoreTicket = @ticket.attributes() currentStoreTicket = @ticket.attributes()
delete currentStoreTicket.article delete currentStoreTicket.article
internal = @Config.get('ui_ticket_zoom_article_note_new_internal')
currentStore = currentStore =
ticket: currentStoreTicket ticket: currentStoreTicket
article: article:
to: '' to: ''
cc: '' cc: ''
subject: ''
type: 'note' type: 'note'
body: '' body: ''
internal: 'true' internal: internal
in_reply_to: '' in_reply_to: ''
if @permissionCheck('ticket.customer') if @permissionCheck('ticket.customer')
@ -575,7 +582,7 @@ class App.TicketZoom extends App.Controller
formCurrent: => formCurrent: =>
currentParams = currentParams =
ticket: @formParam(@el.find('.edit')) ticket: @formParam(@el.find('.edit'))
article: @formParam(@el.find('.article-add')) article: @articleNew.params()
# add attachments if exist # add attachments if exist
attachmentCount = @$('.article-add .textBubble .attachments .attachment').length attachmentCount = @$('.article-add .textBubble .attachments .attachment').length

View file

@ -28,107 +28,9 @@ class App.TicketZoomArticleNew extends App.Controller
constructor: -> constructor: ->
super super
# set possble article types
possibleArticleType =
note: true
phone: true
if @ticket && @ticket.create_article_type_id
articleTypeCreate = App.TicketArticleType.find(@ticket.create_article_type_id).name
if articleTypeCreate is 'twitter status'
possibleArticleType['twitter status'] = true
else if articleTypeCreate is 'twitter direct-message'
possibleArticleType['twitter direct-message'] = true
else if articleTypeCreate is 'email'
possibleArticleType['email'] = true
else if articleTypeCreate is 'facebook feed post'
possibleArticleType['facebook feed comment'] = true
else if articleTypeCreate is 'telegram personal-message'
possibleArticleType['telegram personal-message'] = true
if @ticket && @ticket.customer_id
customer = App.User.find(@ticket.customer_id)
if customer.email
possibleArticleType['email'] = true
# gets referenced in @setArticleType
@internalSelector = true @internalSelector = true
@type = @defaults['type'] || 'note' @type = @defaults['type'] || 'note'
@articleTypes = [] @setPossibleArticleTypes()
if possibleArticleType.note
internal = @Config.get('ui_ticket_zoom_article_new_internal')
@articleTypes.push {
name: 'note'
icon: 'note'
attributes: []
internal: internal,
features: ['attachment']
}
if possibleArticleType.email
@articleTypes.push {
name: 'email'
icon: 'email'
attributes: ['to', 'cc']
internal: false,
features: ['attachment']
}
if possibleArticleType['facebook feed comment']
@articleTypes.push {
name: 'facebook feed comment'
icon: 'facebook'
attributes: []
internal: false,
features: []
}
if possibleArticleType['twitter status']
@articleTypes.push {
name: 'twitter status'
icon: 'twitter'
attributes: []
internal: false,
features: ['body:limit', 'body:initials']
maxTextLength: 140
warningTextLength: 30
}
if possibleArticleType['twitter direct-message']
@articleTypes.push {
name: 'twitter direct-message'
icon: 'twitter'
attributes: ['to']
internal: false,
features: ['body:limit', 'body:initials']
maxTextLength: 10000
warningTextLength: 500
}
if possibleArticleType.phone
@articleTypes.push {
name: 'phone'
icon: 'phone'
attributes: []
internal: false,
features: ['attachment']
}
if possibleArticleType['telegram personal-message']
@articleTypes.push {
name: 'telegram personal-message'
icon: 'telegram'
attributes: []
internal: false,
features: ['attachment']
maxTextLength: 10000
warningTextLength: 5000
}
if @permissionCheck('ticket.customer')
@type = 'note'
@articleTypes = [
{
name: 'note'
icon: 'note'
attributes: []
internal: false,
features: ['attachment']
},
]
if @permissionCheck('ticket.customer') if @permissionCheck('ticket.customer')
@internalSelector = false @internalSelector = false
@ -181,6 +83,114 @@ class App.TicketZoomArticleNew extends App.Controller
@render() @render()
) )
setPossibleArticleTypes: =>
possibleArticleType =
note: true
phone: true
if @ticket && @ticket.create_article_type_id
articleTypeCreate = App.TicketArticleType.find(@ticket.create_article_type_id).name
if articleTypeCreate is 'twitter status'
possibleArticleType['twitter status'] = true
else if articleTypeCreate is 'twitter direct-message'
possibleArticleType['twitter direct-message'] = true
else if articleTypeCreate is 'email'
possibleArticleType['email'] = true
else if articleTypeCreate is 'facebook feed post'
possibleArticleType['facebook feed comment'] = true
else if articleTypeCreate is 'telegram personal-message'
possibleArticleType['telegram personal-message'] = true
if @ticket && @ticket.customer_id
customer = App.User.find(@ticket.customer_id)
if customer.email
possibleArticleType['email'] = true
# gets referenced in @setArticleType
@articleTypes = []
if possibleArticleType.note
internal = @Config.get('ui_ticket_zoom_article_note_new_internal')
@articleTypes.push {
name: 'note'
icon: 'note'
attributes: []
internal: internal,
features: ['attachment']
}
if possibleArticleType.email
attributes = ['to', 'cc', 'subject']
if !@Config.get('ui_ticket_zoom_article_email_subject')
attributes = ['to', 'cc']
@articleTypes.push {
name: 'email'
icon: 'email'
attributes: attributes
internal: false,
features: ['attachment']
}
if possibleArticleType['facebook feed comment']
@articleTypes.push {
name: 'facebook feed comment'
icon: 'facebook'
attributes: []
internal: false,
features: []
}
if possibleArticleType['twitter status']
attributes = ['body:limit', 'body:initials']
if !@Config.get('ui_ticket_zoom_article_twitter_initials')
attributes = ['body:limit']
@articleTypes.push {
name: 'twitter status'
icon: 'twitter'
attributes: []
internal: false,
features: ['body:limit', 'body:initials']
maxTextLength: 140
warningTextLength: 30
}
if possibleArticleType['twitter direct-message']
attributes = ['body:limit', 'body:initials']
if !@Config.get('ui_ticket_zoom_article_twitter_initials')
attributes = ['body:limit']
@articleTypes.push {
name: 'twitter direct-message'
icon: 'twitter'
attributes: ['to']
internal: false,
features: ['body:limit', 'body:initials']
maxTextLength: 10000
warningTextLength: 500
}
if possibleArticleType.phone
@articleTypes.push {
name: 'phone'
icon: 'phone'
attributes: []
internal: false,
features: ['attachment']
}
if possibleArticleType['telegram personal-message']
@articleTypes.push {
name: 'telegram personal-message'
icon: 'telegram'
attributes: []
internal: false,
features: ['attachment']
maxTextLength: 10000
warningTextLength: 5000
}
if @permissionCheck('ticket.customer')
@type = 'note'
@articleTypes = [
{
name: 'note'
icon: 'note'
attributes: []
internal: false,
features: ['attachment']
},
]
placeCaretAtEnd: (el) -> placeCaretAtEnd: (el) ->
el.focus() el.focus()
if typeof window.getSelection isnt 'undefined' && typeof document.createRange isnt 'undefined' if typeof window.getSelection isnt 'undefined' && typeof document.createRange isnt 'undefined'
@ -318,9 +328,6 @@ class App.TicketZoomArticleNew extends App.Controller
params.form_id = @form_id params.form_id = @form_id
params.content_type = 'text/html' params.content_type = 'text/html'
if !params['internal']
params['internal'] = false
if @permissionCheck('ticket.customer') if @permissionCheck('ticket.customer')
sender = App.TicketArticleSender.findByAttribute('name', 'Customer') sender = App.TicketArticleSender.findByAttribute('name', 'Customer')
type = App.TicketArticleType.findByAttribute('name', 'web') type = App.TicketArticleType.findByAttribute('name', 'web')
@ -332,6 +339,11 @@ class App.TicketZoomArticleNew extends App.Controller
params.sender_id = sender.id params.sender_id = sender.id
params.type_id = type.id params.type_id = type.id
if params.internal
params.internal = true
else
params.internal = false
if params.type is 'twitter status' if params.type is 'twitter status'
App.Utils.htmlRemoveRichtext(@$('[data-name=body]'), false) App.Utils.htmlRemoveRichtext(@$('[data-name=body]'), false)
params.content_type = 'text/plain' params.content_type = 'text/plain'
@ -478,6 +490,8 @@ class App.TicketZoomArticleNew extends App.Controller
@articleNewEdit.attr('data-type', type) @articleNewEdit.attr('data-type', type)
@$('.js-selectableTypes').addClass('hide').filter("[data-type='#{type}']").removeClass('hide') @$('.js-selectableTypes').addClass('hide').filter("[data-type='#{type}']").removeClass('hide')
@setPossibleArticleTypes()
# get config # get config
config = {} config = {}
for articleTypeConfig in @articleTypes for articleTypeConfig in @articleTypes

View file

@ -0,0 +1,35 @@
class App.TicketZoomSetting extends App.Controller
events:
'click .js-setting': 'show'
constructor: ->
super
return if !@permissionCheck('admin')
@render()
render: ->
@html(App.view('ticket_zoom/setting')())
show: ->
new Modal()
class Modal extends App.ControllerModal
buttonClose: true
buttonCancel: true
buttonSubmit: false
head: 'Settings'
constructor: ->
super
render: =>
super
post: =>
new App.SettingsArea(
area: 'UI::TicketZoom'
el: @el.find('.modal-body')
)
content: ->
App.view('generic/page_loading')()

View file

@ -96,12 +96,15 @@ class _i18nSingleton extends Spine.Module
# prepare locale # prepare locale
localeToSet = localeToSet.toLowerCase() localeToSet = localeToSet.toLowerCase()
dirToSet = 'ltr'
# check if locale exists # check if locale exists
localeFound = false localeFound = false
locales = App.Locale.all() locales = App.Locale.all()
for locale in locales for locale in locales
if locale.locale is localeToSet if locale.locale is localeToSet
localeToSet = locale.locale
dirToSet = locale.dir
localeFound = true localeFound = true
# try aliases # try aliases
@ -109,6 +112,8 @@ class _i18nSingleton extends Spine.Module
for locale in locales for locale in locales
if locale.alias is localeToSet if locale.alias is localeToSet
localeToSet = locale.locale localeToSet = locale.locale
dirToSet = locale.dir
localeFound = true
# if no locale and no alias was found, try to find correct one # if no locale and no alias was found, try to find correct one
if !localeFound if !localeFound
@ -118,13 +123,7 @@ class _i18nSingleton extends Spine.Module
for locale in locales for locale in locales
if locale.alias is localeToSet if locale.alias is localeToSet
localeToSet = locale.locale localeToSet = locale.locale
localeFound = true dirToSet = locale.dir
# try to find by locale
if !localeFound
for locale in locales
if locale.locale is localeToSet
localeToSet = locale.locale
localeFound = true localeFound = true
# check if locale need to be changed # check if locale need to be changed
@ -136,8 +135,9 @@ class _i18nSingleton extends Spine.Module
# set if not translated should be logged # set if not translated should be logged
@_notTranslatedLog = @notTranslatedFeatureEnabled(@locale) @_notTranslatedLog = @notTranslatedFeatureEnabled(@locale)
# set lang attribute of html tag # set lang and dir attribute of html tag
$('html').prop('lang', @locale.substr(0, 2) ) $('html').prop('lang', localeToSet.substr(0, 2))
$('html').prop('dir', dirToSet)
@mapString = {} @mapString = {}
App.Ajax.request( App.Ajax.request(

View file

@ -12,6 +12,7 @@ class App.SearchableSelect extends Spine.Controller
'mouseenter .js-back': 'highlightItem' 'mouseenter .js-back': 'highlightItem'
'shown.bs.dropdown': 'onDropdownShown' 'shown.bs.dropdown': 'onDropdownShown'
'hidden.bs.dropdown': 'onDropdownHidden' 'hidden.bs.dropdown': 'onDropdownHidden'
'keyup .js-input': 'onKeyUp'
elements: elements:
'.js-dropdown': 'dropdown' '.js-dropdown': 'dropdown'
@ -120,6 +121,10 @@ class App.SearchableSelect extends Spine.Controller
$(document).off 'keydown.searchable_select' $(document).off 'keydown.searchable_select'
@isOpen = false @isOpen = false
onKeyUp: =>
return if @input.val().trim() isnt ''
@shadowInput.val('')
toggle: => toggle: =>
@currentItem = null @currentItem = null
@$('[data-toggle="dropdown"]').dropdown('toggle') @$('[data-toggle="dropdown"]').dropdown('toggle')

View file

@ -180,7 +180,7 @@
Plugin.prototype.renderBase = function() { Plugin.prototype.renderBase = function() {
this.$element.after('<div class="shortcut dropdown"><ul class="dropdown-menu" style="max-height: 200px;"></ul></div>') this.$element.after('<div class="shortcut dropdown"><ul class="dropdown-menu" style="max-height: 200px;"></ul></div>')
this.$widget = this.$element.next() this.$widget = this.$element.next()
this.$widget.on('click', 'li', $.proxy(this.onEntryClick, this)) this.$widget.on('mousedown', 'li', $.proxy(this.onEntryClick, this))
this.$widget.on('mouseenter', 'li', $.proxy(this.onMouseEnter, this)) this.$widget.on('mouseenter', 'li', $.proxy(this.onMouseEnter, this))
} }
@ -313,6 +313,7 @@
} }
Plugin.prototype.onEntryClick = function(event) { Plugin.prototype.onEntryClick = function(event) {
event.preventDefault()
var id = $(event.target).data('id') var id = $(event.target).data('id')
this.take(id) this.take(id)
} }

View file

@ -10,8 +10,20 @@
<div class="page-content"> <div class="page-content">
<p><%- @T('With form you can add a form to your web page which directly generates a ticket for you.') %></p> <p><%- @T('With form you can add a form to your web page which directly generates a ticket for you.') %></p>
<h2><%- @T('Settings') %></h2>
<form class="js-paramsSetting">
<fieldset>
<div class="input form-group formGroup--halfSize">
<div class="formGroup-label">
<label for="form-group"><%- @T('Group selection for Ticket creation') %></label>
</div>
<div class="controls js-groupSelector" id="from-group"></div>
</div>
</fieldset>
</form>
<h2><%- @T('Designer') %></h2> <h2><%- @T('Designer') %></h2>
<form class="js-params"> <form class="js-paramsDesigner">
<fieldset> <fieldset>
<div class="input form-group formGroup--halfSize"> <div class="input form-group formGroup--halfSize">

View file

@ -33,13 +33,13 @@
<ul> <ul>
<li><%- @T('%s user to %s user', 'LDAP', 'Zammad') %> (<%= @countDone %>/<%= @job.result.sum %>): <li><%- @T('%s user to %s user', 'LDAP', 'Zammad') %> (<%= @countDone %>/<%= @job.result.sum %>):
<ul> <ul>
<li><%- @T('Users') %>: <%= @job.result.created %> <%- @T('created') %>, <%= @job.result.updated %> <%- @T('updated') %>, <%= @job.result.unchanged %> <%- @T('untouched') %>, <%= @job.result.skipped %> <%- @T('skipped') %>, <%= @job.result.failed %> <%- @T('failed') %> <li><%- @T('Users') %>: <%= @job.result.created %> <%- @T('created') %>, <%= @job.result.updated %> <%- @T('updated') %>, <%= @job.result.unchanged %> <%- @T('untouched') %>, <%= @job.result.skipped %> <%- @T('skipped') %>, <%= @job.result.failed %> <%- @T('failed') %>, <%= @job.result.deactivated %> <%- @T('deactivated') %>
</ul> </ul>
<% if !_.isEmpty(@job.result.roles): %> <% if !_.isEmpty(@job.result.roles): %>
<li><%- @T('%s groups to %s roles assignments', 'LDAP', 'Zammad') %>: <li><%- @T('%s groups to %s roles assignments', 'LDAP', 'Zammad') %>:
<ul> <ul>
<% for role, result of @job.result.roles: %> <% for role, result of @job.result.roles: %>
<li> <%- @T(role) %>: <%= result.created %> <%- @T('created') %>, <%= result.updated %> <%- @T('updated') %>, <%= result.unchanged %> <%- @T('untouched') %>, <%= result.failed %> <%- @T('failed') %> <li> <%- @T(role) %>: <%= result.created %> <%- @T('created') %>, <%= result.updated %> <%- @T('updated') %>, <%= result.unchanged %> <%- @T('untouched') %>, <%= result.failed %> <%- @T('failed') %>, <%= result.deactivated %> <%- @T('deactivated') %>
<% end %> <% end %>
</ul> </ul>
<% end %> <% end %>

View file

@ -1,14 +1,14 @@
<ul> <ul>
<li><%- @T('%s user to %s user', 'LDAP', 'Zammad') %> (<%= @countDone %>): <li><%- @T('%s user to %s user', 'LDAP', 'Zammad') %> (<%= @countDone %>):
<ul> <ul>
<li><%- @T('Users') %>: <%= @job.result.created %> <%- @T('created') %>, <%= @job.result.updated %> <%- @T('updated') %>, <%= @job.result.unchanged %> <%- @T('untouched') %>, <%= @job.result.skipped %> <%- @T('skipped') %>, <%= @job.result.failed %> <%- @T('failed') %> <li><%- @T('Users') %>: <%= @job.result.created %> <%- @T('created') %>, <%= @job.result.updated %> <%- @T('updated') %>, <%= @job.result.unchanged %> <%- @T('untouched') %>, <%= @job.result.skipped %> <%- @T('skipped') %>, <%= @job.result.failed %> <%- @T('failed') %>, <%= @job.result.deactivated %> <%- @T('deactivated') %>
</ul> </ul>
</li> </li>
<% if !_.isEmpty(@job.result.roles): %> <% if !_.isEmpty(@job.result.roles): %>
<li><%- @T('%s groups to %s roles assignments', 'LDAP', 'Zammad') %>: <li><%- @T('%s groups to %s roles assignments', 'LDAP', 'Zammad') %>:
<ul> <ul>
<% for role, result of @job.result.roles: %> <% for role, result of @job.result.roles: %>
<li><%- @T(role) %>: <%= result.created %> <%- @T('created') %>, <%= result.updated %> <%- @T('updated') %>, <%= result.unchanged %> <%- @T('untouched') %>, <%= result.failed %> <%- @T('failed') %> <li><%- @T(role) %>: <%= result.created %> <%- @T('created') %>, <%= result.updated %> <%- @T('updated') %>, <%= result.unchanged %> <%- @T('untouched') %>, <%= result.failed %> <%- @T('failed') %>, <%= result.deactivated %> <%- @T('deactivated') %>
<% end %> <% end %>
</ul> </ul>
</li> </li>

View file

@ -19,7 +19,7 @@
<tbody> <tbody>
<tr> <tr>
<td class="settings-list-row-control"><%- @T('Host') %> <td class="settings-list-row-control"><%- @T('Host') %>
<td class="settings-list-control-cell"><input type="text" name="host_url" class="form-control form-control--small js-hostUrl" value="" placeholder="ldaps://ldap.example.com" autocomplete="new-password"> <td class="settings-list-control-cell"><input type="text" name="host_url" class="form-control form-control--small js-hostUrl" value="" placeholder="ldaps://ldap.example.com" autocomplete="off">
</tbody> </tbody>
</table> </table>
</div> </div>
@ -70,7 +70,7 @@
<td class="settings-list-control-cell js-baseDn"> <td class="settings-list-control-cell js-baseDn">
<tr> <tr>
<td class="settings-list-row-control"><%- @T('Bind User') %> <td class="settings-list-row-control"><%- @T('Bind User') %>
<td class="settings-list-control-cell"><input type="text" name="bind_user" class="form-control form-control--small" value="" placeholder="" autocomplete="new-password"> <td class="settings-list-control-cell"><input type="text" name="bind_user" class="form-control form-control--small" value="" placeholder="" autocomplete="off">
<tr> <tr>
<td class="settings-list-row-control"><%- @T('Bind Password') %> <td class="settings-list-row-control"><%- @T('Bind Password') %>
<td class="settings-list-control-cell"><input type="password" name="bind_pw" class="form-control form-control--small" value="" autocomplete="new-password"> <td class="settings-list-control-cell"><input type="password" name="bind_pw" class="form-control form-control--small" value="" autocomplete="new-password">
@ -203,7 +203,7 @@
<tbody> <tbody>
<tr> <tr>
<td class="settings-list-row-control"><%- @T('User filter') %> <td class="settings-list-row-control"><%- @T('User filter') %>
<td class="settings-list-control-cell"><input type="text" name="user_filter" class="form-control form-control--small" value="" placeholder="" autocomplete="new-password"> <td class="settings-list-control-cell"><input type="text" name="user_filter" class="form-control form-control--small" value="" placeholder="" autocomplete="off">
<tr> <tr>
<td class="settings-list-row-control"><%- @T('Users without assigned LDAP groups') %> <td class="settings-list-row-control"><%- @T('Users without assigned LDAP groups') %>
<td class="settings-list-control-cell js-unassignedUsers"> <td class="settings-list-control-cell js-unassignedUsers">

View file

@ -1,21 +1,22 @@
<div class="tabsSidebar-holder"> <div class="tabsSidebar-holder">
<div class="scrollPageHeader tabsSidebar-sidebarSpacer" style="right: <%- @scrollbarWidth %>px"> <div class="scrollPageHeader tabsSidebar-sidebarSpacer" style="right: <%- @scrollbarWidth %>px">
<small><%- @C('ticket_hook') %> <span class="ticket-number"><%- @ticket.number %></span></small> <small><%- @C('ticket_hook') %> <span class="ticket-number"><%- @ticket.number %></span></small>
<div class="ticket-title"></div> <div class="js-ticketTitleContainer ticket-title"></div>
<div class="highlighter"></div> <div class="js-highlighterContainer highlighter"></div>
<div class="overview-navigator"></div> <div class="js-overviewNavigatorContainer overview-navigator"></div>
</div> </div>
<div class="main no-padding flex tabsSidebar-sidebarSpacer tabsSidebar-tabsSpacer"> <div class="main no-padding flex tabsSidebar-sidebarSpacer tabsSidebar-tabsSpacer">
<div class="ticketZoom"> <div class="ticketZoom">
<div class="ticketZoom-controls"> <div class="ticketZoom-controls">
<div class="highlighter"></div> <div class="js-settingContainer"></div>
<div class="overview-navigator"></div> <div class="js-highlighterContainer highlighter"></div>
<div class="js-overviewNavigatorContainer overview-navigator"></div>
</div> </div>
<div class="ticketZoom-header"> <div class="ticketZoom-header">
<div class="flex vertical center"> <div class="flex vertical center">
<div class="js-avatar"></div> <div class="js-avatar"></div>
<div class="ticket-title"></div> <div class="js-ticketTitleContainer ticket-title"></div>
<div class="ticket-meta"></div> <div class="js-ticketMetaContainer"></div>
</div> </div>
</div> </div>
<div class="ticket-article"></div> <div class="ticket-article"></div>

View file

@ -41,13 +41,19 @@
<div class="formGroup-label"> <div class="formGroup-label">
<label for=""><%- @T('To') %></label> <label for=""><%- @T('To') %></label>
</div> </div>
<div class="controls"><input id="" type="text" name="to" value="<%= @article.to %>" class="form-control js-mail-inputs" required="required"></div> <div class="controls"><input type="text" name="to" value="<%= @article.to %>" class="form-control js-mail-inputs" required="required"></div>
</div> </div>
<div class="input form-group"> <div class="input form-group">
<div class="formGroup-label"> <div class="formGroup-label">
<label for=""><%- @T('Cc') %></label> <label for=""><%- @T('Cc') %></label>
</div> </div>
<div class="controls"><input id="" type="text" name="cc" value="<%= @article.cc %>" class="form-control js-mail-inputs"></div> <div class="controls"><input type="text" name="cc" value="<%= @article.cc %>" class="form-control js-mail-inputs"></div>
</div>
<div class="input form-group">
<div class="formGroup-label">
<label for=""><%- @T('Subject') %></label>
</div>
<div class="controls"><input type="text" name="subject" value="<%= @article.subject %>" class="form-control js-mail-inputs2"></div>
</div> </div>
<div class="textBubble js-writeArea"> <div class="textBubble js-writeArea">

View file

@ -0,0 +1,3 @@
<div class="btn btn--action btn--split--first js-setting centered">
<%- @Icon('cog', 'dropdown-icon') %>
</div>

View file

@ -13,6 +13,8 @@
<th><%- @T('Agent') %> <th><%- @T('Agent') %>
<th><%- @T('Time Units') %> <th><%- @T('Time Units') %>
<th><%- @T('Time Units Total') %> <th><%- @T('Time Units Total') %>
<th><%- @T('Created at') %>
<th><%- @T('Closed at') %>
</thead> </thead>
<tbody> <tbody>
<% for row in @rows: %> <% for row in @rows: %>
@ -24,6 +26,8 @@
<td><%= row.agent %> <td><%= row.agent %>
<td><%= row.time_unit %> <td><%= row.time_unit %>
<td><%= row.ticket.time_unit %> <td><%= row.ticket.time_unit %>
<td><%- @humanTime(row.ticket.created_at) %>
<td><%- @humanTime(row.ticket.close_at) %>
<% end %> <% end %>
</tbody> </tbody>
</table> </table>

View file

@ -7,6 +7,8 @@ class FormController < ApplicationController
def config def config
return if !enabled? return if !enabled?
return if !fingerprint_exists?
return if limit_reached?
api_path = Rails.configuration.api_path api_path = Rails.configuration.api_path
http_type = Setting.get('http_type') http_type = Setting.get('http_type')
@ -17,6 +19,7 @@ class FormController < ApplicationController
config = { config = {
enabled: Setting.get('form_ticket_create'), enabled: Setting.get('form_ticket_create'),
endpoint: endpoint, endpoint: endpoint,
token: token_gen(params[:fingerprint])
} }
if params[:test] && current_user && current_user.permissions?('admin.channel_formular') if params[:test] && current_user && current_user.permissions?('admin.channel_formular')
@ -28,35 +31,35 @@ class FormController < ApplicationController
def submit def submit
return if !enabled? return if !enabled?
return if !fingerprint_exists?
return if !token_valid?(params[:token], params[:fingerprint])
return if limit_reached?
# validate input # validate input
errors = {} errors = {}
if !params[:name] || params[:name].empty? if params[:name].blank?
errors['name'] = 'required' errors['name'] = 'required'
end end
if !params[:email] || params[:email].empty? if params[:email].blank?
errors['email'] = 'required' errors['email'] = 'required'
end elsif params[:email] !~ /@/
if params[:email] !~ /@/ errors['email'] = 'invalid'
elsif params[:email] =~ /(>|<|\||\!|"|§|'|\$|%|&|\(|\)|\?|\s|\.\.)/
errors['email'] = 'invalid' errors['email'] = 'invalid'
end end
if params[:email] =~ /(>|<|\||\!|"|§|'|\$|%|&|\(|\)|\?|\s)/ if params[:title].blank?
errors['email'] = 'invalid'
end
if !params[:title] || params[:title].empty?
errors['title'] = 'required' errors['title'] = 'required'
end end
if !params[:body] || params[:body].empty? if params[:body].blank?
errors['body'] = 'required' errors['body'] = 'required'
end end
# realtime verify # realtime verify
if !errors['email'] if errors['email'].blank?
begin begin
checker = EmailVerifier::Checker.new(params[:email]) address = ValidEmail2::Address.new(params[:email])
checker.connect if !address || !address.valid? || !address.valid_mx?
if !checker.verify errors['email'] = 'invalid'
errors['email'] = "Unable to send to '#{params[:email]}'"
end end
rescue => e rescue => e
message = e.to_s message = e.to_s
@ -69,7 +72,7 @@ class FormController < ApplicationController
end end
end end
if errors && !errors.empty? if errors.present?
render json: { render json: {
errors: errors errors: errors
}, status: :ok }, status: :ok
@ -86,7 +89,6 @@ class FormController < ApplicationController
firstname: name, firstname: name,
lastname: '', lastname: '',
email: email, email: email,
password: '',
active: true, active: true,
role_ids: role_ids, role_ids: role_ids,
updated_by_id: 1, updated_by_id: 1,
@ -97,10 +99,23 @@ class FormController < ApplicationController
# set current user # set current user
UserInfo.current_user_id = customer.id UserInfo.current_user_id = customer.id
group = Group.find_by(id: Setting.get('form_ticket_create_group_id'))
if !group
group = Group.where(active: true).first
if !group
group = Group.first
end
end
ticket = Ticket.create!( ticket = Ticket.create!(
group_id: 1, group_id: group.id,
customer_id: customer.id, customer_id: customer.id,
title: params[:title], title: params[:title],
preferences: {
form: {
remote_ip: request.remote_ip,
fingerprint_md5: Digest::MD5.hexdigest(params[:fingerprint]),
}
}
) )
article = Ticket::Article.create!( article = Ticket::Article.create!(
ticket_id: ticket.id, ticket_id: ticket.id,
@ -138,6 +153,91 @@ class FormController < ApplicationController
private private
def token_gen(fingerprint)
crypt = ActiveSupport::MessageEncryptor.new(Setting.get('application_secret'))
fingerprint = "#{Base64.strict_encode64(Setting.get('fqdn'))}:#{Time.zone.now.to_i}:#{Base64.strict_encode64(fingerprint)}"
Base64.strict_encode64(crypt.encrypt_and_sign(fingerprint))
end
def token_valid?(token, fingerprint)
if token.blank?
Rails.logger.info 'No token for form!'
response_access_deny
return false
end
begin
crypt = ActiveSupport::MessageEncryptor.new(Setting.get('application_secret'))
result = crypt.decrypt_and_verify(Base64.decode64(token))
rescue
Rails.logger.info 'Invalid token for form!'
response_access_deny
return false
end
if result.blank?
Rails.logger.info 'Invalid token for form!'
response_access_deny
return false
end
parts = result.split(/:/)
if parts.count != 3
Rails.logger.info "Invalid token for form (need to have 3 parts, only #{parts.count} found)!"
response_access_deny
return false
end
fqdn_local = Base64.decode64(parts[0])
if fqdn_local != Setting.get('fqdn')
Rails.logger.info "Invalid token for form (invalid fqdn found #{fqdn_local} != #{Setting.get('fqdn')})!"
response_access_deny
return false
end
fingerprint_local = Base64.decode64(parts[2])
if fingerprint_local != fingerprint
Rails.logger.info "Invalid token for form (invalid fingerprint found #{fingerprint_local} != #{fingerprint})!"
response_access_deny
return false
end
if parts[1].to_i < (Time.zone.now.to_i - 60 * 60 * 24)
Rails.logger.info 'Invalid token for form (token expired})!'
response_access_deny
return false
end
true
end
def limit_reached?
return false if !SearchIndexBackend.enabled?
form_limit_by_ip_per_hour = Setting.get('form_ticket_create_by_ip_per_hour') || 20
result = SearchIndexBackend.search("preferences.form.remote_ip:'#{request.remote_ip}' AND created_at:>now-1h", form_limit_by_ip_per_hour, 'Ticket')
if result.count >= form_limit_by_ip_per_hour.to_i
response_access_deny
return true
end
form_limit_by_ip_per_day = Setting.get('form_ticket_create_by_ip_per_day') || 240
result = SearchIndexBackend.search("preferences.form.remote_ip:'#{request.remote_ip}' AND created_at:>now-1d", form_limit_by_ip_per_day, 'Ticket')
if result.count >= form_limit_by_ip_per_day.to_i
response_access_deny
return true
end
form_limit_per_day = Setting.get('form_ticket_create_per_day') || 5000
result = SearchIndexBackend.search('preferences.form.remote_ip:* AND created_at:>now-1d', form_limit_per_day, 'Ticket')
if result.count >= form_limit_per_day.to_i
response_access_deny
return true
end
false
end
def fingerprint_exists?
return true if params[:fingerprint].present? && params[:fingerprint].length > 30
Rails.logger.info 'No fingerprint given!'
response_access_deny
false
end
def enabled? def enabled?
return true if params[:test] && current_user && current_user.permissions?('admin.channel_formular') return true if params[:test] && current_user && current_user.permissions?('admin.channel_formular')
return true if Setting.get('form_ticket_create') return true if Setting.get('form_ticket_create')

View file

@ -93,9 +93,84 @@ class TimeAccountingsController < ApplicationController
name: 'Time Units Total', name: 'Time Units Total',
width: 10, width: 10,
}, },
{
name: 'Created at',
width: 10,
},
{
name: 'Closed at',
width: 10,
},
{
name: 'Close Escalation At',
width: 10,
},
{
name: 'Close In Min',
width: 10,
},
{
name: 'Close Diff In Min',
width: 10,
},
{
name: 'First Response At',
width: 10,
},
{
name: 'First Response Escalation At',
width: 10,
},
{
name: 'First Response In Min',
width: 10,
},
{
name: 'First Response Diff In Min',
width: 10,
},
{
name: 'Update Escalation At',
width: 10,
},
{
name: 'Update In Min',
width: 10,
},
{
name: 'Update Diff In Min',
width: 10,
},
{
name: 'Last Contact At',
width: 10,
},
{
name: 'Last Contact Agent At',
width: 10,
},
{
name: 'Last Contact Customer At',
width: 10,
},
{
name: 'Article Count',
width: 10,
},
{
name: 'Escalation At',
width: 10,
},
] ]
result = [] result = []
results.each { |row| results.each { |row|
row[:ticket].keys.each { |field|
next if row[:ticket][field].blank?
next if !row[:ticket][field].is_a?(ActiveSupport::TimeWithZone)
row[:ticket][field] = row[:ticket][field].iso8601
}
result_row = [ result_row = [
row[:ticket]['number'], row[:ticket]['number'],
row[:ticket]['title'], row[:ticket]['title'],
@ -104,6 +179,23 @@ class TimeAccountingsController < ApplicationController
row[:agent], row[:agent],
row[:time_unit], row[:time_unit],
row[:ticket]['time_unit'], row[:ticket]['time_unit'],
row[:ticket]['created_at'],
row[:ticket]['close_at'],
row[:ticket]['close_escalation_at'],
row[:ticket]['close_in_min'],
row[:ticket]['close_diff_in_min'],
row[:ticket]['first_response_at'],
row[:ticket]['first_response_escalation_at'],
row[:ticket]['first_response_in_min'],
row[:ticket]['first_response_diff_in_min'],
row[:ticket]['update_escalation_at'],
row[:ticket]['update_in_min'],
row[:ticket]['update_diff_in_min'],
row[:ticket]['last_contact_at'],
row[:ticket]['last_contact_agent_at'],
row[:ticket]['last_contact_customer_at'],
row[:ticket]['article_count'],
row[:ticket]['escalation_at'],
] ]
result.push result_row result.push result_row
} }

View file

@ -504,7 +504,7 @@ condition example
query += "#{attribute} IN (?)" query += "#{attribute} IN (?)"
bind_params.push 1 bind_params.push 1
else else
query += "#{attribute} IS NOT NULL" query += "#{attribute} IS NULL"
end end
elsif selector['pre_condition'] == 'current_user.id' elsif selector['pre_condition'] == 'current_user.id'
raise "Use current_user.id in selector, but no current_user is set #{selector.inspect}" if !current_user_id raise "Use current_user.id in selector, but no current_user is set #{selector.inspect}" if !current_user_id
@ -518,7 +518,7 @@ condition example
else else
# rubocop:disable Style/IfInsideElse # rubocop:disable Style/IfInsideElse
if selector['value'].nil? if selector['value'].nil?
query += "#{attribute} IS NOT NULL" query += "#{attribute} IS NULL"
else else
query += "#{attribute} IN (?)" query += "#{attribute} IN (?)"
bind_params.push selector['value'] bind_params.push selector['value']
@ -531,7 +531,7 @@ condition example
query += "#{attribute} NOT IN (?)" query += "#{attribute} NOT IN (?)"
bind_params.push 1 bind_params.push 1
else else
query += "#{attribute} IS NULL" query += "#{attribute} IS NOT NULL"
end end
elsif selector['pre_condition'] == 'current_user.id' elsif selector['pre_condition'] == 'current_user.id'
query += "#{attribute} NOT IN (?)" query += "#{attribute} NOT IN (?)"
@ -825,9 +825,11 @@ perform changes on ticket
# loop protection / check if maximal count of trigger mail has reached # loop protection / check if maximal count of trigger mail has reached
map = { map = {
10 => 10,
30 => 15, 30 => 15,
60 => 25, 60 => 25,
180 => 50, 180 => 50,
600 => 100,
} }
skip = false skip = false
map.each { |minutes, count| map.each { |minutes, count|
@ -843,18 +845,20 @@ perform changes on ticket
} }
next if skip next if skip
map = { map = {
1 => 150, 10 => 30,
3 => 250, 30 => 60,
6 => 450, 60 => 120,
180 => 240,
600 => 360,
} }
skip = false skip = false
map.each { |hours, count| map.each { |minutes, count|
already_sent = Ticket::Article.where( already_sent = Ticket::Article.where(
sender: Ticket::Article::Sender.find_by(name: 'System'), sender: Ticket::Article::Sender.find_by(name: 'System'),
type: Ticket::Article::Type.find_by(name: 'email'), type: Ticket::Article::Type.find_by(name: 'email'),
).where("ticket_articles.created_at > ? AND ticket_articles.to LIKE '%#{recipient_email.strip}%'", Time.zone.now - hours.hours).count ).where("ticket_articles.created_at > ? AND ticket_articles.to LIKE '%#{recipient_email.strip}%'", Time.zone.now - minutes.minutes).count
next if already_sent < count next if already_sent < count
logger.info "Send no trigger based notification to #{recipient_email} because already sent #{count} in total within last #{hours} hour(s) (loop protection)" logger.info "Send no trigger based notification to #{recipient_email} because already sent #{count} in total within last #{minutes} minutes (loop protection)"
skip = true skip = true
break break
} }

View file

@ -1,4 +1,5 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
require 'csv'
class Translation < ApplicationModel class Translation < ApplicationModel
before_create :set_initial before_create :set_initial
@ -212,7 +213,7 @@ translate strings in ruby context, e. g. for notifications
=begin =begin
load locales from local load translations from local
all: all:
@ -282,6 +283,65 @@ all:
true true
end end
=begin
load translations from csv file
all:
Translation.load_from_csv
or
Translation.load_from_csv(locale, file_location, file_charset) # e. g. 'en-us' or 'de-de' and /path/to/translation_list.csv
e. g.
Translation.load_from_csv('he-il', '/Users/me/Downloads/Hebrew_translation_list-1.csv', 'Windows-1255')
Get source file at https://i18n.zammad.com/api/v1/translations_empty_translation_list
=end
def self.load_from_csv(locale_name, location, charset = 'UTF8')
locale = Locale.find_by(locale: locale_name)
if !locale
raise "No such locale: #{locale_name}"
end
if !::File.exist?(location)
raise "No such file: #{location}"
end
content = ::File.open(location, "r:#{charset}").read
params = {
col_sep: ',',
}
rows = ::CSV.parse(content, params)
header = rows.shift
translation_raw = []
rows.each { |row|
raise "Can't import translation, source is missing" if row[0].blank?
if row[1].blank?
warn "Skipped #{row[0]}, because translation is blank"
next
end
raise "Can't import translation, format is missing" if row[2].blank?
raise "Can't import translation, format is invalid (#{row[2]})" if row[2] !~ /^(time|string)$/
item = {
'locale' => locale.locale,
'source' => row[0],
'target' => row[1],
'target_initial' => '',
'format' => row[2],
}
translation_raw.push item
}
to_database(locale.name, translation_raw)
true
end
private_class_method def self.to_database(locale, data) private_class_method def self.to_database(locale, data)
translations = Translation.where(locale: locale).all translations = Translation.where(locale: locale).all
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do

View file

@ -21,8 +21,9 @@ store new device for user if device not already known
def self.add(user_agent, ip, user_id, fingerprint, type) def self.add(user_agent, ip, user_id, fingerprint, type)
# since gem browser 2 is not handling nil for user_agent, set it to '' if user_agent.blank?
user_agent ||= '' user_agent = 'unknown'
end
# get location info # get location info
location_details = Service::GeoIp.location(ip) location_details = Service::GeoIp.location(ip)
@ -60,6 +61,8 @@ store new device for user if device not already known
end end
# get browser details # get browser details
browser = {}
if user_agent != 'unknown'
browser = Browser.new(user_agent, accept_language: 'en-us') browser = Browser.new(user_agent, accept_language: 'en-us')
browser = { browser = {
plattform: browser.platform.to_s.camelize, plattform: browser.platform.to_s.camelize,
@ -67,16 +70,17 @@ store new device for user if device not already known
version: browser.version, version: browser.version,
full_version: browser.full_version, full_version: browser.full_version,
} }
end
# generate device name # generate device name
if browser[:name] == 'Generic Browser' if browser[:name] == 'Generic Browser'
browser[:name] = user_agent browser[:name] = user_agent
end end
name = '' name = ''
if browser[:plattform] && browser[:plattform] != 'Other' if browser[:plattform].present? && browser[:plattform] != 'Other'
name = browser[:plattform] name = browser[:plattform]
end end
if browser[:name] && browser[:name] != 'Other' if browser[:name].present? && browser[:name] != 'Other'
if name.present? if name.present?
name += ', ' name += ', '
end end
@ -84,7 +88,7 @@ store new device for user if device not already known
end end
# if not identified, use user agent # if not identified, use user agent
if !name || name == '' || name == 'Other, Other' || name == 'Other' if name.blank? || name == 'Other, Other' || name == 'Other'
name = user_agent name = user_agent
browser[:name] = user_agent browser[:name] = user_agent
end end
@ -103,7 +107,7 @@ store new device for user if device not already known
end end
# create new device # create new device
user_device = create( user_device = create!(
user_id: user_id, user_id: user_id,
name: name, name: name,
os: browser[:plattform], os: browser[:plattform],

View file

@ -10,7 +10,7 @@ Dir.chdir APP_ROOT do
puts '== Installing dependencies ==' puts '== Installing dependencies =='
system 'gem install bundler --conservative' system 'gem install bundler --conservative'
system 'bundle check || bundle install' system 'bundle check || bundle install --jobs 8'
# puts "\n== Copying sample files ==" # puts "\n== Copying sample files =="
# unless File.exist?("config/database.yml") # unless File.exist?("config/database.yml")

View file

@ -40,6 +40,7 @@ Rails.application.config.html_sanitizer_attributes_whitelist = {
'table' => %w(align bgcolor border cellpadding cellspacing frame rules sortable summary width style), 'table' => %w(align bgcolor border cellpadding cellspacing frame rules sortable summary width style),
'td' => %w(abbr align axis colspan headers rowspan valign width style), 'td' => %w(abbr align axis colspan headers rowspan valign width style),
'th' => %w(abbr align axis colspan headers rowspan scope sorted valign width style), 'th' => %w(abbr align axis colspan headers rowspan scope sorted valign width style),
'tr' => %w(width style),
'ul' => %w(type), 'ul' => %w(type),
'q' => %w(cite), 'q' => %w(cite),
'span' => %w(style), 'span' => %w(style),

View file

@ -3,6 +3,6 @@ Zammad::Application.routes.draw do
# forms # forms
match api_path + '/form_submit', to: 'form#submit', via: :post match api_path + '/form_submit', to: 'form#submit', via: :post
match api_path + '/form_config', to: 'form#config', via: :get match api_path + '/form_config', to: 'form#config', via: :post
end end

View file

@ -209,6 +209,7 @@ class CreateBase < ActiveRecord::Migration
t.string :locale, limit: 20, null: false t.string :locale, limit: 20, null: false
t.string :alias, limit: 20, null: true t.string :alias, limit: 20, null: true
t.string :name, limit: 255, null: false t.string :name, limit: 255, null: false
t.string :dir, limit: 9, null: false, default: 'ltr'
t.boolean :active, null: false, default: true t.boolean :active, null: false, default: true
t.timestamps limit: 3, null: false t.timestamps limit: 3, null: false
end end

View file

@ -1,5 +1,9 @@
class TreeSelect < ActiveRecord::Migration class TreeSelect < ActiveRecord::Migration
def up def up
# return if it's a new setup
return if !Setting.find_by(name: 'system_init_done')
change_column :object_manager_attributes, :data_option, :text, limit: 800.kilobytes + 1, null: true change_column :object_manager_attributes, :data_option, :text, limit: 800.kilobytes + 1, null: true
change_column :object_manager_attributes, :data_option_new, :text, limit: 800.kilobytes + 1, null: true change_column :object_manager_attributes, :data_option_new, :text, limit: 800.kilobytes + 1, null: true
end end

View file

@ -0,0 +1,9 @@
class LocaleAddDirection < ActiveRecord::Migration
def up
# return if it's a new setup
return if !Setting.find_by(name: 'system_init_done')
add_column :locales, :dir, :string, limit: 9, null: false, default: 'ltr'
end
end

View file

@ -0,0 +1,103 @@
class FormGroupSelection < ActiveRecord::Migration
def up
# return if it's a new setup
return if !Setting.find_by(name: 'system_init_done')
group = Group.where(active: true).first
if !group
group = Group.first
end
group_id = 1
if group
group_id = group.id
end
Setting.create_if_not_exists(
title: 'Group selection for Ticket creation',
name: 'form_ticket_create_group_id',
area: 'Form::Base',
description: 'Defines if group of created tickets via web form.',
options: {
form: [
{
display: '',
null: true,
name: 'form_ticket_create_group_id',
tag: 'select',
relation: 'Group',
},
],
},
state: group_id,
preferences: {
permission: ['admin.channel_formular'],
},
frontend: false,
)
Setting.create_if_not_exists(
title: 'Limit tickets by ip per hour',
name: 'form_ticket_create_by_ip_per_hour',
area: 'Form::Base',
description: 'Defines limit of tickets by ip per hour via web form.',
options: {
form: [
{
display: '',
null: true,
name: 'form_ticket_create_by_ip_per_hour',
tag: 'input',
},
],
},
state: 20,
preferences: {
permission: ['admin.channel_formular'],
},
frontend: false,
)
Setting.create_if_not_exists(
title: 'Limit tickets by ip per day',
name: 'form_ticket_create_by_ip_per_day',
area: 'Form::Base',
description: 'Defines limit of tickets by ip per day via web form.',
options: {
form: [
{
display: '',
null: true,
name: 'form_ticket_create_by_ip_per_day',
tag: 'input',
},
],
},
state: 240,
preferences: {
permission: ['admin.channel_formular'],
},
frontend: false,
)
Setting.create_if_not_exists(
title: 'Limit tickets per day',
name: 'form_ticket_create_per_day',
area: 'Form::Base',
description: 'Defines limit of tickets per day via web form.',
options: {
form: [
{
display: '',
null: true,
name: 'form_ticket_create_per_day',
tag: 'input',
},
],
},
state: 5000,
preferences: {
permission: ['admin.channel_formular'],
},
frontend: false,
)
end
end

View file

@ -0,0 +1,99 @@
class TicketZoomSetting < ActiveRecord::Migration
def up
# return if it's a new setup
return if !Setting.find_by(name: 'system_init_done')
setting = Setting.find_by(name: 'ui_ticket_zoom_article_new_internal')
if setting
setting.title = 'Note - default visibility'
setting.name = 'ui_ticket_zoom_article_note_new_internal'
setting.description = 'Default visibility for new articles.'
setting.preferences[:prio] = 100
setting.options[:form][0][:name] = 'ui_ticket_zoom_article_note_new_internal'
setting.save!
end
Setting.create_if_not_exists(
title: 'Note - default visibility',
name: 'ui_ticket_zoom_article_note_new_internal',
area: 'UI::TicketZoom',
description: 'Default visibility for new articles.',
options: {
form: [
{
display: '',
null: true,
name: 'ui_ticket_zoom_article_note_new_internal',
tag: 'boolean',
translate: true,
options: {
true => 'internal',
false => 'public',
},
},
],
},
state: true,
preferences: {
prio: 100,
permission: ['admin.ui'],
},
frontend: true
)
Setting.create_if_not_exists(
title: 'Email - subject field',
name: 'ui_ticket_zoom_article_email_subject',
area: 'UI::TicketZoom',
description: 'Use subject field for emails. If disabled, the ticket title will be used as subject.',
options: {
form: [
{
display: '',
null: true,
name: 'ui_ticket_zoom_article_email_subject',
tag: 'boolean',
translate: true,
options: {
true => 'yes',
false => 'no',
},
},
],
},
state: false,
preferences: {
prio: 200,
permission: ['admin.ui'],
},
frontend: true
)
Setting.create_if_not_exists(
title: 'Twitter - tweet initials',
name: 'ui_ticket_zoom_article_twitter_initials',
area: 'UI::TicketZoom',
description: 'Add sender initials to end of a tweet.',
options: {
form: [
{
display: '',
null: true,
name: 'ui_ticket_zoom_article_twitter_initials',
tag: 'boolean',
translate: true,
options: {
true => 'yes',
false => 'no',
},
},
],
},
state: true,
preferences: {
prio: 300,
permission: ['admin.ui'],
},
frontend: true
)
end
end

View file

@ -546,18 +546,17 @@ Setting.create_if_not_exists(
}, },
frontend: true frontend: true
) )
Setting.create_if_not_exists( Setting.create_if_not_exists(
title: 'Define default visibility of new a new article', title: 'Note - default visibility',
name: 'ui_ticket_zoom_article_new_internal', name: 'ui_ticket_zoom_article_note_new_internal',
area: 'UI::TicketZoom', area: 'UI::TicketZoom',
description: 'Set default visibility of new a new article.', description: 'Default visibility for new note.',
options: { options: {
form: [ form: [
{ {
display: '', display: '',
null: true, null: true,
name: 'ui_ticket_zoom_article_new_internal', name: 'ui_ticket_zoom_article_note_new_internal',
tag: 'boolean', tag: 'boolean',
translate: true, translate: true,
options: { options: {
@ -569,7 +568,61 @@ Setting.create_if_not_exists(
}, },
state: true, state: true,
preferences: { preferences: {
prio: 1, prio: 100,
permission: ['admin.ui'],
},
frontend: true
)
Setting.create_if_not_exists(
title: 'Email - subject field',
name: 'ui_ticket_zoom_article_email_subject',
area: 'UI::TicketZoom',
description: 'Use subject field for emails. If disabled, the ticket title will be used as subject.',
options: {
form: [
{
display: '',
null: true,
name: 'ui_ticket_zoom_article_email_subject',
tag: 'boolean',
translate: true,
options: {
true => 'yes',
false => 'no',
},
},
],
},
state: false,
preferences: {
prio: 200,
permission: ['admin.ui'],
},
frontend: true
)
Setting.create_if_not_exists(
title: 'Twitter - tweet initials',
name: 'ui_ticket_zoom_article_twitter_initials',
area: 'UI::TicketZoom',
description: 'Add sender initials to end of a tweet.',
options: {
form: [
{
display: '',
null: true,
name: 'ui_ticket_zoom_article_twitter_initials',
tag: 'boolean',
translate: true,
options: {
true => 'yes',
false => 'no',
},
},
],
},
state: true,
preferences: {
prio: 300,
permission: ['admin.ui'], permission: ['admin.ui'],
}, },
frontend: true frontend: true
@ -1489,6 +1542,101 @@ Setting.create_if_not_exists(
frontend: false, frontend: false,
) )
group = Group.where(active: true).first
if !group
group = Group.first
end
group_id = 1
if group
group_id = group.id
end
Setting.create_if_not_exists(
title: 'Group selection for Ticket creation',
name: 'form_ticket_create_group_id',
area: 'Form::Base',
description: 'Defines if group of created tickets via web form.',
options: {
form: [
{
display: '',
null: true,
name: 'form_ticket_create_group_id',
tag: 'select',
relation: 'Group',
},
],
},
state: group_id,
preferences: {
permission: ['admin.channel_formular'],
},
frontend: false,
)
Setting.create_if_not_exists(
title: 'Limit tickets by ip per hour',
name: 'form_ticket_create_by_ip_per_hour',
area: 'Form::Base',
description: 'Defines limit of tickets by ip per hour via web form.',
options: {
form: [
{
display: '',
null: true,
name: 'form_ticket_create_by_ip_per_hour',
tag: 'input',
},
],
},
state: 20,
preferences: {
permission: ['admin.channel_formular'],
},
frontend: false,
)
Setting.create_if_not_exists(
title: 'Limit tickets by ip per day',
name: 'form_ticket_create_by_ip_per_day',
area: 'Form::Base',
description: 'Defines limit of tickets by ip per day via web form.',
options: {
form: [
{
display: '',
null: true,
name: 'form_ticket_create_by_ip_per_day',
tag: 'input',
},
],
},
state: 240,
preferences: {
permission: ['admin.channel_formular'],
},
frontend: false,
)
Setting.create_if_not_exists(
title: 'Limit tickets per day',
name: 'form_ticket_create_per_day',
area: 'Form::Base',
description: 'Defines limit of tickets per day via web form.',
options: {
form: [
{
display: '',
null: true,
name: 'form_ticket_create_per_day',
tag: 'input',
},
],
},
state: 5000,
preferences: {
permission: ['admin.channel_formular'],
},
frontend: false,
)
Setting.create_if_not_exists( Setting.create_if_not_exists(
title: 'Ticket Subject Size', title: 'Ticket Subject Size',
name: 'ticket_subject_size', name: 'ticket_subject_size',

View file

@ -56,7 +56,7 @@ or
def self.email(params) def self.email(params)
# send verify email # send verify email
subject = if !params[:subject] || params[:subject].empty? subject = if params[:subject].blank?
'#' + rand(99_999_999_999).to_s '#' + rand(99_999_999_999).to_s
else else
params[:subject] params[:subject]

View file

@ -16,7 +16,7 @@ module Import
end end
def source def source
import_class_namespace self.class.source
end end
def remote_id(resource, *_args) def remote_id(resource, *_args)
@ -57,6 +57,14 @@ module Import
changes changes
end end
def self.source
import_class_namespace
end
def self.import_class_namespace
@import_class_namespace ||= name.to_s.sub('Import::', '')
end
private private
def initialize_associations_states def initialize_associations_states
@ -83,6 +91,9 @@ module Import
@resource = lookup_existing(resource, *args) @resource = lookup_existing(resource, *args)
return false if !@resource return false if !@resource
# lock the current resource for write access
@resource.with_lock do
# delete since we have an update and # delete since we have an update and
# the record is already created # the record is already created
resource.delete(:created_by_id) resource.delete(:created_by_id)
@ -109,6 +120,7 @@ module Import
@resource.save! @resource.save!
true true
end end
end
def lookup_existing(resource, *_args) def lookup_existing(resource, *_args)
@ -214,11 +226,7 @@ module Import
end end
def mapping_config(*_args) def mapping_config(*_args)
import_class_namespace.gsub('::', '_').underscore + '_mapping' self.class.import_class_namespace.gsub('::', '_').underscore + '_mapping'
end
def import_class_namespace
self.class.name.to_s.sub('Import::', '')
end end
def handle_args(_resource, *args) def handle_args(_resource, *args)

View file

@ -6,6 +6,34 @@ module Import
@remote_id @remote_id
end end
def self.lost_map(found_remote_ids)
ExternalSync.joins('INNER JOIN users ON (users.id = external_syncs.o_id)')
.where(
source: source,
object: import_class.name,
users: {
active: true
}
)
.pluck(:source_id, :o_id)
.to_h
.except(*found_remote_ids)
end
def self.deactivate_lost(lost_ids)
# we need to update in slices since some DBs
# have a limit for IN length
lost_ids.each_slice(5000) do |slice|
# we need to instanciate every entry and set
# the active state this way to send notifications
# to the client
::User.where(id: slice).each do |user|
user.update_attribute(:active, false)
end
end
end
private private
def import(resource, *args) def import(resource, *args)
@ -58,7 +86,7 @@ module Import
return true if resource[:login].blank? return true if resource[:login].blank?
# skip resource if only ignored attributes are set # skip resource if only ignored attributes are set
ignored_attributes = %i(login dn created_by_id updated_by_id) ignored_attributes = %i(login dn created_by_id updated_by_id active)
!resource.except(*ignored_attributes).values.any?(&:present?) !resource.except(*ignored_attributes).values.any?(&:present?)
end end
@ -181,6 +209,11 @@ module Import
mapped[attribute] = mapped[attribute].downcase mapped[attribute] = mapped[attribute].downcase
end end
# we have to add the active state manually
# because otherwise disabled instances won't get
# re-activated if they should get synced again
mapped[:active] = true
mapped mapped
end end

View file

@ -33,10 +33,14 @@ module Import
relevant_attributes = config[:user_attributes].keys relevant_attributes = config[:user_attributes].keys
relevant_attributes.push('dn') relevant_attributes.push('dn')
@found_lost_remote_ids = []
@found_remote_ids = []
@ldap.search(config[:user_filter], attributes: relevant_attributes) do |entry| @ldap.search(config[:user_filter], attributes: relevant_attributes) do |entry|
backend_instance = create_instance(entry, config, user_roles, signup_role_ids, kargs) backend_instance = create_instance(entry, config, user_roles, signup_role_ids, kargs)
post_import_hook(entry, backend_instance, config, user_roles, signup_role_ids, kargs) post_import_hook(entry, backend_instance, config, user_roles, signup_role_ids, kargs)
track_found_remote_ids(backend_instance)
next if import_job.blank? next if import_job.blank?
import_job_count += 1 import_job_count += 1
next if import_job_count < 100 next if import_job_count < 100
@ -47,6 +51,7 @@ module Import
import_job_count = 0 import_job_count = 0
end end
handle_lost
end end
def self.pre_import_hook(_records, *_args) def self.pre_import_hook(_records, *_args)
@ -77,18 +82,25 @@ module Import
action = backend_instance.action action = backend_instance.action
add_resource_role_ids_to_statistics(resource.role_ids, action)
action
end
def self.add_resource_role_ids_to_statistics(role_ids, action)
return if role_ids.blank?
known_actions = { known_actions = {
created: 0, created: 0,
updated: 0, updated: 0,
unchanged: 0, unchanged: 0,
failed: 0, failed: 0,
deactivated: 0,
} }
if !@statistics[:role_ids] @statistics[:role_ids] ||= {}
@statistics[:role_ids] = {}
end
resource.role_ids.each do |role_id| role_ids.each do |role_id|
next if !known_actions.key?(action) next if !known_actions.key?(action)
@ -99,8 +111,6 @@ module Import
@statistics[:role_ids][role_id][action] += 1 @statistics[:role_ids][role_id][action] += 1
end end
action
end end
def self.user_roles(ldap:, config:) def self.user_roles(ldap:, config:)
@ -111,6 +121,46 @@ module Import
ldap_group = ::Ldap::Group.new(group_config, ldap: ldap) ldap_group = ::Ldap::Group.new(group_config, ldap: ldap)
ldap_group.user_roles(config[:group_role_map]) ldap_group.user_roles(config[:group_role_map])
end end
def self.track_found_remote_ids(backend_instance)
remote_id = backend_instance.remote_id(nil)
@deactivation_actions ||= %i(skipped failed)
if @deactivation_actions.include?(backend_instance.action)
@found_lost_remote_ids.push(remote_id)
else
@found_remote_ids.push(remote_id)
end
end
def self.handle_lost
backend_class = backend_class(nil)
lost_map = backend_class.lost_map(@found_remote_ids)
# disabled count is tracked as a separate number
# since they don't have to be in the sum (e.g. deleted in LDAP)
@statistics[:deactivated] = lost_map.size
# skipped deactivated are those who
# were found, skipped and will get deactivated
skipped_deactivated = @found_lost_remote_ids & lost_map.keys
@statistics[:skipped] -= skipped_deactivated.size
# loop over every lost user ID and add the
# deactivated count to the statistics
lost_ids = lost_map.values
lost_ids.each do |user_id|
role_ids = ::User.joins(:roles)
.where(id: user_id)
.pluck(:'roles_users.role_id')
add_resource_role_ids_to_statistics(role_ids, :deactivated)
end
# deactivate entries only on live syncs
return if @dry_run
backend_class.deactivate_lost(lost_ids)
end
end end
end end
end end

View file

@ -2,11 +2,19 @@ module Import
class ModelResource < Import::BaseResource class ModelResource < Import::BaseResource
def import_class def import_class
model_name.constantize self.class.import_class
end end
def model_name def model_name
@model_name ||= self.class.name.split('::').last self.class.model_name
end
def self.import_class
model_name.constantize
end
def self.model_name
@model_name ||= name.split('::').last
end end
private private

View file

@ -95,6 +95,8 @@ module Import
def handle_response(response) def handle_response(response)
encoded_body = Encode.conv('utf8', response.body.to_s) encoded_body = Encode.conv('utf8', response.body.to_s)
# remove null bytes otherwise PostgreSQL will fail
encoded_body.gsub!('\u0000', '')
JSON.parse(encoded_body) JSON.parse(encoded_body)
end end

View file

@ -43,9 +43,7 @@ module Import
return if !state return if !state
state.default_create = true state.default_create = true
state.callback_loop = true state.save!
state.save
end end
def update_default_follow_up def update_default_follow_up
@ -56,9 +54,7 @@ module Import
return if !state return if !state
state.default_follow_up = true state.default_follow_up = true
state.callback_loop = true state.save!
state.save
end end
def update_ticket_attributes def update_ticket_attributes

View file

@ -18,6 +18,7 @@ module Import
updated: 0, updated: 0,
unchanged: 0, unchanged: 0,
failed: 0, failed: 0,
deactivated: 0,
} }
end end

View file

@ -80,14 +80,14 @@ class Ldap
filter ||= filter() filter ||= filter()
result = {} result = {}
@ldap.search(filter, attributes: %w(dn member)) do |entry| @ldap.search(filter, attributes: %w(dn member memberuid)) do |entry|
members = entry[:member]
next if members.blank?
roles = mapping[entry.dn.downcase] roles = mapping[entry.dn.downcase]
next if roles.blank? next if roles.blank?
members = group_user_dns(entry)
next if members.blank?
members.each do |user_dn| members.each do |user_dn|
user_dn_key = user_dn.downcase user_dn_key = user_dn.downcase
@ -133,5 +133,18 @@ class Ldap
@uid_attribute = config[:uid_attribute] @uid_attribute = config[:uid_attribute]
@filter = config[:filter] @filter = config[:filter]
end end
def group_user_dns(entry)
return entry[:member] if entry[:member].present?
return if entry[:memberuid].blank?
entry[:memberuid].collect do |uid|
dn = nil
@ldap.search("(uid=#{uid})", attributes: %w(dn)) do |user|
dn = user.dn
end
dn
end.compact
end
end end
end end

View file

@ -162,7 +162,7 @@ class Ldap
# #
# @return [String, nil] The active or found filter or nil if none could be found. # @return [String, nil] The active or found filter or nil if none could be found.
def filter def filter
@filter ||= lookup_filter(['(&(objectClass=user)(samaccountname=*)(!(samaccountname=*$)))', '(objectClass=user)', '(objectClass=posixaccount)']) @filter ||= lookup_filter(['(&(objectClass=user)(samaccountname=*)(!(samaccountname=*$)))', '(objectClass=user)', '(objectClass=posixaccount)', '(objectClass=person)'])
end end
# The active uid attribute of the instance. If none give on initialization an automatic lookup is performed. # The active uid attribute of the instance. If none give on initialization an automatic lookup is performed.

View file

@ -427,8 +427,7 @@ return true if backend is configured
=end =end
def self.enabled? def self.enabled?
return if !Setting.get('es_url') return false if Setting.get('es_url').blank?
return if Setting.get('es_url').empty?
true true
end end

View file

@ -536,7 +536,7 @@ do($ = window.jQuery, window) ->
@maybeAddTimestamp() @maybeAddTimestamp()
# add message before message typing loader # add message before message typing loader
if @el.find('.zammad-chat-message--typing').size() if @el.find('.zammad-chat-message--typing').get(0)
@lastAddedType = 'typing-placeholder' @lastAddedType = 'typing-placeholder'
@el.find('.zammad-chat-message--typing').before messageElement @el.find('.zammad-chat-message--typing').before messageElement
else else
@ -725,7 +725,7 @@ do($ = window.jQuery, window) ->
@stopTypingId = setTimeout(@onAgentTypingEnd, 3000) @stopTypingId = setTimeout(@onAgentTypingEnd, 3000)
# never display two typing indicators # never display two typing indicators
return if @el.find('.zammad-chat-message--typing').size() return if @el.find('.zammad-chat-message--typing').get(0)
@maybeAddTimestamp() @maybeAddTimestamp()

View file

@ -1,64 +1,3 @@
if (!window.zammadChatTemplates) {
window.zammadChatTemplates = {};
}
window.zammadChatTemplates["agent"] = function (__obj) {
if (!__obj) __obj = {};
var __out = [], __capture = function(callback) {
var out = __out, result;
__out = [];
callback.call(this);
result = __out.join('');
__out = out;
return __safe(result);
}, __sanitize = function(value) {
if (value && value.ecoSafe) {
return value;
} else if (typeof value !== 'undefined' && value != null) {
return __escape(value);
} else {
return '';
}
}, __safe, __objSafe = __obj.safe, __escape = __obj.escape;
__safe = __obj.safe = function(value) {
if (value && value.ecoSafe) {
return value;
} else {
if (!(typeof value !== 'undefined' && value != null)) value = '';
var result = new String(value);
result.ecoSafe = true;
return result;
}
};
if (!__escape) {
__escape = __obj.escape = function(value) {
return ('' + value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
};
}
(function() {
(function() {
if (this.agent.avatar) {
__out.push('\n<img class="zammad-chat-agent-avatar" src="');
__out.push(__sanitize(this.agent.avatar));
__out.push('">\n');
}
__out.push('\n<span class="zammad-chat-agent-sentence">\n <span class="zammad-chat-agent-name">');
__out.push(__sanitize(this.agent.name));
__out.push('</span>\n</span>');
}).call(this);
}).call(__obj);
__obj.safe = __objSafe, __obj.escape = __escape;
return __out.join('');
};
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
slice = [].slice, slice = [].slice,
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
@ -823,7 +762,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
unreadClass: '' unreadClass: ''
}); });
this.maybeAddTimestamp(); this.maybeAddTimestamp();
if (this.el.find('.zammad-chat-message--typing').size()) { if (this.el.find('.zammad-chat-message--typing').get(0)) {
this.lastAddedType = 'typing-placeholder'; this.lastAddedType = 'typing-placeholder';
this.el.find('.zammad-chat-message--typing').before(messageElement); this.el.find('.zammad-chat-message--typing').before(messageElement);
} else { } else {
@ -1022,7 +961,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
clearTimeout(this.stopTypingId); clearTimeout(this.stopTypingId);
} }
this.stopTypingId = setTimeout(this.onAgentTypingEnd, 3000); this.stopTypingId = setTimeout(this.onAgentTypingEnd, 3000);
if (this.el.find('.zammad-chat-message--typing').size()) { if (this.el.find('.zammad-chat-message--typing').get(0)) {
return; return;
} }
this.maybeAddTimestamp(); this.maybeAddTimestamp();
@ -1395,6 +1334,67 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
return window.ZammadChat = ZammadChat; return window.ZammadChat = ZammadChat;
})(window.jQuery, window); })(window.jQuery, window);
if (!window.zammadChatTemplates) {
window.zammadChatTemplates = {};
}
window.zammadChatTemplates["agent"] = function (__obj) {
if (!__obj) __obj = {};
var __out = [], __capture = function(callback) {
var out = __out, result;
__out = [];
callback.call(this);
result = __out.join('');
__out = out;
return __safe(result);
}, __sanitize = function(value) {
if (value && value.ecoSafe) {
return value;
} else if (typeof value !== 'undefined' && value != null) {
return __escape(value);
} else {
return '';
}
}, __safe, __objSafe = __obj.safe, __escape = __obj.escape;
__safe = __obj.safe = function(value) {
if (value && value.ecoSafe) {
return value;
} else {
if (!(typeof value !== 'undefined' && value != null)) value = '';
var result = new String(value);
result.ecoSafe = true;
return result;
}
};
if (!__escape) {
__escape = __obj.escape = function(value) {
return ('' + value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
};
}
(function() {
(function() {
if (this.agent.avatar) {
__out.push('\n<img class="zammad-chat-agent-avatar" src="');
__out.push(__sanitize(this.agent.avatar));
__out.push('">\n');
}
__out.push('\n<span class="zammad-chat-agent-sentence">\n <span class="zammad-chat-agent-name">');
__out.push(__sanitize(this.agent.name));
__out.push('</span>\n</span>');
}).call(this);
}).call(__obj);
__obj.safe = __objSafe, __obj.escape = __escape;
return __out.join('');
};
/*! /*!
* ---------------------------------------------------------------------------- * ----------------------------------------------------------------------------
* "THE BEER-WARE LICENSE" (Revision 42): * "THE BEER-WARE LICENSE" (Revision 42):

File diff suppressed because one or more lines are too long

View file

@ -14,7 +14,7 @@
<div id="feedback-form-inline"></div> <div id="feedback-form-inline"></div>
<div class="js-logDisplay"></div> <div class="js-logDisplay" style="overflow-x: hidden;"></div>
<p>Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.</p> <p>Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.</p>

View file

@ -204,8 +204,14 @@ $(function() {
if (this.options.test) { if (this.options.test) {
params.test = true params.test = true
} }
params.fingerprint = this.fingerprint()
$.ajax({ $.ajax({
method: 'post',
url: _this.endpoint_config, url: _this.endpoint_config,
cache: false,
processData: true,
data: params data: params
}).done(function(data) { }).done(function(data) {
_this.log('debug', 'config:', data) _this.log('debug', 'config:', data)
@ -256,7 +262,7 @@ $(function() {
_this.log('debug', 'currentTime', currentTime) _this.log('debug', 'currentTime', currentTime)
_this.log('debug', 'modalOpenTime', _this.modalOpenTime.getTime()) _this.log('debug', 'modalOpenTime', _this.modalOpenTime.getTime())
_this.log('debug', 'diffTime', diff) _this.log('debug', 'diffTime', diff)
if (diff < 1000*8) { if (diff < 1000*10) {
alert('Sorry, you look like an robot!') alert('Sorry, you look like an robot!')
return return
} }
@ -317,7 +323,10 @@ $(function() {
formData.append('test', true) formData.append('test', true)
} }
formData.append('token', this._config.token) formData.append('token', this._config.token)
formData.append('fingerprint', this.fingerprint())
_this.log('debug', 'formData', formData) _this.log('debug', 'formData', formData)
return formData return formData
} }
@ -463,6 +472,22 @@ $(function() {
return string return string
} }
Plugin.prototype.fingerprint = function () {
var canvas = document.createElement('canvas')
var ctx = canvas.getContext('2d')
var txt = 'https://zammad.com'
ctx.textBaseline = 'top'
ctx.font = '12px \'Arial\''
ctx.textBaseline = 'alphabetic'
ctx.fillStyle = '#f60'
ctx.fillRect(125,1,62,20)
ctx.fillStyle = '#069'
ctx.fillText(txt, 2, 15)
ctx.fillStyle = 'rgba(100, 200, 0, 0.7)'
ctx.fillText(txt, 4, 17)
return canvas.toDataURL()
}
$.fn[pluginName] = function (options) { $.fn[pluginName] = function (options) {
return this.each(function () { return this.each(function () {
var instance = $.data(this, 'plugin_' + pluginName) var instance = $.data(this, 'plugin_' + pluginName)

View file

@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
bundle install bundle install --jobs 8
rm -rf tmp/cache* rm -rf tmp/cache*

View file

@ -79,7 +79,7 @@ sudo -u "${USER}" -H bash -l -c 'rvm alias create default 2.1.2'
sudo -u "${USER}" -H bash -l -c 'cd ~/zammad && gem install rails --no-ri --no-rdoc' sudo -u "${USER}" -H bash -l -c 'cd ~/zammad && gem install rails --no-ri --no-rdoc'
sudo -u "${USER}" -H bash -l -c 'cd ~/zammad && bundle install' sudo -u "${USER}" -H bash -l -c 'cd ~/zammad && bundle install --jobs 8'
DBPASS=$(apg -x8|head -1) DBPASS=$(apg -x8|head -1)
echo Password $DBPASS echo Password $DBPASS

View file

@ -15,7 +15,7 @@ export RAILS_SERVE_STATIC_FILES=true
export ZAMMAD_SETTING_TTL=15 export ZAMMAD_SETTING_TTL=15
export Z_LOCALES=en-us:de-de export Z_LOCALES=en-us:de-de
bundle install bundle install --jobs 8
rm -rf tmp/screenshot* rm -rf tmp/screenshot*
rm -rf tmp/cache* rm -rf tmp/cache*

View file

@ -49,7 +49,195 @@ RSpec.describe Import::Ldap::UserFactory do
}.by(1) }.by(1)
end end
it 'supports dry run' do it 'deactivates lost users' do
config = {
user_filter: '(objectClass=user)',
group_filter: '(objectClass=group)',
user_uid: 'uid',
user_attributes: {
'uid' => 'login',
'email' => 'email',
}
}
persistent_entry = build(:ldap_entry)
persistent_entry['uid'] = ['exampleuid']
persistent_entry['email'] = ['example@example.com']
lost_entry = build(:ldap_entry)
lost_entry['uid'] = ['exampleuid_lost']
lost_entry['email'] = ['lost@example.com']
mocked_ldap = double(
host: 'ldap.example.com',
port: 636,
ssl: true,
base_dn: 'dc=example,dc=com'
)
# group user role mapping
expect(mocked_ldap).to receive(:search)
# user counting
expect(mocked_ldap).to receive(:count).and_return(2)
# user search
expect(mocked_ldap).to receive(:search).and_yield(persistent_entry).and_yield(lost_entry)
described_class.import(
config: config,
ldap: mocked_ldap,
)
# group user role mapping
expect(mocked_ldap).to receive(:search)
# user counting
expect(mocked_ldap).to receive(:count).and_return(1)
# user search
expect(mocked_ldap).to receive(:search).and_yield(persistent_entry)
expect do
described_class.import(
config: config,
ldap: mocked_ldap,
)
end.to change {
User.find_by(email: 'lost@example.com').active
}
end
it 're-activates previously lost users' do
config = {
user_filter: '(objectClass=user)',
group_filter: '(objectClass=group)',
user_uid: 'uid',
user_attributes: {
'uid' => 'login',
'email' => 'email',
}
}
persistent_entry = build(:ldap_entry)
persistent_entry['uid'] = ['exampleuid']
persistent_entry['email'] = ['example@example.com']
lost_entry = build(:ldap_entry)
lost_entry['uid'] = ['exampleuid_lost']
lost_entry['email'] = ['lost@example.com']
mocked_ldap = double(
host: 'ldap.example.com',
port: 636,
ssl: true,
base_dn: 'dc=example,dc=com'
)
# group user role mapping
expect(mocked_ldap).to receive(:search)
# user counting
expect(mocked_ldap).to receive(:count).and_return(2)
# user search
expect(mocked_ldap).to receive(:search).and_yield(persistent_entry).and_yield(lost_entry)
described_class.import(
config: config,
ldap: mocked_ldap,
)
# group user role mapping
expect(mocked_ldap).to receive(:search)
# user counting
expect(mocked_ldap).to receive(:count).and_return(1)
# user search
expect(mocked_ldap).to receive(:search).and_yield(persistent_entry)
described_class.import(
config: config,
ldap: mocked_ldap,
)
# group user role mapping
expect(mocked_ldap).to receive(:search)
# user counting
expect(mocked_ldap).to receive(:count).and_return(2)
# user search
expect(mocked_ldap).to receive(:search).and_yield(persistent_entry).and_yield(lost_entry)
expect do
described_class.import(
config: config,
ldap: mocked_ldap,
)
end.to change {
User.find_by(email: 'lost@example.com').active
}
end
it 'deactivates skipped users' do
config = {
user_filter: '(objectClass=user)',
group_filter: '(objectClass=group)',
user_uid: 'uid',
user_attributes: {
'uid' => 'login',
'email' => 'email',
},
}
lost_entry = build(:ldap_entry)
lost_entry['uid'] = ['exampleuid']
lost_entry['email'] = ['example@example.com']
mocked_ldap = double(
host: 'ldap.example.com',
port: 636,
ssl: true,
base_dn: 'dc=example,dc=com'
)
# group user role mapping
expect(mocked_ldap).to receive(:search)
# user counting
expect(mocked_ldap).to receive(:count).and_return(2)
# user search
expect(mocked_ldap).to receive(:search).and_yield(lost_entry)
described_class.import(
config: config,
ldap: mocked_ldap,
)
# activate skipping
config[:unassigned_users] = 'skip_sync'
config[:group_role_map] = {
'dummy' => %w(1 2),
}
# group user role mapping
mocked_entry = build(:ldap_entry)
mocked_entry['dn'] = 'dummy'
mocked_entry['member'] = ['dummy']
expect(mocked_ldap).to receive(:search).and_yield(mocked_entry)
# user counting
expect(mocked_ldap).to receive(:count).and_return(1)
# user search
expect(mocked_ldap).to receive(:search).and_yield(lost_entry)
expect do
described_class.import(
config: config,
ldap: mocked_ldap,
)
end.to change {
User.find_by(email: 'example@example.com').active
}
end
context 'dry run' do
it "doesn't sync users" do
config = { config = {
user_filter: '(objectClass=user)', user_filter: '(objectClass=user)',
@ -90,6 +278,65 @@ RSpec.describe Import::Ldap::UserFactory do
User.count User.count
} }
end end
it "doesn't deactivates lost users" do
config = {
user_filter: '(objectClass=user)',
group_filter: '(objectClass=group)',
user_uid: 'uid',
user_attributes: {
'uid' => 'login',
'email' => 'email',
}
}
persistent_entry = build(:ldap_entry)
persistent_entry['uid'] = ['exampleuid']
persistent_entry['email'] = ['example@example.com']
lost_entry = build(:ldap_entry)
lost_entry['uid'] = ['exampleuid']
lost_entry['email'] = ['example@example.com']
mocked_ldap = double(
host: 'ldap.example.com',
port: 636,
ssl: true,
base_dn: 'dc=example,dc=com'
)
# group user role mapping
expect(mocked_ldap).to receive(:search)
# user counting
expect(mocked_ldap).to receive(:count).and_return(2)
# user search
expect(mocked_ldap).to receive(:search).and_yield(persistent_entry).and_yield(lost_entry)
described_class.import(
config: config,
ldap: mocked_ldap,
dry_run: true
)
# group user role mapping
expect(mocked_ldap).to receive(:search)
# user counting
expect(mocked_ldap).to receive(:count).and_return(1)
# user search
expect(mocked_ldap).to receive(:search).and_yield(persistent_entry)
expect do
described_class.import(
config: config,
ldap: mocked_ldap,
dry_run: true
)
end.not_to change {
User.count
}
end
end
end end
describe '.add_to_statistics' do describe '.add_to_statistics' do
@ -118,13 +365,15 @@ RSpec.describe Import::Ldap::UserFactory do
created: 1, created: 1,
updated: 0, updated: 0,
unchanged: 0, unchanged: 0,
failed: 0 failed: 0,
deactivated: 0,
}, },
2 => { 2 => {
created: 1, created: 1,
updated: 0, updated: 0,
unchanged: 0, unchanged: 0,
failed: 0 failed: 0,
deactivated: 0,
}, },
}, },
skipped: 0, skipped: 0,
@ -132,6 +381,72 @@ RSpec.describe Import::Ldap::UserFactory do
updated: 0, updated: 0,
unchanged: 0, unchanged: 0,
failed: 0, failed: 0,
deactivated: 0,
}
expect(described_class.statistics).to include(expected)
end
it 'adds deactivated users' do
config = {
user_filter: '(objectClass=user)',
group_filter: '(objectClass=group)',
user_uid: 'uid',
user_attributes: {
'uid' => 'login',
'email' => 'email',
}
}
persistent_entry = build(:ldap_entry)
persistent_entry['uid'] = ['exampleuid']
persistent_entry['email'] = ['example@example.com']
lost_entry = build(:ldap_entry)
lost_entry['uid'] = ['exampleuid_lost']
lost_entry['email'] = ['lost@example.com']
mocked_ldap = double(
host: 'ldap.example.com',
port: 636,
ssl: true,
base_dn: 'dc=example,dc=com'
)
# group user role mapping
expect(mocked_ldap).to receive(:search)
# user counting
allow(mocked_ldap).to receive(:count).and_return(2)
# user search
expect(mocked_ldap).to receive(:search).and_yield(persistent_entry).and_yield(lost_entry)
described_class.import(
config: config,
ldap: mocked_ldap,
)
# simulate new import
described_class.reset_statistics
# group user role mapping
expect(mocked_ldap).to receive(:search)
# user counting
allow(mocked_ldap).to receive(:count).and_return(1)
# user search
expect(mocked_ldap).to receive(:search).and_yield(persistent_entry)
described_class.import(
config: config,
ldap: mocked_ldap,
)
expected = {
skipped: 0,
created: 0,
updated: 0,
unchanged: 1,
failed: 0,
deactivated: 1,
} }
expect(described_class.statistics).to include(expected) expect(described_class.statistics).to include(expected)
@ -155,6 +470,7 @@ RSpec.describe Import::Ldap::UserFactory do
updated: 0, updated: 0,
unchanged: 0, unchanged: 0,
failed: 0, failed: 0,
deactivated: 0,
} }
expect(described_class.statistics).to include(expected) expect(described_class.statistics).to include(expected)
@ -180,6 +496,7 @@ RSpec.describe Import::Ldap::UserFactory do
updated: 0, updated: 0,
unchanged: 0, unchanged: 0,
failed: 0, failed: 0,
deactivated: 0,
} }
expect(described_class.statistics).to include(expected) expect(described_class.statistics).to include(expected)

View file

@ -50,6 +50,7 @@ RSpec.describe Import::StatisticalFactory do
unchanged: 0, unchanged: 0,
skipped: 0, skipped: 0,
failed: 0, failed: 0,
deactivated: 0,
} }
expect(Import::Test::GroupFactory.statistics).to eq(statistics) expect(Import::Test::GroupFactory.statistics).to eq(statistics)
end end
@ -72,6 +73,7 @@ RSpec.describe Import::StatisticalFactory do
unchanged: 0, unchanged: 0,
skipped: 0, skipped: 0,
failed: 0, failed: 0,
deactivated: 0,
} }
expect(Import::Test::GroupFactory.statistics).to eq(statistics) expect(Import::Test::GroupFactory.statistics).to eq(statistics)
end end
@ -95,6 +97,7 @@ RSpec.describe Import::StatisticalFactory do
unchanged: 0, unchanged: 0,
skipped: 0, skipped: 0,
failed: 0, failed: 0,
deactivated: 0,
} }
expect(Import::Test::GroupFactory.statistics).to eq(statistics) expect(Import::Test::GroupFactory.statistics).to eq(statistics)
end end
@ -118,6 +121,7 @@ RSpec.describe Import::StatisticalFactory do
unchanged: 0, unchanged: 0,
skipped: 0, skipped: 0,
failed: 0, failed: 0,
deactivated: 0,
} }
expect(Import::Test::GroupFactory.statistics).to eq(statistics) expect(Import::Test::GroupFactory.statistics).to eq(statistics)
end end
@ -139,6 +143,7 @@ RSpec.describe Import::StatisticalFactory do
unchanged: 1, unchanged: 1,
skipped: 0, skipped: 0,
failed: 0, failed: 0,
deactivated: 0,
} }
expect(Import::Test::GroupFactory.statistics).to eq(statistics) expect(Import::Test::GroupFactory.statistics).to eq(statistics)
end end
@ -156,6 +161,7 @@ RSpec.describe Import::StatisticalFactory do
unchanged: 0, unchanged: 0,
skipped: 0, skipped: 0,
failed: 0, failed: 0,
deactivated: 0,
} }
expect(Import::Test::GroupFactory.statistics).to eq(statistics) expect(Import::Test::GroupFactory.statistics).to eq(statistics)
end end
@ -186,6 +192,7 @@ RSpec.describe Import::StatisticalFactory do
unchanged: 0, unchanged: 0,
skipped: 0, skipped: 0,
failed: 0, failed: 0,
deactivated: 0,
} }
expect(Import::Test::GroupFactory.statistics).to eq(statistics) expect(Import::Test::GroupFactory.statistics).to eq(statistics)
end end
@ -204,6 +211,7 @@ RSpec.describe Import::StatisticalFactory do
unchanged: 0, unchanged: 0,
skipped: 0, skipped: 0,
failed: 0, failed: 0,
deactivated: 0,
} }
expect(Import::Test::GroupFactory.statistics).to eq(statistics) expect(Import::Test::GroupFactory.statistics).to eq(statistics)
end end
@ -222,6 +230,7 @@ RSpec.describe Import::StatisticalFactory do
unchanged: 0, unchanged: 0,
skipped: 0, skipped: 0,
failed: 0, failed: 0,
deactivated: 0,
} }
expect(Import::Test::GroupFactory.statistics).to eq(statistics) expect(Import::Test::GroupFactory.statistics).to eq(statistics)
end end
@ -246,6 +255,7 @@ RSpec.describe Import::StatisticalFactory do
unchanged: 1, unchanged: 1,
skipped: 0, skipped: 0,
failed: 0, failed: 0,
deactivated: 0,
} }
expect(Import::Test::GroupFactory.statistics).to eq(statistics) expect(Import::Test::GroupFactory.statistics).to eq(statistics)
end end

View file

@ -1,5 +1,5 @@
RSpec.configure do |config| RSpec.configure do |config|
config.before(:all) do config.before(:each) do
# clear the cache otherwise it won't # clear the cache otherwise it won't
# be able to recognize the rollbacks # be able to recognize the rollbacks
# done by RSpec # done by RSpec

View file

@ -82,23 +82,6 @@ class FormTest < TestCase
browser: agent, browser: agent,
css: 'body div.zammad-form-modal button[type="submit"][disabled]', css: 'body div.zammad-form-modal button[type="submit"][disabled]',
) )
set(
browser: agent,
css: 'body div.zammad-form-modal [name="email"]',
value: 'notexistinginanydomainspacealsonothere@znuny.com',
)
click(
browser: agent,
css: 'body div.zammad-form-modal button[type="submit"]',
)
watch_for(
browser: agent,
css: 'body div.zammad-form-modal .has-error [name="email"]',
)
watch_for_disappear(
browser: agent,
css: 'body div.zammad-form-modal button[type="submit"][disabled]',
)
set( set(
browser: agent, browser: agent,
css: 'body div.zammad-form-modal [name="email"]', css: 'body div.zammad-form-modal [name="email"]',
@ -315,23 +298,6 @@ class FormTest < TestCase
browser: customer, browser: customer,
css: 'body div.zammad-form-modal button[type="submit"][disabled]', css: 'body div.zammad-form-modal button[type="submit"][disabled]',
) )
set(
browser: customer,
css: 'body div.zammad-form-modal [name="email"]',
value: 'notexistinginanydomainspacealsonothere@znuny.com',
)
click(
browser: customer,
css: 'body div.zammad-form-modal button[type="submit"]',
)
watch_for(
browser: customer,
css: 'body div.zammad-form-modal .has-error [name="email"]',
)
watch_for_disappear(
browser: customer,
css: 'body div.zammad-form-modal button[type="submit"][disabled]',
)
set( set(
browser: customer, browser: customer,
css: 'body div.zammad-form-modal [name="email"]', css: 'body div.zammad-form-modal [name="email"]',

View file

@ -2261,7 +2261,7 @@ wait untill text in selector disabppears
9.times { 9.times {
begin begin
text = instance.find_elements(css: '.content.active .js-reset')[0].text text = instance.find_elements(css: '.content.active .js-reset')[0].text
if !text || text.empty? if text.blank?
screenshot(browser: instance, comment: 'ticket_update_ok') screenshot(browser: instance, comment: 'ticket_update_ok')
sleep 1 sleep 1
return true return true

View file

@ -0,0 +1,241 @@
# encoding: utf-8
require 'test_helper'
require 'rake'
class FormControllerTest < ActionDispatch::IntegrationTest
setup do
@headers = { 'ACCEPT' => 'application/json', 'CONTENT_TYPE' => 'application/json', 'REMOTE_ADDR' => '1.2.3.4' }
if ENV['ES_URL'].present?
#fail "ERROR: Need ES_URL - hint ES_URL='http://127.0.0.1:9200'"
Setting.set('es_url', ENV['ES_URL'])
# Setting.set('es_url', 'http://127.0.0.1:9200')
# Setting.set('es_index', 'estest.local_zammad')
# Setting.set('es_user', 'elasticsearch')
# Setting.set('es_password', 'zammad')
if ENV['ES_INDEX_RAND'].present?
ENV['ES_INDEX'] = "es_index_#{rand(999_999_999)}"
end
if ENV['ES_INDEX'].blank?
raise "ERROR: Need ES_INDEX - hint ES_INDEX='estest.local_zammad'"
end
Setting.set('es_index', ENV['ES_INDEX'])
end
Ticket.destroy_all
# drop/create indexes
Setting.reload
Rake::Task.clear
Zammad::Application.load_tasks
Rake::Task['searchindex:rebuild'].execute
end
test '01 - get config call' do
post '/api/v1/form_config', {}.to_json, @headers
assert_response(401)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert_equal(result['error'], 'Not authorized')
end
test '02 - get config call' do
Setting.set('form_ticket_create', true)
post '/api/v1/form_config', {}.to_json, @headers
assert_response(401)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert_equal(result['error'], 'Not authorized')
end
test '03 - get config call & do submit' do
Setting.set('form_ticket_create', true)
fingerprint = SecureRandom.hex(40)
post '/api/v1/form_config', { fingerprint: fingerprint }.to_json, @headers
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert_equal(result['enabled'], true)
assert_equal(result['endpoint'], 'http://zammad.example.com/api/v1/form_submit')
assert(result['token'])
token = result['token']
post '/api/v1/form_submit', { fingerprint: fingerprint, token: 'invalid' }.to_json, @headers
assert_response(401)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert_equal(result['error'], 'Not authorized')
post '/api/v1/form_submit', { fingerprint: fingerprint, token: token }.to_json, @headers
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert(result['errors'])
assert_equal(result['errors']['name'], 'required')
assert_equal(result['errors']['email'], 'required')
assert_equal(result['errors']['title'], 'required')
assert_equal(result['errors']['body'], 'required')
post '/api/v1/form_submit', { fingerprint: fingerprint, token: token, email: 'some' }.to_json, @headers
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert(result['errors'])
assert_equal(result['errors']['name'], 'required')
assert_equal(result['errors']['email'], 'invalid')
assert_equal(result['errors']['title'], 'required')
assert_equal(result['errors']['body'], 'required')
post '/api/v1/form_submit', { fingerprint: fingerprint, token: token, name: 'Bob Smith', email: 'discard@znuny.com', title: 'test', body: 'hello' }.to_json, @headers
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert_not(result['errors'])
assert(result['ticket'])
assert(result['ticket']['id'])
assert(result['ticket']['number'])
travel 5.hours
post '/api/v1/form_submit', { fingerprint: fingerprint, token: token, name: 'Bob Smith', email: 'discard@znuny.com', title: 'test', body: 'hello' }.to_json, @headers
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert_not(result['errors'])
assert(result['ticket'])
assert(result['ticket']['id'])
assert(result['ticket']['number'])
travel 20.hours
post '/api/v1/form_submit', { fingerprint: fingerprint, token: token, name: 'Bob Smith', email: 'discard@znuny.com', title: 'test', body: 'hello' }.to_json, @headers
assert_response(401)
end
test '04 - get config call & do submit' do
Setting.set('form_ticket_create', true)
fingerprint = SecureRandom.hex(40)
post '/api/v1/form_config', { fingerprint: fingerprint }.to_json, @headers
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert_equal(result['enabled'], true)
assert_equal(result['endpoint'], 'http://zammad.example.com/api/v1/form_submit')
assert(result['token'])
token = result['token']
post '/api/v1/form_submit', { fingerprint: fingerprint, token: 'invalid' }.to_json, @headers
assert_response(401)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert_equal(result['error'], 'Not authorized')
post '/api/v1/form_submit', { fingerprint: fingerprint, token: token }.to_json, @headers
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert(result['errors'])
assert_equal(result['errors']['name'], 'required')
assert_equal(result['errors']['email'], 'required')
assert_equal(result['errors']['title'], 'required')
assert_equal(result['errors']['body'], 'required')
post '/api/v1/form_submit', { fingerprint: fingerprint, token: token, email: 'some' }.to_json, @headers
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert(result['errors'])
assert_equal(result['errors']['name'], 'required')
assert_equal(result['errors']['email'], 'invalid')
assert_equal(result['errors']['title'], 'required')
assert_equal(result['errors']['body'], 'required')
post '/api/v1/form_submit', { fingerprint: fingerprint, token: token, name: 'Bob Smith', email: 'somebody@example.com', title: 'test', body: 'hello' }.to_json, @headers
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert(result['errors'])
assert_equal(result['errors']['email'], 'invalid')
end
test '05 - limits' do
return if !SearchIndexBackend.enabled?
Setting.set('form_ticket_create', true)
fingerprint = SecureRandom.hex(40)
post '/api/v1/form_config', { fingerprint: fingerprint }.to_json, @headers
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert_equal(result['enabled'], true)
assert_equal(result['endpoint'], 'http://zammad.example.com/api/v1/form_submit')
assert(result['token'])
token = result['token']
(1..20).each { |count|
travel 10.seconds
post '/api/v1/form_submit', { fingerprint: fingerprint, token: token, name: 'Bob Smith', email: 'discard@znuny.com', title: "test#{count}", body: 'hello' }.to_json, @headers
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert_not(result['errors'])
assert(result['ticket'])
assert(result['ticket']['id'])
assert(result['ticket']['number'])
Scheduler.worker(true)
sleep 1 # wait until elasticsearch is index
}
sleep 10 # wait until elasticsearch is index
post '/api/v1/form_submit', { fingerprint: fingerprint, token: token, name: 'Bob Smith', email: 'discard@znuny.com', title: 'test-last', body: 'hello' }.to_json, @headers
assert_response(401)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert(result['error'])
@headers = { 'ACCEPT' => 'application/json', 'CONTENT_TYPE' => 'application/json', 'REMOTE_ADDR' => '1.2.3.5' }
(1..20).each { |count|
travel 10.seconds
post '/api/v1/form_submit', { fingerprint: fingerprint, token: token, name: 'Bob Smith', email: 'discard@znuny.com', title: "test-2-#{count}", body: 'hello' }.to_json, @headers
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert_not(result['errors'])
assert(result['ticket'])
assert(result['ticket']['id'])
assert(result['ticket']['number'])
Scheduler.worker(true)
sleep 1 # wait until elasticsearch is index
}
sleep 10 # wait until elasticsearch is index
post '/api/v1/form_submit', { fingerprint: fingerprint, token: token, name: 'Bob Smith', email: 'discard@znuny.com', title: 'test-2-last', body: 'hello' }.to_json, @headers
assert_response(401)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert(result['error'])
end
end

View file

@ -1414,6 +1414,228 @@ class TicketTriggerTest < ActiveSupport::TestCase
end end
test '6.1 owner auto assignment based on organization' do
trigger1 = Trigger.create_or_update(
name: 'aaa auto assignment',
condition: {
'ticket.organization_id' => {
'operator' => 'is not',
'pre_condition' => 'not_set',
'value' => '',
'value_completion' => '',
},
'ticket.action' => {
'operator' => 'is',
'value' => 'update',
},
},
perform: {
'ticket.owner_id' => {
'pre_condition' => 'current_user.id',
'value' => '',
'value_completion' => '',
},
},
disable_notification: true,
active: true,
created_by_id: 1,
updated_by_id: 1,
)
roles = Role.where(name: 'Agent')
agent = User.create_or_update(
login: 'agent@example.com',
firstname: 'Trigger',
lastname: 'Agent1',
email: 'agent@example.com',
password: 'agentpw',
active: true,
roles: roles,
updated_by_id: 1,
created_by_id: 1,
)
roles = Role.where(name: 'Customer')
customer = User.create_or_update(
login: 'customer@example.com',
firstname: 'Trigger',
lastname: 'Customer1',
email: 'customer@example.com',
password: 'customerpw',
vip: true,
active: true,
roles: roles,
updated_by_id: 1,
created_by_id: 1,
)
ticket1 = Ticket.create(
title: 'test 123',
group: Group.lookup(name: 'Users'),
customer: customer,
updated_by_id: 1,
created_by_id: 1,
)
Ticket::Article.create(
ticket_id: ticket1.id,
from: 'some_sender@example.com',
to: 'some_recipient@example.com',
subject: 'some subject',
message_id: 'some@id',
body: "some message <b>note</b>\nnew line",
internal: false,
sender: Ticket::Article::Sender.find_by(name: 'Agent'),
type: Ticket::Article::Type.find_by(name: 'note'),
updated_by_id: 1,
created_by_id: 1,
)
Observer::Transaction.commit
assert_equal('test 123', ticket1.title, 'ticket1.title verify')
assert_equal('Users', ticket1.group.name, 'ticket1.group verify')
assert_equal(1, ticket1.owner_id, 'ticket1.owner_id verify')
assert_equal('new', ticket1.state.name, 'ticket1.state verify')
assert_equal('2 normal', ticket1.priority.name, 'ticket1.priority verify')
assert_equal(1, ticket1.articles.count, 'ticket1.articles verify')
assert_equal([], ticket1.tag_list)
ticket1.update_attribute(:customer, User.lookup(email: 'nicole.braun@zammad.org') )
UserInfo.current_user_id = agent.id
Ticket::Article.create(
ticket_id: ticket1.id,
from: 'some_sender@example.com',
to: 'some_recipient@example.com',
subject: 'update',
message_id: 'some@id',
content_type: 'text/html',
body: 'update',
internal: false,
sender: Ticket::Article::Sender.find_by(name: 'Agent'),
type: Ticket::Article::Type.find_by(name: 'note'),
)
Observer::Transaction.commit
UserInfo.current_user_id = nil
ticket1.reload
assert_equal('test 123', ticket1.title, 'ticket1.title verify')
assert_equal('Users', ticket1.group.name, 'ticket1.group verify')
assert_equal(agent.id, ticket1.owner_id, 'ticket1.owner_id verify')
assert_equal('new', ticket1.state.name, 'ticket1.state verify')
assert_equal('2 normal', ticket1.priority.name, 'ticket1.priority verify')
assert_equal(2, ticket1.articles.count, 'ticket1.articles verify')
assert_equal([], ticket1.tag_list)
end
test '6.2 owner auto assignment based on organization' do
trigger1 = Trigger.create_or_update(
name: 'aaa auto assignment',
condition: {
'ticket.organization_id' => {
'operator' => 'is',
'pre_condition' => 'not_set',
'value' => '',
'value_completion' => '',
},
'ticket.action' => {
'operator' => 'is',
'value' => 'update',
},
},
perform: {
'ticket.owner_id' => {
'pre_condition' => 'current_user.id',
'value' => '',
'value_completion' => '',
},
},
disable_notification: true,
active: true,
created_by_id: 1,
updated_by_id: 1,
)
roles = Role.where(name: 'Agent')
agent = User.create_or_update(
login: 'agent@example.com',
firstname: 'Trigger',
lastname: 'Agent1',
email: 'agent@example.com',
password: 'agentpw',
active: true,
roles: roles,
updated_by_id: 1,
created_by_id: 1,
)
roles = Role.where(name: 'Customer')
customer = User.create_or_update(
login: 'customer@example.com',
firstname: 'Trigger',
lastname: 'Customer1',
email: 'customer@example.com',
password: 'customerpw',
vip: true,
active: true,
roles: roles,
updated_by_id: 1,
created_by_id: 1,
)
ticket1 = Ticket.create(
title: 'test 123',
group: Group.lookup(name: 'Users'),
customer: User.lookup(email: 'nicole.braun@zammad.org'),
updated_by_id: 1,
created_by_id: 1,
)
Ticket::Article.create(
ticket_id: ticket1.id,
from: 'some_sender@example.com',
to: 'some_recipient@example.com',
subject: 'some subject',
message_id: 'some@id',
body: "some message <b>note</b>\nnew line",
internal: false,
sender: Ticket::Article::Sender.find_by(name: 'Agent'),
type: Ticket::Article::Type.find_by(name: 'note'),
updated_by_id: 1,
created_by_id: 1,
)
Observer::Transaction.commit
assert_equal('test 123', ticket1.title, 'ticket1.title verify')
assert_equal('Users', ticket1.group.name, 'ticket1.group verify')
assert_equal(1, ticket1.owner_id, 'ticket1.owner_id verify')
assert_equal('new', ticket1.state.name, 'ticket1.state verify')
assert_equal('2 normal', ticket1.priority.name, 'ticket1.priority verify')
assert_equal(1, ticket1.articles.count, 'ticket1.articles verify')
assert_equal([], ticket1.tag_list)
ticket1.update_attribute(:customer, customer )
UserInfo.current_user_id = agent.id
Ticket::Article.create(
ticket_id: ticket1.id,
from: 'some_sender@example.com',
to: 'some_recipient@example.com',
subject: 'update',
message_id: 'some@id',
content_type: 'text/html',
body: 'update',
internal: false,
sender: Ticket::Article::Sender.find_by(name: 'Agent'),
type: Ticket::Article::Type.find_by(name: 'note'),
)
Observer::Transaction.commit
UserInfo.current_user_id = nil
ticket1.reload
assert_equal('test 123', ticket1.title, 'ticket1.title verify')
assert_equal('Users', ticket1.group.name, 'ticket1.group verify')
assert_equal(agent.id, ticket1.owner_id, 'ticket1.owner_id verify')
assert_equal('new', ticket1.state.name, 'ticket1.state verify')
assert_equal('2 normal', ticket1.priority.name, 'ticket1.priority verify')
assert_equal(2, ticket1.articles.count, 'ticket1.articles verify')
assert_equal([], ticket1.tag_list)
end
test '7 owner auto assignment' do test '7 owner auto assignment' do
trigger1 = Trigger.create_or_update( trigger1 = Trigger.create_or_update(
name: 'aaa auto assignment', name: 'aaa auto assignment',
@ -3120,9 +3342,8 @@ class TicketTriggerTest < ActiveSupport::TestCase
Observer::Transaction.commit Observer::Transaction.commit
ticket1.reload ticket1.reload
assert_equal(22, ticket1.articles.count) assert_equal(21, ticket1.articles.count)
assert_equal('some_loop_sender@example.com', ticket1.articles[20].from) assert_equal('some_loop_sender@example.com', ticket1.articles[20].from)
assert_equal('nicole.braun@zammad.org', ticket1.articles[21].to)
Ticket::Article.create( Ticket::Article.create(
ticket_id: ticket1.id, ticket_id: ticket1.id,
@ -3141,92 +3362,8 @@ class TicketTriggerTest < ActiveSupport::TestCase
Observer::Transaction.commit Observer::Transaction.commit
ticket1.reload ticket1.reload
assert_equal(24, ticket1.articles.count) assert_equal(22, ticket1.articles.count)
assert_equal('some_loop_sender@example.com', ticket1.articles[22].from) assert_equal('some_loop_sender@example.com', ticket1.articles[21].from)
assert_equal('nicole.braun@zammad.org', ticket1.articles[23].to)
Ticket::Article.create(
ticket_id: ticket1.id,
from: 'some_loop_sender@example.com',
to: 'some_loop_recipient@example.com',
subject: 'some subject 1234',
message_id: 'some@id',
content_type: 'text/html',
body: 'some message <b>note</b><br>new line',
internal: false,
sender: Ticket::Article::Sender.find_by(name: 'Customer'),
type: Ticket::Article::Type.find_by(name: 'email'),
updated_by_id: 1,
created_by_id: 1,
)
Observer::Transaction.commit
ticket1.reload
assert_equal(26, ticket1.articles.count)
assert_equal('some_loop_sender@example.com', ticket1.articles[24].from)
assert_equal('nicole.braun@zammad.org', ticket1.articles[25].to)
Ticket::Article.create(
ticket_id: ticket1.id,
from: 'some_loop_sender@example.com',
to: 'some_loop_recipient@example.com',
subject: 'some subject 1234',
message_id: 'some@id',
content_type: 'text/html',
body: 'some message <b>note</b><br>new line',
internal: false,
sender: Ticket::Article::Sender.find_by(name: 'Customer'),
type: Ticket::Article::Type.find_by(name: 'email'),
updated_by_id: 1,
created_by_id: 1,
)
Observer::Transaction.commit
ticket1.reload
assert_equal(28, ticket1.articles.count)
assert_equal('some_loop_sender@example.com', ticket1.articles[26].from)
assert_equal('nicole.braun@zammad.org', ticket1.articles[27].to)
Ticket::Article.create(
ticket_id: ticket1.id,
from: 'some_loop_sender@example.com',
to: 'some_loop_recipient@example.com',
subject: 'some subject 1234',
message_id: 'some@id',
content_type: 'text/html',
body: 'some message <b>note</b><br>new line',
internal: false,
sender: Ticket::Article::Sender.find_by(name: 'Customer'),
type: Ticket::Article::Type.find_by(name: 'email'),
updated_by_id: 1,
created_by_id: 1,
)
Observer::Transaction.commit
ticket1.reload
assert_equal(30, ticket1.articles.count)
assert_equal('some_loop_sender@example.com', ticket1.articles[28].from)
assert_equal('nicole.braun@zammad.org', ticket1.articles[29].to)
Ticket::Article.create(
ticket_id: ticket1.id,
from: 'some_loop_sender@example.com',
to: 'some_loop_recipient@example.com',
subject: 'some subject 1234',
message_id: 'some@id',
content_type: 'text/html',
body: 'some message <b>note</b><br>new line',
internal: false,
sender: Ticket::Article::Sender.find_by(name: 'Customer'),
type: Ticket::Article::Type.find_by(name: 'email'),
updated_by_id: 1,
created_by_id: 1,
)
Observer::Transaction.commit
ticket1.reload
assert_equal(31, ticket1.articles.count)
assert_equal('some_loop_sender@example.com', ticket1.articles[30].from)
end end

View file

@ -204,6 +204,42 @@ class UserDeviceTest < ActiveSupport::TestCase
) )
assert_equal(user_device2.id, user_device6.id) assert_equal(user_device2.id, user_device6.id)
# signin without ua from country A via basic auth -> new device #3
user_device7 = UserDevice.add(
'',
'91.115.248.231',
@agent.id,
nil,
'basic_auth',
)
assert_not_equal(user_device6.id, user_device7.id)
user_device8 = UserDevice.add(
'',
'91.115.248.231',
@agent.id,
nil,
'basic_auth',
)
assert_equal(user_device7.id, user_device8.id)
user_device9 = UserDevice.add(
nil,
'91.115.248.231',
@agent.id,
nil,
'basic_auth',
)
assert_equal(user_device8.id, user_device9.id)
user_device10 = UserDevice.add(
nil,
'176.198.137.254',
@agent.id,
nil,
'basic_auth',
)
assert_not_equal(user_device9.id, user_device10.id)
end end
test 'ddd - api test' do test 'ddd - api test' do