Просмотр исходного кода

Fixes #3698 - Import for kayako during the setup.

Dominik Klein 3 лет назад
Родитель
Сommit
dd30b18285

+ 1 - 1
.rubocop/cop/zammad/exists_db_strategy.rb

@@ -17,7 +17,7 @@ module RuboCop
         PATTERN
 
         def_node_matcher :has_reset?, <<-PATTERN
-          $(send _ {:describe :context :it} (_ ...) (hash ... (pair (sym :db_strategy) (sym {:reset :reset_all}))))
+          $(send _ {:describe :context :it :shared_examples} (_ ...) (hash ... (pair (sym :db_strategy) (sym {:reset :reset_all}))))
         PATTERN
 
         MSG = 'Add a `db_strategy: :reset` to your context/decribe when you are creating object manager attributes!'.freeze

+ 0 - 1
app/assets/javascripts/app/controllers/import_freshdesk.coffee

@@ -8,7 +8,6 @@ class ImportFreshdesk extends App.ControllerWizardFullScreen
     '#freshdesk-subdomain-addon':              'freshdeskSubdomainAddon'
     '.freshdesk-subdomain-error':              'linkErrorMessage'
     '.freshdesk-api-token-error':              'apiTokenErrorMessage'
-    '#freshdesk-email':                        'freshdeskEmail'
     '#freshdesk-api-token':                    'freshdeskApiToken'
     '.js-ticket-count-info':                   'ticketCountInfo'
   updateMigrationDisplayLoop: 0

+ 194 - 0
app/assets/javascripts/app/controllers/import_kayako.coffee

