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

This commit is contained in:
rkaldung 2015-02-12 20:22:16 +01:00
commit 591c537325
96 changed files with 2198 additions and 676 deletions

View file

@ -206,14 +206,18 @@ class App.Controller extends Spine.Controller
update = =>
ui = @
$('.humanTimeFromNow').each( ->
item = $(this)
# console.log('rewrite frontendTimeUpdate', this, $(this).hasClass('escalation'))
timestamp = $(this).data('time')
time = ui.humanTime( timestamp, $(this).hasClass('escalation') )
$(this).attr( 'data-tooltip', App.i18n.translateTimestamp(timestamp) )
$(this).html( time )
ui.frontendTimeUpdateItem(item)
)
App.Interval.set( update, 30000, 'frontendTimeUpdate', 'ui' )
frontendTimeUpdateItem: (item) =>
timestamp = item.data('time')
time = @humanTime( timestamp, item.hasClass('escalation') )
item.attr( 'data-tooltip', App.i18n.translateTimestamp(timestamp) )
item.html( time )
ticketPopups: (position = 'right') ->
# open ticket in new task if curent user agent
@ -241,12 +245,17 @@ class App.Controller extends Spine.Controller
ticket = App.Ticket.fullLocal( ticket_id )
App.Utils.htmlEscape( ticket.title )
content: ->
ticket_id = $(@).data('id')
ticket = App.Ticket.fullLocal( ticket_id )
ticket.humanTime = ui.humanTime(ticket.created_at)
App.view('popover/ticket')(
ticket_id = $(@).data('id')
ticket = App.Ticket.fullLocal( ticket_id )
html = App.view('popover/ticket')(
ticket: ticket
)
html = $( html )
html.find('.humanTimeFromNow').each( ->
item = $(this)
ui.frontendTimeUpdateItem(item)
)
html
)
ticketPopupsDestroy: =>
@ -369,7 +378,7 @@ class App.Controller extends Spine.Controller
userTicketPopups: (params) ->
show = (data, tickets) =>
show = (data, ticket_list) =>
if !data.position
data.position = 'left'
@ -377,7 +386,7 @@ class App.Controller extends Spine.Controller
@userTicketPopupsDestroy()
# show user popup
controller = @
ui = @
@userTicketPopupsList = @el.find(data.selector).popover(
trigger: 'hover'
container: 'body'
@ -390,16 +399,21 @@ class App.Controller extends Spine.Controller
content: ->
type = $(@).filter('[data-type]').data('type')
data = tickets[type] || []
# set human time
for ticket in data
ticket.humanTime = controller.humanTime(ticket.created_at)
tickets = []
if ticket_list[type]
for ticket_id in ticket_list[type]
tickets.push App.Ticket.fullLocal( ticket_id )
# insert data
App.view('popover/user_ticket_list')(
tickets: data,
html = App.view('popover/user_ticket_list')(
tickets: tickets
)
html = $( html )
html.find('.humanTimeFromNow').each( ->
item = $(this)
ui.frontendTimeUpdateItem(item)
)
html
)
fetch = (params) =>
@ -411,14 +425,18 @@ class App.Controller extends Spine.Controller
}
processData: true,
success: (data, status, xhr) =>
App.Store.write( "user-ticket-popover::#{params.user_id}", data.tickets )
show( params, data.tickets )
App.Store.write( "user-ticket-popover::#{params.user_id}", data )
# load assets
App.Collection.loadAssets( data.assets )
show( params, { open: data.ticket_ids_open, closed: data.ticket_ids_closed } )
)
# get data
tickets = App.Store.get( "user-ticket-popover::#{params.user_id}" )
if tickets
show( params, tickets )
data = App.Store.get( "user-ticket-popover::#{params.user_id}" )
if data
show( params, { open: data.ticket_ids_open, closed: data.ticket_ids_closed } )
@delay(
=>
fetch(params)
@ -556,10 +574,10 @@ class App.ControllerModal extends App.Controller
@el.addClass('modal--local')
@el.modal
keyboard: @keyboard
show: true
backdrop: @backdrop
container: @container
keyboard: @keyboard
show: true
backdrop: @backdrop
container: @container
.on
'show.bs.modal': @onShow
'shown.bs.modal': @onShown

View file

@ -175,7 +175,7 @@ class App.ControllerForm extends App.Controller
###
formGenItem: (attribute_config, classname, form, attribute_count ) ->
attribute = clone( attribute_config )
attribute = clone( attribute_config, true )
# create item id
attribute.id = classname + '_' + attribute.name
@ -260,8 +260,8 @@ class App.ControllerForm extends App.Controller
# build options list
if _.isEmpty(attribute.options)
attribute.options = [
{ name: 'active', value: true }
{ name: 'inactive', value: false }
{ name: 'yes', value: true }
{ name: 'no', value: false }
]
# set data type
@ -276,6 +276,29 @@ class App.ControllerForm extends App.Controller
# return item
item = $( App.view('generic/select')( attribute: attribute ) )
else if attribute.tag is 'active'
# active attribute is always required
attribute.null = false
# build options list
attribute.options = [
{ name: 'active', value: true }
{ name: 'inactive', value: false }
]
# set data type
if attribute.name
attribute.name = '{boolean}' + attribute.name
# finde selected item of list
for record in attribute.options
if record.value is attribute.value
record.selected = 'selected'
# return item
item = $( App.view('generic/select')( attribute: attribute ) )
# select
else if attribute.tag is 'select'
item = $( App.view('generic/select')( attribute: attribute ) )
@ -320,12 +343,13 @@ class App.ControllerForm extends App.Controller
number
if !reset && (year isnt '' && month isnt '' && day isnt '')
time = new Date( Date.parse( "#{year}-#{format(month)}-#{format(day)}T00:00:00Z" ) )
time.setMinutes( time.getMinutes() + diff + time.getTimezoneOffset() )
else
time = new Date()
#time.setMinutes( time.getMinutes() + diff + time.getTimezoneOffset() )
item.closest('.form-group').find("[name=\"{date}#{name}___day\"]").val( time.getUTCDate() )
item.closest('.form-group').find("[name=\"{date}#{name}___month\"]").val( time.getUTCMonth()+1 )
item.closest('.form-group').find("[name=\"{date}#{name}___year\"]").val( time.getUTCFullYear() )
time.setMinutes( time.getMinutes() + diff )
item.closest('.form-group').find("[name=\"{date}#{name}___day\"]").val( time.getDate() )
item.closest('.form-group').find("[name=\"{date}#{name}___month\"]").val( time.getMonth()+1 )
item.closest('.form-group').find("[name=\"{date}#{name}___year\"]").val( time.getFullYear() )
item.find('.js-today').bind('click', (e) ->
e.preventDefault()
@ -462,9 +486,10 @@ class App.ControllerForm extends App.Controller
number
if !reset && (year isnt '' && month isnt '' && day isnt '' && hour isnt '' && day isnt '')
time = new Date( Date.parse( "#{year}-#{format(month)}-#{format(day)}T#{format(hour)}:#{format(minute)}:00Z" ) )
time.setMinutes( time.getMinutes() + diff + time.getTimezoneOffset() )
else
time = new Date()
time.setMinutes( time.getMinutes() + diff + time.getTimezoneOffset() )
time.setMinutes( time.getMinutes() + diff )
#console.log('T', time, time.getHours(), time.getMinutes())
item.closest('.form-group').find("[name=\"{datetime}#{name}___day\"]").val( time.getDate() )
item.closest('.form-group').find("[name=\"{datetime}#{name}___month\"]").val( time.getMonth()+1 )

View file

@ -2,15 +2,15 @@ class App.ControllerGenericNew extends App.ControllerModal
constructor: (params) ->
super
@head = App.i18n.translateContent( 'New' ) + ': ' + App.i18n.translateContent( @pageData.object )
@head = App.i18n.translateContent( 'New' ) + ': ' + App.i18n.translateContent( @pageData.object )
@cancel = true
@button = true
controller = new App.ControllerForm(
model: App[ @genericObject ]
params: @item
screen: @screen || 'edit'
autofocus: true
model: App[ @genericObject ]
params: @item
screen: @screen || 'edit'
autofocus: true
)
@content = controller.form
@ -98,8 +98,8 @@ class App.ControllerGenericEdit extends App.ControllerModal
class App.ControllerGenericIndex extends App.Controller
events:
'click [data-type=edit]': 'edit'
'click [data-type=new]': 'new'
'click [data-type = edit]': 'edit'
'click [data-type = new]': 'new'
constructor: ->
super
@ -160,6 +160,7 @@ class App.ControllerGenericIndex extends App.Controller
bindRow:
events:
'click': @edit
container: @container
},
@pageData.tableExtend
)
@ -177,6 +178,7 @@ class App.ControllerGenericIndex extends App.Controller
id: item.id
pageData: @pageData
genericObject: @genericObject
container: @container
)
new: (e) ->
@ -184,6 +186,7 @@ class App.ControllerGenericIndex extends App.Controller
new App.ControllerGenericNew(
pageData: @pageData
genericObject: @genericObject
container: @container
)
class App.ControllerGenericDestroyConfirm extends App.ControllerModal

View file

@ -82,7 +82,7 @@ class App.ControllerTable extends App.Controller
el: element
overview: ['time', 'area', 'level', 'browser', 'location', 'data']
attributes: [
{ name: 'time', display: 'Time', type: 'time' },
{ name: 'time', display: 'Time', tag: 'datetime' },
{ name: 'area', display: 'Area', type: 'text' },
{ name: 'level', display: 'Level', type: 'text' },
{ name: 'browser', display: 'Browser', type: 'text' },
@ -235,13 +235,14 @@ class App.ControllerTable extends App.Controller
# bind on delete dialog
if data.model && destroy
table.delegate('[data-type="destroy"]', 'click', (e) ->
table.delegate('[data-type="destroy"]', 'click', (e) =>
e.stopPropagation()
e.preventDefault()
itemId = $(e.target).parents('tr').data('id')
item = data.model.find(itemId)
new App.ControllerGenericDestroyConfirm(
item: item
item: item
container: @container
)
)

View file

@ -64,11 +64,16 @@ class App.ChannelEmailFilter extends App.Controller
new: (e) =>
e.preventDefault()
new App.ChannelEmailFilterEdit( {} )
new App.ChannelEmailFilterEdit(
container: @el.closest('.content')
)
edit: (id, e) =>
e.preventDefault()
new App.ChannelEmailFilterEdit( object: App.PostmasterFilter.find(id) )
new App.ChannelEmailFilterEdit(
object: App.PostmasterFilter.find(id)
container: @el.closest('.content')
)
class App.ChannelEmailFilterEdit extends App.ControllerModal
constructor: ->
@ -152,12 +157,17 @@ class App.ChannelEmailAddress extends App.Controller
new: (e) =>
e.preventDefault()
new App.ChannelEmailAddressEdit( {} )
new App.ChannelEmailAddressEdit(
container: @el.closest('.content')
)
edit: (id, e) =>
e.preventDefault()
item = App.EmailAddress.find(id)
new App.ChannelEmailAddressEdit( object: item )
new App.ChannelEmailAddressEdit(
object: item
container: @el.closest('.content')
)
class App.ChannelEmailAddressEdit extends App.ControllerModal
constructor: ->
@ -238,12 +248,17 @@ class App.ChannelEmailSignature extends App.Controller
new: (e) =>
e.preventDefault()
new App.ChannelEmailSignatureEdit( {} )
new App.ChannelEmailSignatureEdit(
container: @el.closest('.content')
)
edit: (id, e) =>
e.preventDefault()
item = App.Signature.find(id)
new App.ChannelEmailSignatureEdit( object: item )
new App.ChannelEmailSignatureEdit(
object: item
container: @el.closest('.content')
)
class App.ChannelEmailSignatureEdit extends App.ControllerModal
constructor: ->
@ -324,12 +339,17 @@ class App.ChannelEmailInbound extends App.Controller
new: (e) =>
e.preventDefault()
new App.ChannelEmailInboundEdit( {} )
new App.ChannelEmailInboundEdit(
container: @el.closest('.content')
)
edit: (id, e) =>
e.preventDefault()
item = App.Channel.find(id)
new App.ChannelEmailInboundEdit( object: item )
new App.ChannelEmailInboundEdit(
object: item
container: @el.closest('.content')
)
class App.ChannelEmailInboundEdit extends App.ControllerModal

View file

@ -2,9 +2,8 @@ App.Config.set( 'User', {
prio: 1000,
parent: '',
callback: ->
item = {}
item['name'] = App.Session.get( 'login' )
item['image'] = App.Session.get( 'imageUrl' )
item = {}
item['name'] = App.Session.get( 'login' )
if App.Session.get()
item['avatar'] = App.Session.get().avatar()
return item

View file

@ -473,8 +473,8 @@ class Sidebar extends App.Controller
)
new App.WidgetUser(
el: el
user_id: user.id
el: el
user_id: user.id
)
editCustomer = (e, el) =>
@ -483,9 +483,10 @@ class Sidebar extends App.Controller
genericObject: 'User'
screen: 'edit'
pageData:
title: 'Users'
object: 'User'
title: 'Users'
object: 'User'
objects: 'Users'
container: @el.closest('.content')
)
items.push {
head: 'Customer'
@ -493,9 +494,9 @@ class Sidebar extends App.Controller
icon: 'person'
actions: [
{
title: 'Edit Customer'
name: 'Edit Customer'
class: 'glyphicon glyphicon-edit'
title: 'Edit Customer'
name: 'Edit Customer'
class: 'glyphicon glyphicon-edit'
callback: editCustomer
},
]
@ -508,14 +509,15 @@ class Sidebar extends App.Controller
id: user.organization_id
genericObject: 'Organization'
pageData:
title: 'Organizations'
object: 'Organization'
title: 'Organizations'
object: 'Organization'
objects: 'Organizations'
container: @el.closest('.content')
)
showOrganization = (el) =>
new App.WidgetOrganization(
el: el
organization_id: user.organization_id
el: el
organization_id: user.organization_id
)
items.push {
head: 'Organization'
@ -523,6 +525,7 @@ class Sidebar extends App.Controller
icon: 'group'
actions: [
{
title: 'Edit Organization'
name: 'Edit Organization'
class: 'glyphicon glyphicon-edit'
callback: editOrganization

View file

@ -6,22 +6,22 @@ class Index extends App.ControllerContent
return if !@authenticate()
new App.ControllerGenericIndex(
el: @el,
id: @id,
genericObject: 'Group',
pageData: {
title: 'Groups',
home: 'groups',
object: 'Group',
objects: 'Groups',
navupdate: '#groups',
notes: [
el: @el
id: @id
genericObject: 'Group'
pageData:
title: 'Groups'
home: 'groups'
object: 'Group'
objects: 'Groups'
navupdate: '#groups'
notes: [
'Groups are ...'
],
]
buttons: [
{ name: 'New Group', 'data-type': 'new', class: 'btn--success' },
],
},
]
container: @el.closest('.content')
)
App.Config.set( 'Group', { prio: 1500, name: 'Groups', parent: '#manage', target: '#manage/groups', controller: Index, role: ['Admin'] }, 'NavBarAdmin' )

View file

@ -15,11 +15,11 @@ class Index extends App.ControllerContent
e.preventDefault()
params = @formParam(e.target)
App.Event.trigger(
'ws:send'
action: 'broadcast'
event: 'session:maintenance'
spool: false
data: params
'ws:send'
action: 'broadcast'
event: 'session:maintenance'
spool: false
data: params
)
@notify
type: 'success'

View file

@ -156,11 +156,8 @@ class App.Navigation extends App.Controller
area.result = []
for id in area.ids
ticket = App.Ticket.find( id )
ticket.humanTime = @humanTime(ticket.created_at)
data =
display: "##{ticket.number} - #{ticket.title}"
createt_at: "#{ticket.created_at}"
humanTime: "#{ticket.humanTime}"
id: ticket.id
class: "task level-1 ticket-popover"
url: ticket.uiUrl()

View file

@ -7,10 +7,10 @@ class Index extends App.ControllerTabs
# get data
@ajax(
id: 'object_manager_attributes_list',
type: 'GET',
url: @apiPath + '/object_manager_attributes_list',
processData: true,
id: 'object_manager_attributes_list'
type: 'GET'
url: @apiPath + '/object_manager_attributes_list'
processData: true
success: (data, status, xhr) =>
@build(data.objects)
)
@ -19,9 +19,9 @@ class Index extends App.ControllerTabs
@tabs = []
for object in objects
item =
name: object,
target: "c-#{object}",
controller: Items,
name: object
target: "c-#{object}"
controller: Items
params:
object: object
@tabs.push item
@ -120,18 +120,19 @@ class Items extends App.ControllerContent
objects: 'ObjectManagerAttributes'
navupdate: '#object_manager'
genericObject: 'ObjectManagerAttribute'
container: @el.closest('.content')
)
edit: (e) =>
e.preventDefault()
id = $( e.target ).closest('tr').data('id')
new Edit(
pageData: {
pageData:
object: 'ObjectManagerAttribute'
},
genericObject: 'ObjectManagerAttribute'
callback: @render
callback: @render
id: id
container: @el.closest('.content')
)
destroy: (e) ->
@ -158,7 +159,6 @@ class Edit extends App.ControllerModal
items: []
) )
item = App.ObjectManagerAttribute.find(@id)
options =
@ -249,9 +249,8 @@ class Edit extends App.ControllerModal
@content.find('[name=data_type]').trigger('change')
configureAttributesBottom = [
{ name: 'active', display: 'Active', tag: 'boolean', 'default': true, 'null': false },
{ name: 'active', display: 'Active', tag: 'active', default: true },
]
controller = new App.ControllerForm(
model: { configure_attributes: configureAttributesBottom, className: '' },
@ -263,8 +262,6 @@ class Edit extends App.ControllerModal
#@content = controller.form
#@show(content)
@show()

View file

@ -7,9 +7,9 @@ class App.OrganizationHistory extends App.GenericHistory
# get data
@ajax(
id: 'organization_history',
type: 'GET',
url: @apiPath + '/organizations/history/' + @organization_id,
id: 'organization_history'
type: 'GET'
url: @apiPath + '/organizations/history/' + @organization_id
success: (data, status, xhr) =>
# load assets

View file

@ -1,7 +1,4 @@
class App.OrganizationProfile extends App.Controller
events:
'focusout [contenteditable]': 'update'
constructor: (params) ->
super
@ -12,11 +9,8 @@ class App.OrganizationProfile extends App.Controller
@navupdate '#'
# subscribe and reload data / fetch new data if triggered
@subscribeId = App.Organization.full( @organization_id, @render, false, true )
release: =>
App.Organization.unsubscribe(@subscribeId)
# fetch new data if needed
App.Organization.full( @organization_id, @render )
meta: =>
meta =
@ -48,6 +42,39 @@ class App.OrganizationProfile extends App.Controller
@doNotLog = 1
@recentView( 'Organization', @organization_id )
@html App.view('organization_profile/index')(
organization: organization
)
new Object(
el: @$('.js-object-container')
organization: organization
)
new App.TicketStats(
el: @$('.js-ticket-stats')
organization: organization
)
new App.UpdateTastbar(
genericObject: organization
)
class Object extends App.Controller
events:
'focusout [contenteditable]': 'update'
constructor: (params) ->
super
# subscribe and reload data / fetch new data if triggered
@subscribeId = App.Organization.full( @organization.id, @render, false, true )
release: =>
App.Organization.unsubscribe(@subscribeId)
render: (organization) =>
# get display data
organizationData = []
for attributeName, attributeConfig of App.Organization.attributesGet('view')
@ -65,7 +92,7 @@ class App.OrganizationProfile extends App.Controller
if name isnt 'name'
organizationData.push attributeConfig
@html App.view('organization_profile')(
@html App.view('organization_profile/object')(
organization: organization
organizationData: organizationData
)
@ -76,15 +103,6 @@ class App.OrganizationProfile extends App.Controller
maxlength: 250
})
new App.TicketStats(
el: @$('.js-ticket-stats')
organization: organization
)
new App.UpdateTastbar(
genericObject: organization
)
# start action controller
showHistory = =>
new App.OrganizationHistory( organization_id: organization.id )
@ -97,6 +115,7 @@ class App.OrganizationProfile extends App.Controller
title: 'Organizations'
object: 'Organization'
objects: 'Organizations'
container: @el.closest('.content')
)
actions = [
@ -120,13 +139,14 @@ class App.OrganizationProfile extends App.Controller
update: (e) =>
name = $(e.target).attr('data-name')
value = $(e.target).html()
org = App.Organization.find( @organization_id )
org = App.Organization.find( @organization.id )
if org[name] isnt value
data = {}
data[name] = value
org.updateAttributes( data )
@log 'notice', 'update', name, value, org
class Router extends App.ControllerPermanent
constructor: (params) ->
super

View file

@ -6,22 +6,22 @@ class Index extends App.ControllerContent
return if !@authenticate()
new App.ControllerGenericIndex(
el: @el,
id: @id,
genericObject: 'Organization',
pageData: {
title: 'Organizations',
home: 'organizations',
object: 'Organization',
objects: 'Organizations',
navupdate: '#organizations',
el: @el
id: @id
genericObject: 'Organization'
pageData:
title: 'Organizations'
home: 'organizations'
object: 'Organization'
objects: 'Organizations'
navupdate: '#organizations'
notes: [
'Organizations are for any person in the system. Agents (Owners, Resposbiles, ...) and Customers.'
],
]
buttons: [
{ name: 'New Organization', 'data-type': 'new', class: 'btn--success' },
],
},
{ name: 'New Organization', 'data-type': 'new', class: 'btn--success' }
]
container: @el.closest('.content')
)
App.Config.set( 'Organization', { prio: 2000, name: 'Organizations', parent: '#manage', target: '#manage/organizations', controller: Index, role: ['Admin'] }, 'NavBarAdmin' )

View file

@ -6,22 +6,22 @@ class Index extends App.ControllerContent
return if !@authenticate()
new App.ControllerGenericIndex(
el: @el,
id: @id,
genericObject: 'Overview',
pageData: {
title: 'Overviews',
home: 'overviews',
object: 'Overview',
objects: 'Overviews',
navupdate: '#overviews',
el: @el
id: @id
genericObject: 'Overview'
pageData:
title: 'Overviews'
home: 'overviews'
object: 'Overview'
objects: 'Overviews'
navupdate: '#overviews'
notes: [
'Overview are ...'
],
]
buttons: [
{ name: 'New Overview', 'data-type': 'new', class: 'btn--success' },
],
},
{ name: 'New Overview', 'data-type': 'new', class: 'btn--success' }
]
container: @el.closest('.content')
)
App.Config.set( 'Overview', { prio: 2300, name: 'Overviews', parent: '#manage', target: '#manage/overviews', controller: Index, role: ['Admin'] }, 'NavBarAdmin' )

View file

@ -6,22 +6,22 @@ class Index extends App.ControllerContent
return if !@authenticate()
new App.ControllerGenericIndex(
el: @el,
id: @id,
genericObject: 'Job',
pageData: {
title: 'Schedulers',
home: 'schedulers',
object: 'Scheduler',
objects: 'Schedulers',
navupdate: '#schedulers',
el: @el
id: @id
genericObject: 'Job'
pageData:
title: 'Schedulers'
home: 'schedulers'
object: 'Scheduler'
objects: 'Schedulers'
navupdate: '#schedulers'
notes: [
'Scheduler are ...'
],
]
buttons: [
{ name: 'New Scheduler', 'data-type': 'new', class: 'btn--success' },
],
},
{ name: 'New Scheduler', 'data-type': 'new', class: 'btn--success' }
]
container: @el.closest('.content')
)
App.Config.set( 'Scheduler', { prio: 3000, name: 'Schedulers', parent: '#manage', target: '#manage/schedulers', controller: Index, role: ['Admin'] }, 'NavBarAdmin' )

View file

@ -6,22 +6,22 @@ class Index extends App.ControllerContent
return if !@authenticate()
new App.ControllerGenericIndex(
el: @el,
id: @id,
genericObject: 'Sla',
pageData: {
title: 'SLA',
home: 'slas',
object: 'SLA',
objects: 'SLAs',
navupdate: '#slas',
el: @el
id: @id
genericObject: 'Sla'
pageData:
title: 'SLA'
home: 'slas'
object: 'SLA'
objects: 'SLAs'
navupdate: '#slas'
notes: [
# 'SLA are ...'
],
]
buttons: [
{ name: 'New SLA', 'data-type': 'new', class: 'btn--success' },
],
},
{ name: 'New SLA', 'data-type': 'new', class: 'btn--success' }
]
container: @el.closest('.content')
)
App.Config.set( 'Sla', { prio: 2900, name: 'SLAs', parent: '#manage', target: '#manage/slas', controller: Index, role: ['Admin'] }, 'NavBarAdmin' )

View file

@ -6,22 +6,22 @@ class Index extends App.ControllerContent
return if !@authenticate()
new App.ControllerGenericIndex(
el: @el,
id: @id,
genericObject: 'TextModule',
pageData: {
title: 'TextModules',
home: 'text_modules',
object: 'TextModule',
objects: 'TextModules',
navupdate: '#text_modules',
el: @el
id: @id
genericObject: 'TextModule'
pageData:
title: 'TextModules'
home: 'text_modules'
object: 'TextModule'
objects: 'TextModules'
navupdate: '#text_modules'
notes: [
'TextModules are ...'
],
]
buttons: [
{ name: 'New TextModule', 'data-type': 'new', class: 'btn--success' },
],
},
{ name: 'New TextModule', 'data-type': 'new', class: 'btn--success' }
]
container: @el.closest('.content')
)
App.Config.set( 'TextModule', { prio: 2300, name: 'TextModules', parent: '#manage', target: '#manage/text_modules', controller: Index, role: ['Admin'] }, 'NavBarAdmin' )

View file

@ -482,7 +482,7 @@ class Table extends App.ControllerContent
new App.OverviewSettings(
overview_id: @overview.id
view_mode: @view_mode
container: @el
container: @el.closest('.content')
)
class App.OverviewSettings extends App.ControllerModal

View file

@ -124,17 +124,27 @@ class App.TicketZoom extends App.Controller
@doNotLog = 1
@recentView( 'Ticket', ticket_id )
error: (xhr, status, error) =>
error: (xhr) =>
statusText = xhr.statusText
status = xhr.status
detail = xhr.responseText
#console.log('error', status, statusText)
# do not close window if request is aborted
return if status is 'abort'
# ignore if request is aborted
if statusText is 'abort'
return
# if ticket is already loaded, ignore status "0" - network issues e. g. temp. not connection
if @ticketUpdatedAtLastCall && status is 0
console.log('network issues e. g. temp. not connection', status, statusText, detail)
return
# show error message
if xhr.status is 401 || error is 'Unauthorized'
if status is 401 || statusText is 'Unauthorized'
@taskHead = '» ' + App.i18n.translateInline('Unauthorized') + ' «'
@taskIconClass = 'error'
@html App.view('generic/error/unauthorized')( objectName: 'Ticket' )
else if xhr.status is 404 || error is 'Not Found'
else if status is 404 || statusText is 'Not Found'
@taskHead = '» ' + App.i18n.translateInline('Not Found') + ' «'
@taskIconClass = 'error'
@html App.view('generic/error/not_found')( objectName: 'Ticket' )
@ -142,9 +152,7 @@ class App.TicketZoom extends App.Controller
@taskHead = '» ' + App.i18n.translateInline('Error') + ' «'
@taskIconClass = 'error'
status = xhr.status
detail = xhr.responseText
if !status && !detail
if !detail
detail = 'General communication error, maybe internet is not available!'
@html App.view('generic/error/generic')(
status: status
@ -321,21 +329,26 @@ class App.TicketZoom extends App.Controller
)
showTicketHistory = =>
new App.TicketHistory( ticket_id: @ticket.id )
new App.TicketHistory(
ticket_id: @ticket.id
container: @el.closest('.content')
)
showTicketMerge = =>
new App.TicketMerge
ticket: @ticket
task_key: @task_key
container: @el
new App.TicketMerge(
ticket: @ticket
task_key: @task_key
container: @el.closest('.content')
)
changeCustomer = (e, el) =>
new App.TicketCustomer(
ticket: @ticket
ticket: @ticket
container: @el.closest('.content')
)
items = [
{
head: 'Ticket'
name: 'ticket'
icon: 'message'
head: 'Ticket'
name: 'ticket'
icon: 'message'
callback: editTicket
}
]
@ -367,6 +380,7 @@ class App.TicketZoom extends App.Controller
title: 'Users'
object: 'User'
objects: 'Users'
container: @el.closest('.content')
)
showCustomer = (el) =>
new App.WidgetUser(
@ -374,9 +388,9 @@ class App.TicketZoom extends App.Controller
user_id: @ticket.customer_id
)
items.push {
head: 'Customer'
name: 'customer'
icon: 'person'
head: 'Customer'
name: 'customer'
icon: 'person'
actions: [
{
title: 'Change Customer'
@ -400,6 +414,7 @@ class App.TicketZoom extends App.Controller
title: 'Organizations'
object: 'Organization'
objects: 'Organizations'
container: @el.closest('.content')
)
showOrganization = (el) =>
new App.WidgetOrganization(
@ -421,8 +436,8 @@ class App.TicketZoom extends App.Controller
}
new App.Sidebar(
el: @el.find('.tabsSidebar')
items: items
el: @el.find('.tabsSidebar')
items: items
)
# show article

View file

@ -1,7 +1,4 @@
class App.UserProfile extends App.Controller
events:
'focusout [contenteditable]': 'update'
constructor: (params) ->
super
@ -12,8 +9,8 @@ class App.UserProfile extends App.Controller
@navupdate '#'
# subscribe and reload data / fetch new data if triggered
@subscribeId = App.User.full( @user_id, @render, false, true )
# fetch new data if needed
@subscribeId = App.User.full( @user_id, @render )
release: =>
App.User.unsubscribe(@subscribeId)
@ -47,6 +44,40 @@ class App.UserProfile extends App.Controller
@doNotLog = 1
@recentView( 'User', @user_id )
@html App.view('user_profile/index')(
user: user
)
new Object(
el: @$('.js-object-container')
user: user
)
new App.TicketStats(
el: @$('.js-ticket-stats')
user: user
)
new App.UpdateTastbar(
genericObject: user
)
class Object extends App.Controller
events:
'focusout [contenteditable]': 'update'
constructor: (params) ->
super
# subscribe and reload data / fetch new data if triggered
@subscribeId = App.User.full( @user.id, @render, false, true )
release: =>
App.User.unsubscribe(@subscribeId)
render: (user) =>
# get display data
userData = []
for attributeName, attributeConfig of App.User.attributesGet('view')
@ -64,7 +95,7 @@ class App.UserProfile extends App.Controller
if name isnt 'firstname' && name isnt 'lastname' && name isnt 'organization'
userData.push attributeConfig
@html App.view('user_profile')(
@html App.view('user_profile/object')(
user: user
userData: userData
)
@ -75,15 +106,6 @@ class App.UserProfile extends App.Controller
maxlength: 250
})
new App.TicketStats(
el: @$('.js-ticket-stats')
user: user
)
new App.UpdateTastbar(
genericObject: user
)
# start action controller
showHistory = =>
new App.UserHistory( user_id: user.id )
@ -97,6 +119,7 @@ class App.UserProfile extends App.Controller
title: 'Users'
object: 'User'
objects: 'Users'
container: @el.closest('.content')
)
actions = [
@ -120,7 +143,7 @@ class App.UserProfile extends App.Controller
update: (e) =>
name = $(e.target).attr('data-name')
value = $(e.target).html()
user = App.User.find( @user_id )
user = App.User.find( @user.id )
if user[name] isnt value
data = {}
data[name] = value

View file

@ -10,29 +10,117 @@
#= require_tree ./lib/app_post
class App extends Spine.Controller
@viewPrint: (object, attribute_name) ->
attributes = {}
if object.constructor.attributesGet
attributes = object.constructor.attributesGet()
attribute_config = attributes[attribute_name]
value = object[attribute_name]
valueRef = undefined
# check if relation is requested
if !attribute_config
attribute_name_new = "#{attribute_name}_id"
attribute_config = attributes[attribute_name_new]
if attribute_config
attribute_name = attribute_name_new
if object[attribute_name]
valueRef = value
value = object[attribute_name]
# in case of :: key, get the sub value
if !value
parts = attribute_name.split('::')
if parts[0] && parts[1] && object[ parts[0] ]
value = object[ parts[0] ][ parts[1] ]
#console.log('Pa', attribute_name, object, attribute_config, object[attribute_name], valueRef, value)
# if we have no config, get output this way
if !attribute_config
return @viewPrintItem( value )
# check if valueRef already exists, no lookup needed later
if !valueRef
attribute_name_without_ref = attribute_name.substr(attribute_name.length-3, attribute_name.length)
if attribute_name_without_ref is '_id'
attribute_name_without_ref = attribute_name.substr(0, attribute_name.length-3)
if object[attribute_name_without_ref]
valueRef = object[attribute_name_without_ref]
return @viewPrintItem( value, attribute_config, valueRef )
# define print name helper
@viewPrintItem: ( item, attribute_config = {}, valueRef ) ->
return '-' if item is undefined
return '-' if item is ''
return item if !item
result = item
# lookup relation
if attribute_config.relation || valueRef
if valueRef
item = valueRef
else
item = App[attribute_config.relation].find(item)
# if date is a object, get name of the object
isObject = false
if typeof item is 'object'
isObject = true
if item.displayNameLong
result = item.displayNameLong()
else if item.displayName
result = item.displayName()
else
result = item.name
# execute callback on content
if attribute_config.callback
result = attribute_config.callback( result, attribute_config )
# text2html in textarea view
isHtmlEscape = false
if attribute_config.tag is 'textarea'
isHtmlEscape = true
result = App.Utils.text2html( result )
# remember, html snippets are already escaped
else if attribute_config.tag is 'richtext'
isHtmlEscape = true
# fillup options
if !_.isEmpty(attribute_config.options)
if attribute_config.options[result]
result = attribute_config.options[result]
# translate content
if attribute_config.translate || ( isObject && item.translate && item.translate() )
isHtmlEscape = true
result = App.i18n.translateContent( result )
# transform date
if attribute_config.tag is 'date'
isHtmlEscape = true
result = App.i18n.translateDate(result)
# use pretty time for datetime
else if attribute_config.tag is 'datetime'
isHtmlEscape = true
result = "<span class=\"humanTimeFromNow #{attribute_config.class}\" data-time=\"#{result}\">?</span>"
#result = App.i18n.translateTimestamp(result)
if !isHtmlEscape && typeof result is 'string'
result = App.Utils.htmlEscape(result)
result
@view: (name) ->
template = ( params = {} ) =>
# define print name helper
params.P = ( item, row = {} ) ->
return '-' if item is undefined
return '-' if item is ''
return item if !item
# if date is a object, get name of the object
if typeof item is 'object'
if item.displayNameLong
return item.displayNameLong()
else if item.displayName
return item.displayName()
return item.name
# execute callback on content
if row.callback
return row.callback( item, row )
# return raw data
item
params.P = ( object, attribute_name ) ->
App.viewPrint( object, attribute_name )
# define date format helper
params.date = ( time ) ->

View file

@ -30,7 +30,7 @@ class App.Browser
# define min. required browser version
map =
Chrome2: 37
Firefox: 28
Firefox: 31
Explorer: 10
Safari: 6
Opera: 22

View file

@ -276,7 +276,8 @@ class App.UserOrganizationAutocompletion extends App.Controller
if e
e.preventDefault()
new UserNew(
parent: @
parent: @
container: @el.closest('.content')
)
class UserNew extends App.ControllerModal

View file

@ -4,11 +4,11 @@ class App.EmailAddress extends App.Model
@url: @apiPath + '/email_addresses'
@configure_attributes = [
{ name: 'realname', display: 'Realname', tag: 'input', type: 'text', limit: 250, 'null': false, 'class': 'span4' },
{ name: 'email', display: 'Email', tag: 'input', type: 'text', limit: 250, 'null': false, 'class': 'span4' },
{ name: 'note', display: 'Note', tag: 'textarea', note: 'Notes are visible to agents only, never to customers.', limit: 250, 'null': true, 'class': 'span4' },
{ name: 'updated_at', display: 'Updated', type: 'time', readonly: 1 },
{ name: 'active', display: 'Active', tag: 'boolean', type: 'boolean', 'default': true, 'null': false, 'class': 'span4' },
{ name: 'realname', display: 'Realname', tag: 'input', type: 'text', limit: 250, null: false },
{ name: 'email', display: 'Email', tag: 'input', type: 'text', limit: 250, null: false },
{ name: 'note', display: 'Note', tag: 'textarea', note: 'Notes are visible to agents only, never to customers.', limit: 250, null: true },
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },
{ name: 'active', display: 'Active', tag: 'active', default: true },
]
@configure_overview = [
'realname', 'email'

View file

@ -4,15 +4,15 @@ class App.Group extends App.Model
@url: @apiPath + '/groups'
@configure_attributes = [
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, 'null': false, 'class': 'span4' },
{ name: 'assignment_timeout', display: 'Assignment Timeout', tag: 'input', note: 'Assignment timeout in minutes if assigned agent is not working on it. Ticket will be shown as unassigend.', type: 'text', limit: 100, 'null': true, 'class': 'span4' },
{ name: 'follow_up_possible', display: 'Follow up possible',tag: 'select', default: 'yes', options: { yes: 'yes', reject: 'reject follow up/do not reopen Ticket', 'new_ticket': 'do not reopen Ticket but create new Ticket' }, 'null': false, note: 'Follow up for closed ticket possible or not.', 'class': 'span4' },
{ name: 'follow_up_assignment', display: 'Assign Follow Ups', tag: 'select', default: 'yes', options: { true: 'yes', false: 'no' }, 'null': false, note: 'Assign follow up to latest agent again.', 'class': 'span4' },
{ name: 'email_address_id', display: 'Email', tag: 'select', multiple: false, null: true, relation: 'EmailAddress', nulloption: true, class: 'span4' },
{ name: 'signature_id', display: 'Signature', tag: 'select', multiple: false, null: true, relation: 'Signature', nulloption: true, class: 'span4' },
{ name: 'note', display: 'Note', tag: 'textarea', note: 'Notes are visible to agents only, never to customers.', limit: 250, 'null': true, 'class': 'span4' },
{ name: 'updated_at', display: 'Updated', type: 'time', readonly: 1 },
{ name: 'active', display: 'Active', tag: 'boolean', type: 'boolean', 'default': true, 'null': false, 'class': 'span4' },
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false },
{ name: 'assignment_timeout', display: 'Assignment Timeout', tag: 'input', note: 'Assignment timeout in minutes if assigned agent is not working on it. Ticket will be shown as unassigend.', type: 'text', limit: 100, null: true },
{ name: 'follow_up_possible', display: 'Follow up possible',tag: 'select', default: 'yes', options: { yes: 'yes', reject: 'reject follow up/do not reopen Ticket', 'new_ticket': 'do not reopen Ticket but create new Ticket' }, null: false, note: 'Follow up for closed ticket possible or not.' },
{ name: 'follow_up_assignment', display: 'Assign Follow Ups', tag: 'select', default: 'yes', options: { true: 'yes', false: 'no' }, 'null': false, note: 'Assign follow up to latest agent again.' },
{ name: 'email_address_id', display: 'Email', tag: 'select', multiple: false, null: true, relation: 'EmailAddress', nulloption: true },
{ name: 'signature_id', display: 'Signature', tag: 'select', multiple: false, null: true, relation: 'Signature', nulloption: true },
{ name: 'note', display: 'Note', tag: 'textarea', note: 'Notes are visible to agents only, never to customers.', limit: 250, null: true },
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },
{ name: 'active', display: 'Active', tag: 'active', default: true },
]
@configure_overview = [
'name',

View file

@ -8,15 +8,15 @@ class App.Job extends App.Model
{ name: 'condition', display: 'Conditions for matching objects.', tag: 'ticket_attribute_selection', null: true },
{ name: 'execute', display: 'Execute changes on objects.', tag: 'ticket_attribute_set', null: true },
{ name: 'note', display: 'Note', tag: 'textarea', note: 'Notes are visible to agents only, never to customers.', limit: 250, null: true },
{ name: 'active', display: 'Active', tag: 'boolean', note: 'boolean', 'default': true, null: false },
{ name: 'active', display: 'Active', tag: 'active', default: true },
{ name: 'matching', display: 'Matching', readonly: 1 },
{ name: 'processed', display: 'Processed', readonly: 1 },
{ name: 'last_run_at', display: 'Last run', type: 'time', readonly: 1 },
{ name: 'processed', display: 'Processed', readonly: 1 },
{ name: 'last_run_at', display: 'Last run', tag: 'datetime', readonly: 1 },
{ name: 'running', display: 'Running', tag: 'boolean', readonly: 1 },
{ name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 },
{ name: 'created_at', display: 'Created', type: 'time', readonly: 1 },
{ name: 'created_at', display: 'Created', tag: 'datetime', readonly: 1 },
{ name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 },
{ name: 'updated_at', display: 'Updated', type: 'time', readonly: 1 },
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },
]
@configure_delete = true
@configure_overview = [

View file

@ -2,8 +2,8 @@ class App.Network extends App.Model
@configure 'Network', 'name', 'note', 'active', 'updated_at'
@extend Spine.Model.Ajax
@configure_attributes = [
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, 'null': false, 'class': 'xlarge' },
{ name: 'note', display: 'Note', note: 'Notes are visible to agents only, never to customers.', tag: 'textarea', limit: 250, 'null': true, 'class': 'xlarge' },
{ name: 'updated_at', display: 'Updated', type: 'time', readonly: 1 },
{ name: 'active', display: 'Active', tag: 'boolean', type: 'boolean', 'default': true, 'null': false, 'class': 'xlarge' },
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false },
{ name: 'note', display: 'Note', note: 'Notes are visible to agents only, never to customers.', tag: 'textarea', limit: 250, null: true },
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },
{ name: 'active', display: 'Active', tag: 'active', default: true },
]

View file

@ -3,13 +3,13 @@ class App.ObjectManagerAttribute extends App.Model
@extend Spine.Model.Ajax
@url: @apiPath + '/object_manager_attributes'
@configure_attributes = [
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, 'null': false },
{ name: 'display', display: 'Anzeige', tag: 'input', type: 'text', limit: 100, 'null': false },
{ name: 'object', display: 'Object', tag: 'input', readonly: 1 },
{ name: 'position', display: 'Position', tag: 'input', readonly: 1 },
{ name: 'active', display: 'Active', tag: 'boolean', 'default': true, 'null': false },
{ name: 'data_type', display: 'Format', tag: 'input', type: 'text', limit: 100, 'null': false },
{ name: 'updated_at', display: 'Updated', type: 'time', readonly: 1 },
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false },
{ name: 'display', display: 'Anzeige', tag: 'input', type: 'text', limit: 100, null: false },
{ name: 'object', display: 'Object', tag: 'input', readonly: 1 },
{ name: 'position', display: 'Position', tag: 'input', readonly: 1 },
{ name: 'active', display: 'Active', tag: 'active', default: true },
{ name: 'data_type', display: 'Format', tag: 'input', type: 'text', limit: 100, null: false },
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },
]
@configure_overview = [
#'name',

View file

@ -3,11 +3,11 @@ class App.Organization extends App.Model
@extend Spine.Model.Ajax
@url: @apiPath + '/organizations'
@configure_attributes = [
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, 'null': false, info: true },
{ name: 'shared', display: 'Shared organization', tag: 'boolean', note: 'Customers in the organization can view each other items.', type: 'boolean', 'default': true, 'null': false, info: false },
{ name: 'note', display: 'Note', tag: 'textarea', note: 'Notes are visible to agents only, never to customers.', limit: 250, 'null': true, info: true },
{ name: 'updated_at', display: 'Updated', type: 'time', readonly: 1, info: false },
{ name: 'active', display: 'Active', tag: 'boolean', 'default': true, 'null': false, info: false },
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false, info: true },
{ name: 'shared', display: 'Shared organization', tag: 'boolean', note: 'Customers in the organization can view each other items.', type: 'boolean', default: true, null: false, info: false },
{ name: 'note', display: 'Note', tag: 'textarea', note: 'Notes are visible to agents only, never to customers.', limit: 250, null: true, info: true },
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1, info: false },
{ name: 'active', display: 'Active', tag: 'active', default: true, info: false },
]
@configure_overview = [
'name',

View file

@ -131,11 +131,11 @@ class App.Overview extends App.Model
owner: 'Owner'
class: 'span4'
},
{ name: 'active', display: 'Active', tag: 'boolean', note: 'boolean', 'default': true, 'null': false, 'class': 'span4' },
{ name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 },
{ name: 'created_at', display: 'Created', type: 'time', readonly: 1 },
{ name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 },
{ name: 'updated_at', display: 'Updated', type: 'time', readonly: 1 },
{ name: 'active', display: 'Active', tag: 'active', default: true },
{ name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 },
{ name: 'created_at', display: 'Created', tag: 'datetime', readonly: 1 },
{ name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 },
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },
]
@configure_delete = true
@configure_overview = [

View file

@ -8,13 +8,13 @@ class App.PostmasterFilter extends App.Model
{ name: 'channel', display: 'Channel', type: 'input', readonly: 1 },
{ name: 'match', display: 'Match all of the following', tag: 'postmaster_match' },
{ name: 'perform', display: 'Perform action of the following', tag: 'postmaster_set' },
{ name: 'note', display: 'Note', tag: 'textarea', note: 'Notes are visible to agents only, never to customers.', limit: 250, 'null': true },
{ name: 'updated_at', display: 'Updated', type: 'time', readonly: 1 },
{ name: 'active', display: 'Active', tag: 'boolean', type: 'boolean', 'default': true, 'null': false },
{ name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 },
{ name: 'created_at', display: 'Created', type: 'time', readonly: 1 },
{ name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 },
{ name: 'updated_at', display: 'Updated', type: 'time', readonly: 1 },
{ name: 'note', display: 'Note', tag: 'textarea', note: 'Notes are visible to agents only, never to customers.', limit: 250, null: true },
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },
{ name: 'active', display: 'Active', tag: 'active', default: true },
{ name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 },
{ name: 'created_at', display: 'Created', tag: 'datetime', readonly: 1 },
{ name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 },
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },
]
@configure_delete = true
@configure_overview = [

View file

@ -3,13 +3,13 @@ class App.Role extends App.Model
@extend Spine.Model.Ajax
@url: @apiPath + '/roles'
@configure_attributes = [
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false },
{ name: 'note', display: 'Note', tag: 'textarea', note: 'Notes are visible to agents only, never to customers.', limit: 250, null: true },
{ name: 'active', display: 'Active', tag: 'boolean', type: 'boolean', 'default': true, null: false },
{ name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 },
{ name: 'created_at', display: 'Created', type: 'time', readonly: 1 },
{ name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 },
{ name: 'updated_at', display: 'Updated', type: 'time', readonly: 1 },
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false },
{ name: 'note', display: 'Note', tag: 'textarea', note: 'Notes are visible to agents only, never to customers.', limit: 250, null: true },
{ name: 'active', display: 'Active', tag: 'active', default: true },
{ name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 },
{ name: 'created_at', display: 'Created', tag: 'datetime', readonly: 1 },
{ name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 },
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },
]
@configure_overview = [
'name',

View file

@ -4,14 +4,14 @@ class App.Signature extends App.Model
@url: @apiPath + '/signatures'
@configure_attributes = [
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, 'null': false, 'class': 'span4' },
{ name: 'body', display: 'Text', tag: 'textarea', limit: 250, 'null': true, 'class': 'span4', rows: 10 },
{ name: 'note', display: 'Note', tag: 'textarea', note: 'Notes are visible to agents only, never to customers.', limit: 250, 'null': true, 'class': 'span4' },
{ name: 'active', display: 'Active', tag: 'boolean', type: 'boolean', 'default': true, 'null': false, 'class': 'span4' },
{ name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 },
{ name: 'created_at', display: 'Created', type: 'time', readonly: 1 },
{ name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 },
{ name: 'updated_at', display: 'Updated', type: 'time', readonly: 1 },
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, 'null': false },
{ name: 'body', display: 'Text', tag: 'textarea', limit: 250, 'null': true, rows: 10 },
{ name: 'note', display: 'Note', tag: 'textarea', note: 'Notes are visible to agents only, never to customers.', limit: 250, 'null': true },
{ name: 'active', display: 'Active', tag: 'active', default: true },
{ name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 },
{ name: 'created_at', display: 'Created', tag: 'datetime', readonly: 1 },
{ name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 },
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },
]
@configure_overview = [
'name',

View file

@ -31,11 +31,11 @@ class App.Sla extends App.Model
group: 'Group'
owner: 'Owner'
},
{ name: 'active', display: 'Active', tag: 'boolean', note: 'boolean', 'default': true, 'null': false },
{ name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 },
{ name: 'created_at', display: 'Created', type: 'time', readonly: 1 },
{ name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 },
{ name: 'updated_at', display: 'Updated', type: 'time', readonly: 1 },
{ name: 'active', display: 'Active', tag: 'active', default: true },
{ name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 },
{ name: 'created_at', display: 'Created', tag: 'datetime', readonly: 1 },
{ name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 },
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },
]
@configure_delete = true
@configure_overview = [

View file

@ -3,11 +3,11 @@ class App.TextModule extends App.Model
@extend Spine.Model.Ajax
@url: @apiPath + '/text_modules'
@configure_attributes = [
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, 'null': false, 'class': 'span4' },
{ name: 'keywords', display: 'Keywords', tag: 'input', type: 'text', limit: 100, 'null': true, 'class': 'span4' },
{ name: 'content', display: 'Content', tag: 'textarea', limit: 250, 'null': false, 'class': 'span4' },
{ name: 'updated_at', display: 'Updated', type: 'time', readonly: 1 },
{ name: 'active', display: 'Active', tag: 'boolean', note: 'boolean', 'default': true, 'null': false, 'class': 'span4' },
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false },
{ name: 'keywords', display: 'Keywords', tag: 'input', type: 'text', limit: 100, null: true },
{ name: 'content', display: 'Content', tag: 'textarea', limit: 250, null: false },
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },
{ name: 'active', display: 'Active', tag: 'active', default: true },
]
@configure_delete = true
@configure_overview = [

View file

@ -11,18 +11,18 @@ class App.Ticket extends App.Model
{ name: 'title', display: 'Title', tag: 'input', type: 'text', limit: 100, null: false, parentClass: 'noTruncate' },
{ name: 'state_id', display: 'State', tag: 'select', multiple: false, null: false, relation: 'TicketState', default: 'new', style: 'width: 12%', edit: true, customer: true, },
{ name: 'priority_id', display: 'Priority', tag: 'select', multiple: false, null: false, relation: 'TicketPriority', default: '2 normal', style: 'width: 12%', edit: true, customer: true, },
{ name: 'last_contact', display: 'Last contact', type: 'time', null: true, style: 'width: 12%', parentClass: 'noTruncate' },
{ name: 'last_contact_agent', display: 'Last contact (Agent)', type: 'time', null: true, style: 'width: 12%', parentClass: 'noTruncate' },
{ name: 'last_contact_customer', display: 'Last contact (Customer)', type: 'time', null: true, style: 'width: 12%', parentClass: 'noTruncate' },
{ name: 'first_response', display: 'First response', type: 'time', null: true, style: 'width: 12%', parentClass: 'noTruncate' },
{ name: 'close_time', display: 'Close time', type: 'time', null: true, style: 'width: 12%', parentClass: 'noTruncate' },
{ name: 'pending_time', display: 'Pending Time', type: 'time', null: true, style: 'width: 12%', parentClass: 'noTruncate' },
{ name: 'escalation_time', display: 'Escalation', type: 'time', null: true, style: 'width: 12%', class: 'escalation', parentClass: 'noTruncate' },
{ name: 'article_count', display: 'Article#', style: 'width: 12%' },
{ name: 'last_contact', display: 'Last contact', tag: 'datetime', null: true, style: 'width: 12%', parentClass: 'noTruncate' },
{ name: 'last_contact_agent', display: 'Last contact (Agent)', tag: 'datetime', null: true, style: 'width: 12%', parentClass: 'noTruncate' },
{ name: 'last_contact_customer', display: 'Last contact (Customer)', tag: 'datetime', null: true, style: 'width: 12%', parentClass: 'noTruncate' },
{ name: 'first_response', display: 'First response', tag: 'datetime', null: true, style: 'width: 12%', parentClass: 'noTruncate' },
{ name: 'close_time', display: 'Close time', tag: 'datetime', null: true, style: 'width: 12%', parentClass: 'noTruncate' },
{ name: 'pending_time', display: 'Pending Time', tag: 'datetime', null: true, style: 'width: 12%', parentClass: 'noTruncate' },
{ name: 'escalation_time', display: 'Escalation', tag: 'datetime', null: true, style: 'width: 12%', class: 'escalation', parentClass: 'noTruncate' },
{ name: 'article_count', display: 'Article#', style: 'width: 12%' },
{ name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 },
{ name: 'created_at', display: 'Created', type: 'time', style: 'width: 120px', readonly: 1, parentClass: 'noTruncate' },
{ name: 'created_at', display: 'Created', tag: 'datetime', style: 'width: 120px', readonly: 1, parentClass: 'noTruncate' },
{ name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 },
{ name: 'updated_at', display: 'Updated', type: 'time', style: 'width: 120px', readonly: 1, parentClass: 'noTruncate' },
{ name: 'updated_at', display: 'Updated', tag: 'datetime', style: 'width: 120px', readonly: 1, parentClass: 'noTruncate' },
]
uiUrl: ->

View file

@ -3,19 +3,19 @@ class App.TicketArticle extends App.Model
@extend Spine.Model.Ajax
@url: @apiPath + '/ticket_articles'
@configure_attributes = [
{ name: 'ticket_id', display: 'TicketID', null: false, readonly: 1, },
{ name: 'from', display: 'From', tag: 'input', type: 'text', limit: 100, null: false, },
{ name: 'to', display: 'To', tag: 'input', type: 'text', limit: 100, null: true, },
{ name: 'cc', display: 'Cc', tag: 'input', type: 'text', limit: 100, null: true, },
{ name: 'subject', display: 'Subject', tag: 'input', type: 'text', limit: 100, null: true, },
{ name: 'body', display: 'Text', tag: 'textarea', rows: 5, limit: 100, null: false, },
{ name: 'type_id', display: 'Type', tag: 'select', multiple: false, null: false, relation: 'TicketArticleType', default: '', class: 'medium' },
{ name: 'sender_id', display: 'Sender', tag: 'select', multiple: false, null: false, relation: 'TicketArticleSender', default: '', class: 'medium' },
{ name: 'internal', display: 'Visibility', tag: 'radio', default: false, null: true, options: { true: 'internal', false: 'public' }, class: 'medium' },
{ name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 },
{ name: 'created_at', display: 'Created', type: 'time', readonly: 1 },
{ name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 },
{ name: 'updated_at', display: 'Updated', type: 'time', readonly: 1 },
{ name: 'ticket_id', display: 'TicketID', null: false, readonly: 1, },
{ name: 'from', display: 'From', tag: 'input', type: 'text', limit: 100, null: false },
{ name: 'to', display: 'To', tag: 'input', type: 'text', limit: 100, null: true },
{ name: 'cc', display: 'Cc', tag: 'input', type: 'text', limit: 100, null: true },
{ name: 'subject', display: 'Subject', tag: 'input', type: 'text', limit: 100, null: true },
{ name: 'body', display: 'Text', tag: 'textarea', rows: 5, limit: 100, null: false },
{ name: 'type_id', display: 'Type', tag: 'select', multiple: false, null: false, relation: 'TicketArticleType', default: '' },
{ name: 'sender_id', display: 'Sender', tag: 'select', multiple: false, null: false, relation: 'TicketArticleSender', default: '' },
{ name: 'internal', display: 'Visibility', tag: 'radio', default: false, null: true, options: { true: 'internal', false: 'public' } },
{ name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 },
{ name: 'created_at', display: 'Created', tag: 'datetime', readonly: 1 },
{ name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 },
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },
]
uiUrl: ->

View file

@ -3,10 +3,10 @@ class App.TicketPriority extends App.Model
@extend Spine.Model.Ajax
@url: @apiPath + '/ticket_priorities'
@configure_attributes = [
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, 'null': false, translate: true },
{ name: 'active', display: 'Active', tag: 'boolean', type: 'boolean', 'default': true, 'null': false },
{ name: 'updated_at', display: 'Updated', type: 'time', readonly: 1 },
{ name: 'created_at', display: 'Created', type: 'time', readonly: 1 },
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false, translate: true },
{ name: 'active', display: 'Active', tag: 'active', default: true },
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },
{ name: 'created_at', display: 'Created', tag: 'datetime', readonly: 1 },
]
@configure_translate = true
@configure_overview = [

View file

@ -3,10 +3,10 @@ class App.TicketState extends App.Model
@extend Spine.Model.Ajax
@url: @apiPath + '/ticket_states'
@configure_attributes = [
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, 'null': false, translate: true },
{ name: 'active', display: 'Active', tag: 'boolean', type: 'boolean', 'default': true, 'null': false },
{ name: 'updated_at', display: 'Updated', type: 'time', readonly: 1 },
{ name: 'created_at', display: 'Created', type: 'time', readonly: 1 },
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false, translate: true },
{ name: 'active', display: 'Active', tag: 'active', default: true },
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },
{ name: 'created_at', display: 'Created', tag: 'datetime', readonly: 1 },
]
@configure_translate = true
@configure_overview = [

View file

@ -5,25 +5,25 @@ class App.User extends App.Model
# @hasMany 'roles', 'App.Role'
@configure_attributes = [
{ name: 'login', display: 'Login', tag: 'input', type: 'text', limit: 100, null: false, class: 'span4', autocapitalize: false, signup: false, quick: false },
{ name: 'firstname', display: 'Firstname', tag: 'input', type: 'text', limit: 100, null: false, class: 'span4', signup: true, info: true, invite_agent: true },
{ name: 'lastname', display: 'Lastname', tag: 'input', type: 'text', limit: 100, null: false, class: 'span4', signup: true, info: true, invite_agent: true },
{ name: 'email', display: 'Email', tag: 'input', type: 'email', limit: 100, null: false, class: 'span4', signup: true, info: true, invite_agent: true },
{ name: 'web', display: 'Web', tag: 'input', type: 'url', limit: 100, null: true, class: 'span4', signup: false, info: true },
{ name: 'phone', display: 'Phone', tag: 'input', type: 'phone', limit: 100, null: true, class: 'span4', signup: false, info: true },
{ name: 'mobile', display: 'Mobile', tag: 'input', type: 'phone', limit: 100, null: true, class: 'span4', signup: false, info: true },
{ name: 'fax', display: 'Fax', tag: 'input', type: 'phone', limit: 100, null: true, class: 'span4', signup: false, info: true },
{ name: 'organization_id', display: 'Organization', tag: 'select', multiple: false, nulloption: true, null: true, relation: 'Organization', class: 'span4', signup: false, info: true },
{ name: 'department', display: 'Department', tag: 'input', type: 'text', limit: 200, null: true, class: 'span4', signup: false, info: true },
{ name: 'street', display: 'Street', tag: 'input', type: 'text', limit: 100, null: true, class: 'span4', signup: false, info: true },
{ name: 'zip', display: 'Zip', tag: 'input', type: 'text', limit: 100, null: true, class: 'span4', signup: false, info: true },
{ name: 'city', display: 'City', tag: 'input', type: 'text', limit: 100, null: true, class: 'span4', signup: false, info: true },
{ name: 'password', display: 'Password', tag: 'input', type: 'password', limit: 50, null: true, autocomplete: 'off', class: 'span4', signup: true, },
{ name: 'note', display: 'Note', tag: 'textarea', note: 'Notes are visible to agents only, never to customers.', limit: 250, null: true, class: 'span4', info: true },
{ name: 'role_ids', display: 'Roles', tag: 'checkbox', multiple: true, null: false, relation: 'Role', class: 'span4' },
{ name: 'group_ids', display: 'Groups', tag: 'checkbox', multiple: true, null: true, relation: 'Group', class: 'span4', invite_agent: true },
{ name: 'active', display: 'Active', tag: 'boolean', default: true, null: true, class: 'span4' },
{ name: 'updated_at', display: 'Updated', type: 'time', readonly: 1 },
{ name: 'login', display: 'Login', tag: 'input', type: 'text', limit: 100, null: false, autocapitalize: false, signup: false, quick: false },
{ name: 'firstname', display: 'Firstname', tag: 'input', type: 'text', limit: 100, null: false, signup: true, info: true, invite_agent: true },
{ name: 'lastname', display: 'Lastname', tag: 'input', type: 'text', limit: 100, null: false, signup: true, info: true, invite_agent: true },
{ name: 'email', display: 'Email', tag: 'input', type: 'email', limit: 100, null: false, signup: true, info: true, invite_agent: true },
{ name: 'web', display: 'Web', tag: 'input', type: 'url', limit: 100, null: true, signup: false, info: true },
{ name: 'phone', display: 'Phone', tag: 'input', type: 'phone', limit: 100, null: true, signup: false, info: true },
{ name: 'mobile', display: 'Mobile', tag: 'input', type: 'phone', limit: 100, null: true, signup: false, info: true },
{ name: 'fax', display: 'Fax', tag: 'input', type: 'phone', limit: 100, null: true, signup: false, info: true },
{ name: 'organization_id', display: 'Organization', tag: 'select', multiple: false, nulloption: true, null: true, relation: 'Organization', signup: false, info: true },
{ name: 'department', display: 'Department', tag: 'input', type: 'text', limit: 200, null: true, signup: false, info: true },
{ name: 'street', display: 'Street', tag: 'input', type: 'text', limit: 100, null: true, signup: false, info: true },
{ name: 'zip', display: 'Zip', tag: 'input', type: 'text', limit: 100, null: true, signup: false, info: true },
{ name: 'city', display: 'City', tag: 'input', type: 'text', limit: 100, null: true, signup: false, info: true },
{ name: 'password', display: 'Password', tag: 'input', type: 'password', limit: 50, null: true, autocomplete: 'off', signup: true, },
{ name: 'note', display: 'Note', tag: 'textarea', note: 'Notes are visible to agents only, never to customers.', limit: 250, null: true, info: true },
{ name: 'role_ids', display: 'Roles', tag: 'checkbox', multiple: true, null: false, relation: 'Role' },
{ name: 'group_ids', display: 'Groups', tag: 'checkbox', multiple: true, null: true, relation: 'Group', invite_agent: true },
{ name: 'active', display: 'Active', tag: 'active', default: true },
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },
]
@configure_overview = [
# 'login', 'firstname', 'lastname', 'email', 'updated_at',
@ -52,21 +52,21 @@ class App.User extends App.Model
else
return '??'
avatar: (size = 40, placement = '', cssClass = '') ->
cssClass += " size-#{ size }"
avatar: (size = 40, placement = '', cssClass = '', unique = false, avatar) ->
cssClass += " size-#{size}"
if placement
placement = "data-placement=\"#{placement}\""
if !@image || @image is 'none'
return @uniqueAvatar(size, placement, cssClass)
if !@image || @image is 'none' || unique
return @uniqueAvatar(size, placement, cssClass, avatar)
else
"<span class=\"avatar user-popover #{cssClass}\" data-id=\"#{@id}\" style=\"background-image: url(#{ @imageUrl })\" #{placement}></span>"
uniqueAvatar: (size = 40, placement = '', cssClass = '', avatar) ->
if size
cssClass += " size-#{ size }"
if @vip
cssClass += " vip"
image = @imageUrl()
"<span class=\"avatar user-popover #{cssClass}\" data-id=\"#{@id}\" style=\"background-image: url(#{image})\" #{placement}></span>"
uniqueAvatar: (size, placement = '', cssClass = '', avatar) ->
width = 300
height = 226
size = parseInt(size, 10)
@ -76,12 +76,21 @@ class App.User extends App.Model
y = rng() * (height - size)
if !avatar
cssClass += "#{cssClass} user-popover"
cssClass += " user-popover"
data = "data-id=\"#{@id}\""
else
data = "data-avatar-id=\"#{avatar.id}\""
if @vip
cssClass += " vip"
"<span class=\"avatar unique #{cssClass}\" #{data} style=\"background-position: -#{ x }px -#{ y }px;\" #{placement}>#{ @initials() }</span>"
imageUrl: ->
return if !@image
# set image url
@constructor.apiPath + '/users/image/' + @image
@_fillUp: (data) ->
# set socal media links
@ -92,9 +101,6 @@ class App.User extends App.Model
if account == 'facebook'
data['accounts'][account]['link'] = 'https://www.facebook.com/profile.php?id=' + data['accounts'][account]['uid']
# set image url
data.imageUrl = @apiPath + '/users/image/' + data.image
if data.organization_id
data.organization = App.Organization.find(data.organization_id)

View file

@ -11,30 +11,30 @@
<input type="checkbox" value="<%= ticket.id %>" name="bulk" class="pull-left"/>
</td>
<td class="span1">
<img class="thumbnail user-popover" data-id="<%= ticket.customer_id %>" src="<%= ticket.customer.imageUrl %>" alt="">
<img class="thumbnail user-popover" data-id="<%= ticket.customer_id %>" src="<%- ticket.customer.imageUrl() %>" alt="">
</td>
<td class="span10">
<h3><a href="#" data-type="edit"><%= ticket.title %></a> <small><%= ticket.number %> <span class="humanTimeFromNow" data-time="<%= ticket.created_at %>">?</span></small></h3>
<div class="row">
<div class="span2">
<b><%- @T( 'State' ) %></b> <%- @T( ticket.state.name ) %>
<b><%- @T( 'State' ) %></b> <%- @P( ticket, 'state' ) %>
</div>
<div class="span2">
<b><%- @T( 'Group' ) %></b> <%= ticket.group.name %>
<b><%- @T( 'Group' ) %></b> <%- @P( ticket, 'group' ) %>
</div>
<div class="span2">
<b><%- @T( 'Customer' ) %></b> <%= ticket.customer.displayName() %>
<b><%- @T( 'Customer' ) %></b> <%- @P( ticket, 'customer' ) %>
</div>
</div>
<div class="row">
<div class="span2">
<b><%- @T( 'Priority' ) %></b> <%- @T( ticket.priority.name ) %>
<b><%- @T( 'Priority' ) %></b> <%- @P( ticket, 'priority' ) %>
</div>
<div class="span2">
<b><%- @T( 'Owner' ) %></b> <%= ticket.owner.displayName() %>
<b><%- @T( 'Owner' ) %></b> <%- @P( ticket, 'owner' ) %>
</div>
<div class="span2">
<b><%- @T( 'Organization' ) %></b> <%= @P( ticket.customer.organization ) %>
<b><%- @T( 'Organization' ) %></b> <%- @P( ticket, 'organization' ) %>
</div>
</div>
</div>

View file

@ -29,16 +29,9 @@
<% groupLast = '' %>
<% for object in @objects: %>
<% if @groupBy: %>
<% if object[@groupBy] && object[@groupBy].displayName: %>
<% groupByName = object[@groupBy].displayName() %>
<% if object[@groupBy].translate(): %>
<% groupByName = @T(groupByName) %>
<% end %>
<% else: %>
<% groupByName = object[@groupBy] || '-' %>
<% end %>
<% groupByName = @P( object, @groupBy ) %>
<% if groupLast isnt groupByName: %>
<tr class=""><td colspan="<%= length %>"><b><%- @P( groupByName ) %></b></td></tr>
<tr class=""><td colspan="<%= length %>"><b><%= groupByName %></b></td></tr>
<% groupLast = groupByName %>
<% end %>
<% end %>
@ -56,47 +49,26 @@
<td><input type="radio" value="<%= object.id %>" name="radio"/></td>
<% end %>
<% for item in @header: %>
<% translation = false %>
<% value = object[item.name] %>
<% item_id = item.name.substr(item.name.length-3, item.name.length) %>
<% if item_id is '_id' && object[ item.name.substr(0, item.name.length-3) ]: %>
<% value = object[ item.name.substr(0, item.name.length-3) ] %>
<% refObject = object[ item.name.substr(0, item.name.length-3) ] %>
<% end %>
<% if !value: %>
<% parts = item.name.split '::' %>
<% if parts[0] && parts[1] && object[ parts[0] ]: %>
<% value = object[ parts[0] ][ parts[1] ] %>
<% end %>
<% end %>
<% if value && value.displayNameLong : %>
<% translation = true %>
<% value = value.displayNameLong() %>
<% else if value && value.displayName : %>
<% translation = true %>
<% value = value.displayName() %>
<% end %>
<% item_clone = item %>
<% value = @P( object, item.name ) %>
<% if @callbacks: %>
<% if item.name.substr(item.name.length-3, item.name.length) is '_id' && object[ item.name.substr(0, item.name.length-3) ]: %>
<% refObject = object[ item.name.substr(0, item.name.length-3) ] %>
<% end %>
<% for attribute, callbacksAll of @callbacks: %>
<% if attribute is item.name || attribute is item_id: %>
<% if attribute is item.name: %>
<% for callback in callbacksAll: %>
<% value = callback( value, object, item_clone, @header, refObject ) %>
<% value = callback( value, object, item, @header, refObject ) %>
<% end %>
<% end %>
<% end %>
<% end %>
<% #console.log('HH', item_clone.name, item_clone.type, item_clone.translate, item_clone, object.translate(), refObject, translation) %>
<td <% if item_clone.parentClass: %>class="<%= item_clone.parentClass %>"<% end %>>
<% if item_clone.link: %><a href="<%- item_clone.link %>" <% if item_clone.target: %>target="<%= item_clone.target %>"<% end %>><% end %>
<% if item_clone.translate || ( translation && !refObject && object.translate && object.translate() ) || ( translation && refObject && refObject.translate && refObject.translate() ) : %>
<span <% if item_clone.class: %>class="<%= item_clone.class %>"<% end %>><%- @T( @P( value, item_clone ) ) %></span>
<% else if item_clone.type is 'time': %>
<span class="humanTimeFromNow <% if item_clone.class: %><%= item_clone.class %><% end %>" data-time="<%- object[item_clone.name] %>">?</span>
<% else: %>
<span <% if item_clone.class: %>class="<%= item_clone.class %>"<% end %> <% if item_clone.title: %>title="<%= item_clone.title %>"<% end %> <% if item_clone.data: %><% for data_key, data_item of item_clone.data: %>data-<%- data_key %>="<%= data_item %>" <% end %><% end %>><%= @P(value) %></span>
<% end %>
<% if item_clone.link: %></a><% end %>
<td <% if item.parentClass: %>class="<%= item.parentClass %>"<% end %>>
<% if item.link: %><a href="<%- item.link %>" <% if item.target: %>target="<%= item.target %>"<% end %>><% end %>
<span <% if item.class: %>class="<%= item.class %>"<% end %> <% if item.title: %>title="<%= item.title %>"<% end %> <% if item.data: %><% for data_key, data_item of item.data: %>data-<%- data_key %>="<%= data_item %>" <% end %><% end %>><%- value %></span>
<% if item.link: %></a><% end %>
</td>
<% end %>
<% if @destroy: %>
@ -105,4 +77,4 @@
</tr>
<% end %>
</tbody>
</table>
</table>

View file

@ -243,6 +243,10 @@ Oliver<br>
<div class="attachment-name u-highlight">something with very long line which is slonger as the container arount it if you can see it I wish you a very happy day.txt</div>
<div class="attachment-size">17.1 kb</div>
</div>
<div class="attachment horizontal">
<div class="attachment-name u-highlight">something-withverylonglinewhichisslongerasthecontainerarountitifyoucanseeitIwishyouaveryhappyday.txt</div>
<div class="attachment-size">17.1 kb</div>
</div>
</div>
</div>
</div>

View file

@ -1,50 +0,0 @@
<div class="flex profile">
<div class="profile-window">
<div class="profile-section vertical centered">
<div class="align-right profile-action js-action"></div>
<div class="profile-organizationIcon">
<div class="organization icon"></div>
</div>
<h1><%= @organization.displayName() %></h1>
</div>
<div class="profile-section">
<div class="profile-details horizontal wrap">
<% for row in @organizationData: %>
<% if @organization[row.name]: %>
<% if row.tag is 'richtext': %>
<div class="profile-detailsEntry" style="width: 100%;">
<label><%- @Ti( row.display ) %></label>
<div contenteditable="true" data-name="<%= row.name %>" data-type="update" data-placeholder="<%- @T('Add a Note') %>"><%- @organization[row.name] %></div>
</div>
<% else: %>
<div class="profile-detailsEntry">
<label><%- @Ti( row.display ) %></label>
<%- @L( @P( @organization[row.name] ) ) %>
</div>
<% end %>
<% end %>
<% end %>
</div>
</div>
<% if @organization.members: %>
<div class="profile-section profile-memberSection">
<label><%- @T('Members') %></label>
<div class="profile-details horizontal wrap">
<% for user in @organization.members: %>
<div class="profile-organizationMember">
<%- user.avatar("40") %>
<a href="<%- user.uiUrl() %>" class="user-popover" data-id="<%- user.id %>"><%= user.displayName() %></a>
</div>
<% end %>
</div>
</div>
<% end %>
<div class="profile-section js-ticket-stats"></div>
</div>
</div>

View file

@ -0,0 +1,6 @@
<div class="flex profile">
<div class="profile-window">
<div class="js-object-container"></div>
<div class="profile-section js-ticket-stats"></div>
</div>
</div>

View file

@ -0,0 +1,39 @@
<div class="profile-section vertical centered">
<div class="align-right profile-action js-action"></div>
<div class="profile-organizationIcon">
<div class="organization icon"></div>
</div>
<h1><%= @organization.displayName() %></h1>
</div>
<div class="profile-section">
<div class="profile-details horizontal wrap">
<% for row in @organizationData: %>
<% if row.tag is 'richtext': %>
<div class="profile-detailsEntry" style="width: 100%;">
<label><%- @Ti( row.display ) %></label>
<div contenteditable="true" data-name="<%= row.name %>" data-type="update" data-placeholder="<%- @T('Add a Note') %>"><%- @organization[row.name] %></div>
</div>
<% else: %>
<% if @organization[row.name]: %>
<div class="profile-detailsEntry">
<label><%- @Ti( row.display ) %></label>
<%- @P( @organization, row.name ) %>
</div>
<% end %>
<% end %>
<% end %>
</div>
</div>
<% if @organization.members: %>
<div class="profile-section profile-memberSection">
<label><%- @T('Members') %></label>
<div class="profile-details horizontal wrap">
<% for user in @organization.members: %>
<div class="profile-organizationMember">
<%- user.avatar("40") %>
<a href="<%- user.uiUrl() %>" class="user-popover" data-id="<%- user.id %>"><%= user.displayName() %></a>
</div>
<% end %>
</div>
</div>
<% end %>

View file

@ -1,24 +1,19 @@
<hr>
<div>
<% for row in @organizationData: %>
<% if @organization[row.name]: %>
<div class="column">
<h3><%- @T( row.display ) %></h3>
<% if row.tag is 'richtext': %>
<div><%- @organization[row.name] %></div>
<% else: %>
<div><%- @L( @P( @organization[row.name] ) ) %></div>
<% end %>
<div class="popover-block">
<label><%- @T( row.display ) %></label>
<%- @P( @organization, row.name ) %>
</div>
<% end %>
<% end %>
</div>
<% if @organization.members: %>
<hr>
<h3><%- @T('Members') %></h3>
<% for user in @organization.members: %>
<div class="person">
<%= user.displayName() %>
<div class="popover-block">
<label><%- @T('Members') %></label>
<% for user in @organization.members: %>
<div class="person"><%= user.displayName() %></div>
<% end %>
</div>
<% end %>
<% end %>

View file

@ -2,36 +2,40 @@
<span class="<%- @ticket.icon() %>" title="<%- @ticket.iconTitle() %>"></span> <span class="<%- @ticket.iconTextClass() %>"><%- @ticket.iconTitle() %></span>
</div>
<hr>
<h3><%- @P('Agent') %></h3>
<div class="person">
<%= @ticket.owner.displayName() %>
<% if @ticket.owner.organization_id: %>
<span class="organization"><%= @ticket.owner.organization.displayName() %></span>
<% end %>
<div class="popover-block">
<label><%- @T('Agent') %></label>
<div class="person">
<%= @ticket.owner.displayName() %>
<% if @ticket.owner.organization: %>
<span class="organization"><%= @ticket.owner.organization.displayName() %></span>
<% end %>
</div>
</div>
<h3><%- @P('Customer') %></h3>
<div class="person">
<%= @ticket.customer.displayName() %>
<% if @ticket.customer.organization_id: %>
<span class="organization"><%= @ticket.customer.organization.displayName() %></span>
<% end %>
<div class="popover-block">
<label><%- @T('Customer') %></label>
<div class="person">
<%= @ticket.customer.displayName() %>
<% if @ticket.customer.organization: %>
<span class="organization"><%= @ticket.customer.organization.displayName() %></span>
<% end %>
</div>
</div>
<hr>
<div class="horizontal two-columns">
<div class="column">
<h3>#</h3>
<div class="u-textTruncate"><%- @P( @ticket.number ) %></div>
<label>#</label>
<div class="u-textTruncate"><%- @P( @ticket, 'number' ) %></div>
</div>
<div class="column">
<h3><%- @T( 'Priority' ) %></h3>
<div class="u-textTruncate"><%- @T( @ticket.priority.name ) %></div>
<label><%- @T( 'Priority' ) %></label>
<div class="u-textTruncate"><%- @P( @ticket, 'priority' ) %></div>
</div>
<div class="column">
<h3><%- @T( 'Created' ) %></h3>
<div class="u-textTruncate"><%- @P( @ticket.humanTime ) %></div>
<label><%- @T( 'Created' ) %></label>
<div class="u-textTruncate"><%- @P( @ticket, 'created_at' ) %></div>
</div>
<div class="column">
<h3><%- @T( 'Group' ) %></h3>
<div class="u-textTruncate"><%- @P( @ticket.group ) %></div>
<label><%- @T( 'Group' ) %></label>
<div class="u-textTruncate"><%- @P( @ticket, 'group' ) %></div>
</div>
</div>

View file

@ -5,19 +5,15 @@
<div>
<% for row in @userData: %>
<% if @user[row.name]: %>
<div class="column">
<h3><%- @T( row.display ) %></h3>
<% if row.tag is 'richtext': %>
<div><%- @user[row.name] %></div>
<% else: %>
<div><%- @L( @P( @user[row.name] ) ) %></div>
<% end %>
<div class="popover-block">
<label><%- @T( row.display ) %></label>
<%- @P( @user, row.name ) %>
</div>
<% end %>
<% end %>
<% if !_.isEmpty(@user['accounts']): %>
<div class="customer-info">
<h3><%- @T( 'Linked Accounts' ) %></h3>
<div class="popover-block">
<label><%- @T( 'Linked Accounts' ) %></label>
<% for account of @user['accounts']: %>
<a href="<%= @user['accounts'][account]['link'] %>" target="_blank"><%= account %></a>
<% end %>
@ -25,8 +21,8 @@
<% end %>
<% if !_.isEmpty( @user['links'] ): %>
<% for link in @user['links']: %>
<div class="customer-info">
<h3><%- @T( link['title'] ) %></h3>
<div class="popover-block">
<label><%- @T( link['title'] ) %></label>
<% for item in link['items']: %>
<% if item['url']: %>
<a href="<%= item['url'] %>" title="<%- @Ti( item['title'] ) %>" style="<%= item['style'] %>" data-type="<%= item['data'] %>" class="<%= item['class'] %>" <% if link.new_window: %>target="_blank"<% end %>>

View file

@ -1,3 +1,8 @@
<ul>
<% for ticket in @tickets: %>
<div class="customer-info"><a href="#ticket/zoom/<%= ticket.id %>" title="<%= ticket.title %>">T:<%= ticket.number %></a> <span title="<%= ticket.created_at_short %>"><%= ticket.humanTime %></span><br/><%= ticket.title %></div>
<li>
<a href="#ticket/zoom/<%= ticket.id %>" title="<%= ticket.title %>">T:<%= ticket.number %></a> <%- @P( ticket, 'title') %><br/>
<%- @T('Created') %> <%- @P( ticket, 'created_at') %>
</li>
<% end %>
<ul>

View file

@ -17,7 +17,7 @@
<% if avatar.default: %>
<% cssClass = 'is-active' %>
<% end %>
<%- App.Session.get().uniqueAvatar('50', '', cssClass, avatar) %>
<%- App.Session.get().avatar('50', '', cssClass, true, avatar) %>
<% else: %>
<span class="avatar size-50 <% if avatar.default: %>is-active<% end %>" data-avatar-id="<%- avatar.id %>" style="background-image: url(<%- avatar.content %>)"></span>
<% if avatar.deletable: %>

View file

@ -1 +1 @@
<h1 contenteditable="true" class="ticket-title-update" data-placeholder="<%= @T('Enter Title...') %>"><%= @P( @ticket.title ) %></h1>
<div contenteditable="true" class="ticket-title-update" data-placeholder="<%= @T('Enter Title...') %>"><%= @ticket.title %></div>

View file

@ -1,37 +0,0 @@
<div class="flex profile">
<div class="profile-window">
<div class="profile-section vertical centered">
<div class="align-right profile-action js-action"></div>
<%- @user.avatar("80") %>
<h1><%= @user.displayName() %></h1>
<% if @user.organization: %>
<div class="profile-organization"><a href="<%- @user.organization.uiUrl() %>"><%= @user.organization.displayName() %></a></div>
<% end %>
</div>
<div class="profile-section">
<div class="profile-details horizontal wrap">
<% for row in @userData: %>
<% if @user[row.name]: %>
<% if row.tag is 'richtext': %>
<div class="profile-detailsEntry" style="width: 100%;">
<label><%- @Ti( row.display ) %></label>
<div contenteditable="true" data-name="<%= row.name %>" data-type="update" data-placeholder="<%- @T('Add a Note') %>"><%- @user[row.name] %></div>
</div>
<% else: %>
<div class="profile-detailsEntry">
<label><%- @Ti( row.display ) %></label>
<%- @L( @P( @user[row.name] ) ) %>
</div>
<% end %>
<% end %>
<% end %>
</div>
</div>
<div class="profile-section js-ticket-stats"></div>
</div>
</div>

View file

@ -0,0 +1,6 @@
<div class="flex profile">
<div class="profile-window">
<div class="js-object-container"></div>
<div class="profile-section js-ticket-stats"></div>
</div>
</div>

View file

@ -0,0 +1,27 @@
<div class="profile-section vertical centered">
<div class="align-right profile-action js-action"></div>
<%- @user.avatar("80") %>
<h1><%= @user.displayName() %></h1>
<% if @user.organization: %>
<div class="profile-organization"><a href="<%- @user.organization.uiUrl() %>"><%= @user.organization.displayName() %></a></div>
<% end %>
</div>
<div class="profile-section">
<div class="profile-details horizontal wrap">
<% for row in @userData: %>
<% if row.tag is 'richtext': %>
<div class="profile-detailsEntry" style="width: 100%;">
<label><%- @Ti( row.display ) %></label>
<div contenteditable="true" data-name="<%= row.name %>" data-type="update" data-placeholder="<%- @T('Add a Note') %>"><%- @user[row.name] %></div>
</div>
<% else: %>
<% if @user[row.name]: %>
<div class="profile-detailsEntry">
<label><%- @Ti( row.display ) %></label>
<%- @P( @user, row.name ) %>
</div>
<% end %>
<% end %>
<% end %>
</div>
</div>

View file

@ -1,7 +1,8 @@
<div class="sidebar-block">
<h3 class="u-textTruncate" title="<%- @Ti( 'Name') %>">
<%= @organization.displayName() %>
</h3>
<div class="avatar organizationInfo-avatar size-50">
<a href="<%- @organization.uiUrl() %>" class="organization icon"></a>
</div>
<h3 title="<%- @Ti( 'Name') %>"><%= @organization.displayName() %></h3>
</div>
<% for row in @organizationData: %>
@ -9,7 +10,7 @@
<div class="sidebar-block">
<% if row.tag isnt 'richtext': %>
<label><%- @T( row.display ) %></label>
<%- @L( @P( @organization[row.name] ) ) %>
<%- @P( @organization, row.name ) %>
<% else: %>
<label><%- @T( row.display ) %></label>
<div contenteditable="true" data-name="<%= row.name %>" data-type="update-org" data-placeholder="<%- @T('Add a Note') %>"><%- @organization[row.name] %></div>

View file

@ -1,16 +1,13 @@
<div class="userInfo">
<div class="sidebar-block">
<%- @user.avatar("50", "", "userInfo-avatar") %>
<h3 class="u-textTruncate" title="<%- @Ti( 'Name') %>">
<%= @user.displayName() %>
</h3>
</div>
<div class="sidebar-block">
<%- @user.avatar("50", "", "userInfo-avatar") %>
<h3 title="<%- @Ti( 'Name') %>"><%= @user.displayName() %></h3>
</div>
<% for row in @userData: %>
<% if @user[row.name] || row.name is 'note': %>
<div class="sidebar-block">
<% if row.tag isnt 'richtext': %>
<label><%- @T( row.display ) %></label>
<%- @L( @P( @user[row.name] ) ) %>
<%- @P( @user, row.name ) %>
<% else: %>
<label><%- @T( row.display ) %></label>
<div contenteditable="true" data-name="<%= row.name %>" data-type="update" data-placeholder="<%- @T('Add a Note') %>"><%- @user[row.name] %></div>
@ -45,5 +42,4 @@
</div>
<% end %>
<% end %>
<% end %>
</div>
<% end %>

View file

@ -74,41 +74,49 @@ function difference(object1, object2) {
}
// clone, just data, no instances of objects
function clone(item) {
if (!item) { return item; }
function clone(item, full) {
if (!item) { return item }
// ignore certain objects
var acceptedInstances = [ 'Object', 'Number', 'String', 'Boolean', 'Array' ];
var acceptedInstances = [ 'Object', 'Number', 'String', 'Boolean', 'Array' ]
if (full) {
acceptedInstances.push( 'Function' )
}
if (item && item.constructor) {
if (!_.contains(acceptedInstances, item.constructor.name)) {
return;
return
}
}
var result;
// copy array
var result;
if ( _.isArray(item) ) {
result = [];
result = []
item.forEach(function(child, index, array) {
result[index] = clone( child );
result[index] = clone( child, full )
});
}
// copy function
else if ( _.isFunction(item) ) {
result = item.bind({})
}
// copy object
else if ( _.isObject(item) ) {
result = {};
result = {}
for(var key in item) {
if (item.hasOwnProperty(key)) {
result[key] = clone(item[key])
result[key] = clone( item[key], full )
}
}
}
// copy others
else {
result = item;
result = item
}
return result;
return result
}
// taken from http://stackoverflow.com/questions/4459928/how-to-deep-clone-in-javascript

View file

@ -714,6 +714,10 @@ textarea,
background: none;
}
textarea.form-control {
height: 118px;
}
select.form-control {
padding-left: 10px;
padding-right: 34px;
@ -2478,7 +2482,7 @@ footer {
.user-menu .dropdown-menu > li > a:hover {
color: #2594d4;
background: rgba(0,0,0,.05);
background: rgba(0,0,0,.05);
}
.avatar {
@ -2492,6 +2496,11 @@ footer {
vertical-align: bottom;
}
:not(.navigation) .avatar.vip {
border: 2px dotted;
border-color: #fff;
}
.avatar.size-50 {
width: 50px;
height: 50px;
@ -2515,16 +2524,30 @@ footer {
cursor: default;
}
:not(.navigation) .unique.avatar.vip {
border: 2px dotted;
line-height: 40px;
}
.unique.avatar.size-50 {
font-size: 16px;
line-height: 52px;
}
:not(.navigation) .unique.avatar.size-50.vip {
line-height: 50px;
}
.unique.avatar.size-80 {
font-size: 20px;
line-height: 84px;
}
:not(.navigation) .unique.avatar.size-80.vip {
font-size: 20px;
line-height: 82px;
}
.sidebar {
width: 32%;
max-width: 300px;
@ -2549,12 +2572,18 @@ footer {
.sidebar-block {
margin: 20px 0;
word-wrap: break-word;
&:first-child {
margin-top: 0;
}
}
.sidebar-block,
.sidebar-block > * {
@extend .u-textTruncate;
}
.sidebar-block [contenteditable=true] {
white-space: normal; // do no u-textTruncate, we want to edit it inline
}
.main + .sidebar {
border-right: none;
@ -2718,10 +2747,16 @@ footer {
}
.popover .user-organization {
@extend .u-textTruncate;
margin-bottom: 8px;
margin-top: -4px;
}
.popover-block {
@extend .sidebar-block;
margin: 10px 0;
}
.popover hr {
margin: 8px 0;
}
@ -2729,7 +2764,15 @@ footer {
.popover .person .organization:before { content: '('; }
.popover .person .organization:after { content: ')'; }
.popover .column h3 {
.popover label {
font-size: 13px;
color: #a9bcc4;
font-weight: 200;
text-transform: uppercase;
letter-spacing: 0.07em;
}
.popover .column label {
margin: 8px 0 1px;
}
@ -3140,14 +3183,12 @@ footer {
padding: 0 81px;
}
.ticket-title h1 {
margin-top: 15px;
margin-bottom: 8px;
text-align: center;
}
.ticket-title-update {
@extend h1;
white-space: normal;
margin-top: 15px;
margin-bottom: 8px;
text-align: center;
}
.task-subline {
@ -3158,7 +3199,7 @@ footer {
.ticket-article-item {
padding-bottom: 33px;
position: relative;
.avatar {
position: absolute;
right: 0;
@ -4178,6 +4219,7 @@ footer {
border-radius: 0;
border: 1px solid hsl(0,0%,90%);
box-shadow: none;
word-wrap: break-word;
}
.modal-header {
@ -4381,33 +4423,26 @@ footer {
position: relative;
}
.userInfo-avatar:after {
content: "";
background: image_url("/assets/images/sprite.svg");
background-position: -236px 0;
right: 0;
top: 0;
width: 97px;
height: 96px;
position: absolute;
.organizationInfo-avatar {
@extend .userInfo-avatar;
padding: 18px 0 0 18px;
}
.userList {
list-style: none;
padding: 0;
li {
@extend .horizontal;
@extend .center;
position: relative;
margin: 10px 0;
}
a {
@extend .u-textTruncate;
}
.avatar {
margin-right: 7px;
position: absolute;
top: 10px;
right: 0px;
left: 48px;
}
}

View file

@ -264,7 +264,7 @@ class ApplicationController < ActionController::Base
begin
# create object
generic_object = object.new( object.param_cleanup( params[object.to_app_model_url] ) )
generic_object = object.new( object.param_cleanup( params[object.to_app_model_url], true ) )
# save object
generic_object.save!

View file

@ -97,9 +97,7 @@ class TicketsController < ApplicationController
:customer_id => params[:customer_id],
:limit => 15,
)
render :json => {
:tickets => result
}
render :json => result
end
# GET /api/v1/ticket_history/1

View file

@ -64,7 +64,7 @@ class UsersController < ApplicationController
# @response_message 200 [User] Created User record.
# @response_message 401 Invalid session.
def create
user = User.new( User.param_cleanup(params) )
user = User.new( User.param_cleanup(params, true) )
begin
# check if it's first user
@ -122,7 +122,7 @@ class UsersController < ApplicationController
end
end
user.save
user.save!
# if first user was added, set system init done
if count <= 2
@ -702,7 +702,7 @@ curl http://localhost/api/v1/users/avatar -v -u #{login}:#{password} -H "Content
private
def password_policy(password)
if Setting.get('password_min_size') > password.length
if Setting.get('password_min_size').to_i > password.length
return ["Can\'t update password, it must be at least %s characters long!", Setting.get('password_min_size')]
end
if Setting.get('password_need_digit').to_i == 1 && password !~ /\d/

View file

@ -41,10 +41,10 @@ class ApplicationModel < ActiveRecord::Base
attr_accessor :history_changes_last_done
@@import_class_list = ['Ticket', 'Ticket::Article', 'History', 'Ticket::State', 'Ticket::Priority', 'Group', 'User' ]
@@import_class_list = ['Ticket', 'Ticket::Article', 'History', 'Ticket::State', 'Ticket::StateType', 'Ticket::Priority', 'Group', 'User', 'Role' ]
def check_attributes_protected
if Setting.get('import_mode') && @@import_class_list.include?( self.class.to_s )
if ( !Setting.get('system_init_done') || Setting.get('import_mode')) && @@import_class_list.include?( self.class.to_s )
# do noting, use id as it is
else
self[:id] = nil
@ -57,18 +57,28 @@ remove all not used model attributes of params
result = Model.param_cleanup(params)
for object creation, ignore id's
result = Model.param_cleanup(params, true)
returns
result = params # params with valid attributes of model
=end
def self.param_cleanup(params)
def self.param_cleanup(params, newObject = false)
if params == nil
raise "No params for #{self.to_s}!"
end
# ignore id for new objects
if newObject && params[:id]
params[:id] = nil
end
# only use object attributes
data = {}
self.new.attributes.each {|item|

View file

@ -27,7 +27,7 @@ add a new history entry for an object
:id_to => 3,
:id_from => 2,
:value_from => 'open',
:value_to => 'pending',
:value_to => 'pending reminder',
:created_by_id => 1,
:created_at => '2013-06-04 10:00:00',
:updated_at => '2013-06-04 10:00:00'

View file

@ -65,7 +65,7 @@ list of all attributes
=begin
add a new activity entry for an object
add a new attribute entry for an object
ObjectManager::Attribute.add(
:object => 'Ticket',
@ -101,7 +101,6 @@ add a new activity entry for an object
:updated_at => '2014-06-04 10:00:00',
)
=end
def self.add(data)
@ -114,8 +113,8 @@ add a new activity entry for an object
# check newest entry - is needed
result = ObjectManager::Attribute.where(
:object_lookup_id => data[:object_lookup_id],
:name => data[:name],
:object_lookup_id => data[:object_lookup_id],
:name => data[:name],
).first
if result
# raise "ERROR: attribute #{data[:name]} for #{data[:object]} already exists"
@ -129,6 +128,30 @@ add a new activity entry for an object
=begin
get the attribute model based on object and name
attribute = ObjectManager::Attribute.get(
:object => 'Ticket',
:name => 'group_id',
)
=end
def self.get(data)
# lookups
if data[:object]
data[:object_lookup_id] = ObjectLookup.by_name( data[:object] )
end
ObjectManager::Attribute.where(
:object_lookup_id => data[:object_lookup_id],
:name => data[:name],
).first
end
=begin
get user based list of object attributes
attribute_list = ObjectManager::Attribute.by_object('Ticket', user)

View file

@ -128,6 +128,9 @@ class Observer::Ticket::Notification < ActiveRecord::Observer
# return if we run import mode
return if Setting.get('import_mode')
# ignore updates on articles / we just want send notifications on ticket updates
return if record.class.name == 'Ticket::Article'
# ignore certain attributes
real_changes = {}
record.changes.each {|key, value|

View file

@ -165,13 +165,16 @@ class Observer::Ticket::Notification::BackgroundJob
attribute_name = key.to_s
object_manager_attribute = attribute_list[attribute_name]
if attribute_name[-3,3] == '_id'
attribute_name = attribute_name[ 0, attribute_name.length-3 ]
end
if key == attribute_name
changes[key] = value
attribute_name = attribute_name[ 0, attribute_name.length-3 ].to_s
end
value_id = []
# add item to changes hash
if key.to_s == attribute_name
changes[attribute_name] = value
end
# if changed item is an _id field/reference, do an lookup for the realy values
value_id = []
value_str = [ value[0], value[1] ]
if key.to_s[-3,3] == '_id'
value_id[0] = value[0]
@ -201,9 +204,16 @@ class Observer::Ticket::Notification::BackgroundJob
end
end
end
# check if we have an dedcated display name for it
display = attribute_name
if object_manager_attribute && object_manager_attribute[:display]
display = object_manager_attribute[:display]
# delete old key
changes.delete( display )
# set new key
display = object_manager_attribute[:display].to_s
end
if object_manager_attribute && object_manager_attribute[:translate]
changes[display] = ["i18n(#{value_str[0].to_s})", "i18n(#{value_str[1].to_s})"]

View file

@ -0,0 +1,30 @@
# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/
class Observer::Ticket::RefObjectTouch < ActiveRecord::Observer
observe 'ticket'
def after_create(record)
ref_object_touch(record)
end
def after_update(record)
ref_object_touch(record)
end
def after_touch(record)
ref_object_touch(record)
end
def after_destroy(record)
ref_object_touch(record)
end
def ref_object_touch(record)
# return if we run import mode
return if Setting.get('import_mode')
if record.customer
record.customer.touch
end
if record.organization
record.organization.touch
end
end
end

View file

@ -15,7 +15,7 @@ class Ticket < ApplicationModel
extend Ticket::Search
before_create :check_generate, :check_defaults, :check_title
before_update :check_defaults, :check_title
before_update :check_defaults, :check_title, :reset_pending_time
before_destroy :destroy_dependencies
notify_clients_support
@ -187,10 +187,25 @@ returns
end
end
def reset_pending_time
# ignore if no state has changed
return if !self.changes['state_id']
# check if new state isn't pending*
current_state = Ticket::State.lookup( :id => self.state_id )
current_state_type = Ticket::StateType.lookup( :id => current_state.state_type_id )
# in case, set pending_time to nil
if current_state_type.name !~ /^pending/i
self.pending_time = nil
end
end
def destroy_dependencies
# delete articles
self.articles.destroy_all
end
end
end

View file

@ -133,8 +133,9 @@ list tickets by customer groupd in state categroie open and closed
returns
result = {
:open => tickets_open,
:closed => tickets_closed,
:ticket_ids_open => tickets_open,
:ticket_ids_closed => tickets_closed,
:assets => { ...list of assets... },
}
=end
@ -150,15 +151,27 @@ returns
:customer_id => data[:customer_id],
:state_id => state_list_open
).limit( data[:limit] || 15 ).order('created_at DESC')
assets = {}
ticket_ids_open = []
tickets_open.each {|ticket|
ticket_ids_open.push ticket.id
assets = ticket.assets(assets)
}
tickets_closed = Ticket.where(
:customer_id => data[:customer_id],
:state_id => state_list_closed
).limit( data[:limit] || 15 ).order('created_at DESC')
ticket_ids_closed = []
tickets_closed.each {|ticket|
ticket_ids_closed.push ticket.id
assets = ticket.assets(assets)
}
return {
:open => tickets_open,
:closed => tickets_closed,
:ticket_ids_open => ticket_ids_open,
:ticket_ids_closed => ticket_ids_closed,
:assets => assets,
}
end

View file

@ -192,7 +192,7 @@ returns
end
# check failed logins
max_login_failed = Setting.get('password_max_login_failed') || 10
max_login_failed = Setting.get('password_max_login_failed').to_i || 10
if user && user.login_failed > max_login_failed
return false
end

View file

@ -0,0 +1,16 @@
<link rel="stylesheet" href="/assets/tests/qunit-1.10.0.css">
<script src="/assets/tests/qunit-1.10.0.js"></script>
<script src="/assets/tests/model-ui.js"></script>
<style type="text/css">
body {
padding-top: 0px;
}
</style>
<script type="text/javascript">
</script>
<div id="qunit"></div>

View file

@ -40,6 +40,7 @@ module Zammad
'observer::_ticket::_notification',
'observer::_ticket::_reset_new_state',
'observer::_ticket::_escalation_calculation',
'observer::_ticket::_ref_object_touch',
'observer::_tag::_ticket_history',
'observer::_user::_geo'

View file

@ -3,6 +3,7 @@ Zammad::Application.routes.draw do
match '/tests-core', :to => 'tests#core', :via => :get
match '/tests-ui', :to => 'tests#ui', :via => :get
match '/tests-model', :to => 'tests#model', :via => :get
match '/tests-model-ui', :to => 'tests#model_ui', :via => :get
match '/tests-form', :to => 'tests#form', :via => :get
match '/tests-form-extended', :to => 'tests#form_extended', :via => :get
match '/tests-form-validation', :to => 'tests#form_validation', :via => :get

View file

@ -463,6 +463,7 @@ class UpdateObjectManager2 < ActiveRecord::Migration
:maxlength => 100,
:null => true,
:autocomplete => 'off',
:item_class => 'formGroup--halfSize',
},
:editable => false,
:active => true,
@ -492,10 +493,11 @@ class UpdateObjectManager2 < ActiveRecord::Migration
:display => 'Note',
:data_type => 'richtext',
:data_option => {
:type => 'text',
:maxlength => 250,
:null => true,
:note => 'Notes are visible to agents only, never to customers.',
:type => 'text',
:maxlength => 250,
:null => true,
:note => 'Notes are visible to agents only, never to customers.',
# :item_class => 'formGroup--halfSize',
},
:editable => false,
:active => true,
@ -591,9 +593,8 @@ class UpdateObjectManager2 < ActiveRecord::Migration
:object => 'User',
:name => 'active',
:display => 'Active',
:data_type => 'boolean',
:data_type => 'active',
:data_option => {
:maxlength => 250,
:null => true,
:default => true,
},
@ -722,10 +723,8 @@ class UpdateObjectManager2 < ActiveRecord::Migration
:object => 'Organization',
:name => 'active',
:display => 'Active',
:data_type => 'boolean',
:data_type => 'active',
:data_option => {
:maxlength => 250,
:null => true,
:default => true,
},
:editable => false,

View file

@ -0,0 +1,44 @@
class CreateVip < ActiveRecord::Migration
def up
add_column :users, :vip, :boolean, :default => false
ObjectManager::Attribute.add(
:object => 'User',
:name => 'vip',
:display => 'VIP',
:data_type => 'boolean',
:data_option => {
:null => true,
:default => false,
:item_class => 'formGroup--halfSize',
:options => {
:false => 'no',
:true => 'yes',
},
:translate => true,
},
:editable => false,
:active => true,
:screens => {
:edit => {
:Admin => {
:null => true,
},
},
:view => {
'-all-' => {
:shown => false,
},
},
},
:pending_migration => false,
:position => 1490,
:created_by_id => 1,
:updated_by_id => 1,
)
end
def down
end
end

View file

@ -0,0 +1,70 @@
class CreateAddress < ActiveRecord::Migration
def up
add_column :users, :address, :string, :limit => 500, :null => true
User.all.each {|user|
address = ''
if user.street && !user.street.empty?
address += "#{user.street}\n"
end
if user.zip && !user.zip.empty?
address += "#{user.zip} "
end
if user.city && !user.city.empty?
address += "#{user.city}"
end
if !address.empty?
user.address = address
user.save
end
}
['street', 'zip', 'city', 'department'].each {|attribute_name|
attribute = ObjectManager::Attribute.get(
:object => 'User',
:name => attribute_name,
)
if attribute
attribute.active = false
attribute.save
end
}
ObjectManager::Attribute.add(
:object => 'User',
:name => 'address',
:display => 'Address',
:data_type => 'textarea',
:data_option => {
:type => 'text',
:maxlength => 500,
:null => true,
:item_class => 'formGroup--halfSize',
},
:editable => false,
:active => true,
:screens => {
:signup => {},
:invite_agent => {},
:edit => {
'-all-' => {
:null => true,
},
},
:view => {
'-all-' => {
:shown => true,
},
},
},
:pending_migration => false,
:position => 1350,
:created_by_id => 1,
:updated_by_id => 1,
)
end
def down
end
end

View file

@ -0,0 +1,100 @@
class UpdateObjectManager3 < ActiveRecord::Migration
def up
ObjectManager::Attribute.add(
:object => 'User',
:name => 'active',
:display => 'Active',
:data_type => 'active',
:data_option => {
:default => true,
},
:editable => false,
:active => true,
:screens => {
:signup => {},
:invite_agent => {},
:edit => {
:Admin => {
:null => false,
},
},
:view => {
'-all-' => {
:shown => false,
},
},
},
:pending_migration => false,
:position => 1800,
:created_by_id => 1,
:updated_by_id => 1,
)
ObjectManager::Attribute.add(
:object => 'Organization',
:name => 'active',
:display => 'Active',
:data_type => 'active',
:data_option => {
:default => true,
},
:editable => false,
:active => true,
:screens => {
:edit => {
:Admin => {
:null => false,
},
},
:view => {
'-all-' => {
:shown => false,
},
},
},
:pending_migration => false,
:position => 1800,
:created_by_id => 1,
:updated_by_id => 1,
)
ObjectManager::Attribute.add(
:object => 'User',
:name => 'password',
:display => 'Password',
:data_type => 'input',
:data_option => {
:type => 'password',
:maxlength => 100,
:null => true,
:autocomplete => 'off',
:item_class => 'formGroup--halfSize',
},
:editable => false,
:active => true,
:screens => {
:signup => {
'-all-' => {
:null => false,
},
},
:invite_agent => {},
:edit => {
:Admin => {
:null => true,
},
},
:view => {}
},
:pending_migration => false,
:position => 1400,
:created_by_id => 1,
:updated_by_id => 1,
)
end
def down
end
end

View file

@ -1335,13 +1335,13 @@ Link::Object.create_if_not_exists( :name => 'Question/Answer' )
Link::Object.create_if_not_exists( :name => 'Idea' )
Link::Object.create_if_not_exists( :name => 'Bug' )
Ticket::StateType.create_if_not_exists( :id => 1, :name => 'new', :updated_by_id => 1 )
Ticket::StateType.create_if_not_exists( :id => 2, :name => 'open', :updated_by_id => 1 )
Ticket::StateType.create_if_not_exists( :id => 3, :name => 'pending reminder', :updated_by_id => 1 )
Ticket::StateType.create_if_not_exists( :id => 4, :name => 'pending action', :updated_by_id => 1 )
Ticket::StateType.create_if_not_exists( :id => 5, :name => 'closed', :updated_by_id => 1 )
Ticket::StateType.create_if_not_exists( :id => 6, :name => 'merged', :updated_by_id => 1 )
Ticket::StateType.create_if_not_exists( :id => 7, :name => 'removed', :updated_by_id => 1 )
Ticket::StateType.create_if_not_exists( :id => 1, :name => 'new' )
Ticket::StateType.create_if_not_exists( :id => 2, :name => 'open' )
Ticket::StateType.create_if_not_exists( :id => 3, :name => 'pending reminder' )
Ticket::StateType.create_if_not_exists( :id => 4, :name => 'pending action' )
Ticket::StateType.create_if_not_exists( :id => 5, :name => 'closed' )
Ticket::StateType.create_if_not_exists( :id => 6, :name => 'merged' )
Ticket::StateType.create_if_not_exists( :id => 7, :name => 'removed' )
Ticket::State.create_if_not_exists( :id => 1, :name => 'new', :state_type_id => Ticket::StateType.where(:name => 'new').first.id )
Ticket::State.create_if_not_exists( :id => 2, :name => 'open', :state_type_id => Ticket::StateType.where(:name => 'open').first.id )
@ -1938,6 +1938,7 @@ Translation.create_if_not_exists( :locale => 'de', :source => "to", :target => "
Translation.create_if_not_exists( :locale => 'de', :source => "%s ago", :target => "vor %s" )
Translation.create_if_not_exists( :locale => 'de', :source => "in %s", :target => "in %s" )
Translation.create_if_not_exists( :locale => 'de', :source => "Mark all as seen.", :target => "Alle als gelesen markieren." )
Translation.create_if_not_exists( :locale => 'de', :source => "Pending till", :target => "Warten bis" )
#Translation.create_if_not_exists( :locale => 'de', :source => "", :target => "" )
# install all packages in auto_install

View file

@ -971,6 +971,12 @@ module Import::OTRS2
# check if agent already exists
user_old = User.where( :id => user_new[:id] ).first
# check if login is already used
login_in_use = User.where( "login = ? AND id != #{user_new[:id]}", user_new[:login].downcase ).count
if login_in_use > 0
user_new[:login] = "#{user_new[:login]}_#{user_new[:id]}"
end
# create / update agent
if user_old
puts "update User.find(#{user_old[:id]})"

View file

@ -80,7 +80,7 @@ module NotificationFactory
}
# translate
data[:string].gsub!( /i18n\((.+?)\)/ ) { |placeholder|
data[:string].gsub!( /i18n\((|.+?)\)/ ) { |placeholder|
string = $1
locale = data[:locale] || 'en'
placeholder = Translation.translate( locale, string )

View file

@ -486,10 +486,12 @@ test( "clone", function() {
var source = [
{ name: 'some name' },
{ name: 'some name2' },
{ fn: function() { return 'test' } },
]
var reference = [
{ name: 'some name' },
{ name: 'some name2' },
{ fn: undefined },
]
var result = clone( source )
@ -498,6 +500,33 @@ test( "clone", function() {
deepEqual( result, reference, 'clone' );
// full test
var source = [
{ name: 'some name' },
{ name: 'some name2' },
{ fn: function a() { return 'test' } },
]
var reference = [
{ name: 'some name' },
{ name: 'some name2' },
{ fn: function a() { return 'test' } },
]
var result = clone( source, true )
// modify source later, should not have any result
source[0].name = 'some new name'
source[2].fn = 'some new name'
deepEqual( result[0], reference[0], 'clone full' );
deepEqual( result[1], reference[1], 'clone full' );
equal( typeof reference[2].fn, 'function')
equal( typeof result[2].fn, 'function')
equal( reference[2].fn(), 'test')
equal( result[2].fn(), 'test')
});
// diff

View file

@ -17,7 +17,7 @@ test( "form validation check", function() {
{ name: 'richtext1', display: 'Richtext1', tag: 'richtext', maxlength: 100, null: false, type: 'richtext', multiline: true, upload: true, default: defaults['richtext1'] },
{ name: 'datetime1', display: 'Datetime1', tag: 'datetime', null: false, default: defaults['datetime1'] },
{ name: 'date1', display: 'Date1', tag: 'date', null: false, default: defaults['date1'] },
{ name: 'active1', display: 'Active1', tag: 'boolean', type: 'boolean', default: defaults['active1'], null: false },
{ name: 'active1', display: 'Active1', tag: 'active', default: defaults['active1'] },
],
},
params: defaults,
@ -248,4 +248,84 @@ test( "date validation check", function() {
equal( el.find('[data-name="date1"]').closest('.form-group').hasClass('has-error'), true, 'check date1 has-error')
equal( el.find('[data-name="date1"]').closest('.form-group').find('.help-inline').text(), '', 'check date1 error message')
});
test( "datetime selector check", function() {
$('#forms').append('<hr><h1>datetime selector check</h1><form id="form4"></form>')
var el = $('#form4')
var defaults = {}
var form = new App.ControllerForm({
el: el,
model: {
configure_attributes: [
{ name: 'datetime1', display: 'Datetime1', tag: 'datetime', null: false, default: defaults['datetime1'] },
],
},
params: defaults,
});
// check params
params = App.ControllerForm.params( el )
test_params = {
datetime1: undefined,
}
deepEqual( params, test_params, 'params check' )
el.find('.js-today').click()
// check params
timeStamp = new Date()
currentTime = timeStamp.toISOString()
currentTime = currentTime.replace(/(\d\d\.\d\d\dZ)$/, '00.000Z')
params = App.ControllerForm.params( el )
test_params = {
datetime1: currentTime,
}
deepEqual( params, test_params, 'params check' )
});
test( "date selector check", function() {
$('#forms').append('<hr><h1>date selector check</h1><form id="form5"></form>')
var el = $('#form5')
var defaults = {}
var form = new App.ControllerForm({
el: el,
model: {
configure_attributes: [
{ name: 'date1', display: 'Datet1', tag: 'date', null: false, default: defaults['date1'] },
],
},
params: defaults,
});
// check params
params = App.ControllerForm.params( el )
test_params = {
date1: undefined,
}
deepEqual( params, test_params, 'params check' )
el.find('.js-today').click()
// check params
format = function (number) {
if ( parseInt(number) < 10 ) {
number = '0' + number.toString()
}
return number
}
timeStamp = new Date()
currentTime = timeStamp.getFullYear() + '-' + format(timeStamp.getMonth()+1) + '-' + format(timeStamp.getDate())
params = App.ControllerForm.params( el )
test_params = {
date1: currentTime,
}
deepEqual( params, test_params, 'params check' )
});

View file

@ -159,8 +159,8 @@ test( "form params check", function() {
{ name: 'date2', display: 'Date2', tag: 'date', null: true, default: defaults['date2'] },
{ name: 'date3', display: 'Date3', tag: 'date', null: false, default: defaults['date3'] },
{ name: 'date4', display: 'Date4', tag: 'date', null: false, default: defaults['date4'] },
{ name: 'active1', display: 'Active1', tag: 'boolean', type: 'boolean', default: defaults['active1'], null: false },
{ name: 'active2', display: 'Active2', tag: 'boolean', type: 'boolean', default: defaults['active2'], null: false },
{ name: 'active1', display: 'Active1', tag: 'active', default: defaults['active1'] },
{ name: 'active2', display: 'Active2', tag: 'active', default: defaults['active2'] },
],
},
params: defaults,
@ -795,7 +795,7 @@ test( "form required_if + shown_if", function() {
{ name: 'input2', display: 'Input2', tag: 'input', type: 'text', limit: 100, null: true, default: 'some used default', required_if: { active: true }, shown_if: { active: true } },
{ name: 'input3', display: 'Input3', tag: 'input', type: 'text', limit: 100, null: true, default: 'some used default', required_if: { active: [true,false] }, shown_if: { active: [true,false] } },
{ name: 'input4', display: 'Input4', tag: 'input', type: 'text', limit: 100, null: true, default: 'some used default', required_if: { active: [55,66] }, shown_if: { active: [55,66] } },
{ name: 'active', display: 'Active', tag: 'boolean', type: 'boolean', 'default': true, null: false },
{ name: 'active', display: 'Active', tag: 'active', 'default': true },
],
},
params: defaults,

View file

@ -0,0 +1,87 @@
window.onload = function() {
// model
test( "model ui basic tests", function() {
// load ref object
App.Collection.loadAssets({
TicketState: {
1: {
name: 'new', id: 1, updated_at: "2014-11-07T23:43:08.000Z",
},
2: {
name: 'open', id: 2, updated_at: "2014-11-07T23:43:08.000Z",
},
3: {
name: 'closed <>&', id: 3, updated_at: "2014-11-07T23:43:08.000Z",
},
},
})
// create ticket
var attribute1 = {
name: 'date', display: 'date 1', tag: 'date', null: true
};
App.Ticket.configure_attributes.push( attribute1 )
var attribute2 = {
name: 'textarea', display: 'textarea 1', tag: 'textarea', null: true
};
App.Ticket.configure_attributes.push( attribute2 )
var ticket = new App.Ticket()
ticket.load({
id: 1000,
title: 'some title <>&',
state_id: 2,
updated_at: '2014-11-07T23:43:08.000Z',
date: '2015-02-07',
textarea: "some new\nline"
})
App.i18n.set('en')
equal( App.viewPrint( ticket, 'id' ), 1000)
equal( App.viewPrint( ticket, 'title' ), 'some title &lt;&gt;&amp;')
equal( App.viewPrint( ticket, 'state' ), 'open')
equal( App.viewPrint( ticket, 'state_id' ), 'open')
equal( App.viewPrint( ticket, 'not_existing' ), '-')
equal( App.viewPrint( ticket, 'updated_at' ), "<span class=\"humanTimeFromNow undefined\" data-time=\"2014-11-07T23:43:08.000Z\">?</span>")
equal( App.viewPrint( ticket, 'date' ), '2015-02-07')
equal( App.viewPrint( ticket, 'textarea' ), '<div>some new</div><div>line</div>')
App.i18n.set('de')
equal( App.viewPrint( ticket, 'id' ), 1000)
equal( App.viewPrint( ticket, 'title' ), 'some title &lt;&gt;&amp;')
equal( App.viewPrint( ticket, 'state' ), 'offen')
equal( App.viewPrint( ticket, 'state_id' ), 'offen')
equal( App.viewPrint( ticket, 'not_existing' ), '-')
equal( App.viewPrint( ticket, 'updated_at' ), "<span class=\"humanTimeFromNow undefined\" data-time=\"2014-11-07T23:43:08.000Z\">?</span>")
equal( App.viewPrint( ticket, 'date' ), '07.02.2015')
equal( App.viewPrint( ticket, 'textarea' ), '<div>some new</div><div>line</div>')
App.i18n.set('en')
ticket.state_id = 3
equal( App.viewPrint( ticket, 'state' ), 'closed &lt;&gt;&amp;')
equal( App.viewPrint( ticket, 'state_id' ), 'closed &lt;&gt;&amp;')
App.i18n.set('de')
equal( App.viewPrint( ticket, 'state' ), 'closed &lt;&gt;&amp;')
equal( App.viewPrint( ticket, 'state_id' ), 'closed &lt;&gt;&amp;')
// normal string
data = {
a: 1,
b: 'abc',
c: {
displayName: function() { return "my displayName <>&" }
},
}
equal( App.viewPrint( data, 'a' ), 1)
equal( App.viewPrint( data, 'b' ), 'abc')
equal( App.viewPrint( data, 'c' ), 'my displayName &lt;&gt;&amp;')
});
}

View file

@ -410,14 +410,13 @@ test( "table test 2", function() {
equal( el.find('tbody > tr:nth-child(1) > td:nth-child(1)').text().trim(), 'adapter1', 'check row 1')
equal( el.find('tbody > tr:nth-child(1) > td:nth-child(2)').text().trim(), 'host1', 'check row 1')
equal( el.find('tbody > tr:nth-child(1) > td:nth-child(3)').text().trim(), 'user1', 'check row 1')
equal( el.find('tbody > tr:nth-child(1) > td:nth-child(4)').text().trim(), 'true', 'check row 1')
equal( el.find('tbody > tr:nth-child(1) > td:nth-child(4)').text().trim(), 'true', 'check row 1')
equal( el.find('tbody > tr:nth-child(1) > td:nth-child(4)').text().trim(), 'ja', 'check row 1')
equal( el.find('tbody > tr:nth-child(1) > td:nth-child(5)').text().trim(), '', 'check row 1')
equal( el.find('tbody > tr:nth-child(2) > td').length, 5, 'check row 2')
equal( el.find('tbody > tr:nth-child(2) > td:nth-child(1)').text().trim(), 'adapter2', 'check row 2')
equal( el.find('tbody > tr:nth-child(2) > td:nth-child(2)').text().trim(), 'host2', 'check row 2')
equal( el.find('tbody > tr:nth-child(2) > td:nth-child(3)').text().trim(), 'user2', 'check row 2')
equal( el.find('tbody > tr:nth-child(2) > td:nth-child(4)').text().trim(), 'true', 'check row 2')
equal( el.find('tbody > tr:nth-child(2) > td:nth-child(4)').text().trim(), 'ja', 'check row 2')
equal( el.find('tbody > tr:nth-child(2) > td:nth-child(5)').text().trim(), '', 'check row 2')
});

View file

@ -68,6 +68,28 @@ class AAbUnitTest < TestCase
]
browser_single_test(tests)
end
def test_model_ui
tests = [
{
:name => 'start',
:instance => browser_instance,
:url => browser_url + '/tests-model-ui',
:action => [
{
:execute => 'wait',
:value => 8,
},
{
:execute => 'match',
:css => '.result .failed',
:value => '0',
:match_result => true,
},
],
},
]
browser_single_test(tests)
end
def test_form
tests = [
{

View file

@ -0,0 +1,188 @@
# encoding: utf-8
require 'browser_test_helper'
class AgentOrganizationProfileTest < TestCase
def test_search_and_edit_verify_in_second
message = 'comment 1 ' + rand(99999999999999999).to_s
tests = [
{
:name => 'start',
:instance1 => browser_instance,
:instance2 => browser_instance,
:instance1_username => 'master@example.com',
:instance1_password => 'test',
:instance2_username => 'agent1@example.com',
:instance2_password => 'test',
:url => browser_url,
:action => [
{
:where => :instance1,
:execute => 'close_all_tasks',
},
{
:where => :instance2,
:execute => 'close_all_tasks',
},
{
:where => :instance1,
:execute => 'search_organization',
:term => 'Zammad Foundation',
},
{
:where => :instance2,
:execute => 'search_organization',
:term => 'Zammad Foundation',
},
# update note
{
:where => :instance1,
:execute => 'set',
:css => '.active [data-name="note"]',
:value => message,
},
{
:where => :instance1,
:execute => 'click',
:css => '.active .profile',
},
{
:where => :instance1,
:execute => 'wait',
:value => 3,
},
# verify
{
:where => :instance2,
:execute => 'match',
:css => '.active .profile-window',
:value => message,
:match_result => true,
},
],
},
]
browser_double_test(tests)
end
def test_search_and_edit_in_one
message = '1 ' + rand(99999999).to_s
tests = [
{
:name => 'search and edit',
:action => [
{
:execute => 'close_all_tasks',
},
# search and open org
{
:execute => 'search_organization',
:term => 'Zammad Foundation',
},
{
:execute => 'match',
:css => '.active .profile-window',
:value => 'note',
:match_result => true,
},
{
:execute => 'match',
:css => '.active .profile-window',
:value => 'member',
:match_result => true,
},
# update note
{
:execute => 'set',
:css => '.active [data-name="note"]',
:value => 'some note 123'
},
{
:execute => 'click',
:css => '.active .profile',
},
{
:execute => 'wait',
:value => 1,
},
# check and change note again in edit screen
{
:execute => 'click',
:css => '.active .js-action .select-arrow',
},
{
:execute => 'click',
:css => '.active .js-action a[data-type="edit"]',
},
{
:execute => 'wait',
:value => 1,
},
{
:execute => 'match',
:css => '.active .modal',
:value => 'note',
:match_result => true,
},
{
:execute => 'match',
:css => '.active .modal',
:value => 'some note 123',
:match_result => true,
},
{
:execute => 'set',
:css => '.active .modal [data-name="note"]',
:value => 'some note abc'
},
{
:execute => 'click',
:css => '.active .modal button.js-submit',
},
{
:execute => 'wait',
:value => 4,
},
{
:execute => 'match',
:css => '.active .profile-window',
:value => 'some note abc',
:match_result => true,
},
# create new ticket
{
:execute => 'create_ticket',
:group => 'Users',
:subject => 'org profile check ' + message,
:body => 'org profile check ' + message,
},
{
:execute => 'wait',
:value => 4,
},
# switch to org tab, verify if ticket is shown
{
:execute => 'search_organization',
:term => 'Zammad Foundation',
},
{
:execute => 'match',
:css => '.active .profile-window',
:value => 'org profile check ' + message,
:match_result => true,
},
],
},
]
browser_signle_test_with_login(tests, { :username => 'master@example.com' })
end
end

View file

@ -0,0 +1,188 @@
# encoding: utf-8
require 'browser_test_helper'
class AgentUserProfileTest < TestCase
def test_search_and_edit_verify_in_second
message = 'comment 1 ' + rand(99999999999999999).to_s
tests = [
{
:name => 'start',
:instance1 => browser_instance,
:instance2 => browser_instance,
:instance1_username => 'master@example.com',
:instance1_password => 'test',
:instance2_username => 'agent1@example.com',
:instance2_password => 'test',
:url => browser_url,
:action => [
{
:where => :instance1,
:execute => 'close_all_tasks',
},
{
:where => :instance2,
:execute => 'close_all_tasks',
},
{
:where => :instance1,
:execute => 'search_user',
:term => 'Braun',
},
{
:where => :instance2,
:execute => 'search_user',
:term => 'Braun',
},
# update note
{
:where => :instance1,
:execute => 'set',
:css => '.active [data-name="note"]',
:value => message,
},
{
:where => :instance1,
:execute => 'click',
:css => '.active .profile',
},
{
:where => :instance1,
:execute => 'wait',
:value => 3,
},
# verify
{
:where => :instance2,
:execute => 'match',
:css => '.active .profile-window',
:value => message,
:match_result => true,
},
],
},
]
browser_double_test(tests)
end
def test_search_and_edit_in_one
message = '1 ' + rand(99999999).to_s
tests = [
{
:name => 'search and edit',
:action => [
{
:execute => 'close_all_tasks',
},
# search and open user
{
:execute => 'search_user',
:term => 'Braun',
},
{
:execute => 'match',
:css => '.active .profile-window',
:value => 'note',
:match_result => true,
},
{
:execute => 'match',
:css => '.active .profile-window',
:value => 'email',
:match_result => true,
},
# update note
{
:execute => 'set',
:css => '.active [data-name="note"]',
:value => 'some note 123'
},
{
:execute => 'click',
:css => '.active .profile',
},
{
:execute => 'wait',
:value => 1,
},
# check and change note again in edit screen
{
:execute => 'click',
:css => '.active .js-action .select-arrow',
},
{
:execute => 'click',
:css => '.active .js-action a[data-type="edit"]',
},
{
:execute => 'wait',
:value => 1,
},
{
:execute => 'match',
:css => '.active .modal',
:value => 'note',
:match_result => true,
},
{
:execute => 'match',
:css => '.active .modal',
:value => 'some note 123',
:match_result => true,
},
{
:execute => 'set',
:css => '.active .modal [data-name="note"]',
:value => 'some note abc'
},
{
:execute => 'click',
:css => '.active .modal button.js-submit',
},
{
:execute => 'wait',
:value => 4,
},
{
:execute => 'match',
:css => '.active .profile-window',
:value => 'some note abc',
:match_result => true,
},
# create new ticket
{
:execute => 'create_ticket',
:group => 'Users',
:subject => 'user profile check ' + message,
:body => 'user profile check ' + message,
},
{
:execute => 'wait',
:value => 4,
},
# switch to org tab, verify if ticket is shown
{
:execute => 'search_user',
:term => 'Braun',
},
{
:execute => 'match',
:css => '.active .profile-window',
:value => 'user profile check ' + message,
:match_result => true,
},
],
},
]
browser_signle_test_with_login(tests, { :username => 'master@example.com' })
end
end

View file

@ -525,6 +525,55 @@ class TestCase < Test::Unit::TestCase
}
assert( false, "(#{test[:name]}) ticket creation failed, can't get zoom url" )
return
elsif action[:execute] == 'search_user'
element = instance.find_elements( { :css => '#global-search' } )[0]
element.click
element.clear
if @stack
action[:term].gsub! '###stack###', @stack
end
element.send_keys( action[:term] )
sleep 3
element = instance.find_element( { :partial_link_text => action[:term] } ).click
name = instance.find_elements( { :css => '.active h1' } )[0].text
if name !~ /#{action[:term]}/
assert( false, "(#{test[:name]}) unable to search/find user #{action[:term]}!" )
return
end
assert( true, "(#{test[:name]}) user #{action[:term]} found" )
return
elsif action[:execute] == 'search_organization'
element = instance.find_elements( { :css => '#global-search' } )[0]
element.click
element.clear
if @stack
action[:term].gsub! '###stack###', @stack
end
element.send_keys( action[:term] )
sleep 3
instance.find_elements( { :css => '.search .empty-search' } )[0].click
sleep 0.5
text = instance.find_elements( { :css => '#global-search' } )[0].attribute('value')
if !text
assert( false, "(#{test[:name]}) #global-search is not empty!" )
return
end
element = instance.find_elements( { :css => '#global-search' } )[0]
element.click
element.clear
if @stack
action[:term].gsub! '###stack###', @stack
end
element.send_keys( action[:term] )
sleep 2
element = instance.find_element( { :partial_link_text => action[:term] } ).click
name = instance.find_elements( { :css => '.active h1' } )[0].text
if name !~ /#{action[:term]}/
assert( false, "(#{test[:name]}) unable to search/find org #{action[:term]}!" )
return
end
assert( true, "(#{test[:name]}) org #{action[:term]} found" )
return
elsif action[:execute] == 'search_ticket'
element = instance.find_elements( { :css => '#global-search' } )[0]
element.click
@ -544,7 +593,7 @@ class TestCase < Test::Unit::TestCase
element.clear
action[:number].gsub! '###stack###', @stack
element.send_keys( action[:number] )
sleep 3
sleep 2
element = instance.find_element( { :partial_link_text => action[:number] } ).click
number = instance.find_elements( { :css => '.active .page-header .ticket-number' } )[0].text
if number !~ /#{action[:number]}/
@ -684,14 +733,14 @@ class TestCase < Test::Unit::TestCase
match = false
if action[:no_quote]
#puts "aaaa #{text}/#{action[:value]}"
if text =~ /#{action[:value]}/
if text =~ /#{action[:value]}/i
if $1
@stack = $1
end
match = $1 || true
end
else
if text =~ /#{Regexp.quote(action[:value])}/
if text =~ /#{Regexp.quote(action[:value])}/i
match = true
end
end

View file

@ -0,0 +1,56 @@
# encoding: utf-8
require 'test_helper'
class DbAutoIncrementTest < ActiveSupport::TestCase
test 'id overwrite' do
setting_backup = Setting.get('system_init_done')
Setting.set('system_init_done', false)
Ticket::StateType.create_if_not_exists( :id => 200, :name => 'unit test 1', :updated_by_id => 1, :created_by_id => 1 )
state_type = Ticket::StateType.where( :name => 'unit test 1' ).first
assert_equal( Ticket::StateType.to_s, state_type.class.to_s )
assert_equal( 'unit test 1', state_type.name )
Ticket::StateType.create_if_not_exists( :id => 200, :name => 'unit test 1 _ should not be created', :updated_by_id => 1, :created_by_id => 1 )
state_type = Ticket::StateType.where( :id => 200 ).first
assert_equal( Ticket::StateType.to_s, state_type.class.to_s )
assert_equal( 'unit test 1', state_type.name )
Ticket::StateType.create_or_update( :id => 200, :name => 'unit test 1 _ should be updated', :updated_by_id => 1, :created_by_id => 1 )
state_type = Ticket::StateType.where( :name => 'unit test 1 _ should be updated' ).first
assert_equal( Ticket::StateType.to_s, state_type.class.to_s )
assert_equal( 'unit test 1 _ should be updated', state_type.name )
state_type = Ticket::StateType.where( :id => 200 ).first
assert_equal( Ticket::StateType.to_s, state_type.class.to_s )
assert_equal( 'unit test 1 _ should be updated', state_type.name )
Ticket::State.create_if_not_exists( :id => 210, :name => 'unit test 1', :state_type_id => Ticket::StateType.where(:name => 'unit test 1 _ should be updated').first.id, :updated_by_id => 1, :created_by_id => 1 )
state = Ticket::State.where( :name => 'unit test 1' ).first
assert_equal( Ticket::State.to_s, state.class.to_s )
assert_equal( 'unit test 1', state.name )
Ticket::State.create_if_not_exists( :id => 210, :name => 'unit test 1 _ should not be created', :state_type_id => Ticket::StateType.where(:name => 'unit test 1 _ should be updated').first.id, :updated_by_id => 1, :created_by_id => 1 )
state = Ticket::State.where( :id => 210 ).first
assert_equal( Ticket::State.to_s, state.class.to_s )
assert_equal( 'unit test 1', state.name )
Ticket::State.create_or_update( :id => 210, :name => 'unit test 1 _ should be updated', :state_type_id => Ticket::StateType.where(:name => 'unit test 1 _ should be updated').first.id, :updated_by_id => 1, :created_by_id => 1 )
state = Ticket::State.where( :name => 'unit test 1 _ should be updated' ).first
assert_equal( Ticket::State.to_s, state.class.to_s )
assert_equal( 'unit test 1 _ should be updated', state.name )
state = Ticket::State.where( :id => 210 ).first
assert_equal( Ticket::State.to_s, state.class.to_s )
assert_equal( 'unit test 1 _ should be updated', state.name )
Setting.set('system_init_done', setting_backup)
end
end

View file

@ -126,6 +126,21 @@ class NotificationFactoryTest < ActiveSupport::TestCase
:string => '\#{puts `ls`}',
:result => '\#{puts `ls`} (not allowed)',
},
{
:locale => 'de',
:string => 'test i18n(new)',
:result => 'test neu',
},
{
:locale => 'de',
:string => 'test i18n()',
:result => 'test ',
},
{
:locale => 'de',
:string => 'test i18n(new) i18n(open)',
:result => 'test neu offen',
},
]
tests.each { |test|
result = NotificationFactory.build(

View file

@ -288,6 +288,20 @@ class TicketNotificationTest < ActiveSupport::TestCase
assert_equal( 0, notification_check(ticket3, agent2), ticket3.id )
# update article / not notification should be sent
article_inbound.internal = true
article_inbound.save
# execute ticket events
Observer::Ticket::Notification.transaction
#puts Delayed::Job.all.inspect
Delayed::Worker.new.work_off
# verify notifications not to agent1 and not to agent2
assert_equal( 2, notification_check(ticket3, agent1), ticket3.id )
assert_equal( 0, notification_check(ticket3, agent2), ticket3.id )
delete = ticket1.destroy
assert( delete, "ticket1 destroy" )
@ -392,16 +406,22 @@ class TicketNotificationTest < ActiveSupport::TestCase
:article_id => article.id,
:type => 'update',
:changes => {
:priority_id => [1, 2],
'priority_id' => [1, 2],
'pending_time' => [nil, Time.parse("2015-01-11 23:33:47 UTC")],
},
)
# check changed attributes
human_changes = bg.human_changes(agent1,ticket1)
assert( human_changes['Priority'], 'Check if attributes translated based on ObjectManager::Attribute' )
assert( human_changes['Pending till'], 'Check if attributes translated based on ObjectManager::Attribute' )
assert_equal( 'i18n(1 low)', human_changes['Priority'][0] )
assert_equal( 'i18n(2 normal)', human_changes['Priority'][1] )
assert_equal( 'i18n()', human_changes['Pending till'][0] )
assert_equal( 'i18n(2015-01-11 23:33:47 UTC)', human_changes['Pending till'][1] )
assert_not( human_changes['priority_id'] )
assert_not( human_changes['pending_time'] )
assert_not( human_changes['pending_till'] )
# en template
template = bg.template_update(agent2, ticket1, article, human_changes)
@ -410,6 +430,8 @@ class TicketNotificationTest < ActiveSupport::TestCase
assert_match( /Priority/, template[:body] )
assert_match( /1 low/, template[:body] )
assert_match( /2 normal/, template[:body] )
assert_match( /Pending till/, template[:body] )
assert_match( /2015-01-11 23:33:47 UTC/, template[:body] )
assert_match( /updated/i, template[:subject] )
# en notification
@ -435,7 +457,11 @@ class TicketNotificationTest < ActiveSupport::TestCase
assert_match( /Priority/, body )
assert_match( /1 low/, body )
assert_match( /2 normal/, body )
assert_match( /Pending till/, body )
assert_match( /2015-01-11 23:33:47 UTC/, body )
assert_match( /update/, body )
assert_no_match( /pending_till/, body )
assert_no_match( /i18n/, body )
# de template
template = bg.template_update(agent1, ticket1, article, human_changes)
@ -444,6 +470,8 @@ class TicketNotificationTest < ActiveSupport::TestCase
assert_match( /Priority/, template[:body] )
assert_match( /1 low/, template[:body] )
assert_match( /2 normal/, template[:body] )
assert_match( /Pending till/, template[:body] )
assert_match( /2015-01-11 23:33:47 UTC/, template[:body] )
assert_match( /aktualis/, template[:subject] )
# de notification
@ -470,7 +498,45 @@ class TicketNotificationTest < ActiveSupport::TestCase
assert_match( /Priorität/, body )
assert_match( /1 niedrig/, body )
assert_match( /2 normal/, body )
assert_match( /Warten/, body )
assert_match( /2015-01-11 23:33:47 UTC/, body )
assert_match( /aktualis/, body )
assert_no_match( /pending_till/, body )
assert_no_match( /i18n/, body )
bg = Observer::Ticket::Notification::BackgroundJob.new(
:ticket_id => ticket1.id,
:article_id => article.id,
:type => 'update',
:changes => {
:title => ['some notification template test 1', 'some notification template test 1 #2'],
:priority_id => [2, 3],
},
)
puts "hc #{human_changes.inspect}"
# check changed attributes
human_changes = bg.human_changes(agent1,ticket1)
assert( human_changes['Title'], 'Check if attributes translated based on ObjectManager::Attribute' )
assert( human_changes['Priority'], 'Check if attributes translated based on ObjectManager::Attribute' )
assert_equal( 'i18n(2 normal)', human_changes['Priority'][0] )
assert_equal( 'i18n(3 high)', human_changes['Priority'][1] )
assert_equal( 'some notification template test 1', human_changes['Title'][0] )
assert_equal( 'some notification template test 1 #2', human_changes['Title'][1] )
assert_not( human_changes['priority_id'] )
assert_not( human_changes['pending_time'] )
assert_not( human_changes['pending_till'] )
human_changes = bg.human_changes(agent2,ticket1)
puts "hc2 #{human_changes.inspect}"
template = bg.template_update(agent1, ticket1, article, human_changes)
puts "t1 #{template.inspect}"
template = bg.template_update(agent2, ticket1, article, human_changes)
puts "t2 #{template.inspect}"
end

View file

@ -0,0 +1,161 @@
# encoding: utf-8
require 'test_helper'
class TicketRefObjectTouchTest < ActiveSupport::TestCase
# create base
groups = Group.where( :name => 'Users' )
roles = Role.where( :name => 'Agent' )
agent1 = User.create_or_update(
:login => 'ticket-ref-object-update-agent1@example.com',
:firstname => 'Notification',
:lastname => 'Agent1',
:email => 'ticket-ref-object-update-agent1@example.com',
:password => 'agentpw',
:active => true,
:roles => roles,
:groups => groups,
:updated_at => '2015-02-05 16:37:00',
:updated_by_id => 1,
:created_by_id => 1,
)
roles = Role.where( :name => 'Customer' )
organization1 = Organization.create_if_not_exists(
:name => 'Ref Object Update Org',
:updated_at => '2015-02-05 16:37:00',
:updated_by_id => 1,
:created_by_id => 1,
)
customer1 = User.create_or_update(
:login => 'ticket-ref-object-update-customer1@example.com',
:firstname => 'Notification',
:lastname => 'Agent1',
:email => 'ticket-ref-object-update-customer1@example.com',
:password => 'customerpw',
:active => true,
:organization_id => organization1.id,
:roles => roles,
:updated_at => '2015-02-05 16:37:00',
:updated_by_id => 1,
:created_by_id => 1,
)
customer2 = User.create_or_update(
:login => 'ticket-ref-object-update-customer2@example.com',
:firstname => 'Notification',
:lastname => 'Agent2',
:email => 'ticket-ref-object-update-customer2@example.com',
:password => 'customerpw',
:active => true,
:organization_id => nil,
:roles => roles,
:updated_at => '2015-02-05 16:37:00',
:updated_by_id => 1,
:created_by_id => 1,
)
test 'a - check if customer and organization has been updated' do
ticket = Ticket.create(
:title => "some title1\n äöüß",
:group => Group.lookup( :name => 'Users'),
:customer_id => customer1.id,
:owner_id => agent1.id,
:state => Ticket::State.lookup( :name => 'new' ),
:priority => Ticket::Priority.lookup( :name => '2 normal' ),
:updated_by_id => 1,
:created_by_id => 1,
)
assert( ticket, "ticket created" )
assert_equal( ticket.customer.id, customer1.id )
assert_equal( ticket.organization.id, organization1.id )
# check if customer and organization has been touched
customer1 = User.find(customer1.id)
if customer1.updated_at > 2.second.ago
assert( true, "customer1.updated_at has been updated" )
else
assert( false, "customer1.updated_at has not been updated" )
end
organization1 = Organization.find(organization1.id)
if organization1.updated_at > 2.second.ago
assert( true, "organization1.updated_at has been updated" )
else
assert( false, "organization1.updated_at has not been updated" )
end
sleep 4
delete = ticket.destroy
assert( delete, "ticket destroy" )
# check if customer and organization has been touched
customer1 = User.find(customer1.id)
if customer1.updated_at > 2.second.ago
assert( true, "customer1.updated_at has been updated" )
else
assert( false, "customer1.updated_at has not been updated" )
end
organization1 = Organization.find(organization1.id)
if organization1.updated_at > 2.second.ago
assert( true, "organization1.updated_at has been updated" )
else
assert( false, "organization1.updated_at has not been updated" )
end
end
test 'b - check if customer (not organization) has been updated' do
sleep 3
ticket = Ticket.create(
:title => "some title2\n äöüß",
:group => Group.lookup( :name => 'Users'),
:customer_id => customer2.id,
:owner_id => agent1.id,
:state => Ticket::State.lookup( :name => 'new' ),
:priority => Ticket::Priority.lookup( :name => '2 normal' ),
:updated_by_id => 1,
:created_by_id => 1,
)
assert( ticket, "ticket created" )
assert_equal( ticket.customer.id, customer2.id )
assert_equal( ticket.organization, nil )
# check if customer and organization has been touched
customer2 = User.find(customer2.id)
if customer2.updated_at > 2.second.ago
assert( true, "customer2.updated_at has been updated" )
else
assert( false, "customer2.updated_at has not been updated" )
end
organization1 = Organization.find(organization1.id)
if organization1.updated_at > 2.second.ago
assert( false, "organization1.updated_at has been updated" )
else
assert( true, "organization1.updated_at has not been updated" )
end
sleep 3
delete = ticket.destroy
assert( delete, "ticket destroy" )
# check if customer and organization has been touched
customer2 = User.find(customer2.id)
if customer2.updated_at > 2.second.ago
assert( true, "customer2.updated_at has been updated" )
else
assert( false, "customer2.updated_at has not been updated" )
end
organization1 = Organization.find(organization1.id)
if organization1.updated_at > 2.second.ago
assert( false, "organization1.updated_at has been updated" )
else
assert( true, "organization1.updated_at has not been updated" )
end
end
end

View file

@ -101,6 +101,25 @@ class TicketTest < ActiveSupport::TestCase
assert( ticket.close_time, 'ticket.close_time verify - state update' )
# set pending time
ticket.state_id = Ticket::State.where(:name => 'pending reminder').first.id
ticket.pending_time = Time.parse("1977-10-27 22:00:00 +0000")
ticket.save
ticket = Ticket.find(ticket.id)
assert_equal( ticket.state.name, 'pending reminder', 'state verify' )
assert_equal( ticket.pending_time, Time.parse("1977-10-27 22:00:00 +0000"), 'pending_time verify' )
# reset pending state, should also reset pending time
ticket.state_id = Ticket::State.where(:name => 'closed').first.id
ticket.save
ticket = Ticket.find(ticket.id)
assert_equal( ticket.state.name, 'closed', 'state verify' )
assert_equal( ticket.pending_time, nil )
delete = ticket.destroy
assert( delete, "ticket destroy" )
end