Merge branch 'develop' of git.znuny.com:zammad/zammad into develop
This commit is contained in:
commit
8cb2eb406f
62 changed files with 2117 additions and 537 deletions
|
@ -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
|
||||
|
|
|
@ -7,6 +7,7 @@ notifications:
|
|||
env:
|
||||
- DB=mysql
|
||||
- DB=postgresql
|
||||
- BUNDLE_JOBS=8
|
||||
addons:
|
||||
postgresql: "9.4"
|
||||
apt:
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -45,7 +45,7 @@ gem 'twitter'
|
|||
gem 'telegramAPI'
|
||||
gem 'koala'
|
||||
gem 'mail'
|
||||
gem 'email_verifier'
|
||||
gem 'valid_email2'
|
||||
gem 'htmlentities'
|
||||
|
||||
gem 'mime-types'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 },
|
||||
]
|
||||
|
|
|
@ -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, '<')
|
||||
.replace(/\>/g, '>')
|
||||
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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')()
|
|
@ -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(
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 %>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<div class="btn btn--action btn--split--first js-setting centered">
|
||||
<%- @Icon('cog', 'dropdown-icon') %>
|
||||
</div>
|
|
@ -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>
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
9
db/migrate/20170626000001_locale_add_direction.rb
Normal file
9
db/migrate/20170626000001_locale_add_direction.rb
Normal 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
|
103
db/migrate/20170628000001_form_group_selection.rb
Normal file
103
db/migrate/20170628000001_form_group_selection.rb
Normal 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
|
99
db/migrate/20170630000001_ticket_zoom_setting.rb
Normal file
99
db/migrate/20170630000001_ticket_zoom_setting.rb
Normal 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
|
|
@ -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',
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -18,6 +18,7 @@ module Import
|
|||
updated: 0,
|
||||
unchanged: 0,
|
||||
failed: 0,
|
||||
deactivated: 0,
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
};
|
||||
}
|
||||
(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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
};
|
||||
}
|
||||
(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):
|
||||
|
|
2
public/assets/chat/chat.min.js
vendored
2
public/assets/chat/chat.min.js
vendored
File diff suppressed because one or more lines are too long
|
@ -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>
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
|
||||
bundle install
|
||||
bundle install --jobs 8
|
||||
|
||||
rm -rf tmp/cache*
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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*
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]',
|
||||
|
|
|
@ -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
|
||||
|
|
241
test/controllers/form_controller_test.rb
Normal file
241
test/controllers/form_controller_test.rb
Normal 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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue