Initial Redmine tooling and local plugin forks
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
class CannedResponsesController < ApplicationController
|
||||
unloadable
|
||||
|
||||
before_filter :find_canned_response, :except => [:new, :create, :index]
|
||||
before_filter :find_optional_project, :only => [:new, :create, :add, :destroy]
|
||||
before_filter :find_issue, :only => [:add]
|
||||
before_filter :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 = HelpdeskMailer.apply_macro(@canned_response.content, @issue.customer, @issue, User.current)
|
||||
end
|
||||
|
||||
def new
|
||||
@canned_response = CannedResponse.new
|
||||
@canned_response.user = User.current
|
||||
@canned_response.project = @project
|
||||
@canned_response.is_public = false unless User.current.allowed_to?(:manage_public_canned_responses, @project) || User.current.admin?
|
||||
end
|
||||
|
||||
def create
|
||||
@canned_response = CannedResponse.new(params[:canned_response])
|
||||
@canned_response.user = User.current
|
||||
@canned_response.project = params[:canned_response_is_for_all] ? nil : @project
|
||||
@canned_response.is_public = false unless User.current.allowed_to?(:manage_public_canned_responses, @project) || User.current.admin?
|
||||
|
||||
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.attributes = params[:canned_response]
|
||||
@canned_response.project = nil if params[:canned_response_is_for_all]
|
||||
@canned_response.is_public = false unless User.current.allowed_to?(:manage_public_canned_responses, @project) || User.current.admin?
|
||||
|
||||
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
|
||||
@@ -0,0 +1,244 @@
|
||||
class HelpdeskController < ApplicationController
|
||||
unloadable
|
||||
|
||||
before_filter :find_project, :authorize, :except => [:email_note, :update_customer_email]
|
||||
|
||||
accept_api_auth :email_note, :create_ticket
|
||||
|
||||
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)
|
||||
@issue.status_id = params[:message][:status_id] if params[:message][:status_id].blank? && IssueStatus.find_by_id(params[:message][:status_id])
|
||||
@journal.notes = params[:message][:content]
|
||||
@issue.save!
|
||||
|
||||
contact = @issue.customer
|
||||
|
||||
HelpdeskMailer.with_activated_perform_deliveries do
|
||||
if HelpdeskMailer.issue_response(contact, @journal, params).deliver
|
||||
|
||||
@journal_message = JournalMessage.create(:from_address => "",
|
||||
:to_address => contact.primary_email.downcase,
|
||||
:is_incoming => false,
|
||||
:message_date => Time.now,
|
||||
:contact => contact,
|
||||
:journal => @journal)
|
||||
end
|
||||
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.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
|
||||
@contact ||= Contact.new(params[:ticket][:contact])
|
||||
@contact.projects << @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
|
||||
HelpdeskMailer.auto_answer(@contact, @issue).deliver if HelpdeskSettings["helpdesk_send_notification", @project].to_i > 0
|
||||
|
||||
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.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
|
||||
|
||||
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_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_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_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)
|
||||
|
||||
end
|
||||
|
||||
def set_settings_param(param)
|
||||
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] 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
|
||||
render :text => "Issue #{@issue.id} created", :status => :created, :location => issue_url(@issue)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,44 @@
|
||||
class HelpdeskMailerController < ActionController::Base
|
||||
unloadable
|
||||
before_filter :check_credential
|
||||
|
||||
# Submits an incoming email to ContactsMailer
|
||||
def index
|
||||
options = params.dup
|
||||
if options[:issue].present?
|
||||
project = Project.find_by_identifier(options[:issue][:project])
|
||||
options = HelpdeskMailer.get_issue_options(options, project.id) if project
|
||||
end
|
||||
email = options.delete(:email)
|
||||
if HelpdeskMailer.receive(email, options)
|
||||
render :nothing => true, :status => :created
|
||||
else
|
||||
render :nothing => true, :status => :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def get_mail
|
||||
msg_count = 0
|
||||
errors = []
|
||||
Project.active.has_module(:contacts_helpdesk).each do |project|
|
||||
begin
|
||||
msg_count += HelpdeskMailer.check_project(project.id)
|
||||
rescue Exception => e
|
||||
errors << e.message
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
render :status => :ok, :text => {: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 :text => 'Access denied. Incoming emails WS is disabled or key is invalid.', :status => 403
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,35 @@
|
||||
class HelpdeskReportsController < ApplicationController
|
||||
unloadable
|
||||
menu_item :issues
|
||||
|
||||
helper :helpdesk
|
||||
helper :queries
|
||||
include QueriesHelper
|
||||
|
||||
before_filter :find_optional_project, :authorize_global
|
||||
|
||||
def show
|
||||
retrieve_reports_query
|
||||
@collector = HelpdeskDataCollectorManager.new(@report).collect_data(@query)
|
||||
return render_404 unless @collector
|
||||
respond_to do |format|
|
||||
format.html
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def retrieve_reports_query
|
||||
@report = params[:report] || 'first_response_time'
|
||||
report_query_class = @report == 'first_response_time' ? HelpdeskReportsFirstResponseQuery : HelpdeskReportsBusiestTimeQuery
|
||||
if params[:set_filter] || session[:helpdesk_reports_query].nil? || session[:helpdesk_reports_query][:project_id] != (@project ? @project.id : nil)
|
||||
@query = report_query_class.new(:name => '_', :project => @project)
|
||||
params.merge!('f' => ['message_date'], 'op' => { 'message_date' => 'm' }) if params['f'].nil? || params['f'].all?(&:blank?)
|
||||
@query.build_from_params(params)
|
||||
@query[:filters] = { 'message_date' => { :operator => 'm', :values => [''] } } unless @query[:filters]
|
||||
session[:helpdesk_reports_query] = { :project_id => @query.project_id, :filters => @query.filters || {} }
|
||||
else
|
||||
@query = report_query_class.new(:name => '_', :project => @project, :filters => session[:helpdesk_reports_query][:filters] || {})
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,201 @@
|
||||
class HelpdeskSearchController < ApplicationController
|
||||
unloadable
|
||||
|
||||
accept_api_auth :ticket_by_issue, :issues_by_contact, :messages_by_issue, :contact_timeline
|
||||
|
||||
before_filter :require_login
|
||||
|
||||
def usage
|
||||
# Human/browser probes often stop at /helpdesk_search/issues. Return a
|
||||
# machine-readable usage response instead of letting Rails raise a route
|
||||
# exception that looks like an application failure in production.log.
|
||||
render :json => {
|
||||
:helpdesk_search => {
|
||||
:ticket_by_issue => '/helpdesk_search/issues/:issue_id/ticket',
|
||||
:ticket_by_issue_alias => '/helpdesk_search/issues/:issue_id',
|
||||
:issues_by_contact => '/helpdesk_search/contacts/:contact_id/issues',
|
||||
:messages_by_issue => '/helpdesk_search/issues/:issue_id/messages',
|
||||
:contact_timeline => '/helpdesk_search/contacts/:contact_id/timeline'
|
||||
}
|
||||
}, :status => :bad_request
|
||||
end
|
||||
|
||||
def ticket_by_issue
|
||||
issue = Issue.find(params[:issue_id])
|
||||
return unless authorize_helpdesk_project!(issue.project)
|
||||
|
||||
ticket = HelpdeskTicket.where(:issue_id => issue.id).first
|
||||
unless ticket
|
||||
render_404
|
||||
return
|
||||
end
|
||||
|
||||
render :json => {:helpdesk_ticket => serialize_ticket(ticket)}
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
def issues_by_contact
|
||||
contact = Contact.find(params[:contact_id])
|
||||
tickets = HelpdeskTicket.
|
||||
includes(:issue => [:project, :status, :tracker, :assigned_to]).
|
||||
where(:contact_id => contact.id).
|
||||
order("#{HelpdeskTicket.table_name}.ticket_date DESC").
|
||||
limit(api_limit)
|
||||
|
||||
render :json => {
|
||||
:contact_id => contact.id,
|
||||
:issues => tickets.map { |ticket| serialize_ticket_issue(ticket) }.compact
|
||||
}
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
def messages_by_issue
|
||||
issue = Issue.find(params[:issue_id])
|
||||
return unless authorize_helpdesk_project!(issue.project)
|
||||
|
||||
messages = JournalMessage.
|
||||
includes(:contact, :journal).
|
||||
joins(:journal).
|
||||
where(:journals => {:journalized_type => 'Issue', :journalized_id => issue.id}).
|
||||
order("#{JournalMessage.table_name}.message_date ASC").
|
||||
limit(api_limit)
|
||||
|
||||
render :json => {
|
||||
:issue_id => issue.id,
|
||||
:journal_messages => messages.map { |message| serialize_journal_message(message) }
|
||||
}
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
def contact_timeline
|
||||
contact = Contact.find(params[:contact_id])
|
||||
tickets = HelpdeskTicket.
|
||||
includes(:issue => [:project, :status, :tracker]).
|
||||
where(:contact_id => contact.id).
|
||||
order("#{HelpdeskTicket.table_name}.ticket_date DESC").
|
||||
limit(api_limit)
|
||||
|
||||
messages = JournalMessage.
|
||||
includes(:contact, :journal).
|
||||
where(:contact_id => contact.id).
|
||||
order("#{JournalMessage.table_name}.message_date DESC").
|
||||
limit(api_limit)
|
||||
|
||||
events = []
|
||||
tickets.each do |ticket|
|
||||
next unless visible_issue?(ticket.issue)
|
||||
events << serialize_ticket(ticket).merge(:type => 'helpdesk_ticket', :date => iso8601(ticket.ticket_date))
|
||||
end
|
||||
|
||||
messages.each do |message|
|
||||
issue = message_issue(message)
|
||||
next unless visible_issue?(issue)
|
||||
events << serialize_journal_message(message).merge(:type => 'journal_message', :date => iso8601(message.message_date))
|
||||
end
|
||||
|
||||
events.sort_by! { |event| event[:date].to_s }
|
||||
events.reverse!
|
||||
|
||||
render :json => {
|
||||
:contact_id => contact.id,
|
||||
:timeline => events.first(api_limit)
|
||||
}
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authorize_helpdesk_project!(project)
|
||||
# Search endpoints expose customer/email context, so they reuse the native
|
||||
# per-project helpdesk visibility permission instead of adding a new role.
|
||||
return true if project && User.current.allowed_to?(:view_helpdesk_tickets, project)
|
||||
deny_access
|
||||
false
|
||||
end
|
||||
|
||||
def visible_issue?(issue)
|
||||
issue && issue.project && User.current.allowed_to?(:view_helpdesk_tickets, issue.project)
|
||||
end
|
||||
|
||||
def api_limit
|
||||
requested_limit = params[:limit].present? ? params[:limit].to_i : 100
|
||||
[[requested_limit, 1].max, 200].min
|
||||
end
|
||||
|
||||
def serialize_ticket_issue(ticket)
|
||||
issue = ticket.issue
|
||||
return nil unless visible_issue?(issue)
|
||||
|
||||
serialize_ticket(ticket).merge(
|
||||
:issue => {
|
||||
:id => issue.id,
|
||||
:project_id => issue.project_id,
|
||||
:tracker_id => issue.tracker_id,
|
||||
:tracker_name => issue.tracker.try(:name),
|
||||
:status_id => issue.status_id,
|
||||
:status_name => issue.status.try(:name),
|
||||
:assigned_to_id => issue.assigned_to_id,
|
||||
:assigned_to_name => issue.assigned_to.try(:name),
|
||||
:subject => issue.subject,
|
||||
:created_on => iso8601(issue.created_on),
|
||||
:updated_on => iso8601(issue.updated_on)
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def serialize_ticket(ticket)
|
||||
{
|
||||
:id => ticket.id,
|
||||
:issue_id => ticket.issue_id,
|
||||
:contact_id => ticket.contact_id,
|
||||
:message_id => ticket.message_id,
|
||||
:source => ticket.source,
|
||||
:is_incoming => ticket.is_incoming?,
|
||||
:from_address => ticket.from_address,
|
||||
:to_address => ticket.to_address,
|
||||
:cc_address => ticket.cc_address,
|
||||
:ticket_date => iso8601(ticket.ticket_date)
|
||||
}
|
||||
end
|
||||
|
||||
def serialize_journal_message(message)
|
||||
journal = message.journal
|
||||
issue = message_issue(message)
|
||||
|
||||
# Keep this API metadata-only. The external indexer can fetch journal.notes
|
||||
# with its own read policy; this endpoint should not leak message bodies,
|
||||
# private notes, attachments, or BCC addresses by accident.
|
||||
{
|
||||
:id => message.id,
|
||||
:journal_id => message.journal_id,
|
||||
:issue_id => issue.try(:id),
|
||||
:project_id => issue.try(:project_id),
|
||||
:contact_id => message.contact_id,
|
||||
:message_id => message.message_id,
|
||||
:source => message.source,
|
||||
:is_incoming => message.is_incoming?,
|
||||
:from_address => message.from_address,
|
||||
:to_address => message.to_address,
|
||||
:cc_address => message.cc_address,
|
||||
:has_bcc_address => message.bcc_address.present?,
|
||||
:message_date => iso8601(message.message_date),
|
||||
:journal_user_id => journal.try(:user_id),
|
||||
:journal_private_notes => journal.try(:private_notes?),
|
||||
:journal_has_notes => journal.try(:notes).present?
|
||||
}
|
||||
end
|
||||
|
||||
def message_issue(message)
|
||||
journal = message.journal
|
||||
return nil unless journal
|
||||
journal.try(:issue) || journal.try(:journalized)
|
||||
end
|
||||
|
||||
def iso8601(value)
|
||||
value.try(:utc).try(:iso8601)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,67 @@
|
||||
class HelpdeskTicketsController < ApplicationController
|
||||
unloadable
|
||||
|
||||
before_filter :find_issue, :except => :destroy
|
||||
before_filter :find_helpdesk_ticket, :only => :destroy
|
||||
before_filter :authorize
|
||||
|
||||
helper :helpdesk
|
||||
|
||||
def edit
|
||||
@show_form = "true"
|
||||
respond_to do |format|
|
||||
format.js
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@helpdesk_ticket.attributes = params[:helpdesk_ticket]
|
||||
@helpdesk_ticket.cc_address = params[:helpdesk_ticket][:cc_address].reject(&:empty?).join(',') if params[:helpdesk_ticket][:cc_address]
|
||||
@helpdesk_ticket.issue = @issue
|
||||
@helpdesk_ticket.from_address = @helpdesk_ticket.customer.primary_email if @helpdesk_ticket.customer
|
||||
|
||||
if @helpdesk_ticket.save
|
||||
flash[:notice] = l(:notice_successful_update)
|
||||
respond_to do |format|
|
||||
format.html { redirect_back_or_default({:controller => 'issues', :action => 'show', :id => @issue}) }
|
||||
format.api { render_api_ok }
|
||||
end
|
||||
else
|
||||
flash[:error] = @helpdesk_ticket.errors.full_messages.flatten.join("\n")
|
||||
respond_to do |format|
|
||||
format.html { redirect_back_or_default({:controller => 'issues', :action => 'show', :id => @issue}) }
|
||||
format.api { render_validation_errors(@helpdesk_ticket) }
|
||||
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({:controller => 'issues', :action => 'show', :id => @issue}) }
|
||||
format.api { render_api_ok }
|
||||
end
|
||||
else
|
||||
flash[:error] = l(:notice_unsuccessful_save)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def find_helpdesk_ticket
|
||||
@helpdesk_ticket = HelpdeskTicket.find(params[:id])
|
||||
@issue = @helpdesk_ticket.issue
|
||||
@project = @issue.project if @issue
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
def find_issue
|
||||
@issue = Issue.find(params[:issue_id])
|
||||
@project = @issue.project
|
||||
@helpdesk_ticket = @issue.helpdesk_ticket || HelpdeskTicket.new(:ticket_date => Time.now, :issue => @issue)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
class HelpdeskVotesController < ApplicationController
|
||||
unloadable
|
||||
layout 'public_tickets'
|
||||
skip_before_filter :check_if_login_required
|
||||
before_filter :find_ticket, :authorize_ticket
|
||||
before_filter :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
|
||||
@@ -0,0 +1,206 @@
|
||||
class HelpdeskWidgetController < ApplicationController
|
||||
unloadable
|
||||
layout false
|
||||
helper :custom_fields
|
||||
protect_from_forgery :except => [:widget, :load_form, :load_custom_fields, :avatar, :create_ticket, :iframe]
|
||||
skip_before_filter :check_if_login_required, :only => [:widget, :load_form, :load_custom_fields, :avatar, :create_ticket, :iframe]
|
||||
|
||||
before_filter :prepare_data, :only => [:load_custom_fields, :create_ticket]
|
||||
after_filter :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
|
||||
open("http://www.gravatar.com/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 valid_email? && @issue.save
|
||||
save_attachment(@issue)
|
||||
HelpdeskMailer.auto_answer(@issue.helpdesk_ticket.customer, @issue).deliver if HelpdeskSettings["helpdesk_send_notification", @project].to_i > 0
|
||||
{ :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[: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[:subject] = errors
|
||||
end
|
||||
|
||||
# Description
|
||||
if params[:issue][:description].empty?
|
||||
@issue.errors[: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
|
||||
author = redmine_user.present? && redmine_user.allowed_to?(:edit_helpdesk_tickets, @project) ? redmine_user : User.anonymous
|
||||
issue = @project.issues.build(:tracker => @tracker, :author => author)
|
||||
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[:email] = 'Email cannot be empty'
|
||||
return false
|
||||
elsif params[:email].match(/\A([\w\.\+\-]+)@([\w\-]+\.)+([\w]{2,})\z/i).nil?
|
||||
@issue.errors[: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
|
||||
end
|
||||
@@ -0,0 +1,89 @@
|
||||
class MailFetcherController < ApplicationController
|
||||
unloadable
|
||||
require 'timeout'
|
||||
|
||||
before_filter :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 { 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.api', :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.api', :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
|
||||
@@ -0,0 +1,81 @@
|
||||
class PublicTicketsController < ApplicationController
|
||||
unloadable
|
||||
layout 'public_tickets'
|
||||
|
||||
skip_before_filter :check_if_login_required
|
||||
before_filter :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.journals.new(params[:journal])
|
||||
@issue.status_id = HelpdeskSettings["helpdesk_reopen_status", @issue.project_id] unless HelpdeskSettings["helpdesk_reopen_status", @issue.project_id].blank?
|
||||
@journal.user = User.current
|
||||
@journal.journal_message = JournalMessage.new(:from_address => @ticket.customer_email,
|
||||
:contact => @ticket.customer,
|
||||
:journal => @journal,
|
||||
:is_incoming => true,
|
||||
:message_date => Time.now)
|
||||
if @issue.save
|
||||
flash[:notice] = l(:notice_successful_create)
|
||||
@journal.save
|
||||
end
|
||||
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
|
||||
return 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
|
||||
Reference in New Issue
Block a user