##// END OF EJS Templates
Replaces find(:first/:all) calls....
Jean-Philippe Lang -
r10704:ea296a109a86
parent child
Show More
@@ -1,84 +1,83
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class AdminController < ApplicationController
18 class AdminController < ApplicationController
19 layout 'admin'
19 layout 'admin'
20 menu_item :projects, :only => :projects
20 menu_item :projects, :only => :projects
21 menu_item :plugins, :only => :plugins
21 menu_item :plugins, :only => :plugins
22 menu_item :info, :only => :info
22 menu_item :info, :only => :info
23
23
24 before_filter :require_admin
24 before_filter :require_admin
25 helper :sort
25 helper :sort
26 include SortHelper
26 include SortHelper
27
27
28 def index
28 def index
29 @no_configuration_data = Redmine::DefaultData::Loader::no_data?
29 @no_configuration_data = Redmine::DefaultData::Loader::no_data?
30 end
30 end
31
31
32 def projects
32 def projects
33 @status = params[:status] || 1
33 @status = params[:status] || 1
34
34
35 scope = Project.status(@status)
35 scope = Project.status(@status).order('lft')
36 scope = scope.like(params[:name]) if params[:name].present?
36 scope = scope.like(params[:name]) if params[:name].present?
37
37 @projects = scope.all
38 @projects = scope.all(:order => 'lft')
39
38
40 render :action => "projects", :layout => false if request.xhr?
39 render :action => "projects", :layout => false if request.xhr?
41 end
40 end
42
41
43 def plugins
42 def plugins
44 @plugins = Redmine::Plugin.all
43 @plugins = Redmine::Plugin.all
45 end
44 end
46
45
47 # Loads the default configuration
46 # Loads the default configuration
48 # (roles, trackers, statuses, workflow, enumerations)
47 # (roles, trackers, statuses, workflow, enumerations)
49 def default_configuration
48 def default_configuration
50 if request.post?
49 if request.post?
51 begin
50 begin
52 Redmine::DefaultData::Loader::load(params[:lang])
51 Redmine::DefaultData::Loader::load(params[:lang])
53 flash[:notice] = l(:notice_default_data_loaded)
52 flash[:notice] = l(:notice_default_data_loaded)
54 rescue Exception => e
53 rescue Exception => e
55 flash[:error] = l(:error_can_t_load_default_data, e.message)
54 flash[:error] = l(:error_can_t_load_default_data, e.message)
56 end
55 end
57 end
56 end
58 redirect_to :action => 'index'
57 redirect_to :action => 'index'
59 end
58 end
60
59
61 def test_email
60 def test_email
62 raise_delivery_errors = ActionMailer::Base.raise_delivery_errors
61 raise_delivery_errors = ActionMailer::Base.raise_delivery_errors
63 # Force ActionMailer to raise delivery errors so we can catch it
62 # Force ActionMailer to raise delivery errors so we can catch it
64 ActionMailer::Base.raise_delivery_errors = true
63 ActionMailer::Base.raise_delivery_errors = true
65 begin
64 begin
66 @test = Mailer.test_email(User.current).deliver
65 @test = Mailer.test_email(User.current).deliver
67 flash[:notice] = l(:notice_email_sent, User.current.mail)
66 flash[:notice] = l(:notice_email_sent, User.current.mail)
68 rescue Exception => e
67 rescue Exception => e
69 flash[:error] = l(:notice_email_error, e.message)
68 flash[:error] = l(:notice_email_error, e.message)
70 end
69 end
71 ActionMailer::Base.raise_delivery_errors = raise_delivery_errors
70 ActionMailer::Base.raise_delivery_errors = raise_delivery_errors
72 redirect_to :controller => 'settings', :action => 'edit', :tab => 'notifications'
71 redirect_to :controller => 'settings', :action => 'edit', :tab => 'notifications'
73 end
72 end
74
73
75 def info
74 def info
76 @db_adapter_name = ActiveRecord::Base.connection.adapter_name
75 @db_adapter_name = ActiveRecord::Base.connection.adapter_name
77 @checklist = [
76 @checklist = [
78 [:text_default_administrator_account_changed, User.default_admin_account_changed?],
77 [:text_default_administrator_account_changed, User.default_admin_account_changed?],
79 [:text_file_repository_writable, File.writable?(Attachment.storage_path)],
78 [:text_file_repository_writable, File.writable?(Attachment.storage_path)],
80 [:text_plugin_assets_writable, File.writable?(Redmine::Plugin.public_directory)],
79 [:text_plugin_assets_writable, File.writable?(Redmine::Plugin.public_directory)],
81 [:text_rmagick_available, Object.const_defined?(:Magick)]
80 [:text_rmagick_available, Object.const_defined?(:Magick)]
82 ]
81 ]
83 end
82 end
84 end
83 end
@@ -1,105 +1,110
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class BoardsController < ApplicationController
18 class BoardsController < ApplicationController
19 default_search_scope :messages
19 default_search_scope :messages
20 before_filter :find_project_by_project_id, :find_board_if_available, :authorize
20 before_filter :find_project_by_project_id, :find_board_if_available, :authorize
21 accept_rss_auth :index, :show
21 accept_rss_auth :index, :show
22
22
23 helper :sort
23 helper :sort
24 include SortHelper
24 include SortHelper
25 helper :watchers
25 helper :watchers
26
26
27 def index
27 def index
28 @boards = @project.boards.includes(:last_message => :author).all
28 @boards = @project.boards.includes(:last_message => :author).all
29 # show the board if there is only one
29 # show the board if there is only one
30 if @boards.size == 1
30 if @boards.size == 1
31 @board = @boards.first
31 @board = @boards.first
32 show
32 show
33 end
33 end
34 end
34 end
35
35
36 def show
36 def show
37 respond_to do |format|
37 respond_to do |format|
38 format.html {
38 format.html {
39 sort_init 'updated_on', 'desc'
39 sort_init 'updated_on', 'desc'
40 sort_update 'created_on' => "#{Message.table_name}.created_on",
40 sort_update 'created_on' => "#{Message.table_name}.created_on",
41 'replies' => "#{Message.table_name}.replies_count",
41 'replies' => "#{Message.table_name}.replies_count",
42 'updated_on' => "#{Message.table_name}.updated_on"
42 'updated_on' => "#{Message.table_name}.updated_on"
43
43
44 @topic_count = @board.topics.count
44 @topic_count = @board.topics.count
45 @topic_pages = Paginator.new self, @topic_count, per_page_option, params['page']
45 @topic_pages = Paginator.new self, @topic_count, per_page_option, params['page']
46 @topics = @board.topics.reorder("#{Message.table_name}.sticky DESC").order(sort_clause).all(
46 @topics = @board.topics.
47 :include => [:author, {:last_reply => :author}],
47 reorder("#{Message.table_name}.sticky DESC").
48 :limit => @topic_pages.items_per_page,
48 includes(:author, {:last_reply => :author}).
49 :offset => @topic_pages.current.offset)
49 limit(@topic_pages.items_per_page).
50 offset(@topic_pages.current.offset).
51 order(sort_clause).
52 all
50 @message = Message.new(:board => @board)
53 @message = Message.new(:board => @board)
51 render :action => 'show', :layout => !request.xhr?
54 render :action => 'show', :layout => !request.xhr?
52 }
55 }
53 format.atom {
56 format.atom {
54 @messages = @board.messages.find :all, :order => 'created_on DESC',
57 @messages = @board.messages.
55 :include => [:author, :board],
58 reorder('created_on DESC').
56 :limit => Setting.feeds_limit.to_i
59 includes(:author, :board).
60 limit(Setting.feeds_limit.to_i).
61 all
57 render_feed(@messages, :title => "#{@project}: #{@board}")
62 render_feed(@messages, :title => "#{@project}: #{@board}")
58 }
63 }
59 end
64 end
60 end
65 end
61
66
62 def new
67 def new
63 @board = @project.boards.build
68 @board = @project.boards.build
64 @board.safe_attributes = params[:board]
69 @board.safe_attributes = params[:board]
65 end
70 end
66
71
67 def create
72 def create
68 @board = @project.boards.build
73 @board = @project.boards.build
69 @board.safe_attributes = params[:board]
74 @board.safe_attributes = params[:board]
70 if @board.save
75 if @board.save
71 flash[:notice] = l(:notice_successful_create)
76 flash[:notice] = l(:notice_successful_create)
72 redirect_to_settings_in_projects
77 redirect_to_settings_in_projects
73 else
78 else
74 render :action => 'new'
79 render :action => 'new'
75 end
80 end
76 end
81 end
77
82
78 def edit
83 def edit
79 end
84 end
80
85
81 def update
86 def update
82 @board.safe_attributes = params[:board]
87 @board.safe_attributes = params[:board]
83 if @board.save
88 if @board.save
84 redirect_to_settings_in_projects
89 redirect_to_settings_in_projects
85 else
90 else
86 render :action => 'edit'
91 render :action => 'edit'
87 end
92 end
88 end
93 end
89
94
90 def destroy
95 def destroy
91 @board.destroy
96 @board.destroy
92 redirect_to_settings_in_projects
97 redirect_to_settings_in_projects
93 end
98 end
94
99
95 private
100 private
96 def redirect_to_settings_in_projects
101 def redirect_to_settings_in_projects
97 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
102 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
98 end
103 end
99
104
100 def find_board_if_available
105 def find_board_if_available
101 @board = @project.boards.find(params[:id]) if params[:id]
106 @board = @project.boards.find(params[:id]) if params[:id]
102 rescue ActiveRecord::RecordNotFound
107 rescue ActiveRecord::RecordNotFound
103 render_404
108 render_404
104 end
109 end
105 end
110 end
@@ -1,94 +1,94
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class DocumentsController < ApplicationController
18 class DocumentsController < ApplicationController
19 default_search_scope :documents
19 default_search_scope :documents
20 model_object Document
20 model_object Document
21 before_filter :find_project_by_project_id, :only => [:index, :new, :create]
21 before_filter :find_project_by_project_id, :only => [:index, :new, :create]
22 before_filter :find_model_object, :except => [:index, :new, :create]
22 before_filter :find_model_object, :except => [:index, :new, :create]
23 before_filter :find_project_from_association, :except => [:index, :new, :create]
23 before_filter :find_project_from_association, :except => [:index, :new, :create]
24 before_filter :authorize
24 before_filter :authorize
25
25
26 helper :attachments
26 helper :attachments
27
27
28 def index
28 def index
29 @sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category'
29 @sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category'
30 documents = @project.documents.find :all, :include => [:attachments, :category]
30 documents = @project.documents.includes(:attachments, :category).all
31 case @sort_by
31 case @sort_by
32 when 'date'
32 when 'date'
33 @grouped = documents.group_by {|d| d.updated_on.to_date }
33 @grouped = documents.group_by {|d| d.updated_on.to_date }
34 when 'title'
34 when 'title'
35 @grouped = documents.group_by {|d| d.title.first.upcase}
35 @grouped = documents.group_by {|d| d.title.first.upcase}
36 when 'author'
36 when 'author'
37 @grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author}
37 @grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author}
38 else
38 else
39 @grouped = documents.group_by(&:category)
39 @grouped = documents.group_by(&:category)
40 end
40 end
41 @document = @project.documents.build
41 @document = @project.documents.build
42 render :layout => false if request.xhr?
42 render :layout => false if request.xhr?
43 end
43 end
44
44
45 def show
45 def show
46 @attachments = @document.attachments.all
46 @attachments = @document.attachments.all
47 end
47 end
48
48
49 def new
49 def new
50 @document = @project.documents.build
50 @document = @project.documents.build
51 @document.safe_attributes = params[:document]
51 @document.safe_attributes = params[:document]
52 end
52 end
53
53
54 def create
54 def create
55 @document = @project.documents.build
55 @document = @project.documents.build
56 @document.safe_attributes = params[:document]
56 @document.safe_attributes = params[:document]
57 @document.save_attachments(params[:attachments])
57 @document.save_attachments(params[:attachments])
58 if @document.save
58 if @document.save
59 render_attachment_warning_if_needed(@document)
59 render_attachment_warning_if_needed(@document)
60 flash[:notice] = l(:notice_successful_create)
60 flash[:notice] = l(:notice_successful_create)
61 redirect_to :action => 'index', :project_id => @project
61 redirect_to :action => 'index', :project_id => @project
62 else
62 else
63 render :action => 'new'
63 render :action => 'new'
64 end
64 end
65 end
65 end
66
66
67 def edit
67 def edit
68 end
68 end
69
69
70 def update
70 def update
71 @document.safe_attributes = params[:document]
71 @document.safe_attributes = params[:document]
72 if request.put? and @document.save
72 if request.put? and @document.save
73 flash[:notice] = l(:notice_successful_update)
73 flash[:notice] = l(:notice_successful_update)
74 redirect_to :action => 'show', :id => @document
74 redirect_to :action => 'show', :id => @document
75 else
75 else
76 render :action => 'edit'
76 render :action => 'edit'
77 end
77 end
78 end
78 end
79
79
80 def destroy
80 def destroy
81 @document.destroy if request.delete?
81 @document.destroy if request.delete?
82 redirect_to :controller => 'documents', :action => 'index', :project_id => @project
82 redirect_to :controller => 'documents', :action => 'index', :project_id => @project
83 end
83 end
84
84
85 def add_attachment
85 def add_attachment
86 attachments = Attachment.attach_files(@document, params[:attachments])
86 attachments = Attachment.attach_files(@document, params[:attachments])
87 render_attachment_warning_if_needed(@document)
87 render_attachment_warning_if_needed(@document)
88
88
89 if attachments.present? && attachments[:files].present? && Setting.notified_events.include?('document_added')
89 if attachments.present? && attachments[:files].present? && Setting.notified_events.include?('document_added')
90 Mailer.attachments_added(attachments[:files]).deliver
90 Mailer.attachments_added(attachments[:files]).deliver
91 end
91 end
92 redirect_to :action => 'show', :id => @document
92 redirect_to :action => 'show', :id => @document
93 end
93 end
94 end
94 end
@@ -1,101 +1,101
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class TrackersController < ApplicationController
18 class TrackersController < ApplicationController
19 layout 'admin'
19 layout 'admin'
20
20
21 before_filter :require_admin, :except => :index
21 before_filter :require_admin, :except => :index
22 before_filter :require_admin_or_api_request, :only => :index
22 before_filter :require_admin_or_api_request, :only => :index
23 accept_api_auth :index
23 accept_api_auth :index
24
24
25 def index
25 def index
26 respond_to do |format|
26 respond_to do |format|
27 format.html {
27 format.html {
28 @tracker_pages, @trackers = paginate :trackers, :per_page => 10, :order => 'position'
28 @tracker_pages, @trackers = paginate :trackers, :per_page => 10, :order => 'position'
29 render :action => "index", :layout => false if request.xhr?
29 render :action => "index", :layout => false if request.xhr?
30 }
30 }
31 format.api {
31 format.api {
32 @trackers = Tracker.sorted.all
32 @trackers = Tracker.sorted.all
33 }
33 }
34 end
34 end
35 end
35 end
36
36
37 def new
37 def new
38 @tracker ||= Tracker.new(params[:tracker])
38 @tracker ||= Tracker.new(params[:tracker])
39 @trackers = Tracker.find :all, :order => 'position'
39 @trackers = Tracker.sorted.all
40 @projects = Project.all
40 @projects = Project.all
41 end
41 end
42
42
43 def create
43 def create
44 @tracker = Tracker.new(params[:tracker])
44 @tracker = Tracker.new(params[:tracker])
45 if request.post? and @tracker.save
45 if request.post? and @tracker.save
46 # workflow copy
46 # workflow copy
47 if !params[:copy_workflow_from].blank? && (copy_from = Tracker.find_by_id(params[:copy_workflow_from]))
47 if !params[:copy_workflow_from].blank? && (copy_from = Tracker.find_by_id(params[:copy_workflow_from]))
48 @tracker.workflow_rules.copy(copy_from)
48 @tracker.workflow_rules.copy(copy_from)
49 end
49 end
50 flash[:notice] = l(:notice_successful_create)
50 flash[:notice] = l(:notice_successful_create)
51 redirect_to :action => 'index'
51 redirect_to :action => 'index'
52 return
52 return
53 end
53 end
54 new
54 new
55 render :action => 'new'
55 render :action => 'new'
56 end
56 end
57
57
58 def edit
58 def edit
59 @tracker ||= Tracker.find(params[:id])
59 @tracker ||= Tracker.find(params[:id])
60 @projects = Project.all
60 @projects = Project.all
61 end
61 end
62
62
63 def update
63 def update
64 @tracker = Tracker.find(params[:id])
64 @tracker = Tracker.find(params[:id])
65 if request.put? and @tracker.update_attributes(params[:tracker])
65 if request.put? and @tracker.update_attributes(params[:tracker])
66 flash[:notice] = l(:notice_successful_update)
66 flash[:notice] = l(:notice_successful_update)
67 redirect_to :action => 'index'
67 redirect_to :action => 'index'
68 return
68 return
69 end
69 end
70 edit
70 edit
71 render :action => 'edit'
71 render :action => 'edit'
72 end
72 end
73
73
74 def destroy
74 def destroy
75 @tracker = Tracker.find(params[:id])
75 @tracker = Tracker.find(params[:id])
76 unless @tracker.issues.empty?
76 unless @tracker.issues.empty?
77 flash[:error] = l(:error_can_not_delete_tracker)
77 flash[:error] = l(:error_can_not_delete_tracker)
78 else
78 else
79 @tracker.destroy
79 @tracker.destroy
80 end
80 end
81 redirect_to :action => 'index'
81 redirect_to :action => 'index'
82 end
82 end
83
83
84 def fields
84 def fields
85 if request.post? && params[:trackers]
85 if request.post? && params[:trackers]
86 params[:trackers].each do |tracker_id, tracker_params|
86 params[:trackers].each do |tracker_id, tracker_params|
87 tracker = Tracker.find_by_id(tracker_id)
87 tracker = Tracker.find_by_id(tracker_id)
88 if tracker
88 if tracker
89 tracker.core_fields = tracker_params[:core_fields]
89 tracker.core_fields = tracker_params[:core_fields]
90 tracker.custom_field_ids = tracker_params[:custom_field_ids]
90 tracker.custom_field_ids = tracker_params[:custom_field_ids]
91 tracker.save
91 tracker.save
92 end
92 end
93 end
93 end
94 flash[:notice] = l(:notice_successful_update)
94 flash[:notice] = l(:notice_successful_update)
95 redirect_to :action => 'fields'
95 redirect_to :action => 'fields'
96 return
96 return
97 end
97 end
98 @trackers = Tracker.sorted.all
98 @trackers = Tracker.sorted.all
99 @custom_fields = IssueCustomField.all.sort
99 @custom_fields = IssueCustomField.all.sort
100 end
100 end
101 end
101 end
@@ -1,214 +1,211
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class UsersController < ApplicationController
18 class UsersController < ApplicationController
19 layout 'admin'
19 layout 'admin'
20
20
21 before_filter :require_admin, :except => :show
21 before_filter :require_admin, :except => :show
22 before_filter :find_user, :only => [:show, :edit, :update, :destroy, :edit_membership, :destroy_membership]
22 before_filter :find_user, :only => [:show, :edit, :update, :destroy, :edit_membership, :destroy_membership]
23 accept_api_auth :index, :show, :create, :update, :destroy
23 accept_api_auth :index, :show, :create, :update, :destroy
24
24
25 helper :sort
25 helper :sort
26 include SortHelper
26 include SortHelper
27 helper :custom_fields
27 helper :custom_fields
28 include CustomFieldsHelper
28 include CustomFieldsHelper
29
29
30 def index
30 def index
31 sort_init 'login', 'asc'
31 sort_init 'login', 'asc'
32 sort_update %w(login firstname lastname mail admin created_on last_login_on)
32 sort_update %w(login firstname lastname mail admin created_on last_login_on)
33
33
34 case params[:format]
34 case params[:format]
35 when 'xml', 'json'
35 when 'xml', 'json'
36 @offset, @limit = api_offset_and_limit
36 @offset, @limit = api_offset_and_limit
37 else
37 else
38 @limit = per_page_option
38 @limit = per_page_option
39 end
39 end
40
40
41 @status = params[:status] || 1
41 @status = params[:status] || 1
42
42
43 scope = User.logged.status(@status)
43 scope = User.logged.status(@status)
44 scope = scope.like(params[:name]) if params[:name].present?
44 scope = scope.like(params[:name]) if params[:name].present?
45 scope = scope.in_group(params[:group_id]) if params[:group_id].present?
45 scope = scope.in_group(params[:group_id]) if params[:group_id].present?
46
46
47 @user_count = scope.count
47 @user_count = scope.count
48 @user_pages = Paginator.new self, @user_count, @limit, params['page']
48 @user_pages = Paginator.new self, @user_count, @limit, params['page']
49 @offset ||= @user_pages.current.offset
49 @offset ||= @user_pages.current.offset
50 @users = scope.find :all,
50 @users = scope.order(sort_clause).limit(@limit).offset(@offset).all
51 :order => sort_clause,
52 :limit => @limit,
53 :offset => @offset
54
51
55 respond_to do |format|
52 respond_to do |format|
56 format.html {
53 format.html {
57 @groups = Group.all.sort
54 @groups = Group.all.sort
58 render :layout => !request.xhr?
55 render :layout => !request.xhr?
59 }
56 }
60 format.api
57 format.api
61 end
58 end
62 end
59 end
63
60
64 def show
61 def show
65 # show projects based on current user visibility
62 # show projects based on current user visibility
66 @memberships = @user.memberships.all(:conditions => Project.visible_condition(User.current))
63 @memberships = @user.memberships.all(:conditions => Project.visible_condition(User.current))
67
64
68 events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
65 events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
69 @events_by_day = events.group_by(&:event_date)
66 @events_by_day = events.group_by(&:event_date)
70
67
71 unless User.current.admin?
68 unless User.current.admin?
72 if !@user.active? || (@user != User.current && @memberships.empty? && events.empty?)
69 if !@user.active? || (@user != User.current && @memberships.empty? && events.empty?)
73 render_404
70 render_404
74 return
71 return
75 end
72 end
76 end
73 end
77
74
78 respond_to do |format|
75 respond_to do |format|
79 format.html { render :layout => 'base' }
76 format.html { render :layout => 'base' }
80 format.api
77 format.api
81 end
78 end
82 end
79 end
83
80
84 def new
81 def new
85 @user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
82 @user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
86 @auth_sources = AuthSource.all
83 @auth_sources = AuthSource.all
87 end
84 end
88
85
89 def create
86 def create
90 @user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
87 @user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
91 @user.safe_attributes = params[:user]
88 @user.safe_attributes = params[:user]
92 @user.admin = params[:user][:admin] || false
89 @user.admin = params[:user][:admin] || false
93 @user.login = params[:user][:login]
90 @user.login = params[:user][:login]
94 @user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation] unless @user.auth_source_id
91 @user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation] unless @user.auth_source_id
95
92
96 if @user.save
93 if @user.save
97 @user.pref.attributes = params[:pref]
94 @user.pref.attributes = params[:pref]
98 @user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
95 @user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
99 @user.pref.save
96 @user.pref.save
100 @user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
97 @user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
101
98
102 Mailer.account_information(@user, params[:user][:password]).deliver if params[:send_information]
99 Mailer.account_information(@user, params[:user][:password]).deliver if params[:send_information]
103
100
104 respond_to do |format|
101 respond_to do |format|
105 format.html {
102 format.html {
106 flash[:notice] = l(:notice_user_successful_create, :id => view_context.link_to(@user.login, user_path(@user)))
103 flash[:notice] = l(:notice_user_successful_create, :id => view_context.link_to(@user.login, user_path(@user)))
107 redirect_to(params[:continue] ?
104 redirect_to(params[:continue] ?
108 {:controller => 'users', :action => 'new'} :
105 {:controller => 'users', :action => 'new'} :
109 {:controller => 'users', :action => 'edit', :id => @user}
106 {:controller => 'users', :action => 'edit', :id => @user}
110 )
107 )
111 }
108 }
112 format.api { render :action => 'show', :status => :created, :location => user_url(@user) }
109 format.api { render :action => 'show', :status => :created, :location => user_url(@user) }
113 end
110 end
114 else
111 else
115 @auth_sources = AuthSource.all
112 @auth_sources = AuthSource.all
116 # Clear password input
113 # Clear password input
117 @user.password = @user.password_confirmation = nil
114 @user.password = @user.password_confirmation = nil
118
115
119 respond_to do |format|
116 respond_to do |format|
120 format.html { render :action => 'new' }
117 format.html { render :action => 'new' }
121 format.api { render_validation_errors(@user) }
118 format.api { render_validation_errors(@user) }
122 end
119 end
123 end
120 end
124 end
121 end
125
122
126 def edit
123 def edit
127 @auth_sources = AuthSource.all
124 @auth_sources = AuthSource.all
128 @membership ||= Member.new
125 @membership ||= Member.new
129 end
126 end
130
127
131 def update
128 def update
132 @user.admin = params[:user][:admin] if params[:user][:admin]
129 @user.admin = params[:user][:admin] if params[:user][:admin]
133 @user.login = params[:user][:login] if params[:user][:login]
130 @user.login = params[:user][:login] if params[:user][:login]
134 if params[:user][:password].present? && (@user.auth_source_id.nil? || params[:user][:auth_source_id].blank?)
131 if params[:user][:password].present? && (@user.auth_source_id.nil? || params[:user][:auth_source_id].blank?)
135 @user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation]
132 @user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation]
136 end
133 end
137 @user.safe_attributes = params[:user]
134 @user.safe_attributes = params[:user]
138 # Was the account actived ? (do it before User#save clears the change)
135 # Was the account actived ? (do it before User#save clears the change)
139 was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE])
136 was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE])
140 # TODO: Similar to My#account
137 # TODO: Similar to My#account
141 @user.pref.attributes = params[:pref]
138 @user.pref.attributes = params[:pref]
142 @user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
139 @user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
143
140
144 if @user.save
141 if @user.save
145 @user.pref.save
142 @user.pref.save
146 @user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
143 @user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
147
144
148 if was_activated
145 if was_activated
149 Mailer.account_activated(@user).deliver
146 Mailer.account_activated(@user).deliver
150 elsif @user.active? && params[:send_information] && !params[:user][:password].blank? && @user.auth_source_id.nil?
147 elsif @user.active? && params[:send_information] && !params[:user][:password].blank? && @user.auth_source_id.nil?
151 Mailer.account_information(@user, params[:user][:password]).deliver
148 Mailer.account_information(@user, params[:user][:password]).deliver
152 end
149 end
153
150
154 respond_to do |format|
151 respond_to do |format|
155 format.html {
152 format.html {
156 flash[:notice] = l(:notice_successful_update)
153 flash[:notice] = l(:notice_successful_update)
157 redirect_to_referer_or edit_user_path(@user)
154 redirect_to_referer_or edit_user_path(@user)
158 }
155 }
159 format.api { render_api_ok }
156 format.api { render_api_ok }
160 end
157 end
161 else
158 else
162 @auth_sources = AuthSource.all
159 @auth_sources = AuthSource.all
163 @membership ||= Member.new
160 @membership ||= Member.new
164 # Clear password input
161 # Clear password input
165 @user.password = @user.password_confirmation = nil
162 @user.password = @user.password_confirmation = nil
166
163
167 respond_to do |format|
164 respond_to do |format|
168 format.html { render :action => :edit }
165 format.html { render :action => :edit }
169 format.api { render_validation_errors(@user) }
166 format.api { render_validation_errors(@user) }
170 end
167 end
171 end
168 end
172 end
169 end
173
170
174 def destroy
171 def destroy
175 @user.destroy
172 @user.destroy
176 respond_to do |format|
173 respond_to do |format|
177 format.html { redirect_back_or_default(users_url) }
174 format.html { redirect_back_or_default(users_url) }
178 format.api { render_api_ok }
175 format.api { render_api_ok }
179 end
176 end
180 end
177 end
181
178
182 def edit_membership
179 def edit_membership
183 @membership = Member.edit_membership(params[:membership_id], params[:membership], @user)
180 @membership = Member.edit_membership(params[:membership_id], params[:membership], @user)
184 @membership.save
181 @membership.save
185 respond_to do |format|
182 respond_to do |format|
186 format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
183 format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
187 format.js
184 format.js
188 end
185 end
189 end
186 end
190
187
191 def destroy_membership
188 def destroy_membership
192 @membership = Member.find(params[:membership_id])
189 @membership = Member.find(params[:membership_id])
193 if @membership.deletable?
190 if @membership.deletable?
194 @membership.destroy
191 @membership.destroy
195 end
192 end
196 respond_to do |format|
193 respond_to do |format|
197 format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
194 format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
198 format.js
195 format.js
199 end
196 end
200 end
197 end
201
198
202 private
199 private
203
200
204 def find_user
201 def find_user
205 if params[:id] == 'current'
202 if params[:id] == 'current'
206 require_login || return
203 require_login || return
207 @user = User.current
204 @user = User.current
208 else
205 else
209 @user = User.find(params[:id])
206 @user = User.find(params[:id])
210 end
207 end
211 rescue ActiveRecord::RecordNotFound
208 rescue ActiveRecord::RecordNotFound
212 render_404
209 render_404
213 end
210 end
214 end
211 end
@@ -1,354 +1,355
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'diff'
18 require 'diff'
19
19
20 # The WikiController follows the Rails REST controller pattern but with
20 # The WikiController follows the Rails REST controller pattern but with
21 # a few differences
21 # a few differences
22 #
22 #
23 # * index - shows a list of WikiPages grouped by page or date
23 # * index - shows a list of WikiPages grouped by page or date
24 # * new - not used
24 # * new - not used
25 # * create - not used
25 # * create - not used
26 # * show - will also show the form for creating a new wiki page
26 # * show - will also show the form for creating a new wiki page
27 # * edit - used to edit an existing or new page
27 # * edit - used to edit an existing or new page
28 # * update - used to save a wiki page update to the database, including new pages
28 # * update - used to save a wiki page update to the database, including new pages
29 # * destroy - normal
29 # * destroy - normal
30 #
30 #
31 # Other member and collection methods are also used
31 # Other member and collection methods are also used
32 #
32 #
33 # TODO: still being worked on
33 # TODO: still being worked on
34 class WikiController < ApplicationController
34 class WikiController < ApplicationController
35 default_search_scope :wiki_pages
35 default_search_scope :wiki_pages
36 before_filter :find_wiki, :authorize
36 before_filter :find_wiki, :authorize
37 before_filter :find_existing_or_new_page, :only => [:show, :edit, :update]
37 before_filter :find_existing_or_new_page, :only => [:show, :edit, :update]
38 before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy, :destroy_version]
38 before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy, :destroy_version]
39 accept_api_auth :index, :show, :update, :destroy
39 accept_api_auth :index, :show, :update, :destroy
40
40
41 helper :attachments
41 helper :attachments
42 include AttachmentsHelper
42 include AttachmentsHelper
43 helper :watchers
43 helper :watchers
44 include Redmine::Export::PDF
44 include Redmine::Export::PDF
45
45
46 # List of pages, sorted alphabetically and by parent (hierarchy)
46 # List of pages, sorted alphabetically and by parent (hierarchy)
47 def index
47 def index
48 load_pages_for_index
48 load_pages_for_index
49
49
50 respond_to do |format|
50 respond_to do |format|
51 format.html {
51 format.html {
52 @pages_by_parent_id = @pages.group_by(&:parent_id)
52 @pages_by_parent_id = @pages.group_by(&:parent_id)
53 }
53 }
54 format.api
54 format.api
55 end
55 end
56 end
56 end
57
57
58 # List of page, by last update
58 # List of page, by last update
59 def date_index
59 def date_index
60 load_pages_for_index
60 load_pages_for_index
61 @pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
61 @pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
62 end
62 end
63
63
64 # display a page (in editing mode if it doesn't exist)
64 # display a page (in editing mode if it doesn't exist)
65 def show
65 def show
66 if @page.new_record?
66 if @page.new_record?
67 if User.current.allowed_to?(:edit_wiki_pages, @project) && editable? && !api_request?
67 if User.current.allowed_to?(:edit_wiki_pages, @project) && editable? && !api_request?
68 edit
68 edit
69 render :action => 'edit'
69 render :action => 'edit'
70 else
70 else
71 render_404
71 render_404
72 end
72 end
73 return
73 return
74 end
74 end
75 if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
75 if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
76 deny_access
76 deny_access
77 return
77 return
78 end
78 end
79 @content = @page.content_for_version(params[:version])
79 @content = @page.content_for_version(params[:version])
80 if User.current.allowed_to?(:export_wiki_pages, @project)
80 if User.current.allowed_to?(:export_wiki_pages, @project)
81 if params[:format] == 'pdf'
81 if params[:format] == 'pdf'
82 send_data(wiki_page_to_pdf(@page, @project), :type => 'application/pdf', :filename => "#{@page.title}.pdf")
82 send_data(wiki_page_to_pdf(@page, @project), :type => 'application/pdf', :filename => "#{@page.title}.pdf")
83 return
83 return
84 elsif params[:format] == 'html'
84 elsif params[:format] == 'html'
85 export = render_to_string :action => 'export', :layout => false
85 export = render_to_string :action => 'export', :layout => false
86 send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
86 send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
87 return
87 return
88 elsif params[:format] == 'txt'
88 elsif params[:format] == 'txt'
89 send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
89 send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
90 return
90 return
91 end
91 end
92 end
92 end
93 @editable = editable?
93 @editable = editable?
94 @sections_editable = @editable && User.current.allowed_to?(:edit_wiki_pages, @page.project) &&
94 @sections_editable = @editable && User.current.allowed_to?(:edit_wiki_pages, @page.project) &&
95 @content.current_version? &&
95 @content.current_version? &&
96 Redmine::WikiFormatting.supports_section_edit?
96 Redmine::WikiFormatting.supports_section_edit?
97
97
98 respond_to do |format|
98 respond_to do |format|
99 format.html
99 format.html
100 format.api
100 format.api
101 end
101 end
102 end
102 end
103
103
104 # edit an existing page or a new one
104 # edit an existing page or a new one
105 def edit
105 def edit
106 return render_403 unless editable?
106 return render_403 unless editable?
107 if @page.new_record?
107 if @page.new_record?
108 @page.content = WikiContent.new(:page => @page)
108 @page.content = WikiContent.new(:page => @page)
109 if params[:parent].present?
109 if params[:parent].present?
110 @page.parent = @page.wiki.find_page(params[:parent].to_s)
110 @page.parent = @page.wiki.find_page(params[:parent].to_s)
111 end
111 end
112 end
112 end
113
113
114 @content = @page.content_for_version(params[:version])
114 @content = @page.content_for_version(params[:version])
115 @content.text = initial_page_content(@page) if @content.text.blank?
115 @content.text = initial_page_content(@page) if @content.text.blank?
116 # don't keep previous comment
116 # don't keep previous comment
117 @content.comments = nil
117 @content.comments = nil
118
118
119 # To prevent StaleObjectError exception when reverting to a previous version
119 # To prevent StaleObjectError exception when reverting to a previous version
120 @content.version = @page.content.version
120 @content.version = @page.content.version
121
121
122 @text = @content.text
122 @text = @content.text
123 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
123 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
124 @section = params[:section].to_i
124 @section = params[:section].to_i
125 @text, @section_hash = Redmine::WikiFormatting.formatter.new(@text).get_section(@section)
125 @text, @section_hash = Redmine::WikiFormatting.formatter.new(@text).get_section(@section)
126 render_404 if @text.blank?
126 render_404 if @text.blank?
127 end
127 end
128 end
128 end
129
129
130 # Creates a new page or updates an existing one
130 # Creates a new page or updates an existing one
131 def update
131 def update
132 return render_403 unless editable?
132 return render_403 unless editable?
133 was_new_page = @page.new_record?
133 was_new_page = @page.new_record?
134 @page.content = WikiContent.new(:page => @page) if @page.new_record?
134 @page.content = WikiContent.new(:page => @page) if @page.new_record?
135 @page.safe_attributes = params[:wiki_page]
135 @page.safe_attributes = params[:wiki_page]
136
136
137 @content = @page.content
137 @content = @page.content
138 content_params = params[:content]
138 content_params = params[:content]
139 if content_params.nil? && params[:wiki_page].is_a?(Hash)
139 if content_params.nil? && params[:wiki_page].is_a?(Hash)
140 content_params = params[:wiki_page].slice(:text, :comments, :version)
140 content_params = params[:wiki_page].slice(:text, :comments, :version)
141 end
141 end
142 content_params ||= {}
142 content_params ||= {}
143
143
144 @content.comments = content_params[:comments]
144 @content.comments = content_params[:comments]
145 @text = content_params[:text]
145 @text = content_params[:text]
146 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
146 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
147 @section = params[:section].to_i
147 @section = params[:section].to_i
148 @section_hash = params[:section_hash]
148 @section_hash = params[:section_hash]
149 @content.text = Redmine::WikiFormatting.formatter.new(@content.text).update_section(params[:section].to_i, @text, @section_hash)
149 @content.text = Redmine::WikiFormatting.formatter.new(@content.text).update_section(params[:section].to_i, @text, @section_hash)
150 else
150 else
151 @content.version = content_params[:version] if content_params[:version]
151 @content.version = content_params[:version] if content_params[:version]
152 @content.text = @text
152 @content.text = @text
153 end
153 end
154 @content.author = User.current
154 @content.author = User.current
155
155
156 if @page.save_with_content
156 if @page.save_with_content
157 attachments = Attachment.attach_files(@page, params[:attachments])
157 attachments = Attachment.attach_files(@page, params[:attachments])
158 render_attachment_warning_if_needed(@page)
158 render_attachment_warning_if_needed(@page)
159 call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
159 call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
160
160
161 respond_to do |format|
161 respond_to do |format|
162 format.html { redirect_to :action => 'show', :project_id => @project, :id => @page.title }
162 format.html { redirect_to :action => 'show', :project_id => @project, :id => @page.title }
163 format.api {
163 format.api {
164 if was_new_page
164 if was_new_page
165 render :action => 'show', :status => :created, :location => url_for(:controller => 'wiki', :action => 'show', :project_id => @project, :id => @page.title)
165 render :action => 'show', :status => :created, :location => url_for(:controller => 'wiki', :action => 'show', :project_id => @project, :id => @page.title)
166 else
166 else
167 render_api_ok
167 render_api_ok
168 end
168 end
169 }
169 }
170 end
170 end
171 else
171 else
172 respond_to do |format|
172 respond_to do |format|
173 format.html { render :action => 'edit' }
173 format.html { render :action => 'edit' }
174 format.api { render_validation_errors(@content) }
174 format.api { render_validation_errors(@content) }
175 end
175 end
176 end
176 end
177
177
178 rescue ActiveRecord::StaleObjectError, Redmine::WikiFormatting::StaleSectionError
178 rescue ActiveRecord::StaleObjectError, Redmine::WikiFormatting::StaleSectionError
179 # Optimistic locking exception
179 # Optimistic locking exception
180 respond_to do |format|
180 respond_to do |format|
181 format.html {
181 format.html {
182 flash.now[:error] = l(:notice_locking_conflict)
182 flash.now[:error] = l(:notice_locking_conflict)
183 render :action => 'edit'
183 render :action => 'edit'
184 }
184 }
185 format.api { render_api_head :conflict }
185 format.api { render_api_head :conflict }
186 end
186 end
187 rescue ActiveRecord::RecordNotSaved
187 rescue ActiveRecord::RecordNotSaved
188 respond_to do |format|
188 respond_to do |format|
189 format.html { render :action => 'edit' }
189 format.html { render :action => 'edit' }
190 format.api { render_validation_errors(@content) }
190 format.api { render_validation_errors(@content) }
191 end
191 end
192 end
192 end
193
193
194 # rename a page
194 # rename a page
195 def rename
195 def rename
196 return render_403 unless editable?
196 return render_403 unless editable?
197 @page.redirect_existing_links = true
197 @page.redirect_existing_links = true
198 # used to display the *original* title if some AR validation errors occur
198 # used to display the *original* title if some AR validation errors occur
199 @original_title = @page.pretty_title
199 @original_title = @page.pretty_title
200 if request.post? && @page.update_attributes(params[:wiki_page])
200 if request.post? && @page.update_attributes(params[:wiki_page])
201 flash[:notice] = l(:notice_successful_update)
201 flash[:notice] = l(:notice_successful_update)
202 redirect_to :action => 'show', :project_id => @project, :id => @page.title
202 redirect_to :action => 'show', :project_id => @project, :id => @page.title
203 end
203 end
204 end
204 end
205
205
206 def protect
206 def protect
207 @page.update_attribute :protected, params[:protected]
207 @page.update_attribute :protected, params[:protected]
208 redirect_to :action => 'show', :project_id => @project, :id => @page.title
208 redirect_to :action => 'show', :project_id => @project, :id => @page.title
209 end
209 end
210
210
211 # show page history
211 # show page history
212 def history
212 def history
213 @version_count = @page.content.versions.count
213 @version_count = @page.content.versions.count
214 @version_pages = Paginator.new self, @version_count, per_page_option, params['page']
214 @version_pages = Paginator.new self, @version_count, per_page_option, params['page']
215 # don't load text
215 # don't load text
216 @versions = @page.content.versions.find :all,
216 @versions = @page.content.versions.
217 :select => "id, author_id, comments, updated_on, version",
217 select("id, author_id, comments, updated_on, version").
218 :order => 'version DESC',
218 reorder('version DESC').
219 :limit => @version_pages.items_per_page + 1,
219 limit(@version_pages.items_per_page + 1).
220 :offset => @version_pages.current.offset
220 offset(@version_pages.current.offset).
221 all
221
222
222 render :layout => false if request.xhr?
223 render :layout => false if request.xhr?
223 end
224 end
224
225
225 def diff
226 def diff
226 @diff = @page.diff(params[:version], params[:version_from])
227 @diff = @page.diff(params[:version], params[:version_from])
227 render_404 unless @diff
228 render_404 unless @diff
228 end
229 end
229
230
230 def annotate
231 def annotate
231 @annotate = @page.annotate(params[:version])
232 @annotate = @page.annotate(params[:version])
232 render_404 unless @annotate
233 render_404 unless @annotate
233 end
234 end
234
235
235 # Removes a wiki page and its history
236 # Removes a wiki page and its history
236 # Children can be either set as root pages, removed or reassigned to another parent page
237 # Children can be either set as root pages, removed or reassigned to another parent page
237 def destroy
238 def destroy
238 return render_403 unless editable?
239 return render_403 unless editable?
239
240
240 @descendants_count = @page.descendants.size
241 @descendants_count = @page.descendants.size
241 if @descendants_count > 0
242 if @descendants_count > 0
242 case params[:todo]
243 case params[:todo]
243 when 'nullify'
244 when 'nullify'
244 # Nothing to do
245 # Nothing to do
245 when 'destroy'
246 when 'destroy'
246 # Removes all its descendants
247 # Removes all its descendants
247 @page.descendants.each(&:destroy)
248 @page.descendants.each(&:destroy)
248 when 'reassign'
249 when 'reassign'
249 # Reassign children to another parent page
250 # Reassign children to another parent page
250 reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
251 reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
251 return unless reassign_to
252 return unless reassign_to
252 @page.children.each do |child|
253 @page.children.each do |child|
253 child.update_attribute(:parent, reassign_to)
254 child.update_attribute(:parent, reassign_to)
254 end
255 end
255 else
256 else
256 @reassignable_to = @wiki.pages - @page.self_and_descendants
257 @reassignable_to = @wiki.pages - @page.self_and_descendants
257 # display the destroy form if it's a user request
258 # display the destroy form if it's a user request
258 return unless api_request?
259 return unless api_request?
259 end
260 end
260 end
261 end
261 @page.destroy
262 @page.destroy
262 respond_to do |format|
263 respond_to do |format|
263 format.html { redirect_to :action => 'index', :project_id => @project }
264 format.html { redirect_to :action => 'index', :project_id => @project }
264 format.api { render_api_ok }
265 format.api { render_api_ok }
265 end
266 end
266 end
267 end
267
268
268 def destroy_version
269 def destroy_version
269 return render_403 unless editable?
270 return render_403 unless editable?
270
271
271 @content = @page.content_for_version(params[:version])
272 @content = @page.content_for_version(params[:version])
272 @content.destroy
273 @content.destroy
273 redirect_to_referer_or :action => 'history', :id => @page.title, :project_id => @project
274 redirect_to_referer_or :action => 'history', :id => @page.title, :project_id => @project
274 end
275 end
275
276
276 # Export wiki to a single pdf or html file
277 # Export wiki to a single pdf or html file
277 def export
278 def export
278 @pages = @wiki.pages.all(:order => 'title', :include => [:content, {:attachments => :author}])
279 @pages = @wiki.pages.all(:order => 'title', :include => [:content, {:attachments => :author}])
279 respond_to do |format|
280 respond_to do |format|
280 format.html {
281 format.html {
281 export = render_to_string :action => 'export_multiple', :layout => false
282 export = render_to_string :action => 'export_multiple', :layout => false
282 send_data(export, :type => 'text/html', :filename => "wiki.html")
283 send_data(export, :type => 'text/html', :filename => "wiki.html")
283 }
284 }
284 format.pdf {
285 format.pdf {
285 send_data(wiki_pages_to_pdf(@pages, @project), :type => 'application/pdf', :filename => "#{@project.identifier}.pdf")
286 send_data(wiki_pages_to_pdf(@pages, @project), :type => 'application/pdf', :filename => "#{@project.identifier}.pdf")
286 }
287 }
287 end
288 end
288 end
289 end
289
290
290 def preview
291 def preview
291 page = @wiki.find_page(params[:id])
292 page = @wiki.find_page(params[:id])
292 # page is nil when previewing a new page
293 # page is nil when previewing a new page
293 return render_403 unless page.nil? || editable?(page)
294 return render_403 unless page.nil? || editable?(page)
294 if page
295 if page
295 @attachements = page.attachments
296 @attachements = page.attachments
296 @previewed = page.content
297 @previewed = page.content
297 end
298 end
298 @text = params[:content][:text]
299 @text = params[:content][:text]
299 render :partial => 'common/preview'
300 render :partial => 'common/preview'
300 end
301 end
301
302
302 def add_attachment
303 def add_attachment
303 return render_403 unless editable?
304 return render_403 unless editable?
304 attachments = Attachment.attach_files(@page, params[:attachments])
305 attachments = Attachment.attach_files(@page, params[:attachments])
305 render_attachment_warning_if_needed(@page)
306 render_attachment_warning_if_needed(@page)
306 redirect_to :action => 'show', :id => @page.title, :project_id => @project
307 redirect_to :action => 'show', :id => @page.title, :project_id => @project
307 end
308 end
308
309
309 private
310 private
310
311
311 def find_wiki
312 def find_wiki
312 @project = Project.find(params[:project_id])
313 @project = Project.find(params[:project_id])
313 @wiki = @project.wiki
314 @wiki = @project.wiki
314 render_404 unless @wiki
315 render_404 unless @wiki
315 rescue ActiveRecord::RecordNotFound
316 rescue ActiveRecord::RecordNotFound
316 render_404
317 render_404
317 end
318 end
318
319
319 # Finds the requested page or a new page if it doesn't exist
320 # Finds the requested page or a new page if it doesn't exist
320 def find_existing_or_new_page
321 def find_existing_or_new_page
321 @page = @wiki.find_or_new_page(params[:id])
322 @page = @wiki.find_or_new_page(params[:id])
322 if @wiki.page_found_with_redirect?
323 if @wiki.page_found_with_redirect?
323 redirect_to params.update(:id => @page.title)
324 redirect_to params.update(:id => @page.title)
324 end
325 end
325 end
326 end
326
327
327 # Finds the requested page and returns a 404 error if it doesn't exist
328 # Finds the requested page and returns a 404 error if it doesn't exist
328 def find_existing_page
329 def find_existing_page
329 @page = @wiki.find_page(params[:id])
330 @page = @wiki.find_page(params[:id])
330 if @page.nil?
331 if @page.nil?
331 render_404
332 render_404
332 return
333 return
333 end
334 end
334 if @wiki.page_found_with_redirect?
335 if @wiki.page_found_with_redirect?
335 redirect_to params.update(:id => @page.title)
336 redirect_to params.update(:id => @page.title)
336 end
337 end
337 end
338 end
338
339
339 # Returns true if the current user is allowed to edit the page, otherwise false
340 # Returns true if the current user is allowed to edit the page, otherwise false
340 def editable?(page = @page)
341 def editable?(page = @page)
341 page.editable_by?(User.current)
342 page.editable_by?(User.current)
342 end
343 end
343
344
344 # Returns the default content of a new wiki page
345 # Returns the default content of a new wiki page
345 def initial_page_content(page)
346 def initial_page_content(page)
346 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
347 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
347 extend helper unless self.instance_of?(helper)
348 extend helper unless self.instance_of?(helper)
348 helper.instance_method(:initial_page_content).bind(self).call(page)
349 helper.instance_method(:initial_page_content).bind(self).call(page)
349 end
350 end
350
351
351 def load_pages_for_index
352 def load_pages_for_index
352 @pages = @wiki.pages.with_updated_on.order("#{WikiPage.table_name}.title").includes(:wiki => :project).includes(:parent).all
353 @pages = @wiki.pages.with_updated_on.order("#{WikiPage.table_name}.title").includes(:wiki => :project).includes(:parent).all
353 end
354 end
354 end
355 end
@@ -1,1079 +1,1086
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class QueryColumn
18 class QueryColumn
19 attr_accessor :name, :sortable, :groupable, :default_order
19 attr_accessor :name, :sortable, :groupable, :default_order
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 def initialize(name, options={})
22 def initialize(name, options={})
23 self.name = name
23 self.name = name
24 self.sortable = options[:sortable]
24 self.sortable = options[:sortable]
25 self.groupable = options[:groupable] || false
25 self.groupable = options[:groupable] || false
26 if groupable == true
26 if groupable == true
27 self.groupable = name.to_s
27 self.groupable = name.to_s
28 end
28 end
29 self.default_order = options[:default_order]
29 self.default_order = options[:default_order]
30 @caption_key = options[:caption] || "field_#{name}"
30 @caption_key = options[:caption] || "field_#{name}"
31 end
31 end
32
32
33 def caption
33 def caption
34 l(@caption_key)
34 l(@caption_key)
35 end
35 end
36
36
37 # Returns true if the column is sortable, otherwise false
37 # Returns true if the column is sortable, otherwise false
38 def sortable?
38 def sortable?
39 !@sortable.nil?
39 !@sortable.nil?
40 end
40 end
41
41
42 def sortable
42 def sortable
43 @sortable.is_a?(Proc) ? @sortable.call : @sortable
43 @sortable.is_a?(Proc) ? @sortable.call : @sortable
44 end
44 end
45
45
46 def value(issue)
46 def value(issue)
47 issue.send name
47 issue.send name
48 end
48 end
49
49
50 def css_classes
50 def css_classes
51 name
51 name
52 end
52 end
53 end
53 end
54
54
55 class QueryCustomFieldColumn < QueryColumn
55 class QueryCustomFieldColumn < QueryColumn
56
56
57 def initialize(custom_field)
57 def initialize(custom_field)
58 self.name = "cf_#{custom_field.id}".to_sym
58 self.name = "cf_#{custom_field.id}".to_sym
59 self.sortable = custom_field.order_statement || false
59 self.sortable = custom_field.order_statement || false
60 self.groupable = custom_field.group_statement || false
60 self.groupable = custom_field.group_statement || false
61 @cf = custom_field
61 @cf = custom_field
62 end
62 end
63
63
64 def caption
64 def caption
65 @cf.name
65 @cf.name
66 end
66 end
67
67
68 def custom_field
68 def custom_field
69 @cf
69 @cf
70 end
70 end
71
71
72 def value(issue)
72 def value(issue)
73 cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
73 cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
74 cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
74 cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
75 end
75 end
76
76
77 def css_classes
77 def css_classes
78 @css_classes ||= "#{name} #{@cf.field_format}"
78 @css_classes ||= "#{name} #{@cf.field_format}"
79 end
79 end
80 end
80 end
81
81
82 class Query < ActiveRecord::Base
82 class Query < ActiveRecord::Base
83 class StatementInvalid < ::ActiveRecord::StatementInvalid
83 class StatementInvalid < ::ActiveRecord::StatementInvalid
84 end
84 end
85
85
86 belongs_to :project
86 belongs_to :project
87 belongs_to :user
87 belongs_to :user
88 serialize :filters
88 serialize :filters
89 serialize :column_names
89 serialize :column_names
90 serialize :sort_criteria, Array
90 serialize :sort_criteria, Array
91
91
92 attr_protected :project_id, :user_id
92 attr_protected :project_id, :user_id
93
93
94 validates_presence_of :name
94 validates_presence_of :name
95 validates_length_of :name, :maximum => 255
95 validates_length_of :name, :maximum => 255
96 validate :validate_query_filters
96 validate :validate_query_filters
97
97
98 @@operators = { "=" => :label_equals,
98 @@operators = { "=" => :label_equals,
99 "!" => :label_not_equals,
99 "!" => :label_not_equals,
100 "o" => :label_open_issues,
100 "o" => :label_open_issues,
101 "c" => :label_closed_issues,
101 "c" => :label_closed_issues,
102 "!*" => :label_none,
102 "!*" => :label_none,
103 "*" => :label_any,
103 "*" => :label_any,
104 ">=" => :label_greater_or_equal,
104 ">=" => :label_greater_or_equal,
105 "<=" => :label_less_or_equal,
105 "<=" => :label_less_or_equal,
106 "><" => :label_between,
106 "><" => :label_between,
107 "<t+" => :label_in_less_than,
107 "<t+" => :label_in_less_than,
108 ">t+" => :label_in_more_than,
108 ">t+" => :label_in_more_than,
109 "><t+"=> :label_in_the_next_days,
109 "><t+"=> :label_in_the_next_days,
110 "t+" => :label_in,
110 "t+" => :label_in,
111 "t" => :label_today,
111 "t" => :label_today,
112 "w" => :label_this_week,
112 "w" => :label_this_week,
113 ">t-" => :label_less_than_ago,
113 ">t-" => :label_less_than_ago,
114 "<t-" => :label_more_than_ago,
114 "<t-" => :label_more_than_ago,
115 "><t-"=> :label_in_the_past_days,
115 "><t-"=> :label_in_the_past_days,
116 "t-" => :label_ago,
116 "t-" => :label_ago,
117 "~" => :label_contains,
117 "~" => :label_contains,
118 "!~" => :label_not_contains,
118 "!~" => :label_not_contains,
119 "=p" => :label_any_issues_in_project,
119 "=p" => :label_any_issues_in_project,
120 "=!p" => :label_any_issues_not_in_project,
120 "=!p" => :label_any_issues_not_in_project,
121 "!p" => :label_no_issues_in_project}
121 "!p" => :label_no_issues_in_project}
122
122
123 cattr_reader :operators
123 cattr_reader :operators
124
124
125 @@operators_by_filter_type = { :list => [ "=", "!" ],
125 @@operators_by_filter_type = { :list => [ "=", "!" ],
126 :list_status => [ "o", "=", "!", "c", "*" ],
126 :list_status => [ "o", "=", "!", "c", "*" ],
127 :list_optional => [ "=", "!", "!*", "*" ],
127 :list_optional => [ "=", "!", "!*", "*" ],
128 :list_subprojects => [ "*", "!*", "=" ],
128 :list_subprojects => [ "*", "!*", "=" ],
129 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "w", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
129 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "w", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
130 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "w", "!*", "*" ],
130 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "w", "!*", "*" ],
131 :string => [ "=", "~", "!", "!~", "!*", "*" ],
131 :string => [ "=", "~", "!", "!~", "!*", "*" ],
132 :text => [ "~", "!~", "!*", "*" ],
132 :text => [ "~", "!~", "!*", "*" ],
133 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
133 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
134 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
134 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
135 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]}
135 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]}
136
136
137 cattr_reader :operators_by_filter_type
137 cattr_reader :operators_by_filter_type
138
138
139 @@available_columns = [
139 @@available_columns = [
140 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
140 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
141 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
141 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
142 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
142 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
143 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
143 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
144 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
144 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
145 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
145 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
146 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
146 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
147 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
147 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
148 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
148 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
149 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
149 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
150 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
150 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
151 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
151 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
152 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
152 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
153 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
153 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
154 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
154 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
155 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
155 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
156 QueryColumn.new(:relations, :caption => :label_related_issues)
156 QueryColumn.new(:relations, :caption => :label_related_issues)
157 ]
157 ]
158 cattr_reader :available_columns
158 cattr_reader :available_columns
159
159
160 scope :visible, lambda {|*args|
160 scope :visible, lambda {|*args|
161 user = args.shift || User.current
161 user = args.shift || User.current
162 base = Project.allowed_to_condition(user, :view_issues, *args)
162 base = Project.allowed_to_condition(user, :view_issues, *args)
163 user_id = user.logged? ? user.id : 0
163 user_id = user.logged? ? user.id : 0
164 {
164 {
165 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
165 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
166 :include => :project
166 :include => :project
167 }
167 }
168 }
168 }
169
169
170 def initialize(attributes=nil, *args)
170 def initialize(attributes=nil, *args)
171 super attributes
171 super attributes
172 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
172 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
173 @is_for_all = project.nil?
173 @is_for_all = project.nil?
174 end
174 end
175
175
176 def validate_query_filters
176 def validate_query_filters
177 filters.each_key do |field|
177 filters.each_key do |field|
178 if values_for(field)
178 if values_for(field)
179 case type_for(field)
179 case type_for(field)
180 when :integer
180 when :integer
181 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
181 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
182 when :float
182 when :float
183 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
183 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
184 when :date, :date_past
184 when :date, :date_past
185 case operator_for(field)
185 case operator_for(field)
186 when "=", ">=", "<=", "><"
186 when "=", ">=", "<=", "><"
187 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
187 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
188 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
188 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
189 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
189 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
190 end
190 end
191 end
191 end
192 end
192 end
193
193
194 add_filter_error(field, :blank) unless
194 add_filter_error(field, :blank) unless
195 # filter requires one or more values
195 # filter requires one or more values
196 (values_for(field) and !values_for(field).first.blank?) or
196 (values_for(field) and !values_for(field).first.blank?) or
197 # filter doesn't require any value
197 # filter doesn't require any value
198 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
198 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
199 end if filters
199 end if filters
200 end
200 end
201
201
202 def add_filter_error(field, message)
202 def add_filter_error(field, message)
203 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
203 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
204 errors.add(:base, m)
204 errors.add(:base, m)
205 end
205 end
206
206
207 # Returns true if the query is visible to +user+ or the current user.
207 # Returns true if the query is visible to +user+ or the current user.
208 def visible?(user=User.current)
208 def visible?(user=User.current)
209 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
209 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
210 end
210 end
211
211
212 def editable_by?(user)
212 def editable_by?(user)
213 return false unless user
213 return false unless user
214 # Admin can edit them all and regular users can edit their private queries
214 # Admin can edit them all and regular users can edit their private queries
215 return true if user.admin? || (!is_public && self.user_id == user.id)
215 return true if user.admin? || (!is_public && self.user_id == user.id)
216 # Members can not edit public queries that are for all project (only admin is allowed to)
216 # Members can not edit public queries that are for all project (only admin is allowed to)
217 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
217 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
218 end
218 end
219
219
220 def trackers
220 def trackers
221 @trackers ||= project.nil? ? Tracker.sorted.all : project.rolled_up_trackers
221 @trackers ||= project.nil? ? Tracker.sorted.all : project.rolled_up_trackers
222 end
222 end
223
223
224 # Returns a hash of localized labels for all filter operators
224 # Returns a hash of localized labels for all filter operators
225 def self.operators_labels
225 def self.operators_labels
226 operators.inject({}) {|h, operator| h[operator.first] = l(operator.last); h}
226 operators.inject({}) {|h, operator| h[operator.first] = l(operator.last); h}
227 end
227 end
228
228
229 def available_filters
229 def available_filters
230 return @available_filters if @available_filters
230 return @available_filters if @available_filters
231 @available_filters = {
231 @available_filters = {
232 "status_id" => {
232 "status_id" => {
233 :type => :list_status, :order => 0,
233 :type => :list_status, :order => 0,
234 :values => IssueStatus.sorted.all.collect{|s| [s.name, s.id.to_s] }
234 :values => IssueStatus.sorted.all.collect{|s| [s.name, s.id.to_s] }
235 },
235 },
236 "tracker_id" => {
236 "tracker_id" => {
237 :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] }
237 :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] }
238 },
238 },
239 "priority_id" => {
239 "priority_id" => {
240 :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
240 :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
241 },
241 },
242 "subject" => { :type => :text, :order => 8 },
242 "subject" => { :type => :text, :order => 8 },
243 "created_on" => { :type => :date_past, :order => 9 },
243 "created_on" => { :type => :date_past, :order => 9 },
244 "updated_on" => { :type => :date_past, :order => 10 },
244 "updated_on" => { :type => :date_past, :order => 10 },
245 "start_date" => { :type => :date, :order => 11 },
245 "start_date" => { :type => :date, :order => 11 },
246 "due_date" => { :type => :date, :order => 12 },
246 "due_date" => { :type => :date, :order => 12 },
247 "estimated_hours" => { :type => :float, :order => 13 },
247 "estimated_hours" => { :type => :float, :order => 13 },
248 "done_ratio" => { :type => :integer, :order => 14 }
248 "done_ratio" => { :type => :integer, :order => 14 }
249 }
249 }
250 IssueRelation::TYPES.each do |relation_type, options|
250 IssueRelation::TYPES.each do |relation_type, options|
251 @available_filters[relation_type] = {
251 @available_filters[relation_type] = {
252 :type => :relation, :order => @available_filters.size + 100,
252 :type => :relation, :order => @available_filters.size + 100,
253 :label => options[:name]
253 :label => options[:name]
254 }
254 }
255 end
255 end
256 principals = []
256 principals = []
257 if project
257 if project
258 principals += project.principals.sort
258 principals += project.principals.sort
259 unless project.leaf?
259 unless project.leaf?
260 subprojects = project.descendants.visible.all
260 subprojects = project.descendants.visible.all
261 if subprojects.any?
261 if subprojects.any?
262 @available_filters["subproject_id"] = {
262 @available_filters["subproject_id"] = {
263 :type => :list_subprojects, :order => 13,
263 :type => :list_subprojects, :order => 13,
264 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
264 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
265 }
265 }
266 principals += Principal.member_of(subprojects)
266 principals += Principal.member_of(subprojects)
267 end
267 end
268 end
268 end
269 else
269 else
270 if all_projects.any?
270 if all_projects.any?
271 # members of visible projects
271 # members of visible projects
272 principals += Principal.member_of(all_projects)
272 principals += Principal.member_of(all_projects)
273 # project filter
273 # project filter
274 project_values = []
274 project_values = []
275 if User.current.logged? && User.current.memberships.any?
275 if User.current.logged? && User.current.memberships.any?
276 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
276 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
277 end
277 end
278 project_values += all_projects_values
278 project_values += all_projects_values
279 @available_filters["project_id"] = {
279 @available_filters["project_id"] = {
280 :type => :list, :order => 1, :values => project_values
280 :type => :list, :order => 1, :values => project_values
281 } unless project_values.empty?
281 } unless project_values.empty?
282 end
282 end
283 end
283 end
284 principals.uniq!
284 principals.uniq!
285 principals.sort!
285 principals.sort!
286 users = principals.select {|p| p.is_a?(User)}
286 users = principals.select {|p| p.is_a?(User)}
287
287
288 assigned_to_values = []
288 assigned_to_values = []
289 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
289 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
290 assigned_to_values += (Setting.issue_group_assignment? ?
290 assigned_to_values += (Setting.issue_group_assignment? ?
291 principals : users).collect{|s| [s.name, s.id.to_s] }
291 principals : users).collect{|s| [s.name, s.id.to_s] }
292 @available_filters["assigned_to_id"] = {
292 @available_filters["assigned_to_id"] = {
293 :type => :list_optional, :order => 4, :values => assigned_to_values
293 :type => :list_optional, :order => 4, :values => assigned_to_values
294 } unless assigned_to_values.empty?
294 } unless assigned_to_values.empty?
295
295
296 author_values = []
296 author_values = []
297 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
297 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
298 author_values += users.collect{|s| [s.name, s.id.to_s] }
298 author_values += users.collect{|s| [s.name, s.id.to_s] }
299 @available_filters["author_id"] = {
299 @available_filters["author_id"] = {
300 :type => :list, :order => 5, :values => author_values
300 :type => :list, :order => 5, :values => author_values
301 } unless author_values.empty?
301 } unless author_values.empty?
302
302
303 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
303 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
304 @available_filters["member_of_group"] = {
304 @available_filters["member_of_group"] = {
305 :type => :list_optional, :order => 6, :values => group_values
305 :type => :list_optional, :order => 6, :values => group_values
306 } unless group_values.empty?
306 } unless group_values.empty?
307
307
308 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
308 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
309 @available_filters["assigned_to_role"] = {
309 @available_filters["assigned_to_role"] = {
310 :type => :list_optional, :order => 7, :values => role_values
310 :type => :list_optional, :order => 7, :values => role_values
311 } unless role_values.empty?
311 } unless role_values.empty?
312
312
313 if User.current.logged?
313 if User.current.logged?
314 @available_filters["watcher_id"] = {
314 @available_filters["watcher_id"] = {
315 :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]]
315 :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]]
316 }
316 }
317 end
317 end
318
318
319 if project
319 if project
320 # project specific filters
320 # project specific filters
321 categories = project.issue_categories.all
321 categories = project.issue_categories.all
322 unless categories.empty?
322 unless categories.empty?
323 @available_filters["category_id"] = {
323 @available_filters["category_id"] = {
324 :type => :list_optional, :order => 6,
324 :type => :list_optional, :order => 6,
325 :values => categories.collect{|s| [s.name, s.id.to_s] }
325 :values => categories.collect{|s| [s.name, s.id.to_s] }
326 }
326 }
327 end
327 end
328 versions = project.shared_versions.all
328 versions = project.shared_versions.all
329 unless versions.empty?
329 unless versions.empty?
330 @available_filters["fixed_version_id"] = {
330 @available_filters["fixed_version_id"] = {
331 :type => :list_optional, :order => 7,
331 :type => :list_optional, :order => 7,
332 :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] }
332 :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] }
333 }
333 }
334 end
334 end
335 add_custom_fields_filters(project.all_issue_custom_fields)
335 add_custom_fields_filters(project.all_issue_custom_fields)
336 else
336 else
337 # global filters for cross project issue list
337 # global filters for cross project issue list
338 system_shared_versions = Version.visible.find_all_by_sharing('system')
338 system_shared_versions = Version.visible.find_all_by_sharing('system')
339 unless system_shared_versions.empty?
339 unless system_shared_versions.empty?
340 @available_filters["fixed_version_id"] = {
340 @available_filters["fixed_version_id"] = {
341 :type => :list_optional, :order => 7,
341 :type => :list_optional, :order => 7,
342 :values => system_shared_versions.sort.collect{|s|
342 :values => system_shared_versions.sort.collect{|s|
343 ["#{s.project.name} - #{s.name}", s.id.to_s]
343 ["#{s.project.name} - #{s.name}", s.id.to_s]
344 }
344 }
345 }
345 }
346 end
346 end
347 add_custom_fields_filters(IssueCustomField.where(:is_filter => true, :is_for_all => true).all)
347 add_custom_fields_filters(IssueCustomField.where(:is_filter => true, :is_for_all => true).all)
348 end
348 end
349 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
349 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
350 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
350 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
351 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
351 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
352 @available_filters["is_private"] = {
352 @available_filters["is_private"] = {
353 :type => :list, :order => 16,
353 :type => :list, :order => 16,
354 :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
354 :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
355 }
355 }
356 end
356 end
357 Tracker.disabled_core_fields(trackers).each {|field|
357 Tracker.disabled_core_fields(trackers).each {|field|
358 @available_filters.delete field
358 @available_filters.delete field
359 }
359 }
360 @available_filters.each do |field, options|
360 @available_filters.each do |field, options|
361 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
361 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
362 end
362 end
363 @available_filters
363 @available_filters
364 end
364 end
365
365
366 # Returns a representation of the available filters for JSON serialization
366 # Returns a representation of the available filters for JSON serialization
367 def available_filters_as_json
367 def available_filters_as_json
368 json = {}
368 json = {}
369 available_filters.each do |field, options|
369 available_filters.each do |field, options|
370 json[field] = options.slice(:type, :name, :values).stringify_keys
370 json[field] = options.slice(:type, :name, :values).stringify_keys
371 end
371 end
372 json
372 json
373 end
373 end
374
374
375 def all_projects
375 def all_projects
376 @all_projects ||= Project.visible.all
376 @all_projects ||= Project.visible.all
377 end
377 end
378
378
379 def all_projects_values
379 def all_projects_values
380 return @all_projects_values if @all_projects_values
380 return @all_projects_values if @all_projects_values
381
381
382 values = []
382 values = []
383 Project.project_tree(all_projects) do |p, level|
383 Project.project_tree(all_projects) do |p, level|
384 prefix = (level > 0 ? ('--' * level + ' ') : '')
384 prefix = (level > 0 ? ('--' * level + ' ') : '')
385 values << ["#{prefix}#{p.name}", p.id.to_s]
385 values << ["#{prefix}#{p.name}", p.id.to_s]
386 end
386 end
387 @all_projects_values = values
387 @all_projects_values = values
388 end
388 end
389
389
390 def add_filter(field, operator, values)
390 def add_filter(field, operator, values)
391 # values must be an array
391 # values must be an array
392 return unless values.nil? || values.is_a?(Array)
392 return unless values.nil? || values.is_a?(Array)
393 # check if field is defined as an available filter
393 # check if field is defined as an available filter
394 if available_filters.has_key? field
394 if available_filters.has_key? field
395 filter_options = available_filters[field]
395 filter_options = available_filters[field]
396 # check if operator is allowed for that filter
396 # check if operator is allowed for that filter
397 #if @@operators_by_filter_type[filter_options[:type]].include? operator
397 #if @@operators_by_filter_type[filter_options[:type]].include? operator
398 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
398 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
399 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
399 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
400 #end
400 #end
401 filters[field] = {:operator => operator, :values => (values || [''])}
401 filters[field] = {:operator => operator, :values => (values || [''])}
402 end
402 end
403 end
403 end
404
404
405 def add_short_filter(field, expression)
405 def add_short_filter(field, expression)
406 return unless expression && available_filters.has_key?(field)
406 return unless expression && available_filters.has_key?(field)
407 field_type = available_filters[field][:type]
407 field_type = available_filters[field][:type]
408 @@operators_by_filter_type[field_type].sort.reverse.detect do |operator|
408 @@operators_by_filter_type[field_type].sort.reverse.detect do |operator|
409 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
409 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
410 add_filter field, operator, $1.present? ? $1.split('|') : ['']
410 add_filter field, operator, $1.present? ? $1.split('|') : ['']
411 end || add_filter(field, '=', expression.split('|'))
411 end || add_filter(field, '=', expression.split('|'))
412 end
412 end
413
413
414 # Add multiple filters using +add_filter+
414 # Add multiple filters using +add_filter+
415 def add_filters(fields, operators, values)
415 def add_filters(fields, operators, values)
416 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
416 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
417 fields.each do |field|
417 fields.each do |field|
418 add_filter(field, operators[field], values && values[field])
418 add_filter(field, operators[field], values && values[field])
419 end
419 end
420 end
420 end
421 end
421 end
422
422
423 def has_filter?(field)
423 def has_filter?(field)
424 filters and filters[field]
424 filters and filters[field]
425 end
425 end
426
426
427 def type_for(field)
427 def type_for(field)
428 available_filters[field][:type] if available_filters.has_key?(field)
428 available_filters[field][:type] if available_filters.has_key?(field)
429 end
429 end
430
430
431 def operator_for(field)
431 def operator_for(field)
432 has_filter?(field) ? filters[field][:operator] : nil
432 has_filter?(field) ? filters[field][:operator] : nil
433 end
433 end
434
434
435 def values_for(field)
435 def values_for(field)
436 has_filter?(field) ? filters[field][:values] : nil
436 has_filter?(field) ? filters[field][:values] : nil
437 end
437 end
438
438
439 def value_for(field, index=0)
439 def value_for(field, index=0)
440 (values_for(field) || [])[index]
440 (values_for(field) || [])[index]
441 end
441 end
442
442
443 def label_for(field)
443 def label_for(field)
444 label = available_filters[field][:name] if available_filters.has_key?(field)
444 label = available_filters[field][:name] if available_filters.has_key?(field)
445 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
445 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
446 end
446 end
447
447
448 def available_columns
448 def available_columns
449 return @available_columns if @available_columns
449 return @available_columns if @available_columns
450 @available_columns = ::Query.available_columns.dup
450 @available_columns = ::Query.available_columns.dup
451 @available_columns += (project ?
451 @available_columns += (project ?
452 project.all_issue_custom_fields :
452 project.all_issue_custom_fields :
453 IssueCustomField.all
453 IssueCustomField.all
454 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
454 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
455
455
456 if User.current.allowed_to?(:view_time_entries, project, :global => true)
456 if User.current.allowed_to?(:view_time_entries, project, :global => true)
457 index = nil
457 index = nil
458 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
458 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
459 index = (index ? index + 1 : -1)
459 index = (index ? index + 1 : -1)
460 # insert the column after estimated_hours or at the end
460 # insert the column after estimated_hours or at the end
461 @available_columns.insert index, QueryColumn.new(:spent_hours,
461 @available_columns.insert index, QueryColumn.new(:spent_hours,
462 :sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)",
462 :sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)",
463 :default_order => 'desc',
463 :default_order => 'desc',
464 :caption => :label_spent_time
464 :caption => :label_spent_time
465 )
465 )
466 end
466 end
467
467
468 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
468 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
469 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
469 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
470 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
470 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
471 end
471 end
472
472
473 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
473 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
474 @available_columns.reject! {|column|
474 @available_columns.reject! {|column|
475 disabled_fields.include?(column.name.to_s)
475 disabled_fields.include?(column.name.to_s)
476 }
476 }
477
477
478 @available_columns
478 @available_columns
479 end
479 end
480
480
481 def self.available_columns=(v)
481 def self.available_columns=(v)
482 self.available_columns = (v)
482 self.available_columns = (v)
483 end
483 end
484
484
485 def self.add_available_column(column)
485 def self.add_available_column(column)
486 self.available_columns << (column) if column.is_a?(QueryColumn)
486 self.available_columns << (column) if column.is_a?(QueryColumn)
487 end
487 end
488
488
489 # Returns an array of columns that can be used to group the results
489 # Returns an array of columns that can be used to group the results
490 def groupable_columns
490 def groupable_columns
491 available_columns.select {|c| c.groupable}
491 available_columns.select {|c| c.groupable}
492 end
492 end
493
493
494 # Returns a Hash of columns and the key for sorting
494 # Returns a Hash of columns and the key for sorting
495 def sortable_columns
495 def sortable_columns
496 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
496 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
497 h[column.name.to_s] = column.sortable
497 h[column.name.to_s] = column.sortable
498 h
498 h
499 })
499 })
500 end
500 end
501
501
502 def columns
502 def columns
503 # preserve the column_names order
503 # preserve the column_names order
504 (has_default_columns? ? default_columns_names : column_names).collect do |name|
504 (has_default_columns? ? default_columns_names : column_names).collect do |name|
505 available_columns.find { |col| col.name == name }
505 available_columns.find { |col| col.name == name }
506 end.compact
506 end.compact
507 end
507 end
508
508
509 def default_columns_names
509 def default_columns_names
510 @default_columns_names ||= begin
510 @default_columns_names ||= begin
511 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
511 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
512
512
513 project.present? ? default_columns : [:project] | default_columns
513 project.present? ? default_columns : [:project] | default_columns
514 end
514 end
515 end
515 end
516
516
517 def column_names=(names)
517 def column_names=(names)
518 if names
518 if names
519 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
519 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
520 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
520 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
521 # Set column_names to nil if default columns
521 # Set column_names to nil if default columns
522 if names == default_columns_names
522 if names == default_columns_names
523 names = nil
523 names = nil
524 end
524 end
525 end
525 end
526 write_attribute(:column_names, names)
526 write_attribute(:column_names, names)
527 end
527 end
528
528
529 def has_column?(column)
529 def has_column?(column)
530 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
530 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
531 end
531 end
532
532
533 def has_default_columns?
533 def has_default_columns?
534 column_names.nil? || column_names.empty?
534 column_names.nil? || column_names.empty?
535 end
535 end
536
536
537 def sort_criteria=(arg)
537 def sort_criteria=(arg)
538 c = []
538 c = []
539 if arg.is_a?(Hash)
539 if arg.is_a?(Hash)
540 arg = arg.keys.sort.collect {|k| arg[k]}
540 arg = arg.keys.sort.collect {|k| arg[k]}
541 end
541 end
542 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
542 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
543 write_attribute(:sort_criteria, c)
543 write_attribute(:sort_criteria, c)
544 end
544 end
545
545
546 def sort_criteria
546 def sort_criteria
547 read_attribute(:sort_criteria) || []
547 read_attribute(:sort_criteria) || []
548 end
548 end
549
549
550 def sort_criteria_key(arg)
550 def sort_criteria_key(arg)
551 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
551 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
552 end
552 end
553
553
554 def sort_criteria_order(arg)
554 def sort_criteria_order(arg)
555 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
555 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
556 end
556 end
557
557
558 def sort_criteria_order_for(key)
558 def sort_criteria_order_for(key)
559 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
559 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
560 end
560 end
561
561
562 # Returns the SQL sort order that should be prepended for grouping
562 # Returns the SQL sort order that should be prepended for grouping
563 def group_by_sort_order
563 def group_by_sort_order
564 if grouped? && (column = group_by_column)
564 if grouped? && (column = group_by_column)
565 order = sort_criteria_order_for(column.name) || column.default_order
565 order = sort_criteria_order_for(column.name) || column.default_order
566 column.sortable.is_a?(Array) ?
566 column.sortable.is_a?(Array) ?
567 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
567 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
568 "#{column.sortable} #{order}"
568 "#{column.sortable} #{order}"
569 end
569 end
570 end
570 end
571
571
572 # Returns true if the query is a grouped query
572 # Returns true if the query is a grouped query
573 def grouped?
573 def grouped?
574 !group_by_column.nil?
574 !group_by_column.nil?
575 end
575 end
576
576
577 def group_by_column
577 def group_by_column
578 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
578 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
579 end
579 end
580
580
581 def group_by_statement
581 def group_by_statement
582 group_by_column.try(:groupable)
582 group_by_column.try(:groupable)
583 end
583 end
584
584
585 def project_statement
585 def project_statement
586 project_clauses = []
586 project_clauses = []
587 if project && !project.descendants.active.empty?
587 if project && !project.descendants.active.empty?
588 ids = [project.id]
588 ids = [project.id]
589 if has_filter?("subproject_id")
589 if has_filter?("subproject_id")
590 case operator_for("subproject_id")
590 case operator_for("subproject_id")
591 when '='
591 when '='
592 # include the selected subprojects
592 # include the selected subprojects
593 ids += values_for("subproject_id").each(&:to_i)
593 ids += values_for("subproject_id").each(&:to_i)
594 when '!*'
594 when '!*'
595 # main project only
595 # main project only
596 else
596 else
597 # all subprojects
597 # all subprojects
598 ids += project.descendants.collect(&:id)
598 ids += project.descendants.collect(&:id)
599 end
599 end
600 elsif Setting.display_subprojects_issues?
600 elsif Setting.display_subprojects_issues?
601 ids += project.descendants.collect(&:id)
601 ids += project.descendants.collect(&:id)
602 end
602 end
603 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
603 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
604 elsif project
604 elsif project
605 project_clauses << "#{Project.table_name}.id = %d" % project.id
605 project_clauses << "#{Project.table_name}.id = %d" % project.id
606 end
606 end
607 project_clauses.any? ? project_clauses.join(' AND ') : nil
607 project_clauses.any? ? project_clauses.join(' AND ') : nil
608 end
608 end
609
609
610 def statement
610 def statement
611 # filters clauses
611 # filters clauses
612 filters_clauses = []
612 filters_clauses = []
613 filters.each_key do |field|
613 filters.each_key do |field|
614 next if field == "subproject_id"
614 next if field == "subproject_id"
615 v = values_for(field).clone
615 v = values_for(field).clone
616 next unless v and !v.empty?
616 next unless v and !v.empty?
617 operator = operator_for(field)
617 operator = operator_for(field)
618
618
619 # "me" value subsitution
619 # "me" value subsitution
620 if %w(assigned_to_id author_id watcher_id).include?(field)
620 if %w(assigned_to_id author_id watcher_id).include?(field)
621 if v.delete("me")
621 if v.delete("me")
622 if User.current.logged?
622 if User.current.logged?
623 v.push(User.current.id.to_s)
623 v.push(User.current.id.to_s)
624 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
624 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
625 else
625 else
626 v.push("0")
626 v.push("0")
627 end
627 end
628 end
628 end
629 end
629 end
630
630
631 if field == 'project_id'
631 if field == 'project_id'
632 if v.delete('mine')
632 if v.delete('mine')
633 v += User.current.memberships.map(&:project_id).map(&:to_s)
633 v += User.current.memberships.map(&:project_id).map(&:to_s)
634 end
634 end
635 end
635 end
636
636
637 if field =~ /cf_(\d+)$/
637 if field =~ /cf_(\d+)$/
638 # custom field
638 # custom field
639 filters_clauses << sql_for_custom_field(field, operator, v, $1)
639 filters_clauses << sql_for_custom_field(field, operator, v, $1)
640 elsif respond_to?("sql_for_#{field}_field")
640 elsif respond_to?("sql_for_#{field}_field")
641 # specific statement
641 # specific statement
642 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
642 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
643 else
643 else
644 # regular field
644 # regular field
645 filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')'
645 filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')'
646 end
646 end
647 end if filters and valid?
647 end if filters and valid?
648
648
649 filters_clauses << project_statement
649 filters_clauses << project_statement
650 filters_clauses.reject!(&:blank?)
650 filters_clauses.reject!(&:blank?)
651
651
652 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
652 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
653 end
653 end
654
654
655 # Returns the issue count
655 # Returns the issue count
656 def issue_count
656 def issue_count
657 Issue.visible.count(:include => [:status, :project], :conditions => statement)
657 Issue.visible.count(:include => [:status, :project], :conditions => statement)
658 rescue ::ActiveRecord::StatementInvalid => e
658 rescue ::ActiveRecord::StatementInvalid => e
659 raise StatementInvalid.new(e.message)
659 raise StatementInvalid.new(e.message)
660 end
660 end
661
661
662 # Returns the issue count by group or nil if query is not grouped
662 # Returns the issue count by group or nil if query is not grouped
663 def issue_count_by_group
663 def issue_count_by_group
664 r = nil
664 r = nil
665 if grouped?
665 if grouped?
666 begin
666 begin
667 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
667 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
668 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
668 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
669 rescue ActiveRecord::RecordNotFound
669 rescue ActiveRecord::RecordNotFound
670 r = {nil => issue_count}
670 r = {nil => issue_count}
671 end
671 end
672 c = group_by_column
672 c = group_by_column
673 if c.is_a?(QueryCustomFieldColumn)
673 if c.is_a?(QueryCustomFieldColumn)
674 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
674 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
675 end
675 end
676 end
676 end
677 r
677 r
678 rescue ::ActiveRecord::StatementInvalid => e
678 rescue ::ActiveRecord::StatementInvalid => e
679 raise StatementInvalid.new(e.message)
679 raise StatementInvalid.new(e.message)
680 end
680 end
681
681
682 # Returns the issues
682 # Returns the issues
683 # Valid options are :order, :offset, :limit, :include, :conditions
683 # Valid options are :order, :offset, :limit, :include, :conditions
684 def issues(options={})
684 def issues(options={})
685 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
685 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
686 order_option = nil if order_option.blank?
686 order_option = nil if order_option.blank?
687
687
688 issues = Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
688 issues = Issue.visible.where(options[:conditions]).all(
689 :include => ([:status, :project] + (options[:include] || [])).uniq,
689 :conditions => statement,
690 :conditions => statement,
690 :order => order_option,
691 :order => order_option,
691 :joins => joins_for_order_statement(order_option),
692 :joins => joins_for_order_statement(order_option),
692 :limit => options[:limit],
693 :limit => options[:limit],
693 :offset => options[:offset]
694 :offset => options[:offset]
695 )
694
696
695 if has_column?(:spent_hours)
697 if has_column?(:spent_hours)
696 Issue.load_visible_spent_hours(issues)
698 Issue.load_visible_spent_hours(issues)
697 end
699 end
698 if has_column?(:relations)
700 if has_column?(:relations)
699 Issue.load_visible_relations(issues)
701 Issue.load_visible_relations(issues)
700 end
702 end
701 issues
703 issues
702 rescue ::ActiveRecord::StatementInvalid => e
704 rescue ::ActiveRecord::StatementInvalid => e
703 raise StatementInvalid.new(e.message)
705 raise StatementInvalid.new(e.message)
704 end
706 end
705
707
706 # Returns the issues ids
708 # Returns the issues ids
707 def issue_ids(options={})
709 def issue_ids(options={})
708 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
710 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
709 order_option = nil if order_option.blank?
711 order_option = nil if order_option.blank?
710
712
711 Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq,
713 Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq,
712 :conditions => statement,
714 :conditions => statement,
713 :order => order_option,
715 :order => order_option,
714 :joins => joins_for_order_statement(order_option),
716 :joins => joins_for_order_statement(order_option),
715 :limit => options[:limit],
717 :limit => options[:limit],
716 :offset => options[:offset]).find_ids
718 :offset => options[:offset]).find_ids
717 rescue ::ActiveRecord::StatementInvalid => e
719 rescue ::ActiveRecord::StatementInvalid => e
718 raise StatementInvalid.new(e.message)
720 raise StatementInvalid.new(e.message)
719 end
721 end
720
722
721 # Returns the journals
723 # Returns the journals
722 # Valid options are :order, :offset, :limit
724 # Valid options are :order, :offset, :limit
723 def journals(options={})
725 def journals(options={})
724 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
726 Journal.visible.all(
727 :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
725 :conditions => statement,
728 :conditions => statement,
726 :order => options[:order],
729 :order => options[:order],
727 :limit => options[:limit],
730 :limit => options[:limit],
728 :offset => options[:offset]
731 :offset => options[:offset]
732 )
729 rescue ::ActiveRecord::StatementInvalid => e
733 rescue ::ActiveRecord::StatementInvalid => e
730 raise StatementInvalid.new(e.message)
734 raise StatementInvalid.new(e.message)
731 end
735 end
732
736
733 # Returns the versions
737 # Returns the versions
734 # Valid options are :conditions
738 # Valid options are :conditions
735 def versions(options={})
739 def versions(options={})
736 Version.visible.scoped(:conditions => options[:conditions]).find :all, :include => :project, :conditions => project_statement
740 Version.visible.where(options[:conditions]).all(
741 :include => :project,
742 :conditions => project_statement
743 )
737 rescue ::ActiveRecord::StatementInvalid => e
744 rescue ::ActiveRecord::StatementInvalid => e
738 raise StatementInvalid.new(e.message)
745 raise StatementInvalid.new(e.message)
739 end
746 end
740
747
741 def sql_for_watcher_id_field(field, operator, value)
748 def sql_for_watcher_id_field(field, operator, value)
742 db_table = Watcher.table_name
749 db_table = Watcher.table_name
743 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
750 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
744 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
751 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
745 end
752 end
746
753
747 def sql_for_member_of_group_field(field, operator, value)
754 def sql_for_member_of_group_field(field, operator, value)
748 if operator == '*' # Any group
755 if operator == '*' # Any group
749 groups = Group.all
756 groups = Group.all
750 operator = '=' # Override the operator since we want to find by assigned_to
757 operator = '=' # Override the operator since we want to find by assigned_to
751 elsif operator == "!*"
758 elsif operator == "!*"
752 groups = Group.all
759 groups = Group.all
753 operator = '!' # Override the operator since we want to find by assigned_to
760 operator = '!' # Override the operator since we want to find by assigned_to
754 else
761 else
755 groups = Group.find_all_by_id(value)
762 groups = Group.find_all_by_id(value)
756 end
763 end
757 groups ||= []
764 groups ||= []
758
765
759 members_of_groups = groups.inject([]) {|user_ids, group|
766 members_of_groups = groups.inject([]) {|user_ids, group|
760 if group && group.user_ids.present?
767 if group && group.user_ids.present?
761 user_ids << group.user_ids
768 user_ids << group.user_ids
762 end
769 end
763 user_ids.flatten.uniq.compact
770 user_ids.flatten.uniq.compact
764 }.sort.collect(&:to_s)
771 }.sort.collect(&:to_s)
765
772
766 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
773 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
767 end
774 end
768
775
769 def sql_for_assigned_to_role_field(field, operator, value)
776 def sql_for_assigned_to_role_field(field, operator, value)
770 case operator
777 case operator
771 when "*", "!*" # Member / Not member
778 when "*", "!*" # Member / Not member
772 sw = operator == "!*" ? 'NOT' : ''
779 sw = operator == "!*" ? 'NOT' : ''
773 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
780 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
774 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
781 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
775 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
782 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
776 when "=", "!"
783 when "=", "!"
777 role_cond = value.any? ?
784 role_cond = value.any? ?
778 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
785 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
779 "1=0"
786 "1=0"
780
787
781 sw = operator == "!" ? 'NOT' : ''
788 sw = operator == "!" ? 'NOT' : ''
782 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
789 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
783 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
790 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
784 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
791 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
785 end
792 end
786 end
793 end
787
794
788 def sql_for_is_private_field(field, operator, value)
795 def sql_for_is_private_field(field, operator, value)
789 op = (operator == "=" ? 'IN' : 'NOT IN')
796 op = (operator == "=" ? 'IN' : 'NOT IN')
790 va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',')
797 va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',')
791
798
792 "#{Issue.table_name}.is_private #{op} (#{va})"
799 "#{Issue.table_name}.is_private #{op} (#{va})"
793 end
800 end
794
801
795 def sql_for_relations(field, operator, value, options={})
802 def sql_for_relations(field, operator, value, options={})
796 relation_options = IssueRelation::TYPES[field]
803 relation_options = IssueRelation::TYPES[field]
797 return relation_options unless relation_options
804 return relation_options unless relation_options
798
805
799 relation_type = field
806 relation_type = field
800 join_column, target_join_column = "issue_from_id", "issue_to_id"
807 join_column, target_join_column = "issue_from_id", "issue_to_id"
801 if relation_options[:reverse] || options[:reverse]
808 if relation_options[:reverse] || options[:reverse]
802 relation_type = relation_options[:reverse] || relation_type
809 relation_type = relation_options[:reverse] || relation_type
803 join_column, target_join_column = target_join_column, join_column
810 join_column, target_join_column = target_join_column, join_column
804 end
811 end
805
812
806 sql = case operator
813 sql = case operator
807 when "*", "!*"
814 when "*", "!*"
808 op = (operator == "*" ? 'IN' : 'NOT IN')
815 op = (operator == "*" ? 'IN' : 'NOT IN')
809 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}')"
816 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}')"
810 when "=", "!"
817 when "=", "!"
811 op = (operator == "=" ? 'IN' : 'NOT IN')
818 op = (operator == "=" ? 'IN' : 'NOT IN')
812 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
819 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
813 when "=p", "=!p", "!p"
820 when "=p", "=!p", "!p"
814 op = (operator == "!p" ? 'NOT IN' : 'IN')
821 op = (operator == "!p" ? 'NOT IN' : 'IN')
815 comp = (operator == "=!p" ? '<>' : '=')
822 comp = (operator == "=!p" ? '<>' : '=')
816 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})"
823 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})"
817 end
824 end
818
825
819 if relation_options[:sym] == field && !options[:reverse]
826 if relation_options[:sym] == field && !options[:reverse]
820 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
827 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
821 sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
828 sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
822 else
829 else
823 sql
830 sql
824 end
831 end
825 end
832 end
826
833
827 IssueRelation::TYPES.keys.each do |relation_type|
834 IssueRelation::TYPES.keys.each do |relation_type|
828 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
835 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
829 end
836 end
830
837
831 private
838 private
832
839
833 def sql_for_custom_field(field, operator, value, custom_field_id)
840 def sql_for_custom_field(field, operator, value, custom_field_id)
834 db_table = CustomValue.table_name
841 db_table = CustomValue.table_name
835 db_field = 'value'
842 db_field = 'value'
836 filter = @available_filters[field]
843 filter = @available_filters[field]
837 return nil unless filter
844 return nil unless filter
838 if filter[:format] == 'user'
845 if filter[:format] == 'user'
839 if value.delete('me')
846 if value.delete('me')
840 value.push User.current.id.to_s
847 value.push User.current.id.to_s
841 end
848 end
842 end
849 end
843 not_in = nil
850 not_in = nil
844 if operator == '!'
851 if operator == '!'
845 # Makes ! operator work for custom fields with multiple values
852 # Makes ! operator work for custom fields with multiple values
846 operator = '='
853 operator = '='
847 not_in = 'NOT'
854 not_in = 'NOT'
848 end
855 end
849 customized_key = "id"
856 customized_key = "id"
850 customized_class = Issue
857 customized_class = Issue
851 if field =~ /^(.+)\.cf_/
858 if field =~ /^(.+)\.cf_/
852 assoc = $1
859 assoc = $1
853 customized_key = "#{assoc}_id"
860 customized_key = "#{assoc}_id"
854 customized_class = Issue.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
861 customized_class = Issue.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
855 raise "Unknown Issue association #{assoc}" unless customized_class
862 raise "Unknown Issue association #{assoc}" unless customized_class
856 end
863 end
857 "#{Issue.table_name}.#{customized_key} #{not_in} IN (SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id} WHERE " +
864 "#{Issue.table_name}.#{customized_key} #{not_in} IN (SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id} WHERE " +
858 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
865 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
859 end
866 end
860
867
861 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
868 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
862 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
869 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
863 sql = ''
870 sql = ''
864 case operator
871 case operator
865 when "="
872 when "="
866 if value.any?
873 if value.any?
867 case type_for(field)
874 case type_for(field)
868 when :date, :date_past
875 when :date, :date_past
869 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
876 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
870 when :integer
877 when :integer
871 if is_custom_filter
878 if is_custom_filter
872 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
879 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
873 else
880 else
874 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
881 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
875 end
882 end
876 when :float
883 when :float
877 if is_custom_filter
884 if is_custom_filter
878 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
885 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
879 else
886 else
880 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
887 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
881 end
888 end
882 else
889 else
883 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
890 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
884 end
891 end
885 else
892 else
886 # IN an empty set
893 # IN an empty set
887 sql = "1=0"
894 sql = "1=0"
888 end
895 end
889 when "!"
896 when "!"
890 if value.any?
897 if value.any?
891 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
898 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
892 else
899 else
893 # NOT IN an empty set
900 # NOT IN an empty set
894 sql = "1=1"
901 sql = "1=1"
895 end
902 end
896 when "!*"
903 when "!*"
897 sql = "#{db_table}.#{db_field} IS NULL"
904 sql = "#{db_table}.#{db_field} IS NULL"
898 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
905 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
899 when "*"
906 when "*"
900 sql = "#{db_table}.#{db_field} IS NOT NULL"
907 sql = "#{db_table}.#{db_field} IS NOT NULL"
901 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
908 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
902 when ">="
909 when ">="
903 if [:date, :date_past].include?(type_for(field))
910 if [:date, :date_past].include?(type_for(field))
904 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
911 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
905 else
912 else
906 if is_custom_filter
913 if is_custom_filter
907 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
914 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
908 else
915 else
909 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
916 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
910 end
917 end
911 end
918 end
912 when "<="
919 when "<="
913 if [:date, :date_past].include?(type_for(field))
920 if [:date, :date_past].include?(type_for(field))
914 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
921 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
915 else
922 else
916 if is_custom_filter
923 if is_custom_filter
917 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
924 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
918 else
925 else
919 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
926 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
920 end
927 end
921 end
928 end
922 when "><"
929 when "><"
923 if [:date, :date_past].include?(type_for(field))
930 if [:date, :date_past].include?(type_for(field))
924 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
931 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
925 else
932 else
926 if is_custom_filter
933 if is_custom_filter
927 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
934 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
928 else
935 else
929 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
936 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
930 end
937 end
931 end
938 end
932 when "o"
939 when "o"
933 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
940 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
934 when "c"
941 when "c"
935 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
942 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
936 when "><t-"
943 when "><t-"
937 # between today - n days and today
944 # between today - n days and today
938 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
945 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
939 when ">t-"
946 when ">t-"
940 # >= today - n days
947 # >= today - n days
941 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil)
948 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil)
942 when "<t-"
949 when "<t-"
943 # <= today - n days
950 # <= today - n days
944 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
951 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
945 when "t-"
952 when "t-"
946 # = n days in past
953 # = n days in past
947 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
954 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
948 when "><t+"
955 when "><t+"
949 # between today and today + n days
956 # between today and today + n days
950 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
957 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
951 when ">t+"
958 when ">t+"
952 # >= today + n days
959 # >= today + n days
953 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
960 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
954 when "<t+"
961 when "<t+"
955 # <= today + n days
962 # <= today + n days
956 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i)
963 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i)
957 when "t+"
964 when "t+"
958 # = today + n days
965 # = today + n days
959 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
966 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
960 when "t"
967 when "t"
961 # = today
968 # = today
962 sql = relative_date_clause(db_table, db_field, 0, 0)
969 sql = relative_date_clause(db_table, db_field, 0, 0)
963 when "w"
970 when "w"
964 # = this week
971 # = this week
965 first_day_of_week = l(:general_first_day_of_week).to_i
972 first_day_of_week = l(:general_first_day_of_week).to_i
966 day_of_week = Date.today.cwday
973 day_of_week = Date.today.cwday
967 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
974 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
968 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
975 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
969 when "~"
976 when "~"
970 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
977 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
971 when "!~"
978 when "!~"
972 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
979 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
973 else
980 else
974 raise "Unknown query operator #{operator}"
981 raise "Unknown query operator #{operator}"
975 end
982 end
976
983
977 return sql
984 return sql
978 end
985 end
979
986
980 def add_custom_fields_filters(custom_fields, assoc=nil)
987 def add_custom_fields_filters(custom_fields, assoc=nil)
981 return unless custom_fields.present?
988 return unless custom_fields.present?
982 @available_filters ||= {}
989 @available_filters ||= {}
983
990
984 custom_fields.select(&:is_filter?).each do |field|
991 custom_fields.select(&:is_filter?).each do |field|
985 case field.field_format
992 case field.field_format
986 when "text"
993 when "text"
987 options = { :type => :text, :order => 20 }
994 options = { :type => :text, :order => 20 }
988 when "list"
995 when "list"
989 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
996 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
990 when "date"
997 when "date"
991 options = { :type => :date, :order => 20 }
998 options = { :type => :date, :order => 20 }
992 when "bool"
999 when "bool"
993 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
1000 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
994 when "int"
1001 when "int"
995 options = { :type => :integer, :order => 20 }
1002 options = { :type => :integer, :order => 20 }
996 when "float"
1003 when "float"
997 options = { :type => :float, :order => 20 }
1004 options = { :type => :float, :order => 20 }
998 when "user", "version"
1005 when "user", "version"
999 next unless project
1006 next unless project
1000 values = field.possible_values_options(project)
1007 values = field.possible_values_options(project)
1001 if User.current.logged? && field.field_format == 'user'
1008 if User.current.logged? && field.field_format == 'user'
1002 values.unshift ["<< #{l(:label_me)} >>", "me"]
1009 values.unshift ["<< #{l(:label_me)} >>", "me"]
1003 end
1010 end
1004 options = { :type => :list_optional, :values => values, :order => 20}
1011 options = { :type => :list_optional, :values => values, :order => 20}
1005 else
1012 else
1006 options = { :type => :string, :order => 20 }
1013 options = { :type => :string, :order => 20 }
1007 end
1014 end
1008 filter_id = "cf_#{field.id}"
1015 filter_id = "cf_#{field.id}"
1009 filter_name = field.name
1016 filter_name = field.name
1010 if assoc.present?
1017 if assoc.present?
1011 filter_id = "#{assoc}.#{filter_id}"
1018 filter_id = "#{assoc}.#{filter_id}"
1012 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
1019 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
1013 end
1020 end
1014 @available_filters[filter_id] = options.merge({
1021 @available_filters[filter_id] = options.merge({
1015 :name => filter_name,
1022 :name => filter_name,
1016 :format => field.field_format,
1023 :format => field.field_format,
1017 :field => field
1024 :field => field
1018 })
1025 })
1019 end
1026 end
1020 end
1027 end
1021
1028
1022 def add_associations_custom_fields_filters(*associations)
1029 def add_associations_custom_fields_filters(*associations)
1023 fields_by_class = CustomField.where(:is_filter => true).group_by(&:class)
1030 fields_by_class = CustomField.where(:is_filter => true).group_by(&:class)
1024 associations.each do |assoc|
1031 associations.each do |assoc|
1025 association_klass = Issue.reflect_on_association(assoc).klass
1032 association_klass = Issue.reflect_on_association(assoc).klass
1026 fields_by_class.each do |field_class, fields|
1033 fields_by_class.each do |field_class, fields|
1027 if field_class.customized_class <= association_klass
1034 if field_class.customized_class <= association_klass
1028 add_custom_fields_filters(fields, assoc)
1035 add_custom_fields_filters(fields, assoc)
1029 end
1036 end
1030 end
1037 end
1031 end
1038 end
1032 end
1039 end
1033
1040
1034 # Returns a SQL clause for a date or datetime field.
1041 # Returns a SQL clause for a date or datetime field.
1035 def date_clause(table, field, from, to)
1042 def date_clause(table, field, from, to)
1036 s = []
1043 s = []
1037 if from
1044 if from
1038 from_yesterday = from - 1
1045 from_yesterday = from - 1
1039 from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
1046 from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
1040 if self.class.default_timezone == :utc
1047 if self.class.default_timezone == :utc
1041 from_yesterday_time = from_yesterday_time.utc
1048 from_yesterday_time = from_yesterday_time.utc
1042 end
1049 end
1043 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
1050 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
1044 end
1051 end
1045 if to
1052 if to
1046 to_time = Time.local(to.year, to.month, to.day)
1053 to_time = Time.local(to.year, to.month, to.day)
1047 if self.class.default_timezone == :utc
1054 if self.class.default_timezone == :utc
1048 to_time = to_time.utc
1055 to_time = to_time.utc
1049 end
1056 end
1050 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
1057 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
1051 end
1058 end
1052 s.join(' AND ')
1059 s.join(' AND ')
1053 end
1060 end
1054
1061
1055 # Returns a SQL clause for a date or datetime field using relative dates.
1062 # Returns a SQL clause for a date or datetime field using relative dates.
1056 def relative_date_clause(table, field, days_from, days_to)
1063 def relative_date_clause(table, field, days_from, days_to)
1057 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
1064 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
1058 end
1065 end
1059
1066
1060 # Additional joins required for the given sort options
1067 # Additional joins required for the given sort options
1061 def joins_for_order_statement(order_options)
1068 def joins_for_order_statement(order_options)
1062 joins = []
1069 joins = []
1063
1070
1064 if order_options
1071 if order_options
1065 if order_options.include?('authors')
1072 if order_options.include?('authors')
1066 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{Issue.table_name}.author_id"
1073 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{Issue.table_name}.author_id"
1067 end
1074 end
1068 order_options.scan(/cf_\d+/).uniq.each do |name|
1075 order_options.scan(/cf_\d+/).uniq.each do |name|
1069 column = available_columns.detect {|c| c.name.to_s == name}
1076 column = available_columns.detect {|c| c.name.to_s == name}
1070 join = column && column.custom_field.join_for_order_statement
1077 join = column && column.custom_field.join_for_order_statement
1071 if join
1078 if join
1072 joins << join
1079 joins << join
1073 end
1080 end
1074 end
1081 end
1075 end
1082 end
1076
1083
1077 joins.any? ? joins.join(' ') : nil
1084 joins.any? ? joins.join(' ') : nil
1078 end
1085 end
1079 end
1086 end
@@ -1,566 +1,568
1 # Copyright (c) 2005 Rick Olson
1 # Copyright (c) 2005 Rick Olson
2 #
2 #
3 # Permission is hereby granted, free of charge, to any person obtaining
3 # Permission is hereby granted, free of charge, to any person obtaining
4 # a copy of this software and associated documentation files (the
4 # a copy of this software and associated documentation files (the
5 # "Software"), to deal in the Software without restriction, including
5 # "Software"), to deal in the Software without restriction, including
6 # without limitation the rights to use, copy, modify, merge, publish,
6 # without limitation the rights to use, copy, modify, merge, publish,
7 # distribute, sublicense, and/or sell copies of the Software, and to
7 # distribute, sublicense, and/or sell copies of the Software, and to
8 # permit persons to whom the Software is furnished to do so, subject to
8 # permit persons to whom the Software is furnished to do so, subject to
9 # the following conditions:
9 # the following conditions:
10 #
10 #
11 # The above copyright notice and this permission notice shall be
11 # The above copyright notice and this permission notice shall be
12 # included in all copies or substantial portions of the Software.
12 # included in all copies or substantial portions of the Software.
13 #
13 #
14 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
14 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
15 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
16 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
17 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
18 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
19 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
21
22 module ActiveRecord #:nodoc:
22 module ActiveRecord #:nodoc:
23 module Acts #:nodoc:
23 module Acts #:nodoc:
24 # Specify this act if you want to save a copy of the row in a versioned table. This assumes there is a
24 # Specify this act if you want to save a copy of the row in a versioned table. This assumes there is a
25 # versioned table ready and that your model has a version field. This works with optimistic locking if the lock_version
25 # versioned table ready and that your model has a version field. This works with optimistic locking if the lock_version
26 # column is present as well.
26 # column is present as well.
27 #
27 #
28 # The class for the versioned model is derived the first time it is seen. Therefore, if you change your database schema you have to restart
28 # The class for the versioned model is derived the first time it is seen. Therefore, if you change your database schema you have to restart
29 # your container for the changes to be reflected. In development mode this usually means restarting WEBrick.
29 # your container for the changes to be reflected. In development mode this usually means restarting WEBrick.
30 #
30 #
31 # class Page < ActiveRecord::Base
31 # class Page < ActiveRecord::Base
32 # # assumes pages_versions table
32 # # assumes pages_versions table
33 # acts_as_versioned
33 # acts_as_versioned
34 # end
34 # end
35 #
35 #
36 # Example:
36 # Example:
37 #
37 #
38 # page = Page.create(:title => 'hello world!')
38 # page = Page.create(:title => 'hello world!')
39 # page.version # => 1
39 # page.version # => 1
40 #
40 #
41 # page.title = 'hello world'
41 # page.title = 'hello world'
42 # page.save
42 # page.save
43 # page.version # => 2
43 # page.version # => 2
44 # page.versions.size # => 2
44 # page.versions.size # => 2
45 #
45 #
46 # page.revert_to(1) # using version number
46 # page.revert_to(1) # using version number
47 # page.title # => 'hello world!'
47 # page.title # => 'hello world!'
48 #
48 #
49 # page.revert_to(page.versions.last) # using versioned instance
49 # page.revert_to(page.versions.last) # using versioned instance
50 # page.title # => 'hello world'
50 # page.title # => 'hello world'
51 #
51 #
52 # page.versions.earliest # efficient query to find the first version
52 # page.versions.earliest # efficient query to find the first version
53 # page.versions.latest # efficient query to find the most recently created version
53 # page.versions.latest # efficient query to find the most recently created version
54 #
54 #
55 #
55 #
56 # Simple Queries to page between versions
56 # Simple Queries to page between versions
57 #
57 #
58 # page.versions.before(version)
58 # page.versions.before(version)
59 # page.versions.after(version)
59 # page.versions.after(version)
60 #
60 #
61 # Access the previous/next versions from the versioned model itself
61 # Access the previous/next versions from the versioned model itself
62 #
62 #
63 # version = page.versions.latest
63 # version = page.versions.latest
64 # version.previous # go back one version
64 # version.previous # go back one version
65 # version.next # go forward one version
65 # version.next # go forward one version
66 #
66 #
67 # See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options
67 # See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options
68 module Versioned
68 module Versioned
69 CALLBACKS = [:set_new_version, :save_version_on_create, :save_version?, :clear_altered_attributes]
69 CALLBACKS = [:set_new_version, :save_version_on_create, :save_version?, :clear_altered_attributes]
70 def self.included(base) # :nodoc:
70 def self.included(base) # :nodoc:
71 base.extend ClassMethods
71 base.extend ClassMethods
72 end
72 end
73
73
74 module ClassMethods
74 module ClassMethods
75 # == Configuration options
75 # == Configuration options
76 #
76 #
77 # * <tt>class_name</tt> - versioned model class name (default: PageVersion in the above example)
77 # * <tt>class_name</tt> - versioned model class name (default: PageVersion in the above example)
78 # * <tt>table_name</tt> - versioned model table name (default: page_versions in the above example)
78 # * <tt>table_name</tt> - versioned model table name (default: page_versions in the above example)
79 # * <tt>foreign_key</tt> - foreign key used to relate the versioned model to the original model (default: page_id in the above example)
79 # * <tt>foreign_key</tt> - foreign key used to relate the versioned model to the original model (default: page_id in the above example)
80 # * <tt>inheritance_column</tt> - name of the column to save the model's inheritance_column value for STI. (default: versioned_type)
80 # * <tt>inheritance_column</tt> - name of the column to save the model's inheritance_column value for STI. (default: versioned_type)
81 # * <tt>version_column</tt> - name of the column in the model that keeps the version number (default: version)
81 # * <tt>version_column</tt> - name of the column in the model that keeps the version number (default: version)
82 # * <tt>sequence_name</tt> - name of the custom sequence to be used by the versioned model.
82 # * <tt>sequence_name</tt> - name of the custom sequence to be used by the versioned model.
83 # * <tt>limit</tt> - number of revisions to keep, defaults to unlimited
83 # * <tt>limit</tt> - number of revisions to keep, defaults to unlimited
84 # * <tt>if</tt> - symbol of method to check before saving a new version. If this method returns false, a new version is not saved.
84 # * <tt>if</tt> - symbol of method to check before saving a new version. If this method returns false, a new version is not saved.
85 # For finer control, pass either a Proc or modify Model#version_condition_met?
85 # For finer control, pass either a Proc or modify Model#version_condition_met?
86 #
86 #
87 # acts_as_versioned :if => Proc.new { |auction| !auction.expired? }
87 # acts_as_versioned :if => Proc.new { |auction| !auction.expired? }
88 #
88 #
89 # or...
89 # or...
90 #
90 #
91 # class Auction
91 # class Auction
92 # def version_condition_met? # totally bypasses the <tt>:if</tt> option
92 # def version_condition_met? # totally bypasses the <tt>:if</tt> option
93 # !expired?
93 # !expired?
94 # end
94 # end
95 # end
95 # end
96 #
96 #
97 # * <tt>if_changed</tt> - Simple way of specifying attributes that are required to be changed before saving a model. This takes
97 # * <tt>if_changed</tt> - Simple way of specifying attributes that are required to be changed before saving a model. This takes
98 # either a symbol or array of symbols. WARNING - This will attempt to overwrite any attribute setters you may have.
98 # either a symbol or array of symbols. WARNING - This will attempt to overwrite any attribute setters you may have.
99 # Use this instead if you want to write your own attribute setters (and ignore if_changed):
99 # Use this instead if you want to write your own attribute setters (and ignore if_changed):
100 #
100 #
101 # def name=(new_name)
101 # def name=(new_name)
102 # write_changed_attribute :name, new_name
102 # write_changed_attribute :name, new_name
103 # end
103 # end
104 #
104 #
105 # * <tt>extend</tt> - Lets you specify a module to be mixed in both the original and versioned models. You can also just pass a block
105 # * <tt>extend</tt> - Lets you specify a module to be mixed in both the original and versioned models. You can also just pass a block
106 # to create an anonymous mixin:
106 # to create an anonymous mixin:
107 #
107 #
108 # class Auction
108 # class Auction
109 # acts_as_versioned do
109 # acts_as_versioned do
110 # def started?
110 # def started?
111 # !started_at.nil?
111 # !started_at.nil?
112 # end
112 # end
113 # end
113 # end
114 # end
114 # end
115 #
115 #
116 # or...
116 # or...
117 #
117 #
118 # module AuctionExtension
118 # module AuctionExtension
119 # def started?
119 # def started?
120 # !started_at.nil?
120 # !started_at.nil?
121 # end
121 # end
122 # end
122 # end
123 # class Auction
123 # class Auction
124 # acts_as_versioned :extend => AuctionExtension
124 # acts_as_versioned :extend => AuctionExtension
125 # end
125 # end
126 #
126 #
127 # Example code:
127 # Example code:
128 #
128 #
129 # @auction = Auction.find(1)
129 # @auction = Auction.find(1)
130 # @auction.started?
130 # @auction.started?
131 # @auction.versions.first.started?
131 # @auction.versions.first.started?
132 #
132 #
133 # == Database Schema
133 # == Database Schema
134 #
134 #
135 # The model that you're versioning needs to have a 'version' attribute. The model is versioned
135 # The model that you're versioning needs to have a 'version' attribute. The model is versioned
136 # into a table called #{model}_versions where the model name is singlular. The _versions table should
136 # into a table called #{model}_versions where the model name is singlular. The _versions table should
137 # contain all the fields you want versioned, the same version column, and a #{model}_id foreign key field.
137 # contain all the fields you want versioned, the same version column, and a #{model}_id foreign key field.
138 #
138 #
139 # A lock_version field is also accepted if your model uses Optimistic Locking. If your table uses Single Table inheritance,
139 # A lock_version field is also accepted if your model uses Optimistic Locking. If your table uses Single Table inheritance,
140 # then that field is reflected in the versioned model as 'versioned_type' by default.
140 # then that field is reflected in the versioned model as 'versioned_type' by default.
141 #
141 #
142 # Acts_as_versioned comes prepared with the ActiveRecord::Acts::Versioned::ActMethods::ClassMethods#create_versioned_table
142 # Acts_as_versioned comes prepared with the ActiveRecord::Acts::Versioned::ActMethods::ClassMethods#create_versioned_table
143 # method, perfect for a migration. It will also create the version column if the main model does not already have it.
143 # method, perfect for a migration. It will also create the version column if the main model does not already have it.
144 #
144 #
145 # class AddVersions < ActiveRecord::Migration
145 # class AddVersions < ActiveRecord::Migration
146 # def self.up
146 # def self.up
147 # # create_versioned_table takes the same options hash
147 # # create_versioned_table takes the same options hash
148 # # that create_table does
148 # # that create_table does
149 # Post.create_versioned_table
149 # Post.create_versioned_table
150 # end
150 # end
151 #
151 #
152 # def self.down
152 # def self.down
153 # Post.drop_versioned_table
153 # Post.drop_versioned_table
154 # end
154 # end
155 # end
155 # end
156 #
156 #
157 # == Changing What Fields Are Versioned
157 # == Changing What Fields Are Versioned
158 #
158 #
159 # By default, acts_as_versioned will version all but these fields:
159 # By default, acts_as_versioned will version all but these fields:
160 #
160 #
161 # [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
161 # [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
162 #
162 #
163 # You can add or change those by modifying #non_versioned_columns. Note that this takes strings and not symbols.
163 # You can add or change those by modifying #non_versioned_columns. Note that this takes strings and not symbols.
164 #
164 #
165 # class Post < ActiveRecord::Base
165 # class Post < ActiveRecord::Base
166 # acts_as_versioned
166 # acts_as_versioned
167 # self.non_versioned_columns << 'comments_count'
167 # self.non_versioned_columns << 'comments_count'
168 # end
168 # end
169 #
169 #
170 def acts_as_versioned(options = {}, &extension)
170 def acts_as_versioned(options = {}, &extension)
171 # don't allow multiple calls
171 # don't allow multiple calls
172 return if self.included_modules.include?(ActiveRecord::Acts::Versioned::ActMethods)
172 return if self.included_modules.include?(ActiveRecord::Acts::Versioned::ActMethods)
173
173
174 send :include, ActiveRecord::Acts::Versioned::ActMethods
174 send :include, ActiveRecord::Acts::Versioned::ActMethods
175
175
176 cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, :versioned_inheritance_column,
176 cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, :versioned_inheritance_column,
177 :version_column, :max_version_limit, :track_altered_attributes, :version_condition, :version_sequence_name, :non_versioned_columns,
177 :version_column, :max_version_limit, :track_altered_attributes, :version_condition, :version_sequence_name, :non_versioned_columns,
178 :version_association_options
178 :version_association_options
179
179
180 # legacy
180 # legacy
181 alias_method :non_versioned_fields, :non_versioned_columns
181 alias_method :non_versioned_fields, :non_versioned_columns
182 alias_method :non_versioned_fields=, :non_versioned_columns=
182 alias_method :non_versioned_fields=, :non_versioned_columns=
183
183
184 class << self
184 class << self
185 alias_method :non_versioned_fields, :non_versioned_columns
185 alias_method :non_versioned_fields, :non_versioned_columns
186 alias_method :non_versioned_fields=, :non_versioned_columns=
186 alias_method :non_versioned_fields=, :non_versioned_columns=
187 end
187 end
188
188
189 send :attr_accessor, :altered_attributes
189 send :attr_accessor, :altered_attributes
190
190
191 self.versioned_class_name = options[:class_name] || "Version"
191 self.versioned_class_name = options[:class_name] || "Version"
192 self.versioned_foreign_key = options[:foreign_key] || self.to_s.foreign_key
192 self.versioned_foreign_key = options[:foreign_key] || self.to_s.foreign_key
193 self.versioned_table_name = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_versions#{table_name_suffix}"
193 self.versioned_table_name = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_versions#{table_name_suffix}"
194 self.versioned_inheritance_column = options[:inheritance_column] || "versioned_#{inheritance_column}"
194 self.versioned_inheritance_column = options[:inheritance_column] || "versioned_#{inheritance_column}"
195 self.version_column = options[:version_column] || 'version'
195 self.version_column = options[:version_column] || 'version'
196 self.version_sequence_name = options[:sequence_name]
196 self.version_sequence_name = options[:sequence_name]
197 self.max_version_limit = options[:limit].to_i
197 self.max_version_limit = options[:limit].to_i
198 self.version_condition = options[:if] || true
198 self.version_condition = options[:if] || true
199 self.non_versioned_columns = [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
199 self.non_versioned_columns = [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
200 self.version_association_options = {
200 self.version_association_options = {
201 :class_name => "#{self.to_s}::#{versioned_class_name}",
201 :class_name => "#{self.to_s}::#{versioned_class_name}",
202 :foreign_key => versioned_foreign_key,
202 :foreign_key => versioned_foreign_key,
203 :dependent => :delete_all
203 :dependent => :delete_all
204 }.merge(options[:association_options] || {})
204 }.merge(options[:association_options] || {})
205
205
206 if block_given?
206 if block_given?
207 extension_module_name = "#{versioned_class_name}Extension"
207 extension_module_name = "#{versioned_class_name}Extension"
208 silence_warnings do
208 silence_warnings do
209 self.const_set(extension_module_name, Module.new(&extension))
209 self.const_set(extension_module_name, Module.new(&extension))
210 end
210 end
211
211
212 options[:extend] = self.const_get(extension_module_name)
212 options[:extend] = self.const_get(extension_module_name)
213 end
213 end
214
214
215 class_eval do
215 class_eval do
216 has_many :versions, version_association_options do
216 has_many :versions, version_association_options do
217 # finds earliest version of this record
217 # finds earliest version of this record
218 def earliest
218 def earliest
219 @earliest ||= order('version').first
219 @earliest ||= order('version').first
220 end
220 end
221
221
222 # find latest version of this record
222 # find latest version of this record
223 def latest
223 def latest
224 @latest ||= order('version desc').first
224 @latest ||= order('version desc').first
225 end
225 end
226 end
226 end
227 before_save :set_new_version
227 before_save :set_new_version
228 after_create :save_version_on_create
228 after_create :save_version_on_create
229 after_update :save_version
229 after_update :save_version
230 after_save :clear_old_versions
230 after_save :clear_old_versions
231 after_save :clear_altered_attributes
231 after_save :clear_altered_attributes
232
232
233 unless options[:if_changed].nil?
233 unless options[:if_changed].nil?
234 self.track_altered_attributes = true
234 self.track_altered_attributes = true
235 options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array)
235 options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array)
236 options[:if_changed].each do |attr_name|
236 options[:if_changed].each do |attr_name|
237 define_method("#{attr_name}=") do |value|
237 define_method("#{attr_name}=") do |value|
238 write_changed_attribute attr_name, value
238 write_changed_attribute attr_name, value
239 end
239 end
240 end
240 end
241 end
241 end
242
242
243 include options[:extend] if options[:extend].is_a?(Module)
243 include options[:extend] if options[:extend].is_a?(Module)
244 end
244 end
245
245
246 # create the dynamic versioned model
246 # create the dynamic versioned model
247 const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do
247 const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do
248 def self.reloadable? ; false ; end
248 def self.reloadable? ; false ; end
249 # find first version before the given version
249 # find first version before the given version
250 def self.before(version)
250 def self.before(version)
251 find :first, :order => 'version desc',
251 order('version desc').
252 :conditions => ["#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version]
252 where("#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version).
253 first
253 end
254 end
254
255
255 # find first version after the given version.
256 # find first version after the given version.
256 def self.after(version)
257 def self.after(version)
257 find :first, :order => 'version',
258 order('version').
258 :conditions => ["#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version]
259 where("#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version).
260 first
259 end
261 end
260
262
261 def previous
263 def previous
262 self.class.before(self)
264 self.class.before(self)
263 end
265 end
264
266
265 def next
267 def next
266 self.class.after(self)
268 self.class.after(self)
267 end
269 end
268
270
269 def versions_count
271 def versions_count
270 page.version
272 page.version
271 end
273 end
272 end
274 end
273
275
274 versioned_class.cattr_accessor :original_class
276 versioned_class.cattr_accessor :original_class
275 versioned_class.original_class = self
277 versioned_class.original_class = self
276 versioned_class.table_name = versioned_table_name
278 versioned_class.table_name = versioned_table_name
277 versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym,
279 versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym,
278 :class_name => "::#{self.to_s}",
280 :class_name => "::#{self.to_s}",
279 :foreign_key => versioned_foreign_key
281 :foreign_key => versioned_foreign_key
280 versioned_class.send :include, options[:extend] if options[:extend].is_a?(Module)
282 versioned_class.send :include, options[:extend] if options[:extend].is_a?(Module)
281 versioned_class.set_sequence_name version_sequence_name if version_sequence_name
283 versioned_class.set_sequence_name version_sequence_name if version_sequence_name
282 end
284 end
283 end
285 end
284
286
285 module ActMethods
287 module ActMethods
286 def self.included(base) # :nodoc:
288 def self.included(base) # :nodoc:
287 base.extend ClassMethods
289 base.extend ClassMethods
288 end
290 end
289
291
290 # Finds a specific version of this record
292 # Finds a specific version of this record
291 def find_version(version = nil)
293 def find_version(version = nil)
292 self.class.find_version(id, version)
294 self.class.find_version(id, version)
293 end
295 end
294
296
295 # Saves a version of the model if applicable
297 # Saves a version of the model if applicable
296 def save_version
298 def save_version
297 save_version_on_create if save_version?
299 save_version_on_create if save_version?
298 end
300 end
299
301
300 # Saves a version of the model in the versioned table. This is called in the after_save callback by default
302 # Saves a version of the model in the versioned table. This is called in the after_save callback by default
301 def save_version_on_create
303 def save_version_on_create
302 rev = self.class.versioned_class.new
304 rev = self.class.versioned_class.new
303 self.clone_versioned_model(self, rev)
305 self.clone_versioned_model(self, rev)
304 rev.version = send(self.class.version_column)
306 rev.version = send(self.class.version_column)
305 rev.send("#{self.class.versioned_foreign_key}=", self.id)
307 rev.send("#{self.class.versioned_foreign_key}=", self.id)
306 rev.save
308 rev.save
307 end
309 end
308
310
309 # Clears old revisions if a limit is set with the :limit option in <tt>acts_as_versioned</tt>.
311 # Clears old revisions if a limit is set with the :limit option in <tt>acts_as_versioned</tt>.
310 # Override this method to set your own criteria for clearing old versions.
312 # Override this method to set your own criteria for clearing old versions.
311 def clear_old_versions
313 def clear_old_versions
312 return if self.class.max_version_limit == 0
314 return if self.class.max_version_limit == 0
313 excess_baggage = send(self.class.version_column).to_i - self.class.max_version_limit
315 excess_baggage = send(self.class.version_column).to_i - self.class.max_version_limit
314 if excess_baggage > 0
316 if excess_baggage > 0
315 sql = "DELETE FROM #{self.class.versioned_table_name} WHERE version <= #{excess_baggage} AND #{self.class.versioned_foreign_key} = #{self.id}"
317 sql = "DELETE FROM #{self.class.versioned_table_name} WHERE version <= #{excess_baggage} AND #{self.class.versioned_foreign_key} = #{self.id}"
316 self.class.versioned_class.connection.execute sql
318 self.class.versioned_class.connection.execute sql
317 end
319 end
318 end
320 end
319
321
320 def versions_count
322 def versions_count
321 version
323 version
322 end
324 end
323
325
324 # Reverts a model to a given version. Takes either a version number or an instance of the versioned model
326 # Reverts a model to a given version. Takes either a version number or an instance of the versioned model
325 def revert_to(version)
327 def revert_to(version)
326 if version.is_a?(self.class.versioned_class)
328 if version.is_a?(self.class.versioned_class)
327 return false unless version.send(self.class.versioned_foreign_key) == self.id and !version.new_record?
329 return false unless version.send(self.class.versioned_foreign_key) == self.id and !version.new_record?
328 else
330 else
329 return false unless version = versions.find_by_version(version)
331 return false unless version = versions.find_by_version(version)
330 end
332 end
331 self.clone_versioned_model(version, self)
333 self.clone_versioned_model(version, self)
332 self.send("#{self.class.version_column}=", version.version)
334 self.send("#{self.class.version_column}=", version.version)
333 true
335 true
334 end
336 end
335
337
336 # Reverts a model to a given version and saves the model.
338 # Reverts a model to a given version and saves the model.
337 # Takes either a version number or an instance of the versioned model
339 # Takes either a version number or an instance of the versioned model
338 def revert_to!(version)
340 def revert_to!(version)
339 revert_to(version) ? save_without_revision : false
341 revert_to(version) ? save_without_revision : false
340 end
342 end
341
343
342 # Temporarily turns off Optimistic Locking while saving. Used when reverting so that a new version is not created.
344 # Temporarily turns off Optimistic Locking while saving. Used when reverting so that a new version is not created.
343 def save_without_revision
345 def save_without_revision
344 save_without_revision!
346 save_without_revision!
345 true
347 true
346 rescue
348 rescue
347 false
349 false
348 end
350 end
349
351
350 def save_without_revision!
352 def save_without_revision!
351 without_locking do
353 without_locking do
352 without_revision do
354 without_revision do
353 save!
355 save!
354 end
356 end
355 end
357 end
356 end
358 end
357
359
358 # Returns an array of attribute keys that are versioned. See non_versioned_columns
360 # Returns an array of attribute keys that are versioned. See non_versioned_columns
359 def versioned_attributes
361 def versioned_attributes
360 self.attributes.keys.select { |k| !self.class.non_versioned_columns.include?(k) }
362 self.attributes.keys.select { |k| !self.class.non_versioned_columns.include?(k) }
361 end
363 end
362
364
363 # If called with no parameters, gets whether the current model has changed and needs to be versioned.
365 # If called with no parameters, gets whether the current model has changed and needs to be versioned.
364 # If called with a single parameter, gets whether the parameter has changed.
366 # If called with a single parameter, gets whether the parameter has changed.
365 def changed?(attr_name = nil)
367 def changed?(attr_name = nil)
366 attr_name.nil? ?
368 attr_name.nil? ?
367 (!self.class.track_altered_attributes || (altered_attributes && altered_attributes.length > 0)) :
369 (!self.class.track_altered_attributes || (altered_attributes && altered_attributes.length > 0)) :
368 (altered_attributes && altered_attributes.include?(attr_name.to_s))
370 (altered_attributes && altered_attributes.include?(attr_name.to_s))
369 end
371 end
370
372
371 # keep old dirty? method
373 # keep old dirty? method
372 alias_method :dirty?, :changed?
374 alias_method :dirty?, :changed?
373
375
374 # Clones a model. Used when saving a new version or reverting a model's version.
376 # Clones a model. Used when saving a new version or reverting a model's version.
375 def clone_versioned_model(orig_model, new_model)
377 def clone_versioned_model(orig_model, new_model)
376 self.versioned_attributes.each do |key|
378 self.versioned_attributes.each do |key|
377 new_model.send("#{key}=", orig_model.send(key)) if orig_model.respond_to?(key)
379 new_model.send("#{key}=", orig_model.send(key)) if orig_model.respond_to?(key)
378 end
380 end
379
381
380 if self.class.columns_hash.include?(self.class.inheritance_column)
382 if self.class.columns_hash.include?(self.class.inheritance_column)
381 if orig_model.is_a?(self.class.versioned_class)
383 if orig_model.is_a?(self.class.versioned_class)
382 new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column]
384 new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column]
383 elsif new_model.is_a?(self.class.versioned_class)
385 elsif new_model.is_a?(self.class.versioned_class)
384 new_model[self.class.versioned_inheritance_column] = orig_model[orig_model.class.inheritance_column]
386 new_model[self.class.versioned_inheritance_column] = orig_model[orig_model.class.inheritance_column]
385 end
387 end
386 end
388 end
387 end
389 end
388
390
389 # Checks whether a new version shall be saved or not. Calls <tt>version_condition_met?</tt> and <tt>changed?</tt>.
391 # Checks whether a new version shall be saved or not. Calls <tt>version_condition_met?</tt> and <tt>changed?</tt>.
390 def save_version?
392 def save_version?
391 version_condition_met? && changed?
393 version_condition_met? && changed?
392 end
394 end
393
395
394 # Checks condition set in the :if option to check whether a revision should be created or not. Override this for
396 # Checks condition set in the :if option to check whether a revision should be created or not. Override this for
395 # custom version condition checking.
397 # custom version condition checking.
396 def version_condition_met?
398 def version_condition_met?
397 case
399 case
398 when version_condition.is_a?(Symbol)
400 when version_condition.is_a?(Symbol)
399 send(version_condition)
401 send(version_condition)
400 when version_condition.respond_to?(:call) && (version_condition.arity == 1 || version_condition.arity == -1)
402 when version_condition.respond_to?(:call) && (version_condition.arity == 1 || version_condition.arity == -1)
401 version_condition.call(self)
403 version_condition.call(self)
402 else
404 else
403 version_condition
405 version_condition
404 end
406 end
405 end
407 end
406
408
407 # Executes the block with the versioning callbacks disabled.
409 # Executes the block with the versioning callbacks disabled.
408 #
410 #
409 # @foo.without_revision do
411 # @foo.without_revision do
410 # @foo.save
412 # @foo.save
411 # end
413 # end
412 #
414 #
413 def without_revision(&block)
415 def without_revision(&block)
414 self.class.without_revision(&block)
416 self.class.without_revision(&block)
415 end
417 end
416
418
417 # Turns off optimistic locking for the duration of the block
419 # Turns off optimistic locking for the duration of the block
418 #
420 #
419 # @foo.without_locking do
421 # @foo.without_locking do
420 # @foo.save
422 # @foo.save
421 # end
423 # end
422 #
424 #
423 def without_locking(&block)
425 def without_locking(&block)
424 self.class.without_locking(&block)
426 self.class.without_locking(&block)
425 end
427 end
426
428
427 def empty_callback() end #:nodoc:
429 def empty_callback() end #:nodoc:
428
430
429 protected
431 protected
430 # sets the new version before saving, unless you're using optimistic locking. In that case, let it take care of the version.
432 # sets the new version before saving, unless you're using optimistic locking. In that case, let it take care of the version.
431 def set_new_version
433 def set_new_version
432 self.send("#{self.class.version_column}=", self.next_version) if new_record? || (!locking_enabled? && save_version?)
434 self.send("#{self.class.version_column}=", self.next_version) if new_record? || (!locking_enabled? && save_version?)
433 end
435 end
434
436
435 # Gets the next available version for the current record, or 1 for a new record
437 # Gets the next available version for the current record, or 1 for a new record
436 def next_version
438 def next_version
437 return 1 if new_record?
439 return 1 if new_record?
438 (versions.calculate(:max, :version) || 0) + 1
440 (versions.calculate(:max, :version) || 0) + 1
439 end
441 end
440
442
441 # clears current changed attributes. Called after save.
443 # clears current changed attributes. Called after save.
442 def clear_altered_attributes
444 def clear_altered_attributes
443 self.altered_attributes = []
445 self.altered_attributes = []
444 end
446 end
445
447
446 def write_changed_attribute(attr_name, attr_value)
448 def write_changed_attribute(attr_name, attr_value)
447 # Convert to db type for comparison. Avoids failing Float<=>String comparisons.
449 # Convert to db type for comparison. Avoids failing Float<=>String comparisons.
448 attr_value_for_db = self.class.columns_hash[attr_name.to_s].type_cast(attr_value)
450 attr_value_for_db = self.class.columns_hash[attr_name.to_s].type_cast(attr_value)
449 (self.altered_attributes ||= []) << attr_name.to_s unless self.changed?(attr_name) || self.send(attr_name) == attr_value_for_db
451 (self.altered_attributes ||= []) << attr_name.to_s unless self.changed?(attr_name) || self.send(attr_name) == attr_value_for_db
450 write_attribute(attr_name, attr_value_for_db)
452 write_attribute(attr_name, attr_value_for_db)
451 end
453 end
452
454
453 module ClassMethods
455 module ClassMethods
454 # Finds a specific version of a specific row of this model
456 # Finds a specific version of a specific row of this model
455 def find_version(id, version = nil)
457 def find_version(id, version = nil)
456 return find(id) unless version
458 return find(id) unless version
457
459
458 conditions = ["#{versioned_foreign_key} = ? AND version = ?", id, version]
460 conditions = ["#{versioned_foreign_key} = ? AND version = ?", id, version]
459 options = { :conditions => conditions, :limit => 1 }
461 options = { :conditions => conditions, :limit => 1 }
460
462
461 if result = find_versions(id, options).first
463 if result = find_versions(id, options).first
462 result
464 result
463 else
465 else
464 raise RecordNotFound, "Couldn't find #{name} with ID=#{id} and VERSION=#{version}"
466 raise RecordNotFound, "Couldn't find #{name} with ID=#{id} and VERSION=#{version}"
465 end
467 end
466 end
468 end
467
469
468 # Finds versions of a specific model. Takes an options hash like <tt>find</tt>
470 # Finds versions of a specific model. Takes an options hash like <tt>find</tt>
469 def find_versions(id, options = {})
471 def find_versions(id, options = {})
470 versioned_class.find :all, {
472 versioned_class.all({
471 :conditions => ["#{versioned_foreign_key} = ?", id],
473 :conditions => ["#{versioned_foreign_key} = ?", id],
472 :order => 'version' }.merge(options)
474 :order => 'version' }.merge(options))
473 end
475 end
474
476
475 # Returns an array of columns that are versioned. See non_versioned_columns
477 # Returns an array of columns that are versioned. See non_versioned_columns
476 def versioned_columns
478 def versioned_columns
477 self.columns.select { |c| !non_versioned_columns.include?(c.name) }
479 self.columns.select { |c| !non_versioned_columns.include?(c.name) }
478 end
480 end
479
481
480 # Returns an instance of the dynamic versioned model
482 # Returns an instance of the dynamic versioned model
481 def versioned_class
483 def versioned_class
482 const_get versioned_class_name
484 const_get versioned_class_name
483 end
485 end
484
486
485 # Rake migration task to create the versioned table using options passed to acts_as_versioned
487 # Rake migration task to create the versioned table using options passed to acts_as_versioned
486 def create_versioned_table(create_table_options = {})
488 def create_versioned_table(create_table_options = {})
487 # create version column in main table if it does not exist
489 # create version column in main table if it does not exist
488 if !self.content_columns.find { |c| %w(version lock_version).include? c.name }
490 if !self.content_columns.find { |c| %w(version lock_version).include? c.name }
489 self.connection.add_column table_name, :version, :integer
491 self.connection.add_column table_name, :version, :integer
490 end
492 end
491
493
492 self.connection.create_table(versioned_table_name, create_table_options) do |t|
494 self.connection.create_table(versioned_table_name, create_table_options) do |t|
493 t.column versioned_foreign_key, :integer
495 t.column versioned_foreign_key, :integer
494 t.column :version, :integer
496 t.column :version, :integer
495 end
497 end
496
498
497 updated_col = nil
499 updated_col = nil
498 self.versioned_columns.each do |col|
500 self.versioned_columns.each do |col|
499 updated_col = col if !updated_col && %(updated_at updated_on).include?(col.name)
501 updated_col = col if !updated_col && %(updated_at updated_on).include?(col.name)
500 self.connection.add_column versioned_table_name, col.name, col.type,
502 self.connection.add_column versioned_table_name, col.name, col.type,
501 :limit => col.limit,
503 :limit => col.limit,
502 :default => col.default,
504 :default => col.default,
503 :scale => col.scale,
505 :scale => col.scale,
504 :precision => col.precision
506 :precision => col.precision
505 end
507 end
506
508
507 if type_col = self.columns_hash[inheritance_column]
509 if type_col = self.columns_hash[inheritance_column]
508 self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type,
510 self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type,
509 :limit => type_col.limit,
511 :limit => type_col.limit,
510 :default => type_col.default,
512 :default => type_col.default,
511 :scale => type_col.scale,
513 :scale => type_col.scale,
512 :precision => type_col.precision
514 :precision => type_col.precision
513 end
515 end
514
516
515 if updated_col.nil?
517 if updated_col.nil?
516 self.connection.add_column versioned_table_name, :updated_at, :timestamp
518 self.connection.add_column versioned_table_name, :updated_at, :timestamp
517 end
519 end
518 end
520 end
519
521
520 # Rake migration task to drop the versioned table
522 # Rake migration task to drop the versioned table
521 def drop_versioned_table
523 def drop_versioned_table
522 self.connection.drop_table versioned_table_name
524 self.connection.drop_table versioned_table_name
523 end
525 end
524
526
525 # Executes the block with the versioning callbacks disabled.
527 # Executes the block with the versioning callbacks disabled.
526 #
528 #
527 # Foo.without_revision do
529 # Foo.without_revision do
528 # @foo.save
530 # @foo.save
529 # end
531 # end
530 #
532 #
531 def without_revision(&block)
533 def without_revision(&block)
532 class_eval do
534 class_eval do
533 CALLBACKS.each do |attr_name|
535 CALLBACKS.each do |attr_name|
534 alias_method "orig_#{attr_name}".to_sym, attr_name
536 alias_method "orig_#{attr_name}".to_sym, attr_name
535 alias_method attr_name, :empty_callback
537 alias_method attr_name, :empty_callback
536 end
538 end
537 end
539 end
538 block.call
540 block.call
539 ensure
541 ensure
540 class_eval do
542 class_eval do
541 CALLBACKS.each do |attr_name|
543 CALLBACKS.each do |attr_name|
542 alias_method attr_name, "orig_#{attr_name}".to_sym
544 alias_method attr_name, "orig_#{attr_name}".to_sym
543 end
545 end
544 end
546 end
545 end
547 end
546
548
547 # Turns off optimistic locking for the duration of the block
549 # Turns off optimistic locking for the duration of the block
548 #
550 #
549 # Foo.without_locking do
551 # Foo.without_locking do
550 # @foo.save
552 # @foo.save
551 # end
553 # end
552 #
554 #
553 def without_locking(&block)
555 def without_locking(&block)
554 current = ActiveRecord::Base.lock_optimistically
556 current = ActiveRecord::Base.lock_optimistically
555 ActiveRecord::Base.lock_optimistically = false if current
557 ActiveRecord::Base.lock_optimistically = false if current
556 result = block.call
558 result = block.call
557 ActiveRecord::Base.lock_optimistically = true if current
559 ActiveRecord::Base.lock_optimistically = true if current
558 result
560 result
559 end
561 end
560 end
562 end
561 end
563 end
562 end
564 end
563 end
565 end
564 end
566 end
565
567
566 ActiveRecord::Base.send :include, ActiveRecord::Acts::Versioned No newline at end of file
568 ActiveRecord::Base.send :include, ActiveRecord::Acts::Versioned
@@ -1,511 +1,511
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 desc 'Mantis migration script'
18 desc 'Mantis migration script'
19
19
20 require 'active_record'
20 require 'active_record'
21 require 'iconv'
21 require 'iconv'
22 require 'pp'
22 require 'pp'
23
23
24 namespace :redmine do
24 namespace :redmine do
25 task :migrate_from_mantis => :environment do
25 task :migrate_from_mantis => :environment do
26
26
27 module MantisMigrate
27 module MantisMigrate
28
28
29 DEFAULT_STATUS = IssueStatus.default
29 DEFAULT_STATUS = IssueStatus.default
30 assigned_status = IssueStatus.find_by_position(2)
30 assigned_status = IssueStatus.find_by_position(2)
31 resolved_status = IssueStatus.find_by_position(3)
31 resolved_status = IssueStatus.find_by_position(3)
32 feedback_status = IssueStatus.find_by_position(4)
32 feedback_status = IssueStatus.find_by_position(4)
33 closed_status = IssueStatus.find :first, :conditions => { :is_closed => true }
33 closed_status = IssueStatus.where(:is_closed => true).first
34 STATUS_MAPPING = {10 => DEFAULT_STATUS, # new
34 STATUS_MAPPING = {10 => DEFAULT_STATUS, # new
35 20 => feedback_status, # feedback
35 20 => feedback_status, # feedback
36 30 => DEFAULT_STATUS, # acknowledged
36 30 => DEFAULT_STATUS, # acknowledged
37 40 => DEFAULT_STATUS, # confirmed
37 40 => DEFAULT_STATUS, # confirmed
38 50 => assigned_status, # assigned
38 50 => assigned_status, # assigned
39 80 => resolved_status, # resolved
39 80 => resolved_status, # resolved
40 90 => closed_status # closed
40 90 => closed_status # closed
41 }
41 }
42
42
43 priorities = IssuePriority.all
43 priorities = IssuePriority.all
44 DEFAULT_PRIORITY = priorities[2]
44 DEFAULT_PRIORITY = priorities[2]
45 PRIORITY_MAPPING = {10 => priorities[1], # none
45 PRIORITY_MAPPING = {10 => priorities[1], # none
46 20 => priorities[1], # low
46 20 => priorities[1], # low
47 30 => priorities[2], # normal
47 30 => priorities[2], # normal
48 40 => priorities[3], # high
48 40 => priorities[3], # high
49 50 => priorities[4], # urgent
49 50 => priorities[4], # urgent
50 60 => priorities[5] # immediate
50 60 => priorities[5] # immediate
51 }
51 }
52
52
53 TRACKER_BUG = Tracker.find_by_position(1)
53 TRACKER_BUG = Tracker.find_by_position(1)
54 TRACKER_FEATURE = Tracker.find_by_position(2)
54 TRACKER_FEATURE = Tracker.find_by_position(2)
55
55
56 roles = Role.where(:builtin => 0).order('position ASC').all
56 roles = Role.where(:builtin => 0).order('position ASC').all
57 manager_role = roles[0]
57 manager_role = roles[0]
58 developer_role = roles[1]
58 developer_role = roles[1]
59 DEFAULT_ROLE = roles.last
59 DEFAULT_ROLE = roles.last
60 ROLE_MAPPING = {10 => DEFAULT_ROLE, # viewer
60 ROLE_MAPPING = {10 => DEFAULT_ROLE, # viewer
61 25 => DEFAULT_ROLE, # reporter
61 25 => DEFAULT_ROLE, # reporter
62 40 => DEFAULT_ROLE, # updater
62 40 => DEFAULT_ROLE, # updater
63 55 => developer_role, # developer
63 55 => developer_role, # developer
64 70 => manager_role, # manager
64 70 => manager_role, # manager
65 90 => manager_role # administrator
65 90 => manager_role # administrator
66 }
66 }
67
67
68 CUSTOM_FIELD_TYPE_MAPPING = {0 => 'string', # String
68 CUSTOM_FIELD_TYPE_MAPPING = {0 => 'string', # String
69 1 => 'int', # Numeric
69 1 => 'int', # Numeric
70 2 => 'int', # Float
70 2 => 'int', # Float
71 3 => 'list', # Enumeration
71 3 => 'list', # Enumeration
72 4 => 'string', # Email
72 4 => 'string', # Email
73 5 => 'bool', # Checkbox
73 5 => 'bool', # Checkbox
74 6 => 'list', # List
74 6 => 'list', # List
75 7 => 'list', # Multiselection list
75 7 => 'list', # Multiselection list
76 8 => 'date', # Date
76 8 => 'date', # Date
77 }
77 }
78
78
79 RELATION_TYPE_MAPPING = {1 => IssueRelation::TYPE_RELATES, # related to
79 RELATION_TYPE_MAPPING = {1 => IssueRelation::TYPE_RELATES, # related to
80 2 => IssueRelation::TYPE_RELATES, # parent of
80 2 => IssueRelation::TYPE_RELATES, # parent of
81 3 => IssueRelation::TYPE_RELATES, # child of
81 3 => IssueRelation::TYPE_RELATES, # child of
82 0 => IssueRelation::TYPE_DUPLICATES, # duplicate of
82 0 => IssueRelation::TYPE_DUPLICATES, # duplicate of
83 4 => IssueRelation::TYPE_DUPLICATES # has duplicate
83 4 => IssueRelation::TYPE_DUPLICATES # has duplicate
84 }
84 }
85
85
86 class MantisUser < ActiveRecord::Base
86 class MantisUser < ActiveRecord::Base
87 self.table_name = :mantis_user_table
87 self.table_name = :mantis_user_table
88
88
89 def firstname
89 def firstname
90 @firstname = realname.blank? ? username : realname.split.first[0..29]
90 @firstname = realname.blank? ? username : realname.split.first[0..29]
91 @firstname
91 @firstname
92 end
92 end
93
93
94 def lastname
94 def lastname
95 @lastname = realname.blank? ? '-' : realname.split[1..-1].join(' ')[0..29]
95 @lastname = realname.blank? ? '-' : realname.split[1..-1].join(' ')[0..29]
96 @lastname = '-' if @lastname.blank?
96 @lastname = '-' if @lastname.blank?
97 @lastname
97 @lastname
98 end
98 end
99
99
100 def email
100 def email
101 if read_attribute(:email).match(/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i) &&
101 if read_attribute(:email).match(/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i) &&
102 !User.find_by_mail(read_attribute(:email))
102 !User.find_by_mail(read_attribute(:email))
103 @email = read_attribute(:email)
103 @email = read_attribute(:email)
104 else
104 else
105 @email = "#{username}@foo.bar"
105 @email = "#{username}@foo.bar"
106 end
106 end
107 end
107 end
108
108
109 def username
109 def username
110 read_attribute(:username)[0..29].gsub(/[^a-zA-Z0-9_\-@\.]/, '-')
110 read_attribute(:username)[0..29].gsub(/[^a-zA-Z0-9_\-@\.]/, '-')
111 end
111 end
112 end
112 end
113
113
114 class MantisProject < ActiveRecord::Base
114 class MantisProject < ActiveRecord::Base
115 self.table_name = :mantis_project_table
115 self.table_name = :mantis_project_table
116 has_many :versions, :class_name => "MantisVersion", :foreign_key => :project_id
116 has_many :versions, :class_name => "MantisVersion", :foreign_key => :project_id
117 has_many :categories, :class_name => "MantisCategory", :foreign_key => :project_id
117 has_many :categories, :class_name => "MantisCategory", :foreign_key => :project_id
118 has_many :news, :class_name => "MantisNews", :foreign_key => :project_id
118 has_many :news, :class_name => "MantisNews", :foreign_key => :project_id
119 has_many :members, :class_name => "MantisProjectUser", :foreign_key => :project_id
119 has_many :members, :class_name => "MantisProjectUser", :foreign_key => :project_id
120
120
121 def identifier
121 def identifier
122 read_attribute(:name).gsub(/[^a-z0-9\-]+/, '-').slice(0, Project::IDENTIFIER_MAX_LENGTH)
122 read_attribute(:name).gsub(/[^a-z0-9\-]+/, '-').slice(0, Project::IDENTIFIER_MAX_LENGTH)
123 end
123 end
124 end
124 end
125
125
126 class MantisVersion < ActiveRecord::Base
126 class MantisVersion < ActiveRecord::Base
127 self.table_name = :mantis_project_version_table
127 self.table_name = :mantis_project_version_table
128
128
129 def version
129 def version
130 read_attribute(:version)[0..29]
130 read_attribute(:version)[0..29]
131 end
131 end
132
132
133 def description
133 def description
134 read_attribute(:description)[0..254]
134 read_attribute(:description)[0..254]
135 end
135 end
136 end
136 end
137
137
138 class MantisCategory < ActiveRecord::Base
138 class MantisCategory < ActiveRecord::Base
139 self.table_name = :mantis_project_category_table
139 self.table_name = :mantis_project_category_table
140 end
140 end
141
141
142 class MantisProjectUser < ActiveRecord::Base
142 class MantisProjectUser < ActiveRecord::Base
143 self.table_name = :mantis_project_user_list_table
143 self.table_name = :mantis_project_user_list_table
144 end
144 end
145
145
146 class MantisBug < ActiveRecord::Base
146 class MantisBug < ActiveRecord::Base
147 self.table_name = :mantis_bug_table
147 self.table_name = :mantis_bug_table
148 belongs_to :bug_text, :class_name => "MantisBugText", :foreign_key => :bug_text_id
148 belongs_to :bug_text, :class_name => "MantisBugText", :foreign_key => :bug_text_id
149 has_many :bug_notes, :class_name => "MantisBugNote", :foreign_key => :bug_id
149 has_many :bug_notes, :class_name => "MantisBugNote", :foreign_key => :bug_id
150 has_many :bug_files, :class_name => "MantisBugFile", :foreign_key => :bug_id
150 has_many :bug_files, :class_name => "MantisBugFile", :foreign_key => :bug_id
151 has_many :bug_monitors, :class_name => "MantisBugMonitor", :foreign_key => :bug_id
151 has_many :bug_monitors, :class_name => "MantisBugMonitor", :foreign_key => :bug_id
152 end
152 end
153
153
154 class MantisBugText < ActiveRecord::Base
154 class MantisBugText < ActiveRecord::Base
155 self.table_name = :mantis_bug_text_table
155 self.table_name = :mantis_bug_text_table
156
156
157 # Adds Mantis steps_to_reproduce and additional_information fields
157 # Adds Mantis steps_to_reproduce and additional_information fields
158 # to description if any
158 # to description if any
159 def full_description
159 def full_description
160 full_description = description
160 full_description = description
161 full_description += "\n\n*Steps to reproduce:*\n\n#{steps_to_reproduce}" unless steps_to_reproduce.blank?
161 full_description += "\n\n*Steps to reproduce:*\n\n#{steps_to_reproduce}" unless steps_to_reproduce.blank?
162 full_description += "\n\n*Additional information:*\n\n#{additional_information}" unless additional_information.blank?
162 full_description += "\n\n*Additional information:*\n\n#{additional_information}" unless additional_information.blank?
163 full_description
163 full_description
164 end
164 end
165 end
165 end
166
166
167 class MantisBugNote < ActiveRecord::Base
167 class MantisBugNote < ActiveRecord::Base
168 self.table_name = :mantis_bugnote_table
168 self.table_name = :mantis_bugnote_table
169 belongs_to :bug, :class_name => "MantisBug", :foreign_key => :bug_id
169 belongs_to :bug, :class_name => "MantisBug", :foreign_key => :bug_id
170 belongs_to :bug_note_text, :class_name => "MantisBugNoteText", :foreign_key => :bugnote_text_id
170 belongs_to :bug_note_text, :class_name => "MantisBugNoteText", :foreign_key => :bugnote_text_id
171 end
171 end
172
172
173 class MantisBugNoteText < ActiveRecord::Base
173 class MantisBugNoteText < ActiveRecord::Base
174 self.table_name = :mantis_bugnote_text_table
174 self.table_name = :mantis_bugnote_text_table
175 end
175 end
176
176
177 class MantisBugFile < ActiveRecord::Base
177 class MantisBugFile < ActiveRecord::Base
178 self.table_name = :mantis_bug_file_table
178 self.table_name = :mantis_bug_file_table
179
179
180 def size
180 def size
181 filesize
181 filesize
182 end
182 end
183
183
184 def original_filename
184 def original_filename
185 MantisMigrate.encode(filename)
185 MantisMigrate.encode(filename)
186 end
186 end
187
187
188 def content_type
188 def content_type
189 file_type
189 file_type
190 end
190 end
191
191
192 def read(*args)
192 def read(*args)
193 if @read_finished
193 if @read_finished
194 nil
194 nil
195 else
195 else
196 @read_finished = true
196 @read_finished = true
197 content
197 content
198 end
198 end
199 end
199 end
200 end
200 end
201
201
202 class MantisBugRelationship < ActiveRecord::Base
202 class MantisBugRelationship < ActiveRecord::Base
203 self.table_name = :mantis_bug_relationship_table
203 self.table_name = :mantis_bug_relationship_table
204 end
204 end
205
205
206 class MantisBugMonitor < ActiveRecord::Base
206 class MantisBugMonitor < ActiveRecord::Base
207 self.table_name = :mantis_bug_monitor_table
207 self.table_name = :mantis_bug_monitor_table
208 end
208 end
209
209
210 class MantisNews < ActiveRecord::Base
210 class MantisNews < ActiveRecord::Base
211 self.table_name = :mantis_news_table
211 self.table_name = :mantis_news_table
212 end
212 end
213
213
214 class MantisCustomField < ActiveRecord::Base
214 class MantisCustomField < ActiveRecord::Base
215 self.table_name = :mantis_custom_field_table
215 self.table_name = :mantis_custom_field_table
216 set_inheritance_column :none
216 set_inheritance_column :none
217 has_many :values, :class_name => "MantisCustomFieldString", :foreign_key => :field_id
217 has_many :values, :class_name => "MantisCustomFieldString", :foreign_key => :field_id
218 has_many :projects, :class_name => "MantisCustomFieldProject", :foreign_key => :field_id
218 has_many :projects, :class_name => "MantisCustomFieldProject", :foreign_key => :field_id
219
219
220 def format
220 def format
221 read_attribute :type
221 read_attribute :type
222 end
222 end
223
223
224 def name
224 def name
225 read_attribute(:name)[0..29]
225 read_attribute(:name)[0..29]
226 end
226 end
227 end
227 end
228
228
229 class MantisCustomFieldProject < ActiveRecord::Base
229 class MantisCustomFieldProject < ActiveRecord::Base
230 self.table_name = :mantis_custom_field_project_table
230 self.table_name = :mantis_custom_field_project_table
231 end
231 end
232
232
233 class MantisCustomFieldString < ActiveRecord::Base
233 class MantisCustomFieldString < ActiveRecord::Base
234 self.table_name = :mantis_custom_field_string_table
234 self.table_name = :mantis_custom_field_string_table
235 end
235 end
236
236
237 def self.migrate
237 def self.migrate
238
238
239 # Users
239 # Users
240 print "Migrating users"
240 print "Migrating users"
241 User.delete_all "login <> 'admin'"
241 User.delete_all "login <> 'admin'"
242 users_map = {}
242 users_map = {}
243 users_migrated = 0
243 users_migrated = 0
244 MantisUser.all.each do |user|
244 MantisUser.all.each do |user|
245 u = User.new :firstname => encode(user.firstname),
245 u = User.new :firstname => encode(user.firstname),
246 :lastname => encode(user.lastname),
246 :lastname => encode(user.lastname),
247 :mail => user.email,
247 :mail => user.email,
248 :last_login_on => user.last_visit
248 :last_login_on => user.last_visit
249 u.login = user.username
249 u.login = user.username
250 u.password = 'mantis'
250 u.password = 'mantis'
251 u.status = User::STATUS_LOCKED if user.enabled != 1
251 u.status = User::STATUS_LOCKED if user.enabled != 1
252 u.admin = true if user.access_level == 90
252 u.admin = true if user.access_level == 90
253 next unless u.save!
253 next unless u.save!
254 users_migrated += 1
254 users_migrated += 1
255 users_map[user.id] = u.id
255 users_map[user.id] = u.id
256 print '.'
256 print '.'
257 end
257 end
258 puts
258 puts
259
259
260 # Projects
260 # Projects
261 print "Migrating projects"
261 print "Migrating projects"
262 Project.destroy_all
262 Project.destroy_all
263 projects_map = {}
263 projects_map = {}
264 versions_map = {}
264 versions_map = {}
265 categories_map = {}
265 categories_map = {}
266 MantisProject.all.each do |project|
266 MantisProject.all.each do |project|
267 p = Project.new :name => encode(project.name),
267 p = Project.new :name => encode(project.name),
268 :description => encode(project.description)
268 :description => encode(project.description)
269 p.identifier = project.identifier
269 p.identifier = project.identifier
270 next unless p.save
270 next unless p.save
271 projects_map[project.id] = p.id
271 projects_map[project.id] = p.id
272 p.enabled_module_names = ['issue_tracking', 'news', 'wiki']
272 p.enabled_module_names = ['issue_tracking', 'news', 'wiki']
273 p.trackers << TRACKER_BUG unless p.trackers.include?(TRACKER_BUG)
273 p.trackers << TRACKER_BUG unless p.trackers.include?(TRACKER_BUG)
274 p.trackers << TRACKER_FEATURE unless p.trackers.include?(TRACKER_FEATURE)
274 p.trackers << TRACKER_FEATURE unless p.trackers.include?(TRACKER_FEATURE)
275 print '.'
275 print '.'
276
276
277 # Project members
277 # Project members
278 project.members.each do |member|
278 project.members.each do |member|
279 m = Member.new :user => User.find_by_id(users_map[member.user_id]),
279 m = Member.new :user => User.find_by_id(users_map[member.user_id]),
280 :roles => [ROLE_MAPPING[member.access_level] || DEFAULT_ROLE]
280 :roles => [ROLE_MAPPING[member.access_level] || DEFAULT_ROLE]
281 m.project = p
281 m.project = p
282 m.save
282 m.save
283 end
283 end
284
284
285 # Project versions
285 # Project versions
286 project.versions.each do |version|
286 project.versions.each do |version|
287 v = Version.new :name => encode(version.version),
287 v = Version.new :name => encode(version.version),
288 :description => encode(version.description),
288 :description => encode(version.description),
289 :effective_date => (version.date_order ? version.date_order.to_date : nil)
289 :effective_date => (version.date_order ? version.date_order.to_date : nil)
290 v.project = p
290 v.project = p
291 v.save
291 v.save
292 versions_map[version.id] = v.id
292 versions_map[version.id] = v.id
293 end
293 end
294
294
295 # Project categories
295 # Project categories
296 project.categories.each do |category|
296 project.categories.each do |category|
297 g = IssueCategory.new :name => category.category[0,30]
297 g = IssueCategory.new :name => category.category[0,30]
298 g.project = p
298 g.project = p
299 g.save
299 g.save
300 categories_map[category.category] = g.id
300 categories_map[category.category] = g.id
301 end
301 end
302 end
302 end
303 puts
303 puts
304
304
305 # Bugs
305 # Bugs
306 print "Migrating bugs"
306 print "Migrating bugs"
307 Issue.destroy_all
307 Issue.destroy_all
308 issues_map = {}
308 issues_map = {}
309 keep_bug_ids = (Issue.count == 0)
309 keep_bug_ids = (Issue.count == 0)
310 MantisBug.find_each(:batch_size => 200) do |bug|
310 MantisBug.find_each(:batch_size => 200) do |bug|
311 next unless projects_map[bug.project_id] && users_map[bug.reporter_id]
311 next unless projects_map[bug.project_id] && users_map[bug.reporter_id]
312 i = Issue.new :project_id => projects_map[bug.project_id],
312 i = Issue.new :project_id => projects_map[bug.project_id],
313 :subject => encode(bug.summary),
313 :subject => encode(bug.summary),
314 :description => encode(bug.bug_text.full_description),
314 :description => encode(bug.bug_text.full_description),
315 :priority => PRIORITY_MAPPING[bug.priority] || DEFAULT_PRIORITY,
315 :priority => PRIORITY_MAPPING[bug.priority] || DEFAULT_PRIORITY,
316 :created_on => bug.date_submitted,
316 :created_on => bug.date_submitted,
317 :updated_on => bug.last_updated
317 :updated_on => bug.last_updated
318 i.author = User.find_by_id(users_map[bug.reporter_id])
318 i.author = User.find_by_id(users_map[bug.reporter_id])
319 i.category = IssueCategory.find_by_project_id_and_name(i.project_id, bug.category[0,30]) unless bug.category.blank?
319 i.category = IssueCategory.find_by_project_id_and_name(i.project_id, bug.category[0,30]) unless bug.category.blank?
320 i.fixed_version = Version.find_by_project_id_and_name(i.project_id, bug.fixed_in_version) unless bug.fixed_in_version.blank?
320 i.fixed_version = Version.find_by_project_id_and_name(i.project_id, bug.fixed_in_version) unless bug.fixed_in_version.blank?
321 i.status = STATUS_MAPPING[bug.status] || DEFAULT_STATUS
321 i.status = STATUS_MAPPING[bug.status] || DEFAULT_STATUS
322 i.tracker = (bug.severity == 10 ? TRACKER_FEATURE : TRACKER_BUG)
322 i.tracker = (bug.severity == 10 ? TRACKER_FEATURE : TRACKER_BUG)
323 i.id = bug.id if keep_bug_ids
323 i.id = bug.id if keep_bug_ids
324 next unless i.save
324 next unless i.save
325 issues_map[bug.id] = i.id
325 issues_map[bug.id] = i.id
326 print '.'
326 print '.'
327 STDOUT.flush
327 STDOUT.flush
328
328
329 # Assignee
329 # Assignee
330 # Redmine checks that the assignee is a project member
330 # Redmine checks that the assignee is a project member
331 if (bug.handler_id && users_map[bug.handler_id])
331 if (bug.handler_id && users_map[bug.handler_id])
332 i.assigned_to = User.find_by_id(users_map[bug.handler_id])
332 i.assigned_to = User.find_by_id(users_map[bug.handler_id])
333 i.save(:validate => false)
333 i.save(:validate => false)
334 end
334 end
335
335
336 # Bug notes
336 # Bug notes
337 bug.bug_notes.each do |note|
337 bug.bug_notes.each do |note|
338 next unless users_map[note.reporter_id]
338 next unless users_map[note.reporter_id]
339 n = Journal.new :notes => encode(note.bug_note_text.note),
339 n = Journal.new :notes => encode(note.bug_note_text.note),
340 :created_on => note.date_submitted
340 :created_on => note.date_submitted
341 n.user = User.find_by_id(users_map[note.reporter_id])
341 n.user = User.find_by_id(users_map[note.reporter_id])
342 n.journalized = i
342 n.journalized = i
343 n.save
343 n.save
344 end
344 end
345
345
346 # Bug files
346 # Bug files
347 bug.bug_files.each do |file|
347 bug.bug_files.each do |file|
348 a = Attachment.new :created_on => file.date_added
348 a = Attachment.new :created_on => file.date_added
349 a.file = file
349 a.file = file
350 a.author = User.find :first
350 a.author = User.first
351 a.container = i
351 a.container = i
352 a.save
352 a.save
353 end
353 end
354
354
355 # Bug monitors
355 # Bug monitors
356 bug.bug_monitors.each do |monitor|
356 bug.bug_monitors.each do |monitor|
357 next unless users_map[monitor.user_id]
357 next unless users_map[monitor.user_id]
358 i.add_watcher(User.find_by_id(users_map[monitor.user_id]))
358 i.add_watcher(User.find_by_id(users_map[monitor.user_id]))
359 end
359 end
360 end
360 end
361
361
362 # update issue id sequence if needed (postgresql)
362 # update issue id sequence if needed (postgresql)
363 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
363 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
364 puts
364 puts
365
365
366 # Bug relationships
366 # Bug relationships
367 print "Migrating bug relations"
367 print "Migrating bug relations"
368 MantisBugRelationship.all.each do |relation|
368 MantisBugRelationship.all.each do |relation|
369 next unless issues_map[relation.source_bug_id] && issues_map[relation.destination_bug_id]
369 next unless issues_map[relation.source_bug_id] && issues_map[relation.destination_bug_id]
370 r = IssueRelation.new :relation_type => RELATION_TYPE_MAPPING[relation.relationship_type]
370 r = IssueRelation.new :relation_type => RELATION_TYPE_MAPPING[relation.relationship_type]
371 r.issue_from = Issue.find_by_id(issues_map[relation.source_bug_id])
371 r.issue_from = Issue.find_by_id(issues_map[relation.source_bug_id])
372 r.issue_to = Issue.find_by_id(issues_map[relation.destination_bug_id])
372 r.issue_to = Issue.find_by_id(issues_map[relation.destination_bug_id])
373 pp r unless r.save
373 pp r unless r.save
374 print '.'
374 print '.'
375 STDOUT.flush
375 STDOUT.flush
376 end
376 end
377 puts
377 puts
378
378
379 # News
379 # News
380 print "Migrating news"
380 print "Migrating news"
381 News.destroy_all
381 News.destroy_all
382 MantisNews.where('project_id > 0').all.each do |news|
382 MantisNews.where('project_id > 0').all.each do |news|
383 next unless projects_map[news.project_id]
383 next unless projects_map[news.project_id]
384 n = News.new :project_id => projects_map[news.project_id],
384 n = News.new :project_id => projects_map[news.project_id],
385 :title => encode(news.headline[0..59]),
385 :title => encode(news.headline[0..59]),
386 :description => encode(news.body),
386 :description => encode(news.body),
387 :created_on => news.date_posted
387 :created_on => news.date_posted
388 n.author = User.find_by_id(users_map[news.poster_id])
388 n.author = User.find_by_id(users_map[news.poster_id])
389 n.save
389 n.save
390 print '.'
390 print '.'
391 STDOUT.flush
391 STDOUT.flush
392 end
392 end
393 puts
393 puts
394
394
395 # Custom fields
395 # Custom fields
396 print "Migrating custom fields"
396 print "Migrating custom fields"
397 IssueCustomField.destroy_all
397 IssueCustomField.destroy_all
398 MantisCustomField.all.each do |field|
398 MantisCustomField.all.each do |field|
399 f = IssueCustomField.new :name => field.name[0..29],
399 f = IssueCustomField.new :name => field.name[0..29],
400 :field_format => CUSTOM_FIELD_TYPE_MAPPING[field.format],
400 :field_format => CUSTOM_FIELD_TYPE_MAPPING[field.format],
401 :min_length => field.length_min,
401 :min_length => field.length_min,
402 :max_length => field.length_max,
402 :max_length => field.length_max,
403 :regexp => field.valid_regexp,
403 :regexp => field.valid_regexp,
404 :possible_values => field.possible_values.split('|'),
404 :possible_values => field.possible_values.split('|'),
405 :is_required => field.require_report?
405 :is_required => field.require_report?
406 next unless f.save
406 next unless f.save
407 print '.'
407 print '.'
408 STDOUT.flush
408 STDOUT.flush
409 # Trackers association
409 # Trackers association
410 f.trackers = Tracker.all
410 f.trackers = Tracker.all
411
411
412 # Projects association
412 # Projects association
413 field.projects.each do |project|
413 field.projects.each do |project|
414 f.projects << Project.find_by_id(projects_map[project.project_id]) if projects_map[project.project_id]
414 f.projects << Project.find_by_id(projects_map[project.project_id]) if projects_map[project.project_id]
415 end
415 end
416
416
417 # Values
417 # Values
418 field.values.each do |value|
418 field.values.each do |value|
419 v = CustomValue.new :custom_field_id => f.id,
419 v = CustomValue.new :custom_field_id => f.id,
420 :value => value.value
420 :value => value.value
421 v.customized = Issue.find_by_id(issues_map[value.bug_id]) if issues_map[value.bug_id]
421 v.customized = Issue.find_by_id(issues_map[value.bug_id]) if issues_map[value.bug_id]
422 v.save
422 v.save
423 end unless f.new_record?
423 end unless f.new_record?
424 end
424 end
425 puts
425 puts
426
426
427 puts
427 puts
428 puts "Users: #{users_migrated}/#{MantisUser.count}"
428 puts "Users: #{users_migrated}/#{MantisUser.count}"
429 puts "Projects: #{Project.count}/#{MantisProject.count}"
429 puts "Projects: #{Project.count}/#{MantisProject.count}"
430 puts "Memberships: #{Member.count}/#{MantisProjectUser.count}"
430 puts "Memberships: #{Member.count}/#{MantisProjectUser.count}"
431 puts "Versions: #{Version.count}/#{MantisVersion.count}"
431 puts "Versions: #{Version.count}/#{MantisVersion.count}"
432 puts "Categories: #{IssueCategory.count}/#{MantisCategory.count}"
432 puts "Categories: #{IssueCategory.count}/#{MantisCategory.count}"
433 puts "Bugs: #{Issue.count}/#{MantisBug.count}"
433 puts "Bugs: #{Issue.count}/#{MantisBug.count}"
434 puts "Bug notes: #{Journal.count}/#{MantisBugNote.count}"
434 puts "Bug notes: #{Journal.count}/#{MantisBugNote.count}"
435 puts "Bug files: #{Attachment.count}/#{MantisBugFile.count}"
435 puts "Bug files: #{Attachment.count}/#{MantisBugFile.count}"
436 puts "Bug relations: #{IssueRelation.count}/#{MantisBugRelationship.count}"
436 puts "Bug relations: #{IssueRelation.count}/#{MantisBugRelationship.count}"
437 puts "Bug monitors: #{Watcher.count}/#{MantisBugMonitor.count}"
437 puts "Bug monitors: #{Watcher.count}/#{MantisBugMonitor.count}"
438 puts "News: #{News.count}/#{MantisNews.count}"
438 puts "News: #{News.count}/#{MantisNews.count}"
439 puts "Custom fields: #{IssueCustomField.count}/#{MantisCustomField.count}"
439 puts "Custom fields: #{IssueCustomField.count}/#{MantisCustomField.count}"
440 end
440 end
441
441
442 def self.encoding(charset)
442 def self.encoding(charset)
443 @ic = Iconv.new('UTF-8', charset)
443 @ic = Iconv.new('UTF-8', charset)
444 rescue Iconv::InvalidEncoding
444 rescue Iconv::InvalidEncoding
445 return false
445 return false
446 end
446 end
447
447
448 def self.establish_connection(params)
448 def self.establish_connection(params)
449 constants.each do |const|
449 constants.each do |const|
450 klass = const_get(const)
450 klass = const_get(const)
451 next unless klass.respond_to? 'establish_connection'
451 next unless klass.respond_to? 'establish_connection'
452 klass.establish_connection params
452 klass.establish_connection params
453 end
453 end
454 end
454 end
455
455
456 def self.encode(text)
456 def self.encode(text)
457 @ic.iconv text
457 @ic.iconv text
458 rescue
458 rescue
459 text
459 text
460 end
460 end
461 end
461 end
462
462
463 puts
463 puts
464 if Redmine::DefaultData::Loader.no_data?
464 if Redmine::DefaultData::Loader.no_data?
465 puts "Redmine configuration need to be loaded before importing data."
465 puts "Redmine configuration need to be loaded before importing data."
466 puts "Please, run this first:"
466 puts "Please, run this first:"
467 puts
467 puts
468 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
468 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
469 exit
469 exit
470 end
470 end
471
471
472 puts "WARNING: Your Redmine data will be deleted during this process."
472 puts "WARNING: Your Redmine data will be deleted during this process."
473 print "Are you sure you want to continue ? [y/N] "
473 print "Are you sure you want to continue ? [y/N] "
474 STDOUT.flush
474 STDOUT.flush
475 break unless STDIN.gets.match(/^y$/i)
475 break unless STDIN.gets.match(/^y$/i)
476
476
477 # Default Mantis database settings
477 # Default Mantis database settings
478 db_params = {:adapter => 'mysql2',
478 db_params = {:adapter => 'mysql2',
479 :database => 'bugtracker',
479 :database => 'bugtracker',
480 :host => 'localhost',
480 :host => 'localhost',
481 :username => 'root',
481 :username => 'root',
482 :password => '' }
482 :password => '' }
483
483
484 puts
484 puts
485 puts "Please enter settings for your Mantis database"
485 puts "Please enter settings for your Mantis database"
486 [:adapter, :host, :database, :username, :password].each do |param|
486 [:adapter, :host, :database, :username, :password].each do |param|
487 print "#{param} [#{db_params[param]}]: "
487 print "#{param} [#{db_params[param]}]: "
488 value = STDIN.gets.chomp!
488 value = STDIN.gets.chomp!
489 db_params[param] = value unless value.blank?
489 db_params[param] = value unless value.blank?
490 end
490 end
491
491
492 while true
492 while true
493 print "encoding [UTF-8]: "
493 print "encoding [UTF-8]: "
494 STDOUT.flush
494 STDOUT.flush
495 encoding = STDIN.gets.chomp!
495 encoding = STDIN.gets.chomp!
496 encoding = 'UTF-8' if encoding.blank?
496 encoding = 'UTF-8' if encoding.blank?
497 break if MantisMigrate.encoding encoding
497 break if MantisMigrate.encoding encoding
498 puts "Invalid encoding!"
498 puts "Invalid encoding!"
499 end
499 end
500 puts
500 puts
501
501
502 # Make sure bugs can refer bugs in other projects
502 # Make sure bugs can refer bugs in other projects
503 Setting.cross_project_issue_relations = 1 if Setting.respond_to? 'cross_project_issue_relations'
503 Setting.cross_project_issue_relations = 1 if Setting.respond_to? 'cross_project_issue_relations'
504
504
505 # Turn off email notifications
505 # Turn off email notifications
506 Setting.notified_events = []
506 Setting.notified_events = []
507
507
508 MantisMigrate.establish_connection db_params
508 MantisMigrate.establish_connection db_params
509 MantisMigrate.migrate
509 MantisMigrate.migrate
510 end
510 end
511 end
511 end
@@ -1,772 +1,772
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'active_record'
18 require 'active_record'
19 require 'iconv'
19 require 'iconv'
20 require 'pp'
20 require 'pp'
21
21
22 namespace :redmine do
22 namespace :redmine do
23 desc 'Trac migration script'
23 desc 'Trac migration script'
24 task :migrate_from_trac => :environment do
24 task :migrate_from_trac => :environment do
25
25
26 module TracMigrate
26 module TracMigrate
27 TICKET_MAP = []
27 TICKET_MAP = []
28
28
29 DEFAULT_STATUS = IssueStatus.default
29 DEFAULT_STATUS = IssueStatus.default
30 assigned_status = IssueStatus.find_by_position(2)
30 assigned_status = IssueStatus.find_by_position(2)
31 resolved_status = IssueStatus.find_by_position(3)
31 resolved_status = IssueStatus.find_by_position(3)
32 feedback_status = IssueStatus.find_by_position(4)
32 feedback_status = IssueStatus.find_by_position(4)
33 closed_status = IssueStatus.find :first, :conditions => { :is_closed => true }
33 closed_status = IssueStatus.where(:is_closed => true).first
34 STATUS_MAPPING = {'new' => DEFAULT_STATUS,
34 STATUS_MAPPING = {'new' => DEFAULT_STATUS,
35 'reopened' => feedback_status,
35 'reopened' => feedback_status,
36 'assigned' => assigned_status,
36 'assigned' => assigned_status,
37 'closed' => closed_status
37 'closed' => closed_status
38 }
38 }
39
39
40 priorities = IssuePriority.all
40 priorities = IssuePriority.all
41 DEFAULT_PRIORITY = priorities[0]
41 DEFAULT_PRIORITY = priorities[0]
42 PRIORITY_MAPPING = {'lowest' => priorities[0],
42 PRIORITY_MAPPING = {'lowest' => priorities[0],
43 'low' => priorities[0],
43 'low' => priorities[0],
44 'normal' => priorities[1],
44 'normal' => priorities[1],
45 'high' => priorities[2],
45 'high' => priorities[2],
46 'highest' => priorities[3],
46 'highest' => priorities[3],
47 # ---
47 # ---
48 'trivial' => priorities[0],
48 'trivial' => priorities[0],
49 'minor' => priorities[1],
49 'minor' => priorities[1],
50 'major' => priorities[2],
50 'major' => priorities[2],
51 'critical' => priorities[3],
51 'critical' => priorities[3],
52 'blocker' => priorities[4]
52 'blocker' => priorities[4]
53 }
53 }
54
54
55 TRACKER_BUG = Tracker.find_by_position(1)
55 TRACKER_BUG = Tracker.find_by_position(1)
56 TRACKER_FEATURE = Tracker.find_by_position(2)
56 TRACKER_FEATURE = Tracker.find_by_position(2)
57 DEFAULT_TRACKER = TRACKER_BUG
57 DEFAULT_TRACKER = TRACKER_BUG
58 TRACKER_MAPPING = {'defect' => TRACKER_BUG,
58 TRACKER_MAPPING = {'defect' => TRACKER_BUG,
59 'enhancement' => TRACKER_FEATURE,
59 'enhancement' => TRACKER_FEATURE,
60 'task' => TRACKER_FEATURE,
60 'task' => TRACKER_FEATURE,
61 'patch' =>TRACKER_FEATURE
61 'patch' =>TRACKER_FEATURE
62 }
62 }
63
63
64 roles = Role.where(:builtin => 0).order('position ASC').all
64 roles = Role.where(:builtin => 0).order('position ASC').all
65 manager_role = roles[0]
65 manager_role = roles[0]
66 developer_role = roles[1]
66 developer_role = roles[1]
67 DEFAULT_ROLE = roles.last
67 DEFAULT_ROLE = roles.last
68 ROLE_MAPPING = {'admin' => manager_role,
68 ROLE_MAPPING = {'admin' => manager_role,
69 'developer' => developer_role
69 'developer' => developer_role
70 }
70 }
71
71
72 class ::Time
72 class ::Time
73 class << self
73 class << self
74 alias :real_now :now
74 alias :real_now :now
75 def now
75 def now
76 real_now - @fake_diff.to_i
76 real_now - @fake_diff.to_i
77 end
77 end
78 def fake(time)
78 def fake(time)
79 @fake_diff = real_now - time
79 @fake_diff = real_now - time
80 res = yield
80 res = yield
81 @fake_diff = 0
81 @fake_diff = 0
82 res
82 res
83 end
83 end
84 end
84 end
85 end
85 end
86
86
87 class TracComponent < ActiveRecord::Base
87 class TracComponent < ActiveRecord::Base
88 self.table_name = :component
88 self.table_name = :component
89 end
89 end
90
90
91 class TracMilestone < ActiveRecord::Base
91 class TracMilestone < ActiveRecord::Base
92 self.table_name = :milestone
92 self.table_name = :milestone
93 # If this attribute is set a milestone has a defined target timepoint
93 # If this attribute is set a milestone has a defined target timepoint
94 def due
94 def due
95 if read_attribute(:due) && read_attribute(:due) > 0
95 if read_attribute(:due) && read_attribute(:due) > 0
96 Time.at(read_attribute(:due)).to_date
96 Time.at(read_attribute(:due)).to_date
97 else
97 else
98 nil
98 nil
99 end
99 end
100 end
100 end
101 # This is the real timepoint at which the milestone has finished.
101 # This is the real timepoint at which the milestone has finished.
102 def completed
102 def completed
103 if read_attribute(:completed) && read_attribute(:completed) > 0
103 if read_attribute(:completed) && read_attribute(:completed) > 0
104 Time.at(read_attribute(:completed)).to_date
104 Time.at(read_attribute(:completed)).to_date
105 else
105 else
106 nil
106 nil
107 end
107 end
108 end
108 end
109
109
110 def description
110 def description
111 # Attribute is named descr in Trac v0.8.x
111 # Attribute is named descr in Trac v0.8.x
112 has_attribute?(:descr) ? read_attribute(:descr) : read_attribute(:description)
112 has_attribute?(:descr) ? read_attribute(:descr) : read_attribute(:description)
113 end
113 end
114 end
114 end
115
115
116 class TracTicketCustom < ActiveRecord::Base
116 class TracTicketCustom < ActiveRecord::Base
117 self.table_name = :ticket_custom
117 self.table_name = :ticket_custom
118 end
118 end
119
119
120 class TracAttachment < ActiveRecord::Base
120 class TracAttachment < ActiveRecord::Base
121 self.table_name = :attachment
121 self.table_name = :attachment
122 set_inheritance_column :none
122 set_inheritance_column :none
123
123
124 def time; Time.at(read_attribute(:time)) end
124 def time; Time.at(read_attribute(:time)) end
125
125
126 def original_filename
126 def original_filename
127 filename
127 filename
128 end
128 end
129
129
130 def content_type
130 def content_type
131 ''
131 ''
132 end
132 end
133
133
134 def exist?
134 def exist?
135 File.file? trac_fullpath
135 File.file? trac_fullpath
136 end
136 end
137
137
138 def open
138 def open
139 File.open("#{trac_fullpath}", 'rb') {|f|
139 File.open("#{trac_fullpath}", 'rb') {|f|
140 @file = f
140 @file = f
141 yield self
141 yield self
142 }
142 }
143 end
143 end
144
144
145 def read(*args)
145 def read(*args)
146 @file.read(*args)
146 @file.read(*args)
147 end
147 end
148
148
149 def description
149 def description
150 read_attribute(:description).to_s.slice(0,255)
150 read_attribute(:description).to_s.slice(0,255)
151 end
151 end
152
152
153 private
153 private
154 def trac_fullpath
154 def trac_fullpath
155 attachment_type = read_attribute(:type)
155 attachment_type = read_attribute(:type)
156 trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02x', x[0]) }
156 trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02x', x[0]) }
157 "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
157 "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
158 end
158 end
159 end
159 end
160
160
161 class TracTicket < ActiveRecord::Base
161 class TracTicket < ActiveRecord::Base
162 self.table_name = :ticket
162 self.table_name = :ticket
163 set_inheritance_column :none
163 set_inheritance_column :none
164
164
165 # ticket changes: only migrate status changes and comments
165 # ticket changes: only migrate status changes and comments
166 has_many :ticket_changes, :class_name => "TracTicketChange", :foreign_key => :ticket
166 has_many :ticket_changes, :class_name => "TracTicketChange", :foreign_key => :ticket
167 has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
167 has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
168
168
169 def attachments
169 def attachments
170 TracMigrate::TracAttachment.all(:conditions => ["type = 'ticket' AND id = ?", self.id.to_s])
170 TracMigrate::TracAttachment.all(:conditions => ["type = 'ticket' AND id = ?", self.id.to_s])
171 end
171 end
172
172
173 def ticket_type
173 def ticket_type
174 read_attribute(:type)
174 read_attribute(:type)
175 end
175 end
176
176
177 def summary
177 def summary
178 read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary)
178 read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary)
179 end
179 end
180
180
181 def description
181 def description
182 read_attribute(:description).blank? ? summary : read_attribute(:description)
182 read_attribute(:description).blank? ? summary : read_attribute(:description)
183 end
183 end
184
184
185 def time; Time.at(read_attribute(:time)) end
185 def time; Time.at(read_attribute(:time)) end
186 def changetime; Time.at(read_attribute(:changetime)) end
186 def changetime; Time.at(read_attribute(:changetime)) end
187 end
187 end
188
188
189 class TracTicketChange < ActiveRecord::Base
189 class TracTicketChange < ActiveRecord::Base
190 self.table_name = :ticket_change
190 self.table_name = :ticket_change
191
191
192 def self.columns
192 def self.columns
193 # Hides Trac field 'field' to prevent clash with AR field_changed? method (Rails 3.0)
193 # Hides Trac field 'field' to prevent clash with AR field_changed? method (Rails 3.0)
194 super.select {|column| column.name.to_s != 'field'}
194 super.select {|column| column.name.to_s != 'field'}
195 end
195 end
196
196
197 def time; Time.at(read_attribute(:time)) end
197 def time; Time.at(read_attribute(:time)) end
198 end
198 end
199
199
200 TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
200 TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
201 TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
201 TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
202 TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
202 TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
203 TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
203 TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
204 TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
204 TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
205 WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
205 WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
206 CamelCase TitleIndex)
206 CamelCase TitleIndex)
207
207
208 class TracWikiPage < ActiveRecord::Base
208 class TracWikiPage < ActiveRecord::Base
209 self.table_name = :wiki
209 self.table_name = :wiki
210 set_primary_key :name
210 set_primary_key :name
211
211
212 def self.columns
212 def self.columns
213 # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
213 # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
214 super.select {|column| column.name.to_s != 'readonly'}
214 super.select {|column| column.name.to_s != 'readonly'}
215 end
215 end
216
216
217 def attachments
217 def attachments
218 TracMigrate::TracAttachment.all(:conditions => ["type = 'wiki' AND id = ?", self.id.to_s])
218 TracMigrate::TracAttachment.all(:conditions => ["type = 'wiki' AND id = ?", self.id.to_s])
219 end
219 end
220
220
221 def time; Time.at(read_attribute(:time)) end
221 def time; Time.at(read_attribute(:time)) end
222 end
222 end
223
223
224 class TracPermission < ActiveRecord::Base
224 class TracPermission < ActiveRecord::Base
225 self.table_name = :permission
225 self.table_name = :permission
226 end
226 end
227
227
228 class TracSessionAttribute < ActiveRecord::Base
228 class TracSessionAttribute < ActiveRecord::Base
229 self.table_name = :session_attribute
229 self.table_name = :session_attribute
230 end
230 end
231
231
232 def self.find_or_create_user(username, project_member = false)
232 def self.find_or_create_user(username, project_member = false)
233 return User.anonymous if username.blank?
233 return User.anonymous if username.blank?
234
234
235 u = User.find_by_login(username)
235 u = User.find_by_login(username)
236 if !u
236 if !u
237 # Create a new user if not found
237 # Create a new user if not found
238 mail = username[0, User::MAIL_LENGTH_LIMIT]
238 mail = username[0, User::MAIL_LENGTH_LIMIT]
239 if mail_attr = TracSessionAttribute.find_by_sid_and_name(username, 'email')
239 if mail_attr = TracSessionAttribute.find_by_sid_and_name(username, 'email')
240 mail = mail_attr.value
240 mail = mail_attr.value
241 end
241 end
242 mail = "#{mail}@foo.bar" unless mail.include?("@")
242 mail = "#{mail}@foo.bar" unless mail.include?("@")
243
243
244 name = username
244 name = username
245 if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
245 if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
246 name = name_attr.value
246 name = name_attr.value
247 end
247 end
248 name =~ (/(.*)(\s+\w+)?/)
248 name =~ (/(.*)(\s+\w+)?/)
249 fn = $1.strip
249 fn = $1.strip
250 ln = ($2 || '-').strip
250 ln = ($2 || '-').strip
251
251
252 u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
252 u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
253 :firstname => fn[0, limit_for(User, 'firstname')],
253 :firstname => fn[0, limit_for(User, 'firstname')],
254 :lastname => ln[0, limit_for(User, 'lastname')]
254 :lastname => ln[0, limit_for(User, 'lastname')]
255
255
256 u.login = username[0, User::LOGIN_LENGTH_LIMIT].gsub(/[^a-z0-9_\-@\.]/i, '-')
256 u.login = username[0, User::LOGIN_LENGTH_LIMIT].gsub(/[^a-z0-9_\-@\.]/i, '-')
257 u.password = 'trac'
257 u.password = 'trac'
258 u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
258 u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
259 # finally, a default user is used if the new user is not valid
259 # finally, a default user is used if the new user is not valid
260 u = User.first unless u.save
260 u = User.first unless u.save
261 end
261 end
262 # Make sure he is a member of the project
262 # Make sure he is a member of the project
263 if project_member && !u.member_of?(@target_project)
263 if project_member && !u.member_of?(@target_project)
264 role = DEFAULT_ROLE
264 role = DEFAULT_ROLE
265 if u.admin
265 if u.admin
266 role = ROLE_MAPPING['admin']
266 role = ROLE_MAPPING['admin']
267 elsif TracPermission.find_by_username_and_action(username, 'developer')
267 elsif TracPermission.find_by_username_and_action(username, 'developer')
268 role = ROLE_MAPPING['developer']
268 role = ROLE_MAPPING['developer']
269 end
269 end
270 Member.create(:user => u, :project => @target_project, :roles => [role])
270 Member.create(:user => u, :project => @target_project, :roles => [role])
271 u.reload
271 u.reload
272 end
272 end
273 u
273 u
274 end
274 end
275
275
276 # Basic wiki syntax conversion
276 # Basic wiki syntax conversion
277 def self.convert_wiki_text(text)
277 def self.convert_wiki_text(text)
278 # Titles
278 # Titles
279 text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
279 text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
280 # External Links
280 # External Links
281 text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
281 text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
282 # Ticket links:
282 # Ticket links:
283 # [ticket:234 Text],[ticket:234 This is a test]
283 # [ticket:234 Text],[ticket:234 This is a test]
284 text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '"\2":/issues/show/\1')
284 text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '"\2":/issues/show/\1')
285 # ticket:1234
285 # ticket:1234
286 # #1 is working cause Redmine uses the same syntax.
286 # #1 is working cause Redmine uses the same syntax.
287 text = text.gsub(/ticket\:([^\ ]+)/, '#\1')
287 text = text.gsub(/ticket\:([^\ ]+)/, '#\1')
288 # Milestone links:
288 # Milestone links:
289 # [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)]
289 # [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)]
290 # The text "Milestone 0.1.0 (Mercury)" is not converted,
290 # The text "Milestone 0.1.0 (Mercury)" is not converted,
291 # cause Redmine's wiki does not support this.
291 # cause Redmine's wiki does not support this.
292 text = text.gsub(/\[milestone\:\"([^\"]+)\"\ (.+?)\]/, 'version:"\1"')
292 text = text.gsub(/\[milestone\:\"([^\"]+)\"\ (.+?)\]/, 'version:"\1"')
293 # [milestone:"0.1.0 Mercury"]
293 # [milestone:"0.1.0 Mercury"]
294 text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"')
294 text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"')
295 text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"')
295 text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"')
296 # milestone:0.1.0
296 # milestone:0.1.0
297 text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1')
297 text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1')
298 text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1')
298 text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1')
299 # Internal Links
299 # Internal Links
300 text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
300 text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
301 text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
301 text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
302 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
302 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
303 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
303 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
304 text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
304 text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
305 text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
305 text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
306
306
307 # Links to pages UsingJustWikiCaps
307 # Links to pages UsingJustWikiCaps
308 text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
308 text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
309 # Normalize things that were supposed to not be links
309 # Normalize things that were supposed to not be links
310 # like !NotALink
310 # like !NotALink
311 text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
311 text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
312 # Revisions links
312 # Revisions links
313 text = text.gsub(/\[(\d+)\]/, 'r\1')
313 text = text.gsub(/\[(\d+)\]/, 'r\1')
314 # Ticket number re-writing
314 # Ticket number re-writing
315 text = text.gsub(/#(\d+)/) do |s|
315 text = text.gsub(/#(\d+)/) do |s|
316 if $1.length < 10
316 if $1.length < 10
317 # TICKET_MAP[$1.to_i] ||= $1
317 # TICKET_MAP[$1.to_i] ||= $1
318 "\##{TICKET_MAP[$1.to_i] || $1}"
318 "\##{TICKET_MAP[$1.to_i] || $1}"
319 else
319 else
320 s
320 s
321 end
321 end
322 end
322 end
323 # We would like to convert the Code highlighting too
323 # We would like to convert the Code highlighting too
324 # This will go into the next line.
324 # This will go into the next line.
325 shebang_line = false
325 shebang_line = false
326 # Reguar expression for start of code
326 # Reguar expression for start of code
327 pre_re = /\{\{\{/
327 pre_re = /\{\{\{/
328 # Code hightlighing...
328 # Code hightlighing...
329 shebang_re = /^\#\!([a-z]+)/
329 shebang_re = /^\#\!([a-z]+)/
330 # Regular expression for end of code
330 # Regular expression for end of code
331 pre_end_re = /\}\}\}/
331 pre_end_re = /\}\}\}/
332
332
333 # Go through the whole text..extract it line by line
333 # Go through the whole text..extract it line by line
334 text = text.gsub(/^(.*)$/) do |line|
334 text = text.gsub(/^(.*)$/) do |line|
335 m_pre = pre_re.match(line)
335 m_pre = pre_re.match(line)
336 if m_pre
336 if m_pre
337 line = '<pre>'
337 line = '<pre>'
338 else
338 else
339 m_sl = shebang_re.match(line)
339 m_sl = shebang_re.match(line)
340 if m_sl
340 if m_sl
341 shebang_line = true
341 shebang_line = true
342 line = '<code class="' + m_sl[1] + '">'
342 line = '<code class="' + m_sl[1] + '">'
343 end
343 end
344 m_pre_end = pre_end_re.match(line)
344 m_pre_end = pre_end_re.match(line)
345 if m_pre_end
345 if m_pre_end
346 line = '</pre>'
346 line = '</pre>'
347 if shebang_line
347 if shebang_line
348 line = '</code>' + line
348 line = '</code>' + line
349 end
349 end
350 end
350 end
351 end
351 end
352 line
352 line
353 end
353 end
354
354
355 # Highlighting
355 # Highlighting
356 text = text.gsub(/'''''([^\s])/, '_*\1')
356 text = text.gsub(/'''''([^\s])/, '_*\1')
357 text = text.gsub(/([^\s])'''''/, '\1*_')
357 text = text.gsub(/([^\s])'''''/, '\1*_')
358 text = text.gsub(/'''/, '*')
358 text = text.gsub(/'''/, '*')
359 text = text.gsub(/''/, '_')
359 text = text.gsub(/''/, '_')
360 text = text.gsub(/__/, '+')
360 text = text.gsub(/__/, '+')
361 text = text.gsub(/~~/, '-')
361 text = text.gsub(/~~/, '-')
362 text = text.gsub(/`/, '@')
362 text = text.gsub(/`/, '@')
363 text = text.gsub(/,,/, '~')
363 text = text.gsub(/,,/, '~')
364 # Lists
364 # Lists
365 text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
365 text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
366
366
367 text
367 text
368 end
368 end
369
369
370 def self.migrate
370 def self.migrate
371 establish_connection
371 establish_connection
372
372
373 # Quick database test
373 # Quick database test
374 TracComponent.count
374 TracComponent.count
375
375
376 migrated_components = 0
376 migrated_components = 0
377 migrated_milestones = 0
377 migrated_milestones = 0
378 migrated_tickets = 0
378 migrated_tickets = 0
379 migrated_custom_values = 0
379 migrated_custom_values = 0
380 migrated_ticket_attachments = 0
380 migrated_ticket_attachments = 0
381 migrated_wiki_edits = 0
381 migrated_wiki_edits = 0
382 migrated_wiki_attachments = 0
382 migrated_wiki_attachments = 0
383
383
384 #Wiki system initializing...
384 #Wiki system initializing...
385 @target_project.wiki.destroy if @target_project.wiki
385 @target_project.wiki.destroy if @target_project.wiki
386 @target_project.reload
386 @target_project.reload
387 wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
387 wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
388 wiki_edit_count = 0
388 wiki_edit_count = 0
389
389
390 # Components
390 # Components
391 print "Migrating components"
391 print "Migrating components"
392 issues_category_map = {}
392 issues_category_map = {}
393 TracComponent.all.each do |component|
393 TracComponent.all.each do |component|
394 print '.'
394 print '.'
395 STDOUT.flush
395 STDOUT.flush
396 c = IssueCategory.new :project => @target_project,
396 c = IssueCategory.new :project => @target_project,
397 :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
397 :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
398 next unless c.save
398 next unless c.save
399 issues_category_map[component.name] = c
399 issues_category_map[component.name] = c
400 migrated_components += 1
400 migrated_components += 1
401 end
401 end
402 puts
402 puts
403
403
404 # Milestones
404 # Milestones
405 print "Migrating milestones"
405 print "Migrating milestones"
406 version_map = {}
406 version_map = {}
407 TracMilestone.all.each do |milestone|
407 TracMilestone.all.each do |milestone|
408 print '.'
408 print '.'
409 STDOUT.flush
409 STDOUT.flush
410 # First we try to find the wiki page...
410 # First we try to find the wiki page...
411 p = wiki.find_or_new_page(milestone.name.to_s)
411 p = wiki.find_or_new_page(milestone.name.to_s)
412 p.content = WikiContent.new(:page => p) if p.new_record?
412 p.content = WikiContent.new(:page => p) if p.new_record?
413 p.content.text = milestone.description.to_s
413 p.content.text = milestone.description.to_s
414 p.content.author = find_or_create_user('trac')
414 p.content.author = find_or_create_user('trac')
415 p.content.comments = 'Milestone'
415 p.content.comments = 'Milestone'
416 p.save
416 p.save
417
417
418 v = Version.new :project => @target_project,
418 v = Version.new :project => @target_project,
419 :name => encode(milestone.name[0, limit_for(Version, 'name')]),
419 :name => encode(milestone.name[0, limit_for(Version, 'name')]),
420 :description => nil,
420 :description => nil,
421 :wiki_page_title => milestone.name.to_s,
421 :wiki_page_title => milestone.name.to_s,
422 :effective_date => milestone.completed
422 :effective_date => milestone.completed
423
423
424 next unless v.save
424 next unless v.save
425 version_map[milestone.name] = v
425 version_map[milestone.name] = v
426 migrated_milestones += 1
426 migrated_milestones += 1
427 end
427 end
428 puts
428 puts
429
429
430 # Custom fields
430 # Custom fields
431 # TODO: read trac.ini instead
431 # TODO: read trac.ini instead
432 print "Migrating custom fields"
432 print "Migrating custom fields"
433 custom_field_map = {}
433 custom_field_map = {}
434 TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
434 TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
435 print '.'
435 print '.'
436 STDOUT.flush
436 STDOUT.flush
437 # Redmine custom field name
437 # Redmine custom field name
438 field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
438 field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
439 # Find if the custom already exists in Redmine
439 # Find if the custom already exists in Redmine
440 f = IssueCustomField.find_by_name(field_name)
440 f = IssueCustomField.find_by_name(field_name)
441 # Or create a new one
441 # Or create a new one
442 f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
442 f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
443 :field_format => 'string')
443 :field_format => 'string')
444
444
445 next if f.new_record?
445 next if f.new_record?
446 f.trackers = Tracker.all
446 f.trackers = Tracker.all
447 f.projects << @target_project
447 f.projects << @target_project
448 custom_field_map[field.name] = f
448 custom_field_map[field.name] = f
449 end
449 end
450 puts
450 puts
451
451
452 # Trac 'resolution' field as a Redmine custom field
452 # Trac 'resolution' field as a Redmine custom field
453 r = IssueCustomField.where(:name => "Resolution").first
453 r = IssueCustomField.where(:name => "Resolution").first
454 r = IssueCustomField.new(:name => 'Resolution',
454 r = IssueCustomField.new(:name => 'Resolution',
455 :field_format => 'list',
455 :field_format => 'list',
456 :is_filter => true) if r.nil?
456 :is_filter => true) if r.nil?
457 r.trackers = Tracker.all
457 r.trackers = Tracker.all
458 r.projects << @target_project
458 r.projects << @target_project
459 r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
459 r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
460 r.save!
460 r.save!
461 custom_field_map['resolution'] = r
461 custom_field_map['resolution'] = r
462
462
463 # Tickets
463 # Tickets
464 print "Migrating tickets"
464 print "Migrating tickets"
465 TracTicket.find_each(:batch_size => 200) do |ticket|
465 TracTicket.find_each(:batch_size => 200) do |ticket|
466 print '.'
466 print '.'
467 STDOUT.flush
467 STDOUT.flush
468 i = Issue.new :project => @target_project,
468 i = Issue.new :project => @target_project,
469 :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
469 :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
470 :description => convert_wiki_text(encode(ticket.description)),
470 :description => convert_wiki_text(encode(ticket.description)),
471 :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
471 :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
472 :created_on => ticket.time
472 :created_on => ticket.time
473 i.author = find_or_create_user(ticket.reporter)
473 i.author = find_or_create_user(ticket.reporter)
474 i.category = issues_category_map[ticket.component] unless ticket.component.blank?
474 i.category = issues_category_map[ticket.component] unless ticket.component.blank?
475 i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
475 i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
476 i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
476 i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
477 i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
477 i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
478 i.id = ticket.id unless Issue.exists?(ticket.id)
478 i.id = ticket.id unless Issue.exists?(ticket.id)
479 next unless Time.fake(ticket.changetime) { i.save }
479 next unless Time.fake(ticket.changetime) { i.save }
480 TICKET_MAP[ticket.id] = i.id
480 TICKET_MAP[ticket.id] = i.id
481 migrated_tickets += 1
481 migrated_tickets += 1
482
482
483 # Owner
483 # Owner
484 unless ticket.owner.blank?
484 unless ticket.owner.blank?
485 i.assigned_to = find_or_create_user(ticket.owner, true)
485 i.assigned_to = find_or_create_user(ticket.owner, true)
486 Time.fake(ticket.changetime) { i.save }
486 Time.fake(ticket.changetime) { i.save }
487 end
487 end
488
488
489 # Comments and status/resolution changes
489 # Comments and status/resolution changes
490 ticket.ticket_changes.group_by(&:time).each do |time, changeset|
490 ticket.ticket_changes.group_by(&:time).each do |time, changeset|
491 status_change = changeset.select {|change| change.field == 'status'}.first
491 status_change = changeset.select {|change| change.field == 'status'}.first
492 resolution_change = changeset.select {|change| change.field == 'resolution'}.first
492 resolution_change = changeset.select {|change| change.field == 'resolution'}.first
493 comment_change = changeset.select {|change| change.field == 'comment'}.first
493 comment_change = changeset.select {|change| change.field == 'comment'}.first
494
494
495 n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
495 n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
496 :created_on => time
496 :created_on => time
497 n.user = find_or_create_user(changeset.first.author)
497 n.user = find_or_create_user(changeset.first.author)
498 n.journalized = i
498 n.journalized = i
499 if status_change &&
499 if status_change &&
500 STATUS_MAPPING[status_change.oldvalue] &&
500 STATUS_MAPPING[status_change.oldvalue] &&
501 STATUS_MAPPING[status_change.newvalue] &&
501 STATUS_MAPPING[status_change.newvalue] &&
502 (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
502 (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
503 n.details << JournalDetail.new(:property => 'attr',
503 n.details << JournalDetail.new(:property => 'attr',
504 :prop_key => 'status_id',
504 :prop_key => 'status_id',
505 :old_value => STATUS_MAPPING[status_change.oldvalue].id,
505 :old_value => STATUS_MAPPING[status_change.oldvalue].id,
506 :value => STATUS_MAPPING[status_change.newvalue].id)
506 :value => STATUS_MAPPING[status_change.newvalue].id)
507 end
507 end
508 if resolution_change
508 if resolution_change
509 n.details << JournalDetail.new(:property => 'cf',
509 n.details << JournalDetail.new(:property => 'cf',
510 :prop_key => custom_field_map['resolution'].id,
510 :prop_key => custom_field_map['resolution'].id,
511 :old_value => resolution_change.oldvalue,
511 :old_value => resolution_change.oldvalue,
512 :value => resolution_change.newvalue)
512 :value => resolution_change.newvalue)
513 end
513 end
514 n.save unless n.details.empty? && n.notes.blank?
514 n.save unless n.details.empty? && n.notes.blank?
515 end
515 end
516
516
517 # Attachments
517 # Attachments
518 ticket.attachments.each do |attachment|
518 ticket.attachments.each do |attachment|
519 next unless attachment.exist?
519 next unless attachment.exist?
520 attachment.open {
520 attachment.open {
521 a = Attachment.new :created_on => attachment.time
521 a = Attachment.new :created_on => attachment.time
522 a.file = attachment
522 a.file = attachment
523 a.author = find_or_create_user(attachment.author)
523 a.author = find_or_create_user(attachment.author)
524 a.container = i
524 a.container = i
525 a.description = attachment.description
525 a.description = attachment.description
526 migrated_ticket_attachments += 1 if a.save
526 migrated_ticket_attachments += 1 if a.save
527 }
527 }
528 end
528 end
529
529
530 # Custom fields
530 # Custom fields
531 custom_values = ticket.customs.inject({}) do |h, custom|
531 custom_values = ticket.customs.inject({}) do |h, custom|
532 if custom_field = custom_field_map[custom.name]
532 if custom_field = custom_field_map[custom.name]
533 h[custom_field.id] = custom.value
533 h[custom_field.id] = custom.value
534 migrated_custom_values += 1
534 migrated_custom_values += 1
535 end
535 end
536 h
536 h
537 end
537 end
538 if custom_field_map['resolution'] && !ticket.resolution.blank?
538 if custom_field_map['resolution'] && !ticket.resolution.blank?
539 custom_values[custom_field_map['resolution'].id] = ticket.resolution
539 custom_values[custom_field_map['resolution'].id] = ticket.resolution
540 end
540 end
541 i.custom_field_values = custom_values
541 i.custom_field_values = custom_values
542 i.save_custom_field_values
542 i.save_custom_field_values
543 end
543 end
544
544
545 # update issue id sequence if needed (postgresql)
545 # update issue id sequence if needed (postgresql)
546 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
546 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
547 puts
547 puts
548
548
549 # Wiki
549 # Wiki
550 print "Migrating wiki"
550 print "Migrating wiki"
551 if wiki.save
551 if wiki.save
552 TracWikiPage.order('name, version').all.each do |page|
552 TracWikiPage.order('name, version').all.each do |page|
553 # Do not migrate Trac manual wiki pages
553 # Do not migrate Trac manual wiki pages
554 next if TRAC_WIKI_PAGES.include?(page.name)
554 next if TRAC_WIKI_PAGES.include?(page.name)
555 wiki_edit_count += 1
555 wiki_edit_count += 1
556 print '.'
556 print '.'
557 STDOUT.flush
557 STDOUT.flush
558 p = wiki.find_or_new_page(page.name)
558 p = wiki.find_or_new_page(page.name)
559 p.content = WikiContent.new(:page => p) if p.new_record?
559 p.content = WikiContent.new(:page => p) if p.new_record?
560 p.content.text = page.text
560 p.content.text = page.text
561 p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
561 p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
562 p.content.comments = page.comment
562 p.content.comments = page.comment
563 Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
563 Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
564
564
565 next if p.content.new_record?
565 next if p.content.new_record?
566 migrated_wiki_edits += 1
566 migrated_wiki_edits += 1
567
567
568 # Attachments
568 # Attachments
569 page.attachments.each do |attachment|
569 page.attachments.each do |attachment|
570 next unless attachment.exist?
570 next unless attachment.exist?
571 next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
571 next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
572 attachment.open {
572 attachment.open {
573 a = Attachment.new :created_on => attachment.time
573 a = Attachment.new :created_on => attachment.time
574 a.file = attachment
574 a.file = attachment
575 a.author = find_or_create_user(attachment.author)
575 a.author = find_or_create_user(attachment.author)
576 a.description = attachment.description
576 a.description = attachment.description
577 a.container = p
577 a.container = p
578 migrated_wiki_attachments += 1 if a.save
578 migrated_wiki_attachments += 1 if a.save
579 }
579 }
580 end
580 end
581 end
581 end
582
582
583 wiki.reload
583 wiki.reload
584 wiki.pages.each do |page|
584 wiki.pages.each do |page|
585 page.content.text = convert_wiki_text(page.content.text)
585 page.content.text = convert_wiki_text(page.content.text)
586 Time.fake(page.content.updated_on) { page.content.save }
586 Time.fake(page.content.updated_on) { page.content.save }
587 end
587 end
588 end
588 end
589 puts
589 puts
590
590
591 puts
591 puts
592 puts "Components: #{migrated_components}/#{TracComponent.count}"
592 puts "Components: #{migrated_components}/#{TracComponent.count}"
593 puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
593 puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
594 puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
594 puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
595 puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
595 puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
596 puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
596 puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
597 puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
597 puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
598 puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
598 puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
599 end
599 end
600
600
601 def self.limit_for(klass, attribute)
601 def self.limit_for(klass, attribute)
602 klass.columns_hash[attribute.to_s].limit
602 klass.columns_hash[attribute.to_s].limit
603 end
603 end
604
604
605 def self.encoding(charset)
605 def self.encoding(charset)
606 @ic = Iconv.new('UTF-8', charset)
606 @ic = Iconv.new('UTF-8', charset)
607 rescue Iconv::InvalidEncoding
607 rescue Iconv::InvalidEncoding
608 puts "Invalid encoding!"
608 puts "Invalid encoding!"
609 return false
609 return false
610 end
610 end
611
611
612 def self.set_trac_directory(path)
612 def self.set_trac_directory(path)
613 @@trac_directory = path
613 @@trac_directory = path
614 raise "This directory doesn't exist!" unless File.directory?(path)
614 raise "This directory doesn't exist!" unless File.directory?(path)
615 raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
615 raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
616 @@trac_directory
616 @@trac_directory
617 rescue Exception => e
617 rescue Exception => e
618 puts e
618 puts e
619 return false
619 return false
620 end
620 end
621
621
622 def self.trac_directory
622 def self.trac_directory
623 @@trac_directory
623 @@trac_directory
624 end
624 end
625
625
626 def self.set_trac_adapter(adapter)
626 def self.set_trac_adapter(adapter)
627 return false if adapter.blank?
627 return false if adapter.blank?
628 raise "Unknown adapter: #{adapter}!" unless %w(sqlite3 mysql postgresql).include?(adapter)
628 raise "Unknown adapter: #{adapter}!" unless %w(sqlite3 mysql postgresql).include?(adapter)
629 # If adapter is sqlite or sqlite3, make sure that trac.db exists
629 # If adapter is sqlite or sqlite3, make sure that trac.db exists
630 raise "#{trac_db_path} doesn't exist!" if %w(sqlite3).include?(adapter) && !File.exist?(trac_db_path)
630 raise "#{trac_db_path} doesn't exist!" if %w(sqlite3).include?(adapter) && !File.exist?(trac_db_path)
631 @@trac_adapter = adapter
631 @@trac_adapter = adapter
632 rescue Exception => e
632 rescue Exception => e
633 puts e
633 puts e
634 return false
634 return false
635 end
635 end
636
636
637 def self.set_trac_db_host(host)
637 def self.set_trac_db_host(host)
638 return nil if host.blank?
638 return nil if host.blank?
639 @@trac_db_host = host
639 @@trac_db_host = host
640 end
640 end
641
641
642 def self.set_trac_db_port(port)
642 def self.set_trac_db_port(port)
643 return nil if port.to_i == 0
643 return nil if port.to_i == 0
644 @@trac_db_port = port.to_i
644 @@trac_db_port = port.to_i
645 end
645 end
646
646
647 def self.set_trac_db_name(name)
647 def self.set_trac_db_name(name)
648 return nil if name.blank?
648 return nil if name.blank?
649 @@trac_db_name = name
649 @@trac_db_name = name
650 end
650 end
651
651
652 def self.set_trac_db_username(username)
652 def self.set_trac_db_username(username)
653 @@trac_db_username = username
653 @@trac_db_username = username
654 end
654 end
655
655
656 def self.set_trac_db_password(password)
656 def self.set_trac_db_password(password)
657 @@trac_db_password = password
657 @@trac_db_password = password
658 end
658 end
659
659
660 def self.set_trac_db_schema(schema)
660 def self.set_trac_db_schema(schema)
661 @@trac_db_schema = schema
661 @@trac_db_schema = schema
662 end
662 end
663
663
664 mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password
664 mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password
665
665
666 def self.trac_db_path; "#{trac_directory}/db/trac.db" end
666 def self.trac_db_path; "#{trac_directory}/db/trac.db" end
667 def self.trac_attachments_directory; "#{trac_directory}/attachments" end
667 def self.trac_attachments_directory; "#{trac_directory}/attachments" end
668
668
669 def self.target_project_identifier(identifier)
669 def self.target_project_identifier(identifier)
670 project = Project.find_by_identifier(identifier)
670 project = Project.find_by_identifier(identifier)
671 if !project
671 if !project
672 # create the target project
672 # create the target project
673 project = Project.new :name => identifier.humanize,
673 project = Project.new :name => identifier.humanize,
674 :description => ''
674 :description => ''
675 project.identifier = identifier
675 project.identifier = identifier
676 puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
676 puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
677 # enable issues and wiki for the created project
677 # enable issues and wiki for the created project
678 project.enabled_module_names = ['issue_tracking', 'wiki']
678 project.enabled_module_names = ['issue_tracking', 'wiki']
679 else
679 else
680 puts
680 puts
681 puts "This project already exists in your Redmine database."
681 puts "This project already exists in your Redmine database."
682 print "Are you sure you want to append data to this project ? [Y/n] "
682 print "Are you sure you want to append data to this project ? [Y/n] "
683 STDOUT.flush
683 STDOUT.flush
684 exit if STDIN.gets.match(/^n$/i)
684 exit if STDIN.gets.match(/^n$/i)
685 end
685 end
686 project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
686 project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
687 project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
687 project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
688 @target_project = project.new_record? ? nil : project
688 @target_project = project.new_record? ? nil : project
689 @target_project.reload
689 @target_project.reload
690 end
690 end
691
691
692 def self.connection_params
692 def self.connection_params
693 if trac_adapter == 'sqlite3'
693 if trac_adapter == 'sqlite3'
694 {:adapter => 'sqlite3',
694 {:adapter => 'sqlite3',
695 :database => trac_db_path}
695 :database => trac_db_path}
696 else
696 else
697 {:adapter => trac_adapter,
697 {:adapter => trac_adapter,
698 :database => trac_db_name,
698 :database => trac_db_name,
699 :host => trac_db_host,
699 :host => trac_db_host,
700 :port => trac_db_port,
700 :port => trac_db_port,
701 :username => trac_db_username,
701 :username => trac_db_username,
702 :password => trac_db_password,
702 :password => trac_db_password,
703 :schema_search_path => trac_db_schema
703 :schema_search_path => trac_db_schema
704 }
704 }
705 end
705 end
706 end
706 end
707
707
708 def self.establish_connection
708 def self.establish_connection
709 constants.each do |const|
709 constants.each do |const|
710 klass = const_get(const)
710 klass = const_get(const)
711 next unless klass.respond_to? 'establish_connection'
711 next unless klass.respond_to? 'establish_connection'
712 klass.establish_connection connection_params
712 klass.establish_connection connection_params
713 end
713 end
714 end
714 end
715
715
716 private
716 private
717 def self.encode(text)
717 def self.encode(text)
718 @ic.iconv text
718 @ic.iconv text
719 rescue
719 rescue
720 text
720 text
721 end
721 end
722 end
722 end
723
723
724 puts
724 puts
725 if Redmine::DefaultData::Loader.no_data?
725 if Redmine::DefaultData::Loader.no_data?
726 puts "Redmine configuration need to be loaded before importing data."
726 puts "Redmine configuration need to be loaded before importing data."
727 puts "Please, run this first:"
727 puts "Please, run this first:"
728 puts
728 puts
729 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
729 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
730 exit
730 exit
731 end
731 end
732
732
733 puts "WARNING: a new project will be added to Redmine during this process."
733 puts "WARNING: a new project will be added to Redmine during this process."
734 print "Are you sure you want to continue ? [y/N] "
734 print "Are you sure you want to continue ? [y/N] "
735 STDOUT.flush
735 STDOUT.flush
736 break unless STDIN.gets.match(/^y$/i)
736 break unless STDIN.gets.match(/^y$/i)
737 puts
737 puts
738
738
739 def prompt(text, options = {}, &block)
739 def prompt(text, options = {}, &block)
740 default = options[:default] || ''
740 default = options[:default] || ''
741 while true
741 while true
742 print "#{text} [#{default}]: "
742 print "#{text} [#{default}]: "
743 STDOUT.flush
743 STDOUT.flush
744 value = STDIN.gets.chomp!
744 value = STDIN.gets.chomp!
745 value = default if value.blank?
745 value = default if value.blank?
746 break if yield value
746 break if yield value
747 end
747 end
748 end
748 end
749
749
750 DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
750 DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
751
751
752 prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
752 prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
753 prompt('Trac database adapter (sqlite3, mysql2, postgresql)', :default => 'sqlite3') {|adapter| TracMigrate.set_trac_adapter adapter}
753 prompt('Trac database adapter (sqlite3, mysql2, postgresql)', :default => 'sqlite3') {|adapter| TracMigrate.set_trac_adapter adapter}
754 unless %w(sqlite3).include?(TracMigrate.trac_adapter)
754 unless %w(sqlite3).include?(TracMigrate.trac_adapter)
755 prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
755 prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
756 prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
756 prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
757 prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
757 prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
758 prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
758 prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
759 prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
759 prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
760 prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
760 prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
761 end
761 end
762 prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
762 prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
763 prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
763 prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
764 puts
764 puts
765
765
766 # Turn off email notifications
766 # Turn off email notifications
767 Setting.notified_events = []
767 Setting.notified_events = []
768
768
769 TracMigrate.migrate
769 TracMigrate.migrate
770 end
770 end
771 end
771 end
772
772
@@ -1,185 +1,185
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 begin
20 begin
21 require 'mocha'
21 require 'mocha'
22 rescue
22 rescue
23 # Won't run some tests
23 # Won't run some tests
24 end
24 end
25
25
26 class AccountTest < ActionController::IntegrationTest
26 class AccountTest < ActionController::IntegrationTest
27 fixtures :users, :roles
27 fixtures :users, :roles
28
28
29 # Replace this with your real tests.
29 # Replace this with your real tests.
30 def test_login
30 def test_login
31 get "my/page"
31 get "my/page"
32 assert_redirected_to "/login?back_url=http%3A%2F%2Fwww.example.com%2Fmy%2Fpage"
32 assert_redirected_to "/login?back_url=http%3A%2F%2Fwww.example.com%2Fmy%2Fpage"
33 log_user('jsmith', 'jsmith')
33 log_user('jsmith', 'jsmith')
34
34
35 get "my/account"
35 get "my/account"
36 assert_response :success
36 assert_response :success
37 assert_template "my/account"
37 assert_template "my/account"
38 end
38 end
39
39
40 def test_autologin
40 def test_autologin
41 user = User.find(1)
41 user = User.find(1)
42 Setting.autologin = "7"
42 Setting.autologin = "7"
43 Token.delete_all
43 Token.delete_all
44
44
45 # User logs in with 'autologin' checked
45 # User logs in with 'autologin' checked
46 post '/login', :username => user.login, :password => 'admin', :autologin => 1
46 post '/login', :username => user.login, :password => 'admin', :autologin => 1
47 assert_redirected_to '/my/page'
47 assert_redirected_to '/my/page'
48 token = Token.find :first
48 token = Token.first
49 assert_not_nil token
49 assert_not_nil token
50 assert_equal user, token.user
50 assert_equal user, token.user
51 assert_equal 'autologin', token.action
51 assert_equal 'autologin', token.action
52 assert_equal user.id, session[:user_id]
52 assert_equal user.id, session[:user_id]
53 assert_equal token.value, cookies['autologin']
53 assert_equal token.value, cookies['autologin']
54
54
55 # Session is cleared
55 # Session is cleared
56 reset!
56 reset!
57 User.current = nil
57 User.current = nil
58 # Clears user's last login timestamp
58 # Clears user's last login timestamp
59 user.update_attribute :last_login_on, nil
59 user.update_attribute :last_login_on, nil
60 assert_nil user.reload.last_login_on
60 assert_nil user.reload.last_login_on
61
61
62 # User comes back with his autologin cookie
62 # User comes back with his autologin cookie
63 cookies[:autologin] = token.value
63 cookies[:autologin] = token.value
64 get '/my/page'
64 get '/my/page'
65 assert_response :success
65 assert_response :success
66 assert_template 'my/page'
66 assert_template 'my/page'
67 assert_equal user.id, session[:user_id]
67 assert_equal user.id, session[:user_id]
68 assert_not_nil user.reload.last_login_on
68 assert_not_nil user.reload.last_login_on
69 end
69 end
70
70
71 def test_lost_password
71 def test_lost_password
72 Token.delete_all
72 Token.delete_all
73
73
74 get "account/lost_password"
74 get "account/lost_password"
75 assert_response :success
75 assert_response :success
76 assert_template "account/lost_password"
76 assert_template "account/lost_password"
77 assert_select 'input[name=mail]'
77 assert_select 'input[name=mail]'
78
78
79 post "account/lost_password", :mail => 'jSmith@somenet.foo'
79 post "account/lost_password", :mail => 'jSmith@somenet.foo'
80 assert_redirected_to "/login"
80 assert_redirected_to "/login"
81
81
82 token = Token.first
82 token = Token.first
83 assert_equal 'recovery', token.action
83 assert_equal 'recovery', token.action
84 assert_equal 'jsmith@somenet.foo', token.user.mail
84 assert_equal 'jsmith@somenet.foo', token.user.mail
85 assert !token.expired?
85 assert !token.expired?
86
86
87 get "account/lost_password", :token => token.value
87 get "account/lost_password", :token => token.value
88 assert_response :success
88 assert_response :success
89 assert_template "account/password_recovery"
89 assert_template "account/password_recovery"
90 assert_select 'input[type=hidden][name=token][value=?]', token.value
90 assert_select 'input[type=hidden][name=token][value=?]', token.value
91 assert_select 'input[name=new_password]'
91 assert_select 'input[name=new_password]'
92 assert_select 'input[name=new_password_confirmation]'
92 assert_select 'input[name=new_password_confirmation]'
93
93
94 post "account/lost_password", :token => token.value, :new_password => 'newpass123', :new_password_confirmation => 'newpass123'
94 post "account/lost_password", :token => token.value, :new_password => 'newpass123', :new_password_confirmation => 'newpass123'
95 assert_redirected_to "/login"
95 assert_redirected_to "/login"
96 assert_equal 'Password was successfully updated.', flash[:notice]
96 assert_equal 'Password was successfully updated.', flash[:notice]
97
97
98 log_user('jsmith', 'newpass123')
98 log_user('jsmith', 'newpass123')
99 assert_equal 0, Token.count
99 assert_equal 0, Token.count
100 end
100 end
101
101
102 def test_register_with_automatic_activation
102 def test_register_with_automatic_activation
103 Setting.self_registration = '3'
103 Setting.self_registration = '3'
104
104
105 get 'account/register'
105 get 'account/register'
106 assert_response :success
106 assert_response :success
107 assert_template 'account/register'
107 assert_template 'account/register'
108
108
109 post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar",
109 post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar",
110 :password => "newpass123", :password_confirmation => "newpass123"}
110 :password => "newpass123", :password_confirmation => "newpass123"}
111 assert_redirected_to '/my/account'
111 assert_redirected_to '/my/account'
112 follow_redirect!
112 follow_redirect!
113 assert_response :success
113 assert_response :success
114 assert_template 'my/account'
114 assert_template 'my/account'
115
115
116 user = User.find_by_login('newuser')
116 user = User.find_by_login('newuser')
117 assert_not_nil user
117 assert_not_nil user
118 assert user.active?
118 assert user.active?
119 assert_not_nil user.last_login_on
119 assert_not_nil user.last_login_on
120 end
120 end
121
121
122 def test_register_with_manual_activation
122 def test_register_with_manual_activation
123 Setting.self_registration = '2'
123 Setting.self_registration = '2'
124
124
125 post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar",
125 post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar",
126 :password => "newpass123", :password_confirmation => "newpass123"}
126 :password => "newpass123", :password_confirmation => "newpass123"}
127 assert_redirected_to '/login'
127 assert_redirected_to '/login'
128 assert !User.find_by_login('newuser').active?
128 assert !User.find_by_login('newuser').active?
129 end
129 end
130
130
131 def test_register_with_email_activation
131 def test_register_with_email_activation
132 Setting.self_registration = '1'
132 Setting.self_registration = '1'
133 Token.delete_all
133 Token.delete_all
134
134
135 post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar",
135 post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar",
136 :password => "newpass123", :password_confirmation => "newpass123"}
136 :password => "newpass123", :password_confirmation => "newpass123"}
137 assert_redirected_to '/login'
137 assert_redirected_to '/login'
138 assert !User.find_by_login('newuser').active?
138 assert !User.find_by_login('newuser').active?
139
139
140 token = Token.first
140 token = Token.first
141 assert_equal 'register', token.action
141 assert_equal 'register', token.action
142 assert_equal 'newuser@foo.bar', token.user.mail
142 assert_equal 'newuser@foo.bar', token.user.mail
143 assert !token.expired?
143 assert !token.expired?
144
144
145 get 'account/activate', :token => token.value
145 get 'account/activate', :token => token.value
146 assert_redirected_to '/login'
146 assert_redirected_to '/login'
147 log_user('newuser', 'newpass123')
147 log_user('newuser', 'newpass123')
148 end
148 end
149
149
150 def test_onthefly_registration
150 def test_onthefly_registration
151 # disable registration
151 # disable registration
152 Setting.self_registration = '0'
152 Setting.self_registration = '0'
153 AuthSource.expects(:authenticate).returns({:login => 'foo', :firstname => 'Foo', :lastname => 'Smith', :mail => 'foo@bar.com', :auth_source_id => 66})
153 AuthSource.expects(:authenticate).returns({:login => 'foo', :firstname => 'Foo', :lastname => 'Smith', :mail => 'foo@bar.com', :auth_source_id => 66})
154
154
155 post '/login', :username => 'foo', :password => 'bar'
155 post '/login', :username => 'foo', :password => 'bar'
156 assert_redirected_to '/my/page'
156 assert_redirected_to '/my/page'
157
157
158 user = User.find_by_login('foo')
158 user = User.find_by_login('foo')
159 assert user.is_a?(User)
159 assert user.is_a?(User)
160 assert_equal 66, user.auth_source_id
160 assert_equal 66, user.auth_source_id
161 assert user.hashed_password.blank?
161 assert user.hashed_password.blank?
162 end
162 end
163
163
164 def test_onthefly_registration_with_invalid_attributes
164 def test_onthefly_registration_with_invalid_attributes
165 # disable registration
165 # disable registration
166 Setting.self_registration = '0'
166 Setting.self_registration = '0'
167 AuthSource.expects(:authenticate).returns({:login => 'foo', :lastname => 'Smith', :auth_source_id => 66})
167 AuthSource.expects(:authenticate).returns({:login => 'foo', :lastname => 'Smith', :auth_source_id => 66})
168
168
169 post '/login', :username => 'foo', :password => 'bar'
169 post '/login', :username => 'foo', :password => 'bar'
170 assert_response :success
170 assert_response :success
171 assert_template 'account/register'
171 assert_template 'account/register'
172 assert_tag :input, :attributes => { :name => 'user[firstname]', :value => '' }
172 assert_tag :input, :attributes => { :name => 'user[firstname]', :value => '' }
173 assert_tag :input, :attributes => { :name => 'user[lastname]', :value => 'Smith' }
173 assert_tag :input, :attributes => { :name => 'user[lastname]', :value => 'Smith' }
174 assert_no_tag :input, :attributes => { :name => 'user[login]' }
174 assert_no_tag :input, :attributes => { :name => 'user[login]' }
175 assert_no_tag :input, :attributes => { :name => 'user[password]' }
175 assert_no_tag :input, :attributes => { :name => 'user[password]' }
176
176
177 post 'account/register', :user => {:firstname => 'Foo', :lastname => 'Smith', :mail => 'foo@bar.com'}
177 post 'account/register', :user => {:firstname => 'Foo', :lastname => 'Smith', :mail => 'foo@bar.com'}
178 assert_redirected_to '/my/account'
178 assert_redirected_to '/my/account'
179
179
180 user = User.find_by_login('foo')
180 user = User.find_by_login('foo')
181 assert user.is_a?(User)
181 assert user.is_a?(User)
182 assert_equal 66, user.auth_source_id
182 assert_equal 66, user.auth_source_id
183 assert user.hashed_password.blank?
183 assert user.hashed_password.blank?
184 end
184 end
185 end
185 end
@@ -1,1938 +1,1938
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class IssueTest < ActiveSupport::TestCase
20 class IssueTest < ActiveSupport::TestCase
21 fixtures :projects, :users, :members, :member_roles, :roles,
21 fixtures :projects, :users, :members, :member_roles, :roles,
22 :groups_users,
22 :groups_users,
23 :trackers, :projects_trackers,
23 :trackers, :projects_trackers,
24 :enabled_modules,
24 :enabled_modules,
25 :versions,
25 :versions,
26 :issue_statuses, :issue_categories, :issue_relations, :workflows,
26 :issue_statuses, :issue_categories, :issue_relations, :workflows,
27 :enumerations,
27 :enumerations,
28 :issues, :journals, :journal_details,
28 :issues, :journals, :journal_details,
29 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
29 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
30 :time_entries
30 :time_entries
31
31
32 include Redmine::I18n
32 include Redmine::I18n
33
33
34 def teardown
34 def teardown
35 User.current = nil
35 User.current = nil
36 end
36 end
37
37
38 def test_create
38 def test_create
39 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
39 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
40 :status_id => 1, :priority => IssuePriority.all.first,
40 :status_id => 1, :priority => IssuePriority.all.first,
41 :subject => 'test_create',
41 :subject => 'test_create',
42 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
42 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
43 assert issue.save
43 assert issue.save
44 issue.reload
44 issue.reload
45 assert_equal 1.5, issue.estimated_hours
45 assert_equal 1.5, issue.estimated_hours
46 end
46 end
47
47
48 def test_create_minimal
48 def test_create_minimal
49 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
49 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
50 :status_id => 1, :priority => IssuePriority.all.first,
50 :status_id => 1, :priority => IssuePriority.all.first,
51 :subject => 'test_create')
51 :subject => 'test_create')
52 assert issue.save
52 assert issue.save
53 assert issue.description.nil?
53 assert issue.description.nil?
54 assert_nil issue.estimated_hours
54 assert_nil issue.estimated_hours
55 end
55 end
56
56
57 def test_start_date_format_should_be_validated
57 def test_start_date_format_should_be_validated
58 set_language_if_valid 'en'
58 set_language_if_valid 'en'
59 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
59 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
60 issue = Issue.new(:start_date => invalid_date)
60 issue = Issue.new(:start_date => invalid_date)
61 assert !issue.valid?
61 assert !issue.valid?
62 assert_include 'Start date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
62 assert_include 'Start date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
63 end
63 end
64 end
64 end
65
65
66 def test_due_date_format_should_be_validated
66 def test_due_date_format_should_be_validated
67 set_language_if_valid 'en'
67 set_language_if_valid 'en'
68 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
68 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
69 issue = Issue.new(:due_date => invalid_date)
69 issue = Issue.new(:due_date => invalid_date)
70 assert !issue.valid?
70 assert !issue.valid?
71 assert_include 'Due date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
71 assert_include 'Due date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
72 end
72 end
73 end
73 end
74
74
75 def test_due_date_lesser_than_start_date_should_not_validate
75 def test_due_date_lesser_than_start_date_should_not_validate
76 set_language_if_valid 'en'
76 set_language_if_valid 'en'
77 issue = Issue.new(:start_date => '2012-10-06', :due_date => '2012-10-02')
77 issue = Issue.new(:start_date => '2012-10-06', :due_date => '2012-10-02')
78 assert !issue.valid?
78 assert !issue.valid?
79 assert_include 'Due date must be greater than start date', issue.errors.full_messages
79 assert_include 'Due date must be greater than start date', issue.errors.full_messages
80 end
80 end
81
81
82 def test_create_with_required_custom_field
82 def test_create_with_required_custom_field
83 set_language_if_valid 'en'
83 set_language_if_valid 'en'
84 field = IssueCustomField.find_by_name('Database')
84 field = IssueCustomField.find_by_name('Database')
85 field.update_attribute(:is_required, true)
85 field.update_attribute(:is_required, true)
86
86
87 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
87 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
88 :status_id => 1, :subject => 'test_create',
88 :status_id => 1, :subject => 'test_create',
89 :description => 'IssueTest#test_create_with_required_custom_field')
89 :description => 'IssueTest#test_create_with_required_custom_field')
90 assert issue.available_custom_fields.include?(field)
90 assert issue.available_custom_fields.include?(field)
91 # No value for the custom field
91 # No value for the custom field
92 assert !issue.save
92 assert !issue.save
93 assert_equal ["Database can't be blank"], issue.errors.full_messages
93 assert_equal ["Database can't be blank"], issue.errors.full_messages
94 # Blank value
94 # Blank value
95 issue.custom_field_values = { field.id => '' }
95 issue.custom_field_values = { field.id => '' }
96 assert !issue.save
96 assert !issue.save
97 assert_equal ["Database can't be blank"], issue.errors.full_messages
97 assert_equal ["Database can't be blank"], issue.errors.full_messages
98 # Invalid value
98 # Invalid value
99 issue.custom_field_values = { field.id => 'SQLServer' }
99 issue.custom_field_values = { field.id => 'SQLServer' }
100 assert !issue.save
100 assert !issue.save
101 assert_equal ["Database is not included in the list"], issue.errors.full_messages
101 assert_equal ["Database is not included in the list"], issue.errors.full_messages
102 # Valid value
102 # Valid value
103 issue.custom_field_values = { field.id => 'PostgreSQL' }
103 issue.custom_field_values = { field.id => 'PostgreSQL' }
104 assert issue.save
104 assert issue.save
105 issue.reload
105 issue.reload
106 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
106 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
107 end
107 end
108
108
109 def test_create_with_group_assignment
109 def test_create_with_group_assignment
110 with_settings :issue_group_assignment => '1' do
110 with_settings :issue_group_assignment => '1' do
111 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
111 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
112 :subject => 'Group assignment',
112 :subject => 'Group assignment',
113 :assigned_to_id => 11).save
113 :assigned_to_id => 11).save
114 issue = Issue.first(:order => 'id DESC')
114 issue = Issue.first(:order => 'id DESC')
115 assert_kind_of Group, issue.assigned_to
115 assert_kind_of Group, issue.assigned_to
116 assert_equal Group.find(11), issue.assigned_to
116 assert_equal Group.find(11), issue.assigned_to
117 end
117 end
118 end
118 end
119
119
120 def test_create_with_parent_issue_id
120 def test_create_with_parent_issue_id
121 issue = Issue.new(:project_id => 1, :tracker_id => 1,
121 issue = Issue.new(:project_id => 1, :tracker_id => 1,
122 :author_id => 1, :subject => 'Group assignment',
122 :author_id => 1, :subject => 'Group assignment',
123 :parent_issue_id => 1)
123 :parent_issue_id => 1)
124 assert_save issue
124 assert_save issue
125 assert_equal 1, issue.parent_issue_id
125 assert_equal 1, issue.parent_issue_id
126 assert_equal Issue.find(1), issue.parent
126 assert_equal Issue.find(1), issue.parent
127 end
127 end
128
128
129 def test_create_with_sharp_parent_issue_id
129 def test_create_with_sharp_parent_issue_id
130 issue = Issue.new(:project_id => 1, :tracker_id => 1,
130 issue = Issue.new(:project_id => 1, :tracker_id => 1,
131 :author_id => 1, :subject => 'Group assignment',
131 :author_id => 1, :subject => 'Group assignment',
132 :parent_issue_id => "#1")
132 :parent_issue_id => "#1")
133 assert_save issue
133 assert_save issue
134 assert_equal 1, issue.parent_issue_id
134 assert_equal 1, issue.parent_issue_id
135 assert_equal Issue.find(1), issue.parent
135 assert_equal Issue.find(1), issue.parent
136 end
136 end
137
137
138 def test_create_with_invalid_parent_issue_id
138 def test_create_with_invalid_parent_issue_id
139 set_language_if_valid 'en'
139 set_language_if_valid 'en'
140 issue = Issue.new(:project_id => 1, :tracker_id => 1,
140 issue = Issue.new(:project_id => 1, :tracker_id => 1,
141 :author_id => 1, :subject => 'Group assignment',
141 :author_id => 1, :subject => 'Group assignment',
142 :parent_issue_id => '01ABC')
142 :parent_issue_id => '01ABC')
143 assert !issue.save
143 assert !issue.save
144 assert_equal '01ABC', issue.parent_issue_id
144 assert_equal '01ABC', issue.parent_issue_id
145 assert_include 'Parent task is invalid', issue.errors.full_messages
145 assert_include 'Parent task is invalid', issue.errors.full_messages
146 end
146 end
147
147
148 def test_create_with_invalid_sharp_parent_issue_id
148 def test_create_with_invalid_sharp_parent_issue_id
149 set_language_if_valid 'en'
149 set_language_if_valid 'en'
150 issue = Issue.new(:project_id => 1, :tracker_id => 1,
150 issue = Issue.new(:project_id => 1, :tracker_id => 1,
151 :author_id => 1, :subject => 'Group assignment',
151 :author_id => 1, :subject => 'Group assignment',
152 :parent_issue_id => '#01ABC')
152 :parent_issue_id => '#01ABC')
153 assert !issue.save
153 assert !issue.save
154 assert_equal '#01ABC', issue.parent_issue_id
154 assert_equal '#01ABC', issue.parent_issue_id
155 assert_include 'Parent task is invalid', issue.errors.full_messages
155 assert_include 'Parent task is invalid', issue.errors.full_messages
156 end
156 end
157
157
158 def assert_visibility_match(user, issues)
158 def assert_visibility_match(user, issues)
159 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
159 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
160 end
160 end
161
161
162 def test_visible_scope_for_anonymous
162 def test_visible_scope_for_anonymous
163 # Anonymous user should see issues of public projects only
163 # Anonymous user should see issues of public projects only
164 issues = Issue.visible(User.anonymous).all
164 issues = Issue.visible(User.anonymous).all
165 assert issues.any?
165 assert issues.any?
166 assert_nil issues.detect {|issue| !issue.project.is_public?}
166 assert_nil issues.detect {|issue| !issue.project.is_public?}
167 assert_nil issues.detect {|issue| issue.is_private?}
167 assert_nil issues.detect {|issue| issue.is_private?}
168 assert_visibility_match User.anonymous, issues
168 assert_visibility_match User.anonymous, issues
169 end
169 end
170
170
171 def test_visible_scope_for_anonymous_without_view_issues_permissions
171 def test_visible_scope_for_anonymous_without_view_issues_permissions
172 # Anonymous user should not see issues without permission
172 # Anonymous user should not see issues without permission
173 Role.anonymous.remove_permission!(:view_issues)
173 Role.anonymous.remove_permission!(:view_issues)
174 issues = Issue.visible(User.anonymous).all
174 issues = Issue.visible(User.anonymous).all
175 assert issues.empty?
175 assert issues.empty?
176 assert_visibility_match User.anonymous, issues
176 assert_visibility_match User.anonymous, issues
177 end
177 end
178
178
179 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_default
179 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_default
180 assert Role.anonymous.update_attribute(:issues_visibility, 'default')
180 assert Role.anonymous.update_attribute(:issues_visibility, 'default')
181 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
181 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
182 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
182 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
183 assert !issue.visible?(User.anonymous)
183 assert !issue.visible?(User.anonymous)
184 end
184 end
185
185
186 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_own
186 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_own
187 assert Role.anonymous.update_attribute(:issues_visibility, 'own')
187 assert Role.anonymous.update_attribute(:issues_visibility, 'own')
188 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
188 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
189 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
189 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
190 assert !issue.visible?(User.anonymous)
190 assert !issue.visible?(User.anonymous)
191 end
191 end
192
192
193 def test_visible_scope_for_non_member
193 def test_visible_scope_for_non_member
194 user = User.find(9)
194 user = User.find(9)
195 assert user.projects.empty?
195 assert user.projects.empty?
196 # Non member user should see issues of public projects only
196 # Non member user should see issues of public projects only
197 issues = Issue.visible(user).all
197 issues = Issue.visible(user).all
198 assert issues.any?
198 assert issues.any?
199 assert_nil issues.detect {|issue| !issue.project.is_public?}
199 assert_nil issues.detect {|issue| !issue.project.is_public?}
200 assert_nil issues.detect {|issue| issue.is_private?}
200 assert_nil issues.detect {|issue| issue.is_private?}
201 assert_visibility_match user, issues
201 assert_visibility_match user, issues
202 end
202 end
203
203
204 def test_visible_scope_for_non_member_with_own_issues_visibility
204 def test_visible_scope_for_non_member_with_own_issues_visibility
205 Role.non_member.update_attribute :issues_visibility, 'own'
205 Role.non_member.update_attribute :issues_visibility, 'own'
206 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
206 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
207 user = User.find(9)
207 user = User.find(9)
208
208
209 issues = Issue.visible(user).all
209 issues = Issue.visible(user).all
210 assert issues.any?
210 assert issues.any?
211 assert_nil issues.detect {|issue| issue.author != user}
211 assert_nil issues.detect {|issue| issue.author != user}
212 assert_visibility_match user, issues
212 assert_visibility_match user, issues
213 end
213 end
214
214
215 def test_visible_scope_for_non_member_without_view_issues_permissions
215 def test_visible_scope_for_non_member_without_view_issues_permissions
216 # Non member user should not see issues without permission
216 # Non member user should not see issues without permission
217 Role.non_member.remove_permission!(:view_issues)
217 Role.non_member.remove_permission!(:view_issues)
218 user = User.find(9)
218 user = User.find(9)
219 assert user.projects.empty?
219 assert user.projects.empty?
220 issues = Issue.visible(user).all
220 issues = Issue.visible(user).all
221 assert issues.empty?
221 assert issues.empty?
222 assert_visibility_match user, issues
222 assert_visibility_match user, issues
223 end
223 end
224
224
225 def test_visible_scope_for_member
225 def test_visible_scope_for_member
226 user = User.find(9)
226 user = User.find(9)
227 # User should see issues of projects for which he has view_issues permissions only
227 # User should see issues of projects for which he has view_issues permissions only
228 Role.non_member.remove_permission!(:view_issues)
228 Role.non_member.remove_permission!(:view_issues)
229 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
229 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
230 issues = Issue.visible(user).all
230 issues = Issue.visible(user).all
231 assert issues.any?
231 assert issues.any?
232 assert_nil issues.detect {|issue| issue.project_id != 3}
232 assert_nil issues.detect {|issue| issue.project_id != 3}
233 assert_nil issues.detect {|issue| issue.is_private?}
233 assert_nil issues.detect {|issue| issue.is_private?}
234 assert_visibility_match user, issues
234 assert_visibility_match user, issues
235 end
235 end
236
236
237 def test_visible_scope_for_member_with_groups_should_return_assigned_issues
237 def test_visible_scope_for_member_with_groups_should_return_assigned_issues
238 user = User.find(8)
238 user = User.find(8)
239 assert user.groups.any?
239 assert user.groups.any?
240 Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2])
240 Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2])
241 Role.non_member.remove_permission!(:view_issues)
241 Role.non_member.remove_permission!(:view_issues)
242
242
243 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
243 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
244 :status_id => 1, :priority => IssuePriority.all.first,
244 :status_id => 1, :priority => IssuePriority.all.first,
245 :subject => 'Assignment test',
245 :subject => 'Assignment test',
246 :assigned_to => user.groups.first,
246 :assigned_to => user.groups.first,
247 :is_private => true)
247 :is_private => true)
248
248
249 Role.find(2).update_attribute :issues_visibility, 'default'
249 Role.find(2).update_attribute :issues_visibility, 'default'
250 issues = Issue.visible(User.find(8)).all
250 issues = Issue.visible(User.find(8)).all
251 assert issues.any?
251 assert issues.any?
252 assert issues.include?(issue)
252 assert issues.include?(issue)
253
253
254 Role.find(2).update_attribute :issues_visibility, 'own'
254 Role.find(2).update_attribute :issues_visibility, 'own'
255 issues = Issue.visible(User.find(8)).all
255 issues = Issue.visible(User.find(8)).all
256 assert issues.any?
256 assert issues.any?
257 assert issues.include?(issue)
257 assert issues.include?(issue)
258 end
258 end
259
259
260 def test_visible_scope_for_admin
260 def test_visible_scope_for_admin
261 user = User.find(1)
261 user = User.find(1)
262 user.members.each(&:destroy)
262 user.members.each(&:destroy)
263 assert user.projects.empty?
263 assert user.projects.empty?
264 issues = Issue.visible(user).all
264 issues = Issue.visible(user).all
265 assert issues.any?
265 assert issues.any?
266 # Admin should see issues on private projects that he does not belong to
266 # Admin should see issues on private projects that he does not belong to
267 assert issues.detect {|issue| !issue.project.is_public?}
267 assert issues.detect {|issue| !issue.project.is_public?}
268 # Admin should see private issues of other users
268 # Admin should see private issues of other users
269 assert issues.detect {|issue| issue.is_private? && issue.author != user}
269 assert issues.detect {|issue| issue.is_private? && issue.author != user}
270 assert_visibility_match user, issues
270 assert_visibility_match user, issues
271 end
271 end
272
272
273 def test_visible_scope_with_project
273 def test_visible_scope_with_project
274 project = Project.find(1)
274 project = Project.find(1)
275 issues = Issue.visible(User.find(2), :project => project).all
275 issues = Issue.visible(User.find(2), :project => project).all
276 projects = issues.collect(&:project).uniq
276 projects = issues.collect(&:project).uniq
277 assert_equal 1, projects.size
277 assert_equal 1, projects.size
278 assert_equal project, projects.first
278 assert_equal project, projects.first
279 end
279 end
280
280
281 def test_visible_scope_with_project_and_subprojects
281 def test_visible_scope_with_project_and_subprojects
282 project = Project.find(1)
282 project = Project.find(1)
283 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all
283 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all
284 projects = issues.collect(&:project).uniq
284 projects = issues.collect(&:project).uniq
285 assert projects.size > 1
285 assert projects.size > 1
286 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
286 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
287 end
287 end
288
288
289 def test_visible_and_nested_set_scopes
289 def test_visible_and_nested_set_scopes
290 assert_equal 0, Issue.find(1).descendants.visible.all.size
290 assert_equal 0, Issue.find(1).descendants.visible.all.size
291 end
291 end
292
292
293 def test_open_scope
293 def test_open_scope
294 issues = Issue.open.all
294 issues = Issue.open.all
295 assert_nil issues.detect(&:closed?)
295 assert_nil issues.detect(&:closed?)
296 end
296 end
297
297
298 def test_open_scope_with_arg
298 def test_open_scope_with_arg
299 issues = Issue.open(false).all
299 issues = Issue.open(false).all
300 assert_equal issues, issues.select(&:closed?)
300 assert_equal issues, issues.select(&:closed?)
301 end
301 end
302
302
303 def test_errors_full_messages_should_include_custom_fields_errors
303 def test_errors_full_messages_should_include_custom_fields_errors
304 field = IssueCustomField.find_by_name('Database')
304 field = IssueCustomField.find_by_name('Database')
305
305
306 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
306 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
307 :status_id => 1, :subject => 'test_create',
307 :status_id => 1, :subject => 'test_create',
308 :description => 'IssueTest#test_create_with_required_custom_field')
308 :description => 'IssueTest#test_create_with_required_custom_field')
309 assert issue.available_custom_fields.include?(field)
309 assert issue.available_custom_fields.include?(field)
310 # Invalid value
310 # Invalid value
311 issue.custom_field_values = { field.id => 'SQLServer' }
311 issue.custom_field_values = { field.id => 'SQLServer' }
312
312
313 assert !issue.valid?
313 assert !issue.valid?
314 assert_equal 1, issue.errors.full_messages.size
314 assert_equal 1, issue.errors.full_messages.size
315 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}",
315 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}",
316 issue.errors.full_messages.first
316 issue.errors.full_messages.first
317 end
317 end
318
318
319 def test_update_issue_with_required_custom_field
319 def test_update_issue_with_required_custom_field
320 field = IssueCustomField.find_by_name('Database')
320 field = IssueCustomField.find_by_name('Database')
321 field.update_attribute(:is_required, true)
321 field.update_attribute(:is_required, true)
322
322
323 issue = Issue.find(1)
323 issue = Issue.find(1)
324 assert_nil issue.custom_value_for(field)
324 assert_nil issue.custom_value_for(field)
325 assert issue.available_custom_fields.include?(field)
325 assert issue.available_custom_fields.include?(field)
326 # No change to custom values, issue can be saved
326 # No change to custom values, issue can be saved
327 assert issue.save
327 assert issue.save
328 # Blank value
328 # Blank value
329 issue.custom_field_values = { field.id => '' }
329 issue.custom_field_values = { field.id => '' }
330 assert !issue.save
330 assert !issue.save
331 # Valid value
331 # Valid value
332 issue.custom_field_values = { field.id => 'PostgreSQL' }
332 issue.custom_field_values = { field.id => 'PostgreSQL' }
333 assert issue.save
333 assert issue.save
334 issue.reload
334 issue.reload
335 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
335 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
336 end
336 end
337
337
338 def test_should_not_update_attributes_if_custom_fields_validation_fails
338 def test_should_not_update_attributes_if_custom_fields_validation_fails
339 issue = Issue.find(1)
339 issue = Issue.find(1)
340 field = IssueCustomField.find_by_name('Database')
340 field = IssueCustomField.find_by_name('Database')
341 assert issue.available_custom_fields.include?(field)
341 assert issue.available_custom_fields.include?(field)
342
342
343 issue.custom_field_values = { field.id => 'Invalid' }
343 issue.custom_field_values = { field.id => 'Invalid' }
344 issue.subject = 'Should be not be saved'
344 issue.subject = 'Should be not be saved'
345 assert !issue.save
345 assert !issue.save
346
346
347 issue.reload
347 issue.reload
348 assert_equal "Can't print recipes", issue.subject
348 assert_equal "Can't print recipes", issue.subject
349 end
349 end
350
350
351 def test_should_not_recreate_custom_values_objects_on_update
351 def test_should_not_recreate_custom_values_objects_on_update
352 field = IssueCustomField.find_by_name('Database')
352 field = IssueCustomField.find_by_name('Database')
353
353
354 issue = Issue.find(1)
354 issue = Issue.find(1)
355 issue.custom_field_values = { field.id => 'PostgreSQL' }
355 issue.custom_field_values = { field.id => 'PostgreSQL' }
356 assert issue.save
356 assert issue.save
357 custom_value = issue.custom_value_for(field)
357 custom_value = issue.custom_value_for(field)
358 issue.reload
358 issue.reload
359 issue.custom_field_values = { field.id => 'MySQL' }
359 issue.custom_field_values = { field.id => 'MySQL' }
360 assert issue.save
360 assert issue.save
361 issue.reload
361 issue.reload
362 assert_equal custom_value.id, issue.custom_value_for(field).id
362 assert_equal custom_value.id, issue.custom_value_for(field).id
363 end
363 end
364
364
365 def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields
365 def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields
366 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1,
366 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1,
367 :status_id => 1, :subject => 'Test',
367 :status_id => 1, :subject => 'Test',
368 :custom_field_values => {'2' => 'Test'})
368 :custom_field_values => {'2' => 'Test'})
369 assert !Tracker.find(2).custom_field_ids.include?(2)
369 assert !Tracker.find(2).custom_field_ids.include?(2)
370
370
371 issue = Issue.find(issue.id)
371 issue = Issue.find(issue.id)
372 issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}}
372 issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}}
373
373
374 issue = Issue.find(issue.id)
374 issue = Issue.find(issue.id)
375 custom_value = issue.custom_value_for(2)
375 custom_value = issue.custom_value_for(2)
376 assert_not_nil custom_value
376 assert_not_nil custom_value
377 assert_equal 'Test', custom_value.value
377 assert_equal 'Test', custom_value.value
378 end
378 end
379
379
380 def test_assigning_tracker_id_should_reload_custom_fields_values
380 def test_assigning_tracker_id_should_reload_custom_fields_values
381 issue = Issue.new(:project => Project.find(1))
381 issue = Issue.new(:project => Project.find(1))
382 assert issue.custom_field_values.empty?
382 assert issue.custom_field_values.empty?
383 issue.tracker_id = 1
383 issue.tracker_id = 1
384 assert issue.custom_field_values.any?
384 assert issue.custom_field_values.any?
385 end
385 end
386
386
387 def test_assigning_attributes_should_assign_project_and_tracker_first
387 def test_assigning_attributes_should_assign_project_and_tracker_first
388 seq = sequence('seq')
388 seq = sequence('seq')
389 issue = Issue.new
389 issue = Issue.new
390 issue.expects(:project_id=).in_sequence(seq)
390 issue.expects(:project_id=).in_sequence(seq)
391 issue.expects(:tracker_id=).in_sequence(seq)
391 issue.expects(:tracker_id=).in_sequence(seq)
392 issue.expects(:subject=).in_sequence(seq)
392 issue.expects(:subject=).in_sequence(seq)
393 issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'}
393 issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'}
394 end
394 end
395
395
396 def test_assigning_tracker_and_custom_fields_should_assign_custom_fields
396 def test_assigning_tracker_and_custom_fields_should_assign_custom_fields
397 attributes = ActiveSupport::OrderedHash.new
397 attributes = ActiveSupport::OrderedHash.new
398 attributes['custom_field_values'] = { '1' => 'MySQL' }
398 attributes['custom_field_values'] = { '1' => 'MySQL' }
399 attributes['tracker_id'] = '1'
399 attributes['tracker_id'] = '1'
400 issue = Issue.new(:project => Project.find(1))
400 issue = Issue.new(:project => Project.find(1))
401 issue.attributes = attributes
401 issue.attributes = attributes
402 assert_equal 'MySQL', issue.custom_field_value(1)
402 assert_equal 'MySQL', issue.custom_field_value(1)
403 end
403 end
404
404
405 def test_should_update_issue_with_disabled_tracker
405 def test_should_update_issue_with_disabled_tracker
406 p = Project.find(1)
406 p = Project.find(1)
407 issue = Issue.find(1)
407 issue = Issue.find(1)
408
408
409 p.trackers.delete(issue.tracker)
409 p.trackers.delete(issue.tracker)
410 assert !p.trackers.include?(issue.tracker)
410 assert !p.trackers.include?(issue.tracker)
411
411
412 issue.reload
412 issue.reload
413 issue.subject = 'New subject'
413 issue.subject = 'New subject'
414 assert issue.save
414 assert issue.save
415 end
415 end
416
416
417 def test_should_not_set_a_disabled_tracker
417 def test_should_not_set_a_disabled_tracker
418 p = Project.find(1)
418 p = Project.find(1)
419 p.trackers.delete(Tracker.find(2))
419 p.trackers.delete(Tracker.find(2))
420
420
421 issue = Issue.find(1)
421 issue = Issue.find(1)
422 issue.tracker_id = 2
422 issue.tracker_id = 2
423 issue.subject = 'New subject'
423 issue.subject = 'New subject'
424 assert !issue.save
424 assert !issue.save
425 assert_not_nil issue.errors[:tracker_id]
425 assert_not_nil issue.errors[:tracker_id]
426 end
426 end
427
427
428 def test_category_based_assignment
428 def test_category_based_assignment
429 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
429 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
430 :status_id => 1, :priority => IssuePriority.all.first,
430 :status_id => 1, :priority => IssuePriority.all.first,
431 :subject => 'Assignment test',
431 :subject => 'Assignment test',
432 :description => 'Assignment test', :category_id => 1)
432 :description => 'Assignment test', :category_id => 1)
433 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
433 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
434 end
434 end
435
435
436 def test_new_statuses_allowed_to
436 def test_new_statuses_allowed_to
437 WorkflowTransition.delete_all
437 WorkflowTransition.delete_all
438 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
438 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
439 :old_status_id => 1, :new_status_id => 2,
439 :old_status_id => 1, :new_status_id => 2,
440 :author => false, :assignee => false)
440 :author => false, :assignee => false)
441 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
441 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
442 :old_status_id => 1, :new_status_id => 3,
442 :old_status_id => 1, :new_status_id => 3,
443 :author => true, :assignee => false)
443 :author => true, :assignee => false)
444 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1,
444 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1,
445 :new_status_id => 4, :author => false,
445 :new_status_id => 4, :author => false,
446 :assignee => true)
446 :assignee => true)
447 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
447 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
448 :old_status_id => 1, :new_status_id => 5,
448 :old_status_id => 1, :new_status_id => 5,
449 :author => true, :assignee => true)
449 :author => true, :assignee => true)
450 status = IssueStatus.find(1)
450 status = IssueStatus.find(1)
451 role = Role.find(1)
451 role = Role.find(1)
452 tracker = Tracker.find(1)
452 tracker = Tracker.find(1)
453 user = User.find(2)
453 user = User.find(2)
454
454
455 issue = Issue.generate!(:tracker => tracker, :status => status,
455 issue = Issue.generate!(:tracker => tracker, :status => status,
456 :project_id => 1, :author_id => 1)
456 :project_id => 1, :author_id => 1)
457 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
457 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
458
458
459 issue = Issue.generate!(:tracker => tracker, :status => status,
459 issue = Issue.generate!(:tracker => tracker, :status => status,
460 :project_id => 1, :author => user)
460 :project_id => 1, :author => user)
461 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
461 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
462
462
463 issue = Issue.generate!(:tracker => tracker, :status => status,
463 issue = Issue.generate!(:tracker => tracker, :status => status,
464 :project_id => 1, :author_id => 1,
464 :project_id => 1, :author_id => 1,
465 :assigned_to => user)
465 :assigned_to => user)
466 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
466 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
467
467
468 issue = Issue.generate!(:tracker => tracker, :status => status,
468 issue = Issue.generate!(:tracker => tracker, :status => status,
469 :project_id => 1, :author => user,
469 :project_id => 1, :author => user,
470 :assigned_to => user)
470 :assigned_to => user)
471 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
471 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
472 end
472 end
473
473
474 def test_new_statuses_allowed_to_should_return_all_transitions_for_admin
474 def test_new_statuses_allowed_to_should_return_all_transitions_for_admin
475 admin = User.find(1)
475 admin = User.find(1)
476 issue = Issue.find(1)
476 issue = Issue.find(1)
477 assert !admin.member_of?(issue.project)
477 assert !admin.member_of?(issue.project)
478 expected_statuses = [issue.status] +
478 expected_statuses = [issue.status] +
479 WorkflowTransition.find_all_by_old_status_id(
479 WorkflowTransition.find_all_by_old_status_id(
480 issue.status_id).map(&:new_status).uniq.sort
480 issue.status_id).map(&:new_status).uniq.sort
481 assert_equal expected_statuses, issue.new_statuses_allowed_to(admin)
481 assert_equal expected_statuses, issue.new_statuses_allowed_to(admin)
482 end
482 end
483
483
484 def test_new_statuses_allowed_to_should_return_default_and_current_status_when_copying
484 def test_new_statuses_allowed_to_should_return_default_and_current_status_when_copying
485 issue = Issue.find(1).copy
485 issue = Issue.find(1).copy
486 assert_equal [1], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
486 assert_equal [1], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
487
487
488 issue = Issue.find(2).copy
488 issue = Issue.find(2).copy
489 assert_equal [1, 2], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
489 assert_equal [1, 2], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
490 end
490 end
491
491
492 def test_safe_attributes_names_should_not_include_disabled_field
492 def test_safe_attributes_names_should_not_include_disabled_field
493 tracker = Tracker.new(:core_fields => %w(assigned_to_id fixed_version_id))
493 tracker = Tracker.new(:core_fields => %w(assigned_to_id fixed_version_id))
494
494
495 issue = Issue.new(:tracker => tracker)
495 issue = Issue.new(:tracker => tracker)
496 assert_include 'tracker_id', issue.safe_attribute_names
496 assert_include 'tracker_id', issue.safe_attribute_names
497 assert_include 'status_id', issue.safe_attribute_names
497 assert_include 'status_id', issue.safe_attribute_names
498 assert_include 'subject', issue.safe_attribute_names
498 assert_include 'subject', issue.safe_attribute_names
499 assert_include 'description', issue.safe_attribute_names
499 assert_include 'description', issue.safe_attribute_names
500 assert_include 'custom_field_values', issue.safe_attribute_names
500 assert_include 'custom_field_values', issue.safe_attribute_names
501 assert_include 'custom_fields', issue.safe_attribute_names
501 assert_include 'custom_fields', issue.safe_attribute_names
502 assert_include 'lock_version', issue.safe_attribute_names
502 assert_include 'lock_version', issue.safe_attribute_names
503
503
504 tracker.core_fields.each do |field|
504 tracker.core_fields.each do |field|
505 assert_include field, issue.safe_attribute_names
505 assert_include field, issue.safe_attribute_names
506 end
506 end
507
507
508 tracker.disabled_core_fields.each do |field|
508 tracker.disabled_core_fields.each do |field|
509 assert_not_include field, issue.safe_attribute_names
509 assert_not_include field, issue.safe_attribute_names
510 end
510 end
511 end
511 end
512
512
513 def test_safe_attributes_should_ignore_disabled_fields
513 def test_safe_attributes_should_ignore_disabled_fields
514 tracker = Tracker.find(1)
514 tracker = Tracker.find(1)
515 tracker.core_fields = %w(assigned_to_id due_date)
515 tracker.core_fields = %w(assigned_to_id due_date)
516 tracker.save!
516 tracker.save!
517
517
518 issue = Issue.new(:tracker => tracker)
518 issue = Issue.new(:tracker => tracker)
519 issue.safe_attributes = {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}
519 issue.safe_attributes = {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}
520 assert_nil issue.start_date
520 assert_nil issue.start_date
521 assert_equal Date.parse('2012-07-14'), issue.due_date
521 assert_equal Date.parse('2012-07-14'), issue.due_date
522 end
522 end
523
523
524 def test_safe_attributes_should_accept_target_tracker_enabled_fields
524 def test_safe_attributes_should_accept_target_tracker_enabled_fields
525 source = Tracker.find(1)
525 source = Tracker.find(1)
526 source.core_fields = []
526 source.core_fields = []
527 source.save!
527 source.save!
528 target = Tracker.find(2)
528 target = Tracker.find(2)
529 target.core_fields = %w(assigned_to_id due_date)
529 target.core_fields = %w(assigned_to_id due_date)
530 target.save!
530 target.save!
531
531
532 issue = Issue.new(:tracker => source)
532 issue = Issue.new(:tracker => source)
533 issue.safe_attributes = {'tracker_id' => 2, 'due_date' => '2012-07-14'}
533 issue.safe_attributes = {'tracker_id' => 2, 'due_date' => '2012-07-14'}
534 assert_equal target, issue.tracker
534 assert_equal target, issue.tracker
535 assert_equal Date.parse('2012-07-14'), issue.due_date
535 assert_equal Date.parse('2012-07-14'), issue.due_date
536 end
536 end
537
537
538 def test_safe_attributes_should_not_include_readonly_fields
538 def test_safe_attributes_should_not_include_readonly_fields
539 WorkflowPermission.delete_all
539 WorkflowPermission.delete_all
540 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
540 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
541 :role_id => 1, :field_name => 'due_date',
541 :role_id => 1, :field_name => 'due_date',
542 :rule => 'readonly')
542 :rule => 'readonly')
543 user = User.find(2)
543 user = User.find(2)
544
544
545 issue = Issue.new(:project_id => 1, :tracker_id => 1)
545 issue = Issue.new(:project_id => 1, :tracker_id => 1)
546 assert_equal %w(due_date), issue.read_only_attribute_names(user)
546 assert_equal %w(due_date), issue.read_only_attribute_names(user)
547 assert_not_include 'due_date', issue.safe_attribute_names(user)
547 assert_not_include 'due_date', issue.safe_attribute_names(user)
548
548
549 issue.send :safe_attributes=, {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}, user
549 issue.send :safe_attributes=, {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}, user
550 assert_equal Date.parse('2012-07-14'), issue.start_date
550 assert_equal Date.parse('2012-07-14'), issue.start_date
551 assert_nil issue.due_date
551 assert_nil issue.due_date
552 end
552 end
553
553
554 def test_safe_attributes_should_not_include_readonly_custom_fields
554 def test_safe_attributes_should_not_include_readonly_custom_fields
555 cf1 = IssueCustomField.create!(:name => 'Writable field',
555 cf1 = IssueCustomField.create!(:name => 'Writable field',
556 :field_format => 'string',
556 :field_format => 'string',
557 :is_for_all => true, :tracker_ids => [1])
557 :is_for_all => true, :tracker_ids => [1])
558 cf2 = IssueCustomField.create!(:name => 'Readonly field',
558 cf2 = IssueCustomField.create!(:name => 'Readonly field',
559 :field_format => 'string',
559 :field_format => 'string',
560 :is_for_all => true, :tracker_ids => [1])
560 :is_for_all => true, :tracker_ids => [1])
561 WorkflowPermission.delete_all
561 WorkflowPermission.delete_all
562 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
562 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
563 :role_id => 1, :field_name => cf2.id.to_s,
563 :role_id => 1, :field_name => cf2.id.to_s,
564 :rule => 'readonly')
564 :rule => 'readonly')
565 user = User.find(2)
565 user = User.find(2)
566 issue = Issue.new(:project_id => 1, :tracker_id => 1)
566 issue = Issue.new(:project_id => 1, :tracker_id => 1)
567 assert_equal [cf2.id.to_s], issue.read_only_attribute_names(user)
567 assert_equal [cf2.id.to_s], issue.read_only_attribute_names(user)
568 assert_not_include cf2.id.to_s, issue.safe_attribute_names(user)
568 assert_not_include cf2.id.to_s, issue.safe_attribute_names(user)
569
569
570 issue.send :safe_attributes=, {'custom_field_values' => {
570 issue.send :safe_attributes=, {'custom_field_values' => {
571 cf1.id.to_s => 'value1', cf2.id.to_s => 'value2'
571 cf1.id.to_s => 'value1', cf2.id.to_s => 'value2'
572 }}, user
572 }}, user
573 assert_equal 'value1', issue.custom_field_value(cf1)
573 assert_equal 'value1', issue.custom_field_value(cf1)
574 assert_nil issue.custom_field_value(cf2)
574 assert_nil issue.custom_field_value(cf2)
575
575
576 issue.send :safe_attributes=, {'custom_fields' => [
576 issue.send :safe_attributes=, {'custom_fields' => [
577 {'id' => cf1.id.to_s, 'value' => 'valuea'},
577 {'id' => cf1.id.to_s, 'value' => 'valuea'},
578 {'id' => cf2.id.to_s, 'value' => 'valueb'}
578 {'id' => cf2.id.to_s, 'value' => 'valueb'}
579 ]}, user
579 ]}, user
580 assert_equal 'valuea', issue.custom_field_value(cf1)
580 assert_equal 'valuea', issue.custom_field_value(cf1)
581 assert_nil issue.custom_field_value(cf2)
581 assert_nil issue.custom_field_value(cf2)
582 end
582 end
583
583
584 def test_editable_custom_field_values_should_return_non_readonly_custom_values
584 def test_editable_custom_field_values_should_return_non_readonly_custom_values
585 cf1 = IssueCustomField.create!(:name => 'Writable field', :field_format => 'string',
585 cf1 = IssueCustomField.create!(:name => 'Writable field', :field_format => 'string',
586 :is_for_all => true, :tracker_ids => [1, 2])
586 :is_for_all => true, :tracker_ids => [1, 2])
587 cf2 = IssueCustomField.create!(:name => 'Readonly field', :field_format => 'string',
587 cf2 = IssueCustomField.create!(:name => 'Readonly field', :field_format => 'string',
588 :is_for_all => true, :tracker_ids => [1, 2])
588 :is_for_all => true, :tracker_ids => [1, 2])
589 WorkflowPermission.delete_all
589 WorkflowPermission.delete_all
590 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1,
590 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1,
591 :field_name => cf2.id.to_s, :rule => 'readonly')
591 :field_name => cf2.id.to_s, :rule => 'readonly')
592 user = User.find(2)
592 user = User.find(2)
593
593
594 issue = Issue.new(:project_id => 1, :tracker_id => 1)
594 issue = Issue.new(:project_id => 1, :tracker_id => 1)
595 values = issue.editable_custom_field_values(user)
595 values = issue.editable_custom_field_values(user)
596 assert values.detect {|value| value.custom_field == cf1}
596 assert values.detect {|value| value.custom_field == cf1}
597 assert_nil values.detect {|value| value.custom_field == cf2}
597 assert_nil values.detect {|value| value.custom_field == cf2}
598
598
599 issue.tracker_id = 2
599 issue.tracker_id = 2
600 values = issue.editable_custom_field_values(user)
600 values = issue.editable_custom_field_values(user)
601 assert values.detect {|value| value.custom_field == cf1}
601 assert values.detect {|value| value.custom_field == cf1}
602 assert values.detect {|value| value.custom_field == cf2}
602 assert values.detect {|value| value.custom_field == cf2}
603 end
603 end
604
604
605 def test_safe_attributes_should_accept_target_tracker_writable_fields
605 def test_safe_attributes_should_accept_target_tracker_writable_fields
606 WorkflowPermission.delete_all
606 WorkflowPermission.delete_all
607 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
607 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
608 :role_id => 1, :field_name => 'due_date',
608 :role_id => 1, :field_name => 'due_date',
609 :rule => 'readonly')
609 :rule => 'readonly')
610 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
610 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
611 :role_id => 1, :field_name => 'start_date',
611 :role_id => 1, :field_name => 'start_date',
612 :rule => 'readonly')
612 :rule => 'readonly')
613 user = User.find(2)
613 user = User.find(2)
614
614
615 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
615 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
616
616
617 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
617 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
618 'due_date' => '2012-07-14'}, user
618 'due_date' => '2012-07-14'}, user
619 assert_equal Date.parse('2012-07-12'), issue.start_date
619 assert_equal Date.parse('2012-07-12'), issue.start_date
620 assert_nil issue.due_date
620 assert_nil issue.due_date
621
621
622 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
622 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
623 'due_date' => '2012-07-16',
623 'due_date' => '2012-07-16',
624 'tracker_id' => 2}, user
624 'tracker_id' => 2}, user
625 assert_equal Date.parse('2012-07-12'), issue.start_date
625 assert_equal Date.parse('2012-07-12'), issue.start_date
626 assert_equal Date.parse('2012-07-16'), issue.due_date
626 assert_equal Date.parse('2012-07-16'), issue.due_date
627 end
627 end
628
628
629 def test_safe_attributes_should_accept_target_status_writable_fields
629 def test_safe_attributes_should_accept_target_status_writable_fields
630 WorkflowPermission.delete_all
630 WorkflowPermission.delete_all
631 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
631 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
632 :role_id => 1, :field_name => 'due_date',
632 :role_id => 1, :field_name => 'due_date',
633 :rule => 'readonly')
633 :rule => 'readonly')
634 WorkflowPermission.create!(:old_status_id => 2, :tracker_id => 1,
634 WorkflowPermission.create!(:old_status_id => 2, :tracker_id => 1,
635 :role_id => 1, :field_name => 'start_date',
635 :role_id => 1, :field_name => 'start_date',
636 :rule => 'readonly')
636 :rule => 'readonly')
637 user = User.find(2)
637 user = User.find(2)
638
638
639 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
639 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
640
640
641 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
641 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
642 'due_date' => '2012-07-14'},
642 'due_date' => '2012-07-14'},
643 user
643 user
644 assert_equal Date.parse('2012-07-12'), issue.start_date
644 assert_equal Date.parse('2012-07-12'), issue.start_date
645 assert_nil issue.due_date
645 assert_nil issue.due_date
646
646
647 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
647 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
648 'due_date' => '2012-07-16',
648 'due_date' => '2012-07-16',
649 'status_id' => 2},
649 'status_id' => 2},
650 user
650 user
651 assert_equal Date.parse('2012-07-12'), issue.start_date
651 assert_equal Date.parse('2012-07-12'), issue.start_date
652 assert_equal Date.parse('2012-07-16'), issue.due_date
652 assert_equal Date.parse('2012-07-16'), issue.due_date
653 end
653 end
654
654
655 def test_required_attributes_should_be_validated
655 def test_required_attributes_should_be_validated
656 cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string',
656 cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string',
657 :is_for_all => true, :tracker_ids => [1, 2])
657 :is_for_all => true, :tracker_ids => [1, 2])
658
658
659 WorkflowPermission.delete_all
659 WorkflowPermission.delete_all
660 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
660 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
661 :role_id => 1, :field_name => 'due_date',
661 :role_id => 1, :field_name => 'due_date',
662 :rule => 'required')
662 :rule => 'required')
663 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
663 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
664 :role_id => 1, :field_name => 'category_id',
664 :role_id => 1, :field_name => 'category_id',
665 :rule => 'required')
665 :rule => 'required')
666 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
666 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
667 :role_id => 1, :field_name => cf.id.to_s,
667 :role_id => 1, :field_name => cf.id.to_s,
668 :rule => 'required')
668 :rule => 'required')
669
669
670 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
670 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
671 :role_id => 1, :field_name => 'start_date',
671 :role_id => 1, :field_name => 'start_date',
672 :rule => 'required')
672 :rule => 'required')
673 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
673 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
674 :role_id => 1, :field_name => cf.id.to_s,
674 :role_id => 1, :field_name => cf.id.to_s,
675 :rule => 'required')
675 :rule => 'required')
676 user = User.find(2)
676 user = User.find(2)
677
677
678 issue = Issue.new(:project_id => 1, :tracker_id => 1,
678 issue = Issue.new(:project_id => 1, :tracker_id => 1,
679 :status_id => 1, :subject => 'Required fields',
679 :status_id => 1, :subject => 'Required fields',
680 :author => user)
680 :author => user)
681 assert_equal [cf.id.to_s, "category_id", "due_date"],
681 assert_equal [cf.id.to_s, "category_id", "due_date"],
682 issue.required_attribute_names(user).sort
682 issue.required_attribute_names(user).sort
683 assert !issue.save, "Issue was saved"
683 assert !issue.save, "Issue was saved"
684 assert_equal ["Category can't be blank", "Due date can't be blank", "Foo can't be blank"],
684 assert_equal ["Category can't be blank", "Due date can't be blank", "Foo can't be blank"],
685 issue.errors.full_messages.sort
685 issue.errors.full_messages.sort
686
686
687 issue.tracker_id = 2
687 issue.tracker_id = 2
688 assert_equal [cf.id.to_s, "start_date"], issue.required_attribute_names(user).sort
688 assert_equal [cf.id.to_s, "start_date"], issue.required_attribute_names(user).sort
689 assert !issue.save, "Issue was saved"
689 assert !issue.save, "Issue was saved"
690 assert_equal ["Foo can't be blank", "Start date can't be blank"],
690 assert_equal ["Foo can't be blank", "Start date can't be blank"],
691 issue.errors.full_messages.sort
691 issue.errors.full_messages.sort
692
692
693 issue.start_date = Date.today
693 issue.start_date = Date.today
694 issue.custom_field_values = {cf.id.to_s => 'bar'}
694 issue.custom_field_values = {cf.id.to_s => 'bar'}
695 assert issue.save
695 assert issue.save
696 end
696 end
697
697
698 def test_required_attribute_names_for_multiple_roles_should_intersect_rules
698 def test_required_attribute_names_for_multiple_roles_should_intersect_rules
699 WorkflowPermission.delete_all
699 WorkflowPermission.delete_all
700 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
700 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
701 :role_id => 1, :field_name => 'due_date',
701 :role_id => 1, :field_name => 'due_date',
702 :rule => 'required')
702 :rule => 'required')
703 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
703 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
704 :role_id => 1, :field_name => 'start_date',
704 :role_id => 1, :field_name => 'start_date',
705 :rule => 'required')
705 :rule => 'required')
706 user = User.find(2)
706 user = User.find(2)
707 member = Member.find(1)
707 member = Member.find(1)
708 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
708 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
709
709
710 assert_equal %w(due_date start_date), issue.required_attribute_names(user).sort
710 assert_equal %w(due_date start_date), issue.required_attribute_names(user).sort
711
711
712 member.role_ids = [1, 2]
712 member.role_ids = [1, 2]
713 member.save!
713 member.save!
714 assert_equal [], issue.required_attribute_names(user.reload)
714 assert_equal [], issue.required_attribute_names(user.reload)
715
715
716 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
716 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
717 :role_id => 2, :field_name => 'due_date',
717 :role_id => 2, :field_name => 'due_date',
718 :rule => 'required')
718 :rule => 'required')
719 assert_equal %w(due_date), issue.required_attribute_names(user)
719 assert_equal %w(due_date), issue.required_attribute_names(user)
720
720
721 member.role_ids = [1, 2, 3]
721 member.role_ids = [1, 2, 3]
722 member.save!
722 member.save!
723 assert_equal [], issue.required_attribute_names(user.reload)
723 assert_equal [], issue.required_attribute_names(user.reload)
724
724
725 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
725 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
726 :role_id => 2, :field_name => 'due_date',
726 :role_id => 2, :field_name => 'due_date',
727 :rule => 'readonly')
727 :rule => 'readonly')
728 # required + readonly => required
728 # required + readonly => required
729 assert_equal %w(due_date), issue.required_attribute_names(user)
729 assert_equal %w(due_date), issue.required_attribute_names(user)
730 end
730 end
731
731
732 def test_read_only_attribute_names_for_multiple_roles_should_intersect_rules
732 def test_read_only_attribute_names_for_multiple_roles_should_intersect_rules
733 WorkflowPermission.delete_all
733 WorkflowPermission.delete_all
734 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
734 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
735 :role_id => 1, :field_name => 'due_date',
735 :role_id => 1, :field_name => 'due_date',
736 :rule => 'readonly')
736 :rule => 'readonly')
737 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
737 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
738 :role_id => 1, :field_name => 'start_date',
738 :role_id => 1, :field_name => 'start_date',
739 :rule => 'readonly')
739 :rule => 'readonly')
740 user = User.find(2)
740 user = User.find(2)
741 member = Member.find(1)
741 member = Member.find(1)
742 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
742 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
743
743
744 assert_equal %w(due_date start_date), issue.read_only_attribute_names(user).sort
744 assert_equal %w(due_date start_date), issue.read_only_attribute_names(user).sort
745
745
746 member.role_ids = [1, 2]
746 member.role_ids = [1, 2]
747 member.save!
747 member.save!
748 assert_equal [], issue.read_only_attribute_names(user.reload)
748 assert_equal [], issue.read_only_attribute_names(user.reload)
749
749
750 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
750 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
751 :role_id => 2, :field_name => 'due_date',
751 :role_id => 2, :field_name => 'due_date',
752 :rule => 'readonly')
752 :rule => 'readonly')
753 assert_equal %w(due_date), issue.read_only_attribute_names(user)
753 assert_equal %w(due_date), issue.read_only_attribute_names(user)
754 end
754 end
755
755
756 def test_copy
756 def test_copy
757 issue = Issue.new.copy_from(1)
757 issue = Issue.new.copy_from(1)
758 assert issue.copy?
758 assert issue.copy?
759 assert issue.save
759 assert issue.save
760 issue.reload
760 issue.reload
761 orig = Issue.find(1)
761 orig = Issue.find(1)
762 assert_equal orig.subject, issue.subject
762 assert_equal orig.subject, issue.subject
763 assert_equal orig.tracker, issue.tracker
763 assert_equal orig.tracker, issue.tracker
764 assert_equal "125", issue.custom_value_for(2).value
764 assert_equal "125", issue.custom_value_for(2).value
765 end
765 end
766
766
767 def test_copy_should_copy_status
767 def test_copy_should_copy_status
768 orig = Issue.find(8)
768 orig = Issue.find(8)
769 assert orig.status != IssueStatus.default
769 assert orig.status != IssueStatus.default
770
770
771 issue = Issue.new.copy_from(orig)
771 issue = Issue.new.copy_from(orig)
772 assert issue.save
772 assert issue.save
773 issue.reload
773 issue.reload
774 assert_equal orig.status, issue.status
774 assert_equal orig.status, issue.status
775 end
775 end
776
776
777 def test_copy_should_add_relation_with_copied_issue
777 def test_copy_should_add_relation_with_copied_issue
778 copied = Issue.find(1)
778 copied = Issue.find(1)
779 issue = Issue.new.copy_from(copied)
779 issue = Issue.new.copy_from(copied)
780 assert issue.save
780 assert issue.save
781 issue.reload
781 issue.reload
782
782
783 assert_equal 1, issue.relations.size
783 assert_equal 1, issue.relations.size
784 relation = issue.relations.first
784 relation = issue.relations.first
785 assert_equal 'copied_to', relation.relation_type
785 assert_equal 'copied_to', relation.relation_type
786 assert_equal copied, relation.issue_from
786 assert_equal copied, relation.issue_from
787 assert_equal issue, relation.issue_to
787 assert_equal issue, relation.issue_to
788 end
788 end
789
789
790 def test_copy_should_copy_subtasks
790 def test_copy_should_copy_subtasks
791 issue = Issue.generate_with_descendants!
791 issue = Issue.generate_with_descendants!
792
792
793 copy = issue.reload.copy
793 copy = issue.reload.copy
794 copy.author = User.find(7)
794 copy.author = User.find(7)
795 assert_difference 'Issue.count', 1+issue.descendants.count do
795 assert_difference 'Issue.count', 1+issue.descendants.count do
796 assert copy.save
796 assert copy.save
797 end
797 end
798 copy.reload
798 copy.reload
799 assert_equal %w(Child1 Child2), copy.children.map(&:subject).sort
799 assert_equal %w(Child1 Child2), copy.children.map(&:subject).sort
800 child_copy = copy.children.detect {|c| c.subject == 'Child1'}
800 child_copy = copy.children.detect {|c| c.subject == 'Child1'}
801 assert_equal %w(Child11), child_copy.children.map(&:subject).sort
801 assert_equal %w(Child11), child_copy.children.map(&:subject).sort
802 assert_equal copy.author, child_copy.author
802 assert_equal copy.author, child_copy.author
803 end
803 end
804
804
805 def test_copy_should_copy_subtasks_to_target_project
805 def test_copy_should_copy_subtasks_to_target_project
806 issue = Issue.generate_with_descendants!
806 issue = Issue.generate_with_descendants!
807
807
808 copy = issue.copy(:project_id => 3)
808 copy = issue.copy(:project_id => 3)
809 assert_difference 'Issue.count', 1+issue.descendants.count do
809 assert_difference 'Issue.count', 1+issue.descendants.count do
810 assert copy.save
810 assert copy.save
811 end
811 end
812 assert_equal [3], copy.reload.descendants.map(&:project_id).uniq
812 assert_equal [3], copy.reload.descendants.map(&:project_id).uniq
813 end
813 end
814
814
815 def test_copy_should_not_copy_subtasks_twice_when_saving_twice
815 def test_copy_should_not_copy_subtasks_twice_when_saving_twice
816 issue = Issue.generate_with_descendants!
816 issue = Issue.generate_with_descendants!
817
817
818 copy = issue.reload.copy
818 copy = issue.reload.copy
819 assert_difference 'Issue.count', 1+issue.descendants.count do
819 assert_difference 'Issue.count', 1+issue.descendants.count do
820 assert copy.save
820 assert copy.save
821 assert copy.save
821 assert copy.save
822 end
822 end
823 end
823 end
824
824
825 def test_should_not_call_after_project_change_on_creation
825 def test_should_not_call_after_project_change_on_creation
826 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
826 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
827 :subject => 'Test', :author_id => 1)
827 :subject => 'Test', :author_id => 1)
828 issue.expects(:after_project_change).never
828 issue.expects(:after_project_change).never
829 issue.save!
829 issue.save!
830 end
830 end
831
831
832 def test_should_not_call_after_project_change_on_update
832 def test_should_not_call_after_project_change_on_update
833 issue = Issue.find(1)
833 issue = Issue.find(1)
834 issue.project = Project.find(1)
834 issue.project = Project.find(1)
835 issue.subject = 'No project change'
835 issue.subject = 'No project change'
836 issue.expects(:after_project_change).never
836 issue.expects(:after_project_change).never
837 issue.save!
837 issue.save!
838 end
838 end
839
839
840 def test_should_call_after_project_change_on_project_change
840 def test_should_call_after_project_change_on_project_change
841 issue = Issue.find(1)
841 issue = Issue.find(1)
842 issue.project = Project.find(2)
842 issue.project = Project.find(2)
843 issue.expects(:after_project_change).once
843 issue.expects(:after_project_change).once
844 issue.save!
844 issue.save!
845 end
845 end
846
846
847 def test_adding_journal_should_update_timestamp
847 def test_adding_journal_should_update_timestamp
848 issue = Issue.find(1)
848 issue = Issue.find(1)
849 updated_on_was = issue.updated_on
849 updated_on_was = issue.updated_on
850
850
851 issue.init_journal(User.first, "Adding notes")
851 issue.init_journal(User.first, "Adding notes")
852 assert_difference 'Journal.count' do
852 assert_difference 'Journal.count' do
853 assert issue.save
853 assert issue.save
854 end
854 end
855 issue.reload
855 issue.reload
856
856
857 assert_not_equal updated_on_was, issue.updated_on
857 assert_not_equal updated_on_was, issue.updated_on
858 end
858 end
859
859
860 def test_should_close_duplicates
860 def test_should_close_duplicates
861 # Create 3 issues
861 # Create 3 issues
862 issue1 = Issue.generate!
862 issue1 = Issue.generate!
863 issue2 = Issue.generate!
863 issue2 = Issue.generate!
864 issue3 = Issue.generate!
864 issue3 = Issue.generate!
865
865
866 # 2 is a dupe of 1
866 # 2 is a dupe of 1
867 IssueRelation.create!(:issue_from => issue2, :issue_to => issue1,
867 IssueRelation.create!(:issue_from => issue2, :issue_to => issue1,
868 :relation_type => IssueRelation::TYPE_DUPLICATES)
868 :relation_type => IssueRelation::TYPE_DUPLICATES)
869 # And 3 is a dupe of 2
869 # And 3 is a dupe of 2
870 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
870 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
871 :relation_type => IssueRelation::TYPE_DUPLICATES)
871 :relation_type => IssueRelation::TYPE_DUPLICATES)
872 # And 3 is a dupe of 1 (circular duplicates)
872 # And 3 is a dupe of 1 (circular duplicates)
873 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1,
873 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1,
874 :relation_type => IssueRelation::TYPE_DUPLICATES)
874 :relation_type => IssueRelation::TYPE_DUPLICATES)
875
875
876 assert issue1.reload.duplicates.include?(issue2)
876 assert issue1.reload.duplicates.include?(issue2)
877
877
878 # Closing issue 1
878 # Closing issue 1
879 issue1.init_journal(User.first, "Closing issue1")
879 issue1.init_journal(User.first, "Closing issue1")
880 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
880 issue1.status = IssueStatus.where(:is_closed => true).first
881 assert issue1.save
881 assert issue1.save
882 # 2 and 3 should be also closed
882 # 2 and 3 should be also closed
883 assert issue2.reload.closed?
883 assert issue2.reload.closed?
884 assert issue3.reload.closed?
884 assert issue3.reload.closed?
885 end
885 end
886
886
887 def test_should_not_close_duplicated_issue
887 def test_should_not_close_duplicated_issue
888 issue1 = Issue.generate!
888 issue1 = Issue.generate!
889 issue2 = Issue.generate!
889 issue2 = Issue.generate!
890
890
891 # 2 is a dupe of 1
891 # 2 is a dupe of 1
892 IssueRelation.create(:issue_from => issue2, :issue_to => issue1,
892 IssueRelation.create(:issue_from => issue2, :issue_to => issue1,
893 :relation_type => IssueRelation::TYPE_DUPLICATES)
893 :relation_type => IssueRelation::TYPE_DUPLICATES)
894 # 2 is a dup of 1 but 1 is not a duplicate of 2
894 # 2 is a dup of 1 but 1 is not a duplicate of 2
895 assert !issue2.reload.duplicates.include?(issue1)
895 assert !issue2.reload.duplicates.include?(issue1)
896
896
897 # Closing issue 2
897 # Closing issue 2
898 issue2.init_journal(User.first, "Closing issue2")
898 issue2.init_journal(User.first, "Closing issue2")
899 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
899 issue2.status = IssueStatus.where(:is_closed => true).first
900 assert issue2.save
900 assert issue2.save
901 # 1 should not be also closed
901 # 1 should not be also closed
902 assert !issue1.reload.closed?
902 assert !issue1.reload.closed?
903 end
903 end
904
904
905 def test_assignable_versions
905 def test_assignable_versions
906 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
906 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
907 :status_id => 1, :fixed_version_id => 1,
907 :status_id => 1, :fixed_version_id => 1,
908 :subject => 'New issue')
908 :subject => 'New issue')
909 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
909 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
910 end
910 end
911
911
912 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
912 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
913 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
913 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
914 :status_id => 1, :fixed_version_id => 1,
914 :status_id => 1, :fixed_version_id => 1,
915 :subject => 'New issue')
915 :subject => 'New issue')
916 assert !issue.save
916 assert !issue.save
917 assert_not_nil issue.errors[:fixed_version_id]
917 assert_not_nil issue.errors[:fixed_version_id]
918 end
918 end
919
919
920 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
920 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
921 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
921 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
922 :status_id => 1, :fixed_version_id => 2,
922 :status_id => 1, :fixed_version_id => 2,
923 :subject => 'New issue')
923 :subject => 'New issue')
924 assert !issue.save
924 assert !issue.save
925 assert_not_nil issue.errors[:fixed_version_id]
925 assert_not_nil issue.errors[:fixed_version_id]
926 end
926 end
927
927
928 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
928 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
929 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
929 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
930 :status_id => 1, :fixed_version_id => 3,
930 :status_id => 1, :fixed_version_id => 3,
931 :subject => 'New issue')
931 :subject => 'New issue')
932 assert issue.save
932 assert issue.save
933 end
933 end
934
934
935 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
935 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
936 issue = Issue.find(11)
936 issue = Issue.find(11)
937 assert_equal 'closed', issue.fixed_version.status
937 assert_equal 'closed', issue.fixed_version.status
938 issue.subject = 'Subject changed'
938 issue.subject = 'Subject changed'
939 assert issue.save
939 assert issue.save
940 end
940 end
941
941
942 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
942 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
943 issue = Issue.find(11)
943 issue = Issue.find(11)
944 issue.status_id = 1
944 issue.status_id = 1
945 assert !issue.save
945 assert !issue.save
946 assert_not_nil issue.errors[:base]
946 assert_not_nil issue.errors[:base]
947 end
947 end
948
948
949 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
949 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
950 issue = Issue.find(11)
950 issue = Issue.find(11)
951 issue.status_id = 1
951 issue.status_id = 1
952 issue.fixed_version_id = 3
952 issue.fixed_version_id = 3
953 assert issue.save
953 assert issue.save
954 end
954 end
955
955
956 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
956 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
957 issue = Issue.find(12)
957 issue = Issue.find(12)
958 assert_equal 'locked', issue.fixed_version.status
958 assert_equal 'locked', issue.fixed_version.status
959 issue.status_id = 1
959 issue.status_id = 1
960 assert issue.save
960 assert issue.save
961 end
961 end
962
962
963 def test_should_not_be_able_to_keep_unshared_version_when_changing_project
963 def test_should_not_be_able_to_keep_unshared_version_when_changing_project
964 issue = Issue.find(2)
964 issue = Issue.find(2)
965 assert_equal 2, issue.fixed_version_id
965 assert_equal 2, issue.fixed_version_id
966 issue.project_id = 3
966 issue.project_id = 3
967 assert_nil issue.fixed_version_id
967 assert_nil issue.fixed_version_id
968 issue.fixed_version_id = 2
968 issue.fixed_version_id = 2
969 assert !issue.save
969 assert !issue.save
970 assert_include 'Target version is not included in the list', issue.errors.full_messages
970 assert_include 'Target version is not included in the list', issue.errors.full_messages
971 end
971 end
972
972
973 def test_should_keep_shared_version_when_changing_project
973 def test_should_keep_shared_version_when_changing_project
974 Version.find(2).update_attribute :sharing, 'tree'
974 Version.find(2).update_attribute :sharing, 'tree'
975
975
976 issue = Issue.find(2)
976 issue = Issue.find(2)
977 assert_equal 2, issue.fixed_version_id
977 assert_equal 2, issue.fixed_version_id
978 issue.project_id = 3
978 issue.project_id = 3
979 assert_equal 2, issue.fixed_version_id
979 assert_equal 2, issue.fixed_version_id
980 assert issue.save
980 assert issue.save
981 end
981 end
982
982
983 def test_allowed_target_projects_on_move_should_include_projects_with_issue_tracking_enabled
983 def test_allowed_target_projects_on_move_should_include_projects_with_issue_tracking_enabled
984 assert_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
984 assert_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
985 end
985 end
986
986
987 def test_allowed_target_projects_on_move_should_not_include_projects_with_issue_tracking_disabled
987 def test_allowed_target_projects_on_move_should_not_include_projects_with_issue_tracking_disabled
988 Project.find(2).disable_module! :issue_tracking
988 Project.find(2).disable_module! :issue_tracking
989 assert_not_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
989 assert_not_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
990 end
990 end
991
991
992 def test_move_to_another_project_with_same_category
992 def test_move_to_another_project_with_same_category
993 issue = Issue.find(1)
993 issue = Issue.find(1)
994 issue.project = Project.find(2)
994 issue.project = Project.find(2)
995 assert issue.save
995 assert issue.save
996 issue.reload
996 issue.reload
997 assert_equal 2, issue.project_id
997 assert_equal 2, issue.project_id
998 # Category changes
998 # Category changes
999 assert_equal 4, issue.category_id
999 assert_equal 4, issue.category_id
1000 # Make sure time entries were move to the target project
1000 # Make sure time entries were move to the target project
1001 assert_equal 2, issue.time_entries.first.project_id
1001 assert_equal 2, issue.time_entries.first.project_id
1002 end
1002 end
1003
1003
1004 def test_move_to_another_project_without_same_category
1004 def test_move_to_another_project_without_same_category
1005 issue = Issue.find(2)
1005 issue = Issue.find(2)
1006 issue.project = Project.find(2)
1006 issue.project = Project.find(2)
1007 assert issue.save
1007 assert issue.save
1008 issue.reload
1008 issue.reload
1009 assert_equal 2, issue.project_id
1009 assert_equal 2, issue.project_id
1010 # Category cleared
1010 # Category cleared
1011 assert_nil issue.category_id
1011 assert_nil issue.category_id
1012 end
1012 end
1013
1013
1014 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
1014 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
1015 issue = Issue.find(1)
1015 issue = Issue.find(1)
1016 issue.update_attribute(:fixed_version_id, 1)
1016 issue.update_attribute(:fixed_version_id, 1)
1017 issue.project = Project.find(2)
1017 issue.project = Project.find(2)
1018 assert issue.save
1018 assert issue.save
1019 issue.reload
1019 issue.reload
1020 assert_equal 2, issue.project_id
1020 assert_equal 2, issue.project_id
1021 # Cleared fixed_version
1021 # Cleared fixed_version
1022 assert_equal nil, issue.fixed_version
1022 assert_equal nil, issue.fixed_version
1023 end
1023 end
1024
1024
1025 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
1025 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
1026 issue = Issue.find(1)
1026 issue = Issue.find(1)
1027 issue.update_attribute(:fixed_version_id, 4)
1027 issue.update_attribute(:fixed_version_id, 4)
1028 issue.project = Project.find(5)
1028 issue.project = Project.find(5)
1029 assert issue.save
1029 assert issue.save
1030 issue.reload
1030 issue.reload
1031 assert_equal 5, issue.project_id
1031 assert_equal 5, issue.project_id
1032 # Keep fixed_version
1032 # Keep fixed_version
1033 assert_equal 4, issue.fixed_version_id
1033 assert_equal 4, issue.fixed_version_id
1034 end
1034 end
1035
1035
1036 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
1036 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
1037 issue = Issue.find(1)
1037 issue = Issue.find(1)
1038 issue.update_attribute(:fixed_version_id, 1)
1038 issue.update_attribute(:fixed_version_id, 1)
1039 issue.project = Project.find(5)
1039 issue.project = Project.find(5)
1040 assert issue.save
1040 assert issue.save
1041 issue.reload
1041 issue.reload
1042 assert_equal 5, issue.project_id
1042 assert_equal 5, issue.project_id
1043 # Cleared fixed_version
1043 # Cleared fixed_version
1044 assert_equal nil, issue.fixed_version
1044 assert_equal nil, issue.fixed_version
1045 end
1045 end
1046
1046
1047 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
1047 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
1048 issue = Issue.find(1)
1048 issue = Issue.find(1)
1049 issue.update_attribute(:fixed_version_id, 7)
1049 issue.update_attribute(:fixed_version_id, 7)
1050 issue.project = Project.find(2)
1050 issue.project = Project.find(2)
1051 assert issue.save
1051 assert issue.save
1052 issue.reload
1052 issue.reload
1053 assert_equal 2, issue.project_id
1053 assert_equal 2, issue.project_id
1054 # Keep fixed_version
1054 # Keep fixed_version
1055 assert_equal 7, issue.fixed_version_id
1055 assert_equal 7, issue.fixed_version_id
1056 end
1056 end
1057
1057
1058 def test_move_to_another_project_should_keep_parent_if_valid
1058 def test_move_to_another_project_should_keep_parent_if_valid
1059 issue = Issue.find(1)
1059 issue = Issue.find(1)
1060 issue.update_attribute(:parent_issue_id, 2)
1060 issue.update_attribute(:parent_issue_id, 2)
1061 issue.project = Project.find(3)
1061 issue.project = Project.find(3)
1062 assert issue.save
1062 assert issue.save
1063 issue.reload
1063 issue.reload
1064 assert_equal 2, issue.parent_id
1064 assert_equal 2, issue.parent_id
1065 end
1065 end
1066
1066
1067 def test_move_to_another_project_should_clear_parent_if_not_valid
1067 def test_move_to_another_project_should_clear_parent_if_not_valid
1068 issue = Issue.find(1)
1068 issue = Issue.find(1)
1069 issue.update_attribute(:parent_issue_id, 2)
1069 issue.update_attribute(:parent_issue_id, 2)
1070 issue.project = Project.find(2)
1070 issue.project = Project.find(2)
1071 assert issue.save
1071 assert issue.save
1072 issue.reload
1072 issue.reload
1073 assert_nil issue.parent_id
1073 assert_nil issue.parent_id
1074 end
1074 end
1075
1075
1076 def test_move_to_another_project_with_disabled_tracker
1076 def test_move_to_another_project_with_disabled_tracker
1077 issue = Issue.find(1)
1077 issue = Issue.find(1)
1078 target = Project.find(2)
1078 target = Project.find(2)
1079 target.tracker_ids = [3]
1079 target.tracker_ids = [3]
1080 target.save
1080 target.save
1081 issue.project = target
1081 issue.project = target
1082 assert issue.save
1082 assert issue.save
1083 issue.reload
1083 issue.reload
1084 assert_equal 2, issue.project_id
1084 assert_equal 2, issue.project_id
1085 assert_equal 3, issue.tracker_id
1085 assert_equal 3, issue.tracker_id
1086 end
1086 end
1087
1087
1088 def test_copy_to_the_same_project
1088 def test_copy_to_the_same_project
1089 issue = Issue.find(1)
1089 issue = Issue.find(1)
1090 copy = issue.copy
1090 copy = issue.copy
1091 assert_difference 'Issue.count' do
1091 assert_difference 'Issue.count' do
1092 copy.save!
1092 copy.save!
1093 end
1093 end
1094 assert_kind_of Issue, copy
1094 assert_kind_of Issue, copy
1095 assert_equal issue.project, copy.project
1095 assert_equal issue.project, copy.project
1096 assert_equal "125", copy.custom_value_for(2).value
1096 assert_equal "125", copy.custom_value_for(2).value
1097 end
1097 end
1098
1098
1099 def test_copy_to_another_project_and_tracker
1099 def test_copy_to_another_project_and_tracker
1100 issue = Issue.find(1)
1100 issue = Issue.find(1)
1101 copy = issue.copy(:project_id => 3, :tracker_id => 2)
1101 copy = issue.copy(:project_id => 3, :tracker_id => 2)
1102 assert_difference 'Issue.count' do
1102 assert_difference 'Issue.count' do
1103 copy.save!
1103 copy.save!
1104 end
1104 end
1105 copy.reload
1105 copy.reload
1106 assert_kind_of Issue, copy
1106 assert_kind_of Issue, copy
1107 assert_equal Project.find(3), copy.project
1107 assert_equal Project.find(3), copy.project
1108 assert_equal Tracker.find(2), copy.tracker
1108 assert_equal Tracker.find(2), copy.tracker
1109 # Custom field #2 is not associated with target tracker
1109 # Custom field #2 is not associated with target tracker
1110 assert_nil copy.custom_value_for(2)
1110 assert_nil copy.custom_value_for(2)
1111 end
1111 end
1112
1112
1113 context "#copy" do
1113 context "#copy" do
1114 setup do
1114 setup do
1115 @issue = Issue.find(1)
1115 @issue = Issue.find(1)
1116 end
1116 end
1117
1117
1118 should "not create a journal" do
1118 should "not create a journal" do
1119 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1119 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1120 copy.save!
1120 copy.save!
1121 assert_equal 0, copy.reload.journals.size
1121 assert_equal 0, copy.reload.journals.size
1122 end
1122 end
1123
1123
1124 should "allow assigned_to changes" do
1124 should "allow assigned_to changes" do
1125 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1125 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1126 assert_equal 3, copy.assigned_to_id
1126 assert_equal 3, copy.assigned_to_id
1127 end
1127 end
1128
1128
1129 should "allow status changes" do
1129 should "allow status changes" do
1130 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :status_id => 2)
1130 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :status_id => 2)
1131 assert_equal 2, copy.status_id
1131 assert_equal 2, copy.status_id
1132 end
1132 end
1133
1133
1134 should "allow start date changes" do
1134 should "allow start date changes" do
1135 date = Date.today
1135 date = Date.today
1136 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1136 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1137 assert_equal date, copy.start_date
1137 assert_equal date, copy.start_date
1138 end
1138 end
1139
1139
1140 should "allow due date changes" do
1140 should "allow due date changes" do
1141 date = Date.today
1141 date = Date.today
1142 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :due_date => date)
1142 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :due_date => date)
1143 assert_equal date, copy.due_date
1143 assert_equal date, copy.due_date
1144 end
1144 end
1145
1145
1146 should "set current user as author" do
1146 should "set current user as author" do
1147 User.current = User.find(9)
1147 User.current = User.find(9)
1148 copy = @issue.copy(:project_id => 3, :tracker_id => 2)
1148 copy = @issue.copy(:project_id => 3, :tracker_id => 2)
1149 assert_equal User.current, copy.author
1149 assert_equal User.current, copy.author
1150 end
1150 end
1151
1151
1152 should "create a journal with notes" do
1152 should "create a journal with notes" do
1153 date = Date.today
1153 date = Date.today
1154 notes = "Notes added when copying"
1154 notes = "Notes added when copying"
1155 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1155 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1156 copy.init_journal(User.current, notes)
1156 copy.init_journal(User.current, notes)
1157 copy.save!
1157 copy.save!
1158
1158
1159 assert_equal 1, copy.journals.size
1159 assert_equal 1, copy.journals.size
1160 journal = copy.journals.first
1160 journal = copy.journals.first
1161 assert_equal 0, journal.details.size
1161 assert_equal 0, journal.details.size
1162 assert_equal notes, journal.notes
1162 assert_equal notes, journal.notes
1163 end
1163 end
1164 end
1164 end
1165
1165
1166 def test_valid_parent_project
1166 def test_valid_parent_project
1167 issue = Issue.find(1)
1167 issue = Issue.find(1)
1168 issue_in_same_project = Issue.find(2)
1168 issue_in_same_project = Issue.find(2)
1169 issue_in_child_project = Issue.find(5)
1169 issue_in_child_project = Issue.find(5)
1170 issue_in_grandchild_project = Issue.generate!(:project_id => 6, :tracker_id => 1)
1170 issue_in_grandchild_project = Issue.generate!(:project_id => 6, :tracker_id => 1)
1171 issue_in_other_child_project = Issue.find(6)
1171 issue_in_other_child_project = Issue.find(6)
1172 issue_in_different_tree = Issue.find(4)
1172 issue_in_different_tree = Issue.find(4)
1173
1173
1174 with_settings :cross_project_subtasks => '' do
1174 with_settings :cross_project_subtasks => '' do
1175 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1175 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1176 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1176 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1177 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1177 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1178 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1178 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1179 end
1179 end
1180
1180
1181 with_settings :cross_project_subtasks => 'system' do
1181 with_settings :cross_project_subtasks => 'system' do
1182 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1182 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1183 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1183 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1184 assert_equal true, issue.valid_parent_project?(issue_in_different_tree)
1184 assert_equal true, issue.valid_parent_project?(issue_in_different_tree)
1185 end
1185 end
1186
1186
1187 with_settings :cross_project_subtasks => 'tree' do
1187 with_settings :cross_project_subtasks => 'tree' do
1188 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1188 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1189 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1189 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1190 assert_equal true, issue.valid_parent_project?(issue_in_grandchild_project)
1190 assert_equal true, issue.valid_parent_project?(issue_in_grandchild_project)
1191 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1191 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1192
1192
1193 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_same_project)
1193 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_same_project)
1194 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1194 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1195 end
1195 end
1196
1196
1197 with_settings :cross_project_subtasks => 'descendants' do
1197 with_settings :cross_project_subtasks => 'descendants' do
1198 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1198 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1199 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1199 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1200 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1200 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1201 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1201 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1202
1202
1203 assert_equal true, issue_in_child_project.valid_parent_project?(issue)
1203 assert_equal true, issue_in_child_project.valid_parent_project?(issue)
1204 assert_equal false, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1204 assert_equal false, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1205 end
1205 end
1206 end
1206 end
1207
1207
1208 def test_recipients_should_include_previous_assignee
1208 def test_recipients_should_include_previous_assignee
1209 user = User.find(3)
1209 user = User.find(3)
1210 user.members.update_all ["mail_notification = ?", false]
1210 user.members.update_all ["mail_notification = ?", false]
1211 user.update_attribute :mail_notification, 'only_assigned'
1211 user.update_attribute :mail_notification, 'only_assigned'
1212
1212
1213 issue = Issue.find(2)
1213 issue = Issue.find(2)
1214 issue.assigned_to = nil
1214 issue.assigned_to = nil
1215 assert_include user.mail, issue.recipients
1215 assert_include user.mail, issue.recipients
1216 issue.save!
1216 issue.save!
1217 assert !issue.recipients.include?(user.mail)
1217 assert !issue.recipients.include?(user.mail)
1218 end
1218 end
1219
1219
1220 def test_recipients_should_not_include_users_that_cannot_view_the_issue
1220 def test_recipients_should_not_include_users_that_cannot_view_the_issue
1221 issue = Issue.find(12)
1221 issue = Issue.find(12)
1222 assert issue.recipients.include?(issue.author.mail)
1222 assert issue.recipients.include?(issue.author.mail)
1223 # copy the issue to a private project
1223 # copy the issue to a private project
1224 copy = issue.copy(:project_id => 5, :tracker_id => 2)
1224 copy = issue.copy(:project_id => 5, :tracker_id => 2)
1225 # author is not a member of project anymore
1225 # author is not a member of project anymore
1226 assert !copy.recipients.include?(copy.author.mail)
1226 assert !copy.recipients.include?(copy.author.mail)
1227 end
1227 end
1228
1228
1229 def test_recipients_should_include_the_assigned_group_members
1229 def test_recipients_should_include_the_assigned_group_members
1230 group_member = User.generate!
1230 group_member = User.generate!
1231 group = Group.generate!
1231 group = Group.generate!
1232 group.users << group_member
1232 group.users << group_member
1233
1233
1234 issue = Issue.find(12)
1234 issue = Issue.find(12)
1235 issue.assigned_to = group
1235 issue.assigned_to = group
1236 assert issue.recipients.include?(group_member.mail)
1236 assert issue.recipients.include?(group_member.mail)
1237 end
1237 end
1238
1238
1239 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
1239 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
1240 user = User.find(3)
1240 user = User.find(3)
1241 issue = Issue.find(9)
1241 issue = Issue.find(9)
1242 Watcher.create!(:user => user, :watchable => issue)
1242 Watcher.create!(:user => user, :watchable => issue)
1243 assert issue.watched_by?(user)
1243 assert issue.watched_by?(user)
1244 assert !issue.watcher_recipients.include?(user.mail)
1244 assert !issue.watcher_recipients.include?(user.mail)
1245 end
1245 end
1246
1246
1247 def test_issue_destroy
1247 def test_issue_destroy
1248 Issue.find(1).destroy
1248 Issue.find(1).destroy
1249 assert_nil Issue.find_by_id(1)
1249 assert_nil Issue.find_by_id(1)
1250 assert_nil TimeEntry.find_by_issue_id(1)
1250 assert_nil TimeEntry.find_by_issue_id(1)
1251 end
1251 end
1252
1252
1253 def test_destroying_a_deleted_issue_should_not_raise_an_error
1253 def test_destroying_a_deleted_issue_should_not_raise_an_error
1254 issue = Issue.find(1)
1254 issue = Issue.find(1)
1255 Issue.find(1).destroy
1255 Issue.find(1).destroy
1256
1256
1257 assert_nothing_raised do
1257 assert_nothing_raised do
1258 assert_no_difference 'Issue.count' do
1258 assert_no_difference 'Issue.count' do
1259 issue.destroy
1259 issue.destroy
1260 end
1260 end
1261 assert issue.destroyed?
1261 assert issue.destroyed?
1262 end
1262 end
1263 end
1263 end
1264
1264
1265 def test_destroying_a_stale_issue_should_not_raise_an_error
1265 def test_destroying_a_stale_issue_should_not_raise_an_error
1266 issue = Issue.find(1)
1266 issue = Issue.find(1)
1267 Issue.find(1).update_attribute :subject, "Updated"
1267 Issue.find(1).update_attribute :subject, "Updated"
1268
1268
1269 assert_nothing_raised do
1269 assert_nothing_raised do
1270 assert_difference 'Issue.count', -1 do
1270 assert_difference 'Issue.count', -1 do
1271 issue.destroy
1271 issue.destroy
1272 end
1272 end
1273 assert issue.destroyed?
1273 assert issue.destroyed?
1274 end
1274 end
1275 end
1275 end
1276
1276
1277 def test_blocked
1277 def test_blocked
1278 blocked_issue = Issue.find(9)
1278 blocked_issue = Issue.find(9)
1279 blocking_issue = Issue.find(10)
1279 blocking_issue = Issue.find(10)
1280
1280
1281 assert blocked_issue.blocked?
1281 assert blocked_issue.blocked?
1282 assert !blocking_issue.blocked?
1282 assert !blocking_issue.blocked?
1283 end
1283 end
1284
1284
1285 def test_blocked_issues_dont_allow_closed_statuses
1285 def test_blocked_issues_dont_allow_closed_statuses
1286 blocked_issue = Issue.find(9)
1286 blocked_issue = Issue.find(9)
1287
1287
1288 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
1288 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
1289 assert !allowed_statuses.empty?
1289 assert !allowed_statuses.empty?
1290 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1290 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1291 assert closed_statuses.empty?
1291 assert closed_statuses.empty?
1292 end
1292 end
1293
1293
1294 def test_unblocked_issues_allow_closed_statuses
1294 def test_unblocked_issues_allow_closed_statuses
1295 blocking_issue = Issue.find(10)
1295 blocking_issue = Issue.find(10)
1296
1296
1297 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
1297 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
1298 assert !allowed_statuses.empty?
1298 assert !allowed_statuses.empty?
1299 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1299 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1300 assert !closed_statuses.empty?
1300 assert !closed_statuses.empty?
1301 end
1301 end
1302
1302
1303 def test_reschedule_an_issue_without_dates
1303 def test_reschedule_an_issue_without_dates
1304 with_settings :non_working_week_days => [] do
1304 with_settings :non_working_week_days => [] do
1305 issue = Issue.new(:start_date => nil, :due_date => nil)
1305 issue = Issue.new(:start_date => nil, :due_date => nil)
1306 issue.reschedule_on '2012-10-09'.to_date
1306 issue.reschedule_on '2012-10-09'.to_date
1307 assert_equal '2012-10-09'.to_date, issue.start_date
1307 assert_equal '2012-10-09'.to_date, issue.start_date
1308 assert_equal '2012-10-09'.to_date, issue.due_date
1308 assert_equal '2012-10-09'.to_date, issue.due_date
1309 end
1309 end
1310
1310
1311 with_settings :non_working_week_days => %w(6 7) do
1311 with_settings :non_working_week_days => %w(6 7) do
1312 issue = Issue.new(:start_date => nil, :due_date => nil)
1312 issue = Issue.new(:start_date => nil, :due_date => nil)
1313 issue.reschedule_on '2012-10-09'.to_date
1313 issue.reschedule_on '2012-10-09'.to_date
1314 assert_equal '2012-10-09'.to_date, issue.start_date
1314 assert_equal '2012-10-09'.to_date, issue.start_date
1315 assert_equal '2012-10-09'.to_date, issue.due_date
1315 assert_equal '2012-10-09'.to_date, issue.due_date
1316
1316
1317 issue = Issue.new(:start_date => nil, :due_date => nil)
1317 issue = Issue.new(:start_date => nil, :due_date => nil)
1318 issue.reschedule_on '2012-10-13'.to_date
1318 issue.reschedule_on '2012-10-13'.to_date
1319 assert_equal '2012-10-15'.to_date, issue.start_date
1319 assert_equal '2012-10-15'.to_date, issue.start_date
1320 assert_equal '2012-10-15'.to_date, issue.due_date
1320 assert_equal '2012-10-15'.to_date, issue.due_date
1321 end
1321 end
1322 end
1322 end
1323
1323
1324 def test_reschedule_an_issue_with_start_date
1324 def test_reschedule_an_issue_with_start_date
1325 with_settings :non_working_week_days => [] do
1325 with_settings :non_working_week_days => [] do
1326 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1326 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1327 issue.reschedule_on '2012-10-13'.to_date
1327 issue.reschedule_on '2012-10-13'.to_date
1328 assert_equal '2012-10-13'.to_date, issue.start_date
1328 assert_equal '2012-10-13'.to_date, issue.start_date
1329 assert_equal '2012-10-13'.to_date, issue.due_date
1329 assert_equal '2012-10-13'.to_date, issue.due_date
1330 end
1330 end
1331
1331
1332 with_settings :non_working_week_days => %w(6 7) do
1332 with_settings :non_working_week_days => %w(6 7) do
1333 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1333 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1334 issue.reschedule_on '2012-10-11'.to_date
1334 issue.reschedule_on '2012-10-11'.to_date
1335 assert_equal '2012-10-11'.to_date, issue.start_date
1335 assert_equal '2012-10-11'.to_date, issue.start_date
1336 assert_equal '2012-10-11'.to_date, issue.due_date
1336 assert_equal '2012-10-11'.to_date, issue.due_date
1337
1337
1338 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1338 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1339 issue.reschedule_on '2012-10-13'.to_date
1339 issue.reschedule_on '2012-10-13'.to_date
1340 assert_equal '2012-10-15'.to_date, issue.start_date
1340 assert_equal '2012-10-15'.to_date, issue.start_date
1341 assert_equal '2012-10-15'.to_date, issue.due_date
1341 assert_equal '2012-10-15'.to_date, issue.due_date
1342 end
1342 end
1343 end
1343 end
1344
1344
1345 def test_reschedule_an_issue_with_start_and_due_dates
1345 def test_reschedule_an_issue_with_start_and_due_dates
1346 with_settings :non_working_week_days => [] do
1346 with_settings :non_working_week_days => [] do
1347 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-15')
1347 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-15')
1348 issue.reschedule_on '2012-10-13'.to_date
1348 issue.reschedule_on '2012-10-13'.to_date
1349 assert_equal '2012-10-13'.to_date, issue.start_date
1349 assert_equal '2012-10-13'.to_date, issue.start_date
1350 assert_equal '2012-10-19'.to_date, issue.due_date
1350 assert_equal '2012-10-19'.to_date, issue.due_date
1351 end
1351 end
1352
1352
1353 with_settings :non_working_week_days => %w(6 7) do
1353 with_settings :non_working_week_days => %w(6 7) do
1354 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19') # 8 working days
1354 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19') # 8 working days
1355 issue.reschedule_on '2012-10-11'.to_date
1355 issue.reschedule_on '2012-10-11'.to_date
1356 assert_equal '2012-10-11'.to_date, issue.start_date
1356 assert_equal '2012-10-11'.to_date, issue.start_date
1357 assert_equal '2012-10-23'.to_date, issue.due_date
1357 assert_equal '2012-10-23'.to_date, issue.due_date
1358
1358
1359 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19')
1359 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19')
1360 issue.reschedule_on '2012-10-13'.to_date
1360 issue.reschedule_on '2012-10-13'.to_date
1361 assert_equal '2012-10-15'.to_date, issue.start_date
1361 assert_equal '2012-10-15'.to_date, issue.start_date
1362 assert_equal '2012-10-25'.to_date, issue.due_date
1362 assert_equal '2012-10-25'.to_date, issue.due_date
1363 end
1363 end
1364 end
1364 end
1365
1365
1366 def test_rescheduling_an_issue_to_a_later_due_date_should_reschedule_following_issue
1366 def test_rescheduling_an_issue_to_a_later_due_date_should_reschedule_following_issue
1367 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1367 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1368 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1368 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1369 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1369 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1370 :relation_type => IssueRelation::TYPE_PRECEDES)
1370 :relation_type => IssueRelation::TYPE_PRECEDES)
1371 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1371 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1372
1372
1373 issue1.due_date = '2012-10-23'
1373 issue1.due_date = '2012-10-23'
1374 issue1.save!
1374 issue1.save!
1375 issue2.reload
1375 issue2.reload
1376 assert_equal Date.parse('2012-10-24'), issue2.start_date
1376 assert_equal Date.parse('2012-10-24'), issue2.start_date
1377 assert_equal Date.parse('2012-10-26'), issue2.due_date
1377 assert_equal Date.parse('2012-10-26'), issue2.due_date
1378 end
1378 end
1379
1379
1380 def test_rescheduling_an_issue_to_an_earlier_due_date_should_reschedule_following_issue
1380 def test_rescheduling_an_issue_to_an_earlier_due_date_should_reschedule_following_issue
1381 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1381 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1382 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1382 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1383 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1383 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1384 :relation_type => IssueRelation::TYPE_PRECEDES)
1384 :relation_type => IssueRelation::TYPE_PRECEDES)
1385 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1385 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1386
1386
1387 issue1.start_date = '2012-09-17'
1387 issue1.start_date = '2012-09-17'
1388 issue1.due_date = '2012-09-18'
1388 issue1.due_date = '2012-09-18'
1389 issue1.save!
1389 issue1.save!
1390 issue2.reload
1390 issue2.reload
1391 assert_equal Date.parse('2012-09-19'), issue2.start_date
1391 assert_equal Date.parse('2012-09-19'), issue2.start_date
1392 assert_equal Date.parse('2012-09-21'), issue2.due_date
1392 assert_equal Date.parse('2012-09-21'), issue2.due_date
1393 end
1393 end
1394
1394
1395 def test_rescheduling_reschedule_following_issue_earlier_should_consider_other_preceding_issues
1395 def test_rescheduling_reschedule_following_issue_earlier_should_consider_other_preceding_issues
1396 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1396 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1397 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1397 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1398 issue3 = Issue.generate!(:start_date => '2012-10-01', :due_date => '2012-10-02')
1398 issue3 = Issue.generate!(:start_date => '2012-10-01', :due_date => '2012-10-02')
1399 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1399 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1400 :relation_type => IssueRelation::TYPE_PRECEDES)
1400 :relation_type => IssueRelation::TYPE_PRECEDES)
1401 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
1401 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
1402 :relation_type => IssueRelation::TYPE_PRECEDES)
1402 :relation_type => IssueRelation::TYPE_PRECEDES)
1403 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1403 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1404
1404
1405 issue1.start_date = '2012-09-17'
1405 issue1.start_date = '2012-09-17'
1406 issue1.due_date = '2012-09-18'
1406 issue1.due_date = '2012-09-18'
1407 issue1.save!
1407 issue1.save!
1408 issue2.reload
1408 issue2.reload
1409 # Issue 2 must start after Issue 3
1409 # Issue 2 must start after Issue 3
1410 assert_equal Date.parse('2012-10-03'), issue2.start_date
1410 assert_equal Date.parse('2012-10-03'), issue2.start_date
1411 assert_equal Date.parse('2012-10-05'), issue2.due_date
1411 assert_equal Date.parse('2012-10-05'), issue2.due_date
1412 end
1412 end
1413
1413
1414 def test_rescheduling_a_stale_issue_should_not_raise_an_error
1414 def test_rescheduling_a_stale_issue_should_not_raise_an_error
1415 with_settings :non_working_week_days => [] do
1415 with_settings :non_working_week_days => [] do
1416 stale = Issue.find(1)
1416 stale = Issue.find(1)
1417 issue = Issue.find(1)
1417 issue = Issue.find(1)
1418 issue.subject = "Updated"
1418 issue.subject = "Updated"
1419 issue.save!
1419 issue.save!
1420 date = 10.days.from_now.to_date
1420 date = 10.days.from_now.to_date
1421 assert_nothing_raised do
1421 assert_nothing_raised do
1422 stale.reschedule_on!(date)
1422 stale.reschedule_on!(date)
1423 end
1423 end
1424 assert_equal date, stale.reload.start_date
1424 assert_equal date, stale.reload.start_date
1425 end
1425 end
1426 end
1426 end
1427
1427
1428 def test_overdue
1428 def test_overdue
1429 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
1429 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
1430 assert !Issue.new(:due_date => Date.today).overdue?
1430 assert !Issue.new(:due_date => Date.today).overdue?
1431 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
1431 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
1432 assert !Issue.new(:due_date => nil).overdue?
1432 assert !Issue.new(:due_date => nil).overdue?
1433 assert !Issue.new(:due_date => 1.day.ago.to_date,
1433 assert !Issue.new(:due_date => 1.day.ago.to_date,
1434 :status => IssueStatus.where(:is_closed => true).first
1434 :status => IssueStatus.where(:is_closed => true).first
1435 ).overdue?
1435 ).overdue?
1436 end
1436 end
1437
1437
1438 context "#behind_schedule?" do
1438 context "#behind_schedule?" do
1439 should "be false if the issue has no start_date" do
1439 should "be false if the issue has no start_date" do
1440 assert !Issue.new(:start_date => nil,
1440 assert !Issue.new(:start_date => nil,
1441 :due_date => 1.day.from_now.to_date,
1441 :due_date => 1.day.from_now.to_date,
1442 :done_ratio => 0).behind_schedule?
1442 :done_ratio => 0).behind_schedule?
1443 end
1443 end
1444
1444
1445 should "be false if the issue has no end_date" do
1445 should "be false if the issue has no end_date" do
1446 assert !Issue.new(:start_date => 1.day.from_now.to_date,
1446 assert !Issue.new(:start_date => 1.day.from_now.to_date,
1447 :due_date => nil,
1447 :due_date => nil,
1448 :done_ratio => 0).behind_schedule?
1448 :done_ratio => 0).behind_schedule?
1449 end
1449 end
1450
1450
1451 should "be false if the issue has more done than it's calendar time" do
1451 should "be false if the issue has more done than it's calendar time" do
1452 assert !Issue.new(:start_date => 50.days.ago.to_date,
1452 assert !Issue.new(:start_date => 50.days.ago.to_date,
1453 :due_date => 50.days.from_now.to_date,
1453 :due_date => 50.days.from_now.to_date,
1454 :done_ratio => 90).behind_schedule?
1454 :done_ratio => 90).behind_schedule?
1455 end
1455 end
1456
1456
1457 should "be true if the issue hasn't been started at all" do
1457 should "be true if the issue hasn't been started at all" do
1458 assert Issue.new(:start_date => 1.day.ago.to_date,
1458 assert Issue.new(:start_date => 1.day.ago.to_date,
1459 :due_date => 1.day.from_now.to_date,
1459 :due_date => 1.day.from_now.to_date,
1460 :done_ratio => 0).behind_schedule?
1460 :done_ratio => 0).behind_schedule?
1461 end
1461 end
1462
1462
1463 should "be true if the issue has used more calendar time than it's done ratio" do
1463 should "be true if the issue has used more calendar time than it's done ratio" do
1464 assert Issue.new(:start_date => 100.days.ago.to_date,
1464 assert Issue.new(:start_date => 100.days.ago.to_date,
1465 :due_date => Date.today,
1465 :due_date => Date.today,
1466 :done_ratio => 90).behind_schedule?
1466 :done_ratio => 90).behind_schedule?
1467 end
1467 end
1468 end
1468 end
1469
1469
1470 context "#assignable_users" do
1470 context "#assignable_users" do
1471 should "be Users" do
1471 should "be Users" do
1472 assert_kind_of User, Issue.find(1).assignable_users.first
1472 assert_kind_of User, Issue.find(1).assignable_users.first
1473 end
1473 end
1474
1474
1475 should "include the issue author" do
1475 should "include the issue author" do
1476 non_project_member = User.generate!
1476 non_project_member = User.generate!
1477 issue = Issue.generate!(:author => non_project_member)
1477 issue = Issue.generate!(:author => non_project_member)
1478
1478
1479 assert issue.assignable_users.include?(non_project_member)
1479 assert issue.assignable_users.include?(non_project_member)
1480 end
1480 end
1481
1481
1482 should "include the current assignee" do
1482 should "include the current assignee" do
1483 user = User.generate!
1483 user = User.generate!
1484 issue = Issue.generate!(:assigned_to => user)
1484 issue = Issue.generate!(:assigned_to => user)
1485 user.lock!
1485 user.lock!
1486
1486
1487 assert Issue.find(issue.id).assignable_users.include?(user)
1487 assert Issue.find(issue.id).assignable_users.include?(user)
1488 end
1488 end
1489
1489
1490 should "not show the issue author twice" do
1490 should "not show the issue author twice" do
1491 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
1491 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
1492 assert_equal 2, assignable_user_ids.length
1492 assert_equal 2, assignable_user_ids.length
1493
1493
1494 assignable_user_ids.each do |user_id|
1494 assignable_user_ids.each do |user_id|
1495 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length,
1495 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length,
1496 "User #{user_id} appears more or less than once"
1496 "User #{user_id} appears more or less than once"
1497 end
1497 end
1498 end
1498 end
1499
1499
1500 context "with issue_group_assignment" do
1500 context "with issue_group_assignment" do
1501 should "include groups" do
1501 should "include groups" do
1502 issue = Issue.new(:project => Project.find(2))
1502 issue = Issue.new(:project => Project.find(2))
1503
1503
1504 with_settings :issue_group_assignment => '1' do
1504 with_settings :issue_group_assignment => '1' do
1505 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1505 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1506 assert issue.assignable_users.include?(Group.find(11))
1506 assert issue.assignable_users.include?(Group.find(11))
1507 end
1507 end
1508 end
1508 end
1509 end
1509 end
1510
1510
1511 context "without issue_group_assignment" do
1511 context "without issue_group_assignment" do
1512 should "not include groups" do
1512 should "not include groups" do
1513 issue = Issue.new(:project => Project.find(2))
1513 issue = Issue.new(:project => Project.find(2))
1514
1514
1515 with_settings :issue_group_assignment => '0' do
1515 with_settings :issue_group_assignment => '0' do
1516 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1516 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1517 assert !issue.assignable_users.include?(Group.find(11))
1517 assert !issue.assignable_users.include?(Group.find(11))
1518 end
1518 end
1519 end
1519 end
1520 end
1520 end
1521 end
1521 end
1522
1522
1523 def test_create_should_send_email_notification
1523 def test_create_should_send_email_notification
1524 ActionMailer::Base.deliveries.clear
1524 ActionMailer::Base.deliveries.clear
1525 issue = Issue.new(:project_id => 1, :tracker_id => 1,
1525 issue = Issue.new(:project_id => 1, :tracker_id => 1,
1526 :author_id => 3, :status_id => 1,
1526 :author_id => 3, :status_id => 1,
1527 :priority => IssuePriority.all.first,
1527 :priority => IssuePriority.all.first,
1528 :subject => 'test_create', :estimated_hours => '1:30')
1528 :subject => 'test_create', :estimated_hours => '1:30')
1529
1529
1530 assert issue.save
1530 assert issue.save
1531 assert_equal 1, ActionMailer::Base.deliveries.size
1531 assert_equal 1, ActionMailer::Base.deliveries.size
1532 end
1532 end
1533
1533
1534 def test_stale_issue_should_not_send_email_notification
1534 def test_stale_issue_should_not_send_email_notification
1535 ActionMailer::Base.deliveries.clear
1535 ActionMailer::Base.deliveries.clear
1536 issue = Issue.find(1)
1536 issue = Issue.find(1)
1537 stale = Issue.find(1)
1537 stale = Issue.find(1)
1538
1538
1539 issue.init_journal(User.find(1))
1539 issue.init_journal(User.find(1))
1540 issue.subject = 'Subjet update'
1540 issue.subject = 'Subjet update'
1541 assert issue.save
1541 assert issue.save
1542 assert_equal 1, ActionMailer::Base.deliveries.size
1542 assert_equal 1, ActionMailer::Base.deliveries.size
1543 ActionMailer::Base.deliveries.clear
1543 ActionMailer::Base.deliveries.clear
1544
1544
1545 stale.init_journal(User.find(1))
1545 stale.init_journal(User.find(1))
1546 stale.subject = 'Another subjet update'
1546 stale.subject = 'Another subjet update'
1547 assert_raise ActiveRecord::StaleObjectError do
1547 assert_raise ActiveRecord::StaleObjectError do
1548 stale.save
1548 stale.save
1549 end
1549 end
1550 assert ActionMailer::Base.deliveries.empty?
1550 assert ActionMailer::Base.deliveries.empty?
1551 end
1551 end
1552
1552
1553 def test_journalized_description
1553 def test_journalized_description
1554 IssueCustomField.delete_all
1554 IssueCustomField.delete_all
1555
1555
1556 i = Issue.first
1556 i = Issue.first
1557 old_description = i.description
1557 old_description = i.description
1558 new_description = "This is the new description"
1558 new_description = "This is the new description"
1559
1559
1560 i.init_journal(User.find(2))
1560 i.init_journal(User.find(2))
1561 i.description = new_description
1561 i.description = new_description
1562 assert_difference 'Journal.count', 1 do
1562 assert_difference 'Journal.count', 1 do
1563 assert_difference 'JournalDetail.count', 1 do
1563 assert_difference 'JournalDetail.count', 1 do
1564 i.save!
1564 i.save!
1565 end
1565 end
1566 end
1566 end
1567
1567
1568 detail = JournalDetail.first(:order => 'id DESC')
1568 detail = JournalDetail.first(:order => 'id DESC')
1569 assert_equal i, detail.journal.journalized
1569 assert_equal i, detail.journal.journalized
1570 assert_equal 'attr', detail.property
1570 assert_equal 'attr', detail.property
1571 assert_equal 'description', detail.prop_key
1571 assert_equal 'description', detail.prop_key
1572 assert_equal old_description, detail.old_value
1572 assert_equal old_description, detail.old_value
1573 assert_equal new_description, detail.value
1573 assert_equal new_description, detail.value
1574 end
1574 end
1575
1575
1576 def test_blank_descriptions_should_not_be_journalized
1576 def test_blank_descriptions_should_not_be_journalized
1577 IssueCustomField.delete_all
1577 IssueCustomField.delete_all
1578 Issue.update_all("description = NULL", "id=1")
1578 Issue.update_all("description = NULL", "id=1")
1579
1579
1580 i = Issue.find(1)
1580 i = Issue.find(1)
1581 i.init_journal(User.find(2))
1581 i.init_journal(User.find(2))
1582 i.subject = "blank description"
1582 i.subject = "blank description"
1583 i.description = "\r\n"
1583 i.description = "\r\n"
1584
1584
1585 assert_difference 'Journal.count', 1 do
1585 assert_difference 'Journal.count', 1 do
1586 assert_difference 'JournalDetail.count', 1 do
1586 assert_difference 'JournalDetail.count', 1 do
1587 i.save!
1587 i.save!
1588 end
1588 end
1589 end
1589 end
1590 end
1590 end
1591
1591
1592 def test_journalized_multi_custom_field
1592 def test_journalized_multi_custom_field
1593 field = IssueCustomField.create!(:name => 'filter', :field_format => 'list',
1593 field = IssueCustomField.create!(:name => 'filter', :field_format => 'list',
1594 :is_filter => true, :is_for_all => true,
1594 :is_filter => true, :is_for_all => true,
1595 :tracker_ids => [1],
1595 :tracker_ids => [1],
1596 :possible_values => ['value1', 'value2', 'value3'],
1596 :possible_values => ['value1', 'value2', 'value3'],
1597 :multiple => true)
1597 :multiple => true)
1598
1598
1599 issue = Issue.create!(:project_id => 1, :tracker_id => 1,
1599 issue = Issue.create!(:project_id => 1, :tracker_id => 1,
1600 :subject => 'Test', :author_id => 1)
1600 :subject => 'Test', :author_id => 1)
1601
1601
1602 assert_difference 'Journal.count' do
1602 assert_difference 'Journal.count' do
1603 assert_difference 'JournalDetail.count' do
1603 assert_difference 'JournalDetail.count' do
1604 issue.init_journal(User.first)
1604 issue.init_journal(User.first)
1605 issue.custom_field_values = {field.id => ['value1']}
1605 issue.custom_field_values = {field.id => ['value1']}
1606 issue.save!
1606 issue.save!
1607 end
1607 end
1608 assert_difference 'JournalDetail.count' do
1608 assert_difference 'JournalDetail.count' do
1609 issue.init_journal(User.first)
1609 issue.init_journal(User.first)
1610 issue.custom_field_values = {field.id => ['value1', 'value2']}
1610 issue.custom_field_values = {field.id => ['value1', 'value2']}
1611 issue.save!
1611 issue.save!
1612 end
1612 end
1613 assert_difference 'JournalDetail.count', 2 do
1613 assert_difference 'JournalDetail.count', 2 do
1614 issue.init_journal(User.first)
1614 issue.init_journal(User.first)
1615 issue.custom_field_values = {field.id => ['value3', 'value2']}
1615 issue.custom_field_values = {field.id => ['value3', 'value2']}
1616 issue.save!
1616 issue.save!
1617 end
1617 end
1618 assert_difference 'JournalDetail.count', 2 do
1618 assert_difference 'JournalDetail.count', 2 do
1619 issue.init_journal(User.first)
1619 issue.init_journal(User.first)
1620 issue.custom_field_values = {field.id => nil}
1620 issue.custom_field_values = {field.id => nil}
1621 issue.save!
1621 issue.save!
1622 end
1622 end
1623 end
1623 end
1624 end
1624 end
1625
1625
1626 def test_description_eol_should_be_normalized
1626 def test_description_eol_should_be_normalized
1627 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
1627 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
1628 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
1628 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
1629 end
1629 end
1630
1630
1631 def test_saving_twice_should_not_duplicate_journal_details
1631 def test_saving_twice_should_not_duplicate_journal_details
1632 i = Issue.first
1632 i = Issue.first
1633 i.init_journal(User.find(2), 'Some notes')
1633 i.init_journal(User.find(2), 'Some notes')
1634 # initial changes
1634 # initial changes
1635 i.subject = 'New subject'
1635 i.subject = 'New subject'
1636 i.done_ratio = i.done_ratio + 10
1636 i.done_ratio = i.done_ratio + 10
1637 assert_difference 'Journal.count' do
1637 assert_difference 'Journal.count' do
1638 assert i.save
1638 assert i.save
1639 end
1639 end
1640 # 1 more change
1640 # 1 more change
1641 i.priority = IssuePriority.where("id <> ?", i.priority_id).first
1641 i.priority = IssuePriority.where("id <> ?", i.priority_id).first
1642 assert_no_difference 'Journal.count' do
1642 assert_no_difference 'Journal.count' do
1643 assert_difference 'JournalDetail.count', 1 do
1643 assert_difference 'JournalDetail.count', 1 do
1644 i.save
1644 i.save
1645 end
1645 end
1646 end
1646 end
1647 # no more change
1647 # no more change
1648 assert_no_difference 'Journal.count' do
1648 assert_no_difference 'Journal.count' do
1649 assert_no_difference 'JournalDetail.count' do
1649 assert_no_difference 'JournalDetail.count' do
1650 i.save
1650 i.save
1651 end
1651 end
1652 end
1652 end
1653 end
1653 end
1654
1654
1655 def test_all_dependent_issues
1655 def test_all_dependent_issues
1656 IssueRelation.delete_all
1656 IssueRelation.delete_all
1657 assert IssueRelation.create!(:issue_from => Issue.find(1),
1657 assert IssueRelation.create!(:issue_from => Issue.find(1),
1658 :issue_to => Issue.find(2),
1658 :issue_to => Issue.find(2),
1659 :relation_type => IssueRelation::TYPE_PRECEDES)
1659 :relation_type => IssueRelation::TYPE_PRECEDES)
1660 assert IssueRelation.create!(:issue_from => Issue.find(2),
1660 assert IssueRelation.create!(:issue_from => Issue.find(2),
1661 :issue_to => Issue.find(3),
1661 :issue_to => Issue.find(3),
1662 :relation_type => IssueRelation::TYPE_PRECEDES)
1662 :relation_type => IssueRelation::TYPE_PRECEDES)
1663 assert IssueRelation.create!(:issue_from => Issue.find(3),
1663 assert IssueRelation.create!(:issue_from => Issue.find(3),
1664 :issue_to => Issue.find(8),
1664 :issue_to => Issue.find(8),
1665 :relation_type => IssueRelation::TYPE_PRECEDES)
1665 :relation_type => IssueRelation::TYPE_PRECEDES)
1666
1666
1667 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1667 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1668 end
1668 end
1669
1669
1670 def test_all_dependent_issues_with_persistent_circular_dependency
1670 def test_all_dependent_issues_with_persistent_circular_dependency
1671 IssueRelation.delete_all
1671 IssueRelation.delete_all
1672 assert IssueRelation.create!(:issue_from => Issue.find(1),
1672 assert IssueRelation.create!(:issue_from => Issue.find(1),
1673 :issue_to => Issue.find(2),
1673 :issue_to => Issue.find(2),
1674 :relation_type => IssueRelation::TYPE_PRECEDES)
1674 :relation_type => IssueRelation::TYPE_PRECEDES)
1675 assert IssueRelation.create!(:issue_from => Issue.find(2),
1675 assert IssueRelation.create!(:issue_from => Issue.find(2),
1676 :issue_to => Issue.find(3),
1676 :issue_to => Issue.find(3),
1677 :relation_type => IssueRelation::TYPE_PRECEDES)
1677 :relation_type => IssueRelation::TYPE_PRECEDES)
1678
1678
1679 r = IssueRelation.create!(:issue_from => Issue.find(3),
1679 r = IssueRelation.create!(:issue_from => Issue.find(3),
1680 :issue_to => Issue.find(7),
1680 :issue_to => Issue.find(7),
1681 :relation_type => IssueRelation::TYPE_PRECEDES)
1681 :relation_type => IssueRelation::TYPE_PRECEDES)
1682 IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id])
1682 IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id])
1683
1683
1684 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
1684 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
1685 end
1685 end
1686
1686
1687 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
1687 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
1688 IssueRelation.delete_all
1688 IssueRelation.delete_all
1689 assert IssueRelation.create!(:issue_from => Issue.find(1),
1689 assert IssueRelation.create!(:issue_from => Issue.find(1),
1690 :issue_to => Issue.find(2),
1690 :issue_to => Issue.find(2),
1691 :relation_type => IssueRelation::TYPE_RELATES)
1691 :relation_type => IssueRelation::TYPE_RELATES)
1692 assert IssueRelation.create!(:issue_from => Issue.find(2),
1692 assert IssueRelation.create!(:issue_from => Issue.find(2),
1693 :issue_to => Issue.find(3),
1693 :issue_to => Issue.find(3),
1694 :relation_type => IssueRelation::TYPE_RELATES)
1694 :relation_type => IssueRelation::TYPE_RELATES)
1695 assert IssueRelation.create!(:issue_from => Issue.find(3),
1695 assert IssueRelation.create!(:issue_from => Issue.find(3),
1696 :issue_to => Issue.find(8),
1696 :issue_to => Issue.find(8),
1697 :relation_type => IssueRelation::TYPE_RELATES)
1697 :relation_type => IssueRelation::TYPE_RELATES)
1698
1698
1699 r = IssueRelation.create!(:issue_from => Issue.find(8),
1699 r = IssueRelation.create!(:issue_from => Issue.find(8),
1700 :issue_to => Issue.find(7),
1700 :issue_to => Issue.find(7),
1701 :relation_type => IssueRelation::TYPE_RELATES)
1701 :relation_type => IssueRelation::TYPE_RELATES)
1702 IssueRelation.update_all("issue_to_id = 2", ["id = ?", r.id])
1702 IssueRelation.update_all("issue_to_id = 2", ["id = ?", r.id])
1703
1703
1704 r = IssueRelation.create!(:issue_from => Issue.find(3),
1704 r = IssueRelation.create!(:issue_from => Issue.find(3),
1705 :issue_to => Issue.find(7),
1705 :issue_to => Issue.find(7),
1706 :relation_type => IssueRelation::TYPE_RELATES)
1706 :relation_type => IssueRelation::TYPE_RELATES)
1707 IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id])
1707 IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id])
1708
1708
1709 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1709 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1710 end
1710 end
1711
1711
1712 context "#done_ratio" do
1712 context "#done_ratio" do
1713 setup do
1713 setup do
1714 @issue = Issue.find(1)
1714 @issue = Issue.find(1)
1715 @issue_status = IssueStatus.find(1)
1715 @issue_status = IssueStatus.find(1)
1716 @issue_status.update_attribute(:default_done_ratio, 50)
1716 @issue_status.update_attribute(:default_done_ratio, 50)
1717 @issue2 = Issue.find(2)
1717 @issue2 = Issue.find(2)
1718 @issue_status2 = IssueStatus.find(2)
1718 @issue_status2 = IssueStatus.find(2)
1719 @issue_status2.update_attribute(:default_done_ratio, 0)
1719 @issue_status2.update_attribute(:default_done_ratio, 0)
1720 end
1720 end
1721
1721
1722 teardown do
1722 teardown do
1723 Setting.issue_done_ratio = 'issue_field'
1723 Setting.issue_done_ratio = 'issue_field'
1724 end
1724 end
1725
1725
1726 context "with Setting.issue_done_ratio using the issue_field" do
1726 context "with Setting.issue_done_ratio using the issue_field" do
1727 setup do
1727 setup do
1728 Setting.issue_done_ratio = 'issue_field'
1728 Setting.issue_done_ratio = 'issue_field'
1729 end
1729 end
1730
1730
1731 should "read the issue's field" do
1731 should "read the issue's field" do
1732 assert_equal 0, @issue.done_ratio
1732 assert_equal 0, @issue.done_ratio
1733 assert_equal 30, @issue2.done_ratio
1733 assert_equal 30, @issue2.done_ratio
1734 end
1734 end
1735 end
1735 end
1736
1736
1737 context "with Setting.issue_done_ratio using the issue_status" do
1737 context "with Setting.issue_done_ratio using the issue_status" do
1738 setup do
1738 setup do
1739 Setting.issue_done_ratio = 'issue_status'
1739 Setting.issue_done_ratio = 'issue_status'
1740 end
1740 end
1741
1741
1742 should "read the Issue Status's default done ratio" do
1742 should "read the Issue Status's default done ratio" do
1743 assert_equal 50, @issue.done_ratio
1743 assert_equal 50, @issue.done_ratio
1744 assert_equal 0, @issue2.done_ratio
1744 assert_equal 0, @issue2.done_ratio
1745 end
1745 end
1746 end
1746 end
1747 end
1747 end
1748
1748
1749 context "#update_done_ratio_from_issue_status" do
1749 context "#update_done_ratio_from_issue_status" do
1750 setup do
1750 setup do
1751 @issue = Issue.find(1)
1751 @issue = Issue.find(1)
1752 @issue_status = IssueStatus.find(1)
1752 @issue_status = IssueStatus.find(1)
1753 @issue_status.update_attribute(:default_done_ratio, 50)
1753 @issue_status.update_attribute(:default_done_ratio, 50)
1754 @issue2 = Issue.find(2)
1754 @issue2 = Issue.find(2)
1755 @issue_status2 = IssueStatus.find(2)
1755 @issue_status2 = IssueStatus.find(2)
1756 @issue_status2.update_attribute(:default_done_ratio, 0)
1756 @issue_status2.update_attribute(:default_done_ratio, 0)
1757 end
1757 end
1758
1758
1759 context "with Setting.issue_done_ratio using the issue_field" do
1759 context "with Setting.issue_done_ratio using the issue_field" do
1760 setup do
1760 setup do
1761 Setting.issue_done_ratio = 'issue_field'
1761 Setting.issue_done_ratio = 'issue_field'
1762 end
1762 end
1763
1763
1764 should "not change the issue" do
1764 should "not change the issue" do
1765 @issue.update_done_ratio_from_issue_status
1765 @issue.update_done_ratio_from_issue_status
1766 @issue2.update_done_ratio_from_issue_status
1766 @issue2.update_done_ratio_from_issue_status
1767
1767
1768 assert_equal 0, @issue.read_attribute(:done_ratio)
1768 assert_equal 0, @issue.read_attribute(:done_ratio)
1769 assert_equal 30, @issue2.read_attribute(:done_ratio)
1769 assert_equal 30, @issue2.read_attribute(:done_ratio)
1770 end
1770 end
1771 end
1771 end
1772
1772
1773 context "with Setting.issue_done_ratio using the issue_status" do
1773 context "with Setting.issue_done_ratio using the issue_status" do
1774 setup do
1774 setup do
1775 Setting.issue_done_ratio = 'issue_status'
1775 Setting.issue_done_ratio = 'issue_status'
1776 end
1776 end
1777
1777
1778 should "change the issue's done ratio" do
1778 should "change the issue's done ratio" do
1779 @issue.update_done_ratio_from_issue_status
1779 @issue.update_done_ratio_from_issue_status
1780 @issue2.update_done_ratio_from_issue_status
1780 @issue2.update_done_ratio_from_issue_status
1781
1781
1782 assert_equal 50, @issue.read_attribute(:done_ratio)
1782 assert_equal 50, @issue.read_attribute(:done_ratio)
1783 assert_equal 0, @issue2.read_attribute(:done_ratio)
1783 assert_equal 0, @issue2.read_attribute(:done_ratio)
1784 end
1784 end
1785 end
1785 end
1786 end
1786 end
1787
1787
1788 test "#by_tracker" do
1788 test "#by_tracker" do
1789 User.current = User.anonymous
1789 User.current = User.anonymous
1790 groups = Issue.by_tracker(Project.find(1))
1790 groups = Issue.by_tracker(Project.find(1))
1791 assert_equal 3, groups.size
1791 assert_equal 3, groups.size
1792 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1792 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1793 end
1793 end
1794
1794
1795 test "#by_version" do
1795 test "#by_version" do
1796 User.current = User.anonymous
1796 User.current = User.anonymous
1797 groups = Issue.by_version(Project.find(1))
1797 groups = Issue.by_version(Project.find(1))
1798 assert_equal 3, groups.size
1798 assert_equal 3, groups.size
1799 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1799 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1800 end
1800 end
1801
1801
1802 test "#by_priority" do
1802 test "#by_priority" do
1803 User.current = User.anonymous
1803 User.current = User.anonymous
1804 groups = Issue.by_priority(Project.find(1))
1804 groups = Issue.by_priority(Project.find(1))
1805 assert_equal 4, groups.size
1805 assert_equal 4, groups.size
1806 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1806 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1807 end
1807 end
1808
1808
1809 test "#by_category" do
1809 test "#by_category" do
1810 User.current = User.anonymous
1810 User.current = User.anonymous
1811 groups = Issue.by_category(Project.find(1))
1811 groups = Issue.by_category(Project.find(1))
1812 assert_equal 2, groups.size
1812 assert_equal 2, groups.size
1813 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1813 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1814 end
1814 end
1815
1815
1816 test "#by_assigned_to" do
1816 test "#by_assigned_to" do
1817 User.current = User.anonymous
1817 User.current = User.anonymous
1818 groups = Issue.by_assigned_to(Project.find(1))
1818 groups = Issue.by_assigned_to(Project.find(1))
1819 assert_equal 2, groups.size
1819 assert_equal 2, groups.size
1820 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1820 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1821 end
1821 end
1822
1822
1823 test "#by_author" do
1823 test "#by_author" do
1824 User.current = User.anonymous
1824 User.current = User.anonymous
1825 groups = Issue.by_author(Project.find(1))
1825 groups = Issue.by_author(Project.find(1))
1826 assert_equal 4, groups.size
1826 assert_equal 4, groups.size
1827 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1827 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1828 end
1828 end
1829
1829
1830 test "#by_subproject" do
1830 test "#by_subproject" do
1831 User.current = User.anonymous
1831 User.current = User.anonymous
1832 groups = Issue.by_subproject(Project.find(1))
1832 groups = Issue.by_subproject(Project.find(1))
1833 # Private descendant not visible
1833 # Private descendant not visible
1834 assert_equal 1, groups.size
1834 assert_equal 1, groups.size
1835 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1835 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1836 end
1836 end
1837
1837
1838 def test_recently_updated_scope
1838 def test_recently_updated_scope
1839 #should return the last updated issue
1839 #should return the last updated issue
1840 assert_equal Issue.reorder("updated_on DESC").first, Issue.recently_updated.limit(1).first
1840 assert_equal Issue.reorder("updated_on DESC").first, Issue.recently_updated.limit(1).first
1841 end
1841 end
1842
1842
1843 def test_on_active_projects_scope
1843 def test_on_active_projects_scope
1844 assert Project.find(2).archive
1844 assert Project.find(2).archive
1845
1845
1846 before = Issue.on_active_project.length
1846 before = Issue.on_active_project.length
1847 # test inclusion to results
1847 # test inclusion to results
1848 issue = Issue.generate!(:tracker => Project.find(2).trackers.first)
1848 issue = Issue.generate!(:tracker => Project.find(2).trackers.first)
1849 assert_equal before + 1, Issue.on_active_project.length
1849 assert_equal before + 1, Issue.on_active_project.length
1850
1850
1851 # Move to an archived project
1851 # Move to an archived project
1852 issue.project = Project.find(2)
1852 issue.project = Project.find(2)
1853 assert issue.save
1853 assert issue.save
1854 assert_equal before, Issue.on_active_project.length
1854 assert_equal before, Issue.on_active_project.length
1855 end
1855 end
1856
1856
1857 context "Issue#recipients" do
1857 context "Issue#recipients" do
1858 setup do
1858 setup do
1859 @project = Project.find(1)
1859 @project = Project.find(1)
1860 @author = User.generate!
1860 @author = User.generate!
1861 @assignee = User.generate!
1861 @assignee = User.generate!
1862 @issue = Issue.generate!(:project => @project, :assigned_to => @assignee, :author => @author)
1862 @issue = Issue.generate!(:project => @project, :assigned_to => @assignee, :author => @author)
1863 end
1863 end
1864
1864
1865 should "include project recipients" do
1865 should "include project recipients" do
1866 assert @project.recipients.present?
1866 assert @project.recipients.present?
1867 @project.recipients.each do |project_recipient|
1867 @project.recipients.each do |project_recipient|
1868 assert @issue.recipients.include?(project_recipient)
1868 assert @issue.recipients.include?(project_recipient)
1869 end
1869 end
1870 end
1870 end
1871
1871
1872 should "include the author if the author is active" do
1872 should "include the author if the author is active" do
1873 assert @issue.author, "No author set for Issue"
1873 assert @issue.author, "No author set for Issue"
1874 assert @issue.recipients.include?(@issue.author.mail)
1874 assert @issue.recipients.include?(@issue.author.mail)
1875 end
1875 end
1876
1876
1877 should "include the assigned to user if the assigned to user is active" do
1877 should "include the assigned to user if the assigned to user is active" do
1878 assert @issue.assigned_to, "No assigned_to set for Issue"
1878 assert @issue.assigned_to, "No assigned_to set for Issue"
1879 assert @issue.recipients.include?(@issue.assigned_to.mail)
1879 assert @issue.recipients.include?(@issue.assigned_to.mail)
1880 end
1880 end
1881
1881
1882 should "not include users who opt out of all email" do
1882 should "not include users who opt out of all email" do
1883 @author.update_attribute(:mail_notification, :none)
1883 @author.update_attribute(:mail_notification, :none)
1884
1884
1885 assert !@issue.recipients.include?(@issue.author.mail)
1885 assert !@issue.recipients.include?(@issue.author.mail)
1886 end
1886 end
1887
1887
1888 should "not include the issue author if they are only notified of assigned issues" do
1888 should "not include the issue author if they are only notified of assigned issues" do
1889 @author.update_attribute(:mail_notification, :only_assigned)
1889 @author.update_attribute(:mail_notification, :only_assigned)
1890
1890
1891 assert !@issue.recipients.include?(@issue.author.mail)
1891 assert !@issue.recipients.include?(@issue.author.mail)
1892 end
1892 end
1893
1893
1894 should "not include the assigned user if they are only notified of owned issues" do
1894 should "not include the assigned user if they are only notified of owned issues" do
1895 @assignee.update_attribute(:mail_notification, :only_owner)
1895 @assignee.update_attribute(:mail_notification, :only_owner)
1896
1896
1897 assert !@issue.recipients.include?(@issue.assigned_to.mail)
1897 assert !@issue.recipients.include?(@issue.assigned_to.mail)
1898 end
1898 end
1899 end
1899 end
1900
1900
1901 def test_last_journal_id_with_journals_should_return_the_journal_id
1901 def test_last_journal_id_with_journals_should_return_the_journal_id
1902 assert_equal 2, Issue.find(1).last_journal_id
1902 assert_equal 2, Issue.find(1).last_journal_id
1903 end
1903 end
1904
1904
1905 def test_last_journal_id_without_journals_should_return_nil
1905 def test_last_journal_id_without_journals_should_return_nil
1906 assert_nil Issue.find(3).last_journal_id
1906 assert_nil Issue.find(3).last_journal_id
1907 end
1907 end
1908
1908
1909 def test_journals_after_should_return_journals_with_greater_id
1909 def test_journals_after_should_return_journals_with_greater_id
1910 assert_equal [Journal.find(2)], Issue.find(1).journals_after('1')
1910 assert_equal [Journal.find(2)], Issue.find(1).journals_after('1')
1911 assert_equal [], Issue.find(1).journals_after('2')
1911 assert_equal [], Issue.find(1).journals_after('2')
1912 end
1912 end
1913
1913
1914 def test_journals_after_with_blank_arg_should_return_all_journals
1914 def test_journals_after_with_blank_arg_should_return_all_journals
1915 assert_equal [Journal.find(1), Journal.find(2)], Issue.find(1).journals_after('')
1915 assert_equal [Journal.find(1), Journal.find(2)], Issue.find(1).journals_after('')
1916 end
1916 end
1917
1917
1918 def test_css_classes_should_include_priority
1918 def test_css_classes_should_include_priority
1919 issue = Issue.new(:priority => IssuePriority.find(8))
1919 issue = Issue.new(:priority => IssuePriority.find(8))
1920 classes = issue.css_classes.split(' ')
1920 classes = issue.css_classes.split(' ')
1921 assert_include 'priority-8', classes
1921 assert_include 'priority-8', classes
1922 assert_include 'priority-highest', classes
1922 assert_include 'priority-highest', classes
1923 end
1923 end
1924
1924
1925 def test_save_attachments_with_hash_should_save_attachments_in_keys_order
1925 def test_save_attachments_with_hash_should_save_attachments_in_keys_order
1926 set_tmp_attachments_directory
1926 set_tmp_attachments_directory
1927 issue = Issue.generate!
1927 issue = Issue.generate!
1928 issue.save_attachments({
1928 issue.save_attachments({
1929 'p0' => {'file' => mock_file_with_options(:original_filename => 'upload')},
1929 'p0' => {'file' => mock_file_with_options(:original_filename => 'upload')},
1930 '3' => {'file' => mock_file_with_options(:original_filename => 'bar')},
1930 '3' => {'file' => mock_file_with_options(:original_filename => 'bar')},
1931 '1' => {'file' => mock_file_with_options(:original_filename => 'foo')}
1931 '1' => {'file' => mock_file_with_options(:original_filename => 'foo')}
1932 })
1932 })
1933 issue.attach_saved_attachments
1933 issue.attach_saved_attachments
1934
1934
1935 assert_equal 3, issue.reload.attachments.count
1935 assert_equal 3, issue.reload.attachments.count
1936 assert_equal %w(upload foo bar), issue.attachments.map(&:filename)
1936 assert_equal %w(upload foo bar), issue.attachments.map(&:filename)
1937 end
1937 end
1938 end
1938 end
General Comments 0
You need to be logged in to leave comments. Login now