Initial Redmine tooling and local plugin forks

This commit is contained in:
Jason Thistlethwaite
2026-04-24 22:01:18 +00:00
commit 9f682af0eb
683 changed files with 56878 additions and 0 deletions
@@ -0,0 +1,36 @@
# This file is a part of Redmine CRM (redmine_contacts) plugin,
# customer relationship management plugin for Redmine
#
# Copyright (C) 2010-2018 RedmineUP
# http://www.redmineup.com/
#
# redmine_contacts 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_contacts 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_contacts. If not, see <http://www.gnu.org/licenses/>.
class ContactImportsController < ImporterBaseController
menu_item :contacts
helper :contacts
def klass
ContactImport
end
def importer_klass
ContactKernelImport
end
def instance_index
project_contacts_path(:project_id => @project.id)
end
end
@@ -0,0 +1,490 @@
# This file is a part of Redmine CRM (redmine_contacts) plugin,
# customer relationship management plugin for Redmine
#
# Copyright (C) 2010-2018 RedmineUP
# http://www.redmineup.com/
#
# redmine_contacts 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_contacts 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_contacts. If not, see <http://www.gnu.org/licenses/>.
class ContactsController < ApplicationController
unloadable
Mime::Type.register 'text/x-vcard', :vcf
Mime::Type.register 'application/vnd.ms-excel', :xls
default_search_scope :contacts
before_action :find_contact, :only => [:show, :edit, :update, :destroy, :load_tab]
before_action :find_project, :only => [:new, :create]
before_action :authorize, :only => [:create, :new]
before_action :authorize_contacts, :only => [:edit, :update, :destroy]
before_action :find_optional_project, :only => [:index, :contacts_notes, :edit_mails, :send_mails, :bulk_update]
accept_rss_auth :index, :show
accept_api_auth :index, :show, :create, :update, :destroy
helper :attachments
helper :contacts
include ContactsHelper
helper :watchers
helper :deals
helper :notes
helper :custom_fields
include CustomFieldsHelper
helper :context_menus
include WatchersHelper
helper :sort
include SortHelper
helper :queries
include QueriesHelper
helper :crm_queries
include CrmQueriesHelper
include ApplicationHelper
include NotesHelper
def index
retrieve_crm_query('contact')
sort_init(@query.sort_criteria.empty? ? [['last_name', 'asc'], ['first_name', 'asc']] : @query.sort_criteria)
sort_update(@query.sortable_columns)
@query.sort_criteria = sort_criteria.to_a
if @query.valid?
case params[:format]
when 'csv', 'xls', 'vcf'
@limit = Setting.issues_export_limit.to_i
if Redmine::VERSION::STRING < '3.2' && params[:columns] == 'all'
@query.column_names = @query.available_columns.map(&:name)
end
when 'atom'
@limit = Setting.feeds_limit.to_i
when 'xml', 'json'
@offset, @limit = api_offset_and_limit
else
@limit = per_page_option
end
@contacts_count = @query.object_count
@contacts_pages = Paginator.new(@contacts_count, @limit, params['page'])
@offset ||= @contacts_pages.offset
@contact_count_by_group = @query.object_count_by_group
@contacts = @query.results_scope(
:include => [:avatar],
:search => params[:search],
:order => sort_clause,
:limit => @limit,
:offset => @offset
)
@filter_tags = @query.filters['tags'] && @query.filters['tags'][:values]
respond_to do |format|
format.html {
unless request.xhr?
last_notes
@tags = Contact.available_tags(:project => @project)
else
render :partial => contacts_list_style, :layout => false
end
}
format.api
format.atom { render_feed(@contacts, :title => "#{@project || Setting.app_title}: #{l(:label_contact_plural)}") }
format.csv {
send_data(query_to_csv(@contacts, @query, params[:csv] || {}),
:type => 'text/csv; header=present',
:filename => 'contacts.csv')
}
format.xls {
send_data(contacts_to_xls(@contacts),
:filename => 'contacts.xls',
:type => 'application/vnd.ms-excel',
:disposition => 'attachment')
}
format.vcf {
send_data(contacts_to_vcard(@contacts),
:filename => 'contacts.vcf',
:type => 'text/x-vcard',
:disposition => 'attachment')
}
end
else
respond_to do |format|
format.html {
last_notes
@tags = Contact.available_tags(:project => @project)
render(:template => 'contacts/index', :layout => !request.xhr?)
}
format.any(:atom, :csv, :pdf) { render(:nothing => true) }
format.api { render_validation_errors(@query) }
end
end
end
def show
find_contact_issues
respond_to do |format|
format.js if request.xhr?
format.html { @contact.viewed }
format.api
format.atom { render_feed(@notes, :title => "#{@contact.name || Setting.app_title}: #{l(:label_crm_note_plural)}") }
format.vcf { send_data(contact_to_vcard(@contact), :filename => "#{@contact.name}.vcf", :type => 'text/x-vcard;', :disposition => 'attachment') }
end
end
def edit
end
def update
@contact.safe_attributes = params[:contact]
@contact.save_attachments(params[:attachments] || (params[:contact] && params[:contact][:uploads]))
if @contact.save
flash[:notice] = l(:notice_successful_update)
remove_old_avatars
respond_to do |format|
format.html { redirect_to :action => 'show', :project_id => params[:project_id], :id => @contact }
format.api { render_api_ok }
end
else
respond_to do |format|
format.html { render 'edit', :project_id => params[:project_id], :id => @contact }
format.api { render_validation_errors(@contact) }
end
end
end
def destroy
if @contact.destroy
flash[:notice] = l(:notice_successful_delete)
else
flash[:error] = l(:notice_unsuccessful_save)
end
respond_to do |format|
format.html { redirect_back_or_default :action => 'index', :project_id => params[:project_id] }
format.api { render_api_ok }
end
end
def new
@duplicates = []
@contact = Contact.new
@contact.safe_attributes = params[:contact] if params[:contact] && params[:contact].is_a?(Hash)
end
def create
@contact = Contact.new(:project => @project, :author => User.current)
@contact.safe_attributes = params[:contact]
@contact.save_attachments(params[:attachments] || (params[:contact] && params[:contact][:uploads]))
if @contact.save
flash[:notice] = l(:notice_successful_create)
remove_old_avatars
respond_to do |format|
format.html { redirect_to (params[:continue] ? { :action => 'new', :project_id => @project } : { :action => 'show', :project_id => @project, :id => @contact }) }
format.js
format.api { redirect_on_create(params) }
end
else
respond_to do |format|
format.api { render_validation_errors(@contact) }
format.js { render :action => 'new' }
format.html { render :action => 'new' }
end
end
end
def contacts_notes
unless request.xhr?
@tags = Contact.available_tags(:project => @project)
end
contacts = find_contacts(false)
deals = find_deals
joins = " "
joins << " LEFT OUTER JOIN #{Contact.table_name} ON #{Note.table_name}.source_id = #{Contact.table_name}.id AND #{Note.table_name}.source_type = 'Contact' "
joins << " LEFT OUTER JOIN #{Deal.table_name} ON #{Note.table_name}.source_id = #{Deal.table_name}.id AND #{Note.table_name}.source_type = 'Deal' "
cond = "(1 = 1) "
cond << "and (#{Contact.table_name}.id in (#{contacts.any? ? contacts.map(&:id).join(', ') : 'NULL'})"
cond << " or #{Deal.table_name}.id in (#{deals.any? ? deals.map(&:id).join(', ') : 'NULL'}))"
cond << " and (LOWER(#{Note.table_name}.content) LIKE '%#{params[:search_note].downcase}%')" if params[:search_note] and request.xhr?
cond << " and (#{Note.table_name}.author_id = #{params[:note_author_id]})" if !params[:note_author_id].blank?
cond << " and (#{Note.table_name}.type_id = #{params[:type_id]})" if !params[:type_id].blank?
scope = Note.joins(joins).where(cond).order("#{Note.table_name}.created_on DESC")
@notes_pages = Paginator.new(scope.count, 20, params['page'])
@notes = scope.limit(20).offset(@notes_pages.offset)
respond_to do |format|
format.html { render :partial => "notes/notes_list", :layout => false, :locals => { :notes => @notes, :notes_pages => @notes_pages } if request.xhr? }
format.xml { render :xml => @notes }
format.csv { send_data(notes_to_csv(@notes), :type => 'text/csv; header=present', :filename => 'notes.csv') }
format.atom { render_feed(@notes, :title => "#{l(:label_crm_note_plural)}") }
end
end
def context_menu
@project = Project.find(params[:project_id]) unless params[:project_id].blank?
@contacts = Contact.visible.where(:id => params[:selected_contacts])
@contact = @contacts.first if (@contacts.size == 1)
@can = { :edit => (@contact && @contact.editable?) || (@contacts && @contacts.collect { |c| c.editable? }.inject { |memo, d| memo && d }),
:create_deal => (@project && User.current.allowed_to?(:add_deals, @project)),
:create => (@project && User.current.allowed_to?(:add_contacts, @project)),
:delete => @contacts.collect { |c| c.deletable? }.inject { |memo, d| memo && d },
:send_mails => @contacts.collect { |c| c.send_mail_allowed? && !c.primary_email.blank? }.inject { |memo, d| memo && d }
}
render :layout => false
end
def bulk_destroy
@contacts = Contact.deletable.where(:id => params[:ids])
raise ActiveRecord::RecordNotFound if @contacts.empty?
@contacts.each(&:destroy)
redirect_back_or_default({ :action => 'index', :project_id => params[:project_id] })
end
def bulk_edit
@contacts = Contact.editable.where(:id => params[:ids])
@projects = @contacts.collect { |p| p.projects.to_a.compact }.compact.flatten.uniq
raise ActiveRecord::RecordNotFound if @contacts.empty?
@custom_fields = ContactCustomField.order(:name)
@tag_list = RedmineCrm::TagList.from(@contacts.map(&:tag_list).inject { |memo, t| memo | t })
@project = @projects.first
@assignables = @projects.map(&:assignable_users).inject { |memo, a| memo & a }
@add_projects = Project.allowed_to(:edit_contacts).order(:lft)
end
def bulk_update
@contacts = Contact.editable.where(:id => params[:ids])
raise ActiveRecord::RecordNotFound if @contacts.empty?
unsaved_contact_ids = []
@contacts.each do |contact|
contact.reload
params[:contact][:tag_list] = (contact.tag_list + RedmineCrm::TagList.from(params[:add_tag_list]) - RedmineCrm::TagList.from(params[:delete_tag_list])).uniq
add_project_ids = (!params[:add_projects_list].to_s.blank? && params[:add_projects_list].is_a?(Array)) ? Project.allowed_to(:edit_contacts).where(:id => params[:add_projects_list].collect{|p| p.to_i}).map(&:id) : []
delete_project_ids = (!params[:delete_projects_list].to_s.blank? && params[:delete_projects_list].is_a?(Array)) ? Project.allowed_to(:edit_contacts).where(:id => params[:delete_projects_list].collect{|p| p.to_i}).map(&:id) : []
project_ids = contact.project_ids + add_project_ids - delete_project_ids
params[:contact][:project_ids] = project_ids.uniq if project_ids.any?
contact.tags.clear
contact.safe_attributes = parse_params_for_bulk_contact_attributes(params)
unless contact.save
# Keep unsaved issue ids to display them in flash error
unsaved_contact_ids << contact.id
end
if !params[:note][:content].blank?
note = ContactNote.new
note.safe_attributes = params[:note]
note.author = User.current
contact.notes << note
end
end
set_flash_from_bulk_contact_save(@contacts, unsaved_contact_ids)
redirect_back_or_default({ :controller => 'contacts', :action => 'index', :project_id => @project })
end
def edit_mails
@contacts = Contact.visible.where(:id => params[:ids]).reject { |c| c.email.blank? }
raise ActiveRecord::RecordNotFound if @contacts.empty?
if !@contacts.collect { |c| c.send_mail_allowed? }.inject { |memo, d| memo && d }
deny_access
return
end
end
def send_mails
contacts = Contact.visible.where(:id => params[:ids])
raise ActiveRecord::RecordNotFound if contacts.empty?
if !contacts.collect { |c| c.send_mail_allowed? }.inject { |memo, d| memo && d }
deny_access
return
end
raise_delivery_errors = ActionMailer::Base.raise_delivery_errors
# Force ActionMailer to raise delivery errors so we can catch it
ActionMailer::Base.raise_delivery_errors = true
delivered_contacts = []
error_contacts = []
contacts.each do |contact|
begin
params[:message] = mail_macro(contact, params[:"message-content"])
ContactsMailer.bulk_mail(contact, params).deliver
delivered_contacts << contact
note = ContactNote.new
note.subject = params[:subject]
note.content = params[:message]
note.author = User.current
note.type_id = Note.note_types[:email]
contact.notes << note
Attachment.attach_files(note, params[:attachments])
render_attachment_warning_if_needed(note)
rescue Exception => e
error_contacts << [contact, e.message]
end
flash[:notice] = l(:notice_email_sent, delivered_contacts.map { |c| "#{c.name} <span class='icon icon-email'>#{c.emails.first}</span>" }.join(', ')).chomp[0, 500] if delivered_contacts.any?
flash[:error] = l(:notice_email_error, error_contacts.map { |e| "#{e[0].name}: #{e[1]}"}.join(', ')).chomp[0, 500] if error_contacts.any?
end
ActionMailer::Base.raise_delivery_errors = raise_delivery_errors
redirect_back_or_default({:controller => 'contacts', :action => 'index', :project_id => @project})
end
def preview_email
@text = mail_macro(Contact.visible.where(:id => params[:ids][0]).first, params[:"message-content"])
render :partial => 'common/preview'
end
def load_tab
end
private
def find_contact_issues
scope = @contact.issues
scope = scope.open unless RedmineContacts.settings[:show_closed_issues]
@contact_issues_count = scope.visible.count
@contact_issues = scope.visible.order("#{Issue.table_name}.status_id, #{Issue.table_name}.updated_on DESC").limit(10)
end
def remove_old_avatars
params_hash = params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash : params
avatar_params = params_hash[:attachments].find { |_k, v| v['description'] == 'avatar' }.try(:last) if params_hash[:attachments].present?
return unless avatar_params
avatar_id = avatar_params['token'].split('.').first.to_i
@contact.attachments.where(:description => 'avatar').where('id != ?', avatar_id).destroy_all if @contact.avatar
end
def last_notes(count = 5)
scope = ContactNote.where({})
scope = scope.where("#{Project.table_name}.id = ?", @project.id) if @project
scope = scope.includes(:attachments)
@last_notes = scope.visible.
limit(count).
order("#{ContactNote.table_name}.created_on DESC").uniq
end
def find_contact
@contact = Contact.find(params[:id])
unless @contact.visible?
deny_access
return
end
project_id = (params[:contact] && params[:contact][:project_id]) || params[:project_id]
@project = Project.find_by_identifier(project_id)
@project ||= @contact.project
rescue ActiveRecord::RecordNotFound
render_404
end
def find_deals
scope = Deal.where({})
scope = scope.where("#{Deal.table_name}.project_id = ?", @project.id) if @project
scope = scope.where("#{Deal.table_name}.name LIKE ? ", '%' + params[:search] + '%') if params[:search]
scope = scope.where('1=0') if params[:tag]
@deals = scope.visible
end
def parse_params_for_bulk_contact_attributes(params)
attributes = (params[:contact] || {}).reject { |_k, v| v.blank? }
attributes.each { |k, v| attributes[k] = v.reject { |_key, val| val.blank? } if v.is_a?(Hash) }
attributes.keys.each { |k| attributes[k] = '' if attributes[k] == 'none' }
if custom = attributes[:custom_field_values]
custom.reject! { |_k, v| v.blank? }
custom.keys.each do |k|
if custom[k].is_a?(Array)
custom[k] << '' if custom[k].delete('__none__')
else
custom[k] = '' if custom[k] == '__none__'
end
end
attributes[:custom_field_values] = custom
end
attributes
end
def find_contacts(pages = true)
@tag = RedmineCrm::TagList.from(params[:tag]) unless params[:tag].blank?
scope = Contact.where({})
scope = scope.where("#{Contact.table_name}.job_title = ?", params[:job_title]) unless params[:job_title].blank?
scope = scope.where("#{Contact.table_name}.assigned_to_id = ?", params[:assigned_to_id]) unless params[:assigned_to_id].blank?
scope = scope.where("#{Contact.table_name}.is_company = ?", params[:query]) unless (params[:query].blank? || params[:query] == '2' || params[:query] == '3')
scope = scope.where("#{Contact.table_name}.author_id = ?", User.current) if params[:query] == '3'
case params[:query]
when '2' then scope = scope.order_by_creation
when '3' then scope = scope.order_by_creation
else scope = scope.order_by_name
end
scope = scope.by_project(@project)
params[:search].split(' ').collect{ |search_string| scope = scope.live_search(search_string) } if !params[:search].blank?
scope = scope.visible
scope = scope.tagged_with(params[:tag]) if !params[:tag].blank?
scope = scope.tagged_with(params[:notag], :exclude => true) if !params[:notag].blank?
@contacts_count = scope.count
@contacts = scope
if pages
page_size = params[:page_size].blank? ? 20 : params[:page_size].to_i
@contacts_pages = Paginator.new(self, @contacts_count, page_size, params[:page])
@offset = @contacts_pages.offset
@limit = @contacts_pages.items_per_page
@contacts = @contacts.eager_load([:tags, :avatar]).limit(@limit).offset(@offset)
fake_name = @contacts.first.name if @contacts.length > 0
end
@contacts
end
# Filter for bulk issue operations
def bulk_find_contacts
@contacts = Deal.find_all_by_id(params[:id] || params[:ids], :include => :project)
raise ActiveRecord::RecordNotFound if @contact.empty?
if @contacts.detect { |contact| !contact.visible? }
deny_access
return
end
@projects = @contacts.collect(&:projects).compact.uniq
@project = @projects.first if @projects.size == 1
rescue ActiveRecord::RecordNotFound
render_404
end
def find_project(project_id = nil)
project_id ||= (params[:contact] && params[:contact][:project_id]) || params[:project_id]
@project = Project.find(project_id)
rescue ActiveRecord::RecordNotFound
render_404
end
def authorize_contacts(action = params[:action], _global = false)
case action.to_s
when 'edit', 'update'
@contact.editable? ? true : deny_access
when 'destroy'
@contact.deletable? ? true : deny_access
else
deny_access
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 :action => 'show', :status => :created, :location => contact_url(@contact)
end
end
end
@@ -0,0 +1,102 @@
# This file is a part of Redmine CRM (redmine_contacts) plugin,
# customer relationship management plugin for Redmine
#
# Copyright (C) 2010-2018 RedmineUP
# http://www.redmineup.com/
#
# redmine_contacts 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_contacts 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_contacts. If not, see <http://www.gnu.org/licenses/>.
class ContactsDuplicatesController < ApplicationController
unloadable
helper :contacts
before_action :find_project_by_project_id, :authorize, :except => :search
before_action :find_contact, :except => :duplicates
before_action :find_duplicate, :only => :merge
helper :contacts
def index
@contacts = @contact.duplicates
end
def duplicates
search_first_name = params[:contact][:first_name] if params[:contact] && !params[:contact][:first_name].blank?
search_last_name = params[:contact][:last_name] if params[:contact] && !params[:contact][:last_name].blank?
search_middle_name = params[:contact][:middle_name] if params[:contact] && !params[:contact][:middle_name].blank?
@contact = (Contact.find(params[:contact_id]) if !params[:contact_id].blank?) || Contact.new
@contact.first_name = search_first_name || ''
@contact.last_name = search_last_name || ''
@contact.middle_name = search_middle_name || ''
respond_to do |format|
format.html { render :partial => 'duplicates', :layout => false if request.xhr? }
end
end
def merge
@duplicate.notes << @contact.notes
@duplicate.deals << @contact.deals
@duplicate.related_deals << @contact.related_deals
@duplicate.issues << @contact.issues
@duplicate.projects << @contact.projects
@duplicate.email = (@duplicate.emails | @contact.emails).join(', ')
@duplicate.phone = (@duplicate.phones | @contact.phones).join(', ')
call_hook(:controller_contacts_duplicates_merge, { :params => params, :duplicate => @duplicate, :contact => @contact })
@duplicate.tag_list = @duplicate.tag_list | @contact.tag_list
begin
Contact.transaction do
@duplicate.save!
@duplicate.reload
@contact.reload
@contact.destroy
flash[:notice] = l(:notice_successful_merged)
redirect_to :controller => 'contacts', :action => 'show', :project_id => @project, :id => @duplicate
end
rescue
redirect_to :action => 'duplicates', :contact_id => @contact, :project_id => @project
end
end
def search
@contacts = []
q = (params[:q] || params[:term]).to_s.strip
if q.present?
scope = Contact.where({})
scope = scope.limit(params[:limit] || 10)
scope = scope.companies if params[:is_company]
scope = scope.where(["#{Contact.table_name}.id <> ?", params[:contact_id].to_i]) if params[:contact_id]
@contacts = scope.visible.by_project(@project).live_search(q).to_a.sort!{|x, y| x.name <=> y.name }
else
@contacts = @contact.duplicates
end
render :layout => false, :partial => 'list'
end
private
def find_duplicate
@duplicate = Contact.find(params[:duplicate_id])
render_403 unless @duplicate.editable?
rescue ActiveRecord::RecordNotFound
render_404
end
def find_contact
@contact = Contact.find(params[:contact_id])
rescue ActiveRecord::RecordNotFound
render_404 if !request.xhr?
end
end
@@ -0,0 +1,109 @@
# This file is a part of Redmine CRM (redmine_contacts) plugin,
# customer relationship management plugin for Redmine
#
# Copyright (C) 2010-2018 RedmineUP
# http://www.redmineup.com/
#
# redmine_contacts 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_contacts 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_contacts. If not, see <http://www.gnu.org/licenses/>.
class ContactsIssuesController < ApplicationController
unloadable
before_action :find_contact, :only => [:create_issue, :delete]
before_action :find_issue, :except => [:create_issue]
before_action :find_project_by_project_id, :only => [:create_issue]
before_action :authorize_global, :only => [:close]
before_action :authorize
helper :contacts
def create_issue
deny_access unless User.current.allowed_to?(:manage_contact_issue_relations, @project) || User.current.allowed_to?(:add_issues, @project)
issue = Issue.new
issue.project = @project
issue.author = User.current
issue.status = IssueStatus.default if ActiveRecord::VERSION::MAJOR < 4
issue.start_date ||= Date.today
issue.contacts << @contact
issue.safe_attributes = params[:issue] if params[:issue]
if issue.save
flash[:notice] = l(:notice_successful_add)
else
flash[:error] = issue.errors.full_messages.join('<br>').html_safe
end
redirect_to :back
end
def create
contact_ids = []
if params[:contacts_issue].present?
contact_ids << (params[:contacts_issue][:contact_ids] || params[:contacts_issue][:contact_id])
else
contact_ids << params[:contact_id]
end
contact_ids.flatten.compact.uniq.each do |contact_id|
ContactsIssue.create(:issue_id => @issue.id, :contact_id => contact_id)
end
respond_to do |format|
format.html { redirect_to_referer_or { render :text => 'Added.', :layout => true } }
format.js
end
end
def new
end
def delete
@issue.contacts.delete(@contact)
respond_to do |format|
format.html { redirect_to :back }
format.js
end
end
def close
@issue.init_journal(User.current)
@issue.status = IssueStatus.where(:is_closed => true).first
@issue.save
respond_to do |format|
format.js
format.html { redirect_to :back }
end
end
def autocomplete_for_contact
q = params[:q].to_s
scope = Contact.where({})
q.split(' ').collect { |search_string| scope = scope.live_search(search_string) } unless q.blank?
@contacts = scope.visible.includes(:avatar).order(Contact.fields_for_order_statement).by_project(params[:cross_project_contacts] == '1' ? nil : @project).limit(100)
@contacts -= @issue.contacts if @issue
render :layout => false
end
private
def find_contact
@contact = Contact.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
def find_issue
@issue = Issue.find(params[:issue_id])
@project = @issue.project
rescue ActiveRecord::RecordNotFound
render_404
end
end
@@ -0,0 +1,38 @@
# This file is a part of Redmine CRM (redmine_contacts) plugin,
# customer relationship management plugin for Redmine
#
# Copyright (C) 2010-2018 RedmineUP
# http://www.redmineup.com/
#
# redmine_contacts 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_contacts 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_contacts. If not, see <http://www.gnu.org/licenses/>.
class ContactsMailerController < ActionController::Base
before_action :check_credential
helper :contacts
# Submits an incoming email to ContactsMailer
def index
options = params.dup
email = options.delete(:email)
head ContactsMailer.receive(email, options) ? :created : :unprocessable_entity
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,87 @@
# This file is a part of Redmine CRM (redmine_contacts) plugin,
# customer relationship management plugin for Redmine
#
# Copyright (C) 2010-2018 RedmineUP
# http://www.redmineup.com/
#
# redmine_contacts 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_contacts 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_contacts. If not, see <http://www.gnu.org/licenses/>.
class ContactsProjectsController < ApplicationController
unloadable
before_action :find_optional_project, :find_contact
before_action :find_related_project, :only => [:destroy, :create]
before_action :check_count, :only => :destroy
accept_api_auth :create, :destroy
helper :contacts
def new
@show_form = "true"
respond_to do |format|
format.html { redirect_to :back }
format.js
end
rescue ::ActionController::RedirectBackError
render :text => 'Project added.', :layout => true
end
def create
@contact.projects << @related_project
if @contact.save
respond_to do |format|
format.html { redirect_to :back }
format.js { render :action => "new" }
format.api { render_api_ok }
end
else
respond_to do |format|
format.html { redirect_to :back }
format.js { render :action => "new" }
format.api { render_validation_errors(@contact) }
end
end
end
def destroy
@contact.projects.delete(@related_project)
respond_to do |format|
format.html { redirect_to :back }
format.js {render :action => "new"}
format.api { render_api_ok }
end
end
private
def find_related_project
@related_project = Project.find((params[:project] && params[:project][:id]) || params[:id])
raise Unauthorized unless User.current.allowed_to?(:edit_contacts, @related_project)
rescue ActiveRecord::RecordNotFound
render_404
end
def check_count
deny_access if @contact.projects.size <= 1
end
def find_contact
@contact = Contact.find(params[:contact_id])
raise Unauthorized unless @contact.editable?
rescue ActiveRecord::RecordNotFound
render_404
end
end
@@ -0,0 +1,34 @@
# This file is a part of Redmine CRM (redmine_contacts) plugin,
# customer relationship management plugin for Redmine
#
# Copyright (C) 2010-2018 RedmineUP
# http://www.redmineup.com/
#
# redmine_contacts 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_contacts 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_contacts. If not, see <http://www.gnu.org/licenses/>.
class ContactsSettingsController < ApplicationController
unloadable
before_action :find_project_by_project_id, :authorize
def save
settings = params[:contacts_settings]
settings = settings.to_unsafe_hash if settings.class.to_s == 'ActionController::Parameters'
if settings && settings.is_a?(Hash)
settings.map do |k, v|
ContactsSetting[k, @project.id] = v
end
end
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => params[:tab]
end
end
@@ -0,0 +1,98 @@
# This file is a part of Redmine CRM (redmine_contacts) plugin,
# customer relationship management plugin for Redmine
#
# Copyright (C) 2010-2018 RedmineUP
# http://www.redmineup.com/
#
# redmine_contacts 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_contacts 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_contacts. If not, see <http://www.gnu.org/licenses/>.
class ContactsTagsController < ApplicationController
unloadable
before_action :require_admin, :except => [:index]
before_action :find_tag, :only => [:edit, :update]
before_action :bulk_find_tags, :only => [:context_menu, :merge, :destroy]
accept_api_auth :index
def index
@tags = Contact.all_tag_counts(:order => :name)
respond_to do |format|
format.api
end
end
def edit
end
def destroy
@tags.each do |tag|
begin
tag.reload.destroy
Contact.where("#{Contact.table_name}.cached_tag_list LIKE ?", '%' + tag.name + '%').includes(:tags).each{|c| c.tag_list = c.all_tags_list; c.save}
rescue ::ActiveRecord::RecordNotFound # raised by #reload if tag no longer exists
# nothing to do, tag was already deleted (eg. by a parent)
end
end
redirect_back_or_default(:controller => 'settings', :action => 'plugin', :id => 'redmine_contacts', :tab => "tags")
end
def update
old_name = @tag.name
@tag.name = params[:tag][:name]
if @tag.save
Contact.where("#{Contact.table_name}.cached_tag_list LIKE ?", '%' + old_name + '%').includes(:tags).each{|c| c.tag_list = c.all_tags_list; c.save}
flash[:notice] = l(:notice_successful_update)
respond_to do |format|
format.html { redirect_to :controller => 'settings', :action => 'plugin', :id => 'redmine_contacts', :tab => "tags" }
end
else
respond_to do |format|
format.html { render :action => "edit"}
end
end
end
def context_menu
@tag = @tags.first if (@tags.size == 1)
@back = back_url
render :layout => false
end
def merge
if request.post? && params[:tag] && params[:tag][:name]
RedmineCrm::Tagging.transaction do
tag = RedmineCrm::Tag.where(:name => params[:tag][:name]).first || RedmineCrm::Tag.create(params[:tag])
RedmineCrm::Tagging.where(:tag_id => @tags.map(&:id)).update_all(:tag_id => tag.id)
@tags.select{|t| t.id != tag.id}.each do |t|
t.destroy
Contact.where("#{Contact.table_name}.cached_tag_list LIKE ?", '%' + t.name + '%').includes(:tags).each{|c| c.tag_list = c.all_tags_list; c.save}
end
redirect_to :controller => 'settings', :action => 'plugin', :id => 'redmine_contacts', :tab => "tags"
end
end
end
private
def bulk_find_tags
@tags = RedmineCrm::Tag.where(:id => params[:id] ? [params[:id]] : params[:ids])
raise ActiveRecord::RecordNotFound if @tags.empty?
end
def find_tag
@tag = RedmineCrm::Tag.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
end
@@ -0,0 +1,97 @@
# This file is a part of Redmine CRM (redmine_contacts) plugin,
# customer relationship management plugin for Redmine
#
# Copyright (C) 2010-2018 RedmineUP
# http://www.redmineup.com/
#
# redmine_contacts 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_contacts 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_contacts. If not, see <http://www.gnu.org/licenses/>.
class ContactsVcfController < ApplicationController
unloadable
before_action :find_project_by_project_id, :authorize
def load
begin
vcard = Vcard::Vcard.decode(params[:contact_vcf]).first
contact = {}
fill_name(vcard, contact)
contact[:phone] = vcard.telephones.join(', ')
contact[:email] = vcard.emails.join(', ')
contact[:website] = vcard.url.uri if vcard.url
contact[:birthday] = vcard.birthday
fill_background(vcard, contact)
fill_title(vcard, contact)
fill_address(vcard, contact) if vcard['ADR']
fill_company(vcard, contact) if vcard.org
respond_to do |format|
format.html { redirect_to :controller => 'contacts', :action => 'new', :project_id => @project, :contact => contact }
end
rescue Exception => e
flash[:error] = e.message
respond_to do |format|
format.html { redirect_to :back }
end
end
end
private
def fill_name(vcard, contact)
vcard_charset = get_field_encoding(vcard, 'N')
contact[:first_name] = encode(vcard_charset, vcard.name.given)
contact[:middle_name] = encode(vcard_charset, vcard.name.additional)
contact[:last_name] = encode(vcard_charset, vcard.name.family)
end
def fill_address(vcard, contact)
vcard_charset = get_field_encoding(vcard, 'ADR')
contact[:address_attributes] = {}
contact[:address_attributes][:street1] = encode(vcard_charset, vcard.address.street)
contact[:address_attributes][:city] = encode(vcard_charset, vcard.address.locality)
contact[:address_attributes][:postcode] = encode(vcard_charset, vcard.address.postalcode)
contact[:address_attributes][:region] = encode(vcard_charset, vcard.address.region)
end
def fill_background(vcard, contact)
vcard_charset = get_field_encoding(vcard, 'NOTE')
contact[:background] = encode(vcard_charset, vcard.note)
end
def fill_company(vcard, contact)
vcard_charset = get_field_encoding(vcard, 'ORG')
contact[:company] = encode(vcard_charset, vcard.org.first)
end
def fill_title(vcard, contact)
vcard_charset = get_field_encoding(vcard, 'TITLE')
contact[:job_title] = encode(vcard_charset, vcard.title)
end
def get_field_encoding(vcard, field_name)
vcard.fields.find { |field| field.name == field_name }.try(:pvalue, 'CHARSET')
end
def encode(vcard_charset, field)
return field if vcard_charset.nil?
if RUBY_VERSION < '1.9'
Iconv.conv('UTF-8', vcard_charset, field)
else
field.force_encoding(vcard_charset).encode('UTF-8')
end
end
end
@@ -0,0 +1,133 @@
# This file is a part of Redmine CRM (redmine_contacts) plugin,
# customer relationship management plugin for Redmine
#
# Copyright (C) 2010-2018 RedmineUP
# http://www.redmineup.com/
#
# redmine_contacts 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_contacts 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_contacts. If not, see <http://www.gnu.org/licenses/>.
class CrmQueriesController < ApplicationController
before_action :find_query_class
before_action :find_query, :except => [:new, :create, :index]
before_action :find_optional_project, :only => [:new, :create]
before_action :set_menu_item
accept_api_auth :index
helper :queries
include QueriesHelper
def index
case params[:format]
when 'xml', 'json'
@offset, @limit = api_offset_and_limit
else
@limit = per_page_option
end
@query_count = @query_class.visible.count
@query_pages = Paginator.new @query_count, @limit, params['page']
@queries = @query_class.visible.
order("#{Query.table_name}.name").
limit(@limit).
offset(@offset).
all
respond_to do |format|
format.api
end
end
def new
@query = @query_class.new
@query.user = User.current
@query.project = @project
@query.visibility = @query_class::VISIBILITY_PRIVATE unless User.current.allowed_to?("manage_public_#{@object_type}s_queries".to_sym, @project) || User.current.admin?
@query.build_from_params(params)
end
def create
@query = @query_class.new(params_hash[:query])
@query.user = User.current
@query.project = params_hash[:query_is_for_all] ? nil : @project
@query.visibility = @query_class::VISIBILITY_PRIVATE unless User.current.allowed_to?("manage_public_#{@object_type}s_queries".to_sym, @project) || User.current.admin?
@query.build_from_params(params_hash)
@query.column_names = nil if params_hash[:default_columns]
if @query.save
flash[:notice] = l(:notice_successful_create)
redirect_to_list(:query_id => @query)
else
render :action => 'new', :layout => !request.xhr?
end
end
def edit
end
def update
@query.attributes = params_hash[:query]
@query.project = nil if params_hash[:query_is_for_all]
@query.visibility = @query_class::VISIBILITY_PRIVATE unless User.current.allowed_to?("manage_public_#{@object_type}s_queries".to_sym, @project) || User.current.admin?
@query.build_from_params(params_hash)
@query.column_names = nil if params_hash[:default_columns]
if @query.save
flash[:notice] = l(:notice_successful_update)
redirect_to_list(:query_id => @query)
else
render :action => 'edit'
end
end
def destroy
@query.destroy
redirect_to_list(:set_filter => 1)
end
private
def find_query_class
raise NameError if params[:object_type].blank?
@query_class = Object.const_get("#{params[:object_type].to_s.camelcase}Query")
@object_type = params[:object_type]
return false unless @query_class.is_a?(Query)
rescue NameError
render_404
end
def find_query
@query = @query_class.find(params[:id])
@project = @query.project
render_403 unless @query.editable_by?(User.current)
rescue ActiveRecord::RecordNotFound
render_404
end
def find_optional_project
@project = Project.find(params[:project_id]) if params[:project_id]
render_403 unless User.current.allowed_to?("save_#{@object_type}s_queries".to_sym, @project, :global => true)
rescue ActiveRecord::RecordNotFound
render_404
end
def redirect_to_list(options)
redirect_to url_for({:controller => "#{@object_type}s", :action => "index", :project_id => @project}.merge(options))
end
def set_menu_item
menu_items[:project_tabs][:actions][action_name.to_sym] = "#{@object_type}s"
end
def params_hash
@params_hash ||= params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash.symbolize_keys : params
end
end
@@ -0,0 +1,109 @@
# This file is a part of Redmine CRM (redmine_contacts) plugin,
# customer relationship management plugin for Redmine
#
# Copyright (C) 2010-2018 RedmineUP
# http://www.redmineup.com/
#
# redmine_contacts 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_contacts 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_contacts. If not, see <http://www.gnu.org/licenses/>.
class DealCategoriesController < ApplicationController
unloadable
menu_item :settings
model_object DealCategory
before_action :find_model_object, :except => [:new, :index, :create]
before_action :find_project_from_association, :except => [:new, :index, :create]
before_action :find_project_by_project_id, :only => [:new, :index, :create]
before_action :authorize
accept_api_auth :index, :update, :create, :destroy
def index
@categories = @project.deal_categories
respond_to do |format|
format.api
end
end
def create
@category = @project.deal_categories.build
@category.safe_attributes = params[:category]
if @category.save
flash[:notice] = l(:notice_successful_create)
respond_to do |format|
format.html { redirect_to_settings_in_projects }
format.api { render_api_ok }
end
else
respond_to do |format|
format.html { render :action => 'new' }
format.api { render_validation_errors(@category) }
end
end
end
def new
@category = @project.deal_categories.build(params[:category])
end
def edit
end
def update
@category.safe_attributes = params[:category]
if @category.save
# @deal.contacts = [Contact.find(params[:contacts])] if params[:contacts]
flash[:notice] = l(:notice_successful_update)
respond_to do |format|
format.html { redirect_to_settings_in_projects }
format.api { render_api_ok }
end
else
respond_to do |format|
format.html { render :action => 'edit' }
format.api { render_validation_errors(@category) }
end
end
end
def destroy
@deal_count = @category.deals.size
if @deal_count == 0 || params[:todo] || api_request?
reassign_to = nil
if params[:reassign_to_id] && (params[:todo] == 'reassign' || params[:todo].blank?)
reassign_to = @project.deal_categories.find_by_id(params[:reassign_to_id])
end
@category.destroy(reassign_to)
respond_to do |format|
format.html { redirect_to_settings_in_projects }
format.api { render_api_ok }
end
return
end
@categories = @project.deal_categories - [@category]
end
private
def redirect_to_settings_in_projects
redirect_to settings_project_path(@project, :tab => 'deals')
end
# Wrap ApplicationController's find_model_object method to set
# @category instead of just @deal_category
def find_model_object
super
@category = @object
@project = @category.project
end
end
@@ -0,0 +1,81 @@
# This file is a part of Redmine CRM (redmine_contacts) plugin,
# customer relationship management plugin for Redmine
#
# Copyright (C) 2010-2018 RedmineUP
# http://www.redmineup.com/
#
# redmine_contacts 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_contacts 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_contacts. If not, see <http://www.gnu.org/licenses/>.
class DealContactsController < ApplicationController
unloadable
before_action :find_project_by_project_id, :authorize
before_action :find_contact, :only => :delete
before_action :find_deal
helper :deals
helper :contacts
def search
@contacts = contacts.limit(10) - @deal.all_contacts
end
def autocomplete
@contacts = contacts.live_search(params[:q]).limit(100) - @deal.all_contacts
render :layout => false
end
def add
if params[:contact_id] && request.post?
find_contact
unless @deal.all_contacts.include?(@contact)
@deal.related_contacts << @contact
@deal.save
end
end
respond_to do |format|
format.html do
redirect_to :back
end
format.js
end
end
def delete
@deal.related_contacts.delete(@contact)
respond_to do |format|
format.html { redirect_to :back }
format.js
end
end
private
def contacts
Contact.visible.by_project(ContactsSetting.cross_project_contacts? ? nil : @project)
end
def find_contact
@contact = Contact.find(params[:contact_id])
rescue ActiveRecord::RecordNotFound
render_404
end
def find_deal
@deal = Deal.find(params[:deal_id])
rescue ActiveRecord::RecordNotFound
render_404
end
end
@@ -0,0 +1,36 @@
# This file is a part of Redmine CRM (redmine_contacts) plugin,
# customer relationship management plugin for Redmine
#
# Copyright (C) 2010-2018 RedmineUP
# http://www.redmineup.com/
#
# redmine_contacts 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_contacts 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_contacts. If not, see <http://www.gnu.org/licenses/>.
class DealImportsController < ImporterBaseController
menu_item :deals
helper :deals
def klass
DealImport
end
def importer_klass
DealKernelImport
end
def instance_index
project_deals_path(:project_id => @project.id)
end
end
@@ -0,0 +1,95 @@
# This file is a part of Redmine CRM (redmine_contacts) plugin,
# customer relationship management plugin for Redmine
#
# Copyright (C) 2010-2018 RedmineUP
# http://www.redmineup.com/
#
# redmine_contacts 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_contacts 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_contacts. If not, see <http://www.gnu.org/licenses/>.
class DealStatusesController < ApplicationController
unloadable
layout 'admin'
before_action :require_admin, :except => :assing_to_project
before_action :find_project_by_project_id, :authorize, :only => :assing_to_project
accept_api_auth :index
def index
@deal_statuses = DealStatus.order(:position)
respond_to do |format|
format.api
format.html { render :action => 'index', :layout => false if request.xhr? }
end
end
def new
@deal_status = DealStatus.new
end
def create
@deal_status = DealStatus.new
@deal_status.safe_attributes = params[:deal_status]
if @deal_status.save
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'plugin', :id => 'redmine_contacts', :controller => 'settings', :tab => 'deal_statuses'
else
render :action => 'new'
end
end
def edit
@deal_status = DealStatus.find(params[:id])
end
def update
@deal_status = DealStatus.find(params[:id])
@deal_status.safe_attributes = params[:deal_status]
@deal_status.insert_at(@deal_status.position) if @deal_status.position_changed?
if @deal_status.save
respond_to do |format|
format.html do
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'plugin', :id => 'redmine_contacts', :controller => 'settings', :tab => 'deal_statuses'
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
DealStatus.find(params[:id]).destroy
redirect_to :action => 'plugin', :id => 'redmine_contacts', :controller => 'settings', :tab => 'deal_statuses'
rescue
flash[:error] = l(:error_unable_delete_deal_status)
redirect_to :action => 'plugin', :id => 'redmine_contacts', :controller => 'settings', :tab => 'deal_statuses'
end
def assing_to_project
if request.put?
@project.deal_statuses = !params[:deal_statuses].blank? ? DealStatus.find(params[:deal_statuses]) : []
@project.save
flash[:notice] = l(:notice_successful_update)
end
redirect_to :controller => 'projects', :action => 'settings', :tab => 'deals', :id => @project
end
end
@@ -0,0 +1,333 @@
# This file is a part of Redmine CRM (redmine_contacts) plugin,
# customer relationship management plugin for Redmine
#
# Copyright (C) 2010-2018 RedmineUP
# http://www.redmineup.com/
#
# redmine_contacts 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_contacts 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_contacts. If not, see <http://www.gnu.org/licenses/>.
class DealsController < ApplicationController
unloadable
PRICE_TYPE_PULLDOWN = [l(:label_price_fixed_bid), l(:label_price_per_hour)]
before_action :find_deal, :only => [:show, :edit, :update, :destroy]
before_action :find_project, :only => [:new, :create, :update_form]
before_action :bulk_find_deals, :only => [:bulk_update, :bulk_edit, :bulk_destroy, :context_menu]
before_action :authorize, :except => [:index]
before_action :find_optional_project, :only => [:index]
before_action :update_deal_from_params, :only => [:edit, :update]
before_action :build_new_deal_from_params, :only => [:new, :update_form]
before_action :find_deal_attachments, :only => :show
skip_before_filter :authorize, :only => :add_product_line if RedmineContacts.products_plugin_installed?
accept_api_auth :index, :show, :create, :update, :destroy
helper :attachments
helper :timelog
helper :watchers
helper :custom_fields
helper :context_menus
helper :sort
helper :crm_queries
helper :notes
helper :queries
helper :calendars
include QueriesHelper
include CrmQueriesHelper
include WatchersHelper
include DealsHelper
include SortHelper
if RedmineContacts.products_plugin_installed?
include ProductsHelper
helper :products
end
def index
retrieve_crm_query('deal')
sort_init(@query.sort_criteria.empty? ? [['created_on', 'desc']] : @query.sort_criteria)
sort_update(@query.sortable_columns)
@query.sort_criteria = sort_criteria.to_a
if @query.valid?
case params[:format]
when 'csv', 'pdf'
@limit = Setting.issues_export_limit.to_i
when 'atom'
@limit = Setting.feeds_limit.to_i
when 'xml', 'json'
@offset, @limit = api_offset_and_limit
else
@limit = per_page_option
end
@deals_count = @query.object_count
@deals_scope = @query.objects_scope
@deal_amount = @query.deal_amount
@deal_weighted_amount = @query.weighted_amount
@deals_pages = Paginator.new @deals_count, @limit, params['page']
@offset ||= @deals_pages.offset
@deal_count_by_group = @query.object_count_by_group
@deals = @query.results_scope(
:include => [{ :contact => [:avatar, :projects, :address] }, :author],
:search => params[:search],
:order => sort_clause,
:limit => @limit,
:offset => @offset
)
if deals_list_style == 'crm_calendars/crm_calendar'
retrieve_crm_calendar(:start_date_field => 'due_date')
@calendar.events = @query.results_scope(
:include => [:contact],
:search => params[:search],
:conditions => ['due_date BETWEEN ? AND ?', @calendar.startdt, @calendar.enddt]
)
end
respond_to do |format|
format.html { request.xhr? ? render(:partial => deals_list_style, :layout => false) : last_notes }
format.api
format.atom { render_feed(@deals, :title => "#{@project || Setting.app_title}: #{l(:label_order_plural)}") }
format.csv { send_data(deals_to_csv(@deals), :type => 'text/csv; header=present', :filename => 'deals.csv') }
format.pdf { send_data(deals_to_pdf(@deals, @project, @query), :type => 'application/pdf', :filename => 'deals.pdf') }
end
else
respond_to do |format|
format.html { render(:template => 'deals/index', :layout => !request.xhr?) }
format.any(:atom, :csv, :pdf) { render(:nothing => true) }
format.api { render_validation_errors(@query) }
end
end
rescue ActiveRecord::RecordNotFound
render_404
end
def show
@note = DealNote.new(:created_on => Time.now)
respond_to do |format|
format.html do
@deal_issues = @deal.issues.visible
@deal.viewed
@deal_events = (@deal.deal_processes.where("#{DealProcess.table_name}.old_value IS NOT NULL").includes([:to, :from, :author]) | @deal.notes.includes([:attachments, :author])).map{|o| {:date => o.is_a?(DealProcess) ? o.created_at : o.created_on, :author => o.author, :object => o} }
@deal_events.sort! { |x, y| y[:date] <=> x[:date] }
end
format.api
end
end
def new
end
def create
@deal = Deal.new
@deal.safe_attributes = params[:deal]
@deal.project = @project
@deal.author ||= User.current
@deal.price = parsed_price(params[:deal][:price])
@deal.init_deal_process(User.current)
if @deal.save
flash[:notice] = l(:notice_successful_create)
respond_to do |format|
format.html { redirect_to(params[:continue] ? { :action => 'new' } : { :action => 'show', :id => @deal }) }
format.api { render :action => 'show', :status => :created, :location => deal_url(@deal) }
end
else
respond_to do |format|
format.html { render :action => 'new' }
format.api { render_validation_errors(@deal) }
end
end
end
def update
@deal.init_deal_process(User.current)
@deal.safe_attributes = params[:deal]
if @deal.save
# @deal.contacts = [Contact.find(params[:contacts])] if params[:contacts]
retrieve_crm_query('deal')
@deals_scope = @query.objects_scope
flash[:notice] = l(:notice_successful_update)
respond_to do |format|
format.html { redirect_back_or_default(:action => 'show', :id => @deal) }
format.api { render_api_ok }
format.js { render :update_total }
end
else
respond_to do |format|
format.html { render :action => 'edit' }
format.api { render_validation_errors(@deal) }
format.js { render "alert('Error!')" }
end
end
end
def edit
respond_to do |format|
format.html {}
format.xml {}
end
end
def destroy
if @deal.destroy
flash[:notice] = l(:notice_successful_delete)
respond_to do |format|
format.html { redirect_to :action => 'index', :project_id => params[:project_id] }
format.api { render_api_ok }
end
else
flash[:error] = l(:notice_unsuccessful_save)
end
end
def context_menu
@deal = @deals.first if @deals.size == 1
@can = { :edit => User.current.allowed_to?(:edit_deals, @projects),
:delete => User.current.allowed_to?(:delete_deals, @projects) }
@back = back_url
render :layout => false
end
def bulk_destroy
@deals.each do |deal|
begin
deal.reload.destroy
rescue ::ActiveRecord::RecordNotFound # raised by #reload if deal no longer exists
# nothing to do, deal was already deleted (eg. by a parent)
end
end
respond_to do |format|
format.html { redirect_back_or_default(:action => 'index', :project_id => params[:project_id]) }
format.api { head :ok }
end
end
def bulk_edit
@available_statuses = @projects.map(&:deal_statuses).inject { |memo, w| memo & w }
@custom_fields = DealCustomField.order(:name)
@available_categories = @projects.map(&:deal_categories).inject { |memo, w| memo & w }
@assignables = @projects.map(&:assignable_users).inject { |memo, a| memo & a }
end
def bulk_update
unsaved_deal_ids = []
@deals.each do |deal|
deal.reload
deal.init_deal_process(User.current)
deal.safe_attributes = parse_params_for_bulk_deal_attributes(params)
unless deal.save
# Keep unsaved deal ids to display them in flash error
unsaved_deal_ids << deal.id
end
if params[:note] && !params[:note][:content].blank?
note = DealNote.new
note.safe_attributes = params[:note]
note.author = User.current
deal.notes << note
end
end
set_flash_from_bulk_contact_save(@deals, unsaved_deal_ids)
redirect_back_or_default(:controller => 'deals', :action => 'index', :project_id => @project)
end
private
def last_notes(count = 5)
# TODO: Исправить говнокод этот и выделить все в плагин acts-as-noteble
scope = DealNote.where({})
scope = scope.where("#{Deal.table_name}.project_id = ?", @project.id) if @project
@last_notes = scope.visible.order("#{DealNote.table_name}.created_on DESC").limit(count)
end
def build_new_deal_from_params
if params[:id].blank?
@deal = Deal.new
@deal.assigned_to_id = User.current.id
@deal.name = params[:name] if params[:name]
@deal.contact = Contact.find(params[:contact_id]) if params[:contact_id]
if params[:copy_from]
begin
@copy_from = Deal.visible.find(params[:copy_from])
@deal.copy_from(@copy_from)
rescue ActiveRecord::RecordNotFound
render_404
return
end
end
else
@deal = Deal.visible.find(params[:id])
end
@deal.project = @project
@deal.author ||= User.current
@deal.safe_attributes = params[:deal]
@available_watchers = (@deal.project.users.sort + @deal.watcher_users).uniq
end
def update_deal_from_params
end
def update_form
end
def find_deal_attachments
@deal_attachments = Attachment.where(:container_type => 'Note', :container_id => @deal.notes.map(&:id)).order(:created_on)
end
def bulk_find_deals
@deals = Deal.where(:id => (params[:id] || params[:ids])).includes([:project, :contact])
raise ActiveRecord::RecordNotFound if @deals.empty?
if @deals.detect { |deal| !deal.visible? }
deny_access
return
end
@projects = @deals.collect(&:project).compact.uniq
@project = @projects.first if @projects.size == 1
rescue ActiveRecord::RecordNotFound
render_404
end
def find_deal
@deal = Deal.where(:id => params[:id]).includes([:project, :status, :category]).first
@project = @deal.project
rescue ActiveRecord::RecordNotFound
render_404
end
def find_project(project_id = nil)
project_id ||= (params[:deal] && params[:deal][:project_id]) || params[:project_id]
@project = Project.find(project_id)
rescue ActiveRecord::RecordNotFound
render_404
end
def parse_params_for_bulk_deal_attributes(params)
attributes = (params[:deal] || {}).reject { |_k, v| v.blank? }
attributes.keys.each { |k| attributes[k] = '' if attributes[k] == 'none' }
attributes[:custom_field_values].reject! { |_k, v| v.blank? } if attributes[:custom_field_values]
attributes
end
def parsed_price(price)
return unless price
price.gsub!(ContactsSetting.thousands_delimiter, '')
price.gsub!(ContactsSetting.decimal_separator, '.')
price.to_f
end
end
@@ -0,0 +1,74 @@
# This file is a part of Redmine CRM (redmine_contacts) plugin,
# customer relationship management plugin for Redmine
#
# Copyright (C) 2010-2018 RedmineUP
# http://www.redmineup.com/
#
# redmine_contacts 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_contacts 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_contacts. If not, see <http://www.gnu.org/licenses/>.
class DealsTasksController < ApplicationController
unloadable
before_action :find_project_by_project_id, :authorize
before_action :find_deal, :except => [:close]
before_action :find_issue, :except => [:new]
def new
issue = Issue.new
issue.subject = params[:task_subject]
issue.project = @project
issue.tracker_id = params[:task_tracker]
issue.author = User.current
issue.due_date = params[:due_date]
issue.assigned_to_id = params[:assigned_to]
issue.description = params[:task_description]
issue.status = IssueStatus.default
if issue.save
flash[:notice] = l(:notice_successful_add)
@deal.issues << issue
@deal.save
redirect_to :back
return
else
redirect_to :back
end
end
def close
@issue.status = IssueStatus.find(:first, :conditions => { :is_closed => true })
@issue.save
respond_to do |format|
format.js do
render :update do |page|
page["issue_#{params[:issue_id]}"].visual_effect :fade
end
end
format.html {redirect_to :back }
end
end
private
def find_deal
@deal = Deal.find(params[:deal_id])
rescue ActiveRecord::RecordNotFound
render_404
end
def find_issue
@issue = Issue.find(params[:issue_id])
rescue ActiveRecord::RecordNotFound
render_404
end
end
@@ -0,0 +1,149 @@
# This file is a part of Redmine CRM (redmine_contacts) plugin,
# customer relationship management plugin for Redmine
#
# Copyright (C) 2010-2018 RedmineUP
# http://www.redmineup.com/
#
# redmine_contacts 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_contacts 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_contacts. If not, see <http://www.gnu.org/licenses/>.
class ImporterBaseController < ApplicationController
unloadable
if Redmine::VERSION.to_s >= '3.2'
helper :imports
before_action :find_import, :only => [:show, :settings, :mapping, :run]
end
before_action :find_project_by_project_id, :authorize
def new
@importer = klass.new
if Redmine::VERSION.to_s >= '3.2'
render 'importers/kernel_new'
else
render 'importers/new'
end
end
def create
if Redmine::VERSION.to_s >= '3.2'
@import = importer_klass.new
@import.user = User.current
@import.project = @project
@import.file = params[:file]
@import.set_default_settings
if @import.save
redirect_to :controller => klass.name.tableize, :action => 'settings', :id => @import, :project_id => @project
else
render 'importers/kernel_new'
end
else
@importer = klass.new(params[klass.to_s.underscore.to_sym])
@importer.project = @project
if @importer.file && @importer.save
redirect_to instance_index
else
render 'importers/new'
end
end
end
def show
render 'importers/show'
end
def settings
if request.post? && @import.parse_file
return redirect_to :controller => klass.name.tableize, :action => 'mapping', :id => @import, :project_id => @project
end
render 'importers/settings'
rescue CSV::MalformedCSVError => e
flash.now[:error] = l(:error_invalid_csv_file_or_settings)
render 'importers/settings'
rescue ArgumentError, Encoding::InvalidByteSequenceError => e
flash.now[:error] = l(:error_invalid_file_encoding, :encoding => ERB::Util.h(@import.settings['encoding']))
render 'importers/settings'
rescue SystemCallError => e
flash.now[:error] = l(:error_can_not_read_import_file)
render 'importers/settings'
end
def mapping
mapping_object = klass.new.klass.new
@attributes = mapping_object.safe_attribute_names
@custom_fields = mapping_object.custom_field_values.map(&:custom_field)
if request.post?
respond_to do |format|
format.html do
if params[:previous]
redirect_to :controller => klass.name.tableize, :action => 'settings', :id => @import, :project_id => @project
else
redirect_to :controller => klass.name.tableize, :action => 'run', :id => @import, :project_id => @project
end
end
end
else
render 'importers/mapping'
end
end
def run
if request.post?
@current = @import.run(
:max_items => max_items_per_request,
:max_time => 10.seconds
)
respond_to do |format|
format.html do
if @import.finished?
redirect_to :controller => klass.name.tableize, :action => 'show', :id => @import, :project_id => @project
else
redirect_to :controller => klass.name.tableize, :action => 'run', :id => @import, :project_id => @project
end
end
format.js { render 'importers/run' }
end
else
render 'importers/run'
end
end
private
def find_import
@import = Import.where(:user_id => User.current.id, :filename => params[:id]).first
if @import.nil?
render_404
return
elsif @import.finished? && action_name != 'show'
redirect_to new_project_contact_import_path(@import)
return
end
update_from_params if request.post?
end
def update_from_params
if params[:import_settings].present?
@import.settings ||= {}
@import.settings.merge!(params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash['import_settings'] : params['import_settings'])
@import.save!
end
end
def max_items_per_request
5
end
end
@@ -0,0 +1,130 @@
# This file is a part of Redmine CRM (redmine_contacts) plugin,
# customer relationship management plugin for Redmine
#
# Copyright (C) 2010-2018 RedmineUP
# http://www.redmineup.com/
#
# redmine_contacts 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_contacts 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_contacts. If not, see <http://www.gnu.org/licenses/>.
class NotesController < ApplicationController
unloadable
default_search_scope :notes
# before_filter :find_model_object
before_action :find_note, :only => [:show, :edit, :update, :destroy]
before_action :find_project, :only => :create
before_action :find_note_source, :only => :create
before_action :find_optional_project, :only => :show
accept_api_auth :show, :create, :update, :destroy
helper :attachments
helper :custom_fields
def show
(render_403; return false) unless @note.visible?
respond_to do |format|
format.html
format.api
end
end
def new
find_note_source
@note = Note.new
@note.source = @note_source
end
def edit
(render_403; return false) unless @note.editable_by?(User.current, @project)
end
def update
@note.safe_attributes = params[:note]
if @note.save
@note.note_time = params[:note][:note_time] if params[:note] && params[:note][:note_time]
attachments = Attachment.attach_files(@note, (params[:attachments] || (params[:note] && params[:note][:uploads])))
render_attachment_warning_if_needed(@note)
flash[:notice] = l(:notice_successful_update)
respond_to do |format|
format.html { redirect_back_or_default({ :action => 'show', :project_id => @note.source.project, :id => @note }) }
format.api { render_api_ok }
end
else
respond_to do |format|
format.html { render :action => 'edit', :project_id => params[:project_id], :id => @note }
format.api { render_validation_errors(@note) }
end
end
end
def create
@note = Note.new
@note.safe_attributes = params[:note]
@note.source = @note_source
@note.note_time = params[:note][:note_time] if params[:note] && params[:note][:note_time]
@note.author = User.current
if @note.save
attachments = Attachment.attach_files(@note, (params[:attachments] || (params[:note] && params[:note][:uploads])))
render_attachment_warning_if_needed(@note)
flash[:notice] = l(:notice_successful_create)
respond_to do |format|
format.js
format.html { redirect_to :back }
format.api { render :action => 'show', :status => :created, :location => note_url(@note) }
end
else
respond_to do |format|
format.html { redirect_to :back }
format.api { render_validation_errors(@note) }
end
end
end
def destroy
(render_403; return false) unless @note.destroyable_by?(User.current, @project)
@note.destroy
respond_to do |format|
format.js
format.html { redirect_to :action => 'show', :project_id => @project, :id => @note.source }
format.api { render_api_ok }
end
# redirect_to :action => 'show', :project_id => @project, :id => @contact
end
private
def find_project(project_id = nil)
project_id ||= (params[:note] && params[:note][:project_id]) || params[:project_id]
@project = Project.find(project_id)
rescue ActiveRecord::RecordNotFound
render_404
end
def find_note
@note = Note.find(params[:id])
@project ||= @note.project
rescue ActiveRecord::RecordNotFound
render_404
end
def find_note_source
note_source_type = (params[:note] && params[:note][:source_type]) || params[:source_type]
note_source_id = (params[:note] && params[:note][:source_id]) || params[:source_id]
klass = Object.const_get(note_source_type.camelcase)
@note_source = klass.find(note_source_id)
end
end
@@ -0,0 +1,110 @@
# This file is a part of Redmine CRM (redmine_contacts) plugin,
# customer relationship management plugin for Redmine
#
# Copyright (C) 2010-2018 RedmineUP
# http://www.redmineup.com/
#
# redmine_contacts 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_contacts 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_contacts. If not, see <http://www.gnu.org/licenses/>.
class TasksController < ApplicationController
unloadable
before_action :find_project_by_project_id, :authorize, :except => [:index]
before_action :find_optional_project, :only => :index
before_action :find_taskable, :except => [:index, :add, :close]
before_action :find_issue, :except => [:index, :new]
def new
issue = Issue.new
issue.subject = params[:task_subject]
issue.project = @project
issue.tracker_id = params[:task_tracker]
issue.author = User.current
issue.due_date = params[:due_date]
issue.assigned_to_id = params[:assigned_to]
issue.description = params[:task_description]
issue.status = IssueStatus.default
if issue.save
flash[:notice] = l(:notice_successful_add)
@taskable.issues << issue
@taskable.save
redirect_to :back
return
else
redirect_to :back
end
end
def add
@show_form = 'true'
if params[:source_id] && params[:source_type] && request.post?
find_taskable
@taskable.issues << @issue
@taskable.save
end
taskable_name = @taskable.class.name.underscore
respond_to do |format|
format.html { redirect_to :back }
format.js do
render :update do |page|
page.replace_html "issue_#{taskable_name}s", :partial => "issues/#{taskable_name}s"
end
end
end
end
def delete
@issue.taskables.delete(@taskable)
taskable_name = @taskable.class.name.underscore
respond_to do |format|
format.html { redirect_to :back }
format.js do
render :update do |page|
page.replace_html "issue_#{taskable_name}s", :partial => "issues/#{taskable_name}s"
end
end
end
end
def close
@issue.status = IssueStatus.find(:first, :conditions => { :is_closed => true })
@issue.save
respond_to do |format|
format.js do
render :update do |page|
page["issue_#{params[:issue_id]}"].visual_effect :fade
end
end
format.html { redirect_to :back }
end
end
private
def find_taskable
klass = Object.const_get(params[:source_type].camelcase)
@taskable = klass.find(params[:source_id])
rescue ActiveRecord::RecordNotFound
render_404
end
def find_issue
@issue = Issue.find(params[:issue_id])
rescue ActiveRecord::RecordNotFound
render_404
end
end