- Added Exchange integration.
- Added Sequencer. - Prepared migration of LDAP integration to Sequencer. - Added and improved RSpec support helpers.
This commit is contained in:
parent
2b4b1a58cc
commit
4937d742ea
101 changed files with 4560 additions and 125 deletions
3
Gemfile
3
Gemfile
|
@ -80,6 +80,9 @@ gem 'browser'
|
||||||
gem 'slack-notifier'
|
gem 'slack-notifier'
|
||||||
gem 'clearbit'
|
gem 'clearbit'
|
||||||
gem 'zendesk_api'
|
gem 'zendesk_api'
|
||||||
|
gem 'viewpoint'
|
||||||
|
gem 'rubyntlm', git: 'https://github.com/wimm/rubyntlm.git'
|
||||||
|
gem 'autodiscover', git: 'https://github.com/thorsteneckel/autodiscover.git'
|
||||||
|
|
||||||
# event machine
|
# event machine
|
||||||
gem 'eventmachine'
|
gem 'eventmachine'
|
||||||
|
|
30
Gemfile.lock
30
Gemfile.lock
|
@ -1,3 +1,19 @@
|
||||||
|
GIT
|
||||||
|
remote: https://github.com/thorsteneckel/autodiscover.git
|
||||||
|
revision: 29d713ee0c8c25fcf74c4292ff13fe1fa4d0d827
|
||||||
|
specs:
|
||||||
|
autodiscover (1.0.2)
|
||||||
|
httpclient
|
||||||
|
logging
|
||||||
|
nokogiri
|
||||||
|
nori
|
||||||
|
|
||||||
|
GIT
|
||||||
|
remote: https://github.com/wimm/rubyntlm.git
|
||||||
|
revision: 53969639b87b9e5d5fef560f19cf0d977259591c
|
||||||
|
specs:
|
||||||
|
rubyntlm (0.1.2)
|
||||||
|
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
|
@ -166,6 +182,7 @@ GEM
|
||||||
domain_name (~> 0.5)
|
domain_name (~> 0.5)
|
||||||
http-form_data (1.0.3)
|
http-form_data (1.0.3)
|
||||||
http_parser.rb (0.6.0)
|
http_parser.rb (0.6.0)
|
||||||
|
httpclient (2.8.3)
|
||||||
i18n (0.8.6)
|
i18n (0.8.6)
|
||||||
icalendar (2.4.1)
|
icalendar (2.4.1)
|
||||||
inflection (1.0.0)
|
inflection (1.0.0)
|
||||||
|
@ -181,6 +198,10 @@ GEM
|
||||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||||
rb-inotify (~> 0.9, >= 0.9.7)
|
rb-inotify (~> 0.9, >= 0.9.7)
|
||||||
ruby_dep (~> 1.2)
|
ruby_dep (~> 1.2)
|
||||||
|
little-plugger (1.1.4)
|
||||||
|
logging (2.2.2)
|
||||||
|
little-plugger (~> 1.1)
|
||||||
|
multi_json (~> 1.10)
|
||||||
loofah (2.0.3)
|
loofah (2.0.3)
|
||||||
nokogiri (>= 1.5.9)
|
nokogiri (>= 1.5.9)
|
||||||
lumberjack (1.0.10)
|
lumberjack (1.0.10)
|
||||||
|
@ -205,6 +226,7 @@ GEM
|
||||||
netrc (0.11.0)
|
netrc (0.11.0)
|
||||||
nokogiri (1.8.0)
|
nokogiri (1.8.0)
|
||||||
mini_portile2 (~> 2.2.0)
|
mini_portile2 (~> 2.2.0)
|
||||||
|
nori (2.6.0)
|
||||||
notiffany (0.1.1)
|
notiffany (0.1.1)
|
||||||
nenv (~> 0.1)
|
nenv (~> 0.1)
|
||||||
shellany (~> 0.0)
|
shellany (~> 0.0)
|
||||||
|
@ -408,6 +430,11 @@ GEM
|
||||||
valid_email2 (2.0.0)
|
valid_email2 (2.0.0)
|
||||||
activemodel (>= 3.2)
|
activemodel (>= 3.2)
|
||||||
mail (~> 2.5)
|
mail (~> 2.5)
|
||||||
|
viewpoint (1.1.0)
|
||||||
|
httpclient
|
||||||
|
logging
|
||||||
|
nokogiri
|
||||||
|
rubyntlm
|
||||||
webmock (3.0.1)
|
webmock (3.0.1)
|
||||||
addressable (>= 2.3.6)
|
addressable (>= 2.3.6)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
|
@ -428,6 +455,7 @@ DEPENDENCIES
|
||||||
activerecord-nulldb-adapter
|
activerecord-nulldb-adapter
|
||||||
activerecord-session_store
|
activerecord-session_store
|
||||||
argon2
|
argon2
|
||||||
|
autodiscover!
|
||||||
autoprefixer-rails
|
autoprefixer-rails
|
||||||
biz
|
biz
|
||||||
browser
|
browser
|
||||||
|
@ -479,6 +507,7 @@ DEPENDENCIES
|
||||||
rb-fsevent
|
rb-fsevent
|
||||||
rspec-rails
|
rspec-rails
|
||||||
rubocop
|
rubocop
|
||||||
|
rubyntlm!
|
||||||
sass-rails
|
sass-rails
|
||||||
selenium-webdriver
|
selenium-webdriver
|
||||||
simple-rss
|
simple-rss
|
||||||
|
@ -496,6 +525,7 @@ DEPENDENCIES
|
||||||
uglifier
|
uglifier
|
||||||
unicorn
|
unicorn
|
||||||
valid_email2
|
valid_email2
|
||||||
|
viewpoint
|
||||||
webmock
|
webmock
|
||||||
writeexcel
|
writeexcel
|
||||||
zendesk_api
|
zendesk_api
|
||||||
|
|
|
@ -0,0 +1,522 @@
|
||||||
|
class Index extends App.ControllerIntegrationBase
|
||||||
|
featureIntegration: 'exchange_integration'
|
||||||
|
featureName: 'Exchange'
|
||||||
|
featureConfig: 'exchange_config'
|
||||||
|
description: [
|
||||||
|
['This service enables Zammad to connect with your Exchange server.']
|
||||||
|
]
|
||||||
|
events:
|
||||||
|
'change .js-switch input': 'switch'
|
||||||
|
|
||||||
|
render: =>
|
||||||
|
super
|
||||||
|
new Form(
|
||||||
|
el: @$('.js-form')
|
||||||
|
)
|
||||||
|
|
||||||
|
#new App.ImportJob(
|
||||||
|
# el: @$('.js-importJob')
|
||||||
|
# facility: 'exchange'
|
||||||
|
#)
|
||||||
|
|
||||||
|
new App.HttpLog(
|
||||||
|
el: @$('.js-log')
|
||||||
|
facility: 'exchange'
|
||||||
|
)
|
||||||
|
|
||||||
|
switch: =>
|
||||||
|
super
|
||||||
|
active = @$('.js-switch input').prop('checked')
|
||||||
|
if active
|
||||||
|
job_start = =>
|
||||||
|
@ajax(
|
||||||
|
id: 'jobs_config'
|
||||||
|
type: 'POST'
|
||||||
|
url: "#{@apiPath}/integration/exchange/job_start"
|
||||||
|
processData: true
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
@render(true)
|
||||||
|
)
|
||||||
|
|
||||||
|
App.Delay.set(
|
||||||
|
job_start,
|
||||||
|
600,
|
||||||
|
'job_start',
|
||||||
|
)
|
||||||
|
|
||||||
|
class Form extends App.Controller
|
||||||
|
elements:
|
||||||
|
'.js-lastImport': 'lastImport'
|
||||||
|
'.js-wizard': 'wizardButton'
|
||||||
|
events:
|
||||||
|
'click .js-wizard': 'startWizard'
|
||||||
|
'click .js-start-sync': 'startSync'
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
@render()
|
||||||
|
@lastResult()
|
||||||
|
@activeDryRun()
|
||||||
|
|
||||||
|
currentConfig: ->
|
||||||
|
App.Setting.get('exchange_config') || {}
|
||||||
|
|
||||||
|
setConfig: (value) =>
|
||||||
|
App.Setting.set('exchange_config', value, {notify: true})
|
||||||
|
@startSync()
|
||||||
|
|
||||||
|
render: (top = false) =>
|
||||||
|
@config = @currentConfig()
|
||||||
|
|
||||||
|
folders = []
|
||||||
|
if !_.isEmpty(@config.folders)
|
||||||
|
for folder_id in @config.folders
|
||||||
|
folders.push @config.wizardData.backend_folders[folder_id]
|
||||||
|
|
||||||
|
@html App.view('integration/exchange')(
|
||||||
|
config: @config,
|
||||||
|
folders: folders
|
||||||
|
)
|
||||||
|
if _.isEmpty(@config)
|
||||||
|
@$('.js-notConfigured').removeClass('hide')
|
||||||
|
@$('.js-summary').addClass('hide')
|
||||||
|
else
|
||||||
|
@$('.js-notConfigured').addClass('hide')
|
||||||
|
@$('.js-summary').removeClass('hide')
|
||||||
|
|
||||||
|
if top
|
||||||
|
a = =>
|
||||||
|
@scrollToIfNeeded($('.content.active .page-header'))
|
||||||
|
@delay(a, 500)
|
||||||
|
|
||||||
|
startSync: =>
|
||||||
|
@ajax(
|
||||||
|
id: 'jobs_config'
|
||||||
|
type: 'POST'
|
||||||
|
url: "#{@apiPath}/integration/exchange/job_start"
|
||||||
|
processData: true
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
@render(true)
|
||||||
|
@lastResult()
|
||||||
|
)
|
||||||
|
|
||||||
|
startWizard: (e) =>
|
||||||
|
e.preventDefault()
|
||||||
|
new ConnectionWizard(
|
||||||
|
container: @el.closest('.content')
|
||||||
|
config: @config
|
||||||
|
callback: (config) =>
|
||||||
|
@setConfig(config)
|
||||||
|
)
|
||||||
|
|
||||||
|
lastResult: =>
|
||||||
|
@ajax(
|
||||||
|
id: 'jobs_start_index'
|
||||||
|
type: 'GET'
|
||||||
|
url: "#{@apiPath}/integration/exchange/job_start"
|
||||||
|
processData: true
|
||||||
|
success: (job, status, xhr) =>
|
||||||
|
if !_.isEmpty(job)
|
||||||
|
if !@lastResultShowJob || @lastResultShowJob.updated_at != job.updated_at
|
||||||
|
@lastResultShowJob = job
|
||||||
|
@lastResultShow(job)
|
||||||
|
if job.finished_at
|
||||||
|
@wizardButton.attr('disabled', false)
|
||||||
|
else
|
||||||
|
@wizardButton.attr('disabled', true)
|
||||||
|
@delay(@lastResult, 5000)
|
||||||
|
)
|
||||||
|
|
||||||
|
lastResultShow: (job) =>
|
||||||
|
if _.isEmpty(job)
|
||||||
|
@lastImport.html('')
|
||||||
|
return
|
||||||
|
countDone = job.result.created + job.result.updated + job.result.unchanged + job.result.skipped + job.result.failed
|
||||||
|
if !job.result.roles
|
||||||
|
job.result.roles = {}
|
||||||
|
for role_id, statistic of job.result.role_ids
|
||||||
|
role = App.Role.find(role_id)
|
||||||
|
job.result.roles[role.displayName()] = statistic
|
||||||
|
el = $(App.view('integration/exchange_last_import')(job: job, countDone: countDone))
|
||||||
|
@lastImport.html(el)
|
||||||
|
|
||||||
|
activeDryRun: =>
|
||||||
|
@ajax(
|
||||||
|
id: 'jobs_try_index'
|
||||||
|
type: 'GET'
|
||||||
|
url: "#{@apiPath}/integration/exchange/job_try"
|
||||||
|
data:
|
||||||
|
finished: false
|
||||||
|
processData: true
|
||||||
|
success: (job, status, xhr) =>
|
||||||
|
return if _.isEmpty(job)
|
||||||
|
|
||||||
|
# show analyzing
|
||||||
|
new ConnectionWizard(
|
||||||
|
container: @el.closest('.content')
|
||||||
|
config: job.payload
|
||||||
|
start: 'tryLoop'
|
||||||
|
callback: (config) =>
|
||||||
|
@wizardButton.attr('disabled', false)
|
||||||
|
@setConfig(config)
|
||||||
|
)
|
||||||
|
@wizardButton.attr('disabled', true)
|
||||||
|
)
|
||||||
|
|
||||||
|
class State
|
||||||
|
@current: ->
|
||||||
|
App.Setting.get('exchange_integration')
|
||||||
|
|
||||||
|
class ConnectionWizard extends App.WizardModal
|
||||||
|
wizardConfig: {}
|
||||||
|
slideMethod:
|
||||||
|
'js-folders': 'foldersShow'
|
||||||
|
'js-mapping': 'mappingShow'
|
||||||
|
|
||||||
|
events:
|
||||||
|
'submit form.js-discover': 'discover'
|
||||||
|
'submit form.js-bind': 'folders'
|
||||||
|
'submit form.js-folders': 'mapping'
|
||||||
|
'click .js-mapping .js-submitTry': 'mappingChange'
|
||||||
|
'click .js-try .js-submitSave': 'save'
|
||||||
|
'click .js-close': 'hide'
|
||||||
|
'click .js-remove': 'removeRow'
|
||||||
|
'click .js-userMappingForm .js-add': 'addUserMapping'
|
||||||
|
'click .js-goToSlide': 'goToSlide'
|
||||||
|
|
||||||
|
elements:
|
||||||
|
'.modal-body': 'body'
|
||||||
|
'.js-foldersSelect': 'foldersSelect'
|
||||||
|
'.js-userMappingForm': 'userMappingForm'
|
||||||
|
'.js-expertForm': 'expertForm'
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
|
||||||
|
if !_.isEmpty(@config)
|
||||||
|
@wizardConfig = @config
|
||||||
|
|
||||||
|
if @container
|
||||||
|
@el.addClass('modal--local')
|
||||||
|
|
||||||
|
@render()
|
||||||
|
|
||||||
|
@el.modal
|
||||||
|
keyboard: true
|
||||||
|
show: true
|
||||||
|
backdrop: true
|
||||||
|
container: @container
|
||||||
|
.on
|
||||||
|
'show.bs.modal': @onShow
|
||||||
|
'shown.bs.modal': @onShown
|
||||||
|
'hidden.bs.modal': =>
|
||||||
|
@el.remove()
|
||||||
|
|
||||||
|
if @slide
|
||||||
|
@showSlide(@slide)
|
||||||
|
else
|
||||||
|
@showDiscoverDetails()
|
||||||
|
|
||||||
|
if @start
|
||||||
|
@[@start]()
|
||||||
|
|
||||||
|
render: =>
|
||||||
|
@html App.view('integration/exchange_wizard')()
|
||||||
|
|
||||||
|
save: (e) =>
|
||||||
|
e.preventDefault()
|
||||||
|
@callback(@wizardConfig)
|
||||||
|
@hide(e)
|
||||||
|
|
||||||
|
showSlide: (slide) =>
|
||||||
|
method = @slideMethod[slide]
|
||||||
|
if method && @[method]
|
||||||
|
@[method](true)
|
||||||
|
super
|
||||||
|
|
||||||
|
showDiscoverDetails: =>
|
||||||
|
@$('.js-discover input[name="user"]').val(@wizardConfig.user)
|
||||||
|
@$('.js-discover input[name="password"]').val(@wizardConfig.password)
|
||||||
|
|
||||||
|
showBindDetails: =>
|
||||||
|
@$('.js-bind input[name="endpoint"]').val(@wizardConfig.endpoint)
|
||||||
|
@$('.js-bind input[name="user"]').val(@wizardConfig.user)
|
||||||
|
@$('.js-bind input[name="password"]').val(@wizardConfig.password)
|
||||||
|
|
||||||
|
discover: (e) =>
|
||||||
|
e.preventDefault()
|
||||||
|
@showSlide('js-connect')
|
||||||
|
params = @formParam(e.target)
|
||||||
|
@ajax(
|
||||||
|
id: 'exchange_discover'
|
||||||
|
type: 'POST'
|
||||||
|
url: "#{@apiPath}/integration/exchange/autodiscover"
|
||||||
|
data: JSON.stringify(params)
|
||||||
|
processData: true
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
if data.result isnt 'ok'
|
||||||
|
@showSlide('js-discover')
|
||||||
|
@showAlert('js-discover', data.message)
|
||||||
|
return
|
||||||
|
|
||||||
|
@wizardConfig.endpoint = data.endpoint
|
||||||
|
@wizardConfig.user = params.user
|
||||||
|
@wizardConfig.password = params.password
|
||||||
|
|
||||||
|
@showSlide('js-bind')
|
||||||
|
@showBindDetails()
|
||||||
|
|
||||||
|
error: (xhr, statusText, error) =>
|
||||||
|
detailsRaw = xhr.responseText
|
||||||
|
details = {}
|
||||||
|
if !_.isEmpty(detailsRaw)
|
||||||
|
details = JSON.parse(detailsRaw)
|
||||||
|
@showSlide('js-discover')
|
||||||
|
@showAlert('js-discover', details.error || 'Unable to perform backend.')
|
||||||
|
)
|
||||||
|
|
||||||
|
folders: (e) =>
|
||||||
|
e.preventDefault()
|
||||||
|
@showSlide('js-analyze')
|
||||||
|
params = @formParam(e.target)
|
||||||
|
@ajax(
|
||||||
|
id: 'exchange_folders'
|
||||||
|
type: 'POST'
|
||||||
|
url: "#{@apiPath}/integration/exchange/folders"
|
||||||
|
data: JSON.stringify(params)
|
||||||
|
processData: true
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
if data.result isnt 'ok'
|
||||||
|
@showSlide('js-bind')
|
||||||
|
@showAlert('js-bind', data.message)
|
||||||
|
return
|
||||||
|
|
||||||
|
@wizardConfig.endpoint = params.endpoint
|
||||||
|
@wizardConfig.user = params.user
|
||||||
|
@wizardConfig.password = params.password
|
||||||
|
|
||||||
|
# update wizard data
|
||||||
|
@wizardConfig.wizardData = {}
|
||||||
|
@wizardConfig.wizardData.backend_folders = data.folders
|
||||||
|
|
||||||
|
@foldersShow()
|
||||||
|
|
||||||
|
error: (xhr, statusText, error) =>
|
||||||
|
detailsRaw = xhr.responseText
|
||||||
|
details = {}
|
||||||
|
if !_.isEmpty(detailsRaw)
|
||||||
|
details = JSON.parse(detailsRaw)
|
||||||
|
@showSlide('js-bind')
|
||||||
|
@showAlert('js-bind', details.error || 'Unable to perform backend.')
|
||||||
|
)
|
||||||
|
|
||||||
|
foldersShow: (alreadyShown) =>
|
||||||
|
@showSlide('js-folders') if !alreadyShown
|
||||||
|
@foldersSelect.html(@createColumnSelection('folders', @wizardConfig.wizardData.backend_folders, @wizardConfig.folders))
|
||||||
|
|
||||||
|
createColumnSelection: (name, options, selected) ->
|
||||||
|
return App.UiElement.column_select.render(
|
||||||
|
name: name
|
||||||
|
null: false
|
||||||
|
nulloption: false
|
||||||
|
options: options
|
||||||
|
value: selected
|
||||||
|
)
|
||||||
|
|
||||||
|
mapping: (e) =>
|
||||||
|
e.preventDefault()
|
||||||
|
@showSlide('js-analyze')
|
||||||
|
params = @formParam(e.target)
|
||||||
|
|
||||||
|
# folders might be a single selection so we
|
||||||
|
# have to ensure that is an Array so the
|
||||||
|
# backend and frontend can handle it properly
|
||||||
|
if typeof params.folders is 'string'
|
||||||
|
params.folders = [ params.folders ]
|
||||||
|
|
||||||
|
# add login params
|
||||||
|
params.endpoint = @wizardConfig.endpoint
|
||||||
|
params.user = @wizardConfig.user
|
||||||
|
params.password = @wizardConfig.password
|
||||||
|
|
||||||
|
@ajax(
|
||||||
|
id: 'exchange_mapping'
|
||||||
|
type: 'POST'
|
||||||
|
url: "#{@apiPath}/integration/exchange/mapping"
|
||||||
|
data: JSON.stringify(params)
|
||||||
|
processData: true
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
if data.result isnt 'ok'
|
||||||
|
@showSlide('js-folders')
|
||||||
|
@showAlert('js-folders', data.message)
|
||||||
|
return
|
||||||
|
|
||||||
|
attributes = {}
|
||||||
|
for key, value of App.User.attributesGet()
|
||||||
|
continue if key == 'login'
|
||||||
|
if (value.tag is 'input' || value.tag is 'richtext' || value.tag is 'textarea') && value.type isnt 'password'
|
||||||
|
attributes[key] = value.display || key
|
||||||
|
|
||||||
|
@wizardConfig.wizardData.attributes = attributes
|
||||||
|
@wizardConfig.folders = params.folders
|
||||||
|
@wizardConfig.wizardData.backend_attributes = data.attributes
|
||||||
|
|
||||||
|
@mappingShow()
|
||||||
|
|
||||||
|
error: (xhr, statusText, error) =>
|
||||||
|
detailsRaw = xhr.responseText
|
||||||
|
details = {}
|
||||||
|
if !_.isEmpty(detailsRaw)
|
||||||
|
details = JSON.parse(detailsRaw)
|
||||||
|
@showSlide('js-folders')
|
||||||
|
@showAlert('js-folders', details.error || 'Unable to perform backend.')
|
||||||
|
)
|
||||||
|
|
||||||
|
mappingShow: (alreadyShown) =>
|
||||||
|
@showSlide('js-mapping') if !alreadyShown
|
||||||
|
user_attribute_map = @wizardConfig.attributes
|
||||||
|
|
||||||
|
if _.isEmpty(user_attribute_map)
|
||||||
|
user_attribute_map =
|
||||||
|
given_name: 'firstname'
|
||||||
|
surname: 'lastname'
|
||||||
|
'email_addresses.emailaddress1': 'email'
|
||||||
|
'phone_numbers.businessphone': 'phone'
|
||||||
|
|
||||||
|
@userMappingForm.find('tbody tr.js-entry').remove()
|
||||||
|
@userMappingForm.find('tbody tr').before(@buildRowsUserMap(user_attribute_map))
|
||||||
|
|
||||||
|
mappingChange: (e) =>
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
# user map
|
||||||
|
attributes = @formParam(@userMappingForm)
|
||||||
|
for key in ['source', 'dest']
|
||||||
|
if !_.isArray(attributes[key])
|
||||||
|
attributes[key] = [attributes[key]]
|
||||||
|
attributes_local =
|
||||||
|
item_id: 'login'
|
||||||
|
length = attributes.source.length-1
|
||||||
|
for count in [0..length]
|
||||||
|
if attributes.source[count] && attributes.dest[count]
|
||||||
|
attributes_local[attributes.source[count]] = attributes.dest[count]
|
||||||
|
@wizardConfig.attributes = attributes_local
|
||||||
|
|
||||||
|
@tryShow()
|
||||||
|
|
||||||
|
buildRowsUserMap: (user_attribute_map) =>
|
||||||
|
|
||||||
|
# show static login row
|
||||||
|
userUidDisplayValue = @wizardConfig.wizardData.backend_attributes['item_id']
|
||||||
|
el = [
|
||||||
|
$(App.view('integration/ldap_user_attribute_row_read_only')(
|
||||||
|
key: userUidDisplayValue,
|
||||||
|
value: 'Login'
|
||||||
|
))
|
||||||
|
]
|
||||||
|
|
||||||
|
for source, dest of user_attribute_map
|
||||||
|
continue if source == 'item_id'
|
||||||
|
continue if !(source of @wizardConfig.wizardData.backend_attributes)
|
||||||
|
el.push @buildRowUserAttribute(source, dest)
|
||||||
|
el
|
||||||
|
|
||||||
|
buildRowUserAttribute: (source, dest) =>
|
||||||
|
el = $(App.view('integration/exchange_user_attribute_row')())
|
||||||
|
el.find('.js-exchangeAttribute').html(@createSelection('source', @wizardConfig.wizardData.backend_attributes, source))
|
||||||
|
el.find('.js-userAttribute').html(@createSelection('dest', @wizardConfig.wizardData.attributes, dest))
|
||||||
|
el
|
||||||
|
|
||||||
|
createSelection: (name, options, selected, unknown) ->
|
||||||
|
return App.UiElement.searchable_select.render(
|
||||||
|
name: name
|
||||||
|
multiple: false
|
||||||
|
limit: 100
|
||||||
|
null: false
|
||||||
|
nulloption: false
|
||||||
|
options: options
|
||||||
|
value: selected
|
||||||
|
unknown: unknown
|
||||||
|
class: 'form-control--small'
|
||||||
|
)
|
||||||
|
|
||||||
|
removeRow: (e) ->
|
||||||
|
e.preventDefault()
|
||||||
|
$(e.target).closest('tr').remove()
|
||||||
|
|
||||||
|
addUserMapping: (e) =>
|
||||||
|
e.preventDefault()
|
||||||
|
@userMappingForm.find('tbody tr').last().before(@buildRowUserAttribute())
|
||||||
|
|
||||||
|
tryShow: (e) =>
|
||||||
|
if e
|
||||||
|
e.preventDefault()
|
||||||
|
@showSlide('js-analyze')
|
||||||
|
|
||||||
|
# create import job
|
||||||
|
@ajax(
|
||||||
|
id: 'exchange_try'
|
||||||
|
type: 'POST'
|
||||||
|
url: "#{@apiPath}/integration/exchange/job_try"
|
||||||
|
data: JSON.stringify(@wizardConfig)
|
||||||
|
processData: true
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
@tryLoop()
|
||||||
|
)
|
||||||
|
|
||||||
|
tryLoop: =>
|
||||||
|
@showSlide('js-dry')
|
||||||
|
@ajax(
|
||||||
|
id: 'jobs_try_index'
|
||||||
|
type: 'GET'
|
||||||
|
url: "#{@apiPath}/integration/exchange/job_try"
|
||||||
|
data:
|
||||||
|
finished: true
|
||||||
|
processData: true
|
||||||
|
success: (job, status, xhr) =>
|
||||||
|
if job.result && (job.result.error || job.result.info)
|
||||||
|
@showSlide('js-error')
|
||||||
|
@showAlert('js-error', (job.result.error || job.result.info))
|
||||||
|
return
|
||||||
|
|
||||||
|
total = 0
|
||||||
|
if job.result && _.keys(job.result).length > 0
|
||||||
|
@$('.js-preprogress').addClass('hide')
|
||||||
|
@$('.js-analyzing').removeClass('hide')
|
||||||
|
|
||||||
|
analized = 0
|
||||||
|
total = job.result.sum
|
||||||
|
for action, sum of job.result
|
||||||
|
continue if action == 'folders'
|
||||||
|
continue if action == 'sum'
|
||||||
|
analized += sum
|
||||||
|
|
||||||
|
@$('.js-progress progress').attr('value', analized)
|
||||||
|
@$('.js-progress progress').attr('max', total)
|
||||||
|
|
||||||
|
if job.finished_at
|
||||||
|
# reset initial state in case the back button is used
|
||||||
|
@$('.js-preprogress').removeClass('hide')
|
||||||
|
@$('.js-analyzing').addClass('hide')
|
||||||
|
|
||||||
|
@tryResult(job, total)
|
||||||
|
else
|
||||||
|
@delay(@tryLoop, 4000)
|
||||||
|
)
|
||||||
|
|
||||||
|
tryResult: (job, total) =>
|
||||||
|
@showSlide('js-try')
|
||||||
|
el = $(App.view('integration/exchange_summary')(job: job, countDone: total))
|
||||||
|
@el.find('.js-summary').html(el)
|
||||||
|
|
||||||
|
App.Config.set(
|
||||||
|
'IntegrationExchange'
|
||||||
|
{
|
||||||
|
name: 'Exchange'
|
||||||
|
target: '#system/integration/exchange'
|
||||||
|
description: 'Exchange integration for contacts management.'
|
||||||
|
controller: Index
|
||||||
|
state: State
|
||||||
|
}
|
||||||
|
'NavBarIntegrations'
|
||||||
|
)
|
|
@ -28,13 +28,20 @@ class Index extends App.ControllerIntegrationBase
|
||||||
super
|
super
|
||||||
active = @$('.js-switch input').prop('checked')
|
active = @$('.js-switch input').prop('checked')
|
||||||
if active
|
if active
|
||||||
@ajax(
|
job_start = =>
|
||||||
id: 'jobs_config'
|
@ajax(
|
||||||
type: 'POST'
|
id: 'jobs_config'
|
||||||
url: "#{@apiPath}/integration/ldap/job_start"
|
type: 'POST'
|
||||||
processData: true
|
url: "#{@apiPath}/integration/ldap/job_start"
|
||||||
success: (data, status, xhr) =>
|
processData: true
|
||||||
@render(true)
|
success: (data, status, xhr) =>
|
||||||
|
@render(true)
|
||||||
|
)
|
||||||
|
|
||||||
|
App.Delay.set(
|
||||||
|
job_start,
|
||||||
|
600,
|
||||||
|
'job_start',
|
||||||
)
|
)
|
||||||
|
|
||||||
class Form extends App.Controller
|
class Form extends App.Controller
|
||||||
|
@ -91,6 +98,7 @@ class Form extends App.Controller
|
||||||
processData: true
|
processData: true
|
||||||
success: (data, status, xhr) =>
|
success: (data, status, xhr) =>
|
||||||
@render(true)
|
@render(true)
|
||||||
|
@lastResult()
|
||||||
)
|
)
|
||||||
|
|
||||||
startWizard: (e) =>
|
startWizard: (e) =>
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
<div class="js-lastImport"></div>
|
||||||
|
<div class="js-notConfigured">
|
||||||
|
<p><%- @T('No %s configured.', 'Exchange') %></p>
|
||||||
|
<button type="submit" class="btn btn--primary js-wizard"><%- @T('Configure') %></button>
|
||||||
|
</div>
|
||||||
|
<div class="js-summary hide">
|
||||||
|
<h2><%- @T('Settings') %></h2>
|
||||||
|
<table class="settings-list" style="width: 100%;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="30%"><%- @T('Name') %>
|
||||||
|
<th width="70%"><%- @T('Value') %>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="settings-list-row-control"><%- @T('Endpoint') %>
|
||||||
|
<td class="settings-list-row-control"><%= @config.endpoint %>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="settings-list-row-control"><%- @T('User') %>
|
||||||
|
<td class="settings-list-row-control"><%= @config.user %>
|
||||||
|
<tr>
|
||||||
|
<td class="settings-list-row-control"><%- @T('Password') %>
|
||||||
|
<td class="settings-list-row-control"><%= @M(@config.password) %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2><%- @T('Mapping') %></h2>
|
||||||
|
|
||||||
|
<h3><%- @T('Folders') %></h3>
|
||||||
|
<% if _.isEmpty(@folders): %>
|
||||||
|
<table class="settings-list settings-list--stretch settings-list--placeholder">
|
||||||
|
<thead><tr><th><%- @T('No Entries') %>
|
||||||
|
</table>
|
||||||
|
<% else: %>
|
||||||
|
<table class="settings-list" style="width: 100%;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><%- @T('Folder') %>
|
||||||
|
<% for folder_name in @folders: %>
|
||||||
|
<tr>
|
||||||
|
<td class="settings-list-row-control"><%= folder_name %>
|
||||||
|
<% end %>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
</table>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<h3><%- @T('User') %></h3>
|
||||||
|
<% if _.isEmpty(@config.attributes): %>
|
||||||
|
<table class="settings-list settings-list--stretch settings-list--placeholder">
|
||||||
|
<thead><tr><th><%- @T('No Entries') %>
|
||||||
|
</table>
|
||||||
|
<% else: %>
|
||||||
|
<table class="settings-list" style="width: 100%;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="40%"><%- @T('Exchange') %>
|
||||||
|
<th width="60%"><%- @T('Zammad') %>
|
||||||
|
<% for key, value of @config.attributes: %>
|
||||||
|
<tr>
|
||||||
|
<td class="settings-list-row-control"><%= key %>
|
||||||
|
<td class="settings-list-row-control"><%= value %>
|
||||||
|
<% end %>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
</table>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn--primary js-wizard"><%- @T('Change') %></button>
|
||||||
|
</div>
|
|
@ -0,0 +1,54 @@
|
||||||
|
<div class="box box--message">
|
||||||
|
<h2><%- @T('Last sync') %></h2>
|
||||||
|
<% if _.isEmpty(@job.started_at): %>
|
||||||
|
<% if @job.result && @job.result.error: %>
|
||||||
|
<div class="alert alert--danger" role="alert"><%- @T('An error occurred: %s', @job.result.error) %></div>
|
||||||
|
<% else if @job.result && @job.result.info: %>
|
||||||
|
<div class="alert alert--info" role="alert"><%- @T('Info: %s', @job.result.info) %></div>
|
||||||
|
<% else: %>
|
||||||
|
<p><%- @T('Job is waiting to get started...') %></p>
|
||||||
|
<% end %>
|
||||||
|
<% else: %>
|
||||||
|
<% if @job.finished_at: %>
|
||||||
|
<p><%- @Ttimestamp(@job.started_at) %> - <%- @Ttimestamp(@job.finished_at) %></p>
|
||||||
|
<% if @job.result && @job.result.error: %>
|
||||||
|
<div class="alert alert--danger" role="alert"><%- @T('An error occurred: %s', @job.result.error) %></div>
|
||||||
|
<% else if @job.result && @job.result.info: %>
|
||||||
|
<div class="alert alert--info" role="alert"><%- @T('Info: %s', @job.result.info) %></div>
|
||||||
|
<% end %>
|
||||||
|
<% else: %>
|
||||||
|
<% if @job.result && @job.result.error: %>
|
||||||
|
<p><%- @Ttimestamp(@job.started_at) %></p>
|
||||||
|
<div class="alert alert--danger" role="alert"><%- @T('An error occurred: %s', @job.result.error) %></div>
|
||||||
|
<% else if !@countDone: %>
|
||||||
|
<p><%- @Ttimestamp(@job.started_at) %> - <%- @T('Counting entries. This may take a while.') %></p>
|
||||||
|
<% else: %>
|
||||||
|
<p><%- @Ttimestamp(@job.started_at) %> - <%- @T('Running...') %></p>
|
||||||
|
<div class="flex">
|
||||||
|
<progress max="<%= @job.result.sum %>" value="<%= @countDone %>"></progress>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% if !_.isEmpty(@job.result) && @countDone: %>
|
||||||
|
<ul>
|
||||||
|
<li><%- @T('%s user to %s user', 'Exchange', 'Zammad') %> (<%= @countDone %>/<%= @job.result.sum %>):
|
||||||
|
<ul>
|
||||||
|
<li><%- @T('Users') %>: <%= @job.result.created %> <%- @T('created') %>, <%= @job.result.updated %> <%- @T('updated') %>, <%= @job.result.unchanged %> <%- @T('untouched') %>, <%= @job.result.skipped %> <%- @T('skipped') %>, <%= @job.result.failed %> <%- @T('failed') %>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<% if !_.isEmpty(@job.result.folders): %>
|
||||||
|
<li><%- @T('%s folders', 'Exchange') %>:
|
||||||
|
<ul>
|
||||||
|
<% for folder, result of @job.result.folders: %>
|
||||||
|
<li><%- folder %>: <%= result.created %> <%- @T('created') %>, <%= result.updated %> <%- @T('updated') %>, <%= result.unchanged %> <%- @T('untouched') %>, <%= result.failed %> <%- @T('failed') %>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
<% end %>
|
||||||
|
<% if @job.finished_at: %>
|
||||||
|
<button type="submit" class="btn btn--primary js-start-sync"><%- @T('Start new') %></button>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
|
@ -0,0 +1,16 @@
|
||||||
|
<ul>
|
||||||
|
<li><%- @T('%s user to %s user', 'Exchange', 'Zammad') %> (<%= @countDone %>):
|
||||||
|
<ul>
|
||||||
|
<li><%- @T('Users') %>: <%= @job.result.created %> <%- @T('created') %>, <%= @job.result.updated %> <%- @T('updated') %>, <%= @job.result.unchanged %> <%- @T('untouched') %>, <%= @job.result.skipped %> <%- @T('skipped') %>, <%= @job.result.failed %> <%- @T('failed') %>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<% if !_.isEmpty(@job.result.folders): %>
|
||||||
|
<li><%- @T('%s folders', 'Exchange') %>:
|
||||||
|
<ul>
|
||||||
|
<% for folder, result of @job.result.folders: %>
|
||||||
|
<li><%- folder %>: <%= result.created %> <%- @T('created') %>, <%= result.updated %> <%- @T('updated') %>, <%= result.unchanged %> <%- @T('untouched') %>, <%= result.failed %> <%- @T('failed') %>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
|
@ -0,0 +1,7 @@
|
||||||
|
<tr class="js-entry">
|
||||||
|
<td style="max-width: 240px" class="settings-list-control-cell js-exchangeAttribute">
|
||||||
|
<td class="settings-list-control-cell js-userAttribute">
|
||||||
|
<td class="settings-list-row-control">
|
||||||
|
<div class="btn btn--text js-remove">
|
||||||
|
<%- @Icon('trash') %> <%- @T('Remove') %>
|
||||||
|
</div>
|
|
@ -0,0 +1,258 @@
|
||||||
|
<div class="modal-dialog">
|
||||||
|
|
||||||
|
<form class="modal-content setup wizard js-discover">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-close js-close">
|
||||||
|
<%- @Icon('diagonal-cross') %>
|
||||||
|
</div>
|
||||||
|
<h1 class="modal-title"><%- @T('Exchange') %> <%- @T('Configuration') %></h1>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="wizard-body vertical justified">
|
||||||
|
<div class="alert alert--danger hide" role="alert"></div>
|
||||||
|
<table class="settings-list" style="width: 100%;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="30%"><%- @T('Name') %>
|
||||||
|
<th width="70%"><%- @T('Value') %>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="settings-list-row-control"><%- @T('User') %>
|
||||||
|
<td class="settings-list-control-cell"><input type="text" name="user" class="form-control form-control--small js-user" value="" placeholder="" autocomplete="off" required>
|
||||||
|
<tr>
|
||||||
|
<td class="settings-list-row-control"><%- @T('Password') %>
|
||||||
|
<td class="settings-list-control-cell"><input type="password" name="password" class="form-control form-control--small js-password" value="" placeholder="" autocomplete="new-password" required>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div class="modal-rightFooter">
|
||||||
|
<button class="btn btn--primary align-right js-submit"><%- @T('Connect') %></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form class="modal-content setup wizard hide js-connect">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-close js-close">
|
||||||
|
<%- @Icon('diagonal-cross') %>
|
||||||
|
</div>
|
||||||
|
<h1 class="modal-title"><%- @T('Exchange') %> <%- @T('Configuration') %></h1>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="wizard-body vertical justified">
|
||||||
|
<p class="wizard-loadingText">
|
||||||
|
<span class="loading icon"></span> <%- @T('Connecting ...') %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer"></div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form class="modal-content setup wizard hide js-bind">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-close js-close">
|
||||||
|
<%- @Icon('diagonal-cross') %>
|
||||||
|
</div>
|
||||||
|
<h1 class="modal-title"><%- @T('Exchange') %> <%- @T('Configuration') %></h1>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="wizard-body vertical justified">
|
||||||
|
<div class="alert alert--danger hide" role="alert"></div>
|
||||||
|
<table class="settings-list" style="width: 100%;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="30%"><%- @T('Name') %>
|
||||||
|
<th width="70%"><%- @T('Value') %>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="settings-list-row-control"><%- @T('Endpoint') %>
|
||||||
|
<td class="settings-list-control-cell"><input type="text" name="endpoint" class="form-control form-control--small" value="" placeholder="https://outlook.office365.com/EWS/Exchange.asmx" autocomplete="off" required>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="settings-list-row-control"><%- @T('User') %>
|
||||||
|
<td class="settings-list-control-cell"><input type="text" name="user" class="form-control form-control--small js-user" value="" placeholder="" autocomplete="off" required>
|
||||||
|
<tr>
|
||||||
|
<td class="settings-list-row-control"><%- @T('Password') %>
|
||||||
|
<td class="settings-list-control-cell"><input type="password" name="password" class="form-control form-control--small js-password" value="" placeholder="" autocomplete="new-password" required>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div class="modal-rightFooter">
|
||||||
|
<button class="btn btn--primary align-right js-submit"><%- @T('Connect') %></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form class="modal-content setup wizard hide js-folders">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-close js-close">
|
||||||
|
<%- @Icon('diagonal-cross') %>
|
||||||
|
</div>
|
||||||
|
<h1 class="modal-title"><%- @T('Exchange') %> <%- @T('Folders') %></h1>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="wizard-body vertical justified">
|
||||||
|
<div class="alert alert--danger hide" role="alert"></div>
|
||||||
|
|
||||||
|
<div class="column_select form-group">
|
||||||
|
<div class="formGroup-label">
|
||||||
|
<label for="folders"><%- @T('Import %s', 'Folders') %></label>
|
||||||
|
</div>
|
||||||
|
<div class="controls js-foldersSelect">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div class="modal-rightFooter">
|
||||||
|
<a class="btn btn--text btn--secondary js-goToSlide align-left" data-slide="js-bind"><%- @T('Go Back') %></a>
|
||||||
|
</div>
|
||||||
|
<div class="modal-rightFooter">
|
||||||
|
<button class="btn btn--primary align-right js-submitTry"><%- @T('Continue') %></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form class="modal-content setup wizard hide js-analyze">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-close js-close">
|
||||||
|
<%- @Icon('diagonal-cross') %>
|
||||||
|
</div>
|
||||||
|
<h1 class="modal-title"><%- @T('Exchange') %> <%- @T('Configuration') %></h1>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="wizard-body vertical justified">
|
||||||
|
<p class="wizard-loadingText">
|
||||||
|
<span class="loading icon"></span> <%- @T('Analyzing structure...') %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer"></div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="modal-content setup wizard hide js-mapping">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-close js-close">
|
||||||
|
<%- @Icon('diagonal-cross') %>
|
||||||
|
</div>
|
||||||
|
<h1 class="modal-title"><%- @T('Exchange') %> <%- @T('Mapping') %></h1>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="wizard-body vertical justified">
|
||||||
|
<div class="alert alert--danger hide" role="alert"></div>
|
||||||
|
|
||||||
|
<h2><%- @T('User') %></h2>
|
||||||
|
<form class="js-userMappingForm">
|
||||||
|
<table class="settings-list js-userAttributeMap" style="width: 100%;">
|
||||||
|
<colgroup>
|
||||||
|
<col width="240">
|
||||||
|
<col>
|
||||||
|
<col>
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><%- @T('%s Attribute', 'Exchange') %>
|
||||||
|
<th><%- @T('%s Attribute', 'Zammad') %>
|
||||||
|
<th><%- @T('Action') %>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="settings-list-row-control" colspan="3">
|
||||||
|
<div class="btn btn--text btn--create js-add">
|
||||||
|
<%- @Icon('plus-small') %> <%- @T('Add') %>
|
||||||
|
</div>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div class="modal-rightFooter">
|
||||||
|
<a class="btn btn--text btn--secondary js-goToSlide align-left" data-slide="js-bind"><%- @T('Go Back') %></a>
|
||||||
|
</div>
|
||||||
|
<div class="modal-rightFooter">
|
||||||
|
<button class="btn btn--primary align-right js-submitTry"><%- @T('Continue') %></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="modal-content setup wizard hide js-dry">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-close js-close">
|
||||||
|
<%- @Icon('diagonal-cross') %>
|
||||||
|
</div>
|
||||||
|
<h1 class="modal-title"><%- @T('Exchange') %> <%- @T('Configuration') %></h1>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="wizard-body vertical justified">
|
||||||
|
<div class="js-preprogress">
|
||||||
|
<p class="wizard-loadingText">
|
||||||
|
<span class="loading icon"></span>
|
||||||
|
<%- @T('Counting entries. This may take a while.') %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="js-analyzing hide">
|
||||||
|
<p class="wizard-loadingText">
|
||||||
|
<%- @T('Analyzing entries with given configuration...') %>
|
||||||
|
</p>
|
||||||
|
<div class="centered js-progress">
|
||||||
|
<progress max="" value=""></progress>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer"></div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="modal-content setup wizard hide js-try">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-close js-close">
|
||||||
|
<%- @Icon('diagonal-cross') %>
|
||||||
|
</div>
|
||||||
|
<h1 class="modal-title"><%- @T('Exchange') %> <%- @T('Configuration') %></h1>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="wizard-body vertical justified">
|
||||||
|
<div class="alert alert--danger hide" role="alert"></div>
|
||||||
|
<p><%- @T('With your current configuration the following will happen') %>:</p>
|
||||||
|
<div class="js-summary"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div class="modal-rightFooter">
|
||||||
|
<a class="btn btn--text btn--secondary js-goToSlide align-left" data-slide="js-mapping"><%- @T('Go Back') %></a>
|
||||||
|
</div>
|
||||||
|
<div class="modal-rightFooter">
|
||||||
|
<button class="btn btn--primary align-right js-submitSave"><%- @T('Save configuration') %></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="modal-content setup wizard hide js-error">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-close js-close">
|
||||||
|
<%- @Icon('diagonal-cross') %>
|
||||||
|
</div>
|
||||||
|
<h1 class="modal-title"><%- @T('Exchange') %></h1>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="wizard-body vertical justified">
|
||||||
|
<div class="alert alert--danger hide" role="alert"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div class="modal-rightFooter">
|
||||||
|
<button class="btn btn--primary align-right"><%- @T('Cancel') %></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
89
app/controllers/concerns/integration/import_job_base.rb
Normal file
89
app/controllers/concerns/integration/import_job_base.rb
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
module Integration::ImportJobBase
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
def job_try_index
|
||||||
|
job_index(
|
||||||
|
dry_run: true,
|
||||||
|
take_finished: params[:finished] == 'true'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def job_try_create
|
||||||
|
ImportJob.dry_run(name: import_backend_namespace, payload: payload_dry_run)
|
||||||
|
render json: {
|
||||||
|
result: 'ok',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def job_start_index
|
||||||
|
job_index(dry_run: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def job_start_create
|
||||||
|
if !ImportJob.exists?(name: import_backend_namespace, finished_at: nil)
|
||||||
|
job = ImportJob.create(name: import_backend_namespace, payload: payload_import)
|
||||||
|
job.delay.start
|
||||||
|
end
|
||||||
|
render json: {
|
||||||
|
result: 'ok',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def payload_dry_run
|
||||||
|
params
|
||||||
|
end
|
||||||
|
|
||||||
|
def payload_import
|
||||||
|
import_setting
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def answer_with
|
||||||
|
result = yield
|
||||||
|
render json: result.merge(result: 'ok')
|
||||||
|
rescue => e
|
||||||
|
logger.error(e)
|
||||||
|
render json: {
|
||||||
|
result: 'failed',
|
||||||
|
message: e.message,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def import_setting
|
||||||
|
Setting.get(import_setting_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def import_setting_name
|
||||||
|
"#{import_backend_name.downcase}_config"
|
||||||
|
end
|
||||||
|
|
||||||
|
def import_backend_namespace
|
||||||
|
"Import::#{import_backend_name}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def import_backend_name
|
||||||
|
self.class.name.split('::').last.sub('Controller', '')
|
||||||
|
end
|
||||||
|
|
||||||
|
def job_index(dry_run:, take_finished: true)
|
||||||
|
job = ImportJob.find_by(
|
||||||
|
name: import_backend_namespace,
|
||||||
|
dry_run: dry_run,
|
||||||
|
finished_at: nil
|
||||||
|
)
|
||||||
|
if !job && take_finished
|
||||||
|
job = ImportJob.where(
|
||||||
|
name: import_backend_namespace,
|
||||||
|
dry_run: dry_run
|
||||||
|
).order(created_at: :desc).limit(1).first
|
||||||
|
end
|
||||||
|
|
||||||
|
if job
|
||||||
|
model_show_render_item(job)
|
||||||
|
else
|
||||||
|
render json: {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
66
app/controllers/integration/exchange_controller.rb
Normal file
66
app/controllers/integration/exchange_controller.rb
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
class Integration::ExchangeController < ApplicationController
|
||||||
|
include Integration::ImportJobBase
|
||||||
|
|
||||||
|
prepend_before_action { authentication_check(permission: 'admin.integration.exchange') }
|
||||||
|
|
||||||
|
def autodiscover
|
||||||
|
answer_with do
|
||||||
|
client = Autodiscover::Client.new(
|
||||||
|
email: params[:user],
|
||||||
|
password: params[:password],
|
||||||
|
)
|
||||||
|
|
||||||
|
{
|
||||||
|
endpoint: client.autodiscover.ews_url,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def folders
|
||||||
|
answer_with do
|
||||||
|
Sequencer.process('Import::Exchange::AvailableFolders',
|
||||||
|
parameters: {
|
||||||
|
ews_config: {
|
||||||
|
endpoint: params[:endpoint],
|
||||||
|
user: params[:user],
|
||||||
|
password: params[:password],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def mapping
|
||||||
|
answer_with do
|
||||||
|
Sequencer.process('Import::Exchange::AttributesExamples',
|
||||||
|
parameters: {
|
||||||
|
ews_folder_ids: params[:folders],
|
||||||
|
ews_config: {
|
||||||
|
endpoint: params[:endpoint],
|
||||||
|
user: params[:user],
|
||||||
|
password: params[:password],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# currently a workaround till LDAP is migrated to Sequencer
|
||||||
|
def payload_dry_run
|
||||||
|
{
|
||||||
|
ews_attributes: params[:attributes],
|
||||||
|
ews_folder_ids: params[:folders],
|
||||||
|
ews_config: {
|
||||||
|
endpoint: params[:endpoint],
|
||||||
|
user: params[:user],
|
||||||
|
password: params[:password],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def payload_import
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,6 +4,8 @@ require 'ldap/user'
|
||||||
require 'ldap/group'
|
require 'ldap/group'
|
||||||
|
|
||||||
class Integration::LdapController < ApplicationController
|
class Integration::LdapController < ApplicationController
|
||||||
|
include Integration::ImportJobBase
|
||||||
|
|
||||||
prepend_before_action { authentication_check(permission: 'admin.integration.ldap') }
|
prepend_before_action { authentication_check(permission: 'admin.integration.ldap') }
|
||||||
|
|
||||||
def discover
|
def discover
|
||||||
|
@ -60,48 +62,4 @@ class Integration::LdapController < ApplicationController
|
||||||
message: e.message,
|
message: e.message,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def job_try_index
|
|
||||||
job_index(
|
|
||||||
dry_run: true,
|
|
||||||
take_finished: params[:finished] == 'true'
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def job_try_create
|
|
||||||
ImportJob.dry_run(name: 'Import::Ldap', payload: params)
|
|
||||||
render json: {
|
|
||||||
result: 'ok',
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def job_start_index
|
|
||||||
job_index(dry_run: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
def job_start_create
|
|
||||||
backend = 'Import::Ldap'
|
|
||||||
if !ImportJob.exists?(name: backend, finished_at: nil)
|
|
||||||
job = ImportJob.create(name: backend, payload: Setting.get('ldap_config'))
|
|
||||||
job.delay.start
|
|
||||||
end
|
|
||||||
render json: {
|
|
||||||
result: 'ok',
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def job_index(dry_run:, take_finished: true)
|
|
||||||
job = ImportJob.find_by(name: 'Import::Ldap', dry_run: dry_run, finished_at: nil)
|
|
||||||
if !job && take_finished
|
|
||||||
job = ImportJob.where(name: 'Import::Ldap', dry_run: dry_run).order(created_at: :desc).limit(1).first
|
|
||||||
end
|
|
||||||
|
|
||||||
if job
|
|
||||||
model_show_render_item(job)
|
|
||||||
else
|
|
||||||
render json: {}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
11
config/routes/integration_exchange.rb
Normal file
11
config/routes/integration_exchange.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
Zammad::Application.routes.draw do
|
||||||
|
api_path = Rails.configuration.api_path
|
||||||
|
|
||||||
|
match api_path + '/integration/exchange/autodiscover', to: 'integration/exchange#autodiscover', via: :post
|
||||||
|
match api_path + '/integration/exchange/folders', to: 'integration/exchange#folders', via: :post
|
||||||
|
match api_path + '/integration/exchange/mapping', to: 'integration/exchange#mapping', via: :post
|
||||||
|
match api_path + '/integration/exchange/job_try', to: 'integration/exchange#job_try_index', via: :get
|
||||||
|
match api_path + '/integration/exchange/job_try', to: 'integration/exchange#job_try_create', via: :post
|
||||||
|
match api_path + '/integration/exchange/job_start', to: 'integration/exchange#job_start_index', via: :get
|
||||||
|
match api_path + '/integration/exchange/job_start', to: 'integration/exchange#job_start_create', via: :post
|
||||||
|
end
|
51
db/migrate/20170629000001_exchange_integration.rb
Normal file
51
db/migrate/20170629000001_exchange_integration.rb
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
class ExchangeIntegration < ActiveRecord::Migration
|
||||||
|
def up
|
||||||
|
|
||||||
|
# return if it's a new setup
|
||||||
|
return if !Setting.find_by(name: 'system_init_done')
|
||||||
|
|
||||||
|
Setting.set('import_backends', ['Import::Ldap', 'Import::Exchange'])
|
||||||
|
|
||||||
|
Setting.create_if_not_exists(
|
||||||
|
title: 'Exchange config',
|
||||||
|
name: 'exchange_config',
|
||||||
|
area: 'Integration::Exchange',
|
||||||
|
description: 'Defines the Exchange config.',
|
||||||
|
options: {},
|
||||||
|
state: {},
|
||||||
|
preferences: {
|
||||||
|
prio: 2,
|
||||||
|
permission: ['admin.integration'],
|
||||||
|
},
|
||||||
|
frontend: false,
|
||||||
|
)
|
||||||
|
Setting.create_if_not_exists(
|
||||||
|
title: 'Exchange integration',
|
||||||
|
name: 'exchange_integration',
|
||||||
|
area: 'Integration::Switch',
|
||||||
|
description: 'Defines if Exchange is enabled or not.',
|
||||||
|
options: {
|
||||||
|
form: [
|
||||||
|
{
|
||||||
|
display: '',
|
||||||
|
null: true,
|
||||||
|
name: 'exchange_integration',
|
||||||
|
tag: 'boolean',
|
||||||
|
options: {
|
||||||
|
true => 'yes',
|
||||||
|
false => 'no',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
state: false,
|
||||||
|
preferences: {
|
||||||
|
prio: 1,
|
||||||
|
authentication: true,
|
||||||
|
permission: ['admin.integration'],
|
||||||
|
},
|
||||||
|
frontend: true
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -2401,7 +2401,7 @@ Setting.create_if_not_exists(
|
||||||
area: 'Import',
|
area: 'Import',
|
||||||
description: 'A list of active import backends that get scheduled automatically.',
|
description: 'A list of active import backends that get scheduled automatically.',
|
||||||
options: {},
|
options: {},
|
||||||
state: ['Import::Ldap'],
|
state: ['Import::Ldap', 'Import::Exchange'],
|
||||||
preferences: {
|
preferences: {
|
||||||
permission: ['admin'],
|
permission: ['admin'],
|
||||||
},
|
},
|
||||||
|
@ -2874,6 +2874,46 @@ Setting.create_if_not_exists(
|
||||||
},
|
},
|
||||||
frontend: true
|
frontend: true
|
||||||
)
|
)
|
||||||
|
Setting.create_if_not_exists(
|
||||||
|
title: 'Exchange config',
|
||||||
|
name: 'exchange_config',
|
||||||
|
area: 'Integration::Exchange',
|
||||||
|
description: 'Defines the Exchange config.',
|
||||||
|
options: {},
|
||||||
|
state: {},
|
||||||
|
preferences: {
|
||||||
|
prio: 2,
|
||||||
|
permission: ['admin.integration'],
|
||||||
|
},
|
||||||
|
frontend: false,
|
||||||
|
)
|
||||||
|
Setting.create_if_not_exists(
|
||||||
|
title: 'Exchange integration',
|
||||||
|
name: 'exchange_integration',
|
||||||
|
area: 'Integration::Switch',
|
||||||
|
description: 'Defines if Exchange is enabled or not.',
|
||||||
|
options: {
|
||||||
|
form: [
|
||||||
|
{
|
||||||
|
display: '',
|
||||||
|
null: true,
|
||||||
|
name: 'exchange_integration',
|
||||||
|
tag: 'boolean',
|
||||||
|
options: {
|
||||||
|
true => 'yes',
|
||||||
|
false => 'no',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
state: false,
|
||||||
|
preferences: {
|
||||||
|
prio: 1,
|
||||||
|
authentication: true,
|
||||||
|
permission: ['admin.integration'],
|
||||||
|
},
|
||||||
|
frontend: true
|
||||||
|
)
|
||||||
Setting.create_if_not_exists(
|
Setting.create_if_not_exists(
|
||||||
title: 'LDAP config',
|
title: 'LDAP config',
|
||||||
name: 'ldap_config',
|
name: 'ldap_config',
|
||||||
|
|
13
lib/import/exchange.rb
Normal file
13
lib/import/exchange.rb
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
module Import
|
||||||
|
class Exchange < Import::IntegrationBase
|
||||||
|
include Import::Mixin::Sequence
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def sequence_name
|
||||||
|
'Import::Exchange::FolderContacts'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
76
lib/import/exchange/folder.rb
Normal file
76
lib/import/exchange/folder.rb
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
require 'mixin/rails_logger'
|
||||||
|
|
||||||
|
module Import
|
||||||
|
class Exchange
|
||||||
|
class Folder
|
||||||
|
include ::Mixin::RailsLogger
|
||||||
|
|
||||||
|
def initialize(connection)
|
||||||
|
@connection = connection
|
||||||
|
@lookup_map = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def id_folder_map
|
||||||
|
@id_folder_map ||= all.collect do |folder|
|
||||||
|
[folder.id, folder]
|
||||||
|
end.to_h
|
||||||
|
|
||||||
|
# duplicate object to avoid errors where keys get
|
||||||
|
# added via #get_folder while iterating over
|
||||||
|
# the result of this method
|
||||||
|
@lookup_map = @id_folder_map.dup
|
||||||
|
@id_folder_map
|
||||||
|
end
|
||||||
|
|
||||||
|
def find(id)
|
||||||
|
@lookup_map[id] ||= @connection.get_folder(id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def all
|
||||||
|
# request folders only if neccessary and store the result
|
||||||
|
@all ||= children(%i(root msgfolderroot publicfoldersroot))
|
||||||
|
end
|
||||||
|
|
||||||
|
def children(parent_identifiers)
|
||||||
|
parent_identifiers.each_with_object([]) do |parent_identifier, result|
|
||||||
|
|
||||||
|
child_folders = request_children(parent_identifier)
|
||||||
|
|
||||||
|
next if child_folders.blank?
|
||||||
|
|
||||||
|
child_folder_ids = child_folders.collect(&:id)
|
||||||
|
child_folders += children(child_folder_ids)
|
||||||
|
|
||||||
|
result.concat(child_folders)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def display_path(folder)
|
||||||
|
display_name = folder.display_name
|
||||||
|
return display_name if !folder.parent_folder_id
|
||||||
|
|
||||||
|
parent_folder = find(folder.parent_folder_id)
|
||||||
|
return display_name if !parent_folder
|
||||||
|
|
||||||
|
parent_folder = id_folder_map[folder.parent_folder_id]
|
||||||
|
return display_name if !parent_folder
|
||||||
|
|
||||||
|
# recursive
|
||||||
|
parent_folder_path = display_path(parent_folder)
|
||||||
|
|
||||||
|
"#{parent_folder_path} -> #{display_name}"
|
||||||
|
rescue Viewpoint::EWS::EwsError
|
||||||
|
folder.display_name
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def request_children(parent_identifier)
|
||||||
|
@connection.folders(root: parent_identifier)
|
||||||
|
rescue Viewpoint::EWS::EwsFolderNotFound => e
|
||||||
|
logger.warn(e)
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
111
lib/import/exchange/item_attributes.rb
Normal file
111
lib/import/exchange/item_attributes.rb
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
module Import
|
||||||
|
class Exchange
|
||||||
|
class ItemAttributes
|
||||||
|
|
||||||
|
def self.extract(resource)
|
||||||
|
new(resource).extract
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(resource)
|
||||||
|
@resource = resource
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract
|
||||||
|
@attributes ||= begin
|
||||||
|
properties = @resource.get_all_properties!
|
||||||
|
result = normalize(properties)
|
||||||
|
flattened = flatten(result)
|
||||||
|
booleanized = booleanize_values(flattened)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def booleanize_values(properties)
|
||||||
|
properties.each do |key, value|
|
||||||
|
if value.is_a?(String)
|
||||||
|
next if !%w(true false).include?(value)
|
||||||
|
properties[key] = value == 'true'
|
||||||
|
elsif value.is_a?(Hash)
|
||||||
|
properties[key] = booleanize_values(value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalize(properties)
|
||||||
|
result = {}
|
||||||
|
properties.each do |key, value|
|
||||||
|
|
||||||
|
next if key == :body
|
||||||
|
|
||||||
|
if value[:text]
|
||||||
|
result[key] = value[:text]
|
||||||
|
elsif value[:attribs]
|
||||||
|
result[key] = value[:attribs]
|
||||||
|
elsif value[:elems]
|
||||||
|
result[key] = sub_elems(value[:elems])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
def sub_elems(elems)
|
||||||
|
result = {}
|
||||||
|
elems.each do |elem|
|
||||||
|
if elem[:entry]
|
||||||
|
result.merge!( sub_elem_entry( elem[:entry] ) )
|
||||||
|
else
|
||||||
|
result.merge!( normalize(elem) )
|
||||||
|
end
|
||||||
|
end
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
def sub_elem_entry(entry)
|
||||||
|
entry_value = {}
|
||||||
|
if entry[:elems]
|
||||||
|
entry_value = sub_elems(entry[:elems])
|
||||||
|
end
|
||||||
|
|
||||||
|
if entry[:text]
|
||||||
|
entry_value[:text] = entry[:text]
|
||||||
|
end
|
||||||
|
|
||||||
|
if entry[:attribs].present?
|
||||||
|
entry_value.merge!(entry[:attribs])
|
||||||
|
end
|
||||||
|
|
||||||
|
entry_key = entry_value.delete(:key)
|
||||||
|
{
|
||||||
|
entry_key => entry_value
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def flatten(properties, prefix: nil)
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
properties.each do |key, value|
|
||||||
|
|
||||||
|
result_key = key
|
||||||
|
if prefix
|
||||||
|
result_key = if %i(text id).include?(key) && ( !result[result_key] || result[result_key] == value )
|
||||||
|
prefix
|
||||||
|
else
|
||||||
|
"#{prefix}.#{key}".to_sym
|
||||||
|
end
|
||||||
|
end
|
||||||
|
result_key = result_key.to_s.downcase
|
||||||
|
|
||||||
|
if value.is_a?(Hash)
|
||||||
|
sub_result = flatten(value, prefix: result_key)
|
||||||
|
result.merge!(sub_result)
|
||||||
|
else
|
||||||
|
result[result_key] = value.to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
result
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
70
lib/import/helper/attributes_examples.rb
Normal file
70
lib/import/helper/attributes_examples.rb
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
module Import
|
||||||
|
module Helper
|
||||||
|
class AttributesExamples
|
||||||
|
attr_reader :examples, :enough, :max_unkown
|
||||||
|
|
||||||
|
def initialize(&block)
|
||||||
|
@max_unkown = 50
|
||||||
|
@no_new_counter = 1
|
||||||
|
@examples = {}
|
||||||
|
@known = []
|
||||||
|
|
||||||
|
# Support both builder styles:
|
||||||
|
#
|
||||||
|
# Import::Helper::AttributesExamples.new do
|
||||||
|
# extract(attributes)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# and
|
||||||
|
#
|
||||||
|
# Import::Helper::AttributesExamples.new do |extractor|
|
||||||
|
# extractor.extract(attributes)
|
||||||
|
# end
|
||||||
|
return if !block_given?
|
||||||
|
if block.arity.zero?
|
||||||
|
instance_eval(&block)
|
||||||
|
else
|
||||||
|
yield self
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract(attributes)
|
||||||
|
unknown = attributes.keys - @known
|
||||||
|
|
||||||
|
return if !unknown?(unknown)
|
||||||
|
|
||||||
|
store(attributes, unknown)
|
||||||
|
|
||||||
|
@known.concat(unknown)
|
||||||
|
@no_new_counter = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def unknown?(unknown)
|
||||||
|
return true if unknown.present?
|
||||||
|
|
||||||
|
@no_new_counter += 1
|
||||||
|
|
||||||
|
# check max 50 entries with no or no new attributes in a row
|
||||||
|
@enough_examples = @no_new_counter != 50
|
||||||
|
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def store(attributes, unknown)
|
||||||
|
unknown.each do |attribute|
|
||||||
|
value = attributes[attribute]
|
||||||
|
|
||||||
|
next if value.nil?
|
||||||
|
|
||||||
|
example = value.to_s.force_encoding('UTF-8').encode('utf-8', 'binary', invalid: :replace, undef: :replace, replace: '?')
|
||||||
|
example.gsub!(/^(.{20,}?).*$/m, '\1...')
|
||||||
|
|
||||||
|
@examples[attribute] = "#{attribute} (e. g. #{example})"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
143
lib/import/integration_base.rb
Normal file
143
lib/import/integration_base.rb
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
module Import
|
||||||
|
|
||||||
|
# This base class handles regular integrations.
|
||||||
|
# It provides generic interfaces for settings and active state.
|
||||||
|
# It ensures that all requirements for a regular integration are met before a import can start.
|
||||||
|
# It handles the case of an Scheduler interruption.
|
||||||
|
#
|
||||||
|
# It's required to implement the +start_import+ method which only has to start the import.
|
||||||
|
class IntegrationBase < Import::Base
|
||||||
|
|
||||||
|
def self.inherited(subclass)
|
||||||
|
subclass.extend(Forwardable)
|
||||||
|
|
||||||
|
# delegate instance methods to the generic class implementations
|
||||||
|
subclass.delegate [:identifier, :active?, :config, :display_name] => subclass
|
||||||
|
end
|
||||||
|
|
||||||
|
# Defines the integration identifier used for
|
||||||
|
# automatic config lookup and error message generation.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# Import::Ldap.identifier
|
||||||
|
# #=> "Ldap"
|
||||||
|
#
|
||||||
|
# return [String]
|
||||||
|
def self.identifier
|
||||||
|
name.split('::').last
|
||||||
|
end
|
||||||
|
|
||||||
|
# Provides the name that is used in texts visible to the user.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# Import::Exchange.display_name
|
||||||
|
# #=> "Exchange"
|
||||||
|
#
|
||||||
|
# return [String]
|
||||||
|
def self.display_name
|
||||||
|
identifier
|
||||||
|
end
|
||||||
|
|
||||||
|
# Checks if the integration is active.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# Import::Ldap.active?
|
||||||
|
# #=> true
|
||||||
|
#
|
||||||
|
# return [Boolean]
|
||||||
|
def self.active?
|
||||||
|
Setting.get("#{identifier.downcase}_integration") || false
|
||||||
|
end
|
||||||
|
|
||||||
|
# Provides the integration configuration.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# Import::Ldap.config
|
||||||
|
# #=> {"ssl_verify"=>true, "host_url"=>"ldaps://192...", ...}
|
||||||
|
#
|
||||||
|
# return [Hash] the configuration
|
||||||
|
def self.config
|
||||||
|
Setting.get("#{identifier.downcase}_config") || {}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Stores the integration configuration.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# Import::Ldap.config = {"ssl_verify"=>true, "host_url"=>"ldaps://192...", ...}
|
||||||
|
#
|
||||||
|
# return [nil]
|
||||||
|
def self.config=(value)
|
||||||
|
Setting.set("#{identifier.downcase}_config", value)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Checks if the integration is activated and configured.
|
||||||
|
# Otherwise it won't get queued since it will display
|
||||||
|
# an error which is confusing and wrong.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# Import::Ldap.queueable?
|
||||||
|
# #=> true
|
||||||
|
#
|
||||||
|
# return [Boolean]
|
||||||
|
def self.queueable?
|
||||||
|
active? && config.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Starts a live or dry run import.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# instance = Import::Ldap.new(import_job)
|
||||||
|
#
|
||||||
|
# @raise [RuntimeError] Raised if an import should start but the integration is disabled
|
||||||
|
#
|
||||||
|
# return [nil]
|
||||||
|
def start
|
||||||
|
return if !requirements_completed?
|
||||||
|
start_import
|
||||||
|
end
|
||||||
|
|
||||||
|
# Gets called when the Scheduler gets (re-)started and an ImportJob was still
|
||||||
|
# in the queue. The job will always get restarted to avoid the gap till the next
|
||||||
|
# run triggered by the Scheduler. The result will get updated to inform the user
|
||||||
|
# in the agent interface result view.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# instance = Import::Ldap.new(import_job)
|
||||||
|
# instance.reschedule?(delayed_job)
|
||||||
|
# #=> true
|
||||||
|
#
|
||||||
|
# return [true]
|
||||||
|
def reschedule?(_delayed_job)
|
||||||
|
inform('Restarting due to scheduler restart.')
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def start_import
|
||||||
|
raise "Missing implementation of method '#{__method__}' for #{self.class.name}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def requirements_completed?
|
||||||
|
return true if @import_job.dry_run
|
||||||
|
|
||||||
|
if !active?
|
||||||
|
message = "Sync cancelled. #{display_name} integration deactivated. Activate via the switch."
|
||||||
|
elsif config.blank? && @import_job.payload.blank?
|
||||||
|
message = "Sync cancelled. #{display_name} configration or ImportJob payload missing."
|
||||||
|
end
|
||||||
|
|
||||||
|
return true if !message
|
||||||
|
inform(message)
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def inform(message)
|
||||||
|
@import_job.update_attribute(:result, {
|
||||||
|
info: message
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,50 +4,17 @@ require 'ldap'
|
||||||
require 'ldap/group'
|
require 'ldap/group'
|
||||||
|
|
||||||
module Import
|
module Import
|
||||||
class Ldap < Import::Base
|
class Ldap < Import::IntegrationBase
|
||||||
|
|
||||||
# Checks if the integration is activated and configured.
|
# Provides the name that is used in texts visible to the user.
|
||||||
# Otherwise it won't get queued since it will display
|
|
||||||
# an error which is confusing and wrong.
|
|
||||||
#
|
#
|
||||||
# @example
|
# @example
|
||||||
# Import::LDAP.queueable?
|
# Import::Ldap.display_name
|
||||||
# #=> true
|
# #=> "LDAP"
|
||||||
#
|
#
|
||||||
# return [Boolean]
|
# return [String]
|
||||||
def self.queueable?
|
def self.display_name
|
||||||
Setting.get('ldap_integration') && Setting.get('ldap_config').present?
|
identifier.upcase
|
||||||
end
|
|
||||||
|
|
||||||
# Starts a live or dry run LDAP import.
|
|
||||||
#
|
|
||||||
# @example
|
|
||||||
# instance = Import::LDAP.new(import_job)
|
|
||||||
#
|
|
||||||
# @raise [RuntimeError] Raised if an import should start but the ldap integration is disabled
|
|
||||||
#
|
|
||||||
# return [nil]
|
|
||||||
def start
|
|
||||||
return if !requirements_completed?
|
|
||||||
start_import
|
|
||||||
end
|
|
||||||
|
|
||||||
# Gets called when the Scheduler gets (re-)started and a LDAP ImportJob was still
|
|
||||||
# in the queue. The job will always get restarted to avoid the gap till the next
|
|
||||||
# run triggered by the Scheduler. The result will get updated to inform the user
|
|
||||||
# in the agent interface result view.
|
|
||||||
#
|
|
||||||
# @example
|
|
||||||
# instance = Import::LDAP.new(import_job)
|
|
||||||
# instance.reschedule?(delayed_job)
|
|
||||||
# #=> true
|
|
||||||
#
|
|
||||||
# return [true]
|
|
||||||
def reschedule?(_delayed_job)
|
|
||||||
@import_job.update_attribute(:result, {
|
|
||||||
info: 'Restarting due to scheduler restart.'
|
|
||||||
})
|
|
||||||
true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -63,23 +30,5 @@ module Import
|
||||||
|
|
||||||
@import_job.result = Import::Ldap::UserFactory.statistics
|
@import_job.result = Import::Ldap::UserFactory.statistics
|
||||||
end
|
end
|
||||||
|
|
||||||
def requirements_completed?
|
|
||||||
return true if @import_job.dry_run
|
|
||||||
|
|
||||||
if !Setting.get('ldap_integration')
|
|
||||||
message = 'Sync cancelled. LDAP integration deactivated. Activate via the switch.'
|
|
||||||
elsif Setting.get('ldap_config').blank? && @import_job.payload.blank?
|
|
||||||
message = 'Sync cancelled. LDAP configration or ImportJob payload missing.'
|
|
||||||
end
|
|
||||||
|
|
||||||
return true if !message
|
|
||||||
|
|
||||||
@import_job.update_attribute(:result, {
|
|
||||||
info: message
|
|
||||||
})
|
|
||||||
|
|
||||||
false
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
19
lib/import/mixin/sequence.rb
Normal file
19
lib/import/mixin/sequence.rb
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
module Import
|
||||||
|
module Mixin
|
||||||
|
module Sequence
|
||||||
|
private
|
||||||
|
|
||||||
|
def sequence_name
|
||||||
|
raise "Missing implementation of '#{__method__}' method for '#{self.class.name}'"
|
||||||
|
end
|
||||||
|
|
||||||
|
def process
|
||||||
|
Sequencer.process(sequence_name,
|
||||||
|
parameters: {
|
||||||
|
import_job: @import_job
|
||||||
|
})
|
||||||
|
end
|
||||||
|
alias start_import process
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
43
lib/mixin/instance_wrapper.rb
Normal file
43
lib/mixin/instance_wrapper.rb
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
module Mixin
|
||||||
|
# This modules enables to redirect all calls to methods that are
|
||||||
|
# not defined to the declared instance variable. This comes handy
|
||||||
|
# when you wan't extend a Ruby core class like Hash.
|
||||||
|
# To inherit directly from such classes is a bad idea and should be avoided.
|
||||||
|
# This way allows it indirectly.
|
||||||
|
module InstanceWrapper
|
||||||
|
module ClassMethods
|
||||||
|
# Creates the class macro `wrap` that activates
|
||||||
|
# the wrapping for the given instance variable name.
|
||||||
|
#
|
||||||
|
# @param [Symbol] variable the name of the instance variable to wrap around
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# wrap :@some_hash
|
||||||
|
#
|
||||||
|
# @return [nil]
|
||||||
|
def wrap(variable)
|
||||||
|
define_method(:instance) {
|
||||||
|
instance_variable_get(variable)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.included(base)
|
||||||
|
base.extend(ClassMethods)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def method_missing(method, *args, &block)
|
||||||
|
if instance.respond_to?(method)
|
||||||
|
instance.send(method, *args, &block)
|
||||||
|
else
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def respond_to_missing?(method_sym, include_all)
|
||||||
|
instance.respond_to?(method_sym, include_all)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
9
lib/mixin/rails_logger.rb
Normal file
9
lib/mixin/rails_logger.rb
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
module Mixin
|
||||||
|
module RailsLogger
|
||||||
|
extend Forwardable
|
||||||
|
extend SingleForwardable
|
||||||
|
|
||||||
|
instance_delegate [:logger] => self
|
||||||
|
single_delegate [:logger] => :Rails
|
||||||
|
end
|
||||||
|
end
|
44
lib/mixin/required_sub_paths.rb
Normal file
44
lib/mixin/required_sub_paths.rb
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
module Mixin
|
||||||
|
module RequiredSubPaths
|
||||||
|
|
||||||
|
def self.included(_base)
|
||||||
|
path = caller_locations.first.path
|
||||||
|
sub_path = File.join(File.dirname(path), File.basename(path, '.rb'))
|
||||||
|
eager_load_recursive(sub_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Loads a directory recursivly.
|
||||||
|
# The specialty of this method is that it will first load all
|
||||||
|
# files in a directory and then start with the sub directories.
|
||||||
|
# This is needed since otherwise some parent namespaces might not
|
||||||
|
# be initialized yet.
|
||||||
|
#
|
||||||
|
# The cause of this is that Rails autoload doesn't work properly
|
||||||
|
# for same named classes or modules in different namespaces.
|
||||||
|
# Here is a good description how autoload works:
|
||||||
|
# http://urbanautomaton.com/blog/2013/08/27/rails-autoloading-hell/
|
||||||
|
#
|
||||||
|
# This avoids a) Rails autoloading issues and b) require '...' workarounds
|
||||||
|
def self.eager_load_recursive(path)
|
||||||
|
|
||||||
|
excluded = ['.', '..']
|
||||||
|
sub_paths = []
|
||||||
|
Dir.entries(path).each do |entry|
|
||||||
|
next if excluded.include?(entry)
|
||||||
|
|
||||||
|
sub_path = File.join(path, entry)
|
||||||
|
|
||||||
|
if File.directory?(sub_path)
|
||||||
|
sub_paths.push(sub_path)
|
||||||
|
elsif sub_path =~ /\A(.*)\.rb\z/
|
||||||
|
require_path = $1
|
||||||
|
require(require_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
sub_paths.each do |sub_path|
|
||||||
|
eager_load_recursive(sub_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
11
lib/mixin/start_finish_logger.rb
Normal file
11
lib/mixin/start_finish_logger.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
module Mixin
|
||||||
|
module StartFinishLogger
|
||||||
|
include ::Mixin::RailsLogger
|
||||||
|
|
||||||
|
def log_start_finish(level, prefix)
|
||||||
|
logger.public_send(level, "#{prefix} started.")
|
||||||
|
yield
|
||||||
|
logger.public_send(level, "#{prefix} finished.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
83
lib/sequencer.rb
Normal file
83
lib/sequencer.rb
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
require 'mixin/rails_logger'
|
||||||
|
require 'mixin/start_finish_logger'
|
||||||
|
|
||||||
|
class Sequencer
|
||||||
|
include ::Mixin::RailsLogger
|
||||||
|
include ::Mixin::StartFinishLogger
|
||||||
|
|
||||||
|
attr_reader :sequence
|
||||||
|
|
||||||
|
# Convenience wrapper for instant processing with the given attributes.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# Sequencer.process('Example::Sequence')
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# Sequencer.process('Example::Sequence',
|
||||||
|
# parameters: {
|
||||||
|
# some: 'value',
|
||||||
|
# },
|
||||||
|
# expecting: [:result, :answer]
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# @return [Hash{Symbol => Object}] the final result state attributes and values
|
||||||
|
def self.process(sequence, *args)
|
||||||
|
new(sequence, *args).process
|
||||||
|
end
|
||||||
|
|
||||||
|
# Initializes a new Sequencer instance for the given Sequence with parameters and expecting result.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# Sequencer.new('Example::Sequence')
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# Sequencer.new('Example::Sequence',
|
||||||
|
# parameters: {
|
||||||
|
# some: 'value',
|
||||||
|
# },
|
||||||
|
# expecting: [:result, :answer]
|
||||||
|
# )
|
||||||
|
def initialize(sequence, parameters: {}, expecting: nil)
|
||||||
|
@sequence = Sequencer::Sequence.constantize(sequence)
|
||||||
|
@parameters = parameters
|
||||||
|
@expecting = expecting
|
||||||
|
|
||||||
|
# fall back to sequence default expecting if no explicit
|
||||||
|
# expecting was given for this sequence
|
||||||
|
return if !@expecting.nil?
|
||||||
|
@expecting = @sequence.expecting
|
||||||
|
end
|
||||||
|
|
||||||
|
# Processes the Sequence the instance was initialized with.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# sequence.process
|
||||||
|
#
|
||||||
|
# @return [Hash{Symbol => Object}] the final result state attributes and values
|
||||||
|
def process
|
||||||
|
log_start_finish(:info, "Sequence '#{@sequence.name}'") do
|
||||||
|
|
||||||
|
sequence.units.each_with_index do |unit, index|
|
||||||
|
|
||||||
|
state.process do
|
||||||
|
|
||||||
|
log_start_finish(:info, "Sequence '#{sequence.name}' Unit '#{unit.name}' (index: #{index})") do
|
||||||
|
unit.process(state)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
state.to_h.tap do |result|
|
||||||
|
logger.debug("Returning Sequence '#{@sequence.name}' result: #{result.inspect}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def state
|
||||||
|
@state ||= Sequencer::State.new(sequence,
|
||||||
|
parameters: @parameters,
|
||||||
|
expecting: @expecting)
|
||||||
|
end
|
||||||
|
end
|
18
lib/sequencer/mixin/exchange/folder.rb
Normal file
18
lib/sequencer/mixin/exchange/folder.rb
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
class Sequencer
|
||||||
|
module Mixin
|
||||||
|
module Exchange
|
||||||
|
module Folder
|
||||||
|
|
||||||
|
def self.included(base)
|
||||||
|
base.uses :ews_connection
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def ews_folder
|
||||||
|
@ews_folder ||= ::Import::Exchange::Folder.new(ews_connection)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
37
lib/sequencer/mixin/import_job/resource_loop.rb
Normal file
37
lib/sequencer/mixin/import_job/resource_loop.rb
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
require 'sequencer/mixin/sub_sequence'
|
||||||
|
|
||||||
|
class Sequencer
|
||||||
|
module Mixin
|
||||||
|
module ImportJob
|
||||||
|
module ResourceLoop
|
||||||
|
include ::Sequencer::Mixin::SubSequence
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def resource_sequence(sequence_name, items)
|
||||||
|
default_params = {
|
||||||
|
dry_run: import_job.dry_run,
|
||||||
|
import_job: import_job,
|
||||||
|
}
|
||||||
|
|
||||||
|
items.each do |item|
|
||||||
|
resource_params = {}
|
||||||
|
if block_given?
|
||||||
|
resource_params = yield item
|
||||||
|
else
|
||||||
|
resource_params[:resource] = item
|
||||||
|
end
|
||||||
|
|
||||||
|
resource_params[:resource] = resource_params[:resource].with_indifferent_access
|
||||||
|
|
||||||
|
sub_sequence(sequence_name,
|
||||||
|
parameters: default_params.merge(resource_params))
|
||||||
|
end
|
||||||
|
|
||||||
|
# store possible unsaved values in result e.g. statistics
|
||||||
|
import_job.save!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
43
lib/sequencer/mixin/prefixed_constantize.rb
Normal file
43
lib/sequencer/mixin/prefixed_constantize.rb
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
class Sequencer
|
||||||
|
module Mixin
|
||||||
|
# Classes that extend this module need a PREFIX constant.
|
||||||
|
module PrefixedConstantize
|
||||||
|
# Returns the class for a given name String independend of the prefix.
|
||||||
|
#
|
||||||
|
# @param [String] sequence the name String for the requested class
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# Sequencer::Sequence.constantize('ExampleSequence')
|
||||||
|
# #=> Sequencer::Sequence::ExampleSequence
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# Sequencer::Unit.constantize('Sequencer::Unit::Example::Unit')
|
||||||
|
# #=> Sequencer::Unit::Example::Unit
|
||||||
|
#
|
||||||
|
# @return [Object] the class for the given String
|
||||||
|
def constantize(name_string)
|
||||||
|
namespace(name_string).constantize
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the complete class namespace for a given name String
|
||||||
|
# independend of the prefix.
|
||||||
|
#
|
||||||
|
# @param [String] sequence the name String for the requested class namespace
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# Sequencer::Sequence.namespace('ExampleSequence')
|
||||||
|
# #=> 'Sequencer::Sequence::ExampleSequence'
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# Sequencer::Unit.namespace('Sequencer::Unit::Example::Unit')
|
||||||
|
# #=> 'Sequencer::Unit::Example::Unit'
|
||||||
|
#
|
||||||
|
# @return [String] the class namespace for the given String
|
||||||
|
def namespace(name_string)
|
||||||
|
prefix = const_get(:PREFIX)
|
||||||
|
return name_string if name_string.start_with?(prefix)
|
||||||
|
"#{prefix}#{name_string}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
9
lib/sequencer/mixin/sub_sequence.rb
Normal file
9
lib/sequencer/mixin/sub_sequence.rb
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
class Sequencer
|
||||||
|
module Mixin
|
||||||
|
module SubSequence
|
||||||
|
def sub_sequence(sequence, args = {})
|
||||||
|
Sequencer.process(sequence, args)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
16
lib/sequencer/sequence.rb
Normal file
16
lib/sequencer/sequence.rb
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
require 'sequencer/mixin/prefixed_constantize'
|
||||||
|
|
||||||
|
class Sequencer
|
||||||
|
class Sequence
|
||||||
|
extend ::Sequencer::Mixin::PrefixedConstantize
|
||||||
|
|
||||||
|
PREFIX = 'Sequencer::Sequence::'.freeze
|
||||||
|
|
||||||
|
attr_reader :units, :expecting
|
||||||
|
|
||||||
|
def initialize(units:, expecting: [])
|
||||||
|
@units = units
|
||||||
|
@expecting = expecting
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
48
lib/sequencer/sequence/base.rb
Normal file
48
lib/sequencer/sequence/base.rb
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
class Sequencer
|
||||||
|
class Sequence
|
||||||
|
class Base
|
||||||
|
|
||||||
|
# Defines the default attributes that will get returned for the sequence.
|
||||||
|
# These can be overwritten by giving the :expecting key to a sequence process call.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# Sequencer::Sequence::Example.expecting
|
||||||
|
# # => [:result, :list]
|
||||||
|
#
|
||||||
|
# @return [Array<Symbol>] the list of expected result keys.
|
||||||
|
def self.expecting
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Defines the list of Units that form the sequence. The units will get
|
||||||
|
# processed in the order they are defined in. The namespaces can be
|
||||||
|
# absolute or without the `Sequencer::Unit` prefix.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# Sequencer::Sequence::Example.sequence
|
||||||
|
# # => ['Import::Example::Resource', 'Sequencer::Unit::Import::Model::Create', ...]
|
||||||
|
#
|
||||||
|
# @return [Array<String>] the list of units forming the sequence.
|
||||||
|
def self.sequence
|
||||||
|
raise "Missing implementation of '#{__method__}' method for '#{name}'"
|
||||||
|
end
|
||||||
|
|
||||||
|
# This is an internally used method that converts the defined sequence to a
|
||||||
|
# Sequencer::Units instance which has special methods.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# Sequencer::Sequence::Example.units
|
||||||
|
# # => <Sequencer::Units @units=[....>
|
||||||
|
#
|
||||||
|
# @return [Object]
|
||||||
|
def self.units
|
||||||
|
Sequencer::Units.new(*sequence)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @see .units
|
||||||
|
def units
|
||||||
|
self.class.units
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
17
lib/sequencer/sequence/exchange/folder/attributes.rb
Normal file
17
lib/sequencer/sequence/exchange/folder/attributes.rb
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
class Sequencer
|
||||||
|
class Sequence
|
||||||
|
module Exchange
|
||||||
|
module Folder
|
||||||
|
class Attributes < Sequencer::Sequence::Base
|
||||||
|
|
||||||
|
def self.sequence
|
||||||
|
[
|
||||||
|
'Exchange::Connection',
|
||||||
|
'Exchange::Folder::Attributes',
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,23 @@
|
||||||
|
class Sequencer
|
||||||
|
class Sequence
|
||||||
|
module Import
|
||||||
|
module Exchange
|
||||||
|
class AttributesExamples < Sequencer::Sequence::Base
|
||||||
|
|
||||||
|
def self.expecting
|
||||||
|
[:attributes]
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.sequence
|
||||||
|
[
|
||||||
|
'Exchange::Connection',
|
||||||
|
'Exchange::Folders::ByIds',
|
||||||
|
'Import::Exchange::AttributeExamples',
|
||||||
|
'Import::Exchange::AttributeMapper::AttributeExamples',
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
22
lib/sequencer/sequence/import/exchange/available_folders.rb
Normal file
22
lib/sequencer/sequence/import/exchange/available_folders.rb
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
class Sequencer
|
||||||
|
class Sequence
|
||||||
|
module Import
|
||||||
|
module Exchange
|
||||||
|
class AvailableFolders < Sequencer::Sequence::Base
|
||||||
|
|
||||||
|
def self.expecting
|
||||||
|
[:folders]
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.sequence
|
||||||
|
[
|
||||||
|
'Exchange::Connection',
|
||||||
|
'Exchange::Folders::IdPathMap',
|
||||||
|
'Import::Exchange::AttributeMapper::AvailableFolders',
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
33
lib/sequencer/sequence/import/exchange/folder_contact.rb
Normal file
33
lib/sequencer/sequence/import/exchange/folder_contact.rb
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
class Sequencer
|
||||||
|
class Sequence
|
||||||
|
module Import
|
||||||
|
module Exchange
|
||||||
|
class FolderContact < Sequencer::Sequence::Base
|
||||||
|
|
||||||
|
def self.sequence
|
||||||
|
[
|
||||||
|
'Import::Exchange::FolderContact::RemoteId',
|
||||||
|
'Import::Exchange::FolderContact::Mapping',
|
||||||
|
'Import::Common::Model::Skip::Blank::Mapped',
|
||||||
|
'Import::Exchange::FolderContact::StaticAttributes',
|
||||||
|
'Import::Common::Model::ExternalSync::Lookup',
|
||||||
|
'Import::Common::Model::Associations::Extract',
|
||||||
|
'Import::Common::User::Attributes::Downcase',
|
||||||
|
'Import::Common::User::Email::CheckValidity',
|
||||||
|
'Import::Common::Model::Attributes::AddByIds',
|
||||||
|
'Import::Common::Model::Update',
|
||||||
|
'Import::Common::Model::Create',
|
||||||
|
'Import::Common::Model::Associations::Assign',
|
||||||
|
'Import::Common::Model::Save',
|
||||||
|
'Import::Common::Model::ExternalSync::Create',
|
||||||
|
'Import::Exchange::FolderContact::HttpLog',
|
||||||
|
'Import::Exchange::FolderContact::Statistics::Diff',
|
||||||
|
'Import::Common::ImportJob::Statistics::Update',
|
||||||
|
'Import::Common::ImportJob::Statistics::Store',
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
22
lib/sequencer/sequence/import/exchange/folder_contacts.rb
Normal file
22
lib/sequencer/sequence/import/exchange/folder_contacts.rb
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
class Sequencer
|
||||||
|
class Sequence
|
||||||
|
module Import
|
||||||
|
module Exchange
|
||||||
|
class FolderContacts < Sequencer::Sequence::Base
|
||||||
|
|
||||||
|
def self.sequence
|
||||||
|
[
|
||||||
|
'Import::Exchange::FolderContacts::DryRunPayload',
|
||||||
|
'Exchange::Connection',
|
||||||
|
'Import::Exchange::FolderContacts::FolderIds',
|
||||||
|
'Import::Exchange::FolderContacts::Sum',
|
||||||
|
'Import::Common::ImportJob::Statistics::Update',
|
||||||
|
'Import::Common::ImportJob::Statistics::Store',
|
||||||
|
'Import::Exchange::FolderContacts::SubSequence',
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
272
lib/sequencer/state.rb
Normal file
272
lib/sequencer/state.rb
Normal file
|
@ -0,0 +1,272 @@
|
||||||
|
require 'mixin/rails_logger'
|
||||||
|
require 'mixin/start_finish_logger'
|
||||||
|
|
||||||
|
class Sequencer
|
||||||
|
class State
|
||||||
|
include ::Mixin::RailsLogger
|
||||||
|
include ::Mixin::StartFinishLogger
|
||||||
|
|
||||||
|
def initialize(sequence, parameters: {}, expecting: nil)
|
||||||
|
@index = -1
|
||||||
|
@units = sequence.units
|
||||||
|
@result_index = @units.count
|
||||||
|
@values = {}
|
||||||
|
|
||||||
|
initialize_attributes(sequence.units)
|
||||||
|
initialize_parameters(parameters.with_indifferent_access)
|
||||||
|
initialize_expectations(expecting || sequence.expecting)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Stores a value for the given attribute. Value can be a regular object
|
||||||
|
# or the result of a given code block.
|
||||||
|
# The attribute gets validated against the .provides list of attributes.
|
||||||
|
# In the case than an attribute gets provided that is not declared to
|
||||||
|
# be provided an exception will be raised.
|
||||||
|
#
|
||||||
|
# @param [Symbol] attribute the attribute for which the value gets provided.
|
||||||
|
# @param [Object] value the value that should get stored for the given attribute.
|
||||||
|
# @yield [] executes the given block and takes the result as the value.
|
||||||
|
# @yieldreturn [Object] the value for the given attribute.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# state.provide(:sum, 3)
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# state.provide(:sum) do
|
||||||
|
# some_value = rand(100)
|
||||||
|
# some_value * 3
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# @raise [RuntimeError] if the attribute is not provideable from the calling Unit
|
||||||
|
#
|
||||||
|
# @return [nil]
|
||||||
|
def provide(attribute, value = nil)
|
||||||
|
if provideable?(attribute)
|
||||||
|
value = yield if block_given?
|
||||||
|
set(attribute, value)
|
||||||
|
else
|
||||||
|
value = "UNEXECUTED BLOCK: #{caller(1..1).first}" if block_given?
|
||||||
|
unprovideable_setter(attribute, value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the value of the given attribute.
|
||||||
|
# The attribute gets validated against the .uses list of attributes. In the
|
||||||
|
# case than an attribute gets used that is not declared to be used
|
||||||
|
# an exception will be raised.
|
||||||
|
#
|
||||||
|
# @param [Symbol] attribute the attribute for which the value is requested.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# state.use(:answer)
|
||||||
|
# #=> 42
|
||||||
|
#
|
||||||
|
# @raise [RuntimeError] if the attribute is not useable from the calling Unit
|
||||||
|
#
|
||||||
|
# @return [nil]
|
||||||
|
def use(attribute)
|
||||||
|
if useable?(attribute)
|
||||||
|
get(attribute)
|
||||||
|
else
|
||||||
|
unaccessable_getter(attribute)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the value of the given attribute.
|
||||||
|
# The attribute DOES NOT get validated against the .uses list of attributes.
|
||||||
|
#
|
||||||
|
# @param [Symbol] attribute the attribute for which the value is requested.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# state.optional(:answer)
|
||||||
|
# #=> 42
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# state.optional(:unknown)
|
||||||
|
# #=> nil
|
||||||
|
#
|
||||||
|
# @return [Object, nil]
|
||||||
|
def optional(attribute)
|
||||||
|
return get(attribute) if @attributes.known?(attribute)
|
||||||
|
logger.debug("Access to unknown optional attribute '#{attribute}'.")
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# Checks if a value for the given attribute is provided.
|
||||||
|
# The attribute DOES NOT get validated against the .uses list of attributes.
|
||||||
|
#
|
||||||
|
# @param [Symbol] attribute the attribute which should get checked.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# state.provided?(:answer)
|
||||||
|
# #=> true
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# state.provided?(:unknown)
|
||||||
|
# #=> false
|
||||||
|
#
|
||||||
|
# @return [Boolean]
|
||||||
|
def provided?(attribute)
|
||||||
|
optional(attribute) != nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# Unsets the value for the given attribute.
|
||||||
|
# The attribute gets validated against the .uses list of attributes.
|
||||||
|
# In the case than an attribute gets unset that is not declared
|
||||||
|
# to be used an exception will be raised.
|
||||||
|
#
|
||||||
|
# @param [Symbol] attribute the attribute for which the value gets unset.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# state.unset(:answer)
|
||||||
|
#
|
||||||
|
# @raise [RuntimeError] if the attribute is not useable from the calling Unit
|
||||||
|
#
|
||||||
|
# @return [nil]
|
||||||
|
def unset(attribute)
|
||||||
|
value = nil
|
||||||
|
if useable?(attribute)
|
||||||
|
set(attribute, value)
|
||||||
|
else
|
||||||
|
unprovideable_setter(attribute, value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Handles state processing of the next Unit in the Sequence while executing
|
||||||
|
# the given block. After the Unit is processed the state will get cleaned up
|
||||||
|
# and no longer needed attribute values will get discarded.
|
||||||
|
#
|
||||||
|
# @yield [] executes the given block and handles the state changes before and afterwards.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# state.process do
|
||||||
|
# unit.process
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# @return [nil]
|
||||||
|
def process
|
||||||
|
@index += 1
|
||||||
|
yield
|
||||||
|
cleanup
|
||||||
|
end
|
||||||
|
|
||||||
|
# Handles state processing of the next Unit in the Sequence while executing
|
||||||
|
# the given block. After the Unit is processed the state will get cleaned up
|
||||||
|
# and no longer needed attribute values will get discarded.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# state.to_h
|
||||||
|
# #=> {"ssl_verify"=>true, "host_url"=>"ldaps://192...", ...}
|
||||||
|
#
|
||||||
|
# @return [Hash{Symbol => Object}]
|
||||||
|
def to_h
|
||||||
|
available.map { |identifier| [identifier, @values[identifier]] }.to_h
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def available
|
||||||
|
@attributes.select do |_identifier, attribute|
|
||||||
|
@index.between?(attribute.from, attribute.to)
|
||||||
|
end.keys
|
||||||
|
end
|
||||||
|
|
||||||
|
def unit(index = nil)
|
||||||
|
@units[index || @index]
|
||||||
|
end
|
||||||
|
|
||||||
|
def provideable?(attribute)
|
||||||
|
unit.provides.include?(attribute)
|
||||||
|
end
|
||||||
|
|
||||||
|
def useable?(attribute)
|
||||||
|
unit.uses.include?(attribute)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set(attribute, value)
|
||||||
|
logger.debug("Setting '#{attribute}' value (#{value.class.name}): #{value.inspect}")
|
||||||
|
@values[attribute] = value
|
||||||
|
end
|
||||||
|
|
||||||
|
def get(attribute)
|
||||||
|
value = @values[attribute]
|
||||||
|
logger.debug("Getting '#{attribute}' value (#{value.class.name}): #{value.inspect}")
|
||||||
|
value
|
||||||
|
end
|
||||||
|
|
||||||
|
def unprovideable_setter(attribute, value)
|
||||||
|
message = "Unprovideable attribute '#{attribute}' set with value (#{value.class.name}): '#{value}'"
|
||||||
|
logger.error(message)
|
||||||
|
raise message
|
||||||
|
end
|
||||||
|
|
||||||
|
def unaccessable_getter(attribute)
|
||||||
|
message = "Unaccessable getter used for attribute '#{attribute}'"
|
||||||
|
logger.error(message)
|
||||||
|
raise message
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize_attributes(units)
|
||||||
|
log_start_finish(:debug, 'Attributes lifespan initialization') do
|
||||||
|
@attributes = Sequencer::Units::Attributes.new(units.declarations)
|
||||||
|
logger.debug("Attributes lifespan: #{@attributes.inspect}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize_parameters(parameters)
|
||||||
|
logger.debug("Initializing Sequencer::State with initial parameters: #{parameters.inspect}")
|
||||||
|
|
||||||
|
log_start_finish(:debug, 'Attribute value provisioning check and initialization') do
|
||||||
|
|
||||||
|
@attributes.each do |identifier, attribute|
|
||||||
|
|
||||||
|
if !attribute.will_be_used?
|
||||||
|
logger.debug("Attribute '#{identifier}' is provided by Unit(s) but never used.")
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
init_param = parameters.key?(identifier)
|
||||||
|
provided_attr = attribute.will_be_provided?
|
||||||
|
|
||||||
|
if !init_param && !provided_attr
|
||||||
|
message = "Attribute '#{identifier}' is used in Unit '#{unit(attribute.to).name}' (index: #{attribute.to}) but is not provided or given via initial parameters."
|
||||||
|
logger.error(message)
|
||||||
|
raise message
|
||||||
|
end
|
||||||
|
|
||||||
|
# skip if attribute is provided by an Unit but not
|
||||||
|
# an initial parameter
|
||||||
|
next if !init_param
|
||||||
|
|
||||||
|
# update 'from' lifespan information for attribute
|
||||||
|
# since it's provided via the initial parameter
|
||||||
|
attribute.from = @index
|
||||||
|
|
||||||
|
# set initial value
|
||||||
|
set(identifier, parameters[identifier])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize_expectations(expected_attributes)
|
||||||
|
expected_attributes.each do |identifier|
|
||||||
|
logger.debug("Adding attribute '#{identifier}' to the list of expected result attributes.")
|
||||||
|
@attributes[identifier].to = @result_index
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def cleanup
|
||||||
|
log_start_finish(:info, "State cleanup of Unit #{unit.name} (index: #{@index})") do
|
||||||
|
|
||||||
|
@attributes.delete_if do |identifier, attribute|
|
||||||
|
remove = !attribute.will_be_used?
|
||||||
|
remove ||= attribute.to <= @index
|
||||||
|
if remove && attribute.will_be_used?
|
||||||
|
logger.debug("Removing unneeded attribute '#{identifier}': #{@values[identifier]}")
|
||||||
|
end
|
||||||
|
remove
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
71
lib/sequencer/unit.rb
Normal file
71
lib/sequencer/unit.rb
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
require 'sequencer/mixin/prefixed_constantize'
|
||||||
|
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
include ::Mixin::RequiredSubPaths
|
||||||
|
extend ::Sequencer::Mixin::PrefixedConstantize
|
||||||
|
|
||||||
|
PREFIX = 'Sequencer::Unit::'.freeze
|
||||||
|
|
||||||
|
# Convenience wrapper for processing a single Unit.
|
||||||
|
#
|
||||||
|
# ATTENTION: This should only be used for development, testing or debugging purposes.
|
||||||
|
# There might be a check in the future to prevent using this method in other scopes.
|
||||||
|
#
|
||||||
|
# @see #initialize
|
||||||
|
# @see #process
|
||||||
|
def self.process(unit, parameters, &block)
|
||||||
|
new(unit).process(parameters, &block)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Initializes a new Sequencer::Unit for processing it.
|
||||||
|
#
|
||||||
|
# ATTENTION: This should only be used for development, testing or debugging purposes.
|
||||||
|
# There might be a check in the future to prevent using this method in other scopes.
|
||||||
|
#
|
||||||
|
# @param [String] unit the name String for the Unit that should get processed
|
||||||
|
def initialize(unit)
|
||||||
|
@unit = self.class.constantize(unit)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Processes the Sequencer::Unit that the instance was initialized with.
|
||||||
|
#
|
||||||
|
# ATTENTION: This should only be used for development, testing or debugging purposes.
|
||||||
|
# There might be a check in the future to prevent using this method in other scopes.
|
||||||
|
#
|
||||||
|
# @param [Hash{Symbol => Object}] parameters the parameters for initializing the Sequencer::State
|
||||||
|
# @yield [instance] optional block to access the Unit instance
|
||||||
|
# @yieldparam instance [Object] the Unit instance for e.g. adding expectations
|
||||||
|
def process(parameters)
|
||||||
|
@parameters = parameters
|
||||||
|
instance = @unit.new(state)
|
||||||
|
|
||||||
|
# yield instance to apply expectations
|
||||||
|
yield instance if block_given?
|
||||||
|
|
||||||
|
state.process do
|
||||||
|
instance.process
|
||||||
|
end
|
||||||
|
|
||||||
|
state.to_h
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def state
|
||||||
|
@state ||= begin
|
||||||
|
units = Sequencer::Units.new(
|
||||||
|
@unit.name
|
||||||
|
)
|
||||||
|
|
||||||
|
sequence = Sequencer::Sequence.new(
|
||||||
|
units: units,
|
||||||
|
expecting: @unit.provides,
|
||||||
|
)
|
||||||
|
|
||||||
|
Sequencer::State.new(sequence,
|
||||||
|
parameters: @parameters)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
212
lib/sequencer/unit/base.rb
Normal file
212
lib/sequencer/unit/base.rb
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
require 'mixin/rails_logger'
|
||||||
|
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
class Base
|
||||||
|
include ::Mixin::RailsLogger
|
||||||
|
|
||||||
|
attr_reader :state
|
||||||
|
|
||||||
|
# Creates the class macro `uses` that allows a Unit to
|
||||||
|
# declare the attributes it will use via parameter or block.
|
||||||
|
# On the other hand it returns the declared attributes if
|
||||||
|
# called without parameters.
|
||||||
|
#
|
||||||
|
# This method can be called multiple times and will add the
|
||||||
|
# given attributes to the list. It takes care of handling
|
||||||
|
# duplicates so no uniq check is required. It's safe to use
|
||||||
|
# for inheritance structures and modules.
|
||||||
|
#
|
||||||
|
# It additionally creates a getter instance method for each declared
|
||||||
|
# attribute like e.g. attr_reader does. This allows direct access
|
||||||
|
# to an attribute via `attribute_name`. See examples.
|
||||||
|
#
|
||||||
|
# @param [Array<Symbol>] attributes an optional list of attributes that the Unit uses
|
||||||
|
#
|
||||||
|
# @yield [] A block returning a list of attributes
|
||||||
|
#
|
||||||
|
# @example Via regular Array<Symbol> parameter
|
||||||
|
# uses :instance, :action, :connection
|
||||||
|
#
|
||||||
|
# @example Via block
|
||||||
|
# uses do
|
||||||
|
# additional = method(parameter)
|
||||||
|
# [:some, additional]
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# @example Listing declared attributes
|
||||||
|
# Unit::Name.uses
|
||||||
|
# # => [:instance, :action, :connection, :some, :suprise]
|
||||||
|
#
|
||||||
|
# @example Using declared attribute in the Unit via state object
|
||||||
|
# state.use(:instance).id
|
||||||
|
#
|
||||||
|
# @example Using declared attribute in the Unit via getter
|
||||||
|
# instance.id
|
||||||
|
#
|
||||||
|
# @return [Array<Symbol>] the list of all declared uses of a Unit.
|
||||||
|
def self.uses(*attributes, &block)
|
||||||
|
declaration_accessor(
|
||||||
|
key: __method__,
|
||||||
|
attributes: attributes(*attributes, &block)
|
||||||
|
) do |attribute|
|
||||||
|
use_getter(attribute)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Creates the class macro `provides` that allows a Unit to
|
||||||
|
# declare the attributes it will provided via parameter or block.
|
||||||
|
# On the other hand it returns the declared attributes if
|
||||||
|
# called without parameters.
|
||||||
|
#
|
||||||
|
# This method can be called multiple times and will add the
|
||||||
|
# given attributes to the list. It takes care of handling
|
||||||
|
# duplicates so no uniq check is required. It's safe to use
|
||||||
|
# for inheritance structures and modules.
|
||||||
|
#
|
||||||
|
# It additionally creates a setter instance method for each declared
|
||||||
|
# attribute like e.g. attr_writer does. This allows direct access
|
||||||
|
# to an attribute via `self.attribute_name = `. See examples.
|
||||||
|
#
|
||||||
|
# A Unit should usually not provide more than one or two attributes.
|
||||||
|
# If your Unit provides it's doing to much and should be splitted
|
||||||
|
# into multiple Units.
|
||||||
|
#
|
||||||
|
# @param [Array<Symbol>] attributes an optional list of attributes that the Unit provides
|
||||||
|
#
|
||||||
|
# @yield [] A block returning a list of attributes
|
||||||
|
#
|
||||||
|
# @example Via regular Array<Symbol> parameter
|
||||||
|
# provides :instance, :action, :connection
|
||||||
|
#
|
||||||
|
# @example Via block
|
||||||
|
# provides do
|
||||||
|
# additional = method(parameter)
|
||||||
|
# [:some, additional]
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# @example Listing declared attributes
|
||||||
|
# Unit::Name.provides
|
||||||
|
# # => [:instance, :action, :connection, :some, :suprise]
|
||||||
|
#
|
||||||
|
# @example Providing declared attribute in the Unit via state object parameter
|
||||||
|
# state.provide(:action, :created)
|
||||||
|
#
|
||||||
|
# @example Providing declared attribute in the Unit via state object block
|
||||||
|
# state.provide(:instance) do
|
||||||
|
# # ...
|
||||||
|
# instance
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# @example Providing declared attribute in the Unit via setter
|
||||||
|
# self.action = :created
|
||||||
|
#
|
||||||
|
# @return [Array<Symbol>] the list of all declared provides of a Unit.
|
||||||
|
def self.provides(*attributes, &block)
|
||||||
|
declaration_accessor(
|
||||||
|
key: __method__,
|
||||||
|
attributes: attributes(*attributes, &block)
|
||||||
|
) do |attribute|
|
||||||
|
provide_setter(attribute)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.attributes(*attributes)
|
||||||
|
# exectute block if given and add
|
||||||
|
# the result to the (possibly empty)
|
||||||
|
# list of given attributes
|
||||||
|
attributes.concat(yield) if block_given?
|
||||||
|
attributes
|
||||||
|
end
|
||||||
|
|
||||||
|
# This method is the heart of the #uses and #provides method.
|
||||||
|
# It takes the declaration key and decides based on the given
|
||||||
|
# parameters if the given attributes should get stored or
|
||||||
|
# the stored values returned.
|
||||||
|
def self.declaration_accessor(key:, attributes:)
|
||||||
|
|
||||||
|
# if no attributes were given (storing)
|
||||||
|
# return the already stored list of attributes
|
||||||
|
return declarations(key).to_a if attributes.blank?
|
||||||
|
|
||||||
|
# loop over all given attributes and
|
||||||
|
# add them to the list of already stored
|
||||||
|
# attributes for the given declaration key
|
||||||
|
attributes.each do |attribute|
|
||||||
|
next if !declarations(key).add?(attribute)
|
||||||
|
|
||||||
|
# yield callback if given to create
|
||||||
|
# getter or setter or whatever
|
||||||
|
yield(attribute) if block_given?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# This method creates the convenience method
|
||||||
|
# getter for the given attribute.
|
||||||
|
def self.use_getter(attribute)
|
||||||
|
define_method(attribute) do
|
||||||
|
instance_variable_cached(attribute) do
|
||||||
|
state.use(attribute)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# This method creates the convenience method
|
||||||
|
# setter for the given attribute.
|
||||||
|
def self.provide_setter(attribute)
|
||||||
|
define_method("#{attribute}=") do |value|
|
||||||
|
state.provide(attribute, value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# This method is the attribute store for the given declaration key.
|
||||||
|
def self.declarations(key)
|
||||||
|
instance_variable_cached("#{key}_declarations") do
|
||||||
|
declarations_initial(key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# This method initializes the attribute store for the given declaration key.
|
||||||
|
# It checks if a parent class already has an existing store and duplicates it
|
||||||
|
# for independent usage. Otherwise it creates a new one.
|
||||||
|
def self.declarations_initial(key)
|
||||||
|
return Set.new([]) if !superclass.respond_to?(:declarations)
|
||||||
|
superclass.send(:declarations, key).dup
|
||||||
|
end
|
||||||
|
|
||||||
|
# This method creates an accessor to a cached instance variable for the given scope.
|
||||||
|
# It will create a new variable with the result of the given block as an initial value.
|
||||||
|
# On later calls it will return the already initialized, cached variable state.
|
||||||
|
# The variable will be created by default as a class variable. If a instance scope is
|
||||||
|
# passed it will create an instance variable instead.
|
||||||
|
def self.instance_variable_cached(key, scope: self)
|
||||||
|
cache = "@#{key}"
|
||||||
|
value = scope.instance_variable_get(cache)
|
||||||
|
return value if value
|
||||||
|
value = yield
|
||||||
|
scope.instance_variable_set(cache, value)
|
||||||
|
end
|
||||||
|
|
||||||
|
# This method is an instance wrapper around the class method .instance_variable_cached.
|
||||||
|
# It will behave the same but passed the instance scope to create an
|
||||||
|
# cached instance variable.
|
||||||
|
def instance_variable_cached(key, &block)
|
||||||
|
self.class.instance_variable_cached(key, scope: self, &block)
|
||||||
|
end
|
||||||
|
|
||||||
|
# This method is an convenience wrapper to create an instance
|
||||||
|
# and then directly processing it.
|
||||||
|
def self.process(*args)
|
||||||
|
new(*args).process
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(state)
|
||||||
|
@state = state
|
||||||
|
end
|
||||||
|
|
||||||
|
def process
|
||||||
|
raise "Missing implementation of '#{__method__}' method for '#{self.class.name}'"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
28
lib/sequencer/unit/common/attribute_mapper.rb
Normal file
28
lib/sequencer/unit/common/attribute_mapper.rb
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Common
|
||||||
|
class AttributeMapper < Sequencer::Unit::Base
|
||||||
|
|
||||||
|
def self.map
|
||||||
|
raise "Missing implementation of '#{__method__}' method for '#{name}'"
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.uses
|
||||||
|
map.keys
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.provides
|
||||||
|
map.values
|
||||||
|
end
|
||||||
|
|
||||||
|
def process
|
||||||
|
self.class.map.each do |original, renamed|
|
||||||
|
state.provide(renamed) do
|
||||||
|
state.use(original)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
23
lib/sequencer/unit/exchange/connection.rb
Normal file
23
lib/sequencer/unit/exchange/connection.rb
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Exchange
|
||||||
|
class Connection < Sequencer::Unit::Base
|
||||||
|
|
||||||
|
uses :ews_config
|
||||||
|
provides :ews_connection
|
||||||
|
|
||||||
|
def process
|
||||||
|
# check if EWS connection is already given (sub sequence)
|
||||||
|
return if state.provided?(:ews_connection)
|
||||||
|
|
||||||
|
state.provide(:ews_connection) do
|
||||||
|
config = ews_config
|
||||||
|
config ||= ::Import::Exchange.config
|
||||||
|
|
||||||
|
Viewpoint::EWSClient.new(config[:endpoint], config[:user], config[:password])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
24
lib/sequencer/unit/exchange/folders/by_ids.rb
Normal file
24
lib/sequencer/unit/exchange/folders/by_ids.rb
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
require 'sequencer/mixin/exchange/folder'
|
||||||
|
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Exchange
|
||||||
|
module Folders
|
||||||
|
class ByIds < Sequencer::Unit::Base
|
||||||
|
include ::Sequencer::Mixin::Exchange::Folder
|
||||||
|
|
||||||
|
uses :ews_folder_ids
|
||||||
|
provides :ews_folders
|
||||||
|
|
||||||
|
def process
|
||||||
|
state.provide(:ews_folders) do
|
||||||
|
ews_folder_ids.collect do |folder_id|
|
||||||
|
ews_folder.find(folder_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
31
lib/sequencer/unit/exchange/folders/id_path_map.rb
Normal file
31
lib/sequencer/unit/exchange/folders/id_path_map.rb
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
require 'sequencer/mixin/exchange/folder'
|
||||||
|
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Exchange
|
||||||
|
module Folders
|
||||||
|
class IdPathMap < Sequencer::Unit::Base
|
||||||
|
include ::Sequencer::Mixin::Exchange::Folder
|
||||||
|
|
||||||
|
provides :ews_folder_id_path_map
|
||||||
|
|
||||||
|
def process
|
||||||
|
state.provide(:ews_folder_id_path_map) do
|
||||||
|
|
||||||
|
ids = state.optional(:ews_folder_ids)
|
||||||
|
ids ||= []
|
||||||
|
|
||||||
|
ews_folder.id_folder_map.collect do |id, folder|
|
||||||
|
next if ids.present? && ids.exclude?(id)
|
||||||
|
next if folder.total_count.blank?
|
||||||
|
next if folder.total_count.zero?
|
||||||
|
|
||||||
|
[id, ews_folder.display_path(folder)]
|
||||||
|
end.compact.to_h
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,25 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module ImportJob
|
||||||
|
module Payload
|
||||||
|
class ToState < Sequencer::Unit::Base
|
||||||
|
|
||||||
|
uses :import_job
|
||||||
|
|
||||||
|
def process
|
||||||
|
provides = self.class.provides
|
||||||
|
raise "Can't find any provides for #{self.class.name}" if provides.blank?
|
||||||
|
|
||||||
|
provides.each do |attribute|
|
||||||
|
state.provide(attribute, import_job.payload[attribute])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,35 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module ImportJob
|
||||||
|
module Statistics
|
||||||
|
class Store < Sequencer::Unit::Base
|
||||||
|
|
||||||
|
uses :import_job, :statistics
|
||||||
|
|
||||||
|
def process
|
||||||
|
# update the attribute temporarily so we can update it when:
|
||||||
|
# - the last update is more than 10 seconds in the past
|
||||||
|
# - all instances are processed but the last statistics entry is not written here.
|
||||||
|
# This will be done in the calling Unit of the executed sub sequence
|
||||||
|
import_job.result = statistics
|
||||||
|
|
||||||
|
return if !store?
|
||||||
|
import_job.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def store?
|
||||||
|
return true if import_job.updated_at.blank?
|
||||||
|
# update every 10 seconds to reduce DB load
|
||||||
|
import_job.updated_at > Time.zone.now + 10.seconds
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,50 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module ImportJob
|
||||||
|
module Statistics
|
||||||
|
class Update < Sequencer::Unit::Base
|
||||||
|
|
||||||
|
uses :statistics_diff
|
||||||
|
provides :statistics
|
||||||
|
|
||||||
|
def process
|
||||||
|
state.provide(:statistics) do
|
||||||
|
sum_deeply(
|
||||||
|
existing: statistics,
|
||||||
|
additions: statistics_diff
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# reset diff to avoid situations where old diff gets added multiple times
|
||||||
|
state.unset(:statistics_diff)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def statistics
|
||||||
|
import_job = state.optional(:import_job)
|
||||||
|
return {} if import_job.nil?
|
||||||
|
import_job.result
|
||||||
|
end
|
||||||
|
|
||||||
|
def sum_deeply(existing:, additions:)
|
||||||
|
existing.merge(additions) do |_key, oldval, newval|
|
||||||
|
if oldval.is_a?(Hash) || newval.is_a?(Hash)
|
||||||
|
sum_deeply(
|
||||||
|
existing: oldval,
|
||||||
|
additions: newval
|
||||||
|
)
|
||||||
|
else
|
||||||
|
oldval + newval
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,66 @@
|
||||||
|
require 'sequencer/mixin/sub_sequence'
|
||||||
|
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module ImportJob
|
||||||
|
module SubSequence
|
||||||
|
class General < Sequencer::Unit::Base
|
||||||
|
include ::Sequencer::Mixin::SubSequence
|
||||||
|
|
||||||
|
uses :import_job
|
||||||
|
|
||||||
|
def process
|
||||||
|
resource_sequence
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# INFO: Cache results via `@sequence ||= ...`, if needed
|
||||||
|
def sequence
|
||||||
|
raise "Missing implementation of '#{__method__}' method for '#{self.class.name}'"
|
||||||
|
end
|
||||||
|
|
||||||
|
# INFO: Cache results via `@resources ||= ...`, if needed
|
||||||
|
def resources
|
||||||
|
raise "Missing implementation of '#{__method__}' method for '#{self.class.name}'"
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_parameters
|
||||||
|
{
|
||||||
|
dry_run: import_job.dry_run,
|
||||||
|
import_job: import_job,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def resource_sequence
|
||||||
|
return if resources.blank?
|
||||||
|
|
||||||
|
defaults = default_parameters
|
||||||
|
|
||||||
|
resources.each do |resource|
|
||||||
|
|
||||||
|
arguments = {
|
||||||
|
parameters: defaults.merge(resource: resource)
|
||||||
|
}
|
||||||
|
|
||||||
|
yield resource, arguments if block_given?
|
||||||
|
|
||||||
|
arguments[:parameters][:resource] = arguments[:parameters][:resource].with_indifferent_access
|
||||||
|
|
||||||
|
result = sub_sequence(sequence, arguments)
|
||||||
|
|
||||||
|
processed(result)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def processed(_result)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,15 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module ImportJob
|
||||||
|
module SubSequence
|
||||||
|
class Resource < Sequencer::Unit::Import::Common::ImportJob::SubSequence::General
|
||||||
|
uses :resource
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
35
lib/sequencer/unit/import/common/mapping/flat_keys.rb
Normal file
35
lib/sequencer/unit/import/common/mapping/flat_keys.rb
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module Mapping
|
||||||
|
class FlatKeys < Sequencer::Unit::Base
|
||||||
|
include ::Sequencer::Unit::Import::Common::Mapping::Mixin::ProvideMapped
|
||||||
|
|
||||||
|
uses :resource
|
||||||
|
provides :mapped
|
||||||
|
|
||||||
|
def process
|
||||||
|
provide_mapped do
|
||||||
|
mapped
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def mapped
|
||||||
|
resource_with_indifferent_access = resource.with_indifferent_access
|
||||||
|
mapping.symbolize_keys.collect do |source, local|
|
||||||
|
[local, resource_with_indifferent_access[source]]
|
||||||
|
end.to_h.with_indifferent_access
|
||||||
|
end
|
||||||
|
|
||||||
|
def mapping
|
||||||
|
raise "Missing implementation of '#{__method__}' method for '#{self.class.name}'"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,30 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module Mapping
|
||||||
|
module Mixin
|
||||||
|
module ProvideMapped
|
||||||
|
|
||||||
|
def self.included(base)
|
||||||
|
base.provides :mapped
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def existing_mapped
|
||||||
|
@existing_mapped ||= state.optional(:mapped) || ActiveSupport::HashWithIndifferentAccess.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def provide_mapped
|
||||||
|
state.provide(:mapped) do
|
||||||
|
existing_mapped.merge(yield)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,59 @@
|
||||||
|
require 'sequencer/unit/import/common/model/mixin/handle_failure'
|
||||||
|
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module Model
|
||||||
|
module Associations
|
||||||
|
class Assign < Sequencer::Unit::Base
|
||||||
|
include ::Sequencer::Unit::Import::Common::Model::Mixin::HandleFailure
|
||||||
|
|
||||||
|
uses :instance, :associations, :instance_action, :dry_run
|
||||||
|
provides :instance_action
|
||||||
|
|
||||||
|
def process
|
||||||
|
return if dry_run
|
||||||
|
return if instance.blank?
|
||||||
|
|
||||||
|
instance.assign_attributes(associations)
|
||||||
|
|
||||||
|
# execute associations check only if needed for performance reasons
|
||||||
|
return if instance_action != :unchanged
|
||||||
|
return if !changed?
|
||||||
|
state.provide(:instance_action, :changed)
|
||||||
|
rescue => e
|
||||||
|
handle_failure(e)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def changed?
|
||||||
|
logger.debug("Changed instance associations: #{changes.inspect}")
|
||||||
|
changes.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def changes
|
||||||
|
@changes ||= begin
|
||||||
|
return {} if associations.blank?
|
||||||
|
associations.collect do |association, value|
|
||||||
|
before = compareable(instance.send(association))
|
||||||
|
after = compareable(value)
|
||||||
|
next if before == after
|
||||||
|
[association, [before, after]]
|
||||||
|
end.compact.to_h.with_indifferent_access
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def compareable(value)
|
||||||
|
return nil if value.blank?
|
||||||
|
return value.sort if value.respond_to(:sort)
|
||||||
|
value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,65 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module Model
|
||||||
|
module Associations
|
||||||
|
class Extract < Sequencer::Unit::Base
|
||||||
|
|
||||||
|
uses :model_class, :mapped
|
||||||
|
provides :associations
|
||||||
|
|
||||||
|
def process
|
||||||
|
state.provide(:associations) do
|
||||||
|
associations.collect do |association|
|
||||||
|
next if !mapped.key?(association)
|
||||||
|
|
||||||
|
# remove from the mapped values if it's an association
|
||||||
|
value = mapped.delete(association)
|
||||||
|
|
||||||
|
# skip if we don't track them
|
||||||
|
next if tracked_associations.exclude?(association)
|
||||||
|
|
||||||
|
[association, value]
|
||||||
|
end.compact.to_h
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def associations
|
||||||
|
@associations ||= begin
|
||||||
|
associations = []
|
||||||
|
# loop over all reflections
|
||||||
|
model_class.reflect_on_all_associations.each do |reflection|
|
||||||
|
|
||||||
|
# refection name is something like groups or organization (singular/plural)
|
||||||
|
associations.push(reflection.name)
|
||||||
|
|
||||||
|
# key is something like group_id or organization_id (singular)
|
||||||
|
key = reflection.klass.name.foreign_key
|
||||||
|
|
||||||
|
# add trailing 's' to get pluralized key
|
||||||
|
reflection_name = reflection.name.to_s
|
||||||
|
if reflection_name.singularize == reflection_name
|
||||||
|
key = "#{key}s"
|
||||||
|
end
|
||||||
|
|
||||||
|
# store _id/_ids name
|
||||||
|
associations.push(key.to_sym)
|
||||||
|
end
|
||||||
|
associations
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def tracked_associations
|
||||||
|
# track all associations by default
|
||||||
|
associations
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,24 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module Model
|
||||||
|
module Attributes
|
||||||
|
class AddByIds < Sequencer::Unit::Base
|
||||||
|
include ::Sequencer::Unit::Import::Common::Mapping::Mixin::ProvideMapped
|
||||||
|
|
||||||
|
def process
|
||||||
|
provide_mapped do
|
||||||
|
{
|
||||||
|
created_by_id: 1,
|
||||||
|
updated_by_id: 1,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,32 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module Model
|
||||||
|
module Attributes
|
||||||
|
class CheckMandatory < Sequencer::Unit::Base
|
||||||
|
prepend ::Sequencer::Unit::Import::Common::Model::Mixin::SkipOnProvidedInstanceAction
|
||||||
|
|
||||||
|
uses :mapped
|
||||||
|
provides :instance_action
|
||||||
|
|
||||||
|
def process
|
||||||
|
mandatory.each do |mapped_attribute|
|
||||||
|
next if mapped[mapped_attribute].present?
|
||||||
|
state.provide(:instance_action, :skipped)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def mandatory
|
||||||
|
raise "Missing implementation of '#{__method__}' method for '#{self.class.name}'"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,32 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module Model
|
||||||
|
module Attributes
|
||||||
|
class RemoteId < Sequencer::Unit::Base
|
||||||
|
include ::Sequencer::Unit::Import::Common::Model::Mixin::HandleFailure
|
||||||
|
|
||||||
|
uses :resource
|
||||||
|
provides :remote_id
|
||||||
|
|
||||||
|
def process
|
||||||
|
state.provide(:remote_id) do
|
||||||
|
resource.fetch(attribute)
|
||||||
|
end
|
||||||
|
rescue KeyError => e
|
||||||
|
handle_failure(e)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def attribute
|
||||||
|
:id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
25
lib/sequencer/unit/import/common/model/create.rb
Normal file
25
lib/sequencer/unit/import/common/model/create.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module Model
|
||||||
|
class Create < Sequencer::Unit::Base
|
||||||
|
include ::Sequencer::Unit::Import::Common::Model::Mixin::HandleFailure
|
||||||
|
prepend ::Sequencer::Unit::Import::Common::Model::Mixin::SkipOnProvidedInstanceAction
|
||||||
|
|
||||||
|
uses :mapped, :model_class
|
||||||
|
provides :instance, :instance_action
|
||||||
|
|
||||||
|
def process
|
||||||
|
instance = model_class.new(mapped)
|
||||||
|
state.provide(:instance, instance)
|
||||||
|
state.provide(:instance_action, :created)
|
||||||
|
rescue => e
|
||||||
|
handle_failure(e)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,29 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module Model
|
||||||
|
module ExternalSync
|
||||||
|
class Create < Sequencer::Unit::Base
|
||||||
|
prepend ::Sequencer::Unit::Import::Common::Model::Mixin::SkipOnSkippedInstance
|
||||||
|
|
||||||
|
uses :instance, :instance_action, :remote_id, :dry_run, :external_sync_source, :model_class
|
||||||
|
|
||||||
|
def process
|
||||||
|
return if dry_run
|
||||||
|
return if instance_action != :created
|
||||||
|
|
||||||
|
::ExternalSync.create(
|
||||||
|
source: external_sync_source,
|
||||||
|
source_id: remote_id,
|
||||||
|
object: model_class.name,
|
||||||
|
o_id: instance.id
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,63 @@
|
||||||
|
require 'sequencer/unit/import/common/model/mixin/handle_failure'
|
||||||
|
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module Model
|
||||||
|
module ExternalSync
|
||||||
|
class Local < Sequencer::Unit::Base
|
||||||
|
include ::Sequencer::Unit::Import::Common::Model::Mixin::HandleFailure
|
||||||
|
prepend ::Sequencer::Unit::Import::Common::Model::Mixin::SkipOnSkippedInstance
|
||||||
|
|
||||||
|
uses :mapped, :remote_id, :model_class, :external_sync_source, :instance_action
|
||||||
|
provides :instance
|
||||||
|
|
||||||
|
def process
|
||||||
|
return if state.provided?(:instance)
|
||||||
|
|
||||||
|
return if value.blank?
|
||||||
|
return if instance.blank?
|
||||||
|
|
||||||
|
create_external_sync
|
||||||
|
|
||||||
|
state.provide(:instance, instance)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def attribute
|
||||||
|
raise "Missing implementation of '#{__method__}' method for '#{self.class.name}'"
|
||||||
|
end
|
||||||
|
|
||||||
|
def value
|
||||||
|
mapped[attribute]
|
||||||
|
end
|
||||||
|
|
||||||
|
def instance
|
||||||
|
@instance ||= begin
|
||||||
|
model_class.where(attribute => value).find do |local|
|
||||||
|
!ExternalSync.exists?(
|
||||||
|
source: external_sync_source,
|
||||||
|
object: model_class.name,
|
||||||
|
o_id: local.id
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_external_sync
|
||||||
|
ExternalSync.create(
|
||||||
|
source: external_sync_source,
|
||||||
|
source_id: remote_id,
|
||||||
|
object: import_class.name,
|
||||||
|
o_id: instance.id
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,34 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module Model
|
||||||
|
module ExternalSync
|
||||||
|
class Lookup < Sequencer::Unit::Base
|
||||||
|
include ::Sequencer::Unit::Import::Common::Model::Mixin::HandleFailure
|
||||||
|
prepend ::Sequencer::Unit::Import::Common::Model::Mixin::SkipOnSkippedInstance
|
||||||
|
|
||||||
|
uses :remote_id, :model_class, :external_sync_source
|
||||||
|
provides :instance
|
||||||
|
|
||||||
|
def process
|
||||||
|
synced_instance = ::ExternalSync.find_by(
|
||||||
|
source: external_sync_source,
|
||||||
|
source_id: remote_id,
|
||||||
|
object: model_class.name,
|
||||||
|
)
|
||||||
|
return if !synced_instance
|
||||||
|
|
||||||
|
state.provide(:instance) do
|
||||||
|
model_class.find(synced_instance.o_id)
|
||||||
|
end
|
||||||
|
rescue => e
|
||||||
|
handle_failure(e)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
50
lib/sequencer/unit/import/common/model/http_log.rb
Normal file
50
lib/sequencer/unit/import/common/model/http_log.rb
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module Model
|
||||||
|
class HttpLog < Sequencer::Unit::Base
|
||||||
|
|
||||||
|
uses :dry_run, :instance_action, :remote_id, :mapped, :exception
|
||||||
|
|
||||||
|
def process
|
||||||
|
return if dry_run
|
||||||
|
::HttpLog.create(
|
||||||
|
direction: 'out',
|
||||||
|
facility: facility,
|
||||||
|
method: 'tcp',
|
||||||
|
url: "#{instance_action} -> #{remote_id}",
|
||||||
|
status: status,
|
||||||
|
ip: nil,
|
||||||
|
request: {
|
||||||
|
content: mapped,
|
||||||
|
},
|
||||||
|
response: {
|
||||||
|
message: response
|
||||||
|
},
|
||||||
|
created_by_id: 1,
|
||||||
|
updated_by_id: 1,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def status
|
||||||
|
@status ||= begin
|
||||||
|
instance_action == :failed ? :failed : :success
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def response
|
||||||
|
exception ? exception.message : status
|
||||||
|
end
|
||||||
|
|
||||||
|
def facility
|
||||||
|
raise "Missing implementation of '#{__method__}' method for '#{self.class.name}'"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,24 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module Model
|
||||||
|
module Mixin
|
||||||
|
module HandleFailure
|
||||||
|
|
||||||
|
def self.included(base)
|
||||||
|
base.provides :exception, :instance_action
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_failure(e)
|
||||||
|
logger.error(e)
|
||||||
|
state.provide(:exception, e)
|
||||||
|
state.provide(:instance_action, :failed)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,22 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module Model
|
||||||
|
module Mixin
|
||||||
|
module SkipOnProvidedInstanceAction
|
||||||
|
|
||||||
|
def process
|
||||||
|
if state.provided?(:instance_action)
|
||||||
|
logger.debug("Skipping. Attribute 'instance_action' already provided.")
|
||||||
|
else
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,26 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module Model
|
||||||
|
module Mixin
|
||||||
|
module SkipOnSkippedInstance
|
||||||
|
|
||||||
|
def self.prepended(base)
|
||||||
|
base.uses :instance_action
|
||||||
|
end
|
||||||
|
|
||||||
|
def process
|
||||||
|
if instance_action == :skipped
|
||||||
|
logger.debug("Skipping. Attribute 'instance_action' is set to :skipped.")
|
||||||
|
else
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
25
lib/sequencer/unit/import/common/model/save.rb
Normal file
25
lib/sequencer/unit/import/common/model/save.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
require 'sequencer/unit/import/common/model/mixin/handle_failure'
|
||||||
|
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module Model
|
||||||
|
class Save < Sequencer::Unit::Base
|
||||||
|
include ::Sequencer::Unit::Import::Common::Model::Mixin::HandleFailure
|
||||||
|
|
||||||
|
uses :instance, :dry_run
|
||||||
|
|
||||||
|
def process
|
||||||
|
return if dry_run
|
||||||
|
return if instance.blank?
|
||||||
|
instance.save!
|
||||||
|
rescue => e
|
||||||
|
handle_failure(e)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
43
lib/sequencer/unit/import/common/model/skip/blank/base.rb
Normal file
43
lib/sequencer/unit/import/common/model/skip/blank/base.rb
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
require 'sequencer/unit/mixin/dynamic_attribute'
|
||||||
|
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module Model
|
||||||
|
module Skip
|
||||||
|
module Blank
|
||||||
|
class Base < Sequencer::Unit::Base
|
||||||
|
prepend ::Sequencer::Unit::Import::Common::Model::Mixin::SkipOnProvidedInstanceAction
|
||||||
|
include ::Sequencer::Unit::Mixin::DynamicAttribute
|
||||||
|
|
||||||
|
provides :instance_action
|
||||||
|
|
||||||
|
def process
|
||||||
|
return if !skip?
|
||||||
|
logger.debug("Skipping. Blank #{attribute} found: #{attribute_value.inspect}")
|
||||||
|
state.provide(:instance_action, :skipped)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def ignore
|
||||||
|
[:id]
|
||||||
|
end
|
||||||
|
|
||||||
|
def skip?
|
||||||
|
return true if attribute_value.blank?
|
||||||
|
relevant_blank?
|
||||||
|
end
|
||||||
|
|
||||||
|
def relevant_blank?
|
||||||
|
!attribute_value.except(*ignore).values.any?(&:present?)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
17
lib/sequencer/unit/import/common/model/skip/blank/mapped.rb
Normal file
17
lib/sequencer/unit/import/common/model/skip/blank/mapped.rb
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module Model
|
||||||
|
module Skip
|
||||||
|
module Blank
|
||||||
|
class Mapped < Sequencer::Unit::Import::Common::Model::Skip::Blank::Base
|
||||||
|
uses :mapped
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,17 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module Model
|
||||||
|
module Skip
|
||||||
|
module Blank
|
||||||
|
class Resource < Sequencer::Unit::Import::Common::Model::Skip::Blank::Base
|
||||||
|
uses :resource
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,46 @@
|
||||||
|
require 'sequencer/unit/mixin/dynamic_attribute'
|
||||||
|
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module Model
|
||||||
|
module Skip
|
||||||
|
module MissingMandatory
|
||||||
|
class Base < Sequencer::Unit::Base
|
||||||
|
prepend ::Sequencer::Unit::Import::Common::Model::Mixin::SkipOnProvidedInstanceAction
|
||||||
|
include ::Sequencer::Unit::Mixin::DynamicAttribute
|
||||||
|
|
||||||
|
provides :instance_action
|
||||||
|
|
||||||
|
def process
|
||||||
|
return if !skip?
|
||||||
|
logger.debug("Skipping. Missing mandatory attributes for #{attribute}: #{attribute_value.inspect}")
|
||||||
|
state.provide(:instance_action, :skipped)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def mandatory
|
||||||
|
raise "Missing implementation of '#{__method__}' method for '#{self.class.name}'"
|
||||||
|
end
|
||||||
|
|
||||||
|
def skip?
|
||||||
|
return true if attribute_value.blank?
|
||||||
|
mandatory_missing?
|
||||||
|
end
|
||||||
|
|
||||||
|
def mandatory_missing?
|
||||||
|
values = attribute_value.fetch_values(*mandatory)
|
||||||
|
!values.any?(&:present?)
|
||||||
|
rescue KeyError => e
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,17 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module Model
|
||||||
|
module Skip
|
||||||
|
module MissingMandatory
|
||||||
|
class Resource < Sequencer::Unit::Import::Common::Model::Skip::MissingMandatory::Base
|
||||||
|
uses :resource
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
21
lib/sequencer/unit/import/common/model/statistics/diff.rb
Normal file
21
lib/sequencer/unit/import/common/model/statistics/diff.rb
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
require 'sequencer/unit/import/common/model/statistics/mixin/diff'
|
||||||
|
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module Model
|
||||||
|
module Statistics
|
||||||
|
class Diff < Sequencer::Unit::Base
|
||||||
|
include ::Sequencer::Unit::Import::Common::Model::Statistics::Mixin::Diff
|
||||||
|
|
||||||
|
def process
|
||||||
|
state.provide(:statistics_diff, diff)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,47 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module Model
|
||||||
|
module Statistics
|
||||||
|
module Mixin
|
||||||
|
module Diff
|
||||||
|
|
||||||
|
def self.included(base)
|
||||||
|
base.uses :instance_action
|
||||||
|
base.provides :statistics_diff
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def actions
|
||||||
|
%i(skipped created updated unchanged failed deactivated)
|
||||||
|
end
|
||||||
|
|
||||||
|
def diff
|
||||||
|
raise "Unknown action '#{instance_action}'" if !possible?
|
||||||
|
defaults.merge(
|
||||||
|
instance_action => 1,
|
||||||
|
sum: 1,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def possible?
|
||||||
|
possible_actions.include?(instance_action)
|
||||||
|
end
|
||||||
|
|
||||||
|
def defaults
|
||||||
|
possible_actions.collect { |key| [key, 0] }.to_h
|
||||||
|
end
|
||||||
|
|
||||||
|
def possible_actions
|
||||||
|
@possible_actions ||= actions
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
56
lib/sequencer/unit/import/common/model/update.rb
Normal file
56
lib/sequencer/unit/import/common/model/update.rb
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module Model
|
||||||
|
class Update < Sequencer::Unit::Base
|
||||||
|
include ::Sequencer::Unit::Import::Common::Model::Mixin::HandleFailure
|
||||||
|
prepend ::Sequencer::Unit::Import::Common::Model::Mixin::SkipOnProvidedInstanceAction
|
||||||
|
|
||||||
|
uses :instance, :mapped
|
||||||
|
provides :instance_action
|
||||||
|
|
||||||
|
def process
|
||||||
|
# check if no instance is given - so we can't update it
|
||||||
|
return if !instance
|
||||||
|
|
||||||
|
# lock the current instance for write access
|
||||||
|
instance.with_lock do
|
||||||
|
# delete since we have an update and
|
||||||
|
# the record is already created
|
||||||
|
mapped.delete(:created_by_id)
|
||||||
|
|
||||||
|
# assign regular attributes
|
||||||
|
instance.assign_attributes(mapped)
|
||||||
|
|
||||||
|
action = changed? ? :updated : :unchanged
|
||||||
|
state.provide(:instance_action, action)
|
||||||
|
end
|
||||||
|
rescue => e
|
||||||
|
handle_failure(e)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def changed?
|
||||||
|
logger.debug("Changed instance attributes: #{changes.inspect}")
|
||||||
|
changes.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def changes
|
||||||
|
@changes ||= begin
|
||||||
|
if instance.changed?
|
||||||
|
# dry run
|
||||||
|
instance.changes
|
||||||
|
else
|
||||||
|
# live run
|
||||||
|
instance.previous_changes
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
24
lib/sequencer/unit/import/common/user/attributes/downcase.rb
Normal file
24
lib/sequencer/unit/import/common/user/attributes/downcase.rb
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module User
|
||||||
|
module Attributes
|
||||||
|
class Downcase < Sequencer::Unit::Base
|
||||||
|
prepend ::Sequencer::Unit::Import::Common::Model::Mixin::SkipOnSkippedInstance
|
||||||
|
|
||||||
|
uses :mapped
|
||||||
|
|
||||||
|
def process
|
||||||
|
%i(login email).each do |attribute|
|
||||||
|
next if mapped[attribute].blank?
|
||||||
|
mapped[attribute].downcase!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,45 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Common
|
||||||
|
module User
|
||||||
|
module Email
|
||||||
|
class CheckValidity < Sequencer::Unit::Base
|
||||||
|
prepend ::Sequencer::Unit::Import::Common::Model::Mixin::SkipOnSkippedInstance
|
||||||
|
|
||||||
|
uses :mapped
|
||||||
|
|
||||||
|
def process
|
||||||
|
return if mapped[:email].blank?
|
||||||
|
|
||||||
|
# TODO: This should be done totally somewhere central
|
||||||
|
mapped[:email] = ensure_valid_email(mapped[:email])
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def ensure_valid_email(source)
|
||||||
|
# TODO: should get unified with User#check_email
|
||||||
|
email = extract_email(source)
|
||||||
|
return if !email
|
||||||
|
email.downcase
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_email(source)
|
||||||
|
# Support format like "Bob Smith (bob@example.com)"
|
||||||
|
if source =~ /\((.+@.+)\)/
|
||||||
|
source = $1
|
||||||
|
end
|
||||||
|
|
||||||
|
Mail::Address.new(source).address
|
||||||
|
rescue
|
||||||
|
return source if source !~ /<\s*([^>]+)/
|
||||||
|
$1.strip
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
32
lib/sequencer/unit/import/exchange/attribute_examples.rb
Normal file
32
lib/sequencer/unit/import/exchange/attribute_examples.rb
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
require 'sequencer/mixin/exchange/folder'
|
||||||
|
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Exchange
|
||||||
|
class AttributeExamples < Sequencer::Unit::Base
|
||||||
|
include ::Sequencer::Mixin::Exchange::Folder
|
||||||
|
|
||||||
|
uses :ews_folder_ids
|
||||||
|
provides :ews_attributes_examples
|
||||||
|
|
||||||
|
def process
|
||||||
|
state.provide(:ews_attributes_examples) do
|
||||||
|
::Import::Helper::AttributesExamples.new do |extractor|
|
||||||
|
|
||||||
|
ews_folder_ids.collect do |folder_id|
|
||||||
|
|
||||||
|
ews_folder.find(folder_id).items.each do |resource|
|
||||||
|
attributes = ::Import::Exchange::ItemAttributes.extract(resource)
|
||||||
|
extractor.extract(attributes)
|
||||||
|
break if extractor.enough
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end.examples
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,18 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Exchange
|
||||||
|
module AttributeMapper
|
||||||
|
class AttributeExamples < Sequencer::Unit::Common::AttributeMapper
|
||||||
|
|
||||||
|
def self.map
|
||||||
|
{
|
||||||
|
ews_attributes_examples: :attributes,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,18 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Exchange
|
||||||
|
module AttributeMapper
|
||||||
|
class AvailableFolders < Sequencer::Unit::Common::AttributeMapper
|
||||||
|
|
||||||
|
def self.map
|
||||||
|
{
|
||||||
|
ews_folder_id_path_map: :folders,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,18 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Exchange
|
||||||
|
module FolderContact
|
||||||
|
class HttpLog < Import::Common::Model::HttpLog
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def facility
|
||||||
|
'EWS'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
27
lib/sequencer/unit/import/exchange/folder_contact/mapping.rb
Normal file
27
lib/sequencer/unit/import/exchange/folder_contact/mapping.rb
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Exchange
|
||||||
|
module FolderContact
|
||||||
|
class Mapping < Sequencer::Unit::Import::Common::Mapping::FlatKeys
|
||||||
|
|
||||||
|
uses :import_job
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def mapping
|
||||||
|
from_import_job || ::Import::Exchange.config[:attributes]
|
||||||
|
end
|
||||||
|
|
||||||
|
def from_import_job
|
||||||
|
return if !state.provided?(:import_job)
|
||||||
|
payload = import_job.payload
|
||||||
|
return if payload.blank?
|
||||||
|
payload[:ews_attributes]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,17 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Exchange
|
||||||
|
module FolderContact
|
||||||
|
class RemoteId < Sequencer::Unit::Import::Common::Model::Attributes::RemoteId
|
||||||
|
private
|
||||||
|
|
||||||
|
def attribute
|
||||||
|
:item_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,19 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Exchange
|
||||||
|
module FolderContact
|
||||||
|
class StaticAttributes < Sequencer::Unit::Base
|
||||||
|
|
||||||
|
provides :model_class, :external_sync_source
|
||||||
|
|
||||||
|
def process
|
||||||
|
state.provide(:model_class, ::User)
|
||||||
|
state.provide(:external_sync_source, 'EWS::FolderContact')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,39 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Exchange
|
||||||
|
module FolderContact
|
||||||
|
module Statistics
|
||||||
|
class Diff < Sequencer::Unit::Base
|
||||||
|
include ::Sequencer::Unit::Import::Common::Model::Statistics::Mixin::Diff
|
||||||
|
|
||||||
|
uses :ews_folder_name
|
||||||
|
|
||||||
|
def process
|
||||||
|
state.provide(:statistics_diff) do
|
||||||
|
# remove :sum since it's already set via
|
||||||
|
# the exchange item attribte
|
||||||
|
result = diff.except(:sum)
|
||||||
|
|
||||||
|
# build structure for a general diff
|
||||||
|
# and a folder specific sub structure
|
||||||
|
result.merge(
|
||||||
|
folders: {
|
||||||
|
ews_folder_name => result
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def actions
|
||||||
|
%i(created updated unchanged skipped failed)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,14 @@
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Exchange
|
||||||
|
module FolderContacts
|
||||||
|
class DryRunPayload < Sequencer::Unit::Import::Common::ImportJob::Payload::ToState
|
||||||
|
|
||||||
|
provides :ews_config, :ews_folder_ids
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,27 @@
|
||||||
|
require 'sequencer/mixin/exchange/folder'
|
||||||
|
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Exchange
|
||||||
|
module FolderContacts
|
||||||
|
class FolderIds < Sequencer::Unit::Base
|
||||||
|
include ::Sequencer::Mixin::Exchange::Folder
|
||||||
|
|
||||||
|
provides :ews_folder_ids
|
||||||
|
|
||||||
|
def process
|
||||||
|
# check if ids are already processed
|
||||||
|
return if state.provided?(:ews_folder_ids)
|
||||||
|
|
||||||
|
state.provide(:ews_folder_ids) do
|
||||||
|
config = ::Import::Exchange.config
|
||||||
|
config[:folders]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,37 @@
|
||||||
|
require 'sequencer/mixin/exchange/folder'
|
||||||
|
require 'sequencer/mixin/import_job/resource_loop'
|
||||||
|
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Exchange
|
||||||
|
module FolderContacts
|
||||||
|
class SubSequence < Sequencer::Unit::Base
|
||||||
|
include ::Sequencer::Mixin::Exchange::Folder
|
||||||
|
include ::Sequencer::Mixin::ImportJob::ResourceLoop
|
||||||
|
|
||||||
|
uses :ews_folder_ids, :import_job
|
||||||
|
|
||||||
|
def process
|
||||||
|
|
||||||
|
ews_folder_ids.each do |folder_id|
|
||||||
|
folder = ews_folder.find(folder_id)
|
||||||
|
display_path = ews_folder.display_path(folder)
|
||||||
|
|
||||||
|
resource_sequence('Import::Exchange::FolderContact', folder.items) do |item|
|
||||||
|
|
||||||
|
logger.debug("Extracting attributes from Exchange item: #{item.get_all_properties!.inspect}")
|
||||||
|
|
||||||
|
{
|
||||||
|
resource: ::Import::Exchange::ItemAttributes.extract(item),
|
||||||
|
ews_folder_name: display_path,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
48
lib/sequencer/unit/import/exchange/folder_contacts/sum.rb
Normal file
48
lib/sequencer/unit/import/exchange/folder_contacts/sum.rb
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
require 'sequencer/mixin/exchange/folder'
|
||||||
|
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Import
|
||||||
|
module Exchange
|
||||||
|
module FolderContacts
|
||||||
|
class Sum < Sequencer::Unit::Base
|
||||||
|
include ::Sequencer::Mixin::Exchange::Folder
|
||||||
|
|
||||||
|
uses :ews_folder_ids
|
||||||
|
provides :statistics_diff
|
||||||
|
|
||||||
|
def process
|
||||||
|
state.provide(:statistics_diff, diff)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def diff
|
||||||
|
result = {
|
||||||
|
sum: 0,
|
||||||
|
}
|
||||||
|
folder_sum_map.each do |display_path, sum|
|
||||||
|
|
||||||
|
result[display_path] = {
|
||||||
|
sum: sum
|
||||||
|
}
|
||||||
|
|
||||||
|
result[:sum] += sum
|
||||||
|
end
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
def folder_sum_map
|
||||||
|
ews_folder_ids.collect do |folder_id|
|
||||||
|
folder = ews_folder.find(folder_id)
|
||||||
|
display_path = ews_folder.display_path(folder)
|
||||||
|
|
||||||
|
[display_path, folder.total_count]
|
||||||
|
end.to_h
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
35
lib/sequencer/unit/mixin/dynamic_attribute.rb
Normal file
35
lib/sequencer/unit/mixin/dynamic_attribute.rb
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# rubocop:disable Lint/NestedMethodDefinition
|
||||||
|
class Sequencer
|
||||||
|
class Unit
|
||||||
|
module Mixin
|
||||||
|
module DynamicAttribute
|
||||||
|
|
||||||
|
def self.included(base)
|
||||||
|
|
||||||
|
class << base
|
||||||
|
|
||||||
|
def inherited(base)
|
||||||
|
base.extend(Forwardable)
|
||||||
|
base.instance_delegate [:attribute] => base
|
||||||
|
end
|
||||||
|
|
||||||
|
def attribute
|
||||||
|
@attribute ||= begin
|
||||||
|
if uses.size != 1
|
||||||
|
raise "DynamicAttribute classes can use exactly one attribute. Found #{uses.size}."
|
||||||
|
end
|
||||||
|
uses.first
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def attribute_value
|
||||||
|
@attribute_value ||= state.use(attribute)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
70
lib/sequencer/units.rb
Normal file
70
lib/sequencer/units.rb
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
require 'mixin/instance_wrapper'
|
||||||
|
|
||||||
|
class Sequencer
|
||||||
|
class Units
|
||||||
|
include ::Enumerable
|
||||||
|
include ::Mixin::InstanceWrapper
|
||||||
|
|
||||||
|
wrap :@units
|
||||||
|
|
||||||
|
# Initializes a new Sequencer::Units instance with the given Units Array.
|
||||||
|
#
|
||||||
|
# @param [*Array<String>] *units a list of Units with or without the Sequencer::Unit prefix.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# Sequencer::Units.new('Example::Unit', 'Sequencer::Unit::Second::Unit')
|
||||||
|
def initialize(*units)
|
||||||
|
@units = units
|
||||||
|
end
|
||||||
|
|
||||||
|
# Required #each implementation for ::Enumerable functionality. Constantizes
|
||||||
|
# the list of units given when initialized.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# units.each do |unit|
|
||||||
|
# unit.process(sequencer)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# @return [nil]
|
||||||
|
def each
|
||||||
|
@units.each do |unit|
|
||||||
|
yield constantize(unit)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Provides an Array of :uses and :provides declarations for each Unit.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# units.declarations
|
||||||
|
# #=> [{uses: [:question], provides: [:answer], ...}]
|
||||||
|
#
|
||||||
|
# @return [Array<Hash{Symbol => Array<Symbol>}>] the declarations of the Units
|
||||||
|
def declarations
|
||||||
|
collect do |unit|
|
||||||
|
{
|
||||||
|
uses: unit.uses,
|
||||||
|
provides: unit.provides,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Enables the access to an Unit class via index.
|
||||||
|
#
|
||||||
|
# @param [Integer] index the index for the requested Unit class.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# units[1]
|
||||||
|
# #=> Sequencer::Unit::Example
|
||||||
|
#
|
||||||
|
# @return [Object] the Unit class for the requested index
|
||||||
|
def [](index)
|
||||||
|
constantize(@units[index])
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def constantize(unit)
|
||||||
|
Sequencer::Unit.constantize(unit)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
30
lib/sequencer/units/attribute.rb
Normal file
30
lib/sequencer/units/attribute.rb
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
class Sequencer
|
||||||
|
class Units
|
||||||
|
class Attribute
|
||||||
|
|
||||||
|
attr_accessor :from, :to
|
||||||
|
|
||||||
|
# Checks if the attribute will be provided by one or more Units.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# attribute.will_be_provided?
|
||||||
|
# # => true
|
||||||
|
#
|
||||||
|
# @return [Boolean]
|
||||||
|
def will_be_provided?
|
||||||
|
!from.nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Checks if the attribute will be used by one or more Units.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# attribute.will_be_used?
|
||||||
|
# # => true
|
||||||
|
#
|
||||||
|
# @return [Boolean]
|
||||||
|
def will_be_used?
|
||||||
|
!to.nil?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
87
lib/sequencer/units/attributes.rb
Normal file
87
lib/sequencer/units/attributes.rb
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
require 'mixin/instance_wrapper'
|
||||||
|
|
||||||
|
class Sequencer
|
||||||
|
class Units
|
||||||
|
class Attributes
|
||||||
|
include ::Mixin::InstanceWrapper
|
||||||
|
|
||||||
|
wrap :@attributes
|
||||||
|
|
||||||
|
# Initializes the lifespan store for the attributes
|
||||||
|
# of the given Units declarations.
|
||||||
|
#
|
||||||
|
# @param [Array<Hash{Symbol => Array<:Symbol>}>] declarations the list of Unit declarations.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# declarations = [{uses: [:attribute1, ...], provides: [:result], ...}]
|
||||||
|
# attributes = Sequencer::Units::Attributes(declarations)
|
||||||
|
#
|
||||||
|
# @return [nil]
|
||||||
|
def initialize(declarations)
|
||||||
|
@declarations = declarations
|
||||||
|
|
||||||
|
initialize_attributes
|
||||||
|
initialize_lifespan
|
||||||
|
end
|
||||||
|
|
||||||
|
# Lists all `provides` declarations of the Units the instance was initialized with.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# attributes.provided
|
||||||
|
# # => [:result, ...]
|
||||||
|
#
|
||||||
|
# @return [Array<Symbol>] the list of all `provides` declarations
|
||||||
|
def provided
|
||||||
|
select do |_attribute, instance|
|
||||||
|
instance.will_be_provided?
|
||||||
|
end.keys
|
||||||
|
end
|
||||||
|
|
||||||
|
# Lists all `uses` declarations of the Units the instance was initialized with.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# attributes.used
|
||||||
|
# # => [:attribute1, ...]
|
||||||
|
#
|
||||||
|
# @return [Array<Symbol>] the list of all `uses` declarations
|
||||||
|
def used
|
||||||
|
select do |_attribute, instance|
|
||||||
|
instance.will_be_used?
|
||||||
|
end.keys
|
||||||
|
end
|
||||||
|
|
||||||
|
# Checks if the given attribute is known in the list of Unit declarations.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# attributes.known?(:attribute2)
|
||||||
|
# # => false
|
||||||
|
#
|
||||||
|
# @return [Boolean]
|
||||||
|
def known?(attribute)
|
||||||
|
key?(attribute)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def initialize_attributes
|
||||||
|
@attributes = Hash.new do |hash, key|
|
||||||
|
hash[key] = Sequencer::Units::Attribute.new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize_lifespan
|
||||||
|
@declarations.each_with_index do |unit, index|
|
||||||
|
|
||||||
|
unit[:uses].try(:each) do |attribute|
|
||||||
|
self[attribute].to = index
|
||||||
|
end
|
||||||
|
|
||||||
|
unit[:provides].try(:each) do |attribute|
|
||||||
|
next if self[attribute].will_be_provided?
|
||||||
|
self[attribute].from = index
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,7 +1,5 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec::Matchers.define_negated_matcher :not_change, :change
|
|
||||||
|
|
||||||
RSpec.describe Import::BaseResource do
|
RSpec.describe Import::BaseResource do
|
||||||
|
|
||||||
it "needs an implementation of the 'import_class' method" do
|
it "needs an implementation of the 'import_class' method" do
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
require 'import/ldap/user'
|
require 'import/ldap/user'
|
||||||
|
|
||||||
RSpec::Matchers.define_negated_matcher :not_change, :change
|
|
||||||
|
|
||||||
RSpec.describe Import::Ldap::User do
|
RSpec.describe Import::Ldap::User do
|
||||||
|
|
||||||
let(:uid) { 'exampleuid' }
|
let(:uid) { 'exampleuid' }
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
require 'lib/import/transaction_factory_examples'
|
require 'lib/import/transaction_factory_examples'
|
||||||
|
|
||||||
RSpec::Matchers.define_negated_matcher :not_change, :change
|
|
||||||
|
|
||||||
RSpec.describe Import::OTRS::StateFactory do
|
RSpec.describe Import::OTRS::StateFactory do
|
||||||
it_behaves_like 'Import::TransactionFactory'
|
it_behaves_like 'Import::TransactionFactory'
|
||||||
|
|
||||||
|
|
42
spec/lib/sequencer/unit/common/attribute_mapper_spec.rb
Normal file
42
spec/lib/sequencer/unit/common/attribute_mapper_spec.rb
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Sequencer::Unit::Common::AttributeMapper, sequencer: :unit do
|
||||||
|
|
||||||
|
let(:map) {
|
||||||
|
{
|
||||||
|
old_key: :new_key,
|
||||||
|
second: :new_second,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it 'expects an implementation of the .map method' do
|
||||||
|
expect do
|
||||||
|
described_class.map
|
||||||
|
end.to raise_error(RuntimeError)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'declares uses from map keys' do
|
||||||
|
expect(described_class).to receive(:map).and_return(map)
|
||||||
|
expect(described_class.uses).to eq(map.keys)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'declares provides from map values' do
|
||||||
|
expect(described_class).to receive(:map).and_return(map)
|
||||||
|
expect(described_class.provides).to eq(map.values)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'maps as configured' do
|
||||||
|
|
||||||
|
old = {
|
||||||
|
old_key: :value,
|
||||||
|
second: :second_value,
|
||||||
|
}
|
||||||
|
|
||||||
|
allow(described_class).to receive(:map).and_return(map)
|
||||||
|
result = process(old)
|
||||||
|
|
||||||
|
expect(result.keys.size).to eq 2
|
||||||
|
expect(result[:new_key]).to eq old[:old_key]
|
||||||
|
expect(result[:new_second]).to eq old[:second]
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,38 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Sequencer::Unit::Import::Common::Mapping::FlatKeys, sequencer: :unit do
|
||||||
|
|
||||||
|
it 'raises an error if mapping method is not implemented' do
|
||||||
|
expect do
|
||||||
|
process(
|
||||||
|
resource: {
|
||||||
|
remote_attribute: 'value',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end.to raise_error(RuntimeError, /mapping/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'maps flat key structures' do
|
||||||
|
|
||||||
|
parameters = {
|
||||||
|
resource: {
|
||||||
|
remote_attribute: 'value',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mapping = {
|
||||||
|
remote_attribute: :local_attribute
|
||||||
|
}
|
||||||
|
|
||||||
|
provided = process(parameters) do |instance|
|
||||||
|
expect(instance).to receive(:mapping).and_return(mapping)
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(provided).to eq(
|
||||||
|
mapped: {
|
||||||
|
'local_attribute' => 'value',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expect(provided[:mapped]).to be_a(ActiveSupport::HashWithIndifferentAccess)
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,7 +1,5 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec::Matchers.define_negated_matcher :not_change, :change
|
|
||||||
|
|
||||||
RSpec.describe Scheduler do
|
RSpec.describe Scheduler do
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
|
1
spec/support/negated_matchers.rb
Normal file
1
spec/support/negated_matchers.rb
Normal file
|
@ -0,0 +1 @@
|
||||||
|
RSpec::Matchers.define_negated_matcher :not_change, :change
|
19
spec/support/sequencer.rb
Normal file
19
spec/support/sequencer.rb
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
module SequencerUnit
|
||||||
|
|
||||||
|
def process(parameters, &block)
|
||||||
|
Sequencer::Unit.process(described_class.name, parameters, &block)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module SequencerSequence
|
||||||
|
|
||||||
|
def process(parameters = {})
|
||||||
|
Sequencer.process(described_class.name,
|
||||||
|
parameters: parameters)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
RSpec.configure do |config|
|
||||||
|
config.include SequencerUnit, sequencer: :unit
|
||||||
|
config.include SequencerSequence, sequencer: :sequence
|
||||||
|
end
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue