Moved to websockets.

This commit is contained in:
Martin Edenhofer 2012-07-24 00:22:23 +02:00
parent e6f4bd021f
commit b2209ff868
26 changed files with 868 additions and 403 deletions

View file

@ -58,3 +58,6 @@ gem 'simple-rss'
# To use debugger
# gem 'ruby-debug'
# event machine
gem 'eventmachine'
gem 'em-websocket'

View file

@ -2,7 +2,8 @@ class App.Controller extends Spine.Controller
# add @title methode to set title
title: (name) ->
$('html head title').html( Config.product_name + ' - ' + Ti(name) )
# $('html head title').html( Config.product_name + ' - ' + Ti(name) )
document.title = Config.product_name + ' - ' + Ti(name)
# add @notify methode to create notification
notify: (data) ->
@ -545,6 +546,13 @@ class App.Controller extends Spine.Controller
return newInstance
clearInterval: (interval_id) =>
# check global var
if !@intervalID
@intervalID = {}
clearInterval( @intervalID[interval_id] ) if @intervalID[interval_id]
interval: (action, interval, interval_id) =>
# check global var
@ -778,11 +786,14 @@ class App.ControllerModal extends App.Controller
super(options)
modalShow: (params) =>
@el.modal({
defaults = {
backdrop: true,
keyboard: true,
show: true
})
show: true,
}
data = $.extend({}, defaults, params)
@el.modal(data)
@el.bind('hidden', =>
# navigate back to home page
@ -805,3 +816,17 @@ class App.ControllerModal extends App.Controller
submit: (e) =>
e.preventDefault()
@log 'You need to implement your own "submit" method!'
class App.ErrorModal extends App.ControllerModal
constructor: ->
super
@render()
render: ->
@html App.view('error')(
message: @message
)
@modalShow(
backdrop: false,
keyboard: false,
)

View file

@ -9,25 +9,29 @@ class App.DashboardActivityStream extends App.Controller
@items = []
# refresh list ever 140 sec.
@interval( @fetch, 1400000, 'dashboard_activity_stream' )
# @interval( @fetch, 1400000, 'dashboard_activity_stream' )
@fetch()
Spine.bind 'activity_stream_rebuild', (data) =>
@log 'a_stream', data
@fetch()
fetch: =>
# use cache of first page
if window.LastRefresh[ 'dashboard_activity_stream' ]
@render( window.LastRefresh[ 'dashboard_activity_stream' ] )
if window.LastRefresh[ 'activity_stream' ]
@load( window.LastRefresh[ 'activity_stream' ] )
# get data
App.Com.ajax(
id: 'dashoard_activity_stream',
type: 'GET',
url: '/activity_stream',
data: {
limit: @limit,
}
processData: true,
success: @load
)
# # get data
# App.Com.ajax(
# id: 'dashoard_activity_stream',
# type: 'GET',
# url: '/activity_stream',
# data: {
# limit: @limit,
# }
# processData: true,
# success: @load
# )
load: (data) =>
items = data.activity_stream

View file

@ -4,35 +4,17 @@ class App.DashboardRss extends App.Controller
constructor: ->
super
# refresh list ever 600 sec.
@interval( @fetch, 6000000, 'dashboard_rss' )
fetch: =>
Spine.bind 'rss_rebuild', (data) =>
@load(data)
# use cache of first page
if window.LastRefresh[ 'dashboard_rss' ]
@render( window.LastRefresh[ 'dashboard_rss' ] )
# get data
App.Com.ajax(
id: 'dashboard_rss',
type: 'GET',
url: '/rss_fetch',
data: {
limit: @limit,
url: @url,
}
processData: true,
success: @load
)
@load( window.LastRefresh[ 'dashboard_rss' ] )
load: (data) =>
items = data.items || []
# set cache
window.LastRefresh[ 'dashboard_rss' ] = items
@head = data.head || '?'
@render(items)
render: (items) ->

View file

@ -271,7 +271,7 @@ class Index extends App.Controller
@tickets = []
# rebuild navbar with updated ticket count of overviews
Spine.trigger 'navupdate_remote'
App.WebSocket.send( event: 'navupdate_ticket_overview' )
# fetch overview data again
@fetch()

View file

@ -15,7 +15,6 @@ class Index extends App.Controller
@navupdate '#get_started'
@master_user = 0
# @render()
@fetch()
@ -102,12 +101,10 @@ class Index extends App.Controller
# rerender page
@render()
# error: =>
# @modalHide()
)
relogin: (data, status, xhr) =>
@log 'login:success', data
@ -124,6 +121,4 @@ class Index extends App.Controller
@el.find('.agent_user').fadeIn()
)
Config.Routes['getting_started'] = Index

