Zammad Docker and addon updates
This commit is contained in:
parent
dab5ce0521
commit
aa18d3904e
16 changed files with 1972 additions and 2976 deletions
|
|
@ -1,19 +1,17 @@
|
|||
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||
# Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
class Ticket < ApplicationModel
|
||||
include CanBeImported
|
||||
include HasActivityStreamLog
|
||||
include ChecksClientNotification
|
||||
include ChecksLatestChangeObserved
|
||||
include CanCsvImport
|
||||
include ChecksHtmlSanitized
|
||||
include HasHistory
|
||||
include HasTags
|
||||
include HasSearchIndexBackend
|
||||
include HasOnlineNotifications
|
||||
include HasKarmaActivityLog
|
||||
include HasLinks
|
||||
include HasObjectManagerAttributesValidation
|
||||
include HasObjectManagerAttributes
|
||||
include HasTaskbars
|
||||
include Ticket::CallsStatsTicketReopenLog
|
||||
include Ticket::EnqueuesUserTicketCounterJob
|
||||
|
|
@ -21,6 +19,8 @@ class Ticket < ApplicationModel
|
|||
include Ticket::SetsCloseTime
|
||||
include Ticket::SetsOnlineNotificationSeen
|
||||
include Ticket::TouchesAssociations
|
||||
include Ticket::TriggersSubscriptions
|
||||
include Ticket::ChecksReopenAfterCertainTime
|
||||
|
||||
include ::Ticket::Escalation
|
||||
include ::Ticket::Subject
|
||||
|
|
@ -38,10 +38,15 @@ class Ticket < ApplicationModel
|
|||
|
||||
include HasTransactionDispatcher
|
||||
|
||||
# workflow checks should run after before_create and before_update callbacks
|
||||
include ChecksCoreWorkflow
|
||||
|
||||
validates :group_id, presence: true
|
||||
|
||||
activity_stream_permission 'ticket.agent'
|
||||
|
||||
core_workflow_screens 'create_middle', 'edit', 'overview_bulk'
|
||||
|
||||
activity_stream_attributes_ignored :organization_id, # organization_id will change automatically on user update
|
||||
:create_article_type_id,
|
||||
:create_article_sender_id,
|
||||
|
|
@ -57,19 +62,26 @@ class Ticket < ApplicationModel
|
|||
:update_escalation_at,
|
||||
:update_in_min,
|
||||
:update_diff_in_min,
|
||||
:last_close_at,
|
||||
:last_contact_at,
|
||||
:last_contact_agent_at,
|
||||
:last_contact_customer_at,
|
||||
:last_owner_update_at,
|
||||
:preferences
|
||||
|
||||
search_index_attributes_relevant :organization_id,
|
||||
:group_id,
|
||||
:state_id,
|
||||
:priority_id
|
||||
|
||||
history_attributes_ignored :create_article_type_id,
|
||||
:create_article_sender_id,
|
||||
:article_count,
|
||||
:preferences
|
||||
|
||||
history_relation_object 'Ticket::Article', 'Mention'
|
||||
history_relation_object 'Ticket::Article', 'Mention', 'Ticket::SharedDraftZoom'
|
||||
|
||||
validates :note, length: { maximum: 250 }
|
||||
sanitized_html :note
|
||||
|
||||
belongs_to :group, optional: true
|
||||
|
|
@ -78,6 +90,7 @@ class Ticket < ApplicationModel
|
|||
has_many :ticket_time_accounting, class_name: 'Ticket::TimeAccounting', dependent: :destroy, inverse_of: :ticket
|
||||
has_many :flags, class_name: 'Ticket::Flag', dependent: :destroy
|
||||
has_many :mentions, as: :mentionable, dependent: :destroy
|
||||
has_one :shared_draft, class_name: 'Ticket::SharedDraftZoom', inverse_of: :ticket, dependent: :destroy
|
||||
belongs_to :state, class_name: 'Ticket::State', optional: true
|
||||
belongs_to :priority, class_name: 'Ticket::Priority', optional: true
|
||||
belongs_to :owner, class_name: 'User', optional: true
|
||||
|
|
@ -93,43 +106,6 @@ class Ticket < ApplicationModel
|
|||
|
||||
=begin
|
||||
|
||||
get user access conditions
|
||||
|
||||
conditions = Ticket.access_condition( User.find(1) , 'full')
|
||||
|
||||
returns
|
||||
|
||||
result = [user1, user2, ...]
|
||||
|
||||
=end
|
||||
|
||||
def self.access_condition(user, access)
|
||||
sql = []
|
||||
bind = []
|
||||
|
||||
if user.permissions?('ticket.agent')
|
||||
sql.push('group_id IN (?)')
|
||||
bind.push(user.group_ids_access(access))
|
||||
end
|
||||
|
||||
if user.permissions?('ticket.customer')
|
||||
if !user.organization || ( !user.organization.shared || user.organization.shared == false )
|
||||
sql.push('tickets.customer_id = ?')
|
||||
bind.push(user.id)
|
||||
else
|
||||
sql.push('(tickets.customer_id = ? OR tickets.organization_id = ?)')
|
||||
bind.push(user.id)
|
||||
bind.push(user.organization.id)
|
||||
end
|
||||
end
|
||||
|
||||
return if sql.blank?
|
||||
|
||||
[ sql.join(' OR ') ].concat(bind)
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
processes tickets which have reached their pending time and sets next state_id
|
||||
|
||||
processed_tickets = Ticket.process_pending
|
||||
|
|
@ -204,6 +180,31 @@ returns
|
|||
result
|
||||
end
|
||||
|
||||
def auto_assign(user)
|
||||
return if !persisted?
|
||||
return if Setting.get('ticket_auto_assignment').blank?
|
||||
return if owner_id != 1
|
||||
return if !TicketPolicy.new(user, self).full?
|
||||
|
||||
user_ids_ignore = Array(Setting.get('ticket_auto_assignment_user_ids_ignore')).map(&:to_i)
|
||||
return if user_ids_ignore.include?(user.id)
|
||||
|
||||
ticket_auto_assignment_selector = Setting.get('ticket_auto_assignment_selector')
|
||||
return if ticket_auto_assignment_selector.blank?
|
||||
|
||||
condition = ticket_auto_assignment_selector[:condition].merge(
|
||||
'ticket.id' => {
|
||||
'operator' => 'is',
|
||||
'value' => id,
|
||||
}
|
||||
)
|
||||
|
||||
ticket_count, = Ticket.selectors(condition, limit: 1, current_user: user, access: 'full')
|
||||
return if ticket_count.to_i.zero?
|
||||
|
||||
update!(owner: user)
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
processes escalated tickets
|
||||
|
|
@ -220,7 +221,7 @@ returns
|
|||
result = []
|
||||
|
||||
# fetch all escalated and soon to be escalating tickets
|
||||
where('escalation_at <= ?', Time.zone.now + 15.minutes).find_each(batch_size: 500) do |ticket|
|
||||
where('escalation_at <= ?', 15.minutes.from_now).find_each(batch_size: 500) do |ticket|
|
||||
|
||||
article_id = nil
|
||||
article = Ticket::Article.last_customer_agent_article(ticket.id)
|
||||
|
|
@ -241,7 +242,7 @@ returns
|
|||
next
|
||||
end
|
||||
|
||||
# check if warning need to be sent
|
||||
# check if warning needs to be sent
|
||||
TransactionJob.perform_now(
|
||||
object: 'Ticket',
|
||||
type: 'escalation_warning',
|
||||
|
|
@ -321,10 +322,10 @@ returns
|
|||
# prevent cross merging tickets
|
||||
target_ticket = Ticket.find_by(id: data[:ticket_id])
|
||||
raise 'no target ticket given' if !target_ticket
|
||||
raise Exceptions::UnprocessableEntity, 'ticket already merged, no merge into merged ticket possible' if target_ticket.state.state_type.name == 'merged'
|
||||
raise Exceptions::UnprocessableEntity, __('It is not possible to merge into an already merged ticket.') if target_ticket.state.state_type.name == 'merged'
|
||||
|
||||
# check different ticket ids
|
||||
raise Exceptions::UnprocessableEntity, 'Can\'t merge ticket with it self!' if id == target_ticket.id
|
||||
raise Exceptions::UnprocessableEntity, __('A ticket cannot be merged into itself.') if id == target_ticket.id
|
||||
|
||||
# update articles
|
||||
Transaction.execute context: 'merge' do
|
||||
|
|
@ -413,6 +414,26 @@ returns
|
|||
|
||||
# touch new ticket (to broadcast change)
|
||||
target_ticket.touch # rubocop:disable Rails/SkipsModelValidations
|
||||
|
||||
EventBuffer.add('transaction', {
|
||||
object: target_ticket.class.name,
|
||||
type: 'update.received_merge',
|
||||
data: target_ticket,
|
||||
changes: {},
|
||||
id: target_ticket.id,
|
||||
user_id: UserInfo.current_user_id,
|
||||
created_at: Time.zone.now,
|
||||
})
|
||||
|
||||
EventBuffer.add('transaction', {
|
||||
object: self.class.name,
|
||||
type: 'update.merged_into',
|
||||
data: self,
|
||||
changes: {},
|
||||
id: id,
|
||||
user_id: UserInfo.current_user_id,
|
||||
created_at: Time.zone.now,
|
||||
})
|
||||
end
|
||||
true
|
||||
end
|
||||
|
|
@ -489,7 +510,7 @@ get count of tickets and tickets which match on selector
|
|||
access = options[:access] || 'full'
|
||||
raise 'no selectors given' if !selectors
|
||||
|
||||
query, bind_params, tables = selector2sql(selectors, current_user: current_user, execution_time: options[:execution_time])
|
||||
query, bind_params, tables = selector2sql(selectors, options)
|
||||
return [] if !query
|
||||
|
||||
ActiveRecord::Base.transaction(requires_new: true) do
|
||||
|
|
@ -497,20 +518,20 @@ get count of tickets and tickets which match on selector
|
|||
if !current_user || access == 'ignore'
|
||||
ticket_count = Ticket.distinct.where(query, *bind_params).joins(tables).count
|
||||
tickets = Ticket.distinct.where(query, *bind_params).joins(tables).limit(limit)
|
||||
return [ticket_count, tickets]
|
||||
next [ticket_count, tickets]
|
||||
end
|
||||
|
||||
access_condition = Ticket.access_condition(current_user, access)
|
||||
ticket_count = Ticket.distinct.where(access_condition).where(query, *bind_params).joins(tables).count
|
||||
tickets = Ticket.distinct.where(access_condition).where(query, *bind_params).joins(tables).limit(limit)
|
||||
tickets = "TicketPolicy::#{access.camelize}Scope".constantize
|
||||
.new(current_user).resolve
|
||||
.distinct
|
||||
.where(query, *bind_params)
|
||||
.joins(tables)
|
||||
|
||||
return [ticket_count, tickets]
|
||||
next [tickets.count, tickets.limit(limit)]
|
||||
rescue ActiveRecord::StatementInvalid => e
|
||||
Rails.logger.error e
|
||||
raise ActiveRecord::Rollback
|
||||
|
||||
end
|
||||
[]
|
||||
end
|
||||
|
||||
=begin
|
||||
|
|
@ -561,419 +582,7 @@ condition example
|
|||
=end
|
||||
|
||||
def self.selector2sql(selectors, options = {})
|
||||
current_user = options[:current_user]
|
||||
current_user_id = UserInfo.current_user_id
|
||||
if current_user
|
||||
current_user_id = current_user.id
|
||||
end
|
||||
return if !selectors
|
||||
|
||||
# remember query and bind params
|
||||
query = ''
|
||||
bind_params = []
|
||||
like = Rails.application.config.db_like
|
||||
|
||||
if selectors.respond_to?(:permit!)
|
||||
selectors = selectors.permit!.to_h
|
||||
end
|
||||
|
||||
# get tables to join
|
||||
tables = ''
|
||||
selectors.each do |attribute, selector_raw|
|
||||
attributes = attribute.split('.')
|
||||
selector = selector_raw.stringify_keys
|
||||
next if !attributes[1]
|
||||
next if attributes[0] == 'execution_time'
|
||||
next if tables.include?(attributes[0])
|
||||
next if attributes[0] == 'ticket' && attributes[1] != 'mention_user_ids'
|
||||
next if attributes[0] == 'ticket' && attributes[1] == 'mention_user_ids' && selector['pre_condition'] == 'not_set'
|
||||
|
||||
if query != ''
|
||||
query += ' AND '
|
||||
end
|
||||
case attributes[0]
|
||||
when 'customer'
|
||||
tables += ', users customers'
|
||||
query += 'tickets.customer_id = customers.id'
|
||||
when 'organization'
|
||||
tables += ', organizations'
|
||||
query += 'tickets.organization_id = organizations.id'
|
||||
when 'owner'
|
||||
tables += ', users owners'
|
||||
query += 'tickets.owner_id = owners.id'
|
||||
when 'article'
|
||||
tables += ', ticket_articles articles'
|
||||
query += 'tickets.id = articles.ticket_id'
|
||||
when 'ticket_state'
|
||||
tables += ', ticket_states'
|
||||
query += 'tickets.state_id = ticket_states.id'
|
||||
when 'ticket'
|
||||
if attributes[1] == 'mention_user_ids'
|
||||
tables += ', mentions'
|
||||
query += "tickets.id = mentions.mentionable_id AND mentions.mentionable_type = 'Ticket'"
|
||||
end
|
||||
else
|
||||
raise "invalid selector #{attribute.inspect}->#{attributes.inspect}"
|
||||
end
|
||||
end
|
||||
|
||||
# add conditions
|
||||
no_result = false
|
||||
selectors.each do |attribute, selector_raw|
|
||||
|
||||
# validation
|
||||
raise "Invalid selector #{selector_raw.inspect}" if !selector_raw
|
||||
raise "Invalid selector #{selector_raw.inspect}" if !selector_raw.respond_to?(:key?)
|
||||
|
||||
selector = selector_raw.stringify_keys
|
||||
raise "Invalid selector, operator missing #{selector.inspect}" if !selector['operator']
|
||||
raise "Invalid selector, operator #{selector['operator']} is invalid #{selector.inspect}" if !selector['operator'].match?(%r{^(is|is\snot|contains|contains\s(not|all|one|all\snot|one\snot)|(after|before)\s\(absolute\)|(within\snext|within\slast|after|before|till|from)\s\(relative\))|(is\sin\sworking\stime|is\snot\sin\sworking\stime)$})
|
||||
|
||||
# validate value / allow blank but only if pre_condition exists and is not specific
|
||||
if !selector.key?('value') ||
|
||||
(selector['value'].instance_of?(Array) && selector['value'].respond_to?(:blank?) && selector['value'].blank?) ||
|
||||
(selector['operator'].start_with?('contains') && selector['value'].respond_to?(:blank?) && selector['value'].blank?)
|
||||
return nil if selector['pre_condition'].nil?
|
||||
return nil if selector['pre_condition'].respond_to?(:blank?) && selector['pre_condition'].blank?
|
||||
return nil if selector['pre_condition'] == 'specific'
|
||||
end
|
||||
|
||||
# validate pre_condition values
|
||||
return nil if selector['pre_condition'] && selector['pre_condition'] !~ %r{^(not_set|current_user\.|specific)}
|
||||
|
||||
# get attributes
|
||||
attributes = attribute.split('.')
|
||||
attribute = "#{ActiveRecord::Base.connection.quote_table_name("#{attributes[0]}s")}.#{ActiveRecord::Base.connection.quote_column_name(attributes[1])}"
|
||||
|
||||
# magic selectors
|
||||
if attributes[0] == 'ticket' && attributes[1] == 'out_of_office_replacement_id'
|
||||
attribute = "#{ActiveRecord::Base.connection.quote_table_name("#{attributes[0]}s")}.#{ActiveRecord::Base.connection.quote_column_name('owner_id')}"
|
||||
end
|
||||
|
||||
if attributes[0] == 'ticket' && attributes[1] == 'tags'
|
||||
selector['value'] = selector['value'].split(',').collect(&:strip)
|
||||
end
|
||||
|
||||
if selector['operator'].include?('in working time')
|
||||
next if attributes[1] != 'calendar_id'
|
||||
raise 'Please enable execution_time feature to use it (currently only allowed for triggers and schedulers)' if !options[:execution_time]
|
||||
|
||||
biz = Calendar.lookup(id: selector['value'])&.biz
|
||||
next if biz.blank?
|
||||
|
||||
if ( selector['operator'] == 'is in working time' && !biz.in_hours?(Time.zone.now) ) || ( selector['operator'] == 'is not in working time' && biz.in_hours?(Time.zone.now) )
|
||||
no_result = true
|
||||
break
|
||||
end
|
||||
|
||||
# skip to next condition
|
||||
next
|
||||
end
|
||||
|
||||
if query != ''
|
||||
query += ' AND '
|
||||
end
|
||||
|
||||
# because of no grouping support we select not_set by sub select for mentions
|
||||
if attributes[0] == 'ticket' && attributes[1] == 'mention_user_ids'
|
||||
if selector['pre_condition'] == 'not_set'
|
||||
query += if selector['operator'] == 'is'
|
||||
"(SELECT 1 FROM mentions mentions_sub WHERE mentions_sub.mentionable_type = 'Ticket' AND mentions_sub.mentionable_id = tickets.id) IS NULL"
|
||||
else
|
||||
"1 = (SELECT 1 FROM mentions mentions_sub WHERE mentions_sub.mentionable_type = 'Ticket' AND mentions_sub.mentionable_id = tickets.id)"
|
||||
end
|
||||
else
|
||||
query += if selector['operator'] == 'is'
|
||||
'mentions.user_id IN (?)'
|
||||
else
|
||||
'mentions.user_id NOT IN (?)'
|
||||
end
|
||||
if selector['pre_condition'] == 'current_user.id'
|
||||
bind_params.push current_user_id
|
||||
else
|
||||
bind_params.push selector['value']
|
||||
end
|
||||
end
|
||||
next
|
||||
end
|
||||
|
||||
if selector['operator'] == 'is'
|
||||
if selector['pre_condition'] == 'not_set'
|
||||
if attributes[1].match?(%r{^(created_by|updated_by|owner|customer|user)_id})
|
||||
query += "(#{attribute} IS NULL OR #{attribute} IN (?))"
|
||||
bind_params.push 1
|
||||
else
|
||||
query += "#{attribute} IS NULL"
|
||||
end
|
||||
elsif selector['pre_condition'] == 'current_user.id'
|
||||
raise "Use current_user.id in selector, but no current_user is set #{selector.inspect}" if !current_user_id
|
||||
|
||||
query += "#{attribute} IN (?)"
|
||||
if attributes[1] == 'out_of_office_replacement_id'
|
||||
bind_params.push User.find(current_user_id).out_of_office_agent_of.pluck(:id)
|
||||
else
|
||||
bind_params.push current_user_id
|
||||
end
|
||||
elsif selector['pre_condition'] == 'current_user.organization_id'
|
||||
raise "Use current_user.id in selector, but no current_user is set #{selector.inspect}" if !current_user_id
|
||||
|
||||
query += "#{attribute} IN (?)"
|
||||
user = User.find_by(id: current_user_id)
|
||||
bind_params.push user.organization_id
|
||||
else
|
||||
# rubocop:disable Style/IfInsideElse
|
||||
if selector['value'].nil?
|
||||
query += "#{attribute} IS NULL"
|
||||
else
|
||||
if attributes[1] == 'out_of_office_replacement_id'
|
||||
query += "#{attribute} IN (?)"
|
||||
bind_params.push User.find(selector['value']).out_of_office_agent_of.pluck(:id)
|
||||
else
|
||||
if selector['value'].class != Array
|
||||
selector['value'] = [selector['value']]
|
||||
end
|
||||
query += if selector['value'].include?('')
|
||||
"(#{attribute} IN (?) OR #{attribute} IS NULL)"
|
||||
else
|
||||
"#{attribute} IN (?)"
|
||||
end
|
||||
bind_params.push selector['value']
|
||||
end
|
||||
end
|
||||
# rubocop:enable Style/IfInsideElse
|
||||
end
|
||||
elsif selector['operator'] == 'is not'
|
||||
if selector['pre_condition'] == 'not_set'
|
||||
if attributes[1].match?(%r{^(created_by|updated_by|owner|customer|user)_id})
|
||||
query += "(#{attribute} IS NOT NULL AND #{attribute} NOT IN (?))"
|
||||
bind_params.push 1
|
||||
else
|
||||
query += "#{attribute} IS NOT NULL"
|
||||
end
|
||||
elsif selector['pre_condition'] == 'current_user.id'
|
||||
query += "(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
|
||||
if attributes[1] == 'out_of_office_replacement_id'
|
||||
bind_params.push User.find(current_user_id).out_of_office_agent_of.pluck(:id)
|
||||
else
|
||||
bind_params.push current_user_id
|
||||
end
|
||||
elsif selector['pre_condition'] == 'current_user.organization_id'
|
||||
query += "(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
|
||||
user = User.find_by(id: current_user_id)
|
||||
bind_params.push user.organization_id
|
||||
else
|
||||
# rubocop:disable Style/IfInsideElse
|
||||
if selector['value'].nil?
|
||||
query += "#{attribute} IS NOT NULL"
|
||||
else
|
||||
if attributes[1] == 'out_of_office_replacement_id'
|
||||
bind_params.push User.find(selector['value']).out_of_office_agent_of.pluck(:id)
|
||||
query += "(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
|
||||
else
|
||||
if selector['value'].class != Array
|
||||
selector['value'] = [selector['value']]
|
||||
end
|
||||
query += if selector['value'].include?('')
|
||||
"(#{attribute} IS NOT NULL AND #{attribute} NOT IN (?))"
|
||||
else
|
||||
"(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
|
||||
end
|
||||
bind_params.push selector['value']
|
||||
end
|
||||
end
|
||||
# rubocop:enable Style/IfInsideElse
|
||||
end
|
||||
elsif selector['operator'] == 'contains'
|
||||
query += "#{attribute} #{like} (?)"
|
||||
value = "%#{selector['value']}%"
|
||||
bind_params.push value
|
||||
elsif selector['operator'] == 'contains not'
|
||||
query += "#{attribute} NOT #{like} (?)"
|
||||
value = "%#{selector['value']}%"
|
||||
bind_params.push value
|
||||
elsif selector['operator'] == 'contains all' && attributes[0] == 'ticket' && attributes[1] == 'tags'
|
||||
query += "? = (
|
||||
SELECT
|
||||
COUNT(*)
|
||||
FROM
|
||||
tag_objects,
|
||||
tag_items,
|
||||
tags
|
||||
WHERE
|
||||
tickets.id = tags.o_id AND
|
||||
tag_objects.id = tags.tag_object_id AND
|
||||
tag_objects.name = 'Ticket' AND
|
||||
tag_items.id = tags.tag_item_id AND
|
||||
tag_items.name IN (?)
|
||||
)"
|
||||
bind_params.push selector['value'].count
|
||||
bind_params.push selector['value']
|
||||
elsif selector['operator'] == 'contains one' && attributes[0] == 'ticket' && attributes[1] == 'tags'
|
||||
tables += ', tag_objects, tag_items, tags'
|
||||
query += "
|
||||
tickets.id = tags.o_id AND
|
||||
tag_objects.id = tags.tag_object_id AND
|
||||
tag_objects.name = 'Ticket' AND
|
||||
tag_items.id = tags.tag_item_id AND
|
||||
tag_items.name IN (?)"
|
||||
|
||||
bind_params.push selector['value']
|
||||
elsif selector['operator'] == 'contains all not' && attributes[0] == 'ticket' && attributes[1] == 'tags'
|
||||
query += "0 = (
|
||||
SELECT
|
||||
COUNT(*)
|
||||
FROM
|
||||
tag_objects,
|
||||
tag_items,
|
||||
tags
|
||||
WHERE
|
||||
tickets.id = tags.o_id AND
|
||||
tag_objects.id = tags.tag_object_id AND
|
||||
tag_objects.name = 'Ticket' AND
|
||||
tag_items.id = tags.tag_item_id AND
|
||||
tag_items.name IN (?)
|
||||
)"
|
||||
bind_params.push selector['value']
|
||||
elsif selector['operator'] == 'contains one not' && attributes[0] == 'ticket' && attributes[1] == 'tags'
|
||||
query += "(
|
||||
SELECT
|
||||
COUNT(*)
|
||||
FROM
|
||||
tag_objects,
|
||||
tag_items,
|
||||
tags
|
||||
WHERE
|
||||
tickets.id = tags.o_id AND
|
||||
tag_objects.id = tags.tag_object_id AND
|
||||
tag_objects.name = 'Ticket' AND
|
||||
tag_items.id = tags.tag_item_id AND
|
||||
tag_items.name IN (?)
|
||||
) BETWEEN 0 AND 0"
|
||||
bind_params.push selector['value']
|
||||
elsif selector['operator'] == 'before (absolute)'
|
||||
query += "#{attribute} <= ?"
|
||||
bind_params.push selector['value']
|
||||
elsif selector['operator'] == 'after (absolute)'
|
||||
query += "#{attribute} >= ?"
|
||||
bind_params.push selector['value']
|
||||
elsif selector['operator'] == 'within last (relative)'
|
||||
query += "#{attribute} BETWEEN ? AND ?"
|
||||
time = nil
|
||||
case selector['range']
|
||||
when 'minute'
|
||||
time = selector['value'].to_i.minutes.ago
|
||||
when 'hour'
|
||||
time = selector['value'].to_i.hours.ago
|
||||
when 'day'
|
||||
time = selector['value'].to_i.days.ago
|
||||
when 'month'
|
||||
time = selector['value'].to_i.months.ago
|
||||
when 'year'
|
||||
time = selector['value'].to_i.years.ago
|
||||
else
|
||||
raise "Unknown selector attributes '#{selector.inspect}'"
|
||||
end
|
||||
bind_params.push time
|
||||
bind_params.push Time.zone.now
|
||||
elsif selector['operator'] == 'within next (relative)'
|
||||
query += "#{attribute} BETWEEN ? AND ?"
|
||||
time = nil
|
||||
case selector['range']
|
||||
when 'minute'
|
||||
time = selector['value'].to_i.minutes.from_now
|
||||
when 'hour'
|
||||
time = selector['value'].to_i.hours.from_now
|
||||
when 'day'
|
||||
time = selector['value'].to_i.days.from_now
|
||||
when 'month'
|
||||
time = selector['value'].to_i.months.from_now
|
||||
when 'year'
|
||||
time = selector['value'].to_i.years.from_now
|
||||
else
|
||||
raise "Unknown selector attributes '#{selector.inspect}'"
|
||||
end
|
||||
bind_params.push Time.zone.now
|
||||
bind_params.push time
|
||||
elsif selector['operator'] == 'before (relative)'
|
||||
query += "#{attribute} <= ?"
|
||||
time = nil
|
||||
case selector['range']
|
||||
when 'minute'
|
||||
time = selector['value'].to_i.minutes.ago
|
||||
when 'hour'
|
||||
time = selector['value'].to_i.hours.ago
|
||||
when 'day'
|
||||
time = selector['value'].to_i.days.ago
|
||||
when 'month'
|
||||
time = selector['value'].to_i.months.ago
|
||||
when 'year'
|
||||
time = selector['value'].to_i.years.ago
|
||||
else
|
||||
raise "Unknown selector attributes '#{selector.inspect}'"
|
||||
end
|
||||
bind_params.push time
|
||||
elsif selector['operator'] == 'after (relative)'
|
||||
query += "#{attribute} >= ?"
|
||||
time = nil
|
||||
case selector['range']
|
||||
when 'minute'
|
||||
time = selector['value'].to_i.minutes.from_now
|
||||
when 'hour'
|
||||
time = selector['value'].to_i.hours.from_now
|
||||
when 'day'
|
||||
time = selector['value'].to_i.days.from_now
|
||||
when 'month'
|
||||
time = selector['value'].to_i.months.from_now
|
||||
when 'year'
|
||||
time = selector['value'].to_i.years.from_now
|
||||
else
|
||||
raise "Unknown selector attributes '#{selector.inspect}'"
|
||||
end
|
||||
bind_params.push time
|
||||
elsif selector['operator'] == 'till (relative)'
|
||||
query += "#{attribute} <= ?"
|
||||
time = nil
|
||||
case selector['range']
|
||||
when 'minute'
|
||||
time = selector['value'].to_i.minutes.from_now
|
||||
when 'hour'
|
||||
time = selector['value'].to_i.hours.from_now
|
||||
when 'day'
|
||||
time = selector['value'].to_i.days.from_now
|
||||
when 'month'
|
||||
time = selector['value'].to_i.months.from_now
|
||||
when 'year'
|
||||
time = selector['value'].to_i.years.from_now
|
||||
else
|
||||
raise "Unknown selector attributes '#{selector.inspect}'"
|
||||
end
|
||||
bind_params.push time
|
||||
elsif selector['operator'] == 'from (relative)'
|
||||
query += "#{attribute} >= ?"
|
||||
time = nil
|
||||
case selector['range']
|
||||
when 'minute'
|
||||
time = selector['value'].to_i.minutes.ago
|
||||
when 'hour'
|
||||
time = selector['value'].to_i.hours.ago
|
||||
when 'day'
|
||||
time = selector['value'].to_i.days.ago
|
||||
when 'month'
|
||||
time = selector['value'].to_i.months.ago
|
||||
when 'year'
|
||||
time = selector['value'].to_i.years.ago
|
||||
else
|
||||
raise "Unknown selector attributes '#{selector.inspect}'"
|
||||
end
|
||||
bind_params.push time
|
||||
else
|
||||
raise "Invalid operator '#{selector['operator']}' for '#{selector['value'].inspect}'"
|
||||
end
|
||||
end
|
||||
|
||||
return if no_result
|
||||
|
||||
[query, bind_params, tables]
|
||||
Ticket::Selector::Sql.new(selector: selectors, options: options).get
|
||||
end
|
||||
|
||||
=begin
|
||||
|
|
@ -1011,9 +620,10 @@ perform changes on ticket
|
|||
end
|
||||
end
|
||||
|
||||
objects = build_notification_template_objects(article)
|
||||
perform_notification = {}
|
||||
perform_article = {}
|
||||
changed = false
|
||||
perform_article = {}
|
||||
changed = false
|
||||
perform.each do |key, value|
|
||||
(object_name, attribute) = key.split('.', 2)
|
||||
raise "Unable to update object #{object_name}.#{attribute}, only can update tickets, send notifications and create articles!" if object_name != 'ticket' && object_name != 'article' && object_name != 'notification'
|
||||
|
|
@ -1034,23 +644,7 @@ perform changes on ticket
|
|||
when 'static'
|
||||
value['value']
|
||||
when 'relative'
|
||||
pendtil = Time.zone.now
|
||||
val = value['value'].to_i
|
||||
|
||||
case value['range']
|
||||
when 'day'
|
||||
pendtil += val.days
|
||||
when 'minute'
|
||||
pendtil += val.minutes
|
||||
when 'hour'
|
||||
pendtil += val.hours
|
||||
when 'month'
|
||||
pendtil += val.months
|
||||
when 'year'
|
||||
pendtil += val.years
|
||||
end
|
||||
|
||||
pendtil
|
||||
TimeRangeHelper.relative(range: value['range'], value: value['value'])
|
||||
end
|
||||
|
||||
if new_value
|
||||
|
|
@ -1095,7 +689,7 @@ perform changes on ticket
|
|||
if value['pre_condition'].start_with?('not_set')
|
||||
value['value'] = 1
|
||||
elsif value['pre_condition'].start_with?('current_user.')
|
||||
raise 'Unable to use current_user, got no current_user_id for ticket.perform_changes' if !current_user_id
|
||||
raise __("The required parameter 'current_user_id' is missing.") if !current_user_id
|
||||
|
||||
value['value'] = current_user_id
|
||||
end
|
||||
|
|
@ -1106,6 +700,14 @@ perform changes on ticket
|
|||
|
||||
changed = true
|
||||
|
||||
if value['value'].is_a?(String)
|
||||
value['value'] = NotificationFactory::Mailer.template(
|
||||
templateInline: value['value'],
|
||||
objects: objects,
|
||||
quote: true,
|
||||
)
|
||||
end
|
||||
|
||||
self[attribute] = value['value']
|
||||
logger.debug { "set #{object_name}.#{attribute} = #{value['value'].inspect} for ticket_id #{id}" }
|
||||
end
|
||||
|
|
@ -1114,10 +716,8 @@ perform changes on ticket
|
|||
save!
|
||||
end
|
||||
|
||||
objects = build_notification_template_objects(article)
|
||||
|
||||
perform_article.each do |key, value|
|
||||
raise 'Unable to create article, we only support article.note' if key != 'article.note'
|
||||
raise __("Article could not be created. An unsupported key other than 'article.note' was provided.") if key != 'article.note'
|
||||
|
||||
add_trigger_note(id, value, objects, perform_origin)
|
||||
end
|
||||
|
|
@ -1209,7 +809,7 @@ perform active triggers on ticket
|
|||
else
|
||||
::Trigger.where(active: true).order(:name)
|
||||
end
|
||||
return [true, 'No triggers active'] if triggers.blank?
|
||||
return [true, __('No triggers active')] if triggers.blank?
|
||||
|
||||
# check if notification should be send because of customer emails
|
||||
send_notification = true
|
||||
|
|
@ -1226,95 +826,6 @@ perform active triggers on ticket
|
|||
triggers.each do |trigger|
|
||||
logger.debug { "Probe trigger (#{trigger.name}/#{trigger.id}) for this object (Ticket:#{ticket.id}/Loop:#{local_options[:loop_count]})" }
|
||||
|
||||
condition = trigger.condition
|
||||
|
||||
# check if one article attribute is used
|
||||
one_has_changed_done = false
|
||||
article_selector = false
|
||||
trigger.condition.each_key do |key|
|
||||
(object_name, attribute) = key.split('.', 2)
|
||||
next if object_name != 'article'
|
||||
next if attribute == 'id'
|
||||
|
||||
article_selector = true
|
||||
end
|
||||
if article && article_selector
|
||||
one_has_changed_done = true
|
||||
end
|
||||
if article && type == 'update'
|
||||
one_has_changed_done = true
|
||||
end
|
||||
|
||||
# check ticket "has changed" options
|
||||
has_changed_done = true
|
||||
condition.each do |key, value|
|
||||
next if value.blank?
|
||||
next if value['operator'].blank?
|
||||
next if !value['operator']['has changed']
|
||||
|
||||
# remove condition item, because it has changed
|
||||
(object_name, attribute) = key.split('.', 2)
|
||||
next if object_name != 'ticket'
|
||||
next if item[:changes].blank?
|
||||
next if !item[:changes].key?(attribute)
|
||||
|
||||
condition.delete(key)
|
||||
one_has_changed_done = true
|
||||
end
|
||||
|
||||
# check if we have not matching "has changed" attributes
|
||||
condition.each_value do |value|
|
||||
next if value.blank?
|
||||
next if value['operator'].blank?
|
||||
next if !value['operator']['has changed']
|
||||
|
||||
has_changed_done = false
|
||||
break
|
||||
end
|
||||
|
||||
# check ticket action
|
||||
if condition['ticket.action']
|
||||
next if condition['ticket.action']['operator'] == 'is' && condition['ticket.action']['value'] != type
|
||||
next if condition['ticket.action']['operator'] != 'is' && condition['ticket.action']['value'] == type
|
||||
|
||||
condition.delete('ticket.action')
|
||||
end
|
||||
next if !has_changed_done
|
||||
|
||||
# check in min one attribute of condition has changed on update
|
||||
one_has_changed_condition = false
|
||||
if type == 'update'
|
||||
|
||||
# verify if ticket condition exists
|
||||
condition.each_key do |key|
|
||||
(object_name, attribute) = key.split('.', 2)
|
||||
next if object_name != 'ticket'
|
||||
|
||||
one_has_changed_condition = true
|
||||
next if item[:changes].blank?
|
||||
next if !item[:changes].key?(attribute)
|
||||
|
||||
one_has_changed_done = true
|
||||
break
|
||||
end
|
||||
next if one_has_changed_condition && !one_has_changed_done
|
||||
end
|
||||
|
||||
# check if ticket selector is matching
|
||||
condition['ticket.id'] = {
|
||||
operator: 'is',
|
||||
value: ticket.id,
|
||||
}
|
||||
next if article_selector && !article
|
||||
|
||||
# check if article selector is matching
|
||||
if article_selector
|
||||
condition['article.id'] = {
|
||||
operator: 'is',
|
||||
value: article.id,
|
||||
}
|
||||
end
|
||||
|
||||
user_id = ticket.updated_by_id
|
||||
if article
|
||||
user_id = article.updated_by_id
|
||||
|
|
@ -1323,7 +834,7 @@ perform active triggers on ticket
|
|||
user = User.lookup(id: user_id)
|
||||
|
||||
# verify is condition is matching
|
||||
ticket_count, tickets = Ticket.selectors(condition, limit: 1, execution_time: true, current_user: user, access: 'ignore')
|
||||
ticket_count, tickets = Ticket.selectors(trigger.condition, limit: 1, execution_time: true, current_user: user, access: 'ignore', ticket_action: type, ticket_id: ticket.id, article_id: article&.id, changes: item[:changes], changes_required: true)
|
||||
|
||||
next if ticket_count.blank?
|
||||
next if ticket_count.zero?
|
||||
|
|
@ -1455,7 +966,7 @@ result
|
|||
|
||||
customer = User.find_by(id: customer_id)
|
||||
return true if !customer
|
||||
return true if organization_id == customer.organization_id
|
||||
return true if organization_id.present? && customer.organization_id?(organization_id)
|
||||
|
||||
self.organization_id = customer.organization_id
|
||||
true
|
||||
|
|
@ -1523,9 +1034,30 @@ result
|
|||
# if another email notification trigger preceded this one
|
||||
# (see https://github.com/zammad/zammad/issues/1543)
|
||||
def build_notification_template_objects(article)
|
||||
last_article = nil
|
||||
last_internal_article = nil
|
||||
last_external_article = nil
|
||||
all_articles = articles
|
||||
|
||||
if article.nil?
|
||||
last_article = all_articles.last
|
||||
last_internal_article = all_articles.reverse.find(&:internal?)
|
||||
last_external_article = all_articles.reverse.find { |a| !a.internal? }
|
||||
else
|
||||
last_article = article
|
||||
last_internal_article = article.internal? ? article : all_articles.reverse.find(&:internal?)
|
||||
last_external_article = article.internal? ? all_articles.reverse.find { |a| !a.internal? } : article
|
||||
end
|
||||
|
||||
{
|
||||
ticket: self,
|
||||
article: article || articles.last
|
||||
ticket: self,
|
||||
article: last_article,
|
||||
last_article: last_article,
|
||||
last_internal_article: last_internal_article,
|
||||
last_external_article: last_external_article,
|
||||
created_article: article,
|
||||
created_internal_article: article&.internal? ? article : nil,
|
||||
created_external_article: article&.internal? ? nil : article,
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -1587,7 +1119,7 @@ result
|
|||
Mail::AddressList.new(recipient_email).addresses.each do |address|
|
||||
recipient_email = address.address
|
||||
email_address_validation = EmailAddressValidation.new(recipient_email)
|
||||
break if recipient_email.present? && email_address_validation.valid_format?
|
||||
break if recipient_email.present? && email_address_validation.valid?
|
||||
end
|
||||
rescue
|
||||
if recipient_email.present?
|
||||
|
|
@ -1600,7 +1132,7 @@ result
|
|||
end
|
||||
|
||||
email_address_validation = EmailAddressValidation.new(recipient_email)
|
||||
next if !email_address_validation.valid_format?
|
||||
next if !email_address_validation.valid?
|
||||
|
||||
# do not send notification if system address
|
||||
next if EmailAddress.exists?(email: recipient_email.downcase)
|
||||
|
|
@ -1700,7 +1232,7 @@ result
|
|||
sign = value['sign'].present? && value['sign'] != 'no'
|
||||
encryption = value['encryption'].present? && value['encryption'] != 'no'
|
||||
security = {
|
||||
type: security_type,
|
||||
type: security_type,
|
||||
sign: {
|
||||
success: false,
|
||||
},
|
||||
|
|
@ -1717,10 +1249,10 @@ result
|
|||
else
|
||||
cert = SMIMECertificate.for_sender_email_address(from)
|
||||
end
|
||||
|
||||
begin
|
||||
list = Mail::AddressList.new(email_address.email)
|
||||
from = list.addresses.first.to_s
|
||||
|
||||
if cert && !cert.expired?
|
||||
sign_found = true
|
||||
security[:sign][:success] = true
|
||||
|
|
@ -1795,7 +1327,7 @@ result
|
|||
)
|
||||
|
||||
attachments_inline.each do |attachment|
|
||||
Store.add(
|
||||
Store.create!(
|
||||
object: 'Ticket::Article',
|
||||
o_id: message.id,
|
||||
data: attachment[:data],
|
||||
|
|
@ -1805,6 +1337,11 @@ result
|
|||
end
|
||||
|
||||
original_article = objects[:article]
|
||||
|
||||
if ActiveModel::Type::Boolean.new.cast(value['include_attachments']) == true && original_article&.attachments.present?
|
||||
original_article.clone_attachments('Ticket::Article', message.id, only_attached_attachments: true)
|
||||
end
|
||||
|
||||
if original_article&.should_clone_inline_attachments? # rubocop:disable Style/GuardClause
|
||||
original_article.clone_attachments('Ticket::Article', message.id, only_inline_attachments: true)
|
||||
original_article.should_clone_inline_attachments = false # cancel the temporary flag after cloning
|
||||
|
|
@ -1903,7 +1440,15 @@ result
|
|||
return 0 if !user.preferences[:mail_delivery_failed]
|
||||
return 0 if user.preferences[:mail_delivery_failed_data].blank?
|
||||
|
||||
# blocked for 60 full days
|
||||
(user.preferences[:mail_delivery_failed_data].to_date - Time.zone.now.to_date).to_i + 61
|
||||
# blocked for 60 full days; see #4459
|
||||
remaining_days = (user.preferences[:mail_delivery_failed_data].to_date - Time.zone.now.to_date).to_i + 61
|
||||
return remaining_days if remaining_days.positive?
|
||||
|
||||
# cleanup user preferences
|
||||
user.preferences[:mail_delivery_failed] = false
|
||||
user.preferences[:mail_delivery_failed_data] = nil
|
||||
user.save!
|
||||
0
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue