Fixes #2074 - Ability of deleting customers and / or all ticket at once.
This commit is contained in:
parent
8749190a51
commit
3fb9b05027
60 changed files with 1684 additions and 163 deletions
|
@ -12,6 +12,7 @@ class App.ControllerGenericNew extends App.ControllerModal
|
|||
params: @item
|
||||
screen: @screen || 'edit'
|
||||
autofocus: true
|
||||
handlers: @handlers
|
||||
)
|
||||
@controller.form
|
||||
|
||||
|
@ -61,6 +62,7 @@ class App.ControllerGenericEdit extends App.ControllerModal
|
|||
params: @item
|
||||
screen: @screen || 'edit'
|
||||
autofocus: true
|
||||
handlers: @handlers
|
||||
)
|
||||
@controller.form
|
||||
|
||||
|
|
267
app/assets/javascripts/app/controllers/data_privacy.coffee
Normal file
267
app/assets/javascripts/app/controllers/data_privacy.coffee
Normal file
|
@ -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')
|
|
@ -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)
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
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}"
|
||||
},
|
||||
]
|
||||
bindCol:
|
||||
switch_to:
|
||||
events:
|
||||
'click': switchTo
|
||||
bindRow:
|
||||
events:
|
||||
'click': edit
|
||||
|
|
30
app/assets/javascripts/app/models/data_privacy_task.coffee
Normal file
30
app/assets/javascripts/app/models/data_privacy_task.coffee
Normal file
|
@ -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
app/assets/javascripts/app/views/data_privacy/index.jst.eco
Normal file
33
app/assets/javascripts/app/views/data_privacy/index.jst.eco
Normal file
|
@ -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>
|
|
@ -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>
|
54
app/assets/javascripts/app/views/data_privacy/tasks.jst.eco
Normal file
54
app/assets/javascripts/app/views/data_privacy/tasks.jst.eco
Normal file
|
@ -0,0 +1,54 @@
|
|||
<% if @tasks.length > 0: %>
|
||||
<% for task in @tasks: %>
|
||||
<% if task.preferences.user: %>
|
||||
<div class="action" data-id="<%- task.id %>">
|
||||
<div class="action-row">
|
||||
<div class="action-flow action-flow--noWrap">
|
||||
<h2><%- @T('Delete User') %></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-flow action-flow--row">
|
||||
<div class="action-block action-block--flex">
|
||||
<div class="label"><%- @T('User (censored)') %>:</div>
|
||||
<%= task.preferences.user.firstname %> <%= task.preferences.user.lastname %> (<%= task.preferences.user.email %>)
|
||||
<% if task.preferences.user.organization && task.preferences.delete_organization: %>
|
||||
<br><br>
|
||||
<div class="label"><%- @T('Deleted Organization') %>:</div>
|
||||
<%= task.preferences.user.organization %>
|
||||
<% end %>
|
||||
<br><br>
|
||||
<div class="label"><%- @T('Started') %></div>
|
||||
<%- @humanTime(task.created_at) %>
|
||||
<br><br>
|
||||
<div class="label"><%- @T('State') %></div>
|
||||
<% if task.state: %><%= task.state %><% else: %><%- @T('in process') %><% end %>
|
||||
<% if task.preferences.error: %> (<%= task.preferences.error %>)<% end %>
|
||||
</div>
|
||||
<div class="action-block action-block--flex">
|
||||
<div class="label"><%- @T('Deleted tickets (%s in total)', task.preferences.customer_tickets.length) %>:</div>
|
||||
<div class="ticket-list">
|
||||
<% if task.preferences.customer_tickets.length > 0: %>
|
||||
<%= task.preferences.customer_tickets.slice(0, 50).join(', ') %><% if task.preferences.customer_tickets.length > 50: %>, ... <br><div href="#" class="btn btn--text js-toggle-tickets" data-type="customer_tickets" data-id="<%= task.id %>"><%- @T('See more') %></div><% end %>
|
||||
<% else: %>
|
||||
-
|
||||
<% end %>
|
||||
</div>
|
||||
<br><br>
|
||||
<div class="label"><%- @T('Previously owned tickets (%s in total)', task.preferences.owner_tickets.length) %>:</div>
|
||||
<div class="ticket-list">
|
||||
<% if task.preferences.owner_tickets.length > 0: %>
|
||||
<%= task.preferences.owner_tickets.slice(0, 50).join(', ') %><% if task.preferences.owner_tickets.length > 50: %>, ... <br><div href="#" class="btn btn--text js-toggle-tickets" data-type="owner_tickets" data-id="<%= task.id %>"><%- @T('See more') %></div><% end %>
|
||||
<% else: %>
|
||||
-
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% else: %>
|
||||
<div class="action action--placeholder">
|
||||
<%- @T('None') %>
|
||||
</div>
|
||||
<% end %>
|
|
@ -1,6 +1,6 @@
|
|||
<div class="horizontal-filters js-filter">
|
||||
</div>
|
||||
<div class="js-preview <% if @attribute.preview is false: %>hide<% end %>">
|
||||
<h3><%- @T('Preview') %><span class="subtitle js-previewCounterContainer hide"> <span class="u-highlight js-previewCounter">?</span> <%- @T('matches') %></span> <span class="tiny loading icon js-previewLoader hide" style="margin-left: 3px;"></span></h3>
|
||||
<h3><%- @T('Preview') %><span class="subtitle js-previewCounterContainer hide"> (<span class="js-previewCounter">?</span> <%- @T('matches') %></span>) <span class="tiny loading icon js-previewLoader hide" style="margin-left: 3px;"></span></h3>
|
||||
<div class="js-previewTable"></div>
|
||||
</div>
|
|
@ -4,6 +4,7 @@ $ok-color: hsl(41,100%,49%);
|
|||
$bad-color: hsl(30,93%,50%);
|
||||
$superbad-color: hsl(19,90%,51%);
|
||||
$ghost-color: hsl(0,0%,80%);
|
||||
$danger-color: hsl(0,65%,55%);
|
||||
|
||||
$task-state-closed-color: $supergood-color;
|
||||
$task-state-pending-color: hsl(206,7%,28%);
|
||||
|
@ -584,15 +585,15 @@ pre code.hljs {
|
|||
|
||||
&--danger {
|
||||
color: white;
|
||||
background: hsl(0,65%,55%);
|
||||
background: $danger-color;
|
||||
|
||||
&:active {
|
||||
background: hsl(0,65%,45%);
|
||||
background: darken($danger-color, 10%);
|
||||
}
|
||||
|
||||
&.btn--secondary {
|
||||
background: white;
|
||||
color: hsl(0,65%,55%);
|
||||
color: $danger-color;
|
||||
|
||||
&:active {
|
||||
background: hsl(0,0%,98%);
|
||||
|
@ -658,7 +659,7 @@ pre code.hljs {
|
|||
}
|
||||
|
||||
&.btn--danger {
|
||||
color: hsl(0,65%,55%);
|
||||
color: $danger-color;
|
||||
|
||||
&:active {
|
||||
color: hsl(0,65%,40%);
|
||||
|
@ -5092,6 +5093,7 @@ footer {
|
|||
.ok-color { fill: $ok-color; }
|
||||
.bad-color { fill: $bad-color; }
|
||||
.superbad-color { fill: $superbad-color; }
|
||||
.danger-color { color: $danger-color; }
|
||||
|
||||
.stat-widgets {
|
||||
margin: -7px -7px 20px;
|
||||
|
@ -7563,7 +7565,7 @@ footer {
|
|||
|
||||
.dropdown-menu > li.danger:hover,
|
||||
.dropdown-menu > li.danger.is-active {
|
||||
background: hsl(0,65%,55%);
|
||||
background: $danger-color;
|
||||
}
|
||||
|
||||
.dropdown-menu > li.create:hover,
|
||||
|
@ -9483,6 +9485,17 @@ output {
|
|||
}
|
||||
}
|
||||
|
||||
&--placeholder {
|
||||
padding: 30px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: hsl(206,9%,69%);
|
||||
box-shadow: none;
|
||||
background: none;
|
||||
border-style: dashed;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
&-alert {
|
||||
width: calc(100% + 20px);
|
||||
margin: -10px -10px 10px;
|
||||
|
|
26
app/controllers/data_privacy_tasks_controller.rb
Normal file
26
app/controllers/data_privacy_tasks_controller.rb
Normal file
|
@ -0,0 +1,26 @@
|
|||
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
class DataPrivacyTasksController < ApplicationController
|
||||
prepend_before_action { authentication_check && authorize! }
|
||||
|
||||
def index
|
||||
model_index_render(DataPrivacyTask, params)
|
||||
end
|
||||
|
||||
def show
|
||||
model_show_render(DataPrivacyTask, params)
|
||||
end
|
||||
|
||||
def create
|
||||
model_create_render(DataPrivacyTask, params)
|
||||
end
|
||||
|
||||
def update
|
||||
model_update_render(DataPrivacyTask, params)
|
||||
end
|
||||
|
||||
def destroy
|
||||
model_destroy_render(DataPrivacyTask, params)
|
||||
end
|
||||
|
||||
end
|
|
@ -162,6 +162,11 @@ curl http://localhost/api/v1/monitoring/health_check?token=XXX
|
|||
issues.push "Stuck import backend '#{backend}' detected. Last update: #{job.updated_at}"
|
||||
end
|
||||
|
||||
# stuck data privacy tasks
|
||||
DataPrivacyTask.where.not(state: 'completed').where('updated_at <= ?', 30.minutes.ago).find_each do |task|
|
||||
issues.push "Stuck data privacy task (ID #{task.id}) detected. Last update: #{task.updated_at}"
|
||||
end
|
||||
|
||||
token = Setting.get('monitoring_token')
|
||||
|
||||
if issues.blank?
|
||||
|
|
7
app/jobs/data_privacy_task_job.rb
Normal file
7
app/jobs/data_privacy_task_job.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
class DataPrivacyTaskJob < ApplicationJob
|
||||
include HasActiveJobLock
|
||||
|
||||
def perform
|
||||
DataPrivacyTask.where(state: 'in process').find_each(&:perform)
|
||||
end
|
||||
end
|
|
@ -1,6 +1,8 @@
|
|||
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
class ApplicationModel < ActiveRecord::Base
|
||||
include ActiveModel::Validations
|
||||
|
||||
include ApplicationModel::CanActivityStreamLog
|
||||
include ApplicationModel::HasCache
|
||||
include ApplicationModel::CanLookup
|
||||
|
|
|
@ -3,8 +3,13 @@ module ApplicationModel::ChecksUserColumnsFillup
|
|||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_create :fill_up_user_create
|
||||
before_update :fill_up_user_update
|
||||
before_validation :fill_up_user_validate
|
||||
end
|
||||
|
||||
def fill_up_user_validate
|
||||
return fill_up_user_create if new_record?
|
||||
|
||||
fill_up_user_update
|
||||
end
|
||||
|
||||
=begin
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
class Chat::Agent < ApplicationModel
|
||||
|
||||
belongs_to :created_by, class_name: 'User'
|
||||
belongs_to :updated_by, class_name: 'User'
|
||||
|
||||
def seads_available
|
||||
concurrent - active_chat_count
|
||||
end
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
module ChecksClientNotification
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
|
@ -9,134 +10,132 @@ module ChecksClientNotification
|
|||
after_destroy :notify_clients_after_destroy
|
||||
end
|
||||
|
||||
=begin
|
||||
def notify_clients_data(event)
|
||||
class_name = self.class.name
|
||||
class_name.gsub!(/::/, '')
|
||||
|
||||
notify_clients_after_create after model got created
|
||||
{
|
||||
message: {
|
||||
event: "#{class_name}:#{event}",
|
||||
data: { id: id, updated_at: updated_at }
|
||||
},
|
||||
type: 'authenticated',
|
||||
}
|
||||
end
|
||||
|
||||
used as callback in model file
|
||||
def notify_clients_send(data)
|
||||
return notify_clients_send_to(data[:message]) if client_notification_send_to.present?
|
||||
|
||||
class OwnModel < ApplicationModel
|
||||
after_create :notify_clients_after_create
|
||||
after_update :notify_clients_after_update
|
||||
after_touch :notify_clients_after_touch
|
||||
after_destroy :notify_clients_after_destroy
|
||||
PushMessages.send(data)
|
||||
end
|
||||
|
||||
[...]
|
||||
|
||||
=end
|
||||
def notify_clients_send_to(data)
|
||||
client_notification_send_to.each do |user_id|
|
||||
PushMessages.send_to(send(user_id), data)
|
||||
end
|
||||
end
|
||||
|
||||
def notify_clients_after_create
|
||||
|
||||
# return if we run import mode
|
||||
return if Setting.get('import_mode')
|
||||
|
||||
# skip if ignored
|
||||
return if client_notification_events_ignored.include?(:create)
|
||||
|
||||
logger.debug { "#{self.class.name}.find(#{id}) notify created #{created_at}" }
|
||||
class_name = self.class.name
|
||||
class_name.gsub!(/::/, '')
|
||||
PushMessages.send(
|
||||
message: {
|
||||
event: class_name + ':create',
|
||||
data: { id: id, updated_at: updated_at }
|
||||
},
|
||||
type: 'authenticated',
|
||||
)
|
||||
|
||||
data = notify_clients_data(:create)
|
||||
notify_clients_send(data)
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
notify_clients_after_update after model got updated
|
||||
|
||||
used as callback in model file
|
||||
|
||||
class OwnModel < ApplicationModel
|
||||
after_create :notify_clients_after_create
|
||||
after_update :notify_clients_after_update
|
||||
after_touch :notify_clients_after_touch
|
||||
after_destroy :notify_clients_after_destroy
|
||||
|
||||
[...]
|
||||
|
||||
=end
|
||||
|
||||
def notify_clients_after_update
|
||||
|
||||
# return if we run import mode
|
||||
return if Setting.get('import_mode')
|
||||
|
||||
# skip if ignored
|
||||
return if client_notification_events_ignored.include?(:update)
|
||||
|
||||
logger.debug { "#{self.class.name}.find(#{id}) notify UPDATED #{updated_at}" }
|
||||
class_name = self.class.name
|
||||
class_name.gsub!(/::/, '')
|
||||
PushMessages.send(
|
||||
message: {
|
||||
event: class_name + ':update',
|
||||
data: { id: id, updated_at: updated_at }
|
||||
},
|
||||
type: 'authenticated',
|
||||
)
|
||||
|
||||
data = notify_clients_data(:update)
|
||||
notify_clients_send(data)
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
notify_clients_after_touch after model got touched
|
||||
|
||||
used as callback in model file
|
||||
|
||||
class OwnModel < ApplicationModel
|
||||
after_create :notify_clients_after_create
|
||||
after_update :notify_clients_after_update
|
||||
after_touch :notify_clients_after_touch
|
||||
after_destroy :notify_clients_after_destroy
|
||||
|
||||
[...]
|
||||
|
||||
=end
|
||||
|
||||
def notify_clients_after_touch
|
||||
|
||||
# return if we run import mode
|
||||
return if Setting.get('import_mode')
|
||||
|
||||
# skip if ignored
|
||||
return if client_notification_events_ignored.include?(:touch)
|
||||
|
||||
logger.debug { "#{self.class.name}.find(#{id}) notify TOUCH #{updated_at}" }
|
||||
class_name = self.class.name
|
||||
class_name.gsub!(/::/, '')
|
||||
PushMessages.send(
|
||||
message: {
|
||||
event: class_name + ':touch',
|
||||
data: { id: id, updated_at: updated_at }
|
||||
},
|
||||
type: 'authenticated',
|
||||
)
|
||||
|
||||
data = notify_clients_data(:touch)
|
||||
notify_clients_send(data)
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
notify_clients_after_destroy after model got destroyed
|
||||
|
||||
used as callback in model file
|
||||
|
||||
class OwnModel < ApplicationModel
|
||||
after_create :notify_clients_after_create
|
||||
after_update :notify_clients_after_update
|
||||
after_touch :notify_clients_after_touch
|
||||
after_destroy :notify_clients_after_destroy
|
||||
|
||||
[...]
|
||||
|
||||
=end
|
||||
def notify_clients_after_destroy
|
||||
|
||||
# return if we run import mode
|
||||
return if Setting.get('import_mode')
|
||||
|
||||
# skip if ignored
|
||||
return if client_notification_events_ignored.include?(:destroy)
|
||||
|
||||
logger.debug { "#{self.class.name}.find(#{id}) notify DESTOY #{updated_at}" }
|
||||
class_name = self.class.name
|
||||
class_name.gsub!(/::/, '')
|
||||
PushMessages.send(
|
||||
message: {
|
||||
event: class_name + ':destroy',
|
||||
data: { id: id, updated_at: updated_at }
|
||||
},
|
||||
type: 'authenticated',
|
||||
)
|
||||
|
||||
data = notify_clients_data(:destroy)
|
||||
notify_clients_send(data)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def client_notification_events_ignored
|
||||
@client_notification_events_ignored ||= self.class.instance_variable_get(:@client_notification_events_ignored) || []
|
||||
end
|
||||
|
||||
def client_notification_send_to
|
||||
@client_notification_send_to ||= self.class.instance_variable_get(:@client_notification_send_to) || []
|
||||
end
|
||||
|
||||
# methods defined here are going to extend the class, not the instance of it
|
||||
class_methods do
|
||||
|
||||
=begin
|
||||
|
||||
serve method to ignore events
|
||||
|
||||
class Model < ApplicationModel
|
||||
include ChecksClientNotification
|
||||
client_notification_events_ignored :create, :update, :touch
|
||||
end
|
||||
|
||||
=end
|
||||
|
||||
def client_notification_events_ignored(*attributes)
|
||||
@client_notification_events_ignored ||= []
|
||||
@client_notification_events_ignored |= attributes
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
serve method to define recipient user ids
|
||||
|
||||
class Model < ApplicationModel
|
||||
include ChecksClientNotification
|
||||
client_notification_send_to :user_id
|
||||
end
|
||||
|
||||
=end
|
||||
|
||||
def client_notification_send_to(*attributes)
|
||||
@client_notification_send_to ||= []
|
||||
@client_notification_send_to |= attributes
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -3,7 +3,6 @@ module HasObjectManagerAttributesValidation
|
|||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
include ActiveModel::Validations
|
||||
validates_with ObjectManager::Attribute::Validation, on: %i[create update]
|
||||
end
|
||||
end
|
||||
|
|
22
app/models/concerns/has_taskbars.rb
Normal file
22
app/models/concerns/has_taskbars.rb
Normal file
|
@ -0,0 +1,22 @@
|
|||
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||
module HasTaskbars
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_destroy :destroy_taskbars
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
destroy all taskbars for the class object id
|
||||
|
||||
model = Model.find(123)
|
||||
model.destroy
|
||||
|
||||
=end
|
||||
|
||||
def destroy_taskbars
|
||||
Taskbar.where(key: "#{self.class}-#{id}").destroy_all
|
||||
end
|
||||
|
||||
end
|
83
app/models/data_privacy_task.rb
Normal file
83
app/models/data_privacy_task.rb
Normal file
|
@ -0,0 +1,83 @@
|
|||
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
class DataPrivacyTask < ApplicationModel
|
||||
include DataPrivacyTask::HasActivityStreamLog
|
||||
include ChecksClientNotification
|
||||
|
||||
store :preferences
|
||||
|
||||
belongs_to :created_by, class_name: 'User'
|
||||
belongs_to :updated_by, class_name: 'User'
|
||||
|
||||
# optional because related data will get deleted and it would
|
||||
# cause validation errors if e.g. the created_by_id of the task
|
||||
# would need to get mapped by a deletion
|
||||
belongs_to :deletable, polymorphic: true, optional: true
|
||||
|
||||
before_create :prepare_deletion_preview
|
||||
|
||||
validates_with DataPrivacyTask::Validation
|
||||
|
||||
def perform
|
||||
return if deletable.blank?
|
||||
|
||||
prepare_deletion_preview
|
||||
save!
|
||||
|
||||
if delete_organization?
|
||||
deletable.organization.destroy
|
||||
else
|
||||
deletable.destroy
|
||||
end
|
||||
|
||||
update!(state: 'completed')
|
||||
rescue => e
|
||||
handle_exception(e)
|
||||
end
|
||||
|
||||
def handle_exception(e)
|
||||
Rails.logger.error e
|
||||
preferences[:error] = "ERROR: #{e.inspect}"
|
||||
self.state = 'failed'
|
||||
save!
|
||||
end
|
||||
|
||||
def delete_organization?
|
||||
return false if preferences[:delete_organization].blank?
|
||||
return false if preferences[:delete_organization] != 'true'
|
||||
return false if !deletable.organization
|
||||
return false if deletable.organization.members.count != 1
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def prepare_deletion_preview
|
||||
prepare_deletion_preview_tickets
|
||||
prepare_deletion_preview_user
|
||||
prepare_deletion_preview_organization
|
||||
prepare_deletion_preview_anonymize
|
||||
end
|
||||
|
||||
def prepare_deletion_preview_tickets
|
||||
preferences[:owner_tickets] = deletable.owner_tickets.order(id: 'DESC').map(&:number)
|
||||
preferences[:customer_tickets] = deletable.customer_tickets.order(id: 'DESC').map(&:number)
|
||||
end
|
||||
|
||||
def prepare_deletion_preview_user
|
||||
preferences[:user] = {
|
||||
firstname: deletable.firstname,
|
||||
lastname: deletable.lastname,
|
||||
email: deletable.email,
|
||||
}
|
||||
end
|
||||
|
||||
def prepare_deletion_preview_organization
|
||||
return if !deletable.organization
|
||||
|
||||
preferences[:user][:organization] = deletable.organization.name
|
||||
end
|
||||
|
||||
def prepare_deletion_preview_anonymize
|
||||
preferences[:user] = Pseudonymisation.of_hash(preferences[:user])
|
||||
end
|
||||
end
|
18
app/models/data_privacy_task/has_activity_stream_log.rb
Normal file
18
app/models/data_privacy_task/has_activity_stream_log.rb
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||
module DataPrivacyTask::HasActivityStreamLog
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
include ::HasActivityStreamLog
|
||||
after_update :log_activity
|
||||
|
||||
activity_stream_permission 'admin.data_privacy'
|
||||
end
|
||||
|
||||
def log_activity
|
||||
return if !saved_change_to_attribute?('state')
|
||||
return if state != 'completed'
|
||||
|
||||
activity_stream_log('completed', created_by_id, true)
|
||||
end
|
||||
end
|
101
app/models/data_privacy_task/validation.rb
Normal file
101
app/models/data_privacy_task/validation.rb
Normal file
|
@ -0,0 +1,101 @@
|
|||
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||
class DataPrivacyTask::Validation < ActiveModel::Validator
|
||||
|
||||
attr_reader :record
|
||||
|
||||
def validate(record)
|
||||
@record = record
|
||||
|
||||
check_for_user
|
||||
check_for_system_user
|
||||
check_for_current_user
|
||||
check_for_last_admin
|
||||
check_for_existing_task
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_for_user
|
||||
return if deletable_is_user?
|
||||
|
||||
invalid_because(:deletable, 'is not a User')
|
||||
end
|
||||
|
||||
def check_for_system_user
|
||||
return if !deletable_is_user?
|
||||
return if deletable.id != 1
|
||||
|
||||
invalid_because(:deletable, 'is undeletable system User with ID 1')
|
||||
end
|
||||
|
||||
def check_for_current_user
|
||||
return if !deletable_is_user?
|
||||
return if deletable.id != UserInfo.current_user_id
|
||||
|
||||
invalid_because(:deletable, 'is your current account')
|
||||
end
|
||||
|
||||
def check_for_last_admin
|
||||
return if !deletable_is_user?
|
||||
return if !last_admin?
|
||||
|
||||
invalid_because(:deletable, 'is last account with admin permissions')
|
||||
end
|
||||
|
||||
def check_for_existing_task
|
||||
return if !deletable_is_user?
|
||||
return if !tasks_exists?
|
||||
|
||||
invalid_because(:deletable, 'has an existing DataPrivacyTask queued')
|
||||
end
|
||||
|
||||
def deletable_is_user?
|
||||
deletable.is_a?(User)
|
||||
end
|
||||
|
||||
def deletable
|
||||
record.deletable
|
||||
end
|
||||
|
||||
def invalid_because(attribute, message)
|
||||
record.errors.add attribute, message
|
||||
end
|
||||
|
||||
def tasks_exists?
|
||||
DataPrivacyTask.where(
|
||||
deletable: deletable
|
||||
).where.not(
|
||||
id: record.id,
|
||||
state: 'failed'
|
||||
).exists?
|
||||
end
|
||||
|
||||
def last_admin?
|
||||
return false if !deletable_is_admin?
|
||||
|
||||
future_admin_ids.blank?
|
||||
end
|
||||
|
||||
def future_admin_ids
|
||||
other_admin_ids - existing_jobs_admin_ids
|
||||
end
|
||||
|
||||
def other_admin_ids
|
||||
admin_users.where.not(id: deletable.id).pluck(:id)
|
||||
end
|
||||
|
||||
def deletable_is_admin?
|
||||
admin_users.exists?(id: deletable.id)
|
||||
end
|
||||
|
||||
def existing_jobs_admin_ids
|
||||
DataPrivacyTask.where(
|
||||
deletable_id: other_admin_ids,
|
||||
deletable_type: 'User'
|
||||
).pluck(:deletable_id)
|
||||
end
|
||||
|
||||
def admin_users
|
||||
User.with_permissions('admin')
|
||||
end
|
||||
end
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
class Karma::ActivityLog < ApplicationModel
|
||||
belongs_to :object_lookup, optional: true
|
||||
belongs_to :user, class_name: '::User'
|
||||
|
||||
self.table_name = 'karma_activity_logs'
|
||||
|
||||
|
|
|
@ -9,12 +9,14 @@ class Organization < ApplicationModel
|
|||
include CanCsvImport
|
||||
include ChecksHtmlSanitized
|
||||
include HasObjectManagerAttributesValidation
|
||||
include HasTaskbars
|
||||
|
||||
include Organization::Assets
|
||||
include Organization::Search
|
||||
include Organization::SearchIndex
|
||||
|
||||
has_many :members, class_name: 'User'
|
||||
has_many :members, class_name: 'User', dependent: :destroy
|
||||
has_many :tickets, class_name: 'Ticket', dependent: :destroy
|
||||
|
||||
before_create :domain_cleanup
|
||||
before_update :domain_cleanup
|
||||
|
|
|
@ -6,6 +6,7 @@ class RecentView < ApplicationModel
|
|||
# rubocop:disable Rails/InverseOf
|
||||
belongs_to :ticket, foreign_key: 'o_id', optional: true
|
||||
belongs_to :object, class_name: 'ObjectLookup', foreign_key: 'recent_view_object_id', optional: true
|
||||
belongs_to :created_by, class_name: 'User'
|
||||
# rubocop:enable Rails/InverseOf
|
||||
|
||||
after_create :notify_clients
|
||||
|
|
|
@ -1,15 +1,24 @@
|
|||
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
class Taskbar < ApplicationModel
|
||||
include ChecksClientNotification
|
||||
|
||||
store :state
|
||||
store :params
|
||||
store :preferences
|
||||
|
||||
belongs_to :user
|
||||
|
||||
before_create :update_last_contact, :set_user, :update_preferences_infos
|
||||
before_update :update_last_contact, :set_user, :update_preferences_infos
|
||||
|
||||
after_update :notify_clients
|
||||
after_destroy :update_preferences_infos, :notify_clients
|
||||
|
||||
client_notification_events_ignored :create, :update, :touch
|
||||
|
||||
client_notification_send_to :user_id
|
||||
|
||||
attr_accessor :local_update
|
||||
|
||||
def state_changed?
|
||||
|
|
|
@ -3,6 +3,10 @@
|
|||
class Template < ApplicationModel
|
||||
include ChecksClientNotification
|
||||
|
||||
belongs_to :user, optional: true
|
||||
|
||||
store :options
|
||||
validates :name, presence: true
|
||||
|
||||
association_attributes_ignored :user
|
||||
end
|
||||
|
|
|
@ -5,6 +5,8 @@ class TextModule < ApplicationModel
|
|||
include ChecksHtmlSanitized
|
||||
include CanCsvImport
|
||||
|
||||
belongs_to :user, optional: true
|
||||
|
||||
validates :name, presence: true
|
||||
validates :content, presence: true
|
||||
|
||||
|
@ -17,6 +19,8 @@ class TextModule < ApplicationModel
|
|||
|
||||
has_and_belongs_to_many :groups, after_add: :cache_update, after_remove: :cache_update, class_name: 'Group'
|
||||
|
||||
association_attributes_ignored :user
|
||||
|
||||
=begin
|
||||
|
||||
load text modules from online
|
||||
|
|
|
@ -14,6 +14,7 @@ class Ticket < ApplicationModel
|
|||
include HasKarmaActivityLog
|
||||
include HasLinks
|
||||
include HasObjectManagerAttributesValidation
|
||||
include HasTaskbars
|
||||
|
||||
include Ticket::Escalation
|
||||
include Ticket::Subject
|
||||
|
@ -64,6 +65,7 @@ class Ticket < ApplicationModel
|
|||
belongs_to :organization, optional: true
|
||||
has_many :articles, class_name: 'Ticket::Article', after_add: :cache_update, after_remove: :cache_update, dependent: :destroy, inverse_of: :ticket
|
||||
has_many :ticket_time_accounting, class_name: 'Ticket::TimeAccounting', dependent: :destroy, inverse_of: :ticket
|
||||
has_many :flags, class_name: 'Ticket::Flag', dependent: :destroy
|
||||
belongs_to :state, class_name: 'Ticket::State', optional: true
|
||||
belongs_to :priority, class_name: 'Ticket::Priority', optional: true
|
||||
belongs_to :owner, class_name: 'User', optional: true
|
||||
|
@ -73,6 +75,8 @@ class Ticket < ApplicationModel
|
|||
belongs_to :create_article_type, class_name: 'Ticket::Article::Type', optional: true
|
||||
belongs_to :create_article_sender, class_name: 'Ticket::Article::Sender', optional: true
|
||||
|
||||
association_attributes_ignored :flags
|
||||
|
||||
self.inheritance_column = nil
|
||||
|
||||
attr_accessor :callback_loop
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
class Ticket::Flag < ApplicationModel
|
||||
belongs_to :ticket
|
||||
|
||||
association_attributes_ignored :ticket
|
||||
end
|
||||
|
|
|
@ -14,16 +14,32 @@ class User < ApplicationModel
|
|||
include HasRoles
|
||||
include HasObjectManagerAttributesValidation
|
||||
include HasTicketCreateScreenImpact
|
||||
include HasTaskbars
|
||||
include User::HasTicketCreateScreenImpact
|
||||
include User::Assets
|
||||
include User::Search
|
||||
include User::SearchIndex
|
||||
|
||||
has_and_belongs_to_many :organizations, after_add: :cache_update, after_remove: :cache_update, class_name: 'Organization'
|
||||
has_many :tokens, after_add: :cache_update, after_remove: :cache_update
|
||||
has_many :authorizations, after_add: :cache_update, after_remove: :cache_update
|
||||
belongs_to :organization, inverse_of: :members, optional: true
|
||||
has_many :tokens, after_add: :cache_update, after_remove: :cache_update, dependent: :destroy
|
||||
has_many :authorizations, after_add: :cache_update, after_remove: :cache_update, dependent: :destroy
|
||||
has_many :online_notifications, dependent: :destroy
|
||||
has_many :templates, dependent: :destroy
|
||||
has_many :taskbars, dependent: :destroy
|
||||
has_many :user_devices, dependent: :destroy
|
||||
has_one :chat_agent_created_by, class_name: 'Chat::Agent', foreign_key: :created_by_id, dependent: :destroy, inverse_of: :created_by
|
||||
has_one :chat_agent_updated_by, class_name: 'Chat::Agent', foreign_key: :updated_by_id, dependent: :destroy, inverse_of: :updated_by
|
||||
has_many :chat_sessions, class_name: 'Chat::Session', dependent: :destroy
|
||||
has_many :karma_user, class_name: 'Karma::User', dependent: :destroy
|
||||
has_many :karma_activity_logs, class_name: 'Karma::ActivityLog', dependent: :destroy
|
||||
has_many :cti_caller_ids, class_name: 'Cti::CallerId', dependent: :destroy
|
||||
has_many :text_modules, dependent: :destroy
|
||||
has_many :customer_tickets, class_name: 'Ticket', foreign_key: :customer_id, dependent: :destroy, inverse_of: :customer
|
||||
has_many :owner_tickets, class_name: 'Ticket', foreign_key: :owner_id, inverse_of: :owner
|
||||
has_many :created_recent_views, class_name: 'RecentView', foreign_key: :created_by_id, dependent: :destroy, inverse_of: :created_by
|
||||
has_many :permissions, -> { where(roles: { active: true }, active: true) }, through: :roles
|
||||
has_many :data_privacy_tasks, as: :deletable
|
||||
belongs_to :organization, inverse_of: :members, optional: true
|
||||
|
||||
before_validation :check_name, :check_email, :check_login, :ensure_uniq_email, :ensure_password, :ensure_roles, :ensure_identifier
|
||||
before_validation :check_mail_delivery_failed, on: :update
|
||||
|
@ -32,10 +48,12 @@ class User < ApplicationModel
|
|||
after_create :avatar_for_email_check, unless: -> { BulkImportInfo.enabled? }
|
||||
after_update :avatar_for_email_check, unless: -> { BulkImportInfo.enabled? }
|
||||
after_commit :update_caller_id
|
||||
before_destroy :destroy_longer_required_objects
|
||||
before_destroy :destroy_longer_required_objects, :destroy_move_dependency_ownership
|
||||
|
||||
store :preferences
|
||||
|
||||
association_attributes_ignored :online_notifications, :templates, :taskbars, :user_devices, :chat_sessions, :karma_activity_logs, :cti_caller_ids, :text_modules, :customer_tickets, :owner_tickets, :created_recent_views, :chat_agents, :data_privacy_tasks
|
||||
|
||||
activity_stream_permission 'admin.user'
|
||||
|
||||
activity_stream_attributes_ignored :last_login,
|
||||
|
@ -1147,20 +1165,39 @@ raise 'Minimum one user need to have admin permissions'
|
|||
end
|
||||
|
||||
def destroy_longer_required_objects
|
||||
::Authorization.where(user_id: id).destroy_all
|
||||
::Avatar.remove('User', id)
|
||||
::Cti::CallerId.where(user_id: id).destroy_all
|
||||
::Taskbar.where(user_id: id).destroy_all
|
||||
::Karma::ActivityLog.where(user_id: id).destroy_all
|
||||
::Karma::User.where(user_id: id).destroy_all
|
||||
::OnlineNotification.where(user_id: id).destroy_all
|
||||
::RecentView.where(created_by_id: id).destroy_all
|
||||
::Avatar.remove(self.class.to_s, id)
|
||||
::UserDevice.remove(id)
|
||||
::Token.where(user_id: id).destroy_all
|
||||
::StatsStore.remove(
|
||||
object: 'User',
|
||||
object: self.class.to_s,
|
||||
o_id: id,
|
||||
)
|
||||
end
|
||||
|
||||
def destroy_move_dependency_ownership
|
||||
result = Models.references(self.class.to_s, id)
|
||||
|
||||
result.each do |class_name, references|
|
||||
next if class_name.blank?
|
||||
next if references.blank?
|
||||
|
||||
ref_class = class_name.constantize
|
||||
references.each do |column, reference_found|
|
||||
next if !reference_found
|
||||
|
||||
if %w[created_by_id updated_by_id origin_by_id owner_id archived_by_id published_by_id internal_by_id].include?(column)
|
||||
ref_class.where(column => id).find_in_batches(batch_size: 1000) do |batch_list|
|
||||
batch_list.each do |record|
|
||||
record.update!(column => 1)
|
||||
rescue => e
|
||||
Rails.logger.error e
|
||||
end
|
||||
end
|
||||
elsif ref_class.exists?(column => id)
|
||||
raise "Failed deleting references! Check logic for #{class_name}->#{column}."
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
|
|
|
@ -5,9 +5,13 @@ class UserDevice < ApplicationModel
|
|||
store :location_details
|
||||
validates :name, presence: true
|
||||
|
||||
belongs_to :user
|
||||
|
||||
before_create :fingerprint_validation
|
||||
before_update :fingerprint_validation
|
||||
|
||||
association_attributes_ignored :user
|
||||
|
||||
=begin
|
||||
|
||||
store new device for user if device not already known
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
class Controllers::DataPrivacyTasksControllerPolicy < Controllers::ApplicationControllerPolicy
|
||||
default_permit!('admin.data_privacy')
|
||||
end
|
10
config/routes/data_privacy_task.rb
Normal file
10
config/routes/data_privacy_task.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
Zammad::Application.routes.draw do
|
||||
api_path = Rails.configuration.api_path
|
||||
|
||||
match api_path + '/data_privacy_tasks', to: 'data_privacy_tasks#index', via: :get
|
||||
match api_path + '/data_privacy_tasks/:id', to: 'data_privacy_tasks#show', via: :get
|
||||
match api_path + '/data_privacy_tasks', to: 'data_privacy_tasks#create', via: :post
|
||||
match api_path + '/data_privacy_tasks/:id', to: 'data_privacy_tasks#update', via: :put
|
||||
match api_path + '/data_privacy_tasks/:id', to: 'data_privacy_tasks#destroy', via: :delete
|
||||
|
||||
end
|
|
@ -740,5 +740,17 @@ class CreateBase < ActiveRecord::Migration[4.2]
|
|||
add_index :smime_certificates, [:fingerprint], unique: true
|
||||
add_index :smime_certificates, [:modulus]
|
||||
add_index :smime_certificates, [:subject]
|
||||
|
||||
create_table :data_privacy_tasks do |t|
|
||||
t.column :name, :string, limit: 150, null: true
|
||||
t.column :state, :string, limit: 150, default: 'in process', null: true
|
||||
t.references :deletable, polymorphic: true
|
||||
t.string :preferences, limit: 8000, null: true
|
||||
t.column :updated_by_id, :integer, null: false
|
||||
t.column :created_by_id, :integer, null: false
|
||||
t.timestamps limit: 3, null: false
|
||||
end
|
||||
add_index :data_privacy_tasks, [:name]
|
||||
add_index :data_privacy_tasks, [:state]
|
||||
end
|
||||
end
|
||||
|
|
52
db/migrate/20200707000001_data_privacy_init.rb
Normal file
52
db/migrate/20200707000001_data_privacy_init.rb
Normal file
|
@ -0,0 +1,52 @@
|
|||
class DataPrivacyInit < ActiveRecord::Migration[4.2]
|
||||
def up
|
||||
|
||||
# return if it's a new setup
|
||||
return if !Setting.exists?(name: 'system_init_done')
|
||||
|
||||
up_table
|
||||
up_permission
|
||||
up_scheduler
|
||||
end
|
||||
|
||||
def up_table
|
||||
create_table :data_privacy_tasks do |t|
|
||||
t.column :name, :string, limit: 150, null: true
|
||||
t.column :state, :string, limit: 150, default: 'in process', null: true
|
||||
t.references :deletable, polymorphic: true
|
||||
t.string :preferences, limit: 8000, null: true
|
||||
t.column :updated_by_id, :integer, null: false
|
||||
t.column :created_by_id, :integer, null: false
|
||||
t.timestamps limit: 3, null: false
|
||||
end
|
||||
add_index :data_privacy_tasks, [:name]
|
||||
add_index :data_privacy_tasks, [:state]
|
||||
end
|
||||
|
||||
def up_permission
|
||||
Permission.create_if_not_exists(
|
||||
name: 'admin.data_privacy',
|
||||
note: 'Manage %s',
|
||||
preferences: {
|
||||
translations: ['Data Privacy']
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
def up_scheduler
|
||||
Scheduler.create_or_update(
|
||||
name: 'Handle data privacy tasks.',
|
||||
method: 'DataPrivacyTaskJob.perform_now',
|
||||
period: 10.minutes,
|
||||
last_run: Time.zone.now,
|
||||
prio: 2,
|
||||
active: true,
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
end
|
||||
|
||||
def self.down
|
||||
drop_table :data_privacy_tasks
|
||||
end
|
||||
end
|
|
@ -227,6 +227,13 @@ Permission.create_if_not_exists(
|
|||
translations: ['Monitoring']
|
||||
},
|
||||
)
|
||||
Permission.create_if_not_exists(
|
||||
name: 'admin.data_privacy',
|
||||
note: 'Manage %s',
|
||||
preferences: {
|
||||
translations: ['Data Privacy']
|
||||
},
|
||||
)
|
||||
Permission.create_if_not_exists(
|
||||
name: 'admin.maintenance',
|
||||
note: 'Manage %s',
|
||||
|
|
|
@ -199,3 +199,13 @@ Scheduler.create_if_not_exists(
|
|||
updated_by_id: 1,
|
||||
created_by_id: 1
|
||||
)
|
||||
Scheduler.create_if_not_exists(
|
||||
name: 'Handle data privacy tasks.',
|
||||
method: 'DataPrivacyTaskJob.perform_now',
|
||||
period: 10.minutes,
|
||||
last_run: Time.zone.now,
|
||||
prio: 2,
|
||||
active: true,
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
|
|
|
@ -114,7 +114,7 @@ returns
|
|||
|
||||
=end
|
||||
|
||||
def self.references(object_name, object_id)
|
||||
def self.references(object_name, object_id, include_zero = false)
|
||||
object_name = object_name.to_s
|
||||
|
||||
# check if model exists
|
||||
|
@ -143,7 +143,7 @@ returns
|
|||
next if !model_attributes[:attributes].include?(item)
|
||||
|
||||
count = model_class.where("#{item} = ?", object_id).count
|
||||
next if count.zero?
|
||||
next if count.zero? && !include_zero
|
||||
|
||||
if !references[model_class.to_s][item]
|
||||
references[model_class.to_s][item] = 0
|
||||
|
@ -166,7 +166,7 @@ returns
|
|||
|
||||
if reflection_value.options[:class_name] == object_name
|
||||
count = model_class.where("#{col_name} = ?", object_id).count
|
||||
next if count.zero?
|
||||
next if count.zero? && !include_zero
|
||||
|
||||
if !references[model_class.to_s][col_name]
|
||||
references[model_class.to_s][col_name] = 0
|
||||
|
@ -179,7 +179,7 @@ returns
|
|||
next if reflection_value.name != object_name.downcase.to_sym
|
||||
|
||||
count = model_class.where("#{col_name} = ?", object_id).count
|
||||
next if count.zero?
|
||||
next if count.zero? && !include_zero
|
||||
|
||||
if !references[model_class.to_s][col_name]
|
||||
references[model_class.to_s][col_name] = 0
|
||||
|
|
43
lib/pseudonymisation.rb
Normal file
43
lib/pseudonymisation.rb
Normal file
|
@ -0,0 +1,43 @@
|
|||
class Pseudonymisation
|
||||
|
||||
def self.of_hash(source)
|
||||
return if source.blank?
|
||||
|
||||
source.transform_values do |value|
|
||||
of_value(value)
|
||||
end
|
||||
end
|
||||
|
||||
def self.of_value(source)
|
||||
of_email_address(source)
|
||||
rescue
|
||||
of_string(source)
|
||||
end
|
||||
|
||||
def self.of_email_address(source)
|
||||
email_address = Mail::AddressList.new(source).addresses.first
|
||||
"#{of_string(email_address.local)}@#{of_domain(email_address.domain)}"
|
||||
rescue
|
||||
raise ArgumentError
|
||||
end
|
||||
|
||||
def self.of_domain(source)
|
||||
domain_parts = source.split('.')
|
||||
|
||||
# e.g. localhost
|
||||
return of_string(source) if domain_parts.size == 1
|
||||
|
||||
tld = domain_parts[-1]
|
||||
other = domain_parts[0..-2].join('.')
|
||||
"#{of_string(other)}.#{tld}"
|
||||
end
|
||||
|
||||
def self.of_string(source)
|
||||
return '*' if source.length == 1
|
||||
return "#{source.first}*#{source.last}" if source.exclude?(' ')
|
||||
|
||||
source.split(' ').map do |sub_string|
|
||||
of_string(sub_string)
|
||||
end.join(' ')
|
||||
end
|
||||
end
|
|
@ -23,7 +23,8 @@ RSpec.describe Issue1977RemoveInvalidUserForeignKeys, type: :db_migration do
|
|||
without_foreign_key(:online_notifications, column: :user_id)
|
||||
without_foreign_key(:recent_views, column: :created_by_id)
|
||||
|
||||
create(:recent_view, created_by_id: 1337)
|
||||
record = build(:recent_view, created_by_id: 1337)
|
||||
record.save(validate: false)
|
||||
create(:recent_view, created_by_id: existing_user_id)
|
||||
|
||||
expect do
|
||||
|
|
6
spec/factories/chat/agent.rb
Normal file
6
spec/factories/chat/agent.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
FactoryBot.define do
|
||||
factory :'chat/agent' do
|
||||
created_by_id { 1 }
|
||||
updated_by_id { 1 }
|
||||
end
|
||||
end
|
6
spec/factories/data_privacy_task.rb
Normal file
6
spec/factories/data_privacy_task.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
FactoryBot.define do
|
||||
factory :data_privacy_task do
|
||||
created_by_id { 1 }
|
||||
updated_by_id { 1 }
|
||||
end
|
||||
end
|
|
@ -1,5 +1,7 @@
|
|||
FactoryBot.define do
|
||||
factory 'knowledge_base/answer/translation', aliases: %i[knowledge_base_answer_translation] do
|
||||
created_by_id { 1 }
|
||||
updated_by_id { 1 }
|
||||
answer { nil }
|
||||
kb_locale { nil }
|
||||
sequence(:title) { |n| "#{Faker::Appliance.equipment} ##{n}" }
|
||||
|
|
|
@ -7,5 +7,6 @@ FactoryBot.define do
|
|||
state {}
|
||||
prio { 1 }
|
||||
notify { false }
|
||||
user_id { 1 }
|
||||
end
|
||||
end
|
||||
|
|
8
spec/factories/ticket/flag.rb
Normal file
8
spec/factories/ticket/flag.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
FactoryBot.define do
|
||||
factory :'ticket/flag', aliases: %i[ticket_flag] do
|
||||
ticket
|
||||
key { "key_#{rand(100)}" }
|
||||
value { "value_#{rand(100)}" }
|
||||
created_by_id { 1 }
|
||||
end
|
||||
end
|
52
spec/jobs/data_privacy_task_job_spec.rb
Normal file
52
spec/jobs/data_privacy_task_job_spec.rb
Normal file
|
@ -0,0 +1,52 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe DataPrivacyTaskJob, type: :job do
|
||||
|
||||
describe '#perform' do
|
||||
|
||||
before do
|
||||
Setting.set('system_init_done', true)
|
||||
end
|
||||
|
||||
let!(:organization) { create(:organization, name: 'test') }
|
||||
let!(:admin) { create(:admin) }
|
||||
let!(:user) { create(:customer, organization: organization) }
|
||||
|
||||
it 'checks if the user is deleted' do
|
||||
create(:data_privacy_task, deletable: user)
|
||||
described_class.perform_now
|
||||
expect { user.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
it 'checks if the organization is deleted' do
|
||||
create(:data_privacy_task, deletable: user)
|
||||
described_class.perform_now
|
||||
expect(organization.reload).to be_a_kind_of(Organization)
|
||||
end
|
||||
|
||||
it 'checks if the state is completed' do
|
||||
task = create(:data_privacy_task, deletable: user)
|
||||
described_class.perform_now
|
||||
expect(task.reload.state).to eq('completed')
|
||||
end
|
||||
|
||||
it 'checks if the user is deleted (delete_organization=true)' do
|
||||
create(:data_privacy_task, deletable: user, preferences: { delete_organization: 'true' })
|
||||
described_class.perform_now
|
||||
expect { user.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
it 'checks if the organization is deleted (delete_organization=true)' do
|
||||
create(:data_privacy_task, deletable: user, preferences: { delete_organization: 'true' })
|
||||
described_class.perform_now
|
||||
expect { organization.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
it 'checks creation of activity stream log' do
|
||||
create(:data_privacy_task, deletable: user, created_by: admin)
|
||||
travel 15.minutes
|
||||
described_class.perform_now
|
||||
expect(admin.activity_stream(20).any? { |entry| entry.type.name == 'completed' }).to be true
|
||||
end
|
||||
end
|
||||
end
|
111
spec/lib/pseudonymisation_spec.rb
Normal file
111
spec/lib/pseudonymisation_spec.rb
Normal file
|
@ -0,0 +1,111 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Pseudonymisation do
|
||||
|
||||
describe '.of_hash' do
|
||||
|
||||
let(:source) do
|
||||
{
|
||||
firstname: 'John',
|
||||
lastname: 'Doe',
|
||||
email: 'john.doe@example.com',
|
||||
organization: 'Example Inc.',
|
||||
}
|
||||
end
|
||||
|
||||
let(:result) do
|
||||
{
|
||||
firstname: 'J*n',
|
||||
lastname: 'D*e',
|
||||
email: 'j*e@e*e.com',
|
||||
organization: 'E*e I*.',
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates pseudonymous hash' do
|
||||
expect(described_class.of_hash(source)).to eq(result)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.of_value' do
|
||||
|
||||
context 'when email address is given' do
|
||||
let(:source) { 'test@example.com' }
|
||||
|
||||
it 'creates pseudonymous email_address' do
|
||||
expect(described_class.of_value(source)).to eq('t*t@e*e.com')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when string is given' do
|
||||
let(:source) { 'Zammad' }
|
||||
|
||||
it 'creates pseudonymous string' do
|
||||
expect(described_class.of_value(source)).to eq('Z*d')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.of_email_address' do
|
||||
|
||||
let(:source) { 'test@example.com' }
|
||||
|
||||
it 'creates pseudonymous email_address' do
|
||||
expect(described_class.of_email_address(source)).to eq('t*t@e*e.com')
|
||||
end
|
||||
|
||||
context 'when address is invalid' do
|
||||
|
||||
it 'raises ArgumentError for parsing errors' do
|
||||
expect { described_class.of_email_address('i_m_no_address@') }.to raise_exception(ArgumentError)
|
||||
end
|
||||
|
||||
it 'raises ArgumentError for string argument' do
|
||||
expect { described_class.of_email_address('i_m_no_address') }.to raise_exception(ArgumentError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.of_domain' do
|
||||
|
||||
let(:source) { 'zammad.com' }
|
||||
|
||||
it 'creates pseudonymous string with TLD' do
|
||||
expect(described_class.of_domain(source)).to eq('z*d.com')
|
||||
end
|
||||
|
||||
context 'when no TLD is present' do
|
||||
|
||||
let(:source) { 'localhost' }
|
||||
|
||||
it 'creates pseudonymous string' do
|
||||
expect(described_class.of_domain(source)).to eq('l*t')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.of_string' do
|
||||
|
||||
let(:source) { 'Zammad' }
|
||||
|
||||
it 'creates pseudonymous string' do
|
||||
expect(described_class.of_string(source)).to eq('Z*d')
|
||||
end
|
||||
|
||||
context 'when only one char long' do
|
||||
let(:source) { 'a' }
|
||||
|
||||
it 'returns *' do
|
||||
expect(described_class.of_string(source)).to eq('*')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when multiple sub-strings are given' do
|
||||
let(:source) { 'Zammad Foundation' }
|
||||
|
||||
it 'create pseudonymous string for each' do
|
||||
expect(described_class.of_string(source)).to eq('Z*d F*n')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
11
spec/models/concerns/has_taskbars_examples.rb
Normal file
11
spec/models/concerns/has_taskbars_examples.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
RSpec.shared_examples 'HasTaskbars' do
|
||||
subject { create(described_class.name.underscore) }
|
||||
|
||||
describe '#destroy_taskbars' do
|
||||
it 'destroys related taskbars' do
|
||||
taskbar = create(:taskbar, key: "#{described_class.name}-#{subject.id}")
|
||||
subject.destroy
|
||||
expect { taskbar.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
end
|
92
spec/models/data_privacy_task_spec.rb
Normal file
92
spec/models/data_privacy_task_spec.rb
Normal file
|
@ -0,0 +1,92 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe DataPrivacyTask, type: :model do
|
||||
describe '.perform' do
|
||||
let(:organization) { create(:organization, name: 'test') }
|
||||
let!(:admin) { create(:admin) }
|
||||
let(:user) { create(:customer, organization: organization) }
|
||||
|
||||
it 'blocks other objects than user objects' do
|
||||
expect { create(:data_privacy_task, deletable: create(:chat)) }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Deletable is not a User')
|
||||
end
|
||||
|
||||
it 'blocks the multiple deletion tasks for the same user' do
|
||||
create(:data_privacy_task, deletable: user)
|
||||
expect { create(:data_privacy_task, deletable: user) }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Deletable has an existing DataPrivacyTask queued')
|
||||
end
|
||||
|
||||
it 'blocks deletion task for user id 1' do
|
||||
expect { create(:data_privacy_task, deletable: User.find(1)) }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Deletable is undeletable system User with ID 1')
|
||||
end
|
||||
|
||||
it 'blocks deletion task for yourself' do
|
||||
UserInfo.current_user_id = user.id
|
||||
expect { create(:data_privacy_task, deletable: user) }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Deletable is your current account')
|
||||
end
|
||||
|
||||
it 'blocks deletion task for last admin' do
|
||||
expect { create(:data_privacy_task, deletable: admin) }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Deletable is last account with admin permissions')
|
||||
end
|
||||
|
||||
it 'allows deletion task for last two admins' do
|
||||
create(:admin)
|
||||
admin = create(:admin)
|
||||
expect(create(:data_privacy_task, deletable: admin)).to be_truthy
|
||||
end
|
||||
|
||||
it 'sets the failed state when task failed' do
|
||||
task = create(:data_privacy_task, deletable: user)
|
||||
user.destroy
|
||||
task.perform
|
||||
expect(task.reload.state).to eq('failed')
|
||||
end
|
||||
|
||||
it 'sets an error message when task failed' do
|
||||
task = create(:data_privacy_task, deletable: user)
|
||||
user.destroy
|
||||
task.perform
|
||||
expect(task.reload.preferences[:error]).to eq("ERROR: #<ActiveRecord::RecordNotFound: Couldn't find User with 'id'=#{user.id}>")
|
||||
end
|
||||
end
|
||||
|
||||
describe '#prepare_deletion_preview' do
|
||||
|
||||
let(:organization) { create(:organization, name: 'Zammad GmbH') }
|
||||
let(:user) { create(:customer, organization: organization, email: 'secret@example.com') }
|
||||
let(:task) { create(:data_privacy_task, deletable: user) }
|
||||
|
||||
context 'when storing user data' do
|
||||
|
||||
let(:pseudonymous_data) do
|
||||
{
|
||||
'firstname' => 'N*e',
|
||||
'lastname' => 'B*n',
|
||||
'email' => 's*t@e*e.com',
|
||||
'organization' => 'Z*d G*H',
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates pseudonymous representation' do
|
||||
expect(task[:preferences][:user]).to eq(pseudonymous_data)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when User is owner of Tickets' do
|
||||
|
||||
let!(:owner_tickets) { create_list(:ticket, 3, owner: user) }
|
||||
|
||||
it 'stores the numbers' do
|
||||
expect(task[:preferences][:owner_tickets]).to eq(owner_tickets.reverse.map(&:number))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when User is customer of Tickets' do
|
||||
|
||||
let!(:customer_tickets) { create_list(:ticket, 3, customer: user) }
|
||||
|
||||
it 'stores the numbers' do
|
||||
expect(task[:preferences][:customer_tickets]).to eq(customer_tickets.reverse.map(&:number))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,6 +5,7 @@ require 'models/concerns/has_history_examples'
|
|||
require 'models/concerns/has_search_index_backend_examples'
|
||||
require 'models/concerns/has_xss_sanitized_note_examples'
|
||||
require 'models/concerns/has_object_manager_attributes_validation_examples'
|
||||
require 'models/concerns/has_taskbars_examples'
|
||||
|
||||
RSpec.describe Organization, type: :model do
|
||||
it_behaves_like 'ApplicationModel', can_assets: { associations: :members }
|
||||
|
@ -13,6 +14,7 @@ RSpec.describe Organization, type: :model do
|
|||
it_behaves_like 'HasSearchIndexBackend', indexed_factory: :organization
|
||||
it_behaves_like 'HasXssSanitizedNote', model_factory: :organization
|
||||
it_behaves_like 'HasObjectManagerAttributesValidation'
|
||||
it_behaves_like 'HasTaskbars'
|
||||
|
||||
subject(:organization) { create(:organization) }
|
||||
|
||||
|
@ -24,6 +26,28 @@ RSpec.describe Organization, type: :model do
|
|||
expect(organizations).not_to be_blank
|
||||
end
|
||||
end
|
||||
|
||||
describe '.destroy' do
|
||||
|
||||
let!(:refs_known) { { 'Ticket' => { 'organization_id'=> 1 }, 'User' => { 'organization_id'=> 1 } } }
|
||||
let!(:user) { create(:customer, organization: organization) }
|
||||
let!(:ticket) { create(:ticket, organization: organization, customer: user) }
|
||||
|
||||
it 'checks known refs' do
|
||||
refs_organization = Models.references('Organization', organization.id, true)
|
||||
expect(refs_organization).to eq(refs_known)
|
||||
end
|
||||
|
||||
it 'checks user deletion' do
|
||||
organization.destroy
|
||||
expect { user.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
it 'checks ticket deletion' do
|
||||
organization.destroy
|
||||
expect { ticket.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Callbacks, Observers, & Async Transactions -' do
|
||||
|
|
|
@ -116,7 +116,7 @@ RSpec.describe Taskbar do
|
|||
end
|
||||
end
|
||||
|
||||
context 'multible creation' do
|
||||
context 'multiple creation' do
|
||||
|
||||
it 'create tasks' do
|
||||
|
||||
|
@ -132,6 +132,7 @@ RSpec.describe Taskbar do
|
|||
state: {},
|
||||
prio: 1,
|
||||
notify: false,
|
||||
user_id: 1,
|
||||
)
|
||||
|
||||
UserInfo.current_user_id = 2
|
||||
|
@ -145,6 +146,7 @@ RSpec.describe Taskbar do
|
|||
state: {},
|
||||
prio: 2,
|
||||
notify: false,
|
||||
user_id: 1,
|
||||
)
|
||||
|
||||
taskbar1.reload
|
||||
|
@ -171,6 +173,7 @@ RSpec.describe Taskbar do
|
|||
state: {},
|
||||
prio: 2,
|
||||
notify: false,
|
||||
user_id: 1,
|
||||
)
|
||||
|
||||
taskbar1.reload
|
||||
|
@ -205,6 +208,7 @@ RSpec.describe Taskbar do
|
|||
state: {},
|
||||
prio: 4,
|
||||
notify: false,
|
||||
user_id: 1,
|
||||
)
|
||||
|
||||
taskbar1.reload
|
||||
|
|
|
@ -4,6 +4,7 @@ require 'models/concerns/can_be_imported_examples'
|
|||
require 'models/concerns/can_csv_import_examples'
|
||||
require 'models/concerns/has_history_examples'
|
||||
require 'models/concerns/has_tags_examples'
|
||||
require 'models/concerns/has_taskbars_examples'
|
||||
require 'models/concerns/has_xss_sanitized_note_examples'
|
||||
require 'models/concerns/has_object_manager_attributes_validation_examples'
|
||||
|
||||
|
@ -13,6 +14,7 @@ RSpec.describe Ticket, type: :model do
|
|||
it_behaves_like 'CanCsvImport'
|
||||
it_behaves_like 'HasHistory', history_relation_object: 'Ticket::Article'
|
||||
it_behaves_like 'HasTags'
|
||||
it_behaves_like 'HasTaskbars'
|
||||
it_behaves_like 'HasXssSanitizedNote', model_factory: :ticket
|
||||
it_behaves_like 'HasObjectManagerAttributesValidation'
|
||||
|
||||
|
@ -1114,6 +1116,27 @@ RSpec.describe Ticket, type: :model do
|
|||
.to(false)
|
||||
end
|
||||
|
||||
it 'destroys all related dependencies' do
|
||||
refs_known = { 'Ticket::Article' => { 'ticket_id'=>1 },
|
||||
'Ticket::TimeAccounting' => { 'ticket_id'=>1 },
|
||||
'Ticket::Flag' => { 'ticket_id'=>1 } }
|
||||
|
||||
ticket = create(:ticket)
|
||||
article = create(:ticket_article, ticket: ticket)
|
||||
accounting = create(:ticket_time_accounting, ticket: ticket)
|
||||
flag = create(:ticket_flag, ticket: ticket)
|
||||
|
||||
refs_ticket = Models.references('Ticket', ticket.id, true)
|
||||
expect(refs_ticket).to eq(refs_known)
|
||||
|
||||
ticket.destroy
|
||||
|
||||
expect { ticket.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
||||
expect { article.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
||||
expect { accounting.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
||||
expect { flag.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
context 'when ticket is generated from email (with attachments)' do
|
||||
subject(:ticket) { Channel::EmailParser.new.process({}, raw_email).first }
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ require 'models/concerns/can_be_imported_examples'
|
|||
require 'models/concerns/has_object_manager_attributes_validation_examples'
|
||||
require 'models/user/has_ticket_create_screen_impact_examples'
|
||||
require 'models/user/can_lookup_search_index_attributes_examples'
|
||||
require 'models/concerns/has_taskbars_examples'
|
||||
|
||||
RSpec.describe User, type: :model do
|
||||
subject(:user) { create(:user) }
|
||||
|
@ -27,6 +28,7 @@ RSpec.describe User, type: :model do
|
|||
it_behaves_like 'HasObjectManagerAttributesValidation'
|
||||
it_behaves_like 'HasTicketCreateScreenImpact'
|
||||
it_behaves_like 'CanLookupSearchIndexAttributes'
|
||||
it_behaves_like 'HasTaskbars'
|
||||
|
||||
describe 'Class methods:' do
|
||||
describe '.authenticate' do
|
||||
|
@ -812,6 +814,144 @@ RSpec.describe User, type: :model do
|
|||
end
|
||||
|
||||
describe 'Associations:' do
|
||||
subject(:user) { create(:agent, groups: [group_subject]) }
|
||||
|
||||
let!(:group_subject) { create(:group) }
|
||||
|
||||
it 'does remove references before destroy' do
|
||||
refs_known = { 'Group' => { 'created_by_id' => 1, 'updated_by_id' => 0 },
|
||||
'Token' => { 'user_id' => 1 },
|
||||
'Ticket::Article' =>
|
||||
{ 'created_by_id' => 0, 'updated_by_id' => 0, 'origin_by_id' => 1 },
|
||||
'Ticket::StateType' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'Ticket::Article::Sender' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'Ticket::Article::Type' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'Ticket::Article::Flag' => { 'created_by_id' => 0 },
|
||||
'Ticket::Priority' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'Ticket::TimeAccounting' => { 'created_by_id' => 0 },
|
||||
'Ticket::State' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'Ticket::Flag' => { 'created_by_id' => 0 },
|
||||
'PostmasterFilter' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'OnlineNotification' => { 'user_id' => 1, 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'Ticket' =>
|
||||
{ 'created_by_id' => 0, 'updated_by_id' => 0, 'owner_id' => 1, 'customer_id' => 3 },
|
||||
'Template' => { 'user_id' => 1, 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'Avatar' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'Scheduler' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'Chat' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'HttpLog' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'EmailAddress' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'Taskbar' => { 'user_id' => 1 },
|
||||
'Sla' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'UserDevice' => { 'user_id' => 1 },
|
||||
'Chat::Message' => { 'created_by_id' => 0 },
|
||||
'Chat::Agent' => { 'created_by_id' => 1, 'updated_by_id' => 1 },
|
||||
'Chat::Session' => { 'user_id' => 0, 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'Tag' => { 'created_by_id' => 0 },
|
||||
'Karma::User' => { 'user_id' => 0 },
|
||||
'Karma::ActivityLog' => { 'user_id' => 1 },
|
||||
'RecentView' => { 'created_by_id' => 1 },
|
||||
'KnowledgeBase::Answer::Translation' =>
|
||||
{ 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'KnowledgeBase::Answer' =>
|
||||
{ 'archived_by_id' => 1, 'published_by_id' => 1, 'internal_by_id' => 1 },
|
||||
'Report::Profile' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'Package' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'Job' => { 'created_by_id' => 0, 'updated_by_id' => 1 },
|
||||
'Store' => { 'created_by_id' => 0 },
|
||||
'Cti::CallerId' => { 'user_id' => 1 },
|
||||
'DataPrivacyTask' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'Trigger' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'Translation' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'ObjectManager::Attribute' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'User' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'Organization' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'Macro' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'Channel' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'Role' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'History' => { 'created_by_id' => 1 },
|
||||
'Overview' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'ActivityStream' => { 'created_by_id' => 0 },
|
||||
'StatsStore' => { 'created_by_id' => 0 },
|
||||
'TextModule' => { 'user_id' => 1, 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'Calendar' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'UserGroup' => { 'user_id' => 1 },
|
||||
'Signature' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'Authorization' => { 'user_id' => 1 } }
|
||||
|
||||
# delete objects
|
||||
token = create(:token, user: user)
|
||||
online_notification = create(:online_notification, user: user)
|
||||
template = create(:template, :dummy_data, user: user)
|
||||
taskbar = create(:taskbar, user: user)
|
||||
user_device = create(:user_device, user: user)
|
||||
karma_activity_log = create(:karma_activity_log, user: user)
|
||||
cti_caller_id = create(:cti_caller_id, user: user)
|
||||
text_module = create(:text_module, user: user)
|
||||
authorization = create(:twitter_authorization, user: user)
|
||||
recent_view = create(:recent_view, created_by: user)
|
||||
avatar = create(:avatar, o_id: user.id)
|
||||
|
||||
# create a chat agent for admin user (id=1) before agent user
|
||||
# to be sure that the data gets removed and not mapped which
|
||||
# would result in a foreign key because of the unique key on the
|
||||
# created_by_id and updated_by_id.
|
||||
create(:'chat/agent')
|
||||
chat_agent_user = create(:'chat/agent', created_by_id: user.id, updated_by_id: user.id)
|
||||
|
||||
# move ownership objects
|
||||
group = create(:group, created_by_id: user.id)
|
||||
job = create(:job, updated_by_id: user.id)
|
||||
ticket = create(:ticket, group: group_subject, owner: user)
|
||||
ticket_article = create(:ticket_article, ticket: ticket, origin_by_id: user.id)
|
||||
customer_ticket1 = create(:ticket, group: group_subject, customer: user)
|
||||
customer_ticket2 = create(:ticket, group: group_subject, customer: user)
|
||||
customer_ticket3 = create(:ticket, group: group_subject, customer: user)
|
||||
knowledge_base_answer = create(:knowledge_base_answer, archived_by_id: user.id, published_by_id: user.id, internal_by_id: user.id)
|
||||
|
||||
refs_user = Models.references('User', user.id, true)
|
||||
expect(refs_user).to eq(refs_known)
|
||||
|
||||
user.destroy
|
||||
|
||||
expect { token.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
||||
expect { online_notification.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
||||
expect { template.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
||||
expect { taskbar.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
||||
expect { user_device.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
||||
expect { karma_activity_log.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
||||
expect { cti_caller_id.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
||||
expect { text_module.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
||||
expect { authorization.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
||||
expect { recent_view.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
||||
expect { avatar.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
||||
expect { customer_ticket1.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
||||
expect { customer_ticket2.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
||||
expect { customer_ticket3.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
||||
expect { chat_agent_user.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
||||
|
||||
# move ownership objects
|
||||
expect { group.reload }.to change(group, :created_by_id).to(1)
|
||||
expect { job.reload }.to change(job, :updated_by_id).to(1)
|
||||
expect { ticket.reload }.to change(ticket, :owner_id).to(1)
|
||||
expect { ticket_article.reload }.to change(ticket_article, :origin_by_id).to(1)
|
||||
expect { knowledge_base_answer.reload }
|
||||
.to change(knowledge_base_answer, :archived_by_id).to(1)
|
||||
.and change(knowledge_base_answer, :published_by_id).to(1)
|
||||
.and change(knowledge_base_answer, :internal_by_id).to(1)
|
||||
end
|
||||
|
||||
it 'does delete cache after user deletion' do
|
||||
online_notification = create(:online_notification, created_by_id: user.id)
|
||||
online_notification.attributes_with_association_ids
|
||||
user.destroy
|
||||
expect(online_notification.reload.attributes_with_association_ids['created_by_id']).to eq(1)
|
||||
end
|
||||
|
||||
it 'does return an exception on blocking dependencies' do
|
||||
expect { user.send(:destroy_move_dependency_ownership) }.to raise_error(RuntimeError, 'Failed deleting references! Check logic for UserGroup->user_id.')
|
||||
end
|
||||
|
||||
describe '#organization' do
|
||||
describe 'email domain-based assignment' do
|
||||
subject(:user) { build(:user) }
|
||||
|
|
|
@ -509,6 +509,20 @@ RSpec.describe 'Monitoring', type: :request do
|
|||
expect(json_response['healthy']).to eq(false)
|
||||
expect(json_response['message']).to eq("Channel: Email::Notification out ;unprocessable mails: 1;Failed to run import backend 'Import::Ldap'. Cause: Some bad error;Stuck import backend 'Import::Ldap' detected. Last update: #{stuck_updated_at_timestamp}")
|
||||
|
||||
privacy_stuck_updated_at_timestamp = 30.minutes.ago
|
||||
task = create(:data_privacy_task, deletable: customer)
|
||||
task.update(updated_at: privacy_stuck_updated_at_timestamp)
|
||||
|
||||
# health_check
|
||||
get "/api/v1/monitoring/health_check?token=#{token}", params: {}, as: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response['message']).to be_truthy
|
||||
expect(json_response['issues']).to be_truthy
|
||||
expect(json_response['healthy']).to eq(false)
|
||||
expect(json_response['message']).to eq("Channel: Email::Notification out ;unprocessable mails: 1;Failed to run import backend 'Import::Ldap'. Cause: Some bad error;Stuck import backend 'Import::Ldap' detected. Last update: #{stuck_updated_at_timestamp};Stuck data privacy task (ID #{task.id}) detected. Last update: #{privacy_stuck_updated_at_timestamp}")
|
||||
|
||||
Setting.set('ldap_integration', false)
|
||||
end
|
||||
|
||||
|
|
95
spec/system/data_privacy_spec.rb
Normal file
95
spec/system/data_privacy_spec.rb
Normal file
|
@ -0,0 +1,95 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Data Privacy', type: :system, searchindex: true, authenticated_as: :authenticate do
|
||||
before do
|
||||
configure_elasticsearch(rebuild: true)
|
||||
end
|
||||
|
||||
let(:customer) { create(:customer, firstname: 'Frank1') }
|
||||
let(:ticket) { create(:ticket, customer: customer, group: Group.find_by(name: 'Users')) }
|
||||
|
||||
def authenticate
|
||||
customer
|
||||
ticket
|
||||
true
|
||||
end
|
||||
|
||||
context 'when data privacy admin interface' do
|
||||
it 'deletes customer' do
|
||||
visit 'system/data_privacy'
|
||||
click '.js-new'
|
||||
|
||||
find(:css, '.js-input').send_keys(customer.firstname)
|
||||
expect(page).to have_css('.searchableSelect-option-text', wait: 5)
|
||||
click '.searchableSelect-option-text'
|
||||
fill_in 'Are you sure?', with: 'DELETE'
|
||||
expect(page).to have_no_text('DELETE ORGANIZATION?', wait: 5)
|
||||
click '.js-submit'
|
||||
|
||||
expect(page).to have_text('in process', wait: 5)
|
||||
DataPrivacyTaskJob.perform_now
|
||||
expect(page).to have_text('completed', wait: 5)
|
||||
end
|
||||
|
||||
context 'when customer is the single user of the organization' do
|
||||
let(:organization) { create(:organization) }
|
||||
let(:customer) { create(:customer, firstname: 'Frank2', organization: organization) }
|
||||
|
||||
def authenticate
|
||||
organization
|
||||
customer
|
||||
ticket
|
||||
true
|
||||
end
|
||||
|
||||
it 'deletes customer' do
|
||||
visit 'system/data_privacy'
|
||||
click '.js-new'
|
||||
|
||||
find(:css, '.js-input').send_keys(customer.firstname)
|
||||
expect(page).to have_css('.searchableSelect-option-text', wait: 5)
|
||||
click '.searchableSelect-option-text'
|
||||
fill_in 'Are you sure?', with: 'DELETE'
|
||||
expect(page).to have_text('DELETE ORGANIZATION?', wait: 5)
|
||||
click '.js-submit'
|
||||
|
||||
expect(page).to have_text('in process', wait: 5)
|
||||
DataPrivacyTaskJob.perform_now
|
||||
expect(page).to have_text('completed', wait: 5)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user profile' do
|
||||
it 'deletes customer' do
|
||||
visit "user/profile/#{customer.id}"
|
||||
|
||||
click '.dropdown--actions'
|
||||
click_on 'Delete'
|
||||
|
||||
fill_in 'Are you sure?', with: 'DELETE'
|
||||
click '.js-submit'
|
||||
|
||||
expect(page).to have_text('in process', wait: 5)
|
||||
DataPrivacyTaskJob.perform_now
|
||||
expect(page).to have_text('completed', wait: 5)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when ticket zoom' do
|
||||
it 'deletes customer' do
|
||||
visit "ticket/zoom/#{ticket.id}"
|
||||
|
||||
click '.tabsSidebar-tab[data-tab=customer]'
|
||||
click 'h2.js-headline'
|
||||
click_on 'Delete Customer'
|
||||
|
||||
fill_in 'Are you sure?', with: 'DELETE'
|
||||
click '.js-submit'
|
||||
|
||||
expect(page).to have_text('in process', wait: 5)
|
||||
DataPrivacyTaskJob.perform_now
|
||||
expect(page).to have_text('completed', wait: 5)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -22,6 +22,9 @@ class SwitchToUserTest < TestCase
|
|||
@browser.action.move_to(@browser.find_elements({ css: '.content.active .table-overview tbody tr:first-child' } )[0]).release.perform
|
||||
|
||||
sleep 0.5
|
||||
click(
|
||||
css: '.content.active .dropdown--actions',
|
||||
)
|
||||
click(
|
||||
css: '.content.active .icon-switchView',
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue