Browse Source

Fixes #2074 - Ability of deleting customers and / or all ticket at once.

Rolf Schmidt 4 years ago
parent
commit
3fb9b05027

+ 6 - 4
app/assets/javascripts/app/controllers/_application_controller_generic.coffee

@@ -12,6 +12,7 @@ class App.ControllerGenericNew extends App.ControllerModal
       params:    @item
       screen:    @screen || 'edit'
       autofocus: true
+      handlers: @handlers
     )
     @controller.form
 
@@ -57,10 +58,11 @@ class App.ControllerGenericEdit extends App.ControllerModal
     @head = @pageData.head || @pageData.object
 
     @controller = new App.ControllerForm(
-      model:      App[ @genericObject ]
-      params:     @item
-      screen:     @screen || 'edit'
-      autofocus:  true
+      model:     App[ @genericObject ]
+      params:    @item
+      screen:    @screen || 'edit'
+      autofocus: true
+      handlers:  @handlers
     )
     @controller.form
 

+ 267 - 0
app/assets/javascripts/app/controllers/data_privacy.coffee

@@ -0,0 +1,267 @@
+class Index extends App.ControllerSubContent
+  requiredPermission: 'admin.data_privacy'
+  header: 'Data Privacy'
+  events:
+    'click .js-new':         'new'
+    'click .js-description': 'description'
+    'click .js-toggle-tickets': 'toggleTickets'
+
+  constructor: ->
+    super
+    @load()
+    @subscribeDataPrivacyTaskId = App.DataPrivacyTask.subscribe(@render)
+
+  load: =>
+    callback = =>
+      @stopLoading()
+      @render()
+    @startLoading()
+    App.DataPrivacyTask.fetchFull(
+      callback
+      clear: true
+    )
+
+  show: (params) =>
+    for key, value of params
+      if key isnt 'el' && key isnt 'shown' && key isnt 'match'
+        @[key] = value
+
+    if params.integration
+
+      # we reuse the integration parameter
+      # because there is no own route possible
+      # (see manage.coffee)
+      @user_id = params.integration
+      @navigate '#system/data_privacy'
+      return
+
+    if @user_id
+      @new(false, @user_id)
+      @user_id = undefined
+
+  render: =>
+    runningTasks = App.DataPrivacyTask.search(
+      filter:
+        state: 'in process'
+      order:  'DESC'
+    )
+    runningTasksHTML = App.view('data_privacy/tasks')(
+      tasks: runningTasks
+    )
+
+    failedTasks = App.DataPrivacyTask.search(
+      filter:
+        state: 'failed'
+      order:  'DESC'
+    )
+    failedTasksHTML = App.view('data_privacy/tasks')(
+      tasks: failedTasks
+    )
+
+    completedTasks = App.DataPrivacyTask.search(
+      filter:
+        state: 'completed'
+      order:  'DESC'
+    )
+    completedTasksHTML = App.view('data_privacy/tasks')(
+      tasks: completedTasks
+    )
+
+    # show description button, only if content exists
+    description = marked(App.DataPrivacyTask.description)
+
+    @html App.view('data_privacy/index')(
+      taskCount: ( runningTasks.length + failedTasks.length + completedTasks.length )
+      runningTaskCount: runningTasks.length
+      failedTaskCount: failedTasks.length
+      completedTaskCount: completedTasks.length
+      runningTasksHTML: runningTasksHTML
+      failedTasksHTML: failedTasksHTML
+      completedTasksHTML: completedTasksHTML
+      description: description
+    )
+
+  release: =>
+    if @subscribeDataPrivacyTaskId
+      App.DataPrivacyTask.unsubscribe(@subscribeDataPrivacyTaskId)
+
+  new: (e, user_id = undefined) ->
+    if e
+      e.preventDefault()
+
+    new TaskNew(
+      pageData:
+        head: 'Deletion Task'
+        title: 'Deletion Task'
+        object: 'DataPrivacyTask'
+        objects: 'DataPrivacyTasks'
+      genericObject: 'DataPrivacyTask'
+      container:     @el.closest('.content')
+      callback:      @load
+      large:         true
+      handlers: [@formHandler]
+      item:
+        'deletable_id': user_id
+    )
+
+  toggleTickets: (e) ->
+    e.preventDefault()
+
+    id       = $(e.target).data('id')
+    type     = $(e.target).data('type')
+    expanded = $(e.target).hasClass('expanded')
+    return if !id
+
+    new_expanded = ''
+    text         = 'See more'
+    if !expanded
+      new_expanded = ' expanded'
+      text         = 'See less'
+
+    task = App.DataPrivacyTask.find(id)
+
+    list = clone(task.preferences[type])
+    if expanded
+      list = list.slice(0, 50)
+      list.push('...')
+    list = list.join(', ')
+
+    $(e.target).closest('div.ticket-list').html(list + ' <br><div class="btn btn--text js-toggle-tickets' + new_expanded + '" data-type="' + type + '" data-id="' + id + '">' + App.i18n.translateInline(text) + '</div>')
+
+  description: (e) =>
+    new App.ControllerGenericDescription(
+      description: App.DataPrivacyTask.description
+      container:   @el.closest('.content')
+    )
+
+  formHandler: (params, attribute, attributes, classname, form, ui) ->
+    return if !attribute
+
+    userID = params['deletable_id']
+    if userID
+      $('body').find('.js-TaskNew').removeClass('hidden')
+    else
+      $('body').find('.js-TaskNew').addClass('hidden')
+      form.find('.js-preview').remove()
+
+    return if !userID
+
+    conditionCustomer =
+      'condition':
+        'ticket.customer_id':
+          'operator': 'is'
+          'pre_condition':'specific'
+          'value': userID
+
+    conditionOwner =
+      'condition':
+        'ticket.owner_id':
+          'operator': 'is'
+          'pre_condition':'specific'
+          'value': userID
+
+    App.Ajax.request(
+      id:    'ticket_selector'
+      type:  'POST'
+      url:   "#{App.Config.get('api_path')}/tickets/selector"
+      data:        JSON.stringify(conditionCustomer)
+      processData: true,
+      success: (dataCustomer, status, xhr) ->
+        App.Collection.loadAssets(dataCustomer.assets)
+
+        App.Ajax.request(
+          id:    'ticket_selector'
+          type:  'POST'
+          url:   "#{App.Config.get('api_path')}/tickets/selector"
+          data:        JSON.stringify(conditionOwner)
+          processData: true,
+          success: (dataOwner, status, xhr) ->
+            App.Collection.loadAssets(dataOwner.assets)
+
+            user               = App.User.find(userID)
+            deleteOrganization = ''
+            if user.organization_id
+              organization = App.Organization.find(user.organization_id)
+              if organization && organization.member_ids.length < 2
+                attribute          = { name: 'preferences::delete_organization',  display: 'Delete organization?', tag: 'boolean', default: true, translate: true }
+                deleteOrganization = ui.formGenItem(attribute, classname, form).html()
+
+            sure_attribute = { name: 'preferences::sure',  display: 'Are you sure?', tag: 'input', translate: false, placeholder: App.i18n.translateInline('delete').toUpperCase() }
+            sureInput      = ui.formGenItem(sure_attribute, classname, form).html()
+
+            preview_html = App.view('data_privacy/preview')(
+              customer_count:           dataCustomer.ticket_count || 0
+              owner_count:              dataOwner.ticket_count    || 0
+              delete_organization_html: deleteOrganization
+              sure_html:                sureInput
+              user_id:                  userID
+            )
+
+            if form.find('.js-preview').length < 1
+              form.append(preview_html)
+            else
+              form.find('.js-preview').replaceWith(preview_html)
+
+            new App.TicketList(
+              tableId:    'ticket-selector'
+              el:         form.find('.js-previewTableCustomer')
+              ticket_ids: dataCustomer.ticket_ids
+            )
+            new App.TicketList(
+              tableId:    'ticket-selector'
+              el:         form.find('.js-previewTableOwner')
+              ticket_ids: dataOwner.ticket_ids
+            )
+        )
+    )
+
+class TaskNew extends App.ControllerGenericNew
+  buttonSubmit: 'Delete'
+  buttonClass: 'btn--danger js-TaskNew hidden'
+
+  content: ->
+    if @item['deletable_id']
+      @buttonClass = 'btn--danger js-TaskNew'
+    else
+      @buttonClass = 'btn--danger js-TaskNew hidden'
+
+    super
+
+  onSubmit: (e) ->
+    params = @formParam(e.target)
+    params['deletable_type'] = 'User'
+
+    object = new App[ @genericObject ]
+    object.load(params)
+
+    # validate
+    errors = object.validate()
+    if params['preferences']['sure'] isnt App.i18n.translateInline('delete').toUpperCase()
+      if !errors
+        errors = {}
+      errors['preferences::sure'] = 'invalid'
+
+    if errors
+      @log 'error', errors
+      @formValidate( form: e.target, errors: errors )
+      return false
+
+    # disable form
+    @formDisable(e)
+
+    # save object
+    ui = @
+    object.save(
+      done: ->
+        if ui.callback
+          item = App[ ui.genericObject ].fullLocal(@id)
+          ui.callback(item)
+        ui.close()
+
+      fail: (settings, details) ->
+        ui.log 'errors', details
+        ui.formEnable(e)
+        ui.controller.showAlert(details.error_human || details.error || 'Unable to create object!')
+    )
+
+App.Config.set('DataPrivacy', { prio: 3600, name: 'Data Privacy', parent: '#system', target: '#system/data_privacy', controller: Index, permission: ['admin.data_privacy'] }, 'NavBarAdmin')

+ 1 - 1
app/assets/javascripts/app/controllers/manage.coffee

@@ -30,4 +30,4 @@ App.Config.set('system/:target/:integration', ManageRouter, 'Routes')
 App.Config.set('Manage', { prio: 1000, name: 'Manage', target: '#manage', permission: ['admin.*'] }, 'NavBarAdmin')
 App.Config.set('Channels', { prio: 2500, name: 'Channels', target: '#channels', permission: ['admin.*'] }, 'NavBarAdmin')
 App.Config.set('Settings', { prio: 7000, name: 'Settings', target: '#settings', permission: ['admin.*'] }, 'NavBarAdmin')
-App.Config.set('System', { prio: 8000, name: 'System', target: '#system', permission: ['admin.*'] }, 'NavBarAdmin')
+App.Config.set('System', { prio: 8000, name: 'System', target: '#system', permission: ['admin.*'] }, 'NavBarAdmin')

+ 15 - 0
app/assets/javascripts/app/controllers/taskbar_widget.coffee

@@ -14,6 +14,17 @@ class App.TaskbarWidget extends App.CollectionController
   constructor: ->
     super
 
+    App.Event.bind(
+      'Taskbar:destroy'
+      (data, event) =>
+        task = App.Taskbar.find(data.id)
+        return if !task
+        return if !task.key
+
+        @removeTask(task.key)
+      'Collection::Subscribe::Taskbar'
+    )
+
     dndOptions =
       tolerance:            'pointer'
       distance:             15
@@ -80,6 +91,10 @@ class App.TaskbarWidget extends App.CollectionController
           event: e
         )
         return
+    @removeTask(key)
+
+  removeTask: (key = false) =>
+    return if !key
 
     # check if active task is closed
     currentTask    = App.TaskManager.get(key)

+ 9 - 0
app/assets/javascripts/app/controllers/ticket_zoom/sidebar_customer.coffee

@@ -25,6 +25,15 @@ class SidebarCustomer extends App.Controller
           name:     'customer-edit'
           callback: @editCustomer
         }
+
+    if @permissionCheck('admin.data_privacy')
+      @item.sidebarActions.push {
+        title:    'Delete Customer'
+        name:     'customer-delete'
+        callback: =>
+          @navigate "#system/data_privacy/#{@ticket.customer_id}"
+      }
+
     @item
 
   metaBadge: (user) =>

+ 8 - 0
app/assets/javascripts/app/controllers/user_profile.coffee

@@ -149,6 +149,14 @@ class ActionRow extends App.ObserverActionRow
           callback: @resendVerificationEmail
         })
 
+    if @permissionCheck('admin.data_privacy')
+      actions.push {
+        title:    'Delete'
+        name:     'delete'
+        callback: =>
+          @navigate "#system/data_privacy/#{user.id}"
+      }
+
     actions
 
 class Object extends App.ObserverController

+ 32 - 31
app/assets/javascripts/app/controllers/users.coffee

@@ -55,28 +55,6 @@ class Index extends App.ControllerSubContent
   renderResult: (user_ids = []) ->
     @stopLoading()
 
-    callbackHeader = (header) ->
-      attribute =
-        name:         'switch_to'
-        display:      'Action'
-        className:    'actionCell'
-        translation:  true
-        width:        '250px'
-        displayWidth: 250
-        unresizable:  true
-      header.push attribute
-      header
-
-    callbackAttributes = (value, object, attribute, header) ->
-      text                  = App.i18n.translateInline('View from user\'s perspective')
-      value                 = ' '
-      attribute.raw         = ' <span class="btn btn--primary btn--small btn--slim switchView" title="' + text + '">' + App.Utils.icon('switchView') + '<span>' + text + '</span></span>'
-      attribute.class       = ''
-      attribute.parentClass = 'actionCell no-padding'
-      attribute.link        = ''
-      attribute.title       = App.i18n.translateInline('Switch to')
-      value
-
     switchTo = (id,e) =>
       e.preventDefault()
       e.stopPropagation()
@@ -129,15 +107,38 @@ class Index extends App.ControllerSubContent
       model:   App.User
       objects: users
       class:   'user-list'
-      callbackHeader: [callbackHeader]
-      callbackAttributes:
-        switch_to: [
-          callbackAttributes
-        ]
-      bindCol:
-        switch_to:
-          events:
-            'click': switchTo
+      customActions: [
+        {
+          name: 'switchTo'
+          display: 'View from user\'s perspective'
+          icon: 'switchView '
+          class: 'create js-switchTo'
+          callback: (id) =>
+            @disconnectClient()
+            $('#app').hide().attr('style', 'display: none!important')
+            @delay(
+              =>
+                App.Auth._logout(false)
+                @ajax(
+                  id:          'user_switch'
+                  type:        'GET'
+                  url:         "#{@apiPath}/sessions/switch/#{id}"
+                  success:     (data, status, xhr) =>
+                    location = "#{window.location.protocol}//#{window.location.host}#{data.location}"
+                    @windowReload(undefined, location)
+                )
+              800
+            )
+        }
+        {
+          name: 'delete'
+          display: 'Delete'
+          icon: 'trash'
+          class: 'delete'
+          callback: (id) =>
+            @navigate "#system/data_privacy/#{id}"
+        },
+      ]
       bindRow:
         events:
           'click': edit

+ 30 - 0
app/assets/javascripts/app/models/data_privacy_task.coffee

@@ -0,0 +1,30 @@
+class App.DataPrivacyTask extends App.Model
+  @configure 'DataPrivacyTask', 'name', 'state', 'deletable_id', 'deletable_type', 'preferences'
+  @extend Spine.Model.Ajax
+  @url: @apiPath + '/data_privacy_tasks'
+  @configure_attributes = [
+    { name: 'deletable_id',   display: 'User',            tag: 'autocompletion_ajax', relation: 'User', do_not_log: true },
+    { name: 'state',          display: 'State',           tag: 'input', readonly: 1 },
+    { name: 'created_by_id',  display: 'Created by',      relation: 'User', readonly: 1 },
+    { name: 'created_at',     display: 'Created',         tag: 'datetime', readonly: 1 },
+    { name: 'updated_by_id',  display: 'Updated by',      relation: 'User', readonly: 1 },
+    { name: 'updated_at',     display: 'Updated',         tag: 'datetime', readonly: 1 },
+  ]
+  @configure_overview = []
+
+  @description = '''
+** Data Privacy **, helps you to delete and verify the removal of existing data of the system.
+
+It can be used to delete tickets, organizations and users. The owner assignment will be unset in case the deleted user is an agent.
+
+Data Privacy tasks will be executed every 10 minutes. The execution might take some additional time depending of the number of objects that should get deleted.
+'''
+
+  activityMessage: (item) ->
+    if item.type is 'create'
+      return App.i18n.translateContent('%s created data privacy task to delete user id |%s|', item.created_by.displayName(), item.objectNative.deletable_id)
+    else if item.type is 'update'
+      return App.i18n.translateContent('%s updated data privacy task to delete user id |%s|', item.created_by.displayName(), item.objectNative.deletable_id)
+    else if item.type is 'completed'
+      return App.i18n.translateContent('%s completed data privacy task to delete user id |%s|', item.created_by.displayName(), item.objectNative.deletable_id)
+    return "Unknow action for (#{@objectDisplayName()}/#{item.type}), extend activityMessage() of model."

+ 33 - 0
app/assets/javascripts/app/views/data_privacy/index.jst.eco

@@ -0,0 +1,33 @@
+<div class="page-header">
+  <div class="page-header-title">
+    <h1><%- @T('Data Privacy') %> <small><%- @T('Management') %></small></h1>
+  </div>
+
+  <div class="page-header-meta">
+    <a class="btn js-description"><%- @T('Description') %></a>
+    <a class="btn btn--success js-new"><%- @T('New Deletion Task') %></a>
+  </div>
+</div>
+
+<div class="page-content">
+<% if @taskCount < 1: %>
+  <div class="page-description">
+    <%- @description %>
+  </div>
+  <% else: %>
+  <% if @runningTaskCount: %>
+  <h2><%- @T('Running Tasks') %></h2>
+  <%- @runningTasksHTML %>
+  <% end %>
+  <% if @failedTaskCount: %>
+  <div class="spacer"></div>
+  <h2><%- @T('Failed Tasks') %></h2>
+  <%- @failedTasksHTML %>
+  <% end %>
+  <% if @completedTaskCount: %>
+  <div class="spacer"></div>
+  <h2><%- @T('Completed Tasks') %></h2>
+  <%- @completedTasksHTML %>
+  <% end %>
+<% end %>
+</div>

+ 20 - 0
app/assets/javascripts/app/views/data_privacy/preview.jst.eco

@@ -0,0 +1,20 @@
+<div class="js-preview" data-userid="<%= @user_id %>">
+  <div class="form-group js-deleteOrganzation">
+    <%- @delete_organization_html %>
+  </div>
+  <div class="form-group">
+      <h3><%- @T('Preview customer tickets') %> <span class="subtitle js-previewCounterContainer">(<span class="js-previewCounter"><%= @customer_count %></span> <%- @T('matches') %>)</span></h3>
+      <p><%- @T('Customer tickets of the user will get deleted on execution of the task. No rollback possible.') %></p>
+      <div class="js-previewTableCustomer"></div>
+    <% if @owner_count > 0: %>
+      <h3><%- @T('Preview owner tickets') %><span class="subtitle js-previewCounterContainer"> <span class="u-highlight js-previewCounter"><%= @owner_count %></span> <%- @T('matches') %></span></h3>
+      <p><%- @T('Owner tickets of the user will not get deleted. The owner will be mapped to the system user (ID 1).') %></p>
+      <div class="js-previewTableOwner"></div>
+    <% end %>
+  </div>
+  <div class="form-group js-sure">
+    <h3 class="danger-color"><%- @T('Warning') %></h3>
+    <p class="danger-color"><%- @T('There is no rollback of this deletion possible. If you are absolutely sure to do this, then type in "%s" into the input.', App.i18n.translateInline('delete').toUpperCase()) %></p>
+    <%- @sure_html %>
+  </div>
+</div>

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