Refactoring: Migrate Rails Observers to Concerns.

This commit is contained in:
Thorsten Eckel 2021-03-01 08:18:40 +00:00
parent 5b6f0327c6
commit cb2286fae4
54 changed files with 903 additions and 556 deletions

View file

@ -138,6 +138,18 @@ Metrics/AbcSize:
- 'app/models/concerns/has_rich_text.rb'
- 'app/models/concerns/has_search_index_backend.rb'
- 'app/models/concerns/has_search_sortable.rb'
- 'app/models/concerns/ticket/article/adds_metadata_email.rb'
- 'app/models/concerns/ticket/article/adds_metadata_general.rb'
- 'app/models/concerns/ticket/article/adds_metadata_origin_by_id.rb'
- 'app/models/concerns/ticket/article/enqueue_communicate_email_job.rb'
- 'app/models/concerns/ticket/article/enqueue_communicate_facebook_job.rb'
- 'app/models/concerns/ticket/article/enqueue_communicate_sms_job.rb'
- 'app/models/concerns/ticket/article/enqueue_communicate_telegram_job.rb'
- 'app/models/concerns/ticket/article/enqueue_communicate_twitter_job.rb'
- 'app/models/concerns/ticket/article/resets_ticket_state.rb'
- 'app/models/concerns/ticket/sets_last_owner_update_time.rb'
- 'app/models/concerns/ticket/touches_associations.rb'
- 'app/models/concerns/user/performs_geo_lookup.rb'
- 'app/models/cti/caller_id.rb'
- 'app/models/cti/driver/base.rb'
- 'app/models/cti/driver/placetel.rb'
@ -156,19 +168,7 @@ Metrics/AbcSize:
- 'app/models/link.rb'
- 'app/models/object_manager/attribute.rb'
- 'app/models/observer/chat/leave/background_job.rb'
- 'app/models/observer/ticket/article/communicate_email.rb'
- 'app/models/observer/ticket/article/communicate_facebook.rb'
- 'app/models/observer/ticket/article/communicate_sms.rb'
- 'app/models/observer/ticket/article/communicate_telegram.rb'
- 'app/models/observer/ticket/article/communicate_twitter.rb'
- 'app/models/observer/ticket/article/fillup_from_email.rb'
- 'app/models/observer/ticket/article/fillup_from_general.rb'
- 'app/models/observer/ticket/article/fillup_from_origin_by_id.rb'
- 'app/models/observer/ticket/last_owner_update.rb'
- 'app/models/observer/ticket/ref_object_touch.rb'
- 'app/models/observer/ticket/reset_new_state.rb'
- 'app/models/observer/transaction.rb'
- 'app/models/observer/user/geo.rb'
- 'app/models/online_notification.rb'
- 'app/models/online_notification/assets.rb'
- 'app/models/organization/assets.rb'
@ -533,6 +533,17 @@ Metrics/CyclomaticComplexity:
- 'app/models/concerns/has_rich_text.rb'
- 'app/models/concerns/has_search_index_backend.rb'
- 'app/models/concerns/has_search_sortable.rb'
- 'app/models/concerns/ticket/article/adds_metadata_email.rb'
- 'app/models/concerns/ticket/article/adds_metadata_general.rb'
- 'app/models/concerns/ticket/article/adds_metadata_origin_by_id.rb'
- 'app/models/concerns/ticket/article/enqueue_communicate_email_job.rb'
- 'app/models/concerns/ticket/article/enqueue_communicate_facebook_job.rb'
- 'app/models/concerns/ticket/article/enqueue_communicate_sms_job.rb'
- 'app/models/concerns/ticket/article/enqueue_communicate_twitter_job.rb'
- 'app/models/concerns/ticket/article/resets_ticket_state.rb'
- 'app/models/concerns/ticket/sets_last_owner_update_time.rb'
- 'app/models/concerns/ticket/touches_associations.rb'
- 'app/models/concerns/user/performs_geo_lookup.rb'
- 'app/models/cti/caller_id.rb'
- 'app/models/cti/driver/base.rb'
- 'app/models/cti/driver/placetel.rb'
@ -545,18 +556,7 @@ Metrics/CyclomaticComplexity:
- 'app/models/karma/activity_log.rb'
- 'app/models/knowledge_base.rb'
- 'app/models/object_manager/attribute.rb'
- 'app/models/observer/ticket/article/communicate_email.rb'
- 'app/models/observer/ticket/article/communicate_facebook.rb'
- 'app/models/observer/ticket/article/communicate_sms.rb'
- 'app/models/observer/ticket/article/communicate_twitter.rb'
- 'app/models/observer/ticket/article/fillup_from_email.rb'
- 'app/models/observer/ticket/article/fillup_from_general.rb'
- 'app/models/observer/ticket/article/fillup_from_origin_by_id.rb'
- 'app/models/observer/ticket/last_owner_update.rb'
- 'app/models/observer/ticket/ref_object_touch.rb'
- 'app/models/observer/ticket/reset_new_state.rb'
- 'app/models/observer/transaction.rb'
- 'app/models/observer/user/geo.rb'
- 'app/models/online_notification/assets.rb'
- 'app/models/organization/assets.rb'
- 'app/models/organization/search.rb'
@ -763,6 +763,16 @@ Metrics/PerceivedComplexity:
- 'app/models/concerns/has_rich_text.rb'
- 'app/models/concerns/has_search_index_backend.rb'
- 'app/models/concerns/has_search_sortable.rb'
- 'app/models/concerns/ticket/article/adds_metadata_email.rb'
- 'app/models/concerns/ticket/article/adds_metadata_general.rb'
- 'app/models/concerns/ticket/article/adds_metadata_origin_by_id.rb'
- 'app/models/concerns/ticket/article/enqueue_communicate_email_job.rb'
- 'app/models/concerns/ticket/article/enqueue_communicate_facebook_job.rb'
- 'app/models/concerns/ticket/article/enqueue_communicate_twitter_job.rb'
- 'app/models/concerns/ticket/article/resets_ticket_state.rb'
- 'app/models/concerns/ticket/sets_last_owner_update_time.rb'
- 'app/models/concerns/ticket/touches_associations.rb'
- 'app/models/concerns/user/performs_geo_lookup.rb'
- 'app/models/cti/caller_id.rb'
- 'app/models/cti/driver/base.rb'
- 'app/models/cti/driver/placetel.rb'
@ -775,16 +785,6 @@ Metrics/PerceivedComplexity:
- 'app/models/karma/activity_log.rb'
- 'app/models/knowledge_base.rb'
- 'app/models/object_manager/attribute.rb'
- 'app/models/observer/ticket/article/communicate_email.rb'
- 'app/models/observer/ticket/article/communicate_facebook.rb'
- 'app/models/observer/ticket/article/communicate_sms.rb'
- 'app/models/observer/ticket/article/communicate_twitter.rb'
- 'app/models/observer/ticket/article/fillup_from_email.rb'
- 'app/models/observer/ticket/article/fillup_from_general.rb'
- 'app/models/observer/ticket/article/fillup_from_origin_by_id.rb'
- 'app/models/observer/ticket/last_owner_update.rb'
- 'app/models/observer/ticket/ref_object_touch.rb'
- 'app/models/observer/ticket/reset_new_state.rb'
- 'app/models/observer/transaction.rb'
- 'app/models/online_notification/assets.rb'
- 'app/models/organization/assets.rb'

