Merge remote-tracking branch 'upstream/develop' into develop
This commit is contained in:
commit
2bc25e322e
180 changed files with 9574 additions and 1972 deletions
|
@ -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:
|
||||
|
|
3
.rspec
Normal file
3
.rspec
Normal file
|
@ -0,0 +1,3 @@
|
|||
--format documentation
|
||||
--color
|
||||
--require spec_helper
|
2
Gemfile
2
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
|
||||
|
|
22
Gemfile.lock
22
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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
47
app/assets/javascripts/app/controllers/monitoring.coffee
Normal file
47
app/assets/javascripts/app/controllers/monitoring.coffee
Normal file
|
@ -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')
|
|
@ -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')
|
||||
|
|
|
@ -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() ]
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<h2><%- @T('Token Access') %> (HTTP Token Authentication)</h2>
|
||||
</div>
|
||||
|
||||
<p><%- @T('Enable REST API using tokens (not username/email addeess and password). Each user need to create own access tokens in user profile.') %></p>
|
||||
<p><%- @T('Enable REST API using tokens (not username/email address and password). Each user need to create own access tokens in user profile.') %></p>
|
||||
|
||||
<p><%- @T('Example') %>:</p>
|
||||
|
||||
|
@ -74,4 +74,4 @@ OAuth URLs are:
|
|||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -7,7 +7,9 @@
|
|||
<p>
|
||||
<%- @T('Download and install the OTRS Migration Plugin on your OTRS System') %>:
|
||||
</p>
|
||||
<a class="btn btn--primary btn--download js-download" download href="https://portal.znuny.com/api/addon_repos/public/268/latest"><%- @Icon('download') %> <%- @T('Migration Plugin') %></a>
|
||||
<a class="btn btn--primary btn--download js-download" target=_blank href="https://portal.znuny.com/api/addon_repos/public/617/latest"><%- @Icon('download') %> <%- @T('Migration Plugin for OTRS 5') %></a>
|
||||
<a class="btn btn--primary btn--download js-download" target=_blank href="https://portal.znuny.com/api/addon_repos/public/383/latest"><%- @Icon('download') %> <%- @T('Migration Plugin for OTRS 4') %></a>
|
||||
<a class="btn btn--primary btn--download js-download" target=_blank href="https://portal.znuny.com/api/addon_repos/public/287/latest"><%- @Icon('download') %> <%- @T('Migration Plugin for OTRS 3.3 - 3.1') %></a>
|
||||
</div>
|
||||
<div class="wizard-controls horizontal center">
|
||||
<a class="btn btn--text btn--secondary" href="#import"><%- @T('Go Back') %></a>
|
||||
|
|
39
app/assets/javascripts/app/views/monitoring.jst.eco
Normal file
39
app/assets/javascripts/app/views/monitoring.jst.eco
Normal file
|
@ -0,0 +1,39 @@
|
|||
<div class="page-header">
|
||||
<div class="page-header-title">
|
||||
<h1><%- @T('Monitoring') %><small></small></h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content">
|
||||
|
||||
<div class="settings-entry">
|
||||
<div class="page-header-title">
|
||||
<h2><%- @T('Current Token') %></h2>
|
||||
</div>
|
||||
<p><input class="js-select js-token" type="text" value="<%= @data.token %>" readonly></p>
|
||||
<button class="btn btn--primary js-resetToken"><%- @T('Reset') %></button>
|
||||
</div>
|
||||
|
||||
<div class="settings-entry">
|
||||
<div class="page-header-title">
|
||||
<h2><%- @T('Health Check') %></h2>
|
||||
</div>
|
||||
<p><%- @T('Health information can be retrieved as JSON using') %>:</p>
|
||||
<input class="js-select js-url" type="text" value="<%- @C('http_type') %>://<%- @C('fqdn') %>/<%- @C('api_path') %>/monitoring/health_check?token=<%= @data.token %>" readonly></p>
|
||||
</div>
|
||||
|
||||
<div class="settings-entry">
|
||||
<div class="page-header-title">
|
||||
<h2><% if _.isEmpty(@data.issues): %><%- @Icon('status', 'ok inline') %><% else: %><%- @Icon('status', 'error inline') %><% end %> <%- @T('Current Status') %></h2>
|
||||
</div>
|
||||
<ul>
|
||||
<% if _.isEmpty(@data.issues): %>
|
||||
<li><%- @T('no issues') %>
|
||||
<% else: %>
|
||||
<% for issue in @data.issues: %>
|
||||
<li><%= issue %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
|
@ -13,8 +13,8 @@
|
|||
<div class="box box--message">
|
||||
<h2><%- @T('Inline translation') %></h2>
|
||||
<p><%- @T('To do easier translations you can enable and disable inline translation feature by pressing "%s".', 'ctrl+alt+t') %></p>
|
||||
<p><%- @T('Text with disabled inline translations looks like') %> <button class="btn btn-primary"><%- @Ti('Some Text') %></button></p>
|
||||
<p><%- @T('Text with enabled inline translations looks like') %> <button class="btn btn-primary"><span class="translation" contenteditable="true"><%- @Ti('Some Text') %></button></span></p>
|
||||
<p><%- @T('Text with disabled inline translations looks like') %> <button class="btn btn-primary"><%- @Ti('Some Text') %></button></p>
|
||||
<p><%- @T('Text with enabled inline translations looks like') %> <button class="btn btn-primary"><span class="translation" contenteditable="true"><%- @Ti('Some Text') %></button></span></p>
|
||||
<p><%- @T('Just click into the marker and update the words just in place. Enjoy!') %></p>
|
||||
<p><%- @T('If you want to translate it via the translation table, just go ahead below.') %></p>
|
||||
</div>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
182
app/controllers/monitoring_controller.rb
Normal file
182
app/controllers/monitoring_controller.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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\"" <some.body@example.com>'
|
||||
|
||||
=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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
Your #{product_name} password has been changed
|
||||
Your #{config.product_name} password has been changed
|
||||
|
||||
<p>Hi #{user.firstname},</p>
|
||||
<br>
|
||||
<p>The password for your #{product_name} account <b>#{user.login}</b> has been changed recently.</p>
|
||||
<p>The password for your #{config.product_name} account <b>#{user.login}</b> has been changed recently.</p>
|
||||
<br>
|
||||
<p>This activity is not known to you? If not, contact your system administrator.</p>
|
||||
<br>
|
||||
<p>Your #{product_name} Team</p>
|
||||
<p>Your #{config.product_name} Team</p>
|
||||
|
|
3
bin/rspec
Executable file
3
bin/rspec
Executable file
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/env ruby
|
||||
require 'bundler/setup'
|
||||
load Gem.bin_path('rspec-core', 'rspec')
|
15
bin/spring
Executable file
15
bin/spring
Executable file
|
@ -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
|
|
@ -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
|
||||
|
|
8
config/routes/monitoring.rb
Normal file
8
config/routes/monitoring.rb
Normal file
|
@ -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
|
37
db/migrate/20161122000001_monitoring_issue_453.rb
Normal file
37
db/migrate/20161122000001_monitoring_issue_453.rb
Normal file
|
@ -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
|
41
db/seeds.rb
41
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(
|
||||
|
|
|
@ -146,6 +146,9 @@ returns
|
|||
}
|
||||
}
|
||||
|
||||
# reset primary key sequences
|
||||
DbHelper.import_post
|
||||
|
||||
# remove auto wizard file
|
||||
FileUtils.rm auto_wizard_file_location
|
||||
|
||||
|
|
31
lib/db_helper.rb
Normal file
31
lib/db_helper.rb
Normal file
|
@ -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
|
28
lib/import/base_factory.rb
Normal file
28
lib/import/base_factory.rb
Normal file
|
@ -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
|
16
lib/import/factory.rb
Normal file
16
lib/import/factory.rb
Normal file
|
@ -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
|
31
lib/import/helper.rb
Normal file
31
lib/import/helper.rb
Normal file
|
@ -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
|
1692
lib/import/otrs.rb
1692
lib/import/otrs.rb
File diff suppressed because it is too large
Load diff
136
lib/import/otrs/article.rb
Normal file
136
lib/import/otrs/article.rb
Normal file
|
@ -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
|
104
lib/import/otrs/article/attachment_factory.rb
Normal file
104
lib/import/otrs/article/attachment_factory.rb
Normal file
|
@ -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
|
84
lib/import/otrs/article_customer.rb
Normal file
84
lib/import/otrs/article_customer.rb
Normal file
|
@ -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
|
14
lib/import/otrs/article_customer_factory.rb
Normal file
14
lib/import/otrs/article_customer_factory.rb
Normal file
|
@ -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
|
7
lib/import/otrs/article_factory.rb
Normal file
7
lib/import/otrs/article_factory.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
module Import
|
||||
module OTRS
|
||||
module ArticleFactory
|
||||
extend Import::Factory
|
||||
end
|
||||
end
|
||||
end
|
62
lib/import/otrs/async.rb
Normal file
62
lib/import/otrs/async.rb
Normal file
|
@ -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
|
67
lib/import/otrs/customer.rb
Normal file
67
lib/import/otrs/customer.rb
Normal file
|
@ -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
|
7
lib/import/otrs/customer_factory.rb
Normal file
7
lib/import/otrs/customer_factory.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
module Import
|
||||
module OTRS
|
||||
module CustomerFactory
|
||||
extend Import::Factory
|
||||
end
|
||||
end
|
||||
end
|
92
lib/import/otrs/customer_user.rb
Normal file
92
lib/import/otrs/customer_user.rb
Normal file
|
@ -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
|
7
lib/import/otrs/customer_user_factory.rb
Normal file
7
lib/import/otrs/customer_user_factory.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
module Import
|
||||
module OTRS
|
||||
module CustomerUserFactory
|
||||
extend Import::Factory
|
||||
end
|
||||
end
|
||||
end
|
36
lib/import/otrs/diff.rb
Normal file
36
lib/import/otrs/diff.rb
Normal file
|
@ -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
|
61
lib/import/otrs/dynamic_field.rb
Normal file
61
lib/import/otrs/dynamic_field.rb
Normal file
|
@ -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
|
22
lib/import/otrs/dynamic_field/checkbox.rb
Normal file
22
lib/import/otrs/dynamic_field/checkbox.rb
Normal file
|
@ -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
|
24
lib/import/otrs/dynamic_field/date.rb
Normal file
24
lib/import/otrs/dynamic_field/date.rb
Normal file
|
@ -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
|
24
lib/import/otrs/dynamic_field/date_time.rb
Normal file
24
lib/import/otrs/dynamic_field/date_time.rb
Normal file
|
@ -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
|
20
lib/import/otrs/dynamic_field/dropdown.rb
Normal file
20
lib/import/otrs/dynamic_field/dropdown.rb
Normal file
|
@ -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
|
20
lib/import/otrs/dynamic_field/multiselect.rb
Normal file
20
lib/import/otrs/dynamic_field/multiselect.rb
Normal file
|
@ -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
|
19
lib/import/otrs/dynamic_field/text.rb
Normal file
19
lib/import/otrs/dynamic_field/text.rb
Normal file
|
@ -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
|
18
lib/import/otrs/dynamic_field/text_area.rb
Normal file
18
lib/import/otrs/dynamic_field/text_area.rb
Normal file
|
@ -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
|
52
lib/import/otrs/dynamic_field_factory.rb
Normal file
52
lib/import/otrs/dynamic_field_factory.rb
Normal file
|
@ -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
|
37
lib/import/otrs/helper.rb
Normal file
37
lib/import/otrs/helper.rb
Normal file
|
@ -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
|
46
lib/import/otrs/history.rb
Normal file
46
lib/import/otrs/history.rb
Normal file
|
@ -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
|
20
lib/import/otrs/history/article.rb
Normal file
20
lib/import/otrs/history/article.rb
Normal file
|
@ -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
|
33
lib/import/otrs/history/move.rb
Normal file
33
lib/import/otrs/history/move.rb
Normal file
|
@ -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
|
18
lib/import/otrs/history/new_ticket.rb
Normal file
18
lib/import/otrs/history/new_ticket.rb
Normal file
|
@ -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
|
33
lib/import/otrs/history/priority_update.rb
Normal file
33
lib/import/otrs/history/priority_update.rb
Normal file
|
@ -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
|
39
lib/import/otrs/history/state_update.rb
Normal file
39
lib/import/otrs/history/state_update.rb
Normal file
|
@ -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
|
40
lib/import/otrs/history_factory.rb
Normal file
40
lib/import/otrs/history_factory.rb
Normal file
|
@ -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
|
74
lib/import/otrs/import_stats.rb
Normal file
74
lib/import/otrs/import_stats.rb
Normal file
|
@ -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
|
58
lib/import/otrs/priority.rb
Normal file
58
lib/import/otrs/priority.rb
Normal file
|
@ -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
|
7
lib/import/otrs/priority_factory.rb
Normal file
7
lib/import/otrs/priority_factory.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
module Import
|
||||
module OTRS
|
||||
module PriorityFactory
|
||||
extend Import::Factory
|
||||
end
|
||||
end
|
||||
end
|
58
lib/import/otrs/queue.rb
Normal file
58
lib/import/otrs/queue.rb
Normal file
|
@ -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
|
7
lib/import/otrs/queue_factory.rb
Normal file
7
lib/import/otrs/queue_factory.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
module Import
|
||||
module OTRS
|
||||
module QueueFactory
|
||||
extend Import::Factory
|
||||
end
|
||||
end
|
||||
end
|
93
lib/import/otrs/requester.rb
Normal file
93
lib/import/otrs/requester.rb
Normal file
|
@ -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
|
70
lib/import/otrs/state.rb
Normal file
70
lib/import/otrs/state.rb
Normal file
|
@ -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
|
22
lib/import/otrs/state_factory.rb
Normal file
22
lib/import/otrs/state_factory.rb
Normal file
|
@ -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
|
51
lib/import/otrs/sys_config_factory.rb
Normal file
51
lib/import/otrs/sys_config_factory.rb
Normal file
|
@ -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
|
172
lib/import/otrs/ticket.rb
Normal file
172
lib/import/otrs/ticket.rb
Normal file
|
@ -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
|
7
lib/import/otrs/ticket_factory.rb
Normal file
7
lib/import/otrs/ticket_factory.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
module Import
|
||||
module OTRS
|
||||
module TicketFactory
|
||||
extend Import::Factory
|
||||
end
|
||||
end
|
||||
end
|
167
lib/import/otrs/user.rb
Normal file
167
lib/import/otrs/user.rb
Normal file
|
@ -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
|
7
lib/import/otrs/user_factory.rb
Normal file
7
lib/import/otrs/user_factory.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
module Import
|
||||
module OTRS
|
||||
module UserFactory
|
||||
extend Import::Factory
|
||||
end
|
||||
end
|
||||
end
|
18
lib/import/transaction_factory.rb
Normal file
18
lib/import/transaction_factory.rb
Normal file
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,3 +1,152 @@
|
|||
/*!
|
||||
* ----------------------------------------------------------------------------
|
||||
* "THE BEER-WARE LICENSE" (Revision 42):
|
||||
* <jevin9@gmail.com> 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('<div class="autogrow-textarea-mirror"></div>');
|
||||
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(/ /g, ' ')
|
||||
.replace(/\n/g, '<br />') +
|
||||
(settings.extraLine? '.<br/>.' : '')
|
||||
;
|
||||
|
||||
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, '>')
|
||||
.replace(/"/g, '"');
|
||||
};
|
||||
}
|
||||
(function() {
|
||||
(function() {
|
||||
if (this.agent.avatar) {
|
||||
__out.push('\n<img class="zammad-chat-agent-avatar" src="');
|
||||
__out.push(__sanitize(this.agent.avatar));
|
||||
__out.push('">\n');
|
||||
}
|
||||
|
||||
__out.push('\n<span class="zammad-chat-agent-sentence">\n <span class="zammad-chat-agent-name">');
|
||||
|
||||
__out.push(__sanitize(this.agent.name));
|
||||
|
||||
__out.push('</span>\n</span>');
|
||||
|
||||
}).call(this);
|
||||
|
||||
}).call(__obj);
|
||||
__obj.safe = __objSafe, __obj.escape = __escape;
|
||||
return __out.join('');
|
||||
};
|
||||
|
||||
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
|
||||
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):
|
||||
* <jevin9@gmail.com> 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('<div class="autogrow-textarea-mirror"></div>');
|
||||
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(/ /g, ' ')
|
||||
.replace(/\n/g, '<br />') +
|
||||
(settings.extraLine? '.<br/>.' : '')
|
||||
;
|
||||
|
||||
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, '>')
|
||||
.replace(/"/g, '"');
|
||||
};
|
||||
}
|
||||
(function() {
|
||||
(function() {
|
||||
if (this.agent.avatar) {
|
||||
__out.push('\n<img class="zammad-chat-agent-avatar" src="');
|
||||
__out.push(__sanitize(this.agent.avatar));
|
||||
__out.push('">\n');
|
||||
}
|
||||
|
||||
__out.push('\n<span class="zammad-chat-agent-sentence">\n <span class="zammad-chat-agent-name">');
|
||||
|
||||
__out.push(__sanitize(this.agent.name));
|
||||
|
||||
__out.push('</span>\n</span>');
|
||||
|
||||
}).call(this);
|
||||
|
||||
}).call(__obj);
|
||||
__obj.safe = __objSafe, __obj.escape = __escape;
|
||||
return __out.join('');
|
||||
};
|
||||
|
||||
if (!window.zammadChatTemplates) {
|
||||
window.zammadChatTemplates = {};
|
||||
}
|
||||
|
|
4
public/assets/chat/chat.min.js
vendored
4
public/assets/chat/chat.min.js
vendored
File diff suppressed because one or more lines are too long
|
@ -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;
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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);
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
10
spec/fixtures/import/otrs/article/attachment/default.json
vendored
Normal file
10
spec/fixtures/import/otrs/article/attachment/default.json
vendored
Normal file
|
@ -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"
|
||||
}
|
97
spec/fixtures/import/otrs/article/customer_phone.json
vendored
Normal file
97
spec/fixtures/import/otrs/article/customer_phone.json
vendored
Normal file
|
@ -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\" <kunde2@kunde.de>,"
|
||||
}
|
108
spec/fixtures/import/otrs/article/customer_phone_attachment.json
vendored
Normal file
108
spec/fixtures/import/otrs/article/customer_phone_attachment.json
vendored
Normal file
|
@ -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\" <kunde2@kunde.de>,"
|
||||
}
|
130
spec/fixtures/import/otrs/customer/default.json
vendored
Normal file
130
spec/fixtures/import/otrs/customer/default.json
vendored
Normal file
|
@ -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"
|
||||
}
|
348
spec/fixtures/import/otrs/customer_user/default.json
vendored
Normal file
348
spec/fixtures/import/otrs/customer_user/default.json
vendored
Normal file
|
@ -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
|
||||
}
|
15
spec/fixtures/import/otrs/dynamic_field/checkbox/default.json
vendored
Normal file
15
spec/fixtures/import/otrs/dynamic_field/checkbox/default.json
vendored
Normal file
|
@ -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"
|
||||
}
|
19
spec/fixtures/import/otrs/dynamic_field/date/default.json
vendored
Normal file
19
spec/fixtures/import/otrs/dynamic_field/date/default.json
vendored
Normal file
|
@ -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"
|
||||
}
|
19
spec/fixtures/import/otrs/dynamic_field/date_time/default.json
vendored
Normal file
19
spec/fixtures/import/otrs/dynamic_field/date_time/default.json
vendored
Normal file
|
@ -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"
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue