Replaced old Zendesk import with refactored Sequencer based version 🚀.

This commit is contained in:
Thorsten Eckel 2018-01-08 16:29:34 +01:00
parent fc1b66d646
commit f0cf7c3605
181 changed files with 2567 additions and 3121 deletions

View file

@ -154,35 +154,35 @@ class Index extends App.ControllerContent
processData: true
success: (data, status, xhr) =>
if data.result is 'import_done'
window.location.reload()
return
if data.result is 'error'
@$('.js-error').removeClass('hide')
@$('.js-error').html(App.i18n.translateContent(data.message))
else
@$('.js-error').addClass('hide')
if data.message is 'not running' && @updateMigrationDisplayLoop > 16
if _.isEmpty(data.result) && @updateMigrationDisplayLoop > 16
@$('.js-error').removeClass('hide')
@$('.js-error').html(App.i18n.translateContent('Background process did not start or has not finished! Please contact your support.'))
return
if data.result is 'in_progress'
for key, item of data.data
if item.done > item.total
item.done = item.total
if !_.isEmpty(data.result['error'])
@$('.js-error').removeClass('hide')
@$('.js-error').html(App.i18n.translateContent(data.result['error']))
else
@$('.js-error').addClass('hide')
if key == 'Ticket' && item.total >= 1000
if !_.isEmpty(data.finished_at) && _.isEmpty(data.result['error'])
window.location.reload()
return
if !_.isEmpty(data.result)
for model, stats of data.result
if stats.sum > stats.total
stats.sum = stats.total
if model == 'Ticket' && stats.total >= 1000
@ticketCountInfo.removeClass('hide')
element = @$('.js-' + key.toLowerCase() )
element.find('.js-done').text(item.done)
element.find('.js-total').text(item.total)
element.find('progress').attr('max', item.total )
element.find('progress').attr('value', item.done )
if item.total <= item.done
element = @$('.js-' + model.toLowerCase() )
element.find('.js-done').text(stats.sum)
element.find('.js-total').text(stats.total)
element.find('progress').attr('max', stats.total )
element.find('progress').attr('value', stats.sum )
if stats.total <= stats.sum
element.addClass('is-done')
else
element.removeClass('is-done')

View file

@ -64,7 +64,7 @@
<div class="alert alert--info hide js-ticket-count-info" role="alert"><%- @T("There are more than 1000 tickets in the Zendesk system. Due to API rate limit restrictions we can't get the exact number of tickets yet and have to fetch them in batches of 1000. This might take some time, better grab a cup of coffee. The total number of tickets gets updated as soon as the currently known number is surpassed.") %></div>
<div class="wizard-body flex vertical justified">
<table class="progressTable">
<tr class="js-group">
<tr class="js-groups">
<td><span class="js-done">-</span>/<span class="js-total">-</span>
<td><span><%- @T('Groups') %></span>
<td class="progressTable-progressCell">
@ -73,7 +73,7 @@
<%- @Icon('checkmark') %>
</div>
</tr>
<tr class="js-organization">
<tr class="js-organizations">
<td><span class="js-done">-</span>/<span class="js-total">-</span>
<td><span><%- @T('Organizations') %></span>
<td class="progressTable-progressCell">
@ -82,7 +82,7 @@
<%- @Icon('checkmark') %>
</div>
</tr>
<tr class="js-user">
<tr class="js-users">
<td><span class="js-done">-</span>/<span class="js-total">-</span>
<td><span><%- @T('Users') %></span>
<td class="progressTable-progressCell">
@ -91,7 +91,7 @@
<%- @Icon('checkmark') %>
</div>
</tr>
<tr class="js-ticket">
<tr class="js-tickets">
<td><span class="js-done">-</span>/<span class="js-total">-</span>
<td><span><%- @T('Tickets') %></span>
<td class="progressTable-progressCell">

View file

@ -74,7 +74,9 @@ class ImportZendeskController < ApplicationController
Setting.set('import_zendesk_endpoint_username', params[:username])
Setting.set('import_zendesk_endpoint_key', params[:token])
if !Import::Zendesk.connection_test
result = Sequencer.process('Import::Zendesk::ConnectionTest')
if !result[:connected]
Setting.set('import_zendesk_endpoint_username', nil)
Setting.set('import_zendesk_endpoint_key', nil)
@ -96,8 +98,8 @@ class ImportZendeskController < ApplicationController
Setting.set('import_mode', true)
Setting.set('import_backend', 'zendesk')
# start migration
Import::Zendesk.delay.start_bg
job = ImportJob.create(name: 'Import::Zendesk')
job.delay.start
render json: {
result: 'ok',
@ -105,11 +107,13 @@ class ImportZendeskController < ApplicationController
end
def import_status
result = Import::Zendesk.status_bg
if result[:result] == 'import_done'
job = ImportJob.find_by(name: 'Import::Zendesk')
if job.finished_at.present?
Setting.reload
end
render json: result
model_show_render_item(job)
end
private

View file

@ -1,256 +0,0 @@
module Import
class BaseResource
include Import::Helper
attr_reader :resource, :errors
def initialize(resource, *args)
@action = :unknown
handle_args(resource, *args)
initialize_associations_states
import(resource, *args)
end
def import_class
raise NoMethodError, "#{self.class.name} has no implementation of the needed 'import_class' method"
end
def source
self.class.source
end
def remote_id(resource, *_args)
@remote_id ||= resource.delete(:id)
end
def action
return :failed if errors.present?
return :skipped if @resource.blank?
return :unchanged if !attributes_changed?
@action
end
def attributes_changed?
changed_attributes.present? || changed_associations.present?
end
def changed_attributes
return if @resource.blank?
# dry run
return @resource.changes_to_save if @resource.has_changes_to_save?
# live run
@resource.previous_changes
end
def changed_associations
changes = {}
tracked_associations.each do |association|
# skip if no new value will get assigned (no change is performed)
next if !@associations[:after].key?(association)
# skip if both values are equal
next if @associations[:before][association] == @associations[:after][association]
# skip if both values are blank
next if @associations[:before][association].blank? && @associations[:after][association].blank?
# store changes
changes[association] = [@associations[:before][association], @associations[:after][association]]
end
changes
end
def self.source
import_class_namespace
end
def self.import_class_namespace
@import_class_namespace ||= name.to_s.sub('Import::', '')
end
private
def initialize_associations_states
@associations = {}
%i[before after].each do |state|
@associations[state] ||= {}
end
end
def import(resource, *args)
create_or_update(map(resource, *args), *args)
rescue => e
# Don't catch own thrown exceptions from above
raise if e.is_a?(NoMethodError)
handle_error(e)
end
def create_or_update(resource, *args)
return if updated?(resource, *args)
create(resource, *args)
end
def updated?(resource, *args)
@resource = lookup_existing(resource, *args)
return false if !@resource
# lock the current resource for write access
@resource.with_lock do
# delete since we have an update and
# the record is already created
resource.delete(:created_by_id)
# store the current state of the associations
# from the resource hash because if we assign
# them to the instance some (e.g. has_many)
# will get stored even in the dry run :/
store_associations(:after, resource)
associations = tracked_associations
@resource.assign_attributes(resource.except(*associations))
# the return value here is kind of misleading
# and should not be trusted to indicate if a
# resource was actually updated.
# Use .action instead
return true if !attributes_changed?
@action = :updated
return true if @dry_run
@resource.assign_attributes(resource.slice(*associations))
@resource.save!
true
end
end
def lookup_existing(resource, *_args)
synced_instance = ExternalSync.find_by(
source: source,
source_id: remote_id(resource),
object: import_class.name,
)
return if !synced_instance
instance = import_class.find_by(id: synced_instance.o_id)
store_associations(:before, instance)
instance
end
def store_associations(state, source)
@associations[state] = associations_state(source)
end
def associations_state(source)
state = {}
tracked_associations.each do |association|
# we have to support instances and (resource) hashes
# here since in case of an update we only have the
# hash as a source but on create we have an instance
if source.is_a?(Hash)
# ignore if there is no key for the association
# of the Hash (update)
# otherwise wrong changes may get detected
next if !source.key?(association)
state[association] = source[association]
else
state[association] = source.send(association)
end
# sort arrays to avoid wrong change detection
next if !state[association].respond_to?(:sort!)
state[association].sort!
end
state
end
def tracked_associations
# loop over all reflections
import_class.reflect_on_all_associations.collect do |reflection|
# refection name is something like groups or organization (singular/plural)
reflection_name = reflection.name.to_s
# key is something like group_id or organization_id (singular)
key = reflection.klass.name.foreign_key
# add trailing 's' to get pluralized key
if reflection_name.singularize != reflection_name
key = "#{key}s"
end
key.to_sym
end
end
def create(resource, *_args)
@resource = import_class.new(resource)
store_associations(:after, @resource)
@action = :created
return if @dry_run
@resource.save!
external_sync_create(
local: @resource,
remote: resource,
)
end
def external_sync_create(local:, remote:)
ExternalSync.create(
source: source,
source_id: remote_id(remote),
object: import_class.name,
o_id: local.id
)
end
def defaults(_resource, *_args)
{
created_by_id: 1,
updated_by_id: 1,
}
end
def map(resource, *args)
mapped = from_mapping(resource, *args)
attributes = defaults(resource, *args).merge(mapped)
attributes.symbolize_keys
end
def from_mapping(resource, *args)
mapping = mapping(*args)
return resource if !mapping
ExternalSync.map(
mapping: mapping,
source: resource
)
end
def mapping(*args)
Setting.get(mapping_config(*args))
end
def mapping_config(*_args)
self.class.import_class_namespace.gsub('::', '_').underscore + '_mapping'
end
def handle_args(_resource, *args)
return if !args
return if !args.is_a?(Array)
return if args.blank?
last_arg = args.last
return if !last_arg.is_a?(Hash)
handle_modifiers(last_arg)
end
def handle_modifiers(modifiers)
@dry_run = modifiers.fetch(:dry_run, false)
end
def handle_error(e)
@errors ||= []
@errors.push(e)
Rails.logger.error e
end
end
end

View file

@ -1,68 +1,15 @@
require 'base64'
require 'zendesk_api'
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
module Import
end
module Import::Zendesk
extend Import::Helper
extend Import::Zendesk::Async
extend Import::Zendesk::ImportStats
class Zendesk < Import::Base
include Import::Mixin::Sequence
# rubocop:disable Style/ModuleFunction
extend self
def start
process
end
def start
log 'Start import...'
checks
Import::Zendesk::GroupFactory.import(client.groups)
Import::Zendesk::OrganizationFieldFactory.import(client.organization_fields)
Import::Zendesk::OrganizationFactory.import(client.organizations)
Import::Zendesk::UserFieldFactory.import(client.user_fields)
Import::Zendesk::UserFactory.import(client.users)
Import::Zendesk::TicketFieldFactory.import(client.ticket_fields)
Import::Zendesk::TicketFactory.import(all_tickets)
# TODO
Setting.set( 'system_init_done', true )
Setting.set( 'import_mode', false )
true
end
def connection_test
Import::Zendesk::Requester.connection_test
end
private
# this special ticket logic is needed since Zendesk archives tickets
# after 120 days and doesn't return them via the client.tickets
# endpoint as described here:
# https://github.com/zammad/zammad/issues/558#issuecomment-267951351
# the proper way is to use the 'incremental' endpoint which is not available
# via the ruby gem yet but a pull request is pending:
# https://github.com/zendesk/zendesk_api_client_rb/pull/287
# the following workaround is needed to use this functionality
def all_tickets
ZendeskAPI::Collection.new(
client,
ZendeskAPI::Ticket,
path: 'incremental/tickets?start_time=1'
)
end
def client
Import::Zendesk::Requester.client
end
def checks
check_import_mode
check_system_init_done
connection_test
def sequence_name
'Import::Zendesk::Full'
end
end
end

View file

@ -1,64 +0,0 @@
module Import
module Zendesk
module Async
# rubocop:disable Style/ModuleFunction
extend self
def start_bg
Setting.reload
Import::Zendesk.connection_test
# get statistic before starting import
statistic
# start thread to observe current state
status_update_thread = Thread.new do
loop do
result = {
data: current_state,
result: 'in_progress',
}
Cache.write('import:state', result, expires_in: 10.minutes)
sleep 8
end
end
sleep 2
# start import data
begin
Import::Zendesk.start
rescue => e
status_update_thread.exit
status_update_thread.join
Rails.logger.error e
result = {
message: e.message,
result: 'error',
}
Cache.write('import:state', result, expires_in: 10.hours)
return false
end
sleep 16 # wait until new finished import state is on client
status_update_thread.exit
status_update_thread.join
result = {
result: 'import_done',
}
Cache.write('import:state', result, expires_in: 10.hours)
Setting.set('system_init_done', true)
Setting.set('import_mode', false)
end
def status_bg
state = Cache.get('import:state')
return state if state
{
message: 'not running',
}
end
end
end
end

View file

@ -1,16 +0,0 @@
module Import
module Zendesk
module BaseFactory
include Import::Factory
# rubocop:disable Style/ModuleFunction
extend self
private
def import_loop(records, *_args, &import_block)
records.all!(&import_block)
end
end
end
end

View file

@ -1,21 +0,0 @@
module Import
module Zendesk
class Group
include Import::Helper
attr_reader :zendesk_id, :id
def initialize(group)
local_group = ::Group.create_if_not_exists(
name: group.name,
active: !group.deleted,
updated_by_id: 1,
created_by_id: 1
)
@zendesk_id = group.id
@id = local_group.id
end
end
end
end

View file

@ -1,8 +0,0 @@
module Import
module Zendesk
module GroupFactory
extend Import::Zendesk::BaseFactory
extend Import::Zendesk::LocalIDMapperHook
end
end
end

View file

@ -1,19 +0,0 @@
module Import
module Zendesk
module Helper
# rubocop:disable Style/ModuleFunction
extend self
private
def get_fields(zendesk_fields)
return {} if !zendesk_fields
fields = {}
zendesk_fields.each do |key, value|
fields[key] = value
end
fields
end
end
end
end

View file

@ -1,72 +0,0 @@
module Import
module Zendesk
module ImportStats
# rubocop:disable Style/ModuleFunction
extend self
def current_state
data = statistic
{
Group: {
done: ::Group.count,
total: data['Groups'] || 0,
},
Organization: {
done: ::Organization.count,
total: data['Organizations'] || 0,
},
User: {
done: ::User.count,
total: data['Users'] || 0,
},
Ticket: {
done: ::Ticket.count,
total: data['Tickets'] || 0,
},
}
end
def statistic
# check cache
cache = Cache.get('import_zendesk_stats')
return cache if cache
# retrive statistic
result = {
'Tickets' => 0,
'TicketFields' => 0,
'UserFields' => 0,
'OrganizationFields' => 0,
'Groups' => 0,
'Organizations' => 0,
'Users' => 0,
'GroupMemberships' => 0,
'Macros' => 0,
'Views' => 0,
'Automations' => 0,
}
result.each_key do |object|
result[ object ] = statistic_count(object)
end
Cache.write('import_zendesk_stats', result)
result
end
private
def statistic_count(object)
statistic_count_data(object).count!
end
def statistic_count_data(object)
return all_tickets if object == 'Tickets'
Import::Zendesk::Requester.client.send( object.underscore.to_sym )
end
end
end
end

View file

@ -1,25 +0,0 @@
module Import
module Zendesk
module LocalIDMapperHook
# rubocop:disable Style/ModuleFunction
extend self
def local_id(zendesk_id)
init_mapping
@zendesk_mapping[ zendesk_id ]
end
def post_import_hook(_record, backend_instance)
init_mapping
@zendesk_mapping[ backend_instance.zendesk_id ] = backend_instance.id
end
private
def init_mapping
@zendesk_mapping ||= {}
end
end
end
end

View file

@ -1,74 +0,0 @@
module Import
module Zendesk
class ObjectAttribute
def initialize(object, name, attribute)
initialize_data_option(attribute)
init_callback(attribute)
add(object, name, attribute)
end
private
def init_callback(_attribute); end
def add(object, name, attribute)
ObjectManager::Attribute.add( attribute_config(object, name, attribute) )
ObjectManager::Attribute.migration_execute(false)
rescue => e
# rubocop:disable Style/SpecialGlobalVars
raise $!, "Problem with ObjectManager Attribute '#{name}': #{$!}", $!.backtrace
end
def attribute_config(object, name, attribute)
{
object: object,
name: name,
display: attribute.title,
data_type: data_type(attribute),
data_option: @data_option,
editable: !attribute.removable,
active: attribute.active,
screens: screens(attribute),
position: attribute.position,
created_by_id: 1,
updated_by_id: 1,
}
end
def screens(attribute)
config = {
view: {
'-all-' => {
shown: true,
},
}
}
return config if !attribute.visible_in_portal && attribute.required_in_portal
{
edit: {
Customer: {
shown: attribute.visible_in_portal,
null: !attribute.required_in_portal,
},
}.merge(config)
}
end
def initialize_data_option(attribute)
@data_option = {
null: !attribute.required,
note: attribute.description,
}
end
def data_type(attribute)
attribute.type
end
end
end
end

View file

@ -0,0 +1,76 @@
module Import
class Zendesk
module ObjectAttribute
class Base
def initialize(object, name, attribute)
initialize_data_option(attribute)
init_callback(attribute)
add(object, name, attribute)
end
private
def init_callback(_attribute); end
def add(object, name, attribute)
ObjectManager::Attribute.add( attribute_config(object, name, attribute) )
ObjectManager::Attribute.migration_execute(false)
rescue => e
# rubocop:disable Style/SpecialGlobalVars
raise $!, "Problem with ObjectManager Attribute '#{name}': #{$!}", $!.backtrace
end
def attribute_config(object, name, attribute)
{
object: object,
name: name,
display: attribute.title,
data_type: data_type(attribute),
data_option: @data_option,
editable: !attribute.removable,
active: attribute.active,
screens: screens(attribute),
position: attribute.position,
created_by_id: 1,
updated_by_id: 1,
}
end
def screens(attribute)
config = {
view: {
'-all-' => {
shown: true,
},
}
}
return config if !attribute.visible_in_portal && attribute.required_in_portal
{
edit: {
Customer: {
shown: attribute.visible_in_portal,
null: !attribute.required_in_portal,
},
}.merge(config)
}
end
def initialize_data_option(attribute)
@data_option = {
null: !attribute.required,
note: attribute.description,
}
end
def data_type(attribute)
attribute.type
end
end
end
end
end

View file

@ -1,7 +1,13 @@
# this require is required (hehe) because of Rails autoloading
# which causes strange behavior not inheriting correctly
# from Import::OTRS::DynamicField
require 'import/zendesk/object_attribute/base'
module Import
module Zendesk
class ObjectAttribute
class Checkbox < Import::Zendesk::ObjectAttribute
class Zendesk
module ObjectAttribute
class Checkbox < Import::Zendesk::ObjectAttribute::Base
def init_callback(_object_attribte)
@data_option.merge!(
default: false,

View file

@ -1,12 +1,12 @@
# this require is required (hehe) because of Rails autoloading
# which causes strange behavior not inheriting correctly
# from Import::OTRS::DynamicField
require 'import/zendesk/object_attribute'
require 'import/zendesk/object_attribute/base'
module Import
module Zendesk
class ObjectAttribute
class Date < Import::Zendesk::ObjectAttribute
class Zendesk
module ObjectAttribute
class Date < Import::Zendesk::ObjectAttribute::Base
def init_callback(_object_attribte)
@data_option.merge!(
future: true,

View file

@ -1,6 +1,6 @@
module Import
module Zendesk
class ObjectAttribute
class Zendesk
module ObjectAttribute
class Decimal < Import::Zendesk::ObjectAttribute::Text
end
end

View file

@ -1,6 +1,6 @@
module Import
module Zendesk
class ObjectAttribute
class Zendesk
module ObjectAttribute
class Dropdown < Import::Zendesk::ObjectAttribute::Select
end
end

View file

@ -1,12 +1,13 @@
# this require is required (hehe) because of Rails autoloading
# which causes strange behavior not inheriting correctly
# from Import::OTRS::DynamicField
require 'import/zendesk/object_attribute'
require 'import/zendesk/object_attribute/base'
module Import
module Zendesk
class ObjectAttribute
class Integer < Import::Zendesk::ObjectAttribute
class Zendesk
module ObjectAttribute
class Integer < Import::Zendesk::ObjectAttribute::Base
def init_callback(_object_attribte)
@data_option.merge!(
min: 0,

View file

@ -1,12 +1,13 @@
# this require is required (hehe) because of Rails autoloading
# which causes strange behavior not inheriting correctly
# from Import::OTRS::DynamicField
require 'import/zendesk/object_attribute'
require 'import/zendesk/object_attribute/base'
module Import
module Zendesk
class ObjectAttribute
class Regexp < Import::Zendesk::ObjectAttribute
class Zendesk
module ObjectAttribute
class Regexp < Import::Zendesk::ObjectAttribute::Base
def init_callback(object_attribte)
@data_option.merge!(
type: 'text',

View file

@ -1,7 +1,13 @@
# this require is required (hehe) because of Rails autoloading
# which causes strange behavior not inheriting correctly
# from Import::OTRS::DynamicField
require 'import/zendesk/object_attribute/base'
module Import
module Zendesk
class ObjectAttribute
class Select < Import::Zendesk::ObjectAttribute
class Zendesk
module ObjectAttribute
class Select < Import::Zendesk::ObjectAttribute::Base
def init_callback(object_attribte)
@data_option.merge!(
default: '',

View file

@ -1,6 +1,6 @@
module Import
module Zendesk
class ObjectAttribute
class Zendesk
module ObjectAttribute
class Tagger < Import::Zendesk::ObjectAttribute::Select
end
end

View file

@ -1,7 +1,13 @@
# this require is required (hehe) because of Rails autoloading
# which causes strange behavior not inheriting correctly
# from Import::OTRS::DynamicField
require 'import/zendesk/object_attribute/base'
module Import
module Zendesk
class ObjectAttribute
class Text < Import::Zendesk::ObjectAttribute
class Zendesk
module ObjectAttribute
class Text < Import::Zendesk::ObjectAttribute::Base
def init_callback(_object_attribte)
@data_option.merge!(
type: 'text',

View file

@ -1,7 +1,13 @@
# this require is required (hehe) because of Rails autoloading
# which causes strange behavior not inheriting correctly
# from Import::OTRS::DynamicField
require 'import/zendesk/object_attribute/base'
module Import
module Zendesk
class ObjectAttribute
class Textarea < Import::Zendesk::ObjectAttribute
class Zendesk
module ObjectAttribute
class Textarea < Import::Zendesk::ObjectAttribute::Base
def init_callback(_object_attribte)
@data_option.merge!(
type: 'textarea',

View file

@ -1,38 +0,0 @@
module Import
module Zendesk
class ObjectField
attr_reader :zendesk_id, :id
def initialize(object_field)
import(object_field)
@zendesk_id = object_field.id
@id = local_name(object_field)
end
private
def local_name(object_field)
@local_name ||= remote_name(object_field).gsub(%r{[\s\/]}, '_').underscore.gsub(/_{2,}/, '_').gsub(/_id(s?)$/, '_no\1')
end
def remote_name(object_field)
object_field['key'] # TODO: y?!
end
def import(object_field)
backend_class(object_field).new(object_name, local_name(object_field), object_field)
end
def backend_class(object_field)
"Import::Zendesk::ObjectAttribute::#{object_field.type.capitalize}".constantize
end
def object_name
self.class.name.to_s.sub(/Import::Zendesk::/, '').sub(/Field/, '')
end
end
end
end

View file

@ -1,34 +0,0 @@
# https://developer.zendesk.com/rest_api/docs/core/organizations
module Import
module Zendesk
class Organization
include Import::Zendesk::Helper
attr_reader :zendesk_id, :id
def initialize(organization)
local_organization = ::Organization.create_if_not_exists(local_organization_fields(organization))
@zendesk_id = organization.id
@id = local_organization.id
end
private
def local_organization_fields(organization)
{
name: organization.name,
note: organization.note,
shared: organization.shared_tickets,
# shared: organization.shared_comments, # TODO, not yet implemented
# }.merge(organization.organization_fields) # TODO
updated_by_id: 1,
created_by_id: 1
}.merge(custom_fields(organization))
end
def custom_fields(organization)
get_fields(organization.organization_fields)
end
end
end
end

View file

@ -1,10 +0,0 @@
module Import
module Zendesk
module OrganizationFactory
# we need to loop over each instead of all!
# so we can use the default import factory here
extend Import::Factory
extend Import::Zendesk::LocalIDMapperHook
end
end
end

View file

@ -1,6 +0,0 @@
module Import
module Zendesk
class OrganizationField < Import::Zendesk::ObjectField
end
end
end

View file

@ -1,8 +0,0 @@
module Import
module Zendesk
module OrganizationFieldFactory
extend Import::Zendesk::BaseFactory
extend Import::Zendesk::LocalIDMapperHook
end
end
end

View file

@ -1,32 +0,0 @@
module Import
module Zendesk
class Priority
MAPPING = {
'low' => '1 low',
nil => '2 normal',
'normal' => '2 normal',
'high' => '3 high',
'urgent' => '3 high',
}.freeze
class << self
def lookup(ticket)
remote_priority = ticket.priority
@mapping ||= {}
if @mapping[ remote_priority ]
return @mapping[ remote_priority ]
end
@mapping[ remote_priority ] = ::Ticket::Priority.lookup( name: map(remote_priority) )
end
private
def map(priority)
MAPPING.fetch(priority, MAPPING[nil])
end
end
end
end
end

View file

@ -1,44 +0,0 @@
module Import
module Zendesk
module Requester
# rubocop:disable Style/ModuleFunction
extend self
def connection_test
# make sure to reinitialize client
# to react to config changes
initialize_client
return true if client.users.first
false
end
def client
return @client if @client
initialize_client
@client
end
private
def initialize_client
@client = ZendeskAPI::Client.new do |config|
config.url = Setting.get('import_zendesk_endpoint')
# Basic / Token Authentication
config.username = Setting.get('import_zendesk_endpoint_username')
config.token = Setting.get('import_zendesk_endpoint_key')
# when hitting the rate limit, sleep automatically,
# then retry the request.
config.retry = true
# disable cache to avoid unneeded memory consumption
# since we are using each object only once
# Inspired by: https://medium.com/swiftype-engineering/using-jmat-to-find-analyze-memory-in-jruby-1c4196c1ec72
config.cache = false
end
end
end
end
end

View file

@ -1,30 +0,0 @@
module Import
module Zendesk
class State
MAPPING = {
'pending' => 'pending reminder',
'solved' => 'closed',
'deleted' => 'removed',
}.freeze
class << self
def lookup(ticket)
remote_state = ticket.status
@mapping ||= {}
if @mapping[ remote_state ]
return @mapping[ remote_state ]
end
@mapping[ remote_state ] = ::Ticket::State.lookup( name: map( remote_state ) )
end
private
def map(state)
MAPPING.fetch(state, state)
end
end
end
end
end

View file

@ -1,74 +0,0 @@
# https://developer.zendesk.com/rest_api/docs/core/tickets
# https://developer.zendesk.com/rest_api/docs/core/ticket_comments#ticket-comments
# https://developer.zendesk.com/rest_api/docs/core/ticket_audits#the-via-object
# https://developer.zendesk.com/rest_api/docs/help_center/article_attachments
# https://developer.zendesk.com/rest_api/docs/core/ticket_audits # v2
module Import
module Zendesk
class Ticket
include Import::Helper
def initialize(ticket)
create_or_update(ticket)
Import::Zendesk::Ticket::TagFactory.import(ticket.tags, @local_ticket, ticket)
Import::Zendesk::Ticket::CommentFactory.import(ticket.comments, @local_ticket, ticket)
end
private
def create_or_update(ticket)
mapped_ticket = local_ticket_fields(ticket)
return if updated?(mapped_ticket)
create(mapped_ticket)
end
def updated?(ticket)
@local_ticket = ::Ticket.find_by(id: ticket[:id])
return false if !@local_ticket
@local_ticket.update!(ticket)
true
end
def create(ticket)
@local_ticket = ::Ticket.create(ticket)
reset_primary_key_sequence('tickets')
end
def local_ticket_fields(ticket)
local_user_id = Import::Zendesk::UserFactory.local_id( ticket.requester_id ) || 1
{
id: ticket.id,
title: ticket.subject || ticket.description || '-',
owner_id: Import::Zendesk::UserFactory.local_id( ticket.assignee ) || 1,
note: ticket.description,
group_id: Import::Zendesk::GroupFactory.local_id( ticket.group_id ) || 1,
customer_id: local_user_id,
organization_id: Import::Zendesk::OrganizationFactory.local_id( ticket.organization_id ),
priority: Import::Zendesk::Priority.lookup(ticket),
state: Import::Zendesk::State.lookup(ticket),
pending_time: ticket.due_at,
updated_at: ticket.updated_at,
created_at: ticket.created_at,
updated_by_id: local_user_id,
created_by_id: local_user_id,
create_article_sender_id: Import::Zendesk::Ticket::Comment::Sender.local_id(local_user_id),
create_article_type_id: Import::Zendesk::Ticket::Comment::Type.local_id(ticket),
}.merge(custom_fields(ticket))
end
def custom_fields(ticket)
custom_fields = ticket.custom_fields
fields = {}
return fields if !custom_fields
custom_fields.each do |custom_field|
field_name = Import::Zendesk::TicketFieldFactory.local_id(custom_field['id'])
field_value = custom_field['value']
next if field_value.nil?
fields[ field_name.to_sym ] = field_value
end
fields
end
end
end
end

View file

@ -1,71 +0,0 @@
module Import
module Zendesk
class Ticket
class Comment
def initialize(comment, local_ticket, _zendesk_ticket)
create_or_update(comment, local_ticket)
import_attachments(comment)
end
private
def create_or_update(comment, local_ticket)
mapped_article = local_article_fields(comment, local_ticket)
return if updated?(mapped_article)
create(mapped_article)
end
def updated?(article)
@local_article = ::Ticket::Article.find_by(message_id: article[:message_id])
return false if !@local_article
@local_article.update!(article)
true
end
def create(article)
@local_article = ::Ticket::Article.create(article)
end
def local_article_fields(comment, local_ticket)
local_user_id = Import::Zendesk::UserFactory.local_id( comment.author_id ) || 1
{
ticket_id: local_ticket.id,
body: comment.html_body,
content_type: 'text/html',
internal: !comment.public,
message_id: comment.id,
updated_by_id: local_user_id,
created_by_id: local_user_id,
sender_id: Import::Zendesk::Ticket::Comment::Sender.local_id( local_user_id ),
type_id: Import::Zendesk::Ticket::Comment::Type.local_id(comment),
}.merge(from_to(comment))
end
def from_to(comment)
if comment.via.channel == 'email'
{
from: comment.via.source.from.address,
to: comment.via.source.to.address # Notice comment.via.from.original_recipients = [\"another@gmail.com\", \"support@example.zendesk.com\"]
}
elsif comment.via.channel == 'facebook'
{
from: comment.via.source.from.facebook_id,
to: comment.via.source.to.facebook_id
}
else
{}
end
end
def import_attachments(comment)
attachments = comment.attachments
return if attachments.blank?
Import::Zendesk::Ticket::Comment::AttachmentFactory.import(attachments, @local_article)
end
end
end
end
end

View file

@ -1,46 +0,0 @@
module Import
module Zendesk
class Ticket
class Comment
class Attachment
include Import::Helper
def initialize(attachment, local_article)
response = request(attachment)
return if !response
::Store.add(
object: 'Ticket::Article',
o_id: local_article.id,
data: response.body,
filename: attachment.file_name,
preferences: {
'Content-Type' => attachment.content_type
},
created_by_id: 1
)
rescue => e
log e.message
end
private
def request(attachment)
response = UserAgent.get(
attachment.content_url,
{},
{
open_timeout: 10,
read_timeout: 60,
},
)
return response if response.success?
log response.error
nil
end
end
end
end
end
end

View file

@ -1,38 +0,0 @@
module Import
module Zendesk
class Ticket
class Comment
module AttachmentFactory
# we need to loop over each instead of all!
# so we can use the default import factory here
extend Import::Factory
# rubocop:disable Style/ModuleFunction
extend self
private
# special handling which only starts import if needed
# Attention: skip? method can't be used since it (currently)
# only checks for single records - not all
def import_loop(records, *args, &import_block)
local_article = args[0]
local_attachments = local_article.attachments
return if local_attachments.count == records.count
# get a common ground
local_attachments.each(&:delete)
return if records.blank?
records.each(&import_block)
end
def create_instance(record, *args)
local_article = args[0]
backend_class(record).new(record, local_article)
end
end
end
end
end
end

View file

@ -1,49 +0,0 @@
module Import
module Zendesk
class Ticket
class Comment
module Sender
# rubocop:disable Style/ModuleFunction
extend self
def local_id(user_id)
author = author_lookup(user_id)
sender_id(author)
end
private
def author_lookup(user_id)
::User.find( user_id )
end
def sender_id(author)
if author.role?('Customer')
article_sender_customer
elsif author.role?('Agent')
article_sender_agent
else
article_sender_system
end
end
def article_sender_customer
return @article_sender_customer if @article_sender_customer
@article_sender_customer = ::Ticket::Article::Sender.lookup(name: 'Customer').id
end
def article_sender_agent
return @article_sender_agent if @article_sender_agent
@article_sender_agent = ::Ticket::Article::Sender.lookup(name: 'Agent').id
end
def article_sender_system
return @article_sender_system if @article_sender_system
@article_sender_system = ::Ticket::Article::Sender.lookup(name: 'System').id
end
end
end
end
end
end

View file

@ -1,60 +0,0 @@
module Import
module Zendesk
class Ticket
class Comment
module Type
# rubocop:disable Style/ModuleFunction
extend self
def local_id(object)
case object.via.channel
when 'web'
article_type_id[:web]
when 'email'
article_type_id[:email]
when 'sample_ticket'
article_type_id[:note]
when 'twitter'
if object.via.source.rel == 'mention'
article_type_id[:twitter_status]
else
article_type_id[:twitter_direct_message]
end
when 'facebook'
if object.via.source.rel == 'post'
article_type_id[:facebook_feed_post]
else
article_type_id[:facebook_feed_comment]
end
# fallback for other not (yet) supported article types
# See:
# https://support.zendesk.com/hc/en-us/articles/203661746-Zendesk-Glossary#topic_zie_aqe_tf
# https://support.zendesk.com/hc/en-us/articles/203661596-About-Zendesk-Support-channels
else
article_type_id[:web]
end
end
private
def article_type_id
return @article_type_id if @article_type_id
article_types = ['web', 'note', 'email', 'twitter status',
'twitter direct-message', 'facebook feed post',
'facebook feed comment']
@article_type_id = {}
article_types.each do |article_type|
article_type_key = article_type.gsub(/\s|\-/, '_').to_sym
@article_type_id[article_type_key] = ::Ticket::Article::Type.lookup(name: article_type).id
end
@article_type_id
end
end
end
end
end
end

View file

@ -1,9 +0,0 @@
module Import
module Zendesk
class Ticket
module CommentFactory
extend Import::Zendesk::Ticket::SubObjectFactory
end
end
end
end

View file

@ -1,21 +0,0 @@
module Import
module Zendesk
class Ticket
module SubObjectFactory
# we need to loop over each instead of all!
# so we can use the default import factory here
include Import::Factory
private
def create_instance(record, *args)
local_ticket = args[0]
zendesk_ticket = args[1]
backend_class(record).new(record, local_ticket, zendesk_ticket)
end
end
end
end
end

View file

@ -1,16 +0,0 @@
module Import
module Zendesk
class Ticket
class Tag
def initialize(tag, local_ticket, zendesk_ticket)
::Tag.tag_add(
object: 'Ticket',
o_id: local_ticket.id,
item: tag.id,
created_by_id: Import::Zendesk::UserFactory.local_id(zendesk_ticket.requester_id) || 1,
)
end
end
end
end
end

View file

@ -1,9 +0,0 @@
module Import
module Zendesk
class Ticket
module TagFactory
extend Import::Zendesk::Ticket::SubObjectFactory
end
end
end
end

View file

@ -1,46 +0,0 @@
module Import
module Zendesk
module TicketFactory
extend Import::Zendesk::BaseFactory
# rubocop:disable Style/ModuleFunction
extend self
private
def import_loop(records, *args)
count_update_hook = proc do |record|
yield(record)
update_ticket_count(records)
end
super(records, *args, &count_update_hook)
end
def update_ticket_count(collection)
cache_key = 'import_zendesk_stats'
count_variable = :@count
page_variable = :@next_page
next_page = collection.instance_variable_get(page_variable)
@last_page ||= next_page
return if @last_page == next_page
return if !collection.instance_variable_get(count_variable)
@last_page = next_page
# check cache
cache = Cache.get(cache_key)
return if !cache
cache['Tickets'] ||= 0
cache['Tickets'] += collection.instance_variable_get(count_variable)
Cache.write(cache_key, cache)
end
end
end
end

View file

@ -1,12 +0,0 @@
module Import
module Zendesk
class TicketField < Import::Zendesk::ObjectField
private
def remote_name(ticket_field)
ticket_field.title
end
end
end
end

View file

@ -1,34 +0,0 @@
module Import
module Zendesk
module TicketFieldFactory
extend Import::Zendesk::BaseFactory
extend Import::Zendesk::LocalIDMapperHook
MAPPING = {
'subject' => 'title',
'description' => 'note',
'status' => 'state_id',
'tickettype' => 'type',
'priority' => 'priority_id',
'basic_priority' => 'priority_id',
'group' => 'group_id',
'assignee' => 'owner_id',
}.freeze
# rubocop:disable Style/ModuleFunction
extend self
def skip?(field, *_args)
# check if the Ticket object already has a same named column / attribute
# so we want to skip instead of importing it
::Ticket.column_names.include?( local_attribute(field) )
end
private
def local_attribute(field)
MAPPING.fetch(field.type, field.type)
end
end
end
end

View file

@ -1,75 +0,0 @@
# Rails autoload has some issues with same namend sub-classes
# in the importer folder require AND simultaniuos requiring
# of the same file in different threads so we need to
# require them ourself
require 'import/zendesk/user/group'
require 'import/zendesk/user/role'
# https://developer.zendesk.com/rest_api/docs/core/users
module Import
module Zendesk
class User
include Import::Zendesk::Helper
attr_reader :zendesk_id, :id
def initialize(user)
local_user = ::User.create_or_update( local_user_fields(user) )
@zendesk_id = user.id
@id = local_user.id
end
private
def local_user_fields(user)
{
login: login(user),
firstname: user.name,
email: user.email,
phone: user.phone,
password: password(user),
active: !user.suspended,
groups: Import::Zendesk::User::Group.for(user),
roles: roles(user),
note: user.notes,
verified: user.verified,
organization_id: Import::Zendesk::OrganizationFactory.local_id( user.organization_id ),
last_login: user.last_login_at,
image_source: photo(user),
updated_by_id: 1,
created_by_id: 1
}.merge(custom_fields(user))
end
def login(user)
return user.email if user.email
# Zendesk users may have no other identifier than the ID, e.g. twitter users
user.id.to_s
end
def password(user)
return Setting.get('import_zendesk_endpoint_key') if import_user?(user)
''
end
def roles(user)
return Import::Zendesk::User::Role.map(user, 'admin') if import_user?(user)
Import::Zendesk::User::Role.for(user)
end
def import_user?(user)
return false if user.email.blank?
user.email == Setting.get('import_zendesk_endpoint_username')
end
def photo(user)
return if !user.photo
user.photo.content_url
end
def custom_fields(user)
get_fields(user.user_fields)
end
end
end
end

View file

@ -1,51 +0,0 @@
# this require is required (hehe) because of Rails autoloading
# which causes strange behavior not inheriting correctly
# from Import::OTRS::DynamicField
require 'import/zendesk/user'
# https://developer.zendesk.com/rest_api/docs/core/groups
module Import
module Zendesk
class User
module Group
# rubocop:disable Style/ModuleFunction
extend self
def for(user)
groups = []
return groups if mapping[user.id].blank?
mapping[user.id].each do |zendesk_group_id|
local_group_id = Import::Zendesk::GroupFactory.local_id(zendesk_group_id)
next if !local_group_id
group = ::Group.find( local_group_id )
groups.push(group)
end
groups
end
private
def mapping
return @mapping if !@mapping.nil?
@mapping = {}
Import::Zendesk::Requester.client.group_memberships.all! do |group_membership|
@mapping[ group_membership.user_id ] ||= []
@mapping[ group_membership.user_id ].push( group_membership.group_id )
end
@mapping
end
end
end
end
end

View file

@ -1,64 +0,0 @@
# this require is required (hehe) because of Rails autoloading
# which causes strange behavior not inheriting correctly
# from Import::OTRS::DynamicField
require 'import/zendesk/user'
module Import
module Zendesk
class User
module Role
extend Import::Helper
# rubocop:disable Style/ModuleFunction
extend self
def for(user)
map(user, group_method( user.role.name ))
end
def map(user, role)
send(role.to_sym, user)
rescue NoMethodError => e
log "Unknown mapping for role '#{user.role.name}' and user with id '#{user.id}'"
[]
end
private
def end_user(_user)
[role_customer]
end
def agent(user)
return [ role_agent ] if user.restricted_agent
admin(user)
end
def admin(_user)
[role_admin, role_agent]
end
def group_method(role)
role.tr('-', '_')
end
def role_admin
@role_admin ||= lookup('Admin')
end
def role_agent
@role_agent ||= lookup('Agent')
end
def role_customer
@role_customer ||= lookup('Customer')
end
def lookup(role_name)
::Role.lookup(name: role_name)
end
end
end
end
end

View file

@ -1,8 +0,0 @@
module Import
module Zendesk
module UserFactory
extend Import::Zendesk::BaseFactory
extend Import::Zendesk::LocalIDMapperHook
end
end
end

View file

@ -1,6 +0,0 @@
module Import
module Zendesk
class UserField < Import::Zendesk::ObjectField
end
end
end

View file

@ -1,8 +0,0 @@
module Import
module Zendesk
module UserFieldFactory
extend Import::Zendesk::BaseFactory
extend Import::Zendesk::LocalIDMapperHook
end
end
end

View file

@ -0,0 +1,21 @@
class Sequencer
class Sequence
module Import
module Zendesk
class ConnectionTest < Sequencer::Sequence::Base
def self.expecting
[:connected]
end
def self.sequence
[
'Zendesk::Client',
'Zendesk::Connected',
]
end
end
end
end
end
end

View file

@ -0,0 +1,32 @@
class Sequencer
class Sequence
module Import
module Zendesk
class Full < Sequencer::Sequence::Base
def self.sequence
[
'Import::Common::ImportMode::Check',
'Import::Common::SystemInitDone::Check',
'Zendesk::Client',
'Import::Zendesk::ObjectsTotalCount',
'Import::Common::ImportJob::Statistics::Update',
'Import::Common::ImportJob::Statistics::Store',
'Import::Common::ImportJob::DryRun',
'Import::Zendesk::Groups',
'Import::Zendesk::OrganizationFields',
'Import::Zendesk::Organizations',
'Import::Zendesk::UserFields',
'Import::Zendesk::UserGroupMap',
'Import::Zendesk::Users',
'Import::Zendesk::TicketFields',
'Import::Zendesk::Tickets',
'Import::Common::SystemInitDone::Set',
'Import::Common::ImportMode::Unset',
]
end
end
end
end
end
end

View file

@ -0,0 +1,25 @@
class Sequencer
class Sequence
module Import
module Zendesk
class Group < Sequencer::Sequence::Base
def self.sequence
[
'Common::ModelClass::Group',
'Import::Zendesk::Group::Mapping',
'Import::Common::Model::Attributes::AddByIds',
'Import::Common::Model::FindBy::Name',
'Import::Common::Model::Update',
'Import::Common::Model::Create',
'Import::Common::Model::Save',
'Import::Common::Model::Statistics::Diff::ModelKey',
'Import::Common::ImportJob::Statistics::Update',
'Import::Common::ImportJob::Statistics::Store',
]
end
end
end
end
end
end

View file

@ -0,0 +1,26 @@
class Sequencer
class Sequence
module Import
module Zendesk
class Organization < Sequencer::Sequence::Base
def self.sequence
[
'Common::ModelClass::Organization',
'Import::Zendesk::Organization::Mapping',
'Import::Zendesk::Organization::CustomFields',
'Import::Common::Model::Attributes::AddByIds',
'Import::Common::Model::FindBy::Name',
'Import::Common::Model::Update',
'Import::Common::Model::Create',
'Import::Common::Model::Save',
'Import::Common::Model::Statistics::Diff::ModelKey',
'Import::Common::ImportJob::Statistics::Update',
'Import::Common::ImportJob::Statistics::Store',
]
end
end
end
end
end
end

View file

@ -0,0 +1,18 @@
class Sequencer
class Sequence
module Import
module Zendesk
class OrganizationField < Sequencer::Sequence::Base
def self.sequence
[
'Common::ModelClass::Organization',
'Import::Zendesk::ObjectAttribute::SanitizedName',
'Import::Zendesk::ObjectAttribute::Add',
]
end
end
end
end
end
end

View file

@ -0,0 +1,37 @@
class Sequencer
class Sequence
module Import
module Zendesk
class Ticket < Sequencer::Sequence::Base
def self.sequence
[
'Import::Zendesk::Ticket::UserID',
'Import::Zendesk::Ticket::OwnerID',
'Import::Zendesk::Ticket::GroupID',
'Import::Zendesk::Ticket::OrganizationID',
'Import::Zendesk::Ticket::PriorityID',
'Import::Zendesk::Ticket::StateID',
'Import::Zendesk::Common::ArticleSenderID',
'Import::Zendesk::Common::ArticleTypeID',
'Import::Zendesk::Ticket::Subject',
'Import::Zendesk::Ticket::CustomFields',
'Import::Zendesk::Ticket::Mapping',
'Common::ModelClass::Ticket',
'Import::Common::Model::FindBy::Id',
'Import::Common::Model::Update',
'Import::Common::Model::Create',
'Import::Common::Model::Save',
'Import::Common::Model::ResetPrimaryKeySequence',
'Import::Zendesk::Ticket::Tags',
'Import::Zendesk::Ticket::Comments',
'Import::Common::Model::Statistics::Diff::ModelKey',
'Import::Common::ImportJob::Statistics::Update',
'Import::Common::ImportJob::Statistics::Store',
]
end
end
end
end
end
end

View file

@ -0,0 +1,30 @@
class Sequencer
class Sequence
module Import
module Zendesk
class Ticket < Sequencer::Sequence::Base
class Comment < Sequencer::Sequence::Base
def self.sequence
[
'Import::Zendesk::Ticket::Comment::UserID',
'Import::Zendesk::Common::ArticleSenderID',
'Import::Zendesk::Common::ArticleTypeID',
'Import::Zendesk::Ticket::Comment::From',
'Import::Zendesk::Ticket::Comment::To',
'Import::Zendesk::Ticket::Comment::Mapping',
'Import::Zendesk::Ticket::Comment::UnsetInstance',
'Common::ModelClass::Ticket::Article',
'Import::Common::Model::FindBy::Id',
'Import::Common::Model::Update',
'Import::Common::Model::Create',
'Import::Common::Model::Save',
'Import::Zendesk::Ticket::Comment::Attachments',
]
end
end
end
end
end
end
end

View file

@ -0,0 +1,21 @@
class Sequencer
class Sequence
module Import
module Zendesk
class Ticket < Sequencer::Sequence::Base
class Comment < Sequencer::Sequence::Base
class Attachment < Sequencer::Sequence::Base
def self.sequence
[
'Import::Zendesk::Ticket::Comment::Attachment::Request',
'Import::Zendesk::Ticket::Comment::Attachment::Add',
]
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,19 @@
class Sequencer
class Sequence
module Import
module Zendesk
class Ticket < Sequencer::Sequence::Base
class Tag < Sequencer::Sequence::Base
def self.sequence
[
'Import::Zendesk::Ticket::Tag::Item',
'Common::Tag::Add',
]
end
end
end
end
end
end
end

View file

@ -0,0 +1,19 @@
class Sequencer
class Sequence
module Import
module Zendesk
class TicketField < Sequencer::Sequence::Base
def self.sequence
[
'Common::ModelClass::Ticket',
'Import::Zendesk::TicketField::CheckCustom',
'Import::Zendesk::TicketField::SanitizedName',
'Import::Zendesk::TicketField::Add',
]
end
end
end
end
end
end

View file

@ -0,0 +1,33 @@
class Sequencer
class Sequence
module Import
module Zendesk
class User < Sequencer::Sequence::Base
def self.sequence
[
'Import::Zendesk::User::Initiator',
'Import::Zendesk::User::Roles',
'Import::Zendesk::User::Groups',
'Import::Zendesk::User::Login',
'Import::Zendesk::User::Password',
'Import::Zendesk::User::ImageSource',
'Import::Zendesk::User::OrganizationID',
'Common::ModelClass::User',
'Import::Zendesk::User::Mapping',
'Import::Zendesk::User::CustomFields',
'Import::Common::Model::Attributes::AddByIds',
'Import::Common::Model::FindBy::UserAttributes',
'Import::Common::Model::Update',
'Import::Common::Model::Create',
'Import::Common::Model::Save',
'Import::Common::Model::Statistics::Diff::ModelKey',
'Import::Common::ImportJob::Statistics::Update',
'Import::Common::ImportJob::Statistics::Store',
]
end
end
end
end
end
end

View file

@ -0,0 +1,18 @@
class Sequencer
class Sequence
module Import
module Zendesk
class UserField < Sequencer::Sequence::Base
def self.sequence
[
'Common::ModelClass::User',
'Import::Zendesk::ObjectAttribute::SanitizedName',
'Import::Zendesk::ObjectAttribute::Add',
]
end
end
end
end
end
end

View file

@ -0,0 +1,10 @@
class Sequencer
class Unit
module Common
module ModelClass
class Group < Sequencer::Unit::Common::ModelClass::Base
end
end
end
end
end

View file

@ -0,0 +1,10 @@
class Sequencer
class Unit
module Common
module ModelClass
class Organization < Sequencer::Unit::Common::ModelClass::Base
end
end
end
end
end

View file

@ -0,0 +1,10 @@
class Sequencer
class Unit
module Common
module ModelClass
class Ticket < Sequencer::Unit::Common::ModelClass::Base
end
end
end
end
end

View file

@ -0,0 +1,12 @@
class Sequencer
class Unit
module Common
module ModelClass
class Ticket < Sequencer::Unit::Common::ModelClass::Base
class Article < Sequencer::Unit::Common::ModelClass::Base
end
end
end
end
end
end

View file

@ -0,0 +1,21 @@
class Sequencer
class Unit
module Common
module Tag
class Add < Sequencer::Unit::Base
uses :model_class, :instance, :item, :user_id
def process
::Tag.tag_add(
object: model_class.name,
o_id: instance.id,
item: item,
created_by_id: user_id,
)
end
end
end
end
end
end

View file

@ -0,0 +1,17 @@
class Sequencer
class Unit
module Common
class UnsetAttributes < Sequencer::Unit::Base
def process
uses = self.class.uses
return if uses.blank?
uses.each do |attribute|
state.unset(attribute)
end
end
end
end
end
end

View file

@ -0,0 +1,18 @@
class Sequencer
class Unit
module Import
module Common
module ImportMode
class Check < Sequencer::Unit::Base
def process
# check if system is in import mode
return if Setting.get('import_mode')
raise 'System is not in import mode!'
end
end
end
end
end
end
end

View file

@ -0,0 +1,16 @@
class Sequencer
class Unit
module Import
module Common
module ImportMode
class Unset < Sequencer::Unit::Base
def process
Setting.set('import_mode', false)
end
end
end
end
end
end
end

View file

@ -0,0 +1,14 @@
class Sequencer
class Unit
module Import
module Common
module Model
module FindBy
class Id < Sequencer::Unit::Import::Common::Model::FindBy::SameNamedAttribute
end
end
end
end
end
end
end

View file

@ -0,0 +1,14 @@
class Sequencer
class Unit
module Import
module Common
module Model
module FindBy
class Name < Sequencer::Unit::Import::Common::Model::FindBy::SameNamedAttribute
end
end
end
end
end
end
end

View file

@ -0,0 +1,20 @@
class Sequencer
class Unit
module Import
module Common
module Model
module FindBy
class SameNamedAttribute < Sequencer::Unit::Import::Common::Model::Lookup::Attributes
private
def attribute
self.class.name.demodulize.underscore.to_sym
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,20 @@
class Sequencer
class Unit
module Import
module Common
module Model
module FindBy
class UserAttributes < Sequencer::Unit::Import::Common::Model::Lookup::Attributes
private
def attributes
%i[login email]
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,20 @@
class Sequencer
class Unit
module Import
module Common
module Model
class ResetPrimaryKeySequence < Sequencer::Unit::Base
uses :model_class
delegate table_name: :model_class
def process
DbHelper.import_post(table_name)
end
end
end
end
end
end
end

View file

@ -12,12 +12,16 @@ class Sequencer
%i[skipped created updated unchanged failed deactivated]
end
def results
%i[sum total]
end
def empty_diff
possible_actions.collect { |key| [key, 0] }.to_h
end
def possible_actions
@possible_actions ||= actions
@possible_actions ||= actions + results
end
end
end

View file

@ -0,0 +1,29 @@
class Sequencer
class Unit
module Import
module Common
module Model
module Statistics
class Total < Sequencer::Unit::Base
include ::Sequencer::Unit::Import::Common::Model::Statistics::Mixin::EmptyDiff
def process
state.provide(:statistics_diff) do
diff.merge(
total: total
)
end
end
private
def total
raise "Missing implementation if total method for class #{self.class.name}"
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,46 @@
class Sequencer
class Unit
module Import
module Common
module ObjectAttribute
class SanitizedName < Sequencer::Unit::Common::Provider::Named
private
def sanitized_name
# model_no
# model_nos
# model_name
# model_name
without_double_underscores.gsub(/_id(s?)$/, '_no\1')
end
def without_double_underscores
# model_id
# model_ids
# model_name
# model_name
without_spaces_and_slashes.gsub(/_{2,}/, '_')
end
def without_spaces_and_slashes
# model_id
# model_ids
# model___name
# model_name
unsanitized_name.gsub(%r{[\s\/]}, '_').underscore
end
def unsanitized_name
# Model ID
# Model IDs
# Model / Name
# Model Name
raise 'Missing implementation for unsanitized_name method'
end
end
end
end
end
end
end

View file

@ -0,0 +1,17 @@
class Sequencer
class Unit
module Import
module Common
module SystemInitDone
class Check < Sequencer::Unit::Base
def process
return if !Setting.get('system_init_done')
raise 'System is already system_init_done!'
end
end
end
end
end
end
end

View file

@ -0,0 +1,16 @@
class Sequencer
class Unit
module Import
module Common
module SystemInitDone
class Set < Sequencer::Unit::Base
def process
Setting.set('system_init_done', true)
end
end
end
end
end
end
end

View file

@ -0,0 +1,30 @@
class Sequencer
class Unit
module Import
module Zendesk
module Common
class ArticleSenderID < Sequencer::Unit::Common::Provider::Named
uses :user_id
private
def article_sender_id
return article_sender('Customer') if author.role?('Customer')
return article_sender('Agent') if author.role?('Agent')
article_sender('System')
end
def author
@author ||= ::User.find(user_id)
end
def article_sender(name)
::Ticket::Article::Sender.select(:id).find_by(name: name).id
end
end
end
end
end
end
end

View file

@ -0,0 +1,52 @@
class Sequencer
class Unit
module Import
module Zendesk
module Common
class ArticleTypeID < Sequencer::Unit::Common::Provider::Named
uses :resource
private
def article_type_id
::Ticket::Article::Type.select(:id).find_by(name: name).id
end
def name
known_channel || 'web'
end
def known_channel
channel = resource.via.channel
direct_mapping.fetch(channel, indirect_map(channel))
end
def indirect_map(channel)
method_name = "remote_name_#{channel}".to_sym
send(method_name) if respond_to?(method_name, true)
end
def remote_name_facebook
return 'facebook feed post' if resource.via.source.rel == 'post'
'facebook feed comment'
end
def remote_name_twitter
return 'twitter status' if resource.via.source.rel == 'mention'
'twitter direct message'
end
def direct_mapping
{
'web' => 'web',
'email' => 'email',
'sample_ticket' => 'note',
}.freeze
end
end
end
end
end
end
end

View file

@ -0,0 +1,39 @@
class Sequencer
class Unit
module Import
module Zendesk
module Common
class CustomFields < Sequencer::Unit::Base
include ::Sequencer::Unit::Import::Common::Mapping::Mixin::ProvideMapped
uses :resource
def process
provide_mapped do
attributes_hash
end
end
private
def remote_fields
raise 'Missing implementation of remote_fields method'
end
def fields
@fields ||= remote_fields
end
def attributes_hash
return {} if fields.blank?
fields.each_with_object({}) do |(key, value), result|
next if value.nil?
result[key] = value
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,24 @@
class Sequencer
class Unit
module Import
module Zendesk
module Group
class Mapping < Sequencer::Unit::Base
include ::Sequencer::Unit::Import::Common::Mapping::Mixin::ProvideMapped
uses :resource
def process
provide_mapped do
{
name: resource.name,
active: !resource.deleted,
}
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,10 @@
class Sequencer
class Unit
module Import
module Zendesk
class Groups < Sequencer::Unit::Import::Zendesk::SubSequence::Object
end
end
end
end
end

View file

@ -0,0 +1,25 @@
class Sequencer
class Unit
module Import
module Zendesk
module ObjectAttribute
class Add < Sequencer::Unit::Base
uses :model_class, :sanitized_name, :resource
provides :instance
def process
state.provide(:instance) do
backend_class.new(model_class, sanitized_name, resource)
end
end
def backend_class
"Import::Zendesk::ObjectAttribute::#{resource.type.capitalize}".constantize
end
end
end
end
end
end
end

View file

@ -0,0 +1,24 @@
class Sequencer
class Unit
module Import
module Zendesk
module ObjectAttribute
class SanitizedName < Sequencer::Unit::Import::Common::ObjectAttribute::SanitizedName
uses :resource
private
def unsanitized_name
# Model ID
# Model IDs
# Model / Name
# Model Name
resource['key']
end
end
end
end
end
end
end

View file

@ -0,0 +1,50 @@
class Sequencer
class Unit
module Import
module Zendesk
class ObjectsTotalCount < Sequencer::Unit::Common::Provider::Attribute
include ::Sequencer::Unit::Import::Common::Model::Statistics::Mixin::EmptyDiff
uses :client
private
def statistics_diff
%i[Groups Users Organizations Tickets].each_with_object({}) do |object, stats|
stats[object] = empty_diff.merge(
total: request(object).count!
)
end
end
def request(object)
return tickets if object == 'Tickets'
generic(object)
end
def generic(object)
client.send(object.to_s.underscore.to_sym)
end
# this special ticket logic is needed since Zendesk archives tickets
# after 120 days and doesn't return them via the client.tickets
# endpoint as described here:
# https://github.com/zammad/zammad/issues/558#issuecomment-267951351
# the proper way is to use the 'incremental' endpoint which is not available
# via the ruby gem yet but a pull request is pending:
# https://github.com/zendesk/zendesk_api_client_rb/pull/287
# the following workaround is needed to use this functionality
# Counting Tickets has the limitations that max. 1000 are returned
# that's why we need to update the number when it's exceeded while importing
def tickets
ZendeskAPI::Collection.new(
client,
ZendeskAPI::Ticket,
path: 'incremental/tickets?start_time=1'
)
end
end
end
end
end
end

View file

@ -0,0 +1,18 @@
class Sequencer
class Unit
module Import
module Zendesk
module Organization
class CustomFields < Sequencer::Unit::Import::Zendesk::Common::CustomFields
private
def remote_fields
resource.organization_fields
end
end
end
end
end
end
end

View file

@ -0,0 +1,25 @@
class Sequencer
class Unit
module Import
module Zendesk
module Organization
class Mapping < Sequencer::Unit::Base
include ::Sequencer::Unit::Import::Common::Mapping::Mixin::ProvideMapped
uses :resource
def process
provide_mapped do
{
name: resource.name,
note: resource.note,
shared: resource.shared_tickets,
}
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,10 @@
class Sequencer
class Unit
module Import
module Zendesk
class OrganizationFields < Sequencer::Unit::Import::Zendesk::SubSequence::ObjectFields
end
end
end
end
end

View file

@ -0,0 +1,16 @@
class Sequencer
class Unit
module Import
module Zendesk
class Organizations < Sequencer::Unit::Import::Zendesk::SubSequence::Object
private
def resource_iteration_method
:all!
end
end
end
end
end
end

View file

@ -0,0 +1,60 @@
class Sequencer
class Unit
module Import
module Zendesk
module SubSequence
module Base
module ClassMethods
def resource_klass
@resource_klass ||= name.split('::').last.singularize
end
end
def self.included(base)
base.extend(ClassMethods)
base.uses :dry_run, :import_job
end
private
def default_params
{
dry_run: dry_run,
import_job: import_job,
}
end
def resource_klass
# base.instance_delegate [:resource_klass] => base
# doesn't work since we are included and then inherited
# there might be multiple inherited hooks which overwrite
# each other :/
self.class.resource_klass
end
def sequence_name
"Import::Zendesk::#{resource_klass}"
end
def resource_iteration(&block)
resource_collection.public_send(resource_iteration_method, &block)
end
def resource_collection
collection_provider.public_send(resource_collection_attribute)
end
def resource_iteration_method
:all!
end
def resource_collection_attribute
@resource_collection_attribute ||= resource_klass.pluralize.underscore
end
end
end
end
end
end
end

View file

@ -0,0 +1,80 @@
class Sequencer
class Unit
module Import
module Zendesk
module SubSequence
module Mapped
module ClassMethods
def resource_map
"#{resource_klass.underscore}_map".to_sym
end
def inherited(base)
base.provides(base.resource_map)
base.extend(Forwardable)
base.instance_delegate [:resource_map] => base
end
end
def self.included(base)
base.uses :client
base.extend(ClassMethods)
end
def process
state.provide(resource_map) do
process_sub_sequence
mapping
end
end
private
def expecting
raise 'Missing implementation of expecting method'
end
def collection_provider
client
end
def process_sub_sequence
resource_iteration do |resource|
expected_value = expected(resource)
next if expected_value.blank?
mapping[resource.id] = mapping_value(expected_value)
end
end
def expected(resource)
result = sub_sequence(resource)
result[expecting]
end
def sub_sequence(resource)
::Sequencer.process(sequence_name,
parameters: default_params.merge(
resource: resource
),
expecting: [expecting])
end
def mapping_value(expected_value)
expected_value
end
def mapping
@mapping ||= {}
end
end
end
end
end
end
end

View file

@ -0,0 +1,24 @@
class Sequencer
class Unit
module Import
module Zendesk
module SubSequence
class Object < Sequencer::Unit::Base
include ::Sequencer::Unit::Import::Zendesk::SubSequence::Base
include ::Sequencer::Unit::Import::Zendesk::SubSequence::Mapped
private
def expecting
:instance
end
def mapping_value(expected_value)
expected_value.id
end
end
end
end
end
end
end

View file

@ -0,0 +1,20 @@
class Sequencer
class Unit
module Import
module Zendesk
module SubSequence
class ObjectFields < Sequencer::Unit::Base
include ::Sequencer::Unit::Import::Zendesk::SubSequence::Base
include ::Sequencer::Unit::Import::Zendesk::SubSequence::Mapped
private
def expecting
:sanitized_name
end
end
end
end
end
end
end

View file

@ -0,0 +1,39 @@
class Sequencer
class Unit
module Import
module Zendesk
module SubSequence
class SubObject < Sequencer::Unit::Base
include ::Sequencer::Unit::Import::Zendesk::SubSequence::Base
uses :resource, :instance, :user_id, :model_class
def process
resource_iteration do |sub_resource|
::Sequencer.process(sequence_name,
parameters: default_params.merge(
resource: sub_resource
),)
end
end
private
def collection_provider
resource
end
def default_params
super.merge(
instance: instance,
user_id: user_id,
model_class: model_class,
)
end
end
end
end
end
end
end

View file

@ -0,0 +1,18 @@
class Sequencer
class Unit
module Import
module Zendesk
module SubSequence
class TicketSubObject < Sequencer::Unit::Import::Zendesk::SubSequence::SubObject
private
def sequence_name
"Import::Zendesk::Ticket::#{resource_klass}"
end
end
end
end
end
end
end

View file

@ -0,0 +1,37 @@
class Sequencer
class Unit
module Import
module Zendesk
module Ticket
module Comment
module Attachment
class Add < Sequencer::Unit::Base
prepend ::Sequencer::Unit::Import::Common::Model::Mixin::Skip::Action
include ::Sequencer::Unit::Import::Common::Model::Mixin::HandleFailure
skip_action :skipped
uses :instance, :resource, :response, :model_class
def process
::Store.add(
object: model_class.name,
o_id: instance.id,
data: response.body,
filename: resource.file_name,
preferences: {
'Content-Type' => resource.content_type
},
created_by_id: 1
)
rescue => e
handle_failure(e)
end
end
end
end
end
end
end
end
end

Some files were not shown because too many files have changed in this diff Show more