Implemented server push if ticket, article or user has changed.

This commit is contained in:
Martin Edenhofer 2013-06-19 02:40:42 +02:00
parent 8fd45e7297
commit ff5d44bd24
31 changed files with 111 additions and 50 deletions

View file

@ -1,8 +1,15 @@
class App.Controller extends Spine.Controller
@include App.Log
constructor: ->
constructor: (params) ->
# unbind old bindlings
if params && params.el && params.el.unbind
params.el.unbind()
super
# create shortcuts
@Config = App.Config
@Session = App.Session

View file

@ -20,7 +20,21 @@ class App.TicketZoom extends App.Controller
@load(cache)
update = =>
@fetch( @ticket_id, false )
@interval( update, 30000, @key, 'ticket_zoom' )
@interval( update, 120000, @key, 'ticket_zoom' )
# fetch new data if triggered
App.Event.bind(
'ticket:updated'
(data) =>
update = =>
if data.id.toString() is @ticket_id.toString()
ticket = App.Collection.find( 'Ticket', @ticket_id )
console.log('TRY', data.updated_at, ticket.updated_at)
if data.updated_at isnt ticket.updated_at
@fetch( @ticket_id, false )
@delay( update, 2000, 'ticket-zoom-' + @ticket_id )
'ticket-zoom-' + @ticket_id
)
meta: =>
return if !@ticket
@ -43,6 +57,7 @@ class App.TicketZoom extends App.Controller
return true
release: =>
App.Event.unbindLevel 'ticket-zoom-' + @ticket_id
@clearInterval( @key, 'ticket_zoom' )
@el.remove()
@ -75,13 +90,12 @@ class App.TicketZoom extends App.Controller
# return if ticket hasnt changed
return if _.isEqual( @dataLastCall.ticket, data.ticket )
# return if ticket changed by my self
return if data.ticket.updated_by_id is @Session.all().id
# trigger task notify
diff = difference( @dataLastCall.ticket, data.ticket )
console.log('diff', diff)
if !_.isEmpty(diff)
# notify if ticket changed not by my self
if !_.isEmpty(diff) && data.ticket.updated_by_id isnt @Session.all().id
App.TaskManager.notify( @task_key )
# remember current data
@ -601,7 +615,7 @@ class ArticleView extends App.Controller
#@ui.el.find('[name="cc"]').val('')
#@ui.el.find('[name="subject"]').val('')
@ui.el.find('[name="in_reply_to"]').val('')
console.log('repl2', article_type.name)
if article.message_id
@ui.el.find('[name="in_reply_to"]').val(article.message_id)

View file

@ -1,13 +1,16 @@
class App.Model extends Spine.Model
@destroyBind: false
constructor: ->
super
# delete object from local storage on destroy
@bind( 'destroy', (e) ->
className = Object.getPrototypeOf(e).constructor.className
key = "collection::#{className}::#{e.id}"
App.Store.delete(key)
)
if !@constructor.destroyBind
@bind( 'destroy', (e) ->
className = Object.getPrototypeOf(e).constructor.className
key = "collection::#{className}::#{e.id}"
App.Store.delete(key)
)
displayName: ->
return @name if @name

View file

@ -1,4 +1,4 @@
class App.Channel extends App.Model
@configure 'Channel', 'adapter', 'area', 'options', 'group_id', 'active'
@configure 'Channel', 'adapter', 'area', 'options', 'group_id', 'active', 'updated_at'
@extend Spine.Model.Ajax
@url: 'api/channels'

View file

@ -1,5 +1,5 @@
class App.EmailAddress extends App.Model
@configure 'EmailAddress', 'realname', 'email', 'note', 'active'
@configure 'EmailAddress', 'realname', 'email', 'note', 'active', 'updated_at'
@extend Spine.Model.Ajax
@url: 'api/email_addresses'

View file

