Merge branch 'develop' into feature/ui2
This commit is contained in:
commit
88cc1cc401
43 changed files with 1789 additions and 945 deletions
|
@ -99,6 +99,9 @@ class _i18nSingleton extends Spine.Module
|
||||||
locale = 'en'
|
locale = 'en'
|
||||||
@locale = locale
|
@locale = locale
|
||||||
|
|
||||||
|
# set lang attribute of html tag
|
||||||
|
$('html').prop( 'lang', locale.substr(0, 2) )
|
||||||
|
|
||||||
@map = {}
|
@map = {}
|
||||||
App.Ajax.request(
|
App.Ajax.request(
|
||||||
id: 'i18n-set-' + locale,
|
id: 'i18n-set-' + locale,
|
||||||
|
|
|
@ -71,3 +71,8 @@ jQuery.event.special.remove = {
|
||||||
if (e.handler) e.handler();
|
if (e.handler) e.handler();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// start application
|
||||||
|
jQuery(function(){
|
||||||
|
new App.Run();
|
||||||
|
});
|
|
@ -1,7 +1,5 @@
|
||||||
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
|
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
require 'geoip'
|
|
||||||
|
|
||||||
class ApplicationController < ActionController::Base
|
class ApplicationController < ActionController::Base
|
||||||
# http_basic_authenticate_with :name => "test", :password => "ttt"
|
# http_basic_authenticate_with :name => "test", :password => "ttt"
|
||||||
|
|
||||||
|
@ -82,7 +80,7 @@ class ApplicationController < ActionController::Base
|
||||||
# check if remote ip need to be updated
|
# check if remote ip need to be updated
|
||||||
if !session[:remote_id] || session[:remote_id] != request.remote_ip
|
if !session[:remote_id] || session[:remote_id] != request.remote_ip
|
||||||
session[:remote_id] = request.remote_ip
|
session[:remote_id] = request.remote_ip
|
||||||
session[:geo] = Geoip.location( request.remote_ip )
|
session[:geo] = GeoIp.location( request.remote_ip )
|
||||||
end
|
end
|
||||||
|
|
||||||
# fill user agent
|
# fill user agent
|
||||||
|
|
|
@ -8,7 +8,7 @@ class TicketOverviewsController < ApplicationController
|
||||||
|
|
||||||
# get navbar overview data
|
# get navbar overview data
|
||||||
if !params[:view]
|
if !params[:view]
|
||||||
result = Ticket.overview(
|
result = Ticket::Overview.list(
|
||||||
:current_user => current_user,
|
:current_user => current_user,
|
||||||
)
|
)
|
||||||
render :json => result
|
render :json => result
|
||||||
|
@ -17,7 +17,7 @@ class TicketOverviewsController < ApplicationController
|
||||||
|
|
||||||
# get real overview data
|
# get real overview data
|
||||||
if params[:array]
|
if params[:array]
|
||||||
overview = Ticket.overview(
|
overview = Ticket::Overview.list(
|
||||||
:view => params[:view],
|
:view => params[:view],
|
||||||
:current_user => current_user,
|
:current_user => current_user,
|
||||||
:array => true,
|
:array => true,
|
||||||
|
@ -36,7 +36,7 @@ class TicketOverviewsController < ApplicationController
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
overview = Ticket.overview(
|
overview = Ticket::Overview.list(
|
||||||
:view => params[:view],
|
:view => params[:view],
|
||||||
# :view_mode => params[:view_mode],
|
# :view_mode => params[:view_mode],
|
||||||
:current_user => User.find( current_user.id ),
|
:current_user => User.find( current_user.id ),
|
||||||
|
@ -70,7 +70,7 @@ class TicketOverviewsController < ApplicationController
|
||||||
group_ids.push group.id
|
group_ids.push group.id
|
||||||
}
|
}
|
||||||
agents = {}
|
agents = {}
|
||||||
Ticket.agents.each { |user|
|
Ticket::ScreenOptions.agents.each { |user|
|
||||||
agents[ user.id ] = 1
|
agents[ user.id ] = 1
|
||||||
}
|
}
|
||||||
groups_users = {}
|
groups_users = {}
|
||||||
|
|
|
@ -111,7 +111,7 @@ class TicketsController < ApplicationController
|
||||||
def ticket_customer
|
def ticket_customer
|
||||||
|
|
||||||
# return result
|
# return result
|
||||||
result = Ticket.list_by_customer(
|
result = Ticket::ScreenOptions.list_by_customer(
|
||||||
:customer_id => params[:customer_id],
|
:customer_id => params[:customer_id],
|
||||||
:limit => 15,
|
:limit => 15,
|
||||||
)
|
)
|
||||||
|
@ -217,6 +217,9 @@ class TicketsController < ApplicationController
|
||||||
if !users[ data['created_by_id'] ]
|
if !users[ data['created_by_id'] ]
|
||||||
users[ data['created_by_id'] ] = User.user_data_full( data['created_by_id'] )
|
users[ data['created_by_id'] ] = User.user_data_full( data['created_by_id'] )
|
||||||
end
|
end
|
||||||
|
if !users[ data['updated_by_id'] ]
|
||||||
|
users[ data['updated_by_id'] ] = User.user_data_full( data['updated_by_id'] )
|
||||||
|
end
|
||||||
}
|
}
|
||||||
|
|
||||||
recent_viewed = RecentView.list_fulldata( current_user, 8 )
|
recent_viewed = RecentView.list_fulldata( current_user, 8 )
|
||||||
|
@ -326,7 +329,7 @@ class TicketsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
# get attributes to update
|
# get attributes to update
|
||||||
attributes_to_change = Ticket.attributes_to_change( :user => current_user, :ticket => ticket )
|
attributes_to_change = Ticket::ScreenOptions.attributes_to_change( :user => current_user, :ticket => ticket )
|
||||||
|
|
||||||
attributes_to_change[:owner_id].each { |user_id|
|
attributes_to_change[:owner_id].each { |user_id|
|
||||||
if !users[user_id]
|
if !users[user_id]
|
||||||
|
@ -384,7 +387,7 @@ class TicketsController < ApplicationController
|
||||||
def ticket_create
|
def ticket_create
|
||||||
|
|
||||||
# get attributes to update
|
# get attributes to update
|
||||||
attributes_to_change = Ticket.attributes_to_change(
|
attributes_to_change = Ticket::ScreenOptions.attributes_to_change(
|
||||||
:user => current_user,
|
:user => current_user,
|
||||||
# :ticket_id => params[:ticket_id],
|
# :ticket_id => params[:ticket_id],
|
||||||
# :article_id => params[:article_id]
|
# :article_id => params[:article_id]
|
||||||
|
|
|
@ -25,6 +25,18 @@ class ApplicationModel < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
remove all not used model attributes of params
|
||||||
|
|
||||||
|
result = Model.param_cleanup(params)
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = params # params with valid attributes of model
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
def self.param_cleanup(params)
|
def self.param_cleanup(params)
|
||||||
|
|
||||||
# only use object attributes
|
# only use object attributes
|
||||||
|
@ -40,6 +52,18 @@ class ApplicationModel < ActiveRecord::Base
|
||||||
self.param_validation(data)
|
self.param_validation(data)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
remove all not used params of object (per default :updated_at, :created_at, :updated_by_id and :created_by_id)
|
||||||
|
|
||||||
|
result = Model.param_validation(params)
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = params # params without listed attributes
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
def self.param_validation(data)
|
def self.param_validation(data)
|
||||||
|
|
||||||
# we do want to set this via database
|
# we do want to set this via database
|
||||||
|
@ -51,7 +75,20 @@ class ApplicationModel < ActiveRecord::Base
|
||||||
data
|
data
|
||||||
end
|
end
|
||||||
|
|
||||||
# set created_by_id & updated_by_id if not given based on UserInfo
|
=begin
|
||||||
|
|
||||||
|
set created_by_id & updated_by_id if not given based on UserInfo (current session)
|
||||||
|
|
||||||
|
Used as before_create callback, no own use needed
|
||||||
|
|
||||||
|
result = Model.fill_up_user_create(params)
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = params # params with updated_by_id & created_by_id if not given based on UserInfo (current session)
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
def fill_up_user_create
|
def fill_up_user_create
|
||||||
if self.class.column_names.include? 'updated_by_id'
|
if self.class.column_names.include? 'updated_by_id'
|
||||||
if UserInfo.current_user_id
|
if UserInfo.current_user_id
|
||||||
|
@ -71,7 +108,20 @@ class ApplicationModel < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# set updated_by_id if not given based on UserInfo
|
=begin
|
||||||
|
|
||||||
|
set updated_by_id if not given based on UserInfo (current session)
|
||||||
|
|
||||||
|
Used as before_update callback, no own use needed
|
||||||
|
|
||||||
|
result = Model.fill_up_user_update(params)
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = params # params with updated_by_id & created_by_id if not given based on UserInfo (current session)
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
def fill_up_user_update
|
def fill_up_user_update
|
||||||
return if !self.class.column_names.include? 'updated_by_id'
|
return if !self.class.column_names.include? 'updated_by_id'
|
||||||
if UserInfo.current_user_id
|
if UserInfo.current_user_id
|
||||||
|
@ -129,6 +179,20 @@ class ApplicationModel < ActiveRecord::Base
|
||||||
Cache.get( key.to_s )
|
Cache.get( key.to_s )
|
||||||
end
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
lookup model from cache (if exists) or retrieve it from db, id, name or login possible
|
||||||
|
|
||||||
|
result = Model.lookup( :id => 123 )
|
||||||
|
result = Model.lookup( :name => 'some name' )
|
||||||
|
result = Model.lookup( :login => 'some login' )
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = model # with all attributes
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
def self.lookup(data)
|
def self.lookup(data)
|
||||||
if data[:id]
|
if data[:id]
|
||||||
# puts "GET- + #{self.to_s}.#{data[:id].to_s}"
|
# puts "GET- + #{self.to_s}.#{data[:id].to_s}"
|
||||||
|
@ -168,6 +232,18 @@ class ApplicationModel < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
create model if not exists (check exists based on id, name, login or locale)
|
||||||
|
|
||||||
|
result = Model.create_if_not_exists( attributes )
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = model # with all attributes
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
def self.create_if_not_exists(data)
|
def self.create_if_not_exists(data)
|
||||||
if data[:id]
|
if data[:id]
|
||||||
record = self.where( :id => data[:id] ).first
|
record = self.where( :id => data[:id] ).first
|
||||||
|
@ -191,6 +267,18 @@ class ApplicationModel < ActiveRecord::Base
|
||||||
self.create(data)
|
self.create(data)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
create or update model (check exists based on name, login or locale)
|
||||||
|
|
||||||
|
result = Model.create_or_update( attributes )
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = model # with all attributes
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
def self.create_or_update(data)
|
def self.create_or_update(data)
|
||||||
if data[:name]
|
if data[:name]
|
||||||
records = self.where( :name => data[:name] )
|
records = self.where( :name => data[:name] )
|
||||||
|
@ -214,11 +302,37 @@ class ApplicationModel < ActiveRecord::Base
|
||||||
record = self.new( data )
|
record = self.new( data )
|
||||||
record.save
|
record.save
|
||||||
return record
|
return record
|
||||||
|
elsif data[:locale]
|
||||||
|
records = self.where( :locale => data[:locale] )
|
||||||
|
records.each {|record|
|
||||||
|
if record.locale.downcase == data[:locale].downcase
|
||||||
|
record.update_attributes( data )
|
||||||
|
return record
|
||||||
|
end
|
||||||
|
}
|
||||||
|
record = self.new( data )
|
||||||
|
record.save
|
||||||
|
return record
|
||||||
else
|
else
|
||||||
raise "Need name or login for create_or_update()"
|
raise "Need name, login or locale for create_or_update()"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
notify_clients_after_create after model got created
|
||||||
|
|
||||||
|
used as callback in model file
|
||||||
|
|
||||||
|
class OwnModel < ApplicationModel
|
||||||
|
after_create :notify_clients_after_create
|
||||||
|
after_update :notify_clients_after_update
|
||||||
|
after_destroy :notify_clients_after_destroy
|
||||||
|
|
||||||
|
[...]
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
def notify_clients_after_create
|
def notify_clients_after_create
|
||||||
|
|
||||||
# return if we run import mode
|
# return if we run import mode
|
||||||
|
@ -232,6 +346,21 @@ class ApplicationModel < ActiveRecord::Base
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
notify_clients_after_update after model got updated
|
||||||
|
|
||||||
|
used as callback in model file
|
||||||
|
|
||||||
|
class OwnModel < ApplicationModel
|
||||||
|
after_create :notify_clients_after_create
|
||||||
|
after_update :notify_clients_after_update
|
||||||
|
after_destroy :notify_clients_after_destroy
|
||||||
|
|
||||||
|
[...]
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
def notify_clients_after_update
|
def notify_clients_after_update
|
||||||
|
|
||||||
# return if we run import mode
|
# return if we run import mode
|
||||||
|
@ -245,6 +374,20 @@ class ApplicationModel < ActiveRecord::Base
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
notify_clients_after_destroy after model got destroyed
|
||||||
|
|
||||||
|
used as callback in model file
|
||||||
|
|
||||||
|
class OwnModel < ApplicationModel
|
||||||
|
after_create :notify_clients_after_create
|
||||||
|
after_update :notify_clients_after_update
|
||||||
|
after_destroy :notify_clients_after_destroy
|
||||||
|
|
||||||
|
[...]
|
||||||
|
|
||||||
|
=end
|
||||||
def notify_clients_after_destroy
|
def notify_clients_after_destroy
|
||||||
|
|
||||||
# return if we run import mode
|
# return if we run import mode
|
||||||
|
|
|
@ -333,7 +333,7 @@ class Channel::EmailParser
|
||||||
UserInfo.current_user_id = user.id
|
UserInfo.current_user_id = user.id
|
||||||
|
|
||||||
# get ticket# from subject
|
# get ticket# from subject
|
||||||
ticket = Ticket.number_check( mail[:subject] )
|
ticket = Ticket::Number.check( mail[:subject] )
|
||||||
|
|
||||||
# set ticket state to open if not new
|
# set ticket state to open if not new
|
||||||
if ticket
|
if ticket
|
||||||
|
|
|
@ -53,13 +53,8 @@ class Observer::User::Geo < ActiveRecord::Observer
|
||||||
# return if no address is given
|
# return if no address is given
|
||||||
return if address == ''
|
return if address == ''
|
||||||
|
|
||||||
# load adapter
|
# lookup
|
||||||
adapter = Setting.get('geo_backend')
|
latlng = GeoLocation.geocode( address )
|
||||||
return if !adapter
|
|
||||||
adapter_module = Object.const_get(adapter)
|
|
||||||
|
|
||||||
# db lookup
|
|
||||||
latlng = adapter_module.geocode(address)
|
|
||||||
return if !latlng
|
return if !latlng
|
||||||
|
|
||||||
# store data
|
# store data
|
||||||
|
|
|
@ -14,6 +14,6 @@ class Sla < ApplicationModel
|
||||||
private
|
private
|
||||||
def escalation_calculation_rebuild
|
def escalation_calculation_rebuild
|
||||||
Cache.delete( 'SLA::List::Active' )
|
Cache.delete( 'SLA::List::Active' )
|
||||||
Ticket.escalation_calculation_rebuild
|
Ticket::Escalation.rebuild_all
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,7 +4,7 @@ require 'time_calculation'
|
||||||
require 'sla'
|
require 'sla'
|
||||||
|
|
||||||
class Ticket < ApplicationModel
|
class Ticket < ApplicationModel
|
||||||
before_create :number_generate, :check_defaults
|
before_create :check_generate, :check_defaults
|
||||||
before_update :check_defaults
|
before_update :check_defaults
|
||||||
before_destroy :destroy_dependencies
|
before_destroy :destroy_dependencies
|
||||||
after_create :notify_clients_after_create
|
after_create :notify_clients_after_create
|
||||||
|
@ -22,103 +22,44 @@ class Ticket < ApplicationModel
|
||||||
belongs_to :create_article_type, :class_name => 'Ticket::Article::Type'
|
belongs_to :create_article_type, :class_name => 'Ticket::Article::Type'
|
||||||
belongs_to :create_article_sender, :class_name => 'Ticket::Article::Sender'
|
belongs_to :create_article_sender, :class_name => 'Ticket::Article::Sender'
|
||||||
|
|
||||||
|
include Ticket::Escalation
|
||||||
|
include Ticket::Subject
|
||||||
|
include Ticket::Permission
|
||||||
|
extend Ticket::Search
|
||||||
|
|
||||||
attr_accessor :callback_loop
|
attr_accessor :callback_loop
|
||||||
|
|
||||||
def self.number_check (string)
|
=begin
|
||||||
self.number_adapter.number_check_item(string)
|
|
||||||
end
|
list of agents in group of ticket
|
||||||
|
|
||||||
|
ticket = Ticket.find(123)
|
||||||
|
result = ticket.agent_of_group
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = [user1, user2, ...]
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
def agent_of_group
|
def agent_of_group
|
||||||
Group.find( self.group_id ).users.where( :active => true ).joins(:roles).where( 'roles.name' => 'Agent', 'roles.active' => true ).uniq()
|
Group.find( self.group_id ).users.where( :active => true ).joins(:roles).where( 'roles.name' => 'Agent', 'roles.active' => true ).uniq()
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.agents
|
=begin
|
||||||
User.where( :active => true ).joins(:roles).where( 'roles.name' => 'Agent', 'roles.active' => true ).uniq()
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.attributes_to_change(params)
|
merge tickets
|
||||||
if params[:ticket_id]
|
|
||||||
params[:ticket] = self.find( params[:ticket_id] )
|
|
||||||
end
|
|
||||||
if params[:article_id]
|
|
||||||
params[:article] = self.find( params[:article_id] )
|
|
||||||
end
|
|
||||||
|
|
||||||
# get ticket states
|
ticket = Ticket.find(123)
|
||||||
ticket_state_ids = []
|
result = ticket.merge_to(
|
||||||
if params[:ticket]
|
:ticket_id => 123,
|
||||||
ticket_state_type = params[:ticket].ticket_state.state_type
|
)
|
||||||
end
|
|
||||||
ticket_state_types = ['open', 'closed', 'pending action', 'pending reminder']
|
|
||||||
if ticket_state_type && !ticket_state_types.include?(ticket_state_type.name)
|
|
||||||
ticket_state_ids.push params[:ticket].ticket_state.id
|
|
||||||
end
|
|
||||||
ticket_state_types.each {|type|
|
|
||||||
ticket_state_type = Ticket::StateType.where( :name => type ).first
|
|
||||||
if ticket_state_type
|
|
||||||
ticket_state_type.states.each {|ticket_state|
|
|
||||||
ticket_state_ids.push ticket_state.id
|
|
||||||
}
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
# get owner
|
returns
|
||||||
owner_ids = []
|
|
||||||
if params[:ticket]
|
|
||||||
params[:ticket].agent_of_group.each { |user|
|
|
||||||
owner_ids.push user.id
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
# get group
|
result = true|false
|
||||||
group_ids = []
|
|
||||||
Group.where( :active => true ).each { |group|
|
|
||||||
group_ids.push group.id
|
|
||||||
}
|
|
||||||
|
|
||||||
# get group / user relations
|
=end
|
||||||
agents = {}
|
|
||||||
Ticket.agents.each { |user|
|
|
||||||
agents[ user.id ] = 1
|
|
||||||
}
|
|
||||||
groups_users = {}
|
|
||||||
group_ids.each {|group_id|
|
|
||||||
groups_users[ group_id ] = []
|
|
||||||
Group.find( group_id ).users.each {|user|
|
|
||||||
next if !agents[ user.id ]
|
|
||||||
groups_users[ group_id ].push user.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# get priorities
|
|
||||||
ticket_priority_ids = []
|
|
||||||
Ticket::Priority.where( :active => true ).each { |priority|
|
|
||||||
ticket_priority_ids.push priority.id
|
|
||||||
}
|
|
||||||
|
|
||||||
ticket_article_type_ids = []
|
|
||||||
if params[:ticket]
|
|
||||||
ticket_article_types = ['note', 'phone']
|
|
||||||
if params[:ticket].group.email_address_id
|
|
||||||
ticket_article_types.push 'email'
|
|
||||||
end
|
|
||||||
ticket_article_types.each {|ticket_article_type_name|
|
|
||||||
ticket_article_type = Ticket::Article::Type.lookup( :name => ticket_article_type_name )
|
|
||||||
if ticket_article_type
|
|
||||||
ticket_article_type_ids.push ticket_article_type.id
|
|
||||||
end
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
return {
|
|
||||||
:ticket_article_type_id => ticket_article_type_ids,
|
|
||||||
:ticket_state_id => ticket_state_ids,
|
|
||||||
:ticket_priority_id => ticket_priority_ids,
|
|
||||||
:owner_id => owner_ids,
|
|
||||||
:group_id => group_ids,
|
|
||||||
:group_id__owner_id => groups_users,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def merge_to(data)
|
def merge_to(data)
|
||||||
|
|
||||||
|
@ -157,724 +98,32 @@ class Ticket < ApplicationModel
|
||||||
self.save
|
self.save
|
||||||
end
|
end
|
||||||
|
|
||||||
# def self.agent
|
|
||||||
# Role.where( :name => ['Agent'], :active => true ).first.users.where( :active => true ).uniq()
|
|
||||||
# end
|
|
||||||
|
|
||||||
def subject_build (subject)
|
|
||||||
|
|
||||||
# clena subject
|
|
||||||
subject = self.subject_clean(subject)
|
|
||||||
|
|
||||||
ticket_hook = Setting.get('ticket_hook')
|
|
||||||
ticket_hook_divider = Setting.get('ticket_hook_divider')
|
|
||||||
|
|
||||||
# none position
|
|
||||||
if Setting.get('ticket_hook_position') == 'none'
|
|
||||||
return subject
|
|
||||||
end
|
|
||||||
|
|
||||||
# right position
|
|
||||||
if Setting.get('ticket_hook_position') == 'right'
|
|
||||||
return subject + " [#{ticket_hook}#{ticket_hook_divider}#{self.number}] "
|
|
||||||
end
|
|
||||||
|
|
||||||
# left position
|
|
||||||
return "[#{ticket_hook}#{ticket_hook_divider}#{self.number}] " + subject
|
|
||||||
end
|
|
||||||
|
|
||||||
def subject_clean (subject)
|
|
||||||
ticket_hook = Setting.get('ticket_hook')
|
|
||||||
ticket_hook_divider = Setting.get('ticket_hook_divider')
|
|
||||||
ticket_subject_size = Setting.get('ticket_subject_size')
|
|
||||||
|
|
||||||
# remove all possible ticket hook formats with []
|
|
||||||
subject = subject.gsub /\[#{ticket_hook}: #{self.number}\](\s+?|)/, ''
|
|
||||||
subject = subject.gsub /\[#{ticket_hook}:#{self.number}\](\s+?|)/, ''
|
|
||||||
subject = subject.gsub /\[#{ticket_hook}#{ticket_hook_divider}#{self.number}\](\s+?|)/, ''
|
|
||||||
|
|
||||||
# remove all possible ticket hook formats without []
|
|
||||||
subject = subject.gsub /#{ticket_hook}: #{self.number}(\s+?|)/, ''
|
|
||||||
subject = subject.gsub /#{ticket_hook}:#{self.number}(\s+?|)/, ''
|
|
||||||
subject = subject.gsub /#{ticket_hook}#{ticket_hook_divider}#{self.number}(\s+?|)/, ''
|
|
||||||
|
|
||||||
# remove leading "..:\s" and "..[\d+]:\s" e. g. "Re: " or "Re[5]: "
|
|
||||||
subject = subject.gsub /^(..(\[\d+\])?:\s)+/, ''
|
|
||||||
|
|
||||||
# resize subject based on config
|
|
||||||
if subject.length > ticket_subject_size.to_i
|
|
||||||
subject = subject[ 0, ticket_subject_size.to_i ] + '[...]'
|
|
||||||
end
|
|
||||||
|
|
||||||
return subject
|
|
||||||
end
|
|
||||||
|
|
||||||
# ticket.permission(
|
|
||||||
# :current_user => 123
|
|
||||||
# )
|
|
||||||
def permission (data)
|
|
||||||
|
|
||||||
# check customer
|
|
||||||
if data[:current_user].is_role('Customer')
|
|
||||||
|
|
||||||
# access ok if its own ticket
|
|
||||||
return true if self.customer_id == data[:current_user].id
|
|
||||||
|
|
||||||
# access ok if its organization ticket
|
|
||||||
if data[:current_user].organization_id && self.organization_id
|
|
||||||
return true if self.organization_id == data[:current_user].organization_id
|
|
||||||
end
|
|
||||||
|
|
||||||
# no access
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
# check agent
|
|
||||||
|
|
||||||
# access if requestor is owner
|
|
||||||
return true if self.owner_id == data[:current_user].id
|
|
||||||
|
|
||||||
# access if requestor is in group
|
|
||||||
data[:current_user].groups.each {|group|
|
|
||||||
return true if self.group.id == group.id
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
# Ticket.search(
|
|
||||||
# :current_user => 123,
|
|
||||||
# :query => 'search something',
|
|
||||||
# :limit => 15,
|
|
||||||
# )
|
|
||||||
def self.search (params)
|
|
||||||
|
|
||||||
# get params
|
|
||||||
query = params[:query]
|
|
||||||
limit = params[:limit] || 12
|
|
||||||
current_user = params[:current_user]
|
|
||||||
|
|
||||||
conditions = []
|
|
||||||
if current_user.is_role('Agent')
|
|
||||||
group_ids = Group.select( 'groups.id' ).joins(:users).
|
|
||||||
where( 'groups_users.user_id = ?', current_user.id ).
|
|
||||||
where( 'groups.active = ?', true ).
|
|
||||||
map( &:id )
|
|
||||||
conditions = [ 'group_id IN (?)', group_ids ]
|
|
||||||
else
|
|
||||||
if !current_user.organization || ( !current_user.organization.shared || current_user.organization.shared == false )
|
|
||||||
conditions = [ 'customer_id = ?', current_user.id ]
|
|
||||||
else
|
|
||||||
conditions = [ '( customer_id = ? OR organization_id = ? )', current_user.id, current_user.organization.id ]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# do query
|
|
||||||
tickets_all = Ticket.select('DISTINCT(tickets.id)').
|
|
||||||
where(conditions).
|
|
||||||
where( '( `tickets`.`title` LIKE ? OR `tickets`.`number` LIKE ? OR `ticket_articles`.`body` LIKE ? OR `ticket_articles`.`from` LIKE ? OR `ticket_articles`.`to` LIKE ? OR `ticket_articles`.`subject` LIKE ?)', "%#{query}%", "%#{query}%", "%#{query}%", "%#{query}%", "%#{query}%", "%#{query}%" ).
|
|
||||||
joins(:articles).
|
|
||||||
limit(limit).
|
|
||||||
order('`tickets`.`created_at` DESC')
|
|
||||||
|
|
||||||
# build result list
|
|
||||||
tickets = []
|
|
||||||
users = {}
|
|
||||||
tickets_all.each do |ticket|
|
|
||||||
ticket_tmp = Ticket.lookup( :id => ticket.id )
|
|
||||||
tickets.push ticket_tmp
|
|
||||||
end
|
|
||||||
|
|
||||||
return tickets
|
|
||||||
end
|
|
||||||
# Ticket.overview_list(
|
|
||||||
# :current_user => 123,
|
|
||||||
# )
|
|
||||||
def self.overview_list (data)
|
|
||||||
|
|
||||||
# get customer overviews
|
|
||||||
if data[:current_user].is_role('Customer')
|
|
||||||
role = data[:current_user].is_role( 'Customer' )
|
|
||||||
if data[:current_user].organization_id && data[:current_user].organization.shared
|
|
||||||
overviews = Overview.where( :role_id => role.id, :active => true )
|
|
||||||
else
|
|
||||||
overviews = Overview.where( :role_id => role.id, :organization_shared => false, :active => true )
|
|
||||||
end
|
|
||||||
return overviews
|
|
||||||
end
|
|
||||||
|
|
||||||
# get agent overviews
|
|
||||||
role = data[:current_user].is_role( 'Agent' )
|
|
||||||
overviews = Overview.where( :role_id => role.id, :active => true )
|
|
||||||
return overviews
|
|
||||||
end
|
|
||||||
|
|
||||||
# Ticket.overview(
|
|
||||||
# :view => 'some_view_url',
|
|
||||||
# :current_user => OBJECT,
|
|
||||||
# )
|
|
||||||
def self.overview (data)
|
|
||||||
|
|
||||||
overviews = self.overview_list(data)
|
|
||||||
|
|
||||||
# build up attributes hash
|
|
||||||
overview_selected = nil
|
|
||||||
overview_selected_raw = nil
|
|
||||||
|
|
||||||
overviews.each { |overview|
|
|
||||||
|
|
||||||
# remember selected view
|
|
||||||
if data[:view] && data[:view] == overview.link
|
|
||||||
overview_selected = overview
|
|
||||||
overview_selected_raw = Marshal.load( Marshal.dump(overview.attributes) )
|
|
||||||
end
|
|
||||||
|
|
||||||
# replace e.g. 'current_user.id' with current_user.id
|
|
||||||
overview.condition.each { |item, value |
|
|
||||||
if value && value.class.to_s == 'String'
|
|
||||||
parts = value.split( '.', 2 )
|
|
||||||
if parts[0] && parts[1] && parts[0] == 'current_user'
|
|
||||||
overview.condition[item] = data[:current_user][parts[1].to_sym]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if data[:view] && !overview_selected
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
# sortby
|
|
||||||
# prio
|
|
||||||
# state
|
|
||||||
# group
|
|
||||||
# customer
|
|
||||||
|
|
||||||
# order
|
|
||||||
# asc
|
|
||||||
# desc
|
|
||||||
|
|
||||||
# groupby
|
|
||||||
# prio
|
|
||||||
# state
|
|
||||||
# group
|
|
||||||
# customer
|
|
||||||
|
|
||||||
# all = attributes[:myopenassigned]
|
|
||||||
# all.merge( { :group_id => groups } )
|
|
||||||
|
|
||||||
# @tickets = Ticket.where(:group_id => groups, attributes[:myopenassigned] ).limit(params[:limit])
|
|
||||||
# get only tickets with permissions
|
|
||||||
if data[:current_user].is_role('Customer')
|
|
||||||
group_ids = Group.select( 'groups.id' ).
|
|
||||||
where( 'groups.active = ?', true ).
|
|
||||||
map( &:id )
|
|
||||||
else
|
|
||||||
group_ids = Group.select( 'groups.id' ).joins(:users).
|
|
||||||
where( 'groups_users.user_id = ?', [ data[:current_user].id ] ).
|
|
||||||
where( 'groups.active = ?', true ).
|
|
||||||
map( &:id )
|
|
||||||
end
|
|
||||||
|
|
||||||
# overview meta for navbar
|
|
||||||
if !overview_selected
|
|
||||||
|
|
||||||
# loop each overview
|
|
||||||
result = []
|
|
||||||
overviews.each { |overview|
|
|
||||||
|
|
||||||
# get count
|
|
||||||
count = Ticket.where( :group_id => group_ids ).where( self._condition( overview.condition ) ).count()
|
|
||||||
|
|
||||||
# get meta info
|
|
||||||
all = {
|
|
||||||
:name => overview.name,
|
|
||||||
:prio => overview.prio,
|
|
||||||
:link => overview.link,
|
|
||||||
}
|
|
||||||
|
|
||||||
# push to result data
|
|
||||||
result.push all.merge( { :count => count } )
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
|
|
||||||
# get result list
|
|
||||||
if data[:array]
|
|
||||||
order_by = overview_selected[:order][:by].to_s + ' ' + overview_selected[:order][:direction].to_s
|
|
||||||
if overview_selected.group_by && !overview_selected.group_by.empty?
|
|
||||||
order_by = overview_selected.group_by + '_id, ' + order_by
|
|
||||||
end
|
|
||||||
tickets = Ticket.select( 'id' ).
|
|
||||||
where( :group_id => group_ids ).
|
|
||||||
where( self._condition( overview_selected.condition ) ).
|
|
||||||
order( order_by ).
|
|
||||||
limit( 500 )
|
|
||||||
|
|
||||||
ticket_ids = []
|
|
||||||
tickets.each { |ticket|
|
|
||||||
ticket_ids.push ticket.id
|
|
||||||
}
|
|
||||||
|
|
||||||
tickets_count = Ticket.where( :group_id => group_ids ).
|
|
||||||
where( self._condition( overview_selected.condition ) ).
|
|
||||||
count()
|
|
||||||
|
|
||||||
return {
|
|
||||||
:ticket_list => ticket_ids,
|
|
||||||
:tickets_count => tickets_count,
|
|
||||||
:overview => overview_selected_raw,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
# get tickets for overview
|
|
||||||
data[:start_page] ||= 1
|
|
||||||
tickets = Ticket.where( :group_id => group_ids ).
|
|
||||||
where( self._condition( overview_selected.condition ) ).
|
|
||||||
order( overview_selected[:order][:by].to_s + ' ' + overview_selected[:order][:direction].to_s )#.
|
|
||||||
# limit( overview_selected.view[ data[:view_mode].to_sym ][:per_page] ).
|
|
||||||
# offset( overview_selected.view[ data[:view_mode].to_sym ][:per_page].to_i * ( data[:start_page].to_i - 1 ) )
|
|
||||||
|
|
||||||
tickets_count = Ticket.where( :group_id => group_ids ).
|
|
||||||
where( self._condition( overview_selected.condition ) ).
|
|
||||||
count()
|
|
||||||
|
|
||||||
return {
|
|
||||||
:tickets => tickets,
|
|
||||||
:tickets_count => tickets_count,
|
|
||||||
:overview => overview_selected_raw,
|
|
||||||
}
|
|
||||||
|
|
||||||
end
|
|
||||||
def self._condition(condition)
|
|
||||||
sql = ''
|
|
||||||
bind = [nil]
|
|
||||||
condition.each {|key, value|
|
|
||||||
if sql != ''
|
|
||||||
sql += ' AND '
|
|
||||||
end
|
|
||||||
if value.class == Array
|
|
||||||
sql += " #{key} IN (?)"
|
|
||||||
bind.push value
|
|
||||||
elsif value.class == Hash || value.class == ActiveSupport::HashWithIndifferentAccess
|
|
||||||
time = Time.now
|
|
||||||
if value['area'] == 'minute'
|
|
||||||
if value['direction'] == 'last'
|
|
||||||
time -= value['count'].to_i * 60
|
|
||||||
else
|
|
||||||
time += value['count'].to_i * 60
|
|
||||||
end
|
|
||||||
elsif value['area'] == 'hour'
|
|
||||||
if value['direction'] == 'last'
|
|
||||||
time -= value['count'].to_i * 60 * 60
|
|
||||||
else
|
|
||||||
time += value['count'].to_i * 60 * 60
|
|
||||||
end
|
|
||||||
elsif value['area'] == 'day'
|
|
||||||
if value['direction'] == 'last'
|
|
||||||
time -= value['count'].to_i * 60 * 60 * 24
|
|
||||||
else
|
|
||||||
time += value['count'].to_i * 60 * 60 * 24
|
|
||||||
end
|
|
||||||
elsif value['area'] == 'month'
|
|
||||||
if value['direction'] == 'last'
|
|
||||||
time -= value['count'].to_i * 60 * 60 * 24 * 31
|
|
||||||
else
|
|
||||||
time += value['count'].to_i * 60 * 60 * 24 * 31
|
|
||||||
end
|
|
||||||
elsif value['area'] == 'year'
|
|
||||||
if value['direction'] == 'last'
|
|
||||||
time -= value['count'].to_i * 60 * 60 * 24 * 365
|
|
||||||
else
|
|
||||||
time += value['count'].to_i * 60 * 60 * 24 * 365
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if value['direction'] == 'last'
|
|
||||||
sql += " #{key} > ?"
|
|
||||||
bind.push time
|
|
||||||
else
|
|
||||||
sql += " #{key} < ?"
|
|
||||||
bind.push time
|
|
||||||
end
|
|
||||||
else
|
|
||||||
sql += " #{key} = ?"
|
|
||||||
bind.push value
|
|
||||||
end
|
|
||||||
}
|
|
||||||
bind[0] = sql
|
|
||||||
return bind
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.number_adapter
|
|
||||||
|
|
||||||
# load backend based on config
|
|
||||||
adapter_name = Setting.get('ticket_number')
|
|
||||||
adapter = nil
|
|
||||||
case adapter_name
|
|
||||||
when Symbol, String
|
|
||||||
require "ticket/number/#{adapter_name.to_s.downcase}"
|
|
||||||
adapter = Ticket::Number.const_get("#{adapter_name.to_s.capitalize}")
|
|
||||||
else
|
|
||||||
raise "Missing number_adapter '#{adapter_name}'"
|
|
||||||
end
|
|
||||||
return adapter
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.escalation_calculation_rebuild
|
|
||||||
ticket_state_list_open = Ticket::State.by_category( 'open' )
|
|
||||||
|
|
||||||
tickets = Ticket.where( :ticket_state_id => ticket_state_list_open )
|
|
||||||
tickets.each {|ticket|
|
|
||||||
ticket.escalation_calculation
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def _escalation_calculation_get_sla
|
|
||||||
|
|
||||||
sla_selected = nil
|
|
||||||
sla_list = Cache.get( 'SLA::List::Active' )
|
|
||||||
if sla_list == nil
|
|
||||||
sla_list = Sla.where( :active => true ).all
|
|
||||||
Cache.write( 'SLA::List::Active', sla_list, { :expires_in => 1.hour } )
|
|
||||||
end
|
|
||||||
sla_list.each {|sla|
|
|
||||||
if !sla.condition || sla.condition.empty?
|
|
||||||
sla_selected = sla
|
|
||||||
elsif sla.condition
|
|
||||||
hit = false
|
|
||||||
map = [
|
|
||||||
[ 'tickets.ticket_priority_id', 'ticket_priority_id' ],
|
|
||||||
[ 'tickets.group_id', 'group_id' ]
|
|
||||||
]
|
|
||||||
map.each {|item|
|
|
||||||
if sla.condition[ item[0] ]
|
|
||||||
if sla.condition[ item[0] ].class == String
|
|
||||||
sla.condition[ item[0] ] = [ sla.condition[ item[0] ] ]
|
|
||||||
end
|
|
||||||
if sla.condition[ item[0] ].include?( self[ item[1] ].to_s )
|
|
||||||
hit = true
|
|
||||||
else
|
|
||||||
hit = false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
}
|
|
||||||
if hit
|
|
||||||
sla_selected = sla
|
|
||||||
end
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
return sla_selected
|
|
||||||
end
|
|
||||||
|
|
||||||
def _escalation_calculation_higher_time(escalation_time, check_time, done_time)
|
|
||||||
return escalation_time if done_time
|
|
||||||
return check_time if !escalation_time
|
|
||||||
return escalation_time if !check_time
|
|
||||||
return check_time if escalation_time > check_time
|
|
||||||
return escalation_time
|
|
||||||
end
|
|
||||||
|
|
||||||
def escalation_calculation
|
|
||||||
|
|
||||||
# set escalation off if ticket is already closed
|
|
||||||
ticket_state = Ticket::State.lookup( :id => self.ticket_state_id )
|
|
||||||
if ticket_state.ignore_escalation?
|
|
||||||
self.escalation_time = nil
|
|
||||||
# self.first_response_escal_date = nil
|
|
||||||
# self.close_time_escal_date = nil
|
|
||||||
self.callback_loop = true
|
|
||||||
self.save
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
# get sla for ticket
|
|
||||||
sla_selected = self._escalation_calculation_get_sla
|
|
||||||
|
|
||||||
# reset escalation if no sla is set
|
|
||||||
if !sla_selected
|
|
||||||
self.escalation_time = nil
|
|
||||||
# self.first_response_escal_date = nil
|
|
||||||
# self.close_time_escal_date = nil
|
|
||||||
self.callback_loop = true
|
|
||||||
self.save
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
# puts sla_selected.inspect
|
|
||||||
# puts days.inspect
|
|
||||||
self.escalation_time = nil
|
|
||||||
self.first_response_escal_date = nil
|
|
||||||
self.update_time_escal_date = nil
|
|
||||||
self.close_time_escal_date = nil
|
|
||||||
|
|
||||||
# first response
|
|
||||||
if sla_selected.first_response_time
|
|
||||||
|
|
||||||
# get escalation date without pending time
|
|
||||||
self.first_response_escal_date = TimeCalculation.dest_time( self.created_at, sla_selected.first_response_time, sla_selected.data, sla_selected.timezone )
|
|
||||||
|
|
||||||
# get pending time between created and first response escal. time
|
|
||||||
time_in_pending = escalation_suspend( self.created_at, self.first_response_escal_date, 'relative', sla_selected, sla_selected.first_response_time )
|
|
||||||
|
|
||||||
# get new escalation time (original escal_date + time_in_pending)
|
|
||||||
self.first_response_escal_date = TimeCalculation.dest_time( self.first_response_escal_date, time_in_pending.to_i, sla_selected.data, sla_selected.timezone )
|
|
||||||
|
|
||||||
# set ticket escalation
|
|
||||||
self.escalation_time = self._escalation_calculation_higher_time( self.escalation_time, self.first_response_escal_date, self.first_response )
|
|
||||||
end
|
|
||||||
if self.first_response# && !self.first_response_in_min
|
|
||||||
|
|
||||||
# get response time in min between created and first response
|
|
||||||
self.first_response_in_min = escalation_suspend( self.created_at, self.first_response, 'real', sla_selected )
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
# set time to show if sla is raised ot in
|
|
||||||
if sla_selected.first_response_time && self.first_response_in_min
|
|
||||||
self.first_response_diff_in_min = sla_selected.first_response_time - self.first_response_in_min
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# update time
|
|
||||||
last_update = self.last_contact_agent
|
|
||||||
if !last_update
|
|
||||||
last_update = self.created_at
|
|
||||||
end
|
|
||||||
if sla_selected.update_time
|
|
||||||
self.update_time_escal_date = TimeCalculation.dest_time( last_update, sla_selected.update_time, sla_selected.data, sla_selected.timezone )
|
|
||||||
|
|
||||||
# get pending time between created and update escal. time
|
|
||||||
time_in_pending = escalation_suspend( last_update, self.update_time_escal_date, 'relative', sla_selected, sla_selected.update_time )
|
|
||||||
|
|
||||||
# get new escalation time (original escal_date + time_in_pending)
|
|
||||||
self.update_time_escal_date = TimeCalculation.dest_time( self.update_time_escal_date, time_in_pending.to_i, sla_selected.data, sla_selected.timezone )
|
|
||||||
|
|
||||||
# set ticket escalation
|
|
||||||
self.escalation_time = self._escalation_calculation_higher_time( self.escalation_time, self.update_time_escal_date, false )
|
|
||||||
end
|
|
||||||
if self.last_contact_agent
|
|
||||||
self.update_time_in_min = TimeCalculation.business_time_diff( self.created_at, self.last_contact_agent, sla_selected.data, sla_selected.timezone )
|
|
||||||
end
|
|
||||||
|
|
||||||
# set sla time
|
|
||||||
if sla_selected.update_time && self.update_time_in_min
|
|
||||||
self.update_time_diff_in_min = sla_selected.update_time - self.update_time_in_min
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# close time
|
|
||||||
if sla_selected.close_time
|
|
||||||
|
|
||||||
# get escalation date without pending time
|
|
||||||
self.close_time_escal_date = TimeCalculation.dest_time( self.created_at, sla_selected.close_time, sla_selected.data, sla_selected.timezone )
|
|
||||||
|
|
||||||
# get pending time between created and close escal. time
|
|
||||||
extended_escalation = escalation_suspend( self.created_at, self.close_time_escal_date, 'relative', sla_selected, sla_selected.close_time )
|
|
||||||
|
|
||||||
# get new escalation time (original escal_date + time_in_pending)
|
|
||||||
self.close_time_escal_date = TimeCalculation.dest_time( self.close_time_escal_date, extended_escalation.to_i, sla_selected.data, sla_selected.timezone )
|
|
||||||
|
|
||||||
# set ticket escalation
|
|
||||||
self.escalation_time = self._escalation_calculation_higher_time( self.escalation_time, self.close_time_escal_date, self.close_time )
|
|
||||||
end
|
|
||||||
if self.close_time # && !self.close_time_in_min
|
|
||||||
self.close_time_in_min = escalation_suspend( self.created_at, self.close_time, 'real', sla_selected )
|
|
||||||
end
|
|
||||||
# set sla time
|
|
||||||
if sla_selected.close_time && self.close_time_in_min
|
|
||||||
self.close_time_diff_in_min = sla_selected.close_time - self.close_time_in_min
|
|
||||||
end
|
|
||||||
self.callback_loop = true
|
|
||||||
self.save
|
|
||||||
end
|
|
||||||
|
|
||||||
=begin
|
|
||||||
|
|
||||||
list tickets by customer groupd in state categroie open and closed
|
|
||||||
|
|
||||||
result = Ticket.list_by_customer(
|
|
||||||
:customer_id => 123,
|
|
||||||
:limit => 15, # optional, default 15
|
|
||||||
)
|
|
||||||
|
|
||||||
returns
|
|
||||||
|
|
||||||
result = {
|
|
||||||
:open => tickets_open,
|
|
||||||
:closed => tickets_closed,
|
|
||||||
}
|
|
||||||
|
|
||||||
=end
|
|
||||||
|
|
||||||
def self.list_by_customer(data)
|
|
||||||
|
|
||||||
# get closed/open states
|
|
||||||
ticket_state_list_open = Ticket::State.by_category( 'open' )
|
|
||||||
ticket_state_list_closed = Ticket::State.by_category( 'closed' )
|
|
||||||
|
|
||||||
# get tickets
|
|
||||||
tickets_open = Ticket.where(
|
|
||||||
:customer_id => data[:customer_id],
|
|
||||||
:ticket_state_id => ticket_state_list_open
|
|
||||||
).limit( data[:limit] || 15 ).order('created_at DESC')
|
|
||||||
|
|
||||||
tickets_closed = Ticket.where(
|
|
||||||
:customer_id => data[:customer_id],
|
|
||||||
:ticket_state_id => ticket_state_list_closed
|
|
||||||
).limit( data[:limit] || 15 ).order('created_at DESC')
|
|
||||||
|
|
||||||
return {
|
|
||||||
:open => tickets_open,
|
|
||||||
:closed => tickets_closed,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def number_generate
|
def check_generate
|
||||||
return if self.number
|
return if self.number
|
||||||
|
self.number = Ticket::Number.generate
|
||||||
# generate number
|
|
||||||
(1..25_000).each do |i|
|
|
||||||
number = Ticket.number_adapter.number_generate_item()
|
|
||||||
ticket = Ticket.where( :number => number ).first
|
|
||||||
if ticket != nil
|
|
||||||
number = Ticket.number_adapter.number_generate_item()
|
|
||||||
else
|
|
||||||
self.number = number
|
|
||||||
return number
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_defaults
|
def check_defaults
|
||||||
if !self.owner_id
|
if !self.owner_id
|
||||||
self.owner_id = 1
|
self.owner_id = 1
|
||||||
end
|
end
|
||||||
# if self.customer_id && ( !self.organization_id || self.organization_id.empty? )
|
|
||||||
if self.customer_id
|
if self.customer_id
|
||||||
customer = User.find( self.customer_id )
|
customer = User.find( self.customer_id )
|
||||||
if self.organization_id != customer.organization_id
|
if self.organization_id != customer.organization_id
|
||||||
self.organization_id = customer.organization_id
|
self.organization_id = customer.organization_id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy_dependencies
|
def destroy_dependencies
|
||||||
|
|
||||||
# delete history
|
# delete history
|
||||||
History.remove( 'Ticket', self.id )
|
History.remove( 'Ticket', self.id )
|
||||||
|
|
||||||
# delete articles
|
# delete articles
|
||||||
self.articles.destroy_all
|
self.articles.destroy_all
|
||||||
end
|
|
||||||
|
|
||||||
#type could be:
|
|
||||||
# real - time without supsend state
|
|
||||||
# relative - only suspend time
|
|
||||||
|
|
||||||
def escalation_suspend (start_time, end_time, type, sla_selected, sla_time = 0)
|
|
||||||
if type == 'relative'
|
|
||||||
end_time += sla_time * 60
|
|
||||||
end
|
|
||||||
total_time_without_pending = 0
|
|
||||||
total_time = 0
|
|
||||||
#get history for ticket
|
|
||||||
history_list = History.list( 'Ticket', self.id )
|
|
||||||
|
|
||||||
#loop through hist. changes and get time
|
|
||||||
last_state = nil
|
|
||||||
last_state_change = nil
|
|
||||||
last_state_is_pending = false
|
|
||||||
history_list.each { |history_item|
|
|
||||||
|
|
||||||
# ignore if it isn't a state change
|
|
||||||
next if !history_item.history_attribute_id
|
|
||||||
history_attribute = History::Attribute.lookup( :id => history_item.history_attribute_id );
|
|
||||||
next if history_attribute.name != 'ticket_state'
|
|
||||||
|
|
||||||
# ignore all newer state before start_time
|
|
||||||
next if history_item.created_at < start_time
|
|
||||||
|
|
||||||
# ignore all older state changes after end_time
|
|
||||||
next if last_state_change && last_state_change > end_time
|
|
||||||
|
|
||||||
# if created_at is later then end_time, use end_time as last time
|
|
||||||
if history_item.created_at > end_time
|
|
||||||
history_item.created_at = end_time
|
|
||||||
end
|
|
||||||
|
|
||||||
# get initial state and time
|
|
||||||
if !last_state
|
|
||||||
last_state = history_item.value_from
|
|
||||||
last_state_change = start_time
|
|
||||||
end
|
|
||||||
|
|
||||||
# check if time need to be counted
|
|
||||||
counted = true
|
|
||||||
if history_item.value_from == 'pending'
|
|
||||||
counted = false
|
|
||||||
elsif history_item.value_from == 'close'
|
|
||||||
counted = false
|
|
||||||
end
|
|
||||||
|
|
||||||
diff = escalation_time_diff( last_state_change, history_item.created_at, sla_selected )
|
|
||||||
if counted
|
|
||||||
puts "Diff count #{history_item.value_from} -> #{history_item.value_to} / #{last_state_change} -> #{history_item.created_at}"
|
|
||||||
total_time_without_pending = total_time_without_pending + diff
|
|
||||||
else
|
|
||||||
puts "Diff not count #{history_item.value_from} -> #{history_item.value_to} / #{last_state_change} -> #{history_item.created_at}"
|
|
||||||
end
|
|
||||||
total_time = total_time + diff
|
|
||||||
|
|
||||||
if history_item.value_to == 'pending'
|
|
||||||
last_state_is_pending = true
|
|
||||||
else
|
|
||||||
last_state_is_pending = false
|
|
||||||
end
|
|
||||||
|
|
||||||
# remember for next loop last state
|
|
||||||
last_state = history_item.value_to
|
|
||||||
last_state_change = history_item.created_at
|
|
||||||
}
|
|
||||||
|
|
||||||
# if last state isnt pending, count rest
|
|
||||||
if !last_state_is_pending && last_state_change && last_state_change < end_time
|
|
||||||
diff = escalation_time_diff( last_state_change, end_time, sla_selected )
|
|
||||||
puts "Diff count last state was not pending #{diff.to_s} - #{last_state_change} - #{end_time}"
|
|
||||||
total_time_without_pending = total_time_without_pending + diff
|
|
||||||
total_time = total_time + diff
|
|
||||||
end
|
|
||||||
|
|
||||||
# if we have not had any state change
|
|
||||||
if !last_state_change
|
|
||||||
diff = escalation_time_diff( start_time, end_time, sla_selected )
|
|
||||||
puts 'Diff state has not changed ' + diff.to_s
|
|
||||||
total_time_without_pending = total_time_without_pending + diff
|
|
||||||
total_time = total_time + diff
|
|
||||||
end
|
|
||||||
|
|
||||||
#return sum
|
|
||||||
if type == 'real'
|
|
||||||
return total_time_without_pending
|
|
||||||
elsif type == 'relative'
|
|
||||||
relative = total_time - total_time_without_pending
|
|
||||||
return relative
|
|
||||||
else
|
|
||||||
raise "ERROR: Unknown type #{type}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def escalation_time_diff( start_time, end_time, sla_selected )
|
|
||||||
if sla_selected
|
|
||||||
diff = TimeCalculation.business_time_diff( start_time, end_time, sla_selected.data, sla_selected.timezone)
|
|
||||||
else
|
|
||||||
diff = TimeCalculation.business_time_diff( start_time, end_time )
|
|
||||||
end
|
|
||||||
diff
|
|
||||||
end
|
|
||||||
|
|
||||||
class Number
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
305
app/models/ticket/escalation.rb
Normal file
305
app/models/ticket/escalation.rb
Normal file
|
@ -0,0 +1,305 @@
|
||||||
|
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
module Ticket::Escalation
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
rebuild escalations for all open tickets
|
||||||
|
|
||||||
|
result = Ticket::Escalation.rebuild_all
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = true
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
|
def self.rebuild_all
|
||||||
|
ticket_state_list_open = Ticket::State.by_category( 'open' )
|
||||||
|
|
||||||
|
tickets = Ticket.where( :ticket_state_id => ticket_state_list_open )
|
||||||
|
tickets.each {|ticket|
|
||||||
|
ticket.escalation_calculation
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
rebuild escalation for ticket
|
||||||
|
|
||||||
|
ticket = Ticket.find(123)
|
||||||
|
result = ticket.escalation_calculation
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = true
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
|
def escalation_calculation
|
||||||
|
|
||||||
|
# set escalation off if ticket is already closed
|
||||||
|
ticket_state = Ticket::State.lookup( :id => self.ticket_state_id )
|
||||||
|
if ticket_state.ignore_escalation?
|
||||||
|
self.escalation_time = nil
|
||||||
|
# self.first_response_escal_date = nil
|
||||||
|
# self.close_time_escal_date = nil
|
||||||
|
self.callback_loop = true
|
||||||
|
self.save
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
# get sla for ticket
|
||||||
|
sla_selected = escalation_calculation_get_sla
|
||||||
|
|
||||||
|
# reset escalation if no sla is set
|
||||||
|
if !sla_selected
|
||||||
|
self.escalation_time = nil
|
||||||
|
# self.first_response_escal_date = nil
|
||||||
|
# self.close_time_escal_date = nil
|
||||||
|
self.callback_loop = true
|
||||||
|
self.save
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
# puts sla_selected.inspect
|
||||||
|
# puts days.inspect
|
||||||
|
self.escalation_time = nil
|
||||||
|
self.first_response_escal_date = nil
|
||||||
|
self.update_time_escal_date = nil
|
||||||
|
self.close_time_escal_date = nil
|
||||||
|
|
||||||
|
# first response
|
||||||
|
if sla_selected.first_response_time
|
||||||
|
|
||||||
|
# get escalation date without pending time
|
||||||
|
self.first_response_escal_date = TimeCalculation.dest_time( self.created_at, sla_selected.first_response_time, sla_selected.data, sla_selected.timezone )
|
||||||
|
|
||||||
|
# get pending time between created and first response escal. time
|
||||||
|
time_in_pending = escalation_suspend( self.created_at, self.first_response_escal_date, 'relative', sla_selected, sla_selected.first_response_time )
|
||||||
|
|
||||||
|
# get new escalation time (original escal_date + time_in_pending)
|
||||||
|
self.first_response_escal_date = TimeCalculation.dest_time( self.first_response_escal_date, time_in_pending.to_i, sla_selected.data, sla_selected.timezone )
|
||||||
|
|
||||||
|
# set ticket escalation
|
||||||
|
self.escalation_time = calculation_higher_time( self.escalation_time, self.first_response_escal_date, self.first_response )
|
||||||
|
end
|
||||||
|
if self.first_response# && !self.first_response_in_min
|
||||||
|
|
||||||
|
# get response time in min between created and first response
|
||||||
|
self.first_response_in_min = escalation_suspend( self.created_at, self.first_response, 'real', sla_selected )
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
# set time to show if sla is raised ot in
|
||||||
|
if sla_selected.first_response_time && self.first_response_in_min
|
||||||
|
self.first_response_diff_in_min = sla_selected.first_response_time - self.first_response_in_min
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# update time
|
||||||
|
last_update = self.last_contact_agent
|
||||||
|
if !last_update
|
||||||
|
last_update = self.created_at
|
||||||
|
end
|
||||||
|
if sla_selected.update_time
|
||||||
|
self.update_time_escal_date = TimeCalculation.dest_time( last_update, sla_selected.update_time, sla_selected.data, sla_selected.timezone )
|
||||||
|
|
||||||
|
# get pending time between created and update escal. time
|
||||||
|
time_in_pending = escalation_suspend( last_update, self.update_time_escal_date, 'relative', sla_selected, sla_selected.update_time )
|
||||||
|
|
||||||
|
# get new escalation time (original escal_date + time_in_pending)
|
||||||
|
self.update_time_escal_date = TimeCalculation.dest_time( self.update_time_escal_date, time_in_pending.to_i, sla_selected.data, sla_selected.timezone )
|
||||||
|
|
||||||
|
# set ticket escalation
|
||||||
|
self.escalation_time = calculation_higher_time( self.escalation_time, self.update_time_escal_date, false )
|
||||||
|
end
|
||||||
|
if self.last_contact_agent
|
||||||
|
self.update_time_in_min = TimeCalculation.business_time_diff( self.created_at, self.last_contact_agent, sla_selected.data, sla_selected.timezone )
|
||||||
|
end
|
||||||
|
|
||||||
|
# set sla time
|
||||||
|
if sla_selected.update_time && self.update_time_in_min
|
||||||
|
self.update_time_diff_in_min = sla_selected.update_time - self.update_time_in_min
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# close time
|
||||||
|
if sla_selected.close_time
|
||||||
|
|
||||||
|
# get escalation date without pending time
|
||||||
|
self.close_time_escal_date = TimeCalculation.dest_time( self.created_at, sla_selected.close_time, sla_selected.data, sla_selected.timezone )
|
||||||
|
|
||||||
|
# get pending time between created and close escal. time
|
||||||
|
extended_escalation = escalation_suspend( self.created_at, self.close_time_escal_date, 'relative', sla_selected, sla_selected.close_time )
|
||||||
|
|
||||||
|
# get new escalation time (original escal_date + time_in_pending)
|
||||||
|
self.close_time_escal_date = TimeCalculation.dest_time( self.close_time_escal_date, extended_escalation.to_i, sla_selected.data, sla_selected.timezone )
|
||||||
|
|
||||||
|
# set ticket escalation
|
||||||
|
self.escalation_time = calculation_higher_time( self.escalation_time, self.close_time_escal_date, self.close_time )
|
||||||
|
end
|
||||||
|
if self.close_time # && !self.close_time_in_min
|
||||||
|
self.close_time_in_min = escalation_suspend( self.created_at, self.close_time, 'real', sla_selected )
|
||||||
|
end
|
||||||
|
# set sla time
|
||||||
|
if sla_selected.close_time && self.close_time_in_min
|
||||||
|
self.close_time_diff_in_min = sla_selected.close_time - self.close_time_in_min
|
||||||
|
end
|
||||||
|
self.callback_loop = true
|
||||||
|
self.save
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
#type could be:
|
||||||
|
# real - time without supsend state
|
||||||
|
# relative - only suspend time
|
||||||
|
|
||||||
|
def escalation_suspend (start_time, end_time, type, sla_selected, sla_time = 0)
|
||||||
|
if type == 'relative'
|
||||||
|
end_time += sla_time * 60
|
||||||
|
end
|
||||||
|
total_time_without_pending = 0
|
||||||
|
total_time = 0
|
||||||
|
#get history for ticket
|
||||||
|
history_list = History.list( 'Ticket', self.id )
|
||||||
|
|
||||||
|
#loop through hist. changes and get time
|
||||||
|
last_state = nil
|
||||||
|
last_state_change = nil
|
||||||
|
last_state_is_pending = false
|
||||||
|
history_list.each { |history_item|
|
||||||
|
|
||||||
|
# ignore if it isn't a state change
|
||||||
|
next if !history_item.history_attribute_id
|
||||||
|
history_attribute = History::Attribute.lookup( :id => history_item.history_attribute_id );
|
||||||
|
next if history_attribute.name != 'ticket_state'
|
||||||
|
|
||||||
|
# ignore all newer state before start_time
|
||||||
|
next if history_item.created_at < start_time
|
||||||
|
|
||||||
|
# ignore all older state changes after end_time
|
||||||
|
next if last_state_change && last_state_change > end_time
|
||||||
|
|
||||||
|
# if created_at is later then end_time, use end_time as last time
|
||||||
|
if history_item.created_at > end_time
|
||||||
|
history_item.created_at = end_time
|
||||||
|
end
|
||||||
|
|
||||||
|
# get initial state and time
|
||||||
|
if !last_state
|
||||||
|
last_state = history_item.value_from
|
||||||
|
last_state_change = start_time
|
||||||
|
end
|
||||||
|
|
||||||
|
# check if time need to be counted
|
||||||
|
counted = true
|
||||||
|
if history_item.value_from == 'pending'
|
||||||
|
counted = false
|
||||||
|
elsif history_item.value_from == 'close'
|
||||||
|
counted = false
|
||||||
|
end
|
||||||
|
|
||||||
|
diff = escalation_time_diff( last_state_change, history_item.created_at, sla_selected )
|
||||||
|
if counted
|
||||||
|
puts "Diff count #{history_item.value_from} -> #{history_item.value_to} / #{last_state_change} -> #{history_item.created_at}"
|
||||||
|
total_time_without_pending = total_time_without_pending + diff
|
||||||
|
else
|
||||||
|
puts "Diff not count #{history_item.value_from} -> #{history_item.value_to} / #{last_state_change} -> #{history_item.created_at}"
|
||||||
|
end
|
||||||
|
total_time = total_time + diff
|
||||||
|
|
||||||
|
if history_item.value_to == 'pending'
|
||||||
|
last_state_is_pending = true
|
||||||
|
else
|
||||||
|
last_state_is_pending = false
|
||||||
|
end
|
||||||
|
|
||||||
|
# remember for next loop last state
|
||||||
|
last_state = history_item.value_to
|
||||||
|
last_state_change = history_item.created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
# if last state isnt pending, count rest
|
||||||
|
if !last_state_is_pending && last_state_change && last_state_change < end_time
|
||||||
|
diff = escalation_time_diff( last_state_change, end_time, sla_selected )
|
||||||
|
puts "Diff count last state was not pending #{diff.to_s} - #{last_state_change} - #{end_time}"
|
||||||
|
total_time_without_pending = total_time_without_pending + diff
|
||||||
|
total_time = total_time + diff
|
||||||
|
end
|
||||||
|
|
||||||
|
# if we have not had any state change
|
||||||
|
if !last_state_change
|
||||||
|
diff = escalation_time_diff( start_time, end_time, sla_selected )
|
||||||
|
puts 'Diff state has not changed ' + diff.to_s
|
||||||
|
total_time_without_pending = total_time_without_pending + diff
|
||||||
|
total_time = total_time + diff
|
||||||
|
end
|
||||||
|
|
||||||
|
#return sum
|
||||||
|
if type == 'real'
|
||||||
|
return total_time_without_pending
|
||||||
|
elsif type == 'relative'
|
||||||
|
relative = total_time - total_time_without_pending
|
||||||
|
return relative
|
||||||
|
else
|
||||||
|
raise "ERROR: Unknown type #{type}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def escalation_time_diff( start_time, end_time, sla_selected )
|
||||||
|
if sla_selected
|
||||||
|
diff = TimeCalculation.business_time_diff( start_time, end_time, sla_selected.data, sla_selected.timezone)
|
||||||
|
else
|
||||||
|
diff = TimeCalculation.business_time_diff( start_time, end_time )
|
||||||
|
end
|
||||||
|
diff
|
||||||
|
end
|
||||||
|
|
||||||
|
def escalation_calculation_get_sla
|
||||||
|
|
||||||
|
sla_selected = nil
|
||||||
|
sla_list = Cache.get( 'SLA::List::Active' )
|
||||||
|
if sla_list == nil
|
||||||
|
sla_list = Sla.where( :active => true ).all
|
||||||
|
Cache.write( 'SLA::List::Active', sla_list, { :expires_in => 1.hour } )
|
||||||
|
end
|
||||||
|
sla_list.each {|sla|
|
||||||
|
if !sla.condition || sla.condition.empty?
|
||||||
|
sla_selected = sla
|
||||||
|
elsif sla.condition
|
||||||
|
hit = false
|
||||||
|
map = [
|
||||||
|
[ 'tickets.ticket_priority_id', 'ticket_priority_id' ],
|
||||||
|
[ 'tickets.group_id', 'group_id' ]
|
||||||
|
]
|
||||||
|
map.each {|item|
|
||||||
|
if sla.condition[ item[0] ]
|
||||||
|
if sla.condition[ item[0] ].class == String
|
||||||
|
sla.condition[ item[0] ] = [ sla.condition[ item[0] ] ]
|
||||||
|
end
|
||||||
|
if sla.condition[ item[0] ].include?( self[ item[1] ].to_s )
|
||||||
|
hit = true
|
||||||
|
else
|
||||||
|
hit = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}
|
||||||
|
if hit
|
||||||
|
sla_selected = sla
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
return sla_selected
|
||||||
|
end
|
||||||
|
|
||||||
|
def calculation_higher_time(escalation_time, check_time, done_time)
|
||||||
|
return escalation_time if done_time
|
||||||
|
return check_time if !escalation_time
|
||||||
|
return escalation_time if !check_time
|
||||||
|
return check_time if escalation_time > check_time
|
||||||
|
return escalation_time
|
||||||
|
end
|
||||||
|
end
|
58
app/models/ticket/number.rb
Normal file
58
app/models/ticket/number.rb
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
class Ticket::Number < ApplicationLib
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
generate new ticket number
|
||||||
|
|
||||||
|
result = Ticket::Number.generate
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = "1234556" # new ticket number
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
|
def self.generate
|
||||||
|
|
||||||
|
# generate number
|
||||||
|
(1..50_000).each { |i|
|
||||||
|
number = adapter.generate
|
||||||
|
ticket = Ticket.where( :number => number ).first
|
||||||
|
return number if !ticket
|
||||||
|
}
|
||||||
|
raise "Can't generate new ticket number!"
|
||||||
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
check if string contrains a valid ticket number
|
||||||
|
|
||||||
|
result = Ticket::Number.check('some string [Ticket#123456]')
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = ticket # Ticket model of ticket with matching ticket number
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
|
def self.check(string)
|
||||||
|
adapter.check(string)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.adapter
|
||||||
|
|
||||||
|
# load backend based on config
|
||||||
|
adapter_name = Setting.get('ticket_number')
|
||||||
|
if !adapter_name
|
||||||
|
raise "Missing ticket_number setting option"
|
||||||
|
end
|
||||||
|
adapter = self.load_adapter(adapter_name)
|
||||||
|
if !adapter
|
||||||
|
raise "Can't load ticket_number adapter '#{adapter_name}'"
|
||||||
|
end
|
||||||
|
adapter
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
module Ticket::Number::Date
|
module Ticket::Number::Date
|
||||||
extend self
|
extend self
|
||||||
|
|
||||||
def number_generate_item
|
def generate
|
||||||
|
|
||||||
# get config
|
# get config
|
||||||
config = Setting.get('ticket_number_date')
|
config = Setting.get('ticket_number_date')
|
||||||
|
@ -64,7 +64,7 @@ module Ticket::Number::Date
|
||||||
end
|
end
|
||||||
return number
|
return number
|
||||||
end
|
end
|
||||||
def number_check_item (string)
|
def check(string)
|
||||||
|
|
||||||
# get config
|
# get config
|
||||||
system_id = Setting.get('system_id') || ''
|
system_id = Setting.get('system_id') || ''
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
module Ticket::Number::Increment
|
module Ticket::Number::Increment
|
||||||
extend self
|
extend self
|
||||||
|
|
||||||
def number_generate_item
|
def generate
|
||||||
|
|
||||||
# get config
|
# get config
|
||||||
config = Setting.get('ticket_number_increment')
|
config = Setting.get('ticket_number_increment')
|
||||||
|
@ -68,7 +68,7 @@ module Ticket::Number::Increment
|
||||||
return number
|
return number
|
||||||
end
|
end
|
||||||
|
|
||||||
def number_check_item (string)
|
def check(string)
|
||||||
|
|
||||||
# get config
|
# get config
|
||||||
system_id = Setting.get('system_id') || ''
|
system_id = Setting.get('system_id') || ''
|
||||||
|
|
253
app/models/ticket/overview.rb
Normal file
253
app/models/ticket/overview.rb
Normal file
|
@ -0,0 +1,253 @@
|
||||||
|
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
require 'overview'
|
||||||
|
|
||||||
|
class Ticket::Overview
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
all overview by user
|
||||||
|
|
||||||
|
result = Ticket::Overview.all(
|
||||||
|
:current_user => User.find(123),
|
||||||
|
)
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = [overview1, overview2]
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
|
def self.all (data)
|
||||||
|
|
||||||
|
# get customer overviews
|
||||||
|
if data[:current_user].is_role('Customer')
|
||||||
|
role = data[:current_user].is_role( 'Customer' )
|
||||||
|
if data[:current_user].organization_id && data[:current_user].organization.shared
|
||||||
|
overviews = Overview.where( :role_id => role.id, :active => true )
|
||||||
|
else
|
||||||
|
overviews = Overview.where( :role_id => role.id, :organization_shared => false, :active => true )
|
||||||
|
end
|
||||||
|
return overviews
|
||||||
|
end
|
||||||
|
|
||||||
|
# get agent overviews
|
||||||
|
role = data[:current_user].is_role( 'Agent' )
|
||||||
|
overviews = Overview.where( :role_id => role.id, :active => true )
|
||||||
|
return overviews
|
||||||
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
selected overview by user
|
||||||
|
|
||||||
|
result = Ticket::Overview.list(
|
||||||
|
:current_user => User.find(123),
|
||||||
|
:view => 'some_view_url',
|
||||||
|
)
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = {
|
||||||
|
:tickets => tickets, # [ticket1, ticket2, ticket3]
|
||||||
|
:tickets_count => tickets_count, # count of tickets
|
||||||
|
:overview => overview_selected_raw, # overview attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
|
def self.list (data)
|
||||||
|
|
||||||
|
overviews = self.all(data)
|
||||||
|
|
||||||
|
# build up attributes hash
|
||||||
|
overview_selected = nil
|
||||||
|
overview_selected_raw = nil
|
||||||
|
|
||||||
|
overviews.each { |overview|
|
||||||
|
|
||||||
|
# remember selected view
|
||||||
|
if data[:view] && data[:view] == overview.link
|
||||||
|
overview_selected = overview
|
||||||
|
overview_selected_raw = Marshal.load( Marshal.dump(overview.attributes) )
|
||||||
|
end
|
||||||
|
|
||||||
|
# replace e.g. 'current_user.id' with current_user.id
|
||||||
|
overview.condition.each { |item, value |
|
||||||
|
if value && value.class.to_s == 'String'
|
||||||
|
parts = value.split( '.', 2 )
|
||||||
|
if parts[0] && parts[1] && parts[0] == 'current_user'
|
||||||
|
overview.condition[item] = data[:current_user][parts[1].to_sym]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if data[:view] && !overview_selected
|
||||||
|
raise "No such view '#{ data[:view] }'"
|
||||||
|
end
|
||||||
|
|
||||||
|
# sortby
|
||||||
|
# prio
|
||||||
|
# state
|
||||||
|
# group
|
||||||
|
# customer
|
||||||
|
|
||||||
|
# order
|
||||||
|
# asc
|
||||||
|
# desc
|
||||||
|
|
||||||
|
# groupby
|
||||||
|
# prio
|
||||||
|
# state
|
||||||
|
# group
|
||||||
|
# customer
|
||||||
|
|
||||||
|
# all = attributes[:myopenassigned]
|
||||||
|
# all.merge( { :group_id => groups } )
|
||||||
|
|
||||||
|
# @tickets = Ticket.where(:group_id => groups, attributes[:myopenassigned] ).limit(params[:limit])
|
||||||
|
# get only tickets with permissions
|
||||||
|
if data[:current_user].is_role('Customer')
|
||||||
|
group_ids = Group.select( 'groups.id' ).
|
||||||
|
where( 'groups.active = ?', true ).
|
||||||
|
map( &:id )
|
||||||
|
else
|
||||||
|
group_ids = Group.select( 'groups.id' ).joins(:users).
|
||||||
|
where( 'groups_users.user_id = ?', [ data[:current_user].id ] ).
|
||||||
|
where( 'groups.active = ?', true ).
|
||||||
|
map( &:id )
|
||||||
|
end
|
||||||
|
|
||||||
|
# overview meta for navbar
|
||||||
|
if !overview_selected
|
||||||
|
|
||||||
|
# loop each overview
|
||||||
|
result = []
|
||||||
|
overviews.each { |overview|
|
||||||
|
|
||||||
|
# get count
|
||||||
|
count = Ticket.where( :group_id => group_ids ).where( _condition( overview.condition ) ).count()
|
||||||
|
|
||||||
|
# get meta info
|
||||||
|
all = {
|
||||||
|
:name => overview.name,
|
||||||
|
:prio => overview.prio,
|
||||||
|
:link => overview.link,
|
||||||
|
}
|
||||||
|
|
||||||
|
# push to result data
|
||||||
|
result.push all.merge( { :count => count } )
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
# get result list
|
||||||
|
if data[:array]
|
||||||
|
order_by = overview_selected[:order][:by].to_s + ' ' + overview_selected[:order][:direction].to_s
|
||||||
|
if overview_selected.group_by && !overview_selected.group_by.empty?
|
||||||
|
order_by = overview_selected.group_by + '_id, ' + order_by
|
||||||
|
end
|
||||||
|
tickets = Ticket.select( 'id' ).
|
||||||
|
where( :group_id => group_ids ).
|
||||||
|
where( _condition( overview_selected.condition ) ).
|
||||||
|
order( order_by ).
|
||||||
|
limit( 500 )
|
||||||
|
|
||||||
|
ticket_ids = []
|
||||||
|
tickets.each { |ticket|
|
||||||
|
ticket_ids.push ticket.id
|
||||||
|
}
|
||||||
|
|
||||||
|
tickets_count = Ticket.where( :group_id => group_ids ).
|
||||||
|
where( _condition( overview_selected.condition ) ).
|
||||||
|
count()
|
||||||
|
|
||||||
|
return {
|
||||||
|
:ticket_list => ticket_ids,
|
||||||
|
:tickets_count => tickets_count,
|
||||||
|
:overview => overview_selected_raw,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# get tickets for overview
|
||||||
|
data[:start_page] ||= 1
|
||||||
|
tickets = Ticket.where( :group_id => group_ids ).
|
||||||
|
where( _condition( overview_selected.condition ) ).
|
||||||
|
order( overview_selected[:order][:by].to_s + ' ' + overview_selected[:order][:direction].to_s )#.
|
||||||
|
# limit( overview_selected.view[ data[:view_mode].to_sym ][:per_page] ).
|
||||||
|
# offset( overview_selected.view[ data[:view_mode].to_sym ][:per_page].to_i * ( data[:start_page].to_i - 1 ) )
|
||||||
|
|
||||||
|
tickets_count = Ticket.where( :group_id => group_ids ).
|
||||||
|
where( _condition( overview_selected.condition ) ).
|
||||||
|
count()
|
||||||
|
|
||||||
|
return {
|
||||||
|
:tickets => tickets,
|
||||||
|
:tickets_count => tickets_count,
|
||||||
|
:overview => overview_selected_raw,
|
||||||
|
}
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def self._condition(condition)
|
||||||
|
sql = ''
|
||||||
|
bind = [nil]
|
||||||
|
condition.each {|key, value|
|
||||||
|
if sql != ''
|
||||||
|
sql += ' AND '
|
||||||
|
end
|
||||||
|
if value.class == Array
|
||||||
|
sql += " #{key} IN (?)"
|
||||||
|
bind.push value
|
||||||
|
elsif value.class == Hash || value.class == ActiveSupport::HashWithIndifferentAccess
|
||||||
|
time = Time.now
|
||||||
|
if value['area'] == 'minute'
|
||||||
|
if value['direction'] == 'last'
|
||||||
|
time -= value['count'].to_i * 60
|
||||||
|
else
|
||||||
|
time += value['count'].to_i * 60
|
||||||
|
end
|
||||||
|
elsif value['area'] == 'hour'
|
||||||
|
if value['direction'] == 'last'
|
||||||
|
time -= value['count'].to_i * 60 * 60
|
||||||
|
else
|
||||||
|
time += value['count'].to_i * 60 * 60
|
||||||
|
end
|
||||||
|
elsif value['area'] == 'day'
|
||||||
|
if value['direction'] == 'last'
|
||||||
|
time -= value['count'].to_i * 60 * 60 * 24
|
||||||
|
else
|
||||||
|
time += value['count'].to_i * 60 * 60 * 24
|
||||||
|
end
|
||||||
|
elsif value['area'] == 'month'
|
||||||
|
if value['direction'] == 'last'
|
||||||
|
time -= value['count'].to_i * 60 * 60 * 24 * 31
|
||||||
|
else
|
||||||
|
time += value['count'].to_i * 60 * 60 * 24 * 31
|
||||||
|
end
|
||||||
|
elsif value['area'] == 'year'
|
||||||
|
if value['direction'] == 'last'
|
||||||
|
time -= value['count'].to_i * 60 * 60 * 24 * 365
|
||||||
|
else
|
||||||
|
time += value['count'].to_i * 60 * 60 * 24 * 365
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if value['direction'] == 'last'
|
||||||
|
sql += " #{key} > ?"
|
||||||
|
bind.push time
|
||||||
|
else
|
||||||
|
sql += " #{key} < ?"
|
||||||
|
bind.push time
|
||||||
|
end
|
||||||
|
else
|
||||||
|
sql += " #{key} = ?"
|
||||||
|
bind.push value
|
||||||
|
end
|
||||||
|
}
|
||||||
|
bind[0] = sql
|
||||||
|
return bind
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
47
app/models/ticket/permission.rb
Normal file
47
app/models/ticket/permission.rb
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
module Ticket::Permission
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
check if user has access to ticket
|
||||||
|
|
||||||
|
ticket = Ticket.find(123)
|
||||||
|
result = ticket.permission( :current_user => User.find(123) )
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = true|false
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
|
def permission (data)
|
||||||
|
|
||||||
|
# check customer
|
||||||
|
if data[:current_user].is_role('Customer')
|
||||||
|
|
||||||
|
# access ok if its own ticket
|
||||||
|
return true if self.customer_id == data[:current_user].id
|
||||||
|
|
||||||
|
# access ok if its organization ticket
|
||||||
|
if data[:current_user].organization_id && self.organization_id
|
||||||
|
return true if self.organization_id == data[:current_user].organization_id
|
||||||
|
end
|
||||||
|
|
||||||
|
# no access
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
# check agent
|
||||||
|
|
||||||
|
# access if requestor is owner
|
||||||
|
return true if self.owner_id == data[:current_user].id
|
||||||
|
|
||||||
|
# access if requestor is in group
|
||||||
|
data[:current_user].groups.each {|group|
|
||||||
|
return true if self.group.id == group.id
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
170
app/models/ticket/screen_options.rb
Normal file
170
app/models/ticket/screen_options.rb
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
module Ticket::ScreenOptions
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
list of active agents
|
||||||
|
|
||||||
|
result = Ticket::ScreenOptions.agents()
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = [user1, user2]
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
|
def self.agents
|
||||||
|
User.where( :active => true ).joins(:roles).where( 'roles.name' => 'Agent', 'roles.active' => true ).uniq()
|
||||||
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
list attributes
|
||||||
|
|
||||||
|
result = Ticket::ScreenOptions.attributes_to_change(
|
||||||
|
:ticket_id => 123,
|
||||||
|
:article_id => 123,
|
||||||
|
|
||||||
|
:ticket => ticket_model,
|
||||||
|
)
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = {
|
||||||
|
:ticket_article_type_id => ticket_article_type_ids,
|
||||||
|
:ticket_state_id => ticket_state_ids,
|
||||||
|
:ticket_priority_id => ticket_priority_ids,
|
||||||
|
:owner_id => owner_ids,
|
||||||
|
:group_id => group_ids,
|
||||||
|
:group_id__owner_id => groups_users,
|
||||||
|
}
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
|
def self.attributes_to_change(params)
|
||||||
|
if params[:ticket_id]
|
||||||
|
params[:ticket] = self.find( params[:ticket_id] )
|
||||||
|
end
|
||||||
|
if params[:article_id]
|
||||||
|
params[:article] = self.find( params[:article_id] )
|
||||||
|
end
|
||||||
|
|
||||||
|
# get ticket states
|
||||||
|
ticket_state_ids = []
|
||||||
|
if params[:ticket]
|
||||||
|
ticket_state_type = params[:ticket].ticket_state.state_type
|
||||||
|
end
|
||||||
|
ticket_state_types = ['open', 'closed', 'pending action', 'pending reminder']
|
||||||
|
if ticket_state_type && !ticket_state_types.include?(ticket_state_type.name)
|
||||||
|
ticket_state_ids.push params[:ticket].ticket_state.id
|
||||||
|
end
|
||||||
|
ticket_state_types.each {|type|
|
||||||
|
ticket_state_type = Ticket::StateType.where( :name => type ).first
|
||||||
|
if ticket_state_type
|
||||||
|
ticket_state_type.states.each {|ticket_state|
|
||||||
|
ticket_state_ids.push ticket_state.id
|
||||||
|
}
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
# get owner
|
||||||
|
owner_ids = []
|
||||||
|
if params[:ticket]
|
||||||
|
params[:ticket].agent_of_group.each { |user|
|
||||||
|
owner_ids.push user.id
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# get group
|
||||||
|
group_ids = []
|
||||||
|
Group.where( :active => true ).each { |group|
|
||||||
|
group_ids.push group.id
|
||||||
|
}
|
||||||
|
|
||||||
|
# get group / user relations
|
||||||
|
agents = {}
|
||||||
|
Ticket::ScreenOptions.agents.each { |user|
|
||||||
|
agents[ user.id ] = 1
|
||||||
|
}
|
||||||
|
groups_users = {}
|
||||||
|
group_ids.each {|group_id|
|
||||||
|
groups_users[ group_id ] = []
|
||||||
|
Group.find( group_id ).users.each {|user|
|
||||||
|
next if !agents[ user.id ]
|
||||||
|
groups_users[ group_id ].push user.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# get priorities
|
||||||
|
ticket_priority_ids = []
|
||||||
|
Ticket::Priority.where( :active => true ).each { |priority|
|
||||||
|
ticket_priority_ids.push priority.id
|
||||||
|
}
|
||||||
|
|
||||||
|
ticket_article_type_ids = []
|
||||||
|
if params[:ticket]
|
||||||
|
ticket_article_types = ['note', 'phone']
|
||||||
|
if params[:ticket].group.email_address_id
|
||||||
|
ticket_article_types.push 'email'
|
||||||
|
end
|
||||||
|
ticket_article_types.each {|ticket_article_type_name|
|
||||||
|
ticket_article_type = Ticket::Article::Type.lookup( :name => ticket_article_type_name )
|
||||||
|
if ticket_article_type
|
||||||
|
ticket_article_type_ids.push ticket_article_type.id
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
:ticket_article_type_id => ticket_article_type_ids,
|
||||||
|
:ticket_state_id => ticket_state_ids,
|
||||||
|
:ticket_priority_id => ticket_priority_ids,
|
||||||
|
:owner_id => owner_ids,
|
||||||
|
:group_id => group_ids,
|
||||||
|
:group_id__owner_id => groups_users,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
list tickets by customer groupd in state categroie open and closed
|
||||||
|
|
||||||
|
result = Ticket::ScreenOptions.list_by_customer(
|
||||||
|
:customer_id => 123,
|
||||||
|
:limit => 15, # optional, default 15
|
||||||
|
)
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = {
|
||||||
|
:open => tickets_open,
|
||||||
|
:closed => tickets_closed,
|
||||||
|
}
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
|
def self.list_by_customer(data)
|
||||||
|
|
||||||
|
# get closed/open states
|
||||||
|
ticket_state_list_open = Ticket::State.by_category( 'open' )
|
||||||
|
ticket_state_list_closed = Ticket::State.by_category( 'closed' )
|
||||||
|
|
||||||
|
# get tickets
|
||||||
|
tickets_open = Ticket.where(
|
||||||
|
:customer_id => data[:customer_id],
|
||||||
|
:ticket_state_id => ticket_state_list_open
|
||||||
|
).limit( data[:limit] || 15 ).order('created_at DESC')
|
||||||
|
|
||||||
|
tickets_closed = Ticket.where(
|
||||||
|
:customer_id => data[:customer_id],
|
||||||
|
:ticket_state_id => ticket_state_list_closed
|
||||||
|
).limit( data[:limit] || 15 ).order('created_at DESC')
|
||||||
|
|
||||||
|
return {
|
||||||
|
:open => tickets_open,
|
||||||
|
:closed => tickets_closed,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
62
app/models/ticket/search.rb
Normal file
62
app/models/ticket/search.rb
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
module Ticket::Search
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
search tickets
|
||||||
|
|
||||||
|
result = Ticket.search(
|
||||||
|
:current_user => User.find(123),
|
||||||
|
:query => 'search something',
|
||||||
|
:limit => 15,
|
||||||
|
)
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = [ticket_model1, ticket_model2]
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
|
def search (params)
|
||||||
|
|
||||||
|
# get params
|
||||||
|
query = params[:query]
|
||||||
|
limit = params[:limit] || 12
|
||||||
|
current_user = params[:current_user]
|
||||||
|
|
||||||
|
conditions = []
|
||||||
|
if current_user.is_role('Agent')
|
||||||
|
group_ids = Group.select( 'groups.id' ).joins(:users).
|
||||||
|
where( 'groups_users.user_id = ?', current_user.id ).
|
||||||
|
where( 'groups.active = ?', true ).
|
||||||
|
map( &:id )
|
||||||
|
conditions = [ 'group_id IN (?)', group_ids ]
|
||||||
|
else
|
||||||
|
if !current_user.organization || ( !current_user.organization.shared || current_user.organization.shared == false )
|
||||||
|
conditions = [ 'customer_id = ?', current_user.id ]
|
||||||
|
else
|
||||||
|
conditions = [ '( customer_id = ? OR organization_id = ? )', current_user.id, current_user.organization.id ]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# do query
|
||||||
|
tickets_all = Ticket.select('DISTINCT(tickets.id)').
|
||||||
|
where(conditions).
|
||||||
|
where( '( `tickets`.`title` LIKE ? OR `tickets`.`number` LIKE ? OR `ticket_articles`.`body` LIKE ? OR `ticket_articles`.`from` LIKE ? OR `ticket_articles`.`to` LIKE ? OR `ticket_articles`.`subject` LIKE ?)', "%#{query}%", "%#{query}%", "%#{query}%", "%#{query}%", "%#{query}%", "%#{query}%" ).
|
||||||
|
joins(:articles).
|
||||||
|
limit(limit).
|
||||||
|
order('`tickets`.`created_at` DESC')
|
||||||
|
|
||||||
|
# build result list
|
||||||
|
tickets = []
|
||||||
|
users = {}
|
||||||
|
tickets_all.each do |ticket|
|
||||||
|
ticket_tmp = Ticket.lookup( :id => ticket.id )
|
||||||
|
tickets.push ticket_tmp
|
||||||
|
end
|
||||||
|
|
||||||
|
return tickets
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
78
app/models/ticket/subject.rb
Normal file
78
app/models/ticket/subject.rb
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
module Ticket::Subject
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
build new subject with ticket number in there
|
||||||
|
|
||||||
|
ticket = Ticket.find(123)
|
||||||
|
result = ticket.subject_build('some subject')
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = "[Ticket#1234567] some subject"
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
|
def subject_build (subject)
|
||||||
|
|
||||||
|
# clena subject
|
||||||
|
subject = self.subject_clean(subject)
|
||||||
|
|
||||||
|
ticket_hook = Setting.get('ticket_hook')
|
||||||
|
ticket_hook_divider = Setting.get('ticket_hook_divider')
|
||||||
|
|
||||||
|
# none position
|
||||||
|
if Setting.get('ticket_hook_position') == 'none'
|
||||||
|
return subject
|
||||||
|
end
|
||||||
|
|
||||||
|
# right position
|
||||||
|
if Setting.get('ticket_hook_position') == 'right'
|
||||||
|
return subject + " [#{ticket_hook}#{ticket_hook_divider}#{self.number}] "
|
||||||
|
end
|
||||||
|
|
||||||
|
# left position
|
||||||
|
return "[#{ticket_hook}#{ticket_hook_divider}#{self.number}] " + subject
|
||||||
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
clean subject remove ticket number and other not needed chars
|
||||||
|
|
||||||
|
ticket = Ticket.find(123)
|
||||||
|
result = ticket.subject_clean('[Ticket#1234567] some subject')
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = "some subject"
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
|
def subject_clean (subject)
|
||||||
|
ticket_hook = Setting.get('ticket_hook')
|
||||||
|
ticket_hook_divider = Setting.get('ticket_hook_divider')
|
||||||
|
ticket_subject_size = Setting.get('ticket_subject_size')
|
||||||
|
|
||||||
|
# remove all possible ticket hook formats with []
|
||||||
|
subject = subject.gsub /\[#{ticket_hook}: #{self.number}\](\s+?|)/, ''
|
||||||
|
subject = subject.gsub /\[#{ticket_hook}:#{self.number}\](\s+?|)/, ''
|
||||||
|
subject = subject.gsub /\[#{ticket_hook}#{ticket_hook_divider}#{self.number}\](\s+?|)/, ''
|
||||||
|
|
||||||
|
# remove all possible ticket hook formats without []
|
||||||
|
subject = subject.gsub /#{ticket_hook}: #{self.number}(\s+?|)/, ''
|
||||||
|
subject = subject.gsub /#{ticket_hook}:#{self.number}(\s+?|)/, ''
|
||||||
|
subject = subject.gsub /#{ticket_hook}#{ticket_hook_divider}#{self.number}(\s+?|)/, ''
|
||||||
|
|
||||||
|
# remove leading "..:\s" and "..[\d+]:\s" e. g. "Re: " or "Re[5]: "
|
||||||
|
subject = subject.gsub /^(..(\[\d+\])?:\s)+/, ''
|
||||||
|
|
||||||
|
# resize subject based on config
|
||||||
|
if subject.length > ticket_subject_size.to_i
|
||||||
|
subject = subject[ 0, ticket_subject_size.to_i ] + '[...]'
|
||||||
|
end
|
||||||
|
|
||||||
|
return subject
|
||||||
|
end
|
||||||
|
end
|
|
@ -19,6 +19,19 @@ class User < ApplicationModel
|
||||||
|
|
||||||
store :preferences
|
store :preferences
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
fullname of user
|
||||||
|
|
||||||
|
user = User.find(123)
|
||||||
|
result = user.fulename
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = "Bob Smith"
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
def fullname
|
def fullname
|
||||||
fullname = ''
|
fullname = ''
|
||||||
if self.firstname
|
if self.firstname
|
||||||
|
@ -33,6 +46,19 @@ class User < ApplicationModel
|
||||||
return fullname
|
return fullname
|
||||||
end
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
check if user is in role
|
||||||
|
|
||||||
|
user = User.find(123)
|
||||||
|
result = user.is_role('Customer')
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = true|false
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
def is_role( role_name )
|
def is_role( role_name )
|
||||||
self.roles.each { |role|
|
self.roles.each { |role|
|
||||||
return role if role.name == role_name
|
return role if role.name == role_name
|
||||||
|
@ -40,6 +66,18 @@ class User < ApplicationModel
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
authenticate user
|
||||||
|
|
||||||
|
result = User.authenticate(username, password)
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = user_model # user model if authentication was successfully
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
def self.authenticate( username, password )
|
def self.authenticate( username, password )
|
||||||
|
|
||||||
# do not authenticate with nothing
|
# do not authenticate with nothing
|
||||||
|
@ -60,98 +98,52 @@ class User < ApplicationModel
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
# use auth backends
|
user_auth = Auth.check( username, password, user )
|
||||||
config = [
|
|
||||||
{
|
|
||||||
:adapter => 'internal',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
:adapter => 'test',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
Setting.where( :area => 'Security::Authentication' ).each {|setting|
|
|
||||||
if setting.state[:value]
|
|
||||||
config.push setting.state[:value]
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
# try to login against configure auth backends
|
|
||||||
user_auth = nil
|
|
||||||
config.each {|config_item|
|
|
||||||
next if !config_item[:adapter]
|
|
||||||
next if config_item.class == TrueClass
|
|
||||||
file = "auth/#{config_item[:adapter]}"
|
|
||||||
require file
|
|
||||||
user_auth = Auth.const_get("#{config_item[:adapter].to_s.upcase}").check( username, password, config_item, user )
|
|
||||||
|
|
||||||
# auth ok
|
|
||||||
if user_auth
|
|
||||||
|
|
||||||
# remember last login date
|
|
||||||
user_auth.update_last_login
|
|
||||||
|
|
||||||
# reset login failed
|
|
||||||
user_auth.login_failed = 0
|
|
||||||
user_auth.save
|
|
||||||
|
|
||||||
return user_auth
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
# set login failed +1
|
# set login failed +1
|
||||||
if !user_auth && user
|
if !user_auth && user
|
||||||
|
sleep 1
|
||||||
user.login_failed = user.login_failed + 1
|
user.login_failed = user.login_failed + 1
|
||||||
user.save
|
user.save
|
||||||
end
|
end
|
||||||
|
|
||||||
# auth failed
|
# auth ok
|
||||||
sleep 1
|
|
||||||
return user_auth
|
return user_auth
|
||||||
end
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
authenticate user agains sso
|
||||||
|
|
||||||
|
result = User.sso(sso_params)
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = user_model # user model if authentication was successfully
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
def self.sso(params)
|
def self.sso(params)
|
||||||
|
|
||||||
# use auth backends
|
|
||||||
config = [
|
|
||||||
{
|
|
||||||
:adapter => 'env',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
:adapter => 'otrs',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
# Setting.where( :area => 'Security::Authentication' ).each {|setting|
|
|
||||||
# if setting.state[:value]
|
|
||||||
# config.push setting.state[:value]
|
|
||||||
# end
|
|
||||||
# }
|
|
||||||
|
|
||||||
# try to login against configure auth backends
|
# try to login against configure auth backends
|
||||||
user_auth = nil
|
user_auth = Sso.check( params, user )
|
||||||
config.each {|config_item|
|
return if !user_auth
|
||||||
next if !config_item[:adapter]
|
|
||||||
next if config_item.class == TrueClass
|
|
||||||
file = "sso/#{config_item[:adapter]}"
|
|
||||||
require file
|
|
||||||
user_auth = SSO.const_get("#{config_item[:adapter].to_s.upcase}").check( params, config_item )
|
|
||||||
|
|
||||||
# auth ok
|
return user_auth
|
||||||
if user_auth
|
|
||||||
|
|
||||||
# remember last login date
|
|
||||||
user_auth.update_last_login
|
|
||||||
|
|
||||||
# reset login failed
|
|
||||||
user_auth.login_failed = 0
|
|
||||||
user_auth.save
|
|
||||||
|
|
||||||
return user_auth
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
create user from from omni auth hash
|
||||||
|
|
||||||
|
result = User.create_from_hash!(hash)
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = user_model # user model if create was successfully
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
def self.create_from_hash!(hash)
|
def self.create_from_hash!(hash)
|
||||||
url = ''
|
url = ''
|
||||||
if hash['info']['urls'] then
|
if hash['info']['urls'] then
|
||||||
|
@ -173,6 +165,18 @@ class User < ApplicationModel
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
send reset password email with token to user
|
||||||
|
|
||||||
|
result = User.password_reset_send(username)
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = true|false
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
def self.password_reset_send(username)
|
def self.password_reset_send(username)
|
||||||
return if !username || username == ''
|
return if !username || username == ''
|
||||||
|
|
||||||
|
@ -230,7 +234,18 @@ class User < ApplicationModel
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
# check token
|
=begin
|
||||||
|
|
||||||
|
check reset password token
|
||||||
|
|
||||||
|
result = User.password_reset_check(token)
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = user_model # user_model if token was verified
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
def self.password_reset_check(token)
|
def self.password_reset_check(token)
|
||||||
user = Token.check( :action => 'PasswordReset', :name => token )
|
user = Token.check( :action => 'PasswordReset', :name => token )
|
||||||
|
|
||||||
|
@ -242,6 +257,18 @@ class User < ApplicationModel
|
||||||
return user
|
return user
|
||||||
end
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
reset reset password with token and set new password
|
||||||
|
|
||||||
|
result = User.password_reset_via_token(token,password)
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = user_model # user_model if token was verified
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
def self.password_reset_via_token(token,password)
|
def self.password_reset_via_token(token,password)
|
||||||
|
|
||||||
# check token
|
# check token
|
||||||
|
@ -256,6 +283,22 @@ class User < ApplicationModel
|
||||||
return user
|
return user
|
||||||
end
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
search user
|
||||||
|
|
||||||
|
result = User.search(
|
||||||
|
:query => 'some search term'
|
||||||
|
:limit => 15,
|
||||||
|
:current_user => user_model,
|
||||||
|
)
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = [user_model1, user_model2, ...]
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
def self.search(params)
|
def self.search(params)
|
||||||
|
|
||||||
# get params
|
# get params
|
||||||
|
@ -378,6 +421,19 @@ class User < ApplicationModel
|
||||||
return user
|
return user
|
||||||
end
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
update last login date (is automatically done by auth and sso backend)
|
||||||
|
|
||||||
|
user = User.find(123)
|
||||||
|
result = user.update_last_login
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = new_user_model
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
def update_last_login
|
def update_last_login
|
||||||
self.last_login = Time.now
|
self.last_login = Time.now
|
||||||
self.save
|
self.save
|
||||||
|
|
|
@ -6,8 +6,3 @@
|
||||||
<div id="splash">
|
<div id="splash">
|
||||||
<div class="logo">booting...</div>
|
<div class="logo">booting...</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="text/javascript">
|
|
||||||
jQuery(function(){
|
|
||||||
new App.Run();
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -3,7 +3,11 @@
|
||||||
<head>
|
<head>
|
||||||
<title><%= Setting.get('product_name') %></title>
|
<title><%= Setting.get('product_name') %></title>
|
||||||
<%= stylesheet_link_tag "application" %>
|
<%= stylesheet_link_tag "application" %>
|
||||||
|
<% if Rails.configuration.assets.debug %>
|
||||||
<%= javascript_include_tag "application" %>
|
<%= javascript_include_tag "application" %>
|
||||||
|
<% else %>
|
||||||
|
<%= javascript_include_tag "application", :defer => 'defer' %>
|
||||||
|
<% end %>
|
||||||
<%= csrf_meta_tags %>
|
<%= csrf_meta_tags %>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
Zammad::Application.configure do
|
||||||
|
# Settings specified here will take precedence over those in config/application.rb
|
||||||
|
|
||||||
|
# The test environment is used exclusively to run your application's
|
||||||
|
# test suite. You never need to work with it otherwise. Remember that
|
||||||
|
# your test database is "scratch space" for the test suite and is wiped
|
||||||
|
# and recreated between test runs. Don't rely on the data there!
|
||||||
|
config.cache_classes = true
|
||||||
|
|
||||||
|
# Configure static asset server for tests with Cache-Control for performance
|
||||||
|
config.serve_static_assets = true
|
||||||
|
config.static_cache_control = "public, max-age=3600"
|
||||||
|
|
||||||
|
config.assets.compress = false
|
||||||
|
config.assets.compile = true
|
||||||
|
config.assets.digest = true
|
||||||
|
|
||||||
|
# Log error messages when you accidentally call methods on nil
|
||||||
|
config.whiny_nils = true
|
||||||
|
|
||||||
|
# Show full error reports and disable caching
|
||||||
|
config.consider_all_requests_local = true
|
||||||
|
config.action_controller.perform_caching = true
|
||||||
|
|
||||||
|
# Raise exceptions instead of rendering exception templates
|
||||||
|
config.action_dispatch.show_exceptions = false
|
||||||
|
|
||||||
|
# Tell Action Mailer not to deliver emails to the real world.
|
||||||
|
# The :test delivery method accumulates sent emails in the
|
||||||
|
# ActionMailer::Base.deliveries array.
|
||||||
|
config.action_mailer.delivery_method = :test
|
||||||
|
|
||||||
|
# Raise exception on mass assignment protection for Active Record models
|
||||||
|
config.active_record.mass_assignment_sanitizer = :strict
|
||||||
|
|
||||||
|
# Print deprecation notices to the stderr
|
||||||
|
config.active_support.deprecation = :stderr
|
||||||
|
|
||||||
|
# Disable request forgery protection in test environment
|
||||||
|
config.action_controller.allow_forgery_protection = false
|
||||||
|
|
||||||
|
# autoload on
|
||||||
|
config.dependency_loading = true
|
||||||
|
|
||||||
|
end
|
52
db/migrate/20130815000001_update_geo2.rb
Normal file
52
db/migrate/20130815000001_update_geo2.rb
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
class UpdateGeo2 < ActiveRecord::Migration
|
||||||
|
def up
|
||||||
|
Setting.where( :name => 'geo_backend' ).destroy_all
|
||||||
|
Setting.create_if_not_exists(
|
||||||
|
:title => 'Geo Location Backend',
|
||||||
|
:name => 'geo_location_backend',
|
||||||
|
:area => 'System::Geo',
|
||||||
|
:description => 'Defines the backend for geo location lookups.',
|
||||||
|
:options => {
|
||||||
|
:form => [
|
||||||
|
{
|
||||||
|
:display => '',
|
||||||
|
:null => true,
|
||||||
|
:name => 'geo_location_backend',
|
||||||
|
:tag => 'select',
|
||||||
|
:options => {
|
||||||
|
'' => '-',
|
||||||
|
'GeoLocation::Gmaps' => 'Google Maps',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
:state => 'GeoLocation::Gmaps',
|
||||||
|
:frontend => false
|
||||||
|
)
|
||||||
|
Setting.create_if_not_exists(
|
||||||
|
:title => 'Geo IP Backend',
|
||||||
|
:name => 'geo_ip_backend',
|
||||||
|
:area => 'System::Geo',
|
||||||
|
:description => 'Defines the backend for geo ip lookups.',
|
||||||
|
:options => {
|
||||||
|
:form => [
|
||||||
|
{
|
||||||
|
:display => '',
|
||||||
|
:null => true,
|
||||||
|
:name => 'geo_ip_backend',
|
||||||
|
:tag => 'select',
|
||||||
|
:options => {
|
||||||
|
'' => '-',
|
||||||
|
'GeoIp::Freegeoip' => 'freegeoip.net',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
:state => 'GeoIp::Freegeoip',
|
||||||
|
:frontend => false
|
||||||
|
)
|
||||||
|
end
|
||||||
|
def down
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
33
db/migrate/20130815000002_update_ticket_number.rb
Normal file
33
db/migrate/20130815000002_update_ticket_number.rb
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
class UpdateTicketNumber < ActiveRecord::Migration
|
||||||
|
def up
|
||||||
|
Setting.create_or_update(
|
||||||
|
:title => 'Ticket Number Format',
|
||||||
|
:name => 'ticket_number',
|
||||||
|
:area => 'Ticket::Number',
|
||||||
|
:description => 'Selects the ticket number generator module. "Increment" increments the ticket
|
||||||
|
number, the SystemID and the counter are used with SystemID.Counter format (e.g. 1010138, 1010139).
|
||||||
|
With "Date" the ticket numbers will be generated by the current date, the SystemID and the counter.
|
||||||
|
The format looks like Year.Month.Day.SystemID.counter (e.g. 201206231010138, 201206231010139).
|
||||||
|
With param "Checksum => true" the counter will be appended as checksum to the string. The format
|
||||||
|
looks like SystemID.Counter.CheckSum (e. g. 10101384, 10101392) or Year.Month.Day.SystemID.Counter.CheckSum (e.g. 2012070110101520, 2012070110101535).',
|
||||||
|
:options => {
|
||||||
|
:form => [
|
||||||
|
{
|
||||||
|
:display => '',
|
||||||
|
:null => true,
|
||||||
|
:name => 'ticket_number',
|
||||||
|
:tag => 'select',
|
||||||
|
:options => {
|
||||||
|
'Ticket::Number::Increment' => 'Increment (SystemID.Counter)',
|
||||||
|
'Ticket::Number::Date' => 'Date (Year.Month.Day.SystemID.Counter)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
:state => 'Ticket::Number::Increment',
|
||||||
|
:frontend => false
|
||||||
|
)
|
||||||
|
end
|
||||||
|
def down
|
||||||
|
end
|
||||||
|
end
|
42
db/seeds.rb
42
db/seeds.rb
|
@ -135,7 +135,7 @@ Setting.create_if_not_exists(
|
||||||
)
|
)
|
||||||
Setting.create_if_not_exists(
|
Setting.create_if_not_exists(
|
||||||
:title => 'Geo Location Backend',
|
:title => 'Geo Location Backend',
|
||||||
:name => 'geo_backend',
|
:name => 'geo_location_backend',
|
||||||
:area => 'System::Geo',
|
:area => 'System::Geo',
|
||||||
:description => 'Defines the backend for geo location lookups.',
|
:description => 'Defines the backend for geo location lookups.',
|
||||||
:options => {
|
:options => {
|
||||||
|
@ -143,17 +143,39 @@ Setting.create_if_not_exists(
|
||||||
{
|
{
|
||||||
:display => '',
|
:display => '',
|
||||||
:null => true,
|
:null => true,
|
||||||
:name => 'geo_backend',
|
:name => 'geo_location_backend',
|
||||||
:tag => 'select',
|
:tag => 'select',
|
||||||
:options => {
|
:options => {
|
||||||
'' => '-',
|
'' => '-',
|
||||||
'Gmaps' => 'Google Maps',
|
'GeoLocation::Gmaps' => 'Google Maps',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
:state => 'Gmaps',
|
:state => 'GeoLocation::Gmaps',
|
||||||
:frontend => true
|
:frontend => false
|
||||||
|
)
|
||||||
|
Setting.create_if_not_exists(
|
||||||
|
:title => 'Geo IP Backend',
|
||||||
|
:name => 'geo_ip_backend',
|
||||||
|
:area => 'System::Geo',
|
||||||
|
:description => 'Defines the backend for geo ip lookups.',
|
||||||
|
:options => {
|
||||||
|
:form => [
|
||||||
|
{
|
||||||
|
:display => '',
|
||||||
|
:null => true,
|
||||||
|
:name => 'geo_ip_backend',
|
||||||
|
:tag => 'select',
|
||||||
|
:options => {
|
||||||
|
'' => '-',
|
||||||
|
'GeoIp::Freegeoip' => 'freegeoip.net',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
:state => 'GeoIp::Freegeoip',
|
||||||
|
:frontend => false
|
||||||
)
|
)
|
||||||
|
|
||||||
Setting.create_if_not_exists(
|
Setting.create_if_not_exists(
|
||||||
|
@ -228,7 +250,7 @@ Setting.create_if_not_exists(
|
||||||
:area => 'Security::Authentication',
|
:area => 'Security::Authentication',
|
||||||
:description => 'Enables user authentication via OTRS.',
|
:description => 'Enables user authentication via OTRS.',
|
||||||
:state => {
|
:state => {
|
||||||
:adapter => 'otrs',
|
:adapter => 'Auth::Otrs',
|
||||||
:required_group_ro => 'stats',
|
:required_group_ro => 'stats',
|
||||||
:group_rw_role_map => {
|
:group_rw_role_map => {
|
||||||
'admin' => 'Admin',
|
'admin' => 'Admin',
|
||||||
|
@ -249,7 +271,7 @@ Setting.create_if_not_exists(
|
||||||
:area => 'Security::Authentication',
|
:area => 'Security::Authentication',
|
||||||
:description => 'Enables user authentication via LDAP.',
|
:description => 'Enables user authentication via LDAP.',
|
||||||
:state => {
|
:state => {
|
||||||
:adapter => 'ldap',
|
:adapter => 'Auth::Ldap',
|
||||||
:host => 'localhost',
|
:host => 'localhost',
|
||||||
:port => 389,
|
:port => 389,
|
||||||
:bind_dn => 'cn=Manager,dc=example,dc=org',
|
:bind_dn => 'cn=Manager,dc=example,dc=org',
|
||||||
|
@ -690,13 +712,13 @@ Setting.create_if_not_exists(
|
||||||
:name => 'ticket_number',
|
:name => 'ticket_number',
|
||||||
:tag => 'select',
|
:tag => 'select',
|
||||||
:options => {
|
:options => {
|
||||||
'increment' => 'Increment (SystemID.Counter)',
|
'Ticket::Number::Increment' => 'Increment (SystemID.Counter)',
|
||||||
'date' => 'Date (Year.Month.Day.SystemID.Counter)',
|
'Ticket::Number::Date' => 'Date (Year.Month.Day.SystemID.Counter)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
:state => 'increment',
|
:state => 'Ticket::Number::Increment',
|
||||||
:frontend => false
|
:frontend => false
|
||||||
)
|
)
|
||||||
Setting.create_if_not_exists(
|
Setting.create_if_not_exists(
|
||||||
|
|
50
lib/application_lib.rb
Normal file
50
lib/application_lib.rb
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
class ApplicationLib
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
load adapter based on setting option
|
||||||
|
|
||||||
|
result = self.load_adapter_by_setting( 'some_setting_with_class_name' )
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = Some::Classname
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
|
def self.load_adapter_by_setting(setting)
|
||||||
|
adapter = Setting.get( setting )
|
||||||
|
return if !adapter
|
||||||
|
|
||||||
|
# load backend
|
||||||
|
self.load_adapter(adapter)
|
||||||
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
load adapter
|
||||||
|
|
||||||
|
result = self.load_adapter( 'Some::Classname' )
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = Some::Classname
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
|
def self.load_adapter(adapter)
|
||||||
|
|
||||||
|
# load adapter
|
||||||
|
|
||||||
|
# will only work on ruby 2.0
|
||||||
|
# Object.const_get(adapter)
|
||||||
|
|
||||||
|
# will work on ruby 1.9 and 2.0
|
||||||
|
# adapter.split('::').inject(Object) do |mod, class_name|
|
||||||
|
# mod.const_get(class_name)
|
||||||
|
# end
|
||||||
|
|
||||||
|
# will work with active_support
|
||||||
|
adapter.constantize
|
||||||
|
end
|
||||||
|
end
|
63
lib/auth.rb
Normal file
63
lib/auth.rb
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
class Auth < ApplicationLib
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
authenticate user via username and password
|
||||||
|
|
||||||
|
result = Auth.check( username, password, user )
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = user_model # if authentication was successfully
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
|
def self.check(username, password, user)
|
||||||
|
|
||||||
|
# use std. auth backends
|
||||||
|
config = [
|
||||||
|
{
|
||||||
|
:adapter => 'Auth::Internal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
:adapter => 'Auth::Test',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# added configured backends
|
||||||
|
Setting.where( :area => 'Security::Authentication' ).each {|setting|
|
||||||
|
if setting.state[:value]
|
||||||
|
config.push setting.state[:value]
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
# try to login against configure auth backends
|
||||||
|
user_auth = nil
|
||||||
|
config.each {|config_item|
|
||||||
|
next if !config_item[:adapter]
|
||||||
|
|
||||||
|
# load backend
|
||||||
|
backend = self.load_adapter( config_item[:adapter] )
|
||||||
|
return if !backend
|
||||||
|
|
||||||
|
user_auth = backend.check( username, password, config_item, user )
|
||||||
|
|
||||||
|
# auth ok
|
||||||
|
if user_auth
|
||||||
|
|
||||||
|
# remember last login date
|
||||||
|
user_auth.update_last_login
|
||||||
|
|
||||||
|
# reset login failed
|
||||||
|
user_auth.login_failed = 0
|
||||||
|
user_auth.save
|
||||||
|
|
||||||
|
return user_auth
|
||||||
|
end
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,6 +1,6 @@
|
||||||
module Auth
|
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
|
||||||
end
|
|
||||||
module Auth::INTERNAL
|
module Auth::Internal
|
||||||
def self.check( username, password, config, user )
|
def self.check( username, password, config, user )
|
||||||
|
|
||||||
# return if no user exists
|
# return if no user exists
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
|
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
require 'net/ldap'
|
require 'net/ldap'
|
||||||
|
|
||||||
module Auth
|
module Auth::Ldap
|
||||||
end
|
|
||||||
module Auth::LDAP
|
|
||||||
def self.check( username, password, config, user )
|
def self.check( username, password, config, user )
|
||||||
|
|
||||||
scope = Net::LDAP::SearchScope_WholeSubtree
|
scope = Net::LDAP::SearchScope_WholeSubtree
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
|
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
require 'import/otrs'
|
require 'import/otrs'
|
||||||
module Auth
|
|
||||||
end
|
module Auth::Otrs
|
||||||
module Auth::OTRS
|
|
||||||
def self.check( username, password, config, user )
|
def self.check( username, password, config, user )
|
||||||
|
|
||||||
endpoint = Setting.get('import_otrs_endpoint')
|
endpoint = Setting.get('import_otrs_endpoint')
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
module Auth
|
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
|
||||||
end
|
|
||||||
module Auth::TEST
|
module Auth::Test
|
||||||
def self.check( username, password, config, user )
|
def self.check( username, password, config, user )
|
||||||
|
|
||||||
# development systems
|
# development systems
|
||||||
|
|
38
lib/geo_ip.rb
Normal file
38
lib/geo_ip.rb
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
class GeoIp < ApplicationLib
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
lookup location based on ip or hostname
|
||||||
|
|
||||||
|
result = GeoIp.location( '172.0.0.1' )
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"ip" => "172.0.0.1"
|
||||||
|
"country_code" => "DE",
|
||||||
|
"country_name" => "Germany",
|
||||||
|
"region_code" => "05",
|
||||||
|
"region_name" => "Hessen",
|
||||||
|
"city" => "Frankfurt Am Main"
|
||||||
|
"zipcode" => "12345",
|
||||||
|
"latitude" => 50.1167,
|
||||||
|
"longitude" => 8.6833,
|
||||||
|
"metro_code" => "",
|
||||||
|
"areacode" => ""
|
||||||
|
}
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
|
def self.location(address)
|
||||||
|
|
||||||
|
# load backend
|
||||||
|
backend = self.load_adapter_by_setting( 'geo_ip_backend' )
|
||||||
|
return if !backend
|
||||||
|
|
||||||
|
# db lookup
|
||||||
|
backend.location(address)
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,11 +1,13 @@
|
||||||
|
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
require 'faraday'
|
require 'faraday'
|
||||||
require 'cache'
|
require 'cache'
|
||||||
|
|
||||||
module Geoip
|
class GeoIp::Freegeoip
|
||||||
def self.location(address)
|
def self.location(address)
|
||||||
|
|
||||||
# check cache
|
# check cache
|
||||||
cache_key = "geoip::#{address}"
|
cache_key = "freegeoip::#{address}"
|
||||||
cache = Cache.get( cache_key )
|
cache = Cache.get( cache_key )
|
||||||
return cache if cache
|
return cache if cache
|
||||||
|
|
48
lib/geo_location.rb
Normal file
48
lib/geo_location.rb
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
class GeoLocation < ApplicationLib
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
lookup lat and lng for address
|
||||||
|
|
||||||
|
result = GeoLocation.geocode( 'Marienstrasse 13, 10117 Berlin' )
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = [ 4.21312, 1.3123 ]
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
|
def self.geocode(address)
|
||||||
|
|
||||||
|
# load backend
|
||||||
|
backend = self.load_adapter_by_setting( 'geo_location_backend' )
|
||||||
|
return if !backend
|
||||||
|
|
||||||
|
# db lookup
|
||||||
|
backend.geocode(address)
|
||||||
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
lookup address for lat and lng
|
||||||
|
|
||||||
|
result = GeoLocation.reverse_geocode( 4.21312, 1.3123 )
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = 'some address'
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
|
def self.reverse_geocode(lat,lng)
|
||||||
|
|
||||||
|
# load backend
|
||||||
|
backend = self.load_adapter_by_setting( 'geo_location_backend' )
|
||||||
|
return if !backend
|
||||||
|
|
||||||
|
# db lookup
|
||||||
|
backend.reverse_geocode(lat,lng)
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,8 +1,11 @@
|
||||||
module Gmaps
|
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
class GeoLocation::Gmaps
|
||||||
|
|
||||||
def self.geocode(address)
|
def self.geocode(address)
|
||||||
url = "http://maps.googleapis.com/maps/api/geocode/json?address=#{CGI::escape address}&sensor=true"
|
url = "http://maps.googleapis.com/maps/api/geocode/json?address=#{CGI::escape address}&sensor=true"
|
||||||
response = Net::HTTP.get_response( URI.parse(url) )
|
response = Net::HTTP.get_response( URI.parse(url) )
|
||||||
return if response.code.to_s != '200'
|
return if ! response.kind_of? Net::HTTPSuccess
|
||||||
|
|
||||||
result = JSON.parse( response.body )
|
result = JSON.parse( response.body )
|
||||||
|
|
||||||
|
@ -14,7 +17,7 @@ module Gmaps
|
||||||
def self.reverse_geocode(lat,lng)
|
def self.reverse_geocode(lat,lng)
|
||||||
url = "http://maps.googleapis.com/maps/api/geocode/json?latlng=#{lat},#{lng}&sensor=true"
|
url = "http://maps.googleapis.com/maps/api/geocode/json?latlng=#{lat},#{lng}&sensor=true"
|
||||||
response = Net::HTTP.get_response( URI.parse(url) )
|
response = Net::HTTP.get_response( URI.parse(url) )
|
||||||
return if response.code.to_s != '200'
|
return if ! response.kind_of? Net::HTTPSuccess
|
||||||
|
|
||||||
result = JSON.parse( response.body )
|
result = JSON.parse( response.body )
|
||||||
|
|
|
@ -9,11 +9,11 @@ module RSS
|
||||||
response = Net::HTTP.get_response( URI.parse(url) )
|
response = Net::HTTP.get_response( URI.parse(url) )
|
||||||
|
|
||||||
# check if redirect is needed
|
# check if redirect is needed
|
||||||
if response.code.to_s == '301' || response.code.to_s == '302'
|
if response.kind_of? Net::HTTPRedirection
|
||||||
url = response.header['location']
|
url = response.header['location']
|
||||||
response = Net::HTTP.get_response( URI.parse( url ) )
|
response = Net::HTTP.get_response( URI.parse( url ) )
|
||||||
end
|
end
|
||||||
if response.code.to_s != '200'
|
if ! response.kind_of? Net::HTTPSuccess
|
||||||
raise "Can't fetch '#{url}', http code: #{response.code.to_s}"
|
raise "Can't fetch '#{url}', http code: #{response.code.to_s}"
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
|
@ -395,7 +395,7 @@ class UserState
|
||||||
# overview
|
# overview
|
||||||
cache_key = @cache_key + '_overview'
|
cache_key = @cache_key + '_overview'
|
||||||
if CacheIn.expired(cache_key)
|
if CacheIn.expired(cache_key)
|
||||||
overview = Ticket.overview(
|
overview = Ticket::Overview.list(
|
||||||
:current_user => user,
|
:current_user => user,
|
||||||
)
|
)
|
||||||
overview_cache = CacheIn.get( cache_key, { :re_expire => true } )
|
overview_cache = CacheIn.get( cache_key, { :re_expire => true } )
|
||||||
|
@ -405,20 +405,19 @@ class UserState
|
||||||
# puts overview.inspect
|
# puts overview.inspect
|
||||||
# puts '------'
|
# puts '------'
|
||||||
# puts overview_cache.inspect
|
# puts overview_cache.inspect
|
||||||
CacheIn.set( cache_key, overview, { :expires_in => 3.seconds } )
|
CacheIn.set( cache_key, overview, { :expires_in => 4.seconds } )
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# overview lists
|
# overview lists
|
||||||
overviews = Ticket.overview_list(
|
overviews = Ticket::Overview.all(
|
||||||
:current_user => user,
|
:current_user => user,
|
||||||
)
|
)
|
||||||
overviews.each { |overview|
|
overviews.each { |overview|
|
||||||
cache_key = @cache_key + '_overview_data_' + overview.link
|
cache_key = @cache_key + '_overview_data_' + overview.link
|
||||||
if CacheIn.expired(cache_key)
|
if CacheIn.expired(cache_key)
|
||||||
overview_data = Ticket.overview(
|
overview_data = Ticket::Overview.list(
|
||||||
:view => overview.link,
|
:view => overview.link,
|
||||||
# :view_mode => params[:view_mode],
|
|
||||||
:current_user => user,
|
:current_user => user,
|
||||||
:array => true,
|
:array => true,
|
||||||
)
|
)
|
||||||
|
@ -434,7 +433,7 @@ class UserState
|
||||||
# create_attributes
|
# create_attributes
|
||||||
cache_key = @cache_key + '_ticket_create_attributes'
|
cache_key = @cache_key + '_ticket_create_attributes'
|
||||||
if CacheIn.expired(cache_key)
|
if CacheIn.expired(cache_key)
|
||||||
ticket_create_attributes = Ticket.attributes_to_change(
|
ticket_create_attributes = Ticket::ScreenOptions.attributes_to_change(
|
||||||
:current_user_id => user.id,
|
:current_user_id => user.id,
|
||||||
)
|
)
|
||||||
ticket_create_attributes_cache = CacheIn.get( cache_key, { :re_expire => true } )
|
ticket_create_attributes_cache = CacheIn.get( cache_key, { :re_expire => true } )
|
||||||
|
@ -638,7 +637,7 @@ class ClientState
|
||||||
end
|
end
|
||||||
|
|
||||||
# overview_data
|
# overview_data
|
||||||
overviews = Ticket.overview_list(
|
overviews = Ticket::Overview.all(
|
||||||
:current_user => user,
|
:current_user => user,
|
||||||
)
|
)
|
||||||
overviews.each { |overview|
|
overviews.each { |overview|
|
||||||
|
@ -661,7 +660,7 @@ class ClientState
|
||||||
group_ids.push group.id
|
group_ids.push group.id
|
||||||
}
|
}
|
||||||
agents = {}
|
agents = {}
|
||||||
Ticket.agents.each { |user|
|
Ticket::ScreenOptions.agents.each { |user|
|
||||||
agents[ user.id ] = 1
|
agents[ user.id ] = 1
|
||||||
}
|
}
|
||||||
groups_users = {}
|
groups_users = {}
|
||||||
|
|
63
lib/sso.rb
Normal file
63
lib/sso.rb
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
class Sso < ApplicationLib
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
authenticate user via username and password
|
||||||
|
|
||||||
|
result = Sso.check( params, config_item )
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
result = user_model # if authentication was successfully
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
|
def self.check(params)
|
||||||
|
|
||||||
|
# use std. auth backends
|
||||||
|
config = [
|
||||||
|
{
|
||||||
|
:adapter => 'Sso::Env',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
:adapter => 'Sso::Otrs',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# added configured backends
|
||||||
|
Setting.where( :area => 'Security::SSO' ).each {|setting|
|
||||||
|
if setting.state[:value]
|
||||||
|
config.push setting.state[:value]
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
# try to login against configure auth backends
|
||||||
|
user_auth = nil
|
||||||
|
config.each {|config_item|
|
||||||
|
next if !config_item[:adapter]
|
||||||
|
|
||||||
|
# load backend
|
||||||
|
backend = self.load_adapter( config_item[:adapter] )
|
||||||
|
return if !backend
|
||||||
|
|
||||||
|
user_auth = backend.check( params, config_item )
|
||||||
|
|
||||||
|
# auth ok
|
||||||
|
if user_auth
|
||||||
|
|
||||||
|
# remember last login date
|
||||||
|
user_auth.update_last_login
|
||||||
|
|
||||||
|
# reset login failed
|
||||||
|
user_auth.login_failed = 0
|
||||||
|
user_auth.save
|
||||||
|
|
||||||
|
return user_auth
|
||||||
|
end
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,6 +1,6 @@
|
||||||
module SSO
|
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
|
||||||
end
|
|
||||||
module SSO::ENV
|
module Sso::Env
|
||||||
def self.check( params, config_item )
|
def self.check( params, config_item )
|
||||||
|
|
||||||
# try to find user based on login
|
# try to find user based on login
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
module SSO
|
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
|
||||||
end
|
|
||||||
module SSO::OTRS
|
module Sso::Otrs
|
||||||
def self.check( params, config_item )
|
def self.check( params, config_item )
|
||||||
|
|
||||||
endpoint = Setting.get('import_otrs_endpoint')
|
endpoint = Setting.get('import_otrs_endpoint')
|
||||||
|
|
|
@ -55,7 +55,7 @@ class SignupTest < TestCase
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
:execute => 'wait',
|
:execute => 'wait',
|
||||||
:value => 2,
|
:value => 5,
|
||||||
},
|
},
|
||||||
|
|
||||||
# check action
|
# check action
|
||||||
|
|
|
@ -7,7 +7,7 @@ Setting.create_or_update(
|
||||||
:area => 'Security::Authentication',
|
:area => 'Security::Authentication',
|
||||||
:description => 'Enables user authentication via LDAP.',
|
:description => 'Enables user authentication via LDAP.',
|
||||||
:state => {
|
:state => {
|
||||||
:adapter => 'ldap',
|
:adapter => 'Auth::Ldap',
|
||||||
:host => 'localhost',
|
:host => 'localhost',
|
||||||
:port => 389,
|
:port => 389,
|
||||||
:bind_dn => 'cn=Manager,dc=example,dc=org',
|
:bind_dn => 'cn=Manager,dc=example,dc=org',
|
||||||
|
@ -45,6 +45,7 @@ else
|
||||||
:created_by_id => 1
|
:created_by_id => 1
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
class AuthTest < ActiveSupport::TestCase
|
class AuthTest < ActiveSupport::TestCase
|
||||||
test 'auth' do
|
test 'auth' do
|
||||||
tests = [
|
tests = [
|
||||||
|
|
Loading…
Reference in a new issue