Import archive redmine_contacts_helpdesk-4_2_4-pro
This commit is contained in:
commit
d5f6bb5490
112
README.md
Normal file
112
README.md
Normal file
@ -0,0 +1,112 @@
|
||||
# Helpdesk plugin
|
||||
|
||||
### API
|
||||
|
||||
API request #index
|
||||
```
|
||||
GET /helpdesk_tickets.xml or /helpdesk_tickets.json
|
||||
```
|
||||
|
||||
API request #index with filters
|
||||
```
|
||||
GET /helpdesk_tickets.xml?issue_id=1,2,3 or /helpdesk_tickets.json?issue_id=3,1,2
|
||||
GET /helpdesk_tickets.xml?contact_id=1,2,3 or /helpdesk_tickets.json?contact_id=3,1,2
|
||||
```
|
||||
|
||||
API request #show
|
||||
```
|
||||
GET /helpdesk_tickets/<issue_id>.xml or /helpdesk_tickets/<issue_id>.json
|
||||
```
|
||||
|
||||
To get contact with tickets related to it
|
||||
```
|
||||
GET /contacts/<contact_id>.json?include=tickets
|
||||
```
|
||||
|
||||
API request #create
|
||||
Create a ticket without sending an auto answer email
|
||||
```
|
||||
POST /helpdesk_tickets.json?key=<your_api_key>
|
||||
{
|
||||
"helpdesk_ticket": {
|
||||
"issue": {
|
||||
"project_id": "support",
|
||||
"subject": "test helpdesk api",
|
||||
"description": "ticket description"
|
||||
},
|
||||
"contact": {
|
||||
"email": "test@example.com",
|
||||
"first_name": "John"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Create a ticket with sending an auto answer email
|
||||
```
|
||||
POST /helpdesk_tickets.json?key=<your_api_key>
|
||||
{
|
||||
"helpdesk_ticket": {
|
||||
"issue": {
|
||||
"project_id": "support",
|
||||
"subject": "test helpdesk api",
|
||||
"description": "ticket description"
|
||||
},
|
||||
"contact": {
|
||||
"email": "test@example.com",
|
||||
"first_name": "John"
|
||||
},
|
||||
"send_as": "auto_answer"
|
||||
}
|
||||
}
|
||||
```
|
||||
parameter send_as: "auto_answer", "inital_message"
|
||||
|
||||
Old version API request #create
|
||||
```
|
||||
POST /helpdesk/create_ticket.json?key=<your_api_key>
|
||||
{
|
||||
"ticket": {
|
||||
"issue": {
|
||||
"project_id": "support",
|
||||
"tracker_id": 1,
|
||||
"subject": "New ticket subject",
|
||||
"description": "New ticket description",
|
||||
"assigned_to_id": 1
|
||||
},
|
||||
"contact": {
|
||||
"email": "test@example.com",
|
||||
"first_name": "John",
|
||||
"custom_fields: {
|
||||
type": "array",
|
||||
"custom_field: {
|
||||
"id": 1,
|
||||
"value": "Test"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Send email reply to an existed Helpdesk ticket
|
||||
```
|
||||
POST /helpdesk/email_note.json
|
||||
{
|
||||
"message": {
|
||||
"issue_id": 1,
|
||||
"status_id": 2,
|
||||
"content": "New replay to ticket"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Update ticket
|
||||
```
|
||||
PUT /helpdesk_tickets/<ticket_id>.json?key=<your_api_key>
|
||||
{
|
||||
"helpdesk_ticket": {
|
||||
"from_address": "new_email@mail.com"
|
||||
}
|
||||
}
|
||||
```
|
131
app/controllers/canned_responses_controller.rb
Normal file
131
app/controllers/canned_responses_controller.rb
Normal file
@ -0,0 +1,131 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class CannedResponsesController < ApplicationController
|
||||
before_action :find_canned_response, :except => [:new, :create, :index]
|
||||
before_action :find_optional_project, :only => [:new, :create, :edit, :update, :add, :destroy]
|
||||
before_action :find_issue, :only => [:add]
|
||||
before_action :require_admin, :only => [:index]
|
||||
|
||||
accept_api_auth :index
|
||||
|
||||
def index
|
||||
case params[:format]
|
||||
when 'xml', 'json'
|
||||
@offset, @limit = api_offset_and_limit
|
||||
else
|
||||
@limit = per_page_option
|
||||
end
|
||||
|
||||
scope = CannedResponse.visible
|
||||
scope = scope.in_project_or_public(@project) if @project
|
||||
|
||||
@canned_response_count = scope.count
|
||||
@canned_response_pages = Paginator.new @canned_response_count, @limit, params['page']
|
||||
@offset ||= @canned_response_pages.offset
|
||||
@canned_responses = scope.limit(@limit).offset(@offset).order("#{CannedResponse.table_name}.name")
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
end
|
||||
end
|
||||
|
||||
def add
|
||||
@content = HelpdeskMailSupport.apply_text_macro(@canned_response.content, @issue.customer, @issue, User.current)
|
||||
@attachments = @canned_response.attachments.map do |attachment|
|
||||
new_attachment = attachment.copy(container: nil)
|
||||
new_attachment.save
|
||||
new_attachment
|
||||
end
|
||||
end
|
||||
|
||||
def new
|
||||
@canned_response = CannedResponse.new
|
||||
@canned_response.user = User.current
|
||||
@canned_response.project = @project
|
||||
end
|
||||
|
||||
def create
|
||||
@canned_response = CannedResponse.new
|
||||
@canned_response.project = @project
|
||||
@canned_response.safe_attributes = params[:canned_response]
|
||||
@canned_response.user = User.current
|
||||
@canned_response.project = nil if params[:canned_response_is_for_all]
|
||||
@canned_response.save_attachments(params[:attachments] || (params[:canned_response] && params[:canned_response][:uploads]))
|
||||
|
||||
if @canned_response.save
|
||||
flash[:notice] = l(:notice_successful_create)
|
||||
redirect_to_project_or_global
|
||||
else
|
||||
render :action => 'new', :layout => !request.xhr?
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
@canned_response.project = @project
|
||||
@canned_response.safe_attributes = params[:canned_response]
|
||||
@canned_response.project = nil if params[:canned_response_is_for_all]
|
||||
@canned_response.save_attachments(params[:attachments] || (params[:canned_response] && params[:canned_response][:uploads]))
|
||||
|
||||
if @canned_response.save
|
||||
flash[:notice] = l(:notice_successful_update)
|
||||
redirect_to_project_or_global
|
||||
else
|
||||
render :action => 'edit'
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@canned_response.destroy
|
||||
redirect_to_project_or_global
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def redirect_to_project_or_global
|
||||
redirect_to @project ? settings_project_path(@project, :tab => 'helpdesk_canned_responses') : path_to_global_setting
|
||||
end
|
||||
|
||||
def path_to_global_setting
|
||||
{
|
||||
:action => 'plugin',
|
||||
:id => 'redmine_contacts_helpdesk',
|
||||
:controller => 'settings',
|
||||
:tab => 'canned_responses'
|
||||
}
|
||||
end
|
||||
|
||||
def find_issue
|
||||
@issue = Issue.find(params[:issue_id])
|
||||
@project = @issue.project
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
def find_canned_response
|
||||
@canned_response = CannedResponse.find(params[:id])
|
||||
@project = @canned_response.project
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
end
|
40
app/controllers/helpdesk_base_controller.rb
Normal file
40
app/controllers/helpdesk_base_controller.rb
Normal file
@ -0,0 +1,40 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskBaseController < ApplicationController
|
||||
|
||||
def process_helpdesk_send_as(send_as = nil)
|
||||
return unless @issue && User.current.allowed_to?(:send_response, @project)
|
||||
return unless send_as
|
||||
|
||||
case send_as
|
||||
when 'auto_answer'
|
||||
HelpdeskMailer.auto_answer(@issue.customer, @issue)
|
||||
when 'initial_message'
|
||||
if msg = HelpdeskMailer.initial_message(@issue.customer, @issue)
|
||||
@issue.helpdesk_ticket.message_id = msg.message_id
|
||||
@issue.helpdesk_ticket.is_incoming = false
|
||||
@issue.helpdesk_ticket.from_address = @issue.helpdesk_ticket.from_address || @issue.customer.primary_email
|
||||
@issue.helpdesk_ticket.save
|
||||
end
|
||||
else
|
||||
HelpdeskMailer.auto_answer(@contact, @issue) if HelpdeskSettings['helpdesk_send_notification', @project].to_i > 0
|
||||
end
|
||||
end
|
||||
end
|
307
app/controllers/helpdesk_controller.rb
Normal file
307
app/controllers/helpdesk_controller.rb
Normal file
@ -0,0 +1,307 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskController < HelpdeskBaseController
|
||||
helper :attachments
|
||||
include AttachmentsHelper
|
||||
|
||||
skip_before_action :check_if_login_required, :only => [:message_link]
|
||||
before_action :find_project, :authorize, :except => [
|
||||
:email_note,
|
||||
:update_customer_email,
|
||||
:message_link
|
||||
]
|
||||
|
||||
accept_api_auth :email_note, :create_ticket
|
||||
|
||||
helper :attachments
|
||||
include AttachmentsHelper
|
||||
|
||||
def save_settings
|
||||
if request.put?
|
||||
set_settings
|
||||
flash[:notice] = l(:notice_successful_update)
|
||||
end
|
||||
|
||||
redirect_to :controller => 'projects', :action => 'settings', :tab => params[:tab] || 'helpdesk', :id => @project
|
||||
end
|
||||
|
||||
def show_original
|
||||
@attachment = Attachment.find(params[:id])
|
||||
email = Mail.read(@attachment.diskfile)
|
||||
part = email.text_part || email.html_part || email
|
||||
body_charset = Mail::RubyVer.pick_encoding(part.charset).to_s rescue part.charset
|
||||
plain_text_body = Redmine::CodesetUtil.to_utf8(part.body.decoded, body_charset)
|
||||
headers = email.header.fields.map { |f| "#{f.name}: #{Mail::Encodings.unquote_and_convert_to(f.value, 'utf-8')}" }.join("\n")
|
||||
@content = headers + "\n\n" + plain_text_body
|
||||
|
||||
render 'attachments/file'
|
||||
end
|
||||
|
||||
def delete_spam
|
||||
if User.current.allowed_to?(:delete_issues, @project) && User.current.allowed_to?(:delete_contacts, @project)
|
||||
begin
|
||||
@issue = Issue.find(params[:issue_id])
|
||||
@customer = @issue.customer
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
ContactsSetting['helpdesk_blacklist', @project.id] = (ContactsSetting['helpdesk_blacklist', @project.id].split("\n") | [@issue.customer.primary_email.strip]).join("\n")
|
||||
@customer.tickets.map(&:destroy)
|
||||
@customer.destroy
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_back_or_default(:controller => 'issues', :action => 'index', :project_id => @project) }
|
||||
format.api { render_api_ok }
|
||||
end
|
||||
|
||||
else
|
||||
deny_access
|
||||
end
|
||||
end
|
||||
|
||||
def email_note
|
||||
raise Exception, "Param 'message' should be set" unless params[:message]
|
||||
@issue = Issue.find(params[:message][:issue_id])
|
||||
|
||||
raise Exception, "Issue with ID: #{params[:message][:issue_id].to_i} should be present and relate to customer" if @issue.nil? || @issue.customer.nil?
|
||||
|
||||
@journal = @issue.init_journal(User.current)
|
||||
if params[:message][:status_id].present?
|
||||
issue_status = IssueStatus.where(id: params[:message][:status_id]).first
|
||||
@issue.status_id = issue_status.id if issue_status
|
||||
end
|
||||
@journal.notes = params[:message][:content]
|
||||
@issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
|
||||
@issue.save!
|
||||
|
||||
contact = @issue.customer
|
||||
|
||||
if HelpdeskMailer.issue_response(contact, @journal, params)
|
||||
|
||||
@journal_message = JournalMessage.create(:from_address => '',
|
||||
:to_address => contact.primary_email.downcase,
|
||||
:is_incoming => false,
|
||||
:message_date => Time.now,
|
||||
:contact => contact,
|
||||
:journal => @journal)
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.api { render :action => 'show', :status => :created }
|
||||
end
|
||||
rescue Exception => e
|
||||
respond_to do |format|
|
||||
format.api do
|
||||
@error_messages = [e.message]
|
||||
render template: 'common/error_messages', format: [:api], status: :unprocessable_entity, layout: nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def create_ticket
|
||||
raise Exception, "Param 'ticket' should be set" if params[:ticket].blank?
|
||||
@issue = Issue.new
|
||||
@issue.project = @project
|
||||
@issue.author ||= User.current
|
||||
@issue.safe_attributes = params[:ticket][:issue]
|
||||
raise Exception, 'Contact should have email address' unless params[:ticket][:contact] || params[:ticket][:contact][:email]
|
||||
|
||||
@contact = Contact.find_by_emails([params[:ticket][:contact][:email]]).first
|
||||
unless @contact
|
||||
@contact = Contact.new
|
||||
@contact.safe_attributes = params[:ticket][:contact]
|
||||
end
|
||||
@contact.projects << @project unless @contact.projects.include?(@project)
|
||||
|
||||
helpdesk_ticket = HelpdeskTicket.new(:from_address => @contact.primary_email,
|
||||
:to_address => '',
|
||||
:ticket_date => Time.now,
|
||||
:customer => @contact,
|
||||
:is_incoming => true,
|
||||
:issue => @issue,
|
||||
:source => HelpdeskTicket::HELPDESK_WEB_SOURCE)
|
||||
|
||||
@issue.helpdesk_ticket = helpdesk_ticket
|
||||
@issue.assigned_to = @contact.find_assigned_user(@project, @issue.assigned_to)
|
||||
@issue.save_attachments(params[:attachments] || (params[:ticket][:issue] && params[:ticket][:issue][:uploads]))
|
||||
|
||||
if @issue.save
|
||||
process_helpdesk_send_as(process_helpdesk_send_as(params[:ticket][:send_as]))
|
||||
HelpdeskMailer.auto_answer(@contact, @issue) if HelpdeskSettings['helpdesk_send_notification', @project].to_i > 0
|
||||
HelpdeskTicket.autoclose(@project)
|
||||
respond_to do |format|
|
||||
format.api { redirect_on_create(params) }
|
||||
end
|
||||
else
|
||||
raise Exception, "Can't create issue: #{@issue.errors.full_messages}"
|
||||
end
|
||||
rescue Exception => e
|
||||
respond_to do |format|
|
||||
format.api do
|
||||
@error_messages = [e.message]
|
||||
HelpdeskLogger.error "API Create Ticket Error: #{e.message}" if HelpdeskLogger
|
||||
render template: 'common/error_messages', format: [:api], status: :unprocessable_entity, layout: nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def get_mail
|
||||
set_settings
|
||||
|
||||
msg_count = HelpdeskMailer.check_project(@project.id)
|
||||
|
||||
respond_to do |format|
|
||||
format.js do
|
||||
@message = "<div class='flash notice'> #{l(:label_helpdesk_get_mail_success, :count => msg_count)} </div>"
|
||||
flash.discard
|
||||
end
|
||||
format.html { redirect_to :back }
|
||||
end
|
||||
rescue Exception => e
|
||||
respond_to do |format|
|
||||
format.js do
|
||||
@message = "<div class='flash error'> Error: #{e.message} </div>"
|
||||
Rails.logger.error "Helpdesk MailHandler Error: #{e.message}" if Rails.logger && Rails.logger.error
|
||||
flash.discard
|
||||
end
|
||||
format.html { redirect_to :back }
|
||||
end
|
||||
end
|
||||
|
||||
def update_customer_email
|
||||
@journal = Journal.find(params[:journal_id])
|
||||
@issue = @journal.journalized
|
||||
@project = @issue.project
|
||||
@display = HelpdeskSettings[:send_note_by_default, @project] ? 'inline' : 'none'
|
||||
if @journal.is_incoming?
|
||||
@contact = @journal.contact
|
||||
@email = @journal.journal_message.from_address
|
||||
from_address = HelpdeskSettings['helpdesk_answer_from', @issue.project].blank? ? Setting.mail_from : HelpdeskSettings['helpdesk_answer_from', @issue.project]
|
||||
@cc_emails = (@issue.helpdesk_ticket.cc_addresses + @journal.journal_message.cc_address.split(',') - [@email, from_address]).uniq
|
||||
else
|
||||
@contact = @issue.helpdesk_ticket.last_reply_customer
|
||||
@email = @issue.helpdesk_ticket.default_to_address
|
||||
@cc_emails = @issue.helpdesk_ticket.cc_addresses - [@email]
|
||||
end
|
||||
end
|
||||
|
||||
def message_link
|
||||
viewable_object = JournalMessage.not_viewed.where(view_id: params[:vid]).first
|
||||
viewable_object ||= HelpdeskTicket.not_viewed.where(view_id: params[:vid]).first
|
||||
viewable_object.update_attribute(:viewed_on, Time.zone.now) if viewable_object
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_project
|
||||
project_id = params[:project_id] || (params[:ticket] && params[:ticket][:issue] && params[:ticket][:issue][:project_id])
|
||||
@project = Project.find(project_id)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
def set_settings
|
||||
set_settings_param('helpdesk_answer_from')
|
||||
set_settings_param('helpdesk_cc_address')
|
||||
set_settings_param('helpdesk_bcc_address')
|
||||
set_settings_param('helpdesk_send_notification')
|
||||
set_settings_param('helpdesk_is_not_create_contacts')
|
||||
set_settings_param('helpdesk_created_contact_tag')
|
||||
set_settings_param('helpdesk_blacklist')
|
||||
set_settings_param('helpdesk_emails_header')
|
||||
set_settings_param('helpdesk_answer_subject')
|
||||
set_settings_param('helpdesk_first_answer_subject')
|
||||
set_settings_param('helpdesk_first_answer_template')
|
||||
set_settings_param('helpdesk_emails_footer')
|
||||
set_settings_param('helpdesk_answered_status')
|
||||
set_settings_param('helpdesk_reopen_status')
|
||||
set_settings_param('helpdesk_tracker')
|
||||
set_settings_param('helpdesk_assigned_to')
|
||||
set_settings_param('helpdesk_lifetime')
|
||||
set_settings_param('helpdesk_autoclose_subject')
|
||||
set_settings_param('helpdesk_autoclose_template')
|
||||
|
||||
|
||||
set_settings_param(:helpdesk_protocol)
|
||||
set_settings_param(:helpdesk_host)
|
||||
set_settings_param(:helpdesk_port)
|
||||
set_settings_param(:helpdesk_password)
|
||||
set_settings_param(:helpdesk_username)
|
||||
|
||||
set_settings_param(:helpdesk_use_ssl)
|
||||
set_settings_param(:helpdesk_use_starttls)
|
||||
set_settings_param(:helpdesk_imap_folder)
|
||||
set_settings_param(:helpdesk_move_on_success)
|
||||
set_settings_param(:helpdesk_move_on_failure)
|
||||
set_settings_param(:helpdesk_apop)
|
||||
set_settings_param(:helpdesk_delete_unprocessed)
|
||||
|
||||
set_settings_param(:helpdesk_smtp_use_default_settings)
|
||||
set_settings_param(:helpdesk_smtp_server)
|
||||
set_settings_param(:helpdesk_smtp_domain)
|
||||
set_settings_param(:helpdesk_smtp_port)
|
||||
set_settings_param(:helpdesk_smtp_authentication)
|
||||
set_settings_param(:helpdesk_smtp_username)
|
||||
set_settings_param(:helpdesk_smtp_password)
|
||||
set_settings_param(:helpdesk_smtp_tls)
|
||||
set_settings_param(:helpdesk_smtp_ssl)
|
||||
set_settings_param(:helpdesk_smtp_skip_verification)
|
||||
|
||||
set_settings_param(:helpdesk_outlook_folder)
|
||||
set_settings_param(:helpdesk_outlook_move_on_success)
|
||||
set_settings_param(:helpdesk_outlook_move_on_failure)
|
||||
set_settings_param(:helpdesk_outlook_tenant_id)
|
||||
set_settings_param(:helpdesk_send_protocol)
|
||||
|
||||
set_settings_param(:helpdesk_google_folder)
|
||||
set_settings_param(:helpdesk_google_move_on_success)
|
||||
set_settings_param(:helpdesk_google_move_on_failure)
|
||||
end
|
||||
|
||||
def set_settings_param(param)
|
||||
if param == 'helpdesk_created_contact_tag'
|
||||
if params[param].nil?
|
||||
params.delete(param)
|
||||
else
|
||||
params[param] = params[param].blank? ? '' : params[param].reject(&:empty?).join(',')
|
||||
end
|
||||
end
|
||||
|
||||
if param == :helpdesk_password || param == :helpdesk_smtp_password
|
||||
ContactsSetting[param, @project.id] = params[param] if params[param] && !params[param].blank?
|
||||
else
|
||||
ContactsSetting[param, @project.id] = params[param].strip if params[param]
|
||||
end
|
||||
end
|
||||
|
||||
def redirect_on_create(options)
|
||||
if options[:redirect_on_success].to_s.match('^(http|https):\/\/')
|
||||
redirect_to options[:redirect_on_success].to_s
|
||||
else
|
||||
response_key = :plain
|
||||
render response_key => "Issue #{@issue.id} created", :status => :created, :location => issue_url(@issue)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
109
app/controllers/helpdesk_mail_rules_controller.rb
Normal file
109
app/controllers/helpdesk_mail_rules_controller.rb
Normal file
@ -0,0 +1,109 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskMailRulesController < ApplicationController
|
||||
helper :helpdesk
|
||||
|
||||
before_action :find_helpdesk_mail_rule, only: [:edit, :update, :destroy]
|
||||
before_action :require_admin
|
||||
|
||||
def new
|
||||
@helpdesk_mail_rule = HelpdeskMailRule.new
|
||||
end
|
||||
|
||||
def create
|
||||
@helpdesk_mail_rule = HelpdeskMailRule.new
|
||||
@helpdesk_mail_rule.safe_attributes = mail_rule_params
|
||||
@helpdesk_mail_rule.user = User.current
|
||||
|
||||
if @helpdesk_mail_rule.save
|
||||
flash[:notice] = l(:notice_successful_create)
|
||||
redirect_to_settings_tab
|
||||
else
|
||||
render action: 'new', layout: !request.xhr?
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
@helpdesk_mail_rule.safe_attributes = mail_rule_params
|
||||
|
||||
if @helpdesk_mail_rule.save
|
||||
@helpdesk_mail_rule.insert_at(safe_params['position']) if safe_params['position'].present?
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
flash[:notice] = l(:notice_successful_update)
|
||||
redirect_to_settings_tab
|
||||
end
|
||||
format.js { head 200 }
|
||||
end
|
||||
else
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
render action: 'edit'
|
||||
end
|
||||
format.js { head 422 }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@helpdesk_mail_rule.destroy
|
||||
redirect_to_settings_tab
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def safe_params
|
||||
@safe_params ||= params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash : params
|
||||
end
|
||||
|
||||
def mail_rule_params
|
||||
rule_params = {}
|
||||
if safe_params['f'].is_a?(Array) && (safe_params['v'].nil? || safe_params['v'].is_a?(Hash))
|
||||
rule_params['conditions'] = {}
|
||||
safe_params['f'].each do |field|
|
||||
next if field.blank?
|
||||
rule_params['conditions'][field] = { operator: safe_params['op'].try(:[], field), values: (safe_params['v'].try(:[], field) || ['']) }
|
||||
end
|
||||
end
|
||||
if safe_params['a'].is_a?(Array) && (safe_params['v'].nil? || safe_params['v'].is_a?(Hash))
|
||||
rule_params['actions'] = {}
|
||||
safe_params['a'].each do |action|
|
||||
next if action.blank?
|
||||
rule_params['actions'][action] = { operator: safe_params['op'].try(:[], action), values: (safe_params['v'].try(:[], action) || ['']) }
|
||||
end
|
||||
end
|
||||
rule_params['mail_type'] = safe_params['mail_type'] if safe_params['mail_type'].present?
|
||||
rule_params['move_to'] = safe_params['move_to'] if safe_params['move_to'].present?
|
||||
rule_params
|
||||
end
|
||||
|
||||
def find_helpdesk_mail_rule
|
||||
@helpdesk_mail_rule = HelpdeskMailRule.find(params[:id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
def redirect_to_settings_tab
|
||||
redirect_to plugin_settings_path(id: :redmine_contacts_helpdesk, tab: :email_rules)
|
||||
end
|
||||
end
|
62
app/controllers/helpdesk_mailer_controller.rb
Normal file
62
app/controllers/helpdesk_mailer_controller.rb
Normal file
@ -0,0 +1,62 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskMailerController < ActionController::Base
|
||||
before_action :check_credential
|
||||
|
||||
# Submits an incoming email to ContactsMailer
|
||||
def index
|
||||
options = params.dup
|
||||
if options[:issue].present?
|
||||
project = Project.where(identifier: options[:issue][:project]).first
|
||||
options = HelpdeskMailSupport.issue_options(options, project.id) if project
|
||||
end
|
||||
email = options.delete(:email)
|
||||
if HelpdeskMailer.receive(email, options)
|
||||
head :created
|
||||
else
|
||||
head :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def get_mail
|
||||
msg_count = 0
|
||||
errors = []
|
||||
projects = Project.active.has_module(:contacts_helpdesk)
|
||||
projects = projects.where(:id => params[:project_id]) if params[:project_id].present?
|
||||
projects.each do |project|
|
||||
begin
|
||||
msg_count += HelpdeskMailer.check_project(project.id)
|
||||
rescue Exception => e
|
||||
errors << e.message
|
||||
end
|
||||
end
|
||||
response_key = :plain
|
||||
render :status => :ok, response_key => { :count => msg_count, :errors => errors }.to_json
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_credential
|
||||
User.current = nil
|
||||
unless Setting.mail_handler_api_enabled? && params[:key].to_s == Setting.mail_handler_api_key
|
||||
render plain: 'Access denied. Incoming emails WS is disabled or key is invalid.', status: 403
|
||||
end
|
||||
end
|
||||
end
|
87
app/controllers/helpdesk_oauth_controller.rb
Normal file
87
app/controllers/helpdesk_oauth_controller.rb
Normal file
@ -0,0 +1,87 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskOauthController < HelpdeskBaseController
|
||||
before_action :find_project, except: [:resp]
|
||||
before_action :authorize, except: [:resp]
|
||||
|
||||
def auth
|
||||
ContactsSetting[:helpdesk_protocol, @project.id] = params[:provider]
|
||||
oauth_provider = build_oauth_provider(params[:provider], @project.id)
|
||||
return render_404 unless oauth_provider
|
||||
|
||||
redirect_to oauth_provider.authorize_url(generate_state_secret)
|
||||
end
|
||||
|
||||
def auth_remove
|
||||
oauth_provider = build_oauth_provider(params[:provider], @project.id)
|
||||
return render_404 unless oauth_provider
|
||||
|
||||
oauth_provider.reset_token
|
||||
|
||||
redirect_to settings_project_path(@project, tab: 'helpdesk')
|
||||
end
|
||||
|
||||
def resp
|
||||
secret_data = extract_secret_data
|
||||
return render_404 unless secret_data
|
||||
|
||||
oauth_provider = build_oauth_provider(secret_data[:provider], secret_data[:project])
|
||||
oauth_provider.receive_tokens(params[:code])
|
||||
|
||||
redirect_to settings_project_path(secret_data[:project], tab: 'helpdesk')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_oauth_provider(provider, project_id)
|
||||
oauth_provider = HelpdeskOauthProvider.find_by_protocol(provider)
|
||||
return nil unless oauth_provider
|
||||
|
||||
oauth_provider.new(project_id)
|
||||
end
|
||||
|
||||
def find_project
|
||||
@project = Project.find_by(identifier: params[:project])
|
||||
render_404 unless @project
|
||||
end
|
||||
|
||||
def generate_state_secret
|
||||
secret = SecureRandom.uuid
|
||||
secret_data = { 'project' => @project.id, 'provider' => params[:provider] }
|
||||
|
||||
state_secrets = RedmineHelpdesk.settings['helpdesk_oauth_state_secrets'] || {}
|
||||
old_secret = state_secrets.key(secret_data)
|
||||
state_secrets.delete(old_secret) if old_secret
|
||||
RedmineHelpdesk.settings = { 'helpdesk_oauth_state_secrets' => state_secrets.merge(secret => secret_data) }
|
||||
|
||||
secret
|
||||
end
|
||||
|
||||
def extract_secret_data
|
||||
state_secrets = RedmineHelpdesk.settings['helpdesk_oauth_state_secrets'] || {}
|
||||
data = state_secrets[params[:state]]
|
||||
return unless data
|
||||
|
||||
state_secrets.delete(params[:state])
|
||||
RedmineHelpdesk.settings = { 'helpdesk_oauth_state_secrets' => state_secrets }
|
||||
|
||||
data
|
||||
end
|
||||
end
|
69
app/controllers/helpdesk_reports_controller.rb
Normal file
69
app/controllers/helpdesk_reports_controller.rb
Normal file
@ -0,0 +1,69 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskReportsController < ApplicationController
|
||||
menu_item :issues
|
||||
|
||||
helper :helpdesk
|
||||
helper :queries
|
||||
include QueriesHelper
|
||||
|
||||
before_action :find_optional_project
|
||||
before_action :authorize_global
|
||||
before_action :find_query
|
||||
|
||||
def show
|
||||
return render_404 unless @query
|
||||
|
||||
if @query.valid?
|
||||
@collector = HelpdeskDataCollectorManager.new.collect_data(@query)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_query
|
||||
report_query_class =
|
||||
case params[:report]
|
||||
when 'first_response_time'
|
||||
HelpdeskReportsFirstResponseQuery
|
||||
when 'busiest_time_of_day'
|
||||
HelpdeskReportsBusiestTimeQuery
|
||||
when 'customer_satisfaction'
|
||||
HelpdeskReportsCustomerSatisfactionQuery
|
||||
end
|
||||
return unless report_query_class
|
||||
|
||||
if params[:set_filter] || session["helpdesk_reports_#{params[:report]}"].blank?
|
||||
@query = report_query_class
|
||||
.new(:name => params[:report], :project => @project)
|
||||
.build_from_params(params)
|
||||
|
||||
session["helpdesk_reports_#{params[:report]}"] = {
|
||||
:project_id => @query.project_id,
|
||||
:filters => @query.filters
|
||||
}
|
||||
else
|
||||
@query = report_query_class.new(
|
||||
:name => params[:report],
|
||||
:project => @project,
|
||||
:filters => session["helpdesk_reports_#{params[:report]}"][:filters])
|
||||
end
|
||||
end
|
||||
end
|
291
app/controllers/helpdesk_tickets_controller.rb
Normal file
291
app/controllers/helpdesk_tickets_controller.rb
Normal file
@ -0,0 +1,291 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskTicketsController < HelpdeskBaseController
|
||||
before_action :find_issue, except: [:index, :create, :bulk_edit_reply, :bulk_update_reply, :preview_bulk_reply]
|
||||
before_action :find_project, except: [:index, :bulk_edit_reply, :bulk_update_reply, :preview_bulk_reply]
|
||||
before_action :authorize, except: [:index, :preview_bulk_reply, :bulk_edit_reply, :bulk_update_reply]
|
||||
before_action :find_helpdesk_ticket, only: [:show, :edit, :update, :destroy]
|
||||
before_action :find_optional_project, only: [:index]
|
||||
before_action :find_issues, only: [:bulk_edit_reply, :bulk_update_reply]
|
||||
|
||||
accept_api_auth :index, :create, :show, :update, :destroy
|
||||
|
||||
helper :helpdesk
|
||||
helper :helpdesk_api
|
||||
helper :attachments
|
||||
helper :queries
|
||||
include QueriesHelper
|
||||
|
||||
def index
|
||||
retrieve_query(HelpdeskTicketQuery)
|
||||
|
||||
if @query.valid?
|
||||
respond_to do |format|
|
||||
format.api {
|
||||
@offset, @limit = api_offset_and_limit
|
||||
@tickets_count = @query.tickets_count
|
||||
@tickets = @query.tickets(offset: @offset, limit: @limit)
|
||||
Issue.load_visible_relations(@issues) if include_in_api_response?('relations')
|
||||
}
|
||||
format.any { head 200 }
|
||||
end
|
||||
else
|
||||
respond_to do |format|
|
||||
format.any(:atom, :csv, :pdf) { head 422 }
|
||||
format.api { render_validation_errors(@query) }
|
||||
end
|
||||
end
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
def create
|
||||
return unless validate_create_params
|
||||
|
||||
@issue = Issue.new
|
||||
@issue.project = @project
|
||||
@issue.author ||= User.current
|
||||
@issue.safe_attributes = params[:helpdesk_ticket][:issue]
|
||||
|
||||
@contact = Contact.find_by_emails([params[:helpdesk_ticket][:contact][:email]]).first
|
||||
unless @contact
|
||||
@contact = Contact.new
|
||||
@contact.safe_attributes = params[:helpdesk_ticket][:contact]
|
||||
end
|
||||
@contact.projects << @project unless @contact.projects.include?(@project)
|
||||
|
||||
@helpdesk_ticket = HelpdeskTicket.new(ticket_date: Time.now, customer: @contact, is_incoming: true, issue: @issue)
|
||||
@helpdesk_ticket.safe_attributes = params[:helpdesk_ticket].except(:issue, :contact)
|
||||
@helpdesk_ticket.from_address = @contact.primary_email
|
||||
@helpdesk_ticket.source = find_ticket_source
|
||||
|
||||
@issue.helpdesk_ticket = @helpdesk_ticket
|
||||
@issue.assigned_to = @contact.find_assigned_user(@project, @issue.assigned_to)
|
||||
@issue.save_attachments(params[:helpdesk_ticket][:attachments] || (params[:helpdesk_ticket][:issue] && params[:helpdesk_ticket][:issue][:uploads]))
|
||||
|
||||
if @issue.save
|
||||
process_helpdesk_send_as(params[:helpdesk_ticket][:send_as])
|
||||
HelpdeskTicket.autoclose(@project)
|
||||
return render_api_view(action: :create, status: :created)
|
||||
else
|
||||
return render_api_error(message: @issue.errors.full_messages)
|
||||
end
|
||||
rescue Exception => e
|
||||
render_api_error(message: "#{e.message} (#{e.backtrace.first})")
|
||||
end
|
||||
|
||||
def show
|
||||
@show_form = 'false'
|
||||
respond_to do |format|
|
||||
format.js
|
||||
format.api
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
@helpdesk_ticket = @issue.helpdesk_ticket || @issue.build_helpdesk_ticket(ticket_date: Time.now)
|
||||
|
||||
@show_form = 'true'
|
||||
respond_to do |format|
|
||||
format.js
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@helpdesk_ticket.safe_attributes = params[:helpdesk_ticket]
|
||||
if params[:helpdesk_ticket][:cc_address]
|
||||
cc_address = params[:helpdesk_ticket][:cc_address]
|
||||
cc_address = cc_address.reject(&:empty?).join(',') if cc_address.is_a?(Array)
|
||||
@helpdesk_ticket.cc_address = cc_address
|
||||
end
|
||||
if params[:helpdesk_ticket][:from_address].present?
|
||||
from_param = params[:helpdesk_ticket][:from_address]
|
||||
if from_param =~ /^\d+$/
|
||||
customer = Contact.where(id: from_param).first
|
||||
@helpdesk_ticket.customer = customer
|
||||
@helpdesk_ticket.from_address = customer.try(:primary_email)
|
||||
else
|
||||
@helpdesk_ticket.customer = HelpdeskMailSupport.create_contact_from_address(from_param, from_param, @project)
|
||||
end
|
||||
else
|
||||
@helpdesk_ticket.from_address = @helpdesk_ticket.customer.try(:primary_email)
|
||||
end
|
||||
@helpdesk_ticket.source = find_ticket_source if params[:helpdesk_ticket][:source]
|
||||
|
||||
if @helpdesk_ticket.save
|
||||
flash[:notice] = l(:notice_successful_update)
|
||||
respond_to do |format|
|
||||
format.html { redirect_back_or_default(issue_path(@issue)) }
|
||||
format.api { render_api_view(action: :show, status: 200) }
|
||||
end
|
||||
else
|
||||
flash[:error] = @helpdesk_ticket.errors.full_messages.flatten.join("\n")
|
||||
respond_to do |format|
|
||||
format.html { redirect_back_or_default(issue_path(@issue)) }
|
||||
format.api { render_api_error(message: @issue.errors.full_messages) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if @helpdesk_ticket.destroy
|
||||
flash[:notice] = l(:notice_successful_delete)
|
||||
respond_to do |format|
|
||||
format.html { redirect_back_or_default(issue_path(@issue)) }
|
||||
format.api { render_api_view(status: :no_content) }
|
||||
end
|
||||
else
|
||||
flash[:error] = l(:notice_unsuccessful_save)
|
||||
respond_to do |format|
|
||||
format.html { redirect_back_or_default(issue_path(@issue)) }
|
||||
format.api { render_api_error(message: @helpdesk_ticket.errors.full_messages) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def bulk_edit_reply
|
||||
@project = Project.find(params[:project_id])
|
||||
@contacts = @issues.map { |issue| issue.helpdesk_ticket.try(:contact_id) }
|
||||
|
||||
unless @issues.all?(&:is_ticket?)
|
||||
render_404
|
||||
else
|
||||
render 'bulk_reply'
|
||||
end
|
||||
end
|
||||
|
||||
def bulk_update_reply
|
||||
unsaved_issues = []
|
||||
|
||||
@issues.each do |issue|
|
||||
journal = issue.init_journal(User.current, params[:notes])
|
||||
|
||||
to_address = issue.helpdesk_ticket.default_to_address
|
||||
cc = params[:journal_message][:cc_address]
|
||||
bcc = params[:journal_message][:bcc_address]
|
||||
|
||||
options = params.merge(journal_message: { to_address: [to_address], cc_address: [cc], bcc_address: [bcc] })
|
||||
|
||||
if issue.helpdesk_ticket && params[:helpdesk] && params[:helpdesk][:is_send_mail].present? && User.current.allowed_to?(:send_response, issue.project)
|
||||
HelpdeskTicket.send_reply_by_issue(issue, options)
|
||||
end
|
||||
|
||||
unsaved_issues << issue unless issue.save
|
||||
end
|
||||
|
||||
if unsaved_issues.empty?
|
||||
flash[:notice] = l(:notice_successful_update)
|
||||
redirect_back_or_default(bulk_edit_reply_helpdesk_tickets_path(ids: @issues.ids))
|
||||
else
|
||||
bulk_edit_reply
|
||||
render :action => 'bulk_edit_reply'
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def preview_bulk_reply
|
||||
contact = params[:ids].present? ? Contact.visible.where(id: params[:ids][0]).first : nil
|
||||
@text = contact.nil? ? params[:text] : mail_macro(contact, params[:text])
|
||||
render :partial => 'common/preview'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_issue
|
||||
# TODO: Remove params[:issue_id] when new API will be applied
|
||||
@issue = Issue.find(params[:issue_id] || params[:id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_api_error(message: 'Issue not found', status: :not_found)
|
||||
end
|
||||
|
||||
def find_issues
|
||||
@issues = Issue.where(id: (params[:ids] || params[:issue]))
|
||||
render_404 if @issues.empty?
|
||||
end
|
||||
|
||||
def find_project
|
||||
return @project = @issue.project if @issue
|
||||
|
||||
project_id = params[:helpdesk_ticket] && params[:helpdesk_ticket][:issue] && params[:helpdesk_ticket][:issue][:project_id]
|
||||
@project = Project.find_by_param(project_id)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
return render_api_error(message: 'Project not found', status: :not_found) unless @project
|
||||
end
|
||||
|
||||
def find_helpdesk_ticket
|
||||
@helpdesk_ticket = @issue.helpdesk_ticket
|
||||
@helpdesk_ticket ||= @issue.build_helpdesk_ticket(ticket_date: Time.now) unless request.format.api?
|
||||
return render_api_error(message: 'Helpdesk ticket not found', status: :not_found) unless @helpdesk_ticket
|
||||
end
|
||||
|
||||
def validate_create_params
|
||||
return render_api_error(message: 'HelpdeskTicket params cannot be blank') if params[:helpdesk_ticket].blank?
|
||||
return render_api_error(message: 'Issue params cannot be blank') if params[:helpdesk_ticket][:issue].blank?
|
||||
return render_api_error(message: 'Contact params cannot be blank') if params[:helpdesk_ticket][:contact].blank?
|
||||
return render_api_error(message: 'Contact should have email address') if params[:helpdesk_ticket][:contact][:email].blank?
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def find_ticket_source
|
||||
case params[:helpdesk_ticket][:source]
|
||||
when 'email', '0'
|
||||
HelpdeskTicket::HELPDESK_EMAIL_SOURCE
|
||||
when 'web', '1'
|
||||
HelpdeskTicket::HELPDESK_WEB_SOURCE
|
||||
when 'phone', '2'
|
||||
HelpdeskTicket::HELPDESK_PHONE_SOURCE
|
||||
when 'twitter', '3'
|
||||
HelpdeskTicket::HELPDESK_TWITTER_SOURCE
|
||||
when 'conversation', '4'
|
||||
HelpdeskTicket::HELPDESK_CONVERSATION_SOURCE
|
||||
else
|
||||
HelpdeskTicket::HELPDESK_WEB_SOURCE
|
||||
end
|
||||
end
|
||||
|
||||
def render_api_view(opts)
|
||||
status = opts[:status] || 200
|
||||
respond_to do |format|
|
||||
format.api do
|
||||
return head status if opts[:action].blank?
|
||||
render action: opts[:action], status: status
|
||||
end
|
||||
format.any { head status }
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
def render_api_error(opts)
|
||||
@message = opts[:message]
|
||||
@error_messages = [opts[:message]]
|
||||
@status = opts[:status] || :unprocessable_entity
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
render_404
|
||||
end
|
||||
format.api do
|
||||
render template: 'common/error_messages', format: [:api], status: @status
|
||||
end
|
||||
format.any { head @status }
|
||||
end
|
||||
false
|
||||
end
|
||||
end
|
63
app/controllers/helpdesk_votes_controller.rb
Normal file
63
app/controllers/helpdesk_votes_controller.rb
Normal file
@ -0,0 +1,63 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskVotesController < ApplicationController
|
||||
layout 'public_tickets'
|
||||
skip_before_action :check_if_login_required
|
||||
before_action :find_ticket, :authorize_ticket
|
||||
before_action :fill_data
|
||||
|
||||
helper :issues
|
||||
|
||||
def vote
|
||||
@ticket.update_vote(params[:vote], params[:vote_comment]) if params[:vote]
|
||||
end
|
||||
|
||||
def fast_vote
|
||||
if RedmineHelpdesk.vote_comment_allow?
|
||||
@ticket.vote = params[:vote] if params[:vote]
|
||||
render :action => 'show'
|
||||
else
|
||||
@ticket.update_vote(params[:vote]) if params[:vote]
|
||||
render :action => 'vote'
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_ticket
|
||||
@ticket = HelpdeskTicket.find(params[:id])
|
||||
@issue = @ticket.issue
|
||||
@project = @issue.project
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
def authorize_ticket(_action = params[:action])
|
||||
allow = true
|
||||
allow &&= (@ticket.token == params[:hash]) && RedmineHelpdesk.vote_allow?
|
||||
allow &&= !@issue.is_private
|
||||
render_404 unless allow
|
||||
end
|
||||
|
||||
def fill_data
|
||||
@previous_tickets = @ticket.customer.tickets.where(:is_private => false).includes([:status, :helpdesk_ticket]).order_by_status
|
||||
@total_spent_hours = @previous_tickets.map.sum(&:total_spent_hours)
|
||||
end
|
||||
end
|
251
app/controllers/helpdesk_widget_controller.rb
Normal file
251
app/controllers/helpdesk_widget_controller.rb
Normal file
@ -0,0 +1,251 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskWidgetController < ApplicationController
|
||||
layout false
|
||||
helper :custom_fields
|
||||
protect_from_forgery :except => [:widget, :animation, :load_form, :load_custom_fields, :avatar, :create_ticket, :iframe]
|
||||
skip_before_action :check_if_login_required, :only => [:widget, :animation, :load_form, :load_custom_fields, :avatar, :create_ticket, :iframe]
|
||||
|
||||
before_action :prepare_data, :only => [:load_custom_fields, :create_ticket]
|
||||
after_action :set_access_control_header
|
||||
|
||||
def load_form
|
||||
render :json => schema.to_json
|
||||
end
|
||||
|
||||
def load_custom_fields
|
||||
@issue = @project.issues.build(:tracker => @tracker) if @tracker
|
||||
@enabled_cf = HelpdeskSettings["helpdesk_widget_available_custom_fields", nil]
|
||||
end
|
||||
|
||||
def avatar
|
||||
user = User.where(:login => params[:login]).first
|
||||
return render :nothing => true, :status => 404 unless user
|
||||
if user.try(:avatar).nil?
|
||||
avatar_thumb, avatar_type = gravatar_avatar(user) if Setting.gravatar_enabled?
|
||||
else
|
||||
avatar_thumb, avatar_type = local_avatar(user.avatar)
|
||||
end
|
||||
return render :nothing => true, :status => 404 unless avatar_thumb
|
||||
send_avatar(avatar_thumb, avatar_type)
|
||||
end
|
||||
|
||||
def send_avatar(avatar_thumb, avatar_type)
|
||||
send_file avatar_thumb, :filename => (request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(params[:login]) : params[:login]),
|
||||
:type => avatar_type,
|
||||
:disposition => 'inline'
|
||||
end
|
||||
|
||||
def gravatar_avatar(user)
|
||||
email = user.mail if user.respond_to?(:mail)
|
||||
email = user.to_s[/<(.+?)>/, 1] unless email
|
||||
return [nil, nil] unless email
|
||||
default = Setting.gravatar_default ? CGI::escape(Setting.gravatar_default) : ''
|
||||
temp_file = Tempfile.new([user.login, '.jpeg'])
|
||||
temp_file.binmode
|
||||
gravatar_url = Redmine::Configuration['avatar_server_url'] || 'http://www.gravatar.com'
|
||||
open("#{gravatar_url}/avatar/#{Digest::MD5.hexdigest(email)}?rating=PG&size=54&default=#{default}") do |url_file|
|
||||
temp_file.write(url_file.read)
|
||||
end
|
||||
temp_file.rewind
|
||||
[temp_file, 'image/jpeg']
|
||||
end
|
||||
|
||||
def local_avatar(user_avatar)
|
||||
return nil unless user_avatar.readable? || user_avatar.thumbnailable?
|
||||
if (defined?(RedmineContacts::Thumbnail) == 'constant') && Redmine::Thumbnail.convert_available?
|
||||
target = File.join(user_avatar.class.thumbnails_storage_path, "#{user_avatar.id}_#{user_avatar.digest}_54x54.thumb")
|
||||
thumbnail = RedmineContacts::Thumbnail.generate(user_avatar.diskfile, target, '54x54')
|
||||
elsif Redmine::Thumbnail.convert_available?
|
||||
thumbnail = user_avatar.thumbnail(:size => '54x54')
|
||||
else
|
||||
thumbnail = user_avatar.diskfile
|
||||
end
|
||||
[thumbnail, detect_content_type(user_avatar)]
|
||||
end
|
||||
|
||||
def create_ticket
|
||||
@issue = prepare_issue
|
||||
@issue.helpdesk_ticket = prepare_helpdesk_ticket
|
||||
result =
|
||||
if privacy_accepted? && valid_email? && @issue.save
|
||||
save_attachment(@issue.reload)
|
||||
container = HelpdeskMailContainer.new(widget_message_as_email, logger: logger, contact: @widget_contact, issue: @issue)
|
||||
container.is_new_issue = true
|
||||
HelpdeskMailRule.apply_rules(:incoming, container)
|
||||
HelpdeskMailer.auto_answer(@issue.helpdesk_ticket.customer, @issue) if HelpdeskSettings['helpdesk_send_notification', @project].to_i > 0
|
||||
HelpdeskTicket.autoclose(@project)
|
||||
{ :result => true, :errors => [] }
|
||||
else
|
||||
{ :result => false, :errors => prepared_errors }
|
||||
end
|
||||
render :json => result
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def prepare_data
|
||||
@project = Project.find(params[:project_id])
|
||||
@tracker = @project.trackers.where(:id => params[:tracker_id]).first
|
||||
end
|
||||
|
||||
def schema
|
||||
if HelpdeskSettings['helpdesk_widget_enable', nil].to_i > 0
|
||||
projects = Project.has_module('contacts_helpdesk').where(:id => HelpdeskSettings[:helpdesk_widget_available_projects, nil])
|
||||
else
|
||||
projects = []
|
||||
end
|
||||
data_schema = {}
|
||||
data_schema[:projects] = Hash[projects.map { |project| [project.name.capitalize, project.id] }]
|
||||
data_schema[:projects_data] = {}
|
||||
projects.each do |project|
|
||||
data_schema[:projects_data][project.id] = {}
|
||||
if HelpdeskSettings['helpdesk_tracker', project] && HelpdeskSettings['helpdesk_tracker', project] != 'all'
|
||||
data_schema[:projects_data][project.id][:trackers] = Hash[Tracker.where(id: HelpdeskSettings['helpdesk_tracker', project])
|
||||
.map { |tracker| [tracker.name, tracker.id] }]
|
||||
else
|
||||
data_schema[:projects_data][project.id][:trackers] = Hash[project.trackers.map { |tracker| [tracker.name, tracker.id] }]
|
||||
end
|
||||
end
|
||||
data_schema[:custom_fields] = Hash[IssueCustomField.where(id: HelpdeskSettings['helpdesk_widget_available_custom_fields', nil])
|
||||
.map { |custom_field| [custom_field.name, custom_field.id] }]
|
||||
data_schema[:avatar] = HelpdeskSettings[:helpdesk_widget_avatar_login, nil]
|
||||
data_schema
|
||||
end
|
||||
|
||||
def prepared_errors
|
||||
errors_hash = @issue.errors.dup
|
||||
# Username
|
||||
if errors_hash[:'helpdesk_ticket.customer.first_name'].present?
|
||||
@issue.errors.delete(:'helpdesk_ticket.customer.first_name')
|
||||
@issue.errors.add(:username, errors_hash[:'helpdesk_ticket.customer.first_name'].collect { |error| ['Username', error].join(' ') })
|
||||
end
|
||||
|
||||
# Subject
|
||||
if errors_hash[:subject].present?
|
||||
errors = errors_hash[:subject].collect { |error| ['Subject', error].join(' ') }
|
||||
@issue.errors[:subject].clear
|
||||
@issue.errors.add(:subject, errors)
|
||||
end
|
||||
|
||||
# Description
|
||||
if params[:issue][:description].blank?
|
||||
@issue.errors.add(:description, I18n.t(:label_helpdesk_widget_ticket_error_description))
|
||||
end
|
||||
|
||||
# Nested objects
|
||||
if errors_hash[:'helpdesk_ticket.customer.projects'].present?
|
||||
@issue.errors.delete(:'helpdesk_ticket.customer.projects')
|
||||
end
|
||||
@issue.errors
|
||||
end
|
||||
|
||||
def prepare_issue
|
||||
redmine_user = User.where(id: params[:redmine_user]).first
|
||||
redmine_user ||= User.find_by_mail(params[:email])
|
||||
issue_category = @project.issue_categories.find_by(name: params[:issue_category])
|
||||
issue = @project.issues.build(tracker: @tracker, author: redmine_user.present? ? redmine_user : User.anonymous, category: issue_category)
|
||||
issue.safe_attributes = params[:issue].deep_dup
|
||||
issue.assigned_to = widget_contact.find_assigned_user(@project, HelpdeskSettings['helpdesk_assigned_to', @project])
|
||||
issue
|
||||
end
|
||||
|
||||
def prepare_helpdesk_ticket
|
||||
HelpdeskTicket.new(:from_address => params[:email],
|
||||
:ticket_date => Time.now,
|
||||
:customer => widget_contact,
|
||||
:issue => @issue,
|
||||
:source => HelpdeskTicket::HELPDESK_WEB_SOURCE)
|
||||
end
|
||||
|
||||
def save_attachment(issue)
|
||||
return unless params[:attachment].present?
|
||||
|
||||
attachment_hash = split_base64(params[:attachment])
|
||||
attachment = Attachment.new(file: Base64.decode64(attachment_hash[:data]))
|
||||
attachment.filename = params[:attachment_name] || [Redmine::Utils.random_hex(16), attachment_hash[:extension]].join('.')
|
||||
attachment.content_type = attachment_hash[:type]
|
||||
attachment.author = User.anonymous
|
||||
issue.attachments << attachment
|
||||
issue.save
|
||||
end
|
||||
|
||||
def split_base64(uri)
|
||||
matcher = uri.match(/^data:(.*?)\;(.*?),(.*)$/)
|
||||
{ type: matcher[1],
|
||||
encoder: matcher[2],
|
||||
data: matcher[3],
|
||||
extension: matcher[1].split('/')[1] }
|
||||
end
|
||||
|
||||
def widget_contact
|
||||
return @widget_contact if @widget_contact
|
||||
contacts = Contact.find_by_emails([params[:email]])
|
||||
return @widget_contact = contacts.first if contacts.any?
|
||||
@widget_contact = Contact.new(:email => params[:email])
|
||||
@widget_contact.first_name, @widget_contact.last_name = params[:username].split(' ')
|
||||
@widget_contact.projects << @project
|
||||
@widget_contact
|
||||
end
|
||||
|
||||
def set_access_control_header
|
||||
headers['Access-Control-Allow-Origin'] = '*'
|
||||
headers['X-Frame-Options'] = '*'
|
||||
end
|
||||
|
||||
def valid_email?
|
||||
if params[:email].empty?
|
||||
@issue.errors.add(:email, 'Email cannot be empty')
|
||||
return false
|
||||
elsif params[:email].match(/\A([\w\.\+\-]+)@([\w\-]+\.)+([\w]{2,})\z/i).nil?
|
||||
@issue.errors.add(:email, 'Email is incorrect')
|
||||
return false
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
def detect_content_type(attachment)
|
||||
content_type = attachment.content_type
|
||||
if content_type.blank?
|
||||
content_type = Redmine::MimeType.of(attachment.filename)
|
||||
end
|
||||
content_type.to_s
|
||||
end
|
||||
|
||||
def privacy_accepted?
|
||||
return true unless RedmineHelpdesk.add_widget_privacy_policy_checkbox?
|
||||
|
||||
params[:privacy_policy] == '1'
|
||||
end
|
||||
|
||||
def widget_message_as_email
|
||||
message_sender = params[:email]
|
||||
message_subject = params[:issue][:subject]
|
||||
message_body = params[:issue][:description]
|
||||
|
||||
Mail.new(message_sender, message_subject, message_body) do
|
||||
from message_sender
|
||||
subject message_subject
|
||||
text_part do
|
||||
body message_body
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
117
app/controllers/journal_messages_controller.rb
Normal file
117
app/controllers/journal_messages_controller.rb
Normal file
@ -0,0 +1,117 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class JournalMessagesController < ApplicationController
|
||||
before_action :find_issue
|
||||
before_action :find_project
|
||||
before_action :authorize
|
||||
|
||||
accept_api_auth :create
|
||||
|
||||
helper :helpdesk
|
||||
helper :helpdesk_api
|
||||
helper :attachments
|
||||
|
||||
def create
|
||||
return unless validate_create_params
|
||||
|
||||
journal = @issue.init_journal(User.current)
|
||||
if params[:journal_message][:status_id].present?
|
||||
issue_status = IssueStatus.where(id: params[:journal_message][:status_id]).first
|
||||
@issue.status_id = issue_status.id if issue_status
|
||||
end
|
||||
journal.notes = params[:journal_message][:content]
|
||||
@issue.save_attachments(params[:attachments] || params[:journal_message][:uploads])
|
||||
|
||||
if @issue.save
|
||||
contact = @issue.customer
|
||||
@journal_message = JournalMessage.create!(from_address: params[:journal_message][:from_address],
|
||||
to_address: params[:journal_message][:to_address] || contact.primary_email.downcase,
|
||||
cc_address: params[:journal_message][:cc_address],
|
||||
bcc_address: params[:journal_message][:bcc_address],
|
||||
is_incoming: false,
|
||||
message_date: Time.now,
|
||||
contact: contact,
|
||||
journal: journal)
|
||||
@journal_message.update(message_id: HelpdeskMailer.issue_response(contact, journal, params[:journal_message]).message_id)
|
||||
return render_api_view(action: :create, status: :created)
|
||||
else
|
||||
return render_api_error(message: @issue.errors.full_messages)
|
||||
end
|
||||
rescue Exception => e
|
||||
render_api_error(message: "#{e.message} (#{e.backtrace.first})")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_issue
|
||||
@issue = Issue.find(params[:journal_message] && params[:journal_message][:issue_id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_api_error(message: 'Issue not found', status: :not_found)
|
||||
end
|
||||
|
||||
def find_project
|
||||
@project = @issue.project
|
||||
return render_api_error(message: 'Project not found', status: :not_found) unless @project
|
||||
end
|
||||
|
||||
def validate_create_params
|
||||
return render_api_error(message: 'JournalMessage params cannot be blank') if params[:journal_message].blank?
|
||||
return render_api_error(message: 'Content data cannot be blank') if params[:journal_message][:content].blank?
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def render_api_error(opts)
|
||||
@error_messages = [opts[:message]]
|
||||
@status = opts[:status] || :unprocessable_entity
|
||||
respond_to do |format|
|
||||
format.any { head @status }
|
||||
format.api do
|
||||
render template: 'common/error_messages', format: [:api], status: @status, layout: opts[:layout]
|
||||
end
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
def render_api_view(opts)
|
||||
status = opts[:status] || 200
|
||||
respond_to do |format|
|
||||
format.api do
|
||||
return head status if opts[:action].blank?
|
||||
render action: opts[:action], status: status
|
||||
end
|
||||
format.any { head status }
|
||||
end
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
# {
|
||||
# "journal_message": {
|
||||
# "id": 51,
|
||||
# "from_address": "ar@kodep.ru",
|
||||
# "to_address": "reshetov_test@yandex.ru",
|
||||
# "cc_address": "_viruz_@mail.ru",
|
||||
# "bcc_address": "bcc@mail.ru",
|
||||
# "content": "Hello, I'm message text",
|
||||
# "message_date": "2019-05-29T13:21:58Z",
|
||||
# "message_id": "5cee8776327d8_4d922af2ed82386c68127@notebook.mail"
|
||||
# }
|
||||
# }
|
104
app/controllers/mail_fetcher_controller.rb
Normal file
104
app/controllers/mail_fetcher_controller.rb
Normal file
@ -0,0 +1,104 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class MailFetcherController < ApplicationController
|
||||
require 'timeout'
|
||||
require 'redmine/imap.rb'
|
||||
|
||||
before_action :check_credential
|
||||
|
||||
def receive_imap
|
||||
imap_options = { :host => params['host'],
|
||||
:port => params['port'],
|
||||
:ssl => params['ssl'],
|
||||
:username => params['username'],
|
||||
:password => params['password'],
|
||||
:folder => params['folder'],
|
||||
:move_on_success => params['move_on_success'],
|
||||
:move_on_failure => params['move_on_failure'] }
|
||||
|
||||
options = { :issue => {} }
|
||||
%w(project status tracker category priority).each { |a| options[:issue][a.to_sym] = params[a] if params[a] }
|
||||
options[:allow_override] = params['allow_override'] if params['allow_override']
|
||||
options[:unknown_user] = params['unknown_user'] if params['unknown_user']
|
||||
options[:no_permission_check] = params['no_permission_check'] if params['no_permission_check']
|
||||
|
||||
begin
|
||||
Timeout::timeout(15) { Redmine::IMAP.check(imap_options, options) }
|
||||
rescue Exception => e
|
||||
@error_messages = [e.message]
|
||||
end
|
||||
|
||||
if @error_messages.blank?
|
||||
respond_to do |format|
|
||||
format.html { head :ok }
|
||||
format.api { render_api_ok }
|
||||
end
|
||||
else
|
||||
respond_to do |format|
|
||||
response_key = :plain
|
||||
format.html { render response_key => @error_messages, :status => :unprocessable_entity, :layout => nil }
|
||||
format.api { render template: 'common/error_messages', status: :unprocessable_entity, layout: nil }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def receive_pop3
|
||||
pop_options = { :host => params['host'],
|
||||
:port => params['port'],
|
||||
:apop => params['apop'],
|
||||
:username => params['username'],
|
||||
:password => params['password'],
|
||||
:delete_unprocessed => params['delete_unprocessed'] }
|
||||
|
||||
options = { :issue => {} }
|
||||
%w(project status tracker category priority).each { |a| options[:issue][a.to_sym] = params[a] if params[a] }
|
||||
options[:allow_override] = params['allow_override'] if params['allow_override']
|
||||
options[:unknown_user] = params['unknown_user'] if params['unknown_user']
|
||||
options[:no_permission_check] = params['no_permission_check'] if params['no_permission_check']
|
||||
|
||||
begin
|
||||
Timeout::timeout(15) { Redmine::POP3.check(pop_options, options) }
|
||||
rescue Exception => e
|
||||
@error_messages = [e.message]
|
||||
end
|
||||
|
||||
if @error_messages.blank?
|
||||
respond_to do |format|
|
||||
format.html { render :nothing => true, :status => :ok }
|
||||
format.api { render_api_ok }
|
||||
end
|
||||
else
|
||||
respond_to do |format|
|
||||
format.html { render :text => @error_messages, :status => :unprocessable_entity, :layout => nil }
|
||||
format.api { render template: 'common/error_messages', status: :unprocessable_entity, layout: nil }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_credential
|
||||
User.current = nil
|
||||
unless Setting.mail_handler_api_enabled? && params[:key].to_s == Setting.mail_handler_api_key
|
||||
render :text => 'Access denied. Incoming emails WS is disabled or key is invalid.', :status => 403
|
||||
end
|
||||
end
|
||||
|
||||
end
|
91
app/controllers/public_tickets_controller.rb
Normal file
91
app/controllers/public_tickets_controller.rb
Normal file
@ -0,0 +1,91 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class PublicTicketsController < ApplicationController
|
||||
layout 'public_tickets'
|
||||
|
||||
skip_before_action :check_if_login_required
|
||||
before_action :find_ticket, :authorize_ticket
|
||||
|
||||
helper :issues
|
||||
helper :attachments
|
||||
helper :journals
|
||||
helper :custom_fields
|
||||
|
||||
def show
|
||||
@previous_tickets = @ticket.customer.tickets.where(:is_private => false).includes([:status, :helpdesk_ticket]).order_by_status
|
||||
|
||||
@total_spent_hours = @previous_tickets.map.sum(&:total_spent_hours)
|
||||
|
||||
@journals = @issue.journals.includes(:user).
|
||||
includes(:details).
|
||||
order("#{Journal.table_name}.created_on ASC").
|
||||
where(:private_notes => false).
|
||||
where("EXISTS (SELECT * FROM #{JournalMessage.table_name} WHERE #{JournalMessage.table_name}.journal_id = #{Journal.table_name}.id)")
|
||||
@journals = @journals.each_with_index { |j, i| j.indice = i + 1 }.to_a
|
||||
@journals.reverse! if User.current.wants_comments_in_reverse_order?
|
||||
@journal = @issue.journals.new
|
||||
|
||||
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
|
||||
@edit_allowed = User.current.allowed_to?(:edit_issues, @project)
|
||||
@priorities = IssuePriority.active
|
||||
@time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
|
||||
prepend_view_path 'app/views/issues'
|
||||
end
|
||||
|
||||
def add_comment
|
||||
@journal = @issue.init_journal(User.current, params[:journal][:notes])
|
||||
@issue.status_id = HelpdeskSettings['helpdesk_reopen_status', @issue.project_id] unless HelpdeskSettings['helpdesk_reopen_status', @issue.project_id].blank?
|
||||
@journal.journal_message = JournalMessage.new(:from_address => @ticket.customer_email,
|
||||
:contact => @ticket.customer,
|
||||
:journal => @journal,
|
||||
:is_incoming => true,
|
||||
:message_date => Time.now)
|
||||
|
||||
flash[:notice] = l(:notice_successful_create) if @issue.save
|
||||
redirect_back_or_default(public_ticket_path(@ticket, @ticket.token))
|
||||
end
|
||||
|
||||
def render_404(_options = {})
|
||||
@message = l(:notice_file_not_found)
|
||||
respond_to do |format|
|
||||
format.html { render :template => 'common/error', :status => 404 }
|
||||
format.any { head 404 }
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_ticket
|
||||
@ticket = HelpdeskTicket.find(params[:id])
|
||||
@issue = @ticket.issue
|
||||
@project = @issue.project
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
def authorize_ticket(action = params[:action])
|
||||
allow = true
|
||||
allow &&= RedmineHelpdesk.public_comments? if action.to_s == 'add_comment'
|
||||
allow &&= (@ticket.token == params[:hash]) && RedmineHelpdesk.public_tickets?
|
||||
allow &&= !@issue.is_private
|
||||
render_404 unless allow
|
||||
end
|
||||
end
|
85
app/helpers/helpdesk_api_helper.rb
Normal file
85
app/helpers/helpdesk_api_helper.rb
Normal file
@ -0,0 +1,85 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
module HelpdeskApiHelper
|
||||
def render_api_helpdesk_ticket(helpdesk_ticket, api)
|
||||
api.id helpdesk_ticket.issue.id
|
||||
api.from_address helpdesk_ticket.from_address
|
||||
api.to_address helpdesk_ticket.to_address || ''
|
||||
api.cc_address helpdesk_ticket.cc_address
|
||||
api.message_id helpdesk_ticket.message_id
|
||||
api.ticket_date format_date(helpdesk_ticket.ticket_date)
|
||||
api.content helpdesk_ticket.issue.description
|
||||
api.source helpdesk_ticket.ticket_source_name
|
||||
api.is_incoming helpdesk_ticket.is_incoming
|
||||
api.reaction_time helpdesk_ticket.reaction_time || ''
|
||||
api.first_response_time helpdesk_ticket.first_response_time || ''
|
||||
api.resolve_time helpdesk_ticket.resolve_time || ''
|
||||
api.last_agent_response_at format_date(helpdesk_ticket.last_agent_response_at) || ''
|
||||
api.last_customer_response_at format_date(helpdesk_ticket.last_customer_response_at) || ''
|
||||
api.contact(id: helpdesk_ticket.contact_id, name: helpdesk_ticket.customer.name) if helpdesk_ticket.customer.present?
|
||||
api.vote helpdesk_ticket.vote
|
||||
api.vote_comment helpdesk_ticket.vote_comment
|
||||
api.message_file do
|
||||
render_api_attachment(helpdesk_ticket.message_file, api)
|
||||
end if helpdesk_ticket.message_file.present?
|
||||
api.array :journal_messages do
|
||||
helpdesk_ticket.issue.journal_messages.each do |journal_message|
|
||||
api.journal_message do
|
||||
api.contact(id: journal_message.contact_id, name: journal_message.contact.name) if journal_message.contact.present?
|
||||
api.from_address journal_message.from_address
|
||||
api.to_address journal_message.to_address
|
||||
api.cc_address journal_message.cc_address
|
||||
api.bcc_address journal_message.bcc_address
|
||||
api.message_date format_date(journal_message.message_date)
|
||||
api.is_incoming journal_message.is_incoming
|
||||
api.content journal_message.content
|
||||
api.message_id journal_message.message_id
|
||||
api.journal_id journal_message.journal_id
|
||||
api.viewed_on journal_message.viewed_on
|
||||
api.message_file do
|
||||
render_api_attachment(journal_message.message_file, api)
|
||||
end if journal_message.message_file.present?
|
||||
end
|
||||
end
|
||||
end if include_in_api_response?('journal_messages')
|
||||
api.array :journals do
|
||||
helpdesk_ticket.journals.each do |journal|
|
||||
api.journal do
|
||||
api.id journal.id
|
||||
api.notes journal.notes
|
||||
api.created_on journal.created_on
|
||||
end
|
||||
end
|
||||
end if include_in_api_response?('journals')
|
||||
end
|
||||
|
||||
def render_api_journal_message(journal_message, api)
|
||||
api.id journal_message.journal_id
|
||||
api.from_address journal_message.from_address
|
||||
api.to_address journal_message.to_address
|
||||
api.cc_address journal_message.cc_address
|
||||
api.bcc_address journal_message.bcc_address
|
||||
api.content journal_message.journal.notes
|
||||
api.message_date format_date(journal_message.message_date)
|
||||
api.message_id journal_message.message_id
|
||||
end
|
||||
end
|
275
app/helpers/helpdesk_helper.rb
Normal file
275
app/helpers/helpdesk_helper.rb
Normal file
@ -0,0 +1,275 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
module HelpdeskHelper
|
||||
def helpdesk_ticket_source_icon(helpdesk_ticket)
|
||||
case helpdesk_ticket.source
|
||||
when HelpdeskTicket::HELPDESK_EMAIL_SOURCE
|
||||
'icon-email'
|
||||
when HelpdeskTicket::HELPDESK_PHONE_SOURCE
|
||||
'icon-call'
|
||||
when HelpdeskTicket::HELPDESK_WEB_SOURCE
|
||||
'icon-web'
|
||||
when HelpdeskTicket::HELPDESK_TWITTER_SOURCE
|
||||
'icon-twitter'
|
||||
else
|
||||
'icon-helpdesk'
|
||||
end
|
||||
end
|
||||
|
||||
def helpdesk_tickets_source_for_select
|
||||
[[l(:label_helpdesk_tickets_email), HelpdeskTicket::HELPDESK_EMAIL_SOURCE.to_s],
|
||||
[l(:label_helpdesk_tickets_phone), HelpdeskTicket::HELPDESK_PHONE_SOURCE.to_s],
|
||||
[l(:label_helpdesk_tickets_web), HelpdeskTicket::HELPDESK_WEB_SOURCE.to_s],
|
||||
[l(:label_helpdesk_tickets_conversation), HelpdeskTicket::HELPDESK_CONVERSATION_SOURCE.to_s]
|
||||
]
|
||||
end
|
||||
|
||||
def helpdesk_send_as_for_select
|
||||
[[l(:label_helpdesk_not_send), ''],
|
||||
[l(:label_helpdesk_send_as_notification), HelpdeskTicket::SEND_AS_NOTIFICATION.to_s],
|
||||
[l(:label_helpdesk_send_as_message), HelpdeskTicket::SEND_AS_MESSAGE.to_s]
|
||||
]
|
||||
end
|
||||
|
||||
def show_customer_vote(vote, comment)
|
||||
case vote
|
||||
when 2
|
||||
generate_vote_link(vote, 'icon-awesome', comment)
|
||||
when 1
|
||||
generate_vote_link(vote, 'icon-justok', comment)
|
||||
when 0
|
||||
generate_vote_link(vote, 'icon-notgood', comment)
|
||||
end
|
||||
end
|
||||
|
||||
def generate_vote_link(vote, vote_class, title)
|
||||
"<div class='icon #{vote_class}' title='#{title}'>#{HelpdeskTicket.vote_message(vote)}</div>".html_safe
|
||||
end
|
||||
|
||||
def render_helpdesk_chart(report_name, issues_scope)
|
||||
render :partial => 'helpdesk_reports/chart', :locals => { :report => report_name, :issues_scope => issues_scope }
|
||||
end
|
||||
|
||||
def helpdesk_time_label(seconds)
|
||||
hours, minutes = seconds.divmod(60).first.divmod(60)
|
||||
"#{hours}<span>#{l(:label_helpdesk_hour)}</span> #{minutes}<span>#{l(:label_helpdesk_minute)}</span>".html_safe
|
||||
end
|
||||
|
||||
def slim_helpdesk_time_label(seconds)
|
||||
hours, minutes = seconds.divmod(60).first.divmod(60)
|
||||
"#{hours}#{l(:label_helpdesk_hour)} #{minutes}#{l(:label_helpdesk_minute)}".html_safe
|
||||
end
|
||||
|
||||
def progress_in_percents(value)
|
||||
return '0%'.html_safe if value.zero?
|
||||
"<span class='caret #{value > 0 ? 'pos' : 'neg'}'></span>#{value}%".html_safe
|
||||
end
|
||||
|
||||
def mirror_progress_in_percents(value)
|
||||
return '0%'.html_safe if value.zero?
|
||||
"<span class='caret #{value < 0 ? 'mirror_pos' : 'mirror_neg'}'></span>#{value}%".html_safe
|
||||
end
|
||||
|
||||
def process_deviation(before, now, time = true)
|
||||
["#{l(:label_helpdesk_report_previous)}: #{time ? slim_helpdesk_time_label(before) : before}",
|
||||
"#{l(:label_helpdesk_report_deviation)}: #{time ? slim_helpdesk_time_label(calculate_deviation(before, now)) : calculate_deviation(before, now)}"].join("\n").html_safe
|
||||
end
|
||||
|
||||
def calculate_deviation(before, now)
|
||||
before > now ? before - now : now - before
|
||||
end
|
||||
|
||||
def helpdesk_reply_link
|
||||
link_to l(:label_helpdesk_reply), edit_issue_path(@issue), :onclick => 'showAndScrollTo("update", "issue_notes"); showConfiguredSend("update", "issue_notes", true); return false',
|
||||
:class => 'icon icon-helpdesk-reply'
|
||||
end
|
||||
|
||||
def helpdesk_mail_rule_mail_types_for_select(selected = nil)
|
||||
mail_types_options = [
|
||||
[l(:label_helpdesk_mail_rule_mail_type_incoming), HelpdeskMailRule::INCOMING],
|
||||
[l(:label_helpdesk_mail_rule_mail_type_outgoing), HelpdeskMailRule::OUTGOING]
|
||||
# TODO: Manual rules
|
||||
# [l(:label_helpdesk_mail_rule_mail_type_manually), HelpdeskMailRule::MANUALLY]
|
||||
]
|
||||
options_for_select(mail_types_options, selected)
|
||||
end
|
||||
|
||||
def helpdesk_mail_rule_attrs_for_select(collection)
|
||||
elements = []
|
||||
collection.each do |element, element_options|
|
||||
elements << [element_options[:name], element]
|
||||
end
|
||||
options_for_select([[]] + elements)
|
||||
end
|
||||
|
||||
def helpdesk_mail_rule_conditions_to_html(conditions)
|
||||
return content_tag('p', l(:label_helpdesk_mail_rule_condition_for_all), class: 'helpdesk_mail_rule_condition') unless conditions.any?
|
||||
|
||||
conditions.map do |condition, options|
|
||||
next unless HelpdeskMailRule.available_conditions[condition]
|
||||
|
||||
condition = HelpdeskMailRule.available_conditions[condition].new
|
||||
content_tag('p',
|
||||
[
|
||||
condition.options[:name],
|
||||
l(Query.operators[options[:operator]]),
|
||||
content_tag('b', condition.label_for(options) || options[:values].join(','))
|
||||
].join(' ').html_safe,
|
||||
class: 'helpdesk_mail_rule_condition')
|
||||
end.join.html_safe
|
||||
end
|
||||
|
||||
def helpdesk_mail_rule_actions_to_html(actions)
|
||||
actions.map do |action, options|
|
||||
rule = HelpdeskMailRule.available_actions[action].try(:new)
|
||||
next unless rule
|
||||
content_tag('p',
|
||||
helpdesk_mail_rule_change_to_label(rule, options).html_safe,
|
||||
class: "helpdesk_mail_rule_condition #{'red' if rule.is_a?(RedmineHelpdesk::MailRules::Actions::Stop)}")
|
||||
end.compact.join.html_safe
|
||||
end
|
||||
|
||||
def helpdesk_mail_rule_change_to_label(rule, options)
|
||||
return rule.options[:name] if HelpdeskMailRule::NO_VALUES_ACTION_KEYS.include?(rule.field)
|
||||
|
||||
[
|
||||
rule.options[:name],
|
||||
l(:label_helpdesk_mail_rule_action_change_to),
|
||||
content_tag('b', rule.label_for(options) || options[:values].join(','))
|
||||
].join(' ')
|
||||
end
|
||||
|
||||
def helpdesk_select_customer_tag(name, select_values = [], options = {})
|
||||
cross_project_contacts = ContactsSetting.cross_project_contacts? || !!options.delete(:cross_project_contacts)
|
||||
s = select2_tag(
|
||||
name,
|
||||
options_for_select(select_values, options[:selected]),
|
||||
url: auto_complete_contacts_path(project_id: (cross_project_contacts ? nil : @project), is_company: (options[:is_company] ? '1' : nil), multiaddress: options[:multiaddress]),
|
||||
placeholder: '',
|
||||
style: options[:style] || 'width: 60%',
|
||||
width: options[:width] || '60%',
|
||||
include_blank: true,
|
||||
format_state: (options[:multiaddress] ? 'formatStateWithMultiaddress' : 'formatStateWithAvatar'),
|
||||
format_selection: 'formatSelectionWithEmails',
|
||||
allow_clear: !!options[:include_blank],
|
||||
multiple: options[:multiple]
|
||||
)
|
||||
|
||||
if options[:add_contact] && @project.try(:persisted?)
|
||||
if authorize_for('contacts', 'new')
|
||||
s << link_to(
|
||||
image_tag('add.png', style: 'vertical-align: middle; margin-left: 5px;'),
|
||||
new_project_contact_path(@project, contact_field_name: name, contacts_is_company: !!options[:is_company]),
|
||||
remote: true,
|
||||
method: 'get',
|
||||
title: l(:label_crm_contact_new),
|
||||
id: "#{sanitize_to_id(name)}_add_link",
|
||||
tabindex: 200
|
||||
)
|
||||
end
|
||||
|
||||
s << javascript_include_tag('attachments')
|
||||
end
|
||||
|
||||
s.html_safe
|
||||
end
|
||||
|
||||
def helpdesk_issue_contact_emails(issue)
|
||||
settings_cc_address = HelpdeskSettings["helpdesk_cc_address", issue.project.id]
|
||||
settings_bcc_address = HelpdeskSettings["helpdesk_bcc_address", issue.project.id]
|
||||
contact_emails = []
|
||||
|
||||
ticket = issue.helpdesk_ticket
|
||||
([ticket.from_address] + ticket.cc_address.to_s.split(',') + ticket.response_addresses).uniq.each do |email|
|
||||
contact_emails << [email, email.try(:downcase)]
|
||||
end
|
||||
(issue.contacts + [issue.customer]).uniq.compact.each do |contact|
|
||||
contact.emails.each { |email| contact_emails << [contact.email_name, email.downcase] }
|
||||
end
|
||||
contact_emails << [settings_cc_address, settings_cc_address] if settings_cc_address.present?
|
||||
contact_emails << [settings_bcc_address, settings_bcc_address] if settings_bcc_address.present?
|
||||
contact_emails.uniq(&:last)
|
||||
end
|
||||
|
||||
def selected_cc_addresses(issue)
|
||||
cc_addresses = []
|
||||
settings_cc_address = HelpdeskSettings["helpdesk_cc_address", issue.project.id]
|
||||
customer_email = issue.customer.primary_email
|
||||
|
||||
cc_addresses << customer_email if customer_email.downcase != issue.helpdesk_ticket.default_to_address.try(:downcase)
|
||||
cc_addresses += issue.helpdesk_ticket.cc_addresses
|
||||
cc_addresses << settings_cc_address if settings_cc_address.present?
|
||||
cc_addresses
|
||||
end
|
||||
|
||||
def selected_bcc_addresses(issue)
|
||||
settings_bcc_address = HelpdeskSettings["helpdesk_bcc_address", issue.project.id]
|
||||
bcc_addresses = []
|
||||
bcc_addresses << settings_bcc_address if settings_bcc_address.present?
|
||||
bcc_addresses
|
||||
end
|
||||
|
||||
def helpdesk_issue_customer_text_with_email(helpdesk_ticket)
|
||||
return '' unless helpdesk_ticket.customer
|
||||
return helpdesk_ticket.customer.name_with_company unless helpdesk_ticket.from_address
|
||||
|
||||
"#{helpdesk_ticket.customer.name_with_company} <#{helpdesk_ticket.from_address}>"
|
||||
end
|
||||
|
||||
def helpdesk_incoming_protocols
|
||||
default_protocols = [
|
||||
['', ''], ['pop3', 'pop3'], ['imap', 'imap'], ['Gmail', 'gmail'], ['Yahoo', 'yahoo'], ['Yandex', 'yandex']
|
||||
]
|
||||
return default_protocols unless RedmineHelpdesk.oauth_enabled?
|
||||
|
||||
default_protocols << ['Microsoft Outlook', 'outlook'] if RedmineHelpdesk.settings['helpdesk_oauth_outlook_client_id'].present?
|
||||
default_protocols << ['Google OAuth', 'google'] if RedmineHelpdesk.settings['helpdesk_oauth_google_client_id'].present?
|
||||
default_protocols
|
||||
end
|
||||
|
||||
def helpdesk_outgoing_protocols(project)
|
||||
default_protocols = [
|
||||
['smtp', 'smtp'],
|
||||
]
|
||||
return default_protocols unless RedmineHelpdesk.oauth_enabled?
|
||||
|
||||
default_protocols << ['Microsoft Outlook', 'outlook'] if HelpdeskSettings[:helpdesk_protocol, project.id] == 'outlook'
|
||||
default_protocols << ['Google OAuth', 'google'] if HelpdeskSettings[:helpdesk_protocol, project.id] == 'google'
|
||||
default_protocols
|
||||
end
|
||||
|
||||
def helpdesk_outgoing_value(project)
|
||||
value = HelpdeskSettings[:helpdesk_send_protocol, @project.id]
|
||||
return 'outlook' if RedmineHelpdesk.oauth_enabled? && HelpdeskSettings[:helpdesk_protocol, project.id] == 'outlook' && value == 'outlook'
|
||||
return 'google' if RedmineHelpdesk.oauth_enabled? && HelpdeskSettings[:helpdesk_protocol, project.id] == 'google' && value == 'google'
|
||||
|
||||
'smtp'
|
||||
end
|
||||
|
||||
def helpdesk_outlook_folders(project)
|
||||
HelpdeskOauthProvider::Microsoft.new(project.id).folders
|
||||
end
|
||||
|
||||
def helpdesk_google_folders(project)
|
||||
HelpdeskOauthProvider::Google.new(project.id).folders
|
||||
end
|
||||
end
|
46
app/helpers/helpdesk_mailer_helper.rb
Normal file
46
app/helpers/helpdesk_mailer_helper.rb
Normal file
@ -0,0 +1,46 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# encoding: utf-8
|
||||
# include RedCloth
|
||||
|
||||
module HelpdeskMailerHelper
|
||||
def textile(text)
|
||||
Redmine::WikiFormatting.to_html(Setting.text_formatting, text)
|
||||
end
|
||||
|
||||
def message_sender(email)
|
||||
return nil unless email
|
||||
sender = email.reply_to.try(:first) || email.from_addrs.try(:first)
|
||||
sender.to_s.strip[0, 255]
|
||||
end
|
||||
|
||||
def contact_from_email(email, project, logger = nil)
|
||||
from = (email.header['reply-to'].to_s.present? ? email.header['reply-to'] : email.header['from']).to_s.strip[0, 255]
|
||||
addr, name = from, nil
|
||||
if match_result = from.match(/^"?(.+?)"?\s*<(.+@.+)>$/)
|
||||
addr, name = match_result[2], match_result[1]
|
||||
elsif match_result = from.match(/(\S+@\S+)/)
|
||||
addr = match_result[1]
|
||||
end
|
||||
HelpdeskMailSupport.create_contact_from_address(addr, name, project, logger)
|
||||
end
|
||||
end
|
55
app/helpers/public_tickets_helper.rb
Normal file
55
app/helpers/public_tickets_helper.rb
Normal file
@ -0,0 +1,55 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
module PublicTicketsHelper
|
||||
include HelpdeskHelper
|
||||
|
||||
def authoring_public(journal, options = {})
|
||||
if journal.journal_message && journal.journal_message.from_address
|
||||
l(options[:label] || :label_added_time_by, :author => mail_to(journal.journal_message.contact_email), :age => ticket_time_tag(journal.created_on)).html_safe
|
||||
else
|
||||
l(options[:label] || :label_added_time_by, :author => journal.user.name, :age => ticket_time_tag(journal.created_on)).html_safe
|
||||
end
|
||||
end
|
||||
|
||||
def ticket_time_tag(time)
|
||||
text = distance_of_time_in_words(Time.now, time)
|
||||
content_tag('acronym', text, :title => format_time(time))
|
||||
end
|
||||
|
||||
def link_to_attachments_with_hash(container, options = {})
|
||||
options.assert_valid_keys(:author, :thumbnails)
|
||||
|
||||
if container.attachments.any?
|
||||
options = { :deletable => container.attachments_deletable?, :author => true }.merge(options)
|
||||
render :partial => 'attachment_links',
|
||||
:locals => { :attachments => container.attachments, :options => options, :thumbnails => (options[:thumbnails] && Setting.thumbnails_enabled?) }
|
||||
end
|
||||
end
|
||||
|
||||
def link_to_attachment_with_hash(attachment, options = {})
|
||||
text = options.delete(:text) || attachment.filename
|
||||
route_method = options.delete(:download) ? :hashed_download_named_attachment_path : :hashed_named_attachment_path
|
||||
html_options = options.slice!(:only_path)
|
||||
url = send(route_method, attachment, @ticket.id, @ticket.token, attachment.filename, options)
|
||||
link_to text, url, html_options
|
||||
end
|
||||
end
|
77
app/models/canned_response.rb
Normal file
77
app/models/canned_response.rb
Normal file
@ -0,0 +1,77 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class CannedResponse < ApplicationRecord
|
||||
include Redmine::SafeAttributes
|
||||
|
||||
attr_accessor :deleted_attachment_ids
|
||||
|
||||
safe_attributes(
|
||||
'name',
|
||||
'content',
|
||||
'deleted_attachment_ids',
|
||||
'project'
|
||||
)
|
||||
safe_attributes(
|
||||
'is_public',
|
||||
if: lambda do |cr, user|
|
||||
return true if user.admin?
|
||||
|
||||
user.allowed_to?(:manage_public_canned_responses, cr.project)
|
||||
end
|
||||
)
|
||||
|
||||
belongs_to :project
|
||||
belongs_to :user
|
||||
|
||||
acts_as_attachable
|
||||
|
||||
validates_presence_of :name, :content
|
||||
validates_length_of :name, :maximum => 255
|
||||
|
||||
after_save :delete_selected_attachments
|
||||
|
||||
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)) && (is_public? || user_id == user.id)
|
||||
end
|
||||
|
||||
def deleted_attachment_ids
|
||||
Array(@deleted_attachment_ids).map(&:to_i)
|
||||
end
|
||||
|
||||
def delete_selected_attachments
|
||||
if deleted_attachment_ids.present?
|
||||
objects = attachments.where(id: deleted_attachment_ids.map(&:to_i))
|
||||
attachments.delete(objects)
|
||||
end
|
||||
end
|
||||
end
|
167
app/models/helpdesk_data_collector_busiest_time.rb
Normal file
167
app/models/helpdesk_data_collector_busiest_time.rb
Normal file
@ -0,0 +1,167 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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['report_date_period'][: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['report_date_period'][: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['report_date_period'][: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['report_date_period'][: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]['report_date_period'].nil? || @query[:filters]['report_date_period'][:operator].nil?
|
||||
return @previous_query if @previous_query
|
||||
|
||||
previous_operator = ['pre_', @query[:filters]['report_date_period'][:operator]].join
|
||||
previous_filters = @query[:filters].merge('report_date_period' => { :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['report_date_period'][: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['report_date_period'][: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['report_date_period'][: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
|
199
app/models/helpdesk_data_collector_first_response.rb
Normal file
199
app/models/helpdesk_data_collector_first_response.rb
Normal file
@ -0,0 +1,199 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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')
|
||||
@issues = @issues.distinct
|
||||
@previous_issues = previous_query.issues.joins(:helpdesk_ticket).where('helpdesk_tickets.first_response_time > 0')
|
||||
@previous_issues = @previous_issues.distinct
|
||||
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]['report_date_period'].nil? || @query[:filters]['report_date_period'][:operator].nil?
|
||||
return @previous_query if @previous_query
|
||||
|
||||
previous_operator = ['pre_', @query[:filters]['report_date_period'][:operator]].join
|
||||
previous_filters = @query[:filters].merge('report_date_period' => { :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['report_date_period'][:operator], nil, 'journal_messages', 'message_date')
|
||||
end
|
||||
|
||||
def median(array)
|
||||
return 0 if array.compact.empty?
|
||||
range = array.compact.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
|
29
app/models/helpdesk_data_collector_manager.rb
Normal file
29
app/models/helpdesk_data_collector_manager.rb
Normal file
@ -0,0 +1,29 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskDataCollectorManager
|
||||
def collect_data(query)
|
||||
case query.name
|
||||
when 'first_response_time'
|
||||
HelpdeskDataCollectorFirstResponse.new(query)
|
||||
when 'busiest_time_of_day'
|
||||
HelpdeskDataCollectorBusiestTime.new(query)
|
||||
end
|
||||
end
|
||||
end
|
142
app/models/helpdesk_mail_container.rb
Normal file
142
app/models/helpdesk_mail_container.rb
Normal file
@ -0,0 +1,142 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskMailContainer
|
||||
include HelpdeskMailerHelper
|
||||
|
||||
class UnauthorizedAction < StandardError; end
|
||||
class MissingInformation < StandardError; end
|
||||
|
||||
CONTAINER_ACCESSORS = [:email, :options, :logger, :contact, :target_project, :text_body, :from_addr, :from_user, :issue, :is_new_issue].freeze
|
||||
attr_accessor *CONTAINER_ACCESSORS
|
||||
|
||||
def initialize(email_or_raw, options)
|
||||
email_or_raw.force_encoding('ASCII-8BIT') if email_or_raw.respond_to?(:force_encoding)
|
||||
@email = email_or_raw.is_a?(Mail) ? email_or_raw : Mail.new(email_or_raw)
|
||||
@options = options
|
||||
|
||||
fill_accessors_from_options(options)
|
||||
|
||||
@text_body ||= HelpdeskMailSupport.escaped_cleaned_up_text_body(email)
|
||||
@from_addr ||= message_sender(email)
|
||||
@from_user ||= User.find_by_mail(@from_addr) || User.anonymous
|
||||
end
|
||||
|
||||
def validate
|
||||
return false if HelpdeskMailSupport.check_ignored_headers?(email, logger)
|
||||
return logged_error("#{email.message_id}: Ignored duplicated email with Message-ID #{email.message_id}") if duplicated_message_id?
|
||||
return logged_error("#{email.message_id}: Email ignored because no FROM address found") if from_addr.blank?
|
||||
return logged_error("#{email.message_id}: Contacts and issues modules should be enable for #{target_project.name} project") if modules_disabled?
|
||||
return logged_error("#{email.message_id}: Ignoring email from Redmine emission address [#{from_addr}]") if sent_from_emission?
|
||||
return logged_error("#{email.message_id}: Email #{from_addr} ignored because in blacklist") if HelpdeskMailSupport.check_blacklist?(from_addr, target_project)
|
||||
return logged_error("#{email.message_id}: could not create/found contact for [#{@from_addr}]") unless contact
|
||||
|
||||
# TODO: Add custom validate hook
|
||||
true
|
||||
end
|
||||
|
||||
def dispatch
|
||||
# TODO: Before after dispatch hook
|
||||
m = email.subject && email.subject.match(HelpdeskMailSupport::ISSUE_REPLY_SUBJECT_RE)
|
||||
message_identifier = [email.in_reply_to, email.references].flatten.reject(&:blank?)
|
||||
journal_message = JournalMessage.where(message_id: message_identifier).first
|
||||
helpdesk_ticket = HelpdeskTicket.where(message_id: message_identifier).first
|
||||
if journal_message && journal_message.journal && journal_message.journal.issue
|
||||
HelpdeskMailRecipient::IssueReplyRecipient.new(self).receive(journal_message.journal.issue.id)
|
||||
elsif helpdesk_ticket && helpdesk_ticket.issue
|
||||
HelpdeskMailRecipient::IssueReplyRecipient.new(self).receive(helpdesk_ticket.issue.id)
|
||||
elsif m && Issue.find_by(id: m[1].to_i)&.project&.module_enabled?('contacts_helpdesk')
|
||||
HelpdeskMailRecipient::IssueReplyRecipient.new(self).receive(m[1].to_i)
|
||||
else
|
||||
@is_new_issue = true
|
||||
HelpdeskMailRecipient::IssueRecipient.new(self).receive
|
||||
end
|
||||
# TODO: Add after dispatch hook
|
||||
rescue MissingInformation => e
|
||||
logged_error("#{email.message_id}: missing information from #{from_user}: #{e.message}")
|
||||
logged_error(e.backtrace.first)
|
||||
rescue UnauthorizedAction => _e
|
||||
logged_error("#{email.message_id}: unauthorized attempt from #{from_user}")
|
||||
rescue Exception => e
|
||||
logged_error("#{email.message_id}: dispatch error #{e.message}")
|
||||
logged_error(e.backtrace.first)
|
||||
end
|
||||
|
||||
def target_project
|
||||
return @target_project if @target_project
|
||||
|
||||
@target_project ||= Project.where(identifier: get_keyword(:project) || get_keyword(:project_id)).first
|
||||
@target_project ||= Project.where(id: get_keyword(:project_id)).first
|
||||
raise MissingInformation.new('Unable to determine @target_project project') if @target_project.nil?
|
||||
@target_project
|
||||
end
|
||||
|
||||
def get_keyword(attr, extra_options = {})
|
||||
@keywords ||= {}
|
||||
key_options = options.merge(extra_options)
|
||||
unless @keywords[attr]
|
||||
if attr_overridable?(attr, key_options)
|
||||
@keywords[attr] = HelpdeskMailSupport.extract_from_text!(text_body, attr, from_user, key_options[:format])
|
||||
end
|
||||
@keywords[attr] ||= key_options[:issue][attr] if key_options[:issue].present? && key_options[:issue][attr].present?
|
||||
@keywords[attr] ||= key_options[attr] if key_options[attr].present?
|
||||
end
|
||||
@keywords[attr]
|
||||
end
|
||||
|
||||
def contact
|
||||
@contact ||= contact_from_email(email, target_project, logger)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fill_accessors_from_options(options)
|
||||
CONTAINER_ACCESSORS.each do |accessor|
|
||||
send("#{accessor}=", options[accessor]) if options[accessor]
|
||||
end
|
||||
end
|
||||
|
||||
def modules_disabled?
|
||||
[:contacts, :issue_tracking].any? { |m| !target_project.module_enabled?(m) }
|
||||
end
|
||||
|
||||
def sent_from_emission?
|
||||
from_addr.downcase == Setting.mail_from.to_s.strip.downcase
|
||||
end
|
||||
|
||||
def with_ignored_header?
|
||||
HelpdeskMailSupport.ignored_headers(self, email)
|
||||
end
|
||||
|
||||
def duplicated_message_id?
|
||||
HelpdeskTicket.joins(:issue).where("#{Issue.table_name}.project_id": target_project.id)
|
||||
.find_by(message_id: email.message_id.to_s.slice(0, 255))
|
||||
end
|
||||
|
||||
def logged_error(message = nil)
|
||||
logger.error(message) if message
|
||||
false
|
||||
end
|
||||
|
||||
def attr_overridable?(attr, attr_options)
|
||||
attr_options[:override] ||
|
||||
(attr_options[:allow_override].present? && attr_options[:allow_override].include?('all')) ||
|
||||
(attr_options[:allow_override].present? && attr_options[:allow_override].include?(attr.to_s))
|
||||
end
|
||||
end
|
111
app/models/helpdesk_mail_messenger.rb
Normal file
111
app/models/helpdesk_mail_messenger.rb
Normal file
@ -0,0 +1,111 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskMailMessenger < MailHandler
|
||||
include HelpdeskMailerHelper
|
||||
helper :helpdesk_mailer
|
||||
|
||||
class MissingInformation < StandardError; end
|
||||
|
||||
attr_reader :logger, :contact, :object, :options, :email
|
||||
|
||||
def prepare_email(contact, object, options = {})
|
||||
@logger = options[:logger]
|
||||
@contact = contact
|
||||
@object = object
|
||||
@options = (options.respond_to?(:to_unsafe_hash) ? options.to_unsafe_hash : options).with_indifferent_access
|
||||
@email = build_email
|
||||
@container = HelpdeskMailContainer.new(email, logger: logger, contact: contact, issue: issue)
|
||||
|
||||
set_delivery_options
|
||||
|
||||
HelpdeskMailRule.apply_rules(:outgoing, @container)
|
||||
logger.try(:info, "##{object.id}: Sending #{self.class.name} to #{to_address}")
|
||||
email
|
||||
end
|
||||
|
||||
def self.default_url_options
|
||||
{ host: Setting.host_name, protocol: Setting.protocol }
|
||||
end
|
||||
|
||||
def build_email
|
||||
raise 'Method should be implemented in inherited classes'
|
||||
end
|
||||
|
||||
def project
|
||||
raise 'Method should be implemented in inherited classes'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def content_transfer_encoding
|
||||
'binary' if HelpdeskSettings['helpdesk_send_protocol', project] == 'outlook'
|
||||
'binary' if HelpdeskSettings['helpdesk_send_protocol', project] == 'google'
|
||||
end
|
||||
|
||||
def set_delivery_options
|
||||
return if HelpdeskSettings['helpdesk_smtp_use_default_settings', project].to_i == 0
|
||||
|
||||
case HelpdeskSettings['helpdesk_send_protocol', project]
|
||||
when 'outlook'
|
||||
email.delivery_method(HelpdeskOauthMailer::Microsoft)
|
||||
email.delivery_method.settings.merge!(project: project)
|
||||
when 'google'
|
||||
email.delivery_method(HelpdeskOauthMailer::Google)
|
||||
email.delivery_method.settings.merge!(project: project)
|
||||
else
|
||||
email.delivery_method(:smtp)
|
||||
email.delivery_method.settings.merge!(address: HelpdeskSettings['helpdesk_smtp_server', project],
|
||||
port: HelpdeskSettings['helpdesk_smtp_port', project] || 25,
|
||||
authentication: HelpdeskSettings['helpdesk_smtp_authentication', project] || 'plain',
|
||||
user_name: HelpdeskSettings['helpdesk_smtp_username', project],
|
||||
password: HelpdeskSettings['helpdesk_smtp_password', project],
|
||||
domain: HelpdeskSettings['helpdesk_smtp_domain', project],
|
||||
enable_starttls_auto: true,
|
||||
openssl_verify_mode: verification_mode,
|
||||
ssl: HelpdeskSettings['helpdesk_smtp_ssl', project].to_i > 0 &&
|
||||
HelpdeskSettings['helpdesk_smtp_tls', project].to_i == 0)
|
||||
end
|
||||
end
|
||||
|
||||
def validate; end
|
||||
|
||||
def verification_mode
|
||||
HelpdeskSettings['helpdesk_smtp_skip_verification', project] ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
|
||||
end
|
||||
|
||||
def from_address(journal_user = nil)
|
||||
mail_from ||= options[:from_address]
|
||||
mail_from ||= HelpdeskSettings['helpdesk_answer_from', project].blank? ? Setting.mail_from : HelpdeskSettings['helpdesk_answer_from', project]
|
||||
logger.try(:error, "##{object.id}: From address couldn't be blank") if mail_from.blank?
|
||||
HelpdeskMailSupport.apply_from_macro(mail_from.to_s, journal_user)
|
||||
end
|
||||
|
||||
def to_address
|
||||
options[:to_address] || contact.primary_email
|
||||
end
|
||||
|
||||
def cc_addresses
|
||||
options[:cc_addresses].to_s
|
||||
end
|
||||
|
||||
def bcc_addresses
|
||||
options[:bcc_addresses].to_s
|
||||
end
|
||||
end
|
54
app/models/helpdesk_mail_messenger/auto_answer_message.rb
Normal file
54
app/models/helpdesk_mail_messenger/auto_answer_message.rb
Normal file
@ -0,0 +1,54 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskMailMessenger::AutoAnswerMessage < HelpdeskMailMessenger
|
||||
def project
|
||||
object.project
|
||||
end
|
||||
|
||||
def issue
|
||||
object
|
||||
end
|
||||
|
||||
def build_email
|
||||
set_headers
|
||||
|
||||
@email_stylesheet = HelpdeskSettings['helpdesk_helpdesk_css', project].to_s.html_safe
|
||||
@email_body = HelpdeskMailSupport.apply_text_macro(HelpdeskSettings['helpdesk_first_answer_template', project], contact, issue)
|
||||
|
||||
validate
|
||||
|
||||
mail from: from_address.to_s,
|
||||
to: to_address.to_s,
|
||||
cc: HelpdeskSettings["helpdesk_cc_address", issue.project.id].to_s + cc_addresses,
|
||||
bcc: HelpdeskSettings["helpdesk_bcc_address", issue.project.id].to_s + bcc_addresses,
|
||||
subject: HelpdeskMailSupport.apply_text_macro(HelpdeskSettings['helpdesk_first_answer_subject', project], contact, issue) || "Helpdesk auto answer [Case ##{issue.id}]",
|
||||
in_reply_to: "<#{issue.helpdesk_ticket.try(:message_id)}>" do |format|
|
||||
format.text(content_transfer_encoding: content_transfer_encoding) { render 'email_layout' }
|
||||
format.html(content_transfer_encoding: content_transfer_encoding) { render 'email_layout' } unless RedmineHelpdesk.add_plain_text_mail?
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_headers
|
||||
headers['X-Redmine-Ticket-ID'] = issue.id.to_s
|
||||
headers['X-Auto-Response-Suppress'] = 'oof'
|
||||
end
|
||||
end
|
59
app/models/helpdesk_mail_messenger/autoclose_message.rb
Normal file
59
app/models/helpdesk_mail_messenger/autoclose_message.rb
Normal file
@ -0,0 +1,59 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskMailMessenger::AutocloseMessage < HelpdeskMailMessenger
|
||||
def project
|
||||
object.project
|
||||
end
|
||||
|
||||
def issue
|
||||
object
|
||||
end
|
||||
|
||||
def build_email
|
||||
set_headers
|
||||
|
||||
@email_stylesheet = HelpdeskSettings['helpdesk_helpdesk_css', project].to_s.html_safe
|
||||
@email_body = HelpdeskMailSupport.apply_text_macro(HelpdeskSettings['helpdesk_autoclose_template', project], contact, issue)
|
||||
@email_body = I18n.t('label_helpdesk_autoclosed_ticket') unless @email_body.present?
|
||||
|
||||
validate
|
||||
|
||||
mail from: from_address.to_s,
|
||||
to: to_address.to_s,
|
||||
cc: issue.helpdesk_ticket.try(:cc_address).to_s,
|
||||
subject: HelpdeskMailSupport.apply_text_macro(HelpdeskSettings['helpdesk_autoclose_subject', project], contact, issue) || "Helpdesk autoclose message [Case ##{issue.id}]",
|
||||
in_reply_to: "<#{issue.helpdesk_ticket.try(:message_id)}>" do |format|
|
||||
format.text(content_transfer_encoding: content_transfer_encoding) { render 'email_layout' }
|
||||
format.html(content_transfer_encoding: content_transfer_encoding) { render 'email_layout' } unless RedmineHelpdesk.add_plain_text_mail?
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_headers
|
||||
headers['X-Redmine-Ticket-ID'] = issue.id.to_s
|
||||
end
|
||||
|
||||
def validate
|
||||
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?
|
||||
raise MissingInformation.new(l(:text_helpdesk_recipients_cant_be_blank)) if to_address.blank? && cc_address.blank? && bcc_address.blank?
|
||||
end
|
||||
end
|
80
app/models/helpdesk_mail_messenger/initial_message.rb
Normal file
80
app/models/helpdesk_mail_messenger/initial_message.rb
Normal file
@ -0,0 +1,80 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskMailMessenger::InitialMessage < HelpdeskMailMessenger
|
||||
def project
|
||||
object.project
|
||||
end
|
||||
|
||||
def issue
|
||||
object
|
||||
end
|
||||
|
||||
def build_email
|
||||
set_headers
|
||||
|
||||
@email_stylesheet = HelpdeskSettings['helpdesk_helpdesk_css', project].to_s.html_safe
|
||||
@email_header = HelpdeskMailSupport.apply_text_macro(HelpdeskSettings['helpdesk_emails_header', project], contact, issue, issue.author)
|
||||
@email_body = HelpdeskMailSupport.apply_text_macro(issue.description, contact, issue, issue.author)
|
||||
@email_body = HelpdeskMailSupport.apply_attachment_macro(attachments, @email_body, issue)
|
||||
@email_footer = HelpdeskMailSupport.apply_text_macro(HelpdeskSettings['helpdesk_emails_footer', project], contact, issue, issue.author)
|
||||
@image_url = object.helpdesk_ticket.try(:view_id)
|
||||
|
||||
validate
|
||||
|
||||
add_attachments_to_mail(options[:attachments])
|
||||
|
||||
mail from: from_address.to_s,
|
||||
to: to_address.to_s,
|
||||
cc: HelpdeskSettings["helpdesk_cc_address", issue.project.id].to_s + cc_addresses,
|
||||
bcc: HelpdeskSettings["helpdesk_bcc_address", issue.project.id].to_s + bcc_addresses,
|
||||
subject: HelpdeskMailSupport.apply_text_macro(HelpdeskSettings['helpdesk_first_answer_subject', project.try(:id)], contact, issue) || issue.subject do |format|
|
||||
format.text(content_transfer_encoding: content_transfer_encoding) { render 'email_layout' }
|
||||
format.html(content_transfer_encoding: content_transfer_encoding) { render 'email_layout' } unless RedmineHelpdesk.add_plain_text_mail?
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_headers
|
||||
headers['X-Redmine-Ticket-ID'] = issue.id.to_s
|
||||
end
|
||||
|
||||
def validate
|
||||
raise MissingInformation.new(l(:text_helpdesk_message_body_cant_be_blank)) if @email_body.blank?
|
||||
raise MissingInformation.new(l(:text_helpdesk_message_contact_email_blank, contact_name: contact.name)) if contact.email.blank?
|
||||
end
|
||||
|
||||
def add_attachments_to_mail(attachments_hash)
|
||||
return if attachments_hash.blank?
|
||||
attachments_hash.each_value do |mail_attachment|
|
||||
if file = mail_attachment['file']
|
||||
file.rewind
|
||||
attachments[file.original_filename] = file.read
|
||||
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
|
||||
end
|
||||
end
|
96
app/models/helpdesk_mail_messenger/issue_response_message.rb
Normal file
96
app/models/helpdesk_mail_messenger/issue_response_message.rb
Normal file
@ -0,0 +1,96 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskMailMessenger::IssueResponseMessage < HelpdeskMailMessenger
|
||||
def project
|
||||
object.project
|
||||
end
|
||||
|
||||
def issue
|
||||
object.issue
|
||||
end
|
||||
|
||||
def build_email
|
||||
set_headers
|
||||
|
||||
@email_stylesheet = HelpdeskSettings[:helpdesk_helpdesk_css, project].to_s.html_safe
|
||||
@email_header = HelpdeskMailSupport.apply_text_macro(HelpdeskSettings['helpdesk_emails_header', project], contact, issue, object.user)
|
||||
@email_body = HelpdeskMailSupport.apply_text_macro(object.notes, contact, issue, object.user)
|
||||
@email_body = HelpdeskMailSupport.apply_attachment_macro(attachments, @email_body, issue)
|
||||
@email_footer = HelpdeskMailSupport.apply_text_macro(HelpdeskSettings['helpdesk_emails_footer', project], contact, issue, object.user)
|
||||
@image_url = object.journal_message.try(:calculate_view_id)
|
||||
|
||||
validate
|
||||
|
||||
subject_macro = HelpdeskMailSupport.apply_text_macro(HelpdeskSettings['helpdesk_answer_subject', project], contact, issue)
|
||||
|
||||
add_attachments_to_mail(object)
|
||||
|
||||
if object.details.blank? && object.private_notes? && object.notes.present?
|
||||
details_journal = object.class.where('id != ?', object.id).where(:created_on => object.created_on).first
|
||||
add_attachments_to_mail(details_journal) if details_journal
|
||||
end
|
||||
|
||||
mail :from => from_address(object.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? ? issue.subject + " [#{issue.tracker} ##{issue.id}]" : subject_macro) do |format|
|
||||
format.text(content_transfer_encoding: content_transfer_encoding) { render 'email_layout' }
|
||||
format.html(content_transfer_encoding: content_transfer_encoding) { render 'email_layout' } unless RedmineHelpdesk.add_plain_text_mail?
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_headers
|
||||
headers['X-Redmine-Ticket-ID'] = issue.id.to_s
|
||||
end
|
||||
|
||||
def validate
|
||||
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?
|
||||
raise MissingInformation.new(l(:text_helpdesk_recipients_cant_be_blank)) if to_address.blank? && cc_address.blank? && bcc_address.blank?
|
||||
end
|
||||
|
||||
def to_address
|
||||
options[:to_address] || (object.journal_message && object.journal_message.to_address) || contact.primary_email
|
||||
end
|
||||
|
||||
def cc_address
|
||||
options[:cc_address] || (object.journal_message && object.journal_message.cc_address)
|
||||
end
|
||||
|
||||
def bcc_address
|
||||
options[:bcc_address] || (object.journal_message && object.journal_message.bcc_address)
|
||||
end
|
||||
|
||||
def in_reply_to
|
||||
options[:in_reply_to] || (issue.helpdesk_ticket.blank? || issue.helpdesk_ticket.message_id.blank?) ? '' : "<#{issue.helpdesk_ticket.message_id}>"
|
||||
end
|
||||
|
||||
def add_attachments_to_mail(journal)
|
||||
journal.details.where(:property => 'attachment').each do |attachment_detail|
|
||||
if attach = Attachment.where(:id => attachment_detail.prop_key).first
|
||||
attachments[attach.filename] = File.open(attach.diskfile, 'rb') { |io| io.read }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
50
app/models/helpdesk_mail_provider.rb
Normal file
50
app/models/helpdesk_mail_provider.rb
Normal file
@ -0,0 +1,50 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskMailProvider
|
||||
attr_reader :mail_options, :options
|
||||
|
||||
def initialize(mail_options = nil, options = {})
|
||||
@mail_options = mail_options
|
||||
@options = options
|
||||
end
|
||||
|
||||
def self.receive(raw_message, options = {})
|
||||
new.receive(raw_message, options)
|
||||
end
|
||||
|
||||
def receive(raw_message, options = {})
|
||||
mail_container = HelpdeskMailContainer.new(raw_message, options)
|
||||
mail_container.validate ? mail_container.dispatch : false
|
||||
rescue Exception => e
|
||||
Rails.logger.error("#{mail_container.try(:email).try(:message_id)}: receive error #{e.message}")
|
||||
Rails.logger.error(e.backtrace.first)
|
||||
false
|
||||
end
|
||||
|
||||
def check
|
||||
raise 'Implement this method in inherited class'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def logger
|
||||
::Rails.logger
|
||||
end
|
||||
end
|
23
app/models/helpdesk_mail_provider/api_provider.rb
Normal file
23
app/models/helpdesk_mail_provider/api_provider.rb
Normal file
@ -0,0 +1,23 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskMailProvider::ApiProvider < HelpdeskMailProvider
|
||||
def check
|
||||
end
|
||||
end
|
42
app/models/helpdesk_mail_provider/google_provider.rb
Normal file
42
app/models/helpdesk_mail_provider/google_provider.rb
Normal file
@ -0,0 +1,42 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskMailProvider::GoogleProvider < HelpdeskMailProvider
|
||||
def check
|
||||
msg_count = 0
|
||||
oauth_provider = HelpdeskOauthProvider::Google.new(mail_options[:project_id])
|
||||
folder = mail_options[:folder]
|
||||
success_folder = mail_options[:move_on_success]
|
||||
fail_folder = mail_options[:move_on_failure]
|
||||
|
||||
oauth_provider.messages(folder).each do |mid, msg|
|
||||
logger.info "HelpdeskMailProvider::GoogleProvider: Receiving message #{mid}" if logger && logger.info?
|
||||
msg_count += 1
|
||||
oauth_provider.read_message(mid)
|
||||
if self.receive(msg, options)
|
||||
logger.info "HelpdeskMailProvider::GoogleProvider: Message #{mid} successfully received" if logger && logger.info?
|
||||
oauth_provider.move_message(mid, success_folder) if success_folder && success_folder != folder
|
||||
else
|
||||
logger.info "HelpdeskMailProvider::GoogleProvider: Message #{mid} can not be processed" if logger && logger.info?
|
||||
oauth_provider.move_message(mid, fail_folder) if fail_folder && fail_folder != folder
|
||||
end
|
||||
end
|
||||
msg_count
|
||||
end
|
||||
end
|
24
app/models/helpdesk_mail_provider/imap_provider.rb
Normal file
24
app/models/helpdesk_mail_provider/imap_provider.rb
Normal file
@ -0,0 +1,24 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskMailProvider::ImapProvider < HelpdeskMailProvider
|
||||
def check
|
||||
RedmineContacts::Mailer.check_imap(self, mail_options, options)
|
||||
end
|
||||
end
|
42
app/models/helpdesk_mail_provider/microsoft_provider.rb
Normal file
42
app/models/helpdesk_mail_provider/microsoft_provider.rb
Normal file
@ -0,0 +1,42 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskMailProvider::MicrosoftProvider < HelpdeskMailProvider
|
||||
def check
|
||||
msg_count = 0
|
||||
oauth_provider = HelpdeskOauthProvider::Microsoft.new(mail_options[:project_id])
|
||||
folder = mail_options[:folder]
|
||||
success_folder = mail_options[:move_on_success]
|
||||
fail_folder = mail_options[:move_on_failure]
|
||||
|
||||
oauth_provider.messages(folder).each do |mid, msg|
|
||||
logger.info "HelpdeskMailProvider::MicrosoftProvider: Receiving message #{mid}" if logger && logger.info?
|
||||
msg_count += 1
|
||||
oauth_provider.read_message(mid)
|
||||
if self.receive(msg, options)
|
||||
logger.info "HelpdeskMailProvider::MicrosoftProvider: Message #{mid} successfully received" if logger && logger.info?
|
||||
oauth_provider.move_message(mid, success_folder) if success_folder && success_folder != folder
|
||||
else
|
||||
logger.info "HelpdeskMailProvider::MicrosoftProvider: Message #{mid} can not be processed" if logger && logger.info?
|
||||
oauth_provider.move_message(mid, fail_folder) if fail_folder && fail_folder != folder
|
||||
end
|
||||
end
|
||||
msg_count
|
||||
end
|
||||
end
|
24
app/models/helpdesk_mail_provider/pop3_provider.rb
Normal file
24
app/models/helpdesk_mail_provider/pop3_provider.rb
Normal file
@ -0,0 +1,24 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskMailProvider::Pop3Provider < HelpdeskMailProvider
|
||||
def check
|
||||
RedmineContacts::Mailer.check_pop3(self, mail_options, options)
|
||||
end
|
||||
end
|
131
app/models/helpdesk_mail_recipient.rb
Normal file
131
app/models/helpdesk_mail_recipient.rb
Normal file
@ -0,0 +1,131 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskMailRecipient
|
||||
attr_reader :container
|
||||
delegate :email, to: :container
|
||||
delegate :logger, to: :container
|
||||
delegate :get_keyword, to: :container
|
||||
delegate :contact, to: :container
|
||||
|
||||
def initialize(mail_container)
|
||||
@container = mail_container
|
||||
end
|
||||
|
||||
def receive
|
||||
raise 'Implement this method in inherited class'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def project
|
||||
container.target_project
|
||||
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]
|
||||
HelpdeskMailSupport.create_contact_from_address(cc_addr, cc_name, project, logger)
|
||||
end.compact
|
||||
end
|
||||
|
||||
def to_recipients
|
||||
except_service_emails(email.to_addrs)
|
||||
end
|
||||
|
||||
def except_service_emails(emails)
|
||||
project_helpdesk_username = HelpdeskSettings['helpdesk_username', project.id]
|
||||
project_from_address = HelpdeskSettings['helpdesk_answer_from', project].scan(/([^<\ ,]+@[^>\ ,]+)/)
|
||||
system_from_address = Setting.mail_from.scan(/([^<\ ,]+@[^>\ ,]+)/)
|
||||
service_addresses = RedmineHelpdesk.service_email_aliases
|
||||
|
||||
lowcase(emails) - lowcase(([project_helpdesk_username, project_from_address, system_from_address] + service_addresses).flatten.compact)
|
||||
end
|
||||
|
||||
def save_email_as_attachment(object, filename = 'message.eml')
|
||||
Attachment.create(container: object,
|
||||
file: email.raw_source.to_s,
|
||||
author: container.from_user,
|
||||
filename: filename,
|
||||
content_type: 'message/rfc822')
|
||||
end
|
||||
|
||||
def save_email_as_calendar(object, filename = 'calendar_event.ics')
|
||||
return unless email.mime_type == 'text/calendar'
|
||||
|
||||
Attachment.create(container: object,
|
||||
file: email.body.to_s,
|
||||
author: container.from_user,
|
||||
filename: filename,
|
||||
content_type: 'text/calendar')
|
||||
end
|
||||
|
||||
def add_attachments(obj)
|
||||
email_attachments = []
|
||||
email_attachments << Mail::Part.new(container.email) if container.email.mime_type && container.email.mime_type.starts_with?('image/')
|
||||
|
||||
fwd_attachments = email.parts.map do |part|
|
||||
if part.content_type =~ /message\/rfc822/
|
||||
Mail.new(part.body).attachments
|
||||
elsif part.parts.empty?
|
||||
part if part.attachment?
|
||||
else
|
||||
part.attachments
|
||||
end
|
||||
end
|
||||
fwd_attachments = fwd_attachments.flatten.compact
|
||||
|
||||
email_attachments |= fwd_attachments | email.attachments
|
||||
|
||||
return if email_attachments.blank?
|
||||
email_attachments.each do |attachment|
|
||||
attachment_filename = HelpdeskMailSupport.convert_to_utf8(attachment.filename, 'binary')
|
||||
attachment_filename = [attachment_filename, '.', attachment.mime_type.split('/').last].join if attachment.inline? && attachment_filename.exclude?('.')
|
||||
new_attachment = Attachment.new(container: obj,
|
||||
file: (attachment.decoded rescue nil) || (attachment.decode_body rescue nil) || attachment.raw_source,
|
||||
filename: attachment_filename,
|
||||
author: container.from_user,
|
||||
content_type: attachment.mime_type)
|
||||
if valid_attachment?(obj, attachment.body, new_attachment)
|
||||
obj.attachments << new_attachment
|
||||
logger.try(:info, "#{email.message_id}: attachment #{attachment_filename} added to ticket: '#{obj.subject}'")
|
||||
else
|
||||
logger.try(:error, "#{email.message_id}: attachment #{attachment_filename} skipped for ticket: #{new_attachment.errors.full_messages.to_sentence}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def valid_attachment?(obj, body, new_attachment)
|
||||
new_attachment.valid? &&
|
||||
obj.attachments.where(digest: HelpdeskMailSupport.attachment_digest(body.to_s)).empty? &&
|
||||
HelpdeskMailSupport.attachment_acceptable?(new_attachment)
|
||||
end
|
||||
|
||||
def slice_emails(emails_string)
|
||||
return emails_string if emails_string.size <= 255
|
||||
|
||||
sliced_string = emails_string.slice(0, 255)
|
||||
sliced_string.slice(0, sliced_string.rindex(',').to_i)
|
||||
end
|
||||
|
||||
def lowcase(array)
|
||||
array.map(&:downcase)
|
||||
end
|
||||
end
|
194
app/models/helpdesk_mail_recipient/issue_recipient.rb
Normal file
194
app/models/helpdesk_mail_recipient/issue_recipient.rb
Normal file
@ -0,0 +1,194 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskMailRecipient::IssueRecipient < HelpdeskMailRecipient
|
||||
def receive
|
||||
issue = Issue.new(author: container.from_user, project: project)
|
||||
issue.tracker_id = tracker_id(issue)
|
||||
|
||||
issue.safe_attributes = helpdesk_issue_attributes(issue)
|
||||
issue.safe_attributes = { 'custom_field_values' => helpdesk_custom_field_values(issue) }
|
||||
issue.subject = cleaned_up_subject(email)
|
||||
issue.subject = '(no subject)' if issue.subject.blank?
|
||||
issue.description = container.text_body
|
||||
issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
|
||||
issue.assigned_to = contact.find_assigned_user(project, issue.assigned_to_id || HelpdeskSettings['helpdesk_assigned_to', project])
|
||||
|
||||
helpdesk_ticket = HelpdeskTicket.new(from_address: slice_emails(container.from_addr.downcase.to_s),
|
||||
to_address: slice_emails(email.to_addrs.join(',').downcase.to_s),
|
||||
cc_address: slice_emails((to_recipients + except_service_emails(email.cc_addrs)).join(',').downcase.to_s),
|
||||
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
|
||||
|
||||
add_attachments(issue)
|
||||
save_email_as_calendar(issue)
|
||||
save_email_as_attachment(helpdesk_ticket)
|
||||
|
||||
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.reload if issue.persisted?
|
||||
issue.save!(validate: false)
|
||||
container.issue = issue.reload
|
||||
HelpdeskMailRule.apply_rules(:incoming, container)
|
||||
|
||||
add_contact_note(issue) && send_auto_answer(issue)
|
||||
|
||||
logger.try(:info, "#{email.message_id}: issue ##{issue.id} created by #{container.from_user} for #{contact.name}")
|
||||
issue
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def add_contact_note(issue)
|
||||
return true if HelpdeskSettings['helpdesk_add_contact_notes', issue.project].to_i == 0
|
||||
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)
|
||||
end
|
||||
|
||||
def send_auto_answer(issue)
|
||||
return true if HelpdeskSettings['helpdesk_send_notification', issue.project].to_i == 0
|
||||
|
||||
notification = HelpdeskMailer.auto_answer(contact, issue, to_address: slice_emails(container.from_addr.downcase.to_s))
|
||||
logger.try(:info, "#{email.message_id}: notification was sent to #{notification.to_addrs.first}") if notification
|
||||
rescue Exception => e
|
||||
logger.try(:error, "#{email.message_id}: notification was not sent #{e.message}")
|
||||
end
|
||||
|
||||
def helpdesk_issue_attributes(issue)
|
||||
attrs = {
|
||||
'status_id' => status_id,
|
||||
'priority_id' => priority_id,
|
||||
'category_id' => category_id(issue),
|
||||
'assigned_to_id' => assigned_to_id(issue),
|
||||
'fixed_version_id' => version_id(issue),
|
||||
'start_date' => start_date,
|
||||
'due_date' => due_date,
|
||||
'estimated_hours' => estimated_hours,
|
||||
'done_ratio' => done_ratio,
|
||||
'is_private' => is_private,
|
||||
'parent_issue_id' => parent_issue_id,
|
||||
}.delete_if { |_k, v| v.blank? }
|
||||
|
||||
attrs
|
||||
end
|
||||
|
||||
def helpdesk_custom_field_values(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 tracker_id(issue)
|
||||
issue.project.trackers.named(get_keyword(:tracker, override: true)).map(&:id).first ||
|
||||
issue.project.trackers.where(id: get_keyword(:tracker_id)).first.try(:id) ||
|
||||
issue.project.trackers.first.try(:id)
|
||||
end
|
||||
|
||||
def status_id
|
||||
IssueStatus.named(get_keyword(:status)).map(&:id).first || IssueStatus.where(id: get_keyword(:status_id)).first.try(:id)
|
||||
end
|
||||
|
||||
def priority_id
|
||||
IssuePriority.named(get_keyword(:priority)).map(&:id).first || IssuePriority.where(id: get_keyword(:priority_id)).first.try(:id)
|
||||
end
|
||||
|
||||
def category_id(issue)
|
||||
key = get_keyword(:category, override: true).try(:squish)
|
||||
issue.project.issue_categories.named(key).map(&:id).first if key
|
||||
end
|
||||
|
||||
def assigned_to_id(issue)
|
||||
key = get_keyword(:assigned_to, override: true).try(:squish) || get_keyword(:assigned_to_id, override: true)
|
||||
assigned = Principal.detect_by_keyword(issue.assignable_users, key) || User.where(id: key).first
|
||||
assigned ||= Group.where(id: key).first if Setting.issue_group_assignment? && key
|
||||
assigned.try(:id)
|
||||
end
|
||||
|
||||
def version_id(issue)
|
||||
issue.project.shared_versions.named(get_keyword(:fixed_version, override: true)).map(&:id).first
|
||||
end
|
||||
|
||||
def start_date
|
||||
get_keyword(:start_date, override: true, format: '\d{4}-\d{2}-\d{2}')
|
||||
end
|
||||
|
||||
def due_date
|
||||
get_keyword(:due_date, override: true, format: '\d{4}-\d{2}-\d{2}')
|
||||
end
|
||||
|
||||
def estimated_hours
|
||||
get_keyword(:estimated_hours, override: true)
|
||||
end
|
||||
|
||||
def done_ratio
|
||||
get_keyword(:done_ratio, override: true, format: '(\d|10)?0')
|
||||
end
|
||||
|
||||
def is_private
|
||||
get_keyword(:is_private, override: true)
|
||||
end
|
||||
|
||||
def parent_issue_id
|
||||
get_keyword(:parent_issue, override: true)
|
||||
end
|
||||
|
||||
def cleaned_up_subject(email)
|
||||
return '' if email[:subject].blank?
|
||||
subject = decode_subject(email[:subject].respond_to?(:unparsed_value) ? email[:subject].unparsed_value : email[:subject].value)
|
||||
subject = HelpdeskMailSupport.convert_to_utf8(subject)
|
||||
subject.gsub!(/./) { |c| c.bytesize == 4 ? '' : c }
|
||||
subject.strip[0, 255]
|
||||
rescue Exception => e
|
||||
logger.try(:error, "#{email.message_id}: Message subject processing error - #{e.message}")
|
||||
'(Unprocessable subject)'
|
||||
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.gsub(/\=\?.+?\?[qQbB]\?.+?\?\=/) 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
|
||||
end
|
68
app/models/helpdesk_mail_recipient/issue_reply_recipient.rb
Normal file
68
app/models/helpdesk_mail_recipient/issue_reply_recipient.rb
Normal file
@ -0,0 +1,68 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskMailRecipient::IssueReplyRecipient < HelpdeskMailRecipient
|
||||
def receive(issue_id)
|
||||
issue = Issue.where(id: issue_id).first
|
||||
return unless issue
|
||||
return receive_new_issue if HelpdeskSettings['helpdesk_lifetime', project].present? &&
|
||||
HelpdeskSettings['helpdesk_lifetime', project].to_i >= 0 &&
|
||||
lifetime_expired?(issue)
|
||||
|
||||
journal = issue.init_journal(container.from_user)
|
||||
journal.notes = container.text_body
|
||||
journal_message = JournalMessage.create(from_address: slice_emails(container.from_addr.downcase),
|
||||
to_address: slice_emails(email.to_addrs.join(',').downcase),
|
||||
bcc_address: slice_emails(email.bcc_addrs.join(',').downcase),
|
||||
cc_address: slice_emails((to_recipients + except_service_emails(email.cc_addrs)).join(',').downcase.to_s),
|
||||
message_id: email.message_id,
|
||||
is_incoming: true,
|
||||
message_date: email.date || Time.now,
|
||||
contact: contact,
|
||||
journal: journal)
|
||||
|
||||
add_attachments(issue)
|
||||
save_email_as_calendar(issue)
|
||||
save_email_as_attachment(journal_message, "reply-#{DateTime.now.strftime('%d.%m.%y-%H.%M.%S')}.eml")
|
||||
issue.status_id = reopen_status_id if reopen_status_id
|
||||
|
||||
issue.save!(:validate => false)
|
||||
container.issue = issue.reload
|
||||
HelpdeskMailRule.apply_rules(:incoming, container)
|
||||
|
||||
logger.try(:info, "#{email.message_id}: issue ##{issue.id} updated by #{container.from_user}")
|
||||
journal
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def receive_new_issue
|
||||
email.subject = email.subject.to_s.gsub(MailHandler::ISSUE_REPLY_SUBJECT_RE, '')
|
||||
HelpdeskMailRecipient::IssueRecipient.new(container).receive
|
||||
end
|
||||
|
||||
def reopen_status_id
|
||||
IssueStatus.named(get_keyword(:reopen_status)).first.try(:id) || IssueStatus.where(id: get_keyword(:reopen_status_id)).first.try(:id)
|
||||
end
|
||||
|
||||
def lifetime_expired?(issue)
|
||||
last_date = (issue.journals.last || issue).created_on.to_date
|
||||
Date.today - last_date >= HelpdeskSettings['helpdesk_lifetime', project].to_i
|
||||
end
|
||||
end
|
229
app/models/helpdesk_mail_rule.rb
Normal file
229
app/models/helpdesk_mail_rule.rb
Normal file
@ -0,0 +1,229 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskMailRule < ApplicationRecord
|
||||
include Redmine::SafeAttributes
|
||||
include Redmine::I18n
|
||||
|
||||
NO_VALUES_ACTION_KEYS = %w[stop]
|
||||
|
||||
INCOMING = 0
|
||||
OUTGOING = 1
|
||||
MANUALLY = 2
|
||||
|
||||
validate :presence_actions
|
||||
validate :validate_conditions_and_actions
|
||||
|
||||
belongs_to :user
|
||||
serialize :conditions
|
||||
serialize :actions
|
||||
|
||||
cattr_accessor :available_conditions
|
||||
cattr_accessor :available_actions
|
||||
|
||||
up_acts_as_list
|
||||
|
||||
safe_attributes 'mail_type', 'conditions', 'actions', 'move_to', 'position'
|
||||
|
||||
scope :by_position, -> { order(:position) }
|
||||
scope :incoming, -> { where(mail_type: INCOMING) }
|
||||
scope :outgoing, -> { where(mail_type: OUTGOING) }
|
||||
scope :manually, -> { where(mail_type: MANUALLY) }
|
||||
|
||||
def self.add_condition(condition_class)
|
||||
self.available_conditions ||= {}
|
||||
self.available_conditions[condition_class.new.field] = condition_class
|
||||
true
|
||||
end
|
||||
|
||||
def self.add_action(action_class)
|
||||
self.available_actions ||= {}
|
||||
self.available_actions[action_class.new.field] = action_class
|
||||
true
|
||||
end
|
||||
|
||||
def self.apply_rules(direction, container)
|
||||
return unless [:incoming, :outgoing, :manually].include?(direction)
|
||||
with_initialized_journal(container) do
|
||||
send(direction).by_position.find_each { |rule| break unless rule.apply_to(container) }
|
||||
end
|
||||
end
|
||||
|
||||
def self.with_initialized_journal(container)
|
||||
rule_changes_journal = Journal.new(journalized: container.issue,
|
||||
user: container.from_user,
|
||||
notes: l(:label_helpdesk_email_rules_changes))
|
||||
yield
|
||||
rule_changes_journal.save if rule_changes_journal.details.any?
|
||||
end
|
||||
|
||||
def apply_to(container)
|
||||
return true unless suitable_conditions?(container)
|
||||
result = apply_actions(container)
|
||||
actions.keys.include?(RedmineHelpdesk::MailRules::Actions::Stop.new.field) ? false : result
|
||||
end
|
||||
|
||||
def available_conditions
|
||||
return @available_conditions if @available_conditions
|
||||
@available_conditions = ActiveSupport::OrderedHash.new
|
||||
Hash[self.class.available_conditions.sort].each_value do |condition_class|
|
||||
condition = condition_class.new
|
||||
@available_conditions[condition.field] = HelpdeskMailRuleElement.new(condition.field, condition.options.merge(values: condition.values(self)))
|
||||
end
|
||||
@available_conditions
|
||||
end
|
||||
|
||||
def available_actions
|
||||
return @available_actions if @available_actions
|
||||
@available_actions = ActiveSupport::OrderedHash.new
|
||||
Hash[self.class.available_actions.sort].each_value do |action_class|
|
||||
action = action_class.new
|
||||
@available_actions[action.field] = HelpdeskMailRuleElement.new(action.field, action.options.merge(values: action.values(self)))
|
||||
end
|
||||
@available_actions
|
||||
end
|
||||
|
||||
def available_conditions_as_json
|
||||
json = {}
|
||||
available_conditions.each do |field, condition|
|
||||
options = { type: condition[:type], name: condition[:name] }
|
||||
options[:remote] = true if condition.remote
|
||||
|
||||
if has_condition?(field) || !condition.remote
|
||||
options[:values] = condition.values
|
||||
if options[:values] && condition_values_for(field)
|
||||
missing = Array(condition_values_for(field)).select(&:present?) - options[:values].map(&:last)
|
||||
if missing.any? && respond_to?(method = "find_#{field}_filter_values")
|
||||
options[:values] += send(method, missing)
|
||||
end
|
||||
end
|
||||
end
|
||||
json[field] = options.stringify_keys
|
||||
end
|
||||
json
|
||||
end
|
||||
|
||||
def available_actions_as_json
|
||||
json = {}
|
||||
available_actions.each do |field, action|
|
||||
options = { type: action[:type], name: action[:name] }
|
||||
options[:remote] = true if action.remote
|
||||
|
||||
if has_action?(field) || !action.remote
|
||||
options[:values] = action.values
|
||||
if options[:values] && action_values_for(field)
|
||||
missing = Array(action_values_for(field)).select(&:present?) - options[:values].map(&:last)
|
||||
if missing.any? && respond_to?(method = "find_#{field}_filter_values")
|
||||
options[:values] += send(method, missing)
|
||||
end
|
||||
end
|
||||
end
|
||||
json[field] = options.stringify_keys
|
||||
end
|
||||
json
|
||||
end
|
||||
|
||||
def conditions
|
||||
super || {}
|
||||
end
|
||||
|
||||
def actions
|
||||
super || {}
|
||||
end
|
||||
|
||||
def condition_values_for(field)
|
||||
has_condition?(field) ? conditions[field][:values] : []
|
||||
end
|
||||
|
||||
def action_values_for(field)
|
||||
has_action?(field) ? actions[field][:values] : []
|
||||
end
|
||||
|
||||
def operator_for_condition(field)
|
||||
has_condition?(field) ? conditions[field][:operator] : nil
|
||||
end
|
||||
|
||||
def operator_for_action(field)
|
||||
has_action?(field) ? actions[field][:operator] : nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def presence_actions
|
||||
errors.add(:base, l(:mail_rule_should_have_action)) unless actions.any?
|
||||
end
|
||||
|
||||
def validate_conditions_and_actions
|
||||
check_condition_values
|
||||
check_action_values
|
||||
end
|
||||
|
||||
def has_condition?(field)
|
||||
conditions && conditions[field]
|
||||
end
|
||||
|
||||
def has_action?(field)
|
||||
actions && actions[field]
|
||||
end
|
||||
|
||||
def suitable_conditions?(container)
|
||||
return true if conditions.empty?
|
||||
|
||||
begin
|
||||
constions_result = conditions.map do |condition, options|
|
||||
condition_class = self.class.available_conditions[condition]
|
||||
next unless condition_class
|
||||
condition_class.new.check(container, options[:operator], options[:values])
|
||||
end
|
||||
constions_result.all?
|
||||
rescue Exception => e
|
||||
HelpdeskLogger.try(:error, "Rule condition error: #{[e.message, e.backtrace.first].join("\n")}")
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def apply_actions(container)
|
||||
actions.each do |action, options|
|
||||
action_class = self.class.available_actions[action]
|
||||
next unless action_class
|
||||
begin
|
||||
action_class.new.apply(container, options[:operator], options[:values])
|
||||
rescue Exception => e
|
||||
HelpdeskLogger.try(:error, "Rule action applying error: #{[e.message, e.backtrace.first].join("\n")}")
|
||||
next
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def check_condition_values
|
||||
conditions.each do |key, opt|
|
||||
add_error(key, :blank) if ['~', '=', '!~', '!'].include?(opt[:operator]) && (opt[:values].blank? || opt[:values].first.blank?)
|
||||
end
|
||||
end
|
||||
|
||||
def check_action_values
|
||||
actions.except(*NO_VALUES_ACTION_KEYS).each do |key, opt|
|
||||
add_error(key, :blank) if [nil, '~', '=', '!~', '!'].include?(opt[:operator]) && (opt[:values].blank? || opt[:values].first.blank?)
|
||||
end
|
||||
end
|
||||
|
||||
def add_error(elem, message)
|
||||
errors.add(:base, elem.upcase + ' ' + l(message, :scope => 'activerecord.errors.messages'))
|
||||
end
|
||||
end
|
52
app/models/helpdesk_mail_rule_element.rb
Normal file
52
app/models/helpdesk_mail_rule_element.rb
Normal file
@ -0,0 +1,52 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskMailRuleElement
|
||||
include Redmine::I18n
|
||||
|
||||
def initialize(field, options)
|
||||
@field = field.to_s
|
||||
@options = options
|
||||
@options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
|
||||
# Consider filters with a Proc for values as remote by default
|
||||
@remote = options.key?(:remote) ? options[:remote] : options[:values].is_a?(Proc)
|
||||
end
|
||||
|
||||
def [](arg)
|
||||
if arg == :values
|
||||
values
|
||||
else
|
||||
@options[arg]
|
||||
end
|
||||
end
|
||||
|
||||
def values
|
||||
@values ||= begin
|
||||
values = @options[:values]
|
||||
if values.is_a?(Proc)
|
||||
values = values.call
|
||||
end
|
||||
values
|
||||
end
|
||||
end
|
||||
|
||||
def remote
|
||||
@remote
|
||||
end
|
||||
end
|
347
app/models/helpdesk_mail_support.rb
Normal file
347
app/models/helpdesk_mail_support.rb
Normal file
@ -0,0 +1,347 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
require 'digest/md5'
|
||||
class HelpdeskMailSupport < MailHandler
|
||||
URL_HELPER = Rails.application.routes.url_helpers
|
||||
|
||||
class << self
|
||||
def ignored_helpdesk_headers
|
||||
helpdesk_headers = {
|
||||
'X-Auto-Response-Suppress' => /\A(all|AutoReply|oof)/
|
||||
}
|
||||
ignored_emails_headers.merge(helpdesk_headers)
|
||||
end
|
||||
|
||||
def options_for_check(project_id, options = {})
|
||||
[
|
||||
common_options(project_id).merge(provider_options(project_id)),
|
||||
options.merge(issue_options({}, project_id))
|
||||
]
|
||||
end
|
||||
|
||||
def 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]
|
||||
options[:issue][:assigned_to_id] ||= HelpdeskSettings[:helpdesk_assigned_to, project_id]
|
||||
options[:issue][:tracker_id] ||= HelpdeskSettings[:helpdesk_tracker, project_id]
|
||||
options[:issue][:priority_id] ||= HelpdeskSettings[:helpdesk_issue_priority, project_id]
|
||||
options[:issue][:due_date] ||= HelpdeskSettings[:helpdesk_issue_due_date, project_id]
|
||||
options[:issue][:reopen_status_id] ||= HelpdeskSettings[:helpdesk_reopen_status, project_id]
|
||||
options
|
||||
end
|
||||
|
||||
def check_ignored_headers?(email, logger = nil)
|
||||
ignored_helpdesk_headers.each do |key, ignored_value|
|
||||
value = email.header[key].to_s.downcase
|
||||
next if value.blank?
|
||||
if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
|
||||
logger.try(:info, "#{email.message_id}: ignoring email with #{key}:#{value} header")
|
||||
return true
|
||||
end
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
def check_blacklist?(email, project)
|
||||
return false if HelpdeskSettings['helpdesk_blacklist', project].blank?
|
||||
cond = '(' + HelpdeskSettings['helpdesk_blacklist', project].split("\n").map { |u| u.strip if u.present? }.compact.join('|') + ')'
|
||||
!!email.match(cond)
|
||||
end
|
||||
|
||||
def create_contact_from_address(addr, fullname, project, logger = nil)
|
||||
contacts = Contact.find_by_emails([addr])
|
||||
if contacts.present?
|
||||
contact = contacts.first
|
||||
if contact.projects.blank? || HelpdeskSettings['helpdesk_add_contact_to_project', project].to_i > 0
|
||||
contact.projects << project
|
||||
contact.save!
|
||||
end
|
||||
return contact
|
||||
end
|
||||
|
||||
if HelpdeskSettings['helpdesk_is_not_create_contacts', project].to_i > 0
|
||||
logger.try(:error, "#{email.message_id}: can't find contact with email: #{addr} in whitelist. Not create new contacts option enable")
|
||||
nil
|
||||
else
|
||||
contact = contact_from_attributes(addr, fullname)
|
||||
contact.projects << project
|
||||
contact.tag_list = HelpdeskSettings['helpdesk_created_contact_tag', project] if HelpdeskSettings['helpdesk_created_contact_tag', project]
|
||||
|
||||
if contact.save
|
||||
contact
|
||||
else
|
||||
logger.try(:error, "Helpdeks MailHandler: failed to create Contact: #{contact.errors.full_messages}")
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def convert_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 escaped_cleaned_up_text_body(email)
|
||||
text_body = new.substitution_method(:cleaned_up_text_body, email)
|
||||
# replacing inline images for office365, gmail e.g. [cid:image001.png@01D8C6A5.EA89EA00] or [image: image.png]
|
||||
text_body = replace_img_tags(text_body, email)
|
||||
return text_body if (ActiveRecord::Base.connection.adapter_name =~ /mysql/i).blank?
|
||||
text_body.gsub(/./) { |c| c.bytesize == 4 ? ' ' : c }
|
||||
end
|
||||
|
||||
def extract_from_text!(text, attr, user, format = nil)
|
||||
new.substitution_method(:extract_keyword!, nil, user, text, attr, format)
|
||||
end
|
||||
|
||||
def attachment_digest(file_source)
|
||||
encoder = Digest::SHA256.new
|
||||
encoder.update(file_source)
|
||||
encoder.hexdigest
|
||||
end
|
||||
|
||||
def attachment_acceptable?(attachment)
|
||||
new.send(:accept_attachment?, attachment)
|
||||
end
|
||||
|
||||
def apply_text_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.to_s)
|
||||
text = text.gsub(/%%MIDDLE_NAME%%|\{%contact.middle_name%\}/, contact.middle_name.to_s)
|
||||
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%\}/, full_url(URL_HELPER.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%\}/, full_url(URL_HELPER.helpdesk_votes_show_path(issue.helpdesk_ticket.id, issue.helpdesk_ticket.token))) if text.match(/\{%ticket.voting%\}/)
|
||||
text = text.gsub(/\{%ticket.voting.good%\}/, full_url(URL_HELPER.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%\}/, full_url(URL_HELPER.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%\}/, full_url(URL_HELPER.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 = ''
|
||||
journals = issue.journals.eager_load(:journal_message)
|
||||
unless User.current.allowed_to?(:view_private_notes, issue.project)
|
||||
journals = journals.select do |journal|
|
||||
!journal.private_notes? || journal.user == user
|
||||
end
|
||||
end
|
||||
journals.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
|
||||
|
||||
def apply_attachment_macro(mail_attachments, 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
|
||||
mail_attachments[attachment.filename] = { mime_type: attachment.content_type, content: File.open(attachment.diskfile, 'rb') { |io| io.read } } if attachment
|
||||
end
|
||||
text.gsub(/\{\{send_file\(([^%\}]+)\)\}\}/, '')
|
||||
end
|
||||
|
||||
def 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
|
||||
|
||||
private
|
||||
|
||||
def provider_options(project_id)
|
||||
case HelpdeskSettings[:helpdesk_protocol, project_id]
|
||||
when 'gmail'
|
||||
{ protocol: 'imap', host: 'imap.gmail.com', port: '993', ssl: '1', verify_ssl: false }
|
||||
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' }
|
||||
when 'outlook'
|
||||
{
|
||||
protocol: HelpdeskSettings[:helpdesk_protocol, project_id],
|
||||
project_id: project_id,
|
||||
folder: HelpdeskSettings[:helpdesk_outlook_folder, project_id],
|
||||
move_on_success: HelpdeskSettings[:helpdesk_outlook_move_on_success, project_id],
|
||||
move_on_failure: HelpdeskSettings[:helpdesk_outlook_move_on_failure, project_id],
|
||||
}
|
||||
when 'google'
|
||||
{
|
||||
protocol: HelpdeskSettings[:helpdesk_protocol, project_id],
|
||||
project_id: project_id,
|
||||
folder: HelpdeskSettings[:helpdesk_google_folder, project_id],
|
||||
move_on_success: HelpdeskSettings[:helpdesk_google_move_on_success, project_id],
|
||||
move_on_failure: HelpdeskSettings[:helpdesk_google_move_on_failure, project_id],
|
||||
}
|
||||
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',
|
||||
starttls: HelpdeskSettings[:helpdesk_use_starttls, project_id] != '1' ? nil : '1' }
|
||||
end
|
||||
end
|
||||
|
||||
def common_options(project_id)
|
||||
{
|
||||
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
|
||||
}
|
||||
end
|
||||
|
||||
def contact_from_attributes(email, fullname = nil)
|
||||
contact = Contact.new
|
||||
|
||||
# Truncating the email address would result in an invalid format
|
||||
contact.email = email
|
||||
names = fullname.blank? ? email.gsub(/@.*$/, '').split('.') : fullname.split
|
||||
contact.first_name = names.shift.slice(0, 255)
|
||||
contact.last_name = names.join(' ').slice(0, 255)
|
||||
contact.company = email.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
|
||||
end
|
||||
|
||||
def full_url(path)
|
||||
[Setting.protocol, '://', Setting.host_name, path].join
|
||||
end
|
||||
|
||||
def replace_img_tags(text, email)
|
||||
inline_cids = Hash[email.attachments.select(&:content_id).map { |attachment| [attachment.content_id.to_s.gsub(/<|>/, ''), attachment.filename] }]
|
||||
text = text.gsub(/\[cid:\s*(.+)\]/) { |img| inline_cids[$1] ? "!#{inline_cids[$1]}!" : $1 }
|
||||
text = text.gsub(/"cid:\s*(.+?)"/) { |img| inline_cids[$1] ? "\"#{inline_cids[$1]}\"" : $1 }
|
||||
text.gsub(/\[image:\s*(.+)\]/) { |img| inline_cids[$1] ? "!#{inline_cids[$1]}!" : "!#{$1}!" }
|
||||
end
|
||||
end
|
||||
|
||||
def plain_text_body
|
||||
return @plain_text_body unless @plain_text_body.nil?
|
||||
return '' if email.mime_type && email.mime_type.starts_with?('image/')
|
||||
part = RedmineHelpdesk.html_body_prefer? ? email.html_part : email.text_part || email.html_part
|
||||
unless part
|
||||
return @plain_text_body = '' if email.parts.present? && email.parts.all? { |em_part| em_part.attachment? }
|
||||
part = email
|
||||
end
|
||||
|
||||
part_charset = pick_encoding(part.charset).to_s rescue part.charset
|
||||
@plain_text_body = self.class.convert_to_utf8(part.body.decoded, part_charset)
|
||||
|
||||
if email.mime_type == 'text/calendar'
|
||||
@plain_text_body = email.body.to_s.scan(/^DESCRIPTION:(.+)$/) && $1.to_s
|
||||
elsif (email.text_part.present? || email.mime_type == 'text/plain')
|
||||
@plain_text_body = self.class.plain_text_body_to_text(@plain_text_body)
|
||||
@plain_text_body = @plain_text_body.gsub(/[\r|\n]/, '') if RedmineHelpdesk.html_body_prefer?
|
||||
@plain_text_body
|
||||
elsif (email.html_part.present? || email.mime_type == 'text/html' || ['multipart/related', 'multipart/alternative'].include?(email.mime_type))
|
||||
@plain_text_body = self.class.html_body_to_text(@plain_text_body)
|
||||
else
|
||||
@plain_text_body = self.class.plain_text_body_to_text(@plain_text_body)
|
||||
end
|
||||
rescue Exception => e
|
||||
HelpdeskLogger.error "#{email && email.message_id}: Message body processing error - #{e.message}"
|
||||
@plain_text_body = '(Unprocessable message body)'
|
||||
end
|
||||
|
||||
def pick_encoding(enc)
|
||||
Mail::RubyVer.respond_to?(:pick_encoding) ? Mail::RubyVer.pick_encoding(enc) : Mail::Utilities.pick_encoding(enc)
|
||||
end
|
||||
end
|
92
app/models/helpdesk_mailer.rb
Normal file
92
app/models/helpdesk_mailer.rb
Normal file
@ -0,0 +1,92 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskMailer
|
||||
class << self
|
||||
def check_project(project_id, options = {})
|
||||
project = Project.where(id: project_id).first
|
||||
return 0 if project.nil? || HelpdeskSettings[:helpdesk_protocol, project.id].blank?
|
||||
|
||||
mail_options, options = HelpdeskMailSupport.options_for_check(project_id, options)
|
||||
|
||||
msg_count =
|
||||
case mail_options[:protocol]
|
||||
when 'pop3'
|
||||
HelpdeskMailProvider::Pop3Provider.new(mail_options, with_logger(options)).check
|
||||
when 'imap'
|
||||
HelpdeskMailProvider::ImapProvider.new(mail_options, with_logger(options)).check
|
||||
when 'api'
|
||||
HelpdeskMailProvider::ApiProvider.new(mail_options, with_logger(options)).check
|
||||
when 'outlook'
|
||||
HelpdeskMailProvider::MicrosoftProvider.new(mail_options, with_logger(options)).check
|
||||
when 'google'
|
||||
HelpdeskMailProvider::GoogleProvider.new(mail_options, with_logger(options)).check
|
||||
end
|
||||
|
||||
HelpdeskTicket.autoclose(project)
|
||||
msg_count
|
||||
end
|
||||
|
||||
def receive(raw_message, options = {})
|
||||
HelpdeskMailProvider.receive(raw_message, with_logger(options))
|
||||
end
|
||||
|
||||
def initial_message(contact, issue, options = {})
|
||||
with_performed_delivery do
|
||||
HelpdeskMailMessenger::InitialMessage.prepare_email(contact, issue, with_logger(options)).deliver
|
||||
end
|
||||
end
|
||||
|
||||
def auto_answer(contact, issue, options = {})
|
||||
with_performed_delivery do
|
||||
HelpdeskMailMessenger::AutoAnswerMessage.prepare_email(contact, issue, with_logger(options)).deliver
|
||||
end
|
||||
end
|
||||
|
||||
def issue_response(contact, journal, options = {})
|
||||
with_performed_delivery do
|
||||
HelpdeskMailMessenger::IssueResponseMessage.prepare_email(contact, journal, with_logger(options)).deliver
|
||||
end
|
||||
end
|
||||
|
||||
def autoclose_message(contact, issue, options = {})
|
||||
with_performed_delivery do
|
||||
HelpdeskMailMessenger::AutocloseMessage.prepare_email(contact, issue, with_logger(options)).deliver
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def logger
|
||||
HelpdeskLogger
|
||||
end
|
||||
|
||||
def with_logger(options)
|
||||
options.merge(logger: logger)
|
||||
end
|
||||
|
||||
def with_performed_delivery
|
||||
perform_delivery_state = ActionMailer::Base.perform_deliveries
|
||||
ActionMailer::Base.perform_deliveries = true
|
||||
yield
|
||||
ensure
|
||||
ActionMailer::Base.perform_deliveries = perform_delivery_state
|
||||
end
|
||||
end
|
||||
end
|
34
app/models/helpdesk_oauth_mailer.rb
Normal file
34
app/models/helpdesk_oauth_mailer.rb
Normal file
@ -0,0 +1,34 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskOauthMailer
|
||||
attr_accessor :settings
|
||||
|
||||
DEFAULTS = {
|
||||
project: nil
|
||||
}
|
||||
|
||||
def initialize(settings = {})
|
||||
self.settings = DEFAULTS.merge(settings)
|
||||
end
|
||||
|
||||
def deliver!(mail)
|
||||
raise 'Should be implement in inherited class'
|
||||
end
|
||||
end
|
26
app/models/helpdesk_oauth_mailer/google.rb
Normal file
26
app/models/helpdesk_oauth_mailer/google.rb
Normal file
@ -0,0 +1,26 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskOauthMailer::Google < HelpdeskOauthMailer
|
||||
def deliver!(mail)
|
||||
encoded_mail = Base64.urlsafe_encode64(mail.to_s)
|
||||
HelpdeskOauthProvider::Google.new(settings[:project].id).send_message(encoded_mail)
|
||||
self
|
||||
end
|
||||
end
|
26
app/models/helpdesk_oauth_mailer/microsoft.rb
Normal file
26
app/models/helpdesk_oauth_mailer/microsoft.rb
Normal file
@ -0,0 +1,26 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskOauthMailer::Microsoft < HelpdeskOauthMailer
|
||||
def deliver!(mail)
|
||||
encoded_mail = Base64.strict_encode64(mail.to_s)
|
||||
HelpdeskOauthProvider::Microsoft.new(settings[:project].id).send_message(encoded_mail)
|
||||
self
|
||||
end
|
||||
end
|
136
app/models/helpdesk_oauth_provider.rb
Normal file
136
app/models/helpdesk_oauth_provider.rb
Normal file
@ -0,0 +1,136 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskOauthProvider
|
||||
class SendMailError < StandardError; end
|
||||
|
||||
PROVIDERS = {
|
||||
outlook: HelpdeskOauthProvider::Microsoft,
|
||||
google: HelpdeskOauthProvider::Google
|
||||
}.freeze
|
||||
attr_accessor :project, :redirect_uri
|
||||
|
||||
def self.find_by_protocol(protocol)
|
||||
PROVIDERS[protocol&.to_sym]
|
||||
end
|
||||
|
||||
def initialize(project_id)
|
||||
@project = Project.find(project_id)
|
||||
@redirect_uri = Rails.application.routes.url_helpers.helpdesk_oauth_resp_url(protocol: Setting.protocol, host: Setting.host_name)
|
||||
end
|
||||
|
||||
def reset_token
|
||||
write_tokens(nil, nil)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def logger
|
||||
HelpdeskLogger
|
||||
end
|
||||
|
||||
def logged_error(message = nil)
|
||||
logger.error(message) if message
|
||||
false
|
||||
end
|
||||
|
||||
def access_token_name
|
||||
raise NoMethodError, 'Should be implemented in a subclass'
|
||||
end
|
||||
|
||||
def refresh_token_name
|
||||
raise NoMethodError, 'Should be implemented in a subclass'
|
||||
end
|
||||
|
||||
def update_token
|
||||
raise NoMethodError, 'Should be implemented in a subclass'
|
||||
end
|
||||
|
||||
def write_tokens(access, refresh)
|
||||
ContactsSetting[access_token_name, project.id] = access
|
||||
ContactsSetting[refresh_token_name, project.id] = refresh
|
||||
end
|
||||
|
||||
def is_ok?(code)
|
||||
[200, 202].include?(code.to_i)
|
||||
end
|
||||
|
||||
def execute(type, url, params = {})
|
||||
uri = URI(url)
|
||||
req = send("build_#{type.downcase}_req", url, params) if [:GET, :FPOST, :JPOST, :TPOST, :PATCH, :RFPOST].include?(type)
|
||||
raise 'Incorrect request type' unless req
|
||||
|
||||
req['Authorization'] = "Bearer #{HelpdeskSettings[access_token_name, project]}"
|
||||
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |conn|
|
||||
conn.request(req)
|
||||
end
|
||||
|
||||
code, data = execute_with_reloaded_token(type, url, params) if resp.is_a?(Net::HTTPUnauthorized) && !params[:reloaded]
|
||||
[(code || resp.code).to_i, data || (params[:raw] ? resp.body : JSON.parse(resp.body))]
|
||||
end
|
||||
|
||||
def execute_with_reloaded_token(type, url, params)
|
||||
update_token
|
||||
execute(type, url, params.merge(reloaded: true))
|
||||
end
|
||||
|
||||
def build_get_req(url, params)
|
||||
get_url = url
|
||||
if params&.any?
|
||||
quoted_params = params.to_query
|
||||
quoted_params = quoted_params.gsub('labelIds%5B%5D', 'labelIds') if params[:g_labels]
|
||||
get_url = get_url + '?' + quoted_params
|
||||
end
|
||||
Net::HTTP::Get.new(get_url)
|
||||
end
|
||||
|
||||
def build_fpost_req(url, params)
|
||||
post_req = Net::HTTP::Post.new(url)
|
||||
post_req.set_form_data(params) if params
|
||||
post_req
|
||||
end
|
||||
|
||||
def build_jpost_req(url, params)
|
||||
post_req = Net::HTTP::Post.new(url)
|
||||
post_req.content_type = "application/json"
|
||||
post_req.body = JSON.dump(params) if params
|
||||
post_req
|
||||
end
|
||||
|
||||
def build_rfpost_req(url, params)
|
||||
post_req = Net::HTTP::Post.new(url)
|
||||
post_req.content_type = "message/rfc822"
|
||||
post_req.body = JSON.dump(params) if params
|
||||
post_req
|
||||
end
|
||||
|
||||
def build_tpost_req(url, params)
|
||||
post_req = Net::HTTP::Post.new(url)
|
||||
post_req.content_type = "text/plain"
|
||||
post_req.body = params[:body] if params
|
||||
post_req
|
||||
end
|
||||
|
||||
def build_patch_req(url, params)
|
||||
patch_req = Net::HTTP::Patch.new(url)
|
||||
patch_req.content_type = "application/json"
|
||||
patch_req.body = JSON.dump(params) if params
|
||||
patch_req
|
||||
end
|
||||
end
|
189
app/models/helpdesk_oauth_provider/google.rb
Normal file
189
app/models/helpdesk_oauth_provider/google.rb
Normal file
@ -0,0 +1,189 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskOauthProvider::Google < HelpdeskOauthProvider
|
||||
BASE_URL = 'https://accounts.google.com/o/oauth2/v2/auth'.freeze
|
||||
TOKEN_URL = 'https://oauth2.googleapis.com/token'.freeze
|
||||
MAILBOX_URL = 'https://gmail.googleapis.com/gmail/v1/users/<email>'.freeze
|
||||
FOLDERS_URL = MAILBOX_URL + '/labels'.freeze
|
||||
MESSAGES_URL = MAILBOX_URL + '/messages'.freeze
|
||||
SENDMAIL_URL = MESSAGES_URL + '/send'.freeze
|
||||
SCOPE = 'https://www.googleapis.com/auth/gmail.modify'.freeze
|
||||
|
||||
def authorize_url(state_secret)
|
||||
params = {
|
||||
client_id: RedmineHelpdesk.settings['helpdesk_oauth_google_client_id'],
|
||||
scope: SCOPE,
|
||||
redirect_uri: redirect_uri,
|
||||
access_type: 'offline',
|
||||
response_type: 'code'.freeze,
|
||||
response_mode: 'query'.freeze,
|
||||
state: state_secret,
|
||||
}
|
||||
|
||||
BASE_URL + '?' + params.to_query
|
||||
end
|
||||
|
||||
def receive_tokens(code)
|
||||
params = {
|
||||
client_id: RedmineHelpdesk.settings['helpdesk_oauth_google_client_id'],
|
||||
client_secret: RedmineHelpdesk.settings['helpdesk_oauth_google_secret'],
|
||||
scope: SCOPE,
|
||||
redirect_uri: redirect_uri,
|
||||
code: code,
|
||||
grant_type: 'authorization_code'.freeze,
|
||||
}
|
||||
|
||||
code, data = execute(:FPOST, TOKEN_URL, params)
|
||||
logged_error("HelpdeskOauthProvider::Google#receive_tokens error:\nCode: #{code}\nBody: #{data}") unless is_ok?(code)
|
||||
write_tokens(data['access_token'], data['refresh_token']) if is_ok?(code) && data
|
||||
end
|
||||
|
||||
def update_token
|
||||
params = {
|
||||
client_id: RedmineHelpdesk.settings['helpdesk_oauth_google_client_id'],
|
||||
client_secret: RedmineHelpdesk.settings['helpdesk_oauth_google_secret'],
|
||||
refresh_token: HelpdeskSettings['helpdesk_google_refresh_token', project],
|
||||
grant_type: 'refresh_token'.freeze,
|
||||
}
|
||||
|
||||
code, data = execute(:FPOST, TOKEN_URL, params)
|
||||
logged_error("HelpdeskOauthProvider::Google#update_token error:\nCode: #{code}\nBody: #{data}") unless is_ok?(code)
|
||||
if !is_ok?(code) || data.empty?
|
||||
reset_token
|
||||
else
|
||||
write_tokens(data['access_token'], HelpdeskSettings['helpdesk_google_refresh_token', project])
|
||||
end
|
||||
end
|
||||
|
||||
def folders
|
||||
return [] if HelpdeskSettings[:helpdesk_username, project.id].blank?
|
||||
|
||||
Rails.cache.fetch("google_folders_#{project.id}", expires_in: 1.minute) do
|
||||
params = {}
|
||||
folders_url = FOLDERS_URL.gsub('<email>', HelpdeskSettings[:helpdesk_username, project.id])
|
||||
|
||||
code, data = execute(:GET, folders_url, params)
|
||||
return [] if !is_ok?(code) || !data || !data['labels']
|
||||
|
||||
[['INBOX', 'INBOX']] + data['labels'].filter { |fld| fld['type'] == 'user'}.sort_by { |fld| fld['name'] }.map do |fld|
|
||||
full_path = fld['name'].split('/')
|
||||
[
|
||||
'--' * (full_path.size - 1) + ' ' + full_path.last,
|
||||
fld['id']
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def messages(folder = nil)
|
||||
message_ids = new_message_ids(folder)
|
||||
message_ids.map do |mid|
|
||||
[mid, message_raw(mid)]
|
||||
end.to_h
|
||||
end
|
||||
|
||||
def move_message(mid, folder)
|
||||
params = {
|
||||
removeLabelIds: ['INBOX'],
|
||||
addLabelIds: [folder]
|
||||
}
|
||||
|
||||
read_url = message_full_url(mid) + '/modify'
|
||||
code, data = execute(:JPOST, read_url, params)
|
||||
is_ok?(code) ? data : ''
|
||||
end
|
||||
|
||||
def read_message(mid)
|
||||
params = {
|
||||
removeLabelIds: ['UNREAD'],
|
||||
}
|
||||
|
||||
read_url = message_full_url(mid) + '/modify'
|
||||
code, data = execute(:JPOST, read_url, params)
|
||||
is_ok?(code) ? data : ''
|
||||
end
|
||||
|
||||
def send_message(encoded_message)
|
||||
params = {
|
||||
raw: encoded_message
|
||||
}
|
||||
|
||||
sendmail_url = SENDMAIL_URL.gsub('<email>', HelpdeskSettings[:helpdesk_username, project.id])
|
||||
code, data = execute(:RFPOST, sendmail_url, params)
|
||||
logged_error("HelpdeskOauthProvider::Microsoft#send_message error:\nCode: #{code}\nBody: #{data}") unless is_ok?(code)
|
||||
raise SendMailError, data unless is_ok?(code)
|
||||
|
||||
data
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def access_token_name
|
||||
:helpdesk_google_access_token
|
||||
end
|
||||
|
||||
def refresh_token_name
|
||||
:helpdesk_google_refresh_token
|
||||
end
|
||||
|
||||
def message_full_url(mid)
|
||||
MESSAGES_URL.gsub('<email>', HelpdeskSettings[:helpdesk_username, project.id]) + "/#{mid}"
|
||||
end
|
||||
|
||||
def new_message_ids(folder)
|
||||
params = {
|
||||
maxResults: 500,
|
||||
labelIds: [folder, "UNREAD"],
|
||||
g_labels: true,
|
||||
}
|
||||
|
||||
_code, data = execute_with_pages(:GET, MESSAGES_URL.gsub('<email>', HelpdeskSettings[:helpdesk_username, project.id]), params)
|
||||
(data || []).map { |mes| mes['id'] }
|
||||
end
|
||||
|
||||
def message_raw(mid)
|
||||
params = {
|
||||
format: 'raw'
|
||||
}
|
||||
|
||||
code, data = execute(:GET, message_full_url(mid), params)
|
||||
is_ok?(code) ? Base64.urlsafe_decode64(data['raw']) : ''
|
||||
end
|
||||
|
||||
def execute_with_pages(type, url, params = {})
|
||||
total_data = []
|
||||
page_url = url
|
||||
page_params = params
|
||||
next_page = true
|
||||
|
||||
while next_page
|
||||
next_page = false
|
||||
code, data = execute(type, page_url, page_params)
|
||||
if is_ok?(code) && data
|
||||
total_data += data['messages'] if data['messages']
|
||||
if data['nextPageToken']
|
||||
next_page = true
|
||||
page_params = page_params.merge(pageToken: data['nextPageToken'])
|
||||
end
|
||||
end
|
||||
end
|
||||
[code.to_i, total_data]
|
||||
end
|
||||
end
|
207
app/models/helpdesk_oauth_provider/microsoft.rb
Normal file
207
app/models/helpdesk_oauth_provider/microsoft.rb
Normal file
@ -0,0 +1,207 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskOauthProvider::Microsoft < HelpdeskOauthProvider
|
||||
BASE_URL = 'https://login.microsoftonline.com/<tenant_id>/oauth2/v2.0/authorize'.freeze
|
||||
TOKEN_URL = 'https://login.microsoftonline.com/<tenant_id>/oauth2/v2.0/token'.freeze
|
||||
MAILBOX_URL = 'https://graph.microsoft.com/v1.0/users/<email>'.freeze
|
||||
FOLDERS_URL = MAILBOX_URL + '/mailFolders'.freeze
|
||||
MESSAGES_URL = MAILBOX_URL + '/mailFolders/<mail_folder>/messages'.freeze
|
||||
SENDMAIL_URL = MAILBOX_URL + '/sendMail'.freeze
|
||||
SCOPE = 'openid offline_access mail.readwrite mail.send mail.readwrite.shared mail.send.shared'.freeze
|
||||
|
||||
def authorize_url(state_secret)
|
||||
params = {
|
||||
client_id: RedmineHelpdesk.settings['helpdesk_oauth_outlook_client_id'],
|
||||
scope: SCOPE,
|
||||
redirect_uri: redirect_uri,
|
||||
response_type: 'code'.freeze,
|
||||
response_mode: 'query'.freeze,
|
||||
state: state_secret,
|
||||
}
|
||||
|
||||
clarified_url(BASE_URL) + '?' + params.to_query
|
||||
end
|
||||
|
||||
def receive_tokens(code)
|
||||
params = {
|
||||
client_id: RedmineHelpdesk.settings['helpdesk_oauth_outlook_client_id'],
|
||||
client_secret: RedmineHelpdesk.settings['helpdesk_oauth_outlook_secret'],
|
||||
scope: SCOPE,
|
||||
redirect_uri: redirect_uri,
|
||||
code: code,
|
||||
grant_type: 'authorization_code'.freeze,
|
||||
}
|
||||
|
||||
code, data = execute(:FPOST, clarified_url(TOKEN_URL), params)
|
||||
logged_error("HelpdeskOauthProvider::Microsoft#receive_tokens error:\nCode: #{code}\nBody: #{data}") unless is_ok?(code)
|
||||
write_tokens(data['access_token'], data['refresh_token']) if is_ok?(code) && data
|
||||
end
|
||||
|
||||
def update_token
|
||||
params = {
|
||||
client_id: RedmineHelpdesk.settings['helpdesk_oauth_outlook_client_id'],
|
||||
client_secret: RedmineHelpdesk.settings['helpdesk_oauth_outlook_secret'],
|
||||
refresh_token: HelpdeskSettings['helpdesk_outlook_refresh_token', project],
|
||||
grant_type: 'refresh_token'.freeze,
|
||||
}
|
||||
|
||||
code, data = execute(:FPOST, clarified_url(TOKEN_URL), params)
|
||||
logged_error("HelpdeskOauthProvider::Microsoft#update_token error:\nCode: #{code}\nBody: #{data}") unless is_ok?(code)
|
||||
if !is_ok?(code) || data.empty?
|
||||
reset_token
|
||||
else
|
||||
write_tokens(data['access_token'], data['refresh_token'])
|
||||
end
|
||||
end
|
||||
|
||||
def folders
|
||||
return [] if HelpdeskSettings[:helpdesk_username, project.id].blank?
|
||||
|
||||
Rails.cache.fetch("outlook_folders_#{project.id}", expires_in: 1.minute) do
|
||||
folders_for(nil, 0).flatten.map {|fld| [fld[:name], fld[:id]]}
|
||||
end
|
||||
end
|
||||
|
||||
def messages(folder = nil)
|
||||
message_ids = new_message_ids(folder)
|
||||
message_ids.map do |mid|
|
||||
[mid, message_raw(mid)]
|
||||
end.to_h
|
||||
end
|
||||
|
||||
def move_message(mid, folder)
|
||||
params = {
|
||||
destinationId: folder
|
||||
}
|
||||
|
||||
move_url = message_full_url + "/#{mid}/move"
|
||||
code, data = execute(:JPOST, move_url, params)
|
||||
is_ok?(code) ? data : ''
|
||||
end
|
||||
|
||||
def read_message(mid)
|
||||
params = {
|
||||
isRead: true,
|
||||
}
|
||||
|
||||
read_url = message_full_url + "/#{mid}"
|
||||
code, data = execute(:PATCH, read_url, params)
|
||||
is_ok?(code) ? data : ''
|
||||
end
|
||||
|
||||
def send_message(encoded_message)
|
||||
params = {
|
||||
raw: true,
|
||||
body: encoded_message
|
||||
}
|
||||
|
||||
sendmail_url = SENDMAIL_URL.gsub('<email>', HelpdeskSettings[:helpdesk_username, project.id])
|
||||
code, data = execute(:TPOST, sendmail_url, params)
|
||||
logged_error("HelpdeskOauthProvider::Microsoft#send_message error:\nCode: #{code}\nBody: #{data}") unless is_ok?(code)
|
||||
raise SendMailError, data unless is_ok?(code)
|
||||
|
||||
data
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def access_token_name
|
||||
:helpdesk_outlook_access_token
|
||||
end
|
||||
|
||||
def refresh_token_name
|
||||
:helpdesk_outlook_refresh_token
|
||||
end
|
||||
|
||||
def clarified_url(url)
|
||||
tenant_id = RedmineHelpdesk.settings['helpdesk_outlook_tenant_id'].presence || 'common'
|
||||
url.gsub('<tenant_id>', tenant_id)
|
||||
end
|
||||
|
||||
def message_full_url(folder = nil)
|
||||
message_url = MESSAGES_URL
|
||||
message_url = folder ? message_url.gsub('<mail_folder>', folder) : message_url.gsub('/mailFolders/<mail_folder>', '')
|
||||
message_url.gsub('<email>', HelpdeskSettings[:helpdesk_username, project.id])
|
||||
end
|
||||
|
||||
def new_message_ids(folder)
|
||||
params = {
|
||||
count: true,
|
||||
filter: 'IsRead eq false',
|
||||
select: 'id',
|
||||
'$orderby': 'createdDateTime',
|
||||
'$top': 100
|
||||
}
|
||||
|
||||
code, data = execute_with_pages(:GET, message_full_url(folder), params)
|
||||
(data || []).map { |mes| mes['id'] }
|
||||
end
|
||||
|
||||
def message_raw(mid)
|
||||
params = {
|
||||
raw: true
|
||||
}
|
||||
raw_url = message_full_url + "/#{mid}/$value"
|
||||
code, data = execute(:GET, raw_url, params)
|
||||
is_ok?(code) ? data : ''
|
||||
end
|
||||
|
||||
def folders_for(fid, level)
|
||||
params = {
|
||||
'$top': 100
|
||||
}
|
||||
|
||||
folders_url = FOLDERS_URL.gsub('<email>', HelpdeskSettings[:helpdesk_username, project.id])
|
||||
folders_url += "/#{fid}/childFolders" if fid.present?
|
||||
|
||||
code, data = execute_with_pages(:GET, folders_url, params)
|
||||
return [] if !is_ok?(code) || !data
|
||||
|
||||
subfolders = data.map do |folder|
|
||||
{
|
||||
name: '--' * level + ' ' + folder['displayName'],
|
||||
id: folder['id'],
|
||||
descendants: folder['childFolderCount']
|
||||
}
|
||||
end
|
||||
subfolders.map { |item| [item, item[:descendants] > 0 ? folders_for(item[:id], level + 1) : []] }
|
||||
end
|
||||
|
||||
def execute_with_pages(type, url, params = {})
|
||||
total_data = []
|
||||
page_url = url
|
||||
page_params = params
|
||||
next_page = true
|
||||
|
||||
while next_page
|
||||
next_page = false
|
||||
code, data = execute(type, page_url, page_params)
|
||||
if is_ok?(code) && data
|
||||
total_data += data['value'] if data['value']
|
||||
if data['@odata.nextLink']
|
||||
next_page = true
|
||||
page_url = data['@odata.nextLink']
|
||||
page_params = {}
|
||||
end
|
||||
end
|
||||
end
|
||||
[code.to_i, total_data]
|
||||
end
|
||||
end
|
44
app/models/helpdesk_reports_busiest_time_query.rb
Normal file
44
app/models/helpdesk_reports_busiest_time_query.rb
Normal file
@ -0,0 +1,44 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
|
||||
def sql_for_report_date_period_field(_field, operator, value)
|
||||
sql_for_field('message_date', operator, value, queried_class.table_name, 'message_date')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def answered_user_ids
|
||||
@answered_user_ids ||= begin
|
||||
scope = Issue.visible.joins(:project)
|
||||
.where("#{Issue.table_name}.assigned_to_id IS NOT NULL")
|
||||
|
||||
scope = scope.distinct
|
||||
scope.pluck(:assigned_to_id)
|
||||
end
|
||||
end
|
||||
end
|
177
app/models/helpdesk_reports_customer_satisfaction_query.rb
Normal file
177
app/models/helpdesk_reports_customer_satisfaction_query.rb
Normal file
@ -0,0 +1,177 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskReportsCustomerSatisfactionQuery < HelpdeskReportsQuery
|
||||
include HelpdeskHelper
|
||||
|
||||
self.queried_class = HelpdeskTicket
|
||||
|
||||
def initialize_available_filters
|
||||
add_available_filter 'report_date_period', :type => :date_past, :name => l(:label_helpdesk_filter_time_interval)
|
||||
add_available_filter 'customer', type: :contact, name: l(:label_helpdesk_contact)
|
||||
add_available_filter 'customer_company', :type => :string, :name => l(:label_helpdesk_contact_company)
|
||||
add_available_filter 'source', :type => :list, :name => l(:label_helpdesk_ticket_source), :values => helpdesk_tickets_source_for_select
|
||||
|
||||
if Redmine::Plugin.registered_plugins.keys.include?(:redmineup_tags)
|
||||
add_available_filter 'tags', :type => :list, :values => tags_values
|
||||
end
|
||||
|
||||
add_available_filter 'assigned_to_id', :type => :list_optional, :values => assigned_to_values
|
||||
add_available_filter 'author_id', :type => :list, :values => author_values
|
||||
|
||||
if project
|
||||
add_available_filter 'category_id', :type => :list_optional, :values => project.issue_categories.map { |c| [c.name, c.id.to_s] }
|
||||
end
|
||||
|
||||
add_available_filter 'priority_id', :type => :list, :values => IssuePriority.all.map { |p| [p.name, p.id.to_s] }
|
||||
add_available_filter 'tracker_id', :type => :list, :values => trackers.map { |t| [t.name, t.id.to_s] }
|
||||
end
|
||||
|
||||
def base_scope
|
||||
Issue.visible.joins(:project, :helpdesk_ticket)
|
||||
end
|
||||
|
||||
def tickets(options = {})
|
||||
base_scope.where(statement).where(options[:conditions])
|
||||
rescue ::ActiveRecord::StatementInvalid => e
|
||||
raise StatementInvalid.new(e.message)
|
||||
end
|
||||
|
||||
def tickets_with_votes_count
|
||||
@tickets_with_votes_count ||= tickets(:conditions => 'vote IS NOT NULL').count
|
||||
end
|
||||
|
||||
def satisfaction_score
|
||||
awesome_percentage - not_good_percentage
|
||||
end
|
||||
|
||||
def total_votes
|
||||
percentage(tickets_with_votes_count, tickets.count)
|
||||
end
|
||||
|
||||
# Create _count and _percentage methods for votes
|
||||
%w(not_good just_ok awesome).each_with_index do |name, i|
|
||||
define_method "#{name}_count" do
|
||||
cached = instance_variable_get("@#{name}_count")
|
||||
return cached if cached
|
||||
instance_variable_set("@#{name}_count", tickets(:conditions => "vote = #{i}").count)
|
||||
end
|
||||
|
||||
define_method "#{name}_percentage" do
|
||||
cached = instance_variable_get("@#{name}_percentage")
|
||||
return cached if cached
|
||||
instance_variable_set("@#{name}_percentage", percentage(send("#{name}_count"), tickets_with_votes_count))
|
||||
end
|
||||
end
|
||||
|
||||
# Query issues table for the following fields, not helpdesk_tickets
|
||||
%w(assigned_to_id
|
||||
author_id
|
||||
category_id
|
||||
priority_id
|
||||
tracker_id).each do |field|
|
||||
define_method "sql_for_#{field}_field" do |f, op, val|
|
||||
sql_for_field(f, op, val, Issue.table_name, f)
|
||||
end
|
||||
end
|
||||
|
||||
def sql_for_tags_field(_field, operator, value)
|
||||
operator = operator_for('tags').eql?('=') ? 'IN' : 'NOT IN'
|
||||
ids = Issue.tagged_with(value).map(&:id).join(',')
|
||||
"(#{Issue.table_name}.id #{operator} (#{ids}))"
|
||||
end
|
||||
|
||||
def sql_for_customer_field(_field, operator, value)
|
||||
where_clause = ''
|
||||
|
||||
if operator.in?(['=', '!'])
|
||||
where_clause = "WHERE #{HelpdeskTicket.table_name}.contact_id IN (#{value.join(',')})"
|
||||
end
|
||||
|
||||
operator = operator.start_with?('!') ? 'NOT IN' : 'IN'
|
||||
ids = <<-SQL
|
||||
SELECT #{'DISTINCT' if where_clause.blank?}
|
||||
#{HelpdeskTicket.table_name}.issue_id
|
||||
FROM #{HelpdeskTicket.table_name}
|
||||
#{where_clause}
|
||||
SQL
|
||||
|
||||
"(#{Issue.table_name}.id #{operator} (#{ids}))"
|
||||
end
|
||||
|
||||
def sql_for_customer_company_field(_field, operator, value)
|
||||
like_statement =
|
||||
case operator
|
||||
when '='
|
||||
"LIKE '#{value.first.to_s.downcase}'"
|
||||
when '!*'
|
||||
"IS NULL OR #{Contact.table_name}.company = ''"
|
||||
when '*'
|
||||
"IS NOT NULL OR #{Contact.table_name}.company <> ''"
|
||||
when '~', '!~'
|
||||
"LIKE '%#{self.class.connection.quote_string(value.first.to_s.downcase)}%'"
|
||||
end
|
||||
|
||||
operator = operator.start_with?('!') ? 'NOT IN' : 'IN'
|
||||
ids = <<-SQL
|
||||
SELECT #{HelpdeskTicket.table_name}.issue_id
|
||||
FROM #{HelpdeskTicket.table_name}
|
||||
WHERE #{HelpdeskTicket.table_name}.contact_id
|
||||
IN (SELECT #{Contact.table_name}.id
|
||||
FROM #{Contact.table_name}
|
||||
WHERE LOWER(#{Contact.table_name}.company) #{like_statement})
|
||||
SQL
|
||||
|
||||
"(#{Issue.table_name}.id #{operator} (#{ids}))"
|
||||
end
|
||||
|
||||
def sql_for_report_date_period_field(field, operator, value)
|
||||
sql_for_field(field, operator, value, queried_class.table_name, 'last_agent_response_at')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def percentage(part, total)
|
||||
return 0 if total.zero?
|
||||
part / total.to_f * 100
|
||||
end
|
||||
|
||||
def author_values
|
||||
return @author_values if @author_values
|
||||
|
||||
@author_values = []
|
||||
@author_values << ["<< #{l(:label_me)} >>", 'me'] if User.current.logged?
|
||||
Principal.where(:id => base_scope.pluck(:author_id).compact.uniq).sort.each { |p| @author_values << [p.name, p.id.to_s] }
|
||||
@author_values
|
||||
end
|
||||
|
||||
def assigned_to_values
|
||||
return @assigned_to_values if @assigned_to_values
|
||||
|
||||
@assigned_to_values = []
|
||||
@assigned_to_values << ["<< #{l(:label_me)} >>", 'me'] if User.current.logged?
|
||||
Principal.where(:id => base_scope.pluck(:assigned_to_id).compact.uniq).sort.each { |p| @assigned_to_values << [p.name, p.id.to_s] }
|
||||
@assigned_to_values
|
||||
end
|
||||
|
||||
def tags_values
|
||||
optional_project = project ? {:project => project} : {}
|
||||
Issue.available_tags(optional_project).map { |t| [t.name, t.name] }
|
||||
end
|
||||
end
|
25
app/models/helpdesk_reports_first_response_query.rb
Normal file
25
app/models/helpdesk_reports_first_response_query.rb
Normal file
@ -0,0 +1,25 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskReportsFirstResponseQuery < HelpdeskReportsQuery
|
||||
|
||||
def sql_for_report_date_period_field(_field, operator, value)
|
||||
sql_for_field('message_date', operator, value, queried_class.table_name, 'message_date')
|
||||
end
|
||||
end
|
129
app/models/helpdesk_reports_query.rb
Normal file
129
app/models/helpdesk_reports_query.rb
Normal file
@ -0,0 +1,129 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskReportsQuery < Query
|
||||
self.queried_class = JournalMessage
|
||||
operators_by_filter_type[:time_interval] = ['t', 'ld', 'w', 'l2w', 'm', 'lm', 'y']
|
||||
|
||||
def initialize(attributes = nil, *args)
|
||||
super attributes
|
||||
|
||||
self.filters ||= {}
|
||||
# Add report_date_period filter unless it's already there.
|
||||
filters.merge!('report_date_period' => {:operator => 'm', :values => ['']}) { |_, old_value| old_value }
|
||||
end
|
||||
|
||||
def initialize_available_filters
|
||||
add_available_filter 'report_date_period', :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]
|
||||
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 answered_user_ids
|
||||
@answered_user_ids ||= begin
|
||||
scope = Issue.visible
|
||||
.joins(:project, :journals, journals: :journal_message)
|
||||
.where("#{Issue.table_name}.assigned_to_id IS NOT NULL")
|
||||
|
||||
scope = scope.distinct
|
||||
scope.pluck(:assigned_to_id)
|
||||
end
|
||||
end
|
||||
|
||||
def collect_answered_users
|
||||
project ? User.where(id: answered_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)
|
||||
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)
|
||||
date_clause(table, field, date_from, date_to, is_custom_filter)
|
||||
end
|
||||
|
||||
def issue_scope
|
||||
issues = Issue.visible.joins(:project).where(statement)
|
||||
|
||||
issues = issues.
|
||||
joins("INNER JOIN #{Journal.table_name} ON #{Journal.table_name}.journalized_id = #{Issue.table_name}.id AND #{Journal.table_name}.journalized_type = 'Issue'").
|
||||
joins("INNER JOIN #{JournalMessage.table_name} ON #{JournalMessage.table_name}.journal_id = #{Journal.table_name}.id")
|
||||
|
||||
issues.distinct
|
||||
end
|
||||
end
|
308
app/models/helpdesk_ticket.rb
Normal file
308
app/models/helpdesk_ticket.rb
Normal file
@ -0,0 +1,308 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskTicket < ApplicationRecord
|
||||
include Redmine::SafeAttributes
|
||||
include RedmineHelpdesk::Concerns::Viewable
|
||||
include RedmineHelpdesk::Concerns::HelpdeskTicketsVisible
|
||||
|
||||
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
|
||||
|
||||
safe_attributes '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
|
||||
attr_accessor :is_send_mail
|
||||
|
||||
belongs_to :customer, class_name: 'Contact', foreign_key: 'contact_id'
|
||||
belongs_to :issue
|
||||
has_one :message_file, class_name: 'Attachment', as: :container, dependent: :destroy
|
||||
has_many :journals, through: :issue
|
||||
|
||||
acts_as_attachable view_permission: :view_issues, delete_permission: :edit_issues
|
||||
|
||||
acts_as_activity_provider type: 'helpdesk_tickets',
|
||||
timestamp: "#{table_name}.ticket_date",
|
||||
author_key: "#{Issue.table_name}.author_id",
|
||||
scope: joins(issue: :project)
|
||||
|
||||
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
|
||||
|
||||
before_save :calculate_metrics
|
||||
validates_presence_of :customer, :ticket_date
|
||||
after_commit :set_ticket_private, on: :create
|
||||
|
||||
def self.joined_model
|
||||
:issue
|
||||
end
|
||||
|
||||
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 to_s
|
||||
l(:label_helpdesk_string, subject: issue.subject, author: customer)
|
||||
end
|
||||
|
||||
def ticket_time
|
||||
self.ticket_date.strftime("%H:%M") 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_name = ticket_date.time_zone.name if ticket_date.respond_to?(:time_zone) && ticket_date.time_zone
|
||||
timezone = timezone_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 default_to_address
|
||||
return last_response_address if last_journal_message && last_journal_message.is_incoming?
|
||||
|
||||
address = from_address.blank? ? '' : from_address.downcase.strip
|
||||
(customer.emails.include?(address) ? address : customer.primary_email).downcase
|
||||
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 = ((issue.contacts ? 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 => issue_id }).
|
||||
order("#{JournalMessage.table_name}.message_date ASC")
|
||||
end
|
||||
|
||||
def reaction_date
|
||||
@reaction_date ||= issue
|
||||
.journals
|
||||
.eager_load(:journal_message)
|
||||
.where("#{JournalMessage.table_name}.journal_id IS NULL
|
||||
OR #{JournalMessage.table_name}.is_incoming IS NULL
|
||||
OR #{JournalMessage.table_name}.is_incoming = ?", false)
|
||||
.order(:created_on)
|
||||
.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? && !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
|
||||
secret = Rails.application.secret_key_base || Rails.application.config.secret_token
|
||||
Digest::MD5.hexdigest("#{issue.id}:#{self.ticket_date.utc}:#{secret}")
|
||||
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 && 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(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
|
||||
|
||||
pids = [project.id] + project.children.select { |ch| ch.module_enabled?(:contacts_helpdesk) }.map(&:id)
|
||||
issues = Issue.includes(:journals, :helpdesk_ticket).where(project_id: pids).
|
||||
where(status_id: RedmineHelpdesk.autoclose_from_status)
|
||||
issues.find_each do |issue|
|
||||
last_event = [issue.created_on, (issue.journals.order(:created_on).last.try(:created_on) || Time.now)].max
|
||||
next if last_event > Time.now - RedmineHelpdesk.autoclose_time_interval
|
||||
|
||||
issue.init_journal(User.anonymous)
|
||||
issue.current_journal.notes = I18n.t('label_helpdesk_autoclosed_ticket')
|
||||
issue.status_id = RedmineHelpdesk.autoclose_to_status
|
||||
issue.save
|
||||
|
||||
HelpdeskMailer.autoclose_message(issue.customer, issue)
|
||||
end
|
||||
end
|
||||
|
||||
def set_ticket_private
|
||||
return unless RedmineHelpdesk.assign_contact_user?
|
||||
|
||||
issue.reload.update(is_private: true) if RedmineHelpdesk.create_private_tickets?
|
||||
end
|
||||
|
||||
def self.send_reply_by_issue(issue, params = {})
|
||||
unless params.empty?
|
||||
issue.helpdesk_ticket.is_send_mail = params[:helpdesk][:is_send_mail]
|
||||
issue.current_journal.build_journal_message
|
||||
if params[:journal_message]
|
||||
message_params = params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash['journal_message'] : params[:journal_message]
|
||||
journal_params = message_params.merge(Hash[message_params.slice('to_address', 'cc_address', 'bcc_address').map { |k, v| [k, v.join(',')] }])
|
||||
issue.current_journal.journal_message.safe_attributes = journal_params
|
||||
issue.current_journal.journal_message.save
|
||||
end
|
||||
|
||||
issue.current_journal.journal_message.to_address ||= issue.customer.primary_email
|
||||
issue.current_journal.is_send_note = true
|
||||
issue.current_journal.notes = HelpdeskMailSupport.apply_text_macro(issue.current_journal.notes, issue.customer, issue, User.current)
|
||||
end
|
||||
end
|
||||
end
|
60
app/models/helpdesk_ticket_query.rb
Normal file
60
app/models/helpdesk_ticket_query.rb
Normal file
@ -0,0 +1,60 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class HelpdeskTicketQuery < Query
|
||||
self.queried_class = HelpdeskTicket
|
||||
self.view_permission = :view_helpdesk_tickets
|
||||
|
||||
def initialize(attributes=nil, *args)
|
||||
super attributes
|
||||
self.filters ||= { }
|
||||
end
|
||||
|
||||
def initialize_available_filters
|
||||
add_available_filter "ticket_date", :type => :date_past
|
||||
end
|
||||
|
||||
def build_from_params(params, defaults={})
|
||||
super
|
||||
@params = params
|
||||
self
|
||||
end
|
||||
|
||||
def tickets(options = {})
|
||||
scope = base_scope
|
||||
scope = scope.joins(issue: { journals: :journal_message }) if @params[:include] && (%w[journals] & @params[:include].split(',')).any?
|
||||
scope = scope.where("contact_id IN (?)", @params[:contact_id].split(',')) if @params[:contact_id]
|
||||
scope = scope.where("issue_id IN (?)", @params[:issue_id].split(',')) if @params[:issue_id]
|
||||
scope = scope.where("source = ?", @params[:source]) if @params[:source]
|
||||
scope = scope.where("from_address = ?", @params[:from_address]) if @params[:from_address]
|
||||
scope = scope.where("message_id = ?", @params[:message_id]) if @params[:message_id]
|
||||
scope.limit(options[:limit])
|
||||
.offset(options[:offset])
|
||||
end
|
||||
|
||||
def tickets_count
|
||||
tickets.count
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def base_scope
|
||||
HelpdeskTicket.where(statement)
|
||||
end
|
||||
end
|
74
app/models/journal_message.rb
Normal file
74
app/models/journal_message.rb
Normal file
@ -0,0 +1,74 @@
|
||||
# This file is a part of Redmine Helpdesk (redmine_helpdesk) plugin,
|
||||
# customer support management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2011-2024 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_helpdesk is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_helpdesk is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_helpdesk. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class JournalMessage < ApplicationRecord
|
||||
include Redmine::SafeAttributes
|
||||
include RedmineHelpdesk::Concerns::Viewable
|
||||
include RedmineHelpdesk::Concerns::HelpdeskTicketsVisible
|
||||
|
||||
belongs_to :contact
|
||||
belongs_to :journal
|
||||
has_one :message_file, class_name: 'Attachment', as: :container, dependent: :destroy
|
||||
|
||||
safe_attributes '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
|
||||
|
||||
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)
|
||||
|
||||
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 self.joined_model
|
||||
:journal
|
||||
end
|
||||
|
||||
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
|
38
app/views/canned_responses/_form.html.erb
Normal file
38
app/views/canned_responses/_form.html.erb
Normal file
@ -0,0 +1,38 @@
|
||||
<%= back_url_hidden_field_tag %>
|
||||
<%= error_messages_for 'canned_response' %>
|
||||
|
||||
<p><%= f.text_field :name, :size => 80, :required => true %></p>
|
||||
<% if User.current.admin? || User.current.allowed_to?(:manage_public_canned_responses, @project) %>
|
||||
<% if @canned_response.user %>
|
||||
<p>
|
||||
<label><%= l(:field_author) %></label>
|
||||
<%= @canned_response.user.name %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<p>
|
||||
<%= f.check_box :is_public,
|
||||
:label => l(:field_is_public),
|
||||
:onchange => (User.current.admin? ? nil : 'if (this.checked) {$("#canned_response_is_for_all").removeAttr("checked"); $("#canned_response_is_for_all").attr("disabled", true);} else {$("#canned_response_is_for_all").removeAttr("disabled");}') %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<p><label for="canned_response_is_for_all"><%=l(:field_is_for_all)%></label>
|
||||
<%= check_box_tag 'canned_response_is_for_all', 1, @canned_response.project.nil?,
|
||||
:disabled => !@canned_response.new_record? && (@canned_response.project.nil? || (@canned_response.is_public? && !User.current.admin?)) %></p>
|
||||
|
||||
<p><%= f.text_area :content, :required => true, :class => 'wiki-edit', :rows => 5 %>
|
||||
<em class="info"><%= l(:text_helpdesk_answer_macros, :macro => HelpdeskSettings::MACRO_LIST.map{|m| link_to m, "#", :class => "mail-macro"}.join(', ')).html_safe %></em>
|
||||
<%= wikitoolbar_for 'canned_response_content' %>
|
||||
</p>
|
||||
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
$(".info a.mail-macro").bind("click", function() {
|
||||
$('#canned_response_content').insertAtCaret($(this).html());
|
||||
return false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<% content_for :header_tags do %>
|
||||
<%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %>
|
||||
<% end %>
|
37
app/views/canned_responses/_index.html.erb
Normal file
37
app/views/canned_responses/_index.html.erb
Normal file
@ -0,0 +1,37 @@
|
||||
<div class="contextual">
|
||||
<%= link_to l(:label_helpdesk_new_canned_response), {:controller => "canned_responses", :action => 'new'}, :class => 'icon icon-add' %>
|
||||
</div>
|
||||
<h3><%= l(:label_helpdesk_canned_response_plural) %></h3>
|
||||
|
||||
<% if @canned_responses.any? %>
|
||||
<table class="list">
|
||||
<thead><tr>
|
||||
<th><%= l(:field_name) %></th>
|
||||
<th><%= l(:field_content) %></th>
|
||||
<th><%= l(:field_is_public) %></th>
|
||||
<th><%= l(:field_author) %></th>
|
||||
<th><%= l(:field_project) %></th>
|
||||
<th></th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<% @canned_responses.each do |canned_response| %>
|
||||
<tr class="<%= cycle 'odd', 'even' %>">
|
||||
<td class="name"><%= canned_response.name %></td>
|
||||
<td class="name"><em class="info"><%= canned_response.content.gsub(/$/, ' ').truncate(250) %></em></td>
|
||||
<td class="tick"><%= checked_image canned_response.is_public? %></td>
|
||||
<td class="project"><%= canned_response.user.try(:name) %></td>
|
||||
<td class="project"><%= canned_response.project ? canned_response.project.name : l(:field_is_for_all) %></td>
|
||||
<td class="buttons">
|
||||
<%= link_to l(:button_edit), edit_canned_response_path(canned_response), :class => 'icon icon-edit' %>
|
||||
<%= delete_link canned_response_path(canned_response, :project_id => canned_response.project) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% if @canned_response_pages %>
|
||||
<p class="pagination"><%= pagination_links_full @canned_response_pages %></p>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<p class="nodata"><%= l(:label_no_data) %></p>
|
||||
<% end %>
|
52
app/views/canned_responses/add.js.erb
Normal file
52
app/views/canned_responses/add.js.erb
Normal file
@ -0,0 +1,52 @@
|
||||
(function($) {
|
||||
|
||||
$.fn.insertAtCaret = function (myValue) {
|
||||
|
||||
return this.each(function() {
|
||||
|
||||
//IE support
|
||||
if (document.selection) {
|
||||
|
||||
this.focus();
|
||||
sel = document.selection.createRange();
|
||||
sel.text = myValue;
|
||||
this.focus();
|
||||
|
||||
} else if (this.selectionStart || this.selectionStart == '0') {
|
||||
|
||||
//MOZILLA / NETSCAPE support
|
||||
var startPos = this.selectionStart;
|
||||
var endPos = this.selectionEnd;
|
||||
var scrollTop = this.scrollTop;
|
||||
this.value = this.value.substring(0, startPos)+ myValue+ this.value.substring(endPos,this.value.length);
|
||||
this.focus();
|
||||
this.selectionStart = startPos + myValue.length;
|
||||
this.selectionEnd = startPos + myValue.length;
|
||||
this.scrollTop = scrollTop;
|
||||
|
||||
} else {
|
||||
|
||||
this.value += myValue;
|
||||
this.focus();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
})(jQuery);
|
||||
|
||||
$('#issue_notes').insertAtCaret("<%= raw escape_javascript(@content) %>")
|
||||
|
||||
$('#helpdesk_canned_response').val("");
|
||||
|
||||
if ($('#cke_issue_notes').length > 0) {
|
||||
CKEDITOR.instances['issue_notes'].insertHtml("<%= raw escape_javascript(@content) %>");
|
||||
}
|
||||
|
||||
<% @attachments.each do |attachment| %>
|
||||
addAttachment(
|
||||
'<%= j attachment.filename %>',
|
||||
'<%= j attachment.token %>',
|
||||
'<%= j attachment.description %>',
|
||||
'<%= j attachment_path(attachment, format: 'js') %>'
|
||||
);
|
||||
<% end %>
|
36
app/views/canned_responses/edit.html.erb
Normal file
36
app/views/canned_responses/edit.html.erb
Normal file
@ -0,0 +1,36 @@
|
||||
<h2><%=l(:label_helpdesk_canned_response)%></h2>
|
||||
|
||||
<%= labelled_form_for :canned_response, @canned_response, url: { action: 'update', project_id: @project }, html: { multipart: true } do |f| %>
|
||||
<div class="box tabular">
|
||||
<fieldset class="tabular"><legend><%= l(:label_change_properties) %></legend>
|
||||
<div id="all_attributes">
|
||||
<%= render partial: 'canned_responses/form', locals: { f: f } %>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset><legend><%= l(:label_attachment_plural) %></legend>
|
||||
<% if @canned_response.attachments.any? && @canned_response.safe_attribute?('deleted_attachment_ids') %>
|
||||
<div class="contextual"><%= link_to l(:label_edit_attachments), '#', onclick: "$('#existing-attachments').toggle(); return false;" %></div>
|
||||
<div id="existing-attachments" style="<%= @canned_response.deleted_attachment_ids.blank? ? 'display:none;' : '' %>">
|
||||
<% @canned_response.attachments.each do |attachment| %>
|
||||
<span class="existing-attachment">
|
||||
<%= text_field_tag '', attachment.filename, class: 'icon icon-attachment filename', disabled: true %>
|
||||
<label>
|
||||
<%= check_box_tag 'canned_response[deleted_attachment_ids][]',
|
||||
attachment.id,
|
||||
@canned_response.deleted_attachment_ids.include?(attachment.id),
|
||||
id: nil, class: 'deleted_attachment' %> <%= l(:button_delete) %>
|
||||
</label>
|
||||
</span>
|
||||
<% end %>
|
||||
<hr />
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div id="new-attachments" style="display:inline-block;">
|
||||
<%= render partial: 'attachments/form', locals: { container: @canned_response } %>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<%= submit_tag l(:button_save) %>
|
||||
<% end %>
|
1
app/views/canned_responses/index.html.erb
Normal file
1
app/views/canned_responses/index.html.erb
Normal file
@ -0,0 +1 @@
|
||||
<%= render :partial => 'index' %>
|
14
app/views/canned_responses/new.html.erb
Normal file
14
app/views/canned_responses/new.html.erb
Normal file
@ -0,0 +1,14 @@
|
||||
<h2><%=l(:label_helpdesk_new_canned_response)%></h2>
|
||||
|
||||
<%= labelled_form_for :canned_response, @canned_response, url: { action: 'create', project_id: @project }, html: { multipart: true } do |f| %>
|
||||
<div class="box tabular">
|
||||
<%= render partial: 'canned_responses/form', locals: { f: f } %>
|
||||
|
||||
<p id="attachments_form">
|
||||
<label><%= l(:label_attachment_plural) %></label>
|
||||
<%= render partial: 'attachments/form', locals: { container: @canned_response } %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%= submit_tag l(:button_create) %>
|
||||
<% end %>
|
1
app/views/common/_helpdesk_select2_data.html.erb
Normal file
1
app/views/common/_helpdesk_select2_data.html.erb
Normal file
@ -0,0 +1 @@
|
||||
<%= transform_to_select2 'assignee', url: auto_complete_assignee_path(project_id: @project), multiple: true %>
|
9
app/views/contacts/_api_helpdesk_tickets.rsb
Normal file
9
app/views/contacts/_api_helpdesk_tickets.rsb
Normal file
@ -0,0 +1,9 @@
|
||||
if authorize_for(:helpdesk_tickets, :show)
|
||||
root_api.array :helpdesk_tickets do
|
||||
contact.all_tickets.each do |issue|
|
||||
root_api.ticket do
|
||||
render_api_helpdesk_ticket(issue.helpdesk_ticket, root_api)
|
||||
end
|
||||
end
|
||||
end if include_in_api_response?('tickets') && contact.tickets.any? && User.current.allowed_to?(:view_helpdesk_tickets, project)
|
||||
end
|
52
app/views/contacts/_helpdesk_tickets.html.erb
Normal file
52
app/views/contacts/_helpdesk_tickets.html.erb
Normal file
@ -0,0 +1,52 @@
|
||||
<% tickets_scope = @contact.all_tickets.visible.order_by_status %>
|
||||
|
||||
<% tickets = tickets_scope %>
|
||||
<div id="helpdesk_tickets" class="contact-issues">
|
||||
<div class="contextual">
|
||||
<%= link_to l(:label_helpdesk_ticket_new), {:controller => 'issues',
|
||||
:action => 'new',
|
||||
:customer_id => @contact,
|
||||
:issue => {
|
||||
:tracker_id => HelpdeskSettings["helpdesk_tracker", @project.id]
|
||||
},
|
||||
:project_id => @project} if User.current.allowed_to?(:add_issues, @project) && User.current.allowed_to?(:send_response, @project) && HelpdeskSettings["helpdesk_tracker", @project.id] %>
|
||||
</div>
|
||||
|
||||
<h3><%= link_to(l(:label_helpdesk_ticket_plural), {:controller => 'issues',
|
||||
:action => 'index',
|
||||
:set_filter => 1,
|
||||
:customer => [@contact.id],
|
||||
:status_id => "*",
|
||||
:c => ["project", "tracker", "status", "subject", "customer", "customer_company", "last_message"],
|
||||
:sort => 'priority:desc,updated_on:desc'}) %> </h3>
|
||||
|
||||
<% if tickets && tickets.any? %>
|
||||
<%= form_tag({}, :data => {:cm_url => issues_context_menu_path}) do %>
|
||||
<table class="list tickets">
|
||||
<tbody>
|
||||
<% for ticket in tickets %>
|
||||
<tr id="ticket-<%= h(ticket.id) %>" class="issue hascontextmenu <%= cycle('odd', 'even') %> <%= ticket.css_classes %>">
|
||||
<td>
|
||||
<%= check_box_tag("ids[]", ticket.id, false, :style => 'display:none;', :id => nil) %>
|
||||
<span class="icon <%= ticket.helpdesk_ticket.ticket_source_icon %>"></span>
|
||||
</td>
|
||||
<td class="subject">
|
||||
<%= link_to "##{ticket.id} - #{truncate(ticket.subject, length: 60, escape: false)} (#{ticket.status})", issue_path(ticket), :class => ticket.css_classes %>
|
||||
</td>
|
||||
<% if @contact.is_company %>
|
||||
<td class="customer"><%= contact_tag(ticket.customer, :type => 'plain') %></td>
|
||||
<% end %>
|
||||
<td class="last_message"><small>
|
||||
<%= ticket.description.to_s.truncate(250) %>
|
||||
</small></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% end %>
|
||||
<%= context_menu %>
|
||||
<% else %>
|
||||
<p class="nodata"><%= l(:label_no_data) %></p>
|
||||
<% end %>
|
||||
|
||||
</div>
|
15
app/views/context_menus/_helpdesk_contacts.html.erb
Normal file
15
app/views/context_menus/_helpdesk_contacts.html.erb
Normal file
@ -0,0 +1,15 @@
|
||||
<% if @contact && User.current.allowed_to?(:view_helpdesk_tickets, @project) && User.current.allowed_to?(:add_issues, @project) && HelpdeskSettings["helpdesk_tracker", @project.id] %>
|
||||
<li><%= context_menu_link l(:label_helpdesk_ticket_new), {:controller => 'issues',
|
||||
:action => 'new',
|
||||
:customer_id => @contact,
|
||||
:issue => {
|
||||
:tracker_id => HelpdeskSettings["helpdesk_tracker", @project.id]
|
||||
},
|
||||
:project_id => @project,
|
||||
:back_url => @back},
|
||||
:method => :get,
|
||||
:class => 'icon icon-support' %>
|
||||
</li>
|
||||
<% end %>
|
||||
|
||||
|
19
app/views/context_menus/_issues_replies.html.erb
Normal file
19
app/views/context_menus/_issues_replies.html.erb
Normal file
@ -0,0 +1,19 @@
|
||||
<% if @project && @project.module_enabled?(:contacts_helpdesk) &&
|
||||
User.current.allowed_to?(:add_issue_notes, @project) &&
|
||||
User.current.allowed_to?(:send_response, @project)
|
||||
%>
|
||||
<li>
|
||||
<%= context_menu_link l(:label_helpdesk_send_reply),
|
||||
{
|
||||
ids: @issue_ids,
|
||||
:controller => 'helpdesk_tickets',
|
||||
:action => 'bulk_edit_reply',
|
||||
:issue => @issue,
|
||||
:project_id => @project,
|
||||
:back_url => @back
|
||||
},
|
||||
:method => :get,
|
||||
:class => 'icon icon-email-to'
|
||||
%>
|
||||
</li>
|
||||
<% end %>
|
80
app/views/helpdesk/_index.html.erb
Normal file
80
app/views/helpdesk/_index.html.erb
Normal file
@ -0,0 +1,80 @@
|
||||
<div class="contextual">
|
||||
<% if !@query.new_record? && @query.editable_by?(User.current) %>
|
||||
<%= link_to l(:button_edit), edit_query_path(@query), :class => 'icon icon-edit' %>
|
||||
<%= delete_link query_path(@query) %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<h2><%= @query.new_record? ? l(:label_issue_plural) : h(@query.name) %></h2>
|
||||
<% html_title(@query.new_record? ? l(:label_issue_plural) : @query.name) %>
|
||||
|
||||
<%= form_tag({ :controller => 'issues', :action => 'index', :project_id => @project },
|
||||
:method => :get, :id => 'query_form') do %>
|
||||
<%= hidden_field_tag 'set_filter', '1' %>
|
||||
<div id="query_form_content" class="hide-when-print">
|
||||
<fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
|
||||
<legend class="icon icon-<%= @query.new_record? ? 'expanded icon-expended' : 'collapsed' %>" onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
|
||||
<div style="<%= @query.new_record? ? "" : "display: none;" %>">
|
||||
<%= render :partial => 'queries/filters', :locals => {:query => @query} %>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset class="collapsible collapsed">
|
||||
<legend class="icon icon-collapsed" onclick="toggleFieldset(this);"><%= l(:label_options) %></legend>
|
||||
<div style="display: none;">
|
||||
<table>
|
||||
<tr style="display: none;">
|
||||
<td><%= l(:field_column_names) %></td>
|
||||
<td><%= render :partial => 'queries/columns', :locals => {:query => @query} %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for='group_by'><%= l(:field_group_by) %></label></td>
|
||||
<td><%= select_tag('group_by',
|
||||
options_for_select(
|
||||
[[]] + @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]},
|
||||
@query.group_by)
|
||||
) %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for='sort'><%= l(:label_sort) %></label></td>
|
||||
<td><%= select_tag('sort',
|
||||
options_for_select(
|
||||
[[]] + @query.available_columns.select(&:sortable?).collect {|c| [c.caption, "#{c.name.to_s}:desc,id:desc"]},
|
||||
params[:sort])
|
||||
) %></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<p class="buttons hide-when-print">
|
||||
|
||||
<%= link_to_function l(:button_apply), 'submit_query_form("query_form")', :class => 'icon icon-checked' %>
|
||||
<%= link_to l(:button_clear), { :set_filter => 1, :project_id => @project }, :class => 'icon icon-reload' %>
|
||||
<% if @query.new_record? && User.current.allowed_to?(:save_queries, @project, :global => true) %>
|
||||
<%= link_to_function l(:button_save),
|
||||
"$('#query_form').attr('action', '#{ @project ? new_project_query_path(@project) : new_query_path }'); submit_query_form('query_form')",
|
||||
:class => 'icon icon-save' %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<%= error_messages_for 'query' %>
|
||||
<% if @query.valid? %>
|
||||
<% if @issues.empty? %>
|
||||
<p class="nodata"><%= l(:label_no_data) %></p>
|
||||
<% else %>
|
||||
<%= render :partial => 'helpdesk/list', :locals => {:issues => @issues, :query => @query} %>
|
||||
<span class="pagination"><%= pagination_links_full @issue_pages, @issue_count %></span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% content_for :sidebar do %>
|
||||
<%= render :partial => 'issues/sidebar' %>
|
||||
<% end %>
|
||||
|
||||
<% content_for :header_tags do %>
|
||||
<%= stylesheet_link_tag :contacts, :plugin => 'redmine_contacts' %>
|
||||
<%= stylesheet_link_tag :helpdesk, :plugin => 'redmine_contacts_helpdesk' %>
|
||||
<% end %>
|
||||
|
||||
<%= context_menu issues_context_menu_path %>
|
66
app/views/helpdesk/_list.html.erb
Normal file
66
app/views/helpdesk/_list.html.erb
Normal file
@ -0,0 +1,66 @@
|
||||
<%= form_tag({}) do -%>
|
||||
<%= hidden_field_tag 'back_url', url_for(params) %>
|
||||
<%= hidden_field_tag 'project_id', @project.id if @project %>
|
||||
|
||||
<table class="contacts tickets index">
|
||||
<tbody>
|
||||
<% previous_group = false %>
|
||||
<% @issues.each do |issue| %>
|
||||
<% if @query.grouped? && (group = @query.group_by_column.value(issue)) != previous_group %>
|
||||
<% reset_cycle %>
|
||||
<tr class="group open">
|
||||
<td colspan="<%= @query.columns.size + 2 %>">
|
||||
<span class="expander icon icon-expanded" onclick="toggleRowGroup(this);"> </span>
|
||||
<%= group.blank? ? 'None' : column_content(@query.group_by_column, issue) %> <span class="count">(<%= @issue_count_by_group[group] %>)</span>
|
||||
<%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}", "toggleAllRowGroups(this)", :class => 'toggle-all') %>
|
||||
</td>
|
||||
</tr>
|
||||
<% previous_group = group %>
|
||||
<% end %>
|
||||
<tr class="hascontextmenu">
|
||||
<td class="checkbox">
|
||||
<%= check_box_tag("ids[]", issue.id, false, :id => nil) %>
|
||||
</td>
|
||||
<% if Setting.gravatar_enabled? %>
|
||||
<td class="avatar">
|
||||
<% if issue.customer %>
|
||||
<%= link_to avatar_to(issue.customer, :size => "32"), {:controller => 'contacts', :action => 'show', :project_id => @project, :id => issue.customer.id}, :id => "avatar" %>
|
||||
<% else %>
|
||||
<%= avatar(issue.author, :size => 3) %>
|
||||
<% end %>
|
||||
</td>
|
||||
<% end %>
|
||||
<td class="name ticket-name">
|
||||
<h1 class="ticket_name"><%= link_to "#{issue.subject}", {:controller => :issues, :action => :show, :id => issue.id} %> <span id="ticket-id">#<%= issue.id %></span></h1>
|
||||
<p class="ticket-description" >
|
||||
<small><%= issue.description.gsub("(\n|\r)", "").strip.truncate(100) unless issue.description.blank? %></small>
|
||||
</p>
|
||||
<p class="contact-info">
|
||||
<%= issue.customer ? "#{content_tag('span', '', :class => "icon icon-email", :title => l(:label_note_type_email))} #{l(:label_helpdesk_from)}: #{link_to_source(issue.customer)}, ".html_safe : "#{l(:label_helpdesk_from)}: #{link_to_user issue.author}, ".html_safe %>
|
||||
<%= l(:label_updated_time, time_tag(issue.updated_on)).html_safe %>
|
||||
</p>
|
||||
</td>
|
||||
<td class="status">
|
||||
<%= content_tag(:span, issue.status.name, :class => "deal-status ticket-status tags status-#{issue.status.id}") %>
|
||||
</td>
|
||||
<td class="info ticket-info">
|
||||
<% if issue.assigned_to %>
|
||||
<div class="ticket-sum"><%= l(:field_assigned_to) %>: <strong><%= link_to_user issue.assigned_to %><%# "#{issue.currency} " if issue.currency %><%# issue_price(issue.amount) %></strong>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="ticket-priority"><%= l(:field_priority) %>: <strong><%= issue.priority.name %><%# "#{issue.currency} " if issue.currency %><%# issue_price(issue.amount) %></strong>
|
||||
</div>
|
||||
<% if issue.due_date %>
|
||||
<div class="ticket-due-date"><%= l(:field_due_date) %>: <strong><%= format_date issue.due_date %><%# "#{issue.currency} " if issue.currency %><%# issue_price(issue.amount) %></strong>
|
||||
</div>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<% end %>
|
||||
|
||||
|
1
app/views/helpdesk/get_mail.js.erb
Normal file
1
app/views/helpdesk/get_mail.js.erb
Normal file
@ -0,0 +1 @@
|
||||
$('#test_connection_messages').html('<%= escape_javascript @message.html_safe %>')
|
7
app/views/helpdesk/show.api.rsb
Normal file
7
app/views/helpdesk/show.api.rsb
Normal file
@ -0,0 +1,7 @@
|
||||
api.message do
|
||||
api.journal_id @journal.id
|
||||
api.content @journal.notes
|
||||
api.to_address @journal_message.to_address
|
||||
api.message_date format_date(@journal_message.message_date)
|
||||
api.customer(:id => @issue.customer.id, :name => @issue.customer.name) unless @issue.customer.nil?
|
||||
end
|
332
app/views/helpdesk/show.html.erb
Normal file
332
app/views/helpdesk/show.html.erb
Normal file
@ -0,0 +1,332 @@
|
||||
<%= render :partial => 'issues/action_menu' %>
|
||||
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
$(document).ready(function() {
|
||||
toggleSendMail($('#is_send_mail').get(0));
|
||||
});
|
||||
|
||||
function toggleSendMail(element) {
|
||||
if (element.checked) {
|
||||
$('#journal_contacts').show();
|
||||
$('#helpdesk_cc').show();
|
||||
<% unless HelpdeskSettings["helpdesk_emails_footer", @project].blank? %>
|
||||
$('#email_footer').insertAfter($('#is_send_mail').parents().eq(1).find('.jstEditor .wiki-edit'));
|
||||
$('#email_footer').show();
|
||||
<% end %>
|
||||
<% unless HelpdeskSettings["helpdesk_emails_header", @project].blank? %>
|
||||
$('#email_header').insertBefore($('#is_send_mail').parents().eq(1).find('.jstElements'));
|
||||
$('#email_header').show();
|
||||
<% end %>
|
||||
$('#issue_status_id').val("<%= HelpdeskSettings["helpdesk_answered_status", @project] %>");
|
||||
|
||||
} else {
|
||||
$('#journal_contacts').hide();
|
||||
$('#helpdesk_cc').hide();
|
||||
<% unless HelpdeskSettings["helpdesk_emails_footer", @project].blank? %>
|
||||
$('#email_footer').hide();;
|
||||
<% end %>
|
||||
<% unless HelpdeskSettings["helpdesk_emails_header", @project].blank? %>
|
||||
$('#email_header').hide();;
|
||||
<% end %>
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<% unless HelpdeskSettings["helpdesk_emails_header", @project].blank? %>
|
||||
<div id="email_header" style="display: none;" class="email-template">
|
||||
<%= textilizable(HelpdeskMailSupport.apply_text_macro(HelpdeskSettings["helpdesk_emails_header", @project], @issue.contact, @issue, User.current)).html_safe %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% unless HelpdeskSettings["helpdesk_emails_footer", @project].blank? %>
|
||||
<div id="email_footer" style="display: none;" class="email-template">
|
||||
<%= textilizable(HelpdeskMailSupport.apply_text_macro(HelpdeskSettings["helpdesk_emails_footer", @project], @issue.contact, @issue, User.current)).html_safe %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
||||
<h2><%= issue_heading(@issue) %></h2>
|
||||
|
||||
<div class="<%= @issue.css_classes %> details">
|
||||
<% if @prev_issue_id || @next_issue_id %>
|
||||
<div class="next-prev-links contextual">
|
||||
<%= link_to_if @prev_issue_id,
|
||||
"\xc2\xab #{l(:label_previous)}",
|
||||
(@prev_issue_id ? issue_path(@prev_issue_id) : nil),
|
||||
:title => "##{@prev_issue_id}" %> |
|
||||
<% if @issue && @issue %>
|
||||
<span class="position"><%= l(:label_item_position, :position => @issue, :count => @issue) %></span> |
|
||||
<% end %>
|
||||
<%= link_to_if @next_issue_id,
|
||||
"#{l(:label_next)} \xc2\xbb",
|
||||
(@next_issue_id ? issue_path(@next_issue_id) : nil),
|
||||
:title => "##{@next_issue_id}" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="subject">
|
||||
<%= render_issue_subject_with_tree(@issue) %>
|
||||
</div>
|
||||
<p class="author icon icon-email">
|
||||
<%= l(:label_added_time_by, :author => @issue.author.instance_of?(AnonymousUser) ? link_to_source(@issue.contacts.first) : link_to_user(@issue.author), :age => time_tag(@issue.created_on)).html_safe %>
|
||||
<% if @issue.created_on != @issue.updated_on %>
|
||||
<%= l(:label_updated_time, time_tag(@issue.updated_on)).html_safe %>.
|
||||
<% end %>
|
||||
</p>
|
||||
|
||||
<% if @issue.description? || @issue.attachments.any? -%>
|
||||
<hr />
|
||||
<% if @issue.description? %>
|
||||
<div class="contextual">
|
||||
<%= link_to l(:button_quote),
|
||||
{:controller => 'journals', :action => 'new', :id => @issue},
|
||||
:remote => true,
|
||||
:method => 'post',
|
||||
:class => 'icon icon-comment' if authorize_for('issues', 'edit') %>
|
||||
</div>
|
||||
|
||||
<div class="wiki">
|
||||
<%= textilizable @issue, :description, :attachments => @issue.attachments %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= link_to_attachments @issue, :thumbnails => true %>
|
||||
<% end -%>
|
||||
|
||||
</div>
|
||||
|
||||
<% if @journals.present? %>
|
||||
<div id="ticket-history">
|
||||
<h3><%=l(:label_history)%></h3>
|
||||
<% reply_links = authorize_for('issues', 'edit') -%>
|
||||
<% for journal in @journals.select{|j| !j.notes.blank? } %>
|
||||
<div id="change-<%= journal.id %>" class="<%= journal.css_classes %> ticket-note">
|
||||
<% if journal.is_incoming? %>
|
||||
<%= link_to avatar_to(journal.contacts.first, :size => "32"), {:controller => 'contacts', :action => 'show', :project_id => @project, :id => journal.contacts.first.id}, :id => "avatar", :class => "ticket-avatar gravatar" unless journal.contacts.blank? %>
|
||||
<% else %>
|
||||
<%= avatar(journal.user, size: 32, :class => "ticket-avatar gravatar") %>
|
||||
<% end %>
|
||||
|
||||
<div id="note-<%= journal.indice %>" class="ticket-note-content">
|
||||
<h4>
|
||||
<%= link_to_in_place_notes_editor(image_tag('edit.png'), "journal-#{journal.id}-notes",
|
||||
{ :controller => 'journals', :action => 'edit', :id => journal, :format => 'js' },
|
||||
:title => l(:button_edit),
|
||||
:class => "journal-link") if reply_links %>
|
||||
<%= link_to(image_tag('comment.png'),
|
||||
{:controller => 'journals', :action => 'new', :id => @issue, :journal_id => journal},
|
||||
:remote => true,
|
||||
:method => 'post',
|
||||
:title => l(:button_quote),
|
||||
:class => "journal-link") %>
|
||||
<% if journal.contacts && journal.contacts.any? && User.current.allowed_to?(:view_helpdesk_tickets, @project) %>
|
||||
|
||||
<span class="icon <%= journal.is_incoming? ? 'icon-email' : 'icon-email-to' %>">
|
||||
|
||||
<% if journal.is_incoming? %>
|
||||
<%= "#{link_to_source journal.contacts.first} (#{journal.journal_messages.first.email})".html_safe unless journal.contacts.blank? %>
|
||||
<% if journal.journal_messages.first.attachments.any? %>
|
||||
<% attachment = journal.journal_messages.first.attachments.first %>
|
||||
<span class="attachment" style="white-space: nowrap;display: inline-block;">
|
||||
<%= link_to_attachment attachment, :text => l(:label_helpdesk_original), :class => 'icon icon-attachment' -%>
|
||||
<%= h(" - #{attachment.description}") unless attachment.description.blank? %>
|
||||
<span class="size">(<%= number_to_human_size attachment.filesize %>)</span>
|
||||
<%= link_to_if_authorized image_tag('magnifier.png', :plugin => "redmine_contacts_helpdesk"),
|
||||
:controller => 'helpdesk', :action => 'show_original',
|
||||
:id => attachment, :project_id => @project %>
|
||||
</span>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= link_to_user journal.user %>
|
||||
<span class="sent-to">
|
||||
<%= l(:label_sent_to) %>
|
||||
<% journal.journal_messages.each do |journal_message| %>
|
||||
<span class="contact" style="white-space: nowrap;display: inline-block;">
|
||||
<%= link_to_source(journal_message.contact) %>
|
||||
(<%= journal_message.email %>)
|
||||
</span>
|
||||
<% end %>
|
||||
</span>
|
||||
<% end %>
|
||||
- <%= format_time(@issue.updated_on).html_safe %>.
|
||||
</span>
|
||||
<%# authoring journal.created_on, journal.user, :label => :label_updated_time_by %>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<% end %>
|
||||
</h4>
|
||||
|
||||
<div class="wiki editable" id="journal-<%= journal.id %>-notes">
|
||||
<%= textilizable(journal, :notes) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% heads_for_wiki_formatter if User.current.allowed_to?(:edit_issue_notes, @issue.project) || User.current.allowed_to?(:edit_own_issue_notes, @issue.project) %>
|
||||
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
||||
<div style="clear: both;"></div>
|
||||
<%= render :partial => 'issues/action_menu' %>
|
||||
|
||||
<div style="clear: both;"></div>
|
||||
<% if authorize_for('issues', 'edit') %>
|
||||
<div id="update" style="display:none;">
|
||||
<h3><%= l(:button_update) %></h3>
|
||||
<%# labelled_form_for @issue, :html => {:id => 'issue-form', :multipart => true} do |f| %>
|
||||
<%= form_tag({:controller => "helpdesk_tickets", :action => "update"}, :id => 'issue-form', :multipart => true, :method => :put) do |f| %>
|
||||
<%= error_messages_for 'issue' %>
|
||||
<%= render :partial => 'issues/conflict' if @conflict %>
|
||||
<div class="box">
|
||||
|
||||
<p>
|
||||
<%= label_tag :is_send_mail, l(:label_is_send_mail), :class => "icon icon-email-to", :style => "" %>
|
||||
<%= check_box_tag 'is_send_mail', 1, HelpdeskSettings["send_note_by_default", @project], :onclick => "toggleSendMail(this);" %>
|
||||
|
||||
<span id="journal_contacts" style="display: none;">
|
||||
<% @issue.contacts.each do |contact| %>
|
||||
<%= contact_tag(contact) %>
|
||||
(<%= contact.emails.first %>)
|
||||
<% end %>
|
||||
<div id="helpdesk_cc" style="display: none;">
|
||||
<p>
|
||||
<%= label_tag :email_cc, l(:label_email_cc) %>
|
||||
<%= text_field_tag :email_cc, '', :size => "80%" %>
|
||||
</p>
|
||||
<p>
|
||||
<%= label_tag :email_bcc, l(:label_email_bcc) %>
|
||||
<%= text_field_tag :email_bcc, '', :size => "80%" %>
|
||||
</p>
|
||||
</div>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
|
||||
<%= wikitoolbar_for 'notes' %>
|
||||
|
||||
<p><%=l(:label_attachment_plural)%><br /><%= render :partial => 'attachments/form', :locals => {:container => @issue} %></p>
|
||||
</div>
|
||||
|
||||
<%# f.hidden_field :lock_version %>
|
||||
<%# hidden_field_tag 'last_journal_id', params[:last_journal_id] || @issue.last_journal_id %>
|
||||
<%= submit_tag l(:button_submit) %>
|
||||
<%= preview_link preview_edit_issue_path(:project_id => @project, :id => @issue), 'issue-form' %>
|
||||
<% end %>
|
||||
|
||||
<div id="preview" class="wiki"></div>
|
||||
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% other_formats_links do |f| %>
|
||||
<%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
|
||||
<%= f.link_to 'PDF' %>
|
||||
<% end %>
|
||||
|
||||
<% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %>
|
||||
|
||||
<% content_for :sidebar do %>
|
||||
<div id="ticket_attributes">
|
||||
<% if authorize_for('issues', 'edit') %>
|
||||
<div class="contextual">
|
||||
<%= link_to l(:button_update), :onclick => '#' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<h3><%= l(:label_helpdesk_ticket_attributes) %></h3>
|
||||
<table class="attributes">
|
||||
<%= issue_fields_rows do |rows|
|
||||
rows.left l(:field_status), h(@issue.status.name), :class => 'status'
|
||||
rows.left l(:field_priority), h(@issue.priority.name), :class => 'priority'
|
||||
|
||||
unless @issue.disabled_core_fields.include?('assigned_to_id')
|
||||
rows.left l(:field_assigned_to), avatar(@issue.assigned_to, :size => "14").to_s.html_safe + (@issue.assigned_to ? link_to_user(@issue.assigned_to) : "-"), :class => 'assigned-to'
|
||||
end
|
||||
unless @issue.disabled_core_fields.include?('category_id') || @issue.category.blank?
|
||||
rows.left l(:field_category), h(@issue.category ? @issue.category.name : "-"), :class => 'category'
|
||||
end
|
||||
unless @issue.disabled_core_fields.include?('fixed_version_id') || @issue.fixed_version.blank?
|
||||
rows.left l(:field_fixed_version), (@issue.fixed_version ? link_to_version(@issue.fixed_version) : "-"), :class => 'fixed-version'
|
||||
end
|
||||
|
||||
unless @issue.disabled_core_fields.include?('start_date') || @issue.start_date.blank?
|
||||
rows.left l(:field_start_date), format_date(@issue.start_date), :class => 'start-date'
|
||||
end
|
||||
unless @issue.disabled_core_fields.include?('due_date') || @issue.due_date.blank?
|
||||
rows.left l(:field_due_date), format_date(@issue.due_date), :class => 'due-date'
|
||||
end
|
||||
unless @issue.disabled_core_fields.include?('done_ratio')
|
||||
rows.left l(:field_done_ratio), progress_bar(@issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%"), :class => 'progress'
|
||||
end
|
||||
unless @issue.disabled_core_fields.include?('estimated_hours')
|
||||
unless @issue.estimated_hours.nil?
|
||||
rows.left l(:field_estimated_hours), l_hours(@issue.estimated_hours), :class => 'estimated-hours'
|
||||
end
|
||||
end
|
||||
if User.current.allowed_to?(:view_time_entries, @project) && @issue.total_spent_hours > 0
|
||||
rows.left l(:label_spent_time), (@issue.total_spent_hours > 0 ? (link_to l_hours(@issue.total_spent_hours), {:controller => 'timelog', :action => 'index', :project_id => @project, :issue_id => @issue}) : "-"), :class => 'spent-time'
|
||||
end
|
||||
end %>
|
||||
<%= render_custom_fields_rows(@issue) %>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="contacts_previous_issues">
|
||||
<style type="text/css">
|
||||
#contacts_previous_issues ul {margin: 0; padding: 0;}
|
||||
#contacts_previous_issues li {list-style-type:none; margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
|
||||
</style>
|
||||
|
||||
|
||||
<% if RedmineHelpdesk.show_contact_card? %>
|
||||
<h3><%= l(:label_helpdesk_contact) %></h3>
|
||||
<% @issue.contacts.each do |contact| %>
|
||||
<span class="small-card">
|
||||
<%= render :partial => 'contacts/contact_card', :object => contact %>
|
||||
</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% if (issues_count = Issue.count(:include => :contacts, :conditions => ["#{Contact.table_name}.id IN (#{@issue.contact_ids.join(', ')})"]) - 1) > 0 %>
|
||||
<h3><%= "#{l(:label_helpdesk_contact_activity)} (#{issues_count})" %> </h3>
|
||||
|
||||
<ul>
|
||||
<% (Issue.visible.find(:all, :include => :contacts, :conditions => ["#{Contact.table_name}.id IN (#{@issue.contact_ids.join(', ')})"], :order => "#{Issue.table_name}.status_id, #{Issue.table_name}.due_date DESC, #{Issue.table_name}.updated_on DESC", :limit => RedmineHelpdesk.last_message_count > 0 ? RedmineHelpdesk.last_message_count : 11) - [@issue]).each do |issue| %>
|
||||
<li>
|
||||
<%= link_to_issue(issue, :truncate => 60, :project => (@project != issue.project)) %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<div class="contextual">
|
||||
<%= link_to l(:label_issue_view_all), {:controller => 'issues',
|
||||
:action => 'index',
|
||||
:set_filter => 1,
|
||||
:f => [:contacts, :status_id],
|
||||
:v => {:contacts => @issue.contact_ids},
|
||||
:op => {:contacts => '=', :status_id => '*'}} %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
</div>
|
||||
|
||||
<% if User.current.allowed_to?(:add_issue_watchers, @project) ||
|
||||
(@issue.watchers.present? && User.current.allowed_to?(:view_issue_watchers, @project)) %>
|
||||
<div id="watchers">
|
||||
<%= render :partial => 'watchers/watchers', :locals => {:watched => @issue} %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% content_for :header_tags do %>
|
||||
<%= stylesheet_link_tag :contacts, :plugin => 'redmine_contacts' %>
|
||||
<%= stylesheet_link_tag :helpdesk, :plugin => 'redmine_contacts_helpdesk' %>
|
||||
<%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %>
|
||||
<% end %>
|
||||
|
||||
<%= context_menu issues_context_menu_path %>
|
9
app/views/helpdesk/update_customer_email.js.erb
Normal file
9
app/views/helpdesk/update_customer_email.js.erb
Normal file
@ -0,0 +1,9 @@
|
||||
$('#customer_to_email').html('<%= escape_javascript(render :partial => "issues/customer_to_email", :locals => {:contact => @contact, :contact_email => @email}) %>')
|
||||
|
||||
$("#helpdesk_to").val("<%= @email %>");
|
||||
$("#helpdesk_to").attr("value", "<%= @email %>");
|
||||
$("#helpdesk_to").trigger('change');
|
||||
$("#helpdesk_cc").val(["<%= @cc_emails.join('","').html_safe %>"]);
|
||||
$("#helpdesk_cc").trigger('change');
|
||||
|
||||
|
1
app/views/helpdesk/update_ticket_data.js.erb
Normal file
1
app/views/helpdesk/update_ticket_data.js.erb
Normal file
@ -0,0 +1 @@
|
||||
$('#customer_profile_and_issues').html('<%= escape_javascript(render :partial => "issues/helpdesk_customer_profile") %>')
|
26
app/views/helpdesk_mail_messenger/email_layout.html.erb
Normal file
26
app/views/helpdesk_mail_messenger/email_layout.html.erb
Normal file
@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
|
||||
"http://www.w3.org/TR/html4/loose.dtd">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<style type="text/css" media="screen">
|
||||
<%= @email_stylesheet %>
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="email_header">
|
||||
<%= textile(@email_header.to_s).html_safe unless @email_header.blank? %>
|
||||
</div>
|
||||
<div class="wrapper" id="email_body">
|
||||
<%= textile(@email_body.to_s).html_safe %>
|
||||
</div>
|
||||
<div id="email_footer">
|
||||
<%= textile(@email_footer.to_s).html_safe unless @email_footer.blank? %>
|
||||
</div>
|
||||
<% if RedmineHelpdesk.add_readmark? && @image_url %>
|
||||
<span style="display: none;">
|
||||
<img src="<%= message_link_url(vid: @image_url) %>">
|
||||
</span>
|
||||
<% end %>
|
||||
</body>
|
||||
</html>
|
3
app/views/helpdesk_mail_messenger/email_layout.text.erb
Normal file
3
app/views/helpdesk_mail_messenger/email_layout.text.erb
Normal file
@ -0,0 +1,3 @@
|
||||
<%= @email_header %>
|
||||
<%= @email_body %>
|
||||
<%= @email_footer %>
|
61
app/views/helpdesk_mail_rules/_form.html.erb
Normal file
61
app/views/helpdesk_mail_rules/_form.html.erb
Normal file
@ -0,0 +1,61 @@
|
||||
<% include_calendar_headers_tags %>
|
||||
<%= back_url_hidden_field_tag %>
|
||||
<%= error_messages_for 'helpdesk_mail_rule' %>
|
||||
|
||||
<fieldset class="mail_rule_block"><legend><%= l(:label_helpdesk_mail_rule_trigger) %></legend>
|
||||
<div id="mail_type">
|
||||
<%= label_tag('mail_type', l(:label_helpdesk_mail_rule_mail_type_on)) %>
|
||||
<%= select_tag 'mail_type', helpdesk_mail_rule_mail_types_for_select(@helpdesk_mail_rule.mail_type) %>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="mail_rule_block"><legend><%= l(:label_helpdesk_mail_rule_condition_plural) %></legend>
|
||||
<table id="filters-table">
|
||||
<% if @helpdesk_mail_rule.conditions.empty? %>
|
||||
<tr class="all-emails-label">
|
||||
<td><%= l(:label_helpdesk_mail_rule_condition_for_all) %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</table>
|
||||
|
||||
<div class="add-filter">
|
||||
<%= label_tag('add_filter_select', l(:label_helpdesk_mail_rule_condition_add)) %>
|
||||
<%= select_tag 'add_filter_select', helpdesk_mail_rule_attrs_for_select(@helpdesk_mail_rule.available_conditions), :name => nil %>
|
||||
</div>
|
||||
|
||||
<%= hidden_field_tag 'f[]', '' %>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="mail_rule_block"><legend><%= l(:label_helpdesk_mail_rule_action_plural) %></legend>
|
||||
<table id="actions-table"></table>
|
||||
|
||||
<div class="add-filter">
|
||||
<%= label_tag('add_action_select', l(:label_helpdesk_mail_rule_action_add)) %>
|
||||
<%= select_tag 'add_action_select', helpdesk_mail_rule_attrs_for_select(@helpdesk_mail_rule.available_actions), :name => nil %>
|
||||
</div>
|
||||
|
||||
<%= hidden_field_tag 'a[]', '' %>
|
||||
</fieldset>
|
||||
|
||||
<%= javascript_tag do %>
|
||||
var operatorLabels = <%= raw_json Query.operators_labels %>;
|
||||
var operatorByType = <%= raw_json Query.operators_by_filter_type %>;
|
||||
var availableFilters = <%= raw_json @helpdesk_mail_rule.available_conditions_as_json %>;
|
||||
var availableActions = <%= raw_json @helpdesk_mail_rule.available_actions_as_json %>;
|
||||
var labelDayPlural = <%= raw_json l(:label_day_plural) %>;
|
||||
|
||||
var filtersUrl = <%= raw_json helpdesk_mail_rules_condition_path %>;
|
||||
var actionsUrl = <%= raw_json helpdesk_mail_rules_action_path %>;
|
||||
|
||||
$(document).ready(function(){
|
||||
initFilters();
|
||||
initActions();
|
||||
<% @helpdesk_mail_rule.conditions.each do |condition, options| %>
|
||||
addFilter("<%= condition %>", <%= raw_json @helpdesk_mail_rule.operator_for_condition(condition) %>, <%= raw_json @helpdesk_mail_rule.condition_values_for(condition) %>);
|
||||
<% end %>
|
||||
<% @helpdesk_mail_rule.actions.each do |action, options| %>
|
||||
addAction("<%= action %>", <%= raw_json @helpdesk_mail_rule.operator_for_action(action) %>, <%= raw_json @helpdesk_mail_rule.action_values_for(action) %>);
|
||||
<% end %>
|
||||
toogleMailRuleButton();
|
||||
});
|
||||
<% end %>
|
@ -0,0 +1,29 @@
|
||||
<table class="list">
|
||||
<thead><tr>
|
||||
<th><%= l(:label_helpdesk_mail_rule_condition_plural) %></th>
|
||||
<th><%= l(:label_helpdesk_mail_rule_action_plural) %></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<% helpdesk_mail_rules.each do |mail_rule| %>
|
||||
<tr class="<%= cycle 'odd', 'even' %>">
|
||||
<td class="conditions">
|
||||
<%= helpdesk_mail_rule_conditions_to_html(mail_rule.conditions) %>
|
||||
</td>
|
||||
<td class="actions">
|
||||
<%= helpdesk_mail_rule_actions_to_html(mail_rule.actions) %>
|
||||
</td>
|
||||
<td><%= stocked_reorder_link(mail_rule, '', {:controller => "helpdesk_mail_rules", :action => 'update', :id => mail_rule}, :put) %></td>
|
||||
<td class="buttons">
|
||||
<%= link_to l(:button_edit), edit_helpdesk_mail_rule_path(mail_rule), :class => 'icon icon-edit' %>
|
||||
<%= delete_link helpdesk_mail_rule_path(mail_rule) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<%= javascript_tag do %>
|
||||
$(function() { $("table.list tbody").positionedItems(); });
|
||||
<% end %>
|
23
app/views/helpdesk_mail_rules/_index.html.erb
Normal file
23
app/views/helpdesk_mail_rules/_index.html.erb
Normal file
@ -0,0 +1,23 @@
|
||||
<div class="contextual">
|
||||
<%= link_to l(:label_helpdesk_mail_rule_new), {:controller => "helpdesk_mail_rules", :action => 'new'}, :class => 'icon icon-add' %>
|
||||
</div>
|
||||
<h3><%= l(:label_helpdesk_mail_rule_plural) %></h3>
|
||||
|
||||
<% if @helpdesk_mail_rules.any? %>
|
||||
<h4><%= l(:label_helpdesk_mail_rule_incoming) %></h4>
|
||||
<%= render partial: 'helpdesk_mail_rules/helpdesk_mail_rules_table', locals: { helpdesk_mail_rules: @helpdesk_mail_rules.incoming.by_position } %>
|
||||
|
||||
<h4><%= l(:label_helpdesk_mail_rule_outgoing) %></h4>
|
||||
<%= render partial: 'helpdesk_mail_rules/helpdesk_mail_rules_table', locals: { helpdesk_mail_rules: @helpdesk_mail_rules.outgoing.by_position } %>
|
||||
|
||||
<!-- TODO: Manual rules -->
|
||||
<!-- <h4><%# l(:label_helpdesk_mail_rule_manually) %></h4> -->
|
||||
<%# render partial: 'helpdesk_mail_rules/helpdesk_mail_rules_table', locals: { helpdesk_mail_rules: @helpdesk_mail_rules.manually } %>
|
||||
|
||||
<% if @helpdesk_mail_rules_pages %>
|
||||
<p class="pagination"><%= pagination_links_full @helpdesk_mail_rules_pages %></p>
|
||||
<% end %>
|
||||
|
||||
<% else %>
|
||||
<p class="nodata"><%= l(:label_no_data) %></p>
|
||||
<% end %>
|
10
app/views/helpdesk_mail_rules/edit.html.erb
Normal file
10
app/views/helpdesk_mail_rules/edit.html.erb
Normal file
@ -0,0 +1,10 @@
|
||||
<h2><%=l(:label_helpdesk_mail_rule)%></h2>
|
||||
|
||||
<%= labelled_form_for :helpdesk_mail_rule, @helpdesk_mail_rule, url: { action: 'update' } do |f| %>
|
||||
<%= render :partial => 'helpdesk_mail_rules/form', :locals => { f: f } %>
|
||||
<%= submit_tag l(:button_save), class: 'mail-rule-button' %>
|
||||
<% end %>
|
||||
|
||||
<% content_for(:header_tags) do %>
|
||||
<% javascript_include_tag :redmine_helpdesk_mail_rule, plugin: 'redmine_contacts_helpdesk' %>
|
||||
<% end %>
|
1
app/views/helpdesk_mail_rules/index.html.erb
Normal file
1
app/views/helpdesk_mail_rules/index.html.erb
Normal file
@ -0,0 +1 @@
|
||||
<%= render :partial => 'index' %>
|
10
app/views/helpdesk_mail_rules/new.html.erb
Normal file
10
app/views/helpdesk_mail_rules/new.html.erb
Normal file
@ -0,0 +1,10 @@
|
||||
<h2><%= l(:label_helpdesk_mail_rule) %></h2>
|
||||
|
||||
<%= labelled_form_for :helpdesk_mail_rule, @helpdesk_mail_rule, url: { action: 'create' } do |f| %>
|
||||
<%= render partial: 'helpdesk_mail_rules/form', locals: { f: f } %>
|
||||
<%= submit_tag l(:button_create), class: 'mail-rule-button' %>
|
||||
<% end %>
|
||||
|
||||
<% content_for(:header_tags) do %>
|
||||
<% javascript_include_tag :redmine_helpdesk_mail_rule, plugin: 'redmine_contacts_helpdesk' %>
|
||||
<% end %>
|
35
app/views/helpdesk_reports/_busiest_time_of_day.html.erb
Normal file
35
app/views/helpdesk_reports/_busiest_time_of_day.html.erb
Normal file
@ -0,0 +1,35 @@
|
||||
<% if @collector.issues_count.zero? %>
|
||||
<p class="nodata"><%= l(:label_no_data) %></p>
|
||||
<% else %>
|
||||
<div class="helpdesk_chart">
|
||||
<table class="chart_table">
|
||||
<%= render :partial => 'table_data' %>
|
||||
|
||||
<tr class="metrics">
|
||||
<td colspan="<%= @collector.columns.count / 2 %>">
|
||||
<p><%= l(:label_helpdesk_busiest_time_of_day_new_tickets) %></p>
|
||||
<div class="num"><%= @collector.new_issues_count %></div>
|
||||
<div class="change" title="<%= process_deviation(@collector.previous_new_issues_count, @collector.new_issues_count, false) %>">
|
||||
<%= progress_in_percents(-@collector.new_issue_count_progress) %>
|
||||
</div>
|
||||
</td>
|
||||
<td colspan="<%= @collector.columns.count / 2 %>">
|
||||
<p><%= l(:label_helpdesk_busiest_time_of_day_new_contacts) %></p>
|
||||
<div class="num"><%= @collector.contacts_count %></div>
|
||||
<div class="change" title="<%= process_deviation(@collector.previous_contacts_count, @collector.contacts_count, false) %>">
|
||||
<%= progress_in_percents(-@collector.total_contacts_count_progress) %>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="metrics">
|
||||
<td colspan="<%= @collector.columns.count / 2 %>">
|
||||
<p><%= l(:label_helpdesk_busiest_time_of_day_total_incoming) %></p>
|
||||
<div class="num"><%= @collector.issues_count %></div>
|
||||
<div class="change" title="<%= process_deviation(@collector.previous_issues_count, @collector.issues_count, false) %>">
|
||||
<%= progress_in_percents(-@collector.issue_count_progress) %>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<% end %>
|
141
app/views/helpdesk_reports/_customer_satisfaction.html.erb
Normal file
141
app/views/helpdesk_reports/_customer_satisfaction.html.erb
Normal file
@ -0,0 +1,141 @@
|
||||
<% content_for :header_tags do %>
|
||||
<%= chartjs_assets %>
|
||||
<% end %>
|
||||
|
||||
<% if @query.tickets_with_votes_count.zero? %>
|
||||
<p class="nodata"><%= l(:label_no_data) %></p>
|
||||
<% else %>
|
||||
<div class="not-too-wide">
|
||||
<div class="helpdesk-charts-layout">
|
||||
<div class="chart">
|
||||
<canvas id="awesome"></canvas>
|
||||
</div>
|
||||
<div class="chart">
|
||||
<canvas id="just-ok"></canvas>
|
||||
</div>
|
||||
<div class="chart">
|
||||
<canvas id="not-good"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="metrics">
|
||||
<tr>
|
||||
<td>
|
||||
<p><%= l(:label_helpdesk_satisfaction_score) %></p>
|
||||
<div id="satisfaction-score" class="num">
|
||||
<%= @query.satisfaction_score.round %><span>%</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<p><%= l(:label_helpdesk_total_votes) %></p>
|
||||
<div id="total-votes" class="num">
|
||||
<%= @query.total_votes.round %><span>%</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var drawChart = function(el, options) {
|
||||
new Chart(
|
||||
el,
|
||||
{
|
||||
type: "doughnut",
|
||||
data: {
|
||||
labels: [
|
||||
" " + options["title"],
|
||||
" <%= l(:label_helpdesk_mark_otherwise) %>"
|
||||
],
|
||||
datasets: [{
|
||||
data: options["data"],
|
||||
backgroundColor: [
|
||||
options["color"],
|
||||
"#ddd"
|
||||
]
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
responsive: true,
|
||||
title: {
|
||||
display: true,
|
||||
fontSize: 20,
|
||||
text: options["title"]
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/* Show percentage in the middle of the chart. */
|
||||
Chart.pluginService.register({
|
||||
beforeDraw: function(chart) {
|
||||
if (chart.id != options["id"]) return;
|
||||
|
||||
var width = chart.chart.width,
|
||||
height = chart.chart.height,
|
||||
ctx = chart.chart.ctx;
|
||||
|
||||
ctx.restore();
|
||||
var fontSize = (height / 114).toFixed(2);
|
||||
ctx.font = fontSize + "em sans-serif";
|
||||
ctx.fillStyle = "black";
|
||||
ctx.textBaseline = "middle";
|
||||
|
||||
var text = Math.round(options["percentage"]) + "%",
|
||||
textWidth = Math.round((width - ctx.measureText(text).width) / 2),
|
||||
textHeight = (height + chart.chart.titleBlock.height) / 2;
|
||||
|
||||
ctx.fillText(text, textWidth, textHeight);
|
||||
ctx.save();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$(document).ready(function() {
|
||||
drawChart(
|
||||
$("#awesome"),
|
||||
{
|
||||
id: 0,
|
||||
title: "<%= l(:label_helpdesk_mark_awesome) %>",
|
||||
color: "#00762a",
|
||||
data: [
|
||||
<%= @query.awesome_count %>,
|
||||
<%= @query.tickets_with_votes_count - @query.awesome_count %>
|
||||
],
|
||||
percentage: <%= @query.awesome_percentage %>
|
||||
}
|
||||
);
|
||||
|
||||
drawChart(
|
||||
$("#just-ok"),
|
||||
{
|
||||
id: 1,
|
||||
title: "<%= l(:label_helpdesk_mark_justok) %>",
|
||||
color: "#ffce56",
|
||||
data: [
|
||||
<%= @query.just_ok_count %>,
|
||||
<%= @query.tickets_with_votes_count - @query.just_ok_count %>
|
||||
],
|
||||
percentage: <%= @query.just_ok_percentage %>
|
||||
}
|
||||
);
|
||||
|
||||
drawChart(
|
||||
$("#not-good"),
|
||||
{
|
||||
id: 2,
|
||||
title: "<%= l(:label_helpdesk_mark_notgood) %>",
|
||||
color: "#ff6384",
|
||||
data: [
|
||||
<%= @query.not_good_count %>,
|
||||
<%= @query.tickets_with_votes_count - @query.not_good_count %>
|
||||
],
|
||||
percentage: <%= @query.not_good_percentage %>
|
||||
}
|
||||
);
|
||||
});
|
||||
</script>
|
||||
<% end %>
|
43
app/views/helpdesk_reports/_first_response_time.html.erb
Normal file
43
app/views/helpdesk_reports/_first_response_time.html.erb
Normal file
@ -0,0 +1,43 @@
|
||||
<% if @collector.issues_count.zero? %>
|
||||
<p class="nodata"><%= l(:label_no_data) %></p>
|
||||
<% else %>
|
||||
<div class="helpdesk_chart">
|
||||
<table class="chart_table">
|
||||
<%= render :partial => 'table_data' %>
|
||||
|
||||
<tr class="metrics">
|
||||
<td colspan="<%= @collector.columns.count / 2 %>">
|
||||
<p><%= l(:label_helpdesk_average_first_response_time) %></p>
|
||||
<div class="num"><%= helpdesk_time_label(@collector.average_response_time) %></div>
|
||||
<div class="change" title="<%= process_deviation(@collector.previous_average_response_time, @collector.average_response_time) %>">
|
||||
<%= mirror_progress_in_percents(@collector.average_response_time_progress) %>
|
||||
</div>
|
||||
</td>
|
||||
<td colspan="<%= @collector.columns.count / 2 %>">
|
||||
<p><%= l(:label_helpdesk_average_time_to_close) %></p>
|
||||
<div class="num"><%= helpdesk_time_label(@collector.average_close_time) %></div>
|
||||
<div class="change" title="<%= process_deviation(@collector.previous_average_close_time, @collector.average_close_time) %>">
|
||||
<%= mirror_progress_in_percents(@collector.average_close_time_progress) %>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="metrics">
|
||||
<td colspan="<%= @collector.columns.count / 2 %>">
|
||||
<p><%= l(:label_helpdesk_average_responses_count) %></p>
|
||||
<div class="num"><%= @collector.average_response_count %></div>
|
||||
<div class="change" title="<%= process_deviation(@collector.previous_average_response_count, @collector.average_response_count, false) %>">
|
||||
<%= progress_in_percents(@collector.average_response_count_progress) %>
|
||||
</div>
|
||||
</td>
|
||||
<td colspan="<%= @collector.columns.count / 2 %>">
|
||||
<p><%= l(:label_helpdesk_total_replies) %></p>
|
||||
<div class="num"><%= @collector.total_response_count %></div>
|
||||
<div class="change" title="<%= process_deviation(@collector.previous_total_response_count, @collector.total_response_count, false) %>">
|
||||
<%= progress_in_percents(-@collector.total_response_count_progress) %>
|
||||
</div>
|
||||
</td>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<% end %>
|
24
app/views/helpdesk_reports/_table_data.html.erb
Normal file
24
app/views/helpdesk_reports/_table_data.html.erb
Normal file
@ -0,0 +1,24 @@
|
||||
<tr class="header">
|
||||
<% @collector.columns.each do |column| %>
|
||||
<td class="column_data">
|
||||
<p class="issues_count"><%= column[:issues_count] %></p>
|
||||
<p><%= [column[:issues_percent], '%'].join %></p>
|
||||
</td>
|
||||
<% end %>
|
||||
</tr>
|
||||
<tr class="main_block">
|
||||
<% @collector.columns.each do |column| %>
|
||||
<td class="column_data">
|
||||
<% if column[:issues_count] > 0 %>
|
||||
<div class="percents" style='height: <%= (column[:issues_count] * @collector.issue_weight).ceil %>px'></div>
|
||||
<% end %>
|
||||
</td>
|
||||
<% end %>
|
||||
</tr>
|
||||
<tr class="footer">
|
||||
<% @collector.columns.each do |column| %>
|
||||
<td class="column_data">
|
||||
<%= l("label_helpdesk_#{@query.name}_interval_#{column[:name]}") %>
|
||||
</td>
|
||||
<% end %>
|
||||
</tr>
|
39
app/views/helpdesk_reports/show.html.erb
Normal file
39
app/views/helpdesk_reports/show.html.erb
Normal file
@ -0,0 +1,39 @@
|
||||
<h2><%= l("label_helpdesk_#{@query.name}") %></h2>
|
||||
<% html_title(l("label_helpdesk_#{@query.name}")) %>
|
||||
|
||||
<%= form_tag({ :controller => 'helpdesk_reports', :action => 'show', :project_id => @project },
|
||||
:method => :get, :id => 'query_form') do %>
|
||||
<div id="query_form_with_buttons" class="hide-when-print">
|
||||
<%= hidden_field_tag 'set_filter', '1' %>
|
||||
<div id="query_form_content">
|
||||
<fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
|
||||
<legend class="icon icon-<%= @query.new_record? ? 'expanded icon-expended' : 'collapsed' %>" onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
|
||||
<div style="<%= @query.new_record? ? "" : "display: none;" %>">
|
||||
<%= render :partial => 'queries/filters', :locals => {:query => @query} %>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<p class="buttons">
|
||||
<%= link_to_function l(:button_apply), '$("#query_form").submit()', :class => 'icon icon-checked' %>
|
||||
<%= link_to l(:button_clear), { :set_filter => 1, :project_id => @project }, :class => 'icon icon-reload' %>
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if @query.valid? %>
|
||||
<%= render :partial => @query.name %>
|
||||
<% else %>
|
||||
<%= error_messages_for @query %>
|
||||
<% end %>
|
||||
|
||||
<% content_for :sidebar do %>
|
||||
<%= render :partial => 'issues/helpdesk_reports' %>
|
||||
<% end %>
|
||||
|
||||
<script>
|
||||
/* Hide report_date_period checkbox so that it couldn't be unchecked. */
|
||||
$(document).ready(function() {
|
||||
$("#cb_report_date_period").hide();
|
||||
$("label[for=cb_report_date_period]").removeAttr("for");
|
||||
});
|
||||
</script>
|
@ -0,0 +1,82 @@
|
||||
<div id="customer_profile">
|
||||
<div class="contextual">
|
||||
<%= link_to l(:button_update),
|
||||
{:controller => 'helpdesk_tickets',
|
||||
:action => 'edit',
|
||||
:issue_id => @issue},
|
||||
:remote => true if User.current.allowed_to?(:edit_helpdesk_tickets, @project) %>
|
||||
|
||||
</div>
|
||||
<h3><%= l(:label_helpdesk_contact) %></h3>
|
||||
<% unless !(@show_form == "true") %>
|
||||
<%= form_for @helpdesk_ticket, :url => {:controller => 'helpdesk_tickets',
|
||||
:action => 'update',
|
||||
:issue_id => @issue},
|
||||
:html => {:id => 'ticket_data_form',
|
||||
:method => :put} do |f| %>
|
||||
|
||||
<% unless @helpdesk_ticket.new_record? %>
|
||||
<div class="contextual">
|
||||
<%= link_to image_tag('link_break.png'),
|
||||
{:controller => 'helpdesk_tickets', :action => 'destroy', :id => @issue},
|
||||
:method => :delete,
|
||||
:data => {:confirm => l(:text_are_you_sure)},
|
||||
:title => l(:label_relation_delete) %>
|
||||
</div>
|
||||
<% end %>
|
||||
<p class="contact_auto_complete"><%= label_tag :helpdesk_ticket_contact_id, l(:label_helpdesk_contact)%><br>
|
||||
<%= helpdesk_select_customer_tag('helpdesk_ticket[from_address]', [["#{@helpdesk_ticket.customer.try(:name)} <#{@helpdesk_ticket.from_address}>", @helpdesk_ticket.from_address || @helpdesk_ticket.customer.try(:id)]], selected: @helpdesk_ticket.from_address || @helpdesk_ticket.customer.try(:id), include_blank: false, multiaddress: true, add_contact: true, style: 'width: 80%;', width: '85%') %>
|
||||
</p>
|
||||
|
||||
<p><%= label_tag :helpdesk_ticket_source, l(:label_helpdesk_ticket_source)%><br>
|
||||
<%= f.select :source, helpdesk_tickets_source_for_select %></p>
|
||||
|
||||
<p><%= f.text_field :ticket_date, :size => 12, :required => true, :value => @helpdesk_ticket.ticket_date.to_date, :label => l(:label_helpdesk_ticket_date) %> <%= f.text_field :ticket_time, :value => @helpdesk_ticket.ticket_time, :size => 5 %><%= calendar_for('helpdesk_ticket_ticket_date') %> </p>
|
||||
|
||||
<p>
|
||||
<%= label_tag :helpdesk_ticket_cc_address, l(:label_helpdesk_cc_address) %><br>
|
||||
<% @cc_address = @helpdesk_ticket.cc_address.try(:split, ',') || [] %>
|
||||
<%= f.select :cc_address, options_for_select(@cc_address.map { |email|[email, email] }, @cc_address), {}, {:multiple => true } %>
|
||||
<br>
|
||||
</p>
|
||||
|
||||
<%= submit_tag l(:button_update) %>
|
||||
<%= link_to l(:button_cancel), {}, :onclick => "$('#ticket_data_form').hide(); return false;" %>
|
||||
|
||||
<% end %>
|
||||
|
||||
<% end %>
|
||||
<span class="small-card">
|
||||
<%= render :partial => 'contacts/contact_card', :object => @issue.customer if @issue.customer %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<% if @issue.customer && (customer_issues = @issue.customer.all_tickets.preload(:status, :tracker, :helpdesk_ticket).visible.order_by_status.to_a).count - 1 > 0 %>
|
||||
<div id="customer_previous_issues">
|
||||
<div class="contextual">
|
||||
<%= link_to l(:label_helpdesk_all) + " (#{customer_issues.count})", {:controller => 'issues',
|
||||
:action => 'index',
|
||||
:set_filter => 1,
|
||||
:f => [:customer, :status_id],
|
||||
:v => {:customer => [@issue.customer.id]},
|
||||
:op => {:customer => '=', :status_id => '*'}} %>
|
||||
</div>
|
||||
|
||||
<h3><%= l(:label_helpdesk_contact_activity) %> </h3>
|
||||
|
||||
<ul>
|
||||
<% (customer_issues.first(5)).each do |issue| %>
|
||||
<li title="<%= "#{issue.tracker} (#{issue.status})" if issue.tracker && issue.status %>" >
|
||||
<span class="icon <%= helpdesk_ticket_source_icon(issue.helpdesk_ticket) %>"></span>
|
||||
<span class="ticket-title <%= 'selected' if @issue == issue %>">
|
||||
<%= link_to_issue(issue, :truncate => 60, :project => (@project != issue.project), :tracker => false) %>
|
||||
</span>
|
||||
<span class="ticket-meta">
|
||||
<%= format_time(issue.created_on) %>
|
||||
<%= "- #{issue.assigned_to.name}" if issue.assigned_to %>
|
||||
</span>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
121
app/views/helpdesk_tickets/bulk_reply.html.erb
Normal file
121
app/views/helpdesk_tickets/bulk_reply.html.erb
Normal file
@ -0,0 +1,121 @@
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
function updateCannedResposeFrom(url, value) {
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'post',
|
||||
data: {id: value}
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<h2><%= l(:label_helpdesk_send_reply) %></h2>
|
||||
|
||||
<div class="box" id="duplicates">
|
||||
<ul id="bulk-selection">
|
||||
<% @issues.each do |issue| %>
|
||||
<% if issue.is_ticket? %>
|
||||
<%= content_tag 'li' do %>
|
||||
<%= link_to_issue(issue) %>
|
||||
- <%= content_tag('span', l(:label_send_to), class: "icon icon-email-to") %>
|
||||
<span id="journal_contacts" class="email-template">
|
||||
<span id="customer_to_email" class="email-template">
|
||||
<%= render partial: "issues/customer_to_email",
|
||||
:locals => {
|
||||
contact: Contact.where(email: issue.helpdesk_ticket.default_to_address).first || issue.customer,
|
||||
contact_email: issue.helpdesk_ticket.default_to_address
|
||||
}
|
||||
%>
|
||||
</span>
|
||||
</span>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= content_tag 'li', link_to_issue(issue) + " - To: empty" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<%= form_for(:email_message,
|
||||
:url => { :controller => 'helpdesk_tickets', :action => 'bulk_update_reply' },
|
||||
:html => { :multipart => true, :id => 'message-form' }) do %>
|
||||
<%= @issues.collect{ |issue| hidden_field_tag('ids[]', issue, {id: issue.id, value: issue.id }) }.join.html_safe %>
|
||||
|
||||
<%= hidden_field_tag 'helpdesk[is_send_mail]', 1 %>
|
||||
<%= hidden_field_tag 'issue[custom_field_values]' %>
|
||||
<%= hidden_field_tag 'journal_message[to_address][]' %>
|
||||
<%= back_url_hidden_field_tag %>
|
||||
|
||||
<div class="box notes_with-send bulk_reply">
|
||||
<div class="helpdesk-send-response-wrapper">
|
||||
<div class="helpdesk-send-response">
|
||||
<div class="helpdeks-ticket-line email-template" style="margin: auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tabular">
|
||||
<p>
|
||||
<label><%= l(:field_mail_from) %></label>
|
||||
<%= text_field_tag('from',
|
||||
"#{HelpdeskMailSupport.apply_from_macro(HelpdeskSettings[:helpdesk_answer_from, @project], User.current)}",
|
||||
style: "width: 98%;",
|
||||
disabled: 'disabled') %>
|
||||
<%= link_to "#{l(:label_crm_contacts_cc)}/#{l(:label_crm_contacts_bcc)}", '#' , :onclick => "$('#cc_fields').show();$(this).hide();" %>
|
||||
</p>
|
||||
|
||||
<div id="cc_fields" style="display:none;">
|
||||
<% contact_emails = [] %>
|
||||
<% contact_emails << @issues.map { |issue| helpdesk_issue_contact_emails(issue) } %>
|
||||
<p class="cc-list-edit reply-list-edit">
|
||||
<%= label_tag l(:label_helpdesk_cc) %>
|
||||
|
||||
<%= helpdesk_select_customer_tag('journal_message[cc_address]',
|
||||
[
|
||||
[helpdesk_issue_customer_text_with_email(@issues.first.helpdesk_ticket),
|
||||
@issues.first.helpdesk_ticket.try(:from_address) || @issues.first.helpdesk_ticket.try(:contact_id)]
|
||||
],
|
||||
include_blank: true,
|
||||
multiaddress: true,
|
||||
multiple: true) %>
|
||||
</p>
|
||||
<div style="clear: both;"></div>
|
||||
<p class="bcc-list-edit reply-list-edit">
|
||||
<%= label_tag l(:label_helpdesk_bcc) %>
|
||||
<%= helpdesk_select_customer_tag('journal_message[bcc_address]',
|
||||
contact_emails,
|
||||
include_blank: true,
|
||||
multiaddress: true,
|
||||
multiple: true) %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<label><%= l(:field_message) %></label>
|
||||
<%= text_area_tag 'notes', '', :cols => 60, :rows => 10, :class => 'wiki-edit', id: 'issue_notes' %>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
<%= wikitoolbar_for 'issue_notes', "preview_bulk_reply?ids[]=#{@contacts.first}" %>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<%= submit_tag l(:button_submit) %>
|
||||
</p>
|
||||
|
||||
<% end %>
|
||||
|
||||
<div id="preview" class="wiki"></div>
|
||||
|
||||
<% content_for :header_tags do %>
|
||||
<%= javascript_include_tag :redmine_helpdesk, :plugin => 'redmine_contacts_helpdesk' %>
|
||||
<%= stylesheet_link_tag :helpdesk, :plugin => 'redmine_contacts_helpdesk' %>
|
||||
|
||||
<% canned_responses = CannedResponse.visible.in_project_or_public(@project).order("#{CannedResponse.table_name}.name") %>
|
||||
<% if canned_responses.any? %>
|
||||
<%= javascript_tag do %>
|
||||
// Responses from only one issue
|
||||
// but need from several
|
||||
addCannedResponseButton(<%= { url: escape_javascript(add_canned_responses_path(project_id: @project, issue_id: @issues.first, format: 'js')),
|
||||
responses: canned_responses.map { |cr| {id: cr.id, name: cr.name} } }.to_json.html_safe %>);
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
3
app/views/helpdesk_tickets/create.api.rsb
Normal file
3
app/views/helpdesk_tickets/create.api.rsb
Normal file
@ -0,0 +1,3 @@
|
||||
api.helpdesk_ticket do
|
||||
render_api_helpdesk_ticket(@helpdesk_ticket, api)
|
||||
end
|
1
app/views/helpdesk_tickets/destroy.html.erb
Normal file
1
app/views/helpdesk_tickets/destroy.html.erb
Normal file
@ -0,0 +1 @@
|
||||
<h2>HelpdeskTicketsController#destroy</h2>
|
30
app/views/helpdesk_tickets/edit.js.erb
Normal file
30
app/views/helpdesk_tickets/edit.js.erb
Normal file
@ -0,0 +1,30 @@
|
||||
$('#customer_profile_and_issues').html('<%= escape_javascript(render :partial => "helpdesk_tickets/customer_profile_and_issues") %>')
|
||||
$('.subject_header .email').html('<%= mail_to @helpdesk_ticket.from_address %>')
|
||||
$('#helpdesk_ticket_cc_address').select2({
|
||||
ajax: {
|
||||
url: '<%= auto_complete_contacts_path(:project_id => (ContactsSetting.cross_project_contacts? ? nil : @project), :is_company => nil) %>',
|
||||
dataType: 'json',
|
||||
delay: 250,
|
||||
data: function (params) {
|
||||
return { q: params.term };
|
||||
},
|
||||
processResults: function (data, params) {
|
||||
return {
|
||||
results: $.grep(data, function(elem) {
|
||||
if (!elem.email) { return false }
|
||||
elem.id = elem.email;
|
||||
return elem;
|
||||
})
|
||||
};
|
||||
},
|
||||
cache: true
|
||||
},
|
||||
tags: true,
|
||||
placeholder: ' ',
|
||||
minimumInputLength: 1,
|
||||
width: '100%',
|
||||
templateResult: ccEmailTagResult,
|
||||
templateSelection: ccEmailTagSelection,
|
||||
}).on('select2:open', function (e) {
|
||||
$('#helpdesk_ticket_cc_address').closest('p').find('.select2-search__field').val(' ').trigger($.Event('input', { which: 13 })).val('');
|
||||
});
|
7
app/views/helpdesk_tickets/index.api.rsb
Normal file
7
app/views/helpdesk_tickets/index.api.rsb
Normal file
@ -0,0 +1,7 @@
|
||||
api.array :helpdesk_tickets, api_meta(total_count: @tickets_count, offset: @offset, limit: @limit) do
|
||||
@tickets.each do |helpdesk_ticket|
|
||||
api.ticket do
|
||||
render_api_helpdesk_ticket(helpdesk_ticket, api)
|
||||
end
|
||||
end
|
||||
end
|
3
app/views/helpdesk_tickets/show.api.rsb
Normal file
3
app/views/helpdesk_tickets/show.api.rsb
Normal file
@ -0,0 +1,3 @@
|
||||
api.helpdesk_ticket do
|
||||
render_api_helpdesk_ticket(@helpdesk_ticket, api)
|
||||
end
|
2
app/views/helpdesk_tickets/show.js.erb
Normal file
2
app/views/helpdesk_tickets/show.js.erb
Normal file
@ -0,0 +1,2 @@
|
||||
$('#customer_profile_and_issues').html('<%= escape_javascript(render :partial => "helpdesk_tickets/customer_profile_and_issues") %>')
|
||||
$('.subject_header .email').html('<%= mail_to @helpdesk_ticket.from_address %>')
|
1
app/views/helpdesk_tickets/update.html.erb
Normal file
1
app/views/helpdesk_tickets/update.html.erb
Normal file
@ -0,0 +1 @@
|
||||
<h2>HelpdeskTicketsController#update</h2>
|
28
app/views/helpdesk_votes/show.html.erb
Normal file
28
app/views/helpdesk_votes/show.html.erb
Normal file
@ -0,0 +1,28 @@
|
||||
<div class="vote_form">
|
||||
<h2><%= l(:label_helpdesk_mark) %></h2>
|
||||
<%= form_tag helpdesk_votes_vote_path(:id => @ticket.id, :hash => @ticket.token) do %>
|
||||
<p>
|
||||
<%= label_tag :vote_2, nil, :class => 'vote-value' do %>
|
||||
<%= radio_button_tag('vote', 2, @ticket.vote == 2 || @ticket.vote == nil ? true : false) %>
|
||||
<span class="icon icon-awesome"><%= t(:label_helpdesk_mark_awesome) %></span>
|
||||
<% end %>
|
||||
|
||||
<%= label_tag :vote_1, nil, :class => 'vote-value' do %>
|
||||
<%= radio_button_tag('vote', 1, @ticket.vote == 1 ? true : false) %>
|
||||
<span class="icon icon-justok"><%= t(:label_helpdesk_mark_justok) %></span>
|
||||
<% end %>
|
||||
|
||||
<%= label_tag :vote_0, nil, :class => 'vote-value' do %>
|
||||
<%= radio_button_tag('vote', 0, @ticket.vote == 0 ? true : false) %>
|
||||
<span class="icon icon-notgood"><%= t(:label_helpdesk_mark_notgood) %></span>
|
||||
<% end %>
|
||||
</p>
|
||||
<%- if RedmineHelpdesk.vote_comment_allow? %>
|
||||
<%= text_area_tag('vote_comment', nil, { :size => '60x12', :placeholder => t(:label_helpdesk_vote_comment_placeholder) }) %>
|
||||
<% end %>
|
||||
|
||||
<div class='submit'>
|
||||
<%= submit_tag(t(:label_helpdesk_submit)) %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
3
app/views/helpdesk_votes/vote.html.erb
Normal file
3
app/views/helpdesk_votes/vote.html.erb
Normal file
@ -0,0 +1,3 @@
|
||||
<div class="vote_form">
|
||||
<h2><%= t(:label_helpdesk_vote_thank) %></h2>
|
||||
</div>
|
14
app/views/helpdesk_widget/animation.css
Normal file
14
app/views/helpdesk_widget/animation.css
Normal file
@ -0,0 +1,14 @@
|
||||
.loading {
|
||||
border: 8px solid #f3f3f3;
|
||||
border-top: 8px solid #7E8387;
|
||||
border-radius: 50%;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
animation: spin 2s linear infinite;
|
||||
-webkit-animation: spin 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
1
app/views/helpdesk_widget/avatar.html.erb
Normal file
1
app/views/helpdesk_widget/avatar.html.erb
Normal file
@ -0,0 +1 @@
|
||||
<%= avatar(@user, :size => 54, :id => 'avatar') %>
|
262
app/views/helpdesk_widget/iframe.js.erb
Normal file
262
app/views/helpdesk_widget/iframe.js.erb
Normal file
@ -0,0 +1,262 @@
|
||||
function getXmlHttp(){
|
||||
var xmlhttp;
|
||||
try {
|
||||
xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
|
||||
} catch (e) {
|
||||
try {
|
||||
xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
|
||||
} catch (E) {
|
||||
xmlhttp = false;
|
||||
}
|
||||
}
|
||||
if (!xmlhttp && typeof XMLHttpRequest!='undefined') {
|
||||
xmlhttp = new XMLHttpRequest();
|
||||
}
|
||||
return xmlhttp;
|
||||
}
|
||||
|
||||
function serialize(form){
|
||||
var boundary = String(Math.random()).slice(2);
|
||||
var boundaryMiddle = '--' + boundary + '\r\n';
|
||||
var boundaryLast = '--' + boundary + '--\r\n'
|
||||
var cont_start = 'Content-Disposition: form-data; name="';
|
||||
var cont_middle = '"\r\n\r\n';
|
||||
var cont_end = '\r\n';
|
||||
var field = '';
|
||||
var body = ['\r\n'];
|
||||
if (typeof form == 'object' && form.nodeName == "FORM") {
|
||||
for (index = form.elements.length - 1; index >= 0; index--) {
|
||||
field = form.elements[index];
|
||||
if (field.type == 'select-multiple') {
|
||||
for (option = form.elements[index].options.length - 1; option >= 0; option--) {
|
||||
if (field.options[option].selected) { body.push(cont_start + field.name + cont_middle + field.options[option].value + cont_end); }
|
||||
}
|
||||
} else {
|
||||
if (field.type != 'submit' && field.type != 'file' && field.type != 'button') {
|
||||
if ((field.type != 'checkbox' && field.type != 'radio') || field.checked) {
|
||||
body.push(cont_start + field.name + cont_middle + field.value + cont_end);
|
||||
}
|
||||
} else {
|
||||
if (field.type == 'file'){
|
||||
if (field.files.length > 0) {
|
||||
body.push(cont_start + field.name + cont_middle + field.attributes['data-value'] + cont_end);
|
||||
body.push(cont_start + field.name + '_name' + cont_middle + field.files[0].name + cont_end);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return [boundary, body.join(boundaryMiddle) + boundaryLast];
|
||||
}
|
||||
|
||||
function translation(field){
|
||||
return RedmineHelpdeskIframe.configuration['translation'] ? RedmineHelpdeskIframe.configuration['translation'][field] : null;
|
||||
}
|
||||
|
||||
function ticketCreated(){
|
||||
success_div = document.createElement('div');
|
||||
success_div.id = 'submit_button';
|
||||
success_div.className = 'success-message';
|
||||
success_div.style.textAlign = 'center';
|
||||
success_div.style.margin = '15%';
|
||||
success_div.style.font = '20px Arial';
|
||||
success_div.innerHTML = translation('createSuccessLabel') || '<%= t(:label_helpdesk_widget_ticket_created) %>';
|
||||
|
||||
success_desc_div = document.createElement('div');
|
||||
success_desc_div.style.textAlign = 'center';
|
||||
success_desc_div.style.margin = '5%';
|
||||
success_desc_div.style.font = '14px Arial';
|
||||
success_desc_div.innerHTML = translation('createSuccessDescription');
|
||||
|
||||
document.getElementById('widget_form').innerHTML = '';
|
||||
document.getElementById('widget_form').appendChild(success_div);
|
||||
document.getElementById('widget_form').appendChild(success_desc_div);
|
||||
}
|
||||
|
||||
function ticketErrors(errors){
|
||||
errors_div = document.createElement('div');
|
||||
errors_div.id = 'ticket-error-details';
|
||||
errors_div.className = 'ticket-error-details';
|
||||
|
||||
error_p = document.createElement('div');
|
||||
error_p.innerHTML = translation('createErrorLabel') || '<%= t(:label_helpdesk_widget_ticket_errors) %>';
|
||||
errors_div.appendChild(error_p);
|
||||
|
||||
errors_link = document.createElement('a');
|
||||
errors_link.id = 'ticket-errors-link';
|
||||
errors_link.href = 'javascript:void(0)';
|
||||
errors_link.style.paddingLeft = '10px';
|
||||
errors_link.addEventListener('click', toggleErrorsList);
|
||||
errors_link.innerHTML = '<%= t(:label_helpdesk_widget_ticket_error_details) %>';
|
||||
error_p.appendChild(errors_link);
|
||||
|
||||
ul = document.createElement('ul');
|
||||
ul.id = 'ticket-errors';
|
||||
ul.className = 'ticket-errors';
|
||||
ul.style.display = 'none';
|
||||
errors_div.appendChild(ul);
|
||||
|
||||
for (var key in errors) {
|
||||
if (key != 'base') {
|
||||
processErrorForField(ul, key, errors[key])
|
||||
} else {
|
||||
errors[key].forEach(function(error_text) {
|
||||
processErrorForCustomField(ul, key, error_text);
|
||||
});
|
||||
}
|
||||
}
|
||||
document.getElementById('flash').appendChild(errors_div);
|
||||
}
|
||||
|
||||
function createErrorLi(target, text){
|
||||
li = document.createElement('li');
|
||||
li.id = 'ticket-error';
|
||||
li.className = 'ticket-error';
|
||||
li.innerHTML = text;
|
||||
target.appendChild(li);
|
||||
}
|
||||
|
||||
function markFieldAsError(element){
|
||||
element.style.border = '';
|
||||
element.classList.add('error_field');
|
||||
element.addEventListener('keyup', checkFieldContent);
|
||||
}
|
||||
|
||||
function markRequireFieldsAsError(){
|
||||
fields = document.querySelectorAll("[data-require='true'] > input, [data-require='true'] > select, [data-require='true'] > textarea, .required-field");
|
||||
required_fields = Array.from(fields);
|
||||
var respose = false;
|
||||
required_fields.forEach(function(field) {
|
||||
if (field.value.length == 0) {
|
||||
markFieldAsError(field);
|
||||
respose = true;
|
||||
}
|
||||
});
|
||||
return respose;
|
||||
}
|
||||
|
||||
function unmarkFieldsAsError(){
|
||||
error_fields = Array.from(document.getElementsByClassName('error_field'));
|
||||
error_fields.forEach(function(field) {
|
||||
field.classList.remove('error_field');
|
||||
field.removeEventListener('keyup', checkFieldContent);
|
||||
});
|
||||
}
|
||||
|
||||
function checkFieldContent(){
|
||||
if (this.value.length > 0) {
|
||||
this.style.border = '1px solid #d9d9d9';
|
||||
} else {
|
||||
this.style.border = '1px solid red';
|
||||
}
|
||||
}
|
||||
|
||||
function processErrorForField(ul, key, error_text) {
|
||||
createErrorLi(ul, error_text);
|
||||
field = document.getElementById(key);
|
||||
if (field != null) { markFieldAsError(field); }
|
||||
}
|
||||
|
||||
function processErrorForCustomField(ul, key, error_text) {
|
||||
checkCustomFieldsOnError(error_text);
|
||||
createErrorLi(ul, error_text);
|
||||
}
|
||||
|
||||
function checkCustomFieldsOnError(error_text){
|
||||
custom_fields = Array.from(document.getElementsByClassName('custom_field'));
|
||||
custom_fields.forEach(function(cfield) {
|
||||
cfield_regex = new RegExp(cfield.attributes['data-error-key'].value);
|
||||
if (cfield_regex.test(error_text)){
|
||||
markFieldAsError(cfield.getElementsByTagName('input')[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toggleErrorsList(event){
|
||||
event.preventDefault();
|
||||
errors_list = document.getElementById('ticket-errors');
|
||||
if (errors_list == null) { return true }
|
||||
if (errors_list && errors_list.style.display == 'block') {
|
||||
errors_list.style.display = 'none';
|
||||
} else {
|
||||
errors_list.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
function processResponse(response){
|
||||
if (response['result']) {
|
||||
ticketCreated();
|
||||
setTimeout(function() {
|
||||
parent.postMessage(JSON.stringify({ reload: true }), "*");
|
||||
}, 3000);
|
||||
} else {
|
||||
ticketErrors(response['errors']);
|
||||
}
|
||||
var formSubmitBtn = document.getElementById('form-submit-btn');
|
||||
if (formSubmitBtn){
|
||||
formSubmitBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function needReloadProjectData(){
|
||||
parent.postMessage(JSON.stringify({ project_reload: true }), "*");
|
||||
}
|
||||
|
||||
function arrangeIframe(){
|
||||
parent.postMessage(JSON.stringify({ arrange: true }), "*");
|
||||
}
|
||||
|
||||
function closeTicketForm(){
|
||||
parent.postMessage(JSON.stringify({ close: true }), "*");
|
||||
}
|
||||
|
||||
function submitTicketForm(event){
|
||||
event.preventDefault();
|
||||
document.getElementById('flash').innerHTML = '';
|
||||
unmarkFieldsAsError();
|
||||
if (markRequireFieldsAsError()){ return false; }
|
||||
|
||||
var base_url = '<%= Setting.protocol %>://<%= Setting.host_name %>'
|
||||
var xmlhttp = getXmlHttp();
|
||||
var serialize_result = serialize(document.getElementById('widget_form'));
|
||||
var boundary = serialize_result[0];
|
||||
var form_params = serialize_result[1];
|
||||
document.getElementById('form-submit-btn').disabled = true;
|
||||
xmlhttp.open('POST', base_url + '/helpdesk_widget/create_ticket.js', true);
|
||||
xmlhttp.setRequestHeader('Content-Type', 'multipart/form-data; boundary=' + boundary);
|
||||
xmlhttp.onreadystatechange = function() {
|
||||
if (xmlhttp.readyState == 4) {
|
||||
if (xmlhttp.status == 200 || xmlhttp.status == 304) {
|
||||
processResponse(JSON.parse(xmlhttp.responseText));
|
||||
} else {
|
||||
processResponse({result: false, errors: []});
|
||||
}
|
||||
}
|
||||
};
|
||||
xmlhttp.send(form_params);
|
||||
}
|
||||
|
||||
function createHandlerListeners() {
|
||||
document.getElementById("widget_form").addEventListener("submit", submitTicketForm);
|
||||
Array.prototype.forEach.call(document.getElementsByClassName("reloadProjectField"), function(elm) {
|
||||
elm.addEventListener("change", needReloadProjectData)
|
||||
})
|
||||
}
|
||||
|
||||
function createMessageListeners() {
|
||||
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
|
||||
var eventer = window[eventMethod];
|
||||
var messageEvent = eventMethod == "attachEvent" ? "onmessage" : "message";
|
||||
|
||||
eventer(messageEvent,function(e) {
|
||||
data = JSON.parse(e.data);
|
||||
if (data['action'] == 'configuration') { RedmineHelpdeskIframe.configuration = JSON.parse(data['configuration']); }
|
||||
}, false);
|
||||
}
|
||||
|
||||
var RedmineHelpdeskIframe = { configuration: {}}
|
||||
createHandlerListeners();
|
||||
createMessageListeners();
|
||||
arrangeIframe();
|
||||
|
6
app/views/helpdesk_widget/load_custom_fields.erb
Normal file
6
app/views/helpdesk_widget/load_custom_fields.erb
Normal file
@ -0,0 +1,6 @@
|
||||
<% @issue.editable_custom_field_values.each do |value| %>
|
||||
<% if @enabled_cf && @enabled_cf.include?(value.custom_field_id.to_s) %>
|
||||
<% required = value.custom_field.is_required? %>
|
||||
<p class="custom_field" data-error-key="<%= value.custom_field.name %>" data-require="<%= required %>"><%= custom_field_tag_with_label :issue, value, :required => required %></p>
|
||||
<% end %>
|
||||
<% end %>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user