@@ -0,0 +1,194 @@
+class ImportKayako extends App.ControllerWizardFullScreen
+  className: 'getstarted fit'
+  elements:
+    '.input-feedback':                         'urlStatus'
+    '[data-target=kayako-credentials]':     'nextEnterCredentials'
+    '[data-target=kayako-start-migration]': 'nextStartMigration'
+    '#kayako-subdomain':                    'kayakoSubdomain'
+    '#kayako-subdomain-addon':              'kayakoSubdomainAddon'
+    '.kayako-subdomain-error':              'linkErrorMessage'
+    '.kayako-password-error':              'apiTokenErrorMessage'
+    '#kayako-email':                        'kayakoEmail'
+    '#kayako-password':                    'kayakoPassword'
+    '.js-ticket-count-info':                   'ticketCountInfo'
+  updateMigrationDisplayLoop: 0
+
+  events:
+    'click .js-kayako-credentials': 'showCredentials'
+    'click .js-migration-start':     'startMigration'
+    'keyup #kayako-subdomain':           'updateUrl'
+    'keyup #kayako-password':     'updateCredentials'
+
+  constructor: ->
+    super
+
+    # set title
+    @title 'Import'
+
+    @kayakoDomain = '.kayako.com'
+
+    # redirect to login if admin user already exists
+    if @Config.get('system_init_done')
+      @navigate '#login'
+      return
+
+    @fetch()
+
+  fetch: ->
+
+    # get data
+    @ajax(
+      id:          'getting_started'
+      type:        'GET'
+      url:         "#{@apiPath}/getting_started"
+      processData: true
+      success:     (data, status, xhr) =>
+
+        # check if import is active
+        if data.import_mode == true && data.import_backend != 'kayako'
+          @navigate "#import/#{data.import_backend}", { emptyEl: true }
+          return
+
+        # render page
+        @render()
+
+        if data.import_mode == true
+          @showImportState()
+          @updateMigration()
+    )
+
+  render: ->
+    @replaceWith App.view('import/kayako')(
+      kayakoDomain: @kayakoDomain
+    )
+
+  updateUrl: (e) =>
+    @urlStatus.attr('data-state', 'loading')
+    @kayakoSubdomainAddon.attr('style', 'padding-right: 42px')
+    @linkErrorMessage.text('')
+
+    # get data
+    callback = =>
+      @ajax(
+        id:          'import_kayako_url'
+        type:        'POST'
+        url:         "#{@apiPath}/import/kayako/url_check"
+        data:        JSON.stringify(url: "https://#{@kayakoSubdomain.val()}#{@kayakoDomain}")
+        processData: true
+        success:     (data, status, xhr) =>
+
+          # validate form
+          if data.result is 'ok'
+            @urlStatus.attr('data-state', 'success')
+            @linkErrorMessage.text('')
+            @nextEnterCredentials.removeClass('hide')
+          else
+            @urlStatus.attr('data-state', 'error')
+            @linkErrorMessage.text( data.message_human || data.message)
+            @nextEnterCredentials.addClass('hide')
+
+      )
+    @delay( callback, 700, 'import_kayako_url' )
+
+  updateCredentials: (e) =>
+    @urlStatus.attr('data-state', 'loading')
+    @apiTokenErrorMessage.text('')
+
+    # get data
+    callback = =>
+      @ajax(
+        id:          'import_kayako_api_token'
+        type:        'POST'
+        url:         "#{@apiPath}/import/kayako/credentials_check"
+        data:        JSON.stringify(username: @kayakoEmail.val(), password: @kayakoPassword.val())
+        processData: true
+        success:     (data, status, xhr) =>
+
+          # validate form
+          if data.result is 'ok'
+            @urlStatus.attr('data-state', 'success')
+            @apiTokenErrorMessage.text('')
+            @nextStartMigration.removeClass('hide')
+          else
+            @urlStatus.attr('data-state', 'error')
+            @apiTokenErrorMessage.text(data.message_human || data.message)
+            @nextStartMigration.addClass('hide')
+
+      )
+    @delay(callback, 700, 'import_kayako_api_token')
+
+  showCredentials: (e) =>
+    e.preventDefault()
+    @urlStatus.attr('data-state', '')
+    @$('[data-slide=kayako-subdomain]').toggleClass('hide')
+    @$('[data-slide=kayako-credentials]').toggleClass('hide')
+
+  showImportState: =>
+    @$('[data-slide=kayako-subdomain]').addClass('hide')
+    @$('[data-slide=kayako-credentials]').addClass('hide')
+    @$('[data-slide=kayako-import]').removeClass('hide')
+
+  startMigration: (e) =>
+    e.preventDefault()
+    @showImportState()
+    @ajax(
+      id:          'import_start'
+      type:        'POST'
+      url:         "#{@apiPath}/import/kayako/import_start"
+      processData: true
+      success:     (data, status, xhr) =>
+
+        # validate form
+        if data.result is 'ok'
+          @delay(@updateMigration, 3000)
+    )
+
+  updateMigration: =>
+    @updateMigrationDisplayLoop += 1
+    @showImportState()
+    @ajax(
+      id:          'import_status'
+      type:        'GET'
+      url:         "#{@apiPath}/import/kayako/import_status"
+      processData: true
+      success:     (data, status, xhr) =>
+
+        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 !_.isEmpty(data.result['error'])
+          @$('.js-error').removeClass('hide')
+          @$('.js-error').html(App.i18n.translateContent(data.result['error']))
+        else
+          @$('.js-error').addClass('hide')
+
+        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
+
+            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')
+        @delay(@updateMigration, 5000)
+    )
+
+App.Config.set('import/kayako', ImportKayako, 'Routes')
+App.Config.set('kayako', {
+  title: 'Kayako'
+  name:  'Kayako'
+  class: 'js-kayako'
+  url:   '#import/kayako'
+}, 'ImportPlugins')

+ 113 - 0
app/assets/javascripts/app/views/import/kayako.jst.eco

