Added generic activity stream support.

This commit is contained in:
Martin Edenhofer 2013-09-28 02:07:11 +02:00
parent 9aa5afbd65
commit 4e2288e9a4
21 changed files with 611 additions and 90 deletions

View file

@ -40,21 +40,26 @@ class App.DashboardActivityStream extends App.Controller
render: (items) ->
for item in items
if item.history_object is 'Ticket'
if item.object is 'Ticket'
ticket = App.Ticket.find( item.o_id )
item.link = '#ticket/zoom/' + ticket.id
item.title = ticket.title
item.type = 'Ticket'
item.updated_by_id = ticket.updated_by_id
item.updated_by = App.User.find( ticket.updated_by_id )
else if item.history_object is 'Ticket::Article'
item.object = 'Ticket'
else if item.object is 'Ticket::Article'
article = App.TicketArticle.find( item.o_id )
ticket = App.Ticket.find( article.ticket_id )
item.link = '#ticket/zoom/' + ticket.id + '/' + article.id
item.title = article.subject || ticket.title
item.type = 'Article'
item.updated_by_id = article.updated_by_id
item.updated_by = App.User.find( article.updated_by_id )
item.object = 'Article'
else if item.object is 'User'
user = App.User.find( item.o_id )
item.link = '#user/zoom/' + item.o_id
item.title = user.displayName()
item.object = 'User'
item.created_by = App.User.find( item.created_by_id )
html = App.view('dashboard/activity_stream')(
head: 'Activity Stream',
@ -67,3 +72,5 @@ class App.DashboardActivityStream extends App.Controller
# start user popups
@userPopups('right')
# update time
@frontendTimeUpdate()

View file

@ -2,8 +2,8 @@
<h2 class="can-move"><%- @T( @head ) %></h2>
<dl>
<% for item in @items: %>
<dt><span class="user-popover" data-id="<%= item.updated_by_id %>">"<%= item.updated_by.displayName() %>"</span></dt>
<dd><%- @T( item.history_type ) %> <a href="<%- item.link %>"><%= item.type %><% if item.title: %> (<%= item.title %>)<% end %></a>.</dd>
<dt><span class="user-popover" data-id="<%= item.created_by_id %>">"<%= item.created_by.displayName() %>"</span></dt>
<dd><%- @T( item.type ) %> <a href="<%- item.link %>"><%= item.object %><% if item.title: %> (<%= item.title %>)<% end %></a> <span class="humanTimeFromNow" data-time="<%- item.created_at %>">?</span> <%- @T('ago') %>.</dd>
<% end %>
</dl>
</div>

View file

@ -1,11 +1,11 @@
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
class ActivityController < ApplicationController
class ActivityStreamController < ApplicationController
before_filter :authentication_check
# GET /api/v1/activity_stream
def activity_stream
activity_stream = History.activity_stream_fulldata( current_user, params[:limit] )
def show
activity_stream = current_user.activity_stream( params[:limit], true )
# return result
render :json => activity_stream

View file

@ -14,6 +14,8 @@ class SessionsController < ApplicationController
return
end
user.activity_stream_log( 'session started', user.id )
# auto population of default collections
default_collection = SessionHelper::default_collections(user)

View file

@ -0,0 +1,179 @@
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
class ActivityStream < ApplicationModel
self.table_name = 'activity_streams'
belongs_to :activity_stream_type, :class_name => 'ActivityStream::Type'
belongs_to :activity_stream_object, :class_name => 'ActivityStream::Object'
@@cache_type = {}
@@cache_object = {}
=begin
add a new activity entry for an object
ActivityStream.add(
:type => 'updated',
:object => 'Ticket',
:role => 'Admin',
:o_id => ticket.id,
:created_by_id => 1,
:created_at => '2013-06-04 10:00:00',
)
=end
def self.add(data)
# lookups
if data[:type]
type = self.type_lookup( data[:type] )
end
if data[:object]
object = self.object_lookup( data[:object] )
end
role_id = nil
if data[:role]
role_id = Role.lookup( :name => data[:role] )
if !role_id
raise "No such Role #{data[:role]}"
end
end
# check if entry is needed
result = ActivityStream.where(
:o_id => data[:o_id],
# :activity_stream_type_id => type.id,
:role_id => role_id,
:activity_stream_object_id => object.id,
:created_by_id => data[:created_by_id]
).last
# resturn if old entry is really freash
return result if result && result.created_at >= (data[:created_at] - 10.seconds)
puts "AS: #{data[:type]} #{data[:object]} #{data[:o_id]}"
# create history
record = {
:o_id => data[:o_id],
:activity_stream_type_id => type.id,
:activity_stream_object_id => object.id,
:created_at => data[:created_at],
:created_by_id => data[:created_by_id]
}
ActivityStream.create(record)
end
=begin
remove whole activity entries of an object
ActivityStream.remove( 'Ticket', 123 )
=end
def self.remove( object_name, o_id )
object = self.object_lookup( object_name )
ActivityStream.where(
:activity_stream_object_id => object.id,
:o_id => o_id,
).destroy_all
end
=begin
return all activity entries of an user
activity_stream = ActivityStream.list( user )
=end
def self.list(user,limit)
# stream = ActivityStream.where( :role_id => user.roles, :group_id => user.groups )
stream = ActivityStream.where('1=1').
order( 'created_at DESC, id DESC' ).
limit( limit )
list = []
stream.each do |item|
data = item.attributes
data['object'] = self.object_lookup_id( data['activity_stream_object_id'] ).name
data['type'] = self.type_lookup_id( data['activity_stream_type_id'] ).name
data.delete('activity_stream_object_id')
data.delete('activity_stream_type_id')
list.push data
end
list
end
private
def self.type_lookup_id( id )
# use cache
return @@cache_type[ id ] if @@cache_type[ id ]
# lookup
type = ActivityStream::Type.find(id)
@@cache_type[ id ] = type
type
end
def self.type_lookup( name )
# use cache
return @@cache_type[ name ] if @@cache_type[ name ]
# lookup
type = ActivityStream::Type.where( :name => name ).first
if type
@@cache_type[ name ] = type
return type
end
# create
type = ActivityStream::Type.create(
:name => name
)
@@cache_type[ name ] = type
type
end
def self.object_lookup_id( id )
# use cache
return @@cache_object[ id ] if @@cache_object[ id ]
# lookup
object = ActivityStream::Object.find(id)
@@cache_object[ id ] = object
object
end
def self.object_lookup( name )
# use cache
return @@cache_object[ name ] if @@cache_object[ name ]
# lookup
object = ActivityStream::Object.where( :name => name ).first
if object
@@cache_object[ name ] = object
return object
end
# create
object = ActivityStream::Object.create(
:name => name
)
@@cache_object[ name ] = object
object
end
class Object < ApplicationModel
end
class Type < ApplicationModel
end
end

View file

@ -2,6 +2,7 @@
class ApplicationModel < ActiveRecord::Base
include ApplicationModel::HistoryLogBase
include ApplicationModel::ActivityStreamBase
self.abstract_class = true
@ -14,6 +15,13 @@ class ApplicationModel < ActiveRecord::Base
after_update :cache_delete
after_destroy :cache_delete
after_create :activity_stream_create
after_update :activity_stream_update
after_destroy :activity_stream_destroy
# create instance accessor
class << self; attr_accessor :activity_stream_support_config end
@@import_class_list = ['Ticket', 'Ticket::Article', 'History', 'Ticket::State', 'Ticket::Priority', 'Group', 'User' ]
def check_attributes_protected
@ -410,6 +418,59 @@ class OwnModel < ApplicationModel
=begin
serve methode to configure activity stream support for this model
class Model < ApplicationModel
activity_stream_support :role => 'Admin'
end
=end
def self.activity_stream_support(data = {})
@activity_stream_support_config = data
end
=begin
log object create activity stream
model = Model.find(123)
model.activity_stream_create
=end
def activity_stream_create
activity_stream_log( 'created', self['created_by_id'] )
end
=begin
log object update activity stream
model = Model.find(123)
model.activity_stream_update
=end
def activity_stream_update
activity_stream_log( 'updated', self['updated_by_id'] )
end
=begin
delete object activity stream
model = Model.find(123)
model.activity_stream_destroy
=end
def activity_stream_destroy
ActivityStream.remove( self.class.to_s, self.id )
end
=begin
destory object dependencies, will be executed automatically
=end
@ -418,6 +479,7 @@ destory object dependencies, will be executed automatically
# delete history
History.remove( self.class.to_s, self.id )
end
end

View file

@ -0,0 +1,30 @@
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
module ApplicationModel::ActivityStreamBase
=begin
log activity for this object
article = Ticket::Article.find(123)
result = article.activity_stream_log( 'created', user_id )
returns
result = true # false
=end
def activity_stream_log (type, user_id)
return if !self.class.activity_stream_support_config
ActivityStream.add(
:o_id => self['id'],
:type => type,
:object => self.class.name,
# :role => self.activity_stream_role,
:created_at => self.updated_at,
:created_by_id => user_id,
)
end
end

View file

@ -1,8 +1,9 @@
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
class Group < ApplicationModel
has_and_belongs_to_many :users, :after_add => :cache_update, :after_remove => :cache_update
belongs_to :email_address
belongs_to :signature
validates :name, :presence => true
end
has_and_belongs_to_many :users, :after_add => :cache_update, :after_remove => :cache_update
belongs_to :email_address
belongs_to :signature
validates :name, :presence => true
activity_stream_support :role => 'Admin'
end

View file

@ -95,6 +95,7 @@ remove whole history entries of an object
def self.remove( requested_object, requested_object_id )
history_object = History::Object.where( :name => requested_object ).first
return if !history_object
History.where(
:history_object_id => history_object.id,
:o_id => requested_object_id,
@ -103,7 +104,7 @@ remove whole history entries of an object
=begin
return all histoy entries of an object
return all history entries of an object
history_list = History.list( 'Ticket', 123 )
@ -133,56 +134,6 @@ return all histoy entries of an object
return history
end
def self.activity_stream( user, limit = 10 )
# 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").
# where( :history_type_id => History::Type.where( :name => ['created', 'updated']) ).
stream = History.select("distinct(histories.o_id), created_by_id, history_type_id, history_object_id").
where( :history_object_id => History::Object.where( :name => [ 'Ticket', 'Ticket::Article' ] ) ).
where( :history_type_id => History::Type.where( :name => [ 'created', 'updated' ]) ).
order('created_at DESC, id DESC').
limit(limit)
datas = []
stream.each do |item|
data = item.attributes
data['history_object'] = self.object_lookup_id( data['history_object_id'] ).name
data['history_type'] = self.type_lookup_id( data['history_type_id'] ).name
data.delete('history_object_id')
data.delete('history_type_id')
datas.push data
# item['history_attribute'] = item.history_attribute
end
return datas
end
def self.activity_stream_fulldata( user, limit = 10 )
activity_stream = History.activity_stream( user, limit )
# get related users
assets = {}
activity_stream.each {|item|
# load article ids
if item['history_object'] == 'Ticket'
ticket = Ticket.find( item['o_id'] )
assets = ticket.assets(assets)
end
if item['history_object'] == 'Ticket::Article'
article = Ticket::Article.find( item['o_id'] )
assets = article.assets(assets)
end
if item['history_object'] == 'User'
user = User.find( item['o_id'] )
assets = user.assets(assets)
end
}
return {
:activity_stream => activity_stream,
:assets => assets,
}
end
private
def self.type_lookup_id( id )

View file

@ -20,10 +20,6 @@ class Observer::History < ActiveRecord::Observer
record.history_create( 'created', user_id )
end
# log activity stream
if record.respond_to?('activity_stream')
record.activity_stream( 'created', user_id )
end
end
def before_update(record)
@ -149,12 +145,6 @@ class Observer::History < ActiveRecord::Observer
end
end
# log activity stream
if history_logged
if record.respond_to?('activity_stream')
record.activity_stream( 'updated', user_id )
end
end
end
def differences_from?(one, other)

View file

@ -4,7 +4,8 @@ class Organization < ApplicationModel
include Organization::Assets
extend Organization::Search
has_and_belongs_to_many :users
validates :name, :presence => true
has_and_belongs_to_many :users
validates :name, :presence => true
activity_stream_support :role => 'Admin'
end

View file

@ -1,6 +1,7 @@
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
class Role < ApplicationModel
has_and_belongs_to_many :users, :after_add => :cache_update, :after_remove => :cache_update
validates :name, :presence => true
has_and_belongs_to_many :users, :after_add => :cache_update, :after_remove => :cache_update
validates :name, :presence => true
activity_stream_support :role => 'Admin'
end

View file

@ -7,6 +7,7 @@ class Ticket < ApplicationModel
after_create :notify_clients_after_create
after_update :notify_clients_after_update
after_destroy :notify_clients_after_destroy
activity_stream_support :role => 'User'
belongs_to :group
has_many :articles, :class_name => 'Ticket::Article', :after_add => :cache_update, :after_remove => :cache_update

View file

@ -4,15 +4,17 @@ class Ticket::Article < ApplicationModel
include Ticket::Article::Assets
include Ticket::Article::HistoryLog
after_create :attachment_check
belongs_to :ticket
belongs_to :ticket_article_type, :class_name => 'Ticket::Article::Type'
belongs_to :ticket_article_sender, :class_name => 'Ticket::Article::Sender'
belongs_to :created_by, :class_name => 'User'
after_create :attachment_check
after_create :notify_clients_after_create
after_update :notify_clients_after_update
after_destroy :notify_clients_after_destroy
activity_stream_support
attr_accessor :attachments
private

View file

@ -19,6 +19,8 @@ class User < ApplicationModel
store :preferences
activity_stream_support :role => 'Admin'
=begin
fullname of user
@ -43,7 +45,7 @@ returns
end
fullname = fullname + self.lastname
end
return fullname
fullname
end
=begin
@ -68,6 +70,52 @@ returns
=begin
get users activity stream
user = User.find(123)
result = user.activity_stream( 20 )
returns
result = [
{
:id =>2,
:o_id =>2,
:created_by_id => 3,
:created_at => '2013-09-28 00:57:21',
:object => "User",
:type => "created",
},
{
:id =>2,
:o_id =>2,
:created_by_id => 3,
:created_at => '2013-09-28 00:59:21',
:object => "User",
:type => "updated",
},
]
=end
def activity_stream( limit, fulldata = false )
activity_stream = ActivityStream.list( self, limit )
return activity_stream if !fulldata
# get related objects
assets = {}
activity_stream.each {|item|
record = Kernel.const_get( item['object'] ).find( item['o_id'] )
assets = record.assets(assets)
}
return {
:activity_stream => activity_stream,
:assets => assets,
}
end
=begin
authenticate user
result = User.authenticate(username, password)

View file

@ -28,6 +28,12 @@ returns
if !data[ User.to_app_model ][ self.id ]
data[ User.to_app_model ][ self.id ] = User.user_data_full( self.id )
end
if !data[ User.to_app_model ][ self['created_by_id'] ]
data[ User.to_app_model ][ self['created_by_id'] ] = User.user_data_full( self['created_by_id'] )
end
if !data[ User.to_app_model ][ self['updated_by_id'] ]
data[ User.to_app_model ][ self['updated_by_id'] ] = User.user_data_full( self['updated_by_id'] )
end
data
end

View file

@ -16,8 +16,8 @@ module Zammad
# -- all .rb files in that directory are automatically loaded.
# Custom directories with classes and modules you want to be autoloadable.
config.autoload_paths += Dir["#{config.root}/lib/*", "#{config.root}/lib/**/"]
# config.autoload_paths += %W(#{config.root}/lib)
# config.autoload_paths += Dir["#{config.root}/lib/*", "#{config.root}/lib/**/"]
config.autoload_paths += %W(#{config.root}/lib)
# Only load the plugins named here, in the order given (default is alphabetical).
# :all can be used as a placeholder for all plugins not explicitly named.

View file

@ -1,6 +1,6 @@
Zammad::Application.routes.draw do
api_path = Rails.configuration.api_path
match api_path + '/activity_stream', :to => 'activity#activity_stream', :via => :get
match api_path + '/activity_stream', :to => 'activity_stream#show', :via => :get
end

View file

@ -0,0 +1,41 @@
class CreateActivityStream < ActiveRecord::Migration
def up
create_table :activity_streams do |t|
t.references :activity_stream_type, :null => false
t.references :activity_stream_object, :null => false
t.references :role, :null => true
t.references :group, :null => true
t.column :o_id, :integer, :null => false
t.column :created_by_id, :integer, :null => false
t.timestamps
end
add_index :activity_streams, [:o_id]
add_index :activity_streams, [:created_by_id]
add_index :activity_streams, [:role_id]
add_index :activity_streams, [:group_id]
add_index :activity_streams, [:created_at]
add_index :activity_streams, [:activity_stream_object_id]
add_index :activity_streams, [:activity_stream_type_id]
create_table :activity_stream_types do |t|
t.column :name, :string, :limit => 250, :null => false
t.timestamps
end
add_index :activity_stream_types, [:name], :unique => true
create_table :activity_stream_objects do |t|
t.column :name, :string, :limit => 250, :null => false
t.column :note, :string, :limit => 250, :null => true
t.timestamps
end
add_index :activity_stream_objects, [:name], :unique => true
end
def down
drop_table :activity_streams
drop_table :activity_stream_objects
drop_table :activity_stream_types
end
end

View file

@ -4,13 +4,13 @@ module Sessions::Backend::ActivityStream
def self.worker( user, worker )
cache_key = 'user_' + user.id.to_s + '_activity_stream'
if Sessions::CacheIn.expired(cache_key)
activity_stream = History.activity_stream( user, 20 )
activity_stream = user.activity_stream( 20 )
activity_stream_cache = Sessions::CacheIn.get( cache_key, { :re_expire => true } )
worker.log 'notice', 'fetch activity_stream - ' + cache_key
if activity_stream != activity_stream_cache
worker.log 'notify', 'fetch activity_stream changed - ' + cache_key
activity_stream_full = History.activity_stream_fulldata( user, 20 )
activity_stream_full = user.activity_stream( 20, true )
Sessions::CacheIn.set( cache_key, activity_stream, { :expires_in => 0.75.minutes } )
Sessions::CacheIn.set( cache_key + '_push', activity_stream_full )
end

View file

@ -0,0 +1,199 @@
# encoding: utf-8
require 'test_helper'
class ActivityStreamTest < ActiveSupport::TestCase
test 'ticket+user' do
tests = [
# test 1
{
:create => {
:ticket => {
:group_id => Group.lookup( :name => 'Users' ).id,
:customer_id => User.lookup( :login => 'nicole.braun@zammad.org' ).id,
:owner_id => User.lookup( :login => '-' ).id,
:title => 'Unit Test 1 (äöüß)!',
:ticket_state_id => Ticket::State.lookup( :name => 'new' ).id,
:ticket_priority_id => Ticket::Priority.lookup( :name => '2 normal' ).id,
:updated_by_id => User.lookup( :login => 'nicole.braun@zammad.org' ).id,
:created_by_id => User.lookup( :login => 'nicole.braun@zammad.org' ).id,
},
:article => {
:updated_by_id => User.lookup( :login => 'nicole.braun@zammad.org' ).id,
:created_by_id => User.lookup( :login => 'nicole.braun@zammad.org' ).id,
:ticket_article_type_id => Ticket::Article::Type.lookup( :name => 'phone' ).id,
:ticket_article_sender_id => Ticket::Article::Sender.lookup( :name => 'Customer' ).id,
:from => 'Unit Test <unittest@example.com>',
:body => 'Unit Test 123',
:internal => false
},
},
:update => {
:ticket => {
:title => 'Unit Test 1 (äöüß) - update!',
:ticket_state_id => Ticket::State.lookup( :name => 'open' ).id,
:ticket_priority_id => Ticket::Priority.lookup( :name => '1 low' ).id,
},
},
:check => [
{
:object => 'Ticket',
:type => 'created',
},
{
:object => 'Ticket::Article',
:type => 'created',
},
{
:object => 'User',
:type => 'updated',
:o_id => User.lookup( :login => 'nicole.braun@zammad.org' ).id,
},
]
},
]
tickets = []
tests.each { |test|
ticket = nil
article = nil
# use transaction
ActiveRecord::Base.transaction do
ticket = Ticket.create( test[:create][:ticket] )
test[:check][0][:o_id] = ticket.id
test[:check][0][:created_at] = ticket.created_at
test[:check][0][:created_by_id] = User.lookup( :login => 'nicole.braun@zammad.org' ).id
sleep 2
test[:create][:article][:ticket_id] = ticket.id
article = Ticket::Article.create( test[:create][:article] )
test[:check][1][:o_id] = article.id
test[:check][1][:created_at] = article.created_at
test[:check][1][:created_by_id] = User.lookup( :login => 'nicole.braun@zammad.org' ).id
assert_equal( ticket.class.to_s, 'Ticket' )
assert_equal( article.class.to_s, 'Ticket::Article' )
# update ticket
if test[:update][:ticket]
ticket.update_attributes( test[:update][:ticket] )
test[:check][2][:o_id] = ticket.id
test[:check][2][:created_at] = ticket.created_at
test[:check][2][:created_by_id] = User.lookup( :login => 'nicole.braun@zammad.org' ).id
end
if test[:update][:article]
article.update_attributes( test[:update][:article] )
end
end
# remember ticket
tickets.push ticket
# check activity_stream
activity_stream_check( User.find(1).activity_stream(3), test[:check] )
}
# delete tickets
tickets.each { |ticket|
ticket_id = ticket.id
ticket.destroy
found = Ticket.where( :id => ticket_id ).first
assert( !found, "Ticket destroyed")
}
end
test 'organization' do
tests = [
# test 1
{
:create => {
:organization => {
:name => 'some name',
:updated_by_id => User.lookup( :login => 'nicole.braun@zammad.org' ).id,
:created_by_id => User.lookup( :login => 'nicole.braun@zammad.org' ).id,
},
},
:update1 => {
:organization => {
:name => 'some name (äöüß)',
},
},
:update2 => {
:organization => {
:name => 'some name 2 (äöüß)',
},
},
:check => [
{
:object => 'Organization',
:type => 'created',
},
{
:object => 'Organization',
:type => 'updated',
},
]
},
]
organizations = []
tests.each { |test|
organization = Organization.create( test[:create][:organization] )
test[:check][0][:o_id] = organization.id
test[:check][0][:created_at] = organization.created_at
test[:check][0][:created_by_id] = User.lookup( :login => 'nicole.braun@zammad.org' ).id
sleep 11
assert_equal( organization.class.to_s, 'Organization' )
if test[:update1][:organization]
organization.update_attributes( test[:update1][:organization] )
test[:check][1][:o_id] = organization.id
test[:check][1][:updated_at] = organization.updated_at
test[:check][1][:created_by_id] = User.lookup( :login => 'nicole.braun@zammad.org' ).id
sleep 2
end
if test[:update2][:organization]
organization.update_attributes( test[:update2][:organization] )
end
# remember organization
organizations.push organization
# check activity_stream
activity_stream_check( User.find(1).activity_stream(2), test[:check] )
}
# delete tickets
organizations.each { |organization|
organization_id = organization.id
organization.destroy
found = Organization.where( :id => organization_id ).first
assert( !found, "Organization destroyed")
}
end
def activity_stream_check( activity_stream_list, checks )
checks.each { |check_item|
# puts '+++++++++++'
# puts check_item.inspect
match = false
activity_stream_list.each { |item|
next if match
# puts '--------'
# puts item.inspect
# puts item.object
next if item['object'] != check_item[:object]
next if item['type'] != check_item[:type]
next if item['o_id'] != check_item[:o_id]
match = true
}
assert( match, "activity stream check not matched! #{check_item.inspect}")
}
end
end