Merge branch 'develop' of github.com:martini/zammad into develop

This commit is contained in:
Felix Niklas 2015-10-23 16:29:52 +02:00
commit 0ace20fb4b
9 changed files with 293 additions and 89 deletions

View file

@ -1,15 +1,25 @@
class App.ControllerTable extends App.Controller
minColWidth: 20
constructor: (params) ->
for key, value of params
@[key] = value
@table = @tableGen(params)
# apply personal preferences
data = @preferencesGet()
if data['order']
for key, value of data['order']
@[key] = value
if @el
@el.append( @table )
@headerWidth = {}
if data['headerWidth']
for key, value of data['headerWidth']
@headerWidth[key] = value
@render()
render: =>
@html(@tableGen())
###
@ -51,6 +61,7 @@ class App.ControllerTable extends App.Controller
value
new App.ControllerTable(
table_id: 'some_id_to_idientify_user_based_table_preferences'
el: element
overview: ['host', 'user', 'adapter', 'active']
model: App.Channel
@ -98,38 +109,38 @@ class App.ControllerTable extends App.Controller
###
tableGen: (data) ->
if !data.model
data.model = {}
overview = data.overview || data.model.configure_overview || []
attributes = data.attributes || data.model.configure_attributes || {}
tableGen: =>
if !@model
@model = {}
overview = @overview || @model.configure_overview || []
attributes = @attributes || @model.configure_attributes || {}
attributes = App.Model.attributesGet(false, attributes)
destroy = data.model.configure_delete
destroy = @model.configure_delete
# check if table is empty
if _.isEmpty(data.objects)
if _.isEmpty(@objects)
table = App.view('generic/admin/empty')
explanation: data.explanation
explanation: @explanation
return $(table)
# group by
if data.groupBy
if @groupBy
# remove group by attribute from header
overview = _.filter(
overview
(item) ->
return item if item isnt data.groupBy
return item if item isnt @groupBy
return
)
# get new order
groupObjects = _.groupBy(
data.objects
@objects
(item) ->
return '' if !item[data.groupBy]
return item[data.groupBy].displayName() if item[data.groupBy].displayName
item[data.groupBy]
return '' if !item[@groupBy]
return item[@groupBy].displayName() if item[@groupBy].displayName
item[@groupBy]
)
groupOrder = []
for group, value of groupObjects
@ -143,41 +154,71 @@ class App.ControllerTable extends App.Controller
)
# create new data array
data.objects = []
@objects = []
for group in groupOrder
data.objects = data.objects.concat groupObjects[group]
@objects = @objects.concat groupObjects[group]
groupObjects[group] = [] # release old array
# get header data
header = []
headers = []
for item in overview
headerFound = false
for attributeName, attribute of attributes
if attributeName is item
headerFound = true
header.push attribute
if @headerWidth[attribute.name]
attribute.style = "width: #{@headerWidth[attribute.name]}px"
headers.push attribute
else
rowWithoutId = item + '_id'
if attributeName is rowWithoutId
headerFound = true
header.push attribute
if @headerWidth[attribute.name]
attribute.style = "width: #{@headerWidth[attribute.name]}px"
headers.push attribute
if @orderDirection && @orderBy
for header in headers
if header.name is @orderBy
@objects = _.sortBy(
@objects
(item) ->
# if we need to sort translated col.
if header.translate
App.i18n.translateInline(item[header.name])
# if we need to sort a relation
if header.relation
if item[header.name]
App[header.relation].find(item[header.name]).displayName()
else
''
else
item[header.name]
)
if @orderDirection is 'DESC'
header.sortOrderIcon = ['arrow-down', 'table-sort-arrow']
@objects = @objects.reverse()
else
header.sortOrderIcon = ['arrow-up', 'table-sort-arrow']
else
header.sortOrderIcon = undefined
# execute header callback
if data.callbackHeader
for callback in data.callbackHeader
header = callback(header)
if @callbackHeader
for callback in @callbackHeader
headers = callback(headers)
# get content
@log 'debug', 'table', 'header', header, 'overview', 'objects', data.objects
@log 'debug', 'table', 'header', headers, 'overview', 'objects', @objects
table = App.view('generic/table')(
header: header
objects: data.objects
checkbox: data.checkbox
radio: data.radio
groupBy: data.groupBy
class: data.class
header: headers
objects: @objects
checkbox: @checkbox
radio: @radio
groupBy: @groupBy
class: @class
destroy: destroy
callbacks: data.callbackAttributes
callbacks: @callbackAttributes
)
# convert to jquery object
@ -189,15 +230,15 @@ class App.ControllerTable extends App.Controller
#mouseover: 'alias'
# bind col.
if data.bindCol
for name, item of data.bindCol
if @bindCol
for name, item of @bindCol
if item.events
position = 0
if data.checkbox
if @checkbox
position += 1
hit = false
for headerName in header
for headerName in headers
if !hit
position += 1
if headerName.name is name || headerName.name is "#{name}_id"
@ -216,9 +257,9 @@ class App.ControllerTable extends App.Controller
)
# bind row
if data.bindRow
if data.bindRow.events
for event, callback of data.bindRow.events
if @bindRow
if @bindRow.events
for event, callback of @bindRow.events
do (table, event, callback) ->
if cursorMap[event]
table.find('tbody > tr').css( 'cursor', cursorMap[event] )
@ -229,9 +270,9 @@ class App.ControllerTable extends App.Controller
)
# bind bindCheckbox
if data.bindCheckbox
if data.bindCheckbox.events
for event, callback of data.bindCheckbox.events
if @bindCheckbox
if @bindCheckbox.events
for event, callback of @bindCheckbox.events
do (table, event, callback) ->
table.delegate('input[name="bulk"]', event, (e) ->
e.stopPropagation()
@ -241,18 +282,21 @@ class App.ControllerTable extends App.Controller
)
# bind on delete dialog
if data.model && destroy
if @model && destroy
table.delegate('[data-type="destroy"]', 'click', (e) =>
e.stopPropagation()
e.preventDefault()
itemId = $(e.target).parents('tr').data('id')
item = data.model.find(itemId)
item = @model.find(itemId)
new App.ControllerGenericDestroyConfirm(
item: item
container: @container
)
)
# if we have a personalised table
if @table_id
# enable resize column
table.on 'mousedown', '.js-col-resize', @onColResizeMousedown
table.on 'click', '.js-col-resize', @stopPropagation
@ -261,7 +305,7 @@ class App.ControllerTable extends App.Controller
table.on 'click', '.js-sort', @sortByColumn
# enable checkbox bulk selection
if data.checkbox
if @checkbox
# click first tr>td, catch click
table.delegate('tr > td:nth-child(1)', event, (e) ->
@ -285,7 +329,7 @@ class App.ControllerTable extends App.Controller
)
)
return table
table
stopPropagation: (event) =>
event.stopPropagation()
@ -316,17 +360,53 @@ class App.ControllerTable extends App.Controller
# switch to percentage
resizeBaseWidth = @resizeTargetLeft.parents('table').width()
leftWidth = @resizeTargetLeft.outerWidth()
rightWidth= @resizeTargetRight.outerWidth()
rightWidth = @resizeTargetRight.outerWidth()
leftColumnKey = @resizeTargetLeft.attr('data-column-key')
rightColumnKey = @resizeTargetRight.attr('data-column-key')
# save table changed widths
# @storeColWidths [
# { key: leftColumnKey, width: leftWidth }
# { key: rightColumnKey, width: rightWidth }
# ]
storeColWidths = [
{ key: leftColumnKey, width: leftWidth }
{ key: rightColumnKey, width: rightWidth }
]
@log 'error', @table_id, 'leftColumnKey', leftColumnKey, leftWidth, 'rightColumnKey', rightColumnKey, rightWidth
@preferencesStore('headerWidth', leftColumnKey, leftWidth)
@preferencesStore('headerWidth', rightColumnKey, rightWidth)
sortByColumn: (event) =>
column = $(event.currentTarget).closest('[data-column-key]').attr('data-column-key')
# sort
if @orderBy isnt column
@orderBy = column
@orderDirection = 'ASC'
else
if @orderDirection is 'ASC'
@orderDirection = 'DESC'
else
@orderDirection = 'ASC'
@log 'debug', @table_id, 'sortByColumn', @orderBy, 'direction', @orderDirection
@preferencesStore('order', 'orderBy', @orderBy)
@preferencesStore('order', 'orderDirection', @orderDirection)
@render()
preferencesStore: (type, key, value) ->
data = @preferencesGet()
if !data[type]
data[type] = {}
if !data[type][key]
data[type][key] = {}
data[type][key] = value
localStorage.setItem(@preferencesStoreKey(), JSON.stringify(data))
preferencesGet: =>
storeKey = @preferencesStoreKey()
data = localStorage.getItem(storeKey)
return {} if !data
JSON.parse(data)
preferencesStoreKey: =>
"tablePreferences:#{@table_id}"