View file

@ -85,10 +85,14 @@ class Index extends App.Controller
for key, value of data.default_collections
App[key].refresh( value, options: { clear: true } )
# rebuild navbar with user data
Spine.trigger 'navrebuild', data.session
# rebuild navbar with updated ticket count of overviews
Spine.trigger 'navupdate_remote'
# update websocked auth info
App.WebSocket.auth()
# rebuild navbar with ticket overview counter
App.WebSocket.send( event: 'navupdate_ticket_overview' )
# add notify
Spine.trigger 'notify:removeall'

View file

@ -4,7 +4,6 @@ class Index extends Spine.Controller
constructor: ->
super
@signout()
signout: ->

View file

@ -6,35 +6,46 @@ class App.Navigation extends App.Controller
@log 'nav...'
@render()
sync_ticket_overview = =>
@interval( @ticket_overview, 30000, 'nav_ticket_overview' )
sync_recent_viewed = =>
@interval( @recent_viewed, 40000, 'nav_recent_viewed' )
# update selected item
Spine.bind 'navupdate', (data) =>
@update(arguments[0])
# rebuild nav bar with given user data
Spine.bind 'navrebuild', (user) =>
@log 'navbarrebuild', user
@render(user)
Spine.bind 'navupdate_remote', (user) =>
@log 'navupdate_remote'
@delay( sync_ticket_overview, 500 )
@delay( sync_recent_viewed, 1000 )
# rebuild ticket overview data
Spine.bind 'navupdate_ticket_overview', (data) =>
@ticket_overview_build(data)
# rerender if new overview data is there
@delay( sync_ticket_overview, 800 )
@delay( sync_recent_viewed, 1000 )
# rebuild recent viewd data
Spine.bind 'update_recent_viewed', (data) =>
@recent_viewed_build(data)
render: (user) ->
nav_left = @getItems( navbar: Config.NavBar )
nav_right = @getItems( navbar: Config.NavBarRight )
# get open tabs to repopen on rerender
open_tab = {}
@el.find('.open').children('a').each( (i,d) =>
href = $(d).attr('href')
open_tab[href] = true
)
# get active tabs to reactivate on rerender
active_tab = {}
@el.find('.active').children('a').each( (i,d) =>
href = $(d).attr('href')
active_tab[href] = true
)
@html App.view('navigation')(
navbar_left: nav_left,
navbar_right: nav_right,
open_tab: open_tab,
active_tab: active_tab,
user: user,
)
@ -119,32 +130,9 @@ class App.Navigation extends App.Controller
update: (url) =>
@el.find('li').removeClass('active')
# if url isnt '#'
@el.find("[href=\"#{url}\"]").parents('li').addClass('active')
# @el.find("[href*=\"#{url}\"]").parents('li').addClass('active')
# get data
ticket_overview: =>
# do no load and rerender if sub-menu is open
open = @el.find('.open').val()
if open isnt undefined
return
# do no load and rerender if user is not logged in
if !window.Session['id']
return
# only of lod request is already done
if !@req_overview
@req_overview = App.Com.ajax(
id: 'navbar_ticket_overviews',
type: 'GET',
url: '/ticket_overviews',
data: {},
processData: true,
success: (data, status, xhr) =>
ticket_overview_build: (data) =>
# remove old views
for key of Config.NavBar
@ -165,33 +153,7 @@ class App.Navigation extends App.Controller
# rebuild navbar
Spine.trigger 'navrebuild', window.Session
# reset ajax call
@req_overview = undefined
)
# get data
recent_viewed: =>
# do no load and rerender if sub-menu is open
open = @el.find('.open').val()
if open isnt undefined
return
# do no load and rerender if user is not logged in
if !window.Session['id']
return
# only of lod request is already done
if !@req_recent_viewed
@req_recent_viewed = App.Com.ajax(
id: 'navbar_recent_viewed',
type: 'GET',
url: '/recent_viewed',
data: {
limit: 5,
}
processData: true,
success: (data, status, xhr) =>
recent_viewed_build: (data) =>
items = data.recent_viewed
@ -204,21 +166,21 @@ class App.Navigation extends App.Controller
# remove old views
for key of Config.NavBarRight
if Config.NavBarRight[key].parent is '#current_user'
part = Config.NavBarRight[key].target.split '::'
if part is 'RecendViewed'
part = key.split '::'
if part[0] is 'RecendViewed'
delete Config.NavBarRight[key]
# add new views
prio = 5000
prio = 8000
for item in items
divider = false
navheader = false
if prio is 5000
if prio is 8000
divider = true
navheader = 'Recent Viewed'
ticket = App.Ticket.find(item.o_id)
prio++
Config.NavBarRight['RecendViewed::' + ticket.id] = {
Config.NavBarRight['RecendViewed::' + ticket.id + '-' + prio ] = {
prio: prio,
parent: '#current_user',
name: item.history_object.name + ' (' + ticket.title + ')',
@ -230,7 +192,3 @@ class App.Navigation extends App.Controller
# rebuild navbar
Spine.trigger 'navrebuild', window.Session
# reset ajax call
@req_recent_viewed = undefined
)

View file

@ -11,6 +11,7 @@
#= require ./lib/bootstrap-popover.js
#= require ./lib/bootstrap-modal.js
#= require ./lib/bootstrap-tab.js
#= require ./lib/bootstrap-transition.js
#= require ./lib/underscore.coffee
#= require ./lib/ba-linkify.js
@ -23,6 +24,7 @@
#not_used= require_tree ./lib
#= require_self
#= require ./lib/ajax.js.coffee
#= require ./lib/websocket.js.coffee
#= require ./lib/auth.js.coffee
#= require ./lib/i18n.js.coffee
#= require_tree ./models
@ -57,13 +59,15 @@ Config2.set( 'a', 123)
console.log '1112222', Config2.get( 'a')
###
class App.Run extends Spine.Controller
constructor: ->
super
@log 'RUN app'#, @
@log 'RUN app'
@el = $('#app')
# create web socket connection
App.WebSocket.connect()
# init of i18n
new App.i18n
@ -84,27 +88,6 @@ class App.Run extends Spine.Controller
window.Session['UISelection'] = @getSelected() + ''
)
# @ws = new WebSocket("ws://localhost:3001/");
# Set event handlers.
# @ws.onopen = ->
# console.log("onopen")
# @ws.onmessage = (e) ->
# e.data contains received string.
# console.log("onmessage: " + e.data)
# eval e.data
# Spine.bind 'ws:send', (data) =>
# @log 'ws:send', data
# @ws.send(data);
# @ws.onclose = ->
# console.log("onclose")
# @ws.onerror = ->
# console.log("onerror")
getSelected: ->
text = '';
if window.getSelection
@ -125,8 +108,20 @@ class App.Content extends Spine.Controller
for route, callback of Config.Routes
do (route, callback) =>
@route(route, (params) ->
# remember current controller
Config['ActiveController'] = route
Spine.trigger( 'ws:send', JSON.stringify( { action: 'active_controller', controller: route, params: params } ) )
# send current controller
params_only = {}
for i of params
if typeof params[i] isnt 'object'
params_only[i] = params[i]
App.WebSocket.send(
action: 'active_controller',
controller: route,
params: params_only,
)
# unbind in controller area
@el.unbind()
@ -136,7 +131,6 @@ class App.Content extends Spine.Controller
$('footer').waypoint('remove')
params.el = @el
params.auth = @auth
new callback( params )
# scroll to top

View file

@ -34,6 +34,9 @@ class App.Auth
# empty session
window.Session = {}
# update websocked auth info
App.WebSocket.auth()
# rebuild navbar with new navbar items
Spine.trigger 'navrebuild'
@ -51,6 +54,9 @@ class App.Auth
for key, value of data.session
window.Session[key] = value
# update websocked auth info
App.WebSocket.auth()
# refresh/load default collections
for key, value of data.default_collections
App[key].refresh( value, options: { clear: true } )
@ -61,12 +67,14 @@ class App.Auth
# rebuild navbar with updated ticket count of overviews
Spine.trigger 'navupdate_remote'
error: (xhr, statusText, error) =>
console.log 'loginCheck:error'#, error, statusText, xhr.statusCode
# empty session
window.Session = {}
# update websocked auth info
App.WebSocket.auth()
)
@logout: ->
@ -75,4 +83,13 @@ class App.Auth
id: 'logout',
type: 'DELETE',
url: '/signout',
success: =>
# update websocked auth info
App.WebSocket.auth()
error: (xhr, statusText, error) =>
# update websocked auth info
App.WebSocket.auth()
)

View file

@ -0,0 +1,61 @@
/* ===================================================
* bootstrap-transition.js v2.0.4
* http://twitter.github.com/bootstrap/javascript.html#transitions
* ===================================================
* Copyright 2012 Twitter, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ========================================================== */
!function ($) {
$(function () {
"use strict"; // jshint ;_;
/* CSS TRANSITION SUPPORT (http://www.modernizr.com/)
* ======================================================= */
$.support.transition = (function () {
var transitionEnd = (function () {
var el = document.createElement('bootstrap')
, transEndEventNames = {
'WebkitTransition' : 'webkitTransitionEnd'
, 'MozTransition' : 'transitionend'
, 'OTransition' : 'oTransitionEnd'
, 'msTransition' : 'MSTransitionEnd'
, 'transition' : 'transitionend'
}
, name
for (name in transEndEventNames){
if (el.style[name] !== undefined) {
return transEndEventNames[name]
}
}
}())
return transitionEnd && {
end: transitionEnd
}
})()
})
}(window.jQuery);

View file

@ -0,0 +1,114 @@
$ = jQuery.sub()
class App.WebSocket
_instance = undefined # Must be declared here to force the closure on the class
@connect: (args) -> # Must be a static method
if _instance == undefined
_instance ?= new _Singleton
_instance
@send: (args) -> # Must be a static method
@connect()
_instance.send(args)
@auth: (args) -> # Must be a static method
@connect()
_instance.auth(args)
# The actual Singleton class
class _Singleton extends Spine.Controller
queue: []
constructor: (@args) ->
@connect()
send: (data) =>
console.log 'ws:send trying', data, @ws, @ws.readyState
# A value of 0 indicates that the connection has not yet been established.
# A value of 1 indicates that the connection is established and communication is possible.
# A value of 2 indicates that the connection is going through the closing handshake.
# A value of 3 indicates that the connection has been closed or could not be opened.
if @ws.readyState is 0
@queue.push data
else
console.log( 'ws:send', data )
string = JSON.stringify( data )
@ws.send(string)
auth: (data) =>
# logon websocket
data = {
action: 'login',
session: window.Session
}
@send(data)
close: =>
@ws.close()
connect: =>
# console.log '------------ws connect....--------------'
if !window.WebSocket
@error = new App.ErrorModal(
message: 'Sorry, no websocket support!'
)
return
@ws = new window.WebSocket( "ws://" + window.location.hostname + ":6042/" )
# Set event handlers.
@ws.onopen = =>
console.log( "onopen" )
# close error message if exists
if @error
@error.modalHide()
@error = undefined
@auth()
# empty queue
for item in @queue
console.log( 'ws:send queue', item )
@send(item)
@queue = []
@ws.onmessage = (e) ->
pipe = JSON.parse( e.data )
# console.log( "ws:onmessage", pipe )
# go through all blocks
for item in pipe
# fill collection
if item['collection']
console.log( "ws:onmessage collection:" + item['collection'] )
window.LastRefresh[ item['collection'] ] = item['data']
# fire event
if item['event']
console.log( "ws:onmessage event:" + item['event'] )
Spine.trigger( item['event'], item['data'] )
# bind to send messages
Spine.bind 'ws:send', (data) =>
@send(data)
@ws.onclose = (e) =>
console.log( "onclose", e )
# show error message
if !@error
@error = new App.ErrorModal(
message: 'No connection to websocket, trying to reconnect...'
)
# try reconnect after 5 sec.
@delay @connect, 5000
@ws.onerror = ->
console.log( "onerror" )

View file

@ -0,0 +1,17 @@
<div class="modal-header">
<!--
<a href="#" class="close">&times;</a>
-->
<h3><%- T('Error') %></h3>
</div>
<div class="modal-body">
<form class="form-horizontal">
<p><%= @message %>
</form>
</div>
<div class="modal-footer">
<!--
<button class="btn btn-primary submit"><%- T('Submit') %></button>
<button class="btn cancel"><%- T('Cancel') %></button>
-->
</div>

View file

@ -5,7 +5,7 @@
<ul class="nav">
<% for item in @navbar_left: %>
<% if item.child: %>
<li class="dropdown">
<li class="dropdown <% if @open_tab[item.target] : %>open<% end %>">
<a href="<%= item.target %>" class="dropdown-toggle" data-toggle="dropdown"><%- T(item.name) %> <b class="caret"></b></a>
<ul class="dropdown-menu">
<% for item in item.child: %>
@ -15,12 +15,12 @@
<% if item.navheader: %>
<li class="nav-header"><%- T(item.navheader) %></li>
<% end %>
<li><a href="<%= item.target %>"><%- T(item.name) %><% if item['count'] isnt undefined: %> <span class="count">(<%= item['count'] %>)</span><% end %></a></li>
<li class="<% if @active_tab[item.target] : %>active<% end %>"><a href="<%= item.target %>"><%- T(item.name) %><% if item['count'] isnt undefined: %> <span class="count">(<%= item['count'] %>)</span><% end %></a></li>
<% end %>
</ul>
</li>
<% else: %>
<li><a href="<%= item.target %>"><%- T(item.name) %></a></li>
<li class="<% if @active_tab[item.target] : %>active<% end %>"><a href="<%= item.target %>"><%- T(item.name) %></a></li>
<% end %>
<% end %>
</ul>
@ -32,7 +32,7 @@
<ul class="nav pull-right">
<% for item in @navbar_right: %>
<% if item.child: %>
<li class="dropdown">
<li class="dropdown <% if @open_tab[item.target] : %>open<% end %>">
<a href="<%= item.target %>" class="dropdown-toggle" data-toggle="dropdown"><%- T(item.name) %> <b class="caret"></b></a>
<ul class="dropdown-menu">
<% for item in item.child: %>
@ -42,12 +42,12 @@
<% if item.navheader: %>
<li class="nav-header"><%- T(item.navheader) %></li>
<% end %>
<li><a href="<%= item.target %>"><%- T(item.name) %><% if item['count'] isnt undefined: %> <span class="count">(<%= item['count'] %>)</span><% end %></a></li>
<li class="<% if @active_tab[item.target] : %>active<% end %>"><a href="<%= item.target %>"><%- T(item.name) %><% if item['count'] isnt undefined: %> <span class="count">(<%= item['count'] %>)</span><% end %></a></li>
<% end %>
</ul>
</li>
<% else: %>
<li><a href="<%= item.target %>"><%- T(item.name) %></a></li>
<li class="<% if @active_tab[item.target] : %>active<% end %>"><a href="<%= item.target %>"><%- T(item.name) %></a></li>
<% end %>
<% end %>
</ul>

View file

@ -3,56 +3,10 @@ class ActivityController < ApplicationController
# GET /activity_stream
def activity_stream
activity_stream = History.activity_stream(current_user, params[:limit])
# get related users
users = {}
tickets = []
articles = []
activity_stream.each {|item|
# load article ids
if item['history_object'] == 'Ticket'
ticket = Ticket.find( item['o_id'] ).attributes
tickets.push ticket
# load users
if !users[ ticket['owner_id'] ]
users[ ticket['owner_id'] ] = user_data_full( ticket['owner_id'] )
end
if !users[ ticket['customer_id'] ]
users[ ticket['customer_id'] ] = user_data_full( ticket['customer_id'] )
end
end
if item['history_object'] == 'Ticket::Article'
article = Ticket::Article.find( item['o_id'] ).attributes
if !article['subject'] || article['subject'] == ''
article['subject'] = Ticket.find( article['ticket_id'] ).title
end
articles.push article
# load users
if !users[ article['created_by_id'] ]
users[ article['created_by_id'] ] = user_data_full( article['created_by_id'] )
end
end
if item['history_object'] == 'User'
users[ item['o_id'] ] = user_data_full( item['o_id'] )
end
# load users
if !users[ item['created_by_id'] ]
users[ item['created_by_id'] ] = user_data_full( item['created_by_id'] )
end
}
activity_stream = History.activity_stream_fulldata(current_user, params[:limit])
# return result
render :json => {
:activity_stream => activity_stream,
:tickets => tickets,
:articles => articles,
:users => users,
}
render :json => activity_stream
end
end

View file

@ -161,52 +161,7 @@ class ApplicationController < ActionController::Base
end
def user_data_full (user_id)
# get user
user = User.find_fulldata(user_id)
# do not show password
user['password'] = ''
# TEMP: compat. reasons
user['preferences'] = {} if user['preferences'] == nil
items = []
if user['preferences'][:tickets_open].to_i > 0
item = {
:url => '',
:name => 'open',
:count => user['preferences'][:tickets_open] || 0,
:title => 'Open Tickets',
:class => 'user-tickets',
:data => 'open'
}
items.push item
end
if user['preferences'][:tickets_closed].to_i > 0
item = {
:url => '',
:name => 'closed',
:count => user['preferences'][:tickets_closed] || 0,
:title => 'Closed Tickets',
:class => 'user-tickets',
:data => 'closed'
}
items.push item
end
# show linked topics and items
if items.count > 0
topic = {
:title => 'Tickets',
:items => items,
}
user['links'] = []
user['links'].push topic
end
return user
user = User.user_data_full(user_id)
end
end

View file

@ -3,36 +3,10 @@ class RecentViewedController < ApplicationController
# GET /recent_viewed
def recent_viewed
recent_viewed = History.recent_viewed(current_user)
# get related users
users = {}
tickets = []
recent_viewed.each {|item|
# load article ids
# if item.history_object == 'Ticket'
tickets.push Ticket.find( item['o_id'] ).attributes
# end
# if item.history_object 'Ticket::Article'
# tickets.push Ticket::Article.find(item.o_id)
# end
# if item.history_object 'User'
# tickets.push User.find(item.o_id)
# end
# load users
if !users[ item['created_by_id'] ]
users[ item['created_by_id'] ] = user_data_full( item['created_by_id'] )
end
}
recent_viewed = History.recent_viewed_fulldata(current_user)
# return result
render :json => {
:recent_viewed => recent_viewed,
:tickets => tickets,
:users => users,
}
render :json => recent_viewed
end
end

View file

@ -3,33 +3,9 @@ class RssController < ApplicationController
# GET /rss_fetch
def fetch
url = params[:url]
limit = params[:limit] || 10
cache_key = 'rss::' + url
items = Rails.cache.read( cache_key )
if !items
response = Net::HTTP.get_response( URI.parse(url) )
if response.code.to_s != '200'
render :json => { :message => "failed to fetch #{url}, code: #{response.code}"}, :status => :unprocessable_entity
return
end
rss = SimpleRSS.parse response.body
items = []
fetched = 0
rss.items.each { |item|
record = {
:id => item.id,
:title => item.title,
:summary => item.summary,
:link => item.link,
:published => item.published
}
items.push record
fetched += 1
break item if fetched == limit.to_i
}
Rails.cache.write( cache_key, items, :expires_in => 4.hours )
items = RSS.fetch(params[:url], params[:limit])
if items == nil
render :json => { :message => "failed to fetch #{ params[:url] }", :status => :unprocessable_entity }
end
render :json => { :items => items }
end

View file

@ -136,14 +136,65 @@ class History < ActiveRecord::Base
end
return datas
end
def self.activity_stream_fulldata(user, limit = 10)
activity_stream = History.activity_stream( user, limit )
# get related users
users = {}
tickets = []
articles = []
activity_stream.each {|item|
# load article ids
if item['history_object'] == 'Ticket'
ticket = Ticket.find( item['o_id'] ).attributes
tickets.push ticket
# load users
if !users[ ticket['owner_id'] ]
users[ ticket['owner_id'] ] = User.user_data_full( ticket['owner_id'] )
end
if !users[ ticket['customer_id'] ]
users[ ticket['customer_id'] ] = User.user_data_full( ticket['customer_id'] )
end
end
if item['history_object'] == 'Ticket::Article'
article = Ticket::Article.find( item['o_id'] ).attributes
if !article['subject'] || article['subject'] == ''
article['subject'] = Ticket.find( article['ticket_id'] ).title
end
articles.push article
# load users
if !users[ article['created_by_id'] ]
users[ article['created_by_id'] ] = User.user_data_full( article['created_by_id'] )
end
end
if item['history_object'] == 'User'
users[ item['o_id'] ] = User.user_data_full( item['o_id'] )
end
# load users
if !users[ item['created_by_id'] ]
users[ item['created_by_id'] ] = User.user_data_full( item['created_by_id'] )
end
}
return {
:activity_stream => activity_stream,
:tickets => tickets,
:articles => articles,
:users => users,
}
end
def self.recent_viewed(user)
# g = Group.where( :active => true ).joins(:users).where( 'users.id' => user.id )
stream = History.select("distinct(histories.o_id), created_by_id, history_attribute_id, history_type_id, history_object_id, value_from, value_to").
stream = History.select("distinct(o_id), created_by_id, history_type_id, history_object_id, created_at").
where( :history_object_id => History::Object.where( :name => 'Ticket').first.id ).
where( :history_type_id => History::Type.where( :name => ['viewed'] ) ).
where( :created_by_id => user.id ).
order('created_at DESC, id DESC').
order('created_at DESC, id ASC').
limit(10)
datas = []
stream.each do |item|
@ -153,9 +204,49 @@ class History < ActiveRecord::Base
datas.push data
# item['history_attribute'] = item.history_attribute
end
# puts 'pppppppppp'
# puts datas.inspect
return datas
end
def self.recent_viewed_fulldata(user)
recent_viewed = History.recent_viewed(user)
# get related users
users = {}
tickets = []
recent_viewed.each {|item|
# load article ids
# if item.history_object == 'Ticket'
ticket = Ticket.find( item['o_id'] ).attributes
tickets.push ticket
# end
# if item.history_object 'Ticket::Article'
# tickets.push Ticket::Article.find(item.o_id)
# end
# if item.history_object 'User'
# tickets.push User.find(item.o_id)
# end
# load users
if !users[ ticket['owner_id'] ]
users[ ticket['owner_id'] ] = User.user_data_full( ticket['owner_id'] )
end
if !users[ ticket['created_by_id'] ]
users[ ticket['created_by_id'] ] = User.user_data_full( ticket['created_by_id'] )
end
if !users[ item['created_by_id'] ]
users[ item['created_by_id'] ] = User.user_data_full( item['created_by_id'] )
end
}
return {
:recent_viewed => recent_viewed,
:tickets => tickets,
:users => users,
}
end
private
def check_type
puts '--------------'

View file

@ -123,7 +123,6 @@ class Ticket < ActiveRecord::Base
return subject
end
# Ticket.overview(
# :view => 'some_view_url',
# :current_user_id => 123,

View file

@ -207,6 +207,54 @@ Your #{config.product_name} Team
return data
end
def self.user_data_full (user_id)
# get user
user = User.find_fulldata(user_id)
# do not show password
user['password'] = ''
# TEMP: compat. reasons
user['preferences'] = {} if user['preferences'] == nil
items = []
if user['preferences'][:tickets_open].to_i > 0
item = {
:url => '',
:name => 'open',
:count => user['preferences'][:tickets_open] || 0,
:title => 'Open Tickets',
:class => 'user-tickets',
:data => 'open'
}
items.push item
end
if user['preferences'][:tickets_closed].to_i > 0
item = {
:url => '',
:name => 'closed',
:count => user['preferences'][:tickets_closed] || 0,
:title => 'Closed Tickets',
:class => 'user-tickets',
:data => 'closed'
}
items.push item
end
# show linked topics and items
if items.count > 0
topic = {
:title => 'Tickets',
:items => items,
}
user['links'] = []
user['links'].push topic
end
return user
end
# update all users geo data
def self.geo_update_all
User.all.each { |user|

View file

@ -13,8 +13,11 @@ require 'google_oauth2_database'
# load notification factory (replace all tags)
require 'notification_factory'
# load gmaps lookup
# load lib
require 'gmaps'
require 'rss'
require 'web_socket'
# Initialize the rails application
Zammad::Application.initialize!

31
lib/rss.rb Normal file
View file

@ -0,0 +1,31 @@
module RSS
def self.fetch(url, limit = 10)
cache_key = 'rss::' + url
items = Rails.cache.read( cache_key )
if !items
puts 'fetch rss...'
response = Net::HTTP.get_response( URI.parse(url) )
if response.code.to_s != '200'
return
end
rss = SimpleRSS.parse response.body
items = []
fetched = 0
rss.items.each { |item|
record = {
:id => item.id,
:title => item.title,
:summary => item.summary,
:link => item.link,
:published => item.published
}
items.push record
fetched += 1
break item if fetched == limit.to_i
}
Rails.cache.write( cache_key, items, :expires_in => 4.hours )
end
return items
end
end

175
lib/web_socket.rb Normal file
View file

@ -0,0 +1,175 @@
require 'json'
module Session
@path = '/tmp/websocket'
def self.create( client_id, session )
path = @path + '/' + client_id.to_s
FileUtils.mkpath path
File.open( path + '/session', 'w' ) { |file|
user = { :id => session['id'] }
file.puts Marshal.dump(user)
}
end
def self.get( client_id )
session_file = @path + '/' + client_id.to_s + '/session'
data = nil
return if !File.exist? session_file
File.open( session_file, 'r' ) { |file|
all = ''
while line = file.gets
all = all + line
end
begin
data = Marshal.load( all )
rescue
return
end
}
return data
end
def self.transaction( client_id, data )
filename = @path + '/' + client_id.to_s + '/transaction-' + Time.new().to_i.to_s
if File::exists?( filename )
filename = @path + '/' + client_id.to_s + '/transaction-' + Time.new().to_i.to_s + '-1'
if File::exists?( filename )
filename = @path + '/' + client_id.to_s + '/transaction-' + Time.new().to_i.to_s + '-2'
if File::exists?( filename )
filename = @path + '/' + client_id.to_s + '/transaction-' + Time.new().to_i.to_s + '-3'
if File::exists?( filename )
filename = @path + '/' + client_id.to_s + '/transaction-' + Time.new().to_i.to_s + '-4'
end
end
end
end
File.open( filename, 'w' ) { |file|
file.puts data.to_json
}
return true
end
def self.jobs
state_client_ids = {}
while true
client_ids = self.sessions
client_ids.each { |client_id|
if !state_client_ids[client_id]
state_client_ids[client_id] = {}
end
# get current user
user_session = Session.get( client_id )
next if !user_session
next if !user_session[:id]
user = User.find( user_session[:id] )
# overviews
result = Ticket.overview(
:current_user_id => user.id,
)
if state_client_ids[client_id][:overview] != result
state_client_ids[client_id][:overview] = result
# send update to browser
Session.transaction( client_id, {
:action => 'load',
:data => result,
:event => 'navupdate_ticket_overview',
})
end
# recent viewed
recent_viewed = History.recent_viewed(user)
if state_client_ids[client_id][:recent_viewed] != recent_viewed
state_client_ids[client_id][:recent_viewed] = recent_viewed
# tickets and users
recent_viewed = History.recent_viewed_fulldata(user)
# send update to browser
Session.transaction( client_id, {
:action => 'load',
:data => recent_viewed,
:event => 'update_recent_viewed',
})
end
# activity stream
activity_stream = History.activity_stream(user)
if state_client_ids[client_id][:activity_stream] != activity_stream
state_client_ids[client_id][:activity_stream] = activity_stream
activity_stream = History.activity_stream_fulldata(user)
# send update to browser
Session.transaction( client_id, {
:event => 'activity_stream_rebuild',
:collection => 'activity_stream',
:data => activity_stream,
})
end
# rss view
rss_items = RSS.fetch( 'http://www.heise.de/newsticker/heise-atom.xml', 8 )
if state_client_ids[client_id][:rss_items] != rss_items
state_client_ids[client_id][:rss_items] = rss_items
# send update to browser
Session.transaction( client_id, {
:event => 'rss_rebuild',
:collection => 'dashboard_rss',
:data => {
head: 'Heise ATOM',
items: rss_items,
},
})
end
sleep 1
}
end
end
def self.sessions
path = @path + '/'
data = []
Dir.foreach( path ) do |entry|
if entry != '.' && entry != '..'
data.push entry
end
end
return data
end
def self.queue( client_id )
path = @path + '/' + client_id.to_s + '/'
data = []
Dir.foreach( path ) do |entry|
if /^transaction/.match( entry )
data.push Session.queue_file( path + entry )
end
end
return data
end
def self.queue_file( filename )
data = nil
File.open( filename, 'r' ) { |file|
all = ''
while line = file.gets
all = all + line
end
data = JSON.parse( all )
}
File.delete( filename )
return data
end
def self.destory( client_id )
path = @path + '/' + client_id.to_s
FileUtils.rm_rf path
end
end

View file

@ -0,0 +1,86 @@
$LOAD_PATH << './lib'
require 'rubygems'
require 'eventmachine'
require 'em-websocket'
require 'json'
require 'fileutils'
require 'web_socket'
require 'optparse'
# Look for -o with argument, and -I and -D boolean arguments
options = {
:p => 6042,
:b => '0.0.0.0',
}
OptionParser.new do |opts|
opts.banner = "Usage: websocket-server.rb [options]"
opts.on("-p", "--port [OPT]", "port of websocket server") do |p|
options[:p] = p
end
opts.on("-b", "--bind [OPT]", "bind address") do |b|
options[:b] = b
end
end.parse!
puts "Starting websocket server on #{ options[:b] }:#{ options[:p] }"
@clients = {}
EventMachine.run {
EventMachine::WebSocket.start( :host => options[:b], :port => options[:p] ) do |ws|
# register client connection
ws.onopen {
client_id = ws.object_id
puts 'Client ' + client_id.to_s + ' connected'
if !@clients.include? client_id
@clients[client_id] = {
:websocket => ws,
}
end
}
# unregister client connection
ws.onclose {
client_id = ws.object_id
puts 'Client ' + client_id.to_s + ' disconnected'
if @clients.include? client_id
@clients.delete client_id
end
Session.destory( client_id )
}
# manage messages
ws.onmessage { |msg|
client_id = ws.object_id
puts 'From Client ' + client_id.to_s + ' received message: ' + msg
data = JSON.parse(msg)
# get session
if data['action'] == 'login'
@clients[client_id][:session] = data['session']
Session.create( client_id, data['session'] )
end
}
end
EventMachine.add_periodic_timer(0.4) {
# puts "loop"
@clients.each { |client_id, client|
# puts 'checking client...' + client_id.to_s
begin
queue = Session.queue( client_id )
if queue && queue[0]
puts "send to #{client_id} " + queue.inspect
client[:websocket].send( queue.to_json )
end
rescue
puts 'problem'
end
}
}
}