diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9cbd0e4ee..fdf637e0e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -40,6 +40,32 @@ pre:github: script: - script/build/sync_repo.sh git@github.com:zammad/zammad.git +test:rspec:mysql: + stage: test + tags: + - core + - mysql + script: + - export RAILS_ENV=test + - rake db:create + - rake db:migrate + - rake db:seed + - rspec + - rake db:drop + +test:rspec:postgresql: + stage: test + tags: + - core + - postgresql + script: + - export RAILS_ENV=test + - rake db:create + - rake db:migrate + - rake db:seed + - rspec + - rake db:drop + test:unit:mysql: stage: test tags: diff --git a/.rspec b/.rspec new file mode 100644 index 000000000..34c5164d9 --- /dev/null +++ b/.rspec @@ -0,0 +1,3 @@ +--format documentation +--color +--require spec_helper diff --git a/Gemfile b/Gemfile index 307af7e9b..39a5805e2 100644 --- a/Gemfile +++ b/Gemfile @@ -85,8 +85,10 @@ gem 'diffy' # in production environments by default. group :development, :test do + gem 'rspec-rails' gem 'test-unit' gem 'spring' + gem 'spring-commands-rspec' gem 'sqlite3' # code coverage diff --git a/Gemfile.lock b/Gemfile.lock index 81962222b..312357431 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -77,6 +77,7 @@ GEM delayed_job_active_record (4.1.1) activerecord (>= 3.0, < 5.1) delayed_job (>= 3.0, < 5) + diff-lcs (1.2.5) diffy (3.1.0) dnsruby (1.59.3) docile (1.1.5) @@ -271,6 +272,23 @@ GEM ffi (>= 0.5.0) ref (2.0.0) retriable (2.1.0) + rspec-core (3.5.4) + rspec-support (~> 3.5.0) + rspec-expectations (3.5.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.5.0) + rspec-mocks (3.5.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.5.0) + rspec-rails (3.5.2) + actionpack (>= 3.0) + activesupport (>= 3.0) + railties (>= 3.0) + rspec-core (~> 3.5.0) + rspec-expectations (~> 3.5.0) + rspec-mocks (~> 3.5.0) + rspec-support (~> 3.5.0) + rspec-support (3.5.0) rubocop (0.42.0) parser (>= 2.3.1.1, < 3.0) powerpack (~> 0.1) @@ -308,6 +326,8 @@ GEM slack-notifier (1.5.1) slop (3.6.0) spring (1.7.2) + spring-commands-rspec (1.0.4) + spring (>= 0.9.1) sprockets (3.7.0) concurrent-ruby (~> 1.0) rack (> 1, < 3) @@ -403,6 +423,7 @@ DEPENDENCIES rails (= 4.2.7.1) rails-observers rb-fsevent + rspec-rails rubocop sass-rails selenium-webdriver @@ -411,6 +432,7 @@ DEPENDENCIES simplecov-rcov slack-notifier spring + spring-commands-rspec sprockets sqlite3 test-unit diff --git a/app/assets/javascripts/app/controllers/_ui_element/ticket_selector.coffee b/app/assets/javascripts/app/controllers/_ui_element/ticket_selector.coffee index 344441b06..5a4b0d18e 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/ticket_selector.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/ticket_selector.coffee @@ -192,6 +192,7 @@ class App.UiElement.ticket_selector @ticketTable: (ticket_ids, ticket_count, item) -> item.find('.js-previewCounter').html(ticket_count) new App.TicketList( + tableId: 'ticket-selector' el: item.find('.js-previewTable') ticket_ids: ticket_ids ) diff --git a/app/assets/javascripts/app/controllers/agent_ticket_merge.coffee b/app/assets/javascripts/app/controllers/agent_ticket_merge.coffee index a42fe5613..e30750f23 100644 --- a/app/assets/javascripts/app/controllers/agent_ticket_merge.coffee +++ b/app/assets/javascripts/app/controllers/agent_ticket_merge.coffee @@ -26,12 +26,14 @@ class App.TicketMerge extends App.ControllerModal content = $( App.view('agent_ticket_merge')() ) new App.TicketList( + tableId: 'ticket-merge-customer-tickets' el: content.find('#ticket-merge-customer-tickets') ticket_ids: @ticket_ids_by_customer radio: true ) new App.TicketList( + tableId: 'ticket-merge-recent-tickets' el: content.find('#ticket-merge-recent-tickets') ticket_ids: @ticket_ids_recent_viewed radio: true diff --git a/app/assets/javascripts/app/controllers/monitoring.coffee b/app/assets/javascripts/app/controllers/monitoring.coffee new file mode 100644 index 000000000..5850e8102 --- /dev/null +++ b/app/assets/javascripts/app/controllers/monitoring.coffee @@ -0,0 +1,47 @@ +class Index extends App.ControllerSubContent + requiredPermission: 'admin.monitoring' + header: 'Monitoring' + events: + 'click .js-resetToken': 'resetToken' + 'click .js-select': 'selectAll' + + constructor: -> + super + @load() + @interval( + => + @load() + 35000 + ) + + # fetch data, render view + load: -> + @startLoading() + @ajax( + id: 'health_check' + type: 'GET' + url: "#{@apiPath}/monitoring/health_check" + success: (data) => + @stopLoading() + console.log('111', data, @data) + return if @data && data.token is @data.token && data.healthy is @data.healthy && data.message is @data.message + console.log('222') + @data = data + @render() + ) + + render: => + @html App.view('monitoring')(data: @data) + + resetToken: (e) => + e.preventDefault() + @formDisable(e) + @ajax( + id: 'health_check_token' + type: 'POST' + url: "#{@apiPath}/monitoring/token" + success: (data) => + @load() + ) + +App.Config.set('Monitoring', { prio: 3600, name: 'Monitoring', parent: '#system', target: '#system/monitoring', controller: Index, permission: ['admin.monitoring'] }, 'NavBarAdmin') diff --git a/app/assets/javascripts/app/controllers/package.coffee b/app/assets/javascripts/app/controllers/package.coffee index 00d9702a0..8f7bb7af0 100644 --- a/app/assets/javascripts/app/controllers/package.coffee +++ b/app/assets/javascripts/app/controllers/package.coffee @@ -54,4 +54,4 @@ class Index extends App.ControllerSubContent @load() ) -App.Config.set('Packages', { prio: 3600, name: 'Packages', parent: '#system', target: '#system/package', controller: Index, permission: ['admin.package'] }, 'NavBarAdmin') +App.Config.set('Packages', { prio: 3700, name: 'Packages', parent: '#system', target: '#system/package', controller: Index, permission: ['admin.package'] }, 'NavBarAdmin') diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_actions.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_actions.coffee index bc8987d6a..20e8c49ef 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/article_actions.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_actions.coffee @@ -97,15 +97,15 @@ class App.TicketZoomArticleActions extends App.Controller recipientUsed = {} for recipient in recipients if !_.isEmpty(recipient.address) - localRecipientAddeess = recipient.address.toString().toLowerCase() - if !recipientUsed[localRecipientAddeess] - recipientUsed[localRecipientAddeess] = true - localAddess = false + localRecipientAddress = recipient.address.toString().toLowerCase() + if !recipientUsed[localRecipientAddress] + recipientUsed[localRecipientAddress] = true + localAddress = false for address in localAddresses - if localRecipientAddeess is address.email.toString().toLowerCase() - recipientUsed[localRecipientAddeess] = true - localAddess = true - if !localAddess + if localRecipientAddress is address.email.toString().toLowerCase() + recipientUsed[localRecipientAddress] = true + localAddress = true + if !localAddress forgeinRecipients.push recipient # check if reply all is neede @@ -327,12 +327,12 @@ class App.TicketZoomArticleActions extends App.Controller for recipient in recipients if !_.isEmpty(recipient.address) - # check if addess is not local - localAddess = false + # check if address is not local + localAddress = false for address in localAddresses if !_.isEmpty(recipient.address) && recipient.address.toString().toLowerCase() == address.email.toString().toLowerCase() - localAddess = true - if !localAddess + localAddress = true + if !localAddress # filter for uniq recipients if !recipientAddresses[ recipient.address.toString().toLowerCase() ] diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_view.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_view.coffee index 17b8029de..7dff6740d 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/article_view.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_view.coffee @@ -239,11 +239,12 @@ class ArticleViewItem extends App.ObserverController toggleMetaWithDelay: (e) => # allow double click select # by adding a delay to the toggle + delay = 120 - if @lastClick and +new Date - @lastClick < 80 + if @lastClick and +new Date - @lastClick < delay clearTimeout(@toggleMetaTimeout) else - @toggleMetaTimeout = setTimeout(@toggleMeta, 80, e) + @toggleMetaTimeout = setTimeout(@toggleMeta, delay, e) @lastClick = +new Date toggleMeta: (e) => diff --git a/app/assets/javascripts/app/lib/app_post/global_search.coffee b/app/assets/javascripts/app/lib/app_post/global_search.coffee index 298e2b46a..42061f3f7 100644 --- a/app/assets/javascripts/app/lib/app_post/global_search.coffee +++ b/app/assets/javascripts/app/lib/app_post/global_search.coffee @@ -9,7 +9,8 @@ class App.GlobalSearch class _globalSearchSingleton extends Spine.Module constructor: -> - @searchResultCache = {} + @searchResultCache = undefined + @searchResultCacheByKey = {} @apiPath = App.Config.get('api_path') execute: (params) -> @@ -20,8 +21,8 @@ class _globalSearchSingleton extends Spine.Module # use cache for search result currentTime = new Date - if @searchResultCache[cacheKey] && @searchResultCache[cacheKey].time > currentTime.setSeconds(currentTime.getSeconds() - 20) - render(@searchResultCache[cacheKey].result) + if @searchResultCacheByKey[cacheKey] && @searchResultCacheByKey[cacheKey].time > currentTime.setSeconds(currentTime.getSeconds() - 20) + @renderTry(render, @searchResultCacheByKey[cacheKey].result, cacheKey) return App.Ajax.request( @@ -31,7 +32,7 @@ class _globalSearchSingleton extends Spine.Module data: query: query limit: limit - processData: true, + processData: true success: (data, status, xhr) => App.Collection.loadAssets(data.assets) result = {} @@ -48,19 +49,21 @@ class _globalSearchSingleton extends Spine.Module else App.Log.error('_globalSearchSingleton', "No such model App.#{item.type}") - diff = false - if @searchResultCache[cacheKey] - diff = difference(@searchResultCache[cacheKey].resultRaw, data.result) - - # cache search result - @searchResultCache[cacheKey] = - result: result - resultRaw: data.result - limit: limit - time: new Date - - # if result hasn't changed, do not rerender - return if diff isnt false && _.isEmpty(diff) - - render(result) + @renderTry(render, result, cacheKey) ) + + renderTry: (render, result, cacheKey) => + + # if result hasn't changed, do not rerender + diff = false + if @searchResultCache + diff = difference(@searchResultCache, result) + return if diff isnt false && _.isEmpty(diff) + + # cache search result + @searchResultCache = result + @searchResultCacheByKey[cacheKey] = + result: result + time: new Date + + render(result) diff --git a/app/assets/javascripts/app/lib/app_post/i18n.coffee b/app/assets/javascripts/app/lib/app_post/i18n.coffee index f9f6fb251..ba7ae9a5d 100644 --- a/app/assets/javascripts/app/lib/app_post/i18n.coffee +++ b/app/assets/javascripts/app/lib/app_post/i18n.coffee @@ -288,9 +288,11 @@ class _i18nSingleton extends Spine.Module @_notTranslated[locale][key] = true date: (time, offset) => + return time if !time @convert(time, offset, @mapTime['date'] || @dateFormat) timestamp: (time, offset) => + return time if !time @convert(time, offset, @mapTime['timestamp'] || @timestampFormat) convert: (time, offset, format) -> diff --git a/app/assets/javascripts/app/views/api.jst.eco b/app/assets/javascripts/app/views/api.jst.eco index de29d157c..6126a1111 100644 --- a/app/assets/javascripts/app/views/api.jst.eco +++ b/app/assets/javascripts/app/views/api.jst.eco @@ -12,7 +12,7 @@

<%- @T('Token Access') %> (HTTP Token Authentication)

-

<%- @T('Enable REST API using tokens (not username/email addeess and password). Each user need to create own access tokens in user profile.') %>

+

<%- @T('Enable REST API using tokens (not username/email address and password). Each user need to create own access tokens in user profile.') %>

<%- @T('Example') %>:

@@ -74,4 +74,4 @@ OAuth URLs are: - \ No newline at end of file + diff --git a/app/assets/javascripts/app/views/import/otrs.jst.eco b/app/assets/javascripts/app/views/import/otrs.jst.eco index caa7222c7..379aa6b38 100644 --- a/app/assets/javascripts/app/views/import/otrs.jst.eco +++ b/app/assets/javascripts/app/views/import/otrs.jst.eco @@ -7,7 +7,9 @@

<%- @T('Download and install the OTRS Migration Plugin on your OTRS System') %>:

- <%- @Icon('download') %> <%- @T('Migration Plugin') %> + <%- @Icon('download') %> <%- @T('Migration Plugin for OTRS 5') %> + <%- @Icon('download') %> <%- @T('Migration Plugin for OTRS 4') %> + <%- @Icon('download') %> <%- @T('Migration Plugin for OTRS 3.3 - 3.1') %>
<%- @T('Go Back') %> diff --git a/app/assets/javascripts/app/views/monitoring.jst.eco b/app/assets/javascripts/app/views/monitoring.jst.eco new file mode 100644 index 000000000..053c128ed --- /dev/null +++ b/app/assets/javascripts/app/views/monitoring.jst.eco @@ -0,0 +1,39 @@ + +
+ +
+
+

<%- @T('Current Token') %>

+
+

+ +
+ +
+
+

<%- @T('Health Check') %>

+
+

<%- @T('Health information can be retrieved as JSON using') %>:

+

+
+ +
+
+

<% if _.isEmpty(@data.issues): %><%- @Icon('status', 'ok inline') %><% else: %><%- @Icon('status', 'error inline') %><% end %> <%- @T('Current Status') %>

+
+
    + <% if _.isEmpty(@data.issues): %> +
  • <%- @T('no issues') %> + <% else: %> + <% for issue in @data.issues: %> +
  • <%= issue %> + <% end %> + <% end %> +
+
+ +
\ No newline at end of file diff --git a/app/assets/javascripts/app/views/translation/index.jst.eco b/app/assets/javascripts/app/views/translation/index.jst.eco index 39b5858aa..522ea468a 100644 --- a/app/assets/javascripts/app/views/translation/index.jst.eco +++ b/app/assets/javascripts/app/views/translation/index.jst.eco @@ -13,8 +13,8 @@

<%- @T('Inline translation') %>

<%- @T('To do easier translations you can enable and disable inline translation feature by pressing "%s".', 'ctrl+alt+t') %>

-

<%- @T('Text with disabled inline translations looks like') %>

-

<%- @T('Text with enabled inline translations looks like') %>

+

<%- @T('Text with disabled inline translations looks like') %> 

+

<%- @T('Text with enabled inline translations looks like') %> 

<%- @T('Just click into the marker and update the words just in place. Enjoy!') %>

<%- @T('If you want to translate it via the translation table, just go ahead below.') %>

diff --git a/app/assets/stylesheets/zammad.scss b/app/assets/stylesheets/zammad.scss index 9974648c3..de71c75dd 100644 --- a/app/assets/stylesheets/zammad.scss +++ b/app/assets/stylesheets/zammad.scss @@ -608,7 +608,7 @@ pre code.hljs { margin-left: 15px; } -.vertical > .btn + .btn { +.vertical > .btn:not(.hidden) + .btn { margin-left: 0; margin-top: 10px; } @@ -4406,6 +4406,7 @@ footer { .article-content { color: hsl(60,1%,34%); position: relative; + z-index: 1; padding: 0 55px; } @@ -4751,6 +4752,10 @@ footer { border-radius: 0 4px 4px 0; } + .pop-selectable:only-child { + border-radius: 4px; + } + .pop-selectable-icon { fill: hsl(231,3%,40%); } @@ -5384,6 +5389,10 @@ footer { margin-top: 12px; } + .box ul { + padding-left: 10px; + } + .box .two-columns { margin-left: -4px; margin-right: -4px; @@ -5433,6 +5442,15 @@ footer { } } +.horizontal > .box { + margin-top: 4px; + margin-bottom: 4px; + + & + .box { + border-left-width: 0; + } +} + .formset-inset { margin: 34px -24px 24px; padding: 19px 24px 24px; @@ -8434,6 +8452,10 @@ body.fit { align-self: end; } +.span-width { + flex-basis: 100%; +} + .two-columns, .three-columns, .wrap { @@ -8468,3 +8490,17 @@ body.fit { margin-left: auto; margin-right: auto; } + +.spacer { + width: 10px; + height: 10px; +} + +.double-spacer { + width: 20px; + height: 20px; +} + +.flex-spacer { + flex: 1; +} diff --git a/app/controllers/integration/sipgate_controller.rb b/app/controllers/integration/sipgate_controller.rb index c45335cee..69ae1c113 100644 --- a/app/controllers/integration/sipgate_controller.rb +++ b/app/controllers/integration/sipgate_controller.rb @@ -4,14 +4,11 @@ require 'builder' class Integration::SipgateController < ApplicationController + before_action :check_configured + # notify about inbound call / block inbound call def in - http_log_config facility: 'sipgate.io' - return if !configured? - if params['event'] == 'newCall' - - config = Setting.get('sipgate_config') config_inbound = config[:inbound] || {} block_caller_ids = config_inbound[:block_caller_ids] || [] @@ -31,12 +28,12 @@ class Integration::SipgateController < ApplicationController if params['user'] params['comment'] = "#{params['user']} -> reject, busy" end - update_log(params) + Cti::Log.process(params) return true } end - update_log(params) + Cti::Log.process(params) xml = Builder::XmlMarkup.new(indent: 2) xml.instruct! @@ -46,10 +43,6 @@ class Integration::SipgateController < ApplicationController # set caller id of outbound call def out - http_log_config facility: 'sipgate.io' - return if !configured? - - config = Setting.get('sipgate_config') config_outbound = config[:outbound][:routing_table] default_caller_id = config[:outbound][:default_caller_id] @@ -84,125 +77,26 @@ class Integration::SipgateController < ApplicationController if from params['from'] = from end - update_log(params) + Cti::Log.process(params) end private - def configured? + def check_configured + http_log_config facility: 'sipgate.io' + if !Setting.get('sipgate_integration') xml_error('Feature is disable, please contact your admin to enable it!') - return false + return end - config = Setting.get('sipgate_config') if !config || !config[:inbound] || !config[:outbound] xml_error('Feature not configured, please contact your admin!') - return false + return end - true end - def update_log(params) - - user = params['user'] - if params['user'] && params['user'].class == Array - user = params['user'].join(', ') - end - from_comment = nil - to_comment = nil - preferences = nil - if params['direction'] == 'in' - to_comment = user - from_comment, preferences = update_log_item('from') - else - from_comment = user - to_comment, preferences = update_log_item('to') - end - - comment = nil - if params['cause'] - comment = params['cause'] - end - - if params['event'] == 'newCall' - Cti::Log.create( - direction: params['direction'], - from: params['from'], - from_comment: from_comment, - to: params['to'], - to_comment: to_comment, - call_id: params['callId'], - comment: comment, - state: params['event'], - preferences: preferences, - ) - elsif params['event'] == 'answer' - log = Cti::Log.find_by(call_id: params['callId']) - raise "No such call_id #{params['callId']}" if !log - log.state = 'answer' - log.start = Time.zone.now - if user - log.to_comment = user - end - log.comment = comment - log.save - elsif params['event'] == 'hangup' - log = Cti::Log.find_by(call_id: params['callId']) - raise "No such call_id #{params['callId']}" if !log - if params['direction'] == 'in' && log.state == 'newCall' - log.done = false - end - if params['direction'] == 'in' && log.to_comment == 'voicemail' - log.done = false - end - log.state = 'hangup' - log.end = Time.zone.now - log.comment = comment - log.save - else - raise "Unknown event #{params['event']}" - end - - end - - def update_log_item(direction) - from_comment_known = '' - from_comment_maybe = '' - preferences_known = {} - preferences_known[direction] = [] - preferences_maybe = {} - preferences_maybe[direction] = [] - caller_ids = Cti::CallerId.lookup(params[direction]) - caller_ids.each { |record| - if record.level == 'known' - preferences_known[direction].push record - else - preferences_maybe[direction].push record - end - comment = '' - if record.user_id - user = User.lookup(id: record.user_id) - if user - comment += user.fullname - end - elsif !record.comment.empty? - comment += record.comment - end - if record.level == 'known' - if !from_comment_known.empty? - from_comment_known += ',' - end - from_comment_known += comment - else - if !from_comment_maybe.empty? - from_comment_maybe += ',' - end - from_comment_maybe += comment - end - } - return [from_comment_known, preferences_known] if !from_comment_known.empty? - return ["maybe #{from_comment_maybe}", preferences_maybe] if !from_comment_maybe.empty? - nil + def config + @config ||= Setting.get('sipgate_config') end def xml_error(error) diff --git a/app/controllers/monitoring_controller.rb b/app/controllers/monitoring_controller.rb new file mode 100644 index 000000000..1fa9c3bdc --- /dev/null +++ b/app/controllers/monitoring_controller.rb @@ -0,0 +1,182 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ + +class MonitoringController < ApplicationController + before_action -> { authentication_check(permission: 'admin.monitoring') }, except: [:health_check, :status] + +=begin + +Resource: +GET /api/v1/monitoring/health_check?token=XXX + +Response: +{ + "healthy": true, + "message": "success", +} + +{ + "healthy": false, + "message": "authentication of XXX failed; issue #2", + "issues": ["authentication of XXX failed", "issue #2"], +} + +Test: +curl http://localhost/api/v1/monitoring/health_check?token=XXX + +=end + + def health_check + token_or_permission_check + + issues = [] + + # channel check + Channel.where(active: true).each { |channel| + next if (channel.status_in.empty? || channel.status_in == 'ok') && (channel.status_out.empty? || channel.status_out == 'ok') + if channel.status_in == 'error' + message = "Channel: #{channel.area} in " + %w(host user uid).each { |key| + next if !channel.options[key] || channel.options[key].empty? + message += "key:#{channel.options[key]};" + } + issues.push "#{message} #{channel.last_log_in}" + end + next if channel.status_out != 'error' + message = "Channel: #{channel.area} out " + %w(host user uid).each { |key| + next if !channel.options[key] || channel.options[key].empty? + message += "key:#{channel.options[key]};" + } + issues.push "#{message} #{channel.last_log_out}" + } + + # unprocessable mail check + directory = "#{Rails.root}/tmp/unprocessable_mail" + if File.exist?(directory) + count = 0 + Dir.glob("#{directory}/*.eml") { |_entry| + count += 1 + } + if count.nonzero? + issues.push "unprocessable mails: #{count}" + end + end + + # scheduler check + Scheduler.where(active: true).where.not(last_run: nil).each { |scheduler| + next if scheduler.period <= 300 + next if scheduler.last_run + scheduler.period.seconds > Time.zone.now - 5.minutes + issues.push 'scheduler not running' + break + } + if Scheduler.where(active: true, last_run: nil).count == Scheduler.where(active: true).count + issues.push 'scheduler not running' + end + + token = Setting.get('monitoring_token') + + if issues.empty? + result = { + healthy: true, + message: 'success', + token: token, + } + render json: result + return + end + + result = { + healthy: false, + message: issues.join(';'), + issues: issues, + token: token, + } + render json: result + end + +=begin + +Resource: +GET /api/v1/monitoring/status?token=XXX + +Response: +{ + "agents": 8123, + "last_login": "2016-11-21T14:14:14Z", + "counts": { + "users": 12313, + "tickets": 23123, + "ticket_articles": 131451, + }, + "last_created_at": { + "users": "2016-11-21T14:14:14Z", + "tickets": "2016-11-21T14:14:14Z", + "ticket_articles": "2016-11-21T14:14:14Z", + }, +} + +Test: +curl http://localhost/api/v1/monitoring/status?token=XXX + +=end + + def status + token_or_permission_check + + last_login = nil + last_login_user = User.where('last_login IS NOT NULL').order(last_login: :desc).limit(1).first + if last_login_user + last_login = last_login_user.last_login + end + + status = { + counts: {}, + last_created_at: {}, + last_login: last_login, + agents: User.with_permissions('ticket.agent').count, + } + + map = { + users: User, + groups: Group, + overviews: Overview, + tickets: Ticket, + ticket_articles: Ticket::Article, + } + map.each { |key, class_name| + status[:counts][key] = class_name.count + last = class_name.last + status[:last_created_at][key] = if last + last.created_at + end + } + + render json: status + end + + def token + access_check + token = SecureRandom.urlsafe_base64(40) + Setting.set('monitoring_token', token) + + result = { + token: token, + } + render json: result, status: :created + end + + private + + def token_or_permission_check + user = authentication_check_only(permission: 'admin.monitoring') + return if user + return if Setting.get('monitoring_token') == params[:token] + raise Exceptions::NotAuthorized + end + + def access_check + return if Permission.find_by(name: 'admin.monitoring', active: true) + raise Exceptions::NotAuthorized + end + +end diff --git a/app/models/channel/driver/imap.rb b/app/models/channel/driver/imap.rb index ee0efbb87..733efd836 100644 --- a/app/models/channel/driver/imap.rb +++ b/app/models/channel/driver/imap.rb @@ -73,7 +73,7 @@ example Rails.logger.info "fetching imap (#{options[:host]}/#{options[:user]} port=#{port},ssl=#{ssl})" # on check, reduce open_timeout to have faster probing - timeout = 12 + timeout = 24 if check_type == 'check' timeout = 6 end diff --git a/app/models/channel/driver/pop3.rb b/app/models/channel/driver/pop3.rb index cd45ab61e..a227b68eb 100644 --- a/app/models/channel/driver/pop3.rb +++ b/app/models/channel/driver/pop3.rb @@ -61,8 +61,8 @@ returns #@pop.set_debug_output $stderr # on check, reduce open_timeout to have faster probing - @pop.open_timeout = 8 - @pop.read_timeout = 12 + @pop.open_timeout = 12 + @pop.read_timeout = 24 if check_type == 'check' @pop.open_timeout = 4 @pop.read_timeout = 6 diff --git a/app/models/channel/email_build.rb b/app/models/channel/email_build.rb index 540ae4763..8302228e5 100644 --- a/app/models/channel/email_build.rb +++ b/app/models/channel/email_build.rb @@ -32,8 +32,8 @@ module Channel::EmailBuild attr['X-Auto-Response-Suppress'] = 'All' end - #attr['X-Powered-BY'] = 'Zammad - Support/Helpdesk (http://www.zammad.org/)' - attr['X-Mailer'] = 'Zammad Mail Service (1.x)' + attr['X-Powered-By'] = 'Zammad - Helpdesk/Support (https://zammad.org/)' + attr['X-Mailer'] = 'Zammad Mail Service' # set headers attr.each do |key, value| @@ -126,6 +126,21 @@ module Channel::EmailBuild mail end +=begin + + quoted_in_one_line = Channel::EmailBuild.recipient_line('Somebody @ "Company"', 'some.body@example.com') + +returnes + + '"Somebody @ \"Company\"" ' + +=end + + def self.recipient_line(realname, email) + return "#{realname} <#{email}>" if realname =~ /^[A-z]+$/i + "\"#{realname.gsub('"', '\"')}\" <#{email}>" + end + =begin Check if string is a complete html document. If not, add head and css styles. diff --git a/app/models/channel/filter/auto_response_check.rb b/app/models/channel/filter/auto_response_check.rb index c6481beca..588094802 100644 --- a/app/models/channel/filter/auto_response_check.rb +++ b/app/models/channel/filter/auto_response_check.rb @@ -14,11 +14,20 @@ module Channel::Filter::AutoResponseCheck mail[ 'x-zammad-article-preferences'.to_sym ]['send-auto-response'] = false mail[ 'x-zammad-article-preferences'.to_sym ]['is-auto-response'] = true + # do not send an auto respondse if one of the following headers exists + return if mail[ 'list-unsubscribe'.to_sym ] && mail[ 'list-unsubscribe'.to_sym ] =~ /.../ return if mail[ 'x-loop'.to_sym ] && mail[ 'x-loop'.to_sym ] =~ /(yes|true)/i return if mail[ 'precedence'.to_sym ] && mail[ 'precedence'.to_sym ] =~ /(bulk|list|junk)/i return if mail[ 'auto-submitted'.to_sym ] && mail[ 'auto-submitted'.to_sym ] =~ /auto-(generated|replied)/i return if mail[ 'x-auto-response-suppress'.to_sym ] && mail[ 'x-auto-response-suppress'.to_sym ] =~ /all/i + # do not send an auto respondse if sender is system it self + message_id = mail[ 'message_id'.to_sym ] + if message_id + fqdn = Setting.get('fqdn') + return if message_id =~ /@#{Regexp.quote(fqdn)}/i + end + mail[ 'x-zammad-send-auto-response'.to_sym ] = true mail[ 'x-zammad-is-auto-response'.to_sym ] = false diff --git a/app/models/channel/filter/sender_is_system_address.rb b/app/models/channel/filter/sender_is_system_address.rb index 87be321fe..1321edd3f 100644 --- a/app/models/channel/filter/sender_is_system_address.rb +++ b/app/models/channel/filter/sender_is_system_address.rb @@ -8,7 +8,7 @@ module Channel::Filter::SenderIsSystemAddress return if mail[ 'x-zammad-ticket-create-article-sender'.to_sym ] return if mail[ 'x-zammad-article-sender'.to_sym ] - # check if sender addesss is system + # check if sender address is system form = 'raw-from'.to_sym return if !mail[form] return if !mail[:to] diff --git a/app/models/cti/caller_id.rb b/app/models/cti/caller_id.rb index f016d7d42..4118a58df 100644 --- a/app/models/cti/caller_id.rb +++ b/app/models/cti/caller_id.rb @@ -233,5 +233,45 @@ returns caller_ids end + def self.get_comment_preferences(caller_id, direction) + from_comment_known = '' + from_comment_maybe = '' + preferences_known = {} + preferences_known[direction] = [] + preferences_maybe = {} + preferences_maybe[direction] = [] + + lookup(caller_id).each { |record| + if record.level == 'known' + preferences_known[direction].push record + else + preferences_maybe[direction].push record + end + comment = '' + if record.user_id + user = User.lookup(id: record.user_id) + if user + comment += user.fullname + end + elsif !record.comment.empty? + comment += record.comment + end + if record.level == 'known' + if !from_comment_known.empty? + from_comment_known += ',' + end + from_comment_known += comment + else + if !from_comment_maybe.empty? + from_comment_maybe += ',' + end + from_comment_maybe += comment + end + } + return [from_comment_known, preferences_known] if !from_comment_known.empty? + return ["maybe #{from_comment_maybe}", preferences_maybe] if !from_comment_maybe.empty? + nil + end + end end diff --git a/app/models/cti/log.rb b/app/models/cti/log.rb index fc6046ae9..37866c3df 100644 --- a/app/models/cti/log.rb +++ b/app/models/cti/log.rb @@ -254,6 +254,67 @@ returns } end + # processes a incoming event + def self.process(params) + comment = params['cause'] + event = params['event'] + user = params['user'] + if user.class == Array + user = user.join(', ') + end + + from_comment = nil + to_comment = nil + preferences = nil + if params['direction'] == 'in' + to_comment = user + from_comment, preferences = CallerId.get_comment_preferences(params['from'], 'from') + else + from_comment = user + to_comment, preferences = CallerId.get_comment_preferences(params['to'], 'to') + end + + case event + when 'newCall' + create( + direction: params['direction'], + from: params['from'], + from_comment: from_comment, + to: params['to'], + to_comment: to_comment, + call_id: params['callId'], + comment: comment, + state: event, + preferences: preferences, + ) + when 'answer' + log = find_by(call_id: params['callId']) + raise "No such call_id #{params['callId']}" if !log + log.state = 'answer' + log.start = Time.zone.now + if user + log.to_comment = user + end + log.comment = comment + log.save + when 'hangup' + log = find_by(call_id: params['callId']) + raise "No such call_id #{params['callId']}" if !log + if params['direction'] == 'in' && log.state == 'newCall' + log.done = false + end + if params['direction'] == 'in' && log.to_comment == 'voicemail' + log.done = false + end + log.state = 'hangup' + log.end = Time.zone.now + log.comment = comment + log.save + else + raise ArgumentError, "Unknown event #{event}" + end + end + def push_event users = User.with_permissions('cti.agent') users.each { |user| diff --git a/app/models/observer/ticket/article/fillup_from_email.rb b/app/models/observer/ticket/article/fillup_from_email.rb index 1d89764f5..2770e9206 100644 --- a/app/models/observer/ticket/article/fillup_from_email.rb +++ b/app/models/observer/ticket/article/fillup_from_email.rb @@ -46,13 +46,13 @@ class Observer::Ticket::Article::FillupFromEmail < ActiveRecord::Observer if !email_address raise "No email address found for group '#{ticket.group.name}'" end - system_sender = "#{email_address.realname} <#{email_address.email}>" if record.created_by_id != 1 && Setting.get('ticket_define_email_from') == 'AgentNameSystemAddressName' - seperator = Setting.get('ticket_define_email_from_seperator') - sender = User.find(record.created_by_id) - record.from = "#{sender.firstname} #{sender.lastname} #{seperator} #{system_sender}" + seperator = Setting.get('ticket_define_email_from_seperator') + sender = User.find(record.created_by_id) + realname = "#{sender.firstname} #{sender.lastname} #{seperator} #{email_address.realname}" + record.from = Channel::EmailBuild.recipient_line(realname, email_address.email) else - record.from = system_sender + record.from = Channel::EmailBuild.recipient_line(email_address.realname, email_address.email) end end end diff --git a/app/models/setting.rb b/app/models/setting.rb index 5f680f2b8..8dfaeffb0 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -51,11 +51,7 @@ get config setting =end def self.get(name) - if load - logger.debug "Setting.get(#{name.inspect}) # no cache" - else - logger.debug "Setting.get(#{name.inspect}) # from cache" - end + load @@current[:settings_config][name] end diff --git a/app/views/mailer/password_change/en.html.erb b/app/views/mailer/password_change/en.html.erb index 80236ad12..72f939a58 100644 --- a/app/views/mailer/password_change/en.html.erb +++ b/app/views/mailer/password_change/en.html.erb @@ -1,9 +1,9 @@ -Your #{product_name} password has been changed +Your #{config.product_name} password has been changed

Hi #{user.firstname},


-

The password for your #{product_name} account #{user.login} has been changed recently.

+

The password for your #{config.product_name} account #{user.login} has been changed recently.


This activity is not known to you? If not, contact your system administrator.


-

Your #{product_name} Team

+

Your #{config.product_name} Team

diff --git a/bin/rspec b/bin/rspec new file mode 100755 index 000000000..d72fadf39 --- /dev/null +++ b/bin/rspec @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require 'bundler/setup' +load Gem.bin_path('rspec-core', 'rspec') diff --git a/bin/spring b/bin/spring new file mode 100755 index 000000000..7fe232c3a --- /dev/null +++ b/bin/spring @@ -0,0 +1,15 @@ +#!/usr/bin/env ruby + +# This file loads spring without using Bundler, in order to be fast. +# It gets overwritten when you run the `spring binstub` command. + +unless defined?(Spring) + require 'rubygems' + require 'bundler' + + if (match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m)) + Gem.paths = { 'GEM_PATH' => [Bundler.bundle_path.to_s, *Gem.path].uniq.join(Gem.path_separator) } + gem 'spring', match[1] + require 'spring/binstub' + end +end diff --git a/config/environments/development.rb b/config/environments/development.rb index 6f018e1c5..501ea94e4 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -35,6 +35,7 @@ Rails.application.configure do config.middleware.insert_after( ActionDispatch::Static, Rack::LiveReload, + no_swf: true, min_delay: 500, # default 1000 max_delay: 10_000, # default 60_000 live_reload_port: 35_738 diff --git a/config/routes/monitoring.rb b/config/routes/monitoring.rb new file mode 100644 index 000000000..18f2e6c93 --- /dev/null +++ b/config/routes/monitoring.rb @@ -0,0 +1,8 @@ +Zammad::Application.routes.draw do + api_path = Rails.configuration.api_path + + match api_path + '/monitoring/health_check', to: 'monitoring#health_check', via: :get + match api_path + '/monitoring/status', to: 'monitoring#status', via: :get + match api_path + '/monitoring/token', to: 'monitoring#token', via: :post + +end diff --git a/db/migrate/20161122000001_monitoring_issue_453.rb b/db/migrate/20161122000001_monitoring_issue_453.rb new file mode 100644 index 000000000..769ea181f --- /dev/null +++ b/db/migrate/20161122000001_monitoring_issue_453.rb @@ -0,0 +1,37 @@ +class MonitoringIssue453 < ActiveRecord::Migration + def up + # return if it's a new setup + return if !Setting.find_by(name: 'system_init_done') + + Setting.create_if_not_exists( + title: 'Monitoring Token', + name: 'monitoring_token', + area: 'HealthCheck::Base', + description: 'Token for Monitoring.', + options: { + form: [ + { + display: '', + null: false, + name: 'monitoring_token', + tag: 'input', + }, + ], + }, + state: SecureRandom.urlsafe_base64(40), + preferences: { + permission: ['admin.monitoring'], + }, + frontend: false, + ) + + Permission.create_if_not_exists( + name: 'admin.monitoring', + note: 'Manage %s', + preferences: { + translations: ['Monitoring'] + }, + ) + + end +end diff --git a/db/seeds.rb b/db/seeds.rb index b584368de..4882d358f 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1575,11 +1575,11 @@ Setting.create_if_not_exists( frontend: false ) -Setting.create_or_update( +Setting.create_if_not_exists( title: 'API Token Access', name: 'api_token_access', area: 'API::Base', - description: 'Enable REST API using tokens (not username/email addeess and password). Each user need to create own access tokens in user profile.', + description: 'Enable REST API using tokens (not username/email address and password). Each user need to create own access tokens in user profile.', options: { form: [ { @@ -1600,7 +1600,7 @@ Setting.create_or_update( }, frontend: false ) -Setting.create_or_update( +Setting.create_if_not_exists( title: 'API Password Access', name: 'api_password_access', area: 'API::Base', @@ -1626,6 +1626,28 @@ Setting.create_or_update( frontend: false ) +Setting.create_if_not_exists( + title: 'Monitoring Token', + name: 'monitoring_token', + area: 'HealthCheck::Base', + description: 'Token for Monitoring.', + options: { + form: [ + { + display: '', + null: false, + name: 'monitoring_token', + tag: 'input', + }, + ], + }, + state: SecureRandom.urlsafe_base64(40), + preferences: { + permission: ['admin.monitoring'], + }, + frontend: false +) + Setting.create_if_not_exists( title: 'Enable Chat', name: 'chat', @@ -2765,6 +2787,13 @@ Permission.create_if_not_exists( translations: ['Translations'] }, ) +Permission.create_if_not_exists( + name: 'admin.monitoring', + note: 'Manage %s', + preferences: { + translations: ['Monitoring'] + }, +) Permission.create_if_not_exists( name: 'admin.maintenance', note: 'Manage %s', @@ -5418,11 +5447,7 @@ Karma::Activity.create_or_update( ) # reset primary key sequences -if ActiveRecord::Base.connection_config[:adapter] == 'postgresql' - ActiveRecord::Base.connection.tables.each do |t| - ActiveRecord::Base.connection.reset_pk_sequence!(t) - end -end +DbHelper.import_post # install locales and translations Locale.create_if_not_exists( diff --git a/lib/auto_wizard.rb b/lib/auto_wizard.rb index 509409a04..9e71fbeed 100644 --- a/lib/auto_wizard.rb +++ b/lib/auto_wizard.rb @@ -146,6 +146,9 @@ returns } } + # reset primary key sequences + DbHelper.import_post + # remove auto wizard file FileUtils.rm auto_wizard_file_location diff --git a/lib/db_helper.rb b/lib/db_helper.rb new file mode 100644 index 000000000..27b61f699 --- /dev/null +++ b/lib/db_helper.rb @@ -0,0 +1,31 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ + +class DbHelper + +=begin + +execute post database statements after import (e. g. reset primary key sequences for postgresql) + + DbHelper.import_post + +or only for certan tables + + DbHelper.import_post(table_name) + +=end + + def self.import_post(table = nil) + return if ActiveRecord::Base.connection_config[:adapter] != 'postgresql' + + tables = if table + [table] + else + ActiveRecord::Base.connection.tables + end + + tables.each do |t| + ActiveRecord::Base.connection.reset_pk_sequence!(t) + end + end + +end diff --git a/lib/import/base_factory.rb b/lib/import/base_factory.rb new file mode 100644 index 000000000..550d5ebd6 --- /dev/null +++ b/lib/import/base_factory.rb @@ -0,0 +1,28 @@ +module Import + module BaseFactory + + # rubocop:disable Style/ModuleFunction + extend self + + def import(_records) + raise 'Missing implementation for import method for this factory' + end + + def pre_import_hook(_records) + end + + def backend_class(_record) + "Import::#{module_name}".constantize + end + + def skip?(_record) + false + end + + private + + def module_name + name.to_s.sub(/Import::/, '').sub(/Factory/, '') + end + end +end diff --git a/lib/import/factory.rb b/lib/import/factory.rb new file mode 100644 index 000000000..baf6c35ed --- /dev/null +++ b/lib/import/factory.rb @@ -0,0 +1,16 @@ +module Import + module Factory + include Import::BaseFactory + + # rubocop:disable Style/ModuleFunction + extend self + + def import(records) + pre_import_hook(records) + records.each do |record| + next if skip?(record) + backend_class(record).new(record) + end + end + end +end diff --git a/lib/import/helper.rb b/lib/import/helper.rb new file mode 100644 index 000000000..696f1c279 --- /dev/null +++ b/lib/import/helper.rb @@ -0,0 +1,31 @@ +module Import + module Helper + # rubocop:disable Style/ModuleFunction + extend self + + def check_import_mode + # check if system is in import mode + return true if Setting.get('import_mode') + raise 'System is not in import mode!' + end + + # log + def log(message) + thread_no = Thread.current[:thread_no] || '-' + Rails.logger.info "thread##{thread_no}: #{message}" + end + + # utf8 convert + def utf8_encode(data) + data.each { |key, value| + next if !value + next if value.class != String + data[key] = Encode.conv('utf8', value) + } + end + + def reset_primary_key_sequence(table) + DbHelper.import_post(table) + end + end +end diff --git a/lib/import/otrs.rb b/lib/import/otrs.rb index eb5a6971e..b471eff30 100644 --- a/lib/import/otrs.rb +++ b/lib/import/otrs.rb @@ -1,1610 +1,166 @@ -require 'base64' +# Rails autoload has some issues with same namend sub-classes +# in the importer folder require AND simultaniuos requiring +# of the same file in different threads so we need to +# require them ourself +require 'import/otrs/ticket' +require 'import/otrs/ticket_factory' +require 'import/otrs/article_customer' +require 'import/otrs/article_customer_factory' +require 'import/otrs/article' +require 'import/otrs/article_factory' +require 'import/otrs/article/attachment_factory' +require 'import/otrs/history' +require 'import/otrs/history_factory' +require 'import/otrs/history/article' +require 'import/otrs/history/move' +require 'import/otrs/history/new_ticket' +require 'import/otrs/history/priority_update' +require 'import/otrs/history/state_update' +require 'store' +require 'store/object' +require 'store/provider/db' +require 'store/provider/file' module Import -end -module Import::OTRS + module OTRS + extend Import::Helper + extend Import::OTRS::ImportStats + extend Import::OTRS::Async + extend Import::OTRS::Diff -=begin + # rubocop:disable Style/ModuleFunction + extend self - result = request_json(Subaction: 'List', 1) + def start + log 'Start import...' - return + checks - { some json structure } + prerequisites - result = request_json(Subaction: 'List') + base_objects - return + updateable_objects - "some data string" + customer_user -=end + threaded_import('Ticket') - def self.request_json(data, data_only = false) - response = post(data) - if !response - raise "Can't connect to Zammad Migrator" - end - if !response.success? - raise "Can't connect to Zammad Migrator" - end - result = json(response) - if !result - raise 'Invalid response' - end - if data_only - result['Result'] - else - result - end - end - -=begin - - start get request to backend, add auth data automatically - - result = request('Subaction=List') - - return - - "some data string" - -=end - - def self.request(part) - url = Setting.get('import_otrs_endpoint') + part + ';Key=' + Setting.get('import_otrs_endpoint_key') - log 'GET: ' + url - response = UserAgent.get( - url, - {}, - { - open_timeout: 10, - read_timeout: 60, - total_timeout: 180, - user: Setting.get('import_otrs_user'), - password: Setting.get('import_otrs_password'), - }, - ) - if !response.success? - log "ERROR: #{response.error}" - return - end - response - end - -=begin - - start post request to backend, add auth data automatically - - result = request('Subaction=List') - - return - - "some data string" - -=end - - def self.post(data, url = nil) - if !url - url = Setting.get('import_otrs_endpoint') - data['Action'] = 'ZammadMigrator' - end - data['Key'] = Setting.get('import_otrs_endpoint_key') - log 'POST: ' + url - log 'PARAMS: ' + data.inspect - open_timeout = 10 - read_timeout = 120 - total_timeout = 360 - if data.empty? - open_timeout = 6 - read_timeout = 20 - total_timeout = 120 - end - response = UserAgent.post( - url, - data, - { - open_timeout: open_timeout, - read_timeout: read_timeout, - total_timeout: total_timeout, - user: Setting.get('import_otrs_user'), - password: Setting.get('import_otrs_password'), - }, - ) - if !response.success? - log "ERROR: #{response.error}" - return - end - response - end - -=begin - - start post request to backend, add auth data automatically - - result = json('some response string') - - return - - {} - -=end - - def self.json(response) - data = Encode.conv('utf8', response.body.to_s) - JSON.parse(data) - end - -=begin - - start auth on OTRS - just for experimental reasons - - result = auth(username, password) - - return - - { ..user structure.. } - -=end - - def self.auth(username, password) - url = Setting.get('import_otrs_endpoint') - url.gsub!('ZammadMigrator', 'ZammadSSO') - response = post( { Action: 'ZammadSSO', Subaction: 'Auth', User: username, Pw: password }, url ) - return if !response - return if !response.success? - - result = json(response) - result - end - -=begin - - request session data - just for experimental reasons - - result = session(session_id) - - return - - { ..session structure.. } - -=end - - def self.session(session_id) - url = Setting.get('import_otrs_endpoint') - url.gsub!('ZammadMigrator', 'ZammadSSO') - response = post( { Action: 'ZammadSSO', Subaction: 'SessionCheck', SessionID: session_id }, url ) - return if !response - return if !response.success? - result = json(response) - result - end - -=begin - - load objects from otrs - - result = load('SysConfig') - - return - - [ - { ..object1.. }, - { ..object2.. }, - { ..object3.. }, - ] - -=end - - def self.load( object, limit = '', offset = '', diff = 0 ) - request_json( { Subaction: 'Export', Object: object, Limit: limit, Offset: offset, Diff: diff }, 1 ) - end - -=begin - - start get request to backend to check connection - - result = connection_test - - return - - true | false - -=end - - def self.connection_test - request_json({}) - end - -=begin - - get object statistic from remote server ans save it in cache - - result = statistic('Subaction=List') - - return - - { - 'Ticket' => 1234, - 'User' => 123, - 'SomeObject' => 999, - } - -=end - - def self.statistic - - # check cache - cache = Cache.get('import_otrs_stats') - if cache - return cache + true end - # retrive statistic - statistic = request_json( { Subaction: 'List' }, 1) - if statistic - Cache.write('import_otrs_stats', statistic) - end - statistic - end - -=begin - - return current import state - - result = current_state - - return - - { - Ticket: { - total: 1234, - done: 13, - }, - Base: { - total: 1234, - done: 13, - }, - } - -=end - - def self.current_state - data = statistic - base = Group.count + Ticket::State.count + Ticket::Priority.count - base_total = data['Queue'] + data['State'] + data['Priority'] - user = User.count - user_total = data['User'] + data['CustomerUser'] - data = { - Base: { - done: base, - total: base_total || 0, - }, - User: { - done: user, - total: user_total || 0, - }, - Ticket: { - done: Ticket.count, - total: data['Ticket'] || 0, - }, - } - data - end - - # - # start import - # - # Import::OTRS.start - # - - def self.start - log 'Start import...' - - # check if system is in import mode - if !Setting.get('import_mode') - raise 'System is not in import mode!' + def connection_test + Import::OTRS::Requester.connection_test end - result = request_json({}) - if !result['Success'] - raise 'API key not valid!' + private + + def checks + check_import_mode + connection_test end - # set settings - settings = load('SysConfig') - setting(settings) - - # dynamic fields - dynamic_fields = load('DynamicField') - object_manager(dynamic_fields) - - # email accounts - #accounts = load('PostMasterAccount') - #account(accounts) - - # email filter - #filters = load('PostMasterFilter') - #filter(filters) - - # create states - states = load('State') - ActiveRecord::Base.transaction do - state(states) + def prerequisites + # make sure to create store type otherwise + # it might lead to race conditions while + # creating it in different import threads + Store::Object.create_if_not_exists(name: 'Ticket::Article') end - # create priorities - priorities = load('Priority') - ActiveRecord::Base.transaction do - priority(priorities) + def import(remote_object, args = {}) + log "loading #{remote_object}..." + import_action(remote_object, args) end - # create groups - queues = load('Queue') - ActiveRecord::Base.transaction do - ticket_group(queues) - end + def threaded_import(remote_object, args = {}) + thread_count = args[:threads] || 8 + limit = args[:limit] || 20 - # get agents groups - groups = load('Group') + Thread.abort_on_exception = true + threads = {} + (1..thread_count).each { |thread| - # get agents roles - roles = load('Role') + threads[thread] = Thread.new { - # create agents - users = load('User') - ActiveRecord::Base.transaction do - user(users, groups, roles, queues) - end + Thread.current[:thread_no] = thread + Thread.current[:loop_count] = 0 - # create organizations - organizations = load('Customer') - ActiveRecord::Base.transaction do - organization(organizations) - end + log "Importing #{remote_object} in steps of #{limit}" + loop do + # get the offset for the current thread and loop count + thread_offset_base = (Thread.current[:thread_no] - 1) * limit + thread_step = thread_count * limit + offset = Thread.current[:loop_count] * thread_step + thread_offset_base - # create customers - count = 0 - steps = 50 - run = true - while run - count += steps - records = load('CustomerUser', steps, count - steps) - if !records || !records[0] - log 'all customers imported.' - run = false - next - end - customer(records, organizations) - end + break if !imported?( + remote_object: remote_object, + limit: limit, + offset: offset, + diff: args[:diff] + ) - Thread.abort_on_exception = true - thread_count = 8 - threads = {} - steps = 20 - (1..thread_count).each { |thread| - - threads[thread] = Thread.new { - - log "Started import thread# #{thread} ..." - Thread.current[:thread_no] = thread - Thread.current[:loop_count] = 0 - - loop do - # get the offset for the current thread and loop count - thread_offset_base = (Thread.current[:thread_no] - 1) * steps - thread_step = thread_count * steps - offset = Thread.current[:loop_count] * thread_step + thread_offset_base - - log "loading... thread# #{thread} ..." - records = load( 'Ticket', steps, offset) - if !records || !records[0] - log "... thread# #{thread}, no more work." - break + Thread.current[:loop_count] += 1 end - _ticket_result(records, thread) - - Thread.current[:loop_count] += 1 - end - ActiveRecord::Base.connection.close + ActiveRecord::Base.connection.close + } } - } - (1..thread_count).each { |thread| - threads[thread].join - } + (1..thread_count).each { |thread| + threads[thread].join + } + end - true - end - -=begin - start import in background - - Import::OTRS.start_bg -=end - - def self.start_bg - Setting.reload - - Import::OTRS.connection_test - - # start thread to observe current state - status_update_thread = Thread.new { + def limit_import(remote_object, args = {}) + offset = 0 + limit = args[:limit] || 20 + log "Importing #{remote_object} in steps of #{limit}" loop do - result = { - data: current_state, - result: 'in_progress', - } - Cache.write('import:state', result, expires_in: 10.minutes) - sleep 8 - end - } - sleep 2 - # start import data - begin - Import::OTRS.start - rescue => e - status_update_thread.exit - status_update_thread.join - Rails.logger.error e.message - Rails.logger.error e.backtrace.inspect - result = { - message: e.message, - result: 'error', - } - Cache.write('import:state', result, expires_in: 10.hours) - return false - end - sleep 16 # wait until new finished import state is on client - status_update_thread.exit - status_update_thread.join - - result = { - result: 'import_done', - } - Cache.write('import:state', result, expires_in: 10.hours) - - Setting.set('system_init_done', true) - Setting.set('import_mode', false) - end - -=begin - - get import state from background process - - result = Import::OTRS.status_bg - -=end - - def self.status_bg - state = Cache.get('import:state') - return state if state - { - message: 'not running', - } - end - - def self.diff_worker - return if !Setting.get('import_mode') - return if Setting.get('import_otrs_endpoint') == 'http://otrs_host/otrs' - diff - end - - def self.diff - log 'Start diff...' - - # check if system is in import mode - if !Setting.get('import_mode') - raise 'System is not in import mode!' - end - - # create states - states = load('State') - state(states) - - # create priorities - priorities = load('Priority') - priority(priorities) - - # create groups - queues = load('Queue') - ticket_group(queues) - - # get agents groups - groups = load('Group') - - # get agents roles - roles = load('Role') - - # create agents - users = load('User') - user(users, groups, roles, queues) - - # create organizations - organizations = load('Customer') - organization(organizations) - - # get changed tickets - ticket_diff - - end - - def self.ticket_diff - count = 0 - run = true - steps = 20 - while run - count += steps - log 'loading... diff ...' - records = load( 'Ticket', steps, count - steps, 1 ) - if !records || !records[0] - log '... no more work.' - run = false - next - end - _ticket_result(records) - end - - end - - def self._ticket_result(result, _thread = '-') - map = { - Ticket: { - Changed: :updated_at, - Created: :created_at, - CreateBy: :created_by_id, - TicketNumber: :number, - QueueID: :group_id, - StateID: :state_id, - PriorityID: :priority_id, - Owner: :owner, - CustomerUserID: :customer, - Title: :title, - TicketID: :id, - FirstResponse: :first_response_at, - #FirstResponseTimeDestinationDate: :first_response_escalation_at, - #FirstResponseInMin: :first_response_in_min, - #FirstResponseDiffInMin: :first_response_diff_in_min, - Closed: :close_at, - #SoltutionTimeDestinationDate: :close_escalation_at, - #CloseTimeInMin: :close_in_min, - #CloseTimeDiffInMin: :close_diff_in_min, - }, - Article: { - SenderType: :sender, - ArticleType: :type, - TicketID: :ticket_id, - ArticleID: :id, - Body: :body, - From: :from, - To: :to, - Cc: :cc, - Subject: :subject, - InReplyTo: :in_reply_to, - MessageID: :message_id, - #ReplyTo: :reply_to, - References: :references, - Changed: :updated_at, - Created: :created_at, - ChangedBy: :updated_by_id, - CreatedBy: :created_by_id, - }, - } - - result.each { |record| - - # cleanup values - _cleanup(record) - - _utf8_encode(record) - - ticket_new = { - title: '', - created_by_id: 1, - updated_by_id: 1, - } - map[:Ticket].each { |key, value| - next if !record.key?(key.to_s) - ticket_new[value] = record[key.to_s] - } - - record.keys.each { |key| - - key_string = key.to_s - - next if !key_string.start_with?('DynamicField_') - dynamic_field_name = key_string[13, key_string.length] - - next if skip_fields.include?( dynamic_field_name ) - dynamic_field_name = convert_df_name(dynamic_field_name) - - ticket_new[dynamic_field_name.to_sym] = record[key_string] - } - - # find owner - if ticket_new[:owner] - user = User.find_by(login: ticket_new[:owner].downcase) - ticket_new[:owner_id] = if user - user.id - else - 1 - end - ticket_new.delete(:owner) - end - - record['Articles'].each { |article| - # utf8 encode - _utf8_encode(article) - # lookup customers to create first - _article_based_customers(article) - } - - # find customer - if ticket_new[:customer] - user = User.lookup(login: ticket_new[:customer].downcase) - ticket_new[:customer_id] = if user - user.id - else - _first_customer_id(record['Articles']) - end - ticket_new.delete(:customer) - else - ticket_new[:customer_id] = 1 - end - - # update or create ticket - ticket_old = Ticket.find_by(id: ticket_new[:id]) - if ticket_old - log "update Ticket.find(#{ticket_new[:id]})" - ticket_old.update_attributes(ticket_new) - else - log "add Ticket.find(#{ticket_new[:id]})" - - begin - ticket = Ticket.new(ticket_new) - ticket.id = ticket_new[:id] - ticket.save - _reset_pk('tickets') - rescue ActiveRecord::RecordNotUnique - log "Ticket #{ticket_new[:id]} is handled by another thead, skipping." - next - end - end - - record['Articles'].each do |article| - - retries = 3 - begin - - ActiveRecord::Base.transaction do - - # get article values - article_new = { - created_by_id: 1, - updated_by_id: 1, - } - - map[:Article].each { |key, value| - next if !article.key?(key.to_s) - article_new[value] = article[key.to_s] - } - - if article_new[:sender] == 'customer' - article_new[:sender_id] = Ticket::Article::Sender.lookup(name: 'Customer').id - article_new.delete(:sender) - end - if article_new[:sender] == 'agent' - article_new[:sender_id] = Ticket::Article::Sender.lookup(name: 'Agent').id - article_new.delete(:sender) - end - if article_new[:sender] == 'system' - article_new[:sender_id] = Ticket::Article::Sender.lookup(name: 'System').id - article_new.delete(:sender) - end - - if article_new[:type] == 'email-external' - article_new[:type_id] = Ticket::Article::Type.lookup(name: 'email').id - article_new[:internal] = false - elsif article_new[:type] == 'email-internal' - article_new[:type_id] = Ticket::Article::Type.lookup(name: 'email').id - article_new[:internal] = true - elsif article_new[:type] == 'note-external' - article_new[:type_id] = Ticket::Article::Type.lookup(name: 'note').id - article_new[:internal] = false - elsif article_new[:type] == 'note-internal' - article_new[:type_id] = Ticket::Article::Type.lookup(name: 'note').id - article_new[:internal] = true - elsif article_new[:type] == 'phone' - article_new[:type_id] = Ticket::Article::Type.lookup(name: 'phone').id - article_new[:internal] = false - elsif article_new[:type] == 'webrequest' - article_new[:type_id] = Ticket::Article::Type.lookup(name: 'web').id - article_new[:internal] = false - else - article_new[:type_id] = 9 - end - article_new.delete(:type) - article_object = Ticket::Article.find_by(id: article_new[:id]) - - # set state types - if article_object - log "update Ticket::Article.find(#{article_new[:id]})" - article_object.update_attributes(article_new) - else - log "add Ticket::Article.find(#{article_new[:id]})" - begin - article_object = Ticket::Article.new(article_new) - article_object.id = article_new[:id] - article_object.save - _reset_pk('ticket_articles') - rescue ActiveRecord::RecordNotUnique - log "Ticket #{ticket_new[:id]} (article #{article_new[:id]}) is handled by another thead, skipping." - next - end - end - - next if !article['Attachments'] - next if article['Attachments'].empty? - - # TODO: refactor - # check if there are attachments present - if !article_object.attachments.empty? - - # skip attachments if count is equal - next if article_object.attachments.count == article['Attachments'].count - - # if the count differs delete all so we - # can have a fresh start - article_object.attachments.each(&:delete) - end - - # import article attachments - article['Attachments'].each { |attachment| - - filename = Base64.decode64(attachment['Filename']) - - Store.add( - object: 'Ticket::Article', - o_id: article_object.id, - filename: filename, - data: Base64.decode64(attachment['Content']), - preferences: { - 'Mime-Type' => attachment['ContentType'], - 'Content-ID' => attachment['ContentID'], - 'content-alternative' => attachment['ContentAlternative'], - }, - created_by_id: 1, - ) - } - end - rescue ActiveRecord::RecordNotUnique => e - log "Ticket #{ticket_new[:id]} - RecordNotUnique: #{e}" - sleep rand 3 - retry if !(retries -= 1).zero? - raise - end - end - - #puts "HS: #{record['History'].inspect}" - record['History'].each { |history| - - begin - if history['HistoryType'] == 'NewTicket' - History.add( - id: history['HistoryID'], - o_id: history['TicketID'], - history_type: 'created', - history_object: 'Ticket', - created_at: history['CreateTime'], - created_by_id: history['CreateBy'] - ) - elsif history['HistoryType'] == 'StateUpdate' - data = history['Name'] - # "%%new%%open%%" - from = nil - to = nil - if data =~ /%%(.+?)%%(.+?)%%/ - from = $1 - to = $2 - state_from = Ticket::State.lookup(name: from) - state_to = Ticket::State.lookup(name: to) - if state_from - from_id = state_from.id - end - if state_to - to_id = state_to.id - end - end - History.add( - id: history['HistoryID'], - o_id: history['TicketID'], - history_type: 'updated', - history_object: 'Ticket', - history_attribute: 'state', - value_from: from, - id_from: from_id, - value_to: to, - id_to: to_id, - created_at: history['CreateTime'], - created_by_id: history['CreateBy'] - ) - elsif history['HistoryType'] == 'Move' - data = history['Name'] - # "%%Queue1%%5%%Postmaster%%1" - from = nil - to = nil - if data =~ /%%(.+?)%%(.+?)%%(.+?)%%(.+?)$/ - from = $1 - from_id = $2 - to = $3 - to_id = $4 - end - History.add( - id: history['HistoryID'], - o_id: history['TicketID'], - history_type: 'updated', - history_object: 'Ticket', - history_attribute: 'group', - value_from: from, - value_to: to, - id_from: from_id, - id_to: to_id, - created_at: history['CreateTime'], - created_by_id: history['CreateBy'] - ) - elsif history['HistoryType'] == 'PriorityUpdate' - data = history['Name'] - # "%%3 normal%%3%%5 very high%%5" - from = nil - to = nil - if data =~ /%%(.+?)%%(.+?)%%(.+?)%%(.+?)$/ - from = $1 - from_id = $2 - to = $3 - to_id = $4 - end - History.add( - id: history['HistoryID'], - o_id: history['TicketID'], - history_type: 'updated', - history_object: 'Ticket', - history_attribute: 'priority', - value_from: from, - value_to: to, - id_from: from_id, - id_to: to_id, - created_at: history['CreateTime'], - created_by_id: history['CreateBy'] - ) - elsif history['ArticleID'] && !history['ArticleID'].to_i.zero? - History.add( - id: history['HistoryID'], - o_id: history['ArticleID'], - history_type: 'created', - history_object: 'Ticket::Article', - related_o_id: history['TicketID'], - related_history_object: 'Ticket', - created_at: history['CreateTime'], - created_by_id: history['CreateBy'] - ) - end - - rescue ActiveRecord::RecordNotUnique - log "Ticket #{ticket_new[:id]} (history #{history['HistoryID']}) is handled by another thead, skipping." - next - end - } - } - end - - # sync ticket states - def self.state(records) - map = { - ChangeTime: :updated_at, - CreateTime: :created_at, - CreateBy: :created_by_id, - ChangeBy: :updated_by_id, - Name: :name, - ID: :id, - ValidID: :active, - Comment: :note, - } - - # rename states to handle not uniq issues - Ticket::State.all.each { |state| - state.name = state.name + '_tmp' - state.save - } - - records.each { |state| - _set_valid(state) - - # get new attributes - state_new = { - created_by_id: 1, - updated_by_id: 1, - } - map.each { |key, value| - next if !state.key?(key.to_s) - state_new[value] = state[key.to_s] - } - - # check if state already exists - state_old = Ticket::State.lookup(id: state_new[:id]) - - # set state types - if state['TypeName'] == 'pending auto' - state['TypeName'] = 'pending action' - end - state_type = Ticket::StateType.lookup(name: state['TypeName']) - state_new[:state_type_id] = state_type.id - if state_old - state_old.update_attributes(state_new) - else - state = Ticket::State.new(state_new) - state.id = state_new[:id] - state.save - _reset_pk('ticket_states') - end - } - end - - # sync ticket priorities - def self.priority(records) - - map = { - ChangeTime: :updated_at, - CreateTime: :created_at, - CreateBy: :created_by_id, - ChangeBy: :updated_by_id, - Name: :name, - ID: :id, - ValidID: :active, - Comment: :note, - } - - records.each { |priority| - _set_valid(priority) - - # get new attributes - priority_new = { - created_by_id: 1, - updated_by_id: 1, - } - map.each { |key, value| - next if !priority.key?(key.to_s) - priority_new[value] = priority[key.to_s] - } - - # check if state already exists - priority_old = Ticket::Priority.lookup(id: priority_new[:id]) - - # set state types - if priority_old - priority_old.update_attributes(priority_new) - else - priority = Ticket::Priority.new(priority_new) - priority.id = priority_new[:id] - priority.save - _reset_pk('ticket_priorities') - end - } - end - - # sync ticket groups / queues - def self.ticket_group(records) - map = { - ChangeTime: :updated_at, - CreateTime: :created_at, - CreateBy: :created_by_id, - ChangeBy: :updated_by_id, - Name: :name, - QueueID: :id, - ValidID: :active, - Comment: :note, - } - - records.each { |group| - _set_valid(group) - - # get new attributes - group_new = { - created_by_id: 1, - updated_by_id: 1, - } - map.each { |key, value| - next if !group.key?(key.to_s) - group_new[value] = group[key.to_s] - } - - # check if state already exists - group_old = Group.lookup(id: group_new[:id]) - - # set state types - if group_old - group_old.update_attributes(group_new) - else - group = Group.new(group_new) - group.id = group_new[:id] - group.save - _reset_pk('groups') - end - } - end - - # sync agents - def self.user(records, groups, roles, queues) - - map = { - ChangeTime: :updated_at, - CreateTime: :created_at, - CreateBy: :created_by_id, - ChangeBy: :updated_by_id, - UserID: :id, - ValidID: :active, - Comment: :note, - UserEmail: :email, - UserFirstname: :firstname, - UserLastname: :lastname, - UserLogin: :login, - UserPw: :password, - } - - records.each { |user| - _set_valid(user) - - # get roles - role_ids = get_roles_ids(user, groups, roles, queues) - - # get groups - group_ids = get_queue_ids(user, groups, roles, queues) - - # get new attributes - user_new = { - created_by_id: 1, - updated_by_id: 1, - source: 'OTRS Import', - role_ids: role_ids, - group_ids: group_ids, - } - map.each { |key, value| - next if !user.key?(key.to_s) - user_new[value] = user[key.to_s] - } - - # set pw - if user_new[:password] - user_new[:password] = "{sha2}#{user_new[:password]}" - end - - # check if agent already exists - user_old = User.lookup(id: user_new[:id]) - - # check if login is already used - login_in_use = User.where( "login = ? AND id != #{user_new[:id]}", user_new[:login].downcase ).count - if login_in_use.positive? - user_new[:login] = "#{user_new[:login]}_#{user_new[:id]}" - end - - # create / update agent - if user_old - log "update User.find(#{user_old[:id]})" - - # only update roles if different (reduce sql statements) - if user_old.role_ids == user_new[:role_ids] - user_new.delete(:role_ids) - end - - user_old.update_attributes(user_new) - else - log "add User.find(#{user_new[:id]})" - user = User.new(user_new) - user.id = user_new[:id] - user.save - _reset_pk('users') - end - } - end - - def self.get_queue_ids(user, _groups, _roles, queues) - queue_ids = [] - - # lookup by groups - user['GroupIDs'].each { |group_id, permissions| - queues.each { |queue_lookup| - - next if queue_lookup['GroupID'] != group_id - next if !permissions - next if !permissions.include?('rw') - - queue_ids.push queue_lookup['QueueID'] - } - } - - # lookup by roles - - # roles of user - # groups of roles - # queues of group - - queue_ids - end - - def self.get_roles_ids(user, groups, roles, _queues) - local_roles = ['Agent'] - local_role_ids = [] - - # apply group permissions - user['GroupIDs'].each { |group_id, permissions| - groups.each { |group_lookup| - - next if group_id != group_lookup['ID'] - next if !permissions - - if group_lookup['Name'] == 'admin' && permissions.include?('rw') - local_roles.push 'Admin' - end - - next if group_lookup['Name'] !~ /^(stats|report)/ - next if !( permissions.include?('ro') || permissions.include?('rw') ) - - local_roles.push 'Report' - } - } - - # apply role permissions - user['RoleIDs'].each { |role_id| - - # get groups of role - roles.each { |role| - next if role['ID'] != role_id - - # verify group names - role['GroupIDs'].each { |group_id, permissions| - groups.each { |group_lookup| - - next if group_id != group_lookup['ID'] - next if !permissions - - if group_lookup['Name'] == 'admin' && permissions.include?('rw') - local_roles.push 'Admin' - end - - next if group_lookup['Name'] !~ /^(stats|report)/ - next if !( permissions.include?('ro') || permissions.include?('rw') ) - - local_roles.push 'Report' - } - } - } - } - - local_roles.each { |role| - role_lookup = Role.lookup(name: role) - next if !role_lookup - local_role_ids.push role_lookup.id - } - local_role_ids - end - - # sync customers - - def self.customer(records, organizations) - map = { - ChangeTime: :updated_at, - CreateTime: :created_at, - CreateBy: :created_by_id, - ChangeBy: :updated_by_id, - ValidID: :active, - UserComment: :note, - UserEmail: :email, - UserFirstname: :firstname, - UserLastname: :lastname, - UserLogin: :login, - UserPassword: :password, - UserPhone: :phone, - UserFax: :fax, - UserMobile: :mobile, - UserStreet: :street, - UserZip: :zip, - UserCity: :city, - UserCountry: :country, - } - - role_agent = Role.lookup(name: 'Agent') - role_customer = Role.lookup(name: 'Customer') - - records.each { |user| - _set_valid(user) - - # get new attributes - user_new = { - created_by_id: 1, - updated_by_id: 1, - source: 'OTRS Import', - organization_id: get_organization_id(user, organizations), - role_ids: [ role_customer.id ], - } - map.each { |key, value| - next if !user.key?(key.to_s) - user_new[value] = user[key.to_s] - } - - # check if customer already exists - user_old = User.lookup(login: user_new[:login]) - - # create / update agent - if user_old - - # do not update user if it is already agent - if !user_old.role_ids.include?(role_agent.id) - - # only update roles if different (reduce sql statements) - if user_old.role_ids == user_new[:role_ids] - user_new.delete(:role_ids) - end - log "update User.find(#{user_old[:id]})" - user_old.update_attributes(user_new) - end - else - log "add User.find(#{user_new[:id]})" - user = User.new(user_new) - user.save - _reset_pk('users') - end - } - end - - def self.get_organization_id(user, organizations) - organization_id = nil - if user['UserCustomerID'] - organizations.each { |organization| - next if user['UserCustomerID'] != organization['CustomerID'] - organization = Organization.lookup(name: organization['CustomerCompanyName']) - organization_id = organization.id - } - end - organization_id - end - - # sync organizations - def self.organization(records) - map = { - ChangeTime: :updated_at, - CreateTime: :created_at, - CreateBy: :created_by_id, - ChangeBy: :updated_by_id, - CustomerCompanyName: :name, - ValidID: :active, - CustomerCompanyComment: :note, - } - - records.each { |organization| - _set_valid(organization) - - # get new attributes - organization_new = { - created_by_id: 1, - updated_by_id: 1, - } - map.each { |key, value| - next if !organization.key?(key.to_s) - organization_new[value] = organization[key.to_s] - } - - # check if state already exists - organization_old = Organization.lookup(name: organization_new[:name]) - - # set state types - if organization_old - organization_old.update_attributes(organization_new) - else - organization = Organization.new(organization_new) - organization.id = organization_new[:id] - organization.save - _reset_pk('organizations') - end - } - end - - # sync settings - def self.setting(records) - - records.each { |setting| - - # fqdn - if setting['Key'] == 'FQDN' - Setting.set('fqdn', setting['Value']) - end - - # http type - if setting['Key'] == 'HttpType' - Setting.set('http_type', setting['Value']) - end - - # system id - if setting['Key'] == 'SystemID' - Setting.set('system_id', setting['Value']) - end - - # organization - if setting['Key'] == 'Organization' - Setting.set('organization', setting['Value']) - end - - # sending emails - if setting['Key'] == 'SendmailModule' - # TODO - end - - # number generater - if setting['Key'] == 'Ticket::NumberGenerator' - if setting['Value'] == 'Kernel::System::Ticket::Number::DateChecksum' - Setting.set('ticket_number', 'Ticket::Number::Date') - Setting.set('ticket_number_date', { checksum: true }) - elsif setting['Value'] == 'Kernel::System::Ticket::Number::Date' - Setting.set('ticket_number', 'Ticket::Number::Date') - Setting.set('ticket_number_date', { checksum: false }) - end - end - - # ticket hook - if setting['Key'] == 'Ticket::Hook' - Setting.set('ticket_hook', setting['Value']) - end - } - end - - # dynamic fields - def self.object_manager(dynamic_fields) - - dynamic_fields.each { |dynamic_field| - - if dynamic_field['ObjectType'] != 'Ticket' - log "ERROR: Unsupported dynamic field object type '#{dynamic_field['ObjectType']}' for dynamic field '#{dynamic_field['Name']}'" - next - end - - next if skip_fields.include?( dynamic_field['Name'] ) - - internal_name = convert_df_name(dynamic_field['Name']) - - attribute = ObjectManager::Attribute.get( - object: dynamic_field['ObjectType'], - name: internal_name, - ) - next if !attribute.nil? - - object_manager_config = { - object: dynamic_field['ObjectType'], - name: internal_name, - display: dynamic_field['Label'], - screens: { - view: { - '-all-' => { - shown: true, - }, - }, - }, - active: true, - editable: dynamic_field['InternalField'] == '0', - position: dynamic_field['FieldOrder'], - created_by_id: 1, - updated_by_id: 1, - } - - if dynamic_field['FieldType'] == 'Text' - - object_manager_config[:data_type] = 'input' - object_manager_config[:data_option] = { - default: dynamic_field['Config']['DefaultValue'], - type: 'text', - maxlength: 255, - null: false, - } - elsif dynamic_field['FieldType'] == 'TextArea' - - object_manager_config[:data_type] = 'textarea' - object_manager_config[:data_option] = { - default: dynamic_field['Config']['DefaultValue'], - rows: dynamic_field['Config']['Rows'], - null: false, - } - elsif dynamic_field['FieldType'] == 'Checkbox' - - object_manager_config[:data_type] = 'boolean' - object_manager_config[:data_option] = { - default: dynamic_field['Config']['DefaultValue'] == '1', - options: { - true => 'Yes', - false => 'No', - }, - null: false, - translate: true, - } - elsif dynamic_field['FieldType'] == 'DateTime' - - object_manager_config[:data_type] = 'datetime' - object_manager_config[:data_option] = { - future: dynamic_field['Config']['YearsInFuture'] != '0', - past: dynamic_field['Config']['YearsInPast'] != '0', - diff: dynamic_field['Config']['DefaultValue'].to_i / 60 / 60, - null: false, - } - elsif dynamic_field['FieldType'] == 'Date' - - object_manager_config[:data_type] = 'date' - object_manager_config[:data_option] = { - future: dynamic_field['Config']['YearsInFuture'] != '0', - past: dynamic_field['Config']['YearsInPast'] != '0', - diff: dynamic_field['Config']['DefaultValue'].to_i / 60 / 60 / 24, - null: false, - } - elsif dynamic_field['FieldType'] == 'Dropdown' - - object_manager_config[:data_type] = 'select' - object_manager_config[:data_option] = { - default: '', - multiple: false, - options: dynamic_field['Config']['PossibleValues'], - null: dynamic_field['Config']['PossibleNone'] == '1', - translate: dynamic_field['Config']['TranslatableValues'] == '1', - } - elsif dynamic_field['FieldType'] == 'Multiselect' - - object_manager_config[:data_type] = 'select' - object_manager_config[:data_option] = { - default: '', - multiple: true, - options: dynamic_field['Config']['PossibleValues'], - null: dynamic_field['Config']['PossibleNone'] == '1', - translate: dynamic_field['Config']['TranslatableValues'] == '1', - } - else - log "ERROR: Unsupported dynamic field field type '#{dynamic_field['FieldType']}' for dynamic field '#{dynamic_field['Name']}'" - next - end - - ObjectManager::Attribute.add( object_manager_config ) - ObjectManager::Attribute.migration_execute(false) - } - - end - - def self.convert_df_name(dynamic_field_name) - new_name = dynamic_field_name.underscore - new_name.sub(/\_id(s)?\z/, "_no#{$1}") - end - - # log - def self.log(message) - thread_no = Thread.current[:thread_no] || '-' - Rails.logger.info "thread##{thread_no}: #{message}" - end - - # set translate valid ids to active = true|false - def self._set_valid(record) - - # map - record['ValidID'] = if record['ValidID'].to_s == '3' - false - elsif record['ValidID'].to_s == '2' - false - elsif record['ValidID'].to_s == '1' - true - elsif record['ValidID'].to_s == '0' - false - - # fallback - else - true - end - end - - # cleanup invalid values - def self._cleanup(record) - record.each { |key, value| - if value == '0000-00-00 00:00:00' - record[key] = nil - end - } - - # fix OTRS 3.1 bug, no close time if ticket is created - if record['StateType'] == 'closed' && (!record['Closed'] || record['Closed'].empty?) - record['Closed'] = record['Created'] - end - end - - # utf8 convert - def self._utf8_encode(data) - data.each { |key, value| - next if !value - next if value.class != String - data[key] = Encode.conv('utf8', value) - } - end - - # reset primary key sequences - def self._reset_pk(table) - return if ActiveRecord::Base.connection_config[:adapter] != 'postgresql' - ActiveRecord::Base.connection.reset_pk_sequence!(table) - end - - # create customers for article - def self._article_based_customers(article) - - # create customer/sender if needed - return if article['sender'] != 'customer' - return if article['created_by_id'].to_i != 1 - return if article['from'].empty? - - email = nil - begin - email = Mail::Address.new(article['from']).address - rescue - email = article['from'] - if article['from'] =~ /<(.+?)>/ - email = $1 - end - end - - user = User.lookup(email: email) - if !user - user = User.lookup(login: email) - end - if !user - begin - display_name = Mail::Address.new( article['from'] ).display_name || - ( Mail::Address.new( article['from'] ).comments && Mail::Address.new( article['from'] ).comments[0] ) - rescue - display_name = article['from'] - end - - # do extra decoding because we needed to use field.value - display_name = Mail::Field.new('X-From', display_name).to_s - - roles = Role.lookup(name: 'Customer') - begin - user = User.create( - login: email, - firstname: display_name, - lastname: '', - email: email, - password: '', - active: true, - role_ids: [roles.id], - updated_by_id: 1, - created_by_id: 1, + break if !imported?( + remote_object: remote_object, + limit: limit, + offset: offset, + diff: args[:diff] ) - rescue ActiveRecord::RecordNotUnique - log "User #{email} was handled by another thread, taking this." - user = User.lookup(login: email) - if !user - log "User #{email} wasn't created sleep and retry." - sleep rand 3 - retry - end + + offset += limit end end - article['created_by_id'] = user.id - true - end + def imported?(args) + log "loading #{args[:limit]} #{args[:remote_object]} starting at #{args[:offset]}..." + return false if !import_action(args[:remote_object], limit: args[:limit], offset: args[:offset], diff: args[:diff]) + true + end - def self.skip_fields - %w(ProcessManagementProcessID ProcessManagementActivityID ZammadMigratorChanged ZammadMigratorChangedOld) - end + def import_action(remote_object, args = {}) + records = Import::OTRS::Requester.load(remote_object, limit: args[:limit], offset: args[:offset], diff: args[:diff]) + if !records || records.empty? + log '... no more work.' + return false + end + factory_class(remote_object).import(records) + end - def self._first_customer_id(articles) - user_id = 1 - articles.each { |article| - next if article['sender'] != 'customer' - next if article['from'].empty? + def factory_class(object) + "Import::OTRS::#{object}Factory".constantize + end - user_id = article['created_by_id'].to_i - break - } + # sync settings + def base_objects + import('SysConfig') + import('DynamicField') + end - user_id + def updateable_objects + import('State') + import('Priority') + import('Queue') + import('User') + import('Customer') + end + + def customer_user + limit_import('CustomerUser', limit: 50) + end end end diff --git a/lib/import/otrs/article.rb b/lib/import/otrs/article.rb new file mode 100644 index 000000000..bef778d82 --- /dev/null +++ b/lib/import/otrs/article.rb @@ -0,0 +1,136 @@ +module Import + module OTRS + class Article + include Import::Helper + include Import::OTRS::Helper + + MAPPING = { + TicketID: :ticket_id, + ArticleID: :id, + Body: :body, + From: :from, + To: :to, + Cc: :cc, + Subject: :subject, + InReplyTo: :in_reply_to, + MessageID: :message_id, + #ReplyTo: :reply_to, + References: :references, + Changed: :updated_at, + Created: :created_at, + ChangedBy: :updated_by_id, + CreatedBy: :created_by_id, + }.freeze + + def initialize(article) + initialize_article_sender_types + initialize_article_types + + utf8_encode(article) + import(article) + end + + private + + def import(article) + create_or_update(map(article)) + + return if !article['Attachments'] + return if article['Attachments'].empty? + + Import::OTRS::Article::AttachmentFactory.import( + attachments: article['Attachments'], + local_article: @local_article + ) + end + + def create_or_update(article) + return if updated?(article) + create(article) + end + + def updated?(article) + @local_article = ::Ticket::Article.find_by(id: article[:id]) + return false if !@local_article + log "update Ticket::Article.find_by(id: #{article[:id]})" + @local_article.update_attributes(article) + true + end + + def create(article) + log "add Ticket::Article.find_by(id: #{article[:id]})" + @local_article = ::Ticket::Article.new(article) + @local_article.id = article[:id] + @local_article.save + reset_primary_key_sequence('ticket_articles') + rescue ActiveRecord::RecordNotUnique + log "Ticket #{article[:ticket_id]} (article #{article[:id]}) is handled by another thead, skipping." + end + + def map(article) + { + created_by_id: 1, + updated_by_id: 1, + } + .merge(from_mapping(article)) + .merge(article_type(article)) + .merge(article_sender_type(article)) + end + + def article_type(article) + @article_types[article['ArticleType']] || @article_types['note-internal'] + end + + def article_sender_type(article) + { + sender_id: @sender_type_id[article['SenderType']] || @sender_type_id['note-internal'] + } + end + + def initialize_article_sender_types + @sender_type_id = { + 'customer' => article_sender_type_id_lookup('Customer'), + 'agent' => article_sender_type_id_lookup('Agent'), + 'system' => article_sender_type_id_lookup('System'), + } + end + + def article_sender_type_id_lookup(name) + ::Ticket::Article::Sender.find_by(name: name).id + end + + def initialize_article_types + @article_types = { + 'email-external' => { + type_id: article_type_id_lookup('email'), + internal: false + }, + 'email-internal' => { + type_id: article_type_id_lookup('email'), + internal: true + }, + 'note-external' => { + type_id: article_type_id_lookup('note'), + internal: false + }, + 'note-internal' => { + type_id: article_type_id_lookup('note'), + internal: true + }, + 'phone' => { + type_id: article_type_id_lookup('phone'), + internal: false + }, + 'webrequest' => { + type_id: article_type_id_lookup('web'), + internal: false + }, + } + end + + def article_type_id_lookup(name) + ::Ticket::Article::Type.lookup(name: name).id + end + end + end +end diff --git a/lib/import/otrs/article/attachment_factory.rb b/lib/import/otrs/article/attachment_factory.rb new file mode 100644 index 000000000..ad44e663d --- /dev/null +++ b/lib/import/otrs/article/attachment_factory.rb @@ -0,0 +1,104 @@ +module Import + module OTRS + class Article + module AttachmentFactory + extend Import::Helper + + # rubocop:disable Style/ModuleFunction + extend self + + def import(args) + attachments = args[:attachments] || [] + local_article = args[:local_article] + + return if skip_import?(attachments, local_article) + perform_import(attachments, local_article) + end + + private + + def perform_import(attachments, local_article) + attachments.each { |attachment| import_single(local_article, attachment) } + end + + def import_single(local_article, attachment) + + decoded_filename = Base64.decode64(attachment['Filename']) + decoded_content = Base64.decode64(attachment['Content']) + # TODO: should be done by a/the Storage object + # to handle fingerprinting + sha = Digest::SHA256.hexdigest(decoded_content) + + retries = 3 + begin + queueing(sha, decoded_filename) + + log "Ticket #{local_article.ticket_id}, Article #{local_article.id} - Starting import for fingerprint #{sha} (#{decoded_filename})... Queue: #{@sha_queue[sha]}." + ActiveRecord::Base.transaction do + Store.add( + object: 'Ticket::Article', + o_id: local_article.id, + filename: decoded_filename, + data: decoded_content, + preferences: { + 'Mime-Type' => attachment['ContentType'], + 'Content-ID' => attachment['ContentID'], + 'content-alternative' => attachment['ContentAlternative'], + }, + created_by_id: 1, + ) + end + log "Ticket #{local_article.ticket_id}, Article #{local_article.id} - Finished import for fingerprint #{sha} (#{decoded_filename})... Queue: #{@sha_queue[sha]}." + rescue ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid => e + log "Ticket #{local_article.ticket_id} - #{sha} - #{e.class}: #{e}" + sleep rand 3 + retry if !(retries -= 1).zero? + raise + ensure + queue_cleanup(sha) + end + end + + def skip_import?(attachments, local_article) + local_attachments = local_article.attachments + return true if local_attachments.count == attachments.count + # get a common ground + local_attachments.each(&:delete) + return true if attachments.empty? + false + end + + def queueing(sha, decoded_filename) + # this is (currently) needed for avoiding + # race conditions inserting attachments with + # the same fingerprint in the DB in concurrent threads + @sha_queue ||= {} + @sha_queue[sha] ||= [] + + return if !queueing_active? + @sha_queue[sha].push(queue_id) + + while @sha_queue[sha].first != queue_id + sleep_time = 0.25 + log "Found active import for fingerprint #{sha} (#{decoded_filename})... sleeping #{sleep_time} seconds. Queue: #{@sha_queue[sha]}." + sleep sleep_time + end + end + + def queue_cleanup(sha) + return if !queueing_active? + @sha_queue[sha].shift + end + + def queueing_active? + return if !queue_id + true + end + + def queue_id + Thread.current[:thread_no] + end + end + end + end +end diff --git a/lib/import/otrs/article_customer.rb b/lib/import/otrs/article_customer.rb new file mode 100644 index 000000000..330ca164a --- /dev/null +++ b/lib/import/otrs/article_customer.rb @@ -0,0 +1,84 @@ +module Import + module OTRS + class ArticleCustomer + include Import::Helper + + def initialize(article) + user = import(article) + return if !user + article['created_by_id'] = user.id + rescue Exceptions::UnprocessableEntity => e + log "ERROR: Can't extract customer from Article #{article[:id]}" + end + + private + + def import(article) + find_user_or_create(article) + end + + def extract_email(from) + Mail::Address.new(from).address + rescue + return from if from !~ /<\s*([^\s]+)/ + $1 + end + + def find_user_or_create(article) + user = user_found?(article) + return user if user + create_user(article) + end + + def user_found?(article) + email = extract_email(article['From']) + user = ::User.find_by(email: email) + user ||= ::User.find_by(login: email) + user + end + + def create_user(article) + email = extract_email(article['From']) + ::User.create( + login: email, + firstname: extract_display_name(article['from']), + lastname: '', + email: email, + password: '', + active: true, + role_ids: roles, + updated_by_id: 1, + created_by_id: 1, + ) + rescue ActiveRecord::RecordNotUnique + log "User #{email} was handled by another thread, taking this." + + return if user_found?(article) + + log "User #{email} wasn't created sleep and retry." + sleep rand 3 + retry + end + + def roles + [ + Role.find_by(name: 'Customer').id + ] + end + + def extract_display_name(from) + # do extra decoding because we needed to use field.value + Mail::Field.new('X-From', parsed_display_name(from)).to_s + end + + def parsed_display_name(from) + parsed_address = Mail::Address.new(from) + return parsed_address.display_name if parsed_address.display_name + return from if parsed_address.comments.empty? + parsed_address.comments[0] + rescue + from + end + end + end +end diff --git a/lib/import/otrs/article_customer_factory.rb b/lib/import/otrs/article_customer_factory.rb new file mode 100644 index 000000000..1ab64fe58 --- /dev/null +++ b/lib/import/otrs/article_customer_factory.rb @@ -0,0 +1,14 @@ +module Import + module OTRS + module ArticleCustomerFactory + extend Import::Factory + + def skip?(record) + return true if record['sender'] != 'customer' + return true if record['created_by_id'].to_i != 1 + return true if record['from'].empty? + false + end + end + end +end diff --git a/lib/import/otrs/article_factory.rb b/lib/import/otrs/article_factory.rb new file mode 100644 index 000000000..caf2a55bf --- /dev/null +++ b/lib/import/otrs/article_factory.rb @@ -0,0 +1,7 @@ +module Import + module OTRS + module ArticleFactory + extend Import::Factory + end + end +end diff --git a/lib/import/otrs/async.rb b/lib/import/otrs/async.rb new file mode 100644 index 000000000..fd54b1a3a --- /dev/null +++ b/lib/import/otrs/async.rb @@ -0,0 +1,62 @@ +module Import + module OTRS + module Async + # rubocop:disable Style/ModuleFunction + extend self + + def start_bg + Setting.reload + + Import::OTRS::Requester.connection_test + + # start thread to observe current state + status_update_thread = Thread.new { + loop do + result = { + data: current_state, + result: 'in_progress', + } + Cache.write('import:state', result, expires_in: 10.minutes) + sleep 8 + end + } + sleep 2 + + # start import data + begin + Import::OTRS.start + rescue => e + status_update_thread.exit + status_update_thread.join + Rails.logger.error e.message + Rails.logger.error e.backtrace.inspect + result = { + message: e.message, + result: 'error', + } + Cache.write('import:state', result, expires_in: 10.hours) + return false + end + sleep 16 # wait until new finished import state is on client + status_update_thread.exit + status_update_thread.join + + result = { + result: 'import_done', + } + Cache.write('import:state', result, expires_in: 10.hours) + + Setting.set('system_init_done', true) + Setting.set('import_mode', false) + end + + def status_bg + state = Cache.get('import:state') + return state if state + { + message: 'not running', + } + end + end + end +end diff --git a/lib/import/otrs/customer.rb b/lib/import/otrs/customer.rb new file mode 100644 index 000000000..9a3d31e1a --- /dev/null +++ b/lib/import/otrs/customer.rb @@ -0,0 +1,67 @@ +module Import + module OTRS + class Customer + include Import::Helper + include Import::OTRS::Helper + + MAPPING = { + ChangeTime: :updated_at, + CreateTime: :created_at, + CreateBy: :created_by_id, + ChangeBy: :updated_by_id, + CustomerCompanyName: :name, + CustomerCompanyComment: :note, + }.freeze + + def initialize(customer) + import(customer) + end + + def self.by_customer_id(customer_id) + organizations = Import::OTRS::Requester.load('Customer') + + result = nil + organizations.each do |organization| + next if customer_id != organization['CustomerID'] + result = Organization.find_by(name: organization['CustomerCompanyName']) + break + end + result + end + + private + + def import(customer) + create_or_update(map(customer)) + end + + def create_or_update(customer) + return if updated?(customer) + create(customer) + end + + def updated?(customer) + @local_customer = Organization.find_by(name: customer[:name]) + return false if !@local_customer + log "update Organization.find_by(name: #{customer[:name]})" + @local_customer.update_attributes(customer) + true + end + + def create(customer) + log "add Organization.find_by(name: #{customer[:name]})" + @local_customer = Organization.create(customer) + reset_primary_key_sequence('organizations') + end + + def map(customer) + { + created_by_id: 1, + updated_by_id: 1, + active: active?(customer), + } + .merge(from_mapping(customer)) + end + end + end +end diff --git a/lib/import/otrs/customer_factory.rb b/lib/import/otrs/customer_factory.rb new file mode 100644 index 000000000..1c2c36f46 --- /dev/null +++ b/lib/import/otrs/customer_factory.rb @@ -0,0 +1,7 @@ +module Import + module OTRS + module CustomerFactory + extend Import::Factory + end + end +end diff --git a/lib/import/otrs/customer_user.rb b/lib/import/otrs/customer_user.rb new file mode 100644 index 000000000..1d98ba88d --- /dev/null +++ b/lib/import/otrs/customer_user.rb @@ -0,0 +1,92 @@ +module Import + module OTRS + class CustomerUser + include Import::Helper + include Import::OTRS::Helper + + MAPPING = { + ChangeTime: :updated_at, + CreateTime: :created_at, + CreateBy: :created_by_id, + ChangeBy: :updated_by_id, + UserComment: :note, + UserEmail: :email, + UserFirstname: :firstname, + UserLastname: :lastname, + UserLogin: :login, + UserPassword: :password, + UserPhone: :phone, + UserFax: :fax, + UserMobile: :mobile, + UserStreet: :street, + UserZip: :zip, + UserCity: :city, + UserCountry: :country, + }.freeze + + def initialize(customer) + import(customer) + end + + private + + def import(customer) + create_or_update(map(customer)) + end + + def create_or_update(customer) + return if updated?(customer) + create(customer) + end + + def updated?(customer) + @local_customer = ::User.find_by(login: customer[:login]) + return false if !@local_customer + + # do not update user if it is already agent + return true if @local_customer.role_ids.include?(Role.find_by(name: 'Agent').id) + + # only update roles if different (reduce sql statements) + if @local_customer.role_ids == customer[:role_ids] + user.delete(:role_ids) + end + + log "update User.find_by(login: #{customer[:login]})" + @local_customer.update_attributes(customer) + true + end + + def create(customer) + log "add User.find_by(login: #{customer[:login]})" + @local_customer = ::User.new(customer) + @local_customer.save + reset_primary_key_sequence('users') + end + + def map(customer) + { + created_by_id: 1, + updated_by_id: 1, + active: active?(customer), + source: 'OTRS Import', + organization_id: organization_id(customer), + role_ids: role_ids, + } + .merge(from_mapping(customer)) + end + + def role_ids + [ + Role.find_by(name: 'Customer').id + ] + end + + def organization_id(customer) + return if !customer['UserCustomerID'] + organization = Import::OTRS::Customer.by_customer_id(customer['UserCustomerID']) + return if !organization + organization.id + end + end + end +end diff --git a/lib/import/otrs/customer_user_factory.rb b/lib/import/otrs/customer_user_factory.rb new file mode 100644 index 000000000..02ee37daf --- /dev/null +++ b/lib/import/otrs/customer_user_factory.rb @@ -0,0 +1,7 @@ +module Import + module OTRS + module CustomerUserFactory + extend Import::Factory + end + end +end diff --git a/lib/import/otrs/diff.rb b/lib/import/otrs/diff.rb new file mode 100644 index 000000000..316457953 --- /dev/null +++ b/lib/import/otrs/diff.rb @@ -0,0 +1,36 @@ +module Import + module OTRS + module Diff + # rubocop:disable Style/ModuleFunction + extend self + + def diff_worker + return if !diff_import_possible? + diff + end + + private + + def diff_import_possible? + return if !Setting.get('import_mode') + return if Setting.get('import_otrs_endpoint') == 'http://otrs_host/otrs' + true + end + + def diff + log 'Start diff...' + + check_import_mode + + updateable_objects + + # get changed tickets + ticket_diff + end + + def ticket_diff + import('Ticket', diff: true) + end + end + end +end diff --git a/lib/import/otrs/dynamic_field.rb b/lib/import/otrs/dynamic_field.rb new file mode 100644 index 000000000..b108d6aa6 --- /dev/null +++ b/lib/import/otrs/dynamic_field.rb @@ -0,0 +1,61 @@ +module Import + module OTRS + class DynamicField + + def initialize(dynamic_field) + @internal_name = self.class.convert_name(dynamic_field['Name']) + + return if already_imported?(dynamic_field) + + initialize_attribute_config(dynamic_field) + + init_callback(dynamic_field) + add + end + + def self.convert_name(dynamic_field_name) + dynamic_field_name.underscore.sub(/\_id(s)?\z/, '_no\1') + end + + private + + def init_callback(_) + raise 'No init callback defined for this dynamic field!' + end + + def already_imported?(dynamic_field) + attribute = ObjectManager::Attribute.get( + object: dynamic_field['ObjectType'], + name: @internal_name, + ) + attribute ? true : false + end + + def initialize_attribute_config(dynamic_field) + + @attribute_config = { + object: dynamic_field['ObjectType'], + name: @internal_name, + display: dynamic_field['Label'], + screens: { + view: { + '-all-' => { + shown: true, + }, + }, + }, + active: true, + editable: dynamic_field['InternalField'] == '0', + position: dynamic_field['FieldOrder'], + created_by_id: 1, + updated_by_id: 1, + } + end + + def add + ObjectManager::Attribute.add(@attribute_config) + ObjectManager::Attribute.migration_execute(false) + end + end + end +end diff --git a/lib/import/otrs/dynamic_field/checkbox.rb b/lib/import/otrs/dynamic_field/checkbox.rb new file mode 100644 index 000000000..8666986ac --- /dev/null +++ b/lib/import/otrs/dynamic_field/checkbox.rb @@ -0,0 +1,22 @@ +module Import + module OTRS + class DynamicField + class Checkbox < Import::OTRS::DynamicField + def init_callback(dynamic_field) + @attribute_config.merge!( + data_type: 'boolean', + data_option: { + default: dynamic_field['Config']['DefaultValue'] == '1', + options: { + true => 'Yes', + false => 'No', + }, + null: false, + translate: true, + } + ) + end + end + end + end +end diff --git a/lib/import/otrs/dynamic_field/date.rb b/lib/import/otrs/dynamic_field/date.rb new file mode 100644 index 000000000..6be4508bd --- /dev/null +++ b/lib/import/otrs/dynamic_field/date.rb @@ -0,0 +1,24 @@ +# this require is required (hehe) because of Rails autoloading +# which causes strange behavior not inheriting correctly +# from Import::OTRS::DynamicField +require 'import/otrs/dynamic_field' + +module Import + module OTRS + class DynamicField + class Date < Import::OTRS::DynamicField + def init_callback(dynamic_field) + @attribute_config.merge!( + data_type: 'date', + data_option: { + future: dynamic_field['Config']['YearsInFuture'] != '0', + past: dynamic_field['Config']['YearsInPast'] != '0', + diff: dynamic_field['Config']['DefaultValue'].to_i / 60 / 60 / 24, + null: false, + } + ) + end + end + end + end +end diff --git a/lib/import/otrs/dynamic_field/date_time.rb b/lib/import/otrs/dynamic_field/date_time.rb new file mode 100644 index 000000000..997a4ac49 --- /dev/null +++ b/lib/import/otrs/dynamic_field/date_time.rb @@ -0,0 +1,24 @@ +# this require is required (hehe) because of Rails autoloading +# which causes strange behavior not inheriting correctly +# from Import::OTRS::DynamicField +require 'import/otrs/dynamic_field' + +module Import + module OTRS + class DynamicField + class DateTime < Import::OTRS::DynamicField + def init_callback(dynamic_field) + @attribute_config.merge!( + data_type: 'datetime', + data_option: { + future: dynamic_field['Config']['YearsInFuture'] != '0', + past: dynamic_field['Config']['YearsInPast'] != '0', + diff: dynamic_field['Config']['DefaultValue'].to_i / 60 / 60, + null: false, + } + ) + end + end + end + end +end diff --git a/lib/import/otrs/dynamic_field/dropdown.rb b/lib/import/otrs/dynamic_field/dropdown.rb new file mode 100644 index 000000000..a6d0d8c5a --- /dev/null +++ b/lib/import/otrs/dynamic_field/dropdown.rb @@ -0,0 +1,20 @@ +module Import + module OTRS + class DynamicField + class Dropdown < Import::OTRS::DynamicField + def init_callback(dynamic_field) + @attribute_config.merge!( + data_type: 'select', + data_option: { + default: '', + multiple: false, + options: dynamic_field['Config']['PossibleValues'], + null: dynamic_field['Config']['PossibleNone'] == '1', + translate: dynamic_field['Config']['TranslatableValues'] == '1', + } + ) + end + end + end + end +end diff --git a/lib/import/otrs/dynamic_field/multiselect.rb b/lib/import/otrs/dynamic_field/multiselect.rb new file mode 100644 index 000000000..3710fc7d0 --- /dev/null +++ b/lib/import/otrs/dynamic_field/multiselect.rb @@ -0,0 +1,20 @@ +module Import + module OTRS + class DynamicField + class Multiselect < Import::OTRS::DynamicField + def init_callback(dynamic_field) + @attribute_config.merge!( + data_type: 'select', + data_option: { + default: '', + multiple: true, + options: dynamic_field['Config']['PossibleValues'], + null: dynamic_field['Config']['PossibleNone'] == '1', + translate: dynamic_field['Config']['TranslatableValues'] == '1', + } + ) + end + end + end + end +end diff --git a/lib/import/otrs/dynamic_field/text.rb b/lib/import/otrs/dynamic_field/text.rb new file mode 100644 index 000000000..cafcb7f41 --- /dev/null +++ b/lib/import/otrs/dynamic_field/text.rb @@ -0,0 +1,19 @@ +module Import + module OTRS + class DynamicField + class Text < Import::OTRS::DynamicField + def init_callback(dynamic_field) + @attribute_config.merge!( + data_type: 'input', + data_option: { + default: dynamic_field['Config']['DefaultValue'], + type: 'text', + maxlength: 255, + null: false, + } + ) + end + end + end + end +end diff --git a/lib/import/otrs/dynamic_field/text_area.rb b/lib/import/otrs/dynamic_field/text_area.rb new file mode 100644 index 000000000..e4b71ce72 --- /dev/null +++ b/lib/import/otrs/dynamic_field/text_area.rb @@ -0,0 +1,18 @@ +module Import + module OTRS + class DynamicField + class TextArea < Import::OTRS::DynamicField + def init_callback(dynamic_field) + @attribute_config.merge!( + data_type: 'textarea', + data_option: { + default: dynamic_field['Config']['DefaultValue'], + rows: dynamic_field['Config']['Rows'], + null: false, + } + ) + end + end + end + end +end diff --git a/lib/import/otrs/dynamic_field_factory.rb b/lib/import/otrs/dynamic_field_factory.rb new file mode 100644 index 000000000..f6bbf350e --- /dev/null +++ b/lib/import/otrs/dynamic_field_factory.rb @@ -0,0 +1,52 @@ +module Import + module OTRS + module DynamicFieldFactory + extend Import::Factory + extend Import::Helper + + # rubocop:disable Style/ModuleFunction + extend self + + def skip?(record) + return true if !importable?(record) + return true if skip_field?(record['Name']) + false + end + + def backend_class(record) + "Import::OTRS::DynamicField::#{record['FieldType']}".constantize + end + + def skip_field?(dynamic_field_name) + skip_fields.include?(dynamic_field_name) + end + + private + + def importable?(dynamic_field) + return false if !supported_object_type?(dynamic_field) + supported_field_type?(dynamic_field) + end + + def supported_object_type?(dynamic_field) + return true if dynamic_field['ObjectType'] == 'Ticket' + log "ERROR: Unsupported dynamic field object type '#{dynamic_field['ObjectType']}' for dynamic field '#{dynamic_field['Name']}'" + false + end + + def supported_field_type?(dynamic_field) + return true if supported_field_types.include?(dynamic_field['FieldType']) + log "ERROR: Unsupported dynamic field field type '#{dynamic_field['FieldType']}' for dynamic field '#{dynamic_field['Name']}'" + false + end + + def supported_field_types + %w(Text TextArea Checkbox DateTime Date Dropdown Multiselect) + end + + def skip_fields + %w(ProcessManagementProcessID ProcessManagementActivityID ZammadMigratorChanged ZammadMigratorChangedOld) + end + end + end +end diff --git a/lib/import/otrs/helper.rb b/lib/import/otrs/helper.rb new file mode 100644 index 000000000..107b74956 --- /dev/null +++ b/lib/import/otrs/helper.rb @@ -0,0 +1,37 @@ +module Import + module OTRS + module Helper + # rubocop:disable Style/ModuleFunction + extend self + + private + + def from_mapping(record) + result = {} + # use the mapping of the class in which + # this module gets extended + self.class::MAPPING.each { |key_sym, value| + key = key_sym.to_s + next if !record.key?(key) + result[value] = record[key] + } + result + end + + def active?(record) + case record['ValidID'].to_s + when '3' + false + when '2' + false + when '1' + true + when '0' + false + else + true + end + end + end + end +end diff --git a/lib/import/otrs/history.rb b/lib/import/otrs/history.rb new file mode 100644 index 000000000..d1ef84cef --- /dev/null +++ b/lib/import/otrs/history.rb @@ -0,0 +1,46 @@ +# rubocop:disable Style/ClassVars +module Import + module OTRS + class History + + def initialize(history) + init_callback(history) + ensure_history_attribute + add + end + + def init_callback(_) + raise 'No init callback defined for this history!' + end + + private + + def add + ::History.add(@history_attributes) + end + + # make sure that no other thread is importing just the same + # history attribute which causes a ActiveRecord::RecordNotUnique + # exception we (currently) can't handle otherwise + def ensure_history_attribute + history_attribute = @history_attributes[:history_attribute] + return if !history_attribute + return if history_attribute_exists?(history_attribute) + @@created_history_attributes[history_attribute] = true + ::History.attribute_lookup(history_attribute) + end + + def history_attribute_exists?(name) + @@created_history_attributes ||= {} + return false if !@@created_history_attributes[name] + + # make sure the history attribute is added before we + # we perform further import + # otherwise the following import logic (add) will + # try to add the history attribute, too + sleep 1 until ::History::Attribute.find_by(name: name) + true + end + end + end +end diff --git a/lib/import/otrs/history/article.rb b/lib/import/otrs/history/article.rb new file mode 100644 index 000000000..4145f7987 --- /dev/null +++ b/lib/import/otrs/history/article.rb @@ -0,0 +1,20 @@ +module Import + module OTRS + class History + class Article < Import::OTRS::History + def init_callback(history) + @history_attributes = { + id: history['HistoryID'], + o_id: history['ArticleID'], + history_type: 'created', + history_object: 'Ticket::Article', + related_o_id: history['TicketID'], + related_history_object: 'Ticket', + created_at: history['CreateTime'], + created_by_id: history['CreateBy'] + } + end + end + end + end +end diff --git a/lib/import/otrs/history/move.rb b/lib/import/otrs/history/move.rb new file mode 100644 index 000000000..b328e875e --- /dev/null +++ b/lib/import/otrs/history/move.rb @@ -0,0 +1,33 @@ +module Import + module OTRS + class History + class Move < Import::OTRS::History + def init_callback(history) + data = history['Name'] + # "%%Queue1%%5%%Postmaster%%1" + from = nil + to = nil + if data =~ /%%(.+?)%%(.+?)%%(.+?)%%(.+?)$/ + from = $1 + from_id = $2 + to = $3 + to_id = $4 + end + @history_attributes = { + id: history['HistoryID'], + o_id: history['TicketID'], + history_type: 'updated', + history_object: 'Ticket', + history_attribute: 'group', + value_from: from, + value_to: to, + id_from: from_id, + id_to: to_id, + created_at: history['CreateTime'], + created_by_id: history['CreateBy'] + } + end + end + end + end +end diff --git a/lib/import/otrs/history/new_ticket.rb b/lib/import/otrs/history/new_ticket.rb new file mode 100644 index 000000000..5459f6ba2 --- /dev/null +++ b/lib/import/otrs/history/new_ticket.rb @@ -0,0 +1,18 @@ +module Import + module OTRS + class History + class NewTicket < Import::OTRS::History + def init_callback(history) + @history_attributes = { + id: history['HistoryID'], + o_id: history['TicketID'], + history_type: 'created', + history_object: 'Ticket', + created_at: history['CreateTime'], + created_by_id: history['CreateBy'] + } + end + end + end + end +end diff --git a/lib/import/otrs/history/priority_update.rb b/lib/import/otrs/history/priority_update.rb new file mode 100644 index 000000000..4659c148a --- /dev/null +++ b/lib/import/otrs/history/priority_update.rb @@ -0,0 +1,33 @@ +module Import + module OTRS + class History + class PriorityUpdate < Import::OTRS::History + def init_callback(history) + data = history['Name'] + # "%%3 normal%%3%%5 very high%%5" + from = nil + to = nil + if data =~ /%%(.+?)%%(.+?)%%(.+?)%%(.+?)$/ + from = $1 + from_id = $2 + to = $3 + to_id = $4 + end + @history_attributes = { + id: history['HistoryID'], + o_id: history['TicketID'], + history_type: 'updated', + history_object: 'Ticket', + history_attribute: 'priority', + value_from: from, + value_to: to, + id_from: from_id, + id_to: to_id, + created_at: history['CreateTime'], + created_by_id: history['CreateBy'] + } + end + end + end + end +end diff --git a/lib/import/otrs/history/state_update.rb b/lib/import/otrs/history/state_update.rb new file mode 100644 index 000000000..a1b7ce280 --- /dev/null +++ b/lib/import/otrs/history/state_update.rb @@ -0,0 +1,39 @@ +module Import + module OTRS + class History + class StateUpdate < Import::OTRS::History + def init_callback(history) + data = history['Name'] + # "%%new%%open%%" + from = nil + to = nil + if data =~ /%%(.+?)%%(.+?)%%/ + from = $1 + to = $2 + state_from = ::Ticket::State.lookup(name: from) + state_to = ::Ticket::State.lookup(name: to) + if state_from + from_id = state_from.id + end + if state_to + to_id = state_to.id + end + end + @history_attributes = { + id: history['HistoryID'], + o_id: history['TicketID'], + history_type: 'updated', + history_object: 'Ticket', + history_attribute: 'state', + value_from: from, + id_from: from_id, + value_to: to, + id_to: to_id, + created_at: history['CreateTime'], + created_by_id: history['CreateBy'] + } + end + end + end + end +end diff --git a/lib/import/otrs/history_factory.rb b/lib/import/otrs/history_factory.rb new file mode 100644 index 000000000..3d91b66e9 --- /dev/null +++ b/lib/import/otrs/history_factory.rb @@ -0,0 +1,40 @@ +module Import + module OTRS + module HistoryFactory + extend Import::Factory + + # rubocop:disable Style/ModuleFunction + extend self + + def skip?(record) + return true if !determine_class(record) + false + end + + def backend_class(record) + "Import::OTRS::History::#{determine_class(record)}".constantize + end + + private + + def determine_class(history) + check_supported(history) || check_article(history) + end + + def supported_types + %w(NewTicket StateUpdate Move PriorityUpdate) + end + + def check_supported(history) + return if !supported_types.include?(history['HistoryType']) + history['HistoryType'] + end + + def check_article(history) + return if !history['ArticleID'] + return if history['ArticleID'].to_i.zero? + 'Article' + end + end + end +end diff --git a/lib/import/otrs/import_stats.rb b/lib/import/otrs/import_stats.rb new file mode 100644 index 000000000..e1c437501 --- /dev/null +++ b/lib/import/otrs/import_stats.rb @@ -0,0 +1,74 @@ +module Import + module OTRS + module ImportStats + # rubocop:disable Style/ModuleFunction + extend self + + def current_state + { + Base: { + done: base_done, + total: base_total, + }, + User: { + done: user_done, + total: user_total, + }, + Ticket: { + done: ticket_done, + total: ticket_total, + }, + } + end + + private + + def statistic + + # check cache + cache = Cache.get('import_otrs_stats') + return cache if cache + + # retrive statistic + statistic = Import::OTRS::Requester.list + return statistic if !statistic + + Cache.write('import_otrs_stats', statistic) + statistic + end + + def base_done + ::Group.count + ::Ticket::State.count + ::Ticket::Priority.count + end + + def base_total + sum_stat(%w(Queue State Priority)) + end + + def user_done + ::User.count + end + + def user_total + sum_stat(%w(User CustomerUser)) + end + + def ticket_done + ::Ticket.count + end + + def ticket_total + sum_stat(%w(Ticket)) + end + + def sum_stat(objects) + data = statistic + sum = 0 + objects.each { |object| + sum += data[object] + } + sum + end + end + end +end diff --git a/lib/import/otrs/priority.rb b/lib/import/otrs/priority.rb new file mode 100644 index 000000000..8882a7ad9 --- /dev/null +++ b/lib/import/otrs/priority.rb @@ -0,0 +1,58 @@ +module Import + module OTRS + class Priority + include Import::Helper + include Import::OTRS::Helper + + MAPPING = { + ChangeTime: :updated_at, + CreateTime: :created_at, + CreateBy: :created_by_id, + ChangeBy: :updated_by_id, + Name: :name, + ID: :id, + Comment: :note, + }.freeze + + def initialize(priority) + import(priority) + end + + private + + def import(priority) + create_or_update(map(priority)) + end + + def create_or_update(priority) + return if updated?(priority) + create(priority) + end + + def updated?(priority) + @local_priority = ::Ticket::Priority.find_by(id: priority[:id]) + return false if !@local_priority + log "update Ticket::Priority.find_by(id: #{priority[:id]})" + @local_priority.update_attributes(priority) + true + end + + def create(priority) + log "add Ticket::Priority.find_by(id: #{priority[:id]})" + @local_priority = ::Ticket::Priority.new(priority) + @local_priority.id = priority[:id] + @local_priority.save + reset_primary_key_sequence('ticket_priorities') + end + + def map(priority) + { + created_by_id: 1, + updated_by_id: 1, + active: active?(priority), + } + .merge(from_mapping(priority)) + end + end + end +end diff --git a/lib/import/otrs/priority_factory.rb b/lib/import/otrs/priority_factory.rb new file mode 100644 index 000000000..ebd405775 --- /dev/null +++ b/lib/import/otrs/priority_factory.rb @@ -0,0 +1,7 @@ +module Import + module OTRS + module PriorityFactory + extend Import::Factory + end + end +end diff --git a/lib/import/otrs/queue.rb b/lib/import/otrs/queue.rb new file mode 100644 index 000000000..a00e4531a --- /dev/null +++ b/lib/import/otrs/queue.rb @@ -0,0 +1,58 @@ +module Import + module OTRS + class Queue + include Import::Helper + include Import::OTRS::Helper + + MAPPING = { + ChangeTime: :updated_at, + CreateTime: :created_at, + CreateBy: :created_by_id, + ChangeBy: :updated_by_id, + Name: :name, + QueueID: :id, + Comment: :note, + }.freeze + + def initialize(queue) + import(queue) + end + + private + + def import(queue) + create_or_update(map(queue)) + end + + def create_or_update(queue) + return if updated?(queue) + create(queue) + end + + def updated?(queue) + @local_queue = Group.find_by(id: queue[:id]) + return false if !@local_queue + log "update Group.find_by(id: #{queue[:id]})" + @local_queue.update_attributes(queue) + true + end + + def create(queue) + log "add Group.find_by(id: #{queue[:id]})" + @local_queue = Group.new(queue) + @local_queue.id = queue[:id] + @local_queue.save + reset_primary_key_sequence('groups') + end + + def map(queue) + { + created_by_id: 1, + updated_by_id: 1, + active: active?(queue), + } + .merge(from_mapping(queue)) + end + end + end +end diff --git a/lib/import/otrs/queue_factory.rb b/lib/import/otrs/queue_factory.rb new file mode 100644 index 000000000..b2ec3ba35 --- /dev/null +++ b/lib/import/otrs/queue_factory.rb @@ -0,0 +1,7 @@ +module Import + module OTRS + module QueueFactory + extend Import::Factory + end + end +end diff --git a/lib/import/otrs/requester.rb b/lib/import/otrs/requester.rb new file mode 100644 index 000000000..69b1d4fb2 --- /dev/null +++ b/lib/import/otrs/requester.rb @@ -0,0 +1,93 @@ +module Import + module OTRS + module Requester + extend Import::Helper + + # rubocop:disable Style/ModuleFunction + extend self + + def load(object, args = {}) + + @cache ||= {} + if args.empty? && @cache[object] + return @cache[object] + end + + result = request_result( + Subaction: 'Export', + Object: object, + Limit: args[:limit] || '', + Offset: args[:offset] || '', + Diff: args[:diff] ? 1 : 0 + ) + + return result if !args.empty? + @cache[object] = result + @cache[object] + end + + def list + request_result(Subaction: 'List') + end + + # TODO: refactor to something like .connected? + def connection_test + result = request_json({}) + return true if result['Success'] + raise 'API key not valid!' + end + + private + + def request_result(params) + response = request_json(params) + response['Result'] + end + + def request_json(params) + response = post(params) + result = handle_response(response) + + return result if result + + raise 'Invalid response' + end + + def handle_response(response) + encoded_body = Encode.conv('utf8', response.body.to_s) + JSON.parse(encoded_body) + end + + def post(params) + url = Setting.get('import_otrs_endpoint') + params[:Action] = 'ZammadMigrator' + params[:Key] = Setting.get('import_otrs_endpoint_key') + + log 'POST: ' + url + log 'PARAMS: ' + params.inspect + + response = UserAgent.post( + url, + params, + { + open_timeout: 10, + read_timeout: 120, + total_timeout: 360, + user: Setting.get('import_otrs_user'), + password: Setting.get('import_otrs_password'), + }, + ) + + if !response + raise "Can't connect to Zammad Migrator" + end + + if !response.success? + log "ERROR: #{response.error}" + raise 'Zammad Migrator returned an error' + end + response + end + end + end +end diff --git a/lib/import/otrs/state.rb b/lib/import/otrs/state.rb new file mode 100644 index 000000000..3841e441e --- /dev/null +++ b/lib/import/otrs/state.rb @@ -0,0 +1,70 @@ +module Import + module OTRS + class State + include Import::Helper + include Import::OTRS::Helper + + MAPPING = { + ChangeTime: :updated_at, + CreateTime: :created_at, + CreateBy: :created_by_id, + ChangeBy: :updated_by_id, + Name: :name, + ID: :id, + ValidID: :active, + Comment: :note, + }.freeze + + def initialize(state) + import(state) + end + + private + + def import(state) + create_or_update(map(state)) + end + + def create_or_update(state) + return if updated?(state) + create(state) + end + + def updated?(state) + @local_state = ::Ticket::State.find_by(id: state[:id]) + return false if !@local_state + log "update Ticket::State.find_by(id: #{state[:id]})" + @local_state.update_attributes(state) + true + end + + def create(state) + log "add Ticket::State.find_by(id: #{state[:id]})" + @local_state = ::Ticket::State.new(state) + @local_state.id = state[:id] + @local_state.save + reset_primary_key_sequence('ticket_states') + end + + def map(state) + { + created_by_id: 1, + updated_by_id: 1, + active: active?(state), + state_type_id: state_type_id(state) + } + .merge(from_mapping(state)) + end + + def state_type_id(state) + map_type(state) + ::Ticket::StateType.lookup(name: state['TypeName']).id + end + + def map_type(state) + return if state['TypeName'] != 'pending auto' + state['TypeName'] = 'pending action' + end + end + end +end diff --git a/lib/import/otrs/state_factory.rb b/lib/import/otrs/state_factory.rb new file mode 100644 index 000000000..40b85c8b1 --- /dev/null +++ b/lib/import/otrs/state_factory.rb @@ -0,0 +1,22 @@ +module Import + module OTRS + module StateFactory + extend Import::TransactionFactory + + # rubocop:disable Style/ModuleFunction + extend self + + def pre_import_hook(_records) + backup + end + + def backup + # rename states to handle not uniq issues + ::Ticket::State.all.each { |state| + state.name = state.name + '_tmp' + state.save + } + end + end + end +end diff --git a/lib/import/otrs/sys_config_factory.rb b/lib/import/otrs/sys_config_factory.rb new file mode 100644 index 000000000..da118a481 --- /dev/null +++ b/lib/import/otrs/sys_config_factory.rb @@ -0,0 +1,51 @@ +module Import + module OTRS + module SysConfigFactory + + # rubocop:disable Style/ModuleFunction + extend self + + def import(settings) + settings.each do |setting| + next if direct_copy?(setting) + next if number_generator?(setting) + end + end + + private + + def direct_settings + %w(HttpType SystemID Organization TicketHook) + end + + def direct_copy?(setting) + cleaned_name = cleanup_name(setting['Key']) + return false if !direct_settings.include?(cleaned_name) + + internal_name = cleaned_name.underscore + Setting.set(internal_name, setting['Value']) + + true + end + + def cleanup_name(key) + key.tr('::', '') + end + + def number_generator?(setting) + return false if setting['Key'] != 'Ticket::NumberGenerator' + + case setting['Value'] + when 'Kernel::System::Ticket::Number::DateChecksum' + Setting.set('ticket_number', 'Ticket::Number::Date') + Setting.set('ticket_number_date', { checksum: true }) + when 'Kernel::System::Ticket::Number::Date' + Setting.set('ticket_number', 'Ticket::Number::Date') + Setting.set('ticket_number_date', { checksum: false }) + end + + true + end + end + end +end diff --git a/lib/import/otrs/ticket.rb b/lib/import/otrs/ticket.rb new file mode 100644 index 000000000..605b89f51 --- /dev/null +++ b/lib/import/otrs/ticket.rb @@ -0,0 +1,172 @@ +require 'ticket' + +module Import + module OTRS + class Ticket + include Import::Helper + include Import::OTRS::Helper + + MAPPING = { + Changed: :updated_at, + Created: :created_at, + CreateBy: :created_by_id, + TicketNumber: :number, + QueueID: :group_id, + StateID: :state_id, + PriorityID: :priority_id, + Title: :title, + TicketID: :id, + FirstResponse: :first_response_at, + #FirstResponseTimeDestinationDate: :first_response_escalation_at, + #FirstResponseInMin: :first_response_in_min, + #FirstResponseDiffInMin: :first_response_diff_in_min, + Closed: :close_at, + #SoltutionTimeDestinationDate: :close_escalation_at, + #CloseTimeInMin: :close_in_min, + #CloseTimeDiffInMin: :close_diff_in_min, + }.freeze + + def initialize(ticket) + fix(ticket) + import(ticket) + end + + private + + def import(ticket) + Import::OTRS::ArticleCustomerFactory.import(ticket['Articles']) + + create_or_update(map(ticket)) + + Import::OTRS::ArticleFactory.import(ticket['Articles']) + Import::OTRS::HistoryFactory.import(ticket['History']) + end + + def create_or_update(ticket) + return if updated?(ticket) + create(ticket) + end + + def updated?(ticket) + @local_ticket = ::Ticket.find_by(id: ticket[:id]) + return false if !@local_ticket + log "update Ticket.find_by(id: #{ticket[:id]})" + @local_ticket.update_attributes(ticket) + true + end + + def create(ticket) + log "add Ticket.find_by(id: #{ticket[:id]})" + @local_ticket = ::Ticket.new(ticket) + @local_ticket.id = ticket[:id] + @local_ticket.save + reset_primary_key_sequence('tickets') + rescue ActiveRecord::RecordNotUnique + log "Ticket #{ticket[:id]} is handled by another thead, skipping." + end + + def map(ticket) + ensure_map(default_map(ticket)) + end + + def ensure_map(mapped) + return mapped if mapped[:title] + mapped[:title] = '**EMPTY**' + mapped + end + + def default_map(ticket) + { + owner_id: owner_id(ticket), + customer_id: customer_id(ticket), + created_by_id: 1, + updated_by_id: 1, + } + .merge(from_mapping(ticket)) + .merge(dynamic_fields(ticket)) + end + + def dynamic_fields(ticket) + result = {} + ticket.keys.each { |key| + + key_string = key.to_s + + next if !key_string.start_with?('DynamicField_') + dynamic_field_name = key_string[13, key_string.length] + + next if Import::OTRS::DynamicFieldFactory.skip_field?( dynamic_field_name ) + dynamic_field_name = Import::OTRS::DynamicField.convert_name(dynamic_field_name) + + result[dynamic_field_name.to_sym] = ticket[key_string] + } + result + end + + def owner_id(ticket) + default = 1 + owner = ticket['Owner'] + + return default if !owner + user = user_lookup(owner) + + return user.id if user + default + end + + def customer_id(ticket) + default = 1 + customer = ticket['CustomerUserID'] + + return default if !customer + user = user_lookup(customer) + + return user.id if user + + first_customer_id = first_customer_id(ticket['Articles']) + + return first_customer_id if first_customer_id + + default + end + + def user_lookup(login) + ::User.find_by(login: login.downcase) + end + + def first_customer_id(articles) + user_id = nil + articles.each { |article| + next if article['sender'] != 'customer' + next if article['from'].empty? + + user_id = article['created_by_id'].to_i + break + } + user_id + end + + # cleanup invalid values + def fix(ticket) + utf8_encode(ticket) + fix_timestamps(ticket) + fix_close_time(ticket) + end + + def fix_timestamps(ticket) + ticket.each { |key, value| + next if value != '0000-00-00 00:00:00' + ticket[key] = nil + } + end + + # fix OTRS 3.1 bug, no close time if ticket is created + def fix_close_time(ticket) + return if ticket['StateType'] != 'closed' + return if ticket['Closed'] + return if !ticket['Closed'].empty? + ticket['Closed'] = ticket['Created'] + end + end + end +end diff --git a/lib/import/otrs/ticket_factory.rb b/lib/import/otrs/ticket_factory.rb new file mode 100644 index 000000000..501b3c594 --- /dev/null +++ b/lib/import/otrs/ticket_factory.rb @@ -0,0 +1,7 @@ +module Import + module OTRS + module TicketFactory + extend Import::Factory + end + end +end diff --git a/lib/import/otrs/user.rb b/lib/import/otrs/user.rb new file mode 100644 index 000000000..2ff06d478 --- /dev/null +++ b/lib/import/otrs/user.rb @@ -0,0 +1,167 @@ +module Import + module OTRS + class User + include Import::Helper + include Import::OTRS::Helper + + MAPPING = { + ChangeTime: :updated_at, + CreateTime: :created_at, + CreateBy: :created_by_id, + ChangeBy: :updated_by_id, + UserID: :id, + Comment: :note, + UserEmail: :email, + UserFirstname: :firstname, + UserLastname: :lastname, + UserLogin: :login, + }.freeze + + def initialize(user) + import(user) + end + + private + + def import(user) + create_or_update(map(user)) + end + + def create_or_update(user) + ensure_unique_login(user) + return if updated?(user) + create(user) + end + + def updated?(user) + @local_user = ::User.find_by(id: user[:id]) + return false if !@local_user + + # only update roles if different (reduce sql statements) + if @local_user.role_ids == user[:role_ids] + user.delete(:role_ids) + end + + log "update User.find_by(id: #{user[:id]})" + @local_user.update_attributes(user) + true + end + + def create(user) + log "add User.find_by(id: #{user[:id]})" + @local_user = ::User.new(user) + @local_user.id = user[:id] + @local_user.save + reset_primary_key_sequence('users') + end + + def ensure_unique_login(user) + user[:login] = unique_login(user) + end + + def unique_login(user) + login = user[:login] + return login if ::User.where('login = ? AND id != ?', login.downcase, user[:id]).count.zero? + "#{login}_#{user[:id]}" + end + + def map(user) + { + created_by_id: 1, + updated_by_id: 1, + active: active?(user), + source: 'OTRS Import', + role_ids: role_ids(user), + group_ids: group_ids(user), + password: password(user), + } + .merge(from_mapping(user)) + end + + def password(user) + return if !user['UserPw'] + "{sha2}#{user['UserPw']}" + end + + def group_ids(user) + result = [] + queues = Import::OTRS::Requester.load('Queue') + queues.each { |queue| + + permissions = user['GroupIDs'][ queue['GroupID'] ] + + next if !permissions + next if !permissions.include?('rw') + + result.push queue['QueueID'] + } + + # lookup by roles + + # roles of user + # groups of roles + # queues of group + + result + end + + def role_ids(user) + local_role_ids = [] + roles(user).each { |role| + role_lookup = Role.lookup(name: role) + next if !role_lookup + local_role_ids.push role_lookup.id + } + local_role_ids + end + + def roles(user) + local_roles = ['Agent'] + local_roles += groups_from_otrs_groups(user) + local_roles += groups_from_otrs_roles(user) + local_roles.uniq + end + + def groups_from_otrs_groups(user) + groups = Import::OTRS::Requester.load('Group') + groups_from_groups(user, groups) + end + + def groups_from_groups(user, groups) + result = [] + groups.each { |group| + result += groups_from_otrs_group(user, group) + } + result + end + + def groups_from_otrs_group(user, group) + result = [] + return result if user['GroupIDs'].empty? + permissions = user['GroupIDs'][ group['ID'] ] + + return result if !permissions + + if group['Name'] == 'admin' && permissions.include?('rw') + result.push 'Admin' + end + + return result if group['Name'] !~ /^(stats|report)/ + return result if !( permissions.include?('ro') || permissions.include?('rw') ) + + result.push 'Report' + result + end + + def groups_from_otrs_roles(user) + result = [] + roles = Import::OTRS::Requester.load('Role') + roles.each { |role| + next if !user['RoleIDs'].include?(role['ID']) + result += groups_from_groups(user, role['GroupIDs']) + } + result + end + end + end +end diff --git a/lib/import/otrs/user_factory.rb b/lib/import/otrs/user_factory.rb new file mode 100644 index 000000000..ea49ccc77 --- /dev/null +++ b/lib/import/otrs/user_factory.rb @@ -0,0 +1,7 @@ +module Import + module OTRS + module UserFactory + extend Import::Factory + end + end +end diff --git a/lib/import/transaction_factory.rb b/lib/import/transaction_factory.rb new file mode 100644 index 000000000..e35771a9a --- /dev/null +++ b/lib/import/transaction_factory.rb @@ -0,0 +1,18 @@ +module Import + module TransactionFactory + include Import::BaseFactory + + # rubocop:disable Style/ModuleFunction + extend self + + def import(records) + ActiveRecord::Base.transaction do + pre_import_hook(records) + records.each do |record| + next if skip?(record) + backend_class(record).new(record) + end + end + end + end +end diff --git a/lib/import/zendesk.rb b/lib/import/zendesk.rb index 4e9bc9ab6..815df8146 100644 --- a/lib/import/zendesk.rb +++ b/lib/import/zendesk.rb @@ -5,7 +5,8 @@ module Import end module Import::Zendesk - module_function + # rubocop:disable Style/ModuleFunction + extend self def start Rails.logger.info 'Start import...' @@ -998,8 +999,7 @@ module Import::Zendesk # reset primary key sequences def self._reset_pk(table) - return if ActiveRecord::Base.connection_config[:adapter] != 'postgresql' - ActiveRecord::Base.connection.reset_pk_sequence!(table) + DbHelper.import_post(table) end def get_custom_fields(custom_fields) diff --git a/public/assets/chat/chat.css b/public/assets/chat/chat.css index 774230bd4..451ca8b09 100644 --- a/public/assets/chat/chat.css +++ b/public/assets/chat/chat.css @@ -342,10 +342,10 @@ float: left; width: auto; height: auto; - min-height: 1.4em !important; max-height: 6em; + min-height: 1.4em !important; font-family: inherit; - line-height: inherit; + line-height: 1.4em; font-size: inherit; -webkit-appearance: none; -moz-appearance: none; @@ -353,6 +353,7 @@ border: none !important; background: none; box-shadow: none !important; + box-sizing: content-box; outline: none; resize: none; -webkit-flex: 1; diff --git a/public/assets/chat/chat.js b/public/assets/chat/chat.js index ef4ce3006..dd8812265 100644 --- a/public/assets/chat/chat.js +++ b/public/assets/chat/chat.js @@ -1,3 +1,152 @@ +/*! + * ---------------------------------------------------------------------------- + * "THE BEER-WARE LICENSE" (Revision 42): + * wrote this file. As long as you retain this notice you + * can do whatever you want with this stuff. If we meet some day, and you think + * this stuff is worth it, you can buy me a beer in return. Jevin O. Sewaruth + * ---------------------------------------------------------------------------- + * + * Autogrow Textarea Plugin Version v3.0 + * http://www.technoreply.com/autogrow-textarea-plugin-3-0 + * + * THIS PLUGIN IS DELIVERD ON A PAY WHAT YOU WHANT BASIS. IF THE PLUGIN WAS USEFUL TO YOU, PLEASE CONSIDER BUYING THE PLUGIN HERE : + * https://sites.fastspring.com/technoreply/instant/autogrowtextareaplugin + * + * Date: October 15, 2012 + * + * Zammad modification: remove overflow:hidden when maximum height is reached + * + */ + +jQuery.fn.autoGrow = function(options) { + return this.each(function() { + var settings = jQuery.extend({ + extraLine: true, + }, options); + + var createMirror = function(textarea) { + jQuery(textarea).after('
'); + return jQuery(textarea).next('.autogrow-textarea-mirror')[0]; + } + + var sendContentToMirror = function (textarea) { + mirror.innerHTML = String(textarea.value) + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(//g, '>') + .replace(/ /g, ' ') + .replace(/\n/g, '
') + + (settings.extraLine? '.
.' : '') + ; + + if (jQuery(textarea).height() != jQuery(mirror).height()) { + jQuery(textarea).height(jQuery(mirror).height()); + + var maxHeight = parseInt(jQuery(textarea).css('max-height'), 10); + var overflow = jQuery(mirror).height() > maxHeight ? '' : 'hidden' + jQuery(textarea).css('overflow', overflow); + } + } + + var growTextarea = function () { + sendContentToMirror(this); + } + + // Create a mirror + var mirror = createMirror(this); + + // Style the mirror + mirror.style.display = 'none'; + mirror.style.wordWrap = 'break-word'; + mirror.style.whiteSpace = 'normal'; + mirror.style.padding = jQuery(this).css('paddingTop') + ' ' + + jQuery(this).css('paddingRight') + ' ' + + jQuery(this).css('paddingBottom') + ' ' + + jQuery(this).css('paddingLeft'); + + mirror.style.width = jQuery(this).css('width'); + mirror.style.fontFamily = jQuery(this).css('font-family'); + mirror.style.fontSize = jQuery(this).css('font-size'); + mirror.style.lineHeight = jQuery(this).css('line-height'); + mirror.style.letterSpacing = jQuery(this).css('letter-spacing'); + mirror.style.boxSizing = jQuery(this).css('boxSizing'); + + // Style the textarea + this.style.overflow = "hidden"; + this.style.minHeight = this.rows+"em"; + + // Bind the textarea's event + this.onkeyup = growTextarea; + this.onfocus = growTextarea; + + // Fire the event for text already present + sendContentToMirror(this); + + }); +}; +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, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + }; + } + (function() { + (function() { + if (this.agent.avatar) { + __out.push('\n\n'); + } + + __out.push('\n\n '); + + __out.push(__sanitize(this.agent.name)); + + __out.push('\n'); + + }).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); }; }, 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; }, @@ -1275,152 +1424,6 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); return window.ZammadChat = ZammadChat; })(window.jQuery, window); -/*! - * ---------------------------------------------------------------------------- - * "THE BEER-WARE LICENSE" (Revision 42): - * wrote this file. As long as you retain this notice you - * can do whatever you want with this stuff. If we meet some day, and you think - * this stuff is worth it, you can buy me a beer in return. Jevin O. Sewaruth - * ---------------------------------------------------------------------------- - * - * Autogrow Textarea Plugin Version v3.0 - * http://www.technoreply.com/autogrow-textarea-plugin-3-0 - * - * THIS PLUGIN IS DELIVERD ON A PAY WHAT YOU WHANT BASIS. IF THE PLUGIN WAS USEFUL TO YOU, PLEASE CONSIDER BUYING THE PLUGIN HERE : - * https://sites.fastspring.com/technoreply/instant/autogrowtextareaplugin - * - * Date: October 15, 2012 - * - * Zammad modification: remove overflow:hidden when maximum height is reached - * - */ - -jQuery.fn.autoGrow = function(options) { - return this.each(function() { - var settings = jQuery.extend({ - extraLine: true, - }, options); - - var createMirror = function(textarea) { - jQuery(textarea).after('
'); - return jQuery(textarea).next('.autogrow-textarea-mirror')[0]; - } - - var sendContentToMirror = function (textarea) { - mirror.innerHTML = String(textarea.value) - .replace(/&/g, '&') - .replace(/"/g, '"') - .replace(/'/g, ''') - .replace(//g, '>') - .replace(/ /g, ' ') - .replace(/\n/g, '
') + - (settings.extraLine? '.
.' : '') - ; - - if (jQuery(textarea).height() != jQuery(mirror).height()) { - jQuery(textarea).height(jQuery(mirror).height()); - - var maxHeight = parseInt(jQuery(textarea).css('max-height'), 10); - var overflow = jQuery(mirror).height() > maxHeight ? '' : 'hidden' - jQuery(textarea).css('overflow', overflow); - } - } - - var growTextarea = function () { - sendContentToMirror(this); - } - - // Create a mirror - var mirror = createMirror(this); - - // Style the mirror - mirror.style.display = 'none'; - mirror.style.wordWrap = 'break-word'; - mirror.style.whiteSpace = 'normal'; - mirror.style.padding = jQuery(this).css('paddingTop') + ' ' + - jQuery(this).css('paddingRight') + ' ' + - jQuery(this).css('paddingBottom') + ' ' + - jQuery(this).css('paddingLeft'); - - mirror.style.width = jQuery(this).css('width'); - mirror.style.fontFamily = jQuery(this).css('font-family'); - mirror.style.fontSize = jQuery(this).css('font-size'); - mirror.style.lineHeight = jQuery(this).css('line-height'); - - // Style the textarea - this.style.overflow = "hidden"; - this.style.minHeight = this.rows+"em"; - - // Bind the textarea's event - this.onkeyup = growTextarea; - - // Fire the event for text already present - sendContentToMirror(this); - - }); -}; -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, '&') - .replace(//g, '>') - .replace(/"/g, '"'); - }; - } - (function() { - (function() { - if (this.agent.avatar) { - __out.push('\n\n'); - } - - __out.push('\n\n '); - - __out.push(__sanitize(this.agent.name)); - - __out.push('\n'); - - }).call(this); - - }).call(__obj); - __obj.safe = __objSafe, __obj.escape = __escape; - return __out.join(''); -}; - if (!window.zammadChatTemplates) { window.zammadChatTemplates = {}; } diff --git a/public/assets/chat/chat.min.js b/public/assets/chat/chat.min.js index 29cdcc3ec..9a099e7e6 100644 --- a/public/assets/chat/chat.min.js +++ b/public/assets/chat/chat.min.js @@ -1,2 +1,2 @@ -var bind=function(t,e){return function(){return t.apply(e,arguments)}},slice=[].slice,extend=function(t,e){function s(){this.constructor=t}for(var i in e)hasProp.call(e,i)&&(t[i]=e[i]);return s.prototype=e.prototype,t.prototype=new s,t.__super__=e.prototype,t},hasProp={}.hasOwnProperty;!function(t,e){var s,i,n,o,a,r,h,l;return l=document.getElementsByTagName("script"),r=l[l.length-1],h=r.src.match(".*://([^:/]*).*")[1],s=function(){function e(e){this.options=t.extend({},this.defaults,e),this.log=new n({debug:this.options.debug,logPrefix:this.options.logPrefix||this.logPrefix})}return e.prototype.defaults={debug:!1},e}(),n=function(){function e(e){this.log=bind(this.log,this),this.error=bind(this.error,this),this.notice=bind(this.notice,this),this.debug=bind(this.debug,this),this.options=t.extend({},this.defaults,e)}return e.prototype.defaults={debug:!1},e.prototype.debug=function(){var t;return t=1<=arguments.length?slice.call(arguments,0):[],this.options.debug?this.log("debug",t):void 0},e.prototype.notice=function(){var t;return t=1<=arguments.length?slice.call(arguments,0):[],this.log("notice",t)},e.prototype.error=function(){var t;return t=1<=arguments.length?slice.call(arguments,0):[],this.log("error",t)},e.prototype.log=function(e,s){var i,n,o,a;if(s.unshift("||"),s.unshift(e),s.unshift(this.options.logPrefix),console.log.apply(console,s),this.options.debug){for(a="",i=0,o=s.length;o>i;i++)n=s[i],a+=" ",a+="object"==typeof n?JSON.stringify(n):n&&n.toString?n.toString():n;return t(".js-chatLogDisplay").prepend("
"+a+"
")}},e}(),o=function(t){function e(t){this.stop=bind(this.stop,this),this.start=bind(this.start,this),e.__super__.constructor.call(this,t)}return extend(e,t),e.prototype.timeoutStartedAt=null,e.prototype.logPrefix="timeout",e.prototype.defaults={debug:!1,timeout:4,timeoutIntervallCheck:.5},e.prototype.start=function(){var t,e;return this.stop(),e=new Date,t=function(t){return function(){var s;return s=new Date-new Date(e.getTime()+1e3*t.options.timeout*60),t.log.debug("Timeout check for "+t.options.timeout+" minutes (left "+s/1e3+" sec.)"),0>s?void 0:(t.stop(),t.options.callback())}}(this),this.log.debug("Start timeout in "+this.options.timeout+" minutes"),this.intervallId=setInterval(t,1e3*this.options.timeoutIntervallCheck*60)},e.prototype.stop=function(){return this.intervallId?(this.log.debug("Stop timeout of "+this.options.timeout+" minutes"),clearInterval(this.intervallId)):void 0},e}(s),i=function(t){function s(t){this.ping=bind(this.ping,this),this.send=bind(this.send,this),this.reconnect=bind(this.reconnect,this),this.close=bind(this.close,this),this.connect=bind(this.connect,this),this.set=bind(this.set,this),s.__super__.constructor.call(this,t)}return extend(s,t),s.prototype.logPrefix="io",s.prototype.set=function(t){var e,s,i;s=[];for(e in t)i=t[e],s.push(this.options[e]=i);return s},s.prototype.connect=function(){return this.log.debug("Connecting to "+this.options.host),this.ws=new e.WebSocket(""+this.options.host),this.ws.onopen=function(t){return function(e){return t.log.debug("onOpen",e),t.options.onOpen(e),t.ping()}}(this),this.ws.onmessage=function(t){return function(e){var s,i,n,o;for(o=JSON.parse(e.data),t.log.debug("onMessage",e.data),s=0,i=o.length;i>s;s++)n=o[s],"pong"===n.event&&t.ping();return t.options.onMessage?t.options.onMessage(o):void 0}}(this),this.ws.onclose=function(t){return function(e){if(t.log.debug("close websocket connection",e),t.pingDelayId&&clearTimeout(t.pingDelayId),t.manualClose){if(t.log.debug("manual close, onClose callback"),t.manualClose=!1,t.options.onClose)return t.options.onClose(e)}else if(t.log.debug("error close, onError callback"),t.options.onError)return t.options.onError("Connection lost...")}}(this),this.ws.onerror=function(t){return function(e){return t.log.debug("onError",e),t.options.onError?t.options.onError(e):void 0}}(this)},s.prototype.close=function(){return this.log.debug("close websocket manually"),this.manualClose=!0,this.ws.close()},s.prototype.reconnect=function(){return this.log.debug("reconnect"),this.close(),this.connect()},s.prototype.send=function(t,e){var s;return null==e&&(e={}),this.log.debug("send",t,e),s=JSON.stringify({event:t,data:e}),this.ws.send(s)},s.prototype.ping=function(){var t;return t=function(t){return function(){return t.send("ping")}}(this),this.pingDelayId=setTimeout(t,29e3)},s}(s),a=function(s){function n(s){return this.startTimeoutObservers=bind(this.startTimeoutObservers,this),this.onCssLoaded=bind(this.onCssLoaded,this),this.setAgentOnlineState=bind(this.setAgentOnlineState,this),this.onConnectionEstablished=bind(this.onConnectionEstablished,this),this.setSessionId=bind(this.setSessionId,this),this.onConnectionReestablished=bind(this.onConnectionReestablished,this),this.reconnect=bind(this.reconnect,this),this.destroy=bind(this.destroy,this),this.onScrollHintClick=bind(this.onScrollHintClick,this),this.detectScrolledtoBottom=bind(this.detectScrolledtoBottom,this),this.onLeaveTemporary=bind(this.onLeaveTemporary,this),this.onAgentTypingEnd=bind(this.onAgentTypingEnd,this),this.onAgentTypingStart=bind(this.onAgentTypingStart,this),this.onQueue=bind(this.onQueue,this),this.onQueueScreen=bind(this.onQueueScreen,this),this.onWebSocketClose=bind(this.onWebSocketClose,this),this.onCloseAnimationEnd=bind(this.onCloseAnimationEnd,this),this.close=bind(this.close,this),this.toggle=bind(this.toggle,this),this.sessionClose=bind(this.sessionClose,this),this.onOpenAnimationEnd=bind(this.onOpenAnimationEnd,this),this.open=bind(this.open,this),this.renderMessage=bind(this.renderMessage,this),this.receiveMessage=bind(this.receiveMessage,this),this.onSubmit=bind(this.onSubmit,this),this.onFocus=bind(this.onFocus,this),this.onInput=bind(this.onInput,this),this.onReopenSession=bind(this.onReopenSession,this),this.onError=bind(this.onError,this),this.onWebSocketMessage=bind(this.onWebSocketMessage,this),this.send=bind(this.send,this),this.checkForEnter=bind(this.checkForEnter,this),this.render=bind(this.render,this),this.view=bind(this.view,this),this.T=bind(this.T,this),this.options=t.extend({},this.defaults,s),n.__super__.constructor.call(this,this.options),this.isFullscreen=e.matchMedia&&e.matchMedia("(max-width: 768px)").matches,this.scrollRoot=t(this.getScrollRoot()),t?e.WebSocket&&sessionStorage?this.options.chatId?(this.options.lang||(this.options.lang=t("html").attr("lang")),this.options.lang&&(this.options.lang=this.options.lang.replace(/-.+?$/,""),this.log.debug("lang: "+this.options.lang)),this.options.host||this.detectHost(),this.loadCss(),this.io=new i(this.options),this.io.set({onOpen:this.render,onClose:this.onWebSocketClose,onMessage:this.onWebSocketMessage,onError:this.onError}),void this.io.connect()):(this.state="unsupported",void this.log.error("Chat: need chatId as option!")):(this.state="unsupported",void this.log.notice("Chat: Browser not supported!")):(this.state="unsupported",void this.log.notice("Chat: no jquery found!"))}return extend(n,s),n.prototype.defaults={chatId:void 0,show:!0,target:t("body"),host:"",debug:!1,flat:!1,lang:void 0,cssAutoload:!0,cssUrl:void 0,fontSize:void 0,buttonClass:"open-zammad-chat",inactiveClass:"is-inactive",title:"Chat with us!",scrollHint:"Scrolle nach unten um neue Nachrichten zu sehen",idleTimeout:6,idleTimeoutIntervallCheck:.5,inactiveTimeout:8,inactiveTimeoutIntervallCheck:.5,waitingListTimeout:4,waitingListTimeoutIntervallCheck:.5},n.prototype.logPrefix="chat",n.prototype._messageCount=0,n.prototype.isOpen=!1,n.prototype.blinkOnlineInterval=null,n.prototype.stopBlinOnlineStateTimeout=null,n.prototype.showTimeEveryXMinutes=2,n.prototype.lastTimestamp=null,n.prototype.lastAddedType=null,n.prototype.inputTimeout=null,n.prototype.isTyping=!1,n.prototype.state="offline",n.prototype.initialQueueDelay=1e4,n.prototype.translations={de:{"Chat with us!":"Chatte mit uns!","Scroll down to see new messages":"Scrolle nach unten um neue Nachrichten zu sehen",Online:"Online",Online:"Online",Offline:"Offline",Connecting:"Verbinden","Connection re-established":"Verbindung wiederhergestellt",Today:"Heute",Send:"Senden","Compose your message...":"Ihre Nachricht...","All colleagues are busy.":"Alle Kollegen sind belegt.","You are on waiting list position %s.":"Sie sind in der Warteliste an der Position %s.","Start new conversation":"Neue Konversation starten","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation mit %s geschlossen.","Since you didn't respond in the last %s minutes your conversation got closed.":"Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation geschlossen."}},n.prototype.sessionId=void 0,n.prototype.scrolledToBottom=!0,n.prototype.scrollSnapTolerance=10,n.prototype.T=function(){var t,e,s,i,n,o;if(n=arguments[0],s=2<=arguments.length?slice.call(arguments,1):[],this.options.lang&&"en"!==this.options.lang&&(this.translations[this.options.lang]?(o=this.translations[this.options.lang],o[n]||this.log.notice("Translation needed for '"+n+"'"),n=o[n]||n):this.log.notice("Translation '"+this.options.lang+"' needed!")),s)for(t=0,i=s.length;i>t;t++)e=s[t],n=n.replace(/%s/,e);return n},n.prototype.view=function(t){return function(s){return function(i){return i||(i={}),i.T=s.T,i.background=s.options.background,i.flat=s.options.flat,i.fontSize=s.options.fontSize,e.zammadChatTemplates[t](i)}}(this)},n.prototype.getScrollRoot=function(){var t,e,s;return"scrollingElement"in document?document.scrollingElement:(e=document.documentElement,s=e.scrollTop,e.scrollTop=s+1,t=e.scrollTop,e.scrollTop=s,t>s?e:document.body)},n.prototype.render=function(){return this.el&&t(".zammad-chat").get(0)||this.renderBase(),t("."+this.options.buttonClass).addClass(this.inactiveClass),this.setAgentOnlineState("online"),this.log.debug("widget rendered"),this.startTimeoutObservers(),this.idleTimeout.start(),this.sessionId=sessionStorage.getItem("sessionId"),this.send("chat_status_customer",{session_id:this.sessionId,url:e.location.href})},n.prototype.renderBase=function(){return this.el=t(this.view("chat")({title:this.options.title,scrollHint:this.options.scrollHint})),this.options.target.append(this.el),this.input=this.el.find(".zammad-chat-input"),this.el.find(".js-chat-open").click(this.open),this.el.find(".js-chat-toggle").click(this.toggle),this.el.find(".zammad-chat-controls").on("submit",this.onSubmit),this.el.find(".zammad-chat-body").on("scroll",this.detectScrolledtoBottom),this.el.find(".zammad-scroll-hint").click(this.onScrollHintClick),this.input.on({keydown:this.checkForEnter,input:this.onInput}),t(e).on("beforeunload",function(t){return function(){return t.onLeaveTemporary()}}(this)),t(e).bind("hashchange",function(t){return function(){return t.isOpen?void(t.sessionId&&t.send("chat_session_notice",{session_id:t.sessionId,message:e.location.href})):t.idleTimeout.start()}}(this)),this.isFullscreen?this.input.on({focus:this.onFocus,focusout:this.onFocusOut}):void 0},n.prototype.checkForEnter=function(t){return t.shiftKey||13!==t.keyCode?void 0:(t.preventDefault(),this.sendMessage())},n.prototype.send=function(t,e){return null==e&&(e={}),e.chat_id=this.options.chatId,this.io.send(t,e)},n.prototype.onWebSocketMessage=function(t){var e,s,i;for(e=0,s=t.length;s>e;e++)switch(i=t[e],this.log.debug("ws:onmessage",i),i.event){case"chat_error":this.log.notice(i.data),i.data&&"chat_disabled"===i.data.state&&this.destroy({remove:!0});break;case"chat_session_message":if(i.data.self_written)return;this.receiveMessage(i.data);break;case"chat_session_typing":if(i.data.self_written)return;this.onAgentTypingStart();break;case"chat_session_start":this.onConnectionEstablished(i.data);break;case"chat_session_queue":this.onQueueScreen(i.data);break;case"chat_session_closed":this.onSessionClosed(i.data);break;case"chat_session_left":this.onSessionClosed(i.data);break;case"chat_status_customer":switch(i.data.state){case"online":this.sessionId=void 0,!this.options.cssAutoload||this.cssLoaded?this.onReady():this.socketReady=!0;break;case"offline":this.onError("Zammad Chat: No agent online");break;case"chat_disabled":this.onError("Zammad Chat: Chat is disabled");break;case"no_seats_available":this.onError("Zammad Chat: Too many clients in queue. Clients in queue: "+i.data.queue);break;case"reconnect":this.onReopenSession(i.data)}}},n.prototype.onReady=function(){return this.log.debug("widget ready for use"),t("."+this.options.buttonClass).click(this.open).removeClass(this.inactiveClass),this.options.show?this.show():void 0},n.prototype.onError=function(e){return this.log.debug(e),this.addStatus(e),t("."+this.options.buttonClass).hide(),this.isOpen?(this.disableInput(),this.destroy({remove:!1})):this.destroy({remove:!0})},n.prototype.onReopenSession=function(t){var e,s,i,n,o;if(this.log.debug("old messages",t.session),this.inactiveTimeout.start(),o=sessionStorage.getItem("unfinished_message"),t.agent){for(this.onConnectionEstablished(t),n=t.session,e=0,s=n.length;s>e;e++)i=n[e],this.renderMessage({message:i.content,id:i.id,from:i.created_by_id?"agent":"customer"});o&&this.input.val(o)}return t.position&&this.onQueue(t),this.show(),this.open(),this.scrollToBottom(),o?this.input.focus():void 0},n.prototype.onInput=function(){return this.el.find(".zammad-chat-message--unread").removeClass("zammad-chat-message--unread"),sessionStorage.setItem("unfinished_message",this.input.val()),this.onTyping()},n.prototype.onFocus=function(){var s;return t(e).scrollTop(10),s=t(e).scrollTop()>0,t(e).scrollTop(0),s?this.log.notice("virtual keyboard shown"):void 0},n.prototype.onFocusOut=function(){},n.prototype.onTyping=function(){return this.isTyping&&this.isTyping>new Date((new Date).getTime()-1500)?void 0:(this.isTyping=new Date,this.send("chat_session_typing",{session_id:this.sessionId}),this.inactiveTimeout.start())},n.prototype.onSubmit=function(t){return t.preventDefault(),this.sendMessage()},n.prototype.sendMessage=function(){var t,e;return(t=this.input.val())?(this.inactiveTimeout.start(),sessionStorage.removeItem("unfinished_message"),e=this.view("message")({message:t,from:"customer",id:this._messageCount++,unreadClass:""}),this.maybeAddTimestamp(),this.el.find(".zammad-chat-message--typing").size()?(this.lastAddedType="typing-placeholder",this.el.find(".zammad-chat-message--typing").before(e)):(this.lastAddedType="message--customer",this.el.find(".zammad-chat-body").append(e)),this.input.val(""),this.scrollToBottom(),this.send("chat_session_message",{content:t,id:this._messageCount,session_id:this.sessionId})):void 0},n.prototype.receiveMessage=function(t){return this.inactiveTimeout.start(),this.onAgentTypingEnd(),this.maybeAddTimestamp(),this.renderMessage({message:t.message.content,id:t.id,from:"agent"}),this.scrollToBottom({showHint:!0})},n.prototype.renderMessage=function(t){return this.lastAddedType="message--"+t.from,t.unreadClass=document.hidden?" zammad-chat-message--unread":"",this.el.find(".zammad-chat-body").append(this.view("message")(t))},n.prototype.open=function(){var t;return this.isOpen?void this.log.debug("widget already open, block"):(this.isOpen=!0,this.log.debug("open widget"),this.sessionId||this.showLoader(),this.el.addClass("zammad-chat-is-open"),this.inputInitialized||(this.inputInitialized=!0,this.input.autoGrow({extraLine:!1})),t=this.el.height()-this.el.find(".zammad-chat-header").outerHeight(),this.el.css("bottom",-t),this.sessionId?(this.el.css("bottom",0),this.onOpenAnimationEnd()):(this.el.animate({bottom:0},500,this.onOpenAnimationEnd),this.send("chat_session_init",{url:e.location.href})))},n.prototype.onOpenAnimationEnd=function(){return this.idleTimeout.stop(),this.isFullscreen?this.disableScrollOnRoot():void 0},n.prototype.sessionClose=function(){return this.send("chat_session_close",{session_id:this.sessionId}),this.inactiveTimeout.stop(),this.waitingListTimeout.stop(),sessionStorage.removeItem("unfinished_message"),this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),this.setSessionId(void 0)},n.prototype.toggle=function(t){return this.isOpen?this.close(t):this.open(t)},n.prototype.close=function(t){var e;return this.isOpen?(this.initDelayId&&clearTimeout(this.initDelayId),this.sessionId?(this.log.debug("close widget"),t&&t.stopPropagation(),this.sessionClose(),this.isFullscreen&&this.enableScrollOnRoot(),e=this.el.height()-this.el.find(".zammad-chat-header").outerHeight(),this.el.animate({bottom:-e},500,this.onCloseAnimationEnd)):void this.log.debug("can't close widget without sessionId")):void this.log.debug("can't close widget, it's not open")},n.prototype.onCloseAnimationEnd=function(){return this.el.css("bottom",""),this.el.removeClass("zammad-chat-is-open"),this.showLoader(),this.el.find(".zammad-chat-welcome").removeClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent").addClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent-status").addClass("zammad-chat-is-hidden"),this.isOpen=!1,this.io.reconnect()},n.prototype.onWebSocketClose=function(){return this.isOpen?void 0:this.el?(this.el.removeClass("zammad-chat-is-shown"),this.el.removeClass("zammad-chat-is-loaded")):void 0},n.prototype.show=function(){return"offline"!==this.state?(this.el.addClass("zammad-chat-is-loaded"),this.el.addClass("zammad-chat-is-shown")):void 0},n.prototype.disableInput=function(){return this.input.prop("disabled",!0),this.el.find(".zammad-chat-send").prop("disabled",!0)},n.prototype.enableInput=function(){return this.input.prop("disabled",!1),this.el.find(".zammad-chat-send").prop("disabled",!1)},n.prototype.hideModal=function(){return this.el.find(".zammad-chat-modal").html("")},n.prototype.onQueueScreen=function(t){var e;return this.setSessionId(t.session_id),e=function(e){return function(){return e.onQueue(t),e.waitingListTimeout.start()}}(this),this.initialQueueDelay&&!this.onInitialQueueDelayId?void(this.onInitialQueueDelayId=setTimeout(e,this.initialQueueDelay)):(this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),e())},n.prototype.onQueue=function(t){return this.log.notice("onQueue",t.position),this.inQueue=!0,this.el.find(".zammad-chat-modal").html(this.view("waiting")({position:t.position}))},n.prototype.onAgentTypingStart=function(){return this.stopTypingId&&clearTimeout(this.stopTypingId),this.stopTypingId=setTimeout(this.onAgentTypingEnd,3e3),!this.el.find(".zammad-chat-message--typing").size()&&(this.maybeAddTimestamp(),this.el.find(".zammad-chat-body").append(this.view("typingIndicator")()),this.isVisible(this.el.find(".zammad-chat-message--typing"),!0))?this.scrollToBottom():void 0},n.prototype.onAgentTypingEnd=function(){return this.el.find(".zammad-chat-message--typing").remove()},n.prototype.onLeaveTemporary=function(){return this.sessionId?this.send("chat_session_leave_temporary",{session_id:this.sessionId}):void 0},n.prototype.maybeAddTimestamp=function(){var t,e,s;return s=Date.now(),!this.lastTimestamp||s-this.lastTimestamp>6e4*this.showTimeEveryXMinutes?(t=this.T("Today"),e=(new Date).toTimeString().substr(0,5),"timestamp"===this.lastAddedType?(this.updateLastTimestamp(t,e),this.lastTimestamp=s):(this.el.find(".zammad-chat-body").append(this.view("timestamp")({label:t,time:e})),this.lastTimestamp=s,this.lastAddedType="timestamp",this.scrollToBottom())):void 0},n.prototype.updateLastTimestamp=function(t,e){return this.el?this.el.find(".zammad-chat-body").find(".zammad-chat-timestamp").last().replaceWith(this.view("timestamp")({label:t,time:e})):void 0},n.prototype.addStatus=function(t){return this.el?(this.maybeAddTimestamp(),this.el.find(".zammad-chat-body").append(this.view("status")({status:t})),this.scrollToBottom()):void 0},n.prototype.detectScrolledtoBottom=function(){var t;return t=this.el.find(".zammad-chat-body").scrollTop()+this.el.find(".zammad-chat-body").outerHeight(),this.scrolledToBottom=Math.abs(t-this.el.find(".zammad-chat-body").prop("scrollHeight"))<=this.scrollSnapTolerance,this.scrolledToBottom?this.el.find(".zammad-scroll-hint").addClass("is-hidden"):void 0},n.prototype.showScrollHint=function(){return this.el.find(".zammad-scroll-hint").removeClass("is-hidden"),this.el.find(".zammad-chat-body").scrollTop(this.el.find(".zammad-chat-body").scrollTop()+this.el.find(".zammad-scroll-hint").outerHeight())},n.prototype.onScrollHintClick=function(){return this.el.find(".zammad-chat-body").animate({scrollTop:this.el.find(".zammad-chat-body").prop("scrollHeight")},300)},n.prototype.scrollToBottom=function(e){var s;return s=(null!=e?e:{showHint:!1}).showHint,this.scrolledToBottom?this.el.find(".zammad-chat-body").scrollTop(t(".zammad-chat-body").prop("scrollHeight")):s?this.showScrollHint():void 0},n.prototype.destroy=function(t){return null==t&&(t={}),this.log.debug("destroy widget",t),this.setAgentOnlineState("offline"),t.remove&&this.el&&this.el.remove(),this.waitingListTimeout&&this.waitingListTimeout.stop(),this.inactiveTimeout&&this.inactiveTimeout.stop(),this.idleTimeout&&this.idleTimeout.stop(),this.io.close()},n.prototype.reconnect=function(){return this.log.notice("reconnecting"),this.disableInput(),this.lastAddedType="status",this.setAgentOnlineState("connecting"),this.addStatus(this.T("Connection lost"))},n.prototype.onConnectionReestablished=function(){return this.lastAddedType="status",this.setAgentOnlineState("online"),this.addStatus(this.T("Connection re-established"))},n.prototype.onSessionClosed=function(t){return this.addStatus(this.T("Chat closed by %s",t.realname)),this.disableInput(),this.setAgentOnlineState("offline"),this.inactiveTimeout.stop()},n.prototype.setSessionId=function(t){return this.sessionId=t,void 0===t?sessionStorage.removeItem("sessionId"):sessionStorage.setItem("sessionId",t)},n.prototype.onConnectionEstablished=function(t){return this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),this.inQueue=!1,t.agent&&(this.agent=t.agent),t.session_id&&this.setSessionId(t.session_id),this.el.find(".zammad-chat-body").html(""),this.el.find(".zammad-chat-agent").html(this.view("agent")({agent:this.agent})),this.enableInput(),this.hideModal(),this.el.find(".zammad-chat-welcome").addClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent").removeClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent-status").removeClass("zammad-chat-is-hidden"),this.isFullscreen||this.input.focus(),this.setAgentOnlineState("online"),this.waitingListTimeout.stop(),this.idleTimeout.stop(),this.inactiveTimeout.start()},n.prototype.showCustomerTimeout=function(){var t;return this.el.find(".zammad-chat-modal").html(this.view("customer_timeout")({agent:this.agent.name,delay:this.options.inactiveTimeout})),t=function(){return location.reload()},this.el.find(".js-restart").click(t),this.sessionClose()},n.prototype.showWaitingListTimeout=function(){var t;return this.el.find(".zammad-chat-modal").html(this.view("waiting_list_timeout")({delay:this.options.watingListTimeout})),t=function(){return location.reload()},this.el.find(".js-restart").click(t),this.sessionClose()},n.prototype.showLoader=function(){return this.el.find(".zammad-chat-modal").html(this.view("loader")())},n.prototype.setAgentOnlineState=function(t){var e;return this.state=t,this.el?(e=t.charAt(0).toUpperCase()+t.slice(1),this.el.find(".zammad-chat-agent-status").attr("data-status",t).text(this.T(e))):void 0},n.prototype.detectHost=function(){var t;return t="ws://","https:"===e.location.protocol&&(t="wss://"),this.options.host=""+t+h+"/ws"},n.prototype.loadCss=function(){var t,e,s;if(this.options.cssAutoload)return s=this.options.cssUrl,s||(s=this.options.host.replace(/^wss/i,"https").replace(/^ws/i,"http").replace(/\/ws/i,""),s+="/assets/chat/chat.css"),this.log.debug("load css from '"+s+"'"),e="@import url('"+s+"');",t=document.createElement("link"),t.onload=this.onCssLoaded,t.rel="stylesheet",t.href="data:text/css,"+escape(e),document.getElementsByTagName("head")[0].appendChild(t)},n.prototype.onCssLoaded=function(){return this.socketReady?this.onReady():this.cssLoaded=!0},n.prototype.startTimeoutObservers=function(){return this.idleTimeout=new o({logPrefix:"idleTimeout",debug:this.options.debug,timeout:this.options.idleTimeout,timeoutIntervallCheck:this.options.idleTimeoutIntervallCheck,callback:function(t){return function(){return t.log.debug("Idle timeout reached, hide widget",new Date),t.destroy({remove:!0})}}(this)}),this.inactiveTimeout=new o({logPrefix:"inactiveTimeout",debug:this.options.debug,timeout:this.options.inactiveTimeout,timeoutIntervallCheck:this.options.inactiveTimeoutIntervallCheck,callback:function(t){return function(){return t.log.debug("Inactive timeout reached, show timeout screen.",new Date),t.showCustomerTimeout(),t.destroy({remove:!1})}}(this)}),this.waitingListTimeout=new o({logPrefix:"waitingListTimeout",debug:this.options.debug,timeout:this.options.waitingListTimeout,timeoutIntervallCheck:this.options.waitingListTimeoutIntervallCheck,callback:function(t){return function(){return t.log.debug("Waiting list timeout reached, show timeout screen.",new Date),t.showWaitingListTimeout(),t.destroy({remove:!1})}}(this)})},n.prototype.disableScrollOnRoot=function(){return this.rootScrollOffset=this.scrollRoot.scrollTop(),this.scrollRoot.css({overflow:"hidden",position:"fixed"})},n.prototype.enableScrollOnRoot=function(){return this.scrollRoot.scrollTop(this.rootScrollOffset),this.scrollRoot.css({overflow:"",position:""})},n.prototype.isVisible=function(s,i,n,o){var a,r,h,l,d,c,u,p,m,g,f,y,v,b,w,T,z,C,S,I,k,_,O,A,E,L;if(!(s.length<1))if(r=t(e),a=s.length>1?s.eq(0):s,C=a.get(0),L=r.width(),E=r.height(),o=o?o:"both",p=n===!0?C.offsetWidth*C.offsetHeight:!0,"function"==typeof C.getBoundingClientRect){if(z=C.getBoundingClientRect(),S=z.top>=0&&z.top0&&z.bottom<=E,b=z.left>=0&&z.left0&&z.right<=L,I=i?S||u:S&&u,v=i?b||T:b&&T,"both"===o)return p&&I&&v;if("vertical"===o)return p&&I;if("horizontal"===o)return p&&v}else{if(A=r.scrollTop(),k=A+E,_=r.scrollLeft(),O=_+L,w=a.offset(),c=w.top,h=c+a.height(),l=w.left,d=l+a.width(),y=i===!0?h:c,m=i===!0?c:h,g=i===!0?d:l,f=i===!0?l:d,"both"===o)return!!p&&k>=m&&y>=A&&O>=f&&g>=_;if("vertical"===o)return!!p&&k>=m&&y>=A;if("horizontal"===o)return!!p&&O>=f&&g>=_}},n}(s),e.ZammadChat=a}(window.jQuery,window),jQuery.fn.autoGrow=function(t){return this.each(function(){var e=jQuery.extend({extraLine:!0},t),s=function(t){return jQuery(t).after('
'),jQuery(t).next(".autogrow-textarea-mirror")[0]},i=function(t){if(o.innerHTML=String(t.value).replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">").replace(/ /g," ").replace(/\n/g,"
")+(e.extraLine?".
.":""),jQuery(t).height()!=jQuery(o).height()){jQuery(t).height(jQuery(o).height());var s=parseInt(jQuery(t).css("max-height"),10),i=jQuery(o).height()>s?"":"hidden";jQuery(t).css("overflow",i)}},n=function(){i(this)},o=s(this);o.style.display="none",o.style.wordWrap="break-word",o.style.whiteSpace="normal",o.style.padding=jQuery(this).css("paddingTop")+" "+jQuery(this).css("paddingRight")+" "+jQuery(this).css("paddingBottom")+" "+jQuery(this).css("paddingLeft"),o.style.width=jQuery(this).css("width"),o.style.fontFamily=jQuery(this).css("font-family"),o.style.fontSize=jQuery(this).css("font-size"),o.style.lineHeight=jQuery(this).css("line-height"),this.style.overflow="hidden",this.style.minHeight=this.rows+"em",this.onkeyup=n,i(this)})},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.agent=function(t){t||(t={});var e,s=[],i=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},n=t.safe,o=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){this.agent.avatar&&(s.push('\n\n')),s.push('\n\n '),s.push(i(this.agent.name)),s.push("\n")}).call(this)}.call(t),t.safe=n,t.escape=o,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.chat=function(t){t||(t={});var e,s=[],i=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},n=t.safe,o=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
\n
\n
\n \n \n \n \n \n
\n
\n
\n
\n \n '),s.push(this.T(this.title)),s.push('\n
\n
\n
\n \n
\n
\n \n \n
\n
")}).call(this)}.call(t),t.safe=n,t.escape=o,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.customer_timeout=function(t){t||(t={});var e,s=[],i=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},n=t.safe,o=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
\n '), -this.agent?(s.push("\n "),s.push(this.T("Since you didn't respond in the last %s minutes your conversation with %s got closed.",this.delay,this.agent)),s.push("\n ")):(s.push("\n "),s.push(this.T("Since you didn't respond in the last %s minutes your conversation got closed.",this.delay)),s.push("\n ")),s.push('\n
\n
"),s.push(this.T("Start new conversation")),s.push("
\n
")}).call(this)}.call(t),t.safe=n,t.escape=o,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.loader=function(t){t||(t={});var e,s=[],i=t.safe,n=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},n||(n=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('\n \n \n \n\n'),s.push(this.T("Connecting")),s.push("")}).call(this)}.call(t),t.safe=i,t.escape=n,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.message=function(t){t||(t={});var e,s=[],i=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},n=t.safe,o=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
\n "),s.push(this.message),s.push("\n
")}).call(this)}.call(t),t.safe=n,t.escape=o,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.status=function(t){t||(t={});var e,s=[],i=t.safe,n=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},n||(n=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
\n
\n '),s.push(this.status),s.push("\n
\n
")}).call(this)}.call(t),t.safe=i,t.escape=n,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.timestamp=function(t){t||(t={});var e,s=[],i=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},n=t.safe,o=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
'),s.push(i(this.label)),s.push(" "),s.push(i(this.time)),s.push("
")}).call(this)}.call(t),t.safe=n,t.escape=o,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.typingIndicator=function(t){t||(t={});var e,s=[],i=t.safe,n=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},n||(n=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
\n \n \n \n \n \n \n \n
')}).call(this)}.call(t),t.safe=i,t.escape=n,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting=function(t){t||(t={});var e,s=[],i=t.safe,n=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},n||(n=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
\n \n \n \n \n \n '),s.push(this.T("All colleagues are busy.")),s.push("
\n "),s.push(this.T("You are on waiting list position %s.",this.position)),s.push("\n
")}).call(this)}.call(t),t.safe=i,t.escape=n,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting_list_timeout=function(t){t||(t={});var e,s=[],i=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},n=t.safe,o=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
\n '),s.push(this.T("We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!")),s.push('\n
\n
"),s.push(this.T("Start new conversation")),s.push("
\n
")}).call(this)}.call(t),t.safe=n,t.escape=o,s.join("")}; \ No newline at end of file +jQuery.fn.autoGrow=function(t){return this.each(function(){var e=jQuery.extend({extraLine:!0},t),s=function(t){return jQuery(t).after('
'),jQuery(t).next(".autogrow-textarea-mirror")[0]},i=function(t){if(o.innerHTML=String(t.value).replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">").replace(/ /g," ").replace(/\n/g,"
")+(e.extraLine?".
.":""),jQuery(t).height()!=jQuery(o).height()){jQuery(t).height(jQuery(o).height());var s=parseInt(jQuery(t).css("max-height"),10),i=jQuery(o).height()>s?"":"hidden";jQuery(t).css("overflow",i)}},n=function(){i(this)},o=s(this);o.style.display="none",o.style.wordWrap="break-word",o.style.whiteSpace="normal",o.style.padding=jQuery(this).css("paddingTop")+" "+jQuery(this).css("paddingRight")+" "+jQuery(this).css("paddingBottom")+" "+jQuery(this).css("paddingLeft"),o.style.width=jQuery(this).css("width"),o.style.fontFamily=jQuery(this).css("font-family"),o.style.fontSize=jQuery(this).css("font-size"),o.style.lineHeight=jQuery(this).css("line-height"),o.style.letterSpacing=jQuery(this).css("letter-spacing"),o.style.boxSizing=jQuery(this).css("boxSizing"),this.style.overflow="hidden",this.style.minHeight=this.rows+"em",this.onkeyup=n,this.onfocus=n,i(this)})},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.agent=function(t){t||(t={});var e,s=[],i=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},n=t.safe,o=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){this.agent.avatar&&(s.push('\n\n')),s.push('\n\n '),s.push(i(this.agent.name)),s.push("\n")}).call(this)}.call(t),t.safe=n,t.escape=o,s.join("")};var bind=function(t,e){return function(){return t.apply(e,arguments)}},slice=[].slice,extend=function(t,e){function s(){this.constructor=t}for(var i in e)hasProp.call(e,i)&&(t[i]=e[i]);return s.prototype=e.prototype,t.prototype=new s,t.__super__=e.prototype,t},hasProp={}.hasOwnProperty;!function(t,e){var s,i,n,o,a,r,h,l;return l=document.getElementsByTagName("script"),r=l[l.length-1],h=r.src.match(".*://([^:/]*).*")[1],s=function(){function e(e){this.options=t.extend({},this.defaults,e),this.log=new n({debug:this.options.debug,logPrefix:this.options.logPrefix||this.logPrefix})}return e.prototype.defaults={debug:!1},e}(),n=function(){function e(e){this.log=bind(this.log,this),this.error=bind(this.error,this),this.notice=bind(this.notice,this),this.debug=bind(this.debug,this),this.options=t.extend({},this.defaults,e)}return e.prototype.defaults={debug:!1},e.prototype.debug=function(){var t;return t=1<=arguments.length?slice.call(arguments,0):[],this.options.debug?this.log("debug",t):void 0},e.prototype.notice=function(){var t;return t=1<=arguments.length?slice.call(arguments,0):[],this.log("notice",t)},e.prototype.error=function(){var t;return t=1<=arguments.length?slice.call(arguments,0):[],this.log("error",t)},e.prototype.log=function(e,s){var i,n,o,a;if(s.unshift("||"),s.unshift(e),s.unshift(this.options.logPrefix),console.log.apply(console,s),this.options.debug){for(a="",i=0,o=s.length;o>i;i++)n=s[i],a+=" ",a+="object"==typeof n?JSON.stringify(n):n&&n.toString?n.toString():n;return t(".js-chatLogDisplay").prepend("
"+a+"
")}},e}(),o=function(t){function e(t){this.stop=bind(this.stop,this),this.start=bind(this.start,this),e.__super__.constructor.call(this,t)}return extend(e,t),e.prototype.timeoutStartedAt=null,e.prototype.logPrefix="timeout",e.prototype.defaults={debug:!1,timeout:4,timeoutIntervallCheck:.5},e.prototype.start=function(){var t,e;return this.stop(),e=new Date,t=function(t){return function(){var s;return s=new Date-new Date(e.getTime()+1e3*t.options.timeout*60),t.log.debug("Timeout check for "+t.options.timeout+" minutes (left "+s/1e3+" sec.)"),0>s?void 0:(t.stop(),t.options.callback())}}(this),this.log.debug("Start timeout in "+this.options.timeout+" minutes"),this.intervallId=setInterval(t,1e3*this.options.timeoutIntervallCheck*60)},e.prototype.stop=function(){return this.intervallId?(this.log.debug("Stop timeout of "+this.options.timeout+" minutes"),clearInterval(this.intervallId)):void 0},e}(s),i=function(t){function s(t){this.ping=bind(this.ping,this),this.send=bind(this.send,this),this.reconnect=bind(this.reconnect,this),this.close=bind(this.close,this),this.connect=bind(this.connect,this),this.set=bind(this.set,this),s.__super__.constructor.call(this,t)}return extend(s,t),s.prototype.logPrefix="io",s.prototype.set=function(t){var e,s,i;s=[];for(e in t)i=t[e],s.push(this.options[e]=i);return s},s.prototype.connect=function(){return this.log.debug("Connecting to "+this.options.host),this.ws=new e.WebSocket(""+this.options.host),this.ws.onopen=function(t){return function(e){return t.log.debug("onOpen",e),t.options.onOpen(e),t.ping()}}(this),this.ws.onmessage=function(t){return function(e){var s,i,n,o;for(o=JSON.parse(e.data),t.log.debug("onMessage",e.data),s=0,i=o.length;i>s;s++)n=o[s],"pong"===n.event&&t.ping();return t.options.onMessage?t.options.onMessage(o):void 0}}(this),this.ws.onclose=function(t){return function(e){if(t.log.debug("close websocket connection",e),t.pingDelayId&&clearTimeout(t.pingDelayId),t.manualClose){if(t.log.debug("manual close, onClose callback"),t.manualClose=!1,t.options.onClose)return t.options.onClose(e)}else if(t.log.debug("error close, onError callback"),t.options.onError)return t.options.onError("Connection lost...")}}(this),this.ws.onerror=function(t){return function(e){return t.log.debug("onError",e),t.options.onError?t.options.onError(e):void 0}}(this)},s.prototype.close=function(){return this.log.debug("close websocket manually"),this.manualClose=!0,this.ws.close()},s.prototype.reconnect=function(){return this.log.debug("reconnect"),this.close(),this.connect()},s.prototype.send=function(t,e){var s;return null==e&&(e={}),this.log.debug("send",t,e),s=JSON.stringify({event:t,data:e}),this.ws.send(s)},s.prototype.ping=function(){var t;return t=function(t){return function(){return t.send("ping")}}(this),this.pingDelayId=setTimeout(t,29e3)},s}(s),a=function(s){function n(s){return this.startTimeoutObservers=bind(this.startTimeoutObservers,this),this.onCssLoaded=bind(this.onCssLoaded,this),this.setAgentOnlineState=bind(this.setAgentOnlineState,this),this.onConnectionEstablished=bind(this.onConnectionEstablished,this),this.setSessionId=bind(this.setSessionId,this),this.onConnectionReestablished=bind(this.onConnectionReestablished,this),this.reconnect=bind(this.reconnect,this),this.destroy=bind(this.destroy,this),this.onScrollHintClick=bind(this.onScrollHintClick,this),this.detectScrolledtoBottom=bind(this.detectScrolledtoBottom,this),this.onLeaveTemporary=bind(this.onLeaveTemporary,this),this.onAgentTypingEnd=bind(this.onAgentTypingEnd,this),this.onAgentTypingStart=bind(this.onAgentTypingStart,this),this.onQueue=bind(this.onQueue,this),this.onQueueScreen=bind(this.onQueueScreen,this),this.onWebSocketClose=bind(this.onWebSocketClose,this),this.onCloseAnimationEnd=bind(this.onCloseAnimationEnd,this),this.close=bind(this.close,this),this.toggle=bind(this.toggle,this),this.sessionClose=bind(this.sessionClose,this),this.onOpenAnimationEnd=bind(this.onOpenAnimationEnd,this),this.open=bind(this.open,this),this.renderMessage=bind(this.renderMessage,this),this.receiveMessage=bind(this.receiveMessage,this),this.onSubmit=bind(this.onSubmit,this),this.onFocus=bind(this.onFocus,this),this.onInput=bind(this.onInput,this),this.onReopenSession=bind(this.onReopenSession,this),this.onError=bind(this.onError,this),this.onWebSocketMessage=bind(this.onWebSocketMessage,this),this.send=bind(this.send,this),this.checkForEnter=bind(this.checkForEnter,this),this.render=bind(this.render,this),this.view=bind(this.view,this),this.T=bind(this.T,this),this.options=t.extend({},this.defaults,s),n.__super__.constructor.call(this,this.options),this.isFullscreen=e.matchMedia&&e.matchMedia("(max-width: 768px)").matches,this.scrollRoot=t(this.getScrollRoot()),t?e.WebSocket&&sessionStorage?this.options.chatId?(this.options.lang||(this.options.lang=t("html").attr("lang")),this.options.lang&&(this.options.lang=this.options.lang.replace(/-.+?$/,""),this.log.debug("lang: "+this.options.lang)),this.options.host||this.detectHost(),this.loadCss(),this.io=new i(this.options),this.io.set({onOpen:this.render,onClose:this.onWebSocketClose,onMessage:this.onWebSocketMessage,onError:this.onError}),void this.io.connect()):(this.state="unsupported",void this.log.error("Chat: need chatId as option!")):(this.state="unsupported",void this.log.notice("Chat: Browser not supported!")):(this.state="unsupported",void this.log.notice("Chat: no jquery found!"))}return extend(n,s),n.prototype.defaults={chatId:void 0,show:!0,target:t("body"),host:"",debug:!1,flat:!1,lang:void 0,cssAutoload:!0,cssUrl:void 0,fontSize:void 0,buttonClass:"open-zammad-chat",inactiveClass:"is-inactive",title:"Chat with us!",scrollHint:"Scrolle nach unten um neue Nachrichten zu sehen",idleTimeout:6,idleTimeoutIntervallCheck:.5,inactiveTimeout:8,inactiveTimeoutIntervallCheck:.5,waitingListTimeout:4,waitingListTimeoutIntervallCheck:.5},n.prototype.logPrefix="chat",n.prototype._messageCount=0,n.prototype.isOpen=!1,n.prototype.blinkOnlineInterval=null,n.prototype.stopBlinOnlineStateTimeout=null,n.prototype.showTimeEveryXMinutes=2,n.prototype.lastTimestamp=null,n.prototype.lastAddedType=null,n.prototype.inputTimeout=null,n.prototype.isTyping=!1,n.prototype.state="offline",n.prototype.initialQueueDelay=1e4,n.prototype.translations={de:{"Chat with us!":"Chatte mit uns!","Scroll down to see new messages":"Scrolle nach unten um neue Nachrichten zu sehen",Online:"Online",Online:"Online",Offline:"Offline",Connecting:"Verbinden","Connection re-established":"Verbindung wiederhergestellt",Today:"Heute",Send:"Senden","Compose your message...":"Ihre Nachricht...","All colleagues are busy.":"Alle Kollegen sind belegt.","You are on waiting list position %s.":"Sie sind in der Warteliste an der Position %s.","Start new conversation":"Neue Konversation starten","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation mit %s geschlossen.","Since you didn't respond in the last %s minutes your conversation got closed.":"Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation geschlossen."}},n.prototype.sessionId=void 0,n.prototype.scrolledToBottom=!0,n.prototype.scrollSnapTolerance=10,n.prototype.T=function(){var t,e,s,i,n,o;if(n=arguments[0],s=2<=arguments.length?slice.call(arguments,1):[],this.options.lang&&"en"!==this.options.lang&&(this.translations[this.options.lang]?(o=this.translations[this.options.lang],o[n]||this.log.notice("Translation needed for '"+n+"'"),n=o[n]||n):this.log.notice("Translation '"+this.options.lang+"' needed!")),s)for(t=0,i=s.length;i>t;t++)e=s[t],n=n.replace(/%s/,e);return n},n.prototype.view=function(t){return function(s){return function(i){return i||(i={}),i.T=s.T,i.background=s.options.background,i.flat=s.options.flat,i.fontSize=s.options.fontSize,e.zammadChatTemplates[t](i)}}(this)},n.prototype.getScrollRoot=function(){var t,e,s;return"scrollingElement"in document?document.scrollingElement:(e=document.documentElement,s=e.scrollTop,e.scrollTop=s+1,t=e.scrollTop,e.scrollTop=s,t>s?e:document.body)},n.prototype.render=function(){return this.el&&t(".zammad-chat").get(0)||this.renderBase(),t("."+this.options.buttonClass).addClass(this.inactiveClass),this.setAgentOnlineState("online"),this.log.debug("widget rendered"),this.startTimeoutObservers(),this.idleTimeout.start(),this.sessionId=sessionStorage.getItem("sessionId"),this.send("chat_status_customer",{session_id:this.sessionId,url:e.location.href})},n.prototype.renderBase=function(){return this.el=t(this.view("chat")({title:this.options.title,scrollHint:this.options.scrollHint})),this.options.target.append(this.el),this.input=this.el.find(".zammad-chat-input"),this.el.find(".js-chat-open").click(this.open),this.el.find(".js-chat-toggle").click(this.toggle),this.el.find(".zammad-chat-controls").on("submit",this.onSubmit),this.el.find(".zammad-chat-body").on("scroll",this.detectScrolledtoBottom),this.el.find(".zammad-scroll-hint").click(this.onScrollHintClick),this.input.on({keydown:this.checkForEnter,input:this.onInput}),t(e).on("beforeunload",function(t){return function(){return t.onLeaveTemporary()}}(this)),t(e).bind("hashchange",function(t){return function(){return t.isOpen?void(t.sessionId&&t.send("chat_session_notice",{session_id:t.sessionId,message:e.location.href})):t.idleTimeout.start()}}(this)),this.isFullscreen?this.input.on({focus:this.onFocus,focusout:this.onFocusOut}):void 0},n.prototype.checkForEnter=function(t){return t.shiftKey||13!==t.keyCode?void 0:(t.preventDefault(),this.sendMessage())},n.prototype.send=function(t,e){return null==e&&(e={}),e.chat_id=this.options.chatId,this.io.send(t,e)},n.prototype.onWebSocketMessage=function(t){var e,s,i;for(e=0,s=t.length;s>e;e++)switch(i=t[e],this.log.debug("ws:onmessage",i),i.event){case"chat_error":this.log.notice(i.data),i.data&&"chat_disabled"===i.data.state&&this.destroy({remove:!0});break;case"chat_session_message":if(i.data.self_written)return;this.receiveMessage(i.data);break;case"chat_session_typing":if(i.data.self_written)return;this.onAgentTypingStart();break;case"chat_session_start":this.onConnectionEstablished(i.data);break;case"chat_session_queue":this.onQueueScreen(i.data);break;case"chat_session_closed":this.onSessionClosed(i.data);break;case"chat_session_left":this.onSessionClosed(i.data);break;case"chat_status_customer":switch(i.data.state){case"online":this.sessionId=void 0,!this.options.cssAutoload||this.cssLoaded?this.onReady():this.socketReady=!0;break;case"offline":this.onError("Zammad Chat: No agent online");break;case"chat_disabled":this.onError("Zammad Chat: Chat is disabled");break;case"no_seats_available":this.onError("Zammad Chat: Too many clients in queue. Clients in queue: "+i.data.queue);break;case"reconnect":this.onReopenSession(i.data)}}},n.prototype.onReady=function(){return this.log.debug("widget ready for use"),t("."+this.options.buttonClass).click(this.open).removeClass(this.inactiveClass),this.options.show?this.show():void 0},n.prototype.onError=function(e){return this.log.debug(e),this.addStatus(e),t("."+this.options.buttonClass).hide(),this.isOpen?(this.disableInput(),this.destroy({remove:!1})):this.destroy({remove:!0})},n.prototype.onReopenSession=function(t){var e,s,i,n,o;if(this.log.debug("old messages",t.session),this.inactiveTimeout.start(),o=sessionStorage.getItem("unfinished_message"),t.agent){for(this.onConnectionEstablished(t),n=t.session,e=0,s=n.length;s>e;e++)i=n[e],this.renderMessage({message:i.content,id:i.id,from:i.created_by_id?"agent":"customer"});o&&this.input.val(o)}return t.position&&this.onQueue(t),this.show(),this.open(),this.scrollToBottom(),o?this.input.focus():void 0},n.prototype.onInput=function(){return this.el.find(".zammad-chat-message--unread").removeClass("zammad-chat-message--unread"),sessionStorage.setItem("unfinished_message",this.input.val()),this.onTyping()},n.prototype.onFocus=function(){var s;return t(e).scrollTop(10),s=t(e).scrollTop()>0,t(e).scrollTop(0),s?this.log.notice("virtual keyboard shown"):void 0},n.prototype.onFocusOut=function(){},n.prototype.onTyping=function(){return this.isTyping&&this.isTyping>new Date((new Date).getTime()-1500)?void 0:(this.isTyping=new Date,this.send("chat_session_typing",{session_id:this.sessionId}),this.inactiveTimeout.start())},n.prototype.onSubmit=function(t){return t.preventDefault(),this.sendMessage()},n.prototype.sendMessage=function(){var t,e;return(t=this.input.val())?(this.inactiveTimeout.start(),sessionStorage.removeItem("unfinished_message"),e=this.view("message")({message:t,from:"customer",id:this._messageCount++,unreadClass:""}),this.maybeAddTimestamp(),this.el.find(".zammad-chat-message--typing").size()?(this.lastAddedType="typing-placeholder",this.el.find(".zammad-chat-message--typing").before(e)):(this.lastAddedType="message--customer",this.el.find(".zammad-chat-body").append(e)),this.input.val(""),this.scrollToBottom(),this.send("chat_session_message",{content:t,id:this._messageCount,session_id:this.sessionId})):void 0},n.prototype.receiveMessage=function(t){return this.inactiveTimeout.start(),this.onAgentTypingEnd(),this.maybeAddTimestamp(),this.renderMessage({message:t.message.content,id:t.id,from:"agent"}),this.scrollToBottom({showHint:!0})},n.prototype.renderMessage=function(t){return this.lastAddedType="message--"+t.from,t.unreadClass=document.hidden?" zammad-chat-message--unread":"",this.el.find(".zammad-chat-body").append(this.view("message")(t))},n.prototype.open=function(){var t;return this.isOpen?void this.log.debug("widget already open, block"):(this.isOpen=!0,this.log.debug("open widget"),this.sessionId||this.showLoader(),this.el.addClass("zammad-chat-is-open"),this.inputInitialized||(this.inputInitialized=!0,this.input.autoGrow({extraLine:!1})),t=this.el.height()-this.el.find(".zammad-chat-header").outerHeight(),this.el.css("bottom",-t),this.sessionId?(this.el.css("bottom",0),this.onOpenAnimationEnd()):(this.el.animate({bottom:0},500,this.onOpenAnimationEnd),this.send("chat_session_init",{url:e.location.href})))},n.prototype.onOpenAnimationEnd=function(){return this.idleTimeout.stop(),this.isFullscreen?this.disableScrollOnRoot():void 0},n.prototype.sessionClose=function(){return this.send("chat_session_close",{session_id:this.sessionId}),this.inactiveTimeout.stop(),this.waitingListTimeout.stop(),sessionStorage.removeItem("unfinished_message"),this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),this.setSessionId(void 0)},n.prototype.toggle=function(t){return this.isOpen?this.close(t):this.open(t)},n.prototype.close=function(t){var e;return this.isOpen?(this.initDelayId&&clearTimeout(this.initDelayId),this.sessionId?(this.log.debug("close widget"),t&&t.stopPropagation(),this.sessionClose(),this.isFullscreen&&this.enableScrollOnRoot(),e=this.el.height()-this.el.find(".zammad-chat-header").outerHeight(),this.el.animate({bottom:-e},500,this.onCloseAnimationEnd)):void this.log.debug("can't close widget without sessionId")):void this.log.debug("can't close widget, it's not open")},n.prototype.onCloseAnimationEnd=function(){return this.el.css("bottom",""),this.el.removeClass("zammad-chat-is-open"),this.showLoader(),this.el.find(".zammad-chat-welcome").removeClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent").addClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent-status").addClass("zammad-chat-is-hidden"),this.isOpen=!1,this.io.reconnect()},n.prototype.onWebSocketClose=function(){return this.isOpen?void 0:this.el?(this.el.removeClass("zammad-chat-is-shown"),this.el.removeClass("zammad-chat-is-loaded")):void 0},n.prototype.show=function(){return"offline"!==this.state?(this.el.addClass("zammad-chat-is-loaded"),this.el.addClass("zammad-chat-is-shown")):void 0},n.prototype.disableInput=function(){return this.input.prop("disabled",!0),this.el.find(".zammad-chat-send").prop("disabled",!0)},n.prototype.enableInput=function(){return this.input.prop("disabled",!1),this.el.find(".zammad-chat-send").prop("disabled",!1)},n.prototype.hideModal=function(){return this.el.find(".zammad-chat-modal").html("")},n.prototype.onQueueScreen=function(t){var e;return this.setSessionId(t.session_id),e=function(e){return function(){return e.onQueue(t),e.waitingListTimeout.start()}}(this),this.initialQueueDelay&&!this.onInitialQueueDelayId?void(this.onInitialQueueDelayId=setTimeout(e,this.initialQueueDelay)):(this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),e())},n.prototype.onQueue=function(t){return this.log.notice("onQueue",t.position),this.inQueue=!0,this.el.find(".zammad-chat-modal").html(this.view("waiting")({position:t.position}))},n.prototype.onAgentTypingStart=function(){return this.stopTypingId&&clearTimeout(this.stopTypingId),this.stopTypingId=setTimeout(this.onAgentTypingEnd,3e3),!this.el.find(".zammad-chat-message--typing").size()&&(this.maybeAddTimestamp(),this.el.find(".zammad-chat-body").append(this.view("typingIndicator")()),this.isVisible(this.el.find(".zammad-chat-message--typing"),!0))?this.scrollToBottom():void 0},n.prototype.onAgentTypingEnd=function(){return this.el.find(".zammad-chat-message--typing").remove()},n.prototype.onLeaveTemporary=function(){return this.sessionId?this.send("chat_session_leave_temporary",{session_id:this.sessionId}):void 0},n.prototype.maybeAddTimestamp=function(){var t,e,s;return s=Date.now(),!this.lastTimestamp||s-this.lastTimestamp>6e4*this.showTimeEveryXMinutes?(t=this.T("Today"),e=(new Date).toTimeString().substr(0,5),"timestamp"===this.lastAddedType?(this.updateLastTimestamp(t,e),this.lastTimestamp=s):(this.el.find(".zammad-chat-body").append(this.view("timestamp")({label:t,time:e})),this.lastTimestamp=s,this.lastAddedType="timestamp",this.scrollToBottom())):void 0},n.prototype.updateLastTimestamp=function(t,e){return this.el?this.el.find(".zammad-chat-body").find(".zammad-chat-timestamp").last().replaceWith(this.view("timestamp")({label:t,time:e})):void 0},n.prototype.addStatus=function(t){return this.el?(this.maybeAddTimestamp(),this.el.find(".zammad-chat-body").append(this.view("status")({status:t})),this.scrollToBottom()):void 0},n.prototype.detectScrolledtoBottom=function(){var t;return t=this.el.find(".zammad-chat-body").scrollTop()+this.el.find(".zammad-chat-body").outerHeight(),this.scrolledToBottom=Math.abs(t-this.el.find(".zammad-chat-body").prop("scrollHeight"))<=this.scrollSnapTolerance,this.scrolledToBottom?this.el.find(".zammad-scroll-hint").addClass("is-hidden"):void 0},n.prototype.showScrollHint=function(){return this.el.find(".zammad-scroll-hint").removeClass("is-hidden"),this.el.find(".zammad-chat-body").scrollTop(this.el.find(".zammad-chat-body").scrollTop()+this.el.find(".zammad-scroll-hint").outerHeight())},n.prototype.onScrollHintClick=function(){return this.el.find(".zammad-chat-body").animate({scrollTop:this.el.find(".zammad-chat-body").prop("scrollHeight")},300)},n.prototype.scrollToBottom=function(e){var s;return s=(null!=e?e:{showHint:!1}).showHint,this.scrolledToBottom?this.el.find(".zammad-chat-body").scrollTop(t(".zammad-chat-body").prop("scrollHeight")):s?this.showScrollHint():void 0},n.prototype.destroy=function(t){return null==t&&(t={}),this.log.debug("destroy widget",t),this.setAgentOnlineState("offline"),t.remove&&this.el&&this.el.remove(),this.waitingListTimeout&&this.waitingListTimeout.stop(),this.inactiveTimeout&&this.inactiveTimeout.stop(),this.idleTimeout&&this.idleTimeout.stop(),this.io.close()},n.prototype.reconnect=function(){return this.log.notice("reconnecting"),this.disableInput(),this.lastAddedType="status",this.setAgentOnlineState("connecting"),this.addStatus(this.T("Connection lost"))},n.prototype.onConnectionReestablished=function(){return this.lastAddedType="status",this.setAgentOnlineState("online"),this.addStatus(this.T("Connection re-established"))},n.prototype.onSessionClosed=function(t){return this.addStatus(this.T("Chat closed by %s",t.realname)),this.disableInput(),this.setAgentOnlineState("offline"),this.inactiveTimeout.stop()},n.prototype.setSessionId=function(t){return this.sessionId=t,void 0===t?sessionStorage.removeItem("sessionId"):sessionStorage.setItem("sessionId",t)},n.prototype.onConnectionEstablished=function(t){return this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),this.inQueue=!1,t.agent&&(this.agent=t.agent),t.session_id&&this.setSessionId(t.session_id),this.el.find(".zammad-chat-body").html(""),this.el.find(".zammad-chat-agent").html(this.view("agent")({agent:this.agent})),this.enableInput(),this.hideModal(),this.el.find(".zammad-chat-welcome").addClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent").removeClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent-status").removeClass("zammad-chat-is-hidden"),this.isFullscreen||this.input.focus(),this.setAgentOnlineState("online"),this.waitingListTimeout.stop(),this.idleTimeout.stop(),this.inactiveTimeout.start()},n.prototype.showCustomerTimeout=function(){var t;return this.el.find(".zammad-chat-modal").html(this.view("customer_timeout")({agent:this.agent.name,delay:this.options.inactiveTimeout})),t=function(){return location.reload()},this.el.find(".js-restart").click(t),this.sessionClose()},n.prototype.showWaitingListTimeout=function(){var t;return this.el.find(".zammad-chat-modal").html(this.view("waiting_list_timeout")({delay:this.options.watingListTimeout})),t=function(){return location.reload()},this.el.find(".js-restart").click(t),this.sessionClose()},n.prototype.showLoader=function(){return this.el.find(".zammad-chat-modal").html(this.view("loader")())},n.prototype.setAgentOnlineState=function(t){var e;return this.state=t,this.el?(e=t.charAt(0).toUpperCase()+t.slice(1),this.el.find(".zammad-chat-agent-status").attr("data-status",t).text(this.T(e))):void 0},n.prototype.detectHost=function(){var t;return t="ws://","https:"===e.location.protocol&&(t="wss://"),this.options.host=""+t+h+"/ws"},n.prototype.loadCss=function(){var t,e,s;if(this.options.cssAutoload)return s=this.options.cssUrl,s||(s=this.options.host.replace(/^wss/i,"https").replace(/^ws/i,"http").replace(/\/ws/i,""),s+="/assets/chat/chat.css"),this.log.debug("load css from '"+s+"'"),e="@import url('"+s+"');",t=document.createElement("link"),t.onload=this.onCssLoaded,t.rel="stylesheet",t.href="data:text/css,"+escape(e),document.getElementsByTagName("head")[0].appendChild(t)},n.prototype.onCssLoaded=function(){return this.socketReady?this.onReady():this.cssLoaded=!0},n.prototype.startTimeoutObservers=function(){return this.idleTimeout=new o({logPrefix:"idleTimeout",debug:this.options.debug,timeout:this.options.idleTimeout,timeoutIntervallCheck:this.options.idleTimeoutIntervallCheck,callback:function(t){return function(){return t.log.debug("Idle timeout reached, hide widget",new Date),t.destroy({remove:!0})}}(this)}),this.inactiveTimeout=new o({logPrefix:"inactiveTimeout",debug:this.options.debug,timeout:this.options.inactiveTimeout,timeoutIntervallCheck:this.options.inactiveTimeoutIntervallCheck,callback:function(t){return function(){return t.log.debug("Inactive timeout reached, show timeout screen.",new Date),t.showCustomerTimeout(),t.destroy({remove:!1})}}(this)}),this.waitingListTimeout=new o({logPrefix:"waitingListTimeout",debug:this.options.debug,timeout:this.options.waitingListTimeout,timeoutIntervallCheck:this.options.waitingListTimeoutIntervallCheck,callback:function(t){return function(){return t.log.debug("Waiting list timeout reached, show timeout screen.",new Date),t.showWaitingListTimeout(),t.destroy({remove:!1})}}(this)})},n.prototype.disableScrollOnRoot=function(){return this.rootScrollOffset=this.scrollRoot.scrollTop(),this.scrollRoot.css({overflow:"hidden",position:"fixed"})},n.prototype.enableScrollOnRoot=function(){return this.scrollRoot.scrollTop(this.rootScrollOffset),this.scrollRoot.css({overflow:"",position:""})},n.prototype.isVisible=function(s,i,n,o){var a,r,h,l,d,c,u,p,m,g,f,y,v,b,w,T,z,C,S,I,k,_,O,A,E,L;if(!(s.length<1))if(r=t(e),a=s.length>1?s.eq(0):s,C=a.get(0),L=r.width(),E=r.height(),o=o?o:"both",p=n===!0?C.offsetWidth*C.offsetHeight:!0,"function"==typeof C.getBoundingClientRect){if(z=C.getBoundingClientRect(),S=z.top>=0&&z.top0&&z.bottom<=E,b=z.left>=0&&z.left0&&z.right<=L,I=i?S||u:S&&u,v=i?b||T:b&&T,"both"===o)return p&&I&&v;if("vertical"===o)return p&&I;if("horizontal"===o)return p&&v}else{if(A=r.scrollTop(),k=A+E,_=r.scrollLeft(),O=_+L,w=a.offset(),c=w.top,h=c+a.height(),l=w.left,d=l+a.width(),y=i===!0?h:c,m=i===!0?c:h,g=i===!0?d:l,f=i===!0?l:d,"both"===o)return!!p&&k>=m&&y>=A&&O>=f&&g>=_;if("vertical"===o)return!!p&&k>=m&&y>=A;if("horizontal"===o)return!!p&&O>=f&&g>=_}},n}(s),e.ZammadChat=a}(window.jQuery,window),window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.chat=function(t){t||(t={});var e,s=[],i=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},n=t.safe,o=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
\n
\n
\n \n \n \n \n \n
\n
\n
\n
\n \n '),s.push(this.T(this.title)),s.push('\n
\n
\n
\n \n
\n
\n \n \n
\n
")}).call(this)}.call(t),t.safe=n,t.escape=o,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.customer_timeout=function(t){t||(t={});var e,s=[],i=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},n=t.safe,o=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,"""); +}),function(){(function(){s.push('
\n '),this.agent?(s.push("\n "),s.push(this.T("Since you didn't respond in the last %s minutes your conversation with %s got closed.",this.delay,this.agent)),s.push("\n ")):(s.push("\n "),s.push(this.T("Since you didn't respond in the last %s minutes your conversation got closed.",this.delay)),s.push("\n ")),s.push('\n
\n
"),s.push(this.T("Start new conversation")),s.push("
\n
")}).call(this)}.call(t),t.safe=n,t.escape=o,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.loader=function(t){t||(t={});var e,s=[],i=t.safe,n=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},n||(n=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('\n \n \n \n\n'),s.push(this.T("Connecting")),s.push("")}).call(this)}.call(t),t.safe=i,t.escape=n,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.message=function(t){t||(t={});var e,s=[],i=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},n=t.safe,o=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
\n "),s.push(this.message),s.push("\n
")}).call(this)}.call(t),t.safe=n,t.escape=o,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.status=function(t){t||(t={});var e,s=[],i=t.safe,n=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},n||(n=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
\n
\n '),s.push(this.status),s.push("\n
\n
")}).call(this)}.call(t),t.safe=i,t.escape=n,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.timestamp=function(t){t||(t={});var e,s=[],i=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},n=t.safe,o=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
'),s.push(i(this.label)),s.push(" "),s.push(i(this.time)),s.push("
")}).call(this)}.call(t),t.safe=n,t.escape=o,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.typingIndicator=function(t){t||(t={});var e,s=[],i=t.safe,n=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},n||(n=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
\n \n \n \n \n \n \n \n
')}).call(this)}.call(t),t.safe=i,t.escape=n,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting=function(t){t||(t={});var e,s=[],i=t.safe,n=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},n||(n=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
\n \n \n \n \n \n '),s.push(this.T("All colleagues are busy.")),s.push("
\n "),s.push(this.T("You are on waiting list position %s.",this.position)),s.push("\n
")}).call(this)}.call(t),t.safe=i,t.escape=n,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting_list_timeout=function(t){t||(t={});var e,s=[],i=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},n=t.safe,o=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
\n '),s.push(this.T("We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!")),s.push('\n
\n
"),s.push(this.T("Start new conversation")),s.push("
\n
")}).call(this)}.call(t),t.safe=n,t.escape=o,s.join("")}; \ No newline at end of file diff --git a/public/assets/chat/chat.scss b/public/assets/chat/chat.scss index 31aa5219e..1cec7cef1 100644 --- a/public/assets/chat/chat.scss +++ b/public/assets/chat/chat.scss @@ -351,15 +351,16 @@ float: left; width: auto; height: auto; - min-height: 1.4em !important; max-height: 6em; + min-height: 1.4em !important; font-family: inherit; - line-height: inherit; + line-height: 1.4em; font-size: inherit; appearance: none; border: none !important; background: none; box-shadow: none !important; + box-sizing: content-box; outline: none; resize: none; flex: 1; diff --git a/public/assets/chat/index.html b/public/assets/chat/index.html index 6cd4cf9ac..811fc18fa 100644 --- a/public/assets/chat/index.html +++ b/public/assets/chat/index.html @@ -3,71 +3,1782 @@ Zammad Chat @@ -150,7 +1861,7 @@ var chat = new ZammadChat({ chatId: 1, host: 'ws://localhost:6042', - cssUrl: 'http://localhost:3000/assets/chat/chat.css', + cssUrl: 'http://localhost:5000/assets/chat/chat.css', debug: true }); diff --git a/public/assets/chat/jquery.autoGrow.js b/public/assets/chat/jquery.autoGrow.js index 131106ee6..cda824c42 100644 --- a/public/assets/chat/jquery.autoGrow.js +++ b/public/assets/chat/jquery.autoGrow.js @@ -14,7 +14,9 @@ * * Date: October 15, 2012 * - * Zammad modification: remove overflow:hidden when maximum height is reached + * Zammad modification: + * - remove overflow:hidden when maximum height is reached + * - mirror box-sizing * */ @@ -44,8 +46,7 @@ jQuery.fn.autoGrow = function(options) { if (jQuery(textarea).height() != jQuery(mirror).height()) { jQuery(textarea).height(jQuery(mirror).height()); - var maxHeight = parseInt(jQuery(textarea).css('max-height'), 10); - var overflow = jQuery(mirror).height() > maxHeight ? '' : 'hidden' + var overflow = jQuery(mirror).height() > maxHeight ? '' : 'hidden'; jQuery(textarea).css('overflow', overflow); } } @@ -56,6 +57,9 @@ jQuery.fn.autoGrow = function(options) { // Create a mirror var mirror = createMirror(this); + + // Store max-height + var maxHeight = parseInt(jQuery(this).css('max-height'), 10); // Style the mirror mirror.style.display = 'none'; @@ -70,6 +74,8 @@ jQuery.fn.autoGrow = function(options) { mirror.style.fontFamily = jQuery(this).css('font-family'); mirror.style.fontSize = jQuery(this).css('font-size'); mirror.style.lineHeight = jQuery(this).css('line-height'); + mirror.style.letterSpacing = jQuery(this).css('letter-spacing'); + mirror.style.boxSizing = jQuery(this).css('boxSizing'); // Style the textarea this.style.overflow = "hidden"; @@ -77,6 +83,7 @@ jQuery.fn.autoGrow = function(options) { // Bind the textarea's event this.onkeyup = growTextarea; + this.onfocus = growTextarea; // Fire the event for text already present sendContentToMirror(this); diff --git a/public/assets/tests/core.js b/public/assets/tests/core.js index 2cfcbc0ab..3c9401487 100644 --- a/public/assets/tests/core.js +++ b/public/assets/tests/core.js @@ -312,6 +312,27 @@ test('i18n', function() { var timestamp = App.i18n.translateTimestamp('2012-11-06T21:07:24Z', offset); equal(timestamp, '06.11.2012 21:07', 'de-de - timestamp translated correctly') + timestamp = App.i18n.translateTimestamp('', offset); + equal(timestamp, '', 'de-de - timestamp translated correctly') + + timestamp = App.i18n.translateTimestamp(null, offset); + equal(timestamp, null, 'de-de - timestamp translated correctly') + + timestamp = App.i18n.translateTimestamp(undefined, offset); + equal(timestamp, undefined, 'de-de - timestamp translated correctly') + + var date = App.i18n.translateDate('2012-11-06', 0) + equal(date, '06.11.2012', 'de-de - date translated correctly') + + date = App.i18n.translateDate('', 0) + equal(date, '', 'de-de - date translated correctly') + + date = App.i18n.translateDate(null, 0) + equal(date, null, 'de-de - date translated correctly') + + date = App.i18n.translateDate(undefined, 0) + equal(date, undefined, 'de-de - date translated correctly') + // en App.i18n.set('en-us') translated = App.i18n.translateContent('yes') @@ -368,6 +389,27 @@ test('i18n', function() { timestamp = App.i18n.translateTimestamp('2012-11-06T21:07:24Z', offset) equal(timestamp, '11/06/2012 21:07', 'en - timestamp translated correctly') + timestamp = App.i18n.translateTimestamp('', offset); + equal(timestamp, '', 'en - timestamp translated correctly') + + timestamp = App.i18n.translateTimestamp(null, offset); + equal(timestamp, null, 'en - timestamp translated correctly') + + timestamp = App.i18n.translateTimestamp(undefined, offset); + equal(timestamp, undefined, 'en - timestamp translated correctly') + + date = App.i18n.translateDate('2012-11-06', 0) + equal(date, '11/06/2012', 'en - date translated correctly') + + date = App.i18n.translateDate('', 0) + equal(date, '', 'en - date translated correctly') + + date = App.i18n.translateDate(null, 0) + equal(date, null, 'en - date translated correctly') + + date = App.i18n.translateDate(undefined, 0) + equal(date, undefined, 'en - date translated correctly') + // locale alias test // de App.i18n.set('de') diff --git a/script/build/test_slice_tests.sh b/script/build/test_slice_tests.sh index 5020c3220..6d5c0e79d 100755 --- a/script/build/test_slice_tests.sh +++ b/script/build/test_slice_tests.sh @@ -47,6 +47,8 @@ if [ "$LEVEL" == '1' ]; then # test/browser/maintenance_login_message_test.rb # test/browser/maintenance_mode_test.rb # test/browser/maintenance_session_message_test.rb + # test/browser/manage_test.rb + # test/browser/monitoring_test.rb rm test/browser/preferences_language_test.rb rm test/browser/preferences_permission_check_test.rb rm test/browser/preferences_token_access_test.rb @@ -101,6 +103,7 @@ elif [ "$LEVEL" == '2' ]; then rm test/browser/maintenance_mode_test.rb rm test/browser/maintenance_session_message_test.rb rm test/browser/manage_test.rb + rm test/browser/monitoring_test.rb rm test/browser/preferences_language_test.rb rm test/browser/preferences_permission_check_test.rb rm test/browser/preferences_token_access_test.rb @@ -155,6 +158,7 @@ elif [ "$LEVEL" == '3' ]; then rm test/browser/maintenance_mode_test.rb rm test/browser/maintenance_session_message_test.rb rm test/browser/manage_test.rb + rm test/browser/monitoring_test.rb rm test/browser/preferences_language_test.rb rm test/browser/preferences_permission_check_test.rb rm test/browser/preferences_token_access_test.rb @@ -209,6 +213,7 @@ elif [ "$LEVEL" == '4' ]; then rm test/browser/maintenance_mode_test.rb rm test/browser/maintenance_session_message_test.rb rm test/browser/manage_test.rb + rm test/browser/monitoring_test.rb rm test/browser/preferences_language_test.rb rm test/browser/preferences_permission_check_test.rb rm test/browser/preferences_token_access_test.rb @@ -262,6 +267,7 @@ elif [ "$LEVEL" == '5' ]; then rm test/browser/maintenance_mode_test.rb rm test/browser/maintenance_session_message_test.rb rm test/browser/manage_test.rb + rm test/browser/monitoring_test.rb rm test/browser/preferences_language_test.rb rm test/browser/preferences_permission_check_test.rb rm test/browser/preferences_token_access_test.rb @@ -318,6 +324,7 @@ elif [ "$LEVEL" == '6' ]; then rm test/browser/maintenance_mode_test.rb rm test/browser/maintenance_session_message_test.rb rm test/browser/manage_test.rb + rm test/browser/monitoring_test.rb # test/browser/preferences_language_test.rb # test/browser/preferences_permission_check_test.rb # test/browser/preferences_token_access_test.rb diff --git a/spec/fixtures/import/otrs/article/attachment/default.json b/spec/fixtures/import/otrs/article/attachment/default.json new file mode 100644 index 000000000..ee5f39b98 --- /dev/null +++ b/spec/fixtures/import/otrs/article/attachment/default.json @@ -0,0 +1,10 @@ +{ + "ContentAlternative": "", + "ContentID": "", + "Disposition": "inline", + "Filesize": "201 Bytes", + "ContentType": "text/html; charset=\"utf-8\"", + "Filename": "ZmlsZS0y\n", + "FilesizeRaw": "201", + "Content": "PCFET0NUWVBFIGh0bWw+PGh0bWw+PGhlYWQ+PG1ldGEgaHR0cC1lcXVpdj0iQ29udGVudC1UeXBl\nIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9dXRmLTgiLz48L2hlYWQ+PGJvZHkgc3R5bGU9\nImZvbnQtZmFtaWx5OkdlbmV2YSxIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZjsgZm9udC1zaXpl\nOiAxMnB4OyI+dGVzdCAjMzwvYm9keT48L2h0bWw+\n" +} diff --git a/spec/fixtures/import/otrs/article/customer_phone.json b/spec/fixtures/import/otrs/article/customer_phone.json new file mode 100644 index 000000000..7e1636a6c --- /dev/null +++ b/spec/fixtures/import/otrs/article/customer_phone.json @@ -0,0 +1,97 @@ +{ + "Age": 63188310, + "PriorityID": "3", + "ContentType": "text/plain; charset=utf-8", + "AttachmentIDOfHTMLBody": "1", + "DynamicField_SugarCRMCompanySelection": null, + "ServiceID": null, + "TicketFreeText11": null, + "DynamicField_ITSMDueDate": "2014-11-24 00:15:00", + "DynamicField_Topic": null, + "StateID": "2", + "DynamicField_Hostname": null, + "Body": "test #3", + "DynamicField_ZammadMigratorChanged": null, + "EscalationTime": "0", + "Changed": "2014-11-21 00:21:08", + "OwnerID": "3", + "DynamicField_ZarafaTN": null, + "DynamicField_ProcessManagementActivityID": null, + "DynamicField_TopicID": null, + "DynamicField_ScomHostname": null, + "Owner": "agent-2", + "AgeTimeUnix": 63188309, + "TicketFreeKey11": null, + "ArticleID": "3970", + "Created": "2014-11-21 00:17:41", + "DynamicField_ScomUUID": null, + "DynamicField_TicketFreeText11": null, + "DynamicField_TicketFreeKey11": null, + "DynamicField_ITSMReviewRequired": "No", + "DynamicField_OpenExchangeTicketNumber": null, + "DynamicField_ITSMDecisionDate": null, + "ArticleTypeID": "5", + "QueueID": "1", + "ReplyTo": "", + "DynamicField_ITSMImpact": null, + "TicketID": "730", + "DynamicField_ITSMRecoveryStartTime": null, + "Cc": "", + "EscalationResponseTime": "0", + "DynamicField_ProcessManagementProcessID": null, + "IncomingTime": "1416525461", + "Charset": "utf-8", + "DynamicField_CheckboxExample": null, + "DynamicField_Location": null, + "CustomerUserID": "BetreuterKunde2", + "DynamicField_Vertriebsweg": null, + "Attachments": [], + "DynamicField_CustomerLocation": null, + "DynamicField_SugarCRMRemoteID": null, + "DynamicField_OpenExchangeTN": null, + "Service": "", + "Type": "Incident", + "ContentCharset": "utf-8", + "DynamicField_TETest": null, + "Responsible": "root@localhost", + "SenderType": "customer", + "ResponsibleID": "1", + "SLA": "", + "MimeType": "text/plain", + "DynamicField_Combine": null, + "Subject": "test #3", + "InReplyTo": "", + "RealTillTimeNotUsed": "0", + "DynamicField_ScomService": null, + "CustomerID": "3333333333", + "TypeID": "1", + "MessageID": "", + "Priority": "3 normal", + "To": "Postmaster", + "DynamicField_SugarCRMCompanySelectedID": null, + "UntilTime": 0, + "EscalationUpdateTime": "0", + "CreatedBy": "3", + "Queue": "Postmaster", + "DynamicField_ITSMRepairStartTime": null, + "ToRealname": "Postmaster", + "State": "closed successful", + "SenderTypeID": "3", + "DynamicField_ZammadMigratorChangedOld": "1", + "Title": "test #3", + "DynamicField_ScomState": null, + "References": "", + "DynamicField_Department": null, + "ArticleType": "phone", + "StateType": "closed", + "FromRealname": "Betreuter Kunde", + "EscalationSolutionTime": "0", + "LockID": "1", + "TicketNumber": "20141121305000012", + "DynamicField_ITSMDecisionResult": null, + "Lock": "unlock", + "CreateTimeUnix": "1416525460", + "SLAID": null, + "DynamicField_ITSMCriticality": null, + "From": "\"Betreuter Kunde\" ," +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/article/customer_phone_attachment.json b/spec/fixtures/import/otrs/article/customer_phone_attachment.json new file mode 100644 index 000000000..aa1f4ef34 --- /dev/null +++ b/spec/fixtures/import/otrs/article/customer_phone_attachment.json @@ -0,0 +1,108 @@ +{ + "Age": 63188310, + "PriorityID": "3", + "ContentType": "text/plain; charset=utf-8", + "AttachmentIDOfHTMLBody": "1", + "DynamicField_SugarCRMCompanySelection": null, + "ServiceID": null, + "TicketFreeText11": null, + "DynamicField_ITSMDueDate": "2014-11-24 00:15:00", + "DynamicField_Topic": null, + "StateID": "2", + "DynamicField_Hostname": null, + "Body": "test #3", + "DynamicField_ZammadMigratorChanged": null, + "EscalationTime": "0", + "Changed": "2014-11-21 00:21:08", + "OwnerID": "3", + "DynamicField_ZarafaTN": null, + "DynamicField_ProcessManagementActivityID": null, + "DynamicField_TopicID": null, + "DynamicField_ScomHostname": null, + "Owner": "agent-2", + "AgeTimeUnix": 63188309, + "TicketFreeKey11": null, + "ArticleID": "3970", + "Created": "2014-11-21 00:17:41", + "DynamicField_ScomUUID": null, + "DynamicField_TicketFreeText11": null, + "DynamicField_TicketFreeKey11": null, + "DynamicField_ITSMReviewRequired": "No", + "DynamicField_OpenExchangeTicketNumber": null, + "DynamicField_ITSMDecisionDate": null, + "ArticleTypeID": "5", + "QueueID": "1", + "ReplyTo": "", + "DynamicField_ITSMImpact": null, + "TicketID": "730", + "DynamicField_ITSMRecoveryStartTime": null, + "Cc": "", + "EscalationResponseTime": "0", + "DynamicField_ProcessManagementProcessID": null, + "IncomingTime": "1416525461", + "Charset": "utf-8", + "DynamicField_CheckboxExample": null, + "DynamicField_Location": null, + "CustomerUserID": "BetreuterKunde2", + "DynamicField_Vertriebsweg": null, + "Attachments": [ + { + "ContentAlternative": "", + "ContentID": "", + "Disposition": "inline", + "Filesize": "201 Bytes", + "ContentType": "text/html; charset=\"utf-8\"", + "Filename": "ZmlsZS0y\n", + "FilesizeRaw": "201", + "Content": "PCFET0NUWVBFIGh0bWw+PGh0bWw+PGhlYWQ+PG1ldGEgaHR0cC1lcXVpdj0iQ29udGVudC1UeXBl\nIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9dXRmLTgiLz48L2hlYWQ+PGJvZHkgc3R5bGU9\nImZvbnQtZmFtaWx5OkdlbmV2YSxIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZjsgZm9udC1zaXpl\nOiAxMnB4OyI+dGVzdCAjMzwvYm9keT48L2h0bWw+\n" + } + ], + "DynamicField_CustomerLocation": null, + "DynamicField_SugarCRMRemoteID": null, + "DynamicField_OpenExchangeTN": null, + "Service": "", + "Type": "Incident", + "ContentCharset": "utf-8", + "DynamicField_TETest": null, + "Responsible": "root@localhost", + "SenderType": "customer", + "ResponsibleID": "1", + "SLA": "", + "MimeType": "text/plain", + "DynamicField_Combine": null, + "Subject": "test #3", + "InReplyTo": "", + "RealTillTimeNotUsed": "0", + "DynamicField_ScomService": null, + "CustomerID": "3333333333", + "TypeID": "1", + "MessageID": "", + "Priority": "3 normal", + "To": "Postmaster", + "DynamicField_SugarCRMCompanySelectedID": null, + "UntilTime": 0, + "EscalationUpdateTime": "0", + "CreatedBy": "3", + "Queue": "Postmaster", + "DynamicField_ITSMRepairStartTime": null, + "ToRealname": "Postmaster", + "State": "closed successful", + "SenderTypeID": "3", + "DynamicField_ZammadMigratorChangedOld": "1", + "Title": "test #3", + "DynamicField_ScomState": null, + "References": "", + "DynamicField_Department": null, + "ArticleType": "phone", + "StateType": "closed", + "FromRealname": "Betreuter Kunde", + "EscalationSolutionTime": "0", + "LockID": "1", + "TicketNumber": "20141121305000012", + "DynamicField_ITSMDecisionResult": null, + "Lock": "unlock", + "CreateTimeUnix": "1416525460", + "SLAID": null, + "DynamicField_ITSMCriticality": null, + "From": "\"Betreuter Kunde\" ," +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/customer/default.json b/spec/fixtures/import/otrs/customer/default.json new file mode 100644 index 000000000..68d9a2858 --- /dev/null +++ b/spec/fixtures/import/otrs/customer/default.json @@ -0,0 +1,130 @@ +{ + "ChangeTime": "2014-06-06 12:41:03", + "ChangeBy": "1", + "ValidID": "2", + "CustomerCompanyCity": "test922896", + "CreateTime": "2014-06-06 12:41:03", + "CustomerCompanyURL": "test922896", + "Config": { + "CustomerCompanySearchFields": [ + "customer_id", + "name" + ], + "CustomerCompanyListFields": [ + "customer_id", + "name" + ], + "Module": "Kernel::System::CustomerCompany::DB", + "CustomerCompanyKey": "customer_id", + "CustomerCompanySearchSuffix": "*", + "CacheTTL": 86400, + "CustomerCompanySearchListLimit": 250, + "CustomerCompanySearchPrefix": "", + "CustomerCompanyValid": "valid_id", + "Params": { + "Table": "customer_company", + "CaseSensitive": 0 + }, + "Map": [ + [ + "CustomerID", + "CustomerID", + "customer_id", + 0, + 1, + "var", + "", + 0 + ], + [ + "CustomerCompanyName", + "Customer", + "name", + 1, + 1, + "var", + "", + 0 + ], + [ + "CustomerCompanyStreet", + "Street", + "street", + 1, + 0, + "var", + "", + 0 + ], + [ + "CustomerCompanyZIP", + "Zip", + "zip", + 1, + 0, + "var", + "", + 0 + ], + [ + "CustomerCompanyCity", + "City", + "city", + 1, + 0, + "var", + "", + 0 + ], + [ + "CustomerCompanyCountry", + "Country", + "country", + 1, + 0, + "var", + "", + 0 + ], + [ + "CustomerCompanyURL", + "URL", + "url", + 1, + 0, + "var", + "[% Data.CustomerCompanyURL | html %]", + 0 + ], + [ + "CustomerCompanyComment", + "Comment", + "comments", + 1, + 0, + "var", + "", + 0 + ], + [ + "ValidID", + "Valid", + "valid_id", + 0, + 1, + "int", + "", + 0 + ] + ], + "Name": "Database Backend" + }, + "CustomerCompanyName": "test922896", + "CustomerCompanyCountry": "test922896", + "CustomerID": "test922896", + "CustomerCompanyStreet": "test922896", + "CustomerCompanyComment": "test922896", + "CustomerCompanyZIP": "test922896", + "Source": "CustomerCompany", + "CreateBy": "1" +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/customer_user/default.json b/spec/fixtures/import/otrs/customer_user/default.json new file mode 100644 index 000000000..96fd0f920 --- /dev/null +++ b/spec/fixtures/import/otrs/customer_user/default.json @@ -0,0 +1,348 @@ +{ + "CustomerCompanyCity": "test712259", + "Config": { + "CustomerUserEmailUniqCheck": 1, + "CustomerUserSearchListLimit": 250, + "CustomerCompanySupport": 1, + "CustomerValid": "valid_id", + "CustomerUserSearchFields": [ + "login", + "first_name", + "last_name", + "customer_id" + ], + "CustomerUserSearchPrefix": "*", + "Params": { + "Table": "customer_user", + "CaseSensitive": 0 + }, + "CustomerUserListFields": [ + "first_name", + "last_name", + "email" + ], + "Map": [ + [ + "UserTitle", + "Title", + "title", + 1, + 0, + "var", + "", + 0 + ], + [ + "UserFirstname", + "Firstname", + "first_name", + 1, + 1, + "var", + "", + 0 + ], + [ + "UserLastname", + "Lastname", + "last_name", + 1, + 1, + "var", + "", + 0 + ], + [ + "UserLogin", + "Username", + "login", + 1, + 1, + "var", + "", + 0 + ], + [ + "UserPassword", + "Password", + "pw", + 0, + 0, + "var", + "", + 0 + ], + [ + "UserEmail", + "Email", + "email", + 1, + 1, + "var", + "", + 0 + ], + [ + "UserCustomerID", + "CustomerID", + "customer_id", + 0, + 1, + "var", + "", + 0 + ], + [ + "UserPhone", + "Phone", + "phone", + 1, + 0, + "var", + "", + 0 + ], + [ + "UserFax", + "Fax", + "fax", + 1, + 0, + "var", + "", + 0 + ], + [ + "UserMobile", + "Mobile", + "mobile", + 1, + 0, + "var", + "", + 0 + ], + [ + "UserStreet", + "Street", + "street", + 1, + 0, + "var", + "", + 0 + ], + [ + "UserZip", + "Zip", + "zip", + 1, + 0, + "var", + "", + 0 + ], + [ + "UserCity", + "City", + "city", + 1, + 0, + "var", + "", + 0 + ], + [ + "UserCountry", + "Country", + "country", + 1, + 0, + "var", + "", + 0 + ], + [ + "UserComment", + "Comment", + "comments", + 1, + 0, + "var", + "", + 0 + ], + [ + "ValidID", + "Valid", + "valid_id", + 0, + 1, + "int", + "", + 0 + ] + ], + "CustomerKey": "login", + "CustomerUserSearchSuffix": "*", + "Module": "Kernel::System::CustomerUser::DB", + "CacheTTL": 86400, + "Selections": {}, + "CustomerID": "customer_id", + "Name": "Database Backend", + "CustomerUserPostMasterSearchFields": [ + "email" + ], + "CustomerUserNameFields": [ + "title", + "first_name", + "last_name" + ] + }, + "UserCustomerID": "test712259", + "CustomerCompanyComment": "test712259", + "Source": "CustomerUser", + "UserTitle": "", + "CompanyConfig": { + "CustomerCompanySearchFields": [ + "customer_id", + "name" + ], + "CustomerCompanyListFields": [ + "customer_id", + "name" + ], + "Module": "Kernel::System::CustomerCompany::DB", + "CustomerCompanyKey": "customer_id", + "CustomerCompanySearchSuffix": "*", + "CacheTTL": 86400, + "CustomerCompanySearchListLimit": 250, + "CustomerCompanySearchPrefix": "", + "CustomerCompanyValid": "valid_id", + "Params": { + "Table": "customer_company", + "CaseSensitive": 0 + }, + "Map": [ + [ + "CustomerID", + "CustomerID", + "customer_id", + 0, + 1, + "var", + "", + 0 + ], + [ + "CustomerCompanyName", + "Customer", + "name", + 1, + 1, + "var", + "", + 0 + ], + [ + "CustomerCompanyStreet", + "Street", + "street", + 1, + 0, + "var", + "", + 0 + ], + [ + "CustomerCompanyZIP", + "Zip", + "zip", + 1, + 0, + "var", + "", + 0 + ], + [ + "CustomerCompanyCity", + "City", + "city", + 1, + 0, + "var", + "", + 0 + ], + [ + "CustomerCompanyCountry", + "Country", + "country", + 1, + 0, + "var", + "", + 0 + ], + [ + "CustomerCompanyURL", + "URL", + "url", + 1, + 0, + "var", + "[% Data.CustomerCompanyURL | html %]", + 0 + ], + [ + "CustomerCompanyComment", + "Comment", + "comments", + 1, + 0, + "var", + "", + 0 + ], + [ + "ValidID", + "Valid", + "valid_id", + 0, + 1, + "int", + "", + 0 + ] + ], + "Name": "Database Backend" + }, + "UserZip": null, + "UserLastname": "test669673", + "ChangeBy": "1", + "CreateTime": "2014-06-07 02:31:31", + "UserLogin": "test669673", + "UserPhone": null, + "CustomerID": "test712259", + "CustomerCompanyValidID": "1", + "CustomerCompanyZIP": "test712259", + "UserCountry": null, + "UserPassword": "f8be19af2f25837a31eff9131b0e47a5173290652c04a48b49b86474d48825ee", + "ValidID": "1", + "UserRefreshTime": "0", + "UserEmail": "qa100@t-online.de", + "UserComment": "", + "UserID": "test669673", + "UserFirstname": "test669673", + "CustomerCompanyCountry": "test712259", + "UserFax": null, + "CreateBy": "1", + "ChangeTime": "2014-06-07 02:31:31", + "UserShowTickets": "25", + "UserStreet": null, + "CustomerCompanyURL": "test712259", + "CustomerCompanyName": "test712259", + "UserMobile": null, + "CustomerCompanyStreet": "test712259", + "UserCity": null +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/dynamic_field/checkbox/default.json b/spec/fixtures/import/otrs/dynamic_field/checkbox/default.json new file mode 100644 index 000000000..69484d471 --- /dev/null +++ b/spec/fixtures/import/otrs/dynamic_field/checkbox/default.json @@ -0,0 +1,15 @@ +{ + "ID": "47", + "ChangeTime": "2016-05-25 11:14:05", + "InternalField": "0", + "ValidID": "1", + "CreateTime": "2016-05-25 11:14:05", + "Label": "Checkbox Example", + "FieldOrder": "26", + "Config": { + "DefaultValue": "1" + }, + "FieldType": "Checkbox", + "Name": "CheckboxExample", + "ObjectType": "Ticket" +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/dynamic_field/date/default.json b/spec/fixtures/import/otrs/dynamic_field/date/default.json new file mode 100644 index 000000000..9c3e7ab1b --- /dev/null +++ b/spec/fixtures/import/otrs/dynamic_field/date/default.json @@ -0,0 +1,19 @@ +{ + "ID": "46", + "ChangeTime": "2016-05-25 11:14:06", + "InternalField": "0", + "ValidID": "1", + "CreateTime": "2014-11-23 20:01:32", + "Label": "Date Example", + "FieldOrder": "40", + "Config": { + "YearsPeriod": "0", + "YearsInFuture": "0", + "DefaultValue": "0", + "YearsInPast": "0", + "Link": "" + }, + "FieldType": "Date", + "Name": "DateExample", + "ObjectType": "Ticket" +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/dynamic_field/date_time/default.json b/spec/fixtures/import/otrs/dynamic_field/date_time/default.json new file mode 100644 index 000000000..4e2a4c3df --- /dev/null +++ b/spec/fixtures/import/otrs/dynamic_field/date_time/default.json @@ -0,0 +1,19 @@ +{ + "ID": "16", + "ChangeTime": "2014-09-12 09:31:58", + "InternalField": "1", + "ValidID": "1", + "CreateTime": "2014-06-26 09:53:21", + "Label": "DateTime Example", + "FieldOrder": "16", + "Config": { + "YearsPeriod": "1", + "YearsInFuture": "1", + "DefaultValue": "259200", + "YearsInPast": "9", + "Link": "" + }, + "FieldType": "DateTime", + "Name": "DateTimeExample", + "ObjectType": "Ticket" +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/dynamic_field/dropdown/default.json b/spec/fixtures/import/otrs/dynamic_field/dropdown/default.json new file mode 100644 index 000000000..2040d07f8 --- /dev/null +++ b/spec/fixtures/import/otrs/dynamic_field/dropdown/default.json @@ -0,0 +1,25 @@ +{ + "ID": "40", + "ChangeTime": "2016-05-25 11:14:06", + "InternalField": "0", + "ValidID": "1", + "CreateTime": "2014-08-21 14:54:15", + "Label": "Dropdown Example", + "FieldOrder": "30", + "Config": { + "TranslatableValues": "0", + "PossibleValues": { + "Hamburg": "Hamburg", + "München": "München", + "Köln": "Köln", + "Berlin": "Berlin" + }, + "TreeView": "0", + "DefaultValue": "", + "Link": "", + "PossibleNone": "1" + }, + "FieldType": "Dropdown", + "Name": "DropdownExample", + "ObjectType": "Ticket" +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/dynamic_field/multiselect/default.json b/spec/fixtures/import/otrs/dynamic_field/multiselect/default.json new file mode 100644 index 000000000..4bb4dd2cb --- /dev/null +++ b/spec/fixtures/import/otrs/dynamic_field/multiselect/default.json @@ -0,0 +1,24 @@ +{ + "ID": "4", + "ChangeTime": "2014-09-12 09:31:58", + "InternalField": "0", + "ValidID": "1", + "CreateTime": "2014-05-29 20:11:41", + "Label": "Multiselec tExample", + "FieldOrder": "4", + "Config": { + "TranslatableValues": "0", + "PossibleValues": { + "Hamburg": "Hamburg", + "München": "München", + "Köln": "Köln", + "Berlin": "Berlin" + }, + "TreeView": "0", + "DefaultValue": "", + "PossibleNone": "0" + }, + "FieldType": "Multiselect", + "Name": "MultiselectExample", + "ObjectType": "Ticket" +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/dynamic_field/text/default.json b/spec/fixtures/import/otrs/dynamic_field/text/default.json new file mode 100644 index 000000000..90c186bc1 --- /dev/null +++ b/spec/fixtures/import/otrs/dynamic_field/text/default.json @@ -0,0 +1,16 @@ +{ + "ID": "8", + "ChangeTime": "2014-09-12 09:31:58", + "InternalField": "0", + "ValidID": "1", + "CreateTime": "2014-06-19 12:11:23", + "Label": "Text Example", + "FieldOrder": "8", + "Config": { + "DefaultValue": "", + "Link": "" + }, + "FieldType": "Text", + "Name": "TextExample", + "ObjectType": "Ticket" +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/dynamic_field/text_area/default.json b/spec/fixtures/import/otrs/dynamic_field/text_area/default.json new file mode 100644 index 000000000..1322f4448 --- /dev/null +++ b/spec/fixtures/import/otrs/dynamic_field/text_area/default.json @@ -0,0 +1,17 @@ +{ + "ID": "8", + "ChangeTime": "2014-09-12 09:31:58", + "InternalField": "0", + "ValidID": "1", + "CreateTime": "2014-06-19 12:11:23", + "Label": "TextArea Example", + "FieldOrder": "8", + "Config": { + "DefaultValue": "", + "Rows": "20", + "Cols": "10" + }, + "FieldType": "TextArea", + "Name": "TextAreaExample", + "ObjectType": "Ticket" +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/history/article/default.json b/spec/fixtures/import/otrs/history/article/default.json new file mode 100644 index 000000000..ad364f560 --- /dev/null +++ b/spec/fixtures/import/otrs/history/article/default.json @@ -0,0 +1,15 @@ +{ + "HistoryID": "11307", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:21:08", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "AddNote", + "TypeID": "1", + "HistoryTypeID": "15", + "ArticleID": "3973", + "StateID": "2", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%Close" +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/history/move/default.json b/spec/fixtures/import/otrs/history/move/default.json new file mode 100644 index 000000000..cf9296fdb --- /dev/null +++ b/spec/fixtures/import/otrs/history/move/default.json @@ -0,0 +1,15 @@ +{ + "HistoryID": "238", + "PriorityID": "3", + "CreateTime": "2014-05-12 13:42:41", + "OwnerID": "1", + "QueueID": "5", + "HistoryType": "Move", + "TypeID": "1", + "HistoryTypeID": "16", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "1", + "TicketID": "39", + "Name": "%%Source%%5%%Target%%2" +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/history/new_ticket/default.json b/spec/fixtures/import/otrs/history/new_ticket/default.json new file mode 100644 index 000000000..64fb3a8d9 --- /dev/null +++ b/spec/fixtures/import/otrs/history/new_ticket/default.json @@ -0,0 +1,15 @@ +{ + "HistoryID": "11291", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:41", + "OwnerID": "1", + "QueueID": "1", + "HistoryType": "NewTicket", + "TypeID": "1", + "HistoryTypeID": "1", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%20141121305000012%%Postmaster%%3 normal%%open%%730" +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/history/priority_update/default.json b/spec/fixtures/import/otrs/history/priority_update/default.json new file mode 100644 index 000000000..b8bd76cf4 --- /dev/null +++ b/spec/fixtures/import/otrs/history/priority_update/default.json @@ -0,0 +1,15 @@ +{ + "HistoryID": "11131", + "PriorityID": "4", + "CreateTime": "2014-09-22 16:44:55", + "OwnerID": "1", + "QueueID": "1", + "HistoryType": "PriorityUpdate", + "TypeID": "2", + "HistoryTypeID": "22", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "1", + "TicketID": "721", + "Name": "%%2 low%%2%%4 high%%4" +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/history/state_update/default.json b/spec/fixtures/import/otrs/history/state_update/default.json new file mode 100644 index 000000000..e75c368be --- /dev/null +++ b/spec/fixtures/import/otrs/history/state_update/default.json @@ -0,0 +1,15 @@ +{ + "HistoryID": "11305", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:21:08", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "StateUpdate", + "TypeID": "1", + "HistoryTypeID": "27", + "ArticleID": 0, + "StateID": "2", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%new%%open%%" +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/priority/default.json b/spec/fixtures/import/otrs/priority/default.json new file mode 100644 index 000000000..25db5ea26 --- /dev/null +++ b/spec/fixtures/import/otrs/priority/default.json @@ -0,0 +1,9 @@ +{ + "ChangeBy": "1", + "ChangeTime": "2014-04-28 10:53:18", + "ID": "4", + "ValidID": "1", + "CreateTime": "2014-04-28 10:53:18", + "CreateBy": "1", + "Name": "4 high" +} diff --git a/spec/fixtures/import/otrs/queue/default.json b/spec/fixtures/import/otrs/queue/default.json new file mode 100644 index 000000000..4db1dd2f8 --- /dev/null +++ b/spec/fixtures/import/otrs/queue/default.json @@ -0,0 +1,25 @@ +{ + "ValidID": "2", + "FollowUpLock": "0", + "RealName": "UnitTest49130", + "QueueID": "11", + "FirstResponseNotify": "0", + "UpdateTime": "0", + "Email": "unittest15486@example.com", + "ChangeTime": "2014-05-13 10:54:11", + "UnlockTimeout": "0", + "Calendar": "", + "CreateTime": "2014-05-13 10:54:11", + "Comment": "Some comment", + "UpdateNotify": "0", + "DefaultSignKey": "", + "GroupID": "1", + "SolutionTime": "0", + "SolutionNotify": "0", + "SystemAddressID": "8", + "FollowUpID": "1", + "SalutationID": "1", + "Name": "UnitTestQueue45699", + "SignatureID": "8", + "FirstResponseTime": "0" +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/state/default.json b/spec/fixtures/import/otrs/state/default.json new file mode 100644 index 000000000..13c8a8704 --- /dev/null +++ b/spec/fixtures/import/otrs/state/default.json @@ -0,0 +1,10 @@ +{ + "ChangeTime": "2014-04-28 10:53:18", + "ID": "2", + "ValidID": "1", + "TypeID": "3", + "CreateTime": "2014-04-28 10:53:18", + "TypeName": "closed", + "Comment": "Ticket is closed successful.", + "Name": "closed successful" +} diff --git a/spec/fixtures/import/otrs/ticket/default.json b/spec/fixtures/import/otrs/ticket/default.json new file mode 100644 index 000000000..a347cbd61 --- /dev/null +++ b/spec/fixtures/import/otrs/ticket/default.json @@ -0,0 +1,770 @@ +{ + "Age": 63277486, + "PriorityID": "3", + "ServiceID": "", + "DynamicField_SugarCRMCompanySelection": null, + "DynamicField_ITSMDueDate": "2014-11-24 00:15:00", + "DynamicField_Topic": null, + "StateID": "2", + "DynamicField_Hostname": null, + "DynamicField_ZammadMigratorChanged": null, + "EscalationTime": "0", + "Changed": "2014-11-21 00:21:08", + "OwnerID": "3", + "DynamicField_ZarafaTN": null, + "DynamicField_ProcessManagementActivityID": null, + "DynamicField_TopicID": null, + "DynamicField_ScomHostname": null, + "Owner": "agent-2", + "Created": "2014-11-21 00:17:40", + "DynamicField_TicketFreeText11": null, + "DynamicField_ScomUUID": null, + "DynamicField_TicketFreeKey11": null, + "DynamicField_ITSMReviewRequired": "No", + "DynamicField_OpenExchangeTicketNumber": null, + "DynamicField_ITSMDecisionDate": null, + "QueueID": "1", + "CreateBy": "3", + "DynamicField_ITSMImpact": null, + "TicketID": "730", + "DynamicField_ITSMRecoveryStartTime": null, + "SolutionInMin": 0, + "EscalationResponseTime": "0", + "UnlockTimeout": "1416525661", + "DynamicField_ProcessManagementProcessID": null, + "ArchiveFlag": "n", + "DynamicField_CheckboxExample": null, + "SolutionTime": "2014-11-21 00:21:08", + "DynamicField_Location": null, + "CustomerUserID": "BetreuterKunde2", + "DynamicField_Vertriebsweg": null, + "DynamicField_CustomerLocation": null, + "DynamicField_SugarCRMRemoteID": null, + "DynamicField_OpenExchangeTN": null, + "Type": "Incident", + "DynamicField_TETest": null, + "Responsible": "root@localhost", + "ResponsibleID": "1", + "ChangeBy": "3", + "DynamicField_Combine": null, + "RealTillTimeNotUsed": "0", + "GroupID": "1", + "DynamicField_ScomService": null, + "CustomerID": "3333333333", + "TypeID": "1", + "Priority": "3 normal", + "DynamicField_SugarCRMCompanySelectedID": null, + "History": [ + { + "HistoryID": "11291", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:41", + "OwnerID": "1", + "QueueID": "1", + "HistoryType": "NewTicket", + "TypeID": "1", + "HistoryTypeID": "1", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%20141121305000012%%Postmaster%%3 normal%%open%%730" + }, + { + "HistoryID": "11292", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:41", + "OwnerID": "1", + "QueueID": "1", + "HistoryType": "TicketDynamicFieldUpdate", + "TypeID": "1", + "HistoryTypeID": "28", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "1", + "TicketID": "730", + "Name": "%%FieldName%%ZammadMigratorChanged%%Value%%1" + }, + { + "HistoryID": "11293", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:41", + "OwnerID": "1", + "QueueID": "1", + "HistoryType": "ServiceUpdate", + "TypeID": "1", + "HistoryTypeID": "38", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%NULL%%%%NULL%%" + }, + { + "HistoryID": "11294", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:41", + "OwnerID": "1", + "QueueID": "1", + "HistoryType": "SLAUpdate", + "TypeID": "1", + "HistoryTypeID": "39", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%NULL%%%%NULL%%" + }, + { + "HistoryID": "11295", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:41", + "OwnerID": "1", + "QueueID": "1", + "HistoryType": "CustomerUpdate", + "TypeID": "1", + "HistoryTypeID": "21", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%CustomerID=3333333333;CustomerUser=BetreuterKunde2;" + }, + { + "HistoryID": "11296", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:41", + "OwnerID": "1", + "QueueID": "1", + "HistoryType": "TicketDynamicFieldUpdate", + "TypeID": "1", + "HistoryTypeID": "28", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%FieldName%%ITSMDueDate%%Value%%2014-11-24 00:15:00" + }, + { + "HistoryID": "11297", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:41", + "OwnerID": "1", + "QueueID": "1", + "HistoryType": "PhoneCallCustomer", + "TypeID": "1", + "HistoryTypeID": "14", + "ArticleID": "3970", + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%" + }, + { + "HistoryID": "11298", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:42", + "OwnerID": "1", + "QueueID": "1", + "HistoryType": "SendAutoReply", + "TypeID": "1", + "HistoryTypeID": "4", + "ArticleID": "3971", + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%\"Betreuter Kunde\" " + }, + { + "HistoryID": "11299", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:42", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "OwnerUpdate", + "TypeID": "1", + "HistoryTypeID": "23", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%agent-2%%3" + }, + { + "HistoryID": "11300", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:43", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "SendCustomerNotification", + "TypeID": "1", + "HistoryTypeID": "10", + "ArticleID": "3972", + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%kunde2@kunde.de" + }, + { + "HistoryID": "11301", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:43", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "SendAgentNotification", + "TypeID": "1", + "HistoryTypeID": "9", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%TEST%%agent-1@example.com" + }, + { + "HistoryID": "11302", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:44", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "SendAgentNotification", + "TypeID": "1", + "HistoryTypeID": "9", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%TEST%%agent-2-for-role-2@example.com" + }, + { + "HistoryID": "11303", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:21:01", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "Lock", + "TypeID": "1", + "HistoryTypeID": "17", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%lock" + }, + { + "HistoryID": "11304", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:21:01", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "Misc", + "TypeID": "1", + "HistoryTypeID": "25", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "Reset of unlock time." + }, + { + "HistoryID": "11305", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:21:08", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "StateUpdate", + "TypeID": "1", + "HistoryTypeID": "27", + "ArticleID": 0, + "StateID": "2", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%open%%closed successful%%" + }, + { + "HistoryID": "11306", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:21:08", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "Unlock", + "TypeID": "1", + "HistoryTypeID": "18", + "ArticleID": 0, + "StateID": "2", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%unlock" + }, + { + "HistoryID": "11307", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:21:08", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "AddNote", + "TypeID": "1", + "HistoryTypeID": "15", + "ArticleID": "3973", + "StateID": "2", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%Close" + }, + { + "HistoryID": "11308", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:21:09", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "TicketDynamicFieldUpdate", + "TypeID": "1", + "HistoryTypeID": "28", + "ArticleID": 0, + "StateID": "2", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%FieldName%%ITSMReviewRequired%%Value%%No" + } + ], + "UntilTime": 0, + "EscalationUpdateTime": "0", + "DynamicField_ITSMRepairStartTime": null, + "Queue": "Postmaster", + "State": "closed successful", + "Closed": "2014-11-21 00:21:08", + "Title": "test #3", + "DynamicField_ZammadMigratorChangedOld": "1", + "DynamicField_ScomState": null, + "FirstLock": "2014-11-21 00:21:01", + "DynamicField_Department": null, + "Articles": [ + { + "Age": 63277486, + "PriorityID": "3", + "ContentType": "text/plain; charset=utf-8", + "AttachmentIDOfHTMLBody": "1", + "DynamicField_SugarCRMCompanySelection": null, + "ServiceID": null, + "TicketFreeText11": null, + "DynamicField_ITSMDueDate": "2014-11-24 00:15:00", + "DynamicField_Topic": null, + "StateID": "2", + "DynamicField_Hostname": null, + "Body": "test #3", + "DynamicField_ZammadMigratorChanged": null, + "EscalationTime": "0", + "Changed": "2014-11-21 00:21:08", + "OwnerID": "3", + "DynamicField_ZarafaTN": null, + "DynamicField_ProcessManagementActivityID": null, + "DynamicField_TopicID": null, + "DynamicField_ScomHostname": null, + "Owner": "agent-2", + "AgeTimeUnix": 63277485, + "TicketFreeKey11": null, + "ArticleID": "3970", + "Created": "2014-11-21 00:17:41", + "DynamicField_ScomUUID": null, + "DynamicField_TicketFreeText11": null, + "DynamicField_TicketFreeKey11": null, + "DynamicField_ITSMReviewRequired": "No", + "DynamicField_OpenExchangeTicketNumber": null, + "DynamicField_ITSMDecisionDate": null, + "ArticleTypeID": "5", + "QueueID": "1", + "ReplyTo": "", + "DynamicField_ITSMImpact": null, + "TicketID": "730", + "DynamicField_ITSMRecoveryStartTime": null, + "Cc": "", + "EscalationResponseTime": "0", + "DynamicField_ProcessManagementProcessID": null, + "IncomingTime": "1416525461", + "Charset": "utf-8", + "DynamicField_CheckboxExample": null, + "DynamicField_Location": null, + "CustomerUserID": "BetreuterKunde2", + "DynamicField_Vertriebsweg": null, + "Attachments": [ + { + "ContentAlternative": "", + "ContentID": "", + "Disposition": "inline", + "Filesize": "201 Bytes", + "ContentType": "text/html; charset=\"utf-8\"", + "Filename": "ZmlsZS0y\n", + "FilesizeRaw": "201", + "Content": "PCFET0NUWVBFIGh0bWw+PGh0bWw+PGhlYWQ+PG1ldGEgaHR0cC1lcXVpdj0iQ29udGVudC1UeXBl\nIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9dXRmLTgiLz48L2hlYWQ+PGJvZHkgc3R5bGU9\nImZvbnQtZmFtaWx5OkdlbmV2YSxIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZjsgZm9udC1zaXpl\nOiAxMnB4OyI+dGVzdCAjMzwvYm9keT48L2h0bWw+\n" + } + ], + "DynamicField_CustomerLocation": null, + "DynamicField_SugarCRMRemoteID": null, + "DynamicField_OpenExchangeTN": null, + "Service": "", + "Type": "Incident", + "ContentCharset": "utf-8", + "DynamicField_TETest": null, + "Responsible": "root@localhost", + "SenderType": "customer", + "ResponsibleID": "1", + "SLA": "", + "MimeType": "text/plain", + "DynamicField_Combine": null, + "Subject": "test #3", + "InReplyTo": "", + "RealTillTimeNotUsed": "0", + "DynamicField_ScomService": null, + "CustomerID": "3333333333", + "TypeID": "1", + "MessageID": "", + "Priority": "3 normal", + "To": "Postmaster", + "DynamicField_SugarCRMCompanySelectedID": null, + "UntilTime": 0, + "EscalationUpdateTime": "0", + "CreatedBy": "3", + "Queue": "Postmaster", + "DynamicField_ITSMRepairStartTime": null, + "ToRealname": "Postmaster", + "State": "closed successful", + "SenderTypeID": "3", + "DynamicField_ZammadMigratorChangedOld": "1", + "Title": "test #3", + "DynamicField_ScomState": null, + "References": "", + "DynamicField_Department": null, + "ArticleType": "phone", + "StateType": "closed", + "FromRealname": "Betreuter Kunde", + "EscalationSolutionTime": "0", + "LockID": "1", + "TicketNumber": "20141121305000012", + "DynamicField_ITSMDecisionResult": null, + "Lock": "unlock", + "CreateTimeUnix": "1416525460", + "SLAID": null, + "DynamicField_ITSMCriticality": null, + "From": "\"Betreuter Kunde\" ," + }, + { + "Age": 63277486, + "PriorityID": "3", + "ContentType": "text/plain; charset=utf-8", + "AttachmentIDOfHTMLBody": "1", + "DynamicField_SugarCRMCompanySelection": null, + "ServiceID": null, + "TicketFreeText11": null, + "DynamicField_ITSMDueDate": "2014-11-24 00:15:00", + "DynamicField_Topic": null, + "StateID": "2", + "DynamicField_Hostname": null, + "Body": "This is a demo text which is send to every inquiry.\nIt could contain something like:\n\nThanks for your email. A new ticket has been created.\n\nYou wrote:\n> test #3\n\n\nYour email will be answered by a human ASAP\n\nHave fun with OTRS! :-)\n\nYour OTRS Team\n", + "DynamicField_ZammadMigratorChanged": null, + "EscalationTime": "0", + "Changed": "2014-11-21 00:21:08", + "OwnerID": "3", + "DynamicField_ZarafaTN": null, + "DynamicField_ProcessManagementActivityID": null, + "DynamicField_TopicID": null, + "DynamicField_ScomHostname": null, + "Owner": "agent-2", + "AgeTimeUnix": 63277484, + "TicketFreeKey11": null, + "ArticleID": "3971", + "Created": "2014-11-21 00:17:42", + "DynamicField_ScomUUID": null, + "DynamicField_TicketFreeText11": null, + "DynamicField_TicketFreeKey11": null, + "DynamicField_ITSMReviewRequired": "No", + "DynamicField_OpenExchangeTicketNumber": null, + "DynamicField_ITSMDecisionDate": null, + "ArticleTypeID": "1", + "QueueID": "1", + "ReplyTo": "", + "DynamicField_ITSMImpact": null, + "TicketID": "730", + "DynamicField_ITSMRecoveryStartTime": null, + "Cc": "", + "EscalationResponseTime": "0", + "DynamicField_ProcessManagementProcessID": null, + "IncomingTime": "1416525462", + "Charset": "utf-8", + "DynamicField_CheckboxExample": null, + "DynamicField_Location": null, + "CustomerUserID": "BetreuterKunde2", + "DynamicField_Vertriebsweg": null, + "Attachments": [ + { + "ContentAlternative": "", + "ContentID": "", + "Disposition": "inline", + "Filesize": "591 Bytes", + "ContentType": "text/html; charset=\"utf-8\"", + "Filename": "ZmlsZS0y\n", + "FilesizeRaw": "591", + "Content": "PCFET0NUWVBFIGh0bWw+PGh0bWw+PGhlYWQ+PG1ldGEgaHR0cC1lcXVpdj0iQ29udGVudC1UeXBl\nIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9dXRmLTgiLz48L2hlYWQ+PGJvZHkgc3R5bGU9\nImZvbnQtZmFtaWx5OkdlbmV2YSxIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZjsgZm9udC1zaXpl\nOiAxMnB4OyI+VGhpcyBpcyBhIGRlbW8gdGV4dCB3aGljaCBpcyBzZW5kIHRvIGV2ZXJ5IGlucXVp\ncnkuPGJyLz5JdCBjb3VsZCBjb250YWluIHNvbWV0aGluZyBsaWtlOjxici8+PGJyLz5UaGFua3Mg\nZm9yIHlvdXIgZW1haWwuIEEgbmV3IHRpY2tldCBoYXMgYmVlbiBjcmVhdGVkLjxici8+PGJyLz5Z\nb3Ugd3JvdGU6PGJyLz48ZGl2ICB0eXBlPSJjaXRlIiBzdHlsZT0iYm9yZGVyOm5vbmU7Ym9yZGVy\nLWxlZnQ6c29saWQgYmx1ZSAxLjVwdDtwYWRkaW5nOjBjbSAwY20gMGNtIDQuMHB0Ij50ZXN0ICMz\nPC9kaXY+PGJyLz48YnIvPllvdXIgZW1haWwgd2lsbCBiZSBhbnN3ZXJlZCBieSBhIGh1bWFuIEFT\nQVA8YnIvPjxici8+SGF2ZSBmdW4gd2l0aCBPVFJTISA6LSk8YnIvPjxici8+WW91ciBPVFJTIFRl\nYW08YnIvPjwvYm9keT48L2h0bWw+\n" + } + ], + "DynamicField_CustomerLocation": null, + "DynamicField_SugarCRMRemoteID": null, + "DynamicField_OpenExchangeTN": null, + "Service": "", + "Type": "Incident", + "ContentCharset": "utf-8", + "DynamicField_TETest": null, + "Responsible": "root@localhost", + "SenderType": "system", + "ResponsibleID": "1", + "SLA": "", + "MimeType": "text/plain", + "DynamicField_Combine": null, + "Subject": "[Ticket#20141121305000012] RE: test #3", + "InReplyTo": "", + "RealTillTimeNotUsed": "0", + "DynamicField_ScomService": null, + "CustomerID": "3333333333", + "TypeID": "1", + "MessageID": "<1416525462.277752.866124677.730.3@yourhost.example.com>", + "Priority": "3 normal", + "To": "\"Betreuter Kunde\" ", + "DynamicField_SugarCRMCompanySelectedID": null, + "UntilTime": 0, + "EscalationUpdateTime": "0", + "CreatedBy": "3", + "Queue": "Postmaster", + "DynamicField_ITSMRepairStartTime": null, + "ToRealname": "Betreuter Kunde", + "State": "closed successful", + "SenderTypeID": "2", + "DynamicField_ZammadMigratorChangedOld": "1", + "Title": "test #3", + "DynamicField_ScomState": null, + "References": "", + "DynamicField_Department": null, + "ArticleType": "email-external", + "StateType": "closed", + "FromRealname": "OTRS System", + "EscalationSolutionTime": "0", + "LockID": "1", + "TicketNumber": "20141121305000012", + "DynamicField_ITSMDecisionResult": null, + "Lock": "unlock", + "CreateTimeUnix": "1416525460", + "SLAID": null, + "DynamicField_ITSMCriticality": null, + "From": "OTRS System " + }, + { + "Age": 63277486, + "PriorityID": "3", + "ContentType": "text/plain; charset=utf-8", + "DynamicField_SugarCRMCompanySelection": null, + "ServiceID": null, + "TicketFreeText11": null, + "DynamicField_ITSMDueDate": "2014-11-24 00:15:00", + "DynamicField_Topic": null, + "StateID": "2", + "DynamicField_Hostname": null, + "Body": "CustomerUser: Betreuter Kunde\nCustomerUser Email: kunde2@kunde.de\nPartner Email: \"Betreuter Kunde\" ,", + "DynamicField_ZammadMigratorChanged": null, + "EscalationTime": "0", + "Changed": "2014-11-21 00:21:08", + "OwnerID": "3", + "DynamicField_ZarafaTN": null, + "DynamicField_ProcessManagementActivityID": null, + "DynamicField_TopicID": null, + "DynamicField_ScomHostname": null, + "Owner": "agent-2", + "AgeTimeUnix": 63277483, + "TicketFreeKey11": null, + "ArticleID": "3972", + "Created": "2014-11-21 00:17:43", + "DynamicField_ScomUUID": null, + "DynamicField_TicketFreeText11": null, + "DynamicField_TicketFreeKey11": null, + "DynamicField_ITSMReviewRequired": "No", + "DynamicField_OpenExchangeTicketNumber": null, + "DynamicField_ITSMDecisionDate": null, + "ArticleTypeID": "3", + "QueueID": "1", + "ReplyTo": "", + "DynamicField_ITSMImpact": null, + "TicketID": "730", + "DynamicField_ITSMRecoveryStartTime": null, + "Cc": "", + "EscalationResponseTime": "0", + "DynamicField_ProcessManagementProcessID": null, + "IncomingTime": "1416525463", + "Charset": "utf-8", + "DynamicField_CheckboxExample": null, + "DynamicField_Location": null, + "CustomerUserID": "BetreuterKunde2", + "DynamicField_Vertriebsweg": null, + "Attachments": [], + "DynamicField_CustomerLocation": null, + "DynamicField_SugarCRMRemoteID": null, + "DynamicField_OpenExchangeTN": null, + "Service": "", + "Type": "Incident", + "ContentCharset": "utf-8", + "DynamicField_TETest": null, + "Responsible": "root@localhost", + "SenderType": "system", + "ResponsibleID": "1", + "SLA": "", + "MimeType": "text/plain", + "DynamicField_Combine": null, + "Subject": "[Ticket#20141121305000012] EventNotification: test #3", + "InReplyTo": "", + "RealTillTimeNotUsed": "0", + "DynamicField_ScomService": null, + "CustomerID": "3333333333", + "TypeID": "1", + "MessageID": "<1416525463.523919.240611208.730.3@yourhost.example.com>", + "Priority": "3 normal", + "To": "kunde2@kunde.de", + "DynamicField_SugarCRMCompanySelectedID": null, + "UntilTime": 0, + "EscalationUpdateTime": "0", + "CreatedBy": "3", + "Queue": "Postmaster", + "DynamicField_ITSMRepairStartTime": null, + "ToRealname": "kunde2@kunde.de", + "State": "closed successful", + "SenderTypeID": "2", + "DynamicField_ZammadMigratorChangedOld": "1", + "Title": "test #3", + "DynamicField_ScomState": null, + "References": "", + "DynamicField_Department": null, + "ArticleType": "email-notification-ext", + "StateType": "closed", + "FromRealname": "OTRS System", + "EscalationSolutionTime": "0", + "LockID": "1", + "TicketNumber": "20141121305000012", + "DynamicField_ITSMDecisionResult": null, + "Lock": "unlock", + "CreateTimeUnix": "1416525460", + "SLAID": null, + "DynamicField_ITSMCriticality": null, + "From": "OTRS System " + }, + { + "Age": 63277486, + "PriorityID": "3", + "ContentType": "text/plain; charset=utf-8", + "AttachmentIDOfHTMLBody": "1", + "DynamicField_SugarCRMCompanySelection": null, + "ServiceID": null, + "TicketFreeText11": null, + "DynamicField_ITSMDueDate": "2014-11-24 00:15:00", + "DynamicField_Topic": null, + "StateID": "2", + "DynamicField_Hostname": null, + "Body": "close", + "DynamicField_ZammadMigratorChanged": null, + "EscalationTime": "0", + "Changed": "2014-11-21 00:21:08", + "OwnerID": "3", + "DynamicField_ZarafaTN": null, + "DynamicField_ProcessManagementActivityID": null, + "DynamicField_TopicID": null, + "DynamicField_ScomHostname": null, + "Owner": "agent-2", + "AgeTimeUnix": 63277278, + "TicketFreeKey11": null, + "ArticleID": "3973", + "Created": "2014-11-21 00:21:08", + "DynamicField_ScomUUID": null, + "DynamicField_TicketFreeText11": null, + "DynamicField_TicketFreeKey11": null, + "DynamicField_ITSMReviewRequired": "No", + "DynamicField_OpenExchangeTicketNumber": null, + "DynamicField_ITSMDecisionDate": null, + "ArticleTypeID": "9", + "QueueID": "1", + "ReplyTo": "", + "DynamicField_ITSMImpact": null, + "TicketID": "730", + "DynamicField_ITSMRecoveryStartTime": null, + "Cc": "", + "EscalationResponseTime": "0", + "DynamicField_ProcessManagementProcessID": null, + "IncomingTime": "1416525668", + "Charset": "utf-8", + "DynamicField_CheckboxExample": null, + "DynamicField_Location": null, + "CustomerUserID": "BetreuterKunde2", + "DynamicField_Vertriebsweg": null, + "Attachments": [ + { + "ContentAlternative": "", + "ContentID": "", + "Disposition": "inline", + "Filesize": "199 Bytes", + "ContentType": "text/html; charset=\"utf-8\"", + "Filename": "ZmlsZS0y\n", + "FilesizeRaw": "199", + "Content": "PCFET0NUWVBFIGh0bWw+PGh0bWw+PGhlYWQ+PG1ldGEgaHR0cC1lcXVpdj0iQ29udGVudC1UeXBl\nIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9dXRmLTgiLz48L2hlYWQ+PGJvZHkgc3R5bGU9\nImZvbnQtZmFtaWx5OkdlbmV2YSxIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZjsgZm9udC1zaXpl\nOiAxMnB4OyI+Y2xvc2U8L2JvZHk+PC9odG1sPg==\n" + } + ], + "DynamicField_CustomerLocation": null, + "DynamicField_SugarCRMRemoteID": null, + "DynamicField_OpenExchangeTN": null, + "Service": "", + "Type": "Incident", + "ContentCharset": "utf-8", + "DynamicField_TETest": null, + "Responsible": "root@localhost", + "SenderType": "agent", + "ResponsibleID": "1", + "SLA": "", + "MimeType": "text/plain", + "DynamicField_Combine": null, + "Subject": "Close", + "InReplyTo": "", + "RealTillTimeNotUsed": "0", + "DynamicField_ScomService": null, + "CustomerID": "3333333333", + "TypeID": "1", + "MessageID": "", + "Priority": "3 normal", + "To": "", + "DynamicField_SugarCRMCompanySelectedID": null, + "UntilTime": 0, + "EscalationUpdateTime": "0", + "CreatedBy": "3", + "Queue": "Postmaster", + "DynamicField_ITSMRepairStartTime": null, + "State": "closed successful", + "SenderTypeID": "1", + "DynamicField_ZammadMigratorChangedOld": "1", + "Title": "test #3", + "DynamicField_ScomState": null, + "References": "", + "DynamicField_Department": null, + "ArticleType": "note-internal", + "StateType": "closed", + "FromRealname": "agent-2 firstname äöüß agent-2 lastname äöüß", + "EscalationSolutionTime": "0", + "LockID": "1", + "TicketNumber": "20141121305000012", + "DynamicField_ITSMDecisionResult": null, + "Lock": "unlock", + "CreateTimeUnix": "1416525460", + "SLAID": null, + "DynamicField_ITSMCriticality": null, + "From": "\"agent-2 firstname äöüß agent-2 lastname äöüß\" " + } + ], + "StateType": "closed", + "EscalationSolutionTime": "0", + "LockID": "1", + "TicketNumber": "20141121305000012", + "DynamicField_ITSMDecisionResult": null, + "CreateTimeUnix": "1416525460", + "Lock": "unlock", + "SLAID": "", + "DynamicField_ITSMCriticality": null +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/ticket/no_title.json b/spec/fixtures/import/otrs/ticket/no_title.json new file mode 100644 index 000000000..0c0cc764d --- /dev/null +++ b/spec/fixtures/import/otrs/ticket/no_title.json @@ -0,0 +1,770 @@ +{ + "Age": 63277486, + "PriorityID": "3", + "ServiceID": "", + "DynamicField_SugarCRMCompanySelection": null, + "DynamicField_ITSMDueDate": "2014-11-24 00:15:00", + "DynamicField_Topic": null, + "StateID": "2", + "DynamicField_Hostname": null, + "DynamicField_ZammadMigratorChanged": null, + "EscalationTime": "0", + "Changed": "2014-11-21 00:21:08", + "OwnerID": "3", + "DynamicField_ZarafaTN": null, + "DynamicField_ProcessManagementActivityID": null, + "DynamicField_TopicID": null, + "DynamicField_ScomHostname": null, + "Owner": "agent-2", + "Created": "2014-11-21 00:17:40", + "DynamicField_TicketFreeText11": null, + "DynamicField_ScomUUID": null, + "DynamicField_TicketFreeKey11": null, + "DynamicField_ITSMReviewRequired": "No", + "DynamicField_OpenExchangeTicketNumber": null, + "DynamicField_ITSMDecisionDate": null, + "QueueID": "1", + "CreateBy": "3", + "DynamicField_ITSMImpact": null, + "TicketID": "730", + "DynamicField_ITSMRecoveryStartTime": null, + "SolutionInMin": 0, + "EscalationResponseTime": "0", + "UnlockTimeout": "1416525661", + "DynamicField_ProcessManagementProcessID": null, + "ArchiveFlag": "n", + "DynamicField_CheckboxExample": null, + "SolutionTime": "2014-11-21 00:21:08", + "DynamicField_Location": null, + "CustomerUserID": "BetreuterKunde2", + "DynamicField_Vertriebsweg": null, + "DynamicField_CustomerLocation": null, + "DynamicField_SugarCRMRemoteID": null, + "DynamicField_OpenExchangeTN": null, + "Type": "Incident", + "DynamicField_TETest": null, + "Responsible": "root@localhost", + "ResponsibleID": "1", + "ChangeBy": "3", + "DynamicField_Combine": null, + "RealTillTimeNotUsed": "0", + "GroupID": "1", + "DynamicField_ScomService": null, + "CustomerID": "3333333333", + "TypeID": "1", + "Priority": "3 normal", + "DynamicField_SugarCRMCompanySelectedID": null, + "History": [ + { + "HistoryID": "11291", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:41", + "OwnerID": "1", + "QueueID": "1", + "HistoryType": "NewTicket", + "TypeID": "1", + "HistoryTypeID": "1", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%20141121305000012%%Postmaster%%3 normal%%open%%730" + }, + { + "HistoryID": "11292", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:41", + "OwnerID": "1", + "QueueID": "1", + "HistoryType": "TicketDynamicFieldUpdate", + "TypeID": "1", + "HistoryTypeID": "28", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "1", + "TicketID": "730", + "Name": "%%FieldName%%ZammadMigratorChanged%%Value%%1" + }, + { + "HistoryID": "11293", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:41", + "OwnerID": "1", + "QueueID": "1", + "HistoryType": "ServiceUpdate", + "TypeID": "1", + "HistoryTypeID": "38", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%NULL%%%%NULL%%" + }, + { + "HistoryID": "11294", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:41", + "OwnerID": "1", + "QueueID": "1", + "HistoryType": "SLAUpdate", + "TypeID": "1", + "HistoryTypeID": "39", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%NULL%%%%NULL%%" + }, + { + "HistoryID": "11295", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:41", + "OwnerID": "1", + "QueueID": "1", + "HistoryType": "CustomerUpdate", + "TypeID": "1", + "HistoryTypeID": "21", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%CustomerID=3333333333;CustomerUser=BetreuterKunde2;" + }, + { + "HistoryID": "11296", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:41", + "OwnerID": "1", + "QueueID": "1", + "HistoryType": "TicketDynamicFieldUpdate", + "TypeID": "1", + "HistoryTypeID": "28", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%FieldName%%ITSMDueDate%%Value%%2014-11-24 00:15:00" + }, + { + "HistoryID": "11297", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:41", + "OwnerID": "1", + "QueueID": "1", + "HistoryType": "PhoneCallCustomer", + "TypeID": "1", + "HistoryTypeID": "14", + "ArticleID": "3970", + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%" + }, + { + "HistoryID": "11298", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:42", + "OwnerID": "1", + "QueueID": "1", + "HistoryType": "SendAutoReply", + "TypeID": "1", + "HistoryTypeID": "4", + "ArticleID": "3971", + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%\"Betreuter Kunde\" " + }, + { + "HistoryID": "11299", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:42", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "OwnerUpdate", + "TypeID": "1", + "HistoryTypeID": "23", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%agent-2%%3" + }, + { + "HistoryID": "11300", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:43", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "SendCustomerNotification", + "TypeID": "1", + "HistoryTypeID": "10", + "ArticleID": "3972", + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%kunde2@kunde.de" + }, + { + "HistoryID": "11301", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:43", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "SendAgentNotification", + "TypeID": "1", + "HistoryTypeID": "9", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%TEST%%agent-1@example.com" + }, + { + "HistoryID": "11302", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:17:44", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "SendAgentNotification", + "TypeID": "1", + "HistoryTypeID": "9", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%TEST%%agent-2-for-role-2@example.com" + }, + { + "HistoryID": "11303", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:21:01", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "Lock", + "TypeID": "1", + "HistoryTypeID": "17", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%lock" + }, + { + "HistoryID": "11304", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:21:01", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "Misc", + "TypeID": "1", + "HistoryTypeID": "25", + "ArticleID": 0, + "StateID": "4", + "CreateBy": "3", + "TicketID": "730", + "Name": "Reset of unlock time." + }, + { + "HistoryID": "11305", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:21:08", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "StateUpdate", + "TypeID": "1", + "HistoryTypeID": "27", + "ArticleID": 0, + "StateID": "2", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%open%%closed successful%%" + }, + { + "HistoryID": "11306", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:21:08", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "Unlock", + "TypeID": "1", + "HistoryTypeID": "18", + "ArticleID": 0, + "StateID": "2", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%unlock" + }, + { + "HistoryID": "11307", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:21:08", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "AddNote", + "TypeID": "1", + "HistoryTypeID": "15", + "ArticleID": "3973", + "StateID": "2", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%Close" + }, + { + "HistoryID": "11308", + "PriorityID": "3", + "CreateTime": "2014-11-21 00:21:09", + "OwnerID": "3", + "QueueID": "1", + "HistoryType": "TicketDynamicFieldUpdate", + "TypeID": "1", + "HistoryTypeID": "28", + "ArticleID": 0, + "StateID": "2", + "CreateBy": "3", + "TicketID": "730", + "Name": "%%FieldName%%ITSMReviewRequired%%Value%%No" + } + ], + "UntilTime": 0, + "EscalationUpdateTime": "0", + "DynamicField_ITSMRepairStartTime": null, + "Queue": "Postmaster", + "State": "closed successful", + "Closed": "2014-11-21 00:21:08", + "Title": null, + "DynamicField_ZammadMigratorChangedOld": "1", + "DynamicField_ScomState": null, + "FirstLock": "2014-11-21 00:21:01", + "DynamicField_Department": null, + "Articles": [ + { + "Age": 63277486, + "PriorityID": "3", + "ContentType": "text/plain; charset=utf-8", + "AttachmentIDOfHTMLBody": "1", + "DynamicField_SugarCRMCompanySelection": null, + "ServiceID": null, + "TicketFreeText11": null, + "DynamicField_ITSMDueDate": "2014-11-24 00:15:00", + "DynamicField_Topic": null, + "StateID": "2", + "DynamicField_Hostname": null, + "Body": "test #3", + "DynamicField_ZammadMigratorChanged": null, + "EscalationTime": "0", + "Changed": "2014-11-21 00:21:08", + "OwnerID": "3", + "DynamicField_ZarafaTN": null, + "DynamicField_ProcessManagementActivityID": null, + "DynamicField_TopicID": null, + "DynamicField_ScomHostname": null, + "Owner": "agent-2", + "AgeTimeUnix": 63277485, + "TicketFreeKey11": null, + "ArticleID": "3970", + "Created": "2014-11-21 00:17:41", + "DynamicField_ScomUUID": null, + "DynamicField_TicketFreeText11": null, + "DynamicField_TicketFreeKey11": null, + "DynamicField_ITSMReviewRequired": "No", + "DynamicField_OpenExchangeTicketNumber": null, + "DynamicField_ITSMDecisionDate": null, + "ArticleTypeID": "5", + "QueueID": "1", + "ReplyTo": "", + "DynamicField_ITSMImpact": null, + "TicketID": "730", + "DynamicField_ITSMRecoveryStartTime": null, + "Cc": "", + "EscalationResponseTime": "0", + "DynamicField_ProcessManagementProcessID": null, + "IncomingTime": "1416525461", + "Charset": "utf-8", + "DynamicField_CheckboxExample": null, + "DynamicField_Location": null, + "CustomerUserID": "BetreuterKunde2", + "DynamicField_Vertriebsweg": null, + "Attachments": [ + { + "ContentAlternative": "", + "ContentID": "", + "Disposition": "inline", + "Filesize": "201 Bytes", + "ContentType": "text/html; charset=\"utf-8\"", + "Filename": "ZmlsZS0y\n", + "FilesizeRaw": "201", + "Content": "PCFET0NUWVBFIGh0bWw+PGh0bWw+PGhlYWQ+PG1ldGEgaHR0cC1lcXVpdj0iQ29udGVudC1UeXBl\nIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9dXRmLTgiLz48L2hlYWQ+PGJvZHkgc3R5bGU9\nImZvbnQtZmFtaWx5OkdlbmV2YSxIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZjsgZm9udC1zaXpl\nOiAxMnB4OyI+dGVzdCAjMzwvYm9keT48L2h0bWw+\n" + } + ], + "DynamicField_CustomerLocation": null, + "DynamicField_SugarCRMRemoteID": null, + "DynamicField_OpenExchangeTN": null, + "Service": "", + "Type": "Incident", + "ContentCharset": "utf-8", + "DynamicField_TETest": null, + "Responsible": "root@localhost", + "SenderType": "customer", + "ResponsibleID": "1", + "SLA": "", + "MimeType": "text/plain", + "DynamicField_Combine": null, + "Subject": "test #3", + "InReplyTo": "", + "RealTillTimeNotUsed": "0", + "DynamicField_ScomService": null, + "CustomerID": "3333333333", + "TypeID": "1", + "MessageID": "", + "Priority": "3 normal", + "To": "Postmaster", + "DynamicField_SugarCRMCompanySelectedID": null, + "UntilTime": 0, + "EscalationUpdateTime": "0", + "CreatedBy": "3", + "Queue": "Postmaster", + "DynamicField_ITSMRepairStartTime": null, + "ToRealname": "Postmaster", + "State": "closed successful", + "SenderTypeID": "3", + "DynamicField_ZammadMigratorChangedOld": "1", + "Title": "test #3", + "DynamicField_ScomState": null, + "References": "", + "DynamicField_Department": null, + "ArticleType": "phone", + "StateType": "closed", + "FromRealname": "Betreuter Kunde", + "EscalationSolutionTime": "0", + "LockID": "1", + "TicketNumber": "20141121305000012", + "DynamicField_ITSMDecisionResult": null, + "Lock": "unlock", + "CreateTimeUnix": "1416525460", + "SLAID": null, + "DynamicField_ITSMCriticality": null, + "From": "\"Betreuter Kunde\" ," + }, + { + "Age": 63277486, + "PriorityID": "3", + "ContentType": "text/plain; charset=utf-8", + "AttachmentIDOfHTMLBody": "1", + "DynamicField_SugarCRMCompanySelection": null, + "ServiceID": null, + "TicketFreeText11": null, + "DynamicField_ITSMDueDate": "2014-11-24 00:15:00", + "DynamicField_Topic": null, + "StateID": "2", + "DynamicField_Hostname": null, + "Body": "This is a demo text which is send to every inquiry.\nIt could contain something like:\n\nThanks for your email. A new ticket has been created.\n\nYou wrote:\n> test #3\n\n\nYour email will be answered by a human ASAP\n\nHave fun with OTRS! :-)\n\nYour OTRS Team\n", + "DynamicField_ZammadMigratorChanged": null, + "EscalationTime": "0", + "Changed": "2014-11-21 00:21:08", + "OwnerID": "3", + "DynamicField_ZarafaTN": null, + "DynamicField_ProcessManagementActivityID": null, + "DynamicField_TopicID": null, + "DynamicField_ScomHostname": null, + "Owner": "agent-2", + "AgeTimeUnix": 63277484, + "TicketFreeKey11": null, + "ArticleID": "3971", + "Created": "2014-11-21 00:17:42", + "DynamicField_ScomUUID": null, + "DynamicField_TicketFreeText11": null, + "DynamicField_TicketFreeKey11": null, + "DynamicField_ITSMReviewRequired": "No", + "DynamicField_OpenExchangeTicketNumber": null, + "DynamicField_ITSMDecisionDate": null, + "ArticleTypeID": "1", + "QueueID": "1", + "ReplyTo": "", + "DynamicField_ITSMImpact": null, + "TicketID": "730", + "DynamicField_ITSMRecoveryStartTime": null, + "Cc": "", + "EscalationResponseTime": "0", + "DynamicField_ProcessManagementProcessID": null, + "IncomingTime": "1416525462", + "Charset": "utf-8", + "DynamicField_CheckboxExample": null, + "DynamicField_Location": null, + "CustomerUserID": "BetreuterKunde2", + "DynamicField_Vertriebsweg": null, + "Attachments": [ + { + "ContentAlternative": "", + "ContentID": "", + "Disposition": "inline", + "Filesize": "591 Bytes", + "ContentType": "text/html; charset=\"utf-8\"", + "Filename": "ZmlsZS0y\n", + "FilesizeRaw": "591", + "Content": "PCFET0NUWVBFIGh0bWw+PGh0bWw+PGhlYWQ+PG1ldGEgaHR0cC1lcXVpdj0iQ29udGVudC1UeXBl\nIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9dXRmLTgiLz48L2hlYWQ+PGJvZHkgc3R5bGU9\nImZvbnQtZmFtaWx5OkdlbmV2YSxIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZjsgZm9udC1zaXpl\nOiAxMnB4OyI+VGhpcyBpcyBhIGRlbW8gdGV4dCB3aGljaCBpcyBzZW5kIHRvIGV2ZXJ5IGlucXVp\ncnkuPGJyLz5JdCBjb3VsZCBjb250YWluIHNvbWV0aGluZyBsaWtlOjxici8+PGJyLz5UaGFua3Mg\nZm9yIHlvdXIgZW1haWwuIEEgbmV3IHRpY2tldCBoYXMgYmVlbiBjcmVhdGVkLjxici8+PGJyLz5Z\nb3Ugd3JvdGU6PGJyLz48ZGl2ICB0eXBlPSJjaXRlIiBzdHlsZT0iYm9yZGVyOm5vbmU7Ym9yZGVy\nLWxlZnQ6c29saWQgYmx1ZSAxLjVwdDtwYWRkaW5nOjBjbSAwY20gMGNtIDQuMHB0Ij50ZXN0ICMz\nPC9kaXY+PGJyLz48YnIvPllvdXIgZW1haWwgd2lsbCBiZSBhbnN3ZXJlZCBieSBhIGh1bWFuIEFT\nQVA8YnIvPjxici8+SGF2ZSBmdW4gd2l0aCBPVFJTISA6LSk8YnIvPjxici8+WW91ciBPVFJTIFRl\nYW08YnIvPjwvYm9keT48L2h0bWw+\n" + } + ], + "DynamicField_CustomerLocation": null, + "DynamicField_SugarCRMRemoteID": null, + "DynamicField_OpenExchangeTN": null, + "Service": "", + "Type": "Incident", + "ContentCharset": "utf-8", + "DynamicField_TETest": null, + "Responsible": "root@localhost", + "SenderType": "system", + "ResponsibleID": "1", + "SLA": "", + "MimeType": "text/plain", + "DynamicField_Combine": null, + "Subject": "[Ticket#20141121305000012] RE: test #3", + "InReplyTo": "", + "RealTillTimeNotUsed": "0", + "DynamicField_ScomService": null, + "CustomerID": "3333333333", + "TypeID": "1", + "MessageID": "<1416525462.277752.866124677.730.3@yourhost.example.com>", + "Priority": "3 normal", + "To": "\"Betreuter Kunde\" ", + "DynamicField_SugarCRMCompanySelectedID": null, + "UntilTime": 0, + "EscalationUpdateTime": "0", + "CreatedBy": "3", + "Queue": "Postmaster", + "DynamicField_ITSMRepairStartTime": null, + "ToRealname": "Betreuter Kunde", + "State": "closed successful", + "SenderTypeID": "2", + "DynamicField_ZammadMigratorChangedOld": "1", + "Title": "test #3", + "DynamicField_ScomState": null, + "References": "", + "DynamicField_Department": null, + "ArticleType": "email-external", + "StateType": "closed", + "FromRealname": "OTRS System", + "EscalationSolutionTime": "0", + "LockID": "1", + "TicketNumber": "20141121305000012", + "DynamicField_ITSMDecisionResult": null, + "Lock": "unlock", + "CreateTimeUnix": "1416525460", + "SLAID": null, + "DynamicField_ITSMCriticality": null, + "From": "OTRS System " + }, + { + "Age": 63277486, + "PriorityID": "3", + "ContentType": "text/plain; charset=utf-8", + "DynamicField_SugarCRMCompanySelection": null, + "ServiceID": null, + "TicketFreeText11": null, + "DynamicField_ITSMDueDate": "2014-11-24 00:15:00", + "DynamicField_Topic": null, + "StateID": "2", + "DynamicField_Hostname": null, + "Body": "CustomerUser: Betreuter Kunde\nCustomerUser Email: kunde2@kunde.de\nPartner Email: \"Betreuter Kunde\" ,", + "DynamicField_ZammadMigratorChanged": null, + "EscalationTime": "0", + "Changed": "2014-11-21 00:21:08", + "OwnerID": "3", + "DynamicField_ZarafaTN": null, + "DynamicField_ProcessManagementActivityID": null, + "DynamicField_TopicID": null, + "DynamicField_ScomHostname": null, + "Owner": "agent-2", + "AgeTimeUnix": 63277483, + "TicketFreeKey11": null, + "ArticleID": "3972", + "Created": "2014-11-21 00:17:43", + "DynamicField_ScomUUID": null, + "DynamicField_TicketFreeText11": null, + "DynamicField_TicketFreeKey11": null, + "DynamicField_ITSMReviewRequired": "No", + "DynamicField_OpenExchangeTicketNumber": null, + "DynamicField_ITSMDecisionDate": null, + "ArticleTypeID": "3", + "QueueID": "1", + "ReplyTo": "", + "DynamicField_ITSMImpact": null, + "TicketID": "730", + "DynamicField_ITSMRecoveryStartTime": null, + "Cc": "", + "EscalationResponseTime": "0", + "DynamicField_ProcessManagementProcessID": null, + "IncomingTime": "1416525463", + "Charset": "utf-8", + "DynamicField_CheckboxExample": null, + "DynamicField_Location": null, + "CustomerUserID": "BetreuterKunde2", + "DynamicField_Vertriebsweg": null, + "Attachments": [], + "DynamicField_CustomerLocation": null, + "DynamicField_SugarCRMRemoteID": null, + "DynamicField_OpenExchangeTN": null, + "Service": "", + "Type": "Incident", + "ContentCharset": "utf-8", + "DynamicField_TETest": null, + "Responsible": "root@localhost", + "SenderType": "system", + "ResponsibleID": "1", + "SLA": "", + "MimeType": "text/plain", + "DynamicField_Combine": null, + "Subject": "[Ticket#20141121305000012] EventNotification: test #3", + "InReplyTo": "", + "RealTillTimeNotUsed": "0", + "DynamicField_ScomService": null, + "CustomerID": "3333333333", + "TypeID": "1", + "MessageID": "<1416525463.523919.240611208.730.3@yourhost.example.com>", + "Priority": "3 normal", + "To": "kunde2@kunde.de", + "DynamicField_SugarCRMCompanySelectedID": null, + "UntilTime": 0, + "EscalationUpdateTime": "0", + "CreatedBy": "3", + "Queue": "Postmaster", + "DynamicField_ITSMRepairStartTime": null, + "ToRealname": "kunde2@kunde.de", + "State": "closed successful", + "SenderTypeID": "2", + "DynamicField_ZammadMigratorChangedOld": "1", + "Title": "test #3", + "DynamicField_ScomState": null, + "References": "", + "DynamicField_Department": null, + "ArticleType": "email-notification-ext", + "StateType": "closed", + "FromRealname": "OTRS System", + "EscalationSolutionTime": "0", + "LockID": "1", + "TicketNumber": "20141121305000012", + "DynamicField_ITSMDecisionResult": null, + "Lock": "unlock", + "CreateTimeUnix": "1416525460", + "SLAID": null, + "DynamicField_ITSMCriticality": null, + "From": "OTRS System " + }, + { + "Age": 63277486, + "PriorityID": "3", + "ContentType": "text/plain; charset=utf-8", + "AttachmentIDOfHTMLBody": "1", + "DynamicField_SugarCRMCompanySelection": null, + "ServiceID": null, + "TicketFreeText11": null, + "DynamicField_ITSMDueDate": "2014-11-24 00:15:00", + "DynamicField_Topic": null, + "StateID": "2", + "DynamicField_Hostname": null, + "Body": "close", + "DynamicField_ZammadMigratorChanged": null, + "EscalationTime": "0", + "Changed": "2014-11-21 00:21:08", + "OwnerID": "3", + "DynamicField_ZarafaTN": null, + "DynamicField_ProcessManagementActivityID": null, + "DynamicField_TopicID": null, + "DynamicField_ScomHostname": null, + "Owner": "agent-2", + "AgeTimeUnix": 63277278, + "TicketFreeKey11": null, + "ArticleID": "3973", + "Created": "2014-11-21 00:21:08", + "DynamicField_ScomUUID": null, + "DynamicField_TicketFreeText11": null, + "DynamicField_TicketFreeKey11": null, + "DynamicField_ITSMReviewRequired": "No", + "DynamicField_OpenExchangeTicketNumber": null, + "DynamicField_ITSMDecisionDate": null, + "ArticleTypeID": "9", + "QueueID": "1", + "ReplyTo": "", + "DynamicField_ITSMImpact": null, + "TicketID": "730", + "DynamicField_ITSMRecoveryStartTime": null, + "Cc": "", + "EscalationResponseTime": "0", + "DynamicField_ProcessManagementProcessID": null, + "IncomingTime": "1416525668", + "Charset": "utf-8", + "DynamicField_CheckboxExample": null, + "DynamicField_Location": null, + "CustomerUserID": "BetreuterKunde2", + "DynamicField_Vertriebsweg": null, + "Attachments": [ + { + "ContentAlternative": "", + "ContentID": "", + "Disposition": "inline", + "Filesize": "199 Bytes", + "ContentType": "text/html; charset=\"utf-8\"", + "Filename": "ZmlsZS0y\n", + "FilesizeRaw": "199", + "Content": "PCFET0NUWVBFIGh0bWw+PGh0bWw+PGhlYWQ+PG1ldGEgaHR0cC1lcXVpdj0iQ29udGVudC1UeXBl\nIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9dXRmLTgiLz48L2hlYWQ+PGJvZHkgc3R5bGU9\nImZvbnQtZmFtaWx5OkdlbmV2YSxIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZjsgZm9udC1zaXpl\nOiAxMnB4OyI+Y2xvc2U8L2JvZHk+PC9odG1sPg==\n" + } + ], + "DynamicField_CustomerLocation": null, + "DynamicField_SugarCRMRemoteID": null, + "DynamicField_OpenExchangeTN": null, + "Service": "", + "Type": "Incident", + "ContentCharset": "utf-8", + "DynamicField_TETest": null, + "Responsible": "root@localhost", + "SenderType": "agent", + "ResponsibleID": "1", + "SLA": "", + "MimeType": "text/plain", + "DynamicField_Combine": null, + "Subject": "Close", + "InReplyTo": "", + "RealTillTimeNotUsed": "0", + "DynamicField_ScomService": null, + "CustomerID": "3333333333", + "TypeID": "1", + "MessageID": "", + "Priority": "3 normal", + "To": "", + "DynamicField_SugarCRMCompanySelectedID": null, + "UntilTime": 0, + "EscalationUpdateTime": "0", + "CreatedBy": "3", + "Queue": "Postmaster", + "DynamicField_ITSMRepairStartTime": null, + "State": "closed successful", + "SenderTypeID": "1", + "DynamicField_ZammadMigratorChangedOld": "1", + "Title": "test #3", + "DynamicField_ScomState": null, + "References": "", + "DynamicField_Department": null, + "ArticleType": "note-internal", + "StateType": "closed", + "FromRealname": "agent-2 firstname äöüß agent-2 lastname äöüß", + "EscalationSolutionTime": "0", + "LockID": "1", + "TicketNumber": "20141121305000012", + "DynamicField_ITSMDecisionResult": null, + "Lock": "unlock", + "CreateTimeUnix": "1416525460", + "SLAID": null, + "DynamicField_ITSMCriticality": null, + "From": "\"agent-2 firstname äöüß agent-2 lastname äöüß\" " + } + ], + "StateType": "closed", + "EscalationSolutionTime": "0", + "LockID": "1", + "TicketNumber": "20141121305000012", + "DynamicField_ITSMDecisionResult": null, + "CreateTimeUnix": "1416525460", + "Lock": "unlock", + "SLAID": "", + "DynamicField_ITSMCriticality": null +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/user/default.json b/spec/fixtures/import/otrs/user/default.json new file mode 100644 index 000000000..b13dc5baa --- /dev/null +++ b/spec/fixtures/import/otrs/user/default.json @@ -0,0 +1,135 @@ +{ + "OutOfOffice": "1", + "OutOfOfficeStartMonth": "9", + "UserStoredFilterColumns-AgentTicketLockedView": "{}", + "UserTicketOverviewSmallPageShown": "35", + "UserCreateWorkOrderNextMask": "AgentITSMWorkOrderZoom", + "OutOfOfficeEndYear": "2014", + "UserDashboardTicketGenericFilter0110-TicketEscalation": "All", + "UserDashboardPref0120-TicketNew-Columns": "{\"Columns\":{\"Changed\":0,\"CustomerID\":0,\"CustomerName\":0,\"CustomerUserID\":0,\"DynamicField_CustomerLocation\":0,\"EscalationResponseTime\":0,\"EscalationSolutionTime\":0,\"EscalationTime\":0,\"EscalationUpdateTime\":0,\"Lock\":0,\"Owner\":0,\"PendingTime\":0,\"Priority\":0,\"Responsible\":0,\"SLA\":0,\"State\":0,\"Type\":0,\"Age\":1,\"Title\":1,\"Queue\":1,\"Service\":1,\"TicketNumber\":1},\"Order\":[\"Age\",\"Title\",\"Queue\",\"Service\",\"TicketNumber\"]}", + "UserLastUsedZoomViewType": "", + "OutOfOfficeStartDay": "10", + "UserStoredFilterColumns-AgentTicketStatusView": "{}", + "UserTitle": null, + "UserLastname": "OTRS", + "UserTicketOverviewMediumPageShown": "20", + "OutOfOfficeEndDay": "12", + "CreateTime": "2014-04-28 10:53:18", + "UserTicketOverviewPreviewPageShown": "15", + "UserLogin": "root@localhost", + "UserFilterColumnsEnabled-AgentTicketEscalationView": "[\"TicketNumber\",\"Age\",\"EscalationTime\",\"EscalationResponseTime\",\"EscalationSolutionTime\",\"EscalationUpdateTime\",\"Title\",\"State\",\"Lock\",\"Queue\",\"Owner\",\"CustomerID\"]", + "UserLanguage": "de", + "UserDashboardPref0110-TicketEscalation-Columns": "{\"Columns\":{\"Changed\":0,\"CustomerID\":0,\"CustomerUserID\":0,\"EscalationResponseTime\":0,\"EscalationSolutionTime\":0,\"EscalationTime\":0,\"EscalationUpdateTime\":0,\"Lock\":0,\"Owner\":0,\"PendingTime\":0,\"Priority\":0,\"Queue\":0,\"Responsible\":0,\"SLA\":0,\"Service\":0,\"State\":0,\"Type\":0,\"Age\":1,\"Title\":1,\"CustomerName\":1,\"TicketNumber\":1},\"Order\":[\"Age\",\"Title\",\"CustomerName\",\"TicketNumber\"]}", + "OutOfOfficeStartYear": "2014", + "UserDashboardPref0120-TicketNew-Shown": "10", + "UserFullname": "Admin OTRS", + "UserLastLoginTimestamp": "2016-08-10 19:37:44", + "UserLastLogin": "1470850664", + "UserMarkTicketUnseenRedirectURL": "Action=AgentTicketZoom;TicketID=###TicketID####1", + "AdminDynamicFieldsOverviewPageShown": "35", + "UserChangeOverviewSmallPageShown": "25", + "RoleIDs": [], + "ValidID": "1", + "UserStoredFilterColumns-AgentTicketQueue": "{}", + "UserEmail": "root@localhost", + "UserRefreshTime": "0", + "UserDashboardPref0130-TicketOpen-Shown": "10", + "UserTicketOverviewAgentTicketQueue": "Small", + "UserID": "1", + "UserDashboardTicketGenericColumnFiltersRealKeys0120-TicketNew": "{\"QueueIDs\":[\"1\"]}", + "wpt22": "1", + "UserMarkTicketSeenRedirectURL": "Action=AgentTicketZoom;TicketID=###TicketID####1", + "UserStoredFilterColumns-AgentTicketEscalationView": "{}", + "UserDashboardTicketGenericFilter0120-TicketNew": "MyQueues", + "UserCreateNextMask": "", + "UserFirstname": "Admin", + "UserPw": "9faaba2ab242a99bbb6992e9424386375f6757c17e6484ae570f39d9cad9f28ea", + "UserDashboardPref0110-TicketEscalation-Shown": "10", + "UserFilterColumnsEnabled-AgentTicketQueue": "[\"TicketNumber\",\"Age\",\"Title\",\"State\",\"Lock\",\"DynamicField_CustomerLocation\",\"Queue\",\"Owner\",\"CustomerID\",\"DynamicField_Hostname\"]", + "OutOfOfficeEndMonth": "9", + "ChangeTime": "2014-04-28 10:53:18", + "UserDashboardPref0130-TicketOpen-Columns": "{\"Columns\":{\"Changed\":0,\"CustomerID\":0,\"CustomerUserID\":0,\"EscalationResponseTime\":0,\"EscalationTime\":0,\"EscalationUpdateTime\":0,\"Lock\":0,\"Owner\":0,\"PendingTime\":0,\"Priority\":0,\"Queue\":0,\"Responsible\":0,\"SLA\":0,\"Service\":0,\"State\":0,\"Type\":0,\"Age\":1,\"DynamicField_CustomerLocation\":1,\"Title\":1,\"CustomerName\":1,\"EscalationSolutionTime\":1,\"TicketNumber\":1},\"Order\":[\"Age\",\"DynamicField_CustomerLocation\",\"Title\",\"CustomerName\",\"EscalationSolutionTime\",\"TicketNumber\"]}", + "UserTicketOverviewAgentTicketSearch": "Small", + "UserTicketOverviewAgentCustomerSearch": "Small", + "UserDashboardTicketGenericColumnFilters0120-TicketNew": "{\"Queue\":\"1\"}", + "GroupIDs": { + "6": [ + "ro", + "move_into", + "create", + "note", + "owner", + "priority", + "rw" + ], + "3": [ + "ro", + "move_into", + "create", + "note", + "owner", + "priority", + "rw" + ], + "7": [ + "ro", + "move_into", + "create", + "note", + "owner", + "priority", + "rw" + ], + "2": [ + "ro", + "move_into", + "create", + "note", + "owner", + "priority", + "rw" + ], + "8": [ + "ro", + "move_into", + "create", + "note", + "owner", + "priority", + "rw" + ], + "1": [ + "ro", + "move_into", + "create", + "note", + "owner", + "priority", + "rw" + ], + "4": [ + "ro", + "move_into", + "create", + "note", + "owner", + "priority", + "rw" + ], + "5": [ + "ro", + "move_into", + "create", + "note", + "owner", + "priority", + "rw" + ] + }, + "UserConfigItemOverviewSmallPageShown": "25", + "UserAuthBackend": "", + "UserTicketOverviewAgentTicketLockedView": "Small", + "UserTicketOverviewAgentTicketEscalationView": "Small", + "UserTicketOverviewAgentTicketStatusView": "Small", + "UserLoginFailed": "0" +} \ No newline at end of file diff --git a/spec/fixtures/import/otrs/user/no_groups.json b/spec/fixtures/import/otrs/user/no_groups.json new file mode 100644 index 000000000..24022d981 --- /dev/null +++ b/spec/fixtures/import/otrs/user/no_groups.json @@ -0,0 +1,36 @@ +{ + "OutOfOffice": "0", + "OutOfOfficeStartMonth": "11", + "UserTicketOverviewSmallPageShown": "25", + "UserCreateWorkOrderNextMask": "AgentITSMWorkOrderZoom", + "OutOfOfficeEndYear": "2014", + "OutOfOfficeStartDay": "14", + "UserTitle": "", + "UserLastname": "agent-2-for-role-2", + "UserTicketOverviewMediumPageShown": "20", + "OutOfOfficeEndDay": "15", + "CreateTime": "2014-11-14 00:53:20", + "UserTicketOverviewPreviewPageShown": "15", + "UserLogin": "agent-2-for-role-2", + "UserLanguage": "en", + "OutOfOfficeStartYear": "2014", + "UserFullname": "agent-2-for-role-2 agent-2-for-role-2", + "UserChangeOverviewSmallPageShown": "25", + "AdminDynamicFieldsOverviewPageShown": "25", + "RoleIDs": [ + "3" + ], + "ValidID": "1", + "UserRefreshTime": "0", + "UserComment": "", + "UserEmail": "agent-2-for-role-2@example.com", + "UserID": "6", + "UserCreateNextMask": "", + "UserFirstname": "agent-2-for-role-2", + "UserPw": "9edb001ad7900daea0622d89225c9ca729749fd12ae5ea044f072d1b7c56c8cc", + "OutOfOfficeEndMonth": "11", + "ChangeTime": "2014-11-14 00:53:20", + "GroupIDs": {}, + "UserSkin": "default", + "UserConfigItemOverviewSmallPageShown": "25" +} \ No newline at end of file diff --git a/spec/import/base_factory_examples.rb b/spec/import/base_factory_examples.rb new file mode 100644 index 000000000..ca15fafcd --- /dev/null +++ b/spec/import/base_factory_examples.rb @@ -0,0 +1,24 @@ +require 'import/import_factory_examples' + +RSpec.shared_examples 'Import::BaseFactory' do + it_behaves_like 'Import factory' + + it 'responds to pre_import_hook' do + expect(described_class).to respond_to('pre_import_hook') + end + it 'responds to backend_class' do + expect(described_class).to respond_to('backend_class') + end + it 'responds to skip?' do + expect(described_class).to respond_to('skip?') + end +end + +RSpec.shared_examples 'Import::BaseFactory extender' do + it 'calls new on determined backend object' do + record = double() + expect(described_class).to receive(:backend_class).and_return(Class) + expect(Class).to receive(:new).with(record) + described_class.import([record]) + end +end diff --git a/spec/import/base_factory_spec.rb b/spec/import/base_factory_spec.rb new file mode 100644 index 000000000..39462d826 --- /dev/null +++ b/spec/import/base_factory_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/factory_examples' + +RSpec.describe Import::Factory do + it_behaves_like 'Import::Factory' +end diff --git a/spec/import/factory_examples.rb b/spec/import/factory_examples.rb new file mode 100644 index 000000000..ef43fb1a9 --- /dev/null +++ b/spec/import/factory_examples.rb @@ -0,0 +1,5 @@ +require 'import/base_factory_examples' + +RSpec.shared_examples 'Import::Factory' do + it_behaves_like 'Import::BaseFactory' +end diff --git a/spec/import/factory_spec.rb b/spec/import/factory_spec.rb new file mode 100644 index 000000000..397a035bd --- /dev/null +++ b/spec/import/factory_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' +require 'import/factory_examples' + +RSpec.describe Import::Factory do + it_behaves_like 'Import::Factory' + it_behaves_like 'Import::BaseFactory extender' +end diff --git a/spec/import/helper_examples.rb b/spec/import/helper_examples.rb new file mode 100644 index 000000000..6238fbb6a --- /dev/null +++ b/spec/import/helper_examples.rb @@ -0,0 +1,18 @@ +RSpec.shared_examples 'Import::Helper' do + + it 'responds to check_import_mode' do + expect(described_class).to respond_to('check_import_mode') + end + + it 'responds to log' do + expect(described_class).to respond_to('log') + end + + it 'responds to utf8_encode' do + expect(described_class).to respond_to('utf8_encode') + end + + it 'responds to reset_primary_key_sequence' do + expect(described_class).to respond_to('reset_primary_key_sequence') + end +end diff --git a/spec/import/helper_spec.rb b/spec/import/helper_spec.rb new file mode 100644 index 000000000..4194c3470 --- /dev/null +++ b/spec/import/helper_spec.rb @@ -0,0 +1,16 @@ +require 'rails_helper' +require 'import/helper_examples' + +RSpec.describe Import::Helper do + it_behaves_like 'Import::Helper' + + it 'checks if import_mode is active' do + expect(Setting).to receive(:get).with('import_mode').and_return(true) + expect( described_class.check_import_mode ).to be true + end + + it 'throws an exception if import_mode is disabled' do + expect(Setting).to receive(:get).with('import_mode').and_return(false) + expect { described_class.check_import_mode }.to raise_error(RuntimeError) + end +end diff --git a/spec/import/import_factory_examples.rb b/spec/import/import_factory_examples.rb new file mode 100644 index 000000000..258f8d433 --- /dev/null +++ b/spec/import/import_factory_examples.rb @@ -0,0 +1,5 @@ +RSpec.shared_examples 'Import factory' do + it 'responds to import' do + expect(described_class).to respond_to('import') + end +end diff --git a/spec/import/importer_examples.rb b/spec/import/importer_examples.rb new file mode 100644 index 000000000..2bd0b87c9 --- /dev/null +++ b/spec/import/importer_examples.rb @@ -0,0 +1,5 @@ +RSpec.shared_examples 'Import backend' do + it 'responds to start' do + expect(described_class).to respond_to('start') + end +end diff --git a/spec/import/otrs/article/attachment_factory_spec.rb b/spec/import/otrs/article/attachment_factory_spec.rb new file mode 100644 index 000000000..1d5047685 --- /dev/null +++ b/spec/import/otrs/article/attachment_factory_spec.rb @@ -0,0 +1,56 @@ +require 'rails_helper' +require 'import/import_factory_examples' + +RSpec.describe Import::OTRS::Article::AttachmentFactory do + it_behaves_like 'Import factory' + + def load_attachment_json(file) + json_fixture("import/otrs/article/attachment/#{file}") + end + + let(:local_article) { instance_double(Ticket::Article, ticket_id: 1337, id: 42) } + let(:attachments) { + [ + load_attachment_json('default'), + load_attachment_json('default'), + load_attachment_json('default') + ] + } + let(:start_import) { + described_class.import( + attachments: attachments, + local_article: local_article + ) + } + + def import_expectations + expect(Store).to receive(:add).exactly(3).times.with(hash_including( + object: 'Ticket::Article', + o_id: local_article.id, + )) + end + + def article_attachment_expectations(article_attachments) + expect(local_article).to receive(:attachments).and_return(article_attachments) + end + + it 'imports' do + article_attachment_expectations([]) + import_expectations + start_import + end + + it 'deletes old and reimports' do + dummy_attachment = double() + expect(dummy_attachment).to receive(:delete) + article_attachment_expectations([dummy_attachment]) + import_expectations + start_import + end + + it 'skips import for same count' do + article_attachment_expectations([1, 2, 3]) + expect(Store).not_to receive(:add) + start_import + end +end diff --git a/spec/import/otrs/article_customer_factory_spec.rb b/spec/import/otrs/article_customer_factory_spec.rb new file mode 100644 index 000000000..69cc180b8 --- /dev/null +++ b/spec/import/otrs/article_customer_factory_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/factory_examples' + +RSpec.describe Import::OTRS::ArticleCustomerFactory do + it_behaves_like 'Import::Factory' +end diff --git a/spec/import/otrs/article_customer_sepc.rb b/spec/import/otrs/article_customer_sepc.rb new file mode 100644 index 000000000..45396124a --- /dev/null +++ b/spec/import/otrs/article_customer_sepc.rb @@ -0,0 +1,39 @@ +require 'rails_helper' + +RSpec.describe Import::OTRS::ArticleCustomer do + + def load_article_json(file) + json_fixture("import/otrs/article/#{file}") + end + + let(:instance_id) { 1337 } + let(:existing_object) { instance_double(import_object) } + let(:import_object) { User } + let(:object_structure) { load_article_json('customer_phone') } + let(:start_import_test) { described_class.new(object_structure) } + + it 'finds customers by email' do + expect(import_object).to receive(:find_by).with(email: 'kunde2@kunde.de').and_return(existing_object) + expect(existing_object).to receive(:id).and_return(instance_id) + expect(import_object).not_to receive(:create) + start_import_test + expect(object_structure['created_by_id']).to eq(instance_id) + end + + it 'finds customers by login' do + expect(import_object).to receive(:find_by).with(email: 'kunde2@kunde.de') + expect(import_object).to receive(:find_by).with(login: 'kunde2@kunde.de').and_return(existing_object) + expect(existing_object).to receive(:id).and_return(instance_id) + expect(import_object).not_to receive(:create) + start_import_test + expect(object_structure['created_by_id']).to eq(instance_id) + end + + it 'creates customers' do + expect(import_object).to receive(:find_by).at_least(:once) + expect(import_object).to receive(:create).and_return(existing_object) + expect(existing_object).to receive(:id).and_return(instance_id) + start_import_test + expect(object_structure['created_by_id']).to eq(instance_id) + end +end diff --git a/spec/import/otrs/article_factory_spec.rb b/spec/import/otrs/article_factory_spec.rb new file mode 100644 index 000000000..f6a943228 --- /dev/null +++ b/spec/import/otrs/article_factory_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/factory_examples' + +RSpec.describe Import::OTRS::ArticleFactory do + it_behaves_like 'Import::Factory' +end diff --git a/spec/import/otrs/article_spec.rb b/spec/import/otrs/article_spec.rb new file mode 100644 index 000000000..2fa9cb39e --- /dev/null +++ b/spec/import/otrs/article_spec.rb @@ -0,0 +1,79 @@ +require 'rails_helper' + +RSpec.describe Import::OTRS::Article do + + def creates_with(zammad_structure) + expect(import_object).to receive(:new).with(zammad_structure).and_call_original + expect_any_instance_of(import_object).to receive(:save) + expect_any_instance_of(described_class).to receive(:reset_primary_key_sequence) + start_import_test + end + + def updates_with(zammad_structure) + expect(import_object).to receive(:find_by).and_return(existing_object) + expect(existing_object).to receive(:update_attributes).with(zammad_structure) + expect(import_object).not_to receive(:new) + start_import_test + end + + def load_article_json(file) + json_fixture("import/otrs/article/#{file}") + end + + let(:import_object) { Ticket::Article } + let(:existing_object) { instance_double(import_object) } + let(:start_import_test) { described_class.new(object_structure) } + + context 'customer phone' do + + let(:object_structure) { load_article_json('customer_phone_attachment') } + + it 'creates' do + zammad_structure = { + created_by_id: '3', + updated_by_id: 1, + ticket_id: '730', + id: '3970', + body: 'test #3', + from: '"Betreuter Kunde" ,', + to: 'Postmaster', + cc: '', + subject: 'test #3', + in_reply_to: '', + message_id: '', + references: '', + updated_at: '2014-11-21 00:21:08', + created_at: '2014-11-21 00:17:41', + type_id: 5, + internal: false, + sender_id: 2 + } + expect(Import::OTRS::Article::AttachmentFactory).to receive(:import) + creates_with(zammad_structure) + end + + it 'updates' do + zammad_structure = { + created_by_id: '3', + updated_by_id: 1, + ticket_id: '730', + id: '3970', + body: 'test #3', + from: '"Betreuter Kunde" ,', + to: 'Postmaster', + cc: '', + subject: 'test #3', + in_reply_to: '', + message_id: '', + references: '', + updated_at: '2014-11-21 00:21:08', + created_at: '2014-11-21 00:17:41', + type_id: 5, + internal: false, + sender_id: 2 + } + expect(Import::OTRS::Article::AttachmentFactory).to receive(:import) + updates_with(zammad_structure) + end + end +end diff --git a/spec/import/otrs/async_examples.rb b/spec/import/otrs/async_examples.rb new file mode 100644 index 000000000..4ae347102 --- /dev/null +++ b/spec/import/otrs/async_examples.rb @@ -0,0 +1,9 @@ +RSpec.shared_examples 'Import::OTRS::Async' do + it 'responds to start_bg' do + expect(described_class).to respond_to('start_bg') + end + + it 'responds to status_bg' do + expect(described_class).to respond_to('status_bg') + end +end diff --git a/spec/import/otrs/customer_factory_spec.rb b/spec/import/otrs/customer_factory_spec.rb new file mode 100644 index 000000000..40298e1c8 --- /dev/null +++ b/spec/import/otrs/customer_factory_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/factory_examples' + +RSpec.describe Import::OTRS::CustomerFactory do + it_behaves_like 'Import::Factory' +end diff --git a/spec/import/otrs/customer_spec.rb b/spec/import/otrs/customer_spec.rb new file mode 100644 index 000000000..ca16752f7 --- /dev/null +++ b/spec/import/otrs/customer_spec.rb @@ -0,0 +1,70 @@ +require 'rails_helper' + +RSpec.describe Import::OTRS::Customer do + + def creates_with(zammad_structure) + expect(import_object).to receive(:create).with(zammad_structure).and_return(existing_object) + expect_any_instance_of(described_class).to receive(:reset_primary_key_sequence) + start_import_test + end + + def updates_with(zammad_structure) + expect(import_object).to receive(:find_by).and_return(existing_object) + expect(existing_object).to receive(:update_attributes).with(zammad_structure) + expect(import_object).not_to receive(:new) + start_import_test + end + + def load_customer_json(file) + json_fixture("import/otrs/customer/#{file}") + end + + let(:import_object) { Organization } + let(:existing_object) { instance_double(import_object) } + let(:start_import_test) { described_class.new(object_structure) } + + context 'Organization' do + + let(:object_structure) { load_customer_json('default') } + let(:zammad_structure) { + { + created_by_id: '1', + updated_by_id: '1', + active: false, + updated_at: '2014-06-06 12:41:03', + created_at: '2014-06-06 12:41:03', + name: 'test922896', + note: 'test922896' + }} + + it 'creates' do + creates_with(zammad_structure) + end + + it 'updates' do + updates_with(zammad_structure) + end + end + + context 'OTRS CustomerID' do + + let(:customer_id) { 'test922896' } + let(:object_structure) { load_customer_json('default') } + let(:otrs_dummy_response) { + [ + object_structure + ] + } + + it 'responds to by_customer_id' do + expect(described_class).to respond_to('by_customer_id') + end + + it 'finds Organizations by OTRS CustomerID' do + expect(Import::OTRS::Requester).to receive(:load).and_return(otrs_dummy_response) + expect(import_object).to receive(:find_by).with(name: customer_id).and_return(existing_object) + + expect(described_class.by_customer_id(customer_id)).to be(existing_object) + end + end +end diff --git a/spec/import/otrs/customer_user_factory_spec.rb b/spec/import/otrs/customer_user_factory_spec.rb new file mode 100644 index 000000000..bb0761d52 --- /dev/null +++ b/spec/import/otrs/customer_user_factory_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/factory_examples' + +RSpec.describe Import::OTRS::CustomerUserFactory do + it_behaves_like 'Import::Factory' +end diff --git a/spec/import/otrs/customer_user_spec.rb b/spec/import/otrs/customer_user_spec.rb new file mode 100644 index 000000000..c208a7935 --- /dev/null +++ b/spec/import/otrs/customer_user_spec.rb @@ -0,0 +1,73 @@ +require 'rails_helper' + +RSpec.describe Import::OTRS::CustomerUser do + + def creates_with(zammad_structure) + expect_organization_lookup + expect(import_object).to receive(:new).with(zammad_structure).and_call_original + expect_any_instance_of(import_object).to receive(:save) + expect_any_instance_of(described_class).to receive(:reset_primary_key_sequence) + start_import_test + end + + def updates_with(zammad_structure) + expect_organization_lookup + expect(import_object).to receive(:find_by).and_return(existing_object) + expect(existing_object).to receive(:role_ids).and_return([]).at_least(:once) + expect(existing_object).to receive(:update_attributes).with(zammad_structure) + expect(import_object).not_to receive(:new) + start_import_test + end + + def expect_organization_lookup + expect(Import::OTRS::Customer).to receive(:by_customer_id).and_return(organization) + expect(organization).to receive(:id).and_return(organization_id) + end + + def load_customer_json(file) + json_fixture("import/otrs/customer_user/#{file}") + end + + let(:import_object) { User } + let(:existing_object) { instance_double(import_object) } + let(:start_import_test) { described_class.new(object_structure) } + let(:organization) { instance_double(Organization) } + let(:organization_id) { 1337 } + + context 'Customer User' do + + let(:object_structure) { load_customer_json('default') } + let(:zammad_structure) { + { + created_by_id: '1', + updated_by_id: '1', + active: true, + source: 'OTRS Import', + organization_id: 1337, + role_ids: [3], + updated_at: '2014-06-07 02:31:31', + created_at: '2014-06-07 02:31:31', + note: '', + email: 'qa100@t-online.de', + firstname: 'test669673', + lastname: 'test669673', + login: 'test669673', + password: 'f8be19af2f25837a31eff9131b0e47a5173290652c04a48b49b86474d48825ee', + phone: nil, + fax: nil, + mobile: nil, + street: nil, + zip: nil, + city: nil, + country: nil + }} + + it 'creates' do + creates_with(zammad_structure) + end + + it 'updates' do + updates_with(zammad_structure) + end + end +end diff --git a/spec/import/otrs/diff_examples.rb b/spec/import/otrs/diff_examples.rb new file mode 100644 index 000000000..679844d5c --- /dev/null +++ b/spec/import/otrs/diff_examples.rb @@ -0,0 +1,5 @@ +RSpec.shared_examples 'Import::OTRS::Diff' do + it 'responds to diff_worker' do + expect(described_class).to respond_to('diff_worker') + end +end diff --git a/spec/import/otrs/dynamic_field/checkbox_spec.rb b/spec/import/otrs/dynamic_field/checkbox_spec.rb new file mode 100644 index 000000000..61872b7d0 --- /dev/null +++ b/spec/import/otrs/dynamic_field/checkbox_spec.rb @@ -0,0 +1,39 @@ +require 'rails_helper' +require 'import/otrs/dynamic_field_examples' + +RSpec.describe Import::OTRS::DynamicField::Checkbox do + it_behaves_like 'Import::OTRS::DynamicField' + + it 'imports an OTRS Checkbox DynamicField' do + + zammad_structure = { + object: 'Ticket', + name: 'checkbox_example', + display: 'Checkbox Example', + screens: { + view: { + '-all-' => { + shown: true + } + } + }, + active: true, + editable: true, + position: '26', + created_by_id: 1, + updated_by_id: 1, + data_type: 'boolean', + data_option: { + default: true, + options: { + true => 'Yes', + false => 'No' + }, + null: false, + translate: true + } + } + + dynamic_field_from_json('checkbox/default', zammad_structure) + end +end diff --git a/spec/import/otrs/dynamic_field/date_spec.rb b/spec/import/otrs/dynamic_field/date_spec.rb new file mode 100644 index 000000000..4cafe944d --- /dev/null +++ b/spec/import/otrs/dynamic_field/date_spec.rb @@ -0,0 +1,42 @@ +require 'rails_helper' +require 'import/otrs/dynamic_field_examples' + +# this require is required (hehe) because of Rails autoloading +# which causes this error: +# warning: toplevel constant Date referenced by Import::OTRS::DynamicField::Date +# and rspec thinks we want to test Date and stores it into described_class... +require 'import/otrs/dynamic_field/date' + +RSpec.describe Import::OTRS::DynamicField::Date do + it_behaves_like 'Import::OTRS::DynamicField' + + it 'imports an OTRS Date DynamicField' do + + zammad_structure = { + object: 'Ticket', + name: 'date_example', + display: 'Date Example', + screens: { + view: { + '-all-' => { + shown: true + } + } + }, + active: true, + editable: true, + position: '40', + created_by_id: 1, + updated_by_id: 1, + data_type: 'date', + data_option: { + future: false, + past: false, + diff: 0, + null: false + } + } + + dynamic_field_from_json('date/default', zammad_structure) + end +end diff --git a/spec/import/otrs/dynamic_field/date_time_spec.rb b/spec/import/otrs/dynamic_field/date_time_spec.rb new file mode 100644 index 000000000..2e05c246b --- /dev/null +++ b/spec/import/otrs/dynamic_field/date_time_spec.rb @@ -0,0 +1,42 @@ +require 'rails_helper' +require 'import/otrs/dynamic_field_examples' + +# this require is required (hehe) because of Rails autoloading +# which causes this error: +# warning: toplevel constant DateTime referenced by Import::OTRS::DynamicField::DateTime +# and rspec thinks we want to test Date and stores it into described_class... +require 'import/otrs/dynamic_field/date_time' + +RSpec.describe Import::OTRS::DynamicField::DateTime do + it_behaves_like 'Import::OTRS::DynamicField' + + it 'imports an OTRS DateTime DynamicField' do + + zammad_structure = { + object: 'Ticket', + name: 'date_time_example', + display: 'DateTime Example', + screens: { + view: { + '-all-' => { + shown: true + } + } + }, + active: true, + editable: false, + position: '16', + created_by_id: 1, + updated_by_id: 1, + data_type: 'datetime', + data_option: { + future: true, + past: true, + diff: 72, + null: false + } + } + + dynamic_field_from_json('date_time/default', zammad_structure) + end +end diff --git a/spec/import/otrs/dynamic_field/dropdown_spec.rb b/spec/import/otrs/dynamic_field/dropdown_spec.rb new file mode 100644 index 000000000..da3c1d673 --- /dev/null +++ b/spec/import/otrs/dynamic_field/dropdown_spec.rb @@ -0,0 +1,42 @@ +require 'rails_helper' +require 'import/otrs/dynamic_field_examples' + +RSpec.describe Import::OTRS::DynamicField::Dropdown do + it_behaves_like 'Import::OTRS::DynamicField' + + it 'imports an OTRS Dropdown DynamicField' do + + zammad_structure = { + object: 'Ticket', + name: 'dropdown_example', + display: 'Dropdown Example', + screens: { + view: { + '-all-' => { + shown: true + } + } + }, + active: true, + editable: true, + position: '30', + created_by_id: 1, + updated_by_id: 1, + data_type: 'select', + data_option: { + default: '', + multiple: false, + options: { + 'Hamburg' => 'Hamburg', + 'München' => 'München', + 'Köln' => 'Köln', + 'Berlin' => 'Berlin' + }, + null: true, + translate: false + } + } + + dynamic_field_from_json('dropdown/default', zammad_structure) + end +end diff --git a/spec/import/otrs/dynamic_field/multiselect_spec.rb b/spec/import/otrs/dynamic_field/multiselect_spec.rb new file mode 100644 index 000000000..a188bdfca --- /dev/null +++ b/spec/import/otrs/dynamic_field/multiselect_spec.rb @@ -0,0 +1,42 @@ +require 'rails_helper' +require 'import/otrs/dynamic_field_examples' + +RSpec.describe Import::OTRS::DynamicField::Multiselect do + it_behaves_like 'Import::OTRS::DynamicField' + + it 'imports an OTRS Multiselect DynamicField' do + + zammad_structure = { + object: 'Ticket', + name: 'multiselect_example', + display: 'Multiselec tExample', + screens: { + view: { + '-all-' => { + shown: true + } + } + }, + active: true, + editable: true, + position: '4', + created_by_id: 1, + updated_by_id: 1, + data_type: 'select', + data_option: { + default: '', + multiple: true, + options: { + 'Hamburg' => 'Hamburg', + 'München' => 'München', + 'Köln' => 'Köln', + 'Berlin' => 'Berlin' + }, + null: false, + translate: false + } + } + + dynamic_field_from_json('multiselect/default', zammad_structure) + end +end diff --git a/spec/import/otrs/dynamic_field/text_area_spec.rb b/spec/import/otrs/dynamic_field/text_area_spec.rb new file mode 100644 index 000000000..f12d541cd --- /dev/null +++ b/spec/import/otrs/dynamic_field/text_area_spec.rb @@ -0,0 +1,35 @@ +require 'rails_helper' +require 'import/otrs/dynamic_field_examples' + +RSpec.describe Import::OTRS::DynamicField::TextArea do + it_behaves_like 'Import::OTRS::DynamicField' + + it 'imports an OTRS TextArea DynamicField' do + + zammad_structure = { + object: 'Ticket', + name: 'text_area_example', + display: 'TextArea Example', + screens: { + view: { + '-all-' => { + shown: true + } + } + }, + active: true, + editable: true, + position: '8', + created_by_id: 1, + updated_by_id: 1, + data_type: 'textarea', + data_option: { + default: '', + rows: '20', + null: false + } + } + + dynamic_field_from_json('text_area/default', zammad_structure) + end +end diff --git a/spec/import/otrs/dynamic_field/text_spec.rb b/spec/import/otrs/dynamic_field/text_spec.rb new file mode 100644 index 000000000..80486412e --- /dev/null +++ b/spec/import/otrs/dynamic_field/text_spec.rb @@ -0,0 +1,36 @@ +require 'rails_helper' +require 'import/otrs/dynamic_field_examples' + +RSpec.describe Import::OTRS::DynamicField::Text do + it_behaves_like 'Import::OTRS::DynamicField' + + it 'imports an OTRS Text DynamicField' do + + zammad_structure = { + object: 'Ticket', + name: 'text_example', + display: 'Text Example', + screens: { + view: { + '-all-' => { + shown: true + } + } + }, + active: true, + editable: true, + position: '8', + created_by_id: 1, + updated_by_id: 1, + data_type: 'input', + data_option: { + default: '', + type: 'text', + maxlength: 255, + null: false + } + } + + dynamic_field_from_json('text/default', zammad_structure) + end +end diff --git a/spec/import/otrs/dynamic_field_examples.rb b/spec/import/otrs/dynamic_field_examples.rb new file mode 100644 index 000000000..edab67180 --- /dev/null +++ b/spec/import/otrs/dynamic_field_examples.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +def dynamic_field_from_json(file, zammad_structure) + expect(ObjectManager::Attribute).to receive(:add).with(zammad_structure) + expect(ObjectManager::Attribute).to receive(:migration_execute) + described_class.new(load_dynamic_field_json(file)) +end + +def load_dynamic_field_json(file) + json_fixture("import/otrs/dynamic_field/#{file}") +end + +RSpec.shared_examples 'Import::OTRS::DynamicField' do + it 'responds to convert_name' do + expect(described_class).to respond_to('convert_name') + end +end diff --git a/spec/import/otrs/dynamic_field_factory_spec.rb b/spec/import/otrs/dynamic_field_factory_spec.rb new file mode 100644 index 000000000..e10686298 --- /dev/null +++ b/spec/import/otrs/dynamic_field_factory_spec.rb @@ -0,0 +1,19 @@ +require 'rails_helper' +require 'import/factory_examples' +require 'import/otrs/dynamic_field_examples' + +RSpec.describe Import::OTRS::DynamicFieldFactory do + it_behaves_like 'Import::Factory' + + let(:start_import_test) { described_class.import(object_structure) } + let(:object_structure) { [load_dynamic_field_json('text/default')] } + + it 'responds to skip_field?' do + expect(described_class).to respond_to('skip_field?') + end + + it 'imports OTRS DynamicFields' do + expect(Import::OTRS::DynamicField::Text).to receive(:new) + start_import_test + end +end diff --git a/spec/import/otrs/dynamic_field_spec.rb b/spec/import/otrs/dynamic_field_spec.rb new file mode 100644 index 000000000..0bf0829ed --- /dev/null +++ b/spec/import/otrs/dynamic_field_spec.rb @@ -0,0 +1,16 @@ +require 'rails_helper' +require 'import/otrs/dynamic_field_examples' + +RSpec.describe Import::OTRS::DynamicField do + it_behaves_like 'Import::OTRS::DynamicField' + + let(:start_import_test) { described_class.new(object_structure) } + let(:object_structure) { load_dynamic_field_json('text/default') } + + it 'requires an implementation of init_callback' do + expect(ObjectManager::Attribute).to receive(:get).and_return(false) + expect { + start_import_test + }.to raise_error(RuntimeError) + end +end diff --git a/spec/import/otrs/history/article_spec.rb b/spec/import/otrs/history/article_spec.rb new file mode 100644 index 000000000..0b2b883e7 --- /dev/null +++ b/spec/import/otrs/history/article_spec.rb @@ -0,0 +1,22 @@ +require 'rails_helper' +require 'import/otrs/history_examples' + +RSpec.describe Import::OTRS::History::Article do + it_behaves_like 'Import::OTRS::History' + + it 'imports an OTRS AddNote history entry' do + + zammad_structure = { + id: '11307', + o_id: '3973', + history_type: 'created', + history_object: 'Ticket::Article', + related_o_id: '730', + related_history_object: 'Ticket', + created_at: '2014-11-21 00:21:08', + created_by_id: '3' + } + + history_from_json('article/default', zammad_structure) + end +end diff --git a/spec/import/otrs/history/move_spec.rb b/spec/import/otrs/history/move_spec.rb new file mode 100644 index 000000000..d00e931bc --- /dev/null +++ b/spec/import/otrs/history/move_spec.rb @@ -0,0 +1,25 @@ +require 'rails_helper' +require 'import/otrs/history_examples' + +RSpec.describe Import::OTRS::History::Move do + it_behaves_like 'Import::OTRS::History' + + it 'imports an OTRS Move history entry' do + + zammad_structure = { + id: '238', + o_id: '39', + history_type: 'updated', + history_object: 'Ticket', + history_attribute: 'group', + value_from: 'Source', + value_to: 'Target', + id_from: '5', + id_to: '2', + created_at: '2014-05-12 13:42:41', + created_by_id: '1' + } + + history_from_json('move/default', zammad_structure) + end +end diff --git a/spec/import/otrs/history/new_ticket_spec.rb b/spec/import/otrs/history/new_ticket_spec.rb new file mode 100644 index 000000000..ef1b33b4a --- /dev/null +++ b/spec/import/otrs/history/new_ticket_spec.rb @@ -0,0 +1,20 @@ +require 'rails_helper' +require 'import/otrs/history_examples' + +RSpec.describe Import::OTRS::History::NewTicket do + it_behaves_like 'Import::OTRS::History' + + it 'imports an OTRS NewTicket history entry' do + + zammad_structure = { + id: '11291', + o_id: '730', + history_type: 'created', + history_object: 'Ticket', + created_at: '2014-11-21 00:17:41', + created_by_id: '3' + } + + history_from_json('new_ticket/default', zammad_structure) + end +end diff --git a/spec/import/otrs/history/priority_update_spec.rb b/spec/import/otrs/history/priority_update_spec.rb new file mode 100644 index 000000000..f1e407695 --- /dev/null +++ b/spec/import/otrs/history/priority_update_spec.rb @@ -0,0 +1,25 @@ +require 'rails_helper' +require 'import/otrs/history_examples' + +RSpec.describe Import::OTRS::History::PriorityUpdate do + it_behaves_like 'Import::OTRS::History' + + it 'imports an OTRS PriorityUpdate history entry' do + + zammad_structure = { + id: '11131', + o_id: '721', + history_type: 'updated', + history_object: 'Ticket', + history_attribute: 'priority', + value_from: '2 low', + value_to: '4 high', + id_from: '2', + id_to: '4', + created_at: '2014-09-22 16:44:55', + created_by_id: '1' + } + + history_from_json('priority_update/default', zammad_structure) + end +end diff --git a/spec/import/otrs/history/state_update_spec.rb b/spec/import/otrs/history/state_update_spec.rb new file mode 100644 index 000000000..2a2494122 --- /dev/null +++ b/spec/import/otrs/history/state_update_spec.rb @@ -0,0 +1,25 @@ +require 'rails_helper' +require 'import/otrs/history_examples' + +RSpec.describe Import::OTRS::History::StateUpdate do + it_behaves_like 'Import::OTRS::History' + + it 'imports an OTRS StateUpdate history entry' do + + zammad_structure = { + id: '11305', + o_id: '730', + history_type: 'updated', + history_object: 'Ticket', + history_attribute: 'state', + value_from: 'new', + id_from: 1, + value_to: 'open', + id_to: 2, + created_at: '2014-11-21 00:21:08', + created_by_id: '3' + } + + history_from_json('state_update/default', zammad_structure) + end +end diff --git a/spec/import/otrs/history_examples.rb b/spec/import/otrs/history_examples.rb new file mode 100644 index 000000000..1f988d559 --- /dev/null +++ b/spec/import/otrs/history_examples.rb @@ -0,0 +1,20 @@ +require 'rails_helper' +require 'history' + +def history_from_json(file, zammad_structure) + expect(History).to receive(:add).with(zammad_structure) + described_class.new(load_history_json(file)) +end + +def load_history_json(file) + json_fixture("import/otrs/history/#{file}") +end + +RSpec.shared_examples 'Import::OTRS::History' do + it 'responds to init_callback' do + expect(::History).to receive(:add) + allow(::History::Attribute).to receive(:find_by).and_return(true) + blank_instance = described_class.new({}) + expect(blank_instance).to respond_to('init_callback') + end +end diff --git a/spec/import/otrs/history_factory_spec.rb b/spec/import/otrs/history_factory_spec.rb new file mode 100644 index 000000000..838394754 --- /dev/null +++ b/spec/import/otrs/history_factory_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/factory_examples' + +RSpec.describe Import::OTRS::HistoryFactory do + it_behaves_like 'Import::Factory' +end diff --git a/spec/import/otrs/history_spec.rb b/spec/import/otrs/history_spec.rb new file mode 100644 index 000000000..6ca040ed6 --- /dev/null +++ b/spec/import/otrs/history_spec.rb @@ -0,0 +1,14 @@ +require 'rails_helper' +require 'import/otrs/history_examples' + +RSpec.describe Import::OTRS::History do + + let(:start_import_test) { described_class.new(object_structure) } + let(:object_structure) { load_history_json('article/default') } + + it 'requires an implementation of init_callback' do + expect { + start_import_test + }.to raise_error(RuntimeError) + end +end diff --git a/spec/import/otrs/import_stats_examples.rb b/spec/import/otrs/import_stats_examples.rb new file mode 100644 index 000000000..e21cfb5c7 --- /dev/null +++ b/spec/import/otrs/import_stats_examples.rb @@ -0,0 +1,5 @@ +RSpec.shared_examples 'Import::OTRS::ImportStats' do + it 'responds to current_state' do + expect(described_class).to respond_to('current_state') + end +end diff --git a/spec/import/otrs/priority_factory_spec.rb b/spec/import/otrs/priority_factory_spec.rb new file mode 100644 index 000000000..34033008c --- /dev/null +++ b/spec/import/otrs/priority_factory_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/factory_examples' + +RSpec.describe Import::OTRS::PriorityFactory do + it_behaves_like 'Import::Factory' +end diff --git a/spec/import/otrs/priority_spec.rb b/spec/import/otrs/priority_spec.rb new file mode 100644 index 000000000..c7c5a3f6e --- /dev/null +++ b/spec/import/otrs/priority_spec.rb @@ -0,0 +1,50 @@ +require 'rails_helper' + +RSpec.describe Import::OTRS::Priority do + + def creates_with(zammad_structure) + expect(import_object).to receive(:new).with(zammad_structure).and_call_original + expect_any_instance_of(import_object).to receive(:save) + expect_any_instance_of(described_class).to receive(:reset_primary_key_sequence) + start_import_test + end + + def updates_with(zammad_structure) + expect(import_object).to receive(:find_by).and_return(existing_object) + expect(existing_object).to receive(:update_attributes).with(zammad_structure) + expect(import_object).not_to receive(:new) + start_import_test + end + + def load_priority_json(file) + json_fixture("import/otrs/priority/#{file}") + end + + let(:import_object) { Ticket::Priority } + let(:existing_object) { instance_double(import_object) } + let(:start_import_test) { described_class.new(object_structure) } + + context 'default' do + + let(:object_structure) { load_priority_json('default') } + let(:zammad_structure) { + { + created_by_id: '1', + updated_by_id: '1', + active: true, + updated_at: '2014-04-28 10:53:18', + created_at: '2014-04-28 10:53:18', + name: '4 high', + id: '4' + } + } + + it 'creates' do + creates_with(zammad_structure) + end + + it 'updates' do + updates_with(zammad_structure) + end + end +end diff --git a/spec/import/otrs/queue_factory_spec.rb b/spec/import/otrs/queue_factory_spec.rb new file mode 100644 index 000000000..9e89bd0f6 --- /dev/null +++ b/spec/import/otrs/queue_factory_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/factory_examples' + +RSpec.describe Import::OTRS::QueueFactory do + it_behaves_like 'Import::Factory' +end diff --git a/spec/import/otrs/queue_spec.rb b/spec/import/otrs/queue_spec.rb new file mode 100644 index 000000000..4b0621ee9 --- /dev/null +++ b/spec/import/otrs/queue_spec.rb @@ -0,0 +1,51 @@ +require 'rails_helper' + +RSpec.describe Import::OTRS::Queue do + + def creates_with(zammad_structure) + expect(import_object).to receive(:new).with(zammad_structure).and_call_original + expect_any_instance_of(import_object).to receive(:save) + expect_any_instance_of(described_class).to receive(:reset_primary_key_sequence) + start_import_test + end + + def updates_with(zammad_structure) + expect(import_object).to receive(:find_by).and_return(existing_object) + expect(existing_object).to receive(:update_attributes).with(zammad_structure) + expect(import_object).not_to receive(:new) + start_import_test + end + + def load_queue_json(file) + json_fixture("import/otrs/queue/#{file}") + end + + let(:import_object) { Group } + let(:existing_object) { instance_double(import_object) } + let(:start_import_test) { described_class.new(object_structure) } + + context 'default' do + + let(:object_structure) { load_queue_json('default') } + let(:zammad_structure) { + { + created_by_id: 1, + updated_by_id: 1, + active: false, + updated_at: '2014-05-13 10:54:11', + created_at: '2014-05-13 10:54:11', + name: 'UnitTestQueue45699', + id: '11', + note: 'Some comment' + } + } + + it 'creates' do + creates_with(zammad_structure) + end + + it 'updates' do + updates_with(zammad_structure) + end + end +end diff --git a/spec/import/otrs/requester_spec.rb b/spec/import/otrs/requester_spec.rb new file mode 100644 index 000000000..f7b77cc4a --- /dev/null +++ b/spec/import/otrs/requester_spec.rb @@ -0,0 +1,39 @@ +require 'rails_helper' + +RSpec.describe Import::OTRS::Requester do + it 'responds to load' do + expect(described_class).to respond_to('load') + end + + it 'responds to list' do + expect(described_class).to respond_to('list') + end + + it 'responds to connection_test' do + expect(described_class).to respond_to('connection_test') + end + + context 'caching request results' do + + let(:response) { + response = double() + response_body = double() + expect(response_body).to receive(:to_s).at_least(:once).and_return('{"Result": {}}') + expect(response).to receive('success?').at_least(:once).and_return(true) + expect(response).to receive('body').at_least(:once).and_return(response_body) + response + } + + it 'is active if no args are given' do + expect(UserAgent).to receive(:post).and_return(response) + described_class.load('Ticket') + described_class.load('Ticket') + end + + it 'is not active if args are given' do + expect(UserAgent).to receive(:post).twice.and_return(response) + described_class.load('Ticket', offset: 10) + described_class.load('Ticket', offset: 20) + end + end +end diff --git a/spec/import/otrs/state_factory_spec.rb b/spec/import/otrs/state_factory_spec.rb new file mode 100644 index 000000000..40764dca7 --- /dev/null +++ b/spec/import/otrs/state_factory_spec.rb @@ -0,0 +1,11 @@ +require 'rails_helper' +require 'import/transaction_factory_examples' + +RSpec.describe Import::OTRS::StateFactory do + it_behaves_like 'Import::TransactionFactory' + + it 'creates a state backup in the pre_import_hook' do + expect(described_class).to receive(:backup) + described_class.pre_import_hook([]) + end +end diff --git a/spec/import/otrs/state_spec.rb b/spec/import/otrs/state_spec.rb new file mode 100644 index 000000000..5e13ba04f --- /dev/null +++ b/spec/import/otrs/state_spec.rb @@ -0,0 +1,53 @@ +require 'rails_helper' + +RSpec.describe Import::OTRS::State do + + def creates_with(zammad_structure) + expect(import_object).to receive(:find_by).and_return(nil) + expect(import_object).to receive(:new).with(zammad_structure).and_call_original + expect_any_instance_of(import_object).to receive(:save) + expect_any_instance_of(described_class).to receive(:reset_primary_key_sequence) + start_import_test + end + + def updates_with(zammad_structure) + expect(import_object).to receive(:find_by).and_return(existing_object) + expect(existing_object).to receive(:update_attributes).with(zammad_structure) + expect(import_object).not_to receive(:new) + start_import_test + end + + def load_state_json(file) + json_fixture("import/otrs/state/#{file}") + end + + let(:import_object) { Ticket::State } + let(:existing_object) { instance_double(import_object) } + let(:start_import_test) { described_class.new(object_structure) } + + context 'closed' do + + let(:object_structure) { load_state_json('default') } + let(:zammad_structure) { + { + created_by_id: 1, + updated_by_id: 1, + active: '1', + state_type_id: 5, + updated_at: '2014-04-28 10:53:18', + created_at: '2014-04-28 10:53:18', + name: 'closed successful', + id: '2', + note: 'Ticket is closed successful.' + } + } + + it 'creates' do + creates_with(zammad_structure) + end + + it 'updates' do + updates_with(zammad_structure) + end + end +end diff --git a/spec/import/otrs/sys_config_factory_spec.rb b/spec/import/otrs/sys_config_factory_spec.rb new file mode 100644 index 000000000..119568af4 --- /dev/null +++ b/spec/import/otrs/sys_config_factory_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/import_factory_examples' + +RSpec.describe Import::OTRS::SysConfigFactory do + it_behaves_like 'Import factory' +end diff --git a/spec/import/otrs/ticket_factory_spec.rb b/spec/import/otrs/ticket_factory_spec.rb new file mode 100644 index 000000000..fdcb9b201 --- /dev/null +++ b/spec/import/otrs/ticket_factory_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/factory_examples' + +RSpec.describe Import::OTRS::TicketFactory do + it_behaves_like 'Import::Factory' +end diff --git a/spec/import/otrs/ticket_spec.rb b/spec/import/otrs/ticket_spec.rb new file mode 100644 index 000000000..0c4a4612b --- /dev/null +++ b/spec/import/otrs/ticket_spec.rb @@ -0,0 +1,100 @@ +require 'rails_helper' + +RSpec.describe Import::OTRS::Ticket do + + def creates_with(zammad_structure) + expect(import_object).to receive(:new).with(zammad_structure).and_call_original + expect_any_instance_of(import_object).to receive(:save) + expect_any_instance_of(described_class).to receive(:reset_primary_key_sequence) + start_import_test + end + + def updates_with(zammad_structure) + expect(import_object).to receive(:find_by).and_return(existing_object) + expect(existing_object).to receive(:update_attributes).with(zammad_structure) + expect(import_object).not_to receive(:new) + start_import_test + end + + def load_ticket_json(file) + json_fixture("import/otrs/ticket/#{file}") + end + + def import_backend_expectations + expect(Import::OTRS::ArticleCustomerFactory).to receive(:import) + expect(Import::OTRS::ArticleFactory).to receive(:import) + expect(Import::OTRS::HistoryFactory).to receive(:import) + expect(User).to receive(:find_by).twice.and_return(nil) + # needed, otherwise 'ActiveRecord::UnknownAttributeError' for + # DynamicFields will arise + allow(Import::OTRS::DynamicFieldFactory).to receive('skip_field?').and_return(true) + end + + let(:import_object) { Ticket } + let(:existing_object) { instance_double(import_object) } + let(:start_import_test) { described_class.new(object_structure) } + + context 'default' do + + let(:object_structure) { load_ticket_json('default') } + let(:zammad_structure) { + { + title: 'test #3', + owner_id: 1, + customer_id: 1, + created_by_id: '3', + updated_by_id: 1, + updated_at: '2014-11-21 00:21:08', + created_at: '2014-11-21 00:17:40', + number: '20141121305000012', + group_id: '1', + state_id: '2', + priority_id: '3', + id: '730', + close_at: '2014-11-21 00:21:08' + } + } + + it 'creates' do + import_backend_expectations + creates_with(zammad_structure) + end + + it 'updates' do + import_backend_expectations + updates_with(zammad_structure) + end + end + + context 'no title' do + + let(:object_structure) { load_ticket_json('no_title') } + let(:zammad_structure) { + { + title: '**EMPTY**', + owner_id: 1, + customer_id: 1, + created_by_id: '3', + updated_by_id: 1, + updated_at: '2014-11-21 00:21:08', + created_at: '2014-11-21 00:17:40', + number: '20141121305000012', + group_id: '1', + state_id: '2', + priority_id: '3', + id: '730', + close_at: '2014-11-21 00:21:08' + } + } + + it 'creates' do + import_backend_expectations + creates_with(zammad_structure) + end + + it 'updates' do + import_backend_expectations + updates_with(zammad_structure) + end + end +end diff --git a/spec/import/otrs/user_factory_spec.rb b/spec/import/otrs/user_factory_spec.rb new file mode 100644 index 000000000..b88035417 --- /dev/null +++ b/spec/import/otrs/user_factory_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/factory_examples' + +RSpec.describe Import::OTRS::UserFactory do + it_behaves_like 'Import::Factory' +end diff --git a/spec/import/otrs/user_spec.rb b/spec/import/otrs/user_spec.rb new file mode 100644 index 000000000..aa75c3e49 --- /dev/null +++ b/spec/import/otrs/user_spec.rb @@ -0,0 +1,131 @@ +require 'rails_helper' + +RSpec.describe Import::OTRS::User do + + def creates_with(zammad_structure) + expect(import_object).to receive(:find_by).and_return(nil) + expect(import_object).to receive(:new).with(zammad_structure).and_call_original + expect_any_instance_of(import_object).to receive(:save) + expect_any_instance_of(described_class).to receive(:reset_primary_key_sequence) + start_import_test + end + + def updates_with(zammad_structure) + expect(import_object).to receive(:find_by).and_return(existing_object) + expect(existing_object).to receive(:role_ids).and_return([]) + expect(existing_object).to receive(:update_attributes).with(zammad_structure) + expect(import_object).not_to receive(:new) + start_import_test + end + + def load_user_json(file) + json_fixture("import/otrs/user/#{file}") + end + + def prepare_expectations + requester_expectations + user_expectations + end + + def user_expectations + expect(User).to receive(:where).and_return([]) + end + + # this is really bad and should get improved! + # these are integration tests that will likely fail + # soon - sorry :) + def requester_expectations + queue_list = [ + { + 'QueueID' => '1', + 'GroupID' => '2', + } + ] + expect(Import::OTRS::Requester).to receive(:load).with('Queue').and_return(queue_list) + + group_list = [ + { + 'ID' => '2', + 'Name' => 'user', + }, + { + 'ID' => '3', + 'Name' => 'another_group', + }, + ] + expect(Import::OTRS::Requester).to receive(:load).with('Group').and_return(group_list) + + role_list = [{ 'ID' => '3', 'GroupIDs' => %w(2 3) }] + expect(Import::OTRS::Requester).to receive(:load).with('Role').and_return(role_list) + end + + let(:import_object) { ::User } + let(:existing_object) { instance_double(import_object) } + let(:start_import_test) { described_class.new(object_structure) } + + context 'default' do + + let(:object_structure) { load_user_json('default') } + let(:zammad_structure) { + { + created_by_id: 1, + updated_by_id: 1, + active: true, + source: 'OTRS Import', + role_ids: [2], + group_ids: ['1'], + password: '{sha2}9faaba2ab242a99bbb6992e9424386375f6757c17e6484ae570f39d9cad9f28ea', + updated_at: '2014-04-28 10:53:18', + created_at: '2014-04-28 10:53:18', + id: '1', + email: 'root@localhost', + firstname: 'Admin', + lastname: 'OTRS', + login: 'root@localhost' + } + } + + it 'creates' do + prepare_expectations + creates_with(zammad_structure) + end + + it 'updates' do + prepare_expectations + updates_with(zammad_structure) + end + end + + context 'no groups' do + + let(:object_structure) { load_user_json('no_groups') } + let(:zammad_structure) { + { + created_by_id: 1, + updated_by_id: 1, + active: true, + source: 'OTRS Import', + role_ids: [2], + group_ids: [], + password: '{sha2}9edb001ad7900daea0622d89225c9ca729749fd12ae5ea044f072d1b7c56c8cc', + updated_at: '2014-11-14 00:53:20', + created_at: '2014-11-14 00:53:20', + id: '6', + email: 'agent-2-for-role-2@example.com', + firstname: 'agent-2-for-role-2', + lastname: 'agent-2-for-role-2', + login: 'agent-2-for-role-2' + } + } + + it 'creates' do + prepare_expectations + creates_with(zammad_structure) + end + + it 'updates' do + prepare_expectations + updates_with(zammad_structure) + end + end +end diff --git a/spec/import/otrs_spec.rb b/spec/import/otrs_spec.rb new file mode 100644 index 000000000..95bb5a10f --- /dev/null +++ b/spec/import/otrs_spec.rb @@ -0,0 +1,14 @@ +require 'rails_helper' +require 'import/helper_examples' +require 'import/importer_examples' +require 'import/otrs/async_examples' +require 'import/otrs/diff_examples' +require 'import/otrs/import_stats_examples' + +RSpec.describe Import::OTRS do + it_behaves_like 'Import backend' + it_behaves_like 'Import::Helper' + it_behaves_like 'Import::OTRS::Async' + it_behaves_like 'Import::OTRS::Diff' + it_behaves_like 'Import::OTRS::ImportStats' +end diff --git a/spec/import/transaction_factory_examples.rb b/spec/import/transaction_factory_examples.rb new file mode 100644 index 000000000..3abe21c6d --- /dev/null +++ b/spec/import/transaction_factory_examples.rb @@ -0,0 +1,5 @@ +require 'import/base_factory_examples' + +RSpec.shared_examples 'Import::TransactionFactory' do + it_behaves_like 'Import::BaseFactory' +end diff --git a/spec/import/transaction_factory_spec.rb b/spec/import/transaction_factory_spec.rb new file mode 100644 index 000000000..d030be279 --- /dev/null +++ b/spec/import/transaction_factory_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' +require 'import/transaction_factory_examples' + +RSpec.describe Import::TransactionFactory do + it_behaves_like 'Import::TransactionFactory' + it_behaves_like 'Import::BaseFactory extender' +end diff --git a/spec/import/zendesk_spec.rb b/spec/import/zendesk_spec.rb new file mode 100644 index 000000000..7404bbe04 --- /dev/null +++ b/spec/import/zendesk_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' +require 'import/importer_examples' + +RSpec.describe Import::Zendesk do + it_behaves_like 'Import backend' +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 000000000..0c2dfd638 --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1,57 @@ +# This file is copied to spec/ when you run 'rails generate rspec:install' +ENV['RAILS_ENV'] ||= 'test' +require File.expand_path('../../config/environment', __FILE__) +# Prevent database truncation if the environment is production +abort('The Rails environment is running in production mode!') if Rails.env.production? +require 'spec_helper' +require 'rspec/rails' +# Add additional requires below this line. Rails is not loaded until this point! + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +# Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } + +# Checks for pending migration and applies them before tests are run. +# If you are not using ActiveRecord, you can remove this line. +ActiveRecord::Migration.maintain_test_schema! + +RSpec.configure do |config| + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_path = "#{::Rails.root}/spec/fixtures" + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = true + + # RSpec Rails can automatically mix in different behaviours to your tests + # based on their file location, for example enabling you to call `get` and + # `post` in specs under `spec/controllers`. + # + # You can disable this behaviour by removing the line below, and instead + # explicitly tag your specs with their type, e.g.: + # + # RSpec.describe UsersController, :type => :controller do + # # ... + # end + # + # The different available types are documented in the features, such as in + # https://relishapp.com/rspec/rspec-rails/docs + config.infer_spec_type_from_file_location! + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 000000000..1563cc98a --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,107 @@ +# This file was generated by the `rails generate rspec:install` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# The `.rspec` file also contains a few flags that are not defaults but that +# users commonly want. +# +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +require 'spec_helper/zammad_helper' + +RSpec.configure do |config| + # Zammad specific helpers + config.include ZammadHelper + + # skip Zammad helper functions in the stacktrace to lower noise + config.backtrace_exclusion_patterns << %r{/spec/spec_helper/} + + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ + # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode + config.disable_monkey_patching! + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = 'doc' + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end diff --git a/spec/spec_helper/zammad_helper.rb b/spec/spec_helper/zammad_helper.rb new file mode 100644 index 000000000..30dbf3859 --- /dev/null +++ b/spec/spec_helper/zammad_helper.rb @@ -0,0 +1 @@ +require 'spec_helper/zammad_helper/json' diff --git a/spec/spec_helper/zammad_helper/json.rb b/spec/spec_helper/zammad_helper/json.rb new file mode 100644 index 000000000..fc65a810e --- /dev/null +++ b/spec/spec_helper/zammad_helper/json.rb @@ -0,0 +1,5 @@ +module ZammadHelper + def json_fixture(file) + JSON.parse(File.read("spec/fixtures/#{file}.json")) + end +end diff --git a/test/browser/monitoring_test.rb b/test/browser/monitoring_test.rb new file mode 100644 index 000000000..d4e79b4db --- /dev/null +++ b/test/browser/monitoring_test.rb @@ -0,0 +1,43 @@ +# encoding: utf-8 +require 'browser_test_helper' + +class MonitoringTest < TestCase + + def test_mode + browser1 = browser_instance + login( + browser: browser1, + username: 'master@example.com', + password: 'test', + url: browser_url, + ) + click( + browser: browser1, + css: 'a[href="#manage"]', + ) + click( + browser: browser1, + css: 'a[href="#system/monitoring"]', + ) + + token = browser1.find_elements(css: '.active.content .js-token')[0].attribute('value') + url = browser1.find_elements(css: '.active.content .js-url')[0].attribute('value') + + assert_match(token.to_s, url) + + click( + browser: browser1, + css: '.active.content .js-resetToken', + ) + sleep 3 + + token_new = browser1.find_elements(css: '.active.content .js-token')[0].attribute('value') + url_new = browser1.find_elements(css: '.active.content .js-url')[0].attribute('value') + + assert_not_equal(token, token_new) + assert_not_equal(url, url_new) + assert_match(token_new.to_s, url_new) + + end + +end diff --git a/test/controllers/monitoring_controller_test.rb b/test/controllers/monitoring_controller_test.rb new file mode 100644 index 000000000..e1ac7fe63 --- /dev/null +++ b/test/controllers/monitoring_controller_test.rb @@ -0,0 +1,393 @@ +# encoding: utf-8 +require 'test_helper' + +class MonitoringControllerTest < ActionDispatch::IntegrationTest + setup do + + # set accept header + @headers = { 'ACCEPT' => 'application/json', 'CONTENT_TYPE' => 'application/json' } + + # set token + @token = SecureRandom.urlsafe_base64(64) + Setting.set('monitoring_token', @token) + + # create agent + roles = Role.where(name: %w(Admin Agent)) + groups = Group.all + + # channel cleanup + Channel.where.not(area: 'Email::Notification').destroy_all + Channel.all.each { |channel| + channel.status_in = 'ok' + channel.status_out = 'ok' + channel.last_log_in = nil + channel.last_log_out = nil + channel.save! + } + dir = "#{Rails.root}/tmp/unprocessable_mail" + Dir.glob("#{dir}/*.eml") do |entry| + File.delete(entry) + end + + Scheduler.where(active: true).each { |scheduler| + scheduler.last_run = Time.zone.now + scheduler.save! + } + + permission = Permission.find_by(name: 'admin.monitoring') + permission.active = true + permission.save! + + UserInfo.current_user_id = 1 + @admin = User.create_or_update( + login: 'monitoring-admin', + firstname: 'Monitoring', + lastname: 'Admin', + email: 'monitoring-admin@example.com', + password: 'adminpw', + active: true, + roles: roles, + groups: groups, + ) + + # create agent + roles = Role.where(name: 'Agent') + @agent = User.create_or_update( + login: 'monitoring-agent@example.com', + firstname: 'Monitoring', + lastname: 'Agent', + email: 'monitoring-agent@example.com', + password: 'agentpw', + active: true, + roles: roles, + groups: groups, + ) + + # create customer without org + roles = Role.where(name: 'Customer') + @customer_without_org = User.create_or_update( + login: 'monitoring-customer1@example.com', + firstname: 'Monitoring', + lastname: 'Customer1', + email: 'monitoring-customer1@example.com', + password: 'customer1pw', + active: true, + roles: roles, + ) + + end + + test '01 monitoring without token' do + + # health_check + get '/api/v1/monitoring/health_check', {}, @headers + assert_response(401) + + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_not(result['healthy']) + assert_equal('Not authorized', result['error']) + + # status + get '/api/v1/monitoring/status', {}, @headers + assert_response(401) + + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_not(result['agents']) + assert_not(result['last_login']) + assert_not(result['counts']) + assert_not(result['last_created_at']) + assert_equal('Not authorized', result['error']) + + # token + post '/api/v1/monitoring/token', {}, @headers + assert_response(401) + + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_not(result['token']) + assert_equal('authentication failed', result['error']) + + end + + test '02 monitoring with wrong token' do + + # health_check + get '/api/v1/monitoring/health_check?token=abc', {}, @headers + assert_response(401) + + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_not(result['healthy']) + assert_equal('Not authorized', result['error']) + + # status + get '/api/v1/monitoring/status?token=abc', {}, @headers + assert_response(401) + + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_not(result['agents']) + assert_not(result['last_login']) + assert_not(result['counts']) + assert_not(result['last_created_at']) + assert_equal('Not authorized', result['error']) + + # token + post '/api/v1/monitoring/token', { token: 'abc' }.to_json, @headers + assert_response(401) + + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_not(result['token']) + assert_equal('authentication failed', result['error']) + + end + + test '03 monitoring with correct token' do + + # health_check + get "/api/v1/monitoring/health_check?token=#{@token}", {}, @headers + assert_response(200) + + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_not(result['error']) + assert_equal(true, result['healthy']) + assert_equal('success', result['message']) + + # status + get "/api/v1/monitoring/status?token=#{@token}", {}, @headers + assert_response(200) + + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_not(result['error']) + assert(result.key?('agents')) + assert(result.key?('last_login')) + assert(result.key?('counts')) + assert(result.key?('last_created_at')) + + # token + post '/api/v1/monitoring/token', { token: @token }.to_json, @headers + assert_response(401) + + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_not(result['token']) + assert_equal('authentication failed', result['error']) + + end + + test '04 monitoring with admin user' do + + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('monitoring-admin@example.com', 'adminpw') + + # health_check + get '/api/v1/monitoring/health_check', {}, @headers.merge('Authorization' => credentials) + assert_response(200) + + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_not(result['error']) + assert_equal(true, result['healthy']) + assert_equal('success', result['message']) + + # status + get '/api/v1/monitoring/status', {}, @headers.merge('Authorization' => credentials) + assert_response(200) + + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_not(result['error']) + assert(result.key?('agents')) + assert(result.key?('last_login')) + assert(result.key?('counts')) + assert(result.key?('last_created_at')) + + # token + post '/api/v1/monitoring/token', { token: @token }.to_json, @headers.merge('Authorization' => credentials) + assert_response(201) + + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert(result['token']) + @token = result['token'] + assert_not(result['error']) + + end + + test '05 monitoring with agent user' do + + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('monitoring-agent@example.com', 'agentpw') + + # health_check + get '/api/v1/monitoring/health_check', {}, @headers.merge('Authorization' => credentials) + assert_response(401) + + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_not(result['healthy']) + assert_equal('Not authorized (user)!', result['error']) + + # status + get '/api/v1/monitoring/status', {}, @headers.merge('Authorization' => credentials) + assert_response(401) + + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_not(result['agents']) + assert_not(result['last_login']) + assert_not(result['counts']) + assert_not(result['last_created_at']) + assert_equal('Not authorized (user)!', result['error']) + + # token + post '/api/v1/monitoring/token', { token: @token }.to_json, @headers.merge('Authorization' => credentials) + assert_response(401) + + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_not(result['token']) + assert_equal('Not authorized (user)!', result['error']) + + end + + test '06 monitoring with admin user and invalid permission' do + + permission = Permission.find_by(name: 'admin.monitoring') + permission.active = false + permission.save! + + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('monitoring-admin@example.com', 'adminpw') + + # health_check + get '/api/v1/monitoring/health_check', {}, @headers.merge('Authorization' => credentials) + assert_response(401) + + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_not(result['healthy']) + assert_equal('Not authorized (user)!', result['error']) + + # status + get '/api/v1/monitoring/status', {}, @headers.merge('Authorization' => credentials) + assert_response(401) + + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_not(result['agents']) + assert_not(result['last_login']) + assert_not(result['counts']) + assert_not(result['last_created_at']) + assert_equal('Not authorized (user)!', result['error']) + + # token + post '/api/v1/monitoring/token', { token: @token }.to_json, @headers.merge('Authorization' => credentials) + assert_response(401) + + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_not(result['token']) + assert_equal('Not authorized (user)!', result['error']) + + permission.active = true + permission.save! + end + + test '07 monitoring with correct token and invalid permission' do + + permission = Permission.find_by(name: 'admin.monitoring') + permission.active = false + permission.save! + + # health_check + get "/api/v1/monitoring/health_check?token=#{@token}", {}, @headers + assert_response(200) + + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_not(result['error']) + assert_equal(true, result['healthy']) + assert_equal('success', result['message']) + + # status + get "/api/v1/monitoring/status?token=#{@token}", {}, @headers + assert_response(200) + + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_not(result['error']) + assert(result.key?('agents')) + assert(result.key?('last_login')) + assert(result.key?('counts')) + assert(result.key?('last_created_at')) + + # token + post '/api/v1/monitoring/token', { token: @token }.to_json, @headers + assert_response(401) + + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_not(result['token']) + assert_equal('authentication failed', result['error']) + + permission.active = true + permission.save! + + end + + test '08 check health false' do + + channel = Channel.find_by(active: true) + channel.status_in = 'ok' + channel.status_out = 'error' + channel.last_log_in = nil + channel.last_log_out = nil + channel.save! + + # health_check + get "/api/v1/monitoring/health_check?token=#{@token}", {}, @headers + assert_response(200) + + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert(result['message']) + assert(result['issues']) + assert_equal(false, result['healthy']) + assert_equal('Channel: Email::Notification out ', result['message']) + + scheduler = Scheduler.where(active: true).last + scheduler.last_run = Time.zone.now - 1.day + scheduler.period = 600 + scheduler.save! + + # health_check + get "/api/v1/monitoring/health_check?token=#{@token}", {}, @headers + assert_response(200) + + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert(result['message']) + assert(result['issues']) + assert_equal(false, result['healthy']) + assert_equal('Channel: Email::Notification out ;scheduler not running', result['message']) + + dir = "#{Rails.root}/tmp/unprocessable_mail" + FileUtils.touch("#{dir}/test.eml") + + # health_check + get "/api/v1/monitoring/health_check?token=#{@token}", {}, @headers + assert_response(200) + + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert(result['message']) + assert(result['issues']) + assert_equal(false, result['healthy']) + assert_equal('Channel: Email::Notification out ;unprocessable mails: 1;scheduler not running', result['message']) + + end + +end diff --git a/test/controllers/tickets_controller_test.rb b/test/controllers/tickets_controller_test.rb index 21b8ccee5..ef9bc478f 100644 --- a/test/controllers/tickets_controller_test.rb +++ b/test/controllers/tickets_controller_test.rb @@ -318,7 +318,7 @@ class TicketsControllerTest < ActionDispatch::IntegrationTest result = JSON.parse(@response.body) assert_equal(Hash, result.class) assert_equal(ticket.id, result['ticket_id']) - assert_equal('Tickets Agent via Zammad ', result['from']) + assert_equal('"Tickets Agent via Zammad" ', result['from']) assert_equal('some subject', result['subject']) assert_equal('some body', result['body']) assert_equal('text/plain', result['content_type']) @@ -335,7 +335,7 @@ class TicketsControllerTest < ActionDispatch::IntegrationTest result = JSON.parse(@response.body) assert_equal(Hash, result.class) assert_equal(ticket.id, result['ticket_id']) - assert_equal('Tickets Agent via Zammad ', result['from']) + assert_equal('"Tickets Agent via Zammad" ', result['from']) assert_equal('new subject', result['subject']) assert_equal('some body', result['body']) assert_equal('text/plain', result['content_type']) @@ -444,7 +444,7 @@ class TicketsControllerTest < ActionDispatch::IntegrationTest result = JSON.parse(@response.body) assert_equal(Hash, result.class) assert_equal(ticket.id, result['ticket_id']) - assert_equal('Tickets Admin via Zammad ', result['from']) + assert_equal('"Tickets Admin via Zammad" ', result['from']) assert_equal('some subject', result['subject']) assert_equal('some body', result['body']) assert_equal('text/plain', result['content_type']) diff --git a/test/integration/otrs_import_test.rb b/test/integration/otrs_import_test.rb index 194e402f1..95a596180 100644 --- a/test/integration/otrs_import_test.rb +++ b/test/integration/otrs_import_test.rb @@ -19,15 +19,12 @@ class OtrsImportTest < ActiveSupport::TestCase test 'check settings' do http = nil system_id = nil - fqdn = nil if ENV['IMPORT_OTRS_ENDPOINT'] =~ %r{^(http|https)://((.+?)\..+?)/} http = $1 - fqdn = $2 system_id = $3 system_id.gsub!(/[A-z]/, '') # strip chars end assert_equal( system_id, Setting.get('system_id'), 'system_id' ) - assert_equal( fqdn, Setting.get('fqdn'), 'fqdn' ) assert_equal( http, Setting.get('http_type'), 'http_type' ) assert_equal( 'Example Company', Setting.get('organization'), 'organization' ) end diff --git a/test/unit/email_build_test.rb b/test/unit/email_build_test.rb index 326408785..5566ac2b9 100644 --- a/test/unit/email_build_test.rb +++ b/test/unit/email_build_test.rb @@ -211,4 +211,20 @@ text assert_equal(html_should, html_with_fixes) end + test 'from checks' do + + quoted_in_one_line = Channel::EmailBuild.recipient_line('Somebody @ "Company"', 'some.body@example.com') + assert_equal('"Somebody @ \"Company\"" ', quoted_in_one_line) + + quoted_in_one_line = Channel::EmailBuild.recipient_line('Somebody', 'some.body@example.com') + assert_equal('Somebody ', quoted_in_one_line) + + quoted_in_one_line = Channel::EmailBuild.recipient_line('Somebody | Some Org', 'some.body@example.com') + assert_equal('"Somebody | Some Org" ', quoted_in_one_line) + + quoted_in_one_line = Channel::EmailBuild.recipient_line('Test Master Agent via Support', 'some.body@example.com') + assert_equal('"Test Master Agent via Support" ', quoted_in_one_line) + + end + end diff --git a/test/unit/email_process_auto_response_test.rb b/test/unit/email_process_auto_response_test.rb index a0b37e928..1d81118f6 100644 --- a/test/unit/email_process_auto_response_test.rb +++ b/test/unit/email_process_auto_response_test.rb @@ -105,6 +105,39 @@ Some Text" Scheduler.worker(true) assert_equal(1, article_p.ticket.articles.count) + email_raw_string = "From: me@example.com +To: customer@example.com +Subject: some new subject +List-Unsubscribe: + +Some Text" + + fqdn = Setting.get('fqdn') + email_raw_string = "From: me@example.com +To: customer@example.com +Subject: some new subject +Message-ID: <1234@#{fqdn}> + +Some Text" + + ticket_p, article_p, user_p, mail = Channel::EmailParser.new.process({}, email_raw_string) + assert_equal(false, mail['x-zammad-send-auto-response'.to_sym]) + Scheduler.worker(true) + assert_equal(1, article_p.ticket.articles.count) + + fqdn = Setting.get('fqdn') + email_raw_string = "From: me@example.com +To: customer@example.com +Subject: some new subject +Message-ID: <1234@not_matching.#{fqdn}> + +Some Text" + + ticket_p, article_p, user_p, mail = Channel::EmailParser.new.process({}, email_raw_string) + assert_equal(true, mail['x-zammad-send-auto-response'.to_sym]) + Scheduler.worker(true) + assert_equal(2, article_p.ticket.articles.count) + email_raw_string = "Return-Path: X-Original-To: sales@znuny.com Received: from mail-qk0-f170.example.com (mail-qk0-f170.example.com [209.1.1.1])