##// END OF EJS Templates
Replaces Enumeration.get_values and Enumeration.default with named scopes....
Jean-Philippe Lang -
r2411:4601ed2f3aeb
parent child
Show More
@@ -1,88 +1,88
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 before_filter :find_project, :only => [:index, :new]
19 before_filter :find_project, :only => [:index, :new]
20 before_filter :find_document, :except => [:index, :new]
20 before_filter :find_document, :except => [:index, :new]
21 before_filter :authorize
21 before_filter :authorize
22
22
23 helper :attachments
23 helper :attachments
24
24
25 def index
25 def index
26 @sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category'
26 @sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category'
27 documents = @project.documents.find :all, :include => [:attachments, :category]
27 documents = @project.documents.find :all, :include => [:attachments, :category]
28 case @sort_by
28 case @sort_by
29 when 'date'
29 when 'date'
30 @grouped = documents.group_by {|d| d.created_on.to_date }
30 @grouped = documents.group_by {|d| d.created_on.to_date }
31 when 'title'
31 when 'title'
32 @grouped = documents.group_by {|d| d.title.first.upcase}
32 @grouped = documents.group_by {|d| d.title.first.upcase}
33 when 'author'
33 when 'author'
34 @grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author}
34 @grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author}
35 else
35 else
36 @grouped = documents.group_by(&:category)
36 @grouped = documents.group_by(&:category)
37 end
37 end
38 @document = @project.documents.build
38 @document = @project.documents.build
39 render :layout => false if request.xhr?
39 render :layout => false if request.xhr?
40 end
40 end
41
41
42 def show
42 def show
43 @attachments = @document.attachments.find(:all, :order => "created_on DESC")
43 @attachments = @document.attachments.find(:all, :order => "created_on DESC")
44 end
44 end
45
45
46 def new
46 def new
47 @document = @project.documents.build(params[:document])
47 @document = @project.documents.build(params[:document])
48 if request.post? and @document.save
48 if request.post? and @document.save
49 attach_files(@document, params[:attachments])
49 attach_files(@document, params[:attachments])
50 flash[:notice] = l(:notice_successful_create)
50 flash[:notice] = l(:notice_successful_create)
51 Mailer.deliver_document_added(@document) if Setting.notified_events.include?('document_added')
51 Mailer.deliver_document_added(@document) if Setting.notified_events.include?('document_added')
52 redirect_to :action => 'index', :project_id => @project
52 redirect_to :action => 'index', :project_id => @project
53 end
53 end
54 end
54 end
55
55
56 def edit
56 def edit
57 @categories = Enumeration::get_values('DCAT')
57 @categories = Enumeration.document_categories
58 if request.post? and @document.update_attributes(params[:document])
58 if request.post? and @document.update_attributes(params[:document])
59 flash[:notice] = l(:notice_successful_update)
59 flash[:notice] = l(:notice_successful_update)
60 redirect_to :action => 'show', :id => @document
60 redirect_to :action => 'show', :id => @document
61 end
61 end
62 end
62 end
63
63
64 def destroy
64 def destroy
65 @document.destroy
65 @document.destroy
66 redirect_to :controller => 'documents', :action => 'index', :project_id => @project
66 redirect_to :controller => 'documents', :action => 'index', :project_id => @project
67 end
67 end
68
68
69 def add_attachment
69 def add_attachment
70 attachments = attach_files(@document, params[:attachments])
70 attachments = attach_files(@document, params[:attachments])
71 Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('document_added')
71 Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('document_added')
72 redirect_to :action => 'show', :id => @document
72 redirect_to :action => 'show', :id => @document
73 end
73 end
74
74
75 private
75 private
76 def find_project
76 def find_project
77 @project = Project.find(params[:project_id])
77 @project = Project.find(params[:project_id])
78 rescue ActiveRecord::RecordNotFound
78 rescue ActiveRecord::RecordNotFound
79 render_404
79 render_404
80 end
80 end
81
81
82 def find_document
82 def find_document
83 @document = Document.find(params[:id])
83 @document = Document.find(params[:id])
84 @project = @document.project
84 @project = @document.project
85 rescue ActiveRecord::RecordNotFound
85 rescue ActiveRecord::RecordNotFound
86 render_404
86 render_404
87 end
87 end
88 end
88 end
@@ -1,93 +1,93
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 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 EnumerationsController < ApplicationController
18 class EnumerationsController < ApplicationController
19 before_filter :require_admin
19 before_filter :require_admin
20
20
21 def index
21 def index
22 list
22 list
23 render :action => 'list'
23 render :action => 'list'
24 end
24 end
25
25
26 # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)
26 # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)
27 verify :method => :post, :only => [ :destroy, :create, :update ],
27 verify :method => :post, :only => [ :destroy, :create, :update ],
28 :redirect_to => { :action => :list }
28 :redirect_to => { :action => :list }
29
29
30 def list
30 def list
31 end
31 end
32
32
33 def new
33 def new
34 @enumeration = Enumeration.new(:opt => params[:opt])
34 @enumeration = Enumeration.new(:opt => params[:opt])
35 end
35 end
36
36
37 def create
37 def create
38 @enumeration = Enumeration.new(params[:enumeration])
38 @enumeration = Enumeration.new(params[:enumeration])
39 if @enumeration.save
39 if @enumeration.save
40 flash[:notice] = l(:notice_successful_create)
40 flash[:notice] = l(:notice_successful_create)
41 redirect_to :action => 'list', :opt => @enumeration.opt
41 redirect_to :action => 'list', :opt => @enumeration.opt
42 else
42 else
43 render :action => 'new'
43 render :action => 'new'
44 end
44 end
45 end
45 end
46
46
47 def edit
47 def edit
48 @enumeration = Enumeration.find(params[:id])
48 @enumeration = Enumeration.find(params[:id])
49 end
49 end
50
50
51 def update
51 def update
52 @enumeration = Enumeration.find(params[:id])
52 @enumeration = Enumeration.find(params[:id])
53 if @enumeration.update_attributes(params[:enumeration])
53 if @enumeration.update_attributes(params[:enumeration])
54 flash[:notice] = l(:notice_successful_update)
54 flash[:notice] = l(:notice_successful_update)
55 redirect_to :action => 'list', :opt => @enumeration.opt
55 redirect_to :action => 'list', :opt => @enumeration.opt
56 else
56 else
57 render :action => 'edit'
57 render :action => 'edit'
58 end
58 end
59 end
59 end
60
60
61 def move
61 def move
62 @enumeration = Enumeration.find(params[:id])
62 @enumeration = Enumeration.find(params[:id])
63 case params[:position]
63 case params[:position]
64 when 'highest'
64 when 'highest'
65 @enumeration.move_to_top
65 @enumeration.move_to_top
66 when 'higher'
66 when 'higher'
67 @enumeration.move_higher
67 @enumeration.move_higher
68 when 'lower'
68 when 'lower'
69 @enumeration.move_lower
69 @enumeration.move_lower
70 when 'lowest'
70 when 'lowest'
71 @enumeration.move_to_bottom
71 @enumeration.move_to_bottom
72 end if params[:position]
72 end if params[:position]
73 redirect_to :action => 'index'
73 redirect_to :action => 'index'
74 end
74 end
75
75
76 def destroy
76 def destroy
77 @enumeration = Enumeration.find(params[:id])
77 @enumeration = Enumeration.find(params[:id])
78 if !@enumeration.in_use?
78 if !@enumeration.in_use?
79 # No associated objects
79 # No associated objects
80 @enumeration.destroy
80 @enumeration.destroy
81 redirect_to :action => 'index'
81 redirect_to :action => 'index'
82 elsif params[:reassign_to_id]
82 elsif params[:reassign_to_id]
83 if reassign_to = Enumeration.find_by_opt_and_id(@enumeration.opt, params[:reassign_to_id])
83 if reassign_to = Enumeration.find_by_opt_and_id(@enumeration.opt, params[:reassign_to_id])
84 @enumeration.destroy(reassign_to)
84 @enumeration.destroy(reassign_to)
85 redirect_to :action => 'index'
85 redirect_to :action => 'index'
86 end
86 end
87 end
87 end
88 @enumerations = Enumeration.get_values(@enumeration.opt) - [@enumeration]
88 @enumerations = Enumeration.values(@enumeration.opt) - [@enumeration]
89 #rescue
89 #rescue
90 # flash[:error] = 'Unable to delete enumeration'
90 # flash[:error] = 'Unable to delete enumeration'
91 # redirect_to :action => 'index'
91 # redirect_to :action => 'index'
92 end
92 end
93 end
93 end
@@ -1,492 +1,492
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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 IssuesController < ApplicationController
18 class IssuesController < ApplicationController
19 menu_item :new_issue, :only => :new
19 menu_item :new_issue, :only => :new
20
20
21 before_filter :find_issue, :only => [:show, :edit, :reply]
21 before_filter :find_issue, :only => [:show, :edit, :reply]
22 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
22 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
23 before_filter :find_project, :only => [:new, :update_form, :preview]
23 before_filter :find_project, :only => [:new, :update_form, :preview]
24 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]
24 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]
25 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
25 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
26 accept_key_auth :index, :changes
26 accept_key_auth :index, :changes
27
27
28 helper :journals
28 helper :journals
29 helper :projects
29 helper :projects
30 include ProjectsHelper
30 include ProjectsHelper
31 helper :custom_fields
31 helper :custom_fields
32 include CustomFieldsHelper
32 include CustomFieldsHelper
33 helper :issue_relations
33 helper :issue_relations
34 include IssueRelationsHelper
34 include IssueRelationsHelper
35 helper :watchers
35 helper :watchers
36 include WatchersHelper
36 include WatchersHelper
37 helper :attachments
37 helper :attachments
38 include AttachmentsHelper
38 include AttachmentsHelper
39 helper :queries
39 helper :queries
40 helper :sort
40 helper :sort
41 include SortHelper
41 include SortHelper
42 include IssuesHelper
42 include IssuesHelper
43 helper :timelog
43 helper :timelog
44 include Redmine::Export::PDF
44 include Redmine::Export::PDF
45
45
46 def index
46 def index
47 retrieve_query
47 retrieve_query
48 sort_init 'id', 'desc'
48 sort_init 'id', 'desc'
49 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
49 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
50
50
51 if @query.valid?
51 if @query.valid?
52 limit = per_page_option
52 limit = per_page_option
53 respond_to do |format|
53 respond_to do |format|
54 format.html { }
54 format.html { }
55 format.atom { }
55 format.atom { }
56 format.csv { limit = Setting.issues_export_limit.to_i }
56 format.csv { limit = Setting.issues_export_limit.to_i }
57 format.pdf { limit = Setting.issues_export_limit.to_i }
57 format.pdf { limit = Setting.issues_export_limit.to_i }
58 end
58 end
59 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
59 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
60 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
60 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
61 @issues = Issue.find :all, :order => sort_clause,
61 @issues = Issue.find :all, :order => sort_clause,
62 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
62 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
63 :conditions => @query.statement,
63 :conditions => @query.statement,
64 :limit => limit,
64 :limit => limit,
65 :offset => @issue_pages.current.offset
65 :offset => @issue_pages.current.offset
66 respond_to do |format|
66 respond_to do |format|
67 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
67 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
68 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
68 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
69 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
69 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
70 format.pdf { send_data(issues_to_pdf(@issues, @project), :type => 'application/pdf', :filename => 'export.pdf') }
70 format.pdf { send_data(issues_to_pdf(@issues, @project), :type => 'application/pdf', :filename => 'export.pdf') }
71 end
71 end
72 else
72 else
73 # Send html if the query is not valid
73 # Send html if the query is not valid
74 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
74 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
75 end
75 end
76 rescue ActiveRecord::RecordNotFound
76 rescue ActiveRecord::RecordNotFound
77 render_404
77 render_404
78 end
78 end
79
79
80 def changes
80 def changes
81 retrieve_query
81 retrieve_query
82 sort_init 'id', 'desc'
82 sort_init 'id', 'desc'
83 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
83 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
84
84
85 if @query.valid?
85 if @query.valid?
86 @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
86 @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
87 :conditions => @query.statement,
87 :conditions => @query.statement,
88 :limit => 25,
88 :limit => 25,
89 :order => "#{Journal.table_name}.created_on DESC"
89 :order => "#{Journal.table_name}.created_on DESC"
90 end
90 end
91 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
91 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
92 render :layout => false, :content_type => 'application/atom+xml'
92 render :layout => false, :content_type => 'application/atom+xml'
93 rescue ActiveRecord::RecordNotFound
93 rescue ActiveRecord::RecordNotFound
94 render_404
94 render_404
95 end
95 end
96
96
97 def show
97 def show
98 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
98 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
99 @journals.each_with_index {|j,i| j.indice = i+1}
99 @journals.each_with_index {|j,i| j.indice = i+1}
100 @journals.reverse! if User.current.wants_comments_in_reverse_order?
100 @journals.reverse! if User.current.wants_comments_in_reverse_order?
101 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
101 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
102 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
102 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
103 @priorities = Enumeration::get_values('IPRI')
103 @priorities = Enumeration.priorities
104 @time_entry = TimeEntry.new
104 @time_entry = TimeEntry.new
105 respond_to do |format|
105 respond_to do |format|
106 format.html { render :template => 'issues/show.rhtml' }
106 format.html { render :template => 'issues/show.rhtml' }
107 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
107 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
108 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
108 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
109 end
109 end
110 end
110 end
111
111
112 # Add a new issue
112 # Add a new issue
113 # The new issue will be created from an existing one if copy_from parameter is given
113 # The new issue will be created from an existing one if copy_from parameter is given
114 def new
114 def new
115 @issue = Issue.new
115 @issue = Issue.new
116 @issue.copy_from(params[:copy_from]) if params[:copy_from]
116 @issue.copy_from(params[:copy_from]) if params[:copy_from]
117 @issue.project = @project
117 @issue.project = @project
118 # Tracker must be set before custom field values
118 # Tracker must be set before custom field values
119 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
119 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
120 if @issue.tracker.nil?
120 if @issue.tracker.nil?
121 flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.'
121 flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.'
122 render :nothing => true, :layout => true
122 render :nothing => true, :layout => true
123 return
123 return
124 end
124 end
125 if params[:issue].is_a?(Hash)
125 if params[:issue].is_a?(Hash)
126 @issue.attributes = params[:issue]
126 @issue.attributes = params[:issue]
127 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
127 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
128 end
128 end
129 @issue.author = User.current
129 @issue.author = User.current
130
130
131 default_status = IssueStatus.default
131 default_status = IssueStatus.default
132 unless default_status
132 unless default_status
133 flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
133 flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
134 render :nothing => true, :layout => true
134 render :nothing => true, :layout => true
135 return
135 return
136 end
136 end
137 @issue.status = default_status
137 @issue.status = default_status
138 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq
138 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq
139
139
140 if request.get? || request.xhr?
140 if request.get? || request.xhr?
141 @issue.start_date ||= Date.today
141 @issue.start_date ||= Date.today
142 else
142 else
143 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
143 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
144 # Check that the user is allowed to apply the requested status
144 # Check that the user is allowed to apply the requested status
145 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
145 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
146 if @issue.save
146 if @issue.save
147 attach_files(@issue, params[:attachments])
147 attach_files(@issue, params[:attachments])
148 flash[:notice] = l(:notice_successful_create)
148 flash[:notice] = l(:notice_successful_create)
149 Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
149 Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
150 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
150 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
151 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
151 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
152 { :action => 'show', :id => @issue })
152 { :action => 'show', :id => @issue })
153 return
153 return
154 end
154 end
155 end
155 end
156 @priorities = Enumeration::get_values('IPRI')
156 @priorities = Enumeration.priorities
157 render :layout => !request.xhr?
157 render :layout => !request.xhr?
158 end
158 end
159
159
160 # Attributes that can be updated on workflow transition (without :edit permission)
160 # Attributes that can be updated on workflow transition (without :edit permission)
161 # TODO: make it configurable (at least per role)
161 # TODO: make it configurable (at least per role)
162 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
162 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
163
163
164 def edit
164 def edit
165 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
165 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
166 @priorities = Enumeration::get_values('IPRI')
166 @priorities = Enumeration.priorities
167 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
167 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
168 @time_entry = TimeEntry.new
168 @time_entry = TimeEntry.new
169
169
170 @notes = params[:notes]
170 @notes = params[:notes]
171 journal = @issue.init_journal(User.current, @notes)
171 journal = @issue.init_journal(User.current, @notes)
172 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
172 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
173 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
173 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
174 attrs = params[:issue].dup
174 attrs = params[:issue].dup
175 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
175 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
176 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
176 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
177 @issue.attributes = attrs
177 @issue.attributes = attrs
178 end
178 end
179
179
180 if request.post?
180 if request.post?
181 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
181 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
182 @time_entry.attributes = params[:time_entry]
182 @time_entry.attributes = params[:time_entry]
183 attachments = attach_files(@issue, params[:attachments])
183 attachments = attach_files(@issue, params[:attachments])
184 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
184 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
185
185
186 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
186 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
187
187
188 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
188 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
189 # Log spend time
189 # Log spend time
190 if User.current.allowed_to?(:log_time, @project)
190 if User.current.allowed_to?(:log_time, @project)
191 @time_entry.save
191 @time_entry.save
192 end
192 end
193 if !journal.new_record?
193 if !journal.new_record?
194 # Only send notification if something was actually changed
194 # Only send notification if something was actually changed
195 flash[:notice] = l(:notice_successful_update)
195 flash[:notice] = l(:notice_successful_update)
196 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
196 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
197 end
197 end
198 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
198 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
199 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
199 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
200 end
200 end
201 end
201 end
202 rescue ActiveRecord::StaleObjectError
202 rescue ActiveRecord::StaleObjectError
203 # Optimistic locking exception
203 # Optimistic locking exception
204 flash.now[:error] = l(:notice_locking_conflict)
204 flash.now[:error] = l(:notice_locking_conflict)
205 end
205 end
206
206
207 def reply
207 def reply
208 journal = Journal.find(params[:journal_id]) if params[:journal_id]
208 journal = Journal.find(params[:journal_id]) if params[:journal_id]
209 if journal
209 if journal
210 user = journal.user
210 user = journal.user
211 text = journal.notes
211 text = journal.notes
212 else
212 else
213 user = @issue.author
213 user = @issue.author
214 text = @issue.description
214 text = @issue.description
215 end
215 end
216 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
216 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
217 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
217 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
218 render(:update) { |page|
218 render(:update) { |page|
219 page.<< "$('notes').value = \"#{content}\";"
219 page.<< "$('notes').value = \"#{content}\";"
220 page.show 'update'
220 page.show 'update'
221 page << "Form.Element.focus('notes');"
221 page << "Form.Element.focus('notes');"
222 page << "Element.scrollTo('update');"
222 page << "Element.scrollTo('update');"
223 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
223 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
224 }
224 }
225 end
225 end
226
226
227 # Bulk edit a set of issues
227 # Bulk edit a set of issues
228 def bulk_edit
228 def bulk_edit
229 if request.post?
229 if request.post?
230 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
230 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
231 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
231 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
232 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
232 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
233 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
233 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
234 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
234 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
235 custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
235 custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
236
236
237 unsaved_issue_ids = []
237 unsaved_issue_ids = []
238 @issues.each do |issue|
238 @issues.each do |issue|
239 journal = issue.init_journal(User.current, params[:notes])
239 journal = issue.init_journal(User.current, params[:notes])
240 issue.priority = priority if priority
240 issue.priority = priority if priority
241 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
241 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
242 issue.category = category if category || params[:category_id] == 'none'
242 issue.category = category if category || params[:category_id] == 'none'
243 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
243 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
244 issue.start_date = params[:start_date] unless params[:start_date].blank?
244 issue.start_date = params[:start_date] unless params[:start_date].blank?
245 issue.due_date = params[:due_date] unless params[:due_date].blank?
245 issue.due_date = params[:due_date] unless params[:due_date].blank?
246 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
246 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
247 issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty?
247 issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty?
248 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
248 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
249 # Don't save any change to the issue if the user is not authorized to apply the requested status
249 # Don't save any change to the issue if the user is not authorized to apply the requested status
250 if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
250 if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
251 # Send notification for each issue (if changed)
251 # Send notification for each issue (if changed)
252 Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
252 Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
253 else
253 else
254 # Keep unsaved issue ids to display them in flash error
254 # Keep unsaved issue ids to display them in flash error
255 unsaved_issue_ids << issue.id
255 unsaved_issue_ids << issue.id
256 end
256 end
257 end
257 end
258 if unsaved_issue_ids.empty?
258 if unsaved_issue_ids.empty?
259 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
259 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
260 else
260 else
261 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
261 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
262 end
262 end
263 redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project})
263 redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project})
264 return
264 return
265 end
265 end
266 # Find potential statuses the user could be allowed to switch issues to
266 # Find potential statuses the user could be allowed to switch issues to
267 @available_statuses = Workflow.find(:all, :include => :new_status,
267 @available_statuses = Workflow.find(:all, :include => :new_status,
268 :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq.sort
268 :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq.sort
269 @custom_fields = @project.issue_custom_fields.select {|f| f.field_format == 'list'}
269 @custom_fields = @project.issue_custom_fields.select {|f| f.field_format == 'list'}
270 end
270 end
271
271
272 def move
272 def move
273 @allowed_projects = []
273 @allowed_projects = []
274 # find projects to which the user is allowed to move the issue
274 # find projects to which the user is allowed to move the issue
275 if User.current.admin?
275 if User.current.admin?
276 # admin is allowed to move issues to any active (visible) project
276 # admin is allowed to move issues to any active (visible) project
277 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
277 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
278 else
278 else
279 User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)}
279 User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)}
280 end
280 end
281 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
281 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
282 @target_project ||= @project
282 @target_project ||= @project
283 @trackers = @target_project.trackers
283 @trackers = @target_project.trackers
284 if request.post?
284 if request.post?
285 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
285 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
286 unsaved_issue_ids = []
286 unsaved_issue_ids = []
287 @issues.each do |issue|
287 @issues.each do |issue|
288 issue.init_journal(User.current)
288 issue.init_journal(User.current)
289 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker, params[:copy_options])
289 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker, params[:copy_options])
290 end
290 end
291 if unsaved_issue_ids.empty?
291 if unsaved_issue_ids.empty?
292 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
292 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
293 else
293 else
294 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
294 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
295 end
295 end
296 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
296 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
297 return
297 return
298 end
298 end
299 render :layout => false if request.xhr?
299 render :layout => false if request.xhr?
300 end
300 end
301
301
302 def destroy
302 def destroy
303 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
303 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
304 if @hours > 0
304 if @hours > 0
305 case params[:todo]
305 case params[:todo]
306 when 'destroy'
306 when 'destroy'
307 # nothing to do
307 # nothing to do
308 when 'nullify'
308 when 'nullify'
309 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
309 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
310 when 'reassign'
310 when 'reassign'
311 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
311 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
312 if reassign_to.nil?
312 if reassign_to.nil?
313 flash.now[:error] = l(:error_issue_not_found_in_project)
313 flash.now[:error] = l(:error_issue_not_found_in_project)
314 return
314 return
315 else
315 else
316 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
316 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
317 end
317 end
318 else
318 else
319 # display the destroy form
319 # display the destroy form
320 return
320 return
321 end
321 end
322 end
322 end
323 @issues.each(&:destroy)
323 @issues.each(&:destroy)
324 redirect_to :action => 'index', :project_id => @project
324 redirect_to :action => 'index', :project_id => @project
325 end
325 end
326
326
327 def gantt
327 def gantt
328 @gantt = Redmine::Helpers::Gantt.new(params)
328 @gantt = Redmine::Helpers::Gantt.new(params)
329 retrieve_query
329 retrieve_query
330 if @query.valid?
330 if @query.valid?
331 events = []
331 events = []
332 # Issues that have start and due dates
332 # Issues that have start and due dates
333 events += Issue.find(:all,
333 events += Issue.find(:all,
334 :order => "start_date, due_date",
334 :order => "start_date, due_date",
335 :include => [:tracker, :status, :assigned_to, :priority, :project],
335 :include => [:tracker, :status, :assigned_to, :priority, :project],
336 :conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
336 :conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
337 )
337 )
338 # Issues that don't have a due date but that are assigned to a version with a date
338 # Issues that don't have a due date but that are assigned to a version with a date
339 events += Issue.find(:all,
339 events += Issue.find(:all,
340 :order => "start_date, effective_date",
340 :order => "start_date, effective_date",
341 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
341 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
342 :conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
342 :conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
343 )
343 )
344 # Versions
344 # Versions
345 events += Version.find(:all, :include => :project,
345 events += Version.find(:all, :include => :project,
346 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
346 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
347
347
348 @gantt.events = events
348 @gantt.events = events
349 end
349 end
350
350
351 respond_to do |format|
351 respond_to do |format|
352 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
352 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
353 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.png") } if @gantt.respond_to?('to_image')
353 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.png") } if @gantt.respond_to?('to_image')
354 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.pdf") }
354 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.pdf") }
355 end
355 end
356 end
356 end
357
357
358 def calendar
358 def calendar
359 if params[:year] and params[:year].to_i > 1900
359 if params[:year] and params[:year].to_i > 1900
360 @year = params[:year].to_i
360 @year = params[:year].to_i
361 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
361 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
362 @month = params[:month].to_i
362 @month = params[:month].to_i
363 end
363 end
364 end
364 end
365 @year ||= Date.today.year
365 @year ||= Date.today.year
366 @month ||= Date.today.month
366 @month ||= Date.today.month
367
367
368 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
368 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
369 retrieve_query
369 retrieve_query
370 if @query.valid?
370 if @query.valid?
371 events = []
371 events = []
372 events += Issue.find(:all,
372 events += Issue.find(:all,
373 :include => [:tracker, :status, :assigned_to, :priority, :project],
373 :include => [:tracker, :status, :assigned_to, :priority, :project],
374 :conditions => ["(#{@query.statement}) AND ((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
374 :conditions => ["(#{@query.statement}) AND ((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
375 )
375 )
376 events += Version.find(:all, :include => :project,
376 events += Version.find(:all, :include => :project,
377 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
377 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
378
378
379 @calendar.events = events
379 @calendar.events = events
380 end
380 end
381
381
382 render :layout => false if request.xhr?
382 render :layout => false if request.xhr?
383 end
383 end
384
384
385 def context_menu
385 def context_menu
386 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
386 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
387 if (@issues.size == 1)
387 if (@issues.size == 1)
388 @issue = @issues.first
388 @issue = @issues.first
389 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
389 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
390 end
390 end
391 projects = @issues.collect(&:project).compact.uniq
391 projects = @issues.collect(&:project).compact.uniq
392 @project = projects.first if projects.size == 1
392 @project = projects.first if projects.size == 1
393
393
394 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
394 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
395 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
395 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
396 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
396 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
397 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
397 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
398 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
398 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
399 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
399 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
400 }
400 }
401 if @project
401 if @project
402 @assignables = @project.assignable_users
402 @assignables = @project.assignable_users
403 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
403 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
404 end
404 end
405
405
406 @priorities = Enumeration.get_values('IPRI').reverse
406 @priorities = Enumeration.priorities.reverse
407 @statuses = IssueStatus.find(:all, :order => 'position')
407 @statuses = IssueStatus.find(:all, :order => 'position')
408 @back = request.env['HTTP_REFERER']
408 @back = request.env['HTTP_REFERER']
409
409
410 render :layout => false
410 render :layout => false
411 end
411 end
412
412
413 def update_form
413 def update_form
414 @issue = Issue.new(params[:issue])
414 @issue = Issue.new(params[:issue])
415 render :action => :new, :layout => false
415 render :action => :new, :layout => false
416 end
416 end
417
417
418 def preview
418 def preview
419 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
419 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
420 @attachements = @issue.attachments if @issue
420 @attachements = @issue.attachments if @issue
421 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
421 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
422 render :partial => 'common/preview'
422 render :partial => 'common/preview'
423 end
423 end
424
424
425 private
425 private
426 def find_issue
426 def find_issue
427 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
427 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
428 @project = @issue.project
428 @project = @issue.project
429 rescue ActiveRecord::RecordNotFound
429 rescue ActiveRecord::RecordNotFound
430 render_404
430 render_404
431 end
431 end
432
432
433 # Filter for bulk operations
433 # Filter for bulk operations
434 def find_issues
434 def find_issues
435 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
435 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
436 raise ActiveRecord::RecordNotFound if @issues.empty?
436 raise ActiveRecord::RecordNotFound if @issues.empty?
437 projects = @issues.collect(&:project).compact.uniq
437 projects = @issues.collect(&:project).compact.uniq
438 if projects.size == 1
438 if projects.size == 1
439 @project = projects.first
439 @project = projects.first
440 else
440 else
441 # TODO: let users bulk edit/move/destroy issues from different projects
441 # TODO: let users bulk edit/move/destroy issues from different projects
442 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
442 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
443 end
443 end
444 rescue ActiveRecord::RecordNotFound
444 rescue ActiveRecord::RecordNotFound
445 render_404
445 render_404
446 end
446 end
447
447
448 def find_project
448 def find_project
449 @project = Project.find(params[:project_id])
449 @project = Project.find(params[:project_id])
450 rescue ActiveRecord::RecordNotFound
450 rescue ActiveRecord::RecordNotFound
451 render_404
451 render_404
452 end
452 end
453
453
454 def find_optional_project
454 def find_optional_project
455 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
455 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
456 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
456 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
457 allowed ? true : deny_access
457 allowed ? true : deny_access
458 rescue ActiveRecord::RecordNotFound
458 rescue ActiveRecord::RecordNotFound
459 render_404
459 render_404
460 end
460 end
461
461
462 # Retrieve query from session or build a new query
462 # Retrieve query from session or build a new query
463 def retrieve_query
463 def retrieve_query
464 if !params[:query_id].blank?
464 if !params[:query_id].blank?
465 cond = "project_id IS NULL"
465 cond = "project_id IS NULL"
466 cond << " OR project_id = #{@project.id}" if @project
466 cond << " OR project_id = #{@project.id}" if @project
467 @query = Query.find(params[:query_id], :conditions => cond)
467 @query = Query.find(params[:query_id], :conditions => cond)
468 @query.project = @project
468 @query.project = @project
469 session[:query] = {:id => @query.id, :project_id => @query.project_id}
469 session[:query] = {:id => @query.id, :project_id => @query.project_id}
470 else
470 else
471 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
471 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
472 # Give it a name, required to be valid
472 # Give it a name, required to be valid
473 @query = Query.new(:name => "_")
473 @query = Query.new(:name => "_")
474 @query.project = @project
474 @query.project = @project
475 if params[:fields] and params[:fields].is_a? Array
475 if params[:fields] and params[:fields].is_a? Array
476 params[:fields].each do |field|
476 params[:fields].each do |field|
477 @query.add_filter(field, params[:operators][field], params[:values][field])
477 @query.add_filter(field, params[:operators][field], params[:values][field])
478 end
478 end
479 else
479 else
480 @query.available_filters.keys.each do |field|
480 @query.available_filters.keys.each do |field|
481 @query.add_short_filter(field, params[field]) if params[field]
481 @query.add_short_filter(field, params[field]) if params[field]
482 end
482 end
483 end
483 end
484 session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
484 session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
485 else
485 else
486 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
486 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
487 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
487 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
488 @query.project = @project
488 @query.project = @project
489 end
489 end
490 end
490 end
491 end
491 end
492 end
492 end
@@ -1,236 +1,236
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 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 ReportsController < ApplicationController
18 class ReportsController < ApplicationController
19 menu_item :issues
19 menu_item :issues
20 before_filter :find_project, :authorize
20 before_filter :find_project, :authorize
21
21
22 def issue_report
22 def issue_report
23 @statuses = IssueStatus.find(:all, :order => 'position')
23 @statuses = IssueStatus.find(:all, :order => 'position')
24
24
25 case params[:detail]
25 case params[:detail]
26 when "tracker"
26 when "tracker"
27 @field = "tracker_id"
27 @field = "tracker_id"
28 @rows = @project.trackers
28 @rows = @project.trackers
29 @data = issues_by_tracker
29 @data = issues_by_tracker
30 @report_title = l(:field_tracker)
30 @report_title = l(:field_tracker)
31 render :template => "reports/issue_report_details"
31 render :template => "reports/issue_report_details"
32 when "version"
32 when "version"
33 @field = "fixed_version_id"
33 @field = "fixed_version_id"
34 @rows = @project.versions.sort
34 @rows = @project.versions.sort
35 @data = issues_by_version
35 @data = issues_by_version
36 @report_title = l(:field_version)
36 @report_title = l(:field_version)
37 render :template => "reports/issue_report_details"
37 render :template => "reports/issue_report_details"
38 when "priority"
38 when "priority"
39 @field = "priority_id"
39 @field = "priority_id"
40 @rows = Enumeration::get_values('IPRI')
40 @rows = Enumeration.priorities
41 @data = issues_by_priority
41 @data = issues_by_priority
42 @report_title = l(:field_priority)
42 @report_title = l(:field_priority)
43 render :template => "reports/issue_report_details"
43 render :template => "reports/issue_report_details"
44 when "category"
44 when "category"
45 @field = "category_id"
45 @field = "category_id"
46 @rows = @project.issue_categories
46 @rows = @project.issue_categories
47 @data = issues_by_category
47 @data = issues_by_category
48 @report_title = l(:field_category)
48 @report_title = l(:field_category)
49 render :template => "reports/issue_report_details"
49 render :template => "reports/issue_report_details"
50 when "assigned_to"
50 when "assigned_to"
51 @field = "assigned_to_id"
51 @field = "assigned_to_id"
52 @rows = @project.members.collect { |m| m.user }
52 @rows = @project.members.collect { |m| m.user }
53 @data = issues_by_assigned_to
53 @data = issues_by_assigned_to
54 @report_title = l(:field_assigned_to)
54 @report_title = l(:field_assigned_to)
55 render :template => "reports/issue_report_details"
55 render :template => "reports/issue_report_details"
56 when "author"
56 when "author"
57 @field = "author_id"
57 @field = "author_id"
58 @rows = @project.members.collect { |m| m.user }
58 @rows = @project.members.collect { |m| m.user }
59 @data = issues_by_author
59 @data = issues_by_author
60 @report_title = l(:field_author)
60 @report_title = l(:field_author)
61 render :template => "reports/issue_report_details"
61 render :template => "reports/issue_report_details"
62 when "subproject"
62 when "subproject"
63 @field = "project_id"
63 @field = "project_id"
64 @rows = @project.descendants.active
64 @rows = @project.descendants.active
65 @data = issues_by_subproject
65 @data = issues_by_subproject
66 @report_title = l(:field_subproject)
66 @report_title = l(:field_subproject)
67 render :template => "reports/issue_report_details"
67 render :template => "reports/issue_report_details"
68 else
68 else
69 @trackers = @project.trackers
69 @trackers = @project.trackers
70 @versions = @project.versions.sort
70 @versions = @project.versions.sort
71 @priorities = Enumeration::get_values('IPRI')
71 @priorities = Enumeration.priorities
72 @categories = @project.issue_categories
72 @categories = @project.issue_categories
73 @assignees = @project.members.collect { |m| m.user }
73 @assignees = @project.members.collect { |m| m.user }
74 @authors = @project.members.collect { |m| m.user }
74 @authors = @project.members.collect { |m| m.user }
75 @subprojects = @project.descendants.active
75 @subprojects = @project.descendants.active
76 issues_by_tracker
76 issues_by_tracker
77 issues_by_version
77 issues_by_version
78 issues_by_priority
78 issues_by_priority
79 issues_by_category
79 issues_by_category
80 issues_by_assigned_to
80 issues_by_assigned_to
81 issues_by_author
81 issues_by_author
82 issues_by_subproject
82 issues_by_subproject
83
83
84 render :template => "reports/issue_report"
84 render :template => "reports/issue_report"
85 end
85 end
86 end
86 end
87
87
88 def delays
88 def delays
89 @trackers = Tracker.find(:all)
89 @trackers = Tracker.find(:all)
90 if request.get?
90 if request.get?
91 @selected_tracker_ids = @trackers.collect {|t| t.id.to_s }
91 @selected_tracker_ids = @trackers.collect {|t| t.id.to_s }
92 else
92 else
93 @selected_tracker_ids = params[:tracker_ids].collect { |id| id.to_i.to_s } if params[:tracker_ids] and params[:tracker_ids].is_a? Array
93 @selected_tracker_ids = params[:tracker_ids].collect { |id| id.to_i.to_s } if params[:tracker_ids] and params[:tracker_ids].is_a? Array
94 end
94 end
95 @selected_tracker_ids ||= []
95 @selected_tracker_ids ||= []
96 @raw =
96 @raw =
97 ActiveRecord::Base.connection.select_all("SELECT datediff( a.created_on, b.created_on ) as delay, count(a.id) as total
97 ActiveRecord::Base.connection.select_all("SELECT datediff( a.created_on, b.created_on ) as delay, count(a.id) as total
98 FROM issue_histories a, issue_histories b, issues i
98 FROM issue_histories a, issue_histories b, issues i
99 WHERE a.status_id =5
99 WHERE a.status_id =5
100 AND a.issue_id = b.issue_id
100 AND a.issue_id = b.issue_id
101 AND a.issue_id = i.id
101 AND a.issue_id = i.id
102 AND i.tracker_id in (#{@selected_tracker_ids.join(',')})
102 AND i.tracker_id in (#{@selected_tracker_ids.join(',')})
103 AND b.id = (
103 AND b.id = (
104 SELECT min( c.id )
104 SELECT min( c.id )
105 FROM issue_histories c
105 FROM issue_histories c
106 WHERE b.issue_id = c.issue_id )
106 WHERE b.issue_id = c.issue_id )
107 GROUP BY delay") unless @selected_tracker_ids.empty?
107 GROUP BY delay") unless @selected_tracker_ids.empty?
108 @raw ||=[]
108 @raw ||=[]
109
109
110 @x_from = 0
110 @x_from = 0
111 @x_to = 0
111 @x_to = 0
112 @y_from = 0
112 @y_from = 0
113 @y_to = 0
113 @y_to = 0
114 @sum_total = 0
114 @sum_total = 0
115 @sum_delay = 0
115 @sum_delay = 0
116 @raw.each do |r|
116 @raw.each do |r|
117 @x_to = [r['delay'].to_i, @x_to].max
117 @x_to = [r['delay'].to_i, @x_to].max
118 @y_to = [r['total'].to_i, @y_to].max
118 @y_to = [r['total'].to_i, @y_to].max
119 @sum_total = @sum_total + r['total'].to_i
119 @sum_total = @sum_total + r['total'].to_i
120 @sum_delay = @sum_delay + r['total'].to_i * r['delay'].to_i
120 @sum_delay = @sum_delay + r['total'].to_i * r['delay'].to_i
121 end
121 end
122 end
122 end
123
123
124 private
124 private
125 # Find project of id params[:id]
125 # Find project of id params[:id]
126 def find_project
126 def find_project
127 @project = Project.find(params[:id])
127 @project = Project.find(params[:id])
128 rescue ActiveRecord::RecordNotFound
128 rescue ActiveRecord::RecordNotFound
129 render_404
129 render_404
130 end
130 end
131
131
132 def issues_by_tracker
132 def issues_by_tracker
133 @issues_by_tracker ||=
133 @issues_by_tracker ||=
134 ActiveRecord::Base.connection.select_all("select s.id as status_id,
134 ActiveRecord::Base.connection.select_all("select s.id as status_id,
135 s.is_closed as closed,
135 s.is_closed as closed,
136 t.id as tracker_id,
136 t.id as tracker_id,
137 count(i.id) as total
137 count(i.id) as total
138 from
138 from
139 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Tracker.table_name} t
139 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Tracker.table_name} t
140 where
140 where
141 i.status_id=s.id
141 i.status_id=s.id
142 and i.tracker_id=t.id
142 and i.tracker_id=t.id
143 and i.project_id=#{@project.id}
143 and i.project_id=#{@project.id}
144 group by s.id, s.is_closed, t.id")
144 group by s.id, s.is_closed, t.id")
145 end
145 end
146
146
147 def issues_by_version
147 def issues_by_version
148 @issues_by_version ||=
148 @issues_by_version ||=
149 ActiveRecord::Base.connection.select_all("select s.id as status_id,
149 ActiveRecord::Base.connection.select_all("select s.id as status_id,
150 s.is_closed as closed,
150 s.is_closed as closed,
151 v.id as fixed_version_id,
151 v.id as fixed_version_id,
152 count(i.id) as total
152 count(i.id) as total
153 from
153 from
154 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Version.table_name} v
154 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Version.table_name} v
155 where
155 where
156 i.status_id=s.id
156 i.status_id=s.id
157 and i.fixed_version_id=v.id
157 and i.fixed_version_id=v.id
158 and i.project_id=#{@project.id}
158 and i.project_id=#{@project.id}
159 group by s.id, s.is_closed, v.id")
159 group by s.id, s.is_closed, v.id")
160 end
160 end
161
161
162 def issues_by_priority
162 def issues_by_priority
163 @issues_by_priority ||=
163 @issues_by_priority ||=
164 ActiveRecord::Base.connection.select_all("select s.id as status_id,
164 ActiveRecord::Base.connection.select_all("select s.id as status_id,
165 s.is_closed as closed,
165 s.is_closed as closed,
166 p.id as priority_id,
166 p.id as priority_id,
167 count(i.id) as total
167 count(i.id) as total
168 from
168 from
169 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Enumeration.table_name} p
169 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Enumeration.table_name} p
170 where
170 where
171 i.status_id=s.id
171 i.status_id=s.id
172 and i.priority_id=p.id
172 and i.priority_id=p.id
173 and i.project_id=#{@project.id}
173 and i.project_id=#{@project.id}
174 group by s.id, s.is_closed, p.id")
174 group by s.id, s.is_closed, p.id")
175 end
175 end
176
176
177 def issues_by_category
177 def issues_by_category
178 @issues_by_category ||=
178 @issues_by_category ||=
179 ActiveRecord::Base.connection.select_all("select s.id as status_id,
179 ActiveRecord::Base.connection.select_all("select s.id as status_id,
180 s.is_closed as closed,
180 s.is_closed as closed,
181 c.id as category_id,
181 c.id as category_id,
182 count(i.id) as total
182 count(i.id) as total
183 from
183 from
184 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{IssueCategory.table_name} c
184 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{IssueCategory.table_name} c
185 where
185 where
186 i.status_id=s.id
186 i.status_id=s.id
187 and i.category_id=c.id
187 and i.category_id=c.id
188 and i.project_id=#{@project.id}
188 and i.project_id=#{@project.id}
189 group by s.id, s.is_closed, c.id")
189 group by s.id, s.is_closed, c.id")
190 end
190 end
191
191
192 def issues_by_assigned_to
192 def issues_by_assigned_to
193 @issues_by_assigned_to ||=
193 @issues_by_assigned_to ||=
194 ActiveRecord::Base.connection.select_all("select s.id as status_id,
194 ActiveRecord::Base.connection.select_all("select s.id as status_id,
195 s.is_closed as closed,
195 s.is_closed as closed,
196 a.id as assigned_to_id,
196 a.id as assigned_to_id,
197 count(i.id) as total
197 count(i.id) as total
198 from
198 from
199 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a
199 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a
200 where
200 where
201 i.status_id=s.id
201 i.status_id=s.id
202 and i.assigned_to_id=a.id
202 and i.assigned_to_id=a.id
203 and i.project_id=#{@project.id}
203 and i.project_id=#{@project.id}
204 group by s.id, s.is_closed, a.id")
204 group by s.id, s.is_closed, a.id")
205 end
205 end
206
206
207 def issues_by_author
207 def issues_by_author
208 @issues_by_author ||=
208 @issues_by_author ||=
209 ActiveRecord::Base.connection.select_all("select s.id as status_id,
209 ActiveRecord::Base.connection.select_all("select s.id as status_id,
210 s.is_closed as closed,
210 s.is_closed as closed,
211 a.id as author_id,
211 a.id as author_id,
212 count(i.id) as total
212 count(i.id) as total
213 from
213 from
214 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a
214 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a
215 where
215 where
216 i.status_id=s.id
216 i.status_id=s.id
217 and i.author_id=a.id
217 and i.author_id=a.id
218 and i.project_id=#{@project.id}
218 and i.project_id=#{@project.id}
219 group by s.id, s.is_closed, a.id")
219 group by s.id, s.is_closed, a.id")
220 end
220 end
221
221
222 def issues_by_subproject
222 def issues_by_subproject
223 @issues_by_subproject ||=
223 @issues_by_subproject ||=
224 ActiveRecord::Base.connection.select_all("select s.id as status_id,
224 ActiveRecord::Base.connection.select_all("select s.id as status_id,
225 s.is_closed as closed,
225 s.is_closed as closed,
226 i.project_id as project_id,
226 i.project_id as project_id,
227 count(i.id) as total
227 count(i.id) as total
228 from
228 from
229 #{Issue.table_name} i, #{IssueStatus.table_name} s
229 #{Issue.table_name} i, #{IssueStatus.table_name} s
230 where
230 where
231 i.status_id=s.id
231 i.status_id=s.id
232 and i.project_id IN (#{@project.descendants.active.collect{|p| p.id}.join(',')})
232 and i.project_id IN (#{@project.descendants.active.collect{|p| p.id}.join(',')})
233 group by s.id, s.is_closed, i.project_id") if @project.descendants.active.any?
233 group by s.id, s.is_closed, i.project_id") if @project.descendants.active.any?
234 @issues_by_subproject ||= []
234 @issues_by_subproject ||= []
235 end
235 end
236 end
236 end
@@ -1,160 +1,160
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 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 module TimelogHelper
18 module TimelogHelper
19 include ApplicationHelper
19 include ApplicationHelper
20
20
21 def render_timelog_breadcrumb
21 def render_timelog_breadcrumb
22 links = []
22 links = []
23 links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil})
23 links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil})
24 links << link_to(h(@project), {:project_id => @project, :issue_id => nil}) if @project
24 links << link_to(h(@project), {:project_id => @project, :issue_id => nil}) if @project
25 links << link_to_issue(@issue) if @issue
25 links << link_to_issue(@issue) if @issue
26 breadcrumb links
26 breadcrumb links
27 end
27 end
28
28
29 def activity_collection_for_select_options
29 def activity_collection_for_select_options
30 activities = Enumeration::get_values('ACTI')
30 activities = Enumeration.activities
31 collection = []
31 collection = []
32 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless activities.detect(&:is_default)
32 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless activities.detect(&:is_default)
33 activities.each { |a| collection << [a.name, a.id] }
33 activities.each { |a| collection << [a.name, a.id] }
34 collection
34 collection
35 end
35 end
36
36
37 def select_hours(data, criteria, value)
37 def select_hours(data, criteria, value)
38 data.select {|row| row[criteria] == value}
38 data.select {|row| row[criteria] == value}
39 end
39 end
40
40
41 def sum_hours(data)
41 def sum_hours(data)
42 sum = 0
42 sum = 0
43 data.each do |row|
43 data.each do |row|
44 sum += row['hours'].to_f
44 sum += row['hours'].to_f
45 end
45 end
46 sum
46 sum
47 end
47 end
48
48
49 def options_for_period_select(value)
49 def options_for_period_select(value)
50 options_for_select([[l(:label_all_time), 'all'],
50 options_for_select([[l(:label_all_time), 'all'],
51 [l(:label_today), 'today'],
51 [l(:label_today), 'today'],
52 [l(:label_yesterday), 'yesterday'],
52 [l(:label_yesterday), 'yesterday'],
53 [l(:label_this_week), 'current_week'],
53 [l(:label_this_week), 'current_week'],
54 [l(:label_last_week), 'last_week'],
54 [l(:label_last_week), 'last_week'],
55 [l(:label_last_n_days, 7), '7_days'],
55 [l(:label_last_n_days, 7), '7_days'],
56 [l(:label_this_month), 'current_month'],
56 [l(:label_this_month), 'current_month'],
57 [l(:label_last_month), 'last_month'],
57 [l(:label_last_month), 'last_month'],
58 [l(:label_last_n_days, 30), '30_days'],
58 [l(:label_last_n_days, 30), '30_days'],
59 [l(:label_this_year), 'current_year']],
59 [l(:label_this_year), 'current_year']],
60 value)
60 value)
61 end
61 end
62
62
63 def entries_to_csv(entries)
63 def entries_to_csv(entries)
64 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
64 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
65 decimal_separator = l(:general_csv_decimal_separator)
65 decimal_separator = l(:general_csv_decimal_separator)
66 custom_fields = TimeEntryCustomField.find(:all)
66 custom_fields = TimeEntryCustomField.find(:all)
67 export = StringIO.new
67 export = StringIO.new
68 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
68 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
69 # csv header fields
69 # csv header fields
70 headers = [l(:field_spent_on),
70 headers = [l(:field_spent_on),
71 l(:field_user),
71 l(:field_user),
72 l(:field_activity),
72 l(:field_activity),
73 l(:field_project),
73 l(:field_project),
74 l(:field_issue),
74 l(:field_issue),
75 l(:field_tracker),
75 l(:field_tracker),
76 l(:field_subject),
76 l(:field_subject),
77 l(:field_hours),
77 l(:field_hours),
78 l(:field_comments)
78 l(:field_comments)
79 ]
79 ]
80 # Export custom fields
80 # Export custom fields
81 headers += custom_fields.collect(&:name)
81 headers += custom_fields.collect(&:name)
82
82
83 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
83 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
84 # csv lines
84 # csv lines
85 entries.each do |entry|
85 entries.each do |entry|
86 fields = [format_date(entry.spent_on),
86 fields = [format_date(entry.spent_on),
87 entry.user,
87 entry.user,
88 entry.activity,
88 entry.activity,
89 entry.project,
89 entry.project,
90 (entry.issue ? entry.issue.id : nil),
90 (entry.issue ? entry.issue.id : nil),
91 (entry.issue ? entry.issue.tracker : nil),
91 (entry.issue ? entry.issue.tracker : nil),
92 (entry.issue ? entry.issue.subject : nil),
92 (entry.issue ? entry.issue.subject : nil),
93 entry.hours.to_s.gsub('.', decimal_separator),
93 entry.hours.to_s.gsub('.', decimal_separator),
94 entry.comments
94 entry.comments
95 ]
95 ]
96 fields += custom_fields.collect {|f| show_value(entry.custom_value_for(f)) }
96 fields += custom_fields.collect {|f| show_value(entry.custom_value_for(f)) }
97
97
98 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
98 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
99 end
99 end
100 end
100 end
101 export.rewind
101 export.rewind
102 export
102 export
103 end
103 end
104
104
105 def format_criteria_value(criteria, value)
105 def format_criteria_value(criteria, value)
106 value.blank? ? l(:label_none) : ((k = @available_criterias[criteria][:klass]) ? k.find_by_id(value.to_i) : format_value(value, @available_criterias[criteria][:format]))
106 value.blank? ? l(:label_none) : ((k = @available_criterias[criteria][:klass]) ? k.find_by_id(value.to_i) : format_value(value, @available_criterias[criteria][:format]))
107 end
107 end
108
108
109 def report_to_csv(criterias, periods, hours)
109 def report_to_csv(criterias, periods, hours)
110 export = StringIO.new
110 export = StringIO.new
111 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
111 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
112 # Column headers
112 # Column headers
113 headers = criterias.collect {|criteria| l(@available_criterias[criteria][:label]) }
113 headers = criterias.collect {|criteria| l(@available_criterias[criteria][:label]) }
114 headers += periods
114 headers += periods
115 headers << l(:label_total)
115 headers << l(:label_total)
116 csv << headers.collect {|c| to_utf8(c) }
116 csv << headers.collect {|c| to_utf8(c) }
117 # Content
117 # Content
118 report_criteria_to_csv(csv, criterias, periods, hours)
118 report_criteria_to_csv(csv, criterias, periods, hours)
119 # Total row
119 # Total row
120 row = [ l(:label_total) ] + [''] * (criterias.size - 1)
120 row = [ l(:label_total) ] + [''] * (criterias.size - 1)
121 total = 0
121 total = 0
122 periods.each do |period|
122 periods.each do |period|
123 sum = sum_hours(select_hours(hours, @columns, period.to_s))
123 sum = sum_hours(select_hours(hours, @columns, period.to_s))
124 total += sum
124 total += sum
125 row << (sum > 0 ? "%.2f" % sum : '')
125 row << (sum > 0 ? "%.2f" % sum : '')
126 end
126 end
127 row << "%.2f" %total
127 row << "%.2f" %total
128 csv << row
128 csv << row
129 end
129 end
130 export.rewind
130 export.rewind
131 export
131 export
132 end
132 end
133
133
134 def report_criteria_to_csv(csv, criterias, periods, hours, level=0)
134 def report_criteria_to_csv(csv, criterias, periods, hours, level=0)
135 hours.collect {|h| h[criterias[level]].to_s}.uniq.each do |value|
135 hours.collect {|h| h[criterias[level]].to_s}.uniq.each do |value|
136 hours_for_value = select_hours(hours, criterias[level], value)
136 hours_for_value = select_hours(hours, criterias[level], value)
137 next if hours_for_value.empty?
137 next if hours_for_value.empty?
138 row = [''] * level
138 row = [''] * level
139 row << to_utf8(format_criteria_value(criterias[level], value))
139 row << to_utf8(format_criteria_value(criterias[level], value))
140 row += [''] * (criterias.length - level - 1)
140 row += [''] * (criterias.length - level - 1)
141 total = 0
141 total = 0
142 periods.each do |period|
142 periods.each do |period|
143 sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s))
143 sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s))
144 total += sum
144 total += sum
145 row << (sum > 0 ? "%.2f" % sum : '')
145 row << (sum > 0 ? "%.2f" % sum : '')
146 end
146 end
147 row << "%.2f" %total
147 row << "%.2f" %total
148 csv << row
148 csv << row
149
149
150 if criterias.length > level + 1
150 if criterias.length > level + 1
151 report_criteria_to_csv(csv, criterias, periods, hours_for_value, level + 1)
151 report_criteria_to_csv(csv, criterias, periods, hours_for_value, level + 1)
152 end
152 end
153 end
153 end
154 end
154 end
155
155
156 def to_utf8(s)
156 def to_utf8(s)
157 @ic ||= Iconv.new(l(:general_csv_encoding), 'UTF-8')
157 @ic ||= Iconv.new(l(:general_csv_encoding), 'UTF-8')
158 begin; @ic.iconv(s.to_s); rescue; s.to_s; end
158 begin; @ic.iconv(s.to_s); rescue; s.to_s; end
159 end
159 end
160 end
160 end
@@ -1,37 +1,37
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 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 Document < ActiveRecord::Base
18 class Document < ActiveRecord::Base
19 belongs_to :project
19 belongs_to :project
20 belongs_to :category, :class_name => "Enumeration", :foreign_key => "category_id"
20 belongs_to :category, :class_name => "Enumeration", :foreign_key => "category_id"
21 acts_as_attachable :delete_permission => :manage_documents
21 acts_as_attachable :delete_permission => :manage_documents
22
22
23 acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
23 acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
24 acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
24 acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
25 :author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil },
25 :author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil },
26 :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
26 :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
27 acts_as_activity_provider :find_options => {:include => :project}
27 acts_as_activity_provider :find_options => {:include => :project}
28
28
29 validates_presence_of :project, :title, :category
29 validates_presence_of :project, :title, :category
30 validates_length_of :title, :maximum => 60
30 validates_length_of :title, :maximum => 60
31
31
32 def after_initialize
32 def after_initialize
33 if new_record?
33 if new_record?
34 self.category ||= Enumeration.default('DCAT')
34 self.category ||= Enumeration.document_categories.default
35 end
35 end
36 end
36 end
37 end
37 end
@@ -1,81 +1,93
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 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 Enumeration < ActiveRecord::Base
18 class Enumeration < ActiveRecord::Base
19 acts_as_list :scope => 'opt = \'#{opt}\''
19 acts_as_list :scope => 'opt = \'#{opt}\''
20
20
21 before_destroy :check_integrity
21 before_destroy :check_integrity
22
22
23 validates_presence_of :opt, :name
23 validates_presence_of :opt, :name
24 validates_uniqueness_of :name, :scope => [:opt]
24 validates_uniqueness_of :name, :scope => [:opt]
25 validates_length_of :name, :maximum => 30
25 validates_length_of :name, :maximum => 30
26
26
27 # Single table inheritance would be an option
27 # Single table inheritance would be an option
28 OPTIONS = {
28 OPTIONS = {
29 "IPRI" => {:label => :enumeration_issue_priorities, :model => Issue, :foreign_key => :priority_id},
29 "IPRI" => {:label => :enumeration_issue_priorities, :model => Issue, :foreign_key => :priority_id, :scope => :priorities},
30 "DCAT" => {:label => :enumeration_doc_categories, :model => Document, :foreign_key => :category_id},
30 "DCAT" => {:label => :enumeration_doc_categories, :model => Document, :foreign_key => :category_id, :scope => :document_categories},
31 "ACTI" => {:label => :enumeration_activities, :model => TimeEntry, :foreign_key => :activity_id}
31 "ACTI" => {:label => :enumeration_activities, :model => TimeEntry, :foreign_key => :activity_id, :scope => :activities}
32 }.freeze
32 }.freeze
33
33
34 def self.get_values(option)
34 # Creates a named scope for each type of value. The scope has a +default+ method
35 find(:all, :conditions => {:opt => option}, :order => 'position')
35 # that returns the default value, or nil if no value is set as default.
36 # Example:
37 # Enumeration.priorities
38 # Enumeration.priorities.default
39 OPTIONS.each do |k, v|
40 next unless v[:scope]
41 named_scope v[:scope], :conditions => { :opt => k }, :order => 'position' do
42 def default
43 find(:first, :conditions => { :is_default => true })
44 end
45 end
36 end
46 end
37
47
38 def self.default(option)
48 named_scope :values, lambda {|opt| { :conditions => { :opt => opt }, :order => 'position' } } do
39 find(:first, :conditions => {:opt => option, :is_default => true}, :order => 'position')
49 def default
50 find(:first, :conditions => { :is_default => true })
51 end
40 end
52 end
41
53
42 def option_name
54 def option_name
43 OPTIONS[self.opt][:label]
55 OPTIONS[self.opt][:label]
44 end
56 end
45
57
46 def before_save
58 def before_save
47 if is_default? && is_default_changed?
59 if is_default? && is_default_changed?
48 Enumeration.update_all("is_default = #{connection.quoted_false}", {:opt => opt})
60 Enumeration.update_all("is_default = #{connection.quoted_false}", {:opt => opt})
49 end
61 end
50 end
62 end
51
63
52 def objects_count
64 def objects_count
53 OPTIONS[self.opt][:model].count(:conditions => "#{OPTIONS[self.opt][:foreign_key]} = #{id}")
65 OPTIONS[self.opt][:model].count(:conditions => "#{OPTIONS[self.opt][:foreign_key]} = #{id}")
54 end
66 end
55
67
56 def in_use?
68 def in_use?
57 self.objects_count != 0
69 self.objects_count != 0
58 end
70 end
59
71
60 alias :destroy_without_reassign :destroy
72 alias :destroy_without_reassign :destroy
61
73
62 # Destroy the enumeration
74 # Destroy the enumeration
63 # If a enumeration is specified, objects are reassigned
75 # If a enumeration is specified, objects are reassigned
64 def destroy(reassign_to = nil)
76 def destroy(reassign_to = nil)
65 if reassign_to && reassign_to.is_a?(Enumeration)
77 if reassign_to && reassign_to.is_a?(Enumeration)
66 OPTIONS[self.opt][:model].update_all("#{OPTIONS[self.opt][:foreign_key]} = #{reassign_to.id}", "#{OPTIONS[self.opt][:foreign_key]} = #{id}")
78 OPTIONS[self.opt][:model].update_all("#{OPTIONS[self.opt][:foreign_key]} = #{reassign_to.id}", "#{OPTIONS[self.opt][:foreign_key]} = #{id}")
67 end
79 end
68 destroy_without_reassign
80 destroy_without_reassign
69 end
81 end
70
82
71 def <=>(enumeration)
83 def <=>(enumeration)
72 position <=> enumeration.position
84 position <=> enumeration.position
73 end
85 end
74
86
75 def to_s; name end
87 def to_s; name end
76
88
77 private
89 private
78 def check_integrity
90 def check_integrity
79 raise "Can't delete enumeration" if self.in_use?
91 raise "Can't delete enumeration" if self.in_use?
80 end
92 end
81 end
93 end
@@ -1,292 +1,292
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 Issue < ActiveRecord::Base
18 class Issue < ActiveRecord::Base
19 belongs_to :project
19 belongs_to :project
20 belongs_to :tracker
20 belongs_to :tracker
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
25 belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id'
25 belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id'
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
27
27
28 has_many :journals, :as => :journalized, :dependent => :destroy
28 has_many :journals, :as => :journalized, :dependent => :destroy
29 has_many :time_entries, :dependent => :delete_all
29 has_many :time_entries, :dependent => :delete_all
30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
31
31
32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
34
34
35 acts_as_attachable :after_remove => :attachment_removed
35 acts_as_attachable :after_remove => :attachment_removed
36 acts_as_customizable
36 acts_as_customizable
37 acts_as_watchable
37 acts_as_watchable
38 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
38 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
39 :include => [:project, :journals],
39 :include => [:project, :journals],
40 # sort by id so that limited eager loading doesn't break with postgresql
40 # sort by id so that limited eager loading doesn't break with postgresql
41 :order_column => "#{table_name}.id"
41 :order_column => "#{table_name}.id"
42 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
42 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
43 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
43 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
44 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
44 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
45
45
46 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
46 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
47 :author_key => :author_id
47 :author_key => :author_id
48
48
49 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
49 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
50 validates_length_of :subject, :maximum => 255
50 validates_length_of :subject, :maximum => 255
51 validates_inclusion_of :done_ratio, :in => 0..100
51 validates_inclusion_of :done_ratio, :in => 0..100
52 validates_numericality_of :estimated_hours, :allow_nil => true
52 validates_numericality_of :estimated_hours, :allow_nil => true
53
53
54 named_scope :visible, lambda {|*args| { :include => :project,
54 named_scope :visible, lambda {|*args| { :include => :project,
55 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
55 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
56
56
57 # Returns true if usr or current user is allowed to view the issue
57 # Returns true if usr or current user is allowed to view the issue
58 def visible?(usr=nil)
58 def visible?(usr=nil)
59 (usr || User.current).allowed_to?(:view_issues, self.project)
59 (usr || User.current).allowed_to?(:view_issues, self.project)
60 end
60 end
61
61
62 def after_initialize
62 def after_initialize
63 if new_record?
63 if new_record?
64 # set default values for new records only
64 # set default values for new records only
65 self.status ||= IssueStatus.default
65 self.status ||= IssueStatus.default
66 self.priority ||= Enumeration.default('IPRI')
66 self.priority ||= Enumeration.priorities.default
67 end
67 end
68 end
68 end
69
69
70 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
70 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
71 def available_custom_fields
71 def available_custom_fields
72 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
72 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
73 end
73 end
74
74
75 def copy_from(arg)
75 def copy_from(arg)
76 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
76 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
77 self.attributes = issue.attributes.dup
77 self.attributes = issue.attributes.dup
78 self.custom_values = issue.custom_values.collect {|v| v.clone}
78 self.custom_values = issue.custom_values.collect {|v| v.clone}
79 self
79 self
80 end
80 end
81
81
82 # Moves/copies an issue to a new project and tracker
82 # Moves/copies an issue to a new project and tracker
83 # Returns the moved/copied issue on success, false on failure
83 # Returns the moved/copied issue on success, false on failure
84 def move_to(new_project, new_tracker = nil, options = {})
84 def move_to(new_project, new_tracker = nil, options = {})
85 options ||= {}
85 options ||= {}
86 issue = options[:copy] ? self.clone : self
86 issue = options[:copy] ? self.clone : self
87 transaction do
87 transaction do
88 if new_project && issue.project_id != new_project.id
88 if new_project && issue.project_id != new_project.id
89 # delete issue relations
89 # delete issue relations
90 unless Setting.cross_project_issue_relations?
90 unless Setting.cross_project_issue_relations?
91 issue.relations_from.clear
91 issue.relations_from.clear
92 issue.relations_to.clear
92 issue.relations_to.clear
93 end
93 end
94 # issue is moved to another project
94 # issue is moved to another project
95 # reassign to the category with same name if any
95 # reassign to the category with same name if any
96 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
96 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
97 issue.category = new_category
97 issue.category = new_category
98 issue.fixed_version = nil
98 issue.fixed_version = nil
99 issue.project = new_project
99 issue.project = new_project
100 end
100 end
101 if new_tracker
101 if new_tracker
102 issue.tracker = new_tracker
102 issue.tracker = new_tracker
103 end
103 end
104 if options[:copy]
104 if options[:copy]
105 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
105 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
106 issue.status = self.status
106 issue.status = self.status
107 end
107 end
108 if issue.save
108 if issue.save
109 unless options[:copy]
109 unless options[:copy]
110 # Manually update project_id on related time entries
110 # Manually update project_id on related time entries
111 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
111 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
112 end
112 end
113 else
113 else
114 Issue.connection.rollback_db_transaction
114 Issue.connection.rollback_db_transaction
115 return false
115 return false
116 end
116 end
117 end
117 end
118 return issue
118 return issue
119 end
119 end
120
120
121 def priority_id=(pid)
121 def priority_id=(pid)
122 self.priority = nil
122 self.priority = nil
123 write_attribute(:priority_id, pid)
123 write_attribute(:priority_id, pid)
124 end
124 end
125
125
126 def estimated_hours=(h)
126 def estimated_hours=(h)
127 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
127 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
128 end
128 end
129
129
130 def validate
130 def validate
131 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
131 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
132 errors.add :due_date, :activerecord_error_not_a_date
132 errors.add :due_date, :activerecord_error_not_a_date
133 end
133 end
134
134
135 if self.due_date and self.start_date and self.due_date < self.start_date
135 if self.due_date and self.start_date and self.due_date < self.start_date
136 errors.add :due_date, :activerecord_error_greater_than_start_date
136 errors.add :due_date, :activerecord_error_greater_than_start_date
137 end
137 end
138
138
139 if start_date && soonest_start && start_date < soonest_start
139 if start_date && soonest_start && start_date < soonest_start
140 errors.add :start_date, :activerecord_error_invalid
140 errors.add :start_date, :activerecord_error_invalid
141 end
141 end
142 end
142 end
143
143
144 def validate_on_create
144 def validate_on_create
145 errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker)
145 errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker)
146 end
146 end
147
147
148 def before_create
148 def before_create
149 # default assignment based on category
149 # default assignment based on category
150 if assigned_to.nil? && category && category.assigned_to
150 if assigned_to.nil? && category && category.assigned_to
151 self.assigned_to = category.assigned_to
151 self.assigned_to = category.assigned_to
152 end
152 end
153 end
153 end
154
154
155 def before_save
155 def before_save
156 if @current_journal
156 if @current_journal
157 # attributes changes
157 # attributes changes
158 (Issue.column_names - %w(id description)).each {|c|
158 (Issue.column_names - %w(id description)).each {|c|
159 @current_journal.details << JournalDetail.new(:property => 'attr',
159 @current_journal.details << JournalDetail.new(:property => 'attr',
160 :prop_key => c,
160 :prop_key => c,
161 :old_value => @issue_before_change.send(c),
161 :old_value => @issue_before_change.send(c),
162 :value => send(c)) unless send(c)==@issue_before_change.send(c)
162 :value => send(c)) unless send(c)==@issue_before_change.send(c)
163 }
163 }
164 # custom fields changes
164 # custom fields changes
165 custom_values.each {|c|
165 custom_values.each {|c|
166 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
166 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
167 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
167 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
168 @current_journal.details << JournalDetail.new(:property => 'cf',
168 @current_journal.details << JournalDetail.new(:property => 'cf',
169 :prop_key => c.custom_field_id,
169 :prop_key => c.custom_field_id,
170 :old_value => @custom_values_before_change[c.custom_field_id],
170 :old_value => @custom_values_before_change[c.custom_field_id],
171 :value => c.value)
171 :value => c.value)
172 }
172 }
173 @current_journal.save
173 @current_journal.save
174 end
174 end
175 # Save the issue even if the journal is not saved (because empty)
175 # Save the issue even if the journal is not saved (because empty)
176 true
176 true
177 end
177 end
178
178
179 def after_save
179 def after_save
180 # Reload is needed in order to get the right status
180 # Reload is needed in order to get the right status
181 reload
181 reload
182
182
183 # Update start/due dates of following issues
183 # Update start/due dates of following issues
184 relations_from.each(&:set_issue_to_dates)
184 relations_from.each(&:set_issue_to_dates)
185
185
186 # Close duplicates if the issue was closed
186 # Close duplicates if the issue was closed
187 if @issue_before_change && !@issue_before_change.closed? && self.closed?
187 if @issue_before_change && !@issue_before_change.closed? && self.closed?
188 duplicates.each do |duplicate|
188 duplicates.each do |duplicate|
189 # Reload is need in case the duplicate was updated by a previous duplicate
189 # Reload is need in case the duplicate was updated by a previous duplicate
190 duplicate.reload
190 duplicate.reload
191 # Don't re-close it if it's already closed
191 # Don't re-close it if it's already closed
192 next if duplicate.closed?
192 next if duplicate.closed?
193 # Same user and notes
193 # Same user and notes
194 duplicate.init_journal(@current_journal.user, @current_journal.notes)
194 duplicate.init_journal(@current_journal.user, @current_journal.notes)
195 duplicate.update_attribute :status, self.status
195 duplicate.update_attribute :status, self.status
196 end
196 end
197 end
197 end
198 end
198 end
199
199
200 def init_journal(user, notes = "")
200 def init_journal(user, notes = "")
201 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
201 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
202 @issue_before_change = self.clone
202 @issue_before_change = self.clone
203 @issue_before_change.status = self.status
203 @issue_before_change.status = self.status
204 @custom_values_before_change = {}
204 @custom_values_before_change = {}
205 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
205 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
206 # Make sure updated_on is updated when adding a note.
206 # Make sure updated_on is updated when adding a note.
207 updated_on_will_change!
207 updated_on_will_change!
208 @current_journal
208 @current_journal
209 end
209 end
210
210
211 # Return true if the issue is closed, otherwise false
211 # Return true if the issue is closed, otherwise false
212 def closed?
212 def closed?
213 self.status.is_closed?
213 self.status.is_closed?
214 end
214 end
215
215
216 # Returns true if the issue is overdue
216 # Returns true if the issue is overdue
217 def overdue?
217 def overdue?
218 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
218 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
219 end
219 end
220
220
221 # Users the issue can be assigned to
221 # Users the issue can be assigned to
222 def assignable_users
222 def assignable_users
223 project.assignable_users
223 project.assignable_users
224 end
224 end
225
225
226 # Returns an array of status that user is able to apply
226 # Returns an array of status that user is able to apply
227 def new_statuses_allowed_to(user)
227 def new_statuses_allowed_to(user)
228 statuses = status.find_new_statuses_allowed_to(user.role_for_project(project), tracker)
228 statuses = status.find_new_statuses_allowed_to(user.role_for_project(project), tracker)
229 statuses << status unless statuses.empty?
229 statuses << status unless statuses.empty?
230 statuses.uniq.sort
230 statuses.uniq.sort
231 end
231 end
232
232
233 # Returns the mail adresses of users that should be notified for the issue
233 # Returns the mail adresses of users that should be notified for the issue
234 def recipients
234 def recipients
235 recipients = project.recipients
235 recipients = project.recipients
236 # Author and assignee are always notified unless they have been locked
236 # Author and assignee are always notified unless they have been locked
237 recipients << author.mail if author && author.active?
237 recipients << author.mail if author && author.active?
238 recipients << assigned_to.mail if assigned_to && assigned_to.active?
238 recipients << assigned_to.mail if assigned_to && assigned_to.active?
239 recipients.compact.uniq
239 recipients.compact.uniq
240 end
240 end
241
241
242 def spent_hours
242 def spent_hours
243 @spent_hours ||= time_entries.sum(:hours) || 0
243 @spent_hours ||= time_entries.sum(:hours) || 0
244 end
244 end
245
245
246 def relations
246 def relations
247 (relations_from + relations_to).sort
247 (relations_from + relations_to).sort
248 end
248 end
249
249
250 def all_dependent_issues
250 def all_dependent_issues
251 dependencies = []
251 dependencies = []
252 relations_from.each do |relation|
252 relations_from.each do |relation|
253 dependencies << relation.issue_to
253 dependencies << relation.issue_to
254 dependencies += relation.issue_to.all_dependent_issues
254 dependencies += relation.issue_to.all_dependent_issues
255 end
255 end
256 dependencies
256 dependencies
257 end
257 end
258
258
259 # Returns an array of issues that duplicate this one
259 # Returns an array of issues that duplicate this one
260 def duplicates
260 def duplicates
261 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
261 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
262 end
262 end
263
263
264 # Returns the due date or the target due date if any
264 # Returns the due date or the target due date if any
265 # Used on gantt chart
265 # Used on gantt chart
266 def due_before
266 def due_before
267 due_date || (fixed_version ? fixed_version.effective_date : nil)
267 due_date || (fixed_version ? fixed_version.effective_date : nil)
268 end
268 end
269
269
270 def duration
270 def duration
271 (start_date && due_date) ? due_date - start_date : 0
271 (start_date && due_date) ? due_date - start_date : 0
272 end
272 end
273
273
274 def soonest_start
274 def soonest_start
275 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
275 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
276 end
276 end
277
277
278 def to_s
278 def to_s
279 "#{tracker} ##{id}: #{subject}"
279 "#{tracker} ##{id}: #{subject}"
280 end
280 end
281
281
282 private
282 private
283
283
284 # Callback on attachment deletion
284 # Callback on attachment deletion
285 def attachment_removed(obj)
285 def attachment_removed(obj)
286 journal = init_journal(User.current)
286 journal = init_journal(User.current)
287 journal.details << JournalDetail.new(:property => 'attachment',
287 journal.details << JournalDetail.new(:property => 'attachment',
288 :prop_key => obj.id,
288 :prop_key => obj.id,
289 :old_value => obj.filename)
289 :old_value => obj.filename)
290 journal.save
290 journal.save
291 end
291 end
292 end
292 end
@@ -1,79 +1,79
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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 TimeEntry < ActiveRecord::Base
18 class TimeEntry < ActiveRecord::Base
19 # could have used polymorphic association
19 # could have used polymorphic association
20 # project association here allows easy loading of time entries at project level with one database trip
20 # project association here allows easy loading of time entries at project level with one database trip
21 belongs_to :project
21 belongs_to :project
22 belongs_to :issue
22 belongs_to :issue
23 belongs_to :user
23 belongs_to :user
24 belongs_to :activity, :class_name => 'Enumeration', :foreign_key => :activity_id
24 belongs_to :activity, :class_name => 'Enumeration', :foreign_key => :activity_id
25
25
26 attr_protected :project_id, :user_id, :tyear, :tmonth, :tweek
26 attr_protected :project_id, :user_id, :tyear, :tmonth, :tweek
27
27
28 acts_as_customizable
28 acts_as_customizable
29 acts_as_event :title => Proc.new {|o| "#{o.user}: #{lwr(:label_f_hour, o.hours)} (#{(o.issue || o.project).event_title})"},
29 acts_as_event :title => Proc.new {|o| "#{o.user}: #{lwr(:label_f_hour, o.hours)} (#{(o.issue || o.project).event_title})"},
30 :url => Proc.new {|o| {:controller => 'timelog', :action => 'details', :project_id => o.project}},
30 :url => Proc.new {|o| {:controller => 'timelog', :action => 'details', :project_id => o.project}},
31 :author => :user,
31 :author => :user,
32 :description => :comments
32 :description => :comments
33
33
34 validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
34 validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
35 validates_numericality_of :hours, :allow_nil => true, :message => :activerecord_error_invalid
35 validates_numericality_of :hours, :allow_nil => true, :message => :activerecord_error_invalid
36 validates_length_of :comments, :maximum => 255, :allow_nil => true
36 validates_length_of :comments, :maximum => 255, :allow_nil => true
37
37
38 def after_initialize
38 def after_initialize
39 if new_record? && self.activity.nil?
39 if new_record? && self.activity.nil?
40 if default_activity = Enumeration.default('ACTI')
40 if default_activity = Enumeration.activities.default
41 self.activity_id = default_activity.id
41 self.activity_id = default_activity.id
42 end
42 end
43 end
43 end
44 end
44 end
45
45
46 def before_validation
46 def before_validation
47 self.project = issue.project if issue && project.nil?
47 self.project = issue.project if issue && project.nil?
48 end
48 end
49
49
50 def validate
50 def validate
51 errors.add :hours, :activerecord_error_invalid if hours && (hours < 0 || hours >= 1000)
51 errors.add :hours, :activerecord_error_invalid if hours && (hours < 0 || hours >= 1000)
52 errors.add :project_id, :activerecord_error_invalid if project.nil?
52 errors.add :project_id, :activerecord_error_invalid if project.nil?
53 errors.add :issue_id, :activerecord_error_invalid if (issue_id && !issue) || (issue && project!=issue.project)
53 errors.add :issue_id, :activerecord_error_invalid if (issue_id && !issue) || (issue && project!=issue.project)
54 end
54 end
55
55
56 def hours=(h)
56 def hours=(h)
57 write_attribute :hours, (h.is_a?(String) ? (h.to_hours || h) : h)
57 write_attribute :hours, (h.is_a?(String) ? (h.to_hours || h) : h)
58 end
58 end
59
59
60 # tyear, tmonth, tweek assigned where setting spent_on attributes
60 # tyear, tmonth, tweek assigned where setting spent_on attributes
61 # these attributes make time aggregations easier
61 # these attributes make time aggregations easier
62 def spent_on=(date)
62 def spent_on=(date)
63 super
63 super
64 self.tyear = spent_on ? spent_on.year : nil
64 self.tyear = spent_on ? spent_on.year : nil
65 self.tmonth = spent_on ? spent_on.month : nil
65 self.tmonth = spent_on ? spent_on.month : nil
66 self.tweek = spent_on ? Date.civil(spent_on.year, spent_on.month, spent_on.day).cweek : nil
66 self.tweek = spent_on ? Date.civil(spent_on.year, spent_on.month, spent_on.day).cweek : nil
67 end
67 end
68
68
69 # Returns true if the time entry can be edited by usr, otherwise false
69 # Returns true if the time entry can be edited by usr, otherwise false
70 def editable_by?(usr)
70 def editable_by?(usr)
71 (usr == user && usr.allowed_to?(:edit_own_time_entries, project)) || usr.allowed_to?(:edit_time_entries, project)
71 (usr == user && usr.allowed_to?(:edit_own_time_entries, project)) || usr.allowed_to?(:edit_time_entries, project)
72 end
72 end
73
73
74 def self.visible_by(usr)
74 def self.visible_by(usr)
75 with_scope(:find => { :conditions => Project.allowed_to_condition(usr, :view_time_entries) }) do
75 with_scope(:find => { :conditions => Project.allowed_to_condition(usr, :view_time_entries) }) do
76 yield
76 yield
77 end
77 end
78 end
78 end
79 end
79 end
@@ -1,15 +1,15
1 <%= error_messages_for 'document' %>
1 <%= error_messages_for 'document' %>
2 <div class="box">
2 <div class="box">
3 <!--[form:document]-->
3 <!--[form:document]-->
4 <p><label for="document_category_id"><%=l(:field_category)%></label>
4 <p><label for="document_category_id"><%=l(:field_category)%></label>
5 <%= select('document', 'category_id', Enumeration.get_values('DCAT').collect {|c| [c.name, c.id]}) %></p>
5 <%= select('document', 'category_id', Enumeration.document_categories.collect {|c| [c.name, c.id]}) %></p>
6
6
7 <p><label for="document_title"><%=l(:field_title)%> <span class="required">*</span></label>
7 <p><label for="document_title"><%=l(:field_title)%> <span class="required">*</span></label>
8 <%= text_field 'document', 'title', :size => 60 %></p>
8 <%= text_field 'document', 'title', :size => 60 %></p>
9
9
10 <p><label for="document_description"><%=l(:field_description)%></label>
10 <p><label for="document_description"><%=l(:field_description)%></label>
11 <%= text_area 'document', 'description', :cols => 60, :rows => 15, :class => 'wiki-edit' %></p>
11 <%= text_area 'document', 'description', :cols => 60, :rows => 15, :class => 'wiki-edit' %></p>
12 <!--[eoform:document]-->
12 <!--[eoform:document]-->
13 </div>
13 </div>
14
14
15 <%= wikitoolbar_for 'document_description' %>
15 <%= wikitoolbar_for 'document_description' %>
@@ -1,31 +1,31
1 <h2><%=l(:label_enumerations)%></h2>
1 <h2><%=l(:label_enumerations)%></h2>
2
2
3 <% Enumeration::OPTIONS.each do |option, params| %>
3 <% Enumeration::OPTIONS.each do |option, params| %>
4 <h3><%= l(params[:label]) %></h3>
4 <h3><%= l(params[:label]) %></h3>
5
5
6 <% enumerations = Enumeration.get_values(option) %>
6 <% enumerations = Enumeration.values(option) %>
7 <% if enumerations.any? %>
7 <% if enumerations.any? %>
8 <table class="list">
8 <table class="list">
9 <% enumerations.each do |enumeration| %>
9 <% enumerations.each do |enumeration| %>
10 <tr class="<%= cycle('odd', 'even') %>">
10 <tr class="<%= cycle('odd', 'even') %>">
11 <td><%= link_to h(enumeration), :action => 'edit', :id => enumeration %></td>
11 <td><%= link_to h(enumeration), :action => 'edit', :id => enumeration %></td>
12 <td style="width:15%;"><%= image_tag('true.png') if enumeration.is_default? %></td>
12 <td style="width:15%;"><%= image_tag('true.png') if enumeration.is_default? %></td>
13 <td style="width:15%;">
13 <td style="width:15%;">
14 <%= link_to image_tag('2uparrow.png', :alt => l(:label_sort_highest)), {:action => 'move', :id => enumeration, :position => 'highest'}, :method => :post, :title => l(:label_sort_highest) %>
14 <%= link_to image_tag('2uparrow.png', :alt => l(:label_sort_highest)), {:action => 'move', :id => enumeration, :position => 'highest'}, :method => :post, :title => l(:label_sort_highest) %>
15 <%= link_to image_tag('1uparrow.png', :alt => l(:label_sort_higher)), {:action => 'move', :id => enumeration, :position => 'higher'}, :method => :post, :title => l(:label_sort_higher) %> -
15 <%= link_to image_tag('1uparrow.png', :alt => l(:label_sort_higher)), {:action => 'move', :id => enumeration, :position => 'higher'}, :method => :post, :title => l(:label_sort_higher) %> -
16 <%= link_to image_tag('1downarrow.png', :alt => l(:label_sort_lower)), {:action => 'move', :id => enumeration, :position => 'lower'}, :method => :post, :title => l(:label_sort_lower) %>
16 <%= link_to image_tag('1downarrow.png', :alt => l(:label_sort_lower)), {:action => 'move', :id => enumeration, :position => 'lower'}, :method => :post, :title => l(:label_sort_lower) %>
17 <%= link_to image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), {:action => 'move', :id => enumeration, :position => 'lowest'}, :method => :post, :title => l(:label_sort_lowest) %>
17 <%= link_to image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), {:action => 'move', :id => enumeration, :position => 'lowest'}, :method => :post, :title => l(:label_sort_lowest) %>
18 </td>
18 </td>
19 <td align="center" style="width:10%;">
19 <td align="center" style="width:10%;">
20 <%= link_to l(:button_delete), { :action => 'destroy', :id => enumeration }, :method => :post, :confirm => l(:text_are_you_sure), :class => "icon icon-del" %>
20 <%= link_to l(:button_delete), { :action => 'destroy', :id => enumeration }, :method => :post, :confirm => l(:text_are_you_sure), :class => "icon icon-del" %>
21 </td>
21 </td>
22 </tr>
22 </tr>
23 <% end %>
23 <% end %>
24 </table>
24 </table>
25 <% reset_cycle %>
25 <% reset_cycle %>
26 <% end %>
26 <% end %>
27
27
28 <p><%= link_to l(:label_enumeration_new), { :action => 'new', :opt => option } %></p>
28 <p><%= link_to l(:label_enumeration_new), { :action => 'new', :opt => option } %></p>
29 <% end %>
29 <% end %>
30
30
31 <% html_title(l(:label_enumerations)) -%>
31 <% html_title(l(:label_enumerations)) -%>
@@ -1,58 +1,58
1 <h2><%= l(:label_bulk_edit_selected_issues) %></h2>
1 <h2><%= l(:label_bulk_edit_selected_issues) %></h2>
2
2
3 <ul><%= @issues.collect {|i| content_tag('li', link_to(h("#{i.tracker} ##{i.id}"), { :action => 'show', :id => i }) + h(": #{i.subject}")) }.join("\n") %></ul>
3 <ul><%= @issues.collect {|i| content_tag('li', link_to(h("#{i.tracker} ##{i.id}"), { :action => 'show', :id => i }) + h(": #{i.subject}")) }.join("\n") %></ul>
4
4
5 <% form_tag() do %>
5 <% form_tag() do %>
6 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %>
6 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %>
7 <div class="box">
7 <div class="box">
8 <fieldset>
8 <fieldset>
9 <legend><%= l(:label_change_properties) %></legend>
9 <legend><%= l(:label_change_properties) %></legend>
10 <p>
10 <p>
11 <% if @available_statuses.any? %>
11 <% if @available_statuses.any? %>
12 <label><%= l(:field_status) %>:
12 <label><%= l(:field_status) %>:
13 <%= select_tag('status_id', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@available_statuses, :id, :name)) %></label>
13 <%= select_tag('status_id', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@available_statuses, :id, :name)) %></label>
14 <% end %>
14 <% end %>
15 <label><%= l(:field_priority) %>:
15 <label><%= l(:field_priority) %>:
16 <%= select_tag('priority_id', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(Enumeration.get_values('IPRI'), :id, :name)) %></label>
16 <%= select_tag('priority_id', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(Enumeration.priorities, :id, :name)) %></label>
17 <label><%= l(:field_category) %>:
17 <label><%= l(:field_category) %>:
18 <%= select_tag('category_id', content_tag('option', l(:label_no_change_option), :value => '') +
18 <%= select_tag('category_id', content_tag('option', l(:label_no_change_option), :value => '') +
19 content_tag('option', l(:label_none), :value => 'none') +
19 content_tag('option', l(:label_none), :value => 'none') +
20 options_from_collection_for_select(@project.issue_categories, :id, :name)) %></label>
20 options_from_collection_for_select(@project.issue_categories, :id, :name)) %></label>
21 </p>
21 </p>
22 <p>
22 <p>
23 <label><%= l(:field_assigned_to) %>:
23 <label><%= l(:field_assigned_to) %>:
24 <%= select_tag('assigned_to_id', content_tag('option', l(:label_no_change_option), :value => '') +
24 <%= select_tag('assigned_to_id', content_tag('option', l(:label_no_change_option), :value => '') +
25 content_tag('option', l(:label_nobody), :value => 'none') +
25 content_tag('option', l(:label_nobody), :value => 'none') +
26 options_from_collection_for_select(@project.assignable_users, :id, :name)) %></label>
26 options_from_collection_for_select(@project.assignable_users, :id, :name)) %></label>
27 <label><%= l(:field_fixed_version) %>:
27 <label><%= l(:field_fixed_version) %>:
28 <%= select_tag('fixed_version_id', content_tag('option', l(:label_no_change_option), :value => '') +
28 <%= select_tag('fixed_version_id', content_tag('option', l(:label_no_change_option), :value => '') +
29 content_tag('option', l(:label_none), :value => 'none') +
29 content_tag('option', l(:label_none), :value => 'none') +
30 options_from_collection_for_select(@project.versions.sort, :id, :name)) %></label>
30 options_from_collection_for_select(@project.versions.sort, :id, :name)) %></label>
31 </p>
31 </p>
32
32
33 <p>
33 <p>
34 <label><%= l(:field_start_date) %>:
34 <label><%= l(:field_start_date) %>:
35 <%= text_field_tag 'start_date', '', :size => 10 %><%= calendar_for('start_date') %></label>
35 <%= text_field_tag 'start_date', '', :size => 10 %><%= calendar_for('start_date') %></label>
36 <label><%= l(:field_due_date) %>:
36 <label><%= l(:field_due_date) %>:
37 <%= text_field_tag 'due_date', '', :size => 10 %><%= calendar_for('due_date') %></label>
37 <%= text_field_tag 'due_date', '', :size => 10 %><%= calendar_for('due_date') %></label>
38 <label><%= l(:field_done_ratio) %>:
38 <label><%= l(:field_done_ratio) %>:
39 <%= select_tag 'done_ratio', options_for_select([[l(:label_no_change_option), '']] + (0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></label>
39 <%= select_tag 'done_ratio', options_for_select([[l(:label_no_change_option), '']] + (0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></label>
40 </p>
40 </p>
41
41
42 <% @custom_fields.each do |custom_field| %>
42 <% @custom_fields.each do |custom_field| %>
43 <p><label><%= h(custom_field.name) %></label>
43 <p><label><%= h(custom_field.name) %></label>
44 <%= select_tag "custom_field_values[#{custom_field.id}]", options_for_select([[l(:label_no_change_option), '']] + custom_field.possible_values) %></label>
44 <%= select_tag "custom_field_values[#{custom_field.id}]", options_for_select([[l(:label_no_change_option), '']] + custom_field.possible_values) %></label>
45 </p>
45 </p>
46 <% end %>
46 <% end %>
47
47
48 <%= call_hook(:view_issues_bulk_edit_details_bottom, { :issues => @issues }) %>
48 <%= call_hook(:view_issues_bulk_edit_details_bottom, { :issues => @issues }) %>
49 </fieldset>
49 </fieldset>
50
50
51 <fieldset><legend><%= l(:field_notes) %></legend>
51 <fieldset><legend><%= l(:field_notes) %></legend>
52 <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
52 <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
53 <%= wikitoolbar_for 'notes' %>
53 <%= wikitoolbar_for 'notes' %>
54 </fieldset>
54 </fieldset>
55 </div>
55 </div>
56
56
57 <p><%= submit_tag l(:button_submit) %>
57 <p><%= submit_tag l(:button_submit) %>
58 <% end %>
58 <% end %>
@@ -1,505 +1,505
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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.find :first, :conditions => { :is_closed => true }
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 = Enumeration.get_values('IPRI')
43 priorities = Enumeration.priorities
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.find(:all, :conditions => {:builtin => 0}, :order => 'position ASC')
56 roles = Role.find(:all, :conditions => {:builtin => 0}, :order => 'position ASC')
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 set_table_name :mantis_user_table
87 set_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.gsub!(/[^\w\s\'\-]/i, '')
91 @firstname.gsub!(/[^\w\s\'\-]/i, '')
92 @firstname
92 @firstname
93 end
93 end
94
94
95 def lastname
95 def lastname
96 @lastname = realname.blank? ? '-' : realname.split[1..-1].join(' ')[0..29]
96 @lastname = realname.blank? ? '-' : realname.split[1..-1].join(' ')[0..29]
97 @lastname.gsub!(/[^\w\s\'\-]/i, '')
97 @lastname.gsub!(/[^\w\s\'\-]/i, '')
98 @lastname = '-' if @lastname.blank?
98 @lastname = '-' if @lastname.blank?
99 @lastname
99 @lastname
100 end
100 end
101
101
102 def email
102 def email
103 if read_attribute(:email).match(/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i) &&
103 if read_attribute(:email).match(/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i) &&
104 !User.find_by_mail(read_attribute(:email))
104 !User.find_by_mail(read_attribute(:email))
105 @email = read_attribute(:email)
105 @email = read_attribute(:email)
106 else
106 else
107 @email = "#{username}@foo.bar"
107 @email = "#{username}@foo.bar"
108 end
108 end
109 end
109 end
110
110
111 def username
111 def username
112 read_attribute(:username)[0..29].gsub(/[^a-zA-Z0-9_\-@\.]/, '-')
112 read_attribute(:username)[0..29].gsub(/[^a-zA-Z0-9_\-@\.]/, '-')
113 end
113 end
114 end
114 end
115
115
116 class MantisProject < ActiveRecord::Base
116 class MantisProject < ActiveRecord::Base
117 set_table_name :mantis_project_table
117 set_table_name :mantis_project_table
118 has_many :versions, :class_name => "MantisVersion", :foreign_key => :project_id
118 has_many :versions, :class_name => "MantisVersion", :foreign_key => :project_id
119 has_many :categories, :class_name => "MantisCategory", :foreign_key => :project_id
119 has_many :categories, :class_name => "MantisCategory", :foreign_key => :project_id
120 has_many :news, :class_name => "MantisNews", :foreign_key => :project_id
120 has_many :news, :class_name => "MantisNews", :foreign_key => :project_id
121 has_many :members, :class_name => "MantisProjectUser", :foreign_key => :project_id
121 has_many :members, :class_name => "MantisProjectUser", :foreign_key => :project_id
122
122
123 def name
123 def name
124 read_attribute(:name)[0..29]
124 read_attribute(:name)[0..29]
125 end
125 end
126
126
127 def identifier
127 def identifier
128 read_attribute(:name).underscore[0..19].gsub(/[^a-z0-9\-]/, '-')
128 read_attribute(:name).underscore[0..19].gsub(/[^a-z0-9\-]/, '-')
129 end
129 end
130 end
130 end
131
131
132 class MantisVersion < ActiveRecord::Base
132 class MantisVersion < ActiveRecord::Base
133 set_table_name :mantis_project_version_table
133 set_table_name :mantis_project_version_table
134
134
135 def version
135 def version
136 read_attribute(:version)[0..29]
136 read_attribute(:version)[0..29]
137 end
137 end
138
138
139 def description
139 def description
140 read_attribute(:description)[0..254]
140 read_attribute(:description)[0..254]
141 end
141 end
142 end
142 end
143
143
144 class MantisCategory < ActiveRecord::Base
144 class MantisCategory < ActiveRecord::Base
145 set_table_name :mantis_project_category_table
145 set_table_name :mantis_project_category_table
146 end
146 end
147
147
148 class MantisProjectUser < ActiveRecord::Base
148 class MantisProjectUser < ActiveRecord::Base
149 set_table_name :mantis_project_user_list_table
149 set_table_name :mantis_project_user_list_table
150 end
150 end
151
151
152 class MantisBug < ActiveRecord::Base
152 class MantisBug < ActiveRecord::Base
153 set_table_name :mantis_bug_table
153 set_table_name :mantis_bug_table
154 belongs_to :bug_text, :class_name => "MantisBugText", :foreign_key => :bug_text_id
154 belongs_to :bug_text, :class_name => "MantisBugText", :foreign_key => :bug_text_id
155 has_many :bug_notes, :class_name => "MantisBugNote", :foreign_key => :bug_id
155 has_many :bug_notes, :class_name => "MantisBugNote", :foreign_key => :bug_id
156 has_many :bug_files, :class_name => "MantisBugFile", :foreign_key => :bug_id
156 has_many :bug_files, :class_name => "MantisBugFile", :foreign_key => :bug_id
157 has_many :bug_monitors, :class_name => "MantisBugMonitor", :foreign_key => :bug_id
157 has_many :bug_monitors, :class_name => "MantisBugMonitor", :foreign_key => :bug_id
158 end
158 end
159
159
160 class MantisBugText < ActiveRecord::Base
160 class MantisBugText < ActiveRecord::Base
161 set_table_name :mantis_bug_text_table
161 set_table_name :mantis_bug_text_table
162
162
163 # Adds Mantis steps_to_reproduce and additional_information fields
163 # Adds Mantis steps_to_reproduce and additional_information fields
164 # to description if any
164 # to description if any
165 def full_description
165 def full_description
166 full_description = description
166 full_description = description
167 full_description += "\n\n*Steps to reproduce:*\n\n#{steps_to_reproduce}" unless steps_to_reproduce.blank?
167 full_description += "\n\n*Steps to reproduce:*\n\n#{steps_to_reproduce}" unless steps_to_reproduce.blank?
168 full_description += "\n\n*Additional information:*\n\n#{additional_information}" unless additional_information.blank?
168 full_description += "\n\n*Additional information:*\n\n#{additional_information}" unless additional_information.blank?
169 full_description
169 full_description
170 end
170 end
171 end
171 end
172
172
173 class MantisBugNote < ActiveRecord::Base
173 class MantisBugNote < ActiveRecord::Base
174 set_table_name :mantis_bugnote_table
174 set_table_name :mantis_bugnote_table
175 belongs_to :bug, :class_name => "MantisBug", :foreign_key => :bug_id
175 belongs_to :bug, :class_name => "MantisBug", :foreign_key => :bug_id
176 belongs_to :bug_note_text, :class_name => "MantisBugNoteText", :foreign_key => :bugnote_text_id
176 belongs_to :bug_note_text, :class_name => "MantisBugNoteText", :foreign_key => :bugnote_text_id
177 end
177 end
178
178
179 class MantisBugNoteText < ActiveRecord::Base
179 class MantisBugNoteText < ActiveRecord::Base
180 set_table_name :mantis_bugnote_text_table
180 set_table_name :mantis_bugnote_text_table
181 end
181 end
182
182
183 class MantisBugFile < ActiveRecord::Base
183 class MantisBugFile < ActiveRecord::Base
184 set_table_name :mantis_bug_file_table
184 set_table_name :mantis_bug_file_table
185
185
186 def size
186 def size
187 filesize
187 filesize
188 end
188 end
189
189
190 def original_filename
190 def original_filename
191 MantisMigrate.encode(filename)
191 MantisMigrate.encode(filename)
192 end
192 end
193
193
194 def content_type
194 def content_type
195 file_type
195 file_type
196 end
196 end
197
197
198 def read
198 def read
199 content
199 content
200 end
200 end
201 end
201 end
202
202
203 class MantisBugRelationship < ActiveRecord::Base
203 class MantisBugRelationship < ActiveRecord::Base
204 set_table_name :mantis_bug_relationship_table
204 set_table_name :mantis_bug_relationship_table
205 end
205 end
206
206
207 class MantisBugMonitor < ActiveRecord::Base
207 class MantisBugMonitor < ActiveRecord::Base
208 set_table_name :mantis_bug_monitor_table
208 set_table_name :mantis_bug_monitor_table
209 end
209 end
210
210
211 class MantisNews < ActiveRecord::Base
211 class MantisNews < ActiveRecord::Base
212 set_table_name :mantis_news_table
212 set_table_name :mantis_news_table
213 end
213 end
214
214
215 class MantisCustomField < ActiveRecord::Base
215 class MantisCustomField < ActiveRecord::Base
216 set_table_name :mantis_custom_field_table
216 set_table_name :mantis_custom_field_table
217 set_inheritance_column :none
217 set_inheritance_column :none
218 has_many :values, :class_name => "MantisCustomFieldString", :foreign_key => :field_id
218 has_many :values, :class_name => "MantisCustomFieldString", :foreign_key => :field_id
219 has_many :projects, :class_name => "MantisCustomFieldProject", :foreign_key => :field_id
219 has_many :projects, :class_name => "MantisCustomFieldProject", :foreign_key => :field_id
220
220
221 def format
221 def format
222 read_attribute :type
222 read_attribute :type
223 end
223 end
224
224
225 def name
225 def name
226 read_attribute(:name)[0..29].gsub(/[^\w\s\'\-]/, '-')
226 read_attribute(:name)[0..29].gsub(/[^\w\s\'\-]/, '-')
227 end
227 end
228 end
228 end
229
229
230 class MantisCustomFieldProject < ActiveRecord::Base
230 class MantisCustomFieldProject < ActiveRecord::Base
231 set_table_name :mantis_custom_field_project_table
231 set_table_name :mantis_custom_field_project_table
232 end
232 end
233
233
234 class MantisCustomFieldString < ActiveRecord::Base
234 class MantisCustomFieldString < ActiveRecord::Base
235 set_table_name :mantis_custom_field_string_table
235 set_table_name :mantis_custom_field_string_table
236 end
236 end
237
237
238
238
239 def self.migrate
239 def self.migrate
240
240
241 # Users
241 # Users
242 print "Migrating users"
242 print "Migrating users"
243 User.delete_all "login <> 'admin'"
243 User.delete_all "login <> 'admin'"
244 users_map = {}
244 users_map = {}
245 users_migrated = 0
245 users_migrated = 0
246 MantisUser.find(:all).each do |user|
246 MantisUser.find(:all).each do |user|
247 u = User.new :firstname => encode(user.firstname),
247 u = User.new :firstname => encode(user.firstname),
248 :lastname => encode(user.lastname),
248 :lastname => encode(user.lastname),
249 :mail => user.email,
249 :mail => user.email,
250 :last_login_on => user.last_visit
250 :last_login_on => user.last_visit
251 u.login = user.username
251 u.login = user.username
252 u.password = 'mantis'
252 u.password = 'mantis'
253 u.status = User::STATUS_LOCKED if user.enabled != 1
253 u.status = User::STATUS_LOCKED if user.enabled != 1
254 u.admin = true if user.access_level == 90
254 u.admin = true if user.access_level == 90
255 next unless u.save!
255 next unless u.save!
256 users_migrated += 1
256 users_migrated += 1
257 users_map[user.id] = u.id
257 users_map[user.id] = u.id
258 print '.'
258 print '.'
259 end
259 end
260 puts
260 puts
261
261
262 # Projects
262 # Projects
263 print "Migrating projects"
263 print "Migrating projects"
264 Project.destroy_all
264 Project.destroy_all
265 projects_map = {}
265 projects_map = {}
266 versions_map = {}
266 versions_map = {}
267 categories_map = {}
267 categories_map = {}
268 MantisProject.find(:all).each do |project|
268 MantisProject.find(:all).each do |project|
269 p = Project.new :name => encode(project.name),
269 p = Project.new :name => encode(project.name),
270 :description => encode(project.description)
270 :description => encode(project.description)
271 p.identifier = project.identifier
271 p.identifier = project.identifier
272 next unless p.save
272 next unless p.save
273 projects_map[project.id] = p.id
273 projects_map[project.id] = p.id
274 p.enabled_module_names = ['issue_tracking', 'news', 'wiki']
274 p.enabled_module_names = ['issue_tracking', 'news', 'wiki']
275 p.trackers << TRACKER_BUG
275 p.trackers << TRACKER_BUG
276 p.trackers << TRACKER_FEATURE
276 p.trackers << TRACKER_FEATURE
277 print '.'
277 print '.'
278
278
279 # Project members
279 # Project members
280 project.members.each do |member|
280 project.members.each do |member|
281 m = Member.new :user => User.find_by_id(users_map[member.user_id]),
281 m = Member.new :user => User.find_by_id(users_map[member.user_id]),
282 :role => ROLE_MAPPING[member.access_level] || DEFAULT_ROLE
282 :role => ROLE_MAPPING[member.access_level] || DEFAULT_ROLE
283 m.project = p
283 m.project = p
284 m.save
284 m.save
285 end
285 end
286
286
287 # Project versions
287 # Project versions
288 project.versions.each do |version|
288 project.versions.each do |version|
289 v = Version.new :name => encode(version.version),
289 v = Version.new :name => encode(version.version),
290 :description => encode(version.description),
290 :description => encode(version.description),
291 :effective_date => version.date_order.to_date
291 :effective_date => version.date_order.to_date
292 v.project = p
292 v.project = p
293 v.save
293 v.save
294 versions_map[version.id] = v.id
294 versions_map[version.id] = v.id
295 end
295 end
296
296
297 # Project categories
297 # Project categories
298 project.categories.each do |category|
298 project.categories.each do |category|
299 g = IssueCategory.new :name => category.category[0,30]
299 g = IssueCategory.new :name => category.category[0,30]
300 g.project = p
300 g.project = p
301 g.save
301 g.save
302 categories_map[category.category] = g.id
302 categories_map[category.category] = g.id
303 end
303 end
304 end
304 end
305 puts
305 puts
306
306
307 # Bugs
307 # Bugs
308 print "Migrating bugs"
308 print "Migrating bugs"
309 Issue.destroy_all
309 Issue.destroy_all
310 issues_map = {}
310 issues_map = {}
311 keep_bug_ids = (Issue.count == 0)
311 keep_bug_ids = (Issue.count == 0)
312 MantisBug.find(:all, :order => 'id ASC').each do |bug|
312 MantisBug.find(:all, :order => 'id ASC').each do |bug|
313 next unless projects_map[bug.project_id] && users_map[bug.reporter_id]
313 next unless projects_map[bug.project_id] && users_map[bug.reporter_id]
314 i = Issue.new :project_id => projects_map[bug.project_id],
314 i = Issue.new :project_id => projects_map[bug.project_id],
315 :subject => encode(bug.summary),
315 :subject => encode(bug.summary),
316 :description => encode(bug.bug_text.full_description),
316 :description => encode(bug.bug_text.full_description),
317 :priority => PRIORITY_MAPPING[bug.priority] || DEFAULT_PRIORITY,
317 :priority => PRIORITY_MAPPING[bug.priority] || DEFAULT_PRIORITY,
318 :created_on => bug.date_submitted,
318 :created_on => bug.date_submitted,
319 :updated_on => bug.last_updated
319 :updated_on => bug.last_updated
320 i.author = User.find_by_id(users_map[bug.reporter_id])
320 i.author = User.find_by_id(users_map[bug.reporter_id])
321 i.category = IssueCategory.find_by_project_id_and_name(i.project_id, bug.category[0,30]) unless bug.category.blank?
321 i.category = IssueCategory.find_by_project_id_and_name(i.project_id, bug.category[0,30]) unless bug.category.blank?
322 i.fixed_version = Version.find_by_project_id_and_name(i.project_id, bug.fixed_in_version) unless bug.fixed_in_version.blank?
322 i.fixed_version = Version.find_by_project_id_and_name(i.project_id, bug.fixed_in_version) unless bug.fixed_in_version.blank?
323 i.status = STATUS_MAPPING[bug.status] || DEFAULT_STATUS
323 i.status = STATUS_MAPPING[bug.status] || DEFAULT_STATUS
324 i.tracker = (bug.severity == 10 ? TRACKER_FEATURE : TRACKER_BUG)
324 i.tracker = (bug.severity == 10 ? TRACKER_FEATURE : TRACKER_BUG)
325 i.id = bug.id if keep_bug_ids
325 i.id = bug.id if keep_bug_ids
326 next unless i.save
326 next unless i.save
327 issues_map[bug.id] = i.id
327 issues_map[bug.id] = i.id
328 print '.'
328 print '.'
329
329
330 # Assignee
330 # Assignee
331 # Redmine checks that the assignee is a project member
331 # Redmine checks that the assignee is a project member
332 if (bug.handler_id && users_map[bug.handler_id])
332 if (bug.handler_id && users_map[bug.handler_id])
333 i.assigned_to = User.find_by_id(users_map[bug.handler_id])
333 i.assigned_to = User.find_by_id(users_map[bug.handler_id])
334 i.save_with_validation(false)
334 i.save_with_validation(false)
335 end
335 end
336
336
337 # Bug notes
337 # Bug notes
338 bug.bug_notes.each do |note|
338 bug.bug_notes.each do |note|
339 next unless users_map[note.reporter_id]
339 next unless users_map[note.reporter_id]
340 n = Journal.new :notes => encode(note.bug_note_text.note),
340 n = Journal.new :notes => encode(note.bug_note_text.note),
341 :created_on => note.date_submitted
341 :created_on => note.date_submitted
342 n.user = User.find_by_id(users_map[note.reporter_id])
342 n.user = User.find_by_id(users_map[note.reporter_id])
343 n.journalized = i
343 n.journalized = i
344 n.save
344 n.save
345 end
345 end
346
346
347 # Bug files
347 # Bug files
348 bug.bug_files.each do |file|
348 bug.bug_files.each do |file|
349 a = Attachment.new :created_on => file.date_added
349 a = Attachment.new :created_on => file.date_added
350 a.file = file
350 a.file = file
351 a.author = User.find :first
351 a.author = User.find :first
352 a.container = i
352 a.container = i
353 a.save
353 a.save
354 end
354 end
355
355
356 # Bug monitors
356 # Bug monitors
357 bug.bug_monitors.each do |monitor|
357 bug.bug_monitors.each do |monitor|
358 next unless users_map[monitor.user_id]
358 next unless users_map[monitor.user_id]
359 i.add_watcher(User.find_by_id(users_map[monitor.user_id]))
359 i.add_watcher(User.find_by_id(users_map[monitor.user_id]))
360 end
360 end
361 end
361 end
362
362
363 # update issue id sequence if needed (postgresql)
363 # update issue id sequence if needed (postgresql)
364 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
364 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
365 puts
365 puts
366
366
367 # Bug relationships
367 # Bug relationships
368 print "Migrating bug relations"
368 print "Migrating bug relations"
369 MantisBugRelationship.find(:all).each do |relation|
369 MantisBugRelationship.find(:all).each do |relation|
370 next unless issues_map[relation.source_bug_id] && issues_map[relation.destination_bug_id]
370 next unless issues_map[relation.source_bug_id] && issues_map[relation.destination_bug_id]
371 r = IssueRelation.new :relation_type => RELATION_TYPE_MAPPING[relation.relationship_type]
371 r = IssueRelation.new :relation_type => RELATION_TYPE_MAPPING[relation.relationship_type]
372 r.issue_from = Issue.find_by_id(issues_map[relation.source_bug_id])
372 r.issue_from = Issue.find_by_id(issues_map[relation.source_bug_id])
373 r.issue_to = Issue.find_by_id(issues_map[relation.destination_bug_id])
373 r.issue_to = Issue.find_by_id(issues_map[relation.destination_bug_id])
374 pp r unless r.save
374 pp r unless r.save
375 print '.'
375 print '.'
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.find(:all, :conditions => 'project_id > 0').each do |news|
382 MantisNews.find(:all, :conditions => 'project_id > 0').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 end
391 end
392 puts
392 puts
393
393
394 # Custom fields
394 # Custom fields
395 print "Migrating custom fields"
395 print "Migrating custom fields"
396 IssueCustomField.destroy_all
396 IssueCustomField.destroy_all
397 MantisCustomField.find(:all).each do |field|
397 MantisCustomField.find(:all).each do |field|
398 f = IssueCustomField.new :name => field.name[0..29],
398 f = IssueCustomField.new :name => field.name[0..29],
399 :field_format => CUSTOM_FIELD_TYPE_MAPPING[field.format],
399 :field_format => CUSTOM_FIELD_TYPE_MAPPING[field.format],
400 :min_length => field.length_min,
400 :min_length => field.length_min,
401 :max_length => field.length_max,
401 :max_length => field.length_max,
402 :regexp => field.valid_regexp,
402 :regexp => field.valid_regexp,
403 :possible_values => field.possible_values.split('|'),
403 :possible_values => field.possible_values.split('|'),
404 :is_required => field.require_report?
404 :is_required => field.require_report?
405 next unless f.save
405 next unless f.save
406 print '.'
406 print '.'
407
407
408 # Trackers association
408 # Trackers association
409 f.trackers = Tracker.find :all
409 f.trackers = Tracker.find :all
410
410
411 # Projects association
411 # Projects association
412 field.projects.each do |project|
412 field.projects.each do |project|
413 f.projects << Project.find_by_id(projects_map[project.project_id]) if projects_map[project.project_id]
413 f.projects << Project.find_by_id(projects_map[project.project_id]) if projects_map[project.project_id]
414 end
414 end
415
415
416 # Values
416 # Values
417 field.values.each do |value|
417 field.values.each do |value|
418 v = CustomValue.new :custom_field_id => f.id,
418 v = CustomValue.new :custom_field_id => f.id,
419 :value => value.value
419 :value => value.value
420 v.customized = Issue.find_by_id(issues_map[value.bug_id]) if issues_map[value.bug_id]
420 v.customized = Issue.find_by_id(issues_map[value.bug_id]) if issues_map[value.bug_id]
421 v.save
421 v.save
422 end unless f.new_record?
422 end unless f.new_record?
423 end
423 end
424 puts
424 puts
425
425
426 puts
426 puts
427 puts "Users: #{users_migrated}/#{MantisUser.count}"
427 puts "Users: #{users_migrated}/#{MantisUser.count}"
428 puts "Projects: #{Project.count}/#{MantisProject.count}"
428 puts "Projects: #{Project.count}/#{MantisProject.count}"
429 puts "Memberships: #{Member.count}/#{MantisProjectUser.count}"
429 puts "Memberships: #{Member.count}/#{MantisProjectUser.count}"
430 puts "Versions: #{Version.count}/#{MantisVersion.count}"
430 puts "Versions: #{Version.count}/#{MantisVersion.count}"
431 puts "Categories: #{IssueCategory.count}/#{MantisCategory.count}"
431 puts "Categories: #{IssueCategory.count}/#{MantisCategory.count}"
432 puts "Bugs: #{Issue.count}/#{MantisBug.count}"
432 puts "Bugs: #{Issue.count}/#{MantisBug.count}"
433 puts "Bug notes: #{Journal.count}/#{MantisBugNote.count}"
433 puts "Bug notes: #{Journal.count}/#{MantisBugNote.count}"
434 puts "Bug files: #{Attachment.count}/#{MantisBugFile.count}"
434 puts "Bug files: #{Attachment.count}/#{MantisBugFile.count}"
435 puts "Bug relations: #{IssueRelation.count}/#{MantisBugRelationship.count}"
435 puts "Bug relations: #{IssueRelation.count}/#{MantisBugRelationship.count}"
436 puts "Bug monitors: #{Watcher.count}/#{MantisBugMonitor.count}"
436 puts "Bug monitors: #{Watcher.count}/#{MantisBugMonitor.count}"
437 puts "News: #{News.count}/#{MantisNews.count}"
437 puts "News: #{News.count}/#{MantisNews.count}"
438 puts "Custom fields: #{IssueCustomField.count}/#{MantisCustomField.count}"
438 puts "Custom fields: #{IssueCustomField.count}/#{MantisCustomField.count}"
439 end
439 end
440
440
441 def self.encoding(charset)
441 def self.encoding(charset)
442 @ic = Iconv.new('UTF-8', charset)
442 @ic = Iconv.new('UTF-8', charset)
443 rescue Iconv::InvalidEncoding
443 rescue Iconv::InvalidEncoding
444 return false
444 return false
445 end
445 end
446
446
447 def self.establish_connection(params)
447 def self.establish_connection(params)
448 constants.each do |const|
448 constants.each do |const|
449 klass = const_get(const)
449 klass = const_get(const)
450 next unless klass.respond_to? 'establish_connection'
450 next unless klass.respond_to? 'establish_connection'
451 klass.establish_connection params
451 klass.establish_connection params
452 end
452 end
453 end
453 end
454
454
455 def self.encode(text)
455 def self.encode(text)
456 @ic.iconv text
456 @ic.iconv text
457 rescue
457 rescue
458 text
458 text
459 end
459 end
460 end
460 end
461
461
462 puts
462 puts
463 if Redmine::DefaultData::Loader.no_data?
463 if Redmine::DefaultData::Loader.no_data?
464 puts "Redmine configuration need to be loaded before importing data."
464 puts "Redmine configuration need to be loaded before importing data."
465 puts "Please, run this first:"
465 puts "Please, run this first:"
466 puts
466 puts
467 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
467 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
468 exit
468 exit
469 end
469 end
470
470
471 puts "WARNING: Your Redmine data will be deleted during this process."
471 puts "WARNING: Your Redmine data will be deleted during this process."
472 print "Are you sure you want to continue ? [y/N] "
472 print "Are you sure you want to continue ? [y/N] "
473 break unless STDIN.gets.match(/^y$/i)
473 break unless STDIN.gets.match(/^y$/i)
474
474
475 # Default Mantis database settings
475 # Default Mantis database settings
476 db_params = {:adapter => 'mysql',
476 db_params = {:adapter => 'mysql',
477 :database => 'bugtracker',
477 :database => 'bugtracker',
478 :host => 'localhost',
478 :host => 'localhost',
479 :username => 'root',
479 :username => 'root',
480 :password => '' }
480 :password => '' }
481
481
482 puts
482 puts
483 puts "Please enter settings for your Mantis database"
483 puts "Please enter settings for your Mantis database"
484 [:adapter, :host, :database, :username, :password].each do |param|
484 [:adapter, :host, :database, :username, :password].each do |param|
485 print "#{param} [#{db_params[param]}]: "
485 print "#{param} [#{db_params[param]}]: "
486 value = STDIN.gets.chomp!
486 value = STDIN.gets.chomp!
487 db_params[param] = value unless value.blank?
487 db_params[param] = value unless value.blank?
488 end
488 end
489
489
490 while true
490 while true
491 print "encoding [UTF-8]: "
491 print "encoding [UTF-8]: "
492 encoding = STDIN.gets.chomp!
492 encoding = STDIN.gets.chomp!
493 encoding = 'UTF-8' if encoding.blank?
493 encoding = 'UTF-8' if encoding.blank?
494 break if MantisMigrate.encoding encoding
494 break if MantisMigrate.encoding encoding
495 puts "Invalid encoding!"
495 puts "Invalid encoding!"
496 end
496 end
497 puts
497 puts
498
498
499 # Make sure bugs can refer bugs in other projects
499 # Make sure bugs can refer bugs in other projects
500 Setting.cross_project_issue_relations = 1 if Setting.respond_to? 'cross_project_issue_relations'
500 Setting.cross_project_issue_relations = 1 if Setting.respond_to? 'cross_project_issue_relations'
501
501
502 MantisMigrate.establish_connection db_params
502 MantisMigrate.establish_connection db_params
503 MantisMigrate.migrate
503 MantisMigrate.migrate
504 end
504 end
505 end
505 end
@@ -1,750 +1,750
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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.find :first, :conditions => { :is_closed => true }
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 = Enumeration.get_values('IPRI')
40 priorities = Enumeration.priorities
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.find(:all, :conditions => {:builtin => 0}, :order => 'position ASC')
64 roles = Role.find(:all, :conditions => {:builtin => 0}, :order => 'position ASC')
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 set_table_name :component
88 set_table_name :component
89 end
89 end
90
90
91 class TracMilestone < ActiveRecord::Base
91 class TracMilestone < ActiveRecord::Base
92 set_table_name :milestone
92 set_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 set_table_name :ticket_custom
117 set_table_name :ticket_custom
118 end
118 end
119
119
120 class TracAttachment < ActiveRecord::Base
120 class TracAttachment < ActiveRecord::Base
121 set_table_name :attachment
121 set_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 Redmine::MimeType.of(filename) || ''
131 Redmine::MimeType.of(filename) || ''
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 read
138 def read
139 File.open("#{trac_fullpath}", 'rb').read
139 File.open("#{trac_fullpath}", 'rb').read
140 end
140 end
141
141
142 def description
142 def description
143 read_attribute(:description).to_s.slice(0,255)
143 read_attribute(:description).to_s.slice(0,255)
144 end
144 end
145
145
146 private
146 private
147 def trac_fullpath
147 def trac_fullpath
148 attachment_type = read_attribute(:type)
148 attachment_type = read_attribute(:type)
149 trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02x', x[0]) }
149 trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02x', x[0]) }
150 "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
150 "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
151 end
151 end
152 end
152 end
153
153
154 class TracTicket < ActiveRecord::Base
154 class TracTicket < ActiveRecord::Base
155 set_table_name :ticket
155 set_table_name :ticket
156 set_inheritance_column :none
156 set_inheritance_column :none
157
157
158 # ticket changes: only migrate status changes and comments
158 # ticket changes: only migrate status changes and comments
159 has_many :changes, :class_name => "TracTicketChange", :foreign_key => :ticket
159 has_many :changes, :class_name => "TracTicketChange", :foreign_key => :ticket
160 has_many :attachments, :class_name => "TracAttachment",
160 has_many :attachments, :class_name => "TracAttachment",
161 :finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" +
161 :finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" +
162 " WHERE #{TracMigrate::TracAttachment.table_name}.type = 'ticket'" +
162 " WHERE #{TracMigrate::TracAttachment.table_name}.type = 'ticket'" +
163 ' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{id}\''
163 ' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{id}\''
164 has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
164 has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
165
165
166 def ticket_type
166 def ticket_type
167 read_attribute(:type)
167 read_attribute(:type)
168 end
168 end
169
169
170 def summary
170 def summary
171 read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary)
171 read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary)
172 end
172 end
173
173
174 def description
174 def description
175 read_attribute(:description).blank? ? summary : read_attribute(:description)
175 read_attribute(:description).blank? ? summary : read_attribute(:description)
176 end
176 end
177
177
178 def time; Time.at(read_attribute(:time)) end
178 def time; Time.at(read_attribute(:time)) end
179 def changetime; Time.at(read_attribute(:changetime)) end
179 def changetime; Time.at(read_attribute(:changetime)) end
180 end
180 end
181
181
182 class TracTicketChange < ActiveRecord::Base
182 class TracTicketChange < ActiveRecord::Base
183 set_table_name :ticket_change
183 set_table_name :ticket_change
184
184
185 def time; Time.at(read_attribute(:time)) end
185 def time; Time.at(read_attribute(:time)) end
186 end
186 end
187
187
188 TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
188 TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
189 TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
189 TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
190 TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
190 TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
191 TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
191 TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
192 TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
192 TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
193 WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
193 WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
194 CamelCase TitleIndex)
194 CamelCase TitleIndex)
195
195
196 class TracWikiPage < ActiveRecord::Base
196 class TracWikiPage < ActiveRecord::Base
197 set_table_name :wiki
197 set_table_name :wiki
198 set_primary_key :name
198 set_primary_key :name
199
199
200 has_many :attachments, :class_name => "TracAttachment",
200 has_many :attachments, :class_name => "TracAttachment",
201 :finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" +
201 :finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" +
202 " WHERE #{TracMigrate::TracAttachment.table_name}.type = 'wiki'" +
202 " WHERE #{TracMigrate::TracAttachment.table_name}.type = 'wiki'" +
203 ' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{id}\''
203 ' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{id}\''
204
204
205 def self.columns
205 def self.columns
206 # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
206 # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
207 super.select {|column| column.name.to_s != 'readonly'}
207 super.select {|column| column.name.to_s != 'readonly'}
208 end
208 end
209
209
210 def time; Time.at(read_attribute(:time)) end
210 def time; Time.at(read_attribute(:time)) end
211 end
211 end
212
212
213 class TracPermission < ActiveRecord::Base
213 class TracPermission < ActiveRecord::Base
214 set_table_name :permission
214 set_table_name :permission
215 end
215 end
216
216
217 class TracSessionAttribute < ActiveRecord::Base
217 class TracSessionAttribute < ActiveRecord::Base
218 set_table_name :session_attribute
218 set_table_name :session_attribute
219 end
219 end
220
220
221 def self.find_or_create_user(username, project_member = false)
221 def self.find_or_create_user(username, project_member = false)
222 return User.anonymous if username.blank?
222 return User.anonymous if username.blank?
223
223
224 u = User.find_by_login(username)
224 u = User.find_by_login(username)
225 if !u
225 if !u
226 # Create a new user if not found
226 # Create a new user if not found
227 mail = username[0,limit_for(User, 'mail')]
227 mail = username[0,limit_for(User, 'mail')]
228 if mail_attr = TracSessionAttribute.find_by_sid_and_name(username, 'email')
228 if mail_attr = TracSessionAttribute.find_by_sid_and_name(username, 'email')
229 mail = mail_attr.value
229 mail = mail_attr.value
230 end
230 end
231 mail = "#{mail}@foo.bar" unless mail.include?("@")
231 mail = "#{mail}@foo.bar" unless mail.include?("@")
232
232
233 name = username
233 name = username
234 if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
234 if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
235 name = name_attr.value
235 name = name_attr.value
236 end
236 end
237 name =~ (/(.*)(\s+\w+)?/)
237 name =~ (/(.*)(\s+\w+)?/)
238 fn = $1.strip
238 fn = $1.strip
239 ln = ($2 || '-').strip
239 ln = ($2 || '-').strip
240
240
241 u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
241 u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
242 :firstname => fn[0, limit_for(User, 'firstname')].gsub(/[^\w\s\'\-]/i, '-'),
242 :firstname => fn[0, limit_for(User, 'firstname')].gsub(/[^\w\s\'\-]/i, '-'),
243 :lastname => ln[0, limit_for(User, 'lastname')].gsub(/[^\w\s\'\-]/i, '-')
243 :lastname => ln[0, limit_for(User, 'lastname')].gsub(/[^\w\s\'\-]/i, '-')
244
244
245 u.login = username[0,limit_for(User, 'login')].gsub(/[^a-z0-9_\-@\.]/i, '-')
245 u.login = username[0,limit_for(User, 'login')].gsub(/[^a-z0-9_\-@\.]/i, '-')
246 u.password = 'trac'
246 u.password = 'trac'
247 u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
247 u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
248 # finally, a default user is used if the new user is not valid
248 # finally, a default user is used if the new user is not valid
249 u = User.find(:first) unless u.save
249 u = User.find(:first) unless u.save
250 end
250 end
251 # Make sure he is a member of the project
251 # Make sure he is a member of the project
252 if project_member && !u.member_of?(@target_project)
252 if project_member && !u.member_of?(@target_project)
253 role = DEFAULT_ROLE
253 role = DEFAULT_ROLE
254 if u.admin
254 if u.admin
255 role = ROLE_MAPPING['admin']
255 role = ROLE_MAPPING['admin']
256 elsif TracPermission.find_by_username_and_action(username, 'developer')
256 elsif TracPermission.find_by_username_and_action(username, 'developer')
257 role = ROLE_MAPPING['developer']
257 role = ROLE_MAPPING['developer']
258 end
258 end
259 Member.create(:user => u, :project => @target_project, :role => role)
259 Member.create(:user => u, :project => @target_project, :role => role)
260 u.reload
260 u.reload
261 end
261 end
262 u
262 u
263 end
263 end
264
264
265 # Basic wiki syntax conversion
265 # Basic wiki syntax conversion
266 def self.convert_wiki_text(text)
266 def self.convert_wiki_text(text)
267 # Titles
267 # Titles
268 text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
268 text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
269 # External Links
269 # External Links
270 text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
270 text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
271 # Ticket links:
271 # Ticket links:
272 # [ticket:234 Text],[ticket:234 This is a test]
272 # [ticket:234 Text],[ticket:234 This is a test]
273 text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '"\2":/issues/show/\1')
273 text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '"\2":/issues/show/\1')
274 # ticket:1234
274 # ticket:1234
275 # #1 is working cause Redmine uses the same syntax.
275 # #1 is working cause Redmine uses the same syntax.
276 text = text.gsub(/ticket\:([^\ ]+)/, '#\1')
276 text = text.gsub(/ticket\:([^\ ]+)/, '#\1')
277 # Milestone links:
277 # Milestone links:
278 # [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)]
278 # [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)]
279 # The text "Milestone 0.1.0 (Mercury)" is not converted,
279 # The text "Milestone 0.1.0 (Mercury)" is not converted,
280 # cause Redmine's wiki does not support this.
280 # cause Redmine's wiki does not support this.
281 text = text.gsub(/\[milestone\:\"([^\"]+)\"\ (.+?)\]/, 'version:"\1"')
281 text = text.gsub(/\[milestone\:\"([^\"]+)\"\ (.+?)\]/, 'version:"\1"')
282 # [milestone:"0.1.0 Mercury"]
282 # [milestone:"0.1.0 Mercury"]
283 text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"')
283 text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"')
284 text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"')
284 text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"')
285 # milestone:0.1.0
285 # milestone:0.1.0
286 text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1')
286 text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1')
287 text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1')
287 text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1')
288 # Internal Links
288 # Internal Links
289 text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
289 text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
290 text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
290 text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
291 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
291 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
292 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
292 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
293 text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
293 text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
294 text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
294 text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
295
295
296 # Links to pages UsingJustWikiCaps
296 # Links to pages UsingJustWikiCaps
297 text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
297 text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
298 # Normalize things that were supposed to not be links
298 # Normalize things that were supposed to not be links
299 # like !NotALink
299 # like !NotALink
300 text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
300 text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
301 # Revisions links
301 # Revisions links
302 text = text.gsub(/\[(\d+)\]/, 'r\1')
302 text = text.gsub(/\[(\d+)\]/, 'r\1')
303 # Ticket number re-writing
303 # Ticket number re-writing
304 text = text.gsub(/#(\d+)/) do |s|
304 text = text.gsub(/#(\d+)/) do |s|
305 if $1.length < 10
305 if $1.length < 10
306 TICKET_MAP[$1.to_i] ||= $1
306 TICKET_MAP[$1.to_i] ||= $1
307 "\##{TICKET_MAP[$1.to_i] || $1}"
307 "\##{TICKET_MAP[$1.to_i] || $1}"
308 else
308 else
309 s
309 s
310 end
310 end
311 end
311 end
312 # We would like to convert the Code highlighting too
312 # We would like to convert the Code highlighting too
313 # This will go into the next line.
313 # This will go into the next line.
314 shebang_line = false
314 shebang_line = false
315 # Reguar expression for start of code
315 # Reguar expression for start of code
316 pre_re = /\{\{\{/
316 pre_re = /\{\{\{/
317 # Code hightlighing...
317 # Code hightlighing...
318 shebang_re = /^\#\!([a-z]+)/
318 shebang_re = /^\#\!([a-z]+)/
319 # Regular expression for end of code
319 # Regular expression for end of code
320 pre_end_re = /\}\}\}/
320 pre_end_re = /\}\}\}/
321
321
322 # Go through the whole text..extract it line by line
322 # Go through the whole text..extract it line by line
323 text = text.gsub(/^(.*)$/) do |line|
323 text = text.gsub(/^(.*)$/) do |line|
324 m_pre = pre_re.match(line)
324 m_pre = pre_re.match(line)
325 if m_pre
325 if m_pre
326 line = '<pre>'
326 line = '<pre>'
327 else
327 else
328 m_sl = shebang_re.match(line)
328 m_sl = shebang_re.match(line)
329 if m_sl
329 if m_sl
330 shebang_line = true
330 shebang_line = true
331 line = '<code class="' + m_sl[1] + '">'
331 line = '<code class="' + m_sl[1] + '">'
332 end
332 end
333 m_pre_end = pre_end_re.match(line)
333 m_pre_end = pre_end_re.match(line)
334 if m_pre_end
334 if m_pre_end
335 line = '</pre>'
335 line = '</pre>'
336 if shebang_line
336 if shebang_line
337 line = '</code>' + line
337 line = '</code>' + line
338 end
338 end
339 end
339 end
340 end
340 end
341 line
341 line
342 end
342 end
343
343
344 # Highlighting
344 # Highlighting
345 text = text.gsub(/'''''([^\s])/, '_*\1')
345 text = text.gsub(/'''''([^\s])/, '_*\1')
346 text = text.gsub(/([^\s])'''''/, '\1*_')
346 text = text.gsub(/([^\s])'''''/, '\1*_')
347 text = text.gsub(/'''/, '*')
347 text = text.gsub(/'''/, '*')
348 text = text.gsub(/''/, '_')
348 text = text.gsub(/''/, '_')
349 text = text.gsub(/__/, '+')
349 text = text.gsub(/__/, '+')
350 text = text.gsub(/~~/, '-')
350 text = text.gsub(/~~/, '-')
351 text = text.gsub(/`/, '@')
351 text = text.gsub(/`/, '@')
352 text = text.gsub(/,,/, '~')
352 text = text.gsub(/,,/, '~')
353 # Lists
353 # Lists
354 text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
354 text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
355
355
356 text
356 text
357 end
357 end
358
358
359 def self.migrate
359 def self.migrate
360 establish_connection
360 establish_connection
361
361
362 # Quick database test
362 # Quick database test
363 TracComponent.count
363 TracComponent.count
364
364
365 migrated_components = 0
365 migrated_components = 0
366 migrated_milestones = 0
366 migrated_milestones = 0
367 migrated_tickets = 0
367 migrated_tickets = 0
368 migrated_custom_values = 0
368 migrated_custom_values = 0
369 migrated_ticket_attachments = 0
369 migrated_ticket_attachments = 0
370 migrated_wiki_edits = 0
370 migrated_wiki_edits = 0
371 migrated_wiki_attachments = 0
371 migrated_wiki_attachments = 0
372
372
373 #Wiki system initializing...
373 #Wiki system initializing...
374 @target_project.wiki.destroy if @target_project.wiki
374 @target_project.wiki.destroy if @target_project.wiki
375 @target_project.reload
375 @target_project.reload
376 wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
376 wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
377 wiki_edit_count = 0
377 wiki_edit_count = 0
378
378
379 # Components
379 # Components
380 print "Migrating components"
380 print "Migrating components"
381 issues_category_map = {}
381 issues_category_map = {}
382 TracComponent.find(:all).each do |component|
382 TracComponent.find(:all).each do |component|
383 print '.'
383 print '.'
384 STDOUT.flush
384 STDOUT.flush
385 c = IssueCategory.new :project => @target_project,
385 c = IssueCategory.new :project => @target_project,
386 :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
386 :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
387 next unless c.save
387 next unless c.save
388 issues_category_map[component.name] = c
388 issues_category_map[component.name] = c
389 migrated_components += 1
389 migrated_components += 1
390 end
390 end
391 puts
391 puts
392
392
393 # Milestones
393 # Milestones
394 print "Migrating milestones"
394 print "Migrating milestones"
395 version_map = {}
395 version_map = {}
396 TracMilestone.find(:all).each do |milestone|
396 TracMilestone.find(:all).each do |milestone|
397 print '.'
397 print '.'
398 STDOUT.flush
398 STDOUT.flush
399 # First we try to find the wiki page...
399 # First we try to find the wiki page...
400 p = wiki.find_or_new_page(milestone.name.to_s)
400 p = wiki.find_or_new_page(milestone.name.to_s)
401 p.content = WikiContent.new(:page => p) if p.new_record?
401 p.content = WikiContent.new(:page => p) if p.new_record?
402 p.content.text = milestone.description.to_s
402 p.content.text = milestone.description.to_s
403 p.content.author = find_or_create_user('trac')
403 p.content.author = find_or_create_user('trac')
404 p.content.comments = 'Milestone'
404 p.content.comments = 'Milestone'
405 p.save
405 p.save
406
406
407 v = Version.new :project => @target_project,
407 v = Version.new :project => @target_project,
408 :name => encode(milestone.name[0, limit_for(Version, 'name')]),
408 :name => encode(milestone.name[0, limit_for(Version, 'name')]),
409 :description => nil,
409 :description => nil,
410 :wiki_page_title => milestone.name.to_s,
410 :wiki_page_title => milestone.name.to_s,
411 :effective_date => milestone.completed
411 :effective_date => milestone.completed
412
412
413 next unless v.save
413 next unless v.save
414 version_map[milestone.name] = v
414 version_map[milestone.name] = v
415 migrated_milestones += 1
415 migrated_milestones += 1
416 end
416 end
417 puts
417 puts
418
418
419 # Custom fields
419 # Custom fields
420 # TODO: read trac.ini instead
420 # TODO: read trac.ini instead
421 print "Migrating custom fields"
421 print "Migrating custom fields"
422 custom_field_map = {}
422 custom_field_map = {}
423 TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
423 TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
424 print '.'
424 print '.'
425 STDOUT.flush
425 STDOUT.flush
426 # Redmine custom field name
426 # Redmine custom field name
427 field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
427 field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
428 # Find if the custom already exists in Redmine
428 # Find if the custom already exists in Redmine
429 f = IssueCustomField.find_by_name(field_name)
429 f = IssueCustomField.find_by_name(field_name)
430 # Or create a new one
430 # Or create a new one
431 f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
431 f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
432 :field_format => 'string')
432 :field_format => 'string')
433
433
434 next if f.new_record?
434 next if f.new_record?
435 f.trackers = Tracker.find(:all)
435 f.trackers = Tracker.find(:all)
436 f.projects << @target_project
436 f.projects << @target_project
437 custom_field_map[field.name] = f
437 custom_field_map[field.name] = f
438 end
438 end
439 puts
439 puts
440
440
441 # Trac 'resolution' field as a Redmine custom field
441 # Trac 'resolution' field as a Redmine custom field
442 r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" })
442 r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" })
443 r = IssueCustomField.new(:name => 'Resolution',
443 r = IssueCustomField.new(:name => 'Resolution',
444 :field_format => 'list',
444 :field_format => 'list',
445 :is_filter => true) if r.nil?
445 :is_filter => true) if r.nil?
446 r.trackers = Tracker.find(:all)
446 r.trackers = Tracker.find(:all)
447 r.projects << @target_project
447 r.projects << @target_project
448 r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
448 r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
449 r.save!
449 r.save!
450 custom_field_map['resolution'] = r
450 custom_field_map['resolution'] = r
451
451
452 # Tickets
452 # Tickets
453 print "Migrating tickets"
453 print "Migrating tickets"
454 TracTicket.find(:all, :order => 'id ASC').each do |ticket|
454 TracTicket.find(:all, :order => 'id ASC').each do |ticket|
455 print '.'
455 print '.'
456 STDOUT.flush
456 STDOUT.flush
457 i = Issue.new :project => @target_project,
457 i = Issue.new :project => @target_project,
458 :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
458 :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
459 :description => convert_wiki_text(encode(ticket.description)),
459 :description => convert_wiki_text(encode(ticket.description)),
460 :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
460 :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
461 :created_on => ticket.time
461 :created_on => ticket.time
462 i.author = find_or_create_user(ticket.reporter)
462 i.author = find_or_create_user(ticket.reporter)
463 i.category = issues_category_map[ticket.component] unless ticket.component.blank?
463 i.category = issues_category_map[ticket.component] unless ticket.component.blank?
464 i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
464 i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
465 i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
465 i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
466 i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
466 i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
467 i.id = ticket.id unless Issue.exists?(ticket.id)
467 i.id = ticket.id unless Issue.exists?(ticket.id)
468 next unless Time.fake(ticket.changetime) { i.save }
468 next unless Time.fake(ticket.changetime) { i.save }
469 TICKET_MAP[ticket.id] = i.id
469 TICKET_MAP[ticket.id] = i.id
470 migrated_tickets += 1
470 migrated_tickets += 1
471
471
472 # Owner
472 # Owner
473 unless ticket.owner.blank?
473 unless ticket.owner.blank?
474 i.assigned_to = find_or_create_user(ticket.owner, true)
474 i.assigned_to = find_or_create_user(ticket.owner, true)
475 Time.fake(ticket.changetime) { i.save }
475 Time.fake(ticket.changetime) { i.save }
476 end
476 end
477
477
478 # Comments and status/resolution changes
478 # Comments and status/resolution changes
479 ticket.changes.group_by(&:time).each do |time, changeset|
479 ticket.changes.group_by(&:time).each do |time, changeset|
480 status_change = changeset.select {|change| change.field == 'status'}.first
480 status_change = changeset.select {|change| change.field == 'status'}.first
481 resolution_change = changeset.select {|change| change.field == 'resolution'}.first
481 resolution_change = changeset.select {|change| change.field == 'resolution'}.first
482 comment_change = changeset.select {|change| change.field == 'comment'}.first
482 comment_change = changeset.select {|change| change.field == 'comment'}.first
483
483
484 n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
484 n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
485 :created_on => time
485 :created_on => time
486 n.user = find_or_create_user(changeset.first.author)
486 n.user = find_or_create_user(changeset.first.author)
487 n.journalized = i
487 n.journalized = i
488 if status_change &&
488 if status_change &&
489 STATUS_MAPPING[status_change.oldvalue] &&
489 STATUS_MAPPING[status_change.oldvalue] &&
490 STATUS_MAPPING[status_change.newvalue] &&
490 STATUS_MAPPING[status_change.newvalue] &&
491 (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
491 (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
492 n.details << JournalDetail.new(:property => 'attr',
492 n.details << JournalDetail.new(:property => 'attr',
493 :prop_key => 'status_id',
493 :prop_key => 'status_id',
494 :old_value => STATUS_MAPPING[status_change.oldvalue].id,
494 :old_value => STATUS_MAPPING[status_change.oldvalue].id,
495 :value => STATUS_MAPPING[status_change.newvalue].id)
495 :value => STATUS_MAPPING[status_change.newvalue].id)
496 end
496 end
497 if resolution_change
497 if resolution_change
498 n.details << JournalDetail.new(:property => 'cf',
498 n.details << JournalDetail.new(:property => 'cf',
499 :prop_key => custom_field_map['resolution'].id,
499 :prop_key => custom_field_map['resolution'].id,
500 :old_value => resolution_change.oldvalue,
500 :old_value => resolution_change.oldvalue,
501 :value => resolution_change.newvalue)
501 :value => resolution_change.newvalue)
502 end
502 end
503 n.save unless n.details.empty? && n.notes.blank?
503 n.save unless n.details.empty? && n.notes.blank?
504 end
504 end
505
505
506 # Attachments
506 # Attachments
507 ticket.attachments.each do |attachment|
507 ticket.attachments.each do |attachment|
508 next unless attachment.exist?
508 next unless attachment.exist?
509 a = Attachment.new :created_on => attachment.time
509 a = Attachment.new :created_on => attachment.time
510 a.file = attachment
510 a.file = attachment
511 a.author = find_or_create_user(attachment.author)
511 a.author = find_or_create_user(attachment.author)
512 a.container = i
512 a.container = i
513 a.description = attachment.description
513 a.description = attachment.description
514 migrated_ticket_attachments += 1 if a.save
514 migrated_ticket_attachments += 1 if a.save
515 end
515 end
516
516
517 # Custom fields
517 # Custom fields
518 custom_values = ticket.customs.inject({}) do |h, custom|
518 custom_values = ticket.customs.inject({}) do |h, custom|
519 if custom_field = custom_field_map[custom.name]
519 if custom_field = custom_field_map[custom.name]
520 h[custom_field.id] = custom.value
520 h[custom_field.id] = custom.value
521 migrated_custom_values += 1
521 migrated_custom_values += 1
522 end
522 end
523 h
523 h
524 end
524 end
525 if custom_field_map['resolution'] && !ticket.resolution.blank?
525 if custom_field_map['resolution'] && !ticket.resolution.blank?
526 custom_values[custom_field_map['resolution'].id] = ticket.resolution
526 custom_values[custom_field_map['resolution'].id] = ticket.resolution
527 end
527 end
528 i.custom_field_values = custom_values
528 i.custom_field_values = custom_values
529 i.save_custom_field_values
529 i.save_custom_field_values
530 end
530 end
531
531
532 # update issue id sequence if needed (postgresql)
532 # update issue id sequence if needed (postgresql)
533 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
533 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
534 puts
534 puts
535
535
536 # Wiki
536 # Wiki
537 print "Migrating wiki"
537 print "Migrating wiki"
538 if wiki.save
538 if wiki.save
539 TracWikiPage.find(:all, :order => 'name, version').each do |page|
539 TracWikiPage.find(:all, :order => 'name, version').each do |page|
540 # Do not migrate Trac manual wiki pages
540 # Do not migrate Trac manual wiki pages
541 next if TRAC_WIKI_PAGES.include?(page.name)
541 next if TRAC_WIKI_PAGES.include?(page.name)
542 wiki_edit_count += 1
542 wiki_edit_count += 1
543 print '.'
543 print '.'
544 STDOUT.flush
544 STDOUT.flush
545 p = wiki.find_or_new_page(page.name)
545 p = wiki.find_or_new_page(page.name)
546 p.content = WikiContent.new(:page => p) if p.new_record?
546 p.content = WikiContent.new(:page => p) if p.new_record?
547 p.content.text = page.text
547 p.content.text = page.text
548 p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
548 p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
549 p.content.comments = page.comment
549 p.content.comments = page.comment
550 Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
550 Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
551
551
552 next if p.content.new_record?
552 next if p.content.new_record?
553 migrated_wiki_edits += 1
553 migrated_wiki_edits += 1
554
554
555 # Attachments
555 # Attachments
556 page.attachments.each do |attachment|
556 page.attachments.each do |attachment|
557 next unless attachment.exist?
557 next unless attachment.exist?
558 next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
558 next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
559 a = Attachment.new :created_on => attachment.time
559 a = Attachment.new :created_on => attachment.time
560 a.file = attachment
560 a.file = attachment
561 a.author = find_or_create_user(attachment.author)
561 a.author = find_or_create_user(attachment.author)
562 a.description = attachment.description
562 a.description = attachment.description
563 a.container = p
563 a.container = p
564 migrated_wiki_attachments += 1 if a.save
564 migrated_wiki_attachments += 1 if a.save
565 end
565 end
566 end
566 end
567
567
568 wiki.reload
568 wiki.reload
569 wiki.pages.each do |page|
569 wiki.pages.each do |page|
570 page.content.text = convert_wiki_text(page.content.text)
570 page.content.text = convert_wiki_text(page.content.text)
571 Time.fake(page.content.updated_on) { page.content.save }
571 Time.fake(page.content.updated_on) { page.content.save }
572 end
572 end
573 end
573 end
574 puts
574 puts
575
575
576 puts
576 puts
577 puts "Components: #{migrated_components}/#{TracComponent.count}"
577 puts "Components: #{migrated_components}/#{TracComponent.count}"
578 puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
578 puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
579 puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
579 puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
580 puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
580 puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
581 puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
581 puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
582 puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
582 puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
583 puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
583 puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
584 end
584 end
585
585
586 def self.limit_for(klass, attribute)
586 def self.limit_for(klass, attribute)
587 klass.columns_hash[attribute.to_s].limit
587 klass.columns_hash[attribute.to_s].limit
588 end
588 end
589
589
590 def self.encoding(charset)
590 def self.encoding(charset)
591 @ic = Iconv.new('UTF-8', charset)
591 @ic = Iconv.new('UTF-8', charset)
592 rescue Iconv::InvalidEncoding
592 rescue Iconv::InvalidEncoding
593 puts "Invalid encoding!"
593 puts "Invalid encoding!"
594 return false
594 return false
595 end
595 end
596
596
597 def self.set_trac_directory(path)
597 def self.set_trac_directory(path)
598 @@trac_directory = path
598 @@trac_directory = path
599 raise "This directory doesn't exist!" unless File.directory?(path)
599 raise "This directory doesn't exist!" unless File.directory?(path)
600 raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
600 raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
601 @@trac_directory
601 @@trac_directory
602 rescue Exception => e
602 rescue Exception => e
603 puts e
603 puts e
604 return false
604 return false
605 end
605 end
606
606
607 def self.trac_directory
607 def self.trac_directory
608 @@trac_directory
608 @@trac_directory
609 end
609 end
610
610
611 def self.set_trac_adapter(adapter)
611 def self.set_trac_adapter(adapter)
612 return false if adapter.blank?
612 return false if adapter.blank?
613 raise "Unknown adapter: #{adapter}!" unless %w(sqlite sqlite3 mysql postgresql).include?(adapter)
613 raise "Unknown adapter: #{adapter}!" unless %w(sqlite sqlite3 mysql postgresql).include?(adapter)
614 # If adapter is sqlite or sqlite3, make sure that trac.db exists
614 # If adapter is sqlite or sqlite3, make sure that trac.db exists
615 raise "#{trac_db_path} doesn't exist!" if %w(sqlite sqlite3).include?(adapter) && !File.exist?(trac_db_path)
615 raise "#{trac_db_path} doesn't exist!" if %w(sqlite sqlite3).include?(adapter) && !File.exist?(trac_db_path)
616 @@trac_adapter = adapter
616 @@trac_adapter = adapter
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.set_trac_db_host(host)
622 def self.set_trac_db_host(host)
623 return nil if host.blank?
623 return nil if host.blank?
624 @@trac_db_host = host
624 @@trac_db_host = host
625 end
625 end
626
626
627 def self.set_trac_db_port(port)
627 def self.set_trac_db_port(port)
628 return nil if port.to_i == 0
628 return nil if port.to_i == 0
629 @@trac_db_port = port.to_i
629 @@trac_db_port = port.to_i
630 end
630 end
631
631
632 def self.set_trac_db_name(name)
632 def self.set_trac_db_name(name)
633 return nil if name.blank?
633 return nil if name.blank?
634 @@trac_db_name = name
634 @@trac_db_name = name
635 end
635 end
636
636
637 def self.set_trac_db_username(username)
637 def self.set_trac_db_username(username)
638 @@trac_db_username = username
638 @@trac_db_username = username
639 end
639 end
640
640
641 def self.set_trac_db_password(password)
641 def self.set_trac_db_password(password)
642 @@trac_db_password = password
642 @@trac_db_password = password
643 end
643 end
644
644
645 def self.set_trac_db_schema(schema)
645 def self.set_trac_db_schema(schema)
646 @@trac_db_schema = schema
646 @@trac_db_schema = schema
647 end
647 end
648
648
649 mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password
649 mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password
650
650
651 def self.trac_db_path; "#{trac_directory}/db/trac.db" end
651 def self.trac_db_path; "#{trac_directory}/db/trac.db" end
652 def self.trac_attachments_directory; "#{trac_directory}/attachments" end
652 def self.trac_attachments_directory; "#{trac_directory}/attachments" end
653
653
654 def self.target_project_identifier(identifier)
654 def self.target_project_identifier(identifier)
655 project = Project.find_by_identifier(identifier)
655 project = Project.find_by_identifier(identifier)
656 if !project
656 if !project
657 # create the target project
657 # create the target project
658 project = Project.new :name => identifier.humanize,
658 project = Project.new :name => identifier.humanize,
659 :description => ''
659 :description => ''
660 project.identifier = identifier
660 project.identifier = identifier
661 puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
661 puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
662 # enable issues and wiki for the created project
662 # enable issues and wiki for the created project
663 project.enabled_module_names = ['issue_tracking', 'wiki']
663 project.enabled_module_names = ['issue_tracking', 'wiki']
664 else
664 else
665 puts
665 puts
666 puts "This project already exists in your Redmine database."
666 puts "This project already exists in your Redmine database."
667 print "Are you sure you want to append data to this project ? [Y/n] "
667 print "Are you sure you want to append data to this project ? [Y/n] "
668 exit if STDIN.gets.match(/^n$/i)
668 exit if STDIN.gets.match(/^n$/i)
669 end
669 end
670 project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
670 project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
671 project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
671 project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
672 @target_project = project.new_record? ? nil : project
672 @target_project = project.new_record? ? nil : project
673 end
673 end
674
674
675 def self.connection_params
675 def self.connection_params
676 if %w(sqlite sqlite3).include?(trac_adapter)
676 if %w(sqlite sqlite3).include?(trac_adapter)
677 {:adapter => trac_adapter,
677 {:adapter => trac_adapter,
678 :database => trac_db_path}
678 :database => trac_db_path}
679 else
679 else
680 {:adapter => trac_adapter,
680 {:adapter => trac_adapter,
681 :database => trac_db_name,
681 :database => trac_db_name,
682 :host => trac_db_host,
682 :host => trac_db_host,
683 :port => trac_db_port,
683 :port => trac_db_port,
684 :username => trac_db_username,
684 :username => trac_db_username,
685 :password => trac_db_password,
685 :password => trac_db_password,
686 :schema_search_path => trac_db_schema
686 :schema_search_path => trac_db_schema
687 }
687 }
688 end
688 end
689 end
689 end
690
690
691 def self.establish_connection
691 def self.establish_connection
692 constants.each do |const|
692 constants.each do |const|
693 klass = const_get(const)
693 klass = const_get(const)
694 next unless klass.respond_to? 'establish_connection'
694 next unless klass.respond_to? 'establish_connection'
695 klass.establish_connection connection_params
695 klass.establish_connection connection_params
696 end
696 end
697 end
697 end
698
698
699 private
699 private
700 def self.encode(text)
700 def self.encode(text)
701 @ic.iconv text
701 @ic.iconv text
702 rescue
702 rescue
703 text
703 text
704 end
704 end
705 end
705 end
706
706
707 puts
707 puts
708 if Redmine::DefaultData::Loader.no_data?
708 if Redmine::DefaultData::Loader.no_data?
709 puts "Redmine configuration need to be loaded before importing data."
709 puts "Redmine configuration need to be loaded before importing data."
710 puts "Please, run this first:"
710 puts "Please, run this first:"
711 puts
711 puts
712 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
712 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
713 exit
713 exit
714 end
714 end
715
715
716 puts "WARNING: a new project will be added to Redmine during this process."
716 puts "WARNING: a new project will be added to Redmine during this process."
717 print "Are you sure you want to continue ? [y/N] "
717 print "Are you sure you want to continue ? [y/N] "
718 break unless STDIN.gets.match(/^y$/i)
718 break unless STDIN.gets.match(/^y$/i)
719 puts
719 puts
720
720
721 def prompt(text, options = {}, &block)
721 def prompt(text, options = {}, &block)
722 default = options[:default] || ''
722 default = options[:default] || ''
723 while true
723 while true
724 print "#{text} [#{default}]: "
724 print "#{text} [#{default}]: "
725 value = STDIN.gets.chomp!
725 value = STDIN.gets.chomp!
726 value = default if value.blank?
726 value = default if value.blank?
727 break if yield value
727 break if yield value
728 end
728 end
729 end
729 end
730
730
731 DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
731 DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
732
732
733 prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
733 prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
734 prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite') {|adapter| TracMigrate.set_trac_adapter adapter}
734 prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite') {|adapter| TracMigrate.set_trac_adapter adapter}
735 unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter)
735 unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter)
736 prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
736 prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
737 prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
737 prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
738 prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
738 prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
739 prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
739 prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
740 prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
740 prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
741 prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
741 prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
742 end
742 end
743 prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
743 prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
744 prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
744 prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
745 puts
745 puts
746
746
747 TracMigrate.migrate
747 TracMigrate.migrate
748 end
748 end
749 end
749 end
750
750
@@ -1,963 +1,963
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19 require 'issues_controller'
19 require 'issues_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class IssuesController; def rescue_action(e) raise e end; end
22 class IssuesController; def rescue_action(e) raise e end; end
23
23
24 class IssuesControllerTest < Test::Unit::TestCase
24 class IssuesControllerTest < Test::Unit::TestCase
25 fixtures :projects,
25 fixtures :projects,
26 :users,
26 :users,
27 :roles,
27 :roles,
28 :members,
28 :members,
29 :issues,
29 :issues,
30 :issue_statuses,
30 :issue_statuses,
31 :versions,
31 :versions,
32 :trackers,
32 :trackers,
33 :projects_trackers,
33 :projects_trackers,
34 :issue_categories,
34 :issue_categories,
35 :enabled_modules,
35 :enabled_modules,
36 :enumerations,
36 :enumerations,
37 :attachments,
37 :attachments,
38 :workflows,
38 :workflows,
39 :custom_fields,
39 :custom_fields,
40 :custom_values,
40 :custom_values,
41 :custom_fields_trackers,
41 :custom_fields_trackers,
42 :time_entries,
42 :time_entries,
43 :journals,
43 :journals,
44 :journal_details
44 :journal_details
45
45
46 def setup
46 def setup
47 @controller = IssuesController.new
47 @controller = IssuesController.new
48 @request = ActionController::TestRequest.new
48 @request = ActionController::TestRequest.new
49 @response = ActionController::TestResponse.new
49 @response = ActionController::TestResponse.new
50 User.current = nil
50 User.current = nil
51 end
51 end
52
52
53 def test_index_routing
53 def test_index_routing
54 assert_routing(
54 assert_routing(
55 {:method => :get, :path => '/issues'},
55 {:method => :get, :path => '/issues'},
56 :controller => 'issues', :action => 'index'
56 :controller => 'issues', :action => 'index'
57 )
57 )
58 end
58 end
59
59
60 def test_index
60 def test_index
61 get :index
61 get :index
62 assert_response :success
62 assert_response :success
63 assert_template 'index.rhtml'
63 assert_template 'index.rhtml'
64 assert_not_nil assigns(:issues)
64 assert_not_nil assigns(:issues)
65 assert_nil assigns(:project)
65 assert_nil assigns(:project)
66 assert_tag :tag => 'a', :content => /Can't print recipes/
66 assert_tag :tag => 'a', :content => /Can't print recipes/
67 assert_tag :tag => 'a', :content => /Subproject issue/
67 assert_tag :tag => 'a', :content => /Subproject issue/
68 # private projects hidden
68 # private projects hidden
69 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
69 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
70 assert_no_tag :tag => 'a', :content => /Issue on project 2/
70 assert_no_tag :tag => 'a', :content => /Issue on project 2/
71 end
71 end
72
72
73 def test_index_should_not_list_issues_when_module_disabled
73 def test_index_should_not_list_issues_when_module_disabled
74 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
74 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
75 get :index
75 get :index
76 assert_response :success
76 assert_response :success
77 assert_template 'index.rhtml'
77 assert_template 'index.rhtml'
78 assert_not_nil assigns(:issues)
78 assert_not_nil assigns(:issues)
79 assert_nil assigns(:project)
79 assert_nil assigns(:project)
80 assert_no_tag :tag => 'a', :content => /Can't print recipes/
80 assert_no_tag :tag => 'a', :content => /Can't print recipes/
81 assert_tag :tag => 'a', :content => /Subproject issue/
81 assert_tag :tag => 'a', :content => /Subproject issue/
82 end
82 end
83
83
84 def test_index_with_project_routing
84 def test_index_with_project_routing
85 assert_routing(
85 assert_routing(
86 {:method => :get, :path => '/projects/23/issues'},
86 {:method => :get, :path => '/projects/23/issues'},
87 :controller => 'issues', :action => 'index', :project_id => '23'
87 :controller => 'issues', :action => 'index', :project_id => '23'
88 )
88 )
89 end
89 end
90
90
91 def test_index_should_not_list_issues_when_module_disabled
91 def test_index_should_not_list_issues_when_module_disabled
92 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
92 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
93 get :index
93 get :index
94 assert_response :success
94 assert_response :success
95 assert_template 'index.rhtml'
95 assert_template 'index.rhtml'
96 assert_not_nil assigns(:issues)
96 assert_not_nil assigns(:issues)
97 assert_nil assigns(:project)
97 assert_nil assigns(:project)
98 assert_no_tag :tag => 'a', :content => /Can't print recipes/
98 assert_no_tag :tag => 'a', :content => /Can't print recipes/
99 assert_tag :tag => 'a', :content => /Subproject issue/
99 assert_tag :tag => 'a', :content => /Subproject issue/
100 end
100 end
101
101
102 def test_index_with_project_routing
102 def test_index_with_project_routing
103 assert_routing(
103 assert_routing(
104 {:method => :get, :path => 'projects/23/issues'},
104 {:method => :get, :path => 'projects/23/issues'},
105 :controller => 'issues', :action => 'index', :project_id => '23'
105 :controller => 'issues', :action => 'index', :project_id => '23'
106 )
106 )
107 end
107 end
108
108
109 def test_index_with_project
109 def test_index_with_project
110 Setting.display_subprojects_issues = 0
110 Setting.display_subprojects_issues = 0
111 get :index, :project_id => 1
111 get :index, :project_id => 1
112 assert_response :success
112 assert_response :success
113 assert_template 'index.rhtml'
113 assert_template 'index.rhtml'
114 assert_not_nil assigns(:issues)
114 assert_not_nil assigns(:issues)
115 assert_tag :tag => 'a', :content => /Can't print recipes/
115 assert_tag :tag => 'a', :content => /Can't print recipes/
116 assert_no_tag :tag => 'a', :content => /Subproject issue/
116 assert_no_tag :tag => 'a', :content => /Subproject issue/
117 end
117 end
118
118
119 def test_index_with_project_and_subprojects
119 def test_index_with_project_and_subprojects
120 Setting.display_subprojects_issues = 1
120 Setting.display_subprojects_issues = 1
121 get :index, :project_id => 1
121 get :index, :project_id => 1
122 assert_response :success
122 assert_response :success
123 assert_template 'index.rhtml'
123 assert_template 'index.rhtml'
124 assert_not_nil assigns(:issues)
124 assert_not_nil assigns(:issues)
125 assert_tag :tag => 'a', :content => /Can't print recipes/
125 assert_tag :tag => 'a', :content => /Can't print recipes/
126 assert_tag :tag => 'a', :content => /Subproject issue/
126 assert_tag :tag => 'a', :content => /Subproject issue/
127 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
127 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
128 end
128 end
129
129
130 def test_index_with_project_and_subprojects_should_show_private_subprojects
130 def test_index_with_project_and_subprojects_should_show_private_subprojects
131 @request.session[:user_id] = 2
131 @request.session[:user_id] = 2
132 Setting.display_subprojects_issues = 1
132 Setting.display_subprojects_issues = 1
133 get :index, :project_id => 1
133 get :index, :project_id => 1
134 assert_response :success
134 assert_response :success
135 assert_template 'index.rhtml'
135 assert_template 'index.rhtml'
136 assert_not_nil assigns(:issues)
136 assert_not_nil assigns(:issues)
137 assert_tag :tag => 'a', :content => /Can't print recipes/
137 assert_tag :tag => 'a', :content => /Can't print recipes/
138 assert_tag :tag => 'a', :content => /Subproject issue/
138 assert_tag :tag => 'a', :content => /Subproject issue/
139 assert_tag :tag => 'a', :content => /Issue of a private subproject/
139 assert_tag :tag => 'a', :content => /Issue of a private subproject/
140 end
140 end
141
141
142 def test_index_with_project_routing_formatted
142 def test_index_with_project_routing_formatted
143 assert_routing(
143 assert_routing(
144 {:method => :get, :path => 'projects/23/issues.pdf'},
144 {:method => :get, :path => 'projects/23/issues.pdf'},
145 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'pdf'
145 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'pdf'
146 )
146 )
147 assert_routing(
147 assert_routing(
148 {:method => :get, :path => 'projects/23/issues.atom'},
148 {:method => :get, :path => 'projects/23/issues.atom'},
149 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'atom'
149 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'atom'
150 )
150 )
151 end
151 end
152
152
153 def test_index_with_project_and_filter
153 def test_index_with_project_and_filter
154 get :index, :project_id => 1, :set_filter => 1
154 get :index, :project_id => 1, :set_filter => 1
155 assert_response :success
155 assert_response :success
156 assert_template 'index.rhtml'
156 assert_template 'index.rhtml'
157 assert_not_nil assigns(:issues)
157 assert_not_nil assigns(:issues)
158 end
158 end
159
159
160 def test_index_csv_with_project
160 def test_index_csv_with_project
161 get :index, :format => 'csv'
161 get :index, :format => 'csv'
162 assert_response :success
162 assert_response :success
163 assert_not_nil assigns(:issues)
163 assert_not_nil assigns(:issues)
164 assert_equal 'text/csv', @response.content_type
164 assert_equal 'text/csv', @response.content_type
165
165
166 get :index, :project_id => 1, :format => 'csv'
166 get :index, :project_id => 1, :format => 'csv'
167 assert_response :success
167 assert_response :success
168 assert_not_nil assigns(:issues)
168 assert_not_nil assigns(:issues)
169 assert_equal 'text/csv', @response.content_type
169 assert_equal 'text/csv', @response.content_type
170 end
170 end
171
171
172 def test_index_formatted
172 def test_index_formatted
173 assert_routing(
173 assert_routing(
174 {:method => :get, :path => 'issues.pdf'},
174 {:method => :get, :path => 'issues.pdf'},
175 :controller => 'issues', :action => 'index', :format => 'pdf'
175 :controller => 'issues', :action => 'index', :format => 'pdf'
176 )
176 )
177 assert_routing(
177 assert_routing(
178 {:method => :get, :path => 'issues.atom'},
178 {:method => :get, :path => 'issues.atom'},
179 :controller => 'issues', :action => 'index', :format => 'atom'
179 :controller => 'issues', :action => 'index', :format => 'atom'
180 )
180 )
181 end
181 end
182
182
183 def test_index_pdf
183 def test_index_pdf
184 get :index, :format => 'pdf'
184 get :index, :format => 'pdf'
185 assert_response :success
185 assert_response :success
186 assert_not_nil assigns(:issues)
186 assert_not_nil assigns(:issues)
187 assert_equal 'application/pdf', @response.content_type
187 assert_equal 'application/pdf', @response.content_type
188
188
189 get :index, :project_id => 1, :format => 'pdf'
189 get :index, :project_id => 1, :format => 'pdf'
190 assert_response :success
190 assert_response :success
191 assert_not_nil assigns(:issues)
191 assert_not_nil assigns(:issues)
192 assert_equal 'application/pdf', @response.content_type
192 assert_equal 'application/pdf', @response.content_type
193 end
193 end
194
194
195 def test_index_sort
195 def test_index_sort
196 get :index, :sort_key => 'tracker'
196 get :index, :sort_key => 'tracker'
197 assert_response :success
197 assert_response :success
198
198
199 sort_params = @request.session['issuesindex_sort']
199 sort_params = @request.session['issuesindex_sort']
200 assert sort_params.is_a?(Hash)
200 assert sort_params.is_a?(Hash)
201 assert_equal 'tracker', sort_params[:key]
201 assert_equal 'tracker', sort_params[:key]
202 assert_equal 'ASC', sort_params[:order]
202 assert_equal 'ASC', sort_params[:order]
203 end
203 end
204
204
205 def test_gantt
205 def test_gantt
206 get :gantt, :project_id => 1
206 get :gantt, :project_id => 1
207 assert_response :success
207 assert_response :success
208 assert_template 'gantt.rhtml'
208 assert_template 'gantt.rhtml'
209 assert_not_nil assigns(:gantt)
209 assert_not_nil assigns(:gantt)
210 events = assigns(:gantt).events
210 events = assigns(:gantt).events
211 assert_not_nil events
211 assert_not_nil events
212 # Issue with start and due dates
212 # Issue with start and due dates
213 i = Issue.find(1)
213 i = Issue.find(1)
214 assert_not_nil i.due_date
214 assert_not_nil i.due_date
215 assert events.include?(Issue.find(1))
215 assert events.include?(Issue.find(1))
216 # Issue with without due date but targeted to a version with date
216 # Issue with without due date but targeted to a version with date
217 i = Issue.find(2)
217 i = Issue.find(2)
218 assert_nil i.due_date
218 assert_nil i.due_date
219 assert events.include?(i)
219 assert events.include?(i)
220 end
220 end
221
221
222 def test_cross_project_gantt
222 def test_cross_project_gantt
223 get :gantt
223 get :gantt
224 assert_response :success
224 assert_response :success
225 assert_template 'gantt.rhtml'
225 assert_template 'gantt.rhtml'
226 assert_not_nil assigns(:gantt)
226 assert_not_nil assigns(:gantt)
227 events = assigns(:gantt).events
227 events = assigns(:gantt).events
228 assert_not_nil events
228 assert_not_nil events
229 end
229 end
230
230
231 def test_gantt_export_to_pdf
231 def test_gantt_export_to_pdf
232 get :gantt, :project_id => 1, :format => 'pdf'
232 get :gantt, :project_id => 1, :format => 'pdf'
233 assert_response :success
233 assert_response :success
234 assert_equal 'application/pdf', @response.content_type
234 assert_equal 'application/pdf', @response.content_type
235 assert @response.body.starts_with?('%PDF')
235 assert @response.body.starts_with?('%PDF')
236 assert_not_nil assigns(:gantt)
236 assert_not_nil assigns(:gantt)
237 end
237 end
238
238
239 def test_cross_project_gantt_export_to_pdf
239 def test_cross_project_gantt_export_to_pdf
240 get :gantt, :format => 'pdf'
240 get :gantt, :format => 'pdf'
241 assert_response :success
241 assert_response :success
242 assert_equal 'application/pdf', @response.content_type
242 assert_equal 'application/pdf', @response.content_type
243 assert @response.body.starts_with?('%PDF')
243 assert @response.body.starts_with?('%PDF')
244 assert_not_nil assigns(:gantt)
244 assert_not_nil assigns(:gantt)
245 end
245 end
246
246
247 if Object.const_defined?(:Magick)
247 if Object.const_defined?(:Magick)
248 def test_gantt_image
248 def test_gantt_image
249 get :gantt, :project_id => 1, :format => 'png'
249 get :gantt, :project_id => 1, :format => 'png'
250 assert_response :success
250 assert_response :success
251 assert_equal 'image/png', @response.content_type
251 assert_equal 'image/png', @response.content_type
252 end
252 end
253 else
253 else
254 puts "RMagick not installed. Skipping tests !!!"
254 puts "RMagick not installed. Skipping tests !!!"
255 end
255 end
256
256
257 def test_calendar
257 def test_calendar
258 get :calendar, :project_id => 1
258 get :calendar, :project_id => 1
259 assert_response :success
259 assert_response :success
260 assert_template 'calendar'
260 assert_template 'calendar'
261 assert_not_nil assigns(:calendar)
261 assert_not_nil assigns(:calendar)
262 end
262 end
263
263
264 def test_cross_project_calendar
264 def test_cross_project_calendar
265 get :calendar
265 get :calendar
266 assert_response :success
266 assert_response :success
267 assert_template 'calendar'
267 assert_template 'calendar'
268 assert_not_nil assigns(:calendar)
268 assert_not_nil assigns(:calendar)
269 end
269 end
270
270
271 def test_changes
271 def test_changes
272 get :changes, :project_id => 1
272 get :changes, :project_id => 1
273 assert_response :success
273 assert_response :success
274 assert_not_nil assigns(:journals)
274 assert_not_nil assigns(:journals)
275 assert_equal 'application/atom+xml', @response.content_type
275 assert_equal 'application/atom+xml', @response.content_type
276 end
276 end
277
277
278 def test_show_routing
278 def test_show_routing
279 assert_routing(
279 assert_routing(
280 {:method => :get, :path => '/issues/64'},
280 {:method => :get, :path => '/issues/64'},
281 :controller => 'issues', :action => 'show', :id => '64'
281 :controller => 'issues', :action => 'show', :id => '64'
282 )
282 )
283 end
283 end
284
284
285 def test_show_routing_formatted
285 def test_show_routing_formatted
286 assert_routing(
286 assert_routing(
287 {:method => :get, :path => '/issues/2332.pdf'},
287 {:method => :get, :path => '/issues/2332.pdf'},
288 :controller => 'issues', :action => 'show', :id => '2332', :format => 'pdf'
288 :controller => 'issues', :action => 'show', :id => '2332', :format => 'pdf'
289 )
289 )
290 assert_routing(
290 assert_routing(
291 {:method => :get, :path => '/issues/23123.atom'},
291 {:method => :get, :path => '/issues/23123.atom'},
292 :controller => 'issues', :action => 'show', :id => '23123', :format => 'atom'
292 :controller => 'issues', :action => 'show', :id => '23123', :format => 'atom'
293 )
293 )
294 end
294 end
295
295
296 def test_show_by_anonymous
296 def test_show_by_anonymous
297 get :show, :id => 1
297 get :show, :id => 1
298 assert_response :success
298 assert_response :success
299 assert_template 'show.rhtml'
299 assert_template 'show.rhtml'
300 assert_not_nil assigns(:issue)
300 assert_not_nil assigns(:issue)
301 assert_equal Issue.find(1), assigns(:issue)
301 assert_equal Issue.find(1), assigns(:issue)
302
302
303 # anonymous role is allowed to add a note
303 # anonymous role is allowed to add a note
304 assert_tag :tag => 'form',
304 assert_tag :tag => 'form',
305 :descendant => { :tag => 'fieldset',
305 :descendant => { :tag => 'fieldset',
306 :child => { :tag => 'legend',
306 :child => { :tag => 'legend',
307 :content => /Notes/ } }
307 :content => /Notes/ } }
308 end
308 end
309
309
310 def test_show_by_manager
310 def test_show_by_manager
311 @request.session[:user_id] = 2
311 @request.session[:user_id] = 2
312 get :show, :id => 1
312 get :show, :id => 1
313 assert_response :success
313 assert_response :success
314
314
315 assert_tag :tag => 'form',
315 assert_tag :tag => 'form',
316 :descendant => { :tag => 'fieldset',
316 :descendant => { :tag => 'fieldset',
317 :child => { :tag => 'legend',
317 :child => { :tag => 'legend',
318 :content => /Change properties/ } },
318 :content => /Change properties/ } },
319 :descendant => { :tag => 'fieldset',
319 :descendant => { :tag => 'fieldset',
320 :child => { :tag => 'legend',
320 :child => { :tag => 'legend',
321 :content => /Log time/ } },
321 :content => /Log time/ } },
322 :descendant => { :tag => 'fieldset',
322 :descendant => { :tag => 'fieldset',
323 :child => { :tag => 'legend',
323 :child => { :tag => 'legend',
324 :content => /Notes/ } }
324 :content => /Notes/ } }
325 end
325 end
326
326
327 def test_show_should_not_disclose_relations_to_invisible_issues
327 def test_show_should_not_disclose_relations_to_invisible_issues
328 Setting.cross_project_issue_relations = '1'
328 Setting.cross_project_issue_relations = '1'
329 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
329 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
330 # Relation to a private project issue
330 # Relation to a private project issue
331 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
331 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
332
332
333 get :show, :id => 1
333 get :show, :id => 1
334 assert_response :success
334 assert_response :success
335
335
336 assert_tag :div, :attributes => { :id => 'relations' },
336 assert_tag :div, :attributes => { :id => 'relations' },
337 :descendant => { :tag => 'a', :content => /#2$/ }
337 :descendant => { :tag => 'a', :content => /#2$/ }
338 assert_no_tag :div, :attributes => { :id => 'relations' },
338 assert_no_tag :div, :attributes => { :id => 'relations' },
339 :descendant => { :tag => 'a', :content => /#4$/ }
339 :descendant => { :tag => 'a', :content => /#4$/ }
340 end
340 end
341
341
342 def test_new_routing
342 def test_new_routing
343 assert_routing(
343 assert_routing(
344 {:method => :get, :path => '/projects/1/issues/new'},
344 {:method => :get, :path => '/projects/1/issues/new'},
345 :controller => 'issues', :action => 'new', :project_id => '1'
345 :controller => 'issues', :action => 'new', :project_id => '1'
346 )
346 )
347 assert_recognizes(
347 assert_recognizes(
348 {:controller => 'issues', :action => 'new', :project_id => '1'},
348 {:controller => 'issues', :action => 'new', :project_id => '1'},
349 {:method => :post, :path => '/projects/1/issues'}
349 {:method => :post, :path => '/projects/1/issues'}
350 )
350 )
351 end
351 end
352
352
353 def test_show_export_to_pdf
353 def test_show_export_to_pdf
354 get :show, :id => 3, :format => 'pdf'
354 get :show, :id => 3, :format => 'pdf'
355 assert_response :success
355 assert_response :success
356 assert_equal 'application/pdf', @response.content_type
356 assert_equal 'application/pdf', @response.content_type
357 assert @response.body.starts_with?('%PDF')
357 assert @response.body.starts_with?('%PDF')
358 assert_not_nil assigns(:issue)
358 assert_not_nil assigns(:issue)
359 end
359 end
360
360
361 def test_get_new
361 def test_get_new
362 @request.session[:user_id] = 2
362 @request.session[:user_id] = 2
363 get :new, :project_id => 1, :tracker_id => 1
363 get :new, :project_id => 1, :tracker_id => 1
364 assert_response :success
364 assert_response :success
365 assert_template 'new'
365 assert_template 'new'
366
366
367 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
367 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
368 :value => 'Default string' }
368 :value => 'Default string' }
369 end
369 end
370
370
371 def test_get_new_without_tracker_id
371 def test_get_new_without_tracker_id
372 @request.session[:user_id] = 2
372 @request.session[:user_id] = 2
373 get :new, :project_id => 1
373 get :new, :project_id => 1
374 assert_response :success
374 assert_response :success
375 assert_template 'new'
375 assert_template 'new'
376
376
377 issue = assigns(:issue)
377 issue = assigns(:issue)
378 assert_not_nil issue
378 assert_not_nil issue
379 assert_equal Project.find(1).trackers.first, issue.tracker
379 assert_equal Project.find(1).trackers.first, issue.tracker
380 end
380 end
381
381
382 def test_update_new_form
382 def test_update_new_form
383 @request.session[:user_id] = 2
383 @request.session[:user_id] = 2
384 xhr :post, :new, :project_id => 1,
384 xhr :post, :new, :project_id => 1,
385 :issue => {:tracker_id => 2,
385 :issue => {:tracker_id => 2,
386 :subject => 'This is the test_new issue',
386 :subject => 'This is the test_new issue',
387 :description => 'This is the description',
387 :description => 'This is the description',
388 :priority_id => 5}
388 :priority_id => 5}
389 assert_response :success
389 assert_response :success
390 assert_template 'new'
390 assert_template 'new'
391 end
391 end
392
392
393 def test_post_new
393 def test_post_new
394 @request.session[:user_id] = 2
394 @request.session[:user_id] = 2
395 post :new, :project_id => 1,
395 post :new, :project_id => 1,
396 :issue => {:tracker_id => 3,
396 :issue => {:tracker_id => 3,
397 :subject => 'This is the test_new issue',
397 :subject => 'This is the test_new issue',
398 :description => 'This is the description',
398 :description => 'This is the description',
399 :priority_id => 5,
399 :priority_id => 5,
400 :estimated_hours => '',
400 :estimated_hours => '',
401 :custom_field_values => {'2' => 'Value for field 2'}}
401 :custom_field_values => {'2' => 'Value for field 2'}}
402 assert_redirected_to :action => 'show'
402 assert_redirected_to :action => 'show'
403
403
404 issue = Issue.find_by_subject('This is the test_new issue')
404 issue = Issue.find_by_subject('This is the test_new issue')
405 assert_not_nil issue
405 assert_not_nil issue
406 assert_equal 2, issue.author_id
406 assert_equal 2, issue.author_id
407 assert_equal 3, issue.tracker_id
407 assert_equal 3, issue.tracker_id
408 assert_nil issue.estimated_hours
408 assert_nil issue.estimated_hours
409 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
409 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
410 assert_not_nil v
410 assert_not_nil v
411 assert_equal 'Value for field 2', v.value
411 assert_equal 'Value for field 2', v.value
412 end
412 end
413
413
414 def test_post_new_and_continue
414 def test_post_new_and_continue
415 @request.session[:user_id] = 2
415 @request.session[:user_id] = 2
416 post :new, :project_id => 1,
416 post :new, :project_id => 1,
417 :issue => {:tracker_id => 3,
417 :issue => {:tracker_id => 3,
418 :subject => 'This is first issue',
418 :subject => 'This is first issue',
419 :priority_id => 5},
419 :priority_id => 5},
420 :continue => ''
420 :continue => ''
421 assert_redirected_to :controller => 'issues', :action => 'new', :tracker_id => 3
421 assert_redirected_to :controller => 'issues', :action => 'new', :tracker_id => 3
422 end
422 end
423
423
424 def test_post_new_without_custom_fields_param
424 def test_post_new_without_custom_fields_param
425 @request.session[:user_id] = 2
425 @request.session[:user_id] = 2
426 post :new, :project_id => 1,
426 post :new, :project_id => 1,
427 :issue => {:tracker_id => 1,
427 :issue => {:tracker_id => 1,
428 :subject => 'This is the test_new issue',
428 :subject => 'This is the test_new issue',
429 :description => 'This is the description',
429 :description => 'This is the description',
430 :priority_id => 5}
430 :priority_id => 5}
431 assert_redirected_to :action => 'show'
431 assert_redirected_to :action => 'show'
432 end
432 end
433
433
434 def test_post_new_with_required_custom_field_and_without_custom_fields_param
434 def test_post_new_with_required_custom_field_and_without_custom_fields_param
435 field = IssueCustomField.find_by_name('Database')
435 field = IssueCustomField.find_by_name('Database')
436 field.update_attribute(:is_required, true)
436 field.update_attribute(:is_required, true)
437
437
438 @request.session[:user_id] = 2
438 @request.session[:user_id] = 2
439 post :new, :project_id => 1,
439 post :new, :project_id => 1,
440 :issue => {:tracker_id => 1,
440 :issue => {:tracker_id => 1,
441 :subject => 'This is the test_new issue',
441 :subject => 'This is the test_new issue',
442 :description => 'This is the description',
442 :description => 'This is the description',
443 :priority_id => 5}
443 :priority_id => 5}
444 assert_response :success
444 assert_response :success
445 assert_template 'new'
445 assert_template 'new'
446 issue = assigns(:issue)
446 issue = assigns(:issue)
447 assert_not_nil issue
447 assert_not_nil issue
448 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
448 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
449 end
449 end
450
450
451 def test_post_new_with_watchers
451 def test_post_new_with_watchers
452 @request.session[:user_id] = 2
452 @request.session[:user_id] = 2
453 ActionMailer::Base.deliveries.clear
453 ActionMailer::Base.deliveries.clear
454
454
455 assert_difference 'Watcher.count', 2 do
455 assert_difference 'Watcher.count', 2 do
456 post :new, :project_id => 1,
456 post :new, :project_id => 1,
457 :issue => {:tracker_id => 1,
457 :issue => {:tracker_id => 1,
458 :subject => 'This is a new issue with watchers',
458 :subject => 'This is a new issue with watchers',
459 :description => 'This is the description',
459 :description => 'This is the description',
460 :priority_id => 5,
460 :priority_id => 5,
461 :watcher_user_ids => ['2', '3']}
461 :watcher_user_ids => ['2', '3']}
462 end
462 end
463 issue = Issue.find_by_subject('This is a new issue with watchers')
463 issue = Issue.find_by_subject('This is a new issue with watchers')
464 assert_not_nil issue
464 assert_not_nil issue
465 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
465 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
466
466
467 # Watchers added
467 # Watchers added
468 assert_equal [2, 3], issue.watcher_user_ids.sort
468 assert_equal [2, 3], issue.watcher_user_ids.sort
469 assert issue.watched_by?(User.find(3))
469 assert issue.watched_by?(User.find(3))
470 # Watchers notified
470 # Watchers notified
471 mail = ActionMailer::Base.deliveries.last
471 mail = ActionMailer::Base.deliveries.last
472 assert_kind_of TMail::Mail, mail
472 assert_kind_of TMail::Mail, mail
473 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
473 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
474 end
474 end
475
475
476 def test_post_should_preserve_fields_values_on_validation_failure
476 def test_post_should_preserve_fields_values_on_validation_failure
477 @request.session[:user_id] = 2
477 @request.session[:user_id] = 2
478 post :new, :project_id => 1,
478 post :new, :project_id => 1,
479 :issue => {:tracker_id => 1,
479 :issue => {:tracker_id => 1,
480 # empty subject
480 # empty subject
481 :subject => '',
481 :subject => '',
482 :description => 'This is a description',
482 :description => 'This is a description',
483 :priority_id => 6,
483 :priority_id => 6,
484 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
484 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
485 assert_response :success
485 assert_response :success
486 assert_template 'new'
486 assert_template 'new'
487
487
488 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
488 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
489 :content => 'This is a description'
489 :content => 'This is a description'
490 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
490 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
491 :child => { :tag => 'option', :attributes => { :selected => 'selected',
491 :child => { :tag => 'option', :attributes => { :selected => 'selected',
492 :value => '6' },
492 :value => '6' },
493 :content => 'High' }
493 :content => 'High' }
494 # Custom fields
494 # Custom fields
495 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
495 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
496 :child => { :tag => 'option', :attributes => { :selected => 'selected',
496 :child => { :tag => 'option', :attributes => { :selected => 'selected',
497 :value => 'Oracle' },
497 :value => 'Oracle' },
498 :content => 'Oracle' }
498 :content => 'Oracle' }
499 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
499 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
500 :value => 'Value for field 2'}
500 :value => 'Value for field 2'}
501 end
501 end
502
502
503 def test_copy_routing
503 def test_copy_routing
504 assert_routing(
504 assert_routing(
505 {:method => :get, :path => '/projects/world_domination/issues/567/copy'},
505 {:method => :get, :path => '/projects/world_domination/issues/567/copy'},
506 :controller => 'issues', :action => 'new', :project_id => 'world_domination', :copy_from => '567'
506 :controller => 'issues', :action => 'new', :project_id => 'world_domination', :copy_from => '567'
507 )
507 )
508 end
508 end
509
509
510 def test_copy_issue
510 def test_copy_issue
511 @request.session[:user_id] = 2
511 @request.session[:user_id] = 2
512 get :new, :project_id => 1, :copy_from => 1
512 get :new, :project_id => 1, :copy_from => 1
513 assert_template 'new'
513 assert_template 'new'
514 assert_not_nil assigns(:issue)
514 assert_not_nil assigns(:issue)
515 orig = Issue.find(1)
515 orig = Issue.find(1)
516 assert_equal orig.subject, assigns(:issue).subject
516 assert_equal orig.subject, assigns(:issue).subject
517 end
517 end
518
518
519 def test_edit_routing
519 def test_edit_routing
520 assert_routing(
520 assert_routing(
521 {:method => :get, :path => '/issues/1/edit'},
521 {:method => :get, :path => '/issues/1/edit'},
522 :controller => 'issues', :action => 'edit', :id => '1'
522 :controller => 'issues', :action => 'edit', :id => '1'
523 )
523 )
524 assert_recognizes( #TODO: use a PUT on the issue URI isntead, need to adjust form
524 assert_recognizes( #TODO: use a PUT on the issue URI isntead, need to adjust form
525 {:controller => 'issues', :action => 'edit', :id => '1'},
525 {:controller => 'issues', :action => 'edit', :id => '1'},
526 {:method => :post, :path => '/issues/1/edit'}
526 {:method => :post, :path => '/issues/1/edit'}
527 )
527 )
528 end
528 end
529
529
530 def test_get_edit
530 def test_get_edit
531 @request.session[:user_id] = 2
531 @request.session[:user_id] = 2
532 get :edit, :id => 1
532 get :edit, :id => 1
533 assert_response :success
533 assert_response :success
534 assert_template 'edit'
534 assert_template 'edit'
535 assert_not_nil assigns(:issue)
535 assert_not_nil assigns(:issue)
536 assert_equal Issue.find(1), assigns(:issue)
536 assert_equal Issue.find(1), assigns(:issue)
537 end
537 end
538
538
539 def test_get_edit_with_params
539 def test_get_edit_with_params
540 @request.session[:user_id] = 2
540 @request.session[:user_id] = 2
541 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
541 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
542 assert_response :success
542 assert_response :success
543 assert_template 'edit'
543 assert_template 'edit'
544
544
545 issue = assigns(:issue)
545 issue = assigns(:issue)
546 assert_not_nil issue
546 assert_not_nil issue
547
547
548 assert_equal 5, issue.status_id
548 assert_equal 5, issue.status_id
549 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
549 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
550 :child => { :tag => 'option',
550 :child => { :tag => 'option',
551 :content => 'Closed',
551 :content => 'Closed',
552 :attributes => { :selected => 'selected' } }
552 :attributes => { :selected => 'selected' } }
553
553
554 assert_equal 7, issue.priority_id
554 assert_equal 7, issue.priority_id
555 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
555 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
556 :child => { :tag => 'option',
556 :child => { :tag => 'option',
557 :content => 'Urgent',
557 :content => 'Urgent',
558 :attributes => { :selected => 'selected' } }
558 :attributes => { :selected => 'selected' } }
559 end
559 end
560
560
561 def test_reply_routing
561 def test_reply_routing
562 assert_routing(
562 assert_routing(
563 {:method => :post, :path => '/issues/1/quoted'},
563 {:method => :post, :path => '/issues/1/quoted'},
564 :controller => 'issues', :action => 'reply', :id => '1'
564 :controller => 'issues', :action => 'reply', :id => '1'
565 )
565 )
566 end
566 end
567
567
568 def test_reply_to_issue
568 def test_reply_to_issue
569 @request.session[:user_id] = 2
569 @request.session[:user_id] = 2
570 get :reply, :id => 1
570 get :reply, :id => 1
571 assert_response :success
571 assert_response :success
572 assert_select_rjs :show, "update"
572 assert_select_rjs :show, "update"
573 end
573 end
574
574
575 def test_reply_to_note
575 def test_reply_to_note
576 @request.session[:user_id] = 2
576 @request.session[:user_id] = 2
577 get :reply, :id => 1, :journal_id => 2
577 get :reply, :id => 1, :journal_id => 2
578 assert_response :success
578 assert_response :success
579 assert_select_rjs :show, "update"
579 assert_select_rjs :show, "update"
580 end
580 end
581
581
582 def test_post_edit_without_custom_fields_param
582 def test_post_edit_without_custom_fields_param
583 @request.session[:user_id] = 2
583 @request.session[:user_id] = 2
584 ActionMailer::Base.deliveries.clear
584 ActionMailer::Base.deliveries.clear
585
585
586 issue = Issue.find(1)
586 issue = Issue.find(1)
587 assert_equal '125', issue.custom_value_for(2).value
587 assert_equal '125', issue.custom_value_for(2).value
588 old_subject = issue.subject
588 old_subject = issue.subject
589 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
589 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
590
590
591 assert_difference('Journal.count') do
591 assert_difference('Journal.count') do
592 assert_difference('JournalDetail.count', 2) do
592 assert_difference('JournalDetail.count', 2) do
593 post :edit, :id => 1, :issue => {:subject => new_subject,
593 post :edit, :id => 1, :issue => {:subject => new_subject,
594 :priority_id => '6',
594 :priority_id => '6',
595 :category_id => '1' # no change
595 :category_id => '1' # no change
596 }
596 }
597 end
597 end
598 end
598 end
599 assert_redirected_to :action => 'show', :id => '1'
599 assert_redirected_to :action => 'show', :id => '1'
600 issue.reload
600 issue.reload
601 assert_equal new_subject, issue.subject
601 assert_equal new_subject, issue.subject
602 # Make sure custom fields were not cleared
602 # Make sure custom fields were not cleared
603 assert_equal '125', issue.custom_value_for(2).value
603 assert_equal '125', issue.custom_value_for(2).value
604
604
605 mail = ActionMailer::Base.deliveries.last
605 mail = ActionMailer::Base.deliveries.last
606 assert_kind_of TMail::Mail, mail
606 assert_kind_of TMail::Mail, mail
607 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
607 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
608 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
608 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
609 end
609 end
610
610
611 def test_post_edit_with_custom_field_change
611 def test_post_edit_with_custom_field_change
612 @request.session[:user_id] = 2
612 @request.session[:user_id] = 2
613 issue = Issue.find(1)
613 issue = Issue.find(1)
614 assert_equal '125', issue.custom_value_for(2).value
614 assert_equal '125', issue.custom_value_for(2).value
615
615
616 assert_difference('Journal.count') do
616 assert_difference('Journal.count') do
617 assert_difference('JournalDetail.count', 3) do
617 assert_difference('JournalDetail.count', 3) do
618 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
618 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
619 :priority_id => '6',
619 :priority_id => '6',
620 :category_id => '1', # no change
620 :category_id => '1', # no change
621 :custom_field_values => { '2' => 'New custom value' }
621 :custom_field_values => { '2' => 'New custom value' }
622 }
622 }
623 end
623 end
624 end
624 end
625 assert_redirected_to :action => 'show', :id => '1'
625 assert_redirected_to :action => 'show', :id => '1'
626 issue.reload
626 issue.reload
627 assert_equal 'New custom value', issue.custom_value_for(2).value
627 assert_equal 'New custom value', issue.custom_value_for(2).value
628
628
629 mail = ActionMailer::Base.deliveries.last
629 mail = ActionMailer::Base.deliveries.last
630 assert_kind_of TMail::Mail, mail
630 assert_kind_of TMail::Mail, mail
631 assert mail.body.include?("Searchable field changed from 125 to New custom value")
631 assert mail.body.include?("Searchable field changed from 125 to New custom value")
632 end
632 end
633
633
634 def test_post_edit_with_status_and_assignee_change
634 def test_post_edit_with_status_and_assignee_change
635 issue = Issue.find(1)
635 issue = Issue.find(1)
636 assert_equal 1, issue.status_id
636 assert_equal 1, issue.status_id
637 @request.session[:user_id] = 2
637 @request.session[:user_id] = 2
638 assert_difference('TimeEntry.count', 0) do
638 assert_difference('TimeEntry.count', 0) do
639 post :edit,
639 post :edit,
640 :id => 1,
640 :id => 1,
641 :issue => { :status_id => 2, :assigned_to_id => 3 },
641 :issue => { :status_id => 2, :assigned_to_id => 3 },
642 :notes => 'Assigned to dlopper',
642 :notes => 'Assigned to dlopper',
643 :time_entry => { :hours => '', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
643 :time_entry => { :hours => '', :comments => '', :activity_id => Enumeration.activities.first }
644 end
644 end
645 assert_redirected_to :action => 'show', :id => '1'
645 assert_redirected_to :action => 'show', :id => '1'
646 issue.reload
646 issue.reload
647 assert_equal 2, issue.status_id
647 assert_equal 2, issue.status_id
648 j = issue.journals.find(:first, :order => 'id DESC')
648 j = issue.journals.find(:first, :order => 'id DESC')
649 assert_equal 'Assigned to dlopper', j.notes
649 assert_equal 'Assigned to dlopper', j.notes
650 assert_equal 2, j.details.size
650 assert_equal 2, j.details.size
651
651
652 mail = ActionMailer::Base.deliveries.last
652 mail = ActionMailer::Base.deliveries.last
653 assert mail.body.include?("Status changed from New to Assigned")
653 assert mail.body.include?("Status changed from New to Assigned")
654 end
654 end
655
655
656 def test_post_edit_with_note_only
656 def test_post_edit_with_note_only
657 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
657 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
658 # anonymous user
658 # anonymous user
659 post :edit,
659 post :edit,
660 :id => 1,
660 :id => 1,
661 :notes => notes
661 :notes => notes
662 assert_redirected_to :action => 'show', :id => '1'
662 assert_redirected_to :action => 'show', :id => '1'
663 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
663 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
664 assert_equal notes, j.notes
664 assert_equal notes, j.notes
665 assert_equal 0, j.details.size
665 assert_equal 0, j.details.size
666 assert_equal User.anonymous, j.user
666 assert_equal User.anonymous, j.user
667
667
668 mail = ActionMailer::Base.deliveries.last
668 mail = ActionMailer::Base.deliveries.last
669 assert mail.body.include?(notes)
669 assert mail.body.include?(notes)
670 end
670 end
671
671
672 def test_post_edit_with_note_and_spent_time
672 def test_post_edit_with_note_and_spent_time
673 @request.session[:user_id] = 2
673 @request.session[:user_id] = 2
674 spent_hours_before = Issue.find(1).spent_hours
674 spent_hours_before = Issue.find(1).spent_hours
675 assert_difference('TimeEntry.count') do
675 assert_difference('TimeEntry.count') do
676 post :edit,
676 post :edit,
677 :id => 1,
677 :id => 1,
678 :notes => '2.5 hours added',
678 :notes => '2.5 hours added',
679 :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
679 :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.activities.first }
680 end
680 end
681 assert_redirected_to :action => 'show', :id => '1'
681 assert_redirected_to :action => 'show', :id => '1'
682
682
683 issue = Issue.find(1)
683 issue = Issue.find(1)
684
684
685 j = issue.journals.find(:first, :order => 'id DESC')
685 j = issue.journals.find(:first, :order => 'id DESC')
686 assert_equal '2.5 hours added', j.notes
686 assert_equal '2.5 hours added', j.notes
687 assert_equal 0, j.details.size
687 assert_equal 0, j.details.size
688
688
689 t = issue.time_entries.find(:first, :order => 'id DESC')
689 t = issue.time_entries.find(:first, :order => 'id DESC')
690 assert_not_nil t
690 assert_not_nil t
691 assert_equal 2.5, t.hours
691 assert_equal 2.5, t.hours
692 assert_equal spent_hours_before + 2.5, issue.spent_hours
692 assert_equal spent_hours_before + 2.5, issue.spent_hours
693 end
693 end
694
694
695 def test_post_edit_with_attachment_only
695 def test_post_edit_with_attachment_only
696 set_tmp_attachments_directory
696 set_tmp_attachments_directory
697
697
698 # Delete all fixtured journals, a race condition can occur causing the wrong
698 # Delete all fixtured journals, a race condition can occur causing the wrong
699 # journal to get fetched in the next find.
699 # journal to get fetched in the next find.
700 Journal.delete_all
700 Journal.delete_all
701
701
702 # anonymous user
702 # anonymous user
703 post :edit,
703 post :edit,
704 :id => 1,
704 :id => 1,
705 :notes => '',
705 :notes => '',
706 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
706 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
707 assert_redirected_to :action => 'show', :id => '1'
707 assert_redirected_to :action => 'show', :id => '1'
708 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
708 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
709 assert j.notes.blank?
709 assert j.notes.blank?
710 assert_equal 1, j.details.size
710 assert_equal 1, j.details.size
711 assert_equal 'testfile.txt', j.details.first.value
711 assert_equal 'testfile.txt', j.details.first.value
712 assert_equal User.anonymous, j.user
712 assert_equal User.anonymous, j.user
713
713
714 mail = ActionMailer::Base.deliveries.last
714 mail = ActionMailer::Base.deliveries.last
715 assert mail.body.include?('testfile.txt')
715 assert mail.body.include?('testfile.txt')
716 end
716 end
717
717
718 def test_post_edit_with_no_change
718 def test_post_edit_with_no_change
719 issue = Issue.find(1)
719 issue = Issue.find(1)
720 issue.journals.clear
720 issue.journals.clear
721 ActionMailer::Base.deliveries.clear
721 ActionMailer::Base.deliveries.clear
722
722
723 post :edit,
723 post :edit,
724 :id => 1,
724 :id => 1,
725 :notes => ''
725 :notes => ''
726 assert_redirected_to :action => 'show', :id => '1'
726 assert_redirected_to :action => 'show', :id => '1'
727
727
728 issue.reload
728 issue.reload
729 assert issue.journals.empty?
729 assert issue.journals.empty?
730 # No email should be sent
730 # No email should be sent
731 assert ActionMailer::Base.deliveries.empty?
731 assert ActionMailer::Base.deliveries.empty?
732 end
732 end
733
733
734 def test_post_edit_with_invalid_spent_time
734 def test_post_edit_with_invalid_spent_time
735 @request.session[:user_id] = 2
735 @request.session[:user_id] = 2
736 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
736 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
737
737
738 assert_no_difference('Journal.count') do
738 assert_no_difference('Journal.count') do
739 post :edit,
739 post :edit,
740 :id => 1,
740 :id => 1,
741 :notes => notes,
741 :notes => notes,
742 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
742 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
743 end
743 end
744 assert_response :success
744 assert_response :success
745 assert_template 'edit'
745 assert_template 'edit'
746
746
747 assert_tag :textarea, :attributes => { :name => 'notes' },
747 assert_tag :textarea, :attributes => { :name => 'notes' },
748 :content => notes
748 :content => notes
749 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
749 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
750 end
750 end
751
751
752 def test_bulk_edit
752 def test_bulk_edit
753 @request.session[:user_id] = 2
753 @request.session[:user_id] = 2
754 # update issues priority
754 # update issues priority
755 post :bulk_edit, :ids => [1, 2], :priority_id => 7,
755 post :bulk_edit, :ids => [1, 2], :priority_id => 7,
756 :assigned_to_id => '',
756 :assigned_to_id => '',
757 :custom_field_values => {'2' => ''},
757 :custom_field_values => {'2' => ''},
758 :notes => 'Bulk editing'
758 :notes => 'Bulk editing'
759 assert_response 302
759 assert_response 302
760 # check that the issues were updated
760 # check that the issues were updated
761 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
761 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
762
762
763 issue = Issue.find(1)
763 issue = Issue.find(1)
764 journal = issue.journals.find(:first, :order => 'created_on DESC')
764 journal = issue.journals.find(:first, :order => 'created_on DESC')
765 assert_equal '125', issue.custom_value_for(2).value
765 assert_equal '125', issue.custom_value_for(2).value
766 assert_equal 'Bulk editing', journal.notes
766 assert_equal 'Bulk editing', journal.notes
767 assert_equal 1, journal.details.size
767 assert_equal 1, journal.details.size
768 end
768 end
769
769
770 def test_bulk_edit_custom_field
770 def test_bulk_edit_custom_field
771 @request.session[:user_id] = 2
771 @request.session[:user_id] = 2
772 # update issues priority
772 # update issues priority
773 post :bulk_edit, :ids => [1, 2], :priority_id => '',
773 post :bulk_edit, :ids => [1, 2], :priority_id => '',
774 :assigned_to_id => '',
774 :assigned_to_id => '',
775 :custom_field_values => {'2' => '777'},
775 :custom_field_values => {'2' => '777'},
776 :notes => 'Bulk editing custom field'
776 :notes => 'Bulk editing custom field'
777 assert_response 302
777 assert_response 302
778
778
779 issue = Issue.find(1)
779 issue = Issue.find(1)
780 journal = issue.journals.find(:first, :order => 'created_on DESC')
780 journal = issue.journals.find(:first, :order => 'created_on DESC')
781 assert_equal '777', issue.custom_value_for(2).value
781 assert_equal '777', issue.custom_value_for(2).value
782 assert_equal 1, journal.details.size
782 assert_equal 1, journal.details.size
783 assert_equal '125', journal.details.first.old_value
783 assert_equal '125', journal.details.first.old_value
784 assert_equal '777', journal.details.first.value
784 assert_equal '777', journal.details.first.value
785 end
785 end
786
786
787 def test_bulk_unassign
787 def test_bulk_unassign
788 assert_not_nil Issue.find(2).assigned_to
788 assert_not_nil Issue.find(2).assigned_to
789 @request.session[:user_id] = 2
789 @request.session[:user_id] = 2
790 # unassign issues
790 # unassign issues
791 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
791 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
792 assert_response 302
792 assert_response 302
793 # check that the issues were updated
793 # check that the issues were updated
794 assert_nil Issue.find(2).assigned_to
794 assert_nil Issue.find(2).assigned_to
795 end
795 end
796
796
797 def test_move_routing
797 def test_move_routing
798 assert_routing(
798 assert_routing(
799 {:method => :get, :path => '/issues/1/move'},
799 {:method => :get, :path => '/issues/1/move'},
800 :controller => 'issues', :action => 'move', :id => '1'
800 :controller => 'issues', :action => 'move', :id => '1'
801 )
801 )
802 assert_recognizes(
802 assert_recognizes(
803 {:controller => 'issues', :action => 'move', :id => '1'},
803 {:controller => 'issues', :action => 'move', :id => '1'},
804 {:method => :post, :path => '/issues/1/move'}
804 {:method => :post, :path => '/issues/1/move'}
805 )
805 )
806 end
806 end
807
807
808 def test_move_one_issue_to_another_project
808 def test_move_one_issue_to_another_project
809 @request.session[:user_id] = 1
809 @request.session[:user_id] = 1
810 post :move, :id => 1, :new_project_id => 2
810 post :move, :id => 1, :new_project_id => 2
811 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
811 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
812 assert_equal 2, Issue.find(1).project_id
812 assert_equal 2, Issue.find(1).project_id
813 end
813 end
814
814
815 def test_bulk_move_to_another_project
815 def test_bulk_move_to_another_project
816 @request.session[:user_id] = 1
816 @request.session[:user_id] = 1
817 post :move, :ids => [1, 2], :new_project_id => 2
817 post :move, :ids => [1, 2], :new_project_id => 2
818 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
818 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
819 # Issues moved to project 2
819 # Issues moved to project 2
820 assert_equal 2, Issue.find(1).project_id
820 assert_equal 2, Issue.find(1).project_id
821 assert_equal 2, Issue.find(2).project_id
821 assert_equal 2, Issue.find(2).project_id
822 # No tracker change
822 # No tracker change
823 assert_equal 1, Issue.find(1).tracker_id
823 assert_equal 1, Issue.find(1).tracker_id
824 assert_equal 2, Issue.find(2).tracker_id
824 assert_equal 2, Issue.find(2).tracker_id
825 end
825 end
826
826
827 def test_bulk_move_to_another_tracker
827 def test_bulk_move_to_another_tracker
828 @request.session[:user_id] = 1
828 @request.session[:user_id] = 1
829 post :move, :ids => [1, 2], :new_tracker_id => 2
829 post :move, :ids => [1, 2], :new_tracker_id => 2
830 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
830 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
831 assert_equal 2, Issue.find(1).tracker_id
831 assert_equal 2, Issue.find(1).tracker_id
832 assert_equal 2, Issue.find(2).tracker_id
832 assert_equal 2, Issue.find(2).tracker_id
833 end
833 end
834
834
835 def test_bulk_copy_to_another_project
835 def test_bulk_copy_to_another_project
836 @request.session[:user_id] = 1
836 @request.session[:user_id] = 1
837 assert_difference 'Issue.count', 2 do
837 assert_difference 'Issue.count', 2 do
838 assert_no_difference 'Project.find(1).issues.count' do
838 assert_no_difference 'Project.find(1).issues.count' do
839 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}
839 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}
840 end
840 end
841 end
841 end
842 assert_redirected_to 'projects/ecookbook/issues'
842 assert_redirected_to 'projects/ecookbook/issues'
843 end
843 end
844
844
845 def test_context_menu_one_issue
845 def test_context_menu_one_issue
846 @request.session[:user_id] = 2
846 @request.session[:user_id] = 2
847 get :context_menu, :ids => [1]
847 get :context_menu, :ids => [1]
848 assert_response :success
848 assert_response :success
849 assert_template 'context_menu'
849 assert_template 'context_menu'
850 assert_tag :tag => 'a', :content => 'Edit',
850 assert_tag :tag => 'a', :content => 'Edit',
851 :attributes => { :href => '/issues/1/edit',
851 :attributes => { :href => '/issues/1/edit',
852 :class => 'icon-edit' }
852 :class => 'icon-edit' }
853 assert_tag :tag => 'a', :content => 'Closed',
853 assert_tag :tag => 'a', :content => 'Closed',
854 :attributes => { :href => '/issues/1/edit?issue%5Bstatus_id%5D=5',
854 :attributes => { :href => '/issues/1/edit?issue%5Bstatus_id%5D=5',
855 :class => '' }
855 :class => '' }
856 assert_tag :tag => 'a', :content => 'Immediate',
856 assert_tag :tag => 'a', :content => 'Immediate',
857 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
857 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
858 :class => '' }
858 :class => '' }
859 assert_tag :tag => 'a', :content => 'Dave Lopper',
859 assert_tag :tag => 'a', :content => 'Dave Lopper',
860 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
860 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
861 :class => '' }
861 :class => '' }
862 assert_tag :tag => 'a', :content => 'Copy',
862 assert_tag :tag => 'a', :content => 'Copy',
863 :attributes => { :href => '/projects/ecookbook/issues/1/copy',
863 :attributes => { :href => '/projects/ecookbook/issues/1/copy',
864 :class => 'icon-copy' }
864 :class => 'icon-copy' }
865 assert_tag :tag => 'a', :content => 'Move',
865 assert_tag :tag => 'a', :content => 'Move',
866 :attributes => { :href => '/issues/move?ids%5B%5D=1',
866 :attributes => { :href => '/issues/move?ids%5B%5D=1',
867 :class => 'icon-move' }
867 :class => 'icon-move' }
868 assert_tag :tag => 'a', :content => 'Delete',
868 assert_tag :tag => 'a', :content => 'Delete',
869 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
869 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
870 :class => 'icon-del' }
870 :class => 'icon-del' }
871 end
871 end
872
872
873 def test_context_menu_one_issue_by_anonymous
873 def test_context_menu_one_issue_by_anonymous
874 get :context_menu, :ids => [1]
874 get :context_menu, :ids => [1]
875 assert_response :success
875 assert_response :success
876 assert_template 'context_menu'
876 assert_template 'context_menu'
877 assert_tag :tag => 'a', :content => 'Delete',
877 assert_tag :tag => 'a', :content => 'Delete',
878 :attributes => { :href => '#',
878 :attributes => { :href => '#',
879 :class => 'icon-del disabled' }
879 :class => 'icon-del disabled' }
880 end
880 end
881
881
882 def test_context_menu_multiple_issues_of_same_project
882 def test_context_menu_multiple_issues_of_same_project
883 @request.session[:user_id] = 2
883 @request.session[:user_id] = 2
884 get :context_menu, :ids => [1, 2]
884 get :context_menu, :ids => [1, 2]
885 assert_response :success
885 assert_response :success
886 assert_template 'context_menu'
886 assert_template 'context_menu'
887 assert_tag :tag => 'a', :content => 'Edit',
887 assert_tag :tag => 'a', :content => 'Edit',
888 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
888 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
889 :class => 'icon-edit' }
889 :class => 'icon-edit' }
890 assert_tag :tag => 'a', :content => 'Immediate',
890 assert_tag :tag => 'a', :content => 'Immediate',
891 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
891 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
892 :class => '' }
892 :class => '' }
893 assert_tag :tag => 'a', :content => 'Dave Lopper',
893 assert_tag :tag => 'a', :content => 'Dave Lopper',
894 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
894 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
895 :class => '' }
895 :class => '' }
896 assert_tag :tag => 'a', :content => 'Move',
896 assert_tag :tag => 'a', :content => 'Move',
897 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
897 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
898 :class => 'icon-move' }
898 :class => 'icon-move' }
899 assert_tag :tag => 'a', :content => 'Delete',
899 assert_tag :tag => 'a', :content => 'Delete',
900 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
900 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
901 :class => 'icon-del' }
901 :class => 'icon-del' }
902 end
902 end
903
903
904 def test_context_menu_multiple_issues_of_different_project
904 def test_context_menu_multiple_issues_of_different_project
905 @request.session[:user_id] = 2
905 @request.session[:user_id] = 2
906 get :context_menu, :ids => [1, 2, 4]
906 get :context_menu, :ids => [1, 2, 4]
907 assert_response :success
907 assert_response :success
908 assert_template 'context_menu'
908 assert_template 'context_menu'
909 assert_tag :tag => 'a', :content => 'Delete',
909 assert_tag :tag => 'a', :content => 'Delete',
910 :attributes => { :href => '#',
910 :attributes => { :href => '#',
911 :class => 'icon-del disabled' }
911 :class => 'icon-del disabled' }
912 end
912 end
913
913
914 def test_destroy_routing
914 def test_destroy_routing
915 assert_recognizes( #TODO: use DELETE on issue URI (need to change forms)
915 assert_recognizes( #TODO: use DELETE on issue URI (need to change forms)
916 {:controller => 'issues', :action => 'destroy', :id => '1'},
916 {:controller => 'issues', :action => 'destroy', :id => '1'},
917 {:method => :post, :path => '/issues/1/destroy'}
917 {:method => :post, :path => '/issues/1/destroy'}
918 )
918 )
919 end
919 end
920
920
921 def test_destroy_issue_with_no_time_entries
921 def test_destroy_issue_with_no_time_entries
922 assert_nil TimeEntry.find_by_issue_id(2)
922 assert_nil TimeEntry.find_by_issue_id(2)
923 @request.session[:user_id] = 2
923 @request.session[:user_id] = 2
924 post :destroy, :id => 2
924 post :destroy, :id => 2
925 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
925 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
926 assert_nil Issue.find_by_id(2)
926 assert_nil Issue.find_by_id(2)
927 end
927 end
928
928
929 def test_destroy_issues_with_time_entries
929 def test_destroy_issues_with_time_entries
930 @request.session[:user_id] = 2
930 @request.session[:user_id] = 2
931 post :destroy, :ids => [1, 3]
931 post :destroy, :ids => [1, 3]
932 assert_response :success
932 assert_response :success
933 assert_template 'destroy'
933 assert_template 'destroy'
934 assert_not_nil assigns(:hours)
934 assert_not_nil assigns(:hours)
935 assert Issue.find_by_id(1) && Issue.find_by_id(3)
935 assert Issue.find_by_id(1) && Issue.find_by_id(3)
936 end
936 end
937
937
938 def test_destroy_issues_and_destroy_time_entries
938 def test_destroy_issues_and_destroy_time_entries
939 @request.session[:user_id] = 2
939 @request.session[:user_id] = 2
940 post :destroy, :ids => [1, 3], :todo => 'destroy'
940 post :destroy, :ids => [1, 3], :todo => 'destroy'
941 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
941 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
942 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
942 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
943 assert_nil TimeEntry.find_by_id([1, 2])
943 assert_nil TimeEntry.find_by_id([1, 2])
944 end
944 end
945
945
946 def test_destroy_issues_and_assign_time_entries_to_project
946 def test_destroy_issues_and_assign_time_entries_to_project
947 @request.session[:user_id] = 2
947 @request.session[:user_id] = 2
948 post :destroy, :ids => [1, 3], :todo => 'nullify'
948 post :destroy, :ids => [1, 3], :todo => 'nullify'
949 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
949 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
950 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
950 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
951 assert_nil TimeEntry.find(1).issue_id
951 assert_nil TimeEntry.find(1).issue_id
952 assert_nil TimeEntry.find(2).issue_id
952 assert_nil TimeEntry.find(2).issue_id
953 end
953 end
954
954
955 def test_destroy_issues_and_reassign_time_entries_to_another_issue
955 def test_destroy_issues_and_reassign_time_entries_to_another_issue
956 @request.session[:user_id] = 2
956 @request.session[:user_id] = 2
957 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
957 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
958 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
958 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
959 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
959 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
960 assert_equal 2, TimeEntry.find(1).issue_id
960 assert_equal 2, TimeEntry.find(1).issue_id
961 assert_equal 2, TimeEntry.find(2).issue_id
961 assert_equal 2, TimeEntry.find(2).issue_id
962 end
962 end
963 end
963 end
@@ -1,82 +1,82
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class EnumerationTest < Test::Unit::TestCase
20 class EnumerationTest < Test::Unit::TestCase
21 fixtures :enumerations, :issues
21 fixtures :enumerations, :issues
22
22
23 def setup
23 def setup
24 end
24 end
25
25
26 def test_objects_count
26 def test_objects_count
27 # low priority
27 # low priority
28 assert_equal 5, Enumeration.find(4).objects_count
28 assert_equal 5, Enumeration.find(4).objects_count
29 # urgent
29 # urgent
30 assert_equal 0, Enumeration.find(7).objects_count
30 assert_equal 0, Enumeration.find(7).objects_count
31 end
31 end
32
32
33 def test_in_use
33 def test_in_use
34 # low priority
34 # low priority
35 assert Enumeration.find(4).in_use?
35 assert Enumeration.find(4).in_use?
36 # urgent
36 # urgent
37 assert !Enumeration.find(7).in_use?
37 assert !Enumeration.find(7).in_use?
38 end
38 end
39
39
40 def test_default
40 def test_default
41 e = Enumeration.default('IPRI')
41 e = Enumeration.priorities.default
42 assert e.is_a?(Enumeration)
42 assert e.is_a?(Enumeration)
43 assert e.is_default?
43 assert e.is_default?
44 assert_equal 'Normal', e.name
44 assert_equal 'Normal', e.name
45 end
45 end
46
46
47 def test_create
47 def test_create
48 e = Enumeration.new(:opt => 'IPRI', :name => 'Very urgent', :is_default => false)
48 e = Enumeration.new(:opt => 'IPRI', :name => 'Very urgent', :is_default => false)
49 assert e.save
49 assert e.save
50 assert_equal 'Normal', Enumeration.default('IPRI').name
50 assert_equal 'Normal', Enumeration.priorities.default.name
51 end
51 end
52
52
53 def test_create_as_default
53 def test_create_as_default
54 e = Enumeration.new(:opt => 'IPRI', :name => 'Very urgent', :is_default => true)
54 e = Enumeration.new(:opt => 'IPRI', :name => 'Very urgent', :is_default => true)
55 assert e.save
55 assert e.save
56 assert_equal e, Enumeration.default('IPRI')
56 assert_equal e, Enumeration.priorities.default
57 end
57 end
58
58
59 def test_update_default
59 def test_update_default
60 e = Enumeration.default('IPRI')
60 e = Enumeration.priorities.default
61 e.update_attributes(:name => 'Changed', :is_default => true)
61 e.update_attributes(:name => 'Changed', :is_default => true)
62 assert_equal e, Enumeration.default('IPRI')
62 assert_equal e, Enumeration.priorities.default
63 end
63 end
64
64
65 def test_update_default_to_non_default
65 def test_update_default_to_non_default
66 e = Enumeration.default('IPRI')
66 e = Enumeration.priorities.default
67 e.update_attributes(:name => 'Changed', :is_default => false)
67 e.update_attributes(:name => 'Changed', :is_default => false)
68 assert_nil Enumeration.default('IPRI')
68 assert_nil Enumeration.priorities.default
69 end
69 end
70
70
71 def test_change_default
71 def test_change_default
72 e = Enumeration.find_by_name('Urgent')
72 e = Enumeration.find_by_name('Urgent')
73 e.update_attributes(:name => 'Urgent', :is_default => true)
73 e.update_attributes(:name => 'Urgent', :is_default => true)
74 assert_equal e, Enumeration.default('IPRI')
74 assert_equal e, Enumeration.priorities.default
75 end
75 end
76
76
77 def test_destroy_with_reassign
77 def test_destroy_with_reassign
78 Enumeration.find(4).destroy(Enumeration.find(6))
78 Enumeration.find(4).destroy(Enumeration.find(6))
79 assert_nil Issue.find(:first, :conditions => {:priority_id => 4})
79 assert_nil Issue.find(:first, :conditions => {:priority_id => 4})
80 assert_equal 5, Enumeration.find(6).objects_count
80 assert_equal 5, Enumeration.find(6).objects_count
81 end
81 end
82 end
82 end
@@ -1,231 +1,231
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class IssueTest < Test::Unit::TestCase
20 class IssueTest < Test::Unit::TestCase
21 fixtures :projects, :users, :members,
21 fixtures :projects, :users, :members,
22 :trackers, :projects_trackers,
22 :trackers, :projects_trackers,
23 :issue_statuses, :issue_categories,
23 :issue_statuses, :issue_categories,
24 :enumerations,
24 :enumerations,
25 :issues,
25 :issues,
26 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
26 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
27 :time_entries
27 :time_entries
28
28
29 def test_create
29 def test_create
30 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
30 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
31 assert issue.save
31 assert issue.save
32 issue.reload
32 issue.reload
33 assert_equal 1.5, issue.estimated_hours
33 assert_equal 1.5, issue.estimated_hours
34 end
34 end
35
35
36 def test_create_minimal
36 def test_create_minimal
37 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'test_create')
37 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'test_create')
38 assert issue.save
38 assert issue.save
39 assert issue.description.nil?
39 assert issue.description.nil?
40 end
40 end
41
41
42 def test_create_with_required_custom_field
42 def test_create_with_required_custom_field
43 field = IssueCustomField.find_by_name('Database')
43 field = IssueCustomField.find_by_name('Database')
44 field.update_attribute(:is_required, true)
44 field.update_attribute(:is_required, true)
45
45
46 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
46 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
47 assert issue.available_custom_fields.include?(field)
47 assert issue.available_custom_fields.include?(field)
48 # No value for the custom field
48 # No value for the custom field
49 assert !issue.save
49 assert !issue.save
50 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
50 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
51 # Blank value
51 # Blank value
52 issue.custom_field_values = { field.id => '' }
52 issue.custom_field_values = { field.id => '' }
53 assert !issue.save
53 assert !issue.save
54 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
54 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
55 # Invalid value
55 # Invalid value
56 issue.custom_field_values = { field.id => 'SQLServer' }
56 issue.custom_field_values = { field.id => 'SQLServer' }
57 assert !issue.save
57 assert !issue.save
58 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
58 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
59 # Valid value
59 # Valid value
60 issue.custom_field_values = { field.id => 'PostgreSQL' }
60 issue.custom_field_values = { field.id => 'PostgreSQL' }
61 assert issue.save
61 assert issue.save
62 issue.reload
62 issue.reload
63 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
63 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
64 end
64 end
65
65
66 def test_update_issue_with_required_custom_field
66 def test_update_issue_with_required_custom_field
67 field = IssueCustomField.find_by_name('Database')
67 field = IssueCustomField.find_by_name('Database')
68 field.update_attribute(:is_required, true)
68 field.update_attribute(:is_required, true)
69
69
70 issue = Issue.find(1)
70 issue = Issue.find(1)
71 assert_nil issue.custom_value_for(field)
71 assert_nil issue.custom_value_for(field)
72 assert issue.available_custom_fields.include?(field)
72 assert issue.available_custom_fields.include?(field)
73 # No change to custom values, issue can be saved
73 # No change to custom values, issue can be saved
74 assert issue.save
74 assert issue.save
75 # Blank value
75 # Blank value
76 issue.custom_field_values = { field.id => '' }
76 issue.custom_field_values = { field.id => '' }
77 assert !issue.save
77 assert !issue.save
78 # Valid value
78 # Valid value
79 issue.custom_field_values = { field.id => 'PostgreSQL' }
79 issue.custom_field_values = { field.id => 'PostgreSQL' }
80 assert issue.save
80 assert issue.save
81 issue.reload
81 issue.reload
82 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
82 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
83 end
83 end
84
84
85 def test_should_not_update_attributes_if_custom_fields_validation_fails
85 def test_should_not_update_attributes_if_custom_fields_validation_fails
86 issue = Issue.find(1)
86 issue = Issue.find(1)
87 field = IssueCustomField.find_by_name('Database')
87 field = IssueCustomField.find_by_name('Database')
88 assert issue.available_custom_fields.include?(field)
88 assert issue.available_custom_fields.include?(field)
89
89
90 issue.custom_field_values = { field.id => 'Invalid' }
90 issue.custom_field_values = { field.id => 'Invalid' }
91 issue.subject = 'Should be not be saved'
91 issue.subject = 'Should be not be saved'
92 assert !issue.save
92 assert !issue.save
93
93
94 issue.reload
94 issue.reload
95 assert_equal "Can't print recipes", issue.subject
95 assert_equal "Can't print recipes", issue.subject
96 end
96 end
97
97
98 def test_should_not_recreate_custom_values_objects_on_update
98 def test_should_not_recreate_custom_values_objects_on_update
99 field = IssueCustomField.find_by_name('Database')
99 field = IssueCustomField.find_by_name('Database')
100
100
101 issue = Issue.find(1)
101 issue = Issue.find(1)
102 issue.custom_field_values = { field.id => 'PostgreSQL' }
102 issue.custom_field_values = { field.id => 'PostgreSQL' }
103 assert issue.save
103 assert issue.save
104 custom_value = issue.custom_value_for(field)
104 custom_value = issue.custom_value_for(field)
105 issue.reload
105 issue.reload
106 issue.custom_field_values = { field.id => 'MySQL' }
106 issue.custom_field_values = { field.id => 'MySQL' }
107 assert issue.save
107 assert issue.save
108 issue.reload
108 issue.reload
109 assert_equal custom_value.id, issue.custom_value_for(field).id
109 assert_equal custom_value.id, issue.custom_value_for(field).id
110 end
110 end
111
111
112 def test_category_based_assignment
112 def test_category_based_assignment
113 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
113 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
114 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
114 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
115 end
115 end
116
116
117 def test_copy
117 def test_copy
118 issue = Issue.new.copy_from(1)
118 issue = Issue.new.copy_from(1)
119 assert issue.save
119 assert issue.save
120 issue.reload
120 issue.reload
121 orig = Issue.find(1)
121 orig = Issue.find(1)
122 assert_equal orig.subject, issue.subject
122 assert_equal orig.subject, issue.subject
123 assert_equal orig.tracker, issue.tracker
123 assert_equal orig.tracker, issue.tracker
124 assert_equal orig.custom_values.first.value, issue.custom_values.first.value
124 assert_equal orig.custom_values.first.value, issue.custom_values.first.value
125 end
125 end
126
126
127 def test_should_close_duplicates
127 def test_should_close_duplicates
128 # Create 3 issues
128 # Create 3 issues
129 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Duplicates test', :description => 'Duplicates test')
129 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'Duplicates test', :description => 'Duplicates test')
130 assert issue1.save
130 assert issue1.save
131 issue2 = issue1.clone
131 issue2 = issue1.clone
132 assert issue2.save
132 assert issue2.save
133 issue3 = issue1.clone
133 issue3 = issue1.clone
134 assert issue3.save
134 assert issue3.save
135
135
136 # 2 is a dupe of 1
136 # 2 is a dupe of 1
137 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
137 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
138 # And 3 is a dupe of 2
138 # And 3 is a dupe of 2
139 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
139 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
140 # And 3 is a dupe of 1 (circular duplicates)
140 # And 3 is a dupe of 1 (circular duplicates)
141 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
141 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
142
142
143 assert issue1.reload.duplicates.include?(issue2)
143 assert issue1.reload.duplicates.include?(issue2)
144
144
145 # Closing issue 1
145 # Closing issue 1
146 issue1.init_journal(User.find(:first), "Closing issue1")
146 issue1.init_journal(User.find(:first), "Closing issue1")
147 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
147 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
148 assert issue1.save
148 assert issue1.save
149 # 2 and 3 should be also closed
149 # 2 and 3 should be also closed
150 assert issue2.reload.closed?
150 assert issue2.reload.closed?
151 assert issue3.reload.closed?
151 assert issue3.reload.closed?
152 end
152 end
153
153
154 def test_should_not_close_duplicated_issue
154 def test_should_not_close_duplicated_issue
155 # Create 3 issues
155 # Create 3 issues
156 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Duplicates test', :description => 'Duplicates test')
156 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'Duplicates test', :description => 'Duplicates test')
157 assert issue1.save
157 assert issue1.save
158 issue2 = issue1.clone
158 issue2 = issue1.clone
159 assert issue2.save
159 assert issue2.save
160
160
161 # 2 is a dupe of 1
161 # 2 is a dupe of 1
162 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
162 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
163 # 2 is a dup of 1 but 1 is not a duplicate of 2
163 # 2 is a dup of 1 but 1 is not a duplicate of 2
164 assert !issue2.reload.duplicates.include?(issue1)
164 assert !issue2.reload.duplicates.include?(issue1)
165
165
166 # Closing issue 2
166 # Closing issue 2
167 issue2.init_journal(User.find(:first), "Closing issue2")
167 issue2.init_journal(User.find(:first), "Closing issue2")
168 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
168 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
169 assert issue2.save
169 assert issue2.save
170 # 1 should not be also closed
170 # 1 should not be also closed
171 assert !issue1.reload.closed?
171 assert !issue1.reload.closed?
172 end
172 end
173
173
174 def test_move_to_another_project_with_same_category
174 def test_move_to_another_project_with_same_category
175 issue = Issue.find(1)
175 issue = Issue.find(1)
176 assert issue.move_to(Project.find(2))
176 assert issue.move_to(Project.find(2))
177 issue.reload
177 issue.reload
178 assert_equal 2, issue.project_id
178 assert_equal 2, issue.project_id
179 # Category changes
179 # Category changes
180 assert_equal 4, issue.category_id
180 assert_equal 4, issue.category_id
181 # Make sure time entries were move to the target project
181 # Make sure time entries were move to the target project
182 assert_equal 2, issue.time_entries.first.project_id
182 assert_equal 2, issue.time_entries.first.project_id
183 end
183 end
184
184
185 def test_move_to_another_project_without_same_category
185 def test_move_to_another_project_without_same_category
186 issue = Issue.find(2)
186 issue = Issue.find(2)
187 assert issue.move_to(Project.find(2))
187 assert issue.move_to(Project.find(2))
188 issue.reload
188 issue.reload
189 assert_equal 2, issue.project_id
189 assert_equal 2, issue.project_id
190 # Category cleared
190 # Category cleared
191 assert_nil issue.category_id
191 assert_nil issue.category_id
192 end
192 end
193
193
194 def test_copy_to_the_same_project
194 def test_copy_to_the_same_project
195 issue = Issue.find(1)
195 issue = Issue.find(1)
196 copy = nil
196 copy = nil
197 assert_difference 'Issue.count' do
197 assert_difference 'Issue.count' do
198 copy = issue.move_to(issue.project, nil, :copy => true)
198 copy = issue.move_to(issue.project, nil, :copy => true)
199 end
199 end
200 assert_kind_of Issue, copy
200 assert_kind_of Issue, copy
201 assert_equal issue.project, copy.project
201 assert_equal issue.project, copy.project
202 assert_equal "125", copy.custom_value_for(2).value
202 assert_equal "125", copy.custom_value_for(2).value
203 end
203 end
204
204
205 def test_copy_to_another_project_and_tracker
205 def test_copy_to_another_project_and_tracker
206 issue = Issue.find(1)
206 issue = Issue.find(1)
207 copy = nil
207 copy = nil
208 assert_difference 'Issue.count' do
208 assert_difference 'Issue.count' do
209 copy = issue.move_to(Project.find(3), Tracker.find(2), :copy => true)
209 copy = issue.move_to(Project.find(3), Tracker.find(2), :copy => true)
210 end
210 end
211 assert_kind_of Issue, copy
211 assert_kind_of Issue, copy
212 assert_equal Project.find(3), copy.project
212 assert_equal Project.find(3), copy.project
213 assert_equal Tracker.find(2), copy.tracker
213 assert_equal Tracker.find(2), copy.tracker
214 # Custom field #2 is not associated with target tracker
214 # Custom field #2 is not associated with target tracker
215 assert_nil copy.custom_value_for(2)
215 assert_nil copy.custom_value_for(2)
216 end
216 end
217
217
218 def test_issue_destroy
218 def test_issue_destroy
219 Issue.find(1).destroy
219 Issue.find(1).destroy
220 assert_nil Issue.find_by_id(1)
220 assert_nil Issue.find_by_id(1)
221 assert_nil TimeEntry.find_by_issue_id(1)
221 assert_nil TimeEntry.find_by_issue_id(1)
222 end
222 end
223
223
224 def test_overdue
224 def test_overdue
225 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
225 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
226 assert !Issue.new(:due_date => Date.today).overdue?
226 assert !Issue.new(:due_date => Date.today).overdue?
227 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
227 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
228 assert !Issue.new(:due_date => nil).overdue?
228 assert !Issue.new(:due_date => nil).overdue?
229 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
229 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
230 end
230 end
231 end
231 end
General Comments 0
You need to be logged in to leave comments. Login now