Merge branch 'develop' of git.znuny.com:zammad/zammad into develop

This commit is contained in:
André Bauer 2017-07-08 15:37:00 +02:00
commit 8cb2eb406f
62 changed files with 2117 additions and 537 deletions

View file

@ -3,7 +3,7 @@ before_script:
- which ruby
- env
- test -n "$RNAME" && script/build/test_db_config.sh
- test -n "$RNAME" && bundle install
- test -n "$RNAME" && bundle install --jobs 8
stages:
- pre
@ -229,7 +229,7 @@ test:integration:slack:
- rake db:create
- rake db:migrate
- echo "gem 'slack-api'" >> Gemfile.local
- bundle install
- bundle install --jobs 8
- ruby -I test test/integration/slack_test.rb
- rake db:drop
@ -281,6 +281,7 @@ test:integration:es_mysql:
- ruby -I test/ test/integration/elasticsearch_test.rb
- ruby -I test/ test/controllers/search_controller_test.rb
- ruby -I test/ test/integration/report_test.rb
- ruby -I test/ test/controllers/form_controller_test.rb
- rake db:drop
test:integration:es_postgresql:
@ -297,6 +298,7 @@ test:integration:es_postgresql:
- ruby -I test/ test/integration/elasticsearch_test.rb
- ruby -I test/ test/controllers/search_controller_test.rb
- ruby -I test/ test/integration/report_test.rb
- ruby -I test/ test/controllers/form_controller_test.rb
- rake db:drop
test:integration:zendesk_mysql:
@ -427,7 +429,7 @@ test:browser:integration:api_client_ruby:
- script/build/test_startup.sh $RAILS_ENV $BROWSER_PORT $WS_PORT 1
- git clone git@github.com:zammad/zammad-api-client-ruby.git || script/build/test_shutdown.sh $RAILS_ENV $BROWSER_PORT $WS_PORT 1 1
- cd zammad-api-client-ruby
- bundle install
- bundle install --jobs 8
- export TEST_URL=http://$IP:$BROWSER_PORT
- rspec || (cd .. && script/build/test_shutdown.sh $RAILS_ENV $BROWSER_PORT $WS_PORT 1 1)
- cd .. && script/build/test_shutdown.sh $RAILS_ENV $BROWSER_PORT $WS_PORT 0 1

View file

@ -7,6 +7,7 @@ notifications:
env:
- DB=mysql
- DB=postgresql
- BUNDLE_JOBS=8
addons:
postgresql: "9.4"
apt:

View file

@ -45,7 +45,7 @@ gem 'twitter'
gem 'telegramAPI'
gem 'koala'
gem 'mail'
gem 'email_verifier'
gem 'valid_email2'
gem 'htmlentities'
gem 'mime-types'

View file

@ -93,7 +93,6 @@ GEM
delayed_job (>= 3.0, < 5)
diff-lcs (1.2.5)
diffy (3.1.0)
dnsruby (1.59.3)
docile (1.1.5)
domain_name (0.5.20170404)
unf (>= 0.0.5, < 1.0.0)
@ -107,8 +106,6 @@ GEM
em-websocket (0.5.1)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0)
email_verifier (0.1.0)
dnsruby (>= 1.5)
equalizer (0.0.10)
erubis (2.7.0)
eventmachine (1.2.3)
@ -403,6 +400,9 @@ GEM
unicorn (5.2.0)
kgio (~> 2.6)
raindrops (~> 0.7)
valid_email2 (1.2.17)
activemodel (>= 3.2)
mail (~> 2.5)
webmock (2.3.2)
addressable (>= 2.3.6)
crack (>= 0.3.2)
@ -439,7 +439,6 @@ DEPENDENCIES
doorkeeper
eco
em-websocket
email_verifier
eventmachine
execjs
factory_girl_rails
@ -491,6 +490,7 @@ DEPENDENCIES
twitter
uglifier
unicorn
valid_email2
webmock
writeexcel
zendesk_api

View file

@ -531,8 +531,8 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
# base
configureAttributesBase = [
{ name: 'realname', display: 'Organization & Department Name', tag: 'input', type: 'text', limit: 160, null: false, placeholder: 'Organization Support', autocomplete: 'new-password' },
{ name: 'email', display: 'Email', tag: 'input', type: 'email', limit: 120, null: false, placeholder: 'support@example.com', autocapitalize: false, autocomplete: 'new-password' },
{ name: 'realname', display: 'Organization & Department Name', tag: 'input', type: 'text', limit: 160, null: false, placeholder: 'Organization Support', autocomplete: 'off' },
{ name: 'email', display: 'Email', tag: 'input', type: 'email', limit: 120, null: false, placeholder: 'support@example.com', autocapitalize: false, autocomplete: 'off' },
{ name: 'password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', single: true },
{ name: 'group_id', display: 'Destination Group', tag: 'select', null: false, relation: 'Group', nulloption: true },
]
@ -562,7 +562,7 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
configureAttributesInbound = [
{ name: 'adapter', display: 'Type', tag: 'select', multiple: false, null: false, options: @channelDriver.email.inbound },
{ name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false },
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', },
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autocomplete: 'off', },
{ name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', single: true },
{ name: 'options::ssl', display: 'SSL', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, default: true, translate: true, item_class: 'formGroup--halfSize' },
{ name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false, default: '993', item_class: 'formGroup--halfSize' },
@ -616,7 +616,7 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
if adapter is 'smtp'
configureAttributesOutbound = [
{ name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autofocus: true },
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password', },
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, autocomplete: 'off', },
{ name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password', single: true },
{ name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false },
]
@ -930,7 +930,7 @@ class App.ChannelEmailNotificationWizard extends App.WizardModal
if adapter is 'smtp'
configureAttributesOutbound = [
{ name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autofocus: true },
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password' },
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, autocomplete: 'off' },
{ name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password', single: true },
{ name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false },
]

View file

@ -3,12 +3,14 @@ class App.ChannelForm extends App.ControllerSubContent
requiredPermission: 'admin.channel_formular'
header: 'Form'
events:
'change form.js-params': 'updateParams'
'keyup form.js-params': 'updateParams'
'change form.js-paramsDesigner': 'updateParamsDesigner'
'keyup form.js-paramsDesigner': 'updateParamsDesigner'
'change .js-formSetting input': 'toggleFormSetting'
'change .js-paramsSetting select': 'updateGroup'
elements:
'.js-paramsBlock': 'paramsBlock'
'.js-paramsSetting': 'paramsSetting'
'.js-formSetting input': 'formSetting'
constructor: ->
@ -20,22 +22,38 @@ class App.ChannelForm extends App.ControllerSubContent
render: =>
setting = App.Setting.get('form_ticket_create')
@html App.view('channel/form')(
element = $(App.view('channel/form')(
baseurl: window.location.origin
formSetting: setting
))
group_id = App.Setting.get('form_ticket_create_group_id')
selection = App.UiElement.select.render(
name: 'group_id'
multiple: false
null: false
relation: 'Group'
nulloption: false
value: group_id
#class: 'form-control--small'
)
console.log('s', element.find('.js-groupSelector'), selection)
element.find('.js-groupSelector').html(selection)
@html element
@paramsBlock.each (i, block) ->
hljs.highlightBlock block
@updateParams()
@updateParamsDesigner()
updateParams: ->
updateParamsDesigner: ->
quote = (string) ->
string = string.replace('\'', '\\\'')
.replace(/\</g, '&lt;')
.replace(/\>/g, '&gt;')
params = @formParam(@$('.js-params'))
params = @formParam(@$('.js-paramsDesigner'))
paramString = ''
for key, value of params
if value != ''
@ -63,4 +81,8 @@ class App.ChannelForm extends App.ControllerSubContent
value = @formSetting.prop('checked')
App.Setting.set('form_ticket_create', value)
updateGroup: =>
value = @paramsSetting.find('[name=group_id]').val()
App.Setting.set('form_ticket_create_group_id', value)
App.Config.set('Form', { prio: 2000, name: 'Form', parent: '#channels', target: '#channels/form', controller: App.ChannelForm, permission: ['admin.formular'] }, 'NavBarAdmin')

View file

@ -404,7 +404,7 @@ class App.TicketZoom extends App.Controller
)
new App.TicketZoomOverviewNavigator(
el: elLocal.find('.overview-navigator')
el: elLocal.find('.js-overviewNavigatorContainer')
ticket_id: @ticket_id
overview_id: @overview_id
)
@ -412,13 +412,13 @@ class App.TicketZoom extends App.Controller
new App.TicketZoomTitle(
object_id: @ticket_id
overview_id: @overview_id
el: elLocal.find('.ticket-title')
el: elLocal.find('.js-ticketTitleContainer')
task_key: @task_key
)
new App.TicketZoomMeta(
object_id: @ticket_id
el: elLocal.find('.ticket-meta')
el: elLocal.find('.js-ticketMetaContainer')
)
@attributeBar = new App.TicketZoomAttributeBar(
@ -445,7 +445,12 @@ class App.TicketZoom extends App.Controller
)
@highligher = new App.TicketZoomHighlighter(
el: elLocal.find('.highlighter')
el: elLocal.find('.js-highlighterContainer')
ticket_id: @ticket_id
)
new App.TicketZoomSetting(
el: elLocal.find('.js-settingContainer')
ticket_id: @ticket_id
)
@ -557,14 +562,16 @@ class App.TicketZoom extends App.Controller
return if !@ticket
currentStoreTicket = @ticket.attributes()
delete currentStoreTicket.article
internal = @Config.get('ui_ticket_zoom_article_note_new_internal')
currentStore =
ticket: currentStoreTicket
article:
to: ''
cc: ''
subject: ''
type: 'note'
body: ''
internal: 'true'
internal: internal
in_reply_to: ''
if @permissionCheck('ticket.customer')
@ -575,7 +582,7 @@ class App.TicketZoom extends App.Controller
formCurrent: =>
currentParams =
ticket: @formParam(@el.find('.edit'))
article: @formParam(@el.find('.article-add'))
article: @articleNew.params()
# add attachments if exist
attachmentCount = @$('.article-add .textBubble .attachments .attachment').length

View file

@ -28,107 +28,9 @@ class App.TicketZoomArticleNew extends App.Controller
constructor: ->
super
# set possble article types
possibleArticleType =
note: true
phone: true
if @ticket && @ticket.create_article_type_id
articleTypeCreate = App.TicketArticleType.find(@ticket.create_article_type_id).name
if articleTypeCreate is 'twitter status'
possibleArticleType['twitter status'] = true
else if articleTypeCreate is 'twitter direct-message'
possibleArticleType['twitter direct-message'] = true
else if articleTypeCreate is 'email'
possibleArticleType['email'] = true
else if articleTypeCreate is 'facebook feed post'
possibleArticleType['facebook feed comment'] = true
else if articleTypeCreate is 'telegram personal-message'
possibleArticleType['telegram personal-message'] = true
if @ticket && @ticket.customer_id
customer = App.User.find(@ticket.customer_id)
if customer.email
possibleArticleType['email'] = true
# gets referenced in @setArticleType
@internalSelector = true
@type = @defaults['type'] || 'note'
@articleTypes = []
if possibleArticleType.note
internal = @Config.get('ui_ticket_zoom_article_new_internal')
@articleTypes.push {
name: 'note'
icon: 'note'
attributes: []
internal: internal,
features: ['attachment']
}
if possibleArticleType.email
@articleTypes.push {
name: 'email'
icon: 'email'
attributes: ['to', 'cc']
internal: false,
features: ['attachment']
}
if possibleArticleType['facebook feed comment']
@articleTypes.push {
name: 'facebook feed comment'
icon: 'facebook'
attributes: []
internal: false,
features: []
}
if possibleArticleType['twitter status']
@articleTypes.push {
name: 'twitter status'
icon: 'twitter'
attributes: []
internal: false,
features: ['body:limit', 'body:initials']
maxTextLength: 140
warningTextLength: 30
}
if possibleArticleType['twitter direct-message']
@articleTypes.push {
name: 'twitter direct-message'
icon: 'twitter'
attributes: ['to']
internal: false,
features: ['body:limit', 'body:initials']
maxTextLength: 10000
warningTextLength: 500
}
if possibleArticleType.phone
@articleTypes.push {
name: 'phone'
icon: 'phone'
attributes: []
internal: false,
features: ['attachment']
}
if possibleArticleType['telegram personal-message']
@articleTypes.push {
name: 'telegram personal-message'
icon: 'telegram'
attributes: []
internal: false,
features: ['attachment']
maxTextLength: 10000
warningTextLength: 5000
}
if @permissionCheck('ticket.customer')
@type = 'note'
@articleTypes = [
{
name: 'note'
icon: 'note'
attributes: []
internal: false,
features: ['attachment']
},
]
@setPossibleArticleTypes()
if @permissionCheck('ticket.customer')
@internalSelector = false
@ -181,6 +83,114 @@ class App.TicketZoomArticleNew extends App.Controller
@render()
)
setPossibleArticleTypes: =>
possibleArticleType =
note: true
phone: true
if @ticket && @ticket.create_article_type_id
articleTypeCreate = App.TicketArticleType.find(@ticket.create_article_type_id).name
if articleTypeCreate is 'twitter status'
possibleArticleType['twitter status'] = true
else if articleTypeCreate is 'twitter direct-message'
possibleArticleType['twitter direct-message'] = true
else if articleTypeCreate is 'email'
possibleArticleType['email'] = true
else if articleTypeCreate is 'facebook feed post'
possibleArticleType['facebook feed comment'] = true
else if articleTypeCreate is 'telegram personal-message'
possibleArticleType['telegram personal-message'] = true
if @ticket && @ticket.customer_id
customer = App.User.find(@ticket.customer_id)
if customer.email
possibleArticleType['email'] = true
# gets referenced in @setArticleType
@articleTypes = []
if possibleArticleType.note
internal = @Config.get('ui_ticket_zoom_article_note_new_internal')
@articleTypes.push {
name: 'note'
icon: 'note'
attributes: []
internal: internal,
features: ['attachment']
}
if possibleArticleType.email
attributes = ['to', 'cc', 'subject']
if !@Config.get('ui_ticket_zoom_article_email_subject')
attributes = ['to', 'cc']
@articleTypes.push {
name: 'email'
icon: 'email'
attributes: attributes
internal: false,
features: ['attachment']
}
if possibleArticleType['facebook feed comment']
@articleTypes.push {
name: 'facebook feed comment'
icon: 'facebook'
attributes: []
internal: false,
features: []
}
if possibleArticleType['twitter status']
attributes = ['body:limit', 'body:initials']
if !@Config.get('ui_ticket_zoom_article_twitter_initials')
attributes = ['body:limit']
@articleTypes.push {
name: 'twitter status'
icon: 'twitter'
attributes: []
internal: false,
features: ['body:limit', 'body:initials']
maxTextLength: 140
warningTextLength: 30
}
if possibleArticleType['twitter direct-message']
attributes = ['body:limit', 'body:initials']
if !@Config.get('ui_ticket_zoom_article_twitter_initials')
attributes = ['body:limit']
@articleTypes.push {
name: 'twitter direct-message'
icon: 'twitter'
attributes: ['to']
internal: false,
features: ['body:limit', 'body:initials']
maxTextLength: 10000
warningTextLength: 500
}
if possibleArticleType.phone
@articleTypes.push {
name: 'phone'
icon: 'phone'
attributes: []
internal: false,
features: ['attachment']
}
if possibleArticleType['telegram personal-message']
@articleTypes.push {
name: 'telegram personal-message'
icon: 'telegram'
attributes: []
internal: false,
features: ['attachment']
maxTextLength: 10000
warningTextLength: 5000
}
if @permissionCheck('ticket.customer')
@type = 'note'
@articleTypes = [
{
name: 'note'
icon: 'note'
attributes: []
internal: false,
features: ['attachment']
},
]
placeCaretAtEnd: (el) ->
el.focus()
if typeof window.getSelection isnt 'undefined' && typeof document.createRange isnt 'undefined'
@ -318,9 +328,6 @@ class App.TicketZoomArticleNew extends App.Controller
params.form_id = @form_id
params.content_type = 'text/html'
if !params['internal']
params['internal'] = false
if @permissionCheck('ticket.customer')
sender = App.TicketArticleSender.findByAttribute('name', 'Customer')
type = App.TicketArticleType.findByAttribute('name', 'web')
@ -332,6 +339,11 @@ class App.TicketZoomArticleNew extends App.Controller
params.sender_id = sender.id
params.type_id = type.id
if params.internal
params.internal = true
else
params.internal = false
if params.type is 'twitter status'
App.Utils.htmlRemoveRichtext(@$('[data-name=body]'), false)
params.content_type = 'text/plain'
@ -478,6 +490,8 @@ class App.TicketZoomArticleNew extends App.Controller
@articleNewEdit.attr('data-type', type)
@$('.js-selectableTypes').addClass('hide').filter("[data-type='#{type}']").removeClass('hide')
@setPossibleArticleTypes()
# get config
config = {}
for articleTypeConfig in @articleTypes

View file

@ -0,0 +1,35 @@
class App.TicketZoomSetting extends App.Controller
events:
'click .js-setting': 'show'
constructor: ->
super
return if !@permissionCheck('admin')
@render()
render: ->
@html(App.view('ticket_zoom/setting')())
show: ->
new Modal()
class Modal extends App.ControllerModal
buttonClose: true
buttonCancel: true
buttonSubmit: false
head: 'Settings'
constructor: ->
super
render: =>
super
post: =>
new App.SettingsArea(
area: 'UI::TicketZoom'
el: @el.find('.modal-body')
)
content: ->
App.view('generic/page_loading')()

View file

@ -96,12 +96,15 @@ class _i18nSingleton extends Spine.Module
# prepare locale
localeToSet = localeToSet.toLowerCase()
dirToSet = 'ltr'
# check if locale exists
localeFound = false
locales = App.Locale.all()
for locale in locales
if locale.locale is localeToSet
localeToSet = locale.locale
dirToSet = locale.dir
localeFound = true
# try aliases
@ -109,6 +112,8 @@ class _i18nSingleton extends Spine.Module
for locale in locales
if locale.alias is localeToSet
localeToSet = locale.locale
dirToSet = locale.dir
localeFound = true
# if no locale and no alias was found, try to find correct one
if !localeFound
@ -118,13 +123,7 @@ class _i18nSingleton extends Spine.Module
for locale in locales
if locale.alias is localeToSet
localeToSet = locale.locale
localeFound = true
# try to find by locale
if !localeFound
for locale in locales
if locale.locale is localeToSet
localeToSet = locale.locale
dirToSet = locale.dir
localeFound = true
# check if locale need to be changed
@ -136,8 +135,9 @@ class _i18nSingleton extends Spine.Module
# set if not translated should be logged
@_notTranslatedLog = @notTranslatedFeatureEnabled(@locale)
# set lang attribute of html tag
$('html').prop('lang', @locale.substr(0, 2) )
# set lang and dir attribute of html tag
$('html').prop('lang', localeToSet.substr(0, 2))
$('html').prop('dir', dirToSet)
@mapString = {}
App.Ajax.request(

View file

@ -12,6 +12,7 @@ class App.SearchableSelect extends Spine.Controller
'mouseenter .js-back': 'highlightItem'
'shown.bs.dropdown': 'onDropdownShown'
'hidden.bs.dropdown': 'onDropdownHidden'
'keyup .js-input': 'onKeyUp'
elements:
'.js-dropdown': 'dropdown'
@ -120,6 +121,10 @@ class App.SearchableSelect extends Spine.Controller
$(document).off 'keydown.searchable_select'
@isOpen = false
onKeyUp: =>
return if @input.val().trim() isnt ''
@shadowInput.val('')
toggle: =>
@currentItem = null
@$('[data-toggle="dropdown"]').dropdown('toggle')

View file

@ -180,7 +180,7 @@
Plugin.prototype.renderBase = function() {
this.$element.after('<div class="shortcut dropdown"><ul class="dropdown-menu" style="max-height: 200px;"></ul></div>')
this.$widget = this.$element.next()
this.$widget.on('click', 'li', $.proxy(this.onEntryClick, this))
this.$widget.on('mousedown', 'li', $.proxy(this.onEntryClick, this))
this.$widget.on('mouseenter', 'li', $.proxy(this.onMouseEnter, this))
}
@ -313,6 +313,7 @@
}
Plugin.prototype.onEntryClick = function(event) {
event.preventDefault()
var id = $(event.target).data('id')
this.take(id)
}

View file

@ -10,8 +10,20 @@
<div class="page-content">
<p><%- @T('With form you can add a form to your web page which directly generates a ticket for you.') %></p>
<h2><%- @T('Settings') %></h2>
<form class="js-paramsSetting">
<fieldset>
<div class="input form-group formGroup--halfSize">
<div class="formGroup-label">
<label for="form-group"><%- @T('Group selection for Ticket creation') %></label>
</div>
<div class="controls js-groupSelector" id="from-group"></div>
</div>
</fieldset>
</form>
<h2><%- @T('Designer') %></h2>
<form class="js-params">
<form class="js-paramsDesigner">
<fieldset>
<div class="input form-group formGroup--halfSize">

View file

@ -33,13 +33,13 @@
<ul>
<li><%- @T('%s user to %s user', 'LDAP', 'Zammad') %> (<%= @countDone %>/<%= @job.result.sum %>):
<ul>
<li><%- @T('Users') %>: <%= @job.result.created %> <%- @T('created') %>, <%= @job.result.updated %> <%- @T('updated') %>, <%= @job.result.unchanged %> <%- @T('untouched') %>, <%= @job.result.skipped %> <%- @T('skipped') %>, <%= @job.result.failed %> <%- @T('failed') %>
<li><%- @T('Users') %>: <%= @job.result.created %> <%- @T('created') %>, <%= @job.result.updated %> <%- @T('updated') %>, <%= @job.result.unchanged %> <%- @T('untouched') %>, <%= @job.result.skipped %> <%- @T('skipped') %>, <%= @job.result.failed %> <%- @T('failed') %>, <%= @job.result.deactivated %> <%- @T('deactivated') %>
</ul>
<% if !_.isEmpty(@job.result.roles): %>
<li><%- @T('%s groups to %s roles assignments', 'LDAP', 'Zammad') %>:
<ul>
<% for role, result of @job.result.roles: %>
<li> <%- @T(role) %>: <%= result.created %> <%- @T('created') %>, <%= result.updated %> <%- @T('updated') %>, <%= result.unchanged %> <%- @T('untouched') %>, <%= result.failed %> <%- @T('failed') %>
<li> <%- @T(role) %>: <%= result.created %> <%- @T('created') %>, <%= result.updated %> <%- @T('updated') %>, <%= result.unchanged %> <%- @T('untouched') %>, <%= result.failed %> <%- @T('failed') %>, <%= result.deactivated %> <%- @T('deactivated') %>
<% end %>
</ul>
<% end %>

View file

@ -1,14 +1,14 @@
<ul>
<li><%- @T('%s user to %s user', 'LDAP', 'Zammad') %> (<%= @countDone %>):
<ul>
<li><%- @T('Users') %>: <%= @job.result.created %> <%- @T('created') %>, <%= @job.result.updated %> <%- @T('updated') %>, <%= @job.result.unchanged %> <%- @T('untouched') %>, <%= @job.result.skipped %> <%- @T('skipped') %>, <%= @job.result.failed %> <%- @T('failed') %>
<li><%- @T('Users') %>: <%= @job.result.created %> <%- @T('created') %>, <%= @job.result.updated %> <%- @T('updated') %>, <%= @job.result.unchanged %> <%- @T('untouched') %>, <%= @job.result.skipped %> <%- @T('skipped') %>, <%= @job.result.failed %> <%- @T('failed') %>, <%= @job.result.deactivated %> <%- @T('deactivated') %>
</ul>
</li>
<% if !_.isEmpty(@job.result.roles): %>
<li><%- @T('%s groups to %s roles assignments', 'LDAP', 'Zammad') %>:
<ul>
<% for role, result of @job.result.roles: %>
<li><%- @T(role) %>: <%= result.created %> <%- @T('created') %>, <%= result.updated %> <%- @T('updated') %>, <%= result.unchanged %> <%- @T('untouched') %>, <%= result.failed %> <%- @T('failed') %>
<li><%- @T(role) %>: <%= result.created %> <%- @T('created') %>, <%= result.updated %> <%- @T('updated') %>, <%= result.unchanged %> <%- @T('untouched') %>, <%= result.failed %> <%- @T('failed') %>, <%= result.deactivated %> <%- @T('deactivated') %>
<% end %>
</ul>
</li>

View file

@ -19,7 +19,7 @@
<tbody>
<tr>
<td class="settings-list-row-control"><%- @T('Host') %>
<td class="settings-list-control-cell"><input type="text" name="host_url" class="form-control form-control--small js-hostUrl" value="" placeholder="ldaps://ldap.example.com" autocomplete="new-password">
<td class="settings-list-control-cell"><input type="text" name="host_url" class="form-control form-control--small js-hostUrl" value="" placeholder="ldaps://ldap.example.com" autocomplete="off">
</tbody>
</table>
</div>
@ -70,7 +70,7 @@
<td class="settings-list-control-cell js-baseDn">
<tr>
<td class="settings-list-row-control"><%- @T('Bind User') %>
<td class="settings-list-control-cell"><input type="text" name="bind_user" class="form-control form-control--small" value="" placeholder="" autocomplete="new-password">
<td class="settings-list-control-cell"><input type="text" name="bind_user" class="form-control form-control--small" value="" placeholder="" autocomplete="off">
<tr>
<td class="settings-list-row-control"><%- @T('Bind Password') %>
<td class="settings-list-control-cell"><input type="password" name="bind_pw" class="form-control form-control--small" value="" autocomplete="new-password">
@ -203,7 +203,7 @@
<tbody>
<tr>
<td class="settings-list-row-control"><%- @T('User filter') %>
<td class="settings-list-control-cell"><input type="text" name="user_filter" class="form-control form-control--small" value="" placeholder="" autocomplete="new-password">
<td class="settings-list-control-cell"><input type="text" name="user_filter" class="form-control form-control--small" value="" placeholder="" autocomplete="off">
<tr>
<td class="settings-list-row-control"><%- @T('Users without assigned LDAP groups') %>
<td class="settings-list-control-cell js-unassignedUsers">

View file

@ -1,21 +1,22 @@
<div class="tabsSidebar-holder">
<div class="scrollPageHeader tabsSidebar-sidebarSpacer" style="right: <%- @scrollbarWidth %>px">
<small><%- @C('ticket_hook') %> <span class="ticket-number"><%- @ticket.number %></span></small>
<div class="ticket-title"></div>
<div class="highlighter"></div>
<div class="overview-navigator"></div>
<div class="js-ticketTitleContainer ticket-title"></div>
<div class="js-highlighterContainer highlighter"></div>
<div class="js-overviewNavigatorContainer overview-navigator"></div>
</div>
<div class="main no-padding flex tabsSidebar-sidebarSpacer tabsSidebar-tabsSpacer">
<div class="ticketZoom">
<div class="ticketZoom-controls">
<div class="highlighter"></div>
<div class="overview-navigator"></div>
<div class="js-settingContainer"></div>
<div class="js-highlighterContainer highlighter"></div>
<div class="js-overviewNavigatorContainer overview-navigator"></div>
</div>
<div class="ticketZoom-header">
<div class="flex vertical center">
<div class="js-avatar"></div>
<div class="ticket-title"></div>
<div class="ticket-meta"></div>
<div class="js-ticketTitleContainer ticket-title"></div>
<div class="js-ticketMetaContainer"></div>
</div>
</div>
<div class="ticket-article"></div>

View file

@ -41,13 +41,19 @@
<div class="formGroup-label">
<label for=""><%- @T('To') %></label>
</div>
<div class="controls"><input id="" type="text" name="to" value="<%= @article.to %>" class="form-control js-mail-inputs" required="required"></div>
<div class="controls"><input type="text" name="to" value="<%= @article.to %>" class="form-control js-mail-inputs" required="required"></div>
</div>
<div class="input form-group">
<div class="formGroup-label">
<label for=""><%- @T('Cc') %></label>
</div>
<div class="controls"><input id="" type="text" name="cc" value="<%= @article.cc %>" class="form-control js-mail-inputs"></div>
<div class="controls"><input type="text" name="cc" value="<%= @article.cc %>" class="form-control js-mail-inputs"></div>
</div>
<div class="input form-group">
<div class="formGroup-label">
<label for=""><%- @T('Subject') %></label>
</div>
<div class="controls"><input type="text" name="subject" value="<%= @article.subject %>" class="form-control js-mail-inputs2"></div>
</div>
<div class="textBubble js-writeArea">

View file

@ -0,0 +1,3 @@
<div class="btn btn--action btn--split--first js-setting centered">
<%- @Icon('cog', 'dropdown-icon') %>
</div>

View file

@ -13,6 +13,8 @@
<th><%- @T('Agent') %>
<th><%- @T('Time Units') %>
<th><%- @T('Time Units Total') %>
<th><%- @T('Created at') %>
<th><%- @T('Closed at') %>
</thead>
<tbody>
<% for row in @rows: %>
@ -24,6 +26,8 @@
<td><%= row.agent %>
<td><%= row.time_unit %>
<td><%= row.ticket.time_unit %>
<td><%- @humanTime(row.ticket.created_at) %>
<td><%- @humanTime(row.ticket.close_at) %>
<% end %>
</tbody>
</table>

View file

@ -7,6 +7,8 @@ class FormController < ApplicationController
def config
return if !enabled?
return if !fingerprint_exists?
return if limit_reached?
api_path = Rails.configuration.api_path
http_type = Setting.get('http_type')
@ -17,6 +19,7 @@ class FormController < ApplicationController
config = {
enabled: Setting.get('form_ticket_create'),
endpoint: endpoint,
token: token_gen(params[:fingerprint])
}
if params[:test] && current_user && current_user.permissions?('admin.channel_formular')
@ -28,35 +31,35 @@ class FormController < ApplicationController
def submit
return if !enabled?
return if !fingerprint_exists?
return if !token_valid?(params[:token], params[:fingerprint])
return if limit_reached?
# validate input
errors = {}
if !params[:name] || params[:name].empty?
if params[:name].blank?
errors['name'] = 'required'
end
if !params[:email] || params[:email].empty?
if params[:email].blank?
errors['email'] = 'required'
end
if params[:email] !~ /@/
elsif params[:email] !~ /@/
errors['email'] = 'invalid'
elsif params[:email] =~ /(>|<|\||\!|"|§|'|\$|%|&|\(|\)|\?|\s|\.\.)/
errors['email'] = 'invalid'
end
if params[:email] =~ /(>|<|\||\!|"|§|'|\$|%|&|\(|\)|\?|\s)/
errors['email'] = 'invalid'
end
if !params[:title] || params[:title].empty?
if params[:title].blank?
errors['title'] = 'required'
end
if !params[:body] || params[:body].empty?
if params[:body].blank?
errors['body'] = 'required'
end
# realtime verify
if !errors['email']
if errors['email'].blank?
begin
checker = EmailVerifier::Checker.new(params[:email])
checker.connect
if !checker.verify
errors['email'] = "Unable to send to '#{params[:email]}'"
address = ValidEmail2::Address.new(params[:email])
if !address || !address.valid? || !address.valid_mx?
errors['email'] = 'invalid'
end
rescue => e
message = e.to_s
@ -69,7 +72,7 @@ class FormController < ApplicationController
end
end
if errors && !errors.empty?
if errors.present?
render json: {
errors: errors
}, status: :ok
@ -86,7 +89,6 @@ class FormController < ApplicationController
firstname: name,
lastname: '',
email: email,
password: '',
active: true,
role_ids: role_ids,
updated_by_id: 1,
@ -97,10 +99,23 @@ class FormController < ApplicationController
# set current user
UserInfo.current_user_id = customer.id
group = Group.find_by(id: Setting.get('form_ticket_create_group_id'))
if !group
group = Group.where(active: true).first
if !group
group = Group.first
end
end
ticket = Ticket.create!(
group_id: 1,
group_id: group.id,
customer_id: customer.id,
title: params[:title],
preferences: {
form: {
remote_ip: request.remote_ip,
fingerprint_md5: Digest::MD5.hexdigest(params[:fingerprint]),
}
}
)
article = Ticket::Article.create!(
ticket_id: ticket.id,
@ -138,6 +153,91 @@ class FormController < ApplicationController
private
def token_gen(fingerprint)
crypt = ActiveSupport::MessageEncryptor.new(Setting.get('application_secret'))
fingerprint = "#{Base64.strict_encode64(Setting.get('fqdn'))}:#{Time.zone.now.to_i}:#{Base64.strict_encode64(fingerprint)}"
Base64.strict_encode64(crypt.encrypt_and_sign(fingerprint))
end
def token_valid?(token, fingerprint)
if token.blank?
Rails.logger.info 'No token for form!'
response_access_deny
return false
end
begin
crypt = ActiveSupport::MessageEncryptor.new(Setting.get('application_secret'))
result = crypt.decrypt_and_verify(Base64.decode64(token))
rescue
Rails.logger.info 'Invalid token for form!'
response_access_deny
return false
end
if result.blank?
Rails.logger.info 'Invalid token for form!'
response_access_deny
return false
end
parts = result.split(/:/)
if parts.count != 3
Rails.logger.info "Invalid token for form (need to have 3 parts, only #{parts.count} found)!"
response_access_deny
return false
end
fqdn_local = Base64.decode64(parts[0])
if fqdn_local != Setting.get('fqdn')
Rails.logger.info "Invalid token for form (invalid fqdn found #{fqdn_local} != #{Setting.get('fqdn')})!"
response_access_deny
return false
end
fingerprint_local = Base64.decode64(parts[2])
if fingerprint_local != fingerprint
Rails.logger.info "Invalid token for form (invalid fingerprint found #{fingerprint_local} != #{fingerprint})!"
response_access_deny
return false
end
if parts[1].to_i < (Time.zone.now.to_i - 60 * 60 * 24)
Rails.logger.info 'Invalid token for form (token expired})!'
response_access_deny
return false
end
true
end
def limit_reached?
return false if !SearchIndexBackend.enabled?
form_limit_by_ip_per_hour = Setting.get('form_ticket_create_by_ip_per_hour') || 20
result = SearchIndexBackend.search("preferences.form.remote_ip:'#{request.remote_ip}' AND created_at:>now-1h", form_limit_by_ip_per_hour, 'Ticket')
if result.count >= form_limit_by_ip_per_hour.to_i
response_access_deny
return true
end
form_limit_by_ip_per_day = Setting.get('form_ticket_create_by_ip_per_day') || 240
result = SearchIndexBackend.search("preferences.form.remote_ip:'#{request.remote_ip}' AND created_at:>now-1d", form_limit_by_ip_per_day, 'Ticket')
if result.count >= form_limit_by_ip_per_day.to_i
response_access_deny
return true
end
form_limit_per_day = Setting.get('form_ticket_create_per_day') || 5000
result = SearchIndexBackend.search('preferences.form.remote_ip:* AND created_at:>now-1d', form_limit_per_day, 'Ticket')
if result.count >= form_limit_per_day.to_i
response_access_deny
return true
end
false
end
def fingerprint_exists?
return true if params[:fingerprint].present? && params[:fingerprint].length > 30
Rails.logger.info 'No fingerprint given!'
response_access_deny
false
end
def enabled?
return true if params[:test] && current_user && current_user.permissions?('admin.channel_formular')
return true if Setting.get('form_ticket_create')

View file

@ -93,9 +93,84 @@ class TimeAccountingsController < ApplicationController
name: 'Time Units Total',
width: 10,
},
{
name: 'Created at',
width: 10,
},
{
name: 'Closed at',
width: 10,
},
{
name: 'Close Escalation At',
width: 10,
},
{
name: 'Close In Min',
width: 10,
},
{
name: 'Close Diff In Min',
width: 10,
},
{
name: 'First Response At',
width: 10,
},
{
name: 'First Response Escalation At',
width: 10,
},
{
name: 'First Response In Min',
width: 10,
},
{
name: 'First Response Diff In Min',
width: 10,
},
{
name: 'Update Escalation At',
width: 10,
},
{
name: 'Update In Min',
width: 10,
},
{
name: 'Update Diff In Min',
width: 10,
},
{
name: 'Last Contact At',
width: 10,
},
{
name: 'Last Contact Agent At',
width: 10,
},
{
name: 'Last Contact Customer At',
width: 10,
},
{
name: 'Article Count',
width: 10,
},
{
name: 'Escalation At',
width: 10,
},
]
result = []
results.each { |row|
row[:ticket].keys.each { |field|
next if row[:ticket][field].blank?
next if !row[:ticket][field].is_a?(ActiveSupport::TimeWithZone)
row[:ticket][field] = row[:ticket][field].iso8601
}
result_row = [
row[:ticket]['number'],
row[:ticket]['title'],
@ -104,6 +179,23 @@ class TimeAccountingsController < ApplicationController
row[:agent],
row[:time_unit],
row[:ticket]['time_unit'],
row[:ticket]['created_at'],
row[:ticket]['close_at'],
row[:ticket]['close_escalation_at'],
row[:ticket]['close_in_min'],
row[:ticket]['close_diff_in_min'],
row[:ticket]['first_response_at'],
row[:ticket]['first_response_escalation_at'],
row[:ticket]['first_response_in_min'],
row[:ticket]['first_response_diff_in_min'],
row[:ticket]['update_escalation_at'],
row[:ticket]['update_in_min'],
row[:ticket]['update_diff_in_min'],
row[:ticket]['last_contact_at'],
row[:ticket]['last_contact_agent_at'],
row[:ticket]['last_contact_customer_at'],
row[:ticket]['article_count'],
row[:ticket]['escalation_at'],
]
result.push result_row
}

View file

@ -504,7 +504,7 @@ condition example
query += "#{attribute} IN (?)"
bind_params.push 1
else
query += "#{attribute} IS NOT NULL"
query += "#{attribute} IS NULL"
end
elsif selector['pre_condition'] == 'current_user.id'
raise "Use current_user.id in selector, but no current_user is set #{selector.inspect}" if !current_user_id
@ -518,7 +518,7 @@ condition example
else
# rubocop:disable Style/IfInsideElse
if selector['value'].nil?
query += "#{attribute} IS NOT NULL"
query += "#{attribute} IS NULL"
else
query += "#{attribute} IN (?)"
bind_params.push selector['value']
@ -531,7 +531,7 @@ condition example
query += "#{attribute} NOT IN (?)"
bind_params.push 1
else
query += "#{attribute} IS NULL"
query += "#{attribute} IS NOT NULL"
end
elsif selector['pre_condition'] == 'current_user.id'
query += "#{attribute} NOT IN (?)"
@ -825,9 +825,11 @@ perform changes on ticket
# loop protection / check if maximal count of trigger mail has reached
map = {
10 => 10,
30 => 15,
60 => 25,
180 => 50,
600 => 100,
}
skip = false
map.each { |minutes, count|
@ -843,18 +845,20 @@ perform changes on ticket
}
next if skip
map = {
1 => 150,
3 => 250,
6 => 450,
10 => 30,
30 => 60,
60 => 120,
180 => 240,
600 => 360,
}
skip = false
map.each { |hours, count|
map.each { |minutes, count|
already_sent = Ticket::Article.where(
sender: Ticket::Article::Sender.find_by(name: 'System'),
type: Ticket::Article::Type.find_by(name: 'email'),
).where("ticket_articles.created_at > ? AND ticket_articles.to LIKE '%#{recipient_email.strip}%'", Time.zone.now - hours.hours).count
).where("ticket_articles.created_at > ? AND ticket_articles.to LIKE '%#{recipient_email.strip}%'", Time.zone.now - minutes.minutes).count
next if already_sent < count
logger.info "Send no trigger based notification to #{recipient_email} because already sent #{count} in total within last #{hours} hour(s) (loop protection)"
logger.info "Send no trigger based notification to #{recipient_email} because already sent #{count} in total within last #{minutes} minutes (loop protection)"
skip = true
break
}

View file

@ -1,4 +1,5 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
require 'csv'
class Translation < ApplicationModel
before_create :set_initial
@ -212,7 +213,7 @@ translate strings in ruby context, e. g. for notifications
=begin
load locales from local
load translations from local
all:
@ -282,6 +283,65 @@ all:
true
end
=begin
load translations from csv file
all:
Translation.load_from_csv
or
Translation.load_from_csv(locale, file_location, file_charset) # e. g. 'en-us' or 'de-de' and /path/to/translation_list.csv
e. g.
Translation.load_from_csv('he-il', '/Users/me/Downloads/Hebrew_translation_list-1.csv', 'Windows-1255')
Get source file at https://i18n.zammad.com/api/v1/translations_empty_translation_list
=end
def self.load_from_csv(locale_name, location, charset = 'UTF8')
locale = Locale.find_by(locale: locale_name)
if !locale
raise "No such locale: #{locale_name}"
end
if !::File.exist?(location)
raise "No such file: #{location}"
end
content = ::File.open(location, "r:#{charset}").read
params = {
col_sep: ',',
}
rows = ::CSV.parse(content, params)
header = rows.shift
translation_raw = []
rows.each { |row|
raise "Can't import translation, source is missing" if row[0].blank?
if row[1].blank?
warn "Skipped #{row[0]}, because translation is blank"
next
end
raise "Can't import translation, format is missing" if row[2].blank?
raise "Can't import translation, format is invalid (#{row[2]})" if row[2] !~ /^(time|string)$/
item = {
'locale' => locale.locale,
'source' => row[0],
'target' => row[1],
'target_initial' => '',
'format' => row[2],
}
translation_raw.push item
}
to_database(locale.name, translation_raw)
true
end
private_class_method def self.to_database(locale, data)
translations = Translation.where(locale: locale).all
ActiveRecord::Base.transaction do

View file

@ -21,8 +21,9 @@ store new device for user if device not already known
def self.add(user_agent, ip, user_id, fingerprint, type)
# since gem browser 2 is not handling nil for user_agent, set it to ''
user_agent ||= ''
if user_agent.blank?
user_agent = 'unknown'
end
# get location info
location_details = Service::GeoIp.location(ip)
@ -60,6 +61,8 @@ store new device for user if device not already known
end
# get browser details
browser = {}
if user_agent != 'unknown'
browser = Browser.new(user_agent, accept_language: 'en-us')
browser = {
plattform: browser.platform.to_s.camelize,
@ -67,16 +70,17 @@ store new device for user if device not already known
version: browser.version,
full_version: browser.full_version,
}
end
# generate device name
if browser[:name] == 'Generic Browser'
browser[:name] = user_agent
end
name = ''
if browser[:plattform] && browser[:plattform] != 'Other'
if browser[:plattform].present? && browser[:plattform] != 'Other'
name = browser[:plattform]
end
if browser[:name] && browser[:name] != 'Other'
if browser[:name].present? && browser[:name] != 'Other'
if name.present?
name += ', '
end
@ -84,7 +88,7 @@ store new device for user if device not already known
end
# if not identified, use user agent
if !name || name == '' || name == 'Other, Other' || name == 'Other'
if name.blank? || name == 'Other, Other' || name == 'Other'
name = user_agent
browser[:name] = user_agent
end
@ -103,7 +107,7 @@ store new device for user if device not already known
end
# create new device
user_device = create(
user_device = create!(
user_id: user_id,
name: name,
os: browser[:plattform],

View file

@ -10,7 +10,7 @@ Dir.chdir APP_ROOT do
puts '== Installing dependencies =='
system 'gem install bundler --conservative'
system 'bundle check || bundle install'
system 'bundle check || bundle install --jobs 8'
# puts "\n== Copying sample files =="
# unless File.exist?("config/database.yml")

View file

@ -40,6 +40,7 @@ Rails.application.config.html_sanitizer_attributes_whitelist = {
'table' => %w(align bgcolor border cellpadding cellspacing frame rules sortable summary width style),
'td' => %w(abbr align axis colspan headers rowspan valign width style),
'th' => %w(abbr align axis colspan headers rowspan scope sorted valign width style),
'tr' => %w(width style),
'ul' => %w(type),
'q' => %w(cite),
'span' => %w(style),

View file

@ -3,6 +3,6 @@ Zammad::Application.routes.draw do
# forms
match api_path + '/form_submit', to: 'form#submit', via: :post
match api_path + '/form_config', to: 'form#config', via: :get
match api_path + '/form_config', to: 'form#config', via: :post
end

View file

@ -209,6 +209,7 @@ class CreateBase < ActiveRecord::Migration
t.string :locale, limit: 20, null: false
t.string :alias, limit: 20, null: true
t.string :name, limit: 255, null: false
t.string :dir, limit: 9, null: false, default: 'ltr'
t.boolean :active, null: false, default: true
t.timestamps limit: 3, null: false
end

View file

@ -1,5 +1,9 @@
class TreeSelect < ActiveRecord::Migration
def up
# return if it's a new setup
return if !Setting.find_by(name: 'system_init_done')
change_column :object_manager_attributes, :data_option, :text, limit: 800.kilobytes + 1, null: true
change_column :object_manager_attributes, :data_option_new, :text, limit: 800.kilobytes + 1, null: true
end

View file

@ -0,0 +1,9 @@
class LocaleAddDirection < ActiveRecord::Migration
def up
# return if it's a new setup
return if !Setting.find_by(name: 'system_init_done')
add_column :locales, :dir, :string, limit: 9, null: false, default: 'ltr'
end
end

View file

@ -0,0 +1,103 @@
class FormGroupSelection < ActiveRecord::Migration
def up
# return if it's a new setup
return if !Setting.find_by(name: 'system_init_done')
group = Group.where(active: true).first
if !group
group = Group.first
end
group_id = 1
if group
group_id = group.id
end
Setting.create_if_not_exists(
title: 'Group selection for Ticket creation',
name: 'form_ticket_create_group_id',
area: 'Form::Base',
description: 'Defines if group of created tickets via web form.',
options: {
form: [
{
display: '',
null: true,
name: 'form_ticket_create_group_id',
tag: 'select',
relation: 'Group',
},
],
},
state: group_id,
preferences: {
permission: ['admin.channel_formular'],
},
frontend: false,
)
Setting.create_if_not_exists(
title: 'Limit tickets by ip per hour',
name: 'form_ticket_create_by_ip_per_hour',
area: 'Form::Base',
description: 'Defines limit of tickets by ip per hour via web form.',
options: {
form: [
{
display: '',
null: true,
name: 'form_ticket_create_by_ip_per_hour',
tag: 'input',
},
],
},
state: 20,
preferences: {
permission: ['admin.channel_formular'],
},
frontend: false,
)
Setting.create_if_not_exists(
title: 'Limit tickets by ip per day',
name: 'form_ticket_create_by_ip_per_day',
area: 'Form::Base',
description: 'Defines limit of tickets by ip per day via web form.',
options: {
form: [
{
display: '',
null: true,
name: 'form_ticket_create_by_ip_per_day',
tag: 'input',
},
],
},
state: 240,
preferences: {
permission: ['admin.channel_formular'],
},
frontend: false,
)
Setting.create_if_not_exists(
title: 'Limit tickets per day',
name: 'form_ticket_create_per_day',
area: 'Form::Base',
description: 'Defines limit of tickets per day via web form.',
options: {
form: [
{
display: '',
null: true,
name: 'form_ticket_create_per_day',
tag: 'input',
},
],
},
state: 5000,
preferences: {
permission: ['admin.channel_formular'],
},
frontend: false,
)
end
end

View file

@ -0,0 +1,99 @@
class TicketZoomSetting < ActiveRecord::Migration
def up
# return if it's a new setup
return if !Setting.find_by(name: 'system_init_done')
setting = Setting.find_by(name: 'ui_ticket_zoom_article_new_internal')
if setting
setting.title = 'Note - default visibility'
setting.name = 'ui_ticket_zoom_article_note_new_internal'
setting.description = 'Default visibility for new articles.'
setting.preferences[:prio] = 100
setting.options[:form][0][:name] = 'ui_ticket_zoom_article_note_new_internal'
setting.save!
end
Setting.create_if_not_exists(
title: 'Note - default visibility',
name: 'ui_ticket_zoom_article_note_new_internal',
area: 'UI::TicketZoom',
description: 'Default visibility for new articles.',
options: {
form: [
{
display: '',
null: true,
name: 'ui_ticket_zoom_article_note_new_internal',
tag: 'boolean',
translate: true,
options: {
true => 'internal',
false => 'public',
},
},
],
},
state: true,
preferences: {
prio: 100,
permission: ['admin.ui'],
},
frontend: true
)
Setting.create_if_not_exists(
title: 'Email - subject field',
name: 'ui_ticket_zoom_article_email_subject',
area: 'UI::TicketZoom',
description: 'Use subject field for emails. If disabled, the ticket title will be used as subject.',
options: {
form: [
{
display: '',
null: true,
name: 'ui_ticket_zoom_article_email_subject',
tag: 'boolean',
translate: true,
options: {
true => 'yes',
false => 'no',
},
},
],
},
state: false,
preferences: {
prio: 200,
permission: ['admin.ui'],
},
frontend: true
)
Setting.create_if_not_exists(
title: 'Twitter - tweet initials',
name: 'ui_ticket_zoom_article_twitter_initials',
area: 'UI::TicketZoom',
description: 'Add sender initials to end of a tweet.',
options: {
form: [
{
display: '',
null: true,
name: 'ui_ticket_zoom_article_twitter_initials',
tag: 'boolean',
translate: true,
options: {
true => 'yes',
false => 'no',
},
},
],
},
state: true,
preferences: {
prio: 300,
permission: ['admin.ui'],
},
frontend: true
)
end
end

View file

@ -546,18 +546,17 @@ Setting.create_if_not_exists(
},
frontend: true
)
Setting.create_if_not_exists(
title: 'Define default visibility of new a new article',
name: 'ui_ticket_zoom_article_new_internal',
title: 'Note - default visibility',
name: 'ui_ticket_zoom_article_note_new_internal',
area: 'UI::TicketZoom',
description: 'Set default visibility of new a new article.',
description: 'Default visibility for new note.',
options: {
form: [
{
display: '',
null: true,
name: 'ui_ticket_zoom_article_new_internal',
name: 'ui_ticket_zoom_article_note_new_internal',
tag: 'boolean',
translate: true,
options: {
@ -569,7 +568,61 @@ Setting.create_if_not_exists(
},
state: true,
preferences: {
prio: 1,
prio: 100,
permission: ['admin.ui'],
},
frontend: true
)
Setting.create_if_not_exists(
title: 'Email - subject field',
name: 'ui_ticket_zoom_article_email_subject',
area: 'UI::TicketZoom',
description: 'Use subject field for emails. If disabled, the ticket title will be used as subject.',
options: {
form: [
{
display: '',
null: true,
name: 'ui_ticket_zoom_article_email_subject',
tag: 'boolean',
translate: true,
options: {
true => 'yes',
false => 'no',
},
},
],
},
state: false,
preferences: {
prio: 200,
permission: ['admin.ui'],
},
frontend: true
)
Setting.create_if_not_exists(
title: 'Twitter - tweet initials',
name: 'ui_ticket_zoom_article_twitter_initials',
area: 'UI::TicketZoom',
description: 'Add sender initials to end of a tweet.',
options: {
form: [
{
display: '',
null: true,
name: 'ui_ticket_zoom_article_twitter_initials',
tag: 'boolean',
translate: true,
options: {
true => 'yes',
false => 'no',
},
},
],
},
state: true,
preferences: {
prio: 300,
permission: ['admin.ui'],
},
frontend: true
@ -1489,6 +1542,101 @@ Setting.create_if_not_exists(
frontend: false,
)
group = Group.where(active: true).first
if !group
group = Group.first
end
group_id = 1
if group
group_id = group.id
end
Setting.create_if_not_exists(
title: 'Group selection for Ticket creation',
name: 'form_ticket_create_group_id',
area: 'Form::Base',
description: 'Defines if group of created tickets via web form.',
options: {
form: [
{
display: '',
null: true,
name: 'form_ticket_create_group_id',
tag: 'select',
relation: 'Group',
},
],
},
state: group_id,
preferences: {
permission: ['admin.channel_formular'],
},
frontend: false,
)
Setting.create_if_not_exists(
title: 'Limit tickets by ip per hour',
name: 'form_ticket_create_by_ip_per_hour',
area: 'Form::Base',
description: 'Defines limit of tickets by ip per hour via web form.',
options: {
form: [
{
display: '',
null: true,
name: 'form_ticket_create_by_ip_per_hour',
tag: 'input',
},
],
},
state: 20,
preferences: {
permission: ['admin.channel_formular'],
},
frontend: false,
)
Setting.create_if_not_exists(
title: 'Limit tickets by ip per day',
name: 'form_ticket_create_by_ip_per_day',
area: 'Form::Base',
description: 'Defines limit of tickets by ip per day via web form.',
options: {
form: [
{
display: '',
null: true,
name: 'form_ticket_create_by_ip_per_day',
tag: 'input',
},
],
},
state: 240,
preferences: {
permission: ['admin.channel_formular'],
},
frontend: false,
)
Setting.create_if_not_exists(
title: 'Limit tickets per day',
name: 'form_ticket_create_per_day',
area: 'Form::Base',
description: 'Defines limit of tickets per day via web form.',
options: {
form: [
{
display: '',
null: true,
name: 'form_ticket_create_per_day',
tag: 'input',
},
],
},
state: 5000,
preferences: {
permission: ['admin.channel_formular'],
},
frontend: false,
)
Setting.create_if_not_exists(
title: 'Ticket Subject Size',
name: 'ticket_subject_size',

View file

@ -56,7 +56,7 @@ or
def self.email(params)
# send verify email
subject = if !params[:subject] || params[:subject].empty?
subject = if params[:subject].blank?
'#' + rand(99_999_999_999).to_s
else
params[:subject]

View file

@ -16,7 +16,7 @@ module Import
end
def source
import_class_namespace
self.class.source
end
def remote_id(resource, *_args)
@ -57,6 +57,14 @@ module Import
changes
end
def self.source
import_class_namespace
end
def self.import_class_namespace
@import_class_namespace ||= name.to_s.sub('Import::', '')
end
private
def initialize_associations_states
@ -83,6 +91,9 @@ module Import
@resource = lookup_existing(resource, *args)
return false if !@resource
# lock the current resource for write access
@resource.with_lock do
# delete since we have an update and
# the record is already created
resource.delete(:created_by_id)
@ -109,6 +120,7 @@ module Import
@resource.save!
true
end
end
def lookup_existing(resource, *_args)
@ -214,11 +226,7 @@ module Import
end
def mapping_config(*_args)
import_class_namespace.gsub('::', '_').underscore + '_mapping'
end
def import_class_namespace
self.class.name.to_s.sub('Import::', '')
self.class.import_class_namespace.gsub('::', '_').underscore + '_mapping'
end
def handle_args(_resource, *args)

View file

@ -6,6 +6,34 @@ module Import
@remote_id
end
def self.lost_map(found_remote_ids)
ExternalSync.joins('INNER JOIN users ON (users.id = external_syncs.o_id)')
.where(
source: source,
object: import_class.name,
users: {
active: true
}
)
.pluck(:source_id, :o_id)
.to_h
.except(*found_remote_ids)
end
def self.deactivate_lost(lost_ids)
# we need to update in slices since some DBs
# have a limit for IN length
lost_ids.each_slice(5000) do |slice|
# we need to instanciate every entry and set
# the active state this way to send notifications
# to the client
::User.where(id: slice).each do |user|
user.update_attribute(:active, false)
end
end
end
private
def import(resource, *args)
@ -58,7 +86,7 @@ module Import
return true if resource[:login].blank?
# skip resource if only ignored attributes are set
ignored_attributes = %i(login dn created_by_id updated_by_id)
ignored_attributes = %i(login dn created_by_id updated_by_id active)
!resource.except(*ignored_attributes).values.any?(&:present?)
end
@ -181,6 +209,11 @@ module Import
mapped[attribute] = mapped[attribute].downcase
end
# we have to add the active state manually
# because otherwise disabled instances won't get
# re-activated if they should get synced again
mapped[:active] = true
mapped
end

View file

@ -33,10 +33,14 @@ module Import
relevant_attributes = config[:user_attributes].keys
relevant_attributes.push('dn')
@found_lost_remote_ids = []
@found_remote_ids = []
@ldap.search(config[:user_filter], attributes: relevant_attributes) do |entry|
backend_instance = create_instance(entry, config, user_roles, signup_role_ids, kargs)
post_import_hook(entry, backend_instance, config, user_roles, signup_role_ids, kargs)
track_found_remote_ids(backend_instance)
next if import_job.blank?
import_job_count += 1
next if import_job_count < 100
@ -47,6 +51,7 @@ module Import
import_job_count = 0
end
handle_lost
end
def self.pre_import_hook(_records, *_args)
@ -77,18 +82,25 @@ module Import
action = backend_instance.action
add_resource_role_ids_to_statistics(resource.role_ids, action)
action
end
def self.add_resource_role_ids_to_statistics(role_ids, action)
return if role_ids.blank?
known_actions = {
created: 0,
updated: 0,
unchanged: 0,
failed: 0,
deactivated: 0,
}
if !@statistics[:role_ids]
@statistics[:role_ids] = {}
end
@statistics[:role_ids] ||= {}
resource.role_ids.each do |role_id|
role_ids.each do |role_id|
next if !known_actions.key?(action)
@ -99,8 +111,6 @@ module Import
@statistics[:role_ids][role_id][action] += 1
end
action
end
def self.user_roles(ldap:, config:)
@ -111,6 +121,46 @@ module Import
ldap_group = ::Ldap::Group.new(group_config, ldap: ldap)
ldap_group.user_roles(config[:group_role_map])
end
def self.track_found_remote_ids(backend_instance)
remote_id = backend_instance.remote_id(nil)
@deactivation_actions ||= %i(skipped failed)
if @deactivation_actions.include?(backend_instance.action)
@found_lost_remote_ids.push(remote_id)
else
@found_remote_ids.push(remote_id)
end
end
def self.handle_lost
backend_class = backend_class(nil)
lost_map = backend_class.lost_map(@found_remote_ids)
# disabled count is tracked as a separate number
# since they don't have to be in the sum (e.g. deleted in LDAP)
@statistics[:deactivated] = lost_map.size
# skipped deactivated are those who
# were found, skipped and will get deactivated
skipped_deactivated = @found_lost_remote_ids & lost_map.keys
@statistics[:skipped] -= skipped_deactivated.size
# loop over every lost user ID and add the
# deactivated count to the statistics
lost_ids = lost_map.values
lost_ids.each do |user_id|
role_ids = ::User.joins(:roles)
.where(id: user_id)
.pluck(:'roles_users.role_id')
add_resource_role_ids_to_statistics(role_ids, :deactivated)
end
# deactivate entries only on live syncs
return if @dry_run
backend_class.deactivate_lost(lost_ids)
end
end
end
end

View file

@ -2,11 +2,19 @@ module Import
class ModelResource < Import::BaseResource
def import_class
model_name.constantize
self.class.import_class
end
def model_name
@model_name ||= self.class.name.split('::').last
self.class.model_name
end
def self.import_class
model_name.constantize
end
def self.model_name
@model_name ||= name.split('::').last
end
private

View file

@ -95,6 +95,8 @@ module Import
def handle_response(response)
encoded_body = Encode.conv('utf8', response.body.to_s)
# remove null bytes otherwise PostgreSQL will fail
encoded_body.gsub!('\u0000', '')
JSON.parse(encoded_body)
end

View file

@ -43,9 +43,7 @@ module Import
return if !state
state.default_create = true
state.callback_loop = true
state.save
state.save!
end
def update_default_follow_up
@ -56,9 +54,7 @@ module Import
return if !state
state.default_follow_up = true
state.callback_loop = true
state.save
state.save!
end
def update_ticket_attributes

View file

@ -18,6 +18,7 @@ module Import
updated: 0,
unchanged: 0,
failed: 0,
deactivated: 0,
}
end

View file

@ -80,14 +80,14 @@ class Ldap
filter ||= filter()
result = {}
@ldap.search(filter, attributes: %w(dn member)) do |entry|
members = entry[:member]
next if members.blank?
@ldap.search(filter, attributes: %w(dn member memberuid)) do |entry|
roles = mapping[entry.dn.downcase]
next if roles.blank?
members = group_user_dns(entry)
next if members.blank?
members.each do |user_dn|
user_dn_key = user_dn.downcase
@ -133,5 +133,18 @@ class Ldap
@uid_attribute = config[:uid_attribute]
@filter = config[:filter]
end
def group_user_dns(entry)
return entry[:member] if entry[:member].present?
return if entry[:memberuid].blank?
entry[:memberuid].collect do |uid|
dn = nil
@ldap.search("(uid=#{uid})", attributes: %w(dn)) do |user|
dn = user.dn
end
dn
end.compact
end
end
end

View file

@ -162,7 +162,7 @@ class Ldap
#
# @return [String, nil] The active or found filter or nil if none could be found.
def filter
@filter ||= lookup_filter(['(&(objectClass=user)(samaccountname=*)(!(samaccountname=*$)))', '(objectClass=user)', '(objectClass=posixaccount)'])
@filter ||= lookup_filter(['(&(objectClass=user)(samaccountname=*)(!(samaccountname=*$)))', '(objectClass=user)', '(objectClass=posixaccount)', '(objectClass=person)'])
end
# The active uid attribute of the instance. If none give on initialization an automatic lookup is performed.

View file

@ -427,8 +427,7 @@ return true if backend is configured
=end
def self.enabled?
return if !Setting.get('es_url')
return if Setting.get('es_url').empty?
return false if Setting.get('es_url').blank?
true
end

View file

@ -536,7 +536,7 @@ do($ = window.jQuery, window) ->
@maybeAddTimestamp()
# add message before message typing loader
if @el.find('.zammad-chat-message--typing').size()
if @el.find('.zammad-chat-message--typing').get(0)
@lastAddedType = 'typing-placeholder'
@el.find('.zammad-chat-message--typing').before messageElement
else
@ -725,7 +725,7 @@ do($ = window.jQuery, window) ->
@stopTypingId = setTimeout(@onAgentTypingEnd, 3000)
# never display two typing indicators
return if @el.find('.zammad-chat-message--typing').size()
return if @el.find('.zammad-chat-message--typing').get(0)
@maybeAddTimestamp()

View file

@ -1,64 +1,3 @@
if (!window.zammadChatTemplates) {
window.zammadChatTemplates = {};
}
window.zammadChatTemplates["agent"] = function (__obj) {
if (!__obj) __obj = {};
var __out = [], __capture = function(callback) {
var out = __out, result;
__out = [];
callback.call(this);
result = __out.join('');
__out = out;
return __safe(result);
}, __sanitize = function(value) {
if (value && value.ecoSafe) {
return value;
} else if (typeof value !== 'undefined' && value != null) {
return __escape(value);
} else {
return '';
}
}, __safe, __objSafe = __obj.safe, __escape = __obj.escape;
__safe = __obj.safe = function(value) {
if (value && value.ecoSafe) {
return value;
} else {
if (!(typeof value !== 'undefined' && value != null)) value = '';
var result = new String(value);
result.ecoSafe = true;
return result;
}
};
if (!__escape) {
__escape = __obj.escape = function(value) {
return ('' + value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
};
}
(function() {
(function() {
if (this.agent.avatar) {
__out.push('\n<img class="zammad-chat-agent-avatar" src="');
__out.push(__sanitize(this.agent.avatar));
__out.push('">\n');
}
__out.push('\n<span class="zammad-chat-agent-sentence">\n <span class="zammad-chat-agent-name">');
__out.push(__sanitize(this.agent.name));
__out.push('</span>\n</span>');
}).call(this);
}).call(__obj);
__obj.safe = __objSafe, __obj.escape = __escape;
return __out.join('');
};
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
slice = [].slice,
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
@ -823,7 +762,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
unreadClass: ''
});
this.maybeAddTimestamp();
if (this.el.find('.zammad-chat-message--typing').size()) {
if (this.el.find('.zammad-chat-message--typing').get(0)) {
this.lastAddedType = 'typing-placeholder';
this.el.find('.zammad-chat-message--typing').before(messageElement);
} else {
@ -1022,7 +961,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
clearTimeout(this.stopTypingId);
}
this.stopTypingId = setTimeout(this.onAgentTypingEnd, 3000);
if (this.el.find('.zammad-chat-message--typing').size()) {
if (this.el.find('.zammad-chat-message--typing').get(0)) {
return;
}
this.maybeAddTimestamp();
@ -1395,6 +1334,67 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
return window.ZammadChat = ZammadChat;
})(window.jQuery, window);
if (!window.zammadChatTemplates) {
window.zammadChatTemplates = {};
}
window.zammadChatTemplates["agent"] = function (__obj) {
if (!__obj) __obj = {};
var __out = [], __capture = function(callback) {
var out = __out, result;
__out = [];
callback.call(this);
result = __out.join('');
__out = out;
return __safe(result);
}, __sanitize = function(value) {
if (value && value.ecoSafe) {
return value;
} else if (typeof value !== 'undefined' && value != null) {
return __escape(value);
} else {
return '';
}
}, __safe, __objSafe = __obj.safe, __escape = __obj.escape;
__safe = __obj.safe = function(value) {
if (value && value.ecoSafe) {
return value;
} else {
if (!(typeof value !== 'undefined' && value != null)) value = '';
var result = new String(value);
result.ecoSafe = true;
return result;
}
};
if (!__escape) {
__escape = __obj.escape = function(value) {
return ('' + value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
};
}
(function() {
(function() {
if (this.agent.avatar) {
__out.push('\n<img class="zammad-chat-agent-avatar" src="');
__out.push(__sanitize(this.agent.avatar));
__out.push('">\n');
}
__out.push('\n<span class="zammad-chat-agent-sentence">\n <span class="zammad-chat-agent-name">');
__out.push(__sanitize(this.agent.name));
__out.push('</span>\n</span>');
}).call(this);
}).call(__obj);
__obj.safe = __objSafe, __obj.escape = __escape;
return __out.join('');
};
/*!
* ----------------------------------------------------------------------------
* "THE BEER-WARE LICENSE" (Revision 42):

File diff suppressed because one or more lines are too long

View file

@ -14,7 +14,7 @@
<div id="feedback-form-inline"></div>
<div class="js-logDisplay"></div>
<div class="js-logDisplay" style="overflow-x: hidden;"></div>
<p>Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.</p>

View file

@ -204,8 +204,14 @@ $(function() {
if (this.options.test) {
params.test = true
}
params.fingerprint = this.fingerprint()
$.ajax({
method: 'post',
url: _this.endpoint_config,
cache: false,
processData: true,
data: params
}).done(function(data) {
_this.log('debug', 'config:', data)
@ -256,7 +262,7 @@ $(function() {
_this.log('debug', 'currentTime', currentTime)
_this.log('debug', 'modalOpenTime', _this.modalOpenTime.getTime())
_this.log('debug', 'diffTime', diff)
if (diff < 1000*8) {
if (diff < 1000*10) {
alert('Sorry, you look like an robot!')
return
}
@ -317,7 +323,10 @@ $(function() {
formData.append('test', true)
}
formData.append('token', this._config.token)
formData.append('fingerprint', this.fingerprint())
_this.log('debug', 'formData', formData)
return formData
}
@ -463,6 +472,22 @@ $(function() {
return string
}
Plugin.prototype.fingerprint = function () {
var canvas = document.createElement('canvas')
var ctx = canvas.getContext('2d')
var txt = 'https://zammad.com'
ctx.textBaseline = 'top'
ctx.font = '12px \'Arial\''
ctx.textBaseline = 'alphabetic'
ctx.fillStyle = '#f60'
ctx.fillRect(125,1,62,20)
ctx.fillStyle = '#069'
ctx.fillText(txt, 2, 15)
ctx.fillStyle = 'rgba(100, 200, 0, 0.7)'
ctx.fillText(txt, 4, 17)
return canvas.toDataURL()
}
$.fn[pluginName] = function (options) {
return this.each(function () {
var instance = $.data(this, 'plugin_' + pluginName)

View file

@ -1,6 +1,6 @@
#!/bin/bash
bundle install
bundle install --jobs 8
rm -rf tmp/cache*

View file

@ -79,7 +79,7 @@ sudo -u "${USER}" -H bash -l -c 'rvm alias create default 2.1.2'
sudo -u "${USER}" -H bash -l -c 'cd ~/zammad && gem install rails --no-ri --no-rdoc'
sudo -u "${USER}" -H bash -l -c 'cd ~/zammad && bundle install'
sudo -u "${USER}" -H bash -l -c 'cd ~/zammad && bundle install --jobs 8'
DBPASS=$(apg -x8|head -1)
echo Password $DBPASS

View file

@ -15,7 +15,7 @@ export RAILS_SERVE_STATIC_FILES=true
export ZAMMAD_SETTING_TTL=15
export Z_LOCALES=en-us:de-de
bundle install
bundle install --jobs 8
rm -rf tmp/screenshot*
rm -rf tmp/cache*

View file

@ -49,7 +49,195 @@ RSpec.describe Import::Ldap::UserFactory do
}.by(1)
end
it 'supports dry run' do
it 'deactivates lost users' do
config = {
user_filter: '(objectClass=user)',
group_filter: '(objectClass=group)',
user_uid: 'uid',
user_attributes: {
'uid' => 'login',
'email' => 'email',
}
}
persistent_entry = build(:ldap_entry)
persistent_entry['uid'] = ['exampleuid']
persistent_entry['email'] = ['example@example.com']
lost_entry = build(:ldap_entry)
lost_entry['uid'] = ['exampleuid_lost']
lost_entry['email'] = ['lost@example.com']
mocked_ldap = double(
host: 'ldap.example.com',
port: 636,
ssl: true,
base_dn: 'dc=example,dc=com'
)
# group user role mapping
expect(mocked_ldap).to receive(:search)
# user counting
expect(mocked_ldap).to receive(:count).and_return(2)
# user search
expect(mocked_ldap).to receive(:search).and_yield(persistent_entry).and_yield(lost_entry)
described_class.import(
config: config,
ldap: mocked_ldap,
)
# group user role mapping
expect(mocked_ldap).to receive(:search)
# user counting
expect(mocked_ldap).to receive(:count).and_return(1)
# user search
expect(mocked_ldap).to receive(:search).and_yield(persistent_entry)
expect do
described_class.import(
config: config,
ldap: mocked_ldap,
)
end.to change {
User.find_by(email: 'lost@example.com').active
}
end
it 're-activates previously lost users' do
config = {
user_filter: '(objectClass=user)',
group_filter: '(objectClass=group)',
user_uid: 'uid',
user_attributes: {
'uid' => 'login',
'email' => 'email',
}
}
persistent_entry = build(:ldap_entry)
persistent_entry['uid'] = ['exampleuid']
persistent_entry['email'] = ['example@example.com']
lost_entry = build(:ldap_entry)
lost_entry['uid'] = ['exampleuid_lost']
lost_entry['email'] = ['lost@example.com']
mocked_ldap = double(
host: 'ldap.example.com',
port: 636,
ssl: true,
base_dn: 'dc=example,dc=com'
)
# group user role mapping
expect(mocked_ldap).to receive(:search)
# user counting
expect(mocked_ldap).to receive(:count).and_return(2)
# user search
expect(mocked_ldap).to receive(:search).and_yield(persistent_entry).and_yield(lost_entry)
described_class.import(
config: config,
ldap: mocked_ldap,
)
# group user role mapping
expect(mocked_ldap).to receive(:search)
# user counting
expect(mocked_ldap).to receive(:count).and_return(1)
# user search
expect(mocked_ldap).to receive(:search).and_yield(persistent_entry)
described_class.import(
config: config,
ldap: mocked_ldap,
)
# group user role mapping
expect(mocked_ldap).to receive(:search)
# user counting
expect(mocked_ldap).to receive(:count).and_return(2)
# user search
expect(mocked_ldap).to receive(:search).and_yield(persistent_entry).and_yield(lost_entry)
expect do
described_class.import(
config: config,
ldap: mocked_ldap,
)
end.to change {
User.find_by(email: 'lost@example.com').active
}
end
it 'deactivates skipped users' do
config = {
user_filter: '(objectClass=user)',
group_filter: '(objectClass=group)',
user_uid: 'uid',
user_attributes: {
'uid' => 'login',
'email' => 'email',
},
}
lost_entry = build(:ldap_entry)
lost_entry['uid'] = ['exampleuid']
lost_entry['email'] = ['example@example.com']
mocked_ldap = double(
host: 'ldap.example.com',
port: 636,
ssl: true,
base_dn: 'dc=example,dc=com'
)
# group user role mapping
expect(mocked_ldap).to receive(:search)
# user counting
expect(mocked_ldap).to receive(:count).and_return(2)
# user search
expect(mocked_ldap).to receive(:search).and_yield(lost_entry)
described_class.import(
config: config,
ldap: mocked_ldap,
)
# activate skipping
config[:unassigned_users] = 'skip_sync'
config[:group_role_map] = {
'dummy' => %w(1 2),
}
# group user role mapping
mocked_entry = build(:ldap_entry)
mocked_entry['dn'] = 'dummy'
mocked_entry['member'] = ['dummy']
expect(mocked_ldap).to receive(:search).and_yield(mocked_entry)
# user counting
expect(mocked_ldap).to receive(:count).and_return(1)
# user search
expect(mocked_ldap).to receive(:search).and_yield(lost_entry)
expect do
described_class.import(
config: config,
ldap: mocked_ldap,
)
end.to change {
User.find_by(email: 'example@example.com').active
}
end
context 'dry run' do
it "doesn't sync users" do
config = {
user_filter: '(objectClass=user)',
@ -90,6 +278,65 @@ RSpec.describe Import::Ldap::UserFactory do
User.count
}
end
it "doesn't deactivates lost users" do
config = {
user_filter: '(objectClass=user)',
group_filter: '(objectClass=group)',
user_uid: 'uid',
user_attributes: {
'uid' => 'login',
'email' => 'email',
}
}
persistent_entry = build(:ldap_entry)
persistent_entry['uid'] = ['exampleuid']
persistent_entry['email'] = ['example@example.com']
lost_entry = build(:ldap_entry)
lost_entry['uid'] = ['exampleuid']
lost_entry['email'] = ['example@example.com']
mocked_ldap = double(
host: 'ldap.example.com',
port: 636,
ssl: true,
base_dn: 'dc=example,dc=com'
)
# group user role mapping
expect(mocked_ldap).to receive(:search)
# user counting
expect(mocked_ldap).to receive(:count).and_return(2)
# user search
expect(mocked_ldap).to receive(:search).and_yield(persistent_entry).and_yield(lost_entry)
described_class.import(
config: config,
ldap: mocked_ldap,
dry_run: true
)
# group user role mapping
expect(mocked_ldap).to receive(:search)
# user counting
expect(mocked_ldap).to receive(:count).and_return(1)
# user search
expect(mocked_ldap).to receive(:search).and_yield(persistent_entry)
expect do
described_class.import(
config: config,
ldap: mocked_ldap,
dry_run: true
)
end.not_to change {
User.count
}
end
end
end
describe '.add_to_statistics' do
@ -118,13 +365,15 @@ RSpec.describe Import::Ldap::UserFactory do
created: 1,
updated: 0,
unchanged: 0,
failed: 0
failed: 0,
deactivated: 0,
},
2 => {
created: 1,
updated: 0,
unchanged: 0,
failed: 0
failed: 0,
deactivated: 0,
},
},
skipped: 0,
@ -132,6 +381,72 @@ RSpec.describe Import::Ldap::UserFactory do
updated: 0,
unchanged: 0,
failed: 0,
deactivated: 0,
}
expect(described_class.statistics).to include(expected)
end
it 'adds deactivated users' do
config = {
user_filter: '(objectClass=user)',
group_filter: '(objectClass=group)',
user_uid: 'uid',
user_attributes: {
'uid' => 'login',
'email' => 'email',
}
}
persistent_entry = build(:ldap_entry)
persistent_entry['uid'] = ['exampleuid']
persistent_entry['email'] = ['example@example.com']
lost_entry = build(:ldap_entry)
lost_entry['uid'] = ['exampleuid_lost']
lost_entry['email'] = ['lost@example.com']
mocked_ldap = double(
host: 'ldap.example.com',
port: 636,
ssl: true,
base_dn: 'dc=example,dc=com'
)
# group user role mapping
expect(mocked_ldap).to receive(:search)
# user counting
allow(mocked_ldap).to receive(:count).and_return(2)
# user search
expect(mocked_ldap).to receive(:search).and_yield(persistent_entry).and_yield(lost_entry)
described_class.import(
config: config,
ldap: mocked_ldap,
)
# simulate new import
described_class.reset_statistics
# group user role mapping
expect(mocked_ldap).to receive(:search)
# user counting
allow(mocked_ldap).to receive(:count).and_return(1)
# user search
expect(mocked_ldap).to receive(:search).and_yield(persistent_entry)
described_class.import(
config: config,
ldap: mocked_ldap,
)
expected = {
skipped: 0,
created: 0,
updated: 0,
unchanged: 1,
failed: 0,
deactivated: 1,
}
expect(described_class.statistics).to include(expected)
@ -155,6 +470,7 @@ RSpec.describe Import::Ldap::UserFactory do
updated: 0,
unchanged: 0,
failed: 0,
deactivated: 0,
}
expect(described_class.statistics).to include(expected)
@ -180,6 +496,7 @@ RSpec.describe Import::Ldap::UserFactory do
updated: 0,
unchanged: 0,
failed: 0,
deactivated: 0,
}
expect(described_class.statistics).to include(expected)

View file

@ -50,6 +50,7 @@ RSpec.describe Import::StatisticalFactory do
unchanged: 0,
skipped: 0,
failed: 0,
deactivated: 0,
}
expect(Import::Test::GroupFactory.statistics).to eq(statistics)
end
@ -72,6 +73,7 @@ RSpec.describe Import::StatisticalFactory do
unchanged: 0,
skipped: 0,
failed: 0,
deactivated: 0,
}
expect(Import::Test::GroupFactory.statistics).to eq(statistics)
end
@ -95,6 +97,7 @@ RSpec.describe Import::StatisticalFactory do
unchanged: 0,
skipped: 0,
failed: 0,
deactivated: 0,
}
expect(Import::Test::GroupFactory.statistics).to eq(statistics)
end
@ -118,6 +121,7 @@ RSpec.describe Import::StatisticalFactory do
unchanged: 0,
skipped: 0,
failed: 0,
deactivated: 0,
}
expect(Import::Test::GroupFactory.statistics).to eq(statistics)
end
@ -139,6 +143,7 @@ RSpec.describe Import::StatisticalFactory do
unchanged: 1,
skipped: 0,
failed: 0,
deactivated: 0,
}
expect(Import::Test::GroupFactory.statistics).to eq(statistics)
end
@ -156,6 +161,7 @@ RSpec.describe Import::StatisticalFactory do
unchanged: 0,
skipped: 0,
failed: 0,
deactivated: 0,
}
expect(Import::Test::GroupFactory.statistics).to eq(statistics)
end
@ -186,6 +192,7 @@ RSpec.describe Import::StatisticalFactory do
unchanged: 0,
skipped: 0,
failed: 0,
deactivated: 0,
}
expect(Import::Test::GroupFactory.statistics).to eq(statistics)
end
@ -204,6 +211,7 @@ RSpec.describe Import::StatisticalFactory do
unchanged: 0,
skipped: 0,
failed: 0,
deactivated: 0,
}
expect(Import::Test::GroupFactory.statistics).to eq(statistics)
end
@ -222,6 +230,7 @@ RSpec.describe Import::StatisticalFactory do
unchanged: 0,
skipped: 0,
failed: 0,
deactivated: 0,
}
expect(Import::Test::GroupFactory.statistics).to eq(statistics)
end
@ -246,6 +255,7 @@ RSpec.describe Import::StatisticalFactory do
unchanged: 1,
skipped: 0,
failed: 0,
deactivated: 0,
}
expect(Import::Test::GroupFactory.statistics).to eq(statistics)
end

View file

@ -1,5 +1,5 @@
RSpec.configure do |config|
config.before(:all) do
config.before(:each) do
# clear the cache otherwise it won't
# be able to recognize the rollbacks
# done by RSpec

View file

@ -82,23 +82,6 @@ class FormTest < TestCase
browser: agent,
css: 'body div.zammad-form-modal button[type="submit"][disabled]',
)
set(
browser: agent,
css: 'body div.zammad-form-modal [name="email"]',
value: 'notexistinginanydomainspacealsonothere@znuny.com',
)
click(
browser: agent,
css: 'body div.zammad-form-modal button[type="submit"]',
)
watch_for(
browser: agent,
css: 'body div.zammad-form-modal .has-error [name="email"]',
)
watch_for_disappear(
browser: agent,
css: 'body div.zammad-form-modal button[type="submit"][disabled]',
)
set(
browser: agent,
css: 'body div.zammad-form-modal [name="email"]',
@ -315,23 +298,6 @@ class FormTest < TestCase
browser: customer,
css: 'body div.zammad-form-modal button[type="submit"][disabled]',
)
set(
browser: customer,
css: 'body div.zammad-form-modal [name="email"]',
value: 'notexistinginanydomainspacealsonothere@znuny.com',
)
click(
browser: customer,
css: 'body div.zammad-form-modal button[type="submit"]',
)
watch_for(
browser: customer,
css: 'body div.zammad-form-modal .has-error [name="email"]',
)
watch_for_disappear(
browser: customer,
css: 'body div.zammad-form-modal button[type="submit"][disabled]',
)
set(
browser: customer,
css: 'body div.zammad-form-modal [name="email"]',

View file

@ -2261,7 +2261,7 @@ wait untill text in selector disabppears
9.times {
begin
text = instance.find_elements(css: '.content.active .js-reset')[0].text
if !text || text.empty?
if text.blank?
screenshot(browser: instance, comment: 'ticket_update_ok')
sleep 1
return true

View file

@ -0,0 +1,241 @@
# encoding: utf-8
require 'test_helper'
require 'rake'
class FormControllerTest < ActionDispatch::IntegrationTest
setup do
@headers = { 'ACCEPT' => 'application/json', 'CONTENT_TYPE' => 'application/json', 'REMOTE_ADDR' => '1.2.3.4' }
if ENV['ES_URL'].present?
#fail "ERROR: Need ES_URL - hint ES_URL='http://127.0.0.1:9200'"
Setting.set('es_url', ENV['ES_URL'])
# Setting.set('es_url', 'http://127.0.0.1:9200')
# Setting.set('es_index', 'estest.local_zammad')
# Setting.set('es_user', 'elasticsearch')
# Setting.set('es_password', 'zammad')
if ENV['ES_INDEX_RAND'].present?
ENV['ES_INDEX'] = "es_index_#{rand(999_999_999)}"
end
if ENV['ES_INDEX'].blank?
raise "ERROR: Need ES_INDEX - hint ES_INDEX='estest.local_zammad'"
end
Setting.set('es_index', ENV['ES_INDEX'])
end
Ticket.destroy_all
# drop/create indexes
Setting.reload
Rake::Task.clear
Zammad::Application.load_tasks
Rake::Task['searchindex:rebuild'].execute
end
test '01 - get config call' do
post '/api/v1/form_config', {}.to_json, @headers
assert_response(401)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert_equal(result['error'], 'Not authorized')
end
test '02 - get config call' do
Setting.set('form_ticket_create', true)
post '/api/v1/form_config', {}.to_json, @headers
assert_response(401)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert_equal(result['error'], 'Not authorized')
end
test '03 - get config call & do submit' do
Setting.set('form_ticket_create', true)
fingerprint = SecureRandom.hex(40)
post '/api/v1/form_config', { fingerprint: fingerprint }.to_json, @headers
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert_equal(result['enabled'], true)
assert_equal(result['endpoint'], 'http://zammad.example.com/api/v1/form_submit')
assert(result['token'])
token = result['token']
post '/api/v1/form_submit', { fingerprint: fingerprint, token: 'invalid' }.to_json, @headers
assert_response(401)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert_equal(result['error'], 'Not authorized')
post '/api/v1/form_submit', { fingerprint: fingerprint, token: token }.to_json, @headers
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert(result['errors'])
assert_equal(result['errors']['name'], 'required')
assert_equal(result['errors']['email'], 'required')
assert_equal(result['errors']['title'], 'required')
assert_equal(result['errors']['body'], 'required')
post '/api/v1/form_submit', { fingerprint: fingerprint, token: token, email: 'some' }.to_json, @headers
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert(result['errors'])
assert_equal(result['errors']['name'], 'required')
assert_equal(result['errors']['email'], 'invalid')
assert_equal(result['errors']['title'], 'required')
assert_equal(result['errors']['body'], 'required')
post '/api/v1/form_submit', { fingerprint: fingerprint, token: token, name: 'Bob Smith', email: 'discard@znuny.com', title: 'test', body: 'hello' }.to_json, @headers
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert_not(result['errors'])
assert(result['ticket'])
assert(result['ticket']['id'])
assert(result['ticket']['number'])
travel 5.hours
post '/api/v1/form_submit', { fingerprint: fingerprint, token: token, name: 'Bob Smith', email: 'discard@znuny.com', title: 'test', body: 'hello' }.to_json, @headers
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert_not(result['errors'])
assert(result['ticket'])
assert(result['ticket']['id'])
assert(result['ticket']['number'])
travel 20.hours
post '/api/v1/form_submit', { fingerprint: fingerprint, token: token, name: 'Bob Smith', email: 'discard@znuny.com', title: 'test', body: 'hello' }.to_json, @headers
assert_response(401)
end
test '04 - get config call & do submit' do
Setting.set('form_ticket_create', true)
fingerprint = SecureRandom.hex(40)
post '/api/v1/form_config', { fingerprint: fingerprint }.to_json, @headers
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert_equal(result['enabled'], true)
assert_equal(result['endpoint'], 'http://zammad.example.com/api/v1/form_submit')
assert(result['token'])
token = result['token']
post '/api/v1/form_submit', { fingerprint: fingerprint, token: 'invalid' }.to_json, @headers
assert_response(401)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert_equal(result['error'], 'Not authorized')
post '/api/v1/form_submit', { fingerprint: fingerprint, token: token }.to_json, @headers
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert(result['errors'])
assert_equal(result['errors']['name'], 'required')
assert_equal(result['errors']['email'], 'required')
assert_equal(result['errors']['title'], 'required')
assert_equal(result['errors']['body'], 'required')
post '/api/v1/form_submit', { fingerprint: fingerprint, token: token, email: 'some' }.to_json, @headers
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert(result['errors'])
assert_equal(result['errors']['name'], 'required')
assert_equal(result['errors']['email'], 'invalid')
assert_equal(result['errors']['title'], 'required')
assert_equal(result['errors']['body'], 'required')
post '/api/v1/form_submit', { fingerprint: fingerprint, token: token, name: 'Bob Smith', email: 'somebody@example.com', title: 'test', body: 'hello' }.to_json, @headers
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert(result['errors'])
assert_equal(result['errors']['email'], 'invalid')
end
test '05 - limits' do
return if !SearchIndexBackend.enabled?
Setting.set('form_ticket_create', true)
fingerprint = SecureRandom.hex(40)
post '/api/v1/form_config', { fingerprint: fingerprint }.to_json, @headers
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert_equal(result['enabled'], true)
assert_equal(result['endpoint'], 'http://zammad.example.com/api/v1/form_submit')
assert(result['token'])
token = result['token']
(1..20).each { |count|
travel 10.seconds
post '/api/v1/form_submit', { fingerprint: fingerprint, token: token, name: 'Bob Smith', email: 'discard@znuny.com', title: "test#{count}", body: 'hello' }.to_json, @headers
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert_not(result['errors'])
assert(result['ticket'])
assert(result['ticket']['id'])
assert(result['ticket']['number'])
Scheduler.worker(true)
sleep 1 # wait until elasticsearch is index
}
sleep 10 # wait until elasticsearch is index
post '/api/v1/form_submit', { fingerprint: fingerprint, token: token, name: 'Bob Smith', email: 'discard@znuny.com', title: 'test-last', body: 'hello' }.to_json, @headers
assert_response(401)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert(result['error'])
@headers = { 'ACCEPT' => 'application/json', 'CONTENT_TYPE' => 'application/json', 'REMOTE_ADDR' => '1.2.3.5' }
(1..20).each { |count|
travel 10.seconds
post '/api/v1/form_submit', { fingerprint: fingerprint, token: token, name: 'Bob Smith', email: 'discard@znuny.com', title: "test-2-#{count}", body: 'hello' }.to_json, @headers
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert_not(result['errors'])
assert(result['ticket'])
assert(result['ticket']['id'])
assert(result['ticket']['number'])
Scheduler.worker(true)
sleep 1 # wait until elasticsearch is index
}
sleep 10 # wait until elasticsearch is index
post '/api/v1/form_submit', { fingerprint: fingerprint, token: token, name: 'Bob Smith', email: 'discard@znuny.com', title: 'test-2-last', body: 'hello' }.to_json, @headers
assert_response(401)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert(result['error'])
end
end

View file

@ -1414,6 +1414,228 @@ class TicketTriggerTest < ActiveSupport::TestCase
end
test '6.1 owner auto assignment based on organization' do
trigger1 = Trigger.create_or_update(
name: 'aaa auto assignment',
condition: {
'ticket.organization_id' => {
'operator' => 'is not',
'pre_condition' => 'not_set',
'value' => '',
'value_completion' => '',
},
'ticket.action' => {
'operator' => 'is',
'value' => 'update',
},
},
perform: {
'ticket.owner_id' => {
'pre_condition' => 'current_user.id',
'value' => '',
'value_completion' => '',
},
},
disable_notification: true,
active: true,
created_by_id: 1,
updated_by_id: 1,
)
roles = Role.where(name: 'Agent')
agent = User.create_or_update(
login: 'agent@example.com',
firstname: 'Trigger',
lastname: 'Agent1',
email: 'agent@example.com',
password: 'agentpw',
active: true,
roles: roles,
updated_by_id: 1,
created_by_id: 1,
)
roles = Role.where(name: 'Customer')
customer = User.create_or_update(
login: 'customer@example.com',
firstname: 'Trigger',
lastname: 'Customer1',
email: 'customer@example.com',
password: 'customerpw',
vip: true,
active: true,
roles: roles,
updated_by_id: 1,
created_by_id: 1,
)
ticket1 = Ticket.create(
title: 'test 123',
group: Group.lookup(name: 'Users'),
customer: customer,
updated_by_id: 1,
created_by_id: 1,
)
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 <b>note</b>\nnew line",
internal: false,
sender: Ticket::Article::Sender.find_by(name: 'Agent'),
type: Ticket::Article::Type.find_by(name: 'note'),
updated_by_id: 1,
created_by_id: 1,
)
Observer::Transaction.commit
assert_equal('test 123', ticket1.title, 'ticket1.title verify')
assert_equal('Users', ticket1.group.name, 'ticket1.group verify')
assert_equal(1, ticket1.owner_id, 'ticket1.owner_id verify')
assert_equal('new', ticket1.state.name, 'ticket1.state verify')
assert_equal('2 normal', ticket1.priority.name, 'ticket1.priority verify')
assert_equal(1, ticket1.articles.count, 'ticket1.articles verify')
assert_equal([], ticket1.tag_list)
ticket1.update_attribute(:customer, User.lookup(email: 'nicole.braun@zammad.org') )
UserInfo.current_user_id = agent.id
Ticket::Article.create(
ticket_id: ticket1.id,
from: 'some_sender@example.com',
to: 'some_recipient@example.com',
subject: 'update',
message_id: 'some@id',
content_type: 'text/html',
body: 'update',
internal: false,
sender: Ticket::Article::Sender.find_by(name: 'Agent'),
type: Ticket::Article::Type.find_by(name: 'note'),
)
Observer::Transaction.commit
UserInfo.current_user_id = nil
ticket1.reload
assert_equal('test 123', ticket1.title, 'ticket1.title verify')
assert_equal('Users', ticket1.group.name, 'ticket1.group verify')
assert_equal(agent.id, ticket1.owner_id, 'ticket1.owner_id verify')
assert_equal('new', ticket1.state.name, 'ticket1.state verify')
assert_equal('2 normal', ticket1.priority.name, 'ticket1.priority verify')
assert_equal(2, ticket1.articles.count, 'ticket1.articles verify')
assert_equal([], ticket1.tag_list)
end
test '6.2 owner auto assignment based on organization' do
trigger1 = Trigger.create_or_update(
name: 'aaa auto assignment',
condition: {
'ticket.organization_id' => {
'operator' => 'is',
'pre_condition' => 'not_set',
'value' => '',
'value_completion' => '',
},
'ticket.action' => {
'operator' => 'is',
'value' => 'update',
},
},
perform: {
'ticket.owner_id' => {
'pre_condition' => 'current_user.id',
'value' => '',
'value_completion' => '',
},
},
disable_notification: true,
active: true,
created_by_id: 1,
updated_by_id: 1,
)
roles = Role.where(name: 'Agent')
agent = User.create_or_update(
login: 'agent@example.com',
firstname: 'Trigger',
lastname: 'Agent1',
email: 'agent@example.com',
password: 'agentpw',
active: true,
roles: roles,
updated_by_id: 1,
created_by_id: 1,
)
roles = Role.where(name: 'Customer')
customer = User.create_or_update(
login: 'customer@example.com',
firstname: 'Trigger',
lastname: 'Customer1',
email: 'customer@example.com',
password: 'customerpw',
vip: true,
active: true,
roles: roles,
updated_by_id: 1,
created_by_id: 1,
)
ticket1 = Ticket.create(
title: 'test 123',
group: Group.lookup(name: 'Users'),
customer: User.lookup(email: 'nicole.braun@zammad.org'),
updated_by_id: 1,
created_by_id: 1,
)
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 <b>note</b>\nnew line",
internal: false,
sender: Ticket::Article::Sender.find_by(name: 'Agent'),
type: Ticket::Article::Type.find_by(name: 'note'),
updated_by_id: 1,
created_by_id: 1,
)
Observer::Transaction.commit
assert_equal('test 123', ticket1.title, 'ticket1.title verify')
assert_equal('Users', ticket1.group.name, 'ticket1.group verify')
assert_equal(1, ticket1.owner_id, 'ticket1.owner_id verify')
assert_equal('new', ticket1.state.name, 'ticket1.state verify')
assert_equal('2 normal', ticket1.priority.name, 'ticket1.priority verify')
assert_equal(1, ticket1.articles.count, 'ticket1.articles verify')
assert_equal([], ticket1.tag_list)
ticket1.update_attribute(:customer, customer )
UserInfo.current_user_id = agent.id
Ticket::Article.create(
ticket_id: ticket1.id,
from: 'some_sender@example.com',
to: 'some_recipient@example.com',
subject: 'update',
message_id: 'some@id',
content_type: 'text/html',
body: 'update',
internal: false,
sender: Ticket::Article::Sender.find_by(name: 'Agent'),
type: Ticket::Article::Type.find_by(name: 'note'),
)
Observer::Transaction.commit
UserInfo.current_user_id = nil
ticket1.reload
assert_equal('test 123', ticket1.title, 'ticket1.title verify')
assert_equal('Users', ticket1.group.name, 'ticket1.group verify')
assert_equal(agent.id, ticket1.owner_id, 'ticket1.owner_id verify')
assert_equal('new', ticket1.state.name, 'ticket1.state verify')
assert_equal('2 normal', ticket1.priority.name, 'ticket1.priority verify')
assert_equal(2, ticket1.articles.count, 'ticket1.articles verify')
assert_equal([], ticket1.tag_list)
end
test '7 owner auto assignment' do
trigger1 = Trigger.create_or_update(
name: 'aaa auto assignment',
@ -3120,9 +3342,8 @@ class TicketTriggerTest < ActiveSupport::TestCase
Observer::Transaction.commit
ticket1.reload
assert_equal(22, ticket1.articles.count)
assert_equal(21, ticket1.articles.count)
assert_equal('some_loop_sender@example.com', ticket1.articles[20].from)
assert_equal('nicole.braun@zammad.org', ticket1.articles[21].to)
Ticket::Article.create(
ticket_id: ticket1.id,
@ -3141,92 +3362,8 @@ class TicketTriggerTest < ActiveSupport::TestCase
Observer::Transaction.commit
ticket1.reload
assert_equal(24, ticket1.articles.count)
assert_equal('some_loop_sender@example.com', ticket1.articles[22].from)
assert_equal('nicole.braun@zammad.org', ticket1.articles[23].to)
Ticket::Article.create(
ticket_id: ticket1.id,
from: 'some_loop_sender@example.com',
to: 'some_loop_recipient@example.com',
subject: 'some subject 1234',
message_id: 'some@id',
content_type: 'text/html',
body: 'some message <b>note</b><br>new line',
internal: false,
sender: Ticket::Article::Sender.find_by(name: 'Customer'),
type: Ticket::Article::Type.find_by(name: 'email'),
updated_by_id: 1,
created_by_id: 1,
)
Observer::Transaction.commit
ticket1.reload
assert_equal(26, ticket1.articles.count)
assert_equal('some_loop_sender@example.com', ticket1.articles[24].from)
assert_equal('nicole.braun@zammad.org', ticket1.articles[25].to)
Ticket::Article.create(
ticket_id: ticket1.id,
from: 'some_loop_sender@example.com',
to: 'some_loop_recipient@example.com',
subject: 'some subject 1234',
message_id: 'some@id',
content_type: 'text/html',
body: 'some message <b>note</b><br>new line',
internal: false,
sender: Ticket::Article::Sender.find_by(name: 'Customer'),
type: Ticket::Article::Type.find_by(name: 'email'),
updated_by_id: 1,
created_by_id: 1,
)
Observer::Transaction.commit
ticket1.reload
assert_equal(28, ticket1.articles.count)
assert_equal('some_loop_sender@example.com', ticket1.articles[26].from)
assert_equal('nicole.braun@zammad.org', ticket1.articles[27].to)
Ticket::Article.create(
ticket_id: ticket1.id,
from: 'some_loop_sender@example.com',
to: 'some_loop_recipient@example.com',
subject: 'some subject 1234',
message_id: 'some@id',
content_type: 'text/html',
body: 'some message <b>note</b><br>new line',
internal: false,
sender: Ticket::Article::Sender.find_by(name: 'Customer'),
type: Ticket::Article::Type.find_by(name: 'email'),
updated_by_id: 1,
created_by_id: 1,
)
Observer::Transaction.commit
ticket1.reload
assert_equal(30, ticket1.articles.count)
assert_equal('some_loop_sender@example.com', ticket1.articles[28].from)
assert_equal('nicole.braun@zammad.org', ticket1.articles[29].to)
Ticket::Article.create(
ticket_id: ticket1.id,
from: 'some_loop_sender@example.com',
to: 'some_loop_recipient@example.com',
subject: 'some subject 1234',
message_id: 'some@id',
content_type: 'text/html',
body: 'some message <b>note</b><br>new line',
internal: false,
sender: Ticket::Article::Sender.find_by(name: 'Customer'),
type: Ticket::Article::Type.find_by(name: 'email'),
updated_by_id: 1,
created_by_id: 1,
)
Observer::Transaction.commit
ticket1.reload
assert_equal(31, ticket1.articles.count)
assert_equal('some_loop_sender@example.com', ticket1.articles[30].from)
assert_equal(22, ticket1.articles.count)
assert_equal('some_loop_sender@example.com', ticket1.articles[21].from)
end

View file

@ -204,6 +204,42 @@ class UserDeviceTest < ActiveSupport::TestCase
)
assert_equal(user_device2.id, user_device6.id)
# signin without ua from country A via basic auth -> new device #3
user_device7 = UserDevice.add(
'',
'91.115.248.231',
@agent.id,
nil,
'basic_auth',
)
assert_not_equal(user_device6.id, user_device7.id)
user_device8 = UserDevice.add(
'',
'91.115.248.231',
@agent.id,
nil,
'basic_auth',
)
assert_equal(user_device7.id, user_device8.id)
user_device9 = UserDevice.add(
nil,
'91.115.248.231',
@agent.id,
nil,
'basic_auth',
)
assert_equal(user_device8.id, user_device9.id)
user_device10 = UserDevice.add(
nil,
'176.198.137.254',
@agent.id,
nil,
'basic_auth',
)
assert_not_equal(user_device9.id, user_device10.id)
end
test 'ddd - api test' do