View file

@ -273,19 +273,6 @@ class Table extends App.Controller
headers.unshift(0)
headers[0] = attribute
headers
callbackSortOrderHeader = (headers) =>
return headers if !@overview
return headers if !@overview.order
return headers if !@overview.order.by
for header in headers
if header.name is @overview.order.by
if @overview.order.direction is 'DESC'
header.sortOrderIcon = ['arrow-down', 'table-sort-arrow']
else
header.sortOrderIcon = ['arrow-up', 'table-sort-arrow']
else
header.sortOrderIcon = undefined
headers
callbackIcon = (value, object, attribute, header, refObject) ->
value = ' '
attribute.class = object.iconClass()
@ -294,12 +281,15 @@ class Table extends App.Controller
value
new App.ControllerTable(
table_id: "ticket_overview_#{@overview_id}"
overview: @overview.view.s
el: @$('.table-overview')
model: App.Ticket
objects: ticket_list_show
checkbox: checkbox
groupBy: @overview.group_by
orderBy: @overview.order.by
orderDirection: @overview.order.direction
bindRow:
events:
'click': openTicket
@ -307,7 +297,7 @@ class Table extends App.Controller
# customer_id:
# events:
# 'mouseover': popOver
callbackHeader: [ callbackIconHeader, callbackSortOrderHeader ]
callbackHeader: [ callbackIconHeader ]
callbackAttributes:
icon:
[ callbackIcon ]

View file

@ -547,6 +547,15 @@ class App.TicketZoom extends App.Controller
# validate article
articleParams = @formParam( @$('.article-add') )
console.log 'submit article', articleParams
# check if attachment exists but no body
attachmentCount = @$('.article-add .textBubble .attachments .attachment').length
if !articleParams['body'] && attachmentCount > 0
if !confirm( App.i18n.translateContent('Please fill also some text in!') )
@formEnable(e)
@autosaveStart()
return
if articleParams['body']
articleParams.from = @Session.get().displayName()
articleParams.ticket_id = ticket.id
@ -590,8 +599,7 @@ class App.TicketZoom extends App.Controller
# check attachment
if articleParams['body']
if App.Utils.checkAttachmentReference( articleParams['body'] )
if @$('.article-add .textBubble .attachments .attachment').length < 1
if App.Utils.checkAttachmentReference( articleParams['body'] ) && attachmentCount < 1
if !confirm( App.i18n.translateContent('You use attachment in text but no attachment is attached. Do you want to continue?') )
@formEnable(e)
@autosaveStart()

View file

@ -15,8 +15,8 @@
<% end %>
<% for item, i in @header: %>
<th<%= " class='#{ item.className }'" if item.className %><%= " style='#{ item.style }'" if item.style %> data-column-key="<%= item.name %>">
<div class="table-column-head">
<div class="table-column-title js-sort">
<div class="table-column-head js-sort">
<div class="table-column-title">
<%- @T( item.display ) %>
</div>
<div class="table-column-sortIcon">

View file

@ -536,6 +536,7 @@ returns
return if !email
return if email.empty?
return if email !~ /@/
# save/update avatar
avatar = Avatar.auto_detection(

View file

@ -107,6 +107,44 @@ class AgentTicketActionLevel6Test < TestCase
value: 'test 6 - ticket 1-1',
)
# add attachment without body
@browser.execute_script( "App.TestHelper.attachmentUploadFake('.active .article-add .textBubble .attachments')" )
# submit form
click(
css: '.active .js-submit',
)
sleep 2
# check warning
alert = @browser.switch_to.alert
alert.dismiss()
ticket_update(
data: {
body: 'now submit should work',
},
do_not_submit: true,
)
# submit form
click(
css: '.active .js-submit',
)
sleep 2
# discard changes should gone away
watch_for_disappear(
css: '.content.active .js-reset',
value: '(Discard your unsaved changes.|Verwerfen der)',
no_quote: true,
)
ticket_verify(
data: {
body: '',
},
)
#
# ticket customer change checks
#

View file

@ -1076,7 +1076,7 @@ wait untill text in selector disabppears
element.clear
# workaround, sometimes focus is not triggered
element.send_keys( data[:customer] )
element.send_keys( data[:customer] + '*' )
sleep 3.5
# check if pulldown is open, it's not working stable via selenium

63
test/fixtures/mail35.box vendored Normal file
View file

@ -0,0 +1,63 @@
From MAILER-DAEMON Wed Oct 21 14:42:20 2015
Return-Path: <>
X-Original-To: info@example.com
Delivered-To: znuny-sales@arber.example.com
Received-SPF: pass (emea01-am1-obe.outbound.protection.example.com: Sender is authorized to use 'emea01-am1-obe.outbound.protection.example.com' in 'helo' identity (mechanism 'include:spf.protection.example.com' matched)) receiver=arber.example.com; identity=helo; helo=emea01-am1-obe.outbound.protection.example.com; client-ip=7.5.1.1
Received: from emea01-am1-obe.outbound.protection.example.com (mail-am1hn0251.outbound.protection.example.com [7.5.1.1])
by arber.example.com (Postfix) with ESMTPS id C45775FE6A
for <info@example.com>; Wed, 21 Oct 2015 14:42:20 +0200 (CEST)
Received: from DB5PR07MB1224.eurprd07.example.com (10.164.41.30) by
DB5PR07MB1271.eurprd07.example.com (10.164.41.149) with Microsoft SMTP
Server (TLS) id 15.1.306.13; Wed, 21 Oct 2015 12:42:19 +0000
Authentication-Results: spf=none (sender IP is ) smtp.mailfrom=<>;
Received: from [10.254.48.3] (7.1.5.1) by
DB5PR07MB1224.eurprd07.example.com (10.164.41.30) with Microsoft SMTP
Server (TLS) id 15.1.306.13; Wed, 21 Oct 2015 12:42:17 +0000
Content-Type: text/plain; charset="iso-8859-1"
MIME-Version: 1.0
Content-Transfer-Encoding: quoted-printable
Content-Description: Mail message body
Subject: Darlehen bieten jetzt bewerben
To: Recipients
From: "finances8@firstfinanceloanfirm.example.com"
Date: Wed, 21 Oct 2015 13:42:12 +0100
Reply-To: <firstfinanceloanfirm@example.com>
X-Originating-IP: [7.1.5.1]
X-ClientProxiedBy: HE1PR08CA0021.eurprd08.example.com (2.1.1.3) To
DB5PR07MB1224.eurprd07.example.com (2.1.1.3)
Message-ID: <DB5PR07MB1224A6CA607D429AF81150DCB8380@DB5PR07MB1224.eurprd07.example.com>
X-Microsoft-Exchange-Diagnostics: 1;DB5PR07MB1224;2:DvCxn5dPPr2amttb4PujSx7+t6AMFJ+bMPumYN+Dk+H69oto3H01nPU6iR11JyZqjYuc39aPa1k5lilg1WbAYYC0kHdc2mKQP3cz6inS9RukNIIjp80dpFcfU8yflVZsNY8ZgQpWUUY7t8/8kVwNIk4irQFGZXQoXvabUNTR0WE=;3:cJIJTbFfruxjzzq+oDnnGOByaWjKlJGDX3cpo5L+mAR1hw2L5a0fZMkF3wYG+q+GZ8gm2Ylq6Mqhfe6fE0w4uQLvzgqAmKpB3fRRKpApA2W/raC1ervusTDeQp52bwLkuFDfafHeNQyk2ZKMsnFPdQ==;25:ZG++cyGnY1E1dIVYBdN/Zy/fWvaRwl1E1dSpIYrR18AaPp28qkBntNH1fJG8RZLm/ZyOXWGw9Yj6u9ycoyUSCUKmNWSPdSSUfbAoKlwBnZLbzpwmYToJzorzroT+EVXsCkCrGkfMfok+gjpl9H+9az4RQrW8rhwMhSIdA/Ilc3Kd+rNgBJ4sOSqGS7nTbtZHBbW81iXT++s4ab0Jh5KvMc43ue6tDVfHYc3rd1Trr7bBGV+iyE0wtgg164SEMp+3mOaFVMI6UmnL+IDj+bOZGA==
X-Microsoft-Antispam: UriScan:;BCL:0;PCL:0;RULEID:;SRVR:DB5PR07MB1224;
X-Microsoft-Antispam-PRVS: <DB5PR07MB1224BD6289731C40EBA17926B8380@DB5PR07MB1224.eurprd07.example.com>
X-Exchange-Antispam-Report-Test: UriScan:;
X-Exchange-Antispam-Report-CFA-Test: BCL:0;PCL:0;RULEID:(601004)(2401047)(5005006)(520078)(8121501046)(3002001)(102115026)(6004014)(6003046);SRVR:DB5PR07MB1224;BCL:0;PCL:0;RULEID:;SRVR:DB5PR07MB1224;
X-Microsoft-Exchange-Diagnostics: 1;DB5PR07MB1224;4:nnPVuJcrP/HGkCckgwKl7aJvC3EaZ0krj8ntX+WiSR1I+giYX9zPNwiki/7fIAWLYxxG0/aIQ//rReEVZrd/V2EH+PiNIDAHyVGNKBrGHB2R8P4vyh1fHBs8j1bEKNxn+t+4cFXEs7HYuSej9JY/BnQ34PnAsyViJtlWyibsUufjDNziP9JprRSgQf5zcSyffl73Ut0tY6pbX0v9ACVplnon07EhYYRfiBeu2cl6omrPINzMKDUt0BHunryPvXPOMl59CjTavddWiX0aJp/6ZwjF9R7nmgg9hS265qdEUOVhUPEe7cXjC8J3MLvq0auhgaJJVzNtmif56p4CW4eq2XMFbLthte3ORVmY9D8dhcR7tnHh/k9DLTx4zxUrFQWQPx86GNoo4mNssG4uGzUnHtetiDh9OtJbEL6s0aDZQVg=;23:0DFgc9QSh0ZIGritIv/KheEacJ7MAOinGXlKMABgvrIv9kljhHyju2F3owCE3OSUEedBv1vFu2s8OZnJ4m39lvMSxNIrow2MI29QxsoczYojmWHTECeAvkzJ4BYOhR4V0+iv1k1j4jDPFc9eVVY1Wel/ZJuS5DUIdNND9DnUwA2Zyzjm7ng7LF0znPz49lTbW/dkVCg6w4poryjMKWF3+xxT8Wefz7IonyAj+rI666JjaHVgk4puOoRAnDMHdvBF;5:P3QUjzpnvXNdNhdd5ZBd2CjBrl8LjhhxuAV/rMzKVdJCZh8FW6/ILeucXd8JU98DA8RrICLmdb1hbv2KBz4KexXoUD/VQYTn2qAjNqeIChjcjflvgsf6PwlPh4bs9HD+VK8NUSzGd21NkCznQFJaAQ==;24:JiXy13k+O7JUOG7IkzPwFd4RbRutN4QyGqlwL0SWbYsF86ynPrmE0/MLL0JCRqEzzED4KqdqJ2pQ1w86dG0EcA==
X-Forefront-PRVS: 073631BD3D
X-Forefront-Antispam-Report: SFV:SPM;SFS:(10019020)(6009001)(6049001)(5005620100007);DIR:OUT;SFP:1501;SCL:9;SRVR:DB5PR07MB1224;H:[10.254.48.3];FPR:;SPF:None;PTR:InfoNoRecords;LANG:de;
Received-SPF: None (protection.example.com: [10.254.48.3] does not designate
permitted sender hosts)
SpamDiagnosticOutput: 1:22
SpamDiagnosticMetadata: 00000000%2D0000%2D0000%2D0000%2D000000000000
X-Microsoft-Exchange-Diagnostics: 1;DB5PR07MB1224;20:zTAZboReXzFThEimKWUfvgFhjfgaw9a0rToyXCe6+Kb8dHZcQ9EjKmYZWkE62uOnvD4VLpAKakk1FJwRcDxBBA==
X-MS-Exchange-CrossTenant-OriginalArrivalTime: 21 Oct 2015 12:42:17.1900
(UTC)
X-MS-Exchange-CrossTenant-FromEntityHeader: Hosted
X-MS-Exchange-Transport-CrossTenantHeadersStamped: DB5PR07MB1224
X-Microsoft-Exchange-Diagnostics: 1;DB5PR07MB1271;2:5HuMfuoIxYZexWzVgqBch/cN+KXYALcB840unggr+hi7mPMTcPb63gD0Z0sgz1HuRne2t9tCnGlWIfcn7XCzXAAHvuIYuHjTHbFaj/WV0iy94Ehgo6XuM5GfqRlGTuUa/LyJi/BcfZ0jchcBrVjVt0Izn4+UB09P6yRq1/A0YjA=;23:mJyNLyB8E9W7POa18G8yfp1BVI8DgT6RzrItoW2V7KLBKMxiHx443g93/0YeXjBYWpeaIaMy5B9GA5i17vOeCKJZs+LimKbls83Ia+npZB7SXdJj6mBaWAdGwmW9lJ8ePnh1YjSS2oNXepT+uy7E6FZPxqWh3HDN8GJ8u/LJzupxeISrRds+T9crHSexnyVz
X-OriginatorOrg: firstfinanceloanfirm.example.com
X-UID: 3783
Status: RO
Content-Length: 397
Lines: 10
Beantworten :firstfinancelender@example.com
Ich Mr.Squires Peter ist eine zuverl=E4ssige Kreditangebot mit einer
Rate von 1.5% f=FCr den Zeitraum von 1 bis 40yrs nur, von der minimalen
von 5,000.00euro sie an die maximale Menge an 150,000.000.00euro, so
dass f=FCr mehr Details, wenn interessiert kontaktieren Sie mich unter
firstfinancelender@example.com
Beantworten :firstfinancelender@example.com

View file

@ -1953,6 +1953,30 @@ Some Text',
],
}
},
{
data: IO.read('test/fixtures/mail35.box'),
success: true,
result: {
0 => {
priority: '2 normal',
title: 'Darlehen bieten jetzt bewerben',
},
1 => {
sender: 'Customer',
type: 'email',
},
},
verify: {
users: [
{
firstname: '',
lastname: '',
fullname: '"finances8@firstfinanceloanfirm.example.com"',
email: '"finances8@firstfinanceloanfirm.example.com"',
},
],
}
},
]
process(files)
end