Added initial version of Out Of Office functionality.
This commit is contained in:
parent
8958a3ac63
commit
20a8e40484
27 changed files with 882 additions and 56 deletions
|
@ -344,7 +344,10 @@ class App.Controller extends Spine.Controller
|
||||||
title: ->
|
title: ->
|
||||||
userId = $(@).data('id')
|
userId = $(@).data('id')
|
||||||
user = App.User.find(userId)
|
user = App.User.find(userId)
|
||||||
App.Utils.htmlEscape(user.displayName())
|
headline = App.Utils.htmlEscape(user.displayName())
|
||||||
|
if user.isOutOfOffice()
|
||||||
|
headline += " (#{App.Utils.htmlEscape(user.outOfOfficeText())})"
|
||||||
|
headline
|
||||||
content: ->
|
content: ->
|
||||||
userId = $(@).data('id')
|
userId = $(@).data('id')
|
||||||
user = App.User.fullLocal(userId)
|
user = App.User.fullLocal(userId)
|
||||||
|
|
|
@ -0,0 +1,164 @@
|
||||||
|
class Index extends App.ControllerSubContent
|
||||||
|
requiredPermission: 'user_preferences.out_of_office+ticket.agent'
|
||||||
|
header: 'Out of Office'
|
||||||
|
events:
|
||||||
|
'submit form': 'submit'
|
||||||
|
'click .js-disabled': 'disable'
|
||||||
|
'click .js-enable': 'enable'
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
@render()
|
||||||
|
|
||||||
|
render: =>
|
||||||
|
user = @Session.get()
|
||||||
|
if !@localData
|
||||||
|
@localData =
|
||||||
|
out_of_office: user.out_of_office
|
||||||
|
out_of_office_start_at: user.out_of_office_start_at
|
||||||
|
out_of_office_end_at: user.out_of_office_end_at
|
||||||
|
out_of_office_replacement_id: user.out_of_office_replacement_id
|
||||||
|
out_of_office_replacement_id_completion: user.preferences.out_of_office_replacement_id_completion
|
||||||
|
out_of_office_text: user.preferences.out_of_office_text
|
||||||
|
form = $(App.view('profile/out_of_office')(
|
||||||
|
user: user
|
||||||
|
localData: @localData
|
||||||
|
placeholder: App.User.outOfOfficeTextPlaceholder()
|
||||||
|
))
|
||||||
|
|
||||||
|
dateStart = new App.ControllerForm(
|
||||||
|
model:
|
||||||
|
configure_attributes:
|
||||||
|
[
|
||||||
|
name: 'out_of_office_start_at'
|
||||||
|
display: ''
|
||||||
|
tag: 'date'
|
||||||
|
past: false
|
||||||
|
future: true
|
||||||
|
null: false
|
||||||
|
]
|
||||||
|
noFieldset: true
|
||||||
|
params: @localData
|
||||||
|
)
|
||||||
|
form.find('.js-startDate').html(dateStart.form)
|
||||||
|
|
||||||
|
dateEnd = new App.ControllerForm(
|
||||||
|
model:
|
||||||
|
configure_attributes:
|
||||||
|
[
|
||||||
|
name: 'out_of_office_end_at'
|
||||||
|
display: ''
|
||||||
|
tag: 'date'
|
||||||
|
past: false
|
||||||
|
future: true
|
||||||
|
null: false
|
||||||
|
]
|
||||||
|
noFieldset: true
|
||||||
|
params: @localData
|
||||||
|
)
|
||||||
|
form.find('.js-endDate').html(dateEnd.form)
|
||||||
|
|
||||||
|
agentList = new App.ControllerForm(
|
||||||
|
model:
|
||||||
|
configure_attributes:
|
||||||
|
[
|
||||||
|
name: 'out_of_office_replacement_id'
|
||||||
|
display: ''
|
||||||
|
relation: 'User'
|
||||||
|
tag: 'user_autocompletion'
|
||||||
|
autocapitalize: false
|
||||||
|
multiple: false
|
||||||
|
limit: 30
|
||||||
|
minLengt: 2
|
||||||
|
placeholder: 'Enter Person or Organization/Company'
|
||||||
|
null: false
|
||||||
|
translate: false
|
||||||
|
disableCreateObject: true
|
||||||
|
value: @localData
|
||||||
|
]
|
||||||
|
noFieldset: true
|
||||||
|
params: @localData
|
||||||
|
)
|
||||||
|
form.find('.js-recipientDropdown').html(agentList.form)
|
||||||
|
if @localData.out_of_office is true
|
||||||
|
form.find('.js-disabled').removeClass('is-disabled')
|
||||||
|
#form.find('.js-enable').addClass('is-disabled')
|
||||||
|
else
|
||||||
|
form.find('.js-disabled').addClass('is-disabled')
|
||||||
|
#form.find('.js-enable').removeClass('is-disabled')
|
||||||
|
@html(form)
|
||||||
|
|
||||||
|
enable: (e) =>
|
||||||
|
e.preventDefault()
|
||||||
|
params = @formParam(e.target)
|
||||||
|
params.out_of_office = true
|
||||||
|
@store(e, params)
|
||||||
|
|
||||||
|
disable: (e) =>
|
||||||
|
e.preventDefault()
|
||||||
|
params = @formParam(e.target)
|
||||||
|
params.out_of_office = false
|
||||||
|
@store(e, params)
|
||||||
|
|
||||||
|
submit: (e, params) =>
|
||||||
|
e.preventDefault()
|
||||||
|
params = @formParam(e.target)
|
||||||
|
@store(e, params)
|
||||||
|
|
||||||
|
store: (e, params) =>
|
||||||
|
@formDisable(e)
|
||||||
|
for key, value of params
|
||||||
|
@localData[key] = value
|
||||||
|
App.Ajax.request(
|
||||||
|
id: 'user_out_of_office'
|
||||||
|
type: 'PUT'
|
||||||
|
url: "#{@apiPath}/users/out_of_office"
|
||||||
|
data: JSON.stringify(params)
|
||||||
|
processData: true
|
||||||
|
success: @success
|
||||||
|
error: @error
|
||||||
|
)
|
||||||
|
|
||||||
|
success: (data) =>
|
||||||
|
if data.message is 'ok'
|
||||||
|
@render()
|
||||||
|
@notify(
|
||||||
|
type: 'success'
|
||||||
|
msg: App.i18n.translateContent('Successfully!')
|
||||||
|
timeout: 1000
|
||||||
|
)
|
||||||
|
else
|
||||||
|
if data.notice
|
||||||
|
@notify
|
||||||
|
type: 'error'
|
||||||
|
msg: App.i18n.translateContent(data.notice[0], data.notice[1])
|
||||||
|
removeAll: true
|
||||||
|
else
|
||||||
|
@notify
|
||||||
|
type: 'error'
|
||||||
|
msg: 'Please contact your administrator.'
|
||||||
|
removeAll: true
|
||||||
|
@formEnable( @$('form') )
|
||||||
|
|
||||||
|
error: (xhr, status, error) =>
|
||||||
|
@formEnable( @$('form') )
|
||||||
|
|
||||||
|
# do not close window if request is aborted
|
||||||
|
return if status is 'abort'
|
||||||
|
data = JSON.parse(xhr.responseText)
|
||||||
|
|
||||||
|
# show error message
|
||||||
|
if xhr.status is 401 || error is 'Unauthorized'
|
||||||
|
message = '» ' + App.i18n.translateInline('Unauthorized') + ' «'
|
||||||
|
else if xhr.status is 404 || error is 'Not Found'
|
||||||
|
message = '» ' + App.i18n.translateInline('Not Found') + ' «'
|
||||||
|
else if data.error
|
||||||
|
message = App.i18n.translateInline(data.error)
|
||||||
|
else
|
||||||
|
message = '» ' + App.i18n.translateInline('Error') + ' «'
|
||||||
|
@notify
|
||||||
|
type: 'error'
|
||||||
|
msg: App.i18n.translateContent(message)
|
||||||
|
removeAll: true
|
||||||
|
|
||||||
|
App.Config.set('OutOfOffice', { prio: 2800, name: 'Out of Office', parent: '#profile', target: '#profile/out_of_office', permission: ['user_preferences.out_of_office+ticket.agent'], controller: Index }, 'NavBarProfile')
|
|
@ -20,7 +20,7 @@ class App.UiElement.postmaster_set
|
||||||
name: 'Customer'
|
name: 'Customer'
|
||||||
relation: 'User'
|
relation: 'User'
|
||||||
tag: 'user_autocompletion'
|
tag: 'user_autocompletion'
|
||||||
disableCreateUser: true
|
disableCreateObject: true
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
value: 'group_id'
|
value: 'group_id'
|
||||||
|
@ -32,7 +32,7 @@ class App.UiElement.postmaster_set
|
||||||
name: 'Owner'
|
name: 'Owner'
|
||||||
relation: 'User'
|
relation: 'User'
|
||||||
tag: 'user_autocompletion'
|
tag: 'user_autocompletion'
|
||||||
disableCreateUser: true
|
disableCreateObject: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
article:
|
article:
|
||||||
|
|
|
@ -2,5 +2,5 @@
|
||||||
class App.UiElement.user_autocompletion_search
|
class App.UiElement.user_autocompletion_search
|
||||||
@render: (attributeOrig, params = {}) ->
|
@render: (attributeOrig, params = {}) ->
|
||||||
attribute = _.clone(attributeOrig)
|
attribute = _.clone(attributeOrig)
|
||||||
attribute.disableCreateUser = true
|
attribute.disableCreateObject = true
|
||||||
new App.UserOrganizationAutocompletion(attribute: attribute, params: params).element()
|
new App.UserOrganizationAutocompletion(attribute: attribute, params: params).element()
|
||||||
|
|
|
@ -1499,7 +1499,7 @@ class InputsRef extends App.ControllerContent
|
||||||
null: false
|
null: false
|
||||||
relation: 'User'
|
relation: 'User'
|
||||||
autocapitalize: false
|
autocapitalize: false
|
||||||
disableCreateUser: true
|
disableCreateObject: true
|
||||||
multiple: true
|
multiple: true
|
||||||
|
|
||||||
@$('.userOrganizationAutocompletePlaceholder').replaceWith( userOrganizationAutocomplete.element() )
|
@$('.userOrganizationAutocompletePlaceholder').replaceWith( userOrganizationAutocomplete.element() )
|
||||||
|
|
|
@ -6,7 +6,7 @@ class App.TicketCustomer extends App.ControllerModal
|
||||||
|
|
||||||
content: ->
|
content: ->
|
||||||
configure_attributes = [
|
configure_attributes = [
|
||||||
{ name: 'customer_id', display: 'Customer', tag: 'user_autocompletion', null: false, placeholder: 'Enter Person or Organization/Company', minLengt: 2, disableCreateUser: true },
|
{ name: 'customer_id', display: 'Customer', tag: 'user_autocompletion', null: false, placeholder: 'Enter Person or Organization/Company', minLengt: 2, disableCreateObject: true },
|
||||||
]
|
]
|
||||||
controller = new App.ControllerForm(
|
controller = new App.ControllerForm(
|
||||||
model:
|
model:
|
||||||
|
|
|
@ -7,6 +7,12 @@ class App.WidgetAvatar extends App.ObserverController
|
||||||
email: true
|
email: true
|
||||||
image: true
|
image: true
|
||||||
vip: true
|
vip: true
|
||||||
|
out_of_office: true,
|
||||||
|
out_of_office_start_at: true,
|
||||||
|
out_of_office_end_at: true,
|
||||||
|
out_of_office_replacement_id: true,
|
||||||
|
active: true
|
||||||
|
|
||||||
globalRerender: false
|
globalRerender: false
|
||||||
|
|
||||||
render: (user) =>
|
render: (user) =>
|
||||||
|
|
|
@ -8,6 +8,7 @@ class App.Overview extends App.Model
|
||||||
{ name: 'role_ids', display: 'Available for Role', tag: 'column_select', multiple: true, null: false, relation: 'Role', translate: true },
|
{ name: 'role_ids', display: 'Available for Role', tag: 'column_select', multiple: true, null: false, relation: 'Role', translate: true },
|
||||||
{ name: 'user_ids', display: 'Available for User', tag: 'column_select', multiple: true, null: true, relation: 'User', sortBy: 'firstname' },
|
{ name: 'user_ids', display: 'Available for User', tag: 'column_select', multiple: true, null: true, relation: 'User', sortBy: 'firstname' },
|
||||||
{ name: 'organization_shared', display: 'Only available for Users with shared Organization', tag: 'select', options: { true: 'yes', false: 'no' }, default: false, null: true },
|
{ name: 'organization_shared', display: 'Only available for Users with shared Organization', tag: 'select', options: { true: 'yes', false: 'no' }, default: false, null: true },
|
||||||
|
{ name: 'out_of_office', display: 'Only available for Users which are replacements for other users.', tag: 'select', options: { true: 'yes', false: 'no' }, default: false, null: true },
|
||||||
{ name: 'condition', display: 'Conditions for shown Tickets', tag: 'ticket_selector', null: false },
|
{ name: 'condition', display: 'Conditions for shown Tickets', tag: 'ticket_selector', null: false },
|
||||||
{ name: 'prio', display: 'Prio', readonly: 1 },
|
{ name: 'prio', display: 'Prio', readonly: 1 },
|
||||||
{
|
{
|
||||||
|
@ -72,4 +73,4 @@ Sie können auch individuelle Übersichten für einzelne Agenten oder agenten Gr
|
||||||
'''
|
'''
|
||||||
|
|
||||||
uiUrl: ->
|
uiUrl: ->
|
||||||
'#ticket/view/' + @link
|
"#ticket/view/#{@link}"
|
||||||
|
|
|
@ -53,8 +53,14 @@ class App.User extends App.Model
|
||||||
cssClass += ' ' if cssClass
|
cssClass += ' ' if cssClass
|
||||||
cssClass += "size-#{ size }"
|
cssClass += "size-#{ size }"
|
||||||
|
|
||||||
|
if @active is false
|
||||||
|
cssClass += ' avatar--inactive'
|
||||||
|
|
||||||
|
if @isOutOfOffice()
|
||||||
|
cssClass += ' avatar--vacation'
|
||||||
|
|
||||||
if placement
|
if placement
|
||||||
placement = " data-placement='#{ placement }'"
|
placement = " data-placement='#{placement}'"
|
||||||
|
|
||||||
if !avatar
|
if !avatar
|
||||||
if type is 'personal'
|
if type is 'personal'
|
||||||
|
@ -104,6 +110,19 @@ class App.User extends App.Model
|
||||||
vip: vip
|
vip: vip
|
||||||
url: @imageUrl()
|
url: @imageUrl()
|
||||||
|
|
||||||
|
isOutOfOffice: ->
|
||||||
|
return false if @out_of_office isnt true
|
||||||
|
start_time = @out_of_office_start_at
|
||||||
|
return false if !start_time
|
||||||
|
end_time = @out_of_office_end_at
|
||||||
|
return false if !end_time
|
||||||
|
start_time = new Date(Date.parse(start_time))
|
||||||
|
end_time = new Date(Date.parse(end_time))
|
||||||
|
now = new Date((new Date).toDateString())
|
||||||
|
if start_time <= now && end_time >= now
|
||||||
|
return true
|
||||||
|
false
|
||||||
|
|
||||||
imageUrl: ->
|
imageUrl: ->
|
||||||
return if !@image
|
return if !@image
|
||||||
# set image url
|
# set image url
|
||||||
|
@ -237,3 +256,16 @@ class App.User extends App.Model
|
||||||
break
|
break
|
||||||
return access if access
|
return access if access
|
||||||
false
|
false
|
||||||
|
|
||||||
|
@outOfOfficeTextPlaceholder: ->
|
||||||
|
today = new Date()
|
||||||
|
outOfOfficeText = 'Christmas holiday'
|
||||||
|
if today.getMonth() < 3
|
||||||
|
outOfOfficeText = 'Easter holiday'
|
||||||
|
else if today.getMonth() < 9
|
||||||
|
outOfOfficeText = 'Summer holiday'
|
||||||
|
outOfOfficeText
|
||||||
|
|
||||||
|
outOfOfficeText: ->
|
||||||
|
return @preferences.out_of_office_text if !_.isEmpty(@preferences.out_of_office_text)
|
||||||
|
App.User.outOfOfficeTextPlaceholder()
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-header-title"><h1><%- @T('Out of Office') %></h1></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="action">
|
||||||
|
<div class="action-flow action-flow--row">
|
||||||
|
<div class="action-row">
|
||||||
|
<div class="action-flow action-flow--noWrap">
|
||||||
|
<h2><span class="action-form-status">
|
||||||
|
<% if @localData.out_of_office is true: %>
|
||||||
|
<%- @Icon('status', 'ok inline') %>
|
||||||
|
<% else: %>
|
||||||
|
<%- @Icon('status', 'error inline') %>
|
||||||
|
<% end %></span> <input id="out_of_office_reason" name="out_of_office_text" class="form-control form-control--inline" type="text" placeholder="<%- @Ti('e. g.') %> <%- @Ti(@placeholder) %>" value="<% if !_.isEmpty(@localData.out_of_office_text): %><%= @localData.out_of_office_text %><% end %>"></h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="action-block action-block--flex">
|
||||||
|
<div class="label"><%- @T('From') %></div>
|
||||||
|
<div class="form-group js-startDate"></div>
|
||||||
|
</div>
|
||||||
|
<div class="action-block action-block--flex">
|
||||||
|
<div class="label"><%- @T('Till') %></div>
|
||||||
|
<div class="form-group js-endDate"></div>
|
||||||
|
</div>
|
||||||
|
<div class="action-row">
|
||||||
|
<label for="out_of_office_replacement"><%- @T('Replacement') %></label>
|
||||||
|
<div class="dropdown js-recipientDropdown"></div>
|
||||||
|
</div>
|
||||||
|
<div class="action-controls">
|
||||||
|
<div class="btn btn--danger js-disabled"><%- @Ti('Disable') %></div>
|
||||||
|
<div class="btn btn--create js-enable"><%- @Ti('Enable') %></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
|
@ -3695,6 +3695,16 @@ footer {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--inactive {
|
||||||
|
filter: grayscale(100%);
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--vacation {
|
||||||
|
filter: grayscale(70%);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
&--unique {
|
&--unique {
|
||||||
background-image: image_url("/assets/images/avatar-bg.png");
|
background-image: image_url("/assets/images/avatar-bg.png");
|
||||||
background-size: 300px 226px;
|
background-size: 300px 226px;
|
||||||
|
@ -7816,6 +7826,10 @@ output {
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
.action-form-status .icon {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-block,
|
.action-block,
|
||||||
|
|
|
@ -538,7 +538,7 @@ Response:
|
||||||
}
|
}
|
||||||
|
|
||||||
Test:
|
Test:
|
||||||
curl http://localhost/api/v1/users/email_verify.json -v -u #{login}:#{password} -H "Content-Type: application/json" -X POST -d '{"token": "SoMeToKeN"}'
|
curl http://localhost/api/v1/users/email_verify -v -u #{login}:#{password} -H "Content-Type: application/json" -X POST -d '{"token": "SoMeToKeN"}'
|
||||||
|
|
||||||
=end
|
=end
|
||||||
|
|
||||||
|
@ -567,7 +567,7 @@ Response:
|
||||||
}
|
}
|
||||||
|
|
||||||
Test:
|
Test:
|
||||||
curl http://localhost/api/v1/users/email_verify_send.json -v -u #{login}:#{password} -H "Content-Type: application/json" -X POST -d '{"email": "some_email@example.com"}'
|
curl http://localhost/api/v1/users/email_verify_send -v -u #{login}:#{password} -H "Content-Type: application/json" -X POST -d '{"email": "some_email@example.com"}'
|
||||||
|
|
||||||
=end
|
=end
|
||||||
|
|
||||||
|
@ -626,7 +626,7 @@ Response:
|
||||||
}
|
}
|
||||||
|
|
||||||
Test:
|
Test:
|
||||||
curl http://localhost/api/v1/users/password_reset.json -v -u #{login}:#{password} -H "Content-Type: application/json" -X POST -d '{"username": "some_username"}'
|
curl http://localhost/api/v1/users/password_reset -v -u #{login}:#{password} -H "Content-Type: application/json" -X POST -d '{"username": "some_username"}'
|
||||||
|
|
||||||
=end
|
=end
|
||||||
|
|
||||||
|
@ -678,7 +678,7 @@ Response:
|
||||||
}
|
}
|
||||||
|
|
||||||
Test:
|
Test:
|
||||||
curl http://localhost/api/v1/users/password_reset_verify.json -v -u #{login}:#{password} -H "Content-Type: application/json" -X POST -d '{"token": "SoMeToKeN", "password" "new_password"}'
|
curl http://localhost/api/v1/users/password_reset_verify -v -u #{login}:#{password} -H "Content-Type: application/json" -X POST -d '{"token": "SoMeToKeN", "password" "new_password"}'
|
||||||
|
|
||||||
=end
|
=end
|
||||||
|
|
||||||
|
@ -734,7 +734,7 @@ Response:
|
||||||
}
|
}
|
||||||
|
|
||||||
Test:
|
Test:
|
||||||
curl http://localhost/api/v1/users/password_change.json -v -u #{login}:#{password} -H "Content-Type: application/json" -X POST -d '{"password_old": "password_old", "password_new": "password_new"}'
|
curl http://localhost/api/v1/users/password_change -v -u #{login}:#{password} -H "Content-Type: application/json" -X POST -d '{"password_old": "password_old", "password_new": "password_new"}'
|
||||||
|
|
||||||
=end
|
=end
|
||||||
|
|
||||||
|
@ -781,7 +781,7 @@ curl http://localhost/api/v1/users/password_change.json -v -u #{login}:#{passwor
|
||||||
=begin
|
=begin
|
||||||
|
|
||||||
Resource:
|
Resource:
|
||||||
PUT /api/v1/users/preferences.json
|
PUT /api/v1/users/preferences
|
||||||
|
|
||||||
Payload:
|
Payload:
|
||||||
{
|
{
|
||||||
|
@ -795,7 +795,7 @@ Response:
|
||||||
}
|
}
|
||||||
|
|
||||||
Test:
|
Test:
|
||||||
curl http://localhost/api/v1/users/preferences.json -v -u #{login}:#{password} -H "Content-Type: application/json" -X PUT -d '{"language": "de", "notifications": true}'
|
curl http://localhost/api/v1/users/preferences -v -u #{login}:#{password} -H "Content-Type: application/json" -X PUT -d '{"language": "de", "notifications": true}'
|
||||||
|
|
||||||
=end
|
=end
|
||||||
|
|
||||||
|
@ -808,7 +808,7 @@ curl http://localhost/api/v1/users/preferences.json -v -u #{login}:#{password} -
|
||||||
params[:user].each { |key, value|
|
params[:user].each { |key, value|
|
||||||
user.preferences[key.to_sym] = value
|
user.preferences[key.to_sym] = value
|
||||||
}
|
}
|
||||||
user.save
|
user.save!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
render json: { message: 'ok' }, status: :ok
|
render json: { message: 'ok' }, status: :ok
|
||||||
|
@ -817,7 +817,47 @@ curl http://localhost/api/v1/users/preferences.json -v -u #{login}:#{password} -
|
||||||
=begin
|
=begin
|
||||||
|
|
||||||
Resource:
|
Resource:
|
||||||
DELETE /api/v1/users/account.json
|
PUT /api/v1/users/out_of_office
|
||||||
|
|
||||||
|
Payload:
|
||||||
|
{
|
||||||
|
"out_of_office": true,
|
||||||
|
"out_of_office_start_at": true,
|
||||||
|
"out_of_office_end_at": true,
|
||||||
|
"out_of_office_replacement_id": 123,
|
||||||
|
"out_of_office_text": 'honeymoon'
|
||||||
|
}
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
:message => 'ok'
|
||||||
|
}
|
||||||
|
|
||||||
|
Test:
|
||||||
|
curl http://localhost/api/v1/users/out_of_office -v -u #{login}:#{password} -H "Content-Type: application/json" -X PUT -d '{"out_of_office": true, "out_of_office_replacement_id": 123}'
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
|
def out_of_office
|
||||||
|
raise Exceptions::UnprocessableEntity, 'No current user!' if !current_user
|
||||||
|
user = User.find(current_user.id)
|
||||||
|
user.with_lock do
|
||||||
|
user.assign_attributes(
|
||||||
|
out_of_office: params[:out_of_office],
|
||||||
|
out_of_office_start_at: params[:out_of_office_start_at],
|
||||||
|
out_of_office_end_at: params[:out_of_office_end_at],
|
||||||
|
out_of_office_replacement_id: params[:out_of_office_replacement_id],
|
||||||
|
)
|
||||||
|
user.preferences[:out_of_office_text] = params[:out_of_office_text]
|
||||||
|
user.save!
|
||||||
|
end
|
||||||
|
render json: { message: 'ok' }, status: :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
Resource:
|
||||||
|
DELETE /api/v1/users/account
|
||||||
|
|
||||||
Payload:
|
Payload:
|
||||||
{
|
{
|
||||||
|
@ -831,7 +871,7 @@ Response:
|
||||||
}
|
}
|
||||||
|
|
||||||
Test:
|
Test:
|
||||||
curl http://localhost/api/v1/users/account.json -v -u #{login}:#{password} -H "Content-Type: application/json" -X PUT -d '{"provider": "twitter", "uid": 581482342942}'
|
curl http://localhost/api/v1/users/account -v -u #{login}:#{password} -H "Content-Type: application/json" -X PUT -d '{"provider": "twitter", "uid": 581482342942}'
|
||||||
|
|
||||||
=end
|
=end
|
||||||
|
|
||||||
|
|
|
@ -19,11 +19,11 @@ returns
|
||||||
# get customer overviews
|
# get customer overviews
|
||||||
role_ids = User.joins(:roles).where(users: { id: current_user.id, active: true }, roles: { active: true }).pluck('roles.id')
|
role_ids = User.joins(:roles).where(users: { id: current_user.id, active: true }, roles: { active: true }).pluck('roles.id')
|
||||||
if current_user.permissions?('ticket.customer')
|
if current_user.permissions?('ticket.customer')
|
||||||
overviews = if current_user.organization_id && current_user.organization.shared
|
overview_filter = { active: true, organization_shared: false }
|
||||||
Overview.joins(:roles).where(overviews_roles: { role_id: role_ids }, overviews: { active: true }).distinct('overview.id').order(:prio)
|
if current_user.organization_id && current_user.organization.shared
|
||||||
else
|
overview_filter.delete(:organization_shared)
|
||||||
Overview.joins(:roles).where(overviews_roles: { role_id: role_ids }, overviews: { active: true, organization_shared: false }).distinct('overview.id').order(:prio)
|
|
||||||
end
|
end
|
||||||
|
overviews = Overview.joins(:roles).where(overviews_roles: { role_id: role_ids }, overviews: overview_filter).distinct('overview.id').order(:prio)
|
||||||
overviews_list = []
|
overviews_list = []
|
||||||
overviews.each { |overview|
|
overviews.each { |overview|
|
||||||
user_ids = overview.user_ids
|
user_ids = overview.user_ids
|
||||||
|
@ -35,7 +35,12 @@ returns
|
||||||
|
|
||||||
# get agent overviews
|
# get agent overviews
|
||||||
return [] if !current_user.permissions?('ticket.agent')
|
return [] if !current_user.permissions?('ticket.agent')
|
||||||
overviews = Overview.joins(:roles).where(overviews_roles: { role_id: role_ids }, overviews: { active: true }).distinct('overview.id').order(:prio)
|
overview_filter = { active: true }
|
||||||
|
overview_filter_not = { out_of_office: true }
|
||||||
|
if User.where('out_of_office = ? AND out_of_office_start_at <= ? AND out_of_office_end_at >= ? AND out_of_office_replacement_id = ? AND active = ?', true, Time.zone.today, Time.zone.today, current_user.id, true).count.positive?
|
||||||
|
overview_filter_not = {}
|
||||||
|
end
|
||||||
|
overviews = Overview.joins(:roles).where(overviews_roles: { role_id: role_ids }, overviews: overview_filter).where.not(overview_filter_not).distinct('overview.id').order(:prio)
|
||||||
overviews_list = []
|
overviews_list = []
|
||||||
overviews.each { |overview|
|
overviews.each { |overview|
|
||||||
user_ids = overview.user_ids
|
user_ids = overview.user_ids
|
||||||
|
|
|
@ -26,12 +26,11 @@ class Transaction::Notification
|
||||||
|
|
||||||
# return if we run import mode
|
# return if we run import mode
|
||||||
return if Setting.get('import_mode')
|
return if Setting.get('import_mode')
|
||||||
|
|
||||||
return if @item[:object] != 'Ticket'
|
return if @item[:object] != 'Ticket'
|
||||||
|
|
||||||
return if @params[:disable_notification]
|
return if @params[:disable_notification]
|
||||||
|
|
||||||
ticket = Ticket.find(@item[:object_id])
|
ticket = Ticket.find_by(id: @item[:object_id])
|
||||||
|
return if !ticket
|
||||||
if @item[:article_id]
|
if @item[:article_id]
|
||||||
article = Ticket::Article.find(@item[:article_id])
|
article = Ticket::Article.find(@item[:article_id])
|
||||||
|
|
||||||
|
@ -47,20 +46,41 @@ class Transaction::Notification
|
||||||
|
|
||||||
# find recipients
|
# find recipients
|
||||||
recipients_and_channels = []
|
recipients_and_channels = []
|
||||||
|
recipients_reason = {}
|
||||||
|
|
||||||
# loop through all users
|
# loop through all users
|
||||||
possible_recipients = User.group_access(ticket.group_id, 'full').sort_by(&:login)
|
possible_recipients = User.group_access(ticket.group_id, 'full').sort_by(&:login)
|
||||||
if ticket.owner_id == 1
|
|
||||||
|
# apply owner
|
||||||
|
if ticket.owner_id != 1
|
||||||
possible_recipients.push ticket.owner
|
possible_recipients.push ticket.owner
|
||||||
|
recipients_reason[ticket.owner_id] = 'are assigned'
|
||||||
|
end
|
||||||
|
|
||||||
|
# apply out of office agents
|
||||||
|
possible_recipients_additions = Set.new
|
||||||
|
possible_recipients.each { |user|
|
||||||
|
recursive_ooo_replacements(
|
||||||
|
user: user,
|
||||||
|
replacements: possible_recipients_additions,
|
||||||
|
reasons: recipients_reason,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if possible_recipients_additions.present?
|
||||||
|
# join unique entries
|
||||||
|
possible_recipients = possible_recipients | possible_recipients_additions.to_a
|
||||||
end
|
end
|
||||||
|
|
||||||
already_checked_recipient_ids = {}
|
already_checked_recipient_ids = {}
|
||||||
possible_recipients.each { |user|
|
possible_recipients.each { |user|
|
||||||
result = NotificationFactory::Mailer.notification_settings(user, ticket, @item[:type])
|
result = NotificationFactory::Mailer.notification_settings(user, ticket, @item[:type])
|
||||||
next if !result
|
next if !result
|
||||||
next if already_checked_recipient_ids[result[:user].id]
|
next if already_checked_recipient_ids[user.id]
|
||||||
already_checked_recipient_ids[result[:user].id] = true
|
already_checked_recipient_ids[user.id] = true
|
||||||
recipients_and_channels.push result
|
recipients_and_channels.push result
|
||||||
|
next if recipients_reason[user.id]
|
||||||
|
recipients_reason[user.id] = 'are in group'
|
||||||
}
|
}
|
||||||
|
|
||||||
# send notifications
|
# send notifications
|
||||||
|
@ -76,7 +96,7 @@ class Transaction::Notification
|
||||||
end
|
end
|
||||||
|
|
||||||
# ignore inactive users
|
# ignore inactive users
|
||||||
next if !user.active
|
next if !user.active?
|
||||||
|
|
||||||
# ignore if no changes has been done
|
# ignore if no changes has been done
|
||||||
changes = human_changes(user, ticket)
|
changes = human_changes(user, ticket)
|
||||||
|
@ -180,6 +200,7 @@ class Transaction::Notification
|
||||||
recipient: user,
|
recipient: user,
|
||||||
current_user: current_user,
|
current_user: current_user,
|
||||||
changes: changes,
|
changes: changes,
|
||||||
|
reason: recipients_reason[user.id],
|
||||||
},
|
},
|
||||||
message_id: "<notification.#{DateTime.current.to_s(:number)}.#{ticket.id}.#{user.id}.#{rand(999_999)}@#{Setting.get('fqdn')}>",
|
message_id: "<notification.#{DateTime.current.to_s(:number)}.#{ticket.id}.#{user.id}.#{rand(999_999)}@#{Setting.get('fqdn')}>",
|
||||||
references: ticket.get_references,
|
references: ticket.get_references,
|
||||||
|
@ -296,4 +317,27 @@ class Transaction::Notification
|
||||||
changes
|
changes
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def recursive_ooo_replacements(user:, replacements:, reasons:, level: 0)
|
||||||
|
if level == 10
|
||||||
|
Rails.logger.warn("Found more than 10 replacement levels for agent #{user}.")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
replacement = user.out_of_office_agent
|
||||||
|
return if !replacement
|
||||||
|
# return for already found, added and checked users
|
||||||
|
# to prevent re-doing complete lookup paths
|
||||||
|
return if !replacements.add?(replacement)
|
||||||
|
reasons[replacement.id] = 'are the out-of-office replacement of the owner'
|
||||||
|
|
||||||
|
recursive_ooo_replacements(
|
||||||
|
user: replacement,
|
||||||
|
replacements: replacements,
|
||||||
|
reasons: reasons,
|
||||||
|
level: level + 1
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -39,8 +39,8 @@ class User < ApplicationModel
|
||||||
include User::SearchIndex
|
include User::SearchIndex
|
||||||
|
|
||||||
before_validation :check_name, :check_email, :check_login, :ensure_uniq_email, :ensure_password, :ensure_roles, :ensure_identifier
|
before_validation :check_name, :check_email, :check_login, :ensure_uniq_email, :ensure_password, :ensure_roles, :ensure_identifier
|
||||||
before_create :check_preferences_default, :validate_roles, :domain_based_assignment, :set_locale
|
before_create :check_preferences_default, :validate_roles, :validate_ooo, :domain_based_assignment, :set_locale
|
||||||
before_update :check_preferences_default, :validate_roles, :reset_login_failed
|
before_update :check_preferences_default, :validate_roles, :validate_ooo, :reset_login_failed
|
||||||
after_create :avatar_for_email_check
|
after_create :avatar_for_email_check
|
||||||
after_update :avatar_for_email_check
|
after_update :avatar_for_email_check
|
||||||
after_destroy :avatar_destroy, :user_device_destroy
|
after_destroy :avatar_destroy, :user_device_destroy
|
||||||
|
@ -160,6 +160,45 @@ returns
|
||||||
|
|
||||||
=begin
|
=begin
|
||||||
|
|
||||||
|
check if user is in role
|
||||||
|
|
||||||
|
user = User.find(123)
|
||||||
|
result = user.out_of_office?
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = true|false
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
|
def out_of_office?
|
||||||
|
return false if out_of_office != true
|
||||||
|
return false if out_of_office_start_at.blank?
|
||||||
|
return false if out_of_office_end_at.blank?
|
||||||
|
Time.zone.today.between?(out_of_office_start_at, out_of_office_end_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
check if user is in role
|
||||||
|
|
||||||
|
user = User.find(123)
|
||||||
|
result = user.out_of_office_agent
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = user_model
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
|
def out_of_office_agent
|
||||||
|
return if !out_of_office?
|
||||||
|
return if out_of_office_replacement_id.blank?
|
||||||
|
User.find_by(id: out_of_office_replacement_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
get users activity stream
|
get users activity stream
|
||||||
|
|
||||||
user = User.find(123)
|
user = User.find(123)
|
||||||
|
@ -922,6 +961,15 @@ returns
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate_ooo
|
||||||
|
return true if out_of_office != true
|
||||||
|
raise Exceptions::UnprocessableEntity, 'out of office start is required' if out_of_office_start_at.blank?
|
||||||
|
raise Exceptions::UnprocessableEntity, 'out of office end is required' if out_of_office_end_at.blank?
|
||||||
|
raise Exceptions::UnprocessableEntity, 'out of office end is before start' if out_of_office_start_at > out_of_office_end_at
|
||||||
|
raise Exceptions::UnprocessableEntity, 'out of office replacement user is required' if out_of_office_replacement_id.blank?
|
||||||
|
raise Exceptions::UnprocessableEntity, 'out of office no such replacement user' if !User.find_by(id: out_of_office_replacement_id)
|
||||||
|
true
|
||||||
|
end
|
||||||
=begin
|
=begin
|
||||||
|
|
||||||
checks if the current user is the last one
|
checks if the current user is the last one
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
.footer {
|
.footer {
|
||||||
|
font-size: 10px;
|
||||||
color: #aaaaaa;
|
color: #aaaaaa;
|
||||||
border-top-style:solid;
|
border-top-style:solid;
|
||||||
border-top-width:1px;
|
border-top-width:1px;
|
||||||
|
@ -40,6 +41,13 @@
|
||||||
|
|
||||||
<% if @objects[:standalone] != true %>
|
<% if @objects[:standalone] != true %>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<a href="<%= c 'http_type' %>://<%= c 'fqdn' %>/#profile/notifications"><%= t 'Manage your notifications settings' %></a> <% if !c('organization').empty? %>| <%= c 'organization' %><% end %>
|
<% if @objects[:reason] %>
|
||||||
|
<% reason = t('You are receiving this because you "%s".') %>
|
||||||
|
<% reason_item = t (@objects[:reason]) %>
|
||||||
|
<% reason.gsub!('%s', reason_item) %>
|
||||||
|
<%= reason %> |
|
||||||
|
<% end %>
|
||||||
|
<a href="<%= c 'http_type' %>://<%= c 'fqdn' %>/#profile/notifications"><%= t 'Manage your notifications settings' %></a>
|
||||||
|
<% if c('organization').present? %>| <%= c 'organization' %><% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -8,6 +8,7 @@ Zammad::Application.routes.draw do
|
||||||
match api_path + '/users/password_reset_verify', to: 'users#password_reset_verify', via: :post
|
match api_path + '/users/password_reset_verify', to: 'users#password_reset_verify', via: :post
|
||||||
match api_path + '/users/password_change', to: 'users#password_change', via: :post
|
match api_path + '/users/password_change', to: 'users#password_change', via: :post
|
||||||
match api_path + '/users/preferences', to: 'users#preferences', via: :put
|
match api_path + '/users/preferences', to: 'users#preferences', via: :put
|
||||||
|
match api_path + '/users/out_of_office', to: 'users#out_of_office', via: :put
|
||||||
match api_path + '/users/account', to: 'users#account_remove', via: :delete
|
match api_path + '/users/account', to: 'users#account_remove', via: :delete
|
||||||
|
|
||||||
match api_path + '/users/avatar', to: 'users#avatar_new', via: :post
|
match api_path + '/users/avatar', to: 'users#avatar_new', via: :post
|
||||||
|
|
|
@ -40,6 +40,10 @@ class CreateBase < ActiveRecord::Migration
|
||||||
t.timestamp :last_login, limit: 3, null: true
|
t.timestamp :last_login, limit: 3, null: true
|
||||||
t.string :source, limit: 200, null: true
|
t.string :source, limit: 200, null: true
|
||||||
t.integer :login_failed, null: false, default: 0
|
t.integer :login_failed, null: false, default: 0
|
||||||
|
t.boolean :out_of_office, null: false, default: false
|
||||||
|
t.date :out_of_office_start_at, null: true
|
||||||
|
t.date :out_of_office_end_at, null: true
|
||||||
|
t.integer :out_of_office_replacement_id, null: true
|
||||||
t.string :preferences, limit: 8000, null: true
|
t.string :preferences, limit: 8000, null: true
|
||||||
t.integer :updated_by_id, null: false
|
t.integer :updated_by_id, null: false
|
||||||
t.integer :created_by_id, null: false
|
t.integer :created_by_id, null: false
|
||||||
|
@ -54,10 +58,13 @@ class CreateBase < ActiveRecord::Migration
|
||||||
add_index :users, [:phone]
|
add_index :users, [:phone]
|
||||||
add_index :users, [:fax]
|
add_index :users, [:fax]
|
||||||
add_index :users, [:mobile]
|
add_index :users, [:mobile]
|
||||||
|
add_index :users, [:out_of_office, :out_of_office_start_at, :out_of_office_end_at], name: 'index_out_of_office'
|
||||||
|
add_index :users, [:out_of_office_replacement_id]
|
||||||
add_index :users, [:source]
|
add_index :users, [:source]
|
||||||
add_index :users, [:created_by_id]
|
add_index :users, [:created_by_id]
|
||||||
add_foreign_key :users, :users, column: :created_by_id
|
add_foreign_key :users, :users, column: :created_by_id
|
||||||
add_foreign_key :users, :users, column: :updated_by_id
|
add_foreign_key :users, :users, column: :updated_by_id
|
||||||
|
add_foreign_key :users, :users, column: :out_of_office_replacement_id
|
||||||
|
|
||||||
create_table :signatures do |t|
|
create_table :signatures do |t|
|
||||||
t.string :name, limit: 100, null: false
|
t.string :name, limit: 100, null: false
|
||||||
|
|
|
@ -238,6 +238,7 @@ class CreateTicket < ActiveRecord::Migration
|
||||||
t.column :order, :string, limit: 2500, null: false
|
t.column :order, :string, limit: 2500, null: false
|
||||||
t.column :group_by, :string, limit: 250, null: true
|
t.column :group_by, :string, limit: 250, null: true
|
||||||
t.column :organization_shared, :boolean, null: false, default: false
|
t.column :organization_shared, :boolean, null: false, default: false
|
||||||
|
t.column :out_of_office, :boolean, null: false, default: false
|
||||||
t.column :view, :string, limit: 1000, null: false
|
t.column :view, :string, limit: 1000, null: false
|
||||||
t.column :active, :boolean, null: false, default: true
|
t.column :active, :boolean, null: false, default: true
|
||||||
t.column :updated_by_id, :integer, null: false
|
t.column :updated_by_id, :integer, null: false
|
||||||
|
|
53
db/migrate/20170826000001_out_of_office.rb
Normal file
53
db/migrate/20170826000001_out_of_office.rb
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
class OutOfOffice < ActiveRecord::Migration
|
||||||
|
def up
|
||||||
|
|
||||||
|
# return if it's a new setup
|
||||||
|
return if !Setting.find_by(name: 'system_init_done')
|
||||||
|
|
||||||
|
add_column :overviews, :out_of_office, :boolean, null: false, default: false
|
||||||
|
Overview.reset_column_information
|
||||||
|
|
||||||
|
role_ids = Role.with_permissions(['ticket.agent']).map(&:id)
|
||||||
|
overview_role = Role.find_by(name: 'Agent')
|
||||||
|
Overview.create_if_not_exists(
|
||||||
|
name: 'My replacement Tickets',
|
||||||
|
link: 'my_replacement_tickets',
|
||||||
|
prio: 1080,
|
||||||
|
role_ids: role_ids,
|
||||||
|
out_of_office: true,
|
||||||
|
condition: {
|
||||||
|
'ticket.state_id' => {
|
||||||
|
operator: 'is',
|
||||||
|
value: Ticket::State.by_category(:open).pluck(:id),
|
||||||
|
},
|
||||||
|
#'ticket.out_of_office_replacement_id' => {
|
||||||
|
# operator: 'is',
|
||||||
|
# pre_condition: 'current_user.organization_id',
|
||||||
|
#},
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
by: 'created_at',
|
||||||
|
direction: 'DESC',
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
d: %w(title customer group owner escalation_at),
|
||||||
|
s: %w(title customer group owner escalation_at),
|
||||||
|
m: %w(number title customer group owner escalation_at),
|
||||||
|
view_mode_default: 's',
|
||||||
|
},
|
||||||
|
updated_by_id: 1,
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
add_column :users, :out_of_office, :boolean, null: false, default: false
|
||||||
|
add_column :users, :out_of_office_start_at, :date, null: true
|
||||||
|
add_column :users, :out_of_office_end_at, :date, null: true
|
||||||
|
add_column :users, :out_of_office_replacement_id, :integer, null: true
|
||||||
|
|
||||||
|
add_index :users, [:out_of_office, :out_of_office_start_at, :out_of_office_end_at], name: 'index_out_of_office'
|
||||||
|
add_index :users, [:out_of_office_replacement_id]
|
||||||
|
add_foreign_key :users, :users, column: :out_of_office_replacement_id
|
||||||
|
|
||||||
|
Cache.clear
|
||||||
|
end
|
||||||
|
end
|
|
@ -160,6 +160,34 @@ Overview.create_if_not_exists(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Overview.create_if_not_exists(
|
||||||
|
name: 'My replacement Tickets',
|
||||||
|
link: 'my_replacement_tickets',
|
||||||
|
prio: 1080,
|
||||||
|
role_ids: [overview_role.id],
|
||||||
|
out_of_office: true,
|
||||||
|
condition: {
|
||||||
|
'ticket.state_id' => {
|
||||||
|
operator: 'is',
|
||||||
|
value: Ticket::State.by_category(:open).pluck(:id),
|
||||||
|
},
|
||||||
|
#'ticket.out_of_office_replacement_id' => {
|
||||||
|
# operator: 'is',
|
||||||
|
# pre_condition: 'current_user.organization_id',
|
||||||
|
#},
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
by: 'created_at',
|
||||||
|
direction: 'DESC',
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
d: %w(title customer group owner escalation_at),
|
||||||
|
s: %w(title customer group owner escalation_at),
|
||||||
|
m: %w(number title customer group owner escalation_at),
|
||||||
|
view_mode_default: 's',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
overview_role = Role.find_by(name: 'Customer')
|
overview_role = Role.find_by(name: 'Customer')
|
||||||
Overview.create_if_not_exists(
|
Overview.create_if_not_exists(
|
||||||
name: 'My Tickets',
|
name: 'My Tickets',
|
||||||
|
|
|
@ -306,7 +306,7 @@ Permission.create_if_not_exists(
|
||||||
)
|
)
|
||||||
Permission.create_if_not_exists(
|
Permission.create_if_not_exists(
|
||||||
name: 'ticket.customer',
|
name: 'ticket.customer',
|
||||||
note: 'Access to Customer Tickets based on current_user.id and current_user.organization_id',
|
note: 'Access to Customer Tickets based on current_user and organization',
|
||||||
preferences: {
|
preferences: {
|
||||||
not: ['ticket.agent'],
|
not: ['ticket.agent'],
|
||||||
},
|
},
|
||||||
|
|
|
@ -35,11 +35,32 @@ returns
|
||||||
matrix = user.preferences['notification_config']['matrix']
|
matrix = user.preferences['notification_config']['matrix']
|
||||||
return if !matrix
|
return if !matrix
|
||||||
|
|
||||||
# check if group is in selecd groups
|
owned_by_nobody = false
|
||||||
if ticket.owner_id != user.id
|
owned_by_me = false
|
||||||
|
if ticket.owner_id == 1
|
||||||
|
owned_by_nobody = true
|
||||||
|
elsif ticket.owner_id == user.id
|
||||||
|
owned_by_me = true
|
||||||
|
else
|
||||||
|
# check the replacement chain of max 10
|
||||||
|
# if the current user is in it
|
||||||
|
check_for = ticket.owner
|
||||||
|
10.times do
|
||||||
|
replacement = check_for.out_of_office_agent
|
||||||
|
break if !replacement
|
||||||
|
|
||||||
|
check_for = replacement
|
||||||
|
next if replacement.id != user.id
|
||||||
|
|
||||||
|
owned_by_me = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# check if group is in selected groups
|
||||||
|
if !owned_by_me
|
||||||
selected_group_ids = user.preferences['notification_config']['group_ids']
|
selected_group_ids = user.preferences['notification_config']['group_ids']
|
||||||
if selected_group_ids
|
if selected_group_ids.is_a?(Array)
|
||||||
if selected_group_ids.class == Array
|
|
||||||
hit = nil
|
hit = nil
|
||||||
if selected_group_ids.empty?
|
if selected_group_ids.empty?
|
||||||
hit = true
|
hit = true
|
||||||
|
@ -54,8 +75,7 @@ returns
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
return if !hit
|
return if !hit # no group access
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return if !matrix[type]
|
return if !matrix[type]
|
||||||
|
@ -64,13 +84,13 @@ returns
|
||||||
return if !data['criteria']
|
return if !data['criteria']
|
||||||
channels = data['channel']
|
channels = data['channel']
|
||||||
return if !channels
|
return if !channels
|
||||||
if data['criteria']['owned_by_me'] && ticket.owner_id == user.id
|
if data['criteria']['owned_by_me'] && owned_by_me
|
||||||
return {
|
return {
|
||||||
user: user,
|
user: user,
|
||||||
channels: channels
|
channels: channels
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
if data['criteria']['owned_by_nobody'] && ticket.owner_id == 1
|
if data['criteria']['owned_by_nobody'] && owned_by_nobody
|
||||||
return {
|
return {
|
||||||
user: user,
|
user: user,
|
||||||
channels: channels
|
channels: channels
|
||||||
|
|
|
@ -26,6 +26,34 @@ RSpec.describe User do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context '#out_of_office_agent' do
|
||||||
|
|
||||||
|
it 'responds to out_of_office_agent' do
|
||||||
|
user = create(:user)
|
||||||
|
expect(user).to respond_to(:out_of_office_agent)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'replacement' do
|
||||||
|
|
||||||
|
it 'finds assigned' do
|
||||||
|
user_replacement = create(:user)
|
||||||
|
|
||||||
|
user_ooo = create(:user,
|
||||||
|
out_of_office: true,
|
||||||
|
out_of_office_start_at: Time.zone.yesterday,
|
||||||
|
out_of_office_end_at: Time.zone.tomorrow,
|
||||||
|
out_of_office_replacement_id: user_replacement.id,)
|
||||||
|
|
||||||
|
expect(user_ooo.out_of_office_agent).to eq user_replacement
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'finds none for available users' do
|
||||||
|
user = create(:user)
|
||||||
|
expect(user.out_of_office_agent).to be nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context '#max_login_failed?' do
|
context '#max_login_failed?' do
|
||||||
|
|
||||||
it 'responds to max_login_failed?' do
|
it 'responds to max_login_failed?' do
|
||||||
|
|
|
@ -180,6 +180,10 @@ class ZendeskImportTest < ActiveSupport::TestCase
|
||||||
last_login
|
last_login
|
||||||
source
|
source
|
||||||
login_failed
|
login_failed
|
||||||
|
out_of_office
|
||||||
|
out_of_office_start_at
|
||||||
|
out_of_office_end_at
|
||||||
|
out_of_office_replacement_id
|
||||||
preferences
|
preferences
|
||||||
updated_by_id
|
updated_by_id
|
||||||
created_by_id
|
created_by_id
|
||||||
|
|
|
@ -56,6 +56,7 @@ class TicketNotificationTest < ActiveSupport::TestCase
|
||||||
lastname: 'Agent1',
|
lastname: 'Agent1',
|
||||||
email: 'ticket-notification-agent1@example.com',
|
email: 'ticket-notification-agent1@example.com',
|
||||||
password: 'agentpw',
|
password: 'agentpw',
|
||||||
|
out_of_office: false,
|
||||||
active: true,
|
active: true,
|
||||||
roles: roles,
|
roles: roles,
|
||||||
groups: groups,
|
groups: groups,
|
||||||
|
@ -71,6 +72,7 @@ class TicketNotificationTest < ActiveSupport::TestCase
|
||||||
lastname: 'Agent2',
|
lastname: 'Agent2',
|
||||||
email: 'ticket-notification-agent2@example.com',
|
email: 'ticket-notification-agent2@example.com',
|
||||||
password: 'agentpw',
|
password: 'agentpw',
|
||||||
|
out_of_office: false,
|
||||||
active: true,
|
active: true,
|
||||||
roles: roles,
|
roles: roles,
|
||||||
groups: groups,
|
groups: groups,
|
||||||
|
@ -80,6 +82,38 @@ class TicketNotificationTest < ActiveSupport::TestCase
|
||||||
updated_by_id: 1,
|
updated_by_id: 1,
|
||||||
created_by_id: 1,
|
created_by_id: 1,
|
||||||
)
|
)
|
||||||
|
@agent3 = User.create_or_update(
|
||||||
|
login: 'ticket-notification-agent3@example.com',
|
||||||
|
firstname: 'Notification',
|
||||||
|
lastname: 'Agent3',
|
||||||
|
email: 'ticket-notification-agent3@example.com',
|
||||||
|
password: 'agentpw',
|
||||||
|
out_of_office: false,
|
||||||
|
active: true,
|
||||||
|
roles: roles,
|
||||||
|
groups: groups,
|
||||||
|
preferences: {
|
||||||
|
locale: 'de-de',
|
||||||
|
},
|
||||||
|
updated_by_id: 1,
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
@agent4 = User.create_or_update(
|
||||||
|
login: 'ticket-notification-agent4@example.com',
|
||||||
|
firstname: 'Notification',
|
||||||
|
lastname: 'Agent4',
|
||||||
|
email: 'ticket-notification-agent4@example.com',
|
||||||
|
password: 'agentpw',
|
||||||
|
out_of_office: false,
|
||||||
|
active: true,
|
||||||
|
roles: roles,
|
||||||
|
groups: groups,
|
||||||
|
preferences: {
|
||||||
|
locale: 'de-de',
|
||||||
|
},
|
||||||
|
updated_by_id: 1,
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
Group.create_if_not_exists(
|
Group.create_if_not_exists(
|
||||||
name: 'WithoutAccess',
|
name: 'WithoutAccess',
|
||||||
note: 'Test for notification check.',
|
note: 'Test for notification check.',
|
||||||
|
@ -944,6 +978,126 @@ class TicketNotificationTest < ActiveSupport::TestCase
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test 'ticket notification - out of office' do
|
||||||
|
|
||||||
|
# create ticket in group
|
||||||
|
ticket1 = Ticket.create!(
|
||||||
|
title: 'some notification test out of office',
|
||||||
|
group: Group.lookup(name: 'TicketNotificationTest'),
|
||||||
|
customer: @customer,
|
||||||
|
owner_id: @agent2.id,
|
||||||
|
#state: Ticket::State.lookup(name: 'new'),
|
||||||
|
#priority: Ticket::Priority.lookup(name: '2 normal'),
|
||||||
|
updated_by_id: @customer.id,
|
||||||
|
created_by_id: @customer.id,
|
||||||
|
)
|
||||||
|
Ticket::Article.create!(
|
||||||
|
ticket_id: ticket1.id,
|
||||||
|
from: 'some_sender@example.com',
|
||||||
|
to: 'some_recipient@example.com',
|
||||||
|
subject: 'some subject',
|
||||||
|
message_id: 'some@id',
|
||||||
|
body: 'some message',
|
||||||
|
internal: false,
|
||||||
|
sender: Ticket::Article::Sender.where(name: 'Customer').first,
|
||||||
|
type: Ticket::Article::Type.where(name: 'email').first,
|
||||||
|
updated_by_id: @customer.id,
|
||||||
|
created_by_id: @customer.id,
|
||||||
|
)
|
||||||
|
assert(ticket1, 'ticket created - ticket notification simple')
|
||||||
|
|
||||||
|
# execute object transaction
|
||||||
|
Observer::Transaction.commit
|
||||||
|
Scheduler.worker(true)
|
||||||
|
|
||||||
|
# verify notifications to @agent1 + @agent2
|
||||||
|
assert_equal(0, NotificationFactory::Mailer.already_sent?(ticket1, @agent1, 'email'), ticket1.id)
|
||||||
|
assert_equal(1, NotificationFactory::Mailer.already_sent?(ticket1, @agent2, 'email'), ticket1.id)
|
||||||
|
assert_equal(0, NotificationFactory::Mailer.already_sent?(ticket1, @agent3, 'email'), ticket1.id)
|
||||||
|
assert_equal(0, NotificationFactory::Mailer.already_sent?(ticket1, @agent4, 'email'), ticket1.id)
|
||||||
|
|
||||||
|
@agent2.out_of_office = true
|
||||||
|
@agent2.preferences[:out_of_office_text] = 'at the doctor'
|
||||||
|
@agent2.out_of_office_replacement_id = @agent3.id
|
||||||
|
@agent2.out_of_office_start_at = Time.zone.today - 2.days
|
||||||
|
@agent2.out_of_office_end_at = Time.zone.today + 2.days
|
||||||
|
@agent2.save!
|
||||||
|
|
||||||
|
# create ticket in group
|
||||||
|
ticket2 = Ticket.create!(
|
||||||
|
title: 'some notification test out of office',
|
||||||
|
group: Group.lookup(name: 'TicketNotificationTest'),
|
||||||
|
customer: @customer,
|
||||||
|
owner_id: @agent2.id,
|
||||||
|
#state: Ticket::State.lookup(name: 'new'),
|
||||||
|
#priority: Ticket::Priority.lookup(name: '2 normal'),
|
||||||
|
updated_by_id: @customer.id,
|
||||||
|
created_by_id: @customer.id,
|
||||||
|
)
|
||||||
|
Ticket::Article.create!(
|
||||||
|
ticket_id: ticket2.id,
|
||||||
|
from: 'some_sender@example.com',
|
||||||
|
to: 'some_recipient@example.com',
|
||||||
|
subject: 'some subject',
|
||||||
|
message_id: 'some@id',
|
||||||
|
body: 'some message',
|
||||||
|
internal: false,
|
||||||
|
sender: Ticket::Article::Sender.where(name: 'Customer').first,
|
||||||
|
type: Ticket::Article::Type.where(name: 'email').first,
|
||||||
|
updated_by_id: @customer.id,
|
||||||
|
created_by_id: @customer.id,
|
||||||
|
)
|
||||||
|
assert(ticket2, 'ticket created - ticket notification simple')
|
||||||
|
|
||||||
|
# execute object transaction
|
||||||
|
Observer::Transaction.commit
|
||||||
|
Scheduler.worker(true)
|
||||||
|
|
||||||
|
# verify notifications to @agent1 + @agent2
|
||||||
|
assert_equal(0, NotificationFactory::Mailer.already_sent?(ticket2, @agent1, 'email'), ticket2.id)
|
||||||
|
assert_equal(1, NotificationFactory::Mailer.already_sent?(ticket2, @agent2, 'email'), ticket2.id)
|
||||||
|
assert_equal(1, NotificationFactory::Mailer.already_sent?(ticket2, @agent3, 'email'), ticket2.id)
|
||||||
|
assert_equal(0, NotificationFactory::Mailer.already_sent?(ticket2, @agent4, 'email'), ticket2.id)
|
||||||
|
|
||||||
|
# update ticket attributes
|
||||||
|
ticket2.title = "#{ticket2.title} - #2"
|
||||||
|
ticket2.priority = Ticket::Priority.lookup(name: '3 high')
|
||||||
|
ticket2.save!
|
||||||
|
|
||||||
|
# execute object transaction
|
||||||
|
Observer::Transaction.commit
|
||||||
|
Scheduler.worker(true)
|
||||||
|
|
||||||
|
# verify notifications to @agent1 + @agent2
|
||||||
|
assert_equal(0, NotificationFactory::Mailer.already_sent?(ticket2, @agent1, 'email'), ticket2.id)
|
||||||
|
assert_equal(2, NotificationFactory::Mailer.already_sent?(ticket2, @agent2, 'email'), ticket2.id)
|
||||||
|
assert_equal(2, NotificationFactory::Mailer.already_sent?(ticket2, @agent3, 'email'), ticket2.id)
|
||||||
|
assert_equal(0, NotificationFactory::Mailer.already_sent?(ticket2, @agent4, 'email'), ticket2.id)
|
||||||
|
|
||||||
|
@agent3.out_of_office = true
|
||||||
|
@agent3.preferences[:out_of_office_text] = 'at the doctor'
|
||||||
|
@agent3.out_of_office_replacement_id = @agent4.id
|
||||||
|
@agent3.out_of_office_start_at = Time.zone.today - 2.days
|
||||||
|
@agent3.out_of_office_end_at = Time.zone.today + 2.days
|
||||||
|
@agent3.save!
|
||||||
|
|
||||||
|
# update ticket attributes
|
||||||
|
ticket2.title = "#{ticket2.title} - #3"
|
||||||
|
ticket2.priority = Ticket::Priority.lookup(name: '3 high')
|
||||||
|
ticket2.save!
|
||||||
|
|
||||||
|
# execute object transaction
|
||||||
|
Observer::Transaction.commit
|
||||||
|
Scheduler.worker(true)
|
||||||
|
|
||||||
|
# verify notifications to @agent1 + @agent2
|
||||||
|
assert_equal(0, NotificationFactory::Mailer.already_sent?(ticket2, @agent1, 'email'), ticket2.id)
|
||||||
|
assert_equal(3, NotificationFactory::Mailer.already_sent?(ticket2, @agent2, 'email'), ticket2.id)
|
||||||
|
assert_equal(3, NotificationFactory::Mailer.already_sent?(ticket2, @agent3, 'email'), ticket2.id)
|
||||||
|
assert_equal(1, NotificationFactory::Mailer.already_sent?(ticket2, @agent4, 'email'), ticket2.id)
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
test 'ticket notification template' do
|
test 'ticket notification template' do
|
||||||
|
|
||||||
# create ticket in group
|
# create ticket in group
|
||||||
|
|
131
test/unit/user_out_of_office_test.rb
Normal file
131
test/unit/user_out_of_office_test.rb
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
class UserOutOfOfficeTest < ActiveSupport::TestCase
|
||||||
|
setup do
|
||||||
|
|
||||||
|
UserInfo.current_user_id = 1
|
||||||
|
|
||||||
|
groups = Group.all
|
||||||
|
roles = Role.where(name: 'Agent')
|
||||||
|
@agent1 = User.create_or_update(
|
||||||
|
login: 'user-out_of_office-agent1@example.com',
|
||||||
|
firstname: 'UserOutOfOffice',
|
||||||
|
lastname: 'Agent1',
|
||||||
|
email: 'user-out_of_office-agent1@example.com',
|
||||||
|
password: 'agentpw',
|
||||||
|
active: true,
|
||||||
|
roles: roles,
|
||||||
|
groups: groups,
|
||||||
|
)
|
||||||
|
@agent2 = User.create_or_update(
|
||||||
|
login: 'user-out_of_office-agent2@example.com',
|
||||||
|
firstname: 'UserOutOfOffice',
|
||||||
|
lastname: 'Agent2',
|
||||||
|
email: 'user-out_of_office-agent2@example.com',
|
||||||
|
password: 'agentpw',
|
||||||
|
active: true,
|
||||||
|
roles: roles,
|
||||||
|
groups: groups,
|
||||||
|
)
|
||||||
|
@agent3 = User.create_or_update(
|
||||||
|
login: 'user-out_of_office-agent3@example.com',
|
||||||
|
firstname: 'UserOutOfOffice',
|
||||||
|
lastname: 'Agent3',
|
||||||
|
email: 'user-out_of_office-agent3@example.com',
|
||||||
|
password: 'agentpw',
|
||||||
|
active: true,
|
||||||
|
roles: roles,
|
||||||
|
groups: groups,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'check out_of_office?' do
|
||||||
|
|
||||||
|
# check
|
||||||
|
assert_not(@agent1.out_of_office?)
|
||||||
|
assert_not(@agent2.out_of_office?)
|
||||||
|
assert_not(@agent3.out_of_office?)
|
||||||
|
|
||||||
|
assert_raises(Exceptions::UnprocessableEntity) {
|
||||||
|
@agent1.out_of_office = true
|
||||||
|
@agent1.out_of_office_start_at = Time.zone.now + 2.days
|
||||||
|
@agent1.out_of_office_end_at = Time.zone.now
|
||||||
|
@agent1.save!
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_raises(Exceptions::UnprocessableEntity) {
|
||||||
|
@agent1.out_of_office = true
|
||||||
|
@agent1.out_of_office_start_at = Time.zone.now
|
||||||
|
@agent1.out_of_office_end_at = Time.zone.now - 2.days
|
||||||
|
@agent1.save!
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_raises(Exceptions::UnprocessableEntity) {
|
||||||
|
@agent1.out_of_office = true
|
||||||
|
@agent1.out_of_office_start_at = nil
|
||||||
|
@agent1.out_of_office_end_at = Time.zone.now
|
||||||
|
@agent1.save!
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_raises(Exceptions::UnprocessableEntity) {
|
||||||
|
@agent1.out_of_office = true
|
||||||
|
@agent1.out_of_office_start_at = Time.zone.now
|
||||||
|
@agent1.out_of_office_end_at = nil
|
||||||
|
@agent1.save!
|
||||||
|
}
|
||||||
|
|
||||||
|
@agent1.out_of_office = false
|
||||||
|
@agent1.out_of_office_start_at = Time.zone.now + 2.days
|
||||||
|
@agent1.out_of_office_end_at = Time.zone.now
|
||||||
|
@agent1.save!
|
||||||
|
|
||||||
|
assert_not(@agent1.out_of_office?)
|
||||||
|
|
||||||
|
assert_raises(Exceptions::UnprocessableEntity) {
|
||||||
|
@agent1.out_of_office = true
|
||||||
|
@agent1.out_of_office_start_at = Time.zone.now + 2.days
|
||||||
|
@agent1.out_of_office_end_at = Time.zone.now + 4.days
|
||||||
|
@agent1.save!
|
||||||
|
}
|
||||||
|
assert_raises(Exceptions::UnprocessableEntity) {
|
||||||
|
@agent1.out_of_office_replacement_id = 999_999_999_999 # not existing
|
||||||
|
@agent1.save!
|
||||||
|
}
|
||||||
|
@agent1.out_of_office_replacement_id = @agent2.id
|
||||||
|
@agent1.save!
|
||||||
|
|
||||||
|
assert_not(@agent1.out_of_office?)
|
||||||
|
|
||||||
|
travel 2.days
|
||||||
|
|
||||||
|
assert(@agent1.out_of_office?)
|
||||||
|
|
||||||
|
travel 1.day
|
||||||
|
|
||||||
|
assert(@agent1.out_of_office?)
|
||||||
|
|
||||||
|
travel 1.day
|
||||||
|
|
||||||
|
assert(@agent1.out_of_office?)
|
||||||
|
|
||||||
|
travel 1.day
|
||||||
|
|
||||||
|
assert_not(@agent1.out_of_office?)
|
||||||
|
|
||||||
|
assert_not(@agent1.out_of_office_agent)
|
||||||
|
|
||||||
|
assert_not(@agent2.out_of_office_agent)
|
||||||
|
|
||||||
|
@agent2.out_of_office = true
|
||||||
|
@agent2.out_of_office_start_at = Time.zone.now
|
||||||
|
@agent2.out_of_office_end_at = Time.zone.now + 4.days
|
||||||
|
@agent2.out_of_office_replacement_id = @agent3.id
|
||||||
|
@agent2.save!
|
||||||
|
|
||||||
|
assert(@agent2.out_of_office?)
|
||||||
|
|
||||||
|
assert_equal(@agent2.out_of_office_agent.id, @agent3.id)
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
Loading…
Reference in a new issue