Initial Redmine tooling and local plugin forks

This commit is contained in:
Jason Thistlethwaite
2026-04-24 22:01:18 +00:00
commit 9f682af0eb
683 changed files with 56878 additions and 0 deletions
@@ -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