View file

@ -0,0 +1,41 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
# Records added/removed tags also in the ticket history.
module Tag::WritesToTicketHistory
extend ActiveSupport::Concern
included do
after_create :write_tag_added_to_ticket_history
after_destroy :write_tag_removed_to_ticket_history
end
private
def write_tag_added_to_ticket_history
return true if tag_object.name != 'Ticket'
History.add(
o_id: o_id,
history_type: 'added',
history_object: 'Ticket',
history_attribute: 'tag',
value_to: tag_item.name,
created_by_id: created_by_id,
)
end
def write_tag_removed_to_ticket_history
return true if tag_object.name != 'Ticket'
History.add(
o_id: o_id,
history_type: 'removed',
history_object: 'Ticket',
history_attribute: 'tag',
value_to: tag_item.name,
created_by_id: created_by_id,
)
end
end

View file

@ -1,9 +1,16 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
class Observer::Ticket::Article::FillupFromEmail < ActiveRecord::Observer
observe 'ticket::_article'
# Adds certain (missing) meta data when creating email articles.
module Ticket::Article::AddsMetadataEmail
extend ActiveSupport::Concern
def before_create(record)
included do
before_create :ticket_article_add_metadata_email
end
private
def ticket_article_add_metadata_email
# return if we run import mode
return true if Setting.get('import_mode')
@ -13,36 +20,36 @@ class Observer::Ticket::Article::FillupFromEmail < ActiveRecord::Observer
return if ApplicationHandleInfo.postmaster?
# if sender is customer, do not change anything
return true if !record.sender_id
return true if !sender_id
sender = Ticket::Article::Sender.lookup(id: record.sender_id)
sender = Ticket::Article::Sender.lookup(id: sender_id)
return true if sender.nil?
return true if sender.name == 'Customer'
# set email attributes
return true if !record.type_id
return true if !type_id
type = Ticket::Article::Type.lookup(id: record.type_id)
type = Ticket::Article::Type.lookup(id: type_id)
return true if type.nil?
return true if type.name != 'email'
# set subject if empty
ticket = record.ticket
if !record.subject || record.subject == ''
record.subject = ticket.title
ticket = self.ticket
if !subject || subject == ''
self.subject = ticket.title
end
# clean subject
record.subject = ticket.subject_clean(record.subject)
self.subject = ticket.subject_clean(subject)
# generate message id, force it in production, in test allow to set it for testing reasons
if !record.message_id || Rails.env.production?
if !message_id || Rails.env.production?
fqdn = Setting.get('fqdn')
record.message_id = "<#{DateTime.current.to_s(:number)}.#{record.ticket_id}.#{rand(999_999_999_999)}@#{fqdn}>"
self.message_id = "<#{DateTime.current.to_s(:number)}.#{ticket_id}.#{rand(999_999_999_999)}@#{fqdn}>"
end
# generate message_id_md5
record.check_message_id_md5
check_message_id_md5
# set sender
email_address = ticket.group.email_address
@ -51,20 +58,20 @@ class Observer::Ticket::Article::FillupFromEmail < ActiveRecord::Observer
end
# remember email address for background job
record.preferences['email_address_id'] = email_address.id
preferences['email_address_id'] = email_address.id
# fill from
if record.created_by_id != 1 && Setting.get('ticket_define_email_from') == 'AgentNameSystemAddressName'
if created_by_id != 1 && Setting.get('ticket_define_email_from') == 'AgentNameSystemAddressName'
separator = Setting.get('ticket_define_email_from_separator')
sender = User.find(record.created_by_id)
sender = User.find(created_by_id)
realname = "#{sender.firstname} #{sender.lastname} #{separator} #{email_address.realname}"
record.from = Channel::EmailBuild.recipient_line(realname, email_address.email)
self.from = Channel::EmailBuild.recipient_line(realname, email_address.email)
elsif Setting.get('ticket_define_email_from') == 'AgentName'
sender = User.find(record.created_by_id)
sender = User.find(created_by_id)
realname = "#{sender.firstname} #{sender.lastname}"
record.from = Channel::EmailBuild.recipient_line(realname, email_address.email)
self.from = Channel::EmailBuild.recipient_line(realname, email_address.email)
else
record.from = Channel::EmailBuild.recipient_line(email_address.realname, email_address.email)
self.from = Channel::EmailBuild.recipient_line(email_address.realname, email_address.email)
end
true
end

View file

@ -1,9 +1,17 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
class Observer::Ticket::Article::FillupFromGeneral < ActiveRecord::Observer
observe 'ticket::_article'
# Adds certain (missing) meta data when creating articles.
# This module depends on AddsMetadataOriginById to run before it.
module Ticket::Article::AddsMetadataGeneral
extend ActiveSupport::Concern
def before_create(record)
included do
before_create :ticket_article_add_metadata_general
end
private
def ticket_article_add_metadata_general
# return if we run import mode
return true if Setting.get('import_mode')
@ -13,9 +21,9 @@ class Observer::Ticket::Article::FillupFromGeneral < ActiveRecord::Observer
return true if ApplicationHandleInfo.postmaster?
# set from on all article types excluding email|twitter status|twitter direct-message|facebook feed post|facebook feed comment
return true if record.type_id.blank?
return true if type_id.blank?
type = Ticket::Article::Type.lookup(id: record.type_id)
type = Ticket::Article::Type.lookup(id: type_id)
# from will be set by channel backend
return true if type.nil?
@ -26,31 +34,31 @@ class Observer::Ticket::Article::FillupFromGeneral < ActiveRecord::Observer
return true if type.name == 'facebook feed comment'
return true if type.name == 'sms'
user_id = record.created_by_id
user_id = created_by_id
if record.origin_by_id.present?
if origin_by_id.present?
# in case the customer is using origin_by_id, force it to current session user
# and set sender to Customer
if !record.created_by.permissions?('ticket.agent')
record.origin_by_id = record.created_by_id
record.sender_id = Ticket::Article::Sender.lookup(name: 'Customer').id
if !created_by.permissions?('ticket.agent')
self.origin_by_id = created_by_id
self.sender_id = Ticket::Article::Sender.lookup(name: 'Customer').id
end
# in case origin_by is different than created_by, set sender to Customer
# Customer in context of this conversation, not as a permission
if record.origin_by != record.created_by_id
record.sender_id = Ticket::Article::Sender.lookup(name: 'Customer').id
user_id = record.origin_by_id
if origin_by != created_by_id
self.sender_id = Ticket::Article::Sender.lookup(name: 'Customer').id
user_id = origin_by_id
end
end
return true if user_id.blank?
user = User.find(user_id)
if type.name == 'web' || type.name == 'phone'
record.from = "#{user.firstname} #{user.lastname} <#{user.email}>"
self.from = "#{user.firstname} #{user.lastname} <#{user.email}>"
return
end
record.from = "#{user.firstname} #{user.lastname}"
self.from = "#{user.firstname} #{user.lastname}"
end
end

View file

@ -0,0 +1,34 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
# Adds origin_by_id field (if missing) when creating articles.
module Ticket::Article::AddsMetadataOriginById
extend ActiveSupport::Concern
included do
before_create :ticket_article_add_metadata_origin_by_id
end
private
def ticket_article_add_metadata_origin_by_id
# return if we run import mode
return true if Setting.get('import_mode')
# only do fill origin_by_id if article got created via application_server (e. g. not
# if article and sender type is set via *.postmaster)
return true if ApplicationHandleInfo.postmaster?
# check if origin_by_id exists
return true if origin_by_id.present?
return true if ticket.blank?
return true if ticket.customer_id.blank?
return true if sender_id.blank?
return true if sender.name != 'Customer'
type_name = type.name
return true if type_name != 'phone' && type_name != 'note' && type_name != 'web'
self.origin_by_id = ticket.customer_id
end
end

View file

@ -1,9 +1,16 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
class Observer::Ticket::Article::CommunicateEmail < ActiveRecord::Observer
observe 'ticket::_article'
# Schedules a backgrond communication job for new email articles.
module Ticket::Article::EnqueueCommunicateEmailJob
extend ActiveSupport::Concern
def after_create(record)
included do
after_create :ticket_article_enqueue_communicate_email_job
end
private
def ticket_article_enqueue_communicate_email_job
# return if we run import mode
return true if Setting.get('import_mode')
@ -13,20 +20,20 @@ class Observer::Ticket::Article::CommunicateEmail < ActiveRecord::Observer
return true if ApplicationHandleInfo.postmaster?
# if sender is customer, do not communicate
return true if !record.sender_id
return true if !sender_id
sender = Ticket::Article::Sender.lookup(id: record.sender_id)
sender = Ticket::Article::Sender.lookup(id: sender_id)
return true if sender.nil?
return true if sender.name == 'Customer'
# only apply on emails
return true if !record.type_id
return true if !type_id
type = Ticket::Article::Type.lookup(id: record.type_id)
type = Ticket::Article::Type.lookup(id: type_id)
return true if type.nil?
return true if type.name != 'email'
# send background job
TicketArticleCommunicateEmailJob.perform_later(record.id)
TicketArticleCommunicateEmailJob.perform_later(id)
end
end

View file

@ -1,8 +1,16 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
class Observer::Ticket::Article::CommunicateFacebook < ActiveRecord::Observer
observe 'ticket::_article'
def after_create(record)
# Schedules a backgrond communication job for new facebook articles.
module Ticket::Article::EnqueueCommunicateFacebookJob
extend ActiveSupport::Concern
included do
after_create :ticket_article_enqueue_communicate_facebook_job
end
private
def ticket_article_enqueue_communicate_facebook_job
# return if we run import mode
return true if Setting.get('import_mode')
@ -12,20 +20,20 @@ class Observer::Ticket::Article::CommunicateFacebook < ActiveRecord::Observer
return true if ApplicationHandleInfo.postmaster?
# if sender is customer, do not communicate
return true if !record.sender_id
return true if !sender_id
sender = Ticket::Article::Sender.lookup(id: record.sender_id)
sender = Ticket::Article::Sender.lookup(id: sender_id)
return true if sender.nil?
return true if sender.name == 'Customer'
# only apply for facebook
return true if !record.type_id
return true if !type_id
type = Ticket::Article::Type.lookup(id: record.type_id)
type = Ticket::Article::Type.lookup(id: type_id)
return true if type.nil?
return true if !type.name.start_with?('facebook')
CommunicateFacebookJob.perform_later(record.id)
CommunicateFacebookJob.perform_later(id)
end
end

View file

@ -0,0 +1,34 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
# Schedules a backgrond communication job for new SMS articles.
module Ticket::Article::EnqueueCommunicateSmsJob
extend ActiveSupport::Concern
included do
after_create :ticket_article_enqueue_communicate_sms_job
end
private
def ticket_article_enqueue_communicate_sms_job
# return if we run import mode
return true if Setting.get('import_mode')
# if sender is customer, do not communicate
return true if !sender_id
sender = Ticket::Article::Sender.lookup(id: sender_id)
return true if sender.nil?
return true if sender.name == 'Customer'
# only apply on sms
return true if !type_id
type = Ticket::Article::Type.lookup(id: type_id)
return true if type.nil?
return true if type.name != 'sms'
CommunicateSmsJob.perform_later(id)
end
end

View file

@ -0,0 +1,34 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
# Schedules a backgrond communication job for new telegram articles.
module Ticket::Article::EnqueueCommunicateTelegramJob
extend ActiveSupport::Concern
included do
after_create :ticket_article_enqueue_communicate_telegram_job
end
private
def ticket_article_enqueue_communicate_telegram_job
# return if we run import mode
return true if Setting.get('import_mode')
# if sender is customer, do not communicate
return true if !sender_id
sender = Ticket::Article::Sender.lookup(id: sender_id)
return true if sender.nil?
return true if sender.name == 'Customer'
# only apply on telegram messages
return true if !type_id
type = Ticket::Article::Type.lookup(id: type_id)
return true if !type.name.match?(/\Atelegram/i)
CommunicateTelegramJob.perform_later(id)
end
end

View file

@ -1,9 +1,16 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
class Observer::Ticket::Article::CommunicateTwitter < ActiveRecord::Observer
observe 'ticket::_article'
# Schedules a backgrond communication job for new twitter articles.
module Ticket::Article::EnqueueCommunicateTwitterJob
extend ActiveSupport::Concern
def after_create(record)
included do
after_create :ticket_article_enqueue_communicate_twitter_job
end
private
def ticket_article_enqueue_communicate_twitter_job
# return if we run import mode
return true if Setting.get('import_mode')
@ -13,22 +20,22 @@ class Observer::Ticket::Article::CommunicateTwitter < ActiveRecord::Observer
return true if ApplicationHandleInfo.postmaster?
# if sender is customer, do not communicate
return true if !record.sender_id
return true if !sender_id
sender = Ticket::Article::Sender.lookup(id: record.sender_id)
sender = Ticket::Article::Sender.lookup(id: sender_id)
return true if sender.nil?
return true if sender.name == 'Customer'
# only apply on tweets
return true if !record.type_id
return true if !type_id
type = Ticket::Article::Type.lookup(id: record.type_id)
type = Ticket::Article::Type.lookup(id: type_id)
return true if type.nil?
return true if !type.name.match?(/\Atwitter/i)
raise Exceptions::UnprocessableEntity, 'twitter to: parameter is missing' if record.to.blank? && type['name'] == 'twitter direct-message'
raise Exceptions::UnprocessableEntity, 'twitter to: parameter is missing' if to.blank? && type['name'] == 'twitter direct-message'
CommunicateTwitterJob.perform_later(record.id)
CommunicateTwitterJob.perform_later(id)
end
end

View file

@ -1,9 +1,16 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
class Observer::Ticket::ResetNewState < ActiveRecord::Observer
observe 'ticket::_article'
# Reopens the ticket in case certain new articles are created.
module Ticket::Article::ResetsTicketState
extend ActiveSupport::Concern
def after_create(record)
included do
after_create :ticket_article_reset_ticket_state
end
private
def ticket_article_reset_ticket_state
# return if we run import mode
return true if Setting.get('import_mode')
@ -12,16 +19,16 @@ class Observer::Ticket::ResetNewState < ActiveRecord::Observer
return true if ApplicationHandleInfo.postmaster?
# if article in internal
return true if record.internal
return true if internal
# if sender is agent
return true if Ticket::Article::Sender.lookup(id: record.sender_id).name != 'Agent'
return true if Ticket::Article::Sender.lookup(id: sender_id).name != 'Agent'
# if article is a message to customer
return true if !Ticket::Article::Type.lookup(id: record.type_id).communication
return true if !Ticket::Article::Type.lookup(id: type_id).communication
# if current ticket state is still new
ticket = Ticket.find_by(id: record.ticket_id)
ticket = Ticket.find_by(id: ticket_id)
return true if !ticket
new_state = Ticket::State.find_by(default_create: true)

View file

@ -0,0 +1,23 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
require_dependency 'stats/ticket_reopen'
# Adds new and updated tickets to the reopen log processing.
module Ticket::CallsStatsTicketReopenLog
extend ActiveSupport::Concern
included do
before_create :ticket_call_stats_ticket_reopen_log
before_update :ticket_call_stats_ticket_reopen_log
end
private
def ticket_call_stats_ticket_reopen_log
# return if we run import mode
return if Setting.get('import_mode')
Stats::TicketReopen.log('Ticket', id, saved_changes, updated_by_id)
end
end

View file

@ -0,0 +1,30 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
# Adds a background job to update the user's ticket counter on ticket changes.
module Ticket::EnqueuesUserTicketCounterJob
extend ActiveSupport::Concern
included do
after_commit :enqueue_user_ticket_counter_job
end
private
def enqueue_user_ticket_counter_job
# return if we run import mode
return true if Setting.get('import_mode')
return true if BulkImportInfo.enabled?
return true if destroyed?
return true if !customer_id
# send background job
TicketUserTicketCounterJob.perform_later(
customer_id,
UserInfo.current_user_id || updated_by_id,
)
end
end

View file

@ -0,0 +1,21 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
# Ensures pending time is always zero-seconds.
module Ticket::ResetsPendingTimeSeconds
extend ActiveSupport::Concern
included do
before_create :ticket_reset_pending_time_seconds
before_update :ticket_reset_pending_time_seconds
end
private
def ticket_reset_pending_time_seconds
return true if pending_time.blank?
return true if !pending_time_changed?
return true if pending_time.sec.zero?
self.pending_time = pending_time.change sec: 0
end
end

View file

@ -1,34 +1,32 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
class Observer::Ticket::CloseTime < ActiveRecord::Observer
observe 'ticket'
# Adds close time (if missing) when tickets are closed.
module Ticket::SetsCloseTime
extend ActiveSupport::Concern
def before_create(record)
_check(record)
end
def before_update(record)
_check(record)
included do
before_create :ticket_set_close_time
before_update :ticket_set_close_time
end
private
def _check(record)
def ticket_set_close_time
# return if we run import mode
return true if Setting.get('import_mode')
# check if close_at is already set
return true if record.close_at
return true if close_at
# check if ticket is closed now
return true if !record.state_id
return true if !state_id
state = Ticket::State.lookup(id: record.state_id)
state = Ticket::State.lookup(id: state_id)
state_type = Ticket::StateType.lookup(id: state.state_type_id)
return true if state_type.name != 'closed'
# set close_at
record.close_at = Time.zone.now
self.close_at = Time.zone.now
end
end

View file

@ -0,0 +1,49 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
# Adds a last_owner_update time on ticket changes.
module Ticket::SetsLastOwnerUpdateTime
extend ActiveSupport::Concern
included do
before_create :ticket_set_last_owner_update_time
before_update :ticket_set_last_owner_update_time
end
private
def ticket_set_last_owner_update_time
# return if we run import mode
return true if Setting.get('import_mode')
# check if owner, state or group has changed
return true if changes_to_save['owner_id'].blank? && changes_to_save['state_id'].blank? && changes_to_save['group_id'].blank? && changes_to_save['last_contact_agent_at'].blank?
# check if owner is nobody
if changes_to_save['owner_id'].present? && changes_to_save['owner_id'][1] == 1
self.last_owner_update_at = nil
return true
end
# check if group is change
if changes_to_save['group_id'].present?
group = Group.lookup(id: changes_to_save['group_id'][1])
return true if !group
if group.assignment_timeout.blank? || group.assignment_timeout.zero?
self.last_owner_update_at = nil
return true
end
end
# check if state is not new/open
if changes_to_save['state_id'].present?
state_ids = Ticket::State.by_category(:work_on).pluck(:id)
if state_ids.exclude?(changes_to_save['state_id'][1])
self.last_owner_update_at = nil
return true
end
end
self.last_owner_update_at = Time.zone.now
end
end

View file

@ -0,0 +1,30 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
# Schedules a background job to update the user's ticket seen information on ticket changes.
module Ticket::SetsOnlineNotificationSeen
extend ActiveSupport::Concern
included do
after_create :ticket_set_online_notification_seen
after_update :ticket_set_online_notification_seen
end
private
def ticket_set_online_notification_seen
# return if we run import mode
return false if Setting.get('import_mode')
# set seen only if state has changes
return false if !saved_changes?
return false if saved_changes['state_id'].blank?
# check if existing online notifications for this ticket should be set to seen
return true if !online_notification_seen_state
# set all online notifications to seen
# send background job
TicketOnlineNotificationSeenJob.perform_later(id, updated_by_id)
end
end

View file

@ -0,0 +1,37 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
# Update assigned customer and organization change_time information on ticket changes.
module Ticket::TouchesAssociations
extend ActiveSupport::Concern
included do
after_create :ticket_touch_associations
after_update :ticket_touch_associations
after_destroy :ticket_touch_associations
end
private
def ticket_touch_associations
# return if we run import mode
return true if Setting.get('import_mode')
# touch old customer if changed
customer_id_changed = saved_changes['customer_id']
if customer_id_changed && customer_id_changed[0] != customer_id_changed[1] && customer_id_changed[0]
User.find(customer_id_changed[0]).touch # rubocop:disable Rails/SkipsModelValidations
end
# touch new/current customer
customer&.touch # rubocop:disable Rails/SkipsModelValidations
# touch old organization if changed
organization_id_changed = saved_changes['organization_id']
if organization_id_changed && organization_id_changed[0] != organization_id_changed[1] && organization_id_changed[0]
Organization.find(organization_id_changed[0]).touch # rubocop:disable Rails/SkipsModelValidations
end
organization&.touch # rubocop:disable Rails/SkipsModelValidations
end
end

View file

@ -1,26 +1,23 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
class Observer::User::Geo < ActiveRecord::Observer
observe 'user'
# Perform geo data lookup on user changes.
module User::PerformsGeoLookup
extend ActiveSupport::Concern
def before_create(record)
check_geo(record)
true
included do
before_create :user_check_geo_location
before_update :user_check_geo_location
end
def before_update(record)
check_geo(record)
true
end
private
# check if geo need to be updated
def check_geo(record)
def user_check_geo_location
location = %w[address street zip city country]
# check if geo update is needed based on old/new location
if record.id
current = User.find_by(id: record.id)
if id
current = User.find_by(id: id)
return if !current
current_location = {}
@ -32,27 +29,26 @@ class Observer::User::Geo < ActiveRecord::Observer
# get full address
next_location = {}
location.each do |item|
next_location[item] = record[item]
next_location[item] = attributes[item]
end
# return if address hasn't changed and geo data is already available
return if (current_location == next_location) && record.preferences['lat'] && record.preferences['lng']
return if (current_location == next_location) && preferences['lat'] && preferences['lng']
# geo update
geo_update(record)
user_update_geo_location
end
# update geo data of user
def geo_update(record)
def user_update_geo_location
address = ''
location = %w[address street zip city country]
location.each do |item|
next if record[item].blank?
next if attributes[item].blank?
if address.present?
address += ', '
end
address += record[item]
address += attributes[item]
end
# return if no address is given
@ -60,10 +56,11 @@ class Observer::User::Geo < ActiveRecord::Observer
# lookup
latlng = Service::GeoLocation.geocode(address)
return if !latlng
# store data
record.preferences['lat'] = latlng[0]
record.preferences['lng'] = latlng[1]
preferences['lat'] = latlng[0]
preferences['lng'] = latlng[1]
end
end

View file

@ -0,0 +1,36 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
# Update assigned organization change_time information on user changes.
module User::TouchesOrganization
extend ActiveSupport::Concern
included do
after_create :touch_user_organization
after_update :touch_user_organization
after_destroy :touch_user_organization
end
private
def touch_user_organization
# return if we run import mode
return true if Setting.get('import_mode')
organization_id_changed = saved_changes['organization_id']
return true if !organization_id_changed
return true if organization_id_changed[0] == organization_id_changed[1]
# touch old organization
if organization_id_changed[0]
old_organization = Organization.find(organization_id_changed[0])
old_organization&.touch # rubocop:disable Rails/SkipsModelValidations
end
# touch new/current organization
organization&.touch # rubocop:disable Rails/SkipsModelValidations
true
end
end

View file

@ -0,0 +1,28 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
# If a user is assigned to another organization, also assign their latest tickets to it.
module User::UpdatesTicketOrganization
extend ActiveSupport::Concern
included do
after_create :user_update_ticket_organization
after_update :user_update_ticket_organization
end
private
def user_update_ticket_organization
# check if organization has changed
return true if !saved_change_to_attribute?('organization_id')
# update last 100 tickets of user
tickets = Ticket.where(customer_id: id).limit(100)
tickets.each do |ticket|
if ticket.organization_id != organization_id
ticket.organization_id = organization_id
ticket.save
end
end
end
end

View file

@ -1,36 +0,0 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
class Observer::Tag::TicketHistory < ActiveRecord::Observer
observe 'tag'
def after_create(record)
# just process ticket object tags
return true if record.tag_object.name != 'Ticket'
# add ticket history
History.add(
o_id: record.o_id,
history_type: 'added',
history_object: 'Ticket',
history_attribute: 'tag',
value_to: record.tag_item.name,
created_by_id: record.created_by_id,
)
end
def after_destroy(record)
# just process ticket object tags
return true if record.tag_object.name != 'Ticket'
# add ticket history
History.add(
o_id: record.o_id,
history_type: 'removed',
history_object: 'Ticket',
history_attribute: 'tag',
value_to: record.tag_item.name,
created_by_id: record.created_by_id,
)
end
end

View file

@ -1,25 +0,0 @@
class Observer::Ticket::Article::CommunicateSms < ActiveRecord::Observer
observe 'ticket::_article'
def after_create(record)
# return if we run import mode
return true if Setting.get('import_mode')
# if sender is customer, do not communicate
return true if !record.sender_id
sender = Ticket::Article::Sender.lookup(id: record.sender_id)
return true if sender.nil?
return true if sender.name == 'Customer'
# only apply on sms
return true if !record.type_id
type = Ticket::Article::Type.lookup(id: record.type_id)
return true if type.nil?
return true if type.name != 'sms'
CommunicateSmsJob.perform_later(record.id)
end
end

View file

@ -1,27 +0,0 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
class Observer::Ticket::Article::CommunicateTelegram < ActiveRecord::Observer
observe 'ticket::_article'
def after_create(record)
# return if we run import mode
return true if Setting.get('import_mode')
# if sender is customer, do not communicate
return true if !record.sender_id
sender = Ticket::Article::Sender.lookup(id: record.sender_id)
return true if sender.nil?
return true if sender.name == 'Customer'
# only apply on telegram messages
return true if !record.type_id
type = Ticket::Article::Type.lookup(id: record.type_id)
return true if !type.name.match?(/\Atelegram/i)
CommunicateTelegramJob.perform_later(record.id)
end
end

View file

@ -1,27 +0,0 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
class Observer::Ticket::Article::FillupFromOriginById < ActiveRecord::Observer
observe 'ticket::_article'
def before_create(record)
# return if we run import mode
return true if Setting.get('import_mode')
# only do fill origin_by_id if article got created via application_server (e. g. not
# if article and sender type is set via *.postmaster)
return true if ApplicationHandleInfo.postmaster?
# check if origin_by_id exists
return true if record.origin_by_id.present?
return true if record.ticket.blank?
return true if record.ticket.customer_id.blank?
return true if record.sender_id.blank?
return true if record.sender.name != 'Customer'
type_name = record.type.name
return true if type_name != 'phone' && type_name != 'note' && type_name != 'web'
record.origin_by_id = record.ticket.customer_id
end
end

View file

@ -1,52 +0,0 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
class Observer::Ticket::LastOwnerUpdate < ActiveRecord::Observer
observe 'ticket'
def before_create(record)
_check(record)
end
def before_update(record)
_check(record)
end
private
def _check(record)
# return if we run import mode
return true if Setting.get('import_mode')
# check if owner, state or group has changed
return true if record.changes_to_save['owner_id'].blank? && record.changes_to_save['state_id'].blank? && record.changes_to_save['group_id'].blank? && record.changes_to_save['last_contact_agent_at'].blank?
# check if owner is nobody
if record.changes_to_save['owner_id'].present? && record.changes_to_save['owner_id'][1] == 1
record.last_owner_update_at = nil
return true
end
# check if group is change
if record.changes_to_save['group_id'].present?
group = Group.lookup(id: record.changes_to_save['group_id'][1])
return true if !group
if group.assignment_timeout.blank? || group.assignment_timeout.zero?
record.last_owner_update_at = nil
return true
end
end
# check if state is not new/open
if record.changes_to_save['state_id'].present?
state_ids = Ticket::State.by_category(:work_on).pluck(:id)
if state_ids.exclude?(record.changes_to_save['state_id'][1])
record.last_owner_update_at = nil
return true
end
end
record.last_owner_update_at = Time.zone.now
end
end

View file

@ -1,32 +0,0 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
class Observer::Ticket::OnlineNotificationSeen < ActiveRecord::Observer
observe 'ticket'
def after_create(record)
_check(record)
end
def after_update(record)
_check(record)
end
private
def _check(record)
# return if we run import mode
return false if Setting.get('import_mode')
# set seen only if state has changes
return false if !record.saved_changes?
return false if record.saved_changes['state_id'].blank?
# check if existing online notifications for this ticket should be set to seen
return true if !record.online_notification_seen_state
# set all online notifications to seen
# send background job
TicketOnlineNotificationSeenJob.perform_later(record.id, record.updated_by_id)
end
end

View file

@ -1,22 +0,0 @@
# Ensures pending time is always zero-seconds
class Observer::Ticket::PendingTime < ActiveRecord::Observer
observe 'ticket'
def before_create(record)
_check(record)
end
def before_update(record)
_check(record)
end
private
def _check(record)
return true if record.pending_time.blank?
return true if !record.pending_time_changed?
return true if record.pending_time.sec.zero?
record.pending_time = record.pending_time.change sec: 0
end
end

View file

@ -1,43 +0,0 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
class Observer::Ticket::RefObjectTouch < ActiveRecord::Observer
observe 'ticket'
def after_create(record)
ref_object_touch(record)
end
def after_update(record)
ref_object_touch(record)
end
def after_destroy(record)
ref_object_touch(record)
end
def ref_object_touch(record)
# return if we run import mode
return true if Setting.get('import_mode')
# touch old customer if changed
cutomer_id_changed = record.saved_changes['customer_id']
if cutomer_id_changed && cutomer_id_changed[0] != cutomer_id_changed[1] && cutomer_id_changed[0]
User.find(cutomer_id_changed[0]).touch # rubocop:disable Rails/SkipsModelValidations
end
# touch new/current customer
record.customer&.touch # rubocop:disable Rails/SkipsModelValidations
# touch old organization if changed
organization_id_changed = record.saved_changes['organization_id']
if organization_id_changed && organization_id_changed[0] != organization_id_changed[1] && organization_id_changed[0]
Organization.find(organization_id_changed[0]).touch # rubocop:disable Rails/SkipsModelValidations
end
# touch new/current organization
return true if !record.organization
record.organization.touch # rubocop:disable Rails/SkipsModelValidations
end
end

View file

@ -1,26 +0,0 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
require_dependency 'stats/ticket_reopen'
class Observer::Ticket::StatsReopen < ActiveRecord::Observer
observe 'ticket'
def after_create(record)
_check(record)
end
def after_update(record)
_check(record)
end
private
def _check(record)
# return if we run import mode
return if Setting.get('import_mode')
Stats::TicketReopen.log('Ticket', record.id, record.saved_changes, record.updated_by_id)
end
end

View file

@ -1,28 +0,0 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
class Observer::Ticket::UserTicketCounter < ActiveRecord::Observer
observe 'ticket'
def after_commit(record)
user_ticket_counter_update(record)
end
def user_ticket_counter_update(record)
# return if we run import mode
return true if Setting.get('import_mode')
return true if BulkImportInfo.enabled?
return true if record.destroyed?
return true if !record.customer_id
# send background job
TicketUserTicketCounterJob.perform_later(
record.customer_id,
UserInfo.current_user_id || record.updated_by_id,
)
end
end

View file

@ -1,41 +0,0 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
class Observer::User::RefObjectTouch < ActiveRecord::Observer
observe 'user'
def after_create(record)
ref_object_touch(record)
end
def after_update(record)
ref_object_touch(record)
end
def after_destroy(record)
ref_object_touch(record)
end
def ref_object_touch(record)
# return if we run import mode
return true if Setting.get('import_mode')
organization_id_changed = record.saved_changes['organization_id']
return true if !organization_id_changed
return true if organization_id_changed[0] == organization_id_changed[1]
# touch old organization
if organization_id_changed[0]
organization = Organization.find(organization_id_changed[0])
organization.touch # rubocop:disable Rails/SkipsModelValidations
end
# touch new/current organization
if record&.organization
record.organization.touch # rubocop:disable Rails/SkipsModelValidations
end
true
end
end

View file

@ -1,30 +0,0 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
class Observer::User::TicketOrganization < ActiveRecord::Observer
observe 'user'
def after_create(record)
check_organization(record)
end
def after_update(record)
check_organization(record)
end
# check if organization need to be updated
def check_organization(record)
# check if organization has changed
return true if !record.saved_change_to_attribute?('organization_id')
# update last 100 tickets of user
tickets = Ticket.where(customer_id: record.id).limit(100)
tickets.each do |ticket|
if ticket.organization_id != record.organization_id
ticket.organization_id = record.organization_id
ticket.save
end
end
end
end

View file

@ -1,6 +1,7 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
class Tag < ApplicationModel
include Tag::WritesToTicketHistory
belongs_to :tag_object, class_name: 'Tag::Object', optional: true
belongs_to :tag_item, class_name: 'Tag::Item', optional: true

View file

@ -15,6 +15,12 @@ class Ticket < ApplicationModel
include HasLinks
include HasObjectManagerAttributesValidation
include HasTaskbars
include Ticket::CallsStatsTicketReopenLog
include Ticket::EnqueuesUserTicketCounterJob
include Ticket::ResetsPendingTimeSeconds
include Ticket::SetsCloseTime
include Ticket::SetsOnlineNotificationSeen
include Ticket::TouchesAssociations
include ::Ticket::Escalation
include ::Ticket::Subject
@ -27,6 +33,9 @@ class Ticket < ApplicationModel
before_create :check_generate, :check_defaults, :check_title, :set_default_state, :set_default_priority
before_update :check_defaults, :check_title, :reset_pending_time, :check_owner_active
# This must be loaded late as it depends on the internal before_create and before_update handlers of ticket.rb.
include Ticket::SetsLastOwnerUpdateTime
validates :group_id, presence: true
activity_stream_permission 'ticket.agent'

View file

@ -10,7 +10,18 @@ class Ticket::Article < ApplicationModel
include HasObjectManagerAttributesValidation
include Ticket::Article::Assets
include Ticket::Article::EnqueueCommunicateEmailJob
include Ticket::Article::EnqueueCommunicateFacebookJob
include Ticket::Article::EnqueueCommunicateSmsJob
include Ticket::Article::EnqueueCommunicateTelegramJob
include Ticket::Article::EnqueueCommunicateTwitterJob
include Ticket::Article::HasTicketContactAttributesImpact
include Ticket::Article::ResetsTicketState
# AddsMetadataGeneral depends on AddsMetadataOriginById, so load that first
include Ticket::Article::AddsMetadataOriginById
include Ticket::Article::AddsMetadataGeneral
include Ticket::Article::AddsMetadataEmail
belongs_to :ticket, optional: true
has_one :ticket_time_accounting, class_name: 'Ticket::TimeAccounting', foreign_key: :ticket_article_id, dependent: :destroy, inverse_of: :ticket_article

View file

@ -19,6 +19,9 @@ class User < ApplicationModel
include User::Assets
include User::Search
include User::SearchIndex
include User::TouchesOrganization
include User::PerformsGeoLookup
include User::UpdatesTicketOrganization
has_and_belongs_to_many :organizations, after_add: :cache_update, after_remove: :cache_update, class_name: 'Organization'
has_and_belongs_to_many :overviews, dependent: :nullify

View file

@ -27,26 +27,6 @@ module Zammad
# config.active_record.observers = :cacher, :garbage_collector, :forum_observer
config.active_record.observers =
'observer::_session',
'observer::_ticket::_close_time',
'observer::_ticket::_last_owner_update',
'observer::_ticket::_pending_time',
'observer::_ticket::_user_ticket_counter',
'observer::_ticket::_article::_fillup_from_origin_by_id',
'observer::_ticket::_article::_fillup_from_general',
'observer::_ticket::_article::_fillup_from_email',
'observer::_ticket::_article::_communicate_email',
'observer::_ticket::_article::_communicate_facebook',
'observer::_ticket::_article::_communicate_sms',
'observer::_ticket::_article::_communicate_twitter',
'observer::_ticket::_article::_communicate_telegram',
'observer::_ticket::_reset_new_state',
'observer::_ticket::_ref_object_touch',
'observer::_ticket::_online_notification_seen',
'observer::_ticket::_stats_reopen',
'observer::_tag::_ticket_history',
'observer::_user::_ref_object_touch',
'observer::_user::_ticket_organization',
'observer::_user::_geo',
'observer::_transaction'
config.active_job.queue_adapter = :delayed_job

View file

@ -5,13 +5,6 @@ RSpec.describe CommunicateTwitterJob, type: :job do
let(:article) { create(:twitter_article, **(try(:factory_options) || {})) }
describe 'core behavior', :use_vcr do
# This job runs automatically whenever an article is created.
# We disable this auto-execution so we can invoke it manually in the tests below.
around do |example|
ActiveRecord::Base.observers.disable('observer::_ticket::_article::_communicate_twitter')
example.run
ActiveRecord::Base.observers.enable('observer::_ticket::_article::_communicate_twitter')
end
context 'for tweets' do
let(:tweet_attributes) do

View file

@ -0,0 +1,33 @@
RSpec.shared_examples 'TagWritesToTicketHistory' do
subject { create(described_class.name.underscore) }
# The concern is for the tag model, but the shared example needs to be loaded in the ticket test.
it 'can only be loaded for tickets' do
expect(described_class).to eq Ticket
end
it 'creates a ticket history entry for tag_add' do # rubocop:disable RSpec/ExampleLength
subject.tag_add('foo', 1)
expect(subject.history_get.last).to include(
'object' => described_class.name,
'o_id' => subject.id,
'type' => 'added',
'attribute' => 'tag',
'value_to' => 'foo',
'value_from' => nil
)
end
it 'creates a ticket history entry for tag_remove' do # rubocop:disable RSpec/ExampleLength
subject.tag_add('foo', 1)
subject.tag_remove('foo', 1)
expect(subject.history_get.last).to include(
'object' => described_class.name,
'o_id' => subject.id,
'type' => 'removed',
'attribute' => 'tag',
'value_to' => 'foo',
'value_from' => nil
)
end
end

View file

@ -1,6 +1,6 @@
require 'rails_helper'
RSpec.describe Observer::Ticket::Article::FillupFromGeneral, current_user_id: -> { agent.id } do
RSpec.describe Ticket::Article::AddsMetadataGeneral, current_user_id: -> { agent.id } do
let(:agent) { create(:agent) }
context 'when customer is agent' do

View file

@ -0,0 +1,41 @@
require 'rails_helper'
RSpec.describe Ticket::Article::EnqueueCommunicateEmailJob, performs_jobs: true do
before { allow(Delayed::Job).to receive(:enqueue).and_call_original }
let(:article) { create(:ticket_article, **(try(:factory_options) || {})) }
shared_examples 'for no-op' do
it 'is a no-op' do
expect { article }.not_to have_enqueued_job(TicketArticleCommunicateEmailJob)
end
end
shared_examples 'for success' do
it 'enqueues the Email background job' do
expect { article }.to have_enqueued_job(TicketArticleCommunicateEmailJob)
end
end
context 'when in Import Mode' do
before { Setting.set('import_mode', true) }
include_examples 'for no-op'
end
context 'when article is created during Channel::EmailParser#process', application_handle: 'scheduler.postmaster' do
include_examples 'for no-op'
end
context 'when article is from a customer' do
let(:factory_options) { { sender_name: 'Customer' } }
include_examples 'for no-op'
end
context 'when article is an email' do
let(:factory_options) { { sender_name: 'Agent', type_name: 'email' } }
include_examples 'for success'
end
end

View file

@ -0,0 +1,41 @@
require 'rails_helper'
RSpec.describe Ticket::Article::EnqueueCommunicateFacebookJob, performs_jobs: true do
before { allow(Delayed::Job).to receive(:enqueue).and_call_original }
let(:article) { create(:ticket_article, **(try(:factory_options) || {})) }
shared_examples 'for no-op' do
it 'is a no-op' do
expect { article }.not_to have_enqueued_job(CommunicateFacebookJob)
end
end
shared_examples 'for success' do
it 'enqueues the Facebook background job' do
expect { article }.to have_enqueued_job(CommunicateFacebookJob)
end
end
context 'when in Import Mode' do
before { Setting.set('import_mode', true) }
include_examples 'for no-op'
end
context 'when article is created during Channel::EmailParser#process', application_handle: 'scheduler.postmaster' do
include_examples 'for no-op'
end
context 'when article is from a customer' do
let(:factory_options) { { sender_name: 'Customer' } }
include_examples 'for no-op'
end
context 'when article is a facebook post' do
let(:factory_options) { { sender_name: 'Agent', type_name: 'facebook feed post' } }
include_examples 'for success'
end
end

View file

@ -0,0 +1,41 @@
require 'rails_helper'
RSpec.describe Ticket::Article::EnqueueCommunicateSmsJob, performs_jobs: true do
before { allow(Delayed::Job).to receive(:enqueue).and_call_original }
let(:article) { create(:ticket_article, **(try(:factory_options) || {})) }
shared_examples 'for no-op' do
it 'is a no-op' do
expect { article }.not_to have_enqueued_job(CommunicateSmsJob)
end
end
shared_examples 'for success' do
it 'enqueues the SMS background job' do
expect { article }.to have_enqueued_job(CommunicateSmsJob)
end
end
context 'when in Import Mode' do
before { Setting.set('import_mode', true) }
include_examples 'for no-op'
end
context 'when article is created during Channel::EmailParser#process', application_handle: 'scheduler.postmaster' do
include_examples 'for no-op'
end
context 'when article is from a customer' do
let(:factory_options) { { sender_name: 'Customer' } }
include_examples 'for no-op'
end
context 'when article is an sms' do
let(:factory_options) { { sender_name: 'Agent', type_name: 'sms' } }
include_examples 'for success'
end
end

View file

@ -0,0 +1,41 @@
require 'rails_helper'
RSpec.describe 'Ticket::Article::EnqueueCommunicateTelegramJob', performs_jobs: true do
before { allow(Delayed::Job).to receive(:enqueue).and_call_original }
let(:article) { create(:ticket_article, **(try(:factory_options) || {})) }
shared_examples 'for no-op' do
it 'is a no-op' do
expect { article }.not_to have_enqueued_job(CommunicateTelegramJob)
end
end
shared_examples 'for success' do
it 'enqueues the Telegram background job' do
expect { article }.to have_enqueued_job(CommunicateTelegramJob)
end
end
context 'when in Import Mode' do
before { Setting.set('import_mode', true) }
include_examples 'for no-op'
end
context 'when article is created during Channel::EmailParser#process', application_handle: 'scheduler.postmaster' do
include_examples 'for no-op'
end
context 'when article is from a customer' do
let(:factory_options) { { sender_name: 'Customer' } }
include_examples 'for no-op'
end
context 'when article is a Telegram message' do
let(:factory_options) { { sender_name: 'Agent', type_name: 'telegram personal-message' } }
include_examples 'for success'
end
end

View file

@ -1,6 +1,6 @@
require 'rails_helper'
RSpec.describe Observer::Ticket::Article::CommunicateTwitter, performs_jobs: true do
RSpec.describe Ticket::Article::EnqueueCommunicateTwitterJob, performs_jobs: true do
before { allow(Delayed::Job).to receive(:enqueue).and_call_original }
let(:article) { create(:ticket_article, **(try(:factory_options) || {})) }
@ -17,7 +17,7 @@ RSpec.describe Observer::Ticket::Article::CommunicateTwitter, performs_jobs: tru
end
end
context 'in Import Mode' do
context 'when in Import Mode' do
before { Setting.set('import_mode', true) }
include_examples 'for no-op'
@ -50,7 +50,7 @@ RSpec.describe Observer::Ticket::Article::CommunicateTwitter, performs_jobs: tru
include_examples 'for success'
context 'but #to attribute is missing' do
context 'when #to attribute is missing' do
let(:factory_options) { { sender_name: 'Agent', type_name: 'twitter direct-message', to: nil } }
it 'raises an error' do

View file

@ -0,0 +1,12 @@
RSpec.shared_examples 'TicketCallsStatsTicketReopenLog' do
it 'can only be loaded for Ticket' do
expect(described_class).to eq Ticket
end
it 'calls Stats::TicketReopen.log' do
allow(Stats::TicketReopen).to receive(:log)
create(described_class.name.underscore)
expect(Stats::TicketReopen).to have_received(:log)
end
end

View file

@ -0,0 +1,10 @@
RSpec.shared_examples 'TicketEnqueuesTicketUserTicketCounterJob', type: :job do
subject { create(described_class.name.underscore) }
let(:customer) { create('customer') }
it 'enqueues a job for the customer' do
subject.customer = customer
expect { subject.save }.to have_enqueued_job(TicketUserTicketCounterJob)
end
end

View file

@ -0,0 +1,12 @@
RSpec.shared_examples 'TicketResetsPendingTimeSeconds' do
subject { create(described_class.name.underscore) }
it 'can only be loaded for tickets' do
expect(described_class).to eq Ticket
end
it 'resets pending_time seconds' do
subject.update(pending_time: Time.zone.parse('2007-02-10 15:30:45'))
expect(subject.pending_time).to eq(Time.zone.parse('2007-02-10 15:30:00'))
end
end

View file

@ -0,0 +1,18 @@
require 'rails_helper'
RSpec.shared_examples 'TicketSetsCloseTime' do
subject { create(described_class.name.underscore) }
it 'can only be loaded for tickets' do
expect(described_class).to eq Ticket
end
before do
travel_to Time.zone.now
end
it 'resets pending_time seconds' do
subject.update(state: Ticket::State.lookup(name: 'closed'))
expect(subject.close_at).to eq(Time.zone.now)
end
end

View file

@ -0,0 +1,24 @@
require 'rails_helper'
RSpec.shared_examples 'TicketSetsLastOwnerUpdateTime' do
subject { create(described_class.name.underscore) }
let(:new_owner) { create(:agent, groups: [subject.group]) }
it 'can only be loaded for tickets' do
expect(described_class).to eq Ticket
end
before do
travel_to Time.zone.now
end
it 'has no last_owner_update_at initially' do
expect(subject.last_owner_update_at).to be_nil
end
it 'gets last_owner_update_at after user change' do
subject.update(owner: new_owner)
expect(subject.last_owner_update_at).to eq(Time.zone.now)
end
end

View file

@ -0,0 +1,16 @@
RSpec.shared_examples 'UserPerformsGeoLookup' do
it 'can only be loaded for User' do
expect(described_class).to eq User
end
it 'performs geo lookup' do
# Mock the geo lookup as it requires an API key.
allow(Service::GeoLocation).to receive(:geocode).with('Marienstraße 18, 10117, Berlin, Germany').and_return([10.0, 20.0])
user = create(described_class.name.underscore, street: 'Marienstraße 18', zip: '10117', city: 'Berlin', country: 'Germany')
expect(user.preferences).to include(lat: 10.0, lng: 20.0)
end
end

View file

@ -4,9 +4,15 @@ require 'models/concerns/can_be_imported_examples'
require 'models/concerns/can_csv_import_examples'
require 'models/concerns/has_history_examples'
require 'models/concerns/has_tags_examples'
require 'models/concerns/tag/writes_to_ticket_history_examples'
require 'models/concerns/has_taskbars_examples'
require 'models/concerns/has_xss_sanitized_note_examples'
require 'models/concerns/has_object_manager_attributes_validation_examples'
require 'models/concerns/ticket/calls_stats_ticket_reopen_log_examples'
require 'models/concerns/ticket/enqueues_user_ticket_counter_job_examples'
require 'models/concerns/ticket/resets_pending_time_seconds_examples'
require 'models/concerns/ticket/sets_close_time_examples'
require 'models/concerns/ticket/sets_last_owner_update_time_examples'
require 'models/ticket/escalation_examples'
RSpec.describe Ticket, type: :model do
@ -17,10 +23,16 @@ RSpec.describe Ticket, type: :model do
it_behaves_like 'CanCsvImport'
it_behaves_like 'HasHistory', history_relation_object: 'Ticket::Article'
it_behaves_like 'HasTags'
it_behaves_like 'TagWritesToTicketHistory'
it_behaves_like 'HasTaskbars'
it_behaves_like 'HasXssSanitizedNote', model_factory: :ticket
it_behaves_like 'HasObjectManagerAttributesValidation'
it_behaves_like 'Ticket::Escalation'
it_behaves_like 'TicketCallsStatsTicketReopenLog'
it_behaves_like 'TicketEnqueuesTicketUserTicketCounterJob'
it_behaves_like 'TicketResetsPendingTimeSeconds'
it_behaves_like 'TicketSetsCloseTime'
it_behaves_like 'TicketSetsLastOwnerUpdateTime'
describe 'Class methods:' do
describe '.selectors' do

View file

@ -7,6 +7,7 @@ require 'models/concerns/has_groups_permissions_examples'
require 'models/concerns/has_xss_sanitized_note_examples'
require 'models/concerns/can_be_imported_examples'
require 'models/concerns/has_object_manager_attributes_validation_examples'
require 'models/concerns/user/performs_geo_lookup_examples'
require 'models/user/has_ticket_create_screen_impact_examples'
require 'models/user/can_lookup_search_index_attributes_examples'
require 'models/concerns/has_taskbars_examples'
@ -29,6 +30,7 @@ RSpec.describe User, type: :model do
it_behaves_like 'User::HasTicketCreateScreenImpact'
it_behaves_like 'CanLookupSearchIndexAttributes'
it_behaves_like 'HasTaskbars'
it_behaves_like 'UserPerformsGeoLookup'
describe 'Class methods:' do
describe '.authenticate' do