@ -1,5 +1,5 @@
class App.Group extends App.Model
@configure 'Group', 'name', 'assignment_timeout', 'follow_up_possible', 'follow_up_assignment', 'email_address_id', 'signature_id', 'note', 'active'
@configure 'Group', 'name', 'assignment_timeout', 'follow_up_possible', 'follow_up_assignment', 'email_address_id', 'signature_id', 'note', 'active', 'updated_at'
@extend Spine.Model.Ajax
@url: 'api/groups'

View file

@ -1,5 +1,5 @@
class App.Network extends App.Model
@configure 'Network', 'name', 'note', 'active'
@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' },

View file

@ -1,3 +1,3 @@
class App.NetworkCategory extends App.Model
@configure 'NetworkCategory', 'name', 'network_id', 'network_category_type_id', 'network_privacy_id', 'note', 'allow_comments', 'active'
@configure 'NetworkCategory', 'name', 'network_id', 'network_category_type_id', 'network_privacy_id', 'note', 'allow_comments', 'active', 'updated_at'
@extend Spine.Model.Ajax

View file

@ -1,3 +1,3 @@
class App.NetworkCategoryType extends App.Model
@configure 'NetworkCategoryType', 'name', 'note', 'active'
@configure 'NetworkCategoryType', 'name', 'note', 'active', 'updated_at'
@extend Spine.Model.Ajax

View file

@ -1,3 +1,3 @@
class App.NetworkPrivacy extends App.Model
@configure 'NetworkPrivacy', 'name', 'key'
@configure 'NetworkPrivacy', 'name', 'key', 'updated_at'
@extend Spine.Model.Ajax

View file

@ -1,5 +1,5 @@
class App.Organization extends App.Model
@configure 'Organization', 'name', 'shared', 'note', 'active'
@configure 'Organization', 'name', 'shared', 'note', 'active', 'updated_at'
@extend Spine.Model.Ajax
@url: 'api/organizations'
@configure_attributes = [

View file

@ -1,5 +1,5 @@
class App.Overview extends Spine.Model
@configure 'Overview', 'name', 'link', 'prio', 'condition', 'order', 'group_by', 'view', 'user_id', 'organization_shared', 'role_id', 'order', 'group_by', 'active'
@configure 'Overview', 'name', 'link', 'prio', 'condition', 'order', 'group_by', 'view', 'user_id', 'organization_shared', 'role_id', 'order', 'group_by', 'active', 'updated_at'
@extend Spine.Model.Ajax
@url: 'api/overviews'
@configure_attributes = [
@ -97,4 +97,4 @@ class App.Overview extends Spine.Model
'link',
'role',
'prio',
]
]

View file

@ -1,5 +1,5 @@
class App.PostmasterFilter extends App.Model
@configure 'PostmasterFilter', 'name', 'channel', 'match', 'perform', 'note', 'active'
@configure 'PostmasterFilter', 'name', 'channel', 'match', 'perform', 'note', 'active', 'updated_at'
@extend Spine.Model.Ajax
@url: 'api/postmaster_filters'

View file

@ -1,5 +1,5 @@
class App.Role extends App.Model
@configure 'Role', 'name', 'note', 'active'
@configure 'Role', 'name', 'note', 'active', 'updated_at'
@extend Spine.Model.Ajax
@url: 'api/roles'
@configure_attributes = [

View file

@ -1,5 +1,5 @@
class App.Signature extends App.Model
@configure 'Signature', 'name', 'body', 'note', 'active'
@configure 'Signature', 'name', 'body', 'note', 'active', 'updated_at'
@extend Spine.Model.Ajax
@url: 'api/signatures'

View file

@ -1,5 +1,5 @@
class App.Sla extends App.Model
@configure 'Sla', 'name', 'first_response_time', 'update_time', 'close_time', 'condition', 'timezone', 'data', 'active'
@configure 'Sla', 'name', 'first_response_time', 'update_time', 'close_time', 'condition', 'timezone', 'data', 'active', 'updated_at'
@extend Spine.Model.Ajax
@url: 'api/slas'
@configure_attributes = [

View file

@ -1,5 +1,5 @@
class App.Taskbar extends App.Model
@configure 'Taskbar', 'key', 'client_id', 'callback', 'state', 'params', 'prio', 'notify', 'active'
@configure 'Taskbar', 'key', 'client_id', 'callback', 'state', 'params', 'prio', 'notify', 'active', 'updated_at'
# @extend Spine.Model.Local
@extend Spine.Model.Ajax
@url: 'api/taskbar'

View file

@ -1,4 +1,4 @@
class App.Template extends App.Model
@configure 'Template', 'name', 'options', 'group_ids', 'user_id'
@configure 'Template', 'name', 'options', 'group_ids', 'user_id', 'updated_at'
@extend Spine.Model.Ajax
@url: 'api/templates'

View file

@ -1,5 +1,5 @@
class App.TextModule extends App.Model
@configure 'TextModule', 'name', 'keywords', 'content', 'active', 'group_ids', 'user_id'
@configure 'TextModule', 'name', 'keywords', 'content', 'active', 'group_ids', 'user_id', 'updated_at'
@extend Spine.Model.Ajax
@url: 'api/text_modules'
@configure_attributes = [
@ -14,4 +14,4 @@ class App.TextModule extends App.Model
'name',
'keywords',
'content',
]
]

View file

@ -1,5 +1,5 @@
class App.Ticket extends App.Model
@configure 'Ticket', 'number', 'title', 'group_id', 'owner_id', 'customer_id', 'ticket_state_id', 'ticket_priority_id', 'article', 'tags'
@configure 'Ticket', 'number', 'title', 'group_id', 'owner_id', 'customer_id', 'ticket_state_id', 'ticket_priority_id', 'article', 'tags', 'updated_at'
@extend Spine.Model.Ajax
@url: 'api/tickets'
@configure_attributes = [
@ -18,4 +18,4 @@ class App.Ticket extends App.Model
{ name: 'close_time', display: 'Close time', tag: 'time', null: true, style: 'width: 12%' },
{ name: 'escalation_time', display: 'Escalation in', tag: 'time', null: true, style: 'width: 12%' },
{ name: 'article_count', display: 'Article#', style: 'width: 12%' },
]
]

View file

@ -1,5 +1,5 @@
class App.TicketArticle extends App.Model
@configure 'TicketArticle', 'from', 'to', 'cc', 'subject', 'body', 'ticket_id', 'ticket_article_type_id', 'ticket_article_sender_id', 'internal', 'in_reply_to', 'form_id'
@configure 'TicketArticle', 'from', 'to', 'cc', 'subject', 'body', 'ticket_id', 'ticket_article_type_id', 'ticket_article_sender_id', 'internal', 'in_reply_to', 'form_id', 'updated_at'
@extend Spine.Model.Ajax
@url: 'api/ticket_articles'
@configure_attributes = [
@ -12,4 +12,4 @@ class App.TicketArticle extends App.Model
{ name: 'ticket_article_type_id', display: 'Type', tag: 'select', multiple: false, null: false, relation: 'TicketArticleType', default: '', class: 'medium' },
{ name: 'ticket_article_sender_id', display: 'Sender', tag: 'select', multiple: false, null: false, relation: 'TicketArticleSender', default: '', class: 'medium' },
{ name: 'internal', display: 'Visability', tag: 'radio', default: false, null: true, options: { true: 'internal', false: 'public' }, class: 'medium' },
]
]

View file

@ -1,4 +1,4 @@
class App.TicketArticleSender extends App.Model
@configure 'TicketArticleSender', 'name'
@configure 'TicketArticleSender', 'name', 'updated_at'
@extend Spine.Model.Ajax
@url: '/ticket_article_senders'

View file

@ -1,4 +1,4 @@
class App.TicketArticleType extends App.Model
@configure 'TicketArticleType', 'name'
@configure 'TicketArticleType', 'name', 'updated_at'
@extend Spine.Model.Ajax
@url: '/ticket_article_types'

View file