@@ -0,0 +1,113 @@
+<div class="main flex vertical centered darkBackground">
+  <%- @Icon('full-logo', 'wizard-logo') %>
+  <div class="import wizard">
+    <div class="wizard-slide vertical" data-slide="kayako-subdomain">
+      <h2><%- @T('%s URL', 'Kayako') %></h2>
+      <div class="wizard-body flex vertical justified">
+        <p>
+          <%- @T('Enter the Subdomain of your %s system', 'Kayako') %>:
+        </p>
+        <div class="form-group">
+          <label for="kayako-subdomain"><%- @T('%s Subdomain', 'Kayako') %></label>
+          <div class="u-positionOrigin">
+            <div class="input-group">
+              <input type="text" id="kayako-subdomain" class="form-control" placeholder="example" name="kayako-subdomain" aria-describedby="kayako-subdomain-addon">
+              <span class="input-group-addon" id="kayako-subdomain-addon"><%- @kayakoDomain %></span>
+            </div>
+            <div class="input-feedback input-feedback--no-background centered">
+              <div class="small loading icon"></div>
+              <%- @Icon('diagonal-cross', 'icon-error') %>
+              <%- @Icon('checkmark') %>
+            </div>
+          </div>
+          <div class="error kayako-subdomain-error"></div>
+        </div>
+      </div>
+      <div class="wizard-controls horizontal center">
+        <a class="btn btn--text btn--secondary" href="#import"><%- @T('Go Back') %></a>
+        <div class="btn btn--primary align-right hide js-kayako-credentials" data-target="kayako-credentials"><%- @T('Enter credentials') %></div>
+      </div>
+    </div>
+
+    <div class="wizard-slide vertical hide" data-slide="kayako-credentials">
+      <h2><%- @T('%s credentials', 'Kayako') %></h2>
+      <div class="wizard-body flex vertical justified">
+        <p>
+          <%- @T('Enter your email address and password from your %s account which should be used for the import.', 'Kayako') %>
+        </p>
+        <p>
+          <%- @T('Attention: These will also your login password after the import is completed.') %>
+        </p>
+        <div class="form-group">
+          <label for="kayako-email"><%- @T('Email') %></label>
+          <div class="u-positionOrigin">
+            <input type="email" id="kayako-email" class="form-control" placeholder="admin@example.com" name="kayako-email">
+          </div>
+          <label for="kayako-password"><%- @T('Password') %></label>
+          <div class="u-positionOrigin">
+            <input type="password" id="kayako-password" class="form-control" name="kayako-password">
+            <div class="input-feedback centered">
+              <div class="small loading icon"></div>
+              <%- @Icon('diagonal-cross', 'icon-error') %>
+              <%- @Icon('checkmark') %>
+            </div>
+          </div>
+          <div class="error kayako-password-error"></div>
+        </div>
+      </div>
+      <div class="wizard-controls horizontal center">
+        <a class="btn btn--text btn--secondary" href="#import"><%- @T('Go Back') %></a>
+        <div class="btn btn--primary align-right hide js-migration-start" data-target="kayako-start-migration"><%- @T('Migrate %s Data', 'Kayako') %></div>
+      </div>
+    </div>
+
+    <div class="wizard-slide vertical hide" data-slide="kayako-import">
+      <h2><%- @T('%s Migration', 'Kayako') %></h2>
+      <div class="alert alert--danger hide js-error" role="alert"></div>
+      <div class="wizard-body flex vertical justified">
+        <table class="progressTable">
+          <tr class="js-groups">
+            <td><span class="js-done">-</span>/<span class="js-total">-</span>
+            <td><span><%- @T('Groups') %></span>
+            <td class="progressTable-progressCell">
+              <div class="horizontal center">
+                <div class="flex"><progress max="42" value="42"></progress></div>
+                <%- @Icon('checkmark') %>
+              </div>
+          </tr>
+          <tr class="js-organizations">
+            <td><span class="js-done">-</span>/<span class="js-total">-</span>
+            <td><span><%- @T('Organizations') %></span>
+            <td class="progressTable-progressCell">
+              <div class="horizontal center">
+                <div class="flex"><progress max="42" value="42"></progress></div>
+                <%- @Icon('checkmark') %>
+              </div>
+          </tr>
+          <tr class="js-users">
+            <td><span class="js-done">-</span>/<span class="js-total">-</span>
+            <td><span><%- @T('Users') %></span>
+            <td class="progressTable-progressCell">
+              <div class="horizontal center">
+                <div class="flex"><progress max="42" value="42"></progress></div>
+                <%- @Icon('checkmark') %>
+              </div>
+          </tr>
+          <tr class="js-tickets">
+            <td><span class="js-done">-</span>/<span class="js-total">-</span>
+            <td><span><%- @T('Tickets') %></span>
+            <td class="progressTable-progressCell">
+              <div class="horizontal center">
+                <div class="flex"><progress max="42" value="42"></progress></div>
+                <%- @Icon('checkmark') %>
+              </div>
+          </tr>
+        </table>
+      </div>
+      <div class="wizard-controls horizontal center">
+        <a href="#" class="btn btn--primary align-right hide js-finished"><%- @T('done') %></a>
+      </div>
+    </div>
+
+  </div>
+</div>

+ 146 - 0
app/controllers/import_kayako_controller.rb

@@ -0,0 +1,146 @@
+# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
+
+class ImportKayakoController < ApplicationController
+  def url_check
+    return if setup_done_response
+
+    url = params[:url]
+
+    # validate
+    if !valid_url_syntax?(url)
+      render json: {
+        result:  'invalid',
+        message: 'Invalid URL!',
+      }
+      return
+    end
+
+    endpoint = build_endpoint_url(url)
+
+    return if !valid_endpoint?(endpoint)
+
+    Setting.set('import_kayako_endpoint', endpoint)
+
+    render json: {
+      result: 'ok',
+      url:    url,
+    }
+  end
+
+  def credentials_check
+    return if setup_done_response
+
+    if !params[:username] || !params[:password]
+      render json: {
+        result:        'invalid',
+        message_human: 'Incomplete credentials',
+      }
+      return
+    end
+
+    save_endpoint_settings(params[:username], params[:password])
+
+    return if !valid_connection?
+
+    render json: {
+      result: 'ok',
+    }
+  end
+
+  def import_start
+    return if setup_done_response
+
+    Setting.set('import_mode', true)
+    Setting.set('import_backend', 'kayako')
+
+    job = ImportJob.create(name: 'Import::Kayako')
+    AsyncImportJob.perform_later(job)
+
+    render json: {
+      result: 'ok',
+    }
+  end
+
+  def import_status
+    job = ImportJob.find_by(name: 'Import::Kayako')
+
+    if job.finished_at.present?
+      Setting.reload
+    end
+
+    model_show_render_item(job)
+  end
+
+  private
+
+  def setup_done
+    count = User.all.count()
+    done = true
+    if count <= 2
+      done = false
+    end
+    done
+  end
+
+  def setup_done_response
+    if !setup_done
+      return false
+    end
+
+    render json: {
+      setup_done: true,
+    }
+    true
+  end
+
+  def valid_url_syntax?(url)
+    return false if url.blank? || url !~ %r{^(http|https)://.+?$}
+
+    true
+  end
+
+  def valid_endpoint?(endpoint)
+    response = UserAgent.request("#{endpoint}/teams", verify_ssl: true)
+
+    if response.header.nil? || !response.header['x-api-version']
+      render json: {
+        result:        'invalid',
+        message:       response.error.to_s,
+        message_human: 'Hostname not found!',
+      }
+      return false
+    end
+
+    true
+  end
+
+  def build_endpoint_url(url)
+    endpoint = "#{url}/api/v1"
+    endpoint.gsub(%r{([^:])//+}, '\\1/')
+  end
+
+  def valid_connection?
+    result = Sequencer.process('Import::Kayako::ConnectionTest')
+
+    if !result[:connected]
+      reset_endpoint_settings
+
+      render json: {
+        result:        'invalid',
+        message_human: 'Invalid credentials!',
+      }
+      return false
+    end
+
+    true
+  end
+
+  def save_endpoint_settings(username, possword)
+    Setting.set('import_kayako_endpoint_username', username)
+    Setting.set('import_kayako_endpoint_password', possword)
+  end
+
+  def reset_endpoint_settings
+    save_endpoint_settings(nil, nil)
+  end
+end

+ 12 - 0
config/routes/import_kayako.rb

@@ -0,0 +1,12 @@
+# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
+
+Zammad::Application.routes.draw do
+  api_path = Rails.configuration.api_path
+
+  # import kayako
+  match api_path + '/import/kayako/url_check',          to: 'import_kayako#url_check',         via: :post
+  match api_path + '/import/kayako/credentials_check',  to: 'import_kayako#credentials_check', via: :post
+  match api_path + '/import/kayako/import_start',       to: 'import_kayako#import_start',      via: :post
+  match api_path + '/import/kayako/import_status',      to: 'import_kayako#import_status',     via: :get
+
+end

+ 15 - 0
db/migrate/20210827095053_add_ticket_article_type_facebook_direct_message.rb

@@ -0,0 +1,15 @@
+# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
+
+class AddTicketArticleTypeFacebookDirectMessage < ActiveRecord::Migration[6.0]
+  def change
+    # return if it's a new setup
+    return if !Setting.exists?(name: 'system_init_done')
+
+    Ticket::Article::Type.create_if_not_exists(
+      name:          'facebook direct-message',
+      communication: true,
+      updated_by_id: 1,
+      created_by_id: 1,
+    )
+  end
+end

+ 55 - 0
db/seeds/settings.rb

@@ -3282,6 +3282,61 @@ Setting.create_if_not_exists(
   frontend:    false
 )
 
+Setting.create_if_not_exists(
+  title:       'Import Endpoint',
+  name:        'import_kayako_endpoint',
+  area:        'Import::Kayako',
+  description: 'Defines Kayako endpoint to import users, ticket, states and articles.',
+  options:     {
+    form: [
+      {
+        display: '',
+        null:    false,
+        name:    'import_kayako_endpoint',
+        tag:     'input',
+      },
+    ],
+  },
+  state:       'https://yours.kayako.com/api/v1',
+  frontend:    false
+)
+Setting.create_if_not_exists(
+  title:       'Import User for requesting the Kayako API',
+  name:        'import_kayako_endpoint_username',
+  area:        'Import::Kayako',
+  description: 'Defines Kayako endpoint authentication user.',
+  options:     {
+    form: [
+      {
+        display: '',
+        null:    false,
+        name:    'import_kayako_endpoint_username',
+        tag:     'input',
+      },
+    ],
+  },
+  state:       '',
+  frontend:    false
+)
+Setting.create_if_not_exists(
+  title:       'Import Password for requesting the Kayako API',
+  name:        'import_kayako_endpoint_password',
+  area:        'Import::Kayako',
+  description: 'Defines Kayako endpoint authentication password.',
+  options:     {
+    form: [
+      {
+        display: '',
+        null:    false,
+        name:    'import_kayako_endpoint_password',
+        tag:     'input',
+      },
+    ],
+  },
+  state:       '',
+  frontend:    false
+)
+
 Setting.create_if_not_exists(
   title:       'Import Backends',
   name:        'import_backends',

+ 1 - 0
db/seeds/ticket_article_types.rb

@@ -12,3 +12,4 @@ Ticket::Article::Type.create_if_not_exists(id: 9, name: 'facebook feed comment',
 Ticket::Article::Type.create_if_not_exists(id: 10, name: 'note', communication: false)
 Ticket::Article::Type.create_if_not_exists(id: 11, name: 'web', communication: true)
 Ticket::Article::Type.create_if_not_exists(id: 12, name: 'telegram personal-message', communication: true)
+Ticket::Article::Type.create_if_not_exists(id: 13, name: 'facebook direct-message', communication: true)

+ 15 - 0
lib/import/kayako.rb

@@ -0,0 +1,15 @@
+# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
+
+module Import
+  class Kayako < Import::Base
+    include Import::Mixin::Sequence
+
+    def start
+      process
+    end
+
+    def sequence_name
+      'Import::Kayako::Full'
+    end
+  end
+end

Некоторые файлы не были показаны из-за большого количества измененных файлов