Initial Redmine tooling and local plugin forks
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
class CannedResponse < ActiveRecord::Base
|
||||
unloadable
|
||||
attr_accessible :name, :content, :is_public
|
||||
|
||||
belongs_to :project
|
||||
belongs_to :user
|
||||
|
||||
validates_presence_of :name, :content
|
||||
validates_length_of :name, :maximum => 255
|
||||
|
||||
scope :visible, lambda {|*args|
|
||||
user = args.shift || User.current
|
||||
base = Project.allowed_to_condition(user, :view_helpdesk_tickets, *args)
|
||||
user_id = user.logged? ? user.id : 0
|
||||
|
||||
eager_load(:project).where("(#{CannedResponse.table_name}.project_id IS NULL OR (#{base})) AND (#{CannedResponse.table_name}.is_public = ? OR #{CannedResponse.table_name}.user_id = ?)", true, user_id)
|
||||
}
|
||||
|
||||
scope :in_project_or_public, lambda {|project|
|
||||
where("(#{CannedResponse.table_name}.project_id IS NULL) OR #{CannedResponse.table_name}.project_id = ?", project)
|
||||
}
|
||||
|
||||
# Returns true if the query is visible to +user+ or the current user.
|
||||
def visible?(user=User.current)
|
||||
(project.nil? || user.allowed_to?(:view_helpdesk_tickets, project)) && (self.is_public? || self.user_id == user.id)
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,148 @@
|
||||
class HelpdeskDataCollectorBusiestTime
|
||||
MAX_WEIGHT = 200
|
||||
|
||||
RESPONSE_INTERVALS = {'6_8h' => [6, 8],
|
||||
'8_10h' => [8, 10],
|
||||
'10_12h' => [10, 12],
|
||||
'12_14h' => [12, 14],
|
||||
'14_16h' => [14, 16],
|
||||
'16_18h' => [16, 18],
|
||||
'18_20h' => [18, 20],
|
||||
'20_22h' => [20, 22],
|
||||
'22_0h' => [22, 0],
|
||||
'0_2h' => [0, 2],
|
||||
'2_4h' => [2, 4],
|
||||
'4_6h' => [4, 6]}
|
||||
|
||||
def columns
|
||||
@columns ||= collect_columns
|
||||
end
|
||||
|
||||
def issue_weight
|
||||
@issue_weight ||= (MAX_WEIGHT.to_f / columns.map { |column| column[:issues_count] }.sort.last).ceil
|
||||
end
|
||||
|
||||
# New tickets
|
||||
def new_issues_count
|
||||
return @new_issues_count if @new_issues_count
|
||||
condition = @query.send('sql_for_field', nil, @query.filters['message_date'][:operator], nil, 'issues', 'created_on')
|
||||
@new_issues_count ||= @issues.where(condition).count
|
||||
end
|
||||
|
||||
def previous_new_issues_count
|
||||
return @previous_new_issues_count if @previous_new_issues_count
|
||||
condition = previous_query.send('sql_for_field', nil, previous_query.filters['message_date'][:operator], nil, 'issues', 'created_on')
|
||||
@previous_new_issues_count ||= @previous_issues.where(condition).count
|
||||
end
|
||||
|
||||
def new_issue_count_progress
|
||||
return 0 if previous_new_issues_count.zero?
|
||||
calculate_progress(previous_new_issues_count, new_issues_count)
|
||||
end
|
||||
|
||||
# New contacts
|
||||
def contacts_count
|
||||
contacts.count
|
||||
end
|
||||
|
||||
def previous_contacts_count
|
||||
previous_contacts.count
|
||||
end
|
||||
|
||||
def total_contacts_count_progress
|
||||
return 0 if previous_contacts_count.zero?
|
||||
calculate_progress(previous_contacts_count, contacts_count)
|
||||
end
|
||||
|
||||
# Total incoming
|
||||
def issues_count
|
||||
@issues_count ||= @issues.count + @journal_messages.count
|
||||
end
|
||||
|
||||
def previous_issues_count
|
||||
@previous_issues_count ||= @previous_issues.count + @previous_journal_messages.count
|
||||
end
|
||||
|
||||
def issue_count_progress
|
||||
return 0 if previous_issues_count.zero?
|
||||
calculate_progress(previous_issues_count, issues_count)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def initialize(query)
|
||||
@query = query
|
||||
@issues = with_created_issues(@query)
|
||||
@journal_messages = JournalMessage.where(:is_incoming => true).
|
||||
where(@query.send('sql_for_field', nil, @query.filters['message_date'][:operator], nil, 'journal_messages', 'message_date'))
|
||||
@previous_issues = with_created_issues(previous_query)
|
||||
@previous_journal_messages = JournalMessage.where(:is_incoming => true).
|
||||
where(previous_query.send('sql_for_field', nil, @query.filters['message_date'][:operator], nil, 'journal_messages', 'message_date'))
|
||||
end
|
||||
|
||||
def collect_columns
|
||||
columns = []
|
||||
RESPONSE_INTERVALS.each do |interval_name, interval_hours|
|
||||
interval_objects_count = find_incoming_objects_count(interval_hours)
|
||||
columns << { :name => interval_name, :issues_count => interval_objects_count,
|
||||
:issues_percent => ((interval_objects_count.to_f / issues_count.to_f) * 100).round(2) }
|
||||
end
|
||||
columns
|
||||
end
|
||||
|
||||
def find_incoming_objects_count(interval)
|
||||
interval_start = interval.first
|
||||
interval_end = interval.last - 1 < 0 ? 23 : interval.last - 1
|
||||
interval_issues = @issues.each.select do |issue|
|
||||
issue_time = timezone ? issue.created_on.in_time_zone(timezone) : issue.created_on.localtime
|
||||
interval_start <= issue_time.hour && issue_time.hour <= interval_end
|
||||
end
|
||||
interval_messages = @journal_messages.each.select do |message|
|
||||
message_time = timezone ? message.message_date.in_time_zone(timezone) : message.message_date.localtime
|
||||
interval_start <= message_time.hour && message_time.hour <= interval_end
|
||||
end
|
||||
interval_issues.count + interval_messages.count
|
||||
end
|
||||
|
||||
def timezone
|
||||
@timezone ||= User.current.time_zone
|
||||
end
|
||||
|
||||
def previous_query
|
||||
return if @query[:filters].nil? || @query[:filters]['message_date'].nil? || @query[:filters]['message_date'][:operator].nil?
|
||||
return @previous_query if @previous_query
|
||||
|
||||
previous_operator = ['pre_', @query[:filters]['message_date'][:operator]].join
|
||||
previous_filters = @query[:filters].merge('message_date' => { :operator => previous_operator, :values => [Date.today.to_s] })
|
||||
@previous_query = HelpdeskReportsBusiestTimeQuery.new(:name => '_', :project => @query.project, :filters => previous_filters)
|
||||
@previous_query
|
||||
end
|
||||
|
||||
def with_created_issues(query)
|
||||
condition = query.send('sql_for_field', nil, query.filters['message_date'][:operator], nil, 'issues', 'created_on')
|
||||
created_ids = Issue.joins(:project).visible.where(:project_id => query.project).where(condition).pluck(:id)
|
||||
Issue.where(:id => query.issues.pluck(:id) | created_ids).joins(:helpdesk_ticket)
|
||||
end
|
||||
|
||||
def contacts
|
||||
return @contacts if @contacts
|
||||
condition = @query.send('sql_for_field', nil, @query.filters['message_date'][:operator], nil, 'contacts', 'created_on')
|
||||
@contacts = Contact.where(:id => @issues.joins(:customer).map(&:customer).map(&:id).uniq).where(condition)
|
||||
end
|
||||
|
||||
def previous_contacts
|
||||
return @previous_contacts if @previous_contacts
|
||||
condition = @query.send('sql_for_field', nil, @previous_query.filters['message_date'][:operator], nil, 'contacts', 'created_on')
|
||||
@previous_contacts = Contact.where(:id => @previous_issues.joins(:customer).map(&:customer).map(&:id).uniq).where(condition)
|
||||
end
|
||||
|
||||
def calculate_progress(before, now)
|
||||
progress =
|
||||
if before.to_f > now.to_f
|
||||
100 - (now.to_f * 100 / before.to_f)
|
||||
else
|
||||
(100 - (before.to_f * 100 / now.to_f)) * -1
|
||||
end
|
||||
progress.round
|
||||
end
|
||||
end
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
class HelpdeskDataCollectorFirstResponse
|
||||
MAX_WEIGHT = 200
|
||||
|
||||
RESPONSE_INTERVALS = { '0_1h' => [0, 1],
|
||||
'1_2h' => [1, 2],
|
||||
'2_4h' => [2, 4],
|
||||
'4_8h' => [4, 8],
|
||||
'8_12h' => [8, 12],
|
||||
'12_24h' => [12, 24],
|
||||
'24_48h' => [24, 48],
|
||||
'48_0h' => [48, 0] }
|
||||
attr_reader :issues
|
||||
attr_reader :previous_issues
|
||||
|
||||
def columns
|
||||
@columns ||= collect_columns
|
||||
end
|
||||
|
||||
def issue_weight
|
||||
@issue_weight ||= (MAX_WEIGHT.to_f / columns.map { |column| column[:issues_count] }.sort.last).ceil
|
||||
end
|
||||
|
||||
# First response time
|
||||
def average_response_time
|
||||
@average_response_time ||= median(HelpdeskTicket.where(:issue_id => issues.pluck(:id)).pluck(:first_response_time))
|
||||
end
|
||||
|
||||
def previous_average_response_time
|
||||
return 0 if previous_issues_count.zero?
|
||||
@previous_average_response_time ||= median(HelpdeskTicket.where(:issue_id => previous_issues.pluck(:id)).pluck(:first_response_time))
|
||||
end
|
||||
|
||||
def average_response_time_progress
|
||||
return 0 if previous_issues_count.zero?
|
||||
calculate_progress(previous_average_response_time, average_response_time)
|
||||
end
|
||||
|
||||
# Time to close
|
||||
def average_close_time
|
||||
return @average_close_time if @average_close_time
|
||||
closed_issue_ids = issues.joins(:status).where("#{IssueStatus.table_name}.is_closed = ?", true).pluck(:id)
|
||||
return 0 if closed_issue_ids.empty?
|
||||
@average_close_time = median(HelpdeskTicket.where(:issue_id => closed_issue_ids).pluck(:resolve_time))
|
||||
end
|
||||
|
||||
def previous_average_close_time
|
||||
return @previous_average_close_time if @previous_average_close_time.present?
|
||||
closed_issue_ids = previous_issues.joins(:status).where("#{IssueStatus.table_name}.is_closed = ?", true).pluck(:id)
|
||||
return 0 if closed_issue_ids.empty?
|
||||
@previous_average_close_time ||= median(HelpdeskTicket.where(:issue_id => closed_issue_ids).pluck(:resolve_time))
|
||||
end
|
||||
|
||||
def average_close_time_progress
|
||||
closed_issue_ids = previous_issues.joins(:status).where("#{IssueStatus.table_name}.is_closed = ?", true).pluck(:id)
|
||||
return 0 if closed_issue_ids.empty?
|
||||
calculate_progress(previous_average_close_time, average_close_time)
|
||||
end
|
||||
|
||||
# Average responses count
|
||||
def average_response_count
|
||||
return @average_response_count if @average_response_count
|
||||
closed_issue_ids = issues.joins(:status).where("#{IssueStatus.table_name}.is_closed = ?", true).pluck(:id)
|
||||
return @average_response_count = 0 if closed_issue_ids.empty?
|
||||
journal_ids = JournalMessage.joins(:journal).where("journals.journalized_type = 'Issue'").
|
||||
where(journal_message_date_condition(@query)).
|
||||
where("journals.journalized_id IN (#{closed_issue_ids.join(',')})").pluck(:journal_id)
|
||||
@average_response_count = median(Journal.where(:id => journal_ids).group(:journalized_id).count(:id).values)
|
||||
end
|
||||
|
||||
def previous_average_response_count
|
||||
return 0 if previous_issues_count.zero?
|
||||
return @previous_average_response_count if @previous_average_response_count
|
||||
closed_issue_ids = previous_issues.joins(:status).where("#{IssueStatus.table_name}.is_closed = ?", true).pluck(:id)
|
||||
return @previous_average_response_count = 0 if closed_issue_ids.empty?
|
||||
journal_ids = JournalMessage.joins(:journal).where("journals.journalized_type = 'Issue'").
|
||||
where(journal_message_date_condition(previous_query)).
|
||||
where("journals.journalized_id IN (#{closed_issue_ids.join(',')})").pluck(:journal_id)
|
||||
|
||||
@previous_average_response_count = median(Journal.where(:id => journal_ids).group(:journalized_id).count(:id).values)
|
||||
end
|
||||
|
||||
def average_response_count_progress
|
||||
return 0 if previous_average_response_count.zero?
|
||||
calculate_progress(previous_average_response_count, average_response_count)
|
||||
end
|
||||
|
||||
# Total replies
|
||||
def total_response_count
|
||||
return @total_response_count if @total_response_count
|
||||
@total_response_count = JournalMessage.joins(:journal).where("journals.journalized_type = 'Issue'").
|
||||
where(journal_message_date_condition(@query)).
|
||||
where("journals.journalized_id IN (#{issues.pluck(:id).join(',')})").
|
||||
count
|
||||
end
|
||||
|
||||
def previous_total_response_count
|
||||
return 0 if previous_issues_count.zero?
|
||||
return @previous_total_response_count if @previous_total_response_count
|
||||
@previous_total_response_count ||= JournalMessage.joins(:journal).where("journals.journalized_type = 'Issue'").
|
||||
where(journal_message_date_condition(previous_query)).
|
||||
where("journals.journalized_id IN (#{previous_issues.pluck(:id).join(',')})").
|
||||
count
|
||||
end
|
||||
|
||||
def total_response_count_progress
|
||||
return 0 if previous_total_response_count.zero?
|
||||
calculate_progress(previous_total_response_count, total_response_count)
|
||||
end
|
||||
|
||||
def issues_count
|
||||
@issues_count ||= issues.count
|
||||
end
|
||||
|
||||
def previous_issues_count
|
||||
@previous_issues_count ||= previous_issues.count
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def initialize(query)
|
||||
@query = query
|
||||
@issues = @query.issues.joins(:helpdesk_ticket).where('helpdesk_tickets.first_response_time > 0').uniq
|
||||
@previous_issues = previous_query.issues.joins(:helpdesk_ticket).where('helpdesk_tickets.first_response_time > 0').uniq
|
||||
end
|
||||
|
||||
def collect_columns
|
||||
columns = []
|
||||
RESPONSE_INTERVALS.each do |interval_name, interval_hours|
|
||||
interval_issues_count = find_issues_count(interval_hours)
|
||||
columns << { :name => interval_name, :issues_count => interval_issues_count,
|
||||
:issues_percent => ((interval_issues_count.to_f / issues_count.to_f) * 100).round(2) }
|
||||
end
|
||||
columns
|
||||
end
|
||||
|
||||
def find_issues_count(interval)
|
||||
interval_start = (interval.first.hours + 1).to_i
|
||||
interval_end = interval.last.hours.to_i
|
||||
interval_issues =
|
||||
if interval.last > 0
|
||||
issues.joins(:helpdesk_ticket).where('helpdesk_tickets.first_response_time BETWEEN ? AND ?', interval_start, interval_end)
|
||||
else
|
||||
issues.joins(:helpdesk_ticket).where('helpdesk_tickets.first_response_time > ?', interval_start)
|
||||
end
|
||||
interval_issues.count
|
||||
end
|
||||
|
||||
def previous_query
|
||||
return if @query[:filters].nil? || @query[:filters]['message_date'].nil? || @query[:filters]['message_date'][:operator].nil?
|
||||
return @previous_query if @previous_query
|
||||
|
||||
previous_operator = ['pre_', @query[:filters]['message_date'][:operator]].join
|
||||
previous_filters = @query[:filters].merge('message_date' => { :operator => previous_operator, :values => [Date.today.to_s] })
|
||||
@previous_query = HelpdeskReportsFirstResponseQuery.new(:name => '_', :project => @query.project, :filters => previous_filters)
|
||||
@previous_query
|
||||
end
|
||||
|
||||
def journal_message_date_condition(query)
|
||||
query.send('sql_for_field', nil, query.filters['message_date'][:operator], nil, 'journal_messages', 'message_date')
|
||||
end
|
||||
|
||||
def median(array)
|
||||
return 0 if array.compact.empty?
|
||||
range = array.sort.reverse
|
||||
middle = range.count / 2
|
||||
(range.count % 2).zero? ? (range[middle - 1] + range[middle]) / 2 : range[middle]
|
||||
end
|
||||
|
||||
def calculate_progress(before, now)
|
||||
progress =
|
||||
if before.to_f > now.to_f
|
||||
100 - (now.to_f * 100 / before.to_f)
|
||||
else
|
||||
(100 - (before.to_f * 100 / now.to_f)) * -1
|
||||
end
|
||||
progress.round
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
class HelpdeskDataCollectorManager
|
||||
def initialize(report)
|
||||
@report = report
|
||||
end
|
||||
|
||||
def collect_data(query)
|
||||
case @report
|
||||
when 'first_response_time'
|
||||
HelpdeskDataCollectorFirstResponse.new(query)
|
||||
when 'busiest_time_of_day'
|
||||
HelpdeskDataCollectorBusiestTime.new(query)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,823 @@
|
||||
require "digest/md5"
|
||||
|
||||
class HelpdeskMailer < MailHandler
|
||||
include HelpdeskMailerHelper
|
||||
include AbstractController::Callbacks
|
||||
after_filter :set_delivery_options
|
||||
|
||||
attr_reader :contact, :user, :email, :project
|
||||
|
||||
def self.default_url_options
|
||||
{ :host => Setting.host_name, :protocol => Setting.protocol }
|
||||
end
|
||||
|
||||
def self.with_activated_perform_deliveries
|
||||
perform_delivery_state = ActionMailer::Base.perform_deliveries
|
||||
ActionMailer::Base.perform_deliveries = true
|
||||
yield
|
||||
ensure
|
||||
ActionMailer::Base.perform_deliveries = perform_delivery_state
|
||||
end
|
||||
|
||||
def issue_response(contact, journal, options={})
|
||||
@project = journal.issue.project
|
||||
to_address = options[:to_address] || (journal.journal_message && journal.journal_message.to_address) || contact.primary_email
|
||||
cc_address = options[:cc_address] || (journal.journal_message && journal.journal_message.cc_address)
|
||||
bcc_address = options[:bcc_address] || (journal.journal_message && journal.journal_message.bcc_address)
|
||||
from_address = options[:from_address] || (!HelpdeskSettings["helpdesk_answer_from", journal.issue.project].blank? && HelpdeskSettings["helpdesk_answer_from", journal.issue.project] )|| Setting.mail_from
|
||||
in_reply_to = options[:in_reply_to] || ((journal.issue.helpdesk_ticket.blank? || journal.issue.helpdesk_ticket.message_id.blank?) ? '' : "<#{journal.issue.helpdesk_ticket.message_id}>")
|
||||
|
||||
headers['X-Redmine-Ticket-ID'] = journal.issue.id.to_s
|
||||
@email_header = self.class.apply_macro(HelpdeskSettings["helpdesk_emails_header", journal.issue.project], contact, journal.issue, journal.user) unless HelpdeskSettings["helpdesk_emails_header", journal.issue.project].blank?
|
||||
@email_footer = self.class.apply_macro(HelpdeskSettings["helpdesk_emails_footer", journal.issue.project], contact, journal.issue, journal.user) unless HelpdeskSettings["helpdesk_emails_footer", journal.issue.project].blank?
|
||||
@email_body = self.class.apply_macro(journal.notes, contact, journal.issue, journal.user)
|
||||
@email_body = attachment_macro(@email_body, journal.issue)
|
||||
|
||||
raise MissingInformation.new(l(:text_helpdesk_to_address_cant_be_blank)) if to_address.blank?
|
||||
raise MissingInformation.new(l(:text_helpdesk_message_body_cant_be_blank)) if @email_body.blank?
|
||||
raise MissingInformation.new(l(:text_helpdesk_from_address_cant_be_blank)) if from_address.blank?
|
||||
|
||||
subject_macro = self.class.apply_macro(HelpdeskSettings["helpdesk_answer_subject", journal.issue.project], contact, journal.issue)
|
||||
# subject_macro += " - [##{journal.issue.id}]" if !subject_macro.blank? && !subject_macro.include?("##{journal.issue.id}]")
|
||||
@email_stylesheet = HelpdeskSettings[:helpdesk_helpdesk_css, journal.issue.project].to_s.html_safe
|
||||
|
||||
extract_attachments(journal)
|
||||
|
||||
if journal.details.blank? && journal.private_notes? && journal.notes.present?
|
||||
details_journal = Journal.where('id != ?', journal.id).where(:created_on => journal.created_on).first
|
||||
extract_attachments(details_journal) if details_journal
|
||||
end
|
||||
|
||||
mail :from => self.class.apply_from_macro(from_address.to_s, journal.user),
|
||||
:to => to_address.to_s,
|
||||
:cc => cc_address.to_s,
|
||||
:bcc => bcc_address.to_s,
|
||||
:in_reply_to => in_reply_to.to_s,
|
||||
:subject => (subject_macro.blank? ? journal.issue.subject + " [#{journal.issue.tracker} ##{journal.issue.id}]" : subject_macro) do |format|
|
||||
format.text { render 'email_layout' }
|
||||
format.html { render 'email_layout' } unless RedmineHelpdesk.settings["plain_text_mail"]
|
||||
end
|
||||
end
|
||||
|
||||
def extract_attachments(journal)
|
||||
journal.details.where(:property => 'attachment').each do |attachment_journal|
|
||||
if attach = Attachment.where(:id => attachment_journal.prop_key).first
|
||||
attachments[attach.filename] = File.open(attach.diskfile, 'rb') { |io| io.read }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def auto_answer(contact, issue)
|
||||
@project = issue.project
|
||||
|
||||
headers['X-Redmine-Ticket-ID'] = issue.id.to_s
|
||||
headers['X-Auto-Response-Suppress'] = 'oof'
|
||||
|
||||
confirmation_body = self.class.apply_macro(HelpdeskSettings["helpdesk_first_answer_template", issue.project_id], contact, issue)
|
||||
|
||||
@email_stylesheet = HelpdeskSettings[:helpdesk_helpdesk_css, issue.project_id].to_s.html_safe
|
||||
@email_body = confirmation_body
|
||||
from_address = HelpdeskSettings["helpdesk_answer_from", issue.project].blank? ? Setting.mail_from : HelpdeskSettings["helpdesk_answer_from", issue.project]
|
||||
|
||||
mail :from => self.class.apply_from_macro(from_address.to_s, nil),
|
||||
:to => contact.primary_email,
|
||||
:cc => issue.helpdesk_ticket.try(:cc_address),
|
||||
:subject => self.class.apply_macro(HelpdeskSettings["helpdesk_first_answer_subject", issue.project_id], contact, issue) || "Helpdesk auto answer [Case ##{issue.id}]",
|
||||
:in_reply_to => issue.helpdesk_ticket.try(:message_id) do |format|
|
||||
format.text { render 'email_layout'}
|
||||
format.html { render 'email_layout' } unless RedmineHelpdesk.settings["plain_text_mail"]
|
||||
end
|
||||
|
||||
logger.info "##{issue.id}: Sending confirmation to #{contact.primary_email}" if logger
|
||||
end
|
||||
|
||||
def initial_message(contact, issue, params)
|
||||
@project = issue.project
|
||||
|
||||
headers['X-Redmine-Ticket-ID'] = issue.id.to_s
|
||||
@email_header = self.class.apply_macro(HelpdeskSettings["helpdesk_emails_header", issue.project], contact, issue, issue.author) unless HelpdeskSettings["helpdesk_emails_header", issue.project].blank?
|
||||
@email_footer = self.class.apply_macro(HelpdeskSettings["helpdesk_emails_footer", issue.project], contact, issue, issue.author) unless HelpdeskSettings["helpdesk_emails_footer", issue.project].blank?
|
||||
@email_body = self.class.apply_macro(issue.description, contact, issue, issue.author)
|
||||
|
||||
@email_body = attachment_macro(@email_body, issue)
|
||||
|
||||
raise MissingInformation.new("Contact #{contact.name} should have mail") if contact.email.blank?
|
||||
raise MissingInformation.new("Message shouldn't be blank") if @email_body.blank?
|
||||
|
||||
@email_stylesheet = HelpdeskSettings[:helpdesk_helpdesk_css, issue.project].to_s.html_safe
|
||||
|
||||
params[:attachments].each_value do |mail_attachment|
|
||||
if file = mail_attachment['file']
|
||||
file.rewind if file
|
||||
attachments[file.original_filename] = file.read
|
||||
file.rewind if file
|
||||
elsif token = mail_attachment['token']
|
||||
if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
|
||||
attachment_id, attachment_digest = $1, $2
|
||||
if a = Attachment.where(:id => attachment_id, :digest => attachment_digest).first
|
||||
attachments[a.filename] = File.open(a.diskfile, 'rb'){|io| io.read}
|
||||
end
|
||||
end
|
||||
end
|
||||
end unless params[:attachments].blank?
|
||||
|
||||
to_address = (params[:helpdesk] && !params[:helpdesk][:to_address].blank?) ? params[:helpdesk][:to_address] : contact.primary_email
|
||||
from_address = HelpdeskSettings["helpdesk_answer_from", issue.project].blank? ? Setting.mail_from : HelpdeskSettings["helpdesk_answer_from", issue.project]
|
||||
|
||||
logger.error "##{issue.id}: From address couldn't be black" if from_address.blank? && logger
|
||||
|
||||
mail :from => self.class.apply_from_macro(from_address.to_s, nil),
|
||||
:to => to_address,
|
||||
:subject => self.class.apply_macro(HelpdeskSettings["helpdesk_first_answer_subject", issue.project_id], contact, issue) || issue.subject do |format|
|
||||
format.text { render 'email_layout' }
|
||||
format.html { render 'email_layout' } unless RedmineHelpdesk.settings["plain_text_mail"]
|
||||
end
|
||||
end
|
||||
|
||||
# Receive email methods
|
||||
|
||||
def self.receive(raw_email, options={})
|
||||
@@helpdesk_mailer_options = options.dup
|
||||
raw_email.force_encoding('ASCII-8BIT') if raw_email.respond_to?(:force_encoding)
|
||||
email = Mail.new(raw_email)
|
||||
new.receive(email)
|
||||
end
|
||||
|
||||
# Processes incoming emails
|
||||
# Returns the created object (eg. an issue, a message) or false
|
||||
def receive(email)
|
||||
@email = email
|
||||
if !target_project.module_enabled?(:contacts) || !target_project.module_enabled?(:issue_tracking)
|
||||
logger.error "#{email && email.message_id}: Contacts and issues modules should be enable for #{target_project.name} project" if logger
|
||||
return false
|
||||
end
|
||||
|
||||
@@helpdesk_mailer_options = HelpdeskMailer.get_issue_options(@@helpdesk_mailer_options, target_project.id)
|
||||
sender_email = message_sender(email)
|
||||
# Ignore emails received from the application emission address to avoid hell cycles
|
||||
if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
|
||||
logger.info "#{email && email.message_id}: Ignoring email from Redmine emission address [#{sender_email}]" if logger
|
||||
return false
|
||||
end
|
||||
|
||||
return false unless handle_ignored(email)
|
||||
|
||||
if !check_blacklist?(email)
|
||||
logger.info "#{email && email.message_id}: Email #{sender_email} ignored because in blacklist" if logger
|
||||
return false
|
||||
end
|
||||
|
||||
@user = User.find_by_mail(sender_email) || User.anonymous
|
||||
@contact = contact_from_email(email)
|
||||
|
||||
User.current = @user
|
||||
if @contact
|
||||
logger.info "#{email && email.message_id}: [#{@contact.name}] contact created/founded" if logger
|
||||
else
|
||||
logger.error "#{email && email.message_id}: could not create/found contact for [#{sender_email}]" if logger
|
||||
return false
|
||||
end
|
||||
|
||||
dispatch
|
||||
end
|
||||
|
||||
def self.check_project(project_id)
|
||||
msg_count = 0
|
||||
unless Project.find_by_id(project_id).blank? || HelpdeskSettings[:helpdesk_protocol, project_id].blank?
|
||||
|
||||
mail_options, options = self.get_mail_options(project_id)
|
||||
|
||||
case mail_options[:protocol]
|
||||
when "pop3" then
|
||||
msg_count = RedmineContacts::Mailer.check_pop3(self, mail_options, options)
|
||||
when "imap" then
|
||||
msg_count = RedmineContacts::Mailer.check_imap(self, mail_options, options)
|
||||
end
|
||||
end
|
||||
|
||||
if project = Project.find_by_id(project_id)
|
||||
HelpdeskTicket.autoclose(project)
|
||||
end
|
||||
|
||||
msg_count
|
||||
end
|
||||
|
||||
def self.get_mail_options(project_id)
|
||||
case HelpdeskSettings[:helpdesk_protocol, project_id]
|
||||
when "gmail"
|
||||
protocol = "imap"
|
||||
host = "imap.gmail.com"
|
||||
port = "993"
|
||||
ssl = "1"
|
||||
when "yahoo"
|
||||
protocol = "imap"
|
||||
host = "imap.mail.yahoo.com"
|
||||
port = "993"
|
||||
ssl = "1"
|
||||
when "yandex"
|
||||
protocol = "imap"
|
||||
host = "imap.yandex.ru"
|
||||
port = "993"
|
||||
ssl = "1"
|
||||
else
|
||||
protocol = HelpdeskSettings[:helpdesk_protocol, project_id]
|
||||
host = HelpdeskSettings[:helpdesk_host, project_id]
|
||||
port = HelpdeskSettings[:helpdesk_port, project_id]
|
||||
ssl = HelpdeskSettings[:helpdesk_use_ssl, project_id] != "1" ? nil : "1"
|
||||
end
|
||||
|
||||
mail_options = {:protocol => protocol,
|
||||
:host => host,
|
||||
:port => port,
|
||||
:ssl => ssl,
|
||||
:apop => HelpdeskSettings[:helpdesk_apop, project_id],
|
||||
:username => HelpdeskSettings[:helpdesk_username, project_id],
|
||||
:password => HelpdeskSettings[:helpdesk_password, project_id],
|
||||
:folder => HelpdeskSettings[:helpdesk_imap_folder, project_id],
|
||||
:move_on_success => HelpdeskSettings[:helpdesk_move_on_success, project_id],
|
||||
:move_on_failure => HelpdeskSettings[:helpdesk_move_on_failure, project_id],
|
||||
:delete_unprocessed => HelpdeskSettings[:helpdesk_delete_unprocessed, project_id].to_i > 0
|
||||
}
|
||||
options = get_issue_options({}, project_id)
|
||||
[mail_options, options]
|
||||
end
|
||||
|
||||
def self.get_issue_options(options, project_id)
|
||||
options = { :issue => {} } unless options[:issue]
|
||||
options[:issue][:project_id] = project_id
|
||||
options[:issue][:status_id] = HelpdeskSettings[:helpdesk_new_status, project_id] unless options[:issue][:status_id]
|
||||
options[:issue][:assigned_to_id] = HelpdeskSettings["helpdesk_assigned_to", project_id] unless options[:issue][:assigned_to_id]
|
||||
options[:issue][:tracker_id] = HelpdeskSettings["helpdesk_tracker", project_id] unless options[:issue][:tracker_id]
|
||||
options[:issue][:priority_id] = HelpdeskSettings[:helpdesk_issue_priority, project_id] unless options[:issue][:priority_id]
|
||||
options[:issue][:due_date] = HelpdeskSettings[:helpdesk_issue_due_date, project_id] unless options[:issue][:due_date]
|
||||
options[:issue][:reopen_status_id] = HelpdeskSettings["helpdesk_reopen_status", project_id] unless options[:issue][:reopen_status_id]
|
||||
options
|
||||
end
|
||||
|
||||
def attachment_macro(text, issue)
|
||||
text.scan(/\{\{send_file\(([^%\}]+)\)\}\}/).flatten.each do |file_name|
|
||||
attachment = file_name.match(/^(\d)+$/) ? Attachment.where(:id => file_name).first : issue.attachments.where(:filename => file_name).first
|
||||
attachments[attachment.filename] = File.open(attachment.diskfile, 'rb'){|io| io.read} if attachment
|
||||
end
|
||||
text.gsub(/\{\{send_file\(([^%\}]+)\)\}\}/, '')
|
||||
end
|
||||
|
||||
def self.apply_from_macro(text, journal_user = nil)
|
||||
return text unless text =~ /\A\{%.+%\}.*?<.+@.+\..{2,}>\z/
|
||||
text = text[/<.*>/] if journal_user.nil? || journal_user == User.anonymous
|
||||
text = text.gsub(/\{%response.author%\}/, journal_user.name) if text =~ /\{%response.author%\}/
|
||||
text = text.gsub(/\{%response.author.first_name%\}/, journal_user.firstname) if text =~ /\{%response.author.first_name%\}/
|
||||
text
|
||||
end
|
||||
|
||||
def self.apply_macro(text, contact, issue, journal_user=nil)
|
||||
return '' if text.blank?
|
||||
text = text.gsub(/%%NAME%%|\{%contact.first_name%\}/, contact.first_name)
|
||||
text = text.gsub(/%%FULL_NAME%%|\{%contact.name%\}/, contact.name)
|
||||
text = text.gsub(/%%COMPANY%%|\{%contact.company%\}/, contact.company) if contact.company
|
||||
text = text.gsub(/%%LAST_NAME%%|\{%contact.last_name%\}/, contact.last_name.blank? ? "" : contact.last_name)
|
||||
text = text.gsub(/%%MIDDLE_NAME%%|\{%contact.middle_name%\}/, contact.middle_name.blank? ? "" : contact.middle_name)
|
||||
text = text.gsub(/\{%contact.email%\}/, contact.primary_email.to_s)
|
||||
text = text.gsub(/%%DATE%%|\{%date%\}/, ApplicationHelper.format_date(Date.today))
|
||||
text = text.gsub(/%%ASSIGNEE%%|\{%ticket.assigned_to%\}/, issue.assigned_to.blank? ? "" : issue.assigned_to.name)
|
||||
text = text.gsub(/%%ISSUE_ID%%|\{%ticket.id%\}/, issue.id.to_s) if issue.id
|
||||
text = text.gsub(/%%ISSUE_TRACKER%%|\{%ticket.tracker%\}/, issue.tracker.name) if issue.tracker
|
||||
text = text.gsub(/%%QUOTED_ISSUE_DESCRIPTION%%|\{%ticket.quoted_description%\}/, issue.description.gsub(/^/, "> ")) if issue.description
|
||||
text = text.gsub(/%%PROJECT%%|\{%ticket.project%\}/, issue.project.name) if issue.project
|
||||
text = text.gsub(/%%SUBJECT%%|\{%ticket.subject%\}/, issue.subject) if issue.subject
|
||||
text = text.gsub(/%%NOTE_AUTHOR%%|\{%response.author%\}/, journal_user.name) if journal_user
|
||||
text = text.gsub(/%%NOTE_AUTHOR.FIRST_NAME%%|\{%response.author.first_name%\}/, journal_user.firstname) if journal_user
|
||||
text = text.gsub(/%%NOTE_AUTHOR.LAST_NAME%%|\{%response.author.last_name%\}/, journal_user.lastname) if journal_user
|
||||
text = text.gsub(/\{%ticket.status%\}/, issue.status.name) if issue.status
|
||||
text = text.gsub(/\{%ticket.priority%\}/, issue.priority.name) if issue.priority
|
||||
text = text.gsub(/\{%ticket.estimated_hours%\}/, issue.estimated_hours ? issue.estimated_hours.to_s : "")
|
||||
text = text.gsub(/\{%ticket.done_ratio%\}/, issue.done_ratio.to_s) if issue.done_ratio
|
||||
text = text.gsub(/\{%ticket.closed_on%\}/, issue.closed_on ? ApplicationHelper.format_date(issue.closed_on) : "") if issue.respond_to?(:closed_on)
|
||||
text = text.gsub(/\{%ticket.due_date%\}/, issue.due_date ? ApplicationHelper.format_date(issue.due_date) : "")
|
||||
text = text.gsub(/\{%ticket.start_date%\}/, issue.start_date ? ApplicationHelper.format_date(issue.start_date) : "")
|
||||
text = text.gsub(/\{%ticket.public_url%\}/, Setting.protocol + '://' + Setting.host_name + Rails.application.routes.url_helpers.public_ticket_path(issue.helpdesk_ticket.id, issue.helpdesk_ticket.token) ) if text.match(/\{%ticket.public_url%\}/) && issue.helpdesk_ticket
|
||||
if RedmineHelpdesk.vote_allow?
|
||||
text = text.gsub(/\{%ticket.voting%\}/, Setting.protocol + '://' + Setting.host_name + Rails.application.routes.url_helpers.helpdesk_votes_show_path(issue.helpdesk_ticket.id, issue.helpdesk_ticket.token) ) if text.match(/\{%ticket.voting%\}/)
|
||||
text = text.gsub(/\{%ticket.voting.good%\}/, Setting.protocol + '://' + Setting.host_name + Rails.application.routes.url_helpers.helpdesk_votes_fast_vote_path(issue.helpdesk_ticket.id, 2, issue.helpdesk_ticket.token) ) if text.match(/\{%ticket.voting.good%\}/)
|
||||
text = text.gsub(/\{%ticket.voting.okay%\}/, Setting.protocol + '://' + Setting.host_name + Rails.application.routes.url_helpers.helpdesk_votes_fast_vote_path(issue.helpdesk_ticket.id, 1, issue.helpdesk_ticket.token) ) if text.match(/\{%ticket.voting.okay%\}/)
|
||||
text = text.gsub(/\{%ticket.voting.bad%\}/, Setting.protocol + '://' + Setting.host_name + Rails.application.routes.url_helpers.helpdesk_votes_fast_vote_path(issue.helpdesk_ticket.id, 0, issue.helpdesk_ticket.token) ) if text.match(/\{%ticket.voting.bad%\}/)
|
||||
end
|
||||
|
||||
if text =~ /\{%ticket.history%\}/
|
||||
ticket_history = ''
|
||||
issue.journals.eager_load(:journal_message).map(&:journal_message).compact.each do |journal_message|
|
||||
message_author = "*#{l(:label_crm_added_by)} #{journal_message.is_incoming? ? journal_message.from_address : journal_message.journal.user.name}, #{format_time(journal_message.message_date)}*"
|
||||
ticket_history = (message_author + "\n" + journal_message.journal.notes + "\n" + ticket_history).gsub(/^/, "> ")
|
||||
end
|
||||
text = text.gsub(/\{%ticket.history%\}/, ticket_history)
|
||||
end
|
||||
|
||||
issue.custom_field_values.each do |value|
|
||||
text = text.gsub(/%%#{value.custom_field.name}%%/, value.value.to_s)
|
||||
end
|
||||
|
||||
contact.custom_field_values.each do |value|
|
||||
text = text.gsub(/%%#{value.custom_field.name}%%/, value.value.to_s)
|
||||
end if contact.respond_to?("custom_field_values")
|
||||
|
||||
journal_user.custom_field_values.each do |value|
|
||||
text = text.gsub(/\{%response.author.custom_field: #{value.custom_field.name}%\}/, value.value.to_s)
|
||||
end if journal_user
|
||||
|
||||
text
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def dispatch
|
||||
m = email.subject && email.subject.match(ISSUE_REPLY_SUBJECT_RE)
|
||||
journal_message = !email.in_reply_to.blank? && JournalMessage.find_by_message_id(email.in_reply_to)
|
||||
helpdesk_ticket = !email.in_reply_to.blank? && HelpdeskTicket.find_by_message_id(email.in_reply_to)
|
||||
if journal_message && journal_message.journal && journal_message.journal.issue
|
||||
receive_issue_reply(journal_message.journal.issue.id)
|
||||
elsif helpdesk_ticket && helpdesk_ticket.issue
|
||||
receive_issue_reply(helpdesk_ticket.issue.id)
|
||||
elsif m && Issue.exists?(m[1].to_i)
|
||||
receive_issue_reply(m[1].to_i)
|
||||
else
|
||||
dispatch_to_default
|
||||
end
|
||||
rescue MissingInformation => e
|
||||
logger.error "#{email && email.message_id}: missing information from #{user}: #{e.message}" if logger
|
||||
false
|
||||
rescue UnauthorizedAction => e
|
||||
logger.error "#{email && email.message_id}: unauthorized attempt from #{user}" if logger
|
||||
false
|
||||
rescue Exception => e
|
||||
# TODO: send a email to the user
|
||||
logger.error "#{email && email.message_id}: dispatch error #{e.message}" if logger
|
||||
false
|
||||
end
|
||||
|
||||
def dispatch_to_default
|
||||
receive_issue
|
||||
end
|
||||
|
||||
def target_project
|
||||
@target_project ||= Project.find_by_identifier(get_keyword(:project) || get_keyword(:project_id))
|
||||
@target_project ||= Project.find_by_id(get_keyword(:project_id)) if @target_project.nil?
|
||||
raise MissingInformation.new('Unable to determine @target_project project') if @target_project.nil?
|
||||
@target_project
|
||||
end
|
||||
|
||||
def helpdesk_issue_attributes_from_keywords(issue)
|
||||
# assigned_to = ((k = get_keyword(:assigned_to_id, :override => true)) && User.find_by_id(k)) || ((k = get_keyword(:assigned_to, :override => true)) && find_user_from_keyword(k))
|
||||
assigned_to = ((k = get_keyword(:assigned_to_id, :override => true)) && (User.find_by_id(k) || Group.find_by_id(k))) || ((k = get_keyword(:assigned_to, :override => true)) && find_user_from_keyword(k))
|
||||
|
||||
attrs = {
|
||||
'status_id' => ((k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id) ) || ((k = get_keyword(:status_id)) && IssueStatus.find_by_id(k).try(:id)),
|
||||
'priority_id' => ((k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id)) || ((k = get_keyword(:priority_id)) && IssuePriority.find_by_id(k).try(:id)),
|
||||
'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
|
||||
'assigned_to_id' => assigned_to.try(:id),
|
||||
'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) && issue.project.shared_versions.named(k).first.try(:id),
|
||||
'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
|
||||
'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
|
||||
'estimated_hours' => get_keyword(:estimated_hours, :override => true),
|
||||
'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
|
||||
}.delete_if {|k, v| v.blank? }
|
||||
|
||||
attrs
|
||||
end
|
||||
|
||||
def calculated_tracker_id(issue)
|
||||
issue_tracker_id = ((k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id)) ||
|
||||
((k = get_keyword(:tracker_id)) && issue.project.trackers.find_by_id(k).try(:id))
|
||||
issue_tracker_id = issue.project.trackers.first.try(:id) unless issue_tracker_id
|
||||
issue_tracker_id
|
||||
end
|
||||
|
||||
# Creates a new issue
|
||||
def receive_issue
|
||||
project = target_project
|
||||
issue = Issue.new
|
||||
issue.author = user
|
||||
issue.project = project
|
||||
issue.safe_attributes = helpdesk_issue_attributes_from_keywords(issue)
|
||||
issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
|
||||
issue.tracker_id = calculated_tracker_id(issue)
|
||||
issue.subject = cleaned_up_subject(email)
|
||||
issue.subject = '(no subject)' if issue.subject.blank?
|
||||
issue.description = escaped_cleaned_up_text_body
|
||||
issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
|
||||
|
||||
helpdesk_ticket = HelpdeskTicket.new(:from_address => message_sender(email).downcase.to_s.slice(0, 255),
|
||||
:to_address => email.to_addrs.join(',').downcase.to_s.slice(0, 255),
|
||||
:cc_address => email.cc_addrs.join(',').downcase.to_s.slice(0, 255),
|
||||
:ticket_date => email.date || Time.now,
|
||||
:message_id => email.message_id.to_s.slice(0, 255),
|
||||
:is_incoming => true,
|
||||
:customer => contact,
|
||||
:issue => issue,
|
||||
:source => HelpdeskTicket::HELPDESK_EMAIL_SOURCE)
|
||||
|
||||
issue.helpdesk_ticket = helpdesk_ticket
|
||||
issue.contacts << cc_contacts if HelpdeskSettings["helpdesk_save_cc", target_project.id].to_i > 0
|
||||
issue.assigned_to = @contact.find_assigned_user(project, issue.assigned_to_id)
|
||||
|
||||
|
||||
save_email_as_attachment(helpdesk_ticket)
|
||||
add_attachments(issue)
|
||||
|
||||
Redmine::Hook.call_hook(:helpdesk_mailer_receive_issue_before_save, { :issue => issue, :contact => contact, :helpdesk_ticket => helpdesk_ticket, :email => email})
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
issue.save!(:validate => false)
|
||||
ContactNote.create(:content => "*#{issue.subject}* [#{issue.tracker.name} - ##{issue.id}]\n\n" + issue.description,
|
||||
:type_id => Note.note_types[:email],
|
||||
:source => contact,
|
||||
:author_id => issue.author_id) if HelpdeskSettings["helpdesk_add_contact_notes", project]
|
||||
begin
|
||||
notification = HelpdeskMailer.auto_answer(contact, issue).deliver if HelpdeskSettings["helpdesk_send_notification", project].to_i > 0
|
||||
logger.info "#{email && email.message_id}: notification was sent to #{notification.to_addrs.first}" if logger && notification
|
||||
rescue Exception => e
|
||||
logger.error "#{email && email.message_id}: notification was not sent #{e.message}" if logger
|
||||
false
|
||||
end
|
||||
|
||||
logger.info "#{email && email.message_id}: issue ##{issue.id} created by #{user} for #{contact.name}" if logger
|
||||
issue
|
||||
end #transaction
|
||||
|
||||
end
|
||||
|
||||
# Adds a note to an existing issue
|
||||
def receive_issue_reply(issue_id)
|
||||
issue = Issue.find_by_id(issue_id)
|
||||
return unless issue
|
||||
# if lifetime expaired create new issue
|
||||
if (HelpdeskSettings["helpdesk_lifetime", target_project].to_i > 0) && issue.journals && issue.journals.last && ((Date.today) - issue.journals.last.created_on.to_date > HelpdeskSettings["helpdesk_lifetime", target_project].to_i)
|
||||
email.subject = email.subject.to_s.gsub(ISSUE_REPLY_SUBJECT_RE, '')
|
||||
return receive_issue
|
||||
end
|
||||
journal = issue.init_journal(user)
|
||||
journal.notes = escaped_cleaned_up_text_body
|
||||
|
||||
journal_message = JournalMessage.create(:from_address => message_sender(email).downcase,
|
||||
:to_address => email.to_addrs.join(',').downcase,
|
||||
:bcc_address => email.bcc_addrs.join(',').downcase,
|
||||
:cc_address => email.cc_addrs.join(',').downcase,
|
||||
:message_id => email.message_id,
|
||||
:is_incoming => true,
|
||||
:message_date => email.date || Time.now,
|
||||
:contact => contact,
|
||||
:journal => journal)
|
||||
|
||||
issue.contacts << cc_contacts if HelpdeskSettings["helpdesk_save_cc", target_project.id].to_i > 0
|
||||
|
||||
add_attachments(issue)
|
||||
|
||||
save_email_as_attachment(journal_message, "reply-#{DateTime.now.strftime('%d.%m.%y-%H.%M.%S')}.eml")
|
||||
|
||||
if reopen_status_id = ((k = @@helpdesk_mailer_options[:reopen_status]) && IssueStatus.named(k).first.try(:id) ) || ((k = get_keyword(:reopen_status_id)) && IssueStatus.find_by_id(k).try(:id))
|
||||
issue.status_id = reopen_status_id
|
||||
end
|
||||
|
||||
issue.save!
|
||||
logger.info "#{email && email.message_id}: issue ##{issue.id} updated by #{user}" if logger
|
||||
journal
|
||||
end
|
||||
|
||||
# Reply will be added to the issue
|
||||
def receive_journal_reply(journal_id)
|
||||
journal = Journal.find_by_id(journal_id)
|
||||
if journal && journal.journalized_type == 'Issue'
|
||||
receive_issue_reply(journal.journalized_id)
|
||||
end
|
||||
end
|
||||
|
||||
def add_attachments(obj)
|
||||
fwd_attachments = email.parts.map { |p|
|
||||
if p.content_type =~ /message\/rfc822/
|
||||
Mail.new(p.body).attachments
|
||||
elsif p.parts.empty?
|
||||
p if p.attachment?
|
||||
else
|
||||
p.attachments
|
||||
end
|
||||
}.flatten.compact
|
||||
|
||||
email_attachments = fwd_attachments | email.attachments
|
||||
|
||||
unless email_attachments.blank?
|
||||
email_attachments.each do |attachment|
|
||||
if RUBY_VERSION < '1.9'
|
||||
attachment_filename = (attachment[:content_type].filename rescue nil) ||
|
||||
(attachment[:content_disposition].filename rescue nil) ||
|
||||
(attachment[:content_location].location rescue nil) ||
|
||||
"attachment"
|
||||
attachment_filename = Mail::Encodings.unquote_and_convert_to(attachment_filename, 'utf-8') rescue 'unprocessable_filename'
|
||||
attachment_filename = helpdesk_to_utf8(attachment_filename)
|
||||
else
|
||||
attachment_filename = helpdesk_to_utf8(attachment.filename, 'binary')
|
||||
end
|
||||
|
||||
new_attachment = Attachment.new(:container => obj,
|
||||
:file => (attachment.decoded rescue nil) || (attachment.decode_body rescue nil) || attachment.raw_source,
|
||||
:filename => attachment_filename,
|
||||
:author => user,
|
||||
:content_type => attachment.mime_type)
|
||||
if obj.attachments.where(:digest => attachment_digest(attachment.body.to_s)).empty? && accept_attachment?(new_attachment)
|
||||
obj.attachments << new_attachment
|
||||
logger.info "#{email && email.message_id}: attachment #{attachment_filename} added to ticket: '#{obj.subject}'" if logger
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def get_keyword(attr, options = {})
|
||||
@keywords ||= {}
|
||||
if !@keywords.has_key?(attr)
|
||||
if (options[:override] || attr_overridable?(attr)) &&
|
||||
v = extract_keyword!(escaped_cleaned_up_text_body, attr, options[:format])
|
||||
@keywords[attr] = v
|
||||
elsif !@@helpdesk_mailer_options[:issue][attr].blank?
|
||||
@keywords[attr] = @@helpdesk_mailer_options[:issue][attr]
|
||||
end
|
||||
end
|
||||
@keywords[attr]
|
||||
end
|
||||
|
||||
def attr_overridable?(attr)
|
||||
@@helpdesk_mailer_options[:allow_override].present? &&
|
||||
@@helpdesk_mailer_options[:allow_override].include?(attr.to_s)
|
||||
end
|
||||
|
||||
def find_user_from_keyword(keyword)
|
||||
user ||= User.find_by_mail(keyword)
|
||||
user ||= User.find_by_login(keyword)
|
||||
if user.nil? && keyword.match(/ /)
|
||||
firstname, lastname = *(keyword.split) # "First Last Throwaway"
|
||||
user ||= User.find_by_firstname_and_lastname(firstname, lastname)
|
||||
end
|
||||
user
|
||||
end
|
||||
|
||||
def check_blacklist?(email)
|
||||
return true if HelpdeskSettings["helpdesk_blacklist", target_project].blank?
|
||||
addr = email.from_addrs.first.to_s.strip
|
||||
from_addr = addr # (addr && !addr.spec.blank?) ? addr.spec : email.header["from"].inspect.match(/[-A-z0-9.]+@[-A-z0-9.]+/).to_s
|
||||
cond = "(" + HelpdeskSettings["helpdesk_blacklist", target_project].split("\n").map{|u| u.strip unless u.blank?}.compact.join('|') + ")"
|
||||
!from_addr.match(cond)
|
||||
end
|
||||
|
||||
def new_contact_from_attributes(email_address, fullname=nil)
|
||||
contact = Contact.new
|
||||
|
||||
# Truncating the email address would result in an invalid format
|
||||
contact.email = email_address
|
||||
names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
|
||||
contact.first_name = names.shift.slice(0, 255)
|
||||
contact.last_name = names.join(' ').slice(0, 255)
|
||||
contact.company = email_address.downcase.slice(0, 255)
|
||||
contact.last_name = '-' if contact.last_name.blank?
|
||||
|
||||
if contact.last_name =~ %r(\((.*)\))
|
||||
contact.last_name, contact.company = $`, $1
|
||||
end
|
||||
|
||||
if contact.first_name =~ /,$/
|
||||
contact.first_name = contact.last_name
|
||||
contact.last_name = $` # everything before the match
|
||||
end
|
||||
|
||||
contact.projects << target_project
|
||||
contact.tag_list = HelpdeskSettings["helpdesk_created_contact_tag", target_project] if HelpdeskSettings["helpdesk_created_contact_tag", target_project]
|
||||
|
||||
contact
|
||||
end
|
||||
|
||||
def cc_contacts
|
||||
email[:cc].to_s
|
||||
email.cc_addrs.each_with_index.map do |cc_addr, index|
|
||||
cc_name = email[:cc].display_names[index]
|
||||
create_contact_from_address(cc_addr, cc_name)
|
||||
end.compact
|
||||
end
|
||||
|
||||
def create_contact_from_address(addr, name)
|
||||
contacts = Contact.find_by_emails([addr])
|
||||
unless contacts.blank?
|
||||
contact = contacts.first
|
||||
if contact.projects.blank? || HelpdeskSettings[:helpdesk_add_contact_to_project, target_project].to_i > 0
|
||||
contact.projects << target_project
|
||||
contact.save!
|
||||
end
|
||||
|
||||
return contact
|
||||
end
|
||||
|
||||
if HelpdeskSettings["helpdesk_is_not_create_contacts", target_project].to_i > 0
|
||||
logger.error "#{email && email.message_id}: can't find contact with email: #{addr} in whitelist. Not create new contacts option enable" if logger
|
||||
nil
|
||||
else
|
||||
contact = new_contact_from_attributes(addr, name)
|
||||
if contact.save(:validate => false)
|
||||
contact
|
||||
else
|
||||
logger.error "Helpdeks MailHandler: failed to create Contact: #{contact.errors.full_messages}" if logger
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Get or create contact for the +email+ sender
|
||||
def contact_from_email(email)
|
||||
# from = email.header['from'].to_s
|
||||
# debugger
|
||||
from = cleaned_up_from_address
|
||||
addr, name = from, nil
|
||||
if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
|
||||
addr, name = m[2], m[1]
|
||||
end
|
||||
if addr.present?
|
||||
create_contact_from_address(addr, name)
|
||||
else
|
||||
logger.error "#{email && email.message_id}: failed to create Contact: no FROM address found" if logger
|
||||
nil
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# Returns a Hash of issue custom field values extracted from keywords in the email body
|
||||
def custom_field_values_from_keywords(customized)
|
||||
customized.custom_field_values.inject({}) do |h, v|
|
||||
if value = get_keyword(v.custom_field.name, :override => true)
|
||||
h[v.custom_field.id.to_s] = value
|
||||
end
|
||||
h
|
||||
end
|
||||
end
|
||||
|
||||
def save_email_as_attachment(container, filename="message.eml")
|
||||
Attachment.create(:container => container,
|
||||
:file => email.raw_source.to_s,
|
||||
:author => user,
|
||||
:filename => filename,
|
||||
:content_type => "message/rfc822")
|
||||
end
|
||||
|
||||
def plain_text_body
|
||||
return @plain_text_body unless @plain_text_body.nil?
|
||||
part = email.text_part || email.html_part
|
||||
unless part
|
||||
return @plain_text_body = '' if email.parts.present? && email.parts.all? { |part| part.attachment? }
|
||||
part = email
|
||||
end
|
||||
|
||||
is_html = email.text_part.blank?
|
||||
part_charset = Mail::RubyVer.pick_encoding(part.charset).to_s rescue part.charset
|
||||
@plain_text_body = helpdesk_to_utf8(part.body.decoded, part_charset)
|
||||
|
||||
# strip html tags and remove doctype directive
|
||||
@plain_text_body.gsub! %r{^[ ]+}, ''
|
||||
if is_html && RedmineHelpdesk.strip_tags?
|
||||
@plain_text_body.gsub! %r{<head>(?:.|\n|\r)+?<\/head>}, ""
|
||||
@plain_text_body.gsub! %r{<\/(li|ol|ul|h1|h2|h3|h4)>}, "\r\n"
|
||||
@plain_text_body.gsub! %r{<\/(p|div|pre)>}, "\r\n\r\n"
|
||||
@plain_text_body.gsub! %r{<li>}, " - "
|
||||
@plain_text_body.gsub! %r{<br[^>]*>}, "\r\n"
|
||||
@plain_text_body = strip_tags(@plain_text_body.strip)
|
||||
@plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
|
||||
end
|
||||
@plain_text_body.strip
|
||||
|
||||
rescue Exception => e
|
||||
logger.error "#{email && email.message_id}: Message body processing error - #{e.message}" if logger
|
||||
@plain_text_body = '(Unprocessable message body)'
|
||||
end
|
||||
|
||||
def cleaned_up_subject(email)
|
||||
return "" if email[:subject].blank?
|
||||
subject = decode_subject(email[:subject].value)
|
||||
subject = helpdesk_to_utf8(subject)
|
||||
subject.strip[0,255]
|
||||
rescue Exception => e
|
||||
logger.error "#{email && email.message_id}: Message subject processing error - #{e.message}" if logger
|
||||
'(Unprocessable subject)'
|
||||
end
|
||||
|
||||
def cleaned_up_from_address
|
||||
from = email.header['reply-to'].to_s.present? ? email.header['reply-to'] : email.header['from']
|
||||
from.to_s.strip[0, 255]
|
||||
end
|
||||
|
||||
def logger
|
||||
HelpdeskLogger
|
||||
end
|
||||
|
||||
def helpdesk_to_utf8(str, encoding="UTF-8")
|
||||
return str if str.nil?
|
||||
if str.respond_to?(:force_encoding)
|
||||
begin
|
||||
cleaned = str.force_encoding('UTF-8')
|
||||
cleaned = cleaned.encode("UTF-8", encoding) if encoding.upcase == 'ISO-2022-JP'
|
||||
unless cleaned.valid_encoding?
|
||||
cleaned = str.encode('UTF-8', encoding, :invalid => :replace, :undef => :replace, :replace => '').chars.select{|i| i.valid_encoding?}.join
|
||||
end
|
||||
str = cleaned
|
||||
rescue EncodingError
|
||||
str.encode!( 'UTF-8', :invalid => :replace, :undef => :replace )
|
||||
end
|
||||
elsif RUBY_PLATFORM == 'java'
|
||||
begin
|
||||
ic = Iconv.new('UTF-8', encoding + '//IGNORE')
|
||||
str = ic.iconv(str)
|
||||
rescue
|
||||
str = str.gsub(%r{[^\r\n\t\x20-\x7e]}, '?')
|
||||
end
|
||||
else
|
||||
ic = Iconv.new('UTF-8', encoding + '//IGNORE')
|
||||
txtar = ""
|
||||
begin
|
||||
txtar += ic.iconv(str)
|
||||
rescue Iconv::IllegalSequence
|
||||
txtar += $!.success
|
||||
str = '?' + $!.failed[1,$!.failed.length]
|
||||
retry
|
||||
rescue
|
||||
txtar += $!.success
|
||||
end
|
||||
str = txtar
|
||||
end
|
||||
str
|
||||
end
|
||||
|
||||
def attachment_digest(file_source)
|
||||
encoder = Redmine::VERSION.to_s >= '3.4' || RedmineContacts.unstable_branch? ? Digest::SHA256.new : Digest::MD5.new
|
||||
encoder.update(file_source)
|
||||
encoder.hexdigest
|
||||
end
|
||||
|
||||
def set_delivery_options
|
||||
return false if HelpdeskSettings[:helpdesk_smtp_use_default_settings, project.id].to_i == 0
|
||||
message.delivery_method(:smtp)
|
||||
settings = {:address => HelpdeskSettings[:helpdesk_smtp_server, project.id],
|
||||
:port => HelpdeskSettings[:helpdesk_smtp_port, project.id] || 25,
|
||||
:domain => HelpdeskSettings[:helpdesk_smtp_domain, project.id],
|
||||
:enable_starttls_auto => true,
|
||||
:ssl => HelpdeskSettings[:helpdesk_smtp_ssl, project.id].to_i > 0 &&
|
||||
HelpdeskSettings[:helpdesk_smtp_tls, project.id].to_i == 0}
|
||||
authentication = HelpdeskSettings[:helpdesk_smtp_authentication, project.id]
|
||||
unless authentication.blank?
|
||||
settings.merge!(:authentication => authentication,
|
||||
:user_name => HelpdeskSettings[:helpdesk_smtp_username, project.id],
|
||||
:password => HelpdeskSettings[:helpdesk_smtp_password, project.id])
|
||||
end
|
||||
message.delivery_method.settings.merge!(settings)
|
||||
end
|
||||
|
||||
def decode_subject(str)
|
||||
# Optimization: If there's no encoded-words in the string, just return it
|
||||
return str unless str.index("=?")
|
||||
|
||||
str = str.gsub(/\?=(\s*)=\?/, '?=????=?') # Replace whitespaces between 'encoded-word's on special symbols
|
||||
str.split('????').map do |text|
|
||||
if text.index('=?') .nil?
|
||||
text
|
||||
else
|
||||
text.gsub!(/[\r\n]/, '')
|
||||
text.scan(/\=\?.+?\?[qQbB]\?.+?\?\=/).map do |part|
|
||||
if part.index(/\=\?.+\?[Bb]\?.+\?\=/m)
|
||||
part.gsub(/\=\?.+\?[Bb]\?.+\?=/m) { |substr| Mail::Encodings.b_value_decode(substr) }
|
||||
elsif part.index(/\=\?.+\?[Qq]\?.+\?\=/m)
|
||||
part.gsub(/\=\?.+\?[Qq]\?.+\?\=/m) { |substr| Mail::Encodings.q_value_decode(substr) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end.join('')
|
||||
end
|
||||
|
||||
def self.ignored_helpdesk_headers
|
||||
helpdesk_headers = {
|
||||
'X-Auto-Response-Suppress' => /\A(all|AutoReply|oof)/
|
||||
}
|
||||
ignored_emails_headers.merge(helpdesk_headers)
|
||||
end
|
||||
|
||||
def handle_ignored(email)
|
||||
self.class.ignored_helpdesk_headers.each do |key, ignored_value|
|
||||
value = email.header[key]
|
||||
if value
|
||||
value = value.to_s.downcase
|
||||
if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
|
||||
if logger
|
||||
logger.info "#{email && email.message_id}: ignoring email with #{key}:#{value} header"
|
||||
end
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
def escaped_cleaned_up_text_body
|
||||
text_body = cleaned_up_text_body
|
||||
return text_body unless (ActiveRecord::Base.connection.adapter_name =~ /mysql/i).present?
|
||||
text_body.gsub(/./) { |c| c.bytesize == 4 ? ' ' : c }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,18 @@
|
||||
class HelpdeskReportsBusiestTimeQuery < HelpdeskReportsQuery
|
||||
def sql_for_staff_field(_field, operator, value)
|
||||
issue_table = Issue.table_name
|
||||
compare = operator == '=' ? 'IN' : 'NOT IN'
|
||||
staff_ids = value.join(',')
|
||||
"#{issue_table}.id IN(SELECT #{issue_table}.id FROM #{issue_table} WHERE (#{issue_table}.assigned_to_id #{compare} (#{staff_ids})))"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def collect_answered_users
|
||||
return [] unless project
|
||||
user_ids = Issue.joins(:project).
|
||||
visible.uniq.
|
||||
pluck(:assigned_to_id).compact
|
||||
User.where(:id => user_ids)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
class HelpdeskReportsFirstResponseQuery < HelpdeskReportsQuery
|
||||
def sql_for_staff_field(_field, operator, value)
|
||||
issue_table = Issue.table_name
|
||||
journal_table = Journal.table_name
|
||||
compare = operator == '=' ? 'IN' : 'NOT IN'
|
||||
staff_ids = value.join(',')
|
||||
"#{issue_table}.id IN(SELECT #{issue_table}.id FROM #{issue_table} INNER JOIN #{journal_table} ON #{journal_table}.journalized_id = #{issue_table}.id AND #{journal_table}.journalized_type = 'Issue' WHERE (#{journal_table}.user_id #{compare} (#{staff_ids})))"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def collect_answered_users
|
||||
return [] unless project
|
||||
user_ids = Issue.joins(:project).
|
||||
joins(:journals).
|
||||
joins(:journals => :journal_message).
|
||||
visible.uniq.
|
||||
pluck(:assigned_to_id).compact
|
||||
User.where(:id => user_ids)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,94 @@
|
||||
class HelpdeskReportsQuery < Query
|
||||
self.queried_class = JournalMessage
|
||||
operators_by_filter_type[:time_interval] = ['t', 'ld', 'w', 'l2w', 'm', 'lm', 'y']
|
||||
|
||||
def initialize_available_filters
|
||||
add_available_filter 'message_date', :type => :time_interval, :name => l(:label_helpdesk_filter_time_interval)
|
||||
author_values = collect_answered_users.collect { |user| [user.name, user.id.to_s] }
|
||||
add_available_filter 'staff', :type => :list, :name => l(:field_assigned_to), :values => author_values
|
||||
end
|
||||
|
||||
def build_from_params(params)
|
||||
if params[:fields] || params[:f]
|
||||
self.filters = {}
|
||||
add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
|
||||
else
|
||||
available_filters.keys.each do |field|
|
||||
add_short_filter(field, params[field]) if params[field]
|
||||
end
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
def issues(options = {})
|
||||
scope = issue_scope.eager_load((options[:include] || []).uniq).
|
||||
where(options[:conditions]).
|
||||
limit(options[:limit]).
|
||||
offset(options[:offset])
|
||||
scope
|
||||
rescue ::ActiveRecord::StatementInvalid => e
|
||||
raise StatementInvalid.new(e.message)
|
||||
end
|
||||
|
||||
def sql_for_staff_field(_field, operator, value)
|
||||
issue_table = Issue.table_name
|
||||
journal_table = Journal.table_name
|
||||
compare = operator == '=' ? 'IN' : 'NOT IN'
|
||||
staff_ids = value.join(',')
|
||||
"#{issue_table}.id IN(SELECT #{issue_table}.id FROM #{issue_table} INNER JOIN #{journal_table} ON #{journal_table}.journalized_id = #{issue_table}.id AND #{journal_table}.journalized_type = 'Issue' WHERE (#{journal_table}.user_id #{compare} (#{staff_ids})))"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def collect_answered_users
|
||||
return [] unless project
|
||||
user_ids = Issue.joins(:project).
|
||||
joins(:journals).
|
||||
joins(:journals => :journal_message).
|
||||
visible.uniq.
|
||||
pluck(:assigned_to_id).compact
|
||||
User.where(:id => user_ids)
|
||||
end
|
||||
|
||||
def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter = false)
|
||||
sql = ''
|
||||
first_day_of_week = l(:general_first_day_of_week).to_i
|
||||
date = Date.today
|
||||
day_of_week = date.cwday
|
||||
case operator
|
||||
when 'pre_t'
|
||||
sql = date_clause_selector(db_table, db_field, -1, -1, is_custom_filter)
|
||||
when 'pre_ld'
|
||||
sql = date_clause_selector(db_table, db_field, -2, -2, is_custom_filter)
|
||||
when 'pre_w'
|
||||
days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
|
||||
sql = date_clause_selector(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter)
|
||||
when 'pre_l2w'
|
||||
days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
|
||||
sql = date_clause_selector(db_table, db_field, - days_ago - 28, - days_ago - 14 - 1, is_custom_filter)
|
||||
when 'pre_m'
|
||||
sql = date_clause_selector_for_date(db_table, db_field, (date - 1.month).beginning_of_month, (date - 1.month).end_of_month, is_custom_filter)
|
||||
when 'pre_lm'
|
||||
sql = date_clause_selector_for_date(db_table, db_field, (date - 2.months).beginning_of_month, (date - 2.months).end_of_month, is_custom_filter)
|
||||
when 'pre_y'
|
||||
sql = date_clause_selector_for_date(db_table, db_field, (date - 1.year).beginning_of_year, (date - 1.year).end_of_year, is_custom_filter)
|
||||
end
|
||||
sql = super(field, operator, value, db_table, db_field, is_custom_filter) if sql.blank?
|
||||
|
||||
sql
|
||||
end
|
||||
|
||||
def date_clause_selector(table, field, from, to, is_custom_filter)
|
||||
return date_clause(table, field, (from ? Date.today + from : nil), (to ? Date.today + to : nil)) if Redmine::VERSION.to_s < '3.0'
|
||||
date_clause(table, field, (from ? Date.today + from : nil), (to ? Date.today + to : nil), is_custom_filter)
|
||||
end
|
||||
|
||||
def date_clause_selector_for_date(table, field, date_from, date_to, is_custom_filter)
|
||||
return date_clause(table, field, date_from, date_to) if Redmine::VERSION.to_s < '3.0'
|
||||
date_clause(table, field, date_from, date_to, is_custom_filter)
|
||||
end
|
||||
|
||||
def issue_scope
|
||||
Issue.visible.joins(:project, :journals => :journal_message).where(statement).uniq
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,268 @@
|
||||
class HelpdeskTicket < ActiveRecord::Base
|
||||
HELPDESK_EMAIL_SOURCE = 0
|
||||
HELPDESK_WEB_SOURCE = 1
|
||||
HELPDESK_PHONE_SOURCE = 2
|
||||
HELPDESK_TWITTER_SOURCE = 3
|
||||
HELPDESK_CONVERSATION_SOURCE = 4
|
||||
|
||||
SEND_AS_NOTIFICATION = 1
|
||||
SEND_AS_MESSAGE = 2
|
||||
|
||||
attr_accessible :vote, :vote_comment,:from_address,
|
||||
:to_address, :cc_address, :ticket_date,
|
||||
:message_id, :is_incoming, :customer, :issue, :source, :contact_id, :ticket_time
|
||||
|
||||
attr_accessor :ticket_time
|
||||
|
||||
unloadable
|
||||
belongs_to :customer, :class_name => 'Contact', :foreign_key => 'contact_id'
|
||||
belongs_to :issue
|
||||
has_one :message_file, :class_name => "Attachment", :as => :container, :dependent => :destroy
|
||||
|
||||
acts_as_attachable :view_permission => :view_issues,
|
||||
:delete_permission => :edit_issues
|
||||
|
||||
|
||||
if ActiveRecord::VERSION::MAJOR >= 4
|
||||
acts_as_activity_provider :type => 'helpdesk_tickets',
|
||||
:permission => :view_helpdesk_tickets,
|
||||
:timestamp => "#{table_name}.ticket_date",
|
||||
:author_key => "#{Issue.table_name}.author_id",
|
||||
:scope => eager_load(:issue => :project)
|
||||
else
|
||||
acts_as_activity_provider :type => 'helpdesk_tickets',
|
||||
:permission => :view_helpdesk_tickets,
|
||||
:timestamp => "#{table_name}.ticket_date",
|
||||
:author_key => "#{Issue.table_name}.author_id",
|
||||
:find_options => {:include => {:issue => :project}}
|
||||
end
|
||||
|
||||
acts_as_event :datetime => :ticket_date,
|
||||
:project_key => "#{Project.table_name}.id",
|
||||
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue_id}},
|
||||
:type => Proc.new {|o| 'icon icon-email' + (o.issue.closed? ? ' closed' : '') if o.issue },
|
||||
:title => Proc.new {|o| "##{o.issue.id} (#{o.issue.status}): #{o.issue.subject}" if o.issue },
|
||||
:author => Proc.new {|o| o.customer},
|
||||
:description => Proc.new{|o| o.issue.description if o.issue}
|
||||
|
||||
accepts_nested_attributes_for :customer
|
||||
|
||||
after_create :set_ticket_private
|
||||
before_save :calculate_metrics
|
||||
validates_presence_of :customer, :ticket_date
|
||||
|
||||
def initialize(attributes=nil, *args)
|
||||
super
|
||||
if new_record?
|
||||
# set default values for new records only
|
||||
self.ticket_date ||= Time.now
|
||||
self.source ||= HelpdeskTicket::HELPDESK_EMAIL_SOURCE
|
||||
end
|
||||
end
|
||||
|
||||
def ticket_time
|
||||
self.ticket_date.to_s(:time) unless self.ticket_date.blank?
|
||||
end
|
||||
|
||||
def ticket_time=(val)
|
||||
if !self.ticket_date.blank? && val.to_s.gsub(/\s/, "").match(/^(\d{1,2}):(\d{1,2})$/)
|
||||
timezone = ticket_date.try(:time_zone).try(:name) || Time.zone.name
|
||||
self.ticket_date = ActiveSupport::TimeZone.new(timezone).local_to_utc(self.ticket_date.utc).
|
||||
in_time_zone(timezone).
|
||||
change({:hour => $1.to_i % 24, :min => $2.to_i % 60})
|
||||
end
|
||||
end
|
||||
|
||||
def recalculate_events
|
||||
unless issue.closed?
|
||||
close_journal_id = nil
|
||||
end
|
||||
end
|
||||
|
||||
def available_addresses
|
||||
@available_addresses ||= ([self.default_to_address] | self.customer.emails.map{|e| e} | [self.from_address.blank? ? nil : self.from_address.downcase.strip]).compact.uniq if self.customer
|
||||
end
|
||||
|
||||
def default_to_address
|
||||
return last_response_address if last_journal_message && last_journal_message.is_incoming?
|
||||
address = self.from_address.blank? ? "" : self.from_address.downcase.strip
|
||||
self.customer.emails.include?(address) ? address : self.customer.primary_email
|
||||
end
|
||||
|
||||
def last_reply_customer
|
||||
return customer unless default_to_address
|
||||
customer.primary_email == default_to_address ? customer : Contact.find_by_emails([default_to_address]).first
|
||||
end
|
||||
|
||||
def cc_addresses
|
||||
@cc_addresses = ((self.issue.contacts ? self.issue.contacts.map(&:primary_email) : []) | cc_address.to_s.split(',')).compact.uniq
|
||||
end
|
||||
|
||||
def project
|
||||
issue.project if issue
|
||||
end
|
||||
|
||||
def author
|
||||
issue.author if issue
|
||||
end
|
||||
|
||||
def customer_name
|
||||
customer.name if customer
|
||||
end
|
||||
|
||||
def responses
|
||||
@responses ||= JournalMessage.
|
||||
joins(:journal).
|
||||
where(:journals => {:journalized_id => self.issue_id}).
|
||||
order("#{JournalMessage.table_name}.message_date ASC")
|
||||
end
|
||||
|
||||
def reaction_date
|
||||
@reaction_date ||= self.issue.journals.
|
||||
joins(:journal_message).
|
||||
where("#{JournalMessage.table_name}.journal_id IS NULL OR #{JournalMessage.table_name}.is_incoming = ?", false).
|
||||
order("#{Journal.table_name}.created_on ASC").
|
||||
first.
|
||||
try(:created_on).try(:utc)
|
||||
end
|
||||
|
||||
def response_addresses
|
||||
responses.where(:is_incoming => true).map { |response| response.from_address }.uniq
|
||||
end
|
||||
|
||||
def first_response_date
|
||||
@first_response_date ||= responses.select {|r| !r.is_incoming? }.first.try(:message_date).try(:utc)
|
||||
end
|
||||
|
||||
def last_response_time
|
||||
@last_response_time ||= last_journal_message && last_journal_message.is_incoming? && !self.issue.closed? ? last_journal_message.message_date.utc : nil
|
||||
end
|
||||
|
||||
def last_response_address
|
||||
response_addresses.last
|
||||
end
|
||||
|
||||
def last_agent_response
|
||||
@last_agent_response ||= responses.select { |r| !r.is_incoming? }.last
|
||||
end
|
||||
|
||||
def last_journal_message
|
||||
@last_journal_message ||= responses.last
|
||||
end
|
||||
|
||||
def last_customer_response
|
||||
@last_customer_response ||= responses.select { |r| r.is_incoming? }.last
|
||||
end
|
||||
|
||||
def average_response_time
|
||||
|
||||
end
|
||||
|
||||
def ticket_source_name
|
||||
case self.source
|
||||
when HelpdeskTicket::HELPDESK_EMAIL_SOURCE then l(:label_helpdesk_tickets_email)
|
||||
when HelpdeskTicket::HELPDESK_PHONE_SOURCE then l(:label_helpdesk_tickets_phone)
|
||||
when HelpdeskTicket::HELPDESK_WEB_SOURCE then l(:label_helpdesk_tickets_web)
|
||||
when HelpdeskTicket::HELPDESK_TWITTER_SOURCE then l(:label_helpdesk_tickets_twitter)
|
||||
when HelpdeskTicket::HELPDESK_CONVERSATION_SOURCE then l(:label_helpdesk_tickets_conversation)
|
||||
else ""
|
||||
end
|
||||
end
|
||||
|
||||
def ticket_source_icon
|
||||
case self.source
|
||||
when HelpdeskTicket::HELPDESK_EMAIL_SOURCE then "icon-email"
|
||||
when HelpdeskTicket::HELPDESK_PHONE_SOURCE then "icon-call"
|
||||
when HelpdeskTicket::HELPDESK_WEB_SOURCE then "icon-web"
|
||||
when HelpdeskTicket::HELPDESK_TWITTER_SOURCE then "icon-twitter"
|
||||
else "icon-helpdesk"
|
||||
end
|
||||
end
|
||||
|
||||
def content
|
||||
issue.description if issue
|
||||
end
|
||||
|
||||
def customer_email
|
||||
customer.primary_email if customer
|
||||
end
|
||||
|
||||
def last_message
|
||||
@last_message ||= JournalMessage.eager_load(:journal => :issue).where(:issues => {:id => issue.id}).order("#{Journal.table_name}.created_on ASC").last || self
|
||||
end
|
||||
|
||||
def last_message_date
|
||||
last_message.is_a?(HelpdeskTicket) ? self.ticket_date : last_message.message_date if last_message
|
||||
end
|
||||
|
||||
def ticket_date
|
||||
return nil if super.blank?
|
||||
zone = User.current.time_zone
|
||||
zone ? super.in_time_zone(zone) : (super.utc? ? super.localtime : super)
|
||||
end
|
||||
|
||||
def token
|
||||
Digest::MD5.hexdigest("#{issue.id}:#{self.ticket_date.utc}:#{Rails.application.config.secret_token}")
|
||||
end
|
||||
|
||||
def calculate_metrics
|
||||
self.reaction_time = reaction_date - ticket_date.utc if reaction_date && ticket_date
|
||||
self.first_response_time = first_response_date - ticket_date.utc if first_response_date && ticket_date
|
||||
self.resolve_time = self.issue.closed? ? self.issue.closed_on - ticket_date.utc : nil if ticket_date && self.issue.closed_on && last_agent_response
|
||||
self.last_agent_response_at = last_agent_response.message_date if last_agent_response
|
||||
self.last_customer_response_at = last_customer_response.message_date if last_customer_response
|
||||
end
|
||||
|
||||
def self.vote_message(vote)
|
||||
case vote.to_i
|
||||
when 0
|
||||
l(:label_helpdesk_mark_notgood)
|
||||
when 1
|
||||
l(:label_helpdesk_mark_justok)
|
||||
when 2
|
||||
l(:label_helpdesk_mark_awesome)
|
||||
else
|
||||
""
|
||||
end
|
||||
end
|
||||
|
||||
def update_vote(new_vote, comment = nil)
|
||||
old_vote = vote
|
||||
old_vote_comment = vote_comment
|
||||
if update_attributes(:vote => new_vote, :vote_comment => comment )
|
||||
if old_vote != vote || old_vote_comment != vote_comment
|
||||
journal = Journal.new(:journalized => issue, :user => User.current)
|
||||
journal.details << JournalDetail.new(:property => 'attr',
|
||||
:prop_key => 'vote',
|
||||
:old_value => old_vote,
|
||||
:value => vote) if old_vote != vote
|
||||
journal.details << JournalDetail.new(:property => 'attr',
|
||||
:prop_key => 'vote_comment',
|
||||
:old_value => old_vote_comment,
|
||||
:value => vote_comment) if old_vote_comment != vote_comment
|
||||
journal.save
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.autoclose(project)
|
||||
return unless RedmineHelpdesk.autoclose_tickets_after > 0
|
||||
issues = Issue.includes(:helpdesk_ticket).where(:project_id => project.id).
|
||||
where(:status_id => RedmineHelpdesk.autoclose_from_status).
|
||||
where('created_on < ?', Time.now - RedmineHelpdesk.autoclose_time_interval)
|
||||
issues.find_each do |issue|
|
||||
issue.init_journal(User.anonymous)
|
||||
issue.current_journal.notes = I18n.t('label_helpdesk_autoclosed_ticket')
|
||||
issue.status_id = RedmineHelpdesk.autoclose_to_status
|
||||
issue.save
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_ticket_private
|
||||
return unless RedmineHelpdesk.settings["helpdesk_assign_contact_user"].to_i > 0
|
||||
issue.assign_attributes(:is_private => true) if RedmineHelpdesk.settings["helpdesk_create_private_tickets"].to_i > 0
|
||||
issue.save unless issue.new_record?
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,58 @@
|
||||
class JournalMessage < ActiveRecord::Base
|
||||
unloadable
|
||||
belongs_to :contact
|
||||
belongs_to :journal
|
||||
has_one :message_file, :class_name => "Attachment", :as => :container, :dependent => :destroy
|
||||
|
||||
attr_accessible :source, :from_address, :to_address, :bcc_address, :cc_address, :message_id, :is_incoming, :message_date, :contact, :journal
|
||||
|
||||
acts_as_attachable :view_permission => :view_issues,
|
||||
:delete_permission => :edit_issues
|
||||
|
||||
if ActiveRecord::VERSION::MAJOR >= 4
|
||||
acts_as_activity_provider :type => 'helpdesk_tickets',
|
||||
:permission => :view_helpdesk_tickets,
|
||||
:timestamp => "#{table_name}.message_date",
|
||||
:author_key => "#{Journal.table_name}.user_id",
|
||||
:scope => eager_load({:journal => [{:issue => [:project, :tracker]}, :details, :user]}, :contact)
|
||||
else
|
||||
acts_as_activity_provider :type => 'helpdesk_tickets',
|
||||
:permission => :view_helpdesk_tickets,
|
||||
:timestamp => "#{table_name}.message_date",
|
||||
:author_key => "#{Journal.table_name}.user_id",
|
||||
:find_options => {:include => [{:journal => [{:issue => [:project, :tracker]}, :details, :user]}, :contact]}
|
||||
end
|
||||
|
||||
acts_as_event :title => Proc.new {|o| "#{o.journal.issue.tracker} ##{o.journal.issue.id}: #{o.journal.issue.subject}" if o.journal && o.journal.issue},
|
||||
:datetime => :message_date,
|
||||
:group => :helpdesk_ticket,
|
||||
:project_key => "#{Project.table_name}.id",
|
||||
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.journal.issue.id, :anchor => "change-#{o.id}"} if o.journal},
|
||||
:type => Proc.new {|o| ('icon' + (o.is_incoming? ? " icon-email" : " icon-email-to")) },
|
||||
:author => Proc.new {|o| o.is_incoming? ? o.contact : o.journal.user },
|
||||
:description => Proc.new{|o| o.journal.notes if o.journal}
|
||||
|
||||
validates_presence_of :contact, :journal, :message_date
|
||||
|
||||
def project
|
||||
journal.project
|
||||
end
|
||||
|
||||
def contact_name
|
||||
contact.name
|
||||
end
|
||||
|
||||
def contact_email
|
||||
contact.emails.first
|
||||
end
|
||||
|
||||
def helpdesk_ticket
|
||||
journal.issue.helpdesk_ticket
|
||||
end
|
||||
|
||||
def content
|
||||
journal.notes
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
Reference in New Issue
Block a user