@ -1,4 +1,4 @@
class App.TicketPriority extends App.Model
@configure 'TicketPriority', 'name', 'note', 'active'
@configure 'TicketPriority', 'name', 'note', 'active', 'updated_at'
@extend Spine.Model.Ajax
@url: 'api/ticket_priorities'

View file

@ -1,4 +1,4 @@
class App.TicketStateType extends App.Model
@configure 'TicketStateType', 'name', 'note', 'active'
@configure 'TicketStateType', 'name', 'note', 'active', 'updated_at'
@extend Spine.Model.Ajax
@url: '/ticket_state_types'

View file

@ -1,5 +1,5 @@
class App.User extends App.Model
@configure 'User', 'login', 'firstname', 'lastname', 'email', 'web', 'password', 'phone', 'fax', 'mobile', 'street', 'zip', 'city', 'country', 'organization_id', 'department', 'note', 'role_ids', 'group_ids', 'active', 'invite'
@configure 'User', 'login', 'firstname', 'lastname', 'email', 'web', 'password', 'phone', 'fax', 'mobile', 'street', 'zip', 'city', 'country', 'organization_id', 'department', 'note', 'role_ids', 'group_ids', 'active', 'invite', 'updated_at'
@extend Spine.Model.Ajax
@url: 'api/users'

View file

@ -0,0 +1,29 @@
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
require 'session'
class Observer::WebSocketNotify < ActiveRecord::Observer
observe :ticket, :user, 'ticket::_article'
def after_create(record)
# return if we run import mode
return if Setting.get('import_mode')
Session.broadcast(
:event => record.class.name.downcase + ':created',
:data => { :id => record.id, :updated_at => record.updated_at }
)
end
def after_update(record)
# return if we run import mode
return if Setting.get('import_mode')
puts "#{record.class.name.downcase} UPDATED " + record.updated_at.to_s
Session.broadcast(
:event => record.class.name.downcase + ':updated',
:data => { :id => record.id, :updated_at => record.updated_at }
)
end
end

View file

@ -25,7 +25,7 @@ module Zammad
# Activate observers that should always be running.
# config.active_record.observers = :cacher, :garbage_collector, :forum_observer
config.active_record.observers =
config.active_record.observers =
'observer::_history',
'observer::_ticket::_first_response',
'observer::_ticket::_last_contact',
@ -41,11 +41,8 @@ module Zammad
'observer::_ticket::_notification',
'observer::_tag::_ticket_history',
'observer::_ticket::_reset_new_state',
'observer::_ticket::_escalation_calculation'
# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
# Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
# config.time_zone = 'Central Time (US & Canada)'
'observer::_ticket::_escalation_calculation',
'observer::_web_socket_notify'
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]

View file

@ -293,6 +293,16 @@ module Session
return data
end
def self.broadcast( data )
# list all current clients
client_list = self.list
client_list.each {|local_client_id, local_client|
Session.send( local_client_id, data )
}
return true
end
def self.destory( client_id )
path = @path + '/' + client_id.to_s
FileUtils.rm_rf path

View file

@ -24,10 +24,6 @@ require 'daemons'
:i => Dir.pwd.to_s + '/tmp/pids/websocket.pid'
}
if ARGV[0] != 'start' && ARGV[0] != 'stop'
puts "Usage: websocket-server.rb start|stop [options]"
exit;
end
tls_options = {}
OptionParser.new do |opts|
opts.banner = "Usage: websocket-server.rb start|stop [options]"
@ -58,6 +54,11 @@ OptionParser.new do |opts|
end
end.parse!
if ARGV[0] != 'start' && ARGV[0] != 'stop'
puts "Usage: websocket-server.rb start|stop [options]"
exit;
end
puts "Starting websocket server on #{ @options[:b] }:#{ @options[:p] } (secure:#{ @options[:s].to_s },pid:#{@options[:i].to_s})"
#puts options.inspect

View file

@ -224,7 +224,7 @@ class AgentTicketActionsLevel2Test < TestCase
# change task and page title in first browser
{
:execute => 'wait',
:value => 30,
:value => 10,
},
{
:where => :instance1,